Merge meshcore-dev dev with companion and board integrations

This commit is contained in:
mikecarper committed 2026-09-15 20:51:43 -07:00
commit 476aac92de
78 files changed
+1904 -190

No files matched your search

+1
View File
@@ -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
+3 -3
View File
@@ -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 "+<helpers/esp32/SerialWifiInterface.cpp>"
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 "+<helpers/wifi/SerialWifiInterface.cpp>"
fi
# WiFi recipes can have the inverse narrow filter. Preserve the existing
+2
View File
@@ -20,10 +20,12 @@ for item in menv.get("CPPDEFINES", []):
src_filter.append("+<helpers/stm32/*>")
elif item == "ESP32":
src_filter.append("+<helpers/esp32/*>")
src_filter.append("+<helpers/wifi/*>")
elif item == "NRF52_PLATFORM":
src_filter.append("+<helpers/nrf52/*>")
elif item == "RP2040_PLATFORM":
src_filter.append("+<helpers/rp2040/*>")
src_filter.append("+<helpers/wifi/*>")
# DISPLAY HANDLING
elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS":
+10 -5
View File
@@ -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.
---
+2 -2
View File
@@ -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).
+2 -2
View File
@@ -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
+9 -1
View File
@@ -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:
-5
View File
@@ -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}
```
+49
View File
@@ -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");
+181 -11
View File
@@ -8,6 +8,9 @@
#endif
#include <Arduino.h> // needed for PlatformIO
#ifdef ENABLE_WIFI_INTERFACE
#include <WiFi.h>
#endif
#include <Mesh.h>
#include <helpers/CompanionHardwareCommandCompat.h>
#include <helpers/CompanionStatusResponse.h>
@@ -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<uint32_t>(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();
+33
View File
@@ -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;
+40 -25
View File
@@ -1,5 +1,9 @@
#pragma once
#if defined(ENABLE_WIFI_INTERFACE) && !defined(WIFI_SSID)
#define WIFI_SSID ""
#endif
#include <Arduino.h>
#include <cstdint>
@@ -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;
+59 -3
View File
@@ -53,19 +53,29 @@ MultiSerialInterface interface_manager;
// include nrf52 bluetooth interface
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface bluetooth_interface;
#elif defined(RP2040_PLATFORM)
// include rp2040 (Pico W / CYW43) bluetooth interface
#include <helpers/rp2040/SerialBLEInterface.h>
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 <helpers/esp32/SerialWifiInterface.h>
#include <helpers/wifi/SerialWifiInterface.h>
#include <helpers/WiFiSetupPortal.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/WiFiReconnectPolicy.h>
@@ -86,6 +96,19 @@ MultiSerialInterface interface_manager;
#ifndef WIFI_PWD
#define WIFI_PWD ""
#endif
#elif defined(RP2040_PLATFORM)
#include <helpers/wifi/SerialWifiInterface.h>
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<uint32_t>(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();
+63 -6
View File
@@ -16,7 +16,7 @@
#include "../CompanionWiFi.h"
#include "target.h"
#include <time.h>
#ifdef WIFI_SSID
#if defined(ENABLE_WIFI_INTERFACE) || defined(WIFI_SSID)
#include <WiFi.h>
#endif
#if UI_WIFI_SETUP_HOME_PAGE == 1
@@ -28,10 +28,6 @@
#include <esp_timer.h>
#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<uint32_t>(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<uint32_t>(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
+2 -2
View File
@@ -8,7 +8,7 @@
#include "target.h"
#include "u8g2_icons.h"
#ifdef WIFI_SSID
#if defined(ENABLE_WIFI_INTERFACE) || defined(WIFI_SSID)
#include <WiFi.h>
#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);
+10 -2
View File
@@ -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
+10 -2
View File
@@ -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
+10 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2
View File
@@ -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; }
+1
View File
@@ -1,4 +1,5 @@
#include "ConfigSerializer.h"
#include <stdlib.h> // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not)
bool ConfigSerializer::saveSerial(Stream& s) {
Context context(&s, OP::WRITE);
+6 -10
View File
@@ -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
+4
View File
@@ -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
};
+1
View File
@@ -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) {
+6 -1
View File
@@ -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);
+4 -1
View File
@@ -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);
};
+1 -1
View File
@@ -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() {
+208
View File
@@ -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 <BluetoothLock.h>
#include <stdio.h>
#include <string.h>
// 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
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include "../BaseSerialInterface.h"
#include <BLE.h>
#include <btstack.h>
#include <helpers/BluetoothMac.h>
// 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 <Arduino.h>
#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
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
#include <helpers/esp32/SerialWifiInterface.h>
#include <helpers/wifi/SerialWifiInterface.h>
#include <cassert>
#include <cstdio>
#include <cstdlib>
+3 -3
View File
@@ -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"* ]] \
+1 -1
View File
@@ -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):
+74
View File
@@ -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 <cassert>
#include <cstdint>
#include <cstring>
#include <cstddef>
#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()
+29 -3
View File
@@ -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 <helpers/IdentityStore.h>'))
(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)
@@ -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;
+1 -1
View File
@@ -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+"
+2 -1
View File
@@ -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:
+3
View File
@@ -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;
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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 "+<helpers/esp32/SerialWifiInterface.cpp>" in profile
assert "+<helpers/wifi/SerialWifiInterface.cpp>" 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 "
+4
View File
@@ -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}
+<helpers/ui/buzzer.cpp>
+<helpers/ui/NullDisplayDriver.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../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}
+<helpers/ui/buzzer.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
@@ -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}
+<helpers/ui/ST7735Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+4
View File
@@ -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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
+4
View File
@@ -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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../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}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
+1
View File
@@ -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;
+4
View File
@@ -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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../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}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
+287 -52
View File
@@ -3,18 +3,10 @@
#include <driver/gpio.h>
#include <driver/rtc_io.h>
#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 <errno.h>
#include <limits.h>
#include <math.h>
#include <stdlib.h>
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<TBeam1WBoard*>(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<uint32_t>(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<uint32_t>(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;
}
+34 -8
View File
@@ -1,6 +1,8 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <helpers/ESP32Board.h>
#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;
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
+12 -2
View File
@@ -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
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+4 -2
View File
@@ -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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
@@ -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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
@@ -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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-orig/*.cpp>
lib_deps =
+53 -27
View File
@@ -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}
+<helpers/rp2040/SerialBLEInterface.cpp>
+<../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}
+<helpers/rp2040/SerialBLEInterface.cpp>
+<helpers/wifi/*.cpp>
+<../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}
+<helpers/wifi/*.cpp>
+<../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
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/buzzer.cpp>
+<../examples/companion_radio/*.cpp>
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/NullDisplayDriver.cpp>
+<../examples/companion_radio/*.cpp>
-1
View File
@@ -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
+2
View File
@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
@@ -0,0 +1,50 @@
#include <Arduino.h>
#include <Wire.h>
#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);
}
@@ -0,0 +1,47 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/NRF52Board.h>
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();
}
};
+121
View File
@@ -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}
+<WioTrackerL1Board.cpp>
+<../variants/wio-tracker-l1-1w>
+<helpers/ui/SH1106Display.cpp>
+<helpers/sensors>
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}
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+<helpers/ui/SH1106Display.cpp>
+<helpers/ui/buzzer.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}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/buzzer.cpp>
+<../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/>
+40
View File
@@ -0,0 +1,40 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
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
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <WioTrackerL1Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#if defined(WIO_TRACKER_L1_EINK)
#include <helpers/ui/GxEPDDisplay.h>
#else
#include <helpers/ui/SH1106Display.h>
#endif
#include <helpers/ui/MomentaryButton.h>
#endif
#include <helpers/sensors/EnvironmentSensorManager.h>
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();
+78
View File
@@ -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);
}
+130
View File
@@ -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
+2
View File
@@ -121,8 +121,10 @@ extends = Xiao_esp32_C3
build_src_filter = ${Xiao_esp32_C3.build_src_filter}
+<../examples/companion_radio/*.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.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
+2
View File
@@ -286,12 +286,14 @@ extends = Xiao_S3_WIO
build_src_filter = ${Xiao_S3_WIO.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../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