#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_GPS
#include "GPS.h"
#endif
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "PowerMon.h"
#include "ReliableRouter.h"
#include "airtime.h"
#include "buzz.h"

#include "FSCommon.h"
#include "Led.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/RAKled.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>

#ifdef ELECROW_ThinkNode_M5
PCA9557 io(0x18, &Wire);
#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

#if HAS_WIFI || defined(USE_WS5500)
#include "mesh/api/WiFiServerAPI.h"
#include "mesh/wifi/WiFiAPClient.h"
#endif

#if HAS_ETHERNET && !defined(USE_WS5500)
#include "mesh/api/ethServerAPI.h"
#include "mesh/eth/ethClient.h"
#endif

#if !MESHTASTIC_EXCLUDE_MQTT
#include "mqtt/MQTT.h"
#endif

#include "LLCC68Interface.h"
#include "LR1110Interface.h"
#include "LR1120Interface.h"
#include "LR1121Interface.h"
#include "RF95Interface.h"
#include "SX1262Interface.h"
#include "SX1268Interface.h"
#include "SX1280Interface.h"
#include "detect/LoRaRadioType.h"

#ifdef ARCH_STM32WL
#include "STM32WLE5JCInterface.h"
#endif

#if defined(ARCH_PORTDUINO)
#include "platform/portduino/SimRadio.h"
#endif

#ifdef ARCH_PORTDUINO
#include "linux/LinuxHardwareI2C.h"
#include "mesh/raspihttp/PiWebServer.h"
#include "platform/portduino/PortduinoGlue.h"
#include "platform/portduino/USBHal.h"
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
#endif

#if HAS_BUTTON || defined(ARCH_PORTDUINO)
#include "input/ButtonThread.h"

#if defined(BUTTON_PIN_TOUCH)
ButtonThread *TouchButtonThread = nullptr;
#endif

#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO)
ButtonThread *UserButtonThread = nullptr;
#endif

#if defined(ALT_BUTTON_PIN)
ButtonThread *BackButtonThread = nullptr;
#endif

#if defined(CANCEL_BUTTON_PIN)
ButtonThread *CancelButtonThread = nullptr;
#endif

#endif

#include "AmbientLightingThread.h"
#include "PowerFSMThread.h"

#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
#include "motion/AccelerometerThread.h"
AccelerometerThread *accelerometerThread = nullptr;
#endif

#ifdef HAS_I2S
#include "AudioThread.h"
AudioThread *audioThread = nullptr;
#endif

#ifdef USE_XL9555
#include "ExtensionIOXL9555.hpp"
ExtensionIOXL9555 io;
#endif

#ifdef HAS_TCA9535_BUTTON
#include "input/TCA9535ButtonThread.h"
TCA9535ButtonThread *tca9535ButtonThread = nullptr;
#endif

// ---------------------------------------------------------------------------
// 自定义 HAL：拦截 TCA9535 虚拟引脚的 GPIO 操作，转发到 I²C
// 用于 LoRa RST 通过 TCA9535 P1.4 控制
// ---------------------------------------------------------------------------
#ifdef TCA9535_LORA_RST_VIRTUAL_PIN
#include <RadioLib.h>

class TCA9535GpioHal : public LockingArduinoHal
{
  public:
    TCA9535GpioHal(SPIClass &spi, SPISettings spiSettings) : LockingArduinoHal(spi, spiSettings) {}

    void pinMode(uint32_t pin, uint32_t mode) override
    {
        if (pin == TCA9535_LORA_RST_VIRTUAL_PIN) {
            // P1.4 已由 TCA9535 驱动配置为输出，无需重复操作
            return;
        }
        LockingArduinoHal::pinMode(pin, mode);
    }

    void digitalWrite(uint32_t pin, uint32_t value) override
    {
        if (pin == TCA9535_LORA_RST_VIRTUAL_PIN) {
            tca9535LoraReset(value == GpioLevelHigh);
            return;
        }
        LockingArduinoHal::digitalWrite(pin, value);
    }
};
#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 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;

#if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
Adafruit_DRV2605 drv;
#endif

// Global LoRa radio type
LoRaRadioType radioType = NO_RADIO;

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;
}

static int32_t ledBlinker()
{
    // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if
    // config.device.led_heartbeat_disabled is changed
    if (config.device.led_heartbeat_disabled)
        return 1000;

    static bool ledOn;
    ledOn ^= 1;

    ledBlink.set(ledOn);

    // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that
    return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000);
}

uint32_t timeLastPowered = 0;

static Periodic *ledPeriodic;
static OSThread *powerFSMthread;
static OSThread *ambientLightingThread;

RadioInterface *rIf = NULL;
#ifdef ARCH_PORTDUINO
RadioLibHal *RadioLibHAL = NULL;
#endif

