root/Lego/python/wiimote/Wiimote.py

Revision 16, 16.2 KB (checked in by Jan-Klaas Kollhof, 4 years ago)

added bpa dropzone map

  • Property executable set to True
Line 
1#!/usr/bin/python
2#
3# wiimote.py - Wii Remote data inspector. This will be used as a learning
4# framework until we have enough data to write an actual wiimote driver.
5#
6# Copyright (C) 2007 Will Woods <wwoods@redhat.com>
7#
8# This program is free software; you can redistribute it and/or
9# modify it under the terms of the GNU General Public License
10# as published by the Free Software Foundation; either version 2
11# of the License, or (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21#
22# requires pybluez - http://org.csail.mit.edu/pybluez/
23
24import bluetooth
25import os
26import sys
27import math
28import time
29import fcntl,struct
30from time import strftime,localtime
31from logging import debug
32
33def i2bs(x):
34    '''Convert a (32-bit) int to a list of 4 byte values, e.g.
35    i2bs(0xdeadbeef) = [222,173,190,239]
36    12bs(0x4)        = [0,0,0,4]'''
37    out=[]
38    while x or len(out) < 4:
39        out = [x & 0xff] + out
40        x = x >> 8
41    return out
42
43class WiiDiscoverer(bluetooth.DeviceDiscoverer):
44    def __init__(self,maxdevs=1):
45        bluetooth.DeviceDiscoverer.__init__(self) # init parent
46        self.wiimotes = []
47        self.done = False
48        self.inprogress = False
49        self.maxdevs = maxdevs
50
51    # We identify wiimotes by their device name at the moment
52    def device_discovered(self, address,device_class, name):
53        if not name:
54            name = bluetooth.lookup_name(address)
55
56        if name.startswith('Nintendo RVL-CNT'):
57            debug("Found wiimote at address %s" % address)
58            w=Wiimote(address,len(self.wiimotes))
59            self.wiimotes.append(w)
60            if len(self.wiimotes) == self.maxdevs:
61                self.done = True
62
63    def pre_inquiry(self):
64        self.inprogress = True
65
66    def inquiry_complete(self):
67        self.inprogress = False
68        self.done = True
69
70buttonmap = {
71    '2': 0x0001,
72    '1': 0x0002,
73    'B': 0x0004,
74    'A': 0x0008,
75    '-': 0x0010,
76    'H': 0x0080,
77    'L': 0x0100,
78    'R': 0x0200,
79    'D': 0x0400,
80    'U': 0x0800,
81    '+': 0x1000,
82}
83
84# BLUH. These should be less C-ish.
85CMD_SET_REPORT = 0x52
86RID_LEDS = 0x11
87RID_MODE = 0x12
88RID_IR_EN = 0x13
89RID_SPK_EN = 0x14
90RID_STATUS = 0x15
91RID_WMEM = 0x16
92RID_RMEM = 0x17
93RID_SPK = 0x18
94RID_SPK_MUTE = 0x19
95RID_IR_EN2 = 0x1a
96MODE_BASIC = 0x30
97MODE_ACC = 0x31
98MODE_IR = 0x32
99MODE_FULL = 0x3e
100IR_MODE_OFF =  0
101IR_MODE_STD =  1
102IR_MODE_EXP =  3
103IR_MODE_FULL = 5
104FEATURE_DISABLE = 0x00
105FEATURE_ENABLE = 0x04
106
107# Max value for IR dots
108DOT_MAX = 0x3ff 
109
110def rotate(x,y,theta):
111    '''Rotates the given (x,y) coordinates by theta radians around the center
112    of the dots' view'''
113
114    # Translate dot values so the center is (0,0)
115    c=(DOT_MAX/2)
116    x = c - x
117    y = c - y
118
119    # rotate about the center
120    xprime = x*math.cos(theta) - y*math.sin(theta)
121    yprime = x*math.sin(theta) + y*math.cos(theta)
122
123    # now retranslate
124    xprime = xprime + c
125    yprime = yprime + c
126
127    return (int(xprime),int(yprime))
128
129class Wiimote(object):
130    def __init__(self,addr,number=0):
131        self.connected=False
132        self.done=False
133        self.addr=addr
134        self.number=number
135        self.mode       = 0
136        self.ledmask    = 0
137        self.buttonmask = 0
138        self.force      = [0,0,0]
139        self.force_zero = [0,0,0]
140        self.force_1g   = [0,0,0]
141        self.force_1g_diff = [0,0,0] # Difference between zero and 1g
142        self.theta_g    = 0.0 # Angle of the remote with respect to gravity,
143                              # calculated from the z-axis force. In radians.
144
145        self.theta_g_x  = 0.0 # Same, but calculated from x-axis.
146        self.dots       = [(DOT_MAX,DOT_MAX),(DOT_MAX,DOT_MAX)]
147        self.theta      = 0.0 # dots' angle (again, in rad) from horizontal
148        self.dotlist    = []  # a fifo queue of recent dots
149        self.maxdots    = 10  # max length for dotlist
150        self.pointer = [0,0]  # Location of pointer. range is (0,DOT_MAX)
151        self.rx = bluetooth.BluetoothSocket(bluetooth.L2CAP)
152        self.cx = bluetooth.BluetoothSocket(bluetooth.L2CAP)
153
154    def connect(self):
155        debug("Attaching to Wiimote #%i at %s", self.number+1, self.addr)
156        self.rx.connect((self.addr,19))
157        self.cx.connect((self.addr,17))
158        self.setled(self.number)
159        self.connected=True
160
161    def disconnect(self):
162        debug("Disconnecting from Wiimote #%i", self.number+1)
163        self.cx.close()
164        self.rx.close()
165        self.connected=False
166
167    def mainloop(self):
168        debug("Receiving data from Wiimote #%i", self.number+1)
169
170        while not self.done:
171            self._getpacket()
172            debug("time %f force= %s %s ", time.time(),self.force_str(),self.buttons_str())
173
174    def _handle_button_data(self,data):
175        if len(data) != 4:
176            return False
177
178        # XXX: what's byte 1 for?
179        newmask = (ord(data[2])<<8) + ord(data[3])
180
181        # TODO: check newmask against current mask and send events?
182        if newmask & buttonmap['H'] and not self.buttonmask & buttonmap['H']:
183            debug("Re-enabling IR")
184            self.enable_IR()
185
186        self.buttonmask = newmask
187
188    def _handle_force_data(self,data):
189        if len(data) != 3:
190            return False
191
192        self.force = [ord(d) for d in data]
193        return True
194
195    def _handle_IR_data(self,data):
196        if len(data) != 6:
197            return False
198
199        if data ==' \xff' * 6:
200            self.dots=[(DOT_MAX,DOT_MAX),(DOT_MAX,DOT_MAX)]
201        else:
202            a,b,c,d,e,f = [ord(d) for d in data]
203            # processing dots:
204            # each tuple is 3 bytes in the form: x,y,extra
205            # extra contains 8 bits of extra data as follows: [yyxxssss]
206            # x and y are the high two bits for the full 10-bit x/y values.
207            # s is some unknown info (size data?)
208            x1=a+((c & 0x30) << 4)
209            y1=b+((c & 0xc0) << 2)
210            x2=d+((f & 0x30) << 4)
211            y2=e+((f & 0xc0) << 2)
212            self.dots=[(x1,y1),(x2,y2)]
213            self.dotlist.insert(0,self.dots)
214           
215            if len(self.dotlist) > self.maxdots:
216                self.dotlist.pop()
217        return True
218
219    def _getpacket(self):
220        data=self.rx.recv(1024)
221       
222        if len(data) == 4:    # button
223            self._handle_button_data(data)
224           
225        elif len(data) == 7:  # button + accelerometer
226            self._handle_button_data(data[0:4])
227            self._handle_force_data(data[4:7])
228
229        elif len(data) == 19: # button + accel + IR
230            self._handle_button_data(data[0:4])
231            self._handle_force_data(data[4:7])
232            self._handle_IR_data(data[7:13])
233            # I think the extra data is emitted if we see more than two dots
234            extradata = data[13:19]
235            if extradata != "\xff"*len(extradata):
236                debug("Interesting extradata: %s\n", extradata.encode("hex"))
237
238        elif len(data) == 0:  # Wiimote went away!
239            debug("Lost wiimote #%i",  self.number+1)
240            self.done = True
241        else:
242            debug("Unknown packet len %i: 0x%s" , len(data), data.encode("hex"))
243
244    def setled(self, num):
245        debug("setled(%i)", num)
246
247        if num < 4:
248            self.ledmask = self.ledmask | (0x10 << num)
249            self._led_command()
250
251    def clearled(self,num):
252        debug("clearled(%i)", num)
253
254        if num < 4:
255            self.ledmask = self.ledmask & ~(0x10 << num)
256            self._led_command()
257
258    def buttons_str(self):
259        buttonlist='+UDLRH-AB12'
260        out=''
261        for c in buttonlist:
262            if not self.buttonmask & buttonmap[c]:
263                c = '.'
264            out = out + c
265        return out
266
267    def force_gx(self):
268        return (self.force[0]-self.force_zero[0])
269       
270    def force_gy(self):
271        return (self.force[1]-self.force_zero[1])
272       
273    def force_gz(self):
274        return (self.force[2]-self.force_zero[2])
275
276    def force_str(self):
277        return "% 4i,% 4i,% 4i" % (self.force_gx(), self.force_gy(), self.force_gz())
278
279    def dots_str(self):
280        (a,b),(c,d) = self.dots
281        return "((%4i,%4i),(%4i,%4i))" % (a,b,c,d)
282
283    def setmode(self,mode):
284        self.mode = mode
285        # XXX wiimotulator.py has flags for setting 0x01 in the first byte for
286        # 'rmbl' and 0x04 for 'cont'. Both of these are always off.
287        # No idea why.
288        self._send_command(CMD_SET_REPORT,RID_MODE,[0,mode])
289
290    def enable_force(self):
291        self.setmode(self.mode | MODE_ACC)
292        self.get_force_calibration()
293
294    def enable_IR(self):
295        self.setmode(self.mode | MODE_IR)
296        self._send_command(CMD_SET_REPORT,RID_IR_EN,[FEATURE_ENABLE])
297        self._send_command(CMD_SET_REPORT,RID_IR_EN2,[FEATURE_ENABLE])
298        # Enable IR device
299        self._write_mem(0x04b00030,[0x01])
300        # Set sensitivity constants
301        self._write_mem(0x04b00030,[0x08])
302        self._write_mem(0x04b00006,[0x90])
303        self._write_mem(0x04b00008,[0xc0])
304        self._write_mem(0x04b0001a,[0x40])
305        self._write_mem(0x04b00033,[0x33])
306        # Enable IR data output
307        self._write_mem(0x04b00030,[8])
308
309    def get_force_calibration(self):
310        data=[ord(b) for b in self._read_mem(0x16,10)]
311        self.force_zero = data[0:3]
312        self.force_1g   = data[4:7]
313        # XXX currently we don't know what data[3], data[7], or data[8:9] are
314        debug("Got force calibration data: zero=%s, 1g=%s", self.force_zero, self.force_1g)
315        # Calculate the difference between zero and 1g for each axis
316        for b in range(0,3):
317            self.force_1g_diff[b] = self.force_1g[b] - self.force_zero[b]
318
319    def _led_command(self):
320        self._send_command(CMD_SET_REPORT,RID_LEDS,[self.ledmask])
321
322    def _waitforpacket(self,header,max=32):
323        r=''
324        n=0
325        while (n<max) and not r.startswith(header):
326            r = self.rx.recv(1024)
327            n = n + 1
328       
329        debug("Leaving _waitforpacket() after %i packets",  n)
330       
331        if not r.startswith(header):
332            return None
333        else:
334            return r
335
336    def _waitforok(self):
337        self._waitforpacket('\xa1\x22\x00')
338
339    def _read_mem(self,offset,size):
340        if size >= 16:
341            debug("ERROR: _read_mem can't handle size > 15 yet")
342            return None
343
344        # RMEM command wants: [offset,size]
345        self._send_command(CMD_SET_REPORT,RID_RMEM,i2bs(offset)+[0,size])
346        data = self._waitforpacket('\xa1\x21')
347        if data:
348            # TODO check error flag, continuation, etc
349            return data[7:]
350        else:
351            return None
352
353    def _write_mem(self,offset,data):
354        # WMEM command wants: [offset,size,data]
355        # offset = 32-bit, bigendian. data is 0-padded to 16 bytes.
356        size = len(data)
357        if size > 16:
358            return False # Too much data!
359
360        if size < 16:
361            data = data + [0]*(16-size)
362
363        self._send_command(CMD_SET_REPORT,RID_WMEM,i2bs(offset)+[size]+data)
364        self._waitforok()
365
366    def _send_command(self,cmd,report,data):
367        debug("_send_command(%#x,%#x,%s)", cmd, report, data)
368        self.cx.send(chr(cmd) + chr(report) + "".join([chr(d) for d in data]))
369
370    def calc_theta_g(self):
371        '''Use the z and x accelerometer values to figure out the wiimote's
372        orientation with respect to gravity.'''
373
374        # sanity - return if we have no calibration data
375        if self.force_1g[0] == 0:
376            return self.theta_g
377
378        # rotating from face-up to upside-down, force[2] goes from
379        # force_1g[2] to force_zero[2]-force_1g[2]. The normal force of
380        # gravity should be force_zero-force_1g - call this 'g'.
381        # It seems intuitive that this should map to a cosine wave - we start
382        # at 1g for face-up, then zero for a quarter-turn, -1g for half, etc.
383        zg = float(self.force[2]-self.force_zero[2])/self.force_1g_diff[2]
384        # If we're seeing more than 1g, probably this data isn't reliable
385        # for determining orientation, so we ignore it
386        if abs(zg) <= 1.0:
387            self.theta_g = math.acos(zg)
388        # Do the same thing with force[0] - it goes from 0->+/-1g->0, just like
389        # a sine wave
390        xg = float(self.force[0]-self.force_zero[0])/self.force_1g_diff[0]
391        if abs(xg) <= 1.0:
392            self.theta_g_x = math.asin(xg)
393        # For convenience, return theta_g
394        return self.theta_g
395
396
397    def calc_pointer(self):
398        '''Calculate the position of the pointer, taking into account the
399        rotation of the controller.
400        Sets self.theta and self.pointer; returns self.pointer.'''
401
402        # Credit for most of the math here goes to my esteemed colleague Mike
403        # (mikem@redhat.com). Finally, all those years TA-ing Calc 1 are
404        # paying off!
405        # One of the dots is bogus/missing. Bail out.
406        # TODO: keep track of the previous dot positions and guess instead of
407        # immediately bailing out?
408        if (DOT_MAX, DOT_MAX) in self.dots:
409            return self.pointer
410
411        ((x1,y1),(x2,y2)) = self.dots
412        # FIXME: for some reason, py never goes above ~750.
413        # Might be my bogus IR emitters (half-power every 15 degrees
414        # away from center! Thanks, Radio Shack.)
415        # But it might also be that the IR camera is calibrated to
416        # assume the sensor bar should be on the bottom of the TV.
417        # Since IR calibration is still Black Magick, I am forcing
418        # a scale factor for y here.
419        y1 = y1 * DOT_MAX / 760
420        y2 = y2 * DOT_MAX / 760
421        # Determine rotation angle. SOH CAH TOA ftw.
422        if (x1 != x2):
423            self.theta = math.atan(float(y2-y1)/float(x1-x2))
424        else:
425            self.theta = math.pi/2
426            if y1 > y2:
427                self.theta = -self.theta
428
429        # If the accel. says we are upside-down, add half a turn to theta
430        tg = math.degrees(self.calc_theta_g())
431        if tg > 90.0:
432            self.theta = self.theta+math.pi
433        if tg < -90.0:
434            self.theta = self.theta-math.pi
435        # rotate dots around center by theta.
436        (x1,y1) = rotate(x1,y1,self.theta)
437        (x2,y2) = rotate(x2,y2,self.theta)
438        # They should now be horizontal (y1 should be very close to y2).
439        # Average the two X values (find the center between them)
440        px = (x1+x2)/2
441        # Horizontal means y1 = y2, so there's no need to average them.
442        # In fact, let's output an error message if the rotate messed up.
443        if y2 != y1:
444            debug("post-rotation Y delta=%i", abs(y1-y2))
445        # We do need to flip the incoming y data.
446        py = DOT_MAX - y1
447        # Do some scaling - ignore the outer edges of the screen
448        # FIXME: fix scaling such that the center of the wiimote image
449        # maps to the top of the screen
450        # Center point of the screen is (c,c)
451        c = DOT_MAX/2
452        maxd = 0.33 * DOT_MAX # max allowable distance from center
453        # If this point is less than (maxd) from the center of the image,
454        # draw it.
455        if (abs(px-c) <= maxd) and (abs(py-c) <= maxd):
456            # px/py are in the range [c-maxd,c+maxd]
457            px = px - (c-maxd)
458            py = py - (c-maxd)
459            # Now they're in the range [0,2*maxd]. Scale to DOT_MAX.
460            px = px * (DOT_MAX/(2*maxd))
461            py = py * (DOT_MAX/(2*maxd))
462            # Hooray! We did it!
463            self.pointer = [int(px),int(py)]
464        return self.pointer
465
466    def pointer_str(self):
467        return "(%4i,%4i)" % (self.pointer[0],self.pointer[1])
468
469
470if __name__ == '__main__':
471    import logging
472
473    logging.basicConfig(level=logging.DEBUG,
474                    format='%(message)s')
475                   
476    w=Wiimote("00:19:1D:BB:7D:28",0)
477    w.connect()
478    print "Enabling accelerometer."
479    w.enable_force()
480    #w.enable_IR()
481    try:
482        last=time.time()
483        while not w.done:
484            w._getpacket()
485            debug("time %f force= %s %s ", time.time(), w.force_str(), w.buttons_str())
486    finally:
487        w.disconnect()
488
Note: See TracBrowser for help on using the browser.