THOM-AwS/b6charger-ctl
GitHub: THOM-AwS/b6charger-ctl
面向 SkyRC B6 系列充电器的跨平台开源控制工具,通过 CLI 和 HTTP API 替代仅限 Windows 的官方软件,在 Linux/树莓派上实现无头充电管理与监控。
Stars: 0 | Forks: 0
# b6charger-ctl
[](https://github.com/THOM-AwS/b6charger-ctl/actions/workflows/test.yml)
[](LICENSE)
[](pyproject.toml)
针对 SkyRC B6 系列 USB HID 平衡充电器(iMAX
B6 / B6AC / B6 mini)以及其他品牌贴牌销售的克隆版本(Jaycar
POWERTECH PLUS MB-3633 / JMB-3633 是本工具开发与测试所基于的型号)的读写控制工具。
该硬件系列的官方 PC 应用程序("Charge Master")仅限
Windows、仅限 GUI 且为闭源软件。而这是一个轻量级、
无第三方依赖的 Python 库、CLI 以及可选的 HTTP 包装器,旨在
在已经插入充电器 USB 端口的设备(参考设置中为
Raspberry Pi Zero W)上无头运行。
**⚠️ 状态**:写入路径(启动/停止/配置/限制)已经过
真机测试并正常运行 - 具体验证内容及方法请参阅
[`DRY_RUN.md`](DRY_RUN.md)。
该工具仍需控制一台调节 LiPo/LiHV 充电
电压和电流的设备。在进行首次实际
`start` 之前,请务必阅读下方的 **Safety** 章节。
## 目录
- [为什么开发此工具](#why-this-exists)
- [安装](#install)
- [配置你的电池](#configure-your-batteries)
- [命令参考](#command-reference)
- [HTTP API](#http-api)
- [能自动识别电池吗?](#can-this-identify-the-battery-automatically)
- [协议说明](#protocol-notes--findings-worth-knowing-about)
- [安全](#safety)
- [贡献](#contributing)
- [License](#license)
## 为什么开发此工具
充电器自带的说明书宣称可以通过其 Micro USB
端口实现真正的 PC 控制("通过 Charge Master 发起、控制
充电并更新固件")。这个功能确实存在;只是使用它的官方软件无法
在 Linux 上运行。本项目基于
[libb6](https://github.com/maciek134/libb6)(GPL-3 协议,针对 "SkyRC B6xx
系列充电器")直接重新实现了底层通信协议 - 这正是该硬件所使用的
同系列协议。
## 安装
需要 Python 3.11+(因为使用了标准库中的 `tomllib` TOML
解析器 - 在正常使用情况下完全没有第三方依赖)。
```
git clone https://github.com/THOM-AwS/b6charger-ctl
cd b6charger-ctl
pip install -e .
```
这会安装两个命令:`b6ctl`(即 CLI)和 `b6httpd`(即
可选的 HTTP 包装器)。你可以立即零风险试用,且不需要
硬件:
```
b6ctl --fake status
```
如果打印出状态信息块,说明安装成功。下面所有
涉及真实硬件的操作,都可以使用 `--fake` 参数来运行一个基于内存的
模拟充电器 - 这对于在将命令指向你的真实设备之前
安全地进行尝试非常有用。
## 配置你的电池
这部分内容能让 `b6ctl` 在日常使用中更加得心应手,而
不必在每次充电时重新输入化学类型/电池芯数/电流。
**1. 复制示例文件:**
```
cp packs.example.toml packs.toml
```
`packs.toml` 已包含在 `.gitignore` 中 - 你的电池列表会保留在你的
机器上,永远不会被提交,也
不会包含在此代码库中(内置的 [`packs.example.toml`](packs.example.toml)
只是一个无害的单条目占位符,并非真实的电池数据)。
**2. 编辑 `packs.toml`** - 每个电池对应一个 `[[pack]]` 块。每个
参数都可以直接从电池本身印制的标签上获取;你不需要
猜测或测量任何东西:
```
[[pack]]
name = "zeee2200" # what you'll type as --pack zeee2200
description = "Zeee 2200mAh 3S, standard LiPo"
chemistry = "lipo" # "lipo" (4.20V/cell) or "lihv" (4.35V/cell)
cells = 3 # the "S" number printed on the pack
capacity_mah = 2200 # the mAh rating printed on the pack
default_current_ma = 1100 # used when you don't pass --current-ma (0.5C here)
```
| 字段 | 含义 | 数据来源 |
|---|---|---|
| `name` | 简短 ID,不含空格 - 这是你 `--pack NAME` 的参数 | 你自行决定 |
| `description` | 自由文本标签,仅供你自己参考 | 你自行决定 |
| `chemistry` | `"lipo"` (4.20V/cell) 或 `"lihv"` (4.35V/cell) - **最重要的字段** | 印在电池上 ("LiPo"/"LiHV"/"HV") |
| `cells` | "S" 数量 (3S = 3, 4S = 4, ...) | 印在电池上 |
| `capacity_mah` | 容量 | 印在电池上 |
| `default_current_ma` | 未指定 `--current-ma` 时使用的充电电流 | 建议选择 0.5C(容量的一半)作为安全的起始值 |
| `max_current_ma` (可选) | 针对单块电池的硬性电流上限;省略时默认为 1C | 可选 - 无论设置多少,该工具绝不允许其超过 1C |
包含详尽字段说明的完整文档(包括关于化学类型"不确定时用哪个"
的指导),都在
[`packs.example.toml`](packs.example.toml) 文件内部的
注释中。
**3. 通过名称充电:**
```
b6ctl start --pack zeee2200
```
这是推荐的充电启动方式,因为它附带了一个
手动指定 `--chemistry`/`--cells` 参数所没有的安全检查(参见
下一节)。
### 为什么 `--pack` 比手动输入参数更安全
`start --pack NAME` 会读取充电器**实时**的电池芯数,
并且如果不匹配该电池在配置中注册的芯数,则
拒绝发送任何指令:
```
$ b6ctl start --pack wrong_cell_count
error: pack 'wrong_cell_count' is configured as 4S, but the charger
currently detects 3 real cell(s) connected - refusing to start. Check
the physical connection and the pack you meant to select before
retrying.
```
这里有意省去了跳过此检查的参数。如果你非常确定并且
需要覆盖此设置,请直接使用手动的 `--chemistry`/`--cells`/
`--current-ma` 参数(见下文) - 这是一种
显式的独立操作,而不是"安全"路径上的一个复选框。
注册表还会**在代码层面**强制执行安全边界,
而不仅仅是在文件中:对于某块电池的容量,`max_current_ma` 永远不能超过 1C,并且
无论你在 `packs.toml` 中写入了什么,`default_current_ma` 也
永远不能超过 `max_current_ma`。如果因为拼写错误设置了危险的电流,
在加载时就会被拦截并返回明确的错误,而不会被默默接受。
## 命令参考
下方的每个表格都是详尽无遗的 - 包含了每个命令接受的每一个参数、其
有效值以及默认值。你完全不需要去阅读源码
或运行 `--help` 来了解可用功能。
### 全局参数(适用于所有命令)
| 参数 | 取值 | 默认值 | 含义 |
|---|---|---|---|
| `--fake` | (布尔值) | 关闭 | 使用基于内存的模拟充电器 - 完全不会触及真实硬件 |
| `--device` | 路径,例如 `/dev/hidraw0` | 自动发现 | 使用特定设备,而不是探测每一个 `/dev/hidraw*` |
| `-v`, `--verbose` | (布尔值) | 关闭 | 在发送之前,以十六进制格式记录发送的每一帧 |
### `status` - 读取实时充电信息
```
b6ctl status
b6ctl status --json
```
| 参数 | 取值 | 默认值 | 含义 |
|---|---|---|---|
| `--json` | (布尔值) | 关闭 | 输出机器可读格式,而非人类可读的信息块 |
报告内容:状态、电池组电压、电流、已充入容量、经过
时间、内部/外部温度、阻抗、各节电池电压,以及
电池压差。始终安全 - 为只读操作,绝不进行任何写入。
### `sysinfo` - 读取当前系统设置
```
b6ctl sysinfo
b6ctl sysinfo --json
```
| 参数 | 取值 | 默认值 | 含义 |
|---|---|---|---|
| `--json` | (布尔值) | 关闭 | 输出机器可读格式,而非人类可读的信息块 |
报告内容:循环次数、时间限制(开/关 + 分钟数)、容量限制
(开/关 + mAh)、温度限制、低 DC 截止电压、按键/系统蜂鸣器状态。
通过这个命令可以验证 `set-limits` 的写入是否
真正生效,而不是仅仅盲目相信它没有报错。只读操作。
### `packs list` / `packs show` - 查看你的电池列表
```
b6ctl packs list
b6ctl packs show zeee2200
```
`packs show` 接受一个位置参数:来自
`packs.toml` 的电池 `name`。除了全局参数之外,这两个子命令都不接受任何
其他参数。
请参阅 [配置你的电池](#configure-your-batteries) 获取完整的
`packs.toml` 字段说明(`chemistry`, `cells`, `capacity_mah`,
`default_current_ma`, `max_current_ma`)。
### `start` - 开始充电
**推荐**方式,使用已配置的电池:
```
b6ctl start --pack zeee2200
b6ctl start --pack zeee2200 --current-ma 1500 # override the default current
```
**手动**方式,无需注册表条目:
```
b6ctl start --chemistry lipo --cells 3 --current-ma 1500
```
| 参数 | 取值 | 默认值 | 含义 |
|---|---|---|---|
| `--pack` | `packs.toml` 中的名称 | - | 使用已配置的电池。与 `--chemistry`/`--cells` 互斥;运行时会执行实时电池芯数安全检查(见上文) |
| `--chemistry` | `lipo`, `lihv` | - | 标准 LiPo (4.20V/cell) 或 高压 LiHV (4.35V/cell)。如果不使用 `--pack` 则为必填项;与 `--pack` 同时使用会报错 |
| `--cells` | 整数,1-16 | - | 电池芯数("S" 数量)。如果不使用 `--pack` 则为必填项;与 `--pack` 同时使用会报错 |
| `--current-ma` | 整数,毫安 | 使用 `--pack` 时默认为该电池组的 `default_current_ma`;不使用 `--pack` 时**必填** | 充电电流。使用 `--pack` 时,受限于该电池组的 `max_current_ma` |
| `--discharge-current-ma` | 整数,毫安 | `1000` | 放电电流(仅在放电/存储模式下相关) |
| `--mode` | `standard`, `discharge`, `storage`, `fast`, `balance` | `balance` | 锂电池充电模式 - `balance`(单节平衡)是常规充电的首选 |
| `--dry-run` | (布尔值) | 关闭 | 构建并记录数据帧(包括执行 `--pack` 电池芯数检查),但不发送任何内容 |
| `--auto-approve`, `--yes` | (布尔值) | 关闭 | 跳过交互式确认提示。无论是否设置此项,配置信息总是会被优先打印出来 |
完整的 `chemistry` 取值供参考(通过该参数仅暴露了 `lipo`/`lihv` -
`protocol.py` 中的协议层还
支持 `LIION`/`LIFE`/`NIMH`/`NICD`/`PB`,供扩展 CLI 的开发者使用,详见 `protocol.py` 中的 `BatteryType`):
| 化学类型 | 截止电压 | CLI 参数值 |
|---|---|---|
| 标准 LiPo / LiIon / LiFe | 4.20V/cell | `lipo` |
| 高压 LiPo (LiHV) | 4.35V/cell | `lihv` |
完整的 `--mode` 取值:
| 模式 | 含义 |
|---|---|
| `standard` | 普通充电,无平衡功能 |
| `discharge` | 对电池组进行放电 |
| `storage` | 充电/放电至存储电压(约 3.8V/cell),适用于长期存放 |
| `fast` | 快速充电,终止充电的判定精度较低 |
| `balance` | 带有单节平衡功能的充电(常规选择,也是默认模式) |
### `stop` - 停止当前充电
```
b6ctl stop
b6ctl stop --dry-run
```
| 参数 | 取值 | 默认值 | 含义 |
|---|---|---|---|
| `--dry-run` | (布尔值) | 关闭 | 记录 STOP 帧,但不发送任何内容 |
### `set-limits` - 配置安全截止参数
```
b6ctl set-limits --temp-limit 50 --time-limit 200 --capacity-limit 6000
```
| 参数 | 取值 | 默认值 | 含义 |
|---|---|---|---|
| `--cycle-time` | 整数,1-60(分钟) | 若未指定则不发送 | 循环充放电的循环次数 |
| `--time-limit` | 整数,1-720(分钟) | 若未指定则不发送 | 充电时间限制安全截止点(设置后启用) |
|--capacity-limit` | 整数,100-50000 (mAh) | 若未指定则不发送 | 充电容量限制安全截止点(设置后启用) |
| `--temp-limit` | 整数,20-80 (°C) | 若未指定则不发送 | 内部温度截止点 |
| `--dry-run` | (布尔值) | 关闭 | 记录 SET 帧但不发送任何内容 |
只有你实际传入的限制参数会被发送 - 该命令不会
触碰你未提及的设置。之后可以通过
`b6ctl sysinfo` 来验证任何更改。
## HTTP API
这是位于 ht-infra 只读 `b6_poller.py` 导出器旁边(而
非合并到其中)的一个轻量级控制接口 - 特意将其作为
独立的进程/端口,因为一旦出现 bug,监控 endpoint 和控制 endpoint 的
影响范围截然不同。
```
b6httpd --port 9111 # binds 127.0.0.1:9111 - widen deliberately
b6httpd --listen 0.0.0.0:9111 # combined interface:port form
b6httpd --listen [::1]:9111 # IPv6 needs bracket notation
```
### `b6httpd` 参数
| 参数 | 取值 | 默认值 | 含义 |
|---|---|---|---|
| `--host` | 接口地址,例如 `0.0.0.0` | `127.0.0.1` | 要绑定的网络接口。与 `--listen` 互斥 |
| `--port` | 整数,1-65535 | `9111` | 要绑定的端口。与 `--listen` 互斥 |
| `--listen` | `HOST:PORT`, 例如 `0.0.0.0:9111`;IPv6 则为 `[HOST]:PORT`, 例如 `[::1]:9111` | - | 在单个参数中合并接口+端口。与 `--host`/`--port` 互斥 - 只能选择其中一种格式 |
| `--fake` | (布尔值) | 关闭 | 使用基于内存的模拟充电器 - 完全不会触及真实硬件 |
| `--device` | 路径,例如 `/dev/hidraw0` | 自动发现 | 使用特定设备,而不是探测每一个 `/dev/hidraw*` |
| `--dry-run` | (布尔值) | 关闭 | 记录每一次写入,但不向设备发送任何内容 |
绑定到 `127.0.0.1` 以外的地址是一个需要慎重对待的决定 - 因为
任何能够访问到该地址的人,都可以指示此
进程向 LiPo 充电器下达指令。请谨慎扩大暴露范围,不要将其作为默认行为。
### Endpoints
```
GET /status -> same fields as `b6ctl status --json`
POST /start {"chemistry": "lipo", "cells": 3, "current_ma": 1500, "mode": "balance"}
POST /stop
```
每一项写入操作在发送之前,都会连同调用者的 IP 地址一起被记录下来。
注意:HTTP API 目前不支持 `packs.toml` 注册表
或其电池芯数交叉检查 - 它接收的是原始配置信息。如果你基于此
构建自动化任务,请考虑首先将相同的
安全检查移植到这里。
## 能自动识别电池吗?
**不能,而且有必要了解原因**,而不是假设未来的
版本可能会做到:
- **电池芯数**:是的,非常可靠 - 这正是 `status` 的电池电压
读数直接测量的内容。
- **具体是哪块电池,或者其容量**:不能。除了平衡插头和
主电源线之外,没有 ID 芯片也
没有数据引脚 - 容量和化学类型并没有在任何
这台充电器能够读取的地方进行电气编码。插入时的电压反映的是
充电状态,而不是电池身份
(无论具体是哪块物理电池,3S 电池组的静置
电压都可以在 ~9.6V 到 12.6V 之间的任意位置)。
- **"电池芯数唯一标识了电池组 X" 的陷阱**:如果你只有
一块特定芯数的电池组,那么在今天,它的电池芯数可能*看起来*像是一个
可靠的标识符。但这并非真正的技术能力 - 这只是你当前
电池组列表的一个巧合,一旦你增加了另一块芯数相同但
化学类型不同的电池组,它就会失效。
这正是因为 `packs.toml` 的设计是对由你**亲自**提供的名称进行交叉检查,
而不是一套自动识别系统。
## 协议说明 / 值得了解的发现
- **帧格式**:`[0x0F, LEN, CMD, 0x00, ...payload..., checksum,
0xFF, 0xFF]`,校验和 = `sum(bytes from index 2 onward) & 0xFF`。
详细内容请见 `b6charger/protocol.py` 的模块文档字符串。
- **电池电压噪声过滤器**:充电器的平衡插座支持的
电池芯数多于大多数电池组的实际使用数量,一旦有真实的
充电电流流过,未使用的引脚可能会读取到
稳定的、但在物理上不可能存在的电压(观察到约为 ~9V) - 这在读
取闲置设备时是看不到的。此处通过
`[2000mV, 4400mV]` 的范围检查(`protocol.py` 中的
`CELL_MIN_MV`/`CELL_MAX_MV`)将其过滤掉,而不是仅进行下限检查。完整的实时追踪记录请见
[`DRY_RUN.md`](DRY_RUN.md)。
- **阻抗字段**:`GET_CHARGE_INFO` 的响应包含了
单块电池组的内阻读数(说明书中的"Battery Internal
Resistance Meter" 功能) - 在这里被解码为 `impedance_mohm`。
- **STATE 4 差异 - 未经核实**:libb6 的 `Enum.hh` 将
充电器状态 `4` 定义为第二种错误状态(`ERROR_2`);而其他一些
B6 系列工具将其标记为"idle"。目前尚未核实哪种情况对
应哪种固件。详见 `protocol.py` 中 `State` 枚举的文档字符串。
## 安全
请先参阅 [`DISCLAIMER.md`](DISCLAIMER.md)。
本工具控制的是一台专门负责调节 LiPo/LiHV 充电
电压和电流的设备。这里的 bug 与只读
监控工具中的 bug 属于不同的风险等级。
- 请优先使用 `start --pack NAME` 而非手动指定参数 - 它具备
手动方式所缺乏的实时硬件交叉校验。
- 在执行任何不熟悉的操作时,请务必先尝试 `--dry-run` - 它
会执行真实命令的所有读取操作(因此像
电池芯数交叉检查这样的功能依然会运行并反馈真实结果),但不会
发送任何内容。
- 在信任你未曾亲自测试过的代码路径之前,请先阅读
[`DRY_RUN.md`](DRY_RUN.md) - 其中准确记录了哪些功能已经过
真机验证,哪些只是根据协议规范实现但尚未核实。
- 在没有亲自见证其行为正确之前,切勿将写入路径用于
无人值守的自动化任务。
## 贡献
请参阅 [`CONTRIBUTING.md`](CONTRIBUTING.md)。
## License
GPL-3.0-or-later - 请参阅 [`LICENSE`](LICENSE)。之所以选择该协议,是因为这里的帧
编码方式紧密遵循了 libb6 自身的内部结构,而
非纯粹通过对黑盒通信抓包独立推导出来,因此将本作视为
衍生作品并采用与之相匹配的许可证是
正确的做法,而不仅仅是出于风格偏好。
标签:HTTP API, Python, Python安全, 无后门, 物联网, 电池充电器, 硬件控制, 逆向工具