mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
fix(companion): CLI NaN, APC on TX changes, tz-aware alarm, late ACKs, ping timeout and widths, TX limits
- set af / dutycycle / rxdelay reject nan. - A TX power change from the app or CLI re-baselines APC. - CMD_SET_RADIO_PARAMS stores repeat as 0/1. - The alarm reschedules itself when the time zone changes. - A late ACK for an earlier DM attempt marks it delivered. - A new auto-advert interval counts from the last advert. - Both UIs take the TX power range from radioctl, up to MAX_LORA_TX_POWER (30 dBm on the GAT562 30S). - The ping timeout comes from the airtime; 3-byte path hash mode pings with 4-byte trace hashes, and trace flags are right for width 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1940,7 +1940,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
_prefs.cr = cr;
|
||||
_prefs.freq = (float)freq / 1000.0;
|
||||
_prefs.bw = (float)bw / 1000.0;
|
||||
_prefs.client_repeat = repeat;
|
||||
_prefs.client_repeat = repeat ? 1 : 0; // a bool for every reader, whatever byte the app sent
|
||||
savePrefs();
|
||||
|
||||
applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set
|
||||
@@ -1971,6 +1971,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
// Store what the radio actually applied, not the raw request -- on a
|
||||
// board with an external-PA gain curve those can differ (see setTxPower()).
|
||||
_prefs.tx_power_dbm = radio_driver.getTxPower();
|
||||
if (_prefs.tx_apc) applyApc(); // the new ceiling: re-baseline APC, as the device's own setting does
|
||||
savePrefs();
|
||||
writeOKFrame();
|
||||
}
|
||||
@@ -2882,10 +2883,18 @@ void MyMesh::loop() {
|
||||
_last_rtc_save_ms = millis();
|
||||
}
|
||||
|
||||
if (_prefs.advert_auto_interval_sec != _auto_advert_sec) {
|
||||
// Interval changed (Settings, CLI, app): count the new one from the last
|
||||
// advert instead of waiting out what was left of the old one.
|
||||
bool first = _auto_advert_sec == UINT32_MAX; // boot: keep the advert due straight away
|
||||
_auto_advert_sec = _prefs.advert_auto_interval_sec;
|
||||
if (!first) _next_auto_advert_ms = _last_auto_advert_ms + _auto_advert_sec * 1000UL;
|
||||
}
|
||||
if (_prefs.advert_auto_interval_sec > 0 && millisHasNowPassed(_next_auto_advert_ms)) {
|
||||
// The position only where Privacy lets adverts carry it, at its precision.
|
||||
mesh::Packet* pkt = createOwnAdvert();
|
||||
if (pkt) sendZeroHop(pkt);
|
||||
_last_auto_advert_ms = _ms->getMillis();
|
||||
_next_auto_advert_ms = futureMillis(_prefs.advert_auto_interval_sec * 1000UL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ public:
|
||||
unsigned long sent_ms;
|
||||
};
|
||||
|
||||
uint32_t sendPing(const uint8_t* dest_pubkey, uint8_t hash_width = 1);
|
||||
uint32_t sendPing(const uint8_t* dest_pubkey, uint8_t hash_width = 1, uint32_t* est_timeout = nullptr);
|
||||
void setPingCallback(PingCallback cb, void* arg);
|
||||
void clearPingResult(uint32_t tag);
|
||||
PingResult* getPingResult(uint32_t tag);
|
||||
@@ -684,6 +684,8 @@ private:
|
||||
unsigned long _bot_last_ch_reply_ms;
|
||||
unsigned long _bot_last_room_reply_ms;
|
||||
unsigned long _next_auto_advert_ms;
|
||||
unsigned long _last_auto_advert_ms = 0;
|
||||
uint32_t _auto_advert_sec = UINT32_MAX; // the interval _next_auto_advert_ms was set with (MAX: not yet)
|
||||
|
||||
// Per-contact DM reply throttle: a small ring of the most recent recipients so
|
||||
// one chatty contact can't be spammed while a different sender is still served.
|
||||
|
||||
@@ -120,7 +120,7 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) {
|
||||
if (memcmp(command, "set af ", 7) == 0) {
|
||||
char* end;
|
||||
float af = strtof(&command[7], &end);
|
||||
if (end == &command[7] || af < 0 || af > 9) snprintf(reply, n, "ERROR: af must be 0-9");
|
||||
if (end == &command[7] || !(af >= 0 && af <= 9)) snprintf(reply, n, "ERROR: af must be 0-9");
|
||||
else { _prefs.airtime_factor = af; savePrefs(); snprintf(reply, n, "OK"); }
|
||||
return true;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) {
|
||||
}
|
||||
if (memcmp(command, "set dutycycle ", 14) == 0) {
|
||||
float dc = atof(&command[14]);
|
||||
if (dc < 1 || dc > 100) { snprintf(reply, n, "ERROR: dutycycle must be 1-100"); return true; }
|
||||
if (!(dc >= 1 && dc <= 100)) // written so "nan" fails too { snprintf(reply, n, "ERROR: dutycycle must be 1-100"); return true; }
|
||||
_prefs.airtime_factor = 100.0f / dc - 1.0f;
|
||||
savePrefs();
|
||||
float a = 100.0f / (_prefs.airtime_factor + 1.0f);
|
||||
@@ -172,6 +172,7 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) {
|
||||
if (p < -9 || p > MAX_LORA_TX_POWER) { snprintf(reply, n, "Error, must be -9 to %d", MAX_LORA_TX_POWER); return true; }
|
||||
radio_driver.setTxPower(p);
|
||||
_prefs.tx_power_dbm = radio_driver.getTxPower(); // what the radio applied (see CMD_SET_RADIO_TX_POWER)
|
||||
if (_prefs.tx_apc) applyApc(); // the new APC ceiling
|
||||
savePrefs();
|
||||
snprintf(reply, n, "OK");
|
||||
return true;
|
||||
@@ -179,7 +180,7 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) {
|
||||
if (strcmp(command, "get rxdelay") == 0) { snprintf(reply, n, "> %s", StrHelper::ftoa(_prefs.rx_delay_base)); return true; }
|
||||
if (memcmp(command, "set rxdelay ", 12) == 0) {
|
||||
float d = atof(&command[12]);
|
||||
if (d < 0 || d > 20.0f) snprintf(reply, n, "Error, must be 0-20");
|
||||
if (!(d >= 0 && d <= 20.0f)) snprintf(reply, n, "Error, must be 0-20");
|
||||
else { _prefs.rx_delay_base = d; savePrefs(); snprintf(reply, n, "OK"); }
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ int MyMesh::getDiscoverResults(DiscoverResult dest[], int max_count) {
|
||||
|
||||
// ── Ping/Trace functionality ─────────────────────────────────────────────────
|
||||
|
||||
uint32_t MyMesh::sendPing(const uint8_t* dest_pubkey, uint8_t hash_width) {
|
||||
if (hash_width == 0 || hash_width > 2) return 0;
|
||||
uint32_t MyMesh::sendPing(const uint8_t* dest_pubkey, uint8_t hash_width, uint32_t* est_timeout) {
|
||||
// A trace's hashes are 1 << flags bytes wide: 1, 2, 4 (no 3).
|
||||
if (hash_width != 1 && hash_width != 2 && hash_width != 4) return 0;
|
||||
|
||||
// Generate random tag and auth code
|
||||
uint32_t tag, auth;
|
||||
@@ -61,9 +62,13 @@ uint32_t MyMesh::sendPing(const uint8_t* dest_pubkey, uint8_t hash_width) {
|
||||
memcpy(path, dest_pubkey, path_len);
|
||||
|
||||
// Create and send trace packet
|
||||
auto pkt = createTrace(tag, auth, hash_width - 1); // flags = hash_width - 1
|
||||
auto pkt = createTrace(tag, auth, hash_width == 4 ? 2 : hash_width - 1); // flags = log2(hash_width)
|
||||
if (pkt) {
|
||||
sendDirect(pkt, path, path_len);
|
||||
if (est_timeout) { // as CMD_SEND_TRACE_PATH answers the app: from the airtime, one hop
|
||||
uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
|
||||
*est_timeout = calcDirectTimeoutMillisFor(t, 1);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,9 @@ private:
|
||||
_alarm_check_ms = now_ms;
|
||||
uint32_t now_wall = rtc_clock.getCurrentTime();
|
||||
if (now_wall < 1000000000UL) return; // need a real time sync first
|
||||
// A time zone change (Settings, CLI tz.offset, the app) moves the local
|
||||
// alarm time: reschedule here rather than trusting every caller to say so.
|
||||
if (_alarm_tz != _prefs->tz_offset_hours) { _alarm_tz = _prefs->tz_offset_hours; _alarm_next_fire = 0; }
|
||||
if (_alarm_next_fire == 0) _alarm_next_fire = computeAlarmNextFire(now_wall);
|
||||
if (now_wall < _alarm_next_fire) return;
|
||||
if (now_wall - _alarm_next_fire < ALARM_CATCHUP_SECS) {
|
||||
@@ -124,6 +127,7 @@ private:
|
||||
NodePrefs* _prefs = nullptr;
|
||||
UiEventQueue* _events = nullptr;
|
||||
uint32_t _alarm_next_fire = 0; // unix; 0 = (re)compute lazily once time is valid
|
||||
int8_t _alarm_tz = 0; // the tz_offset_hours _alarm_next_fire was computed with
|
||||
uint32_t _alarm_check_ms = 0; // throttle the wall-clock read to ~2 Hz
|
||||
bool _timer_running = false;
|
||||
uint32_t _timer_deadline_ms = 0;
|
||||
|
||||
@@ -459,6 +459,28 @@ public:
|
||||
e.ack_status = ACK_OK;
|
||||
#ifdef HIST_ARCHIVE
|
||||
if (_arc) _arc->dmUpdate(e);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
// A late ACK for an earlier attempt still proves delivery. Only the latest
|
||||
// attempt's tag is kept, so rebuild the earlier ones (BaseChatMesh::
|
||||
// composeMsgPacket(): timestamp, attempt & 3, text, our public key).
|
||||
for (int i = 0; i < _dm_hist_count; i++) {
|
||||
DmHistEntry& e = _dm_hist[(_dm_hist_head + i) % DM_HIST_MAX];
|
||||
if (!e.outgoing || e.ack_status != ACK_PENDING || e.attempt == 0) continue;
|
||||
uint8_t buf[5 + MSG_TEXT_BUF];
|
||||
int tl = strnlen(e.text, MSG_TEXT_BUF - 1);
|
||||
memcpy(buf, &e.msg_ts, 4);
|
||||
memcpy(&buf[5], e.text, tl);
|
||||
for (uint8_t a = 0; a < e.attempt && a < 4; a++) { // the hash only sees attempt & 3
|
||||
buf[4] = (uint8_t)(a & 3);
|
||||
uint32_t crc;
|
||||
mesh::Utils::sha256((uint8_t*)&crc, 4, buf, 5 + tl, the_mesh.self_id.pub_key, PUB_KEY_SIZE);
|
||||
if (crc != ack_crc) continue;
|
||||
e.ack_status = ACK_OK;
|
||||
#ifdef HIST_ARCHIVE
|
||||
if (_arc) _arc->dmUpdate(e);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,18 +4,17 @@
|
||||
// view can keep showing it. The reply arrives through MyMesh's ping callback,
|
||||
// which is a plain function pointer -- routed here via a single static instance.
|
||||
//
|
||||
// The ping timeout is still decided by the view (NearbyScreen's
|
||||
// PING_TIMEOUT_MS), which calls clear() when it gives up.
|
||||
// The view gives up after timeoutMs() (from the airtime, so it scales with
|
||||
// the spreading factor) and calls clear().
|
||||
|
||||
class PingEngine {
|
||||
public:
|
||||
enum StartResult : uint8_t { STARTED = 0, BUSY, UNSUPPORTED, SEND_FAILED };
|
||||
enum StartResult : uint8_t { STARTED = 0, BUSY, SEND_FAILED };
|
||||
|
||||
void begin(NodePrefs* prefs) { _prefs = prefs; s_instance = this; }
|
||||
|
||||
StartResult start(const uint8_t* pub_key) {
|
||||
if (_active || !pub_key) return BUSY;
|
||||
if (_prefs && _prefs->path_hash_mode > 1) return UNSUPPORTED; // no 3-byte path hash support
|
||||
|
||||
_active = true;
|
||||
_tag = 0;
|
||||
@@ -25,7 +24,11 @@ public:
|
||||
|
||||
// Always install the callback before sending so the response cannot race it.
|
||||
the_mesh.setPingCallback(onPingResult, NULL);
|
||||
_tag = the_mesh.sendPing(pub_key, _prefs ? _prefs->path_hash_mode + 1 : 1);
|
||||
// The trace's hash width follows the path hash mode; a trace has no 3-byte
|
||||
// width, so mode 2 pings with 4 bytes of the key.
|
||||
uint8_t mode = _prefs ? _prefs->path_hash_mode : 0;
|
||||
_timeout_ms = 3000;
|
||||
_tag = the_mesh.sendPing(pub_key, mode >= 2 ? 4 : mode + 1, &_timeout_ms);
|
||||
if (_tag == 0) {
|
||||
clear();
|
||||
return SEND_FAILED;
|
||||
@@ -41,6 +44,7 @@ public:
|
||||
}
|
||||
|
||||
bool isActive() const { return _active; }
|
||||
uint32_t timeoutMs() const { return _timeout_ms; }
|
||||
void getResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const {
|
||||
snr_out_x4 = _snr_out_x4;
|
||||
snr_back_x4 = _snr_back_x4;
|
||||
@@ -70,6 +74,7 @@ private:
|
||||
int16_t _snr_out_x4 = 0;
|
||||
int16_t _snr_back_x4 = 0;
|
||||
uint32_t _rtt_ms = 0;
|
||||
uint32_t _timeout_ms = 3000;
|
||||
};
|
||||
|
||||
// Header-only Core, single TU (see UiCore.h) -- the one definition lives here.
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
|
||||
namespace radioctl {
|
||||
|
||||
// The TX power range the device's own settings offer (the app and CLI take
|
||||
// -9 up). The top is the board's limit: 30 dBm on the GAT562 30S, not 22.
|
||||
static const int TX_MIN_DBM = 2;
|
||||
static const int TX_MAX_DBM = MAX_LORA_TX_POWER;
|
||||
|
||||
// freq / bw / sf / cr -> the radio (or the repeater profile, while relaying with one).
|
||||
static void applyParams() { the_mesh.applyRepeaterRadio(); }
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ void UITask::refreshNode() {
|
||||
setTextFmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt,
|
||||
out / 4.0f, back / 4.0f);
|
||||
_pinging = false;
|
||||
} else if (millis() - _ping_started_ms > 3000) {
|
||||
} else if (millis() - _ping_started_ms > _core->ping.timeoutMs()) {
|
||||
_core->ping.clear();
|
||||
setText(_node_ping, "Ping: no reply");
|
||||
_pinging = false;
|
||||
@@ -472,8 +472,7 @@ void UITask::nodeAction(uint8_t action) {
|
||||
_next_nearby_ms = millis() + 250;
|
||||
lv_label_set_text(_node_ping, "Pinging...");
|
||||
} else {
|
||||
lv_label_set_text(_node_ping, r == PingEngine::UNSUPPORTED ? "Ping needs 1-2 byte path hashes"
|
||||
: "Ping failed");
|
||||
lv_label_set_text(_node_ping, "Ping failed");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,8 @@
|
||||
namespace radioview {
|
||||
|
||||
enum : uint8_t { R_PRESET, R_SF, R_BW, R_CR, R_TX, R_APC };
|
||||
static const int TX_MIN = 2;
|
||||
#ifdef LORA_TX_POWER
|
||||
static const int TX_MAX = LORA_TX_POWER;
|
||||
#else
|
||||
static const int TX_MAX = 22;
|
||||
#endif
|
||||
static const int TX_MIN = radioctl::TX_MIN_DBM;
|
||||
static const int TX_MAX = radioctl::TX_MAX_DBM;
|
||||
|
||||
static lv_obj_t* s_overlay = nullptr; // frequency entry
|
||||
static lv_obj_t* s_ta = nullptr;
|
||||
|
||||
@@ -66,7 +66,6 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
char _ping_snr_back_str[24];
|
||||
bool _pinging;
|
||||
unsigned long _ping_started_ms;
|
||||
static const unsigned long PING_TIMEOUT_MS = 3000UL;
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
static void pubKeyToBase64(const uint8_t* key, char* out, int out_len) {
|
||||
@@ -266,7 +265,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
if (snr_back != 0)
|
||||
snprintf(_ping_snr_back_str, sizeof(_ping_snr_back_str), "SNR back: %.1f", snr_back / 4.0f);
|
||||
_pinging = false;
|
||||
} else if (_pinging && millis() - _ping_started_ms >= PING_TIMEOUT_MS) {
|
||||
} else if (_pinging && millis() - _ping_started_ms >= _task->pingTimeoutMs()) {
|
||||
snprintf(_ping_time_str, sizeof(_ping_time_str), "RTT: timeout");
|
||||
_ping_snr_out_str[0] = '\0';
|
||||
_ping_snr_back_str[0] = '\0';
|
||||
|
||||
@@ -1036,8 +1036,8 @@ public:
|
||||
return enter;
|
||||
}
|
||||
if (_selected == TX_POWER && p) {
|
||||
if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; }
|
||||
if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; }
|
||||
if (right && p->tx_power_dbm < radioctl::TX_MAX_DBM) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; }
|
||||
if (left && p->tx_power_dbm > radioctl::TX_MIN_DBM) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == RADIO_PRESET && p && enter) {
|
||||
_picker.open(p, radioTarget(p), "Radio Preset");
|
||||
|
||||
@@ -356,10 +356,9 @@ void UITask::gotoAutoAdvertScreen() { setCurrScreen(auto_advert_screen); }
|
||||
void UITask::gotoBatteryCurve() { setCurrScreen(batt_curve_screen); }
|
||||
|
||||
bool UITask::startPing(const uint8_t* pub_key) {
|
||||
PingEngine::StartResult r = _core->ping.start(pub_key);
|
||||
if (r == PingEngine::UNSUPPORTED) showAlert("Ping not supported with 3-byte path hashes", 3000);
|
||||
return r == PingEngine::STARTED;
|
||||
return _core->ping.start(pub_key) == PingEngine::STARTED;
|
||||
}
|
||||
uint32_t UITask::pingTimeoutMs() const { return _core->ping.timeoutMs(); }
|
||||
bool UITask::isPingActive() const { return _core->ping.isActive(); }
|
||||
void UITask::getPingResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const {
|
||||
_core->ping.getResult(snr_out_x4, snr_back_x4, rtt_ms);
|
||||
|
||||
@@ -473,6 +473,7 @@ public:
|
||||
// Ping helpers (engine in ui-core/PingEngine.h)
|
||||
bool startPing(const uint8_t* pub_key);
|
||||
bool isPingActive() const;
|
||||
uint32_t pingTimeoutMs() const;
|
||||
void getPingResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const;
|
||||
void clearPing();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user