mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-10-23 15:02:32 +00:00
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:
1 parent
d3a9cf1453
commit
ef0b27f1e0
11 files changed
+13628
-14022
No files matched your search
+12
-12
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user