2026-07-01 LoRa 项目整理开发记录
今天主要是在收口一个 LoRa 相关项目的现场稳定性和配置体验。不是只修一个点,而是把几个长期摩擦放到同一条链路里整理:现场 LoRa 轮询被异常设备拖慢、转发模式开关和运行态保活互相打架、LR 配置页信息太散、子设备卡片缺少运行来源,以及 Codex 自身的 worktree、skill、commit scope 规则需要整理。
从本地 Codex 原始会话看,今天 2026-07-01 一共有 9 个 Codex 会话文件,其中 8 个是开发/排查/整理会话,最后 1 个是这篇日志整理会话。可核验的提交记录里,后端项目当天有 19 条 commit(含 merge),前端项目当天有 9 条 commit。下面记录只写机制和脱敏后的链路,不放真实设备编号、内网地址、SSH 用户、镜像仓库和本地绝对路径。
先整理开发现场
第一件事是工作区整理。定时 Codex 会话检查了 10 个项目的 worktree,只移除满足三个条件的登记工作区:
1 | |
最后移除了 3 个已合入基线的 worktree。没有用目录名猜测,也没有全盘扫描;判断依据是 git worktree list --porcelain、git status --porcelain 和 merge-base --is-ancestor。这个清理本身不大,但它把后面一天的开发环境先降噪了:真正还在进行中的 worktree 被保留,已合入且干净的工作区被清掉。
同一天还整理了两个 Codex 使用规则。
一个是 SSH 排查 skill 的目标解析。之前用户明确说要连某个现场别名,agent 还是优先去了默认测试机。修正后的规则是:只要用户给了 SSH alias、现场设备编号或显式 host,这就是最高优先级目标,默认小电脑只能在没有显式目标时使用。对应文档也补了 ssh -G "$HOST" 这种预检模板,先确认解析结果,再进入排查。
另一个是 commit scope 规则。旧规则强调“提交前看最近 10 条 commit,如果有同义 scope 就沿用”,这容易让 agent 把入口现象当 scope。比如问题入口是 LoRa 扫描失败,但根因可能是系统配置兼容。规则被压缩成两行:
1 | |
这个改动很小,但能减少后续提交历史里的“症状型 scope”。
LoRa 现场稳定性
上午的核心问题是:某类大报文设备不应该进入 LoRa2,却曾经在 LoRa2 上持续收发,拖慢同信道上的其他设备。排查后确认这不是日志误判,而是真实收发。原始会话里统计到异常期间有大量 LoRa2 报文,且包含较大的设备报文。为了脱敏,这里不贴 PID 和现场编号,只保留机制:
1 | |
修正方向不是给某个 PID 打补丁,而是把“设备类型与 LoRa 归属”做成更硬的通用约束。某些设备类型默认只能在 LoRa1 归属下被接纳;即使历史运行态里误登记成 LoRa2,后续接入和轮询也要再次收口,避免一个错误 holder 存活很久。
简化后的核心代码逻辑是这样:
1 | |
这段示例不是原始代码,只表达机制:通道归属不是按单个 PID 黑名单判断,而是先用设备类型的归属策略兜住,再进入业务槽位判断。
轻量运行态快照
另一个现场问题更隐蔽。某次 LoRa2 轮询里,DGC 类设备约 10 秒没有更新,随后被离线检测移除。继续追日志后发现,无线收发本身没有耗时 8 秒,真正的空窗出现在发包前的应用链路。
可核验的数字是:
1 | |
旧链路大概是:
1 | |
业务上真正需要的只是当前重量、锁重状态、休眠状态、电量、更新时间等轻量字段,不需要把计数 map 复制到实时轮询线程里。于是把“完整 Context”拆出一个轻量只读投影:
1 | |
这里的重点不是少复制几 KB,而是把实时轮询链路从“完整业务上下文”改成“最小运行态投影”。这类接口应该服务轮询稳定性,不应该顺手暴露页面统计、锁重分布、holder 等重量级对象。
转发模式和运行态生命周期
下午对 LoRa 转发服务做了一轮结构整理。旧代码里,转发模式、LoRa 开关、串口运行态、host 配置释放转发层几个概念混在多个地方。实际问题表现为:关闭转发模式时,Java runtime 的保活线程可能又把 systemd 转发层拉起来,导致用户看到“关闭超时”或“关闭后又启动”。
旧链路:
1 | |
整理后的顺序是:
1 | |
脱敏后的代码形态大概是:
1 | |
随后又继续重构了运行时边界:把 LoRa1/LoRa2 原来类似复制的 manager 收口到 slot runtime、runtime registry、lifecycle service 这一组对象里。收益是后续关闭、重启、转发模式切换、host LoRa config 前释放转发层,都能走同一个运行态入口,而不是散落在 controller、service、manager 之间。
这轮重构不是为了抽象而抽象。它解决的是一个很具体的职责错位:systemd 转发层是底层资源,Java runtime 是使用者,业务配置只是期望状态。关闭底层资源前,必须先让使用者停下来。
1 秒保活和防重入
后面还补了一版 LoRa 运行时健康保活。现场有一种情况:关闭 LR2、写配置、再开启 LR2 后,LR1 可能进入无响应状态,直到人工重开 LR1 才恢复。讨论后没有让外部 activity sampler 暴露更多细节,而是让 LoRa runtime 内部维护自己的端口健康状态。
核心机制是:
1 | |
测试时分了 RAW 和 FORWARD 两种模式。RAW 模式下,关闭 LR2、读写配置、再开启 LR2,LR1 没进入 NO_RESP,LR2 触发一次重建后恢复。FORWARD 模式下,重启后转发层首轮打开大约需要 31 到 33 秒,但 recovering=true 挡住了 1 秒保活的重复触发;后续没有出现“触发、完成、再触发”的循环。
这段机制的关键点是防重入。1 秒保活本身很容易从“兜底”变成“重启风暴”,所以状态机里必须有 recovering 或等价状态,明确表达“当前已经有人在修,不要再派第二个任务”。
前端配置页整理
前端主要围绕 LR 设置页做了几轮压缩和信息分层。
当天的前端提交集中在这些点:
1 | |
其中一个比较典型的问题是“当前信道”和“用户正在编辑的信道”混在同一个字段里。旧链路是:
1 | |
修正后把草稿值和已生效值拆开:
1 | |
对应的脱敏前端状态逻辑:
1 | |
这个改动的边界很清楚:下拉框服务编辑态,标题 tag 服务已生效态。不要用一个字段同时表达“我想写什么”和“现场已经是什么”。
子设备卡片显示 LR 来源
晚上又补了一个 UI/数据链路问题:子设备管理卡片需要显示当前设备最近由 LR1 还是 LR2 收到。前端共享卡片先做了展示逻辑:
1 | |
随后现场发现某个在线设备仍显示 LR-。继续追后发现不是前端颜色判断错,而是部分设备列表接口的 option 没带 loraWho。
旧链路:
1 | |
修复后把运行来源下沉到通用 DTO:
1 | |
这里同样避免了“只修某个页面”。主路径、手写 option copy、加料终端已配置吊钩秤列表都补了字段,避免后续其他设备卡片还是缺值。
验证方式
今天验证比较杂,但都围绕“改完后现场链路是否真的闭环”。
后端主要跑了针对性 Maven 测试,例如 LoRa 通道归属、运行时生命周期、串口转发服务、轻量快照、子设备 option 透传等测试。前端主要跑了 LoRa 配置相关的 Node 单测、运行态显示测试,以及 npm run build。现场侧的验证包括:
1 | |
还有多次部署后检查:容器 runtime commit、Java 启动时间、转发层 active 状态、接口返回字段、页面卡片依赖字段。发布链路里也明确避开了一个已经不可用的旧 build flow,改成等待提交触发的 CI 产物,再用部署脚本更新目标服务。
今天真正沉淀下来的东西
今天看起来改了很多文件,但核心沉淀其实是几条机制:
- LoRa 归属判断必须是设备类型和业务槽位共同决定,不能只按“从哪个串口收到 SYN”决定。
- 实时轮询线程只能依赖轻量运行态投影,不能复制和打印完整业务 Context。
- 转发模式切换要先停 Java runtime,再动 systemd 转发层,避免保活线程和关闭流程互相打架。
- 1 秒保活必须带 recovering 防重入,否则保活会变成重复重启。
- 前端配置页要区分草稿态和已生效态,不能让旧缓存覆盖用户正在编辑的值。
- 共享卡片需要共享 DTO 字段兜底,不能只在某个页面上拼展示逻辑。
- Codex 工作流也需要整理:显式目标优先、commit scope 按根因命名、worktree 清理只移除已被基线包含的干净工作区。
如果只看单点修复,今天像是在修 LoRa 和页面;如果看项目整理,今天实际是在把“现场运行态、配置状态、展示状态、agent 工作流”这几条容易互相污染的链路拆清楚。拆清楚之后,后面的 bug 就更容易定位到具体层级,而不是继续在 controller、manager、页面和部署脚本之间来回猜。