← 文章 / 编程开发
Hacker News 8小时前 · 2026-09-13 23:47:00 · 15 阅读

逆向我的电动滑板车,用 Rust 重写固件

我对自己那辆 Egret GT 电动滑板车的硬件和固件做了逆向工程。本文讲述我是如何入手的、如何分析各组件之间的通信、如何逆向固件,以及如何为显示屏单元编写自定义固件。

去年我给自己买了一辆 Egret GT。这款电动滑板车号称续航 100 公里,轮胎很大,骑起来相当舒服。为了彰显它的高端定位,它还配了一块 320x480 的 LCD 显示屏作为 HUD,用来显示速度、骑行模式、电量和剩余续航。

我这人见什么都想拆开研究一番,所以最终决定搞清楚这辆车的工作原理。具体起因我记不清了,可能是有一天发现按住面板上的“下”键再开机,滑板车会进入固件升级模式——这时只要点一下按钮退出菜单,就能进入正常骑行模式,不用输 PIN 码就能骑车。虽然我平时都用一把还不错的锁把车锁好,但这件事还是让我有点不舒服。

我从手机 App 入手。这个 App 可以远程解锁滑板车、修改一些设置、查看电量。细节过程就不赘述了,总之翻完 App 里的蓝牙处理代码后,我发现了这些:

  1. 滑板车支持通过蓝牙升级固件,而且固件升级似乎可以针对好几个部件(显示屏、控制器、按键面板)。
  2. 一些在 App 和滑板车上都没显示的指标也会通过蓝牙传输,比如各骑行模式的使用时长、设备温度、电机电流、电池电压、充电历史。总骑行时长、里程表、充电历史等信息会被发送到厂商,并和滑板车 ID 绑定存储——这一点在 App 里可没有明说 :)))))))
  3. 滑板车本身并不知道自己的 VIN(车辆识别码),要等 App 连接后才会写入。如果用蓝牙调试工具自己写入 VIN,就可以骗过 Egret 官方 App,让它以为滑板车是另一个型号。我试着伪装成 45km/h 版本的 VIN,想看看限速是不是就靠这么简单的一个判断实现,结果没成功。

玩腻了蓝牙接口后,我把目光转向了显示屏上的 USB-C 端口。厂家声称该端口仅用于给手机充电。我试用了几台不同设备,发现只要数据引脚连接上,显示屏模块就既不会充当 USB 主机也不会充当 USB 设备。但我不这么认为,于是订了一块 USB-C 破板(breakout board)。东西到货后,我插上设备,用示波器逐一探测每个引脚。让我意外的是,两个 USB-C 引脚竟然被用作 CAN 总线(这种设计极其不合规)。

CAN 总线嗅探

图 1:示波器连接在滑板车的 CAN 总线上,正在解码消息。

为了嗅探这些 CAN 流量,我用一块 ESP32-C6、一片 SN65HVD230 和一个 MCP2515^0,快速拼凑了一个略显怪异的装置(如下图 2 所示)。

图 2:该装置

我写了一个简单的程序来初始化 CAN 外设并记录所有 CAN 消息。接着,我把 CAN 记录器插到滑板车上,并录下了启动过程中的消息:

复制代码 复制
1CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]2CAN_FRAME:1025,false,[74, bd, 0, 0, 16, c, 0, 0]3CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]4CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]5CAN_FRAME:1025,false,[74, bd, 0, 0, 16, c, 0, 0]6CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]7CAN_FRAME:513,false,[0, 0, 0, 0, 0]8CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]9CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]10CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]11CAN_FRAME:1025,false,[73, bd, 0, 0, 3, c, 0, 0]12CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]13CAN_FRAME:513,false,[0, 0, 0, 0, 0]14CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]15CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]16CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]17CAN_FRAME:1025,false,[73, bd, 0, 0, 3, c, 0, 0]18CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]19CAN_FRAME:513,false,[0, 0, 0, 0, 0]20CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]21CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]22CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]23CAN_FRAME:1025,false,[72, bd, 0, 0, ef, b, 0, 0]24CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]25CAN_FRAME:513,false,[0, 0, 0, 0, 0]26CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]27CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]28CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]29CAN_FRAME:1025,false,[72, bd, 0, 0, ef, b, 0, 0]30CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]31CAN_FRAME:513,false,[0, 0, 0, 0, 0]32CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]33CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]34CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]35CAN_FRAME:1025,false,[74, bd, 0, 0, e9, b, 0, 0]36CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]37CAN_FRAME:513,false,[0, 0, 0, 0, 0]38CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]39CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]40CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]41CAN_FRAME:1025,false,[74, bd, 0, 0, e9, b, 0, 0]42CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]43CAN_FRAME:774,false,[55, 0, 0, 0, 2, 0, 0, 0]44CAN_FRAME:513,false,[0, 0, 0, 0, 0]45CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]46CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]47CAN_FRAME:768,false,[0, 5a, 64, 5a, 64, 0, 0, 0]48CAN_FRAME:494,false,[60, 0, 0, 0, 0, 0, 0, 0]49CAN_FRAME:495,false,[4c, 44, 2e, 43, 52, 2e, 53, 38]50CAN_FRAME:495,false,[30, 37, 2e, 43, 2e, 32, 2e, 31]51CAN_FRAME:495,false,[45, 47, 2e, 32, 2e, 32, 2e, 31]52CAN_FRAME:495,false,[31, 0, 0, 0, 0, 0, 0, 0]53CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]54CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]55CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]56CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]57CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]58CAN_FRAME:768,false,[0, 5a, 64, 5a, 64, 0, 0, 0]59CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]60CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]61CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]62CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]63CAN_FRAME:1856,true,[4b, 0]64CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]65CAN_FRAME:1025,false,[74, bd, 0, 0, e4, b, 0, 0]66CAN_FRAME:1857,true,[4b, 0, 0, 0, 69, 99, 52, 42]67CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]68CAN_FRAME:1860,true,[1]69CAN_FRAME:1861,true,[1, 15, 57, 20, 50, 59, 54, 34]70CAN_FRAME:774,false,[54, 0, 0, 0, 2, 0, 0, 0]71CAN_FRAME:513,false,[0, 0, 0, 0, 2]72CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, ff, 1f]73CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]74CAN_FRAME:768,false,[0, 5a, 64, 5a, 64, 0, 0, 0]
Copy codeCopyCopied!
1CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
2CAN_FRAME:1025,false,[74, bd, 0, 0, 16, c, 0, 0]
3CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
4CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
5CAN_FRAME:1025,false,[74, bd, 0, 0, 16, c, 0, 0]
6CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
7CAN_FRAME:513,false,[0, 0, 0, 0, 0]
8CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]
9CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]
10CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
11CAN_FRAME:1025,false,[73, bd, 0, 0, 3, c, 0, 0]
12CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
13CAN_FRAME:513,false,[0, 0, 0, 0, 0]
14CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]
15CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]
16CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
17CAN_FRAME:1025,false,[73, bd, 0, 0, 3, c, 0, 0]
18CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
19CAN_FRAME:513,false,[0, 0, 0, 0, 0]
20CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]
21CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]
22CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
23CAN_FRAME:1025,false,[72, bd, 0, 0, ef, b, 0, 0]
24CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
25CAN_FRAME:513,false,[0, 0, 0, 0, 0]
26CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]
27CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]
28CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
29CAN_FRAME:1025,false,[72, bd, 0, 0, ef, b, 0, 0]
30CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
31CAN_FRAME:513,false,[0, 0, 0, 0, 0]
32CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]
33CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]
34CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
35CAN_FRAME:1025,false,[74, bd, 0, 0, e9, b, 0, 0]
36CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
37CAN_FRAME:513,false,[0, 0, 0, 0, 0]
38CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]
39CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]
40CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
41CAN_FRAME:1025,false,[74, bd, 0, 0, e9, b, 0, 0]
42CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
43CAN_FRAME:774,false,[55, 0, 0, 0, 2, 0, 0, 0]
44CAN_FRAME:513,false,[0, 0, 0, 0, 0]
45CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, 21, 0]
46CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]
47CAN_FRAME:768,false,[0, 5a, 64, 5a, 64, 0, 0, 0]
48CAN_FRAME:494,false,[60, 0, 0, 0, 0, 0, 0, 0]
49CAN_FRAME:495,false,[4c, 44, 2e, 43, 52, 2e, 53, 38]
50CAN_FRAME:495,false,[30, 37, 2e, 43, 2e, 32, 2e, 31]
51CAN_FRAME:495,false,[45, 47, 2e, 32, 2e, 32, 2e, 31]
52CAN_FRAME:495,false,[31, 0, 0, 0, 0, 0, 0, 0]
53CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
54CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
55CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
56CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
57CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
58CAN_FRAME:768,false,[0, 5a, 64, 5a, 64, 0, 0, 0]
59CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
60CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
61CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
62CAN_FRAME:495,false,[0, 0, 0, 0, 0, 0, 0, 0]
63CAN_FRAME:1856,true,[4b, 0]
64CAN_FRAME:1024,false,[0, 40, 87, 4, 0, 0, 0, 0]
65CAN_FRAME:1025,false,[74, bd, 0, 0, e4, b, 0, 0]
66CAN_FRAME:1857,true,[4b, 0, 0, 0, 69, 99, 52, 42]
67CAN_FRAME:1028,false,[20, 4e, 0, 0, 1, 0, b9, b]
68CAN_FRAME:1860,true,[1]
69CAN_FRAME:1861,true,[1, 15, 57, 20, 50, 59, 54, 34]
70CAN_FRAME:774,false,[54, 0, 0, 0, 2, 0, 0, 0]
71CAN_FRAME:513,false,[0, 0, 0, 0, 2]
72CAN_FRAME:515,false,[0, 0, 0, 0, 0, 0, ff, 1f]
73CAN_FRAME:528,false,[0, 0, 0, 0, 0, 0, 0, 0]
74CAN_FRAME:768,false,[0, 5a, 64, 5a, 64, 0, 0, 0]
CAN 总线噪声相当大。为了弄清楚到底发生了什么,我基于 egui 开发了一个小工具,用来绘制 CAN 消息随时间变化的曲线图。把每条 CAN 消息画成一个点,纵轴设为消息 ID,就能很容易地分辨出哪些是命令、响应,哪些是周期性数据。

