From 534dd78a05e35f128890134ddc7580e40faf238e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 7 Oct 2026 09:23:15 +0200 Subject: [PATCH] Python: drop pound force fields and rename newton force fields. AverageDriveForce (6) and PeakDriveForce (7) are the only force fields. IDs 4 and 5 are no longer written or read. Co-authored-by: Cursor --- python/README.md | 7 +++++-- python/src/rowing_data/fields.py | 24 ++---------------------- python/src/rowing_data/model.py | 12 ++++-------- python/src/rowing_data/validation.py | 4 ++-- python/tests/sample_sessions.py | 12 ++++++------ python/tests/test_codec.py | 5 ++--- python/tests/test_fields.py | 6 ++---- python/tests/test_roundtrip.py | 10 +++++----- python/tests/test_writer.py | 12 +++++++----- 9 files changed, 35 insertions(+), 57 deletions(-) diff --git a/python/README.md b/python/README.md index 2d4bfaa..49ab878 100644 --- a/python/README.md +++ b/python/README.md @@ -10,14 +10,17 @@ shipping products. ## Scope -Implements Draft v0.1 **§4 native FIT fields**, **§5.1 core metrics** (IDs 0–9, -19, and **StrokeRate 93**), and Session **RecordingStrategy** (ID 10), under +Implements Draft v0.1 **§4 native FIT fields**, **§5.1 core metrics** (IDs 0–3, +6–9, and 19, and **StrokeRate 93**), and Session **RecordingStrategy** (ID 10), under application UUID `89e86158-6d47-5c98-9d46-7d29437f27b9`. That is conformance **Levels 1–2** as in §7, plus StrokeRate (93). §7’s Level 2 bullet list omits 93; §5.1 still requires native `cadence` when rate is known, so this library writes and reads ID 93. +Force is `AverageDriveForce` (6) and `PeakDriveForce` (7), in newtons. Fields 4 +and 5 are not part of this sample. + **Not implemented** (Level 3–4, or blocked on draft defects): - Oarlock summary fields 11–18 diff --git a/python/src/rowing_data/fields.py b/python/src/rowing_data/fields.py index f8d2720..791201e 100644 --- a/python/src/rowing_data/fields.py +++ b/python/src/rowing_data/fields.py @@ -106,28 +106,8 @@ def _native( _dev(1, "StrokeDriveTime", MessageType.RECORD, BaseType.UINT16, 1, "ms"), _dev(2, "DragFactor", MessageType.RECORD, BaseType.UINT16, 1, ""), _dev(3, "StrokeRecoveryTime", MessageType.RECORD, BaseType.UINT16, 1, "ms"), - _dev( - 4, - "AverageDriveForceLbs", - MessageType.RECORD, - BaseType.UINT16, - 10, - "lbs", - deprecated=True, - notes="Use AverageDriveForceN (6).", - ), - _dev( - 5, - "PeakDriveForceLbs", - MessageType.RECORD, - BaseType.UINT16, - 10, - "lbs", - deprecated=True, - notes="Use PeakDriveForceN (7).", - ), - _dev(6, "AverageDriveForceN", MessageType.RECORD, BaseType.UINT16, 10, "N"), - _dev(7, "PeakDriveForceN", MessageType.RECORD, BaseType.UINT16, 10, "N"), + _dev(6, "AverageDriveForce", MessageType.RECORD, BaseType.UINT16, 10, "N"), + _dev(7, "PeakDriveForce", MessageType.RECORD, BaseType.UINT16, 10, "N"), _dev(8, "AverageBoatSpeed", MessageType.RECORD, BaseType.UINT16, 255, "m/s"), _dev( 9, diff --git a/python/src/rowing_data/model.py b/python/src/rowing_data/model.py index 81fb6dc..d811eaa 100644 --- a/python/src/rowing_data/model.py +++ b/python/src/rowing_data/model.py @@ -29,10 +29,8 @@ class Record: stroke_drive_time_ms: int | None = None drag_factor: int | None = None stroke_recovery_time_ms: int | None = None - average_drive_force_lbs: float | None = None - peak_drive_force_lbs: float | None = None - average_drive_force_n: float | None = None - peak_drive_force_n: float | None = None + average_drive_force: float | None = None + peak_drive_force: float | None = None average_boat_speed_mps: float | None = None workout_state: int | None = None stroke_work_j: int | None = None @@ -80,10 +78,8 @@ def stroke_counts(self) -> list[int]: (1, "stroke_drive_time_ms"), (2, "drag_factor"), (3, "stroke_recovery_time_ms"), - (4, "average_drive_force_lbs"), - (5, "peak_drive_force_lbs"), - (6, "average_drive_force_n"), - (7, "peak_drive_force_n"), + (6, "average_drive_force"), + (7, "peak_drive_force"), (8, "average_boat_speed_mps"), (9, "workout_state"), (19, "stroke_work_j"), diff --git a/python/src/rowing_data/validation.py b/python/src/rowing_data/validation.py index a5201a6..9aeb9a6 100644 --- a/python/src/rowing_data/validation.py +++ b/python/src/rowing_data/validation.py @@ -14,8 +14,8 @@ # Informative typical ranges from spec §8.1 — not encoding limits. _TYPICAL = { - "average_drive_force_n": (0.0, 2000.0), - "peak_drive_force_n": (0.0, 2000.0), + "average_drive_force": (0.0, 2000.0), + "peak_drive_force": (0.0, 2000.0), "drive_length_mm": (300.0, 1500.0), "stroke_rate": (10.0, 100.0), } diff --git a/python/tests/sample_sessions.py b/python/tests/sample_sessions.py index 4b60e60..b45018a 100644 --- a/python/tests/sample_sessions.py +++ b/python/tests/sample_sessions.py @@ -29,8 +29,8 @@ def stroke_boundary_session() -> RowingSession: stroke_drive_time_ms=450, stroke_recovery_time_ms=1650, cycle_length_m=8.5, - average_drive_force_n=412.3, - peak_drive_force_n=780.0, + average_drive_force=412.3, + peak_drive_force=780.0, stroke_work_j=250, heart_rate=140, power=210, @@ -44,8 +44,8 @@ def stroke_boundary_session() -> RowingSession: drive_length_mm=1410, stroke_drive_time_ms=440, stroke_recovery_time_ms=1630, - average_drive_force_n=400.0, - peak_drive_force_n=760.0, + average_drive_force=400.0, + peak_drive_force=760.0, stroke_work_j=240, heart_rate=142, power=215, @@ -59,8 +59,8 @@ def stroke_boundary_session() -> RowingSession: drive_length_mm=1415, stroke_drive_time_ms=455, stroke_recovery_time_ms=1640, - average_drive_force_n=405.0, - peak_drive_force_n=770.0, + average_drive_force=405.0, + peak_drive_force=770.0, stroke_work_j=245, heart_rate=141, power=212, diff --git a/python/tests/test_codec.py b/python/tests/test_codec.py index 230315c..ff3ba0d 100644 --- a/python/tests/test_codec.py +++ b/python/tests/test_codec.py @@ -13,13 +13,12 @@ [ (0, 1420, 1420), # DriveLength mm, scale 1 (1, 450, 450), # StrokeDriveTime ms - (6, 412.3, 4123), # AverageDriveForceN, scale 10 + (6, 412.3, 4123), # AverageDriveForce, scale 10 (7, 0, 0), # measured zero is a real value (8, 4.0, 1020), # AverageBoatSpeed, scale 255 (19, 250, 250), # StrokeWork J (93, 28.50, 2850), # StrokeRate, scale 100 (10, 1, 1), # RecordingStrategy - (4, 90.5, 905), # deprecated pounds still encode ], ) def test_developer_encode_decode(field_id: int, physical: float, raw: int) -> None: @@ -66,7 +65,7 @@ def test_sint32_position_roundtrip() -> None: def test_encode_overflow_raises() -> None: force = field_by_id(6) # UINT16 max 65535 / scale 10 => 6553.5 N is in range; 7000 N is not. - with pytest.raises(CodecError, match="AverageDriveForceN"): + with pytest.raises(CodecError, match="AverageDriveForce"): encode(force, 7000) cadence = field_by_name("cadence") diff --git a/python/tests/test_fields.py b/python/tests/test_fields.py index 658f8b0..ad18f6b 100644 --- a/python/tests/test_fields.py +++ b/python/tests/test_fields.py @@ -23,10 +23,8 @@ (1, "StrokeDriveTime", MessageType.RECORD, BaseType.UINT16, 1, "ms", False), (2, "DragFactor", MessageType.RECORD, BaseType.UINT16, 1, "", False), (3, "StrokeRecoveryTime", MessageType.RECORD, BaseType.UINT16, 1, "ms", False), - (4, "AverageDriveForceLbs", MessageType.RECORD, BaseType.UINT16, 10, "lbs", True), - (5, "PeakDriveForceLbs", MessageType.RECORD, BaseType.UINT16, 10, "lbs", True), - (6, "AverageDriveForceN", MessageType.RECORD, BaseType.UINT16, 10, "N", False), - (7, "PeakDriveForceN", MessageType.RECORD, BaseType.UINT16, 10, "N", False), + (6, "AverageDriveForce", MessageType.RECORD, BaseType.UINT16, 10, "N", False), + (7, "PeakDriveForce", MessageType.RECORD, BaseType.UINT16, 10, "N", False), (8, "AverageBoatSpeed", MessageType.RECORD, BaseType.UINT16, 255, "m/s", False), (9, "WorkoutState", MessageType.RECORD, BaseType.UINT8, 1, "", False), (10, "RecordingStrategy", MessageType.SESSION, BaseType.UINT8, 1, "", False), diff --git a/python/tests/test_roundtrip.py b/python/tests/test_roundtrip.py index 2d334be..6549b60 100644 --- a/python/tests/test_roundtrip.py +++ b/python/tests/test_roundtrip.py @@ -68,7 +68,7 @@ def test_validate_overflow_is_error() -> None: records=( Record( timestamp=datetime(2026, 1, 1, tzinfo=UTC), - average_drive_force_n=7000, + average_drive_force=7000, ), ) ) @@ -85,10 +85,10 @@ def _assert_record_roundtrip(got: Record, want: Record) -> None: assert got.drive_length_mm == want.drive_length_mm assert got.stroke_drive_time_ms == want.stroke_drive_time_ms assert got.stroke_recovery_time_ms == want.stroke_recovery_time_ms - if want.average_drive_force_n is not None: - assert got.average_drive_force_n == pytest.approx(want.average_drive_force_n) - if want.peak_drive_force_n is not None: - assert got.peak_drive_force_n == pytest.approx(want.peak_drive_force_n) + if want.average_drive_force is not None: + assert got.average_drive_force == pytest.approx(want.average_drive_force) + if want.peak_drive_force is not None: + assert got.peak_drive_force == pytest.approx(want.peak_drive_force) assert got.stroke_work_j == want.stroke_work_j assert got.heart_rate == want.heart_rate assert got.power == want.power diff --git a/python/tests/test_writer.py b/python/tests/test_writer.py index 2e3bec0..68556d0 100644 --- a/python/tests/test_writer.py +++ b/python/tests/test_writer.py @@ -50,7 +50,7 @@ def test_writer_application_id_and_recording_strategy(tmp_path: Path) -> None: stroke_rate=28.5, drive_length_mm=1420, stroke_work_j=0, - average_drive_force_n=412.3, + average_drive_force=412.3, ), Record( timestamp=_ts(2), @@ -74,8 +74,10 @@ def test_writer_application_id_and_recording_strategy(tmp_path: Path) -> None: assert descriptions[93] == "StrokeRate" assert descriptions[0] == "DriveLength" assert descriptions[19] == "StrokeWork" - assert descriptions[6] == "AverageDriveForceN" - assert 7 not in descriptions # omitted PeakDriveForceN + assert descriptions[6] == "AverageDriveForce" + assert 4 not in descriptions + assert 5 not in descriptions + assert 7 not in descriptions # omitted PeakDriveForce sessions = list(fit.get_messages("session")) strategy = next(field for field in sessions[0] if field.name == "RecordingStrategy") @@ -94,7 +96,7 @@ def test_writer_application_id_and_recording_strategy(tmp_path: Path) -> None: assert drive.raw_value == 1420 work = next(field for field in first if field.name == "StrokeWork") assert work.raw_value == 0 - force = next(field for field in first if field.name == "AverageDriveForceN") + force = next(field for field in first if field.name == "AverageDriveForce") assert force.raw_value == 4123 assert first.get_value("total_cycles") == 1 assert first.get_value("distance") == pytest.approx(0.0) @@ -109,7 +111,7 @@ def test_writer_rejects_values_that_overflow_fit_types(tmp_path: Path) -> None: with pytest.raises(CodecError): write_fit( _session( - Record(timestamp=_ts(0), average_drive_force_n=7000), + Record(timestamp=_ts(0), average_drive_force=7000), ), tmp_path / "overflow.fit", )