* Seeed Tracker X1 Support * silence CPPCheck * fix macro order regression and adapt stm32hal for new radiolib * STM32 is a radiolib upstream fix ( https://github.com/jgromes/RadioLib/issues/1825 ) * fix STM32 regression * Seeed Tracker X1 Support * silence CPPCheck * fix macro order regression and adapt stm32hal for new radiolib * STM32 is a radiolib upstream fix ( https://github.com/jgromes/RadioLib/issues/1825 ) * fix STM32 regression * address copilot OCD * Split behavior only when the two LEDs are on distinct pins. * bring naming in line with the other seeed devices * update env name to fit convention too * guarantee evaluation order * Guard sensor readings against null values, make sensor use less power
1439 lines
50 KiB
C++
1439 lines
50 KiB
C++
#include "configuration.h"
|
||
#ifdef ARCH_PORTDUINO_WASM
|
||
#include <emscripten.h>
|
||
#endif
|
||
#if !MESHTASTIC_EXCLUDE_GPS
|
||
#include "GPS.h"
|
||
#endif
|
||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||
#include "input/InputBroker.h"
|
||
#endif
|
||
#include "MeshRadio.h"
|
||
#include "MeshService.h"
|
||
#include "NodeDB.h"
|
||
#include "PowerFSM.h"
|
||
#include "PowerMon.h"
|
||
#include "RadioLibInterface.h"
|
||
#include "ReliableRouter.h"
|
||
#include "TransmitHistory.h"
|
||
#include "airtime.h"
|
||
#include "buzz.h"
|
||
#include "power/PowerHAL.h"
|
||
|
||
#include "FSCommon.h"
|
||
#include "RTC.h"
|
||
#include "SPILock.h"
|
||
#include "Throttle.h"
|
||
#include "concurrency/OSThread.h"
|
||
#include "concurrency/Periodic.h"
|
||
#include "detect/ScanI2C.h"
|
||
#include "error.h"
|
||
#include "power.h"
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
#include "detect/ScanI2CConsumer.h"
|
||
#include "detect/ScanI2CTwoWire.h"
|
||
#include <Wire.h>
|
||
#endif
|
||
#include "detect/einkScan.h"
|
||
#include "graphics/Screen.h"
|
||
#include "main.h"
|
||
#include "mesh/generated/meshtastic/config.pb.h"
|
||
#include "meshUtils.h"
|
||
#include "modules/Modules.h"
|
||
#include "sleep.h"
|
||
#include "target_specific.h"
|
||
#include <memory>
|
||
#include <utility>
|
||
#if HAS_SCREEN
|
||
#include "MessageStore.h"
|
||
#endif
|
||
|
||
#ifdef ARCH_ESP32
|
||
#include "freertosinc.h"
|
||
#if !MESHTASTIC_EXCLUDE_WEBSERVER
|
||
#include "mesh/http/WebServer.h"
|
||
#endif
|
||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||
#include "nimble/NimbleBluetooth.h"
|
||
NimbleBluetooth *nimbleBluetooth = nullptr;
|
||
#endif
|
||
#endif
|
||
|
||
#ifdef ARCH_NRF52
|
||
#include "NRF52Bluetooth.h"
|
||
NRF52Bluetooth *nrf52Bluetooth = nullptr;
|
||
#endif
|
||
|
||
#ifdef ARCH_NRF54L15
|
||
void nrf54l15Setup();
|
||
void nrf54l15Loop();
|
||
NRF54L15Bluetooth *nrf54l15Bluetooth = nullptr;
|
||
#endif
|
||
|
||
#ifdef MESHTASTIC_ENABLE_APPROTECT
|
||
#include "security/APProtect.h"
|
||
#endif
|
||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||
#include "security/EncryptedStorage.h"
|
||
#endif
|
||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||
#include "mesh/PhoneAPI.h"
|
||
#endif
|
||
#ifdef MESHTASTIC_LOCKDOWN
|
||
#include "security/LockdownDisplay.h"
|
||
#endif
|
||
|
||
#if HAS_WIFI || defined(USE_WS5500) || defined(USE_CH390D)
|
||
#include "mesh/api/WiFiServerAPI.h"
|
||
#include "mesh/wifi/WiFiAPClient.h"
|
||
#endif
|
||
|
||
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
|
||
#include "mesh/api/ethServerAPI.h"
|
||
#include "mesh/eth/ethClient.h"
|
||
#endif
|
||
|
||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||
#include "mqtt/MQTT.h"
|
||
#endif
|
||
|
||
#ifdef ARCH_PORTDUINO
|
||
#include "linux/LinuxHardwareI2C.h"
|
||
#ifndef ARCH_PORTDUINO_WASM // raspi HTTP server (ulfius/zlib/openssl) excluded in the browser/wasm build
|
||
#include "mesh/raspihttp/PiWebServer.h"
|
||
#endif
|
||
#include "platform/portduino/PortduinoGlue.h"
|
||
#include <cstdlib>
|
||
#include <fstream>
|
||
#include <iostream>
|
||
#include <string>
|
||
#endif
|
||
|
||
#ifdef ARCH_ESP32
|
||
#ifdef DEBUG_PARTITION_TABLE
|
||
#include "esp_partition.h"
|
||
|
||
void printPartitionTable()
|
||
{
|
||
printf("\n--- Partition Table ---\n");
|
||
// Print Column Headers
|
||
printf("| %-16s | %-4s | %-7s | %-10s | %-10s |\n", "Label", "Type", "Subtype", "Offset", "Size");
|
||
printf("|------------------|------|---------|------------|------------|\n");
|
||
|
||
// Create an iterator to find ALL partitions (Type ANY, Subtype ANY)
|
||
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||
|
||
// Loop through the iterator
|
||
if (it != NULL) {
|
||
do {
|
||
const esp_partition_t *part = esp_partition_get(it);
|
||
|
||
// Print details: Label, Type (Hex), Subtype (Hex), Offset (Hex), Size (Hex)
|
||
printf("| %-16s | 0x%02x | 0x%02x | 0x%08x | 0x%08x |\n", part->label, part->type, part->subtype, part->address,
|
||
part->size);
|
||
|
||
// Move to next partition
|
||
it = esp_partition_next(it);
|
||
} while (it != NULL);
|
||
|
||
// Release the iterator memory
|
||
esp_partition_iterator_release(it);
|
||
} else {
|
||
printf("No partitions found.\n");
|
||
}
|
||
printf("-----------------------\n");
|
||
}
|
||
#endif // DEBUG_PARTITION_TABLE
|
||
#endif // ARCH_ESP32
|
||
|
||
#include "AmbientLightingThread.h"
|
||
#include "PowerFSMThread.h"
|
||
|
||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||
#include "motion/AccelerometerThread.h"
|
||
AccelerometerThread *accelerometerThread = nullptr;
|
||
#endif
|
||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||
#include "motion/MagnetometerThread.h"
|
||
MagnetometerThread *magnetometerThread = nullptr;
|
||
#endif
|
||
|
||
#ifdef HAS_I2S
|
||
#include "AudioThread.h"
|
||
AudioThread *audioThread = nullptr;
|
||
#endif
|
||
|
||
#ifdef USE_XL9555
|
||
#include "ExtensionIOXL9555.hpp"
|
||
ExtensionIOXL9555 io;
|
||
#endif
|
||
|
||
#if HAS_TFT
|
||
extern void tftSetup(void);
|
||
#endif
|
||
|
||
#ifdef HAS_UDP_MULTICAST
|
||
#include "mesh/udp/UdpMulticastHandler.h"
|
||
UdpMulticastHandler *udpHandler = nullptr;
|
||
#endif
|
||
|
||
#if defined(TCXO_OPTIONAL)
|
||
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down.
|
||
#endif
|
||
|
||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||
void setupNicheGraphics();
|
||
#include "nicheGraphics.h"
|
||
#endif
|
||
|
||
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
|
||
SPIClass SPI1(HSPI);
|
||
#endif
|
||
|
||
using namespace concurrency;
|
||
|
||
volatile static const char slipstreamTZString[] = {USERPREFS_TZ_STRING};
|
||
|
||
// We always create a screen object, but we only init it if we find the hardware
|
||
graphics::Screen *screen = nullptr;
|
||
|
||
// Global power status
|
||
meshtastic::PowerStatus *powerStatus = new meshtastic::PowerStatus();
|
||
|
||
// Global GPS status
|
||
meshtastic::GPSStatus *gpsStatus = new meshtastic::GPSStatus();
|
||
|
||
// Global Node status
|
||
meshtastic::NodeStatus *nodeStatus = new meshtastic::NodeStatus();
|
||
|
||
// Global Bluetooth status
|
||
meshtastic::BluetoothStatus *bluetoothStatus = new meshtastic::BluetoothStatus();
|
||
|
||
// Scan for I2C Devices
|
||
|
||
/// The I2C address of our display (if found)
|
||
ScanI2C::DeviceAddress screen_found = ScanI2C::ADDRESS_NONE;
|
||
|
||
// The I2C address of the cardkb or RAK14004 (if found)
|
||
ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE;
|
||
// 0x02 for RAK14004, 0x00 for cardkb, 0x10 for T-Deck
|
||
uint8_t kb_model;
|
||
// global bool to record that a kb is present
|
||
bool kb_found = false;
|
||
// global bool to record that on-screen keyboard (OSK) is present
|
||
bool osk_found = false;
|
||
|
||
// The I2C address of the RTC Module (if found)
|
||
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
|
||
// The I2C address of the Accelerometer (if found)
|
||
ScanI2C::DeviceAddress accelerometer_found = ScanI2C::ADDRESS_NONE;
|
||
// The I2C address of the Magnetometer (if found)
|
||
ScanI2C::DeviceAddress magnetometer_found = ScanI2C::ADDRESS_NONE;
|
||
// The I2C address of the RGB LED (if found)
|
||
ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ScanI2C::ADDRESS_NONE);
|
||
/// The I2C address of our Air Quality Indicator (if found)
|
||
ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE;
|
||
|
||
#ifdef HAS_DRV2605
|
||
Adafruit_DRV2605 drv;
|
||
#endif
|
||
|
||
bool isVibrating = false;
|
||
|
||
bool eink_found = true;
|
||
|
||
uint32_t serialSinceMsec;
|
||
bool pauseBluetoothLogging = false;
|
||
|
||
bool pmu_found;
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
// Array map of sensor types with i2c address and wire as we'll find in the i2c scan
|
||
std::pair<uint8_t, TwoWire *> nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1] = {};
|
||
#endif
|
||
|
||
Router *router = NULL; // Users of router don't care what sort of subclass implements that API
|
||
|
||
const char *firmware_version = optstr(APP_VERSION_SHORT);
|
||
|
||
const char *getDeviceName()
|
||
{
|
||
uint8_t dmac[6];
|
||
|
||
getMacAddr(dmac);
|
||
|
||
// Meshtastic_ab3c or Shortname_abcd
|
||
static char name[20];
|
||
snprintf(name, sizeof(name), "%02x%02x", dmac[4], dmac[5]);
|
||
// if the shortname exists and is NOT the new default of ab3c, use it for BLE name.
|
||
if (strcmp(owner.short_name, name) != 0) {
|
||
snprintf(name, sizeof(name), "%s_%02x%02x", owner.short_name, dmac[4], dmac[5]);
|
||
} else {
|
||
snprintf(name, sizeof(name), "Meshtastic_%02x%02x", dmac[4], dmac[5]);
|
||
}
|
||
return name;
|
||
}
|
||
|
||
uint32_t timeLastPowered = 0;
|
||
|
||
static OSThread *powerFSMthread;
|
||
AmbientLightingThread *ambientLightingThread;
|
||
|
||
RadioLibHal *RadioLibHAL = NULL;
|
||
|
||
/**
|
||
* Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep.
|
||
*/
|
||
__attribute__((weak, noinline)) bool loopCanSleep()
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Weak empty variant initialization function.
|
||
// May be redefined by variant files.
|
||
void lateInitVariant() __attribute__((weak));
|
||
void lateInitVariant() {}
|
||
|
||
void earlyInitVariant() __attribute__((weak));
|
||
void earlyInitVariant() {}
|
||
|
||
// NRF52 (and probably other platforms) can report when system is in power failure mode
|
||
// (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc).
|
||
// For example NRF52 will prevent any flash writes in that case automatically
|
||
// (but it causes issues we need to handle).
|
||
// This detection is independent from whatever ADC or dividers used in Meshtastic
|
||
// boards and is internal to chip.
|
||
|
||
// we use powerHAL layer to get this info and delay booting until power level is safe
|
||
|
||
// wait until power level is safe to continue booting (to avoid bootloops)
|
||
// blink user led in 3 flashes sequence to indicate what is happening
|
||
void waitUntilPowerLevelSafe()
|
||
{
|
||
while (powerHAL_isPowerLevelSafe() == false) {
|
||
|
||
#ifdef LED_POWER
|
||
|
||
// 3x: blink for 300 ms, pause for 300 ms
|
||
|
||
for (int i = 0; i < 3; i++) {
|
||
digitalWrite(LED_POWER, LED_STATE_ON);
|
||
delay(300);
|
||
digitalWrite(LED_POWER, LED_STATE_OFF);
|
||
delay(300);
|
||
}
|
||
#endif
|
||
|
||
// sleep for 2s
|
||
delay(2000);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Print info as a structured log message (for automated log processing)
|
||
*/
|
||
void printInfo()
|
||
{
|
||
LOG_INFO("S:B:%d,%s,%s,%s", HW_VENDOR, optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO));
|
||
}
|
||
#ifndef PIO_UNIT_TESTING
|
||
void setup()
|
||
{
|
||
|
||
// initialize power HAL layer as early as possible
|
||
powerHAL_init();
|
||
|
||
#ifdef LED_POWER
|
||
pinMode(LED_POWER, OUTPUT);
|
||
digitalWrite(LED_POWER, LED_STATE_ON);
|
||
#endif
|
||
|
||
// prevent booting if device is in power failure mode
|
||
// boot sequence will follow when battery level raises to safe mode
|
||
waitUntilPowerLevelSafe();
|
||
|
||
// Defined in variant.cpp for early init code
|
||
earlyInitVariant();
|
||
|
||
#if defined(PIN_POWER_EN)
|
||
pinMode(PIN_POWER_EN, OUTPUT);
|
||
digitalWrite(PIN_POWER_EN, HIGH);
|
||
#endif
|
||
|
||
#ifdef LED_NOTIFICATION
|
||
pinMode(LED_NOTIFICATION, OUTPUT);
|
||
digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON);
|
||
#endif
|
||
|
||
#ifdef WIFI_LED
|
||
pinMode(WIFI_LED, OUTPUT);
|
||
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
|
||
#endif
|
||
|
||
#ifdef BLE_LED
|
||
pinMode(BLE_LED, OUTPUT);
|
||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||
#endif
|
||
|
||
concurrency::hasBeenSetup = true;
|
||
#if HAS_SCREEN
|
||
meshtastic_Config_DisplayConfig_OledType screen_model =
|
||
meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
|
||
#endif
|
||
OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64;
|
||
|
||
#ifdef USE_SEGGER
|
||
auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM;
|
||
#ifdef NRF52840_XXAA
|
||
auto buflen = 4096; // this board has a fair amount of ram
|
||
#else
|
||
auto buflen = 256; // this board has a fair amount of ram
|
||
#endif
|
||
SEGGER_RTT_ConfigUpBuffer(SEGGER_STDOUT_CH, NULL, NULL, buflen, mode);
|
||
#endif
|
||
|
||
#ifdef DEBUG_PORT
|
||
consoleInit(); // Set serial baud rate and init our mesh console
|
||
#endif
|
||
|
||
// M23 (audit): APPROTECT engagement moved below fsInit() so we can gate
|
||
// on EncryptedStorage::isProvisioned(). Engaging on an unprovisioned dev
|
||
// board permanently locks SWD before the operator has even set a
|
||
// passphrase - a misconfigured CI build flashed to a developer device
|
||
// would brick its debug port on first boot. Now we only engage when the
|
||
// device has a DEK file on flash, i.e. the operator has explicitly
|
||
// committed to lockdown via passphrase provisioning.
|
||
|
||
#ifdef UNPHONE
|
||
unphone.printStore();
|
||
#endif
|
||
|
||
#if ARCH_PORTDUINO
|
||
RTCQuality ourQuality = RTCQualityDevice;
|
||
|
||
std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c");
|
||
if (timeCommandResult[0] == '1') {
|
||
ourQuality = RTCQualityNTP;
|
||
}
|
||
|
||
struct timeval tv;
|
||
tv.tv_sec = time(NULL);
|
||
tv.tv_usec = 0;
|
||
perhapsSetRTC(ourQuality, &tv);
|
||
#endif
|
||
|
||
powerMonInit();
|
||
serialSinceMsec = millis();
|
||
|
||
LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");
|
||
|
||
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
|
||
#ifndef SENSECAP_INDICATOR
|
||
// use PSRAM for malloc calls > 2048 bytes
|
||
heap_caps_malloc_extmem_enable(2048);
|
||
#endif
|
||
#endif
|
||
|
||
// The DEBUG_MUTE "we are muted, FYI" banner spills APP_VERSION / APP_ENV /
|
||
// APP_REPO out the USB CDC even with logging otherwise suppressed - a free
|
||
// firmware-fingerprinting primitive for an attacker holding the cable.
|
||
// Under MESHTASTIC_LOCKDOWN we want the device to look uniformly silent
|
||
// until the operator authenticates, so skip the banner entirely there.
|
||
#if defined(DEBUG_MUTE) && defined(DEBUG_PORT) && !defined(MESHTASTIC_LOCKDOWN)
|
||
DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n");
|
||
DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO));
|
||
DEBUG_PORT.printf("Debug mute is enabled, there will be no serial output.\r\n");
|
||
#endif
|
||
|
||
initDeepSleep();
|
||
|
||
#if defined(MODEM_POWER_EN)
|
||
pinMode(MODEM_POWER_EN, OUTPUT);
|
||
digitalWrite(MODEM_POWER_EN, LOW);
|
||
#endif
|
||
|
||
#if defined(MODEM_PWRKEY)
|
||
pinMode(MODEM_PWRKEY, OUTPUT);
|
||
digitalWrite(MODEM_PWRKEY, LOW);
|
||
#endif
|
||
|
||
#if defined(LORA_TCXO_GPIO)
|
||
pinMode(LORA_TCXO_GPIO, OUTPUT);
|
||
digitalWrite(LORA_TCXO_GPIO, HIGH);
|
||
#endif
|
||
|
||
#if defined(VEXT_ENABLE)
|
||
pinMode(VEXT_ENABLE, OUTPUT);
|
||
digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power
|
||
#endif
|
||
|
||
#if defined(PIN_SENSOR_EN)
|
||
pinMode(PIN_SENSOR_EN, OUTPUT);
|
||
digitalWrite(PIN_SENSOR_EN, PIN_SENSOR_EN_ACTIVE); // turn on sensor power
|
||
#endif
|
||
|
||
#if defined(BIAS_T_ENABLE)
|
||
pinMode(BIAS_T_ENABLE, OUTPUT);
|
||
digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna
|
||
#endif
|
||
|
||
#if defined(VTFT_CTRL)
|
||
pinMode(VTFT_CTRL, OUTPUT);
|
||
digitalWrite(VTFT_CTRL, LOW);
|
||
#endif
|
||
|
||
#ifdef RESET_OLED
|
||
pinMode(RESET_OLED, OUTPUT);
|
||
digitalWrite(RESET_OLED, 1);
|
||
delay(2);
|
||
digitalWrite(RESET_OLED, 0);
|
||
delay(10);
|
||
digitalWrite(RESET_OLED, 1);
|
||
#endif
|
||
|
||
#ifdef SENSOR_POWER_CTRL_PIN
|
||
pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT);
|
||
digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON);
|
||
#endif
|
||
|
||
#ifdef SENSOR_GPS_CONFLICT
|
||
bool sensor_detected = false;
|
||
#endif
|
||
#ifdef PERIPHERAL_WARMUP_MS
|
||
// Some peripherals may require additional time to stabilize after power is connected
|
||
// e.g. I2C on Heltec Vision Master
|
||
LOG_INFO("Wait for peripherals to stabilize");
|
||
delay(PERIPHERAL_WARMUP_MS);
|
||
#endif
|
||
initSPI();
|
||
|
||
OSThread::setup();
|
||
|
||
fsInit();
|
||
|
||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||
EncryptedStorage::initLocked();
|
||
if (!EncryptedStorage::isUnlocked()) {
|
||
if (!EncryptedStorage::isProvisioned()) {
|
||
LOG_WARN("Lockdown: Device not provisioned - connect and set a passphrase to unlock storage");
|
||
} else {
|
||
LOG_WARN("Lockdown: Device locked - connect and provide passphrase to unlock storage");
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if defined(MESHTASTIC_ENABLE_APPROTECT) && defined(MESHTASTIC_ENCRYPTED_STORAGE)
|
||
// M23 (audit): only engage the irreversible UICR APPROTECT lockout once
|
||
// the device has been provisioned with a passphrase. A misconfigured
|
||
// CI build of a lockdown variant flashed to a developer board would
|
||
// otherwise burn SWD on first boot before the operator has even set a
|
||
// passphrase, taking the board out of the dev/recovery workflow with
|
||
// no real security benefit (there's no DEK to protect yet). Once a
|
||
// DEK file exists, the operator has committed to lockdown - engaging
|
||
// APPROTECT then is the protection they asked for.
|
||
if (EncryptedStorage::isProvisioned()) {
|
||
enableAPProtect();
|
||
} else {
|
||
LOG_INFO("APPROTECT deferred: device not yet provisioned");
|
||
}
|
||
#elif defined(MESHTASTIC_ENABLE_APPROTECT)
|
||
// Lockdown without encrypted storage shouldn't be reachable per
|
||
// configuration.h, but if it ever is, fall back to the unconditional
|
||
// engagement.
|
||
enableAPProtect();
|
||
#endif
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
#if defined(I2C_SDA1) && defined(ARCH_RP2040)
|
||
Wire1.setSDA(I2C_SDA1);
|
||
Wire1.setSCL(I2C_SCL1);
|
||
Wire1.begin();
|
||
#elif defined(I2C_SDA1) && !defined(ARCH_RP2040)
|
||
Wire1.begin(I2C_SDA1, I2C_SCL1);
|
||
#elif WIRE_INTERFACES_COUNT == 2
|
||
Wire1.begin();
|
||
#endif
|
||
|
||
#if defined(I2C_SDA) && defined(ARCH_RP2040)
|
||
Wire.setSDA(I2C_SDA);
|
||
Wire.setSCL(I2C_SCL);
|
||
Wire.begin();
|
||
#elif defined(I2C_SDA) && !defined(ARCH_RP2040)
|
||
LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL);
|
||
Wire.begin(I2C_SDA, I2C_SCL);
|
||
#elif defined(ARCH_PORTDUINO)
|
||
if (portduino_config.i2cdev != "") {
|
||
LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str());
|
||
Wire.begin(portduino_config.i2cdev.c_str());
|
||
} else {
|
||
LOG_INFO("No I2C device configured, Skip");
|
||
}
|
||
#elif HAS_WIRE
|
||
Wire.begin();
|
||
#endif
|
||
#endif
|
||
|
||
#if defined(M5STACK_UNITC6L)
|
||
pinMode(LORA_CS, OUTPUT);
|
||
digitalWrite(LORA_CS, 1);
|
||
c6l_init();
|
||
#endif
|
||
|
||
#ifdef PIN_LCD_RESET
|
||
// FIXME - move this someplace better, LCD is at address 0x3F
|
||
pinMode(PIN_LCD_RESET, OUTPUT);
|
||
digitalWrite(PIN_LCD_RESET, 0);
|
||
delay(1);
|
||
digitalWrite(PIN_LCD_RESET, 1);
|
||
delay(1);
|
||
#endif
|
||
|
||
#ifdef AQ_SET_PIN
|
||
// RAK-12039 set pin for Air quality sensor. Detectable on I2C after ~3 seconds, so we need to rescan later
|
||
pinMode(AQ_SET_PIN, OUTPUT);
|
||
digitalWrite(AQ_SET_PIN, HIGH);
|
||
#endif
|
||
|
||
// Currently only the tbeam has a PMU
|
||
// PMU initialization needs to be placed before i2c scanning
|
||
power = new Power();
|
||
power->setStatusHandler(powerStatus);
|
||
powerStatus->observe(&power->newStatus);
|
||
power->setup(); // Must be after status handler is installed, so that handler gets notified of the initial configuration
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
// We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to
|
||
// accessories
|
||
auto i2cScanner = std::unique_ptr<ScanI2CTwoWire>(new ScanI2CTwoWire());
|
||
#if HAS_WIRE
|
||
LOG_INFO("Scan for i2c devices");
|
||
#endif
|
||
|
||
#if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2))
|
||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1);
|
||
#endif
|
||
|
||
#if defined(I2C_SDA)
|
||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
|
||
#elif defined(ARCH_PORTDUINO)
|
||
if (portduino_config.i2cdev != "") {
|
||
LOG_INFO("Scan for i2c devices");
|
||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
|
||
}
|
||
#elif HAS_WIRE
|
||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
|
||
#endif
|
||
|
||
auto i2cCount = i2cScanner->countDevices();
|
||
if (i2cCount == 0) {
|
||
LOG_INFO("No I2C devices found");
|
||
} else {
|
||
LOG_INFO("%i I2C devices found", i2cCount);
|
||
#ifdef SENSOR_GPS_CONFLICT
|
||
sensor_detected = true;
|
||
#endif
|
||
}
|
||
#ifdef ARCH_ESP32
|
||
#ifdef DEBUG_PARTITION_TABLE
|
||
printPartitionTable();
|
||
#endif
|
||
#endif // ARCH_ESP32
|
||
#ifdef ARCH_ESP32
|
||
// Don't init display if we don't have one or we are waking headless due to a timer event
|
||
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) {
|
||
LOG_DEBUG("suppress screen wake because this is a headless timer wakeup");
|
||
i2cScanner->setSuppressScreen();
|
||
}
|
||
#endif
|
||
|
||
#if HAS_SCREEN
|
||
auto screenInfo = i2cScanner->firstScreen();
|
||
screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE;
|
||
|
||
if (screen_found.port != ScanI2C::I2CPort::NO_I2C) {
|
||
switch (screenInfo.type) {
|
||
case ScanI2C::DeviceType::SCREEN_SH1106:
|
||
screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SH1106;
|
||
break;
|
||
case ScanI2C::DeviceType::SCREEN_SSD1306:
|
||
screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306;
|
||
break;
|
||
case ScanI2C::DeviceType::SCREEN_ST7567:
|
||
case ScanI2C::DeviceType::SCREEN_UNKNOWN:
|
||
default:
|
||
screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#define UPDATE_FROM_SCANNER(FIND_FN)
|
||
#if defined(USE_VIRTUAL_KEYBOARD)
|
||
kb_found = true;
|
||
#endif
|
||
auto rtc_info = i2cScanner->firstRTC();
|
||
rtc_found = rtc_info.type != ScanI2C::DeviceType::NONE ? rtc_info.address : rtc_found;
|
||
|
||
auto kb_info = i2cScanner->firstKeyboard();
|
||
|
||
if (kb_info.type != ScanI2C::DeviceType::NONE) {
|
||
kb_found = true;
|
||
cardkb_found = kb_info.address;
|
||
switch (kb_info.type) {
|
||
case ScanI2C::DeviceType::RAK14004:
|
||
kb_model = 0x02;
|
||
break;
|
||
case ScanI2C::DeviceType::CARDKB:
|
||
kb_model = 0x00;
|
||
break;
|
||
case ScanI2C::DeviceType::TDECKKB:
|
||
// assign an arbitrary value to distinguish from other models
|
||
kb_model = 0x10;
|
||
break;
|
||
case ScanI2C::DeviceType::BBQ10KB:
|
||
// assign an arbitrary value to distinguish from other models
|
||
kb_model = 0x11;
|
||
break;
|
||
case ScanI2C::DeviceType::MPR121KB:
|
||
// assign an arbitrary value to distinguish from other models
|
||
kb_model = 0x37;
|
||
break;
|
||
case ScanI2C::DeviceType::TCA8418KB:
|
||
// assign an arbitrary value to distinguish from other models
|
||
kb_model = 0x84;
|
||
break;
|
||
default:
|
||
// use this as default since it's also just zero
|
||
LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type);
|
||
kb_model = 0x00;
|
||
}
|
||
}
|
||
|
||
pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101);
|
||
|
||
auto aqiInfo = i2cScanner->firstAQI();
|
||
aqi_found = aqiInfo.type != ScanI2C::DeviceType::NONE ? aqiInfo.address : ScanI2C::ADDRESS_NONE;
|
||
|
||
/*
|
||
* There are a bunch of sensors that have no further logic than to be found and stuffed into the
|
||
* nodeTelemetrySensorsMap singleton. This wraps that logic in a temporary scope to declare the temporary field
|
||
* "found".
|
||
*/
|
||
|
||
// Two supported RGB LED currently
|
||
#ifdef HAS_RGB_LED
|
||
rgb_found = i2cScanner->firstRGBLED();
|
||
#endif
|
||
|
||
#ifdef HAS_TPS65233
|
||
// TPS65233 is a power management IC for satellite modems, used in the Dreamcatcher
|
||
// We are switching it off here since we don't use an LNB.
|
||
if (i2cScanner->exists(ScanI2C::DeviceType::TPS65233)) {
|
||
Wire.beginTransmission(TPS65233_ADDR);
|
||
Wire.write(0); // Register 0
|
||
Wire.write(128); // Turn off the LNB power, keep I2C Control enabled
|
||
Wire.endTransmission();
|
||
Wire.beginTransmission(TPS65233_ADDR);
|
||
Wire.write(1); // Register 1
|
||
Wire.write(0); // Turn off Tone Generator 22kHz
|
||
Wire.endTransmission();
|
||
}
|
||
#endif
|
||
|
||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||
auto acc_info = i2cScanner->firstAccelerometer();
|
||
accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found;
|
||
LOG_DEBUG("acc_info = %i", acc_info.type);
|
||
#endif
|
||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||
auto mag_info = i2cScanner->firstMagnetometer();
|
||
magnetometer_found = mag_info.type != ScanI2C::DeviceType::NONE ? mag_info.address : magnetometer_found;
|
||
LOG_DEBUG("mag_info = %i", mag_info.type);
|
||
#endif
|
||
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310U, meshtastic_TelemetrySensorType_QMC6310);
|
||
// TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N
|
||
// scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MMC5983MA, meshtastic_TelemetrySensorType_MMC5983MA);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM42607P, meshtastic_TelemetrySensorType_ICM42607P);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
|
||
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
|
||
#endif
|
||
|
||
#ifdef HAS_SDCARD
|
||
setupSDCard();
|
||
#endif
|
||
|
||
// Hello
|
||
printInfo();
|
||
#ifdef BUILD_EPOCH
|
||
LOG_INFO("Build timestamp: %ld", BUILD_EPOCH);
|
||
#endif
|
||
|
||
#ifdef ARCH_ESP32
|
||
esp32Setup();
|
||
#endif
|
||
|
||
#ifdef ARCH_NRF52
|
||
nrf52Setup();
|
||
#endif
|
||
#ifdef ARCH_NRF54L15
|
||
nrf54l15Setup();
|
||
#endif
|
||
|
||
#ifdef ARCH_RP2040
|
||
rp2040Setup();
|
||
#endif
|
||
|
||
// We do this as early as possible because this loads preferences from flash
|
||
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
|
||
nodeDB = new NodeDB;
|
||
#ifdef ARCH_ESP32
|
||
// Config is loaded now, and Bluetooth has not been initialized yet. If the
|
||
// saved config will keep Bluetooth inactive, return its reserved memory early.
|
||
esp32ReleaseBluetoothMemoryIfUnused();
|
||
#endif
|
||
|
||
// Initialize transmit history to persist broadcast throttle timers across reboots
|
||
TransmitHistory::getInstance()->loadFromDisk();
|
||
#if HAS_TFT
|
||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||
tftSetup();
|
||
}
|
||
#endif
|
||
|
||
router = new ReliableRouter();
|
||
|
||
// only play start melody when role is not tracker or sensor
|
||
if (config.power.is_power_saving == true &&
|
||
IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER,
|
||
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR))
|
||
LOG_DEBUG("Tracker/Sensor: Skip start melody");
|
||
else
|
||
playStartMelody();
|
||
|
||
#if HAS_SCREEN
|
||
// fixed screen override?
|
||
#if defined(USE_SH1107)
|
||
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128
|
||
screen_geometry = GEOMETRY_128_128;
|
||
#elif defined(USE_SH1107_128_64)
|
||
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
|
||
#else
|
||
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) {
|
||
screen_model = config.display.oled;
|
||
|
||
// Fix: update geometry for SH1107 128x128 selected via menu
|
||
if (screen_model == meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128) {
|
||
screen_geometry = GEOMETRY_128_128;
|
||
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // normalize
|
||
}
|
||
}
|
||
#endif
|
||
#ifdef OLED_GEOMETRY_OVERRIDE
|
||
// Per-variant geometry (e.g. 72x40 micro-OLEDs). Takes precedence over the
|
||
// default GEOMETRY_128_64 set at the top of setup().
|
||
screen_geometry = OLED_GEOMETRY_OVERRIDE;
|
||
#endif
|
||
#endif
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||
if (acc_info.type != ScanI2C::DeviceType::NONE) {
|
||
accelerometerThread = new AccelerometerThread(acc_info.type);
|
||
}
|
||
#endif
|
||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
|
||
if (mag_info.type != ScanI2C::DeviceType::NONE) {
|
||
magnetometerThread = new MagnetometerThread(mag_info.type);
|
||
}
|
||
#endif
|
||
|
||
#if defined(HAS_NEOPIXEL) || defined(UNPHONE) || defined(RGBLED_RED)
|
||
ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE);
|
||
#elif !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
||
if (rgb_found.type != ScanI2C::DeviceType::NONE) {
|
||
ambientLightingThread = new AmbientLightingThread(rgb_found.type);
|
||
}
|
||
#endif
|
||
#endif
|
||
|
||
#ifdef HAS_DRV2605
|
||
#if defined(PIN_DRV_EN)
|
||
pinMode(PIN_DRV_EN, OUTPUT);
|
||
digitalWrite(PIN_DRV_EN, HIGH);
|
||
delay(10);
|
||
#endif
|
||
drv.begin();
|
||
|
||
// Bits Field Value Meaning
|
||
// 7 N_ERM_LRA 1 LRA mode (vs 0 = ERM)
|
||
// 6:4 FB_BRAKE_FACTOR 3 4× brake factor
|
||
// 3:2 LOOP_GAIN 1 medium loop gain
|
||
// 1:0 BEMF_GAIN 2 back-EMF gain
|
||
|
||
#if defined(DRV2605_USE_LRA)
|
||
drv.writeRegister8(DRV2605_REG_FEEDBACK, 0xB6);
|
||
#endif
|
||
|
||
drv.selectLibrary(1);
|
||
// I2C trigger by sending 'go' command
|
||
drv.setMode(DRV2605_MODE_INTTRIG);
|
||
#endif
|
||
|
||
// Init our SPI controller (must be before screen and lora)
|
||
#ifdef ARCH_RP2040
|
||
#ifdef HW_SPI1_DEVICE
|
||
SPI1.setSCK(LORA_SCK);
|
||
SPI1.setTX(LORA_MOSI);
|
||
SPI1.setRX(LORA_MISO);
|
||
pinMode(LORA_CS, OUTPUT);
|
||
digitalWrite(LORA_CS, HIGH);
|
||
SPI1.begin(false);
|
||
#else // HW_SPI1_DEVICE
|
||
SPI.setSCK(LORA_SCK);
|
||
SPI.setTX(LORA_MOSI);
|
||
SPI.setRX(LORA_MISO);
|
||
SPI.begin(false);
|
||
#endif // HW_SPI1_DEVICE
|
||
#elif ARCH_PORTDUINO
|
||
if (portduino_config.lora_spi_dev != "ch341") {
|
||
SPI.begin();
|
||
}
|
||
#elif !defined(ARCH_ESP32) // ARCH_RP2040
|
||
#if defined(RAK3401) || defined(RAK13302)
|
||
pinMode(WB_IO2, OUTPUT);
|
||
digitalWrite(WB_IO2, HIGH);
|
||
SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI);
|
||
SPI1.begin();
|
||
#else
|
||
SPI.begin();
|
||
#endif
|
||
#else
|
||
// ESP32
|
||
#if defined(HW_SPI1_DEVICE)
|
||
SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
|
||
LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
|
||
SPI1.setFrequency(4000000);
|
||
#else
|
||
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
|
||
LOG_DEBUG("SPI.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
|
||
SPI.setFrequency(4000000);
|
||
#endif
|
||
#endif
|
||
|
||
// Initialize the screen first so we can show the logo while we start up everything else.
|
||
#if HAS_SCREEN
|
||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||
|
||
#if defined(HAS_SPI_TFT) || defined(USE_EINK) || defined(USE_SPISSD1306)
|
||
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
|
||
#elif defined(ARCH_PORTDUINO)
|
||
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) &&
|
||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
|
||
}
|
||
#else
|
||
if (screen_found.port != ScanI2C::I2CPort::NO_I2C)
|
||
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
|
||
#endif
|
||
}
|
||
#endif // HAS_SCREEN
|
||
|
||
// TODO Remove magic string
|
||
// setup TZ prior to time actions.
|
||
#if !MESHTASTIC_EXCLUDE_TZ
|
||
LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string
|
||
if (*config.device.tzdef && config.device.tzdef[0] != 0) {
|
||
LOG_DEBUG("Saved TZ: %s ", config.device.tzdef);
|
||
setenv("TZ", config.device.tzdef, 1);
|
||
} else {
|
||
if (strncmp((const char *)slipstreamTZString, "tzpl", 4) == 0) {
|
||
setenv("TZ", "GMT0", 1);
|
||
} else {
|
||
setenv("TZ", (const char *)slipstreamTZString, 1);
|
||
strcpy(config.device.tzdef, (const char *)slipstreamTZString);
|
||
}
|
||
}
|
||
tzset();
|
||
LOG_DEBUG("Set Timezone to %s", getenv("TZ"));
|
||
#endif
|
||
|
||
readFromRTC(); // read the main CPU RTC at first (in case we can't get GPS time)
|
||
|
||
#if !MESHTASTIC_EXCLUDE_GPS
|
||
// If we're taking on the repeater role, ignore GPS
|
||
#ifdef SENSOR_GPS_CONFLICT
|
||
if (sensor_detected == false) {
|
||
#endif
|
||
if (HAS_GPS) {
|
||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
|
||
gps = GPS::createGps();
|
||
if (gps) {
|
||
gpsStatus->observe(&gps->newStatus);
|
||
|
||
// If lora region is unset, disable the gps thread
|
||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET &&
|
||
config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||
gps->disable();
|
||
}
|
||
} else {
|
||
LOG_DEBUG("Run without GPS");
|
||
}
|
||
}
|
||
}
|
||
#ifdef SENSOR_GPS_CONFLICT
|
||
}
|
||
#endif
|
||
|
||
#endif
|
||
|
||
nodeStatus->observe(&nodeDB->newStatus);
|
||
|
||
#ifdef HAS_I2S
|
||
LOG_DEBUG("Start audio thread");
|
||
audioThread = new AudioThread();
|
||
#endif
|
||
|
||
#ifdef HAS_UDP_MULTICAST
|
||
LOG_DEBUG("Start multicast thread");
|
||
udpHandler = new UdpMulticastHandler();
|
||
#ifdef ARCH_PORTDUINO
|
||
// FIXME: portduino does not ever call onNetworkConnected so call it here because I don't know what happen if I call
|
||
// onNetworkConnected there
|
||
if (config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) {
|
||
udpHandler->start();
|
||
}
|
||
#endif
|
||
#endif
|
||
service = new MeshService();
|
||
service->init();
|
||
|
||
// Set osk_found for trackball/encoder devices BEFORE setupModules so CannedMessageModule can detect it
|
||
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
|
||
#ifndef HAS_PHYSICAL_KEYBOARD
|
||
osk_found = true;
|
||
#endif
|
||
#endif
|
||
|
||
// Now that the mesh service is created, create any modules
|
||
setupModules();
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
// Inform modules about I2C devices
|
||
ScanI2CCompleted(i2cScanner.get());
|
||
i2cScanner.reset();
|
||
#endif
|
||
|
||
#if !defined(MESHTASTIC_EXCLUDE_PKI)
|
||
// warn the user about a low entropy key
|
||
if (nodeDB->keyIsLowEntropy && !nodeDB->hasWarned) {
|
||
LOG_WARN(LOW_ENTROPY_WARNING);
|
||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||
cn->time = getValidTime(RTCQualityFromNet);
|
||
sprintf(cn->message, LOW_ENTROPY_WARNING);
|
||
service->sendClientNotification(cn);
|
||
nodeDB->hasWarned = true;
|
||
}
|
||
#endif
|
||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||
if (inputBroker)
|
||
inputBroker->Init();
|
||
#endif
|
||
|
||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||
// After modules are setup, so we can observe modules
|
||
setupNicheGraphics();
|
||
#endif
|
||
|
||
// Do this after service.init (because that clears error_code)
|
||
#ifdef HAS_PMU
|
||
if (!pmu_found)
|
||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_AXP192); // Record a hardware fault for missing hardware
|
||
#endif
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
// Don't call screen setup until after nodedb is setup (because we need
|
||
// the current region name)
|
||
#if defined(HAS_SPI_TFT) || defined(USE_EINK) || defined(USE_SPISSD1306)
|
||
if (screen)
|
||
screen->setup();
|
||
#elif defined(ARCH_PORTDUINO)
|
||
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) &&
|
||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||
screen->setup();
|
||
}
|
||
#else
|
||
if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen)
|
||
screen->setup();
|
||
#endif
|
||
#endif
|
||
|
||
auto rIf = initLoRa();
|
||
|
||
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
|
||
|
||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||
mqttInit();
|
||
#endif
|
||
|
||
#ifdef RF95_FAN_EN
|
||
// Ability to disable FAN if PIN has been set with RF95_FAN_EN.
|
||
// Make sure LoRa has been started before disabling FAN.
|
||
if (config.lora.pa_fan_disabled)
|
||
digitalWrite(RF95_FAN_EN, LOW ^ 0);
|
||
#endif
|
||
|
||
#ifndef ARCH_PORTDUINO
|
||
|
||
// Initialize Wifi
|
||
#if HAS_WIFI
|
||
initWifi();
|
||
#endif
|
||
|
||
#if HAS_ETHERNET
|
||
// Initialize Ethernet
|
||
initEthernet();
|
||
#endif
|
||
#endif
|
||
|
||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
|
||
// Start web server thread.
|
||
webServerThread = new WebServerThread();
|
||
#endif
|
||
|
||
#ifdef ARCH_PORTDUINO
|
||
#if __has_include(<ulfius.h>)
|
||
if (portduino_config.webserverport != -1) {
|
||
piwebServerThread = new PiWebServerThread();
|
||
std::atexit([] { delete piwebServerThread; });
|
||
}
|
||
#endif
|
||
initApiServer(TCPPort);
|
||
#endif
|
||
|
||
// Start airtime logger thread.
|
||
airTime = new AirTime();
|
||
|
||
if (!rIf)
|
||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO);
|
||
else {
|
||
#ifndef ARCH_PORTDUINO_WASM
|
||
// Log bit rate to debug output
|
||
LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) /
|
||
(float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) *
|
||
1000);
|
||
#endif
|
||
|
||
router->addInterface(std::move(rIf));
|
||
}
|
||
|
||
// This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values
|
||
PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking from SDS
|
||
powerFSMthread = new PowerFSMThread();
|
||
|
||
#if !HAS_TFT
|
||
setCPUFast(false); // 80MHz is fine for our slow peripherals
|
||
#endif
|
||
|
||
#ifdef ARDUINO_ARCH_ESP32
|
||
LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap());
|
||
LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram());
|
||
#endif
|
||
|
||
// We manually run this to update the NodeStatus
|
||
nodeDB->notifyObservers(true);
|
||
}
|
||
|
||
#endif
|
||
uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes)
|
||
uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client)
|
||
bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for OTA handoff)
|
||
|
||
#if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL)
|
||
volatile bool lockdownReloadPending; // see main.h - deferred NodeDB reload after lockdown unlock
|
||
volatile bool lockdownDisablePending; // see main.h - deferred decrypt-revert after lockdown disable
|
||
#endif
|
||
|
||
// If a thread does something that might need for it to be rescheduled ASAP it can set this flag
|
||
// This will suppress the current delay and instead try to run ASAP.
|
||
bool runASAP;
|
||
|
||
// TODO find better home than main.cpp
|
||
extern meshtastic_DeviceMetadata getDeviceMetadata()
|
||
{
|
||
meshtastic_DeviceMetadata deviceMetadata;
|
||
strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version));
|
||
deviceMetadata.device_state_version = DEVICESTATE_CUR_VER;
|
||
deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN;
|
||
deviceMetadata.hasBluetooth = HAS_BLUETOOTH;
|
||
deviceMetadata.hasWifi = HAS_WIFI;
|
||
deviceMetadata.hasEthernet = HAS_ETHERNET;
|
||
deviceMetadata.role = config.device.role;
|
||
deviceMetadata.position_flags = config.position.position_flags;
|
||
deviceMetadata.hw_model = HW_VENDOR;
|
||
deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled;
|
||
deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE;
|
||
#if MESHTASTIC_EXCLUDE_REMOTEHARDWARE
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG;
|
||
#endif
|
||
#if MESHTASTIC_EXCLUDE_AUDIO
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG;
|
||
#endif
|
||
// Option to explicitly include canned messages for edge cases, e.g. niche graphics
|
||
#if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG;
|
||
#endif
|
||
#if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG;
|
||
#endif
|
||
// Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts
|
||
// We'll have to macro guard against those targets potentially
|
||
#if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_DETECTIONSENSOR
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_DETECTIONSENSOR_CONFIG;
|
||
#endif
|
||
// If we don't have any GPIO and we don't have GPS OR we don't want too - no purpose in having serial config
|
||
#if NO_EXT_GPIO && NO_GPS || MESHTASTIC_EXCLUDE_SERIAL
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG;
|
||
#endif
|
||
#ifndef ARCH_ESP32
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG;
|
||
#endif
|
||
#if !defined(HAS_RGB_LED) && !RAK_4631
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG;
|
||
#endif
|
||
|
||
// No bluetooth on these targets (yet):
|
||
// Pico W / 2W may get it at some point
|
||
// Portduino and ESP32-C6 are excluded because we don't have a working bluetooth stacks integrated yet.
|
||
#if defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_BLUETOOTH_CONFIG;
|
||
#endif
|
||
|
||
#if defined(ARCH_NRF52) && !HAS_ETHERNET // nrf52 doesn't have network unless it's a RAK ethernet gateway currently
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on nRF52
|
||
#elif defined(ARCH_RP2040) && !HAS_WIFI && !HAS_ETHERNET
|
||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on RP2040
|
||
#endif
|
||
|
||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||
deviceMetadata.hasPKC = true;
|
||
#endif
|
||
return deviceMetadata;
|
||
}
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
void scannerToSensorsMap(const std::unique_ptr<ScanI2CTwoWire> &i2cScanner, ScanI2C::DeviceType deviceType,
|
||
meshtastic_TelemetrySensorType sensorType)
|
||
{
|
||
auto found = i2cScanner->find(deviceType);
|
||
if (found.type != ScanI2C::DeviceType::NONE) {
|
||
nodeTelemetrySensorsMap[sensorType].first = found.address.address;
|
||
nodeTelemetrySensorsMap[sensorType].second = i2cScanner->fetchI2CBus(found.address);
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#ifndef PIO_UNIT_TESTING
|
||
void loop()
|
||
{
|
||
runASAP = false;
|
||
|
||
#if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL)
|
||
if (lockdownDisablePending) {
|
||
lockdownDisablePending = false;
|
||
LOG_INFO("Lockdown: disabling - reverting encrypted storage to plaintext");
|
||
if (nodeDB->disableLockdownToPlaintext()) {
|
||
LOG_INFO("Lockdown: disabled, rebooting into normal mode");
|
||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_DISABLED, "", 0, 0, 0);
|
||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||
} else {
|
||
// Revert failed mid-way (a file couldn't be decrypted/rewritten).
|
||
// The DEK file is still present (it's deleted last), so the device
|
||
// stays in lockdown and the operator can retry disable. Surface
|
||
// the failure rather than leaving the client hanging.
|
||
LOG_ERROR("Lockdown: disable revert failed - device remains in lockdown");
|
||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_LOCKED, "disable_failed", 0, 0, 0);
|
||
}
|
||
}
|
||
|
||
if (lockdownReloadPending) {
|
||
lockdownReloadPending = false;
|
||
LOG_INFO("Lockdown: reloading config from disk after unlock");
|
||
bool reloadOk = nodeDB->reloadFromDisk();
|
||
if (!reloadOk) {
|
||
// Storage decrypt/decode failed during reload. Treat as
|
||
// unrecoverable for this boot: lock storage, revoke any
|
||
// auth that managed to slip through (defense in depth - the
|
||
// cold-unlock path doesn't authorize until completion, but
|
||
// a concurrent re-verify-path call from another connection
|
||
// might have), and notify clients. Storage will be locked
|
||
// on next boot anyway; deferring to the user-visible
|
||
// notification path is sufficient for now.
|
||
LOG_ERROR("Lockdown: reload failed - locking and notifying clients");
|
||
EncryptedStorage::lockNow();
|
||
PhoneAPI::revokeAllAuth();
|
||
}
|
||
PhoneAPI::completePendingUnlocks(reloadOk);
|
||
}
|
||
|
||
// Periodic session-expiry check. Cheap - millis() comparison. Don't
|
||
// hammer it every loop tick; once a second is plenty.
|
||
static uint32_t lastSessionCheckMs = 0;
|
||
if (millis() - lastSessionCheckMs > 1000) {
|
||
lastSessionCheckMs = millis();
|
||
if (rebootAtMsec == 0 && EncryptedStorage::isUnlocked() && EncryptedStorage::isSessionExpired()) {
|
||
// The session expired. Two paths:
|
||
// 1. Budget remains (bootsRemaining > 0): decrement the
|
||
// on-flash boot count in place, revoke per-connection
|
||
// auth, re-engage screen redaction, re-arm the uptime
|
||
// timer - all WITHOUT rebooting. Storage stays unlocked
|
||
// so the mesh keeps routing. Clients must re-authenticate
|
||
// to see content again. The decrement is what enforces
|
||
// the rollback ceiling - bootsRemaining ticks down
|
||
// monotonically whether the device reboots or not.
|
||
// 2. Budget exhausted (bootsRemaining == 0): no more
|
||
// sessions to grant. Hard lock (token deleted, DEK
|
||
// zeroed) and reboot. Operator must re-enter passphrase.
|
||
if (EncryptedStorage::getBootsRemaining() == 0) {
|
||
LOG_WARN("Lockdown: session limit reached and boot budget exhausted, locking and rebooting");
|
||
EncryptedStorage::lockNow();
|
||
PhoneAPI::revokeAllAuth();
|
||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_LOCKED, "session_budget_exhausted", 0, 0, 0);
|
||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||
} else {
|
||
uint8_t newBoots = EncryptedStorage::consumeSessionBoot();
|
||
LOG_WARN("Lockdown: session expired, rolled to next budget slot (boots=%u remaining)", newBoots);
|
||
PhoneAPI::revokeAllAuth();
|
||
meshtastic_security::lockScreen();
|
||
// Signal clients that they need to re-auth on this
|
||
// connection. Storage is still unlocked (DEK in RAM,
|
||
// mesh keeps routing) but per-connection auth is gone.
|
||
// Reusing the LOCKED(needs_auth) post-config emission
|
||
// pattern so existing clients don't need a new state.
|
||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_LOCKED, "needs_auth", newBoots,
|
||
EncryptedStorage::getValidUntilEpoch(), 0);
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#ifdef ARCH_ESP32
|
||
esp32Loop();
|
||
#endif
|
||
#ifdef ARCH_NRF52
|
||
nrf52Loop();
|
||
#endif
|
||
#ifdef ARCH_NRF54L15
|
||
nrf54l15Loop();
|
||
#endif
|
||
#ifdef ARCH_RP2040
|
||
rp2040Loop();
|
||
#endif
|
||
power->powerCommandsCheck();
|
||
|
||
if (RadioLibInterface::instance != nullptr) {
|
||
static uint32_t lastRadioMissedIrqPoll;
|
||
if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) {
|
||
lastRadioMissedIrqPoll = millis();
|
||
RadioLibInterface::instance->pollMissedIrqs();
|
||
}
|
||
|
||
// Periodic AGC reset - warm sleep + recalibrate to prevent stuck AGC gain
|
||
static uint32_t lastAgcReset;
|
||
if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) {
|
||
lastAgcReset = millis();
|
||
RadioLibInterface::instance->resetAGC();
|
||
}
|
||
}
|
||
|
||
#ifdef DEBUG_STACK
|
||
static uint32_t lastPrint = 0;
|
||
if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) {
|
||
lastPrint = millis();
|
||
meshtastic::printThreadInfo("main");
|
||
}
|
||
#endif
|
||
|
||
service->loop();
|
||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
|
||
if (inputBroker)
|
||
inputBroker->processInputEventQueue();
|
||
#endif
|
||
#if ARCH_PORTDUINO
|
||
if (portduino_config.lora_spi_dev == "ch341" && ch341Hal != nullptr) {
|
||
ch341Hal->checkError();
|
||
}
|
||
if (portduino_status.LoRa_in_error && rebootAtMsec == 0) {
|
||
LOG_ERROR("LoRa in error detected, attempting to recover");
|
||
router->addInterface(nullptr);
|
||
if (portduino_config.lora_spi_dev == "ch341") {
|
||
if (ch341Hal != nullptr) {
|
||
delete ch341Hal;
|
||
ch341Hal = nullptr;
|
||
sleep(3);
|
||
}
|
||
try {
|
||
ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid,
|
||
portduino_config.lora_usb_pid);
|
||
} catch (std::exception &e) {
|
||
std::cerr << e.what() << std::endl;
|
||
std::cerr << "Could not initialize CH341 device!" << std::endl;
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
}
|
||
auto rIf = initLoRa();
|
||
if (rIf) {
|
||
router->addInterface(std::move(rIf));
|
||
portduino_status.LoRa_in_error = false;
|
||
} else {
|
||
LOG_WARN("Reconfigure failed, rebooting");
|
||
if (screen) {
|
||
screen->showSimpleBanner("Rebooting...");
|
||
}
|
||
rebootAtMsec = millis() + 25;
|
||
}
|
||
}
|
||
#if HAS_TFT
|
||
if (screen && portduino_config.displayPanel == x11 &&
|
||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||
auto dispdev = screen->getDisplayDevice();
|
||
if (dispdev)
|
||
static_cast<TFTDisplay *>(dispdev)->sdlLoop();
|
||
}
|
||
#endif
|
||
#endif
|
||
#if (HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)) && ENABLE_MESSAGE_PERSISTENCE
|
||
messageStoreAutosaveTick();
|
||
#endif
|
||
long delayMsec = mainController.runOrDelay();
|
||
|
||
// We want to sleep as long as possible here - because it saves power
|
||
if (!runASAP && loopCanSleep()) {
|
||
#ifdef DEBUG_LOOP_TIMING
|
||
LOG_DEBUG("main loop delay: %d", delayMsec);
|
||
#endif
|
||
#ifdef ARCH_PORTDUINO_WASM
|
||
// Single-threaded wasm: mainDelay's InterruptableDelay is a pthread
|
||
// cond/mutex semaphore that no other thread can ever give(), and
|
||
// emscripten's single-threaded pthread_cond_timedwait busy-spins. Suspend
|
||
// cooperatively via Asyncify instead, capping idle sleep so the per-tick
|
||
// IRQ poll latency stays bounded (RX/TX-done is detected by polling).
|
||
emscripten_sleep(delayMsec > 50 ? 50 : delayMsec);
|
||
#else
|
||
mainDelay.delay(delayMsec);
|
||
#endif
|
||
}
|
||
}
|
||
#endif
|