1 '''
2 the module for interfacing with an ioLan button box.
3 USBBox is the main class that should be used from this module
4 '''
5
6
7 import logging
8
9
10
11 import time
12 import struct
13 from Queue import Queue, Empty
14 import hid
15
16 IO_LABS_VENDOR_ID=0x19BC
17 BUTTON_BOX_PRODUCT_ID=0x0001
18
21
23 '''simple class that takes keyword arguments and uses them to create fields on itself'''
25 self.__dict__.update(kw)
26
28 attribs=[]
29 for key,value in self.__dict__.items():
30 attribs.append('%s=%s'%(key,value))
31 return ','.join(attribs)
32
34 attribs=[]
35 for key,value in self.__dict__.items():
36 attribs.append('%s=%r'%(key,value))
37 return "dict_struct(%s)" % ','.join(attribs)
38
39
40
41
42 COMMAND_SUMMARY={
43
44 0x21 : ('PACSET', 'BBBBBBB', ('data1','data2','data3','data4','data5','data6','data7') ),
45 0x59 : ('VERGET', 'xxxxxxx', () ),
46 0x5A : ('NUMGET', 'xxxxxxx', () ),
47 0x52 : ('RESRTC', 'xxxxxxx', () ),
48 0x54 : ('RTCGET', 'xxxxxxx', () ),
49 0x48 : ('HBSET', 'Hxxxxx', ('rate',) ),
50 0x51 : ('QPURGE', 'xxxxxxx', () ),
51 0x53 : ('SEROUT', 'BBBBBBB', ('nb_data','data1','data2','data3','data4','data5','data6') ),
52 0x43 : ('VCKSET', 'BBBBBxx', ('action','data1','data2','data3','data4') ),
53 0x56 : ('VCKGET', 'xxxxxxx', () ),
54
55 0x4D : ('MSKSET', 'xBBxxxx', ('port3_bits','port1_bits') ),
56 0x3F : ('MSKGET', 'xxxxxxx', () ),
57 0x47 : ('KEYGET', 'xxxxxxx', () ),
58 0x4A : ('DEBSET', 'xBBBBBB', ('port1_down','port1_up','int0_down','int0_up','int1_down','int1_up') ),
59 0x46 : ('DEBGET', 'xxxxxxx', () ),
60
61 0x57 : ('DIRSET', 'BBBxxxx', ('port2_mode','port2_bits','port0_bits') ),
62 0x44 : ('DIRGET', 'xxxxxxx', () ),
63 0x4C : ('LOGSET', 'xBBxxxx', ('port2_bits','port0_bits') ),
64 0x42 : ('LOGGET', 'xxxxxxx', () ),
65 0x30 : ('P0SET', 'Bxxxxxx', ('bits',) ),
66 0x41 : ('P0AND', 'Bxxxxxx', ('bits',) ),
67 0x4F : ('P0_OR', 'Bxxxxxx', ('bits',) ),
68 0x58 : ('P0XOR', 'Bxxxxxx', ('bits',) ),
69 0x32 : ('P2SET', 'Bxxxxxx', ('bits',) ),
70 0x61 : ('P2AND', 'Bxxxxxx', ('bits',) ),
71 0x6F : ('P2_OR', 'Bxxxxxx', ('bits',) ),
72 0x78 : ('P2XOR', 'Bxxxxxx', ('bits',) ),
73 0x3D : ('PXSET', 'xBBxxxx', ('port2_bits', 'port0_bits' ) ),
74 0x26 : ('PXAND', 'xBBxxxx', ('port2_bits', 'port0_bits' ) ),
75 0x2B : ('PX_OR', 'xBBxxxx', ('port2_bits', 'port0_bits' ) ),
76 0x5E : ('PXXOR', 'xBBxxxx', ('port2_bits', 'port0_bits' ) ),
77 0x50 : ('PXGET', 'xxxxxxx', () ),
78 0x4E : ('PXPGET', 'xxxxxxx', () ),
79 }
80
81 REPORT_SUMMARY = {
82 0x59 : ('VERREP', 'BBBBBxx', ('error_code','rel_main','rev_main','rel_dtvk','rev_dtvk') ),
83 0x5A : ('NUMREP', 'Bx5s', ('error_code','serial_num') ),
84 0x54 : ('RTCREP', 'BxxI', ('queue_len', 'rtc') ),
85 0x48 : ('HBREP', 'BHI', ('queue_len', 'rate', 'rtc' ) ),
86 0x53 : ('SERIN', 'BBBBBBB', ('status_code','data1','data2','data3','data4','data5','data6') ),
87 0x56 : ('VCKREP', 'BBBBBBB', ('status_code','min_duration','min_silence','trigger_level','peak_level','primary_gain','secondary_gain') ),
88 0x4A : ('DEBREP', 'xBBBBBB', ('port1_down','port1_up','int0_down','int0_up','int1_down','int1_up') ),
89 0x44 : ('KEYDN', 'xxBI', ('key_code','rtc') ),
90 0x55 : ('KEYUP', 'xxBI', ('key_code','rtc') ),
91 0x4B : ('KEYREP', 'xBBI', ('port3_bits','port1_bits','rtc') ),
92 0x50 : ('PXREP', 'xBBI', ('port2_bits','port0_bits','rtc') ),
93 0x4E : ('PXPREP', 'xBBI', ('port2_bits','port0_bits','rtc') ),
94 0x4D : ('MSKREP', 'xBBI', ('port3_bits','port1_bits','rtc') ),
95 0x57 : ('DIRREP', 'BBBI', ('port2_mode','port2_bits','port0_bits','rtc') ),
96 0x4C : ('LOGREP', 'xBBI', ('port2_bits','port0_bits','rtc') ),
97 0x45 : ('ERROR', 'BBBBBBB', ('data1','data2','data3','data4','data5','data6','data7') )
98 }
99
101 '''class to handle message id lookup, and packing message objects into binary'''
103 self.message_summaries=message_summaries
104 for message_id,message_summary in message_summaries.items():
105
106 message_name=message_summary[0]
107 self.__dict__[message_name]=message_id
108
109 self.__dict__[message_name.lower()]=self._create_packing_function(message_id,message_summary)
110
112 return self.message_summaries.keys()
113
115
116 format='>B'+message_summary[1]
117 expected_args=message_summary[2]
118 def packing_function(*args):
119 if len(args) != len(expected_args):
120 raise RuntimeError("wrong number of args for: %s(), expected: %s" % (message_summary[0].lower(),expected_args))
121 return struct.pack(format,message_id,*args)
122 return packing_function
123
125 '''
126 get the 'name' from the id of the message
127 '''
128 return self.message_summaries[message_id][0]
129
130 - def parse(self,message_data):
131 '''
132 convert raw binary data into a structure
133 '''
134 id_byte=struct.unpack('B',message_data[0])[0]
135
136 if self.message_summaries.has_key(id_byte):
137 summary=self.message_summaries[id_byte]
138
139 format='>B'+summary[1]
140 unpacked=struct.unpack(format,message_data)
141 msg_fields={'name':summary[0]}
142 field_names=('id',)+summary[2]
143 if len(field_names) != len(unpacked):
144 raise RuntimeError("message did not unpack correctly: %r"%message_data)
145 for name,value in zip(field_names,unpacked):
146 msg_fields[name]=value
147 return dict_struct(**msg_fields)
148 else:
149
150 logging.info("unknown message id: %d",id_byte)
151 return dict_struct(id=id_byte,message_data=message_data)
152
153
154
155
156
157 COMMAND=messages(COMMAND_SUMMARY)
158 REPORT=messages(REPORT_SUMMARY)
159
161 '''
162 class to handle sending reports to device and parsing incoming reports.
163 dynamically looks up/creates method for sending reports when none is
164 found on the class. this will let us override the default behavior
165 to make things friendlier when appropriate.
166 all received messages are queued up and require a call to 'process_received_reports'
167 to trigger the user's callbacks, so as to avoid thread issues.
168 '''
175
177 logging.info('%r',report_data)
178 msg=REPORT.parse(report_data)
179 logging.info('received msg: %r',msg)
180 self.queue.put(msg)
181
183 '''
184 process all reports that have been received and call the
185 relevant callbacks.
186 by default this method returns immediately if no reports
187 are on the queue, but can be made to block and wait for
188 a report if needeed
189 '''
190 while block or not self.queue.empty():
191 report=self.queue.get(block,timeout)
192 self._process_report(report)
193
194
195 block=False
196
198 callbacks=self.callbacks.get(report.id,self.default_callbacks)
199 for callback in callbacks:
200 callback(report)
201
203 '''
204 return a list of received reports (removes them from the queue)
205 '''
206 reports=[]
207 while not self.queue.empty():
208 msg=self.queue.get()
209 reports.append(msg)
210 return reports
211
213 '''remove all received reports from the queue'''
214 while not self.queue.empty():
215 self.queue.get()
216
218 '''
219 add a callback function that will be called when
220 a report with the given id arrives. callback
221 should take a single value that is the report
222 that was received
223 '''
224 callbacks=self.callbacks.get(report_id,set())
225 callbacks.add(report_callback)
226 self.callbacks[report_id]=callbacks
227
229 callbacks=self.callbacks.get(report_id,set())
230 callbacks.discard(report_callback)
231
233 self.default_callbacks.add(report_callback)
234
236 self.default_callbacks.discard(report_callback)
237
239 '''
240 blocks until we receive a report with the given id
241 from the box and then returns it (may trigger other callbacks)
242 '''
243
244 reports=[]
245 def callback(report):
246 reports.append(report)
247 self.add_callback(report_id,callback)
248
249
250 try:
251 try:
252 while len(reports) == 0:
253 self.process_received_reports(block=True,timeout=2)
254 except Empty:
255 return None
256 finally:
257
258 self.remove_callback(report_id,callback)
259
260
261 return reports[0]
262
264 '''
265 send a command with the given arguments and wait for the reply
266 '''
267 command_name=COMMAND.name_from_id(command_id).lower()
268 getattr(self,command_name)(*args)
269 return self.wait_for_report(report_id)
270
272 '''
273 send a command (with the args), wait for the report and return the field on the
274 report
275 '''
276 return getattr(self.send_wait_reply(command_id,report_id,*args),field_name)
277
279 '''return a function to send the named command to the device'''
280 packing_function=getattr(COMMAND,name,None)
281 if packing_function:
282 return lambda *arg: self.device.set_report(packing_function(*arg))
283 raise AttributeError("couldn't find: %s" % name)
284
285
287 '''base class for Lines (individual parts of ports)'''
289 self._port=port
290 self._mask=mask
291
293 '''return 1/0 depending on whether relevent bit set'''
294 if (bits & self._mask) != 0:
295 return 1
296 else:
297 return 0;
298
300 if high:
301 return bits | self._mask
302 else:
303 return bits & (self._mask ^ 0xFF)
304
305
307 '''
308 class representing ports 0 and 2 (leds)
309 '''
311 self._commands=commands
312 self._port_num=port_num
313 self._port_bits='port%d_bits'%port_num
314
315
316
319
326
327 direction=property(_get_direction,_set_direction)
328 '''get/set the direction of the port'''
329
330
333
338
339 logic=property(_get_logic,_set_logic)
340 '''get/set the logic on the port'''
341
342
345
347 port_set=getattr(self._commands,'p%dset'%self._port_num)
348 port_set(bits)
349
350 self._commands.wait_for_report(REPORT.PXREP)
351
352 state=property(_get_state,_set_state)
353 '''get/set the port state'''
354
355
356
358 port_logic=getattr(self._commands,command_format%self._port_num)
359 port_logic(logic_bits)
360 return getattr(self._commands.wait_for_report(REPORT.PXREP),self._port_bits)
361
363 '''logically 'and' the value on the port, returns the port state'''
364 return self._logic_state(and_bits,'p%dand')
365
367 '''logically 'or' the value on the port, returns the port state'''
368 return self._logic_state(or_bits,'p%d_or')
369
371 '''logically 'xor' the value on the port, returns the port state'''
372 return self._logic_state(xor_bits,'p%dxor')
373
375 '''
376 return an object that let's the user modify/query values
377 on a single line of the port (1-bit)
378 '''
379
380 class PortLine(Line):
381
382 def _get_state(self):
383 return self._bit_state(self._port.state)
384
385 def _set_state(self,high):
386 if high:
387 self._port.or_state(self._mask)
388 else:
389 self._port.and_state(self._mask ^ 0xFF)
390
391 state=property(_get_state,_set_state)
392
393
394 def _get_direction(self):
395 return self._bit_state(self._port.direction)
396
397 def _set_direction(self,high):
398 self._port.direction=self._set_bit_state(self._port.direction,high)
399
400 direction=property(_get_direction,_set_direction)
401
402
403 def _get_logic(self):
404 return self._bit_state(self._port.logic)
405
406 def _set_logic(self,high):
407 self._port.logic=self._set_bit_state(self._port.logic,high)
408
409 logic=property(_get_logic,_set_logic)
410
411 mask=1<<line_no
412
413 return PortLine(self,mask)
414
415
416 line0=property(lambda self: self._get_line(0))
417 line1=property(lambda self: self._get_line(1))
418 line2=property(lambda self: self._get_line(2))
419 line3=property(lambda self: self._get_line(3))
420 line4=property(lambda self: self._get_line(4))
421 line5=property(lambda self: self._get_line(5))
422 line6=property(lambda self: self._get_line(6))
423 line7=property(lambda self: self._get_line(7))
424
425
426 lines=property(lambda self: [self._get_line(i) for i in range(8)])
427 '''
428 list of individual lines.
429 each line has properties for state, direction and logic
430 that can be used to alter the individual bits/lines on the whole
431 port
432 '''
433
434
435
436 -def _set_debounce(commands,port1_down=None,port1_up=None,int0_down=None,int0_up=None,int1_down=None,int1_up=None):
437 '''helper for setting debounce of a single field'''
438 rep=commands.send_wait_reply(COMMAND.DEBGET,REPORT.DEBREP)
439
440 if port1_down is not None:
441 rep.port1_down=port1_down
442 if port1_up is not None:
443 rep.port1_up=port1_up
444 if int0_down is not None:
445 rep.int0_down=int0_down
446 if int0_up is not None:
447 rep.int0_up=int0_up
448 if int1_down is not None:
449 rep.int1_down=int1_down
450 if int1_up is not None:
451 rep.int1_up=int1_up
452
453 commands.send_wait_reply(
454 COMMAND.DEBSET,
455 REPORT.DEBREP,
456 rep.port1_down,rep.port1_up,
457 rep.int0_down,rep.int0_up,
458 rep.int1_down,rep.int1_up)
459
460
519
520 def _set_enabled(self,high):
521 self._port.enabled=self._set_bit_state(self._port.enabled,high)
522
523 enabled=property(_get_enabled,_set_enabled)
524 '''get/set whether the line/button is enable or not'''
525
526 mask=1<<line_no
527
528 return ButtonLine(self,mask)
529
530
531 line0=property(lambda self: self._get_line(0))
532 '''individual line (one button)'''
533 line1=property(lambda self: self._get_line(1))
534 line2=property(lambda self: self._get_line(2))
535 line3=property(lambda self: self._get_line(3))
536 line4=property(lambda self: self._get_line(4))
537 line5=property(lambda self: self._get_line(5))
538 line6=property(lambda self: self._get_line(6))
539 line7=property(lambda self: self._get_line(7))
540
541 lines=property(lambda self: [self._get_line(i) for i in range(8)])
542 '''property for all 8 lines.
543 each line (button) has a state and enabled property
544 so each button can be queried/modified separately
545 '''
546
547
549 '''either int0 or int1 on the USBBox'''
550 - def __init__(self,commands,mask,int_num):
551 self._commands=commands
552 self._mask=mask
553 self._int_num=int_num
554
556 bits=self._commands.send_wait_field(COMMAND.MSKGET,REPORT.MSKREP,'port3_bits')
557 if bits & self._mask != 0:
558 return 1
559 else:
560 return 0
561
571
572 enabled=property(_get_enabled,_set_enabled)
573 '''enable/disable the interrupt'''
574
577
579
580 args={}
581 args['int%d_down'%self._int_num]=debounce
582 _set_debounce(self._commands,**args)
583
584 debounce_down=property(_get_debounce_down,_set_debounce_down)
585
586
589
591
592 args={}
593 args['int%d_up'%self._int_num]=debounce
594 _set_debounce(self._commands,**args)
595
596 debounce_up=property(_get_debounce_up,_set_debounce_up)
597
598
600 while True:
601 rep=commands.send_wait_reply(COMMAND.VCKGET,REPORT.VCKREP)
602 if rep.status_code in [0x58,0x28]:
603 break
604 if rep.status_code == 0x48:
605
606
607 time.sleep(0.005)
608 else:
609 raise RuntimeError("error getting voice key code: 0x%x"%rep.status_code)
610
611 return getattr(rep,field)
612
613 -def _set_voice_key(commands,min_duration=None,min_silence=None,trigger_level=None,mic_pass_thru=0):
638
640 '''object representing the voice input on the USBBox'''
644
645
648
651
652 primary_gain=property(_get_primary_gain,_set_primary_gain)
653
654
657
660
661 secondary_gain=property(_get_secondary_gain,_set_secondary_gain)
662
663
665 return _get_voice_key(self._commands,'min_duration')
666
669
670 min_duration=property(_get_min_duration,_set_min_duration)
671
672
674 return _get_voice_key(self._commands,'min_silence')
675
678
679 min_silence=property(_get_min_silence,_set_min_silence)
680
681
683 return _get_voice_key(self._commands,'trigger_level')
684
687
688 trigger_level=property(_get_trigger_level,_set_trigger_level)
689 '''get/set trigger level'''
690
691
692
694 return self._mic_pass_thru
695
697 if mic_pass_thru:
698 self._mic_pass_thru=1
699 else:
700 self._mic_pass_thru=0
701 _set_voice_key(self._commands,mic_pass_thru=self._mic_pass_thru)
702
703 mic_pass_thru=property(_get_mic_pass_thru,_set_mic_pass_thru)
704 '''get/set mic pass through state'''
705
706
708 '''serial port on the USBBox'''
710 self._commands=commands
711
712
713 self._commands.add_callback(
714 REPORT.SERIN,
715 self._serial_in
716 )
717 self._bytes_received=[]
718
720 num_bytes=report.status_code
721 if num_bytes <= 6:
722
723 for i in range(0,num_bytes):
724
725 self._bytes_received.append(getattr(report,'data%d'%(i+1)))
726 elif report.status_code in ['0xFF','0xFE','0xF7']:
727 raise RuntimeError("error reading from serial port: 0x%x"%report.status_code)
728
730 '''write bytes to the serial port'''
731 for i in range(0,len(bytes),6):
732 b=list(bytes[i:i+6])
733
734 b=[struct.unpack('B',byte)[0] for byte in b]
735 num_b=len(b)
736 while len(b) < 6:
737 b.append(0)
738
739
740
741 while True:
742 rep=self._commands.send_wait_reply(COMMAND.SEROUT,REPORT.SERIN,num_b,*b)
743 if rep.status_code == 0xF0:
744 break
745
747 '''
748 wait for input on the serial port. blocks for a while, but times
749 out if nothing received and returns an empty string
750 '''
751 self._commands.wait_for_report(REPORT.SERIN)
752
753 bytes = ''.join([struct.pack('B',byte) for byte in self._bytes_received])
754 self._bytes_received[:]=[]
755 return bytes
756
757
758
760 '''the USBBox itself'''
761
763 self._device=None
764 for dev in hid.find_hid_devices():
765 if is_usb_bbox(dev):
766 logging.info("found USB button box")
767 self._device=dev
768 break
769
770 if self._device is None:
771 raise RuntimeError("could not find button box - check it's plugged in")
772
773 self._device.open()
774
775 self._commands=Commands(self._device)
776
777 self.recording=False
778 self.recording_callback=None
779 self.report_ids=None
780
781 self._port0=Port0_2(self.commands,0)
782 self._port1=Buttons(self.commands)
783 self._port2=Port0_2(self.commands,2)
784
785 self._int0=VoiceKey(self.commands)
786 self._int1=Interrupt(self.commands,(1<<3),int_num=1)
787
788 self._serial=Serial(self.commands)
789
790 if do_reset:
791 self.reset_box()
792
796
797 device = property(lambda self: self._device)
798 '''the HIDDevice (the physical box itself)'''
799
800 commands = property(lambda self: self._commands)
801 '''Commands object for low-level API'''
802
803 port0 = property(lambda self: self._port0)
804 '''Port0_2 object for "port 0" on the box'''
805
806 port1 = property(lambda self: self._port1)
807 '''Buttons object for "port 1" on the box'''
808
809 port2 = property(lambda self: self._port2)
810 '''Port0_2 object for "port 2" on the box'''
811
812 int0 = property(lambda self: self._int0)
813 '''VoiceKey object for "interrupt 0" on the box'''
814
815 int1 = property(lambda self: self._int1)
816 '''Interrupt object for "interrupt 1" on the box'''
817
818 serial = property(lambda self: self._serial)
819 '''Serial object for the serial port on the box'''
820
821
822 leds=port2
823 '''synonym for port2'''
824 buttons=port1
825 '''synonym for port1'''
826 voice_key=int0
827 '''synonym for int0'''
828 optic_key=int1
829 '''synonym for int1'''
830
831
833 '''send a command to the box (one command_id byte and 7 data bytes)'''
834 self.device.set_report(struct.pack("B7s",command_id,bytes))
835
839
842
844 '''
845 whenever we read a report write it to the given
846 file (if the id is in report_ids)
847 '''
848 if self._recording:
849 raise RuntimeError("sorry already recording, please stop_recording() first")
850 self._recording=True
851
852 self._recording_callback=lambda report: out_file.write("%s\n"%report)
853 self._report_ids=set(report_ids)
854 for report_id in self._report_ids:
855 self.commands.add_callback(report_id,self._recording_callback)
856
858 '''removes the callbacks we had in place for recording'''
859 if self._recording:
860
861 self.process_received_reports()
862 self._recording=False
863 self._out_file=None
864 for report_id in self._report_ids:
865 self.commands.remove_callback(report_id,self._recording_callback)
866 self._report_ids=None
867
868
871 serial_num=property(_get_serial_num)
872 '''read the serial number of the box'''
873
874
876
877
878
879 self.commands.pacset(*value)
880 PAC=property(fset=_set_PAC)
881 ''' set the PAC code (write only) '''
882
883
887 version=property(_get_version)
888 '''get the main board version number'''
889
890
894 voice_version=property(_get_voice_version)
895 '''get the version number of the voice board'''
896
897
900 clock=property(_get_clock)
901 '''get the current clock value'''
902
903
906 heartbeat=property(fset=_set_heartbeat)
907 '''set the heartbeat rate (write only)'''
908
910 '''purge the event queue on the box'''
911 self.commands.qpurge()
912
916
921
925
929
933
960
961 if __name__ == '__main__':
962 import sys
963
964 usbbox=USBBox()
965
966 print "USBBox connected"
967 print "serial #:",usbbox.serial_num
968 print "version:",usbbox.version
969 print "voice version:", usbbox.voice_version
970
971
973 print "received:",msg
974 for command_id in REPORT.ALL_IDS():
975 usbbox.commands.add_callback(command_id,report_callback)
976
977 from StringIO import StringIO
978 outfile=StringIO()
979
980 usbbox.start_recording(REPORT_SUMMARY.keys(),outfile)
981
982 import re
983
984 while True:
985 time.sleep(0.5)
986 usbbox.process_received_reports()
987 command=raw_input("command: ").strip()
988 if command == 'exit':
989 break
990 elif command == '':
991 continue
992 elif command == 'help':
993
994 print "commands:"
995 print " exit"
996 print " help"
997 for command_id in COMMAND_SUMMARY.keys():
998 command_name=COMMAND_SUMMARY[command_id][0].lower()
999 command_args=COMMAND_SUMMARY[command_id][2]
1000 command_args=['<%s>' % arg for arg in command_args]
1001 print " %s %s" % (command_name,' '.join(command_args))
1002 else:
1003 try:
1004 command_parts=command.split()
1005 command_name,command_args=command_parts[0],command_parts[1:]
1006
1007 known=False
1008 for command_id in COMMAND_SUMMARY.keys():
1009 if COMMAND_SUMMARY[command_id][0].lower() == command_name:
1010 known=True
1011 break
1012 if not known:
1013 print "error, unknown command: " + command_name
1014 else:
1015 command_fn=getattr(usbbox.commands,command_name)
1016
1017 command_args=[int(arg) for arg in command_args]
1018 command_fn(*command_args)
1019 except:
1020 print "error running: " + command
1021
1022
1023 usbbox.process_received_reports()
1024 usbbox.stop_recording()
1025
1026 print "recorded reports:"
1027 print outfile.getvalue()
1028