refactor(storage): partition protocol persistence

This commit is contained in:
liu weikai committed 2026-07-20 22:28:48 +08:00
1 parent ac457dce9e
commit eced19abfc
55 files changed
+9368 -3950

No files matched your search

@@ -0,0 +1,265 @@
# Protocol-Partitioned Storage V2 Detailed Design
## Components
### `FixedSlotJournalEngine`
Owns journal framing and descriptor validation:
```text
magic | schema | protocol | kind | slot_size | descriptor_crc
slot 0
slot 1
...
```
The engine reports `Missing`, `Ready`, `Incompatible`, `PartialTail`, or
`IoError`. It never interprets protocol payloads.
`replaceFileAtomically` and `recoverAtomicFile` own snapshot rename recovery.
Callers must not duplicate remove/rename sequences.
### Protocol Chat Codecs
`protocol_chat_codec` owns MT, MC, and RT message/catalog/read/status/seen slot
layouts. Each slot has its own CRC.
Text capacity is protocol-fixed. It is not represented by a single large union
record. RT-only identity and LXMF hash fields exist only in RT slots.
### `SdStore`
Owns:
- message segments;
- catalog hot projection;
- read-watermark hot projection;
- outgoing status hot projection;
- RT seen hot projection.
Hot vectors use strict PSRAM allocators. The 512-byte codec scratch buffer is a
store member, not a large task-stack local.
### Protocol Peer Codecs
`protocol_peer_codec` owns protocol peer and contact slots.
Peer slot rules:
- common prefix: identity kind, timestamps, display fact, source, radio
observations, device metrics, position;
- MT suffix: MT node facts, next hop, public key, verification state;
- MC suffix: MC node facts, public key, peer hash, next hop, node-id hint;
- RT suffix: destination/identity hashes, public keys, ratchet, delivery and
propagation capabilities.
Contact slot rules:
- MT key: node ID;
- MC key: public key plus node-id projection hint;
- RT key: destination hash;
- payload: alias and user flags.
Peer slots intentionally exclude alias and user flags.
### `SdProtocolPeerRepository`
Owns canonical PSRAM vectors:
- `peers_`;
- `contacts_`;
- `pending_peer_deltas_`.
It also owns a temporary active-protocol `NodeEntry` projection required by the
current `INodeStore` API. That vector is a read projection and is never a fact
source.
`NodeStoreView` and `ContactStoreView` forward application operations into the
repository. They have no files and no independent caches.
## Initialization
```text
create SdStore
-> acquire startup storage lease
-> recover snapshot backups
-> load protocol projections
-> reconcile catalog from message segments
-> rebuild RT seen ledger if required
-> compact threshold-exceeding projections
create SdProtocolPeerRepository(SdStore)
-> acquire startup storage lease
-> create /data/v2/{mt,mc,rt}
-> recover peer/contact snapshot backups
-> load peer snapshot + delta
-> load contact snapshot + delta
-> reconcile temporary/stable identities
-> overlay contact facts
-> compact threshold-exceeding journals
create ContactService(repository.nodeStoreView,
repository.contactStoreView)
```
The repository outlives `ContactService`; AppContext stores node/contact as
non-owning pointers and owns the repository in the chat service bundle.
## Message Transactions
### Inbound MT/MC
1. Append the protocol message slot and flush it.
2. Return durable success to `MessageLedger`.
3. Update the in-memory catalog.
4. Attempt catalog delta append.
5. If step 4 fails, mark the catalog dirty but keep durable success.
### Inbound RT
1. Detect an existing LXMF hash from hot seen state or the seen journal.
2. Append the RT message slot.
3. Append the LXMF hash to `seen.delta`.
4. Only after both writes succeed, return durable success.
5. Catalog failure remains a rebuildable projection failure.
If step 2 succeeds and step 3 fails, retry sees the same stored message and
completes the missing seen append without duplicating the message.
## Peer Merge Transaction
For an exact stable identity:
1. Merge non-empty names and fresh observations.
2. Preserve first-seen minimum and last-seen maximum.
3. Preserve contact alias and flags from the contact projection.
4. Preserve a verified key when a conflicting unverified key arrives.
5. Append a complete merged peer delta or queue it in PSRAM.
For temporary-to-stable identity upgrade:
1. Find temporary peer by projected node ID.
2. Merge temporary facts into the stable incoming record.
3. Append/queue the stable record.
4. Append/queue the temporary identity tombstone.
5. Replace the in-memory identity.
Stable-first ordering prefers a recoverable duplicate over loss. Startup
reconciliation merges a duplicate temporary/stable pair if power fails before
the tombstone is durable.
## Contact Transaction
1. Resolve the node ID in the active protocol partition.
2. Require MT node identity, MC public key, or RT destination identity.
3. Build a complete contact projection with alias and flags.
4. Acquire a bounded durable storage lease.
5. Append and flush `contacts.delta`.
6. Apply the projection to memory.
7. Refresh only the affected peer/node projection.
Failure before step 5 leaves memory unchanged and returns failure to the user
intent. A contact edit is never queued invisibly.
## Retention Algorithm
Insertion first resolves exact or upgrade identity. Capacity handling occurs
only for a genuinely new peer.
At the 2048 nearby threshold, the repository loads the protocol conversation
catalog once, then scans peers once. It does not query storage separately for
each peer.
A candidate is eligible only when:
- it belongs to the target protocol;
- it has no contact/user-fact projection;
- no conversation references its stable identity or projected node ID.
The eligible peer with the smallest `last_seen_s` receives a tombstone. If no
eligible peer exists, insertion returns `CapacityExceeded`.
## Pending Peer Delta Queue
Peer observations must not block packet or UI work on shared SPI contention.
The queue uses a PSRAM vector plus a head index; draining does not repeatedly
erase the first vector element. Consecutive pending full-record updates for the
same identity and deletion state are coalesced to the newest projection.
Each service tick drains at most four entries. Call realtime preemption prevents
the AppContext flush call, so audio resource ownership remains intact.
## Snapshot Compaction
Snapshot compaction uses complete current projections and runs at startup when
delta thresholds are exceeded:
- catalog: 512 deltas;
- read state: 256 deltas;
- status: 2048 deltas;
- peer/contact: 1024 deltas.
Normal chat `flush()` does not inspect and compact all healthy journals. It
only retries a dirty protocol projection, at most once per five seconds, using
a non-blocking background SPI lease.
Snapshot replacement sequence:
1. remove stale temp;
2. create and completely flush temp journal;
3. remove stale backup;
4. rename final to backup;
5. rename temp to final;
6. remove backup;
7. truncate/remove the now-covered delta.
Startup recovery restores backup when final is missing. If final exists,
stale temp and backup are removed.
## Corruption Recovery
Message segments are authoritative and are never discarded because a catalog
slot is corrupt.
Catalog reconciliation enumerates conversation segment directories and
reconstructs count, latest preview, timestamp, and unread state.
RT seen recovery streams all RT message slots and writes every valid LXMF hash
into a new seen snapshot. It does not allocate a complete message corpus in
memory.
Peer/contact incompatible v2 headers fail repository startup for that v2
partition. This is an explicit storage error, not permission to read legacy
files. Partial tails retain complete slots; contact/peer snapshots are recovered
through backup files.
## Concurrency And SPI
The peer repository serializes its aggregate with a FreeRTOS mutex. It holds one
repository lock while applying an aggregate transaction.
All peer/contact journal I/O uses the shared storage bus arbiter. Peer appends
request a zero-wait background lease; contact edits use a bounded durable lease.
Chat authoritative append uses a zero-wait durable lease and delegates retry to
`MessageLedger`. No renderer waits for radio TX, MQTT forwarding, LoRa airtime,
or projection compaction.
## Test Matrix
| Case | Expected result |
|---|---|
| MQTT inbound, catalog append fails | Message publishes; catalog dirty |
| RT message slot exists, seen append failed | Retry completes seen; no duplicate message |
| Seen journal corrupt | Rebuilt from RT message journals |
| Peer append SPI busy | Peer visible in memory; ordered delta queued |
| Contact append SPI busy | Contact edit fails; memory unchanged |
| MC node observed before key | Temporary peer persists; cannot become contact |
| MC key later arrives | Stable record then temporary tombstone |
| RT protocol event before destination identity | Temporary record upgrades to destination |
| Conflicting unverified key | Verified key retained |
| Nearby capacity full | Oldest unprotected nearby evicted |
| All peers protected | New nearby rejected; contacts retained |
| Power loss during snapshot rename | Backup restored at startup |
@@ -0,0 +1,156 @@
# Protocol-Partitioned Storage V2 Overview Design
## Context
The previous ESP runtime maintained overlapping facts in chat files,
`/nodes.bin`, `/contacts.dat`, and `/mesh/peers.bin`. A single remote node could
be represented by multiple records with different update semantics. Whole-file
projection writes also allowed an index failure to prevent a durable inbound
message from reaching the UI.
Storage v2 replaces that shape with two owners:
```text
ChatService / MessageLedger
-> SdStore
-> MT chat partition
-> MC chat partition
-> RT chat partition
Protocol adapters + ContactService
-> IProtocolPeerRepository
-> SdProtocolPeerRepository
-> MT peer/contact partition
-> MC peer/contact partition
-> RT peer/contact partition
```
## Architectural Pattern
The design combines four patterns.
**Repository:** `SdProtocolPeerRepository` owns the aggregate of stable peer
identity, protocol facts, observations, and user contact facts.
**Ports and views:** Protocol adapters use `IMeshPeerDirectory`; application
contact logic uses `INodeStore` and `IContactStore`. Node/contact views do not
own storage and cannot diverge from the repository.
**Append-only event journal:** Messages, delivery status, read watermarks,
peer changes, and contact changes append immutable slots. Snapshots are compact
projections, not authoritative mutable blobs.
**CQRS projection:** UI queries read catalog/contact/node projections. Incoming
message and peer writes do not wait for expensive UI-oriented reconstruction.
## High-Level Commit Flows
### Incoming Message
```text
protocol RX
-> decode ChatMessage
-> MessageLedger
-> SdStore.appendIncomingDurably
-> message slot
-> RT seen identity when required
-> update in-memory catalog projection
-> attempt catalog delta; mark dirty on contention/failure
-> observer publication
-> model
-> event bus
-> notification
-> background catalog projection retry/rebuild when dirty
```
### Peer Observation
```text
protocol announce/node info/key observation
-> IMeshPeerDirectory.record
-> merge by protocol stable identity
-> preserve verified key and contact facts
-> non-blocking peer delta append
-> success
-> or ordered PSRAM pending queue
```
### Contact Edit
```text
user contact intent
-> ContactService
-> IContactStore view
-> resolve active-protocol stable identity
-> durable contact delta
-> update repository aggregate
-> invalidate contact projection
```
## Why Protocol Partitions
Physical partitioning removes fields that are meaningless to a protocol:
- MT does not carry RT destination/identity/LXMF hashes;
- MC does not carry MT channel-key state;
- RT does not carry Meshtastic role/hardware/MAC fields.
The business layer remains shared. This preserves one delivery state machine,
one read-state meaning, one retry policy, and one UI presentation model.
## Capacity And Cost
Approximate fixed slot sizes are:
| Data | MT | MC | RT |
|---|---:|---:|---:|
| Message | 279 B | 217 B | 365 B |
| Peer | 217 B | 225 B | 264 B |
| Contact | 29 B | 61 B | 41 B |
The previous peer union was roughly 488 B and coexisted with a separate node
record of roughly 180 B. A normal MT peer therefore drops from two overlapping
records totaling about 668 B to one protocol peer slot of about 217 B, plus a
29 B contact slot only when the user owns contact facts.
At fixed-slot payload level, one MiB stores approximately:
- 3758 MT messages;
- 4832 MC messages;
- 2872 RT messages;
- 4832 MT peers, 4660 MC peers, or 3971 RT peers.
Filesystem, journal headers, segments, status deltas, and catalog projections
reduce those theoretical counts. There is no 200-node physical store limit and
no fixed message-count cap; practical capacity is governed by SD space and the
explicit runtime retention policy.
The runtime retains up to 2048 unprotected nearby peers per protocol. Contacts
and conversation-referenced peers are outside that eviction budget. Up to 4096
contact/user-fact projections are accepted in total; existing contacts are
never evicted to admit strangers.
## Evolution Space
The partition boundary allows protocol evolution without widening every
record:
- MT can add a new slot schema carrying MT-only telemetry or key state;
- MC can change route/peer-hash facts without touching MT or RT files;
- RT can add propagation-node, ratchet, or resource metadata only to RT;
- a new protocol can add a codec and partition while reusing MessageLedger,
ChatService, delivery projection, and UI contracts;
- catalogs can gain secondary indexes without changing message durability;
- old message segments can be archived or pruned by segment, not per-message
file churn;
- background compaction can move to a dedicated storage worker without
changing repository semantics;
- active-protocol paging can replace the current transient node vector without
changing persisted facts.
## Product Result
Protocol switching now changes the complete Chat/Contacts/Nearby projection,
not just a UI filter. A peer has one canonical repository record, contact facts
cannot be overwritten by node refreshes, and projection I/O can no longer hide
an already durable MQTT/LoRa/Reticulum message.
@@ -89,6 +89,33 @@ platform feedback through `IChatDeliveryFeedbackPort`.
`ChatPresentationSource` reads the delivery read model and projects state
into `MessageRow`.
`ChatMessageLedger` also owns transient persistence pressure. A persistent
store reports authoritative append success through `appendDurably(...)`.
Transient failure enters a bounded pending-write queue owned by the ledger;
the presentation source reads a merged ledger page, never a page-local fallback.
Incoming publication is a two-phase boundary:
```text
decode -> durable append/defer -> delivery commit -> model/event/notification
```
`appendIncomingDurably(...)` failure must not drop the message and must not
publish it early. The runtime retries with a bounded per-tick budget. Once the
append succeeds, observers are invoked exactly once. Queue exhaustion is an
explicit rejection, not a silent loss.
On ESP, the physical commit and recovery rules are frozen by
`PROTOCOL_PARTITIONED_STORAGE_V2_SPEC.md`. A protocol message slot is
authoritative; catalog/read/status are projections. Once an inbound message and
its required RT LXMF dedup identity are durable, catalog failure must not block
observer, UI, or notification publication.
On ESP shared-SPI targets, chat append and status transactions use a
non-blocking outer SPI lease. Failure to obtain the lease produces `Deferred`;
the hot path must not multiply the storage runtime's per-operation wait or run
a synchronous full index rebuild.
## Boundaries
`ChatDeliveryReadModel` may:
@@ -200,9 +227,15 @@ Examples:
Phase 7.1 does not implement a full retry engine.
The bounded persistence retry owned by `ChatMessageLedger` is not a protocol
retransmission engine. It only completes local authoritative message/status
commit after transient storage contention.
Phase 7.1 does not change radio packet format.
Phase 7.1 does not move all ChatService storage.
Phase 7.1 does not move delivery business ownership into the physical store.
`SdStore` may own protocol-partitioned journals, but delivery meaning remains in
MessageLedger and the delivery projector.
Phase 7.1 does not make `ChatWorkspaceModel` own delivery state.
@@ -0,0 +1,266 @@
# Protocol-Partitioned Storage V2 Specification
## Purpose
This specification freezes the authoritative storage model for MT, MC, and RT
chat, peer, contact, delivery, and read-state data on the ESP Arduino product.
The design has four goals:
- a received message becomes visible only after its authoritative message
record is durable;
- projection failures never suppress an already durable message;
- protocol-specific facts are not stored in a cross-protocol union;
- contacts and conversation-referenced peers cannot be displaced by nearby
discovery traffic.
## Product Boundary
The active ESP Arduino runtime must use only the v2 layout:
```text
/data/v2/mt/
/data/v2/mc/
/data/v2/rt/
```
It must not read or register compatibility readers for:
```text
/chat/*
/nodes.bin
/contacts.dat
/mesh/peers.bin
```
Failure to load a v2 journal may trigger v2 recovery from an authoritative v2
journal. It must not fall back to a v1 file.
## Ownership
`SdStore` owns authoritative chat message journals and their projections.
`SdProtocolPeerRepository` is the single owner of protocol peer facts and user
contact facts. It exposes three ports:
- `IMeshPeerDirectory` for protocol runtimes;
- `INodeStore` as an active-protocol contact projection;
- `IContactStore` as an active-protocol user-fact projection.
The node and contact ports are views of the same repository. They must never be
backed by independent ESP stores.
`ChatService`, `MessageLedger`, `ChatDeliveryEventProjector`, and UI
presentation remain protocol-independent business owners. Physical protocol
partitioning must not duplicate these services or their state machines.
## Authoritative Commit Rule
For an inbound message:
```text
decode
-> append protocol message slot
-> append RT LXMF dedup identity when applicable
-> commit MessageLedger delivery
-> publish model/event/notification
-> append/rebuild catalog projection
```
The message slot and required RT dedup identity are authoritative. Catalog,
preview, unread count, and status indexes are projections.
If the message append fails, publication is deferred by `MessageLedger`.
If the message append succeeds but a catalog projection append fails, the
message is still committed and published. The catalog is marked dirty and is
rebuilt from message journals.
This rule specifically prevents the former MQTT failure mode where a downlink
reached `ChatService`, storage projection I/O failed, and the UI/notification
observer never saw the message.
## Chat Physical Layout
Each protocol owns independent chat files:
```text
/data/v2/<protocol>/chat/
catalog.snapshot
catalog.delta
read.snapshot
read.delta
status.snapshot
status.delta
conversations/<conversation-key>/0000.msg
conversations/<conversation-key>/0001.msg
```
RT additionally owns:
```text
/data/v2/rt/chat/seen.snapshot
/data/v2/rt/chat/seen.delta
```
Message segments are 128 KiB fixed-slot journals. Slots are protocol-specific:
- MT text capacity is 233 bytes and the MT slot is fixed-size;
- MC text capacity follows the MC wire/product limit;
- RT text and identity fields follow the LXMF product limit;
- only RT slots carry RT destination, identity, and LXMF hash fields.
Status changes are appended as status projection records. Existing message
slots are never rewritten to update delivery state.
Read state is a `last_read_sequence` projection. Receiving a message does not
rewrite the complete read-state file.
## Peer And Contact Physical Layout
Each protocol owns:
```text
/data/v2/<protocol>/
peers.snapshot
peers.delta
contacts.snapshot
contacts.delta
```
Peer slots contain only protocol identity, protocol facts, timestamps, source,
display facts, metrics, and position observations.
Contact slots contain only stable identity plus user-owned facts:
- alias;
- favorite;
- ignored;
- trusted;
- MC node-id projection hint where required by the current UI identity.
Alias and user flags must not be duplicated in peer slots.
## Identity Rules
MT stable identity is node ID. Its public key is a protocol fact.
MC stable identity is the 32-byte public key. A node-ID-only observation may be
persisted temporarily, but it cannot become a contact until a stable public key
is known. When the key arrives, the repository appends the stable record before
tombstoning the temporary identity.
RT stable identity is the destination hash. A node-ID-only observation may be
persisted temporarily. A later destination identity upgrades it using the same
stable-first ordering.
A manually verified MT key or verified MC key must not be replaced by a
conflicting unverified runtime observation. Other fresh node facts may still be
merged.
## Retention Rules
Nearby/discovered records are preemptible storage entries.
Per protocol, at most 2048 unprotected nearby records are retained. When that
budget is full, only the oldest unprotected nearby record may be tombstoned.
A peer is protected when either condition is true:
- it has a contact/user-fact projection;
- an existing chat conversation references it.
Protected peers do not consume the nearby eviction budget and must never be
selected as nearby eviction victims.
The repository accepts up to 4096 protected contact/user-fact projections in
total. Reaching that explicit limit rejects a new contact; it never evicts an
existing contact. This limit is independent of the 2048 nearby budget.
## Active Protocol Query Rule
Switching protocol changes the application query partition:
- Chat returns only conversations for the active protocol;
- Contacts returns only contacts for the active protocol;
- Nearby returns only peers for the active protocol.
The UI must not load all protocols and filter them locally.
## Memory Rule
Canonical chat projections, peer records, contact records, pending peer deltas,
and codec scratch ownership use the single platform `PsramAllocator`.
On ESP that allocator requests `MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT` and has no
internal-heap fallback. Allocation failure is explicit and fatal rather than a
silent migration of large protocol data to internal RAM.
Small RTOS objects and the existing `INodeStore` active UI projection may use
internal memory. They must not become an additional fact source.
## Scheduling Rule
Message append uses a non-blocking durable SPI lease. Contention defers the
message through the ledger rather than blocking the UI.
Peer observation deltas use a non-blocking append. Failed appends enter a
PSRAM-backed ordered queue. The runtime drains at most four peer deltas per
service tick and does not drain during call realtime resource preemption.
Contact edits are explicit user transactions and use a bounded durable lease.
Memory is updated only after the contact delta is durable.
Large snapshot compaction runs during startup. Normal runtime flush only
retries a dirty chat projection at a throttled interval; it does not compact
healthy read/status journals on every UI loop.
## Recovery Rule
Every journal has a versioned descriptor and every slot has a CRC.
A partial tail exposes only complete slots. Authoritative message journals are
reconciled into catalog/read projections.
Snapshot replacement uses:
```text
temp complete
-> final renamed to backup
-> temp renamed to final
-> backup removed
```
Startup restores the backup if power was lost between the two renames.
The RT seen ledger is a projection of authoritative RT messages. If it is
missing, incompatible, partially written, or contains a corrupt slot, it is
rebuilt by streaming RT message journals. This prevents propagation from
re-presenting already stored LXMF messages after index damage.
## Forbidden Changes
- Do not add a v1 reader or v2-to-v1 fallback.
- Do not reintroduce an ESP `NodeStore` or `ContactStore` beside the protocol
repository.
- Do not make catalog/index success a prerequisite for publishing a durable
message.
- Do not write contact aliases or flags into peer slots.
- Do not evict contacts to admit nearby observations.
- Do not rewrite complete index/read/contact files on every message.
- Do not compact large snapshots in a renderer, notification callback, packet
callback, or ordinary UI tick.
- Do not use a second PSRAM STL allocator implementation.
## Verification
Required checks include:
- MT/MC/RT max-size message slot round trips and CRC rejection;
- temporary and stable MC/RT peer identity round trips;
- contact identity and MC node-hint round trips;
- peer slots do not round-trip contact flags or aliases;
- verified keys survive conflicting unverified updates;
- ESP source contract contains no registered v1 paths or old store classes;
- MQTT durable-before-projection ordering remains enforced;
- Pager PlatformIO build and ESP stack hygiene pass.
@@ -272,6 +272,50 @@ Settings 可以配置:
3. 在 retry、delivery action、presentation lookup 中丢掉 protocol 字段。
4. 因为找不到消息就创建另一个同内容 outgoing item。
### Message Persistence And Publication
消息内容、发送状态和 UI/通知事件必须走同一条 ledger 主路径:
```text
decoded incoming
-> MessageLedger durable append or bounded deferred queue
-> incoming delivery commit
-> ChatModel / EventBus / notification
-> conversation projection
outgoing protocol acceptance
-> MessageLedger durable append or bounded deferred queue
-> protocol-aware delivery event
-> conversation projection
```
硬约束:
1. incoming 只有在 authoritative message record、dedup identity 和 read-state commit
成功后才能发布 `ChatNewMessageEvent`、通知、震动或声音。
2. SD/SPI 暂时不可用时,incoming 进入固定深度 deferred queue;重试成功后只提交和发布
一次。队列满必须输出明确 rejected/drop reason,并向支持 two-phase commit 的协议返回失败。
3. outgoing record 或后续 status 写入失败时,由 `MessageLedger` 保留 bounded pending write;
message page、conversation page 和 lookup 必须合并该 pending state,不能谎报 `stored`,
也不能让 UI 另造临时消息。
4. pending write 每个 runtime tick 只允许执行有限预算。ESP chat store 必须先以 non-blocking
方式取得共享 SPI transaction lease;拿不到立即 deferred,不能在同一 tick 连续等待多个
250ms SD 操作。
5. conversation index、header mirror 和 UI cache 仍然只是 projection。projection 写失败可以让
ledger operation 保持 pending,但不得触发收发热路径中的同步全盘 `rebuildIndex()`。
6. message/status retry 必须幂等。已经写入 conversation log、但后续 ledger/projection 写失败的
消息,重试时只能完成未完成的提交,不能追加第二条相同记录。
7. chat workspace 最新页固定为 10 条;翻页继续使用同一个 ledger page API。runtime event 对
当前会话最多触发一次 snapshot reload,不允许辅助函数和调用者各做一次全量重建。
禁止:
1. `appendIncomingDurably(...) == false` 后直接 `continue` 并丢弃消息。
2. 调用返回 `void append(...)` 后无条件记录 `stored`。
3. UI 监听 raw MQTT/LoRa packet,或在持久化失败时自行合成 message bubble。
4. 为了修复通知而绕过 durable commit 直接调用 notification/audio。
5. index 写失败后在消息收发 tick 内同步扫描所有 conversation log。
## Read And Unread Contract
`ReadStateLedger` 是 read/unread 的唯一权威。
@@ -494,6 +538,41 @@ Contacts 和 Network 都只能消费 projection,不允许读取 raw announce
4. old mutation 已删除,而不是留作 fallback。
5. compatibility code 与 product graph 隔离。
## Protocol-Partitioned Storage V2 Contract
ESP Arduino 产品运行时的 Chat/Peer/Contact 持久化只允许使用:
```text
/data/v2/mt
/data/v2/mc
/data/v2/rt
```
权威规则:
1. message journal 是消息事实;catalog/read/status 是可重建 projection。
2. RT message journal 是 LXMF seen ledger 的重建来源。
3. `SdProtocolPeerRepository` 是 peer facts 和 contact user facts 的唯一 owner。
4. `INodeStore`、`IContactStore` 是 repository view,不是独立 store。
5. contact alias/favorite/ignored/trusted 只写 contact journal,不重复写 peer slot。
6. active protocol 必须在 application query 边界过滤,UI 不读取三协议全量后再过滤。
7. nearby 只能淘汰 unprotected peer;contact 和 conversation reference 永远受保护。
8. snapshot 只能通过 temp/backup/final 原子替换;掉电后只恢复 v2 backup。
9. 大 projection/peer/contact/pending buffer 优先使用唯一的 strict PSRAM allocator。
10. 启动阶段可以做有界 compaction;普通 UI tick 不得反复扫描或重写完整 projection。
ESP product graph 禁止重新注册:
```text
/chat/*
/nodes.bin
/contacts.dat
/mesh/peers.bin
```
禁止以“兼容”为名在 v2 失败后读取旧格式。完整规则见
`PROTOCOL_PARTITIONED_STORAGE_V2_SPEC.md`。
## Prohibited Patch Patterns
以下修改方式禁止进入主线:
@@ -546,6 +625,10 @@ Contacts 和 Network 都只能消费 projection,不允许读取 raw announce
14. message alerts/contact alerts/vibration/audio volume 策略不改变 delivery 或 unread 事实。
15. Contacts 不出现 propagation、service、gateway/interface、unknown announce。
16. Network 能呈现服务和网络基础设施,不污染 Contacts。
17. MQTT/LoRa/RT durable message 不因 catalog projection 失败而不展示或不通知。
18. peer refresh 不会覆盖 contact alias/flags 或已验证 key。
19. nearby 达到容量时不会淘汰 contact 或已有 conversation peer。
20. RT seen journal 损坏后从 authoritative RT message journal 重建,历史消息不重复投影。
## Relationship To Existing Specs
@@ -559,5 +642,8 @@ Contacts 和 Network 都只能消费 projection,不允许读取 raw announce
6. `docs/reticulum_client_architecture.md`
7. `docs/wifi_access_resource_policy.md`
8. `docs/MULTI_PROTOCOL_SUPPORT.md`
9. `docs/specification/PROTOCOL_PARTITIONED_STORAGE_V2_SPEC.md`
10. `docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_OVERVIEW.md`
11. `docs/design/PROTOCOL_PARTITIONED_STORAGE_V2_DETAILED_DESIGN.md`
当实现与本文档冲突时,不能通过局部代码补丁解决;必须回到 owner 边界,修正主路径。
@@ -4,9 +4,21 @@
#include "chat/domain/chat_model.h"
#include "chat/ports/i_chat_store.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>
namespace chat::delivery
{
enum class LedgerPersistence : uint8_t
{
Durable,
Deferred,
Rejected,
};
class ChatMessageLedger final
{
public:
@@ -14,9 +26,11 @@ class ChatMessageLedger final
void setDeliveryEventPort(IChatDeliveryEventPort* delivery_event_port);
void recordOutbound(const ChatMessage& message,
bool model_enabled,
SendFailureKind failure = SendFailureKind::Unknown);
LedgerPersistence recordOutbound(
const ChatMessage& message,
bool model_enabled,
SendFailureKind failure = SendFailureKind::Unknown);
LedgerPersistence recordIncoming(const ChatMessage& message);
bool applyOutboundStatus(MessageId msg_id,
MessageStatus status,
bool model_enabled,
@@ -34,19 +48,70 @@ class ChatMessageLedger final
bool markRetryQueuedForProtocol(MessageId msg_id,
MeshProtocol protocol,
bool model_enabled);
std::size_t flushPendingWrites(std::size_t budget = 1);
std::vector<ChatMessage> loadPageFromLatest(
const ConversationId& conversation,
std::size_t offset_from_latest,
std::size_t limit,
std::size_t* total) const;
std::vector<ChatMessage> loadRecent(const ConversationId& conversation,
std::size_t limit) const;
std::vector<ConversationMeta> loadConversationPage(MeshProtocol protocol,
std::size_t offset,
std::size_t limit,
std::size_t* total) const;
bool findMessage(MessageId msg_id, ChatMessage& out) const;
bool findMessageForProtocol(MessageId msg_id,
MeshProtocol protocol,
ChatMessage& out) const;
void clearConversation(const ConversationId& conversation);
void clear();
private:
static constexpr std::size_t kPendingOutboundWriteDepth = 8;
static constexpr std::size_t kPendingStatusWriteDepth = 16;
struct PendingOutboundWrite
{
bool used = false;
uint32_t sequence = 0;
ChatMessage message{};
};
struct PendingStatusWrite
{
bool used = false;
uint32_t sequence = 0;
MessageId msg_id = 0;
MeshProtocol protocol = MeshProtocol::Meshtastic;
MessageStatus status = MessageStatus::Queued;
};
bool lookupMessage(MessageId msg_id, ChatMessage& out) const;
bool lookupMessageForProtocol(MessageId msg_id,
MeshProtocol protocol,
ChatMessage& out) const;
bool writeStatus(MessageId msg_id,
MessageStatus status,
bool model_enabled);
bool writeStatusForProtocol(MessageId msg_id,
MeshProtocol protocol,
MessageStatus status,
bool model_enabled);
PendingOutboundWrite* findPendingOutbound(MessageId msg_id);
PendingOutboundWrite* findPendingOutboundForProtocol(
MessageId msg_id,
MeshProtocol protocol);
const PendingOutboundWrite* findPendingOutbound(MessageId msg_id) const;
const PendingOutboundWrite* findPendingOutboundForProtocol(
MessageId msg_id,
MeshProtocol protocol) const;
bool enqueuePendingOutbound(const ChatMessage& message);
bool enqueuePendingStatus(MessageId msg_id,
MeshProtocol protocol,
MessageStatus status);
void removePendingStatus(MessageId msg_id, MeshProtocol protocol);
void applyPendingStatus(ChatMessage& message) const;
int oldestPendingOutboundIndex() const;
int oldestPendingStatusIndex() const;
uint32_t nextPendingSequence();
void publishDeliveryEvent(const ChatMessage& message,
MessageStatus status,
uint32_t timestamp_ms = 0,
@@ -56,6 +121,11 @@ class ChatMessageLedger final
ChatModel& model_;
IChatStore& store_;
IChatDeliveryEventPort* delivery_event_port_ = nullptr;
std::array<PendingOutboundWrite, kPendingOutboundWriteDepth>
pending_outbound_writes_{};
std::array<PendingStatusWrite, kPendingStatusWriteDepth>
pending_status_writes_{};
uint32_t next_pending_sequence_ = 1;
};
} // namespace chat::delivery
@@ -1,6 +1,7 @@
#pragma once
#include "chat/domain/chat_types.h"
#include "chat/domain/contact_types.h"
#include "chat/domain/reticulum_identity.h"
#include <cstddef>
@@ -18,6 +19,7 @@ constexpr std::size_t kMeshPeerMeshCorePublicKeyLen = 32;
constexpr std::size_t kMeshPeerReticulumPublicKeyLen = 32;
constexpr std::size_t kMeshPeerReticulumRatchetLen = 32;
constexpr std::size_t kMeshPeerMacAddrLen = 6;
constexpr std::size_t kMeshPeerUserAliasMaxLen = 12;
enum class MeshPeerSource : uint8_t
{
@@ -69,6 +71,8 @@ struct MeshPeerNodeFacts
struct MeshtasticPeerFacts
{
MeshPeerNodeFacts node{};
bool has_next_hop = false;
uint8_t next_hop = 0;
bool has_public_key = false;
uint8_t public_key[kMeshPeerMeshtasticPublicKeyLen] = {};
bool key_manually_verified = false;
@@ -84,6 +88,7 @@ struct MeshCorePeerFacts
uint8_t peer_hash = 0;
bool has_next_hop = false;
uint8_t next_hop = 0;
NodeId node_id_hint = 0;
};
struct ReticulumPeerFacts
@@ -99,6 +104,18 @@ struct ReticulumPeerFacts
bool propagation = false;
};
struct MeshPeerObservations
{
bool has_snr = false;
float snr = 0.0f;
bool has_rssi = false;
float rssi = 0.0f;
bool has_device_metrics = false;
contacts::NodeDeviceMetrics device_metrics{};
bool has_position = false;
contacts::NodePosition position{};
};
struct MeshPeerRecord
{
bool valid = false;
@@ -107,7 +124,9 @@ struct MeshPeerRecord
uint32_t first_seen_s = 0;
uint32_t last_seen_s = 0;
char display_name[kMeshPeerDisplayNameMaxLen] = {};
char user_alias[kMeshPeerUserAliasMaxLen + 1] = {};
MeshPeerUserFlags flags{};
MeshPeerObservations observations{};
MeshtasticPeerFacts meshtastic{};
MeshCorePeerFacts meshcore{};
ReticulumPeerFacts reticulum{};
@@ -10,6 +10,9 @@
namespace chat
{
MeshPeerRecord mergeMeshPeerRecordFacts(const MeshPeerRecord& existing,
const MeshPeerRecord& incoming);
class MeshPeerDirectoryCore final : public IMeshPeerDirectory
{
public:
@@ -6,6 +6,9 @@
#pragma once
#include "../domain/chat_types.h"
#include <algorithm>
#include <cstddef>
#include <vector>
namespace chat
@@ -26,6 +29,19 @@ class IChatStore
*/
virtual void append(const ChatMessage& msg) = 0;
/**
* Append a message and report whether its authoritative record is durable.
*
* In-memory stores cannot fail independently and may inherit this default.
* Persistent stores override it so the message ledger can defer transient
* storage failures without inventing a second UI or protocol path.
*/
virtual bool appendDurably(const ChatMessage& msg)
{
append(msg);
return true;
}
/**
* Persist an incoming message and any durable delivery identity it owns.
*
@@ -35,8 +51,7 @@ class IChatStore
*/
virtual bool appendIncomingDurably(const ChatMessage& msg)
{
append(msg);
return true;
return appendDurably(msg);
}
/**
@@ -97,6 +112,53 @@ class IChatStore
size_t limit,
size_t* total) = 0;
/**
* Load conversations owned by one product protocol partition.
*
* Stores with physical protocol partitions override this method. The
* default keeps non-persistent stores source-compatible while ensuring the
* application query, rather than the UI, owns protocol filtering.
*/
virtual std::vector<ConversationMeta> loadConversationPageForProtocol(
MeshProtocol protocol,
size_t offset,
size_t limit,
size_t* total)
{
const MeshProtocol normalized =
protocol == MeshProtocol::RNode ? MeshProtocol::Reticulum : protocol;
size_t all_total = 0;
std::vector<ConversationMeta> all =
loadConversationPage(0, 0, &all_total);
std::vector<ConversationMeta> filtered;
filtered.reserve(all.size());
for (const ConversationMeta& meta : all)
{
const MeshProtocol item_protocol =
meta.id.protocol == MeshProtocol::RNode
? MeshProtocol::Reticulum
: meta.id.protocol;
if (item_protocol == normalized)
{
filtered.push_back(meta);
}
}
if (total)
{
*total = filtered.size();
}
if (offset >= filtered.size())
{
return {};
}
const size_t end =
limit == 0 ? filtered.size()
: std::min(filtered.size(), offset + limit);
return std::vector<ConversationMeta>(
filtered.begin() + static_cast<std::ptrdiff_t>(offset),
filtered.begin() + static_cast<std::ptrdiff_t>(end));
}
/**
* @brief Set unread count for conversation
* @param conv Conversation ID
@@ -5,6 +5,8 @@
#pragma once
#include "chat/domain/chat_types.h"
#include <cstdint>
#include <string>
#include <vector>
@@ -23,6 +25,11 @@ class IContactStore
public:
virtual ~IContactStore() = default;
virtual void setActiveProtocol(MeshProtocol protocol)
{
(void)protocol;
}
/**
* @brief Initialize store (load from persistent storage)
*/
@@ -5,6 +5,7 @@
#pragma once
#include "chat/domain/chat_types.h"
#include "chat/domain/contact_types.h"
#include <cstdint>
#include <vector>
@@ -64,6 +65,11 @@ class INodeStore
public:
virtual ~INodeStore() = default;
virtual void setActiveProtocol(MeshProtocol protocol)
{
(void)protocol;
}
/**
* @brief Initialize store (load from persistent storage)
*/
@@ -0,0 +1,25 @@
#pragma once
#include "chat/ports/i_contact_store.h"
#include "chat/ports/i_mesh_peer_directory.h"
#include "chat/ports/i_node_store.h"
namespace chat
{
/**
* Canonical owner for protocol peer facts and user contact facts.
*
* The three inherited ports are separate read/write views of one repository;
* they must never be backed by independent stores in the product runtime.
*/
class IProtocolPeerRepository : public IMeshPeerDirectory
{
public:
~IProtocolPeerRepository() override = default;
virtual contacts::INodeStore& nodeStoreView() = 0;
virtual contacts::IContactStore& contactStoreView() = 0;
};
} // namespace chat
@@ -270,6 +270,45 @@ class ChatService
std::size_t count = 0;
};
static constexpr std::size_t kDeferredIncomingDepth = 8;
static constexpr std::size_t kDeferredIncomingTextMaxLen = 255;
struct DeferredIncomingQueue
{
struct Slot
{
MeshProtocol protocol = MeshProtocol::Meshtastic;
ChannelId channel = ChannelId::PRIMARY;
NodeId from = 0;
NodeId to = 0;
MessageId msg_id = 0;
uint32_t timestamp = 0;
uint8_t hop_limit = 0xFF;
bool encrypted = false;
ReticulumPeerIdentity reticulum_identity{};
bool has_reticulum_lxmf_hash = false;
uint8_t reticulum_lxmf_hash[kReticulumLxmfHashSize] = {};
bool source_unverified = false;
RxMeta rx_meta{};
IncomingIdentity identity{};
std::array<char, kDeferredIncomingTextMaxLen + 1> text{};
uint16_t text_len = 0;
};
void clear();
[[nodiscard]] bool empty() const { return count == 0; }
[[nodiscard]] bool contains(const IncomingIdentity& identity) const;
bool push(const MeshIncomingText& incoming,
MeshProtocol protocol,
const IncomingIdentity& identity);
bool peek(MeshIncomingText& incoming, MeshProtocol& protocol) const;
void pop();
std::array<Slot, kDeferredIncomingDepth> slots{};
std::size_t head = 0;
std::size_t count = 0;
};
ChatModel& model_;
IMeshAdapter& adapter_;
IChatStore& store_;
@@ -280,6 +319,7 @@ class ChatService
MeshProtocol active_protocol_ = MeshProtocol::Meshtastic;
mutable ChatMessage store_lookup_cache_{};
RecentIncomingWindow recent_incoming_{};
DeferredIncomingQueue deferred_incoming_{};
std::vector<IncomingTextObserver*> incoming_text_observers_;
std::vector<IncomingMessageObserver*> incoming_message_observers_;
@@ -287,7 +327,15 @@ class ChatService
std::vector<IncomingDataObserver*> incoming_data_observers_;
[[nodiscard]] bool isDuplicateIncoming(const ChatMessage& msg) const;
[[nodiscard]] bool isDeferredIncoming(const ChatMessage& msg) const;
[[nodiscard]] static IncomingIdentity incomingIdentityForMessage(
const ChatMessage& msg);
void rememberIncoming(const ChatMessage& msg);
static ChatMessage incomingChatMessage(const MeshIncomingText& incoming,
MeshProtocol protocol);
void commitIncoming(const MeshIncomingText& incoming,
const ChatMessage& message);
void retryDeferredIncoming();
MeshSendResult sendTextResolvedDetailed(
ChannelId channel,
const std::string& text,
@@ -40,6 +40,9 @@ class ContactService
*/
void begin();
/** Selects the only protocol partition visible to contact queries. */
void setActiveProtocol(MeshProtocol protocol);
void applyNodeUpdate(uint32_t node_id, const NodeUpdate& update);
/**
@@ -159,6 +162,7 @@ class ContactService
private:
INodeStore& node_store_;
IContactStore& contact_store_;
MeshProtocol active_protocol_ = MeshProtocol::Meshtastic;
mutable std::vector<NodeInfo> cached_nodes_; // Cache for getContacts/getNearby
mutable uint32_t cache_timestamp_;
static constexpr uint32_t kCacheTimeoutMs = 1000; // 1 second cache
@@ -4,6 +4,9 @@
#include "chat/delivery/chat_delivery_send_result_projection.h"
#include "chat/delivery/chat_outbox_service.h"
#include <algorithm>
#include <cstdio>
namespace chat::delivery
{
@@ -18,23 +21,41 @@ void ChatMessageLedger::setDeliveryEventPort(
delivery_event_port_ = delivery_event_port;
}
void ChatMessageLedger::recordOutbound(const ChatMessage& message,
bool model_enabled,
SendFailureKind failure)
LedgerPersistence ChatMessageLedger::recordOutbound(
const ChatMessage& message,
bool model_enabled,
SendFailureKind failure)
{
if (model_enabled)
{
model_.onSendQueued(message);
if (message.status == MessageStatus::Failed && message.msg_id != 0)
{
model_.onSendResult(message.msg_id, false);
model_.updateMessageStatusForProtocol(message.msg_id,
message.protocol,
MessageStatus::Failed);
}
}
store_.append(message);
LedgerPersistence persistence = LedgerPersistence::Durable;
if (!store_.appendDurably(message))
{
persistence = enqueuePendingOutbound(message)
? LedgerPersistence::Deferred
: LedgerPersistence::Rejected;
}
if (ChatOutboxService::isOutboundStatusUpdate(message.status))
{
publishDeliveryEvent(message, message.status, 0, failure);
}
return persistence;
}
LedgerPersistence ChatMessageLedger::recordIncoming(
const ChatMessage& message)
{
return store_.appendIncomingDurably(message)
? LedgerPersistence::Durable
: LedgerPersistence::Deferred;
}
bool ChatMessageLedger::applyOutboundStatus(MessageId msg_id,
@@ -52,7 +73,10 @@ bool ChatMessageLedger::applyOutboundStatus(MessageId msg_id,
{
return false;
}
if (!writeStatus(msg_id, status, model_enabled))
if (!writeStatusForProtocol(msg_id,
current.protocol,
status,
model_enabled))
{
return false;
}
@@ -95,7 +119,10 @@ bool ChatMessageLedger::markRetryQueued(MessageId msg_id, bool model_enabled)
{
return false;
}
if (!writeStatus(msg_id, MessageStatus::Queued, model_enabled))
if (!writeStatusForProtocol(msg_id,
current.protocol,
MessageStatus::Queued,
model_enabled))
{
return false;
}
@@ -133,12 +160,24 @@ bool ChatMessageLedger::lookupMessage(MessageId msg_id, ChatMessage& out) const
{
return false;
}
if (const PendingOutboundWrite* pending = findPendingOutbound(msg_id))
{
out = pending->message;
applyPendingStatus(out);
return true;
}
if (const ChatMessage* message = model_.getMessage(msg_id))
{
out = *message;
applyPendingStatus(out);
return true;
}
return store_.getMessage(msg_id, &out);
if (!store_.getMessage(msg_id, &out))
{
return false;
}
applyPendingStatus(out);
return true;
}
bool ChatMessageLedger::lookupMessageForProtocol(MessageId msg_id,
@@ -149,29 +188,26 @@ bool ChatMessageLedger::lookupMessageForProtocol(MessageId msg_id,
{
return false;
}
if (const PendingOutboundWrite* pending =
findPendingOutboundForProtocol(msg_id, protocol))
{
out = pending->message;
applyPendingStatus(out);
return true;
}
if (const ChatMessage* message =
model_.getMessageForProtocol(msg_id, protocol))
{
out = *message;
applyPendingStatus(out);
return true;
}
return store_.getMessageForProtocol(msg_id, protocol, &out);
}
bool ChatMessageLedger::writeStatus(MessageId msg_id,
MessageStatus status,
bool model_enabled)
{
if (msg_id == 0)
if (!store_.getMessageForProtocol(msg_id, protocol, &out))
{
return false;
}
bool updated = false;
if (model_enabled)
{
updated = model_.updateMessageStatus(msg_id, status);
}
return store_.updateMessageStatus(msg_id, status) || updated;
applyPendingStatus(out);
return true;
}
bool ChatMessageLedger::writeStatusForProtocol(MessageId msg_id,
@@ -189,8 +225,503 @@ bool ChatMessageLedger::writeStatusForProtocol(MessageId msg_id,
updated =
model_.updateMessageStatusForProtocol(msg_id, protocol, status);
}
return store_.updateMessageStatusForProtocol(msg_id, protocol, status) ||
updated;
if (PendingOutboundWrite* pending =
findPendingOutboundForProtocol(msg_id, protocol))
{
pending->message.status = status;
return true;
}
if (store_.updateMessageStatusForProtocol(msg_id, protocol, status))
{
removePendingStatus(msg_id, protocol);
return true;
}
return enqueuePendingStatus(msg_id, protocol, status) || updated;
}
std::size_t ChatMessageLedger::flushPendingWrites(std::size_t budget)
{
std::size_t flushed = 0;
while (flushed < budget)
{
const int outbound_index = oldestPendingOutboundIndex();
if (outbound_index >= 0)
{
PendingOutboundWrite& pending =
pending_outbound_writes_[static_cast<std::size_t>(outbound_index)];
if (!store_.appendDurably(pending.message))
{
break;
}
removePendingStatus(pending.message.msg_id,
pending.message.protocol);
pending = PendingOutboundWrite{};
++flushed;
continue;
}
const int status_index = oldestPendingStatusIndex();
if (status_index < 0)
{
break;
}
PendingStatusWrite& pending =
pending_status_writes_[static_cast<std::size_t>(status_index)];
if (!store_.updateMessageStatusForProtocol(pending.msg_id,
pending.protocol,
pending.status))
{
break;
}
pending = PendingStatusWrite{};
++flushed;
}
return flushed;
}
std::vector<ChatMessage> ChatMessageLedger::loadPageFromLatest(
const ConversationId& conversation,
std::size_t offset_from_latest,
std::size_t limit,
std::size_t* total) const
{
std::array<const PendingOutboundWrite*, kPendingOutboundWriteDepth>
pending{};
std::size_t pending_count = 0;
for (const PendingOutboundWrite& entry : pending_outbound_writes_)
{
if (entry.used &&
conversationIdForMessage(entry.message) == conversation)
{
pending[pending_count++] = &entry;
}
}
std::sort(pending.begin(),
pending.begin() + static_cast<long>(pending_count),
[](const PendingOutboundWrite* lhs,
const PendingOutboundWrite* rhs)
{
return lhs->sequence < rhs->sequence;
});
std::size_t durable_total = 0;
std::vector<ChatMessage> result;
if (limit == 0)
{
(void)store_.loadPageFromLatest(conversation, 0, 1, &durable_total);
if (total)
{
*total = durable_total + pending_count;
}
return result;
}
if (offset_from_latest >= pending_count)
{
result = store_.loadPageFromLatest(conversation,
offset_from_latest - pending_count,
limit,
&durable_total);
}
else
{
const std::size_t pending_end = pending_count - offset_from_latest;
const std::size_t pending_start =
pending_end > limit ? pending_end - limit : 0;
const std::size_t pending_take = pending_end - pending_start;
const std::size_t durable_take = limit - pending_take;
if (durable_take > 0)
{
result = store_.loadPageFromLatest(conversation,
0,
durable_take,
&durable_total);
}
else
{
(void)store_.loadPageFromLatest(conversation,
0,
1,
&durable_total);
}
result.reserve(result.size() + pending_take);
for (std::size_t index = pending_start; index < pending_end; ++index)
{
result.push_back(pending[index]->message);
}
}
for (ChatMessage& message : result)
{
applyPendingStatus(message);
}
if (total)
{
*total = durable_total + pending_count;
}
return result;
}
std::vector<ChatMessage> ChatMessageLedger::loadRecent(
const ConversationId& conversation,
std::size_t limit) const
{
return loadPageFromLatest(conversation, 0, limit, nullptr);
}
std::vector<ConversationMeta> ChatMessageLedger::loadConversationPage(
MeshProtocol protocol,
std::size_t offset,
std::size_t limit,
std::size_t* total) const
{
protocol = protocol == MeshProtocol::RNode ? MeshProtocol::Reticulum
: protocol;
std::size_t durable_total = 0;
std::vector<ConversationMeta> conversations =
store_.loadConversationPageForProtocol(protocol,
0,
0,
&durable_total);
std::array<const PendingOutboundWrite*, kPendingOutboundWriteDepth>
pending{};
std::size_t pending_count = 0;
for (const PendingOutboundWrite& entry : pending_outbound_writes_)
{
const MeshProtocol entry_protocol =
entry.message.protocol == MeshProtocol::RNode
? MeshProtocol::Reticulum
: entry.message.protocol;
if (entry.used && entry_protocol == protocol)
{
pending[pending_count++] = &entry;
}
}
std::sort(pending.begin(),
pending.begin() + static_cast<long>(pending_count),
[](const PendingOutboundWrite* lhs,
const PendingOutboundWrite* rhs)
{
return lhs->sequence < rhs->sequence;
});
for (std::size_t pending_index = 0;
pending_index < pending_count;
++pending_index)
{
const ChatMessage& message = pending[pending_index]->message;
const ConversationId conversation = conversationIdForMessage(message);
auto existing = std::find_if(
conversations.begin(),
conversations.end(),
[&](const ConversationMeta& meta)
{
return meta.id == conversation;
});
if (existing == conversations.end())
{
ConversationMeta meta{};
meta.id = conversation;
if (conversation.peer == 0)
{
meta.name = "Broadcast";
}
else
{
char name[16]{};
std::snprintf(name,
sizeof(name),
"%04lX",
static_cast<unsigned long>(conversation.peer &
0xFFFFU));
meta.name = name;
}
conversations.push_back(meta);
existing = conversations.end() - 1;
}
existing->preview = message.text;
existing->last_timestamp = message.timestamp;
existing->reticulum_identity = message.reticulum_identity;
}
std::stable_sort(conversations.begin(),
conversations.end(),
[](const ConversationMeta& lhs,
const ConversationMeta& rhs)
{
return lhs.last_timestamp > rhs.last_timestamp;
});
if (total)
{
*total = conversations.size();
}
if (offset >= conversations.size())
{
return {};
}
const std::size_t end =
limit == 0
? conversations.size()
: std::min(conversations.size(), offset + limit);
return std::vector<ConversationMeta>(
conversations.begin() + static_cast<long>(offset),
conversations.begin() + static_cast<long>(end));
}
bool ChatMessageLedger::findMessage(MessageId msg_id, ChatMessage& out) const
{
return lookupMessage(msg_id, out);
}
bool ChatMessageLedger::findMessageForProtocol(MessageId msg_id,
MeshProtocol protocol,
ChatMessage& out) const
{
return lookupMessageForProtocol(msg_id, protocol, out);
}
void ChatMessageLedger::clearConversation(
const ConversationId& conversation)
{
for (PendingOutboundWrite& pending : pending_outbound_writes_)
{
if (!pending.used ||
!(conversationIdForMessage(pending.message) == conversation))
{
continue;
}
removePendingStatus(pending.message.msg_id,
pending.message.protocol);
pending = PendingOutboundWrite{};
}
for (PendingStatusWrite& pending : pending_status_writes_)
{
if (!pending.used)
{
continue;
}
ChatMessage message{};
if (lookupMessageForProtocol(pending.msg_id,
pending.protocol,
message) &&
conversationIdForMessage(message) == conversation)
{
pending = PendingStatusWrite{};
}
}
}
void ChatMessageLedger::clear()
{
for (PendingOutboundWrite& pending : pending_outbound_writes_)
{
pending = PendingOutboundWrite{};
}
for (PendingStatusWrite& pending : pending_status_writes_)
{
pending = PendingStatusWrite{};
}
next_pending_sequence_ = 1;
}
ChatMessageLedger::PendingOutboundWrite*
ChatMessageLedger::findPendingOutbound(MessageId msg_id)
{
if (msg_id == 0)
{
return nullptr;
}
for (PendingOutboundWrite& pending : pending_outbound_writes_)
{
if (pending.used && pending.message.msg_id == msg_id)
{
return &pending;
}
}
return nullptr;
}
ChatMessageLedger::PendingOutboundWrite*
ChatMessageLedger::findPendingOutboundForProtocol(MessageId msg_id,
MeshProtocol protocol)
{
if (msg_id == 0)
{
return nullptr;
}
for (PendingOutboundWrite& pending : pending_outbound_writes_)
{
if (pending.used && pending.message.msg_id == msg_id &&
pending.message.protocol == protocol)
{
return &pending;
}
}
return nullptr;
}
const ChatMessageLedger::PendingOutboundWrite*
ChatMessageLedger::findPendingOutbound(MessageId msg_id) const
{
if (msg_id == 0)
{
return nullptr;
}
for (const PendingOutboundWrite& pending : pending_outbound_writes_)
{
if (pending.used && pending.message.msg_id == msg_id)
{
return &pending;
}
}
return nullptr;
}
const ChatMessageLedger::PendingOutboundWrite*
ChatMessageLedger::findPendingOutboundForProtocol(
MessageId msg_id,
MeshProtocol protocol) const
{
if (msg_id == 0)
{
return nullptr;
}
for (const PendingOutboundWrite& pending : pending_outbound_writes_)
{
if (pending.used && pending.message.msg_id == msg_id &&
pending.message.protocol == protocol)
{
return &pending;
}
}
return nullptr;
}
bool ChatMessageLedger::enqueuePendingOutbound(const ChatMessage& message)
{
if (PendingOutboundWrite* existing =
findPendingOutboundForProtocol(message.msg_id,
message.protocol))
{
existing->message = message;
return true;
}
for (PendingOutboundWrite& pending : pending_outbound_writes_)
{
if (pending.used)
{
continue;
}
pending.used = true;
pending.sequence = nextPendingSequence();
pending.message = message;
return true;
}
return false;
}
bool ChatMessageLedger::enqueuePendingStatus(MessageId msg_id,
MeshProtocol protocol,
MessageStatus status)
{
for (PendingStatusWrite& pending : pending_status_writes_)
{
if (pending.used && pending.msg_id == msg_id &&
pending.protocol == protocol)
{
pending.status = status;
return true;
}
}
for (PendingStatusWrite& pending : pending_status_writes_)
{
if (pending.used)
{
continue;
}
pending.used = true;
pending.sequence = nextPendingSequence();
pending.msg_id = msg_id;
pending.protocol = protocol;
pending.status = status;
return true;
}
return false;
}
void ChatMessageLedger::removePendingStatus(MessageId msg_id,
MeshProtocol protocol)
{
for (PendingStatusWrite& pending : pending_status_writes_)
{
if (pending.used && pending.msg_id == msg_id &&
pending.protocol == protocol)
{
pending = PendingStatusWrite{};
}
}
}
void ChatMessageLedger::applyPendingStatus(ChatMessage& message) const
{
for (const PendingStatusWrite& pending : pending_status_writes_)
{
if (pending.used && pending.msg_id == message.msg_id &&
pending.protocol == message.protocol)
{
message.status = pending.status;
}
}
}
int ChatMessageLedger::oldestPendingOutboundIndex() const
{
int oldest = -1;
for (std::size_t index = 0; index < pending_outbound_writes_.size(); ++index)
{
const PendingOutboundWrite& pending = pending_outbound_writes_[index];
if (!pending.used ||
(oldest >= 0 &&
pending.sequence >=
pending_outbound_writes_[static_cast<std::size_t>(oldest)]
.sequence))
{
continue;
}
oldest = static_cast<int>(index);
}
return oldest;
}
int ChatMessageLedger::oldestPendingStatusIndex() const
{
int oldest = -1;
for (std::size_t index = 0; index < pending_status_writes_.size(); ++index)
{
const PendingStatusWrite& pending = pending_status_writes_[index];
if (!pending.used ||
(oldest >= 0 &&
pending.sequence >=
pending_status_writes_[static_cast<std::size_t>(oldest)]
.sequence))
{
continue;
}
oldest = static_cast<int>(index);
}
return oldest;
}
uint32_t ChatMessageLedger::nextPendingSequence()
{
const uint32_t sequence = next_pending_sequence_++;
if (next_pending_sequence_ == 0)
{
next_pending_sequence_ = 1;
}
return sequence;
}
void ChatMessageLedger::publishDeliveryEvent(const ChatMessage& message,
@@ -355,6 +355,11 @@ void mergeMeshtasticFacts(MeshtasticPeerFacts& dst,
const MeshtasticPeerFacts& src)
{
mergeNodeFacts(dst.node, src.node);
if (src.has_next_hop)
{
dst.has_next_hop = true;
dst.next_hop = src.next_hop;
}
if (src.has_public_key &&
meshPeerHasNonZeroBytes(src.public_key,
kMeshPeerMeshtasticPublicKeyLen))
@@ -364,6 +369,10 @@ void mergeMeshtasticFacts(MeshtasticPeerFacts& dst,
std::memcmp(dst.public_key,
src.public_key,
kMeshPeerMeshtasticPublicKeyLen) != 0;
if (changed && dst.has_public_key && dst.key_manually_verified)
{
return;
}
dst.has_public_key = true;
std::memcpy(dst.public_key,
src.public_key,
@@ -387,6 +396,10 @@ void mergeMeshCoreFacts(MeshCorePeerFacts& dst,
std::memcmp(dst.public_key,
src.public_key,
kMeshPeerMeshCorePublicKeyLen) != 0;
if (changed && dst.has_public_key && dst.public_key_verified)
{
return;
}
dst.has_public_key = true;
std::memcpy(dst.public_key,
src.public_key,
@@ -405,6 +418,10 @@ void mergeMeshCoreFacts(MeshCorePeerFacts& dst,
dst.has_next_hop = true;
dst.next_hop = src.next_hop;
}
if (src.node_id_hint != 0)
{
dst.node_id_hint = src.node_id_hint;
}
}
void mergeReticulumFacts(ReticulumPeerFacts& dst,
@@ -459,8 +476,8 @@ MeshPeerSource mergePeerSource(MeshPeerSource existing,
return incoming;
}
MeshPeerRecord mergePeerRecordFacts(const MeshPeerRecord& existing,
const MeshPeerRecord& incoming)
MeshPeerRecord mergePeerRecordFactsImpl(const MeshPeerRecord& existing,
const MeshPeerRecord& incoming)
{
MeshPeerRecord next = existing;
next.valid = existing.valid || incoming.valid;
@@ -483,7 +500,31 @@ MeshPeerRecord mergePeerRecordFacts(const MeshPeerRecord& existing,
copyNonEmptyMeshPeerText(next.display_name,
sizeof(next.display_name),
incoming.display_name);
copyNonEmptyMeshPeerText(next.user_alias,
sizeof(next.user_alias),
incoming.user_alias);
next.flags = existing.flags;
if (incoming.observations.has_snr)
{
next.observations.has_snr = true;
next.observations.snr = incoming.observations.snr;
}
if (incoming.observations.has_rssi)
{
next.observations.has_rssi = true;
next.observations.rssi = incoming.observations.rssi;
}
if (incoming.observations.has_device_metrics)
{
next.observations.has_device_metrics = true;
next.observations.device_metrics =
incoming.observations.device_metrics;
}
if (incoming.observations.has_position)
{
next.observations.has_position = true;
next.observations.position = incoming.observations.position;
}
mergeMeshtasticFacts(next.meshtastic, incoming.meshtastic);
mergeMeshCoreFacts(next.meshcore, incoming.meshcore);
mergeReticulumFacts(next.reticulum, incoming.reticulum);
@@ -504,6 +545,12 @@ NodeId reticulumNodeIdFromDestinationHash(const uint8_t* destination_hash)
} // namespace
MeshPeerRecord mergeMeshPeerRecordFacts(const MeshPeerRecord& existing,
const MeshPeerRecord& incoming)
{
return mergePeerRecordFactsImpl(existing, incoming);
}
MeshPeerDirectoryCore::MeshPeerDirectoryCore(
IMeshPeerDirectoryBlobStore& blob_store)
: blob_store_(blob_store)
@@ -566,7 +613,7 @@ MeshPeerDirectoryStatus MeshPeerDirectoryCore::record(
if (existing_index < records_.size())
{
const MeshPeerRecord& existing = records_[existing_index];
MeshPeerRecord next = mergePeerRecordFacts(existing, record);
MeshPeerRecord next = mergeMeshPeerRecordFacts(existing, record);
records_[existing_index] = next;
dirty_ = true;
maybeSave();
+293 -94
View File
@@ -38,6 +38,20 @@ uint32_t persistable_incoming_timestamp(const MeshIncomingText& incoming)
}
return persistable_now_timestamp();
}
const char* persistence_name(delivery::LedgerPersistence persistence)
{
switch (persistence)
{
case delivery::LedgerPersistence::Durable:
return "durable";
case delivery::LedgerPersistence::Deferred:
return "deferred";
case delivery::LedgerPersistence::Rejected:
return "rejected";
}
return "unknown";
}
} // namespace
#ifndef CHAT_SERVICE_LOG_ENABLE
@@ -51,7 +65,7 @@ uint32_t persistable_incoming_timestamp(const MeshIncomingText& incoming)
#endif
#ifndef CHAT_SERVICE_DIAG_LOG_ENABLE
#define CHAT_SERVICE_DIAG_LOG_ENABLE 1
#define CHAT_SERVICE_DIAG_LOG_ENABLE 0
#endif
#if CHAT_SERVICE_DIAG_LOG_ENABLE
@@ -60,6 +74,16 @@ uint32_t persistable_incoming_timestamp(const MeshIncomingText& incoming)
#define CHAT_SERVICE_DIAG_LOG(...)
#endif
#ifndef CHAT_SERVICE_EVENT_LOG_ENABLE
#define CHAT_SERVICE_EVENT_LOG_ENABLE 1
#endif
#if CHAT_SERVICE_EVENT_LOG_ENABLE
#define CHAT_SERVICE_EVENT_LOG(...) std::printf(__VA_ARGS__)
#else
#define CHAT_SERVICE_EVENT_LOG(...)
#endif
const char* failure_name(MeshOperationFailure failure)
{
switch (failure)
@@ -231,6 +255,110 @@ void ChatService::RecentIncomingWindow::remember(const IncomingIdentity& identit
}
}
void ChatService::DeferredIncomingQueue::clear()
{
for (Slot& slot : slots)
{
slot = Slot{};
}
head = 0;
count = 0;
}
bool ChatService::DeferredIncomingQueue::contains(
const IncomingIdentity& identity) const
{
for (std::size_t offset = 0; offset < count; ++offset)
{
const std::size_t index = (head + offset) % slots.size();
if (slots[index].identity == identity)
{
return true;
}
}
return false;
}
bool ChatService::DeferredIncomingQueue::push(
const MeshIncomingText& incoming,
MeshProtocol protocol,
const IncomingIdentity& identity)
{
if (count >= slots.size() ||
incoming.text.size() > kDeferredIncomingTextMaxLen)
{
return false;
}
const std::size_t index = (head + count) % slots.size();
Slot& slot = slots[index];
slot = Slot{};
slot.protocol = protocol;
slot.channel = incoming.channel;
slot.from = incoming.from;
slot.to = incoming.to;
slot.msg_id = incoming.msg_id;
slot.timestamp = incoming.timestamp;
slot.hop_limit = incoming.hop_limit;
slot.encrypted = incoming.encrypted;
slot.reticulum_identity = incoming.reticulum_identity;
slot.has_reticulum_lxmf_hash = incoming.has_reticulum_lxmf_hash;
std::memcpy(slot.reticulum_lxmf_hash,
incoming.reticulum_lxmf_hash,
sizeof(slot.reticulum_lxmf_hash));
slot.source_unverified = incoming.source_unverified;
slot.rx_meta = incoming.rx_meta;
slot.identity = identity;
slot.text_len = static_cast<uint16_t>(incoming.text.size());
if (slot.text_len > 0)
{
std::memcpy(slot.text.data(), incoming.text.data(), slot.text_len);
}
slot.text[slot.text_len] = '\0';
++count;
return true;
}
bool ChatService::DeferredIncomingQueue::peek(
MeshIncomingText& incoming,
MeshProtocol& protocol) const
{
if (count == 0)
{
return false;
}
const Slot& slot = slots[head];
protocol = slot.protocol;
incoming.channel = slot.channel;
incoming.from = slot.from;
incoming.to = slot.to;
incoming.msg_id = slot.msg_id;
incoming.timestamp = slot.timestamp;
incoming.hop_limit = slot.hop_limit;
incoming.encrypted = slot.encrypted;
incoming.reticulum_identity = slot.reticulum_identity;
incoming.has_reticulum_lxmf_hash = slot.has_reticulum_lxmf_hash;
std::memcpy(incoming.reticulum_lxmf_hash,
slot.reticulum_lxmf_hash,
sizeof(incoming.reticulum_lxmf_hash));
incoming.source_unverified = slot.source_unverified;
incoming.rx_meta = slot.rx_meta;
incoming.text.assign(slot.text.data(), slot.text_len);
return true;
}
void ChatService::DeferredIncomingQueue::pop()
{
if (count == 0)
{
return;
}
slots[head] = Slot{};
head = (head + 1) % slots.size();
--count;
}
MessageId ChatService::sendText(ChannelId channel, const std::string& text, NodeId peer)
{
return sendTextWithId(channel, text, 0, peer);
@@ -354,17 +482,20 @@ MeshSendResult ChatService::sendTextResolvedDetailed(
msg.reticulum_identity = result.reticulum_identity;
msg.status = result.ok ? MessageStatus::Queued : MessageStatus::Failed;
message_ledger_.recordOutbound(
msg,
model_enabled_,
result.ok ? delivery::SendFailureKind::None
: delivery_failure_from_mesh_operation(result.failure));
CHAT_SERVICE_DIAG_LOG("[ChatService][TX] stored msg=%lu status=%u peer=%08lX dest=%s text=\"%s\"\n",
static_cast<unsigned long>(msg.msg_id),
static_cast<unsigned>(msg.status),
static_cast<unsigned long>(msg.peer),
dest_hash,
text_preview);
const delivery::LedgerPersistence persistence =
message_ledger_.recordOutbound(
msg,
model_enabled_,
result.ok ? delivery::SendFailureKind::None
: delivery_failure_from_mesh_operation(result.failure));
if (persistence != delivery::LedgerPersistence::Durable)
{
CHAT_SERVICE_EVENT_LOG("[ChatService][TX] ledger msg=%lu persistence=%s status=%u protocol=%s\n",
static_cast<unsigned long>(msg.msg_id),
persistence_name(persistence),
static_cast<unsigned>(msg.status),
protocol_name(msg.protocol));
}
if (result.ok && result.msg_id != 0)
{
@@ -537,7 +668,7 @@ bool ChatService::resendFailedForProtocol(MessageId msg_id,
std::vector<ChatMessage> ChatService::getRecentMessages(const ConversationId& conv, size_t limit) const
{
return store_.loadRecent(conv, limit);
return message_ledger_.loadRecent(conv, limit);
}
std::vector<ChatMessage> ChatService::getMessagePageFromLatest(
@@ -546,20 +677,29 @@ std::vector<ChatMessage> ChatService::getMessagePageFromLatest(
size_t limit,
size_t* total) const
{
return store_.loadPageFromLatest(conv, offset_from_latest, limit, total);
return message_ledger_.loadPageFromLatest(conv,
offset_from_latest,
limit,
total);
}
std::vector<ConversationMeta> ChatService::getConversations(size_t offset,
size_t limit,
size_t* total) const
{
return store_.loadConversationPage(offset, limit, total);
return message_ledger_.loadConversationPage(active_protocol_,
offset,
limit,
total);
}
int ChatService::getTotalUnread() const
{
size_t total = 0;
auto convs = store_.loadConversationPage(0, 0, &total);
auto convs = store_.loadConversationPageForProtocol(active_protocol_,
0,
0,
&total);
int sum = 0;
for (const auto& conv : convs)
{
@@ -570,6 +710,8 @@ int ChatService::getTotalUnread() const
void ChatService::clearAllMessages()
{
message_ledger_.clear();
deferred_incoming_.clear();
model_.clearAll();
store_.clearAll();
recent_incoming_.clear();
@@ -577,6 +719,8 @@ void ChatService::clearAllMessages()
void ChatService::clearConversation(const ConversationId& conv)
{
message_ledger_.clearConversation(conv);
deferred_incoming_.clear();
model_.clearConversation(conv);
store_.clearConversation(conv);
recent_incoming_.clear();
@@ -589,25 +733,19 @@ bool ChatService::markConversationRead(const ConversationId& conv)
void ChatService::processIncoming()
{
(void)message_ledger_.flushPendingWrites(1);
retryDeferredIncoming();
static constexpr std::size_t kIncomingDrainBudget = 4;
std::size_t drained = 0;
MeshIncomingText incoming_text;
while (adapter_.pollIncomingText(&incoming_text))
while (drained < kIncomingDrainBudget &&
adapter_.pollIncomingText(&incoming_text))
{
ChatMessage msg;
msg.protocol = active_protocol_;
msg.channel = incoming_text.channel;
msg.from = incoming_text.from;
msg.peer = normalize_conversation_peer(incoming_text.to) == 0 ? 0 : incoming_text.from;
msg.msg_id = incoming_text.msg_id;
msg.timestamp = persistable_incoming_timestamp(incoming_text);
msg.text = incoming_text.text;
msg.reticulum_identity = incoming_text.reticulum_identity;
msg.has_reticulum_lxmf_hash = incoming_text.has_reticulum_lxmf_hash;
std::memcpy(msg.reticulum_lxmf_hash,
incoming_text.reticulum_lxmf_hash,
sizeof(msg.reticulum_lxmf_hash));
msg.source_unverified = incoming_text.source_unverified;
msg.rx_origin = incoming_text.rx_meta.origin;
msg.status = MessageStatus::Incoming;
++drained;
const MeshProtocol incoming_protocol = active_protocol_;
ChatMessage msg = incomingChatMessage(incoming_text,
incoming_protocol);
const ReticulumPeerIdentity* incoming_identity =
hasReticulumDestinationIdentity(incoming_text.reticulum_identity)
@@ -618,7 +756,7 @@ void ChatService::processIncoming()
incoming_dest_hash,
sizeof(incoming_dest_hash));
CHAT_SERVICE_DIAG_LOG("[ChatService][RX] text protocol=%s msg=%lu from=%08lX to=%08lX peer=%08lX dest=%s len=%u encrypted=%u unverified=%u lxmf_hash=%u\n",
protocol_name(active_protocol_),
protocol_name(incoming_protocol),
static_cast<unsigned long>(msg.msg_id),
static_cast<unsigned long>(msg.from),
static_cast<unsigned long>(incoming_text.to),
@@ -653,48 +791,43 @@ void ChatService::processIncoming()
continue;
}
if (!store_.appendIncomingDurably(msg))
if (isDeferredIncoming(msg))
{
CHAT_SERVICE_DIAG_LOG("[ChatService][RX] durable append failed protocol=%s msg=%lu lxmf_hash=%u\n",
protocol_name(active_protocol_),
static_cast<unsigned long>(msg.msg_id),
chat::hasReticulumLxmfMessageHash(msg) ? 1U : 0U);
if (IIncomingDeliveryCommitPort* commit_port =
adapter_.incomingDeliveryCommitPort())
{
commit_port->commitIncomingText(incoming_text, false);
}
CHAT_SERVICE_DIAG_LOG("[ChatService][RX] pending duplicate held protocol=%s msg=%lu\n",
protocol_name(incoming_protocol),
static_cast<unsigned long>(msg.msg_id));
continue;
}
rememberIncoming(msg);
if (model_enabled_)
if (message_ledger_.recordIncoming(msg) !=
delivery::LedgerPersistence::Durable)
{
model_.onIncoming(msg);
}
if (IIncomingDeliveryCommitPort* commit_port =
adapter_.incomingDeliveryCommitPort())
{
commit_port->commitIncomingText(incoming_text, true);
}
for (auto* observer : incoming_message_observers_)
{
if (observer)
const IncomingIdentity identity = incomingIdentityForMessage(msg);
if (deferred_incoming_.push(incoming_text,
incoming_protocol,
identity))
{
observer->onIncomingMessage(msg, &incoming_text.rx_meta);
CHAT_SERVICE_EVENT_LOG("[ChatService][RX] deferred protocol=%s msg=%lu depth=%u lxmf_hash=%u\n",
protocol_name(incoming_protocol),
static_cast<unsigned long>(msg.msg_id),
static_cast<unsigned>(deferred_incoming_.count),
chat::hasReticulumLxmfMessageHash(msg) ? 1U : 0U);
}
}
for (auto* observer : incoming_text_observers_)
{
if (observer)
else
{
observer->onIncomingText(incoming_text);
CHAT_SERVICE_EVENT_LOG("[ChatService][RX] rejected reason=deferred_full protocol=%s msg=%lu depth=%u\n",
protocol_name(incoming_protocol),
static_cast<unsigned long>(msg.msg_id),
static_cast<unsigned>(deferred_incoming_.count));
if (IIncomingDeliveryCommitPort* commit_port =
adapter_.incomingDeliveryCommitPort())
{
commit_port->commitIncomingText(incoming_text, false);
}
}
continue;
}
commitIncoming(incoming_text, msg);
}
if (incoming_data_observers_.empty())
@@ -741,6 +874,21 @@ bool ChatService::isDuplicateIncoming(const ChatMessage& msg) const
return false;
}
return recent_incoming_.contains(incomingIdentityForMessage(msg));
}
bool ChatService::isDeferredIncoming(const ChatMessage& msg) const
{
if (msg.msg_id == 0 && !chat::hasReticulumLxmfMessageHash(msg))
{
return false;
}
return deferred_incoming_.contains(incomingIdentityForMessage(msg));
}
ChatService::IncomingIdentity ChatService::incomingIdentityForMessage(
const ChatMessage& msg)
{
IncomingIdentity identity{};
identity.protocol = msg.protocol;
identity.channel = msg.channel;
@@ -761,7 +909,7 @@ bool ChatService::isDuplicateIncoming(const ChatMessage& msg) const
identity.reticulum_destination_hash,
msg.reticulum_identity);
}
return recent_incoming_.contains(identity);
return identity;
}
void ChatService::rememberIncoming(const ChatMessage& msg)
@@ -772,27 +920,84 @@ void ChatService::rememberIncoming(const ChatMessage& msg)
return;
}
IncomingIdentity identity{};
identity.protocol = msg.protocol;
identity.channel = msg.channel;
identity.from = msg.from;
identity.peer = msg.peer;
identity.msg_id = msg.msg_id;
if (chat::hasReticulumLxmfMessageHash(msg))
recent_incoming_.remember(incomingIdentityForMessage(msg));
}
ChatMessage ChatService::incomingChatMessage(
const MeshIncomingText& incoming,
MeshProtocol protocol)
{
ChatMessage message;
message.protocol = protocol;
message.channel = incoming.channel;
message.from = incoming.from;
message.peer = normalize_conversation_peer(incoming.to) == 0
? 0
: incoming.from;
message.msg_id = incoming.msg_id;
message.timestamp = persistable_incoming_timestamp(incoming);
message.text = incoming.text;
message.reticulum_identity = incoming.reticulum_identity;
message.has_reticulum_lxmf_hash = incoming.has_reticulum_lxmf_hash;
std::memcpy(message.reticulum_lxmf_hash,
incoming.reticulum_lxmf_hash,
sizeof(message.reticulum_lxmf_hash));
message.source_unverified = incoming.source_unverified;
message.rx_origin = incoming.rx_meta.origin;
message.status = MessageStatus::Incoming;
return message;
}
void ChatService::commitIncoming(const MeshIncomingText& incoming,
const ChatMessage& message)
{
rememberIncoming(message);
if (model_enabled_)
{
identity.has_reticulum_lxmf_hash = true;
std::memcpy(identity.reticulum_lxmf_hash,
msg.reticulum_lxmf_hash,
sizeof(identity.reticulum_lxmf_hash));
model_.onIncoming(message);
}
if (msg.protocol == MeshProtocol::Reticulum &&
hasReticulumDestinationIdentity(msg.reticulum_identity))
if (IIncomingDeliveryCommitPort* commit_port =
adapter_.incomingDeliveryCommitPort())
{
identity.has_reticulum_destination = copyReticulumDestinationHash(
identity.reticulum_destination_hash,
msg.reticulum_identity);
commit_port->commitIncomingText(incoming, true);
}
recent_incoming_.remember(identity);
for (auto* observer : incoming_message_observers_)
{
if (observer)
{
observer->onIncomingMessage(message, &incoming.rx_meta);
}
}
for (auto* observer : incoming_text_observers_)
{
if (observer)
{
observer->onIncomingText(incoming);
}
}
}
void ChatService::retryDeferredIncoming()
{
MeshIncomingText incoming{};
MeshProtocol protocol = MeshProtocol::Meshtastic;
if (!deferred_incoming_.peek(incoming, protocol))
{
return;
}
const ChatMessage message = incomingChatMessage(incoming, protocol);
if (message_ledger_.recordIncoming(message) !=
delivery::LedgerPersistence::Durable)
{
return;
}
deferred_incoming_.pop();
CHAT_SERVICE_EVENT_LOG("[ChatService][RX] deferred commit protocol=%s msg=%lu remaining=%u\n",
protocol_name(protocol),
static_cast<unsigned long>(message.msg_id),
static_cast<unsigned>(deferred_incoming_.count));
commitIncoming(incoming, message);
}
void ChatService::flushStore()
@@ -929,11 +1134,7 @@ void ChatService::setDeliveryEventPort(
const ChatMessage* ChatService::getMessage(MessageId msg_id) const
{
if (const ChatMessage* msg = model_.getMessage(msg_id))
{
return msg;
}
if (store_.getMessage(msg_id, &store_lookup_cache_))
if (message_ledger_.findMessage(msg_id, store_lookup_cache_))
{
return &store_lookup_cache_;
}
@@ -944,11 +1145,9 @@ const ChatMessage* ChatService::getMessageForProtocol(
MessageId msg_id,
MeshProtocol protocol) const
{
if (const ChatMessage* msg = model_.getMessageForProtocol(msg_id, protocol))
{
return msg;
}
if (store_.getMessageForProtocol(msg_id, protocol, &store_lookup_cache_))
if (message_ledger_.findMessageForProtocol(msg_id,
protocol,
store_lookup_cache_))
{
return &store_lookup_cache_;
}
@@ -92,6 +92,8 @@ ContactService::ContactService(INodeStore& node_store, IContactStore& contact_st
void ContactService::begin()
{
node_store_.setActiveProtocol(active_protocol_);
contact_store_.setActiveProtocol(active_protocol_);
node_store_.begin();
contact_store_.begin();
@@ -104,6 +106,16 @@ void ContactService::begin()
invalidateCache();
}
void ContactService::setActiveProtocol(MeshProtocol protocol)
{
active_protocol_ = protocol == MeshProtocol::RNode
? MeshProtocol::Reticulum
: protocol;
node_store_.setActiveProtocol(active_protocol_);
contact_store_.setActiveProtocol(active_protocol_);
invalidateCache();
}
void ContactService::applyNodeUpdate(uint32_t node_id, const NodeUpdate& update)
{
node_store_.applyUpdate(node_id, update);
@@ -435,6 +447,14 @@ void ContactService::buildCache() const
const auto& node_entries = node_store_.getEntries();
for (const auto& entry : node_entries)
{
const MeshProtocol entry_protocol =
entry.protocol == static_cast<uint8_t>(MeshProtocol::RNode)
? MeshProtocol::Reticulum
: static_cast<MeshProtocol>(entry.protocol);
if (entry_protocol != active_protocol_)
{
continue;
}
if (!isNodeVisible(entry.last_seen))
{
continue;
@@ -21,6 +21,38 @@ class RecordingDeliveryEventPort final
int count = 0;
};
class TransientStore final : public chat::RamStore
{
public:
bool appendDurably(const chat::ChatMessage& message) override
{
if (append_failures > 0)
{
--append_failures;
return false;
}
append(message);
return true;
}
bool updateMessageStatusForProtocol(chat::MessageId msg_id,
chat::MeshProtocol protocol,
chat::MessageStatus status) override
{
if (status_failures > 0)
{
--status_failures;
return false;
}
return RamStore::updateMessageStatusForProtocol(msg_id,
protocol,
status);
}
int append_failures = 0;
int status_failures = 0;
};
chat::ChatMessage outgoing(chat::MessageId id, chat::MessageStatus status)
{
chat::ChatMessage message;
@@ -202,5 +234,81 @@ int main()
&stored));
assert(stored.status == chat::MessageStatus::Delivered);
chat::ChatModel deferred_model;
TransientStore deferred_store;
deferred_store.append_failures = 1;
chat::delivery::ChatMessageLedger deferred_ledger(deferred_model,
deferred_store);
assert(deferred_ledger.recordOutbound(
outgoing(600, chat::MessageStatus::Queued),
true) == chat::delivery::LedgerPersistence::Deferred);
assert(!deferred_store.getMessage(600, &stored));
std::size_t deferred_total = 0;
const chat::ConversationId deferred_conversation(
chat::ChannelId::PRIMARY,
0xAABBCCDD,
chat::MeshProtocol::Meshtastic);
auto deferred_page = deferred_ledger.loadPageFromLatest(
deferred_conversation,
0,
10,
&deferred_total);
assert(deferred_total == 1);
assert(deferred_page.size() == 1);
assert(deferred_page.front().msg_id == 600);
auto deferred_conversations = deferred_ledger.loadConversationPage(
chat::MeshProtocol::Meshtastic,
0,
10,
&deferred_total);
assert(deferred_total == 1);
assert(deferred_conversations.size() == 1);
assert(deferred_conversations.front().id == deferred_conversation);
assert(deferred_conversations.front().preview == "ledger");
assert(deferred_ledger.applyOutboundStatusForProtocol(
600,
chat::MeshProtocol::Meshtastic,
chat::MessageStatus::Delivered,
true));
assert(deferred_ledger.flushPendingWrites(1) == 1);
assert(deferred_store.getMessageForProtocol(
600,
chat::MeshProtocol::Meshtastic,
&stored));
assert(stored.status == chat::MessageStatus::Delivered);
deferred_page = deferred_ledger.loadPageFromLatest(
deferred_conversation,
0,
10,
&deferred_total);
assert(deferred_total == 1);
assert(deferred_page.size() == 1);
deferred_store.status_failures = 1;
deferred_ledger.recordOutbound(
outgoing(601, chat::MessageStatus::Queued),
true);
assert(deferred_ledger.applyOutboundStatusForProtocol(
601,
chat::MeshProtocol::Meshtastic,
chat::MessageStatus::Delivered,
true));
assert(deferred_store.getMessageForProtocol(
601,
chat::MeshProtocol::Meshtastic,
&stored));
assert(stored.status == chat::MessageStatus::Queued);
assert(deferred_ledger.findMessageForProtocol(
601,
chat::MeshProtocol::Meshtastic,
stored));
assert(stored.status == chat::MessageStatus::Delivered);
assert(deferred_ledger.flushPendingWrites(1) == 1);
assert(deferred_store.getMessageForProtocol(
601,
chat::MeshProtocol::Meshtastic,
&stored));
assert(stored.status == chat::MessageStatus::Delivered);
return 0;
}
@@ -162,6 +162,23 @@ class CountingIncomingObserver final : public chat::ChatService::IncomingMessage
chat::MessageId last_msg_id = 0;
};
class TransientIncomingStore final : public chat::RamStore
{
public:
bool appendIncomingDurably(const chat::ChatMessage& message) override
{
if (failures > 0)
{
--failures;
return false;
}
append(message);
return true;
}
int failures = 0;
};
const chat::ChatMessage* onlyMessage(chat::ChatService& service,
const chat::ConversationId& conv)
{
@@ -540,7 +557,10 @@ int main()
{
incoming_mesh.pushIncoming(0x1234ABCDU, 0x1000U + i, "window fill");
}
incoming_service.processIncoming();
for (std::size_t tick = 0; tick < 64; ++tick)
{
incoming_service.processIncoming();
}
assert(incoming_observer.count == 257);
incoming_mesh.pushIncoming(0x1234ABCDU, 0x10FFU, "recent duplicate");
@@ -550,6 +570,34 @@ int main()
assert(incoming_observer.last_msg_id == 0x42U);
}
{
chat::ChatModel incoming_model;
FakeMeshAdapter incoming_mesh;
TransientIncomingStore incoming_store;
incoming_store.failures = 1;
chat::ChatService incoming_service(incoming_model,
incoming_mesh,
incoming_store);
CountingIncomingObserver incoming_observer;
incoming_service.addIncomingMessageObserver(&incoming_observer);
const chat::ConversationId broadcast(chat::ChannelId::PRIMARY,
0,
chat::MeshProtocol::Meshtastic);
incoming_mesh.pushIncoming(0x1234ABCDU, 0x77U, "deferred");
incoming_service.processIncoming();
assert(incoming_store.loadRecent(broadcast, 10).empty());
assert(incoming_observer.count == 0);
incoming_mesh.pushIncoming(0x1234ABCDU, 0x77U, "deferred");
incoming_service.processIncoming();
const auto committed = incoming_store.loadRecent(broadcast, 10);
assert(committed.size() == 1);
assert(committed.front().msg_id == 0x77U);
assert(incoming_store.getUnread(broadcast) == 1);
assert(incoming_observer.count == 1);
}
{
chat::ChatModel incoming_model;
FakeMeshAdapter incoming_mesh;
@@ -58,65 +58,125 @@ int main(int argc, char** argv)
const std::string source = readFile(
repo_root /
"platform/esp/arduino_common/src/chat/infra/store/sd_store.cpp");
const std::string codec = readFile(
repo_root /
"platform/esp/arduino_common/src/chat/infra/store/protocol_chat_codec.cpp");
const std::string journal = readFile(
repo_root /
"platform/esp/arduino_common/src/chat/infra/store/fixed_slot_journal.cpp");
const std::string peer_header = readFile(
repo_root /
"platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_protocol_peer_repository.h");
const std::string peer_source = readFile(
repo_root /
"platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp");
const std::string peer_codec = readFile(
repo_root /
"platform/esp/arduino_common/src/chat/infra/store/protocol_peer_codec.cpp");
const std::string bindings = readFile(
repo_root /
"platform/esp/arduino_common/src/app_context_platform_bindings.cpp");
assert(contains(header, "kReadStateFile = \"/chat/read_state.bin\""));
assert(contains(header, "struct ReadStateEntry"));
assert(contains(header, "kReadStateMagic"));
assert(contains(header, "readStateUnreadOrLegacy"));
assert(contains(header, "writeReadStateUnread"));
assert(contains(header, "removeReadStateEntry"));
assert(contains(header, "kRoot = \"/data/v2\""));
assert(contains(header, "kMeshtasticRoot = \"/data/v2/mt/chat\""));
assert(contains(header, "kMeshCoreRoot = \"/data/v2/mc/chat\""));
assert(contains(header, "kReticulumRoot = \"/data/v2/rt/chat\""));
assert(contains(header, "PsramAllocator"));
assert(contains(header, "mutable ScratchBuffer scratch_"));
assert(!contains(header, "mutable uint8_t scratch_"));
assert(!contains(header, "RecordV2"));
assert(!contains(header, "RecordV3"));
assert(!contains(header, "RecordV4"));
assert(!contains(header, "kReadStateFile"));
assert(!contains(source, "\"/chat/"));
assert(!contains(source, "Legacy"));
const std::string ctor_body =
bodyBetween(source, "SdStore::SdStore()", "void SdStore::append");
assert(contains(ctor_body, "reconcileIndexUnread(entries)"));
assert(positionOf(ctor_body, "reconcileIndexUnread(entries)") <
positionOf(ctor_body, "writeIndex(entries)"));
assert(contains(codec, "kMeshtasticTextMax"));
assert(contains(codec, "kMeshCoreTextMax"));
assert(contains(codec, "kReticulumTextMax"));
assert(contains(codec, "struct MeshtasticMessageSlot"));
assert(contains(codec, "struct MeshCoreMessageSlot"));
assert(contains(codec, "struct ReticulumMessageSlot"));
assert(contains(journal, "kStorageSchemaVersion"));
assert(contains(journal, "PartialTail"));
assert(contains(journal, "replaceFileAtomically"));
assert(contains(journal, "recoverAtomicFile"));
const std::string append_body = bodyBetween(
source, "bool SdStore::appendInternal", "std::vector<ChatMessage> SdStore::loadRecent");
assert(positionOf(append_body, "already_committed") <
positionOf(append_body, "readStateUnreadOrLegacy(conv, &committed_unread)"));
assert(positionOf(append_body, "readStateUnreadOrLegacy(conv, &committed_unread)") <
positionOf(append_body, "updateIndexForMessage(msg, committed_unread)"));
assert(positionOf(append_body, "writeReadStateUnread(conv, unread)") <
positionOf(append_body, "updateIndexForMessage(msg, unread)"));
assert(positionOf(append_body, "CHAT_STORE_LOG(\"[AppContext] chat unread persist failed stage=read_state") <
positionOf(append_body, "removeReadStateEntry(conv)"));
source,
"bool SdStore::appendInternal",
"std::vector<ChatMessage> SdStore::loadRecent");
assert(positionOf(append_body, "appendMessageRecord(message, sequence)") <
positionOf(append_body, "rememberReticulumHash"));
assert(positionOf(append_body, "rememberReticulumHash") <
positionOf(append_body, "appendCatalogProjection"));
assert(contains(append_body, "projection_dirty_"));
assert(contains(append_body, "authoritative=1"));
assert(contains(append_body, "return true;"));
const std::string set_unread_body =
const std::string unread_body =
bodyBetween(source, "bool SdStore::setUnread", "int SdStore::getUnread");
assert(positionOf(set_unread_body, "writeReadStateUnread(conv, unread_count)") <
positionOf(set_unread_body, "writeConversationUnread(conv, unread_count)"));
assert(positionOf(set_unread_body, "writeConversationUnread(conv, unread_count)") <
positionOf(set_unread_body, "entries[index].unread = unread_count"));
assert(positionOf(set_unread_body, "entries[index].unread = unread_count") <
positionOf(set_unread_body, "writeIndex(entries)"));
assert(positionOf(unread_body, "appendReadProjection(projection)") <
positionOf(unread_body, "appendCatalogProjection(*catalog)"));
assert(contains(unread_body, "last_read_sequence"));
const std::string get_unread_body =
bodyBetween(source, "int SdStore::getUnread", "void SdStore::clearConversation");
assert(contains(get_unread_body, "readStateUnreadOrLegacy(conv, &unread)"));
const std::string conversation_query = bodyBetween(
source,
"SdStore::loadConversationPageForProtocol",
"bool SdStore::setUnread");
assert(contains(conversation_query, "sameProtocol"));
const std::string clear_conversation_body =
bodyBetween(source, "void SdStore::clearConversation", "void SdStore::clearAll");
assert(contains(clear_conversation_body, "removeReadStateEntry(conv)"));
const std::string clear_all = bodyBetween(
source,
"void SdStore::clearAll",
"bool SdStore::updateMessageStatus");
assert(contains(clear_all, "removeTree(protocolRoot(protocol))"));
assert(!contains(clear_all, "RecordV"));
const std::string clear_all_body =
bodyBetween(source, "void SdStore::clearAll", "bool SdStore::updateMessageStatus");
assert(contains(clear_all_body, "sd_remove(kReadStateFile)"));
assert(contains(clear_all_body, "sd_remove(kTempReadStateFile)"));
assert(contains(clear_all_body, "sd_remove(kBackupReadStateFile)"));
assert(contains(peer_header, "IProtocolPeerRepository"));
assert(contains(peer_header, "PsramAllocator<MeshPeerRecord>"));
assert(contains(peer_source, "\"peers.snapshot\""));
assert(contains(peer_source, "\"peers.delta\""));
assert(contains(peer_source, "\"contacts.snapshot\""));
assert(contains(peer_source, "\"contacts.delta\""));
assert(contains(peer_source, "kEphemeralPeerCapacity = 2048U"));
assert(contains(peer_source, "kProtectedContactCapacity = 4096U"));
assert(contains(peer_source, "peerIsProtected"));
assert(contains(peer_source, "peerReferencedByConversation"));
assert(contains(peer_source, "replaceFileAtomically"));
assert(contains(peer_codec, "validPeerIdentityForProtocol"));
assert(contains(peer_codec, "validContactIdentityForProtocol"));
const std::string peer_prefix = bodyBetween(
peer_codec,
"struct PeerPrefix",
"struct NodeFactsSlot");
assert(!contains(peer_prefix, "user_alias"));
assert(!contains(bindings, "EspSdMeshPeerDirectoryBlobStore"));
assert(!contains(bindings, "chat::meshtastic::NodeStore"));
assert(!contains(bindings, "chat::contacts::ContactStore"));
assert(!contains(bindings, "\"/nodes.bin\""));
assert(!contains(bindings, "\"/contacts.dat\""));
assert(!contains(bindings, "\"/mesh/peers.bin\""));
assert(!std::filesystem::exists(
repo_root /
"platform/esp/arduino_common/src/chat/infra/meshtastic/node_store.cpp"));
assert(!std::filesystem::exists(
repo_root /
"platform/esp/arduino_common/src/chat/infra/contact_store.cpp"));
const std::string reconcile_body =
bodyBetween(source, "bool SdStore::reconcileIndexUnread", "bool SdStore::readIndex");
assert(positionOf(reconcile_body, "readStateUnreadOrLegacy(conv, &durable_unread)") <
positionOf(reconcile_body, "durable_unread = entry.unread"));
assert(positionOf(reconcile_body, "durable_unread = entry.unread") <
positionOf(reconcile_body, "writeReadStateUnread(conv, durable_unread)"));
const std::string rebuild_body =
bodyBetween(source, "void SdStore::rebuildIndex", "bool SdStore::loadFileHeader");
assert(contains(rebuild_body, "readStateUnreadOrLegacy(conv, &ledger_unread)"));
assert(contains(rebuild_body, "writeReadStateUnread(conv, unread)"));
const std::string seen_load = bodyBetween(
source,
"bool SdStore::loadSeenJournal",
"bool SdStore::reconcileProtocolCatalog");
assert(contains(seen_load, "rebuildSeenJournalFromMessages"));
assert(contains(seen_load, "authoritative=messages"));
const std::string flush_body = bodyBetween(
source,
"void SdStore::flush",
"bool SdStore::ensureLayout");
assert(contains(flush_body, "projection_dirty_"));
assert(contains(flush_body, "kProjectionRetryIntervalMs"));
assert(contains(flush_body, "BackgroundWorkerBounded"));
return 0;
}
@@ -811,6 +811,40 @@ void core_merges_sparse_meshtastic_records_without_losing_key_or_node_facts()
}
}
void verified_keys_cannot_be_replaced_by_runtime_observations()
{
const uint32_t node_id = 0x12345678U;
chat::MeshPeerRecord verified =
makeMeshtasticKeyOnlyPeer(node_id, 0x20U, 10U);
verified.meshtastic.key_manually_verified = true;
chat::MeshPeerRecord observed =
makeMeshtasticKeyOnlyPeer(node_id, 0x80U, 20U);
observed.meshtastic.key_manually_verified = false;
chat::copyMeshPeerText(observed.display_name,
sizeof(observed.display_name),
"fresh name");
const chat::MeshPeerRecord merged =
chat::mergeMeshPeerRecordFacts(verified, observed);
assert(merged.meshtastic.key_manually_verified);
assert(std::memcmp(merged.meshtastic.public_key,
verified.meshtastic.public_key,
chat::kMeshPeerMeshtasticPublicKeyLen) == 0);
assert(std::strcmp(merged.display_name, "fresh name") == 0);
chat::MeshPeerRecord meshcore = makeMeshCorePeer(0x11U, "trusted", 10U);
chat::MeshPeerRecord conflicting = meshcore;
conflicting.meshcore.public_key_verified = false;
conflicting.meshcore.public_key[0] ^= 0x7FU;
const chat::MeshPeerRecord meshcore_merged =
chat::mergeMeshPeerRecordFacts(meshcore, conflicting);
assert(meshcore_merged.meshcore.public_key_verified);
assert(std::memcmp(meshcore_merged.meshcore.public_key,
meshcore.meshcore.public_key,
chat::kMeshPeerMeshCorePublicKeyLen) == 0);
}
void remove_and_clear_protocol_are_directory_behaviors()
{
MemoryMeshPeerDirectory directory;
@@ -842,6 +876,7 @@ int main()
find_by_node_id_preserves_reticulum_destination_identity();
core_persists_records_and_preserves_user_flags();
core_merges_sparse_meshtastic_records_without_losing_key_or_node_facts();
verified_keys_cannot_be_replaced_by_runtime_observations();
remove_and_clear_protocol_are_directory_behaviors();
return 0;
}
@@ -0,0 +1,104 @@
#include "platform/esp/arduino_common/chat/infra/store/protocol_chat_codec.h"
#include <cassert>
#include <cstring>
#include <string>
#include <vector>
namespace storage = chat::storage::v2;
namespace
{
chat::ChatMessage messageFor(chat::MeshProtocol protocol,
std::size_t text_length)
{
chat::ChatMessage message{};
message.protocol = protocol;
message.channel = chat::ChannelId::SECONDARY;
message.from = 0x11223344U;
message.peer = 0x55667788U;
message.msg_id = 0xAABBCCDDU;
message.timestamp = 123456U;
message.text.assign(text_length, 'x');
message.has_geo = true;
message.geo_lat_e7 = 123;
message.geo_lon_e7 = -456;
message.source_unverified = true;
message.rx_origin = chat::RxOrigin::WiFi;
message.status = chat::MessageStatus::Incoming;
return message;
}
void roundTrip(chat::MeshProtocol protocol, std::size_t text_length)
{
chat::ChatMessage input = messageFor(protocol, text_length);
if (protocol == chat::MeshProtocol::Reticulum)
{
uint8_t destination[chat::kReticulumPeerHashSize]{};
uint8_t identity[chat::kReticulumPeerHashSize]{};
destination[0] = 0x42U;
identity[0] = 0x24U;
input.reticulum_identity =
chat::makeReticulumPeerIdentity(destination, identity);
input.has_reticulum_lxmf_hash = true;
input.reticulum_lxmf_hash[0] = 0x99U;
}
std::vector<uint8_t> slot(storage::messageSlotSize(protocol));
assert(storage::encodeMessageSlot(input, 17U, slot.data(), slot.size()));
chat::ChatMessage output{};
uint32_t sequence = 0;
assert(storage::decodeMessageSlot(protocol,
slot.data(),
slot.size(),
output,
&sequence));
assert(sequence == 17U);
assert(output.protocol == protocol);
assert(output.msg_id == input.msg_id);
assert(output.text == input.text);
assert(output.has_geo);
assert(output.geo_lon_e7 == input.geo_lon_e7);
assert(output.source_unverified);
if (protocol == chat::MeshProtocol::Reticulum)
{
assert(chat::sameReticulumDestinationHash(output.reticulum_identity,
input.reticulum_identity));
assert(output.has_reticulum_lxmf_hash);
assert(std::memcmp(output.reticulum_lxmf_hash,
input.reticulum_lxmf_hash,
chat::kReticulumLxmfHashSize) == 0);
}
slot.back() ^= 0x80U;
assert(!storage::decodeMessageSlot(protocol,
slot.data(),
slot.size(),
output));
}
} // namespace
int main()
{
roundTrip(chat::MeshProtocol::Meshtastic,
storage::kMeshtasticTextMax);
roundTrip(chat::MeshProtocol::MeshCore, storage::kMeshCoreTextMax);
roundTrip(chat::MeshProtocol::Reticulum,
storage::kReticulumTextMax);
chat::ChatMessage too_long = messageFor(
chat::MeshProtocol::Meshtastic,
storage::kMeshtasticTextMax + 1U);
std::vector<uint8_t> slot(
storage::messageSlotSize(chat::MeshProtocol::Meshtastic));
assert(!storage::encodeMessageSlot(too_long,
1U,
slot.data(),
slot.size()));
assert(storage::messageSlotSize(chat::MeshProtocol::Meshtastic) <
storage::messageSlotSize(chat::MeshProtocol::Reticulum));
return 0;
}
@@ -0,0 +1,179 @@
#include "platform/esp/arduino_common/chat/infra/store/protocol_peer_codec.h"
#include <cassert>
#include <cstring>
#include <vector>
namespace storage = chat::storage::v2;
namespace
{
void fillKey(uint8_t* key, std::size_t len, uint8_t seed)
{
for (std::size_t index = 0U; index < len; ++index)
{
key[index] = static_cast<uint8_t>(seed + index);
}
}
storage::PeerProjection meshtasticPeer()
{
storage::PeerProjection projection{};
chat::MeshPeerRecord& peer = projection.record;
peer.valid = true;
peer.identity = chat::makeMeshPeerNodeIdentity(
chat::MeshProtocol::Meshtastic,
0x11223344U);
peer.source = chat::MeshPeerSource::RuntimeRx;
peer.first_seen_s = 10U;
peer.last_seen_s = 20U;
chat::copyMeshPeerText(peer.display_name,
sizeof(peer.display_name),
"Meshtastic peer");
chat::copyMeshPeerText(peer.user_alias,
sizeof(peer.user_alias),
"not-in-peer");
peer.flags.favorite = true;
chat::copyMeshPeerText(peer.meshtastic.node.short_name,
sizeof(peer.meshtastic.node.short_name),
"MT");
chat::copyMeshPeerText(peer.meshtastic.node.long_name,
sizeof(peer.meshtastic.node.long_name),
"Meshtastic Node");
peer.meshtastic.node.channel = 1U;
peer.meshtastic.node.hops_away = 2U;
peer.meshtastic.has_next_hop = true;
peer.meshtastic.next_hop = 7U;
peer.meshtastic.has_public_key = true;
peer.meshtastic.key_manually_verified = true;
fillKey(peer.meshtastic.public_key,
sizeof(peer.meshtastic.public_key),
1U);
peer.observations.has_snr = true;
peer.observations.snr = 4.5F;
peer.observations.has_position = true;
peer.observations.position.valid = true;
peer.observations.position.latitude_i = 123;
return projection;
}
void roundTripPeer(chat::MeshProtocol protocol,
const storage::PeerProjection& input)
{
std::vector<uint8_t> slot(storage::peerSlotSize(protocol));
assert(storage::encodePeerSlot(protocol,
input,
slot.data(),
slot.size()));
storage::PeerProjection output{};
assert(storage::decodePeerSlot(protocol,
slot.data(),
slot.size(),
output));
assert(chat::sameMeshPeerIdentity(input.record.identity,
output.record.identity));
assert(output.record.last_seen_s == input.record.last_seen_s);
assert(std::strcmp(output.record.display_name,
input.record.display_name) == 0);
assert(output.record.user_alias[0] == '\0');
assert(!output.record.flags.favorite);
slot.back() ^= 0x80U;
assert(!storage::decodePeerSlot(protocol,
slot.data(),
slot.size(),
output));
}
void unresolvedIdentityRoundTrips()
{
storage::PeerProjection mc{};
mc.record.valid = true;
mc.record.identity = chat::makeMeshPeerNodeIdentity(
chat::MeshProtocol::MeshCore,
0xAABBCCDDU);
mc.record.meshcore.node_id_hint = 0xAABBCCDDU;
roundTripPeer(chat::MeshProtocol::MeshCore, mc);
storage::PeerProjection rt{};
rt.record.valid = true;
rt.record.identity = chat::makeMeshPeerNodeIdentity(
chat::MeshProtocol::Reticulum,
0x01020304U);
roundTripPeer(chat::MeshProtocol::Reticulum, rt);
}
void stableIdentityAndContactRoundTrips()
{
uint8_t public_key[chat::kMeshPeerMeshCorePublicKeyLen]{};
fillKey(public_key, sizeof(public_key), 9U);
storage::PeerProjection mc{};
mc.record.valid = true;
assert(chat::makeMeshPeerPublicKeyIdentity(
chat::MeshProtocol::MeshCore,
public_key,
sizeof(public_key),
mc.record.identity));
mc.record.meshcore.has_public_key = true;
mc.record.meshcore.node_id_hint = 0x55667788U;
std::memcpy(mc.record.meshcore.public_key,
public_key,
sizeof(public_key));
roundTripPeer(chat::MeshProtocol::MeshCore, mc);
storage::ContactProjection contact{};
contact.identity = mc.record.identity;
contact.node_id_hint = mc.record.meshcore.node_id_hint;
contact.flags.favorite = true;
contact.flags.trusted = true;
chat::copyMeshPeerText(contact.alias,
sizeof(contact.alias),
"base");
std::vector<uint8_t> slot(
storage::contactSlotSize(chat::MeshProtocol::MeshCore));
assert(storage::encodeContactSlot(chat::MeshProtocol::MeshCore,
contact,
slot.data(),
slot.size()));
storage::ContactProjection decoded{};
assert(storage::decodeContactSlot(chat::MeshProtocol::MeshCore,
slot.data(),
slot.size(),
decoded));
assert(decoded.node_id_hint == contact.node_id_hint);
assert(decoded.flags.favorite);
assert(decoded.flags.trusted);
assert(std::strcmp(decoded.alias, contact.alias) == 0);
uint8_t destination[chat::kReticulumPeerHashSize]{};
uint8_t identity[chat::kReticulumPeerHashSize]{};
fillKey(destination, sizeof(destination), 3U);
fillKey(identity, sizeof(identity), 33U);
storage::PeerProjection rt{};
rt.record.valid = true;
rt.record.identity = chat::makeMeshPeerReticulumIdentity(
chat::makeReticulumPeerIdentity(destination, identity));
rt.record.reticulum.identity = rt.record.identity.reticulum;
roundTripPeer(chat::MeshProtocol::Reticulum, rt);
}
} // namespace
int main()
{
const storage::PeerProjection mt = meshtasticPeer();
roundTripPeer(chat::MeshProtocol::Meshtastic, mt);
unresolvedIdentityRoundTrips();
stableIdentityAndContactRoundTrips();
assert(storage::peerSlotSize(chat::MeshProtocol::Meshtastic) <
sizeof(chat::MeshPeerRecord));
assert(storage::peerSlotSize(chat::MeshProtocol::MeshCore) <
sizeof(chat::MeshPeerRecord));
assert(storage::peerSlotSize(chat::MeshProtocol::Reticulum) <
sizeof(chat::MeshPeerRecord));
assert(storage::contactSlotSize(chat::MeshProtocol::Meshtastic) < 64U);
assert(storage::contactSlotSize(chat::MeshProtocol::MeshCore) < 80U);
assert(storage::contactSlotSize(chat::MeshProtocol::Reticulum) < 64U);
return 0;
}
@@ -13,6 +13,7 @@
#include "chat/ports/i_mesh_adapter.h"
#include "chat/ports/i_mesh_peer_directory.h"
#include "chat/ports/i_node_store.h"
#include "chat/ports/i_protocol_peer_repository.h"
#include "chat/usecase/chat_service.h"
#include "chat/usecase/contact_service.h"
#include "team/ports/i_team_crypto.h"
@@ -40,7 +41,7 @@ struct ChatServicesBundle
{
std::unique_ptr<chat::ChatModel> model;
std::unique_ptr<chat::IChatStore> store;
std::unique_ptr<chat::IMeshPeerDirectory> mesh_peer_directory;
std::unique_ptr<chat::IProtocolPeerRepository> mesh_peer_directory;
std::unique_ptr<chat::IMeshAdapter> mesh_runtime;
std::unique_ptr<chat::ChatService> service;
std::unique_ptr<chat::ChatService::IncomingMessageObserver> incoming_message_observer;
@@ -54,13 +55,11 @@ struct ChatServicesBundle
struct ContactServicesBundle
{
std::unique_ptr<chat::contacts::INodeStore> node_store;
std::unique_ptr<chat::contacts::IContactStore> contact_store;
std::unique_ptr<chat::contacts::ContactService> service;
bool isValid() const
{
return node_store && contact_store && service;
return service != nullptr;
}
};
@@ -106,7 +105,8 @@ struct AppContextPlatformBindings
std::unique_ptr<chat::IMeshAdapter> (*)(chat::MeshProtocol protocol,
LoraBoard& lora_board,
chat::IMeshPeerDirectory* peer_directory);
using ContactServicesFactory = ContactServicesBundle (*)();
using ContactServicesFactory =
ContactServicesBundle (*)(chat::IProtocolPeerRepository& repository);
using TeamServicesFactory = TeamServicesBundle (*)(chat::IMeshAdapter& mesh_adapter);
using SelfNodeIdProvider = chat::NodeId (*)();
@@ -115,7 +115,7 @@ struct ChatWorkspaceSnapshot
ui::SnapshotHeader header;
static constexpr size_t kMaxLocationParticipants = 16;
static constexpr size_t kMaxMessages = 20;
static constexpr size_t kMaxMessages = 10;
ConversationRow conversations[16]{};
size_t conversation_count = 0;
@@ -5,7 +5,7 @@
#include <cstring>
#ifndef CHAT_UI_SEND_TRACE_ENABLE
#define CHAT_UI_SEND_TRACE_ENABLE 1
#define CHAT_UI_SEND_TRACE_ENABLE 0
#endif
#if CHAT_UI_SEND_TRACE_ENABLE
@@ -152,7 +152,6 @@ class UiController : public IChatUiRefreshSink
std::string resolveConversationDisplayName(const chat::ConversationId& conv) const;
void applyConversationListToUi();
void updateConversationMetaForMessage(const chat::ChatMessage& msg, bool increment_unread);
bool updateConversationViewForIncoming(const chat::ChatMessage& msg);
void reloadConversationView();
// Team text, read projection, and location action sends flow through
// ChatTeamWorkflow.
@@ -14,7 +14,7 @@
#include <string>
#ifndef CHAT_UI_SEND_TRACE_ENABLE
#define CHAT_UI_SEND_TRACE_ENABLE 1
#define CHAT_UI_SEND_TRACE_ENABLE 0
#endif
#if CHAT_UI_SEND_TRACE_ENABLE
@@ -7,7 +7,7 @@
#include <string>
#ifndef CHAT_UI_SEND_TRACE_ENABLE
#define CHAT_UI_SEND_TRACE_ENABLE 1
#define CHAT_UI_SEND_TRACE_ENABLE 0
#endif
#if CHAT_UI_SEND_TRACE_ENABLE
@@ -29,7 +29,7 @@
#include <ctime>
#ifndef CHAT_CONVERSATION_LOG_ENABLE
#define CHAT_CONVERSATION_LOG_ENABLE 1
#define CHAT_CONVERSATION_LOG_ENABLE 0
#endif
#if CHAT_CONVERSATION_LOG_ENABLE
@@ -35,7 +35,7 @@
#include <ctime>
#ifndef CHAT_UI_LOG_ENABLE
#define CHAT_UI_LOG_ENABLE 1
#define CHAT_UI_LOG_ENABLE 0
#endif
#if CHAT_UI_LOG_ENABLE
@@ -883,7 +883,6 @@ void UiController::onRuntimeMessageArrived(chat::MessageId msg_id)
updateConversationMetaForMessage(*latest, !is_current_conversation);
if (is_current_conversation)
{
(void)updateConversationViewForIncoming(*latest);
reloadConversationView();
(void)chat_model_.markRead(
chat_presentation_adapters::toUiConversationId(current_conv_));
@@ -1742,22 +1741,6 @@ void UiController::updateConversationMetaForMessage(const chat::ChatMessage& msg
normalizeConversationNames(cached_conversations_);
}
bool UiController::updateConversationViewForIncoming(const chat::ChatMessage& msg)
{
if (!conversation_)
{
return false;
}
if (!(current_conv_ == chat::conversationIdForMessage(msg)))
{
return false;
}
reloadConversationView();
return true;
}
void UiController::reloadConversationView()
{
if (!conversation_ || team_conv_active_)
@@ -503,6 +503,7 @@ void setNodePosition(::chat::contacts::ContactService& contacts,
int main()
{
static_assert(::ui::chat::ChatWorkspaceSnapshot::kMaxMessages == 10);
sys::set_epoch_seconds_provider([]() -> uint32_t
{ return 1700000000U; });
sys::set_millis_provider([]() -> uint32_t
@@ -161,12 +161,12 @@ class AppContext final : public IAppBleFacade
chat::contacts::INodeStore* getNodeStore() override
{
return node_store_.get();
return node_store_;
}
const chat::contacts::INodeStore* getNodeStore() const override
{
return node_store_.get();
return node_store_;
}
bool getDeviceMacAddress(uint8_t out_mac[6]) const override;
@@ -175,12 +175,12 @@ class AppContext final : public IAppBleFacade
chat::contacts::IContactStore* getContactStore()
{
return contact_store_.get();
return contact_store_;
}
const chat::contacts::IContactStore* getContactStore() const
{
return contact_store_.get();
return contact_store_;
}
void saveConfig() override;
@@ -272,10 +272,10 @@ class AppContext final : public IAppBleFacade
std::unique_ptr<chat::ChatModel> chat_model_;
std::unique_ptr<chat::IChatStore> chat_store_;
std::unique_ptr<chat::IMeshPeerDirectory> mesh_peer_directory_;
std::unique_ptr<chat::IProtocolPeerRepository> mesh_peer_directory_;
std::unique_ptr<chat::IMeshAdapter> mesh_router_;
std::unique_ptr<chat::contacts::INodeStore> node_store_;
std::unique_ptr<chat::contacts::IContactStore> contact_store_;
chat::contacts::INodeStore* node_store_ = nullptr;
chat::contacts::IContactStore* contact_store_ = nullptr;
std::unique_ptr<chat::ChatService> chat_service_;
std::unique_ptr<chat::contacts::ContactService> contact_service_;
@@ -1,68 +0,0 @@
/**
* @file contact_store.h
* @brief Contact nickname storage shell backed by a single selected persistence backend
*/
#pragma once
#include "chat/infra/contact_store_core.h"
#include "chat/ports/i_contact_blob_store.h"
#include <cstddef>
namespace chat
{
namespace contacts
{
class ContactStore : public IContactStore,
private chat::IContactBlobStore
{
public:
static void* operator new(std::size_t size);
static void operator delete(void* ptr) noexcept;
static void operator delete(void* ptr, std::size_t) noexcept;
ContactStore();
void begin() override;
std::string getNickname(uint32_t node_id) const override;
bool setNickname(uint32_t node_id, const char* nickname) override;
bool removeNickname(uint32_t node_id) override;
bool hasNickname(const char* nickname) const override;
std::vector<uint32_t> getAllContactIds() const override;
size_t getCount() const override;
private:
enum class StorageBackend : uint8_t
{
Sd,
Flash,
};
enum class LoadResult : uint8_t
{
Loaded,
MissingOrInvalid,
Busy,
};
static constexpr const char* kSdPath = "/contacts.dat";
static constexpr const char* kPrefNs = "contacts";
static constexpr const char* kPrefKey = "contact_blob";
bool loadBlob(std::vector<uint8_t>& out) override;
bool saveBlob(const uint8_t* data, size_t len) override;
LoadResult loadFromSD(std::vector<uint8_t>& out) const;
bool saveToSD(const uint8_t* data, size_t len) const;
bool loadFromFlash(std::vector<uint8_t>& out) const;
bool saveToFlash(const uint8_t* data, size_t len) const;
void clearFlash() const;
ContactStoreCore core_;
StorageBackend backend_ = StorageBackend::Flash;
};
} // namespace contacts
} // namespace chat
@@ -6,101 +6,21 @@
#pragma once
#include "chat/infra/lxmf/lxmf_wire.h"
#include "platform/esp/arduino_common/memory/psram_allocator.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <new>
#include <utility>
#include <vector>
#if defined(ESP_PLATFORM)
#include <esp_heap_caps.h>
#endif
namespace chat::lxmf::runtime
{
[[noreturn]] inline void allocation_failed()
{
#if defined(__cpp_exceptions) || defined(__EXCEPTIONS)
throw std::bad_alloc();
#else
std::abort();
#endif
}
// LXMF keeps its public buffer aliases while sharing the platform allocator.
template <typename T>
class PsramAllocator
{
public:
using value_type = T;
PsramAllocator() noexcept = default;
template <typename U>
PsramAllocator(const PsramAllocator<U>&) noexcept
{
}
T* allocate(std::size_t count)
{
if (count > std::numeric_limits<std::size_t>::max() / sizeof(T))
{
allocation_failed();
}
const std::size_t bytes = count * sizeof(T);
if (bytes == 0)
{
return nullptr;
}
#if defined(ESP_PLATFORM)
void* ptr = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
#else
void* ptr = std::malloc(bytes);
#endif
if (!ptr)
{
allocation_failed();
}
return static_cast<T*>(ptr);
}
void deallocate(T* ptr, std::size_t) noexcept
{
if (!ptr)
{
return;
}
#if defined(ESP_PLATFORM)
heap_caps_free(ptr);
#else
std::free(ptr);
#endif
}
template <typename U>
struct rebind
{
using other = PsramAllocator<U>;
};
};
template <typename T, typename U>
bool operator==(const PsramAllocator<T>&, const PsramAllocator<U>&) noexcept
{
return true;
}
template <typename T, typename U>
bool operator!=(const PsramAllocator<T>&, const PsramAllocator<U>&) noexcept
{
return false;
}
using PsramAllocator =
::platform::esp::arduino_common::memory::PsramAllocator<T>;
using RuntimeByteBuffer = std::vector<uint8_t, PsramAllocator<uint8_t>>;
using RuntimeByteBufferList =
@@ -1,98 +0,0 @@
/**
* @file node_store.h
* @brief Persisted NodeInfo store shell backed by a single selected persistence backend
*/
#pragma once
#include "chat/infra/node_store_core.h"
#include "chat/ports/i_node_blob_store.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include <cstddef>
#include <vector>
namespace chat
{
namespace meshtastic
{
class NodeStore : public contacts::INodeStore,
private contacts::INodeBlobStore
{
public:
static void* operator new(std::size_t size);
static void operator delete(void* ptr) noexcept;
static void operator delete(void* ptr, std::size_t) noexcept;
NodeStore();
void begin() override;
void applyUpdate(uint32_t node_id, const contacts::NodeUpdate& update) override;
void upsert(uint32_t node_id, const char* short_name, const char* long_name,
uint32_t now_secs, float snr = 0.0f, float rssi = 0.0f, uint8_t protocol = 0,
uint8_t role = contacts::kNodeRoleUnknown, uint8_t hops_away = 0xFF,
uint8_t hw_model = 0, uint8_t channel = 0xFF) override;
void updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) override;
void updatePosition(uint32_t node_id, const contacts::NodePosition& position) override;
bool remove(uint32_t node_id) override;
const std::vector<contacts::NodeEntry>& getEntries() const override;
void clear() override;
bool flush() override;
private:
enum class StorageBackend : uint8_t
{
Sd,
Nvs,
};
enum class LoadResult : uint8_t
{
Loaded,
MissingOrInvalid,
Busy,
};
static constexpr const char* kPersistNodesFile = "/nodes.bin";
static constexpr const char* kPersistNodesNs = "nodes";
static constexpr const char* kPersistNodesKey = "node_blob";
static constexpr const char* kPersistNodesKeyVer = "ver";
static constexpr const char* kPersistNodesKeyCrc = "crc";
bool loadBlob(std::vector<uint8_t>& out) override;
bool saveBlob(const uint8_t* data, size_t len) override;
void clearBlob() override;
bool loadFromNvs(std::vector<uint8_t>& out);
LoadResult loadFromSd(std::vector<uint8_t>& out) const;
bool saveToNvs(const uint8_t* data, size_t len) const;
bool saveToSd(const uint8_t* data, size_t len) const;
bool saveToBackend(const uint8_t* data, size_t len, StorageBackend backend) const;
void ensureAsyncSaveWorker();
bool enqueueAsyncSave(const uint8_t* data, size_t len);
bool waitForAsyncSave(TickType_t wait_ticks);
void asyncSaveLoop();
static void asyncSaveTaskEntry(void* context);
void clearNvs() const;
contacts::NodeStoreCore core_;
StorageBackend backend_ = StorageBackend::Nvs;
SemaphoreHandle_t async_save_mutex_ = nullptr;
QueueHandle_t async_save_queue_ = nullptr;
TaskHandle_t async_save_task_ = nullptr;
std::vector<uint8_t> pending_save_blob_;
StorageBackend pending_save_backend_ = StorageBackend::Nvs;
uint32_t pending_save_generation_ = 0;
uint32_t completed_save_generation_ = 0;
bool async_save_pending_ = false;
bool async_save_busy_ = false;
bool async_save_failed_ = false;
};
} // namespace meshtastic
} // namespace chat
@@ -0,0 +1,92 @@
#pragma once
#include "platform/esp/arduino_common/chat/infra/store/protocol_chat_codec.h"
#include <cstddef>
#include <cstdint>
namespace chat::storage::v2
{
class FixedSlotJournalEngine final
{
public:
enum class State : uint8_t
{
Missing = 0,
Ready,
Incompatible,
PartialTail,
IoError,
};
struct Inspection
{
State state = State::Missing;
uint32_t slot_count = 0;
};
Inspection inspect(const char* path,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size) const;
bool create(const char* path,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size) const;
bool append(const char* path,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size,
const void* slot) const;
bool read(const char* path,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size,
uint32_t slot_index,
void* out_slot) const;
static constexpr std::size_t headerSize();
private:
struct Header
{
uint32_t magic = 0;
uint16_t schema = 0;
uint8_t protocol = 0;
uint8_t kind = 0;
uint16_t slot_size = 0;
uint16_t reserved = 0;
uint32_t crc = 0;
} __attribute__((packed));
static constexpr uint32_t kMagic = 0x324C4E4AU; // JNL2
static bool validDescriptor(MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size);
static Header makeHeader(MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size);
static bool matches(const Header& header,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size);
};
bool replaceFileAtomically(const char* temp_path,
const char* final_path,
const char* backup_path);
bool recoverAtomicFile(const char* final_path,
const char* temp_path,
const char* backup_path);
constexpr std::size_t FixedSlotJournalEngine::headerSize()
{
return sizeof(Header);
}
} // namespace chat::storage::v2
@@ -0,0 +1,118 @@
#pragma once
#include "chat/domain/chat_types.h"
#include <cstddef>
#include <cstdint>
namespace chat::storage::v2
{
constexpr uint16_t kStorageSchemaVersion = 2;
constexpr std::size_t kMeshtasticTextMax = 233;
constexpr std::size_t kMeshCoreTextMax = 171;
constexpr std::size_t kReticulumTextMax = 255;
constexpr std::size_t kCatalogPreviewMax = 48;
enum class JournalKind : uint8_t
{
MessageSegment = 1,
CatalogSnapshot = 2,
CatalogDelta = 3,
ReadStateSnapshot = 4,
ReadStateDelta = 5,
StatusSnapshot = 6,
StatusDelta = 7,
ReticulumSeen = 8,
PeerSnapshot = 9,
PeerDelta = 10,
ContactSnapshot = 11,
ContactDelta = 12,
};
struct ChatCatalogProjection
{
ConversationId conversation{};
MessageId last_message_id = 0;
uint32_t message_count = 0;
uint32_t last_sequence = 0;
uint32_t last_timestamp = 0;
uint32_t unread = 0;
MessageStatus last_status = MessageStatus::Incoming;
bool deleted = false;
char preview[kCatalogPreviewMax + 1] = {};
};
struct ChatReadProjection
{
ConversationId conversation{};
uint32_t last_read_sequence = 0;
bool deleted = false;
};
struct ChatStatusProjection
{
MessageId message_id = 0;
MessageStatus status = MessageStatus::Queued;
uint32_t sequence = 0;
};
struct ReticulumSeenProjection
{
uint8_t hash[kReticulumLxmfHashSize] = {};
};
uint32_t crc32(const void* data, std::size_t len);
MeshProtocol canonicalProtocol(MeshProtocol protocol);
bool supportedProtocol(MeshProtocol protocol);
std::size_t messageSlotSize(MeshProtocol protocol);
std::size_t catalogSlotSize(MeshProtocol protocol);
std::size_t readStateSlotSize(MeshProtocol protocol);
std::size_t statusSlotSize();
std::size_t reticulumSeenSlotSize();
bool encodeMessageSlot(const ChatMessage& message,
uint32_t sequence,
void* out,
std::size_t out_len);
bool decodeMessageSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
ChatMessage& out_message,
uint32_t* out_sequence = nullptr);
bool encodeCatalogSlot(MeshProtocol protocol,
const ChatCatalogProjection& projection,
void* out,
std::size_t out_len);
bool decodeCatalogSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
ChatCatalogProjection& out_projection);
bool encodeReadStateSlot(MeshProtocol protocol,
const ChatReadProjection& projection,
void* out,
std::size_t out_len);
bool decodeReadStateSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
ChatReadProjection& out_projection);
bool encodeStatusSlot(const ChatStatusProjection& projection,
void* out,
std::size_t out_len);
bool decodeStatusSlot(const void* data,
std::size_t len,
ChatStatusProjection& out_projection);
bool encodeReticulumSeenSlot(const ReticulumSeenProjection& projection,
void* out,
std::size_t out_len);
bool decodeReticulumSeenSlot(const void* data,
std::size_t len,
ReticulumSeenProjection& out_projection);
} // namespace chat::storage::v2
@@ -0,0 +1,47 @@
#pragma once
#include "chat/domain/mesh_peer_directory.h"
#include <cstddef>
#include <cstdint>
namespace chat::storage::v2
{
struct PeerProjection
{
MeshPeerRecord record{};
bool deleted = false;
};
struct ContactProjection
{
MeshPeerIdentity identity{};
NodeId node_id_hint = 0;
MeshPeerUserFlags flags{};
char alias[kMeshPeerUserAliasMaxLen + 1] = {};
bool deleted = false;
};
std::size_t peerSlotSize(MeshProtocol protocol);
std::size_t contactSlotSize(MeshProtocol protocol);
bool encodePeerSlot(MeshProtocol protocol,
const PeerProjection& projection,
void* out,
std::size_t out_len);
bool decodePeerSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
PeerProjection& out_projection);
bool encodeContactSlot(MeshProtocol protocol,
const ContactProjection& projection,
void* out,
std::size_t out_len);
bool decodeContactSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
ContactProjection& out_projection);
} // namespace chat::storage::v2
@@ -0,0 +1,216 @@
#pragma once
#include "chat/infra/mesh_peer_directory_core.h"
#include "chat/ports/i_chat_store.h"
#include "chat/ports/i_protocol_peer_repository.h"
#include "platform/esp/arduino_common/chat/infra/store/fixed_slot_journal.h"
#include "platform/esp/arduino_common/chat/infra/store/protocol_peer_codec.h"
#include "platform/esp/arduino_common/memory/psram_allocator.h"
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace chat
{
class SdProtocolPeerRepository final : public IProtocolPeerRepository
{
public:
explicit SdProtocolPeerRepository(IChatStore& chat_store);
~SdProtocolPeerRepository() override;
SdProtocolPeerRepository(const SdProtocolPeerRepository&) = delete;
SdProtocolPeerRepository& operator=(const SdProtocolPeerRepository&) = delete;
MeshPeerDirectoryStatus begin() override;
MeshPeerDirectoryStatus record(const MeshPeerRecord& record) override;
MeshPeerDirectoryStatus find(const MeshPeerIdentity& identity,
MeshPeerRecord& out_record) override;
MeshPeerDirectoryStatus findByNodeId(MeshProtocol protocol,
NodeId node_id,
MeshPeerRecord& out_record) override;
MeshPeerDirectoryStatus loadRecent(MeshProtocol protocol,
MeshPeerRecord* out_records,
std::size_t max_records,
std::size_t* out_count) override;
MeshPeerDirectoryStatus search(MeshProtocol protocol,
const char* query,
MeshPeerRecord* out_records,
std::size_t max_records,
std::size_t* out_count) override;
MeshPeerDirectoryStatus setUserFlags(
const MeshPeerIdentity& identity,
const MeshPeerUserFlags& flags) override;
MeshPeerDirectoryStatus remove(const MeshPeerIdentity& identity) override;
MeshPeerDirectoryStatus clearProtocol(MeshProtocol protocol) override;
MeshPeerDirectoryCapacity capacityFor(MeshProtocol protocol) const override;
MeshPeerDirectoryStatus flush() override;
contacts::INodeStore& nodeStoreView() override;
contacts::IContactStore& contactStoreView() override;
private:
using PeerVector = std::vector<
MeshPeerRecord,
::platform::esp::arduino_common::memory::PsramAllocator<MeshPeerRecord>>;
using ContactVector = std::vector<
storage::v2::ContactProjection,
::platform::esp::arduino_common::memory::PsramAllocator<
storage::v2::ContactProjection>>;
using PendingPeerVector = std::vector<
storage::v2::PeerProjection,
::platform::esp::arduino_common::memory::PsramAllocator<
storage::v2::PeerProjection>>;
using PsramByteVector = std::vector<
uint8_t,
::platform::esp::arduino_common::memory::PsramAllocator<uint8_t>>;
struct PartitionState
{
uint32_t peer_delta_count = 0;
uint32_t contact_delta_count = 0;
};
class NodeStoreView final : public contacts::INodeStore
{
public:
explicit NodeStoreView(SdProtocolPeerRepository& owner);
void setActiveProtocol(MeshProtocol protocol) override;
void begin() override;
void applyUpdate(uint32_t node_id,
const contacts::NodeUpdate& update) override;
void upsert(uint32_t node_id,
const char* short_name,
const char* long_name,
uint32_t now_secs,
float snr,
float rssi,
uint8_t protocol,
uint8_t role,
uint8_t hops_away,
uint8_t hw_model,
uint8_t channel) override;
void updateProtocol(uint32_t node_id,
uint8_t protocol,
uint32_t now_secs) override;
void updatePosition(uint32_t node_id,
const contacts::NodePosition& position) override;
bool remove(uint32_t node_id) override;
const std::vector<contacts::NodeEntry>& getEntries() const override;
void clear() override;
bool flush() override;
private:
SdProtocolPeerRepository& owner_;
};
class ContactStoreView final : public contacts::IContactStore
{
public:
explicit ContactStoreView(SdProtocolPeerRepository& owner);
void setActiveProtocol(MeshProtocol protocol) override;
void begin() override;
std::string getNickname(uint32_t node_id) const override;
bool setNickname(uint32_t node_id, const char* nickname) override;
bool removeNickname(uint32_t node_id) override;
bool hasNickname(const char* nickname) const override;
std::vector<uint32_t> getAllContactIds() const override;
size_t getCount() const override;
private:
SdProtocolPeerRepository& owner_;
};
bool ensureLayout();
bool ensureProtocolLayout(MeshProtocol protocol);
bool loadProtocol(MeshProtocol protocol);
bool loadPeerJournal(MeshProtocol protocol, const char* name);
bool loadContactJournal(MeshProtocol protocol, const char* name);
bool compactProtocolAtBoot(MeshProtocol protocol);
bool rewritePeerSnapshot(MeshProtocol protocol);
bool rewriteContactSnapshot(MeshProtocol protocol);
bool appendPeerDelta(const storage::v2::PeerProjection& projection);
bool appendContactDelta(
const storage::v2::ContactProjection& projection);
bool queueOrAppendPeerDelta(
const storage::v2::PeerProjection& projection);
bool drainPendingPeerDeltas(std::size_t budget);
bool applyPeerProjection(const storage::v2::PeerProjection& projection);
bool applyContactProjection(
const storage::v2::ContactProjection& projection);
void overlayContactFacts();
void overlayContactFactsForPeer(MeshPeerRecord& peer) const;
void reconcileStableIdentities(MeshProtocol protocol);
std::size_t findPeerIndex(const MeshPeerIdentity& identity) const;
std::size_t findPeerIndexByNodeId(MeshProtocol protocol,
NodeId node_id) const;
std::size_t findContactIndex(const MeshPeerIdentity& identity) const;
bool stableContactIdentity(const MeshPeerRecord& peer,
MeshPeerIdentity& out_identity) const;
NodeId projectedNodeId(const MeshPeerRecord& peer) const;
bool peerIsProtected(const MeshPeerRecord& peer) const;
bool peerReferencedByConversation(const MeshPeerRecord& peer) const;
bool peerReferencedByConversations(
const MeshPeerRecord& peer,
const std::vector<ConversationMeta>& conversations) const;
std::size_t ephemeralCount(MeshProtocol protocol) const;
bool evictOldestEphemeral(MeshProtocol protocol);
void applyNodeUpdate(uint32_t node_id,
const contacts::NodeUpdate& update);
bool removeNode(uint32_t node_id);
void clearActiveNodes();
const std::vector<contacts::NodeEntry>& activeNodeEntries() const;
void rebuildActiveNodeProjection() const;
bool toNodeEntry(const MeshPeerRecord& peer,
contacts::NodeEntry& out) const;
std::string nicknameForNode(uint32_t node_id) const;
bool setNicknameForNode(uint32_t node_id, const char* nickname);
bool removeNicknameForNode(uint32_t node_id);
bool hasNicknameForActiveProtocol(const char* nickname) const;
std::vector<uint32_t> activeContactIds() const;
size_t activeContactCount() const;
bool persistContactFacts(const MeshPeerIdentity& identity,
const MeshPeerUserFlags& flags,
const char* alias,
bool deleted);
static MeshProtocol normalizeProtocol(MeshProtocol protocol);
static const char* protocolSlug(MeshProtocol protocol);
static std::size_t protocolIndex(MeshProtocol protocol);
static NodeId reticulumNodeId(const ReticulumPeerIdentity& identity);
static bool ensureDirectory(const char* path);
static void buildProtocolPath(MeshProtocol protocol,
const char* name,
char* out,
std::size_t out_len);
bool lock() const;
void unlock() const;
IChatStore& chat_store_;
storage::v2::FixedSlotJournalEngine journal_{};
PeerVector peers_{};
ContactVector contacts_{};
PendingPeerVector pending_peer_deltas_{};
std::size_t pending_peer_head_ = 0U;
PsramByteVector slot_scratch_{};
PartitionState partitions_[3]{};
mutable std::vector<contacts::NodeEntry> active_node_projection_{};
mutable bool active_node_projection_dirty_ = true;
MeshProtocol active_protocol_ = MeshProtocol::Meshtastic;
bool begun_ = false;
mutable SemaphoreHandle_t mutex_ = nullptr;
NodeStoreView node_store_view_;
ContactStoreView contact_store_view_;
};
} // namespace chat
@@ -1,33 +1,33 @@
/**
* @file sd_store.h
* @brief ESP Arduino chat store backed by the mounted SD card.
* @brief Protocol-partitioned append-only ESP chat storage.
*/
#pragma once
#include "chat/ports/i_chat_store.h"
#include "platform/esp/arduino_common/chat/infra/store/fixed_slot_journal.h"
#include "platform/esp/arduino_common/chat/infra/store/protocol_chat_codec.h"
#include "platform/esp/arduino_common/memory/psram_allocator.h"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace platform::esp::arduino_common::storage
{
class SdRuntimeFile;
} // namespace platform::esp::arduino_common::storage
namespace chat
{
class SdStore final : public IChatStore
{
public:
static constexpr const char* kDir = "/chat";
static constexpr const char* kIndexFile = "/chat/index.bin";
static constexpr const char* kReadStateFile = "/chat/read_state.bin";
static constexpr const char* kReticulumLxmfSeenFile = "/chat/lxmf_seen.bin";
static constexpr size_t kMaxMessagesPerConv = 1000;
static constexpr size_t kMaxReticulumLxmfSeen = 1024;
static constexpr size_t kMaxTextLen = 233;
static constexpr size_t kPreviewLen = 48;
static constexpr const char* kRoot = "/data/v2";
static constexpr const char* kMeshtasticRoot = "/data/v2/mt/chat";
static constexpr const char* kMeshCoreRoot = "/data/v2/mc/chat";
static constexpr const char* kReticulumRoot = "/data/v2/rt/chat";
static constexpr std::size_t kMessageSegmentBytes = 128U * 1024U;
static constexpr std::size_t kCatalogPreviewLen =
storage::v2::kCatalogPreviewMax;
static constexpr std::size_t kSeenHotCapacity = 4096;
SdStore();
~SdStore() override = default;
@@ -35,20 +35,30 @@ class SdStore final : public IChatStore
bool isReady() const { return ready_; }
void append(const ChatMessage& msg) override;
bool appendDurably(const ChatMessage& msg) override;
bool appendIncomingDurably(const ChatMessage& msg) override;
std::vector<ChatMessage> loadRecent(const ConversationId& conv, size_t n) override;
std::vector<ChatMessage> loadPageFromLatest(const ConversationId& conv,
size_t offset_from_latest,
size_t limit,
size_t* total) override;
std::vector<ConversationMeta> loadConversationPage(size_t offset,
size_t limit,
size_t* total) override;
std::vector<ChatMessage> loadRecent(const ConversationId& conv,
std::size_t n) override;
std::vector<ChatMessage> loadPageFromLatest(
const ConversationId& conv,
std::size_t offset_from_latest,
std::size_t limit,
std::size_t* total) override;
std::vector<ConversationMeta> loadConversationPage(
std::size_t offset,
std::size_t limit,
std::size_t* total) override;
std::vector<ConversationMeta> loadConversationPageForProtocol(
MeshProtocol protocol,
std::size_t offset,
std::size_t limit,
std::size_t* total) override;
bool setUnread(const ConversationId& conv, int unread) override;
int getUnread(const ConversationId& conv) const override;
void clearConversation(const ConversationId& conv) override;
void clearAll() override;
bool updateMessageStatus(MessageId msg_id, MessageStatus status) override;
bool updateMessageStatus(MessageId msg_id,
MessageStatus status) override;
bool updateMessageStatusForProtocol(MessageId msg_id,
MeshProtocol protocol,
MessageStatus status) override;
@@ -56,245 +66,153 @@ class SdStore final : public IChatStore
bool getMessageForProtocol(MessageId msg_id,
MeshProtocol protocol,
ChatMessage* out) const override;
bool hasReticulumLxmfMessageHash(const uint8_t* lxmf_hash) const override;
bool hasReticulumLxmfMessageHash(
const uint8_t* lxmf_hash) const override;
void flush() override;
private:
struct FileHeader
using CatalogList = std::vector<
storage::v2::ChatCatalogProjection,
::platform::esp::arduino_common::memory::PsramAllocator<
storage::v2::ChatCatalogProjection>>;
using ReadStateList = std::vector<
storage::v2::ChatReadProjection,
::platform::esp::arduino_common::memory::PsramAllocator<
storage::v2::ChatReadProjection>>;
struct ProtocolStatusProjection
{
uint32_t magic = 0;
uint16_t version = 0;
uint16_t head = 0;
uint16_t count = 0;
uint16_t reserved = 0;
} __attribute__((packed));
MeshProtocol protocol = MeshProtocol::Meshtastic;
storage::v2::ChatStatusProjection value{};
};
struct RecordV2
{
uint8_t protocol = 0;
uint8_t channel = 0;
uint8_t status = 0;
uint16_t text_len = 0;
uint32_t from = 0;
uint32_t peer = 0;
uint32_t msg_id = 0;
uint32_t timestamp = 0;
char text[kMaxTextLen] = {};
} __attribute__((packed));
using StatusList = std::vector<
ProtocolStatusProjection,
::platform::esp::arduino_common::memory::PsramAllocator<
ProtocolStatusProjection>>;
using SeenList = std::vector<
storage::v2::ReticulumSeenProjection,
::platform::esp::arduino_common::memory::PsramAllocator<
storage::v2::ReticulumSeenProjection>>;
using ScratchBuffer = std::vector<
uint8_t,
::platform::esp::arduino_common::memory::PsramAllocator<uint8_t>>;
struct RecordV3
{
uint8_t protocol = 0;
uint8_t channel = 0;
uint8_t status = 0;
uint8_t flags = 0;
uint16_t text_len = 0;
uint32_t from = 0;
uint32_t peer = 0;
uint32_t msg_id = 0;
uint32_t timestamp = 0;
uint8_t reticulum_destination_hash[kReticulumPeerHashSize] = {};
uint8_t reticulum_identity_hash[kReticulumPeerHashSize] = {};
char text[kMaxTextLen] = {};
} __attribute__((packed));
static constexpr std::size_t kScratchCapacity = 512;
static constexpr std::size_t kCatalogCompactThreshold = 512;
static constexpr std::size_t kReadCompactThreshold = 256;
static constexpr std::size_t kStatusCompactThreshold = 2048;
static constexpr uint32_t kProjectionRetryIntervalMs = 5000U;
struct RecordV4
{
uint8_t protocol = 0;
uint8_t channel = 0;
uint8_t status = 0;
uint8_t flags = 0;
uint8_t rx_origin = 0;
uint16_t text_len = 0;
uint32_t from = 0;
uint32_t peer = 0;
uint32_t msg_id = 0;
uint32_t timestamp = 0;
uint8_t reticulum_destination_hash[kReticulumPeerHashSize] = {};
uint8_t reticulum_identity_hash[kReticulumPeerHashSize] = {};
char text[kMaxTextLen] = {};
} __attribute__((packed));
bool appendInternal(const ChatMessage& msg, bool incoming_commit);
bool ensureLayout() const;
bool ensureProtocolLayout(MeshProtocol protocol) const;
bool loadRuntimeState();
bool loadProtocolState(MeshProtocol protocol);
bool loadCatalogJournal(MeshProtocol protocol, const char* name);
bool loadReadJournal(MeshProtocol protocol, const char* name);
bool loadStatusJournal(MeshProtocol protocol, const char* name);
bool loadSeenJournal();
bool rebuildSeenJournalFromMessages();
bool recoverProjectionSnapshot(MeshProtocol protocol,
const char* base_name);
bool reconcileProtocolCatalog(MeshProtocol protocol);
bool reconcileConversationDirectory(MeshProtocol protocol,
const char* directory_name);
struct Record
{
uint8_t protocol = 0;
uint8_t channel = 0;
uint8_t status = 0;
uint8_t flags = 0;
uint8_t rx_origin = 0;
uint16_t text_len = 0;
uint32_t from = 0;
uint32_t peer = 0;
uint32_t msg_id = 0;
uint32_t timestamp = 0;
uint8_t reticulum_destination_hash[kReticulumPeerHashSize] = {};
uint8_t reticulum_identity_hash[kReticulumPeerHashSize] = {};
uint8_t reticulum_lxmf_hash[kReticulumLxmfHashSize] = {};
char text[kMaxTextLen] = {};
} __attribute__((packed));
std::size_t slotsPerMessageSegment(MeshProtocol protocol) const;
uint32_t messageCountOnDisk(const ConversationId& conv) const;
bool readMessageByOrdinal(const ConversationId& conv,
uint32_t ordinal,
ChatMessage& out_message,
uint32_t* out_sequence = nullptr) const;
bool latestStoredMessage(const ConversationId& conv,
ChatMessage& out_message,
uint32_t* out_count = nullptr) const;
bool storedMessageMatches(const ChatMessage& message,
bool* out_matches,
uint32_t* out_count) const;
bool appendMessageRecord(const ChatMessage& message,
uint32_t sequence);
bool repairPartialJournal(const char* path,
MeshProtocol protocol,
storage::v2::JournalKind kind,
std::size_t slot_size) const;
struct IndexHeader
{
uint32_t magic = 0;
uint16_t version = 0;
uint16_t count = 0;
} __attribute__((packed));
bool appendCatalogProjection(
const storage::v2::ChatCatalogProjection& projection);
bool appendReadProjection(
const storage::v2::ChatReadProjection& projection);
bool appendStatusProjection(MeshProtocol protocol,
const storage::v2::ChatStatusProjection& projection);
bool appendSeenProjection(
const storage::v2::ReticulumSeenProjection& projection) const;
bool rememberReticulumHash(const uint8_t* hash);
struct IndexEntryV2
{
uint8_t protocol = 0;
uint8_t channel = 0;
uint8_t status = 0;
uint16_t unread = 0;
uint32_t peer = 0;
uint32_t last_msg_id = 0;
uint32_t last_timestamp = 0;
uint32_t last_from = 0;
uint16_t preview_len = 0;
char preview[kPreviewLen] = {};
} __attribute__((packed));
storage::v2::ChatCatalogProjection* findCatalog(
const ConversationId& conversation);
const storage::v2::ChatCatalogProjection* findCatalog(
const ConversationId& conversation) const;
storage::v2::ChatReadProjection* findReadState(
const ConversationId& conversation);
const storage::v2::ChatReadProjection* findReadState(
const ConversationId& conversation) const;
storage::v2::ChatStatusProjection* findStatus(MessageId message_id,
MeshProtocol protocol);
const storage::v2::ChatStatusProjection* findStatus(
MessageId message_id,
MeshProtocol protocol) const;
void applyStoredStatus(ChatMessage& message) const;
uint32_t countUnreadAfter(const ConversationId& conversation,
uint32_t last_read_sequence) const;
uint32_t sequenceForUnread(const ConversationId& conversation,
uint32_t unread) const;
struct IndexEntry
{
uint8_t protocol = 0;
uint8_t channel = 0;
uint8_t status = 0;
uint8_t flags = 0;
uint16_t unread = 0;
uint32_t peer = 0;
uint32_t last_msg_id = 0;
uint32_t last_timestamp = 0;
uint32_t last_from = 0;
uint16_t preview_len = 0;
uint8_t reticulum_destination_hash[kReticulumPeerHashSize] = {};
uint8_t reticulum_identity_hash[kReticulumPeerHashSize] = {};
char preview[kPreviewLen] = {};
} __attribute__((packed));
bool rewriteCatalogSnapshot(MeshProtocol protocol);
bool rewriteReadSnapshot(MeshProtocol protocol);
bool rewriteStatusSnapshot(MeshProtocol protocol);
bool compactProtocolProjections(MeshProtocol protocol);
bool rewriteJournalFromCatalog(MeshProtocol protocol,
const char* final_path,
const char* temp_path);
bool rewriteJournalFromReadState(MeshProtocol protocol,
const char* final_path,
const char* temp_path);
bool rewriteJournalFromStatus(MeshProtocol protocol,
const char* final_path,
const char* temp_path);
struct ReadStateHeader
{
uint32_t magic = 0;
uint16_t version = 0;
uint16_t count = 0;
} __attribute__((packed));
struct ReadStateEntry
{
uint8_t protocol = 0;
uint8_t channel = 0;
uint8_t flags = 0;
uint8_t reserved = 0;
uint16_t unread = 0;
uint16_t reserved2 = 0;
uint32_t peer = 0;
uint8_t reticulum_destination_hash[kReticulumPeerHashSize] = {};
uint8_t reticulum_identity_hash[kReticulumPeerHashSize] = {};
} __attribute__((packed));
struct LxmfSeenHeader
{
uint32_t magic = 0;
uint16_t version = 0;
uint16_t head = 0;
uint16_t count = 0;
} __attribute__((packed));
struct LxmfSeenRecord
{
uint8_t hash[kReticulumLxmfHashSize] = {};
} __attribute__((packed));
static constexpr uint32_t kFileMagic = 0x474F4C43; // "CLOG"
static constexpr uint32_t kIndexMagic = 0x54414843; // "CHAT"
static constexpr uint32_t kReadStateMagic = 0x44525343; // "CSRD"
static constexpr uint32_t kLxmfSeenMagic = 0x4E45584C; // "LXEN"
static constexpr uint16_t kLegacyVersion = 2;
static constexpr uint16_t kReticulumIdentityVersion = 3;
static constexpr uint16_t kRxOriginVersion = 4;
static constexpr uint16_t kFileVersion = 5;
static constexpr uint16_t kIndexVersion = 3;
static constexpr uint16_t kReadStateVersion = 1;
static constexpr uint16_t kLxmfSeenVersion = 1;
bool ensureFs() const;
bool ensureDir() const;
bool recoverIndex() const;
bool reconcileIndexUnread(std::vector<IndexEntry>& entries) const;
bool readIndex(std::vector<IndexEntry>& entries) const;
bool writeIndex(const std::vector<IndexEntry>& entries) const;
bool ensureIndex(std::vector<IndexEntry>& entries);
bool readReadState(std::vector<ReadStateEntry>& entries) const;
bool writeReadState(const std::vector<ReadStateEntry>& entries) const;
bool findReadStateEntry(const ConversationId& conv,
std::vector<ReadStateEntry>& entries,
size_t* out_idx) const;
bool findReadStateEntry(const ConversationId& conv,
const std::vector<ReadStateEntry>& entries,
size_t* out_idx) const;
bool readStateUnreadOnly(const ConversationId& conv, uint16_t* unread) const;
bool readStateUnreadOrLegacy(const ConversationId& conv, uint16_t* unread) const;
bool writeReadStateUnread(const ConversationId& conv, uint16_t unread) const;
bool removeReadStateEntry(const ConversationId& conv) const;
bool findIndexEntry(const ConversationId& conv,
std::vector<IndexEntry>& entries,
size_t* out_idx) const;
bool findIndexEntry(const ConversationId& conv,
const std::vector<IndexEntry>& entries,
size_t* out_idx) const;
void updateIndexForMessage(const ChatMessage& msg, uint16_t unread);
void rebuildIndex();
bool loadFileHeader(::platform::esp::arduino_common::storage::SdRuntimeFile& file,
FileHeader& header) const;
bool initFileHeader(::platform::esp::arduino_common::storage::SdRuntimeFile& file) const;
bool upgradeConversationFile(::platform::esp::arduino_common::storage::SdRuntimeFile& file,
const char* path,
FileHeader& header) const;
bool readRecord(::platform::esp::arduino_common::storage::SdRuntimeFile& file,
const FileHeader& header,
uint16_t slot,
Record& rec) const;
bool writeRecord(::platform::esp::arduino_common::storage::SdRuntimeFile& file,
uint16_t slot,
const Record& rec) const;
bool initLxmfSeenFile(::platform::esp::arduino_common::storage::SdRuntimeFile& file) const;
bool loadLxmfSeenHeader(::platform::esp::arduino_common::storage::SdRuntimeFile& file,
LxmfSeenHeader& header) const;
bool readLxmfSeenRecord(::platform::esp::arduino_common::storage::SdRuntimeFile& file,
uint16_t slot,
LxmfSeenRecord& rec) const;
bool writeLxmfSeenRecord(::platform::esp::arduino_common::storage::SdRuntimeFile& file,
uint16_t slot,
const LxmfSeenRecord& rec) const;
bool rememberReticulumLxmfMessageHash(const uint8_t* lxmf_hash) const;
bool conversationContainsReticulumLxmfHash(
::platform::esp::arduino_common::storage::SdRuntimeFile& file,
const FileHeader& header,
const uint8_t* lxmf_hash,
bool* found) const;
bool appendInternal(const ChatMessage& msg);
bool openConversationForUpdate(const ConversationId& conv,
::platform::esp::arduino_common::storage::SdRuntimeFile& file,
FileHeader& header) const;
bool readConversationUnread(const ConversationId& conv, uint16_t* unread) const;
bool writeConversationUnread(const ConversationId& conv, uint16_t unread) const;
void buildConversationPath(const ConversationId& conv, char* out, size_t out_len) const;
const char* channelName(ChannelId channel) const;
static ChatMessage messageFromRecord(const Record& rec);
static Record recordFromMessage(const ChatMessage& msg);
static bool indexEntryHasReticulumIdentity(const IndexEntry& entry);
static bool indexEntryMatchesConversation(const IndexEntry& entry,
const ConversationId& conv);
static ConversationId conversationFromIndexEntry(const IndexEntry& entry);
static ConversationMeta metaFromIndexEntry(const IndexEntry& entry);
static bool readStateEntryHasReticulumIdentity(const ReadStateEntry& entry);
static bool readStateEntryMatchesConversation(const ReadStateEntry& entry,
const ConversationId& conv);
static ReadStateEntry readStateEntryFromConversation(const ConversationId& conv,
uint16_t unread);
static bool hasLogSuffix(const char* name);
static MeshProtocol normalizeProtocol(MeshProtocol protocol);
static const char* protocolRoot(MeshProtocol protocol);
static const char* protocolSlug(MeshProtocol protocol);
static bool sameProtocol(MeshProtocol lhs, MeshProtocol rhs);
static bool sameStoredMessage(const ChatMessage& lhs,
const ChatMessage& rhs);
static ConversationMeta makeMeta(
const storage::v2::ChatCatalogProjection& projection);
static void buildConversationDirectory(const ConversationId& conversation,
char* out,
std::size_t out_len);
static void buildMessageSegmentPath(const ConversationId& conversation,
uint32_t segment,
char* out,
std::size_t out_len);
static void buildProjectionPath(MeshProtocol protocol,
const char* name,
char* out,
std::size_t out_len);
static bool ensureDirectory(const char* path);
static bool removeTree(const char* path);
storage::v2::FixedSlotJournalEngine journal_{};
CatalogList catalog_{};
ReadStateList read_state_{};
StatusList statuses_{};
mutable SeenList seen_hot_{};
mutable ScratchBuffer scratch_{};
bool projection_dirty_[3] = {};
uint8_t flush_protocol_cursor_ = 0;
uint32_t last_projection_retry_ms_ = 0;
bool ready_ = false;
bool unread_reconcile_pending_ = false;
};
} // namespace chat
@@ -0,0 +1,96 @@
#pragma once
#include <cstddef>
#include <cstdlib>
#include <limits>
#include <new>
#include <type_traits>
#if defined(ESP_PLATFORM)
#include <esp_heap_caps.h>
#endif
namespace platform::esp::arduino_common::memory
{
[[noreturn]] inline void psramAllocationFailed()
{
#if defined(__cpp_exceptions) || defined(__EXCEPTIONS)
throw std::bad_alloc();
#else
std::abort();
#endif
}
template <typename T>
class PsramAllocator
{
public:
using value_type = T;
using is_always_equal = std::true_type;
using propagate_on_container_move_assignment = std::true_type;
PsramAllocator() noexcept = default;
template <typename U>
PsramAllocator(const PsramAllocator<U>&) noexcept
{
}
T* allocate(std::size_t count)
{
if (count > std::numeric_limits<std::size_t>::max() / sizeof(T))
{
psramAllocationFailed();
}
const std::size_t bytes = count * sizeof(T);
if (bytes == 0)
{
return nullptr;
}
#if defined(ESP_PLATFORM)
void* ptr = heap_caps_malloc(bytes,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
#else
void* ptr = std::malloc(bytes);
#endif
if (!ptr)
{
psramAllocationFailed();
}
return static_cast<T*>(ptr);
}
void deallocate(T* ptr, std::size_t) noexcept
{
if (!ptr)
{
return;
}
#if defined(ESP_PLATFORM)
heap_caps_free(ptr);
#else
std::free(ptr);
#endif
}
template <typename U>
struct rebind
{
using other = PsramAllocator<U>;
};
};
template <typename T, typename U>
bool operator==(const PsramAllocator<T>&, const PsramAllocator<U>&) noexcept
{
return true;
}
template <typename T, typename U>
bool operator!=(const PsramAllocator<T>&, const PsramAllocator<U>&) noexcept
{
return false;
}
} // namespace platform::esp::arduino_common::memory
@@ -16,6 +16,7 @@
#include "chat/runtime/self_identity_policy.h"
#include "platform/esp/arduino_common/app_tasks.h"
#include "platform/esp/arduino_common/memory_diag.h"
#include "platform/ui/reticulum_call_runtime.h"
#include "platform/ui/reticulum_directory_runtime.h"
#include "platform/ui/reticulum_group_config_runtime.h"
#include "sys/event_bus.h"
@@ -222,16 +223,27 @@ void AppContext::initContactServices()
return;
}
auto contact_services = platform_bindings_.create_contact_services();
if (!mesh_peer_directory_)
{
Serial.printf("[AppContext] protocol peer repository unavailable\n");
return;
}
auto contact_services =
platform_bindings_.create_contact_services(*mesh_peer_directory_);
if (!contact_services.isValid())
{
Serial.printf("[AppContext] contact service bundle invalid\n");
return;
}
node_store_ = std::move(contact_services.node_store);
contact_store_ = std::move(contact_services.contact_store);
node_store_ = &mesh_peer_directory_->nodeStoreView();
contact_store_ = &mesh_peer_directory_->contactStoreView();
contact_service_ = std::move(contact_services.service);
if (contact_service_)
{
contact_service_->setActiveProtocol(config_.mesh_protocol);
}
}
chat::IMeshAdapter* AppContext::getMeshAdapter()
@@ -582,6 +594,10 @@ bool AppContext::switchMeshProtocol(chat::MeshProtocol protocol, bool persist)
{
chat_service_->setActiveProtocol(normalized);
}
if (contact_service_)
{
contact_service_->setActiveProtocol(normalized);
}
if (persist)
{
@@ -629,6 +645,11 @@ void AppContext::updateCoreServices()
{
event_runtime_hooks_.update_core_services(*this);
}
if (mesh_peer_directory_ &&
!::platform::ui::reticulum_call::resource_preempt_active())
{
(void)mesh_peer_directory_->flush();
}
}
void AppContext::tickEventRuntime()
@@ -6,26 +6,22 @@
#include "app/app_facades.h"
#include "board/GpsBoard.h"
#include "board/MotionBoard.h"
#include "chat/infra/mesh_peer_directory_core.h"
#include "chat/infra/store/ram_store.h"
#include "chat/usecase/contact_service.h"
#include "platform/esp/arduino_common/chat/infra/chat_event_bus_bridge.h"
#include "platform/esp/arduino_common/chat/infra/contact_store.h"
#include "platform/esp/arduino_common/chat/infra/mesh_adapter_router.h"
#include "platform/esp/arduino_common/chat/infra/meshtastic/node_store.h"
#include "platform/esp/arduino_common/chat/infra/protocol_factory.h"
#include "platform/esp/arduino_common/chat/infra/store/sd_protocol_peer_repository.h"
#include "platform/esp/arduino_common/chat/infra/store/sd_store.h"
#include "platform/esp/arduino_common/device_identity.h"
#include "platform/esp/arduino_common/gps/gps_service.h"
#include "platform/esp/arduino_common/gps/track_recorder.h"
#include "platform/esp/arduino_common/storage/persistence_bus_gate.h"
#include "platform/esp/arduino_common/storage/sd_card_runtime.h"
#include "platform/esp/arduino_common/team/crypto/team_crypto.h"
#include "platform/esp/arduino_common/team/event/team_app_data_event_bus_bridge.h"
#include "platform/esp/arduino_common/team/event/team_event_bus_sink.h"
#include "platform/esp/arduino_common/team/event/team_pairing_event_bus_sink.h"
#include "platform/esp/arduino_common/team_platform_bundle.h"
#include "platform/esp/common/shared_spi_bus_arbiter.h"
#include "platform/ui/team_ui_store_runtime.h"
#include "team/usecase/team_controller.h"
#include "team/usecase/team_track_sampler.h"
@@ -34,37 +30,10 @@
#include "freertos/FreeRTOS.h"
#include <new>
#include <vector>
namespace
{
constexpr uint32_t kMeshPeerDirectorySdWaitMs = 50;
constexpr const char* kMeshPeerDirectoryDir = "/mesh";
constexpr const char* kMeshPeerDirectoryPath = "/mesh/peers.bin";
constexpr const char* kMeshPeerDirectoryTempPath = "/mesh/peers.tmp";
constexpr std::size_t kMeshPeerDirectoryMaxBlobBytes = 768U * 1024U;
constexpr uint32_t kMeshPeerDirectoryBusResource = 5;
constexpr uint32_t kMeshPeerDirectoryBusOwnerId = 0x4D504452u; // 'MPDR'
::platform::esp::common::SharedSpiBusAdapter s_mesh_peer_directory_load_adapter(
"mesh_peer_dir_load",
kMeshPeerDirectoryBusOwnerId);
::platform::esp::common::SharedSpiBusAdapter s_mesh_peer_directory_save_adapter(
"mesh_peer_dir_save",
kMeshPeerDirectoryBusOwnerId);
::platform::esp::common::FixedSharedSpiBusPolicyStrategy s_mesh_peer_directory_bus_policy(
kMeshPeerDirectorySdWaitMs,
kMeshPeerDirectorySdWaitMs,
kMeshPeerDirectorySdWaitMs,
kMeshPeerDirectorySdWaitMs);
sys::runtime::StorageBusArbiter s_mesh_peer_directory_load_arbiter(
s_mesh_peer_directory_load_adapter,
s_mesh_peer_directory_bus_policy);
sys::runtime::StorageBusArbiter s_mesh_peer_directory_save_arbiter(
s_mesh_peer_directory_save_adapter,
s_mesh_peer_directory_bus_policy);
gps::GpsReceiverInitConfig make_receiver_init_config(const app::AppConfig& config)
{
gps::GpsReceiverInitConfig init{};
@@ -151,7 +120,7 @@ std::unique_ptr<chat::IChatStore> create_chat_store()
std::unique_ptr<chat::SdStore> sd_store(new chat::SdStore());
if (sd_store && sd_store->isReady())
{
Serial.printf("[AppContext] chat store=SdStore backend=%s layout=/chat/index.bin+/chat/*.log\n",
Serial.printf("[AppContext] chat store=SdStore backend=%s layout=/data/v2/{mt,mc,rt}/chat\n",
::platform::esp::arduino_common::storage::sd_card_backend_name());
return std::unique_ptr<chat::IChatStore>(sd_store.release());
}
@@ -163,237 +132,19 @@ std::unique_ptr<chat::IChatStore> create_chat_store()
return std::unique_ptr<chat::IChatStore>(new chat::RamStore());
}
bool ensure_mesh_peer_directory_dir()
std::unique_ptr<chat::IProtocolPeerRepository> create_mesh_peer_directory(
chat::IChatStore& chat_store)
{
using namespace ::platform::esp::arduino_common::storage;
return sd_exists(kMeshPeerDirectoryDir) || sd_mkdir(kMeshPeerDirectoryDir);
}
class EspSdMeshPeerDirectoryBlobStore final
: public chat::IMeshPeerDirectoryBlobStore
{
public:
chat::MeshPeerDirectoryBlobLoadResult loadBlob(
std::vector<uint8_t>& out) override
std::unique_ptr<chat::IProtocolPeerRepository> repository(
new (std::nothrow) chat::SdProtocolPeerRepository(chat_store));
if (!repository)
{
using namespace ::platform::esp::arduino_common::storage;
out.clear();
if (!sd_card_ready())
{
return chat::MeshPeerDirectoryBlobLoadResult::Unavailable;
}
if (!sd_exists(kMeshPeerDirectoryPath))
{
return chat::MeshPeerDirectoryBlobLoadResult::Missing;
}
::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate(
s_mesh_peer_directory_load_arbiter,
sys::runtime::BusAccessPolicy::BackgroundWorkerBounded,
kMeshPeerDirectorySdWaitMs,
kMeshPeerDirectoryBusResource,
kMeshPeerDirectoryBusOwnerId + 1,
kMeshPeerDirectoryBusOwnerId);
if (!bus_gate.locked())
{
return chat::MeshPeerDirectoryBlobLoadResult::Unavailable;
}
SdRuntimeFile file;
if (!file.open(kMeshPeerDirectoryPath, "r"))
{
return chat::MeshPeerDirectoryBlobLoadResult::IoError;
}
const uint64_t size = file.size();
if (size == 0 || size > kMeshPeerDirectoryMaxBlobBytes)
{
file.close();
return chat::MeshPeerDirectoryBlobLoadResult::IoError;
}
out.resize(static_cast<std::size_t>(size));
const int read = file.read(out.data(), out.size());
file.close();
if (read < 0 || static_cast<std::size_t>(read) != out.size())
{
out.clear();
return chat::MeshPeerDirectoryBlobLoadResult::IoError;
}
return chat::MeshPeerDirectoryBlobLoadResult::Loaded;
return repository;
}
bool saveBlob(const uint8_t* data, std::size_t len) override
{
using namespace ::platform::esp::arduino_common::storage;
if (!sd_card_ready() || (!data && len > 0) ||
len > kMeshPeerDirectoryMaxBlobBytes || !ensure_mesh_peer_directory_dir())
{
return false;
}
::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate(
s_mesh_peer_directory_save_arbiter,
sys::runtime::BusAccessPolicy::DurableCommit,
kMeshPeerDirectorySdWaitMs,
kMeshPeerDirectoryBusResource,
kMeshPeerDirectoryBusOwnerId + 2,
kMeshPeerDirectoryBusOwnerId);
if (!bus_gate.locked())
{
return false;
}
if (sd_exists(kMeshPeerDirectoryTempPath))
{
sd_remove(kMeshPeerDirectoryTempPath);
}
SdRuntimeFile file;
if (!file.open(kMeshPeerDirectoryTempPath, "w"))
{
return false;
}
bool ok = true;
if (len > 0)
{
ok = file.write(data, len) == len;
}
file.close();
if (!ok)
{
sd_remove(kMeshPeerDirectoryTempPath);
return false;
}
if (sd_exists(kMeshPeerDirectoryPath))
{
sd_remove(kMeshPeerDirectoryPath);
}
if (!sd_rename(kMeshPeerDirectoryTempPath, kMeshPeerDirectoryPath))
{
sd_remove(kMeshPeerDirectoryTempPath);
return false;
}
return true;
}
void clearBlob() override
{
using namespace ::platform::esp::arduino_common::storage;
if (!sd_card_ready())
{
return;
}
if (sd_exists(kMeshPeerDirectoryTempPath))
{
sd_remove(kMeshPeerDirectoryTempPath);
}
if (sd_exists(kMeshPeerDirectoryPath))
{
sd_remove(kMeshPeerDirectoryPath);
}
}
};
class EspSdMeshPeerDirectory final : public chat::IMeshPeerDirectory
{
public:
EspSdMeshPeerDirectory()
: core_(blob_store_)
{
}
chat::MeshPeerDirectoryStatus begin() override
{
return core_.begin();
}
chat::MeshPeerDirectoryStatus record(
const chat::MeshPeerRecord& record) override
{
return core_.record(record);
}
chat::MeshPeerDirectoryStatus find(
const chat::MeshPeerIdentity& identity,
chat::MeshPeerRecord& out_record) override
{
return core_.find(identity, out_record);
}
chat::MeshPeerDirectoryStatus findByNodeId(
chat::MeshProtocol protocol,
chat::NodeId node_id,
chat::MeshPeerRecord& out_record) override
{
return core_.findByNodeId(protocol, node_id, out_record);
}
chat::MeshPeerDirectoryStatus loadRecent(
chat::MeshProtocol protocol,
chat::MeshPeerRecord* out_records,
std::size_t max_records,
std::size_t* out_count) override
{
return core_.loadRecent(protocol, out_records, max_records, out_count);
}
chat::MeshPeerDirectoryStatus search(chat::MeshProtocol protocol,
const char* query,
chat::MeshPeerRecord* out_records,
std::size_t max_records,
std::size_t* out_count) override
{
return core_.search(protocol, query, out_records, max_records, out_count);
}
chat::MeshPeerDirectoryStatus setUserFlags(
const chat::MeshPeerIdentity& identity,
const chat::MeshPeerUserFlags& flags) override
{
return core_.setUserFlags(identity, flags);
}
chat::MeshPeerDirectoryStatus remove(
const chat::MeshPeerIdentity& identity) override
{
return core_.remove(identity);
}
chat::MeshPeerDirectoryStatus clearProtocol(
chat::MeshProtocol protocol) override
{
return core_.clearProtocol(protocol);
}
chat::MeshPeerDirectoryCapacity capacityFor(
chat::MeshProtocol protocol) const override
{
return core_.capacityFor(protocol);
}
chat::MeshPeerDirectoryStatus flush() override
{
return core_.flush();
}
private:
EspSdMeshPeerDirectoryBlobStore blob_store_;
chat::MeshPeerDirectoryCore core_;
};
std::unique_ptr<chat::IMeshPeerDirectory> create_mesh_peer_directory()
{
std::unique_ptr<chat::IMeshPeerDirectory> directory(
new (std::nothrow) EspSdMeshPeerDirectory());
if (!directory)
{
return directory;
}
const auto status = directory->begin();
Serial.printf("[MeshPeerDirectory] backend=sd path=%s status=%u\n",
kMeshPeerDirectoryPath,
const auto status = repository->begin();
Serial.printf("[PeerStoreV2] backend=sd root=/data/v2 status=%u\n",
static_cast<unsigned>(status.code));
return directory;
return repository;
}
std::unique_ptr<chat::IMeshAdapter> create_mesh_backend(chat::MeshProtocol protocol,
@@ -403,24 +154,21 @@ std::unique_ptr<chat::IMeshAdapter> create_mesh_backend(chat::MeshProtocol proto
return chat::ProtocolFactory::createAdapter(protocol, lora_board, peer_directory);
}
app::ContactServicesBundle create_contact_services()
app::ContactServicesBundle create_contact_services(
chat::IProtocolPeerRepository& repository)
{
app::ContactServicesBundle bundle;
bundle.node_store = std::unique_ptr<chat::contacts::INodeStore>(new chat::meshtastic::NodeStore());
bundle.contact_store = std::unique_ptr<chat::contacts::IContactStore>(new chat::contacts::ContactStore());
if (!bundle.node_store || !bundle.contact_store)
{
return bundle;
}
bundle.service = std::unique_ptr<chat::contacts::ContactService>(
new chat::contacts::ContactService(*bundle.node_store, *bundle.contact_store));
new chat::contacts::ContactService(repository.nodeStoreView(),
repository.contactStoreView()));
if (bundle.service)
{
bundle.service->begin();
Serial.printf("[ContactService] startup nodes=%u nicknames=%u\n",
static_cast<unsigned>(bundle.node_store->getEntries().size()),
static_cast<unsigned>(bundle.contact_store->getCount()));
static_cast<unsigned>(
repository.nodeStoreView().getEntries().size()),
static_cast<unsigned>(
repository.contactStoreView().getCount()));
}
return bundle;
}
@@ -445,7 +193,11 @@ app::ChatServicesBundle create_chat_services(const app::AppConfig& config,
bundle.model->setPolicy(config.chat_policy);
bundle.store = create_chat_store();
bundle.mesh_peer_directory = create_mesh_peer_directory();
if (!bundle.store)
{
return bundle;
}
bundle.mesh_peer_directory = create_mesh_peer_directory(*bundle.store);
bundle.mesh_runtime = create_mesh_runtime();
if (!bundle.store || !bundle.mesh_peer_directory || !bundle.mesh_runtime)
{
@@ -1,246 +0,0 @@
/**
* @file contact_store.cpp
* @brief Contact nickname storage shell implementation
*/
#include "platform/esp/arduino_common/chat/infra/contact_store.h"
#include "../internal/blob_store_io.h"
#include "platform/esp/arduino_common/storage/persistence_bus_gate.h"
#include "platform/esp/arduino_common/storage/sd_card_runtime.h"
#include "platform/esp/common/shared_spi_bus_arbiter.h"
#include <Arduino.h>
#include <esp_heap_caps.h>
#include <new>
namespace chat
{
namespace contacts
{
#ifndef CONTACT_STORE_LOG_ENABLE
#define CONTACT_STORE_LOG_ENABLE 1
#endif
#if CONTACT_STORE_LOG_ENABLE
#define CONTACT_STORE_LOG(...) Serial.printf(__VA_ARGS__)
#else
#define CONTACT_STORE_LOG(...)
#endif
void* ContactStore::operator new(std::size_t size)
{
void* ptr = heap_caps_malloc_prefer(size,
2,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT,
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
return ptr != nullptr ? ptr : ::operator new(size);
}
void ContactStore::operator delete(void* ptr) noexcept
{
heap_caps_free(ptr);
}
void ContactStore::operator delete(void* ptr, std::size_t) noexcept
{
operator delete(ptr);
}
namespace
{
constexpr uint32_t kSdLoadWaitMs = 250;
constexpr uint32_t kSdPersistWaitMs = 750;
constexpr uint32_t kContactStoreBusResource = 5;
constexpr uint32_t kContactStoreBusOwnerId = 0x434F4E54u; // 'CONT'
constexpr const char* kContactStoreBusOwner = "contact_store_sd";
::platform::esp::common::SharedSpiBusAdapter s_contact_store_bus_adapter(
kContactStoreBusOwner,
kContactStoreBusOwnerId);
::platform::esp::common::FixedSharedSpiBusPolicyStrategy s_contact_store_bus_policy(
kSdLoadWaitMs,
kSdLoadWaitMs,
kSdLoadWaitMs,
kSdLoadWaitMs);
sys::runtime::StorageBusArbiter s_contact_store_bus_arbiter(
s_contact_store_bus_adapter,
s_contact_store_bus_policy);
} // namespace
ContactStore::ContactStore()
: core_(*this)
{
}
void ContactStore::begin()
{
backend_ = ::platform::esp::arduino_common::storage::sd_card_ready()
? StorageBackend::Sd
: StorageBackend::Flash;
CONTACT_STORE_LOG("[ContactStore] backend=%s\n",
backend_ == StorageBackend::Sd ? "sd" : "flash");
core_.begin();
}
std::string ContactStore::getNickname(uint32_t node_id) const
{
return core_.getNickname(node_id);
}
bool ContactStore::setNickname(uint32_t node_id, const char* nickname)
{
return core_.setNickname(node_id, nickname);
}
bool ContactStore::removeNickname(uint32_t node_id)
{
return core_.removeNickname(node_id);
}
bool ContactStore::hasNickname(const char* nickname) const
{
return core_.hasNickname(nickname);
}
std::vector<uint32_t> ContactStore::getAllContactIds() const
{
return core_.getAllContactIds();
}
size_t ContactStore::getCount() const
{
return core_.getCount();
}
bool ContactStore::loadBlob(std::vector<uint8_t>& out)
{
if (backend_ == StorageBackend::Sd)
{
const LoadResult sd_result = loadFromSD(out);
if (sd_result == LoadResult::Loaded)
{
CONTACT_STORE_LOG("[ContactStore] load source=sd path=%s len=%u\n",
kSdPath,
static_cast<unsigned>(out.size()));
return true;
}
if (sd_result == LoadResult::Busy)
{
CONTACT_STORE_LOG("[ContactStore] load source=none reason=sd_busy\n");
out.clear();
return false;
}
std::vector<uint8_t> fallback;
if (loadFromFlash(fallback))
{
CONTACT_STORE_LOG("[ContactStore] load source=flash fallback=sd_miss ns=%s len=%u\n",
kPrefNs,
static_cast<unsigned>(fallback.size()));
const bool migrated = saveToSD(fallback.data(), fallback.size());
CONTACT_STORE_LOG("[ContactStore] migrate source=flash target=sd path=%s len=%u ok=%u\n",
kSdPath,
static_cast<unsigned>(fallback.size()),
migrated ? 1U : 0U);
if (migrated)
{
clearFlash();
}
out.swap(fallback);
return true;
}
}
else
{
if (loadFromFlash(out))
{
CONTACT_STORE_LOG("[ContactStore] load source=flash ns=%s len=%u\n",
kPrefNs,
static_cast<unsigned>(out.size()));
return true;
}
}
out.clear();
CONTACT_STORE_LOG("[ContactStore] load source=none\n");
return false;
}
bool ContactStore::saveBlob(const uint8_t* data, size_t len)
{
if (backend_ == StorageBackend::Sd)
{
const bool ok = saveToSD(data, len);
CONTACT_STORE_LOG("[ContactStore] save target=sd path=%s len=%u ok=%u\n",
kSdPath,
static_cast<unsigned>(len),
ok ? 1U : 0U);
return ok;
}
const bool ok = saveToFlash(data, len);
CONTACT_STORE_LOG("[ContactStore] save target=flash ns=%s len=%u ok=%u\n",
kPrefNs,
static_cast<unsigned>(len),
ok ? 1U : 0U);
return ok;
}
ContactStore::LoadResult ContactStore::loadFromSD(std::vector<uint8_t>& out) const
{
if (!::platform::esp::arduino_common::storage::sd_card_ready())
{
out.clear();
return LoadResult::MissingOrInvalid;
}
::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate(
s_contact_store_bus_arbiter,
sys::runtime::BusAccessPolicy::BackgroundWorkerBounded,
kSdLoadWaitMs,
kContactStoreBusResource,
kContactStoreBusOwnerId + 1,
kContactStoreBusOwnerId);
if (!bus_gate.locked())
{
out.clear();
return LoadResult::Busy;
}
constexpr size_t kMaxContactBlobBytes =
ContactStoreCore::kMaxContacts * ContactStoreCore::kSerializedEntrySize;
return chat::infra::loadRawBlobFromSd(kSdPath, out, kMaxContactBlobBytes)
? LoadResult::Loaded
: LoadResult::MissingOrInvalid;
}
bool ContactStore::saveToSD(const uint8_t* data, size_t len) const
{
::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate(
s_contact_store_bus_arbiter,
sys::runtime::BusAccessPolicy::DurableCommit,
kSdPersistWaitMs,
kContactStoreBusResource,
kContactStoreBusOwnerId + 2,
kContactStoreBusOwnerId);
if (!bus_gate.locked())
{
CONTACT_STORE_LOG("[ContactStore] save SD skipped: spi busy len=%u\n",
static_cast<unsigned>(len));
return false;
}
return chat::infra::saveRawBlobToSd(kSdPath, data, len);
}
bool ContactStore::loadFromFlash(std::vector<uint8_t>& out) const
{
return chat::infra::loadRawBlobFromPreferences(kPrefNs, kPrefKey, out);
}
bool ContactStore::saveToFlash(const uint8_t* data, size_t len) const { return chat::infra::saveRawBlobToPreferences(kPrefNs, kPrefKey, data, len); }
void ContactStore::clearFlash() const
{
chat::infra::clearRawBlobFromPreferences(kPrefNs, kPrefKey);
}
} // namespace contacts
} // namespace chat
@@ -1677,6 +1677,8 @@ void MeshCoreAdapter::savePeerPubKeyToDirectory(const PeerRouteEntry& route)
record.meshcore.public_key_verified = route.pubkey_verified;
record.meshcore.has_peer_hash = true;
record.meshcore.peer_hash = route.peer_hash;
record.meshcore.node_id_hint =
deriveNodeIdFromPubkey(route.pubkey, sizeof(route.pubkey));
const MeshPeerDirectoryStatus status = peer_directory_->record(record);
if (!status.succeeded())
@@ -1,853 +0,0 @@
/**
* @file node_store.cpp
* @brief Persisted NodeInfo store shell backed by a single selected persistence backend
*/
#include "platform/esp/arduino_common/chat/infra/meshtastic/node_store.h"
#include "../../internal/blob_store_io.h"
#include "chat/infra/node_store_blob_format.h"
#include "platform/esp/arduino_common/storage/persistence_bus_gate.h"
#include "platform/esp/arduino_common/storage/sd_card_runtime.h"
#include "platform/esp/common/shared_spi_bus_arbiter.h"
#include <Arduino.h>
#include <algorithm>
#include <cstdio>
#include <esp_err.h>
#include <esp_heap_caps.h>
#include <new>
#include <nvs.h>
#include <string>
namespace chat
{
namespace meshtastic
{
#ifndef NODE_STORE_LOG_ENABLE
#define NODE_STORE_LOG_ENABLE 1
#endif
#if NODE_STORE_LOG_ENABLE
#define NODE_STORE_LOG(...) Serial.printf(__VA_ARGS__)
#else
#define NODE_STORE_LOG(...)
#endif
void* NodeStore::operator new(std::size_t size)
{
void* ptr = heap_caps_malloc_prefer(size,
2,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT,
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
return ptr != nullptr ? ptr : ::operator new(size);
}
void NodeStore::operator delete(void* ptr) noexcept
{
heap_caps_free(ptr);
}
void NodeStore::operator delete(void* ptr, std::size_t) noexcept
{
operator delete(ptr);
}
namespace
{
constexpr TickType_t kAsyncSaveMutexWait = pdMS_TO_TICKS(20);
constexpr TickType_t kAsyncSavePollInterval = pdMS_TO_TICKS(10);
constexpr TickType_t kAsyncSaveRetryDelay = pdMS_TO_TICKS(500);
constexpr uint32_t kSdLoadWaitMs = 250;
constexpr uint32_t kSdPersistWaitMs = 100;
constexpr uint32_t kAsyncSaveTaskStackBytes = 4 * 1024;
constexpr UBaseType_t kAsyncSaveTaskPriority = 2;
constexpr size_t kSdReadChunkBytes = 256;
constexpr uint32_t kNodeStoreBusResource = 5;
constexpr uint32_t kNodeStoreBusOwnerId = 0x4E4F4445u; // 'NODE'
constexpr const char* kNodeStoreBusOwner = "node_store_sd";
::platform::esp::common::SharedSpiBusAdapter s_node_store_bus_adapter(
kNodeStoreBusOwner,
kNodeStoreBusOwnerId);
::platform::esp::common::FixedSharedSpiBusPolicyStrategy s_node_store_bus_policy(
kSdLoadWaitMs,
kSdLoadWaitMs,
kSdLoadWaitMs,
kSdLoadWaitMs);
sys::runtime::StorageBusArbiter s_node_store_bus_arbiter(s_node_store_bus_adapter,
s_node_store_bus_policy);
void logNvsStats(const char* tag, const char* ns)
{
nvs_stats_t stats{};
esp_err_t err = nvs_get_stats(nullptr, &stats);
if (err == ESP_OK)
{
NODE_STORE_LOG("[NodeStore] NVS stats(%s): used=%u free=%u total=%u namespaces=%u\n",
tag ? tag : "?",
static_cast<unsigned>(stats.used_entries),
static_cast<unsigned>(stats.free_entries),
static_cast<unsigned>(stats.total_entries),
static_cast<unsigned>(stats.namespace_count));
}
else
{
NODE_STORE_LOG("[NodeStore] NVS stats(%s) err=%s\n",
tag ? tag : "?",
esp_err_to_name(err));
}
if (ns && ns[0])
{
nvs_handle_t handle;
err = nvs_open(ns, NVS_READONLY, &handle);
if (err == ESP_OK)
{
NODE_STORE_LOG("[NodeStore] NVS open ns=%s ok\n", ns);
nvs_close(handle);
}
else
{
NODE_STORE_LOG("[NodeStore] NVS open ns=%s err=%s\n", ns, esp_err_to_name(err));
}
}
}
bool canonicalizeNodePayload(const uint8_t* data, size_t len, uint8_t version, std::vector<uint8_t>& out)
{
std::vector<contacts::NodeEntry> entries;
if (!contacts::NodeStoreCore::decodeBlob(entries, data, len, version))
{
out.clear();
return false;
}
contacts::NodeStoreCore::encodeBlob(out, entries);
return true;
}
} // namespace
NodeStore::NodeStore()
: core_(*this)
{
}
void NodeStore::begin()
{
backend_ = ::platform::esp::arduino_common::storage::sd_card_ready()
? StorageBackend::Sd
: StorageBackend::Nvs;
NODE_STORE_LOG("[NodeStore] backend=%s\n",
backend_ == StorageBackend::Sd ? "sd" : "nvs");
core_.begin();
}
void NodeStore::applyUpdate(uint32_t node_id, const contacts::NodeUpdate& update)
{
core_.applyUpdate(node_id, update);
}
void NodeStore::upsert(uint32_t node_id, const char* short_name, const char* long_name,
uint32_t now_secs, float snr, float rssi, uint8_t protocol,
uint8_t role, uint8_t hops_away, uint8_t hw_model, uint8_t channel)
{
NODE_STORE_LOG("[NodeStore] upsert node=%08lX ts=%lu snr=%.1f rssi=%.1f\n",
static_cast<unsigned long>(node_id),
static_cast<unsigned long>(now_secs),
snr,
rssi);
core_.upsert(node_id, short_name, long_name, now_secs, snr, rssi,
protocol, role, hops_away, hw_model, channel);
}
void NodeStore::updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs)
{
core_.updateProtocol(node_id, protocol, now_secs);
}
void NodeStore::updatePosition(uint32_t node_id, const contacts::NodePosition& position)
{
core_.updatePosition(node_id, position);
}
bool NodeStore::remove(uint32_t node_id)
{
const bool removed = core_.remove(node_id);
if (removed)
{
NODE_STORE_LOG("[NodeStore] remove node=%08lX remaining=%u\n",
static_cast<unsigned long>(node_id),
static_cast<unsigned>(core_.getEntries().size()));
}
return removed;
}
const std::vector<contacts::NodeEntry>& NodeStore::getEntries() const
{
return core_.getEntries();
}
void NodeStore::clear()
{
core_.clear();
}
bool NodeStore::flush()
{
const bool accepted = core_.flush();
const bool persisted = waitForAsyncSave(pdMS_TO_TICKS(1500));
return accepted && persisted;
}
bool NodeStore::loadBlob(std::vector<uint8_t>& out)
{
out.clear();
if (backend_ == StorageBackend::Sd)
{
const LoadResult sd_result = loadFromSd(out);
if (sd_result == LoadResult::Loaded)
{
NODE_STORE_LOG("[NodeStore] load source=sd path=%s len=%u\n",
kPersistNodesFile,
static_cast<unsigned>(out.size()));
return true;
}
if (sd_result == LoadResult::Busy)
{
NODE_STORE_LOG("[NodeStore] load source=none reason=sd_busy\n");
return false;
}
std::vector<uint8_t> fallback;
if (loadFromNvs(fallback))
{
NODE_STORE_LOG("[NodeStore] load source=nvs fallback=sd_miss ns=%s len=%u\n",
kPersistNodesNs,
static_cast<unsigned>(fallback.size()));
const bool migrated = saveToSd(fallback.data(), fallback.size());
NODE_STORE_LOG("[NodeStore] migrate source=nvs target=sd path=%s len=%u ok=%u\n",
kPersistNodesFile,
static_cast<unsigned>(fallback.size()),
migrated ? 1U : 0U);
if (migrated)
{
clearNvs();
}
out.swap(fallback);
return true;
}
}
else
{
if (loadFromNvs(out))
{
NODE_STORE_LOG("[NodeStore] load source=nvs ns=%s len=%u\n",
kPersistNodesNs,
static_cast<unsigned>(out.size()));
return true;
}
}
NODE_STORE_LOG("[NodeStore] load source=none\n");
return false;
}
bool NodeStore::saveBlob(const uint8_t* data, size_t len)
{
return enqueueAsyncSave(data, len);
}
void NodeStore::clearBlob()
{
if (async_save_mutex_ != nullptr &&
xSemaphoreTake(async_save_mutex_, kAsyncSaveMutexWait) == pdTRUE)
{
pending_save_blob_.clear();
async_save_pending_ = false;
async_save_failed_ = false;
xSemaphoreGive(async_save_mutex_);
}
if (::platform::esp::arduino_common::storage::sd_card_ready())
{
::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate(
s_node_store_bus_arbiter,
sys::runtime::BusAccessPolicy::DurableCommit,
kSdPersistWaitMs,
kNodeStoreBusResource,
kNodeStoreBusOwnerId + 3,
kNodeStoreBusOwnerId);
if (bus_gate.locked() &&
::platform::esp::arduino_common::storage::sd_exists(kPersistNodesFile))
{
::platform::esp::arduino_common::storage::sd_remove(kPersistNodesFile);
}
}
clearNvs();
}
bool NodeStore::loadFromNvs(std::vector<uint8_t>& out)
{
out.clear();
chat::infra::PreferencesBlobMetadata meta;
if (!chat::infra::loadRawBlobFromPreferencesWithMetadata(kPersistNodesNs,
kPersistNodesKey,
kPersistNodesKeyVer,
kPersistNodesKeyCrc,
out,
&meta))
{
NODE_STORE_LOG("[NodeStore] NVS blob missing/unreadable ns=%s\n", kPersistNodesNs);
logNvsStats("begin", kPersistNodesNs);
return false;
}
NODE_STORE_LOG("[NodeStore] blob len=%u ver=%u crc=%08lX has_crc=%u\n",
static_cast<unsigned>(meta.len),
static_cast<unsigned>(meta.version),
static_cast<unsigned long>(meta.crc),
meta.has_crc ? 1U : 0U);
if (meta.len == 0)
{
if (meta.has_crc || meta.has_version)
{
NODE_STORE_LOG("[NodeStore] stale meta detected ver=%u has_crc=%u\n",
static_cast<unsigned>(meta.version),
meta.has_crc ? 1U : 0U);
}
NODE_STORE_LOG("[NodeStore] NVS backup empty\n");
return false;
}
if (!meta.has_version)
{
NODE_STORE_LOG("[NodeStore] missing version len=%u\n",
static_cast<unsigned>(meta.len),
static_cast<unsigned>(meta.len));
out.clear();
return false;
}
const size_t entry_size = contacts::nodeBlobEntrySizeForVersion(meta.version);
if (entry_size == 0 ||
meta.len == 0 ||
(meta.len % entry_size) != 0 ||
(meta.len / entry_size) > contacts::NodeStoreCore::kMaxNodes)
{
NODE_STORE_LOG("[NodeStore] invalid blob shape len=%u ver=%u\n",
static_cast<unsigned>(meta.len),
static_cast<unsigned>(meta.version));
out.clear();
return false;
}
if (!meta.has_crc)
{
NODE_STORE_LOG("[NodeStore] missing crc\n");
out.clear();
return false;
}
const uint32_t calc_crc = contacts::NodeStoreCore::computeBlobCrc(out.data(), out.size());
if (calc_crc != meta.crc)
{
NODE_STORE_LOG("[NodeStore] crc mismatch stored=%08lX calc=%08lX\n",
static_cast<unsigned long>(meta.crc),
static_cast<unsigned long>(calc_crc));
out.clear();
return false;
}
std::vector<uint8_t> normalized;
if (!canonicalizeNodePayload(out.data(), out.size(), meta.version, normalized))
{
NODE_STORE_LOG("[NodeStore] decode failed ver=%u len=%u\n",
static_cast<unsigned>(meta.version),
static_cast<unsigned>(out.size()));
out.clear();
return false;
}
out.swap(normalized);
NODE_STORE_LOG("[NodeStore] loaded=%u\n",
static_cast<unsigned>(out.size() / contacts::NodeStoreCore::kSerializedEntrySize));
return true;
}
NodeStore::LoadResult NodeStore::loadFromSd(std::vector<uint8_t>& out) const
{
if (!::platform::esp::arduino_common::storage::sd_card_ready())
{
NODE_STORE_LOG("[NodeStore] load SD skipped: card none\n");
return LoadResult::MissingOrInvalid;
}
::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate(
s_node_store_bus_arbiter,
sys::runtime::BusAccessPolicy::BackgroundWorkerBounded,
kSdLoadWaitMs,
kNodeStoreBusResource,
kNodeStoreBusOwnerId + 1,
kNodeStoreBusOwnerId);
if (!bus_gate.locked())
{
NODE_STORE_LOG("[NodeStore] load SD skipped: spi busy\n");
return LoadResult::Busy;
}
::platform::esp::arduino_common::storage::SdRuntimeFile file;
if (!file.open(kPersistNodesFile, "r"))
{
NODE_STORE_LOG("[NodeStore] load SD open failed path=%s\n", kPersistNodesFile);
return LoadResult::MissingOrInvalid;
}
const size_t file_size = static_cast<size_t>(file.size());
if (file_size < sizeof(contacts::NodeStoreSdHeader))
{
NODE_STORE_LOG("[NodeStore] load SD short file path=%s size=%u\n",
kPersistNodesFile,
static_cast<unsigned>(file_size));
file.close();
return LoadResult::MissingOrInvalid;
}
contacts::NodeStoreSdHeader header{};
if (file.read(reinterpret_cast<uint8_t*>(&header), sizeof(header)) != sizeof(header))
{
NODE_STORE_LOG("[NodeStore] load SD header read failed path=%s\n", kPersistNodesFile);
file.close();
return LoadResult::MissingOrInvalid;
}
if (header.count == 0 || header.count > contacts::NodeStoreCore::kMaxNodes)
{
NODE_STORE_LOG("[NodeStore] load SD header invalid path=%s ver=%u count=%u\n",
kPersistNodesFile,
static_cast<unsigned>(header.ver),
static_cast<unsigned>(header.count));
file.close();
return LoadResult::MissingOrInvalid;
}
const size_t payload_len = file_size - sizeof(header);
if (payload_len == 0)
{
NODE_STORE_LOG("[NodeStore] load SD empty payload path=%s ver=%u count=%u\n",
kPersistNodesFile,
static_cast<unsigned>(header.ver),
static_cast<unsigned>(header.count));
file.close();
return LoadResult::MissingOrInvalid;
}
const size_t entry_size = contacts::nodeBlobEntrySizeForVersion(header.ver);
if (entry_size == 0)
{
NODE_STORE_LOG("[NodeStore] load SD unsupported version path=%s ver=%u\n",
kPersistNodesFile,
static_cast<unsigned>(header.ver));
file.close();
return LoadResult::MissingOrInvalid;
}
const size_t expected_len = static_cast<size_t>(header.count) * entry_size;
if (expected_len != payload_len)
{
NODE_STORE_LOG("[NodeStore] load SD blob size mismatch path=%s len=%u ver=%u count=%u expected=%u\n",
kPersistNodesFile,
static_cast<unsigned>(payload_len),
static_cast<unsigned>(header.ver),
static_cast<unsigned>(header.count),
static_cast<unsigned>(expected_len));
file.close();
return LoadResult::MissingOrInvalid;
}
out.resize(expected_len);
size_t total_read = 0;
while (total_read < expected_len)
{
const size_t chunk = std::min(kSdReadChunkBytes, expected_len - total_read);
const size_t read_bytes = file.read(out.data() + total_read, chunk);
if (read_bytes != chunk)
{
NODE_STORE_LOG("[NodeStore] load SD payload read mismatch path=%s want=%u got=%u\n",
kPersistNodesFile,
static_cast<unsigned>(chunk),
static_cast<unsigned>(read_bytes));
file.close();
out.clear();
return LoadResult::MissingOrInvalid;
}
total_read += read_bytes;
}
file.close();
if (out.size() != expected_len)
{
NODE_STORE_LOG("[NodeStore] load SD blob size mismatch path=%s len=%u ver=%u count=%u expected=%u\n",
kPersistNodesFile,
static_cast<unsigned>(out.size()),
static_cast<unsigned>(header.ver),
static_cast<unsigned>(header.count),
static_cast<unsigned>(expected_len));
out.clear();
return LoadResult::MissingOrInvalid;
}
const uint32_t calc_crc = contacts::NodeStoreCore::computeBlobCrc(out.data(), out.size());
if (calc_crc != header.crc)
{
NODE_STORE_LOG("[NodeStore] load SD crc mismatch path=%s stored=%08lX calc=%08lX\n",
kPersistNodesFile,
static_cast<unsigned long>(header.crc),
static_cast<unsigned long>(calc_crc));
out.clear();
return LoadResult::MissingOrInvalid;
}
std::vector<uint8_t> normalized;
if (!canonicalizeNodePayload(out.data(), out.size(), header.ver, normalized))
{
NODE_STORE_LOG("[NodeStore] load SD decode failed path=%s ver=%u len=%u\n",
kPersistNodesFile,
static_cast<unsigned>(header.ver),
static_cast<unsigned>(out.size()));
out.clear();
return LoadResult::MissingOrInvalid;
}
out.swap(normalized);
NODE_STORE_LOG("[NodeStore] loaded=%u (SD)\n",
static_cast<unsigned>(out.size() / contacts::NodeStoreCore::kSerializedEntrySize));
return LoadResult::Loaded;
}
bool NodeStore::saveToNvs(const uint8_t* data, size_t len) const
{
chat::infra::PreferencesBlobMetadata meta;
if (data && len > 0)
{
if (!contacts::isValidNodeBlobSize(len) ||
contacts::nodeBlobEntryCount(len) > contacts::NodeStoreCore::kMaxNodes)
{
return false;
}
meta.len = len;
meta.has_version = true;
meta.version = contacts::NodeStoreCore::kPersistVersion;
meta.has_crc = true;
meta.crc = contacts::NodeStoreCore::computeBlobCrc(data, len);
}
const bool ok = chat::infra::saveRawBlobToPreferencesWithMetadata(kPersistNodesNs,
kPersistNodesKey,
kPersistNodesKeyVer,
kPersistNodesKeyCrc,
data,
len,
&meta,
true);
if (!ok)
{
NODE_STORE_LOG("[NodeStore] save failed ns=%s\n", kPersistNodesNs);
logNvsStats("save-write", kPersistNodesNs);
}
return ok;
}
bool NodeStore::saveToSd(const uint8_t* data, size_t len) const
{
if (!::platform::esp::arduino_common::storage::sd_card_ready())
{
return false;
}
if (!contacts::isValidNodeBlobSize(len) ||
contacts::nodeBlobEntryCount(len) > contacts::NodeStoreCore::kMaxNodes)
{
return false;
}
::platform::esp::arduino_common::storage::PersistenceBusGate bus_gate(
s_node_store_bus_arbiter,
sys::runtime::BusAccessPolicy::DurableCommit,
kSdPersistWaitMs,
kNodeStoreBusResource,
kNodeStoreBusOwnerId + 2,
kNodeStoreBusOwnerId);
if (!bus_gate.locked())
{
NODE_STORE_LOG("[NodeStore] save SD skipped: spi busy len=%u\n",
static_cast<unsigned>(len));
return false;
}
const std::string temp_path = std::string(kPersistNodesFile) + ".tmp";
if (::platform::esp::arduino_common::storage::sd_exists(temp_path.c_str()))
{
::platform::esp::arduino_common::storage::sd_remove(temp_path.c_str());
}
::platform::esp::arduino_common::storage::SdRuntimeFile file;
if (!file.open(temp_path.c_str(), "w"))
{
return false;
}
contacts::NodeStoreSdHeader header = contacts::makeNodeStoreSdHeader(data, len);
bool ok = (file.write(reinterpret_cast<const uint8_t*>(&header), sizeof(header)) == sizeof(header));
if (ok && data && len > 0)
{
ok = (file.write(data, len) == len);
}
file.close();
if (!ok)
{
::platform::esp::arduino_common::storage::sd_remove(temp_path.c_str());
return false;
}
if (::platform::esp::arduino_common::storage::sd_exists(kPersistNodesFile))
{
::platform::esp::arduino_common::storage::sd_remove(kPersistNodesFile);
}
if (!::platform::esp::arduino_common::storage::sd_rename(temp_path.c_str(), kPersistNodesFile))
{
::platform::esp::arduino_common::storage::sd_remove(temp_path.c_str());
return false;
}
NODE_STORE_LOG("[NodeStore] save SD path=%s len=%u ver=%u count=%u\n",
kPersistNodesFile,
static_cast<unsigned>(len),
static_cast<unsigned>(header.ver),
static_cast<unsigned>(header.count));
return true;
}
bool NodeStore::saveToBackend(const uint8_t* data, size_t len, StorageBackend backend) const
{
if (backend == StorageBackend::Sd)
{
const bool ok = saveToSd(data, len);
NODE_STORE_LOG("[NodeStore] save target=sd len=%u count=%u ok=%u\n",
static_cast<unsigned>(len),
static_cast<unsigned>(contacts::nodeBlobEntryCount(len)),
ok ? 1U : 0U);
return ok;
}
const bool ok = saveToNvs(data, len);
NODE_STORE_LOG("[NodeStore] save target=nvs len=%u count=%u ok=%u\n",
static_cast<unsigned>(len),
static_cast<unsigned>(contacts::nodeBlobEntryCount(len)),
ok ? 1U : 0U);
return ok;
}
void NodeStore::ensureAsyncSaveWorker()
{
if (async_save_mutex_ == nullptr)
{
async_save_mutex_ = xSemaphoreCreateMutex();
}
if (async_save_queue_ == nullptr)
{
async_save_queue_ = xQueueCreate(1, sizeof(uint8_t));
}
if (async_save_task_ == nullptr && async_save_mutex_ != nullptr && async_save_queue_ != nullptr)
{
BaseType_t ok = xTaskCreate(asyncSaveTaskEntry,
"node_store_io",
kAsyncSaveTaskStackBytes,
this,
kAsyncSaveTaskPriority,
&async_save_task_);
if (ok != pdPASS)
{
NODE_STORE_LOG("[NodeStore] async save task create failed\n");
async_save_task_ = nullptr;
}
}
}
bool NodeStore::enqueueAsyncSave(const uint8_t* data, size_t len)
{
if (len > 0 && data == nullptr)
{
return false;
}
if (!contacts::isValidNodeBlobSize(len) ||
contacts::nodeBlobEntryCount(len) > contacts::NodeStoreCore::kMaxNodes)
{
return false;
}
ensureAsyncSaveWorker();
if (async_save_mutex_ == nullptr || async_save_queue_ == nullptr || async_save_task_ == nullptr)
{
return false;
}
if (xSemaphoreTake(async_save_mutex_, kAsyncSaveMutexWait) != pdTRUE)
{
NODE_STORE_LOG("[NodeStore] async save enqueue busy len=%u\n",
static_cast<unsigned>(len));
return false;
}
pending_save_blob_.assign(data, data + len);
pending_save_backend_ = backend_;
++pending_save_generation_;
async_save_pending_ = true;
async_save_failed_ = false;
const uint32_t generation = pending_save_generation_;
const StorageBackend target = pending_save_backend_;
xSemaphoreGive(async_save_mutex_);
const uint8_t signal = 1;
if (xQueueOverwrite(async_save_queue_, &signal) != pdTRUE)
{
NODE_STORE_LOG("[NodeStore] async save signal failed gen=%lu\n",
static_cast<unsigned long>(generation));
return false;
}
NODE_STORE_LOG("[NodeStore] save queued gen=%lu target=%s len=%u count=%u\n",
static_cast<unsigned long>(generation),
target == StorageBackend::Sd ? "sd" : "nvs",
static_cast<unsigned>(len),
static_cast<unsigned>(contacts::nodeBlobEntryCount(len)));
return true;
}
bool NodeStore::waitForAsyncSave(TickType_t wait_ticks)
{
ensureAsyncSaveWorker();
if (async_save_mutex_ == nullptr || async_save_queue_ == nullptr || async_save_task_ == nullptr)
{
return false;
}
const TickType_t start = xTaskGetTickCount();
while (true)
{
bool done = false;
bool failed = false;
if (xSemaphoreTake(async_save_mutex_, kAsyncSaveMutexWait) == pdTRUE)
{
done = !async_save_pending_ && !async_save_busy_;
failed = async_save_failed_;
xSemaphoreGive(async_save_mutex_);
}
if (done)
{
return !failed;
}
if ((xTaskGetTickCount() - start) >= wait_ticks)
{
return false;
}
vTaskDelay(kAsyncSavePollInterval);
}
}
void NodeStore::asyncSaveLoop()
{
uint8_t signal = 0;
for (;;)
{
if (xQueueReceive(async_save_queue_, &signal, portMAX_DELAY) != pdTRUE)
{
continue;
}
for (;;)
{
std::vector<uint8_t> blob;
StorageBackend backend = StorageBackend::Nvs;
uint32_t generation = 0;
if (xSemaphoreTake(async_save_mutex_, portMAX_DELAY) != pdTRUE)
{
break;
}
if (!async_save_pending_)
{
async_save_busy_ = false;
xSemaphoreGive(async_save_mutex_);
break;
}
blob = pending_save_blob_;
backend = pending_save_backend_;
generation = pending_save_generation_;
async_save_pending_ = false;
async_save_busy_ = true;
xSemaphoreGive(async_save_mutex_);
const bool ok = saveToBackend(blob.data(), blob.size(), backend);
bool has_more = false;
if (xSemaphoreTake(async_save_mutex_, portMAX_DELAY) == pdTRUE)
{
async_save_busy_ = false;
async_save_failed_ = !ok;
if (ok)
{
completed_save_generation_ = generation;
}
else if (!async_save_pending_)
{
pending_save_blob_ = blob;
pending_save_backend_ = backend;
async_save_pending_ = true;
}
has_more = async_save_pending_;
xSemaphoreGive(async_save_mutex_);
}
NODE_STORE_LOG("[NodeStore] save done gen=%lu ok=%u more=%u\n",
static_cast<unsigned long>(generation),
ok ? 1U : 0U,
has_more ? 1U : 0U);
if (!ok)
{
vTaskDelay(kAsyncSaveRetryDelay);
}
if (!has_more)
{
break;
}
}
}
}
void NodeStore::asyncSaveTaskEntry(void* context)
{
auto* self = static_cast<NodeStore*>(context);
if (self)
{
self->asyncSaveLoop();
}
vTaskDelete(nullptr);
}
void NodeStore::clearNvs() const
{
chat::infra::clearPreferencesKeys(kPersistNodesNs,
kPersistNodesKey,
kPersistNodesKeyVer,
kPersistNodesKeyCrc);
}
} // namespace meshtastic
} // namespace chat
@@ -0,0 +1,276 @@
#include "platform/esp/arduino_common/chat/infra/store/fixed_slot_journal.h"
#include "platform/esp/arduino_common/storage/sd_card_runtime.h"
#include <cstring>
#include <limits>
namespace chat::storage::v2
{
namespace
{
namespace storage = ::platform::esp::arduino_common::storage;
bool readExact(storage::SdRuntimeFile& file, void* out, std::size_t len)
{
return out && len != 0U &&
file.read(out, len) == static_cast<int>(len);
}
bool writeExact(storage::SdRuntimeFile& file,
const void* data,
std::size_t len)
{
return data && len != 0U && file.write(data, len) == len;
}
} // namespace
FixedSlotJournalEngine::Inspection FixedSlotJournalEngine::inspect(
const char* path,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size) const
{
Inspection result{};
if (!path || path[0] == '\0' ||
!validDescriptor(protocol, kind, slot_size))
{
result.state = State::Incompatible;
return result;
}
if (!storage::sd_exists(path))
{
result.state = State::Missing;
return result;
}
storage::SdRuntimeFile file;
if (!file.open(path, "r"))
{
result.state = State::IoError;
return result;
}
Header header{};
if (file.size() < sizeof(header) || !readExact(file, &header, sizeof(header)))
{
result.state = State::Incompatible;
return result;
}
if (!matches(header, protocol, kind, slot_size))
{
result.state = State::Incompatible;
return result;
}
const uint64_t payload_size = file.size() - sizeof(header);
if (payload_size % slot_size != 0U)
{
result.state = State::PartialTail;
result.slot_count = static_cast<uint32_t>(payload_size / slot_size);
return result;
}
const uint64_t count = payload_size / slot_size;
if (count > std::numeric_limits<uint32_t>::max())
{
result.state = State::Incompatible;
return result;
}
result.state = State::Ready;
result.slot_count = static_cast<uint32_t>(count);
return result;
}
bool FixedSlotJournalEngine::create(const char* path,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size) const
{
if (!path || path[0] == '\0' ||
!validDescriptor(protocol, kind, slot_size))
{
return false;
}
storage::SdRuntimeFile file;
if (!file.open(path, "w+"))
{
return false;
}
const Header header = makeHeader(protocol, kind, slot_size);
return writeExact(file, &header, sizeof(header)) && file.flush();
}
bool FixedSlotJournalEngine::append(const char* path,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size,
const void* slot) const
{
if (!slot)
{
return false;
}
Inspection inspection = inspect(path, protocol, kind, slot_size);
if (inspection.state == State::Missing)
{
if (!create(path, protocol, kind, slot_size))
{
return false;
}
inspection = inspect(path, protocol, kind, slot_size);
}
if (inspection.state != State::Ready)
{
return false;
}
storage::SdRuntimeFile file;
if (!file.open(path, "r+") || !file.seek(file.size()))
{
return false;
}
return writeExact(file, slot, slot_size) && file.flush();
}
bool FixedSlotJournalEngine::read(const char* path,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size,
uint32_t slot_index,
void* out_slot) const
{
if (!out_slot)
{
return false;
}
const Inspection inspection = inspect(path, protocol, kind, slot_size);
if ((inspection.state != State::Ready &&
inspection.state != State::PartialTail) ||
slot_index >= inspection.slot_count)
{
return false;
}
storage::SdRuntimeFile file;
const uint64_t offset = sizeof(Header) +
static_cast<uint64_t>(slot_index) * slot_size;
return file.open(path, "r") && file.seek(offset) &&
readExact(file, out_slot, slot_size);
}
bool FixedSlotJournalEngine::validDescriptor(MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size)
{
return supportedProtocol(protocol) &&
static_cast<uint8_t>(kind) >=
static_cast<uint8_t>(JournalKind::MessageSegment) &&
static_cast<uint8_t>(kind) <=
static_cast<uint8_t>(JournalKind::ContactDelta) &&
slot_size > 0U &&
slot_size <= std::numeric_limits<uint16_t>::max();
}
FixedSlotJournalEngine::Header FixedSlotJournalEngine::makeHeader(
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size)
{
Header header{};
header.magic = kMagic;
header.schema = kStorageSchemaVersion;
header.protocol = static_cast<uint8_t>(canonicalProtocol(protocol));
header.kind = static_cast<uint8_t>(kind);
header.slot_size = static_cast<uint16_t>(slot_size);
header.crc = crc32(&header, sizeof(header) - sizeof(header.crc));
return header;
}
bool FixedSlotJournalEngine::matches(const Header& header,
MeshProtocol protocol,
JournalKind kind,
std::size_t slot_size)
{
return header.magic == kMagic &&
header.schema == kStorageSchemaVersion &&
header.protocol ==
static_cast<uint8_t>(canonicalProtocol(protocol)) &&
header.kind == static_cast<uint8_t>(kind) &&
header.slot_size == slot_size &&
header.crc == crc32(&header, sizeof(header) - sizeof(header.crc));
}
bool replaceFileAtomically(const char* temp_path,
const char* final_path,
const char* backup_path)
{
if (!temp_path || !final_path || !backup_path ||
!storage::sd_exists(temp_path))
{
return false;
}
if (storage::sd_exists(backup_path) &&
!storage::sd_remove(backup_path))
{
return false;
}
const bool had_final = storage::sd_exists(final_path);
if (had_final && !storage::sd_rename(final_path, backup_path))
{
return false;
}
if (!storage::sd_rename(temp_path, final_path))
{
if (had_final)
{
(void)storage::sd_rename(backup_path, final_path);
}
return false;
}
if (had_final)
{
(void)storage::sd_remove(backup_path);
}
return true;
}
bool recoverAtomicFile(const char* final_path,
const char* temp_path,
const char* backup_path)
{
if (!final_path || !temp_path || !backup_path)
{
return false;
}
if (storage::sd_exists(final_path))
{
if (storage::sd_exists(temp_path))
{
(void)storage::sd_remove(temp_path);
}
if (storage::sd_exists(backup_path))
{
(void)storage::sd_remove(backup_path);
}
return true;
}
if (storage::sd_exists(backup_path))
{
if (!storage::sd_rename(backup_path, final_path))
{
return false;
}
if (storage::sd_exists(temp_path))
{
(void)storage::sd_remove(temp_path);
}
return true;
}
if (storage::sd_exists(temp_path))
{
(void)storage::sd_remove(temp_path);
}
return true;
}
} // namespace chat::storage::v2
@@ -0,0 +1,789 @@
#include "platform/esp/arduino_common/chat/infra/store/protocol_chat_codec.h"
#include <algorithm>
#include <cstring>
namespace chat::storage::v2
{
namespace
{
constexpr uint32_t kMessageMagic = 0x3247534DU; // MSG2
constexpr uint32_t kCatalogMagic = 0x32544143U; // CAT2
constexpr uint32_t kReadMagic = 0x32444152U; // RAD2
constexpr uint32_t kStatusMagic = 0x32535453U; // STS2
constexpr uint32_t kSeenMagic = 0x324E4553U; // SEN2
constexpr uint8_t kMessageSourceUnverified = 0x01U;
constexpr uint8_t kMessageHasGeo = 0x02U;
constexpr uint8_t kMessageHasReticulumIdentity = 0x04U;
constexpr uint8_t kMessageHasLxmfHash = 0x08U;
constexpr uint8_t kProjectionDeleted = 0x01U;
constexpr uint8_t kProjectionHasReticulumIdentity = 0x02U;
struct MessagePrefix
{
uint32_t magic = kMessageMagic;
uint16_t schema = kStorageSchemaVersion;
uint8_t flags = 0;
uint8_t reserved = 0;
uint32_t sequence = 0;
uint32_t message_id = 0;
uint32_t timestamp = 0;
uint32_t from = 0;
uint32_t peer = 0;
int32_t geo_lat_e7 = 0;
int32_t geo_lon_e7 = 0;
uint16_t text_len = 0;
uint8_t status = 0;
uint8_t channel = 0;
uint8_t rx_origin = 0;
uint8_t team_location_icon = 0;
} __attribute__((packed));
struct MeshtasticMessageSlot
{
MessagePrefix prefix{};
char text[kMeshtasticTextMax] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct MeshCoreMessageSlot
{
MessagePrefix prefix{};
char text[kMeshCoreTextMax] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct ReticulumMessageSlot
{
MessagePrefix prefix{};
uint8_t destination_hash[kReticulumPeerHashSize] = {};
uint8_t identity_hash[kReticulumPeerHashSize] = {};
uint8_t lxmf_hash[kReticulumLxmfHashSize] = {};
char text[kReticulumTextMax] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct CatalogPrefix
{
uint32_t magic = kCatalogMagic;
uint16_t schema = kStorageSchemaVersion;
uint8_t flags = 0;
uint8_t channel = 0;
uint32_t peer = 0;
uint32_t last_message_id = 0;
uint32_t message_count = 0;
uint32_t last_sequence = 0;
uint32_t last_timestamp = 0;
uint32_t unread = 0;
uint8_t last_status = 0;
uint8_t preview_len = 0;
uint16_t reserved = 0;
char preview[kCatalogPreviewMax] = {};
} __attribute__((packed));
struct NodeCatalogSlot
{
CatalogPrefix prefix{};
uint32_t crc = 0;
} __attribute__((packed));
struct ReticulumCatalogSlot
{
CatalogPrefix prefix{};
uint8_t destination_hash[kReticulumPeerHashSize] = {};
uint8_t identity_hash[kReticulumPeerHashSize] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct ReadPrefix
{
uint32_t magic = kReadMagic;
uint16_t schema = kStorageSchemaVersion;
uint8_t flags = 0;
uint8_t channel = 0;
uint32_t peer = 0;
uint32_t last_read_sequence = 0;
} __attribute__((packed));
struct NodeReadSlot
{
ReadPrefix prefix{};
uint32_t crc = 0;
} __attribute__((packed));
struct ReticulumReadSlot
{
ReadPrefix prefix{};
uint8_t destination_hash[kReticulumPeerHashSize] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct StatusSlot
{
uint32_t magic = kStatusMagic;
uint16_t schema = kStorageSchemaVersion;
uint8_t status = 0;
uint8_t reserved = 0;
uint32_t message_id = 0;
uint32_t sequence = 0;
uint32_t crc = 0;
} __attribute__((packed));
struct SeenSlot
{
uint32_t magic = kSeenMagic;
uint16_t schema = kStorageSchemaVersion;
uint16_t reserved = 0;
uint8_t hash[kReticulumLxmfHashSize] = {};
uint32_t crc = 0;
} __attribute__((packed));
static_assert(sizeof(MessagePrefix) == 42,
"v2 message prefix size changed");
static_assert(sizeof(MeshtasticMessageSlot) == 279,
"v2 Meshtastic message slot size changed");
static_assert(sizeof(MeshCoreMessageSlot) == 217,
"v2 MeshCore message slot size changed");
static_assert(sizeof(ReticulumMessageSlot) == 365,
"v2 Reticulum message slot size changed");
bool validStatus(uint8_t raw)
{
return raw <= static_cast<uint8_t>(MessageStatus::Delivered);
}
template <typename T>
bool validSlot(const T& slot, uint32_t expected_magic)
{
return slot.crc == crc32(&slot, sizeof(T) - sizeof(slot.crc)) &&
slot.prefix.magic == expected_magic &&
slot.prefix.schema == kStorageSchemaVersion;
}
bool validStatusSlot(const StatusSlot& slot)
{
return slot.magic == kStatusMagic &&
slot.schema == kStorageSchemaVersion &&
validStatus(slot.status) &&
slot.crc == crc32(&slot, sizeof(slot) - sizeof(slot.crc));
}
bool validSeenSlot(const SeenSlot& slot)
{
return slot.magic == kSeenMagic &&
slot.schema == kStorageSchemaVersion &&
slot.crc == crc32(&slot, sizeof(slot) - sizeof(slot.crc));
}
void encodeMessagePrefix(MessagePrefix& prefix,
const ChatMessage& message,
uint32_t sequence,
std::size_t text_len)
{
prefix = MessagePrefix{};
prefix.sequence = sequence;
prefix.message_id = message.msg_id;
prefix.timestamp = message.timestamp;
prefix.from = message.from;
prefix.peer = message.peer;
prefix.geo_lat_e7 = message.geo_lat_e7;
prefix.geo_lon_e7 = message.geo_lon_e7;
prefix.text_len = static_cast<uint16_t>(text_len);
prefix.status = static_cast<uint8_t>(message.status);
prefix.channel = static_cast<uint8_t>(message.channel);
prefix.rx_origin = static_cast<uint8_t>(message.rx_origin);
prefix.team_location_icon = message.team_location_icon;
if (message.source_unverified)
{
prefix.flags |= kMessageSourceUnverified;
}
if (message.has_geo)
{
prefix.flags |= kMessageHasGeo;
}
}
bool decodeMessagePrefix(const MessagePrefix& prefix,
std::size_t text_capacity,
ChatMessage& message,
uint32_t* out_sequence)
{
if (prefix.magic != kMessageMagic ||
prefix.schema != kStorageSchemaVersion ||
prefix.text_len > text_capacity || !validStatus(prefix.status))
{
return false;
}
message.channel = static_cast<ChannelId>(prefix.channel);
message.from = prefix.from;
message.peer = prefix.peer;
message.msg_id = prefix.message_id;
message.timestamp = prefix.timestamp;
message.team_location_icon = prefix.team_location_icon;
message.has_geo = (prefix.flags & kMessageHasGeo) != 0;
message.geo_lat_e7 = prefix.geo_lat_e7;
message.geo_lon_e7 = prefix.geo_lon_e7;
message.source_unverified =
(prefix.flags & kMessageSourceUnverified) != 0;
message.rx_origin = static_cast<RxOrigin>(prefix.rx_origin);
message.status = static_cast<MessageStatus>(prefix.status);
if (out_sequence)
{
*out_sequence = prefix.sequence;
}
return true;
}
template <typename T>
bool encodeNodeMessage(const ChatMessage& message,
uint32_t sequence,
void* out,
std::size_t out_len)
{
T slot{};
if (!out || out_len != sizeof(T) ||
message.text.size() > sizeof(slot.text))
{
return false;
}
encodeMessagePrefix(slot.prefix, message, sequence, message.text.size());
if (!message.text.empty())
{
std::memcpy(slot.text, message.text.data(), message.text.size());
}
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
template <typename T>
bool decodeNodeMessage(MeshProtocol protocol,
const void* data,
std::size_t len,
ChatMessage& message,
uint32_t* out_sequence)
{
if (!data || len != sizeof(T))
{
return false;
}
T slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validSlot(slot, kMessageMagic) ||
!decodeMessagePrefix(slot.prefix,
sizeof(slot.text),
message,
out_sequence))
{
return false;
}
message.protocol = protocol;
message.text.assign(slot.text, slot.prefix.text_len);
return true;
}
void encodeCatalogPrefix(CatalogPrefix& prefix,
const ChatCatalogProjection& projection)
{
prefix = CatalogPrefix{};
prefix.channel = static_cast<uint8_t>(projection.conversation.channel);
prefix.peer = projection.conversation.peer;
prefix.last_message_id = projection.last_message_id;
prefix.message_count = projection.message_count;
prefix.last_sequence = projection.last_sequence;
prefix.last_timestamp = projection.last_timestamp;
prefix.unread = projection.unread;
prefix.last_status = static_cast<uint8_t>(projection.last_status);
if (projection.deleted)
{
prefix.flags |= kProjectionDeleted;
}
const std::size_t preview_len =
std::min<std::size_t>(std::strlen(projection.preview),
kCatalogPreviewMax);
prefix.preview_len = static_cast<uint8_t>(preview_len);
if (preview_len > 0)
{
std::memcpy(prefix.preview, projection.preview, preview_len);
}
}
bool decodeCatalogPrefix(MeshProtocol protocol,
const CatalogPrefix& prefix,
ChatCatalogProjection& projection)
{
if (prefix.magic != kCatalogMagic ||
prefix.schema != kStorageSchemaVersion ||
prefix.preview_len > kCatalogPreviewMax ||
!validStatus(prefix.last_status))
{
return false;
}
projection = ChatCatalogProjection{};
projection.conversation.protocol = protocol;
projection.conversation.channel = static_cast<ChannelId>(prefix.channel);
projection.conversation.peer = prefix.peer;
projection.last_message_id = prefix.last_message_id;
projection.message_count = prefix.message_count;
projection.last_sequence = prefix.last_sequence;
projection.last_timestamp = prefix.last_timestamp;
projection.unread = prefix.unread;
projection.last_status = static_cast<MessageStatus>(prefix.last_status);
projection.deleted = (prefix.flags & kProjectionDeleted) != 0;
std::memcpy(projection.preview, prefix.preview, prefix.preview_len);
projection.preview[prefix.preview_len] = '\0';
return true;
}
void encodeReadPrefix(ReadPrefix& prefix,
const ChatReadProjection& projection)
{
prefix = ReadPrefix{};
prefix.channel = static_cast<uint8_t>(projection.conversation.channel);
prefix.peer = projection.conversation.peer;
prefix.last_read_sequence = projection.last_read_sequence;
if (projection.deleted)
{
prefix.flags |= kProjectionDeleted;
}
}
bool decodeReadPrefix(MeshProtocol protocol,
const ReadPrefix& prefix,
ChatReadProjection& projection)
{
if (prefix.magic != kReadMagic || prefix.schema != kStorageSchemaVersion)
{
return false;
}
projection = ChatReadProjection{};
projection.conversation.protocol = protocol;
projection.conversation.channel = static_cast<ChannelId>(prefix.channel);
projection.conversation.peer = prefix.peer;
projection.last_read_sequence = prefix.last_read_sequence;
projection.deleted = (prefix.flags & kProjectionDeleted) != 0;
return true;
}
} // namespace
uint32_t crc32(const void* data, std::size_t len)
{
const auto* bytes = static_cast<const uint8_t*>(data);
uint32_t crc = 0xFFFFFFFFU;
for (std::size_t index = 0; index < len; ++index)
{
crc ^= bytes[index];
for (uint8_t bit = 0; bit < 8; ++bit)
{
crc = (crc & 1U) != 0U ? (crc >> 1U) ^ 0xEDB88320U
: crc >> 1U;
}
}
return ~crc;
}
MeshProtocol canonicalProtocol(MeshProtocol protocol)
{
return protocol == MeshProtocol::RNode ? MeshProtocol::Reticulum : protocol;
}
bool supportedProtocol(MeshProtocol protocol)
{
switch (canonicalProtocol(protocol))
{
case MeshProtocol::Meshtastic:
case MeshProtocol::MeshCore:
case MeshProtocol::Reticulum:
return true;
default:
return false;
}
}
std::size_t messageSlotSize(MeshProtocol protocol)
{
switch (canonicalProtocol(protocol))
{
case MeshProtocol::Meshtastic:
return sizeof(MeshtasticMessageSlot);
case MeshProtocol::MeshCore:
return sizeof(MeshCoreMessageSlot);
case MeshProtocol::Reticulum:
return sizeof(ReticulumMessageSlot);
default:
return 0;
}
}
std::size_t catalogSlotSize(MeshProtocol protocol)
{
return canonicalProtocol(protocol) == MeshProtocol::Reticulum
? sizeof(ReticulumCatalogSlot)
: supportedProtocol(protocol) ? sizeof(NodeCatalogSlot)
: 0;
}
std::size_t readStateSlotSize(MeshProtocol protocol)
{
return canonicalProtocol(protocol) == MeshProtocol::Reticulum
? sizeof(ReticulumReadSlot)
: supportedProtocol(protocol) ? sizeof(NodeReadSlot)
: 0;
}
std::size_t statusSlotSize()
{
return sizeof(StatusSlot);
}
std::size_t reticulumSeenSlotSize()
{
return sizeof(SeenSlot);
}
bool encodeMessageSlot(const ChatMessage& message,
uint32_t sequence,
void* out,
std::size_t out_len)
{
switch (canonicalProtocol(message.protocol))
{
case MeshProtocol::Meshtastic:
return encodeNodeMessage<MeshtasticMessageSlot>(message,
sequence,
out,
out_len);
case MeshProtocol::MeshCore:
return encodeNodeMessage<MeshCoreMessageSlot>(message,
sequence,
out,
out_len);
case MeshProtocol::Reticulum:
{
if (!out || out_len != sizeof(ReticulumMessageSlot) ||
message.text.size() > kReticulumTextMax)
{
return false;
}
ReticulumMessageSlot slot{};
encodeMessagePrefix(slot.prefix,
message,
sequence,
message.text.size());
if (hasReticulumDestinationIdentity(message.reticulum_identity))
{
slot.prefix.flags |= kMessageHasReticulumIdentity;
std::memcpy(slot.destination_hash,
message.reticulum_identity.destination_hash,
sizeof(slot.destination_hash));
std::memcpy(slot.identity_hash,
message.reticulum_identity.identity_hash,
sizeof(slot.identity_hash));
}
if (hasReticulumLxmfMessageHash(message))
{
slot.prefix.flags |= kMessageHasLxmfHash;
std::memcpy(slot.lxmf_hash,
message.reticulum_lxmf_hash,
sizeof(slot.lxmf_hash));
}
if (!message.text.empty())
{
std::memcpy(slot.text,
message.text.data(),
message.text.size());
}
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
default:
return false;
}
}
bool decodeMessageSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
ChatMessage& out_message,
uint32_t* out_sequence)
{
protocol = canonicalProtocol(protocol);
if (protocol == MeshProtocol::Meshtastic)
{
return decodeNodeMessage<MeshtasticMessageSlot>(protocol,
data,
len,
out_message,
out_sequence);
}
if (protocol == MeshProtocol::MeshCore)
{
return decodeNodeMessage<MeshCoreMessageSlot>(protocol,
data,
len,
out_message,
out_sequence);
}
if (protocol != MeshProtocol::Reticulum || !data ||
len != sizeof(ReticulumMessageSlot))
{
return false;
}
ReticulumMessageSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validSlot(slot, kMessageMagic) ||
!decodeMessagePrefix(slot.prefix,
sizeof(slot.text),
out_message,
out_sequence))
{
return false;
}
out_message.protocol = protocol;
out_message.text.assign(slot.text, slot.prefix.text_len);
if ((slot.prefix.flags & kMessageHasReticulumIdentity) != 0)
{
out_message.reticulum_identity =
makeReticulumPeerIdentity(slot.destination_hash,
slot.identity_hash);
}
if ((slot.prefix.flags & kMessageHasLxmfHash) != 0)
{
out_message.has_reticulum_lxmf_hash = true;
std::memcpy(out_message.reticulum_lxmf_hash,
slot.lxmf_hash,
sizeof(slot.lxmf_hash));
}
return true;
}
bool encodeCatalogSlot(MeshProtocol protocol,
const ChatCatalogProjection& projection,
void* out,
std::size_t out_len)
{
protocol = canonicalProtocol(protocol);
if (protocol == MeshProtocol::Reticulum)
{
if (!out || out_len != sizeof(ReticulumCatalogSlot))
{
return false;
}
ReticulumCatalogSlot slot{};
encodeCatalogPrefix(slot.prefix, projection);
if (hasReticulumDestinationIdentity(
projection.conversation.reticulum_identity))
{
slot.prefix.flags |= kProjectionHasReticulumIdentity;
std::memcpy(slot.destination_hash,
projection.conversation.reticulum_identity
.destination_hash,
sizeof(slot.destination_hash));
std::memcpy(slot.identity_hash,
projection.conversation.reticulum_identity
.identity_hash,
sizeof(slot.identity_hash));
}
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
if (!supportedProtocol(protocol) || !out ||
out_len != sizeof(NodeCatalogSlot))
{
return false;
}
NodeCatalogSlot slot{};
encodeCatalogPrefix(slot.prefix, projection);
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
bool decodeCatalogSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
ChatCatalogProjection& out_projection)
{
protocol = canonicalProtocol(protocol);
if (protocol == MeshProtocol::Reticulum)
{
if (!data || len != sizeof(ReticulumCatalogSlot))
{
return false;
}
ReticulumCatalogSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validSlot(slot, kCatalogMagic) ||
!decodeCatalogPrefix(protocol, slot.prefix, out_projection))
{
return false;
}
if ((slot.prefix.flags & kProjectionHasReticulumIdentity) != 0)
{
out_projection.conversation.reticulum_identity =
makeReticulumPeerIdentity(slot.destination_hash,
slot.identity_hash);
}
return true;
}
if (!data || len != sizeof(NodeCatalogSlot) ||
!supportedProtocol(protocol))
{
return false;
}
NodeCatalogSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
return validSlot(slot, kCatalogMagic) &&
decodeCatalogPrefix(protocol, slot.prefix, out_projection);
}
bool encodeReadStateSlot(MeshProtocol protocol,
const ChatReadProjection& projection,
void* out,
std::size_t out_len)
{
protocol = canonicalProtocol(protocol);
if (protocol == MeshProtocol::Reticulum)
{
if (!out || out_len != sizeof(ReticulumReadSlot) ||
!hasReticulumDestinationIdentity(
projection.conversation.reticulum_identity))
{
return false;
}
ReticulumReadSlot slot{};
encodeReadPrefix(slot.prefix, projection);
slot.prefix.flags |= kProjectionHasReticulumIdentity;
std::memcpy(slot.destination_hash,
projection.conversation.reticulum_identity
.destination_hash,
sizeof(slot.destination_hash));
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
if (!supportedProtocol(protocol) || !out ||
out_len != sizeof(NodeReadSlot))
{
return false;
}
NodeReadSlot slot{};
encodeReadPrefix(slot.prefix, projection);
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
bool decodeReadStateSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
ChatReadProjection& out_projection)
{
protocol = canonicalProtocol(protocol);
if (protocol == MeshProtocol::Reticulum)
{
if (!data || len != sizeof(ReticulumReadSlot))
{
return false;
}
ReticulumReadSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validSlot(slot, kReadMagic) ||
!decodeReadPrefix(protocol, slot.prefix, out_projection) ||
(slot.prefix.flags & kProjectionHasReticulumIdentity) == 0)
{
return false;
}
out_projection.conversation.reticulum_identity =
makeReticulumDestinationIdentity(slot.destination_hash);
return true;
}
if (!data || len != sizeof(NodeReadSlot) ||
!supportedProtocol(protocol))
{
return false;
}
NodeReadSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
return validSlot(slot, kReadMagic) &&
decodeReadPrefix(protocol, slot.prefix, out_projection);
}
bool encodeStatusSlot(const ChatStatusProjection& projection,
void* out,
std::size_t out_len)
{
if (!out || out_len != sizeof(StatusSlot) ||
projection.message_id == 0)
{
return false;
}
StatusSlot slot{};
slot.status = static_cast<uint8_t>(projection.status);
slot.message_id = projection.message_id;
slot.sequence = projection.sequence;
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
bool decodeStatusSlot(const void* data,
std::size_t len,
ChatStatusProjection& out_projection)
{
if (!data || len != sizeof(StatusSlot))
{
return false;
}
StatusSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validStatusSlot(slot))
{
return false;
}
out_projection.message_id = slot.message_id;
out_projection.status = static_cast<MessageStatus>(slot.status);
out_projection.sequence = slot.sequence;
return true;
}
bool encodeReticulumSeenSlot(const ReticulumSeenProjection& projection,
void* out,
std::size_t out_len)
{
if (!out || out_len != sizeof(SeenSlot) ||
isAllZeroKeyBytes(projection.hash, sizeof(projection.hash)))
{
return false;
}
SeenSlot slot{};
std::memcpy(slot.hash, projection.hash, sizeof(slot.hash));
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
bool decodeReticulumSeenSlot(const void* data,
std::size_t len,
ReticulumSeenProjection& out_projection)
{
if (!data || len != sizeof(SeenSlot))
{
return false;
}
SeenSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validSeenSlot(slot))
{
return false;
}
std::memcpy(out_projection.hash, slot.hash, sizeof(slot.hash));
return true;
}
} // namespace chat::storage::v2
@@ -0,0 +1,724 @@
#include "platform/esp/arduino_common/chat/infra/store/protocol_peer_codec.h"
#include "platform/esp/arduino_common/chat/infra/store/protocol_chat_codec.h"
#include <algorithm>
#include <cstring>
namespace chat::storage::v2
{
namespace
{
constexpr uint32_t kPeerMagic = 0x32524550U; // PER2
constexpr uint32_t kContactMagic = 0x32544E43U; // CNT2
constexpr uint16_t kDeleted = 0x0001U;
constexpr uint16_t kFavorite = 0x0002U;
constexpr uint16_t kIgnored = 0x0004U;
constexpr uint16_t kTrusted = 0x0008U;
constexpr uint16_t kHasSnr = 0x0010U;
constexpr uint16_t kHasRssi = 0x0020U;
constexpr uint16_t kHasMetrics = 0x0040U;
constexpr uint16_t kHasPosition = 0x0080U;
constexpr uint8_t kHasBattery = 0x01U;
constexpr uint8_t kHasVoltage = 0x02U;
constexpr uint8_t kHasChannelUtilization = 0x04U;
constexpr uint8_t kHasAirUtilization = 0x08U;
constexpr uint8_t kHasUptime = 0x10U;
constexpr uint8_t kPositionValid = 0x01U;
constexpr uint8_t kPositionHasAltitude = 0x02U;
struct PeerPrefix
{
uint32_t magic = kPeerMagic;
uint16_t schema = kStorageSchemaVersion;
uint16_t flags = 0;
uint8_t source = 0;
uint8_t identity_kind = 0;
uint16_t reserved = 0;
uint32_t node_id = 0;
uint32_t first_seen_s = 0;
uint32_t last_seen_s = 0;
char display_name[kMeshPeerDisplayNameMaxLen] = {};
float snr = 0.0f;
float rssi = 0.0f;
uint8_t metrics_flags = 0;
uint8_t position_flags = 0;
uint16_t reserved2 = 0;
uint32_t battery_level = 0;
float voltage = 0.0f;
float channel_utilization = 0.0f;
float air_util_tx = 0.0f;
uint32_t uptime_seconds = 0;
int32_t latitude_i = 0;
int32_t longitude_i = 0;
int32_t altitude = 0;
uint32_t position_timestamp = 0;
uint32_t precision_bits = 0;
uint32_t pdop = 0;
uint32_t hdop = 0;
uint32_t vdop = 0;
uint32_t gps_accuracy_mm = 0;
} __attribute__((packed));
struct NodeFactsSlot
{
char short_name[kMeshPeerShortNameMaxLen] = {};
char long_name[kMeshPeerDisplayNameMaxLen] = {};
uint8_t role = 0xFF;
uint8_t hw_model = 0;
uint8_t channel = 0xFF;
uint8_t hops_away = 0xFF;
uint8_t flags = 0;
uint8_t macaddr[kMeshPeerMacAddrLen] = {};
} __attribute__((packed));
struct MeshtasticPeerSlot
{
PeerPrefix prefix{};
NodeFactsSlot node{};
uint8_t has_public_key = 0;
uint8_t key_verified = 0;
uint8_t has_next_hop = 0;
uint8_t next_hop = 0;
uint8_t public_key[kMeshPeerMeshtasticPublicKeyLen] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct MeshCorePeerSlot
{
PeerPrefix prefix{};
NodeFactsSlot node{};
uint8_t has_public_key = 0;
uint8_t key_verified = 0;
uint8_t has_peer_hash = 0;
uint8_t peer_hash = 0;
uint8_t has_next_hop = 0;
uint8_t next_hop = 0;
uint16_t reserved = 0;
uint32_t node_id_hint = 0;
uint8_t public_key[kMeshPeerMeshCorePublicKeyLen] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct ReticulumPeerSlot
{
PeerPrefix prefix{};
uint8_t destination_hash[kReticulumPeerHashSize] = {};
uint8_t identity_hash[kReticulumPeerHashSize] = {};
uint8_t has_public_keys = 0;
uint8_t has_ratchet = 0;
uint8_t delivery = 0;
uint8_t propagation = 0;
uint8_t enc_pub[kMeshPeerReticulumPublicKeyLen] = {};
uint8_t sig_pub[kMeshPeerReticulumPublicKeyLen] = {};
uint8_t ratchet_pub[kMeshPeerReticulumRatchetLen] = {};
uint32_t ratchet_seen_s = 0;
uint32_t crc = 0;
} __attribute__((packed));
struct NodeContactSlot
{
uint32_t magic = kContactMagic;
uint16_t schema = kStorageSchemaVersion;
uint16_t flags = 0;
uint32_t node_id = 0;
char alias[kMeshPeerUserAliasMaxLen + 1] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct MeshCoreContactSlot
{
uint32_t magic = kContactMagic;
uint16_t schema = kStorageSchemaVersion;
uint16_t flags = 0;
uint32_t node_id_hint = 0;
uint8_t public_key[kMeshPeerMeshCorePublicKeyLen] = {};
char alias[kMeshPeerUserAliasMaxLen + 1] = {};
uint32_t crc = 0;
} __attribute__((packed));
struct ReticulumContactSlot
{
uint32_t magic = kContactMagic;
uint16_t schema = kStorageSchemaVersion;
uint16_t flags = 0;
uint8_t destination_hash[kReticulumPeerHashSize] = {};
char alias[kMeshPeerUserAliasMaxLen + 1] = {};
uint32_t crc = 0;
} __attribute__((packed));
template <typename T>
bool validSlot(const T& slot, uint32_t magic)
{
return slot.prefix.magic == magic &&
slot.prefix.schema == kStorageSchemaVersion &&
slot.crc == crc32(&slot, sizeof(slot) - sizeof(slot.crc));
}
template <typename T>
bool validContactSlot(const T& slot)
{
return slot.magic == kContactMagic &&
slot.schema == kStorageSchemaVersion &&
slot.crc == crc32(&slot, sizeof(slot) - sizeof(slot.crc));
}
uint16_t userFlags(const MeshPeerUserFlags& flags, bool deleted)
{
uint16_t result = deleted ? kDeleted : 0U;
result |= flags.favorite ? kFavorite : 0U;
result |= flags.ignored ? kIgnored : 0U;
result |= flags.trusted ? kTrusted : 0U;
return result;
}
MeshPeerUserFlags decodeUserFlags(uint16_t flags)
{
MeshPeerUserFlags result{};
result.favorite = (flags & kFavorite) != 0U;
result.ignored = (flags & kIgnored) != 0U;
result.trusted = (flags & kTrusted) != 0U;
return result;
}
void encodeNodeFacts(NodeFactsSlot& out, const MeshPeerNodeFacts& facts)
{
std::memcpy(out.short_name, facts.short_name, sizeof(out.short_name));
std::memcpy(out.long_name, facts.long_name, sizeof(out.long_name));
out.role = facts.role;
out.hw_model = facts.hw_model;
out.channel = facts.channel;
out.hops_away = facts.hops_away;
out.flags = facts.has_macaddr ? 0x01U : 0U;
out.flags |= facts.via_mqtt ? 0x02U : 0U;
std::memcpy(out.macaddr, facts.macaddr, sizeof(out.macaddr));
}
void decodeNodeFacts(MeshPeerNodeFacts& out, const NodeFactsSlot& facts)
{
std::memcpy(out.short_name, facts.short_name, sizeof(out.short_name));
out.short_name[sizeof(out.short_name) - 1U] = '\0';
std::memcpy(out.long_name, facts.long_name, sizeof(out.long_name));
out.long_name[sizeof(out.long_name) - 1U] = '\0';
out.role = facts.role;
out.hw_model = facts.hw_model;
out.channel = facts.channel;
out.hops_away = facts.hops_away;
out.has_macaddr = (facts.flags & 0x01U) != 0U;
out.via_mqtt = (facts.flags & 0x02U) != 0U;
std::memcpy(out.macaddr, facts.macaddr, sizeof(out.macaddr));
}
void encodePrefix(PeerPrefix& out, const PeerProjection& projection)
{
const MeshPeerRecord& record = projection.record;
out.flags = projection.deleted ? kDeleted : 0U;
out.source = static_cast<uint8_t>(record.source);
out.identity_kind = static_cast<uint8_t>(record.identity.kind);
out.node_id = record.identity.node_id;
out.first_seen_s = record.first_seen_s;
out.last_seen_s = record.last_seen_s;
std::memcpy(out.display_name,
record.display_name,
sizeof(out.display_name));
const MeshPeerObservations& observations = record.observations;
if (observations.has_snr)
{
out.flags |= kHasSnr;
out.snr = observations.snr;
}
if (observations.has_rssi)
{
out.flags |= kHasRssi;
out.rssi = observations.rssi;
}
if (observations.has_device_metrics)
{
out.flags |= kHasMetrics;
const contacts::NodeDeviceMetrics& metrics =
observations.device_metrics;
out.metrics_flags |= metrics.has_battery_level ? kHasBattery : 0U;
out.metrics_flags |= metrics.has_voltage ? kHasVoltage : 0U;
out.metrics_flags |= metrics.has_channel_utilization
? kHasChannelUtilization
: 0U;
out.metrics_flags |= metrics.has_air_util_tx ? kHasAirUtilization : 0U;
out.metrics_flags |= metrics.has_uptime_seconds ? kHasUptime : 0U;
out.battery_level = metrics.battery_level;
out.voltage = metrics.voltage;
out.channel_utilization = metrics.channel_utilization;
out.air_util_tx = metrics.air_util_tx;
out.uptime_seconds = metrics.uptime_seconds;
}
if (observations.has_position)
{
out.flags |= kHasPosition;
const contacts::NodePosition& position = observations.position;
out.position_flags = position.valid ? kPositionValid : 0U;
out.position_flags |= position.has_altitude ? kPositionHasAltitude : 0U;
out.latitude_i = position.latitude_i;
out.longitude_i = position.longitude_i;
out.altitude = position.altitude;
out.position_timestamp = position.timestamp;
out.precision_bits = position.precision_bits;
out.pdop = position.pdop;
out.hdop = position.hdop;
out.vdop = position.vdop;
out.gps_accuracy_mm = position.gps_accuracy_mm;
}
}
void decodePrefix(MeshProtocol protocol,
const PeerPrefix& prefix,
PeerProjection& projection)
{
projection = PeerProjection{};
projection.deleted = (prefix.flags & kDeleted) != 0U;
MeshPeerRecord& record = projection.record;
record.valid = true;
record.identity.protocol = canonicalProtocol(protocol);
record.identity.kind =
static_cast<MeshPeerIdentityKind>(prefix.identity_kind);
record.identity.node_id = prefix.node_id;
record.source = static_cast<MeshPeerSource>(prefix.source);
record.first_seen_s = prefix.first_seen_s;
record.last_seen_s = prefix.last_seen_s;
std::memcpy(record.display_name,
prefix.display_name,
sizeof(record.display_name));
record.display_name[sizeof(record.display_name) - 1U] = '\0';
record.observations.has_snr = (prefix.flags & kHasSnr) != 0U;
record.observations.snr = prefix.snr;
record.observations.has_rssi = (prefix.flags & kHasRssi) != 0U;
record.observations.rssi = prefix.rssi;
record.observations.has_device_metrics =
(prefix.flags & kHasMetrics) != 0U;
contacts::NodeDeviceMetrics& metrics =
record.observations.device_metrics;
metrics.has_battery_level = (prefix.metrics_flags & kHasBattery) != 0U;
metrics.battery_level = prefix.battery_level;
metrics.has_voltage = (prefix.metrics_flags & kHasVoltage) != 0U;
metrics.voltage = prefix.voltage;
metrics.has_channel_utilization =
(prefix.metrics_flags & kHasChannelUtilization) != 0U;
metrics.channel_utilization = prefix.channel_utilization;
metrics.has_air_util_tx =
(prefix.metrics_flags & kHasAirUtilization) != 0U;
metrics.air_util_tx = prefix.air_util_tx;
metrics.has_uptime_seconds = (prefix.metrics_flags & kHasUptime) != 0U;
metrics.uptime_seconds = prefix.uptime_seconds;
record.observations.has_position =
(prefix.flags & kHasPosition) != 0U;
contacts::NodePosition& position = record.observations.position;
position.valid = (prefix.position_flags & kPositionValid) != 0U;
position.has_altitude =
(prefix.position_flags & kPositionHasAltitude) != 0U;
position.latitude_i = prefix.latitude_i;
position.longitude_i = prefix.longitude_i;
position.altitude = prefix.altitude;
position.timestamp = prefix.position_timestamp;
position.precision_bits = prefix.precision_bits;
position.pdop = prefix.pdop;
position.hdop = prefix.hdop;
position.vdop = prefix.vdop;
position.gps_accuracy_mm = prefix.gps_accuracy_mm;
}
bool validPeerIdentityForProtocol(MeshProtocol protocol,
const MeshPeerIdentity& identity)
{
protocol = canonicalProtocol(protocol);
return meshPeerSameProtocol(protocol, identity.protocol) &&
((protocol == MeshProtocol::Meshtastic &&
identity.kind == MeshPeerIdentityKind::NodeId) ||
(protocol == MeshProtocol::MeshCore &&
((identity.kind == MeshPeerIdentityKind::NodeId &&
identity.node_id != 0U) ||
(identity.kind == MeshPeerIdentityKind::PublicKey &&
identity.public_key_len == kMeshPeerMeshCorePublicKeyLen))) ||
(protocol == MeshProtocol::Reticulum &&
((identity.kind == MeshPeerIdentityKind::NodeId &&
identity.node_id != 0U) ||
identity.kind == MeshPeerIdentityKind::ReticulumDestination)));
}
bool validContactIdentityForProtocol(MeshProtocol protocol,
const MeshPeerIdentity& identity)
{
protocol = canonicalProtocol(protocol);
return meshPeerSameProtocol(protocol, identity.protocol) &&
((protocol == MeshProtocol::Meshtastic &&
identity.kind == MeshPeerIdentityKind::NodeId) ||
(protocol == MeshProtocol::MeshCore &&
identity.kind == MeshPeerIdentityKind::PublicKey &&
identity.public_key_len == kMeshPeerMeshCorePublicKeyLen) ||
(protocol == MeshProtocol::Reticulum &&
identity.kind == MeshPeerIdentityKind::ReticulumDestination));
}
} // namespace
std::size_t peerSlotSize(MeshProtocol protocol)
{
switch (canonicalProtocol(protocol))
{
case MeshProtocol::Meshtastic:
return sizeof(MeshtasticPeerSlot);
case MeshProtocol::MeshCore:
return sizeof(MeshCorePeerSlot);
case MeshProtocol::Reticulum:
return sizeof(ReticulumPeerSlot);
default:
return 0U;
}
}
std::size_t contactSlotSize(MeshProtocol protocol)
{
switch (canonicalProtocol(protocol))
{
case MeshProtocol::Meshtastic:
return sizeof(NodeContactSlot);
case MeshProtocol::MeshCore:
return sizeof(MeshCoreContactSlot);
case MeshProtocol::Reticulum:
return sizeof(ReticulumContactSlot);
default:
return 0U;
}
}
bool encodePeerSlot(MeshProtocol protocol,
const PeerProjection& projection,
void* out,
std::size_t out_len)
{
protocol = canonicalProtocol(protocol);
if (!out || !meshPeerRecordIsValid(projection.record) ||
!validPeerIdentityForProtocol(protocol, projection.record.identity))
{
return false;
}
if (protocol == MeshProtocol::Meshtastic &&
out_len == sizeof(MeshtasticPeerSlot))
{
MeshtasticPeerSlot slot{};
encodePrefix(slot.prefix, projection);
encodeNodeFacts(slot.node, projection.record.meshtastic.node);
slot.has_public_key = projection.record.meshtastic.has_public_key ? 1U : 0U;
slot.key_verified =
projection.record.meshtastic.key_manually_verified ? 1U : 0U;
slot.has_next_hop =
projection.record.meshtastic.has_next_hop ? 1U : 0U;
slot.next_hop = projection.record.meshtastic.next_hop;
std::memcpy(slot.public_key,
projection.record.meshtastic.public_key,
sizeof(slot.public_key));
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
if (protocol == MeshProtocol::MeshCore &&
out_len == sizeof(MeshCorePeerSlot))
{
MeshCorePeerSlot slot{};
encodePrefix(slot.prefix, projection);
encodeNodeFacts(slot.node, projection.record.meshcore.node);
slot.has_public_key = projection.record.meshcore.has_public_key ? 1U : 0U;
slot.key_verified =
projection.record.meshcore.public_key_verified ? 1U : 0U;
slot.has_peer_hash = projection.record.meshcore.has_peer_hash ? 1U : 0U;
slot.peer_hash = projection.record.meshcore.peer_hash;
slot.has_next_hop = projection.record.meshcore.has_next_hop ? 1U : 0U;
slot.next_hop = projection.record.meshcore.next_hop;
slot.node_id_hint = projection.record.meshcore.node_id_hint;
if (projection.record.identity.kind ==
MeshPeerIdentityKind::PublicKey)
{
std::memcpy(slot.public_key,
projection.record.identity.public_key,
sizeof(slot.public_key));
}
else if (projection.record.meshcore.has_public_key)
{
std::memcpy(slot.public_key,
projection.record.meshcore.public_key,
sizeof(slot.public_key));
}
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
if (protocol == MeshProtocol::Reticulum &&
out_len == sizeof(ReticulumPeerSlot))
{
ReticulumPeerSlot slot{};
encodePrefix(slot.prefix, projection);
const ReticulumPeerFacts& facts = projection.record.reticulum;
if (projection.record.identity.kind ==
MeshPeerIdentityKind::ReticulumDestination)
{
(void)copyReticulumIdentityHashes(
slot.destination_hash,
slot.identity_hash,
projection.record.identity.reticulum);
}
slot.has_public_keys = facts.has_public_keys ? 1U : 0U;
slot.has_ratchet = facts.has_ratchet ? 1U : 0U;
slot.delivery = facts.delivery ? 1U : 0U;
slot.propagation = facts.propagation ? 1U : 0U;
std::memcpy(slot.enc_pub, facts.enc_pub, sizeof(slot.enc_pub));
std::memcpy(slot.sig_pub, facts.sig_pub, sizeof(slot.sig_pub));
std::memcpy(slot.ratchet_pub,
facts.ratchet_pub,
sizeof(slot.ratchet_pub));
slot.ratchet_seen_s = facts.ratchet_seen_s;
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
return false;
}
bool decodePeerSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
PeerProjection& out_projection)
{
protocol = canonicalProtocol(protocol);
if (!data)
{
return false;
}
if (protocol == MeshProtocol::Meshtastic &&
len == sizeof(MeshtasticPeerSlot))
{
MeshtasticPeerSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validSlot(slot, kPeerMagic))
{
return false;
}
decodePrefix(protocol, slot.prefix, out_projection);
out_projection.record.identity = makeMeshPeerNodeIdentity(
protocol,
slot.prefix.node_id);
decodeNodeFacts(out_projection.record.meshtastic.node, slot.node);
out_projection.record.meshtastic.has_public_key = slot.has_public_key != 0U;
out_projection.record.meshtastic.key_manually_verified =
slot.key_verified != 0U;
out_projection.record.meshtastic.has_next_hop =
slot.has_next_hop != 0U;
out_projection.record.meshtastic.next_hop = slot.next_hop;
std::memcpy(out_projection.record.meshtastic.public_key,
slot.public_key,
sizeof(slot.public_key));
return meshPeerRecordIsValid(out_projection.record);
}
if (protocol == MeshProtocol::MeshCore &&
len == sizeof(MeshCorePeerSlot))
{
MeshCorePeerSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validSlot(slot, kPeerMagic))
{
return false;
}
MeshPeerIdentity identity{};
if (static_cast<MeshPeerIdentityKind>(slot.prefix.identity_kind) ==
MeshPeerIdentityKind::NodeId)
{
identity = makeMeshPeerNodeIdentity(protocol,
slot.prefix.node_id);
}
else if (!makeMeshPeerPublicKeyIdentity(protocol,
slot.public_key,
sizeof(slot.public_key),
identity))
{
return false;
}
decodePrefix(protocol, slot.prefix, out_projection);
out_projection.record.identity = identity;
decodeNodeFacts(out_projection.record.meshcore.node, slot.node);
out_projection.record.meshcore.has_public_key = slot.has_public_key != 0U;
out_projection.record.meshcore.public_key_verified =
slot.key_verified != 0U;
out_projection.record.meshcore.has_peer_hash = slot.has_peer_hash != 0U;
out_projection.record.meshcore.peer_hash = slot.peer_hash;
out_projection.record.meshcore.has_next_hop = slot.has_next_hop != 0U;
out_projection.record.meshcore.next_hop = slot.next_hop;
out_projection.record.meshcore.node_id_hint = slot.node_id_hint;
std::memcpy(out_projection.record.meshcore.public_key,
slot.public_key,
sizeof(slot.public_key));
return meshPeerRecordIsValid(out_projection.record);
}
if (protocol == MeshProtocol::Reticulum &&
len == sizeof(ReticulumPeerSlot))
{
ReticulumPeerSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validSlot(slot, kPeerMagic))
{
return false;
}
decodePrefix(protocol, slot.prefix, out_projection);
if (static_cast<MeshPeerIdentityKind>(slot.prefix.identity_kind) ==
MeshPeerIdentityKind::NodeId)
{
out_projection.record.identity = makeMeshPeerNodeIdentity(
protocol,
slot.prefix.node_id);
}
else
{
out_projection.record.identity = makeMeshPeerReticulumIdentity(
makeReticulumPeerIdentity(slot.destination_hash,
slot.identity_hash));
}
ReticulumPeerFacts& facts = out_projection.record.reticulum;
if (out_projection.record.identity.kind ==
MeshPeerIdentityKind::ReticulumDestination)
{
facts.identity = out_projection.record.identity.reticulum;
}
facts.has_public_keys = slot.has_public_keys != 0U;
facts.has_ratchet = slot.has_ratchet != 0U;
facts.delivery = slot.delivery != 0U;
facts.propagation = slot.propagation != 0U;
std::memcpy(facts.enc_pub, slot.enc_pub, sizeof(facts.enc_pub));
std::memcpy(facts.sig_pub, slot.sig_pub, sizeof(facts.sig_pub));
std::memcpy(facts.ratchet_pub,
slot.ratchet_pub,
sizeof(facts.ratchet_pub));
facts.ratchet_seen_s = slot.ratchet_seen_s;
return meshPeerRecordIsValid(out_projection.record);
}
return false;
}
bool encodeContactSlot(MeshProtocol protocol,
const ContactProjection& projection,
void* out,
std::size_t out_len)
{
protocol = canonicalProtocol(protocol);
if (!out ||
!validContactIdentityForProtocol(protocol, projection.identity))
{
return false;
}
const uint16_t flags = userFlags(projection.flags, projection.deleted);
if (protocol == MeshProtocol::Meshtastic &&
out_len == sizeof(NodeContactSlot))
{
NodeContactSlot slot{};
slot.flags = flags;
slot.node_id = projection.identity.node_id;
std::memcpy(slot.alias, projection.alias, sizeof(slot.alias));
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
if (protocol == MeshProtocol::MeshCore &&
out_len == sizeof(MeshCoreContactSlot))
{
MeshCoreContactSlot slot{};
slot.flags = flags;
slot.node_id_hint = projection.node_id_hint;
std::memcpy(slot.public_key,
projection.identity.public_key,
sizeof(slot.public_key));
std::memcpy(slot.alias, projection.alias, sizeof(slot.alias));
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
if (protocol == MeshProtocol::Reticulum &&
out_len == sizeof(ReticulumContactSlot))
{
ReticulumContactSlot slot{};
slot.flags = flags;
std::memcpy(slot.destination_hash,
projection.identity.reticulum.destination_hash,
sizeof(slot.destination_hash));
std::memcpy(slot.alias, projection.alias, sizeof(slot.alias));
slot.crc = crc32(&slot, sizeof(slot) - sizeof(slot.crc));
std::memcpy(out, &slot, sizeof(slot));
return true;
}
return false;
}
bool decodeContactSlot(MeshProtocol protocol,
const void* data,
std::size_t len,
ContactProjection& out_projection)
{
protocol = canonicalProtocol(protocol);
out_projection = ContactProjection{};
if (!data)
{
return false;
}
uint16_t flags = 0;
if (protocol == MeshProtocol::Meshtastic &&
len == sizeof(NodeContactSlot))
{
NodeContactSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validContactSlot(slot))
{
return false;
}
flags = slot.flags;
out_projection.identity = makeMeshPeerNodeIdentity(protocol,
slot.node_id);
std::memcpy(out_projection.alias, slot.alias, sizeof(slot.alias));
}
else if (protocol == MeshProtocol::MeshCore &&
len == sizeof(MeshCoreContactSlot))
{
MeshCoreContactSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validContactSlot(slot) ||
!makeMeshPeerPublicKeyIdentity(protocol,
slot.public_key,
sizeof(slot.public_key),
out_projection.identity))
{
return false;
}
flags = slot.flags;
out_projection.node_id_hint = slot.node_id_hint;
std::memcpy(out_projection.alias, slot.alias, sizeof(slot.alias));
}
else if (protocol == MeshProtocol::Reticulum &&
len == sizeof(ReticulumContactSlot))
{
ReticulumContactSlot slot{};
std::memcpy(&slot, data, sizeof(slot));
if (!validContactSlot(slot))
{
return false;
}
flags = slot.flags;
out_projection.identity = makeMeshPeerReticulumIdentity(
makeReticulumDestinationIdentity(slot.destination_hash));
std::memcpy(out_projection.alias, slot.alias, sizeof(slot.alias));
}
else
{
return false;
}
out_projection.alias[sizeof(out_projection.alias) - 1U] = '\0';
out_projection.flags = decodeUserFlags(flags);
out_projection.deleted = (flags & kDeleted) != 0U;
return meshPeerIdentityIsValid(out_projection.identity);
}
} // namespace chat::storage::v2
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -40,7 +40,7 @@ using ::platform::esp::arduino_common::storage::SdRuntimeFile;
constexpr const char* kConfigDir = "/trailmate/reticulum";
constexpr const char* kAnnouncesPath = "/trailmate/reticulum/announces.tsv";
constexpr const char* kAnnouncesTempPath = "/trailmate/reticulum/announces.tmp";
constexpr const char* kLxmfAddressesPath = "/mesh/peers.bin";
constexpr const char* kLxmfAddressesPath = "/data/v2/rt/peers.*";
constexpr std::size_t kMaxDirectoryEntries = 1024;
constexpr std::size_t kMaxLineBytes = 4096;
constexpr std::size_t kMaxTsvFields = 14;
+11 -2
View File
@@ -25,6 +25,10 @@ HOT_PATH_PREFIXES = (
HOT_PATH_FILES = {
"platform/esp/arduino_common/src/ble/meshtastic_ble.cpp",
"platform/esp/arduino_common/src/chat/infra/store/protocol_chat_codec.cpp",
"platform/esp/arduino_common/src/chat/infra/store/protocol_peer_codec.cpp",
"platform/esp/arduino_common/src/chat/infra/store/sd_protocol_peer_repository.cpp",
"platform/esp/arduino_common/src/chat/infra/store/sd_store.cpp",
}
HOT_HEADER_PREFIXES = (
@@ -36,6 +40,10 @@ HOT_HEADER_PREFIXES = (
HOT_HEADER_FILES = {
"platform/esp/arduino_common/include/ble/meshtastic_ble.h",
"platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/protocol_chat_codec.h",
"platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/protocol_peer_codec.h",
"platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_protocol_peer_repository.h",
"platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/sd_store.h",
}
APP_CONFIG_PATHS = {
@@ -100,7 +108,7 @@ def is_under(relative: str, prefixes: tuple[str, ...]) -> bool:
def git_tracked_files() -> list[Path]:
try:
result = subprocess.run(
["git", "ls-files"],
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
cwd=REPO_ROOT,
check=True,
text=True,
@@ -109,7 +117,8 @@ def git_tracked_files() -> list[Path]:
)
except (OSError, subprocess.CalledProcessError):
return []
return [REPO_ROOT / line for line in result.stdout.splitlines() if line]
paths = [REPO_ROOT / line for line in result.stdout.splitlines() if line]
return [path for path in paths if path.is_file()]
def walk_source_files() -> list[Path]: