Friday, 11 July 2014

FreeCAD + Arduino

Modifying shapes by reading sensors can create a lot of possibilities, for example, this video:



An Arduino DUE with a varible resistor commands a servo inside FreeCAD.


This post is about making what is shown in the video.

What you need:


-Linux

-Any Arduino board with serial connection

-Arduino ide

-FreeCAD

- My servo model

-Pyserial library ( sudo apt-get install python-serial )

-A look at the Oficial arduino and python guide ( not really needed, but my main source )

The idea:

-Print the value of the sensor by serial
-Create a function that reads the Arduino serial
-Create another function that, with the value of the serial, updates the object in FreeCAD
-Call them repetitively by a timer

The Arduino part:

At the video I'm using an Arduino DUE because it was handy, but an UNO board is valid too.

The electrical thing consists of connecting a variable resistor to the A0 pin, like the scheme:


Any variable resistor above 1kΩ will do the job, in the video I use a 4.7kΩ one. 

The code is quite simple:
void setup()
{
  Serial.begin(9600);                  /// Start serial at 9600 baud
  pinMode( A0, INPUT );                /// Set pin A0 as input
}

void loop()
{
  Serial.println( analogRead( A0 ) );  /// Print to serial A0 value
}

Initialize serial and then constantly print the sensor value.

The Python code:

This are the needed libraries:

import serial
from PySide import QtCore

Serial for reading the serial (obvious one) and PySide for the timer object.

To initialize the serial at 9600 baud pointed to the Arduino:

ser = serial.Serial('/dev/ttyACM0', 9600)

The Arduino serial should be in '/dev/ttyACM0' but sometimes it switches to '/dev/ttyACM1',
if you fire up the Arduino Ide and open the serial window, the path is at the window title.

The name "ser" now contains the serial, which we can read this way:

ser.readline()

That prints whatever the serial is saying.

The FreeCAD part:

We need a function that changes the position of the servo arm to the input value. From the arduino, we receive a number from 0 to 1024, a servo rotates ~180 degrees, so a conversion is needed.

def SERVO(valor):
  angle = valor*-180.0/1024.0
  Position = FreeCAD.Placement(App.Vector(12.5,12,53),App.Vector(0,0,1),angle) 
  FreeCAD.ActiveDocument.Fillet004.Placement = Position

The first line of the function SERVO is the 1024 to 180º conversion, the second and third ones do the position change of the FreeCAD object. Note that the servo arm is called "Fillet004".

You can test this function by giving values to it, like SERVO(200) or SERVO(1000), it should move.

The link:

The function SERIAL links  everything together:

servalue0 = 0 # avoid problems with serial initialization

def SERIAL():
  global servalue0
  hysteresis = 6.0 # to smooth the movement
  try:
    servalue = int(ser.readline()) # to int the serial value
  except:
    servalue = servalue0
  if servalue > servalue0 + hysteresis or servalue < servalue0 - hysteresis:
    SERVO(servalue) # update the servo position
    servalue0 = servalue # keep last value to check hysteresis

The timer:

To give it life we need to call SERIAL function repetitively with a timer:

timer = QtCore.QTimer()
timer.timeout.connect(SERIAL)
timer.start(1)


First line creates timer object, second one connects its signals to the SERIAL function and the third one makes it emit a signal every 1 ms.


Complete Python script:

If you just want to test it, create the Arduino circuit, download the servo model and copy paste this at FreeCAD console:

"""
Javier Martinez Garcia, 2014 
"""
import serial
from PySide import QtCore

try:
  ser = serial.Serial('/dev/ttyACM0', 9600)
  
except:
  ser = serial.Serial('/dev/ttyACM1', 9600)

def SERVO(valor):
  angle = valor*-180.0/1024.0
  Position = FreeCAD.Placement(App.Vector(12.5,12,53),App.Vector(0,0,1),angle) 
  FreeCAD.ActiveDocument.Fillet004.Placement = Position 

servalue0 = 0 # avoid problems with serial initialization

def SERIAL():
  global servalue0
  hysteresis = 6.0 # to smooth the movement
  try:
    servalue = int(ser.readline()) # to int the serial value
  except:
    servalue = servalue0
  if servalue > servalue0 + hysteresis or servalue < servalue0 - hysteresis:
    SERVO(servalue) # update the servo position
    servalue0 = servalue # keep last value to check hysteresis

timer = QtCore.QTimer()
timer.timeout.connect(SERIAL)
timer.start(1)



And this is all you need to repeat my video.
There are things that can be improved, for example the timer. Maybe using the threading library can give the same result.

About the possibilities, the communication can be bi-directional too, here I show realworld->FreeCAD, but the opposite is perfectly possible. And the FreeCAD objects attributes that can be changed do not need to be exclusively placement, but color or even parametric models.

