burst-link-protocol 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ # Maintain dependencies for GitHub Actions
4
+ - package-ecosystem: "github-actions"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ groups:
9
+ actions:
10
+ patterns:
11
+ - "*"
@@ -0,0 +1,38 @@
1
+ name: Pip
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ push:
7
+ branches:
8
+ - master
9
+
10
+ jobs:
11
+ build:
12
+ name: Build with Pip
13
+ runs-on: ${{ matrix.platform }}
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ # platform: [windows-latest, macos-latest, ubuntu-latest]
18
+ platform: [ ubuntu-latest]
19
+ python-version: ["3.12"]
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - uses: actions/setup-python@v5
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+
28
+ - name: Set min macOS version
29
+ if: runner.os == 'macOS'
30
+ run: |
31
+ echo "MACOSX_DEPLOYMENT_TARGET=10.14" >> $GITHUB_ENV
32
+
33
+ - name: Build and install
34
+ run: |
35
+ pip install --verbose .
36
+
37
+ - name: Test
38
+ run: python -m pytest
@@ -0,0 +1,82 @@
1
+ name: Wheels
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ push:
7
+ branches:
8
+ - master
9
+ release:
10
+ types:
11
+ - published
12
+
13
+ jobs:
14
+ build_sdist:
15
+ name: Build SDist
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ submodules: true
21
+
22
+ - name: Build SDist
23
+ run: pipx run build --sdist
24
+
25
+ - name: Check metadata
26
+ run: pipx run twine check dist/*
27
+
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: dist-sdist
31
+ path: dist/*.tar.gz
32
+
33
+
34
+ build_wheels:
35
+ name: Wheels on ${{ matrix.os }}
36
+ runs-on: ${{ matrix.os }}
37
+ strategy:
38
+ fail-fast: false
39
+ matrix:
40
+ os: [ubuntu-latest
41
+ # , macos-13, macos-14, windows-latest
42
+ ]
43
+
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+ with:
47
+ submodules: true
48
+
49
+ - uses: pypa/cibuildwheel@v2.22
50
+ # only build 3.12
51
+ env:
52
+ # CIBW_BUILD: "cp312-*"
53
+ CIBW_BUILD: "cp312-manylinux_x86_64"
54
+
55
+ - name: Verify clean directory
56
+ run: git diff --exit-code
57
+ shell: bash
58
+
59
+ - name: Upload wheels
60
+ uses: actions/upload-artifact@v4
61
+ with:
62
+ path: wheelhouse/*.whl
63
+ name: dist-${{ matrix.os }}
64
+
65
+ upload_all:
66
+ name: Upload if release
67
+ needs: [build_wheels, build_sdist]
68
+ runs-on: ubuntu-latest
69
+ # if: github.event_name == 'release' && github.event.action == 'published'
70
+ permissions:
71
+ id-token: write
72
+ environment: pypi_release
73
+
74
+ steps:
75
+ - uses: actions/setup-python@v5
76
+ - uses: actions/download-artifact@v4
77
+ with:
78
+ path: dist
79
+ pattern: dist-*
80
+ merge-multiple: true
81
+
82
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ build
2
+ .venv
3
+ *.whl
4
+ poetry.lock
5
+ **.pyc
6
+ .coverage
7
+ *.tar.gz
@@ -0,0 +1,23 @@
1
+ {
2
+ "configurations": [
3
+ {
4
+ "name": "Win32",
5
+ "includePath": [
6
+ "${workspaceFolder}/**",
7
+ "${workspaceFolder}/.venv/Lib/site-packages/nanobind/include/nanobind", //To be fixed
8
+ "${env:PYTHON_INCLUDE}"
9
+ ],
10
+ "defines": [
11
+ "_DEBUG",
12
+ "UNICODE",
13
+ "_UNICODE"
14
+ ],
15
+ "windowsSdkVersion": "10.0.26100.0",
16
+ "compilerPath": "cl.exe",
17
+ "cStandard": "c17",
18
+ "cppStandard": "c++17",
19
+ "intelliSenseMode": "windows-msvc-x64"
20
+ }
21
+ ],
22
+ "version": 4
23
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Python C++ Debug",
6
+ "type": "pythoncpp",
7
+ "request": "launch",
8
+ "pythonLaunchName": "Python: Current File",
9
+ "cppAttachName": "(Windows) Attach",
10
+ },
11
+ {
12
+ "name": "(Windows) Attach",
13
+ "type": "cppvsdbg",
14
+ "request": "attach",
15
+ "processId": ""
16
+ },
17
+ {
18
+ "name": "Python: Current File",
19
+ "type": "debugpy",
20
+ "request": "launch",
21
+ "program": "test/test.py",
22
+ "console": "integratedTerminal"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,86 @@
1
+ {
2
+ "python-envs.pythonProjects": [],
3
+ "files.associations": {
4
+ "algorithm": "cpp",
5
+ "array": "cpp",
6
+ "atomic": "cpp",
7
+ "bit": "cpp",
8
+ "cctype": "cpp",
9
+ "charconv": "cpp",
10
+ "chrono": "cpp",
11
+ "clocale": "cpp",
12
+ "cmath": "cpp",
13
+ "compare": "cpp",
14
+ "complex": "cpp",
15
+ "concepts": "cpp",
16
+ "cstdarg": "cpp",
17
+ "cstddef": "cpp",
18
+ "cstdint": "cpp",
19
+ "cstdio": "cpp",
20
+ "cstdlib": "cpp",
21
+ "cstring": "cpp",
22
+ "ctime": "cpp",
23
+ "cwchar": "cpp",
24
+ "exception": "cpp",
25
+ "filesystem": "cpp",
26
+ "format": "cpp",
27
+ "forward_list": "cpp",
28
+ "functional": "cpp",
29
+ "initializer_list": "cpp",
30
+ "iomanip": "cpp",
31
+ "ios": "cpp",
32
+ "iosfwd": "cpp",
33
+ "iostream": "cpp",
34
+ "istream": "cpp",
35
+ "iterator": "cpp",
36
+ "limits": "cpp",
37
+ "list": "cpp",
38
+ "locale": "cpp",
39
+ "map": "cpp",
40
+ "memory": "cpp",
41
+ "new": "cpp",
42
+ "optional": "cpp",
43
+ "ostream": "cpp",
44
+ "ratio": "cpp",
45
+ "set": "cpp",
46
+ "sstream": "cpp",
47
+ "stdexcept": "cpp",
48
+ "stop_token": "cpp",
49
+ "streambuf": "cpp",
50
+ "string": "cpp",
51
+ "system_error": "cpp",
52
+ "thread": "cpp",
53
+ "tuple": "cpp",
54
+ "type_traits": "cpp",
55
+ "typeinfo": "cpp",
56
+ "unordered_map": "cpp",
57
+ "unordered_set": "cpp",
58
+ "utility": "cpp",
59
+ "variant": "cpp",
60
+ "vector": "cpp",
61
+ "xfacet": "cpp",
62
+ "xhash": "cpp",
63
+ "xiosbase": "cpp",
64
+ "xlocale": "cpp",
65
+ "xlocbuf": "cpp",
66
+ "xlocinfo": "cpp",
67
+ "xlocmes": "cpp",
68
+ "xlocmon": "cpp",
69
+ "xlocnum": "cpp",
70
+ "xloctime": "cpp",
71
+ "xmemory": "cpp",
72
+ "xstring": "cpp",
73
+ "xtr1common": "cpp",
74
+ "xtree": "cpp",
75
+ "xutility": "cpp",
76
+ "crc.h": "c",
77
+ "decoder.h": "c",
78
+ "stddef.h": "c",
79
+ "stdint.h": "c"
80
+ },
81
+ "python.testing.pytestArgs": [
82
+ "test"
83
+ ],
84
+ "python.testing.unittestEnabled": false,
85
+ "python.testing.pytestEnabled": true
86
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "type": "shell",
6
+ "label": "pip install",
7
+ "command": "pip",
8
+ "args": [
9
+ "install",
10
+ "--no-build-isolation",
11
+ "-ve",
12
+ "."
13
+ ],
14
+ "group": {
15
+ "kind": "build",
16
+ "isDefault": true
17
+ },
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,90 @@
1
+ cmake_minimum_required(VERSION 3.15...3.26)
2
+
3
+ project(i_think_this_name_does_not_matter LANGUAGES C CXX)
4
+
5
+ if (NOT SKBUILD)
6
+ message(WARNING "\
7
+ This CMake file is meant to be executed using 'scikit-build'. Running
8
+ it directly will almost certainly not produce the desired result. If
9
+ you are a user trying to install this package, please use the command
10
+ below, which will install all necessary build dependencies, compile
11
+ the package in an isolated environment, and then install it.
12
+ =====================================================================
13
+ $ pip install .
14
+ =====================================================================
15
+ If you are a software developer, and this is your own package, then
16
+ it is usually much more efficient to install the build dependencies
17
+ in your environment once and use the following command that avoids
18
+ a costly creation of a new virtual environment at every compilation:
19
+ =====================================================================
20
+ $ pip install nanobind scikit-build-core[pyproject]
21
+ $ pip install --no-build-isolation -ve .
22
+ =====================================================================
23
+ You may optionally add -Ceditable.rebuild=true to auto-rebuild when
24
+ the package is imported. Otherwise, you need to re-run the above
25
+ after editing C++ files.")
26
+ endif()
27
+
28
+ # Try to import all Python components potentially needed by nanobind
29
+ find_package(Python 3.8
30
+ REQUIRED COMPONENTS Interpreter Development.Module
31
+ OPTIONAL_COMPONENTS Development.SABIModule)
32
+
33
+ if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
34
+ set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
35
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
36
+ endif()
37
+
38
+ option(COVERAGE "Enable coverage reporting" ON)
39
+ if(COVERAGE)
40
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -fprofile-arcs -ftest-coverage")
41
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g -fprofile-arcs -ftest-coverage")
42
+ endif()
43
+
44
+ execute_process(
45
+ COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
46
+ OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR)
47
+ list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}")
48
+
49
+ # Import nanobind through CMake's find_package mechanism
50
+ find_package(nanobind CONFIG REQUIRED)
51
+
52
+ # We are now ready to compile the actual extension module
53
+ nanobind_add_module(
54
+ # Name of the extension
55
+ burst_interface_c
56
+
57
+ # Target the stable ABI for Python 3.12+, which reduces
58
+ # the number of binary wheels that must be built. This
59
+ # does nothing on older Python versions
60
+ STABLE_ABI
61
+
62
+ # Build libnanobind statically and merge it into the
63
+ # extension (which itself remains a shared library)
64
+ #
65
+ # If your project builds multiple extensions, you can
66
+ # replace this flag by NB_SHARED to conserve space by
67
+ # reusing a shared libnanobind across libraries
68
+ NB_STATIC
69
+
70
+ # Source code goes here
71
+ src/decoder.c
72
+ src/encoder.c
73
+ src/python_bindings.cpp
74
+ src/crc.c
75
+ )
76
+
77
+ nanobind_add_stub(
78
+
79
+ burst_interface_c_stub
80
+ INSTALL_TIME
81
+ MODULE burst_interface_c
82
+ OUTPUT burst_interface_c.pyi
83
+ PYTHON_PATH $<TARGET_FILE_DIR:burst_interface_c>
84
+ DEPENDS burst_interface_c
85
+ )
86
+
87
+ target_include_directories(burst_interface_c PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
88
+
89
+ # Install directive for scikit-build-core
90
+ install(TARGETS burst_interface_c LIBRARY DESTINATION ".")
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.1
2
+ Name: burst-link-protocol
3
+ Version: 0.1.0
4
+ Summary: Binary Utility for Reliable Stream Transfer (BURST) is a library for encoding and decoding binary data streams into and from a byte stream.
5
+ Author-Email: Floris vernieuwe <floris@vernieuwe.eu>
6
+ Requires-Python: <4.0,>=3.10
7
+ Requires-Dist: cobs<2.0.0,>=1.2.1
8
+ Requires-Dist: numpy<3.0.0,>=2.2.3
9
+ Requires-Dist: crc<8.0.0,>=7.1.0
10
+ Requires-Dist: pytest<9.0.0,>=8.3.4
11
+ Requires-Dist: pytest-cov<7.0.0,>=6.0.0
12
+ Requires-Dist: pytest-benchmark<6.0.0,>=5.1.0
13
+ Requires-Dist: scikit-build-core[pyproject]<0.11.0,>=0.10.7; extra == "dev"
14
+ Requires-Dist: nanobind<3.0.0,>=2.5.0; extra == "dev"
15
+ Requires-Dist: pytest<9.0.0,>=8.3.4; extra == "dev"
16
+ Requires-Dist: pytest-cov<7.0.0,>=6.0.0; extra == "dev"
17
+ Requires-Dist: pytest-benchmark<6.0.0,>=5.1.0; extra == "dev"
18
+ Provides-Extra: dev
@@ -0,0 +1,69 @@
1
+ # BURST interface
2
+ Binary Utility for Reliable Stream Transfer (BURST) is a library for encoding and decoding binary data streams, a packet format.
3
+ It combines a 16 bit checksum and cobs encoding to convert packets into a format that can be sent over a stream.
4
+
5
+ This projects is written so it can be used both in python, c and c++ based project
6
+
7
+ # Installation instuctions
8
+
9
+ ## As an user
10
+
11
+ Simple installation
12
+ ```sh
13
+ pip install -e .
14
+ ```
15
+
16
+ ## As a developer
17
+
18
+ Fast build
19
+ ```sh
20
+ pip install --no-build-isolation -ve .
21
+ ```
22
+
23
+ Auto rebuild on run
24
+ ```sh
25
+ pip install --no-build-isolation -Ceditable.rebuild=true -ve .
26
+ ```
27
+
28
+
29
+ ### Python Stub files generation
30
+
31
+ They are generated automatically buy can also be generated
32
+
33
+ ```
34
+ python -m nanobind.stubgen -m nanobind_example_ext
35
+ ```
36
+
37
+ # Publishing instructions
38
+
39
+ ```
40
+
41
+ ```
42
+
43
+ # Test
44
+
45
+ ```sh
46
+ pytest
47
+ ```
48
+
49
+ # BURST protocol
50
+ TODO
51
+ * STAGE 1
52
+ * Convert cpp to c files [OK]
53
+ * Formalise naming [OK]
54
+ * Add c encode functions [OK]
55
+ * Test c encode functions [OK]
56
+ * Update README
57
+ * Improve poetry.toml [OK]
58
+
59
+
60
+ * STAGE 2
61
+ * Add CI/CD on github to compile x86
62
+ * Fix dependencies once compilation succeeds
63
+ * Publish on pypi
64
+ * STAGE 3
65
+ * Add a way to get C test coverage
66
+
67
+
68
+
69
+
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "burst-link-protocol"
3
+ version = "0.1.0" # Choose one version reference here
4
+ description = "Binary Utility for Reliable Stream Transfer (BURST) is a library for encoding and decoding binary data streams into and from a byte stream."
5
+ requires-python = ">=3.10,<4.0"
6
+ authors = [
7
+ { name = "Floris vernieuwe", email = "floris@vernieuwe.eu" }
8
+ ]
9
+
10
+ dependencies = [
11
+ "cobs>=1.2.1,<2.0.0",
12
+ "numpy>=2.2.3,<3.0.0",
13
+ "crc>=7.1.0,<8.0.0",
14
+ "pytest>=8.3.4,<9.0.0",
15
+ "pytest-cov>=6.0.0,<7.0.0",
16
+ "pytest-benchmark>=5.1.0,<6.0.0"
17
+
18
+ ]
19
+
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "scikit-build-core[pyproject]>=0.10.7,<0.11.0",
24
+ "nanobind>=2.5.0,<3.0.0",
25
+ "pytest>=8.3.4,<9.0.0",
26
+ "pytest-cov>=6.0.0,<7.0.0",
27
+ "pytest-benchmark>=5.1.0,<6.0.0"
28
+ ]
29
+
30
+ [build-system]
31
+ requires = ["scikit-build-core>=0.10", "nanobind>=1.3.2"]
32
+ build-backend = "scikit_build_core.build"
33
+
34
+ [tool.scikit-build]
35
+ minimum-version = "build-system.requires"
36
+ cmake.build-type = "Debug"
37
+ cmake.args = ["-DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCOVERAGE=ON"]
38
+ build-dir = "build/{wheel_tag}"
39
+ wheel.py-api = "cp312"
40
+
41
+
42
+
43
+ [tool.cibuildwheel]
44
+ # Necessary to see build output from the actual compilation
45
+ build-verbosity = 1
46
+
47
+ # Run pytest to ensure that the package was correctly built
48
+ # test-command = "pytest test"
49
+ # test-requires = "pytest"
50
+
51
+ # Don't test Python 3.8 wheels on macOS/arm64
52
+ test-skip="cp38-macosx_*:arm64"
53
+
54
+ # Needed for full C++17 support
55
+ [tool.cibuildwheel.macos.environment]
56
+ MACOSX_DEPLOYMENT_TARGET = "10.14"
@@ -0,0 +1,67 @@
1
+ #ifndef BURST_INTERFACE_H
2
+ #define BURST_INTERFACE_H
3
+
4
+ #include <stddef.h>
5
+ #include <stdint.h>
6
+ #include <stdbool.h>
7
+
8
+ #define COBS_DELIMITER 0x00
9
+ #define COBS_MAX_CODE 0xFF
10
+
11
+ // Status codes returned by the encoder.
12
+ typedef enum
13
+ {
14
+ BURST_DATA_CONSUMED,
15
+ BURST_PACKET_READY,
16
+ BURST_OVERFLOW_ERROR,
17
+ BURST_ENCODE_ERROR,
18
+ BURST_CRC_ERROR,
19
+ BURST_DECODE_ERROR
20
+ } burst_status_t;
21
+
22
+ typedef struct
23
+ {
24
+ uint8_t *data;
25
+ size_t size;
26
+ } burst_packet_t;
27
+
28
+ typedef struct
29
+ {
30
+ uint8_t *buffer; // Output buffer for encoded packets.
31
+ size_t buffer_size; // Total size of the output buffer.
32
+ size_t out_head; // Current offset (number of bytes written).
33
+ } burst_encoder_t;
34
+
35
+ typedef struct
36
+ {
37
+ uint8_t *buffer; // Output buffer for decoded data.
38
+ size_t buffer_size; // Size of the output buffer.
39
+ size_t out_head; // Current count of decoded bytes stored.
40
+
41
+ uint8_t current_code; // Current block’s code byte; 0 indicates a new block is
42
+ // expected.
43
+ uint8_t bytes_remaining; // Number of data bytes left to copy for the current
44
+ // block.
45
+ bool pending_zero; // true if a zero should be inserted before starting the
46
+ // next block.
47
+
48
+ bool finished; // true if the packet is complete and available in the buffer
49
+ } burst_decoder_t;
50
+
51
+
52
+ // Encoder
53
+ void burst_encoder_init(burst_encoder_t *ctx, uint8_t *buffer, size_t size);
54
+ burst_status_t burst_encoder_add_packet(burst_encoder_t *ctx, const uint8_t *data, size_t size);
55
+ burst_packet_t burst_encoder_flush(burst_encoder_t *ctx);
56
+
57
+
58
+ // Decoder
59
+ void burst_decoder_init(burst_decoder_t *ctx, uint8_t *buffer, size_t size);
60
+ burst_status_t bust_decoder_add_data(burst_decoder_t *ctx, const uint8_t *data, size_t size,
61
+ size_t *consumed_bytes);
62
+ void burst_decoder_reset(burst_decoder_t *ctx);
63
+ burst_status_t burst_decoder_add_byte(burst_decoder_t *ctx, uint8_t byte);
64
+
65
+ burst_packet_t burst_decoder_get_packet(burst_decoder_t *ctx);
66
+
67
+ #endif // ENCODER_H
@@ -0,0 +1,4 @@
1
+ from burst_interface_c import BurstInterfaceC
2
+ from .main import BurstInterfacePy
3
+
4
+ __all__ = ["BurstInterfaceC", "BurstInterfacePy"]
@@ -0,0 +1,41 @@
1
+ from cobs import cobs
2
+ from crc import Calculator,Crc16
3
+
4
+ crc = Calculator(Crc16.IBM_3740)
5
+
6
+ class BurstInterfacePy:
7
+ buffer = b""
8
+
9
+ def __init__(self):
10
+ pass
11
+
12
+ @staticmethod
13
+ def crc16( data: bytes) -> bytes:
14
+ return crc.checksum(data).to_bytes(2, "big")
15
+
16
+
17
+ @staticmethod
18
+ def encode_packet(packet: bytes) -> bytes:
19
+ packet_with_crc = packet + BurstInterfacePy.crc16(packet)
20
+ return cobs.encode(packet_with_crc) + b"\x00"
21
+
22
+ @staticmethod
23
+ def decode_packet(packet: bytes) -> bytes:
24
+ # decode and check crc
25
+ decoded = cobs.decode(packet)
26
+
27
+ if BurstInterfacePy.crc16(decoded[:-2]) != decoded[-2:]:
28
+ raise ValueError("CRC mismatch")
29
+
30
+ return decoded[:-2]
31
+
32
+ def encode(self, packets: list[bytes]) -> bytes:
33
+ return b"".join([self.encode_packet(packet) for packet in packets])
34
+
35
+ def decode(self, steam: bytes) -> list[bytes]:
36
+ self.buffer += steam
37
+ separated_packets = self.buffer.split(b"\x00")
38
+ # Add last packet to buffer
39
+ self.buffer = separated_packets.pop()
40
+ return [self.decode_packet(packet) for packet in separated_packets]
41
+
@@ -0,0 +1,20 @@
1
+ #include <stddef.h>
2
+ #include <stdint.h>
3
+
4
+ /*
5
+ * Calculate CRC16-CCITT (polynomial 0x1021, initial value 0xFFFF) over
6
+ * the provided data.
7
+ */
8
+ uint16_t crc16_ccitt(const uint8_t *data, size_t length) {
9
+ uint16_t crc = 0xFFFF;
10
+ for (size_t i = 0; i < length; i++) {
11
+ crc ^= ((uint16_t)data[i]) << 8;
12
+ for (int j = 0; j < 8; j++) {
13
+ if (crc & 0x8000)
14
+ crc = (crc << 1) ^ 0x1021;
15
+ else
16
+ crc <<= 1;
17
+ }
18
+ }
19
+ return crc;
20
+ }
@@ -0,0 +1,4 @@
1
+ #include <stdint.h>
2
+
3
+ uint16_t crc16_ccitt(const uint8_t *data, size_t length);
4
+ #define CRC_SIZE sizeof(uint16_t)
@@ -0,0 +1,150 @@
1
+ #include "burst_interface.h"
2
+ #include "crc.h"
3
+ #include <stdbool.h>
4
+ #include <stddef.h>
5
+ #include <stdint.h>
6
+ #include <stdio.h>
7
+
8
+ void burst_decoder_init(burst_decoder_t *ctx, uint8_t *buffer, size_t size)
9
+ {
10
+ ctx->buffer = buffer;
11
+ ctx->buffer_size = size;
12
+ burst_decoder_reset(ctx);
13
+ }
14
+
15
+ burst_status_t bust_decoder_add_data(burst_decoder_t *ctx, const uint8_t *data, size_t size,
16
+ size_t *consumed_bytes)
17
+ {
18
+ // If the decoder was finished, reset it.
19
+ if (ctx->finished)
20
+ {
21
+ burst_decoder_reset(ctx);
22
+ }
23
+
24
+ for (size_t i = 0; i < size; i++)
25
+ {
26
+ uint8_t byte = data[i];
27
+ (*consumed_bytes)++;
28
+
29
+ burst_status_t result = burst_decoder_add_byte(ctx, byte);
30
+
31
+ if (result != BURST_DATA_CONSUMED)
32
+ {
33
+ ctx->finished = true;
34
+ return result;
35
+ }
36
+ }
37
+ return BURST_DATA_CONSUMED;
38
+ }
39
+ void burst_decoder_reset(burst_decoder_t *ctx)
40
+ {
41
+ ctx->out_head = 0;
42
+ ctx->current_code = 0;
43
+ ctx->bytes_remaining = 0;
44
+ ctx->pending_zero = false;
45
+ ctx->finished = false;
46
+ }
47
+
48
+ burst_status_t burst_decoder_complete_packet(burst_decoder_t *ctx)
49
+ {
50
+ // Ensure we have at least two bytes for the CRC.
51
+ if (ctx->out_head < CRC_SIZE)
52
+ {
53
+ return BURST_CRC_ERROR;
54
+ }
55
+
56
+ // Calculate the CRC over the packet data excluding the last two CRC bytes.
57
+ uint16_t computed_crc = crc16_ccitt(ctx->buffer, ctx->out_head - CRC_SIZE);
58
+
59
+ // Extract the received CRC from the last two bytes (big-endian).
60
+ uint16_t received_crc =
61
+ ((uint16_t)ctx->buffer[ctx->out_head - CRC_SIZE] << 8) |
62
+ ctx->buffer[ctx->out_head - 1];
63
+
64
+ // Check if the CRCs match.
65
+ if (computed_crc != received_crc)
66
+ {
67
+ return BURST_CRC_ERROR;
68
+ }
69
+
70
+ // CRC check passed, we can remove it from the packet.
71
+ ctx->out_head -= CRC_SIZE;
72
+ return BURST_PACKET_READY;
73
+ }
74
+
75
+ // If byte is a delimiter but a block is not complete, return COBS_DECODE_ERROR
76
+ // If the buffer is full, return COBS_OVERFLOW_ERROR
77
+ // If the byte is consumed, but the packet is not complete, return COBS_DATA_CONSUMED
78
+ // If the packet is complete, return COBS_PACKET_READY
79
+ burst_status_t burst_decoder_add_byte(burst_decoder_t *ctx, uint8_t byte)
80
+ {
81
+ // Check if there is space for more data.
82
+ if (ctx->out_head >= ctx->buffer_size) {
83
+ return BURST_OVERFLOW_ERROR;
84
+ }
85
+
86
+ // If the byte is a delimiter, decide if it terminates the packet.
87
+ if (byte == COBS_DELIMITER) {
88
+ // If in the middle of a block, a delimiter is not allowed.
89
+ if (ctx->current_code != 0) {
90
+ return BURST_DECODE_ERROR;
91
+ }
92
+ // Otherwise, the packet is complete.
93
+ return burst_decoder_complete_packet(ctx);
94
+ }
95
+
96
+ // If a zero is pending from a previous block, insert it now.
97
+ if (ctx->pending_zero) {
98
+ if (ctx->out_head >= ctx->buffer_size) {
99
+ return BURST_OVERFLOW_ERROR;
100
+ }
101
+ ctx->buffer[ctx->out_head++] = COBS_DELIMITER;
102
+ ctx->pending_zero = false;
103
+ // Now, treat the current byte as a new block code.
104
+ ctx->current_code = byte;
105
+ ctx->bytes_remaining = (byte > 0 ? byte - 1 : 0);
106
+ return BURST_DATA_CONSUMED;
107
+ }
108
+
109
+ // If not currently in a block, this byte is the new block code.
110
+ if (ctx->current_code == 0) {
111
+ ctx->current_code = byte;
112
+ ctx->bytes_remaining = (byte > 0 ? byte - 1 : 0);
113
+ return BURST_DATA_CONSUMED;
114
+ }
115
+
116
+ // Otherwise, we are in the middle of a block so treat the byte as data.
117
+ ctx->buffer[ctx->out_head++] = byte;
118
+ if (ctx->bytes_remaining > 0) {
119
+ ctx->bytes_remaining--;
120
+ }
121
+
122
+ // When the block is complete...
123
+ if (ctx->bytes_remaining == 0) {
124
+ // If the block's code is less than COBS_MAX_CODE, a zero is pending.
125
+ if (ctx->current_code < COBS_MAX_CODE) {
126
+ ctx->pending_zero = true;
127
+ }
128
+ ctx->current_code = 0;
129
+ }
130
+
131
+ return BURST_DATA_CONSUMED;
132
+ }
133
+
134
+
135
+ burst_packet_t burst_decoder_get_packet(burst_decoder_t *ctx)
136
+ {
137
+
138
+ if (!ctx->finished)
139
+ {
140
+ burst_packet_t packet;
141
+ packet.data = NULL;
142
+ packet.size = 0;
143
+ return packet;
144
+ }
145
+
146
+ burst_packet_t packet;
147
+ packet.data = ctx->buffer;
148
+ packet.size = ctx->out_head;
149
+ return packet;
150
+ }
@@ -0,0 +1,91 @@
1
+ #include "burst_interface.h"
2
+ #include "crc.h" // Assumes crc16_ccitt() is available.
3
+ #include <stddef.h>
4
+ #include <stdint.h>
5
+
6
+ // COBS encoding with CRC appending.
7
+ // The input to be encoded is the raw packet data followed by the two CRC bytes.
8
+ // The algorithm works by maintaining a "code" (the count of nonzero bytes)
9
+ // and inserting that count at the start of each block. When a zero is encountered
10
+ // (or when the block length reaches COBS_MAX_CODE) the block is terminated.
11
+ burst_status_t burst_encoder_add_packet(burst_encoder_t *ctx, const uint8_t *data, size_t size)
12
+ {
13
+ // Compute the CRC over the raw packet data.
14
+ uint16_t crc = crc16_ccitt(data, size);
15
+ uint8_t crc_high = (crc >> 8) & 0xFF;
16
+ uint8_t crc_low = crc & 0xFF;
17
+ // The total number of bytes to encode: raw data + 2 bytes of CRC.
18
+ size_t total_bytes = size + CRC_SIZE;
19
+
20
+ // Initialize COBS block state.
21
+ uint8_t code = 1; // Code value starts at 1.
22
+ // Reserve space for the code byte.
23
+ if (ctx->out_head >= ctx->buffer_size)
24
+ return BURST_OVERFLOW_ERROR;
25
+ size_t code_index = ctx->out_head;
26
+ ctx->buffer[ctx->out_head++] = 0; // Placeholder for the code.
27
+
28
+ // Process each byte from the raw data and then the CRC.
29
+ for (size_t i = 0; i < total_bytes; i++) {
30
+ uint8_t byte;
31
+ if (i < size)
32
+ byte = data[i];
33
+ else if (i == size)
34
+ byte = crc_high;
35
+ else // i == size + 1
36
+ byte = crc_low;
37
+
38
+ if (byte == 0) {
39
+ // Write the current code to the reserved position.
40
+ ctx->buffer[code_index] = code;
41
+ // Start a new block.
42
+ code = 1;
43
+ if (ctx->out_head >= ctx->buffer_size)
44
+ return BURST_OVERFLOW_ERROR;
45
+ code_index = ctx->out_head;
46
+ ctx->buffer[ctx->out_head++] = 0; // Reserve placeholder for new code.
47
+ } else {
48
+ // Append the nonzero byte.
49
+ if (ctx->out_head >= ctx->buffer_size)
50
+ return BURST_OVERFLOW_ERROR;
51
+ ctx->buffer[ctx->out_head++] = byte;
52
+ code++;
53
+ // If the maximum code value is reached, finish the block.
54
+ if (code == COBS_MAX_CODE) {
55
+ ctx->buffer[code_index] = code;
56
+ code = 1;
57
+ if (ctx->out_head >= ctx->buffer_size)
58
+ return BURST_OVERFLOW_ERROR;
59
+ code_index = ctx->out_head;
60
+ ctx->buffer[ctx->out_head++] = 0; // Reserve new placeholder.
61
+ }
62
+ }
63
+ }
64
+
65
+ // Finalize the last block.
66
+ ctx->buffer[code_index] = code;
67
+
68
+ // Append the packet delimiter.
69
+ if (ctx->out_head >= ctx->buffer_size)
70
+ return BURST_OVERFLOW_ERROR;
71
+ ctx->buffer[ctx->out_head++] = COBS_DELIMITER;
72
+
73
+ return BURST_PACKET_READY;
74
+ }
75
+
76
+ void burst_encoder_init(burst_encoder_t *ctx, uint8_t *buffer, size_t size)
77
+ {
78
+ ctx->buffer = buffer;
79
+ ctx->buffer_size = size;
80
+ ctx->out_head = 0;
81
+ }
82
+
83
+ burst_packet_t burst_encoder_flush(burst_encoder_t *ctx)
84
+ {
85
+ burst_packet_t packet;
86
+ packet.data = ctx->buffer;
87
+ packet.size = ctx->out_head;
88
+ // Reset the encoder context for the next use.
89
+ ctx->out_head = 0;
90
+ return packet;
91
+ }
@@ -0,0 +1,80 @@
1
+ #include <nanobind/nanobind.h>
2
+ extern "C"
3
+ {
4
+ #include <burst_interface.h>
5
+ }
6
+
7
+ namespace nb = nanobind;
8
+ using namespace nb::literals;
9
+
10
+ struct BurstInterface
11
+ {
12
+ burst_decoder_t decoder;
13
+ uint8_t decoder_buffer[1024] = {0};
14
+ burst_encoder_t encoder;
15
+ uint8_t encoder_buffer[1024] = {0};
16
+
17
+ BurstInterface()
18
+ {
19
+ burst_decoder_init(&decoder, decoder_buffer, sizeof(decoder_buffer));
20
+ burst_encoder_init(&encoder, encoder_buffer, sizeof(encoder_buffer));
21
+ }
22
+
23
+ nb::list decode(nb::bytes data, bool fail_on_crc_error = false)
24
+ {
25
+ nb::list result;
26
+ uint8_t *data_ptr = (uint8_t *)data.data();
27
+ size_t data_size = data.size();
28
+
29
+ size_t bytes_consumed = 0;
30
+ while (bytes_consumed < data_size)
31
+ {
32
+ burst_status_t status = bust_decoder_add_data(&decoder, data_ptr + bytes_consumed, data_size - bytes_consumed, &bytes_consumed);
33
+
34
+ if (status == BURST_PACKET_READY)
35
+ {
36
+ burst_packet_t packet = burst_decoder_get_packet(&decoder);
37
+ nb::bytes packet_bytes(reinterpret_cast<const char *>(packet.data), packet.size);
38
+ result.append(packet_bytes);
39
+ }
40
+
41
+ if (fail_on_crc_error)
42
+ {
43
+ if (status == BURST_CRC_ERROR)
44
+ {
45
+ throw std::runtime_error("CRC error");
46
+ }
47
+ if (status == BURST_DECODE_ERROR)
48
+ {
49
+ throw std::runtime_error("Decode error");
50
+ }
51
+ if (status == BURST_OVERFLOW_ERROR)
52
+ {
53
+ throw std::runtime_error("Overflow error");
54
+ }
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+
60
+ nb::bytes encode(nb::list data)
61
+ {
62
+ for (size_t i = 0; i < data.size(); i++)
63
+ {
64
+ nb::bytes data_bytes = data[i];
65
+ burst_encoder_add_packet(&encoder, (uint8_t *)data_bytes.data(), data_bytes.size());
66
+ }
67
+ // flush the encoder
68
+ burst_packet_t packet = burst_encoder_flush(&encoder);
69
+ return nb::bytes(reinterpret_cast<const char *>(packet.data), packet.size);
70
+ }
71
+ };
72
+
73
+ NB_MODULE(burst_interface_c, m)
74
+ {
75
+
76
+ nb::class_<BurstInterface>(m, "BurstInterfaceC")
77
+ .def(nb::init<>())
78
+ .def("decode", &BurstInterface::decode, "data"_a, "fail_on_crc_error"_a = false)
79
+ .def("encode", &BurstInterface::encode, "packets"_a);
80
+ }
@@ -0,0 +1,52 @@
1
+
2
+ from burst_link_protocol import BurstInterfacePy,BurstInterfaceC
3
+ import pytest
4
+ import numpy as np
5
+
6
+ def test_c_decoder_python():
7
+ python_interface = BurstInterfacePy()
8
+ c_interface = BurstInterfaceC()
9
+
10
+ packets = [b"Hello, world!", b"Goodbye, world!"]
11
+
12
+ data = python_interface.encode(packets)
13
+ decoded = c_interface.decode(data)
14
+ print(decoded)
15
+ assert packets == decoded
16
+
17
+ def test_python_crc_validation():
18
+ interface = BurstInterfacePy()
19
+ packets = [b"Hello, world!", b"Goodbye, world!"]
20
+
21
+ data = bytearray(interface.encode(packets))
22
+
23
+ for i in range( len(data)):
24
+ c_interface = BurstInterfaceC()
25
+
26
+ data_copy = data.copy()
27
+
28
+ # modify byte x
29
+ data_copy[i] = (data_copy[i] + 1) % 256
30
+ with pytest.raises(Exception):
31
+ decoded = c_interface.decode(bytes(data_copy),fail_on_crc_error=True)
32
+ print(decoded)
33
+ assert len(decoded) == len(packets), f"Expected {len(packets)} packets, got {len(decoded)}"
34
+
35
+ def test_max_size_error():
36
+ interface = BurstInterfacePy()
37
+ c_interface = BurstInterfaceC()
38
+ packets = np.random.bytes(1500)
39
+ with pytest.raises(Exception):
40
+ data = c_interface.decode(interface.encode([packets]),fail_on_crc_error=True)
41
+
42
+ def test_encoding():
43
+ interface = BurstInterfacePy()
44
+ c_interface = BurstInterfaceC()
45
+ packets = [b"Hello, world!", b"Goodbye, world!"]
46
+
47
+ assert interface.encode(packets) == c_interface.encode(packets)
48
+
49
+ # if __name__ == "__main__":
50
+ # # test_c_decoder_python()
51
+ # # test_python_crc_validation()
52
+ # # max_size_error()
@@ -0,0 +1,27 @@
1
+
2
+ from burst_link_protocol import BurstInterfacePy
3
+ import pytest
4
+
5
+ def test_python():
6
+ interface = BurstInterfacePy()
7
+ packets = [b"Hello, world!", b"Goodbye, world!"]
8
+
9
+ data = interface.encode(packets)
10
+ decoded = interface.decode(data)
11
+ assert packets == decoded
12
+
13
+ def test_python_crc_validation():
14
+ interface = BurstInterfacePy()
15
+ packets = [b"Hello, world!", b"Goodbye, world!"]
16
+
17
+ data = bytearray(interface.encode(packets))
18
+
19
+ for i in range( len(data)):
20
+ data_copy = data.copy()
21
+
22
+ # modify byte x
23
+ data_copy[i] = (data_copy[i] + 1) % 256
24
+ with pytest.raises(Exception):
25
+ decoded = interface.decode(bytes(data_copy))
26
+ print(decoded)
27
+ assert len(decoded) == len(packets), f"Expected {len(packets)} packets, got {len(decoded)}"
@@ -0,0 +1,20 @@
1
+
2
+ from burst_link_protocol import BurstInterfacePy,BurstInterfaceC
3
+ import pytest
4
+ import numpy as np
5
+
6
+ def prepare_packets(packet_size:int) -> bytes:
7
+ python_interface = BurstInterfacePy()
8
+ return python_interface.encode([np.zeros(packet_size).tobytes()])
9
+
10
+
11
+
12
+ @pytest.mark.parametrize("n", [1, 10, 100, 1000])
13
+ def test_performance_c(benchmark, n):
14
+ c_interface = BurstInterfaceC()
15
+ benchmark(c_interface.decode, prepare_packets(n))
16
+
17
+ @pytest.mark.parametrize("n", [1, 10, 100, 1000])
18
+ def test_performance_python(benchmark, n):
19
+ c_interface = BurstInterfacePy()
20
+ benchmark(c_interface.decode, prepare_packets(n))