遗憾的是,当时我仍然不清楚每条消息的具体用途。但通过在实际骑行时嗅探总线,我很快摸清了哪些消息负责传递油门、驾驶模式和电机速度:

  • 0x300:由显示屏发送至控制器。包含当前驾驶模式(步行、经济、驾驶、运动)、大灯开关状态;在步行模式下,最低半字节包含一个计数器。如果将消息的第四个字节设为 a5 而非常见的 5a,会触发控制器重置。

    示例 解码后为:

    驾驶模式步行(0x00_90
    大灯开启(0x64
    步行计数器0
  • 0x306:由显示屏发送至控制器。包含油门位置、转向灯状态以及电动车速限制。对于标准 GT 控制器,速度限制字段无效;但在 GTS 上,它会将限速设为 25、35 或 45 km/h。不知出于何种原因,油门值被传输为 9 位无符号整数,其中最高位(MSB)是第二个字节的最低有效位。

    示例 解码后为:

    油门511
    左转向灯true
    右转向灯false
    速度限制25 km/h(0
  • 0x201:包含电机转速及若干状态标志。

    示例 解码后为:

    电机转速1031
    步行模式false
    大灯开启false
    刹车灯开启true

最终,我将所有 CAN 消息整理归档: 此处

到这里,我已经能干些好玩的事了,比如远程控制滑板车的电机,但这既不实用也没什么意思。这个项目一度搁浅,因为我拿不到固件,能做的也就到此为止了。几个月后,我发现网上可以买到电机控制器和显示屏的替换件,实在忍不住,就把两样都下单了。

拆解与固件提取

我拆的第一个部件是控制器。这一步格外费劲:后盖用十字螺丝拧得死紧,其中两颗螺丝头直接滑丝了,只好用电磨开了一条槽。设备内部还灌了封装胶,不过万幸这胶其实挺软,很容易刮掉。

清掉封装胶后,眼前的景象可谓惊喜:所有有源元件的丝印都完好无损,而且电路板背面还有一排四个焊盘。MCU 上印着 APM32E103xCxE(STM32F103 的克隆版),所以这些引脚大概率就是 SWD 调试口。借助 OpenOCD^1,我成功在启动后不久转储了 flash 和 RAM^2 的内容。

拿到固件后,我就可以用 Ghidra^3 开始分析了。很快我就找到了 CAN 消息的主处理函数,借此进一步梳理出了每条 CAN 消息的用途。

图 3:反编译结果,展示了 0x300 和 0x306 消息的处理逻辑

我还发现,控制器 MCU 上总共运行着三个应用程序:位于 0x8000000 的 Bootloader、位于 0x8003000 的“更新器”,以及位于 0x8006200 的主程序。Bootloader 负责初始化 CAN 总线并短暂监听,以检查是否有“更新”数据包到达,从而判断是否正在进行固件升级。不知为何,Bootloader 和“更新器”固件都包含通过 CAN 总线升级应用程序固件的机制,但两者采用的升级方案却各不相同。

图 4:Ghidra 中打开控制器“bootload”函数的界面。可以通过其行为识别该函数:它将复位函数的地址(image[1])写入 RAM 起始位置(0x20000000),设置栈指针(image[0]),然后跳转至复位函数。复位函数将负责配置 NVIC。

图 5:应用程序映像。前两个字分别是初始栈指针地址和复位函数,随后是中断处理程序地址。可以看出内容较为重复,这有助于快速识别。

另一个有趣的细节是,在 0x8006000 处存储了应用程序固件的长度,但它并非像通常检查那样使用 4 字节或 8 字节的无符号整数,而是存储为该数字十进制表示的 ASCII 字符串。更奇怪的是,从长度字段之后到 0x80061ff 的整个区域都填充了 ASCII 空格字符,并以 \r\n 结尾。

图 6:主程序启动前的内存内容。

在进一步探索了一段时间后,我决定将注意力转向显示单元。控制器中的大部分代码似乎是 FOC 电机控制代码,我不太愿意修改这种涉及设备安全的关键部分,尤其是在发现控制器包含了一些相当合理的安全预防措施之后,例如,如果在短时间内未收到有效的油门位置信号,系统将会关机。

显示单元

拆开显示屏单元的难点远大于控制器。其外壳由厚度达 2mm 的高强度注塑材料制成,我用万用旋转工具从背面切口入手。原本以为前部屏幕盖板是热熔焊接的,于是也用万用旋转工具在边缘尝试,但在切出一个缝隙获得杠杆点后,发现盖板仅仅是胶粘的,直接撬开即可。

图 7:显示屏单元顶部,此处使用 Glasgow 作为调试器

显示屏主板结构颇具看点,板上排列着多组未使用的通孔排针和多个微控制器。我识别出以下芯片:

  1. 主 MCU:AT32F415
  2. 蓝牙 MCU:CH573
  3. NFC 读卡器 IC:FM17520
  4. CAN 收发器
  5. SPI 闪存芯片:W25Q128FV

其中一个调试排针是主 MCU 的 SWD 接口,因此我沿用了之前的方法提取固件。另一个排针提供 SPI 闪存访问,我也将其转储,但内容仅为显示屏 GUI 使用的位图图像。

显示屏固件的结构与控制单元类似,包含一个可通过 CAN 总线接收固件更新的引导程序。

  1. 显示屏固件由引导程序和位于 0x8008000 的主应用程序组成。
  2. GUI 使用 SEGGER EMWin 绘制。
  3. 蓝牙 MCU 通过 GPIOA 2 和 3 以 57500k 的 UART 速率通信,采用简单的帧结构。当读取蓝牙属性时,CH573 发送一个带有处理器索引的请求消息,该索引对应主 MCU 固件中的处理函数。主 MCU 随后回复带有相同命令编号和响应主体的消息。
  4. NFC 模块同样通过 UART 通信,波特率为 115200k,协议略有不同。我没有进一步深入研究。
  5. 车把上的按钮面板也通过 UART 与显示屏单元通信,波特率为 9600k。它仅发送简单消息,内容为被按下按钮的位域。有趣的是,面板自行处理指示灯的闪烁;它驱动灯光闪烁,并在消息中包含两位状态位指示闪烁器状态。它似乎还支持接收固件更新。
  6. CAN 总线连接在 GPIOA 11 和 12 引脚上。
  7. 屏幕采用 ST7796 控制器,通过并行接口连接;GPIOB 的全部 16 个引脚都用作并行数据总线,这样固件只需一条指令就能同时更新所有引脚的状态。
  8. ADC 读取三个通道:ch12 上的环境光传感器、ch13 上的转把电压、ch15 上的电池电压。固件只在电池电压过低时读取它来触发错误提示,其他用到电量信息的地方,读取的都是一个由 CAN 消息更新的变量——这条消息由电池自己发送到总线上。(没错,电池也在总线上。)
  9. 显示屏单元的固件和控制器一样,也是通过 CAN 更新的。而且更新代码同样放在 bootloader 里:应用固件收到更新启动消息后直接重启自己,bootloader 接着收到下一条消息并启动更新流程。是的,这也意味着任何人都可以在没有认证的情况下刷写任意一辆滑板车的固件 :)))))

