Integrating WaterRower S4 monitor #260
Replies: 3 comments 10 replies
|
I think it looks pretty good. Playing with the flywheel inertia allows you to change the pace, so if you feel it is too slow, that is the place to change that. Is there a reason to keep the S4 in the loop? There are quite some people who simply replaced the S4 completely with ORM. |
|
I changed flywheelInertia to 0.1 However, I notice that drive length is overestimated. For example it is stated 4m of drive length. A small pull on the cord would state 1m or so. Any idea ? Another thing, I had issues with bluetooth. When trying to connect to EXR, it was either not detecting it, or detecting it then when trying to connect, it was failing (then not detecting it anymore). But I don't know if it will be stable after that (like... if it works once, it will still work) or if I need to have some kind of easy restart procedure. |
|
I don't know if I put this in another topic, or maybe it is related to my stuff only. I did my first "real" session since configuring ORM.
I attach an extract of the session. You see that during the time interval of one minute, there are only 18 strokes, but:
Any idea what's happening ? edit: my Pi is undervolted, so it probably causes throttling.. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I wanted more precise monitoring than what the S4 monitor itself provides. I first tried PiRowFlo, which reads the pre-computed values the S4 outputs over serial — but those values update slowly and are smoothed quite heavily, which becomes a real problem with games like EXR that need responsive data.
While digging into the S4's USB serial protocol, I noticed it also exposes raw pulse counts (the flywheel's has 40 ticks), not just the pre-computed metrics. Poking around further, I came across this project, and started wondering whether I could feed the S4's raw serial output into ORM's engine instead.
Approach 1 — naive per-pulse reconstruction
The S4 only reports an aggregate pulse count per fixed 25ms window (e.g. P05 = 5 ticks passes in the last 25ms), not individual pulse timestamps. First attempt: for each line, divide the 25ms window evenly by the pulse count and call the engine once per pulse with that uniform dt. This produced a very noisy, "staircase" signal (visible in the raw dt recordings vs. the reference examples) with sharp discontinuities at line boundaries, which caused significant stroke over-counting (~1.5-2x too many strokes detected) — the engine's drive/recovery detection was reacting to quantization artifacts, not real acceleration.
Approach 2 — smoothing, but with a performance regression
To fix the staircase artifact, added a buffer that accumulated N lines before flushing a smoothed batch. This fixed the counting issue but introduced two problems: (1) discontinuities at batch boundaries (fixed by switching to a proper sliding window with weighted smoothing across neighbouring points instead of tumbling batches), and (2) CPU load spiking to 100-176% on a rPi 3B, because each individual physical pulse still triggered its own handleRotationImpulse call — at ~250 calls/sec during active rowing, the per-call overhead was probably too expensive.
Final approach — sliding window smoothing + impulse grouping
Combined a continuous sliding window (weighted average across neighbouring line-readings, no batch discontinuities) with grouping N=5 real physical impulses into a single engine call, while dividing numOfImpulsesPerRevolution by the same factor (40 → 8) to keep the reported rotation physically correct. This cut CPU load roughly 5x (down to ~35% steady-state) while keeping the smoothed signal quality that fixed the over-counting. End result: stroke counts now match real strokes, drive/recovery timing looks physically realistic, and CPU load is stable.
Here the plotted dT, it looks very clean to me:

I played around a LOT with the config,I don't know if my values are correct, but it seems to be working.
'use strict'
export default {
loglevel: {
default: 'info',
RowingEngine: 'debug'
},
createRawDataFiles: true,
rowerSettings: {
numOfImpulsesPerRevolution: 8,
minimumStrokeQuality: 0.85,
dragFactor: 5000,
autoAdjustDragFactor: true,
flankLength: 10,
minimumTimeBetweenImpulses: 0.001,
maximumTimeBetweenImpulses: 0.05,
flywheelInertia: 0.073,
naturalDeceleration: -0.015,
minimumDriveTime: 0.65,
minimumRecoveryTime: 0.8
}
}
Is my approach seems reasonable ? Should I change something ?
All reactions