mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 13:24:09 +00:00
595 lines
24 KiB
C++
595 lines
24 KiB
C++
/*
|
|
* SPDX-License-Identifier: MIT
|
|
* CompanionMesh - ZephCore Companion device application layer
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <helpers/BaseChatMesh.h>
|
|
#include <helpers/MeshTimeSync.h>
|
|
#include <helpers/TransportKeyStore.h>
|
|
#include <ZephyrDataStore.h>
|
|
#include <NodePrefs.h>
|
|
#include <zephyr/kernel.h>
|
|
|
|
/* BLE push notification codes */
|
|
#define PUSH_CODE_ADVERT 0x80
|
|
#define PUSH_CODE_PATH_UPDATED 0x81
|
|
#define PUSH_CODE_SEND_CONFIRMED 0x82
|
|
#define PUSH_CODE_MSG_WAITING 0x83
|
|
#define PUSH_CODE_RAW_DATA 0x84
|
|
#define PUSH_CODE_LOGIN_SUCCESS 0x85
|
|
#define PUSH_CODE_LOGIN_FAIL 0x86
|
|
#define PUSH_CODE_STATUS_RESPONSE 0x87
|
|
#define PUSH_CODE_LOG_RX_DATA 0x88
|
|
#define PUSH_CODE_TRACE_DATA 0x89
|
|
#define PUSH_CODE_NEW_ADVERT 0x8A
|
|
#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B
|
|
#define PUSH_CODE_BINARY_RESPONSE 0x8C
|
|
#define PUSH_CODE_PATH_DISCOVERY_RESP 0x8D
|
|
#define PUSH_CODE_CONTROL_DATA 0x8E
|
|
#define PUSH_CODE_CONTACT_DELETED 0x8F
|
|
#define PUSH_CODE_CONTACTS_FULL 0x90
|
|
|
|
/* Auto-add config bitmask */
|
|
#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0)
|
|
#define AUTO_ADD_CHAT (1 << 1)
|
|
#define AUTO_ADD_REPEATER (1 << 2)
|
|
#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 */
|
|
#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 */
|
|
#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 */
|
|
struct AdvertPath {
|
|
uint8_t pubkey_prefix[7];
|
|
uint8_t path_len;
|
|
char name[32];
|
|
uint32_t recv_timestamp;
|
|
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 */
|
|
typedef void (*RadioReconfigureCallback)(void);
|
|
|
|
/* 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. */
|
|
#define COMPANION_CLI_REPLY_SIZE 256
|
|
typedef void (*CompanionCLICallback)(const char *line, 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,
|
|
mesh::RTCClock &rtc, mesh::PacketManager &mgr, mesh::MeshTables &tables,
|
|
ZephyrDataStore &store);
|
|
|
|
void begin();
|
|
void loop();
|
|
|
|
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).
|
|
*/
|
|
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);
|
|
|
|
/**
|
|
* 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; }
|
|
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. */
|
|
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. */
|
|
void vcontactPushAdvert();
|
|
/** Push CONTACT_DELETED for the v-contact (call after 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. */
|
|
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. */
|
|
bool vcontactConfirmPending() const { return _vcontact_confirm_ack != 0; }
|
|
|
|
/**
|
|
* Continue contact iteration (call each main loop iteration).
|
|
* Returns true if contacts are still being sent.
|
|
*/
|
|
bool continueContactIteration();
|
|
|
|
/**
|
|
* Cancel contact iteration silently (no frame sent).
|
|
* Call on BLE disconnect — there's nobody to send CONTACT_END to.
|
|
*/
|
|
void cancelContactIterator() { _contact_iter_active = false; }
|
|
|
|
/** True while a contact dump is in progress (drives the stall watchdog). */
|
|
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; }
|
|
|
|
/**
|
|
* Cancel pending message sync. Un-ACKed message stays in queue.
|
|
* Call on BLE disconnect so the message is re-sent on reconnect.
|
|
*/
|
|
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.
|
|
*/
|
|
void cleanupSignState() {
|
|
if (_sign_data) {
|
|
delete[] _sign_data;
|
|
_sign_data = nullptr;
|
|
}
|
|
_sign_data_len = 0;
|
|
_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.
|
|
*/
|
|
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).
|
|
*/
|
|
void queueLocalSentChannelMessage(uint8_t channel_idx, uint32_t timestamp,
|
|
const char *text, bool heard_repeat);
|
|
|
|
/* DataStoreHost interface */
|
|
bool onContactLoaded(const ContactInfo &c) override;
|
|
bool getContactForSave(uint32_t idx, ContactInfo &c) override;
|
|
bool onChannelLoaded(uint8_t idx, const ChannelDetails &ch) override;
|
|
bool getChannelForSave(uint8_t idx, ChannelDetails &ch) override;
|
|
|
|
/* 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). */
|
|
void timeSyncTick();
|
|
|
|
/* Prefs (includes node_lat/lon) */
|
|
NodePrefs prefs;
|
|
|
|
protected:
|
|
/* BaseChatMesh virtual implementations */
|
|
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 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;
|
|
void onSendTimeout() override;
|
|
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, const char *text) override;
|
|
void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
|
|
const uint8_t *data, size_t data_len) override;
|
|
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 */
|
|
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) 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) */
|
|
bool allowPacketForward(const mesh::Packet *packet) override;
|
|
|
|
/* Path discovery - intercept path data before 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 */
|
|
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. */
|
|
bool passivelyTrackFloods() const override { return false; }
|
|
uint32_t getInitialFloodJitter(const mesh::Packet *packet) override;
|
|
|
|
uint8_t getDutyCyclePercent() const override;
|
|
uint8_t getExtraAckTransmitCount() const override;
|
|
|
|
/* Adaptive CAD: persist the staircase's learned offset */
|
|
void onCadOffsetChanged(int8_t offset) override {
|
|
prefs.cad_offset = offset;
|
|
if (_store) {
|
|
_store->savePrefs(prefs);
|
|
}
|
|
}
|
|
|
|
/* Auto-add filtering overrides */
|
|
bool isAutoAddEnabled() const override;
|
|
bool shouldAutoAddContactType(uint8_t type) const override;
|
|
bool shouldOverwriteWhenFull() const override;
|
|
uint8_t getAutoAddMaxHops() const override;
|
|
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;
|
|
|
|
private:
|
|
ZephyrDataStore *_store;
|
|
PushCallback _push_cb;
|
|
WriteFrameCallback _write_cb;
|
|
GetBatteryCallback _batt_cb;
|
|
RadioReconfigureCallback _radio_reconfig_cb;
|
|
PinChangeCallback _pin_change_cb;
|
|
|
|
/* Contact iteration state */
|
|
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 */
|
|
|
|
/* Offline message queue */
|
|
struct QueuedFrame {
|
|
uint8_t len;
|
|
uint8_t buf[172];
|
|
};
|
|
QueuedFrame _offline_queue[OFFLINE_QUEUE_SIZE];
|
|
int _offline_queue_head;
|
|
int _offline_queue_tail;
|
|
int _offline_queue_count;
|
|
bool _sync_pending; /* true = last peeked message not yet ACKed by phone */
|
|
|
|
/* ACK tracking table */
|
|
struct AckEntry {
|
|
uint32_t expected_ack;
|
|
uint32_t sent_time;
|
|
int contact_idx;
|
|
bool active;
|
|
};
|
|
AckEntry _ack_table[ACK_TABLE_SIZE];
|
|
|
|
/* Advert path table for tracking recently heard nodes */
|
|
AdvertPath _advert_paths[ADVERT_PATH_TABLE_SIZE];
|
|
int _next_advert_path_idx;
|
|
|
|
/* Signing state */
|
|
uint8_t *_sign_data;
|
|
uint32_t _sign_data_len;
|
|
uint32_t _sign_data_capacity;
|
|
|
|
/* Pending request tracking (for response matching) */
|
|
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 */
|
|
#endif
|
|
|
|
/* Lazy contacts/channels write - batches rapid updates */
|
|
int64_t _dirty_contacts_expiry;
|
|
int64_t _dirty_channels_expiry;
|
|
static constexpr int64_t LAZY_WRITE_DELAY_MS = 5000; /* 5 seconds, matches Arduino */
|
|
|
|
/* 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. */
|
|
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) */
|
|
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;
|
|
|
|
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
|
|
public:
|
|
void setJoystickPingTag(uint32_t tag) { _pending_joystick_ping_tag = tag; }
|
|
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). */
|
|
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]. */
|
|
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). */
|
|
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. */
|
|
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. */
|
|
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);
|
|
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);
|
|
|
|
/* 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;
|
|
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. */
|
|
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][64]; /* notices buffered 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). */
|
|
bool _vcontact_hold_msgwait;
|
|
/* 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). */
|
|
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. */
|
|
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. */
|
|
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. */
|
|
void vcontactFlushConfirm();
|
|
bool vcontactClockValid();
|
|
bool vcontactReady() {
|
|
return isVContactEnabled() && !_vcontact_app_hidden && _vcontact_lastmod != 0;
|
|
}
|
|
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). */
|
|
void deriveVContactKey();
|
|
/** Intercept protocol frames addressed to the v-contact. Returns true when
|
|
* the frame was fully handled (response already written). */
|
|
bool vcontactHandleFrame(const uint8_t *data, size_t len);
|
|
/** Chunk `text` into offline-queue contact messages from the v-contact. */
|
|
void vcontactQueueText(const char *text);
|
|
|
|
void addPendingAck(uint32_t expected, int contact_idx);
|
|
int findAndRemoveAck(uint32_t ack, uint32_t *out_sent_time = nullptr);
|
|
|
|
uint8_t _pending_channel_idx[MAX_GROUP_CHANNELS];
|
|
uint8_t _pending_channel_head;
|
|
uint8_t _pending_channel_tail;
|
|
uint8_t _pending_channel_count;
|
|
};
|