Monday, 13 July 2015

Wi-Fi-controlled Armdroid

Back during Easter Bytes at TNMOC, a visitor to the museum asked if it's possible to control the Armdroid on display using their Mobile Phone or Tablet Device...   This got me thinking, and decided to hack something together for the following weekend...

If your interested in controlling your Armdroid using Wi-Fi or across the internet, here's how you can setup your own stand-alone, web-enabled Armdroid, using the Arduino Yun.   When you're done, you'll be able to control your Armdroid using any web-browser, or control programmatically over the internet using Python or other scripting languages.

What exactly is the Arduino Yun you might be asking.... It's basically a combination of a classic Arduino Leonardo (based on the ATmega32U4 microcontroller) with a WiFi system-on-a-chip Atheros AR9331 running Linino (a MIPS GNU/Linux distribution based on OpenWrt).  The two processing units are connected together using the Bridge library allowing you to combine the power of Linux with ease of Arduino.

OpenWrt supports REST services for clients and servers.  REST is an acronym for "Representational State Transfer".  It is a software architecture that exposes functionality through URLs.  REST has gained widespread acceptance across the World Wide Web as a simple alternative to SOAP and WSDL-based web services.   A nice introduction to the concepts behind REST can be found here.

This project implements an Armdroid REST API allowing functions of the robotic arm to be manipulated through URLs.  I've prepared a simple web page that consumes this service to get you going, although you could easily interact with this from say, a Raspberry PI using the CURL library, etc.


The photographs above shows the project from Easter Bytes.  In this arrangement, we configured the Yun's WiFi as a standalone Access Point - visitors would simply connect to this network using a Mobile Phone web-browser, then take control.   This actually proved to be a real hit with many visitors to the museum, and was especially rewarding to get usability feedback from a 7-year old! (pictured)

You can see the simplicity of the set-up in the following photographs:

I've added the source code for this project to the Armdroid Library examples directory  https://github.com/Armdroid/Armdroid-Arduino-Library

If you wish to use the sample web page, you'll need to prepare a memory card by creating the directory structure "arduino/www" which ensures the Yun will create a link to the SD card "/mnt/sd" path.

The REST API is structured around verbs and Armdroid functions - for example, you want to move the shoulder stepper motor x steps, you would simply issue an HTTP web request like http://arduino/armdroid/shoulder/position/x.   Likewise, other Armdroid functions are described by their function - base, elbow, gripper etc.

If you wish to determine what is the current location for any stepper motor, you would use a URL such as http://arduino/armdroid/shoulder/position (without position value) and this will return in the response the offset counter for this channel.

The complete REST API comprises of the following URL structure:
  • http://myArduinoYun.local/arduino/armdroid/base/position : returns base motor offset counter
  • http://myArduinoYun.local/arduino/armdroid/base/position/x : rotates base motor x steps clockwise or counterclockwise
  • http://myArduinoYun.local/arduino/armdroid/base/position/x/y : rotates base motor x steps clockwise or counterclockwise at y revolutions-per-second
  • http://myArduinoYun.local/arduino/armdroid/base/sensor : returns base sensor reading
  • http://myArduinoYun.local/arduino/armdroid/shoulder/position : returns shoulder motor offset counter
  • http://myArduinoYun.local/arduino/armdroid/shoulder/position/x : rotates shoulder motor x steps clockwise or counterclockwise
  • http://myArduinoYun.local/arduino/armdroid/shoulder/position/x/y : rotates shoulder motor x steps clockwise or counterclockwise at y revolutions-per-second
  • http://myArduinoYun.local/arduino/armdroid/shoulder/sensor : returns shoulder sensor reading
  • http://myArduinoYun.local/arduino/armdroid/elbow/position : returns elbow motor offset counter
  • http://myArduinoYun.local/arduino/armdroid/elbow/position/x : rotates elbow motor x steps clockwise or counterclockwise
  • http://myArduinoYun.local/arduino/armdroid/elbow/position/x/y : rotates elbow motor x steps clockwise or counterclockwise at y revolutions-per-second
  • http://myArduinoYun.local/arduino/armdroid/elbow/sensor : returns elbow sensor reading
  • http://myArduinoYun.local/arduino/armdroid/wrist/pitch: returns both LHS and RHS wrist offset counters
  • http://myArduinoYun.local/arduino/armdroid/wrist/pitch/x : counter-rotates wrist motors to pitch gripper up/down
  • http://myArduinoYun.local/arduino/armdroid/wrist/pitch/x/y : counter-rotates wrist motors to pitch gripper up/down at y revolutions-per-second
  • http://myArduinoYun.local/arduino/armdroid/wrist/rotate : returns both LHS and RHS wrist offset counters
  • http://myArduinoYun.local/arduino/armdroid/wrist/rotate/x : rotates wrist motors to roll gripper clockwise/counterclockwise
  • http://myArduinoYun.local/arduino/armdroid/wrist/rotate/x/y : rotates wrist motors to roll gripper clockwise/counterclockwise at y revolutions-per-second
  • http://myArduinoYun.local/arduino/armdroid/wrist/left/position : returns LHS wrist offset counter
  • http://myArduinoYun.local/arduino/armdroid/wrist/left/position/x : rotates LHS wrist motor x steps clockwise or counterclockwise
  • http://myArduinoYun.local/arduino/armdroid/wrist/left/position/x/y : rotates LHS wrist motor x steps clockwise or counterclockwise at y revolutions-per-second
  • http://myArduinoYun.local/arduino/armdroid/wrist/left/sensor : returns LHS wrist sensor reading
  • http://myArduinoYun.local/arduino/armdroid/wrist/right/position : returns RHS wrist offset counter
  • http://myArduinoYun.local/arduino/armdroid/wrist/right/position/x : rotates RHS wrist motor x steps clockwise or counterclockwise
  • http://myArduinoYun.local/arduino/armdroid/wrist/right/position/x/y : rotates RHS wrist motor x steps clockwise or counterclockwise at y revolutions-per-second
  • http://myArduinoYun.local/arduino/armdroid/wrist/right/sensor : returns RHS wrist sensor reading
  • http://myArduinoYun.local/arduino/armdroid/gripper/position : return gripper motor offset counter
  • http://myArduinoYun.local/arduino/armdroid/gripper/position/x : rotates gripper motor to open/close fingers
  • http://myArduinoYun.local/arduino/armdroid/gripper/position/x/y : rotates gripper motor to open/close fingers at y revolutions-per-second
  • http://myArduinoYun.local/arduino/armdroid/gripper/sensor : returns gripper motor offset counter
  • http://myArduinoYun.local/arduino/armdroid/torque/enabled : returns a value indicating torque has been applied to all motors
  • http://myArduinoYun.local/arduino/armdroid/torque/enabled/x : enables/disables torque
  • http://myArduinoYun.local/arduino/armdroid/home : returns to home (starting) position
