@@ -31,6 +31,7 @@ nordictrackelliptical::nordictrackelliptical(bool noWriteResistance, bool noHear
3131 QSettings settings;
3232 nordictrack_elliptical_c7_5 = settings.value (QZSettings::nordictrack_elliptical_c7_5, QZSettings::default_nordictrack_elliptical_c7_5).toBool ();
3333 nordictrack_se7i = settings.value (QZSettings::nordictrack_se7i, QZSettings::default_nordictrack_se7i).toBool ();
34+ nordictrack_elliptical_s700 = settings.value (QZSettings::nordictrack_elliptical_s700, QZSettings::default_nordictrack_elliptical_s700).toBool ();
3435
3536 initDone = false ;
3637 connect (refresh, &QTimer::timeout, this , &nordictrackelliptical::update);
@@ -279,7 +280,7 @@ void nordictrackelliptical::forceIncline(double requestIncline) {
279280 writeCharacteristic ((uint8_t *)inc200, sizeof (inc200), QStringLiteral (" inc200" ), false , true );
280281 break ;
281282 }
282- } else if (nordictrack_se7i) {
283+ } else if (nordictrack_se7i || nordictrack_elliptical_s700 ) {
283284 // SE7i uses ff 0d packet with byte[10]=0x02 for incline
284285 // Incline encoding: value = incline% * 100
285286 uint16_t incValue = (uint16_t )(requestIncline * 100 );
@@ -411,7 +412,7 @@ void nordictrackelliptical::forceResistance(resistance_t requestResistance) {
411412 writeCharacteristic ((uint8_t *)res22, sizeof (res22), QStringLiteral (" resistance22" ), false , true );
412413 break ;
413414 }
414- } else if (nordictrack_se7i) {
415+ } else if (nordictrack_se7i || nordictrack_elliptical_s700 ) {
415416 // SE7i uses ff 0d packet with byte[10]=0x04 for resistance
416417 // Resistance encoding: value = resistance * 454 - 1
417418 uint16_t resValue = (requestResistance * 454 ) - 1 ;
@@ -610,7 +611,7 @@ void nordictrackelliptical::forceResistance(resistance_t requestResistance) {
610611}
611612
612613void nordictrackelliptical::se7i_send_next_frame () {
613- if (!nordictrack_se7i) {
614+ if (!nordictrack_se7i && !nordictrack_elliptical_s700 ) {
614615 return ;
615616 }
616617
@@ -709,7 +710,10 @@ void nordictrackelliptical::update() {
709710 settings.value (QZSettings::proform_hybrid_trainer_xt, QZSettings::default_proform_hybrid_trainer_xt)
710711 .toBool ();
711712
712- update_metrics (false , watts ());
713+ {
714+ bool use_machine_watts = (nordictrack_se7i || nordictrack_elliptical_s700) && m_watts > 0 ;
715+ update_metrics (use_machine_watts, use_machine_watts ? (double )m_watts : watts ());
716+ }
713717
714718 if (nordictrack_elliptical_c7_5) {
715719 uint8_t noOpData1[] = {0xfe , 0x02 , 0x17 , 0x03 };
@@ -762,7 +766,7 @@ void nordictrackelliptical::update() {
762766 if (counterPoll > 4 ) {
763767 counterPoll = 0 ;
764768 }
765- } else if (nordictrack_se7i) {
769+ } else if (nordictrack_se7i || nordictrack_elliptical_s700 ) {
766770 // NordicTrack Elliptical SE7i - 6 packet sendPoll cycle
767771 uint8_t se7i_noOpData1[] = {0xfe , 0x02 , 0x17 , 0x03 };
768772 uint8_t se7i_noOpData2[] = {0x00 , 0x12 , 0x02 , 0x04 , 0x02 , 0x13 , 0x06 , 0x13 , 0x02 , 0x00 ,
@@ -895,6 +899,43 @@ void nordictrackelliptical::serviceDiscovered(const QBluetoothUuid &gatt) {
895899 emit debug (QStringLiteral (" serviceDiscovered " ) + gatt.toString ());
896900}
897901
902+ bool nordictrackelliptical::isS700SpeedPacket (const QByteArray &packet) {
903+ // Byte[2] is a free-running counter/cadence, not part of the marker.
904+ // Before SE7i init the machine sends byte[4]=0x5a; after init completes (remote-control mode)
905+ // it switches to byte[4]=0x46. Both carry the same speed/cadence layout.
906+ return packet.length () == 20 && (uint8_t )packet.at (0 ) == 0x01 && (uint8_t )packet.at (1 ) == 0x12 &&
907+ (uint8_t )packet.at (3 ) == 0x00 &&
908+ ((uint8_t )packet.at (4 ) == 0x5a || (uint8_t )packet.at (4 ) == 0x46 );
909+ }
910+
911+ double nordictrackelliptical::s700SpeedFromPacket (const QByteArray &packet) {
912+ return (double )(((uint16_t )((uint8_t )packet.at (13 )) << 8 ) + (uint16_t )((uint8_t )packet.at (12 ))) / 100.0 ;
913+ }
914+
915+ uint8_t nordictrackelliptical::s700CadenceFromPacket (const QByteArray &packet) {
916+ return (uint8_t )packet.at (2 );
917+ }
918+
919+ bool nordictrackelliptical::isSe7iResistanceInclinationPacket (const QByteArray &packet) {
920+ return packet.length () == 20 && (uint8_t )packet.at (0 ) == 0x00 && (uint8_t )packet.at (1 ) == 0x12 &&
921+ (uint8_t )packet.at (2 ) == 0x01 && (uint8_t )packet.at (3 ) == 0x04 && (uint8_t )packet.at (4 ) == 0x02 &&
922+ ((uint8_t )packet.at (5 ) == 0x30 || (uint8_t )packet.at (5 ) == 0x31 );
923+ }
924+
925+ double nordictrackelliptical::se7iInclinationFromPacket (const QByteArray &packet) {
926+ uint16_t incValue = ((uint16_t )((uint8_t )packet.at (10 ))) + ((uint16_t )((uint8_t )packet.at (11 )) << 8 );
927+ return ((double )incValue) / 100.0 ;
928+ }
929+
930+ double nordictrackelliptical::se7iResistanceFromPacket (const QByteArray &packet) {
931+ uint16_t resValue = ((uint16_t )((uint8_t )packet.at (12 ))) + ((uint16_t )((uint8_t )packet.at (13 )) << 8 );
932+ return ((double )(resValue + 1 )) / 454.0 ;
933+ }
934+
935+ uint16_t nordictrackelliptical::se7iWattsFromPacket (const QByteArray &packet) {
936+ return ((uint16_t )((uint8_t )packet.at (14 ))) + ((uint16_t )((uint8_t )packet.at (15 )) << 8 );
937+ }
938+
898939double nordictrackelliptical::GetInclinationFromPacket (QByteArray packet) {
899940 uint16_t r = ((uint16_t )((uint8_t )packet.at (10 ))) + ((uint16_t )((uint8_t )packet.at (11 )) << 8 );
900941 return ((double )r) / 100.0 ;
@@ -1101,7 +1142,7 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
11011142 emit debug (QStringLiteral (" << " ) + newValue.toHex (' ' ));
11021143
11031144 // SE7i frame-based protocol: check for 0xFF marker indicating end of response frame
1104- if (nordictrack_se7i && se7i_waiting_for_response && newValue.length () > 0 ) {
1145+ if (( nordictrack_se7i || nordictrack_elliptical_s700) && se7i_waiting_for_response && newValue.length () > 0 ) {
11051146 if ((uint8_t )newValue.at (0 ) == 0xFF ) {
11061147 emit debug (QStringLiteral (" SE7i: Received 0xFF marker - end of response frame detected" ));
11071148 se7i_waiting_for_response = false ;
@@ -1139,6 +1180,29 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
11391180 return ;
11401181 }
11411182
1183+ // NordicTrack Elliptical Spacesaver S700 Speed/Cadence parsing (Type 0x01 packets, header
1184+ // 01 12 xx 00 5a ..., where byte[2] carries the stride cadence in RPM). This unit uses the
1185+ // same SE7i wire protocol for init/incline/resistance, but its live telemetry packet is shifted
1186+ // by 2 bytes compared to the SE7i marker above: the 0x5a marker sits at byte[4] instead of
1187+ // byte[4]==0x46, so the SE7i check above never matches it.
1188+ if (nordictrack_elliptical_s700 && initDone == true && isS700SpeedPacket (newValue)) {
1189+ Speed = s700SpeedFromPacket (newValue);
1190+ emit debug (QStringLiteral (" Current Speed: " ) + QString::number (Speed.value ()));
1191+ lastSpeedChanged = QDateTime::currentDateTime ();
1192+
1193+ uint8_t c = s700CadenceFromPacket (newValue);
1194+ if (c > 0 )
1195+ Cadence = (c * cadence_gain) + cadence_offset;
1196+ else
1197+ Cadence = 0 ;
1198+ emit debug (QStringLiteral (" Current Cadence: " ) + QString::number (Cadence.value ()));
1199+ if (Cadence.value () > 0 ) {
1200+ CrankRevs++;
1201+ LastCrankEventTime += (uint16_t )(1024.0 / (((double )(Cadence.value ())) / 60.0 ));
1202+ }
1203+ return ;
1204+ }
1205+
11421206 if (!proform_hybrid_trainer_xt && !nordictrack_elliptical_c7_5 && newValue.length () == 20 &&
11431207 newValue.at (0 ) == 0x01 && newValue.at (1 ) == 0x12 && newValue.at (19 ) == 0x2C ) {
11441208 uint8_t c = newValue.at (2 );
@@ -1154,7 +1218,7 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
11541218 return ;
11551219 }
11561220
1157- if (!nordictrack_elliptical_c7_5 && !nordictrack_se7i && newValue.length () == 20 && newValue.at (0 ) == 0x01 && newValue.at (1 ) == 0x12 &&
1221+ if (!nordictrack_elliptical_c7_5 && !nordictrack_se7i && !nordictrack_elliptical_s700 && newValue.length () == 20 && newValue.at (0 ) == 0x01 && newValue.at (1 ) == 0x12 &&
11581222 initDone == true ) {
11591223 Speed = (double )(((uint16_t )((uint8_t )newValue.at (15 )) << 8 ) + (uint16_t )((uint8_t )newValue.at (14 ))) / 100.0 ;
11601224 emit debug (QStringLiteral (" Current Speed: " ) + QString::number (Speed.value ()));
@@ -1166,7 +1230,7 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
11661230 emit debug (QStringLiteral (" Current Heart from machinery: " ) + QString::number (heart));
11671231 }
11681232
1169- } else if (!nordictrack_se7i && QDateTime::currentDateTime ().secsTo (lastSpeedChanged) > 3 ) {
1233+ } else if (!nordictrack_se7i && !nordictrack_elliptical_s700 && QDateTime::currentDateTime ().secsTo (lastSpeedChanged) > 3 ) {
11701234 Speed = 0 ;
11711235 emit debug (QStringLiteral (" Current Speed: " ) + QString::number (Speed.value ()));
11721236 }
@@ -1203,7 +1267,7 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
12031267 }
12041268
12051269 // Skip the strict packet validation for SE7i (it has different packet structures)
1206- if (!nordictrack_se7i &&
1270+ if (!nordictrack_se7i && !nordictrack_elliptical_s700 &&
12071271 (newValue.length () != 20 || (newValue.at (0 ) != 0x00 && newValue.at (0 ) != 0x01 ) || newValue.at (1 ) != 0x12 ||
12081272 newValue.at (2 ) != 0x01 || newValue.at (3 ) != 0x04 || newValue.at (4 ) != 0x02 ||
12091273 (newValue.at (5 ) != 0x2e && newValue.at (5 ) != 0x30 && newValue.at (5 ) != 0x31 ) ||
@@ -1229,20 +1293,19 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
12291293 }
12301294 }
12311295
1232- if (nordictrack_se7i && newValue.length () == 20 && newValue.at (0 ) == 0x00 &&
1233- newValue.at (1 ) == 0x12 && newValue.at (2 ) == 0x01 && newValue.at (3 ) == 0x04 && newValue.at (4 ) == 0x02 &&
1234- (newValue.at (5 ) == 0x30 || newValue.at (5 ) == 0x31 )) {
1235- // SE7i Resistance and Inclination parsing (Type 0x00 packets)
1236- // Inclination from bytes 10-11 (little endian, divided by 100)
1237- uint16_t incValue = ((uint16_t )((uint8_t )newValue.at (10 ))) + ((uint16_t )((uint8_t )newValue.at (11 )) << 8 );
1238- Inclination = ((double )incValue) / 100.0 ;
1296+ if ((nordictrack_se7i || nordictrack_elliptical_s700) && isSe7iResistanceInclinationPacket (newValue)) {
1297+ // SE7i/S700 Resistance, Inclination and Wattage parsing (Type 0x00 packets)
1298+ Inclination = se7iInclinationFromPacket (newValue);
12391299 emit debug (QStringLiteral (" Current Inclination from packet: " ) + QString::number (Inclination.value ()));
12401300
1241- // Resistance from bytes 12-13 (little endian): resistance = (value + 1) / 454
1242- uint16_t resValue = ((uint16_t )((uint8_t )newValue.at (12 ))) + ((uint16_t )((uint8_t )newValue.at (13 )) << 8 );
1243- Resistance = ((double )(resValue + 1 )) / 454.0 ;
1301+ Resistance = se7iResistanceFromPacket (newValue);
12441302 emit debug (QStringLiteral (" Current Resistance from packet: " ) + QString::number (Resistance.value ()));
1245- } else if (!nordictrack_elliptical_c7_5 && !nordictrack_se7i) {
1303+
1304+ m_watts = se7iWattsFromPacket (newValue);
1305+ emit debug (QStringLiteral (" Current Watt from packet: " ) + QString::number (m_watts));
1306+ if (m_watts > 0 )
1307+ m_watt.setValue (m_watts);
1308+ } else if (!nordictrack_elliptical_c7_5 && !nordictrack_se7i && !nordictrack_elliptical_s700) {
12461309 Resistance = GetResistanceFromPacket (newValue);
12471310 } else if (nordictrack_elliptical_c7_5 && newValue.length () == 20 && newValue.at (0 ) == 0x00 &&
12481311 newValue.at (1 ) == 0x12 && newValue.at (2 ) == 0x01 && newValue.at (3 ) == 0x04 && newValue.at (4 ) == 0x02 &&
@@ -1256,11 +1319,14 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
12561319 p = 100 ;
12571320 m_pelotonResistance = p;
12581321
1259- if (watts ())
1260- KCal += ((((0.048 * ((double )watts ()) + 1.19 ) * weight * 3.5 ) / 200.0 ) /
1261- (60000.0 / ((double )lastRefreshCharacteristicChanged.msecsTo (
1262- QDateTime::currentDateTime ())))); // (( (0.048* Output in watts +1.19) * body weight in
1263- // kg * 3.5) / 200 ) / 60
1322+ {
1323+ double w = ((nordictrack_se7i || nordictrack_elliptical_s700) && m_watts > 0 ) ? (double )m_watts : (double )watts ();
1324+ if (w > 0 )
1325+ KCal += ((((0.048 * w + 1.19 ) * weight * 3.5 ) / 200.0 ) /
1326+ (60000.0 / ((double )lastRefreshCharacteristicChanged.msecsTo (
1327+ QDateTime::currentDateTime ())))); // (( (0.048* Output in watts +1.19) * body weight in
1328+ // kg * 3.5) / 200 ) / 60
1329+ }
12641330 // KCal = (((uint16_t)((uint8_t)newValue.at(15)) << 8) + (uint16_t)((uint8_t) newValue.at(14)));
12651331 Distance += ((Speed.value () / 3600000.0 ) *
12661332 ((double )lastRefreshCharacteristicChanged.msecsTo (QDateTime::currentDateTime ())));
@@ -1285,7 +1351,8 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
12851351 emit debug (QStringLiteral (" Current Resistance: " ) + QString::number (Resistance.value ()));
12861352 emit debug (QStringLiteral (" Current Calculate Distance: " ) + QString::number (Distance.value ()));
12871353 // debug("Current Distance: " + QString::number(distance));
1288- emit debug (QStringLiteral (" Current Watt: " ) + QString::number (watts ()));
1354+ emit debug (QStringLiteral (" Current Watt: " ) + QString::number (
1355+ (nordictrack_se7i || nordictrack_elliptical_s700) && m_watts > 0 ? (double )m_watt.value () : watts ()));
12891356 emit debug (QStringLiteral (" Current Heart: " ) + QString::number (Heart.value ()));
12901357
12911358 if (m_control->error () != QLowEnergyController::NoError) {
@@ -1396,7 +1463,7 @@ void nordictrackelliptical::btinit() {
13961463 QThread::msleep (400 );
13971464 writeCharacteristic (initData9, sizeof (initData9), QStringLiteral (" init" ), false , false );
13981465 QThread::msleep (400 );
1399- if (nordictrack_se7i) {
1466+ if (nordictrack_se7i || nordictrack_elliptical_s700 ) {
14001467 max_resistance = 22 ;
14011468 max_inclination = 20 ;
14021469
0 commit comments