Skip to content

Commit f090cd4

Browse files
committed
Add GitHub action to keep the usage example synced with the README
1 parent 3232cb9 commit f090cd4

File tree

3 files changed

+272
-0
lines changed

3 files changed

+272
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Update README with Usage Example
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths:
7+
- 'usage_example.py'
8+
- 'README.md'
9+
- '.github/workflows/update-readme.yml' # Also run when this workflow changes
10+
workflow_dispatch: # Allow manual triggering
11+
12+
permissions:
13+
contents: write
14+
15+
jobs:
16+
update-readme:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v4
22+
with:
23+
token: ${{ secrets.GITHUB_TOKEN }}
24+
25+
- name: Update README with latest usage example
26+
run: |
27+
# Read the usage example file
28+
USAGE_EXAMPLE=$(cat usage_example.py)
29+
30+
# Create a Python script to update the README
31+
cat > update_readme.py << 'EOF'
32+
import re
33+
import sys
34+
35+
def update_readme_code_block(readme_content, new_code):
36+
# Pattern to match the code block in the Usage section
37+
pattern = r'(Here\'s a complete working example.*?```python\n)(.*?)(```)'
38+
39+
# Track if we found and replaced the pattern
40+
pattern_found = False
41+
42+
def replacer(match):
43+
nonlocal pattern_found
44+
pattern_found = True
45+
return match.group(1) + new_code + '\n' + match.group(3)
46+
47+
updated_content = re.sub(pattern, replacer, readme_content, flags=re.DOTALL)
48+
49+
if not pattern_found:
50+
print("ERROR: Could not find the code block pattern to update", file=sys.stderr)
51+
return None, False
52+
53+
# Check if the replacement actually changed anything
54+
content_changed = updated_content != readme_content
55+
return updated_content, content_changed
56+
57+
# Read current README
58+
with open('README.md', 'r', encoding='utf-8-sig') as f:
59+
readme_content = f.read()
60+
61+
# Read new code
62+
with open('usage_example.py', 'r', encoding='utf-8-sig') as f:
63+
new_code = f.read()
64+
65+
# Update the README
66+
result = update_readme_code_block(readme_content, new_code)
67+
68+
if result is None or result[0] is None:
69+
print("Failed to update README - code block pattern not found")
70+
sys.exit(1)
71+
72+
updated_readme, content_changed = result
73+
74+
if not content_changed:
75+
print("Code block found and processed, but no changes needed to README.md")
76+
else:
77+
# Write the updated README
78+
with open('README.md', 'w', encoding='utf-8') as f:
79+
f.write(updated_readme)
80+
print("README.md updated with latest usage example")
81+
EOF
82+
83+
# Run the Python script
84+
python3 update_readme.py
85+
86+
- name: Commit and push changes
87+
run: |
88+
git config --local user.email "action@github.com"
89+
git config --local user.name "GitHub Action"
90+
91+
if git diff --quiet; then
92+
echo "No changes to commit"
93+
else
94+
git add README.md
95+
git commit -m "Auto-update README with latest usage example [skip ci]"
96+
git push
97+
fi

