Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c330d6327 | ||
|
|
ae3990b73e | ||
|
|
db1a636cfc |
No files matched your search
@@ -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,113 +1,158 @@
|
||||
# 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.9`; current release tag: `v1.1.9`.
|
||||
- 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.3`。
|
||||
- 版本字符串始终保留前导 `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.
|
||||
- MS602 opens its six channel capture threads with `ms602.open_stagger_sec` spacing; default is `0.2` seconds.
|
||||
- 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 必须完整显示画面。
|
||||
- 配置写入保持格式化 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 连续读帧失败或满足反向垂直抖动阈值时,只重启受影响通道。
|
||||
- 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` |
|
||||
|
||||
## 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。
|
||||
@@ -1,140 +1,177 @@
|
||||
# PGU5-Core API
|
||||
|
||||
Base URL:
|
||||
本文档对应 PGU5-Core `v2.0.3`,以 `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/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 预览 |
|
||||
|
||||
### 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/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,91 +180,470 @@ 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.3",
|
||||
"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/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,
|
||||
"jitter_detection_enabled": true,
|
||||
"jitter_shift_min": 0.4,
|
||||
"jitter_shift_max": 2.5,
|
||||
"jitter_horizontal_max": 0.5,
|
||||
"jitter_restart_reversals": 3,
|
||||
"jitter_window_sec": 3.0,
|
||||
"jitter_cancellation_ratio": 0.35,
|
||||
"jitter_total_shift_min": 2.0
|
||||
}
|
||||
```
|
||||
|
||||
### 服务和电源接口
|
||||
|
||||
| 接口 | 行为 |
|
||||
| --- | --- |
|
||||
| `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.9
|
||||
```text
|
||||
IDLE CHECKED FETCHING CHECKOUT VERIFYING RESTARTING SUCCESS ERROR
|
||||
```
|
||||
|
||||
- MS602 six-channel startup now uses configurable `ms602.open_stagger_sec`; default is `0.2` seconds between channel capture thread starts.
|
||||
- This is a PGU5-Core `main` release and does not add application-specific PLC logic.
|
||||
### `GET /api/ota/status`
|
||||
|
||||
## Release Notes For v1.1.8
|
||||
合并返回当前版本、最近一次远端检查结果和 `backups/ota_state.json` 中的任务状态。
|
||||
|
||||
- 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`.
|
||||
### `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 重新引入。
|
||||
@@ -1,170 +1,136 @@
|
||||
# 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 上的 PGU5A 开放管理平台核心程序,提供相机采集、预览与保存、传感器电源管理、文件管理、网络配置、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.9` |
|
||||
| Release tag | `v1.1.9` |
|
||||
| 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` |
|
||||
| 产品 | PGU5A 开放管理平台 |
|
||||
| 应用版本 | `v2.0.3` |
|
||||
| 发布标签 | `v2.0.3` |
|
||||
| 设备序列号 | `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.9` adds configurable MS602 six-channel startup staggering while keeping PGU5-Core application boundaries unchanged.
|
||||
- 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.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.
|
||||
## 相机与电源
|
||||
|
||||
MS602 six-channel startup uses `ms602.open_stagger_sec`; default is `0.2` seconds between channel capture thread starts.
|
||||
- MS602 和 DCL 唤醒时立即启动。
|
||||
- MS602 六通道按 `ms602.open_stagger_sec` 间隔启动,当前默认 `0.5` 秒。
|
||||
- M3Lite 使用 GPIO 8 延时上电,当前唤醒延迟 `2.0` 秒。
|
||||
- UVC-RGB 使用 GPIO 9 延时上电,当前唤醒延迟 `1.0` 秒。
|
||||
- 休眠会递增唤醒令牌,取消尚未完成的延时启动。
|
||||
- 定时保存和保存队列工作期间暂停自动休眠。
|
||||
|
||||
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.
|
||||
## OTA
|
||||
|
||||
## OTA Strategy
|
||||
界面当前版本来自:
|
||||
|
||||
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.3"
|
||||
APP_RELEASE_TAG = "v2.0.3"
|
||||
```
|
||||
|
||||
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
@@ -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())
|
||||
+8071
-1896
File diff suppressed because it is too large.
Load diff
+15
-5
@@ -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,14 +38,24 @@
|
||||
"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,
|
||||
"open_stagger_sec": 0.2
|
||||
"open_stagger_sec": 0.5,
|
||||
"read_fail_restart_count": 5,
|
||||
"channel_reconnect_delay_sec": 0.8,
|
||||
"jitter_detection_enabled": true,
|
||||
"jitter_shift_min": 0.4,
|
||||
"jitter_shift_max": 2.5,
|
||||
"jitter_horizontal_max": 0.5,
|
||||
"jitter_restart_reversals": 3,
|
||||
"jitter_window_sec": 3.0,
|
||||
"jitter_cancellation_ratio": 0.35,
|
||||
"jitter_total_shift_min": 2.0
|
||||
},
|
||||
"m3lite": {
|
||||
"width": 640,
|
||||
|
||||
Reference in new issue
Block a user