Skip to content

Commit 2410fc7

Browse files
authored
Add NordicTrack Elliptical Spacesaver S700 profile (#4796)
1 parent d06b630 commit 2410fc7

9 files changed

Lines changed: 293 additions & 35 deletions

src/devices/nordictrackelliptical/nordictrackelliptical.cpp

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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

612613
void 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+
898939
double 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

src/devices/nordictrackelliptical/nordictrackelliptical.h

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,55 @@ class nordictrackelliptical : public elliptical {
3939
double bikeResistanceGain);
4040
bool connected() override;
4141
int pelotonToEllipticalResistance(int pelotonResistance) override;
42-
bool inclinationAvailableByHardware() override {
43-
if(nordictrack_elliptical_c7_5 || nordictrack_se7i)
44-
return true;
45-
else
46-
return false;
42+
bool inclinationAvailableByHardware() override {
43+
if(nordictrack_elliptical_c7_5 || nordictrack_se7i || nordictrack_elliptical_s700)
44+
return true;
45+
else
46+
return false;
4747
}
4848

49+
/**
50+
* @brief Detects the NordicTrack Elliptical Spacesaver S700 live telemetry speed packet.
51+
* It reuses the SE7i type 0x01 frame, but shifted by 2 bytes: the 0x5a marker sits at byte[4]
52+
* (with byte[3]==0x00) instead of the SE7i's byte[4]==0x46. Byte[2] carries the stride cadence.
53+
*/
54+
static bool isS700SpeedPacket(const QByteArray &packet);
55+
56+
/**
57+
* @brief Parses the speed (km/h) from a NordicTrack Elliptical Spacesaver S700 telemetry packet,
58+
* as identified by isS700SpeedPacket(). Bytes 12-13 little endian, divided by 100.
59+
*/
60+
static double s700SpeedFromPacket(const QByteArray &packet);
61+
62+
/**
63+
* @brief Parses the cadence (RPM) from a NordicTrack Elliptical Spacesaver S700 telemetry packet,
64+
* as identified by isS700SpeedPacket(). Byte[2] contains the stride rate in RPM.
65+
*/
66+
static uint8_t s700CadenceFromPacket(const QByteArray &packet);
67+
68+
/**
69+
* @brief Detects the SE7i-style (shared with the S700) type 0x00 resistance/inclination packet.
70+
*/
71+
static bool isSe7iResistanceInclinationPacket(const QByteArray &packet);
72+
73+
/**
74+
* @brief Parses the inclination (%) from a SE7i/S700 type 0x00 packet, as identified by
75+
* isSe7iResistanceInclinationPacket(). Bytes 10-11 little endian, divided by 100.
76+
*/
77+
static double se7iInclinationFromPacket(const QByteArray &packet);
78+
79+
/**
80+
* @brief Parses the resistance level from a SE7i/S700 type 0x00 packet, as identified by
81+
* isSe7iResistanceInclinationPacket(). Bytes 12-13 little endian: resistance = (value + 1) / 454.
82+
*/
83+
static double se7iResistanceFromPacket(const QByteArray &packet);
84+
85+
/**
86+
* @brief Parses the machine-reported wattage from a SE7i/S700 type 0x00 packet, as identified
87+
* by isSe7iResistanceInclinationPacket(). Bytes 14-15 little endian, unit is watts.
88+
*/
89+
static uint16_t se7iWattsFromPacket(const QByteArray &packet);
90+
4991
private:
5092
double GetDistanceFromPacket(QByteArray packet);
5193
QTime GetElapsedFromPacket(QByteArray packet);
@@ -85,6 +127,7 @@ class nordictrackelliptical : public elliptical {
85127
bool noHeartService = false;
86128
bool nordictrack_elliptical_c7_5 = false;
87129
bool nordictrack_se7i = false;
130+
bool nordictrack_elliptical_s700 = false;
88131

89132
// SE7i frame-based initialization state management
90133
int se7i_init_state = 0;

src/qzsettings.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,7 @@ const QString QZSettings::proform_treadmill_cst_505_pftl59420_0 = QStringLiteral
11181118
const QString QZSettings::proform_treadmill_105_cst = QStringLiteral("proform_treadmill_105_cst");
11191119
const QString QZSettings::applewatch_as_treadmill_speed = QStringLiteral("applewatch_as_treadmill_speed");
11201120
const QString QZSettings::horizon_treadmill_omega_z = QStringLiteral("horizon_treadmill_omega_z");
1121+
const QString QZSettings::nordictrack_elliptical_s700 = QStringLiteral("nordictrack_elliptical_s700");
11211122

11221123
// Zwift Play/Ride per-button gear mapping
11231124
const QString QZSettings::zwiftplay_gear_ls1 = QStringLiteral("zwiftplay_gear_ls1");
@@ -1279,7 +1280,7 @@ const QString QZSettings::default_shortcut_start_stop = QStringLiteral("");
12791280
const QString QZSettings::shortcut_stop = QStringLiteral("shortcut_stop");
12801281
const QString QZSettings::default_shortcut_stop = QStringLiteral("");
12811282

1282-
const uint32_t allSettingsCount = 1001;
1283+
const uint32_t allSettingsCount = 1002;
12831284

12841285
QVariant allSettings[allSettingsCount][2] = {
12851286
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -2296,6 +2297,7 @@ QVariant allSettings[allSettingsCount][2] = {
22962297
{QZSettings::proform_treadmill_105_cst, QZSettings::default_proform_treadmill_105_cst},
22972298
{QZSettings::applewatch_as_treadmill_speed, QZSettings::default_applewatch_as_treadmill_speed},
22982299
{QZSettings::horizon_treadmill_omega_z, QZSettings::default_horizon_treadmill_omega_z},
2300+
{QZSettings::nordictrack_elliptical_s700, QZSettings::default_nordictrack_elliptical_s700},
22992301
{QZSettings::zwiftplay_gear_ls1, QZSettings::default_zwiftplay_gear_ls1},
23002302
{QZSettings::zwiftplay_gear_ls2, QZSettings::default_zwiftplay_gear_ls2},
23012303
{QZSettings::zwiftplay_gear_rs1, QZSettings::default_zwiftplay_gear_rs1},

src/qzsettings.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3288,6 +3288,12 @@ class QZSettings {
32883288
static constexpr bool default_horizon_treadmill_omega_z = false;
32893289

32903290
/**
3291+
* @brief NordicTrack Elliptical Spacesaver S700: uses its own BLE telemetry/control quirks
3292+
* (speed packet marker and resistance byte mapping differ from the other NordicTrack elliptical
3293+
* profiles), so it is gated behind its own setting to avoid affecting other models.
3294+
*/
3295+
static const QString nordictrack_elliptical_s700;
3296+
static constexpr bool default_nordictrack_elliptical_s700 = false;
32913297
* @brief Per-button gear mapping for Zwift Play/Ride controllers.
32923298
* Values follow MyWhoosh::Action: 0 = Disabled, 1 = Gear Up, 2 = Gear Down.
32933299
*/

src/settings-catalog.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://qdomyos-zwift.local/settings-catalog.schema.json",
33
"schemaVersion": 1,
44
"format": "qdomyos-zwift-settings-catalog",
5-
"settingCount": 965,
5+
"settingCount": 966,
66
"pages": [
77
{
88
"key": "page_custom_gear_table",
@@ -9241,6 +9241,19 @@
92419241
"defaultExpression": "60.0",
92429242
"options": null
92439243
},
9244+
{
9245+
"key": "nordictrack_elliptical_s700",
9246+
"name": "NordicTrack Elliptical Spacesaver S700",
9247+
"description": null,
9248+
"parent": "Proform/Nordictrack Elliptical Options",
9249+
"type": "boolean",
9250+
"qmlType": "bool",
9251+
"control": "switch",
9252+
"visible": true,
9253+
"defaultValue": false,
9254+
"defaultExpression": "false",
9255+
"options": null
9256+
},
92449257
{
92459258
"key": "trainprogram_pid_hr_pushy_zone_limit",
92469259
"name": "PID Pushy Zone Limit",

0 commit comments

Comments
 (0)