Reduce nRF52 Companion flash and stream Reader chapters

Use a 960-byte constant-time Ed25519 base table, compact hash/scalar code and unused-library trims while retaining Reader, BLE P-256 and the 8 KiB stack. RAK4631 Full saves 57,496 flash bytes and 3,152 static RAM bytes versus preview3.

Support complete chapters and verse ranges with bounded app continuation replies and cooperative terminal output. Use nonblocking TCP writes under reader backpressure. Add scalar/signature comparisons and a linked ARM crypto/stack CI gate.

Validated clean RAK4631, T1000-E, T096 and V4 Full builds, host regressions, and physical RAK chapter/reconnect and signing comparisons. Physical signing averages 35.9 ms versus 14.4 ms with the original table.
This commit is contained in:
mikecarper committed 2026-10-13 01:02:30 -07:00
1 parent 9ce2a076eb
commit 8c698dc2d1
21 files changed
+803 -86

No files matched your search

+15 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
@@ -8,7 +9,20 @@
class Stream {
public:
std::string text;
size_t print(const char* value) { text += value; return std::strlen(value); }
size_t max_write = 65535;
size_t credit = 65535;
size_t writes = 0;
size_t write(const uint8_t* value, size_t size) {
++writes;
if (size > max_write) size = max_write;
if (size > credit) size = credit;
credit -= size;
text.append(reinterpret_cast<const char*>(value), size);
return size;
}
size_t print(const char* value) {
return write(reinterpret_cast<const uint8_t*>(value), std::strlen(value));
}
size_t printf(const char* format, ...) {
char buf[256];
va_list args;
+108 -1
View File
@@ -8,6 +8,67 @@
using namespace mesh::bible;
static void drain(Stream& output) {
unsigned calls = 0;
while (mesh::readerCommandActive()) {
const size_t writes = output.writes, bytes = output.text.size();
mesh::serviceReaderCommand();
assert(output.writes <= writes + 1 && output.text.size() <= bytes + 96);
assert(++calls < 20000);
}
}
static void checkSelection(unsigned chapter, unsigned first, unsigned last, bool whole) {
char command[64];
if (whole) snprintf(command, sizeof(command), "get reader %u", chapter);
else snprintf(command, sizeof(command), "get reader %u:%u-%u", chapter, first, last);
std::string expected, terminal_expected;
for (unsigned verse = first; verse <= last; ++verse) {
char scratch[kBlockSize], header[48];
const char* text;
assert(mesh::readReaderVerse({uint8_t(chapter), uint8_t(verse)}, scratch,
sizeof(scratch), text) == LookupResult::Found);
expected += text;
snprintf(header, sizeof(header), " Reader %u:%u (WEB)\r\n", chapter, verse);
terminal_expected += std::string(header) + text + "\r\n";
}
terminal_expected += generated::readerCorpus.attribution;
terminal_expected += "\r\n> ";
Stream terminal;
terminal.max_write = 7; // Genuine short writes, including header/newline cuts.
assert(mesh::handleReaderCommand(command, terminal, true));
assert(mesh::readerCommandActive(&terminal));
for (unsigned tick = 0; mesh::readerCommandActive(); ++tick) {
assert(tick < 20000);
terminal.credit = tick % 3 == 0 ? 0 : 11; // Stops accepting bytes regularly.
const size_t before = terminal.text.size();
mesh::serviceReaderCommand();
assert(terminal.text.size() - before <= (tick % 3 == 0 ? 0 : 7));
}
assert(terminal.text == terminal_expected);
for (const size_t capacity : {size_t(160), size_t(157)}) {
std::string next = command, reconstructed;
for (unsigned replies = 0; ; ++replies) {
assert(replies < 1000);
char reply[162];
memset(reply, '!', sizeof(reply));
assert(mesh::handleReaderReply(next.c_str(), reply, capacity));
assert(reply[capacity] == '!' && strlen(reply) < capacity);
std::string page(reply);
assert(page.find("Reader ") == 0);
const size_t body = page.find('\n') + 1;
const size_t continuation = page.find("\nNext: ");
reconstructed += page.substr(body, continuation == std::string::npos
? continuation : continuation - body);
if (continuation == std::string::npos) break;
const std::string command_next = page.substr(continuation + 7);
assert(command_next != next);
next = command_next;
}
assert(reconstructed == expected);
}
}
int main(int argc, char**) {
Reference ref = {};
for (const char* cmd : {"get reader 3:16", " GET\treader 3:16 \t", "get reader 03:16"}) {
@@ -39,6 +100,7 @@ int main(int argc, char**) {
snprintf(cmd, sizeof(cmd), "get reader %u:%u", chapter, verse);
Stream output;
assert(mesh::handleReaderCommand(cmd, output));
drain(output);
const std::string expected = "\r\n" + std::string(text) + "\r\n";
assert(output.text.find(expected) != std::string::npos);
for (const unsigned char* c = reinterpret_cast<const unsigned char*>(text); *c; ++c)
@@ -83,6 +145,47 @@ int main(int argc, char**) {
}
}
assert(count == 879);
for (unsigned chapter = 1; chapter <= 21; ++chapter)
checkSelection(chapter, 1, kChapterVerses[chapter - 1], true);
checkSelection(2, 2, 12, false);
checkSelection(21, 16, 18, false); // Includes a long, multi-page verse.
checkSelection(6, 1, 71, false); // Larger than the real 4 KiB USB queue.
checkSelection(2, 25, 25, false);
Stream cancelled, unrelated;
assert(mesh::handleReaderCommand("get reader 6", cancelled));
mesh::serviceReaderCommand();
mesh::cancelReaderCommand(&unrelated);
assert(mesh::readerCommandActive(&cancelled));
mesh::cancelReaderCommand(&cancelled);
const std::string before_cancel = cancelled.text;
mesh::serviceReaderCommand();
assert(cancelled.text == before_cancel);
assert(mesh::handleReaderCommand("get reader 6", cancelled));
assert(!mesh::handleReaderCommand("get name", unrelated));
assert(!mesh::readerCommandActive());
// TCP bypasses its blocking Stream::write using one socket attempt. Keep
// the original owner for session cancellation, and retry zero/short writes.
Stream socket_owner, socket_bytes, reference;
assert(mesh::handleReaderCommand("get reader 2:2-12", reference, true));
drain(reference);
static Stream* callback_bytes = nullptr;
callback_bytes = &socket_bytes;
auto try_write = [](Stream& owner, const uint8_t* data, size_t size) -> size_t {
assert(owner.writes == 0);
return callback_bytes->write(data, size);
};
assert(mesh::handleReaderCommand("get reader 2:2-12", socket_owner, true, try_write));
assert(mesh::readerCommandActive(&socket_owner));
for (unsigned tick = 0; mesh::readerCommandActive(); ++tick) {
assert(tick < 20000);
socket_bytes.max_write = 5;
socket_bytes.credit = tick % 3 == 0 ? 0 : 9;
mesh::serviceReaderCommand();
}
assert(socket_owner.writes == 0 && socket_bytes.text == reference.text);
assert(mesh::handleReaderCommand("get reader 6", socket_owner, false, try_write));
mesh::cancelReaderCommand(&socket_owner);
assert(!mesh::readerCommandActive());
const char* text;
assert(lookup(corpus, {22, 1}, buf.scratch, kBlockSize, text) == LookupResult::Invalid);
assert(lookup(corpus, {1, 1}, buf.scratch, kBlockSize - 1, text) == LookupResult::Corrupt);
@@ -125,7 +228,11 @@ int main(int argc, char**) {
assert(invalid.text.find("ERROR") != std::string::npos);
for (const char* cmd : {"get reader", "get reader 3:16 0", "get reader 3:16 -1",
"get reader 3:16 100", "get reader 3:16 99999999999", "get reader 3:16 1 junk",
"get reader 3:16 1:2", "get reader 3:16 1\x1b", "get reader 22:1"}) {
"get reader 3:16 1:2", "get reader 3:16 1\x1b", "get reader 22:1",
"get reader 0", "get reader 22", "get reader 2:12-2", "get reader 2:0-12",
"get reader 2:2-26", "get reader 2:2-99999999", "get reader 2:2-",
"get reader 2:2-12-14", "get reader 2:2-12junk", "get reader 2x",
"get reader 2:2--12", "get reader 2:2-3:12"}) {
char reply[160];
assert(mesh::handleReaderReply(cmd, reply, sizeof(reply)));
assert(std::string(reply).find("ERROR: use get reader") == 0);
+47 -1
View File
@@ -111,6 +111,52 @@ class CompanionReaderTest(unittest.TestCase):
self.assertNotIn("readerData", result)
self.assertNotIn("handleReaderCommand", result)
def test_tcp_reader_write_returns_immediately_under_socket_backpressure(self):
compiler = shutil.which("g++")
if not compiler:
self.skipTest("host G++ required")
source = (ROOT / "examples/companion_radio/main.cpp").read_text()
callback = extract_braced(source, "static size_t ota_console_try_reader_write(")
harness = r'''
#include <Arduino.h>
#include <cassert>
#include <sys/socket.h>
#include <unistd.h>
class WiFiClient : public Stream {
int socket_;
public:
explicit WiFiClient(int socket) : socket_(socket) {}
int fd() const { return socket_; }
};
@CALLBACK@
int main() {
alarm(3); // A blocking write to a full socket fails the test, never hangs CI.
int sockets[2];
assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0);
int capacity = 1024;
assert(setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &capacity, sizeof(capacity)) == 0);
uint8_t data[96] = {};
while (send(sockets[0], data, sizeof(data), MSG_DONTWAIT) > 0) {}
WiFiClient client(sockets[0]), invalid(-1);
for (int i = 0; i < 100; ++i)
assert(ota_console_try_reader_write(client, data, sizeof(data)) == 0);
assert(client.writes == 0); // The blocking Stream path must never be used.
uint8_t drained[8192];
assert(recv(sockets[1], drained, sizeof(drained), 0) > 0);
assert(ota_console_try_reader_write(client, data, sizeof(data)) > 0);
assert(ota_console_try_reader_write(invalid, data, sizeof(data)) == 0);
close(sockets[0]);
close(sockets[1]);
}
'''.replace("@CALLBACK@", callback)
with tempfile.TemporaryDirectory(prefix="mesh-reader-socket-") as directory:
cpp = Path(directory) / "socket.cpp"
binary = Path(directory) / "socket-test"
cpp.write_text(harness)
self.run_checked([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-I" + str(FIXTURE), str(cpp), "-o", str(binary)])
self.run_checked([str(binary)])
def test_terminal_dispatch_is_separate_from_bounded_api_replies(self):
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8")
start = source.index("void MyMesh::handleTerminalCommand(")
@@ -119,7 +165,7 @@ class CompanionReaderTest(unittest.TestCase):
self.assertEqual(source.count("mesh::handleReaderCommand("), 2)
self.assertLess(terminal.index("mesh::handleReaderCommand("),
terminal.index("char local_reply[160]"))
self.assertIn("get reader <chapter>:<verse> (World English Bible, offline)", terminal)
self.assertIn("get reader <chapter>[:<verse>[-<last>]] [page] (World English Bible, offline)", terminal)
self.assertEqual(terminal.count("#if COMPANION_FEATURE_READER"), 2)
rescue = extract_braced(source, "void MyMesh::checkCLIRescueCmd()")
self.assertLess(rescue.index("mesh::handleReaderCommand(cli_command, output)"),
+67
View File
@@ -34,6 +34,69 @@ class SHA512Context(ctypes.Structure):
class Ed25519CompactTest(unittest.TestCase):
def test_small_flash_table_matches_standard_for_scalars_keys_and_signatures(self):
# Exercise every bit/carry boundary and deterministic random scalars,
# rather than validating only the two RFC keypair examples.
with tempfile.TemporaryDirectory() as directory:
wrapper = Path(directory) / "wrapper.c"
wrapper.write_text('''#include "ge.h"
void public_from_scalar(unsigned char *out, const unsigned char *scalar) {
ge_p3 point;
ge_scalarmult_base(&point, scalar);
ge_p3_tobytes(out, &point);
}
''')
libraries = []
for label, flags in (
("standard", []),
("small_sha512", ["-DED25519_SMALL_BASE=1", "-DED25519_COMPACT_SHA512=1"]),
("small_all", ["-DED25519_SMALL_BASE=1", "-DED25519_COMPACT_SHA512=1",
"-DED25519_COMPACT_SCALAR=1"]),
):
path = Path(directory) / (label + ".so")
subprocess.run(["cc", "-std=c99", "-O2", "-Wall", "-Wextra", "-Werror",
"-shared", "-fPIC", "-DED25519_NO_SEED=1", *flags,
"-I" + str(ED25519), str(wrapper),
*(str(source) for source in SOURCES),
str(ED25519 / "key_exchange.c"), "-o", str(path)], check=True)
library = ctypes.CDLL(str(path))
library.public_from_scalar.argtypes = [ctypes.c_void_p] * 2
library.ed25519_create_keypair.argtypes = [ctypes.c_void_p] * 3
library.ed25519_sign.argtypes = [ctypes.c_void_p, ctypes.c_void_p,
ctypes.c_size_t, ctypes.c_void_p, ctypes.c_void_p]
library.ed25519_key_exchange.argtypes = [ctypes.c_void_p] * 3
libraries.append(library)
rng = random.Random(0xED960)
scalars = [0, (1 << 255) - 1]
scalars += [value for bit in range(255)
for value in ((1 << bit) - 1, 1 << bit, (1 << bit) + 1)]
scalars += [rng.getrandbits(255) for _ in range(1024)]
for scalar in scalars:
outputs = []
for library in libraries:
out = ctypes.create_string_buffer(32)
library.public_from_scalar(out, scalar.to_bytes(32, "little"))
outputs.append(out.raw)
for output in outputs[1:]:
self.assertEqual(outputs[0], output, "scalar=%x" % scalar)
for iteration in range(128):
seed = rng.getrandbits(256).to_bytes(32, "little")
peer_seed = rng.getrandbits(256).to_bytes(32, "little")
length = (0, 1, 111, 112, 127, 128, 129, 255, 256, 4097)[iteration % 10]
message = bytes(rng.randrange(256) for _ in range(length))
outputs = []
for library in libraries:
pub, prv = ctypes.create_string_buffer(32), ctypes.create_string_buffer(64)
peer, peer_prv = ctypes.create_string_buffer(32), ctypes.create_string_buffer(64)
sig, secret = ctypes.create_string_buffer(64), ctypes.create_string_buffer(32)
library.ed25519_create_keypair(pub, prv, seed)
library.ed25519_create_keypair(peer, peer_prv, peer_seed)
library.ed25519_sign(sig, message, len(message), pub, prv)
library.ed25519_key_exchange(secret, peer, prv)
outputs.append((pub.raw, prv.raw, sig.raw, secret.raw))
for output in outputs[1:]:
self.assertEqual(outputs[0], output)
def build_sha512_library(self, temp_dir, compact):
library = Path(temp_dir) / f"sha512-{int(compact)}.so"
flags = ["-DED25519_COMPACT_SHA512=1"] if compact else []
@@ -113,6 +176,10 @@ class Ed25519CompactTest(unittest.TestCase):
for label, extra_flags in (
("standard", []),
("compact", ["-DED25519_COMPACT_BASE=1"]),
("small", ["-DED25519_SMALL_BASE=1"]),
("small_sha512", ["-DED25519_SMALL_BASE=1", "-DED25519_COMPACT_SHA512=1"]),
("small_all", ["-DED25519_SMALL_BASE=1", "-DED25519_COMPACT_SHA512=1",
"-DED25519_COMPACT_SCALAR=1"]),
("compact_sha512", ["-DED25519_COMPACT_SHA512=1"]),
("compact_base_sha512", ["-DED25519_COMPACT_BASE=1",
"-DED25519_COMPACT_SHA512=1"]),
+56
View File
@@ -160,6 +160,62 @@ class Nrf52FlashTrimTest(unittest.TestCase):
str(fixture / "test_domain_trim.c"), "-o", str(executable)], check=True)
subprocess.run([str(executable)], check=True)
def test_companion_keeps_small_crypto_stack_fonts_reader_and_usb_transports(self):
for platform, mcu, expected in (("nordicnrf52", "nrf52840", True),
("nordicnrf52", "nrf52832", True),
("espressif32", "esp32s3", False)):
with self.subTest(platform=platform, mcu=mcu):
defines = TRIM.companion_trim_policy(platform, mcu,
"+<*> +<../examples/companion_radio/*.cpp>")
self.assertEqual(bool(defines), expected)
if not expected:
continue
self.assertEqual(defines["MESH_NRF52_COMPANION_TRIM"], 1)
self.assertEqual(defines["ED25519_SMALL_BASE"], 1)
self.assertEqual(defines["ED25519_COMPACT_SHA512"], 1)
self.assertEqual(defines["ED25519_COMPACT_SCALAR"], 1)
self.assertEqual(defines["CFG_TUD_MSC"], 0)
self.assertEqual(defines["CFG_TUD_HID"], 0)
for name in ("MESH_NRF52_FLASH_TRIM", "ED25519_COMPACT_BASE",
"CFG_TUD_CDC", "CFG_TUD_DFU",
"CFG_TUD_DFU_RUNTIME", "COMPANION_FEATURE_READER"):
self.assertNotIn(name, defines)
self.assertFalse(any(name.startswith("CFG_TUH") for name in defines))
def test_companion_role_requires_selected_source_and_honors_explicit_options(self):
for sources in ("+<*>", "+<../examples/simple_secure_chat>",
"+<../examples/companion_radio> -<../examples/companion_radio>",
"+<../examples/companion_radio> +<../examples/simple_repeater>"):
self.assertEqual(TRIM.companion_trim_policy("nordicnrf52", "nrf52840", sources), {})
for flags in ("-DMESH_NRF52_COMPANION_TRIM=0", "-UMESH_NRF52_COMPANION_TRIM",
"-DMESH_NRF52_FLASH_TRIM=0"):
self.assertEqual(TRIM.companion_trim_policy("nordicnrf52", "nrf52840",
"+<../examples/companion_radio>", flags), {})
result = TRIM.companion_trim_policy("nordicnrf52", "nrf52840",
"+<../examples/companion_radio>", "-DCFG_TUD_MSC=1 -USSD1306_NO_SPLASH")
self.assertNotIn("CFG_TUD_MSC", result)
self.assertNotIn("SSD1306_NO_SPLASH", result)
result = TRIM.companion_trim_policy("nordicnrf52", "nrf52840",
"+<../examples/companion_radio>",
"-DED25519_SMALL_BASE=0 -UED25519_COMPACT_SHA512 -UED25519_COMPACT_SCALAR")
for name in ("ED25519_SMALL_BASE", "ED25519_COMPACT_SHA512", "ED25519_COMPACT_SCALAR"):
self.assertNotIn(name, result)
for value in (0, 1):
result = TRIM.companion_trim_policy("nordicnrf52", "nrf52840",
"+<../examples/companion_radio>", "-DED25519_COMPACT_BASE=%d" % value)
self.assertNotIn("ED25519_SMALL_BASE", result)
@unittest.skipUnless(shutil.which("cc"), "a native C compiler is required")
def test_companion_uses_same_p256_domain_wrapper(self):
fixture = ROOT / "test/fixtures/nrf52_flash_trim"
with tempfile.TemporaryDirectory() as temp_dir:
executable = Path(temp_dir) / "cc310-companion-test"
subprocess.run(["cc", "-std=c99", "-Wall", "-Wextra", "-Werror",
"-DMESH_NRF52_COMPANION_TRIM=1", "-I" + str(fixture),
str(ROOT / "src/helpers/nrf52/Cc310DomainTrim.c"),
str(fixture / "test_domain_trim.c"), "-o", str(executable)], check=True)
subprocess.run([str(executable)], check=True)
if __name__ == "__main__":
unittest.main()