HEAD 之前只有导入提交,阶段 1–9 的实现一直只存在于工作区,且与本次控制链改动
混在同一批文件里(如 HmiRuntimeView.vue / points.go / plbridge/main.go)。
这次按当前状态一次性入库,作为检查点。
服务端(零 cgo)
- iam:双因素登录、刷新令牌轮换(重用即撤整链)、独立 step-up、六角色 + 权限点 + ABAC 范围
- audit:append-only + SHA256 前向链,库层触发器禁 UPDATE/DELETE
- points:project.db 只读挂载,查询按调用者范围裁剪;**YT 点补 min/max/step**(本次)
- rt:一条上游订阅流被所有浏览器共享的合流器 + WS hub(令牌复核、慢消费者保护)
- history:taosAdapter REST(自行解析 TDengine 2.6 的 column_meta,官方 v3 驱动解不了)
- hmi:图元(props/events/builtin)、模板与 rev、画面树与发布、批量升级/回滚、工程包
- control:控制下发编排(本次新增)
- api:路由 / 鉴权中间件先于路由 / 审计 / 配置反代(方法分流 + step-up + 范围裁剪)
桥(cgo,独立 go.mod)
- rtdb:libphotolink_rtdb_capi 只读封装,写方法在类型边界上不可达
- ctrl:libphotolink_ctrl 控制面(本次新增)—— SELECT→EXECUTE→等 CtrlEvent
- ring / grpcserver / supervise / cmd{plbridge,probe,ctrlprobe}
- api/proto:bridge.proto(只读数据面)+ ctrl.proto(控制面,独立的 BridgeCtrl 服务)
前端
- 实时点表(视口驱动订阅、每点一槽位)、历史、告警、组态编辑器与运行态(自绘 DOM/SVG)、
16 个配置视图、认证与审计页
- 组态按钮真下发(本次新增):POST /api/v1/control/write,按设备回执提示而非"已发送"
控制链(本次新增,板载实测)
- 控制只支持遥控/遥调(YK/YT);YC/YX 一律 L5012(底层约定)
- ABAC 范围 L4031 → YT 值域 L5013 → 同点"在飞"去重 L5011 → 同点 500ms 冷却 L5015(429,
LCVIEW_CONTROL_COOLDOWN_MS 可调、0=关闭) → 桥控制面 → 成功失败都写审计
- 结果取自设备回执:accepted 只表示引擎收下;TIMEOUT 按"结果不确定"呈现,绝不自动重试
- 板载实测:1.35.YT ← 25 → OK 43ms,读回 1.35.YC=25 / 5.35.YC=25;
连点第二次 429 L5015「距上次 63 ms,限流 500 ms」;600 ms 后放行
验证:server `go test ./...` 全绿;bridge `go test -race ./...` 全绿;web `vue-tsc` 零错误。
|
||
|---|---|---|
| api | ||
| bridge | ||
| config/hmi | ||
| deploy | ||
| server | ||
| tools | ||
| web | ||
| .gitignore | ||
| development-outline.md | ||
| go.work | ||
| go.work.sum | ||
| misty-reef-char.md | ||
| README.md | ||
lcview
PhotoLink 的唯一 Web 门户:实时监视 + 组态 + 历史 + 告警 + 控制 + 策略配置 + 统一认证与审计。 开发路线图见 development-outline.md,实现级规格见 misty-reef-char.md。
仓库结构
| 目录 | 说明 |
|---|---|
server/ |
lcview 主进程,零 cgo,可静态交叉编译到 ARM64(module lcview/server) |
bridge/ |
plbridge,cgo 桥(module lcview/bridge,独立 go.mod) |
api/ |
bridge.proto 与生成的 gRPC Go 代码,server 与 bridge 共用 |
web/ |
Vue 3 + TS + Vite + TDesign 前端 |
tools/bridge-mock-so/ |
RTDB 桩库,无采集数据源时验证全链路 |
deploy/ |
systemd 单元与联调/冒烟脚本 |
两个进程
浏览器 ─HTTP(S)→ lcview(:8443) ─gRPC over UDS→ plbridge ─cgo→ libphotolink_rtdb_capi.so
bridge 独立成进程的两个硬理由:cgo 破坏零依赖交叉编译;共享内存与 ipc:// 是主机本地语义,必须与 PhotoLink 同机。
开发
bash deploy/dev-up.sh start # 构建并起 plbridge + lcview(默认桩库,不依赖数据源)
bash deploy/smoke.sh # 阶段 1 验收:21 项断言(快照/健康/错误语义/断连恢复)
bash deploy/dev-up.sh status|stop|restart
前端开发期单独跑:cd web && pnpm dev(:5173,mock 数据,?demo=<角色> 可免登录看角色差异)。
读真实 SHM(去掉 LD_LIBRARY_PATH 遮蔽即切回真库):
RTDB_IMPL=rtdb LCVIEW_RTDB_IMPL=rtdb_capi bash deploy/dev-up.sh start
测试
cd bridge && go test -race ./... # ring 无锁语义、订阅差集、慢消费者保护、seq 单调
工具链要求:Go ≥ 1.25、protoc + protoc-gen-go/protoc-gen-go-grpc(改 api/proto 后需重新生成)。
protoc --proto_path=api/proto \
--go_out=api --go_opt=module=lcview/api \
--go-grpc_out=api --go-grpc_opt=module=lcview/api \
api/proto/bridge/v1/bridge.proto
当前进度
阶段 1(骨架跑通)已完成:两进程经 UDS 连通,快照可用,bridge 重启后 1s 内恢复。./deploy/smoke.sh 20 项。
阶段 2(登录 + 权限 + 审计)已完成,含前端接线:
internal/cryptoargon2id 口令、RFC 6238 TOTP(±1 周期容偏,已知答案向量测试)、ES256 令牌 + JWKS、AES-GCM 封装 TOTP 密钥internal/iam双因素登录、刷新令牌轮换与重用即撤整条家族链、step-up 时效、口令强度与历史复用、失败锁定、六角色 + 23 权限点 + ABAC 范围 + 三权分立互斥- step-up 只认独立一次 TOTP(
sessions.stepup_time,令牌声明sua并随刷新前传);双因素登录不算复核
- step-up 只认独立一次 TOTP(
internal/auditappend-only + SHA256 前向链;库层触发器禁止 UPDATE/DELETE;查询/分页/CSV 导出/integrity 校验internal/api鉴权中间件先于路由(匿名者无法靠状态码探测端点)、权限点门禁、状态变更全量审计、无权路由重定向- 反代
config_server_go:剥离门户令牌不外传、危险端点需config.danger+X-Confirm、lua-run默认按部署基线封死;上游-bind默认值已改为127.0.0.1 - TLS:
tlsHardened()(≥TLS1.2)+deploy/tls/gen-cert.sh自签脚本 - 前端:
api/client.ts(信封解包、Bearer 注入、401 静默刷新且并发去重、L4032 唤起全局 step-up 弹窗)、auth store 改服务端下发 perms/scope、登录页接真接口、审计页与用户与权限页取真数据;Vite dev 加/api代理
./deploy/smoke.sh 20 项 + ./deploy/smoke2.sh 46 项,go test ./... 全绿,前端 vue-tsc 零错误。
阶段 3(实时点表)后端已完成,前端待接线:
internal/points只读挂载上游project.db(mode=ro,缺表即拒启),分组/点表/单点/搜索四类查询全部在 SQL 层按调用者 ABAC 区间裁剪- 点名搜索用
LIKE … LIMIT而非 FTS5:unicode61把整段中文当一个词元,实测搜"电压"命中 0 条而 LIKE 命中 128 条;%/_按字面转义 internal/rtLive合流器:一条上游订阅流被所有浏览器共享(键并集去重、引用式退订),250ms tick 只推变化、同点多次变化保留最新值;无变化的 tick 不产生帧;订阅即补一次batch_read快照,避免首屏空窗GET /api/v1/ws/realtime(op走 auth/sub/unsub/resync/ping):令牌在首条消息里(浏览器套接字带不了 header)、同源 Origin 校验、每 60s 复核令牌(撤销会话立刻断流)、单连接 5000 点上限、积压 >4 帧判定慢消费者并断开- 断流重订阅:新流建立即重发全量期望集(声明式协议使重放幂等),实测 bridge 进程消失后亚秒级恢复,验收线 3s
- 新端点:
GET /api/v1/groups、/groups/zones、/groups/{gid}/points、/points/search、/points?key=、/rt/groups;/rt/health增hub(连接数/并集/重连次数/上游缺口) tools/pointgen造 200 组 / 100.4 万点的同构库;压测数字:搜索 p95 2ms、点表分页 p95 2ms、分组首屏 p95 62ms(目标分别为 300ms / 300ms / 1s)
./deploy/smoke3.sh 29 项(含 deploy/wsprobe.mjs 的 WS 端到端探针)+ go test ./... 全绿;实时链路的合流/共享/重连/慢消费者另有 7 项 Go 测试用进程内替身上游(internal/bridgetest)驱动。
阶段 3.3 前端接线已完成:
web/src/api/rtws.ts实时通道客户端:首帧 auth、sub/unsub 150ms 合并、seq 缺口自动 resync、指数退避重连、令牌轮换就地重认证;无令牌不握手(未登录页签按退避重连,每次失败都会往审核链里塞一条)stores/rt.ts改为每点一个 ref 的槽位:只有真正变化的点才唤醒依赖它的组件;订阅数显式记账,退订即回收槽位(一班值守翻动的点远超 5000)- 只订阅看得见的点:
composables/useVisible.ts用 IntersectionObserver(含 300px 预取)把订阅集从"整页 200 行"收到"视口内 16 行";页签切到后台就断开——Chrome 会在网络层替我们回 ping,留着连接等于让看不见的页签一直占着后端订阅 - 点表页接真接口:区域/分组树、服务端分页与按类型切换、组内点名过滤、
?jump=gid.idx.TYPE定位高亮;"没收到值"与"值为 0"分开呈现(占位色 + 质量码"无值"),且只有可见行没值才用告警色 - 数据源可切:默认走真链路,
localStorage['lcview.rt-source']='mock'或VITE_RT_SOURCE=mock回到本地 250ms 仿真(设计评审用);状态栏标注来源与"桩库 mock-rtdb" - 静态资源按需 gzip:首屏 3.0MB → 0.82MB;哈希命名的
/assets/*给immutable,index.html给no-cache(否则改版后运营终端拿旧壳);Vite dev 代理补ws: true - 修掉三处路由离开不清理的
setInterval(驾驶舱两个、运维页一个) - 界面验收走真无头 Chrome + CDP(内置浏览器视口是 0×0,IntersectionObserver 永不命中):点表/驾驶舱/运维/控制台四页零 JS 异常、无 NaN
仍走 mock 的页面:驾驶舱、历史、告警、控制台(数据面属阶段 4/6/7);点表、全部配置页、组态运行态/编辑器/批量生成已接真数据,状态栏里 mock 条目用斜体弱对比标注。
待验一项:桩库路线下"快照值与真实 SHM 一致"仍未验证,需接上采集数据源后补一次。
阶段 8 前端已完成(2026-09-17):
- 运行态渲染
HmiRuntimeView:X6 只管静态图元且必须开virtual(spike ② 结论),动态数值走 X6 容器的兄弟节点 HTML 层,刷新只改textContent、平移缩放只给容器加一个matrix;值来自stores/rt的视口订阅,图元states按声明顺序首条命中着色(TS 端hmi/states.ts与 Go 的MatchStates/MatchWhen逐条对齐,否则同一画面两处变色不同)⚠️ 这一条已被阶段 9 取代:X6 整体移除,编辑态与运行态共用一个自绘 DOM/SVG 渲染器。保留在这里是为了说明当时为什么分两层(下面的"根因"就是它导致的)。
- 发布驱动导航:
GET /hmi/nav决定"实时监控"里有哪些画面,未发布/越界的画面不出现在菜单也打不开;/monitor/hmi/:pageId为参数化路由 - 画面编辑器
HmiEditorView:图元面板拖拽入画、点选后按图元槽位绑点位(PointSelect走门户点名检索,越界点搜不到)、保存只写作者意图(symbol/位置/绑定,运行态算出来的样式一律不落文档)、发布需确认;只用了 X6 核心,没引入@antv/x6-plugin-*,因此没有框选/对齐线/小地图/撤销重做,连线改为"点两个图元"⚠️ 这一条已被阶段 9 取代:撤销重做、复制粘贴、图层树、对齐辅助线、网格吸附、八向手柄、图元缩略图都已具备。
- 批量生成器
HmiGenerateView+POST /hmi/generate:模板实例化 N 组,*_base/*_pid这类点位偏移按每组点表推断,推不出来进"待人工确认"且该组不落库,确认后用overrides回填再生成;dry_run只出计划 - 验收:
./deploy/smoke7.sh22 项(预演不写库 / blocked 不留半成品 / 回填后生成并渲染 / 发布进导航 / 再生成只跳过 / 丢 symbol 的文档被拒 / 模板实例拒绝直写文档 / 渲染吃 ABAC);界面真点:运行态、编辑器、生成器三个页共 12 项断言(含 overlay 数值两次采样不同即"真在刷新") - 一起修掉的两个真缺陷:
toGraphJSON丢了symbol字段导致编辑器导出把画面写空(服务端已加校验拒绝声明为 symbol 却无 symbol 的文档);运行态与编辑器的属性面板此前会把"选中边"和"选中的节点解析不出图元"混成同一句话
阶段 8 后端已完成(2026-09-09):
- 图元库:19 个图元(
config/hmi/symbols/<id>/symbol.json),服务端把 SVG 净化成元素树(白名单:g/rect/circle/ellipse/line/polyline/polygon/path/text/tspan),装载失败逐条上报 - 模板引擎:表达式语言 +
repeat+ rev 台账;500 图元展开实测 2.9ms - 画面注册表:画面树 / 发布 / 批量升级与回滚 / 渲染缓存 / ABAC 双路裁剪;模板发新 rev 不自动传播,须显式批量升级
- 验收:
./deploy/smoke4.sh30 项、./deploy/smoke5.sh34 项
阶段 5(策略与配置)已完成,含 16 个视图移植:
- 闸门改成按方法分流:
api.config.read(sysadmin+engineer,能看)与api.config.write(仅 sysadmin,且必须过一次独立 step-up);原api.config.proxy已删除,启动时自动回收废弃权限点与其授予 - 配置面开始吃 ABAC:路径
/groups/{gid}、列表filter的 group_id、写正文里的 group_id/*_group_id/group_ids 都要落在调用者范围内(此前代理完全不看范围) - 写请求把正文样本写进审计(4 KiB 取样、UTF-8 边界截断、multipart 只记类型与字节数),此前只有 method+path
- 前端 16 视图全部落到
web/src/views/config/+web/src/components/config/:字段清单收在fields.ts一处(老工具把它抄在 .vue / ColumnRules / 字段字典三处且已漂移),通用ResourcePage/DynamicForm/InlineGrid/CellEditor,规约表由/meta/protocol-schemas驱动不写死表名列名 - 表达式编辑器按运行时真实文法校验:
max/min/比较/&&/@dur/$1..128.1.YC一律当场报错(C++ 侧未知函数会静默按 sum 算),点位候选改走门户点名检索 - 修掉老前端六处确定错误:链路搜索与协议过滤互相屏蔽、触发器对话框列出全部脚本(
?script_id=上游根本不认)、保存链路+关联分组失败不区分、批量粘贴索引跨页撞车、切页签静默丢改动、导入结果表被当成逐 sheet 报告 - 验收:
./deploy/smoke6.sh42 项,跑在一次性夹具上:
脚本开头自检、结尾断言"仓库源库与现网库指纹未变、组态资产一个没少",删除只对./deploy/cfg-fixture.sh start # 复制 project.db 到 .run/cfg 并起 127.0.0.1:18080 LCVIEW_CONFIG_UPSTREAM=http://127.0.0.1:18080 ./deploy/dev-up.sh restart ./deploy/smoke6.shsmoke_前缀行发起 - 顺带发现两处"源码已改、现网二进制没跟上":
/opt/photolink/bin/config_server(08-13 构建)的-bind默认仍是0.0.0.0;lua-scripts/lua-triggers在该二进制上是 404。处理办法见./deploy/config-server-loopback.sh check(只读诊断)与build(重编到.run/,含 arm64);替换现网要显式install --yes - 界面真点验证(真无头 Chrome + CDP,门户自身托管的构建产物):16 个配置页全部通过,零 JS 异常、无 NaN/undefined,种子数据渲染符合预期(分组点位 20 行、协议插件 6 行、字段字典 144 行、进程管理 14 行);并跑通一次真实写入:新建分组 → 保存被 L4032 拦 → 全局 step-up 弹窗填 TOTP → 原请求自动重放 → "已创建" → 列表出现该行
- 由此补了一处可用性缺口:
client.ts之前只把 L4032 抛给弹窗、调用方拿到的是失败,等于每次写都要点两下;现在弹窗结算一个门闩,认证通过即原地重放(并发请求共用一次认证),取消则照常失败 - 单元与集成测试:
go test ./...全绿(含新增的配置权限/范围/step-up/Lua 路径 5 个用例),前端vue-tsc零错误
阶段 9(组态改版)已完成(2026-09-21):把编辑器与渲染内核换掉,目标是"组件效果可调、属性面板自动生成、画布能用"。
背景:旧实现有两个结构性问题。一是图元是一张定型的 SVG,实例只能改文字 + 绑点,没有可调外观;二是编辑态与运行态是两套渲染路径(编辑器 X6 不虚拟、运行态 X6
virtual+ 独立 HTML 数值层),于是组态文档分裂出两个方言,而没人负责统一。
- 弃用 X6,自绘 DOM/SVG。决定性的一个事实:X6 的官方插件生态停在 2.x(
transform/snapline/selection/history/minimap的latest都是 2.x、peer 要@antv/x6 ^2.x;名义上的 3.0.0 是 2022-11-25 与 x6 3.0.0 同批发布的空壳,四年没动),而项目在 3.1.8 上 —— "留 X6 + 装官方插件"这条便宜路不存在,继续留着只能像原来那样自己绕它的坑(代码里记了 7 处)。web/package.json已移除@antv/x6 - 一个渲染器通吃两级:
components/hmi/CanvasStage.vue同时服务编辑器与运行态,SymbolInstance用mode区分交互。数值槽是图元实例的子元素,跟着left/top走,平移缩放由整张画布的一个 CSStransform带走 —— 旧实现那套"图元本地坐标换算成画布坐标再喂给独立数值层"的矩阵运算整体消失 - 文档只剩一种坐标方言:
server/internal/hmi/geometry.go把position/size/zIndex归一成扁平的x/y/width/height/z,模板展开与画面装载都走它。此前模板引擎输出的是 X6 方言、渲染器只读扁平形式,于是cluster_face里{{ per_row * 62 + 24 }}这类参数化尺寸一次都没生效过(图元全落原点、尺寸回落到 bbox)——geometry_test.go把这条钉住了 - 图元参数化:
symbol.json新增props(面板按 type 自动生成表单,落点是sel+attr或text:true)、events(可点触发)、builtin(少数子结构随值变形的组件交给前端渲染器)。服务端校验 props 的每一项:sel必须真实存在、attr在白名单内、同一(sel,attr)不被两条属性抢、default类型匹配;画面文档里的取值也过同一套检查。图元从 14 个增到 19 个(补 仪表盘/液位罐/数值显示/按钮/开关,其中 5 个走builtin,渲染代码注册在web/src/hmi/builtin/) - 画布能力:撤销重做(整文档快照 60 层,
hmi/history.ts)、复制粘贴/原地复制、方向键微调、z 序上下移、对齐(左中右上下+居中)/等大等小/等距、成组解组、图层树、八向缩放手柄、网格吸附、对齐辅助线、图元库真样缩略图、快捷键(Ctrl+Z/Y/S/C/V/D/A/G、Delete、Esc、方向键) - 事件与工程包:
cell.data.events支持write(运行态走控制链,需独立复核)与goto(只跳已发布画面);GET /hmi/bundle把一个工程(图元库 + 模板 + 画面树 + 全部画面文档)装进单个 JSON,POST /hmi/bundle/import只增不改:画面 id 撞车落新 id、模板内容相同不造 rev、图元撞车只报冲突,默认dry_run出对账报告,并要求一次独立 step-up - 验收:
./deploy/smoke8.sh30 项(props 下发与校验 / 事件参数校验 / 工程包导出与只增不改导入 / 结尾断言仓库资产一个没少且没被改写);smoke4.sh30 项、smoke5.sh32/2(2 项是 09-18 遗留画面导致,与本次无关)、smoke7.sh22 项;go test ./...全绿(新增geometry_test.go、props_test.go、bundle_test.go);前端vue-tsc零错误 - 本机起栈/跑冒烟用
bash .run/up.sh restart与bash .run/up.sh smoke smoke8.sh:非交互 shell 不读.bashrc,PHOTOLINK_PROJECT/LCVIEW_POINTS_DB必须显式给(缺前者会让 plbridge 退到不存在的/opt/photolink/config/project.bin,bridge 永远附加不上)
阶段 7 前置:bridge 控制面骨架已落地(2026-09-21)
bridge/ctrl:cgo 封装libphotolink_ctrl的ctrl_client_*(ZMQipc:///tmp/photolink_ctrl→ CtrlEngine)。既不用 rtdb 写、也不用 hmi_sender:PhotoLink 的doc/rtdb_capi_usage.md:409-412写明"YK/YT 不经 RTDB,本库仅提供状态回读";而hmi_sender是把 EXECUTE 直投总线的 DIRECT 旁路(《详设》misty-reef-char.md:387要求封死)。bridge/rtdb 的只读类型边界未动。- 控制面独立成第二个 gRPC 服务
bridge.v1.BridgeCtrl(CtrlWrite/CtrlStatus),不往只读的Bridge上加方法 —— "数据面只读"这条不变量在接口层仍然成立;控制面接不上时明确回Unavailable,不静默、不降级。 - 协议语义(实测确认,反直觉):最终结果是 CtrlEvent 推的,不是
wait的(EXECUTE 应答只有"execute sent");SELECT 与 EXECUTE 共用同一个 request_id 与等待条目,所以顺序必须是select → wait(消费应答) → execute → 等事件。点 ID 是(gid<<16)|pidx,点类型 ctrl 侧是YK=0/YT=1(与 rtdb 的 YC=0/YX=1/YK=2/YT=3 不是一套)。 - 本机实测(x86_64):watchDog +
libphotolink_ctrl.so+run_loop(modbus_loop 自环) 起栈,bridge/cmd/ctrlprobe下发 YK 与 YT:accepted=true,ctrl_listen同时看到REQ topic=0x601000a/RESP topic=0x701000a—— 命令真的到了设备(modbus master 实发 FC)。点 ID、类型映射、SBO 时序、超时归因都由此验证。 - ⚠️ 但结果读不回来(上游 bug):
src/data/ctrl/src/client/ctrl_client.cpp:60-103的接收线程先按CtrlResponse判request_id()!=0,而 CtrlEvent 的 field0 是point_id,被当 u64 读出来非 0 → 事件帧进了 response 分支、continue,ctrl_client_subscribe的回调永不触发。裸 ZMQ 抓帧可见事件本身是好的(as CtrlEvent: result=0 verify=1 msg='OK')。因此当前门户语义只能是accepted=true + TIMEOUT(结果不确定),绝不误报成功。 - 上游待修四处:①
ctrl_client.cpp收包处加Verify()区分 CtrlResponse/CtrlEvent;②src/data/ctrl/config/ctrl.yml:5的ipc_path缺ipc://前缀(bind 失败但plugin_start仍返回 0,表现为"启动成功却没监听");③watchdog_ctrl.yml引用的sub_shell_mod/libctrl_mod.so在仓库与安装包里都不存在;④packaging/watchdog.yml.in里没有 ctrl 子进程项 —— 部署基线根本没起 CtrlEngine。 - 验收:
cd bridge && go test -race ./...全绿。新增ctrl_test.go(SBO 时序 / 结果归因 / 超时不确定 / 同点串行,靠注入假 transport,不需要真引擎)与grpcserver/ctrl_test.go(pb↔ctrl 映射与错误语义);api与server均照常 build。
板子实测:真实控制链路打通(EM-1000 / aarch64 / Ubuntu 20.04,2026-09-21)
- 代码侧的根因修复(
ctrl_client.cpp用flatbuffers::Verifier区分 CtrlResponse / CtrlEvent,响应优先、命中即停)当时已在源码里,但build/aarch64-release的.so还是修复前的产物(md5 与板上一致,都是旧的)。重编 aarch64 并推板后生效(新.somd5e862c684…,旧件备份为libphotolink_ctrl.so.bak-ctrlfix)。 - 板上还有两处配置阻塞,与代码 bug 同等致命:
config/ctrl.yml的ipc_path: "/tmp/photolink_ctrl"缺ipc://前缀 —— CtrlEngine 把它直接喂给 zmqbind(),失败会抛Invalid argument,而plugin_start()仍返回 0,表现为"启动成功但没监听"。- 同文件的
bus.shm_name: "photolink_bus"是总线名(ctrl_plugin.cpp直接传给Bus::init),必须与 scada 同一条;板上实际是photolink(见/tmp/pl_photolink_*、/dev/shm/photolink_*)。写成photolink_bus会另开一条总线,CtrlRouter 永远收不到CONTROL_REQUEST。
config/watchdog.yml新增ctrl子进程条目,CtrlEngine 随photolink-watchdog自启(重启后/tmp/photolink_ctrl在;生产sub_shell里能看到libphotolink_ctrl.so)。两个配置文件分别备份为*.bak-ctrlfix。- 新的 plbridge(含
bridge/ctrl控制面)已部署到/opt/lcview/bin/plbridge(旧件plbridge.bak-ctrlena),并附诊断工具/opt/lcview/bin/ctrlprobe。 - 端到端实测(板子自带 modbus_loop 自环,隔离起栈):
YK 0x1000A=1 → result=OK accepted=true stage=event 耗时 5ms;YT 0x10008=500 → result=OK 耗时 9ms。链路完整走通:bridge/ctrl → ctrl_client → CtrlEngine → bus CONTROL_REQUEST → CtrlRouter → modbus → 设备 → CONTROL_RESPONSE → CtrlEvent。 - 生产工程那 5 条链路是 RS485 主站(
/dev/ttyRS485-1..4主站、-5从站模拟),台上没有物理 PCS 应答,所以生产点0x10000实测仍是accepted=true + TIMEOUT(结果不确定)—— 这是设备侧不回,不是软件问题;口径上正好验证了"命令已下发"与"设备未确认"被分开表达。 - 上游仓库待补(让下次安装自带这些修正):
packaging/watchdog.yml.in加 ctrl 子进程;src/data/ctrl/config/ctrl.yml与ctrl_config.h的默认值补ipc://并将bus_shm_name默认改成photolink。 - 小瑕疵:新客户端会把订阅确认(
CtrlResponse且request_id==0)记成"丢弃无法识别的帧",每个控制客户端启动打一次;无害,但值得在上游收一下。
功率下发(YT 遥调)链路在板上实测跑通(2026-09-21)
- 坐标:板载工程的 modbus 映射(
config/modbus_1.bin,schemaschemas/modbus_config.fbs)把 group 1 的 64 个保持寄存器全列成HOLDING_REGISTER:1.0–1.31 是遥测/状态,1.32–1.63 是控制寄存器;其中 1.35 = PCS有功功率参考值(point_id0x10023,寄存器 35,scale=1.0,INT16)。ctrl_router.bin为 1.32–1.63 各生成一条control_<id>→modbus_master、reply_back: true的 DIRECT 路由(注意:它与ctrl_router.yml那份手写配置完全不同,.bin 才是运行态)。 - 完整链路(实测):
ctrl_client→ CtrlEngine SBO(select→execute)→ 总线CONTROL_REQUEST(topic0x6010023)→ CtrlRoutercontrol_65571→ProtocolSession::execute_control(iprotocol/session/protocol_session.h:101)→ModbusMasterProtocol::encode_control_write(iprotocol/src/protocols/modbus/modbus_master.cpp:328:按 point_id 反查寄存器,reverse_convert_registers(value, data_type, byte_order, scale, offset)反算 raw)→ FC06 写单寄存器(addr 35 / slave 1,串口/dev/ttyRS485-1;COIL 走 0x05、多寄存器走 0x10)→ 从站(板载「PCS1破解(从站模拟)」ttyRS485-5)→CONTROL_RESPONSE(topic0x7010023)→ CtrlEngineon_control_response→ CtrlEvent → 客户端。 - 实测往返:写
YT 0x10023 = 25→result=OK accepted=true stage=event 耗时 45ms;主站轮询读回1.35.YC = 25、从站侧5.35.YC = 25;再写 0 复位 → 两处都回到 0。ctrl_listen同时抓到REQ topic=0x6010023/RESP topic=0x7010023。控制真的落到设备且可读回。 - ⚠️ 门户现在还发不出去的原因(点表与运行态错位):
project.db把这 32 个控制寄存器全放在yc_points(遥测),yt_points只有一个 index 0 的「功率设定」;而ctrl_router.bin只认 1.32–1.63。于是门户点表里1.35类型是 YC,而控制台与 HMI 的ctrl事件只接受 YK/YT(server/internal/hmi/props.go的ctrlTypes)—— 真正的可控点反而选不出来。1.0.YT「功率设定」是孤儿点:project.bin里 group 1 的 yt=yk=0(没有控制槽位)、路由表里没有 1.0、modbus_1.bin里 1.0 是「A相电网电压」。之前测0x10000超时是"没有路由",不是"没有设备"。project.bin/ctrl_router.bin/modbus_1.bin生成于 09-20 13:48,而project.db13:54 —— 改点表必须重新 generate,否则门户与运行态永久不一致。- 值域没暴露:
points.One()取 YT 时不选min_value/max_value/step,/api/v1/points也不返回 → 门户无法校验设定值范围。 - 回执不带测量值:
send_control_response里current_value_bool/f64固定 false/0.0,CtrlEngine select 时读到的 RTDB 值被丢弃((void)state)→ "下发后实际值"只能另读1.35.YC。
门户控制下发已接真链路(2026-09-21)
- 服务端:新增
internal/control(编排)+POST /api/v1/control/write。分工——bridge/ctrl负责 SBO 时序与设备回执,本包只做:只接受 YK/YT(底层约定,YC/YX 一律 L5012)→ 分组范围(ABAC,L4031)→ 遥调值域(L5013)→ 同点"在飞"去重(L5011)→ 同点 500ms 冷却/限流(L5015,429) → 转 gRPC → 成功失败都写审计。桥不可达回 L5014,不重试(重试会在现场造成第二次动作)。- 冷却按
gid.pidx.TYPE分别计(1.35.YK与1.35.YT互不影响),默认500ms,可用LCVIEW_CONTROL_COOLDOWN_MS调整,0= 关闭。它挡的是"连点两下"这类往返之后的重复动作 —— 在飞去重只挡得住并发的那一次。实测:第 1 次 OK 42ms → 立即第 2 次429 L5015「距上次 63 ms,限流 500 ms」→ 600ms 后 OK 43ms;换点位不下发不受影响。
- 冷却按
- 权限点:
control.execute早在角色表里,这是它第一次被真正强制(s.need("control.execute", …))。 - 点位 API 补值域:
points.One现在为 YT 点取min_value/max_value/step并随/api/v1/points返回 —— 没有它,门户无法校验设定值范围。 - 前端去 mock:
api/control.ts新增;stores/control.ts的execute()改为真调用并按设备回执建记录(不再Math.random());控制的种子记录与操作票清空,页面里不再混假数据;HmiRuntimeView.dispatchCtrl按结果提示——OK才是成功,TIMEOUT明确说"结果不确定、请现场确认、不会自动重试"。step-up 闸门按本部署的简化策略改为不阻断并写明原因。 - 板载实测(经门户 HTTP,非内部工具):
POST /api/v1/control/write {"key":"1.35.YT","value":25}→200 {result:OK, accepted:true, stage:event, elapsed_ms:43};读回1.35.YC=25、从站侧5.35.YC=25;复位 0 同样 OK。护栏实测:越界120 → L5013「允许范围 0 ~ 100」、遥测点1.9.YC → L5012、不存在9.9.YT → L4041。审计里能查到 5 条control.write(含 DENY 的 L5013/L5012/L4041 与 OK 的记录、耗时、参数)。 - 点表已按约定正名(板载
project.db):yt_points(1,35) 功率设定(原来错放在 index 0)、yk_points(1,32) 控制命令字;同索引的 YC 保留作读回。 - ⚠️ 遗留两件(都还没做,且互相独立):
- 控制路由没有生产生成路径。正式 generate 只有
project/modbus/iec61850/lua_engine四份规则(src/tools/config/schemas/*.sql),不产ctrl_router.bin;板子上那份是 demo 工具gen_goose_ctrl_router --dst-proto modbus_master的产物(描述里还留着 "DIRECT GOOSE control")。现在能下发,靠的是这份演示产物恰好覆盖了 1.32–1.63。 - RTDB 的 YT/YK 槽位还没补(
project.bin里 group1 仍是 yt=yk=0)。下发本身不需要槽位(已实测),但"下发后实际值"的回读与read_yt_state需要。重新生成project.bin会让 scada 启动即崩:差异是新版多了一个group_id=0、零点位的空组「测试组1」(它 13:54 才进project.db,比旧 bin 晚 6 分钟)。 修法已验证(离线,不动运行态):把空组从project.db删掉后,用官方 CLI 在/tmp副本上重生成 ——
产物结构:cp -a /opt/photolink/config/project.db /tmp/t.db && sqlite3 /tmp/t.db 'delete from groups where group_id=0' cd /opt/photolink/share/photolink/config_server PATH=/opt/photolink/bin:$PATH LD_LIBRARY_PATH=/opt/photolink/lib \ /opt/photolink/bin/photolink_config generate rules/project_rules.json \ -d /tmp/t.db -o /tmp/t.bin --schemas-dir schemasgroups=5,group1yc=64 yx=1 yk=1 yt=1—— 与已知可用的旧 bin 同形,只少了那个空组。剩下的是把它部署上去重启验证(会短暂中断采集,且重启后必须重起sim_pcs.py)。较彻底的替代方案是给运行态加载器补上对空组的容忍。
- 控制路由没有生产生成路径。正式 generate 只有
- ⚠️ 台架依赖:板载从站数据由手工启动的
/opt/photolink/bin/sim_pcs.py提供(往 RTDB group5 写 220.5V/50kW…),它默认只跑 1000 轮(≈17 分钟)就退出;一旦停了,group1 主站读回全 0、看起来像"链路全断"。建议给它做个 systemd 单元(或至少--loop拉长)。