6 Commits
Author SHA1 Message Date
clear e357930bee Release PGU5-Core v2.0.5 2026-08-12 11:01:31 +08:00
clear 5744fca1bd Release PGU5-Core v2.0.4 2026-08-12 10:53:29 +08:00
clear 9c330d6327 Release PGU5-Core v2.0.3 2026-08-11 18:21:35 +08:00
clear ae3990b73e Revise v1.1.19 documentation and API reference 2026-08-04 19:30:33 +08:00
clear db1a636cfc Release v1.1.19 Core synchronization 2026-08-04 17:18:39 +08:00
clear faac732509 Release v1.1.9 MS602 startup stagger 2026-07-02 15:28:40 +08:00
8 changed files with 9870 additions and 2453 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
* text=auto eol=lf
*.py text eol=lf
*.md text eol=lf
*.json text eol=lf
*.service text eol=lf
*.sh text eol=lf
*.logrotate text eol=lf
-1
View File
@@ -4,7 +4,6 @@ run.log
captures/
backups/
.vscode/
.codex
.codex/
# Local SDK/cache artifacts
+130 -77
View File
@@ -1,112 +1,165 @@
# AGENTS.md
This is operational memory for coding agents working on PGU5-Core. The live Orange Pi checkout is the source of truth. If local files, notes, or conversation memory conflict with `/root/PGU5-Core`, follow the Pi unless the user explicitly says otherwise.
这是 PGU5-Core 的现场操作规则。Orange Pi 上的 `/root/PGU5-Core` 是事实来源;本地文件、旧记录与现场冲突时,以现场为准,除非用户明确指定其他基线。
## Local Environment
## 环境
- Local Python: `C:\Users\clear\.conda\envs\clear_conda_main\python.exe`.
- Use Paramiko for password SSH automation from Windows.
- Avoid Chinese text through raw PowerShell here-strings. Use UTF-8 Python writes, Unicode escapes, HTML entities, or Pi-side editing. Always verify no mojibake question-mark runs and no Unicode replacement characters.
## Remote Device
| Item | Value |
| 项目 | 值 |
| --- | --- |
| Device | Orange Pi 4 Pro |
| Host | `192.168.101.172` |
| SSH | `root / orangepi` |
| Project path | `/root/PGU5-Core` |
| Web URL | `http://192.168.101.172:8080/` |
| Runtime Python | `/opt/miniconda3/envs/py310/bin/python3` |
| 本地 Python | `C:\Users\clear\.conda\envs\clear_conda_main\python.exe` |
| 现场主机 | `192.168.101.172` |
| SSH 用户 | `root` |
| 现场目录 | `/root/PGU5-Core` |
| Web | `http://192.168.101.172:8080/` |
| 运行 Python | `/opt/miniconda3/envs/py310/bin/python3` |
| Gitea | `http://107.173.157.41:3000/clear/PGU5-Core` |
## Identity And Boundary
SSH 密码、Gitea 密码和 AuthKey 由当前会话或外部安全方式提供,不写入仓库。
- PGU5-Core is the PGU5A open management platform, not the old seed-germination phenotyping platform.
- Current app version: `v1.1.8`; current release tag: `v1.1.8`.
- Keep repository, versioning, serial prefix, UI identity, and feature boundary independent from AOT/seedbed projects.
- Do not reintroduce PLC/Modbus, watering device, Hikrobot/MVS/GenICam, AOT automation capture/watering, planned tasks, seed phenotype path logic, AOT-PhenoXYZ identity, or XYZ2 serial prefix.
Windows 侧密码 SSH 自动化使用 Paramiko。编辑中文时必须保持 UTF-8,上传后检查 `U+FFFD`、连续问号和换行漂移。`.gitattributes` 对 Python、Markdown、JSON、systemd、Shell 和 logrotate 文件强制 LF。
## Source Of Truth
## 项目边界
- Pi checkout: `/root/PGU5-Core`.
- Pi config: `/root/PGU5-Core/pgu5a_config.json`; treat it as real field config.
- Save root: `/nvme-data/pgu_data`.
- Gitea repo: `http://107.173.157.41:3000/clear/PGU5-Core`.
- Git remote must stay credential-free: `http://107.173.157.41:3000/clear/PGU5-Core.git`.
- Do not commit, push, tag, or release unless the user explicitly asks.
- Version strings must include the leading `v` everywhere: `APP_VERSION`, `APP_RELEASE_TAG`, API display, Git tag, and Gitea release. Do not publish bare versions.
- 当前应用版本和发布标签均为 `v2.0.5`。
- 版本字符串始终保留前导 `v`。
- 序列号使用 `PGU5A-` 加大写以太网 MAC。
- Core 不包含 PLC/Modbus、运动、水肥、业务自动化、计划任务、业务路径、Hikrobot/MVS/GenICam 或任何应用专属身份。
- 不把应用专属功能放回 `main`。
## Services
## 事实来源
| Service | Purpose | Normal state |
| --- | --- | --- |
| `pgu5a_base.service` | Main Web/API/camera service | active running |
| `pgu5a_oled.service` | Independent SSD1306 OLED display | active running |
| `ds3231-sync.service` | RTC/NTP one-shot sync | enabled, often inactive after completion |
| `nvme-data-auto.service` | NVMe mount helper | active exited |
- 代码:`/root/PGU5-Core`
- 配置:`/root/PGU5-Core/pgu5a_config.json`
- 保存目录:`/nvme-data/pgu_data`
- 主日志:`/root/PGU5-Core/run.log`
- API 文档:`API.md`
- Git 远端必须保持无凭据:
## OTA Rules
```text
http://107.173.157.41:3000/clear/PGU5-Core.git
```
- Visible current version must come from `APP_VERSION` and `APP_RELEASE_TAG`.
- Git tag/commit are diagnostic only.
- `/api/system/version` exposes `app_version`, `release_tag`, `current_version`, `current_tag`, and diagnostic `git_tag`.
- Online upgrade starts disabled, and is enabled only after check update returns `update_available: true`.
- Upgrade opens a progress modal and polls `/api/ota/status`.
- States: `IDLE`, `CHECKED`, `FETCHING`, `CHECKOUT`, `VERIFYING`, `RESTARTING`, `SUCCESS`, `ERROR`.
- Fetch target tag with `git fetch --force origin refs/tags/<tag>:refs/tags/<tag>`.
- No manual rollback UI. No project-external OTA backup folders.
- OTA state file: `/root/PGU5-Core/backups/ota_state.json`.
- Standalone updater: `/root/PGU5-Core/ota/ota_update.py`.
现场配置是实际设备配置。发布时如果用户要求提交当前现场状态,应保留并审查配置差异,不得直接丢弃。
## M3 / L640 Thermal USB Control
## 服务
- Use PyUSB bulk, not `/dev/ttyACM0`.
- Default VID:PID `0424:6000`, interface `3`, OUT `0x05`, IN `0x85`, poll `5 s`.
- Missing PyUSB, disconnected device, sleep state, or USB failure must return JSON with `ok: false` and must not affect the main service or previews.
- Temperature parsing: frame header/length/tail primary, checksum diagnostic, little-endian signed int16 divided by 100.
- Commands: auto NUC open `AA 05 01 01 01 01 B3 EB AA`, close `AA 05 01 01 01 00 B2 EB AA`, save `AA 04 01 7F 02 30 EB AA`.
- Always send save settings after open/close auto NUC.
- APIs: `GET /api/m3/control`, `POST /api/m3/auto_nuc`.
- Admin card label is thermal control, placed near auto sleep and kept compact.
| 服务 | 正常状态 |
| --- | --- |
| `pgu5a_base.service` | `active` |
| `pgu5a_oled.service` | `active` |
| `ds3231-sync.service` | 一次性任务,完成后可为 `inactive` |
| `nvme-data-auto.service` | `active (exited)` |
| `syncthing@root.service` | `active` |
## Sensor Power Rules
OLED 是独立服务,OLED 故障不得影响主服务。
- MS602 and DCL start immediately.
- M3Lite wakes asynchronously: delay, GPIO 8 on, start worker.
- UVC-RGB/IMX766 wakes asynchronously: delay, GPIO 9 on, start worker.
- Sleep increments `sensor_wake_token` to cancel stale delayed wake threads.
- Process start/soft restart uses the same relay-aware startup path.
- Auto sleep pauses while timed capture or save queue work is active.
- Current field timing: M3 startup check `3`, M3 wake delay `2`, UVC wake delay `1`.
## 代码规则
## Frontend Rules
- `pgu5a_base.py` 内嵌 HTML、CSS 和 JavaScript,没有独立前端构建。
- 前端修改后提取脚本做 JavaScript 语法检查;一个语法错误会导致时钟、按钮和状态刷新同时失效。
- UI 保持紧凑、现场可操作,移动端不得横向溢出,DCL 必须完整显示画面。
- 管理中心“多光谱控制”页面通过 `GET/POST /api/ms602/detection` 管理开流检测时长、抖动和撕裂检测;保存后必须持久化并立即更新运行中的 worker,不得重启整机或全部通道。
- 配置写入保持格式化 UTF-8 JSON 和一个结尾换行。
- 不进行与用户请求无关的大范围重构。
- `pgu5a_base.py` embeds HTML/CSS/JS; no separate frontend build.
- One JS syntax error can stop clock/buttons/status updates. Extract and run `node --check` after frontend edits when possible.
- Keep UI compact and operational. Avoid filler text and redundant cards.
- Mobile layout must not overflow. DCL preview must show the full frame.
## 相机与电源
## Validation
- MS602 和 DCL 立即启动。
- MS602 六通道按 `open_stagger_sec` 错开启动,各通道独立重连。
- MS602 连续读帧失败时始终允许独立重连;抖动和撕裂检测只在每次通道成功打开后的 `startup_detection_window_sec` 内运行。
- 首次启动、休眠唤醒和错误重连都会为对应通道开启新检测窗口;窗口通过后必须停止昂贵图像分析,且不得由状态轮询或保存配置重新启动检测。
- 每个通道的检测样本、截止时间和代次必须独立并受锁保护,旧开流代次的计算结果不得影响新通道实例。
- 抖动位移是在 `160x100` 检测样本上计算,现场 720 上下抖动实测约为 `0.04..0.20` 像素;不得恢复按原始分辨率误设的 `0.4` 最小位移和 `2.0` 累计位移。
- 撕裂检测同时使用跨帧低相关度大帧差和单帧横向/纵向梯度比例,避免已经撕裂且持续撕裂时因缺少连续切换而漏检。
- `last_jitter_trigger_*` 和 `last_integrity_trigger_*` 必须跨通道重开保留,便于区分真实故障与阈值误判;保存检测参数不得清空已完成窗口的诊断证据。
- M3Lite 使用 GPIO 8 异步唤醒;UVC-RGB 使用 GPIO 9 异步唤醒。
- UVC-RGB 在视频节点消失、被替换或持续无新帧时重连。
- 休眠递增 `sensor_wake_token`,取消旧延时唤醒线程。
- 定时保存或保存队列工作时暂停自动休眠。
当前现场时序:
| 参数 | 值 |
| --- | --- |
| M3 startup NUC check | `3.0 s` |
| M3 wake delay | `2.0 s` |
| UVC-RGB wake delay | `1.0 s` |
| MS602 open stagger | `0.5 s` |
| MS602 startup detection | `5.0 s` |
## M3/L640 控制
- 使用 PyUSB bulk,不使用 `/dev/ttyACM0`。
- 默认 VID:PID `0424:6000`,接口 `3`,OUT `0x05`,IN `0x85`。
- 打开或关闭自动 NUC 后必须发送保存参数命令。
- 设备缺失、休眠、PyUSB 缺失或 USB 错误必须返回 `ok=false`,不得影响主服务和其他预览。
- 每次事务完成后释放 USB 接口和 PyUSB 资源。
## OTA
- 可见版本来自 `APP_VERSION` 和 `APP_RELEASE_TAG`。
- `/api/system/version` 的 Git 字段只用于诊断。
- 检查更新后只有 `update_available=true` 才能升级。
- 状态:`IDLE`、`CHECKED`、`FETCHING`、`CHECKOUT`、`VERIFYING`、`RESTARTING`、`SUCCESS`、`ERROR`。
- 强制刷新目标标签:
```bash
git fetch --force origin refs/tags/<tag>:refs/tags/<tag>
```
- 无手动回滚接口。
- 不创建项目外 OTA 备份目录。
- OTA 状态文件:`backups/ota_state.json`。
## 有副作用的接口
常规验证不得调用:
- `POST /api/system/reboot`
- `POST /api/system/shutdown`
- `POST /api/syncthing/identity-init/start`
- `POST /api/files/delete`
- `POST /api/system/update/apply`
- `POST /api/m3/auto_nuc`
配置保存、网络修改、传感器电源、自动休眠和自动清理接口也会改变现场状态,调用前必须确认任务需要。
## 验证
```bash
cd /root/PGU5-Core
git status --short
/opt/miniconda3/envs/py310/bin/python3 -m py_compile pgu5a_base.py
/opt/miniconda3/envs/py310/bin/python3 -m py_compile ota/ota_update.py
git diff --check
/opt/miniconda3/envs/py310/bin/python3 -m py_compile \
pgu5a_base.py ota/ota_update.py oled/oled_status.py ds3231/sync_ds3231.py
/opt/miniconda3/envs/py310/bin/python3 -m json.tool pgu5a_config.json >/dev/null
/opt/miniconda3/envs/py310/bin/python3 -m py_compile oled/oled_status.py
systemctl restart pgu5a_base.service
systemctl is-active pgu5a_base.service
curl -fsS http://127.0.0.1:8080/api/system/version
curl -fsS http://127.0.0.1:8080/api/status
curl -fsS http://127.0.0.1:8080/api/network
curl -fsS http://127.0.0.1:8080/api/ota/status
curl -fsS http://127.0.0.1:8080/api/m3/control
curl -fsS -X POST http://127.0.0.1:8080/api/m3/auto_nuc -d 'enabled=1'
curl -fsS -X POST http://127.0.0.1:8080/api/m3/auto_nuc -d 'enabled=0'
curl -fsS http://127.0.0.1:8080/api/syncthing/identity-init/status
```
If thermal hardware or PyUSB is unavailable, M3 APIs should still return JSON errors and `pgu5a_base.service` should remain active.
发布后必须确认:
## Secrets
- `main`、标签、Gitea Release 和运行时提交一致。
- `/api/system/version` 中版本、标签正确且 `dirty=false`。
- 主服务、OLED、Syncthing、Avahi 和 NVMe 正常。
- MS602 六通道、M3Lite、DCL 和 UVC-RGB 恢复连接。
- 实际服务页面 HTTP `200`,嵌入式 JavaScript 无语法错误。
- Core 活动目录和配置中没有应用业务残余或旧工程标识。
Do not write passwords, AuthKeys, tokens, cookies, or credential-bearing remotes into source, docs, skills, or persistent config.
## Git 与发布
- 未经用户明确要求,不提交、不推送、不打标签、不发布。
- 发布前检查目标标签是否存在。
- 用户明确要求同名修订发布时,才允许移动现有标签,并同时更新 Gitea Release。
- 临时使用凭据后再次检查 `git remote -v`,确保远端地址无凭据。
- 发布说明使用清晰 ASCII/English,避免 Gitea 编码问题。
## 密钥
不在源码、配置、Markdown、技能或提交中保存密码、AuthKey、令牌、Cookie 或带凭据的 Git URL。
+677 -162
View File
@@ -1,140 +1,183 @@
# PGU5-Core API
Base URL:
本文档对应 PGU5-Core `v2.0.5`,以 `pgu5a_base.py` 中的实际路由为准。
```text
http://192.168.101.172:8080/
```
## 基本约定
Local URL on the Pi:
```text
http://127.0.0.1:8080/
```
Most endpoints return UTF-8 JSON. Image, raw-file, and ZIP endpoints return binary payloads. Errors should return JSON where possible so the frontend does not crash while parsing an HTML error page.
## System Status
```http
GET /api/status
```
Returns the current runtime state: time, version, camera states, schedule, save queue, auto sleep, M3 control, FPS stats, preview metadata, and logs.
Important fields:
| Field | Meaning |
| 项目 | 说明 |
| --- | --- |
| `app_version` | Application version constant |
| `sensor_power.awake` | Whether sensors are awake |
| `sensor_power.auto_sleep.paused` | True while timed capture or save queue work is running |
| `sensor_power.auto_sleep.automation_running` | Frontend alias for paused |
| `schedule.enabled` | Timed save state |
| `save_queue.pending_groups` | Pending save groups |
| `m3_control` | M3/L640 USB control status |
| 设备地址 | `http://192.168.101.172:8080` |
| 本机地址 | `http://127.0.0.1:8080` |
| JSON 编码 | UTF-8 |
| POST 编码 | `application/x-www-form-urlencoded` |
| 缓存 | 动态页面、JSON 和预览帧均返回禁止缓存响应头 |
| 鉴权 | 管理 API 本身没有登录层,应只在可信管理网络中开放 |
`m3_control` when available:
大多数 JSON 接口即使业务失败也返回 HTTP `200`,调用方必须检查响应中的 `ok`。文件预览、原始文件和 ZIP 下载接口会按错误情况返回 HTTP `4xx`。预览帧暂时不可用时返回 HTTP `204`。
```json
{
"available": true,
"ok": true,
"status": "OK",
"message": "thermal USB control OK",
"fpa_temp_c": 34.12,
"core_temp_c": 35.28,
"auto_nuc": true,
"updated_at_text": "2026-06-23 10:00:00"
}
```
If unavailable, `available` is false and `status/message` explain why.
## Version
```http
GET /api/system/version
```
Visible current version comes from application constants, not Git history.
| Field | Meaning |
| --- | --- |
| `version` | Current release tag, same as `APP_RELEASE_TAG` |
| `app_version` | Current app version, same as `APP_VERSION`; PGU5-Core versions are `v`-prefixed starting with `v1.1.8` |
| `release_tag` | Current release tag |
| `current_version` | Current app version, `v`-prefixed |
| `current_tag` | Current release tag |
| `git_tag` | Diagnostic Git describe value only |
| `commit` | Short commit hash |
| `branch` | Current branch |
| `dirty` | Dirty worktree flag |
| `serial` | PGU5A serial derived from Ethernet MAC |
## OTA
```http
GET /api/ota/status
POST /api/system/update/check
POST /api/system/update/apply
```
`GET /api/ota/status` returns current OTA state from `/root/PGU5-Core/backups/ota_state.json` plus current version fields.
States: `IDLE`, `CHECKED`, `FETCHING`, `CHECKOUT`, `VERIFYING`, `RESTARTING`, `SUCCESS`, `ERROR`.
`POST /api/system/update/check` queries Gitea releases and returns `update_available`. The frontend enables online upgrade only when this is true.
`POST /api/system/update/apply` accepts optional form field `tag`. Tags should include the leading `v`, for example `v1.1.8`. The upgrade flow forces the target tag refresh:
```bash
git fetch --force origin refs/tags/<tag>:refs/tags/<tag>
```
Then it checks out the tag, runs `py_compile`, and restarts `pgu5a_base.service`. There is no manual rollback endpoint and no project-external OTA backup directory.
## M3 / L640 Thermal Control
```http
GET /api/m3/control
POST /api/m3/auto_nuc
```
`GET /api/m3/control` returns the cached low-rate USB bulk control status. The control channel uses PyUSB, interface 3, OUT `0x05`, IN `0x85`, default VID:PID `0424:6000`, and does not depend on `/dev/ttyACM0`.
`POST /api/m3/auto_nuc` form field:
| Field | Meaning |
| --- | --- |
| `enabled` | `1/true/on` enables auto NUC; `0/false` disables it |
Success example:
通用 JSON 成功响应:
```json
{
"ok": true,
"enabled": true,
"message": "auto NUC enabled and saved",
"response_hex": "55 ... eb aa",
"save_response_hex": "55 ... eb aa",
"m3_control": {}
"message": "operation completed"
}
```
Failure example:
通用 JSON 失败响应:
```json
{
"ok": false,
"message": "thermal control interface not found",
"m3_control": {}
"message": "failure reason"
}
```
After setting auto NUC, the save-settings command is always sent. Checksum is diagnostic; frame header, length, and tail are the primary validity checks.
## 接口总览
## Preview Frames
### GET
| 路径 | 用途 |
| --- | --- |
| `/api/status` | 运行状态、相机状态、FPS、保存队列和自动休眠 |
| `/api/system/version` | 应用版本、Git 诊断信息和设备序列号 |
| `/api/storage` | 存储总量、已用/剩余空间和自动清理状态 |
| `/api/network` | AP、STA、以太网、Syncthing 和 HS 状态 |
| `/api/network/headscale/task` | HS 后台任务进度 |
| `/api/syncthing/identity-init/status` | Syncthing 身份初始化进度 |
| `/api/ota/status` | OTA 当前状态 |
| `/api/m3/control` | M3/L640 热成像 USB 控制状态 |
| `/api/ms602/detection` | MS602 抖动、撕裂检测参数和六通道诊断 |
| `/api/ssh/read` | 读取 Web SSH PTY 输出 |
| `/api/config` | 读取当前 JSON 配置文本 |
| `/api/logs` | 获取内存运行日志 |
| `/api/files` | 分页列出保存目录 |
| `/api/files/preview` | 获取图像预览 |
| `/api/files/raw` | 获取原始文件 |
| `/api/schedule` | 获取定时保存配置 |
| `/frame/ms602/{band}.jpg` | MS602 单波段预览 |
| `/frame/m3lite.jpg` | M3Lite 预览 |
| `/frame/dcl.jpg` | DCL 预览 |
| `/frame/uvc_rgb.jpg` | UVC-RGB 预览 |
| `/assets/logo.png` | 产品 Logo |
| `/assets/xterm.js` | Web SSH 终端脚本,按需加载 |
| `/assets/xterm.css` | Web SSH 终端样式,按需加载 |
| `/assets/xterm.LICENSE` | xterm.js 许可证文本 |
### POST
| 路径 | 用途 |
| --- | --- |
| `/api/save/{target}` | 立即保存指定目标 |
| `/api/save` | 兼容形式的立即保存 |
| `/api/schedule` | 修改定时保存 |
| `/api/storage/cleanup` | 修改自动清理配置并执行空间检查 |
| `/api/files/delete` | 后台永久删除文件或目录 |
| `/api/files/download` | 打包下载文件或目录 |
| `/api/sensors/power` | 唤醒或休眠传感器 |
| `/api/sensors/auto_sleep` | 设置自动休眠时间 |
| `/api/m3/auto_nuc` | 设置并保存自动 NUC |
| `/api/ms602/detection` | 保存并立即应用 MS602 抖动、撕裂检测参数 |
| `/api/network/scan` | 扫描 Wi-Fi |
| `/api/network/ap` | 修改 AP |
| `/api/network/sta/connect` | 连接 STA |
| `/api/network/sta/disconnect` | 断开 STA |
| `/api/network/headscale/install` | 安装 HS 组件 |
| `/api/network/headscale/uninstall` | 卸载 HS 组件 |
| `/api/network/headscale/login` | 登录 Headscale |
| `/api/network/headscale/logout` | 退出 Headscale |
| `/api/syncthing/identity-init/start` | 重新生成 Syncthing 设备身份 |
| `/api/ssh/open` | 打开 Web SSH 会话 |
| `/api/ssh/input` | 写入 Web SSH PTY |
| `/api/ssh/close` | 关闭 Web SSH 会话 |
| `/api/config/save` | 保存 JSON 配置并重启服务 |
| `/api/system/update/check` | 检查 Gitea Release |
| `/api/system/update/apply` | 应用 OTA 标签 |
| `/api/service/restart` | 重启主服务 |
| `/api/system/reboot` | 重启系统 |
| `/api/system/shutdown` | 同步磁盘并关机 |
## 状态与版本
### `GET /api/status`
返回当前运行快照。主要字段:
| 字段 | 说明 |
| --- | --- |
| `app_version` | `APP_VERSION` |
| `server_time` / `server_time_text` | 设备时间 |
| `save_root` | 保存根目录 |
| `message` | 最近一条运行消息 |
| `logs` | 有界内存日志 |
| `sensor_power.awake` | 传感器是否处于唤醒状态 |
| `sensor_power.auto_sleep` | 自动休眠配置、倒计时和暂停状态 |
| `schedule` | 定时保存开关与间隔 |
| `save_queue` | 保存队列占用情况 |
| `refresh` | 前端刷新周期 |
| `preview_cache_ts` | 各预览缓存更新时间 |
| `stats.capture` | 采集 FPS |
| `stats.preview` | 预览编码 FPS |
| `stats.request` | 页面请求 FPS |
| `ms602` | MS602 总体状态 |
| `ms602_channels` | 六通道状态和恢复诊断 |
| `m3lite` / `dcl` / `uvc_rgb` | 各相机状态 |
| `m3_control` | 热成像控制状态 |
| `dcl_frame` / `uvc_rgb_frame` | 原始帧尺寸 |
`ms602_channels` 的键为 `480`、`550`、`660`、`720`、`840`、`rgb`。每个通道包含:
```json
{
"status": "CONNECTED",
"message": "Connected: /dev/video12",
"device_index": 12,
"restart_count": 0,
"jitter_count": 0,
"jitter_reversals": 0,
"jitter_cancellation": 1.0,
"jitter_total_shift": 0.0,
"last_shift_x": 0.01,
"last_shift_y": -0.02,
"updated_at": 1785835208.1
}
```
### `GET /api/system/version`
界面显示版本来自应用常量,不由 Git 自动覆盖。
| 字段 | 说明 |
| --- | --- |
| `version` | 当前发布标签 |
| `app_version` | `APP_VERSION` |
| `release_tag` | `APP_RELEASE_TAG` |
| `current_version` | 当前应用版本 |
| `current_tag` | 当前发布标签 |
| `git_tag` | `git describe` 诊断值 |
| `commit` | 短提交号 |
| `branch` | 当前分支 |
| `dirty` | 工作区是否有未提交改动 |
| `serial` | `PGU5A-` 加以太网 MAC |
### `GET /api/logs`
返回:
```json
{
"logs": [
{
"time": 1785835208.1,
"time_text": "2026-08-04 17:20:08",
"message": "Ready"
}
],
"message": "Ready"
}
```
## 预览接口
```http
GET /frame/ms602/{band}.jpg
@@ -143,86 +186,558 @@ GET /frame/dcl.jpg
GET /frame/uvc_rgb.jpg
```
MS602 bands: `480`, `550`, `660`, `720`, `840`, `rgb`.
MS602 `band` 允许值:
## Save And Schedule
```http
POST /api/save/{target}
POST /api/save
GET /api/schedule
POST /api/schedule
```text
480 550 660 720 840 rgb
```
Targets: `dcl`, `m3lite`, `ms602`, `uvc_rgb`, `all`, `online`. Timed save uses `online`.
有帧时返回 `image/jpeg`;尚无缓存帧时返回 HTTP `204`。这些地址是轮询快照,不是 MJPEG 长连接。
`POST /api/schedule` fields: `interval_sec`, `enabled`.
## 保存与定时任务
## Sensor Power
### `POST /api/save/{target}`
```http
POST /api/sensors/power
POST /api/sensors/auto_sleep
### `POST /api/save`
两种形式都执行一次同步保存,文件写入 `${save_root}/Once`。
`target` 允许值:
| 值 | 行为 |
| --- | --- |
| `ms602` | 保存完整六波段 TIFF |
| `m3lite` | 保存温度 CSV 和温度图 |
| `dcl` | 保存 DCL JPEG |
| `uvc_rgb` | 保存 UVC-RGB JPEG |
| `all` | 要求全部相机保存,任一失败时 `ok=false` |
| `online` | 只保存当前已连接的相机 |
路径形式示例:
```bash
curl -X POST http://127.0.0.1:8080/api/save/all
```
`/api/sensors/power` uses `awake=1/0`. `/api/sensors/auto_sleep` uses `minutes`; `0` disables auto sleep.
兼容形式:
## Storage And Files
```http
GET /api/storage
POST /api/storage/cleanup
GET /api/files?path={relative_path}
GET /api/files/preview?path={relative_file}
GET /api/files/raw?path={relative_file}
POST /api/files/delete
POST /api/files/download
```bash
curl -X POST http://127.0.0.1:8080/api/save -d "target=all"
```
All file paths are constrained under the save root.
### `GET /api/schedule`
## Network And HS
```json
{
"enabled": false,
"interval_sec": 5.0
}
```
### `POST /api/schedule`
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `interval_sec` | 否 | 最小为 `1.0` 秒 |
| `enabled` | 否 | `1/true/on` 开启,其他值关闭 |
开启时创建本次定时任务目录,并周期性将 `online` 目标加入异步保存队列。定时保存或保存队列有任务时,自动休眠暂停。
## 存储与文件管理
所有传入路径均为相对 `save_root` 的路径。服务会解析规范路径并拒绝越出保存根目录的访问。
### `GET /api/storage`
返回存储总量、已用/剩余空间、刷新周期、版本以及自动清理结果:
```json
{
"save_root": "/nvme-data/pgu_data",
"free_gb": 120.5,
"used_gb": 118.0,
"total_gb": 238.5,
"refresh_ms": 5000,
"app_version": "v2.0.5",
"auto_cleanup": {}
}
```
读取该接口会执行一次自动清理空间检查;只有配置已启用且空间低于阈值时才会删除候选采集目录。`Once` 目录受保护。
### `POST /api/storage/cleanup`
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `enabled` | 是 | 自动清理开关 |
| `min_free_gb` | 否 | 最低剩余空间,最小 `1 GB`,默认 `30` |
该接口会持久化配置,并立即执行一次空间检查。
### `GET /api/files`
查询参数:
| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `path` | 空 | 相对目录 |
| `page` | `1` | 页码 |
| `page_size` | `20` | 每页数量,最大 `20` |
目录在前、文件在后,名称按不区分大小写排序。隐藏项不会返回。
```json
{
"ok": true,
"root": "/nvme-data/pgu_data",
"path": "Once",
"parent": "",
"items": [
{
"name": "capture.jpg",
"path": "Once/capture.jpg",
"is_dir": false,
"size": 102400,
"mtime": 1785835208.1,
"mtime_text": "2026-08-04 17:20",
"ext": ".jpg",
"is_image": true,
"inline_image": true,
"child_count": 0
}
],
"total_count": 1,
"page": 1,
"page_size": 20,
"total_pages": 1,
"dir_count": 0,
"file_count": 1,
"total_size": 102400
}
```
### `GET /api/files/preview`
```text
/api/files/preview?path=Once/capture.tiff
```
仅接受图像文件。JPEG、PNG、WebP、BMP 可直接返回;TIFF 等图像会缩放到最大 `1800 x 1800` 并转换为 JPEG 预览。
### `GET /api/files/raw`
```text
/api/files/raw?path=Once/capture.jpg
/api/files/raw?path=Once/capture.jpg&download=1
```
`download=1` 时使用附件下载响应头,否则以内联方式返回。
### `POST /api/files/download`
`paths` 为换行分隔的相对路径,可同时包含文件和目录:
```bash
curl -X POST http://127.0.0.1:8080/api/files/download \
--data-urlencode $'paths=Once/a.jpg\nOnce/session'
```
返回 `application/zip`。隐藏文件、符号链接和越界路径不会加入压缩包。
### `POST /api/files/delete`
`paths` 同样为换行分隔。接口先校验路径,随后启动后台永久删除:
```json
{
"ok": true,
"message": "已开始后台删除 2 项",
"queued": true,
"paths": [
"Once/a.jpg",
"Once/session"
]
}
```
保存根目录不能删除。该操作没有回收站和撤销功能。
## 传感器与热成像控制
### `POST /api/sensors/power`
| 字段 | 说明 |
| --- | --- |
| `awake` | `1/true/on` 唤醒,否则休眠 |
唤醒时 MS602 和 DCL 立即启动;M3Lite 与 UVC-RGB 按各自延迟异步上电。休眠会停止相机、断开继电器电源并清除预览缓存。
### `POST /api/sensors/auto_sleep`
| 字段 | 说明 |
| --- | --- |
| `minutes` | 自动休眠分钟数;`0` 关闭 |
修改后立即写入 `pgu5a_config.json`。
`GET /api/status` 的 `sensor_power.auto_sleep` 返回:
```json
{
"minutes": 15.0,
"idle_for_sec": 120.0,
"remaining_sec": 780.0,
"enabled": true,
"paused": false,
"activity_running": false
}
```
定时采集开启或保存队列仍有任务时,`paused` 和 `activity_running` 为 `true`,自动休眠倒计时暂停。
### `GET /api/ms602/detection`
返回当前运行中的检测参数和六通道实时诊断:
```json
{
"ok": true,
"config": {
"startup_detection_window_sec": 5.0,
"jitter_detection_enabled": true,
"jitter_shift_min": 0.04,
"jitter_shift_max": 0.5,
"jitter_horizontal_max": 0.25,
"jitter_restart_reversals": 3,
"jitter_window_sec": 3.0,
"jitter_cancellation_ratio": 0.35,
"jitter_total_shift_min": 0.35,
"frame_integrity_detection_enabled": true,
"frame_integrity_phase_response_min": 0.55,
"frame_integrity_delta_min": 8.0,
"frame_integrity_restart_events": 2,
"frame_integrity_window_sec": 3.0,
"frame_tearing_gradient_ratio_min": 2.0,
"frame_tearing_row_diff_min": 5.5
},
"channels": {
"480": {
"status": "CONNECTED",
"device_index": 20,
"restart_count": 1,
"jitter_count": 0,
"frame_integrity_count": 1,
"detection_active": false,
"detection_remaining_sec": 0.0,
"detection_result": "passed",
"detection_run_count": 1,
"detection_sample_count": 24,
"last_phase_response": 0.98,
"last_frame_delta": 1.2,
"last_tearing_gradient_ratio": 1.31,
"last_tearing_row_diff": 4.58,
"last_integrity_trigger_events": 2,
"last_integrity_trigger_gradient_ratio": 2.64,
"last_integrity_trigger_row_diff": 8.01
}
}
}
```
### `POST /api/ms602/detection`
表单字段与范围:
| 字段 | 范围 | 说明 |
| --- | --- | --- |
| `startup_detection_window_sec` | `1.0..60.0` | 每次成功打开通道后的检测时长,默认 `5.0` 秒 |
| `jitter_detection_enabled` | 布尔值 | 开关小抖动检测 |
| `jitter_shift_min` | `0.01..10.0` | `160x100` 检测样本上的最小上下位移 |
| `jitter_shift_max` | `0.1..20.0` | 最大上下位移,必须不小于最小值 |
| `jitter_horizontal_max` | `0.01..10.0` | 允许的最大左右位移 |
| `jitter_restart_reversals` | `2..20` | 反向次数阈值 |
| `jitter_window_sec` | `1.0..30.0` | 抖动统计窗口,秒 |
| `jitter_cancellation_ratio` | `0.05..0.8` | 来回位移抵消比例阈值 |
| `jitter_total_shift_min` | `0.05..50.0` | 累计位移阈值 |
| `frame_integrity_detection_enabled` | 布尔值 | 开关撕裂检测 |
| `frame_integrity_phase_response_min` | `0.1..0.95` | 相位相关度阈值;调高更敏感 |
| `frame_integrity_delta_min` | `1.0..100.0` | 帧差阈值;调低更敏感 |
| `frame_integrity_restart_events` | `2..20` | 窗口内异常次数阈值 |
| `frame_integrity_window_sec` | `1.0..30.0` | 撕裂统计窗口,秒 |
| `frame_tearing_gradient_ratio_min` | `1.1..5.0` | 单帧纵向变化与横向变化的比例阈值 |
| `frame_tearing_row_diff_min` | `1.0..30.0` | 单帧横向断层强度阈值 |
保存成功后先写入 `pgu5a_config.json`,再更新运行中的 MS602 worker 并清空当前检测样本。参数立即用于仍在进行的开流自检;已经通过的通道不会因为保存配置而重新开始检测。该操作不重启服务、不重启整机,也不会主动重启正常通道。
每个通道只在首次启动、休眠唤醒或错误重连后开启独立检测窗口。窗口结束且未触发异常时,`detection_result` 变为 `passed`,后续预览帧在进入灰度转换、缩放、相位相关和帧差计算前直接返回。检测触发时只请求重启对应通道;通道重新打开后自动进入新一轮检测。
管理中心诊断表只显示 `device_index` 的数字节点编号,例如 `12`;API 仍返回完整的 `device_index` 和通道状态消息,后台设备路径与相机打开逻辑不变。
### `GET /api/m3/control`
返回低频轮询缓存:
```json
{
"available": true,
"ok": true,
"status": "OK",
"message": "热成像控制接口正常",
"fpa_temp_c": 43.12,
"core_temp_c": 43.13,
"auto_nuc": true,
"updated_at": 1785835205.2,
"updated_at_text": "2026-08-04 17:20:05",
"fpa_response_hex": "55 ... eb aa",
"core_response_hex": "55 ... eb aa"
}
```
控制链路使用 PyUSB bulk,不使用 `/dev/ttyACM0`。默认 VID:PID 为 `0424:6000`,接口 `3`,OUT `0x05`,IN `0x85`。
### `POST /api/m3/auto_nuc`
| 字段 | 说明 |
| --- | --- |
| `enabled` | `1/true/on` 开启,否则关闭 |
接口先发送自动 NUC 命令,再发送保存参数命令。成功响应包含 `response_hex`、`save_response_hex` 和更新后的 `m3_control`。传感器休眠、PyUSB 缺失或设备断开时返回 `ok=false`,不会影响主服务。
## 网络、HS 与 Syncthing
### `GET /api/network`
返回:
| 字段 | 说明 |
| --- | --- |
| `ap` | AP 开关、活动状态、接口、SSID、地址 |
| `sta` | STA 配置和连接状态 |
| `ethernet` | 以太网接口、IPv4、MAC |
| `scan` | 最近一次 Wi-Fi 扫描结果 |
| `syncthing` | 安装、服务和 GUI 可达状态 |
| `headscale` | HS 状态与 IPv4 |
| `serial` | 设备序列号 |
响应中可能包含 AP 密码,只能在可信管理网络中调用和记录。
### `POST /api/network/scan`
扫描 Wi-Fi 并更新 `/api/network` 的 `scan` 字段。
### `POST /api/network/ap`
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `enabled` | 是 | AP 开关 |
| `ssid` | 启用时是 | AP SSID |
| `password` | 启用时是 | 至少 8 位 |
会修改 NetworkManager 连接并持久化到配置。
### `POST /api/network/sta/connect`
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `ssid` | 是 | 目标 SSID |
| `password` | 否 | Wi-Fi 密码 |
连接成功后持久化 STA 配置。
### `POST /api/network/sta/disconnect`
断开 STA,并将配置中的 STA 开关设为关闭。
### HS 接口
```http
GET /api/network
POST /api/network/scan
POST /api/network/ap
POST /api/network/sta/connect
POST /api/network/sta/disconnect
GET /api/network/headscale/task
GET /api/network/headscale/task
POST /api/network/headscale/install
POST /api/network/headscale/uninstall
POST /api/network/headscale/login
POST /api/network/headscale/logout
```
HS login requires user-provided server and AuthKey. Do not provide or store a default AuthKey.
安装、卸载、登录和退出都是后台任务。启动接口返回 `task`,前端轮询 `GET /api/network/headscale/task`。
登录参数:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `server` | 是 | Headscale 服务地址 |
| `authkey` | 是 | 临时 AuthKey |
AuthKey 不应写入配置、日志或文档。任务正在运行时再次启动会返回 `ok=false`。
### Syncthing 身份初始化
```http
GET /api/syncthing/identity-init/status
POST /api/syncthing/identity-init/start
```
启动后会:
1. 停止 `syncthing@root.service`。
2. 只删除 `/root/.config/syncthing/cert.pem` 和 `key.pem`。
3. 重新启动 Syncthing。
4. 等待新身份文件生成。
保留 `config.xml`、文件夹配置、数据库和同步数据。状态接口返回 `running`、`ok`、`progress`、`stage`、`message` 和有界日志。
这是会改变设备身份的接口,常规健康检查不得调用。
## Web SSH
```http
POST /api/ssh/open
GET /api/ssh/read
POST /api/ssh/input
POST /api/ssh/close
GET /api/ssh/read
```
The backend starts `/usr/bin/ssh root@127.0.0.1` in a PTY. The frontend uses xterm.js.
后端启动 PTY,并执行:
## Config And Service
```text
/usr/bin/ssh root@127.0.0.1
```
| 接口 | 字段 | 说明 |
| --- | --- | --- |
| `/api/ssh/open` | `password` | 打开会话并完成本机 SSH 登录 |
| `/api/ssh/input` | `text` | 写入终端文本或控制字符 |
| `/api/ssh/read` | 无 | 读取增量输出和连接状态 |
| `/api/ssh/close` | 无 | 关闭 PTY 和子进程 |
密码仅用于当前请求,不应持久化。前端关闭终端窗口时必须调用 `close`。
## 配置、服务与电源
### `GET /api/config`
返回当前 `pgu5a_config.json` 的原始文本:
```json
{
"ok": true,
"message": "配置读取成功",
"path": "/root/PGU5-Core/pgu5a_config.json",
"text": "{\n ...\n}\n"
}
```
### `POST /api/config/save`
| 字段 | 说明 |
| --- | --- |
| `config_text` | 完整 JSON 对象文本 |
保存流程:
1. 解析并验证根节点为 JSON 对象。
2. 将旧配置备份到 `backups/config/`。
3. 用 UTF-8、两空格缩进和一个结尾换行写入。
4. 重新加载配置。
5. 异步重启 `pgu5a_base.service`。
该接口只验证 JSON 结构,不验证每个业务字段的类型和范围。编辑前应保留现场配置。
### 配置结构
顶层键:
| 键 | 用途 |
| --- | --- |
| `save_root` | 保存根目录 |
| `network` | AP 和 STA |
| `web` | Web、保存、刷新、休眠和清理 |
| `ms602` | MS602 采集与通道恢复 |
| `m3lite` | 热成像采集、上电和 USB 控制 |
| `dcl` | DCL 相机 |
| `uvc_rgb` | RGB 相机和继电器 |
`ms602` 恢复相关键:
```json
{
"open_stagger_sec": 0.5,
"read_fail_restart_count": 5,
"channel_reconnect_delay_sec": 0.8,
"startup_detection_window_sec": 5.0,
"jitter_detection_enabled": true,
"jitter_shift_min": 0.04,
"jitter_shift_max": 0.5,
"jitter_horizontal_max": 0.25,
"jitter_restart_reversals": 3,
"jitter_window_sec": 3.0,
"jitter_cancellation_ratio": 0.35,
"jitter_total_shift_min": 0.35,
"frame_integrity_detection_enabled": true,
"frame_integrity_phase_response_min": 0.55,
"frame_integrity_delta_min": 8.0,
"frame_integrity_restart_events": 2,
"frame_integrity_window_sec": 3.0,
"frame_tearing_gradient_ratio_min": 2.0,
"frame_tearing_row_diff_min": 5.5
}
```
小幅反向垂直抖动继续使用位移方向、抵消比例和累计位移判断,位移值基于 `160x100` 检测样本。对于画面撕裂、错行等坏帧,检测器同时统计跨帧低相位相关度大帧差,以及单帧横向断层梯度;在开流检测窗口内达到阈值后只重启对应 MS602 通道。`ms602_channels` 会附带检测是否运行、剩余秒数、结果、实际分析帧数、恢复计数、相位相关度、帧差和断层指标等诊断字段。`last_jitter_trigger_*` 与 `last_integrity_trigger_*` 保留最近一次触发证据,通道重新打开和保存配置后仍可用于诊断。
### 服务和电源接口
| 接口 | 行为 |
| --- | --- |
| `POST /api/service/restart` | 1 秒后重启主服务 |
| `POST /api/system/reboot` | 1 秒后重启操作系统 |
| `POST /api/system/shutdown` | 2 秒后执行 `sync` 和 `poweroff` |
这三个接口先返回 JSON,再执行后台动作。常规验证只允许调用服务重启;不得调用系统重启或关机。
## OTA
```http
GET /api/config
POST /api/config/save
POST /api/service/restart
POST /api/system/reboot
GET /api/ota/status
POST /api/system/update/check
POST /api/system/update/apply
```
Config save creates a backup under `backups/config/` and restarts the service.
OTA 状态:
## Release Notes For v1.1.8
```text
IDLE CHECKED FETCHING CHECKOUT VERIFYING RESTARTING SUCCESS ERROR
```
- Version strings are now documented as `v`-prefixed across app constants, UI, API, Git tags, and Gitea releases.
- OTA status is JSON-first and progress-modal friendly. Current version comes from `APP_VERSION`/`APP_RELEASE_TAG`; Git tag is diagnostic only.
- OTA target tags are fetched with forced tag refresh to support republished Gitea tags.
- M3/L640 thermal USB control exposes low-rate status and auto NUC control through `/api/m3/control` and `/api/m3/auto_nuc`.
### `GET /api/ota/status`
合并返回当前版本、最近一次远端检查结果和 `backups/ota_state.json` 中的任务状态。
### `POST /api/system/update/check`
查询 Gitea Release,返回 `latest` 和 `update_available`。只有 `update_available=true` 时前端才允许升级。
### `POST /api/system/update/apply`
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `tag` | 否 | 目标标签;空值时使用最近检查到的版本 |
标签必须包含前导 `v`。更新器强制刷新同名标签:
```bash
git fetch --force origin refs/tags/<tag>:refs/tags/<tag>
```
随后切换标签、执行 Python 编译验证并重启主服务。没有手动回滚接口,也不创建项目外 OTA 备份目录。
## 排除的应用接口
PGU5-Core 不提供应用层业务控制接口。以下路径应返回 HTTP `404`:
```text
/api/plc/*
/api/automation/*
/api/light_recipes
```
PLC、运动、水肥、业务路径、计划任务和应用专属身份不得通过 Core API 重新引入。
+110 -124
View File
@@ -1,168 +1,154 @@
# PGU5-Core
PGU5-Core is the Orange Pi 4 Pro core application for the PGU5A open management platform. It provides the embedded Web UI, camera preview/capture, sensor power control, file management, network setup, OTA upgrade, OLED status display, DS3231 RTC sync, and NVMe data mount integration.
PGU5-Core 是运行在 Orange Pi 4 Pro 上的 PGU5 开放平台核心程序,提供相机采集、预览与保存、传感器电源管理、文件管理、网络配置、OTA、Web SSH、OLED、RTC 和 NVMe 集成。
This is a PGU5-Core project, not the old seed-germination phenotyping platform. Do not reintroduce old AOT-only domains such as PLC/Modbus, precision watering, Hikrobot/MVS/GenICam, seed phenotype path logic, AOT-PhenoXYZ branding, or XYZ2 serial prefixes.
## 项目标识
## Identity
| Item | Value |
| 项目 | 当前值 |
| --- | --- |
| Product role | PGU5A open management platform |
| Browser title | `AOT-PGU5Base` |
| App version | `v1.1.8` |
| Release tag | `v1.1.8` |
| Serial number | `PGU5A-` + uppercase Ethernet MAC without separators |
| Web URL | `http://192.168.101.172:8080/` |
| Gitea repo | `http://107.173.157.41:3000/clear/PGU5-Core` |
| 产品 | PGU5 开放平台 |
| 应用版本 | `v2.0.5` |
| 发布标签 | `v2.0.5` |
| 设备序列号 | `PGU5A-` + 大写以太网 MAC |
| 现场目录 | `/root/PGU5-Core` |
| Web | `http://192.168.101.172:8080/` |
| Gitea | `http://107.173.157.41:3000/clear/PGU5-Core` |
版本字符串在应用常量、API、Git 标签和 Gitea Release 中必须保留前导 `v`。
## Release Version Rule
## 功能范围
Starting with `v1.1.8`, PGU5-Core uses `v`-prefixed version strings everywhere: app constant, release tag, UI display, OTA status, documentation, Git tag, and Gitea release. Do not publish bare versions such as `1.1.8`.
当前 Core 功能:
Release `v1.1.8` includes OTA progress/state cleanup, forced target-tag refresh, current-version display from app constants, compact Admin thermal-control UI, M3/L640 PyUSB bulk control, and refreshed project documentation.
- MS602 六通道采集、预览、保存,以及开流自检阶段的小幅反向抖动、低相关度撕裂帧和读帧失败独立通道恢复。
- M3Lite 热成像采集、温度数据保存和 PyUSB 控制。
- DCL 与 UVC-RGB/IMX766 采集、预览和保存。
- 单次保存、在线定时保存、保存队列和自动休眠。
- 文件浏览、预览、ZIP 下载、后台永久删除和空间自动清理。
- AP、STA、以太网、HS/Headscale 和 Syncthing 管理。
- Web SSH、配置编辑、服务重启、安全关机和 OTA。
- 管理中心提供多光谱控制页,可开关并调节小抖动与撕裂检测,保存后即时生效并持久化,同时查看六通道实时恢复诊断。
- 独立 OLED、DS3231 和 NVMe 服务。
## Runtime
不属于 Core 的功能:
| Item | Value |
- PLC/Modbus、运动控制、水肥控制。
- 业务自动化、计划任务和业务路径配置。
- Hikrobot/MVS/GenICam。
- 任何应用专属产品身份、业务页面或部署路径。
## v2.0.5
- 多光谱六通道诊断表的“设备”列仅显示视频节点编号,例如 `/dev/video12` 显示为 `12`,减少现场界面的实现细节。
- 后端 `device_index`、设备路径、运行日志和通道打开逻辑保持不变,不影响设备识别、恢复和 API 诊断。
## v2.0.4
- 新增管理中心“多光谱控制”页面,可持久化调整 MS602 开流检测时长、小抖动和撕裂检测参数,并查看六通道诊断。
- MS602 昂贵图像分析只在首次启动、休眠唤醒或错误重连后的短暂窗口内运行;通过后停止检测,读帧失败仍保持独立通道恢复。
- 增强持续撕裂和 720 通道轻微上下抖动识别,使用通道代次、独立锁和诊断证据避免旧线程影响新实例。
- 管理中心所有页面统一可滚动高度链;多光谱诊断表完整显示通道、设备节点和检测指标,移动端改为无横向溢出的分组布局。
- 修复暗色主题移动端导航按钮、系统设置菜单、根页面白边和多处文字/组件样式,统一非业务管理界面。
- 清理空响应式规则、Python 缓存和历史部署临时备份,保留现场配置、配置备份、运行日志和 OTA 状态。
## v2.0.3
- 全面升级桌面与移动端 Web UI,统一浅色/深色主题、弹窗、导航、文件管理和管理工具布局。
- 增加中英文界面切换、商业化产品文案、现代化品牌区和紧凑设备状态展示。
- 文件管理支持分页、图像预览、原始文件访问、ZIP 下载、后台永久删除和空间统计。
- Web SSH 静态资源改为按需加载,并随仓库保留许可证文件。
- 自动清理在用户编辑期间不覆盖表单,日志增加结构化时间信息和轻量提示反馈。
- 保留多相机独立恢复、异步传感器上电、M3/L640 PyUSB 控制、OTA、Syncthing、OLED、RTC 和 NVMe 能力。
- 清除旧工程标识、业务接口说明和不属于 Core 的历史残余。
完整接口、参数和副作用见 [API.md](API.md)。
## 运行结构
| 路径 | 用途 |
| --- | --- |
| Device | Orange Pi 4 Pro |
| Project path | `/root/PGU5-Core` |
| Runtime Python | `/opt/miniconda3/envs/py310/bin/python3` |
| Main service | `pgu5a_base.service` |
| OLED service | `pgu5a_oled.service` |
| Save root | `/nvme-data/pgu_data` |
| Main log | `/root/PGU5-Core/run.log` |
| `pgu5a_base.py` | Web、API、相机、保存、网络、OTA 和管理逻辑 |
| `pgu5a_config.json` | 现场运行配置 |
| `assets/` | Logo、按需加载的 xterm.js/xterm.css 及许可证 |
| `ota/ota_update.py` | 独立 OTA 更新器 |
| `oled/` | SSD1306 OLED 独立服务 |
| `ds3231/` | RTC/NTP 同步 |
| `nvme/` | NVMe 自动挂载 |
| `backups/config/` | 配置编辑器备份,不提交 |
| `backups/ota_state.json` | OTA 运行状态,不提交 |
The Pi-side `/root/PGU5-Core/pgu5a_config.json` is field configuration and should be treated as the source of truth.
现场 `pgu5a_config.json` 是配置事实来源,不应当作普通本地差异丢弃。
## Files
## 服务
| Path | Purpose |
| --- | --- |
| `pgu5a_base.py` | Main app: Web UI, HTTP API, camera workers, save queue, network, OTA, Web SSH, admin logic |
| `pgu5a_config.json` | Runtime config for cameras, network, save, M3 control, and power timing |
| `ota/ota_update.py` | Standalone OTA updater using forced tag fetch, checkout, py_compile, service restart |
| `assets/` | Static Web assets including xterm.js files |
| `oled/` | Independent SSD1306 OLED status service |
| `ds3231/` | RTC/NTP sync helper |
| `nvme/` | NVMe mount helper |
| `backups/config/` | Runtime config-editor backups, not committed |
| `backups/ota_state.json` | Runtime OTA state, not committed |
| 服务 | 用途 | 正常状态 |
| --- | --- | --- |
| `pgu5a_base.service` | Web、API 和相机 | `active` |
| `pgu5a_oled.service` | OLED | `active` |
| `ds3231-sync.service` | RTC/NTP 一次性同步 | 完成后可为 `inactive` |
| `nvme-data-auto.service` | NVMe 挂载 | `active (exited)` |
| `syncthing@root.service` | 文件同步 | `active` |
## Active Scope
主服务和 OLED 互相独立,OLED 失败不得影响 Web、API 或相机。
Active features include MS602, M3Lite, DCL, UVC-RGB/IMX766, timed save, online save, auto sleep, Web SSH, network admin, HS networking, Syncthing, OTA, OLED, DS3231, and NVMe.
## 相机与电源
Removed domains must stay removed: PLC/Modbus, precision watering, watering-device communication, Hikrobot/MVS/GenICam, AOT automation capture/watering, planned tasks, seed phenotype paths, AOT-PhenoXYZ identity, and XYZ2 serial prefix.
- MS602 和 DCL 唤醒时立即启动。
- MS602 六通道按 `ms602.open_stagger_sec` 间隔启动,当前默认 `0.5` 秒。
- MS602 每个通道仅在首次启动、休眠唤醒或错误重连后执行开流自检,默认持续 `5.0` 秒;通过后停止灰度、缩放、相位相关和帧差分析,正常取流不再持续承担检测开销。
- 开流自检期间分别判断小幅反向垂直抖动、跨帧低相关度大帧差和单帧横向断层;达到阈值时只重启受影响通道,并在重新打开后开始新一轮独立自检。
- 管理中心的“多光谱控制”页面可调整两类检测器的全部判断参数;保存写入 `pgu5a_config.json`,无需重启服务或整机。
- M3Lite 使用 GPIO 8 延时上电,当前唤醒延迟 `2.0` 秒。
- UVC-RGB 使用 GPIO 9 延时上电,当前唤醒延迟 `1.0` 秒。
- 休眠会递增唤醒令牌,取消尚未完成的延时启动。
- 定时保存和保存队列工作期间暂停自动休眠。
## OTA Strategy
## OTA
The Admin page has an OTA online upgrade card. The behavior is aligned with the same-hardware AOT project while keeping PGU5-Core identity and repository boundaries.
界面当前版本来自:
Current version shown in the UI must come from application constants:
- `APP_VERSION`
- `APP_RELEASE_TAG`
Git information is diagnostic only and must not override the visible current firmware version.
`GET /api/system/version` returns `app_version`, `release_tag`, `current_version`, `current_tag`, diagnostic `git_tag`, `commit`, `branch`, `dirty`, and `serial`.
The online upgrade button starts disabled. The user must click check update first. If no newer release is found, the button remains disabled. If a newer release is found, the button is enabled.
Upgrade opens a progress modal and polls:
```http
GET /api/ota/status
```python
APP_VERSION = "v2.0.5"
APP_RELEASE_TAG = "v2.0.5"
```
OTA states are `IDLE`, `CHECKED`, `FETCHING`, `CHECKOUT`, `VERIFYING`, `RESTARTING`, `SUCCESS`, and `ERROR`.
Git 信息只用于诊断。在线升级先检查 Gitea Release,只有发现更新后才启用升级按钮。OTA 状态包括:
Target tags must be refreshed with:
```text
IDLE CHECKED FETCHING CHECKOUT VERIFYING RESTARTING SUCCESS ERROR
```
更新目标标签使用强制刷新,以支持明确要求的同名标签修订发布:
```bash
git fetch --force origin refs/tags/<tag>:refs/tags/<tag>
```
Do not rely only on `git fetch --tags`, because republished same-name tags can otherwise fail with `would clobber existing tag`.
项目没有手动回滚接口,也不创建项目外 OTA 备份目录。
PGU5-Core OTA has no manual rollback UI and does not create project-external backup folders. Runtime OTA state lives in `backups/ota_state.json`.
## M3 / L640 Thermal USB Control
The M3/L640 thermal module exposes a USB composite device. Control uses PyUSB bulk transfers and must not depend on `/dev/ttyACM0`.
Default control settings:
| Item | Value |
| --- | --- |
| VID:PID | `0424:6000` |
| Interface | `3` |
| Bulk OUT | `0x05` |
| Bulk IN | `0x85` |
| Timeout | `2500 ms` |
| Poll interval | `5 s` |
Config keys under `m3lite`:
```json
{
"control_poll_sec": 5.0,
"control_interface": 3,
"control_ep_out": "0x05",
"control_ep_in": "0x85",
"control_timeout_ms": 2500
}
```
Commands:
| Function | HEX |
| --- | --- |
| FPA temperature | `AA 04 01 C3 00 72 EB AA` |
| Core temperature | `AA 04 01 7C 00 2B EB AA` |
| Enable auto NUC | `AA 05 01 01 01 01 B3 EB AA` |
| Disable auto NUC | `AA 05 01 01 01 00 B2 EB AA` |
| Save settings | `AA 04 01 7F 02 30 EB AA` |
After enabling or disabling auto NUC, always send the save-settings command.
Response parsing uses frame header, length, and tail as the primary validity check. Checksum is diagnostic. Temperature values are little-endian signed int16 divided by 100.
If PyUSB is missing, the device is sleeping, disconnected, or USB control fails, the API returns JSON with `ok: false`; the main Web service and camera preview must continue running.
The Admin page contains a compact thermal-control card near auto sleep. It shows FPA/core temperature and has buttons for enabling/disabling auto NUC. Results are shown in a feedback modal with response HEX values.
## Sensor Power
MS602 and DCL start immediately. M3Lite wakes asynchronously by waiting `m3lite.wake_power_delay_sec`, powering GPIO 8, and then starting the worker. UVC-RGB/IMX766 wakes asynchronously by waiting `uvc_rgb.wake_power_delay_sec`, powering GPIO 9, and then starting the worker.
Sleep increments `sensor_wake_token` so stale delayed wake threads cannot re-power devices after sleep. Auto sleep pauses while timed capture is enabled or save queue work is pending.
Current field timing is M3 startup check `3.0`, M3 wake delay `2.0`, UVC wake delay `1.0`.
## Validation
## 验证
```bash
cd /root/PGU5-Core
/opt/miniconda3/envs/py310/bin/python3 -m py_compile pgu5a_base.py
/opt/miniconda3/envs/py310/bin/python3 -m py_compile ota/ota_update.py
git status --short
/opt/miniconda3/envs/py310/bin/python3 -m py_compile \
pgu5a_base.py ota/ota_update.py oled/oled_status.py ds3231/sync_ds3231.py
/opt/miniconda3/envs/py310/bin/python3 -m json.tool pgu5a_config.json >/dev/null
/opt/miniconda3/envs/py310/bin/python3 -m py_compile oled/oled_status.py
systemctl restart pgu5a_base.service
systemctl is-active pgu5a_base.service
curl -fsS http://127.0.0.1:8080/api/system/version
curl -fsS http://127.0.0.1:8080/api/status
curl -fsS http://127.0.0.1:8080/api/network
curl -fsS http://127.0.0.1:8080/api/ota/status
curl -fsS http://127.0.0.1:8080/api/m3/control
curl -fsS -X POST http://127.0.0.1:8080/api/m3/auto_nuc -d 'enabled=1'
curl -fsS -X POST http://127.0.0.1:8080/api/m3/auto_nuc -d 'enabled=0'
curl -fsS http://127.0.0.1:8080/api/syncthing/identity-init/status
```
Frontend checks: navigation works, clock updates, Admin opens, OTA current version is correct, online upgrade is disabled before check, progress modal works, thermal-control card renders, M3 control returns JSON, browser console has no JS errors, mobile layout does not overflow.
常规验证不得调用系统重启、关机、Syncthing 身份初始化、文件删除或 M3 参数修改接口。
## Security
## 安全
Do not store passwords, AuthKeys, tokens, cookies, or credential-bearing remotes in source, docs, skills, or persistent config. Do not commit logs, `backups/`, captures, or `__pycache__/`.
- Git 远端不得包含用户名或密码。
- 不在源码、配置、文档和提交中保存 SSH 密码、AuthKey、令牌或 Cookie。
- 管理 API 没有独立登录层,只应在可信管理网络中开放。
- 不提交日志、采集数据、`backups/` 或 `__pycache__/`。
+189 -189
View File
@@ -1,190 +1,190 @@
#!/opt/miniconda3/envs/py310/bin/python3
# -*- coding: utf-8 -*-
import os
import time
import socket
import struct
import subprocess
from datetime import datetime
from smbus2 import SMBus
I2C_BUS = 0
DS3231_ADDR = 0x68
NTP_SERVERS = [
"ntp.aliyun.com",
"ntp.tencent.com",
"cn.pool.ntp.org",
"pool.ntp.org",
"time.google.com",
]
NTP_PORT = 123
NTP_TIMEOUT = 3
NTP_DELTA = 2208988800
# 启动后最多等多久给网络/NTP机会
NTP_RETRY_SECONDS = 60
NTP_RETRY_INTERVAL = 5
def log(msg: str):
print(msg, flush=True)
def run_cmd(cmd, check=False):
return subprocess.run(
cmd,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=check
)
def int_to_bcd(val: int) -> int:
return ((val // 10) << 4) | (val % 10)
def bcd_to_int(val: int) -> int:
return ((val >> 4) * 10) + (val & 0x0F)
def check_ds3231_present(bus_num: int) -> bool:
try:
with SMBus(bus_num) as bus:
bus.read_byte_data(DS3231_ADDR, 0x00)
return True
except Exception:
return False
def ds3231_write_datetime(bus_num: int, dt: datetime):
data = [
int_to_bcd(dt.second),
int_to_bcd(dt.minute),
int_to_bcd(dt.hour),
int_to_bcd(dt.isoweekday()),
int_to_bcd(dt.day),
int_to_bcd(dt.month),
int_to_bcd(dt.year - 2000),
]
with SMBus(bus_num) as bus:
bus.write_i2c_block_data(DS3231_ADDR, 0x00, data)
def ds3231_read_datetime(bus_num: int) -> datetime:
with SMBus(bus_num) as bus:
data = bus.read_i2c_block_data(DS3231_ADDR, 0x00, 7)
sec = bcd_to_int(data[0] & 0x7F)
minute = bcd_to_int(data[1] & 0x7F)
hour_reg = data[2]
if hour_reg & 0x40:
hour = bcd_to_int(hour_reg & 0x1F)
pm = bool(hour_reg & 0x20)
if pm and hour != 12:
hour += 12
elif not pm and hour == 12:
hour = 0
else:
hour = bcd_to_int(hour_reg & 0x3F)
day = bcd_to_int(data[4] & 0x3F)
month = bcd_to_int(data[5] & 0x1F)
year = 2000 + bcd_to_int(data[6])
return datetime(year, month, day, hour, minute, sec)
def set_system_time(dt: datetime):
time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
run_cmd(["date", "-s", time_str], check=True)
def is_rtc_time_reasonable(dt: datetime) -> bool:
return 2024 <= dt.year <= 2099
def query_ntp_time_once(server: str, timeout: int = 3):
packet = b"\x1b" + 47 * b"\0"
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(timeout)
s.sendto(packet, (server, NTP_PORT))
data, _ = s.recvfrom(48)
if len(data) < 48:
raise RuntimeError("NTP 响应长度不足")
transmit_timestamp = struct.unpack("!I", data[40:44])[0]
unix_time = transmit_timestamp - NTP_DELTA
return datetime.fromtimestamp(unix_time)
def query_ntp_time():
last_err = "未开始查询"
for server in NTP_SERVERS:
try:
dt = query_ntp_time_once(server, timeout=NTP_TIMEOUT)
return True, dt, server, f"NTP 查询成功: {server}"
except Exception as e:
last_err = f"{server} 查询失败: {e}"
return False, None, None, last_err
def main():
if os.geteuid() != 0:
log("[ERROR] 请以 root 运行")
return 1
log(f"[INFO] 使用 I2C 总线: /dev/i2c-{I2C_BUS}")
log(f"[INFO] DS3231 地址: 0x{DS3231_ADDR:02X}")
if not check_ds3231_present(I2C_BUS):
log("[ERROR] 未检测到 DS3231")
return 2
# 1) 启动早期先从 RTC 恢复
rtc_dt = ds3231_read_datetime(I2C_BUS)
log(f"[INFO] DS3231 当前时间: {rtc_dt.strftime('%Y-%m-%d %H:%M:%S')}")
if is_rtc_time_reasonable(rtc_dt):
set_system_time(rtc_dt)
log("[INFO] 已先使用 DS3231 时间设置系统时间")
else:
log("[WARN] DS3231 时间无效,跳过 RTC 恢复")
# 2) 再给网络/NTP一个等待窗口
deadline = time.time() + NTP_RETRY_SECONDS
ntp_ok = False
ntp_dt = None
ntp_msg = ""
while time.time() < deadline:
ntp_ok, ntp_dt, ntp_server, ntp_msg = query_ntp_time()
log(f"[INFO] {ntp_msg}")
if ntp_ok:
break
time.sleep(NTP_RETRY_INTERVAL)
log(f"[INFO] NTP最终判定: {'成功' if ntp_ok else '失败'}")
if ntp_ok:
set_system_time(ntp_dt)
log(f"[INFO] 已将 NTP 时间设置为系统时间: {ntp_dt.strftime('%Y-%m-%d %H:%M:%S')}")
ds3231_write_datetime(I2C_BUS, ntp_dt)
log("[INFO] 已将 NTP 时间写入 DS3231")
else:
log("[WARN] NTP 失败,保留当前系统时间不变")
verify_rtc = ds3231_read_datetime(I2C_BUS)
verify_sys = datetime.now()
log(f"[INFO] DS3231 最终时间: {verify_rtc.strftime('%Y-%m-%d %H:%M:%S')}")
log(f"[INFO] 系统最终时间: {verify_sys.strftime('%Y-%m-%d %H:%M:%S')}")
return 0
if __name__ == "__main__":
#!/opt/miniconda3/envs/py310/bin/python3
# -*- coding: utf-8 -*-
import os
import time
import socket
import struct
import subprocess
from datetime import datetime
from smbus2 import SMBus
I2C_BUS = 0
DS3231_ADDR = 0x68
NTP_SERVERS = [
"ntp.aliyun.com",
"ntp.tencent.com",
"cn.pool.ntp.org",
"pool.ntp.org",
"time.google.com",
]
NTP_PORT = 123
NTP_TIMEOUT = 3
NTP_DELTA = 2208988800
# 启动后最多等多久给网络/NTP机会
NTP_RETRY_SECONDS = 60
NTP_RETRY_INTERVAL = 5
def log(msg: str):
print(msg, flush=True)
def run_cmd(cmd, check=False):
return subprocess.run(
cmd,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=check
)
def int_to_bcd(val: int) -> int:
return ((val // 10) << 4) | (val % 10)
def bcd_to_int(val: int) -> int:
return ((val >> 4) * 10) + (val & 0x0F)
def check_ds3231_present(bus_num: int) -> bool:
try:
with SMBus(bus_num) as bus:
bus.read_byte_data(DS3231_ADDR, 0x00)
return True
except Exception:
return False
def ds3231_write_datetime(bus_num: int, dt: datetime):
data = [
int_to_bcd(dt.second),
int_to_bcd(dt.minute),
int_to_bcd(dt.hour),
int_to_bcd(dt.isoweekday()),
int_to_bcd(dt.day),
int_to_bcd(dt.month),
int_to_bcd(dt.year - 2000),
]
with SMBus(bus_num) as bus:
bus.write_i2c_block_data(DS3231_ADDR, 0x00, data)
def ds3231_read_datetime(bus_num: int) -> datetime:
with SMBus(bus_num) as bus:
data = bus.read_i2c_block_data(DS3231_ADDR, 0x00, 7)
sec = bcd_to_int(data[0] & 0x7F)
minute = bcd_to_int(data[1] & 0x7F)
hour_reg = data[2]
if hour_reg & 0x40:
hour = bcd_to_int(hour_reg & 0x1F)
pm = bool(hour_reg & 0x20)
if pm and hour != 12:
hour += 12
elif not pm and hour == 12:
hour = 0
else:
hour = bcd_to_int(hour_reg & 0x3F)
day = bcd_to_int(data[4] & 0x3F)
month = bcd_to_int(data[5] & 0x1F)
year = 2000 + bcd_to_int(data[6])
return datetime(year, month, day, hour, minute, sec)
def set_system_time(dt: datetime):
time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
run_cmd(["date", "-s", time_str], check=True)
def is_rtc_time_reasonable(dt: datetime) -> bool:
return 2024 <= dt.year <= 2099
def query_ntp_time_once(server: str, timeout: int = 3):
packet = b"\x1b" + 47 * b"\0"
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(timeout)
s.sendto(packet, (server, NTP_PORT))
data, _ = s.recvfrom(48)
if len(data) < 48:
raise RuntimeError("NTP 响应长度不足")
transmit_timestamp = struct.unpack("!I", data[40:44])[0]
unix_time = transmit_timestamp - NTP_DELTA
return datetime.fromtimestamp(unix_time)
def query_ntp_time():
last_err = "未开始查询"
for server in NTP_SERVERS:
try:
dt = query_ntp_time_once(server, timeout=NTP_TIMEOUT)
return True, dt, server, f"NTP 查询成功: {server}"
except Exception as e:
last_err = f"{server} 查询失败: {e}"
return False, None, None, last_err
def main():
if os.geteuid() != 0:
log("[ERROR] 请以 root 运行")
return 1
log(f"[INFO] 使用 I2C 总线: /dev/i2c-{I2C_BUS}")
log(f"[INFO] DS3231 地址: 0x{DS3231_ADDR:02X}")
if not check_ds3231_present(I2C_BUS):
log("[ERROR] 未检测到 DS3231")
return 2
# 1) 启动早期先从 RTC 恢复
rtc_dt = ds3231_read_datetime(I2C_BUS)
log(f"[INFO] DS3231 当前时间: {rtc_dt.strftime('%Y-%m-%d %H:%M:%S')}")
if is_rtc_time_reasonable(rtc_dt):
set_system_time(rtc_dt)
log("[INFO] 已先使用 DS3231 时间设置系统时间")
else:
log("[WARN] DS3231 时间无效,跳过 RTC 恢复")
# 2) 再给网络/NTP一个等待窗口
deadline = time.time() + NTP_RETRY_SECONDS
ntp_ok = False
ntp_dt = None
ntp_msg = ""
while time.time() < deadline:
ntp_ok, ntp_dt, ntp_server, ntp_msg = query_ntp_time()
log(f"[INFO] {ntp_msg}")
if ntp_ok:
break
time.sleep(NTP_RETRY_INTERVAL)
log(f"[INFO] NTP最终判定: {'成功' if ntp_ok else '失败'}")
if ntp_ok:
set_system_time(ntp_dt)
log(f"[INFO] 已将 NTP 时间设置为系统时间: {ntp_dt.strftime('%Y-%m-%d %H:%M:%S')}")
ds3231_write_datetime(I2C_BUS, ntp_dt)
log("[INFO] 已将 NTP 时间写入 DS3231")
else:
log("[WARN] NTP 失败,保留当前系统时间不变")
verify_rtc = ds3231_read_datetime(I2C_BUS)
verify_sys = datetime.now()
log(f"[INFO] DS3231 最终时间: {verify_rtc.strftime('%Y-%m-%d %H:%M:%S')}")
log(f"[INFO] 系统最终时间: {verify_sys.strftime('%Y-%m-%d %H:%M:%S')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+8731 -1894
View File
File diff suppressed because it is too large. Load diff
+25 -6
View File
@@ -5,15 +5,15 @@
"enabled": true,
"ifname": "ap0",
"base_ifname": "wlan0",
"connection_name": "PGU5C-AP",
"connection_name": "PGU5A-AP",
"ssid": "PGU5A-Base",
"password": "Bone1644",
"password": "",
"ipv4_address": "192.168.88.1/24"
},
"sta": {
"enabled": false,
"ifname": "wlan0",
"connection_name": "PGU5C-STA",
"connection_name": "PGU5A-STA",
"ssid": "",
"password": ""
}
@@ -38,13 +38,32 @@
"save_max_pending_groups": 2,
"sensor_auto_sleep_minutes": 0.0,
"sensor_wake_wait_sec": 7.0,
"auto_cleanup_enabled": false,
"auto_cleanup_enabled": true,
"auto_cleanup_min_free_gb": 30.0
},
"ms602": {
"target_fps": 8,
"gain": 8,
"d12": 0
"d12": 0,
"open_stagger_sec": 0.5,
"read_fail_restart_count": 5,
"channel_reconnect_delay_sec": 0.8,
"startup_detection_window_sec": 5.0,
"jitter_detection_enabled": true,
"jitter_shift_min": 0.04,
"jitter_shift_max": 0.5,
"jitter_horizontal_max": 0.25,
"jitter_restart_reversals": 3,
"jitter_window_sec": 3.0,
"jitter_cancellation_ratio": 0.35,
"jitter_total_shift_min": 0.35,
"frame_integrity_detection_enabled": true,
"frame_integrity_phase_response_min": 0.55,
"frame_integrity_delta_min": 8.0,
"frame_integrity_restart_events": 2,
"frame_integrity_window_sec": 3.0,
"frame_tearing_gradient_ratio_min": 2.0,
"frame_tearing_row_diff_min": 5.5
},
"m3lite": {
"width": 640,
@@ -83,4 +102,4 @@
"gui": {
"disk_refresh_sec": 5.0
}
}
}