Remote stepper motor control with Raspberry Pi

Recently we wrote the article regarding remote monitoring using websockets. Just to remind you shortly, the temperature sensors connected to Raspberry Pi were periodically read and the readings were sent to the webpage. The client side was just a visualization of the received readings in JavaScript. Now, we would like to control things over Web, i.e. when user do some action on a webpage (like press the button), to trigger appropriate action on the server side or even other connected client. In this case we want to control the rotation of a unipolar stepper motor that is connected to RPi from the web page. More precisely, we would like to "tell" the motor from appropriate Web GUI on which position to go (angle in degrees from referent position) and at what speed to perform this transition. The final result can be seen in this youtube video.

The article is divided into three parts. The first part deals with the hardware, more precisely stepper motor driver. The second part is about software side made for stepper control and websocket communication in Python. In the third one we are describing client side made in JavaScript using d3.js library for data visualization.

 

Hardware

If you are not familiar with the stepper motors, please read this page before proceeding. To control the stepper motor with the RPi you will need appropriate driver. The driver is connected between the RPi and stepper motor as shown in the image below. Between RPi and the driver board there are power lines (3.3V and GND) and four control lines which are used for stepper motor phases switching. However, we mounted motor and optical sensor (based on optotransistor) on the driver PCB. Therefore, one signal line is coming from optical sensor to RPi which will be used for driving motor in the referent position. The motor is connected to the driver with six wires. The motor is powered with external power supply.

The image below shows the schematic of stepper driver board in more details. Connector X1 is the motor power supply, X2 connector is used for RPi connection (3.3V, GND, control lines and optical sensor output) and X3 is the stepper motor connector. As can be seen, the driver is based on Darlington transistor array ULN2003A which is used for switching the phases of unipolar stepper motor. The Darlington array is not directly connected to the RPi GPIO pins, we used quad channel phototransistor optocouplers ILQ-621 between. Mounted optical sensor needs power supply which is coming from RPi. LEDs are used to visually indicate activity on control lines and optical sensor output. 

On the image below you can see the built motor driver. We mounted some old stepper motor with 7.5 degrees per step on the driver PCB. Small "arm" is mounted on motor shaft. When the motor rotates, the "arm" can activate optical sensor. This particular position is referent position and we will designate it as "0" degrees position.

 

Software - server side (Raspberry Pi)

As already mentioned, the stepper motor driver is connected to Raspberry Pi GPIO pins. To move the motor shaft, sequence of square wave pulses have to be generated on these GPIO pins. These pulses activate Darlington transistor array so that phases of stepper motor are energized. We are using half stepping technique which increases angular resolution to 3.75 degrees.

The stepper control program is written in Python. We defined a class "stepMotor". The class has the following instance attributes:

  • "motorPosDeg"          - stores current motor position in degrees
  • "halfStepping"            - coil energize sequence
  • "stepPointer"              - pointer to current combination in halfstepping sequence
  • "motor_A"                   - first motor phase wiring to RPi GPIO
  • "motor_B"                   - second motor phase wiring to RPi GPIO
  • "motor_C"                   - third motor phase wiring to RPi GPIO
  • "motor_D"                   - fourth motor phase wiring to RPi GPIO
  • "sensor"                      - optical sensor wiring to RPi GPIO

The class has the following methods which enable moving of the motor arm to the desired angle at the deisred speed:

  • rotateR                    - rotate motor shaft in clockwise direction with desired speed
  • rotateL                     - rotate motor shaft in counterclockwise direction with desired speed
  • rotateToAngle        - rotate motor shaft to particular angle with desired speed
  • initPos                    - rotate motor to 0 degrees
  • turnOff                    - set low level on GPIO pins i.e. turn off motor phases.
  • makeStep              - set high level on GPIO pins according to control sequence

For the communication with the Web page we used websockets. If you are not familiar with webosckets, please read this article. On the client side i.e. web page the speed of rotation and desired arm angle is defined and sent to our Python script. Therefore, when message is received over websocket, the motor should be moved according to the received data. This is done by calling "rotateToAngle" method in "on_message" method of websocket handler class. The whole code should look like this:

#!/usr/bin/env python
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web
import RPi.GPIO as GPIO
import time

class stepMotor:
	
  #init gpio and rotate motor to initial position (0 degrees)
  def __init__(self, motor_A, motor_B, motor_C, motor_D, sensor):
	
    self.sensor = sensor
    self.motor_A = motor_A
    self.motor_B = motor_B
    self.motor_C = motor_C
    self.motor_D = motor_D
		
    self.halfStepping = [[1,0,1,0],[1,0,0,0],[1,0,0,1],[0,0,0,1],[0,1,0,1],[0,1,0,0],[0,1,1,0],[0,0,1,0]]
    self.motorPosDeg = 0	#current motor position in degrees [0,360]
    self.stepPointer = 0	
		
    #unipolar motor wiring to RPi
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(sensor,GPIO.IN)
    GPIO.setup(motor_A, GPIO.OUT)
    GPIO.setup(motor_B, GPIO.OUT)
    GPIO.setup(motor_C, GPIO.OUT)
    GPIO.setup(motor_D, GPIO.OUT)
		
    self.initPos()

  #rotate motor in clockwise direction
  def rotateR(self, noSteps, speed):
	
    self.makeStep(self.halfStepping[self.stepPointer])
    for i in range (0, noSteps):
			
      if self.stepPointer == 7:
        self.stepPointer = 0
      else:
        self.stepPointer += 1
			
    self.makeStep(self.halfStepping[self.stepPointer])
    time.sleep(speed)
		
    self.turnOff()
    self.motorPosDeg -= noSteps*3.75
    if (self.motorPosDeg<0):
      self.motorPosDeg = 360 + self.motorPosDeg
	
  #rotate motor in counterclockwise direction
  def rotateL(self, noSteps, speed):
		
    self.makeStep(self.halfStepping[self.stepPointer])
    for i in range (0, noSteps):
			
      if self.stepPointer == 0:
        self.stepPointer = 7
      else:
        self.stepPointer -= 1
				
    self.makeStep(self.halfStepping[self.stepPointer])
    time.sleep(speed)
		
    self.turnOff()
    self.motorPosDeg += noSteps*3.75
    if (self.motorPosDeg >= 360):
      self.motorPosDeg = self.motorPosDeg - 360
	
  #rotate motor to specific position
  def rotateToAngle(self, desiredAngle, speed):

    deltaAngle = abs(self.motorPosDeg - desiredAngle)

    if(desiredAngle > self.motorPosDeg):
      if(deltaAngle >= 180):
        self.rotateR(int((360-deltaAngle)/3.75),speed)
      else:
        self.rotateL(int(deltaAngle/3.75),speed)
    else:
      if(deltaAngle >= 180):
        self.rotateL(int((360-deltaAngle)/3.75),speed)
      else:
        self.rotateR(int(deltaAngle/3.75),speed)

				
  #go to initial position defined by optical sensor
  def initPos(self):

    while (GPIO.input(self.sensor)):
      self.rotateR(1, 0.02)
		
    self.motorPosDeg = 0

		
  #turn coils off
  def turnOff(self):

    GPIO.output(self.motor_A, False)
    GPIO.output(self.motor_B, False)
    GPIO.output(self.motor_C, False)
    GPIO.output(self.motor_D, False)
		
  #make single step	
  def makeStep(self, coil_state):
		
    GPIO.output(self.motor_A, coil_state[0])
    GPIO.output(self.motor_B, coil_state[1])
    GPIO.output(self.motor_C, coil_state[2])
    GPIO.output(self.motor_D, coil_state[3])

class WSHandler(tornado.websocket.WebSocketHandler):

  def open(self):
    print 'Connected.\n' 
    self.stepper = stepMotor(17,22,23,24,18)

  def on_message(self, message):
    print 'received message: %s\n' %message
    data = message.split(";")
    self.stepper.rotateToAngle(float(data[0]), float(data[1]))

  def on_close(self):
    print 'connection closed\n'

application = tornado.web.Application([(r'/ws', WSHandler),])