显示屏固件起初逆向得很费劲:我用的 Ghidra 版本有个 bug,当函数指针的低位被置位(表示该函数使用 THUMB 指令)时,无法正确标记那些位于数据段中的函数指针。而这个固件是围绕各种回调表组织起来的——CAN、蓝牙、GUI 界面各有自己的表——因此很多函数的调用者我根本找不到。后来运气不错,我碰巧遇到了扫描 CAN 处理器表的函数,从而搞清楚了这张表的结构。表中每一项都指定了要匹配的消息 ID,还可以带一个时间间隔和 tx/rx 回调,于是之后每观察到一个 CAN 消息,我都能迅速定位到对应的处理代码。

图 8:Ghidra 中显示屏单元发送 0x300 CAN 消息的函数

图 9:定义在 0x200001a0 处的 CAN 处理器表

图 10:CAN 消息 0x306 的条目,包含发送回调函数和指定间隔

通过大量交叉比对显示屏和控制器固件,我基本完全掌握了 CAN 消息的结构。唯一没搞完的是几条与 Apple “查找” 功能相关的消息,但我对这部分没太大兴趣,因为我不使用 iPhone。相反,我基于 OpenHaystack 自建了一个追踪设备,它每 30 分钟轮换一次身份,附带的好处是不会触发“追踪器跟随”类消息 :)

接下来的任务是搞清楚 GPIO 和周边设备的配置,这样才能开始编写自己的固件。幸运的是,这部分其实很简单。固件使用的是厂商提供的周边设备库,且编译时未使用任何形式的 LTO(链接时优化),因此反编译出的 HAL 编译后函数与源代码高度吻合。

图 11:GPIO_Init 函数的反编译结果,与源代码几乎完全一致

图 12:初始化 UART5 的函数反编译结果。通过 GPIO_Pins 字段的内容,可以看出哪些引脚被静态配置为 TX 和 RX,以及 UART 周边设备的配置。出于某种原因,波特率被作为参数传入。

通过将反编译的库函数与源代码进行匹配,并利用由此获得的名字和类型信息来发现周边设备配置,我成功绘制了所有 GPIO 引脚及所有周边设备配置的完整映射。

有助于我逆向工程的另一个因素是,固件中遗留了一个调试菜单(它似乎无法从实际固件入口到达,但代码依然存在于二进制文件中)。该调试菜单显示一些按钮和大灯的状态,因此我能立刻补全“按钮状态”枚举类型。

图 13:调试菜单的反编译结果

到这一步,我已经收集到了开始编写自有固件所需的大多数信息。操作电机控制器所需的 CAN 消息、GPIO 引脚及外设配置已全部梳理清楚,蓝牙 MCU 的 UART 协议也完成了逆向。我还绘制了一张系统框图,展示了电动滑板车各组件间的通信关系:

在拆开的显示单元上运行自有固件很简单,用调试探针烧录即可。但要在一个可用的显示单元上运行我的固件,还得先逆向固件更新流程。

固件更新

幸运的是,固件更新过程极其简单,不涉及加密算法,整个更新生命周期基本都在 bootloader 的一个函数内完成。

固件更新始于 ID 为 0x384 的 CAN 消息处理器。如果收到空消息,固件将复位;如果收到特定指令,滑板车会擦除存储 VIN 和滑板车配置的闪存区域。

