Temporarily disable incompatible cached side detectors while applying a temporary primary tuple, preserving their saved configuration for normal restoration. Use primary-only CAD while those detectors are suspended. Keep compatible temporary detectors and real hardware errors intact. Report pending TempRadio starts instead of off, preserve live-tuple status during replacements and future schedules, and include the two-second delay in the accepted reply. Share primary validation to retain STM32 Mini flash capacity without dropping features. Validated production CLI status, scheduler and persistence cases; LR2021 detector suspension, restoration, CAD and hardware-error paths; reply handoff, room/sensor recovery, Morse and OTA automation regressions. W12 repeater, room-server and sensor builds pass. Canonical STM32 Mini Repeater passes flash and runtime RAM admission with 436 bytes of flash remaining. Physical W12 RF testing remains unverified.
352 lines
15 KiB
C++
352 lines
15 KiB
C++
#pragma once
|
|
|
|
#include <Arduino.h> // needed for PlatformIO
|
|
#include <Mesh.h>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
|
|
#include "TimeSeriesData.h"
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
#include <InternalFileSystem.h>
|
|
#elif defined(RP2040_PLATFORM)
|
|
#include <LittleFS.h>
|
|
#elif defined(ESP32)
|
|
#include <SPIFFS.h>
|
|
#endif
|
|
|
|
#include <helpers/ArduinoHelpers.h>
|
|
#include <helpers/StaticPoolPacketManager.h>
|
|
#include <helpers/SimpleMeshTables.h>
|
|
#include <helpers/IdentityStore.h>
|
|
#include <helpers/AdvertDataHelpers.h>
|
|
#include <helpers/TxtDataHelpers.h>
|
|
#include <helpers/CommonCLI.h>
|
|
#include <helpers/MeshClockSync.h>
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
#include <helpers/UserGpioReplyTracker.h>
|
|
#endif
|
|
#include <helpers/StatsFormatHelper.h>
|
|
#include <helpers/ClientACL.h>
|
|
#include <helpers/RegionMap.h>
|
|
#include <RTClib.h>
|
|
#include <target.h>
|
|
|
|
#ifndef MESH_ENABLE_SENSOR_TRACKER
|
|
#if !defined(STM32_PLATFORM) && ENV_INCLUDE_GPS == 1
|
|
#define MESH_ENABLE_SENSOR_TRACKER 1
|
|
#else
|
|
#define MESH_ENABLE_SENSOR_TRACKER 0
|
|
#endif
|
|
#endif
|
|
#if MESH_ENABLE_SENSOR_TRACKER
|
|
#include <helpers/SensorTrackerStore.h>
|
|
#include <helpers/TrackerProtocol.h>
|
|
#endif
|
|
|
|
#define PERM_RESERVED1 (1 << 2)
|
|
#define PERM_RESERVED2 (1 << 3)
|
|
#define PERM_RESERVED3 (1 << 4)
|
|
#define PERM_RESERVED4 (1 << 5)
|
|
#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts
|
|
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
|
|
|
|
#ifndef FIRMWARE_BUILD_DATE
|
|
#define FIRMWARE_BUILD_DATE "15 Oct 2026"
|
|
#endif
|
|
|
|
#ifndef FIRMWARE_VERSION
|
|
#define FIRMWARE_VERSION "v1.18.0"
|
|
#endif
|
|
|
|
#define FIRMWARE_ROLE "sensor"
|
|
|
|
#define MAX_SEARCH_RESULTS 24
|
|
#define MAX_CONCURRENT_ALERTS 4
|
|
|
|
class SensorMesh : public mesh::Mesh, public CommonCLICallbacks,
|
|
public mesh::MeshClockSyncCallbacks {
|
|
mesh::StaticFloodAdvertLimiter<> flood_advert_limiter;
|
|
mesh::FloodAdvertLimiter* getFloodAdvertLimiter() override { return &flood_advert_limiter; }
|
|
public:
|
|
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
|
void begin(FILESYSTEM* fs);
|
|
void loop();
|
|
bool canRecoverUsbLogging() const {
|
|
return dirty_contacts_expiry == 0 && !hasOutbound() && !isAnyTempRadioActive()
|
|
&& !hasPendingOtaApply() && !saved_radio_apply_pending && !_cli.hasPendingRadioSettings()
|
|
&& set_radio_at == 0;
|
|
}
|
|
void handleCommand(uint32_t sender_timestamp, char* command, char* reply,
|
|
int gpio_client_index = -1,
|
|
uint8_t gpio_path_hash_size = 1);
|
|
|
|
// CommonCLI callbacks
|
|
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
|
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
|
const char* getRole() override { return FIRMWARE_ROLE; }
|
|
const char* getNodeName() { return _prefs.node_name; }
|
|
NodePrefs* getNodePrefs() { return &_prefs; }
|
|
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
|
|
uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms) const;
|
|
bool isTrackerModeEnabled() const {
|
|
#if MESH_ENABLE_SENSOR_TRACKER
|
|
return tracker.enabled;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
void setTrackerStorageAvailable(bool available) {
|
|
#if MESH_ENABLE_SENSOR_TRACKER
|
|
tracker_storage_available = available;
|
|
#else
|
|
(void)available;
|
|
#endif
|
|
}
|
|
void savePrefs(
|
|
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common) override {
|
|
_cli.savePrefs(_fs, scope);
|
|
}
|
|
void onManualClockSet() override { _clock_sync.onManualClockSet(); }
|
|
bool formatFileSystem() override;
|
|
void sendSelfAdvertisement(int delay_millis, bool flood) override;
|
|
void updateAdvertTimer() override;
|
|
void updateFloodAdvertTimer() override;
|
|
void setLoggingOn(bool enable) override { }
|
|
void eraseLogFile() override { }
|
|
void dumpLogFile() override { }
|
|
bool setTxPower(int8_t power_dbm) override;
|
|
mesh::RadioParamApplyResult trySetTxPower(int8_t power_dbm) override;
|
|
#if defined(MESH_HIL_TX_FREQ_MHZ)
|
|
bool isTxPowerApplied(int8_t power_dbm) const override;
|
|
#endif
|
|
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
|
|
bool supportsRxPowerSaving() const override;
|
|
mesh::RadioParamApplyResult trySetRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
|
|
bool supportsRxPowerSavingRfRxDisable() const override;
|
|
mesh::RadioParamApplyResult trySetRxPowerSavingRfRxDisabled(bool disabled) override;
|
|
bool isRxPowerSavingRfRxDisabled() const override;
|
|
bool supportsRxBoostedGain() const override;
|
|
mesh::RadioParamApplyResult trySetRxBoostedGain(bool enable) override;
|
|
#if defined(USE_LR2021)
|
|
mesh::RadioParamApplyResult tryConfigSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) override;
|
|
#endif
|
|
void recalibrateNoiseFloor() override { _radio->recalibrateNoiseFloor(); }
|
|
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
|
|
void formatNeighborsReply(char *reply) override {
|
|
strcpy(reply, "not supported");
|
|
}
|
|
void formatStatsReply(char *reply) override;
|
|
void formatRadioStatsReply(char *reply) override;
|
|
void formatPacketStatsReply(char *reply) override;
|
|
mesh::LocalIdentity& getSelfId() override { return self_id; }
|
|
bool saveIdentity(const mesh::LocalIdentity& new_id) override;
|
|
void clearStats() override { }
|
|
mesh::Radio* getProfileRadio() override { return _radio; }
|
|
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) override;
|
|
bool isTempRadioPending() const override { return set_radio_at != 0 && !temp_radio_applied; }
|
|
bool scheduleNormalRadio() override;
|
|
void startRegionsLoad() override;
|
|
bool saveRegions() override;
|
|
void onDefaultRegionChanged(const RegionEntry* r) override;
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
uint32_t getUserGpioRequestSource() const override {
|
|
return _gpio_reply_tracker.requestSource();
|
|
}
|
|
void onUserGpioTimerScheduled(uint8_t pin, uint32_t request_id) override {
|
|
_gpio_reply_tracker.timerScheduled(pin, request_id);
|
|
}
|
|
void onUserGpioTimerCancelled(uint8_t pin) override {
|
|
_gpio_reply_tracker.timerCancelled(pin);
|
|
}
|
|
void onUserGpioTimerCompleted(uint8_t pin, uint8_t state,
|
|
uint32_t request_id) override;
|
|
#endif
|
|
|
|
float getTelemValue(uint8_t channel, uint8_t type);
|
|
|
|
bool sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
|
|
bool sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
|
|
|
|
protected:
|
|
bool isTempRadioActive() const override {
|
|
return temp_radio_applied && revert_radio_at != 0 && !millisHasNowPassed(revert_radio_at);
|
|
}
|
|
// current telemetry data queries
|
|
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
|
|
float getCurrent(uint8_t channel) { return getTelemValue(channel, LPP_CURRENT); }
|
|
float getPower(uint8_t channel) { return getTelemValue(channel, LPP_POWER); }
|
|
float getTemperature(uint8_t channel) { return getTelemValue(channel, LPP_TEMPERATURE); }
|
|
float getRelativeHumidity(uint8_t channel) { return getTelemValue(channel, LPP_RELATIVE_HUMIDITY); }
|
|
float getBarometricPressure(uint8_t channel) { return getTelemValue(channel, LPP_BAROMETRIC_PRESSURE); }
|
|
float getAltitude(uint8_t channel) { return getTelemValue(channel, LPP_ALTITUDE); }
|
|
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
|
|
|
|
// alerts
|
|
enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT };
|
|
|
|
struct Trigger {
|
|
uint32_t timestamp;
|
|
uint32_t expected_acks[4];
|
|
AlertPriority pri;
|
|
int8_t curr_contact_idx;
|
|
uint8_t attempt;
|
|
unsigned long send_expiry;
|
|
char text[MAX_PACKET_PAYLOAD];
|
|
|
|
Trigger() { text[0] = 0; }
|
|
bool isTriggered() const { return text[0] != 0; }
|
|
};
|
|
void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text);
|
|
|
|
virtual void onSensorDataRead() = 0; // for app to implement
|
|
virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement
|
|
virtual bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) { return false; }
|
|
|
|
// Mesh overrides
|
|
float getAirtimeBudgetFactor() const override;
|
|
bool allowPacketForward(const mesh::Packet* packet) override;
|
|
bool allowPacketTransmit(const mesh::Packet* packet) const override;
|
|
int calcRxDelay(float score, uint32_t air_time) const override;
|
|
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
|
|
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
|
|
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
|
|
uint8_t next_hop_hash_len) const override;
|
|
void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
|
|
uint8_t retry_attempt) override;
|
|
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
|
|
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
|
|
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
|
|
bool allowFloodRetry(const mesh::Packet* packet) const override;
|
|
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
|
|
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
|
|
|
|
bool supportsBasicRetryConfig() const override { return true; }
|
|
void onRetryConfigChanged() override;
|
|
int getInterferenceThreshold() const override;
|
|
bool getCADEnabled() const override;
|
|
int getAGCResetInterval() const override;
|
|
bool useAdaptiveAGC() const override { return _prefs.powersaving_enabled != 0; }
|
|
uint8_t getDefaultTxCodingRate() const override {
|
|
return active_cr;
|
|
}
|
|
mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
|
|
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t 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 onControlDataRecv(mesh::Packet* packet) override;
|
|
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
|
|
void onSendComplete(mesh::Packet* packet) override;
|
|
void onSendFail(mesh::Packet* packet) override;
|
|
void onRadioProfileCopyQueued(mesh::Packet* packet, const mesh::Packet* original,
|
|
uint8_t priority) override;
|
|
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
|
|
uint32_t timestamp, const uint8_t* app_data,
|
|
size_t app_data_len) override;
|
|
void onGroupPacketRecv(mesh::Packet* packet) override;
|
|
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len);
|
|
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1);
|
|
private:
|
|
#if MESH_ENABLE_SENSOR_TRACKER
|
|
mesh::tracker::TrackerRecord tracker;
|
|
uint8_t tracker_secret[PUB_KEY_SIZE] = {};
|
|
bool tracker_storage_available = true;
|
|
bool tracker_storage_fault = false;
|
|
bool tracker_radio_asleep = false;
|
|
bool tracker_acquiring = false;
|
|
bool tracker_pending = false;
|
|
bool tracker_sent = false;
|
|
uint8_t tracker_packet_hash[MAX_HASH_SIZE] = {};
|
|
uint32_t tracker_next_check = 0;
|
|
uint32_t tracker_pending_tag = 0;
|
|
unsigned long tracker_reply_until = 0;
|
|
unsigned long tracker_awake_until = 0;
|
|
uint32_t tracker_boot_anchor = 0;
|
|
uint32_t tracker_clock_rtc = 0;
|
|
unsigned long tracker_clock_millis = 0;
|
|
bool handleTrackerCommand(uint32_t sender_timestamp, const char* command, char* reply);
|
|
void configureTrackerRuntime();
|
|
bool saveTrackerRecord(mesh::tracker::TrackerRecord& candidate);
|
|
bool trackerNeedsRadio() const;
|
|
void wakeTrackerRadio();
|
|
void cancelTrackerPending(bool delivered = false);
|
|
void serviceTracker();
|
|
bool handleTrackerResponse(mesh::Packet* packet, uint8_t type,
|
|
const uint8_t* data, size_t length,
|
|
const uint8_t* learned_path = nullptr,
|
|
uint8_t learned_path_len = 0xff);
|
|
bool handleTrackerPath(mesh::Packet* packet, uint8_t* path, uint8_t path_len,
|
|
uint8_t extra_type, uint8_t* extra, size_t extra_len);
|
|
#endif
|
|
FILESYSTEM* _fs;
|
|
uint16_t pending_preamble = 0;
|
|
unsigned long next_local_advert, next_flood_advert;
|
|
NodePrefs _prefs;
|
|
ClientACL acl;
|
|
CommonCLI _cli;
|
|
mesh::MeshClockSync _clock_sync;
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
UserGpioReplyTracker _gpio_reply_tracker;
|
|
#endif
|
|
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
|
unsigned long dirty_contacts_expiry;
|
|
uint8_t contacts_save_failures;
|
|
CayenneLPP telemetry;
|
|
TransportKeyStore key_store;
|
|
RegionMap region_map, temp_map;
|
|
RegionEntry* recv_pkt_region;
|
|
RegionEntry* load_stack[8];
|
|
TransportKey default_scope;
|
|
uint32_t last_read_time;
|
|
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
|
int num_alert_tasks;
|
|
Trigger* alert_tasks[MAX_CONCURRENT_ALERTS];
|
|
unsigned long set_radio_at, revert_radio_at;
|
|
float pending_freq;
|
|
float pending_bw;
|
|
uint8_t pending_sf;
|
|
uint8_t pending_cr;
|
|
uint8_t active_cr;
|
|
bool temp_radio_applied;
|
|
bool saved_radio_apply_pending;
|
|
unsigned long radio_apply_retry_at;
|
|
uint8_t radio_apply_failures;
|
|
|
|
bool applySavedRadioParams();
|
|
void servicePendingPrimaryRadioSettings(bool tx_gap = false);
|
|
bool isMillisTimerDue(unsigned long timestamp) const;
|
|
uint32_t limitSleepToMillisTimer(unsigned long timestamp,
|
|
uint32_t sleep_ms) const;
|
|
bool hasPendingWork() const;
|
|
mesh::powerdiag::Mask pendingWorkReason() const;
|
|
bool region_load_active;
|
|
|
|
bool telemHasChanged(ClientInfo* c);
|
|
void snapshotTelemetry(ClientInfo* c);
|
|
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
|
uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp,
|
|
uint8_t req_type, uint8_t* payload, size_t payload_len,
|
|
size_t reply_capacity = MAX_PACKET_PAYLOAD - CIPHER_MAC_SIZE - (CIPHER_BLOCK_SIZE - 1));
|
|
bool hasLocationTelemetryClient();
|
|
void updateGpsTelemetryPolicy();
|
|
mesh::Packet* createSelfAdvert();
|
|
|
|
void sendAlert(const ClientInfo* c, Trigger* t);
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
void applyGpsPrefs() {
|
|
sensors.setPowerSavingEnabled(_prefs.powersaving_enabled != 0);
|
|
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
|
|
char interval_str[12];
|
|
sprintf(interval_str, "%u", _prefs.gps_interval);
|
|
sensors.setSettingValue("gps_interval", interval_str);
|
|
sensors.applyGpsTimeSyncInterval(_prefs.gps_sync_interval_hours);
|
|
}
|
|
#endif
|
|
};
|
|
|
|
// The display implementation is kept independent of the sketch-local MyMesh
|
|
// subclass, while still needing its configured radio profiles.
|
|
SensorMesh& activeSensorMesh();
|