1 __author__ = 'Copyright (c) 2013 Alan Yorinks All rights reserved.'
2
3 """
4 Copyright (c) 2013-14 Alan Yorinks All rights reserved.
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public
8 License as published by the Free Software Foundation; either
9 version 3 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
15
16 You should have received a copy of the GNU Lesser General Public
17 License along with this library; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 """
20
21 import threading
22 import time
23
24
26 """
27 This class handles all data interchanges with Firmata
28 The receive loop runs in its own thread.
29
30 Messages to be sent to Firmata are queued through a deque to allow for priority
31 messages to take precedence. The deque is checked within the receive loop for any
32 outgoing messages.
33
34 There is no blocking in either communications direction.
35
36 There is blocking when accessing the data tables through the _data_lock
37 """
38
39
40
41
42
43 MSG_CMD_MIN = 0x80
44 REPORT_ANALOG = 0xC0
45 REPORT_DIGITAL = 0xD0
46 SET_PIN_MODE = 0xF4
47 START_SYSEX = 0xF0
48 END_SYSEX = 0xF7
49 SYSTEM_RESET = 0xFF
50
51
52 DIGITAL_MESSAGE = 0x90
53 ANALOG_MESSAGE = 0xE0
54 REPORT_VERSION = 0xF9
55
56
57
58 ENCODER_CONFIG = 0x20
59 TONE_PLAY = 0x5F
60 SONAR_CONFIG = 0x60
61
62
63 ENCODER_DATA = 0x21
64 SONAR_DATA = 0x61
65
66
67
68 SERVO_CONFIG = 0x70
69 STRING_DATA = 0x71
70 I2C_REQUEST = 0x76
71 I2C_REPLY = 0x77
72 I2C_CONFIG = 0x78
73 REPORT_FIRMWARE = 0x79
74 SAMPLING_INTERVAL = 0x7A
75
76 EXTENDED_ANALOG = 0x6F
77 PIN_STATE_QUERY = 0x6D
78 PIN_STATE_RESPONSE = 0x6E
79 CAPABILITY_QUERY = 0x6B
80 CAPABILITY_RESPONSE = 0x6C
81 ANALOG_MAPPING_QUERY = 0x69
82 ANALOG_MAPPING_RESPONSE = 0x6A
83
84
85
86 SYSEX_NON_REALTIME = 0x7E
87 SYSEX_REALTIME = 0x7F
88
89
90
91 INPUT = 0x00
92 OUTPUT = 0x01
93 ANALOG = 0x02
94 PWM = 0x03
95 SERVO = 0x04
96 I2C = 0x06
97 ONEWIRE = 0x07
98 STEPPER = 0x08
99 TONE = 0x09
100 ENCODER = 0x0a
101 SONAR = 0x0b
102 IGNORE = 0x7f
103
104
105
106
107 DIGITAL = 0x20
108
109
110
111
112
113
114
115
116 analog_response_table = []
117
118
119
120 digital_response_table = []
121
122
123
124
125
126
127
128
129
130
131
132
133 analog_latch_table = []
134 digital_latch_table = []
135
136
137 LATCH_STATE = 0
138 LATCHED_THRESHOLD_TYPE = 1
139
140 ANALOG_LATCH_DATA_TARGET = 2
141 ANALOG_LATCHED_DATA = 3
142 ANALOG_TIME_STAMP = 4
143
144 DIGITAL_LATCHED_DATA = 2
145 DIGITAL_TIME_STAMP = 3
146
147
148
149 LATCH_IGNORE = 0
150 LATCH_ARMED = 1
151
152 LATCH_LATCHED = 2
153
154
155 DIGITAL_LATCH_LOW = 0
156 DIGITAL_LATCH_HIGH = 1
157 ANALOG_LATCH_GT = 2
158 ANALOG_LATCH_LT = 3
159 ANALOG_LATCH_GTE = 4
160 ANALOG_LATCH_LTE = 5
161
162
163
164 RESPONSE_TABLE_MODE = 0
165 RESPONSE_TABLE_PIN_DATA_VALUE = 1
166
167
168 MSB = 2
169 LSB = 1
170
171
172
173
174
175
176
177 command_dispatch = {}
178
179
180
181 command_deque = None
182
183
184 firmata_version = []
185
186
187 firmata_firmware = []
188
189
190 data_lock = None
191
192
193 total_pins_discovered = 0
194
195
196 number_of_analog_pins_discovered = 0
197
198
199 i2c_map = {}
200
201
202 active_sonar_map = {}
203
204 - def __init__(self, transport, command_deque, data_lock):
205 """
206 constructor for CommandHandler class
207 @param transport: A reference to the communications port designator.
208 @param command_deque: A reference to a command deque.
209 @param data_lock: A reference to a thread lock.
210 """
211
212
213 self.last_pin_query_results = []
214
215
216 self.capability_query_results = []
217
218
219 self.analog_mapping_query_results = []
220
221 self.data_lock = data_lock
222 self.transport = transport
223 self.command_deque = command_deque
224
225 self.total_pins_discovered = 0
226
227 self.number_of_analog_pins_discovered = 0
228
229 threading.Thread.__init__(self)
230 self.daemon = True
231
232 self.stop_event = threading.Event()
233
235 self.stop_event.set()
236
238 return self.stop_event.is_set()
239
241 """
242 This method will allow up to max_wait_time seconds for discovery (communicating with) an Arduino board
243 and then will determine a pin configuration table for the board.
244 @param max_wait_time: The maximum time to wait for discovery of the board
245 @return: True if board is successfully discovered or False upon timeout
246 """
247
248 start_time = time.time()
249
250
251
252 while len(self.analog_mapping_query_results) == 0:
253 if time.time() - start_time > max_wait_time:
254 return False
255
256 self.send_sysex(self.ANALOG_MAPPING_QUERY, None)
257 time.sleep(.1)
258
259 print "Board initialized in %d seconds" % (time.time() - start_time)
260
261 for pin in self.analog_mapping_query_results:
262 self.total_pins_discovered += 1
263
264 if pin != self.IGNORE:
265 self.number_of_analog_pins_discovered += 1
266
267 print 'Total Number of Pins Detected = %d' % self.total_pins_discovered
268 print 'Total Number of Analog Pins Detected = %d' % self.number_of_analog_pins_discovered
269
270
271
272 for pin in range(0, self.total_pins_discovered):
273 response_entry = [self.INPUT, 0]
274 self.digital_response_table.append(response_entry)
275
276 for pin in range(0, self.number_of_analog_pins_discovered):
277 response_entry = [self.INPUT, 0]
278 self.analog_response_table.append(response_entry)
279
280
281 for pin in range(0, self.total_pins_discovered):
282 digital_latch_table_entry = [0, 0, 0, 0]
283 self.digital_latch_table.append(digital_latch_table_entry)
284
285 for pin in range(0, self.number_of_analog_pins_discovered):
286 analog_latch_table_entry = [0, 0, 0, 0, 0]
287 self.analog_latch_table.append(analog_latch_table_entry)
288
289 return True
290
292 """
293 This method processes the report version message, sent asynchronously by Firmata when it starts up
294 or after refresh_report_version() is called
295
296 Use the api method api_get_version to retrieve this information
297 @param data: Message data from Firmata
298 @return: No return value.
299 """
300 self.firmata_version.append(data[0])
301 self.firmata_version.append(data[1])
302
304 """
305 This method "arms" a pin to allow data latching for the pin.
306 @param pin: Analog pin number (value following an 'A' designator, i.e. A5 = 5
307 @param threshold_type: ANALOG_LATCH_GT | ANALOG_LATCH_LT | ANALOG_LATCH_GTE | ANALOG_LATCH_LTE
308 @param threshold_value: numerical value
309 """
310 with self.data_lock:
311 self.analog_latch_table[pin] = [self.LATCH_ARMED, threshold_type, threshold_value, 0, 0]
312
314 """
315 This method "arms" a pin to allow data latching for the pin.
316 @param pin: digital pin number
317 @param threshold_type: DIGITAL_LATCH_HIGH | DIGITAL_LATCH_LOW
318 """
319 with self.data_lock:
320 self.digital_latch_table[pin] = [self.LATCH_ARMED, threshold_type, 0, 0]
321
323 """
324 This method reads the analog latch table for the specified pin and returns a list that contains:
325 [latch_state, latched_data, and time_stamp].
326 If the latch state is latched, the entry in the table is cleared
327 @param pin: pin number
328 @return: [latch_state, latched_data, and time_stamp]
329 """
330 with self.data_lock:
331 pin_data = self.analog_latch_table[pin]
332 current_latch_data = [pin,
333 pin_data[self.LATCH_STATE],
334 pin_data[self.ANALOG_LATCHED_DATA],
335 pin_data[self.ANALOG_TIME_STAMP]]
336
337 if pin_data[self.LATCH_STATE] == self.LATCH_LATCHED:
338 self.analog_latch_table[pin] = [0, 0, 0, 0, 0]
339
340 return current_latch_data
341
342
344 """
345 This method reads the digital latch table for the specified pin and returns a list that contains:
346 [latch_state, latched_data, and time_stamp].
347 If the latch state is latched, the entry in the table is cleared
348 @param pin: pin number
349 @return: [latch_state, latched_data, and time_stamp]
350 """
351 with self.data_lock:
352 pin_data = self.digital_latch_table[pin]
353 current_latch_data = [pin,
354 pin_data[self.LATCH_STATE],
355 pin_data[self.DIGITAL_LATCHED_DATA],
356 pin_data[self.DIGITAL_TIME_STAMP]]
357 if pin_data[self.LATCH_STATE] == self.LATCH_LATCHED:
358 self.digital_latch_table[pin] = [0, 0, 0, 0]
359
360 return current_latch_data
361
362
364 """
365 This method processes the report firmware message, sent asynchronously by Firmata when it starts up
366 or after refresh_report_firmware() is called
367
368 Use the api method api_get_firmware_version to retrieve this information
369 @param data: Message data from Firmata
370 @return: No return value.
371 """
372 self.firmata_firmware.append(data[0])
373 self.firmata_firmware.append(data[1])
374
375
376
377 name_data = data[2:]
378
379
380 file_name = []
381
382
383
384 for i in name_data[::2]:
385 file_name.append(chr(i))
386
387
388 self.firmata_firmware.append("".join(file_name))
389
454
499
513
527
529 """
530 This method returns the entire analog response table to the caller
531 @return: The analog response table.
532 """
533 with self.data_lock:
534 data = self.analog_response_table
535
536 return data
537
539 """
540 This method returns the entire digital response table to the caller
541 @return: The digital response table.
542 """
543 with self.data_lock:
544 data = self.digital_response_table
545
546 return data
547
548
549 - def send_sysex(self, sysex_command, sysex_data=None):
550 """
551 This method will send a Sysex command to Firmata with any accompanying data
552
553 @param sysex_command: sysex command
554 @param sysex_data: data for command
555 @return : No return value.
556 """
557 if not sysex_data:
558 sysex_data = []
559
560
561 sysex_message = chr(self.START_SYSEX)
562 sysex_message += chr(sysex_command)
563 if len(sysex_data):
564 for d in sysex_data:
565 sysex_message += chr(d)
566 sysex_message += chr(self.END_SYSEX)
567
568 for data in sysex_message:
569 self.transport.write(data)
570
572 """
573 This method is used to transmit a non-sysex command.
574 @param command: Command to send to firmata includes command + data formatted by caller
575 @return : No return value.
576 """
577 send_message = ""
578 for i in command:
579 send_message += chr(i)
580
581 for data in send_message:
582 self.transport.write(data)
583
614
615
616
617
619 """
620 This method handles the incoming string data message from Firmata.
621 The string is printed to the console
622
623 @param data: Message data from Firmata
624 @return: No return value.s
625 """
626 print "_string_data:"
627 string_to_print = []
628 for i in data[::2]:
629 string_to_print.append(chr(i))
630 print string_to_print
631
633 """
634 This method receives replies to i2c_read requests. It stores the data for each i2c device
635 address in a dictionary called i2c_map. The data is retrieved via a call to i2c_get_read_data()
636 in pymata.py
637 @param data: raw data returned from i2c device
638 """
639 reply_data = []
640 address = (data[0] & 0x7f) + (data[1] << 7)
641 register = data[2] & 0x7f + data[3] << 7
642 reply_data.append(register)
643 for i in xrange(4, len(data), 2):
644 data_item = (data[i] & 0x7f) + (data[i + 1] << 7)
645 reply_data.append(data_item)
646 self.i2c_map[address] = reply_data
647
649 """
650 This method handles a capability response message and stores the results to be retrieved
651 via get_capability_query_results() in pymata.py
652 @param data: raw capability data
653 """
654 self.capability_query_results = data
655
657 """
658 This method handles a pin state response message and stores the results to be retrieved
659 via get_pin_state_query_results() in pymata.py
660 @param data: raw pin state data
661 """
662 self.last_pin_query_results = data
663
665 """
666 This method handles an analog mapping query response message and stores the results to be retrieved
667 via get_analog_mapping_request_results() in pymata.py
668 @param data: raw analog mapping data
669 """
670 self.analog_mapping_query_results = data
671
672
674 """
675 This method starts the thread that continuously runs to receive and interpret
676 messages coming from Firmata. This must be the last method in this file
677
678 It also checks the deque for messages to be sent to Firmata.
679 """
680
681
682 self.command_dispatch.update({self.REPORT_VERSION: [self.report_version, 2]})
683 self.command_dispatch.update({self.REPORT_FIRMWARE: [self.report_firmware, 1]})
684 self.command_dispatch.update({self.ANALOG_MESSAGE: [self.analog_message, 2]})
685 self.command_dispatch.update({self.DIGITAL_MESSAGE: [self.digital_message, 2]})
686 self.command_dispatch.update({self.ENCODER_DATA: [self.encoder_data, 3]})
687 self.command_dispatch.update({self.SONAR_DATA: [self.sonar_data, 3]})
688 self.command_dispatch.update({self.STRING_DATA: [self._string_data, 2]})
689 self.command_dispatch.update({self.I2C_REPLY: [self.i2c_reply, 2]})
690 self.command_dispatch.update({self.CAPABILITY_RESPONSE: [self.capability_response, 2]})
691 self.command_dispatch.update({self.PIN_STATE_RESPONSE: [self.pin_state_response, 2]})
692 self.command_dispatch.update({self.ANALOG_MAPPING_RESPONSE: [self.analog_mapping_response, 2]})
693
694 MAX_COMMAND_WAIT_TIME = 0.5
695
696 while not self.is_stopped():
697
698 if len(self.command_deque):
699
700 data = self.command_deque.popleft()
701
702
703 command_data = []
704
705 command_start_time = time.time()
706
707 try:
708
709
710
711 if data == self.START_SYSEX:
712
713
714
715 while len(self.command_deque) == 0 and time.time() - command_start_time < MAX_COMMAND_WAIT_TIME:
716 pass
717 sysex_command = self.command_deque.popleft()
718
719 dispatch_entry = self.command_dispatch.get(sysex_command)
720
721 if dispatch_entry == None:
722 print "Error: No dispatch entry for sysex_command {0:X}".format( sysex_command )
723 continue
724
725
726 method = dispatch_entry[0]
727
728
729 end_of_sysex = False
730 while not end_of_sysex and time.time() - command_start_time < MAX_COMMAND_WAIT_TIME:
731
732 while len(self.command_deque) == 0 and time.time() - command_start_time < MAX_COMMAND_WAIT_TIME:
733 pass
734 data = self.command_deque.popleft()
735 if data != self.END_SYSEX:
736 command_data.append(data)
737 else:
738 end_of_sysex = True
739
740
741 method(command_data)
742
743 continue
744
745
746 elif 0x80 <= data <= 0xff:
747
748
749
750
751 if 0x90 <= data <= 0x9f:
752 port = data & 0xf
753 command_data.append(port)
754 data = 0x90
755
756 elif 0xe0 <= data <= 0xef:
757 pin = data & 0xf
758 command_data.append(pin)
759 data = 0xe0
760 else:
761 pass
762
763 dispatch_entry = self.command_dispatch.get(data)
764 if dispatch_entry == None:
765 print "Error: No dispatch error for data {0:X}".format( data )
766 continue
767
768
769 method = dispatch_entry[0]
770
771
772 num_args = dispatch_entry[1]
773
774
775
776 for i in range(num_args):
777 while len(self.command_deque) == 0 and time.time() - command_start_time < MAX_COMMAND_WAIT_TIME:
778 pass
779 data = self.command_deque.popleft()
780 command_data.append(data)
781
782 method(command_data)
783
784
785 continue
786
787 except Exception as e:
788
789 print "Error: Caught exception in command handler -", e
790