/**
 * 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() {}

/**
 * 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()
{
// 适用于 R1_NEO 硬件：初始化 DCDC 和 NRF 模块电源使能引脚
#if defined(R1_NEO)
    // 将 DCDC 电源保持引脚设置为输出模式，用于后续控制 DCDC 电源使能
    pinMode(DCDC_EN_HOLD, OUTPUT);
    // 拉高 DCDC 电源保持引脚，使能 DCDC 电源，确保核心电路供电正常
    digitalWrite(DCDC_EN_HOLD, HIGH);
    // 将 NRF 模块电源使能引脚设置为输出模式
    pinMode(NRF_ON, OUTPUT);
    // 拉高 NRF 电源使能引脚，开启 NRF 模块电源
    digitalWrite(NRF_ON, HIGH);
#endif

// 适用于带 PIN_POWER_EN 引脚的硬件：初始化通用电源使能引脚
#if defined(PIN_POWER_EN)
    // 将通用电源使能引脚设置为输出模式
    pinMode(PIN_POWER_EN, OUTPUT);
    // 拉高电源使能引脚，开启外设电源
    digitalWrite(PIN_POWER_EN, HIGH);
#endif

// 适用于 ELECROW ThinkNode M5 硬件：初始化 I2C 总线及 PCA9557 IO 扩展器电源引脚
#if defined(ELECROW_ThinkNode_M5)
    // 初始化 I2C 总线，SDA=48，SCL=47
    Wire.begin(48, 47);
    // 设置 PCA9557 的电子墨水屏使能引脚为输出模式
    io.pinMode(PCA_PIN_EINK_EN, OUTPUT);
    // 设置 PCA9557 的电源使能引脚为输出模式
    io.pinMode(PCA_PIN_POWER_EN, OUTPUT);
    // 拉高 PCA9557 电源使能引脚，开启外设电源
    io.digitalWrite(PCA_PIN_POWER_EN, HIGH);
    // io.pinMode(C2_PIN, OUTPUT);
#endif

// 适用于带 LED_POWER 引脚的硬件：初始化电源指示灯
#ifdef LED_POWER
    // 将电源指示灯引脚设置为输出模式
    pinMode(LED_POWER, OUTPUT);
    // 设置电源指示灯为点亮状态（LED_STATE_ON 为点亮电平）
    digitalWrite(LED_POWER, LED_STATE_ON);
#endif

// 适用于带 USER_LED 引脚的硬件：初始化用户指示灯
#ifdef USER_LED
    // 将用户 LED 引脚设置为输出模式
    pinMode(USER_LED, OUTPUT);
    // 设置用户 LED 状态（HIGH 与 LED_STATE_ON 异或，适配 LED 电平逻辑）
    digitalWrite(USER_LED, HIGH ^ LED_STATE_ON);
#endif

// 适用于带 WIFI_LED 引脚的硬件：初始化 WiFi 指示灯
#ifdef WIFI_LED
    // 将 WiFi 指示灯引脚设置为输出模式
    pinMode(WIFI_LED, OUTPUT);
    // 初始设置 WiFi 指示灯为熄灭状态
    digitalWrite(WIFI_LED, LOW);
#endif

// 适用于带 BLE_LED 引脚的硬件：初始化蓝牙指示灯
#ifdef BLE_LED
    // 将蓝牙指示灯引脚设置为输出模式
    pinMode(BLE_LED, OUTPUT);
#ifdef BLE_LED_INVERTED
    // 若蓝牙 LED 为低电平点亮（反逻辑），则拉高引脚熄灭
    digitalWrite(BLE_LED, HIGH);
#else
    // 若蓝牙 LED 为高电平点亮，则拉低引脚熄灭
    digitalWrite(BLE_LED, LOW);
#endif
#endif

// 适用于 T-Deck、T-Deck Pro、T-LoRa Pager 等硬件：初始化外设电源、SPI 片选引脚及 IO 扩展器
#if defined(T_DECK)
    // GPIO10 控制所有外设电源，启动后立即开启避免低电压导致 ESP32 复位
    // GPIO10 manages all peripheral power supplies
    // Turn on peripheral power immediately after MUC starts.
    // If some boards are turned on late, ESP32 will reset due to low voltage.
    // 外设包括：ESP32-C3(键盘)、MAX98357A(音频功放)、TF卡、显示屏背光、轨迹球、解码器
    // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
    // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
    // 将键盘电源使能引脚设置为输出模式
    pinMode(KB_POWERON, OUTPUT);
    // 拉高键盘电源引脚，开启外设电源
    digitalWrite(KB_POWERON, HIGH);
    // T-Deck 的 TFT、SD、LoRa 共用同一 SPI 总线，需提前初始化所有片选引脚避免通信冲突
    // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
    // We need to initialize all CS pins in advance otherwise there will be SPI communication issues
    // e.g. when detecting the SD card
    // 初始化 LoRa 片选引脚为输出并拉高（默认不选中）
    pinMode(LORA_CS, OUTPUT);
    digitalWrite(LORA_CS, HIGH);
    // 初始化 SD 卡片选引脚为输出并拉高（默认不选中）
    pinMode(SDCARD_CS, OUTPUT);
    digitalWrite(SDCARD_CS, HIGH);
    // 初始化 TFT 屏幕片选引脚为输出并拉高（默认不选中）
    pinMode(TFT_CS, OUTPUT);
    digitalWrite(TFT_CS, HIGH);
    // 等待外设电源稳定
    delay(100);
#elif defined(T_DECK_PRO)
    // T-Deck Pro 硬件：初始化 LoRa、SD 卡、电子墨水屏的片选引脚并拉高
    pinMode(LORA_EN, OUTPUT);
    digitalWrite(LORA_EN, HIGH);
    pinMode(LORA_CS, OUTPUT);
    digitalWrite(LORA_CS, HIGH);
    pinMode(SDCARD_CS, OUTPUT);
    digitalWrite(SDCARD_CS, HIGH);
    pinMode(PIN_EINK_CS, OUTPUT);
    digitalWrite(PIN_EINK_CS, HIGH);
#elif defined(T_LORA_PAGER)
    // T-LoRa Pager 硬件：初始化 SPI 片选引脚、键盘中断引脚及 XL9555 IO 扩展器
    pinMode(LORA_CS, OUTPUT);
    digitalWrite(LORA_CS, HIGH);
    pinMode(SDCARD_CS, OUTPUT);
    digitalWrite(SDCARD_CS, HIGH);
    pinMode(TFT_CS, OUTPUT);
    digitalWrite(TFT_CS, HIGH);
    // 设置键盘中断引脚为上拉输入模式
    pinMode(KB_INT, INPUT_PULLUP);
    // 初始化 XL9555 IO 扩展器，使用默认 Wire 总线，地址为 XL9555_SLAVE_ADDRESS0
    // io expander
    io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
    // 设置驱动使能引脚为输出并拉高，开启驱动
    io.pinMode(EXPANDS_DRV_EN, OUTPUT);
    io.digitalWrite(EXPANDS_DRV_EN, HIGH);
    // 设置音频功放使能引脚为输出并拉低，关闭功放（默认不开启）
    io.pinMode(EXPANDS_AMP_EN, OUTPUT);
    io.digitalWrite(EXPANDS_AMP_EN, LOW);
    // 设置 LoRa 使能引脚为输出并拉高，开启 LoRa 模块
    io.pinMode(EXPANDS_LORA_EN, OUTPUT);
    io.digitalWrite(EXPANDS_LORA_EN, HIGH);
    // 设置 GPS 使能引脚为输出并拉高，开启 GPS 模块
    io.pinMode(EXPANDS_GPS_EN, OUTPUT);
    io.digitalWrite(EXPANDS_GPS_EN, HIGH);
    // 设置键盘使能引脚为输出并拉高，开启键盘
    io.pinMode(EXPANDS_KB_EN, OUTPUT);
    io.digitalWrite(EXPANDS_KB_EN, HIGH);
    // 设置 SD 卡使能引脚为输出并拉高，开启 SD 卡
    io.pinMode(EXPANDS_SD_EN, OUTPUT);
    io.digitalWrite(EXPANDS_SD_EN, HIGH);
    // 设置 GPIO 扩展使能引脚为输出并拉高，开启扩展 GPIO
    io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
    io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
    // 设置 SD 卡上拉使能引脚为输入模式
    io.pinMode(EXPANDS_SD_PULLEN, INPUT);
#endif
    // 标记并发框架已完成 setup 初始化
    concurrency::hasBeenSetup = true;
// 根据平台配置 SPI 通信参数：时钟速度、位顺序（MSB 优先）、SPI 模式
#if ARCH_PORTDUINO
    // Portduino 平台：使用用户配置的 SPI 速度
    SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
#else
    // 其他平台：默认 SPI 时钟 4MHz，MSB 优先，模式0
    SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0);
#endif

    // 设置屏幕默认型号为 OLED_AUTO（自动检测），默认分辨率为 128x64
    meshtastic_Config_DisplayConfig_OledType screen_model =
        meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
    OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64;

// 适用于启用 SEGGER RTT 调试输出的硬件：配置 RTT 上行缓冲区
#ifdef USE_SEGGER
    // 设置 RTT 模式：非阻塞裁剪模式（FIFO 满时丢弃数据）
    auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM;
#ifdef NRF52840_XXAA
    // NRF52840 内存较大，设置 RTT 缓冲区大小为 4096 字节
    auto buflen = 4096; // this board has a fair amount of ram
#else
    // 其他板卡内存较小，设置 RTT 缓冲区大小为 256 字节
    auto buflen = 256; // this board has a fair amount of ram
#endif
    // 配置 SEGGER RTT 上行缓冲区（用于调试输出）
    SEGGER_RTT_ConfigUpBuffer(SEGGER_STDOUT_CH, NULL, NULL, buflen, mode);
#endif

// 适用于启用调试串口（DEBUG_PORT）的硬件：初始化串口控制台
#ifdef DEBUG_PORT
    // 初始化调试串口（设置波特率等）并启动 mesh 控制台
    consoleInit(); // Set serial baud rate and init our mesh console
#endif

// 适用于 UNPHONE 硬件：打印存储信息
#ifdef UNPHONE
    // 打印 UNPHONE 的存储状态信息
    unphone.printStore();
#endif

// 适用于 Portduino 平台：初始化系统时间并设置 RTC
#if ARCH_PORTDUINO
    // 定义时间结构体
    struct timeval tv;
    // 获取当前系统时间（秒级）并赋值
    tv.tv_sec = time(NULL);
    // 微秒级时间设为 0
    tv.tv_usec = 0;
    // 尝试设置 RTC 时间，质量标记为设备级
    perhapsSetRTC(RTCQualityDevice, &tv);
#endif

    powerMonInit();
    serialSinceMsec = millis();

    LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");

#if defined(DEBUG_MUTE) && defined(DEBUG_PORT)
    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(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);
#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

#ifdef BUTTON_PIN
#ifdef ARCH_ESP32

#if ESP_ARDUINO_VERSION_MAJOR >= 3
#ifdef BUTTON_NEED_PULLUP
    pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP);
#else
    pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
#endif
#else
    pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
#ifdef BUTTON_NEED_PULLUP
    gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
    delay(10);
#endif
#ifdef BUTTON_NEED_PULLUP2
    gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2);
    delay(10);
#endif
#endif
#endif
#endif

    // 初始化 SPI 总线（配置 SPI 参数并启动）
    initSPI();

    // 初始化 OSThread 并发框架（设置线程调度基础）
    OSThread::setup();

// 适用于 ELECROW ThinkNode M1/M2 硬件：使用自定义 LED 闪烁逻辑（当前注释未启用）
#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
    // The ThinkNodes have their own blink logic
    // ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
#else
    // 其他硬件：创建 LED 周期闪烁线程（默认 1Hz 心跳效果）
    ledPeriodic = new Periodic("Blink", ledBlinker);
#endif

    // 初始化文件系统（挂载 SPI Flash/SD 卡等存储设备）
    fsInit();

// 阶段3：I2C 总线初始化与设备扫描（排除 I2C 功能时跳过）
#if !MESHTASTIC_EXCLUDE_I2C
// 初始化第二 I2C 总线（Wire1，适用于有 2 个 I2C 接口的平台）
#if defined(I2C_SDA1) && defined(ARCH_RP2040)
    // RP2040 平台：设置 Wire1 的 SDA/SCL 引脚并启动总线
    Wire1.setSDA(I2C_SDA1);
    Wire1.setSCL(I2C_SCL1);
    Wire1.begin();
#elif defined(I2C_SDA1) && !defined(ARCH_RP2040)
    // 非 RP2040 平台：启动 Wire1 总线并指定 SDA/SCL 引脚
    Wire1.begin(I2C_SDA1, I2C_SCL1);
#elif WIRE_INTERFACES_COUNT == 2
    // 平台有 2 个 I2C 接口：启动默认 Wire1 总线
    Wire1.begin();
#endif

// 初始化主 I2C 总线（Wire）
#if defined(I2C_SDA) && defined(ARCH_RP2040)
    // RP2040 平台：设置主 Wire 的 SDA/SCL 引脚并启动总线
    Wire.setSDA(I2C_SDA);
    Wire.setSCL(I2C_SCL);
    Wire.begin();
#elif defined(I2C_SDA) && !defined(ARCH_RP2040)
    // 非 RP2040 平台：启动主 Wire 总线并指定 SDA/SCL 引脚
    Wire.begin(I2C_SDA, I2C_SCL);
#ifdef HAS_TCA9535_BUTTON
    // TCA9535 电源使能必须在 I2C 初始化后立即拉高，防止用户松开按键后系统掉电
    // TCA9535 POWER_EN 必须在 I²C 初始化完成后立即拉高，否则用户松开按键后
    // MOS 断电，系统在 setup() 中途就会掉电。
    tca9535PowerEn(true);
    LOG_INFO("TCA9535: POWER_EN latched HIGH (early boot)");
#endif
#elif defined(ARCH_PORTDUINO)
    // Portduino 平台：根据配置启动主 I2C 总线
    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 总线的平台：启动默认主 I2C 总线
    Wire.begin();
#endif
#endif

// 适用于 M5STACK_UNITC6L 硬件：初始化 LoRa 片选引脚并启动 C6L 模块
#if defined(M5STACK_UNITC6L)
    // 设置 LoRa 片选引脚为输出模式
    pinMode(LORA_CS, OUTPUT);
    // 拉高片选引脚，默认不选中 LoRa
    digitalWrite(LORA_CS, 1);
    // 初始化 M5STACK_UNITC6L 模块
    c6l_init();
#endif

// 适用于带 LCD 复位引脚的硬件：执行 LCD 复位时序
#ifdef PIN_LCD_RESET
    // FIXME - 后续将该复位逻辑移至更合适的位置，LCD I2C 地址为 0x3F
    // FIXME - move this someplace better, LCD is at address 0x3F
    // 设置 LCD 复位引脚为输出模式
    pinMode(PIN_LCD_RESET, OUTPUT);
    // 拉低复位引脚，触发 LCD 复位
    digitalWrite(PIN_LCD_RESET, 0);
    // 延时 1ms 确保复位生效
    delay(1);
    // 拉高复位引脚，结束复位状态
    digitalWrite(PIN_LCD_RESET, 1);
    // 延时 1ms 等待 LCD 启动
    delay(1);
#endif

// 适用于带空气质量传感器（AQ）设置引脚的硬件：初始化 AQ 传感器电源
#ifdef AQ_SET_PIN
    // RAK-12039 空气质量传感器设置引脚，上电后约 3 秒可在 I2C 检测到，需后续重新扫描
    // RAK-12039 set pin for Air quality sensor. Detectable on I2C after ~3 seconds, so we need to rescan later
    // 设置 AQ 传感器电源引脚为输出模式
    pinMode(AQ_SET_PIN, OUTPUT);
    // 拉高引脚，开启 AQ 传感器电源
    digitalWrite(AQ_SET_PIN, HIGH);
#endif

    // 初始化电源管理单元（PMU，目前仅 T-Beam 等硬件有）
    // 当前仅 T-Beam 等硬件有 PMU
    // Currently only the tbeam has a PMU
    // PMU 初始化必须放在 I2C 扫描之前，因为需要 PMU 为外设供电
    // PMU initialization needs to be placed before i2c scanning
    // 创建 Power 实例（电源管理核心对象）
    power = new Power();
    // 设置电源状态处理器为 powerStatus，用于接收电源状态更新
    power->setStatusHandler(powerStatus);
    // 让 powerStatus 观察 power 的新状态事件
    powerStatus->observe(&power->newStatus);
    // 执行 PMU 初始化（必须在状态处理器安装后，确保接收初始配置通知）
    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
    // 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

    // 检测 I2C 扫描到的第一个屏幕设备，更新屏幕型号
    auto screenInfo = i2cScanner->firstScreen();
    // 记录屏幕设备地址（未找到则为 ADDRESS_NONE）
    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:
            // SH1106 屏幕型号（128x64 OLED）
            screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SH1106;
            break;
        case ScanI2C::DeviceType::SCREEN_SSD1306:
            // SSD1306 屏幕型号（128x64 OLED）
            screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306;
            break;
        case ScanI2C::DeviceType::SCREEN_ST7567:
        case ScanI2C::DeviceType::SCREEN_UNKNOWN:
        default:
            // 未知或 ST7567 屏幕，保持自动检测模式
            screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO;
        }
    }

// 宏定义：从扫描结果更新传感器映射（当前未使用，预留扩展）
#define UPDATE_FROM_SCANNER(FIND_FN)
// 若定义了虚拟键盘，直接标记键盘已找到
#if defined(USE_VIRTUAL_KEYBOARD)
    kb_found = true;
#endif
    // 获取第一个 RTC 设备信息，更新 RTC 地址
    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;
        // 根据键盘型号设置 kb_model 值（用于区分不同键盘型号）
        switch (kb_info.type) {
        case ScanI2C::DeviceType::RAK14004:
            // RAK14004 键盘型号标识（0x02）
            kb_model = 0x02;
            break;
        case ScanI2C::DeviceType::CARDKB:
            // CARDKB 键盘型号标识（0x00）
            kb_model = 0x00;
            break;
        case ScanI2C::DeviceType::TDECKKB:
            // T-Deck 键盘型号标识（0x10）
            // assign an arbitrary value to distinguish from other models
            kb_model = 0x10;
            break;
        case ScanI2C::DeviceType::BBQ10KB:
            // BBQ10 键盘型号标识（0x11）
            // assign an arbitrary value to distinguish from other models
            kb_model = 0x11;
            break;
        case ScanI2C::DeviceType::MPR121KB:
            // MPR121 键盘型号标识（0x37）
            // assign an arbitrary value to distinguish from other models
            kb_model = 0x37;
            break;
        case ScanI2C::DeviceType::TCA8418KB:
            // TCA8418 键盘型号标识（0x84）
            // assign an arbitrary value to distinguish from other models
            kb_model = 0x84;
            break;
        default:
            // 未知键盘型号，默认使用 0x00
            // 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（AXP192/AXP2101）是否存在，更新 pmu_found 标志
    pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101);

    // 获取第一个空气质量传感器（AQI）信息，更新 aqi_found 地址
    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".
 */

