Video
This is what it looks like:
Components
I picked up robotics when I attended the Institute for Personal Robotics in Education (IPRE) workshop at Georgia Tech.
The IPRE workshop meta-teaches programming using robotics.
The IPRE platform has two components:
(If you don't have Bluetooth, you'll want a Bluetooth USB adapter too.)
Instructions for set up and install under Mac, Linux or Windows are available on the IPRE wiki.
The principles of the iPhone multitouch remote in this article are general, and adapt to other robotics platforms like LEGO Mindstorms NXT.
My code should work on another robotics platform with some modifications. The iOS app should also work on many Android phones.
Basics: A higher-order Cha Cha
The warm-up task from the workshop gives a sense of how easy it is to program a Scribbler+Fluke system.
The first task was to teach the robot to dance.
After half an hour of playing with it, mine was doing this:
The code is simple, and it even shows off higher-orderness:
def courtsey(): turnLeft(1,0.05) turnRight(1,0.05) turnRight(1,0.05) turnLeft(1,0.05) def chachacha(action): action(0.7) beep(0.05,800) stop() action(0.7) beep(0.05,900) stop() action(0.9) beep(0.2,800) stop() curtsey() chachacha(forward) curtsey() chachacha(backward) turnLeft(0.5,0.5) chachacha(forward)
The movement primitives for actions like rotation and translation are uniform: all take an amount and a duration.
As a result, the procedure chachacha
, which encodes a beat,
can "chachachaify" any movement, like turnRight
or
backward
.
Multitouch iOS and web-based remotes
In the workshop, we each created an assignment we could give to students.
Since childhood, I've been fascinated by the prospect of having a home full of semi-autonomous minions. (No, children don't count.)
Controlling a robot with multitouch gestures seemed like a fun assignment.
I broke it into two parts:
- an httpd that controls the robot; and
- an iOS app in JavaScript that talks to the HTTPD.
Here's the interface for the iPhone app when it starts up:
To go forward or back, stick your finger down and drag it forward or back.
To rotate, touch two fingers and twist in the direction you want it to rotate.
When you lift your fingers, it stops moving.
To view the camera, rotate the screen.
(It wouldn't be hard to make it a continuously updated image stream.)
Web server
The web server uses BaseHTTPServer
to handle HTTP GET requests.
The httpd responds to requests for
/move
, /spin
and /stop
by controlling the robot.
The query parameters determine how fast the robot should move.
The file /cam.gif
contains whatever is in front of the robot
at that moment.
from myro import * from os import curdir, sep from urlparse import urlparse, parse_qs from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer # Configuration SerialPort = "/dev/tty.IPRE6-365906-DevB" # Determine the mode: Test or Real Mode = "test" if len(sys.argv) > 1 and \ sys.argv[1] == "--test" else "real" if Mode == "real": print "Mode: Real" init(SerialPort) # Handlers for commands: def handle_move(query): speed = float(query['speed'][0]) print "move(%s)" % speed if Mode == "real": if speed < 0.0: backward(-speed) else: forward(speed) def handle_spin(query): speed = float(query['speed'][0]) print "spin(%s)" % speed if Mode == "real": if speed < 0.0: turnLeft(-speed) else: turnRight(speed) def handle_stop(query): print "stop()" if Mode == "real": stop() # Custom HTTP request handler: class ActionHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/": self.path = "/index.html" url = urlparse(self.path) action = url.path[1:] print "action: ", action query = parse_qs(url.query) print "query: ", query # Send back an image, if requested: if url.path == "/cam.gif": if (Mode == "real"): p = takePicture() savePicture(p,"cam.gif") if action == "move": handle_move(query) elif action == "spin": handle_spin(query) elif action == "stop": handle_stop(query) else: # grab a file try: print "sending file: ", url.path f = open(curdir + sep + url.path) self.send_response(200) if url.path.endswith('.html'): self.send_header('Content-type', 'text/html') elif url.path.endswith('.js'): self.send_header('Content-type', 'text/javascript') elif url.path.endswith('.gif'): self.send_header('Content-type', 'image/gif') self.end_headers() self.wfile.write(f.read()); f.close() return except IOError: self.send_error(404,'File Not Found: %s' % self.path) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write('OK: ' + self.path) ; try: server = HTTPServer(('0.0.0.0', 1701), ActionHandler) print 'Awaiting commands...' server.serve_forever() except KeyboardInterrupt: print 'User terminated server' server.socket.close()
iOS remote app
The iOS app watches for dragging and twisting gestures.
It turns those multitouch gestures into asynchronous web requests that tell the robot what to do.
var UpdatePeriod = 300 ; // milliseconds var LastTouch = 0 ; // The Y coordinate when the finger goes down: var InitY = 0 ; // Is the user gesturing or touching? var InGesture = false ; // Last and current command: var LastCommand = {"action": "stop"} ; var Command = LastCommand ; var Pad = document.getElementById("pad") ; function HttpSend(url) { var req = new XMLHttpRequest() ; req.open("GET", url, true) ; req.send(null) ; } // Called periodically to issue commands: function UpdateRobot() { if ((LastCommand.action == Command.action) && (LastCommand.speed == Command.speed)) return ; Pad.innerHTML = Command.action + ": " + Command.speed ; switch (Command.action) { case "move": case "spin": HttpSend("/"+Command.action+"?speed=" + encodeURIComponent(Command.speed)) ; break ; case "stop": HttpSend("/stop") ; break ; } LastCommand = Command ; } // Check for new actions at regular intervals: setInterval(UpdateRobot,UpdatePeriod) ; // Watch for touches and gestures: Pad.ontouchstart = function (event) { var finger = event.touches[0] ; InitY = finger.clientY ; var newTouch = (new Date()).getTime() ; LastTouch = newTouch ; } ; document.body.onorientationchange = function (event) { Pad.style.backgroundImage = "url(./cam.gif?r="+Math.random()+")" ; } ; Pad.ontouchend = function (event) { Command = {"action": "stop"} ; var finger = event.touches[0] ; } ; Pad.ontouchmove = function (event) { if (InGesture) return ; var finger = event.touches[0] ; var speed = (InitY - finger.clientY) / window.innerHeight ; Command = {"action": "move", "speed": speed} ; } ; Pad.ongesturestart = function (event) { InGesture = true ; } ; Pad.ongestureend = function (event) { InGesture = false ; Command = {"action": "stop"} ; } ; Pad.ongesturechange = function (event) { var rotation = event.rotation ; if (rotation < -180.0) rotation += 360.0 ; if (rotation > 180.0) rotation -= 360.0 ; Command = {"action": "spin", "speed": rotation / 180.0} ; } ;
HTML interface
Grab the code.
Exercises
If you'd like some exercises, try out:
- It's easy to extend this code to send a live stream of images back.
- You could have the app switch to an alternate control mode in landscape mode. Each thumb could control a wheel independently, for fine-grained control over the robot's movement.
- Currently, the iOS app syncs with the robot three times a second. Bumping up the frequency improves responsiveness, but bumping it too high floods the robot. You should modulate the frequency dynamically, based on response times to HTTP requests, so that it settles around the best possible update frequency.
More resources
- IPRE wiki.
- The Scribbler robot kit on amazon.
- The Fluke plug-in card on amazon.
- LEGO Mindstorms NXT.
- HOWTO: Create native-looking iOS apps in JavaScript and HTML.