mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-05 10:18:00 +00:00
Add get leds and set leds on|off for Companion, repeater, room server and sensor builds. Preserve the setting across reboots and cover GPIO, RGB, Bluetooth, notification and expander indicators without changing radio amplifier controls or screen backlights. RAK Companions blink while advertising and keep the Bluetooth LED off once connected, including late timer callbacks. Adapted from meshcore-dev/MeshCore#3605 and #2694. Validated with host regressions and firmware builds for SenseCAP Solar, RAK4631, T096, Heltec V4, T-Beam, Xiao S3, Pico W and Wio E5 Mini. Co-authored-by: R0ck <develop@hectospark.co.uk> Co-authored-by: Nick Dunklee <nick@dunklee.org>
366 lines
16 KiB
C++
366 lines
16 KiB
C++
#pragma once
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <math.h>
|
|
#include "helpers/UsbLogging.h"
|
|
#include "helpers/BatteryChargeSupport.h"
|
|
#include "helpers/RegulatorMode.h"
|
|
#include "helpers/PowerSavingDiagnostics.h"
|
|
#include "helpers/LedPolicy.h"
|
|
#if defined(ARDUINO) && (defined(RP2040_PLATFORM) || defined(STM32_PLATFORM))
|
|
#include <Arduino.h>
|
|
#endif
|
|
|
|
#define MAX_HASH_SIZE 8
|
|
#define PUB_KEY_SIZE 32
|
|
#define PRV_KEY_SIZE 64
|
|
#define SEED_SIZE 32
|
|
#define SIGNATURE_SIZE 64
|
|
#define MAX_ADVERT_DATA_SIZE 32
|
|
#define CIPHER_KEY_SIZE 16
|
|
#define CIPHER_BLOCK_SIZE 16
|
|
|
|
// V1
|
|
#define CIPHER_MAC_SIZE 2
|
|
#define PATH_HASH_SIZE 1
|
|
|
|
#define MAX_PACKET_PAYLOAD 184
|
|
#define MAX_GROUP_DATA_LENGTH (MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3)
|
|
#define MAX_PATH_SIZE 64
|
|
#define MAX_TRANS_UNIT 255
|
|
|
|
#if defined(ARDUINO) && \
|
|
(defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE) && \
|
|
((defined(MESH_DEBUG) && MESH_DEBUG) || \
|
|
(defined(BRIDGE_DEBUG) && BRIDGE_DEBUG) || \
|
|
(defined(POWERSAVING_DEBUG) && POWERSAVING_DEBUG))
|
|
#include <Arduino.h>
|
|
#include <atomic>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "helpers/NonBlockingWriteStream.h"
|
|
|
|
namespace mesh {
|
|
|
|
// Adafruit_USBD_CDC::write() waits until the complete buffer has entered the
|
|
// TinyUSB FIFO. That is normally convenient, but it can wait forever when a
|
|
// host has opened the dedicated logging CDC without draining it. Keep debug
|
|
// output best-effort on native TinyUSB: format into one bounded record, preserve a
|
|
// visible truncation marker, and submit it only when the whole record fits in
|
|
// the available FIFO or ESP32 software queue. The atomic flag also prevents
|
|
// overlapping debug formatter calls. The historical helper name
|
|
// is retained for callers; ESP32 TinyUSB uses the same bounded formatter.
|
|
inline size_t nrf52DebugPrintf(const char* format, ...) {
|
|
if (format == nullptr || !isUsbDebugLoggingEnabled()) return 0;
|
|
|
|
static std::atomic_flag writer_busy = ATOMIC_FLAG_INIT;
|
|
if (writer_busy.test_and_set(std::memory_order_acquire)) return 0;
|
|
|
|
char output[256];
|
|
va_list args;
|
|
va_start(args, format);
|
|
const int required = vsnprintf(output, sizeof(output), format, args);
|
|
va_end(args);
|
|
|
|
size_t length = 0;
|
|
if (required > 0) {
|
|
length = static_cast<size_t>(required);
|
|
if (length >= sizeof(output)) {
|
|
length = sizeof(output) - 1;
|
|
const size_t format_length = strlen(format);
|
|
const bool preserve_newline = format_length > 0
|
|
&& format[format_length - 1] == '\n';
|
|
const size_t marker_length = preserve_newline ? 4 : 3;
|
|
memcpy(output + length - marker_length, "...", 3);
|
|
if (preserve_newline) output[length - 1] = '\n';
|
|
}
|
|
}
|
|
|
|
size_t written = 0;
|
|
if (length > 0) {
|
|
Stream& port = usbLoggingPort();
|
|
written = port.write(reinterpret_cast<const uint8_t*>(output), length);
|
|
}
|
|
|
|
writer_busy.clear(std::memory_order_release);
|
|
return written;
|
|
}
|
|
|
|
} // namespace mesh
|
|
#endif
|
|
|
|
#if MESH_DEBUG && ARDUINO
|
|
#include <Arduino.h>
|
|
#if defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE
|
|
#define MESH_DEBUG_PRINT(F, ...) do { mesh::nrf52DebugPrintf("DEBUG: " F, ##__VA_ARGS__); } while(0)
|
|
#define MESH_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("DEBUG: " F "\n", ##__VA_ARGS__); } while(0)
|
|
#else
|
|
#define MESH_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("DEBUG: " F, ##__VA_ARGS__); } } while(0)
|
|
#define MESH_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("DEBUG: " F "\n", ##__VA_ARGS__); } } while(0)
|
|
#endif
|
|
#else
|
|
#define MESH_DEBUG_PRINT(...) {}
|
|
#define MESH_DEBUG_PRINTLN(...) {}
|
|
#endif
|
|
|
|
#if BRIDGE_DEBUG && ARDUINO
|
|
#if defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE
|
|
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } while(0)
|
|
#else
|
|
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } } while(0)
|
|
#endif
|
|
#else
|
|
#define BRIDGE_DEBUG_PRINTLN(...) {}
|
|
#endif
|
|
|
|
#if POWERSAVING_DEBUG && ARDUINO
|
|
#include <Arduino.h>
|
|
#if defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE
|
|
#define POWERSAVING_DEBUG_PRINT(F, ...) do { mesh::nrf52DebugPrintf("POWERSAVING: " F, ##__VA_ARGS__); } while(0)
|
|
#define POWERSAVING_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("POWERSAVING: " F "\n", ##__VA_ARGS__); } while(0)
|
|
#else
|
|
#define POWERSAVING_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("POWERSAVING: " F, ##__VA_ARGS__); } } while(0)
|
|
#define POWERSAVING_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("POWERSAVING: " F "\n", ##__VA_ARGS__); } } while(0)
|
|
#endif
|
|
#else
|
|
#define POWERSAVING_DEBUG_PRINT(...) {}
|
|
#define POWERSAVING_DEBUG_PRINTLN(...) {}
|
|
#endif
|
|
|
|
namespace mesh {
|
|
|
|
#define BD_STARTUP_NORMAL 0 // getStartupReason() codes
|
|
#define BD_STARTUP_RX_PACKET 1
|
|
|
|
class MainBoard {
|
|
bool _radio_test_active = false;
|
|
public:
|
|
void setRadioTestActive(bool active) { _radio_test_active = active; }
|
|
bool isRadioTestActive() const { return _radio_test_active; }
|
|
virtual uint16_t getBattMilliVolts() = 0;
|
|
virtual float getMCUTemperature() { return NAN; }
|
|
virtual bool setAdcMultiplier(float multiplier) { return false; };
|
|
virtual float getAdcMultiplier() const { return 0.0f; }
|
|
virtual const char* getManufacturerName() const = 0;
|
|
virtual void onBeforeTransmit() { }
|
|
virtual void onAfterTransmit() { }
|
|
// Update only indicator outputs; transmit hooks may also control RF hardware.
|
|
virtual void applyLedPolicy() { }
|
|
virtual void reboot() = 0;
|
|
MESH_REGULATOR_VIRTUAL uint8_t mcuRegulatorModes() const { return 0; }
|
|
MESH_REGULATOR_VIRTUAL power::RegulatorMode defaultMcuRegulatorMode() const {
|
|
return power::RegulatorMode::Default;
|
|
}
|
|
MESH_REGULATOR_VIRTUAL bool applyMcuRegulator(power::RegulatorMode) { return false; }
|
|
MESH_REGULATOR_VIRTUAL power::RegulatorMode activeMcuRegulatorMode() const {
|
|
return power::RegulatorMode::Default;
|
|
}
|
|
// Reboot into a UF2-capable bootloader when the platform supports the
|
|
// retained reset request. Returns false only when unsupported or rejected;
|
|
// a successful implementation resets and does not return.
|
|
virtual bool rebootToUf2Bootloader() { return false; }
|
|
virtual void powerOff() { /* no op */ }
|
|
// Reload an already-running system watchdog without enabling one. Long,
|
|
// internally bounded operations can use this while retaining their own
|
|
// timeout. Boards without an explicit watchdog need no implementation.
|
|
virtual void serviceWatchdog() { /* no op */ }
|
|
virtual void enterDeepSleep(uint32_t secs) { (void)secs; }
|
|
// Called by example setup() functions to signal that boot is complete.
|
|
// Boards may override to stop a boot-indicator LED sequence or similar.
|
|
// Default no-op: boards that don't care need not implement anything.
|
|
virtual void onBootComplete() { /* no op */ }
|
|
virtual uint32_t getIRQGpio() { return -1; } // not supported. Returns DIO1 (SX1262) and DIO0 (SX127x)
|
|
virtual void sleep(uint32_t secs) {
|
|
if (isRadioTestActive()) {
|
|
#if defined(ARDUINO) && (defined(RP2040_PLATFORM) || defined(STM32_PLATFORM))
|
|
if (powerdiag::enabled()) powerdiag::boardVeto(powerdiag::Reason::Board, millis(), 2);
|
|
#endif
|
|
return;
|
|
}
|
|
(void)secs;
|
|
#if defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
|
|
// These platforms have no shared deep-sleep board implementation. WFI is
|
|
// still a real CPU idle state and preserves all configured interrupt wake
|
|
// sources, including the radio and USB.
|
|
#if defined(ARDUINO)
|
|
if (powerdiag::enabled()) powerdiag::sleepBegin(micros());
|
|
#endif
|
|
__asm volatile("wfi");
|
|
#if defined(ARDUINO)
|
|
if (powerdiag::enabled()) powerdiag::sleepEnd(micros());
|
|
#endif
|
|
#endif
|
|
}
|
|
// Bounded MCU idle without changing the radio's receive mode. Interrupt-idle
|
|
// ports keep their existing periodic-clock wake contract; timed sleepers
|
|
// override this to provide a real sub-second timer. An unknown timed port
|
|
// must not round a short deadline up and sleep past it.
|
|
virtual void sleepMilliseconds(uint32_t millisecs) {
|
|
if (millisecs == 0) return;
|
|
#if defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
|
|
sleep(0);
|
|
#else
|
|
if (millisecs >= 1000UL) sleep(millisecs / 1000UL);
|
|
#endif
|
|
}
|
|
virtual uint32_t getGpio() { return 0; }
|
|
virtual void setGpio(uint32_t values) {}
|
|
// Returns true only for physical MCU GPIOs that are safe for the user to
|
|
// control in this build. Board implementations must reject pins already
|
|
// claimed by firmware or internal hardware.
|
|
virtual bool isUserGpioAvailable(uint8_t pin) const { return false; }
|
|
virtual uint8_t getStartupReason() const = 0;
|
|
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
|
|
virtual bool startOTAUpdate(const char* id, char reply[], bool force_ap = false) { return false; } // not supported
|
|
virtual bool stopOTAUpdate(char reply[]) { return false; } // not supported
|
|
virtual bool isOTAUpdateRunning() const { return false; }
|
|
// Pull-based OTA: fetch the firmware build for this variant from a baked-in manifest and flash it.
|
|
// current_ver is the running firmware version string (used to skip if already up to date); when
|
|
// dry_run is true the build is only reported, not flashed. Observer (ESP32+WiFi) builds only.
|
|
virtual bool otaFromManifest(const char* manifest_base, const char* current_ver, bool dry_run, char reply[]) { return false; }
|
|
|
|
// LoRa front-end-module LNA (RX gain) control. Only FEM-equipped boards override
|
|
// these; others report they can't control it. Driven by NodePrefs.radio_fem_rxgain.
|
|
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
|
|
virtual bool canControlLoRaFemLna() const { return false; }
|
|
virtual bool isLoRaFemLnaEnabled() const { return false; }
|
|
// Board-level physical radio reset or power cycle. Most radios expose NRST
|
|
// directly and need no override. Boards that route reset through an I/O
|
|
// expander or a dedicated regulator use this hook so liveness recovery does
|
|
// not claim a hard reset that it cannot actually perform.
|
|
virtual bool supportsRadioHardReset() const { return false; }
|
|
virtual bool resetRadio() { return false; }
|
|
// Select board-level RF hardware for a carrier before the radio is
|
|
// configured. Dual-band boards use this to power the matching FEM rail.
|
|
virtual bool prepareRadioFrequency(float frequency) {
|
|
(void)frequency;
|
|
return true;
|
|
}
|
|
// Restore board-level hardware that must remain quiescent until the radio
|
|
// has been reinitialized, such as an external FEM enable. This runs only
|
|
// after a successful hard-reset reinitialization.
|
|
virtual bool finishRadioHardReset() { return true; }
|
|
// Software-selectable external FEM transmit gain. This is not a PA power switch.
|
|
virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; }
|
|
virtual bool canControlLoRaFemPaGain() const { return false; }
|
|
virtual bool isLoRaFemPaGainEnabled() const { return false; }
|
|
#if defined(ENABLE_OTA)
|
|
// 4-byte build-target discriminator for OTA-over-LoRa (docs/ota_protocol.md Section 9). Default is the
|
|
// MOTA_TARGET_ID build flag injected by build.sh; 0 when unset (e.g. a bare IDE build).
|
|
virtual uint32_t getOtaTargetId() const {
|
|
#ifdef MOTA_TARGET_ID
|
|
return (uint32_t)(MOTA_TARGET_ID);
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
// Human-readable hardware tag (<=32 ASCII chars, e.g. "RAK4631") naming the hardware this firmware can
|
|
// boot on. Same tag == bootable-compatible; the OTA applier refuses a `.mota` whose hw_id differs (brick-
|
|
// safety). Defined per-variant via the MOTA_HW_ID build flag; "" when unset (then the check is skipped).
|
|
virtual const char* getOtaHwId() const {
|
|
#ifdef MOTA_HW_ID
|
|
return MOTA_HW_ID;
|
|
#else
|
|
return "";
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// Power management interface (boards with power management override these)
|
|
virtual bool isPwrMgtInitialised() const { return false; }
|
|
virtual bool isExternalPowered() { return false; }
|
|
virtual bool isUsbDataConnected() { return false; }
|
|
// True when the device is enumerated by a USB host, even if its serial port
|
|
// is not open. Defaults to the stricter data-connection signal on boards
|
|
// that cannot distinguish a computer from USB power.
|
|
virtual bool isUsbHostConnected() { return isUsbDataConnected(); }
|
|
// True while a board can identify an active battery-charging source.
|
|
virtual bool isChargerActive() { return false; }
|
|
|
|
#if MESH_BATTERY_CHARGE_CONTROL
|
|
// Actual charger voltage, in millivolts. Battery reporting and protection
|
|
// thresholds are separate capabilities. A successful setter must verify
|
|
// hardware readback and persist the setting for the next boot.
|
|
virtual bool getBatteryChargeTarget(uint16_t& millivolts) { return false; }
|
|
virtual bool supportsBatteryChargeTarget(uint16_t millivolts) { return false; }
|
|
virtual bool setBatteryChargeTarget(uint16_t millivolts) { return false; }
|
|
virtual const char* getBatteryChargeTargetOptions() const { return nullptr; }
|
|
virtual bool batteryChargeTargetRestoreFailed() const { return false; }
|
|
virtual const char* getBatteryChargeTargetUnsupportedReason() const {
|
|
return "charge.voltage is not supported on this board";
|
|
}
|
|
#endif
|
|
|
|
// Optional, source-specific power detection. Boards that can distinguish a USB
|
|
// supply from a solar charger override these; defaults keep every other board
|
|
// unaffected (the generic CLI prints "n/a" when a capability is absent).
|
|
virtual bool hasUsbPowerDetect() const { return false; }
|
|
virtual bool isUsbPowered() { return false; }
|
|
virtual bool hasSolarChargerDetect() const { return false; }
|
|
virtual bool isSolarChargerActive() { return false; }
|
|
virtual uint16_t getBootVoltage() { return 0; }
|
|
virtual bool getWakeLpcompSupported() const { return false; }
|
|
virtual uint32_t getResetReason() const { return 0; }
|
|
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
|
|
virtual uint8_t getShutdownReason() const { return 0; }
|
|
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
|
|
virtual bool isPowerManagementInitialized() const { return false; }
|
|
virtual bool supportsVoltageWake() const { return false; }
|
|
|
|
virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
|
(void)command;
|
|
(void)sender_timestamp;
|
|
(void)reply;
|
|
return false;
|
|
}
|
|
|
|
// Let a board feed its watchdog and perform periodic housekeeping.
|
|
virtual void loop() {}
|
|
|
|
inline static uint32_t n_cad_busy = 0;
|
|
};
|
|
|
|
/**
|
|
* An abstraction of the device's Realtime Clock.
|
|
*/
|
|
class RTCClock {
|
|
uint32_t last_unique;
|
|
protected:
|
|
RTCClock() { last_unique = 0; }
|
|
|
|
public:
|
|
/**
|
|
* \returns the current time. in UNIX epoch seconds.
|
|
*/
|
|
virtual uint32_t getCurrentTime() = 0;
|
|
|
|
/**
|
|
* \param time current time in UNIX epoch seconds.
|
|
*/
|
|
virtual void setCurrentTime(uint32_t time) = 0;
|
|
|
|
/**
|
|
* override in classes that need to periodically update internal state
|
|
*/
|
|
virtual void tick() { /* no op */}
|
|
|
|
uint32_t getCurrentTimeUnique() {
|
|
uint32_t t = getCurrentTime();
|
|
if (t <= last_unique) {
|
|
return ++last_unique;
|
|
}
|
|
return last_unique = t;
|
|
}
|
|
|
|
/** Reset the monotonic timestamp helper after an intentional RTC correction.
|
|
* This is needed when a caller explicitly permits moving the wall clock
|
|
* backward and wants subsequent generated timestamps to use the new clock. */
|
|
void resetUniqueTime(uint32_t time) {
|
|
last_unique = time > 0 ? time - 1 : 0;
|
|
}
|
|
};
|
|
|
|
}
|