图 14:Ghidra 中显示单元处理 ID 为 0x384 的 CAN 消息的函数

图 15:固件更新循环的核心逻辑。在通过 CRC 校验后,bootloader 直接将数据写入闪存。

执行更新流程的设备会持续发送 消息,直到 bootloader 启动、检测到更新初始化消息并回复 为止。随后,更新设备将 64 字节的数据块分散在 9 个 CAN 0x384 帧中发送,每个帧的结构如下:

  • 帧 0
  • 帧 1..9
  • 帧 9

CRC 采用 CRC-16-CCITT 算法计算 data。在计算 CRC 之前,每个数据块会用零填充至 64 字节。sequence 是一个无符号字节,从 0 开始,每发送一个块递增一次,超过 0xFF 后回绕为 0。

第一个数据块并非固件的前 64 字节,而是以文件名(例如:`AT_R2_JHZY_GT1_GE_FM_HW02_4.0.2`)作为以 null 结尾的字符串,随后跟固件长度(以十进制编码、同样以 null 结尾的字符串)。Bootloader 会对第一个数据块回复四次,对之后所有数据块则各回复一次。 第一个数据块发送完毕后,Updater 设备会逐块发送固件镜像。当某帧的最后一帧 CAN 消息发送且 CRC 校验通过后,滑板车回复一条消息。固件传输完成后,Updater 发送一条消息,触发显示单元重启。该更新机制直接在 Flash 上覆盖应用镜像,因此若更新失败会导致显示单元变砖。不过,Bootloader 在启动时始终检查特定数据包的存在,使得即使应用代码失效,也能启动固件更新流程。 综上所述,更新流程遵循下图所示的时序(可以看出我在 Typst 上花了不少心思 :)): Figure 16: 更新流程时序图 为了实际执行固件更新,我将之前编写的 CAN 转储固件扩展成了[该工具](https://github.com/simmsb/egret-can-flasher),它可以直接刷写内嵌的固件镜像。 现在我可以更新设备上的固件了。为了验证是否成功,我尝试使用最初从拆开设备中 dump 出来的固件镜像进行测试,一次就成功了。 用 Rust 重写 此时我本可以开始用 Rust 编写固件,但存在一个小问题:显示单元的 MCU 是 AT32F415,它是 STM 的克隆,但似乎并非特定 STM 芯片的克隆,而是 STM32 外设的大杂烩。大多数外设与 STM32F1 匹配,但 RTC 似乎来自 STM32F3。这很麻烦,意味着我不能直接用 [Embassy](https://embassy.dev/) 快速启动固件开发,而需要先构建自己的 HAL[^4]。

Kossnikita 已经基于 stm32-rs 的一个 fork 做了一些前期工作,所以我得以在此基础上继续补充我所需外设的支持。不得不承认,这部分我大部分是偷懒完成的:大多数外设我先直接借用 Embassy 的实现,然后对照着 stm32f1 和 at32f415 两份 datasheet,把外设代码更新为 AT32 所用的寄存器名称。其实很可能有更好的做法,比如把这个芯片作为新条目加入 stm32-metapac——它是 Embassy 的一个子项目,通过处理 SVD 文件来生成 PAC^5 crate——但起初我高估了 AT32 与 STM32 的差异程度。

我是逐个外设推进的:先是时钟和定时器,因为有了定时器才能实现 embassy-time-driver。接着是 ADC、GPIO 外部中断、UART、CAN 和 RTC。HAL 驱动写好后,就可以开始编写驱动显示屏、读取 ADC 输入、以及通过 CAN 和 UART 总线通信的代码了。

点亮显示屏非常顺利,用了 mipidsi 这个显示驱动 crate,我唯一需要自己做的是在 HAL 中实现一个 ParallelInterface,它能通过一次操作把 u16 写到所有 GPIO 引脚上:

rust 代码复制
1/// gpio 引脚的总线
2///
3/// SHIFT: 指定操作哪个范围的引脚:0 => 0..16, 8 => 8..16
4/// MASK: 位掩码,用于选择哪些引脚属于此总线。掩码未进行移位。
5pub struct Bus<const P: char, const SHIFT: u8, const MASK: u16, MODE = DefaultMode> {
6    _mode: PhantomData<MODE>,
7}
8
9impl<const P: char, const SHIFT: u8, const MASK: u16, MODE> Bus<P, SHIFT, MASK, MODE> {
10    fn _set_state(&mut self, state: u16) {
11        unsafe {
12            (*Gpio::<P>::ptr()).odt().modify(|r, w| {
13                // 只有当掩码未覆盖所有位时,才需要读取之前的状态。
14                let prev = if const { MASK & 0xFFFF != 0xFFFF } {
15                    r.bits() & !(MASK as u32)
16                } else {
17                    0
18                };
19                let new = ((state << SHIFT) & MASK) as u32;
20                w.bits(prev | new)
21            });
22        }
23    }
24
25    fn _get_state(&self) -> u16 {
26        unsafe {
27            let unshifted = (*Gpio::<P>::ptr()).odt().read().bits() & !(MASK as u32);
28            (unshifted >> SHIFT) as u16
29        }
30    }
31}
32
33impl<const P: char, const SHIFT: u8, const MASK: u16> mipidsi::interface::OutputBus
34    for Bus<P, SHIFT, MASK, Output>
35{
36    type Word = u16;
37
38    const KIND: mipidsi::interface::InterfaceKind = InterfaceKind::Parallel16Bit;
39
40    type Error = Infallible;
41
42    #[inline(always)]
43    fn set_value(&mut self, value: Self::Word) -> Result<(), Self::Error> {
44        self.set_state(value);
45        Ok(())
46    }
47}
rustCopy codeCopyCopied!
1/// GPIO 引脚总线2///3/// SHIFT:操作引脚的范围偏移:0 => 0..16,8 => 8..164/// MASK:位掩码,用于选择哪些引脚属于该总线。掩码未移位。5pub struct Bus<const P: char, const SHIFT: u8, const MASK: u16, MODE = DefaultMode> {6    _mode: PhantomData<MODE>,7}89impl<const P: char, const SHIFT: u8, const MASK: u16, MODE> Bus<P, SHIFT, MASK, MODE> {10    fn _set_state(&mut self, state: u16) {11        unsafe {12            (*Gpio::<P>::ptr()).odt().modify(|r, w| {13                // 仅当掩码未覆盖全部位时,才需读取前一状态14                // (即掩码不等于 0xFFFF)15                let prev = if const { MASK & 0xFFFF != 0xFFFF } {16                    r.bits() & !(MASK as u32)17                } else {18                    019                };20                let new = ((state << SHIFT) & MASK) as u32;21                w.bits(prev | new)22            });23        }24    }2526    fn _get_state(&self) -> u16 {27        unsafe {28            let unshifted = (*Gpio::<P>::ptr()).odt().read().bits() & !(MASK as u32);29            (unshifted >> SHIFT) as u1630        }31    }32}3334impl<const P: char, const SHIFT: u8, const MASK: u16> mipidsi::interface::OutputBus35    for Bus<P, SHIFT, MASK, Output>36{37    type Word = u16;3839    const KIND: mipidsi::interface::InterfaceKind = InterfaceKind::Parallel16Bit;4041    type Error = Infallible;4243    #[inline(always)]44    fn set_value(&mut self, value: Self::Word) -> Result<(), Self::Error> {45        self.set_state(value);46        Ok(())47    }48}

接下来,用 Rust 类型声明显示屏用到的引脚:

rust 复制代码 已复制!
1pub type Bus = at32f4xx_hal::gpio::Bus<'B', 0, 0xFFFF, Output>;2pub type CsPin = Pin<'C', 13, Output>;3pub type DcPin = Pin<'C', 14, Output>;4pub type RdPin = Pin<'C', 0, Output>;5pub type WrPin = Pin<'C', 15, Output>;6pub type RstPin = Pin<'C', 1, Output>;7pub type Backlight = PwmChannel<at32f4xx_hal::pac::TMR2, 0>;8pub type InnerDisplay = mipidsi::Display<9    mipidsi::interface::ParallelInterface<Bus>,10    mipidsi::models::ST7796,11    RstPin,12>;1314pub fn init(15    mut rd: RdPin,16    mut cs: CsPin,17    dc: DcPin,18    wr: WrPin,19    rst: RstPin,20    bus: Bus,21    delay: &mut SysDelay,22    backlight: Backlight,23) -> Display {24    cs.set_low();25    rd.set_high();2627    let interface = mipidsi::interface::ParallelInterface::new(bus, dc, wr);28    let mut display = mipidsi::Builder::new(mipidsi::models::ST7796, interface)29        .reset_pin(rst)30        .invert_colors(mipidsi::options::ColorInversion::Inverted)31        .orientation(mipidsi::options::Orientation {32            rotation: mipidsi::options::Rotation::Deg0,33            mirrored: true,34        })35        .color_order(mipidsi::options::ColorOrder::Bgr)36        .init(delay)37        .unwrap();3839    Display {40        _cs_pin: cs,41        _rd_pin: rd,42        inner: display,43        backlight,44    }45}
rustCopy codeCopyCopied!
1pub type Bus = at32f4xx_hal::gpio::Bus<'B', 0, 0xFFFF, Output>;2pub type CsPin = Pin<'C', 13, Output>;3pub type DcPin = Pin<'C', 14, Output>;4pub type RdPin = Pin<'C', 0, Output>;5pub type WrPin = Pin<'C', 15, Output>;6pub type RstPin = Pin<'C', 1, Output>;7pub type Backlight = PwmChannel<at32f4xx_hal::pac::TMR2, 0>;8pub type InnerDisplay = mipidsi::Display<9    mipidsi::interface::ParallelInterface<Bus>,10    mipidsi::models::ST7796,11    RstPin,12>;1314pub fn init(15    mut rd: RdPin,16    mut cs: CsPin,17    dc: DcPin,18    wr: WrPin,19    rst: RstPin,20    bus: Bus,21    delay: &mut SysDelay,22    backlight: Backlight,23) -> Display {24    cs.set_low();25    rd.set_high();2627    let interface = mipidsi::interface::ParallelInterface::new(bus, dc, wr);28    let mut display = mipidsi::Builder::new(mipidsi::models::ST7796, interface)29        .reset_pin(rst)30        .invert_colors(mipidsi::options::ColorInversion::Inverted)31        .orientation(mipidsi::options::Orientation {32            rotation: mipidsi::options::Rotation::Deg0,33            mirrored: true,34        })35        .color_order(mipidsi::options::ColorOrder::Bgr)36        .init(delay)37        .unwrap();3839    Display {40        _cs_pin: cs,41        _rd_pin: rd,42        inner: display,43        backlight,44    }45}

现在,我们拥有了一个可以进行绘制的 Display。通过用 Ghidra 打开编译后的固件,我们还能确认数据发送循环被编译成了一个简单的循环,该循环将字节序列写入单个 MMIO 寄存器:

cCopy codeCopyCopied!
1void __rustcall mipidsi::interface::parallel::send_command<>(ParallelInterface<> *self,u8 command,&[u8] args)23{4  byte *pbVar1;5  u8 *puVar2;67  _DAT_40010c0c = command & 0xff;8  _DAT_422202b8 = 1;9  _DAT_42220238 = 1;10  pbVar1 = args.data_ptr;11  for (puVar2 = args.len; puVar2 != 0x0; puVar2 = puVar2 + -1) {12    _DAT_40010c0c = *pbVar1;13    pbVar1 = pbVar1 + 1;14    _DAT_422202bc = 1;15    _DAT_4222023c = 1;16  }17  return;18}

屏幕点亮之后,我接下来实现了 CAN 和蓝牙协议的编码与解码。这里我用了 deku,它能通过非常简洁的宏为结构体声明字节级和位级的解析器^6

```rust /// 513 #[derive(deku::DekuRead, deku::DekuSize, defmt::Format, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(deku::DekuWrite, Debug))] #[deku(bit_order = "lsb", endian = "little")] pub struct ControllerSpeed { /// 单位:km/h * 100 #[deku(pad_bytes_after = "2")] pub motor_speed: u16, #[deku(bits = 1)] pub walk_mode: bool, #[deku(bits = 1)] pub headlight_on: bool, #[deku(bits = 1, pad_bits_after = "5")] pub brake_light_on: bool, } #[test] fn test_display_throttle() { let mut buf = [0u8; 8]; deser_roundtrip(&mut buf, &DisplayThrottle::new(511, false, false, 0)); assert_eq!(buf, [0xff, 0b1, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]); deser_roundtrip(&mut buf, &DisplayThrottle::new(511, true, false, 0)); assert_eq!(buf, [0xff, 0b011, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]); deser_roundtrip(&mut buf, &DisplayThrottle::new(511, true, true, 2)); assert_eq!(buf, [0xff, 0b111, 0x00, 0x02, 0x02, 0x00, 0x00, 0x00]); deser_roundtrip(&mut buf, &DisplayThrottle::new(1, false, true, 2)); assert_eq!(buf, [0x01, 0b100, 0x00, 0x02, 0x02, 0x00, 0x00, 0x00]); deser_roundtrip(&mut buf, &DisplayThrottle::new(256, false, true, 2)); assert_eq!(buf, [0x00, 0b101, 0x00, 0x02, 0x02, 0x00, 0x00, 0x00]); } ``` 用 Rust 实现的一个妙处在于,这些定义可以直接复用到另一个程序中,将 CAN 日志解码为人类可读的格式。
Copy codeCopyCopied!
1L1 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })2L2 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3094 })3L3 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })4L4 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })5L5 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3094 })6L6 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })7L7 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })8L8 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })9L9 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })10L10 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })11L11 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48499, current_ma: 3075 })12L12 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })13L13 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })14L14 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })15L15 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })16L16 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })17L17 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48499, current_ma: 3075 })18L18 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })19L19 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })20L20 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })21L21 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })22L22 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })23L23 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48498, current_ma: 3055 })24L24 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })25L25 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })26L26 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })27L27 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })28L28 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })29L29 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48498, current_ma: 3055 })30L30 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })31L31 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })32L32 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })33L33 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })34L34 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })35L35 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3049 })36L36 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })37L37 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })38L38 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })39L39 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })40L40 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })41L41 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3049 })42L42 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })43L43 id=774 ext=false from=display DisplayThrottle(DisplayThrottle { throttle: 85, left_blinker: false, right_blinker: false, speed_limit: 0, magic: DekuConst })44L44 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })45L45 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })46L46 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })47L47 id=768 ext=false from=display DisplaySpeedMode(DisplaySpeedMode { mode: 0, mode_high: 90, headlight: 100, magic: Normal, speed_mode_byte: 0, walk_counter: 0 })48L48 id=494 ext=false from=display unknown [60, 00, 00, 00, 00, 00, 00, 00]49L49 id=495 ext=false from=unknown unknown [4c, 44, 2e, 43, 52, 2e, 53, 38]50L50 id=495 ext=false from=unknown unknown [30, 37, 2e, 43, 2e, 32, 2e, 31]51L51 id=495 ext=false from=unknown unknown [45, 37, 2e, 32, 2e, 32, 2e, 31]52L52 id=495 ext=false from=unknown unknown [31, 00, 00, 00, 00, 00, 00, 00]53L53 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]54L54 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]55L55 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]56L56 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]57L57 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]58L58 id=768 ext=false from=display DisplaySpeedMode(DisplaySpeedMode { mode: 0, mode_high: 90, headlight: 100, magic: Normal, speed_mode_byte: 0, walk_counter: 0 })59L59 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]60L60 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]61L61 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]62L62 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]63L63 id=1856 ext=true from=display unknown [4b, 00]64L64 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })65L65 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3044 })66L66 id=1857 ext=true from=unknown unknown [4b, 00, 00, 00, 69, 99, 52, 42]67L67 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })68L68 id=1860 ext=true from=display unknown [01]69L69 id=1861 ext=true from=battery unknown [01, 15, 57, 20, 50, 59, 54, 34]70L70 id=774 ext=false from=display DisplayThrottle(DisplayThrottle { throttle: 84, left_blinker: false, right_blinker: false, speed_limit: 0, magic: DekuConst })71L71 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: true, brake_light_on: false })72L72 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 8191 })73L73 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })74L74 id=768 ext=false from=display DisplaySpeedMode(DisplaySpeedMode { mode: 0, mode_high: 90, headlight: 100, magic: Normal, speed_mode_byte: 0, walk_counter: 0 })

