Keep T-Beam companion available without radio
This commit is contained in:
1 parent
7afa6b11a8
commit
bcedfdd019
11 files changed
+310
-53
No files matched your search
@@ -1960,12 +1960,12 @@ is_nrf52_companion_radio_full_target() {
|
||||
}
|
||||
|
||||
requires_esp32_companion_full_ota_fallback() {
|
||||
# Some classic ESP32 companions cannot hold their configured high-capacity
|
||||
# contact, channel, and offline-queue tables together with LoRa OTA in
|
||||
# Some ESP32 companions cannot hold their configured high-capacity contact,
|
||||
# channel, and offline-queue tables together with BLE, WiFi, and LoRa OTA in
|
||||
# internal DRAM. Keep their ordinary high-capacity image unchanged and emit
|
||||
# a separately named FULL OTA image with the companion default capacities.
|
||||
case "${1,,}" in
|
||||
heltec_v2_companion_radio_wifi|lilygo_tlora_v2_1_1_6_companion_radio_wifi|meshadventurer_sx1262_companion_radio_usb|meshadventurer_sx1268_companion_radio_usb) return 0 ;;
|
||||
heltec_v2_companion_radio_wifi|lilygo_tbeam_1w_companion_radio_wifi|lilygo_tlora_v2_1_1_6_companion_radio_wifi|meshadventurer_sx1262_companion_radio_usb|meshadventurer_sx1268_companion_radio_usb) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
@@ -2180,7 +2180,7 @@ apply_esp32_full_size_profile() {
|
||||
fi
|
||||
|
||||
if requires_esp32_companion_full_ota_fallback "$env_name"; then
|
||||
append_platformio_build_unflags "-DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=128"
|
||||
append_platformio_build_unflags "-DMAX_CONTACTS=350 -DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=256 -DOFFLINE_QUEUE_SIZE=128"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=8 -DOFFLINE_QUEUE_SIZE=16"
|
||||
fi
|
||||
}
|
||||
@@ -2382,11 +2382,11 @@ apply_companion_radio_full_profile() {
|
||||
append_platformio_build_unflags "-DOTA_FOLDER_SERIAL"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UOTA_FOLDER_SERIAL -DWIFI_OTA_SEEDER=1"
|
||||
|
||||
# BLE + WiFi exhaust internal DRAM on the two high-capacity classic ESP32
|
||||
# recipes. Use the same measured-safe tables as their existing FULL OTA
|
||||
# fallback without changing ordinary USB/BLE/WiFi companion builds.
|
||||
# BLE + WiFi exhaust internal DRAM on these high-capacity ESP32 recipes. Use
|
||||
# measured-safe tables for FULL OTA without changing ordinary USB/BLE/WiFi
|
||||
# companion builds.
|
||||
if requires_esp32_companion_full_ota_fallback "$pio_env_name"; then
|
||||
append_platformio_build_unflags "-DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=128"
|
||||
append_platformio_build_unflags "-DMAX_CONTACTS=350 -DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=256 -DOFFLINE_QUEUE_SIZE=128"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=8 -DOFFLINE_QUEUE_SIZE=16"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1293,6 +1293,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
clearTerminalTrace();
|
||||
#endif
|
||||
saved_radio_apply_pending = false;
|
||||
_radio_available = true;
|
||||
radio_apply_retry_at = 0;
|
||||
radio_apply_failures = 0;
|
||||
command_radio_apply_pending = false;
|
||||
@@ -1373,7 +1374,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::begin(bool has_display) {
|
||||
void MyMesh::begin(bool has_display, bool radio_available) {
|
||||
_radio_available = radio_available;
|
||||
setRadioAvailable(radio_available);
|
||||
BaseChatMesh::begin();
|
||||
|
||||
const bool is_new_install = !_store->loadMainIdentity(self_id)
|
||||
@@ -1487,19 +1490,7 @@ void MyMesh::begin(bool has_display) {
|
||||
_store->saveChannels(this);
|
||||
}
|
||||
|
||||
saved_radio_apply_pending = !applySavedRadioParams();
|
||||
if (!saved_radio_apply_pending) {
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
}
|
||||
const bool fem_gain_changed = board.canControlLoRaFemLna()
|
||||
&& board.isLoRaFemLnaEnabled() != (_prefs.radio_fem_rxgain != 0);
|
||||
if (board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain) && fem_gain_changed) {
|
||||
_radio->recalibrateNoiseFloor();
|
||||
}
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
configureRadioFromPrefs();
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
|
||||
applyMQTTDefaults(&_mqtt_prefs);
|
||||
@@ -1539,7 +1530,42 @@ void MyMesh::begin(bool has_display) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::configureRadioFromPrefs() {
|
||||
if (!_radio_available) {
|
||||
saved_radio_apply_pending = false;
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
MESH_DEBUG_PRINTLN("Radio unavailable: companion services are running in recovery mode");
|
||||
return;
|
||||
}
|
||||
|
||||
saved_radio_apply_pending = !applySavedRadioParams();
|
||||
if (!saved_radio_apply_pending) {
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
}
|
||||
const bool fem_gain_changed = board.canControlLoRaFemLna()
|
||||
&& board.isLoRaFemLnaEnabled() != (_prefs.radio_fem_rxgain != 0);
|
||||
if (board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain) && fem_gain_changed) {
|
||||
_radio->recalibrateNoiseFloor();
|
||||
}
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
void MyMesh::activateRadio() {
|
||||
if (_radio_available) return;
|
||||
|
||||
_radio_available = true;
|
||||
setRadioAvailable(true);
|
||||
configureRadioFromPrefs();
|
||||
MESH_DEBUG_PRINTLN("Radio recovery completed; mesh transport is active");
|
||||
}
|
||||
|
||||
mesh::RadioParamApplyResult MyMesh::tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
if (!_radio_available) return mesh::RadioParamApplyResult::FAILED;
|
||||
|
||||
uint32_t rx_us = _prefs.rx_ps_rx_us;
|
||||
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
|
||||
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0
|
||||
@@ -2045,7 +2071,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
snprintf(reply, 160, "Error: TX power must be -9 to %d", MAX_LORA_TX_POWER);
|
||||
} else {
|
||||
_prefs.tx_power_dbm = static_cast<int8_t>(parsed);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
if (_radio_available) radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
@@ -2071,7 +2097,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
} else {
|
||||
if (strcmp(key, "radio.rxgain") == 0) {
|
||||
_prefs.rx_boosted_gain = enabled;
|
||||
radio_driver.setRxBoostedGainMode(enabled);
|
||||
if (_radio_available) radio_driver.setRxBoostedGainMode(enabled);
|
||||
} else {
|
||||
_prefs.client_repeat = enabled;
|
||||
}
|
||||
@@ -2837,7 +2863,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
} else {
|
||||
_prefs.tx_power_dbm = power;
|
||||
savePrefs();
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
if (_radio_available) radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
writeOKFrame();
|
||||
}
|
||||
} else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) {
|
||||
@@ -3510,7 +3536,7 @@ bool MyMesh::applyAndSaveFemRxGain(bool enabled) {
|
||||
const bool changed = board.isLoRaFemLnaEnabled() != enabled;
|
||||
if (!board.setLoRaFemLnaEnabled(enabled)) return false;
|
||||
|
||||
if (changed) _radio->recalibrateNoiseFloor();
|
||||
if (changed && _radio_available) _radio->recalibrateNoiseFloor();
|
||||
_prefs.radio_fem_rxgain = enabled ? 1 : 0;
|
||||
_prefs.radio_fem_rxgain_override = 1;
|
||||
savePrefs();
|
||||
@@ -3636,7 +3662,8 @@ bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!radio_driver.setRxPowerSaving(enabled != 0, rx_us, sleep_us)) {
|
||||
if (_radio_available
|
||||
&& !radio_driver.setRxPowerSaving(enabled != 0, rx_us, sleep_us)) {
|
||||
strcpy(reply, "Error: radio busy; retry");
|
||||
return false;
|
||||
}
|
||||
@@ -4846,7 +4873,9 @@ bool MyMesh::advert() {
|
||||
|
||||
// To check if there is pending work
|
||||
bool MyMesh::hasPendingWork() const {
|
||||
if (radio_driver.isWatchdogObserving() || radio_driver.isCalibratingNoiseFloor()) return true;
|
||||
if (_radio_available
|
||||
&& (radio_driver.isWatchdogObserving()
|
||||
|| radio_driver.isCalibratingNoiseFloor())) return true;
|
||||
return (_serial != NULL && _serial->hasPendingIO())
|
||||
|| (_iter_started && _serial != NULL && _serial->isConnected())
|
||||
|| command_radio_apply_pending
|
||||
|
||||
@@ -112,7 +112,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost
|
||||
public:
|
||||
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
|
||||
|
||||
void begin(bool has_display);
|
||||
void begin(bool has_display, bool radio_available = true);
|
||||
void activateRadio();
|
||||
bool isRadioReady() const { return _radio_available; }
|
||||
void startInterface(BaseSerialInterface &serial);
|
||||
|
||||
const char *getNodeName();
|
||||
@@ -311,6 +313,7 @@ private:
|
||||
void updateGpsTelemetryPolicy();
|
||||
mesh::RadioParamApplyResult tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr);
|
||||
bool applySavedRadioParams();
|
||||
void configureRadioFromPrefs();
|
||||
void finishRadioParamApply(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t repeat);
|
||||
void cancelPendingRadioParamApply();
|
||||
void servicePendingRadioParamApply();
|
||||
@@ -373,6 +376,7 @@ private:
|
||||
char _terminal_trace_target[32];
|
||||
#endif
|
||||
bool saved_radio_apply_pending;
|
||||
bool _radio_available;
|
||||
unsigned long radio_apply_retry_at;
|
||||
uint8_t radio_apply_failures;
|
||||
bool command_radio_apply_pending;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#ifdef ESP32_PLATFORM
|
||||
#include "esp_bt.h"
|
||||
#endif
|
||||
#ifdef TBEAM_1W
|
||||
#include <esp_random.h>
|
||||
#endif
|
||||
|
||||
// Believe it or not, this std C function is busted on some platforms!
|
||||
static uint32_t _atoi(const char* sp) {
|
||||
@@ -122,6 +125,29 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
|
||||
/* END GLOBAL OBJECTS */
|
||||
|
||||
#ifdef TBEAM_1W
|
||||
static bool companion_radio_available = true;
|
||||
static unsigned long companion_radio_retry_at = 0;
|
||||
static const unsigned long COMPANION_RADIO_RETRY_MS = 60000UL;
|
||||
|
||||
static void serviceCompanionRadioRecovery() {
|
||||
if (companion_radio_available
|
||||
|| (long)(millis() - companion_radio_retry_at) < 0) return;
|
||||
|
||||
companion_radio_retry_at = millis() + COMPANION_RADIO_RETRY_MS;
|
||||
Serial.println("Radio recovery probe starting");
|
||||
board.powerCycleRadio();
|
||||
if (!radio_init()) {
|
||||
Serial.println("Radio recovery probe failed; companion services remain available");
|
||||
return;
|
||||
}
|
||||
|
||||
companion_radio_available = true;
|
||||
the_mesh.activateRadio();
|
||||
Serial.println("Radio recovered; mesh transport is active");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ESP32_PLATFORM
|
||||
static int8_t applied_power_saving = -1;
|
||||
static int8_t attempted_power_saving = -1;
|
||||
@@ -705,17 +731,39 @@ void setup() {
|
||||
#endif
|
||||
|
||||
int radioinit_attempts = 0;
|
||||
while (!radio_init()) {
|
||||
bool radio_available = false;
|
||||
while (!(radio_available = radio_init())) {
|
||||
++radioinit_attempts;
|
||||
MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts);
|
||||
if (radioinit_attempts >= 3) {
|
||||
#if defined(TBEAM_1W)
|
||||
// Continue into a recovery-capable Companion instead of trapping native
|
||||
// USB in a reboot loop. The main loop retries the radio independently.
|
||||
Serial.println("Radio unavailable; starting display, USB, BLE, and WiFi recovery services");
|
||||
break;
|
||||
#else
|
||||
MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting");
|
||||
board.reboot();
|
||||
#endif
|
||||
}
|
||||
delay(500);
|
||||
}
|
||||
|
||||
#ifdef TBEAM_1W
|
||||
companion_radio_available = radio_available;
|
||||
companion_radio_retry_at = millis() + COMPANION_RADIO_RETRY_MS;
|
||||
fast_rng.begin(radio_available ? radio_driver.getRngSeed() : esp_random());
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (!radio_available && disp != NULL) {
|
||||
disp->startFrame();
|
||||
disp->drawTextCentered(disp->width() / 2, 20, "Radio unavailable");
|
||||
disp->drawTextCentered(disp->width() / 2, 40, "Starting WiFi...");
|
||||
disp->endFrame();
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
InternalFS.begin();
|
||||
@@ -748,6 +796,8 @@ void setup() {
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
,
|
||||
radio_available
|
||||
);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
LittleFS.begin();
|
||||
@@ -758,6 +808,8 @@ void setup() {
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
,
|
||||
radio_available
|
||||
);
|
||||
#elif defined(ESP32)
|
||||
SPIFFS.begin(true);
|
||||
@@ -768,6 +820,8 @@ void setup() {
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
,
|
||||
radio_available
|
||||
);
|
||||
#else
|
||||
#error "need to define filesystem"
|
||||
@@ -874,6 +928,9 @@ void loop() {
|
||||
board.feedWatchdog();
|
||||
#endif
|
||||
the_mesh.loop();
|
||||
#ifdef TBEAM_1W
|
||||
serviceCompanionRadioRecovery();
|
||||
#endif
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
serviceUsbTerminal();
|
||||
#endif
|
||||
|
||||
+44
-3
@@ -35,6 +35,28 @@ static uint32_t scaleCADDelayForQueue(uint32_t normal_delay, int ready_count,
|
||||
return scaled < floor ? floor : scaled;
|
||||
}
|
||||
|
||||
void Dispatcher::setRadioAvailable(bool available) {
|
||||
if (radio_available == available) return;
|
||||
|
||||
radio_available = available;
|
||||
if (!dispatcher_started || !radio_available) return;
|
||||
|
||||
const unsigned long now = _ms->getMillis();
|
||||
radio_nonrx_start = now;
|
||||
next_floor_calib_time = now;
|
||||
next_agc_reset_time = now;
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
last_observed_radio_irq = _radio->getLastRadioInterruptMillis();
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
last_radio_activity_ms = now;
|
||||
#else
|
||||
radio_liveness.begin(now);
|
||||
nonrx_soft_recovery_attempted = false;
|
||||
#endif
|
||||
MESH_DEBUG_PRINTLN("Dispatcher: radio transport activated");
|
||||
}
|
||||
|
||||
void Dispatcher::begin() {
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
@@ -49,10 +71,17 @@ void Dispatcher::begin() {
|
||||
tx_budget_ms = (unsigned long)(duty_cycle_window_ms * duty_cycle);
|
||||
last_budget_update = _ms->getMillis();
|
||||
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
const unsigned long now = _ms->getMillis();
|
||||
last_observed_radio_irq = _radio->getLastRadioInterruptMillis();
|
||||
dispatcher_started = true;
|
||||
if (radio_available) {
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
last_observed_radio_irq = _radio->getLastRadioInterruptMillis();
|
||||
} else {
|
||||
prev_isrecv_mode = false;
|
||||
last_observed_radio_irq = 0;
|
||||
MESH_DEBUG_PRINTLN("Dispatcher: starting without a radio transport");
|
||||
}
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
last_radio_activity_ms = now;
|
||||
#else
|
||||
@@ -267,6 +296,14 @@ uint32_t Dispatcher::getRadioWatchdogMillis() const {
|
||||
#endif
|
||||
|
||||
void Dispatcher::loop() {
|
||||
if (!radio_available) {
|
||||
// Keep packet-manager cleanup alive, but never touch an uninitialized or
|
||||
// physically unavailable radio. sendPacket() rejects new outbound work
|
||||
// while this state is active, so no queue can silently fill up.
|
||||
releaseDroppedOutbound();
|
||||
return;
|
||||
}
|
||||
|
||||
if (millisHasNowPassed(next_floor_calib_time)) {
|
||||
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
|
||||
_radio->setCADEnabled(getCADEnabled());
|
||||
@@ -713,6 +750,10 @@ void Dispatcher::releasePacket(Packet* packet) {
|
||||
}
|
||||
|
||||
bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
|
||||
if (!radio_available) {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): radio unavailable", getLogDateTime());
|
||||
return false;
|
||||
}
|
||||
if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
|
||||
return false;
|
||||
|
||||
@@ -245,6 +245,8 @@ class Dispatcher {
|
||||
unsigned long radio_nonrx_start;
|
||||
unsigned long next_floor_calib_time, next_agc_reset_time;
|
||||
bool prev_isrecv_mode;
|
||||
bool radio_available;
|
||||
bool dispatcher_started;
|
||||
bool outbound_radio_retry_pending;
|
||||
bool outbound_radio_retry_used;
|
||||
#ifndef RADIO_LIVENESS_SOFT_ONLY
|
||||
@@ -286,6 +288,8 @@ protected:
|
||||
_err_flags = 0;
|
||||
radio_nonrx_start = 0;
|
||||
prev_isrecv_mode = true;
|
||||
radio_available = true;
|
||||
dispatcher_started = false;
|
||||
tx_budget_ms = 0;
|
||||
last_budget_update = 0;
|
||||
duty_cycle_window_ms = 3600000;
|
||||
@@ -346,6 +350,8 @@ protected:
|
||||
}
|
||||
bool queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis);
|
||||
bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
|
||||
void setRadioAvailable(bool available);
|
||||
bool isRadioAvailable() const { return radio_available; }
|
||||
|
||||
public:
|
||||
void begin();
|
||||
|
||||
@@ -585,17 +585,31 @@ bool WebConfigServer::startSetupMode(char reply[]) {
|
||||
// AP_STA (not pure AP) so the WiFi scan for the SSID picker works while
|
||||
// the AP is up. STA stays unconnected - the bridge won't touch WiFi
|
||||
// while wifi_ssid is empty, and `start webconfig ap` requires it stopped.
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
bool mode_ok = WiFi.mode(WIFI_AP_STA);
|
||||
// Setup mode has no login. Drop any STA association so the open setup API is
|
||||
// reachable only from the setup AP, not from the operator's LAN.
|
||||
WiFi.setAutoReconnect(false);
|
||||
WiFi.disconnect(false, true);
|
||||
bool disconnect_ok = WiFi.disconnect(false, true);
|
||||
delay(100);
|
||||
snprintf(_ap_ssid, sizeof(_ap_ssid), "MeshCore-Setup-%02X%02X", _pub_key[0], _pub_key[1]);
|
||||
bool ap_ok = false;
|
||||
for (uint8_t attempt = 1; attempt <= 6 && !ap_ok; ++attempt) {
|
||||
#ifdef WEBCONFIG_AP_PASSWORD
|
||||
bool ap_ok = WiFi.softAP(_ap_ssid, WEBCONFIG_AP_PASSWORD);
|
||||
ap_ok = WiFi.softAP(_ap_ssid, WEBCONFIG_AP_PASSWORD);
|
||||
#else
|
||||
bool ap_ok = WiFi.softAP(_ap_ssid);
|
||||
ap_ok = WiFi.softAP(_ap_ssid);
|
||||
#endif
|
||||
if (ap_ok) break;
|
||||
|
||||
Serial.printf(
|
||||
"WebConfig AP attempt %u failed: mode_ok=%d disconnect_ok=%d mode=%d heap=%u largest=%u\n",
|
||||
(unsigned)attempt, mode_ok, disconnect_ok, (int)WiFi.getMode(),
|
||||
(unsigned)ESP.getFreeHeap(),
|
||||
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
|
||||
WiFi.softAPdisconnect(true);
|
||||
delay(250);
|
||||
mode_ok = WiFi.mode(WIFI_AP_STA);
|
||||
}
|
||||
if (!ap_ok) {
|
||||
if (!promote_lan) WiFi.mode(WIFI_OFF);
|
||||
strcpy(reply, "Err: failed to start AP");
|
||||
@@ -782,7 +796,11 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
} else if (_connect_deadline && (int32_t)(now - _connect_deadline) >= 0) {
|
||||
Serial.printf("WebConfig: WiFi '%s' unavailable; opening setup AP\n", _wifi_ssid);
|
||||
const bool retry_saved_wifi = _wifi_ssid[0] != 0;
|
||||
WiFi.disconnect(true);
|
||||
// Keep the WiFi driver running while changing from STA to AP+STA.
|
||||
// Powering it off here and starting an AP immediately can race the
|
||||
// asynchronous ESP32-S3 netif teardown and make softAP() return false.
|
||||
WiFi.disconnect(false, false);
|
||||
delay(50);
|
||||
_mode = MODE_OFF;
|
||||
_connect_deadline = 0;
|
||||
char ignored[160];
|
||||
|
||||
@@ -9,6 +9,7 @@ public:
|
||||
|
||||
class TestRadio : public mesh::Radio {
|
||||
public:
|
||||
int begins = 0;
|
||||
uint8_t pending_rx[MAX_TRANS_UNIT];
|
||||
int pending_rx_len = 0;
|
||||
int send_starts = 0;
|
||||
@@ -26,6 +27,7 @@ public:
|
||||
uint8_t sent_raw[3][MAX_TRANS_UNIT] = {};
|
||||
int sent_len[3] = {};
|
||||
|
||||
void begin() override { begins++; }
|
||||
int recvRaw(uint8_t* dest, int max_len) override {
|
||||
if (pending_rx_len == 0) return 0;
|
||||
int len = pending_rx_len < max_len ? pending_rx_len : max_len;
|
||||
@@ -118,8 +120,37 @@ public:
|
||||
bool parse(mesh::Packet* packet, const uint8_t* raw, int len) {
|
||||
return tryParsePacket(packet, raw, len);
|
||||
}
|
||||
void makeRadioAvailable(bool available) { setRadioAvailable(available); }
|
||||
};
|
||||
|
||||
TEST(Dispatcher, UnavailableRadioDoesNotBlockStartupAndRejectsSends) {
|
||||
RxReservePacketManager manager(4, 1);
|
||||
TestClock clock;
|
||||
TestRadio radio;
|
||||
TestDispatcher dispatcher(radio, clock, manager);
|
||||
|
||||
dispatcher.makeRadioAvailable(false);
|
||||
dispatcher.begin();
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.begins);
|
||||
EXPECT_EQ(0, radio.cad_set_calls);
|
||||
|
||||
mesh::Packet* packet = dispatcher.obtainNewPacket();
|
||||
ASSERT_NE(packet, nullptr);
|
||||
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
|
||||
packet->payload[0] = 0x42;
|
||||
packet->payload_len = 1;
|
||||
EXPECT_FALSE(dispatcher.sendPacket(packet, 0));
|
||||
EXPECT_EQ(4, manager.getFreeCount());
|
||||
EXPECT_EQ(packet, dispatcher.failed_packet);
|
||||
|
||||
dispatcher.makeRadioAvailable(true);
|
||||
EXPECT_EQ(1, radio.begins);
|
||||
clock.now = 1;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(1, radio.cad_set_calls);
|
||||
}
|
||||
|
||||
TEST(Packet, ReadFromRejectsTruncatedHeadersAndPaths) {
|
||||
mesh::Packet packet;
|
||||
const uint8_t one_byte[] = {ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT)};
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
#include "TBeam1WBoard.h"
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/rtc_io.h>
|
||||
|
||||
void TBeam1WBoard::begin() {
|
||||
ESP32Board::begin();
|
||||
|
||||
// Power on radio module (must be done before radio init)
|
||||
// ESP32Board::enterDeepSleep() holds NSS high. The LilyGo factory firmware
|
||||
// can also hold the dedicated radio LDO off. These pad holds survive wake
|
||||
// and can survive a subsequent software reset, so release them on every
|
||||
// startup before RadioLib tries to select the SX1262.
|
||||
esp_err_t nss_hold_result = rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
|
||||
esp_err_t dio_result = rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
|
||||
esp_err_t power_hold_result = gpio_hold_dis((gpio_num_t)SX126X_POWER_EN);
|
||||
MESH_DEBUG_PRINTLN("T-Beam 1W pad recovery: NSS=%d DIO1=%d LDO=%d",
|
||||
nss_hold_result, dio_result, power_hold_result);
|
||||
|
||||
// The microSD card and radio share this SPI bus. Keep the card deselected
|
||||
// before RadioLib starts; a floating SD CS can make the card drive MISO and
|
||||
// make the SX1262 appear absent (all reads return 0xFF).
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
|
||||
// A software reset can leave the external radio regulator enabled while the
|
||||
// ESP32 and SX1262 are in different states. Force a real regulator cycle so
|
||||
// flashing or rebooting does not require removing USB/battery power before
|
||||
// RadioLib probes the chip.
|
||||
pinMode(SX126X_POWER_EN, OUTPUT);
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
radio_powered = true;
|
||||
delay(10); // Allow radio to power up
|
||||
powerCycleRadio();
|
||||
|
||||
// GPIO21/CTRL is initialized by RadioLib with the normal RXEN mode table.
|
||||
// Do not drive it independently during board startup.
|
||||
@@ -22,6 +41,18 @@ void TBeam1WBoard::begin() {
|
||||
digitalWrite(FAN_CTRL_PIN, HIGH);
|
||||
}
|
||||
|
||||
void TBeam1WBoard::powerCycleRadio() {
|
||||
digitalWrite(SX126X_POWER_EN, LOW);
|
||||
radio_powered = false;
|
||||
delay(100);
|
||||
int power_off_level = digitalRead(SX126X_POWER_EN);
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
radio_powered = true;
|
||||
delay(100);
|
||||
MESH_DEBUG_PRINTLN("T-Beam 1W radio LDO cycle: off=%d on=%d",
|
||||
power_off_level, digitalRead(SX126X_POWER_EN));
|
||||
}
|
||||
|
||||
void TBeam1WBoard::onBeforeTransmit() {
|
||||
// RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins()
|
||||
digitalWrite(LED_PIN, HIGH); // TX LED on
|
||||
@@ -66,16 +97,26 @@ void TBeam1WBoard::powerOff() {
|
||||
|
||||
void TBeam1WBoard::attachRadioDriver(CustomSX1262Wrapper* driver) {
|
||||
radio_driver = driver;
|
||||
// radio.std_init() installs the normal RXEN table, whose RX state is on.
|
||||
// A setting saved while the radio was unavailable still needs to be pushed
|
||||
// into that fresh table when the radio later recovers.
|
||||
lna_driver_synced = lna_enabled;
|
||||
}
|
||||
|
||||
bool TBeam1WBoard::setLoRaFemLnaEnabled(bool enable) {
|
||||
// The normal/default ON state is already installed by radio.std_init().
|
||||
// Avoid touching GPIO21 or reconfiguring the radio during boot unless the
|
||||
// saved setting actually requests a different state.
|
||||
if (lna_enabled == enable) {
|
||||
return radio_driver != nullptr;
|
||||
if (radio_driver == nullptr) {
|
||||
// Preserve the requested setting while booting in radio-unavailable mode.
|
||||
// It will be applied after a later successful probe.
|
||||
lna_enabled = enable;
|
||||
lna_driver_synced = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The normal/default ON state is already installed by radio.std_init().
|
||||
// Avoid touching GPIO21 or reconfiguring the radio unless the desired table
|
||||
// differs from the one currently installed in the radio driver.
|
||||
if (lna_driver_synced && lna_enabled == enable) return true;
|
||||
|
||||
if (radio_driver == nullptr || !radio_driver->setExternalRxLnaEnabled(enable)) {
|
||||
return false;
|
||||
}
|
||||
@@ -83,11 +124,12 @@ bool TBeam1WBoard::setLoRaFemLnaEnabled(bool enable) {
|
||||
// This is the requested RX-mode state. TX and standby always force CTRL low
|
||||
// regardless of this preference, protecting both the LNA and PA paths.
|
||||
lna_enabled = enable;
|
||||
lna_driver_synced = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TBeam1WBoard::canControlLoRaFemLna() const {
|
||||
return radio_driver != nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TBeam1WBoard::isLoRaFemLnaEnabled() const {
|
||||
|
||||
@@ -33,6 +33,7 @@ class TBeam1WBoard : public ESP32Board {
|
||||
private:
|
||||
bool radio_powered = false;
|
||||
bool lna_enabled = true;
|
||||
bool lna_driver_synced = false;
|
||||
CustomSX1262Wrapper* radio_driver = nullptr;
|
||||
|
||||
public:
|
||||
@@ -42,6 +43,7 @@ public:
|
||||
uint16_t getBattMilliVolts() override;
|
||||
const char* getManufacturerName() const override;
|
||||
void powerOff() override;
|
||||
void powerCycleRadio();
|
||||
|
||||
void attachRadioDriver(CustomSX1262Wrapper* driver);
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include <esp_random.h>
|
||||
#include <helpers/ESP32TrueRandom.h>
|
||||
#include "target.h"
|
||||
|
||||
TBeam1WBoard board;
|
||||
@@ -9,6 +11,8 @@ TBeam1WBoard board;
|
||||
#endif
|
||||
|
||||
static SPIClass spi;
|
||||
static bool target_peripherals_initialized = false;
|
||||
static bool target_radio_available = false;
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
|
||||
|
||||
@@ -26,25 +30,48 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
#endif
|
||||
|
||||
bool radio_init() {
|
||||
fallback_clock.begin();
|
||||
rtc_clock.begin(Wire);
|
||||
if (!target_peripherals_initialized) {
|
||||
fallback_clock.begin();
|
||||
rtc_clock.begin(Wire);
|
||||
|
||||
// Initialize SPI for radio
|
||||
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
|
||||
// Initialize the shared SPI bus once. Repeated radio recovery probes only
|
||||
// reset and initialize the SX1262 itself.
|
||||
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
|
||||
target_peripherals_initialized = true;
|
||||
}
|
||||
|
||||
// GPS serial initialized by EnvironmentSensorManager::begin()
|
||||
|
||||
bool success = radio.std_init(&spi);
|
||||
target_radio_available = success;
|
||||
if (success) {
|
||||
board.attachRadioDriver(&radio_driver);
|
||||
// T-Beam 1W has external PA requiring longer ramp time (>800us recommended)
|
||||
// RADIOLIB_SX126X_PA_RAMP_800U = 0x05
|
||||
radio.setTxParams(LORA_TX_POWER, RADIOLIB_SX126X_PA_RAMP_800U);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"T-Beam 1W radio pins: LDO=%d NSS=%d RST=%d BUSY=%d MISO=%d DIO1=%d CTRL=%d",
|
||||
digitalRead(SX126X_POWER_EN), digitalRead(P_LORA_NSS),
|
||||
digitalRead(P_LORA_RESET), digitalRead(P_LORA_BUSY),
|
||||
digitalRead(P_LORA_MISO), digitalRead(P_LORA_DIO_1),
|
||||
digitalRead(SX126X_RXEN));
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
if (target_radio_available) {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng);
|
||||
}
|
||||
|
||||
class ESP32IdentityRNG : public mesh::RNG {
|
||||
public:
|
||||
void random(uint8_t* dest, size_t sz) override {
|
||||
esp_fill_random(dest, sz);
|
||||
mesh::mixESP32TrueRandom(dest, sz);
|
||||
}
|
||||
} rng;
|
||||
return mesh::LocalIdentity(&rng);
|
||||
}
|
||||
Reference in new issue
Block a user