Skip to content

Commit 25c19c3

Browse files
authored
Merge pull request #77 from haydenroche5/cobs
2 parents e70a8f9 + 93402ce commit 25c19c3

File tree

12 files changed

+1169
-26
lines changed

12 files changed

+1169
-26
lines changed

.github/workflows/hil-circuitpython.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ on:
2121
type: boolean
2222
default: true
2323

24+
schedule:
25+
- cron: '30 4 * * 1'
26+
2427
jobs:
2528
test:
2629
runs-on: [self-hosted, linux, circuitpython, swan-3.0, notecard-serial]

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ flake8:
2929
# F403 'from module import *' used; unable to detect undefined names https://www.flake8rules.com/rules/F403.html
3030
# W503 Line break occurred before a binary operator https://www.flake8rules.com/rules/W503.html
3131
# E501 Line too long (>79 characters) https://www.flake8rules.com/rules/E501.html
32-
${PYTHON} -m flake8 test/ notecard/ examples/ mpy_board/ --count --ignore=E722,F401,F403,W503,E501,E502 --show-source --statistics
32+
${PYTHON} -m flake8 --exclude=notecard/md5.py test/ notecard/ examples/ mpy_board/ --count --ignore=E722,F401,F403,W503,E501,E502 --show-source --statistics
3333

3434
coverage:
3535
${RUN_VENV_ACTIVATE}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""note-python binary loopback example.
2+
3+
This example writes an array of bytes to the binary data store on a Notecard and
4+
reads them back. It checks that what was written exactly matches what's read
5+
back.
6+
7+
Supports MicroPython, CircuitPython, and Raspberry Pi (Linux).
8+
"""
9+
import sys
10+
11+
12+
def run_example(product_uid, use_uart=True):
13+
"""Connect to Notcard and run a binary loopback test."""
14+
tx_buf = bytearray([
15+
0x67, 0x48, 0xa8, 0x1e, 0x9f, 0xbb, 0xb7, 0x27, 0xbb, 0x31, 0x89, 0x00, 0x1f,
16+
0x60, 0x49, 0x8a, 0x63, 0xa1, 0x2b, 0xac, 0xb8, 0xa9, 0xb0, 0x59, 0x71, 0x65,
17+
0xdd, 0x87, 0x73, 0x8a, 0x06, 0x9d, 0x40, 0xc1, 0xee, 0x24, 0xca, 0x31, 0xee,
18+
0x88, 0xf7, 0xf1, 0x23, 0x60, 0xf2, 0x01, 0x98, 0x39, 0x21, 0x18, 0x25, 0x3c,
19+
0x36, 0xf7, 0x93, 0xae, 0x50, 0xd6, 0x7d, 0x93, 0x55, 0xff, 0xcb, 0x56, 0xd3,
20+
0xd3, 0xd5, 0xe9, 0xf0, 0x60, 0xf7, 0xe9, 0xd3, 0xa4, 0x40, 0xe7, 0x8a, 0x71,
21+
0x72, 0x8b, 0x28, 0x5d, 0x57, 0x57, 0x8c, 0xc3, 0xd4, 0xe2, 0x05, 0xfa, 0x98,
22+
0xd2, 0x26, 0x4f, 0x5d, 0xb3, 0x08, 0x02, 0xf2, 0x50, 0x23, 0x5d, 0x9c, 0x6e,
23+
0x63, 0x7e, 0x03, 0x22, 0xa5, 0xb3, 0x5e, 0x95, 0xf2, 0x74, 0xfd, 0x3c, 0x2d,
24+
0x06, 0xf8, 0xdc, 0x34, 0xe4, 0x3d, 0x42, 0x47, 0x7c, 0x61, 0xe6, 0xe1, 0x53
25+
])
26+
27+
biggest_notecard_response = 400
28+
binary_chunk_size = 32
29+
uart_rx_buf_size = biggest_notecard_response + binary_chunk_size
30+
31+
if sys.implementation.name == 'micropython':
32+
from machine import UART
33+
from machine import I2C
34+
from machine import Pin
35+
import board
36+
37+
if use_uart:
38+
port = UART(board.UART, 9600)
39+
port.init(9600, bits=8, parity=None, stop=1,
40+
timeout=3000, timeout_char=100, rxbuf=uart_rx_buf_size)
41+
else:
42+
port = I2C(board.I2C_ID, scl=Pin(board.SCL), sda=Pin(board.SDA))
43+
elif sys.implementation.name == 'circuitpython':
44+
import busio
45+
import board
46+
47+
if use_uart:
48+
port = busio.UART(board.TX, board.RX, baudrate=9600, receiver_buffer_size=uart_rx_buf_size)
49+
else:
50+
port = busio.I2C(board.SCL, board.SDA)
51+
else:
52+
import os
53+
54+
sys.path.insert(0, os.path.abspath(
55+
os.path.join(os.path.dirname(__file__), '..')))
56+
57+
from periphery import I2C
58+
import serial
59+
60+
if use_uart:
61+
port = serial.Serial('/dev/ttyACM0', 9600)
62+
else:
63+
port = I2C('/dev/i2c-1')
64+
65+
import notecard
66+
from notecard import binary_helpers
67+
68+
if use_uart:
69+
card = notecard.OpenSerial(port, debug=True)
70+
else:
71+
card = notecard.OpenI2C(port, 0, 0, debug=True)
72+
73+
print('Clearing out any old data...')
74+
binary_helpers.binary_store_reset(card)
75+
76+
print('Sending buffer...')
77+
binary_helpers.binary_store_transmit(card, tx_buf, 0)
78+
print(f'Sent {len(tx_buf)} bytes to the Notecard.')
79+
80+
print('Reading it back...')
81+
rx_buf = bytearray()
82+
83+
left = binary_helpers.binary_store_decoded_length(card)
84+
offset = 0
85+
while left > 0:
86+
chunk_size = left if binary_chunk_size > left else binary_chunk_size
87+
chunk = binary_helpers.binary_store_receive(card, offset, chunk_size)
88+
rx_buf.extend(chunk)
89+
left -= chunk_size
90+
offset += chunk_size
91+
92+
print(f'Received {len(rx_buf)} bytes from the Notecard.')
93+
94+
print('Checking if received matches transmitted...')
95+
rx_len = len(rx_buf)
96+
tx_len = len(tx_buf)
97+
assert rx_len == tx_len, f'Length mismatch between sent and received data. Sent {tx_len} bytes. Received {rx_len} bytes.'
98+
99+
for idx, (tx_byte, rx_byte) in enumerate(zip(tx_buf, rx_buf)):
100+
assert tx_byte == rx_byte, f'Data mismatch detected at index {idx}. Sent: {tx_byte}. Received: {rx_byte}.'
101+
102+
print('Received matches transmitted.')
103+
print('Example complete.')
104+
105+
106+
if __name__ == '__main__':
107+
product_uid = 'com.your-company.your-project'
108+
# Choose either UART or I2C for Notecard
109+
use_uart = True
110+
run_example(product_uid, use_uart)

notecard/binary_helpers.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Helper methods for doing binary transfers to/from a Notecard."""
2+
3+
import sys
4+
from .cobs import cobs_encode, cobs_decode
5+
from .notecard import Notecard, CARD_INTRA_TRANSACTION_TIMEOUT_SEC
6+
7+
BINARY_RETRIES = 2
8+
9+
if sys.implementation.name == 'cpython':
10+
import hashlib
11+
12+
def _md5_hash(data):
13+
"""Create an MD5 digest of the given data."""
14+
return hashlib.md5(data).hexdigest()
15+
else:
16+
from .md5 import digest as _md5_hash
17+
18+
19+
def binary_store_decoded_length(card: Notecard):
20+
"""Get the length of the decoded binary data store."""
21+
rsp = card.Transaction({'req': 'card.binary'})
22+
# Ignore {bad-bin} errors, but fail on other types of errors.
23+
if 'err' in rsp and '{bad-bin}' not in rsp['err']:
24+
raise Exception(
25+
f'Error in response to card.binary request: {rsp["err"]}.')
26+
27+
return rsp['length'] if 'length' in rsp else 0
28+
29+
30+
def binary_store_reset(card: Notecard):
31+
"""Reset the binary data store."""
32+
rsp = card.Transaction({'req': 'card.binary', 'delete': True})
33+
if 'err' in rsp:
34+
raise Exception(
35+
f'Error in response to card.binary delete request: {rsp["err"]}.')
36+
37+
38+
def binary_store_transmit(card: Notecard, data: bytearray, offset: int):
39+
"""Write bytes to index `offset` of the binary data store."""
40+
# Make a copy of the data to transmit. We do not modify the user's passed in
41+
# `data` object.
42+
tx_data = bytearray(data)
43+
rsp = card.Transaction({'req': 'card.binary'})
44+
45+
# Ignore `{bad-bin}` errors, because we intend to overwrite the data.
46+
if 'err' in rsp and '{bad-bin}' not in rsp['err']:
47+
raise Exception(rsp['err'])
48+
49+
if 'max' not in rsp or rsp['max'] == 0:
50+
raise Exception(('Unexpected card.binary response: max is zero or not '
51+
'present.'))
52+
53+
curr_len = rsp['length'] if 'length' in rsp else 0
54+
if offset != curr_len:
55+
raise Exception('Notecard data length is misaligned with offset.')
56+
57+
max_len = rsp['max']
58+
remaining = max_len - curr_len if offset > 0 else max_len
59+
if len(tx_data) > remaining:
60+
raise Exception(('Data to transmit won\'t fit in the Notecard\'s binary'
61+
' store.'))
62+
63+
encoded = cobs_encode(tx_data, ord('\n'))
64+
req = {
65+
'req': 'card.binary.put',
66+
'cobs': len(encoded),
67+
'status': _md5_hash(tx_data)
68+
}
69+
encoded.append(ord('\n'))
70+
if offset > 0:
71+
req['offset'] = offset
72+
73+
tries = 1 + BINARY_RETRIES
74+
while tries > 0:
75+
try:
76+
# We need to hold the lock for both the card.binary.put transaction
77+
# and the subsequent transmission of the binary data.
78+
card.lock()
79+
80+
# Pass lock=false because we're already locked.
81+
rsp = card.Transaction(req, lock=False)
82+
if 'err' in rsp:
83+
raise Exception(rsp['err'])
84+
85+
# Send the binary data.
86+
card.transmit(encoded, delay=False)
87+
finally:
88+
card.unlock()
89+
90+
rsp = card.Transaction({'req': 'card.binary'})
91+
if 'err' in rsp:
92+
# Retry on {bad-bin} errors.
93+
if '{bad-bin}' in rsp['err']:
94+
tries -= 1
95+
96+
if card._debug and tries > 0:
97+
print('Error during binary transmission, retrying...')
98+
# Fail on all other error types.
99+
else:
100+
raise Exception(rsp['err'])
101+
else:
102+
break
103+
104+
if tries == 0:
105+
raise Exception('Failed to transmit binary data.')
106+
107+
108+
def binary_store_receive(card, offset: int, length: int):
109+
"""Receive `length' bytes from index `offset` of the binary data store."""
110+
req = {
111+
'req': 'card.binary.get',
112+
'offset': offset,
113+
'length': length
114+
}
115+
try:
116+
# We need to hold the lock for both the card.binary.get transaction
117+
# and the subsequent receipt of the binary data.
118+
card.lock()
119+
120+
# Pass lock=false because we're already locked.
121+
rsp = card.Transaction(req, lock=False)
122+
if 'err' in rsp:
123+
raise Exception(rsp['err'])
124+
125+
# Receive the binary data, keeping everything except the last byte,
126+
# which is a newline.
127+
try:
128+
encoded = card.receive(delay=False)[:-1]
129+
except Exception as e:
130+
# Queue up a reset if there was an issue receiving the binary data.
131+
# The reset will attempt to drain the binary data from the Notecard
132+
# so that the comms channel with the Notecard is clean before the
133+
# next transaction.
134+
card._reset_required = True
135+
raise e
136+
137+
finally:
138+
card.unlock()
139+
140+
decoded = cobs_decode(encoded, ord('\n'))
141+
142+
if _md5_hash(decoded) != rsp['status']:
143+
raise Exception('Computed MD5 does not match received MD5.')
144+
145+
return decoded

