Skip to content

Add native Topiom FS-442900 FitShow rower support - #4919

Open
cagnulein wants to merge 2 commits into
masterfrom
codex/add-support-for-topiom-fs-442900-rower
Open

Add native Topiom FS-442900 FitShow rower support#4919
cagnulein wants to merge 2 commits into
masterfrom
codex/add-support-for-topiom-fs-442900-rower

Conversation

@cagnulein

Copy link
Copy Markdown
Owner

Motivation

  • Add proper native support for the Topiom FS-442900 / Topiom V2 water rower so it is represented as a rower (not a bike) and exposes rower-specific metrics like strokes and stroke count.
  • Reuse the FitShow-family wire protocol semantics while avoiding regressions to existing FitShow bike/treadmill/rower implementations.

Description

  • Added a new fitshowrower class (src/devices/fitshowrower/fitshowrower.h and .../fitshowrower.cpp) derived from rower that implements FitShow protocol parsing, checksum validation, lifecycle, and virtual device integration.
  • Implemented robust FitShow framing checks (STX/ETX, minimum length, and XOR FCS) and packet parsers for capability (0x41), live status (0x42), cumulative workout (0x43), and start/stop (0x44) messages, including distance high-bit scaling and power/cadence decoding.
  • Wire-level behavior uses service FFF0, subscribes to notifications on FFF1, and writes polls/commands to FFF2 (using WriteWithoutResponse when available), performs the captured initialization sequence, and polls at ~2 Hz alternating live-status and cumulative-data without blocking for one-to-one replies.
  • Device detection added narrowly for the Topiom name with FS-442900 (case-insensitive) and inserted before the generic FS-*/FitPlus handlers in src/devices/bluetooth.cpp, and the new device is registered in src/qdomyos-zwift.pri and src/devices/bluetooth.h.
  • Behavior specifics: reports zero controllable resistance (maxResistance()==0), maps cadence->SPM, cumulative 0x43 distance->Distance (km), StrokesCount, KCal, elapsed, and power (from 0x42), clears stale cadence/power when idle, and logs malformed frames and diagnostic information.
  • Added parser/detection regression tests under tst/Devices/TestFitshowRowerParser.{h,cpp} covering capability packets, live-running and idle examples, cumulative distance scaling, checksum rejection, and detection narrowing, and updated test project include lists.

Testing

  • Ran git diff --check and repository local checks which reported no whitespace/patch errors and passed.
  • Executed a standalone python3 script to verify XOR checksums for the exact captured packets used in tests, which matched the expected FCS values.
  • Added Google Test regression test sources for the FitShow parser, but a full Qt/qmake build and execution of the test binary could not be run in this environment because Qt build tools (qmake/development toolchain) were not available.
  • Basic runtime/interaction with system BLE and integration was not executed here; reviewers should run a full build (qmake && make) and exercise with a real Topiom FS-442900 device to validate end-to-end BLE behavior and virtual-device interactions.

Codex Task

@maverickgmt

Copy link
Copy Markdown

Hardware test report — Topiom FS-442900 (thanks for the fast turnaround on this! 🚣)

Tested the windows-msvc2019-binary-no-python and fdroid-android-trial builds from run 31706112961.

Working ✅ (Android, Pixel/Galaxy)

  • Detection & connection: advertises service fff0, connects as a rower, logs Found Topiom rower: FS-442900
  • Stroke rate (observed avg 12 / max 20 SPM), stroke count increments, zeroes when idle
  • Power (plausible watts), distance/odometer (accumulated correctly), elapsed time
  • With virtual_device_rower enabled, MyWhoosh bridges fine (~3 s reflection lag, expected)

Bug 1 — 500m pace/split always 0:00

Expected, given applyPacket() hardcodes Speed = 0 in the 0x42 branch ("no verified unit"), and 0x42/0x43 polls alternate so any Speed would be re-zeroed each status poll. Since the console blanks while a BLE client is connected, the 0x42 speed bytes can't be cross-checked on hardware. Proposed fix below derives Speed from the already-verified 0x43 distance/time deltas instead — no need to decode the 0x42 speed bytes.