1L1 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })2L2 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3094 })3L3 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })4L4 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })5L5 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3094 })6L6 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })7L7 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })8L8 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })9L9 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })10L10 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })11L11 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48499, current_ma: 3075 })12L12 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })13L13 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })14L14 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })15L15 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })16L16 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })17L17 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48499, current_ma: 3075 })18L18 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })19L19 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })20L20 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })21L21 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })22L22 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })23L23 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48498, current_ma: 3055 })24L24 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })25L25 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })26L26 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })27L27 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })28L28 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })29L29 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48498, current_ma: 3055 })30L30 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })31L31 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })32L32 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })33L33 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })34L34 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })35L35 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3049 })36L36 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })37L37 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })38L38 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })39L39 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })40L40 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })41L41 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3049 })42L42 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })43L43 id=774 ext=false from=display DisplayThrottle(DisplayThrottle { throttle: 85, left_blinker: false, right_blinker: false, speed_limit: 0, magic: DekuConst })44L44 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: false, brake_light_on: false })45L45 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 33 })46L46 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })47L47 id=768 ext=false from=display DisplaySpeedMode(DisplaySpeedMode { mode: 0, mode_high: 90, headlight: 100, magic: Normal, speed_mode_byte: 0, walk_counter: 0 })48L48 id=494 ext=false from=display unknown [60, 00, 00, 00, 00, 00, 00, 00]49L49 id=495 ext=false from=unknown unknown [4c, 44, 2e, 43, 52, 2e, 53, 38]50L50 id=495 ext=false from=unknown unknown [30, 37, 2e, 43, 2e, 32, 2e, 31]51L51 id=495 ext=false from=unknown unknown [45, 47, 2e, 32, 2e, 32, 2e, 31]52L52 id=495 ext=false from=unknown unknown [31, 00, 00, 00, 00, 00, 00, 00]53L53 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]54L54 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]55L55 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]56L56 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]57L57 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]58L58 id=768 ext=false from=display DisplaySpeedMode(DisplaySpeedMode { mode: 0, mode_high: 90, headlight: 100, magic: Normal, speed_mode_byte: 0, walk_counter: 0 })59L59 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]60L60 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]61L61 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]62L62 id=495 ext=false from=unknown unknown [00, 00, 00, 00, 00, 00, 00, 00]63L63 id=1856 ext=true from=display unknown [4b, 00]64L64 id=1024 ext=false from=battery BatteryCommandState(BatteryCommandState { command: 16384, state: 1159, estimated_range: 0 })65L65 id=1025 ext=false from=battery BatteryVoltageCurrent(BatteryVoltageCurrent { voltage_mv: 48500, current_ma: 3044 })66L66 id=1857 ext=true from=unknown unknown [4b, 00, 00, 00, 69, 99, 52, 42]67L67 id=1028 ext=false from=battery BatteryCapacityTemp(BatteryCapacityTemp { capacity_mah: 20000, battery_charged: true, battery_charging: false, battery_temp: 270 })68L68 id=1860 ext=true from=display unknown [01]69L69 id=1861 ext=true from=battery unknown [01, 15, 57, 20, 50, 59, 54, 34]70L70 id=774 ext=false from=display DisplayThrottle(DisplayThrottle { throttle: 84, left_blinker: false, right_blinker: false, speed_limit: 0, magic: DekuConst })71L71 id=513 ext=false from=controller ControllerSpeed(ControllerSpeed { motor_speed: 0, walk_mode: false, headlight_on: true, brake_light_on: false })72L72 id=515 ext=false from=controller ControllerSpeedMode(ControllerSpeedMode { unknown: 8191 })73L73 id=528 ext=false from=controller ControllerSpeedLimit(ControllerSpeedLimit { speed_limit: false })74L74 id=768 ext=false from=display DisplaySpeedMode(DisplaySpeedMode { mode: 0, mode_high: 90, headlight: 100, magic: Normal, speed_mode_byte: 0, walk_counter: 0 })
清单 2:将前文展示的 CAN 日志解码后的结果

Actor 模型

原始来源: Hacker News

评论 (0)