// 当前支持两种 RGB LED 检测
#ifdef HAS_RGB_LED
    // 从扫描结果获取 RGB LED 设备信息
    rgb_found = i2cScanner->firstRGBLED();
#endif

#ifdef HAS_TPS65233
    // TPS65233 是卫星调制解调器的电源管理 IC，用于 Dreamcatcher 硬件
    // TPS65233 is a power management IC for satellite modems, used in the Dreamcatcher
    // 此处关闭 LNB 电源（我们使用卫星调制解调器时不使用 LNB）
    // We are switching it off here since we don't use an LNB.
    if (i2cScanner->exists(ScanI2C::DeviceType::TPS65233)) {
        // 写入寄存器 0：关闭 LNB 电源，保持 I2C 控制使能
        Wire.beginTransmission(TPS65233_ADDR);
        Wire.write(0);   // Register 0
        Wire.write(128); // Turn off the LNB power, keep I2C Control enabled
        Wire.endTransmission();
        // 写入寄存器 1：关闭 22kHz 音调发生器
        Wire.beginTransmission(TPS65233_ADDR);
        Wire.write(1); // Register 1
        Wire.write(0); // Turn off Tone Generator 22kHz
        Wire.endTransmission();
    }
#endif

// 非 STM32WL 平台：检测加速度计并获取设备地址
#if !defined(ARCH_STM32WL)
    // 获取第一个加速度计设备信息
    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

    // 将 INA260 传感器映射到遥测传感器类型
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260);
    // 将 INA226 传感器映射到遥测传感器类型
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226);
    // 将 INA219 传感器映射到遥测传感器类型
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219);
    // 将 INA3221 传感器映射到遥测传感器类型
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221);
    // 将 MAX17048 传感器映射到遥测传感器类型
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048);
    // 将 QMC6310 传感器映射到遥测传感器类型
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310);
    // 将 QMI8658 传感器映射到遥测传感器类型
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658);
    // 将 QMC5883L 传感器映射到遥测传感器类型
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I);
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
    scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);

