THOM-AwS/b6charger-ctl

GitHub: THOM-AwS/b6charger-ctl

面向 SkyRC B6 系列充电器的跨平台开源控制工具,通过 CLI 和 HTTP API 替代仅限 Windows 的官方软件,在 Linux/树莓派上实现无头充电管理与监控。

Stars: 0 | Forks: 0

# b6charger-ctl [![test](https://github.com/yuin/goldmark/actions?query=workflow:test](https://static.pigsec.cn/wp-content/uploads/repos/cas/96/96516d7a51f21139fae950e3129296fedeb5ab5f68f6a4dd1d280445b5bfdb15.svg)](https://github.com/THOM-AwS/b6charger-ctl/actions/workflows/test.yml) [![License: GPL-3.0-or-later](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](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安全, 无后门, 物联网, 电池充电器, 硬件控制, 逆向工具