5656# We can push data at it far, far faster than it can process it,
5757# therefore we push it in segments with a pause between each segment.
5858CARD_REQUEST_SEGMENT_MAX_LEN = 250
59+ # "a 250ms delay is required to separate "segments", ~256 byte
60+ # I2C transactions." See
61+ # https://dev.blues.io/guides-and-tutorials/notecard-guides/serial-over-i2c-protocol/#data-write
5962CARD_REQUEST_SEGMENT_DELAY_MS = 250
63+ # "A 20ms delay is commonly used to separate smaller I2C transactions known as
64+ # 'chunks'". See the same document linked above.
65+ I2C_CHUNK_DELAY_MS = 20
6066
6167
62- def prepareRequest (req , debug = False ):
68+ def _prepare_request (req , debug = False ):
6369 """Format the request string as a JSON object and add a newline."""
6470 req_json = json .dumps (req )
6571 if debug :
@@ -105,28 +111,29 @@ def serialReset(port):
105111
106112def serialTransaction (port , req , debug , txn_manager = None ):
107113 """Perform a single write to and read from a Notecard."""
108- req_json = prepareRequest (req , debug )
114+ req_json = _prepare_request (req , debug )
109115
110116 transaction_timeout_secs = 30
111117 if txn_manager :
112118 txn_manager .start (transaction_timeout_secs )
113119
114- seg_off = 0
115- seg_left = len (req_json )
116- while True :
117- seg_len = seg_left
118- if seg_len > CARD_REQUEST_SEGMENT_MAX_LEN :
119- seg_len = CARD_REQUEST_SEGMENT_MAX_LEN
120-
121- port .write (req_json [seg_off :seg_off + seg_len ].encode ('utf-8' ))
122- seg_off += seg_len
123- seg_left -= seg_len
124- if seg_left == 0 :
125- break
126- time .sleep (CARD_REQUEST_SEGMENT_DELAY_MS / 1000 )
127-
128- if txn_manager :
129- txn_manager .stop ()
120+ try :
121+ seg_off = 0
122+ seg_left = len (req_json )
123+ while True :
124+ seg_len = seg_left
125+ if seg_len > CARD_REQUEST_SEGMENT_MAX_LEN :
126+ seg_len = CARD_REQUEST_SEGMENT_MAX_LEN
127+
128+ port .write (req_json [seg_off :seg_off + seg_len ].encode ('utf-8' ))
129+ seg_off += seg_len
130+ seg_left -= seg_len
131+ if seg_left == 0 :
132+ break
133+ time .sleep (CARD_REQUEST_SEGMENT_DELAY_MS / 1000 )
134+ finally :
135+ if txn_manager :
136+ txn_manager .stop ()
130137
131138 rsp_json = port .readline ()
132139 if debug :
@@ -136,23 +143,31 @@ def serialTransaction(port, req, debug, txn_manager=None):
136143 return rsp
137144
138145
139- def serialCommand (port , req , debug ):
146+ def serialCommand (port , req , debug , txn_manager = None ):
140147 """Perform a single write to and read from a Notecard."""
141- req_json = prepareRequest (req , debug )
142-
143- seg_off = 0
144- seg_left = len (req_json )
145- while True :
146- seg_len = seg_left
147- if seg_len > CARD_REQUEST_SEGMENT_MAX_LEN :
148- seg_len = CARD_REQUEST_SEGMENT_MAX_LEN
149-
150- port .write (req_json [seg_off :seg_off + seg_len ].encode ('utf-8' ))
151- seg_off += seg_len
152- seg_left -= seg_len
153- if seg_left == 0 :
154- break
155- time .sleep (CARD_REQUEST_SEGMENT_DELAY_MS / 1000 )
148+ req_json = _prepare_request (req , debug )
149+
150+ transaction_timeout_secs = 30
151+ if txn_manager :
152+ txn_manager .start (transaction_timeout_secs )
153+
154+ try :
155+ seg_off = 0
156+ seg_left = len (req_json )
157+ while True :
158+ seg_len = seg_left
159+ if seg_len > CARD_REQUEST_SEGMENT_MAX_LEN :
160+ seg_len = CARD_REQUEST_SEGMENT_MAX_LEN
161+
162+ port .write (req_json [seg_off :seg_off + seg_len ].encode ('utf-8' ))
163+ seg_off += seg_len
164+ seg_left -= seg_len
165+ if seg_left == 0 :
166+ break
167+ time .sleep (CARD_REQUEST_SEGMENT_DELAY_MS / 1000 )
168+ finally :
169+ if txn_manager :
170+ txn_manager .stop ()
156171
157172
158173class Notecard :
@@ -178,7 +193,7 @@ def __init__(self):
178193 self ._user_agent ['os_family' ] = os .uname ().machine
179194 self ._transaction_manager = None
180195
181- def _preprocessReq (self , req ):
196+ def _preprocess_req (self , req ):
182197 """Inspect the request for hub.set and add the User Agent."""
183198 if 'hub.set' in req .values ():
184199 # Merge the User Agent to send along with the hub.set request.
@@ -212,24 +227,24 @@ class OpenSerial(Notecard):
212227
213228 def Command (self , req ):
214229 """Perform a Notecard command and exit with no response."""
215- req = self ._preprocessReq (req )
230+ req = self ._preprocess_req (req )
216231 if 'cmd' not in req :
217232 raise Exception ("Please use 'cmd' instead of 'req'" )
218233
219234 if use_serial_lock :
220235 try :
221236 self .lock .acquire (timeout = 5 )
222- serialCommand (self .uart , req , self ._debug )
237+ serialCommand (self .uart , req , self ._debug , self . _transaction_manager )
223238 except Timeout :
224239 raise Exception ("Notecard in use" )
225240 finally :
226241 self .lock .release ()
227242 else :
228- serialCommand (self .uart , req , self ._debug )
243+ serialCommand (self .uart , req , self ._debug , self . _transaction_manager )
229244
230245 def Transaction (self , req ):
231246 """Perform a Notecard transaction and return the result."""
232- req = self ._preprocessReq (req )
247+ req = self ._preprocess_req (req )
233248 if use_serial_lock :
234249 try :
235250 self .lock .acquire (timeout = 5 )
@@ -274,50 +289,122 @@ def __init__(self, uart_id, debug=False):
274289class OpenI2C (Notecard ):
275290 """Notecard class for I2C communication."""
276291
277- def _sendPayload (self , json ):
292+ def _send_payload (self , json ):
278293 chunk_offset = 0
279294 json_left = len (json )
280295 sent_in_seg = 0
296+ write_length = bytearray (1 )
297+
281298 while json_left > 0 :
282- time .sleep (.001 )
283299 chunk_len = min (json_left , self .max )
284- reg = bytearray (1 )
285- reg [0 ] = chunk_len
300+ write_length [0 ] = chunk_len
286301 write_data = bytes (json [chunk_offset :chunk_offset + chunk_len ],
287302 'utf-8' )
303+ # Send a message with the length of the incoming bytes followed
304+ # by the bytes themselves.
288305 if use_periphery :
289- msgs = [I2C .Message (reg + write_data )]
306+ msgs = [I2C .Message (write_length + write_data )]
290307 self .i2c .transfer (self .addr , msgs )
291308 else :
292- self .i2c .writeto (self .addr , reg + write_data )
309+ self .i2c .writeto (self .addr , write_length + write_data )
310+
293311 chunk_offset += chunk_len
294312 json_left -= chunk_len
295313 sent_in_seg += chunk_len
314+
296315 if sent_in_seg > CARD_REQUEST_SEGMENT_MAX_LEN :
297316 sent_in_seg -= CARD_REQUEST_SEGMENT_MAX_LEN
298- time .sleep (CARD_REQUEST_SEGMENT_DELAY_MS / 1000 )
317+ time .sleep (CARD_REQUEST_SEGMENT_DELAY_MS / 1000 )
318+
319+ time .sleep (I2C_CHUNK_DELAY_MS / 1000 )
320+
321+ def _receive (self , timeout_secs , chunk_delay_secs , wait_for_newline ):
322+ chunk_len = 0
323+ received_newline = False
324+ start = start_timeout ()
325+ read_data = bytearray ()
326+
327+ while True :
328+ initiate_read = bytearray (2 )
329+ # 0 indicates we are reading from the Notecard.
330+ initiate_read [0 ] = 0
331+ # This indicates how many bytes we are prepared to read.
332+ initiate_read [1 ] = chunk_len
333+ # read_buf is a buffer to store the data we're reading.
334+ # chunk_len accounts for the payload and the +2 is for the
335+ # header. The header sent by the Notecard has one byte to
336+ # indicate the number of bytes still available to read and a
337+ # second byte to indicate the number of bytes coming in the
338+ # current chunk.
339+ read_buf = bytearray (chunk_len + 2 )
340+
341+ if use_periphery :
342+ msgs = [I2C .Message (initiate_read ), I2C .Message (read_buf , read = True )]
343+ self .i2c .transfer (self .addr , msgs )
344+ read_buf = msgs [1 ].data
345+ elif sys .implementation .name == 'micropython' :
346+ self .i2c .writeto (self .addr , initiate_read , False )
347+ self .i2c .readfrom_into (self .addr , read_buf )
348+ else :
349+ self .i2c .writeto_then_readfrom (self .addr , initiate_read , read_buf )
350+
351+ # The number of bytes still available to read.
352+ num_bytes_available = read_buf [0 ]
353+ # The number of bytes in this chunk.
354+ num_bytes_this_chunk = read_buf [1 ]
355+ if num_bytes_this_chunk > 0 :
356+ read_data += read_buf [2 :2 + num_bytes_this_chunk ]
357+ received_newline = read_buf [- 1 ] == ord ('\n ' )
358+
359+ chunk_len = min (num_bytes_available , self .max )
360+ # Keep going if there's still byte available to read, even if
361+ # we've received a newline.
362+ if chunk_len > 0 :
363+ continue
364+
365+ # Otherwise, if there's no bytes available to read and we either
366+ # 1) don't care about waiting for a newline or 2) do care and
367+ # received the newline, we're done.
368+ if not wait_for_newline or received_newline :
369+ break
370+
371+ # Delay between reading chunks. Note that as long as bytes are
372+ # available to read (i.e. chunk_len > 0), we don't delay here, nor
373+ # do we check the timeout below. This is intentional and mimics the
374+ # behavior of other SDKs (e.g. note-c).
375+ time .sleep (chunk_delay_secs )
376+
377+ if (timeout_secs != 0 and has_timed_out (start , timeout_secs )):
378+ raise Exception ("Timed out while reading data from the Notecard." )
379+
380+ return read_data
299381
300382 def Command (self , req ):
301- """Perform a Notecard command and exit with no response."""
302- req = self ._preprocessReq (req )
383+ """Perform a Notecard command and return with no response."""
303384 if 'cmd' not in req :
304385 raise Exception ("Please use 'cmd' instead of 'req'" )
305386
306- req_json = prepareRequest (req , self ._debug )
387+ req = self ._preprocess_req (req )
388+ req_json = _prepare_request (req , self ._debug )
307389
308390 while not self .lock ():
309391 pass
310392
311393 try :
312- self ._sendPayload (req_json )
394+ transaction_timeout_secs = 30
395+ if self ._transaction_manager :
396+ self ._transaction_manager .start (transaction_timeout_secs )
397+
398+ self ._send_payload (req_json )
313399 finally :
314400 self .unlock ()
401+ if self ._transaction_manager :
402+ self ._transaction_manager .stop ()
315403
316404 def Transaction (self , req ):
317405 """Perform a Notecard transaction and return the result."""
318- req = self ._preprocessReq (req )
319-
320- req_json = prepareRequest (req , self ._debug )
406+ req = self ._preprocess_req (req )
407+ req_json = _prepare_request (req , self ._debug )
321408 rsp_json = ""
322409
323410 while not self .lock ():
@@ -328,88 +415,31 @@ def Transaction(self, req):
328415 if self ._transaction_manager :
329416 self ._transaction_manager .start (transaction_timeout_secs )
330417
331- self ._sendPayload (req_json )
332-
333- chunk_len = 0
334- received_newline = False
335- start = start_timeout ()
336- while True :
337- time .sleep (.001 )
338- reg = bytearray (2 )
339- reg [0 ] = 0
340- reg [1 ] = chunk_len
341- readlen = chunk_len + 2
342- buf = bytearray (readlen )
343- if use_periphery :
344- msgs = [I2C .Message (reg ), I2C .Message (buf , read = True )]
345- self .i2c .transfer (self .addr , msgs )
346- buf = msgs [1 ].data
347- elif sys .implementation .name == 'micropython' :
348- self .i2c .writeto (self .addr , reg , False )
349- self .i2c .readfrom_into (self .addr , buf )
350- else :
351- self .i2c .writeto_then_readfrom (self .addr , reg , buf )
352- available = buf [0 ]
353- good = buf [1 ]
354- data = buf [2 :2 + good ]
355- if good > 0 and buf [- 1 ] == 0x0a :
356- received_newline = True
357- try :
358- rsp_json += "" .join (map (chr , data ))
359- except :
360- pass
361- chunk_len = min (available , self .max )
362- if chunk_len > 0 :
363- continue
364- if received_newline :
365- break
366- if (has_timed_out (start , transaction_timeout_secs )):
367- raise Exception ("notecard request or response was lost" )
368- time .sleep (0.05 )
418+ self ._send_payload (req_json )
369419
420+ read_data = self ._receive (transaction_timeout_secs , 0.05 , True )
421+ rsp_json = "" .join (map (chr , read_data ))
370422 finally :
371423 self .unlock ()
372424 if self ._transaction_manager :
373425 self ._transaction_manager .stop ()
374426
375427 if self ._debug :
376428 print (rsp_json .rstrip ())
377- rsp = json . loads ( rsp_json )
378- return rsp
429+
430+ return json . loads ( rsp_json )
379431
380432 def Reset (self ):
381433 """Reset the Notecard."""
382- chunk_len = 0
383-
384434 while not self .lock ():
385435 pass
386436
387437 try :
388- while True :
389- time .sleep (.001 )
390- reg = bytearray (2 )
391- reg [0 ] = 0
392- reg [1 ] = chunk_len
393- readlen = chunk_len + 2
394- buf = bytearray (readlen )
395- if use_periphery :
396- msgs = [I2C .Message (reg ), I2C .Message (buf , read = True )]
397- self .i2c .transfer (self .addr , msgs )
398- buf = msgs [1 ].data
399- elif sys .implementation .name == 'micropython' :
400- self .i2c .writeto (self .addr , reg , False )
401- self .i2c .readfrom_into (self .addr , buf )
402- else :
403- self .i2c .writeto_then_readfrom (self .addr , reg , buf )
404- available = buf [0 ]
405- if available == 0 :
406- break
407- chunk_len = min (available , self .max )
438+ # Read from the Notecard until there's nothing left to read.
439+ self ._receive (0 , .001 , False )
408440 finally :
409441 self .unlock ()
410442
411- pass
412-
413443 def lock (self ):
414444 """Lock the I2C port so the host can interact with the Notecard."""
415445 if use_i2c_lock :
0 commit comments