core: MeshCore 1.18 (core-v1.18.0 = meshcomod 8aba438f), V4 language table deflated

Pins move from core-v1.17.4 to core-v1.18.0, cut from the main meshcomod
companion-v1.18.0.1 was released from. The companion changes are ported from
meshcomod's rebase into wadamesh's own MyMesh:

- onCLICommandRecv: a contact's remote CLI command runs when the user granted
  that contact remote CLI, and lands in the chat as a CLI message otherwise.
- CMD_RUN_CLI_COMMAND (66) / RESP_CODE_CLI_REPLY (29) and the local companion CLI
  (name, pin, Wi-Fi, radio, af/dutycycle, rxgain, tx, rxdelay, path.hash.mode,
  multi.acks, board, ver), also reachable from the serial CLI. Settings without a
  prefs field answer "unsupported". NodePrefs layout unchanged.
- TXT_TYPE_CLI_COMMAND is accepted for sending; sendCommandData (and our queue-
  preserving wrapper) carry the text type.
- Channel data is delivered to the app (onChannelDataRecv), unsolicited responses
  (pushed telemetry) reach it as PUSH_CODE_BINARY_RESPONSE, and raw-data sends
  decode multi-byte-hash paths.

Clock: 1.18 seeds an unsynced ESP32 clock at 1 Mar 2026, so ClockFloorRTC's
MIN_VALID_EPOCH follows it (a set equal to the seed must stay invalid).
Pager LR1121: the AGC reset re-applies the boosted-gain setting (new core arg).
IDF builds exclude the new CommonRadioPrefs.cpp (needs <target.h>/radio_driver).

The 2 MB Heltec V4 went to 100.0% with the CLI. Its built-in language table is now
one raw-deflated blob per language (895 KB -> 312 KB), inflated into PSRAM by the
S3 ROM on first use (~10 ms, ~75 KB for the active language): 91.7% flash. Checked
on a T-Deck forced onto the table (nl, hu, el) and by the post-link key check.

Builds: all 15 S3 touch envs, Tanmatsu (3,237,024 B), T-Display P4 SX1262 + LR2021.
T-Deck boots and runs on 1.18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaj SchittecatandClaude Opus 5.5 committed 2026-10-15 17:50:32 +02:00
1 parent d3a9cf1453
commit ef0b27f1e0
11 files changed
+13628 -14022

No files matched your search

