Files
trail-mate/docs/team/persist.md
vicliuandGitHub 54cf0301e2 Gat562 mesh evb pro (#13)
* feat(gat562): wire nrf52 board runtime and bluetooth stack

* refactor(chat): align shared BLE cores and nrf meshtastic routing

- extract Meshtastic and MeshCore phone/business flows into shared core_chat BLE cores with owner hook seams for ESP and nRF backends

- replace nrf-specific lite adapters with board/platform adapters that consume shared cores and shared storage contracts

- extend nrf Meshtastic radio path with packet history dedupe, flooding rebroadcast, route discovery/traceroute responses, routing ACK/error handling, observed relay learning, retransmit queueing, and next-hop persistence

- persist nrf device identity and node metadata more explicitly, including next-hop state and node-id initialization support

- move nrf chat/contact/blob storage further toward InternalFS-backed persistence and update contact/node store behavior accordingly

- wire gat562 protocol/app facade pieces to the concrete nrf stores/adapters and add supporting app time-sync hooks used by platform runtimes

- add owner-hook split files and planning/status docs to document the ongoing alignment with the reference meshtastic-firmware multi-platform structure

* Add NRF Fusion Pixel font and fix Meshtastic RX

Integrate Fusion Pixel 8px as the NRF mono UI font stack and switch the mono runtime to a dedicated MonoFont/TextRenderer path that can render ASCII, CJK, and symbols from one asset set.

Add the generated NRF font assets and generator script, including the compact font format with per-glyph advance support, and fix the BDF bitmap parsing bug that previously produced blank ASCII glyphs on device.

Move the LVGL Noto CJK font asset out of shared modules into the ESP-specific platform tree and update the related build/config references so the ESP-only font boundary is explicit and NRF does not compile the LVGL asset by accident.

Update the mono UI runtime to use the new renderer, normalize major menu and title labels to uppercase, and wire the GAT562 UI runtime to the Fusion Pixel font.

Fix the core mesh adapter router so Meshtastic and MeshCore backends can coexist without overwriting each other, and ensure active protocol selection is applied before mesh config changes are pushed into the runtime.

Improve GAT562 runtime diagnostics by mirroring debug output to both Serial and Serial2, logging startup radio configuration, and logging raw RX packets with RSSI/SNR so radio-path issues are visible during bring-up.

Relax SX1262 receive polling so it no longer hard-gates RX handling on DIO1 state alone, avoiding a failure mode where the board appeared alive but never delivered packets into the receive path.

Align the NRF Meshtastic adapter with the working ESP behavior for channel handling: use the default expanded public PSK for an unconfigured primary channel, compute channel hashes from the active preset name or Custom label instead of a hard-coded Primary string, and apply the same logic to self-announcement/NodeInfo packets.

Add protocol-layer Meshtastic RX diagnostics for parse, decrypt, decode, text queueing, app-data queueing, unknown-channel, and dedup events to make packet flow visible while validating interoperability.

Fix early duplicate handling for relayed packets so the first valid packet is still delivered into text processing while later relays are correctly suppressed, which restores Broadcast conversation creation and message visibility in the CHATS page.

* feat(gat562): align nrf ui and meshtastic behavior

* Refine mono UI GPS pages and board diagnostics

* refactor: align board runtimes, GAT562 app shell, and BLE integrations

- migrate ESP board-specific implementations and runtime adapters into boards/*
- continue GAT562/Tab5/UI runtime alignment across app and board layers
- sync BLE and runtime integrations, and apply repository clang-format rules

* Refine gat562 mono UI and persist UI settings

* Fix settings persistence and GNSS UI behavior

* Improve mono UI paging and Meshtastic RX fallback

* Align nRF52 Meshtastic BLE and PKI behavior

* Refine mono UI node and chat interactions

* Fix deferred config persistence on gat562 BLE

* Refine mono node compass and GPS layouts

* Include node position in Meshtastic phone info

* Refactor shared board contracts out of ESP platform layer

* Persist node positions across restarts

* Ignore local compile commands database

* Sync README acknowledgements updates

* Apply CI formatting

* Fix board ownership drift and restore pager walkie support

* Add APRS asset, expand System controls, and harden GAT562 self-position persistence

Bundle the current workspace changes into a single checkpoint covering the in-progress shared UI, settings, and GAT562 runtime work.

What this includes:

- add the APRS asset source and image resource currently used by the shared UI asset set

- extend the shared System/settings flow so vibration and screen brightness can be surfaced through the device runtime and settings UI plumbing

- update the mono 128x64 runtime and the GAT562 platform bindings so the new settings controls are available in the GAT562 experience

- wire the ESP runtime/startup helpers needed to support the same device-runtime settings path on the ESP side

- persist the GAT562 self GPS position into the node store during core service updates so a device that already obtained a fix can retain its own last known position across reboot

- add an explicit node-store flush interface and implement it in both the nRF52 and ESP Meshtastic node-store backends so critical self-position updates can be forced to persistent storage immediately instead of waiting for the normal debounce window

- keep the existing debounced persistence behavior for ordinary node-store churn while allowing targeted immediate flushes for data that would otherwise be easy to lose during restart or power loss

* chore: checkpoint current chat and contacts changes

* perf: reduce chat list and message handling latency

* Align Meshtastic phone sync and chat UI refresh

* Remove key verification UI and hide MC position exchange

* Add T-Deck Pro board bring-up and sync pending UI fixes

* Refactor GAT562 runtime and stabilize Meshtastic BLE

Refactor the GAT562 board runtime by extracting dedicated GPS and input runtime components and routing config application through a focused runtime apply service.

Narrow the nRF52 BLE manager dependencies, rework the Meshtastic BLE transport to better match the Android client handshake, and harden the FromRadio/FromNum flow with improved staging, read pacing, diagnostics, and stack-safe config snapshot generation.

Also repair repository text encoding issues in docs and UI comments, and add Meshtastic BLE timing documentation to capture the expected Android/firmware interaction model for future debugging.

* fix: gat562 settings

* Define LoRa tx power caps per board

* Refine mono conversation bubble focus

* Polish mono conversation and message info layout

* Harden Meshtastic BLE flow and cross-target builds

Adjust the Meshtastic BLE transport timing on nRF52, retain pending FROMNUM notifications, and defer local admin/self replies to reduce request/response races during Android settings flows.

Also make the shared chat/UI code compile across non-nRF targets by gating RTOS and Arduino-only helpers and by updating the team page layout to the current page profile fields.

Note: this does not resolve the gat562 Meshtastic Android settings progress bar issue yet; the BLE/admin response path is improved and instrumented, but the root cause of the stuck loading state remains unresolved.

* Fix GAT562 mono channel slot UI and screen timeout

* Revert Meshtastic BLE hardening changes

Revert b9e20b2662 because it causes app-side configuration reads to hang on GAT562.

This restores the previous Meshtastic BLE timing/notification behavior and also drops the cross-target compatibility adjustments that came with that commit.

* Refactor nrf52 fs helpers and show flash free space

* fix sync node info

* Harden Meshtastic BLE receive path

* Fix Meshtastic nodeinfo persistence

* Persist NodeInfo position updates

* Format files to match CI style

* Prepare 0.1.14-alpha docs and CI

* Fix watch chat build and CI formatting

* Format sources for CI clang-format-14
2026-04-13 17:33:21 +08:00

18 KiB
Raw Permalink Blame History

# Pager Team CoreSD-only 持久化方案(完整版)

0. 目标与三条原则

目标

  • 简单:文件少、格式少、逻辑少
  • 可信:断电可恢复、日志不会写坏全局
  • 资源消耗低:避免高频随机写;控制日志增长

核心原则

  • SD-only(不磨损 flash
  • append-only(日志只追加,不回写不 seek
  • 无每条 CRC:靠 magic + len 判定完整记录;尾巴不完整直接丢弃
  • “意图 vs 事实”分离UI 触发 Intent;落盘写入 Committed Fact

1. 目录结构(定稿)

/team/
  current.txt                 # 当前队伍目录名,一行文本
  current.tmp                 # 原子写临时文件
  T_A7K3/                      # team_id 的短码/hash(稳定目录名)
    keys.bin                  # 当前 team_psk(可选,单独持久化)
    snapshot.bin              # 当前世界快照(低频原子写)
    snapshot.tmp              # 原子写临时文件
    events.log                # 关键事件日志(追加写,sync 依据)
    posring.log               # 位置 ring(固定大小,覆盖最旧)
    chatlog.log               # 聊天日志(滚动上限)

current.txt 格式

  • 内容示例:T_A7K3\n
  • 原子写:写 current.tmp → flush → rename 覆盖 current.txt

2. 数据模型分层(必须坚持的边界)

2.1 关键事件(Key Events

改变团队结构 / epoch / waypoint 的事实,必须:

  • 写入 events.log
  • event_seq(单调递增)
  • 作为 SYNC 的依据

必须写入的关键事件类型:

  • TeamCreated
  • MemberAccepted
  • MemberKicked
  • LeaderTransferred
  • EpochRotated (Waypoint 可选,后续加同样走 key event)

2.2 高频态势(Non-key

  • Presence:心跳/在线状态(不落盘)
  • Position:落 posring.logring + 节流)
  • Chat:落 chatlog.log(滚动上限)

关键事件 = “世界规则变化”;位置/聊天 = “现场噪声与记录”。


3. event_seq:权威来源与落盘规则(答疑合并)

3.1 event_seq 谁维护?

Leader 维护并分配 event_seq(推荐在 TeamCore/Usecase 层维护;TeamService 保持 IO 职责)

  • Leader 产生 key event 时:event_seq = last_event_seq + 1
  • Member 只接受带 seq 的 key event,并校验连续性

3.2 UI 触发还是接收后落盘?

两者都有,但落盘只能写“已提交事实(Committed Fact)”

  • UI 触发的是 Intent(踢人/转让/轮换等请求)
  • 只有当 Leader / Member 确认提交后,才写入 events.log

具体:

  • LeaderUI Intent → 校验权限/状态 → Commit(分配 seq)→ append events.log → apply 内存状态 → 发送消息(失败靠后续 SYNC 修复)
  • Member:收到 leader 的 key event → 校验 team_id/epoch/权限/seq 连续性 → append events.log → apply

避免出现“UI 点了但网络没发出去/没人承认,日志却写死了”的分裂。


4. 文件格式(team_id 统一 8 字节,无 CRC

通用约定:Little-endian、紧凑写、不依赖 struct padding。


4.1 TeamId

  • team_iduint64_t8 bytes
  • 目录名建议:T_ + base32(team_id)(或短 hash),保证 FAT 兼容

4.2 snapshot.bin(当前世界快照)

Header(固定)

SnapshotHeaderV1 {
  char   magic[4] = "TMS1";
  uint8  version  = 1;
  uint8  flags;                 // bit0: in_team
  uint16 reserved;

  uint32 updated_ts;            // 写快照时间(秒)
  uint64 team_id;               // 8B
  uint32 epoch;                 // 当前 epoch
  uint32 last_event_seq;        // 已应用到的最后 seq

  uint32 self_node_id;
  uint32 leader_node_id;

  uint8  self_role;             // 0 None, 1 Member, 2 Leader
  uint8  reserved2[3];

  uint16 member_count;
  uint16 reserved3;
}

成员表(重复 member_count 次,变长)

MemberRecV1 {
  uint32 node_id;
  uint8  role;                  // 1 Member, 2 Leader
  uint8  flags;                 // bit0: has_name
  uint16 name_len;              // 建议 <= 24
  char   name[name_len];        // UTF-8
}

原子写

snapshot.tmp → flush → rename 覆盖 snapshot.bin

写入节流(低消耗)

  • 10 条 key event 或 60 秒写一次(取先到)
  • epoch rotate / kick / transfer / join 成功 / 离队:强制写一次

4.3 events.log(关键事件日志:真相源 + SYNC 依据)

Record header(固定)

EventRecHeaderV1 {
  char   magic[2] = "EV";
  uint8  version  = 1;
  uint8  type;                  // KeyEventType

  uint32 event_seq;             // 单调递增
  uint32 ts;                    // 秒

  uint16 payload_len;
  uint16 reserved;
}
payload[payload_len]

无 CRC 的可信规则

扫描时若:

  • magic/version 不匹配 → 停止(最简单策略)
  • 文件剩余长度 < header + payload_len → 停止(尾部半条丢弃)

因为只 append,停止点之前的数据可信。

KeyEventType

  • 1 TeamCreated
  • 2 MemberAccepted
  • 3 MemberKicked
  • 4 LeaderTransferred
  • 5 EpochRotated

Payload 定义

TeamCreated

uint64 team_id
uint32 leader_node_id
uint32 epoch          // 初始 1

MemberAccepted

uint32 member_node_id
uint8  role           // 默认 1 Member
uint8  reserved[3]

MemberKicked

uint32 member_node_id

LeaderTransferred

uint32 new_leader_node_id

EpochRotated

uint32 new_epoch

payload 尽量固定字段、少字符串:更省空间,更低 IO。


4.3.1 keys.binteam_psk 独立持久化,避免重启后 keys 未就绪)

snapshot 不保存密钥,密钥单独落盘。
仅保存当前生效的 team_psk + key_idepoch),不做历史。

格式(固定)

TeamKeysFileV1 {
  char   magic[4] = "TMK1";
  uint8  version  = 1;
  uint8  psk_len;              // 16
  uint16 reserved;

  uint64 team_id;
  uint32 key_id;               // epoch/key_id
  uint8  psk[16];              // team_psk
}

原子写

keys.tmp → flush → rename 覆盖 keys.bin


4.4 posring.log(位置 ring,高频但节流)

Header(固定)

PosRingHeaderV1 {
  char   magic[4] = "PSR1";
  uint8  version  = 1;
  uint8  reserved1[3];

  uint32 data_capacity;         // data 区大小(固定)
  uint32 write_offset;          // 下一条写入位置
  uint32 rec_size;              // 固定 = sizeof(PosRecV1)
  uint32 reserved2;
}
data[data_capacity]

Record(固定)

PosRecV1 {
  uint16 magic = 0x5053;        // 'PS'
  uint8  ver   = 1;
  uint8  flags;

  uint32 ts;
  uint32 member_id;
  int32  lat_e7;
  int32  lon_e7;

  int16  alt_m;
  uint16 speed_dmps;
}

写入节流(必须)

  • 每成员 15–30 秒最多写一条,或位移 > 20m 才写
  • ring 满了覆盖最旧,文件大小恒定

4.5 chatlog.log(聊天追加写 + 上限滚动)

Record(变长)

ChatRecHeaderV1 {
  char   magic[2] = "CH";
  uint8  version  = 1;
  uint8  flags;                 // bit0 incoming/outgoing

  uint32 ts;
  uint32 peer_id;

  uint16 text_len;
  uint16 reserved;
}
text[text_len]                  // UTF-8

Team chat 记录格式(V2,对应 TEAM_CHAT_APP

当收到 TeamChat 负载并完成解码后,按 V2 结构追加写入 chatlog.log。 相比 V1,V2 在记录头中额外保存消息类型,便于统一回放文本、位置和指令类消息。

ChatRecHeaderV2 {
  char   magic[2] = "CH";
  uint8  version  = 2;
  uint8  flags;                 // bit0 incoming/outgoing

  uint32 ts;
  uint32 peer_id;               // sender node_id

  uint8  msg_type;              // 1=Text 2=Location 3=Command
  uint8  reserved1[3];

  uint16 payload_len;
  uint16 reserved2;
}
payload[payload_len]            // decoded TeamChat payload

上限策略(简单)

  • 上限:256KB1000 条(二选一)

  • 超限:

    • rename chatlog.logchatlog.old(可选)
    • 新建空 chatlog.log

5. 启动恢复流程(有 current.txt,最快)

  1. /team/current.txt
  • 不存在/空 → 当前无队伍
  1. 打开 /team/<dir>/snapshot.bin
  • 若不存在 → 从空状态开始(但通常 join 后会写一次)
  1. 增量回放 events.log
  • snapshot.last_event_seq + 1 开始顺序扫描应用
  • 遇到尾部不完整 → 停止
  1. UI Ready

6. SYNC 流程(依赖 event_seq

  • Presence(不落盘)携带:team_id, epoch, last_event_seq

  • 发现对方 last_event_seq > my_last_event_seq

    • 发送 SYNC_REQ(from_seq = my_last_event_seq + 1)
  • 对方从 events.log 读 seq 范围(最多 N 条)回 SYNC_RSP

  • 本地 apply + append(对已存在 seq 可跳过),更新 snapshot


7. event_seq + commit 流程(ASCII 状态/时序图)

7.1 LeaderUI Intent → Commit → Log → Broadcast

UI                         TeamCore/Usecase                 TeamStore(SD)              TeamService(IO)           Mesh
 |   intentKick(target)          |                              |                           |                    |
 |------------------------------>|  check(role==Leader)         |                           |                    |
 |                               |  build KeyEvent(Kicked)      |                           |                    |
 |                               |  seq = last_seq + 1          |                           |                    |
 |                               |----------------------------->| append events.log(EV,seq)|                    |
 |                               |<-----------------------------| ok                        |                    |
 |                               |  apply to in-mem state        |                           |                    |
 |                               |  maybe snapshot throttle      |                           |                    |
 |                               |------------------------------>| save snapshot.tmp->bin    |                    |
 |                               |<-----------------------------| ok                        |                    |
 |                               |----------------------------------------------------------->| sendKick(seq,...)  |
 |                               |                                                            |------------------->|
 |                               |                                                            |   (may fail)       |
 |                               |  NOTE: even if send fails, state is committed;             |                    |
 |                               |        later Status/SYNC will repair delivery              |                    |

7.2 MemberRX KeyEvent → Verify seq → Log → Apply(缺 seq 触发 SYNC

Mesh                 TeamService(IO)             TeamCore/Usecase             TeamStore(SD)
 |  RX Kick(seq,...)       |                           |                         |
 |------------------------>| decode/decrypt            |                         |
 |                         | sink_.onTeamKick(...) ---->| verify team_id/epoch   |
 |                         |                           | verify seq == last+1 ? |
 |                         |                           |    NO -> request SYNC  |
 |                         |                           |    YES -> commit apply |
 |                         |                           |------------------------> append events.log
 |                         |                           |<------------------------ ok
 |                         |                           | apply in-mem state
 |                         |                           | maybe snapshot throttle
 |                         |                           |------------------------> save snapshot atomic
 |                         |                           |<------------------------ ok

7.3 seq 不连续时(Member 触发 SYNC

Member Core                          Mesh                         Leader Core
    |  see seq gap (expected=41 got=45)                             |
    |---------------- SYNC_REQ(from=41) --------------------------->|
    |                                                               | read events.log seq>=41
    |<---------------- SYNC_RSP(events 41..45) ---------------------|
    | apply + append + snapshot                                     |

8. 工程接入点(与 TeamService / ITeamEventSink 对齐)

你现在 TeamService::processIncoming() 会把消息解包后丢给 sink_.onTeamXxx(event)

推荐最小改动:

  • sink_ 的实现(TeamCore/Usecase)负责:

    • 权限判断
    • seq 分配(leader
    • seq 连续性检查(member
    • 调用 TeamStore.append_key_event(...)events.log
    • 更新内存状态
    • 触发 snapshot(节流)
  • TeamService 仅做 IOdecode/encode/send,不直接写 SD


9. PosRing / ChatLog 接入点(答疑合并入规范)

9.1 posring.log:从接收写还是从发送写?

组队模式下 GPS 从 posring.log 渲染队员位置(包含自己)。

定稿:两边都写,并统一走同一个节流器

  • 收到 TeamPosition(队友):写入 posring.log(队友态势来源)
  • 本机 GPS fix(自己):也写入 posring.log(重启后仍能立即看到自己最后位置/轨迹片段)

原因:

  • 只写接收 → 自己历史为空
  • 只写发送 → 队友态势重启后为空
  • posring 是“事实流缓存”,不参与一致性:不用 event_seq,不用严格去重 只需做到:每成员节流 + ts 最新覆盖 UI

推荐接入(最干净):只在 **TeamCoresink 实现)**里写 posring

  • sink_.onTeamPosition(event)decode → posring_append_throttled(from_id, pos, ts)
  • 本机 GPS 更新处:TeamCore::onLocalPositionFix(fix)posring_append_throttled(self_id, pos, ts) → 再决定是否 TeamService.sendPosition(...)

9.2 chatlog.log:记录 Mesh chatTeam channel)还是仅 Team 协议?

决策(v0.1):队伍聊天使用 TEAM_CHAT_APPTeamChat 消息按 V2 写入 chatlog.log。 地图交互:收到消息只弹系统通知;弹窗由 Chat 会话中选中地图标注后触发,显示该位置瓦片地图的裁剪图。 不要把 TEAM_MGMT_APP 记录到 chatlog.log。

先分清三种消息域:

  1. Meshtastic 普通聊天(你现有 chat 模块)
  2. Team 管理消息(TEAM_MGMT_APPJoin/Kick/Rotate/Status…(不是聊天)
  3. 队内聊天(可复用 Meshtastic text,也可未来自定义 Team chat port

定稿:chatlog.log 只记录用户可见的“聊天文本”,不记录管理消息

  • 不记录 TEAM_MGMT_APP 到 chatlog(它们属于 events.log / diagnostics

  • 记录“队内聊天文本”

    • 推荐走 路线 A:记录 Meshtastic text 中属于当前 Team channel 的消息

路线 A(推荐):记录 Meshtastic 文本聊天(Team channel

  • 优点:立刻兼容现有生态(安卓/ATAK/其他节点)
  • 缺点:聊天与 Team 语义解耦(可接受)

路线 B(后续版本):Team 专用 chat 协议(TEAM_CHAT_APP / TEAM_MGMT.Chat

  • 优点:聊天与 team_id/epoch 绑定、成员可控
  • 缺点:需要新增协议与兼容处理

chatlog 的接入点(工程)

  • 因为 TeamService 不处理 text chat,所以 chatlog 应在 chat 模块接入:

    • ChatService/ChatUsecase 收到/发送文本时:

      • channel_id == current_team_channelchatlog_append(...)
  • TeamCore 只需要提供“当前 team channel_id”


10. 缺口补齐总结(当前约束)

  • team_id 8 字节(uint64
  • 关键事件必须落盘: events.log + type + payload 完整定义
  • event_seq leader 权威维护;snapshot 记录 last_seqmember 检查连续性;缺口走 SYNC
  • Intent vs Fact UI 触发 Intent;落盘只写 Committed Fact
  • posring/chatlog 接入点与记录范围明确(pos 双写、chat 记 team channel 文本)

Team Position(对齐 Meshtastic POSITION_APP

1) 设计目标

  • 对齐 Meshtasticpayload 直接使用 meshtastic_Position protobuf
  • 生态兼容:与 Meshtastic POSITION_APP 解码一致
  • 扩展性:保留 Meshtastic 字段的自然扩展空间

2) Wire Payload

  • 类型meshtastic_Positionprotobuf
  • 端口meshtastic_PortNum_POSITION_APP
  • 编码nanopb / pb_encode

说明:不再使用自定义布局。Position payload 与 Meshtastic 完全一致。


3) 取值规则(最小集)

  • latitude_i / longitude_iE7int32
  • location_source:默认 LOC_INTERNAL
  • sats_in_view:有则填
  • timestamp:有有效 epoch 秒则填

其余字段按需填充,保持与 Meshtastic 语义一致。


5) 编码/解码接入点(对齐你现有 TeamService + sink

5.1 发送端(本机 GPS 更新)

推荐路径TeamCore 层做事):

  1. TeamCore::onLocalPositionFix(fix)
  2. 生成 meshtastic_Position payloadprotobuf
  3. posring_append_throttled(self_id, decoded_pos)(同一个点落盘)
  4. 再调用 TeamService.sendPosition(payload, team_channel)

你问过“posring 从发送写还是接收写”:这里就是“发送也写”。

5.2 接收端(TEAM_POSITION_APP

你现在流程已经有:

  • TeamService::processIncoming() 解密 → sink_.onTeamPosition(event{ctx, plain})

sink 的实现(TeamCore)里:

  1. decode meshtastic_Position → 得到标准化字段
  2. posring_append_throttled(from_id, pos)(队友点落盘)
  3. 更新内存 last_seenpresence/online 状态)

6) posring.log 落盘字段对齐建议

你现有 PosRecV1 足够用(ts、member_id、lat/lon、alt、speed)。 coursevbat

  • 可以不落盘(UI 大多用轨迹方向估计就够了;vbat 在 topbar 也不一定要显示队友)
  • 需要时再出 PosRecV2(加 2 字节 course、2 字节 vbat,不难)

7) 版本与兼容规则

  • payload 必须能解码为 meshtastic_Position,否则丢弃(或统计 error
  • 不做自定义版本字段检查(以 Meshtastic 协议为准)