Skip to content

Commit 200e991

Browse files
committed
wip
1 parent 0a89ebe commit 200e991

File tree

8 files changed

+348
-25
lines changed

8 files changed

+348
-25
lines changed
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: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Helper methods for doing binary transfers to/from a Notecard."""
2+
3+
import sys
4+
import hashlib
5+
from .cobs import cobs_encode, cobs_decode
6+
from .notecard import CARD_INTRA_TRANSACTION_TIMEOUT_SEC
7+
8+
9+
if sys.implementation.name == 'micropython':
10+
def _md5_hash(data):
11+
"""Create an MD5 digest of the given data."""
12+
hasher = hashlib.md5()
13+
hasher.update(data)
14+
return hasher.digest().hex()
15+
else:
16+
def _md5_hash(data):
17+
"""Create an MD5 digest of the given data."""
18+
return hashlib.md5(data).hexdigest()
19+
20+
21+
def binary_store_decoded_length(card):
22+
"""Get the length of the decoded binary data store."""
23+
rsp = card.Transaction({'req': 'card.binary'})
24+
if 'err' in rsp:
25+
raise Exception(f'Error in response to card.binary request: {rsp["err"]}.')
26+
27+
return rsp['length']
28+
29+
30+
def binary_store_reset(card):
31+
"""Reset the binary data store."""
32+
rsp = card.Transaction({'req': 'card.binary', 'delete': True})
33+
if 'err' in rsp:
34+
raise Exception(f'Error in response to card.binary delete request: {rsp["err"]}.')
35+
36+
37+
def binary_store_transmit(card, data: bytearray, offset: int):
38+
"""Write bytes to index `offset` of the binary data store."""
39+
rsp = card.Transaction({'req': 'card.binary'})
40+
41+
if 'err' in rsp:
42+
# Swallow `{bad-bin}` errors, because we intend to overwrite the
43+
# data.
44+
if '{bad-bin}' not in rsp['err']:
45+
raise Exception(rsp['err'])
46+
47+
if 'max' not in rsp or rsp['max'] == 0:
48+
raise Exception('Unexpected response: max is zero or not present.')
49+
50+
curr_len = rsp['length'] if 'length' in rsp else 0
51+
if offset != curr_len:
52+
raise Exception('Notecard data length is misaligned with offset.')
53+
54+
max_len = rsp['max']
55+
remaining = max_len - curr_len if offset > 0 else max_len
56+
if len(data) > remaining:
57+
raise Exception('Buffer size exceeds available memory.')
58+
59+
encoded = cobs_encode(data, ord('\n'))
60+
req = {
61+
'req': 'card.binary.put',
62+
'cobs': len(encoded),
63+
'status': _md5_hash(data)
64+
}
65+
encoded.append(ord('\n'))
66+
if offset > 0:
67+
req['offset'] = offset
68+
69+
retries = 3
70+
while retries > 0:
71+
try:
72+
# We need to hold the lock for both the card.binary.put
73+
# transaction and the subsequent transmission of the binary
74+
# data.
75+
card.lock()
76+
77+
# Pass lock=false because we already locked.
78+
rsp = card.Transaction(req, lock=False)
79+
if 'err' in rsp:
80+
raise Exception(rsp['err'])
81+
82+
# Send the binary data.
83+
card.transmit(encoded, delay=False)
84+
finally:
85+
card.unlock()
86+
87+
rsp = card.Transaction({'req': 'card.binary'})
88+
if 'err' in rsp:
89+
# Retry on {bad-bin} errors..
90+
if '{bad-bin}' in rsp['err']:
91+
retries -= 1
92+
else:
93+
raise Exception(rsp['err'])
94+
else:
95+
break
96+
97+
if retries == 0:
98+
raise Exception('Binary data invalid.')
99+
100+
101+
def binary_store_receive(card, offset: int, length: int):
102+
"""Receive `length' bytes from index `offset` of the binary data store."""
103+
req = {
104+
'req': 'card.binary.get',
105+
'offset': offset,
106+
'length': length
107+
}
108+
try:
109+
# We need to hold the lock for both the card.binary.get transaction
110+
# and the subsequent receipt of the binary data.
111+
card.lock()
112+
113+
# Pass lock=false because we already locked.
114+
rsp = card.Transaction(req, lock=False)
115+
if 'err' in rsp:
116+
raise Exception(rsp['err'])
117+
118+
# Receive the binary data, keeping everything except the last byte,
119+
# which is a newline.
120+
encoded = card.receive(delay=False,
121+
timeout_secs=CARD_INTRA_TRANSACTION_TIMEOUT_SEC)[:-1]
122+
finally:
123+
card.unlock()
124+
125+
decoded = cobs_decode(encoded, ord('\n'))
126+
127+
if _md5_hash(decoded) != rsp['status']:
128+
raise Exception('Computed MD5 does not match received MD5.')
129+
130+
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]

test/hitl/conftest.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,19 @@ def setup_host(port, platform, mpy_board):
5353
mkdir_on_host(pyb, '/lib/notecard')
5454
copy_files_to_host(pyb, notecard_files, '/lib/notecard')
5555

56-
# Copy over mpy_example.py. We'll run this example code on the MicroPython
57-
# host to 1) verify that the host is able to use note-python to communicate
58-
# with the Notecard and 2) verify that the example isn't broken.
56+
examples_dir = note_python_root_dir / 'examples'
57+
example_files = [examples_dir / 'binary-mode' / 'binary_loopback_example.py']
5958
if platform == 'circuitpython':
60-
example_file = 'cpy_example.py'
59+
example_files.append(examples_dir / 'notecard-basics' / 'cpy_example.py')
6160
else:
62-
example_file = 'mpy_example.py'
61+
example_files.append(examples_dir / 'notecard-basics' / 'mpy_example.py')
6362
if mpy_board:
6463
boards_dir = note_python_root_dir / 'mpy_board'
6564
board_file_path = boards_dir / f"{mpy_board}.py"
6665
copy_file_to_host(pyb, board_file_path, '/board.py')
6766

68-
examples_dir = note_python_root_dir / 'examples'
69-
example_file_path = examples_dir / 'notecard-basics' / example_file
70-
copy_file_to_host(pyb, example_file_path, '/example.py')
67+
for file in example_files:
68+
copy_file_to_host(pyb, file, f'/{file.name}')
7169

7270
pyb.close()
7371

test/hitl/example_runner.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import pyboard
2+
3+
4+
class ExampleRunner:
5+
def __init__(self, pyboard_port, example_file, product_uid):
6+
self.pyboard_port = pyboard_port
7+
self.example_module = example_file[:-3] # Remove .py suffix.
8+
self.product_uid = product_uid
9+
10+
def run(self, use_uart, assert_success=True):
11+
pyb = pyboard.Pyboard(self.pyboard_port, 115200)
12+
pyb.enter_raw_repl()
13+
try:
14+
cmd = f'from {self.example_module} import run_example; run_example("{self.product_uid}", {use_uart})'
15+
output = pyb.exec(cmd)
16+
output = output.decode()
17+
finally:
18+
pyb.exit_raw_repl()
19+
pyb.close()
20+
21+
print(output)
22+
assert 'Example complete.' in output
23+
return output

test/hitl/test_basic_comms.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
import pyboard
22
import pytest
3+
from example_runner import ExampleRunner
34

45

5-
def run_example(port, product_uid, use_uart):
6-
pyb = pyboard.Pyboard(port, 115200)
7-
pyb.enter_raw_repl()
8-
try:
9-
cmd = f'from example import run_example; run_example("{product_uid}", {use_uart})'
10-
output = pyb.exec(cmd)
11-
output = output.decode()
12-
print(output)
13-
assert 'Example complete.' in output
14-
finally:
15-
pyb.exit_raw_repl()
16-
pyb.close()
6+
def run_basic_comms_test(config, use_uart):
7+
if config.platform == 'micropython':
8+
example_file = 'mpy_example.py'
9+
elif config.platform == 'circuitpython':
10+
example_file = 'cpy_example.py'
11+
else:
12+
raise Exception(f'Unsupported platform: {config.platform}')
1713

14+
runner = ExampleRunner(config.port, example_file, config.product_uid)
15+
runner.run(use_uart)
1816

19-
def test_example_i2c(pytestconfig):
20-
run_example(pytestconfig.port, pytestconfig.product_uid, use_uart=False)
2117

22-
23-
def test_example_serial(pytestconfig):
24-
run_example(pytestconfig.port, pytestconfig.product_uid, use_uart=True)
18+
@pytest.mark.parametrize('use_uart', [False, True])
19+
def test_basic_comms(pytestconfig, use_uart):
20+
run_basic_comms_test(pytestconfig, use_uart)

test/hitl/test_binary.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import pyboard
2+
import pytest
3+
from example_runner import ExampleRunner
4+
5+
def run_binary_test(config, use_uart):
6+
runner = ExampleRunner(config.port, 'binary_loopback_example.py', config.product_uid)
7+
runner.run(use_uart)
8+
9+
def test_binary_i2c(pytestconfig):
10+
run_binary_test(pytestconfig, False)
11+
12+
def test_binary_serial(pytestconfig):
13+
run_binary_test(pytestconfig, True)

test/test_binary_helpers.py

Whitespace-only changes.

0 commit comments

Comments
 (0)