notecard/cobs.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Methods for COBS encoding and decoding arbitrary bytearrays."""
2+
3+
4+
def cobs_encode(data: bytearray, eop: int) -> bytearray:
5+
"""COBS encode an array of bytes, using eop as the end of packet marker."""
6+
cobs_overhead = 1 + (len(data) // 254)
7+
encoded = bytearray(len(data) + cobs_overhead)
8+
code = 1
9+
idx = 0
10+
code_idx = idx
11+
idx += 1
12+
13+
for byte in data:
14+
if byte != 0:
15+
encoded[idx] = byte ^ eop
16+
idx += 1
17+
code += 1
18+
if byte == 0 or code == 0xFF:
19+
encoded[code_idx] = code ^ eop
20+
code = 1
21+
code_idx = idx
22+
idx += 1
23+
24+
encoded[code_idx] = code ^ eop
25+
26+
return encoded[:idx]
27+
28+
29+
def cobs_decode(encoded: bytes, eop: int) -> bytearray:
30+
"""COBS decode an array of bytes, using eop as the end of packet marker."""
31+
decoded = bytearray(len(encoded))
32+
idx = 0
33+
copy = 0
34+
code = 0xFF
35+
36+
for byte in encoded:
37+
if copy != 0:
38+
decoded[idx] = byte ^ eop
39+
idx += 1
40+
else:
41+
if code != 0xFF:
42+
decoded[idx] = 0
43+
idx += 1
44+
45+
copy = byte ^ eop
46+
code = copy
47+
48+
if code == 0:
49+
break
50+
51+
copy -= 1
52+
53+
return decoded[:idx]

0 commit comments

Comments
 (0)