Skip to content

Commit

Permalink
Add beginning of calibration logic to page.
Browse files Browse the repository at this point in the history
  • Loading branch information
tdicola committed Sep 10, 2013
1 parent bee5dd1 commit bd2a921
Show file tree
Hide file tree
Showing 13 changed files with 866 additions and 267 deletions.
56 changes: 55 additions & 1 deletion README.md
@@ -1,7 +1,61 @@
# Raspberry Pi Cat Laser Toy
Raspberry Pi-based web application to monitor the state of a rice cooker.

Demo of a Raspberry Pi-based cat laser toy that can be controlled through the web.

See the demo on Adafruit's show and tell show:

<iframe width="560" height="315" src="//www.youtube.com/embed/aKMIensR_Lc" frameborder="0" allowfullscreen></iframe>

## Hardware

* Raspberry Pi connected to a [PCA9685-based servo controller](http://www.adafruit.com/products/815). See also [this tutorial on using a servo controller with a Raspberry Pi](http://learn.adafruit.com/adafruit-16-channel-servo-driver-with-raspberry-pi/hooking-it-up).

* Two [servos](http://www.adafruit.com/products/169) glued to each other at a right angle so one servo controls rotation left/right and the other controls rotation up/down.

<a href="http://imgur.com/KukjeZu" title="Mobile Upload"><img src="http://i.imgur.com/KukjeZus.jpg" title="Hosted by imgur.com" alt="Mobile Upload"/></a>

* Laser diode glued to the servo. You can [buy one](http://www.adafruit.com/products/1054) or scavenge one from a laser pointer (what I chose to do).

* Network video camera that outputs an MJPEG video stream. I use a [this Wansview brand camera](http://www.amazon.com/Wansview-Wireless-Surveillance-Microphone-monitoring/dp/B003LNZ1L6/ref=sr_1_1?ie=UTF8&qid=1378666733&sr=8-1&keywords=wansview), but other brands [like Foscam have MJPEG streams](http://www.ispyconnect.com/man.aspx?n=foscam).

### Note about cameras and video streams

You can potentially use other cameras like a webcam or even Raspberry Pi camera but you will need to be careful about the latency and display of the video in a web application. I tried using H.264 encoded video streamed from both an iPhone and webcam through services such as Ustream, Livestream, and even Amazon CloudFront's flash media server. Unfortunately in all cases the latency of the video stream was extremely high, on the order of 10-15 seconds. High latency makes the control of laser over the web impossible.

Furthermore if you use a video stream that must be embedded in a web page with an iframe or Flash object (like Ustream, Livestream, Youtube, etc.) you will not easily be able to control targeting from directly clicking on the video. The problem is that web browsers enforce a strict cross-domain security model where click and mouse move events (among others) over an iframe are not visible to the parent web page. However I found using an MJPEG stream in an img tag had none of these restrictions and you can detect click events, mouse locations, etc. on the video stream image.

## Software

The following software needs to be installed on the Raspberry-Pi:

* Python 2.7

* [Flask](http://flask.pocoo.org/)

Can be installed on Raspbian with pip using these commands:

sudo apt-get install python-pip
sudo pip install flask

* [Adafruit Raspberry Pi Code installed](http://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/adafruit-pi-code) and the Adafruit\_PWM\_Servo\_Driver.py and Adafruit\_I2C.py files copied into the same directory as the pi cat laser code.

* [I2C setup on the Raspberry Pi](http://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/configuring-i2c)

## Setup and Usage

### Testing the application outside the Raspberry Pi

For convenience the server can be run from outside the Raspberry Pi on a Windows/Linux/Mac machine (with Python and Flask installed) by executing:

python server.py test

The test parameter will instruct the server to use a mock servo control class which does not depend on Raspberry Pi-specific libraries and functions.

## Future


## License

This work is licensed under a [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/deed.en_US).

<a rel="license" href="http://creativecommons.org/licenses/by/3.0/deed.en_US"><img alt="Creative Commons License" style="border-width:0" src="http://i.creativecommons.org/l/by/3.0/88x31.png" /></a>
46 changes: 11 additions & 35 deletions model.py
@@ -1,58 +1,34 @@
class LaserModel(object):
def __init__(self, servos):
def __init__(self, servos, servoMin, servoMax, servoCenter):
self.servos = servos
self.axisMin = 150
self.axisMax = 650
# X axis points up/down
self.setXAxis(400)
# Y axis points left/right
self.setYAxis(400)
# Setup corners of targeting box
self.targetXBounds = (0.0, 240.0)
self.targetYBounds = (0.0, 320.0)
self.laserXBounds = (150.0, 650.0)
self.laserTopYBounds = (200.0, 600.0)
self.laserBottomYBounds = (150.0, 650.0)
self.servoMin = servoMin
self.servoMax = servoMax
self.setXAxis(servoCenter)
self.setYAxis(servoCenter)
self.targetCalibration = None
self.servoCalibration = [{'x': servoMin, 'y': servoMin}, {'x': servoMax, 'y': servoMin}, {'x': servoMax, 'y': servoMax}, {'x': servoMin, 'y': servoMax}]

def validateAxis(self, value):
try:
v = int(value)
if v < self.axisMin or v > self.axisMax:
if v < self.servoMin or v > self.servoMax:
raise ValueError()
return v
except:
raise ValueError('Invalid value! Must be a number between %i and %i.' % (self.axisMin, self.axisMax))
raise ValueError('Invalid value! Must be a value between %i and %i.' % (self.servoMin, self.servoMax))

# X axis servo rotation points the laser up/down
def setXAxis(self, value):
self.x_axis_value = self.validateAxis(value)
self.servos.setXAxis(self.x_axis_value)

def getXAxis(self):
return self.x_axis_value

# Y axis servo rotation points the laser left/right
def setYAxis(self, value):
self.y_axis_value = self.validateAxis(value)
self.servos.setYAxis(self.y_axis_value)

def getYAxis(self):
return self.y_axis_value

def interpolate(self, x, xbounds, x1bounds):
"""
Linear interpolation of point x along xmin, xmax to a new point
between x1min, x1max.
"""
return x1bounds[0] + (x1bounds[1] - x1bounds[0])*((x - xbounds[0])/(xbounds[1] - xbounds[0]))

def target(self, x, y):
"""
Map from the targeting coordinate space (rectangle 320x240) to the
laser coordinate space (parrallelogram wider at top than bottom).
"""
laserx = self.interpolate(float(x), self.targetXBounds, self.laserXBounds)
# Interpolate the Y bounds based on how far up/down the laser is targeting
laserYBounds = (self.interpolate(float(x), self.targetXBounds, (self.laserTopYBounds[0], self.laserBottomYBounds[0])),
self.interpolate(float(x), self.targetXBounds, (self.laserTopYBounds[1], self.laserBottomYBounds[1])))
lasery = self.interpolate(float(y), self.targetYBounds, laserYBounds)
self.setXAxis(laserx)
self.setYAxis(lasery)
22 changes: 0 additions & 22 deletions modeltests.py
Expand Up @@ -30,28 +30,6 @@ def test_axis_defaults_to_400(self):
assert self.servos.xaxis == 400
assert self.servos.yaxis == 400

def test_interpolate(self):
assert self.model.interpolate(0.0, (0.0, 100.0), (200.0, 600.0)) == 200.0
assert self.model.interpolate(100.0, (0.0, 100.0), (200.0, 600.0)) == 600.0
assert self.model.interpolate(50.0, (0.0, 100.0), (200.0, 600.0)) == 400.0

def test_target(self):
# Assumes:
# self.targetXBounds = (0.0, 240.0)
# self.targetYBounds = (0.0, 320.0)
# self.laserXBounds = (150.0, 650.0)
# self.laserTopYBounds = (150.0, 650.0)
# self.laserBottomYBounds = (200.0, 600.0)
self.model.target(120, 160)
assert self.servos.xaxis == 400
assert self.servos.yaxis == 400
self.model.target(0, 0)
assert self.servos.xaxis == 150
assert self.servos.yaxis == 150
self.model.target(240, 320)
assert self.servos.xaxis == 650
assert self.servos.yaxis == 600


class TestServos(object):
def __init__(self):
Expand Down
25 changes: 0 additions & 25 deletions photocell.py

This file was deleted.

87 changes: 50 additions & 37 deletions server.py
@@ -1,68 +1,81 @@
# TODO:
# - error checking tests for target?
# - clean up set and target functions below so they don't duplicate code
# - bug: need correct bounds error message when target fails because input is outside range

from flask import *
import json, sys

import model

# Flask app configuration
DEBUG = True

# Cat laser toy configuration
SERVO_I2C_ADDRESS = 0x40 # I2C address of the PCA9685-based servo controller
SERVO_XAXIS_CHANNEL = 0 # Channel for the x axis rotation which controls laser up/down
SERVO_YAXIS_CHANNEL = 1 # Channel for the y axis rotation which controls laser left/right
SERVO_PWM_FREQ = 50 # PWM frequency for the servos in HZ (should be 50)
SERVO_MIN = 150 # Minimum rotation value for the servo, should be -90 degrees of rotation.
SERVO_MAX = 650 # Maximum rotation value for the servo, should be 90 degrees of rotation.
SERVO_CENTER = 400 # Center value for the servo, should be 0 degrees of rotation.

# Initialize flask app
app = Flask(__name__)
app.config.from_object(__name__)

# Setup the servo and laser model
servos = None
if len(sys.argv) > 1 and sys.argv[1] == "test":
# Setup test servo for running outside a Raspberry Pi
import modeltests
servos = modeltests.TestServos()
else:
# Setup the real servo when running on a Raspberry Pi
import servos
servos = servos.Servos()
servos = servos.Servos(SERVO_I2CADDRESS, SERVO_XAXIS_CHANNEL, SERVO_YAXIS_CHANNEL, SERVO_PWM_FREQ)

model = model.LaserModel(servos, SERVO_MIN, SERVO_MAX, SERVO_CENTER)

model = model.LaserModel(servos)
# Setup application views and API's below

# Define views
# Main view for rendering the web page
@app.route('/')
def main():
return render_template('main.html', model=model)

@app.route('/set/xaxis/<xaxis>', methods=['PUT'])
def setXAxis(xaxis):
try:
model.setXAxis(xaxis)
except ValueError as e:
return jsonify({'result': e.message}), 500
# Error handler for API call failures
@app.errorhandler(ValueError)
def value_error_handler(error):
return jsonify({'result': error.message}), 500

# REST API to set x axis servo rotation
@app.route('/set/servo/xaxis/<xaxis>', methods=['PUT'])
def setServoXAxis(xaxis):
model.setXAxis(xaxis)
return jsonify({'result': 'success'}), 204

# REST API to set y axis servo rotation
@app.route('/set/servo/yaxis/<yaxis>', methods=['PUT'])
def setServoYAaxis(yaxis):
model.setYAxis(yaxis)
return jsonify({'result': 'success'}), 204

@app.route('/set/yaxis/<yaxis>', methods=['PUT'])
def setYAxis(yaxis):
try:
model.setYAxis(yaxis)
except ValueError as e:
return jsonify({'result': e.message}), 500
# REST API to set both x and y axis servo rotation
@app.route('/set/servos/<xaxis>/<yaxis>', methods=['PUT'])
def setServos(xaxis, yaxis):
model.setXAxis(xaxis)
model.setYAxis(yaxis)
return jsonify({'result': 'success'}), 204

@app.route('/setboth/<xaxis>/<yaxis>', methods=['PUT'])
def setBoth(xaxis, yaxis):
try:
model.setXAxis(xaxis)
model.setYAxis(yaxis)
except ValueError as e:
return jsonify({'result': e.message}), 500
return jsonify({'result': 'sucssess'}), 204

@app.route('/get', methods=['GET'])
def get():
# REST API to read x and y axis servo rotation
@app.route('/get/servos', methods=['GET'])
def getServos():
return jsonify({'xaxis': model.getXAxis(), 'yaxis': model.getYAxis() }), 200

@app.route('/target/<xaxis>/<yaxis>', methods=['PUT'])
def target(xaxis, yaxis):
try:
model.target(xaxis, yaxis)
except ValueError as e:
return jsonify({'result': e.message}), 500
@app.route('/get/calibration', methods=['GET'])
def getCalibration():
return jsonify({'target': model.targetCalibration, 'servo': model.servoCalibration}), 200

@app.route('/set/calibration', methods=['POST'])
def setCalibration():
model.targetCalibration = json.loads(request.form['targetCalibration'])
model.servoCalibration = json.loads(request.form['servoCalibration'])
return jsonify({'result': 'success'}), 204

# Start running the flask app
Expand Down
12 changes: 7 additions & 5 deletions servos.py
@@ -1,12 +1,14 @@
from Adafruit_PWM_Servo_Driver import PWM

class Servos(object):
def __init__(self):
self.pwm = PWM(0x40, debug=True)
self.pwm.setPWMFreq(50)
def __init__(self, i2cAddress, xAxisChannel, yAxisChannel, pwmFreqHz):
self.pwm = PWM(i2cAddress, debug=True)
self.pwm.setPWMFreq(pwmFreqHz)
self.xaxis = xAxisChannel
self.yaxis = yAxisChannel

def setXAxis(self, value):
self.pwm.setPWM(0, 0, value)
self.pwm.setPWM(self.xaxis, 0, value)

def setYAxis(self, value):
self.pwm.setPWM(1, 0, value)
self.pwm.setPWM(self.yaxis, 0, value)
13 changes: 13 additions & 0 deletions static/css/main.css
@@ -0,0 +1,13 @@
#video {
position: relative;
top: 0px;
left: 0px;
z-index: 1;
}

#calibrateLayer {
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
}

0 comments on commit bd2a921

Please sign in to comment.