Files
MeshCore/examples/simple_sensor/SensorMesh.h
T
mikecarper ba6e48b5ce Fix Room and Sensor requests and add G3 Sensor builds
Bound ACL replies for encrypted routes, authorize Sensor ACL access using the actual Admin role, and preserve request retries after failed queue admission. Validate stored paths for replies, while keeping Room keep-alives direct-only.

Add both G3 RF-slot Sensor profiles, suppress battery alarms on unavailable measurements, and remove the unsupported Sensor WebConfig capability. Add executable regression coverage and CI checks.
2026-10-06 09:20:05 -07:00

269 lines
12 KiB
C++

#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.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>
#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 "14 Aug 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.17.1"
#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
&& 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;
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;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
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 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;
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;
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 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:
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();
bool isMillisTimerDue(unsigned long timestamp) const;
uint32_t limitSleepToMillisTimer(unsigned long timestamp,
uint32_t sleep_secs) const;
bool hasPendingWork() const;
bool region_load_active;
bool telemHasChanged(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();