Bug 2 — Windows build connects then immediately drops (no data)

On Windows (msvc2019, Qt5/WinRT) QZ discovers FS-442900, creates the fitshowrower, subscribes MQTT — then FitShow controller error 7: Remote device closed the connection ~1 s later, before any notification frame arrives. The MinGW windows-binary couldn't discover the device at all. Root cause looks like the WinRT GATT stack requiring the peripheral to be paired for data access, while the Topiom refuses OS pairing ("try connecting your device again"). Android (unpaired GATT) is unaffected — hence it works there. Flagging in case it's worth a WinRT connection-path note; the driver logic itself is fine.

Proposed patch for Bug 1 (Speed / 500m split)

Derive instantaneous Speed (km/h) from Δdistance/Δtime across consecutive 0x43 cumulative frames (both fields already decoded & verified — they drive the odometer). Guards against session resets and same-second polls. QZ then produces the 500m split from Speed automatically.

--- a/src/devices/fitshowrower/fitshowrower.h
+++ b/src/devices/fitshowrower/fitshowrower.h
@@ class fitshowrower : public rower {
     QDateTime lastStroke = QDateTime::currentDateTime();
+    // Track previous cumulative (0x43) sample to derive Speed from distance/time deltas.
+    qint32 lastCumulativeElapsed = -1;
+    quint16 lastCumulativeDistance = 0;
 };

--- a/src/devices/fitshowrower/fitshowrower.cpp
+++ b/src/devices/fitshowrower/fitshowrower.cpp
@@ void fitshowrower::applyPacket(const Packet &packet) {
     if (packet.command == 0x42) {
         const bool running = packet.status == 0x02;
         Cadence = running ? packet.cadence : 0;
         m_watt = running ? packet.power : 0;
-        Speed = 0; // The Topiom speed field has no verified unit; distance comes from command 0x43.
+        // Speed is derived from the verified cumulative distance/time deltas in the 0x43
+        // frame (below), NOT the unverified 0x42 speed bytes. Only clear it when idle so
+        // the 500m pace tile stays live while rowing.
+        if (!running)
+            Speed = 0;
@@     } else if (packet.command == 0x43) {
         if (packet.strokeCount > StrokesCount.value()) {
             lastStroke = QDateTime::currentDateTime();
         }
+        // Instantaneous speed (km/h) from change in cumulative distance over change in
+        // elapsed time. Guards session resets (values going backwards) and same-second
+        // polls (deltaTime == 0 -> keep previous Speed).
+        if (lastCumulativeElapsed >= 0 &&
+            packet.elapsedSeconds >= static_cast<quint16>(lastCumulativeElapsed) &&
+            packet.distanceMeters >= lastCumulativeDistance) {
+            const int deltaTime = packet.elapsedSeconds - lastCumulativeElapsed;
+            const int deltaDistance = packet.distanceMeters - lastCumulativeDistance;
+            if (deltaTime > 0)
+                Speed = (static_cast<double>(deltaDistance) / deltaTime) * 3.6;
+        }
+        lastCumulativeElapsed = packet.elapsedSeconds;
+        lastCumulativeDistance = packet.distanceMeters;
         elapsed = packet.elapsedSeconds;
         Distance = packet.distanceMeters / 1000.0;
         KCal = packet.calories;
         StrokesCount = packet.strokeCount;

Note: distance is integer meters polled ~1 s apart, so the split has ~±0.5 m/s quantization (a few seconds of jitter). A 3-sample moving average on Speed would smooth it if preferred, but the raw delta already gives a correct, usable 500m split. Happy to capture a debug log of a steady-state row if useful.

@cagnulein

Copy link
Copy Markdown
Owner Author

Hi @maverickgmt i don't like the speed derived from distance, I would prefer to check in a log if it sends it directly.

For the windows build use the msvc one instead

Let me know

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Topiom FS-442900 connects on the Bluetooth screen but shows zero metrics,

2 participants