Just imagine.

Bye!

Friday, 4 July 2014

FreeCAD: Animated Spring

Not really a spring, but..

... this script creates a helix object and constantly adjust its pitch and height to create a compression-like effect.
As always, the function that calculates and commands the changes is called repetitively by a timer.

Video:


While the animation is running, you can change the values of pitch, length and compression by just typing in, for example:

Pitch = 3
Compression = 0.1   # Relative to the length of the "spring" 


The code:

from __future__ import division 

from PyQt4 import QtCore
import math as mt
import FreeCADGui
App.ActiveDocument.addObject("Part::Helix","Helix")
App.ActiveDocument.Helix.Pitch=5.00
App.ActiveDocument.Helix.Height=20.00
App.ActiveDocument.Helix.Radius=5.00
App.ActiveDocument.Helix.Angle=0.00
App.ActiveDocument.Helix.LocalCoord=0
App.ActiveDocument.Helix.Style=1
App.ActiveDocument.Helix.Label='Helix'
FreeCADGui.ActiveDocument.getObject("Helix").LineColor = (1.00,0.67,0.00)

i = 0
Length = 20
Pitch = 5
Compression = 0.5

def Spring():
  global i, Length, Pitch, Compression
  
  i+=0.01

  R = Pitch / Length
  IH = Compression*Length*mt.cos(i)
  P = Pitch + (R*IH)
  App.ActiveDocument.Helix.Height = Length + IH
  App.ActiveDocument.Helix.Pitch = P
  
  if i == 1000:
    i=0


timer = QtCore.QTimer()
timer.timeout.connect(Spring)
timer.start(5)



Bye!
 :D

Monday, 23 June 2014

FreeCAD: Pseudo-mill simulator

Before starting, I must warn that this is not intended to produce any realistic simulation.
I created it as a proof of concept and for fun.


How it works:

The milling script removes material by cutting the workpiece with the tool object (a rectangular box) in a simple Part.cut(tool) operation.
The tool object position is determined by taking the last point and the next point from the path points list. Then, the script creates vector and walks through it by steps. At every step, the tool cuts the workpiece, and every 4 steps, the document is refreshed. Refreshed means the command Part.show(workpiece). Because this command creates a shape every time is called, before Part.show(workpiece), the previous shape is removed.
Wild and dirty.

Below I try to explain how to use this script. You can get the full code here


Creating toolpath:

The first thing we need is the list of points that the "tool" will follow.
This list has the form:

points = ( ( x,y,z ), ( x1,y1,z1 ), ... , ( xn, yn, zn), ( x,y,z ))

But manually creating the points is not the most adequate way, at least for more-less complex or large toolpaths.

If we want to do something like this...



...it could take forever manually, so I wrote a pocket function instead:
from FreeCAD import Gui
from PyQt4 import QtCore
from FreeCAD import Base, Draft, Part
import math as mt

# Basical definitions
raw_size=(30,30,10)

Tool_radius = 0.5 
Tool_heigh = 8.0 
feed_rate = 10.0 

# Path Generator

Program=[(-5,0,30)] # Start position

def pocket(V0,L,H):
  global Program, Tool_radius
  YCycles = int(mt.floor((L / (2*Tool_radius))/2)) 
  V0 = (V0[0] + Tool_radius, V0[1] + Tool_radius, Program[0][2])
  Program.append(V0)
  V0 = (V0[0], V0[1], H)
  Program.append(V0)
  for i in range(YCycles):
    V0 = (V0[0]+(L-2*Tool_radius), V0[1], H)
    Program.append(V0)
    V0 = (V0[0],(2*Tool_radius)+V0[1], H)
    Program.append(V0)
    V0 = (V0[0]-(L-2*Tool_radius),V0[1], H)
    Program.append(V0)
    V0 = (V0[0],2*Tool_radius+V0[1], H)
    Program.append(V0)
  V0 = (V0[0]+(L-2*Tool_radius), V0[1], H)
  Program.append(V0)
  V0 = (V0[0], V0[1], Program[0][2])
  Program.append(V0)

It generates a pocket with initial position (lower-right corner) V0(x,y,z), side length L and absolute deepness H.

An screenshot of pocket((2,2,15),25,8)


Imagine to create that zigzag pattern by hand. 

The function pocket itself does not represent the points, just creates the list. To create the wire that represents the toolpath, I have coded this:

Wire_done = False  # wire end condition
i=0
while Wire_done == False:
  i+=1
  if i == len(Program):
    Wire_done = True
    break
    
  if i == 1: # Starts the wire by creating the first line
    Line0 = Part.makeLine(Program[i-1], Program[i])
    Wire0 = Part.Wire([Line0])
  else: #Creates the rest of the wire
    Line1 = Part.makeLine(Program[i-1],Program[i])
    Trajectory_Wire = Part.Wire([Wire0,Line1])
    Wire0 = Trajectory_Wire

