Monday, December 20, 2010

An aside: Twitter and RSS

The Department of Engineering twitter news feed has been dead since the autumn, when Twitter ended simple authentication in favour of OAuth. I've just resurrected it (the sort of job I get round to doing in the week before Christmas), and here's how (for the OAuth part I found this blog post very helpful - there are various broken and/or poorly-documented attempts to link Python, OAuth and Twitter around the web).
  1. Sign in to Twitter, and go to http://dev.twitter.com/ - then "Your apps" (top of the page) and register a new application.
  2. Assuming you've registered correctly, the next page will include a consumer key (a long alphanumeric string) and a consumer secret (an even longer alphanumeric string). Put these in a text file called consumer_keys.txt.
  3. Go to "My Access Token" (right hand side of the page) to get your access token. Two long strings again, put these in a text file called access_token.txt.
  4. Go to http://bit.ly, sign in, and then http://bit.ly/a/your_api_key
    - put your bit.ly username and this API key in a text file called bitly.txt
  5. Install tweepy and feedparser. (Some useful info on feedparser here).
  6. Code is below (note that I have shortened the RSS URL to prevent it running off the screen on this blog entry - in the actual program it needs to be the full RSS URL). I now just need to add a cron job to my PC, and it will check the website's news RSS feed and update Twitter periodically. You could, of course, simply put the various API and OAuth keys directly into the code, instead of having them in their own files.

#!/usr/bin/python

import tweepy
import feedparser
import urllib
import urllib2

rss_url = "http://www2.le.ac.uk/.../RSS"

oauth_file = open("access_token.txt", "r")
oauth_token = oauth_file.readline().rstrip()
oauth_token_secret = oauth_file.readline().rstrip()

consumer_file = open("consumer_keys.txt", "r")
consumer_key = consumer_file.readline().rstrip()
consumer_secret = consumer_file.readline().rstrip()

bitly_file = open("bitly.txt", "r")
bitly_username = bitly_file.readline().rstrip()
bitly_apikey = bitly_file.readline().rstrip()

bitly_base = "http://api.bit.ly/v3/shorten?"
bitly_data = {
"login" : bitly_username,
"apiKey" : bitly_apikey,
"format" : "txt",
"longUrl" : ""
}

already_done = []
done_file = open("done.txt", "r")
for line in done_file:
already_done.append(line.rstrip())
done_file.close()

auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(oauth_token, oauth_token_secret)

api = tweepy.API(auth)
feed = feedparser.parse(rss_url)

for item in feed["items"]:
url = item["link"]
title = item["title"]
if url not in already_done:
bitly_data["longUrl"] = url
to_shorten = bitly_base + urllib.urlencode(bitly_data)
result = urllib2.urlopen(to_shorten).read()
api.update_status(title + " : " + result)
already_done.append(url)

done_file = open("done.txt", "w")
for url in already_done:
done_file.write(url + "\n")
done_file.close()


Thursday, December 16, 2010

Callback function

Just a quick bit of code to show how the callback function works - 1+2 to connect, then it starts streaming accelerometer and (if available) motionplus data. Press the home button to end.

I'm using two global variables - loop to keep the main loop running, and callback_active to make sure that when you do press the home button, the loop doesn't terminate while the callback function is doing something (I was getting all sorts of exceptions and segmentation faults before including this).

#!/usr/bin/python

import cwiid

loop = True
callback_active = False

def main():
print "Press 1+2"
wiimote = cwiid.Wiimote()
print "OK"

wiimote.mesg_callback = callback
wiimote.enable(cwiid.FLAG_MESG_IFC | cwiid.FLAG_MOTIONPLUS)
wiimote.rpt_mode = cwiid.RPT_ACC | cwiid.RPT_BTN | cwiid.RPT_MOTIONPLUS

global loop
global callback_active
while loop or callback_active:
# Messages will be sent to callback function
pass

wiimote.disable(cwiid.FLAG_MESG_IFC | cwiid.FLAG_MOTIONPLUS)

#----------------------------------------------------------------------
def callback (mesg_list, time):
global callback_active
callback_active = True
for (message, data) in mesg_list:
if message == cwiid.MESG_ACC:
print data
elif message == cwiid.MESG_MOTIONPLUS:
print data
elif message == cwiid.MESG_BTN:
if data & cwiid.BTN_HOME:
global loop
loop = False
else:
pass
callback_active = False

#----------------------------------------------------------------------
main()

Wednesday, December 15, 2010

Translation

Final video of the day - why translation can't be picked up by the motionplus.

Horizontal orientation


I can thoroughly recommend Will Kymlicka's Contemporary Political Philosophy, if you're into that sort of thing. What I mean while I'm waggling my hand around is that an airship shouldn't pitch or roll too much (yaw being rotation in the horizontal plane).

More code

This will assume the wiimote has a motionplus attached - if it doesn't, no cwiid.MESG_MOTIONPLUS events will be recorded, and it will just plot the three graphs for the accelerometers rather than the five for accelerometers and motionplus.


#!/usr/bin/python

import cwiid
from time import time, asctime, sleep
from numpy import *
from pylab import *