#endif

#ifdef HAS_SDCARD
    setupSDCard();
#endif

    // LED init

#ifdef LED_PIN
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now
#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_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;

#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();

    // fixed screen override?
    if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
        screen_model = config.display.oled;

#if defined(USE_SH1107)
    screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128
    screen_geometry = GEOMETRY_128_128;
#endif

#if defined(USE_SH1107_128_64)
    screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
#endif

#if !MESHTASTIC_EXCLUDE_I2C
#if !defined(ARCH_STM32WL)
    if (acc_info.type != ScanI2C::DeviceType::NONE) {
        accelerometerThread = new AccelerometerThread(acc_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

#if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
    drv.begin();
    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(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) ||       \
    defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) ||              \
    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

    // 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);
#ifdef TCA9535_GPS_HAS_CTRL
                    // 替换 enablePin 为 TCA9535 P1.7（GPS EN），I²C 控制
                    // createGps() 创建了空的 GpioVirtPin，替换为实际的 TCA9535 驱动
                    class GpioTca9535GpsEnPin : public GpioPin {
                      public:
                        void set(bool value) override { tca9535GpsEn(value); }
                    } *tca9535GpsEnPin = new GpioTca9535GpsEnPin();
                    new GpioUnaryTransformer(gps->enablePin, tca9535GpsEnPin);
                    // GPS 已在 TCA9535 init 中上电（P1.7=高），无需额外操作
#endif
                } 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();

    // Now that the mesh service is created, create any modules
    setupModules();

