Skip to content

Commit 3f1d360

Browse files
committed
Initial commit
0 parents  commit 3f1d360

20 files changed

+1856
-0
lines changed

.coverage

52 KB
Binary file not shown.

.github/workflows/publish.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Publish Python Package
2+
3+
on:
4+
release:
5+
types: [created]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
cache: pip
23+
cache-dependency-path: pyproject.toml
24+
- name: Install dependencies
25+
run: |
26+
pip install '.[test]'
27+
- name: Run tests
28+
run: |
29+
python -m pytest
30+
deploy:
31+
runs-on: ubuntu-latest
32+
needs: [test]
33+
environment: release
34+
permissions:
35+
id-token: write
36+
steps:
37+
- uses: actions/checkout@v4
38+
- name: Set up Python
39+
uses: actions/setup-python@v5
40+
with:
41+
python-version: "3.12"
42+
cache: pip
43+
cache-dependency-path: pyproject.toml
44+
- name: Install dependencies
45+
run: |
46+
pip install setuptools wheel build
47+
- name: Build
48+
run: |
49+
python -m build
50+
- name: Publish
51+
uses: pypa/gh-action-pypi-publish@release/v1
52+

.github/workflows/test.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Test
2+
3+
on: [push, pull_request]
4+
5+
permissions:
6+
contents: read
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
python-version: ["3.10", "3.11", "3.12"]
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up Python ${{ matrix.python-version }}
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: ${{ matrix.python-version }}
20+
cache: pip
21+
cache-dependency-path: pyproject.toml
22+
- name: Install dependencies
23+
run: |
24+
pip install '.[test]'
25+
- name: Run tests
26+
run:
27+
python -m pytest --cov=compact_frame_format --cov-report=xml --cov-fail-under=95
28+

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.venv
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
venv
6+
.eggs
7+
.pytest_cache
8+
*.egg-info
9+
.DS_Store
10+
.vscode

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Richard Keelan
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
## Setup
48+
49+
Checkout the code:
50+
```powershell
51+
git clone https://github.com/CompactFrameFormat/cff-python.git
52+
cd cff-python
53+
```
54+
55+
Create a new virtual environment:
56+
```powershell
57+
uv venv && .venv\Scripts\activate.ps1
58+
```
59+
60+
Install dependencies (including test):
61+
```powershell
62+
uv pip install -e '.[test]'
63+
```
64+
65+
Run the tests:
66+
```powershell
67+
python -m pytest
68+
```

compact_frame_format/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .cff import Frame, PREAMBLE, HEADER_SIZE, PAYLOAD_CRC_SIZE
2+
3+
__all__ = ["Frame", "PREAMBLE", "HEADER_SIZE", "PAYLOAD_CRC_SIZE"]

0 commit comments

Comments
 (0)