boardctl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boardctl-0.1.0/.github/workflows/publish.yml +20 -0
- boardctl-0.1.0/.gitignore +17 -0
- boardctl-0.1.0/PKG-INFO +115 -0
- boardctl-0.1.0/README.md +105 -0
- boardctl-0.1.0/boardctl/__init__.py +11 -0
- boardctl-0.1.0/boardctl/__main__.py +4 -0
- boardctl-0.1.0/boardctl/boards/sg2002.toml +67 -0
- boardctl-0.1.0/boardctl/cli.py +68 -0
- boardctl-0.1.0/boardctl/config.py +77 -0
- boardctl-0.1.0/boardctl/console.py +207 -0
- boardctl-0.1.0/boardctl/plugins/__init__.py +42 -0
- boardctl-0.1.0/boardctl/plugins/executors/__init__.py +8 -0
- boardctl-0.1.0/boardctl/plugins/executors/go.py +7 -0
- boardctl-0.1.0/boardctl/plugins/executors/none.py +7 -0
- boardctl-0.1.0/boardctl/plugins/executors/source.py +7 -0
- boardctl-0.1.0/boardctl/plugins/power/__init__.py +10 -0
- boardctl-0.1.0/boardctl/plugins/power/command.py +31 -0
- boardctl-0.1.0/boardctl/plugins/power/mijia.py +37 -0
- boardctl-0.1.0/boardctl/plugins/transport/__init__.py +9 -0
- boardctl-0.1.0/boardctl/plugins/transport/loady.py +70 -0
- boardctl-0.1.0/boardctl/plugins/transport/tftp.py +97 -0
- boardctl-0.1.0/boardctl/power.py +95 -0
- boardctl-0.1.0/boardctl/runner.py +100 -0
- boardctl-0.1.0/boardctl/session.py +49 -0
- boardctl-0.1.0/boardctl/shell.py +21 -0
- boardctl-0.1.0/boardctl/tftp_server.py +121 -0
- boardctl-0.1.0/boardctl.sh +4 -0
- boardctl-0.1.0/main.py +11 -0
- boardctl-0.1.0/pyproject.toml +23 -0
- boardctl-0.1.0/requirements.txt +2 -0
- boardctl-0.1.0/run.sh +1 -0
- boardctl-0.1.0/tests/README.md +54 -0
- boardctl-0.1.0/tests/baremetal/EXPECTED.txt +13 -0
- boardctl-0.1.0/tests/baremetal/build.sh +17 -0
- boardctl-0.1.0/tests/baremetal/hello.S +56 -0
- boardctl-0.1.0/tests/baremetal/hello.bin +0 -0
- boardctl-0.1.0/tests/baremetal/hello.dis +69 -0
- boardctl-0.1.0/tests/baremetal/hello.elf +0 -0
- boardctl-0.1.0/tests/script/EXPECTED.txt +21 -0
- boardctl-0.1.0/tests/script/build_script.sh +8 -0
- boardctl-0.1.0/tests/script/test_uboot.scr +0 -0
- boardctl-0.1.0/tests/script/test_uboot.txt +5 -0
- boardctl-0.1.0/tests/verify_e2e.py +89 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*'] # 打 tag(如 v0.1.0)自动构建发布
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write # PyPI trusted publishing (OIDC) 必需
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: '3.12'
|
|
17
|
+
- name: Build
|
|
18
|
+
run: uv build
|
|
19
|
+
- name: Publish to PyPI
|
|
20
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
boardctl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: boardctl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 开发板控制工具:串口终端 / 电源控制 / 文件传输(TFTP、Ymodem) / 一键启动测试,插件化架构(设计参考 ostool)
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: pyserial>=3.5
|
|
7
|
+
Provides-Extra: mijia
|
|
8
|
+
Requires-Dist: mijiaapi>=4; extra == 'mijia'
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# remote-serial / boardctl
|
|
12
|
+
|
|
13
|
+
开发板控制工具(设计参考 [ostool](https://crates.io/crates/ostool)):每块开发板一个
|
|
14
|
+
TOML 配置,**模块化 + 插件化**架构——加传输方式/执行方式只需在插件目录丢一个文件,
|
|
15
|
+
改某块功能只需动对应模块。
|
|
16
|
+
|
|
17
|
+
当前接入的板卡:**Sipeed LicheeRv Nano**(Sophgo SG2002 / CV181x,RISC-V 玄铁 C906,
|
|
18
|
+
U-Boot 2021.10,提示符 `soph#`),串口经 TCP 桥(gem12)透传,电源为小米智能插座。
|
|
19
|
+
|
|
20
|
+
## 快速上手
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
./boardctl.sh -b sg2002 run # 列出该板一键启动目标
|
|
24
|
+
./boardctl.sh -b sg2002 run hello # 裸机测试:冷启动→传输→go→断言→断电
|
|
25
|
+
./boardctl.sh -b sg2002 run script # U-Boot 脚本测试
|
|
26
|
+
./boardctl.sh -b sg2002 console # 交互终端(Ctrl-\ 退出;配置 off_on_exit 时退出自动断电)
|
|
27
|
+
./boardctl.sh -b sg2002 cmd 'printenv'
|
|
28
|
+
./boardctl.sh -b sg2002 exec 'ls /srv/tftp' # 在板卡命令环境执行 shell(配置 ssh_host 则经 ssh 远端执行)
|
|
29
|
+
./boardctl.sh -b sg2002 send FILE [--method loady|tftp] [--addr 0x80080000]
|
|
30
|
+
./boardctl.sh -b sg2002 power on|off|status
|
|
31
|
+
./boardctl.sh -b sg2002 reset
|
|
32
|
+
# 等价:./.venv/bin/python -m boardctl ...(任意目录可用 boardctl.sh)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 架构(松耦合,单向依赖)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
boardctl/
|
|
39
|
+
├── cli.py 命令行接线(argparse + 分发,无业务逻辑)
|
|
40
|
+
├── config.py 板卡 TOML 加载(不依赖其他模块)
|
|
41
|
+
├── session.py U-Boot 串口会话 ← config
|
|
42
|
+
├── power.py 电源/冷启动 ← config, session
|
|
43
|
+
├── console.py 交互终端 ← config, session, power
|
|
44
|
+
├── runner.py run/cmd 编排 ← 上述全部 + plugins
|
|
45
|
+
└── plugins/ 插件(目录约定自动发现,零注册代码)
|
|
46
|
+
├── transport/ 传输插件:loady.py、tftp.py
|
|
47
|
+
├── executors/ 执行插件:go.py、source.py、none.py
|
|
48
|
+
└── power/ 电源插件:mijia.py(原生小米云调用,免子进程)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**插件接口**(接口约定写在各 `__init__.py` 里;插件可选声明 `CFG_SECTION` + `DEFAULTS` 自带配置默认值,TOML 优先):
|
|
52
|
+
|
|
53
|
+
- 传输插件(`plugins/transport/xxx.py`):`NAME` + `send(cfg, path, addr) -> bool`
|
|
54
|
+
- 执行插件(`plugins/executors/xxx.py`):`NAME` + `build_cmd(addr) -> str | None`
|
|
55
|
+
- 电源插件(`plugins/power/xxx.py`):`NAME` + `set_power(cfg, on)` + `get_power(cfg) -> bool | None`
|
|
56
|
+
|
|
57
|
+
新建插件 = 加一个文件,`--method`/`[run].exec` 立即可用,核心代码零改动。
|
|
58
|
+
|
|
59
|
+
## 板卡配置(boards/*.toml)
|
|
60
|
+
|
|
61
|
+
搜索顺序(先找到的优先):`$BOARDCTL_BOARDS` → 当前目录 `./boards` →
|
|
62
|
+
**`~/.config/boardctl/boards/`(推荐:用户自己的板放这里)** → 包内置示例。
|
|
63
|
+
|
|
64
|
+
| 段 | 键 | 说明 |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| 顶层 | `ssh_host` | 命令执行位置:空 = 本机(power/exec 等);填 ssh 别名(如 `gem12`)= 经 ssh 在远端主机执行,别名/端口/用户走 `~/.ssh/config` |
|
|
67
|
+
| `[serial]` | `url` / `timeout` | 串口桥 URL(本板为 `socket://...` 裸 TCP) |
|
|
68
|
+
| `[uboot]` | `prompt` / `load_addr` | 提示符、默认加载地址 |
|
|
69
|
+
| | `server_ip` / `ensure_server_ip` | TFTP 服务器地址;该 U-Boot 无 saveenv,连接时自动恢复 |
|
|
70
|
+
| `[power]` | `method = "mijia"` + `[power.mijia]` dev_name/did | **方式一·电源插件**:进程内原生调用小米云(凭证复用 `~/.config/mijia-api/auth.json`,首次需 `mijiaAPI login` 扫码),不走 ssh_host |
|
|
71
|
+
| | `method = "command"` + `on_cmd`/`off_cmd`/`status_cmd` | **方式二·命令插件**:任意开关机 shell 命令(经 ssh_host 决定本机/远端);method 未配置时默认即此。改 `method` 一行切换 |
|
|
72
|
+
| | `off_on_exit = true` | console 退出时自动断电(覆盖所有退出路径:Ctrl-\、Ctrl-A x、kill、串口掉线) |
|
|
73
|
+
| `[tftp]` | `method=remote` + `ssh_host`/`remote_dir` | scp 到远端 tftpd(gem12 的 tftpd-hpa) |
|
|
74
|
+
| | `method=local` + `local_dir` | 本机临时拉起 `tftp_server.py`(UDP 69 需特权,退出自动回收) |
|
|
75
|
+
| `[loady]` | `sender` | Ymodem 发送器(Arch 为 `lrzsz-sb`) |
|
|
76
|
+
| `[run.<名字>]` | `file` / `exec` / `method` / `timeout` | 一键启动目标(exec/method 即插件名) |
|
|
77
|
+
| | `addr` / `entry` | 加载地址 / 跳转执行地址(`go 0x...` 的目标);缺省都取 `uboot.load_addr`,加载与入口不同时分别指定 |
|
|
78
|
+
| | `reset_before` | 开头冷启动:关→开→等提示符(从任意脏状态恢复) |
|
|
79
|
+
| | `after = off/reset/none` | 收尾动作 |
|
|
80
|
+
| | `expect = [..]` | 输出断言,全命中才 PASS(退出码 0/1) |
|
|
81
|
+
|
|
82
|
+
## 端到端验收
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
./.venv/bin/python tests/verify_e2e.py # reset→tftp 部署→原始流逐字节断言,两套测试
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
裸机测试:`tests/baremetal/`(riscv64-linux-gnu-gcc,注意 DW APB UART reg-shift=2、
|
|
89
|
+
`-fno-pie`、`--build-id=none`)。脚本测试:`tests/script/`(`source` 需 mkimage 的
|
|
90
|
+
legacy uImage 头)。构建细节与踩坑记录见 `tests/README.md`。
|
|
91
|
+
|
|
92
|
+
## 依赖
|
|
93
|
+
|
|
94
|
+
Python 3.11+(stdlib `tomllib`)+ pyserial(见 requirements.txt)。交叉构建另需
|
|
95
|
+
`riscv64-linux-gnu-gcc`、`mkimage`(uboot-tools)、`lrzsz`。
|
|
96
|
+
|
|
97
|
+
## 安装与发布
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# 从 PyPI 安装(发布后)
|
|
101
|
+
pip install boardctl # 核心:串口/console/cmd/send(loady)
|
|
102
|
+
pip install 'boardctl[mijia]' # + 小米云电源插件
|
|
103
|
+
|
|
104
|
+
# 板卡配置搜索顺序:$BOARDCTL_BOARDS → ./boards → ~/.config/boardctl/boards → 包内置示例
|
|
105
|
+
# 用户自己的板卡放 ~/.config/boardctl/boards/(优先级高于包内置示例),不碰安装目录
|
|
106
|
+
|
|
107
|
+
# 发布(项目根目录)
|
|
108
|
+
uv build # 产出 dist/*.whl + *.tar.gz
|
|
109
|
+
uv publish --token <PyPI_API_Token> # 建议先发 TestPyPI 演练
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
可选发布路径:GitHub 仓库 + Actions trusted publishing(打 tag 自动发布,免 token),
|
|
113
|
+
或直接 `pip install git+https://...`(零发布设施,适合私有/内网)。
|
|
114
|
+
|
|
115
|
+
## 依赖
|
boardctl-0.1.0/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# remote-serial / boardctl
|
|
2
|
+
|
|
3
|
+
开发板控制工具(设计参考 [ostool](https://crates.io/crates/ostool)):每块开发板一个
|
|
4
|
+
TOML 配置,**模块化 + 插件化**架构——加传输方式/执行方式只需在插件目录丢一个文件,
|
|
5
|
+
改某块功能只需动对应模块。
|
|
6
|
+
|
|
7
|
+
当前接入的板卡:**Sipeed LicheeRv Nano**(Sophgo SG2002 / CV181x,RISC-V 玄铁 C906,
|
|
8
|
+
U-Boot 2021.10,提示符 `soph#`),串口经 TCP 桥(gem12)透传,电源为小米智能插座。
|
|
9
|
+
|
|
10
|
+
## 快速上手
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
./boardctl.sh -b sg2002 run # 列出该板一键启动目标
|
|
14
|
+
./boardctl.sh -b sg2002 run hello # 裸机测试:冷启动→传输→go→断言→断电
|
|
15
|
+
./boardctl.sh -b sg2002 run script # U-Boot 脚本测试
|
|
16
|
+
./boardctl.sh -b sg2002 console # 交互终端(Ctrl-\ 退出;配置 off_on_exit 时退出自动断电)
|
|
17
|
+
./boardctl.sh -b sg2002 cmd 'printenv'
|
|
18
|
+
./boardctl.sh -b sg2002 exec 'ls /srv/tftp' # 在板卡命令环境执行 shell(配置 ssh_host 则经 ssh 远端执行)
|
|
19
|
+
./boardctl.sh -b sg2002 send FILE [--method loady|tftp] [--addr 0x80080000]
|
|
20
|
+
./boardctl.sh -b sg2002 power on|off|status
|
|
21
|
+
./boardctl.sh -b sg2002 reset
|
|
22
|
+
# 等价:./.venv/bin/python -m boardctl ...(任意目录可用 boardctl.sh)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 架构(松耦合,单向依赖)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
boardctl/
|
|
29
|
+
├── cli.py 命令行接线(argparse + 分发,无业务逻辑)
|
|
30
|
+
├── config.py 板卡 TOML 加载(不依赖其他模块)
|
|
31
|
+
├── session.py U-Boot 串口会话 ← config
|
|
32
|
+
├── power.py 电源/冷启动 ← config, session
|
|
33
|
+
├── console.py 交互终端 ← config, session, power
|
|
34
|
+
├── runner.py run/cmd 编排 ← 上述全部 + plugins
|
|
35
|
+
└── plugins/ 插件(目录约定自动发现,零注册代码)
|
|
36
|
+
├── transport/ 传输插件:loady.py、tftp.py
|
|
37
|
+
├── executors/ 执行插件:go.py、source.py、none.py
|
|
38
|
+
└── power/ 电源插件:mijia.py(原生小米云调用,免子进程)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**插件接口**(接口约定写在各 `__init__.py` 里;插件可选声明 `CFG_SECTION` + `DEFAULTS` 自带配置默认值,TOML 优先):
|
|
42
|
+
|
|
43
|
+
- 传输插件(`plugins/transport/xxx.py`):`NAME` + `send(cfg, path, addr) -> bool`
|
|
44
|
+
- 执行插件(`plugins/executors/xxx.py`):`NAME` + `build_cmd(addr) -> str | None`
|
|
45
|
+
- 电源插件(`plugins/power/xxx.py`):`NAME` + `set_power(cfg, on)` + `get_power(cfg) -> bool | None`
|
|
46
|
+
|
|
47
|
+
新建插件 = 加一个文件,`--method`/`[run].exec` 立即可用,核心代码零改动。
|
|
48
|
+
|
|
49
|
+
## 板卡配置(boards/*.toml)
|
|
50
|
+
|
|
51
|
+
搜索顺序(先找到的优先):`$BOARDCTL_BOARDS` → 当前目录 `./boards` →
|
|
52
|
+
**`~/.config/boardctl/boards/`(推荐:用户自己的板放这里)** → 包内置示例。
|
|
53
|
+
|
|
54
|
+
| 段 | 键 | 说明 |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| 顶层 | `ssh_host` | 命令执行位置:空 = 本机(power/exec 等);填 ssh 别名(如 `gem12`)= 经 ssh 在远端主机执行,别名/端口/用户走 `~/.ssh/config` |
|
|
57
|
+
| `[serial]` | `url` / `timeout` | 串口桥 URL(本板为 `socket://...` 裸 TCP) |
|
|
58
|
+
| `[uboot]` | `prompt` / `load_addr` | 提示符、默认加载地址 |
|
|
59
|
+
| | `server_ip` / `ensure_server_ip` | TFTP 服务器地址;该 U-Boot 无 saveenv,连接时自动恢复 |
|
|
60
|
+
| `[power]` | `method = "mijia"` + `[power.mijia]` dev_name/did | **方式一·电源插件**:进程内原生调用小米云(凭证复用 `~/.config/mijia-api/auth.json`,首次需 `mijiaAPI login` 扫码),不走 ssh_host |
|
|
61
|
+
| | `method = "command"` + `on_cmd`/`off_cmd`/`status_cmd` | **方式二·命令插件**:任意开关机 shell 命令(经 ssh_host 决定本机/远端);method 未配置时默认即此。改 `method` 一行切换 |
|
|
62
|
+
| | `off_on_exit = true` | console 退出时自动断电(覆盖所有退出路径:Ctrl-\、Ctrl-A x、kill、串口掉线) |
|
|
63
|
+
| `[tftp]` | `method=remote` + `ssh_host`/`remote_dir` | scp 到远端 tftpd(gem12 的 tftpd-hpa) |
|
|
64
|
+
| | `method=local` + `local_dir` | 本机临时拉起 `tftp_server.py`(UDP 69 需特权,退出自动回收) |
|
|
65
|
+
| `[loady]` | `sender` | Ymodem 发送器(Arch 为 `lrzsz-sb`) |
|
|
66
|
+
| `[run.<名字>]` | `file` / `exec` / `method` / `timeout` | 一键启动目标(exec/method 即插件名) |
|
|
67
|
+
| | `addr` / `entry` | 加载地址 / 跳转执行地址(`go 0x...` 的目标);缺省都取 `uboot.load_addr`,加载与入口不同时分别指定 |
|
|
68
|
+
| | `reset_before` | 开头冷启动:关→开→等提示符(从任意脏状态恢复) |
|
|
69
|
+
| | `after = off/reset/none` | 收尾动作 |
|
|
70
|
+
| | `expect = [..]` | 输出断言,全命中才 PASS(退出码 0/1) |
|
|
71
|
+
|
|
72
|
+
## 端到端验收
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
./.venv/bin/python tests/verify_e2e.py # reset→tftp 部署→原始流逐字节断言,两套测试
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
裸机测试:`tests/baremetal/`(riscv64-linux-gnu-gcc,注意 DW APB UART reg-shift=2、
|
|
79
|
+
`-fno-pie`、`--build-id=none`)。脚本测试:`tests/script/`(`source` 需 mkimage 的
|
|
80
|
+
legacy uImage 头)。构建细节与踩坑记录见 `tests/README.md`。
|
|
81
|
+
|
|
82
|
+
## 依赖
|
|
83
|
+
|
|
84
|
+
Python 3.11+(stdlib `tomllib`)+ pyserial(见 requirements.txt)。交叉构建另需
|
|
85
|
+
`riscv64-linux-gnu-gcc`、`mkimage`(uboot-tools)、`lrzsz`。
|
|
86
|
+
|
|
87
|
+
## 安装与发布
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# 从 PyPI 安装(发布后)
|
|
91
|
+
pip install boardctl # 核心:串口/console/cmd/send(loady)
|
|
92
|
+
pip install 'boardctl[mijia]' # + 小米云电源插件
|
|
93
|
+
|
|
94
|
+
# 板卡配置搜索顺序:$BOARDCTL_BOARDS → ./boards → ~/.config/boardctl/boards → 包内置示例
|
|
95
|
+
# 用户自己的板卡放 ~/.config/boardctl/boards/(优先级高于包内置示例),不碰安装目录
|
|
96
|
+
|
|
97
|
+
# 发布(项目根目录)
|
|
98
|
+
uv build # 产出 dist/*.whl + *.tar.gz
|
|
99
|
+
uv publish --token <PyPI_API_Token> # 建议先发 TestPyPI 演练
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
可选发布路径:GitHub 仓库 + Actions trusted publishing(打 tag 自动发布,免 token),
|
|
103
|
+
或直接 `pip install git+https://...`(零发布设施,适合私有/内网)。
|
|
104
|
+
|
|
105
|
+
## 依赖
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""boardctl — 开发板控制工具包(设计参考 crates.io 上的 ostool)
|
|
2
|
+
|
|
3
|
+
结构(松耦合,单向依赖):
|
|
4
|
+
config.py 板卡 TOML 加载(不依赖其他模块)
|
|
5
|
+
session.py U-Boot 串口会话(依赖 config)
|
|
6
|
+
power.py 电源控制与冷启动(依赖 config, session)
|
|
7
|
+
console.py 交互终端(依赖 config, session, power)
|
|
8
|
+
runner.py run/cmd 编排(依赖上述 + plugins)
|
|
9
|
+
cli.py 命令行入口,只做接线
|
|
10
|
+
plugins/ 插件(按目录约定自动发现,见 plugins/__init__.py)
|
|
11
|
+
"""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Sophgo SG2002 开发板(Cvitek / RISC-V 玄铁 C906 / U-Boot 2021.10)
|
|
2
|
+
description = "Sophgo SG2002 (RISC-V C906, U-Boot 2021.10, soph# 提示符)"
|
|
3
|
+
|
|
4
|
+
# 命令执行位置:留空 = power/exec 等命令在本机执行;
|
|
5
|
+
# 填 ssh 别名(如 "gem12")= 这些命令经 ssh 在该主机上执行(别名/端口/用户走 ~/.ssh/config)
|
|
6
|
+
ssh_host = ""
|
|
7
|
+
|
|
8
|
+
[serial]
|
|
9
|
+
url = "socket://172.16.0.7:5000" # 串口桥(gem12 的 VPN 地址),裸 TCP 透传
|
|
10
|
+
timeout = 0.2
|
|
11
|
+
|
|
12
|
+
[uboot]
|
|
13
|
+
prompt = "soph#"
|
|
14
|
+
load_addr = "0x80080000"
|
|
15
|
+
ip_addr = "192.168.1.201" # 设备自身 IP(静态)
|
|
16
|
+
server_ip = "192.168.1.9" # TFTP 服务器 = gem12 的 wlan 口
|
|
17
|
+
ensure_server_ip = true # 该构建无 saveenv,每次连接自动恢复 serverip
|
|
18
|
+
|
|
19
|
+
[power]
|
|
20
|
+
# 两种方式二选一(改 method 一行切换):
|
|
21
|
+
method = "mijia" # 方式一:原生插件,进程内直连小米云(参数见 [power.mijia])
|
|
22
|
+
# method = "command" # 方式二:特制开关机命令(用下面的 on_cmd/off_cmd/status_cmd)
|
|
23
|
+
reset_delay = 3 # 断电到上电的间隔(秒)
|
|
24
|
+
off_on_exit = true # console 退出时自动断电(任何退出路径)
|
|
25
|
+
# 方式二的命令(method = "command" 时生效;经顶层 ssh_host 决定本机/远端执行):
|
|
26
|
+
on_cmd = "./run.sh 1" # 开机命令
|
|
27
|
+
off_cmd = "./run.sh 0" # 关机命令
|
|
28
|
+
status_cmd = "mijiaAPI get --dev_name sg2002 --prop_name on" # 查状态(可选)
|
|
29
|
+
|
|
30
|
+
[power.mijia] # 方式一的参数:dev_name 或 did 二选一
|
|
31
|
+
dev_name = "sg2002" # 米家里的设备名(即插座命名)
|
|
32
|
+
|
|
33
|
+
[tftp]
|
|
34
|
+
method = "remote" # remote: scp 到远端 tftp 目录 | local: 本机起内置 tftp 服务器
|
|
35
|
+
ssh_host = "gem12"
|
|
36
|
+
remote_dir = "/srv/tftp" # 需一次性: ssh -t gem12 'sudo chown $USER /srv/tftp'
|
|
37
|
+
local_dir = "tftpboot" # method=local 时的根目录
|
|
38
|
+
|
|
39
|
+
[loady]
|
|
40
|
+
sender = "/usr/bin/lrzsz-sb" # Arch 的 lrzsz;Debian/Ubuntu 为 /usr/bin/sb
|
|
41
|
+
|
|
42
|
+
# 一键启动目标:boardctl.py -b sg2002 run <名字>(省略名字列出全部)
|
|
43
|
+
[run.hello]
|
|
44
|
+
desc = "RISC-V 裸机测试(冷启动→go 跳转→5 行标记→断电收尾)"
|
|
45
|
+
file = "tests/baremetal/hello.bin"
|
|
46
|
+
addr = "0x80080000" # 加载地址(send/run 默认取 uboot.load_addr,可省)
|
|
47
|
+
# entry = "0x80080010" # 跳转地址(go 的目标);缺省 = addr。加载和入口不同时用它
|
|
48
|
+
exec = "go" # go / source / none(只加载不执行)
|
|
49
|
+
method = "tftp"
|
|
50
|
+
timeout = 8 # 执行后收集输出的秒数
|
|
51
|
+
reset_before = true # 关 → 开 → 等 U-Boot 提示符,从确定状态开始
|
|
52
|
+
after = "off" # 收尾:off 断电 | reset 重启回提示符 | none 保持
|
|
53
|
+
expect = [ # 输出必须全部包含这些标记才算 PASS(缺省则不判定)
|
|
54
|
+
"BM-TEST-START",
|
|
55
|
+
"BM-COUNTER-1",
|
|
56
|
+
"BM-COUNTER-2",
|
|
57
|
+
"BM-COUNTER-3",
|
|
58
|
+
"BM-TEST-DONE",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[run.script]
|
|
62
|
+
desc = "U-Boot 脚本(source 执行,三标记后干净返回,设备保持开机)"
|
|
63
|
+
file = "tests/script/test_uboot.scr"
|
|
64
|
+
exec = "source"
|
|
65
|
+
method = "tftp"
|
|
66
|
+
timeout = 8
|
|
67
|
+
expect = ["SCRIPT-START", "SCRIPT-MIDDLE", "SCRIPT-DONE"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""命令行入口:参数解析与子命令分发,只做接线不含业务逻辑"""
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import power, runner, shell
|
|
7
|
+
from .config import available_boards, load_board
|
|
8
|
+
from .console import do_console
|
|
9
|
+
from .plugins import TRANSPORT
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
ap = argparse.ArgumentParser(
|
|
14
|
+
prog='boardctl',
|
|
15
|
+
description='开发板控制工具(参考 ostool 设计,每板一个 boards/*.toml,插件化传输/执行)')
|
|
16
|
+
ap.add_argument('-b', '--board', default='sg2002', help='开发板名(boards/ 下的 TOML 文件名)')
|
|
17
|
+
sub = ap.add_subparsers(dest='op', required=True)
|
|
18
|
+
|
|
19
|
+
sub.add_parser('console', help='交互式串口终端')
|
|
20
|
+
p_power = sub.add_parser('power', help='电源控制')
|
|
21
|
+
p_power.add_argument('state', choices=['on', 'off', 'status'])
|
|
22
|
+
sub.add_parser('reset', help='断电重启')
|
|
23
|
+
p_cmd = sub.add_parser('cmd', help='在 U-Boot 提示符执行命令')
|
|
24
|
+
p_cmd.add_argument('commands', nargs='+')
|
|
25
|
+
p_send = sub.add_parser('send', help='向开发板传输文件')
|
|
26
|
+
p_send.add_argument('file')
|
|
27
|
+
p_send.add_argument('--method', choices=sorted(TRANSPORT) or ['loady'],
|
|
28
|
+
default='loady', help='传输插件')
|
|
29
|
+
p_send.add_argument('--addr', help='加载地址(默认取板卡 uboot.load_addr)')
|
|
30
|
+
p_run = sub.add_parser('run', help='按板卡配置的 [run.<名字>] 一键启动')
|
|
31
|
+
p_run.add_argument('name', nargs='?', help='启动目标名(省略则列出可用目标)')
|
|
32
|
+
p_exec = sub.add_parser('exec', help='在板卡命令环境执行 shell 命令(配置 ssh_host 则经 ssh 远端执行)')
|
|
33
|
+
p_exec.add_argument('command', nargs='+')
|
|
34
|
+
sub.add_parser('boards', help='列出开发板')
|
|
35
|
+
|
|
36
|
+
args = ap.parse_args()
|
|
37
|
+
if args.op == 'boards':
|
|
38
|
+
boards = available_boards()
|
|
39
|
+
if not boards:
|
|
40
|
+
sys.exit('没有找到任何板卡配置(搜索:$BOARDCTL_BOARDS → ./boards → ~/.config/boardctl/boards → 包内置)')
|
|
41
|
+
for name in sorted(boards):
|
|
42
|
+
cfg = load_board(name)
|
|
43
|
+
desc = f' — {cfg["description"]}' if cfg['description'] else ''
|
|
44
|
+
print(f'{cfg["name"]}{desc}')
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
cfg = load_board(args.board)
|
|
48
|
+
if args.op == 'console':
|
|
49
|
+
do_console(cfg)
|
|
50
|
+
elif args.op == 'power':
|
|
51
|
+
power.do_power(cfg, args.state)
|
|
52
|
+
elif args.op == 'reset':
|
|
53
|
+
power.do_reset(cfg)
|
|
54
|
+
elif args.op == 'cmd':
|
|
55
|
+
runner.do_cmd(cfg, args.commands)
|
|
56
|
+
elif args.op == 'send':
|
|
57
|
+
if not os.path.isfile(args.file):
|
|
58
|
+
sys.exit(f'文件不存在: {args.file}')
|
|
59
|
+
addr = args.addr or cfg['uboot']['load_addr']
|
|
60
|
+
ok = TRANSPORT[args.method].send(cfg, args.file, addr)
|
|
61
|
+
sys.exit(0 if ok else 1)
|
|
62
|
+
elif args.op == 'run':
|
|
63
|
+
runner.do_run(cfg, args.name)
|
|
64
|
+
elif args.op == 'exec':
|
|
65
|
+
r = shell.run(cfg, ' '.join(args.command), check=False, capture=True)
|
|
66
|
+
sys.stdout.write(r.stdout or '')
|
|
67
|
+
sys.stderr.write(r.stderr or '')
|
|
68
|
+
sys.exit(r.returncode)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""板卡配置加载:boards/*.toml + 默认值合并(TOML 同名键覆盖)"""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# 项目根目录(本包的上一级):开发运行时 boards/、run.sh 等在这里
|
|
8
|
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
9
|
+
|
|
10
|
+
# 全局默认值(仅真正跨模块的段;插件自己的默认值由插件模块的 DEFAULTS 提供)
|
|
11
|
+
DEFAULTS = {
|
|
12
|
+
'serial': {'url': 'socket://localhost:5000', 'timeout': 0.2},
|
|
13
|
+
'uboot': {
|
|
14
|
+
'prompt': '=>',
|
|
15
|
+
'load_addr': '0x80080000',
|
|
16
|
+
'ip_addr': '',
|
|
17
|
+
'server_ip': '',
|
|
18
|
+
'ensure_server_ip': False,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def boards_dirs():
|
|
24
|
+
"""板卡配置目录搜索顺序(去重,仅保留存在的):
|
|
25
|
+
$BOARDCTL_BOARDS → 当前目录 boards/ → ~/.config/boardctl/boards → 包内置示例
|
|
26
|
+
"""
|
|
27
|
+
candidates = [
|
|
28
|
+
os.environ.get('BOARDCTL_BOARDS'),
|
|
29
|
+
os.path.join(os.getcwd(), 'boards'),
|
|
30
|
+
str(Path.home() / '.config' / 'boardctl' / 'boards'),
|
|
31
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'boards'),
|
|
32
|
+
]
|
|
33
|
+
seen, out = set(), []
|
|
34
|
+
for d in candidates:
|
|
35
|
+
if d:
|
|
36
|
+
d = os.path.abspath(d)
|
|
37
|
+
if d not in seen and os.path.isdir(d):
|
|
38
|
+
seen.add(d)
|
|
39
|
+
out.append(d)
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def available_boards():
|
|
44
|
+
"""全部可用板卡名(按目录优先级去重,先出现的优先)"""
|
|
45
|
+
names = {}
|
|
46
|
+
for d in boards_dirs():
|
|
47
|
+
for f in sorted(os.listdir(d)):
|
|
48
|
+
if f.endswith('.toml'):
|
|
49
|
+
names.setdefault(f[:-5], os.path.join(d, f))
|
|
50
|
+
return names
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_board(name):
|
|
54
|
+
boards = available_boards()
|
|
55
|
+
if name not in boards:
|
|
56
|
+
sys.exit(f"未知开发板 {name!r},可用: {' '.join(sorted(boards)) or '(没有任何 boards/ 目录里有配置)'}")
|
|
57
|
+
with open(boards[name], 'rb') as f:
|
|
58
|
+
data = tomllib.load(f)
|
|
59
|
+
cfg = {'name': name, 'description': data.get('description', ''),
|
|
60
|
+
'ssh_host': data.get('ssh_host', '')}
|
|
61
|
+
for section, defaults in DEFAULTS.items():
|
|
62
|
+
cfg[section] = {**defaults, **data.get(section, {})}
|
|
63
|
+
cfg['power'] = dict(data.get('power', {}))
|
|
64
|
+
cfg['tftp'] = dict(data.get('tftp', {}))
|
|
65
|
+
cfg['run'] = data.get('run', {})
|
|
66
|
+
|
|
67
|
+
# 插件自带默认值:按插件的 CFG_SECTION 声明合并(TOML 值优先)。
|
|
68
|
+
# 函数内 import,避免 config <-> plugins 模块级循环依赖
|
|
69
|
+
from .plugins import all_plugins
|
|
70
|
+
for mod in all_plugins():
|
|
71
|
+
section = getattr(mod, 'CFG_SECTION', None)
|
|
72
|
+
defaults = getattr(mod, 'DEFAULTS', None)
|
|
73
|
+
if section and isinstance(defaults, dict):
|
|
74
|
+
merged = dict(defaults)
|
|
75
|
+
merged.update(cfg.get(section, {}))
|
|
76
|
+
cfg[section] = merged
|
|
77
|
+
return cfg
|