#ifdef HAS_TCA9535_BUTTON
    // TCA9535PWR I²C IO 扩展器按键驱动
    // Wire 已在 I2C 初始化段完成 begin()，inputBroker 已在 setupModules() 中创建
    tca9535ButtonThread = new TCA9535ButtonThread("TCA9535Btn");
    if (!tca9535ButtonThread->init()) {
        LOG_WARN("TCA9535 init failed, disabling button thread");
        delete tca9535ButtonThread;
        tca9535ButtonThread = nullptr;
    } else {
        LOG_INFO("TCA9535 button thread started");
    }
#endif

#if !MESHTASTIC_EXCLUDE_I2C
    // Inform modules about I2C devices
    ScanI2CCompleted(i2cScanner.get());
    i2cScanner.reset();
#endif

    // 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;
    }

// buttons are now inputBroker, so have to come after setupModules
#if HAS_BUTTON
    int pullup_sense = 0;
#ifdef INPUT_PULLUP_SENSE
    // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
#ifdef BUTTON_SENSE_TYPE
    pullup_sense = BUTTON_SENSE_TYPE;
#else
    pullup_sense = INPUT_PULLUP_SENSE;
#endif
#endif
#if defined(ARCH_PORTDUINO)

    if (portduino_config.userButtonPin.enabled) {

        LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
        UserButtonThread = new ButtonThread("UserButton");
        if (screen) {
            ButtonConfig config;
            config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
            config.activeLow = true;
            config.activePullup = true;
            config.pullupSense = INPUT_PULLUP;
            config.intRoutine = []() {
                UserButtonThread->userButton.tick();
                UserButtonThread->setIntervalFromNow(0);
                runASAP = true;
                BaseType_t higherWake = 0;
                mainDelay.interruptFromISR(&higherWake);
            };
            config.singlePress = INPUT_BROKER_USER_PRESS;
            config.longPress = INPUT_BROKER_SELECT;
            UserButtonThread->initButton(config);
        }
    }