def plotter(plot_title, timevector, data, position, n_graphs):
subplot(n_graphs, 1, position)
plot(timevector, data[0], "r",
timevector, data[1], "g",
timevector, data[2], "b")
xlabel("time (s)")
ylabel(plot_title)

print "Press 1+2 on the Wiimote now"
wiimote = cwiid.Wiimote()

# Rumble to indicate a connection
wiimote.rumble = 1
print "Connection established - release buttons"
sleep(0.2)
wiimote.rumble = 0
sleep(1.0)

wiimote.enable(cwiid.FLAG_MESG_IFC | cwiid.FLAG_MOTIONPLUS)
wiimote.rpt_mode = cwiid.RPT_BTN | cwiid.RPT_ACC | cwiid.RPT_MOTIONPLUS

print "Press plus to start recording, minus to end recording"
loop = True
record = False
accel_data = []
angle_data = []

while (loop):
sleep(0.01)
messages = wiimote.get_mesg()
for mesg in messages:
# Motion plus:
if mesg[0] == cwiid.MESG_MOTIONPLUS:
if record:
angle_data.append({"Time" : time(), \
"Rate" : mesg[1]['angle_rate']})
# Accelerometer:
elif mesg[0] == cwiid.MESG_ACC:
if record:
accel_data.append({"Time" : time(), "Acc" : mesg[1]})
# Button:
elif mesg[0] == cwiid.MESG_BTN:
if mesg[1] & cwiid.BTN_PLUS and not record:
print "Recording - press minus button to stop"
record = True
start_time = time()
if mesg[1] & cwiid.BTN_MINUS and record:
if len(accel_data) == 0:
print "No data recorded"
else:
print "End recording"
print "{0} data points in {1} seconds".format(
len(accel_data), time() - accel_data[0]["Time"])
record = False
loop = False
else:
pass

wiimote.disable(cwiid.FLAG_MESG_IFC | cwiid.FLAG_MOTIONPLUS)
if len(accel_data) == 0:
sys.exit()


timevector = []
a = [[],[],[]]
v = [[],[],[]]
p = [[],[],[]]
last_time = 0
velocity = [0,0,0]
position = [0,0,0]

for n, x in enumerate(accel_data):
if (n == 0):
origin = x
else:
elapsed = x["Time"] - origin["Time"]
delta_t = x["Time"] - last_time
timevector.append(elapsed)
for i in range(3):
acceleration = x["Acc"][i] - origin["Acc"][i]
velocity[i] = velocity[i] + delta_t * acceleration
position[i] = position[i] + delta_t * velocity[i]
a[i].append(acceleration)
v[i].append(velocity[i])
p[i].append(position[i])
last_time = x["Time"]

n_graphs = 3

if len(angle_data) == len(accel_data):
n_graphs = 5
ar = [[],[],[]] # Angle rates
aa = [[],[],[]] # Angles
angle = [0,0,0]
for n, x in enumerate(angle_data):
if (n == 0):
origin = x
else:
delta_t = x["Time"] - last_time
for i in range(3):
rate = x["Rate"][i] - origin["Rate"][i]
angle[i] = angle[i] + delta_t * rate
ar[i].append(rate)
aa[i].append(angle[i])
last_time = x["Time"]


plotter("Acceleration", timevector, a, 1, n_graphs)
plotter("Velocity", timevector, v, 2, n_graphs)
plotter("Position", timevector, p, 3, n_graphs)
if n_graphs == 5:
plotter("Angle Rate", timevector, ar, 4, n_graphs)
plotter("Angle", timevector, aa, 5, n_graphs)

show()

Angle

Secondly, I now have a genuine wiimote with the motionplus attachment. This, at the moment, only gives the rotation speed (or rate of change of angle, if you want to put it that way) of the three axes - none of the reverse-engineering projects seem to have been able to get any other information out of it.

Top three graphs are the acceleration, velocity, and position again. The bottom two are the rate of angle change and (by integration again) the angle. What I did with the wiimote was to hold it in a steady orientation, and then twist it (as accurately as I could) 90 degrees around each axis and back again fairly quickly - you can see that in the three humps in the bottom graph. This integrated data seems to be more accurate in some respects than the raw orientation data you get from the accelerometers.


It may look as if I fumble and drop the wiimote at the start of this video. This was entirely intentional. Honest.

Position

Still haven't become a LabVIEW expert, though I did start the computer-based training and there's a fairly good explanation of state transition diagrams which should be useful for the students when they come to design code.

What I have done is to modify the code from here, so I can plot graphs of acceleration, velocity and position. As I thought, nowhere near accurate enough for navigation - although talking to Simon before a meeting this morning, it seems they aren't going to be expected to fly the airship around, just take off, hover and land.

Here's a graph showing what happens when I lift the wiimote off the desk and put it down again, fairly quickly.





The top graph shows acceleration, the middle one velocity, and the bottom one position. I think the best you can say about this is that it's clear that the blue line is the vertical acceleration, and the wiimote started moving about 1.75 seconds after I hit the record button. Calculation errors eventually lead to a bogus velocity in all three directions, even when the device is returned to the rest position - all three velocity lines and the position line should return to zero after about 2.5 seconds.