mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 03:49:55 +00:00
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:
+1
-1
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1835
-2250
File diff suppressed because it is too large
Load Diff
+191
-289
@@ -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);
|
||||
|
||||
@@ -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); }
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+926
-927
File diff suppressed because it is too large
Load Diff
+166
-198
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
+172
-455
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user