README.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Python Reference Implementation of Compact Frame Format
2+
3+
[![PyPI](https://img.shields.io/pypi/v/compact-frame-format.svg)](https://pypi.org/project/compact-frame-format/)
4+
[![Changelog](https://img.shields.io/github/v/release/CompactFrameFormat/compact-frame-format?include_prereleases&label=changelog)](https://github.com/CompactFrameFormat/cff-python/releases)
5+
[![Tests](https://github.com/CompactFrameFormat/cff-python/actions/workflows/test.yml/badge.svg)](https://github.com/CompactFrameFormat/cff-python/actions/workflows/test.yml)
6+
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/CompactFrameFormat/cff-python/blob/master/LICENSE)
7+
8+
## Overview
9+
10+
Compact Frame Format (CFF) is a way of delineating messages (called the _payload_) in a byte stream. It is designed specifically with Microcontrollers (MCUs) in mind, leading to the following design goals:
11+
12+
1. Take advantage of hardware acceleration like Direct Memory Access (DMA) controllers and the CRC peripherals available on most 32-bit MCUs. This precludes the use of delimiter-based packet boundaries that require the CPU to examine every byte.
13+
2. Exploit the fact that modern serial links are already reliable. Universal Serial Bus (USB), Controller Area Network Bus (CANBus), and Bluetooth Low Energy (BLE), already detect and retransmit lost or corrupted packets. Other serial interfaces like Universal Asynchronous Receiver-Transmitters (UART), Serial Peripheral Interface (SPI), and Inter-Integrated Circuit (I2C) are often reliable in practice, with extremely low error rates when the wiring is short and clean. Therefore, the it's okay for error recovery to be expensive, so long as it's possible and the happy path is cheap.
14+
3. Easy to implement and debug. Firmware running on MCUs is not amenable to taking dependencies on 3rd party libraries, so the implementation should be small enough to fit comfortably in a single file, and simple enough that you wouldn't mind implementing it yourself if you had to.
15+
4. Interoperate cleanly with binary serialization formats like [FlatBuffers](https://flatbuffers.dev/) and [CBOR](https://cbor.io/).
16+
17+
In CFF, a frame consists of a header, a payload, and a payload CRC.
18+
19+
```mermaid
20+
block-beta
21+
columns 6
22+
block:Header["Header<br><br><br>"]:4
23+
columns 4
24+
space:4
25+
Preamble FrameCounter["Frame Counter"] PayloadSize["Payload Size"] HeaderCRC["Header CRC"]
26+
end
27+
Payload
28+
PayloadCRC["Payload CRC"]
29+
```
30+
31+
The header consists of:
32+
33+
* A 2-byte preamble: [0xFA, 0xCE]. Note that this is better though of as an array of two bytes rather than an unsigned short (ushort) because it is transmitted as 0xFA, 0xCE (spelling face), whereas the ushort 0xFACE would be transmitted as 0xCE, 0xFA (spelling nothing) in little endian.
34+
* A little-endian ushort frame counter which increments for every frame sent and rolls over to 0 after 65,535 (2^16 - 1) frames have been sent.
35+
* A little-endian ushort payload size, in bytes. This gives a theoretical maximum payload size of 65,535, though few MCU-based applications would want to support this. A protocol making use of CFF can enforce smaller maximum payload sizes if desired. Note that this excludes both the header and the payload CRC at the end. In other words, the _frame_ size is `header_size + payload_size + crc_size`.
36+
* A 16-bit header CRC (see below for details) calculated over the preamble, frame counter, and payload size. This allows the receiver to validate the header and, crucially, the payload size without having to read in the entire frame, as would be the case if there were just one CRC, at the end, covering the entire frame. The problem with having a single CRC is that the if the payload size is corrupted in such a way that it is extremely large (65,535 in the pathological case) the reciever will not detect this until it reads that many bytes, calculates the CRC, and discovers that it doesn't match. Depending on the transmitter's data rate at the time of the error, it could take a long time to receive this many bytes, making the issue look like a dropped link.
37+
38+
Both CRCs are calculated using CRC-16/CCITT-FALSE, with the following settings:
39+
40+
- Width: 16
41+
- Polynomial: 0x1021
42+
- Init: 0xFFFF
43+
- RefIn/RefOut: false / false
44+
- XorOut: 0x0000
45+
- Check("123456789): 0x29B1l
46+
47+
## Usage
48+
49+
### Installation
50+
51+
Install the package using pip:
52+
53+
```powershell
54+
pip install compact-frame-format
55+
```
56+
57+
Or using uv:
58+
59+
```powershell
60+
uv add compact-frame-format
61+
```
62+
63+
### Example
64+
65+
Here's a complete working example (from [`usage_example.py`](https://github.com/CompactFrameFormat/cff-python/blob/main/usage_example.py)):
66+
67+
```python
68+
from compact_frame_format import Cff
69+
70+
# Create and send frames
71+
messages = ["Hello", "World", "CFF"]
72+
buffer = bytearray()
73+
74+
cff = Cff()
75+
76+
print("Creating frames:")
77+
for i, message in enumerate(messages):
78+
payload = message.encode("utf-8")
79+
frame = cff.create(payload)
80+
81+
print(f" Frame {i}: {message} -> {len(frame)} bytes")
82+
83+
# Simulate adding to receive buffer
84+
buffer.extend(frame)
85+
86+
print(f"\nBuffer contains {len(buffer)} bytes total")
87+
88+
# Parse all frames from buffer
89+
received_frames, consumed_bytes = cff.parse_frames(bytes(buffer))
90+
91+
print(f"\nProcessed {consumed_bytes} bytes from buffer")
92+
print("Received frames:")
93+
for frame in received_frames:
94+
message = frame.payload.decode("utf-8")
95+
print(f" Frame {frame.frame_counter}: {message}")
96+
97+
```
98+
99+
## Development
100+
101+
Checkout the code:
102+
```powershell
103+
git clone https://github.com/CompactFrameFormat/cff-python.git
104+
cd cff-python
105+
```
106+
107+
Create a new virtual environment:
108+
```powershell
109+
uv venv && .venv\Scripts\activate.ps1
110+
```
111+
112+
Install dependencies:
113+
```powershell
114+
uv sync --all-extras
115+
```
116+
117+
Install pre-commit hooks:
118+
```powershell
119+
pre-commit install
120+
```
121+
122+
### Code Quality
123+
124+
This project uses [Ruff](https://docs.astral.sh/ruff/) for linting and formatting. Pre-commit hooks are configured to run code quality checks automatically.
125+
126+
Check code formatting and linting:
127+
```powershell
128+
ruff check .
129+
ruff format --check .
130+
```
131+
132+
Format code and fix linting issues:
133+
```powershell
134+
ruff check --fix . && ruff format .
135+
```
136+
137+
### Testing
138+
139+
Run the tests:
140+
```powershell
141+
python -m pytest
142+
```
143+
144+
Run tests with coverage:
145+
```powershell
146+
python -m pytest --cov=compact_frame_format
147+
```

usage_example.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from compact_frame_format import Cff
2+
3+
# Create and send frames
4+
messages = ["Hello", "World", "CFF"]
5+
buffer = bytearray()
6+
7+
cff = Cff()
8+
9+
print("Creating frames:")
10+
for i, message in enumerate(messages):
11+
payload = message.encode("utf-8")
12+
frame = cff.create(payload)
13+
14+
print(f" Frame {i}: {message} -> {len(frame)} bytes")
15+
16+
# Simulate adding to receive buffer
17+
buffer.extend(frame)
18+
19+
print(f"\nBuffer contains {len(buffer)} bytes total")
20+
21+
# Parse all frames from buffer
22+
received_frames, consumed_bytes = cff.parse_frames(bytes(buffer))
23+
24+
print(f"\nProcessed {consumed_bytes} bytes from buffer")
25+
print("Received frames:")
26+
for frame in received_frames:
27+
message = frame.payload.decode("utf-8")
28+
print(f" Frame {frame.frame_counter}: {message}")

0 commit comments

Comments
 (0)