diff --git a/build.sh b/build.sh index 4dc7c255..d1a19d49 100755 --- a/build.sh +++ b/build.sh @@ -31,7 +31,7 @@ PARSED_COMMAND_ARGS=() FIRMWARE_VERSION_EXPLICIT=0 OUTPUT_POLICY_EXPLICIT=0 -ENV_VARIANT_SUFFIX_PATTERN='companion_radio_(wifi_mqtt|serial|wifi|usb|ble)(_ps)?(_fem(on|off))?|companion_radio_ethernet|comp_radio_usb|companion_usb|companion_ble|repeater_lora_ota_no_external_sensors|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|repeater_observer_mqtt|repeater_ethernet|room_server_observer_mqtt|room_server_ethernet|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater' +ENV_VARIANT_SUFFIX_PATTERN='companion_radio_(wifi_mqtt|serial|wifi|usb|ble)(_ps)?(_fem(on|off))?|companion_radio_ethernet|comp_radio_usb|companion_usb|companion_ble|repeater_bridge_rs232_serial1_lora_ota_no_external_sensors|repeater_bridge_rs232_serial2_lora_ota_no_external_sensors|repeater_bridge_rs232_lora_ota_no_external_sensors|room_server_lora_ota_no_external_sensors|repeater_lora_ota_no_external_sensors|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|repeater_observer_mqtt|repeater_ethernet|room_server_observer_mqtt|room_server_ethernet|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater' BOARD_MODIFIER_WITHOUT_DISPLAY="_without_display" BOARD_MODIFIER_LOGGING="_logging" BOARD_MODIFIER_TFT="_tft" diff --git a/docs/cli_commands.md b/docs/cli_commands.md index da27cb09..eb325c90 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -567,6 +567,22 @@ send text.flood checking ridge link --- +#### Enable or disable the nRF52 system watchdog +**Usage:** +- `get system.watchdog` +- `set system.watchdog on` +- `set system.watchdog off` + +**Default:** `on` + +**Notes:** +- This nRF52-only hardware watchdog resets the device if the application loop stops for 60 seconds, including an indefinite SoftDevice flash-write wait. +- Enabling takes effect without a reboot. +- The nRF52 hardware cannot stop a watchdog after it has started. Disabling is persisted immediately, then the current firmware stops feeding it so the board performs one watchdog restart within 60 seconds. It remains off after that restart. +- This setting is nRF52-only. The SoftDevice flash deadlock does not apply to ESP32, whose existing watchdog behavior is unchanged. + +--- + #### Estimate and correct repeater time after startup **Usage:** diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ac95ba50..091b14a0 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -503,6 +503,9 @@ void setup() { } void loop() { +#if defined(NRF52_PLATFORM) + board.feedWatchdog(); +#endif the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 7fbcaed1..5a1ce8a2 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -124,6 +124,9 @@ void setup() { } void loop() { +#if defined(NRF52_PLATFORM) + board.feedWatchdog(); +#endif modem->loop(); if (!modem->isActuallyTransmitting()) { diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 960167ed..9537ef07 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -148,6 +148,9 @@ void setup() { } void loop() { +#if defined(NRF52_PLATFORM) + board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0); +#endif // Handle Serial CLI int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 3e339ed0..bbdc9992 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -107,6 +107,9 @@ void setup() { } void loop() { +#if defined(NRF52_PLATFORM) + board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0); +#endif int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 08047a2b..fb33e00d 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -597,6 +597,9 @@ void setup() { } void loop() { +#if defined(NRF52_PLATFORM) + board.feedWatchdog(); +#endif the_mesh.loop(); rtc_clock.tick(); #ifdef HAS_EXTERNAL_WATCHDOG diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a..6fbc8e6d 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -121,6 +121,9 @@ void setup() { } void loop() { +#if defined(NRF52_PLATFORM) + board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0); +#endif int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); diff --git a/platformio.ini b/platformio.ini index f4bbf843..05a50006 100644 --- a/platformio.ini +++ b/platformio.ini @@ -201,13 +201,26 @@ lib_deps = adafruit/Adafruit BME680 Library @ ^2.0.4 adafruit/Adafruit BMP085 Library @ ^1.2.4 -; The VL53L0X and BME680/BSEC stacks prevent some nRF52840 repeater images from fitting -; the safe in-place LoRa OTA window. Space-constrained targets retain GPS, the other -; environmental sensors, and board-native telemetry while omitting these optional sensors. -[nrf52_ota_sensor_trim] +; Lean nRF52 LoRa OTA roles use this complete external-sensor trim. Board-native +; radio, display, buttons, battery monitoring, and other board telemetry remain. +[nrf52_no_external_sensors] build_flags = + -UENV_INCLUDE_GPS + -UENV_INCLUDE_AHTX0 + -UENV_INCLUDE_BME280 + -UENV_INCLUDE_BMP280 + -UENV_INCLUDE_SHTC3 + -UENV_INCLUDE_SHT4X + -UENV_INCLUDE_LPS22HB + -UENV_INCLUDE_INA3221 + -UENV_INCLUDE_INA219 + -UENV_INCLUDE_INA226 + -UENV_INCLUDE_INA260 + -UENV_INCLUDE_MLX90614 -UENV_INCLUDE_VL53L0X -UENV_INCLUDE_BME680 + -UENV_INCLUDE_BMP085 + -UENV_INCLUDE_RAK12035 -UENV_INCLUDE_BME680_BSEC ; ----------------- TESTING --------------------- diff --git a/src/helpers/AtomicFileWriter.cpp b/src/helpers/AtomicFileWriter.cpp new file mode 100644 index 00000000..ff90e93c --- /dev/null +++ b/src/helpers/AtomicFileWriter.cpp @@ -0,0 +1,4 @@ +#include "IdentityStore.h" + +#define ATOMIC_FILE_WRITER_IMPLEMENTATION +#include "AtomicFileWriter.h" diff --git a/src/helpers/AtomicFileWriter.h b/src/helpers/AtomicFileWriter.h new file mode 100644 index 00000000..809b2281 --- /dev/null +++ b/src/helpers/AtomicFileWriter.h @@ -0,0 +1,181 @@ +#pragma once + +#if defined(NRF52_PLATFORM) + +#include +#include +#include + +namespace mesh { + +/** + * Power-loss-safe file replacement for the nRF52 LittleFS backend. + * + * Data is written to a sibling .tmp file. commit() flushes and closes that + * file, reads it back while checking its size and CRC32, and only then uses + * LittleFS rename() to atomically replace the live file. A failed or abandoned + * write removes the temporary file and leaves the previous live file intact. + */ +class AtomicFileWriter { + static const size_t TEMP_PATH_CAPACITY = 96; + + FILESYSTEM* _fs; + const char* _target_path; + char _temp_path[TEMP_PATH_CAPACITY]; + File _file; + size_t _bytes_written; + uint32_t _crc; + bool _opened; + bool _write_ok; + bool _finished; + + static uint32_t updateCRC32(uint32_t crc, const uint8_t* data, size_t len); + void removeTempFile(); + bool validateTempFile(); + +public: + AtomicFileWriter(FILESYSTEM* fs, const char* target_path); + ~AtomicFileWriter(); + AtomicFileWriter(const AtomicFileWriter&) = delete; + AtomicFileWriter& operator=(const AtomicFileWriter&) = delete; + + operator bool() const { return _opened && _write_ok; } + + size_t write(uint8_t value); + size_t write(const uint8_t* data, size_t len); + + /** + * Commit the staged file. content_valid lets callers include format-specific + * checks in the same decision; false always preserves the previous file. + */ + bool commit(bool content_valid = true); + + size_t bytesWritten() const { return _bytes_written; } + const char* tempPath() const { return _temp_path; } +}; + +} // namespace mesh + +#if defined(ATOMIC_FILE_WRITER_IMPLEMENTATION) + +namespace mesh { + +uint32_t AtomicFileWriter::updateCRC32(uint32_t crc, const uint8_t* data, size_t len) { + while (len-- > 0) { + crc ^= *data++; + for (uint8_t bit = 0; bit < 8; bit++) { + crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320UL : 0); + } + } + return crc; +} + +void AtomicFileWriter::removeTempFile() { + if (_temp_path[0] != 0 && _fs->exists(_temp_path)) { + _fs->remove(_temp_path); + } +} + +bool AtomicFileWriter::validateTempFile() { + File verify(*_fs); + if (!verify.open(_temp_path, FILE_O_READ)) return false; + + bool valid = verify.size() == _bytes_written; + uint32_t read_crc = 0xFFFFFFFFUL; + size_t remaining = _bytes_written; + uint8_t buf[64]; + + while (valid && remaining > 0) { + size_t chunk = remaining < sizeof(buf) ? remaining : sizeof(buf); + int count = verify.read(buf, (uint16_t)chunk); + if (count != (int)chunk) { + valid = false; + break; + } + read_crc = updateCRC32(read_crc, buf, chunk); + remaining -= chunk; + } + + verify.close(); + return valid && remaining == 0 && read_crc == _crc; +} + +AtomicFileWriter::AtomicFileWriter(FILESYSTEM* fs, const char* target_path) + : _fs(fs), _target_path(target_path), _file(*fs), _bytes_written(0), + _crc(0xFFFFFFFFUL), _opened(false), _write_ok(true), _finished(false) { + _temp_path[0] = 0; + if (target_path == NULL) { + _write_ok = false; + return; + } + + size_t target_len = strlen(target_path); + if (target_len + sizeof(".tmp") > sizeof(_temp_path)) { + _write_ok = false; + return; + } + + memcpy(_temp_path, target_path, target_len); + memcpy(_temp_path + target_len, ".tmp", sizeof(".tmp")); + + // A reset before a previous commit may leave a harmless stale temp file. + // Never append to it: remove it before opening the new transaction. + removeTempFile(); + _opened = _file.open(_temp_path, FILE_O_WRITE); + _write_ok = _opened; +} + +AtomicFileWriter::~AtomicFileWriter() { + if (_finished) return; + if (_opened) { + _file.close(); + _opened = false; + } + removeTempFile(); +} + +size_t AtomicFileWriter::write(uint8_t value) { + return write(&value, 1); +} + +size_t AtomicFileWriter::write(const uint8_t* data, size_t len) { + if (!_opened || !_write_ok || (data == NULL && len != 0)) { + _write_ok = false; + return 0; + } + + size_t written = _file.write(data, len); + if (written != len) { + _write_ok = false; + return written; + } + + _bytes_written += written; + _crc = updateCRC32(_crc, data, written); + return written; +} + +bool AtomicFileWriter::commit(bool content_valid) { + if (_finished) return false; + + bool success = _opened && _write_ok && content_valid; + if (_opened) { + _file.flush(); + if (_file.size() != _bytes_written) success = false; + _file.close(); + _opened = false; + } + + if (success) success = validateTempFile(); + if (success) success = _fs->rename(_temp_path, _target_path); + if (!success) removeTempFile(); + + _finished = true; + return success; +} + +} // namespace mesh + +#endif // ATOMIC_FILE_WRITER_IMPLEMENTATION + +#endif // NRF52_PLATFORM diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index f16d5ecf..652d68e6 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -1,9 +1,13 @@ #include "ClientACL.h" +#if defined(NRF52_PLATFORM) +#include "AtomicFileWriter.h" +#endif static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1; +#if !defined(NRF52_PLATFORM) static File openWrite(FILESYSTEM* _fs, const char* filename) { - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #if defined(STM32_PLATFORM) _fs->remove(filename); return _fs->open(filename, FILE_O_WRITE); #elif defined(RP2040_PLATFORM) @@ -12,6 +16,7 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) { return _fs->open(filename, "w", true); #endif } +#endif void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { _fs = fs; @@ -61,17 +66,22 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { _fs = fs; +#if defined(NRF52_PLATFORM) + mesh::AtomicFileWriter file(_fs, "/s_contacts"); +#else File file = openWrite(_fs, "/s_contacts"); +#endif if (file) { uint8_t unused[2]; unused[0] = CONTACT_RECORD_VERSION_ALT_PATH; unused[1] = 0; - for (int i = 0; i < num_clients; i++) { + bool success = true; + for (int i = 0; success && i < num_clients; i++) { auto c = &clients[i]; if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted entries, or by filter function - bool success = (file.write(c->id.pub_key, 32) == 32); + success = (file.write(c->id.pub_key, 32) == 32); success = success && (file.write((uint8_t *) &c->permissions, 1) == 1); success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4); success = success && (file.write(unused, 2) == 2); @@ -83,7 +93,13 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { if (!success) break; // write failed } +#if defined(NRF52_PLATFORM) + if (!file.commit(success)) { + MESH_DEBUG_PRINTLN("ERROR: ClientACL::save atomic commit failed"); + } +#else file.close(); +#endif } } @@ -92,6 +108,11 @@ bool ClientACL::clear() { if (_fs->exists("/s_contacts")) { _fs->remove("/s_contacts"); } +#if defined(NRF52_PLATFORM) + if (_fs->exists("/s_contacts.tmp")) { + _fs->remove("/s_contacts.tmp"); + } +#endif memset(clients, 0, sizeof(clients)); num_clients = 0; return true; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 0769af2c..584c6768 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,6 +4,9 @@ #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include "AlertReporter.h" // for alertReporterBannedChannelMatch() +#if defined(NRF52_PLATFORM) +#include "AtomicFileWriter.h" +#endif #include #include #include @@ -804,6 +807,10 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { bool is_upgrade = false; bool loaded = false; + // The hardware main-loop watchdog is opt-out. Older preference files do not + // contain its appended byte, so they safely inherit the enabled default. + _prefs->system_watchdog_enabled = 1; + if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); loaded = true; // new filename } else if (fs->exists("/node_prefs")) { @@ -943,6 +950,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->telemetry_access = TELEMETRY_ACCESS_ALL; _prefs->flood_retry_group_max_path = FLOOD_RETRY_GROUP_MAX_PATH_DEFAULT; _prefs->rx_watchdog_enabled = 0; + _prefs->system_watchdog_enabled = 1; // A remainder larger than the smallest legacy MQTT gap (864) means an old fork // file with the zero-filled gap; detect and recover it below. Anything smaller // (upstream/flex 5-byte tails, or the ~384-byte keymind retry tail) takes the @@ -1198,6 +1206,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->rx_watchdog_enabled, sizeof(_prefs->rx_watchdog_enabled)); } + if (file.available() >= (int)sizeof(_prefs->system_watchdog_enabled)) { + file.read((uint8_t *)&_prefs->system_watchdog_enabled, + sizeof(_prefs->system_watchdog_enabled)); + } } // sanitise bad pref values @@ -1290,6 +1302,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { #endif _prefs->rx_powersaving_enabled = constrain(_prefs->rx_powersaving_enabled, 0, 1); _prefs->rx_watchdog_enabled = constrain(_prefs->rx_watchdog_enabled, 0, 1); + _prefs->system_watchdog_enabled = constrain(_prefs->system_watchdog_enabled, 0, 1); _prefs->rx_ps_level = constrain(_prefs->rx_ps_level, 0, 10); if (_prefs->rx_ps_preamble != 16 && _prefs->rx_ps_preamble != 32) { _prefs->rx_ps_preamble = 0; // 0 = auto (derive from SF) @@ -1302,7 +1315,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } void CommonCLI::savePrefs(FILESYSTEM* fs) { -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#if defined(NRF52_PLATFORM) + mesh::AtomicFileWriter file(fs, "/com_prefs"); +#elif defined(STM32_PLATFORM) fs->remove("/com_prefs"); File file = fs->open("/com_prefs", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) @@ -1428,9 +1443,17 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { sizeof(_prefs->flood_retry_group_max_path)); // 853 file.write((uint8_t *)&_prefs->rx_watchdog_enabled, sizeof(_prefs->rx_watchdog_enabled)); // 854 - // next: 855 + file.write((uint8_t *)&_prefs->system_watchdog_enabled, + sizeof(_prefs->system_watchdog_enabled)); // 855 + // next: 856 +#if defined(NRF52_PLATFORM) + if (!file.commit()) { + MESH_DEBUG_PRINTLN("ERROR: savePrefs atomic commit failed"); + } +#else file.close(); +#endif } #ifdef WITH_MQTT_BRIDGE // Observer config (MQTT/WiFi/timezone/SNMP/alert) is persisted separately. The @@ -1702,7 +1725,9 @@ void CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) { MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs from newer firmware, not overwriting"); return; } -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#if defined(NRF52_PLATFORM) + mesh::AtomicFileWriter file(fs, "/mqtt_prefs"); +#elif defined(STM32_PLATFORM) fs->remove("/mqtt_prefs"); File file = fs->open("/mqtt_prefs", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) @@ -1718,7 +1743,13 @@ void CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) { hdr.payload_len = (uint16_t)sizeof(_mqtt_prefs); file.write((uint8_t *)&hdr, sizeof(hdr)); file.write((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); +#if defined(NRF52_PLATFORM) + if (!file.commit()) { + MESH_DEBUG_PRINTLN("ERROR: saveMQTTPrefs atomic commit failed"); + } +#else file.close(); +#endif } } @@ -2219,6 +2250,23 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); sprintf(reply, "OK - %d.%d%%", a_int, a_frac); } + } else if (memcmp(config, "system.watchdog ", 16) == 0) { +#if defined(NRF52_PLATFORM) + const char* value = &config[16]; + if (strcmp(value, "on") == 0) { + _prefs->system_watchdog_enabled = 1; + savePrefs(); + strcpy(reply, "OK - system watchdog enabled"); + } else if (strcmp(value, "off") == 0) { + _prefs->system_watchdog_enabled = 0; + savePrefs(); + strcpy(reply, "OK - disabled; restarting within 60s"); + } else { + strcpy(reply, "Error: use set system.watchdog on|off"); + } +#else + strcpy(reply, "Error: unsupported on this platform"); +#endif } else if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); savePrefs(); @@ -3035,6 +3083,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep int dc_int = (int)dc; int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); sprintf(reply, "> %d.%d%%", dc_int, dc_frac); + } else if (strcmp(config, "system.watchdog") == 0) { +#if defined(NRF52_PLATFORM) + sprintf(reply, "> %s", _prefs->system_watchdog_enabled ? "on" : "off"); +#else + strcpy(reply, "Error: unsupported on this platform"); +#endif } else if (memcmp(config, "af", 2) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index a46782b8..0bcc6878 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -219,6 +219,7 @@ struct NodePrefs { // persisted to file char battery_alert_region[31]; // named scope for low-battery floods; empty = no alert scope uint8_t flood_retry_group_max_path; // PAYLOAD_TYPE_GRP_DATA retry path gate; 0xFF = use only the general gate uint8_t rx_watchdog_enabled; // repeater RX-inactivity reboot watchdog (boolean) + uint8_t system_watchdog_enabled; // nRF52 main-loop hardware watchdog (boolean; default on) }; #ifdef WITH_MQTT_BRIDGE diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index e7a99bf1..b8140002 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -5,6 +5,7 @@ #include #include "ble_gap.h" #include "ble_hci.h" +#include #include #ifdef USE_TINYUSB #include @@ -15,6 +16,19 @@ static uint16_t ota_conn_handle = BLE_CONN_HANDLE_INVALID; static bool ota_active = false; static bool ota_ble_started = false; +// A normal internal-flash operation completes in milliseconds. One minute is +// deliberately generous for other legitimate application work while still +// recovering an indefinitely blocked SoftDevice flash wait without requiring +// a physical power cycle. Builds can override this, or set it to 0 to disable +// the watchdog for diagnostics. +#ifndef NRF52_WATCHDOG_TIMEOUT_SECONDS +#define NRF52_WATCHDOG_TIMEOUT_SECONDS 60UL +#endif + +#if NRF52_WATCHDOG_TIMEOUT_SECONDS > 131071UL +#error "NRF52_WATCHDOG_TIMEOUT_SECONDS exceeds the nRF52 WDT counter range" +#endif + static void format_ota_reply(char reply[]) { uint8_t mac_addr[6]; memset(mac_addr, 0, sizeof(mac_addr)); @@ -41,9 +55,42 @@ void NRF52Board::begin() { startup_reason = BD_STARTUP_NORMAL; } -#ifdef NRF52_POWER_MANAGEMENT -#include "nrf.h" +void NRF52Board::feedWatchdog(bool enabled) { +#if NRF52_WATCHDOG_TIMEOUT_SECONDS > 0 + // The nRF52 watchdog cannot be stopped after it starts. When the persisted + // setting is turned off, deliberately stop reloading it; the resulting + // watchdog reset is the only software-only way to return it to the stopped + // state. On that next boot the disabled preference prevents it starting. + if (!enabled) return; + const bool running = NRF_WDT->RUNSTATUS != 0; + if (!running) { + // Keep running during CPU sleep: the flash-driver failure this protects + // against sleeps in sd_app_evt_wait(). Pause while halted so breakpoints + // do not reset a board being debugged. + NRF_WDT->CONFIG = + (WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos) | + (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos); + NRF_WDT->CRV = (uint32_t)(NRF52_WATCHDOG_TIMEOUT_SECONDS * 32768UL); + NRF_WDT->RREN = WDT_RREN_RR0_Msk; + NRF_WDT->TASKS_START = 1; + } + + // Ordinarily only RR0 is enabled. If a bootloader left the watchdog running + // with a different reload channel, service every enabled channel rather + // than assuming ownership of RR0 and immediately entering a reset loop. + const uint32_t enabled_channels = NRF_WDT->RREN & 0xFFUL; + for (uint8_t channel = 0; channel < 8; channel++) { + if (enabled_channels & (1UL << channel)) { + NRF_WDT->RR[channel] = WDT_RR_RR_Reload; + } + } +#else + (void)enabled; +#endif +} + +#ifdef NRF52_POWER_MANAGEMENT // Power Management global variables uint32_t g_nrf52_reset_reason = 0; // Reset/Startup reason uint8_t g_nrf52_shutdown_reason = 0; // Shutdown reason diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index ec653e4b..55e6890d 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -47,6 +47,11 @@ protected: public: NRF52Board(char *otaname) : ota_name(otaname) {} virtual void begin(); + // Feed the hardware main-loop watchdog. The first call starts it, so normal + // setup (including filesystem mount and radio initialization) is not timed. + // A loop blocked forever in SoftDevice-backed flash I/O will stop feeding it + // and be reset automatically. + void feedWatchdog(bool enabled = true); virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 69c9fb1a..620320d4 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -1,6 +1,9 @@ #include "RegionMap.h" #include #include +#if defined(NRF52_PLATFORM) +#include +#endif #include // helper class for region map exporter, we emulate Stream with a safe buffer writer. @@ -59,8 +62,9 @@ static const char* skip_hash(const char* name) { return RegionNameUtils::canonical(name); } +#if !defined(NRF52_PLATFORM) static File openWrite(FILESYSTEM* _fs, const char* filename) { - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #if defined(STM32_PLATFORM) _fs->remove(filename); return _fs->open(filename, FILE_O_WRITE); #elif defined(RP2040_PLATFORM) @@ -69,6 +73,7 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) { return _fs->open(filename, "w", true); #endif } +#endif bool RegionMap::load(FILESYSTEM* _fs, const char* path) { if (_fs->exists(path ? path : "/regions2")) { @@ -185,7 +190,12 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) { } bool RegionMap::save(FILESYSTEM* _fs, const char* path) { - File file = openWrite(_fs, path ? path : "/regions2"); + const char* target_path = path ? path : "/regions2"; +#if defined(NRF52_PLATFORM) + mesh::AtomicFileWriter file(_fs, target_path); +#else + File file = openWrite(_fs, target_path); +#endif if (file) { uint8_t pad[128]; memset(pad, 0, sizeof(pad)); @@ -208,7 +218,11 @@ bool RegionMap::save(FILESYSTEM* _fs, const char* path) { if (!success) break; // write failed } } +#if defined(NRF52_PLATFORM) + success = file.commit(success); +#else file.close(); +#endif return success; } return false; // failed diff --git a/test/test_atomic_file_writer/test_atomic_file_writer.cpp b/test/test_atomic_file_writer/test_atomic_file_writer.cpp new file mode 100644 index 00000000..049b08f8 --- /dev/null +++ b/test/test_atomic_file_writer/test_atomic_file_writer.cpp @@ -0,0 +1,211 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class FakeFilesystem; + +class FakeFile { + FakeFilesystem* _fs; + std::string _path; + size_t _position; + bool _open; + bool _write_mode; + +public: + explicit FakeFile(FakeFilesystem& fs); + + bool open(const char* path, uint8_t mode); + size_t write(const uint8_t* data, size_t len); + int read(void* data, uint16_t len); + uint32_t size() const; + void flush() {} + void close() { _open = false; } + explicit operator bool() const { return _open; } +}; + +class FakeFilesystem { +public: + std::unordered_map> files; + size_t max_write = std::numeric_limits::max(); + bool fail_read_open = false; + bool fail_rename = false; + unsigned rename_calls = 0; + + bool exists(const char* path) const { + return files.find(path) != files.end(); + } + + bool remove(const char* path) { + return files.erase(path) != 0; + } + + bool rename(const char* from, const char* to) { + rename_calls++; + if (fail_rename) return false; + auto source = files.find(from); + if (source == files.end()) return false; + files[to] = source->second; + files.erase(source); + return true; + } +}; + +enum { + FILE_O_READ = 0, + FILE_O_WRITE = 1, +}; + +FakeFile::FakeFile(FakeFilesystem& fs) + : _fs(&fs), _position(0), _open(false), _write_mode(false) {} + +bool FakeFile::open(const char* path, uint8_t mode) { + _path = path; + _write_mode = mode == FILE_O_WRITE; + if (!_write_mode && (_fs->fail_read_open || !_fs->exists(path))) return false; + if (_write_mode) { + auto& contents = _fs->files[_path]; + _position = contents.size(); + } else { + _position = 0; + } + _open = true; + return true; +} + +size_t FakeFile::write(const uint8_t* data, size_t len) { + if (!_open || !_write_mode) return 0; + size_t written = std::min(len, _fs->max_write); + auto& contents = _fs->files[_path]; + contents.insert(contents.end(), data, data + written); + _position += written; + return written; +} + +int FakeFile::read(void* data, uint16_t len) { + if (!_open || _write_mode) return -1; + const auto& contents = _fs->files[_path]; + size_t available = contents.size() - std::min(_position, contents.size()); + size_t count = std::min(len, available); + if (count > 0) memcpy(data, contents.data() + _position, count); + _position += count; + return (int)count; +} + +uint32_t FakeFile::size() const { + auto found = _fs->files.find(_path); + return found == _fs->files.end() ? 0 : (uint32_t)found->second.size(); +} + +#define NRF52_PLATFORM 1 +#define FILESYSTEM FakeFilesystem +using File = FakeFile; +#define ATOMIC_FILE_WRITER_IMPLEMENTATION +#include + +static std::vector bytes(const char* text) { + return std::vector(text, text + strlen(text)); +} + +TEST(AtomicFileWriter, ReplacesLiveFileOnlyAfterVerifiedCommit) { + FakeFilesystem fs; + fs.files["/prefs"] = bytes("old"); + + mesh::AtomicFileWriter writer(&fs, "/prefs"); + ASSERT_TRUE(writer); + const uint8_t replacement[] = {'n', 'e', 'w'}; + EXPECT_EQ(writer.write(replacement, sizeof(replacement)), sizeof(replacement)); + EXPECT_EQ(fs.files["/prefs"], bytes("old")); + + EXPECT_TRUE(writer.commit()); + EXPECT_EQ(fs.files["/prefs"], bytes("new")); + EXPECT_FALSE(fs.exists("/prefs.tmp")); + EXPECT_EQ(fs.rename_calls, 1u); +} + +TEST(AtomicFileWriter, PartialWritePreservesLiveFile) { + FakeFilesystem fs; + fs.files["/prefs"] = bytes("old"); + fs.max_write = 2; + + mesh::AtomicFileWriter writer(&fs, "/prefs"); + const uint8_t replacement[] = {'n', 'e', 'w'}; + EXPECT_EQ(writer.write(replacement, sizeof(replacement)), 2u); + EXPECT_FALSE(writer.commit()); + + EXPECT_EQ(fs.files["/prefs"], bytes("old")); + EXPECT_FALSE(fs.exists("/prefs.tmp")); + EXPECT_EQ(fs.rename_calls, 0u); +} + +TEST(AtomicFileWriter, ReadbackMismatchPreservesLiveFile) { + FakeFilesystem fs; + fs.files["/prefs"] = bytes("old"); + + mesh::AtomicFileWriter writer(&fs, "/prefs"); + const uint8_t replacement[] = {'n', 'e', 'w'}; + ASSERT_EQ(writer.write(replacement, sizeof(replacement)), sizeof(replacement)); + fs.files["/prefs.tmp"][1] ^= 0x01; + + EXPECT_FALSE(writer.commit()); + EXPECT_EQ(fs.files["/prefs"], bytes("old")); + EXPECT_FALSE(fs.exists("/prefs.tmp")); + EXPECT_EQ(fs.rename_calls, 0u); +} + +TEST(AtomicFileWriter, CallerValidationFailurePreservesLiveFile) { + FakeFilesystem fs; + fs.files["/prefs"] = bytes("old"); + + mesh::AtomicFileWriter writer(&fs, "/prefs"); + const uint8_t replacement[] = {'n', 'e', 'w'}; + ASSERT_EQ(writer.write(replacement, sizeof(replacement)), sizeof(replacement)); + + EXPECT_FALSE(writer.commit(false)); + EXPECT_EQ(fs.files["/prefs"], bytes("old")); + EXPECT_FALSE(fs.exists("/prefs.tmp")); + EXPECT_EQ(fs.rename_calls, 0u); +} + +TEST(AtomicFileWriter, RenameFailurePreservesLiveFileAndCleansTemp) { + FakeFilesystem fs; + fs.files["/prefs"] = bytes("old"); + fs.fail_rename = true; + + mesh::AtomicFileWriter writer(&fs, "/prefs"); + const uint8_t replacement[] = {'n', 'e', 'w'}; + ASSERT_EQ(writer.write(replacement, sizeof(replacement)), sizeof(replacement)); + + EXPECT_FALSE(writer.commit()); + EXPECT_EQ(fs.files["/prefs"], bytes("old")); + EXPECT_FALSE(fs.exists("/prefs.tmp")); + EXPECT_EQ(fs.rename_calls, 1u); +} + +TEST(AtomicFileWriter, AbandonedWriteAndStaleTempAreCleaned) { + FakeFilesystem fs; + fs.files["/prefs"] = bytes("old"); + fs.files["/prefs.tmp"] = bytes("stale"); + + { + mesh::AtomicFileWriter writer(&fs, "/prefs"); + ASSERT_TRUE(writer); + EXPECT_TRUE(fs.files["/prefs.tmp"].empty()); + const uint8_t replacement[] = {'n', 'e', 'w'}; + ASSERT_EQ(writer.write(replacement, sizeof(replacement)), sizeof(replacement)); + } + + EXPECT_EQ(fs.files["/prefs"], bytes("old")); + EXPECT_FALSE(fs.exists("/prefs.tmp")); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 10b622f7..aea6cfae 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -36,14 +36,8 @@ lib_deps = extends = GAT562_30S_Mesh_Kit build_flags = ${GAT562_30S_Mesh_Kit.build_flags} - ; Keep the repeater image inside the nRF52 in-place LoRa OTA workspace. The - ; room-server and companion targets retain the complete optional sensor set. - ${nrf52_ota_sensor_trim.build_flags} - -UENV_INCLUDE_MLX90614 - -UENV_INCLUDE_BMP085 - -UENV_INCLUDE_INA219 - -UENV_INCLUDE_INA226 - -UENV_INCLUDE_INA260 + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"GAT562 Repeater"' -D ADVERT_LAT=0.0 @@ -56,6 +50,25 @@ build_src_filter = ${GAT562_30S_Mesh_Kit.build_src_filter} + +<../examples/simple_repeater> +; Explicit lean alternative to the full-sensor, non-OTA repeater above. +[env:GAT562_30S_Mesh_Kit_repeater_lora_ota_no_external_sensors] +extends = GAT562_30S_Mesh_Kit +build_flags = + ${GAT562_30S_Mesh_Kit.build_flags} + ${nrf52_no_external_sensors.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"GAT562 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${GAT562_30S_Mesh_Kit.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${nrf52_lora_ota.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + [env:GAT562_30S_Mesh_Kit_room_server] extends = GAT562_30S_Mesh_Kit diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index fb0a3b03..a2467bf2 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -32,6 +32,8 @@ lib_deps = extends = GAT562_Mesh_Tracker_Pro build_flags = ${GAT562_Mesh_Tracker_Pro.build_flags} + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"GAT562 Repeater"' -D ADVERT_LAT=0.0 @@ -44,6 +46,25 @@ build_src_filter = ${GAT562_Mesh_Tracker_Pro.build_src_filter} + +<../examples/simple_repeater> +; Explicit lean alternative to the full-sensor, non-OTA repeater above. +[env:GAT562_Mesh_Tracker_Pro_repeater_lora_ota_no_external_sensors] +extends = GAT562_Mesh_Tracker_Pro +build_flags = + ${GAT562_Mesh_Tracker_Pro.build_flags} + ${nrf52_no_external_sensors.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"GAT562 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${GAT562_Mesh_Tracker_Pro.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${nrf52_lora_ota.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + [env:GAT562_Mesh_Tracker_Pro_room_server] extends = GAT562_Mesh_Tracker_Pro diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index ed9301f2..132a6612 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -67,16 +67,8 @@ build_src_filter = ${Heltec_t096.build_src_filter} build_flags = ${Heltec_t096.build_flags} - ; Keep the repeater image inside the nRF52 in-place LoRa OTA workspace. The - ; sensor and companion targets retain the complete optional sensor set. - ${nrf52_ota_sensor_trim.build_flags} - -UENV_INCLUDE_BMP085 - -UENV_INCLUDE_MLX90614 - -UENV_INCLUDE_INA226 - -UENV_INCLUDE_INA260 - -UENV_INCLUDE_INA219 - -UENV_INCLUDE_LPS22HB - -UENV_INCLUDE_BMP280 + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling -D ADVERT_NAME='"Heltec_t096 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -85,19 +77,29 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; Explicit lean alternative to the full-sensor, non-OTA repeater above. +[env:Heltec_t096_repeater_lora_ota_no_external_sensors] +extends = Heltec_t096 +build_src_filter = ${Heltec_t096.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${Heltec_t096.build_flags} + ${nrf52_no_external_sensors.build_flags} + -D ADVERT_NAME='"Heltec_t096 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +lib_deps = + ${nrf52_lora_ota.lib_deps} + bodmer/TFT_eSPI @ ^2.4.31 + [env:Heltec_t096_repeater_bridge_rs232] extends = Heltec_t096 build_flags = ${Heltec_t096.build_flags} - ; The RS232 bridge shares the repeater's strict in-place LoRa OTA budget. - ${nrf52_ota_sensor_trim.build_flags} - -UENV_INCLUDE_BMP085 - -UENV_INCLUDE_MLX90614 - -UENV_INCLUDE_INA226 - -UENV_INCLUDE_INA260 - -UENV_INCLUDE_INA219 - -UENV_INCLUDE_LPS22HB - -UENV_INCLUDE_BMP280 + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling -D ADVERT_NAME='"RS232 Bridge"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -113,6 +115,27 @@ build_src_filter = ${Heltec_t096.build_src_filter} + +<../examples/simple_repeater> +; Explicit lean alternative to the full-sensor, non-OTA RS232 bridge above. +[env:Heltec_t096_repeater_bridge_rs232_lora_ota_no_external_sensors] +extends = Heltec_t096 +build_flags = + ${Heltec_t096.build_flags} + ${nrf52_no_external_sensors.build_flags} + -D ADVERT_NAME='"RS232 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_RS232_BRIDGE=Serial2 + -D WITH_RS232_BRIDGE_RX=9 + -D WITH_RS232_BRIDGE_TX=10 +build_src_filter = ${Heltec_t096.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${nrf52_lora_ota.lib_deps} + bodmer/TFT_eSPI @ ^2.4.31 + [env:Heltec_t096_room_server] extends = Heltec_t096 build_src_filter = ${Heltec_t096.build_src_filter} diff --git a/variants/heltec_t1/platformio.ini b/variants/heltec_t1/platformio.ini index c617a509..00db3367 100644 --- a/variants/heltec_t1/platformio.ini +++ b/variants/heltec_t1/platformio.ini @@ -36,16 +36,26 @@ build_src_filter = ${Heltec_t1.build_src_filter} build_flags = ${Heltec_t1.build_flags} - ; Keep the repeater image inside the nRF52 in-place LoRa OTA workspace. The - ; room-server and companion targets retain the complete optional sensor set. - ${nrf52_ota_sensor_trim.build_flags} - -UENV_INCLUDE_BMP085 - -UENV_INCLUDE_MLX90614 - -UENV_INCLUDE_INA226 - -UENV_INCLUDE_INA260 - -UENV_INCLUDE_INA219 - -UENV_INCLUDE_LPS22HB - -UENV_INCLUDE_BMP280 + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling + -D ADVERT_NAME='"Heltec_t1 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + +; Explicit lean alternative to the full-sensor, non-OTA repeater above. +[env:Heltec_t1_repeater_lora_ota_no_external_sensors] +extends = Heltec_t1 +extra_scripts = ${nrf52_lora_ota.extra_scripts} +build_src_filter = ${Heltec_t1.build_src_filter} + + + +<../examples/simple_repeater> +build_flags = + ${Heltec_t1.build_flags} + ${nrf52_no_external_sensors.build_flags} -D ADVERT_NAME='"Heltec_t1 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -54,8 +64,9 @@ build_flags = -D ENABLE_OTA=1 -D OTA_FLASH_STORE=1 -D OTA_FOLDER_SERIAL -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 +lib_deps = + ${nrf52_lora_ota.lib_deps} + bodmer/TFT_eSPI @ ^2.4.31 [env:Heltec_t1_room_server] extends = Heltec_t1 diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index a470a36b..d97eba8c 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -164,7 +164,8 @@ build_src_filter = ${Heltec_t114_with_display.build_src_filter} build_flags = ${Heltec_t114_with_display.build_flags} - ${nrf52_ota_sensor_trim.build_flags} + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling -D ADVERT_NAME='"Heltec_T114 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -173,6 +174,23 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; Explicit lean alternative to the full-sensor, non-OTA repeater above. +[env:Heltec_t114_repeater_lora_ota_no_external_sensors] +extends = Heltec_t114_with_display +build_src_filter = ${Heltec_t114_with_display.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${Heltec_t114_with_display.build_flags} + ${nrf52_no_external_sensors.build_flags} + -D ADVERT_NAME='"Heltec_T114 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +lib_deps = + ${nrf52_lora_ota.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + [env:Heltec_t114_repeater_bridge_rs232] extends = Heltec_t114 build_flags = diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index fa53ba20..eb648ee0 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -30,6 +30,28 @@ extends = rak3401 extra_scripts = ${nrf52_lora_ota.extra_scripts} build_flags = ${rak3401.build_flags} + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RAK3401 1W Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + ;-D MESH_PACKET_LOGGING=1 + ;-D MESH_DEBUG=1 +build_src_filter = ${rak3401.build_src_filter} + + + + + +<../examples/simple_repeater> + +; Explicit lean alternative to the full-sensor, non-OTA repeater above. +[env:RAK_3401_repeater_lora_ota_no_external_sensors] +extends = rak3401 +extra_scripts = ${nrf52_lora_ota.extra_scripts} +build_flags = + ${rak3401.build_flags} + ${nrf52_no_external_sensors.build_flags} -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"RAK3401 1W Repeater"' -D ADVERT_LAT=0.0 @@ -39,12 +61,13 @@ build_flags = -D ENABLE_OTA=1 -D OTA_FLASH_STORE=1 -D OTA_FOLDER_SERIAL - ;-D MESH_PACKET_LOGGING=1 - ;-D MESH_DEBUG=1 build_src_filter = ${rak3401.build_src_filter} + + +<../examples/simple_repeater> +lib_deps = + ${nrf52_lora_ota.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 [env:RAK_3401_room_server] extends = rak3401 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 798d2640..daf8c43a 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -72,23 +72,7 @@ extends = rak4631 extra_scripts = ${nrf52_lora_ota.extra_scripts} build_flags = ${rak4631.build_flags} - -UENV_INCLUDE_GPS - -UENV_INCLUDE_AHTX0 - -UENV_INCLUDE_BME280 - -UENV_INCLUDE_BMP280 - -UENV_INCLUDE_SHTC3 - -UENV_INCLUDE_SHT4X - -UENV_INCLUDE_LPS22HB - -UENV_INCLUDE_INA3221 - -UENV_INCLUDE_INA219 - -UENV_INCLUDE_INA226 - -UENV_INCLUDE_INA260 - -UENV_INCLUDE_MLX90614 - -UENV_INCLUDE_VL53L0X - -UENV_INCLUDE_BME680 - -UENV_INCLUDE_BMP085 - -UENV_INCLUDE_RAK12035 - -UENV_INCLUDE_BME680_BSEC + ${nrf52_no_external_sensors.build_flags} -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"RAK4631 LoRa OTA No Ext Sensors"' -D ADVERT_LAT=0.0 @@ -130,7 +114,8 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} - ${nrf52_ota_sensor_trim.build_flags} + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"RS232 Bridge"' -D ADVERT_LAT=0.0 @@ -150,11 +135,35 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> +; Explicit lean alternative to the full-sensor, non-OTA Serial1 bridge above. +[env:RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + ${nrf52_no_external_sensors.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RS232 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_RS232_BRIDGE=Serial1 + -D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX + -D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX +build_src_filter = ${rak4631.build_src_filter} + + + + + +<../examples/simple_repeater> +lib_deps = + ${nrf52_lora_ota.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + [env:RAK_4631_repeater_bridge_rs232_serial2] extends = rak4631 build_flags = ${rak4631.build_flags} - ${nrf52_ota_sensor_trim.build_flags} + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"RS232 Bridge"' -D ADVERT_LAT=0.0 @@ -174,10 +183,35 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> +; Explicit lean alternative to the full-sensor, non-OTA Serial2 bridge above. +[env:RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + ${nrf52_no_external_sensors.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RS232 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_RS232_BRIDGE=Serial2 + -D WITH_RS232_BRIDGE_RX=PIN_SERIAL2_RX + -D WITH_RS232_BRIDGE_TX=PIN_SERIAL2_TX +build_src_filter = ${rak4631.build_src_filter} + + + + + +<../examples/simple_repeater> +lib_deps = + ${nrf52_lora_ota.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + [env:RAK_4631_room_server] extends = rak4631 build_flags = ${rak4631.build_flags} + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"Test Room"' -D ADVERT_LAT=0.0 @@ -190,6 +224,25 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_room_server> +; Explicit lean alternative to the full-sensor, non-OTA room server above. +[env:RAK_4631_room_server_lora_ota_no_external_sensors] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + ${nrf52_no_external_sensors.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Test Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${rak4631.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${nrf52_lora_ota.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + [env:RAK_4631_room_server_ethernet] extends = rak4631 build_flags = @@ -309,6 +362,8 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} + -UENABLE_OTA + -D DISABLE_LORA_OTA=1 ; Full sensor image exceeds the safe nRF52 in-place OTA ceiling -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"RAK4631 Sensor"' -D ADVERT_LAT=0.0