Arduino Listing:
// pins for motor 1 const byte M1_A = 8; const byte M1_B = 7; const byte M1_C = 6; const byte M1_D = 5; // pins for motor 2 const byte M2_A = 4; const byte M2_B = 3; const byte M2_C = 2; const byte M2_D = A1; // (note: analog pin used as digital) // pins for input. (note: analog pins used as digital) const byte I_LEFT = A2; const byte I_UP = A3; const byte I_DOWN = A4; const byte I_RIGHT = A5; // pin sequence for stepping motor1 and motor2 const byte M1PINS[] = {M1_A,M1_B,M1_C,M1_D}; const byte M2PINS[] = {M2_A,M2_B,M2_C,M2_D}; // each step needs to be a pulse a few ms wide, or motor doesn't step. byte stepDwell = 10; // pause a few ms between repeating input triggers byte iPause = 20; // track the current pin index in the step sequence byte m1Pin = 0; byte m2Pin = 0; // holds the command code, sent over serial char commandValue = '\0'; // holds the data, sent over serial int dataValue = 0; // void setup() { Serial.begin(9600); // setup pins for output pinMode(M1_A, OUTPUT); pinMode(M1_B, OUTPUT); pinMode(M1_C, OUTPUT); pinMode(M1_D, OUTPUT); pinMode(M2_A, OUTPUT); pinMode(M2_B, OUTPUT); pinMode(M2_C, OUTPUT); pinMode(M2_D, OUTPUT); // setup pins for input pinMode(I_LEFT, INPUT); pinMode(I_UP, INPUT); pinMode(I_DOWN, INPUT); pinMode(I_RIGHT, INPUT); } // void loop() { // priority is for commands coming over serial if (Serial.available()) { char incomingChar = Serial.read(); if ('\n' == incomingChar) { // eol recvd, so process command and value collected so far processCommand(commandValue, dataValue); Serial.print("(");Serial.print(commandValue);Serial.print(dataValue); Serial.println(") ok >");// reply back to calling device commandValue = '\0'; // reset dataValue = 0; // reset } else if (isDigit(incomingChar)) { dataValue = (dataValue * 10) + (incomingChar - '0'); // accumulate the data value } else { // non-digit, so consider as a command code commandValue = incomingChar; // set the current command } } else { // if no serial commands to be processed, check for direct inputs and button presses if (digitalRead(I_LEFT) == HIGH) step(I_LEFT); if (digitalRead(I_UP) == HIGH) step(I_UP); if (digitalRead(I_DOWN) == HIGH) step(I_DOWN); if (digitalRead(I_RIGHT) == HIGH) step(I_RIGHT); } } /* -------------- support routines -------------- */ // returns true if 'digit' represents an ascii digit between 0 and 9 boolean isDigit (char digit) { return (digit >= '0' && digit <= '9'); } // void processCommand(char commandValue, int dataValue) { if ('i' == commandValue) {// set iPause iPause = dataValue; return; } else if ('s' == commandValue) {// set stepDwell stepDwell = dataValue; return; } // if "move" command, process it byte direction = 255; if ('L' == commandValue) direction = I_LEFT; else if ('U' == commandValue) direction = I_UP; else if ('D' == commandValue) direction = I_DOWN; else if ('R' == commandValue) direction = I_RIGHT; if (255 != direction) { for (int i = 0; i < dataValue; i++) step(direction); return; } } // void step(byte direction) { switch (direction) { case I_LEFT: m1Pin = cShift(-1,m1Pin); pulse(M1PINS[m1Pin]); break; case I_UP: m2Pin = cShift(1,m2Pin); pulse(M2PINS[m2Pin]); break; case I_DOWN: m2Pin = cShift(-1,m2Pin); pulse(M2PINS[m2Pin]); break; case I_RIGHT: m1Pin = cShift(1,m1Pin); pulse(M1PINS[m1Pin]); break; default: // invalid value, ignore break; } } // void pulse(byte pinToPulse) { digitalWrite(pinToPulse,HIGH); delay(stepDwell); digitalWrite(pinToPulse,LOW); delay(iPause); } /* performs a circular shift of the index indexToShift, by shiftDirection. operation is done on a 4-bit word. returns the index shifted to. cShift(1,0) -> 1 (1000 -> 0100), cShift(1,1) -> 2 (0100 -> 0010) cShift(1,2) -> 3 (0010 -> 0001), cShift(1,3) -> 0 (0001 -> 1000) cShift(-1,0) -> 3 (1000 -> 0001), cShift(-1,1) -> 0 (0100 -> 1000) cShift(-1,2) -> 1 (0010 -> 0100), cShift(-1,3) -> 2 (0001 -> 0010) */ byte cShift(char shiftDirection, byte indexToShift) { char shiftedIndex = shiftDirection + indexToShift; if (shiftedIndex > 3) shiftedIndex = 0; else if (shiftedIndex < 0) shiftedIndex = 3; return shiftedIndex; }
Web UI for the StepASketch:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Style-Type" content="text/css"> <title>Step a Sketch Controller</title> <link rel="icon" href="favicon.ico" type="image/x-icon" /> <script type="text/javascript"> /* ----------------- base utils -----------------*/ function $() { return document.getElementById(arguments[0]); } function $F() { return document.getElementById(arguments[0]).value; } function getKeyCode(ev) { if (window.event) return window.event.keyCode; return ev.keyCode; } function syncAjax(url,data) { var ajax = new XMLHttpRequest(); try { ajax.open("POST", url, false); ajax.setRequestHeader("Content-type", "text/plain"); ajax.send(data); } catch (e) {console.log('exception:' + e);} return ajax.responseText; } /* ----------------- base utils -----------------*/ // basic drawing related .................................................. var CANVAS; var CTX; function init() { CANVAS = document.getElementById('plot'); CTX = CANVAS.getContext('2d'); clearCanvas(); } function clearCanvas() { CTX.clearRect(0, 0, CANVAS.width, CANVAS.height); CTX.strokeStyle = "#000000"; CTX.strokeRect(0, 0, 800, 500); } // manual line drawing related .................................................. var P1 = null; var P2 = null; function getCoordinates(e) { var x = e.clientX; var y = e.clientY; $("xycoordinates").innerHTML = "(" + x + "," + y + ")"; } function clearCoordinates() { $("xycoordinates").innerHTML = ""; } function canvasClicked(e) { var markerSize = 6; var x = e.clientX; var y = e.clientY; baseDrawLine(x,y-markerSize,x,y+markerSize); baseDrawLine(x-markerSize,y,x+markerSize,y); if (null == P1) { P1 = {}; P1.x = x; P1.y = y; } else { P2 = {}; P2.x = x; P2.y = y; } } function manualLine() { drawLine (P1.x, P1.y, P2.x, P2.y); } // machine drawing related .................................................. /* q determinant m dx dy ------------------------------- a x1 > x2, y1 > y2, + - - b x1 < x2, y1 > y2, - + - c x1 < x2, y1 < y2, + + + d x1 > x2, y1 < y2, - - + */ /* drawing primitive function from the underlying system. */ function baseDrawLine(fromX, fromY, toX, toY) { //console.log('from:' + fromX + ',' + fromY + ' to:' + toX + ',' + toY); CTX.lineWidth = 1; CTX.strokeStyle = "#000000"; CTX.beginPath(); CTX.moveTo(fromX, fromY); CTX.lineTo(toX, toY); CTX.stroke(); } // suports simulating the stepper motor drive function stepDrawLine(fromX, fromY, toX, toY) { var relX = toX - fromX; var relY = toY - fromY; if (relX < 0) stepRender('L', Math.abs(relX), fromX, fromY); else stepRender('R', Math.abs(relX), fromX, fromY); if (relY < 0) stepRender('U', Math.abs(relY), fromX, fromY); else stepRender('D', Math.abs(relY), fromX, fromY); } // simulates the stepper motor drive on the device function stepRender(direction, steps, startingX, startingY) { switch (direction) { case 'L': baseDrawLine(startingX, startingY, startingX-steps, startingY); break; case 'R': baseDrawLine(startingX, startingY, startingX+steps, startingY); break; case 'U': baseDrawLine(startingX, startingY, startingX, startingY-steps); break; case 'D': baseDrawLine(startingX, startingY, startingX, startingY+steps); break; default: break; } if (! $('simulate_switch').checked) toDevice(direction, steps); } /* calculates the interpolated points for straight line */ function drawLine(fromX, fromY, toX, toY) { console.log('from:' + fromX + ',' + fromY + ' to:' + toX + ',' + toY); if (fromX == toX || fromY == toY) { console.log('X\'s or Y\'s equal'); //baseDrawLine(fromX, fromY, toX, toY); stepDrawLine(fromX, fromY, toX, toY); return; } var P_next = {}; var P_current = {}; P_current.x = fromX; P_current.y = fromY; var slope_m = Math.abs((toY - fromY) / (toX - fromX)); var slope_type = (fromX > toX) ? ((fromY > toY) ? 'A' : 'D') : ((fromY > toY) ? 'B' : 'C'); var is_slope_upward = slope_m > 1; console.log('slope_m = ' + slope_m + ' is_slope_upward = ' + is_slope_upward + ' slope_type = ' + slope_type); var step_range = (is_slope_upward) ? Math.abs(fromY - toY) : Math.abs(fromX - toX); console.log('step_range ' + step_range); for (var step = 1; step < step_range; step++) { var relX = (is_slope_upward) ? Math.round(step / slope_m) : step; var relY = (is_slope_upward) ? step : Math.round(step * slope_m); switch (slope_type) { case 'A': relX = -relX; relY = -relY; break; case 'B': relY = -relY; break; case 'D': relX = -relX; break; default: break; } P_next.x = fromX + relX; P_next.y = fromY + relY; //console.log('rel ' + relX + ' ' + relY + ' Pc:' + P_current.x + ',' + P_current.y + ' Pn:' + P_next.x + ',' + P_next.y); //baseDrawLine(P_current.x, P_current.y, P_next.x, P_next.y); stepDrawLine(P_current.x, P_current.y, P_next.x, P_next.y); P_current.x = P_next.x; P_current.y = P_next.y; } } function processInput() { var lines = $F('textarea1').split('\n'); var prevX = -1; var prevY = -1; for (var i = 0; i < lines.length; i++) { var currX = -1; var currY = -1; var line = lines[i]; if (line.indexOf('G00') == 0 || line.indexOf('G01') == 0) { var parts = line.split(' '); if (parts.length > 2) { if (parts[1].indexOf('X') == 0) currX = parseInt(parts[1].substring(1)); // cast to wholenumber if (parts[2].indexOf('Y') == 0) currY = parseInt(parts[2].substring(1)); // cast to wholenumber if (currX > -1 && currY > -1) { console.log('x,y:' + currX + ',' + currY); console.log(prevX + ',' + prevY); if (prevX != -1 && prevY != -1) { //baseDrawLine(prevX, prevY, currX, currY); drawLine(prevX, prevY, currX, currY); } prevX = currX; prevY = currY; } } } } } // device communication related ................................................. function deviceCommand(ref) { var command = ref.id.substring(0,1); var value = $F(command + '_value'); $('comm_log').innerHTML = command + value + '<br />'; var response = toDevice(command, value); $('comm_log').innerHTML += response; } function toDevice(command, value) { return response = syncAjax('/', command + value); } function test() { } </script> <style media="screen" type="text/css"> body,table, button {font-family:verdana,helvetica,arial,sans-serif;margin:0px;font-size:10px;} button {cursor:pointer;} input {border: solid 1px gray;} input.d_input {width:24px;} </style> </head> <body onload="init()"> <table width="100%" cellpadding="0" cellspacing="0"> <tr> <td valign="top" width="800"> <canvas id="plot" width="800" height="500" style="background-color:#DCDCDC;" onmousemove="getCoordinates(event)" onmouseout="clearCoordinates()" onclick="canvasClicked(event)"> <p>browser doesn't support canvas.</p> </canvas> <table width="100%"> <tr> <td> <button onclick="clearCanvas();P1=P2=null;">clear canvas</button> <button onclick="manualLine()" title="draw line between 2 points picked on canvas">line</button> </td> <td align="right"><span id="xycoordinates"></span></td> </tr> </table> </td> <td valign="top"> G-code to be processed:<br /> <textarea id="textarea1" rows="30" cols="40"> </textarea> <br /> <button onclick="processInput()">process</button> <input type="checkbox" id="simulate_switch" checked="checked" /> simulate </td> </tr> </table> <hr /> <table> <tr> <td valign="top"> <table style="padding:5px;"> <tr><td colspan="2">serial port <button id="o_command" onClick="deviceCommand(this)">open</button><input type="hidden" id="o_value" value="" /> <button id="c_command" onClick="deviceCommand(this)">close</button><input type="hidden" id="c_value" value="" /> <hr /></td></tr> <tr><td><input type="text" class="d_input" id="i_value" maxlength="3" /></td><td><button id="i_command" onclick="deviceCommand(this)">set pause</button></td></tr> <tr><td><input type="text" class="d_input" id="s_value" maxlength="3" /></td><td><button id="s_command" onclick="deviceCommand(this)">set dwell</button></td></tr> <tr><td colspan="2"><hr /><div id="comm_log" /></td></tr> </table> </td> <td> <table style="border:1px solid gray;padding:5px;"> <tr align="middle"><td colspan="2"></td><td><input type="text" class="d_input" id="U_value" maxlength="3" /></td><td colspan="2"></td></tr> <tr align="middle"><td colspan="2"></td><td><button id="U_command" onclick="deviceCommand(this)">U</button></td><td colspan="2"></td></tr> <tr align="middle"><td><input type="text" class="d_input" id="L_value" maxlength="3" /></td> <td><button id="L_command" onclick="deviceCommand(this)">L</button></td> <td></td><td><button id="R_command" onclick="deviceCommand(this)">R</button></td> <td><input type="text" class="d_input" id="R_value" maxlength="3" /></td></tr> <tr align="middle"><td colspan="2"></td><td><button id="D_command" onclick="deviceCommand(this)">D</button></td><td colspan="2"></td></tr> <tr align="middle"><td colspan="2"></td><td><input type="text" class="d_input" id="D_value" maxlength="3" /></td><td colspan="2"></td></tr> </table> </td> </tr> </table> </body> </html>
Python web server and serial port connector:
#!/usr/bin/python import BaseHTTPServer import SimpleHTTPServer import serial HTTP_PORT = 56789 SERIAL_PORT = '/dev/ttyUSB0' SERIAL_REF = None def execute_command(commandData): global SERIAL_REF if 'o' == commandData: SERIAL_REF = serial.Serial(SERIAL_PORT, baudrate=9600, timeout=None) return 'ok - opened' elif 'c' == commandData: SERIAL_REF.close() return 'ok - closed' else: SERIAL_REF.write(commandData + '\n') response = SERIAL_REF.readline() return response class SimpleHTTPHandlerImpl(SimpleHTTPServer.SimpleHTTPRequestHandler): def do_GET(self): try: #serve files, and directory listings by following self.path from current working directory SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) except: result = '<html><body>error...</body></html>' self.wfile.write(result) def do_POST(self): """Handle a post request""" length = int(self.headers.getheader('content-length')) data_string = self.rfile.read(length) result = execute_command(data_string) self.wfile.write(result) def main(): server = BaseHTTPServer.HTTPServer(("localhost", HTTP_PORT), SimpleHTTPHandlerImpl) try: print "serving at port", HTTP_PORT server.serve_forever() except KeyboardInterrupt: print 'shutting down...' server.socket.close() if __name__ == '__main__': main()