Skip to content

Commit c500c2b

Browse files
author
root
committed
feat: add HCI profile for ProForm PFTL59720
1 parent ea0ffc3 commit c500c2b

6 files changed

Lines changed: 188 additions & 10 deletions

File tree

src/devices/proformtreadmill/proformtreadmill.cpp

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,17 @@ void proformtreadmill::forceIncline(double incline) {
7676
write[10] = 0x00;
7777
write[11] = 0x10;
7878
write[12] = (uint8_t)incline;
79+
} else if (proform_carbon_tl_PFTL59720_hci) {
80+
// Native PFTL59720 incline writes use 02 00 <incline>.
81+
write[9] = 0x02;
82+
write[10] = 0x00;
7983
write[14] = write[11] + write[12] + 0x11;
8084
} else if (norditrack_s25i_treadmill) {
8185
write[14] = write[11] + write[12] + 0x11;
8286
} else if (proform_treadmill_8_0 || proform_treadmill_705_cst || proform_treadmill_705_cst_V78_239 || proform_treadmill_9_0 || proform_treadmill_se ||
8387
proform_treadmill_z1300i || proform_treadmill_l6_0s || norditrack_s25_treadmill || proform_8_5_treadmill || nordictrack_treadmill_exp_5i || proform_2000_treadmill ||
8488
proform_treadmill_sport_8_5 || proform_treadmill_505_cst || proform_505_cst_80_44 || proform_carbon_tl || proform_proshox2 || nordictrack_s20i_treadmill || proform_595i_proshox2 ||
85-
proform_treadmill_8_7 || proform_carbon_tl_PFTL59720 || proform_treadmill_sport_70 || proform_treadmill_575i || proform_performance_300i || proform_performance_400i || proform_treadmill_c700 ||
89+
proform_treadmill_8_7 || proform_carbon_tl_PFTL59720 || proform_carbon_tl_PFTL59720_hci || proform_treadmill_sport_70 || proform_treadmill_575i || proform_performance_300i || proform_performance_400i || proform_treadmill_c700 ||
8690
proform_treadmill_c960i || nordictrack_tseries5_treadmill || proform_carbon_tl_PFTL59722c || proform_treadmill_1500_pro || proform_trainer_8_0 || proform_trainer_8_0_pftl59721_int_0 || proform_treadmill_705_cst_V80_44 ||
8791
nordictrack_treadmill_ultra_le || nordictrack_treadmill_commercial_le || proform_treadmill_carbon_tls || proform_treadmill_sport_3_0 || proform_treadmill_995i || nordictrack_series_7 ||
8892
proform_carbon_tlx_treadmill || proform_carbon_tlx_v84_314_treadmill || proform_carbon_tl_PFTL59723_6 || proform_treadmill_cst_505_pftl59420_0
@@ -113,7 +117,7 @@ void proformtreadmill::forceSpeed(double speed) {
113117
} else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_cadence_lt ||
114118
proform_treadmill_z1300i || proform_treadmill_l6_0s || norditrack_s25_treadmill || proform_8_5_treadmill || nordictrack_treadmill_exp_5i || proform_2000_treadmill ||
115119
proform_treadmill_sport_8_5 || proform_treadmill_505_cst || proform_505_cst_80_44 || proform_treadmill_705_cst || proform_treadmill_705_cst_V78_239 || proform_carbon_tl || proform_proshox2 || nordictrack_s20i_treadmill || proform_595i_proshox2 ||
116-
proform_treadmill_8_7 || proform_carbon_tl_PFTL59720 || proform_treadmill_sport_70 || proform_treadmill_575i || proform_performance_300i || proform_performance_400i || proform_treadmill_c700 ||
120+
proform_treadmill_8_7 || proform_carbon_tl_PFTL59720 || proform_carbon_tl_PFTL59720_hci || proform_treadmill_sport_70 || proform_treadmill_575i || proform_performance_300i || proform_performance_400i || proform_treadmill_c700 ||
117121
proform_treadmill_c960i || nordictrack_tseries5_treadmill || proform_carbon_tl_PFTL59722c || proform_treadmill_1500_pro || proform_trainer_8_0 || proform_trainer_8_0_pftl59721_int_0 || proform_treadmill_705_cst_V80_44 ||
118122
nordictrack_treadmill_ultra_le || nordictrack_treadmill_commercial_le || proform_treadmill_carbon_tls || proform_treadmill_sport_3_0 || proform_treadmill_995i || nordictrack_series_7 ||
119123
proform_carbon_tlx_treadmill || proform_carbon_tlx_v84_314_treadmill || proform_carbon_tl_PFTL59723_6 || proform_treadmill_cst_505_pftl59420_0 || proform_treadmill_105_cst) {
@@ -2726,6 +2730,71 @@ void proformtreadmill::update() {
27262730
if (counterPoll > 5) {
27272731
counterPoll = 0;
27282732
}
2733+
} else if (proform_carbon_tl_PFTL59720_hci) {
2734+
// Runtime poll sequence captured from the PFTL59720 I_TL HCI snoop.
2735+
uint8_t noOpData1[] = {0xfe, 0x02, 0x14, 0x03};
2736+
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x10, 0x04, 0x10, 0x02, 0x00,
2737+
0x0a, 0x1b, 0x94, 0x30, 0x00, 0x10, 0x40, 0x50, 0x00, 0x80};
2738+
uint8_t noOpData3[] = {0xff, 0x02, 0x18, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2739+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
2740+
uint8_t noOpData4[] = {0xfe, 0x02, 0x19, 0x03};
2741+
uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x00,
2742+
0x0f, 0x80, 0x02, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
2743+
uint8_t noOpData6[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x91, 0x00, 0x10, 0x90, 0x00,
2744+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
2745+
2746+
switch (counterPoll) {
2747+
case 0:
2748+
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp"));
2749+
break;
2750+
case 1:
2751+
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp"));
2752+
break;
2753+
case 2:
2754+
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp"), false, true);
2755+
if (requestInclination != -100) {
2756+
if (requestInclination < 0)
2757+
requestInclination = 0;
2758+
if (requestInclination != currentInclination().value() && requestInclination >= 0 &&
2759+
requestInclination <= 15) {
2760+
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
2761+
forceIncline(requestInclination);
2762+
}
2763+
requestInclination = -100;
2764+
}
2765+
if (requestSpeed != -1) {
2766+
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) {
2767+
emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));
2768+
forceSpeed(requestSpeed);
2769+
}
2770+
requestSpeed = -1;
2771+
}
2772+
break;
2773+
case 3:
2774+
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"));
2775+
break;
2776+
case 4:
2777+
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp"));
2778+
break;
2779+
case 5:
2780+
writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("noOp"), false, true);
2781+
if (requestStart != -1) {
2782+
emit debug(QStringLiteral("starting..."));
2783+
requestStart = -1;
2784+
emit tapeStarted();
2785+
}
2786+
if (requestStop != -1 || requestPause != -1) {
2787+
forceSpeed(0);
2788+
emit debug(QStringLiteral("stopping..."));
2789+
requestStop = -1;
2790+
requestPause = -1;
2791+
}
2792+
break;
2793+
}
2794+
counterPoll++;
2795+
if (counterPoll > 5) {
2796+
counterPoll = 0;
2797+
}
27292798
} else if (proform_carbon_tl_PFTL59720) {
27302799
uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03};
27312800
uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x00, 0x0f, 0x80, 0x0a, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
@@ -3795,7 +3864,7 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
37953864
((proform_trainer_8_0_pftl59721_int_0) &&
37963865
(newValue.at(4) != 0x02 || (newValue.at(5) != 0x2e && newValue.at(5) != 0x2f))) ||
37973866

