update server
This commit is contained in:
@@ -1,256 +0,0 @@
|
|||||||
# B 端需求拆解与实现说明
|
|
||||||
|
|
||||||
## 1. 需求翻译
|
|
||||||
|
|
||||||
系统包含两个流程:
|
|
||||||
|
|
||||||
1. 容积测量:A 端发起一次配置请求,B 端将严格 8 字段的配置 JSON 提交到该请求,A 端下载参数并调用 `start_volume_measurement`。
|
|
||||||
2. 辨识:B 端发布“函数 2 参数”;A 端采集稳定压力 JSON 和辨识 CSV 并上传;B 端打印稳定压力、下载新 CSV、绘图并人工返回 0/1。
|
|
||||||
|
|
||||||
辨识结果约定:
|
|
||||||
|
|
||||||
- `1`:参数通过,A 端结束本次辨识。
|
|
||||||
- `0`:参数未通过,B 端必须同时提交一套新的函数 2 参数,A 端下载后重新辨识。
|
|
||||||
|
|
||||||
## 2. 已实现内容
|
|
||||||
|
|
||||||
### Server 中转接口
|
|
||||||
|
|
||||||
- `getPendingVolumeConfigRequest`:返回指定设备当前等待 B 端响应的容积请求。
|
|
||||||
- `submitVolumeConfigFile`:将 B 端上传的容积配置绑定到对应请求。
|
|
||||||
- `publishIdentificationConfig`:按设备发布函数 2 的 CSV 配置。
|
|
||||||
- `getIdentificationConfig`:A 端按设备读取函数 2 配置。
|
|
||||||
- `setIdentificationFeedback`:B 端按设备与运行 ID 返回 0/1。
|
|
||||||
- `getPendingPanelFile`:按设备返回下一条待处理 CSV/JSON 消息。
|
|
||||||
- `ackPanelFile`:确认处理完成并删除 server 暂存文件。
|
|
||||||
- 参数发布和评审写入要求 `B_ADMIN_TOKEN`。
|
|
||||||
|
|
||||||
server 使用 `fileRecords` 保存上传文件索引,使用 `identificationFeedback`
|
|
||||||
保存当前设备待消费的辨识反馈。
|
|
||||||
|
|
||||||
### B 端本地程序
|
|
||||||
|
|
||||||
- `b-admin.js`:响应设备容积请求并上传配置 JSON,同时发布和读取函数 2 参数。
|
|
||||||
- `poll-panel-inbox.js`:获取 server 中待处理的数组 JSON 与辨识 CSV 消息。
|
|
||||||
- `plot-json.js`:将数字数组、数值对象数组或多个数值数组绘制为折线图。
|
|
||||||
- 新 CSV 到达后自动下载并生成上下组合时序图。
|
|
||||||
- 人工输入 0/1;输入 0 时读取新函数 2 JSON 并提交。
|
|
||||||
- 只有下载、绘图、评审提交全部成功后,文件才标记为已处理。
|
|
||||||
|
|
||||||
## 3. 参数契约
|
|
||||||
|
|
||||||
函数 1,对应 `start_volume_measurement`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"q_in_val": 91,
|
|
||||||
"dt": 0.1,
|
|
||||||
"xa_full": 1000,
|
|
||||||
"p_max": 200,
|
|
||||||
"fit_low": 50,
|
|
||||||
"fit_high": 200,
|
|
||||||
"T_delta": 30,
|
|
||||||
"num_runs": 6
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
函数 2,对应 `start_identification`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"q_in_val": 91,
|
|
||||||
"dt": 0.1,
|
|
||||||
"n_order": 8,
|
|
||||||
"t_c": 2.5,
|
|
||||||
"levels": [10, 20, 30, 40, 50, 60, 70, 80],
|
|
||||||
"dead_area": 0,
|
|
||||||
"xa_full": 1000,
|
|
||||||
"V_val": 1,
|
|
||||||
"repeat": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
示例数值仅用于联调,正式值需要算法或产品确认。
|
|
||||||
|
|
||||||
## 4. A 端调用契约
|
|
||||||
|
|
||||||
函数 1 使用一次性请求,不监听或扫描文件路径:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "createVolumeConfigRequest", "deviceId": "设备 ID" }
|
|
||||||
{ "type": "getVolumeConfigRequest", "deviceId": "设备 ID", "requestId": "请求 ID" }
|
|
||||||
{ "type": "ackVolumeConfigRequest", "deviceId": "设备 ID", "requestId": "请求 ID" }
|
|
||||||
```
|
|
||||||
|
|
||||||
B 端只能在请求有效期内提交容积配置;A 端确认接收后,server 删除请求和临时文件。
|
|
||||||
|
|
||||||
读取函数 2 参数:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "getIdentificationConfig", "deviceId": "设备 ID" }
|
|
||||||
```
|
|
||||||
|
|
||||||
查询某个辨识 CSV 的结果:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "getIdentificationFeedback",
|
|
||||||
"deviceId": "设备 ID",
|
|
||||||
"runId": "CSV 文件名"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
未评审时返回 `ready: false`;评审后返回 `ready: true` 和 `result: 0|1`。
|
|
||||||
当结果为 0 时,A 端重新调用 `getIdentificationConfig` 获取已经更新的 CSV。
|
|
||||||
|
|
||||||
## 5. Server 部署
|
|
||||||
|
|
||||||
1. 配置 server 环境变量 `B_ADMIN_TOKEN`、`HOST`、`PORT` 和可选的 `DATA_DIR`。
|
|
||||||
2. ControlPanel 与 ReinLoop 使用相同的 server URL 和设备 ID。
|
|
||||||
3. Panel 不扫描文件目录,仅按设备 ID 消费 server 消息队列。
|
|
||||||
4. CSV 处理完成后由 server 自动删除暂存文件。
|
|
||||||
|
|
||||||
## 6. B 端运行
|
|
||||||
|
|
||||||
PowerShell 环境变量:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:REINLOOP_API_URL="http://服务器地址:3000/api"
|
|
||||||
$env:B_ADMIN_TOKEN="与 server 相同的管理令牌"
|
|
||||||
$env:REINLOOP_DEVICE_ID="设备 ID"
|
|
||||||
$env:POLL_INTERVAL_MS="1000"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应 A 端当前待处理的函数 1 参数请求:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
node .\b-admin.js publish-volume .\volume-config.example.json
|
|
||||||
```
|
|
||||||
|
|
||||||
发布函数 2 参数:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
node .\b-admin.js publish-identification .\identification-config.example.json
|
|
||||||
```
|
|
||||||
|
|
||||||
读取当前函数 2 参数:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
node .\b-admin.js get-identification
|
|
||||||
```
|
|
||||||
|
|
||||||
启动监听与评审:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Electron 图形界面
|
|
||||||
|
|
||||||
首次使用安装依赖:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd ControlPanel
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
启动桌面应用:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm run gui
|
|
||||||
```
|
|
||||||
|
|
||||||
图形界面提供以下功能:
|
|
||||||
|
|
||||||
- 选择本地辨识 CSV,调用现有绘图模块生成并预览上下组合时序图。
|
|
||||||
- 打开并预览已有 PNG/JPG 绘图结果。
|
|
||||||
- 导入或直接编辑容积测量、系统辨识 JSON 配置。
|
|
||||||
- 读取当前系统辨识配置并发布新配置。
|
|
||||||
- 响应 ReinLoop 已发起的容积请求;没有待处理请求时拒绝上传。
|
|
||||||
- 容积配置发布前强制校验 8 个字段、数值类型以及 `num_runs` 整数类型。
|
|
||||||
|
|
||||||
Server API URL 和 Admin Token 可以在界面顶部输入,也可以在启动应用前设置
|
|
||||||
`REINLOOP_API_URL`、`B_ADMIN_TOKEN` 环境变量。Token 仅由 Electron 主进程用于请求,
|
|
||||||
不会保存到浏览器存储或配置文件。
|
|
||||||
|
|
||||||
### 打包 Windows EXE
|
|
||||||
|
|
||||||
在 `ControlPanel` 目录执行:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm install
|
|
||||||
npm run pack:win
|
|
||||||
```
|
|
||||||
|
|
||||||
构建结果输出到仓库根目录的 `Build` 文件夹:
|
|
||||||
|
|
||||||
- 安装版 EXE:运行后可选择安装目录,并创建桌面和开始菜单快捷方式。
|
|
||||||
- 便携版 EXE:无需安装,可直接运行。
|
|
||||||
- `win-unpacked`:未压缩的应用目录,适合排查打包后的运行问题。
|
|
||||||
|
|
||||||
应用包含原生 `canvas` 绘图模块,打包配置会自动将它从 ASAR 中解包。不要手动删除
|
|
||||||
`win-unpacked/resources/app.asar.unpacked`。未配置代码签名证书时,Windows 首次运行可能
|
|
||||||
显示 SmartScreen 提示;正式对外分发时应配置可信的 Windows 代码签名证书。
|
|
||||||
|
|
||||||
## 7. 仍需产品/A 端确认
|
|
||||||
|
|
||||||
- A 端数组 JSON 的最终结构尚未定义;当前兼容数字数组、数值对象数组和对象内多个数值数组。
|
|
||||||
- B→A 配置、A→B CSV、A→B 数组 JSON 都保存在 server 的 `DATA_DIR` 下。
|
|
||||||
- 两套参数示例中的正式默认值、单位和合法范围尚未定义。
|
|
||||||
- A 端上传稳定压力 JSON 与辨识 CSV 到 `<设备 ID>/ind_data`。
|
|
||||||
- A 端按 CSV 文件名登记 `runId`,B 端以相同 `runId` 提交反馈。
|
|
||||||
- 当前图像是否通过由 B 端人工判断;产品未提供自动判断算法或阈值。
|
|
||||||
|
|
||||||
## 8. 公司、产线、许可证与模型管理
|
|
||||||
|
|
||||||
Electron 工作台现已使用“公司 + 产线”选择代替手工设备 ID。Server 返回的
|
|
||||||
`deviceId` 固定为 `<company_code>/<production_line_code>`,配置发布、模型目录、
|
|
||||||
绘图收件箱和辨识反馈均使用同一个值。
|
|
||||||
|
|
||||||
应用启动时首先显示连接门禁页,只提供 Server API URL 和 Admin Token。点击
|
|
||||||
“连接并校验”后,主进程调用需要管理权限的 `listOrganizations` 接口同时检查
|
|
||||||
网络、API 地址和 Token;只有请求成功才显示公司、产线以及后续业务标签页。
|
|
||||||
连接失败时业务区保持隐藏并显示 Server 返回的错误。本次应用会话不提供更改连接
|
|
||||||
入口,需要切换 Server 或 Token 时重新启动应用。
|
|
||||||
|
|
||||||
公司与产线菜单会显示 `● 在线`、`○ 离线` 或 `◇ 状态未知`,并在连接成功后
|
|
||||||
每 10 秒静默刷新。在线状态来自 `listOrganizations` 中每条产线的 `online` 字段,
|
|
||||||
可选的 `lastSeenAt` 用于 Server 判断心跳是否超时;旧 Server 未返回该字段时显示
|
|
||||||
“状态未知”,不会误报在线。
|
|
||||||
|
|
||||||
组织管理页支持:
|
|
||||||
|
|
||||||
- 添加公司,编码只允许小写字母、数字、下划线和连字符。
|
|
||||||
- 在公司下添加产线;同一公司的产线编码必须唯一。
|
|
||||||
- 刷新组织后,顶部公司和产线菜单同步更新。
|
|
||||||
|
|
||||||
许可证页支持:
|
|
||||||
|
|
||||||
- 根据当前公司和产线签发许可证。
|
|
||||||
- 每次签发由操作者选择外部 RSA 私钥和本地保存位置;Panel 不保存或上传私钥。
|
|
||||||
- 许可证采用与 ReinLoop 相同的 RSA-PSS SHA-256 格式,并包含公司、产线和组合设备 ID。
|
|
||||||
- 本地文件写入成功后才登记 Server;登记失败会删除本次本地文件,避免半完成状态。
|
|
||||||
- 查看已签发许可证详情和撤销许可证。
|
|
||||||
|
|
||||||
模型管理页按当前产线列出 `<deviceId>/model_config`,支持上传、下载和删除。
|
|
||||||
|
|
||||||
绘图页收到辨识 CSV 后提供“通过/未通过”操作。选择未通过时必须先导入并成功
|
|
||||||
发布一份新的系统辨识配置,随后才提交数字 `0`;通过则提交数字 `1`。CSV 在结论
|
|
||||||
提交成功前不会从 Server 收件箱删除,应用中途退出后仍可重新获取。行程 JSON 在
|
|
||||||
绘图成功后直接确认。
|
|
||||||
|
|
||||||
Panel 当前依赖以下新增 Server type:
|
|
||||||
|
|
||||||
`listOrganizations`、`createCompany`、`createProductionLine`、`createLicense`、
|
|
||||||
`listLicenses`、`getLicense`、`revokeLicense`、`listControlFiles`、
|
|
||||||
`getControlFileDownload`、`deleteControlFile`。既有模型和反馈接口继续使用。
|
|
||||||
|
|
||||||
控制数据接口约定:`listControlFiles` 按 `deviceId` 分页返回控制结束后上传到
|
|
||||||
`<deviceId>/data_record/` 的文件元数据;`getControlFileDownload` 按 `fileID` 返回带时效的
|
|
||||||
下载 URL;`deleteControlFile` 按 `fileID` 删除文件和元数据。三者均应要求 B 端管理令牌,且
|
|
||||||
服务端必须验证文件归属控制数据目录,避免使用该接口操作其他业务文件。
|
|
||||||
|
|
||||||
## 9. 已知依赖风险
|
|
||||||
|
|
||||||
依赖审计仍报告第三方构建依赖存在安全告警。未执行可能引入破坏性升级的
|
|
||||||
`npm audit fix --force`,发布前应结合 Electron Builder 兼容性单独评估。
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# ReinLoop
|
||||||
|
|
||||||
|
ReinLoop 是一套面向压力控制、容积测量和系统辨识的完整系统,由以下三个部分组成:
|
||||||
|
|
||||||
|
- **ReinLoop 客户端**:基于 Modbus 的 Python GUI,提供 PID、强化学习和手动控制,并执行容积测量与系统辨识。
|
||||||
|
- **ControlPanel**:Electron B 端工作台,用于组织、许可证、模型、配置、辨识数据和控制数据管理。
|
||||||
|
- **Server**:Node.js 服务,负责文件中转、配置发布、辨识反馈、许可证与元数据持久化。
|
||||||
|
|
||||||
|
接口、权限边界、请求字段和响应格式统一维护在 [server/features.md](server/features.md)。
|
||||||
|
|
||||||
|
## 仓库结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ReinLoop/
|
||||||
|
├── ReinLoop/ # Python 控制客户端与 GUI
|
||||||
|
├── ControlPanel/ # Electron B 端工作台
|
||||||
|
├── server/ # Node.js / Express 服务
|
||||||
|
├── changelog.md # 变更记录
|
||||||
|
└── toserver.md # 服务端待办与协议说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心流程
|
||||||
|
|
||||||
|
### 压力控制、容积测量与辨识
|
||||||
|
|
||||||
|
ReinLoop 客户端通过 Modbus TCP 连接 MT2-AM8 模块,支持 PID、强化学习和手动控制。容积测量采用一次性配置请求:A 端发起请求,B 端提交经校验的配置后,A 端读取并执行测量。
|
||||||
|
|
||||||
|
系统辨识由 B 端发布参数,A 端上传稳定压力 JSON 与辨识 CSV。ControlPanel 下载并绘图后由操作者评审:结果为 `1` 时结束本轮辨识;结果为 `0` 时必须先发布一套新参数,A 端读取后重新辨识。
|
||||||
|
|
||||||
|
### 组织、许可证与模型
|
||||||
|
|
||||||
|
设备 ID 固定为 `<company-code>/<line-code>`,公司、产线、模型目录、配置发布、绘图收件箱和辨识反馈均使用该 ID。ControlPanel 支持:
|
||||||
|
|
||||||
|
- 新增公司与产线,并显示基于服务端心跳的在线状态;
|
||||||
|
- 签发、登记、查看、下载和撤销 RSA-PSS 许可证;私钥只在操作者本地使用;
|
||||||
|
- 按产线上传、下载、删除模型及管理控制数据;
|
||||||
|
- 发布容积测量和系统辨识配置,处理辨识数据与人工反馈。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
要求 Node.js 20 或更高版本。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd server
|
||||||
|
npm install
|
||||||
|
$env:B_ADMIN_TOKEN="your-admin-token"
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
默认服务地址为 `http://127.0.0.1:3000`,健康检查为 `http://127.0.0.1:3000/health`。业务请求可使用根路径或 `/api`。跨设备部署时,应使用服务器局域网 IP 或 HTTPS 域名,而不是 `127.0.0.1`。
|
||||||
|
|
||||||
|
### ControlPanel
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd ControlPanel
|
||||||
|
npm install
|
||||||
|
$env:REINLOOP_API_URL="http://server-address:3000/api"
|
||||||
|
$env:B_ADMIN_TOKEN="your-admin-token"
|
||||||
|
npm run gui
|
||||||
|
```
|
||||||
|
|
||||||
|
也可在界面顶部输入 Server URL 和管理令牌;令牌只由 Electron 主进程使用,不会保存到浏览器存储或配置文件。Windows 打包使用 `npm run pack:win`,产物位于仓库根目录的 `Build` 文件夹。
|
||||||
|
|
||||||
|
### ReinLoop 客户端
|
||||||
|
|
||||||
|
安装 Python 依赖后,在 `ReinLoop` 目录运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
客户端服务地址和设备目录由 `api.py` 配置。控制数据、辨识数据和容积测量结果经 Server 上传;许可证、模型、公司和产线操作由 ControlPanel 完成。
|
||||||
|
|
||||||
|
## 部署与数据
|
||||||
|
|
||||||
|
- 新生产环境使用 PostgreSQL:设置 `NODE_ENV=production` 与 `DATABASE_URL`;迁移可通过 `npm run migrate` 重复执行。
|
||||||
|
- 设置高熵 `B_ADMIN_TOKEN`、`LICENSE_PUBLIC_KEY_PATH`、`PUBLIC_BASE_URL`、`HOST`、`PORT` 和可选的 `DATA_DIR`。
|
||||||
|
- 生产环境应使用 HTTPS,并定期备份 PostgreSQL 与完整 `DATA_DIR`;不得提交、上传或保存 RSA 私钥。
|
||||||
|
- 模型存于 `data/models/<company-code>/<line-code>/`;其他文件由 `DATA_DIR` 管理。已处理辨识文件默认保留 30 天后清理。
|
||||||
|
- 非模型文件下载需要服务端签发的短期凭证;直接拼接文件地址会被拒绝。
|
||||||
|
|
||||||
|
外部访问出现 `502 Bad Gateway` 时,先检查 `http://127.0.0.1:3000/health`,再检查反向代理上游地址、端口和服务日志。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd server
|
||||||
|
npm run check
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 已知事项
|
||||||
|
|
||||||
|
- 稳定压力数组 JSON 的最终结构、两套参数的正式默认值、单位与范围仍需产品或算法确认。
|
||||||
|
- 图像是否通过目前由人工判断,尚未提供自动判定阈值或算法。
|
||||||
|
- Electron 的第三方构建依赖存在已知安全告警;未执行可能造成破坏性升级的 `npm audit fix --force`。
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
# ReinLoop V1.0 — 收敛有界
|
|
||||||
|
|
||||||
基于 Modbus 通讯的压力控制 GUI,支持 PID / 强化学习 / 手动三种控制模式。
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
pressure_control_gui/
|
|
||||||
├── main.py # 应用入口
|
|
||||||
├── PcControl.py # Modbus 通讯类
|
|
||||||
│ # MT2AM8Client - MT2-AM8 模块 TCP(AI 读压力/流量,AO 写电机)
|
|
||||||
│ # Easy521ModbusClient - PLC TCP(读压力/流量,备用)
|
|
||||||
│ # MotorModbusRTUClient - 电机 RTU(直接写位置,备用)
|
|
||||||
│ # PressureModbusRTUClient - 压力变送器 RTU(备用)
|
|
||||||
├── controllers.py # 增量式 PID 控制器
|
|
||||||
├── api.py # Express Server API 配置
|
|
||||||
├── styles.py # 全局 QSS 样式表
|
|
||||||
├── ind_collector.py # PRBS 辨识数据采集
|
|
||||||
├── get_V.py # 容积测量
|
|
||||||
├── license_utils.py # 许可证签发与校验
|
|
||||||
├── core/
|
|
||||||
│ ├── control_engine.py # 控制主循环(PID / RL / 手动),含 EMA 压力滤波
|
|
||||||
│ ├── connection_manager.py# 连接管理器(封装 MT2AM8Client)
|
|
||||||
│ ├── model_manager.py # RL 模型管理
|
|
||||||
│ ├── data_collector.py # 数据采集与云服务器上传
|
|
||||||
│ └── identification.py # 系统辨识与容积测量管理
|
|
||||||
├── ui/
|
|
||||||
│ ├── main_window.py # 主窗口(布局与信号槽绑定)
|
|
||||||
│ ├── connection_tab.py # 连接设置页(Modbus TCP)
|
|
||||||
│ ├── control_tab.py # 控制设置页
|
|
||||||
│ ├── debug_tab.py # 模型调试页(高级参数:死区/限幅/模拟量映射)
|
|
||||||
│ ├── status_bar.py # 底部状态栏
|
|
||||||
│ └── plot_window.py # 数据绘图窗口
|
|
||||||
├── src/ # SVG 图标资产
|
|
||||||
├── model_config/ # RL 模型配置文件
|
|
||||||
├── ind_data/ # 辨识数据本地输出目录
|
|
||||||
└── tool/ # 本地调试与诊断工具
|
|
||||||
```
|
|
||||||
|
|
||||||
## 环境要求
|
|
||||||
|
|
||||||
```bash
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 运行
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 控制模式
|
|
||||||
|
|
||||||
| 模式 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| **PID** | 增量式 PID,参数可在线调整,死区/限幅可配置 |
|
|
||||||
| **RL** | 强化学习模型实时预测 Kp/Ki,需先加载模型 |
|
|
||||||
| **手动** | 直接设定阀门开度百分比 |
|
|
||||||
|
|
||||||
控制周期由 PID 的 `dt` 参数决定(默认 0.1s),QTimer 驱动主线程执行,每周期末自动 sleep 补足时长保证精确周期。
|
|
||||||
|
|
||||||
## 硬件连接
|
|
||||||
|
|
||||||
GUI 主程序使用 **MT2-AM8 模拟量模块**(艾莫迅),通过 Modbus TCP 统一 IO:
|
|
||||||
|
|
||||||
- **MT2-AM8 模块**:Modbus TCP,默认 `192.168.1.12:502`,模块地址 1
|
|
||||||
- AI(输入寄存器 0x00~0x03):读压力传感器(4-20mA → 0-4095 → kPa)、读流量计
|
|
||||||
- AO(保持寄存器 0x00~0x03):写电机伺服驱动器(0-10V 模拟量控制行程)
|
|
||||||
- 模拟量映射范围、压力/流量量程可在界面中配置
|
|
||||||
|
|
||||||
### PcControl.py 中其他可用通讯类
|
|
||||||
|
|
||||||
以下类在 GUI 主循环中**未直接使用**,但可供独立脚本(如 `get_V.py` 的 `main()`)或调试调用:
|
|
||||||
|
|
||||||
| 类 | 协议 | 默认参数 | 用途 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `Easy521ModbusClient` | Modbus TCP | `192.168.1.88:502` | 读 PLC 压力寄存器 504(32-bit float)、写线圈控制 |
|
|
||||||
| `MotorModbusRTUClient` | Modbus RTU | `/dev/cu.usbserial-BG02B0IX`,站号 4,115200 | 通过 RS-485 直接读写电机驱动器寄存器 |
|
|
||||||
| `PressureModbusRTUClient` | Modbus RTU | `/dev/cu.usbserial-D30JITMY`,站号 1,9600 | 读压力变送器保持寄存器(备用) |
|
|
||||||
|
|
||||||
## 数据上传
|
|
||||||
|
|
||||||
控制数据(Episode)、辨识数据和容积测量结果通过 Express Server 的上传接口保存,
|
|
||||||
服务地址与设备目录配置在 `api.py`。许可证、模型和公司/产线等管理操作由
|
|
||||||
ControlPanel 完成,不由客户端工具执行。
|
|
||||||
+16
-2
@@ -119,9 +119,23 @@
|
|||||||
- `ControlPanel/electron-preload.js`
|
- `ControlPanel/electron-preload.js`
|
||||||
- `server/src/app.js`
|
- `server/src/app.js`
|
||||||
|
|
||||||
### Server 跟进
|
### Server:完善许可证撤销并完成部署
|
||||||
|
|
||||||
- 已在 `toserver.md` 中记录服务器侧建议:为 `revokeLicense` 增加明确错误码和必要的审计日志,并核对线上部署接口是否兼容 `POST /` 或 `POST /api` 的 `revokeLicense` 协议。
|
- `revokeLicense` 现在会去除 `licenseId` 首尾空白,并返回稳定错误码:`ADMIN_TOKEN_INVALID`、`ADMIN_TOKEN_NOT_CONFIGURED`、`LICENSE_ID_REQUIRED`、`LICENSE_NOT_FOUND`。
|
||||||
|
- 增加撤销审计日志,记录时间、可选请求 ID、许可证 ID、执行结果和错误码,不记录管理令牌。
|
||||||
|
- 兼容旧撤销请求类型 `revoke_license`、`licenseRevoke`、`revoke` 及字段 `license_id`、`admin_token`。
|
||||||
|
- 已完成 `npm run check`、完整服务端测试(19 项通过)、运行中服务验证,并重启 `reinloop-server.service` 部署。
|
||||||
|
|
||||||
|
涉及文件:
|
||||||
|
|
||||||
|
- `server/src/app.js`
|
||||||
|
- `server/test/server.test.js`
|
||||||
|
- `server/features.md`
|
||||||
|
|
||||||
|
### 文档:合并项目说明
|
||||||
|
|
||||||
|
- 将 ReinLoop 客户端、ControlPanel 和 Server 的三份说明合并为根目录 `README.md`。
|
||||||
|
- 删除已合并的 `B端实现说明.md` 和 `README1.md`;接口定义继续统一维护在 `server/features.md`。
|
||||||
|
|
||||||
### 验证
|
### 验证
|
||||||
|
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
# ReinLoop Express Server
|
|
||||||
|
|
||||||
该服务将原微信云函数中的文件中转、配置发布、辨识反馈和容积配置请求迁移到服务器。
|
|
||||||
请求体继续使用原来的 `type` 字段,因此 ReinLoop 和 ControlPanel 只需更换服务 URL。
|
|
||||||
|
|
||||||
## 本地运行
|
|
||||||
|
|
||||||
要求 Node.js 20 或更高版本。
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd server
|
|
||||||
npm install
|
|
||||||
$env:B_ADMIN_TOKEN="your-admin-token"
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
默认监听:
|
|
||||||
|
|
||||||
- 业务接口:`http://127.0.0.1:3000`(同时兼容原有 `/api` 路径)
|
|
||||||
- 健康检查:`http://127.0.0.1:3000/health`
|
|
||||||
|
|
||||||
ControlPanel 本地联调:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:REINLOOP_API_URL="http://127.0.0.1:3000"
|
|
||||||
$env:B_ADMIN_TOKEN="your-admin-token"
|
|
||||||
$env:REINLOOP_DEVICE_ID="local-test-device"
|
|
||||||
cd ControlPanel
|
|
||||||
npm run gui
|
|
||||||
```
|
|
||||||
|
|
||||||
ReinLoop 无 GUI 核心联调:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:REINLOOP_SERVER_URL="http://127.0.0.1:3000"
|
|
||||||
$env:REINLOOP_API_URL="http://127.0.0.1:3000"
|
|
||||||
$env:REINLOOP_DEVICE_ID="local-test-device"
|
|
||||||
```
|
|
||||||
|
|
||||||
如果 ReinLoop 或 ControlPanel 运行在其他设备上,不可使用 `127.0.0.1`,
|
|
||||||
应改为服务器的局域网 IP 或 HTTPS 域名。
|
|
||||||
|
|
||||||
业务请求可直接发送到域名根路径,也继续兼容 `/api`。反向代理需要将根路径完整转发到
|
|
||||||
Node 服务,例如 Nginx:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
外部访问返回 `502 Bad Gateway` 表示请求尚未到达 Express,通常是 Node 服务未运行、
|
|
||||||
代理的端口不一致或代理无法连接上游。先在服务器执行
|
|
||||||
`curl http://127.0.0.1:3000/health`,确认返回 `success: true`,再检查代理配置和服务日志。
|
|
||||||
|
|
||||||
## 数据与上传
|
|
||||||
|
|
||||||
- 开发和单元测试时,元数据保存在 `data/database.json`。全新生产部署必须设置
|
|
||||||
`NODE_ENV=production` 和 `DATABASE_URL`;已有 `database.json` 的旧生产实例可继续启动,
|
|
||||||
但会输出迁移警告。PostgreSQL 启动时会执行可重复的规范化表迁移。
|
|
||||||
- 模型文件保存在 `data/models/<公司编码>/<产线编码>/`。
|
|
||||||
- 模型上传可通过 `modelName` 重命名;数据库同时保存 `originalFileName`,供 Panel
|
|
||||||
显示和识别本地来源名称。未传 `modelName` 时保持原名。
|
|
||||||
- 其他上传文件保存在 `data/files/ReinLoop_GUI/`。
|
|
||||||
- ReinLoop 上传到 `<设备 ID>/ind_data` 的 CSV/JSON 会进入 Panel 消息队列;
|
|
||||||
Panel 处理并确认后,server 将其标记为已处理并保留,默认 30 天后自动清理。
|
|
||||||
- B 端可通过 `listIdentificationFiles` 查看暂存历史,通过
|
|
||||||
`getIdentificationFileDownload` 获取短期签名 URL 下载原始文件,也可通过
|
|
||||||
`deleteIdentificationFile` 显式删除。
|
|
||||||
- 除模型外,`/files/:fileID` 必须携带服务端签发且绑定文件与过期时间的下载 token;
|
|
||||||
直接拼接文件地址会返回 `403`。
|
|
||||||
- `uploadDataFile` 仍返回 `uploadMetadata`,现有 Python 与 ControlPanel 的 multipart
|
|
||||||
两步上传代码可以继续使用。
|
|
||||||
- 可通过 `DATA_DIR` 将数据目录放到独立磁盘。
|
|
||||||
- 单文件默认上限为 100 MB。
|
|
||||||
|
|
||||||
## API 参考
|
|
||||||
|
|
||||||
完整的业务功能、接口字段、权限边界、上传协议和流程说明见
|
|
||||||
[features.md](features.md)。
|
|
||||||
|
|
||||||
## PostgreSQL 与密钥
|
|
||||||
|
|
||||||
生产环境需要以下变量:
|
|
||||||
|
|
||||||
- `DATABASE_URL`:PostgreSQL 连接串。
|
|
||||||
- `B_ADMIN_TOKEN`:高熵管理令牌。
|
|
||||||
- `LICENSE_PUBLIC_KEY_PATH`:只读 RSA 公钥 PEM 路径,用于验证 Panel 已签名许可证。
|
|
||||||
- `PUBLIC_BASE_URL`:外部 HTTPS 根地址。
|
|
||||||
- `DATA_DIR`:文件存储目录;文件二进制仍保存在该目录的 `files/` 下。
|
|
||||||
- `IDENTIFICATION_RETENTION_MS`:已处理辨识 CSV/JSON 的保留时长,默认 30 天。
|
|
||||||
- `IDENTIFICATION_PURGE_INTERVAL_MS`:过期清理周期,默认 1 小时。
|
|
||||||
- `DOWNLOAD_TOKEN_TTL_MS`:非模型文件短期下载 URL 有效期,默认 5 分钟。
|
|
||||||
- `HOST`、`PORT`:监听地址和端口。
|
|
||||||
|
|
||||||
迁移可单独执行,且可重复运行:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
DATABASE_URL=postgres://... npm run migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
数据库保存文件元数据,文件本体目前需要共享卷或单实例部署;多实例部署前应改为对象存储。
|
|
||||||
不得上传、保存或提交 RSA 私钥。通过 HTTPS 部署,定期备份 PostgreSQL 与 `DATA_DIR`,
|
|
||||||
密钥轮换时先部署新公钥并验证新许可证,再废止旧签发私钥;恢复时先恢复数据库,再恢复同一
|
|
||||||
时间点的文件卷。
|
|
||||||
|
|
||||||
## 生产部署注意事项
|
|
||||||
|
|
||||||
1. 设置强随机 `B_ADMIN_TOKEN`,不要使用默认开发令牌。
|
|
||||||
2. 设置 `HOST=0.0.0.0` 并通过 Nginx/Caddy 提供 HTTPS,或由容器平台映射端口。
|
|
||||||
3. 设置 `PUBLIC_BASE_URL` 为外部 HTTPS 根地址,否则下载和上传 URL 会按请求 Host 生成。
|
|
||||||
4. 微信小程序后台需要把 HTTPS 域名加入 request、uploadFile 和 downloadFile 合法域名。
|
|
||||||
5. 定期备份 PostgreSQL 与整个 `DATA_DIR`;生产环境不可回退到 JSON 存储。
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm run check
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
测试会在随机本地端口验证健康检查、multipart 上传、文件列表、下载、辨识反馈和容积请求流程。
|
|
||||||
+1
-1
@@ -39,7 +39,7 @@ Panel 应每 10 秒调用 `listOrganizations` 刷新在线状态,不应自行
|
|||||||
| `createLicense` | Admin | `licenseId`、`companyId`、`productionLineId`、`customer`、`issued`、`expiry`、`features`、`license` | 创建已签名许可证。服务端以 RSA-PSS 公钥验签,校验签名载荷、组织关系和设备 ID。`issued`/`expiry` 使用 `YYYY-MM-DD HH:MM`,按 `Asia/Shanghai` 解析并存为 UTC。相同 ID 和内容幂等成功,不同内容冲突。 |
|
| `createLicense` | Admin | `licenseId`、`companyId`、`productionLineId`、`customer`、`issued`、`expiry`、`features`、`license` | 创建已签名许可证。服务端以 RSA-PSS 公钥验签,校验签名载荷、组织关系和设备 ID。`issued`/`expiry` 使用 `YYYY-MM-DD HH:MM`,按 `Asia/Shanghai` 解析并存为 UTC。相同 ID 和内容幂等成功,不同内容冲突。 |
|
||||||
| `listLicenses` | Admin | 无 | 返回许可证摘要列表,不返回原始 `license`。 |
|
| `listLicenses` | Admin | 无 | 返回许可证摘要列表,不返回原始 `license`。 |
|
||||||
| `getLicense` | Admin | `licenseId` | 返回完整许可证详情,可包含原始 `license`。 |
|
| `getLicense` | Admin | `licenseId` | 返回完整许可证详情,可包含原始 `license`。 |
|
||||||
| `revokeLicense` | Admin | `licenseId`、`reason` | 撤销许可证,保留历史、撤销时间和原因。 |
|
| `revokeLicense` | Admin | `licenseId`、`reason` | 撤销许可证,保留历史、撤销时间和原因。`licenseId` 会去除首尾空白。失败时返回 `errCode`:`ADMIN_TOKEN_INVALID`、`ADMIN_TOKEN_NOT_CONFIGURED`、`LICENSE_ID_REQUIRED` 或 `LICENSE_NOT_FOUND`。兼容旧类型 `revoke_license`、`licenseRevoke`、`revoke`,以及旧字段 `license_id`、`admin_token`。每次撤销会记录不含令牌的结构化审计日志。 |
|
||||||
| `validateLicense` | 无 | `licenseId`、`deviceId` | 返回 `valid`、`status`、`licenseId`。状态为 `active`、`revoked`、`expired`、`not_found` 或 `device_mismatch`;不泄露客户信息和许可证原文。 |
|
| `validateLicense` | 无 | `licenseId`、`deviceId` | 返回 `valid`、`status`、`licenseId`。状态为 `active`、`revoked`、`expired`、`not_found` 或 `device_mismatch`;不泄露客户信息和许可证原文。 |
|
||||||
|
|
||||||
许可证格式为 `payloadBase64|signatureBase64`。服务端只读取 `LICENSE_PUBLIC_KEY_PATH` 的公钥,绝不接收或保存 RSA 私钥。
|
许可证格式为 `payloadBase64|signatureBase64`。服务端只读取 `LICENSE_PUBLIC_KEY_PATH` 的公钥,绝不接收或保存 RSA 私钥。
|
||||||
|
|||||||
+46
-5
@@ -174,6 +174,32 @@ function createApp({
|
|||||||
return event.adminToken === adminToken ? null : "B端管理令牌无效";
|
return event.adminToken === adminToken ? null : "B端管理令牌无效";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logRevokeAction({ event, licenseId, success, errCode = null }) {
|
||||||
|
console.info("[license] revoke", {
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
requestId: event.requestId || null,
|
||||||
|
type: "revokeLicense",
|
||||||
|
licenseId: licenseId || null,
|
||||||
|
result: success ? "success" : "failed",
|
||||||
|
errCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEventCompat(event) {
|
||||||
|
if (!event || typeof event !== "object" || Array.isArray(event)) return event;
|
||||||
|
const type = String(event.type || event.action || "").trim();
|
||||||
|
const legacyRevokeTypes = new Set(["revoke_license", "licenseRevoke", "revoke"]);
|
||||||
|
if (!legacyRevokeTypes.has(type) && !(event.action === "revokeLicense" && !event.type)) return event;
|
||||||
|
console.warn("[license] legacy revoke request mapped", { type: type || null, requestId: event.requestId || null });
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
type: "revokeLicense",
|
||||||
|
licenseId: event.licenseId ?? event.license_id ?? event.id,
|
||||||
|
reason: event.reason ?? event.revocationReason,
|
||||||
|
adminToken: event.adminToken ?? event.admin_token ?? event.token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function signDownload(fileID, expiresAtMs) {
|
function signDownload(fileID, expiresAtMs) {
|
||||||
return createHmac("sha256", adminToken).update(`${fileID}\n${expiresAtMs}`).digest("hex");
|
return createHmac("sha256", adminToken).update(`${fileID}\n${expiresAtMs}`).digest("hex");
|
||||||
}
|
}
|
||||||
@@ -469,15 +495,30 @@ function createApp({
|
|||||||
}
|
}
|
||||||
case "revokeLicense": {
|
case "revokeLicense": {
|
||||||
const authError = requireAdmin(event);
|
const authError = requireAdmin(event);
|
||||||
if (authError) return { success: false, errMsg: authError };
|
const licenseId = String(event.licenseId || "").trim();
|
||||||
return store.update((database) => {
|
if (authError) {
|
||||||
const record = database.licenses.find((item) => item.licenseId === event.licenseId);
|
const errCode = authError === "B端管理令牌无效"
|
||||||
if (!record) return { success: false, errMsg: "许可证不存在" };
|
? "ADMIN_TOKEN_INVALID"
|
||||||
|
: "ADMIN_TOKEN_NOT_CONFIGURED";
|
||||||
|
const result = { success: false, errMsg: authError, errCode };
|
||||||
|
logRevokeAction({ event, licenseId, success: false, errCode: result.errCode });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!licenseId) {
|
||||||
|
const result = { success: false, errMsg: "licenseId 不能为空", errCode: "LICENSE_ID_REQUIRED" };
|
||||||
|
logRevokeAction({ event, licenseId, success: false, errCode: result.errCode });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const result = await store.update((database) => {
|
||||||
|
const record = database.licenses.find((item) => item.licenseId === licenseId);
|
||||||
|
if (!record) return { success: false, errMsg: "许可证不存在", errCode: "LICENSE_NOT_FOUND" };
|
||||||
record.status = "revoked";
|
record.status = "revoked";
|
||||||
record.revokedAt = new Date().toISOString();
|
record.revokedAt = new Date().toISOString();
|
||||||
record.revocationReason = String(event.reason || "管理员撤销").trim();
|
record.revocationReason = String(event.reason || "管理员撤销").trim();
|
||||||
return { success: true, license: publicLicense(record) };
|
return { success: true, license: publicLicense(record) };
|
||||||
});
|
});
|
||||||
|
logRevokeAction({ event, licenseId, success: result.success, errCode: result.errCode });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
case "validateLicense": {
|
case "validateLicense": {
|
||||||
const database = await store.read();
|
const database = await store.read();
|
||||||
@@ -978,7 +1019,7 @@ function createApp({
|
|||||||
|
|
||||||
const apiHandler = async (req, res) => {
|
const apiHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.json(await dispatch(req.body || {}, req));
|
res.json(await dispatch(normalizeEventCompat(req.body || {}), req));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({ success: false, errMsg: error.message });
|
res.status(400).json({ success: false, errMsg: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -619,6 +619,34 @@ test("issued licenses can be listed, validated, and revoked", async () => {
|
|||||||
assert.equal(rejected.status, "revoked");
|
assert.equal(rejected.status, "revoked");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("revokeLicense returns actionable errors and accepts legacy request fields", async () => {
|
||||||
|
const invalidToken = await post({
|
||||||
|
type: "revokeLicense", licenseId: crypto.randomUUID(), adminToken: "wrong-token"
|
||||||
|
});
|
||||||
|
assert.deepEqual(invalidToken, {
|
||||||
|
success: false, errMsg: "B端管理令牌无效", errCode: "ADMIN_TOKEN_INVALID"
|
||||||
|
});
|
||||||
|
|
||||||
|
const missingLicenseId = await post({ type: "revokeLicense", licenseId: " ", adminToken: "test-token" });
|
||||||
|
assert.deepEqual(missingLicenseId, {
|
||||||
|
success: false, errMsg: "licenseId 不能为空", errCode: "LICENSE_ID_REQUIRED"
|
||||||
|
});
|
||||||
|
|
||||||
|
const notFound = await post({
|
||||||
|
type: "revokeLicense", licenseId: crypto.randomUUID(), adminToken: "test-token"
|
||||||
|
});
|
||||||
|
assert.deepEqual(notFound, {
|
||||||
|
success: false, errMsg: "许可证不存在", errCode: "LICENSE_NOT_FOUND"
|
||||||
|
});
|
||||||
|
|
||||||
|
const legacy = await post({
|
||||||
|
type: "revoke_license", license_id: " ", admin_token: "test-token"
|
||||||
|
});
|
||||||
|
assert.deepEqual(legacy, {
|
||||||
|
success: false, errMsg: "licenseId 不能为空", errCode: "LICENSE_ID_REQUIRED"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("license creation verifies the signed payload and validates expiry in real time", async () => {
|
test("license creation verifies the signed payload and validates expiry in real time", async () => {
|
||||||
const company = await post({
|
const company = await post({
|
||||||
type: "createCompany", name: "安全校验公司", code: "security-co", adminToken: "test-token"
|
type: "createCompany", name: "安全校验公司", code: "security-co", adminToken: "test-token"
|
||||||
|
|||||||
Reference in New Issue
Block a user