Building the machine


Overview

Last updated: 12/04/2016

Objectives
1.Build a 2 axis machine

UPDATES


Summary
For the assignment, we have decided to build a physical display
Through a python interface, is possible to interact with the spikes of the display which are pushed from behind by a solenoid attached to the axis.
For the movement, we have decided to use the same mechanics of the Ultimaker.






3D & mechanical design

The first step for the machine building was to 3D design and assembly the model.
We have used Rhinoceros for the job.
We decided to build a wooden frame to attach the movement system which is composed by two cross axis and a central holder for the solenoid.
We have cutted the frame with the laser cutter





After we had renders of the machine, we've started to design the other mechanical parts.
First of all, we have designed the pulleys.
We knew that we had to use a gt2 belt so we designed the theet of the pulleys to fit that dimension.
All the parts of the machine (except for the ball bearings of the frame) are 3D printed.





The following is the central holder for the solenoid.
This piece holds the solenoid and it is tied directly to the axis.





After the frame and the motors where putted in place, we have started to think about a way to design the spikes.
This is one of the first tries we did with 3d printers.



By watching the ultimaker movement system, we have designed the lateral holders for the belt in order to stick togheter the vertical and the horizontal axis.







We've also tried to realize the spikes in resin but the result was not good so we went back to the first 3D printed pieces.



Electrical hardware

We have used the following pieces to build the machine:


We have used to drivers with the CNC shield, one for each axis.
We have setted the voltage across the driver empirically using the trimmer on the driver to adjust the value accordingly to the behaviour of the stepper. We see that the optimal values to avoid loss on steps was around 1V.


The solenoid is connected through the following electrical scheme:


Instead of the motor, we have used a solenoid.We have used the transistor to amplify the current to 500mA. Reading (well...watching pictures because it is in chinese) the datasheet we realized that 1.1A at 5V should be the optimal value but we have tested that at 500mA it works well too.
We've tried to replace the Arduino with the satsha kit but actually it is not working. The grbl firmware on the board works perfectly but we still have to fix the electrical part in order to let the board working with the CNC shield.

Programming



To program the machine, we have followed the following workflow:



Testing grbl on Arduino


First of all, we have followed this instructions to upload the grbl firmware on the Arduino


As described in the guide, is possible to view all the grbl settings by sending the "$$" code through the serial port.
After changing some values to test if everything worked fine, we moved on to test if the firmware could have handled some G-code files.

Chili Pepper


We have decided to use this web-based g-code sender to test if the firmware worked fine with Gcode files. In order to use Chili Pepper, is necessary to install a Json serial server that can be downloaded directly inside the chili pepper page


We've used the fab modules to generate the G-code file starting from a black and white image and then we have uploaded the code to Chili Pepper.
After these preliminary tests, we were sure that the firmware worked properly and we moved to python language to write our custom G-code sender.

Python

We had no previous knowledge of python so we started from the ver basics. First of all i'video looked at a simple G-code streaming interface and i've found this example really useful.

#!/usr/bin/env python
"""\
Simple g-code streaming script for grbl
"""

import serial
import time

# Open grbl serial port
s = serial.Serial('COM10',115200) #change this accordingly to comport

# Open g-code file
f = open('somefile.gcode','r');

# Wake up grbl
s.write("\r\n\r\n")
time.sleep(2)   # Wait for grbl to initialize
s.flushInput()  # Flush startup text in serial input

# Stream g-code to grbl
for line in f:
    l = line.strip() # Strip all EOL characters for streaming
    print 'Sending: ' + l,
    s.write(l + '\n') # Send g-code block to grbl
    grbl_out = s.readline() # Wait for grbl response with carriage return
    print ' : ' + grbl_out.strip()

# Wait here until grbl is finished to close serial port and file.
raw_input("  Press Enter to exit and disable grbl.")

# Close file and serial port
f.close()
s.close()


This code simply reads a file line by line and sends the commando to the grbl firmware through the serial port.
We've than moved to build a better program that can fit the physical charateristics of our machine. Due to the fact that we had 144 physical spikes to make pictures, we have decided to build an interface which can represent all these elements.
We have decided to use the cross-platform library Pygame. We found a good documentation on this library full of examples and tutorials.
Our instructor, showed us how to build a program in python which can work as a virtual layer for the machine (DOWNLOAD).


import serial
import serial.tools.list_ports
import time
import cmd

​

​class Controller:



	def __init__(self, com, speed):
		self.comport = com
		self.comspeed = speed

	def open(self):
		self.serial = serial.Serial(port=self.comport,baudrate=self.comspeed,timeout=5)

	def readline(self):
		return self.serial.readline()

	def write(self, str):
		self.serial.write(str)

	def send(self, buffer):
		send_data = ''.join(buffer)
		self.serial.write(send_data)

	def sendAndWait(self, buffer):
		self.send(buffer)
		ch = self.serial.readline()
		while ch:
			if ch.strip() == 'ok':
			   return True
			ch = self.serial.readline()
		raise Exception("Command Failed")

	def close(self):
		self.serial.close()

