Skip to content

Add NordicTrack Elliptical Spacesaver S700 profile - #4796

Merged
cagnulein merged 10 commits into
masterfrom
fix/issue-4794-nordictrack-s700
Aug 8, 2026
Merged

Add NordicTrack Elliptical Spacesaver S700 profile#4796
cagnulein merged 10 commits into
masterfrom
fix/issue-4794-nordictrack-s700

Conversation

@cagnulein

Copy link
Copy Markdown
Owner

Summary

Fixes #4794.

Analysis of the two debug logs attached to the issue showed:

  • In default (non-SE7i) mode: incline never changes and resistance drops back to 1 because the raw resistance byte is read from a field shared with the (unrelated) inclination high byte, and the lookup table has no fallback for unmatched machine-specific values.
  • In SE7i mode: incline/resistance work (parsed from a separate 00 12 01 04 02 30/31 ... packet type), but pace/calories/watts never update. Root cause: the live telemetry speed packet on this unit is the SE7i type 0x01 frame shifted by 2 bytes — its 0x5a marker sits at byte[4] (with byte[3]==0x00, byte[2] a free-running counter) instead of the SE7i's byte[4]==0x46 — so the existing SE7i speed-detection branch never matched, Speed stayed 0 forever, and since elliptical::watts()/KCal only depend on currentSpeed(), calories/watts/pace never appeared.

Changes

  • New nordictrack_elliptical_s700 setting (off by default), added as the last setting in settings.qml, qzsettings.h/qzsettings.cpp, and settings-catalog.json, so this fix is fully opt-in and doesn't touch the default/C7.5/SE7i profiles for other users.
  • When enabled, the S700 reuses the SE7i init/incline/resistance wire protocol (which already works correctly for this unit), and adds correct S700 speed-packet detection/decoding.
  • Extracted the packet detection/decoding into small static helpers (isS700SpeedPacket, s700SpeedFromPacket, isSe7iResistanceInclinationPacket, se7iInclinationFromPacket, se7iResistanceFromPacket) on nordictrackelliptical, so the exact production parsing logic is directly unit-testable (matching the existing pattern used by apexbike's static parser helpers).
  • Added tst/Devices/TestNordictrackEllipticalS700Parser.h/.cpp, a regression test that feeds real captured BLE frames from the issue's debug logs into these helpers and checks the decoded values (and that they don't collide with the legacy SE7i or default-mode packet shapes).

Test plan

  • Full existing test suite passes (474 tests, 0 failures) after the change — no regressions to other NordicTrack elliptical profiles or other devices.
  • New NordictrackEllipticalS700ParserTest.* tests pass (5/5), using real packets captured from the attached debug logs.
  • Manual confirmation from the reporter with nordictrack_elliptical_s700 enabled on their Spacesaver S700 unit.

🤖 Generated with Claude Code

cagnulein and others added 8 commits July 9, 2026 11:56
The S700 works over the SE7i wire protocol for incline/resistance, but
its live telemetry speed packet is shifted 2 bytes from the SE7i frame:
the 0x5a marker sits at byte[4] (byte[3]==0x00) instead of the SE7i's
byte[4]==0x46, so the existing SE7i speed check never matched. Since
elliptical watts()/KCal only depend on currentSpeed(), this meant no
pace, no watts and no calories for the whole session, even though
incline and resistance (parsed from a separate packet type) worked.

Gate the fix behind a new nordictrack_elliptical_s700 setting so other
NordicTrack elliptical profiles (default, C7.5, SE7i) are unaffected:
enabling it reuses the SE7i init/incline/resistance protocol and adds
correct S700 speed-packet detection/decoding.

Extracted the packet detection/decoding into small static helpers
(isS700SpeedPacket, s700SpeedFromPacket, isSe7iResistanceInclinationPacket,
se7iInclinationFromPacket, se7iResistanceFromPacket) so the exact
production parsing logic is directly unit-testable, and added a
regression test that feeds real captured BLE frames from the issue's
debug logs through this code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Byte[2] of the S700 speed packet carries the stride cadence in RPM,
identical to the SE7i format. The previous commit incorrectly described
it as a "free-running counter" and omitted cadence parsing, leaving
Cadence stuck at 0 for the whole session.

Evidence from real captured packets in the debug logs:
- SE7i log (09:51): byte[2] values increase from 9→45 as user ramps up
- S700 post-patch log (Jul 13): byte[2] values decrease from 34→23 as
  user winds down — both correlate directly with speed, confirming it is
  cadence, not a counter.

Add s700CadenceFromPacket() static helper (byte[2]) and call it in the
S700 speed-packet handler alongside the existing speed parse, matching
the SE7i cadence-parse pattern exactly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… estimate (#4794)

The 00 12 01 04 02 30/31 resistance packet carries machine-measured
wattage at bytes 14-15 (little-endian uint16). Confirmed from Jul 14
debug log where user reported the console showing 0->23W: byte[14]=0x17=23
appears in the first packet after exercise starts, matching exactly.

Previously elliptical::watts() returned ~200W for typical elliptical
speeds via a running VO2 formula (210/pace * weight / 1000 * 75),
which is inaccurate for an elliptical and ignores resistance level.

Add se7iWattsFromPacket() static helper (bytes 14-15 LE) and use it
for SE7i/S700 modes in update_metrics() and the KCal accumulator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tile (#4794)

update_metrics(bool watt_calc, double watts) only writes m_watt (the
value shown in the watt tile) when watt_calc=true. The previous commit
always passed false, so m_watt was never updated to the machine-reported
value even though m_watts was correctly parsed from the packet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hine watts (#4794)

elliptical::watts() always calls m_watt.setValue() as a side effect,
so every incoming BLE packet was resetting m_watt to the VO2-calculated
value, undoing what update_metrics(true, m_watts) had just written.

Two-part fix:
- Set m_watt directly from m_watts in the resistance packet handler,
  immediately after parsing, so it stays current between update() ticks.
- In the characteristicChanged debug line, skip the watts() call (and
  its side effect) for SE7i/S700 mode; read m_watt.value() instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After the SE7i init sequence completes, the S700 switches to
"remote control mode" and its live telemetry packets change from
byte[4]=0x5a to byte[4]=0x46. isS700SpeedPacket was only checking
for 0x5a, so speed/cadence went to 0 once init completed, which
also blocked watt tile updates (update_metrics requires speed > 0).

Extend the check to accept both markers. The speed/cadence layout
(cadence at byte[2], speed LE at bytes[12-13] / 100) is identical
between the two variants. Regression test added from the real log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@stale

stale Bot commented Aug 1, 2026

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the wontfix This will not be worked on label Aug 1, 2026
@stale stale Bot closed this Aug 8, 2026
@cagnulein cagnulein reopened this Aug 8, 2026
@stale stale Bot removed the wontfix This will not be worked on label Aug 8, 2026
@cagnulein
cagnulein merged commit 2410fc7 into master Aug 8, 2026
5 of 17 checks passed
@cagnulein
cagnulein deleted the fix/issue-4794-nordictrack-s700 branch August 8, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

{BUG} Nordictrack Spacesaver S700 Elliptical

1 participant