Skip to content

Commit 93927f5

Browse files
authored
Merge pull request #67 from haydenroche5/readability
2 parents 86fa832 + e9cc262 commit 93927f5

File tree

2 files changed

+148
-118
lines changed

2 files changed

+148
-118
lines changed

notecard/notecard.py

Lines changed: 147 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,16 @@
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.
5858
CARD_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
5962
CARD_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

106112
def 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

158173
class 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):
274289
class 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:

test/test_notecard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ def setUserAgentInfo(self, info=None):
462462
nCard = MockNotecard()
463463
orgReq = {"req": "hub.set"}
464464
nCard.SetAppUserAgent(info)
465-
req = nCard._preprocessReq(orgReq)
465+
req = nCard._preprocess_req(orgReq)
466466
return req
467467

468468
def test_amends_hub_set_request(self):

0 commit comments

Comments
 (0)