feat(train): rowing on the WP 5000 over FTMS (ADR 0030, D28)

Rowing indoors is offered again, with the rower chosen on the Record
sheet: a sheet looks for Fitness Machine Service rowers, and the one
chosen is remembered in the person's folder. The recorder service
connects to it as to the strap (one NotifyLink for both), parses
FTMS's Rower Data in Kotlin (tested on the JVM on frames written from
the specification and D8's), counts its totals from its first frame,
pauses 5 s after the last stroke, and gets the connectedDevice type. A
row's screen shows its metres, split per 500 m, stroke rate and watts;
its page its average split, stroke rate and power, the highest power,
its strokes, splits per 500 m and charts of the split, stroke rate and
power. Not yet tried on the WP 5000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 16:20:37 +01:00
commit f3d08e4e69
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -120,7 +120,7 @@
"@strapUnnamed": {"description": "A strap that sends no name."},
"strapForget": "Forget strap",
"strapRefused": "Train needs nearby devices to find your strap. You can allow it in the system's settings.",
"strapBluetoothOff": "Bluetooth is off. Turn it on to find your strap.",
"strapBluetoothOff": "Bluetooth is off. Turn it on to look for devices nearby.",
"recordingHeartRate": "heart rate",
"@recordingHeartRate": {"description": "Under the heart rate, lower case."},
"recordingStrapLost": "Strap lost",
@ -131,5 +131,48 @@
"@workoutMaxHeartRate": {"description": "Lower case, under a figure."},
"workoutBpmValue": "{bpm} bpm",
"@workoutBpmValue": {"placeholders": {"bpm": {"type": "int"}}},
"workoutHeartRateChart": "Heart rate"
"workoutHeartRateChart": "Heart rate",
"recordRower": "Rowing machine",
"recordNoRower": "Choose your rowing machine",
"recordRowerRefused": "Train needs nearby devices to record from your rowing machine. You can allow it in the system's settings.",
"rowerSheetTitle": "Rowing machine",
"rowerSearching": "Looking for rowing machines nearby…",
"rowerSearchingHint": "Wake the console: most show once you pull or press a button. One another app holds (Christopeit Connect, Kinomap) can't be found: close that app first.",
"rowerUnnamed": "Unnamed rowing machine",
"rowerForget": "Forget rowing machine",
"rowerRefused": "Train needs nearby devices to find your rowing machine. You can allow it in the system's settings.",
"recordingRowerConnecting": "Looking for the rowing machine",
"@recordingRowerConnecting": {"description": "While recording a row: the rower isn't connected yet. Another app may hold it."},
"recordingRowerLost": "Rowing machine lost",
"@recordingRowerLost": {"description": "While recording a row: the rower is out of reach or off; Train keeps asking for it."},
"recordingSplit": "split",
"@recordingSplit": {"description": "Lower case, under a figure: how long 500 m take now."},
"recordingStrokeRate": "strokes a minute",
"@recordingStrokeRate": {"description": "Lower case, under a figure."},
"recordingWatts": "watts",
"@recordingWatts": {"description": "Lower case, under a figure."},
"workoutSplitValue": "{split} /500 m",
"@workoutSplitValue": {"description": "Minutes and seconds 500 m take.", "placeholders": {"split": {"type": "String", "example": "2:04"}}},
"workoutAverageSplit": "average split",
"@workoutAverageSplit": {"description": "Lower case, under a figure."},
"workoutAverageStrokeRate": "average stroke rate",
"@workoutAverageStrokeRate": {"description": "Lower case, under a figure."},
"workoutStrokeRateValue": "{rate} /min",
"@workoutStrokeRateValue": {"description": "Strokes a minute.", "placeholders": {"rate": {"type": "String", "example": "24"}}},
"workoutAveragePower": "average power",
"@workoutAveragePower": {"description": "Lower case, under a figure."},
"workoutMaxPower": "highest power",
"@workoutMaxPower": {"description": "Lower case, under a figure."},
"workoutWattsValue": "{watts} W",
"@workoutWattsValue": {"placeholders": {"watts": {"type": "int"}}},
"workoutStrokes": "strokes",
"@workoutStrokes": {"description": "Lower case, under a figure: how many strokes."},
"workoutSplitMetres": "{from}–{to} m",
"@workoutSplitMetres": {"description": "A rowing split of 500 m: where it starts and ends.", "placeholders": {"from": {"type": "int"}, "to": {"type": "int"}}},
"workoutSplitChart": "Split",
"workoutStrokeRateChart": "Stroke rate",
"workoutPowerChart": "Power",
"recordAutoPauseRowingHint": "When you stop rowing, going on with the next stroke",
"recordingMetres": "metres"
}