#endif

#ifdef BUTTON_PIN_TOUCH
    TouchButtonThread = new ButtonThread("BackButton");
    ButtonConfig touchConfig;
    touchConfig.pinNumber = BUTTON_PIN_TOUCH;
    touchConfig.activeLow = true;
    touchConfig.activePullup = true;
    touchConfig.pullupSense = pullup_sense;
    touchConfig.intRoutine = []() {
        TouchButtonThread->userButton.tick();
        TouchButtonThread->setIntervalFromNow(0);
        runASAP = true;
        BaseType_t higherWake = 0;
        mainDelay.interruptFromISR(&higherWake);
    };
    touchConfig.singlePress = INPUT_BROKER_NONE;
    touchConfig.longPress = INPUT_BROKER_BACK;
    TouchButtonThread->initButton(touchConfig);
#endif

#if defined(CANCEL_BUTTON_PIN)
    // Buttons. Moved here cause we need NodeDB to be initialized
    CancelButtonThread = new ButtonThread("CancelButton");
    ButtonConfig cancelConfig;
    cancelConfig.pinNumber = CANCEL_BUTTON_PIN;
    cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW;
    cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP;
    cancelConfig.pullupSense = pullup_sense;
    cancelConfig.intRoutine = []() {
        CancelButtonThread->userButton.tick();
        CancelButtonThread->setIntervalFromNow(0);
        runASAP = true;
        BaseType_t higherWake = 0;
        mainDelay.interruptFromISR(&higherWake);
    };
    cancelConfig.singlePress = INPUT_BROKER_CANCEL;
    cancelConfig.longPress = INPUT_BROKER_SHUTDOWN;
    cancelConfig.longPressTime = 4000;
    CancelButtonThread->initButton(cancelConfig);
#endif

#if defined(ALT_BUTTON_PIN)
    // Buttons. Moved here cause we need NodeDB to be initialized
    BackButtonThread = new ButtonThread("BackButton");
    ButtonConfig backConfig;
    backConfig.pinNumber = ALT_BUTTON_PIN;
    backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW;
    backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP;
    backConfig.pullupSense = pullup_sense;
    backConfig.intRoutine = []() {
        BackButtonThread->userButton.tick();
        BackButtonThread->setIntervalFromNow(0);
        runASAP = true;
        BaseType_t higherWake = 0;
        mainDelay.interruptFromISR(&higherWake);
    };
    backConfig.singlePress = INPUT_BROKER_ALT_PRESS;
    backConfig.longPress = INPUT_BROKER_ALT_LONG;
    backConfig.longPressTime = 500;
    BackButtonThread->initButton(backConfig);
#endif

#if defined(BUTTON_PIN)
#if defined(USERPREFS_BUTTON_PIN)
    int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
#else
    int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
#endif
#ifndef BUTTON_ACTIVE_LOW
#define BUTTON_ACTIVE_LOW true
#endif
#ifndef BUTTON_ACTIVE_PULLUP
#define BUTTON_ACTIVE_PULLUP true
#endif

    // Buttons. Moved here cause we need NodeDB to be initialized
    // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
    UserButtonThread = new ButtonThread("UserButton");
    if (screen) {
        ButtonConfig userConfig;
        userConfig.pinNumber = (uint8_t)_pinNum;
        userConfig.activeLow = BUTTON_ACTIVE_LOW;
        userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
        userConfig.pullupSense = pullup_sense;
        userConfig.intRoutine = []() {
            UserButtonThread->userButton.tick();
            UserButtonThread->setIntervalFromNow(0);
            runASAP = true;
            BaseType_t higherWake = 0;
            mainDelay.interruptFromISR(&higherWake);
        };
        userConfig.singlePress = INPUT_BROKER_USER_PRESS;
        userConfig.longPress = INPUT_BROKER_SELECT;
        userConfig.longPressTime = 500;
        userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
#ifdef BUTTON_SINGLE_PRESS_EVENT
        userConfig.singlePress = BUTTON_SINGLE_PRESS_EVENT;
#endif
#ifdef BUTTON_DISABLE_LONG_PRESS
        userConfig.longPress = INPUT_BROKER_NONE;
        userConfig.longLongPress = INPUT_BROKER_NONE;
#endif
        UserButtonThread->initButton(userConfig);
    } else {
        ButtonConfig userConfigNoScreen;
        userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
        userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
        userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP;
        userConfigNoScreen.pullupSense = pullup_sense;
        userConfigNoScreen.intRoutine = []() {
            UserButtonThread->userButton.tick();
            UserButtonThread->setIntervalFromNow(0);
            runASAP = true;
            BaseType_t higherWake = 0;
            mainDelay.interruptFromISR(&higherWake);
        };
        userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
        userConfigNoScreen.longPress = INPUT_BROKER_NONE;
        userConfigNoScreen.longPressTime = 500;
        userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
        userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
        userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
        UserButtonThread->initButton(userConfigNoScreen);
    }
#endif

#endif

#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
    // After modules are setup, so we can observe modules
    setupNicheGraphics();
#endif

#ifdef LED_PIN
    // Turn LED off after boot, if heartbeat by config
    if (config.device.led_heartbeat_disabled)
        digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON);
#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(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) ||       \
    defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) ||              \
    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

#ifdef PIN_PWR_DELAY_MS
    // This may be required to give the peripherals time to power up.
    delay(PIN_PWR_DELAY_MS);
#endif

