companion: upstream-shaped BaseChatMesh, per-opcode handlers, companion CLI

- BaseChatMesh/ContactInfo/ChannelDetails restored to upstream text, ZephCore
  changes fenced; added to the Shared-file list
- upstream names: handleCmdFrame, writeOKFrame/writeErrFrame,
  addToOfflineQueue, queueMessage; dead dequeue removed
- handleCmdFrame split into handleCmd<Name>() in upstream order;
  CMD_SEND_PATH_DISCOVERY -> _REQ
- CompanionMesh::handleCommand as the single CLI entry point; over-the-air
  TXT_TYPE_CLI_COMMAND from flag-0x10 contacts (upstream command set only)
- V-contact moved to app/VContact.cpp, protocol constants to
  app/CompanionProtocol.h; comment diet (image-identical)
This commit is contained in:
liquidraver
2026-09-24 07:15:44 +02:00
parent 10b36094ae
commit b361c33901
13 changed files with 3848 additions and 4160 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ indent_size = unset
# changes, each fenced `// ZEPHCORE:`. Upstream mixes 2-space and tab indentation
# and has trailing whitespace, so none of it is enforced here. Add a file to this
# list when it is converted to upstream shape.
[{zephcore/src/Mesh.cpp,zephcore/src/Dispatcher.cpp,zephcore/src/Packet.cpp,zephcore/src/StaticPoolPacketManager.cpp,zephcore/include/mesh/MeshCore.h,zephcore/include/mesh/Dispatcher.h,zephcore/include/mesh/Mesh.h,zephcore/include/mesh/Packet.h,zephcore/include/mesh/Identity.h,zephcore/include/mesh/Utils.h,zephcore/include/mesh/SimpleMeshTables.h,zephcore/include/mesh/StaticPoolPacketManager.h}]
[{zephcore/src/Mesh.cpp,zephcore/src/Dispatcher.cpp,zephcore/src/Packet.cpp,zephcore/src/StaticPoolPacketManager.cpp,zephcore/include/mesh/MeshCore.h,zephcore/include/mesh/Dispatcher.h,zephcore/include/mesh/Mesh.h,zephcore/include/mesh/Packet.h,zephcore/include/mesh/Identity.h,zephcore/include/mesh/Utils.h,zephcore/include/mesh/SimpleMeshTables.h,zephcore/include/mesh/StaticPoolPacketManager.h,zephcore/helpers/BaseChatMesh.cpp,zephcore/helpers/BaseChatMesh.h,zephcore/helpers/ContactInfo.h,zephcore/helpers/ChannelDetails.h}]
indent_style = unset
indent_size = unset
tab_width = unset
+2 -2
View File
@@ -1175,14 +1175,14 @@ DIO1 interrupt → Zephyr lora driver → async RX callback
→ direct: process immediately
→ Mesh::onRecvPacket() → decrypt → dispatch by type
→ BaseChatMesh::onPeerDataRecv() → onMessageRecv()
→ CompanionMesh: writeFrame() to phone or queueOfflineMessage()
→ CompanionMesh: writeFrame() to phone or addToOfflineQueue()
```
### 14.2 Sending a Text Message
```
Phone sends CMD_SEND_TXT_MSG via BLE NUS
→ CompanionMesh::handleProtocolFrame()
→ CompanionMesh::handleCmdFrame() → handleCmd<Name>()
→ BaseChatMesh::sendMessage(contact, text)
→ composeMsgPacket(): ECDH secret → AES encrypt → MAC
→ if contact has path: trySendDirect()
+1
View File
@@ -577,6 +577,7 @@ else()
helpers/TransportKeyStore.cpp
helpers/ui/ui_mesh_actions.cpp
app/CompanionMesh.cpp
app/VContact.cpp
# CommonCLI backs the v-contact loopback admin chat (companion_cli_exec in
# main_companion.cpp), which is NOT gated on the wired-USB stack — so it
# must be compiled unconditionally. Builds without CONFIG_LOG/COMPANION_USB/
+1 -1
View File
@@ -94,7 +94,7 @@ static struct k_spinlock usb_tx_lock;
/* Main-thread wake for assembled binary frames (set by init). The byte
* assembly below runs on sysworkq, but V3-protocol parsing must happen on the
* main thread (handleProtocolFrame mutates mesh state shared with loop()), so
* main thread (handleCmdFrame mutates mesh state shared with loop()), so
* we post this event instead of running the parser here. */
static struct k_event *s_mesh_events;
static uint32_t s_mesh_event_ble_rx;
File diff suppressed because it is too large Load Diff
+191 -289
View File
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: MIT
* CompanionMesh - ZephCore Companion device application layer
* CompanionMesh - the companion role (upstream: examples/companion_radio/MyMesh)
*/
#pragma once
@@ -12,7 +12,7 @@
#include <NodePrefs.h>
#include <zephyr/kernel.h>
/* BLE push notification codes */
/* Push notification codes */
#define PUSH_CODE_ADVERT 0x80
#define PUSH_CODE_PATH_UPDATED 0x81
#define PUSH_CODE_SEND_CONFIRMED 0x82
@@ -31,6 +31,13 @@
#define PUSH_CODE_CONTACT_DELETED 0x8F
#define PUSH_CODE_CONTACTS_FULL 0x90
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
/* Telemetry permissions (upstream: helpers/SensorManager.h) */
#define TELEM_PERM_BASE 0x01
#define TELEM_PERM_LOCATION 0x02
#define TELEM_PERM_ENVIRONMENT 0x04
/* Auto-add config bitmask */
#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0)
#define AUTO_ADD_CHAT (1 << 1)
@@ -38,36 +45,33 @@
#define AUTO_ADD_ROOM_SERVER (1 << 3)
#define AUTO_ADD_SENSOR (1 << 4)
/* Canonical definition is in ZephyrBLE.h; guard here for TUs that don't include it */
/* Canonical definition is in ZephyrBLE.h */
#ifndef MAX_FRAME_SIZE
#define MAX_FRAME_SIZE 176
#endif
/* 1 header + 32 pubkey + 1 type + 1 flags + 1 path_len + 64 path + 32 name + 4*4 fields = 148 */
/* 1 header + 32 pubkey + 1 type + 1 flags + 1 path_len + 64 path + 32 name + 4*4 fields */
#define CONTACT_FRAME_SIZE 148
/* Offline message queue depth */
#ifdef CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE
#define OFFLINE_QUEUE_SIZE CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE
#else
#define OFFLINE_QUEUE_SIZE 16
#endif
/* Pending ACK tracking slots */
#ifdef CONFIG_ZEPHCORE_ACK_TABLE_SIZE
#define ACK_TABLE_SIZE CONFIG_ZEPHCORE_ACK_TABLE_SIZE
#else
#define ACK_TABLE_SIZE 16
#endif
/* Recently-heard advert path slots */
#ifdef CONFIG_ZEPHCORE_ADVERT_PATH_TABLE_SIZE
#define ADVERT_PATH_TABLE_SIZE CONFIG_ZEPHCORE_ADVERT_PATH_TABLE_SIZE
#else
#define ADVERT_PATH_TABLE_SIZE 16
#endif
/* Cached advert path entry */
/* A recently heard advert's path */
struct AdvertPath {
uint8_t pubkey_prefix[7];
uint8_t path_len;
@@ -76,39 +80,26 @@ struct AdvertPath {
uint8_t path[MAX_PATH_SIZE];
};
/* BLE push notification callback */
typedef void (*PushCallback)(uint8_t code, const uint8_t *data, size_t len);
/* BLE write frame callback */
typedef size_t (*WriteFrameCallback)(const uint8_t *data, size_t len);
/* Battery millivolt read callback */
typedef uint16_t (*GetBatteryCallback)(void);
/* Radio reconfigure callback. preset_changed = the channel or the modem
* config moved (freq/bw/sf), not just TX power — the adaptive-CAD state is
* tied to those and has to be reset with them (see main_companion). */
/* preset_changed: freq/bw/sf moved, not just TX power; the adaptive-CAD state
* is tied to those and is reset with them. */
typedef void (*RadioReconfigureCallback)(bool preset_changed);
/* BLE PIN change callback */
typedef void (*PinChangeCallback)(uint32_t new_pin);
/* Companion CLI execution callback — runs a text-CLI line and fills `reply`
* (buffer is COMPANION_CLI_REPLY_SIZE). Registered by main_companion so every
* companion-side CLI entry point shares one CommonCLI instance: the USB text
* CLI, the v-contact chat, and CMD_RUN_CLI_COMMAND from the app. */
/* Runs one CLI command for CompanionMesh::handleCommand (prefix already
* stripped). main_companion owns the CommonCLI behind it. sender_timestamp 0
* means local, where the buffer is COMPANION_CLI_PREFIX_ROOM +
* COMPANION_CLI_REPLY_SIZE; a remote buffer is CLI_REMOTE_REPLY_SIZE.
* reply_hdr_used is what the reflected prefix took of it. */
#define COMPANION_CLI_REPLY_SIZE 256
typedef void (*CompanionCLICallback)(const char *line, char *reply);
#define COMPANION_CLI_PREFIX_ROOM 3
typedef void (*CompanionCLICallback)(const char *command, uint32_t sender_timestamp,
uint8_t reply_hdr_used, char *reply);
/**
* CompanionMesh: Application layer for ZephCore Companion device
*
* Extends BaseChatMesh with:
* - BLE protocol frame handling
* - Offline message queue
* - Push notifications for incoming messages/adverts
* - ACK tracking for sent messages
*/
class CompanionMesh : public BaseChatMesh, public DataStoreHost {
public:
CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
@@ -120,125 +111,64 @@ public:
int getOfflineQueueCount() const { return _offline_queue_count; }
/**
* Build and send a self advert using the configured location policy,
* path-hash mode, and default transport scope. This is the canonical
* path shared by the BLE/USB CMD_SEND_SELF_ADVERT handler and the UI
* (button / joystick) advert actions, so flood adverts always honor
* prefs.path_hash_mode and the region scope regardless of trigger.
*
* @param flood true for a flood advert, false for zero-hop.
* @return false if the packet pool was full (advert not sent).
*/
/* The one self-advert path (app command and UI buttons alike), so every
* advert honours the location policy, path-hash mode and default scope.
* False if the packet pool was full. */
bool sendSelfAdvert(bool flood) override;
/**
* Handle a protocol frame from BLE.
* Returns true if frame was handled.
*/
bool handleProtocolFrame(const uint8_t *data, size_t len);
/* One companion-protocol frame from the app; true if handled. */
bool handleCmdFrame(const uint8_t *data, size_t len);
/**
* Set callback for BLE push notifications.
*/
void setPushCallback(PushCallback cb) { _push_cb = cb; }
/**
* Set callback for writing response frames to BLE.
*/
void setWriteFrameCallback(WriteFrameCallback cb) { _write_cb = cb; }
/**
* Set callback for getting battery voltage.
*/
void setBatteryCallback(GetBatteryCallback cb) { _batt_cb = cb; }
/**
* Set callback for radio reconfigure.
*/
void setRadioReconfigureCallback(RadioReconfigureCallback cb) { _radio_reconfig_cb = cb; }
/**
* Set callback for BLE PIN change.
*/
void setPinChangeCallback(PinChangeCallback cb) { _pin_change_cb = cb; }
/* ---- V-contact: loopback admin contact ("v<node_name>") ----
* A synthesized CHAT contact visible only to the connected BLE/USB app.
* Messages to it are short-circuited into the CLI before any packet is
* created — nothing ever reaches the dispatcher or the radio. Its pubkey
* is the Ed25519 point of a keypair seeded from
* SHA256("zc-vcontact" || self prv_key || counter) — a real point, because
* strict clients decompress peer keys and reject anything else. The private
* half is derived and dropped: never stored, never used. The key is never
* registered in the RF RX matching path, so over-the-air traffic addressed
* to it is inert. */
void setCLICallback(CompanionCLICallback cb) { _cli_exec_cb = cb; }
/* The companion CLI (upstream MyMesh::handleCommand). Front-ends: the app's
* CMD_RUN_CLI_COMMAND, the USB text console and the v-contact chat (all
* local, sender_timestamp 0), and TXT_TYPE_CLI_COMMAND from a contact with
* flag 0x10. False for an unknown command. */
bool handleCommand(const char *command, uint32_t sender_timestamp, char *reply);
/* ---- V-contact ("v<node_name>"), see app/VContact.cpp ----
* A chat contact that exists only toward the connected app; messages to it
* run the CLI and never reach the radio. */
bool isVContactEnabled() const { return prefs.v_contact_enabled != 0; }
/** Queue an unsolicited v-contact message (battery alert, restart reason).
* Goes through the offline queue — delivered on next app connect/sync. */
/* Unsolicited notice (battery alert, restart reason), via the offline queue. */
void vcontactNotify(const char *text);
/** Push the v-contact to a connected app as a NEW_ADVERT (call after
* runtime enable or rename). No-op push when nothing is connected. */
/* Push it as NEW_ADVERT after a runtime enable or rename. */
void vcontactPushAdvert();
/** Push CONTACT_DELETED for the v-contact (call after runtime disable). */
/* Push CONTACT_DELETED after a runtime disable. */
void vcontactPushDeleted();
/** Clock became (or may have become) valid — activate the v-contact if it
* was deferred (no more 1970 adverts) and flush buffered notices so they
* carry sane timestamps. Self-gating; safe to call speculatively. Hooked
* at CMD_APP_START, CMD_SET_DEVICE_TIME, and GPS time sync. */
/* The clock may have become valid: activate a deferred v-contact and flush
* buffered notices. Self-gating, safe to call speculatively. */
void vcontactClockSynced();
/** True while a v-contact delivery-ack is still waiting to be emitted.
* Reboot-class CLI commands gate on this (plus transport TX idle) so the
* ack is not cut off by the reset it just scheduled. */
/* A delivery ack is still waiting; reboot-class commands wait for it. */
bool vcontactConfirmPending() const { return _vcontact_confirm_ack != 0; }
/**
* Continue contact iteration (call each main loop iteration).
* Returns true if contacts are still being sent.
*/
/* Streams the contact dump; call each loop. True while contacts remain. */
bool continueContactIteration();
/**
* Cancel contact iteration silently (no frame sent).
* Call on BLE disconnect — there's nobody to send CONTACT_END to.
*/
/* On disconnect: nobody to send CONTACT_END to. */
void cancelContactIterator() { _contact_iter_active = false; }
/** True while a contact dump is in progress (drives the stall watchdog). */
/* Dump state for the stall watchdog: it re-kicks only if the index stops. */
bool isContactIterActive() const { return _contact_iter_active; }
/** Dump progress cursor — the watchdog re-kicks only if this stops moving. */
int getContactIterIdx() const { return _contact_iter_idx; }
/**
* Offline-queue stall watchdog — call from the housekeeping tick.
*
* A queued message reaches the app through exactly one best-effort
* PUSH_CODE_MSG_WAITING, which the app answers with CMD_SYNC_NEXT_MESSAGE.
* Nothing retried that push: if it was dropped (BLE congestion) or
* suppressed (_vcontact_hold_msgwait), or the app simply ignored it because
* it believed a sync was already running, the queue sat untouched until the
* next connect — the app showed a delivered message with no reply, then
* received the whole backlog at once on reconnect. The contact dump and
* advertising both already have a watchdog here; the message queue did not.
*
* MSG_WAITING is one of the three genuinely idempotent push codes (see
* zephcore_ble_send's overflow note), so a duplicate prompt costs nothing.
*/
/* Housekeeping tick: re-prompt MSG_WAITING when the offline queue has gone
* quiet. A queued message reaches the app through one best-effort prompt;
* if that was dropped, held, or ignored, the queue sat until reconnect.
* MSG_WAITING is idempotent, so a duplicate costs nothing. */
void msgWaitingWatchdog();
/**
* Cancel pending message sync. Un-ACKed message stays in queue.
* Call on BLE disconnect so the message is re-sent on reconnect.
*/
/* On disconnect: the un-ACKed message stays queued and is re-sent. */
void cancelSyncPending() { _sync_pending = false; }
/**
* Free the Ed25519 signing buffer if allocated.
* Call on BLE disconnect to prevent 8KB leak when disconnect
* occurs between CMD_SIGN_START and CMD_SIGN_FINISH.
*/
/* On disconnect: free the 8 KB sign buffer a session may have abandoned
* between CMD_SIGN_START and CMD_SIGN_FINISH. */
void cleanupSignState() {
if (_sign_data) {
delete[] _sign_data;
@@ -248,38 +178,18 @@ public:
_sign_data_capacity = 0;
}
/**
* Get BLE device name for advertising.
*/
const char *getDeviceName() const { return prefs.node_name[0] ? prefs.node_name : nullptr; }
/**
* Get recently heard advert paths.
*/
int getRecentlyHeard(AdvertPath dest[], int max_num);
/**
* Find advert path by pubkey prefix.
*/
const AdvertPath *findAdvertPath(const uint8_t *pubkey_prefix, int prefix_len);
/**
* Queue a locally-originated DM into the BLE offline queue and signal
* MSG_WAITING. The frame uses path_len = OUT_PATH_SENT (0xFE) and the
* body is prefixed with "(>>✓) " on delivery or "(>>✗) " on failure so
* the phone app shows a visible outcome indicator without needing
* protocol-level support.
*/
/* A DM sent from the device UI, queued for the app with path_len
* OUT_PATH_SENT and a "(>>✓) "/"(>>✗) " delivery marker. */
void queueLocalSentContactMessage(const ContactInfo &contact, uint32_t timestamp,
const char *text, bool delivered);
/**
* Queue a locally-originated channel message into the BLE offline queue
* and signal MSG_WAITING. The body is rendered as
* "<heard-marker> <node_name>: <text>" — heard_repeat picks
* "(>>✓) " (at least one neighbor repeated the flood) vs "(>>✗) "
* (no repeats heard within the joystick UI's feedback window).
*/
/* A channel message sent from the device UI, queued for the app as
* "<marker> <node_name>: <text>"; the marker says whether a repeat was heard. */
void queueLocalSentChannelMessage(uint8_t channel_idx, uint32_t timestamp,
const char *text, bool heard_repeat);
@@ -292,20 +202,20 @@ public:
/* Mesh time sync */
MeshTimeSync *getMeshTimeSync() { return &_timesync; }
void noteGPSTimeSync() { _timesync.noteGPSSync((uint32_t)(k_uptime_get() / 1000)); }
/* Paced evaluation — called from the housekeeping event (loop() only runs
* on packet-driven events). */
/* From the housekeeping event: loop() only runs on packet events. */
void timeSyncTick();
/* Prefs (includes node_lat/lon) */
NodePrefs prefs;
protected:
/* BaseChatMesh virtual implementations */
/* BaseChatMesh */
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t *path) override;
ContactInfo *processAck(const uint8_t *data) override;
void onContactPathUpdated(const ContactInfo &contact) override;
void onMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override;
void onCommandDataRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override;
void onCLICommandRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text, char *reply) override;
void onSignedMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override;
uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override;
uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override;
@@ -316,40 +226,34 @@ protected:
uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) override;
void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override;
/* Raw packet logging for app RX log */
/* Raw packet logging for the app's RX log */
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
void logRx(mesh::Packet *pkt, int len, float score) override;
void logTx(mesh::Packet *pkt, int len) override;
/* Trace path response */
void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override;
/* Control data response (repeater discovery, etc) */
void onControlDataRecv(mesh::Packet *packet) override;
/* Raw data response (custom packets) */
void onRawDataRecv(mesh::Packet *packet) override;
/* Packet forwarding (client repeat / offgrid mode) */
/* Client repeat / off-grid forwarding */
bool allowPacketForward(const mesh::Packet *packet) override;
/* Path discovery - intercept path data before base class strips it */
/* Path discovery: sees the path data before the base class strips it */
bool onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8_t in_path_len,
uint8_t *out_path, uint8_t out_path_len, uint8_t extra_type,
uint8_t *extra, uint8_t extra_len) override;
/* Flood scope - scoped sending for region filtering */
/* Region-scoped flooding */
void sendFloodScoped(const TransportKey &scope, mesh::Packet *pkt, uint32_t delay_millis);
void sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis = 0) override;
void sendFloodScoped(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t delay_millis = 0) override;
/* Dispatcher tuning (uses prefs) */
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
/* Companion initial flood jitter is fixed-window; passive flood
* tracking is not needed unless forwarding is enabled. */
/* Fixed-window initial jitter; passive flood tracking only matters when
* forwarding. */
bool passivelyTrackFloods() const override { return false; }
uint32_t getInitialFloodJitter(const mesh::Packet *packet) override;
@@ -364,7 +268,7 @@ protected:
}
}
/* Auto-add filtering overrides */
/* Auto-add filtering */
bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override;
bool shouldOverwriteWhenFull() const override;
@@ -372,7 +276,6 @@ protected:
void onContactsFull() override;
void onContactOverwrite(const uint8_t *pub_key) override;
/* Storage overrides */
int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override;
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) override;
@@ -384,17 +287,16 @@ private:
RadioReconfigureCallback _radio_reconfig_cb;
PinChangeCallback _pin_change_cb;
/* Contact iteration state */
/* Contact dump. The bound and the v-contact's inclusion are snapshotted at
* PACKET_CONTACT_START, so the dump never streams more than the total it
* announced: the table grows mid-dump, and the v-contact appears the moment
* a cold-booted clock becomes valid. */
bool _contact_iter_active;
int _contact_iter_idx;
/* Table bound and v-contact inclusion are snapshotted at PACKET_CONTACT_START
* so the dump can never stream more entries than the total it promised: the
* table grows from inbound adverts mid-dump, and vcontactReady() flips false
* ->true the moment a cold-booted clock goes valid (CMD_SET_DEVICE_TIME). */
int _contact_iter_num;
bool _contact_iter_vc;
uint32_t _contact_iter_lastmod;
uint32_t _contact_iter_since; /* Filter: only send contacts with lastmod > this */
uint32_t _contact_iter_since; /* only contacts with lastmod > this */
/* Offline message queue */
struct QueuedFrame {
@@ -405,9 +307,8 @@ private:
int _offline_queue_head;
int _offline_queue_tail;
int _offline_queue_count;
bool _sync_pending; /* true = last peeked message not yet ACKed by phone */
bool _sync_pending; /* the last peeked message is not yet ACKed by the app */
/* ACK tracking table */
struct AckEntry {
uint32_t expected_ack;
uint32_t sent_time;
@@ -416,70 +317,55 @@ private:
};
AckEntry _ack_table[ACK_TABLE_SIZE];
/* Advert path table for tracking recently heard nodes */
AdvertPath _advert_paths[ADVERT_PATH_TABLE_SIZE];
/* Signing state */
/* CMD_SIGN_* state */
uint8_t *_sign_data;
uint32_t _sign_data_len;
uint32_t _sign_data_capacity;
/* Pending request tracking (for response matching) */
/* Pending requests, for matching responses */
uint32_t _pending_login;
uint32_t _pending_status;
uint32_t _pending_telemetry;
uint32_t _pending_discovery;
uint32_t _pending_req;
#ifdef CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK
uint32_t _pending_joystick_ping_tag; /* tag-based match, checked before pubkey-based _pending_status */
uint32_t _pending_joystick_admin_tag; /* same protection for admin binary requests */
uint32_t _pending_joystick_ping_tag; /* matched by tag, before _pending_status */
uint32_t _pending_joystick_admin_tag; /* same, for admin binary requests */
#endif
/* Lazy contacts/channels write - batches rapid updates */
/* Lazy contacts/channels writes */
int64_t _dirty_contacts_expiry;
int64_t _dirty_channels_expiry;
static constexpr int64_t LAZY_WRITE_DELAY_MS = 5000; /* 5 seconds, matches Arduino */
static constexpr int64_t LAZY_WRITE_DELAY_MS = 5000; /* as upstream */
/* Deadline for liveness-only contact updates — a re-heard advert from a
* contact we already know, where the only fields that moved are
* last_advert_timestamp and lastmod.
*
* saveContacts() rewrites the WHOLE file (fixed 152-byte records, no
* incremental path), which on a board without external flash is ~47 KB
* into a 128 KB LittleFS partition. Measured on a T1000-E 2026-08-24:
* 21 full rewrites in 90 minutes, one per advert arrival, because every
* re-advert marked the file dirty on the same 5 s deadline as a real
* change. That is a flash-wear problem on a battery tracker.
*
* Liveness still persists — it just waits, so an hour of re-adverts
* costs one write instead of fourteen. A substantive change (new
* contact, message, path update) still pulls the deadline back in. */
/* Deadline for a liveness-only change (a known contact re-advertised).
* saveContacts() rewrites the whole file (~47 KB without external flash);
* on the 5 s deadline a T1000-E did 21 rewrites in 90 minutes, one per
* advert. A substantive change still pulls the deadline in. */
static constexpr int64_t LAZY_WRITE_LIVENESS_MS = 600000; /* 10 minutes */
void onLoginSent(const ContactInfo &contact) override;
void onChannelAdded(ChannelDetails *ch) override;
void markContactsDirty(bool substantive = true);
void markChannelsDirty();
void flushDirtyContacts();
void flushDirtyChannels();
/* Mesh time sync (forward-only: our clock stamps outgoing DMs and peers
* hold per-sender replay high-water marks) */
/* Forward-only: our clock stamps outgoing DMs, and peers keep per-sender
* replay high-water marks. */
MeshTimeSync _timesync{FIRMWARE_BUILD_EPOCH, true};
void onAdvertTimeSample(const mesh::Identity &id, uint32_t timestamp,
uint8_t hops) override;
/* Protocol version negotiation */
uint8_t _app_target_ver;
/* Flood scope for transport filtering (all zeros = disabled) */
TransportKey _send_scope;
bool _send_scope_force_unscoped;
/* The joystick ping tag is independent of app requests and not cleared. */
void clearPendingReqs() {
_pending_login = _pending_status = _pending_telemetry = _pending_discovery = _pending_req = 0;
/* _pending_joystick_ping_tag is intentionally NOT cleared here
* joystick ping is independent of BLE request state. */
}
#ifdef CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK
@@ -488,129 +374,147 @@ public:
void clearJoystickPingTag() { _pending_joystick_ping_tag = 0; }
void setJoystickAdminTag(uint32_t tag) { _pending_joystick_admin_tag = tag; }
void clearJoystickAdminTag() { _pending_joystick_admin_tag = 0; }
/* Force a contacts-flush schedule from outside (joystick UI clears a
* stale out_path_len during 5th-attempt fallback flood and needs the
* change to persist to /ext/contacts3). */
/* The joystick UI clears a stale out_path_len on its fallback flood. */
void markContactsDirtyPublic() { markContactsDirty(); }
private:
#endif
bool writeFrame(const uint8_t *data, size_t len);
void sendPacketOk();
void sendPacketError(uint8_t code);
/* Emit the PACKET_SENT response: [PACKET_SENT][is_flood][tag:4][est_timeout:4]. */
/* Companion protocol opcode handlers, in upstream handleCmdFrame order */
bool handleCmdDeviceQuery(const uint8_t *data, size_t len);
bool handleCmdAppStart(const uint8_t *data, size_t len);
bool handleCmdRunCliCommand(const uint8_t *data, size_t len);
bool handleCmdSendTxtMsg(const uint8_t *data, size_t len);
bool handleCmdSendChannelTxtMsg(const uint8_t *data, size_t len);
bool handleCmdSendChannelData(const uint8_t *data, size_t len);
bool handleCmdGetContacts(const uint8_t *data, size_t len);
bool handleCmdSetAdvertName(const uint8_t *data, size_t len);
bool handleCmdSetAdvertLatlon(const uint8_t *data, size_t len);
bool handleCmdGetDeviceTime(const uint8_t *data, size_t len);
bool handleCmdSetDeviceTime(const uint8_t *data, size_t len);
bool handleCmdSendSelfAdvert(const uint8_t *data, size_t len);
bool handleCmdResetPath(const uint8_t *data, size_t len);
bool handleCmdAddUpdateContact(const uint8_t *data, size_t len);
bool handleCmdRemoveContact(const uint8_t *data, size_t len);
bool handleCmdShareContact(const uint8_t *data, size_t len);
bool handleCmdGetContactByKey(const uint8_t *data, size_t len);
bool handleCmdExportContact(const uint8_t *data, size_t len);
bool handleCmdImportContact(const uint8_t *data, size_t len);
bool handleCmdSyncNextMessage(const uint8_t *data, size_t len);
bool handleCmdSetRadioParams(const uint8_t *data, size_t len);
bool handleCmdSetRadioTxPower(const uint8_t *data, size_t len);
bool handleCmdSetTuningParams(const uint8_t *data, size_t len);
bool handleCmdGetTuningParams(const uint8_t *data, size_t len);
bool handleCmdSetOtherParams(const uint8_t *data, size_t len);
bool handleCmdSetPathHashMode(const uint8_t *data, size_t len);
bool handleCmdReboot(const uint8_t *data, size_t len);
bool handleCmdGetBattAndStorage(const uint8_t *data, size_t len);
bool handleCmdExportPrivateKey(const uint8_t *data, size_t len);
bool handleCmdImportPrivateKey(const uint8_t *data, size_t len);
bool handleCmdSendRawData(const uint8_t *data, size_t len);
bool handleCmdSendLogin(const uint8_t *data, size_t len);
bool handleCmdSendAnonReq(const uint8_t *data, size_t len);
bool handleCmdSendStatusReq(const uint8_t *data, size_t len);
bool handleCmdSendPathDiscoveryReq(const uint8_t *data, size_t len);
bool handleCmdSendTelemetryReq(const uint8_t *data, size_t len);
bool handleCmdSendBinaryReq(const uint8_t *data, size_t len);
bool handleCmdHasConnection(const uint8_t *data, size_t len);
bool handleCmdLogout(const uint8_t *data, size_t len);
bool handleCmdGetChannel(const uint8_t *data, size_t len);
bool handleCmdSetChannel(const uint8_t *data, size_t len);
bool handleCmdSignStart(const uint8_t *data, size_t len);
bool handleCmdSignData(const uint8_t *data, size_t len);
bool handleCmdSignFinish(const uint8_t *data, size_t len);
bool handleCmdSendTracePath(const uint8_t *data, size_t len);
bool handleCmdSetDevicePin(const uint8_t *data, size_t len);
bool handleCmdGetCustomVars(const uint8_t *data, size_t len);
bool handleCmdSetCustomVar(const uint8_t *data, size_t len);
bool handleCmdGetAdvertPath(const uint8_t *data, size_t len);
bool handleCmdGetStats(const uint8_t *data, size_t len);
bool handleCmdFactoryReset(const uint8_t *data, size_t len);
bool handleCmdSetFloodScopeKey(const uint8_t *data, size_t len);
bool handleCmdSetDefaultFloodScope(const uint8_t *data, size_t len);
bool handleCmdGetDefaultFloodScope(const uint8_t *data, size_t len);
bool handleCmdSendControlData(const uint8_t *data, size_t len);
bool handleCmdSetAutoaddConfig(const uint8_t *data, size_t len);
bool handleCmdGetAutoaddConfig(const uint8_t *data, size_t len);
bool handleCmdGetAllowedRepeatFreq(const uint8_t *data, size_t len);
bool handleCmdSendRawPacket(const uint8_t *data, size_t len);
void writeOKFrame();
void writeErrFrame(uint8_t code);
/* [PACKET_SENT][is_flood][tag:4][est_timeout:4] */
void sendPacketSent(uint8_t result, uint32_t tag, uint32_t est_timeout);
void sendPush(uint8_t code, const uint8_t *data = nullptr, size_t len = 0);
/* Shared body for the recipient/channel sendFloodScoped overloads — both
* resolve to the same default-scope logic (see the TODOs at each site). */
/* The default-scope body shared by the recipient and channel overloads */
void sendFloodScopedDefault(mesh::Packet *pkt, uint32_t delay_millis);
/* Append self-telemetry as Cayenne LPP into `out`, returning bytes written.
* `permissions` gates the LOCATION and ENVIRONMENT sections (battery is
* always included); pass all TELEM_PERM_* bits for unconditional output. */
/* Self telemetry as Cayenne LPP; bytes written. Battery is always included,
* `permissions` gates location and environment. */
int appendSelfTelemetry(uint8_t *out, uint8_t permissions);
/** Serialize a ContactInfo into buf. If header != 0, prepend it.
* Returns total bytes written. buf must be >= CONTACT_FRAME_SIZE. */
/* header != 0 is prepended; buf must hold CONTACT_FRAME_SIZE. */
static size_t serializeContact(uint8_t *buf, const ContactInfo &c, uint8_t header = 0);
void queueOfflineMessage(const uint8_t *data, size_t len);
bool dequeueOfflineMessage(uint8_t *dest, size_t &len);
void addToOfflineQueue(const uint8_t *data, size_t len);
bool peekOfflineMessage(uint8_t *dest, size_t &len);
void confirmOfflineMessage();
bool enqueuePendingChannelInfo(uint8_t idx);
bool sendChannelInfoFrame(uint8_t idx);
void drainPendingChannelInfos();
void queueContactMessage(const ContactInfo &contact, mesh::Packet *pkt,
uint8_t txt_type, uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text);
void queueMessage(const ContactInfo &contact, uint8_t txt_type, mesh::Packet *pkt,
uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text);
/* V-contact internals. _vcontact_lastmod == 0 means "not yet activated":
* the clock was invalid (pre-1970s epoch) when we would have stamped it,
* so the contact is withheld from sync/adverts until a time source
* arrives — otherwise the app shows a 1970 last-heard timestamp. */
CompanionCLICallback _cli_exec_cb;
/* ---- V-contact state (app/VContact.cpp) ---- */
/* _vcontact_lastmod == 0: not activated yet. The clock was invalid when it
* would have been stamped, so it is withheld rather than shown as 1970. */
uint8_t _vcontact_pubkey[PUB_KEY_SIZE];
uint32_t _vcontact_lastmod;
/* Dedupe app resends: a retry reuses the message timestamp (only the
* attempt byte changes). The retry lands several seconds later, after other
* messages, so a single last-seen slot misses it — track a ring of recent
* timestamps instead. */
/* App resends reuse the message timestamp and can land seconds later,
* behind other messages, hence a ring rather than one last-seen slot. */
static const uint8_t VCONTACT_DEDUP_SLOTS = 16;
uint32_t _vcontact_recent_ts[VCONTACT_DEDUP_SLOTS];
uint8_t _vcontact_recent_head;
char _vcontact_pending[2][128]; /* notices buffered while the clock is invalid */
char _vcontact_pending[2][128]; /* notices held while the clock is invalid */
uint8_t _vcontact_pending_count;
/* Suppress a v-contact notice's MSG_WAITING push during the app's initial
* sync. Sent before/mid sync (e.g. the reboot-cause notice flushed at
* CMD_SET_DEVICE_TIME) it makes the app interleave message-sync into the
* contact stream, which trips the "reset iterator on any other command"
* guard and truncates the sync. Held from CMD_APP_START until the first
* PACKET_NO_MORE_MSGS (end of the contacts+messages initial sync).
*
* Bounded by _vcontact_hold_expiry. PACKET_NO_MORE_MSGS used to be the only
* exit, and the app is not obliged to produce it: a session that set the
* latch and then never message-synced to empty kept it forever, so every
* later CLI reply, battery alert and notice queued silently and the user
* saw the backlog only on reconnect. Confirmed on hardware — the app
* re-issues CMD_APP_START when its device-settings screen opens, about 4 s
* before the GPS toggle that appeared to cause it, on a connection already
* hours old and long past its initial sync.
*
* The real hazard (a prompt landing mid contact-dump) is checked directly
* in vcontactMsgWaitHeld(), so this latch is now only the CMD_APP_START ->
* CMD_GET_CONTACTS handoff guard and its deadline is sized accordingly.
* See VCONTACT_HOLD_MAX_MS. */
/* Holds a notice's MSG_WAITING from CMD_APP_START until CMD_GET_CONTACTS:
* a prompt there makes the app interleave a message sync into the contact
* stream. Bounded by _vcontact_hold_expiry, because the app need not ever
* sync to empty; the mid-dump case is checked directly in
* vcontactMsgWaitHeld(). See VCONTACT_HOLD_MAX_MS. */
bool _vcontact_hold_msgwait;
int64_t _vcontact_hold_expiry; /* _ms->getMillis() deadline for the latch */
/** Read the latch, expiring it first. Use instead of touching the flag. */
int64_t _vcontact_hold_expiry; /* _ms->getMillis() deadline */
/* Reads the latch, expiring it first; use instead of the flag. */
bool vcontactMsgWaitHeld();
/* Offline-queue stall watchdog state, both _ms->getMillis(). The watchdog
* re-prompts only once the later of the two has gone quiet. */
/* Offline-queue watchdog: it re-prompts once the later of these is quiet. */
int64_t _last_msgwait_ms; /* last PUSH_CODE_MSG_WAITING we emitted */
int64_t _last_sync_req_ms; /* last CMD_SYNC_NEXT_MESSAGE the app sent */
/** Emit PUSH_CODE_MSG_WAITING and stamp the watchdog. Every prompt goes
* through here so the watchdog cannot fire on top of a live sync. */
int64_t _last_sync_req_ms; /* last CMD_SYNC_NEXT_MESSAGE from the app */
/* Every MSG_WAITING goes through here, so the watchdog never fires over a
* live sync. */
void pushMsgWaiting();
/* App-side delete (CMD_REMOVE_CONTACT for the loopback key) hides the
* v-contact for the rest of the session only — it deliberately does NOT
* touch prefs.v_contact_enabled. The v-contact is an ordinary entry in the
* app's contact list, so a "purge all contacts" walks it like any other and
* used to permanently disable a firmware feature with no way back except
* the USB CLI. The pref is node-side state: `set v.contact off` is the only
* durable disable. Cleared at CMD_APP_START (the session reset). */
/* An app-side delete hides the v-contact for the session only: a "purge
* all contacts" walks it like any other entry. `set v.contact off` is the
* durable disable. Cleared at CMD_APP_START. */
bool _vcontact_app_hidden;
/* Deferred delivery-ack for a v-contact chat message.
*
* The ack itself is computed and PACKET_SENT emitted synchronously (the app
* is waiting on that as the response to its own write), but the
* PUSH_CODE_SEND_CONFIRMED that marks the bubble delivered is held back.
* Emitted inline it lands sub-millisecond after the response to the very
* same write -- before the app has committed the outgoing message to its
* own state -- and the app drops it. It then resends at est_timeout, the
* dedup ring above suppresses the CLI re-run, and the resend's ack is the
* one that finally sticks: one reply, two acks, delivery mark seconds late.
*
* No radio ack can arrive that fast, so the real send path never provokes
* this; the v-contact is the only sub-millisecond acker in the system.
*
* 0 = nothing pending. Emitted by the timer only — see the note on
* VCONTACT_CONFIRM_DELAY_MS for why an inbound frame is not the trigger. */
/* Deferred SEND_CONFIRMED for a v-contact message (0 = none). Emitted
* inline it lands sub-millisecond after the response to the same write,
* before the app has committed the message, and the app drops it; the
* resend's ack then sticks, seconds late. See VCONTACT_CONFIRM_DELAY_MS. */
uint32_t _vcontact_confirm_ack;
/* CONTAINER_OF is offsetof underneath, and offsetof on a non-standard-layout
* type is only conditionally supported — CompanionMesh has base classes and
* virtuals, so it does not qualify (CommonCLI does, which is why the same
* trick is clean there). Wrap the work item in a POD that carries its own
* back-pointer: offsetof stays inside a standard-layout struct, and the
* owner comes from the pointer rather than from pointer arithmetic. */
/* CONTAINER_OF relies on offsetof, which is only conditionally supported on
* a non-standard-layout type like CompanionMesh; this POD carries its own
* back-pointer instead. */
struct ConfirmWork {
struct k_work_delayable work;
CompanionMesh *self;
};
ConfirmWork _vcontact_confirm_work;
static void vcontactConfirmWorkHandler(struct k_work *work);
/** Emit a deferred SEND_CONFIRMED now, if one is pending. Idempotent. */
/* Emit a pending SEND_CONFIRMED now. Idempotent. */
void vcontactFlushConfirm();
bool vcontactClockValid();
bool vcontactReady() {
@@ -618,13 +522,11 @@ private:
}
void buildVContact(ContactInfo &c) const;
bool isVContactKey(const uint8_t *key, int prefix_len) const;
/** (Re)derive _vcontact_pubkey from the current identity. Call on boot and
* whenever the identity changes (CMD_IMPORT_PRIVATE_KEY). */
/* On boot and whenever the identity changes (CMD_IMPORT_PRIVATE_KEY). */
void deriveVContactKey();
/** Intercept protocol frames addressed to the v-contact. Returns true when
* the frame was fully handled (response already written). */
/* True when a frame addressed to the v-contact was fully handled. */
bool vcontactHandleFrame(const uint8_t *data, size_t len);
/** Chunk `text` into offline-queue contact messages from the v-contact. */
/* Chunks `text` into v-contact messages on the offline queue. */
void vcontactQueueText(const char *text);
void addPendingAck(uint32_t expected, int contact_idx);
+121
View File
@@ -0,0 +1,121 @@
/*
* SPDX-License-Identifier: MIT
* Companion protocol: opcodes, response and error codes (upstream keeps these
* at the top of examples/companion_radio/MyMesh.cpp).
*/
#pragma once
#include <stdint.h>
/* Protocol commands (matches Arduino companion_radio) - sorted by opcode */
#define CMD_APP_START 0x01
#define CMD_SEND_TXT_MSG 0x02
#define CMD_SEND_CHANNEL_TXT_MSG 0x03
#define CMD_GET_CONTACTS 0x04
#define CMD_GET_DEVICE_TIME 0x05
#define CMD_SET_DEVICE_TIME 0x06
#define CMD_SEND_SELF_ADVERT 0x07
#define CMD_SET_ADVERT_NAME 0x08
#define CMD_ADD_UPDATE_CONTACT 0x09
#define CMD_SYNC_NEXT_MESSAGE 0x0A
#define CMD_SET_RADIO_PARAMS 0x0B
#define CMD_SET_RADIO_TX_POWER 0x0C
#define CMD_RESET_PATH 0x0D
#define CMD_SET_ADVERT_LATLON 0x0E
#define CMD_REMOVE_CONTACT 0x0F
#define CMD_SHARE_CONTACT 0x10
#define CMD_EXPORT_CONTACT 0x11
#define CMD_IMPORT_CONTACT 0x12
#define CMD_REBOOT 0x13
#define CMD_GET_BATT_AND_STORAGE 0x14
#define CMD_SET_TUNING_PARAMS 0x15
#define CMD_DEVICE_QUERY 0x16
#define CMD_EXPORT_PRIVATE_KEY 0x17
#define CMD_IMPORT_PRIVATE_KEY 0x18
#define CMD_SEND_RAW_DATA 0x19
#define CMD_SEND_LOGIN 0x1A
#define CMD_SEND_STATUS_REQ 0x1B
#define CMD_HAS_CONNECTION 0x1C
#define CMD_LOGOUT 0x1D
#define CMD_GET_CONTACT_BY_KEY 0x1E
#define CMD_GET_CHANNEL 0x1F
#define CMD_SET_CHANNEL 0x20
#define CMD_SIGN_START 0x21
#define CMD_SIGN_DATA 0x22
#define CMD_SIGN_FINISH 0x23
#define CMD_SEND_TRACE_PATH 0x24
#define CMD_SET_DEVICE_PIN 0x25
#define CMD_SET_OTHER_PARAMS 0x26
#define CMD_SEND_TELEMETRY_REQ 0x27
#define CMD_GET_CUSTOM_VARS 0x28
#define CMD_SET_CUSTOM_VAR 0x29
#define CMD_GET_ADVERT_PATH 0x2A
#define CMD_GET_TUNING_PARAMS 0x2B
#define CMD_SEND_BINARY_REQ 0x32
#define CMD_FACTORY_RESET 0x33
#define CMD_SEND_PATH_DISCOVERY_REQ 0x34
#define CMD_SET_FLOOD_SCOPE_KEY 0x36 /* v8+ (renamed from CMD_SET_FLOOD_SCOPE) */
#define CMD_SEND_CONTROL_DATA 0x37
#define CMD_GET_STATS 0x38
#define CMD_SEND_ANON_REQ 0x39
#define CMD_SET_AUTOADD_CONFIG 0x3A
#define CMD_GET_AUTOADD_CONFIG 0x3B
#define CMD_GET_ALLOWED_REPEAT_FREQ 0x3C
#define CMD_SET_PATH_HASH_MODE 0x3D
#define CMD_SEND_CHANNEL_DATA 0x3E
#define CMD_SET_DEFAULT_FLOOD_SCOPE 0x3F /* v11+ */
#define CMD_GET_DEFAULT_FLOOD_SCOPE 0x40 /* v11+ */
#define CMD_SEND_RAW_PACKET 0x41 /* v12+ */
#define CMD_RUN_CLI_COMMAND 0x42 /* v14+ */
/* Response packet types */
#define PACKET_OK 0x00
#define PACKET_ERROR 0x01
#define PACKET_CONTACT_START 0x02
#define PACKET_CONTACT 0x03
#define PACKET_CONTACT_END 0x04
#define PACKET_SELF_INFO 0x05
#define PACKET_SENT 0x06
#define PACKET_CONTACT_MSG_RECV 0x07 /* Legacy - ver < 3 */
#define PACKET_CHANNEL_MSG_RECV 0x08 /* Legacy - ver < 3 */
#define PACKET_CURR_TIME 0x09
#define PACKET_NO_MORE_MSGS 0x0A
#define PACKET_EXPORT_CONTACT 0x0B
#define PACKET_BATTERY 0x0C
#define PACKET_DEVICE_INFO 0x0D
#define PACKET_PRIVATE_KEY 0x0E
#define PACKET_DISABLED 0x0F
#define PACKET_CONTACT_MSG_V3 0x10 /* Contact message for app ver >= 3 */
#define PACKET_CHANNEL_MSG_V3 0x11 /* Channel message for app ver >= 3 */
#define PACKET_CHANNEL_INFO 0x12
#define PACKET_SIGN_START 0x13
#define PACKET_SIGNATURE 0x14
#define PACKET_CUSTOM_VARS 0x15
#define PACKET_ADVERT_PATH 0x16
#define PACKET_TUNING_PARAMS 0x17
#define PACKET_STATS 0x18
#define PACKET_AUTOADD_CONFIG 0x19
#define PACKET_ALLOWED_REPEAT_FREQ 0x1A
#define PACKET_CHANNEL_DATA_RECV 0x1B
#define PACKET_DEFAULT_FLOOD_SCOPE 0x1C
#define PACKET_CLI_REPLY 0x1D /* v14+, reply to CMD_RUN_CLI_COMMAND */
#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)
#define MAX_SIGN_DATA_LEN (8 * 1024)
/* Error codes */
#define ERR_UNSUPPORTED 0x01
#define ERR_NOT_FOUND 0x02
#define ERR_TABLE_FULL 0x03
#define ERR_BAD_STATE 0x04
#define ERR_ILLEGAL_ARG 0x06
/* Max LoRa TX power (dBm) for SX1262 */
#define MAX_LORA_TX_POWER 22
/* Helper to put little-endian values */
static inline void put_le16(uint8_t *p, uint16_t v) { p[0] = v & 0xFF; p[1] = (v >> 8) & 0xFF; }
static inline void put_le32(uint8_t *p, uint32_t v) { p[0] = v & 0xFF; p[1] = (v >> 8) & 0xFF; p[2] = (v >> 16) & 0xFF; p[3] = (v >> 24) & 0xFF; }
static inline uint32_t get_le32(const uint8_t *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); }
+401
View File
@@ -0,0 +1,401 @@
/*
* SPDX-License-Identifier: MIT
* The v-contact ("v<node_name>"): a chat contact that exists only toward the
* connected app. Messages to it run the companion CLI; replies and notices
* (battery alert, restart reason) come back as its messages via the offline
* queue. Invariants:
* - it never enters the contacts table, so never the RF RX matching path;
* - it is intercepted before any send path, so no packet is ever created;
* - nobody holds its private key, so over-the-air traffic to it is inert.
*/
#include "CompanionMesh.h"
#include "CompanionProtocol.h"
#include <mesh/Utils.h>
#include <ZephyrSensorManager.h>
#include <string.h>
#include <stdio.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zephcore_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
void CompanionMesh::buildVContact(ContactInfo &c) const
{
memcpy(c.id.pub_key, _vcontact_pubkey, PUB_KEY_SIZE);
c.type = ADV_TYPE_CHAT;
/* The app owns the flags (favourite, telemetry permissions); with no table
* record they live in prefs. Echoing 0 cleared the favourite on every sync. */
c.flags = prefs.v_contact_flags;
c.out_path_len = 0; /* zero-hop direct — renders as "0 hops" in the app */
c.shared_secret_valid = false;
memset(c.out_path, 0, sizeof(c.out_path));
c.name[0] = 'v';
StrHelper::strzcpy(&c.name[1], prefs.node_name, sizeof(c.name) - 1);
c.last_advert_timestamp = _vcontact_lastmod;
c.lastmod = _vcontact_lastmod;
c.gps_lat = 0;
c.gps_lon = 0;
c.sync_since = 0;
}
bool CompanionMesh::isVContactKey(const uint8_t *key, int prefix_len) const
{
if (!isVContactEnabled()) return false;
if (prefix_len > PUB_KEY_SIZE) prefix_len = PUB_KEY_SIZE;
return memcmp(key, _vcontact_pubkey, prefix_len) == 0;
}
bool CompanionMesh::vcontactClockValid()
{
/* Anything before the firmware build epoch is a never-synced clock. */
return (uint32_t)getRTCClock()->getCurrentTime() >= (uint32_t)FIRMWARE_BUILD_EPOCH;
}
void CompanionMesh::vcontactClockSynced()
{
if (!isVContactEnabled() || !vcontactClockValid()) {
return;
}
if (_vcontact_lastmod == 0) {
/* Deferred activation: first valid time source — stamp and announce.
* From here the contact also appears in CMD_GET_CONTACTS syncs. */
vcontactPushAdvert();
}
/* Flush the notices held for a valid clock, now with real timestamps. */
for (uint8_t i = 0; i < _vcontact_pending_count; i++) {
vcontactQueueText(_vcontact_pending[i]);
}
_vcontact_pending_count = 0;
}
void CompanionMesh::vcontactQueueText(const char *text)
{
/* A queued frame caps at 172 bytes and the V3 header takes 16: split into
* <=150-char messages, preferring line breaks. getCurrentTimeUnique() keeps
* their timestamps increasing so the app orders them. */
static const size_t CHUNK_MAX = 150;
ContactInfo vc;
buildVContact(vc);
const char *p = text;
size_t remaining = strlen(text);
while (remaining > 0) {
size_t take = remaining;
if (take > CHUNK_MAX) {
take = CHUNK_MAX;
for (size_t i = take; i > CHUNK_MAX / 2; i--) {
if (p[i - 1] == '\n') { take = i; break; }
}
}
char chunk[CHUNK_MAX + 1];
memcpy(chunk, p, take);
chunk[take] = '\0';
size_t adv = take;
for (size_t i = 0; i < take; i++) {
if (chunk[i] == '\r') chunk[i] = ' '; /* CRLF CLI output → LF */
}
while (take > 0 && (chunk[take - 1] == '\n' || chunk[take - 1] == ' ')) {
chunk[--take] = '\0'; /* trim trailing break of this bubble */
}
if (take > 0) {
queueMessage(vc, TXT_TYPE_PLAIN, nullptr,
getRTCClock()->getCurrentTimeUnique(), nullptr, 0, chunk);
}
p += adv;
remaining -= adv;
}
/* No prompt while held (the app's own initial sync drains the queue) or
* while the app has hidden the v-contact this session; the messages stay
* queued either way. */
if (!vcontactMsgWaitHeld() && !_vcontact_app_hidden) {
pushMsgWaiting();
} else {
LOG_INF("msgwait: suppressed (hold=%d hidden=%d), %d queued",
(int)_vcontact_hold_msgwait, (int)_vcontact_app_hidden,
_offline_queue_count);
}
}
void CompanionMesh::vcontactNotify(const char *text)
{
if (!isVContactEnabled() || !text || !text[0]) return;
LOG_INF("vcontact notify: %s", text);
if (!vcontactClockValid()) {
/* Would show as 1970: hold it for vcontactClockSynced(). Drop-oldest
* (restart reason + battery is the whole expected population). */
if (_vcontact_pending_count >= (uint8_t)ARRAY_SIZE(_vcontact_pending)) {
memmove(_vcontact_pending[0], _vcontact_pending[1],
sizeof(_vcontact_pending[0]) * (ARRAY_SIZE(_vcontact_pending) - 1));
_vcontact_pending_count = ARRAY_SIZE(_vcontact_pending) - 1;
}
strncpy(_vcontact_pending[_vcontact_pending_count], text,
sizeof(_vcontact_pending[0]) - 1);
_vcontact_pending[_vcontact_pending_count][sizeof(_vcontact_pending[0]) - 1] = '\0';
_vcontact_pending_count++;
return;
}
vcontactQueueText(text);
}
/* How long SEND_CONFIRMED is held after its PACKET_SENT. A timer, not "the
* next inbound frame": the app answers our MSG_WAITING within one connection
* interval, which would flush it almost as early as inline. Must land well
* inside the 3000 ms est_timeout, or the app's resend races it. */
#define VCONTACT_CONFIRM_DELAY_MS 300
void CompanionMesh::vcontactConfirmWorkHandler(struct k_work *work)
{
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
ConfirmWork *cw = CONTAINER_OF(dwork, ConfirmWork, work);
cw->self->vcontactFlushConfirm();
}
void CompanionMesh::vcontactFlushConfirm()
{
/* No lock: a race can at worst emit the (idempotent) push twice. */
uint32_t ack = _vcontact_confirm_ack;
if (ack == 0) {
return;
}
_vcontact_confirm_ack = 0;
k_work_cancel_delayable(&_vcontact_confirm_work.work);
uint8_t ack_push[8];
memcpy(ack_push, &ack, 4);
memset(&ack_push[4], 0, 4); /* trip time: 0 ms — never left the box */
LOG_DBG("vcontact: emitting deferred ack 0x%08x", ack);
sendPush(PUSH_CODE_SEND_CONFIRMED, ack_push, 8);
}
void CompanionMesh::deriveVContactKey()
{
/* The Ed25519 public point of a keypair seeded from
* SHA256("zc-vcontact" || self prv_key || counter).
* - A real point: strict clients decompress peer keys and reject the ~50%
* of random 32-byte strings that are not valid points.
* - Seeded from the private key, so nobody else can link the v-contact to
* this node or derive its private half, which is dropped here unused.
* - The counter skips pub_key[0] of 0x00/0xFF, which MeshCore reserves
* (P = 2/256 per try, so still deterministic). */
static const char vc_salt[] = "zc-vcontact";
uint8_t material[PRV_KEY_SIZE + 1];
uint8_t seed[SEED_SIZE];
mesh::LocalIdentity vc;
/* writeTo() writes prv || pub; max_len PRV_KEY_SIZE asks for prv alone. */
if (self_id.writeTo(material, PRV_KEY_SIZE) != PRV_KEY_SIZE) {
/* Cannot happen; fails loudly instead of seeding off stack garbage. */
LOG_ERR("vcontact: private key unavailable, key not derived");
mesh::Utils::secureZeroize(material, sizeof(material));
return;
}
for (int counter = 0; counter < 256; counter++) {
material[PRV_KEY_SIZE] = (uint8_t)counter;
mesh::Utils::sha256(seed, SEED_SIZE,
(const uint8_t *)vc_salt, sizeof(vc_salt) - 1,
material, sizeof(material));
vc.fromSeed(seed);
if (vc.pub_key[0] != 0x00 && vc.pub_key[0] != 0xFF) break;
}
memcpy(_vcontact_pubkey, vc.pub_key, PUB_KEY_SIZE);
/* Wipe the identity key and the discarded private half. LocalIdentity has
* no wipe and no virtuals, so it is cleared wholesale. */
mesh::Utils::secureZeroize(material, sizeof(material));
mesh::Utils::secureZeroize(seed, sizeof(seed));
mesh::Utils::secureZeroize(&vc, sizeof(vc));
}
void CompanionMesh::vcontactPushAdvert()
{
if (!isVContactEnabled()) return;
/* Hidden by the app this session; it returns at the next CMD_APP_START. */
if (_vcontact_app_hidden) return;
if (!vcontactClockValid()) {
/* Deferred to vcontactClockSynced(); now it would say 1970. */
return;
}
/* Bump lastmod so incremental syncs pick up the rename or re-enable. */
_vcontact_lastmod = (uint32_t)getRTCClock()->getCurrentTime();
ContactInfo vc;
buildVContact(vc);
uint8_t rsp[CONTACT_FRAME_SIZE];
size_t n = serializeContact(rsp, vc); /* no header — push code is separate */
sendPush(PUSH_CODE_NEW_ADVERT, rsp, n);
}
void CompanionMesh::vcontactPushDeleted()
{
sendPush(PUSH_CODE_CONTACT_DELETED, _vcontact_pubkey, PUB_KEY_SIZE);
}
bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len)
{
if (!isVContactEnabled()) return false;
switch (data[0]) {
case CMD_SEND_TXT_MSG:
/* cmd(1) + txt_type(1) + attempt(1) + timestamp(4) + pub_key_prefix(6) + text(N) */
if (len >= 14 && isVContactKey(&data[7], 6)) {
uint8_t txt_type = data[1];
if (txt_type != TXT_TYPE_PLAIN && txt_type != TXT_TYPE_CLI_DATA &&
txt_type != TXT_TYPE_CLI_COMMAND) {
writeErrFrame(ERR_UNSUPPORTED);
return true;
}
uint32_t msg_timestamp = get_le32(&data[3]);
char line[MAX_TEXT_LEN + 1];
size_t text_len = len - 13;
if (text_len > MAX_TEXT_LEN) text_len = MAX_TEXT_LEN;
memcpy(line, &data[13], text_len);
line[text_len] = '\0';
/* The delivery ack the app derives for this (timestamp, attempt,
* text), as composeMsgPacket() computes it; a random one made the
* app resend. */
uint8_t hbuf[5 + MAX_TEXT_LEN];
memcpy(hbuf, &data[3], 4); /* timestamp, on-wire LE bytes */
hbuf[4] = (data[2] & 3); /* attempt & 3 */
memcpy(&hbuf[5], line, text_len);
uint32_t ack = 0;
mesh::Utils::sha256((uint8_t *)&ack, 4, hbuf, 5 + text_len,
self_id.pub_key, PUB_KEY_SIZE);
if (ack == 0) ack = 1;
sendPacketSent(MSG_SEND_SENT_DIRECT, ack, 3000);
/* One slot: flush a still-pending confirm first (it is past the
* window that needed deferring) rather than overwrite it. */
vcontactFlushConfirm();
_vcontact_confirm_ack = ack;
k_work_reschedule(&_vcontact_confirm_work.work,
K_MSEC(VCONTACT_CONFIRM_DELAY_MS));
/* A resend reuses the timestamp: re-ack it (done above) without
* re-running the CLI. */
bool dup = false;
if (msg_timestamp != 0) {
for (uint8_t i = 0; i < VCONTACT_DEDUP_SLOTS; i++) {
if (_vcontact_recent_ts[i] == msg_timestamp) { dup = true; break; }
}
if (!dup) {
_vcontact_recent_ts[_vcontact_recent_head] = msg_timestamp;
_vcontact_recent_head =
(_vcontact_recent_head + 1) % VCONTACT_DEDUP_SLOTS;
}
}
if (!dup) {
/* Undo a phone keyboard's autocapitalisation ("Get cad"), on
* character 0 only: it is always inside the command verb.
* Folding more once lowercased admin passwords. After the ack
* hash, which covers the original text. */
if (line[0] >= 'A' && line[0] <= 'Z') {
line[0] = (char)(line[0] - 'A' + 'a');
}
LOG_INF("vcontact CLI: '%s'", line);
char reply[COMPANION_CLI_PREFIX_ROOM + COMPANION_CLI_REPLY_SIZE];
handleCommand(line, 0, reply);
if (reply[0] != '\0') {
vcontactQueueText(reply);
}
} else {
LOG_DBG("vcontact CLI: dup ts=%u, re-ack only", msg_timestamp);
}
return true;
}
return false;
case CMD_GET_CONTACT_BY_KEY:
if (len >= 1 + PUB_KEY_SIZE && isVContactKey(&data[1], PUB_KEY_SIZE)) {
ContactInfo vc;
buildVContact(vc);
uint8_t rsp[CONTACT_FRAME_SIZE];
size_t n = serializeContact(rsp, vc, PACKET_CONTACT);
writeFrame(rsp, n);
return true;
}
return false;
case CMD_GET_ADVERT_PATH:
/* It has no over-the-air advert, and the app treats ERR_NOT_FOUND here
* as fatal. It is this node, so the path is direct.
* [cmd][reserved][7-byte pubkey prefix] */
if (len >= 2 + 7 && isVContactKey(&data[2], 7)) {
uint8_t rsp[6];
size_t i = 0;
rsp[i++] = PACKET_ADVERT_PATH;
put_le32(&rsp[i], _vcontact_lastmod ? _vcontact_lastmod
: (uint32_t)getRTCClock()->getCurrentTime()); i += 4;
rsp[i++] = 0; // path_len = 0 → direct (zero hop)
writeFrame(rsp, i);
return true;
}
return false;
case CMD_ADD_UPDATE_CONTACT:
/* Never into the contacts table; keep only the flags byte the app owns.
* [cmd][32-byte pubkey][type][flags][...] */
if (len >= 1 + PUB_KEY_SIZE && isVContactKey(&data[1], PUB_KEY_SIZE)) {
if (len >= 1 + PUB_KEY_SIZE + 2) {
uint8_t flags = data[1 + PUB_KEY_SIZE + 1];
if (flags != prefs.v_contact_flags) {
prefs.v_contact_flags = flags;
_store->savePrefs(prefs);
}
}
writeOKFrame();
return true;
}
return false;
case CMD_RESET_PATH:
/* Meaningless for a loopback contact: accept and drop. */
if (len >= 1 + PUB_KEY_SIZE && isVContactKey(&data[1], PUB_KEY_SIZE)) {
writeOKFrame();
return true;
}
return false;
case CMD_REMOVE_CONTACT:
/* Hide for this session only (see _vcontact_app_hidden); flags are kept
* so the favourite returns with it. */
if (len >= 1 + PUB_KEY_SIZE && isVContactKey(&data[1], PUB_KEY_SIZE)) {
_vcontact_app_hidden = true;
LOG_INF("vcontact: hidden by app delete (this session only)");
writeOKFrame();
return true;
}
return false;
case CMD_SEND_TELEMETRY_REQ:
/* [cmd][3 reserved][32-byte pubkey]. Its telemetry is our own: answer
* the SENT the app waits on, then push a TELEMETRY_RESPONSE under the
* v-contact key. */
if (len >= 4 + PUB_KEY_SIZE && isVContactKey(&data[4], PUB_KEY_SIZE)) {
uint32_t tag = 0;
getRNG()->random((uint8_t *)&tag, 4);
if (tag == 0) tag = 1;
sendPacketSent(MSG_SEND_SENT_DIRECT, tag, 3000);
uint8_t rsp[8 + 4 + 11 + 15 + (12 * POWER_MAX_CHANNELS) + 8];
int i = 0;
rsp[i++] = PUSH_CODE_TELEMETRY_RESPONSE;
rsp[i++] = 0; /* reserved */
memcpy(&rsp[i], _vcontact_pubkey, 6);
i += 6;
i += appendSelfTelemetry(&rsp[i],
TELEM_PERM_BASE | TELEM_PERM_LOCATION | TELEM_PERM_ENVIRONMENT);
sendPush(rsp[0], &rsp[1], i - 1);
return true;
}
return false;
default:
/* Every other pubkey-addressed opcode looks the contact up in the table,
* misses, and fails with ERR_NOT_FOUND before any packet exists. */
return false;
}
}
File diff suppressed because it is too large Load Diff
+166 -198
View File
@@ -1,241 +1,209 @@
/*
* SPDX-License-Identifier: MIT
* ZephCore BaseChatMesh - base class for chat-style mesh clients
*/
#pragma once
//#include <Arduino.h> // needed for PlatformIO
#include <mesh/Mesh.h>
#include "AdvertDataHelpers.h"
#include "TxtDataHelpers.h"
#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1)
#include "ContactInfo.h"
#include "ChannelDetails.h"
#define MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE)
#ifdef CONFIG_ZEPHCORE_MAX_CONTACTS
#define MAX_CONTACTS CONFIG_ZEPHCORE_MAX_CONTACTS
#else
#define MAX_CONTACTS 32
#endif
/* Headroom for transient "anon" contacts (type ADV_TYPE_NONE) created to service
* non-contact requests (CMD_SEND_ANON_REQ to a pubkey not in the contact list).
* These are never persisted or synced to the app. Mirrors upstream. */
#define MAX_ANON_CONTACTS 8
#ifdef CONFIG_ZEPHCORE_MAX_CHANNELS
#define MAX_GROUP_CHANNELS CONFIG_ZEPHCORE_MAX_CHANNELS
#else
#define MAX_GROUP_CHANNELS 8
#endif
#define MAX_SEARCH_RESULTS 8
#ifdef CONFIG_ZEPHCORE_MAX_CONNECTIONS
#define MAX_CONNECTIONS CONFIG_ZEPHCORE_MAX_CONNECTIONS
#else
#define MAX_CONNECTIONS 16
#endif
#define MSG_SEND_FAILED 0
#define MSG_SEND_SENT_FLOOD 1
#define MSG_SEND_SENT_DIRECT 2
#define REQ_TYPE_GET_STATUS 0x01
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY 0x03
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 // alias
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define RESP_SERVER_LOGIN_OK 0
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
/* Telemetry permissions */
#define TELEM_PERM_BASE 0x01
#define TELEM_PERM_LOCATION 0x02
#define TELEM_PERM_ENVIRONMENT 0x04
/* Connection info for room server keep-alive */
struct ConnectionInfo {
mesh::Identity server_id;
unsigned long next_ping;
uint32_t last_activity;
uint32_t keep_alive_millis;
uint32_t expected_ack;
class ContactVisitor {
public:
virtual void onContactVisit(const ContactInfo& contact) = 0;
};
class BaseChatMesh;
class ContactVisitor {
class ContactsIterator {
int next_idx;
public:
virtual void onContactVisit(const ContactInfo &contact) = 0;
ContactsIterator(int start) { next_idx = start; }
bool hasNext(const BaseChatMesh* mesh, ContactInfo& dest);
};
class ContactsIterator {
int next_idx = 0;
public:
bool hasNext(const BaseChatMesh *mesh, ContactInfo &dest);
// ZEPHCORE: table sizes come from Kconfig.
#ifdef CONFIG_ZEPHCORE_MAX_CONTACTS
#define MAX_CONTACTS CONFIG_ZEPHCORE_MAX_CONTACTS
#endif
#ifdef CONFIG_ZEPHCORE_MAX_CHANNELS
#define MAX_GROUP_CHANNELS CONFIG_ZEPHCORE_MAX_CHANNELS
#else
#define MAX_GROUP_CHANNELS 8
#endif
#ifdef CONFIG_ZEPHCORE_MAX_CONNECTIONS
#define MAX_CONNECTIONS CONFIG_ZEPHCORE_MAX_CONNECTIONS
#endif
#ifndef MAX_CONTACTS
#define MAX_CONTACTS 32
#endif
#define MAX_ANON_CONTACTS 8
#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 16
#endif
struct ConnectionInfo {
mesh::Identity server_id;
unsigned long next_ping;
uint32_t last_activity;
uint32_t keep_alive_millis;
uint32_t expected_ack;
};
#include "ChannelDetails.h"
/**
* Abstract Mesh class for common 'chat' client functionality.
* Handles contact management, message encryption/decryption, ACK handling.
* \brief abstract Mesh class for common 'chat' client
*/
class BaseChatMesh : public mesh::Mesh {
friend class ContactsIterator;
ContactInfo contacts[MAX_CONTACTS + MAX_ANON_CONTACTS];
int num_contacts;
int sort_array[MAX_CONTACTS + MAX_ANON_CONTACTS];
int matching_peer_indexes[MAX_SEARCH_RESULTS];
unsigned long txt_send_timeout;
friend class ContactsIterator;
ChannelDetails channels[MAX_GROUP_CHANNELS];
int num_channels;
ContactInfo contacts[MAX_CONTACTS+MAX_ANON_CONTACTS];
int num_contacts;
int sort_array[MAX_CONTACTS+MAX_ANON_CONTACTS];
int matching_peer_indexes[MAX_SEARCH_RESULTS];
unsigned long txt_send_timeout;
#ifdef MAX_GROUP_CHANNELS
ChannelDetails channels[MAX_GROUP_CHANNELS];
int num_channels; // only for addChannel()
#endif
mesh::Packet* _pendingLoopback;
uint8_t temp_buf[MAX_TRANS_UNIT];
ConnectionInfo connections[MAX_CONNECTIONS];
ConnectionInfo connections[MAX_CONNECTIONS];
mesh::Packet *_pendingLoopback;
uint8_t temp_buf[MAX_TRANS_UNIT];
mesh::Packet *composeMsgPacket(const ContactInfo &recipient, uint32_t timestamp, uint8_t attempt,
const char *text, uint32_t &expected_ack);
void sendAckTo(const ContactInfo &dest, const uint8_t *ack_hash, uint8_t ack_len = 4);
/* Shared flood-vs-direct dispatch tail used by sendMessage/sendCommandData/
* sendLogin/sendAnonReq/sendRequest. Sets est_timeout and (when
* set_txt_timeout) txt_send_timeout; returns MSG_SEND_SENT_FLOOD/DIRECT. */
int dispatchToRecipient(mesh::Packet *pkt, const ContactInfo &recipient,
uint32_t &est_timeout, bool set_txt_timeout);
mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack);
void sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len=4);
protected:
BaseChatMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, mesh::RTCClock &rtc,
mesh::PacketManager &mgr, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
{
num_contacts = 0;
memset(channels, 0, sizeof(channels));
num_channels = 0;
txt_send_timeout = 0;
_pendingLoopback = nullptr;
}
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
{
resetContacts();
void bootstrapRTCfromContacts();
void resetContacts() { num_contacts = 0; }
void populateContactFromAdvert(ContactInfo &ci, const mesh::Identity &id, const AdvertDataParser &parser, uint32_t timestamp);
ContactInfo *allocateContactSlot(bool transient_only = false);
#ifdef MAX_GROUP_CHANNELS
memset(channels, 0, sizeof(channels));
num_channels = 0;
#endif
txt_send_timeout = 0;
_pendingLoopback = NULL;
// ZEPHCORE: no memset(connections): Identity has a ctor (-Wclass-memaccess),
// and the mesh is a static object, so the table starts zeroed.
}
// UI concepts for subclasses to implement
virtual bool isAutoAddEnabled() const { return true; }
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
virtual void onContactsFull() {}
virtual bool shouldOverwriteWhenFull() const { return false; }
virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops
virtual void onContactOverwrite(const uint8_t *pub_key) {}
virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t *path) = 0;
virtual ContactInfo *processAck(const uint8_t *data) = 0;
virtual void onContactPathUpdated(const ContactInfo &contact) = 0;
virtual bool onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8_t in_path_len,
uint8_t *out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len);
virtual void onMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCommandDataRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onSignedMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) = 0;
virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0;
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
virtual void onSendTimeout() = 0;
virtual void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, const char *text) = 0;
virtual void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
const uint8_t *data, size_t data_len) {}
virtual uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp,
const uint8_t *data, uint8_t len, uint8_t *reply) = 0;
virtual void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) = 0;
virtual void handleReturnPathRetry(const ContactInfo &contact, const uint8_t *path, uint8_t path_len);
void bootstrapRTCfromContacts();
virtual void sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis = 0);
virtual void sendFloodScoped(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t delay_millis = 0);
void resetContacts() {
num_contacts = 0; // ZEPHCORE: anon slots are interspersed, none reserved at the front
}
void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp);
ContactInfo* allocateContactSlot(bool transient_only=false); // helper to find slot for new contact
virtual void onLoginSent(const ContactInfo &contact) {}
virtual void onChannelAdded(ChannelDetails *ch) {}
// 'UI' concepts, for sub-classes to implement
virtual bool isAutoAddEnabled() const { return true; }
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
virtual void onContactsFull() {};
virtual bool shouldOverwriteWhenFull() const { return false; }
virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops
virtual void onContactOverwrite(const uint8_t* pub_key) {};
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
virtual ContactInfo* processAck(const uint8_t *data) = 0;
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0;
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;
virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0;
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
virtual void onSendTimeout() = 0;
virtual void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) = 0;
virtual void onChannelDataRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint16_t data_type,
const uint8_t* data, size_t data_len) {}
virtual uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) = 0;
virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0;
virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len);
/* Every signature-verified advert, before contact filtering/dedup —
* mesh time-sync harvesting hook. */
virtual void onAdvertTimeSample(const mesh::Identity &id, uint32_t timestamp,
uint8_t hops) { (void)id; (void)timestamp; (void)hops; }
virtual void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0);
virtual void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0);
// Storage concepts for subclasses to override
virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; }
virtual bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) { return false; }
// ZEPHCORE: every signature-verified advert, before contact filtering and
// dedup: the mesh time-sync harvesting hook.
virtual void onAdvertTimeSample(const mesh::Identity& id, uint32_t timestamp, uint8_t hops) { }
// Mesh overrides
void onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp,
const uint8_t *app_data, size_t app_data_len) override;
int searchPeersByHash(const uint8_t *hash) override;
void getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret,
uint8_t *data, size_t len) override;
bool onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path,
uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) override;
void onAckRecv(mesh::Packet *packet, uint32_t ack_crc) override;
int searchChannelsByHash(const uint8_t *hash, mesh::GroupChannel channels[], int max_matches) override;
void onGroupDataRecv(mesh::Packet *packet, uint8_t type, const mesh::GroupChannel &channel,
uint8_t *data, size_t len) override;
// storage concepts, for sub-classes to override/implement
virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } // not implemented
virtual bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) { return false; }
// Mesh overrides
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
#ifdef MAX_GROUP_CHANNELS
int searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel channels[], int max_matches) override;
#endif
void onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mesh::GroupChannel& channel, uint8_t* data, size_t len) override;
// Connections
bool startConnection(const ContactInfo& contact, uint16_t keep_alive_secs);
void stopConnection(const uint8_t* pub_key);
bool hasConnectionTo(const uint8_t* pub_key);
void markConnectionActive(const ContactInfo& contact);
ContactInfo* checkConnectionsAck(const uint8_t* data);
void checkConnections();
public:
mesh::Packet *createSelfAdvert(const char *name);
mesh::Packet *createSelfAdvert(const char *name, double lat, double lon);
mesh::Packet* createSelfAdvert(const char* name);
mesh::Packet* createSelfAdvert(const char* name, double lat, double lon);
// ZEPHCORE: a self advert with the node's location policy, path-hash mode and
// scope, so UI code holding only a BaseChatMesh* can send one. CompanionMesh
// implements it.
virtual bool sendSelfAdvert(bool flood) { return false; }
int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout);
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout);
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len,
uint32_t* out_hash = NULL); // ZEPHCORE: out_hash, for send feedback
bool sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uint8_t path_len, uint16_t data_type, const uint8_t* data, int data_len);
int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
int sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout);
bool shareContactZeroHop(const ContactInfo& contact);
uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]);
bool importContact(const uint8_t src_buf[], uint8_t len);
void resetPathTo(ContactInfo& recipient);
void scanRecentContacts(int last_n, ContactVisitor* visitor);
ContactInfo* searchContactsByPrefix(const char* name_prefix);
ContactInfo* lookupContactByPubKey(const uint8_t* pub_key, int prefix_len);
bool removeContact(ContactInfo& contact);
bool addContact(const ContactInfo& contact);
int getTotalContactSlots() const { return num_contacts; }
int getNumContacts() const { return num_contacts; } // ZEPHCORE: no reserved slots; anon slots are counted
bool getContactByIdx(uint32_t idx, ContactInfo& contact);
ContactsIterator startContactsIterator();
ChannelDetails* addChannel(const char* name, const uint8_t* psk, size_t psk_len); // ZEPHCORE: raw PSK
bool getChannel(int idx, ChannelDetails& dest);
bool setChannel(int idx, const ChannelDetails& src);
int findChannelIdx(const mesh::GroupChannel& ch);
int getNumChannels() const { return num_channels; } // ZEPHCORE
/* Build and send a self advert honoring the node's location policy,
* path-hash mode, and transport scope. Overridden by CompanionMesh
* (the only mesh with interactive advert UI triggers); the base default
* does nothing and returns false. Lets UI screens that only hold a
* BaseChatMesh* trigger adverts via the canonical scoped path instead
* of a bare sendFlood() that drops path_hash_mode and scope. */
virtual bool sendSelfAdvert(bool flood) { (void)flood; return false; }
int sendMessage(const ContactInfo &recipient, uint32_t timestamp, uint8_t attempt, const char *text,
uint32_t &expected_ack, uint32_t &est_timeout);
/* @param txt_type TXT_TYPE_CLI_DATA (understood by every MeshCore version)
* or TXT_TYPE_CLI_COMMAND (v1.18+ only). Callers that must reach older
* repeaters have to keep using TXT_TYPE_CLI_DATA. */
int sendCommandData(const ContactInfo &recipient, uint32_t timestamp, uint8_t attempt,
uint8_t txt_type, const char *text, uint32_t &est_timeout);
/* @param out_hash if non-null, filled with the FNV-1a packet hash so the
* caller can later query the contention tracker for "how many neighbors
* heard and retransmitted this?" — used by the joystick UI's send-feedback
* mechanism. When provided, the packet is also pre-registered with the
* contention tracker (so heard dupes match). */
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel &channel, const char *sender_name,
const char *text, int text_len, uint32_t *out_hash = nullptr);
bool sendGroupData(mesh::GroupChannel &channel, uint8_t *path, uint8_t path_len,
uint16_t data_type, const uint8_t *data, int data_len);
int sendLogin(const ContactInfo &recipient, const char *password, uint32_t &est_timeout);
int sendAnonReq(const ContactInfo &recipient, const uint8_t *data, uint8_t len, uint32_t &tag, uint32_t &est_timeout);
int sendRequest(const ContactInfo &recipient, uint8_t req_type, uint32_t &tag, uint32_t &est_timeout);
int sendRequest(const ContactInfo &recipient, const uint8_t *req_data, uint8_t data_len, uint32_t &tag, uint32_t &est_timeout);
bool shareContactZeroHop(const ContactInfo &contact);
uint8_t exportContact(const ContactInfo &contact, uint8_t dest_buf[]);
bool importContact(const uint8_t src_buf[], uint8_t len);
void resetPathTo(ContactInfo &recipient);
void scanRecentContacts(int last_n, ContactVisitor *visitor);
ContactInfo *searchContactsByPrefix(const char *name_prefix);
ContactInfo *lookupContactByPubKey(const uint8_t *pub_key, int prefix_len);
bool removeContact(ContactInfo &contact);
bool addContact(const ContactInfo &contact);
int getNumContacts() const { return num_contacts; }
bool getContactByIdx(uint32_t idx, ContactInfo &contact);
ContactsIterator startContactsIterator();
ChannelDetails *addChannel(const char *name, const uint8_t *psk, size_t psk_len);
bool getChannel(int idx, ChannelDetails &dest);
bool setChannel(int idx, const ChannelDetails &src);
int findChannelIdx(const mesh::GroupChannel &ch);
int getNumChannels() const { return num_channels; }
/* Connection management for room servers */
bool startConnection(const ContactInfo &contact, uint16_t keep_alive_secs);
void stopConnection(const uint8_t *pub_key);
bool hasConnectionTo(const uint8_t *pub_key);
void markConnectionActive(const ContactInfo &contact);
ContactInfo *checkConnectionsAck(const uint8_t *data);
void checkConnections();
void loop();
void loop();
};
+3 -8
View File
@@ -1,14 +1,9 @@
/*
* SPDX-License-Identifier: MIT
* ChannelDetails - group channel info
*/
#pragma once
//#include <Arduino.h>
#include <mesh/Mesh.h>
#include <mesh/MeshCore.h>
struct ChannelDetails {
mesh::GroupChannel channel;
char name[32];
mesh::GroupChannel channel;
char name[32];
};
+28 -29
View File
@@ -1,41 +1,40 @@
/*
* SPDX-License-Identifier: MIT
* ContactInfo - contact/peer info for mesh
*/
#pragma once
//#include <Arduino.h>
#include <mesh/Mesh.h>
#include <mesh/MeshCore.h>
#define OUT_PATH_UNKNOWN 0xFF
/* Marker used in BLE offline-queue frames to indicate the message was sent
* by THIS device (not received). Also reused by the joystick UI's local
* message-preview structs for the same purpose. Not a routing value —
* never appears in over-the-air packet path_len fields. */
// ZEPHCORE: offline-queue frames mark a message this device sent (the joystick
// UI's message previews reuse it). Never an over-the-air path_len.
#define OUT_PATH_SENT 0xFE
struct ContactInfo {
mesh::Identity id;
char name[32];
uint8_t type;
uint8_t flags;
uint8_t out_path_len;
mutable bool shared_secret_valid;
uint8_t out_path[MAX_PATH_SIZE];
uint32_t last_advert_timestamp;
uint32_t lastmod;
int32_t gps_lat, gps_lon;
uint32_t sync_since;
mesh::Identity id;
char name[32];
uint8_t type; // on of ADV_TYPE_*
uint8_t flags;
uint8_t out_path_len;
mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated
uint8_t out_path[MAX_PATH_SIZE];
uint32_t last_advert_timestamp; // by THEIR clock
uint32_t lastmod; // by OUR clock
int32_t gps_lat, gps_lon; // 6 dec places
uint32_t sync_since;
const uint8_t *getSharedSecret(const mesh::LocalIdentity &self_id) const {
if (!shared_secret_valid) {
self_id.calcSharedSecret(shared_secret, id.pub_key);
shared_secret_valid = true;
}
return shared_secret;
}
const uint8_t* getSharedSecret(const mesh::LocalIdentity& self_id) const {
if (!shared_secret_valid) {
self_id.calcSharedSecret(shared_secret, id.pub_key);
shared_secret_valid = true;
}
return shared_secret;
}
bool isFav() const { return flags & 0x01; }
bool isTelemBaseAllowed() const { return flags & 0x02; }
bool isTelemLocAllowed() const { return flags & 0x04; }
bool isTelemEnvAllowed() const { return flags & 0x08; }
bool isRemoteCLIAllowed() const { return flags & 0x10; }
private:
mutable uint8_t shared_secret[PUB_KEY_SIZE];
mutable uint8_t shared_secret[PUB_KEY_SIZE];
};
File diff suppressed because it is too large Load Diff