class Axis:

	def __init__(self, name, var, controller):
		self.name = name
		self.var = var
		self.position = 0 # in mm
		self.targetPosition = 0 # in mm`
		self.controller= controller
		self.ratio = 250 #(step / mm )
		self.headPosition = 0

	def setup(self):
		buffer = []
		buffer.append('%s=%d' % (self.var, self.ratio))
		self.controller.send(buffer)
​
	def move(self, destination):
		self.targetPosition = destination
		self.sync()

	def sync(self):
		buffer = []
		if self.position != self.targetPosition:
				if self.headPosition > 0:
					buffer.append('G1 %s%d\n' % (self.name, self.targetPosition )) ## absolute
				else:
					buffer.append('G0 %s%d\n' % (self.name, self.targetPosition )) ## absolute
				try:
					print buffer
					self.controller.sendAndWait(buffer)
					self.position = self.targetPosition
				except Exception,s:
					print s


​class Console(cmd.Cmd):
​

	def __init__(self):
		cmd.Cmd.__init__(self)
		self.controller = None
		self.x_axis = None
		self.y_axis = None
​
	def do_open(self,line):
		""" Opens a serial connection to the controller"""
		c = Controller("COM8", 115200)
		c.open()
		c.write('?')
		time.sleep(1)
		ch = c.readline()
		while ch:
			print ch
			ch = c.readline()
		print ch
		self.controller = c
		self.x_axis = Axis('X', '$100', c)
		self.x_axis.setup()
		self.y_axis = Axis('Y', '$101', c)
		self.y_axis.setup()

	def do_moveX(self, line):

		""" Moves the X axis"""
		print line
		self.x_axis.move(int(line.strip()))

	def do_moveY(self, line):
		""" Moves the Y axis"""
		print line
		self.y_axis.move(int(line.strip()))

	def do_close(self, line):
		""" Closes the serial connection """
		self.controller.close()
		self.controller = None

	def do_EOF(self, line):
		self.do_close()
		return True

if __name__ == '__main__':

	#x_axis = Axis('X','$100', c)
	#x_axis.setup()
	Console().cmdloop("Enter option")
	c.close()

We've used this code as a starting point and then we decided to make a less sophisticated version of the program (DOWNLOAD):

import serial
  import time
  import pygame
  import re
  import sys

  def text_objects(text, font):
      textSurface = font.render(text, True, WHITE)
      return textSurface, textSurface.get_rect()

  class Controller:
          def __init__(self, com, speed):
              self.comport = com
              self.comspeed= speed

          def opencom(self):
              self.serial = serial.Serial(port=self.comport,baudrate=self.comspeed,timeout=5)

          def write(self, str):
              self.serial.write(str)
              print('scrivo : {}'.format(str))

          def readline(self):
              return self.serial.readline()

          def grbl_init(self):
              print "init grbl"
              self.serial.write("\r\n\r\n")
              time.sleep(2)
              self.serial.flushInput()
              print "grbl READY!"

          def closecom(self):
              self.serial.close()
              print "chiusura..."

          def send_wait(self, str):
              l_block = str.strip()
              c.write(l_block + '\n')
              grbl_out = c.readline().strip()
              print (grbl_out)

  class Axis:
          def __init__(self, name, var, var2, var3, controller):
                  self.name = name
                  self.var = var
                  self.var2 = var2
                  self.var3 = var3
                  self.position = 0
                  self.targetPosition= 0
                  self.step_mm = 160   # (step/mm)
                  self.max_rate = 14000 # (max speed mm/min)
                  self.accel = 1000     # acceleration
                  self.headPosition = 0
                  self.controller = controller

          def setup(self):
                  line = ('{}={}{}'.format(self.var,self.step_mm, "\n"))
                  c.send_wait(line)
                  line2 = ('{}={}{}'.format(self.var2,self.max_rate, "\n"))
                  c.send_wait(line2)
                  line3 = ('{}={}{}'.format(self.var3,self.accel, "\n"))
                  c.send_wait(line3)
                  print "Axis Initialized"
                  line4 = ('{}{}'.format("G21", "\n"))
                  c.send_wait(line4)
                  print "using mm as unit"
                  line5 = ('{}{}'.format("G90", "\n"))
                  c.send_wait(line5)
                  print "using absolute coordinates"


  class Commands:

              def __init__(self):
                  pass

              def move_X(self,vel,posX,feed):
                  line = ('{}{}{}{}{}{}'.format(vel,"X",posX,"F",feed,"\n"))
                  c.send_wait(line)
              def move_both(self, vel, posX, posY, feed):
                  line = ('{}{}{}{}{}{}{}{}'.format(vel,"X",posX,"Y",posY,"F",feed,"\n"))
                  c.send_wait(line)

              def sol_on(self):
                  line = ('{}{}'.format("M8", "\n"))
                  time.sleep(0.5)
                  c.send_wait(line)

              def sol_off(self):
                  line = ('{}{}'.format("M9", "\n"))
                  c.send_wait(line)


  # Define some colors
  BLACK = (0, 0, 0)
  WHITE = (255, 255, 255)
  GREEN = (0, 255, 0)
  RED = (255, 0, 0)

  # Define offset
  OFFSET = 30

  # This sets the WIDTH and HEIGHT of each grid location
  WIDTH = 20
  HEIGHT = 20

  # This sets the margin between each cell
  MARGIN = 5

  # Create a 2 dimensional array. A two dimensional
  # array is simply a list of lists.
  grid = []
  for row in range(12):
      # Add an empty array that will hold each cell
      # in this row
      grid.append([])
      for column in range(12):
          grid[row].append(0)  # Append a cell

  # Initialize pygame
  pygame.init()

  # Set the HEIGHT and WIDTH of the screen
  WINDOW_SIZE = [305, 365]
  screen = pygame.display.set_mode(WINDOW_SIZE)

  # Set title of screen
  pygame.display.set_caption("Steam Machine v1.0")

  # Loop until the user clicks the close button.
  done = False

  # Used to manage how fast the screen updates
  clock = pygame.time.Clock()

  if __name__ == '__main__':

      c = Controller("COM7", 115200)
      c.opencom()
      c.grbl_init()
      #time.sleep(5)
      #grbl_out = c.readline().strip()
      #print(grbl_out)
      x_axis = Axis('X','$100','$110','$120',c)
      x_axis.setup()
      y_axis = Axis('Y','$101','$111','$121',c)
      y_axis.setup()
      d = Commands()


  # -------- Main Program Loop -----------
      while not done:
          for event in pygame.event.get():  # User did something
              if event.type == pygame.QUIT:  # If user clicked close
                  done = True  # Flag that we are done so we exit this loop
              elif event.type == pygame.MOUSEBUTTONDOWN:
                  # User clicks the mouse. Get the position
                  pos = pygame.mouse.get_pos()
                  # Change the x/y screen coordinates to grid coordinates
                  column = pos[0] // (WIDTH + MARGIN)
                  row = pos[1] // (HEIGHT + MARGIN)
                  print("Click ", pos, "Grid coordinates: ", row, column)
                  #Tracking changes of grid elements status
                  try:
                      #state 1-->0 do nothing
                      if grid[row][column] == 1:
                          grid[row][column] = 0

                      #State 0-->1, move the machine
                      elif grid[row][column] == 0:
                          grid[row][column] = 1
                          d.move_both('G0',OFFSET*column,OFFSET*row,'500')
                          d.sol_on()
                          time.sleep(.3)
                          d.sol_off()


                  except:
                      pass

          # Set the screen background
          screen.fill(BLACK)
          #pygame.draw.circle(screen, BLUE, (300, 50), 20, 0)
          #pygame.draw.rect(gameDisplay, red,(550,450,100,50))

          #draw the send button
          width, height = pygame.display.Info().current_w, pygame.display.Info().current_h
          pygame.draw.rect(screen, GREEN, [(width/2)-50, height-60, 100, 50])

          #text inside the button
          smallText = pygame.font.Font("freesansbold.ttf",20)
          textSurf, textRect = text_objects("G-CODE!", smallText)
          textRect.center = ( (width/2), height-30 )
          screen.blit(textSurf, textRect)
          # Draw the grid
          for row in range(12):
              for column in range(12):
                  color = WHITE
                  if grid[row][column] == 1:
                      color = GREEN
                  pygame.draw.rect(screen,
                                   color,
                                   [(MARGIN + WIDTH) * column + MARGIN,
                                    (MARGIN + HEIGHT) * row + MARGIN,
                                    WIDTH,
                                    HEIGHT])

          # Limit to 60 frames per second
          clock.tick(60)

          # Go ahead and update the screen with what we've drawn.
          pygame.display.flip()

      # Be IDLE friendly. If you forget this line, the program will 'hang'
      # on exit.
      pygame.quit()
      c.closecom()

With this code, we were able to address the solenoid to each position of our grid and to activate each spike.
By clicking on an element of the grid, the correspondent physical elemnt comes out from the grid through the pressure of the solenoid.




Resources