#ifdef ARCH_PORTDUINO
    // as one can't use a function pointer to the class constructor:
    auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
                                  RADIOLIB_PIN_TYPE busy) {
        switch (portduino_config.lora_module) {
        case use_rf95:
            return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy);
        case use_sx1262:
            return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy);
        case use_sx1268:
            return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy);
        case use_sx1280:
            return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy);
        case use_lr1110:
            return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy);
        case use_lr1120:
            return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy);
        case use_lr1121:
            return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy);
        case use_llcc68:
            return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy);
        case use_simradio:
            return (RadioInterface *)new SimRadio;
        default:
            assert(0); // shouldn't happen
            return (RadioInterface *)nullptr;
        }
    };

    LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(),
              portduino_config.lora_spi_dev.c_str());
    if (portduino_config.lora_spi_dev == "ch341") {
        RadioLibHAL = ch341Hal;
    } else {
        RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
    }
    rIf =
        loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
                            portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin);

    if (!rIf->init()) {
        LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
        delete rIf;
        rIf = NULL;
        exit(EXIT_FAILURE);
    } else {
        LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
    }

#elif defined(HW_SPI1_DEVICE)
    LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings);
#elif defined(TCA9535_LORA_RST_VIRTUAL_PIN)
    TCA9535GpioHal *RadioLibHAL = new TCA9535GpioHal(SPI, spiSettings);
#else // HW_SPI1_DEVICE
    LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
#endif

    // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init)
#if defined(USE_STM32WLx)
    if (!rIf) {
        rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
        if (!rIf->init()) {
            LOG_WARN("No STM32WL radio");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("STM32WL init success");
            radioType = STM32WLx_RADIO;
        }
    }
#endif

#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
    if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
        rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1);
        if (!rIf->init()) {
            LOG_WARN("No RF95 radio");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("RF95 init success");
            radioType = RF95_RADIO;
        }
    }
#endif

#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1
    if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
        auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
#ifdef SX126X_DIO3_TCXO_VOLTAGE
        sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
#endif
        if (!sxIf->init()) {
            LOG_WARN("No SX1262 radio");
            delete sxIf;
            rIf = NULL;
        } else {
            LOG_INFO("SX1262 init success");
            rIf = sxIf;
            radioType = SX1262_RADIO;
        }
    }
#endif

#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL)
    if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
        // try using the specified TCXO voltage
        auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
        sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
        if (!sxIf->init()) {
            LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
            delete sxIf;
            rIf = NULL;
        } else {
            LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
            rIf = sxIf;
            radioType = SX1262_RADIO;
        }
    }

    if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
        // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead
        rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
        if (!rIf->init()) {
            LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("SX1262 init success, XTAL, Vref 0.0V");
            radioType = SX1262_RADIO;
        }
    }
#endif

#if defined(USE_SX1268)
#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL)
    if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
        // try using the specified TCXO voltage
        auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
        sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
        if (!sxIf->init()) {
            LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
            delete sxIf;
            rIf = NULL;
        } else {
            LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
            rIf = sxIf;
            radioType = SX1268_RADIO;
        }
    }
#endif
    if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
        rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
        if (!rIf->init()) {
            LOG_WARN("No SX1268 radio");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("SX1268 init success");
            radioType = SX1268_RADIO;
        }
    }
#endif

#if defined(USE_LLCC68)
    if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
        rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
        if (!rIf->init()) {
            LOG_WARN("No LLCC68 radio");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("LLCC68 init success");
            radioType = LLCC68_RADIO;
        }
    }
#endif

#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1
    if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
        rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN);
        if (!rIf->init()) {
            LOG_WARN("No LR1110 radio");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("LR1110 init success");
            radioType = LR1110_RADIO;
        }
    }
#endif

#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1
    if (!rIf) {
        rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN);
        if (!rIf->init()) {
            LOG_WARN("No LR1120 radio");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("LR1120 init success");
            radioType = LR1120_RADIO;
        }
    }
#endif

#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1
    if (!rIf) {
        rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN);
        if (!rIf->init()) {
            LOG_WARN("No LR1121 radio");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("LR1121 init success");
            radioType = LR1121_RADIO;
        }
    }
#endif

#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
    if (!rIf) {
        rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY);
        if (!rIf->init()) {
            LOG_WARN("No SX1280 radio");
            delete rIf;
            rIf = NULL;
        } else {
            LOG_INFO("SX1280 init success");
            radioType = SX1280_RADIO;
        }
    }
#endif

    // check if the radio chip matches the selected region
    if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) {
        LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
        config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
        nodeDB->saveToDisk(SEGMENT_CONFIG);

        if (!rIf->reconfigure()) {
            LOG_WARN("Reconfigure failed, rebooting");
            if (screen) {
                screen->showSimpleBanner("Rebooting...");
            }
            rebootAtMsec = millis() + 5000;
        }
    }

    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(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
    osk_found = true;
#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 {
        router->addInterface(rIf);

        // 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);
    }

    // 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)

// 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;

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_STM32WL) || 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;

#ifdef ARCH_ESP32
    esp32Loop();
#endif
#ifdef ARCH_NRF52
    nrf52Loop();
#endif
    power->powerCommandsCheck();

#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 && 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
    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
        mainDelay.delay(delayMsec);
    }
}
#endif
