几年前,我在一辆健身车上挥霍无度。这是一个相当昂贵的项目,无论是预付的订阅费还是持续的订阅费都是如此。但我能够证明它是合理的,因为我一直在骑它,这样每次骑一次就不会那么贵了。我很高兴地说,这对我的健康是一个巨大的恩惠,而且每一分钱都是物有所值。
这辆自行车是飞轮运动推出的飞轮家用自行车。你打开应用程序,选择一个班级,然后开始骑马。这款应用程序会向你显示教练的实时视频流,你在排行榜上的位置,以便你可以与班上其他人竞争,以及你的实时统计数据,如功率(瓦数)和节奏(Rpm)。如今,健身应用程序附带了所有常见的励志徽章,每一次骑行都会被记录下来,这样你就可以跟踪自己随着时间的推移而取得的进展。
在与竞争对手佩洛顿(Peloton)打了一场法律战后,飞轮最近突然关闭了Home Bike服务。这辆自行车仍然可以工作,因为你仍然可以踩踏板,调整阻力,从技术上得到锻炼。但这个应用程序不再是这样了,所以没有课程,没有比赛,也没有统计数据。
把它换成免费翻新的佩洛顿。以同样的月费加入佩洛顿,改为参加他们的课程。这不是一笔糟糕的交易,而且可以说是一次升级。
使用免费的LifeFitness ICG训练应用程序。飞轮家庭自行车是一款更名的LifeFitness IC5,所以它恰好可以和这款应用程序一起使用。这为你提供了实时的统计数据,如力量和节奏,以及跟踪你的进度的能力,但不提供任何实时的课程或比赛,也没有得到官方的支持。
添加一组功率计踏板($)。将这款自行车与大型多人在线自行车模拟器Zwift和其他训练应用程序一起使用。
对自行车进行反向工程。将其数据免费设置为与Zwift和其他培训应用程序配合使用。不需要电表踏板。
这篇帖子的其余部分是我编写一些代码的经历的演练,这些代码使飞轮家庭自行车能够与Zwift和其他培训应用程序一起工作。它可能也适用于LifeFitness IC5,而且对其他自行车的支持应该很容易添加。
主要目标是让飞轮家庭自行车与Zwift一起工作。如果它也能与TrainerRoad和Rouvy等其他自行车应用程序配合使用,那就太好了。
该解决方案应该是易于使用的非开发人员和非破坏性的自行车。
编写一些代码以从自行车的专有协议中获取功率和节奏数据。
编写一些代码将功率和节奏数据发送到模拟蓝牙自行车的Zwift。
只需要两条信息就可以让一辆自行车在Zwift上工作:功率(瓦特)和节奏(Rpm)。POWER可以让Zwift计算你在游戏中的速度和位置。Cadence改进了基于Cadence的锻炼体验,并使Zwift能够准确地设置角色动画。
我们从经验中知道,这款自行车能够产生这些信息,而且它使用蓝牙与官方飞轮应用程序进行通信,因此第一步是打开蓝牙服务浏览器,看看有什么可用。
自行车宣传具有两个特征(也称为数据值)的单一服务。对服务UUID的网络搜索告诉我们这是北欧UART服务,这是由北欧半导体定义的定制服务。它允许模拟串行端口来回发送任意数据。
这些特征是从自行车的角度命名的。为了将数据传输到自行车,我们写入Receive(RX)特征。为了从自行车接收数据,我们订阅了传输(TX)特性。
单击(TX)特征上的Subscribe显示自行车已经在发送一些数据。不过,这里的数据有点难看。下面的简短JavaScript程序使用Noble Bluetooth客户端库连接到自行车,并将所有接收到的数据转储到控制台,以便我们可以开始分析它。
/*连接到飞轮家庭自行车的蓝牙UART服务,并将*接收的数据记录到控制台。*/从';@deduconware/noble';;import{on,once}从';events';(async()=>;{//北欧UART服务和特征常量UUID=';6e400001b5a3f393e0a9e50e24dcca9e';const rxUuid=';6e400002b5a3const rxUuid=';6e400002b5a3const UUID=';6e400002b5a3const rxUuid=';6e400002b5a3。//等待适配器常量[STATE]=等待一次(NOBUE,';stateChange';);IF(state!==';poweredOn';){抛出新错误(`蓝牙适配器状态${state}`);}//扫描等待NOVE。StartScaningAsync([UUID],false);const[外围设备]=等待一次(NOBE,#39;DISCOVER&39;);等待NOBUE。StopScaningAsync();//连接等待外设。ConnectAsync();const{特征:[TX,RX]}=等待外围设备。DiscoverSomeServicesAndCharacteristic sAsync([uuid],[txUuid,rxUuid]);//开始接收等待发送。ScribeAsync();const Packets=on(tx,';read';);//在ctrl-c上退出,let exit=false;process。On(';SIGINT';,()=>;{exit=true;});//打印所有接收的数据const start=new date();等待(const[Packet]of Packets){IF(退出)Break;const t=new date()-start;const mm=`${Math.。地板(t/60)}`。PadStart(2,';0';);const ss=`${t%60}`。PadStart(2,';0';);const MMSS=`${mm}:${ss}`;控制台。Log(MMSS,Packet);}//停止接收等待TX。UnscribeAsync();//等待外设断开。DisconnectAsync();})();
运行该程序后,我们第一次清楚地看到了自行车发送的数据:
00:00<;Buffer ff 1f 0C 00 00 00>;Buffer 00 00 00<;Buffer 00 00 00<;Buffer 00 00 00 01 38 55>;Buffer ff 1f 0C 00 00 00>;缓冲区00:01<;Buffer 00 00 00 01 55>;Buffer 00 00 00<;Buffer 00 00 00<;Buffer 00 00 00 01 55>;Buffer ff 1f 0C 00 00 00 26 00 00 00>;00:02<;Buffer 00 00 00 0C 00 00 01 38 55>;00:03<;Buffer ff 1f 0C 00 00 00<;Buffer 00 00 00<;Buffer 00 00 00 0C 00 00 01 38 55<;00:04<;Buffer 00 00 00<;Buffer 00 00 00 0C 00 00 01 38 55>;00:04<;00:04<;缓冲区00 00 00 0C 00 00 00 01 38 55>;
蓝牙LE4.0和4.1允许每个数据包最多20字节的应用程序数据,因此这可能是在两个块中发送的单个34字节的消息。对程序稍作修改,就可以将两个块重新连接在一起,并添加一个标题以使输出更易于阅读和引用。
偏移量0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 3300:23<;缓冲区ff 1f 0C 00 00 00 0C 00 00 01 1e 55>;00:24<;缓冲区ff 1f 0C 00 00 00。缓冲区ff 1f 0C 00 00 00;缓冲区ff 1f 0C 00 00 00 01 1E 55>;00:26<;Buffer ff 1f 0C 00 00 00<;Buffer ff 1f 0C 00 00 00。(开始骑车)00:28<;Buffer ff 1f 0C 00 00 00 31 00 00 00 0d 00 00 00 01 2e 55>;00:29<;Buffer ff 1f 0C 00 00 00 0E 00 00 00 01 2d 55>;00:30<;Buffer ff 1f 0C 00 21 00 14 00 00 00 04 58 00 95 00 00 00:31<;buffer ff 1f 0C 00 21 00 14 00 00 00 04 58 00 95 00 00 00 10 00 00 00 01 fe55>;00:32<;buffer ff 1f 0C 00 21 00 14 00 00 00 04 60 00 95 00 00 00。Buffer ff 1f 0C 00 21 00 14 00 00 00 04 62 00 95 00 00 00:34<;buffer ff 1f 0C 00 21 00 14 00 00 00 04 62 00 95 00 00 00 13 00 00 01 c7 55>;00:35<;buffer ff 1f 0C 00 20 00 14 00 00 00 04 5e 00 93 00 00。缓冲器ff 1f 0C 001f 00 13 00 00 00 04 5e 00 90 00 00 00 15 00 01 00 01 c055>;
记录在静止状态下开始和结束,否则应该大约为60rpm。任何最大偏移量高于80rpm或低于50rpm,或者不是以0开始和结束,都是不匹配的,可以丢弃。我们只剩下两位候选人了:
从图表中可以清楚地看出,在偏移量12处,节奏是uint8。几个不同节奏下的额外测试证实了这一点。在偏移量11和13处有非零值,其确认节奏是uint8而不是uint16的低位字节。
这意味着自行车可以报告的最大节奏为每分钟255转。256转/分时会发生什么情况?换行到0还是钳位到255?事实证明,要骑得那么快是相当困难的,所以这仍然是一个谜。
完全相同的方法在这里也可以奏效:以稳定的已知瓦数骑行30-60秒,然后在数据中寻找该值。然而,我没有一个很好的方法来确切地知道我在做什么瓦数。
所以这一次我们将保持稳定的节奏,每隔几秒就增加一次阻力。数据应该显示出一系列的高原,每个高原都比上一个更高。绝对值也应该在一个合理的范围内,从100瓦左右开始,并保持在1000瓦以下。
这一次我们最有可能寻找的是16位整数,我们必须同时考虑大端和小端编码。
偏移量0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 3300:00<;缓冲区ff 1f 0C 00 00 00 2d 00 00 00 01 b2 00 12 00 25 ba 55>;00:01<;缓冲区ff 1f 0C 00 00 00。Buffer ff 1f 0C 00 00 00 28 00 00 2d 00 00 00<;b3 00 12 00 25 93 55>;00:03<;buffer ff 1f 0C 00 00 00 28 00 2d 00 00 00 01 b4 00 12 00 25 94 55>;00:04<;buffer ff 1f 0C 00 00 00 2d。Buffer ff 1f 0C 00 00 00 25 00 00 2d 00 00 00 01 b6 00 12 00 25 9b 55>;00:06<;buffer ff 1f 0C 00 00 00 29 00 00 2d 00 00 00 01 b7 00 12 00 25 96 55>;00:07<;buffer ff 1f 0C 00 00 56 00 35 00 00。缓冲器ff 1f 0C 005a 00 38 00 00 00 0C 35 00 f5 2d 00 00 00 01 b9 00 12 00 26 1c 55>;
任何最大偏移量小于100W或大于1000W,或者开头和结尾不是0的都可以丢弃。这一次,我们只剩下三种可能:偏移量3、5和13。
绝对值表明偏移3是以瓦为单位的当前功率。根据我的努力,偏移量13开始太高,而偏移量5没有达到足够高的峰值。
在对ICG训练应用程序进行了一些试验后,我发现这款自行车有一些飞轮应用程序没有暴露出来的隐藏功能。其中一个特点是,你可以进行测试,看看你在一个小时的骑行中可以承受的最大功率,这就是所谓的功能阈值功率(FTP)。FTP结果存储在自行车上,它以瓦数(偏移量3)和FTP百分比(偏移量5)报告您的功率。FTP%经常用于培训计划。当偏移量为3≈160时,自行车中存储的默认ftp显示为160W,偏移量为5≈100时。
偏移量13处的值是一个非常乐观的速度估计值,单位为km/h×10。可能是骑手体重默认为0。
另外,当我记录数据时,我注意到偏移量15是电阻刻度盘的位置,范围从0(容易)到100(硬)。这在程序的输出中很容易看到,因为它是唯一改变的值之一,所以在不踩踏板的情况下调整阻力时很容易看到这一点。这并不是让自行车与Zwift一起工作所必需的。
我在这次测试中注意到的另一件事是自行车上似乎有一个错误:
偏移量0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 02:21<;buffer ff 1f 0c 01 7b 00 ea 00 00 00 33 6c 01 f6 3a 00 00 00 7e 00 06 00 0E 67 55>;02:22<;buffer ff 1f 0c 01 92 00 f8 00 00 00 36 6b 02 05 3b 00。Buffer ff 1f 0C 01 96 00 fb 00 00 00 36 6b 02 08 3b 00 00 00 0f 9b 55>;02:24<;buffer ff 1f 0C 00 00 00 6b 00 00 00 81 00 07 00 0f F1 55>;02:25<;buffer ff 1f 0C 01 a2 01 02 00 00 00 38 6fF1 55>;02:25<;buffer ff 1f0C 00 00 00 6b;Buffer ff 1f 0C 01 a2 01 02 00 00 00 38 6f。Buffer ff 1f 0c 01 97 00 fb 00 00 00 36 6c 02 08 3b 00 00 00 83 00 07 00 10 81 55>;|电源节拍电阻。
2点24分的数据显示了一个短暂的信号,功率、电阻和其他一些值短暂下降到0,而节奏没有受到影响。然后在2点25分他们回来了。事实证明,在整个数据中都有几个这样的例子。
这意味着你可能正以非常高的努力骑车,突然之间你的力量在一秒钟内降到了0。这不是世界末日,但它有可能令人讨厌。似乎每隔几分钟就会发生一次。
飞轮没有公布自行车内部如何工作的任何细节,但原始制造商确实暗示了功率是如何计算的。从LifeFitness IC5页面:
WattRate®功率表以瓦为单位显示用户工作的精确测量。这种精度是通过测量施加到磁制动系统上的电阻的定位传感器来实现的。
因此,自行车实际上并不像电力表那样测量功率。取而代之的是,它将磁制动器的位置映射到某些工厂校准的曲线或查找表上的点。
一种可能的解释是定位传感器有硬件问题,或者更有可能是固件错误导致偶尔错误读取0。功率值是从电阻读数中得出的,因此它最终也是0。
一个简单的解决办法是,如果有的话,只使用以前的功率值:节奏非零,前一次功率非零,当前功率为零。一个轻微的改进是跟踪坡度,并在计算预测值时将其考虑在内。如果错误发生在快速加速或减速期间,应该会产生稍微好一点的结果。
偏移量0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 02:23<;Buffer ff 1f 0C 01 96 00 fb 00 00 00 36 6b 02 08 3b 00 00 00 07 00 0f 9b 55>;|power|cadence|阻力功率%速度。
我们已经有了我们需要的东西,但是我们可以对包的其余部分做出一些有根据的猜测。
名称“北欧UART服务”暗示该协议旨在用于真正的UART,如果是这样的话,第一个和最后几个字节将用于串行帧同步和错误检测。
对我们程序输出的最后一个小更改是,对于飞轮应用程序的“搭便车”功能,我们提供了一个非常原始的替代功能。
电源节拍电阻23W 63rpm 0%24W 73rpm 0%26W 73rpm 0%27W 82rpm 0%29W 82rpm 0%30W 89rpm 0%32W 89rpm 0%33W 99rpm 0%35W。
.