3798-
((norditrack_s25i_treadmill || nordictrack_treadmill_ultra_le || nordictrack_treadmill_commercial_le || proform_treadmill_carbon_tls || proform_carbon_tlx_treadmill || proform_carbon_tlx_v84_314_treadmill || proform_carbon_tl_PFTL59723_6) &&
3867+
((norditrack_s25i_treadmill || nordictrack_treadmill_ultra_le || nordictrack_treadmill_commercial_le || proform_treadmill_carbon_tls || proform_carbon_tlx_treadmill || proform_carbon_tlx_v84_314_treadmill || proform_carbon_tl_PFTL59723_6 || proform_carbon_tl_PFTL59720_hci) &&
37993868
(newValue.at(4) != 0x02 || (newValue.at(5) != 0x2f))) ||
38003869

38013870
((nordictrack_t65s_treadmill || nordictrack_t65s_treadmill_81_miles || proform_pro_1000_treadmill || nordictrack_t65s_83_treadmill || nordictrack_s30_treadmill ||
@@ -3813,7 +3882,7 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
38133882
((uint8_t)newValue.at(18)) == 0xFF && ((uint8_t)newValue.at(19)) == 0xFF) ||
38143883

38153884
(((((uint8_t)newValue.at(18)) == 0xFF && ((uint8_t)newValue.at(19)) == 0xFF)) &&
3816-
(proform_proshox2 || proform_595i_proshox2 || proform_treadmill_sport_3_0 || proform_carbon_tlx_treadmill || proform_carbon_tlx_v84_314_treadmill || proform_carbon_tl_PFTL59723_6))
3885+
(proform_proshox2 || proform_595i_proshox2 || proform_treadmill_sport_3_0 || proform_carbon_tlx_treadmill || proform_carbon_tlx_v84_314_treadmill || proform_carbon_tl_PFTL59723_6 || proform_carbon_tl_PFTL59720_hci))
38173886
)
38183887
{
38193888
return;
@@ -3823,7 +3892,7 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
38233892
m_watts = (((uint16_t)((uint8_t)newValue.at(15)) << 8) + (uint16_t)((uint8_t)newValue.at(14)));
38243893

38253894
// for the proform_treadmill_se this field is the distance in meters ;)
3826-
if (m_watts > 3000 && !proform_treadmill_se && !nordictrack_s20i_treadmill && !nordictrack_tseries5_treadmill && !proform_treadmill_sport_3_0 && !proform_carbon_tlx_treadmill && !proform_carbon_tlx_v84_314_treadmill && !proform_carbon_tl_PFTL59723_6) {
3895+
if (m_watts > 3000 && !proform_treadmill_se && !nordictrack_s20i_treadmill && !nordictrack_tseries5_treadmill && !proform_treadmill_sport_3_0 && !proform_carbon_tlx_treadmill && !proform_carbon_tlx_v84_314_treadmill && !proform_carbon_tl_PFTL59723_6 && !proform_carbon_tl_PFTL59720_hci) {
38273896
m_watts = 0;
38283897
} else {
38293898
if (!proform_cadence_lt) {
@@ -3939,6 +4008,7 @@ void proformtreadmill::btinit() {
39394008
proform_595i_proshox2 = settings.value(QZSettings::proform_595i_proshox2, QZSettings::default_proform_595i_proshox2).toBool();
39404009
proform_treadmill_8_7 = settings.value(QZSettings::proform_treadmill_8_7, QZSettings::default_proform_treadmill_8_7).toBool();
39414010
proform_carbon_tl_PFTL59720 = settings.value(QZSettings::proform_carbon_tl_PFTL59720, QZSettings::default_proform_carbon_tl_PFTL59720).toBool();
4011+
proform_carbon_tl_PFTL59720_hci = settings.value(QZSettings::proform_carbon_tl_PFTL59720_hci, QZSettings::default_proform_carbon_tl_PFTL59720_hci).toBool();
39424012
proform_treadmill_sport_70 = settings.value(QZSettings::proform_treadmill_sport_70, QZSettings::default_proform_treadmill_sport_70).toBool();
39434013
proform_treadmill_575i = settings.value(QZSettings::proform_treadmill_575i, QZSettings::default_proform_treadmill_575i).toBool();
39444014
proform_performance_300i = settings.value(QZSettings::proform_performance_300i, QZSettings::default_proform_performance_300i).toBool();
@@ -4984,7 +5054,86 @@ void proformtreadmill::btinit() {
49845054
QThread::msleep(sleepms);
49855055
writeCharacteristic(noOpData12, sizeof(noOpData12), QStringLiteral("init"), false, false);
49865056
QThread::msleep(sleepms);
4987-
} else if (proform_carbon_tl_PFTL59720) {
5057+
} else if (proform_carbon_tl_PFTL59720_hci) {
5058+
// PFTL59720 I_TL init sequence captured from the native HCI snoop.
5059+
// The 0x2c payload is session-specific; this preserves the captured handshake for comparison.
5060+
uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02};
5061+
uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5062+
uint8_t initData3[] = {0xfe, 0x02, 0x08, 0x02};
5063+
uint8_t initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x80, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5064+
uint8_t initData5[] = {0xfe, 0x02, 0x08, 0x02};
5065+
uint8_t initData6[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x88, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5066+
uint8_t initData7[] = {0xfe, 0x02, 0x0b, 0x02};
5067+
uint8_t initData8[] = {0xff, 0x0b, 0x02, 0x04, 0x02, 0x07, 0x02, 0x07, 0x82, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5068+
uint8_t initData9[] = {0xfe, 0x02, 0x0a, 0x02};
5069+
uint8_t initData10[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5070+
uint8_t initData11[] = {0xfe, 0x02, 0x08, 0x02};
5071+
uint8_t initData12[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5072+
uint8_t initData13[] = {0xfe, 0x02, 0x2c, 0x04};
5073+
uint8_t initData14[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x04, 0x28, 0x90, 0x07, 0x01, 0xe2, 0x34, 0x84, 0x4d, 0x22, 0xe8, 0x8d, 0x03, 0xe9};
5074+
uint8_t initData15[] = {0x01, 0x12, 0xfc, 0x5c, 0xba, 0x16, 0x90, 0xf8, 0x46, 0xd2, 0x24, 0xb4, 0x02, 0x9e, 0x18, 0x60, 0xee, 0x6a, 0xec, 0x6c};
5075+
uint8_t initData16[] = {0xff, 0x08, 0xea, 0x66, 0x20, 0x98, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5076+
uint8_t initData17[] = {0xfe, 0x02, 0x19, 0x03};
5077+
uint8_t initData18[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5078+
uint8_t initData19[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5079+
uint8_t initData20[] = {0xfe, 0x02, 0x17, 0x03};
5080+
uint8_t initData21[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5081+
uint8_t initData22[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5082+
uint8_t initData23[] = {0xfe, 0x02, 0x19, 0x03};
5083+
uint8_t initData24[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x00, 0x0f, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x48, 0x00, 0x00, 0xe0};
5084+
uint8_t initData25[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x08, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
5085+
5086+
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
5087+
QThread::msleep(sleepms);
5088+
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false);
5089+
QThread::msleep(sleepms);
5090+
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false);
5091+
QThread::msleep(sleepms);
5092+
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false);
5093+
QThread::msleep(sleepms);
5094+
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false);
5095+
QThread::msleep(sleepms);
5096+
writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false);
5097+
QThread::msleep(sleepms);
5098+
writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false);
5099+
QThread::msleep(sleepms);
5100+
writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false);
5101+
QThread::msleep(sleepms);
5102+
writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false);
5103+
QThread::msleep(sleepms);
5104+
writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false);
5105+
QThread::msleep(sleepms);
5106+
writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false);
5107+
QThread::msleep(sleepms);
5108+
writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false);
5109+
QThread::msleep(sleepms);
5110+
writeCharacteristic(initData13, sizeof(initData13), QStringLiteral("init"), false, false);
5111+
QThread::msleep(sleepms);
5112+
writeCharacteristic(initData14, sizeof(initData14), QStringLiteral("init"), false, false);
5113+
QThread::msleep(sleepms);
5114+
writeCharacteristic(initData15, sizeof(initData15), QStringLiteral("init"), false, false);
5115+
QThread::msleep(sleepms);
5116+
writeCharacteristic(initData16, sizeof(initData16), QStringLiteral("init"), false, false);
5117+
QThread::msleep(sleepms);
5118+
writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init"), false, false);
5119+
QThread::msleep(sleepms);
5120+
writeCharacteristic(initData18, sizeof(initData18), QStringLiteral("init"), false, false);
5121+
QThread::msleep(sleepms);
5122+
writeCharacteristic(initData19, sizeof(initData19), QStringLiteral("init"), false, false);
5123+
QThread::msleep(sleepms);
5124+
writeCharacteristic(initData20, sizeof(initData20), QStringLiteral("init"), false, false);
5125+
QThread::msleep(sleepms);
5126+
writeCharacteristic(initData21, sizeof(initData21), QStringLiteral("init"), false, false);
5127+
QThread::msleep(sleepms);
5128+
writeCharacteristic(initData22, sizeof(initData22), QStringLiteral("init"), false, false);
5129+
QThread::msleep(sleepms);
5130+
writeCharacteristic(initData23, sizeof(initData23), QStringLiteral("init"), false, false);
5131+
QThread::msleep(sleepms);
5132+
writeCharacteristic(initData24, sizeof(initData24), QStringLiteral("init"), false, false);
5133+
QThread::msleep(sleepms);
5134+
writeCharacteristic(initData25, sizeof(initData25), QStringLiteral("init"), false, false);
5135+
QThread::msleep(sleepms);
5136+
} else if (proform_carbon_tl_PFTL59720) {
49885137
uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02};
49895138
uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87,
49905139
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

src/devices/proformtreadmill/proformtreadmill.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class proformtreadmill : public treadmill {
104104
bool proform_treadmill_705_cst_V78_239 = false;
105105
bool nordictrack_treadmill_exp_5i = false;
106106
bool proform_carbon_tl_PFTL59720 = false;
107+
bool proform_carbon_tl_PFTL59720_hci = false;
107108
bool proform_treadmill_sport_70 = false;
108109
bool proform_treadmill_575i = false;
109110
bool proform_performance_300i = false;

0 commit comments

Comments
 (0)