TjWire = App.ActiveDocument.addObject("Part::Feature", "Trajectory") 
TjWire.Shape = Trajectory_Wire  #
TjWire_UserName = TjWire.Label
FreeCADGui.ActiveDocument.getObject(TjWire_UserName).LineColor = (1.00,0.67,0.00)

It iterates over the points list (named Program) and generates the tool trajectory wire.

You can generate multiple pockets, this is the result of...



pocket((4,4,10),20,6)
pocket((8,8,10),10,3)
pocket((8,20,5),5,3)
pocket((15,20,5),5,3)
pocket((21,20,5),3,3)
pocket((10,10,5),5,2)



Create workpiece:

Raw = Part.makeBox(raw_size[0],raw_size[1],raw_size[2])
Raw_shape = App.ActiveDocument.addObject("Part::Feature", "Workpiece")
Raw_shape.Shape = Raw
Gui.ActiveDocument.getObject("Workpiece").Visibility=False

Create tool (the one that performs the cut):

Tool = Part.makeBox(Tool_radius*2, Tool_radius*2, Tool_heigh)
Tool_shape = App.ActiveDocument.addObject("Part::Feature", "Tool")
Tool_shape.Shape = Tool
Tool_shape_gui = Tool_shape.Label
FreeCADGui.ActiveDocument.getObject(Tool_shape_gui).ShapeColor = (0.33,0.33,1.00)
 
Tool_shape.Placement = App.Placement(App.Vector(Program[0]),ToolR )
Gui.ActiveDocument.getObject("Tool").Visibility=False

Create the animated tool:

AnimatedTool_shape= Part.makeCylinder(Tool_radius,Tool_heigh)
RTTH = Part.makeBox(Tool_radius/3.0, Tool_radius*2, Tool_heigh*1.1)

for i in range(5):
  alpha = i*360/5
  RTTH = Part.makeBox(Tool_radius/3.0, Tool_radius*2, Tool_heigh*1.1)
  RTTH.translate(Base.Vector((2*Tool_radius*mt.cos(mt.radians(i))/3.0,2*Tool_radius*mt.sin(mt.radians(i))/3.0,0)))
  RTTH.rotate(Base.Vector(0,0,0),Base.Vector(0,0,1), 15 + alpha)
  AnimatedTool_shape = AnimatedTool_shape.cut(RTTH)


AnimatedTool= App.ActiveDocument.addObject("Part::Feature","AnimatedTool")
AnimatedTool.Shape = AnimatedTool_shape
AnimatedTool_gui = AnimatedTool.Label
FreeCADGui.ActiveDocument.getObject(AnimatedTool_gui).ShapeColor = (0.33,0.33,1.00)
AnimatedTool.Placement = App.Placement(App.Vector(Program[0]), App.Rotation(App.Vector(0,0,1),0))

Pseudo-Mill core:


This is the function that performs the milling-like action:

L1 = Raw.cut(Tool_shape.Shape)
Part.show(L1)

i=0
n=0.0
swd = 3 # cut refreshing interval
s=0


def Machining():
  global n, i,feed_rate, L1, Tool_radius, swd, s
  if i <= len(Program):
    Current_position = Tool_shape.Placement.Base
    Vector_trajectory = App.Vector(Program[i+1])-App.Vector(Program[i])
    Vector_direction = (App.Vector(Program[i+1])-App.Vector(Program[i])).normalize()
    VT_modulus = Vector_trajectory.Length
    VD_modulus = 1.0
    if VT_modulus > VD_modulus*n*feed_rate:
      Next_position = App.Vector(Program[i])+Vector_direction.multiply(n*feed_rate)+App.Vector(-Tool_radius, -Tool_radius)
      n+=0.1
      
    else:
      Next_Position = App.Vector(Program[i+1])
      i+=1
      n=0.0
    
    s += 1
    if s > swd:
      App.ActiveDocument.removeObject("Shape")
      L1 = L1.removeSplitter()
      Part.show(L1)
      s = 0
    Tool_shape.Placement = App.Placement(Next_position, ToolR)
    AnimatedTool.Placement = App.Placement(Next_position+App.Vector(Tool_radius,Tool_radius,0), App.Rotation(App.Vector(0,0,1),n*43))
    L1 = L1.cut(Tool_shape.Shape)
Then, by calling repeatedly Machining() with a timer starts the animation:

timer=QtCore.QTimer()
timer.timeout.connect(Machining)
timer.start(1)
With the tool-path created above this is the result:



What's next?

Well, I've been playing with creating the points list using a script that follows the contour of a FreeCAD part. That way you will only need to execute that script and then run the simulation.


The green line is the output of the script.



Bye!