deancochran-ftms 0.1.0a1__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.
- deancochran_ftms-0.1.0a1/.gitignore +8 -0
- deancochran_ftms-0.1.0a1/CHANGELOG.md +24 -0
- deancochran_ftms-0.1.0a1/LICENSE +21 -0
- deancochran_ftms-0.1.0a1/PKG-INFO +217 -0
- deancochran_ftms-0.1.0a1/README.md +198 -0
- deancochran_ftms-0.1.0a1/RELEASING.md +60 -0
- deancochran_ftms-0.1.0a1/pyproject.toml +83 -0
- deancochran_ftms-0.1.0a1/scripts/run_features_conformance.py +708 -0
- deancochran_ftms-0.1.0a1/scripts/run_measurement_matrix.py +152 -0
- deancochran_ftms-0.1.0a1/scripts/run_measurement_status_conformance.py +277 -0
- deancochran_ftms-0.1.0a1/scripts/verify.py +38 -0
- deancochran_ftms-0.1.0a1/scripts/verify_package.py +220 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/__init__.py +90 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/_binary.py +92 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/_errors.py +13 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/control.py +195 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/features.py +158 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/measurements.py +429 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/py.typed +0 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/ranges.py +175 -0
- deancochran_ftms-0.1.0a1/src/deancochran_ftms/statuses.py +462 -0
- deancochran_ftms-0.1.0a1/tests/test_binary.py +44 -0
- deancochran_ftms-0.1.0a1/tests/test_conformance.py +107 -0
- deancochran_ftms-0.1.0a1/tests/test_features.py +64 -0
- deancochran_ftms-0.1.0a1/tests/test_measurement_validation.py +53 -0
- deancochran_ftms-0.1.0a1/tests/test_measurements_statuses.py +172 -0
- deancochran_ftms-0.1.0a1/tests/test_ranges_control.py +182 -0
- deancochran_ftms-0.1.0a1/tests/test_report_integrity.py +75 -0
- deancochran_ftms-0.1.0a1/tests/test_status_validation.py +190 -0
- deancochran_ftms-0.1.0a1/uv.lock +570 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Python distribution versions are independent of FTMS specification, shared
|
|
4
|
+
conformance corpus, and other language-package versions.
|
|
5
|
+
|
|
6
|
+
## 0.1.0a1
|
|
7
|
+
|
|
8
|
+
Initial partial alpha; APIs may change before a stable release.
|
|
9
|
+
|
|
10
|
+
- Pure synchronous Python with no runtime dependencies, Python >=3.11,
|
|
11
|
+
immutable raw models, and inline typing.
|
|
12
|
+
- Bidirectional raw Features, all five supported ranges, structural range
|
|
13
|
+
inspection, and normalized Feature/range views.
|
|
14
|
+
- Bidirectional raw codecs for all 21 Control Point requests and responses.
|
|
15
|
+
- Bidirectional measurements for treadmill, cross trainer, step climber,
|
|
16
|
+
stair climber, rower, and indoor bike, with normalized metric views.
|
|
17
|
+
- Bidirectional Training Status and Machine Status, normalized views, and
|
|
18
|
+
preservation of malformed/unknown decoding evidence.
|
|
19
|
+
- Explicit, independent range, command, and measurement wire-format options.
|
|
20
|
+
- Canonical shared fixture, structural matrix, and isolated package checks.
|
|
21
|
+
|
|
22
|
+
Not included: static capability evaluation, a public human-unit control encoder,
|
|
23
|
+
Bluetooth transport, device lifecycle, retries, execution permission, or safety
|
|
24
|
+
policy. Host tests are not device interoperability or Bluetooth qualification.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dean Cochran
|
|
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.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: deancochran-ftms
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Pure Python FTMS feature, range, control, measurement, and status codecs (alpha)
|
|
5
|
+
Project-URL: Source, https://github.com/deancochran/ftms
|
|
6
|
+
Project-URL: Issues, https://github.com/deancochran/ftms/issues
|
|
7
|
+
Project-URL: Documentation, https://github.com/deancochran/ftms/blob/python-v0.1.0a1/packages/python/README.md
|
|
8
|
+
Author: Dean Cochran
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# deancochran-ftms
|
|
21
|
+
|
|
22
|
+
`deancochran-ftms` is a pure, synchronous Python protocol package. This
|
|
23
|
+
**0.1.0a1 is a partial alpha release with an evolving API**: it implements FTMS Features, all five supported ranges (raw and normalized), structural range inspection, raw Control Point requests/responses, all six raw measurement families, and bidirectional Machine/Training Status. It has no BLE, lifecycle, logging, or capability APIs. It is not a complete port of every API in the TypeScript/C packages.
|
|
24
|
+
|
|
25
|
+
## Install and compatibility
|
|
26
|
+
|
|
27
|
+
The distribution name is `deancochran-ftms`; import `deancochran_ftms`. It has
|
|
28
|
+
no runtime dependencies and declares Python >=3.11. Python 3.11 and 3.14 are
|
|
29
|
+
tested by the package verification commands in this milestone.
|
|
30
|
+
|
|
31
|
+
Install this explicitly selected prerelease from PyPI:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
python -m pip install 'deancochran-ftms==0.1.0a1'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from deancochran_ftms import decode_features, encode_features_raw, FeaturesRaw
|
|
39
|
+
|
|
40
|
+
wire = encode_features_raw(FeaturesRaw(machine=0, target=1 << 3))
|
|
41
|
+
result = decode_features(wire)
|
|
42
|
+
assert result.ok and result.value is not None
|
|
43
|
+
assert result.value.power_target_setting_supported
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`decode_features_raw()` and `encode_features_raw()` are strict wire codecs and
|
|
47
|
+
raise `RawCodecError` for wrong byte inputs, non-8-byte payloads, or invalid raw
|
|
48
|
+
words. `decode_features()` instead returns an immutable `FeatureDecodeResult`:
|
|
49
|
+
wrong lengths produce a `FeatureDiagnostic` with `code="length"`. Feature
|
|
50
|
+
payloads are exactly eight bytes; trailing bytes are rejected, matching the
|
|
51
|
+
canonical TypeScript Feature behavior. `bytes`, `bytearray`, and contiguous
|
|
52
|
+
one-dimensional byte `memoryview` inputs are accepted and copied as immutable
|
|
53
|
+
evidence. Raw words retain all unknown/reserved bits. Integer raw words must be
|
|
54
|
+
plain `int` values from 0 through 2^32-1; `bool` is rejected.
|
|
55
|
+
|
|
56
|
+
The normalized `Features` model exposes the canonical v1 feature names in
|
|
57
|
+
snake_case. Its three convenience properties (`supports_erg`, `supports_sim`,
|
|
58
|
+
and `supports_resistance`) correspond to the v1 compatibility names.
|
|
59
|
+
|
|
60
|
+
## Ranges and inspection
|
|
61
|
+
|
|
62
|
+
`decode_supported_range_raw(data, kind, options=None)` and
|
|
63
|
+
`encode_supported_range_raw(value, options=None)` operate on `SupportedRangeRaw`.
|
|
64
|
+
Kinds are `speed`, `inclination`, `resistance`, `heartRate`, and `power`.
|
|
65
|
+
Values retain integer numerators, `scale_divisor`, and units; the normalized
|
|
66
|
+
`decode_supported_range()` divides these values into physical units. All three
|
|
67
|
+
raise `RawCodecError` for invalid arguments, lengths, or range values.
|
|
68
|
+
|
|
69
|
+
Resistance ranges default to three unsigned whole-level bytes. Select the
|
|
70
|
+
six-byte signed-tenths alternative explicitly with
|
|
71
|
+
`RangeFormatOptions(resistance_format="signed16Tenths")`; this option is invalid
|
|
72
|
+
for other range kinds. Range units do not imply resistance percentages.
|
|
73
|
+
|
|
74
|
+
`inspect_supported_range_raw()` returns the shared inspection report shape:
|
|
75
|
+
selected profile, actual/expected lengths, status, selected value, and ordered
|
|
76
|
+
structural candidates. Malformed wire lengths/values appear as candidate statuses;
|
|
77
|
+
invalid caller kinds/options still raise. Report values use the canonical numeric
|
|
78
|
+
unit identifiers (0 speed, 1 inclination, 2 resistance, 3 heart rate, 4 power).
|
|
79
|
+
The returned dictionary is caller-owned; selected and candidate values do not
|
|
80
|
+
alias. A valid alternative candidate never selects a profile automatically or
|
|
81
|
+
establishes physical units on a particular machine.
|
|
82
|
+
|
|
83
|
+
## Control Point requests and responses
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from deancochran_ftms import (
|
|
87
|
+
ControlRequestRaw,
|
|
88
|
+
decode_control_request_raw,
|
|
89
|
+
encode_control_request_raw,
|
|
90
|
+
decode_control_response_raw,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Set Target Power: opcode 0x05, raw operand in watts. This does not send anything.
|
|
94
|
+
wire = encode_control_request_raw(ControlRequestRaw(0x05, (75,)))
|
|
95
|
+
assert wire == bytes.fromhex("05 4b 00")
|
|
96
|
+
assert decode_control_request_raw(wire).operands == (75,)
|
|
97
|
+
response = decode_control_response_raw(bytes.fromhex("80 05 01"))
|
|
98
|
+
assert response.request_opcode == 5 and response.result_code == 1
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
All 21 request opcodes support raw encode/decode. Operands must be tuples of plain
|
|
102
|
+
integers in wire units, not arbitrary human-unit floats. For example simulation
|
|
103
|
+
operands are wind speed in thousandths of m/s, grade in hundredths of percent,
|
|
104
|
+
rolling coefficient in ten-thousandths, and wind coefficient in hundredths kg/m.
|
|
105
|
+
There is no public normalized control encoder in this milestone; the legacy
|
|
106
|
+
codec-v1 test adapter translates its normalized fixtures to raw operands.
|
|
107
|
+
|
|
108
|
+
`ControlFormatOptions(resistance_format="uint8Tenths")` explicitly selects the
|
|
109
|
+
alternative resistance command width; the default is `signed16Tenths`. Control
|
|
110
|
+
options are independent of range options and do not change other opcodes.
|
|
111
|
+
|
|
112
|
+
`ControlResponseRaw` preserves request/result codes plus integer diagnostic flags
|
|
113
|
+
`unknown_request`, `unknown_result`, and `unexpected_parameters`. Its `parameter`
|
|
114
|
+
is 0 for none or 1 for successful spin-down speeds (`low`/`high` in hundredths
|
|
115
|
+
km/h). `encode_control_response_raw()` accepts only canonical response forms;
|
|
116
|
+
decodable malformed/unknown evidence is not necessarily encodable. Invalid wire
|
|
117
|
+
headers/lengths and invalid encoder arguments raise `RawCodecError`.
|
|
118
|
+
|
|
119
|
+
No codec acquires control, sends bytes, retries commands, or authorizes execution.
|
|
120
|
+
|
|
121
|
+
## Development verification
|
|
122
|
+
|
|
123
|
+
From `packages/python/` in a full repository checkout (with `uv` installed):
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
uv run --locked --group dev python scripts/verify.py
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
That aggregate command runs both supported test interpreters, quality checks,
|
|
130
|
+
all codec corpus runners, the structural matrix, and isolated installation.
|
|
131
|
+
Individual checks are also available:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
uv run --locked --group dev pytest
|
|
135
|
+
uv run --locked --group dev ruff format --check .
|
|
136
|
+
uv run --locked --group dev ruff check .
|
|
137
|
+
uv run --locked --group dev mypy src tests scripts
|
|
138
|
+
uv run --locked --group dev python scripts/run_features_conformance.py
|
|
139
|
+
uv run --locked --group dev python scripts/run_measurement_status_conformance.py
|
|
140
|
+
uv run --locked --group dev python scripts/run_measurement_matrix.py
|
|
141
|
+
uv run --locked --group dev python scripts/verify_package.py
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The conformance test reads `../../shared/conformance/v1` and
|
|
145
|
+
`../../shared/conformance/values/v1` directly; it never copies fixtures.
|
|
146
|
+
It executes all codec-v1 categories and the additive values, controls,
|
|
147
|
+
inspection, compatibility, measurements and statuses contracts directly from
|
|
148
|
+
shared fixtures. Capability interpretation is not implemented or claimed.
|
|
149
|
+
`scripts/verify_package.py` builds both artifacts, rebuilds from
|
|
150
|
+
an extracted sdist outside this checkout, installs the wheel non-editably into
|
|
151
|
+
a fresh environment, and writes its distinct machine-readable artifact hashes,
|
|
152
|
+
interpreter, and per-step install/rebuild evidence to ignored
|
|
153
|
+
`build/package-verification-report.json`. Conformance identity and case evidence
|
|
154
|
+
are separately recorded in `build/verification-report.json` and
|
|
155
|
+
`build/measurement-status-verification-report.json`. The structural matrix
|
|
156
|
+
records 181,760 layouts in both directions, 46 sentinel cases, 47 RFU cases,
|
|
157
|
+
and 315 incomplete prefixes in `build/measurement-matrix-verification-report.json`.
|
|
158
|
+
These generated cases are not separate pytest test registrations. The sdist
|
|
159
|
+
intentionally contains no shared corpus dependency: installing or building the
|
|
160
|
+
package needs no checkout, but running the conformance suite does. The package
|
|
161
|
+
check asserts that rebuilding the extracted sdist produces an identical wheel;
|
|
162
|
+
it does not assert byte-for-byte reproducibility of the sdist archive itself.
|
|
163
|
+
|
|
164
|
+
This is host protocol regression evidence only, not live control execution, device
|
|
165
|
+
interoperability or Bluetooth qualification. Public availability is separate
|
|
166
|
+
from these host verification results.
|
|
167
|
+
|
|
168
|
+
## Coverage and limits
|
|
169
|
+
|
|
170
|
+
Version `0.1.0a1` is an **alpha with partial API coverage**. Features, ranges, controls,
|
|
171
|
+
all six measurement families and both status characteristics have raw codecs.
|
|
172
|
+
Normalized measurement/status projections and normalized range decoding are
|
|
173
|
+
available. Capability evaluation remains outstanding. Range, control and
|
|
174
|
+
measurement format choices are independent and explicit; no API infers them
|
|
175
|
+
from packet length, feature declarations or BLE state. Host fixtures establish
|
|
176
|
+
codec regression evidence, not live-device compatibility or qualification.
|
|
177
|
+
|
|
178
|
+
## Measurements and statuses
|
|
179
|
+
|
|
180
|
+
`MeasurementRaw` is immutable raw evidence: `flags`, 30 wire-unit `values`, and
|
|
181
|
+
`present`/`unavailable` bit masks retain field-level availability. Use
|
|
182
|
+
`decode_measurement_raw(data, kind, options=None)` and
|
|
183
|
+
`encode_measurement_raw(value, options=None)`. Kinds are 0 Treadmill, 1 Cross
|
|
184
|
+
Trainer, 2 Step Climber, 3 Stair Climber, 4 Rower, and 5 Indoor Bike. Decoders
|
|
185
|
+
retain More Data, Cross Trainer backward direction, RFU, truncation, and trailing
|
|
186
|
+
byte diagnostics. Encoders validate the wire flags, presence/unavailable masks
|
|
187
|
+
and selected values; auxiliary decode diagnostics do not themselves authorize
|
|
188
|
+
or prevent encoding, matching the shared raw contract. Compatibility formats
|
|
189
|
+
are explicit only: `MeasurementFormatOptions(resistance_format="signed16Tenths")`
|
|
190
|
+
and `treadmill_pace_format="uint8Legacy"`; they are independent selections and
|
|
191
|
+
are never inferred from packet length, features, or machine identity.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from deancochran_ftms import MeasurementRaw, decode_measurement_raw, encode_measurement_raw
|
|
195
|
+
|
|
196
|
+
raw = MeasurementRaw(5, 0, 1, 0, (1234,) + (0,) * 29) # speed: 12.34 km/h
|
|
197
|
+
assert encode_measurement_raw(raw) == bytes.fromhex("00 00 d2 04")
|
|
198
|
+
assert decode_measurement_raw(bytes.fromhex("00 00 d2 04"), 5).values[0] == 1234
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Machine Status uses `MachineStatusRaw` with raw opcode/action and optional
|
|
202
|
+
`(control_opcode, operands)` parameter tuple. Training Status uses
|
|
203
|
+
`TrainingStatusRaw`; text is UTF-8 `bytes`, so malformed UTF-8 is retained and
|
|
204
|
+
diagnosed by `invalid_utf8` rather than silently replaced. The corresponding
|
|
205
|
+
`decode_*_status_raw` APIs preserve partial/trailing/reserved evidence; encoders
|
|
206
|
+
accept only canonical values and raise `RawCodecError` on invalid widths, flags,
|
|
207
|
+
UTF-8, sentinels, or diagnostics. Raw units/scales remain FTMS wire units.
|
|
208
|
+
|
|
209
|
+
Host tests cover Python 3.11 and 3.14 plus shared literal measurement/status
|
|
210
|
+
corpora. This finite evidence is not BLE, real-device, PTS, or Bluetooth
|
|
211
|
+
qualification evidence.
|
|
212
|
+
|
|
213
|
+
`normalize_measurement(raw, options=None)` provides the immutable-raw-to-codec-v1
|
|
214
|
+
normalized metric projection (for example raw speed / 360 to m/s, cadence / 2,
|
|
215
|
+
and unavailable or incomplete fields as `None`). `normalize_machine_status()` and
|
|
216
|
+
`normalize_training_status()` provide the corresponding normalized status views;
|
|
217
|
+
raw codecs remain the source of wire evidence.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# deancochran-ftms
|
|
2
|
+
|
|
3
|
+
`deancochran-ftms` is a pure, synchronous Python protocol package. This
|
|
4
|
+
**0.1.0a1 is a partial alpha release with an evolving API**: it implements FTMS Features, all five supported ranges (raw and normalized), structural range inspection, raw Control Point requests/responses, all six raw measurement families, and bidirectional Machine/Training Status. It has no BLE, lifecycle, logging, or capability APIs. It is not a complete port of every API in the TypeScript/C packages.
|
|
5
|
+
|
|
6
|
+
## Install and compatibility
|
|
7
|
+
|
|
8
|
+
The distribution name is `deancochran-ftms`; import `deancochran_ftms`. It has
|
|
9
|
+
no runtime dependencies and declares Python >=3.11. Python 3.11 and 3.14 are
|
|
10
|
+
tested by the package verification commands in this milestone.
|
|
11
|
+
|
|
12
|
+
Install this explicitly selected prerelease from PyPI:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
python -m pip install 'deancochran-ftms==0.1.0a1'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from deancochran_ftms import decode_features, encode_features_raw, FeaturesRaw
|
|
20
|
+
|
|
21
|
+
wire = encode_features_raw(FeaturesRaw(machine=0, target=1 << 3))
|
|
22
|
+
result = decode_features(wire)
|
|
23
|
+
assert result.ok and result.value is not None
|
|
24
|
+
assert result.value.power_target_setting_supported
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`decode_features_raw()` and `encode_features_raw()` are strict wire codecs and
|
|
28
|
+
raise `RawCodecError` for wrong byte inputs, non-8-byte payloads, or invalid raw
|
|
29
|
+
words. `decode_features()` instead returns an immutable `FeatureDecodeResult`:
|
|
30
|
+
wrong lengths produce a `FeatureDiagnostic` with `code="length"`. Feature
|
|
31
|
+
payloads are exactly eight bytes; trailing bytes are rejected, matching the
|
|
32
|
+
canonical TypeScript Feature behavior. `bytes`, `bytearray`, and contiguous
|
|
33
|
+
one-dimensional byte `memoryview` inputs are accepted and copied as immutable
|
|
34
|
+
evidence. Raw words retain all unknown/reserved bits. Integer raw words must be
|
|
35
|
+
plain `int` values from 0 through 2^32-1; `bool` is rejected.
|
|
36
|
+
|
|
37
|
+
The normalized `Features` model exposes the canonical v1 feature names in
|
|
38
|
+
snake_case. Its three convenience properties (`supports_erg`, `supports_sim`,
|
|
39
|
+
and `supports_resistance`) correspond to the v1 compatibility names.
|
|
40
|
+
|
|
41
|
+
## Ranges and inspection
|
|
42
|
+
|
|
43
|
+
`decode_supported_range_raw(data, kind, options=None)` and
|
|
44
|
+
`encode_supported_range_raw(value, options=None)` operate on `SupportedRangeRaw`.
|
|
45
|
+
Kinds are `speed`, `inclination`, `resistance`, `heartRate`, and `power`.
|
|
46
|
+
Values retain integer numerators, `scale_divisor`, and units; the normalized
|
|
47
|
+
`decode_supported_range()` divides these values into physical units. All three
|
|
48
|
+
raise `RawCodecError` for invalid arguments, lengths, or range values.
|
|
49
|
+
|
|
50
|
+
Resistance ranges default to three unsigned whole-level bytes. Select the
|
|
51
|
+
six-byte signed-tenths alternative explicitly with
|
|
52
|
+
`RangeFormatOptions(resistance_format="signed16Tenths")`; this option is invalid
|
|
53
|
+
for other range kinds. Range units do not imply resistance percentages.
|
|
54
|
+
|
|
55
|
+
`inspect_supported_range_raw()` returns the shared inspection report shape:
|
|
56
|
+
selected profile, actual/expected lengths, status, selected value, and ordered
|
|
57
|
+
structural candidates. Malformed wire lengths/values appear as candidate statuses;
|
|
58
|
+
invalid caller kinds/options still raise. Report values use the canonical numeric
|
|
59
|
+
unit identifiers (0 speed, 1 inclination, 2 resistance, 3 heart rate, 4 power).
|
|
60
|
+
The returned dictionary is caller-owned; selected and candidate values do not
|
|
61
|
+
alias. A valid alternative candidate never selects a profile automatically or
|
|
62
|
+
establishes physical units on a particular machine.
|
|
63
|
+
|
|
64
|
+
## Control Point requests and responses
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from deancochran_ftms import (
|
|
68
|
+
ControlRequestRaw,
|
|
69
|
+
decode_control_request_raw,
|
|
70
|
+
encode_control_request_raw,
|
|
71
|
+
decode_control_response_raw,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Set Target Power: opcode 0x05, raw operand in watts. This does not send anything.
|
|
75
|
+
wire = encode_control_request_raw(ControlRequestRaw(0x05, (75,)))
|
|
76
|
+
assert wire == bytes.fromhex("05 4b 00")
|
|
77
|
+
assert decode_control_request_raw(wire).operands == (75,)
|
|
78
|
+
response = decode_control_response_raw(bytes.fromhex("80 05 01"))
|
|
79
|
+
assert response.request_opcode == 5 and response.result_code == 1
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
All 21 request opcodes support raw encode/decode. Operands must be tuples of plain
|
|
83
|
+
integers in wire units, not arbitrary human-unit floats. For example simulation
|
|
84
|
+
operands are wind speed in thousandths of m/s, grade in hundredths of percent,
|
|
85
|
+
rolling coefficient in ten-thousandths, and wind coefficient in hundredths kg/m.
|
|
86
|
+
There is no public normalized control encoder in this milestone; the legacy
|
|
87
|
+
codec-v1 test adapter translates its normalized fixtures to raw operands.
|
|
88
|
+
|
|
89
|
+
`ControlFormatOptions(resistance_format="uint8Tenths")` explicitly selects the
|
|
90
|
+
alternative resistance command width; the default is `signed16Tenths`. Control
|
|
91
|
+
options are independent of range options and do not change other opcodes.
|
|
92
|
+
|
|
93
|
+
`ControlResponseRaw` preserves request/result codes plus integer diagnostic flags
|
|
94
|
+
`unknown_request`, `unknown_result`, and `unexpected_parameters`. Its `parameter`
|
|
95
|
+
is 0 for none or 1 for successful spin-down speeds (`low`/`high` in hundredths
|
|
96
|
+
km/h). `encode_control_response_raw()` accepts only canonical response forms;
|
|
97
|
+
decodable malformed/unknown evidence is not necessarily encodable. Invalid wire
|
|
98
|
+
headers/lengths and invalid encoder arguments raise `RawCodecError`.
|
|
99
|
+
|
|
100
|
+
No codec acquires control, sends bytes, retries commands, or authorizes execution.
|
|
101
|
+
|
|
102
|
+
## Development verification
|
|
103
|
+
|
|
104
|
+
From `packages/python/` in a full repository checkout (with `uv` installed):
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
uv run --locked --group dev python scripts/verify.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
That aggregate command runs both supported test interpreters, quality checks,
|
|
111
|
+
all codec corpus runners, the structural matrix, and isolated installation.
|
|
112
|
+
Individual checks are also available:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
uv run --locked --group dev pytest
|
|
116
|
+
uv run --locked --group dev ruff format --check .
|
|
117
|
+
uv run --locked --group dev ruff check .
|
|
118
|
+
uv run --locked --group dev mypy src tests scripts
|
|
119
|
+
uv run --locked --group dev python scripts/run_features_conformance.py
|
|
120
|
+
uv run --locked --group dev python scripts/run_measurement_status_conformance.py
|
|
121
|
+
uv run --locked --group dev python scripts/run_measurement_matrix.py
|
|
122
|
+
uv run --locked --group dev python scripts/verify_package.py
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The conformance test reads `../../shared/conformance/v1` and
|
|
126
|
+
`../../shared/conformance/values/v1` directly; it never copies fixtures.
|
|
127
|
+
It executes all codec-v1 categories and the additive values, controls,
|
|
128
|
+
inspection, compatibility, measurements and statuses contracts directly from
|
|
129
|
+
shared fixtures. Capability interpretation is not implemented or claimed.
|
|
130
|
+
`scripts/verify_package.py` builds both artifacts, rebuilds from
|
|
131
|
+
an extracted sdist outside this checkout, installs the wheel non-editably into
|
|
132
|
+
a fresh environment, and writes its distinct machine-readable artifact hashes,
|
|
133
|
+
interpreter, and per-step install/rebuild evidence to ignored
|
|
134
|
+
`build/package-verification-report.json`. Conformance identity and case evidence
|
|
135
|
+
are separately recorded in `build/verification-report.json` and
|
|
136
|
+
`build/measurement-status-verification-report.json`. The structural matrix
|
|
137
|
+
records 181,760 layouts in both directions, 46 sentinel cases, 47 RFU cases,
|
|
138
|
+
and 315 incomplete prefixes in `build/measurement-matrix-verification-report.json`.
|
|
139
|
+
These generated cases are not separate pytest test registrations. The sdist
|
|
140
|
+
intentionally contains no shared corpus dependency: installing or building the
|
|
141
|
+
package needs no checkout, but running the conformance suite does. The package
|
|
142
|
+
check asserts that rebuilding the extracted sdist produces an identical wheel;
|
|
143
|
+
it does not assert byte-for-byte reproducibility of the sdist archive itself.
|
|
144
|
+
|
|
145
|
+
This is host protocol regression evidence only, not live control execution, device
|
|
146
|
+
interoperability or Bluetooth qualification. Public availability is separate
|
|
147
|
+
from these host verification results.
|
|
148
|
+
|
|
149
|
+
## Coverage and limits
|
|
150
|
+
|
|
151
|
+
Version `0.1.0a1` is an **alpha with partial API coverage**. Features, ranges, controls,
|
|
152
|
+
all six measurement families and both status characteristics have raw codecs.
|
|
153
|
+
Normalized measurement/status projections and normalized range decoding are
|
|
154
|
+
available. Capability evaluation remains outstanding. Range, control and
|
|
155
|
+
measurement format choices are independent and explicit; no API infers them
|
|
156
|
+
from packet length, feature declarations or BLE state. Host fixtures establish
|
|
157
|
+
codec regression evidence, not live-device compatibility or qualification.
|
|
158
|
+
|
|
159
|
+
## Measurements and statuses
|
|
160
|
+
|
|
161
|
+
`MeasurementRaw` is immutable raw evidence: `flags`, 30 wire-unit `values`, and
|
|
162
|
+
`present`/`unavailable` bit masks retain field-level availability. Use
|
|
163
|
+
`decode_measurement_raw(data, kind, options=None)` and
|
|
164
|
+
`encode_measurement_raw(value, options=None)`. Kinds are 0 Treadmill, 1 Cross
|
|
165
|
+
Trainer, 2 Step Climber, 3 Stair Climber, 4 Rower, and 5 Indoor Bike. Decoders
|
|
166
|
+
retain More Data, Cross Trainer backward direction, RFU, truncation, and trailing
|
|
167
|
+
byte diagnostics. Encoders validate the wire flags, presence/unavailable masks
|
|
168
|
+
and selected values; auxiliary decode diagnostics do not themselves authorize
|
|
169
|
+
or prevent encoding, matching the shared raw contract. Compatibility formats
|
|
170
|
+
are explicit only: `MeasurementFormatOptions(resistance_format="signed16Tenths")`
|
|
171
|
+
and `treadmill_pace_format="uint8Legacy"`; they are independent selections and
|
|
172
|
+
are never inferred from packet length, features, or machine identity.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from deancochran_ftms import MeasurementRaw, decode_measurement_raw, encode_measurement_raw
|
|
176
|
+
|
|
177
|
+
raw = MeasurementRaw(5, 0, 1, 0, (1234,) + (0,) * 29) # speed: 12.34 km/h
|
|
178
|
+
assert encode_measurement_raw(raw) == bytes.fromhex("00 00 d2 04")
|
|
179
|
+
assert decode_measurement_raw(bytes.fromhex("00 00 d2 04"), 5).values[0] == 1234
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Machine Status uses `MachineStatusRaw` with raw opcode/action and optional
|
|
183
|
+
`(control_opcode, operands)` parameter tuple. Training Status uses
|
|
184
|
+
`TrainingStatusRaw`; text is UTF-8 `bytes`, so malformed UTF-8 is retained and
|
|
185
|
+
diagnosed by `invalid_utf8` rather than silently replaced. The corresponding
|
|
186
|
+
`decode_*_status_raw` APIs preserve partial/trailing/reserved evidence; encoders
|
|
187
|
+
accept only canonical values and raise `RawCodecError` on invalid widths, flags,
|
|
188
|
+
UTF-8, sentinels, or diagnostics. Raw units/scales remain FTMS wire units.
|
|
189
|
+
|
|
190
|
+
Host tests cover Python 3.11 and 3.14 plus shared literal measurement/status
|
|
191
|
+
corpora. This finite evidence is not BLE, real-device, PTS, or Bluetooth
|
|
192
|
+
qualification evidence.
|
|
193
|
+
|
|
194
|
+
`normalize_measurement(raw, options=None)` provides the immutable-raw-to-codec-v1
|
|
195
|
+
normalized metric projection (for example raw speed / 360 to m/s, cadence / 2,
|
|
196
|
+
and unavailable or incomplete fields as `None`). `normalize_machine_status()` and
|
|
197
|
+
`normalize_training_status()` provide the corresponding normalized status views;
|
|
198
|
+
raw codecs remain the source of wire evidence.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Python alpha release
|
|
2
|
+
|
|
3
|
+
Publishing requires explicit authorization. The distribution is `deancochran-ftms`;
|
|
4
|
+
`pyproject.toml` defines its independently versioned Python release. The current
|
|
5
|
+
`0.1.0a1` is a partial alpha, not full cross-language API parity or device evidence.
|
|
6
|
+
|
|
7
|
+
## One-time Trusted Publishing setup
|
|
8
|
+
|
|
9
|
+
At <https://pypi.org/manage/account/publishing/>, add a **pending publisher**
|
|
10
|
+
(the first successful upload creates the project):
|
|
11
|
+
|
|
12
|
+
| Field | Value |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| PyPI project name | `deancochran-ftms` |
|
|
15
|
+
| GitHub owner | `deancochran` |
|
|
16
|
+
| Repository | `ftms` |
|
|
17
|
+
| Workflow filename | `release-python.yml` |
|
|
18
|
+
| Environment | `pypi` |
|
|
19
|
+
|
|
20
|
+
The GitHub repository's `pypi` environment must permit only `python-v*` **tags**
|
|
21
|
+
and require approval by `deancochran` before publishing. Repository administrators
|
|
22
|
+
must not bypass that approval. Tag restrictions alone are not an approval gate.
|
|
23
|
+
No long-lived PyPI token or GitHub secret is needed. Registering a pending
|
|
24
|
+
publisher does not reserve the package name or publish anything.
|
|
25
|
+
|
|
26
|
+
## Authorized release procedure
|
|
27
|
+
|
|
28
|
+
1. Verify the scope, changelog, version and documentation. Keep capability
|
|
29
|
+
interpretation and other unimplemented APIs explicitly outside this alpha.
|
|
30
|
+
2. Commit the reviewed source. From a clean checkout at that exact commit,
|
|
31
|
+
run `uv run --locked --group dev python scripts/verify.py` in `packages/python/`.
|
|
32
|
+
Inspect reports, source identity, clean state and archive contents. Then run:
|
|
33
|
+
`uv tool run --from twine==7.0.0 twine check --strict build/isolated/*.whl build/isolated/*.tar.gz`.
|
|
34
|
+
3. Push the source branch and require the Python workflow's verification job to
|
|
35
|
+
pass. A branch push or pull request cannot execute the publishing job.
|
|
36
|
+
4. Confirm the PyPI publisher setup and explicit release authorization before
|
|
37
|
+
creating and pushing the immutable `python-vVERSION` tag at that reviewed
|
|
38
|
+
commit. Python tags are independent of `v*` (npm) and `c-v*` tags.
|
|
39
|
+
The initial alpha can be tagged on the reviewed Python branch; this does not
|
|
40
|
+
merge that branch to `main` or authorize merging other ports.
|
|
41
|
+
5. The tag workflow verifies the version/tag match, both test interpreters,
|
|
42
|
+
all scoped codec corpora, the structural matrix, and the isolated rebuilt-wheel
|
|
43
|
+
consumer. It checks metadata and saves distributions plus identity/hash reports.
|
|
44
|
+
6. Approve the publishing deployment only after reviewing that exact tag/commit
|
|
45
|
+
and its verification job. A separate job, restricted to the `pypi` environment, checks the distribution
|
|
46
|
+
hashes and publishes those exact artifacts using OIDC and attestations. Only
|
|
47
|
+
this job receives `id-token: write`; it does not check out or build source.
|
|
48
|
+
7. Confirm successful workflow completion, query the exact PyPI version, compare
|
|
49
|
+
both public SHA-256 digests to the workflow artifacts, and perform a fresh
|
|
50
|
+
non-editable registry installation. Only then describe the version as published.
|
|
51
|
+
|
|
52
|
+
The workflow deliberately fails on duplicate uploads; do not overwrite or move a
|
|
53
|
+
release tag. On a partial upload or network failure, inspect PyPI and compare
|
|
54
|
+
existing artifact digests before deciding how to recover. Never bypass a failed
|
|
55
|
+
verification gate or claim publication from a pushed tag alone.
|
|
56
|
+
|
|
57
|
+
The source archive is installable without shared fixtures. Full repository
|
|
58
|
+
verification requires a checkout containing the canonical `shared/conformance/`
|
|
59
|
+
assets. The package verifier asserts identical wheel bytes after an extracted-sdist
|
|
60
|
+
rebuild; it does not claim byte-identical source archives after that rebuild.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "deancochran-ftms"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Pure Python FTMS feature, range, control, measurement, and status codecs (alpha)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Dean Cochran" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Source = "https://github.com/deancochran/ftms"
|
|
26
|
+
Issues = "https://github.com/deancochran/ftms/issues"
|
|
27
|
+
Documentation = "https://github.com/deancochran/ftms/blob/python-v0.1.0a1/packages/python/README.md"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["jsonschema>=4.23", "mypy>=1.13", "pytest>=8.3", "ruff>=0.8", "types-jsonschema>=4.23"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/deancochran_ftms"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.sdist]
|
|
36
|
+
only-include = [
|
|
37
|
+
".gitignore",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"README.md",
|
|
41
|
+
"RELEASING.md",
|
|
42
|
+
"pyproject.toml",
|
|
43
|
+
"uv.lock",
|
|
44
|
+
"src/deancochran_ftms/__init__.py",
|
|
45
|
+
"src/deancochran_ftms/_binary.py",
|
|
46
|
+
"src/deancochran_ftms/_errors.py",
|
|
47
|
+
"src/deancochran_ftms/control.py",
|
|
48
|
+
"src/deancochran_ftms/features.py",
|
|
49
|
+
"src/deancochran_ftms/measurements.py",
|
|
50
|
+
"src/deancochran_ftms/py.typed",
|
|
51
|
+
"src/deancochran_ftms/ranges.py",
|
|
52
|
+
"src/deancochran_ftms/statuses.py",
|
|
53
|
+
"scripts/run_features_conformance.py",
|
|
54
|
+
"scripts/run_measurement_matrix.py",
|
|
55
|
+
"scripts/run_measurement_status_conformance.py",
|
|
56
|
+
"scripts/verify.py",
|
|
57
|
+
"scripts/verify_package.py",
|
|
58
|
+
"tests/test_binary.py",
|
|
59
|
+
"tests/test_conformance.py",
|
|
60
|
+
"tests/test_features.py",
|
|
61
|
+
"tests/test_measurement_validation.py",
|
|
62
|
+
"tests/test_measurements_statuses.py",
|
|
63
|
+
"tests/test_ranges_control.py",
|
|
64
|
+
"tests/test_report_integrity.py",
|
|
65
|
+
"tests/test_status_validation.py",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
addopts = ["--import-mode=importlib"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
target-version = "py311"
|
|
74
|
+
line-length = 100
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
select = ["E", "F", "I", "UP"]
|
|
78
|
+
ignore = ["E501", "E702"]
|
|
79
|
+
|
|
80
|
+
[tool.mypy]
|
|
81
|
+
python_version = "3.11"
|
|
82
|
+
strict = true
|
|
83
|
+
files = ["src", "tests", "scripts"]
|