diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 945daac7..fbb5cc6f 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -81,6 +81,7 @@ jobs: sudo apt-get update sudo apt-get install -y libssl-dev python3 -B test/test_companion_tx_routing.py -v + python3 -B test/test_companion_discovery.py -v python3 -B test/test_companion_preferences_transaction.py -v python3 -B test/test_companion_radio_settings_transaction.py -v python3 -B test/test_companion_prefs_transactions.py -v diff --git a/build.sh b/build.sh index 9db250b9..54c839da 100755 --- a/build.sh +++ b/build.sh @@ -3782,9 +3782,9 @@ apply_companion_radio_full_profile() { # A few BLE recipes list only their BLE implementation instead of the full # ESP32 helper directory. Add the WiFi transport explicitly in that case. - if ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/esp32/*.cpp" \ - && ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/esp32/SerialWifiInterface.cpp"; then - append_platformio_build_src_filter "+" + if ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/wifi/*.cpp" \ + && ! pio_env_option_contains "$pio_env_name" build_src_filter "helpers/wifi/SerialWifiInterface.cpp"; then + append_platformio_build_src_filter "+" fi # WiFi recipes can have the inverse narrow filter. Preserve the existing diff --git a/build_as_lib.py b/build_as_lib.py index d8e95378..fbc7c15a 100644 --- a/build_as_lib.py +++ b/build_as_lib.py @@ -20,10 +20,12 @@ for item in menv.get("CPPDEFINES", []): src_filter.append("+") elif item == "ESP32": src_filter.append("+") + src_filter.append("+") elif item == "NRF52_PLATFORM": src_filter.append("+") elif item == "RP2040_PLATFORM": src_filter.append("+") + src_filter.append("+") # DISPLAY HANDLING elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS": diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 88569147..2ae187f6 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1174,7 +1174,7 @@ capacity. Remote use follows the existing administrator-command permissions. **Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR` -**Default:** `869.525,250,11,5` +**Default:** `869.618,62.5,8,5` **Note:** Requires reboot to apply. If RXPS is enabled and the saved minimum level/preamble cannot safely cover the new radio timing, the command reply @@ -1345,7 +1345,7 @@ preamble. LoRa OTA treats v1.17.1.5 as that capability boundary. **Parameters:** - `frequency`: Frequency in MHz -**Default:** `869.525` +**Default:** `869.618` **Note:** Requires reboot to apply @@ -2275,7 +2275,7 @@ installed images need an updated build. See the [setting-dispatch audit](cli_set **Parameters:** - `value`: Direct transmit delay factor (0-2) -**Default:** `0.2` +**Default:** `0.3` (Repeater) - `0.2` (Room Server, Sensor) **Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them. @@ -2426,7 +2426,7 @@ reduce unnecessary recoveries when no traffic is expected. **Parameters:** - `hours`: Interval in hours (3-168) -**Default:** `12` (Repeater) - `0` (Sensor) +**Default:** `47` (Repeater, Room Server) - `0`, disabled (Sensor) --- @@ -2438,7 +2438,12 @@ reduce unnecessary recoveries when no traffic is expected. **Parameters:** - `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240) -**Default:** `0` +**Default:** `2` on a factory-fresh node, then `0` (disabled) once configured. + +**Note:** A new install ships with a 2 minute zero-hop advert interval. Saving +any setting resets an interval below the 60 minute minimum to `0`, on the +assumption that the node has now been deliberately configured. To keep zero-hop +adverts running, set an explicit value in the 60-240 range. --- diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 236fb0df..b28e1ddf 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -588,12 +588,12 @@ a raw byte count. - Registered application/community namespaces occupy `0x0100`-`0xFEFF`; the remaining nonzero ranges are reserved for internal or development use. See the [Registered data_type values](#registered-data_type-values) table below. **Limits**: -- Maximum payload length is `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 167` bytes. +- Maximum payload length is `MAX_GROUP_DATA_LENGTH = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3 = 165` bytes. - Larger payloads are rejected with `PACKET_ERROR` (`ERR_CODE_ILLEGAL_ARG`). **Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) with one of: - `ERR_CODE_NOT_FOUND` (2) - unknown `channel_idx` -- `ERR_CODE_ILLEGAL_ARG` (6) - invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH` +- `ERR_CODE_ILLEGAL_ARG` (6) - invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_GROUP_DATA_LENGTH` - `ERR_CODE_TABLE_FULL` (3) - outbound send queue is full; retry later **Inbound datagrams** are delivered to the host via `RESP_CODE_CHANNEL_DATA_RECV` (0x1B); see [Receive Channel Data Datagram](#receive-channel-data-datagram). diff --git a/docs/faq.md b/docs/faq.md index d9e3bf2c..e67a7f81 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -621,8 +621,8 @@ Mac: python3 should be already installed. Then it should be the same for all platforms: ``` -python3 -m venv meshcore -cd meshcore && source bin/activate +python3 -m venv meshcore-venv +cd meshcore-venv && source bin/activate pip install -U platformio git clone https://github.com/meshcore-dev/MeshCore.git cd MeshCore diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index cec20e90..a747374f 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -190,11 +190,19 @@ Power management status can be queried via the CLI: | `get pwrmgt.bootreason` | Returns reset and shutdown reason strings | | `get pwrmgt.bootmv` | Returns boot voltage in millivolts | -On boards without power management enabled, all commands except `get pwrmgt.support` return: +On boards without power management enabled, `get pwrmgt.source` and +`get pwrmgt.bootmv` return: ``` ERROR: Power management not supported ``` +`get pwrmgt.support` returns `unsupported`. `get pwrmgt.bootreason` is not +compiled out at all and answers on every board: `getResetReason()` and +`getShutdownReason()` are virtuals on the base board class, so a board that +does not override them reports `Not available` rather than an error. ESP32 +boards override the reset half with `esp_reset_reason()`, so they return a real +reset reason and `Not available` for the shutdown reason. + ## Debug Output When `MESH_DEBUG=1` is enabled, the power management module outputs: diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index f07bf0b7..2298ffea 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -249,11 +249,6 @@ set lon {longitude} ``` Sets your advertisement map longitude. (decimal degrees) -``` -set dutycycle {percent} -``` -Sets the transmit duty cycle limit (1-100%). Example: `set dutycycle 10` for 10%. - ``` set af {air-time-factor} ``` diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 4491f93b..3a38a4ba 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -825,6 +825,13 @@ bool DataStore::loadPrefsInt(const char *filename, + sizeof(loaded_prefs.bluetooth_stealth_peer_type) + sizeof(loaded_prefs.bluetooth_stealth_peer) + sizeof(loaded_prefs.bluetooth_stealth_mode), + 226, // prior 215-byte image plus radio timing, interference, AGC and timezone +#ifdef TBEAM_1W + 233, // fan mode and low/high thermistor thresholds +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + 323, // Pico W additionally persists its SSID/password (ESP32 uses NVS) +#endif }; const uint32_t prefs_size = file.size(); bool known_size = false; @@ -939,6 +946,23 @@ bool DataStore::loadPrefsInt(const char *filename, sizeof(loaded_prefs.bluetooth_stealth_peer)); // 208 readOptionalField(&loaded_prefs.bluetooth_stealth_mode, sizeof(loaded_prefs.bluetooth_stealth_mode)); // 214 + readOptionalField(&loaded_prefs.tx_delay_factor, sizeof(loaded_prefs.tx_delay_factor)); + readOptionalField(&loaded_prefs.direct_tx_delay_factor, sizeof(loaded_prefs.direct_tx_delay_factor)); + readOptionalField(&loaded_prefs.interference_threshold, sizeof(loaded_prefs.interference_threshold)); + readOptionalField(&loaded_prefs.agc_reset_interval, sizeof(loaded_prefs.agc_reset_interval)); + readOptionalField(&loaded_prefs.tz_offset, sizeof(loaded_prefs.tz_offset)); +#ifdef TBEAM_1W + readOptionalField(loaded_prefs.fan_mode, sizeof(loaded_prefs.fan_mode)); + readOptionalField(&loaded_prefs.fan_lo, sizeof(loaded_prefs.fan_lo)); + readOptionalField(&loaded_prefs.fan_hi, sizeof(loaded_prefs.fan_hi)); + loaded_prefs.fan_mode[sizeof(loaded_prefs.fan_mode) - 1] = 0; +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + readOptionalField(loaded_prefs.wifi_ssid, sizeof(loaded_prefs.wifi_ssid)); + readOptionalField(loaded_prefs.wifi_pwd, sizeof(loaded_prefs.wifi_pwd)); + loaded_prefs.wifi_ssid[sizeof(loaded_prefs.wifi_ssid) - 1] = 0; + loaded_prefs.wifi_pwd[sizeof(loaded_prefs.wifi_pwd) - 1] = 0; +#endif // Any bytes left over form only part of a historically appended field. // Preserve the file and defaults rather than treating that tail as EOF. @@ -1065,6 +1089,31 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou sizeof(_prefs.bluetooth_stealth_mode)) == sizeof(_prefs.bluetooth_stealth_mode); + success = success && file.write((uint8_t *)&_prefs.tx_delay_factor, + sizeof(_prefs.tx_delay_factor)) == sizeof(_prefs.tx_delay_factor); + success = success && file.write((uint8_t *)&_prefs.direct_tx_delay_factor, + sizeof(_prefs.direct_tx_delay_factor)) == sizeof(_prefs.direct_tx_delay_factor); + success = success && file.write((uint8_t *)&_prefs.interference_threshold, + sizeof(_prefs.interference_threshold)) == sizeof(_prefs.interference_threshold); + success = success && file.write((uint8_t *)&_prefs.agc_reset_interval, + sizeof(_prefs.agc_reset_interval)) == sizeof(_prefs.agc_reset_interval); + success = success && file.write((uint8_t *)&_prefs.tz_offset, + sizeof(_prefs.tz_offset)) == sizeof(_prefs.tz_offset); +#ifdef TBEAM_1W + success = success && file.write((uint8_t *)_prefs.fan_mode, + sizeof(_prefs.fan_mode)) == sizeof(_prefs.fan_mode); + success = success && file.write((uint8_t *)&_prefs.fan_lo, + sizeof(_prefs.fan_lo)) == sizeof(_prefs.fan_lo); + success = success && file.write((uint8_t *)&_prefs.fan_hi, + sizeof(_prefs.fan_hi)) == sizeof(_prefs.fan_hi); +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + success = success && file.write((uint8_t *)_prefs.wifi_ssid, + sizeof(_prefs.wifi_ssid)) == sizeof(_prefs.wifi_ssid); + success = success && file.write((uint8_t *)_prefs.wifi_pwd, + sizeof(_prefs.wifi_pwd)) == sizeof(_prefs.wifi_pwd); +#endif + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) success = file.commit(success); if (!success) MESH_DEBUG_PRINTLN("DataStore: atomic preferences write failed"); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2d7ecfdf..69e9b0b3 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -8,6 +8,9 @@ #endif #include // needed for PlatformIO +#ifdef ENABLE_WIFI_INTERFACE +#include +#endif #include #include #include @@ -291,6 +294,10 @@ static mesh::Packet* emergency_client_repeat_packet; #define ERR_CODE_FILE_IO_ERROR 5 #define ERR_CODE_ILLEGAL_ARG 6 +// Copied from simple_repeater (could probably be shared) +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + #define MAX_SIGN_DATA_LEN (8 * 1024) // 8K static constexpr unsigned long SIGN_SESSION_TIMEOUT_MILLIS = 120000; @@ -571,7 +578,7 @@ uint32_t MyMesh::getCADFailMaxDuration() const { } int MyMesh::getInterferenceThreshold() const { - return 0; // disabled for now, until currentRSSI() problem is resolved + return _prefs.interference_threshold; } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { @@ -580,11 +587,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } @@ -788,6 +795,60 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { + if (!nodes || max_num <= 0) return 0; + if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; + if (max_num > disc_nodes_count) max_num = disc_nodes_count; + + for (int i = 0; i < max_num; i++) { + nodes[i] = discovered_nodes[i]; + } + return max_num; +} + +bool MyMesh::requestRepeatersDiscovery() { + uint8_t cmd_bytes[6]; + cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only + cmd_bytes[1] = 0xFF; // Repeaters + getRNG()->random(&cmd_bytes[2], 4); // tag + disc_nodes_count = 0; + disc_node_req_active = false; + memcpy(&disc_node_req_tag, &cmd_bytes[2], sizeof(disc_node_req_tag)); + mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes)); + if (req) { + sendZeroHop(req); + disc_node_req_active = true; + disc_node_req_at = millis(); + return disc_node_req_active; + } + return false; +} + +void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) { + if (!disc_node_req_active + || static_cast(millis() - disc_node_req_at) >= 30000UL + || (p_len < 14) + || ((payload[0] & 0xF0) != CTL_TYPE_NODE_DISCOVER_RESP) + || (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE) + || (memcmp(&payload[2], &disc_node_req_tag, 4))) { + return; + } + memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8); + discovered_nodes[disc_nodes_count].type = payload[0] & 0xF; + discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0; + discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR(); + ContactInfo* c = lookupContactByPubKey(&payload[6], 8); + if (c != NULL) { + StrHelper::strncpy(discovered_nodes[disc_nodes_count].name, c->name, + sizeof(discovered_nodes[disc_nodes_count].name)); + } else { + discovered_nodes[disc_nodes_count].name[0] = 0; + } + disc_nodes_count ++; +} +#endif + #if COMPANION_FEATURE_TEXT_TERMINAL void MyMesh::onContactVisit(const ContactInfo& contact) { if (contact.type == ADV_TYPE_NONE) return; @@ -1453,6 +1514,9 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); +#endif int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(packet->getSNR() * 4); @@ -1620,6 +1684,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _temp_radio_failures = 0; _temp_radio_applied = false; #endif + cli_command[0] = 0; offline_queue_len = 0; offline_queue_head = 0; #if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM) @@ -1642,7 +1707,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe send_unscoped = false; // defaults - _prefs.airtime_factor = 1.0; // one half + _prefs.airtime_factor = 1.0; + _prefs.tx_delay_factor = 0.5f; + _prefs.direct_tx_delay_factor = 0.2f; strcpy(_prefs.node_name, "NONAME"); _prefs.freq = LORA_FREQ; _prefs.sf = LORA_SF; @@ -1656,7 +1723,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.gps_interval = 0; // Use the default 1-second fix-processing interval _prefs.autoadd_config = DEFAULT_AUTOADD_CONFIG; _prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE; +#ifdef RADIO_FEM_TXGAIN + _prefs.radio_fem_txgain = RADIO_FEM_TXGAIN; +#else _prefs.radio_fem_txgain = 0; +#endif #ifdef DEFAULT_RX_DELAY_BASE _prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE; #endif @@ -1865,6 +1936,10 @@ void MyMesh::begin(bool has_display, bool radio_available) { // sanitise bad pref values _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); + if (!isfinite(_prefs.tx_delay_factor)) _prefs.tx_delay_factor = 0.5f; + if (!isfinite(_prefs.direct_tx_delay_factor)) _prefs.direct_tx_delay_factor = 0.2f; + _prefs.tx_delay_factor = constrain(_prefs.tx_delay_factor, 0, 2.0f); + _prefs.direct_tx_delay_factor = constrain(_prefs.direct_tx_delay_factor, 0, 2.0f); if (!isfinite(_prefs.airtime_factor)) _prefs.airtime_factor = 1.0f; // `set dutycycle 1` stores factor 99; the legacy `set af` input limit is // narrower, but must not truncate an accepted duty cycle on restart. @@ -4519,8 +4594,8 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx } else if (data_type == DATA_TYPE_RESERVED) { writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } else if (payload_len > MAX_CHANNEL_DATA_LENGTH) { - MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH); + } else if (payload_len > MAX_GROUP_DATA_LENGTH) { + MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_GROUP_DATA_LENGTH); writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) { writeOKFrame(); @@ -8406,7 +8481,8 @@ static bool isCompanionRadioPrefsCommand(const char* command) { static const char* const exact_commands[] = { "get radio", "get freq", "get af", "get dutycycle", "get tx", - "get rxdelay", "get path.hash.mode", "get multi.acks" + "get rxdelay", "get path.hash.mode", "get multi.acks", + "get txdelay", "get direct.txdelay", "get int.thresh", "get agc.reset.interval" }; for (const char* candidate : exact_commands) { if (strcmp(command, candidate) == 0) return true; @@ -8414,7 +8490,8 @@ static bool isCompanionRadioPrefsCommand(const char* command) { static const char* const set_prefixes[] = { "set radio ", "set af ", "set dutycycle ", "set rxdelay ", - "set path.hash.mode ", "set multi.acks " + "set path.hash.mode ", "set multi.acks ", "set txdelay ", + "set direct.txdelay ", "set int.thresh ", "set agc.reset.interval " }; for (const char* prefix : set_prefixes) { if (strncmp(command, prefix, strlen(prefix)) == 0) return true; @@ -8612,6 +8689,10 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, const uint8_t previous_cr = _prefs.cr; const float previous_af = _prefs.airtime_factor; const float previous_rxdelay = _prefs.rx_delay_base; + const float previous_txdelay = _prefs.tx_delay_factor; + const float previous_direct_txdelay = _prefs.direct_tx_delay_factor; + const uint8_t previous_interference = _prefs.interference_threshold; + const uint8_t previous_agc = _prefs.agc_reset_interval; const uint8_t previous_hash_mode = _prefs.path_hash_mode; const uint8_t previous_multi_acks = _prefs.multi_acks; const uint32_t previous_rx_us = _prefs.rx_ps_rx_us; @@ -8640,6 +8721,10 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, _prefs.cr = previous_cr; _prefs.airtime_factor = previous_af; _prefs.rx_delay_base = previous_rxdelay; + _prefs.tx_delay_factor = previous_txdelay; + _prefs.direct_tx_delay_factor = previous_direct_txdelay; + _prefs.interference_threshold = previous_interference; + _prefs.agc_reset_interval = previous_agc; _prefs.path_hash_mode = previous_hash_mode; _prefs.multi_acks = previous_multi_acks; _prefs.rx_ps_rx_us = previous_rx_us; @@ -8691,13 +8776,92 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, return true; } - return false; +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + if (strcmp(command, "get wifi.ssid") == 0) { + const char* ssid = _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID; + snprintf(reply, reply_capacity, "> %s", *ssid ? ssid : "(not set)"); + return true; + } + if (strcmp(command, "get wifi.enabled") == 0) { + snprintf(reply, reply_capacity, "> %d", _prefs.wifi_enabled); + return true; + } + if (strcmp(command, "get wifi.status") == 0 || strcmp(command, "get wifi.ip") == 0) { + const bool connected = WiFi.status() == WL_CONNECTED; + if (strcmp(command, "get wifi.status") == 0) { + snprintf(reply, reply_capacity, "> %s", connected ? "connected" : "disconnected"); + } else { + snprintf(reply, reply_capacity, "> %s", connected ? WiFi.localIP().toString().c_str() : "(not connected)"); + } + return true; + } + const bool set_ssid = strncmp(command, "set wifi.ssid ", 14) == 0; + const bool set_pwd = strncmp(command, "set wifi.pwd ", 13) == 0; + const bool set_enabled = strncmp(command, "set wifi.enabled ", 17) == 0; + const bool clear_wifi = strcmp(command, "set wifi.clear") == 0; + if (set_ssid || set_pwd || set_enabled || clear_wifi) { + char old_ssid[sizeof(_prefs.wifi_ssid)], old_pwd[sizeof(_prefs.wifi_pwd)]; + memcpy(old_ssid, _prefs.wifi_ssid, sizeof(old_ssid)); + memcpy(old_pwd, _prefs.wifi_pwd, sizeof(old_pwd)); + const uint8_t old_enabled = _prefs.wifi_enabled; + const char* value = command + (set_ssid ? 14 : set_pwd ? 13 : set_enabled ? 17 : 0); + int32_t enabled = 0; + if ((set_ssid && strlen(value) >= sizeof(_prefs.wifi_ssid)) + || (set_pwd && strlen(value) >= sizeof(_prefs.wifi_pwd)) + || (set_enabled && (!mesh::cli::parseIntegerStrict(value, enabled) || enabled < 0 || enabled > 1))) { + strcpy(reply, "Error: invalid WiFi setting"); + return true; + } + if (set_ssid) strcpy(_prefs.wifi_ssid, value); + if (set_pwd) strcpy(_prefs.wifi_pwd, value); + if (set_enabled) _prefs.wifi_enabled = enabled; + if (clear_wifi) { _prefs.wifi_ssid[0] = 0; _prefs.wifi_pwd[0] = 0; } + if (savePrefs()) strcpy(reply, "OK - reboot to apply WiFi settings"); + else { + memcpy(_prefs.wifi_ssid, old_ssid, sizeof(old_ssid)); + memcpy(_prefs.wifi_pwd, old_pwd, sizeof(old_pwd)); + _prefs.wifi_enabled = old_enabled; + strcpy(reply, "Error: WiFi setting could not be saved"); + } + memset(old_pwd, 0, sizeof(old_pwd)); + return true; + } +#endif + + if (strcmp(command, "get tz.offset") == 0) { + sprintf(reply, "> %d", _prefs.tz_offset); + return true; + } + if (strncmp(command, "set tz.offset ", 14) == 0) { + int32_t tz; + if (!mesh::cli::parseIntegerStrict(command + 14, tz) || tz < -12 || tz > 14) { + strcpy(reply, "Error, must be from -12 to +14"); + } else { + const int8_t previous = _prefs.tz_offset; + _prefs.tz_offset = tz; + if (savePrefs()) strcpy(reply, "OK"); + else { + _prefs.tz_offset = previous; + strcpy(reply, "Error: timezone could not be saved"); + } + } + return true; + } + + return false; // not handled } void MyMesh::checkCLIRescueCmd() { Stream& output = mesh::usbTerminalPort(); Stream& input = mesh::usbCompanionPort(); int len = strlen(cli_command); + // `cli_command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(cli_command) and the loop below would + // then index past the buffer, so clamp defensively. + if (len >= (int)sizeof(cli_command)) { + cli_command[0] = 0; + len = 0; + } while (input.available() && len < sizeof(cli_command)-1) { char c = input.read(); if (c != '\n') { @@ -8706,8 +8870,9 @@ void MyMesh::checkCLIRescueCmd() { } output.print(c); // echo } - if (len == sizeof(cli_command)-1) { // command buffer full - cli_command[sizeof(cli_command)-1] = '\r'; + if (len == sizeof(cli_command)-1) { // buffer full: treat as a completed line + cli_command[sizeof(cli_command)-2] = '\r'; // place end-of-line marker inside the buffer + cli_command[sizeof(cli_command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && cli_command[len - 1] == '\r') { // received complete line @@ -9051,6 +9216,11 @@ void MyMesh::loop() { checkCLIRescueCmd(); } else { checkSerialInterface(); +#if defined(ENABLE_WIFI_INTERFACE) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE) + // RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a + // display + long-press), so serve config commands on the otherwise unused USB serial + checkCLIRescueCmd(); +#endif } servicePendingRadioParamApply(); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 08898b0d..2bbb947b 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -122,6 +122,16 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +struct DiscoveredNode { + uint8_t pubkey_prefix[9]; + float snr_in; + float snr_out; + char name[32]; + uint8_t type; +}; +#endif + class MyMesh : public BaseChatMesh, public DataStoreHost, public UIShutdownGuard #if COMPANION_FEATURE_TEXT_TERMINAL , public ContactVisitor @@ -241,6 +251,11 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + bool requestRepeatersDiscovery(); + int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); +#endif + protected: #if COMPANION_FEATURE_TEMP_RADIO bool isTempRadioActive() const override { @@ -256,6 +271,9 @@ protected: #ifdef WITH_MQTT_BRIDGE uint32_t getRadioWatchdogMillis() const override { return 0; } #endif + int getAGCResetInterval() const override { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds + } int calcRxDelay(float score, uint32_t air_time) const override; uint32_t getRetransmitDelay(const mesh::Packet *packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; @@ -671,6 +689,21 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table + +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + #ifdef UI_RECENT_LIST_SIZE + #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE + #else + #define DISCOVERED_NODES_TABLE_SIZE 4 + #endif + DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept + uint32_t disc_node_req_tag = 0; + uint32_t disc_node_req_at = 0; + bool disc_node_req_active = false; + uint32_t disc_nodes_count = 0; + + void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); +#endif }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index f929b428..5a0ed6a0 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,5 +1,9 @@ #pragma once +#if defined(ENABLE_WIFI_INTERFACE) && !defined(WIFI_SSID) +#define WIFI_SSID "" +#endif + #include #include @@ -81,6 +85,20 @@ public: mesh::companion::BLUETOOTH_PEER_ADDRESS_NONE; uint8_t bluetooth_stealth_peer[mesh::companion::BLUETOOTH_MAC_BYTES] = {}; uint8_t bluetooth_stealth_mode = mesh::companion::BLUETOOTH_STEALTH_OFF; + float tx_delay_factor = 0.5f; + float direct_tx_delay_factor = 0.2f; + uint8_t interference_threshold = 0; + uint8_t agc_reset_interval = 0; // seconds / 4 + int8_t tz_offset = 0; +#ifdef TBEAM_1W + char fan_mode[5] = "auto"; + uint8_t fan_lo = 30; + uint8_t fan_hi = 36; +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + char wifi_ssid[33] = {}; + char wifi_pwd[64] = {}; +#endif private: class RadioPrefs : public CommonRadioPrefs { @@ -93,14 +111,23 @@ private: def("sf", _parent->sf); def("cr", _parent->cr); def("cad", _parent->cad_enabled); + def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->radio_fem_rxgain); def("fem_txgain", _parent->radio_fem_txgain); def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); + def("f_txdelay", _parent->tx_delay_factor); + def("d_txdelay", _parent->direct_tx_delay_factor); + def("agc_int", _parent->agc_reset_interval); def("hash_mode", _parent->path_hash_mode); def("multi_ack", _parent->multi_acks); +#ifdef TBEAM_1W + def("fan", _parent->fan_mode, sizeof(_parent->fan_mode)); + def("fan_lo", _parent->fan_lo); + def("fan_hi", _parent->fan_hi); +#endif } public: @@ -127,17 +154,11 @@ private: markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } - void setAirtimeFactor(float value) override { - _parent->airtime_factor = value; - markDirty(); - } - bool isCadEnabled() const override { return _parent->cad_enabled != 0; } - void setCadEnabled(bool enabled) override { - _parent->cad_enabled = enabled ? 1 : 0; - markDirty(); - } - uint8_t getIntThresh() const override { return 0; } - void setIntThresh(uint8_t) override { } + void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } + bool isCadEnabled() const override { return _parent->cad_enabled; } + void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } + uint8_t getIntThresh() const override { return _parent->interference_threshold; } + void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } void setRxGain(uint8_t value) override { _parent->rx_boosted_gain = value; @@ -149,26 +170,20 @@ private: markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } - void setRxDelay(float value) override { - _parent->rx_delay_base = value; - markDirty(); - } - uint8_t getAgcResetInt() const override { return 0; } - void setAgcResetInt(uint8_t) override { } + void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } + uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t value) override { _parent->path_hash_mode = value; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } - void setMultiAcks(uint8_t value) override { - _parent->multi_acks = value; - markDirty(); - } - float getFloodTxDelay() const override { return 0.5f; } - void setFloodTxDelay(float) override { } - float getDirectTxDelay() const override { return 0.2f; } - void setDirectTxDelay(float) override { } + void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); } + float getFloodTxDelay() const override { return _parent->tx_delay_factor; } + void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); } + float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; } + void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); } uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; } void setFEMRxGain(uint8_t value) override { _parent->radio_fem_rxgain = value; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 95a15ebe..a8a99eea 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -53,19 +53,29 @@ MultiSerialInterface interface_manager; // include nrf52 bluetooth interface #include SerialBLEInterface bluetooth_interface; + #elif defined(RP2040_PLATFORM) + // include rp2040 (Pico W / CYW43) bluetooth interface + #include + SerialBLEInterface bluetooth_interface; #else #error "SerialBLEInterface is not defined for this platform" #endif #endif // include wifi interface -#ifdef WIFI_SSID +#if defined(ENABLE_WIFI_INTERFACE) || defined(WIFI_SSID) + #ifndef WIFI_SSID + #define WIFI_SSID "" + #endif + #ifndef WIFI_PWD + #define WIFI_PWD "" + #endif #ifndef TCP_PORT #define TCP_PORT 5000 #endif #ifdef ESP32 // include esp32 wifi interface - #include + #include #include #include #include @@ -86,6 +96,19 @@ MultiSerialInterface interface_manager; #ifndef WIFI_PWD #define WIFI_PWD "" #endif + #elif defined(RP2040_PLATFORM) + #include + SerialWifiInterface wifi_interface; + static uint32_t last_wifi_reconnect_attempt = 0; + static bool pico_wifi_active = false; + static char pico_wifi_ssid[33] = {}; + static char pico_wifi_pwd[64] = {}; + #ifndef WIFI_RETRY_INTERVAL + #define WIFI_RETRY_INTERVAL 30000UL + #endif + #ifndef WIFI_RETRY_TIMEOUT + #define WIFI_RETRY_TIMEOUT 5000UL + #endif #else #error "SerialWifiInterface is not defined for this platform" #endif @@ -2795,7 +2818,7 @@ void setup() { #endif // add wifi interface -#ifdef WIFI_SSID +#if defined(ESP32) && defined(WIFI_SSID) wifi_interface.setSessionChangedCallback(cancelCompanionWiFiSession, nullptr); #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) if (companionTransportWiFiActiveAtBoot()) { @@ -2859,6 +2882,30 @@ void setup() { #endif // add usb interface +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + wifi_interface.setSessionChangedCallback([](void*) { + if (interface_manager.isReplyRouteFor(&wifi_interface)) { + the_mesh.cancelSerialResponseStream(); + } + the_mesh.cancelSerialOperationsForRoute(&wifi_interface); + interface_manager.forgetReplyRouteForDisconnected(&wifi_interface); + }, nullptr); + const auto* wifi_prefs = the_mesh.getNodePrefs(); + StrHelper::strncpy(pico_wifi_ssid, + wifi_prefs->wifi_ssid[0] ? wifi_prefs->wifi_ssid : WIFI_SSID, + sizeof(pico_wifi_ssid)); + StrHelper::strncpy(pico_wifi_pwd, + wifi_prefs->wifi_ssid[0] ? wifi_prefs->wifi_pwd : WIFI_PWD, + sizeof(pico_wifi_pwd)); + pico_wifi_active = wifi_prefs->wifi_enabled && pico_wifi_ssid[0]; + if (pico_wifi_active) { + WiFi.setTimeout(WIFI_RETRY_TIMEOUT); + WiFi.beginNoBlock(pico_wifi_ssid, pico_wifi_pwd); + last_wifi_reconnect_attempt = millis(); + wifi_interface.begin(TCP_PORT); + interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); + } +#endif #if defined(ENABLE_USB_INTERFACE) #if COMPANION_FEATURE_USB_MOTA_SOURCE usb_serial_interface.begin(mesh::usbCompanionPort(), @@ -2953,6 +3000,15 @@ void setup() { } void loop() { +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + if (pico_wifi_active && WiFi.status() != WL_CONNECTED + && static_cast(millis() - last_wifi_reconnect_attempt) + >= WIFI_RETRY_INTERVAL) { + WiFi.setTimeout(WIFI_RETRY_TIMEOUT); + WiFi.beginNoBlock(pico_wifi_ssid, pico_wifi_pwd); + last_wifi_reconnect_attempt = millis(); + } +#endif mesh::wireless::control().service(millis()); #if defined(NRF52_PLATFORM) board.feedWatchdog(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index f600927b..ebd6ccdb 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -16,7 +16,7 @@ #include "../CompanionWiFi.h" #include "target.h" #include -#ifdef WIFI_SSID +#if defined(ENABLE_WIFI_INTERFACE) || defined(WIFI_SSID) #include #endif #if UI_WIFI_SETUP_HOME_PAGE == 1 @@ -28,10 +28,6 @@ #include #endif -#ifndef UI_TZ_OFFSET - #define UI_TZ_OFFSET 0 -#endif - #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif @@ -406,6 +402,9 @@ class HomeScreen : public UIScreen { #endif #if UI_WIFI_SETUP_HOME_PAGE == 1 WIFI_SETUP, +#endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + DISCOVERY, #endif Count // keep as last }; @@ -423,6 +422,12 @@ class HomeScreen : public UIScreen { uint32_t _uptime_last_millis; uint64_t _uptime_millis; AdvertPath recent[UI_RECENT_LIST_SIZE]; +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; + uint32_t discovery_req_time = 0; + bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) +#endif + int renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -829,7 +834,7 @@ public: #ifdef UI_SHOW_CLOCK display.setTextSize(3); uint32_t now = _rtc->getCurrentTime(); - int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ... + int8_t tz = _node_prefs->tz_offset; now += (int32_t)tz * 3600; DateTime dt (now); sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute()); @@ -1099,6 +1104,40 @@ public: } else if (_page == HomePage::WIFI_SETUP) { drawCompanionWiFiSetupPage(display); #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + } else if (_page == HomePage::DISCOVERY) { + int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); + display.setColor(UIColor::primary_txt); + int y = 20; + for (int i = 0; i < count; i++, y += 11) { + char name[32]; + auto a = &discovered[i]; + if ((a->name[0] == 0) || !discovery_disp_names) { + mesh::Utils::toHex(name, a->pubkey_prefix, 4); + } else { + StrHelper::strncpy(name, a->name, sizeof(name)); + } + char filtered_name[sizeof(name)]; + char snr_s[24]; + if (strlen(name) <= 8) { // display snr_o + snprintf(snr_s, sizeof(snr_s), "%02.1f>%02.1f", a->snr_out, a->snr_in); + } else { + snprintf(snr_s, sizeof(snr_s), "%02.1f", a->snr_in); + } + int snr_width = display.getTextWidth(snr_s); + int max_name_width = display.width() - snr_width - 1; + display.translateUTF8ToBlocks(filtered_name, name, sizeof(filtered_name)); + display.drawTextEllipsized(0, y, max_name_width, filtered_name); + display.setCursor(display.width() - snr_width - 1, y); + display.print(snr_s); + } + if (discovery_req_time && static_cast(millis() - discovery_req_time) < 5000) { + return 1000; // more frequent updates just after req + } else if (count < UI_RECENT_LIST_SIZE -1) { // show only 5 sec after last disc + y = 10 + 11 * UI_RECENT_LIST_SIZE; + display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); + } +#endif #ifndef UI_NO_HIBERNATE } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); @@ -1141,6 +1180,11 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + if (_page == HomePage::DISCOVERY) { + _task->showAlert("Repeater disc", 800); + } +#endif return true; } #ifdef COMPANION_EXCLUSIVE_WIFI_BLE @@ -1223,6 +1267,19 @@ public: return true; } #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + if (key == KEY_ENTER && _page == HomePage::DISCOVERY) { + if (!discovery_req_time || static_cast(millis() - discovery_req_time) >= 5000) { + the_mesh.requestRepeatersDiscovery(); + discovery_req_time = millis(); + } + return true; + } + if (key == KEY_SELECT && _page == HomePage::DISCOVERY) { + discovery_disp_names = !discovery_disp_names; + return true; + } +#endif #ifndef UI_NO_HIBERNATE if (key == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index b14cf70c..d2b6df5f 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -8,7 +8,7 @@ #include "target.h" #include "u8g2_icons.h" -#ifdef WIFI_SSID +#if defined(ENABLE_WIFI_INTERFACE) || defined(WIFI_SSID) #include #endif @@ -181,7 +181,7 @@ public: display.setCursor(0, 19); display.print(tmp); - #ifdef WIFI_SSID + #ifdef ENABLE_WIFI_INTERFACE IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index b1bf87b1..c95c2b25 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -171,6 +171,13 @@ void loop() { #endif Stream& console = mesh::usbConsolePort(); int len = strlen(command); + // `command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(command) and the loop below would then + // index past the buffer, so clamp defensively. + if (len >= (int)sizeof(command)) { + command[0] = 0; + len = 0; + } while (usb_ready && console.available() && len < sizeof(command)-1) { char c = console.read(); if (c != '\n') { @@ -179,8 +186,9 @@ void loop() { } console.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer + command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && command[len - 1] == '\r') { // received complete line diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 76c13250..b0f6d239 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -642,6 +642,13 @@ public: BaseChatMesh::loop(); int len = strlen(command); + // `command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(command) and the loop below would then + // index past the buffer, so clamp defensively. + if (len >= (int)sizeof(command)) { + command[0] = 0; + len = 0; + } while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -650,8 +657,9 @@ public: } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer + command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && command[len - 1] == '\r') { // received complete line diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index e8ff788d..19253b13 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -165,6 +165,13 @@ void loop() { #endif Stream& console = mesh::usbConsolePort(); int len = strlen(command); + // `command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(command) and the loop below would then + // index past the buffer, so clamp defensively. + if (len >= (int)sizeof(command)) { + command[0] = 0; + len = 0; + } while (usb_ready && console.available() && len < sizeof(command)-1) { char c = console.read(); if (c != '\n') { @@ -173,8 +180,9 @@ void loop() { } console.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer + command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && command[len - 1] == '\r') { // received complete line diff --git a/platformio.ini b/platformio.ini index 4e0f3a92..64cad5a0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -146,7 +146,7 @@ platform_packages = ; use internal fork that includes patch to ble stack to prevent firmware lockup during rapid connect/disconnect ; https://github.com/meshcore-dev/MeshCore/pull/1177 ; https://github.com/meshcore-dev/MeshCore/pull/1295 - framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 + framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301665b40959682252911e57b11df3ee651a platformio/toolchain-gccarmnoneeabi@^1.140201.0 extra_scripts = create-uf2.py pre:scripts/nrf52_usb_power_fix.py diff --git a/src/MeshCore.h b/src/MeshCore.h index 4f73429e..c8f9e749 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -228,6 +228,7 @@ public: #endif // Power management interface (boards with power management override these) + virtual bool isPwrMgtInitialised() const { return false; } virtual bool isExternalPowered() { return false; } virtual bool isUsbDataConnected() { return false; } // True when the device is enumerated by a USB host, even if its serial port @@ -235,6 +236,7 @@ public: // that cannot distinguish a computer from USB power. virtual bool isUsbHostConnected() { return isUsbDataConnected(); } virtual uint16_t getBootVoltage() { return 0; } + virtual bool getWakeLpcompSupported() const { return false; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 8986c17e..2982bf62 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -1,4 +1,5 @@ #include "ConfigSerializer.h" +#include // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not) bool ConfigSerializer::saveSerial(Stream& s) { Context context(&s, OP::WRITE); diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 91f3d2c5..8c484096 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -263,7 +263,7 @@ const char* NRF52Board::getShutdownReasonString(uint8_t reason) { } bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { - initPowerMgr(); + pwrmgtInit(); if (config == nullptr) return true; @@ -381,22 +381,19 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled; NRF_LPCOMP->TASKS_START = 1; - // Wait for comparator to settle before entering SYSTEMOFF + // Wait for comparator to settle for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) { delayMicroseconds(50); } if (refsel == 7) { - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel); + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=ARef)", ain_channel); } else if (refsel <= 6) { - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)", - ain_channel, refsel + 1); + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/8 VDD)", ain_channel, refsel + 1); } else { uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1); - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)", - ain_channel, ref_num); + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/16 VDD)", ain_channel, ref_num); } - } void NRF52Board::armVbusWake() { @@ -409,8 +406,7 @@ void NRF52Board::armVbusWake() { NRF_POWER->EVENTS_USBDETECTED = 0; NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk; } - - MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); + MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake armed"); } #endif diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 1ed83dd1..5ec928f0 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -37,6 +37,7 @@ protected: char *ota_name; #ifdef NRF52_POWER_MANAGEMENT + void pwrmgtInit() { initPowerMgr(); } uint32_t reset_reason; // RESETREAS register value uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF) uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts) @@ -44,6 +45,7 @@ protected: bool checkBootVoltage(const PowerMgtConfig* config); void enterSystemOff(uint8_t reason); void configureVoltageWake(uint8_t ain_channel, uint8_t refsel); + void pwrmgtWakeArmVbus() { armVbusWake(); } virtual void initiateShutdown(uint8_t reason); #endif @@ -85,6 +87,8 @@ public: const char* getShutdownReasonString(uint8_t reason) override; bool isPowerManagementInitialized() const override { return power_mgr_initialized; } bool supportsVoltageWake() const override { return true; } + bool isPwrMgtInitialised() const override { return isPowerManagementInitialized(); } + bool getWakeLpcompSupported() const override { return supportsVoltageWake(); } #endif }; diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index e9bb23a0..865739a5 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -140,6 +140,7 @@ public: (void)blocked; return false; } + virtual bool isGPSDetected() const { return false; } // Helper functions to manage setting by keys (useful in many places ...) const char* getSettingByKey(const char* key) { diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index 997b2994..600ae19d 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -172,10 +172,14 @@ mesh::Packet* StaticPoolPacketManager::allocNew() { } void StaticPoolPacketManager::free(mesh::Packet* packet) { - unused.add(packet, 0, 0); + if (packet == NULL) return; + if (!unused.add(packet, 0, 0)) { + MESH_DEBUG_PRINTLN("free: unused queue full, possible double-free detected"); + } } bool StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { + if (packet == NULL) return false; if (!send_queue.add(packet, priority, scheduled_for)) { MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); return false; @@ -216,6 +220,7 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) { } void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) { + if (packet == NULL) return; if (!rx_queue.add(packet, 0, scheduled_for)) { MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet"); free(packet); diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 0cfb26f8..2c44ba9b 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -28,7 +28,10 @@ public: int count() const { return _num; } int countBefore(uint32_t now) const; bool getNextTime(uint32_t now, uint32_t& scheduled_for) const; - mesh::Packet* itemAt(int i) const { return _table[i]; } + mesh::Packet* itemAt(int i) const { + if (i < 0 || i >= _num) return NULL; + return _table[i]; + } mesh::Packet* removeByIdx(int i); bool reschedule(mesh::Packet* packet, uint32_t scheduled_for); }; diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h index 403a28c0..6d056168 100644 --- a/src/helpers/radiolib/CustomLR2021.h +++ b/src/helpers/radiolib/CustomLR2021.h @@ -157,7 +157,7 @@ class CustomLR2021 : public LR2021 { int16_t startReceive() override { // include the PREAMBLE_DETECTED irq bit in reported flags - return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } bool isReceiving() { diff --git a/src/helpers/rp2040/SerialBLEInterface.cpp b/src/helpers/rp2040/SerialBLEInterface.cpp new file mode 100644 index 00000000..ef6f609a --- /dev/null +++ b/src/helpers/rp2040/SerialBLEInterface.cpp @@ -0,0 +1,208 @@ +// only built when the env enables the core BLE stack (build_as_lib.py globs this dir) +#ifdef PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH +#include "SerialBLEInterface.h" +#include +#include +#include + +// Nordic UART 6E4000xx-B5A3-F393-E0A9-E50E24DCCA9E, as raw bytes: the core lib's string +// parser uses sscanf("%llx"), which newlib-nano on the RP2040 doesn't support (yields all zeros) +#define NUS_UUID(n) { 0x6E, 0x40, 0x00, n, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E } +static const uint8_t SERVICE_UUID[16] = NUS_UUID(0x01); +static const uint8_t CHARACTERISTIC_UUID_RX[16] = NUS_UUID(0x02); +static const uint8_t CHARACTERISTIC_UUID_TX[16] = NUS_UUID(0x03); + +// The BLE core lib's own setValue()/notify path reallocs the value buffer on every call, +// so a second frame queued before the radio drained the first would corrupt it. We keep our +// own frame queue and drive att_server_notify() from the can-send-now callback instead. + +SerialBLEInterface::SerialBLEInterface() + : BLEService(BLEUUID(SERVICE_UUID)), + _rx(BLEUUID(CHARACTERISTIC_UUID_RX), BLEWrite, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED), + _tx(BLEUUID(CHARACTERISTIC_UUID_TX), BLERead | BLENotify, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED) +{ + _isEnabled = false; + _tx_pending = false; + send_queue_len = 0; + recv_queue_len = 0; + memset(&_can_send, 0, sizeof(_can_send)); + _rx.setCallbacks(this); + addCharacteristic(&_rx); + addCharacteristic(&_tx); +} + +bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pin_code, + const uint8_t* custom_address, bool clear_bonds, + bool stealth_pair_once, + const mesh::companion::BluetoothPeerIdentity* bonded_only_peer) { + if (custom_address || clear_bonds || stealth_pair_once || bonded_only_peer) return false; + // JustWorks here only makes the server request pairing on connect; caps are overridden below + BLE.setSecurity(BLESecurityJustWorks); + // adv data carries the 128-bit service UUID, leaving room for only 8 name chars; + // the full name goes in the scan response + BLE.begin(prefix); + + char mac_name[13]; + if (strcmp(name, "@@MAC") == 0) { + bd_addr_t a; + gap_local_bd_addr(a); + snprintf(mac_name, sizeof(mac_name), "%02X%02X%02X%02X%02X%02X", a[0], a[1], a[2], a[3], a[4], a[5]); + name = mac_name; + } + char dev_name[32+16]; + snprintf(dev_name, sizeof(dev_name), "%s%s", prefix, name); + + BLE.server()->setName(dev_name); // GAP device name characteristic + BLE.server()->addService(this); + BLE.server()->setCallbacks(this); + + // static passkey pairing with MITM protection, matching the esp32/nrf52 interfaces + sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY); + sm_set_authentication_requirements(SM_AUTHREQ_MITM_PROTECTION | SM_AUTHREQ_BONDING); + sm_use_fixed_passkey_in_display_role(pin_code); + + size_t n = strlen(dev_name); + if (n > sizeof(_scan_rsp) - 2) n = sizeof(_scan_rsp) - 2; + _scan_rsp[0] = n + 1; + _scan_rsp[1] = BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME; + memcpy(&_scan_rsp[2], dev_name, n); + gap_scan_response_set_data(n + 2, _scan_rsp); + + BLE_DEBUG_PRINTLN("begin: name=%s", dev_name); + return true; +} + +void SerialBLEInterface::clearBuffers() { + send_queue_len = 0; + recv_queue_len = 0; + _tx_pending = false; // a stale registration just fires into an empty queue; BTstack ignores double-adds +} + +void SerialBLEInterface::onConnect(BLEServer* s) { + BLE_DEBUG_PRINTLN("connected handle=0x%04X", _tx.conHandle()); + clearBuffers(); +} + +void SerialBLEInterface::onDisconnect(BLEServer* s) { + BLE_DEBUG_PRINTLN("disconnected"); + clearBuffers(); // BTstack re-enables advertising on its own +} + +void SerialBLEInterface::onWrite(BLECharacteristic* c) { + if (c != &_rx) return; + size_t len = _rx.valueLen(); + if (len == 0 || len > MAX_FRAME_SIZE) { + BLE_DEBUG_PRINTLN("onWrite: bad frame len=%u", (unsigned)len); + return; + } + if (recv_queue_len >= FRAME_QUEUE_SIZE) { + BLE_DEBUG_PRINTLN("onWrite: recv queue full, dropping frame"); + return; + } + recv_queue[recv_queue_len].len = len; + memcpy(recv_queue[recv_queue_len].buf, _rx.valueData(), len); + recv_queue_len++; +} + +// caller holds the BT lock (or is in the BT context) +void SerialBLEInterface::kickSend() { + if (_tx_pending) return; + _tx_pending = true; + _can_send.callback = onCanSend; + _can_send.context = this; + if (att_server_register_can_send_now_callback(&_can_send, _tx.conHandle()) != 0) { + _tx_pending = false; + } +} + +// BT context: one notification per can-send-now, re-arm while frames remain +void SerialBLEInterface::sendNext() { + _tx_pending = false; + if (send_queue_len == 0) return; + if (!isConnected()) { + BLE_DEBUG_PRINTLN("sendNext: not connected, clearing send queue"); + send_queue_len = 0; + return; + } + uint16_t h = _tx.conHandle(); + Frame& f = send_queue[0]; + uint16_t mtu = att_server_get_mtu(h); + if (f.len + 3 > mtu) { + // att would silently truncate; drop instead (client must negotiate MTU >= MAX_FRAME_SIZE+3) + BLE_DEBUG_PRINTLN("sendNext: frame len=%u exceeds mtu=%u, dropping", f.len, mtu); + } else { + uint8_t err = att_server_notify(h, _tx.valueHandle(), f.buf, f.len); + if (err == BTSTACK_ACL_BUFFERS_FULL) { + kickSend(); + return; + } + if (err) { + BLE_DEBUG_PRINTLN("sendNext: notify failed err=%u, dropping", err); + } else { + BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", f.len, f.buf[0]); + } + } + send_queue_len--; + memmove(&send_queue[0], &send_queue[1], send_queue_len * sizeof(Frame)); + if (send_queue_len > 0) kickSend(); +} + +void SerialBLEInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); + BLE.startAdvertising(true); +} + +void SerialBLEInterface::disconnect() { + uint16_t h = _tx.conHandle(); + if (h) gap_disconnect(h); +} + +void SerialBLEInterface::disable() { + _isEnabled = false; + BLE_DEBUG_PRINTLN("disable"); + disconnect(); + BLE.stopAdvertising(); +} + +bool SerialBLEInterface::isConnected() const { + // notifications can only be enabled once the link is authenticated (CCCD inherits the perms) + return _isEnabled && _tx.conHandle() != 0 && _tx.notifyEnabled(); +} + +bool SerialBLEInterface::isWriteBusy() const { + return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); +} + +size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { + if (len == 0 || len > MAX_FRAME_SIZE) { + BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len); + return 0; + } + if (!isConnected()) return 0; + + BluetoothLock lock; + if (send_queue_len >= FRAME_QUEUE_SIZE) { + BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + send_queue[send_queue_len].len = len; + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + kickSend(); + return len; +} + +size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { + BluetoothLock lock; + if (recv_queue_len == 0) return 0; + + size_t len = recv_queue[0].len; + memcpy(dest, recv_queue[0].buf, len); + recv_queue_len--; + memmove(&recv_queue[0], &recv_queue[1], recv_queue_len * sizeof(Frame)); + BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, dest[0]); + return len; +} +#endif diff --git a/src/helpers/rp2040/SerialBLEInterface.h b/src/helpers/rp2040/SerialBLEInterface.h new file mode 100644 index 00000000..80bdfbe1 --- /dev/null +++ b/src/helpers/rp2040/SerialBLEInterface.h @@ -0,0 +1,85 @@ +#pragma once + +#include "../BaseSerialInterface.h" +#include +#include +#include + +// Nordic UART service over the arduino-pico BLE library (BTstack on the CYW43). +// Build with -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH so the core links liblwip-bt. +class SerialBLEInterface : public BaseSerialInterface, BLEService, BLEServerCallbacks, BLECharacteristicCallbacks { + // subclass only to reach the protected connection/notify state + struct Characteristic : public BLECharacteristic { + using BLECharacteristic::BLECharacteristic; + uint16_t valueHandle() const { return _valueHandle; } + uint16_t conHandle() const { return con_handle; } + bool notifyEnabled() const { return _notificationEnabled; } + }; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 8 + + Characteristic _rx; + Characteristic _tx; + bool _isEnabled; + bool _tx_pending; // a can-send-now callback is registered + btstack_context_callback_registration_t _can_send; + uint8_t _scan_rsp[31]; // complete local name; BTstack keeps the pointer + + uint8_t send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + uint8_t recv_queue_len; + Frame recv_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers(); + void kickSend(); + void sendNext(); + static void onCanSend(void* ctx) { ((SerialBLEInterface*)ctx)->sendNext(); } + + // BLE library callbacks (run in the BT context) + void onWrite(BLECharacteristic* c) override; + void onConnect(BLEServer* s) override; + void onDisconnect(BLEServer* s) override; + +public: + SerialBLEInterface(); + + /** + * init the BLE interface. + * @param prefix a prefix for the device name + * @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned + * @param pin_code the BLE security pin + */ + bool begin(const char* prefix, const char* name, uint32_t pin_code, + const uint8_t* custom_address = nullptr, bool clear_bonds = false, + bool stealth_pair_once = false, + const mesh::companion::BluetoothPeerIdentity* bonded_only_peer = nullptr); + // Pico W currently supports normal authenticated pairing, not the optional + // ESP32/nRF52 identity-rotation and bonded-only advertising policies. + bool takeSuccessfulConnection(mesh::companion::BluetoothPeerIdentity* = nullptr) { return false; } + bool takeBondedOnlyRecovery() { return false; } + bool enableBondedOnlyAdvertising(const mesh::companion::BluetoothPeerIdentity&) { return false; } + void cancelStealthPairingTransition() { } + + void disconnect(); + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + bool isConnected() const override; + bool isWriteBusy() const override; + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + +#if BLE_DEBUG_LOGGING && ARDUINO + #include + #define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__) + #define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__) +#else + #define BLE_DEBUG_PRINT(...) {} + #define BLE_DEBUG_PRINTLN(...) {} +#endif diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 71cb1189..ef923786 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -47,6 +47,7 @@ public: EnvironmentSensorManager(LocationProvider &location): _location(&location), _configured_location(&location) {}; LocationProvider* getLocationProvider() { return _location; } + bool isGPSDetected() const override { return gps_detected; } #else EnvironmentSensorManager(){}; #endif diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/wifi/SerialWifiInterface.cpp similarity index 100% rename from src/helpers/esp32/SerialWifiInterface.cpp rename to src/helpers/wifi/SerialWifiInterface.cpp diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/wifi/SerialWifiInterface.h similarity index 100% rename from src/helpers/esp32/SerialWifiInterface.h rename to src/helpers/wifi/SerialWifiInterface.h diff --git a/test/fixtures/serial_wifi_sessions/test.cpp b/test/fixtures/serial_wifi_sessions/test.cpp index f3fcfbfe..d0b691ba 100644 --- a/test/fixtures/serial_wifi_sessions/test.cpp +++ b/test/fixtures/serial_wifi_sessions/test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/test/test_build_profiles.sh b/test/test_build_profiles.sh index 0f00138d..9178cf80 100644 --- a/test/test_build_profiles.sh +++ b/test/test_build_profiles.sh @@ -470,11 +470,11 @@ for full_env in "${SUPPORTED_PIO_ENVS[@]}"; do && [[ "$PLATFORMIO_BUILD_FLAGS" != *"WIFI_SSID"* ]]; then fail "$full_env Full Companion omitted ordinary WiFi credentials" fi - if ! pio_env_option_contains "$pio_env" build_src_filter "helpers/esp32/*.cpp" \ + if ! pio_env_option_contains "$pio_env" build_src_filter "helpers/wifi/*.cpp" \ && ! pio_env_option_contains "$pio_env" build_src_filter \ - "helpers/esp32/SerialWifiInterface.cpp" \ + "helpers/wifi/SerialWifiInterface.cpp" \ && [[ "$PLATFORMIO_BUILD_SRC_FILTER" \ - != *"helpers/esp32/SerialWifiInterface.cpp"* ]]; then + != *"helpers/wifi/SerialWifiInterface.cpp"* ]]; then fail "$full_env Full Companion omitted SerialWifiInterface" fi [[ "$PLATFORMIO_BUILD_FLAGS" == *"WIFI_OTA_SEEDER=1"* ]] \ diff --git a/test/test_cli_settings_contract.py b/test/test_cli_settings_contract.py index 15f36ed7..0a34e2fa 100644 --- a/test/test_cli_settings_contract.py +++ b/test/test_cli_settings_contract.py @@ -190,7 +190,7 @@ class CLISettingsContractTest(unittest.TestCase): 'password', 'prv.key', # Local secret/identity reads 'pwrmgt.bootreason', 'role', # Boot/build facts 'radio.rxps.config', # Detail view of radio.rxps - 'wifi.pwd', # Saved transport credentials + 'wifi.status', 'wifi.ip', # Live WiFi link state }) def test_literal_key_comparison_lengths(self): diff --git a/test/test_companion_discovery.py b/test/test_companion_discovery.py new file mode 100644 index 00000000..2f218e1f --- /dev/null +++ b/test/test_companion_discovery.py @@ -0,0 +1,74 @@ +"""Execute the merged discovery parser with truncated and unrelated radio data.""" +from pathlib import Path +import subprocess +import tempfile +import unittest +from test_radio_receive_contract import method + +ROOT = Path(__file__).resolve().parents[1] + + +class CompanionDiscoveryTests(unittest.TestCase): + def test_bounded_responses_and_request_lifetime(self): + source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text() + parser = method(source, 'void MyMesh::checkControlDataForPendingDiscovery(') + query = method(source, 'int MyMesh::getDiscoveredNodes(') + code = r''' +#include +#include +#include +#include +#define DISCOVERED_NODES_TABLE_SIZE 4 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 +static uint32_t now=100; +uint32_t millis(){return now;} +struct StrHelper{static void strncpy(char* o,const char* i,size_t n){::strncpy(o,i,n-1);o[n-1]=0;}}; +struct DiscoveredNode{uint8_t pubkey_prefix[9];float snr_in,snr_out;char name[32];uint8_t type;}; +struct ContactInfo{char name[32];}; +struct Radio{float getLastSNR(){return 5.0f;}}; +struct MyMesh { + DiscoveredNode discovered_nodes[4]={}; + uint32_t disc_node_req_tag=123,disc_nodes_count=0,disc_node_req_at=100; + bool disc_node_req_active=true; + Radio radio;Radio* _radio=&radio; + ContactInfo contact; + MyMesh(){memset(contact.name,'A',32);} + ContactInfo* lookupContactByPubKey(const uint8_t*,size_t){return &contact;} + void checkControlDataForPendingDiscovery(uint8_t[],size_t); + int getDiscoveredNodes(DiscoveredNode[],int); +}; +@METHODS@ +int main(){ + MyMesh mesh; + uint8_t response[14]={0x92,20};memcpy(response+2,&mesh.disc_node_req_tag,4); + for(size_t len=0;len<14;len++){ + uint8_t* short_frame=new uint8_t[len];memcpy(short_frame,response,len); + mesh.checkControlDataForPendingDiscovery(short_frame,len);delete[] short_frame; + assert(mesh.disc_nodes_count==0); + } + response[0]=0x82;mesh.checkControlDataForPendingDiscovery(response,14); + assert(mesh.disc_nodes_count==0);response[0]=0x92; + response[2]^=1;mesh.checkControlDataForPendingDiscovery(response,14); + assert(mesh.disc_nodes_count==0);response[2]^=1; + mesh.disc_node_req_active=false;mesh.checkControlDataForPendingDiscovery(response,14); + assert(mesh.disc_nodes_count==0);mesh.disc_node_req_active=true; + now=30100;mesh.checkControlDataForPendingDiscovery(response,14);assert(mesh.disc_nodes_count==0); + mesh.disc_node_req_at=0xfffffff0;now=20; + mesh.checkControlDataForPendingDiscovery(response,14);assert(mesh.disc_nodes_count==1); + assert(mesh.discovered_nodes[0].name[31]==0&&mesh.discovered_nodes[0].snr_out==5.0f); + for(int i=0;i<10;i++)mesh.checkControlDataForPendingDiscovery(response,14); + assert(mesh.disc_nodes_count==4); + DiscoveredNode result[4];assert(mesh.getDiscoveredNodes(result,100)==4); + assert(mesh.getDiscoveredNodes(nullptr,4)==0&&mesh.getDiscoveredNodes(result,-1)==0); +} +'''.replace('@METHODS@', parser + '\n' + query) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / 'test.cpp').write_text(code) + subprocess.run(['g++', '-std=c++17', '-fsanitize=address,undefined', + '-fno-pie', '-no-pie', str(path / 'test.cpp'), '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_companion_preferences_transaction.py b/test/test_companion_preferences_transaction.py index 5be5af02..245966da 100644 --- a/test/test_companion_preferences_transaction.py +++ b/test/test_companion_preferences_transaction.py @@ -49,6 +49,14 @@ int main(int argc,char** argv){ strcpy(original.node_name,"durable node");original.freq=910.525f; original.ble_pin=876543;original.bluetooth_stealth_mode=2; original.gps_interval=123;original.sf=7;original.cr=7;original.bw=62.5f; + original.tx_delay_factor=1.25f;original.direct_tx_delay_factor=0.75f; + original.interference_threshold=8;original.agc_reset_interval=15;original.tz_offset=-7; +#ifdef TBEAM_1W + strcpy(original.fan_mode,"on");original.fan_lo=33;original.fan_hi=42; +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + strcpy(original.wifi_ssid,"test-network");strcpy(original.wifi_pwd,"test-password"); +#endif assert(store.savePrefs(original,47.1,-122.2)); const auto disk=store.fs.files["/new_prefs"]; CompanionNodePrefs changed=original;changed.freq=910.1f; @@ -74,6 +82,14 @@ int main(int argc,char** argv){ CompanionNodePrefs loaded;double lat=0,lon=0; assert(store.loadPrefs(loaded,lat,lon)); assert(loaded.freq==changed.freq&&loaded.ble_pin==original.ble_pin); + assert(loaded.tx_delay_factor==1.25f&&loaded.direct_tx_delay_factor==0.75f); + assert(loaded.interference_threshold==8&&loaded.agc_reset_interval==15&&loaded.tz_offset==-7); +#ifdef TBEAM_1W + assert(!strcmp(loaded.fan_mode,"on")&&loaded.fan_lo==33&&loaded.fan_hi==42); +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + assert(!strcmp(loaded.wifi_ssid,"test-network")&&!strcmp(loaded.wifi_pwd,"test-password")); +#endif assert(lat==42.3&&lon==-121.2); } else if(scenario==1){ for(int fault : {0,1,2,3,4}){ @@ -143,6 +159,14 @@ int main(int argc,char** argv){ CompanionNodePrefs snapshot=*prefs; assert(!snapshot.isDirty()); prefs->~CompanionNodePrefs(); + } else if(scenario==5){ + DataStore legacy;legacy.fs.files["/new_prefs"]=disk; + legacy.fs.files["/new_prefs"].resize(215); + CompanionNodePrefs loaded;double lat=0,lon=0; + assert(legacy.loadPrefs(loaded,lat,lon)); + assert(loaded.tx_delay_factor==0.5f&&loaded.direct_tx_delay_factor==0.2f); + assert(loaded.interference_threshold==0&&loaded.agc_reset_interval==0&&loaded.tz_offset==0); + assert(loaded.freq==original.freq&&loaded.ble_pin==original.ble_pin); } else if(scenario==4){ DataStore legacy;legacy.fs.files["/node_prefs"]=disk; legacy.fs.fail_write_after=17; @@ -211,11 +235,13 @@ inline char* utoa(unsigned int value,char* output,int base){ (work / 'ContactFileTransaction.h').write_text(transaction.replace( '#include "IdentityStore.h"', '#include ')) (work / 'test.cpp').write_text(HARNESS.replace('@METHODS@', methods)) - for platform in ('ESP32_PLATFORM', 'RP2040_PLATFORM', 'STM32_PLATFORM', 'NRF52_PLATFORM'): + for platform in ('ESP32_PLATFORM', 'RP2040_PLATFORM', 'STM32_PLATFORM', 'NRF52_PLATFORM', + 'ESP32_PLATFORM,TBEAM_1W', 'RP2040_PLATFORM,ENABLE_WIFI_INTERFACE'): with self.subTest(platform=platform): binary = work / 'test' build = subprocess.run(['g++', '-std=c++17', - '-D'+platform+'=1', '-fsanitize=address,undefined', '-fno-sanitize-recover=all', + *['-D'+flag+'=1' for flag in platform.split(',')], + '-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie', '-include', str(work / 'platform_shim.h'), '-I', str(work), '-I', str(ROOT / 'test/fixtures/radio_profiles/mocks'), @@ -228,7 +254,7 @@ inline char* utoa(unsigned int value,char* output,int base){ str(ROOT / 'src/helpers/TxtDataHelpers.cpp'), '-o', str(binary)], capture_output=True, text=True) self.assertEqual(build.returncode, 0, build.stdout + build.stderr) - for scenario in range(5): + for scenario in range(6): with self.subTest(scenario=scenario): run = subprocess.run([str(binary), str(scenario)], capture_output=True, text=True) self.assertEqual(run.returncode, 0, run.stdout + run.stderr) diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index 6ab085a6..e32a9225 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -343,6 +343,47 @@ TEST(NodePrefs, ExplicitWrappedBridgeFormatReplacesRawInMemoryValue) { EXPECT_EQ(mesh::bridge::ESPNOW_FORMAT_WRAPPED, loaded.bridge_format); } +class TestNested : public ConfigSerializer { + class Inner : public ConfigSerializer { + protected: + void structure() override { } // no properties, so it writes as '{}' + }; + Inner inner; + protected: + void structure() override { + def("age", age); + def("inner", inner); + def("name", name, sizeof(name)); // comes *after* the empty sub-object + } + public: + int32_t age; + char name[16]; +}; + +TEST(ConfigSerializer, LoadSerial_EmptyObject) { + MockInputStream s("{age:" TEST_INT_S ",inner:{},name:\"Scott\"}"); + TestNested data; + data.name[0] = 0; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); // properties after an empty object must still load +} + +TEST(ConfigSerializer, LoadSerial_EmptyObjectWithWhitespace) { + MockInputStream s("{age:" TEST_INT_S ",inner:{ },name:\"Scott\"}"); + TestNested data; + data.name[0] = 0; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + TEST(DynamicConfigSerializer, GetSet_Basic) { DynamicConfigSerializer data; diff --git a/test/test_indicator_exclusive_transport.py b/test/test_indicator_exclusive_transport.py index 60705234..d95c9cdb 100644 --- a/test/test_indicator_exclusive_transport.py +++ b/test/test_indicator_exclusive_transport.py @@ -88,7 +88,7 @@ assert re.search( setup, ) assert re.search( - r"#ifdef WIFI_SSID\s+" + r"#if defined\(ESP32\) && defined\(WIFI_SSID\)\s+" # Registering a callback does not start the WiFi stack. It must remain # available for session cleanup, while startup stays behind the boot gate. r"wifi_interface\.setSessionChangedCallback\(cancelCompanionWiFiSession, nullptr\);\s+" diff --git a/test/test_indicator_messages_profile.py b/test/test_indicator_messages_profile.py index df475af0..79793e99 100644 --- a/test/test_indicator_messages_profile.py +++ b/test/test_indicator_messages_profile.py @@ -63,7 +63,8 @@ static_assert(TRANSPORT == 4, "transport"); static_assert(ADVERT == 5, "advert"); static_assert(SENSORS == 6, "sensors"); static_assert(WIFI_SETUP == 7, "wifi setup"); -static_assert(Count == 8, "count"); +static_assert(DISCOVERY == 8, "discovery appended without reordering"); +static_assert(Count == 9, "count"); int main() {{ return 0; }} """ with tempfile.TemporaryDirectory() as temp_dir: diff --git a/test/test_local_cli_access.py b/test/test_local_cli_access.py index 1bf0c864..a8fa61cf 100644 --- a/test/test_local_cli_access.py +++ b/test/test_local_cli_access.py @@ -63,6 +63,9 @@ struct Store { bool formatFileSystem(){++erased;return success;} }; struct Prefs { + float tx_delay_factor=0.5f, direct_tx_delay_factor=0.2f; + uint8_t interference_threshold=0, agc_reset_interval=0; + int8_t tz_offset=0; float freq=910, bw=125, airtime_factor=1, rx_delay_base=1; uint8_t sf=7, cr=5, path_hash_mode=0, multi_acks=0, rx_ps_level=0, rx_ps_preamble=16; uint32_t rx_ps_rx_us=1, rx_ps_sleep_us=1; diff --git a/test/test_message_navigation.py b/test/test_message_navigation.py index 95a56d10..f434703e 100644 --- a/test/test_message_navigation.py +++ b/test/test_message_navigation.py @@ -368,7 +368,7 @@ class MessageNavigationTest(unittest.TestCase): home_navigation = home_navigation.split("#ifdef COMPANION_EXCLUSIVE_WIFI_BLE", 1)[0] home_navigation = home_navigation.replace("{\n", "{\n Screen::handleInput(c);\n", 1) implementation += ("class HomeScreen : public Screen { public: UITask* _task; " - "enum HomePage {FIRST,MESSAGES,RECENT,RADIO,Count}; int _page=0; " + "enum HomePage {FIRST,MESSAGES,RECENT,RADIO,DISCOVERY,Count}; int _page=0; " "HomeScreen(UITask* task):_task(task){}\n" + home_navigation + "return false; } };\n") target = (ROOT / "variants/sensecap_indicator-espnow/target.cpp").read_text() diff --git a/test/test_serial_wifi_sessions.py b/test/test_serial_wifi_sessions.py index 2bf9f6dc..77a5db54 100644 --- a/test/test_serial_wifi_sessions.py +++ b/test/test_serial_wifi_sessions.py @@ -71,7 +71,7 @@ class SerialWifiSessionTests(unittest.TestCase): *SANITIZER_FLAGS, f"-I{FIXTURE / 'mocks'}", f"-I{ROOT / 'src'}", str(FIXTURE / "test.cpp"), - str(ROOT / "src/helpers/esp32/SerialWifiInterface.cpp"), + str(ROOT / "src/helpers/wifi/SerialWifiInterface.cpp"), "-o", str(binary)], capture_output=True, text=True, timeout=60, ) diff --git a/tools/mota/test_mota.py b/tools/mota/test_mota.py index 53cee8ad..9742c12c 100644 --- a/tools/mota/test_mota.py +++ b/tools/mota/test_mota.py @@ -518,7 +518,7 @@ def test_measured_full_companion_promotions_are_exact_and_bounded(): profile = build.split("apply_companion_radio_full_profile()", 1)[1] profile = profile.split("apply_radio_overrides()", 1)[0] assert "-DWIFI_SSID=" in profile - assert "+" in profile + assert "+" in profile assert "meshadventurer_sx1262_companion_radio_full" in profile assert "meshadventurer_sx1268_companion_radio_full" in profile assert ("-DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=30 " diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 75194415..6001638a 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -176,6 +176,7 @@ extends = Heltec_RC32 build_flags = ${Heltec_RC32.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -188,6 +189,7 @@ build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -323,6 +325,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -333,6 +336,7 @@ build_flags = build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index 870463e1..f936dee8 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -395,6 +395,7 @@ extends = Heltec_tracker_v2 build_flags = ${Heltec_tracker_v2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7735Display @@ -408,6 +409,7 @@ build_src_filter = ${Heltec_tracker_v2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 198a1778..4bbde0b5 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -185,6 +185,7 @@ extends = Heltec_lora32_v2 build_flags = ${Heltec_lora32_v2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 @@ -196,6 +197,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index c998622f..e13d3f94 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -322,6 +322,7 @@ extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display @@ -339,6 +340,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -537,6 +539,7 @@ lib_deps = extends = Heltec_WSL3 build_flags = ${Heltec_WSL3.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -547,6 +550,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 968170af..1a849004 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -497,6 +497,7 @@ extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display @@ -509,6 +510,7 @@ build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -757,6 +759,7 @@ extends = heltec_v4_tft build_flags = ${heltec_v4_tft.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=512 @@ -770,6 +773,7 @@ build_src_filter = ${heltec_v4_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h index 74878db5..0fc90f3b 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.h +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -24,6 +24,7 @@ public: HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void powerOff() override; diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index 1ced2b4a..bcf8bbdb 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -260,6 +260,7 @@ extends = heltec_v4_r8_oled build_flags = ${heltec_v4_r8_oled.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=512 @@ -271,6 +272,7 @@ build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -508,6 +510,7 @@ extends = heltec_v4_r8_tft build_flags = ${heltec_v4_r8_tft.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=512 @@ -519,6 +522,7 @@ build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index cc8da308..477be994 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -3,18 +3,10 @@ #include #include -#ifndef FAN_TEMP_ON_C -#define FAN_TEMP_ON_C 45.0f -#endif -#ifndef FAN_TEMP_OFF_C -#define FAN_TEMP_OFF_C 41.0f -#endif -#ifndef FAN_MIN_RUN_TIME_MS -#define FAN_MIN_RUN_TIME_MS 5000UL -#endif -#ifndef FAN_TEMP_POLL_INTERVAL_MS -#define FAN_TEMP_POLL_INTERVAL_MS 1000UL -#endif +#include +#include +#include +#include void TBeam1WBoard::begin() { ESP32Board::begin(); @@ -49,15 +41,34 @@ void TBeam1WBoard::begin() { pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); - // Start with the fan off. updateFanControl() turns it on at the validated - // temperature threshold and applies hysteresis, avoiding the former - // always-on battery drain while still failing safe on an invalid reading. + // NTC ADC (PA-adjacent thermistor) + pinMode(NTC_PIN, INPUT); + analogSetPinAttenuation(NTC_PIN, ADC_11db); + analogReadResolution(12); + + // Fan: auto/onoff. Thermal on at 36C / off below 30C; TX still forces a cooldown. pinMode(FAN_CTRL_PIN, OUTPUT); - fan_running = false; - fan_temperature_valid = false; - fan_started_at_ms = 0; - fan_last_poll_ms = 0; - digitalWrite(FAN_CTRL_PIN, LOW); + digitalWrite(FAN_CTRL_PIN, HIGH); + _fan_on = true; + _temp_c = readNtcTempC(); + startFanTask(); +} + +void TBeam1WBoard::startFanTask() { + if (_fan_task) return; + xTaskCreate(fanTaskThunk, "tbeam1w_fan", 4096, this, 1, &_fan_task); +} + +void TBeam1WBoard::updateFanControl() { + if (!_fan_task) updateFan(); +} + +void TBeam1WBoard::fanTaskThunk(void* arg) { + auto* self = static_cast(arg); + for (;;) { + self->updateFan(); + vTaskDelay(pdMS_TO_TICKS(1000)); + } } void TBeam1WBoard::powerCycleRadio() { @@ -73,14 +84,24 @@ void TBeam1WBoard::powerCycleRadio() { } void TBeam1WBoard::onBeforeTransmit() { - // RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins() - updateFanControl(); digitalWrite(LED_PIN, HIGH); // TX LED on + portENTER_CRITICAL(&_fan_mux); + _tx_active = true; + if (_mode == FAN_AUTO && !_stopped) { + setFanOutputLocked(true); + } + portEXIT_CRITICAL(&_fan_mux); } void TBeam1WBoard::onAfterTransmit() { digitalWrite(LED_PIN, LOW); // TX LED off - updateFanControl(); + portENTER_CRITICAL(&_fan_mux); + if (!_stopped) { + _tx_until_ms = millis() + FAN_TX_COOLDOWN_MS; + _tx_cooldown_active = true; + } + _tx_active = false; + portEXIT_CRITICAL(&_fan_mux); } uint16_t TBeam1WBoard::getBattMilliVolts() { @@ -109,6 +130,13 @@ const char* TBeam1WBoard::getManufacturerName() const { } void TBeam1WBoard::powerOff() { + portENTER_CRITICAL(&_fan_mux); + _stopped = true; + _tx_active = false; + _tx_cooldown_active = false; + setFanOutputLocked(false); + portEXIT_CRITICAL(&_fan_mux); + // Turn off radio LNA (CTRL pin must be LOW when not receiving) digitalWrite(SX126X_RXEN, LOW); @@ -116,9 +144,7 @@ void TBeam1WBoard::powerOff() { digitalWrite(SX126X_POWER_EN, LOW); radio_powered = false; - // Turn off LED and fan digitalWrite(LED_PIN, LOW); - setFanEnabled(false); ESP32Board::powerOff(); } @@ -165,39 +191,248 @@ bool TBeam1WBoard::isLoRaFemLnaEnabled() const { } void TBeam1WBoard::setFanEnabled(bool enabled) { - if (fan_running == enabled) return; - fan_running = enabled; - if (enabled) fan_started_at_ms = millis(); - digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); + portENTER_CRITICAL(&_fan_mux); + setFanOutputLocked(enabled && !_stopped); + portEXIT_CRITICAL(&_fan_mux); } bool TBeam1WBoard::isFanEnabled() const { - return fan_running; + portENTER_CRITICAL(&_fan_mux); + bool enabled = _fan_on; + portEXIT_CRITICAL(&_fan_mux); + return enabled; } -void TBeam1WBoard::updateFanControl() { - const uint32_t now = millis(); - if (!fan_temperature_valid - || static_cast(now - fan_last_poll_ms) - >= FAN_TEMP_POLL_INTERVAL_MS) { - const float temperature_c = getMCUTemperature(); - fan_last_poll_ms = now; - if (isnan(temperature_c)) { - // Cooling is safer than silently leaving the 1 W PA unprotected if the - // ESP32 temperature sensor ever reports an invalid sample. - setFanEnabled(true); - return; - } - fan_last_temperature_c = temperature_c; - fan_temperature_valid = true; +float TBeam1WBoard::readNtcTempC() { + analogReadMilliVolts(NTC_PIN); // settle + uint32_t sum = 0; + for (int i = 0; i < 8; i++) { + uint32_t sample_mv = analogReadMilliVolts(NTC_PIN); + // GPIO14 is ADC2 on ESP32-S3. Wi-Fi/ESP-NOW arbitration failures are + // reported by Arduino as 0 mV; never average a failed sample into a + // plausible-but-low temperature. + if (sample_mv == 0 || sample_mv >= NTC_VCC_MV) return NAN; + sum += sample_mv; + } + float mv = sum / 8.0f; + + // R_ntc = R_fixed * (Vcc - V) / V for 3.3V-NTC-ADC-10k-GND + float r_ntc = NTC_R_FIXED * (NTC_VCC_MV - mv) / mv; + if (r_ntc <= 0.0f) return NAN; + + float temp_k = 1.0f / (1.0f / 298.15f + (1.0f / NTC_B) * logf(r_ntc / NTC_R25)); + return temp_k - 273.15f; +} + +bool TBeam1WBoard::ntcImplausible(float temp_c) const { + return isnan(temp_c) || temp_c < -20.0f || temp_c > 120.0f; +} + +bool TBeam1WBoard::isTxCoolingLocked(uint32_t now) { + if (_tx_active) return true; + if (!_tx_cooldown_active) return false; + if ((int32_t)(now - _tx_until_ms) >= 0) { + _tx_cooldown_active = false; + return false; + } + return true; +} + +int TBeam1WBoard::cooldownSecsLocked() { + if (_tx_active) return (FAN_TX_COOLDOWN_MS + 999) / 1000; + if (!_tx_cooldown_active) return 0; + int32_t remain_ms = (int32_t)(_tx_until_ms - millis()); + if (remain_ms <= 0) { + _tx_cooldown_active = false; + return 0; + } + return (remain_ms + 999) / 1000; +} + +void TBeam1WBoard::setFanOutputLocked(bool enabled) { + _fan_on = enabled; + digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); +} + +void TBeam1WBoard::updateFan() { + float t = readNtcTempC(); + uint32_t now = millis(); + + portENTER_CRITICAL(&_fan_mux); + if (_stopped) { + portEXIT_CRITICAL(&_fan_mux); + return; } - if (fan_running) { - if (static_cast(now - fan_started_at_ms) >= FAN_MIN_RUN_TIME_MS - && fan_last_temperature_c < FAN_TEMP_OFF_C) { - setFanEnabled(false); - } - } else if (fan_last_temperature_c >= FAN_TEMP_ON_C) { - setFanEnabled(true); + _temp_c = t; + bool tx_cooling = isTxCoolingLocked(now); + + bool enabled; + if (_mode == FAN_ON) { + enabled = true; + } else if (_mode == FAN_OFF) { + enabled = false; + } else if (ntcImplausible(t)) { + enabled = true; // fail-safe: treat bad NTC as hot + } else { + // TX cooldown must not latch _thermal_on, or a TX at 30C keeps the fan + // running until the temperature dips under lo. + if (t >= (float)_hi_c) _thermal_on = true; + else if (t < (float)_lo_c) _thermal_on = false; + enabled = _thermal_on || tx_cooling; } + + setFanOutputLocked(enabled); + portEXIT_CRITICAL(&_fan_mux); +} + +bool TBeam1WBoard::persistKey(const char* key, const char* value) { + return _prefs && _prefs->setByKey(key, value); +} + +bool TBeam1WBoard::parseIntArg(const char* text, int& value) { + if (!text || !*text) return false; + errno = 0; + char* end = nullptr; + long parsed = strtol(text, &end, 10); + if (errno == ERANGE || end == text || *end != '\0' || parsed < INT_MIN || parsed > INT_MAX) { + return false; + } + value = (int)parsed; + return true; +} + +void TBeam1WBoard::loadFanPrefs() { + if (!_prefs) return; + + FanMode mode = FAN_AUTO; + char buf[12]; + buf[0] = 0; + if (_prefs->getByKey("fan", buf, 11)) { + if (strcmp(buf, "off") == 0) mode = FAN_OFF; + else if (strcmp(buf, "on") == 0) mode = FAN_ON; + } + + int lo = FAN_DEFAULT_LO_C; + int hi = FAN_DEFAULT_HI_C; + buf[0] = 0; + if (_prefs->getByKey("fan_lo", buf, 11)) lo = atoi(buf); + buf[0] = 0; + if (_prefs->getByKey("fan_hi", buf, 11)) hi = atoi(buf); + + portENTER_CRITICAL(&_fan_mux); + _mode = mode; + if (lo >= 0 && hi <= 120 && lo < hi) { + _lo_c = lo; + _hi_c = hi; + } + portEXIT_CRITICAL(&_fan_mux); +} + +void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + loadFanPrefs(); + updateFan(); +} + +const char* TBeam1WBoard::modeNameLocked() const { + if (_mode == FAN_AUTO) return "auto"; + if (_mode == FAN_OFF) return "off"; + return "on"; +} + +bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + (void)sender_timestamp; + + if (strcmp(command, "get fan") == 0) { + portENTER_CRITICAL(&_fan_mux); + int cd = cooldownSecsLocked(); + float temp_c = _temp_c; + bool enabled = _fan_on; + const char* mode = modeNameLocked(); + portEXIT_CRITICAL(&_fan_mux); + if (ntcImplausible(temp_c)) { + sprintf(reply, "> %s n/a fan=%s cd=%ds", mode, enabled ? "on" : "off", cd); + } else { + sprintf(reply, "> %s %.1fC fan=%s cd=%ds", + mode, (double)temp_c, enabled ? "on" : "off", cd); + } + return true; + } + + if (strncmp(command, "set fan.lo ", 11) == 0) { + int lo; + portENTER_CRITICAL(&_fan_mux); + int hi_limit = _hi_c; + portEXIT_CRITICAL(&_fan_mux); + if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= hi_limit || lo > 100) { + strcpy(reply, "Error: fan.lo must be 0..100 and < fan.hi"); + } else if (!persistKey("fan_lo", &command[11])) { + strcpy(reply, "Error: failed to save fan.lo"); + } else { + portENTER_CRITICAL(&_fan_mux); + _lo_c = lo; + portEXIT_CRITICAL(&_fan_mux); + sprintf(reply, "OK - fan.lo %d", lo); + } + return true; + } + + if (strncmp(command, "set fan.hi ", 11) == 0) { + int hi; + portENTER_CRITICAL(&_fan_mux); + int lo_limit = _lo_c; + portEXIT_CRITICAL(&_fan_mux); + if (!parseIntArg(&command[11], hi) || hi <= lo_limit || hi > 120) { + strcpy(reply, "Error: fan.hi must be > fan.lo and <= 120"); + } else if (!persistKey("fan_hi", &command[11])) { + strcpy(reply, "Error: failed to save fan.hi"); + } else { + portENTER_CRITICAL(&_fan_mux); + _hi_c = hi; + portEXIT_CRITICAL(&_fan_mux); + sprintf(reply, "OK - fan.hi %d", hi); + } + return true; + } + + if (strncmp(command, "set fan ", 8) == 0) { + const char* arg = &command[8]; + if (strcmp(arg, "on") == 0) { + if (!persistKey("fan", "on")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + portENTER_CRITICAL(&_fan_mux); + _mode = FAN_ON; + if (!_stopped) setFanOutputLocked(true); + portEXIT_CRITICAL(&_fan_mux); + strcpy(reply, "OK - fan on"); + } + } else if (strcmp(arg, "off") == 0) { + if (!persistKey("fan", "off")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + portENTER_CRITICAL(&_fan_mux); + _mode = FAN_OFF; + setFanOutputLocked(false); + portEXIT_CRITICAL(&_fan_mux); + strcpy(reply, "OK - fan off"); + } + } else if (strcmp(arg, "auto") == 0) { + if (!persistKey("fan", "auto")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + portENTER_CRITICAL(&_fan_mux); + _mode = FAN_AUTO; + portEXIT_CRITICAL(&_fan_mux); + updateFan(); + strcpy(reply, "OK - fan auto"); + } + } else { + strcpy(reply, "Error: fan must be on, off, or auto"); + } + return true; + } + + return false; } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index 9f1bb428..390711ac 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "variant.h" @@ -30,19 +32,45 @@ class CustomSX1262Wrapper; // - Battery must support 2A+ discharge for high-power TX class TBeam1WBoard : public ESP32Board { +public: + enum FanMode { FAN_ON, FAN_OFF, FAN_AUTO }; + private: bool radio_powered = false; bool lna_enabled = true; bool lna_driver_synced = false; CustomSX1262Wrapper* radio_driver = nullptr; - bool fan_running = false; - bool fan_temperature_valid = false; - uint32_t fan_started_at_ms = 0; - uint32_t fan_last_poll_ms = 0; - float fan_last_temperature_c = 0.0f; + bool _stopped = false; + KeyValueStore* _prefs = nullptr; + FanMode _mode = FAN_AUTO; + int _lo_c = FAN_DEFAULT_LO_C; + int _hi_c = FAN_DEFAULT_HI_C; + bool _thermal_on = false; // onoff hysteresis; TX boost must not latch this + bool _fan_on = true; + float _temp_c = NAN; + bool _tx_active = false; + bool _tx_cooldown_active = false; + uint32_t _tx_until_ms = 0; + TaskHandle_t _fan_task = nullptr; + mutable portMUX_TYPE _fan_mux = portMUX_INITIALIZER_UNLOCKED; + + void startFanTask(); + void updateFan(); + void setFanOutputLocked(bool enabled); + float readNtcTempC(); + int cooldownSecsLocked(); + bool isTxCoolingLocked(uint32_t now); + bool ntcImplausible(float temp_c) const; + bool persistKey(const char* key, const char* value); + static bool parseIntArg(const char* text, int& value); + void loadFanPrefs(); + const char* modeNameLocked() const; + static void fanTaskThunk(void* arg); public: void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; void onBeforeTransmit() override; void onAfterTransmit() override; uint16_t getBattMilliVolts() override; @@ -60,9 +88,7 @@ public: bool canControlLoRaFemLna() const override; bool isLoRaFemLnaEnabled() const override; - // Temperature-gated cooling for the external PA. Every T-Beam 1W role - // services this from its main loop; the TX hooks also run it at the points - // where the PA can begin heating. + // Main-loop fallback if the background fan task could not be allocated. void updateFanControl(); void setFanEnabled(bool enabled); bool isFanEnabled() const; diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index e0d85b42..7d730c68 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -167,6 +167,7 @@ extends = LilyGo_TBeam_1W build_flags = ${LilyGo_TBeam_1W.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=512 @@ -179,6 +180,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h index 764f4194..682a4312 100644 --- a/variants/lilygo_tbeam_1w/variant.h +++ b/variants/lilygo_tbeam_1w/variant.h @@ -77,11 +77,21 @@ #define BATTERY_SENSE_SAMPLES 30 #define ADC_MULTIPLIER 2.9333f -// NTC temperature sensor +// NTC thermistor (Murata NCP18XH103F03RB, 10k, B25/50=3380K) +// Divider: 3.3V -> NTC -> GPIO14 -> 10k pull-down -> GND (Vadc rises with temp) #define NTC_PIN 14 +#define NTC_B 3380.0f +#define NTC_R25 10000.0f +#define NTC_R_FIXED 10000.0f +#define NTC_VCC_MV 3300.0f -// Fan control +// Fan control (GPIO41). NTC is PA-adjacent PCB temp, not die temp, so trip +// well below the SX1262/ESP32 85C operating limit. Hardware testing confirmed +// that this fan/MOSFET path is on/off; PWM below 100% does not spin the fan. #define FAN_CTRL_PIN 41 +#define FAN_TX_COOLDOWN_MS 15000 +#define FAN_DEFAULT_LO_C 30 // off below typical indoor idle (~86F) +#define FAN_DEFAULT_HI_C 36 // on at ~97F PCB; still far below 85C chip ratings // PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) // Value 0x06 = RADIOLIB_SX126X_PA_RAMP_1700U diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 4d7409ad..5845c57a 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -231,6 +231,7 @@ extends = T_Beam_S3_Supreme_SX1262 build_flags = ${T_Beam_S3_Supreme_SX1262.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=512 @@ -243,6 +244,7 @@ build_flags = ; -D CORE_DEBUG_LEVEL=4 build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 98e36a73..a9eb93ff 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -270,6 +270,7 @@ board_build.partitions = variants/lilygo_tlora_v2_1/dual_ota_1984k.csv build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 -D WIFI_SSID='"ssid"' @@ -278,6 +279,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=128 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index d9fd50fa..c99d9dac 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -27,8 +27,8 @@ build_flags = -D PIN_USER_BTN=0 -D PIN_VEXT_EN=45 -D PIN_VEXT_EN_ACTIVE=HIGH - -D LORA_TX_POWER=4 - -D MAX_LORA_TX_POWER=4 ; GC1109 datasheet says max input at TX port is +5dbm, confirmed full saturation seems to occur at around 3-4dbm + -D LORA_TX_POWER=5 ; default to 5, which gives ~22dbm + -D MAX_LORA_TX_POWER=13 ; tested with tinySA, 13 gives ~28dbm -D PIN_GPS_RX=38 -D PIN_GPS_TX=39 -D PIN_GPS_RESET=42 @@ -172,6 +172,7 @@ extends = meshnology_w12 build_flags = ${meshnology_w12.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=512 @@ -185,6 +186,7 @@ build_src_filter = ${meshnology_w12.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 644e6f8f..b3ed2849 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -144,6 +144,7 @@ extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 @@ -155,6 +156,7 @@ build_src_filter = ${nibble_screen_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 5a8a0b65..885ca7b0 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -141,6 +141,7 @@ extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 @@ -151,6 +152,7 @@ build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index c7746c3a..e26d8ab9 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -276,6 +276,7 @@ extends = rak3112 build_flags = ${rak3112.build_flags} -I examples/companion_radio/ui-orig + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -286,6 +287,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 53224050..3a0c23c1 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -69,34 +69,60 @@ lib_ignore = ${rp2040_base.lib_ignore} BLE -; [env:PicoW_companion_radio_ble] -; extends = rpi_picow -; build_flags = ${rpi_picow.build_flags} -; -D MAX_CONTACTS=100 -; -D MAX_GROUP_CHANNELS=8 -; -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 -; ; -D MESH_PACKET_LOGGING=1 -; ; -D MESH_DEBUG=1 -; build_src_filter = ${rpi_picow.build_src_filter} -; +<../examples/companion_radio/*.cpp> -; lib_deps = ${rpi_picow.lib_deps} -; densaugeo/base64 @ ~1.4.0 +[env:PicoW_companion_radio_ble] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 -; [env:PicoW_companion_radio_wifi] -; extends = rpi_picow -; build_flags = ${rpi_picow.build_flags} -; -D MAX_CONTACTS=100 -; -D MAX_GROUP_CHANNELS=8 -; -D WIFI_DEBUG_LOGGING=1 -; -D WIFI_SSID='"myssid"' -; -D WIFI_PWD='"mypwd"' -; ; -D MESH_PACKET_LOGGING=1 -; ; -D MESH_DEBUG=1 -; build_src_filter = ${rpi_picow.build_src_filter} -; +<../examples/companion_radio/*.cpp> -; lib_deps = ${rpi_picow.lib_deps} -; densaugeo/base64 @ ~1.4.0 +; USB + WiFi + BLE together; the interface manager fans frames out to all of them +[env:PicoW_companion_radio] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE + -D ENABLE_WIFI_INTERFACE + -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH + -D BLE_PIN_CODE=123456 +; NOTE: DO NOT ENABLE --> -D BLE_DEBUG_LOGGING=1 (shares Serial with the USB interface) +; NOTE: DO NOT ENABLE --> -D WIFI_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:PicoW_companion_radio_wifi] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D ENABLE_WIFI_INTERFACE + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='""' ; no default network, configure with 'set wifi.ssid' / 'set wifi.pwd' + -D WIFI_PWD='""' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 +lib_ignore = BLE [env:PicoW_terminal_chat] extends = rpi_picow diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 47624e34..0a3884ce 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -313,6 +313,7 @@ build_flags = -UENABLE_OTA -D DISABLE_LORA_OTA=1 -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -323,6 +324,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Station_G2.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index 04505737..189d3c2a 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -142,6 +142,7 @@ extends = Station_G3_ESP32 build_flags = ${Station_G3_ESP32.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -152,6 +153,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Station_G3_ESP32.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index 58d1b765..ac18bda1 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -178,6 +178,7 @@ extends = ThinkNode_M2 build_flags = ${ThinkNode_M2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -186,6 +187,7 @@ build_flags = -D WIFI_PWD='"mypwd"' build_src_filter = ${ThinkNode_M2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 93d646e0..a73a790f 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -190,6 +190,7 @@ extends = ThinkNode_M5 build_flags = ${ThinkNode_M5.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -198,6 +199,7 @@ build_flags = -D WIFI_PWD='"mypwd"' build_src_filter = ${ThinkNode_M5.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 6de358a2..351cc2c7 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -136,6 +136,7 @@ extends = ThinkNode_M7 build_flags = ${ThinkNode_M7.build_flags} -I examples/companion_radio/ui-orig + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=NullDisplayDriver @@ -146,6 +147,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index 870382e3..f50c7d71 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -22,7 +22,6 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_ROTARY_INPUT=1 -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 - -D UI_TZ_OFFSET=-4 # GMT-4 -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini index 20ff7b9c..4705bf17 100755 --- a/variants/thinknode_m9/platformio.ini +++ b/variants/thinknode_m9/platformio.ini @@ -135,6 +135,7 @@ extends = ThinkNode_M9 build_flags = ${ThinkNode_M9.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -144,6 +145,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M9.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp new file mode 100644 index 00000000..62d6f7f9 --- /dev/null +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp @@ -0,0 +1,50 @@ +#include +#include + +#include "WioTrackerL1Board.h" + +void WioTrackerL1Board::begin() { + NRF52BoardDCDC::begin(); + btn_prev_state = HIGH; + + pinMode(PIN_VBAT_READ, INPUT); // VBAT ADC input + // Set all button pins to INPUT_PULLUP + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + pinMode(PIN_BUTTON3, INPUT_PULLUP); + pinMode(PIN_BUTTON4, INPUT_PULLUP); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + + #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); + #endif + + Wire.begin(); + + pinMode(SX126X_POWER_EN, OUTPUT); + + #ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); + #endif + + delay(10); // give sx1262 some time to power up +} + +bool WioTrackerL1Board::setLoRaFemPaGainEnabled(bool enable) { + _is_pa_enabled = enable; + digitalWrite(SX126X_POWER_EN, enable ? HIGH : LOW); // enable/disable the 1W PA + return true; +} + +void WioTrackerL1Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + if (!_prefs) return; + + char gain[8]; + + gain[0] = 0; + _prefs->getByKey("fem_txgain", gain, 7); // get initial values + setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0); +} diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h new file mode 100644 index 00000000..400d0178 --- /dev/null +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +class WioTrackerL1Board : public NRF52BoardDCDC { +protected: + uint8_t btn_prev_state; + bool _is_pa_enabled = false; + KeyValueStore* _prefs = NULL; + +public: + WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool setLoRaFemPaGainEnabled(bool enable) override; + bool isLoRaFemPaGainEnabled() const override { return _is_pa_enabled; } + bool canControlLoRaFemPaGain() const override { return true; } + +#if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + } +#endif + + uint16_t getBattMilliVolts() override { + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL); + delay(10); + adcvalue = analogRead(PIN_VBAT_READ); + return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return "Seeed Wio Tracker L1"; + } + + void powerOff() override { + setLoRaFemPaGainEnabled(false); // turn Off PA + NRF52Board::powerOff(); + } +}; diff --git a/variants/wio-tracker-l1-1w/platformio.ini b/variants/wio-tracker-l1-1w/platformio.ini new file mode 100644 index 00000000..e5b4a3be --- /dev/null +++ b/variants/wio-tracker-l1-1w/platformio.ini @@ -0,0 +1,121 @@ +[WioTrackerL1-1W] +extends = nrf52_base +board = seeed-wio-tracker-l1 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + ${sensor_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/wio-tracker-l1-1w + -D WIO_TRACKER_L1 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_OLED_RESET=-1 + -D GPS_BAUD_RATE=9600 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/wio-tracker-l1-1w> + + + + +lib_deps= ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + adafruit/Adafruit SH110X @ ^2.1.13 + adafruit/Adafruit GFX Library @ ^1.12.1 + +[env:WioTrackerL1-1W_repeater] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${WioTrackerL1-1W.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Repeater"' + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1-1W_room_server] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/simple_room_server> +build_flags = ${WioTrackerL1-1W.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Room"' + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1-1W_companion_radio_usb] +extends = WioTrackerL1-1W +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${WioTrackerL1-1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SH1106Display + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D RADIO_FEM_TXGAIN=1 + -D ENABLE_USB_INTERFACE +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + + + +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1-1W_companion_radio_ble] +extends = WioTrackerL1-1W +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${WioTrackerL1-1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1106Display + -D UI_HAS_JOYSTICK=1 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D RADIO_FEM_TXGAIN=1 + -D ADVERT_NAME='"@@MAC"' + -D ENV_PIN_SDA=PIN_WIRE1_SDA + -D ENV_PIN_SCL=PIN_WIRE1_SCL + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1-1W_kiss_modem] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/wio-tracker-l1-1w/target.cpp b/variants/wio-tracker-l1-1w/target.cpp new file mode 100644 index 00000000..7a573258 --- /dev/null +++ b/variants/wio-tracker-l1-1w/target.cpp @@ -0,0 +1,40 @@ +#include +#include "target.h" +#include +#include + +WioTrackerL1Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false); + MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false); + MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false); + MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true); +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + diff --git a/variants/wio-tracker-l1-1w/target.h b/variants/wio-tracker-l1-1w/target.h new file mode 100644 index 00000000..05bc7ff1 --- /dev/null +++ b/variants/wio-tracker-l1-1w/target.h @@ -0,0 +1,35 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #if defined(WIO_TRACKER_L1_EINK) + #include + #else + #include + #endif + #include +#endif +#include + +extern WioTrackerL1Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern MomentaryButton joystick_left; + extern MomentaryButton joystick_right; + extern MomentaryButton back_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); + diff --git a/variants/wio-tracker-l1-1w/variant.cpp b/variants/wio-tracker-l1-1w/variant.cpp new file mode 100644 index 00000000..18efe7ea --- /dev/null +++ b/variants/wio-tracker-l1-1w/variant.cpp @@ -0,0 +1,78 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1,07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 29, // D5 P0.29 (AIN5) LORA_VDET 鈥?Pro 1W: P0.29 (not P1.08) + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.01 Mesh_LED (orange) 鈥?Pro 1W: P1.01 (not P1.15) + // Buzzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - OLED + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVE SDA + 44, // D18 P0.01 GROVE SCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + // JOYSTICK + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + + // VBAT ENABLE + 4, // D30 BAT_CTL + + // D31-D33 - Pro 1W only + 13, // D31 P0.13 BOOST_EN (Grove 5V Boost) + 47, // D32 P1.15 nRF_Sig_Charge_State (BQ25616 STAT) + 14, // D33 P0.14 LORA_PWR_EN (SX1262 + 1 W PA LDO) +}; + +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + // VBAT_ENABLE + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, HIGH); + + // set LED pin as output and set it low + pinMode(PIN_LED, OUTPUT); + digitalWrite(PIN_LED, LOW); + + // set buzzer pin as output and set it low + pinMode(12, OUTPUT); + digitalWrite(12, LOW); + pinMode(12, OUTPUT); +} diff --git a/variants/wio-tracker-l1-1w/variant.h b/variants/wio-tracker-l1-1w/variant.h new file mode 100644 index 00000000..7520460a --- /dev/null +++ b/variants/wio-tracker-l1-1w/variant.h @@ -0,0 +1,130 @@ +#ifndef _SEEED_WIO_TRACKER_L1_H_ +#define _SEEED_WIO_TRACKER_L1_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#define PINS_COUNT (34) +#define NUM_DIGITAL_PINS (34) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED (11) +#define LED_BLUE (-1) // Disable annoying flashing caused by Bluefruit +#define LED_BUILTIN PIN_LED +#define P_LORA_TX_LED PIN_LED +#define LED_STATE_ON 1 + +// Buttons +#define PIN_BUTTON1 (13) // Menu / User Button +#define PIN_BUTTON2 (25) // Joystick Up +#define PIN_BUTTON3 (26) // Joystick Down +#define PIN_BUTTON4 (27) // Joystick Left +#define PIN_BUTTON5 (28) // Joystick Right +#define PIN_BUTTON6 (29) // Joystick Press +#define PIN_BACK_BTN PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define JOYSTICK_PRESS PIN_BUTTON6 +#define PIN_USER_BTN PIN_BUTTON6 + +// Buzzer +// #define PIN_BUZZER (12) // Buzzer pin (defined per firmware type) + +#define VBAT_ENABLE (30) + +// Analog pins +#define PIN_VBAT_READ (16) +#define AREF_VOLTAGE (3.6F) +#define ADC_MULTIPLIER (2.0F) +#define ADC_RESOLUTION (12) + +// Serial interfaces +#define PIN_SERIAL1_RX (7) +#define PIN_SERIAL1_TX (6) + +// SPI Interfaces +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +// BQ25616 single-wire charge status (Pro 1W) +#define PIN_BOOST_EN (31) // D31 / P0.13 鈥?Grove 5V Boost enable +#define EXT_CHRG_DETECT (32) // D32 / P1.15 鈥?BQ25616 STAT +#define EXT_CHRG_DETECT_VALUE LOW // 0 = charging, 1 = full / charger sleep +#define BOOST_EN_ACTIVE HIGH // HIGH enables Grove 5V Boost + +// Lora Pins +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI +#define P_LORA_DIO_1 (1) +#define P_LORA_RESET (2) +#define P_LORA_BUSY (3) +#define P_LORA_NSS (4) +#define SX126X_RXEN (5) +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) + +// LORA_PWR_EN (P0.14): external LDO enable for the SX1262 + 1 W PA on the Pro 1W board. +#define LORA_PWR_EN (33) +#define SX126X_POWER_EN LORA_PWR_EN + +// Wire Interfaces +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE_SDA (14) +#define PIN_WIRE_SCL (15) +#define PIN_WIRE1_SDA (18) +#define PIN_WIRE1_SCL (17) +#define I2C_NO_RESCAN +#define DISPLAY_ADDRESS 0x3D // SH1106 OLED I2C address + +// GPS L76KB +#define GPS_BAUDRATE 9600 +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_STANDBY (0) +#define PIN_GPS_EN (PIN_GPS_STANDBY) + +// QSPI Pins +#define PIN_QSPI_SCK (19) +#define PIN_QSPI_CS (20) +#define PIN_QSPI_IO0 (21) +#define PIN_QSPI_IO1 (22) +#define PIN_QSPI_IO2 (23) +#define PIN_QSPI_IO3 (24) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// // EInk on SPI1 +// #define PIN_DISPLAY_CS (36) +// #define PIN_DISPLAY_BUSY (35) +// #define PIN_DISPLAY_DC (34) +// #define PIN_DISPLAY_RST (32) + +#define PIN_SPI1_MISO (37) +#define PIN_SPI1_MOSI (33) +#define PIN_SPI1_SCK (31) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +#endif \ No newline at end of file diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 3ae23397..7f85e307 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -121,8 +121,10 @@ extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio/*.cpp> + + + build_flags = ${Xiao_esp32_C3.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 15a81764..e273ffd1 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -286,12 +286,14 @@ extends = Xiao_S3_WIO build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> build_flags = ${Xiao_S3_WIO.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256