if __name__ == "__main__":
  http_server = tornado.httpserver.HTTPServer(application)
  http_server.listen(8888)
  main_loop = tornado.ioloop.IOLoop.instance()
  main_loop.start()

 

Software - client side

In this third part the visualization of stepper motor control project is described. The web page for visualization is using three libraries: Bootstrap (for responsiveness), jQuery (for websocket communication and Bootstrap's JavaScript plugins) and D3.js (for adding SVG elements and manipulating with them). We will not describe libraries itself because there are plenty of texts about them online, but we will describe how did we make our web controller for speed and angular position of stepper motor by using these libraries.
As can be seen from the following code, the body of html document consists only of three div elements (container, row and wrapper) to which everything else will be added with some JS code:

<!doctype html>
<html>
<head>
  <title>Remote stepper motor control</title>
  <meta charset="utf-8" />
  <style>
    .col-centered{
    float: none;
    text-align:center;
    margin: 0 auto;
  }
  </style>

  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Bootstrap -->
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">

  <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
  <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
  <!--[if lt IE 9]>
  <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
  <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
  <![endif]-->

  <script src="http://code.jquery.com/jquery.min.js"></script>
  <script src="http://d3js.org/d3.v3.min.js"></script>

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> -->
  <!-- Include all compiled plugins (below), or include individual files as needed -->

  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
</head>
<body>
  <div class="container">
    <div class="row">
      <div id="wrapper" class="col-xs-12 col-md-6 col-lg-4 col-centered" >
        <h2>Stepper motor control</h2>
        <label id="conn_text">Connection status: Connecting!</label><br />
        <br />
      </div>
    </div>
  </div>
<script>

//...more code to add

</script>	
</body>
</html>

After page opening the following message is shown:

Stepper motor control

In the JS script we will add the code that will be executed after loading of all external JS documents and in that code we will try to establish a websocket connection with our server. In the case of successful connection the web page will inform the user with the message "Connection status: Connected!" or with "Connection status: Disonnected!" if connection is closed or can't be established. In the script we have some variables describing stepper motor (motor_step, halfstep, step), variables for web controller visualisation (angle, trans_x, trans_y, in_transition, pointer_data) and message variable for sending speed and curr_angle variables to the RPi which controls the stepper motor.

<script>
$(document).ready(function ()
{
  var ws = new WebSocket("ws://example.com:8888/ws");
  ws.onopen = function(evt){
  var conn_status = document.getElementById('conn_text');
  conn_status.innerHTML = "Connection status: Connected!"
};
  ws.onclose = function(evt){
  var conn_status = document.getElementById('conn_text');
  conn_status.innerHTML = "Connection status: Disonnected!"
};

var angle = new Array();
var motor_step = 7.5;
var halfstep = 2;
var step = motor_step/halfstep;
var r1 = 100;
var r2 = 80;
var r3 = 100;
var curr_angle = 0;
var speed = 5;
var message = {"speed": speed, "angle": curr_angle}
var trans_x = 150;
var trans_y = 150;
var in_transition=0;
var RTSmessage = [];
var pointer_data = [
{"d":"M -10 0 L 0 90 L 10 0A 10 10 0 0 0-10 0","id":"pointer_ghost","a":0, "style":"opacity:0.5;"},
{"d":"M -10 0 L 0 90 L 10 0A 10 10 0 0 0-10 0","id":"pointer","a":0, "style":"opacity:1;"}];
for (i=0;i<360;i=i+step)
  angle.push(i);

//...more code to add

});
</script>

As a first element we will add label with speed information and after that an input element as a slider. That is made by selecting wrapper (div element) and appending of appropriate elements. For input element we will set attributes as min and max value, type, step and event handler („input“) for updating a new speed value in a message that is sent to the server. Update function has other functionality, updating of speed value info label on the web page. After this step, the appropriate page part should look similar to this:

//...more code to add
var slider_label = d3.select("#wrapper")
  .append("label")
  .attr("id","slider-value")
  .html("Speed: " + speed + " s/step");

var slider = d3.select("#wrapper")
  .append("input")
  .attr("type","range")
  .attr("id","slider")
  .attr("min", 0.005)
  .attr("max", 0.5)
  .attr("step", 0.005)
  .attr("class", "col-centered")
  .attr("viewBox","0 0 300 300")
  .attr("preserveAspectRatio","xMidYMin meet")
  .on("input", function() { update(this.value);});

// Initial starting speed 
update(0.005);

function update(newSpeed) {
  // adjust the text on the range slider
  slider_label.text("Speed: " + newSpeed + " s/step");
  slider.property("value", newSpeed);

  // update the speed
  message.speed = newSpeed;
//...more code to add

Angular position selector is the SVG element with the angle values distributed around a circle and with the pointer for the angle selection. Actually, we have two pointers, first one is floating one whichs depicts the value of angle that we will choose and the other one is for the animation of a real motor pointer movement. Therefore, SVG element is appended to the wrapper, and we have defined an id, some view attributes and two event handlers for that SVG element. The event handlers are the same function but we will differentiate events inside that function (function code is presented later in the text). After addition of SVG element we can't see any change on our screen except an empty SVG element.

var svg = d3.select("#wrapper.append("svg").attr("id","svg1").attr("viewBox","0 0 300 300").attr("preserveAspectRatio","xMidYMin meet").on("mousemove", click).on("click", click);

// Define the gradient
var gradient = svg.append("svg:defs")
  .append("svg:linearGradient")
  .attr("id", "gradient")
  .attr("x1", "0%")
  .attr("y1", "0%")
  .attr("x2", "100%")
  .attr("y2", "100%")
  .attr("spreadMethod", "pad");

// Define the gradient colors
gradient.append("svg:stop")
  .attr("offset", "0%")
  .attr("stop-color", "AliceBlue")
  .attr("stop-opacity", 1);

gradient.append("svg:stop")
  .attr("offset", "100%")
  .attr("stop-color", "LightBlue")
  .attr("stop-opacity", 1);

function click() {
//code will be added later
}

//...more code to add

To make circular angular position selector define three circles. The first circle is the outter ring of angular position selector. We will style it with gradient blue what is defined in the code above and actual code for circle adding is:

//adding outter ring
var ring = svg
  .append("circle")
  .attr("cx",trans_x)
  .attr("cy",trans_y)
  .attr("r",r1+30)
  .style("fill", "url(#gradient)" )
  .style("stroke","black");

//...more code to add

The other two cirles are filled in with white color and gradient blue. All three circles are translated in the center of SVG element

//adding outter ring – second part
var circle = svg
  .append("circle")
  .attr("cx",trans_x)
  .attr("cy",trans_y)
  .attr("r",r1)		
  .style("fill", "white")
  .style("stroke","#B90925");

//...more code to add

//adding inner ring
var circle2 = svg
  .append("circle")
  .attr("cx",trans_x)
  .attr("cy",trans_y)
  .attr("r",r2)		
  .style("fill", "url(#gradient)") 
  .style("stroke","DarkBlue");
//...more code to add

The aangular position selector has marking lines for available angles. Every angle that is divisionable with 45 degrees without remaining is marked with the longer line and code for adding the lines is:

//adding lines
var lines = svg.selectAll("line")
  .data(angle)
  .enter()
  .append("line")
  .attr("x1",function(d){ if (d%45 == 0){ return (r2-10)*Math.cos(d* (Math.PI/180)) + trans_x;}
    else{  return (r2)*Math.cos(d* (Math.PI/180)) + trans_x;}})
  .attr("y1",function(d){ if (d%45 == 0){ return (r2-10)*Math.sin(d* (Math.PI/180)) + trans_y;}
    else {return r2*Math.sin(d* (Math.PI/180)) + trans_y;}})
  .attr("x2",function(d){ return r3*Math.cos(d* (Math.PI/180)) + trans_x})
  .attr("y2",function(d){ return r3*Math.sin(d* (Math.PI/180)) + trans_y})
  .attr("id",function(d,i){ return i})
  .style("stroke","black")
  .style("stroke-width",function(d){ if (d%45 == 0){ return 2;}
    else {return 1}});
//...more code to add

The longer lines are marked with appropriate text:

//adding text
var angles_txt = svg.selectAll("text")
  .data(angle)
  .enter()
  .append("text")
  .attr("text-anchor","middle")
  .attr("x",function(d){ if (d%45 == 0){ return (r1+15)*Math.cos(d* (Math.PI/180)) + trans_x;}
    else{  return (r2)*Math.cos(d* (Math.PI/180)) + trans_x;}})
  .attr("y",function(d){ if (d%45 == 0){ return (r1+15)*Math.sin(d* (Math.PI/180)) + trans_y+5;}
    else {return r2*Math.sin(d* (Math.PI/180)) + trans_y;}})
  .attr("id",function(d){return d;})
  .html(function(d){ if (d%45 == 0){ return (360-(d-90))%360;} else {return ""}})
  .style("font-weight", "bold")
  .style("font-size",function(d){ if (d%45 == 0){ return 16;}
    else {return 12}});

//...more code to add

The final step in making of angular position selector is appending of two SVG path elements for selecting and animating of choosen and current angle pointers. One pointer is pointing on current angle of a motor and other will float depending of mouse position above SVG element.

//adding pointers
var gpointers = svg.selectAll(".container")
  .data(pointer_data)
  .enter()
  .append("g")
  .classed("container", true)
  .attr("transform", "translate(150, 150)")
  .append("path")
  .attr("id",function(d){return d.id})
  .attr("d",function(d){return d.d})
  .attr("style",function(d){return d.style})
  .style("fill", "Black")
  .style("stroke","#B90925");

//...more code to add

After clicking on SVG element the x and y position are read and rotateElement function for pointer animation is called.

//reading mouse position
function click() {
  //code will be added later
  var position = d3.mouse(this),
  mouse_point = {"x": position[0], "y": position[1]};
  rotateElement(trans_x,trans_y,mouse_point.x,mouse_point.y);
}

//...more code to add

Pointer animation is the main role of rotateElement function. It accepts origin of coordinate system and current x and y position of mouse. If the motor is not in transition the function calculates current angle in degrees and quantize it according to possible set of angles. Because of the rotation of our coordinate system, where the 0 degrees is set on the 6 o'clock, we have extra code for calculation of that custom angles. We will rotate ghost pointer for every mouse movement above SVG element if the motor is not in transition. If the type of event is click, other pointer is animated in the amount of time according to calculation from selected speed and angle. Finally the message is sent to the server to make the actual motor shaft movement:

ws.send(message.angle.toString() + ";" + message.speed.toString());

to the server.

 

//pointer rotation
function rotateElement(originX,originY,towardsX,towardsY){
  if (in_transition==0)
  {
    var degrees = Math.atan2(towardsY-originY,towardsX-originX)*180/Math.PI -90;
    degrees = Math.round(degrees/step)*step		//quantization
	
    if (degrees<=0)
    {
      curr_angle = Math.abs(degrees);
    }
    else
    {
      curr_angle = 360-degrees;
    }

    gpointers.filter("#pointer_ghost").attr("transform",function(d,i){
      d.a =  curr_angle;
      return "rotate("+ (360 - d.a ) +")";		
    });

    if (window.event.type == "click")
    {
      gpointers.filter("#pointer")
        .transition()
        .each("end",function(){in_transition=0;})
        .each("start",function(){in_transition=1;})
        .duration(function(d){
          var delta = Math.abs(d.a - curr_angle);
          if (delta<=180)
          {
            trans_duration = 1000*message.speed*delta/step;
          }
          else
          {
            trans_duration = 1000*message.speed*(360 - delta)/step;
          }
          return trans_duration;
        })
        .ease("linear")
        .attr("transform",function(d) {
          d.a =  curr_angle;
          message.angle = curr_angle;
          ws.send(message.angle.toString() + ";" + message.speed.toString());
          return "rotate("+ (360 - d.a ) +")";
        });
      }
    }
}

//...there is no more code to add.

The final result is presented below. Click on in to see how it works.

Stepper motor control


 
Share:  Add to Facebook Tweet This Add to Delicious Submit to Digg Stumble This