All responses are returned using JSON (JavaScript Object Notation) formatted replies allowing simplified parsing in many programming languages.

If your interested to understand how the code works - you need to first understand how the Bridge example works as a starting point - see https://www.arduino.cc/en/Tutorial/Bridge

Basically, each of the six channels (base/shoulder/elbow/left wrist/right wrist/gripper) are implemented using a command method as follows:

 void baseCommand(YunClient client)  
 {  
  String baseCmd = client.readStringUntil('/');  
  if (baseCmd.startsWith("position")) {  
   // read number of steps, if none have been specified, parseInt()  
   // will simply return zero which will be ignored, and we'll  
   // finish by feeding back current position to the client.  
   const int steps = client.parseInt();  
   if (steps != 0) {  
    // if the URL includes a speed value, use it  
    int whatSpeed = DEF_MOTOR_SPEED;  
    if (client.read() == '/') {  
     const int speedInRpm = client.parseInt();  
     if (speedInRpm > 0)  
      whatSpeed = speedInRpm;  
    }  
    // drive motor to rotate base clockwise/counterclockwise  
    driveMotor( ARMDROID_BASE_CHANNEL, steps, whatSpeed );  
   }  
   // send feedback to client  
   client.print(F("{\"base-position\":"));  
   client.print(getPositionReading( ARMDROID_BASE_CHANNEL ));  
   client.println(F("}"));  
  }  
  else if (baseCmd.startsWith("sensor")) {  
   client.print(F("{\"base-sensor\":"));  
   client.print(getSensorReading( ARMDROID_BASE_CHANNEL ));  
   client.println(F("}"));  
  }  
  else  
   client.println(F("ERROR: invalid base command"));  
 }  

The code begins by reading the client URL and tries to match the "position" command.  If this is present, we can then determine if the URL is specifying a value representing the position in which to drive the motors, or alternatively, we'll simply report on the current position.  If we do have a valid "position" command then we look to see if a speed has been specified.  If this is not the case, the default (120 RPM) will be used instead.

If the command matches "sensor" then we have the ability to return the current sensor reading for the given channel.

In either case, after calling the Armdroid Library to perform the desired function, we send feedback to the client such as the new position after the operation completes.


Limitations

Yun's web server times-out after approximately 5seconds when performing lengthy Armdroid operations.  This is mostly due to the design of the current Armdroid Library which is synchronous in operation, or blocking, when running motors for long periods of time.  In this situation, a response doesn't get returned from the microcontroller before the web server gives up, and returns 500 (Internal Server Error) back to the client.

This isn't immediately obvious using the example web page, although can be easily demonstrated when browser debugging is enabled:
Fig 1. short base rotation (200 steps)
Issuing the request /arduino/armdroid/base/position/200 rotates the base stepper motor 200 steps resulting in HTTP status code 200 (OK) returned.  You can see the reply includes our JSON response indicating the position (current offset counter) of this motor channel after performing the operation.

Now observe what happens when the request is made with 2000 steps:
Fig 2. lengthy base rotation (2,000 steps)
HTTP status code 500 is returned after 5seconds, and before the motor has finished running.  By the time the motor reaches the target position, the connection no longer exists, no response is returned.


We'll revisit this when Asynchronous enhancements have been added to the library, but in the meantime, you can work around this by calling the API to retrieve the current position and wait if necessary until the previous operation has completed.

The sample web page included with the example is very basic...  It serves purely as a demonstration only... if you want something with more sex appeal, you might want to look at DoJo or similar frameworks to create a richer experience.