+12 -12
View File
@@ -197,7 +197,7 @@ lib_deps =
; MeshCore core = the meshcomod monorepo built as a lib, pinned by tag.
; Touch-app files (TouchPrefsStore, WifiRuntimeStore, transports, ...) are
; vendored in this app's src/ and dropped from the lib via -DMC_VENDORED_TOUCH_APP.
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -494,7 +494,7 @@ build_src_filter =
+<../variants/attaky_mesh_series/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -680,7 +680,7 @@ build_src_filter =
+<../variants/lilygo_tdeck/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -858,7 +858,7 @@ build_src_filter =
+<../variants/lilygo_tdeck_pro/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
SD
@@ -1136,7 +1136,7 @@ build_src_filter =
+<../variants/lilygo_tlora_pager/Es8311Codec.cpp> ; shared ES8311 DAC driver (Max speaker path)
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
SD
@@ -1306,7 +1306,7 @@ build_src_filter =
+<../variants/wio_tracker_l2/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
SD_MMC
@@ -1461,7 +1461,7 @@ build_src_filter =
+<../variants/crowpanel_35/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
SD
@@ -1617,7 +1617,7 @@ build_src_filter =
+<../variants/crowpanel_50/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
SD
@@ -1830,7 +1830,7 @@ build_src_filter =
+<../variants/lilygo_tlora_pager/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -2058,7 +2058,7 @@ build_src_filter =
+<../variants/lilygo_tlora_pager/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -2328,7 +2328,7 @@ build_src_filter =
+<../variants/thinknode_m9/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -2525,7 +2525,7 @@ build_src_filter =
+<../variants/rak_tap_v2/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0
SPI
Wire
jgromes/RadioLib @ ^7.6.0
+33 -25
View File
@@ -15,7 +15,7 @@ them, and the table lives in .rodata — flash, not RAM.
Run from the repo root (a PlatformIO pre-build step does this automatically):
python3 scripts/build/gen-lang-builtin.py
"""
import glob, os, sys
import zlib, glob, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import lang_reach # noqa: E402
@@ -113,38 +113,45 @@ def main():
print('i18n: cannot work out the reachable keys (%s); i18n_builtin.h left as it is' % e)
return
# Each language is ONE raw-deflated blob of its sorted "key\0value\0" rows. The
# V4 inflates the active language into PSRAM on first use (i18n.cpp) and binary-
# searches it there. The plain table was ~694 KB of the V4's flash (2026-10-15,
# the 1.18 core left 1.5 KB free); deflated it is roughly a third of that.
w = ['// Generated by scripts/build/gen-lang-builtin.py — DO NOT EDIT.',
'// Source: deploy/apps/lang/*.lang (the same files the Lua Store serves).',
'// Compiled-in fallback for boards that cannot rely on downloading a language.',
'#pragma once', '#include "i18n.h"', '']
'// Compiled-in fallback for boards that cannot rely on downloading a language:',
'// one raw-deflated blob of sorted "key\\0value\\0" rows per language (i18n.cpp).',
'#pragma once', '#include <stdint.h>', '#include "i18n.h"', '',
'struct I18nBlob { const uint8_t* z; uint32_t zlen; uint32_t len; uint16_t count; };', '']
blobs = {}
for code in CODES[1:]:
rows = langs.get(code, {})
sym = 'kBuiltin_' + code.replace('-', '_')
w.append('static const I18nPair %s[] = {' % sym)
# A row whose translation IS the key does nothing: TR() already returns the
# key when it finds no match. The .lang files carry those deliberately, as
# placeholders telling translators the string exists, but baking them costs
# flash on exactly the board that needs the flash (the V4 sits at ~90%).
for k in sorted(rows): # sorted: TR() binary-searches
if rows[k] == k or k not in keep: # placeholder, or no source can ask for it
continue
w.append(' { "%s", "%s" },' % (c_escape(k), c_escape(rows[k])))
# flash on exactly the board that needs the flash.
keys = [k for k in sorted(rows, key=lambda x: x.encode('utf-8')) # TR() strcmp-searches
if rows[k] != k and k in keep]
if not keys:
continue
raw = b''.join(k.encode('utf-8') + b'\0' + rows[k].encode('utf-8') + b'\0' for k in keys)
c = zlib.compressobj(9, zlib.DEFLATED, -15)
z = c.compress(raw) + c.flush()
sym = 'kI18nZ_' + code.replace('-', '_')
blobs[code] = (sym, len(z), len(raw), len(keys))
w.append('static const uint8_t %s[%d] = { // %d rows, %d B' % (sym, len(z), len(keys), len(raw)))
for i in range(0, len(z), 24):
w.append(' ' + ','.join('0x%02x' % x for x in z[i:i + 24]) + ',')
w.append('};')
w.append('')
w.append('static const I18nPair* const kBuiltinLang[LANG_COUNT] = {')
w.append(' nullptr,') # LANG_EN: the key is the English
w.append('static const I18nBlob kBuiltinBlob[LANG_COUNT] = {')
w.append(' { nullptr, 0, 0, 0 },') # LANG_EN: the key is the English
for code in CODES[1:]:
sym = 'kBuiltin_' + code.replace('-', '_')
w.append(' %s,' % (sym if langs.get(code) else 'nullptr'))
w.append('};')
w.append('static const int kBuiltinLangCount[LANG_COUNT] = {')
w.append(' 0,')
for code in CODES[1:]:
sym = 'kBuiltin_' + code.replace('-', '_')
w.append(' %s,' % (('(int)(sizeof(%s)/sizeof(%s[0]))' % (sym, sym))
if langs.get(code) else '0'))
if code in blobs:
sym, zl, rl, n = blobs[code]
w.append(' { %s, %d, %d, %d },' % (sym, zl, rl, n))
else:
w.append(' { nullptr, 0, 0, 0 },')
w.append('};')
w.append('')
@@ -155,9 +162,10 @@ def main():
else:
os.utime(OUT) # unchanged: mark it current so the pre-build check stops asking
total = sum(sum(1 for k, v in r.items() if v != k and k in keep) for r in langs.values())
print('%s: %d languages, %d translated rows (%d of %d keys reachable), %d bytes%s' %
print('%s: %d languages, %d translated rows (%d of %d keys reachable), %d B deflated from %d B%s' %
(os.path.relpath(OUT, ROOT), len(langs), total, len(keep), len(allkeys),
os.path.getsize(OUT), '' if text != old else ', unchanged'))
sum(b[1] for b in blobs.values()), sum(b[2] for b in blobs.values()),
'' if text != old else ', unchanged'))
if __name__ == '__main__':
+430 -18
View File
@@ -115,6 +115,7 @@ static void persistProtocolClockBeforeReset(mesh::RTCClock* clock) {
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
#define CMD_SEND_RAW_PACKET 65
#define CMD_RUN_CLI_COMMAND 66 // v14+ (MeshCore 1.18)
// Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0
@@ -150,6 +151,7 @@ static void persistProtocolClockBeforeReset(mesh::RTCClock* clock) {
#define RESP_ALLOWED_REPEAT_FREQ 26
#define RESP_CODE_CHANNEL_DATA_RECV 27
#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND
#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)
#define RESP_CODE_SYNC_SINCE_DONE 61 // sent once after SyncSince delta stream; client sets last-sync to now
@@ -1970,7 +1972,7 @@ int MyMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_
}
int MyMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp,
uint8_t attempt, const char* text, uint32_t& est_timeout,
uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout,
uint32_t* out_packet_hash4, TxtTxDebugInfo* out_dbg) {
if (!text) {
est_timeout = 0;
@@ -1985,7 +1987,7 @@ int MyMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp,
_last_plain_tx_meta_valid = false;
uint32_t packet_hash4 = 0;
int result = BaseChatMesh::sendCommandData(recipient, timestamp, attempt, text,
int result = BaseChatMesh::sendCommandData(recipient, timestamp, attempt, txt_type, text,
est_timeout, &packet_hash4, out_dbg);
uint8_t retry_key[MAX_HASH_SIZE] = {};
@@ -3394,6 +3396,21 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text);
}
// MeshCore 1.18: a contact's remote CLI command (TXT_TYPE_CLI_COMMAND). Contacts the
// user granted remote CLI run it here and get the reply; everyone else's lands in the
// chat as a CLI message, as upstream does.
void MyMesh::onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text, char* reply) {
markConnectionActive(from); // in case this is from a server, and we have a connection
if (from.isRemoteCLIAllowed()) {
if (!handleCommand(text, sender_timestamp, reply)) {
strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text'
}
} else {
queueMessage(from, TXT_TYPE_CLI_COMMAND, pkt, sender_timestamp, NULL, 0, text);
}
}
void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) {
markConnectionActive(from);
@@ -3506,6 +3523,49 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
#endif
}
void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
const uint8_t *data, size_t data_len) {
if (data_len > MAX_CHANNEL_DATA_LENGTH) {
MESH_DEBUG_PRINTLN("onChannelDataRecv: dropping payload_len=%d exceeds frame limit=%d",
(uint32_t)data_len, (uint32_t)MAX_CHANNEL_DATA_LENGTH);
return;
}
int i = 0;
out_frame[i++] = RESP_CODE_CHANNEL_DATA_RECV;
out_frame[i++] = (int8_t)(pkt->getSNR() * 4);
out_frame[i++] = 0; // reserved1
out_frame[i++] = 0; // reserved2
uint8_t channel_idx = findChannelIdx(channel);
out_frame[i++] = channel_idx;
out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF;
out_frame[i++] = (uint8_t)(data_type & 0xFF);
out_frame[i++] = (uint8_t)(data_type >> 8);
out_frame[i++] = (uint8_t)data_len;
int copy_len = (int)data_len;
if (copy_len > 0) {
memcpy(&out_frame[i], data, copy_len);
i += copy_len;
}
// Deliver through meshcomod's per-client history ring, as onChannelMessageRecv does.
// Upstream queues this with addToOfflineQueue(), but meshcomod's CMD_SYNC_NEXT_MESSAGE
// reads getNextFromHistoryForClient() and never the offline queue, so channel data
// queued there would never reach any client.
uint32_t hist_seq = addToHistoryRing(out_frame, i);
if (_serial->isConnected()) {
if (_serial->writeFrameToAll(out_frame, i) == (size_t)i && hist_seq != 0 &&
_serial->companionUnsolicitedPushesBroadcastToAll()) {
advanceHistoryClientsAfterV3Broadcast(hist_seq);
}
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_serial->writeFrameToAll(frame, 1);
}
}
uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
uint8_t len, uint8_t *reply) {
if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
@@ -3743,6 +3803,16 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
memcpy(&out_frame[i], &data[4], len - 4);
i += (len - 4);
_serial->writeFrame(out_frame, i);
} else {
// MeshCore 1.18: let the app see unknown/unsolicited responses (eg. pushed telemetry)
int i = 0;
out_frame[i++] = PUSH_CODE_BINARY_RESPONSE;
out_frame[i++] = 0; // reserved
int n = len;
if (n > MAX_FRAME_SIZE - i) n = MAX_FRAME_SIZE - i; // a max-size response would overrun out_frame
memcpy(&out_frame[i], data, n);
i += n;
_serial->writeFrame(out_frame, i);
}
}
@@ -4308,6 +4378,20 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&out_frame[i], _prefs.node_name, tlen);
i += tlen;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 3) { // V14+ (MeshCore 1.18)
int i = 1;
char *text = (char *)&cmd_frame[i];
int tlen = len - i;
text[tlen] = 0; // ensure null
reply_buf[0] = 0;
if (!handleCommand(text, 0, reply_buf)) {
strcat(reply_buf, "Unknown command"); // reply_buf may have cmd prefix from 'text'
}
out_frame[0] = RESP_CODE_CLI_REPLY;
int rlen = strlen(reply_buf);
memcpy(&out_frame[1], reply_buf, rlen);
_serial->writeFrame(out_frame, 1 + rlen);
} else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) {
int i = 1;
uint8_t txt_type = cmd_frame[i++];
@@ -4345,7 +4429,7 @@ void MyMesh::handleCmdFrame(size_t len) {
}
} else {
ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6);
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) {
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) {
char *text = (char *)&cmd_frame[i];
int tlen = len - i;
uint32_t est_timeout;
@@ -4380,10 +4464,11 @@ void MyMesh::handleCmdFrame(size_t len) {
s_last_cmd_txt_seen_ms = now_ms;
}
}
if (txt_type == TXT_TYPE_CLI_DATA) {
// CLI_COMMAND (1.18) targets a companion's remote CLI; CLI_DATA a repeater's admin CLI.
if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) {
msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection
TxtTxDebugInfo dbg{};
result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout, nullptr, &dbg);
result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type, text, est_timeout, nullptr, &dbg);
expected_ack = 0; // no Ack expected
if (_ui) {
char line[160];
@@ -4529,8 +4614,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();
@@ -4959,10 +5044,13 @@ void MyMesh::handleCmdFrame(size_t len) {
#endif
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
int i = 1;
int8_t path_len = cmd_frame[i++];
if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload
uint8_t *path = &cmd_frame[i];
i += path_len;
uint8_t path_len = cmd_frame[i++];
if (mesh::Packet::isValidPathLen(path_len)) {
// MeshCore 1.18: path_len encodes the hash size too (multi-byte path hashes);
// writePath decodes it and says how many bytes the path took. An empty path is
// 0 and consumes nothing, so path_len == 0 still means empty below.
uint8_t path[MAX_PATH_SIZE];
i += mesh::Packet::writePath(path, &cmd_frame[i], path_len);
// Companion OTA panel currently sends `ota url ...` using CMD_SEND_RAW_DATA with empty path.
// Treat zero-path ASCII `ota ...` as a local meshcomod command.
if (path_len == 0 && i < (int)len) {
@@ -4999,12 +5087,16 @@ void MyMesh::handleCmdFrame(size_t len) {
return;
}
}
auto pkt = createRawData(&cmd_frame[i], len - i);
if (pkt) {
sendDirect(pkt, path, path_len);
writeOKFrame();
if (i + 4 > (int)len) { // min payload 4 bytes
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
auto pkt = createRawData(&cmd_frame[i], len - i);
if (pkt) {
sendDirect(pkt, path, path_len);
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
}
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet)
@@ -5558,6 +5650,326 @@ void MyMesh::enterCLIRescue() {
Serial.println("========= CLI Rescue =========");
}
// Companion CLI (1.18): app v1.50's Command Line and Wi-Fi Settings (CMD_RUN_CLI_COMMAND), the
// serial CLI's fall-through, and the remote CLI of contacts granted that permission. Upstream
// drives this off its JSON NodePrefs; meshcomod keeps the binary prefs layout (adding fields
// shifts it), so commands map onto the fields we have, Wi-Fi goes through the NVS runtime store
// shared with the UI, and settings without a field here answer "unsupported".
// sender_timestamp == 0 means a local caller (app over USB/BLE/Wi-Fi, or serial); remote CLI
// requests carry the sender's timestamp.
bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
while (*command == ' ') command++; // skip leading spaces
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
memcpy(reply, command, 3); // reflect the prefix back
reply += 3;
*reply = 0;
command += 3;
}
if (handleRadioCommand(command, reply)) return true;
// hook for variant-specific CLI processing
if (board.handleCommand(command, sender_timestamp, reply)) return true;
if (strcmp(command, "reboot") == 0) {
if (dirty_contacts_expiry) saveContacts(); // as CMD_REBOOT does
board.reboot(); // doesn't return
return true;
}
if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
if (dirty_contacts_expiry) saveContacts();
board.powerOff(); // doesn't return
return true;
}
if (memcmp(command, "set name ", 9) == 0) {
if (AdvertDataParser::isValidName(&command[9])) {
StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name));
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, bad chars");
}
return true;
}
if (strcmp(command, "get name") == 0) {
sprintf(reply, "> %s", _prefs.node_name);
return true;
}
if (memcmp(command, "set pin ", 8) == 0) {
_prefs.ble_pin = atoi(&command[8]);
savePrefs();
sprintf(reply, "> pin is now %06d", _prefs.ble_pin);
return true;
}
#if defined(ESP32) && (defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION))
if (memcmp(command, "set wifi.ssid ", 14) == 0) {
if (wifiConfigSetSsid(&command[14])) {
sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too)", &command[14]);
} else {
strcpy(reply, "Error: invalid/too long SSID");
}
return true;
}
if (memcmp(command, "set wifi.pwd ", 13) == 0) {
if (wifiConfigSetPwd(&command[13])) {
wifiConfigRequestApply(); // reconnect from the main loop with the new credentials
strcpy(reply, "> wifi.pwd updated, reconnecting");
} else {
strcpy(reply, "Error: password too long");
}
return true;
}
if (strcmp(command, "get wifi.pwd") == 0) {
if (sender_timestamp != 0) { // never hand the network password to a remote node
strcpy(reply, "Error: not available remotely");
} else {
char pwd[WIFI_CONFIG_PWD_MAX + 1];
wifiConfigGetPwd(pwd, sizeof(pwd));
sprintf(reply, "> %s", pwd);
}
return true;
}
if (strcmp(command, "set wifi.clear") == 0 || strcmp(command, "wifi.clear") == 0) {
wifiConfigClear();
wifiConfigRequestApply();
strcpy(reply, "> wifi config cleared");
return true;
}
if (strcmp(command, "wifi.apply") == 0 || strcmp(command, "set wifi.apply") == 0) {
wifiConfigRequestApply();
strcpy(reply, "OK");
return true;
}
if (strcmp(command, "get wifi.ssid") == 0) {
char ssid[WIFI_CONFIG_SSID_MAX + 1];
wifiConfigGetSsid(ssid, sizeof(ssid));
sprintf(reply, "> %s", ssid[0] ? ssid : "(not set)");
return true;
}
if (memcmp(command, "set wifi.enabled ", 17) == 0) {
bool on = atoi(&command[17]) != 0;
wifiConfigSetRadioEnabled(on); // the main loop brings the radio up/down live
sprintf(reply, "> wifi.enabled is now %d", on ? 1 : 0);
return true;
}
if (strcmp(command, "get wifi.enabled") == 0) {
sprintf(reply, "> %d", wifiConfigGetRadioEnabled() ? 1 : 0);
return true;
}
if (strcmp(command, "get wifi.status") == 0) {
strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected");
return true;
}
if (strcmp(command, "get wifi.ip") == 0) {
if (WiFi.status() == WL_CONNECTED) {
sprintf(reply, "> %s", WiFi.localIP().toString().c_str());
} else {
strcpy(reply, "> (not connected)");
}
return true;
}
#endif
if (strcmp(command, "board") == 0) {
strcpy(reply, board.getManufacturerName());
return true;
}
if (strcmp(command, "ver") == 0) {
sprintf(reply, "%s (Build: %s)", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE);
return true;
}
if (strcmp(command, "get tz.offset") == 0 || memcmp(command, "set tz.offset ", 14) == 0) {
strcpy(reply, "Error: unsupported");
return true;
}
return false; // not handled
}
// Upstream's radio command set (CommonRadioPrefs::handleCommand), same syntax and replies, on
// meshcomod's NodePrefs. Settings that have no field here answer "unsupported" rather than an
// "OK" that would not survive a reboot.
bool MyMesh::handleRadioCommand(const char* command, char* reply) {
if (strcmp(command, "get radio") == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs.freq));
strcpy(bw, StrHelper::ftoa3(_prefs.bw));
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs.sf, (uint32_t)_prefs.cr);
return true;
}
if (memcmp(command, "set radio ", 10) == 0) {
char tmp[132];
StrHelper::strncpy(tmp, &command[10], sizeof(tmp));
const char *parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
_prefs.freq = freq;
_prefs.bw = bw;
_prefs.sf = sf;
_prefs.cr = cr;
savePrefs();
strcpy(reply, "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid radio params");
}
return true;
}
if (strcmp(command, "get freq") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs.freq));
return true;
}
if (strcmp(command, "get af") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs.airtime_factor));
return true;
}
if (memcmp(command, "set af ", 7) == 0) {
char* end;
float af = strtof(&command[7], &end);
if (end == &command[7] || af < 0 || af > 9) {
strcpy(reply, "ERROR: af must be 0-9");
} else {
_prefs.airtime_factor = af;
savePrefs();
strcpy(reply, "OK");
}
return true;
}
if (strcmp(command, "get dutycycle") == 0) {
float dc = 100.0f / (_prefs.airtime_factor + 1.0f);
int dc_int = (int)dc;
int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
return true;
}
if (memcmp(command, "set dutycycle ", 14) == 0) {
float dc = atof(&command[14]);
if (dc < 1 || dc > 100) {
strcpy(reply, "ERROR: dutycycle must be 1-100");
} else {
_prefs.airtime_factor = (100.0f / dc) - 1.0f;
savePrefs();
float actual = 100.0f / (_prefs.airtime_factor + 1.0f);
int a_int = (int)actual;
int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
}
return true;
}
#if defined(USE_SX1262) || defined(USE_SX1268)
if (strcmp(command, "get radio.rxgain") == 0) {
sprintf(reply, "> %s", _prefs.rx_boosted_gain ? "on" : "off");
return true;
}
if (memcmp(command, "set radio.rxgain ", 17) == 0) {
bool enabled = memcmp(&command[17], "on", 2) == 0;
_prefs.rx_boosted_gain = enabled ? 1 : 0;
savePrefs();
if (radio_driver.setRxBoostedGainMode(enabled)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: unsupported");
}
return true;
}
#endif
if (memcmp(command, "get tx", 6) == 0 && (command[6] == 0 || command[6] == ' ')) {
sprintf(reply, "> %d", (int32_t)_prefs.tx_power_dbm);
return true;
}
if (memcmp(command, "set tx ", 7) == 0) {
int dbm = atoi(&command[7]);
if (dbm < -9 || dbm > MAX_LORA_TX_POWER) {
sprintf(reply, "Error, must be -9 to %d", (int)MAX_LORA_TX_POWER);
} else {
_prefs.tx_power_dbm = dbm;
savePrefs();
radio_driver.setTxPower(_prefs.tx_power_dbm);
strcpy(reply, "OK");
}
return true;
}
if (strcmp(command, "get rxdelay") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs.rx_delay_base));
return true;
}
if (memcmp(command, "set rxdelay ", 12) == 0) {
float db = atof(&command[12]);
if (db >= 0 && db <= 20.0f) {
_prefs.rx_delay_base = db;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-20");
}
return true;
}
if (strcmp(command, "get path.hash.mode") == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs.path_hash_mode);
return true;
}
if (memcmp(command, "set path.hash.mode ", 19) == 0) {
uint8_t mode = atoi(command + 19);
if (mode < 3) {
_prefs.path_hash_mode = mode;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0,1, or 2");
}
return true;
}
if (strcmp(command, "get multi.acks") == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs.multi_acks);
return true;
}
if (memcmp(command, "set multi.acks ", 15) == 0) {
_prefs.multi_acks = atoi(&command[15]);
savePrefs();
strcpy(reply, "OK");
return true;
}
// No field in meshcomod's prefs (CAD and interference threshold are fixed off in the companion).
static const char* const unsupported[] = {
"cad", "int.thresh", "agc.reset.interval", "txdelay", "direct.txdelay"
};
for (const char* name : unsupported) {
size_t n = strlen(name);
if ((memcmp(command, "get ", 4) == 0 || memcmp(command, "set ", 4) == 0)
&& memcmp(&command[4], name, n) == 0 && (command[4 + n] == 0 || command[4 + n] == ' ')) {
strcpy(reply, "Error: unsupported");
return true;
}
}
return false;
}
// Serial CLI fall-through to the companion CLI (handleCommand), so the commands app v1.50
// sends also work over USB serial. Returns false when the command is not one of them.
bool MyMesh::printSharedCliReply() {
reply_buf[0] = 0;
if (!handleCommand(cli_command, 0, reply_buf)) return false;
Serial.print(" "); Serial.println(reply_buf);
return true;
}
void MyMesh::checkCLIRescueCmd() {
int len = strlen(cli_command);
bool line_complete = false;
@@ -5659,7 +6071,7 @@ void MyMesh::checkCLIRescueCmd() {
#else
Serial.println(" Error: WiFi config only supported on ESP32 builds");
#endif
} else {
} else if (!printSharedCliReply()) { // name, radio, tx, ... (the companion CLI)
Serial.printf(" Error: unknown config: %s\n", config);
}
} else if (strcmp(cli_command, "mqtt.status") == 0 ||
@@ -5908,7 +6320,7 @@ void MyMesh::checkCLIRescueCmd() {
} else if (strcmp(cli_command, "reboot") == 0) {
persistProtocolClockBeforeReset(getRTCClock());
board.reboot(); // doesn't return
} else {
} else if (!printSharedCliReply()) { // get name, get radio, ver, board, ... (the companion CLI)
Serial.println(" Error: unknown command");
}
+12 -2
View File
@@ -149,7 +149,7 @@ public:
const char* text, uint32_t& expected_ack, uint32_t& est_timeout,
uint32_t* out_packet_hash4 = nullptr, TxtTxDebugInfo* out_dbg = nullptr);
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt,
const char* text, uint32_t& est_timeout,
uint8_t txt_type, const char* text, uint32_t& est_timeout,
uint32_t* out_packet_hash4 = nullptr, TxtTxDebugInfo* out_dbg = nullptr);
const char *getNodeName();
@@ -308,6 +308,10 @@ protected:
const char *text) override;
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) override;
void onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text, char* reply) override;
void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
const uint8_t *data, size_t data_len) override;
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) override;
// An UNCONFIGURED channel slot is all zeroes — including its secret, its hash
@@ -537,7 +541,7 @@ public:
if (!text || !text[0]) return MSG_SEND_FAILED;
uint32_t est = 0;
uint32_t ts = getRTCClock()->getCurrentTimeUnique();
return sendCommandData(recipient, ts, 0, text, est);
return sendCommandData(recipient, ts, 0, TXT_TYPE_CLI_DATA, text, est);
}
/** Cancel UI ping pending state (used by timeout). */
void cancelUIPingPending() {
@@ -1394,6 +1398,12 @@ private:
// 200 not 80: the serial sideload ("fadd ...", see cliPutChunk) carries up
// to ~130 chars per line; every other command is far shorter.
char cli_command[200];
char reply_buf[166]; // CMD_RUN_CLI_COMMAND replies (MeshCore 1.18)
// The companion CLI (MeshCore 1.18): app v1.50's Command Line, the serial CLI's
// fall-through and the remote CLI of contacts granted that permission.
bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply);
bool handleRadioCommand(const char* command, char* reply);
bool printSharedCliReply();
#if defined(ESP32)
// Serial sideload state ("fput" / "fadd" / "fend"): the file being written.
File _cli_put;
+5 -1
View File
@@ -54,7 +54,11 @@ private:
public:
ClockFloorRTC(mesh::RTCClock& fallback) : AutoDiscoverRTCClock(fallback) {}
static constexpr uint32_t MIN_VALID_EPOCH = 1715770351UL; // 15 May 2024 — the core's own unset-clock seed
// The core's own unset-clock seed (ESP32RTCClock::begin, RTC_TIME_MIN): 1 Mar 2026 from
// MeshCore 1.18, 15 May 2024 (1715770351) before. It has to be the seed itself, not
// anything below it: a set equal to the seed is the "Sync clock from system" poisoning
// described at pushSystemClock(), and a seed above this value would read as a real time.
static constexpr uint32_t MIN_VALID_EPOCH = 1772323200UL;
static constexpr uint32_t TRUSTED_BACK_CAP = 10UL * 60UL;
// High-side twin of MIN_VALID_EPOCH: an external RTC chip with corrupted/unset date
+25
View File
@@ -80320,7 +80320,32 @@ static void docMusicAudit() {
}
#endif
#if defined(DOC_I18N_AUDIT) && CAP_BUILTIN_LANGS
// The deflated built-in language table: switch languages straight from it (no .lang
// file overlay), print a few translations and how long each language took to inflate.
static void docI18nAudit() {
const uint8_t was = i18nGetLang();
i18nSetFileOverlay(nullptr, 0);
const uint8_t langs[] = { LANG_NL, LANG_HU, LANG_EL, LANG_EN };
const char* keys[] = { "Settings", "Channel", "Busy", "Sound is off: Settings > Sound", "Delete chat" };
for (uint8_t l : langs) {
const uint32_t t0 = micros();
i18nSetLang(l);
const uint32_t us = micros() - t0;
Serial.printf("DBG: I18N lang %s inflated in %lu us, heap psram free %u\n", kUiLangCodes[l],
(unsigned long)us, (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
for (const char* k : keys) Serial.printf("DBG: I18N %s -> %s\n", k, TR(k));
}
i18nSetLang(was);
Serial.println("DBG: I18N done (the file overlay comes back on the next boot)");
}
#endif
static void docAuditFocus() {
#if defined(DOC_I18N_AUDIT) && CAP_BUILTIN_LANGS
docI18nAudit();
return;
#endif
#if defined(DOC_MUSIC_AUDIT) && CAP_MICROSD
docMusicAudit();
return;
+65 -5
View File
@@ -2,7 +2,10 @@
#include <string.h>
#include "device_caps.h"
#if CAP_BUILTIN_LANGS
#include "i18n_builtin.h" // generated: baked-in translations
#include "i18n_builtin.h" // generated: baked-in translations, one deflated blob per language
#include <stdlib.h>
#include "esp_heap_caps.h"
#include "rom/miniz.h" // the S3 ROM's tinfl: inflating costs no flash
#endif
// Native language names shown in the picker. Index order == UiLang.
@@ -20,7 +23,63 @@ const char* const kUiLangCodes[LANG_COUNT] = {
};
static uint8_t s_ui_lang = LANG_EN;
void i18nSetLang(uint8_t l) { s_ui_lang = (l < LANG_COUNT) ? l : LANG_EN; }
#if CAP_BUILTIN_LANGS
// The active built-in language, inflated into PSRAM: its "key\0value\0" rows and a
// sorted pair index into them (~50 KB for one language; the V4 has 2 MB of PSRAM).
// Only one language is ever resident: switching frees the previous one. A failed
// allocation or inflate leaves the table empty, so the UI simply stays English.
static I18nPair* s_bi_pairs = nullptr;
static char* s_bi_text = nullptr;
static int s_bi_n = 0;
static uint8_t s_bi_lang = 0xFF;
static void i18nBuiltinLoad(uint8_t lang) {
if (lang == s_bi_lang) return;
s_bi_lang = lang;
s_bi_n = 0;
if (s_bi_pairs) { heap_caps_free(s_bi_pairs); s_bi_pairs = nullptr; }
if (s_bi_text) { heap_caps_free(s_bi_text); s_bi_text = nullptr; }
const I18nBlob& b = kBuiltinBlob[lang];
if (!b.z || !b.count) return;
char* text = (char*)heap_caps_malloc(b.len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
I18nPair* pairs = (I18nPair*)heap_caps_malloc(b.count * sizeof(I18nPair), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
tinfl_decompressor* d = (tinfl_decompressor*)heap_caps_malloc(sizeof(tinfl_decompressor),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
bool ok = false;
if (text && pairs && d) {
tinfl_init(d); // ~11 KB of state: never on a task stack
size_t in = b.zlen, out = b.len;
ok = tinfl_decompress(d, b.z, &in, (mz_uint8*)text, (mz_uint8*)text, &out,
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF) == TINFL_STATUS_DONE && out == b.len;
}
if (d) heap_caps_free(d);
int n = 0;
if (ok) { // walk the rows; the generator wrote them sorted, so the index is too
const char* p = text;
const char* end = text + b.len;
while (n < b.count && p < end) {
pairs[n].key = p; p += strlen(p) + 1;
if (p >= end) break;
pairs[n].val = p; p += strlen(p) + 1;
++n;
}
ok = n == b.count;
}
if (!ok) {
if (text) heap_caps_free(text);
if (pairs) heap_caps_free(pairs);
return;
}
s_bi_text = text; s_bi_pairs = pairs; s_bi_n = n;
}
#endif
void i18nSetLang(uint8_t l) {
s_ui_lang = (l < LANG_COUNT) ? l : LANG_EN;
#if CAP_BUILTIN_LANGS
i18nBuiltinLoad(s_ui_lang);
#endif
}
uint8_t i18nGetLang() { return s_ui_lang; }
// File-language overlay: sorted key/translation pairs owned by the loader
@@ -130,9 +189,10 @@ const char* TR(const char* en) {
}
}
#if CAP_BUILTIN_LANGS
if (!v && s_ui_lang != LANG_EN && kBuiltinLang[s_ui_lang]) {
const I18nPair* tab = kBuiltinLang[s_ui_lang];
int lo = 0, hi = kBuiltinLangCount[s_ui_lang] - 1;
if (!v && s_ui_lang != LANG_EN) {
if (s_bi_lang != s_ui_lang) i18nBuiltinLoad(s_ui_lang); // first use without i18nSetLang
const I18nPair* tab = s_bi_pairs;
int lo = 0, hi = s_bi_n - 1;
while (lo <= hi) { // sorted by the generator
const int mid = (lo + hi) / 2;
const int c = strcmp(base, tab[mid].key);
+13042 -13958
View File
File diff suppressed because it is too large. Load diff
@@ -27,6 +27,7 @@ file(GLOB_RECURSE CORE_SRCS "${CMAKE_CURRENT_LIST_DIR}/${CORE}/*.cpp")
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/radiolib/")
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/sensors/")
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/ethernet/") # 1.17: RAK13800/CH390 ethernet ifaces need libs we don't vendor
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/CommonRadioPrefs\\.cpp$") # 1.18: includes <target.h> + radio_driver (S3 board globals); the companion CLI is in MyMesh
list(FILTER CORE_SRCS EXCLUDE REGEX "(NRF52Board|TBeamBoard)\\.cpp$") # keep ESP32Board (base TanmatsuBoard extends); drop nRF52 + the RadioLib S3 board
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/ui/.*(Display|OLED|TFT|Epaper|EInk|GxEPD|U8g2|SH1106|SSD1306|ST77|ILI9|Adafruit).*\\.cpp$")
list(FILTER CORE_SRCS EXCLUDE REGEX "(LvglPsramAlloc|TouchDiagTrace)\\.cpp$")
@@ -40,6 +40,7 @@ file(GLOB_RECURSE CORE_SRCS "${CMAKE_CURRENT_LIST_DIR}/${CORE}/*.cpp")
# leaving just the GPS/NMEA path). Only the RAK soil-moisture sensor (a separate I2C lib) is dropped.
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/sensors/RAK12035_SoilMoisture\\.cpp$")
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/ethernet/") # 1.17: RAK13800/CH390 ethernet ifaces need libs we don't vendor
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/CommonRadioPrefs\\.cpp$") # 1.18: includes <target.h> + radio_driver (S3 board globals); the companion CLI is in MyMesh
list(FILTER CORE_SRCS EXCLUDE REGEX "(NRF52Board|TBeamBoard)\\.cpp$") # keep ESP32Board (base TDisplayP4Board extends); drop nRF52 + TBeam
list(FILTER CORE_SRCS EXCLUDE REGEX "/helpers/ui/.*(Display|OLED|TFT|Epaper|EInk|GxEPD|U8g2|SH1106|SSD1306|ST77|ILI9|Adafruit).*\\.cpp$")
list(FILTER CORE_SRCS EXCLUDE REGEX "(LvglPsramAlloc|TouchDiagTrace)\\.cpp$")
@@ -25,7 +25,8 @@ public:
updatePreamble(sf);
}
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz()); }
// MeshCore 1.18: the AGC reset re-applies the boosted-gain setting it would otherwise clear.
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); }
bool isReceivingPacket() override {
return ((CustomLR1121 *)_radio)->isReceiving();
}