@@ -143,56 +143,176 @@ void ftmsrower::serviceDiscovered(const QBluetoothUuid &gatt) {
143143 emit debug (QStringLiteral (" serviceDiscovered " ) + gatt.toString ());
144144}
145145
146+ void ftmsrower::processPm5ParserState (ParserRegressionState &state, const QString &charUuid,
147+ const QByteArray &newValue, qint64 nowMs) {
148+ if (charUuid == QStringLiteral (" {ce060031-43e5-11e4-916c-0800200c9a66}" )) {
149+ if (!state.hasFtmsService && newValue.length () >= 6 ) {
150+ const uint32_t distance_dm =
151+ ((((uint32_t )(uint8_t )newValue.at (5 )) << 16 ) | (((uint32_t )(uint8_t )newValue.at (4 )) << 8 ) |
152+ (uint32_t )(uint8_t )newValue.at (3 ));
153+
154+ if (distance_dm > 0 || state.distanceKm == 0.0 ) {
155+ state.distanceKm = distance_dm / 10000.0 ;
156+ state.distanceReceivedFromPm5 = (distance_dm > 0 );
157+ state.lastPm5DistanceUpdateMs = nowMs;
158+ }
159+ }
160+
161+ if (newValue.length () >= 10 ) {
162+ state.rowState = (uint8_t )newValue.at (9 );
163+ state.rowStateReceived = true ;
164+ }
165+ return ;
166+ }
167+
168+ if (charUuid == QStringLiteral (" {ce060032-43e5-11e4-916c-0800200c9a66}" )) {
169+ if (newValue.length () < 7 ) {
170+ return ;
171+ }
172+
173+ const uint8_t spm = (uint8_t )newValue.at (5 );
174+ if (spm > 0 && (!state.rowStateReceived || state.rowState != 0 )) {
175+ state.cadence = spm;
176+ }
177+ if (state.rowStateReceived && state.rowState == 0 ) {
178+ state.cadence = 0 ;
179+ }
180+
181+ const uint16_t speedRaw = ((uint8_t )newValue.at (4 ) << 8 ) | (uint8_t )newValue.at (3 );
182+ if (speedRaw > 0 && (!state.rowStateReceived || state.rowState != 0 )) {
183+ state.speedKmh = (speedRaw * 0.001 ) * 3.6 ;
184+ }
185+ if (state.rowStateReceived && state.rowState == 0 ) {
186+ state.speedKmh = 0 ;
187+ }
188+
189+ if (!state.hasFtmsService && !state.distanceReceivedFromPm5 && state.lastPm5DistanceUpdateMs > 0 ) {
190+ state.distanceKm += ((state.speedKmh / 3600000.0 ) * (nowMs - state.lastPm5DistanceUpdateMs ));
191+ }
192+ if (!state.hasFtmsService ) {
193+ state.lastPm5DistanceUpdateMs = nowMs;
194+ }
195+ }
196+ }
197+
198+ void ftmsrower::processFtmsParserState (ParserRegressionState &state, const QByteArray &newValue, qint64 nowMs,
199+ bool iconsolePlus, bool fitshow, bool mrkR11s, bool whipr, bool kingsmith,
200+ bool dfitLR) {
201+ Q_UNUSED (dfitLR);
202+ union flags {
203+ struct {
204+ uint16_t moreData : 1 ;
205+ uint16_t avgStroke : 1 ;
206+ uint16_t totDistance : 1 ;
207+ uint16_t instantPace : 1 ;
208+ uint16_t avgPace : 1 ;
209+ uint16_t instantPower : 1 ;
210+ uint16_t avgPower : 1 ;
211+ uint16_t resistanceLvl : 1 ;
212+ uint16_t expEnergy : 1 ;
213+ uint16_t heartRate : 1 ;
214+ uint16_t metabolic : 1 ;
215+ uint16_t elapsedTime : 1 ;
216+ uint16_t remainingTime : 1 ;
217+ uint16_t spare : 3 ;
218+ };
219+
220+ uint16_t word_flags;
221+ };
222+
223+ flags Flags;
224+ int index = 0 ;
225+ Q_UNUSED (whipr);
226+ Q_UNUSED (kingsmith);
227+
228+ Flags.word_flags = (newValue.at (1 ) << 8 ) | newValue.at (0 );
229+ index += 2 ;
230+
231+ if (!Flags.moreData ) {
232+ index += 3 ;
233+ }
234+
235+ if (Flags.avgStroke ) {
236+ index += 1 ;
237+ }
238+
239+ if (Flags.totDistance ) {
240+ if (iconsolePlus || fitshow || mrkR11s) {
241+ if (state.lastRefreshMs > 0 ) {
242+ state.distanceKm += ((state.speedKmh / 3600000.0 ) * (nowMs - state.lastRefreshMs ));
243+ }
244+ } else if (newValue.length () >= index + 3 ) {
245+ state.distanceKm = ((double )((((uint32_t )((uint8_t )newValue.at (index + 2 )) << 16 ) |
246+ (uint32_t )((uint8_t )newValue.at (index + 1 )) << 8 ) |
247+ (uint32_t )((uint8_t )newValue.at (index)))) /
248+ 1000.0 ;
249+ }
250+ index += 3 ;
251+ } else if (state.lastRefreshMs > 0 ) {
252+ state.distanceKm += ((state.speedKmh / 3600000.0 ) * (nowMs - state.lastRefreshMs ));
253+ }
254+
255+ if (Flags.instantPace && newValue.length () >= index + 2 ) {
256+ const double instantPace =
257+ ((double )(((uint16_t )((uint8_t )newValue.at (index + 1 )) << 8 ) | (uint16_t )((uint8_t )newValue.at (index))));
258+ if (instantPace == 0 || instantPace == 65535 ) {
259+ state.speedKmh = 0 ;
260+ } else {
261+ state.speedKmh = (60.0 / instantPace) * 30.0 ;
262+ }
263+ }
264+
265+ state.lastRefreshMs = nowMs;
266+ }
267+
146268void ftmsrower::parseConcept2Data (const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
147269 QDateTime now = QDateTime::currentDateTime ();
148270 QSettings settings;
149271
150272 QString charUuid = characteristic.uuid ().toString ();
273+
274+ ParserRegressionState parserState;
275+ parserState.distanceKm = Distance.value ();
276+ parserState.speedKmh = Speed.value ();
277+ parserState.cadence = Cadence.value ();
278+ parserState.rowState = pm5RowState;
279+ parserState.rowStateReceived = pm5RowStateReceived;
280+ parserState.hasFtmsService = pm5HasFTMSService;
281+ parserState.distanceReceivedFromPm5 = pm5DistanceReceived;
282+ parserState.lastRefreshMs = lastRefreshCharacteristicChanged.toMSecsSinceEpoch ();
283+ parserState.lastPm5DistanceUpdateMs = lastPm5DistanceUpdate.toMSecsSinceEpoch ();
284+
285+ processPm5ParserState (parserState, charUuid, newValue, now.toMSecsSinceEpoch ());
286+
287+ Distance = parserState.distanceKm ;
288+ Speed = parserState.speedKmh ;
289+ Cadence = parserState.cadence ;
290+ pm5RowState = parserState.rowState ;
291+ pm5RowStateReceived = parserState.rowStateReceived ;
292+ pm5DistanceReceived = parserState.distanceReceivedFromPm5 ;
293+ if (parserState.lastPm5DistanceUpdateMs > 0 ) {
294+ lastPm5DistanceUpdate = QDateTime::fromMSecsSinceEpoch (parserState.lastPm5DistanceUpdateMs );
295+ }
151296
152297 if (charUuid == QStringLiteral (" {ce060031-43e5-11e4-916c-0800200c9a66}" )) {
153298 // Parse characteristic CE060031 - Based on go-row implementation
154299 if (newValue.length () >= 10 ) {
155- // Extract RowState from byte 9 - this indicates if user is actively rowing
156- pm5RowState = (uint8_t )newValue.at (9 );
157- pm5RowStateReceived = true ; // Mark that we've received RowState at least once
158-
159300 emit debug (QStringLiteral (" PM5 CE060031 RAW: " ) + newValue.toHex (' ' ) +
301+ QStringLiteral (" Distance: " ) + QString::number (Distance.value ()) +
160302 QStringLiteral (" RowState: " ) + QString::number (pm5RowState));
161303 }
162304 }
163305 else if (charUuid == QStringLiteral (" {ce060032-43e5-11e4-916c-0800200c9a66}" )) {
164306 // Parse characteristic CE060032 - Based on go-row implementation
165307 if (newValue.length () >= 7 ) {
166- // Extract cadence (SPM) from byte 5
167- uint8_t spm = (uint8_t )newValue.at (5 );
168- if (spm > 0 ) {
169- // Only check RowState if we've received it at least once
170- if (!pm5RowStateReceived || pm5RowState != 0 ) {
171- Cadence = spm;
172- lastStroke = now;
173- }
174- }
175- // Zero cadence if RowState indicates not rowing (and we've received RowState)
176- if (pm5RowStateReceived && pm5RowState == 0 ) {
177- Cadence = 0 ;
178- }
179-
180- // Extract speed from bytes 3-4 (little endian) in 0.001m/s
181- uint16_t speedRaw = ((uint8_t )newValue.at (4 ) << 8 ) | (uint8_t )newValue.at (3 );
182- if (speedRaw > 0 ) {
183- // Only check RowState if we've received it at least once
184- if (!pm5RowStateReceived || pm5RowState != 0 ) {
185- Speed = (speedRaw * 0.001 ) * 3.6 ; // Convert m/s to km/h
186- }
187- }
188- // Zero speed if RowState indicates not rowing (and we've received RowState)
189- if (pm5RowStateReceived && pm5RowState == 0 ) {
190- Speed = 0 ;
308+ if (Cadence.value () > 0 ) {
309+ lastStroke = now;
191310 }
192311
193312 emit debug (QStringLiteral (" PM5 CE060032 RAW: " ) + newValue.toHex (' ' ) +
194313 QStringLiteral (" Cadence: " ) + QString::number (Cadence.value ()) +
195314 QStringLiteral (" Speed: " ) + QString::number (Speed.value ()) +
315+ QStringLiteral (" Distance: " ) + QString::number (Distance.value ()) +
196316 QStringLiteral (" RowState: " ) + QString::number (pm5RowState));
197317 }
198318 }
@@ -273,6 +393,9 @@ void ftmsrower::parseConcept2Data(const QLowEnergyCharacteristic &characteristic
273393 m_watt = 0 ;
274394 Cadence = 0 ;
275395 Speed = 0 ;
396+ if (!pm5HasFTMSService) {
397+ lastPm5DistanceUpdate = now;
398+ }
276399 }
277400
278401 // Update metrics for virtual device
@@ -395,22 +518,25 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
395518 emit debug (QStringLiteral (" Current Average Stroke: " ) + QString::number (avgStroke));
396519 }
397520
521+ ParserRegressionState parserState;
522+ parserState.distanceKm = Distance.value ();
523+ parserState.speedKmh = Speed.value ();
524+ parserState.cadence = Cadence.value ();
525+ parserState.rowState = pm5RowState;
526+ parserState.rowStateReceived = pm5RowStateReceived;
527+ parserState.hasFtmsService = pm5HasFTMSService;
528+ parserState.distanceReceivedFromPm5 = pm5DistanceReceived;
529+ parserState.lastRefreshMs = lastRefreshCharacteristicChanged.toMSecsSinceEpoch ();
530+ parserState.lastPm5DistanceUpdateMs = lastPm5DistanceUpdate.toMSecsSinceEpoch ();
531+
532+ processFtmsParserState (parserState, newValue, now.toMSecsSinceEpoch (), ICONSOLE_PLUS , FITSHOW , MRK_R11S , WHIPR ,
533+ KINGSMITH , DFIT_L_R );
534+
535+ Distance = parserState.distanceKm ;
536+ Speed = parserState.speedKmh ;
537+
398538 if (Flags.totDistance ) {
399- if (ICONSOLE_PLUS || FITSHOW || MRK_R11S ) {
400- // For ICONSOLE+/FITSHOW/MRK_R11S, always calculate distance from speed instead of using characteristic data
401- Distance += ((Speed.value () / 3600000.0 ) *
402- ((double )lastRefreshCharacteristicChanged.msecsTo (now)));
403- } else {
404- // For other devices, use the distance from characteristic data
405- Distance = ((double )((((uint32_t )((uint8_t )newValue.at (index + 2 )) << 16 ) |
406- (uint32_t )((uint8_t )newValue.at (index + 1 )) << 8 ) |
407- (uint32_t )((uint8_t )newValue.at (index)))) /
408- 1000.0 ;
409- }
410539 index += 3 ;
411- } else {
412- Distance += ((Speed.value () / 3600000.0 ) *
413- ((double )lastRefreshCharacteristicChanged.msecsTo (now)));
414540 }
415541
416542 emit debug (QStringLiteral (" Current Distance: " ) + QString::number (Distance.value ()));
@@ -423,14 +549,6 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
423549 index += 2 ;
424550 emit debug (QStringLiteral (" Current Pace: " ) + QString::number (instantPace));
425551
426- // Always handle invalid pace values to prevent division by zero
427- if (instantPace == 0 || instantPace == 65535 ) {
428- Speed = 0 ;
429- } else {
430- if ((DFIT_L_R && Cadence.value () > 0 ) || !DFIT_L_R )
431- Speed = (60.0 / instantPace) * 30.0 ; // translating pace (min/500m) to km/h in order to match the pace function in the rower.cpp
432- }
433-
434552 emit debug (QStringLiteral (" Current Speed: " ) + QString::number (Speed.value ()));
435553 }
436554
@@ -768,6 +886,7 @@ void ftmsrower::serviceScanDone(void) {
768886 break ;
769887 }
770888 }
889+ pm5HasFTMSService = hasFTMSService;
771890
772891 // If no FTMS service, check for Concept2 PM5 services
773892 if (!hasFTMSService && PM5 ) {
0 commit comments