// Copyright (C) 2024, Mark Qvist // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see . // CBA Reticulum includes must come before local to avoid collision with local defines #ifdef HAS_RNS #include #include #include #include #include #include #endif #include #include #include "Utilities.h" // CBA Boundary Mode #ifdef BOUNDARY_MODE #include "BoundaryMode.h" #include "TcpInterface.h" #include "BoundaryConfig.h" #include "esp_bt.h" #endif // CBA FileSystem #if defined(RNS_USE_FS) #include "FileSystem.h" #else #include "NoopFileSystem.h" #endif // CBA SD #if HAS_SDCARD #include SPIClass SDSPI(HSPI); #endif #if MCU_VARIANT == MCU_ESP32 #include #endif // WDT timeout #define WDT_TIMEOUT 60 // seconds FIFOBuffer serialFIFO; uint8_t serialBuffer[CONFIG_UART_BUFFER_SIZE+1]; FIFOBuffer16 packet_starts; uint16_t packet_starts_buf[CONFIG_QUEUE_MAX_LENGTH+1]; FIFOBuffer16 packet_lengths; uint16_t packet_lengths_buf[CONFIG_QUEUE_MAX_LENGTH+1]; uint8_t packet_queue[CONFIG_QUEUE_SIZE]; volatile uint8_t queue_height = 0; volatile uint16_t queued_bytes = 0; volatile uint16_t queue_cursor = 0; volatile uint16_t current_packet_start = 0; volatile bool serial_buffering = false; #if HAS_BLUETOOTH || HAS_BLE == true bool bt_init_ran = false; #endif #if HAS_CONSOLE #include "Console.h" #endif #if PLATFORM == PLATFORM_ESP32 || PLATFORM == PLATFORM_NRF52 #define MODEM_QUEUE_SIZE 8 typedef struct { size_t len; int rssi; int snr_raw; uint8_t data[]; } modem_packet_t; static xQueueHandle modem_packet_queue = NULL; #endif char sbuf[128]; #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 bool packet_ready = false; #endif #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 void update_csma_parameters(); #endif #ifdef HAS_RNS // CBA LoRa interface class LoRaInterface : public RNS::InterfaceImpl { public: LoRaInterface() : RNS::InterfaceImpl("LoRaInterface") { _IN = true; _OUT = true; _HW_MTU = 508; } LoRaInterface(const char *name) : RNS::InterfaceImpl(name) { _IN = true; _OUT = true; _HW_MTU = 508; } virtual ~LoRaInterface() { _name = "deleted"; } protected: virtual void handle_incoming(const RNS::Bytes& data) { TRACEF("LoRaInterface.handle_incoming: (%u bytes) data: %s", data.size(), data.toHex().c_str()); TRACE("LoRaInterface.handle_incoming: sending packet to rns..."); InterfaceImpl::handle_incoming(data); } virtual void send_outgoing(const RNS::Bytes& data) { // CBA NOTE header will be addded later by transmit function TRACEF("LoRaInterface.send_outgoing: (%u bytes) data: %s", data.size(), data.toHex().c_str()); TRACE("LoRaInterface.send_outgoing: adding packet to outgoing queue..."); for (size_t i = 0; i < data.size(); i++) { if (queue_height < CONFIG_QUEUE_MAX_LENGTH && queued_bytes < CONFIG_QUEUE_SIZE) { queued_bytes++; packet_queue[queue_cursor++] = data.data()[i]; if (queue_cursor == CONFIG_QUEUE_SIZE) queue_cursor = 0; } } if (!fifo16_isfull(&packet_starts) && queued_bytes < CONFIG_QUEUE_SIZE) { uint16_t s = current_packet_start; int16_t e = queue_cursor-1; if (e == -1) e = CONFIG_QUEUE_SIZE-1; uint16_t l; if (s != e) { l = (s < e) ? e - s + 1 : CONFIG_QUEUE_SIZE - s + e + 1; } else { l = 1; } if (l >= MIN_L) { queue_height++; fifo16_push(&packet_starts, s); fifo16_push(&packet_lengths, l); current_packet_start = queue_cursor; } } // Perform post-send housekeeping InterfaceImpl::handle_outgoing(data); } }; // CBA logger callback void on_log(const char* msg, RNS::LogLevel level) { /* Serial.print(RNS::getTimeString()); Serial.print(" ["); Serial.print(RNS::getLevelName(level)); Serial.print("] "); Serial.println(msg); Serial.flush(); */ String line = RNS::getTimeString() + String(" [") + RNS::getLevelName(level) + "] " + msg + "\n"; Serial.print(line); Serial.flush(); #ifdef HAS_SDCARD File file = SD.open("/logfile.txt", FILE_APPEND); if (file) { file.write((uint8_t*)line.c_str(), line.length()); file.close(); } #endif // HAS_SDCARD } // CBA receive packet callback void on_receive_packet(const RNS::Bytes& raw, const RNS::Interface& interface) { #ifdef HAS_SDCARD TRACE("Logging receive packet to SD"); String line = RNS::getTimeString() + String(" recv: ") + String(raw.toHex().c_str()) + "\n"; File file = SD.open("/tracefile.txt", FILE_APPEND); if (file) { file.write((uint8_t*)line.c_str(), line.length()); file.close(); } RNS::Packet packet({RNS::Type::NONE}, raw); if (packet.unpack()) { String line = RNS::getTimeString() + String(" recv: ") + String(packet.dumpString().c_str()) + "\n"; File file = SD.open("/tracedetails.txt", FILE_APPEND); if (file) { file.write((uint8_t*)line.c_str(), line.length()); file.close(); } } #endif // HAS_SDCARD } // CBA transmit packet callback void on_transmit_packet(const RNS::Bytes& raw, const RNS::Interface& interface) { #ifdef HAS_SDCARD TRACE("Logging transmit packet to SD"); String line = RNS::getTimeString() + String(" send: ") + String(raw.toHex().c_str()) + "\n"; File file = SD.open("/tracefile.txt", FILE_APPEND); if (file) { file.write((uint8_t*)line.c_str(), line.length()); file.close(); } RNS::Packet packet({RNS::Type::NONE}, raw); if (packet.unpack()) { String line = RNS::getTimeString() + String(" send: ") + String(packet.dumpString().c_str()) + "\n"; File file = SD.open("/tracedetails.txt", FILE_APPEND); if (file) { file.write((uint8_t*)line.c_str(), line.length()); file.close(); } } #endif // HAS_SDCARD } // CBA RNS RNS::Reticulum reticulum(RNS::Type::NONE); RNS::Interface lora_interface(RNS::Type::NONE); RNS::FileSystem filesystem(RNS::Type::NONE); #ifdef BOUNDARY_MODE // Boundary mode: TCP backbone interface + state BoundaryState boundary_state = {}; RNS::Interface tcp_rns_interface(RNS::Type::NONE); TcpInterface* tcp_interface_ptr = nullptr; // Local TCP server (MODE_ACCESS_POINT, doesn't forward announces) RNS::Interface local_tcp_rns_interface(RNS::Type::NONE); TcpInterface* local_tcp_interface_ptr = nullptr; // RTC memory flag — survives software reset but not power cycle RTC_NOINIT_ATTR uint32_t boundary_config_request; #define BOUNDARY_CONFIG_MAGIC 0xC0F19A7E // RTC flag to skip config portal on next boot (set when user powers off from WCC) RTC_NOINIT_ATTR uint32_t boundary_skip_config; #define BOUNDARY_SKIP_MAGIC 0x5E1FC0F0 // Bootloop detection: count rapid reboots in RTC memory. // After BOOTLOOP_THRESHOLD consecutive reboots within BOOTLOOP_WINDOW_MS, // force entry into the config portal so the user can fix settings. #define BOOTLOOP_THRESHOLD 5 #define BOOTLOOP_WINDOW_MS 120000 // 2 minutes #define BOOTLOOP_MAGIC 0xB007100D RTC_NOINIT_ATTR uint32_t bootloop_magic; RTC_NOINIT_ATTR uint32_t bootloop_count; RTC_NOINIT_ATTR uint32_t bootloop_first_boot_ms; #endif #endif // HAS_RNS void setup() { // Initialise serial communication memset(serialBuffer, 0, sizeof(serialBuffer)); fifo_init(&serialFIFO, serialBuffer, CONFIG_UART_BUFFER_SIZE); Serial.begin(serial_baudrate); // CBA Safely wait for serial initialization while (!Serial) { if (millis() > 2000) { break; } delay(10); } // CBA Test delay(2000); // Configure WDT #if MCU_VARIANT == MCU_ESP32 esp_task_wdt_init(WDT_TIMEOUT, true); // enable panic so ESP32 restarts esp_task_wdt_add(NULL); // add current thread to WDT watch #elif MCU_VARIANT == MCU_NRF52 NRF_WDT->CONFIG = 0x01; // Configure WDT to run when CPU is asleep NRF_WDT->CRV = WDT_TIMEOUT * 32768 + 1; // set timeout NRF_WDT->RREN = 0x01; // Enable the RR[0] reload register NRF_WDT->TASKS_START = 1; // Start WDT #endif #if MCU_VARIANT == MCU_ESP32 boot_seq(); EEPROM.begin(EEPROM_SIZE); Serial.setRxBufferSize(CONFIG_UART_BUFFER_SIZE); #if BOARD_MODEL == BOARD_TDECK pinMode(pin_poweron, OUTPUT); digitalWrite(pin_poweron, HIGH); pinMode(SD_CS, OUTPUT); pinMode(DISPLAY_CS, OUTPUT); digitalWrite(SD_CS, HIGH); digitalWrite(DISPLAY_CS, HIGH); pinMode(DISPLAY_BL_PIN, OUTPUT); #endif #endif #if MCU_VARIANT == MCU_NRF52 #if BOARD_MODEL == BOARD_TECHO delay(200); pinMode(PIN_VEXT_EN, OUTPUT); digitalWrite(PIN_VEXT_EN, HIGH); pinMode(pin_btn_usr1, INPUT_PULLUP); pinMode(pin_btn_touch, INPUT_PULLUP); pinMode(PIN_LED_RED, OUTPUT); pinMode(PIN_LED_GREEN, OUTPUT); pinMode(PIN_LED_BLUE, OUTPUT); delay(200); #endif if (!eeprom_begin()) { Serial.write("EEPROM initialisation failed.\r\n"); } #endif // Seed the PRNG for CSMA R-value selection #if MCU_VARIANT == MCU_ESP32 // On ESP32, get the seed value from the // hardware RNG unsigned long seed_val = (unsigned long)esp_random(); #elif MCU_VARIANT == MCU_NRF52 // On nRF, get the seed value from the // hardware RNG unsigned long seed_val = get_rng_seed(); #else // Otherwise, get a pseudo-random seed // value from an unconnected analog pin // // CAUTION! If you are implementing the // firmware on a platform that does not // have a hardware RNG, you MUST take // care to get a seed value with enough // entropy at each device reset! unsigned long seed_val = analogRead(0); #endif randomSeed(seed_val); #if HAS_NP led_init(); #endif #if MCU_VARIANT == MCU_NRF52 && HAS_NP == true boot_seq(); #endif #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 && BOARD_MODEL != BOARD_HELTEC32_V3 // Some boards need to wait until the hardware UART is set up before booting // the full firmware. In the case of the RAK4631, Heltec T114, and Heltec V3, // the line below will wait until a serial connection is actually established // with a master. Thus, it is disabled on these platforms. while (!Serial); #endif serial_interrupt_init(); // Configure input and output pins #if HAS_INPUT input_init(); #endif #if HAS_NP == false pinMode(pin_led_rx, OUTPUT); pinMode(pin_led_tx, OUTPUT); #endif #if HAS_TCXO == true if (pin_tcxo_enable != -1) { pinMode(pin_tcxo_enable, OUTPUT); digitalWrite(pin_tcxo_enable, HIGH); } #endif // Initialise buffers memset(pbuf, 0, sizeof(pbuf)); memset(cmdbuf, 0, sizeof(cmdbuf)); memset(packet_queue, 0, sizeof(packet_queue)); memset(packet_starts_buf, 0, sizeof(packet_starts_buf)); fifo16_init(&packet_starts, packet_starts_buf, CONFIG_QUEUE_MAX_LENGTH); memset(packet_lengths_buf, 0, sizeof(packet_starts_buf)); fifo16_init(&packet_lengths, packet_lengths_buf, CONFIG_QUEUE_MAX_LENGTH); #if PLATFORM == PLATFORM_ESP32 || PLATFORM == PLATFORM_NRF52 modem_packet_queue = xQueueCreate(MODEM_QUEUE_SIZE, sizeof(modem_packet_t*)); #endif // Set chip select, reset and interrupt // pins for the LoRa module #if MODEM == SX1276 || MODEM == SX1278 LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy); #elif MODEM == SX1262 LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen); #elif MODEM == SX1280 LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen, pin_txen); #endif #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 init_channel_stats(); #if BOARD_MODEL == BOARD_T3S3 #if MODEM == SX1280 delay(300); LoRa->reset(); delay(100); #endif #endif #if BOARD_MODEL == BOARD_XIAO_S3 // Improve wakeup from sleep delay(300); LoRa->reset(); delay(100); #endif // Check installed transceiver chip and // probe boot parameters. if (LoRa->preInit()) { modem_installed = true; #if HAS_INPUT // Skip quick-reset console activation #else uint32_t lfr = LoRa->getFrequency(); if (lfr == 0) { // Normal boot } else if (lfr == M_FRQ_R) { // Quick reboot #if HAS_CONSOLE if (rtc_get_reset_reason(0) == POWERON_RESET) { console_active = true; } #endif } else { // Unknown boot } LoRa->setFrequency(M_FRQ_S); #endif } else { modem_installed = false; } #else // Older variants only came with SX1276/78 chips, // so assume that to be the case for now. modem_installed = true; #endif #if HAS_DISPLAY #if HAS_EEPROM if (EEPROM.read(eeprom_addr(ADDR_CONF_DSET)) != CONF_OK_BYTE) { #elif MCU_VARIANT == MCU_NRF52 if (eeprom_read(eeprom_addr(ADDR_CONF_DSET)) != CONF_OK_BYTE) { #endif eeprom_update(eeprom_addr(ADDR_CONF_DSET), CONF_OK_BYTE); #if BOARD_MODEL == BOARD_TECHO eeprom_update(eeprom_addr(ADDR_CONF_DINT), 0x03); #else eeprom_update(eeprom_addr(ADDR_CONF_DINT), 0xFF); #endif } #if BOARD_MODEL == BOARD_TECHO display_add_callback(work_while_waiting); #endif display_unblank(); disp_ready = display_init(); if (disp_ready) { update_display(); } else { headless_mode = true; Serial.println("[Headless] No display detected — running in headless mode"); } #endif // LED solid on at boot for V3/V4 boards (with or without display) #if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3 headless_led_solid(); #endif // ── Boundary Mode: check if config portal is needed ── #ifdef BOUNDARY_MODE { // Load LoRa config from EEPROM so the portal can show current values eeprom_conf_load(); // Load boundary config so the portal can show current/default values boundary_load_config(); // ── Bootloop detection ─────────────────────────────────────────────── // Track rapid reboots in RTC memory. If the device reboots more than // BOOTLOOP_THRESHOLD times within BOOTLOOP_WINDOW_MS, force the config // portal so the user can fix bad settings. bool bootloop_detected = false; { uint32_t now = millis(); if (bootloop_magic != BOOTLOOP_MAGIC) { // First boot or power cycle — initialize counter bootloop_magic = BOOTLOOP_MAGIC; bootloop_count = 1; bootloop_first_boot_ms = now; } else { bootloop_count++; // Check if we're within the time window if (bootloop_count >= BOOTLOOP_THRESHOLD) { Serial.printf("[Boundary] BOOTLOOP DETECTED: %lu reboots — forcing config portal\r\n", bootloop_count); bootloop_detected = true; // Reset counter so next reboot after config portal doesn't re-trigger bootloop_count = 0; bootloop_magic = 0; } } } // Enter config mode if: first boot with no config, OR button-triggered reboot, // OR bootloop detected bool need_config = boundary_needs_config(); bool config_requested = (boundary_config_request == BOUNDARY_CONFIG_MAGIC); bool skip_config = (boundary_skip_config == BOUNDARY_SKIP_MAGIC); boundary_config_request = 0; // Clear flag immediately boundary_skip_config = 0; // Clear skip flag immediately // Skip flag only suppresses a button-triggered re-entry, not a genuinely // unconfigured device. If there's no config saved, always show the portal. if (skip_config && config_requested) { Serial.println("[Boundary] Skipping config portal — user requested normal boot"); config_requested = false; } if (need_config || config_requested || bootloop_detected) { if (bootloop_detected) { Serial.println("[Boundary] Entering config portal due to bootloop recovery"); } else if (config_requested) { Serial.println("[Boundary] Config mode requested via button hold"); } else { Serial.println("[Boundary] No configuration found — starting config portal"); } config_portal_start(); // Block here: only run the config portal until user saves and device reboots // Track button state for "off" action (1-3s press = sleep) bool wcc_btn_down = false; uint32_t wcc_btn_down_at = 0; while (config_portal_is_active()) { config_portal_loop(); // Headless LED: slow ramp breathe effect during WCC mode headless_led_ramp(); // Button handling: allow 1-3s press to turn off (deep sleep) // Next power-on boots to normal mode since boundary_config_request is cleared #if HAS_INPUT { int btn = digitalRead(pin_btn_usr1); if (btn == LOW && !wcc_btn_down) { wcc_btn_down = true; wcc_btn_down_at = millis(); } else if (btn == HIGH && wcc_btn_down) { uint32_t held = millis() - wcc_btn_down_at; wcc_btn_down = false; if (held >= 700 && held <= 5000) { Serial.println("[Boundary] Button press in WCC mode — powering off"); boundary_skip_config = BOUNDARY_SKIP_MAGIC; // Skip config on next boot headless_led_off(); config_portal_stop(); #if HAS_SLEEP sleep_now(); #endif } } } #endif #if MCU_VARIANT == MCU_ESP32 esp_task_wdt_reset(); #endif delay(1); } // If we exit (shouldn't normally), reboot anyway ESP.restart(); } } #endif #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 #if HAS_PMU == true pmu_ready = init_pmu(); #endif #if HAS_BLUETOOTH || HAS_BLE == true #ifndef BOUNDARY_MODE bt_init(); bt_init_ran = true; #else // Boundary mode: release BT controller memory (~70KB) btStop(); esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); #endif #else #ifdef BOUNDARY_MODE // Even when BLE/BT are compile-time disabled (e.g. V3 boundary), // the ESP32 BT controller is still loaded. Release its ~70KB of RAM. btStop(); esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); Serial.write("[Boundary] Released BT controller memory\r\n"); #endif #endif #ifdef BOUNDARY_MODE // Initialize bt_devname for WiFi hostname when BT is disabled #if HAS_BLUETOOTH || HAS_BLE == true if (!bt_init_ran) #endif { uint8_t mac[6]; esp_read_mac(mac, ESP_MAC_WIFI_STA); sprintf(bt_devname, "RNode %02X%02X", mac[4], mac[5]); } #endif if (console_active) { #if HAS_CONSOLE console_start(); #else kiss_indicate_reset(); #endif } else { #if HAS_WIFI wifi_mode = EEPROM.read(eeprom_addr(ADDR_CONF_WIFI)); if (wifi_mode == WR_WIFI_STA || wifi_mode == WR_WIFI_AP) { wifi_remote_init(); } #endif kiss_indicate_reset(); } #endif #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 #if MODEM == SX1280 avoid_interference = false; #else #if HAS_EEPROM uint8_t ia_conf = EEPROM.read(eeprom_addr(ADDR_CONF_DIA)); if (ia_conf == 0x00) { avoid_interference = true; } else { avoid_interference = false; } #elif MCU_VARIANT == MCU_NRF52 uint8_t ia_conf = eeprom_read(eeprom_addr(ADDR_CONF_DIA)); if (ia_conf == 0x00) { avoid_interference = true; } else { avoid_interference = false; } #endif #endif #endif // Feed WDT before validation + radio start, which may take time #if MCU_VARIANT == MCU_ESP32 esp_task_wdt_reset(); #endif // Validate board health, EEPROM and config validate_status(); if (op_mode != MODE_TNC) LoRa->setFrequency(0); // CBA SD #ifdef HAS_SDCARD pinMode(SDCARD_MISO, INPUT_PULLUP); SDSPI.begin(SDCARD_SCLK, SDCARD_MISO, SDCARD_MOSI, SDCARD_CS); if (!SD.begin(SDCARD_CS, SDSPI)) { Serial.println("setupSDCard FAIL"); } else { uint32_t cardSize = SD.cardSize() / (1024 * 1024); Serial.print("setupSDCard PASS . SIZE = "); Serial.print(cardSize / 1024.0); Serial.println(" GB"); SD.remove("/logfile"); SD.remove("/logfile.txt"); SD.remove("/tracefile"); SD.remove("/tracedetails"); SD.remove("/tracefile.txt"); SD.remove("/tracedetails.txt"); Serial.println("DIR: /"); File root = SD.open("/"); File file = root.openNextFile(); while(file){ Serial.print(" FILE: "); Serial.println(file.name()); file = root.openNextFile(); } } delay(3000); #endif #ifdef HAS_RNS try { // Feed WDT before filesystem init (may format on first boot) #if MCU_VARIANT == MCU_ESP32 esp_task_wdt_reset(); #endif // CBA Init filesystem #if defined(RNS_USE_FS) filesystem = new FileSystem(); ((FileSystem*)filesystem.get())->init(); #else filesystem = new NoopFileSystem(); ((FileSystem*)filesystem.get())->init(); #endif // Feed WDT after filesystem init #if MCU_VARIANT == MCU_ESP32 esp_task_wdt_reset(); #endif HEAD("Registering filesystem...", RNS::LOG_TRACE); RNS::Utilities::OS::register_filesystem(filesystem); #ifndef NDEBUG //filesystem.remove_directory("/cache"); //filesystem.remove_file("/destination_table"); //filesystem.reformat(); TRACE("Listing filesystem..."); #if defined(RNS_USE_FS) //FileSystem::listDir("/"); #endif TRACE("Finished listing"); //TRACE("Dumping filesystem..."); //FileSystem::dumpDir("/"); //TRACE("Finished dumping"); //reticulum.clear_caches(); // CBA DEBUG /* std::list files = filesystem.list_directory("/cache"); for (auto& file : files) { Serial.print(" FILE: "); Serial.println(file.c_str()); //RNS::Bytes content = filesystem.read_file(file.c_str()); //DEBUG(std::string("FILE: ") + file); //DEBUG(content.toString()); } */ TRACE("FILE: destination_table"); RNS::Bytes content; if (filesystem.read_file("/destination_table", content) > 0) { TRACE(content.toString() + "\r\n"); } #endif // NDEBUG // CBA Start RNS if (hw_ready) { // Feed WDT before RNS startup (identity generation + crypto can be slow) #if MCU_VARIANT == MCU_ESP32 esp_task_wdt_reset(); #endif RNS::setLogCallback(&on_log); RNS::Transport::set_receive_packet_callback(on_receive_packet); RNS::Transport::set_transmit_packet_callback(on_transmit_packet); Serial.write("Starting RNS...\r\n"); RNS::loglevel(RNS::LOG_VERBOSE); //RNS::loglevel(RNS::LOG_TRACE); //RNS::loglevel(RNS::LOG_MEM); HEAD("Registering LoRA Interface...", RNS::LOG_TRACE); lora_interface = new LoRaInterface(); lora_interface.mode(RNS::Type::Interface::MODE_ACCESS_POINT); RNS::Transport::register_interface(lora_interface); #ifdef BOUNDARY_MODE // ── Boundary Mode: Load config and optionally set up WiFi + TCP ── HEAD("Boundary Mode: Initializing...", RNS::LOG_TRACE); // ESP32 has only ~324KB heap. Each path entry with random_blobs costs // ~200-500 bytes. Keep tables small to avoid heap exhaustion. // cull_path_table() evicts backbone paths first, preserving local ones. RNS::Transport::path_table_maxsize(24); RNS::Transport::path_table_maxpersist(12); boundary_load_config(); // Set up IFAC on the LoRa interface if configured if (boundary_state.ifac_enabled && (boundary_state.ifac_netname[0] != '\0' || boundary_state.ifac_passphrase[0] != '\0')) { HEAD("Setting up IFAC on LoRa interface...", RNS::LOG_TRACE); lora_interface.setup_ifac(boundary_state.ifac_netname, boundary_state.ifac_passphrase); { char _ifac_msg[96]; snprintf(_ifac_msg, sizeof(_ifac_msg), "IFAC configured: netname=%s, passphrase=%s", boundary_state.ifac_netname[0] ? boundary_state.ifac_netname : "(none)", boundary_state.ifac_passphrase[0] ? "***" : "(none)"); HEAD(_ifac_msg, RNS::LOG_TRACE); } } // Start WiFi if enabled if (boundary_state.wifi_enabled) { if (!wifi_initialized) { if (wifi_mode != WR_WIFI_STA && wifi_mode != WR_WIFI_AP) { wifi_mode = WR_WIFI_STA; EEPROM.write(eeprom_addr(ADDR_CONF_WIFI), wifi_mode); EEPROM.commit(); } wifi_remote_init(); } } else { HEAD("Boundary Mode: WiFi DISABLED (LoRa-only repeater)", RNS::LOG_TRACE); } // Register TCP backbone interface if enabled (mode 1 = client) if (boundary_state.wifi_enabled && boundary_state.tcp_mode == 1) { tcp_interface_ptr = new TcpInterface( TCP_IF_MODE_CLIENT, boundary_state.tcp_port, boundary_state.backbone_host, boundary_state.backbone_port ); tcp_rns_interface = tcp_interface_ptr; tcp_rns_interface.mode(RNS::Type::Interface::MODE_BOUNDARY); tcp_rns_interface.is_backbone(true); RNS::Transport::register_interface(tcp_rns_interface); { char _bm_msg[128]; snprintf(_bm_msg, sizeof(_bm_msg), "TCP backbone: client -> %s:%d", boundary_state.backbone_host, boundary_state.backbone_port); HEAD(_bm_msg, RNS::LOG_TRACE); } } else if (boundary_state.tcp_mode == 0) { HEAD("Boundary Mode: TCP backbone DISABLED", RNS::LOG_TRACE); } // Register local TCP server if enabled // MODE_GATEWAY allows announce rebroadcasts so local TCP clients // can discover each other and receive backbone announces. // (MODE_ACCESS_POINT blocks all announce broadcasts in outbound(), // which prevented local clients from finding paths to each other.) if (boundary_state.wifi_enabled && boundary_state.ap_tcp_enabled) { local_tcp_interface_ptr = new TcpInterface( TCP_IF_MODE_SERVER, boundary_state.ap_tcp_port, "", // no target host for server mode 0, "LocalTcpInterface" ); // rnsd can be quiet for long stretches — use 10 min timeout // to prevent unnecessary reconnection cycles that leak lwIP memory local_tcp_interface_ptr->setReadTimeout(600000); local_tcp_rns_interface = local_tcp_interface_ptr; local_tcp_rns_interface.mode(RNS::Type::Interface::MODE_GATEWAY); RNS::Transport::register_interface(local_tcp_rns_interface); // Register as local client interface so Transport forwards // announces, link packets, and proofs to TCP clients RNS::Transport::register_local_client_interface(local_tcp_rns_interface); { char _bm_msg[128]; snprintf(_bm_msg, sizeof(_bm_msg), "Local TCP server: port %d (GATEWAY mode)", boundary_state.ap_tcp_port); HEAD(_bm_msg, RNS::LOG_TRACE); } } #endif // Feed WDT before Reticulum instance creation (loads caches, generates keys) #if MCU_VARIANT == MCU_ESP32 esp_task_wdt_reset(); #endif HEAD("Creating Reticulum instance...", RNS::LOG_TRACE); reticulum = RNS::Reticulum(); #ifdef BOUNDARY_MODE // In boundary mode, transport is ALWAYS enabled reticulum.transport_enabled(true); #else reticulum.transport_enabled(op_mode == MODE_TNC); #endif reticulum.probe_destination_enabled(true); reticulum.start(); #ifdef BOUNDARY_MODE // Start TCP interfaces after Reticulum is running if (boundary_state.wifi_enabled && (wifi_is_connected() || wifi_mode == WR_WIFI_AP)) { if (tcp_interface_ptr) { tcp_interface_ptr->start(); HEAD("Boundary Mode: TCP backbone started", RNS::LOG_TRACE); } if (local_tcp_interface_ptr) { local_tcp_interface_ptr->start(); HEAD("Boundary Mode: Local TCP server started", RNS::LOG_TRACE); } } else if (boundary_state.wifi_enabled) { HEAD("Boundary Mode: Waiting for WiFi before starting TCP interfaces", RNS::LOG_WARNING); } #endif // CBA load/create local destination for admin node /* RNS::Identity identity = {RNS::Type::NONE}; std::string local_identity_path = RNS::Reticulum::_storagepath + "/local_identity"; if (RNS::Utilities::OS::file_exists(local_identity_path.c_str())) { identity = RNS::Identity::from_file(local_identity_path.c_str()); } if (!identity) { RNS::verbose("No valid local identity in storage, creating..."); identity = RNS::Identity(); identity.to_file(local_identity_path.c_str()); } else { RNS::verbose("Loaded local identity from storage"); } RNS::Destination destination(identity, RNS::Type::Destination::IN, RNS::Type::Destination::SINGLE, "rnstransport", "local"); */ RNS::Destination destination(RNS::Transport::identity(), RNS::Type::Destination::IN, RNS::Type::Destination::SINGLE, "rnstransport", "local"); HEAD("RNS is READY!", RNS::LOG_TRACE); #ifdef BOUNDARY_MODE HEAD("*** BOUNDARY MODE ACTIVE ***", RNS::LOG_TRACE); HEAD("RNS transport mode is ENABLED (boundary)", RNS::LOG_TRACE); HEAD("LoRa Interface: MODE_ACCESS_POINT", RNS::LOG_TRACE); { char _bm_info[128]; if (boundary_state.tcp_mode == 1) { snprintf(_bm_info, sizeof(_bm_info), "TCP Backbone: client -> %s:%d", boundary_state.backbone_host, boundary_state.backbone_port); HEAD(_bm_info, RNS::LOG_TRACE); } else { HEAD("TCP Backbone: DISABLED", RNS::LOG_TRACE); } if (boundary_state.ap_tcp_enabled) { snprintf(_bm_info, sizeof(_bm_info), "Local TCP Server: port %d (MODE_ACCESS_POINT)", boundary_state.ap_tcp_port); HEAD(_bm_info, RNS::LOG_TRACE); } if (!boundary_state.wifi_enabled) { HEAD("WiFi: DISABLED (LoRa-only repeater)", RNS::LOG_TRACE); } } #endif if (op_mode == MODE_TNC) { HEAD("RNS transport mode is ENABLED", RNS::LOG_TRACE); TRACEF("Frequency: %d Hz", lora_freq); TRACEF("Bandwidth: %d Hz", lora_bw); TRACEF("TX Power: %d dBm", lora_txp); TRACEF("Spreading Factor: %d", lora_sf); TRACEF("Coding Rate: %d", lora_cr); } else { HEAD("RNS transport mode is DISABLED", RNS::LOG_INFO); HEAD("Configure TNC mode with radio configuration to enable RNS transport", RNS::LOG_INFO); } //RNS::loglevel(RNS::LOG_NONE); } else { HEAD("RNS is inoperable because hardware is not ready!", RNS::LOG_ERROR); HEAD("Check firmware signature and eeprom provisioning", RNS::LOG_ERROR); // CBA Clear cached files just in case cached files are responsible for failure //reticulum.clear_caches(); } } catch (std::exception& e) { ERROR("RNS startup failed: " + std::string(e.what())); } #endif // HAS_RNS } void lora_receive() { if (!implicit) { LoRa->receive(); } else { LoRa->receive(implicit_l); } } inline void kiss_write_packet() { #ifdef HAS_RNS TRACEF("Received %d byte packet", host_write_len); // CBA send packet received over LoRa to RNS in addition to connected client // CBA RESERVE //RNS::Bytes data(); RNS::Bytes data(512); for (uint16_t i = 0; i < host_write_len; i++) { #if MCU_VARIANT == MCU_NRF52 portENTER_CRITICAL(); uint8_t byte = pbuf[i]; portEXIT_CRITICAL(); #else uint8_t byte = pbuf[i]; #endif data << byte; } lora_interface.handle_incoming(data); #endif serial_write(FEND); serial_write(CMD_DATA); for (uint16_t i = 0; i < host_write_len; i++) { #if MCU_VARIANT == MCU_NRF52 portENTER_CRITICAL(); uint8_t byte = pbuf[i]; portEXIT_CRITICAL(); #else uint8_t byte = pbuf[i]; #endif if (byte == FEND) { serial_write(FESC); byte = TFEND; } if (byte == FESC) { serial_write(FESC); byte = TFESC; } serial_write(byte); } serial_write(FEND); host_write_len = 0; #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 packet_ready = false; #endif #if MCU_VARIANT == MCU_ESP32 #if HAS_BLE bt_flush(); #endif #endif } inline void getPacketData(uint16_t len) { #if MCU_VARIANT != MCU_NRF52 while (len-- && read_len < MTU) { pbuf[read_len++] = LoRa->read(); } #else BaseType_t int_mask = taskENTER_CRITICAL_FROM_ISR(); while (len-- && read_len < MTU) { pbuf[read_len++] = LoRa->read(); } taskEXIT_CRITICAL_FROM_ISR(int_mask); #endif } void ISR_VECT receive_callback(int packet_size) { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 BaseType_t int_mask; #endif if (!promisc) { // The standard operating mode allows large // packets with a payload up to 500 bytes, // by combining two raw LoRa packets. // We read the 1-byte header and extract // packet sequence number and split flags uint8_t header = LoRa->read(); packet_size--; uint8_t sequence = packetSequence(header); bool ready = false; if (isSplitPacket(header) && seq == SEQ_UNSET) { // This is the first part of a split // packet, so we set the seq variable // and add the data to the buffer #if MCU_VARIANT == MCU_NRF52 int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask); #else read_len = 0; #endif seq = sequence; #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 last_rssi = LoRa->packetRssi(); last_snr_raw = LoRa->packetSnrRaw(); #endif getPacketData(packet_size); } else if (isSplitPacket(header) && seq == sequence) { // This is the second part of a split // packet, so we add it to the buffer // and set the ready flag. #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 last_rssi = (last_rssi+LoRa->packetRssi())/2; last_snr_raw = (last_snr_raw+LoRa->packetSnrRaw())/2; #endif getPacketData(packet_size); seq = SEQ_UNSET; ready = true; } else if (isSplitPacket(header) && seq != sequence) { // This split packet does not carry the // same sequence id, so we must assume // that we are seeing the first part of // a new split packet. #if MCU_VARIANT == MCU_NRF52 int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask); #else read_len = 0; #endif seq = sequence; #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 last_rssi = LoRa->packetRssi(); last_snr_raw = LoRa->packetSnrRaw(); #endif getPacketData(packet_size); } else if (!isSplitPacket(header)) { // This is not a split packet, so we // just read it and set the ready // flag to true. if (seq != SEQ_UNSET) { // If we already had part of a split // packet in the buffer, we clear it. #if MCU_VARIANT == MCU_NRF52 int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask); #else read_len = 0; #endif seq = SEQ_UNSET; } #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 last_rssi = LoRa->packetRssi(); last_snr_raw = LoRa->packetSnrRaw(); #endif getPacketData(packet_size); ready = true; } if (ready) { #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 // We first signal the RSSI of the // recieved packet to the host. kiss_indicate_stat_rssi(); kiss_indicate_stat_snr(); // And then write the entire packet host_write_len = read_len; kiss_write_packet(); read_len = 0; #else // Allocate packet struct, but abort if there // is not enough memory available. modem_packet_t *modem_packet = (modem_packet_t*)malloc(sizeof(modem_packet_t) + read_len); if(!modem_packet) { memory_low = true; return; } // Get packet RSSI and SNR #if MCU_VARIANT == MCU_ESP32 modem_packet->snr_raw = LoRa->packetSnrRaw(); modem_packet->rssi = LoRa->packetRssi(modem_packet->snr_raw); #endif // Send packet to event queue, but free the // allocated memory again if the queue is // unable to receive the packet. modem_packet->len = read_len; memcpy(modem_packet->data, pbuf, read_len); read_len = 0; if (!modem_packet_queue || xQueueSendFromISR(modem_packet_queue, &modem_packet, NULL) != pdPASS) { free(modem_packet); } #endif } } else { // In promiscuous mode, raw packets are // output directly to the host read_len = 0; #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 last_rssi = LoRa->packetRssi(); last_snr_raw = LoRa->packetSnrRaw(); getPacketData(packet_size); // We first signal the RSSI of the // recieved packet to the host. kiss_indicate_stat_rssi(); kiss_indicate_stat_snr(); // And then write the entire packet kiss_write_packet(); #else getPacketData(packet_size); packet_ready = true; #endif } } bool startRadio() { update_radio_lock(); if (!radio_online && !console_active) { if (!radio_locked && hw_ready) { if (!LoRa->begin(lora_freq)) { // The radio could not be started. // Indicate this failure over both the // serial port and with the onboard LEDs radio_error = true; kiss_indicate_error(ERROR_INITRADIO); led_indicate_error(0); return false; } else { radio_online = true; init_channel_stats(); setTXPower(); setBandwidth(); setSpreadingFactor(); setCodingRate(); getFrequency(); LoRa->enableCrc(); LoRa->onReceive(receive_callback); lora_receive(); // Flash an info pattern to indicate // that the radio is now on kiss_indicate_radiostate(); led_indicate_info(3); return true; } } else { // Flash a warning pattern to indicate // that the radio was locked, and thus // not started radio_online = false; kiss_indicate_radiostate(); led_indicate_warning(3); return false; } } else { // If radio is already on, we silently // ignore the request. kiss_indicate_radiostate(); return true; } } void stopRadio() { LoRa->end(); radio_online = false; } void update_radio_lock() { if (lora_freq != 0 && lora_bw != 0 && lora_txp != 0xFF && lora_sf != 0) { radio_locked = false; } else { radio_locked = true; } } bool queue_full() { return (queue_height >= CONFIG_QUEUE_MAX_LENGTH || queued_bytes >= CONFIG_QUEUE_SIZE); } volatile bool queue_flushing = false; void flush_queue(void) { if (!queue_flushing) { queue_flushing = true; led_tx_on(); #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 while (!fifo16_isempty(&packet_starts)) { #else while (!fifo16_isempty_locked(&packet_starts)) { #endif uint16_t start = fifo16_pop(&packet_starts); uint16_t length = fifo16_pop(&packet_lengths); if (length >= MIN_L && length <= MTU) { for (uint16_t i = 0; i < length; i++) { uint16_t pos = (start+i)%CONFIG_QUEUE_SIZE; tbuf[i] = packet_queue[pos]; } transmit(length); } } lora_receive(); led_tx_off(); } queue_height = 0; queued_bytes = 0; #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 update_airtime(); #endif queue_flushing = false; #if HAS_DISPLAY display_tx = true; #endif } void pop_queue() { if (!queue_flushing) { queue_flushing = true; led_tx_on(); #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 if (!fifo16_isempty(&packet_starts)) { #else if (!fifo16_isempty_locked(&packet_starts)) { #endif uint16_t start = fifo16_pop(&packet_starts); uint16_t length = fifo16_pop(&packet_lengths); if (length >= MIN_L && length <= MTU) { for (uint16_t i = 0; i < length; i++) { uint16_t pos = (start+i)%CONFIG_QUEUE_SIZE; tbuf[i] = packet_queue[pos]; } transmit(length); } queue_height -= 1; queued_bytes -= length; } lora_receive(); led_tx_off(); } #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 update_airtime(); #endif queue_flushing = false; #if HAS_DISPLAY display_tx = true; #endif } void add_airtime(uint16_t written) { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 float lora_symbols = 0; float packet_cost_ms = 0.0; int ldr_opt = 0; if (lora_low_datarate) ldr_opt = 1; #if MODEM == SX1276 || MODEM == SX1278 lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + 8 + PHY_HEADER_LORA_SYMBOLS); lora_symbols /= 4*(lora_sf-2*ldr_opt); lora_symbols *= lora_cr; lora_symbols += lora_preamble_symbols + 0.25 + 8; packet_cost_ms += lora_symbols * lora_symbol_time_ms; #elif MODEM == SX1262 || MODEM == SX1280 if (lora_sf < 7) { lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + PHY_HEADER_LORA_SYMBOLS); lora_symbols /= 4*lora_sf; lora_symbols *= lora_cr; lora_symbols += lora_preamble_symbols + 2.25 + 8; packet_cost_ms += lora_symbols * lora_symbol_time_ms; } else { lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + 8 + PHY_HEADER_LORA_SYMBOLS); lora_symbols /= 4*(lora_sf-2*ldr_opt); lora_symbols *= lora_cr; lora_symbols += lora_preamble_symbols + 0.25 + 8; packet_cost_ms += lora_symbols * lora_symbol_time_ms; } #endif uint16_t cb = current_airtime_bin(); uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; } airtime_bins[cb] += packet_cost_ms; airtime_bins[nb] = 0; #endif } void update_airtime() { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 uint16_t cb = current_airtime_bin(); uint16_t pb = cb-1; if (cb-1 < 0) { pb = AIRTIME_BINS-1; } uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; } airtime_bins[nb] = 0; airtime = (float)(airtime_bins[cb]+airtime_bins[pb])/(2.0*AIRTIME_BINLEN_MS); uint32_t longterm_airtime_sum = 0; for (uint16_t bin = 0; bin < AIRTIME_BINS; bin++) { longterm_airtime_sum += airtime_bins[bin]; } longterm_airtime = (float)longterm_airtime_sum/(float)AIRTIME_LONGTERM_MS; float longterm_channel_util_sum = 0.0; for (uint16_t bin = 0; bin < AIRTIME_BINS; bin++) { longterm_channel_util_sum += longterm_bins[bin]; } longterm_channel_util = (float)longterm_channel_util_sum/(float)AIRTIME_BINS; #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 update_csma_parameters(); #endif kiss_indicate_channel_stats(); #endif } void transmit(uint16_t size) { if (radio_online) { if (!promisc) { uint16_t written = 0; uint8_t header = random(256) & 0xF0; if (size > SINGLE_MTU - HEADER_L) { header = header | FLAG_SPLIT; } LoRa->beginPacket(); LoRa->write(header); written++; for (uint16_t i=0; i < size; i++) { LoRa->write(tbuf[i]); written++; if (written == 255 && isSplitPacket(header)) { if (!LoRa->endPacket()) { kiss_indicate_error(ERROR_MODEM_TIMEOUT); kiss_indicate_error(ERROR_TXFAILED); led_indicate_error(5); hard_reset(); } add_airtime(written); LoRa->beginPacket(); LoRa->write(header); written = 1; } } if (!LoRa->endPacket()) { kiss_indicate_error(ERROR_MODEM_TIMEOUT); kiss_indicate_error(ERROR_TXFAILED); led_indicate_error(5); hard_reset(); } add_airtime(written); } else { led_tx_on(); uint16_t written = 0; if (size > SINGLE_MTU) { size = SINGLE_MTU; } if (!implicit) { LoRa->beginPacket(); } else { LoRa->beginPacket(size); } for (uint16_t i=0; i < size; i++) { LoRa->write(tbuf[i]); written++; } LoRa->endPacket(); add_airtime(written); } } else { kiss_indicate_error(ERROR_TXFAILED); led_indicate_error(5); } } void serial_callback(uint8_t sbyte) { if (IN_FRAME && sbyte == FEND && command == CMD_DATA) { IN_FRAME = false; if (!fifo16_isfull(&packet_starts) && queued_bytes < CONFIG_QUEUE_SIZE) { uint16_t s = current_packet_start; int16_t e = queue_cursor-1; if (e == -1) e = CONFIG_QUEUE_SIZE-1; uint16_t l; if (s != e) { l = (s < e) ? e - s + 1 : CONFIG_QUEUE_SIZE - s + e + 1; } else { l = 1; } if (l >= MIN_L) { queue_height++; fifo16_push(&packet_starts, s); fifo16_push(&packet_lengths, l); current_packet_start = queue_cursor; } } } else if (sbyte == FEND) { IN_FRAME = true; command = CMD_UNKNOWN; frame_len = 0; } else if (IN_FRAME && frame_len < MTU) { // Have a look at the command byte first if (frame_len == 0 && command == CMD_UNKNOWN) { command = sbyte; } else if (command == CMD_DATA) { if (bt_state != BT_STATE_CONNECTED) { cable_state = CABLE_STATE_CONNECTED; } if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (queue_height < CONFIG_QUEUE_MAX_LENGTH && queued_bytes < CONFIG_QUEUE_SIZE) { queued_bytes++; packet_queue[queue_cursor++] = sbyte; if (queue_cursor == CONFIG_QUEUE_SIZE) queue_cursor = 0; } } } else if (command == CMD_FREQUENCY) { if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } if (frame_len == 4) { uint32_t freq = (uint32_t)cmdbuf[0] << 24 | (uint32_t)cmdbuf[1] << 16 | (uint32_t)cmdbuf[2] << 8 | (uint32_t)cmdbuf[3]; if (freq == 0) { kiss_indicate_frequency(); } else { lora_freq = freq; if (op_mode == MODE_HOST) setFrequency(); kiss_indicate_frequency(); } } } else if (command == CMD_BANDWIDTH) { if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } if (frame_len == 4) { uint32_t bw = (uint32_t)cmdbuf[0] << 24 | (uint32_t)cmdbuf[1] << 16 | (uint32_t)cmdbuf[2] << 8 | (uint32_t)cmdbuf[3]; if (bw == 0) { kiss_indicate_bandwidth(); } else { lora_bw = bw; if (op_mode == MODE_HOST) setBandwidth(); kiss_indicate_bandwidth(); } } } else if (command == CMD_TXPOWER) { if (sbyte == 0xFF) { kiss_indicate_txpower(); } else { int txp = sbyte; #if MODEM == SX1262 #if HAS_LORA_PA if (txp > PA_MAX_OUTPUT) txp = PA_MAX_OUTPUT; #else if (txp > 22) txp = 22; #endif #elif MODEM == SX1280 #if HAS_PA if (txp > 20) txp = 20; #else if (txp > 13) txp = 13; #endif #else if (txp > 17) txp = 17; #endif lora_txp = txp; if (op_mode == MODE_HOST) setTXPower(); kiss_indicate_txpower(); } } else if (command == CMD_SF) { if (sbyte == 0xFF) { kiss_indicate_spreadingfactor(); } else { int sf = sbyte; if (sf < 5) sf = 5; if (sf > 12) sf = 12; lora_sf = sf; if (op_mode == MODE_HOST) setSpreadingFactor(); kiss_indicate_spreadingfactor(); } } else if (command == CMD_CR) { if (sbyte == 0xFF) { kiss_indicate_codingrate(); } else { int cr = sbyte; if (cr < 5) cr = 5; if (cr > 8) cr = 8; lora_cr = cr; if (op_mode == MODE_HOST) setCodingRate(); kiss_indicate_codingrate(); } } else if (command == CMD_IMPLICIT) { set_implicit_length(sbyte); kiss_indicate_implicit_length(); } else if (command == CMD_LEAVE) { if (sbyte == 0xFF) { display_unblank(); cable_state = CABLE_STATE_DISCONNECTED; current_rssi = -292; last_rssi = -292; last_rssi_raw = 0x00; last_snr_raw = 0x80; } } else if (command == CMD_RADIO_STATE) { if (bt_state != BT_STATE_CONNECTED) { cable_state = CABLE_STATE_CONNECTED; display_unblank(); } if (sbyte == 0xFF) { kiss_indicate_radiostate(); } else if (sbyte == 0x00) { stopRadio(); kiss_indicate_radiostate(); } else if (sbyte == 0x01) { startRadio(); kiss_indicate_radiostate(); } } else if (command == CMD_ST_ALOCK) { if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } if (frame_len == 2) { uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1]; if (at == 0) { st_airtime_limit = 0.0; } else { st_airtime_limit = (float)at/(100.0*100.0); if (st_airtime_limit >= 1.0) { st_airtime_limit = 0.0; } } kiss_indicate_st_alock(); } } else if (command == CMD_LT_ALOCK) { if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } if (frame_len == 2) { uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1]; if (at == 0) { lt_airtime_limit = 0.0; } else { lt_airtime_limit = (float)at/(100.0*100.0); if (lt_airtime_limit >= 1.0) { lt_airtime_limit = 0.0; } } kiss_indicate_lt_alock(); } } else if (command == CMD_STAT_RX) { kiss_indicate_stat_rx(); } else if (command == CMD_STAT_TX) { kiss_indicate_stat_tx(); } else if (command == CMD_STAT_RSSI) { kiss_indicate_stat_rssi(); } else if (command == CMD_RADIO_LOCK) { update_radio_lock(); kiss_indicate_radio_lock(); } else if (command == CMD_BLINK) { led_indicate_info(sbyte); } else if (command == CMD_RANDOM) { kiss_indicate_random(getRandom()); } else if (command == CMD_DETECT) { if (sbyte == DETECT_REQ) { if (bt_state != BT_STATE_CONNECTED) cable_state = CABLE_STATE_CONNECTED; kiss_indicate_detect(); } } else if (command == CMD_PROMISC) { if (sbyte == 0x01) { promisc_enable(); } else if (sbyte == 0x00) { promisc_disable(); } kiss_indicate_promisc(); } else if (command == CMD_READY) { if (!queue_full()) { kiss_indicate_ready(); } else { kiss_indicate_not_ready(); } } else if (command == CMD_UNLOCK_ROM) { if (sbyte == ROM_UNLOCK_BYTE) { unlock_rom(); } } else if (command == CMD_RESET) { if (sbyte == CMD_RESET_BYTE) { hard_reset(); } } else if (command == CMD_ROM_READ) { kiss_dump_eeprom(); } else if (command == CMD_CFG_READ) { kiss_dump_config(); } else if (command == CMD_ROM_WRITE) { if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } if (frame_len == 2) { eeprom_write(cmdbuf[0], cmdbuf[1]); } } else if (command == CMD_FW_VERSION) { kiss_indicate_version(); } else if (command == CMD_PLATFORM) { kiss_indicate_platform(); } else if (command == CMD_MCU) { kiss_indicate_mcu(); } else if (command == CMD_BOARD) { kiss_indicate_board(); } else if (command == CMD_CONF_SAVE) { eeprom_conf_save(); } else if (command == CMD_CONF_DELETE) { eeprom_conf_delete(); } else if (command == CMD_FB_EXT) { #if HAS_DISPLAY == true if (sbyte == 0xFF) { kiss_indicate_fbstate(); } else if (sbyte == 0x00) { ext_fb_disable(); kiss_indicate_fbstate(); } else if (sbyte == 0x01) { ext_fb_enable(); kiss_indicate_fbstate(); } #endif } else if (command == CMD_FB_WRITE) { if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } #if HAS_DISPLAY if (frame_len == 9) { uint8_t line = cmdbuf[0]; if (line > 63) line = 63; int fb_o = line*8; memcpy(fb+fb_o, cmdbuf+1, 8); } #endif } else if (command == CMD_FB_READ) { if (sbyte != 0x00) { kiss_indicate_fb(); } } else if (command == CMD_DISP_READ) { if (sbyte != 0x00) { kiss_indicate_disp(); } } else if (command == CMD_DEV_HASH) { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 if (sbyte != 0x00) { kiss_indicate_device_hash(); } #endif } else if (command == CMD_DEV_SIG) { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } if (frame_len == DEV_SIG_LEN) { memcpy(dev_sig, cmdbuf, DEV_SIG_LEN); device_save_signature(); } #endif } else if (command == CMD_FW_UPD) { if (sbyte == 0x01) { firmware_update_mode = true; } else { firmware_update_mode = false; } } else if (command == CMD_HASHES) { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 if (sbyte == 0x01) { kiss_indicate_target_fw_hash(); } else if (sbyte == 0x02) { kiss_indicate_fw_hash(); } else if (sbyte == 0x03) { kiss_indicate_bootloader_hash(); } else if (sbyte == 0x04) { kiss_indicate_partition_table_hash(); } #endif } else if (command == CMD_FW_HASH) { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } if (frame_len == DEV_HASH_LEN) { memcpy(dev_firmware_hash_target, cmdbuf, DEV_HASH_LEN); device_save_firmware_hash(); } #endif } else if (command == CMD_WIFI_CHN) { #if HAS_WIFI if (sbyte > 0 && sbyte < 14) { eeprom_update(eeprom_addr(ADDR_CONF_WCHN), sbyte); } #endif } else if (command == CMD_WIFI_MODE) { #if HAS_WIFI if (sbyte == WR_WIFI_OFF || sbyte == WR_WIFI_STA || sbyte == WR_WIFI_AP) { wr_conf_save(sbyte); wifi_mode = sbyte; wifi_remote_init(); } #endif } else if (command == CMD_WIFI_SSID) { #if HAS_WIFI if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; } if (sbyte == 0x00) { for (uint8_t i = 0; i<33; i++) { if (i 0x00) recondition_display = true; } #endif } else if (command == CMD_NP_INT) { #if HAS_NP if (sbyte == FESC) { ESCAPE = true; } else { if (ESCAPE) { if (sbyte == TFEND) sbyte = FEND; if (sbyte == TFESC) sbyte = FESC; ESCAPE = false; } sbyte; led_set_intensity(sbyte); np_int_conf_save(sbyte); } #endif } } } #if MCU_VARIANT == MCU_ESP32 portMUX_TYPE update_lock = portMUX_INITIALIZER_UNLOCKED; #endif bool medium_free() { update_modem_status(); if (avoid_interference && interference_detected) { return false; } return !dcd; } bool noise_floor_sampled = false; int noise_floor_sample = 0; int noise_floor_buffer[NOISE_FLOOR_SAMPLES] = {0}; void update_noise_floor() { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 if (!dcd) { #if BOARD_MODEL != BOARD_HELTEC32_V4 if (!noise_floor_sampled || current_rssi < noise_floor + CSMA_INFR_THRESHOLD_DB) { #else if ((!noise_floor_sampled || current_rssi < noise_floor + CSMA_INFR_THRESHOLD_DB) || (noise_floor_sampled && (noise_floor < LNA_GD_THRSHLD && current_rssi <= LNA_GD_LIMIT))) { #endif #if HAS_LORA_LNA // Discard invalid samples due to gain variance // during LoRa LNA re-calibration if (current_rssi < noise_floor-LORA_LNA_GVT) { return; } #endif bool sum_noise_floor = false; noise_floor_buffer[noise_floor_sample] = current_rssi; noise_floor_sample = noise_floor_sample+1; if (noise_floor_sample >= NOISE_FLOOR_SAMPLES) { noise_floor_sample %= NOISE_FLOOR_SAMPLES; noise_floor_sampled = true; sum_noise_floor = true; } if (noise_floor_sampled && sum_noise_floor) { noise_floor = 0; for (int ni = 0; ni < NOISE_FLOOR_SAMPLES; ni++) { noise_floor += noise_floor_buffer[ni]; } noise_floor /= NOISE_FLOOR_SAMPLES; } } } #endif } #define LED_ID_TRIG 16 uint8_t led_id_filter = 0; uint32_t interference_start = 0; bool interference_persists = false; void update_modem_status() { #if MCU_VARIANT == MCU_ESP32 portENTER_CRITICAL(&update_lock); #elif MCU_VARIANT == MCU_NRF52 portENTER_CRITICAL(); #endif bool carrier_detected = LoRa->dcd(); current_rssi = LoRa->currentRssi(); last_status_update = millis(); #if MCU_VARIANT == MCU_ESP32 portEXIT_CRITICAL(&update_lock); #elif MCU_VARIANT == MCU_NRF52 portEXIT_CRITICAL(); #endif #if BOARD_MODEL == BOARD_HELTEC32_V4 if (noise_floor > LNA_GD_THRSHLD) { interference_detected = !carrier_detected && (current_rssi > (noise_floor+CSMA_INFR_THRESHOLD_DB)); } else { interference_detected = !carrier_detected && (current_rssi > LNA_GD_LIMIT); } #else interference_detected = !carrier_detected && (current_rssi > (noise_floor+CSMA_INFR_THRESHOLD_DB)); #endif if (interference_detected) { if (led_id_filter < LED_ID_TRIG) { led_id_filter += 1; } } else { if (led_id_filter > 0) {led_id_filter -= 1; } } // Handle potential false interference detection due to // LNA recalibration, antenna swap, moving into new RF // environment or similar. if (interference_detected && current_rssi < CSMA_RFENV_RECAL_LIMIT_DB) { if (!interference_persists) { interference_persists = true; interference_start = millis(); } else { if (millis()-interference_start >= CSMA_RFENV_RECAL_MS) { noise_floor_sampled = false; interference_persists = false; } } } else { interference_persists = false; } if (carrier_detected) { dcd = true; } else { dcd = false; } dcd_led = dcd; if (dcd_led) { led_rx_on(); } else { if (interference_detected) { if (led_id_filter >= LED_ID_TRIG && noise_floor_sampled) { led_id_on(); } } else { if (airtime_lock) { led_indicate_airtime_lock(); } else { led_rx_off(); led_id_off(); } } } } void check_modem_status() { if (millis()-last_status_update >= status_interval_ms) { update_modem_status(); update_noise_floor(); #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 if (dcd) { util_samples[dcd_sample >> 3] |= (1 << (dcd_sample & 7)); } else { util_samples[dcd_sample >> 3] &= ~(1 << (dcd_sample & 7)); } dcd_sample = (dcd_sample+1)%DCD_SAMPLES; if (dcd_sample % UTIL_UPDATE_INTERVAL == 0) { int util_count = 0; for (int ui = 0; ui < DCD_BITFIELD_SIZE; ui++) { uint8_t b = util_samples[ui]; while (b) { util_count += (b & 1); b >>= 1; } } local_channel_util = (float)util_count / (float)DCD_SAMPLES; total_channel_util = local_channel_util + airtime; if (total_channel_util > 1.0) total_channel_util = 1.0; int16_t cb = current_airtime_bin(); uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; } if (total_channel_util > longterm_bins[cb]) longterm_bins[cb] = total_channel_util; longterm_bins[nb] = 0.0; update_airtime(); } #endif } } void validate_status() { #if MCU_VARIANT == MCU_1284P uint8_t boot_flags = OPTIBOOT_MCUSR; uint8_t F_POR = PORF; uint8_t F_BOR = BORF; uint8_t F_WDR = WDRF; #elif MCU_VARIANT == MCU_2560 uint8_t boot_flags = OPTIBOOT_MCUSR; if (boot_flags == 0x00) boot_flags = 0x03; uint8_t F_POR = PORF; uint8_t F_BOR = BORF; uint8_t F_WDR = WDRF; #elif MCU_VARIANT == MCU_ESP32 // TODO: Get ESP32 boot flags uint8_t boot_flags = 0x02; uint8_t F_POR = 0x00; uint8_t F_BOR = 0x00; uint8_t F_WDR = 0x01; #elif MCU_VARIANT == MCU_NRF52 // TODO: Get NRF52 boot flags uint8_t boot_flags = 0x02; uint8_t F_POR = 0x00; uint8_t F_BOR = 0x00; uint8_t F_WDR = 0x01; #endif if (hw_ready || device_init_done) { hw_ready = false; Serial.write("Error, invalid hardware check state\r\n"); #if HAS_DISPLAY if (disp_ready) { device_init_done = true; update_display(); } #endif led_indicate_boot_error(); } if (boot_flags & (1< CSMA_CW_BANDS) { new_cw_band = CSMA_CW_BANDS; } if (new_cw_band != cw_band) { cw_band = (uint8_t)(new_cw_band); cw_min = (cw_band-1) * CSMA_CW_PER_BAND_WINDOWS; cw_max = (cw_band) * CSMA_CW_PER_BAND_WINDOWS - 1; kiss_indicate_csma_stats(); } } #endif void tx_queue_handler() { if (!airtime_lock && queue_height > 0) { if (csma_cw == -1) { csma_cw = random(cw_min, cw_max); cw_wait_target = csma_cw * csma_slot_ms; } if (difs_wait_start == -1) { // DIFS wait not yet started if (medium_free()) { difs_wait_start = millis(); return; } // Set DIFS wait start time else { return; } } // Medium not yet free, continue waiting else { // We are waiting for DIFS or CW to pass if (!medium_free()) { difs_wait_start = -1; cw_wait_start = -1; return; } // Medium became occupied while in DIFS wait, restart waiting when free again else { // Medium is free, so continue waiting if (millis() < difs_wait_start+difs_ms) { return; } // DIFS has not yet passed, continue waiting else { // DIFS has passed, and we are now in CW wait if (cw_wait_start == -1) { cw_wait_start = millis(); return; } // If we haven't started counting CW wait time, do it from now else { // If we are already counting CW wait time, add it to the counter cw_wait_passed += millis()-cw_wait_start; cw_wait_start = millis(); if (cw_wait_passed < cw_wait_target) { return; } // Contention window wait time has not yet passed, continue waiting else { // Wait time has passed, flush the queue bool should_flush = !lora_limit_rate && !lora_guard_rate; if (should_flush) { flush_queue(); } else { pop_queue(); } cw_wait_passed = 0; csma_cw = -1; difs_wait_start = -1; } } } } } } } void work_while_waiting() { loop(); } void loop() { #ifdef HAS_RNS // CBA if (reticulum) { reticulum.loop(); } #ifdef BOUNDARY_MODE // ── Clear bootloop counter once we reach a stable loop iteration ────────── if (bootloop_magic == BOOTLOOP_MAGIC) { bootloop_magic = 0; bootloop_count = 0; Serial.println("[Boundary] Boot stable — bootloop counter cleared"); } // ── Heap + WiFi watchdog ─────────────────────────────────────────────────── // Monitor heap and WiFi health. Auto-reboot on critical conditions: // 1) Internal heap drops below 20KB (WiFi needs ~16KB for RX buffers) // 2) WiFi down for >15s after having been connected (unrecoverable) { static bool _wifi_watchdog_armed = false; // armed once WiFi first connects static uint32_t _wifi_lost_at = 0; // millis() when WiFi first lost static const uint32_t WIFI_GRACE_MS = 15000; // 15s grace before reboot static const uint32_t HEAP_CRITICAL = 20000; // 20KB minimum internal heap // ── Heap pressure check (runs always) ───────────────────────────────── uint32_t free_heap = ESP.getFreeHeap(); if (free_heap < HEAP_CRITICAL) { Serial.printf("\r\n[WATCHDOG] CRITICAL: Free heap %u < %u — REBOOTING\r\n", free_heap, HEAP_CRITICAL); Serial.printf("[WATCHDOG] Min free: %u Max alloc: %u\r\n", ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); Serial.flush(); delay(100); ESP.restart(); } bool wifi_now = wifi_is_connected(); // Arm the watchdog once WiFi has been up at least once if (!_wifi_watchdog_armed && wifi_now) { _wifi_watchdog_armed = true; _wifi_lost_at = 0; } if (_wifi_watchdog_armed && !wifi_now) { if (_wifi_lost_at == 0) { _wifi_lost_at = millis(); Serial.printf("\r\n[WATCHDOG] WiFi lost at %lu ms (grace %lu ms)\r\n", _wifi_lost_at, WIFI_GRACE_MS); Serial.printf("[WATCHDOG] WiFi.status()=%d heap=%u min_heap=%u\r\n", (int)WiFi.status(), free_heap, ESP.getMinFreeHeap()); Serial.flush(); } // Check if grace period expired — unrecoverable, reboot if ((millis() - _wifi_lost_at) >= WIFI_GRACE_MS) { Serial.printf("\r\n[WATCHDOG] WiFi down %lu ms — REBOOTING\r\n", millis() - _wifi_lost_at); Serial.printf("[WATCHDOG] WiFi.status()=%d heap=%u\r\n", (int)WiFi.status(), ESP.getFreeHeap()); Serial.printf("[WATCHDOG] Bridged: L→T=%lu T→L=%lu\r\n", boundary_state.packets_bridged_lora_to_tcp, boundary_state.packets_bridged_tcp_to_lora); Serial.flush(); delay(100); ESP.restart(); } } else if (_wifi_watchdog_armed && wifi_now && _wifi_lost_at != 0) { // WiFi recovered within grace period Serial.printf("[WATCHDOG] WiFi back after %lu ms\r\n", millis() - _wifi_lost_at); _wifi_lost_at = 0; } } // Boundary Mode: poll TCP interfaces for incoming data if (boundary_state.wifi_enabled) { // Start TCP interfaces if WiFi just connected and not yet started if (wifi_is_connected()) { if (tcp_interface_ptr && !tcp_interface_ptr->isStarted()) { tcp_interface_ptr->start(); Serial.println("[Boundary] WiFi connected, TCP backbone started"); } if (local_tcp_interface_ptr && !local_tcp_interface_ptr->isStarted()) { local_tcp_interface_ptr->start(); Serial.println("[Boundary] WiFi connected, local TCP server started"); } } if (tcp_interface_ptr) { tcp_interface_ptr->loop(); } if (local_tcp_interface_ptr) { local_tcp_interface_ptr->loop(); } boundary_state.tcp_connected = (tcp_interface_ptr && tcp_interface_ptr->isConnected()); boundary_state.ap_tcp_connected = (local_tcp_interface_ptr && local_tcp_interface_ptr->isConnected()); boundary_state.wifi_connected = wifi_is_connected(); } #endif #endif if (radio_online) { // Poll for deferred DIO0 interrupt (SPI work moved out of ISR) LoRa->pollDio0(); #if MCU_VARIANT == MCU_ESP32 modem_packet_t *modem_packet = NULL; if(modem_packet_queue && xQueueReceive(modem_packet_queue, &modem_packet, 0) == pdTRUE && modem_packet) { host_write_len = modem_packet->len; last_rssi = modem_packet->rssi; last_snr_raw = modem_packet->snr_raw; memcpy(&pbuf, modem_packet->data, modem_packet->len); free(modem_packet); modem_packet = NULL; kiss_indicate_stat_rssi(); kiss_indicate_stat_snr(); kiss_write_packet(); } airtime_lock = false; if (st_airtime_limit != 0.0 && airtime >= st_airtime_limit) airtime_lock = true; if (lt_airtime_limit != 0.0 && longterm_airtime >= lt_airtime_limit) airtime_lock = true; #elif MCU_VARIANT == MCU_NRF52 modem_packet_t *modem_packet = NULL; if(modem_packet_queue && xQueueReceive(modem_packet_queue, &modem_packet, 0) == pdTRUE && modem_packet) { memcpy(&pbuf, modem_packet->data, modem_packet->len); host_write_len = modem_packet->len; free(modem_packet); modem_packet = NULL; portENTER_CRITICAL(); last_rssi = LoRa->packetRssi(); last_snr_raw = LoRa->packetSnrRaw(); portEXIT_CRITICAL(); kiss_indicate_stat_rssi(); kiss_indicate_stat_snr(); kiss_write_packet(); } airtime_lock = false; if (st_airtime_limit != 0.0 && airtime >= st_airtime_limit) airtime_lock = true; if (lt_airtime_limit != 0.0 && longterm_airtime >= lt_airtime_limit) airtime_lock = true; #endif tx_queue_handler(); check_modem_status(); } else { if (hw_ready) { if (console_active) { #if HAS_CONSOLE console_loop(); #endif } else { led_indicate_standby(); } } else { led_indicate_not_ready(); stopRadio(); } } #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 buffer_serial(); if (!fifo_isempty(&serialFIFO)) serial_poll(); #else if (!fifo_isempty_locked(&serialFIFO)) serial_poll(); #endif #if HAS_DISPLAY if (disp_ready && !display_updating) update_display(); #endif // LED solid when operational on V3/V4 boards (yield to fast blink during white screen) #if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3 if (radio_online && !display_lock_white) { headless_led_solid(); } #endif #if HAS_PMU if (pmu_ready) update_pmu(); #endif #if HAS_BLUETOOTH || HAS_BLE == true if (!console_active && bt_ready) update_bt(); #endif #if HAS_WIFI if (wifi_initialized) update_wifi(); #endif #if HAS_INPUT input_read(); #endif // Feed WDT #if MCU_VARIANT == MCU_ESP32 esp_task_wdt_reset(); #elif MCU_VARIANT == MCU_NRF52 NRF_WDT->RR[0] = WDT_RR_RR_Reload; #endif if (memory_low) { #if PLATFORM == PLATFORM_ESP32 if (esp_get_free_heap_size() < 8192) { kiss_indicate_error(ERROR_MEMORY_LOW); memory_low = false; } else { memory_low = false; } #else kiss_indicate_error(ERROR_MEMORY_LOW); memory_low = false; #endif } } void sleep_now() { #if HAS_SLEEP == true stopRadio(); // TODO: Check this on all platforms #if PLATFORM == PLATFORM_ESP32 #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_XIAO_S3 #if HAS_DISPLAY display_intensity = 0; update_display(true); #endif #endif #if BOARD_MODEL == BOARD_HELTEC32_V4 headless_led_off(); headless_led_detach_pwm(); digitalWrite(LORA_PA_CPS, LOW); digitalWrite(LORA_PA_CSD, LOW); digitalWrite(LORA_PA_PWR_EN, LOW); digitalWrite(Vext, HIGH); #endif #if PIN_DISP_SLEEP >= 0 pinMode(PIN_DISP_SLEEP, OUTPUT); digitalWrite(PIN_DISP_SLEEP, DISP_SLEEP_LEVEL); #endif #if HAS_BLUETOOTH if (bt_state == BT_STATE_CONNECTED) { bt_stop(); delay(100); } #endif esp_sleep_enable_ext0_wakeup(PIN_WAKEUP, WAKEUP_LEVEL); esp_deep_sleep_start(); #elif PLATFORM == PLATFORM_NRF52 #if BOARD_MODEL == BOARD_HELTEC_T114 npset(0,0,0); digitalWrite(PIN_VEXT_EN, LOW); digitalWrite(PIN_T114_TFT_BLGT, HIGH); digitalWrite(PIN_T114_TFT_EN, HIGH); #elif BOARD_MODEL == BOARD_TECHO for (uint8_t i = display_intensity; i > 0; i--) { analogWrite(pin_backlight, i-1); delay(1); } epd_black(true); delay(300); epd_black(true); delay(300); epd_black(false); delay(2000); analogWrite(PIN_VEXT_EN, 0); delay(100); #endif sd_power_gpregret_set(0, 0x6d); nrf_gpio_cfg_sense_input(pin_btn_usr1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); NRF_POWER->SYSTEMOFF = 1; #endif #endif } void button_event(uint8_t event, unsigned long duration) { #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 if (display_blanked) { display_unblank(); } else { #ifdef BOUNDARY_MODE // Boundary Mode button mapping: // >5s = reboot into config mode (clean restart) // >700ms = sleep // short = display unblank if (duration > 5000) { Serial.println("[Boundary] Button hold >5s — rebooting into config mode"); boundary_config_request = BOUNDARY_CONFIG_MAGIC; delay(100); ESP.restart(); } else if (duration > 700) { #if HAS_SLEEP sleep_now(); #endif } else { display_unblank(); } #else // Standard RNode button mapping if (duration > 10000) { #if HAS_CONSOLE #if HAS_BLUETOOTH || HAS_BLE bt_stop(); #endif console_active = true; console_start(); #endif } else if (duration > 5000) { #if HAS_BLUETOOTH || HAS_BLE if (bt_state != BT_STATE_CONNECTED) { bt_enable_pairing(); } #endif } else if (duration > 700) { #if HAS_SLEEP sleep_now(); #endif } else { #if HAS_BLUETOOTH || HAS_BLE if (bt_state != BT_STATE_CONNECTED) { if (bt_state == BT_STATE_OFF) { bt_start(); bt_conf_save(true); } else { bt_stop(); bt_conf_save(false); } } #endif } #endif // BOUNDARY_MODE } #endif } volatile bool serial_polling = false; void serial_poll() { serial_polling = true; #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 while (!fifo_isempty_locked(&serialFIFO)) { #else while (!fifo_isempty(&serialFIFO)) { #endif char sbyte = fifo_pop(&serialFIFO); serial_callback(sbyte); } serial_polling = false; } #if MCU_VARIANT != MCU_ESP32 #define MAX_CYCLES 20 #else #define MAX_CYCLES 10 #endif void buffer_serial() { if (!serial_buffering) { serial_buffering = true; uint8_t c = 0; #if HAS_BLUETOOTH || HAS_BLE == true while ( c < MAX_CYCLES && #if HAS_WIFI ( (bt_state != BT_STATE_CONNECTED && Serial.available()) || (bt_state == BT_STATE_CONNECTED && SerialBT.available()) || (wr_state >= WR_STATE_ON && wifi_remote_available()) ) #else ( (bt_state != BT_STATE_CONNECTED && Serial.available()) || (bt_state == BT_STATE_CONNECTED && SerialBT.available()) ) #endif ) #else while (c < MAX_CYCLES && Serial.available()) #endif { c++; #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 if (!fifo_isfull_locked(&serialFIFO)) { fifo_push_locked(&serialFIFO, Serial.read()); } #elif HAS_BLUETOOTH || HAS_BLE == true || HAS_WIFI #if HAS_BLUETOOTH || HAS_BLE == true if (bt_state == BT_STATE_CONNECTED) { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, SerialBT.read()); } } #if HAS_WIFI else if (wifi_host_is_connected()) { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, wifi_remote_read()); } } #endif else { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, Serial.read()); } } #elif HAS_WIFI if (wifi_host_is_connected()) { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, wifi_remote_read()); } } else { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, Serial.read()); } } #endif #else if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, Serial.read()); } #endif } serial_buffering = false; } } void serial_interrupt_init() { #if MCU_VARIANT == MCU_1284P TCCR3A = 0; TCCR3B = _BV(CS10) | _BV(WGM33)| _BV(WGM32); // Buffer incoming frames every 1ms ICR3 = 16000; TIMSK3 = _BV(ICIE3); #elif MCU_VARIANT == MCU_2560 // TODO: This should probably be updated for // atmega2560 support. Might be source of // reported issues from snh. TCCR3A = 0; TCCR3B = _BV(CS10) | _BV(WGM33)| _BV(WGM32); // Buffer incoming frames every 1ms ICR3 = 16000; TIMSK3 = _BV(ICIE3); #elif MCU_VARIANT == MCU_ESP32 // No interrupt-based polling on ESP32 #endif } #if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 ISR(TIMER3_CAPT_vect) { buffer_serial(); } #endif