refactor: organize factory host release layout
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
logs/
|
build/
|
||||||
dist/
|
dist/
|
||||||
.pyinstaller-work/
|
vendor/
|
||||||
.pyinstaller-spec/
|
|
||||||
|
|||||||
+84
-73
@@ -1,94 +1,105 @@
|
|||||||
# EG800 FT1 工厂烧录与产测上位机
|
# EG800 FT1 工厂烧录与产测上位机
|
||||||
|
|
||||||
本工具配套 `test/factory-test` 分支的独立产测固件,支持批量烧录、DBG UART
|
本目录包含上位机源码、默认配置、测试、构建脚本和发布文档。最终交付物统一生成到
|
||||||
协议采集、6 个硬件节点判定和结果落盘。
|
`factory_host/dist/`,工厂不需要接触项目源码。
|
||||||
|
|
||||||
## 主要功能
|
## 工程结构
|
||||||
|
|
||||||
- 产测固件和正式固件分开保存,每个工位可独立选择。
|
|
||||||
- 烧录串口和产测 DBG UART 分开配置,避免把 DIAG/AT/调试口误认成烧录口。
|
|
||||||
- 主界面提供醒目的“启动自动烧录”:每 0.5 秒持续检测烧录串口,先让下载器按工位进入等待,再由设备上电触发烧录。
|
|
||||||
- 每个工位完成后会锁定当前设备;只有检测到该端口消失并重新出现(断开旧设备、连接下一台并上电)才会再次烧录,避免同一设备反复烧录。
|
|
||||||
- 烧录与串口扫描均在后台运行,Windows 子进程隐藏控制台窗口,不阻塞界面。
|
|
||||||
- 烧录和产测运行期间实时日志强制跟随最新一行,停止后才允许手工滚动。
|
|
||||||
- 只解析以 `FT1 ` 开头的协议行;普通 SDK 日志只显示,不进入产测数据库。
|
|
||||||
- 严格校验协议版本、消息类型、节点编号/名称、会话 ID 和递增序号。
|
|
||||||
- 显示 `BOOT_AUDIO`、`POWER_ADC`、`AUDIO_LOOPBACK`、`ESIM`、
|
|
||||||
`DATA_CALL`、`NFC` 的实时状态与结构化数据。
|
|
||||||
- 收到 `session_result` 或 `power_off` 时自动保存完整 JSON 和汇总 CSV。
|
|
||||||
- 保存 IMEI、ICCID、NFC UID、失败码、固件版本、节点耗时和总耗时。
|
|
||||||
|
|
||||||
## 目录和启动
|
|
||||||
|
|
||||||
完整烧录工具位于:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe
|
factory_host/
|
||||||
|
├─ src/ # Python 上位机源码
|
||||||
|
│ ├─ factory_host.py
|
||||||
|
│ └─ ft1_protocol.py
|
||||||
|
├─ config/ # 开发和发布使用的默认配置
|
||||||
|
│ └─ factory_host_config.json
|
||||||
|
├─ scripts/ # 音频、固件和 release 构建脚本
|
||||||
|
│ ├─ build_factory_audio_package.py
|
||||||
|
│ ├─ build_factory_firmware.ps1
|
||||||
|
│ └─ build_release.ps1
|
||||||
|
├─ tests/ # 自动测试
|
||||||
|
│ └─ test_factory_host.py
|
||||||
|
├─ docs/
|
||||||
|
│ ├─ USAGE.md # 工厂现场操作说明
|
||||||
|
│ └─ DEVELOPMENT.md # 开发、换包和发布说明
|
||||||
|
├─ firmware/ # 默认产测与正式固件
|
||||||
|
├─ vendor/ # 本地 Aboot 厂商工具,不提交
|
||||||
|
├─ build/ # 中间产物和开发日志,不提交
|
||||||
|
├─ dist/ # 最终交付目录,每次发布会重建
|
||||||
|
├─ requirements.txt
|
||||||
|
└─ README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
发布程序:
|
`build/` 和 `dist/` 都被 Git 忽略。发布脚本只会清理 `dist/`,不会修改 `src/`、
|
||||||
|
`config/`、`docs/`、`tests/` 或 `build/`。
|
||||||
|
|
||||||
```text
|
## 功能
|
||||||
factory_host/dist/EG800FactoryHost.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
默认“产测固件”和“正式固件”分别位于:
|
- 产测固件与正式固件独立保存并醒目标识。
|
||||||
|
- 持续检测烧录串口,支持多工位自动等待设备上电后烧录。
|
||||||
|
- 同一设备烧录成功后锁定,端口断开并重新出现后才允许烧录下一台。
|
||||||
|
- 自动识别 DBG UART,解析设备每 500 ms 广播的 FT1 状态。
|
||||||
|
- 显示六个测试节点、IMEI、ICCID、NFC UID、失败码和实时日志。
|
||||||
|
- 自动保存汇总 CSV 与单机 JSON,烧录和扫描过程不弹出控制台黑框。
|
||||||
|
|
||||||
```text
|
## 快速验证
|
||||||
burn_tool/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip
|
|
||||||
burn_tool/firmware/official/EG800AKCN_11LG-official-fac.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
两者都可通过主界面切换并选择,也可在外置
|
在项目根目录执行:
|
||||||
`factory_host_config.json` 中替换,烧录时不要选择 `-target.zip` 外层归档。
|
|
||||||
|
|
||||||
双击表格中的工位可以分别设置烧录口和产测 DBG UART 口。默认 USB 自动等待模式不需要
|
|
||||||
|
|
||||||
填写烧录口;产测 UART 可留空并自动识别。“烧录选中”要求只选择一个工位;“批量烧录”
|
|
||||||
在未选中行时作用于全部工位,选中后只作用于选中工位。
|
|
||||||
|
|
||||||
单工位烧录时保持设备关机并连接 USB,选择工位后点击“烧录选中”。工位状态显示
|
|
||||||
“等待上电”后再给设备开机。此时 `adownload` 会捕获 AROM USB 下载口并开始烧录,
|
|
||||||
操作顺序与 Aboot 的 `usbOnly + usbAutoEnable` 默认设置一致。
|
|
||||||
|
|
||||||
串口和协议记录位于:
|
|
||||||
|
|
||||||
```text
|
|
||||||
factory_host/dist/logs/factory_results.csv
|
|
||||||
factory_host/dist/logs/sessions/*.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## FT1 结果规则
|
|
||||||
|
|
||||||
产测结果完全采用固件的 `session_result`,上位机不会用模糊正则猜测硬件是否通过。
|
|
||||||
非零失败码会显示 `docs/factory_test.md` 中对应的中文含义。收到 `power_off` 后会再次
|
|
||||||
刷新同一 JSON 文件,但不会在 CSV 中重复增加记录。
|
|
||||||
|
|
||||||
`AUDIO_LOOPBACK=PASS` 表示 GPIO84(原三击测试按键)的录放数据链路成功。
|
|
||||||
Loopback 仍然是按住录音、松开回放,最长录音 5 秒。
|
|
||||||
|
|
||||||
## 产测音频包
|
|
||||||
|
|
||||||
重新生成音频包:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python -B .\factory_host\build_factory_audio_package.py
|
python -m pip install -r .\factory_host\requirements.txt
|
||||||
|
python -B -m unittest discover -s .\factory_host\tests -p "test_*.py" -v
|
||||||
|
python -B .\factory_host\src\factory_host.py
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本会执行以下检查:
|
源码模式会读取 `factory_host/config/factory_host_config.json`,运行日志写入
|
||||||
|
`factory_host/build/logs/`,固件和下载器从现有 `factory_host/dist/` 解析。
|
||||||
|
|
||||||
1. 源目录必须且只能包含五个固定文件名。
|
## 生成 Release
|
||||||
2. 每个文件必须为 16 kHz、单声道、48 kbps MP3。
|
|
||||||
3. 使用 SDK `mklfs-v2.exe -m` 生成 262144 字节 `customer_fs.bin`。
|
|
||||||
4. 立即反向解包镜像并逐文件比对 SHA-256。
|
|
||||||
5. 输出 ZIP、清单及校验值到 `factory_host/dist/factory_audio/`。
|
|
||||||
|
|
||||||
不要直接覆盖正式业务 `customer_fs.bin`。需要构建产测整包时,按照
|
默认使用项目中的以下输入:
|
||||||
`docs/factory_test_windows_fs.md` 先备份业务镜像,再临时放入产测镜像。
|
|
||||||
|
|
||||||
## 开发验证
|
```text
|
||||||
|
factory_host/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip
|
||||||
|
factory_host/firmware/official/EG800AKCN_11LG-official-fac.zip
|
||||||
|
factory_host/vendor/aboot-tools-2024.08.08-win-x64/
|
||||||
|
```
|
||||||
|
|
||||||
|
执行:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python -B -m unittest discover -s factory_host -p "test_*.py" -v
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\scripts\build_release.ps1
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File factory_host\build_release.ps1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
生成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
factory_host/dist/
|
||||||
|
├─ START-EG800-FACTORY-TOOL.exe
|
||||||
|
├─ firmware/
|
||||||
|
│ ├─ EG800AKCN_11LG-FT1-factory-test.zip
|
||||||
|
│ └─ EG800AKCN_11LG-official-fac.zip
|
||||||
|
├─ .runtime/ # Windows 隐藏目录
|
||||||
|
│ ├─ burn_tool/
|
||||||
|
│ ├─ docs/USAGE.md
|
||||||
|
│ ├─ factory_host_config.json
|
||||||
|
│ ├─ release_manifest.json
|
||||||
|
│ └─ logs/
|
||||||
|
└─ EG800-Factory-Tool-Full-Package.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
交付工厂时只发送 `EG800-Factory-Tool-Full-Package.zip`。解压后双击醒目的
|
||||||
|
`START-EG800-FACTORY-TOOL.exe`;不要单独移动 EXE、固件或 `.runtime`。
|
||||||
|
|
||||||
|
默认配置使用相对于发布目录的路径:
|
||||||
|
|
||||||
|
```text
|
||||||
|
firmware/EG800AKCN_11LG-FT1-factory-test.zip
|
||||||
|
firmware/EG800AKCN_11LG-official-fac.zip
|
||||||
|
.runtime/burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- 工厂操作:[docs/USAGE.md](docs/USAGE.md)
|
||||||
|
- 开发、更新固件与发布:[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)
|
||||||
|
- 固件侧 FT1 协议:[../docs/factory_test.md](../docs/factory_test.md)
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
param(
|
|
||||||
[string]$Output = "dist"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
||||||
python -c "import PyInstaller" 2>$null
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "未找到 PyInstaller。先执行: python -m pip install pyinstaller pyserial"
|
|
||||||
}
|
|
||||||
|
|
||||||
$outDir = Join-Path $root $Output
|
|
||||||
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
|
||||||
python -m PyInstaller --noconfirm --clean --onefile --windowed `
|
|
||||||
--name EG800FactoryHost `
|
|
||||||
--distpath $outDir `
|
|
||||||
--workpath (Join-Path $root ".pyinstaller-work") `
|
|
||||||
--specpath (Join-Path $root ".pyinstaller-spec") `
|
|
||||||
(Join-Path $root "factory_host.py")
|
|
||||||
Copy-Item (Join-Path $root "factory_host_config.json") $outDir -Force
|
|
||||||
Write-Host "输出: $(Join-Path $outDir 'EG800FactoryHost.exe')"
|
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"flash": {
|
"flash": {
|
||||||
"aboot_path": "burn_tool/aboot.exe",
|
"aboot_path": ".runtime/burn_tool/aboot-tools-2024.08.08-win-x64/aboot.exe",
|
||||||
"downloader_path": "burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe",
|
"downloader_path": ".runtime/burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe",
|
||||||
"args": [
|
"args": [
|
||||||
"-u",
|
"-u",
|
||||||
"-a",
|
"-a",
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
"workers": 2
|
"workers": 2
|
||||||
},
|
},
|
||||||
"firmwares": {
|
"firmwares": {
|
||||||
"production": "burn_tool/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip",
|
"production": "firmware/EG800AKCN_11LG-FT1-factory-test.zip",
|
||||||
"official": "burn_tool/firmware/official/EG800AKCN_11LG-official-fac.zip"
|
"official": "firmware/EG800AKCN_11LG-official-fac.zip"
|
||||||
},
|
},
|
||||||
"serial": {
|
"serial": {
|
||||||
"baudrate": 115200,
|
"baudrate": 115200,
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# EG800 产测上位机开发与发布
|
||||||
|
|
||||||
|
## 1. 目录职责
|
||||||
|
|
||||||
|
| 目录 | 用途 | 是否提交 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/` | 上位机与 FT1 协议源码 | 是 |
|
||||||
|
| `config/` | 默认发布配置 | 是 |
|
||||||
|
| `scripts/` | 音频、固件、上位机发布脚本 | 是 |
|
||||||
|
| `tests/` | 单元测试 | 是 |
|
||||||
|
| `docs/` | 工厂与开发文档 | 是 |
|
||||||
|
| `firmware/` | 默认产测与正式固件 | 是,二进制由 Git LFS 管理 |
|
||||||
|
| `vendor/` | 完整 Aboot 厂商工具 | 否,本地依赖 |
|
||||||
|
| `build/` | 中间产物、备份和开发日志 | 否 |
|
||||||
|
| `dist/` | 最终 release 目录和 ZIP | 否 |
|
||||||
|
|
||||||
|
不要在 `dist/` 保存需要长期保留的文件。`build_release.ps1` 每次发布前都会校验目标位于
|
||||||
|
`factory_host` 内,然后清空并重建目标目录。
|
||||||
|
|
||||||
|
## 2. 环境准备
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install -r .\factory_host\requirements.txt
|
||||||
|
python -m pip install pyinstaller
|
||||||
|
```
|
||||||
|
|
||||||
|
完整固件构建还需要当前 EG800 SDK 的编译环境能够正常执行 `build.bat app` 和
|
||||||
|
`build.bat firmware`。
|
||||||
|
|
||||||
|
发布还要求本机存在完整厂商工具目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
factory_host/vendor/aboot-tools-2024.08.08-win-x64/
|
||||||
|
```
|
||||||
|
|
||||||
|
`vendor/` 被 Git 忽略,不能只放一个 `adownload.exe`;请保留原工具的 DLL、配置、驱动和
|
||||||
|
平台文件。发布脚本缺少该目录时会直接报错,不会生成残缺包。
|
||||||
|
|
||||||
|
## 3. 开发与测试
|
||||||
|
|
||||||
|
运行源码:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -B .\factory_host\src\factory_host.py
|
||||||
|
```
|
||||||
|
|
||||||
|
运行全部测试:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -B -m unittest discover -s .\factory_host\tests -p "test_*.py" -v
|
||||||
|
```
|
||||||
|
|
||||||
|
源码模式读取 `factory_host/config/factory_host_config.json`,日志写入
|
||||||
|
`factory_host/build/logs/`。默认相对固件路径会解析到 `factory_host/dist/firmware/`。
|
||||||
|
|
||||||
|
## 4. 更新产测音频和固件
|
||||||
|
|
||||||
|
音频源位于:
|
||||||
|
|
||||||
|
```text
|
||||||
|
audio_prompts/factory_test/customer_fs/
|
||||||
|
```
|
||||||
|
|
||||||
|
生成并验证音频文件系统:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -B .\factory_host\scripts\build_factory_audio_package.py
|
||||||
|
```
|
||||||
|
|
||||||
|
输出位于 `factory_host/build/audio/`。脚本会检查五个固定 MP3 的名称与格式,生成
|
||||||
|
262144 字节 `customer_fs.bin`,反向解包并逐文件核对 SHA-256。
|
||||||
|
|
||||||
|
生成产测完整固件:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\scripts\build_factory_firmware.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
输出位于 `factory_host/build/firmware/`。脚本会临时替换 SDK 的业务
|
||||||
|
`customer_fs.bin`,完成后恢复原文件并核对哈希。恢复失败时不得继续发布。
|
||||||
|
|
||||||
|
确认新固件后,将其更新为默认产测固件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
factory_host/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以不覆盖默认文件,在发布时通过 `-FactoryFirmware` 显式选择。
|
||||||
|
|
||||||
|
## 5. 生成交付包
|
||||||
|
|
||||||
|
使用默认两份固件:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\scripts\build_release.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
使用指定固件,路径相对于项目根目录,也支持绝对路径:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||||
|
-File .\factory_host\scripts\build_release.ps1 `
|
||||||
|
-FactoryFirmware "factory_host\build\firmware\EG800AKCN_11LG-FT1-factory-test.zip" `
|
||||||
|
-OfficialFirmware "factory_host\firmware\official\EG800AKCN_11LG-official-fac.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
发布脚本会:
|
||||||
|
|
||||||
|
1. 清空并重建 `factory_host/dist/`。
|
||||||
|
2. 从 `src/` 生成无控制台窗口的单文件 EXE。
|
||||||
|
3. 复制完整 Aboot 工具目录,不能只复制 `adownload.exe`。
|
||||||
|
4. 复制产测与正式固件到 `dist/firmware/`。
|
||||||
|
5. 把配置、使用说明、下载器和日志目录放入 `.runtime/`。
|
||||||
|
6. 生成包含 Git revision、文件大小和 SHA-256 的 `release_manifest.json`。
|
||||||
|
7. 生成 `EG800-Factory-Tool-Full-Package.zip`。
|
||||||
|
|
||||||
|
## 6. 发布检查
|
||||||
|
|
||||||
|
发布前至少执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -B -m unittest discover -s .\factory_host\tests -p "test_*.py" -v
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\scripts\build_release.ps1
|
||||||
|
tar.exe -tf .\factory_host\dist\EG800-Factory-Tool-Full-Package.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
ZIP 顶层只能包含:
|
||||||
|
|
||||||
|
```text
|
||||||
|
START-EG800-FACTORY-TOOL.exe
|
||||||
|
firmware/
|
||||||
|
.runtime/
|
||||||
|
```
|
||||||
|
|
||||||
|
还应在一台真实设备上验证:自动等待上电烧录、断开后才允许下一台、DBG UART 自动识别、
|
||||||
|
六项 FT1 状态、JSON/CSV 保存,以及产测固件和正式固件切换。
|
||||||
|
|
||||||
|
## 7. 固件替换规则
|
||||||
|
|
||||||
|
- 产测与正式固件都必须是 Aboot 可直接烧录的内层 ZIP,不要选择外层 `*-target.zip`。
|
||||||
|
- 默认文件名保持不变时无需修改配置。
|
||||||
|
- 修改文件名时同步更新 `config/factory_host_config.json`。
|
||||||
|
- 每次发布核对 `.runtime/release_manifest.json` 中的大小和 SHA-256。
|
||||||
|
- 不要把源码、`build/`、SDK 编译目录或历史日志交付给工厂。
|
||||||
@@ -5,42 +5,43 @@
|
|||||||
现场使用时至少保留以下内容,并保持相对目录不变:
|
现场使用时至少保留以下内容,并保持相对目录不变:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
banban/
|
factory_host/dist/
|
||||||
├─ burn_tool/
|
├─ START-EG800-FACTORY-TOOL.exe
|
||||||
│ ├─ aboot-tools-2024.08.08-win-x64/
|
├─ firmware/
|
||||||
│ ├─ adownload.exe
|
│ ├─ EG800AKCN_11LG-FT1-factory-test.zip
|
||||||
│ └─ 其他随工具提供的 DLL、配置和驱动文件
|
│ └─ EG800AKCN_11LG-official-fac.zip
|
||||||
│ └─ firmware/
|
└─ .runtime/
|
||||||
│ ├─ test/EG800AKCN_11LG-FT1-factory-test.zip
|
├─ factory_host_config.json
|
||||||
│ └─ official/EG800AKCN_11LG-official-fac.zip
|
├─ burn_tool/
|
||||||
├─ factory_host/
|
│ └─ aboot-tools-2024.08.08-win-x64/
|
||||||
│ └─ dist/
|
│ ├─ adownload.exe
|
||||||
│ ├─ EG800FactoryHost.exe
|
│ └─ 其他随工具提供的 DLL、配置和驱动文件
|
||||||
│ ├─ factory_host_config.json
|
├─ docs/USAGE.md
|
||||||
│ └─ factory_firmware/
|
├─ release_manifest.json
|
||||||
│ └─ EG800AKCN_11LG-FT1-factory-test.zip
|
└─ logs/
|
||||||
└─ version/
|
|
||||||
└─ 正式固件目录/
|
|
||||||
└─ *-fac.zip
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`adownload.exe` 不能脱离原来的工具目录单独复制,否则可能缺少 DLL、配置或驱动。
|
`adownload.exe` 不能脱离原来的工具目录单独复制,否则可能缺少 DLL、配置或驱动。
|
||||||
|
|
||||||
|
工厂现场只需接收并解压 `EG800-Factory-Tool-Full-Package.zip`,然后双击根目录的
|
||||||
|
`START-EG800-FACTORY-TOOL.exe`。不要把 `factory_host` 源码目录交付给现场;`.runtime`
|
||||||
|
用于保存下载器、配置和日志,程序启动时会自动将其设为 Windows 隐藏目录,不应移动或删除。
|
||||||
|
|
||||||
## 2. 首次配置
|
## 2. 首次配置
|
||||||
|
|
||||||
上位机读取与 EXE 同目录的 `factory_host_config.json`。推荐配置:
|
上位机读取 `.runtime/factory_host_config.json`。推荐配置:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"flash": {
|
"flash": {
|
||||||
"downloader_path": "burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe",
|
"downloader_path": ".runtime/burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe",
|
||||||
"args": ["-u", "-a", "-s", "{baudrate}", "-q", "{firmware}"],
|
"args": ["-u", "-a", "-s", "{baudrate}", "-q", "{firmware}"],
|
||||||
"timeout_seconds": 900,
|
"timeout_seconds": 900,
|
||||||
"workers": 2
|
"workers": 2
|
||||||
},
|
},
|
||||||
"firmwares": {
|
"firmwares": {
|
||||||
"production": "burn_tool/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip",
|
"production": "firmware/EG800AKCN_11LG-FT1-factory-test.zip",
|
||||||
"official": "burn_tool/firmware/official/EG800AKCN_11LG-official-fac.zip"
|
"official": "firmware/EG800AKCN_11LG-official-fac.zip"
|
||||||
},
|
},
|
||||||
"serial": {
|
"serial": {
|
||||||
"baudrate": 115200,
|
"baudrate": 115200,
|
||||||
@@ -57,13 +58,13 @@ banban/
|
|||||||
|
|
||||||
- `production`:产测固件,包含 FT1 测试程序、开机/Loopback/NFC/关机音频,以及暂不参与流程的 GNSS 音频。
|
- `production`:产测固件,包含 FT1 测试程序、开机/Loopback/NFC/关机音频,以及暂不参与流程的 GNSS 音频。
|
||||||
- `official`:正式业务固件,供产测完成后换烧。
|
- `official`:正式业务固件,供产测完成后换烧。
|
||||||
- 固件路径可以使用相对于项目根目录的路径,也可以填写绝对路径。
|
- 固件路径可以使用相对于 EXE 所在发布目录的路径,也可以填写绝对路径。
|
||||||
- 必须选择可直接烧录的内层 FT1 ZIP 或 `*-fac.zip`,不要选择外层 `*-target.zip` 归档。
|
- 必须选择可直接烧录的内层 FT1 ZIP 或 `*-fac.zip`,不要选择外层 `*-target.zip` 归档。
|
||||||
- 修改配置后重新启动上位机,或使用界面中的重新加载配置功能。
|
- 修改配置后重新启动上位机,或使用界面中的重新加载配置功能。
|
||||||
|
|
||||||
## 3. 烧录产测固件
|
## 3. 烧录产测固件
|
||||||
|
|
||||||
1. 启动 `EG800FactoryHost.exe`,固件类型选择“产测固件”或“正式固件”,确认顶部完整路径。
|
1. 启动 `START-EG800-FACTORY-TOOL.exe`,固件类型选择“产测固件”或“正式固件”,确认顶部完整路径。
|
||||||
2. 连接一个或多个烧录器,设备保持关机。
|
2. 连接一个或多个烧录器,设备保持关机。
|
||||||
3. 点击醒目的“启动自动烧录”,工具每 0.5 秒持续检测烧录串口,并为每个串口建立工位。
|
3. 点击醒目的“启动自动烧录”,工具每 0.5 秒持续检测烧录串口,并为每个串口建立工位。
|
||||||
4. 工位显示“烧录中·等待上电”后给对应设备开机,设备进入 AROM 后自动开始烧录。
|
4. 工位显示“烧录中·等待上电”后给对应设备开机,设备进入 AROM 后自动开始烧录。
|
||||||
@@ -98,8 +99,8 @@ banban/
|
|||||||
默认自动保存位置:
|
默认自动保存位置:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
factory_host/dist/logs/factory_results.csv
|
factory_host/dist/.runtime/logs/factory_results.csv
|
||||||
factory_host/dist/logs/sessions/*.json
|
factory_host/dist/.runtime/logs/sessions/*.json
|
||||||
```
|
```
|
||||||
|
|
||||||
- `factory_results.csv`:每台设备一行,适合生产汇总。
|
- `factory_results.csv`:每台设备一行,适合生产汇总。
|
||||||
@@ -144,23 +145,25 @@ audio_prompts/factory_test/customer_fs/
|
|||||||
文件必须保持脚本要求的名称和音频格式。更新后在项目根目录执行:
|
文件必须保持脚本要求的名称和音频格式。更新后在项目根目录执行:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python -B .\factory_host\build_factory_audio_package.py
|
python -B .\factory_host\scripts\build_factory_audio_package.py
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\build_factory_firmware.ps1
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\scripts\build_factory_firmware.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
生成物:
|
生成物:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
factory_host/dist/factory_audio/customer_fs.bin
|
factory_host/build/audio/customer_fs.bin
|
||||||
factory_host/dist/factory_audio/EG800AKCN_11LG-FT1-factory-audio.zip
|
factory_host/build/audio/EG800AKCN_11LG-FT1-factory-audio.zip
|
||||||
factory_host/dist/factory_firmware/EG800AKCN_11LG-FT1-factory-test.zip
|
factory_host/build/firmware/EG800AKCN_11LG-FT1-factory-test.zip
|
||||||
factory_host/dist/factory_firmware/firmware_manifest.json
|
factory_host/build/firmware/firmware_manifest.json
|
||||||
```
|
```
|
||||||
|
|
||||||
固件脚本会临时替换构建目录中的 `customer_fs.bin`,完成后自动恢复业务镜像并核对哈希。若脚本报告恢复失败,不要继续发布固件,应先检查备份和当前业务镜像。
|
固件脚本会临时替换构建目录中的 `customer_fs.bin`,完成后自动恢复业务镜像并核对哈希。若脚本报告恢复失败,不要继续发布固件,应先检查备份和当前业务镜像。
|
||||||
|
|
||||||
## 8. 重新打包上位机
|
## 8. 重新打包上位机
|
||||||
|
|
||||||
|
完整的工程结构、固件更新和发布参数见 `factory_host/docs/DEVELOPMENT.md`。
|
||||||
|
|
||||||
开发环境先安装依赖:
|
开发环境先安装依赖:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -171,8 +174,8 @@ python -m pip install pyinstaller
|
|||||||
运行测试并打包:
|
运行测试并打包:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python -B -m unittest discover -s factory_host -p "test_*.py" -v
|
python -B -m unittest discover -s .\factory_host\tests -p "test_*.py" -v
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\build_release.ps1
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\scripts\build_release.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
发布前至少使用一台设备验证:产测固件烧录、调试 UART 自动识别、START 握手、六项测试结果、JSON/CSV 保存,以及正式固件换烧。
|
发布前至少使用一台设备验证:产测固件烧录、调试 UART 自动识别、START 握手、六项测试结果、JSON/CSV 保存,以及正式固件换烧。
|
||||||
+2
-2
@@ -188,8 +188,8 @@ def build_package(root: Path, output: Path) -> tuple[Path, Path, dict]:
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent)
|
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])
|
||||||
parser.add_argument("--output", type=Path, default=Path(__file__).resolve().parent / "dist" / "factory_audio")
|
parser.add_argument("--output", type=Path, default=Path(__file__).resolve().parents[1] / "build" / "audio")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
image, package, manifest = build_package(args.root.resolve(), args.output.resolve())
|
image, package, manifest = build_package(args.root.resolve(), args.output.resolve())
|
||||||
print(f"customer_fs={image}")
|
print(f"customer_fs={image}")
|
||||||
+6
-5
@@ -1,14 +1,15 @@
|
|||||||
param(
|
param(
|
||||||
[string]$Output = "dist\factory_firmware"
|
[string]$Output = "build\firmware"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$toolRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
$projectRoot = Split-Path -Parent $toolRoot
|
$factoryHostRoot = Split-Path -Parent $scriptRoot
|
||||||
$audioImage = Join-Path $toolRoot "dist\factory_audio\customer_fs.bin"
|
$projectRoot = Split-Path -Parent $factoryHostRoot
|
||||||
|
$audioImage = Join-Path $factoryHostRoot "build\audio\customer_fs.bin"
|
||||||
$configuredImage = Join-Path $projectRoot "ql-config\config\EG800AKCN_11LG\customer_fs.bin"
|
$configuredImage = Join-Path $projectRoot "ql-config\config\EG800AKCN_11LG\customer_fs.bin"
|
||||||
$flashPackage = Join-Path $projectRoot "target\EG800AKCN_11LG\EG800AKCN_11LG.zip"
|
$flashPackage = Join-Path $projectRoot "target\EG800AKCN_11LG\EG800AKCN_11LG.zip"
|
||||||
$outDir = Join-Path $toolRoot $Output
|
$outDir = if ([System.IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $factoryHostRoot $Output }
|
||||||
$backup = Join-Path $outDir "customer_fs.business-backup.bin"
|
$backup = Join-Path $outDir "customer_fs.business-backup.bin"
|
||||||
$export = Join-Path $outDir "EG800AKCN_11LG-FT1-factory-test.zip"
|
$export = Join-Path $outDir "EG800AKCN_11LG-FT1-factory-test.zip"
|
||||||
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
param(
|
||||||
|
[string]$Output = "dist",
|
||||||
|
[string]$FactoryFirmware = "",
|
||||||
|
[string]$OfficialFirmware = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$factoryHostRoot = Split-Path -Parent $scriptRoot
|
||||||
|
$projectRoot = Split-Path -Parent $factoryHostRoot
|
||||||
|
$buildRoot = Join-Path $factoryHostRoot "build"
|
||||||
|
|
||||||
|
function Resolve-ProjectInput([string]$Value, [string]$DefaultRelativePath) {
|
||||||
|
$selected = if ([string]::IsNullOrWhiteSpace($Value)) { $DefaultRelativePath } else { $Value }
|
||||||
|
if ([System.IO.Path]::IsPathRooted($selected)) {
|
||||||
|
return [System.IO.Path]::GetFullPath($selected)
|
||||||
|
}
|
||||||
|
return [System.IO.Path]::GetFullPath((Join-Path $projectRoot $selected))
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputCandidate = if ([System.IO.Path]::IsPathRooted($Output)) {
|
||||||
|
$Output
|
||||||
|
} else {
|
||||||
|
Join-Path $factoryHostRoot $Output
|
||||||
|
}
|
||||||
|
$outDir = [System.IO.Path]::GetFullPath($outputCandidate)
|
||||||
|
$factoryHostPrefix = [System.IO.Path]::GetFullPath($factoryHostRoot).TrimEnd("\") + "\"
|
||||||
|
if (-not $outDir.StartsWith($factoryHostPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw "Output 必须位于 factory_host 目录内: $outDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyinstallerDist = Join-Path $buildRoot "pyinstaller-dist"
|
||||||
|
$pyinstallerWork = Join-Path $buildRoot "pyinstaller-work"
|
||||||
|
$pyinstallerSpec = Join-Path $buildRoot "pyinstaller-spec"
|
||||||
|
$buildExe = Join-Path $pyinstallerDist "EG800FactoryHost.exe"
|
||||||
|
$releaseExe = Join-Path $outDir "START-EG800-FACTORY-TOOL.exe"
|
||||||
|
$runtimeDir = Join-Path $outDir ".runtime"
|
||||||
|
$firmwareDir = Join-Path $outDir "firmware"
|
||||||
|
$toolSource = Join-Path $factoryHostRoot "vendor\aboot-tools-2024.08.08-win-x64"
|
||||||
|
$toolDestination = Join-Path $runtimeDir "burn_tool\aboot-tools-2024.08.08-win-x64"
|
||||||
|
$factoryFirmwareSource = Resolve-ProjectInput $FactoryFirmware "factory_host\firmware\test\EG800AKCN_11LG-FT1-factory-test.zip"
|
||||||
|
$officialFirmwareSource = Resolve-ProjectInput $OfficialFirmware "factory_host\firmware\official\EG800AKCN_11LG-official-fac.zip"
|
||||||
|
$factoryFirmwareDestination = Join-Path $firmwareDir "EG800AKCN_11LG-FT1-factory-test.zip"
|
||||||
|
$officialFirmwareDestination = Join-Path $firmwareDir "EG800AKCN_11LG-official-fac.zip"
|
||||||
|
$releaseZip = Join-Path $outDir "EG800-Factory-Tool-Full-Package.zip"
|
||||||
|
|
||||||
|
python -c "import PyInstaller" 2>$null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "未找到 PyInstaller。先执行: python -m pip install pyinstaller pyserial"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($required in @($toolSource, $factoryFirmwareSource, $officialFirmwareSource)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $required)) {
|
||||||
|
throw "发布所需文件不存在: $required"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# dist is disposable. Source and intermediate artifacts live elsewhere.
|
||||||
|
New-Item -ItemType Directory -Force -Path $outDir, $buildRoot | Out-Null
|
||||||
|
Get-ChildItem -LiteralPath $outDir -Force | Remove-Item -Recurse -Force
|
||||||
|
if (Test-Path -LiteralPath $pyinstallerDist) {
|
||||||
|
Remove-Item -LiteralPath $pyinstallerDist -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
python -m PyInstaller --noconfirm --clean --onefile --windowed `
|
||||||
|
--name EG800FactoryHost `
|
||||||
|
--distpath $pyinstallerDist `
|
||||||
|
--workpath $pyinstallerWork `
|
||||||
|
--specpath $pyinstallerSpec `
|
||||||
|
(Join-Path $factoryHostRoot "src\factory_host.py")
|
||||||
|
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $buildExe)) {
|
||||||
|
throw "PyInstaller 构建失败,退出码: $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $runtimeDir, $firmwareDir | Out-Null
|
||||||
|
Move-Item -LiteralPath $buildExe -Destination $releaseExe -Force
|
||||||
|
Copy-Item -LiteralPath (Join-Path $factoryHostRoot "config\factory_host_config.json") `
|
||||||
|
-Destination (Join-Path $runtimeDir "factory_host_config.json") -Force
|
||||||
|
Copy-Item -LiteralPath $toolSource -Destination $toolDestination -Recurse -Force
|
||||||
|
Copy-Item -LiteralPath $factoryFirmwareSource -Destination $factoryFirmwareDestination -Force
|
||||||
|
Copy-Item -LiteralPath $officialFirmwareSource -Destination $officialFirmwareDestination -Force
|
||||||
|
New-Item -ItemType Directory -Force -Path `
|
||||||
|
(Join-Path $runtimeDir "docs"), (Join-Path $runtimeDir "logs") | Out-Null
|
||||||
|
Copy-Item -LiteralPath (Join-Path $factoryHostRoot "docs\USAGE.md") `
|
||||||
|
-Destination (Join-Path $runtimeDir "docs\USAGE.md") -Force
|
||||||
|
|
||||||
|
$revision = ""
|
||||||
|
try {
|
||||||
|
$revisionOutput = & git -C $projectRoot rev-parse HEAD 2>$null
|
||||||
|
if ($LASTEXITCODE -eq 0 -and $revisionOutput) {
|
||||||
|
$revision = ($revisionOutput | Select-Object -First 1).Trim()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$revision = ""
|
||||||
|
}
|
||||||
|
$manifest = [ordered]@{
|
||||||
|
format = "eg800-factory-host-release-v1"
|
||||||
|
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
||||||
|
git_revision = $revision
|
||||||
|
files = [ordered]@{
|
||||||
|
executable = [ordered]@{
|
||||||
|
path = "START-EG800-FACTORY-TOOL.exe"
|
||||||
|
bytes = (Get-Item -LiteralPath $releaseExe).Length
|
||||||
|
sha256 = (Get-FileHash -LiteralPath $releaseExe -Algorithm SHA256).Hash
|
||||||
|
}
|
||||||
|
factory_firmware = [ordered]@{
|
||||||
|
path = "firmware/EG800AKCN_11LG-FT1-factory-test.zip"
|
||||||
|
bytes = (Get-Item -LiteralPath $factoryFirmwareDestination).Length
|
||||||
|
sha256 = (Get-FileHash -LiteralPath $factoryFirmwareDestination -Algorithm SHA256).Hash
|
||||||
|
}
|
||||||
|
official_firmware = [ordered]@{
|
||||||
|
path = "firmware/EG800AKCN_11LG-official-fac.zip"
|
||||||
|
bytes = (Get-Item -LiteralPath $officialFirmwareDestination).Length
|
||||||
|
sha256 = (Get-FileHash -LiteralPath $officialFirmwareDestination -Algorithm SHA256).Hash
|
||||||
|
}
|
||||||
|
downloader = [ordered]@{
|
||||||
|
path = ".runtime/burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe"
|
||||||
|
bytes = (Get-Item -LiteralPath (Join-Path $toolDestination "adownload.exe")).Length
|
||||||
|
sha256 = (Get-FileHash -LiteralPath (Join-Path $toolDestination "adownload.exe") -Algorithm SHA256).Hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$manifest | ConvertTo-Json -Depth 5 | Set-Content `
|
||||||
|
-LiteralPath (Join-Path $runtimeDir "release_manifest.json") -Encoding UTF8
|
||||||
|
|
||||||
|
$runtimeItem = Get-Item -LiteralPath $runtimeDir -Force
|
||||||
|
$runtimeItem.Attributes = $runtimeItem.Attributes -bor [System.IO.FileAttributes]::Hidden
|
||||||
|
Push-Location $outDir
|
||||||
|
try {
|
||||||
|
& tar.exe -a -c -f $releaseZip "START-EG800-FACTORY-TOOL.exe" "firmware" ".runtime"
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "tar.exe 创建完整交付包失败,退出码: $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "发布目录: $outDir"
|
||||||
|
Write-Host "上位机: $releaseExe"
|
||||||
|
Write-Host "完整交付包: $releaseZip"
|
||||||
|
Write-Host "SHA256: $((Get-FileHash -LiteralPath $releaseZip -Algorithm SHA256).Hash)"
|
||||||
@@ -32,34 +32,66 @@ CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
|
|||||||
|
|
||||||
|
|
||||||
APP_DIR = Path(sys.executable).resolve().parent if getattr(sys, "frozen", False) else Path(__file__).resolve().parent
|
APP_DIR = Path(sys.executable).resolve().parent if getattr(sys, "frozen", False) else Path(__file__).resolve().parent
|
||||||
|
FACTORY_HOST_DIR = APP_DIR if getattr(sys, "frozen", False) else APP_DIR.parent
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_hidden_directory(path: Path) -> None:
|
||||||
|
"""Restore the Windows hidden bit that ZIP extraction may discard."""
|
||||||
|
if os.name != "nt" or not path.is_dir():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||||
|
get_attributes = kernel32.GetFileAttributesW
|
||||||
|
get_attributes.argtypes = [ctypes.c_wchar_p]
|
||||||
|
get_attributes.restype = ctypes.c_uint32
|
||||||
|
set_attributes = kernel32.SetFileAttributesW
|
||||||
|
set_attributes.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32]
|
||||||
|
set_attributes.restype = ctypes.c_int
|
||||||
|
attributes = get_attributes(str(path))
|
||||||
|
if attributes != 0xFFFFFFFF:
|
||||||
|
set_attributes(str(path), attributes | 0x02)
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
ensure_hidden_directory(APP_DIR / ".runtime")
|
||||||
|
|
||||||
|
|
||||||
def discover_project_root(start: Path) -> Path:
|
def discover_project_root(start: Path) -> Path:
|
||||||
"""Find the checkout root from either the source or the dist directory."""
|
"""Find the checkout root from either the source or the dist directory."""
|
||||||
for candidate in (start, *start.parents):
|
for candidate in (start, *start.parents):
|
||||||
if (candidate / "burn_tool").exists() and ((candidate / "version").exists() or (candidate / "target").exists()):
|
if ((candidate / "factory_host").is_dir()
|
||||||
|
and (candidate / "ql-application").is_dir()
|
||||||
|
and (candidate / "ql-config").is_dir()):
|
||||||
return candidate
|
return candidate
|
||||||
return start.parent
|
return start.parent
|
||||||
|
|
||||||
|
|
||||||
ROOT_DIR = discover_project_root(APP_DIR)
|
ROOT_DIR = discover_project_root(FACTORY_HOST_DIR)
|
||||||
CONFIG_PATH = APP_DIR / "factory_host_config.json"
|
CONFIG_PATH = (APP_DIR / ".runtime" / "factory_host_config.json"
|
||||||
LOG_DIR = APP_DIR / "logs"
|
if getattr(sys, "frozen", False)
|
||||||
|
else FACTORY_HOST_DIR / "config" / "factory_host_config.json")
|
||||||
|
LOG_DIR = (APP_DIR / ".runtime" / "logs"
|
||||||
|
if getattr(sys, "frozen", False)
|
||||||
|
else FACTORY_HOST_DIR / "build" / "logs")
|
||||||
SESSION_DIR = LOG_DIR / "sessions"
|
SESSION_DIR = LOG_DIR / "sessions"
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"flash": {
|
"flash": {
|
||||||
"aboot_path": "burn_tool/aboot.exe",
|
"aboot_path": ".runtime/burn_tool/aboot-tools-2024.08.08-win-x64/aboot.exe",
|
||||||
"downloader_path": "burn_tool/adownload.exe",
|
"downloader_path": ".runtime/burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe",
|
||||||
"args": ["-u", "-a", "-s", "{baudrate}", "-q", "{firmware}"],
|
"args": ["-u", "-a", "-s", "{baudrate}", "-q", "{firmware}"],
|
||||||
"auto_args": ["-p", "{port}", "-a", "-s", "{baudrate}", "-q", "{firmware}"],
|
"auto_args": ["-p", "{port}", "-a", "-s", "{baudrate}", "-q", "{firmware}"],
|
||||||
"timeout_seconds": 900,
|
"timeout_seconds": 900,
|
||||||
"workers": 2,
|
"workers": 2,
|
||||||
},
|
},
|
||||||
"firmwares": {
|
"firmwares": {
|
||||||
"production": "burn_tool/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip",
|
"production": "firmware/EG800AKCN_11LG-FT1-factory-test.zip",
|
||||||
"official": "burn_tool/firmware/official/EG800AKCN_11LG-official-fac.zip",
|
"official": "firmware/EG800AKCN_11LG-official-fac.zip",
|
||||||
},
|
},
|
||||||
"serial": {"baudrate": 115200, "encoding": "utf-8", "line_ending": "\\r\\n"},
|
"serial": {"baudrate": 115200, "encoding": "utf-8", "line_ending": "\\r\\n"},
|
||||||
"protocol": {"prefix": "FT1 ", "version": 1, "auto_save": True},
|
"protocol": {"prefix": "FT1 ", "version": 1, "auto_save": True},
|
||||||
@@ -96,7 +128,10 @@ def resolve_project_path(value: str) -> Path:
|
|||||||
raw = Path(value)
|
raw = Path(value)
|
||||||
if raw.is_absolute():
|
if raw.is_absolute():
|
||||||
return raw
|
return raw
|
||||||
candidates = [ROOT_DIR / raw, APP_DIR / raw, Path.cwd() / raw]
|
candidates = [APP_DIR / raw]
|
||||||
|
if not getattr(sys, "frozen", False):
|
||||||
|
candidates.insert(0, FACTORY_HOST_DIR / "dist" / raw)
|
||||||
|
candidates.extend([CONFIG_PATH.parent / raw, ROOT_DIR / raw, Path.cwd() / raw])
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if candidate.exists():
|
if candidate.exists():
|
||||||
return candidate.resolve()
|
return candidate.resolve()
|
||||||
@@ -104,21 +139,25 @@ def resolve_project_path(value: str) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def find_downloader(configured: str, aboot_configured: str = "") -> Path | None:
|
def find_downloader(configured: str, aboot_configured: str = "") -> Path | None:
|
||||||
|
package_dir = APP_DIR if getattr(sys, "frozen", False) else FACTORY_HOST_DIR / "dist"
|
||||||
candidates = []
|
candidates = []
|
||||||
if configured:
|
if configured:
|
||||||
raw = Path(configured)
|
raw = Path(configured)
|
||||||
candidates.extend([raw, ROOT_DIR / raw, APP_DIR / raw])
|
candidates.extend([raw, package_dir / raw, ROOT_DIR / raw])
|
||||||
if aboot_configured:
|
if aboot_configured:
|
||||||
raw = Path(aboot_configured)
|
raw = Path(aboot_configured)
|
||||||
candidates.extend([raw.parent / "adownload.exe", ROOT_DIR / raw.parent / "adownload.exe", APP_DIR / raw.parent / "adownload.exe"])
|
candidates.extend([raw.parent / "adownload.exe", package_dir / raw.parent / "adownload.exe",
|
||||||
candidates.extend([ROOT_DIR / "burn_tool" / "adownload.exe", APP_DIR / "burn_tool" / "adownload.exe"])
|
ROOT_DIR / raw.parent / "adownload.exe"])
|
||||||
|
candidates.extend([package_dir / ".runtime" / "burn_tool" / "adownload.exe",
|
||||||
|
FACTORY_HOST_DIR / "vendor" / "adownload.exe"])
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
try:
|
try:
|
||||||
if candidate.is_file():
|
if candidate.is_file():
|
||||||
return candidate.resolve()
|
return candidate.resolve()
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
for root in (ROOT_DIR / "burn_tool", APP_DIR / "burn_tool"):
|
for root in (package_dir / ".runtime" / "burn_tool",
|
||||||
|
FACTORY_HOST_DIR / "vendor"):
|
||||||
try:
|
try:
|
||||||
if root.is_dir():
|
if root.is_dir():
|
||||||
found = next(root.rglob("adownload.exe"), None)
|
found = next(root.rglob("adownload.exe"), None)
|
||||||
@@ -412,7 +451,7 @@ class FactoryHost(Tk):
|
|||||||
def _refresh_tool_status(self) -> None:
|
def _refresh_tool_status(self) -> None:
|
||||||
downloader = find_downloader(self.config_data["flash"].get("downloader_path", ""), self.config_data["flash"].get("aboot_path", ""))
|
downloader = find_downloader(self.config_data["flash"].get("downloader_path", ""), self.config_data["flash"].get("aboot_path", ""))
|
||||||
firmware = resolve_project_path(self.config_data["firmwares"].get(self.firmware_kind.get(), ""))
|
firmware = resolve_project_path(self.config_data["firmwares"].get(self.firmware_kind.get(), ""))
|
||||||
tool_text = f"adownload: {downloader}" if downloader else "adownload: 未找到(请把 burn_tool 完整目录放在根目录或在配置中指定)"
|
tool_text = f"adownload: {downloader}" if downloader else "adownload: 未找到(请检查 .runtime/burn_tool 或 factory_host/vendor)"
|
||||||
package_text = f"固件: {firmware}" if firmware.is_file() else "固件: 未选择"
|
package_text = f"固件: {firmware}" if firmware.is_file() else "固件: 未选择"
|
||||||
self.tool_status.set(tool_text + " | " + package_text)
|
self.tool_status.set(tool_text + " | " + package_text)
|
||||||
kind_label = "产测固件" if self.firmware_kind.get() == "production" else "正式固件"
|
kind_label = "产测固件" if self.firmware_kind.get() == "production" else "正式固件"
|
||||||
@@ -427,8 +466,8 @@ class FactoryHost(Tk):
|
|||||||
return "break" if self.log_follow else None
|
return "break" if self.log_follow else None
|
||||||
|
|
||||||
def choose_firmware(self) -> None:
|
def choose_firmware(self) -> None:
|
||||||
kind_dir = "test" if self.firmware_kind.get() == "production" else "official"
|
initial_dir = (APP_DIR if getattr(sys, "frozen", False)
|
||||||
initial_dir = ROOT_DIR / "burn_tool" / "firmware" / kind_dir
|
else FACTORY_HOST_DIR / "dist") / "firmware"
|
||||||
path = filedialog.askopenfilename(
|
path = filedialog.askopenfilename(
|
||||||
title="选择固件包",
|
title="选择固件包",
|
||||||
initialdir=str(initial_dir if initial_dir.is_dir() else ROOT_DIR),
|
initialdir=str(initial_dir if initial_dir.is_dir() else ROOT_DIR),
|
||||||
@@ -663,7 +702,7 @@ class FactoryHost(Tk):
|
|||||||
return
|
return
|
||||||
downloader = find_downloader(self.config_data["flash"].get("downloader_path", ""), self.config_data["flash"].get("aboot_path", ""))
|
downloader = find_downloader(self.config_data["flash"].get("downloader_path", ""), self.config_data["flash"].get("aboot_path", ""))
|
||||||
if not downloader:
|
if not downloader:
|
||||||
messagebox.showerror("无法烧录", "未找到 adownload.exe。请把 burn_tool 完整目录放到根目录,或在配置中指定 flash.downloader_path。aboot.exe 是图形界面,不能作为命令行烧录进程。")
|
messagebox.showerror("无法烧录", "未找到 adownload.exe。请检查发布包的 .runtime/burn_tool,或开发目录的 factory_host/vendor。aboot.exe 是图形界面,不能作为命令行烧录进程。")
|
||||||
return
|
return
|
||||||
jobs = [
|
jobs = [
|
||||||
self.stations[iid]
|
self.stations[iid]
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
FACTORY_HOST_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(FACTORY_HOST_DIR / "src"))
|
||||||
|
|
||||||
import factory_host
|
import factory_host
|
||||||
from ft1_protocol import FT1Session, ProtocolError, parse_ft1_line
|
from ft1_protocol import FT1Session, ProtocolError, parse_ft1_line
|
||||||
|
|
||||||
@@ -47,7 +51,11 @@ class FactoryHostTests(unittest.TestCase):
|
|||||||
self.assertIn("{port}", args)
|
self.assertIn("{port}", args)
|
||||||
|
|
||||||
def test_project_root_and_relative_path(self):
|
def test_project_root_and_relative_path(self):
|
||||||
self.assertTrue((factory_host.ROOT_DIR / "burn_tool").is_dir())
|
self.assertTrue((factory_host.FACTORY_HOST_DIR / "vendor" / "aboot-tools-2024.08.08-win-x64").is_dir())
|
||||||
|
self.assertTrue((factory_host.FACTORY_HOST_DIR / "firmware").is_dir())
|
||||||
|
self.assertEqual(factory_host.FACTORY_HOST_DIR, FACTORY_HOST_DIR)
|
||||||
|
self.assertEqual(factory_host.CONFIG_PATH, FACTORY_HOST_DIR / "config" / "factory_host_config.json")
|
||||||
|
self.assertEqual(factory_host.LOG_DIR, FACTORY_HOST_DIR / "build" / "logs")
|
||||||
config = factory_host.load_config(factory_host.CONFIG_PATH)
|
config = factory_host.load_config(factory_host.CONFIG_PATH)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Path(config["firmwares"]["production"]).name,
|
Path(config["firmwares"]["production"]).name,
|
||||||
@@ -59,8 +67,10 @@ class FactoryHostTests(unittest.TestCase):
|
|||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
factory_host.resolve_project_path(config["firmwares"]["official"]).is_file()
|
factory_host.resolve_project_path(config["firmwares"]["official"]).is_file()
|
||||||
)
|
)
|
||||||
self.assertIn("burn_tool/firmware/test", config["firmwares"]["production"])
|
self.assertTrue(config["firmwares"]["production"].startswith("firmware/"))
|
||||||
self.assertIn("burn_tool/firmware/official", config["firmwares"]["official"])
|
self.assertTrue(config["firmwares"]["official"].startswith("firmware/"))
|
||||||
|
self.assertTrue(config["flash"]["aboot_path"].startswith(".runtime/"))
|
||||||
|
self.assertTrue(config["flash"]["downloader_path"].startswith(".runtime/"))
|
||||||
|
|
||||||
def test_ft1_ignores_sdk_log_and_parses_node(self):
|
def test_ft1_ignores_sdk_log_and_parses_node(self):
|
||||||
self.assertIsNone(parse_ft1_line("DP1312EA is OK!"))
|
self.assertIsNone(parse_ft1_line("DP1312EA is OK!"))
|
||||||
Reference in New Issue
Block a user