structwire 0.1.0__tar.gz → 0.1.0.post1__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.
- {structwire-0.1.0 → structwire-0.1.0.post1}/CONTRIBUTING.md +8 -7
- {structwire-0.1.0/src/structwire.egg-info → structwire-0.1.0.post1}/PKG-INFO +30 -17
- {structwire-0.1.0 → structwire-0.1.0.post1}/README.md +29 -16
- structwire-0.1.0.post1/RELEASING.md +52 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/docs/COMPATIBILITY.md +7 -2
- structwire-0.1.0.post1/docs/INTEGRATION_MODEL.md +61 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/docs/SCHEMA.md +7 -7
- structwire-0.1.0.post1/examples/bcd_record.py +14 -0
- structwire-0.1.0.post1/examples/ble_payload.py +21 -0
- structwire-0.1.0.post1/examples/catalog_demo.py +21 -0
- structwire-0.1.0.post1/examples/codecs.yaml +43 -0
- structwire-0.1.0.post1/examples/packed_fields.py +14 -0
- structwire-0.1.0.post1/examples/transport_bindings.yaml +35 -0
- structwire-0.1.0.post1/examples/uart_bindings.yaml +13 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/scripts/check_dist.py +3 -1
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire/__init__.py +1 -1
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire/model.py +1 -1
- {structwire-0.1.0 → structwire-0.1.0.post1/src/structwire.egg-info}/PKG-INFO +30 -17
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire.egg-info/SOURCES.txt +3 -4
- {structwire-0.1.0 → structwire-0.1.0.post1}/tests/test_codec.py +16 -18
- {structwire-0.1.0 → structwire-0.1.0.post1}/tests/test_examples.py +1 -1
- {structwire-0.1.0 → structwire-0.1.0.post1}/tests/test_loaders.py +4 -4
- {structwire-0.1.0 → structwire-0.1.0.post1}/tests/test_schema.py +5 -5
- structwire-0.1.0/RELEASING.md +0 -31
- structwire-0.1.0/docs/AUTOSPAN_ADOPTION.md +0 -41
- structwire-0.1.0/docs/INTEGRATION_MODEL.md +0 -68
- structwire-0.1.0/examples/automotive_bindings.yaml +0 -36
- structwire-0.1.0/examples/ble_payload.py +0 -14
- structwire-0.1.0/examples/can_like_signal.py +0 -12
- structwire-0.1.0/examples/catalog_demo.py +0 -19
- structwire-0.1.0/examples/codecs.yaml +0 -59
- structwire-0.1.0/examples/diagnostic_record.py +0 -13
- structwire-0.1.0/examples/uart_bindings.yaml +0 -13
- {structwire-0.1.0 → structwire-0.1.0.post1}/LICENSE +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/MANIFEST.in +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/SECURITY.md +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/docs/API.md +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/docs/DESIGN.md +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/pyproject.toml +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/scripts/check_readme.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/scripts/verify_install.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/setup.cfg +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire/catalog.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire/codec.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire/errors.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire/formatting.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire/loaders.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire/py.typed +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire.egg-info/dependency_links.txt +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire.egg-info/requires.txt +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/src/structwire.egg-info/top_level.txt +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/tests/test_check_readme.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/tests/test_compilation.py +0 -0
- {structwire-0.1.0 → structwire-0.1.0.post1}/tests/test_properties.py +0 -0
|
@@ -30,7 +30,7 @@ coverage. Document byte order, width and failure behavior. Update the
|
|
|
30
30
|
output, or schema validation. Add boundary and differential/property tests for
|
|
31
31
|
numeric changes.
|
|
32
32
|
|
|
33
|
-
Pytest executes the
|
|
33
|
+
Pytest executes the Python examples and every Python block in the README and
|
|
34
34
|
`docs/`. YAML snippets are parsed and their StructWire schemas compiled; application
|
|
35
35
|
routing examples are checked for valid nested layouts. The example routing files
|
|
36
36
|
are checked against the supplied catalog. Property tests compare integer and float
|
|
@@ -81,9 +81,10 @@ See [releasing](RELEASING.md) for publication using those artifacts.
|
|
|
81
81
|
|
|
82
82
|
## Source material
|
|
83
83
|
|
|
84
|
-
Contribute only source, schemas and fixtures that you have the right to publish
|
|
85
|
-
under MIT.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
Contribute only source, schemas, and fixtures that you have the right to publish
|
|
85
|
+
under MIT. Use invented, vendor-neutral examples rather than proprietary
|
|
86
|
+
identifiers, session tables, packet definitions, logs, or internal names.
|
|
87
|
+
Keep private payload catalogs, bench configurations, and credentials out of
|
|
88
|
+
this repository. Application-specific routing and payload definitions belong
|
|
89
|
+
in the consuming application unless publication rights are explicit.
|
|
90
|
+
Automated checks cannot establish those rights.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structwire
|
|
3
|
-
Version: 0.1.0
|
|
3
|
+
Version: 0.1.0.post1
|
|
4
4
|
Summary: A small schema-driven binary payload codec for Python.
|
|
5
5
|
Author: Nidhal Abidi
|
|
6
6
|
License-Expression: MIT
|
|
@@ -49,7 +49,13 @@ Your application supplies the bytes and handles transport I/O and routing.
|
|
|
49
49
|
|
|
50
50
|
## Install
|
|
51
51
|
|
|
52
|
-
Python 3.11 or newer is required. Install from
|
|
52
|
+
Python 3.11 or newer is required. Install from PyPI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pip install "structwire>=0.1.0.post1,<0.2"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or install from the source checkout:
|
|
53
59
|
|
|
54
60
|
```bash
|
|
55
61
|
git clone https://github.com/nidops/structwire.git
|
|
@@ -57,7 +63,7 @@ cd structwire
|
|
|
57
63
|
python -m pip install .
|
|
58
64
|
```
|
|
59
65
|
|
|
60
|
-
Version `0.1.0` is an alpha API. PyYAML is installed automatically for YAML loading.
|
|
66
|
+
Version `0.1.0.post1` is an alpha API. PyYAML is installed automatically for YAML loading.
|
|
61
67
|
|
|
62
68
|
## Quick start
|
|
63
69
|
|
|
@@ -65,13 +71,13 @@ Version `0.1.0` is an alpha API. PyYAML is installed automatically for YAML load
|
|
|
65
71
|
from structwire import Field, Layout
|
|
66
72
|
|
|
67
73
|
codec = Layout(
|
|
68
|
-
Field("
|
|
69
|
-
Field("
|
|
74
|
+
Field("level", "i16le", scale=0.1),
|
|
75
|
+
Field("quality", "u8"),
|
|
70
76
|
).compile()
|
|
71
77
|
|
|
72
|
-
payload = codec.encode({"
|
|
78
|
+
payload = codec.encode({"level": 23.5, "quality": 1})
|
|
73
79
|
assert payload == bytes.fromhex("eb 00 01")
|
|
74
|
-
assert codec.decode(payload) == {"
|
|
80
|
+
assert codec.decode(payload) == {"level": 23.5, "quality": 1}
|
|
75
81
|
```
|
|
76
82
|
|
|
77
83
|
Compile a layout once and reuse its codec.
|
|
@@ -83,27 +89,27 @@ Save this as `codecs.yaml`:
|
|
|
83
89
|
```yaml
|
|
84
90
|
version: 1
|
|
85
91
|
codecs:
|
|
86
|
-
|
|
92
|
+
frame_status:
|
|
87
93
|
size: 4
|
|
88
94
|
fields:
|
|
89
|
-
- name:
|
|
95
|
+
- name: phase
|
|
90
96
|
type: enum
|
|
91
|
-
values: {0:
|
|
92
|
-
- name:
|
|
97
|
+
values: {0: idle, 1: running, 2: paused}
|
|
98
|
+
- name: value
|
|
93
99
|
type: uint16_le
|
|
94
100
|
offset: 2
|
|
95
|
-
scale: 0.
|
|
96
|
-
unit:
|
|
101
|
+
scale: 0.01
|
|
102
|
+
unit: arbitrary
|
|
97
103
|
```
|
|
98
104
|
|
|
99
105
|
```python
|
|
100
106
|
from structwire import catalog_from_yaml
|
|
101
107
|
|
|
102
108
|
catalog = catalog_from_yaml("codecs.yaml")
|
|
103
|
-
payload = bytes.fromhex("01 00
|
|
104
|
-
values = catalog.decode("
|
|
105
|
-
assert values == {"
|
|
106
|
-
assert catalog.encode("
|
|
109
|
+
payload = bytes.fromhex("01 00 64 00")
|
|
110
|
+
values = catalog.decode("frame_status", payload)
|
|
111
|
+
assert values == {"phase": "running", "value": 1.0}
|
|
112
|
+
assert catalog.encode("frame_status", values) == payload
|
|
107
113
|
```
|
|
108
114
|
|
|
109
115
|
Offsets and sizes are in bytes. Here, byte 1 is unused and encodes as zero.
|
|
@@ -140,6 +146,13 @@ limits; applications must bound untrusted schema and payload sizes. See
|
|
|
140
146
|
|
|
141
147
|
## Documentation and examples
|
|
142
148
|
|
|
149
|
+
All examples use fictional values and are independent of specific products.
|
|
150
|
+
|
|
151
|
+
- [BCD records](https://github.com/nidops/structwire/blob/master/examples/bcd_record.py)
|
|
152
|
+
- [Packed bit fields](https://github.com/nidops/structwire/blob/master/examples/packed_fields.py)
|
|
153
|
+
- [YAML codec catalog](https://github.com/nidops/structwire/blob/master/examples/codecs.yaml)
|
|
154
|
+
- [BLE-style payload](https://github.com/nidops/structwire/blob/master/examples/ble_payload.py)
|
|
155
|
+
- [Generic message routing](https://github.com/nidops/structwire/blob/master/examples/transport_bindings.yaml)
|
|
143
156
|
- [Python API](https://github.com/nidops/structwire/blob/master/docs/API.md)
|
|
144
157
|
- [Schema reference](https://github.com/nidops/structwire/blob/master/docs/SCHEMA.md)
|
|
145
158
|
- [Examples](https://github.com/nidops/structwire/tree/master/examples)
|
|
@@ -7,7 +7,13 @@ Your application supplies the bytes and handles transport I/O and routing.
|
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
-
Python 3.11 or newer is required. Install from
|
|
10
|
+
Python 3.11 or newer is required. Install from PyPI:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python -m pip install "structwire>=0.1.0.post1,<0.2"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or install from the source checkout:
|
|
11
17
|
|
|
12
18
|
```bash
|
|
13
19
|
git clone https://github.com/nidops/structwire.git
|
|
@@ -15,7 +21,7 @@ cd structwire
|
|
|
15
21
|
python -m pip install .
|
|
16
22
|
```
|
|
17
23
|
|
|
18
|
-
Version `0.1.0` is an alpha API. PyYAML is installed automatically for YAML loading.
|
|
24
|
+
Version `0.1.0.post1` is an alpha API. PyYAML is installed automatically for YAML loading.
|
|
19
25
|
|
|
20
26
|
## Quick start
|
|
21
27
|
|
|
@@ -23,13 +29,13 @@ Version `0.1.0` is an alpha API. PyYAML is installed automatically for YAML load
|
|
|
23
29
|
from structwire import Field, Layout
|
|
24
30
|
|
|
25
31
|
codec = Layout(
|
|
26
|
-
Field("
|
|
27
|
-
Field("
|
|
32
|
+
Field("level", "i16le", scale=0.1),
|
|
33
|
+
Field("quality", "u8"),
|
|
28
34
|
).compile()
|
|
29
35
|
|
|
30
|
-
payload = codec.encode({"
|
|
36
|
+
payload = codec.encode({"level": 23.5, "quality": 1})
|
|
31
37
|
assert payload == bytes.fromhex("eb 00 01")
|
|
32
|
-
assert codec.decode(payload) == {"
|
|
38
|
+
assert codec.decode(payload) == {"level": 23.5, "quality": 1}
|
|
33
39
|
```
|
|
34
40
|
|
|
35
41
|
Compile a layout once and reuse its codec.
|
|
@@ -41,27 +47,27 @@ Save this as `codecs.yaml`:
|
|
|
41
47
|
```yaml
|
|
42
48
|
version: 1
|
|
43
49
|
codecs:
|
|
44
|
-
|
|
50
|
+
frame_status:
|
|
45
51
|
size: 4
|
|
46
52
|
fields:
|
|
47
|
-
- name:
|
|
53
|
+
- name: phase
|
|
48
54
|
type: enum
|
|
49
|
-
values: {0:
|
|
50
|
-
- name:
|
|
55
|
+
values: {0: idle, 1: running, 2: paused}
|
|
56
|
+
- name: value
|
|
51
57
|
type: uint16_le
|
|
52
58
|
offset: 2
|
|
53
|
-
scale: 0.
|
|
54
|
-
unit:
|
|
59
|
+
scale: 0.01
|
|
60
|
+
unit: arbitrary
|
|
55
61
|
```
|
|
56
62
|
|
|
57
63
|
```python
|
|
58
64
|
from structwire import catalog_from_yaml
|
|
59
65
|
|
|
60
66
|
catalog = catalog_from_yaml("codecs.yaml")
|
|
61
|
-
payload = bytes.fromhex("01 00
|
|
62
|
-
values = catalog.decode("
|
|
63
|
-
assert values == {"
|
|
64
|
-
assert catalog.encode("
|
|
67
|
+
payload = bytes.fromhex("01 00 64 00")
|
|
68
|
+
values = catalog.decode("frame_status", payload)
|
|
69
|
+
assert values == {"phase": "running", "value": 1.0}
|
|
70
|
+
assert catalog.encode("frame_status", values) == payload
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
Offsets and sizes are in bytes. Here, byte 1 is unused and encodes as zero.
|
|
@@ -98,6 +104,13 @@ limits; applications must bound untrusted schema and payload sizes. See
|
|
|
98
104
|
|
|
99
105
|
## Documentation and examples
|
|
100
106
|
|
|
107
|
+
All examples use fictional values and are independent of specific products.
|
|
108
|
+
|
|
109
|
+
- [BCD records](https://github.com/nidops/structwire/blob/master/examples/bcd_record.py)
|
|
110
|
+
- [Packed bit fields](https://github.com/nidops/structwire/blob/master/examples/packed_fields.py)
|
|
111
|
+
- [YAML codec catalog](https://github.com/nidops/structwire/blob/master/examples/codecs.yaml)
|
|
112
|
+
- [BLE-style payload](https://github.com/nidops/structwire/blob/master/examples/ble_payload.py)
|
|
113
|
+
- [Generic message routing](https://github.com/nidops/structwire/blob/master/examples/transport_bindings.yaml)
|
|
101
114
|
- [Python API](https://github.com/nidops/structwire/blob/master/docs/API.md)
|
|
102
115
|
- [Schema reference](https://github.com/nidops/structwire/blob/master/docs/SCHEMA.md)
|
|
103
116
|
- [Examples](https://github.com/nidops/structwire/tree/master/examples)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Releasing StructWire
|
|
2
|
+
|
|
3
|
+
Package version lives in `src/structwire/__init__.py`; setuptools derives metadata
|
|
4
|
+
from it. Keep this as the single version source.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
Configure [trusted publishing](https://docs.pypi.org/trusted-publishers/)
|
|
9
|
+
separately on TestPyPI and PyPI for project `structwire`, repository
|
|
10
|
+
`nidops/structwire`, and workflow `publish.yml`. Use GitHub environments `testpypi`
|
|
11
|
+
and `pypi` respectively, restricted to release tags. For a new project, register
|
|
12
|
+
a pending publisher; this does not reserve the package name. Confirm account
|
|
13
|
+
ownership and public documentation links.
|
|
14
|
+
|
|
15
|
+
## Release
|
|
16
|
+
|
|
17
|
+
Before tagging, perform a publication-content review across source, examples,
|
|
18
|
+
tests, documentation, and configuration. Check the actual contents of both built
|
|
19
|
+
archives, not just their filenames. Ensure example schemas, identifiers, byte
|
|
20
|
+
sequences, and names are invented or explicitly cleared for public release.
|
|
21
|
+
Automated checks and passing CI do not prove publication rights.
|
|
22
|
+
|
|
23
|
+
1. Update the package version and documentation.
|
|
24
|
+
2. Run the [development checks](CONTRIBUTING.md#development) and
|
|
25
|
+
[distribution checks](CONTRIBUTING.md#distribution-checks).
|
|
26
|
+
3. From a clean checkout, tag the reviewed `master` commit as `v<package version>`
|
|
27
|
+
after all supported CI jobs pass. For `0.1.0.post1`, use `v0.1.0.post1` and verify
|
|
28
|
+
it with `python scripts/check_dist.py dist --tag v0.1.0.post1`.
|
|
29
|
+
4. Push the tag, then manually run **Publish** in GitHub Actions with that tag selected.
|
|
30
|
+
|
|
31
|
+
The workflow reruns CI, uploads verified artifacts to TestPyPI, checks the downloaded
|
|
32
|
+
wheel's hash and installation, then publishes the same artifacts to PyPI and verifies
|
|
33
|
+
again. Pushes alone do not publish. Check the package page after completion.
|
|
34
|
+
|
|
35
|
+
If publication fails, rerun failed jobs in the same run while its `release-dist`
|
|
36
|
+
artifact is available. Never overwrite tags or uploaded files, or hide mismatches
|
|
37
|
+
with `skip-existing`. Changed release contents require a new version.
|
|
38
|
+
|
|
39
|
+
## Replacing an existing release
|
|
40
|
+
|
|
41
|
+
`0.1.0.post1` replaces the examples, documentation and test fixtures shipped in
|
|
42
|
+
`0.1.0` while preserving library behavior. Publish and verify the replacement on
|
|
43
|
+
both indexes before retiring the earlier release. Consumers with exact `==0.1.0`
|
|
44
|
+
pins must update them; a post-release cannot satisfy that exact pin.
|
|
45
|
+
|
|
46
|
+
[Yanking](https://docs.pypi.org/project-management/yanking/) leaves files available
|
|
47
|
+
to exact pins and direct downloads. If the old files must be removed, delete the
|
|
48
|
+
old release separately on PyPI and TestPyPI after verifying the replacement.
|
|
49
|
+
[Deleted filenames cannot be reused](https://pypi.org/help/#file-name-reuse).
|
|
50
|
+
Deleting an index release does not remove Git tags, branches, source archives,
|
|
51
|
+
or copies already downloaded. Keep recovery branches local when retiring public
|
|
52
|
+
history, and review old public refs separately.
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## API and versions
|
|
4
4
|
|
|
5
|
-
StructWire `0.1.0` is an alpha API. Consumers that depend on this series can use
|
|
6
|
-
`structwire>=0.1,<0.2`.
|
|
5
|
+
StructWire `0.1.0.post1` is an alpha API. Consumers that depend on this series can use
|
|
6
|
+
`structwire>=0.1.0.post1,<0.2`.
|
|
7
|
+
|
|
8
|
+
This post-release updates documentation, examples and test fixtures. The public
|
|
9
|
+
API, schema version and encoding/decoding behavior are unchanged from `0.1.0`.
|
|
10
|
+
The example files are teaching material, outside the library's compatibility
|
|
11
|
+
contract. Exact pins to `structwire==0.1.0` must be updated to select the post-release.
|
|
7
12
|
|
|
8
13
|
Schema documents have a separate `version: 1`. Unsupported schema versions,
|
|
9
14
|
unknown keys, conflicting aliases, and duplicate YAML mapping keys are rejected
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Integration examples
|
|
2
|
+
|
|
3
|
+
These teaching examples use invented identifiers and payloads, not real-world
|
|
4
|
+
message definitions.
|
|
5
|
+
|
|
6
|
+
The application selects a codec and passes it payload bytes. It handles
|
|
7
|
+
identifiers, framing, checksums and device I/O. The YAML below is illustrative
|
|
8
|
+
**application configuration**; StructWire does not load the outer mappings.
|
|
9
|
+
|
|
10
|
+
## Named codec reference
|
|
11
|
+
|
|
12
|
+
A fictional message type can be mapped to a named catalog entry:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
messages:
|
|
16
|
+
by_kind:
|
|
17
|
+
status:
|
|
18
|
+
codec: frame_status
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The application looks up `frame_status` and calls
|
|
22
|
+
`catalog.decode("frame_status", payload)`.
|
|
23
|
+
See [catalog_demo.py](../examples/catalog_demo.py) and
|
|
24
|
+
[codecs.yaml](../examples/codecs.yaml) for runnable examples.
|
|
25
|
+
|
|
26
|
+
## Inline layout
|
|
27
|
+
|
|
28
|
+
Application configuration may also contain a complete synthetic layout:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
messages:
|
|
32
|
+
by_kind:
|
|
33
|
+
custom:
|
|
34
|
+
layout:
|
|
35
|
+
size: 2
|
|
36
|
+
fields:
|
|
37
|
+
- name: category
|
|
38
|
+
type: enum
|
|
39
|
+
values: {0: basic, 1: extended}
|
|
40
|
+
- name: count
|
|
41
|
+
type: uint8
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The application extracts the nested `layout` mapping and passes it to
|
|
45
|
+
`layout_from_mapping(...).compile()`. All routing remains outside StructWire.
|
|
46
|
+
|
|
47
|
+
## Framed byte stream
|
|
48
|
+
|
|
49
|
+
For example, an imaginary stream might contain:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
C7 21 | <6 payload bytes> | checksum
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The application checks the packet envelope, extracts six bytes and uses
|
|
56
|
+
`catalog.decode("environment_sample", payload)`. This library does **not**
|
|
57
|
+
implement the fictional envelope or checksum.
|
|
58
|
+
|
|
59
|
+
The [transport bindings](../examples/transport_bindings.yaml) and
|
|
60
|
+
[UART bindings](../examples/uart_bindings.yaml) illustrate application-owned
|
|
61
|
+
routing independently of any particular framework.
|
|
@@ -52,11 +52,11 @@ Encoding fills unused bytes with zero; decoding does not preserve their values.
|
|
|
52
52
|
- `unit`: display/documentation metadata only
|
|
53
53
|
- `description`: display/documentation metadata only
|
|
54
54
|
|
|
55
|
-
Numeric `bias` also accepts the
|
|
55
|
+
Numeric `bias` also accepts the alias `physical_offset`. If both
|
|
56
56
|
spellings of an alias are supplied, their values must agree. Unknown catalog,
|
|
57
57
|
layout and field keys are rejected.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
A valid `byteorder` is accepted on every kind. Integer/float
|
|
60
60
|
kind names take precedence even when the supplied order disagrees (including a
|
|
61
61
|
little-endian kind with the default `big`). BCD, ASCII and bytes ignore it and
|
|
62
62
|
retain wire order. `describe()` reports the kind-defined order for integers and
|
|
@@ -90,7 +90,7 @@ labels must be strings; field, flag and catalog names must be non-empty. Enum
|
|
|
90
90
|
labels must be unique. Quote YAML labels such as
|
|
91
91
|
`"on"`, `"off"`, `"yes"` and `"no"`: PyYAML otherwise resolves these as booleans.
|
|
92
92
|
|
|
93
|
-
###
|
|
93
|
+
### Kind aliases
|
|
94
94
|
|
|
95
95
|
`uint8`/`int8` map to `u8`/`i8`. For widths 16, 24, 32 and 64,
|
|
96
96
|
`uint<width>_be`/`uint<width>_le` and `int<width>_be`/`int<width>_le` map to their
|
|
@@ -137,12 +137,12 @@ overlapping fields unless their masks are disjoint:
|
|
|
137
137
|
from structwire import Field, Layout
|
|
138
138
|
|
|
139
139
|
status = Layout(
|
|
140
|
-
Field("
|
|
141
|
-
Field("
|
|
140
|
+
Field("upper_nibble", "u8", mask=0xF0, shift=4),
|
|
141
|
+
Field("lower_nibble", "u8", offset=0, mask=0x0F),
|
|
142
142
|
).compile()
|
|
143
143
|
|
|
144
|
-
assert status.decode(b"\x53") == {"
|
|
145
|
-
assert status.encode({"
|
|
144
|
+
assert status.decode(b"\x53") == {"upper_nibble": 5, "lower_nibble": 3}
|
|
145
|
+
assert status.encode({"upper_nibble": 5, "lower_nibble": 3}) == b"\x53"
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
Routing keys belong in project configuration, outside this schema.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from structwire import Field, Layout
|
|
2
|
+
|
|
3
|
+
# Invented numeric record demonstrating one-byte and multi-byte packed BCD.
|
|
4
|
+
record = Layout(
|
|
5
|
+
Field("batch", "bcd", length=2),
|
|
6
|
+
Field("quantity", "bcd"),
|
|
7
|
+
name="example-bcd-record",
|
|
8
|
+
).compile()
|
|
9
|
+
|
|
10
|
+
values = {"batch": 1234, "quantity": 25}
|
|
11
|
+
encoded = record.encode(values)
|
|
12
|
+
|
|
13
|
+
assert encoded == bytes.fromhex("12 34 25")
|
|
14
|
+
assert record.decode(encoded) == values
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from structwire import Field, Layout
|
|
2
|
+
|
|
3
|
+
# Invented BLE application payload. Not based on any product's GATT specification.
|
|
4
|
+
measurement = Layout(
|
|
5
|
+
Field("format", "u8"),
|
|
6
|
+
Field("channel", "u8"),
|
|
7
|
+
Field("temperature_c", "i16le", scale=0.1),
|
|
8
|
+
Field("humidity_pct", "u8"),
|
|
9
|
+
Field("battery_pct", "u8"),
|
|
10
|
+
name="example-ble-measurement",
|
|
11
|
+
).compile()
|
|
12
|
+
|
|
13
|
+
values = {
|
|
14
|
+
"format": 1,
|
|
15
|
+
"channel": 3,
|
|
16
|
+
"temperature_c": 21.5,
|
|
17
|
+
"humidity_pct": 48,
|
|
18
|
+
"battery_pct": 82,
|
|
19
|
+
}
|
|
20
|
+
raw = measurement.encode(values)
|
|
21
|
+
assert measurement.decode(raw) == values
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from structwire import catalog_from_yaml
|
|
4
|
+
|
|
5
|
+
catalog = catalog_from_yaml(Path(__file__).with_name("codecs.yaml"))
|
|
6
|
+
|
|
7
|
+
# Purely synthetic example bytes: phase=running, both flags set, value=100.
|
|
8
|
+
payload = bytes.fromhex("01 03 10 27")
|
|
9
|
+
decoded = catalog.decode("frame_status", payload)
|
|
10
|
+
assert decoded == {"phase": "running", "ready": True, "flagged": True, "value": 100.0}
|
|
11
|
+
assert catalog.encode("frame_status", decoded) == payload
|
|
12
|
+
|
|
13
|
+
values = {
|
|
14
|
+
"channel": 7,
|
|
15
|
+
"state": "sampling",
|
|
16
|
+
"temperature_c": 21.5,
|
|
17
|
+
"humidity_pct": 48.0,
|
|
18
|
+
}
|
|
19
|
+
encoded = catalog.encode("environment_sample", values)
|
|
20
|
+
assert catalog.decode("environment_sample", encoded) == values
|
|
21
|
+
print(encoded.hex(" ").upper())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# All layouts and values are fictional teaching examples, not production formats.
|
|
2
|
+
version: 1
|
|
3
|
+
|
|
4
|
+
codecs:
|
|
5
|
+
frame_status:
|
|
6
|
+
description: Synthetic packed record demonstrating enums, bits and scaling.
|
|
7
|
+
size: 4
|
|
8
|
+
fields:
|
|
9
|
+
- name: phase
|
|
10
|
+
type: enum
|
|
11
|
+
values: {0: idle, 1: running, 2: paused}
|
|
12
|
+
- name: ready
|
|
13
|
+
type: bool
|
|
14
|
+
offset: 1
|
|
15
|
+
bitmask: 0x01
|
|
16
|
+
- name: flagged
|
|
17
|
+
type: bool
|
|
18
|
+
offset: 1
|
|
19
|
+
bitmask: 0x02
|
|
20
|
+
bitshift: 1
|
|
21
|
+
- name: value
|
|
22
|
+
type: uint16_le
|
|
23
|
+
offset: 2
|
|
24
|
+
scale: 0.01
|
|
25
|
+
unit: arbitrary
|
|
26
|
+
|
|
27
|
+
environment_sample:
|
|
28
|
+
description: Synthetic six-byte environmental measurement.
|
|
29
|
+
size: 6
|
|
30
|
+
fields:
|
|
31
|
+
- name: channel
|
|
32
|
+
type: uint8
|
|
33
|
+
- name: state
|
|
34
|
+
type: enum
|
|
35
|
+
values: {0: idle, 1: sampling, 2: paused}
|
|
36
|
+
- name: temperature_c
|
|
37
|
+
type: int16_le
|
|
38
|
+
scale: 0.01
|
|
39
|
+
unit: degC
|
|
40
|
+
- name: humidity_pct
|
|
41
|
+
type: uint16_le
|
|
42
|
+
scale: 0.01
|
|
43
|
+
unit: pct
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from structwire import Field, Layout
|
|
2
|
+
|
|
3
|
+
# Invented record demonstrating two disjoint nibble masks in the same byte.
|
|
4
|
+
record = Layout(
|
|
5
|
+
Field("upper_code", "u8", mask=0xF0, shift=4),
|
|
6
|
+
Field("lower_code", "u8", offset=0, mask=0x0F),
|
|
7
|
+
Field("reading", "u16le", scale=0.1),
|
|
8
|
+
name="example-packed-fields",
|
|
9
|
+
).compile()
|
|
10
|
+
|
|
11
|
+
values = {"upper_code": 2, "lower_code": 7, "reading": 12.5}
|
|
12
|
+
payload = record.encode(values)
|
|
13
|
+
assert record.decode(payload) == values
|
|
14
|
+
print(payload.hex(" ").upper())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Invented routing keys, not values from any real device or protocol.
|
|
2
|
+
# This is application configuration, not a StructWire schema.
|
|
3
|
+
messages:
|
|
4
|
+
by_kind:
|
|
5
|
+
sample:
|
|
6
|
+
codec: environment_sample
|
|
7
|
+
status:
|
|
8
|
+
codec: frame_status
|
|
9
|
+
by_address:
|
|
10
|
+
0x10:
|
|
11
|
+
codec: frame_status
|
|
12
|
+
0x20:
|
|
13
|
+
codec: environment_sample
|
|
14
|
+
address_ranges:
|
|
15
|
+
- base: 0x40
|
|
16
|
+
count: 4
|
|
17
|
+
codec: environment_sample
|
|
18
|
+
index_name: channel
|
|
19
|
+
|
|
20
|
+
# Only the nested layout is passed to layout_from_mapping().
|
|
21
|
+
inline_example:
|
|
22
|
+
kinds:
|
|
23
|
+
custom:
|
|
24
|
+
layout:
|
|
25
|
+
size: 4
|
|
26
|
+
fields:
|
|
27
|
+
- name: category
|
|
28
|
+
type: enum
|
|
29
|
+
values: {0: basic, 1: extended}
|
|
30
|
+
- name: count
|
|
31
|
+
type: uint8
|
|
32
|
+
- name: ratio
|
|
33
|
+
type: uint16_le
|
|
34
|
+
scale: 0.01
|
|
35
|
+
unit: pct
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Completely fictional packet envelope used to illustrate application-side routing.
|
|
2
|
+
# The application owns framing, checksums and I/O; StructWire sees only payload bytes.
|
|
3
|
+
# Example: [marker=0xC7][kind=0x21][6-byte payload][checksum]
|
|
4
|
+
uart:
|
|
5
|
+
messages:
|
|
6
|
+
sample:
|
|
7
|
+
match:
|
|
8
|
+
marker: 0xC7
|
|
9
|
+
kind: 0x21
|
|
10
|
+
payload:
|
|
11
|
+
offset: 2
|
|
12
|
+
length: 6
|
|
13
|
+
codec: environment_sample
|
|
@@ -135,7 +135,9 @@ def inspect_distributions(directory: Path, tag: str | None = None) -> dict:
|
|
|
135
135
|
def main() -> None:
|
|
136
136
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
137
137
|
parser.add_argument("directory", type=Path, nargs="?", default=Path("dist"))
|
|
138
|
-
parser.add_argument(
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--tag", help="Require a version tag matching the built version (e.g. v0.1.0.post1)"
|
|
140
|
+
)
|
|
139
141
|
args = parser.parse_args()
|
|
140
142
|
print(json.dumps(inspect_distributions(args.directory, args.tag), indent=2))
|
|
141
143
|
|
|
@@ -78,7 +78,7 @@ def _schema_integer(value: Any, where: str) -> int:
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
def canonical_kind(kind: str) -> str:
|
|
81
|
-
"""Return a stable kind name, accepting
|
|
81
|
+
"""Return a stable kind name, accepting kind aliases."""
|
|
82
82
|
if not isinstance(kind, str):
|
|
83
83
|
raise SchemaError("Field kind must be a string")
|
|
84
84
|
key = kind.strip().lower().replace("-", "_")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structwire
|
|
3
|
-
Version: 0.1.0
|
|
3
|
+
Version: 0.1.0.post1
|
|
4
4
|
Summary: A small schema-driven binary payload codec for Python.
|
|
5
5
|
Author: Nidhal Abidi
|
|
6
6
|
License-Expression: MIT
|
|
@@ -49,7 +49,13 @@ Your application supplies the bytes and handles transport I/O and routing.
|
|
|
49
49
|
|
|
50
50
|
## Install
|
|
51
51
|
|
|
52
|
-
Python 3.11 or newer is required. Install from
|
|
52
|
+
Python 3.11 or newer is required. Install from PyPI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pip install "structwire>=0.1.0.post1,<0.2"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or install from the source checkout:
|
|
53
59
|
|
|
54
60
|
```bash
|
|
55
61
|
git clone https://github.com/nidops/structwire.git
|
|
@@ -57,7 +63,7 @@ cd structwire
|
|
|
57
63
|
python -m pip install .
|
|
58
64
|
```
|
|
59
65
|
|
|
60
|
-
Version `0.1.0` is an alpha API. PyYAML is installed automatically for YAML loading.
|
|
66
|
+
Version `0.1.0.post1` is an alpha API. PyYAML is installed automatically for YAML loading.
|
|
61
67
|
|
|
62
68
|
## Quick start
|
|
63
69
|
|
|
@@ -65,13 +71,13 @@ Version `0.1.0` is an alpha API. PyYAML is installed automatically for YAML load
|
|
|
65
71
|
from structwire import Field, Layout
|
|
66
72
|
|
|
67
73
|
codec = Layout(
|
|
68
|
-
Field("
|
|
69
|
-
Field("
|
|
74
|
+
Field("level", "i16le", scale=0.1),
|
|
75
|
+
Field("quality", "u8"),
|
|
70
76
|
).compile()
|
|
71
77
|
|
|
72
|
-
payload = codec.encode({"
|
|
78
|
+
payload = codec.encode({"level": 23.5, "quality": 1})
|
|
73
79
|
assert payload == bytes.fromhex("eb 00 01")
|
|
74
|
-
assert codec.decode(payload) == {"
|
|
80
|
+
assert codec.decode(payload) == {"level": 23.5, "quality": 1}
|
|
75
81
|
```
|
|
76
82
|
|
|
77
83
|
Compile a layout once and reuse its codec.
|
|
@@ -83,27 +89,27 @@ Save this as `codecs.yaml`:
|
|
|
83
89
|
```yaml
|
|
84
90
|
version: 1
|
|
85
91
|
codecs:
|
|
86
|
-
|
|
92
|
+
frame_status:
|
|
87
93
|
size: 4
|
|
88
94
|
fields:
|
|
89
|
-
- name:
|
|
95
|
+
- name: phase
|
|
90
96
|
type: enum
|
|
91
|
-
values: {0:
|
|
92
|
-
- name:
|
|
97
|
+
values: {0: idle, 1: running, 2: paused}
|
|
98
|
+
- name: value
|
|
93
99
|
type: uint16_le
|
|
94
100
|
offset: 2
|
|
95
|
-
scale: 0.
|
|
96
|
-
unit:
|
|
101
|
+
scale: 0.01
|
|
102
|
+
unit: arbitrary
|
|
97
103
|
```
|
|
98
104
|
|
|
99
105
|
```python
|
|
100
106
|
from structwire import catalog_from_yaml
|
|
101
107
|
|
|
102
108
|
catalog = catalog_from_yaml("codecs.yaml")
|
|
103
|
-
payload = bytes.fromhex("01 00
|
|
104
|
-
values = catalog.decode("
|
|
105
|
-
assert values == {"
|
|
106
|
-
assert catalog.encode("
|
|
109
|
+
payload = bytes.fromhex("01 00 64 00")
|
|
110
|
+
values = catalog.decode("frame_status", payload)
|
|
111
|
+
assert values == {"phase": "running", "value": 1.0}
|
|
112
|
+
assert catalog.encode("frame_status", values) == payload
|
|
107
113
|
```
|
|
108
114
|
|
|
109
115
|
Offsets and sizes are in bytes. Here, byte 1 is unused and encodes as zero.
|
|
@@ -140,6 +146,13 @@ limits; applications must bound untrusted schema and payload sizes. See
|
|
|
140
146
|
|
|
141
147
|
## Documentation and examples
|
|
142
148
|
|
|
149
|
+
All examples use fictional values and are independent of specific products.
|
|
150
|
+
|
|
151
|
+
- [BCD records](https://github.com/nidops/structwire/blob/master/examples/bcd_record.py)
|
|
152
|
+
- [Packed bit fields](https://github.com/nidops/structwire/blob/master/examples/packed_fields.py)
|
|
153
|
+
- [YAML codec catalog](https://github.com/nidops/structwire/blob/master/examples/codecs.yaml)
|
|
154
|
+
- [BLE-style payload](https://github.com/nidops/structwire/blob/master/examples/ble_payload.py)
|
|
155
|
+
- [Generic message routing](https://github.com/nidops/structwire/blob/master/examples/transport_bindings.yaml)
|
|
143
156
|
- [Python API](https://github.com/nidops/structwire/blob/master/docs/API.md)
|
|
144
157
|
- [Schema reference](https://github.com/nidops/structwire/blob/master/docs/SCHEMA.md)
|
|
145
158
|
- [Examples](https://github.com/nidops/structwire/tree/master/examples)
|
|
@@ -6,17 +6,16 @@ RELEASING.md
|
|
|
6
6
|
SECURITY.md
|
|
7
7
|
pyproject.toml
|
|
8
8
|
docs/API.md
|
|
9
|
-
docs/AUTOSPAN_ADOPTION.md
|
|
10
9
|
docs/COMPATIBILITY.md
|
|
11
10
|
docs/DESIGN.md
|
|
12
11
|
docs/INTEGRATION_MODEL.md
|
|
13
12
|
docs/SCHEMA.md
|
|
14
|
-
examples/
|
|
13
|
+
examples/bcd_record.py
|
|
15
14
|
examples/ble_payload.py
|
|
16
|
-
examples/can_like_signal.py
|
|
17
15
|
examples/catalog_demo.py
|
|
18
16
|
examples/codecs.yaml
|
|
19
|
-
examples/
|
|
17
|
+
examples/packed_fields.py
|
|
18
|
+
examples/transport_bindings.yaml
|
|
20
19
|
examples/uart_bindings.yaml
|
|
21
20
|
scripts/check_dist.py
|
|
22
21
|
scripts/check_readme.py
|
|
@@ -42,21 +42,19 @@ def test_nonfinite_integer_values_raise_encode_error(value: float) -> None:
|
|
|
42
42
|
Layout(Field("count", "u8")).encode({"count": value})
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def
|
|
45
|
+
def test_sequential_bcd_and_integers_round_trip() -> None:
|
|
46
46
|
codec = Layout(
|
|
47
|
-
Field("
|
|
48
|
-
Field("
|
|
49
|
-
Field("
|
|
50
|
-
Field("
|
|
51
|
-
Field("compile_minute", "bcd"),
|
|
47
|
+
Field("batch", "bcd", length=2),
|
|
48
|
+
Field("quantity", "uint8"),
|
|
49
|
+
Field("group", "bcd"),
|
|
50
|
+
Field("sequence", "bcd"),
|
|
52
51
|
).compile()
|
|
53
|
-
payload = bytes.fromhex("
|
|
52
|
+
payload = bytes.fromhex("12 34 07 25 05")
|
|
54
53
|
assert codec.decode(payload) == {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"compile_minute": 36,
|
|
54
|
+
"batch": 1234,
|
|
55
|
+
"quantity": 7,
|
|
56
|
+
"group": 25,
|
|
57
|
+
"sequence": 5,
|
|
60
58
|
}
|
|
61
59
|
assert codec.encode(codec.decode(payload)) == payload
|
|
62
60
|
|
|
@@ -96,9 +94,9 @@ def test_raw_bytes_exact_width() -> None:
|
|
|
96
94
|
|
|
97
95
|
def test_mac_roundtrip() -> None:
|
|
98
96
|
codec = Layout(Field("address", "mac_address")).compile()
|
|
99
|
-
payload = bytes.fromhex("
|
|
100
|
-
assert codec.decode(payload)["address"] == "
|
|
101
|
-
assert codec.encode({"address": "
|
|
97
|
+
payload = bytes.fromhex("02 00 00 00 00 01")
|
|
98
|
+
assert codec.decode(payload)["address"] == "02:00:00:00:00:01"
|
|
99
|
+
assert codec.encode({"address": "02:00:00:00:00:01"}) == payload
|
|
102
100
|
|
|
103
101
|
|
|
104
102
|
@pytest.mark.parametrize("kind,raw", [("u64be", 2**53 + 1), ("i64be", -(2**53 + 1))])
|
|
@@ -368,9 +366,9 @@ def test_raw_integer_is_not_treated_as_zero_bytes() -> None:
|
|
|
368
366
|
|
|
369
367
|
def test_little_endian_mac_roundtrip() -> None:
|
|
370
368
|
codec = Layout(Field("address", "mac", byteorder="little")).compile()
|
|
371
|
-
payload = bytes.fromhex("
|
|
372
|
-
assert codec.decode(payload) == {"address": "
|
|
373
|
-
assert codec.encode({"address": "
|
|
369
|
+
payload = bytes.fromhex("01 00 00 00 00 02")
|
|
370
|
+
assert codec.decode(payload) == {"address": "02:00:00:00:00:01"}
|
|
371
|
+
assert codec.encode({"address": "02:00:00:00:00:01"}) == payload
|
|
374
372
|
|
|
375
373
|
|
|
376
374
|
@pytest.mark.parametrize("payload", [1, True, "x", [1], None])
|
|
@@ -119,5 +119,5 @@ def test_example_bindings_reference_existing_codecs() -> None:
|
|
|
119
119
|
for nested in value:
|
|
120
120
|
validate_bindings(nested)
|
|
121
121
|
|
|
122
|
-
for name in ("
|
|
122
|
+
for name in ("transport_bindings.yaml", "uart_bindings.yaml"):
|
|
123
123
|
validate_bindings(_yaml_load(ROOT / "examples" / name))
|
|
@@ -15,18 +15,18 @@ from structwire import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def test_mapping_loader_accepts_schema_aliases() -> None:
|
|
19
19
|
layout = layout_from_mapping(
|
|
20
20
|
{
|
|
21
21
|
"name": "status",
|
|
22
22
|
"fields": [
|
|
23
|
-
{"name": "
|
|
24
|
-
{"name": "
|
|
23
|
+
{"name": "upper_nibble", "type": "uint8", "bitmask": "0xF0", "bitshift": 4},
|
|
24
|
+
{"name": "lower_nibble", "type": "uint8", "offset": 0, "bitmask": "0x0F"},
|
|
25
25
|
],
|
|
26
26
|
}
|
|
27
27
|
)
|
|
28
28
|
codec = layout.compile()
|
|
29
|
-
assert codec.decode(b"\x53") == {"
|
|
29
|
+
assert codec.decode(b"\x53") == {"upper_nibble": 5, "lower_nibble": 3}
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def test_mapping_loader_parses_enum_hex_keys() -> None:
|
|
@@ -45,13 +45,13 @@ def test_describe_reports_resolved_offsets() -> None:
|
|
|
45
45
|
assert description["fields"][1]["offset"] == 1
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def
|
|
49
|
-
assert format_mac_address(bytes.fromhex("
|
|
48
|
+
def test_formatting_helpers_render_readable_values() -> None:
|
|
49
|
+
assert format_mac_address(bytes.fromhex("02 00 00 00 00 01")) == "02:00:00:00:00:01"
|
|
50
50
|
assert (
|
|
51
|
-
format_mac_address(bytes.fromhex("
|
|
52
|
-
== "
|
|
51
|
+
format_mac_address(bytes.fromhex("01 00 00 00 00 02"), byteorder="little")
|
|
52
|
+
== "02:00:00:00:00:01"
|
|
53
53
|
)
|
|
54
|
-
assert format_value(b"\
|
|
54
|
+
assert format_value(b"\x12\x34\x56") == "12 34 56"
|
|
55
55
|
assert format_value(b"") == "<empty>"
|
|
56
56
|
assert format_record({"raw": b"\x01", "nested": {"x": b"\x02"}}) == {
|
|
57
57
|
"raw": "01",
|
structwire-0.1.0/RELEASING.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Releasing StructWire
|
|
2
|
-
|
|
3
|
-
Package version lives in `src/structwire/__init__.py`; setuptools derives metadata
|
|
4
|
-
from it. Keep this as the single version source.
|
|
5
|
-
|
|
6
|
-
## Setup
|
|
7
|
-
|
|
8
|
-
Configure [trusted publishing](https://docs.pypi.org/trusted-publishers/)
|
|
9
|
-
separately on TestPyPI and PyPI for project `structwire`, repository
|
|
10
|
-
`nidops/structwire`, and workflow `publish.yml`. Use GitHub environments `testpypi`
|
|
11
|
-
and `pypi` respectively, restricted to release tags. For a new project, register
|
|
12
|
-
a pending publisher; this does not reserve the package name. Confirm account
|
|
13
|
-
ownership and public documentation links.
|
|
14
|
-
|
|
15
|
-
## Release
|
|
16
|
-
|
|
17
|
-
1. Update the package version and documentation.
|
|
18
|
-
2. Run the [development checks](CONTRIBUTING.md#development) and
|
|
19
|
-
[distribution checks](CONTRIBUTING.md#distribution-checks).
|
|
20
|
-
3. From a clean checkout, tag the reviewed `master` commit as `vX.Y.Z` after all
|
|
21
|
-
supported CI jobs pass. Verify the version with
|
|
22
|
-
`python scripts/check_dist.py dist --tag vX.Y.Z`, substituting the actual version.
|
|
23
|
-
4. Push the tag, then manually run **Publish** in GitHub Actions with that tag selected.
|
|
24
|
-
|
|
25
|
-
The workflow reruns CI, uploads verified artifacts to TestPyPI, checks the downloaded
|
|
26
|
-
wheel's hash and installation, then publishes the same artifacts to PyPI and verifies
|
|
27
|
-
again. Pushes alone do not publish. Check the package page after completion.
|
|
28
|
-
|
|
29
|
-
If publication fails, rerun failed jobs in the same run while its `release-dist`
|
|
30
|
-
artifact is available. Never overwrite tags or uploaded files, or hide mismatches
|
|
31
|
-
with `skip-existing`. Changed release contents require a new version.
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Optional AutoSpan adoption
|
|
2
|
-
|
|
3
|
-
An AutoSpan project plugin can use StructWire to encode and decode payloads.
|
|
4
|
-
StructWire has no AutoSpan dependency, and using it does not require AutoSpan core
|
|
5
|
-
to depend on it.
|
|
6
|
-
|
|
7
|
-
Suggested responsibilities for this optional integration:
|
|
8
|
-
|
|
9
|
-
| Component | Responsibility |
|
|
10
|
-
|---|---|
|
|
11
|
-
| AutoSpan core | Test planning, execution and evidence |
|
|
12
|
-
| StructWire | Payload layouts, encoding, decoding and value formatting |
|
|
13
|
-
| Driver or adapter | Transport, protocol and device operations |
|
|
14
|
-
| Project plugin | Device configuration, routing and payload definitions |
|
|
15
|
-
|
|
16
|
-
## Project payload catalog
|
|
17
|
-
|
|
18
|
-
A project-specific diagnostic or signal catalog should own the mapping from its identifier to a
|
|
19
|
-
StructWire `Codec`:
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
from structwire import Field, Layout
|
|
23
|
-
|
|
24
|
-
SOFTWARE_VERSION = Layout(
|
|
25
|
-
Field("major", "bcd"),
|
|
26
|
-
Field("minor", "bcd"),
|
|
27
|
-
Field("patch", "bcd"),
|
|
28
|
-
).compile()
|
|
29
|
-
|
|
30
|
-
PAYLOADS = {
|
|
31
|
-
0x1001: SOFTWARE_VERSION,
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The protocol layer remains responsible for extracting or adding service identifiers and headers.
|
|
36
|
-
StructWire receives only the payload region whose binary layout it owns.
|
|
37
|
-
|
|
38
|
-
## Integration rule
|
|
39
|
-
|
|
40
|
-
Declare the StructWire dependency in the plugin that uses it. For named catalogs
|
|
41
|
-
and inline layouts, see the [integration examples](INTEGRATION_MODEL.md).
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# Integration examples
|
|
2
|
-
|
|
3
|
-
The application selects a codec and passes it payload bytes. It handles protocol
|
|
4
|
-
identifiers, headers, framing, checksums and device I/O.
|
|
5
|
-
|
|
6
|
-
The routing YAML below is illustrative application configuration. StructWire does
|
|
7
|
-
not load these outer mappings; application code must extract the codec name or
|
|
8
|
-
nested layout.
|
|
9
|
-
|
|
10
|
-
## Named codec reference
|
|
11
|
-
|
|
12
|
-
Use a named catalog when several messages or tests share a layout:
|
|
13
|
-
|
|
14
|
-
```yaml
|
|
15
|
-
can:
|
|
16
|
-
frames:
|
|
17
|
-
0x321:
|
|
18
|
-
codec: node_status
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
The application reads the frame identifier, looks up `node_status` in its
|
|
22
|
-
configuration, then calls `catalog.decode("node_status", payload)`.
|
|
23
|
-
See [catalog_demo.py](../examples/catalog_demo.py) and
|
|
24
|
-
[codecs.yaml](../examples/codecs.yaml) for a working catalog example.
|
|
25
|
-
|
|
26
|
-
## Inline layout
|
|
27
|
-
|
|
28
|
-
An application can keep a layout alongside a single routing entry:
|
|
29
|
-
|
|
30
|
-
```yaml
|
|
31
|
-
diagnostics:
|
|
32
|
-
dids:
|
|
33
|
-
0xF186:
|
|
34
|
-
layout:
|
|
35
|
-
size: 1
|
|
36
|
-
fields:
|
|
37
|
-
- name: session
|
|
38
|
-
type: enum
|
|
39
|
-
values: {0x01: default, 0x02: programming, 0x03: extended}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
The diagnostic application passes the nested `layout` mapping to
|
|
43
|
-
`layout_from_mapping(...).compile()`. The diagnostic identifier (`0xF186`) stays
|
|
44
|
-
in the application.
|
|
45
|
-
|
|
46
|
-
## UART example
|
|
47
|
-
|
|
48
|
-
A small device may use a packet envelope such as:
|
|
49
|
-
|
|
50
|
-
```text
|
|
51
|
-
A5 10 | <6 payload bytes> | CRC16
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
The application validates the framing and CRC, then extracts the payload.
|
|
55
|
-
Its configuration could map the message type to a codec:
|
|
56
|
-
|
|
57
|
-
```yaml
|
|
58
|
-
uart:
|
|
59
|
-
messages:
|
|
60
|
-
status:
|
|
61
|
-
match: {sync: 0xA5, type: 0x10}
|
|
62
|
-
payload: {offset: 2, length: 6, codec: sensor_status}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The application passes exactly six bytes to `catalog.decode("sensor_status", payload)`.
|
|
66
|
-
The illustrative [automotive](../examples/automotive_bindings.yaml) and
|
|
67
|
-
[UART](../examples/uart_bindings.yaml) configuration files reference codecs from
|
|
68
|
-
the example catalog. For AutoSpan projects, see [optional adoption](AUTOSPAN_ADOPTION.md).
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Illustrative application configuration, not a StructWire schema.
|
|
2
|
-
# Application code maps these protocol identifiers to codecs from codecs.yaml.
|
|
3
|
-
|
|
4
|
-
diagnostics:
|
|
5
|
-
dids:
|
|
6
|
-
0xF186:
|
|
7
|
-
codec: diagnostic_session
|
|
8
|
-
|
|
9
|
-
can:
|
|
10
|
-
frames:
|
|
11
|
-
0x321:
|
|
12
|
-
codec: node_status
|
|
13
|
-
|
|
14
|
-
# Application-defined range routing.
|
|
15
|
-
ranges:
|
|
16
|
-
- base_id: 0x300
|
|
17
|
-
count: 16
|
|
18
|
-
codec: node_status
|
|
19
|
-
index_name: node_id
|
|
20
|
-
|
|
21
|
-
# Application code passes only the nested layout to layout_from_mapping().
|
|
22
|
-
diagnostics_inline_example:
|
|
23
|
-
dids:
|
|
24
|
-
0xF187:
|
|
25
|
-
layout:
|
|
26
|
-
size: 4
|
|
27
|
-
fields:
|
|
28
|
-
- name: state
|
|
29
|
-
type: enum
|
|
30
|
-
values: {0x00: inactive, 0x01: active}
|
|
31
|
-
- name: counter
|
|
32
|
-
type: uint8
|
|
33
|
-
- name: voltage
|
|
34
|
-
type: uint16_le
|
|
35
|
-
scale: 0.001
|
|
36
|
-
unit: V
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from structwire import Field, Layout
|
|
2
|
-
|
|
3
|
-
advertisement = Layout(
|
|
4
|
-
Field("version", "u8"),
|
|
5
|
-
Field("sensor_id", "u16le"),
|
|
6
|
-
Field("temperature_c", "i16le", scale=0.01),
|
|
7
|
-
Field("battery_pct", "u8"),
|
|
8
|
-
name="sensor-advertisement",
|
|
9
|
-
).compile()
|
|
10
|
-
|
|
11
|
-
raw = advertisement.encode(
|
|
12
|
-
{"version": 1, "sensor_id": 42, "temperature_c": 23.75, "battery_pct": 91}
|
|
13
|
-
)
|
|
14
|
-
assert advertisement.decode(raw)["sensor_id"] == 42
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from structwire import Field, Layout
|
|
2
|
-
|
|
3
|
-
status = Layout(
|
|
4
|
-
Field("error_state", "u8", mask=0xF0, shift=4),
|
|
5
|
-
Field("alive_state", "u8", offset=0, mask=0x0F),
|
|
6
|
-
Field("voltage", "u16le", scale=0.01),
|
|
7
|
-
name="status-frame",
|
|
8
|
-
).compile()
|
|
9
|
-
|
|
10
|
-
payload = status.encode({"error_state": 2, "alive_state": 7, "voltage": 13.42})
|
|
11
|
-
print(payload.hex(" ").upper())
|
|
12
|
-
print(status.decode(payload))
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from structwire import catalog_from_yaml
|
|
4
|
-
|
|
5
|
-
catalog = catalog_from_yaml(Path(__file__).with_name("codecs.yaml"))
|
|
6
|
-
|
|
7
|
-
payload = bytes.fromhex("01 03 34 30")
|
|
8
|
-
print(catalog.decode("node_status", payload))
|
|
9
|
-
|
|
10
|
-
encoded = catalog.encode(
|
|
11
|
-
"sensor_status",
|
|
12
|
-
{
|
|
13
|
-
"sensor_id": 7,
|
|
14
|
-
"state": "measuring",
|
|
15
|
-
"temperature": 23.45,
|
|
16
|
-
"battery_voltage": 3.721,
|
|
17
|
-
},
|
|
18
|
-
)
|
|
19
|
-
print(encoded.hex(" ").upper())
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
version: 1
|
|
2
|
-
|
|
3
|
-
codecs:
|
|
4
|
-
diagnostic_session:
|
|
5
|
-
description: One-byte diagnostic session state.
|
|
6
|
-
size: 1
|
|
7
|
-
fields:
|
|
8
|
-
- name: session
|
|
9
|
-
type: enum
|
|
10
|
-
values:
|
|
11
|
-
0x01: default
|
|
12
|
-
0x02: programming
|
|
13
|
-
0x03: extended
|
|
14
|
-
|
|
15
|
-
node_status:
|
|
16
|
-
description: Synthetic automotive cyclic status payload.
|
|
17
|
-
size: 4
|
|
18
|
-
fields:
|
|
19
|
-
- name: mode
|
|
20
|
-
type: enum
|
|
21
|
-
values:
|
|
22
|
-
0x00: asleep
|
|
23
|
-
0x01: awake
|
|
24
|
-
0x02: fault
|
|
25
|
-
- name: communication_ok
|
|
26
|
-
type: bool
|
|
27
|
-
offset: 1
|
|
28
|
-
bitmask: 0x01
|
|
29
|
-
- name: sensor_ok
|
|
30
|
-
type: bool
|
|
31
|
-
offset: 1
|
|
32
|
-
bitmask: 0x02
|
|
33
|
-
bitshift: 1
|
|
34
|
-
- name: supply_voltage
|
|
35
|
-
type: uint16_le
|
|
36
|
-
offset: 2
|
|
37
|
-
scale: 0.001
|
|
38
|
-
unit: V
|
|
39
|
-
|
|
40
|
-
sensor_status:
|
|
41
|
-
description: Small device payload suitable for UART, BLE, or another transport.
|
|
42
|
-
size: 6
|
|
43
|
-
fields:
|
|
44
|
-
- name: sensor_id
|
|
45
|
-
type: uint8
|
|
46
|
-
- name: state
|
|
47
|
-
type: enum
|
|
48
|
-
values:
|
|
49
|
-
0x00: idle
|
|
50
|
-
0x01: measuring
|
|
51
|
-
0x02: error
|
|
52
|
-
- name: temperature
|
|
53
|
-
type: int16_le
|
|
54
|
-
scale: 0.01
|
|
55
|
-
unit: degC
|
|
56
|
-
- name: battery_voltage
|
|
57
|
-
type: uint16_le
|
|
58
|
-
scale: 0.001
|
|
59
|
-
unit: V
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from structwire import Field, Layout
|
|
2
|
-
|
|
3
|
-
software_version = Layout(
|
|
4
|
-
Field("major", "bcd"),
|
|
5
|
-
Field("minor", "bcd"),
|
|
6
|
-
Field("patch", "bcd"),
|
|
7
|
-
Field("build", "u16be"),
|
|
8
|
-
name="software-version",
|
|
9
|
-
).compile()
|
|
10
|
-
|
|
11
|
-
raw = bytes.fromhex("26 10 03 01 F4")
|
|
12
|
-
assert software_version.decode(raw) == {"major": 26, "minor": 10, "patch": 3, "build": 500}
|
|
13
|
-
assert software_version.encode(software_version.decode(raw)) == raw
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Illustrative application configuration; the application handles framing and routing.
|
|
2
|
-
# Example wire packet: [sync=0xA5][type=0x10][6-byte payload][crc16]
|
|
3
|
-
|
|
4
|
-
uart:
|
|
5
|
-
messages:
|
|
6
|
-
status:
|
|
7
|
-
match:
|
|
8
|
-
sync: 0xA5
|
|
9
|
-
type: 0x10
|
|
10
|
-
payload:
|
|
11
|
-
offset: 2
|
|
12
|
-
length: 6
|
|
13
|
-
codec: sensor_status
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|