Qualify Full Meshtext builds and share cold storage on constrained companions

This commit is contained in:
mikecarper committed 2026-10-23 18:08:25 -07:00
1 parent 127d1bec24
commit ab5de10e79
10 files changed
+860 -14

No files matched your search

+82 -3
View File
@@ -4541,6 +4541,10 @@ apply_companion_radio_full_profile() {
heltec_tracker_v2_companion_radio_full_*)
append_platformio_build_unflags "-DMAX_CONTACTS=350 -DOFFLINE_QUEUE_SIZE=256"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=100 -DOFFLINE_QUEUE_SIZE=224"
# Reuse idle queue storage during explicit OTA; retain all 224 normal slots.
if [[ "${env_name,,}" != generic_espnow_companion_radio_full ]]; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DOTA_SHARED_COMPANION_QUEUE=1 -DONE_KEY_DM_COMPACT_HELD=1"
fi
record_build_reduction \
"companion.capacity limited to 100 contacts for runtime RAM; 224 queued frames and all Full transports retained"
;;
@@ -4678,6 +4682,76 @@ copy_build_output() {
cp -- "$source_path" "$output_path"
}
requires_meshtext_companion_code() {
local env_name=$1
local pio_env_name=$2
local decision_status=0
is_companion_radio_full_target "$env_name" || return 1
# Check the effective recipe and overlay, including explicit feature-off
# builds and the Indicator LoRa recipe's inherited ESP-NOW undefine.
python3 -c '
import json, os, re, shlex, sys
rows = {name: dict(options) for name, options in json.load(sys.stdin)}
options = rows.get("env:" + sys.argv[1], {})
def operations(value):
words = iter(shlex.split(value if isinstance(value, str) else "\n".join(map(str, value))))
result = []
for word in words:
if word in ("-D", "-U"):
word += next(words, "")
match = re.fullmatch(r"-([DU])(\w+)(?:=(.*))?", word)
if match:
result.append(match.groups())
return result
flags = operations(options.get("build_flags", []))
flags += operations(os.environ.get("PLATFORMIO_BUILD_FLAGS", ""))
removed = set(operations(options.get("build_unflags", []))
+ operations(os.environ.get("PLATFORMIO_BUILD_UNFLAGS", "")))
flags = [flag for flag in flags if flag not in removed
and not (flag[0] == "D" and ("D", flag[1], None) in removed)]
defines = {name: "1" if value is None else value
for kind, name, value in flags if kind == "D"}
# PlatformIO emits CPPDEFINES before CCFLAGS, so every surviving -U wins.
for kind, name, _value in flags:
if kind == "U":
defines.pop(name, None)
def enabled(name, default=False, seen=()):
if name not in defines:
return default
if name in seen:
return None
value = defines[name].strip().strip("() ")
if value in ("false", "true"):
return value == "true"
if re.fullmatch(r"[A-Za-z_]\w*", value):
return enabled(value, False, (*seen, name))
number = re.fullmatch(r"([+-]?(?:0[xX][0-9a-fA-F]+|[0-9]+))[uUlL]*", value)
if number:
value = number[1]
base = 16 if "x" in value.lower() else 8 if re.fullmatch(r"[+-]?0[0-7]+", value) else 10
return int(value, base) != 0
return None
# Explicit enablement overrides the normal ESP-NOW exclusion, as in the header.
# Unknown expressions retain the requirement rather than silently dropping it.
if "MESH_ENABLE_MESHTEXT_CHAT" in defines:
required = enabled("MESH_ENABLE_MESHTEXT_CHAT") is not False
else:
required = (enabled("COMPANION_RADIO_FULL", True) is not False
and enabled("MESH_PRIMARY_ESPNOW") is not True)
sys.exit(0 if required else 3)
' "$pio_env_name" <<<"$PIO_CONFIG_JSON" || decision_status=$?
case "$decision_status" in
0) return 0 ;;
3) return 1 ;;
*) return 2 ;;
esac
}
write_build_capability_manifest() {
local env_name=$1
local env_platform=$2
@@ -4705,10 +4779,15 @@ write_build_capability_manifest() {
|| is_xiao_qspi_canonical_repeater_build "$env_name"; then
checker_args+=(--require-ota)
fi
if [ "$env_name" = "RAK_4631_companion_radio_full" ]; then
# This profile is offered for MeshTalk testing. Check executable codec and
# Companion transport definitions, not a channel name or CLI help string.
local meshtext_status=0
requires_meshtext_companion_code "$env_name" "$pio_env_name" || meshtext_status=$?
if [ "$meshtext_status" -eq 0 ]; then
# Every enabled Full LoRa Companion must retain executable codec and
# transport definitions, not merely a channel name or CLI help string.
checker_args+=(--require-meshtext)
elif [ "$meshtext_status" -ne 1 ]; then
echo "Cannot determine the Full Companion Meshtext capability contract." >&2
return "$meshtext_status"
fi
if [ "$env_platform" = "ESP32_PLATFORM" ]; then
checker_args+=(--firmware-bin "${build_output_dir}/firmware.bin"
+3
View File
@@ -568,6 +568,9 @@ accepts an `mt-` name; a factory reset cannot add code omitted from an image.
The common default and modem support do not establish physical qualification
for every board. Each release image must still pass its flash and runtime RAM
budgets, and hardware interoperability must be checked separately.
For example, [LLCC68 modulation limits](https://www.semtech.com/products/wireless-rf/lora-connect/llcc68)
exclude LongFast (SF11 at BW250): enabling the backend cannot add that modem
combination. Compatible presets such as MediumSlow or LongTurbo remain available.
In an enabled build, an `mt-` name always requests Meshtext, even if radio2 is
off or setup is blocked. It never falls back to sending MeshCore frames. In a
+42 -5
View File
@@ -567,9 +567,9 @@ uint16_t MyMesh::drainOfflineQueueForOta(void* owner) {
MyMesh* mesh = static_cast<MyMesh*>(owner);
uint8_t discarded[MAX_FRAME_SIZE];
uint16_t removed = 0;
// Explicit OTA work borrows the upper half of this volatile queue. Keep the
// newest 128 frames and notify the UI for each oldest frame consumed.
while (mesh->offline_queue_len > 128) {
// Explicit OTA work borrows the queue's workspace region. Keep the newest
// retained frames and notify the UI for each oldest frame consumed.
while (mesh->offline_queue_len > static_cast<int>(OTA_RETAINED_OFFLINE_FRAMES)) {
if (mesh->getFromOfflineQueue(discarded) <= 0) break;
++removed;
}
@@ -1442,8 +1442,14 @@ bool MyMesh::onAddressedTextPacket(mesh::Packet* packet, uint8_t src_hash,
uint8_t id[ONE_KEY_DM_ID_SIZE];
makeOneKeyDMId(id, timestamp, text);
for (uint8_t j = 0; j < held_dm_count; ++j) {
#if ONE_KEY_DM_COMPACT_HELD
uint8_t held_id[ONE_KEY_DM_ID_SIZE];
makeOneKeyDMId(held_id, held_dms[j].timestamp, held_dms[j].text);
#else
const uint8_t* held_id = held_dms[j].id;
#endif
if (memcmp(held_dms[j].sender_key, pub_key, PUB_KEY_SIZE) == 0 &&
memcmp(held_dms[j].id, id, ONE_KEY_DM_ID_SIZE) == 0) return true;
memcmp(held_id, id, ONE_KEY_DM_ID_SIZE) == 0) return true;
}
// Only verified and decryptable messages count toward the FIFO of 15.
@@ -1483,8 +1489,19 @@ bool MyMesh::onAddressedTextPacket(mesh::Packet* packet, uint8_t src_hash,
}
HeldOneKeyDM& entry = held_dms[held_dm_count++];
memcpy(entry.sender_key, pub_key, PUB_KEY_SIZE);
#if ONE_KEY_DM_COMPACT_HELD
entry.timestamp = timestamp;
// Preserve the exact ACK of the authenticated original plaintext,
// including its attempt/type byte; the delivery ID is derived separately.
mesh::Utils::sha256(entry.ack_hash, sizeof(entry.ack_hash), data,
5 + strlen(text), pub_key, PUB_KEY_SIZE);
entry.path_len = packet->isRouteFlood() ? packet->path_len : 0xFF;
entry.snr = packet->_snr;
memcpy(entry.text, text, strlen(text) + 1);
#else
memcpy(entry.id, id, ONE_KEY_DM_ID_SIZE);
entry.packet = *packet;
#endif
#endif
return true;
}
@@ -1541,6 +1558,19 @@ void MyMesh::releaseHeldOneKeyDMs() {
#else
if (offline_queue_len >= getOfflineQueueCapacity()) break;
#if ONE_KEY_DM_COMPACT_HELD
uint8_t id[ONE_KEY_DM_ID_SIZE];
makeOneKeyDMId(id, entry.timestamp, entry.text);
if (!wasDeliveredOneKeyDM(entry.sender_key, id)) {
mesh::Packet packet;
packet.header = entry.path_len == 0xFF ? ROUTE_TYPE_DIRECT : ROUTE_TYPE_FLOOD;
packet.path_len = entry.path_len == 0xFF ? 0 : entry.path_len;
packet._snr = entry.snr;
onMessageRecv(*contact, &packet, entry.timestamp, entry.text);
rememberDeliveredOneKeyDM(entry.sender_key, id);
}
sendAckTo(*contact, entry.ack_hash, sizeof(entry.ack_hash));
#else
uint8_t data[MAX_PACKET_PAYLOAD + 1];
const int decoded_len = mesh::Utils::MACThenDecrypt(
contact->getSharedSecret(self_id), data, &entry.packet.payload[2],
@@ -1563,6 +1593,7 @@ void MyMesh::releaseHeldOneKeyDMs() {
}
}
}
#endif
for (uint8_t j = i + 1; j < held_dm_count; ++j) held_dms[j - 1] = held_dms[j];
--held_dm_count;
#endif
@@ -2062,8 +2093,14 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
bool replaced_held_dm = false;
#endif
for (uint8_t i = 0; i < held_dm_count; ++i) {
#if ONE_KEY_DM_COMPACT_HELD
uint8_t held_id[ONE_KEY_DM_ID_SIZE];
makeOneKeyDMId(held_id, held_dms[i].timestamp, held_dms[i].text);
#else
const uint8_t* held_id = held_dms[i].id;
#endif
if (memcmp(held_dms[i].sender_key, from.id.pub_key, PUB_KEY_SIZE) == 0 &&
memcmp(held_dms[i].id, dm_id, ONE_KEY_DM_ID_SIZE) == 0) {
memcmp(held_id, dm_id, ONE_KEY_DM_ID_SIZE) == 0) {
#if ONE_KEY_DM_SHARED_OFFLINE_QUEUE
// Removing the held frame frees exactly one ordinary slot for this retry.
removeHeldOneKeyDM(i);
+31 -2
View File
@@ -124,6 +124,18 @@ namespace mesh { class BeaconManager; }
#define ONE_KEY_DM_SHARED_OFFLINE_QUEUE 0
#endif
// Constrained Full recipes may retain only the authenticated plain DM data.
// This is independent of the ordinary offline queue and its OTA workspace.
#ifndef ONE_KEY_DM_COMPACT_HELD
#define ONE_KEY_DM_COMPACT_HELD 0
#endif
#if ONE_KEY_DM_COMPACT_HELD != 0 && ONE_KEY_DM_COMPACT_HELD != 1
#error "ONE_KEY_DM_COMPACT_HELD must be 0 or 1"
#endif
#if ONE_KEY_DM_COMPACT_HELD && ONE_KEY_DM_SHARED_OFFLINE_QUEUE
#error "Compact held DMs and shared held-DM queue storage are separate policies"
#endif
#ifndef OFFLINE_QUEUE_SIZE
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
#define OFFLINE_QUEUE_SIZE 512
@@ -955,8 +967,13 @@ private:
// Local onboarding only: RAM-backed like the receive queue, not RF traffic.
uint8_t pending_emergency_notices[(MAX_GROUP_CHANNELS + 7) / 8] = {};
#if defined(OTA_SHARED_COMPANION_QUEUE)
static_assert(OFFLINE_QUEUE_SIZE == 256, "Shared mOTA queue requires 256 normal slots");
mesh::BorrowableFrameBuffer<Frame, OFFLINE_QUEUE_SIZE, 128, mesh::ota::OtaContext> offline_queue;
static constexpr unsigned OTA_RETAINED_OFFLINE_FRAMES = 128;
static_assert(OFFLINE_QUEUE_SIZE > OTA_RETAINED_OFFLINE_FRAMES,
"Shared mOTA queue must retain 128 frames and provide workspace storage");
// The buffer also asserts that the complete context fits in the remaining
// frames. Constrained ESP32 recipes keep 224 normal slots; nRF52 and Paper keep 256.
mesh::BorrowableFrameBuffer<Frame, OFFLINE_QUEUE_SIZE,
OTA_RETAINED_OFFLINE_FRAMES, mesh::ota::OtaContext> offline_queue;
static mesh::ota::OtaContext* acquireOfflineQueueForOta(void* owner);
static uint16_t drainOfflineQueueForOta(void* owner);
static void releaseOfflineQueueFromOta(void* owner);
@@ -1021,11 +1038,23 @@ private:
uint8_t verified_pending_count = 0;
struct HeldOneKeyDM {
uint8_t sender_key[PUB_KEY_SIZE];
#if ONE_KEY_DM_COMPACT_HELD
uint32_t timestamp;
uint8_t ack_hash[4];
uint8_t path_len;
int8_t snr;
char text[MAX_TEXT_LEN + 1];
#else
uint8_t id[ONE_KEY_DM_ID_SIZE];
#if !ONE_KEY_DM_SHARED_OFFLINE_QUEUE
mesh::Packet packet;
#endif
#endif
};
#if ONE_KEY_DM_COMPACT_HELD
static_assert(sizeof(HeldOneKeyDM) <= PUB_KEY_SIZE + MAX_TEXT_LEN + 12,
"Compact held DMs must not retain a complete packet or duplicate ID");
#endif
HeldOneKeyDM held_dms[MAX_HELD_ONE_KEY_DMS];
uint8_t held_dm_count = 0;
struct DeliveredOneKeyDM {
+4 -4
View File
@@ -152,10 +152,10 @@ class ESPNowRawReassemblerT {
public:
static_assert(SlotCount > 0, "ESP-NOW reassembly needs at least one slot");
explicit ESPNowRawReassemblerT(uint32_t timeout_ms = 2000)
: _timeout_ms(timeout_ms) {
reset();
}
// Constant initialization also lets the linker discard the primary-radio
// workspace when this shared source is compiled into a LoRa-only image.
explicit constexpr ESPNowRawReassemblerT(uint32_t timeout_ms = 2000)
: _timeout_ms(timeout_ms), _slots{} {}
void reset() {
memset(_slots, 0, sizeof(_slots));
+3
View File
@@ -24,6 +24,9 @@
// Qualified Full Companions stream host-provided firmware and need no permanent
// mOTA workspace. Apply the same policy to build.sh and direct PlatformIO builds.
// Other qualified recipes, including the constrained ESP32 224-frame queue, opt in through
// OTA_SHARED_COMPANION_QUEUE. The queue checks the complete context size against
// its actual upper region and retains the same 128 frames during explicit OTA.
#if defined(OTA_SEEDER_ONLY) && defined(COMPANION_RADIO_FULL) && \
(defined(NRF52_PLATFORM) || \
(defined(ESP32_PLATFORM) && defined(HELTEC_WIRELESS_PAPER)))
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Exercise C3's full queue while its explicit OTA work borrows cold storage."""
from pathlib import Path
import re
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(text, signature):
start = text.index(signature)
end = text.index("{", start) + 1
depth = 1
while depth:
depth += (text[end] == "{") - (text[end] == "}")
end += 1
return text[start:end]
class SharedCompanionOtaCapacityTest(unittest.TestCase):
def compile(self, text, *, run=False):
with tempfile.TemporaryDirectory(prefix="meshcore-shared-c3-") as temp:
binary = Path(temp) / "queue"
command = ["c++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-I", str(ROOT / "src"), "-x", "c++", "-",
"-o", str(binary)]
if run:
command[1:1] = ["-fsanitize=address,undefined", "-g"]
result = subprocess.run(command, input=text, text=True,
capture_output=True)
if run:
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], text=True,
capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
return result
def test_224_normal_slots_and_128_retained_slots_preserve_fifo_and_ownership(self):
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
declaration = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
retained = re.search(
r"static constexpr unsigned OTA_RETAINED_OFFLINE_FRAMES = (\d+);",
declaration)[1]
methods = "\n".join(method(source, signature) for signature in (
"mesh::ota::OtaContext* MyMesh::acquireOfflineQueueForOta(void* owner)",
"uint16_t MyMesh::drainOfflineQueueForOta(void* owner)",
"void MyMesh::releaseOfflineQueueFromOta(void* owner)",
))
harness = r'''
#include <helpers/BorrowableFrameBuffer.h>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <deque>
constexpr unsigned MAX_FRAME_SIZE = 176;
namespace mesh { namespace ota {
struct OtaContext {
static unsigned live, constructions, destructions;
uint8_t bytes[11284]; // source-only ESP32 linked context, not a RAM budget
OtaContext() {
++live; ++constructions;
memset(bytes, 0xA5, sizeof bytes);
}
~OtaContext() {
--live; ++destructions;
memset(bytes, 0x5A, sizeof bytes);
}
};
unsigned OtaContext::live = 0, OtaContext::constructions = 0,
OtaContext::destructions = 0;
} }
struct MyMesh {
struct Frame { uint8_t len; uint8_t buf[MAX_FRAME_SIZE]; };
static constexpr unsigned OTA_RETAINED_OFFLINE_FRAMES = @RETAINED@;
mesh::BorrowableFrameBuffer<Frame, 224, OTA_RETAINED_OFFLINE_FRAMES,
mesh::ota::OtaContext> offline_queue;
int offline_queue_len = 0, offline_queue_head = 0;
unsigned next = 0, notifications = 0;
std::deque<unsigned> expected;
static mesh::ota::OtaContext* acquireOfflineQueueForOta(void* owner);
static uint16_t drainOfflineQueueForOta(void* owner);
static void releaseOfflineQueueFromOta(void* owner);
void add() {
assert(offline_queue_len < int(offline_queue.capacity()));
Frame& frame = offline_queue.at(
(offline_queue_head + offline_queue_len) % offline_queue.capacity());
frame.len = sizeof frame.buf;
memset(frame.buf, next % 251, sizeof frame.buf);
memcpy(frame.buf, &next, sizeof next);
expected.push_back(next++);
++offline_queue_len;
}
int getFromOfflineQueue(uint8_t* bytes) {
if (!offline_queue_len) return 0;
Frame& frame = offline_queue.at(offline_queue_head);
unsigned sequence;
memcpy(&sequence, frame.buf, sizeof sequence);
assert(frame.len == sizeof frame.buf && sequence == expected.front());
for (unsigned i = sizeof sequence; i < sizeof frame.buf; ++i)
assert(frame.buf[i] == sequence % 251);
memcpy(bytes, frame.buf, frame.len);
expected.pop_front();
--offline_queue_len;
offline_queue_head = offline_queue_len
? (offline_queue_head + 1) % offline_queue.capacity() : 0;
++notifications;
return frame.len;
}
void pop() { uint8_t bytes[MAX_FRAME_SIZE]; assert(getFromOfflineQueue(bytes)); }
};
@METHODS@
int main() {
using mesh::ota::OtaContext;
MyMesh mesh;
for (unsigned head = 0; head < 224; ++head) {
while (mesh.offline_queue_len < 224) mesh.add();
// Visit every wrapped head with a full 224-frame normal queue.
for (unsigned i = 0; i < head; ++i) { mesh.pop(); mesh.add(); }
int original_head = mesh.offline_queue_head;
unsigned constructed = OtaContext::constructions;
assert(!MyMesh::acquireOfflineQueueForOta(&mesh));
assert(mesh.offline_queue_len == 224);
assert(mesh.offline_queue_head == original_head);
assert(OtaContext::constructions == constructed && OtaContext::live == 0);
// Only the explicit drain consumes frames, preserving the newest 128.
unsigned notified = mesh.notifications;
assert(MyMesh::drainOfflineQueueForOta(&mesh) == 96);
assert(mesh.notifications == notified + 96 && mesh.offline_queue_len == 128);
assert(MyMesh::drainOfflineQueueForOta(&mesh) == 0);
OtaContext* owner = MyMesh::acquireOfflineQueueForOta(&mesh);
assert(owner && OtaContext::live == 1 && mesh.offline_queue.capacity() == 128);
assert(OtaContext::constructions == constructed + 1);
assert(MyMesh::acquireOfflineQueueForOta(&mesh) == owner);
assert(OtaContext::constructions == constructed + 1);
memset(owner->bytes, 0xCC, sizeof owner->bytes);
for (unsigned i = 0; i < head % 128; ++i) { mesh.pop(); mesh.add(); }
MyMesh::releaseOfflineQueueFromOta(&mesh);
assert(OtaContext::live == 0 && mesh.offline_queue.capacity() == 224);
assert(OtaContext::constructions == OtaContext::destructions);
MyMesh::releaseOfflineQueueFromOta(&mesh); // idle release cannot destroy twice
while (mesh.offline_queue_len < 224) mesh.add();
while (mesh.offline_queue_len) mesh.pop();
assert(mesh.expected.empty());
}
}
'''.replace("@RETAINED@", retained).replace("@METHODS@", methods)
self.compile(harness, run=True)
def test_insufficient_workspace_or_no_retained_region_cannot_compile(self):
harness = r'''
#include <helpers/BorrowableFrameBuffer.h>
#include <cstdint>
struct Frame { uint8_t len; uint8_t bytes[176]; };
struct Workspace { uint8_t bytes[@SIZE@]; };
mesh::BorrowableFrameBuffer<Frame, @CAPACITY@, 128, Workspace> queue;
int main() { return queue.capacity() == @CAPACITY@ ? 0 : 1; }
'''
# Exact region bounds and the existing 256/128 geometry remain valid.
for capacity, size, accepted in ((224, 96 * 177, True),
(224, 96 * 177 + 1, False),
(256, 19624, True),
(128, 1, False)):
with self.subTest(capacity=capacity, size=size):
result = self.compile(harness.replace("@CAPACITY@", str(capacity))
.replace("@SIZE@", str(size)))
self.assertEqual(result.returncode == 0, accepted, result.stderr)
if not accepted:
self.assertIn("static assertion failed", result.stderr)
if __name__ == "__main__":
unittest.main()
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Check cold ESP-NOW storage initialization and unused linker reclamation."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class EspNowRawInitializationTest(unittest.TestCase):
def test_constexpr_initialization_does_not_root_an_unused_workspace(self):
with tempfile.TemporaryDirectory(prefix="espnow-unused-init-") as temp:
workspace = Path(temp) / "workspace.cpp"
workspace.write_text(r'''
#include <helpers/ESPNowRawFragmentation.h>
constexpr mesh::espnow::ESPNowRawReassembler constant_initialized(1234);
mesh::espnow::ESPNowRawReassembler unused_reassembler;
''')
main = Path(temp) / "main.cpp"
main.write_text("int main() { return 0; }\n")
binary = Path(temp) / "without-espnow"
# Deliberately disable optimization/LTO: only the linker's real
# section reclamation may drop this externally visible global.
result = subprocess.run([
"c++", "-std=c++11", "-O0", "-fno-lto",
"-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections",
"-I", str(ROOT / "src"), str(workspace), str(main),
"-o", str(binary),
], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
symbols = subprocess.run(["nm", "--defined-only", str(binary)],
text=True, capture_output=True, check=True).stdout
self.assertNotIn("unused_reassembler", symbols)
self.assertNotIn("_GLOBAL__sub_I_unused_reassembler", symbols)
subprocess.run([str(binary)], check=True)
def test_default_and_custom_timeouts_with_fragmentation_reset_and_rollover(self):
harness = r'''
#include <helpers/ESPNowRawFragmentation.h>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <new>
using namespace mesh::espnow;
ESPNowRawReassembler default_initialized;
int main() {
uint8_t packet[255], output[255], mac[6] = {2, 1, 2, 3, 4, 5};
for (unsigned i = 0; i < sizeof packet; ++i) packet[i] = i;
ESPNowRawFrames frames;
assert(encodeEspNowRawFrames(packet, sizeof packet, frames));
size_t size = 99;
assert(default_initialized.expire(0) == 0);
assert(default_initialized.acceptFrame(mac, frames.data[0], frames.lengths[0],
1, output, sizeof output, size) == ESPNowRawReassemblyResult::FRAGMENT_STORED);
assert(default_initialized.expire(2000) == 0);
assert(default_initialized.expire(2001) == 1); // unchanged 2000 ms default
// A placement-new object must clear arbitrary prior bytes, not depend on
// global .bss zeroing. Custom initialization preserves its exact timeout.
alignas(ESPNowRawReassemblerT<1>) uint8_t arena[sizeof(ESPNowRawReassemblerT<1>)];
memset(arena, 0xCC, sizeof arena);
auto* reassembler = new (arena) ESPNowRawReassemblerT<1>(20);
assert(reassembler->expire(1000) == 0);
assert(reassembler->acceptFrame(mac, frames.data[0], frames.lengths[0],
0xFFFFFFF0UL, output, sizeof output, size)
== ESPNowRawReassemblyResult::FRAGMENT_STORED);
assert(reassembler->expire(3) == 0);
assert(reassembler->expire(4) == 1);
assert(reassembler->acceptFrame(mac, frames.data[1], frames.lengths[1],
5, output, sizeof output, size) == ESPNowRawReassemblyResult::FRAGMENT_STORED);
assert(reassembler->acceptFrame(mac, frames.data[0], frames.lengths[0],
6, output, sizeof output, size) == ESPNowRawReassemblyResult::PACKET_COMPLETE);
assert(size == sizeof packet && !memcmp(output, packet, sizeof packet));
assert(reassembler->acceptFrame(mac, frames.data[0], frames.lengths[0],
7, output, sizeof output, size) == ESPNowRawReassemblyResult::DUPLICATE);
reassembler->reset();
assert(reassembler->expire(1000) == 0);
assert(reassembler->acceptFrame(mac, frames.data[0], frames.lengths[0],
8, output, sizeof output, size) == ESPNowRawReassemblyResult::FRAGMENT_STORED);
assert(reassembler->acceptFrame(mac, frames.data[1], frames.lengths[1],
9, output, sizeof output, size) == ESPNowRawReassemblyResult::PACKET_COMPLETE);
assert(size == sizeof packet && !memcmp(output, packet, sizeof packet));
reassembler->~ESPNowRawReassemblerT<1>();
}
'''
with tempfile.TemporaryDirectory(prefix="espnow-live-init-") as temp:
binary = Path(temp) / "reassembler"
result = subprocess.run([
"c++", "-std=c++11", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-g", "-I", str(ROOT / "src"),
"-x", "c++", "-", "-o", str(binary),
], input=harness, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()
+102
View File
@@ -1,6 +1,7 @@
"""Qualify linked chat code and ship its MIT notice without touching firmware."""
import hashlib
import json
import os
from pathlib import Path
import shutil
import struct
@@ -42,6 +43,107 @@ def elf32(path, symbols=SYMBOLS, flags=6, undefined=(), zero=()):
class MeshtextLicenseTest(unittest.TestCase):
def builder_contract(self, target, platform, *, flags=(), overlay="", unflags="",
config_unflags=(), symbols=("unrelated_defined_function",), invalid_config=False):
with tempfile.TemporaryDirectory(prefix="meshtext-build-contract-") as folder:
root = Path(folder)
build = root / "build/base"
build.mkdir(parents=True)
elf32(build / "firmware.elf", symbols=symbols)
config = [["env:base", [["build_flags", ["-DCOMPANION_RADIO_FULL=1", *flags]],
["build_unflags", list(config_unflags)]]]]
script = r'''
set -euo pipefail
pio() { printf 'unexpected PlatformIO invocation\n' >&2; return 99; }
source build.sh
PIO_ENV_PLATFORM_BY_NAME[$1]=$2
PIO_CONFIG_JSON=$MESH_TEST_CONFIG
OUTPUT_DIR=$MESH_TEST_DIRECTORY/out
PIO_BUILD_DIR_OVERRIDE=$MESH_TEST_DIRECTORY/build
BUILD_PROFILE_FOR_TARGET=full
BUILD_RECIPE_SHA256=test
BUILD_CAPABILITIES=(); BUILD_EXPECTATIONS=()
BUILD_APPLICATION_EXPECTATIONS=(); BUILD_REDUCTIONS=()
requires_wireless_self_update() { return 1; }
is_xiao_qspi_canonical_repeater_build() { return 1; }
is_nrf52_sensor_ota_pair_target() { return 1; }
embedded_bootloader_artifact_target() { printf '%s\n' "$1"; }
python3() {
# Only the unrelated recipe attachment is substituted. The real builder
# decision and ELF capability checker must reject omitted chat code.
if [ "$1" = scripts/firmware_build_recipe.py ]; then return 0; fi
command "$MESH_TEST_PYTHON" "$@"
}
write_build_capability_manifest "$1" "$2" base fixture
'''
result = subprocess.run(["bash", "-c", script, "meshtext-contract", target, platform],
cwd=ROOT, text=True, capture_output=True, env={**os.environ,
"MESH_TEST_CONFIG": "invalid JSON" if invalid_config else json.dumps(config),
"MESH_TEST_DIRECTORY": str(root), "MESH_TEST_PYTHON": sys.executable,
"PLATFORMIO_BUILD_FLAGS": overlay, "PLATFORMIO_BUILD_UNFLAGS": unflags})
manifest = root / "out/fixture.capabilities.json"
return result, json.loads(manifest.read_text()) if manifest.exists() else None
def test_builder_requires_real_chat_on_every_full_companion_family(self):
for target, platform in (
("RAK_4631_companion_radio_full", "NRF52_PLATFORM"),
("RAK_3401_companion_radio_full", "NRF52_PLATFORM"),
("t1000e_companion_radio_full", "NRF52_PLATFORM"),
("Xiao_nrf52_companion_radio_full", "NRF52_PLATFORM"),
("Heltec_v2_companion_radio_full", "ESP32_PLATFORM"),
("heltec_v4_companion_radio_full_femon", "ESP32_PLATFORM"),
("Xiao_C3_companion_radio_full", "ESP32_PLATFORM"),
("Xiao_C6_companion_radio_full_", "ESP32_PLATFORM"),
):
with self.subTest(target=target):
result, manifest = self.builder_contract(target, platform)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertFalse(manifest["verified"])
self.assertIn("promised", result.stderr)
result, manifest = self.builder_contract("t1000e_companion_radio_full", "NRF52_PLATFORM",
symbols=SYMBOLS)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertTrue(manifest["verified"])
self.assertIn(license.CAPABILITY, manifest["capabilities"])
def test_builder_preserves_effective_feature_overrides_and_espnow_exclusion(self):
cases = (
(dict(flags=["-DMESH_ENABLE_MESHTEXT_CHAT=0"]), False),
(dict(overlay="-D MESH_ENABLE_MESHTEXT_CHAT=0"), False),
(dict(flags=["-DMESH_ENABLE_MESHTEXT_CHAT=0"], overlay="-DMESH_ENABLE_MESHTEXT_CHAT=1"), True),
(dict(flags=["-DMESH_ENABLE_MESHTEXT_CHAT=1"], overlay="-DMESH_ENABLE_MESHTEXT_CHAT=0"), False),
(dict(flags=["-DMESH_PRIMARY_ESPNOW=1"]), False),
(dict(flags=["-DMESH_PRIMARY_ESPNOW=1", "-UMESH_PRIMARY_ESPNOW"]), True),
(dict(flags=["-DMESH_PRIMARY_ESPNOW=1"], overlay="-DMESH_PRIMARY_ESPNOW=0"), True),
(dict(flags=["-DMESH_PRIMARY_ESPNOW=1", "-DMESH_ENABLE_MESHTEXT_CHAT=1"]), True),
(dict(flags=["-DMESH_ENABLE_MESHTEXT_CHAT=0"], overlay="-UMESH_ENABLE_MESHTEXT_CHAT"), True),
(dict(flags=["-DMESH_ENABLE_MESHTEXT_CHAT=0"], unflags="-D MESH_ENABLE_MESHTEXT_CHAT=0"), True),
(dict(flags=["-DMESH_ENABLE_MESHTEXT_CHAT=0"], config_unflags=["-DMESH_ENABLE_MESHTEXT_CHAT"]), True),
(dict(flags=["-DMESH_PRIMARY_ESPNOW=1", "-UMESH_PRIMARY_ESPNOW"],
config_unflags=["-UMESH_PRIMARY_ESPNOW"]), False),
(dict(flags=["-DCHAT_POLICY=0", "-DMESH_ENABLE_MESHTEXT_CHAT=CHAT_POLICY"]), False),
(dict(flags=["-DMESH_ENABLE_MESHTEXT_CHAT=(0UL)"]), False),
(dict(flags=["-DCOMPANION_RADIO_FULL=0"]), False),
)
for options, required in cases:
with self.subTest(options=options):
result, manifest = self.builder_contract("Heltec_v2_companion_radio_full",
"ESP32_PLATFORM", **options)
self.assertEqual(result.returncode, int(required), result.stdout + result.stderr)
self.assertEqual(manifest["verified"], not required)
for target, platform in (("t1000e_companion_radio_usb", "NRF52_PLATFORM"),
("PicoW_companion_radio_full", "RP2040_PLATFORM"),
("RAK_3x72_companion_radio_full", "STM32_PLATFORM")):
with self.subTest(target=target):
result, manifest = self.builder_contract(target, platform)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertTrue(manifest["verified"])
result, manifest = self.builder_contract("Heltec_v2_companion_radio_full", "ESP32_PLATFORM",
invalid_config=True)
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
self.assertIsNone(manifest)
self.assertIn("Cannot determine", result.stderr)
def qualify(self, build, stem):
output = stem.parent / (stem.name + ".capabilities.json")
result = subprocess.run([sys.executable, str(ROOT / "scripts/check_firmware_capabilities.py"),
+316
View File
@@ -0,0 +1,316 @@
#!/usr/bin/env python3
"""Run the C3 compact-held-DM policy through the real Companion methods."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def block(text, signature):
start = text.index(signature)
end = text.index("{", start) + 1
depth = 1
while depth:
depth += (text[end] == "{") - (text[end] == "}")
end += 1
return text[start:end]
class CompactHeldDmTest(unittest.TestCase):
def test_independent_capacity_fifo_promotion_metadata_ack_and_retry_dedup(self):
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
methods = "\n".join(block(source, signature) for signature in (
"bool MyMesh::Frame::isChannelMsg() const",
"int MyMesh::getOfflineQueueCapacity() const",
"MyMesh::Frame& MyMesh::offlineQueueFrameAt(int logical_index)",
"bool MyMesh::addToOfflineQueue(const uint8_t frame[], int len)",
"int MyMesh::peekOfflineQueue(uint8_t frame[])",
"void MyMesh::popOfflineQueue()",
"int MyMesh::getFromOfflineQueue(uint8_t frame[])",
"void MyMesh::forgetVerifiedPendingSender(const uint8_t* pub_key)",
"void MyMesh::makeOneKeyDMId(uint8_t id[ONE_KEY_DM_ID_SIZE], uint32_t timestamp,",
"bool MyMesh::wasDeliveredOneKeyDM(const uint8_t* pub_key,",
"void MyMesh::rememberDeliveredOneKeyDM(const uint8_t* pub_key,",
"bool MyMesh::onAddressedTextPacket(mesh::Packet* packet, uint8_t src_hash,",
"void MyMesh::releaseHeldOneKeyDMs()",
"void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,",
))
harness = r'''
#include <helpers/BorrowableFrameBuffer.h>
#include <openssl/sha.h>
#include <array>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#define ONE_KEY_DM_COMPACT_HELD 1
#define ONE_KEY_DM_SHARED_OFFLINE_QUEUE 0
#define OTA_SHARED_COMPANION_QUEUE 1
#define OFFLINE_QUEUE_SIZE 224
#define MAX_FRAME_SIZE 176
#define MAX_PACKET_PAYLOAD 184
#define MAX_TEXT_LEN 160
#define PUB_KEY_SIZE 32
#define TXT_TYPE_PLAIN 0
#define ROUTE_TYPE_DIRECT 2
#define ROUTE_TYPE_FLOOD 1
#define MESH_ENABLE_ONE_KEY_DM 1
#define MESH_ENABLE_LOST_REPLY 0
#define RESP_CODE_CHANNEL_MSG_RECV 0x8c
#define RESP_CODE_CHANNEL_MSG_RECV_V3 0x90
#define RESP_CODE_CHANNEL_DATA_RECV 0x8d
#define MESH_DEBUG_PRINTLN(...) ((void)0)
namespace mesh {
struct Packet {
uint8_t header = ROUTE_TYPE_FLOOD;
uint8_t path_len = 0;
int8_t _snr = 0;
bool isRouteFlood() const { return header == ROUTE_TYPE_FLOOD; }
};
struct Utils {
static bool authentic;
static int MACThenDecrypt(const uint8_t*, uint8_t* out,
const uint8_t* in, size_t len) {
if (!authentic) return 0;
memcpy(out, in, len);
return static_cast<int>(len);
}
static void sha256(uint8_t* out, size_t size, const uint8_t* a, size_t a_size,
const uint8_t* b, size_t b_size) {
std::vector<uint8_t> input(a, a + a_size);
input.insert(input.end(), b, b + b_size);
uint8_t digest[SHA256_DIGEST_LENGTH];
SHA256(input.data(), input.size(), digest);
memcpy(out, digest, size);
}
};
bool Utils::authentic = true;
namespace ota { struct OtaContext { uint8_t bytes[11284]; }; }
}
struct ContactInfo { struct { uint8_t pub_key[PUB_KEY_SIZE] = {}; } id; };
struct SelfIdentity {
void calcSharedSecret(uint8_t* secret, const uint8_t*) { memset(secret, 0, PUB_KEY_SIZE); }
};
struct Delivery {
uint32_t timestamp;
std::string text;
uint8_t route, path;
int8_t snr;
};
class MyMesh {
public:
static constexpr uint8_t MAX_HELD_ONE_KEY_DMS = 15;
static constexpr uint8_t ONE_KEY_DM_ID_SIZE = 8;
struct Frame {
uint8_t len = 0, buf[MAX_FRAME_SIZE] = {};
bool isChannelMsg() const;
};
@HELD@;
@DELIVERED@;
static_assert(sizeof(HeldOneKeyDM) == 204, "15 slots must save 1740 bytes");
mesh::BorrowableFrameBuffer<Frame, 224, 128, mesh::ota::OtaContext> offline_queue;
int offline_queue_len = 0, offline_queue_head = 0;
HeldOneKeyDM held_dms[MAX_HELD_ONE_KEY_DMS] = {};
uint8_t held_dm_count = 0;
uint8_t verified_pending_keys[MAX_HELD_ONE_KEY_DMS][PUB_KEY_SIZE] = {};
uint8_t verified_pending_count = 1;
DeliveredOneKeyDM delivered_dms[MAX_HELD_ONE_KEY_DMS] = {};
uint8_t delivered_dm_count = 0, delivered_dm_next = 0;
std::vector<Delivery> deliveries;
std::vector<std::array<uint8_t, 4>> acks;
bool accepted = false;
unsigned writes = 0, active = 0;
ContactInfo contact;
SelfIdentity self_id;
MyMesh() {
for (unsigned i = 0; i < PUB_KEY_SIZE; ++i)
contact.id.pub_key[i] = verified_pending_keys[0][i] = 0x42 + i;
}
int getOfflineQueueCapacity() const;
Frame& offlineQueueFrameAt(int index);
bool addToOfflineQueue(const uint8_t frame[], int len);
int peekOfflineQueue(uint8_t frame[]);
void popOfflineQueue();
int getFromOfflineQueue(uint8_t frame[]);
bool onAddressedTextPacket(mesh::Packet*, uint8_t, const uint8_t*, size_t);
void releaseHeldOneKeyDMs();
void forgetVerifiedPendingSender(const uint8_t*);
static void makeOneKeyDMId(uint8_t[ONE_KEY_DM_ID_SIZE], uint32_t, const char*);
bool wasDeliveredOneKeyDM(const uint8_t*, const uint8_t[ONE_KEY_DM_ID_SIZE]) const;
void rememberDeliveredOneKeyDM(const uint8_t*, const uint8_t[ONE_KEY_DM_ID_SIZE]);
void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*);
ContactInfo* lookupPersistentContactByPubKey(const uint8_t* key, size_t) {
return accepted && !memcmp(contact.id.pub_key, key, PUB_KEY_SIZE) ? &contact : nullptr;
}
void markConnectionActive(const ContactInfo&) { ++active; }
void scheduleContactWrite(const ContactInfo&) { ++writes; }
bool queueMessage(const ContactInfo&, uint8_t, mesh::Packet* packet,
uint32_t timestamp, const uint8_t*, int, const char* text) {
uint8_t frame[MAX_FRAME_SIZE] = {1}; // private, never evicted as a channel
memcpy(frame + 1, &timestamp, 4);
memcpy(frame + 5, text, strlen(text));
if (!addToOfflineQueue(frame, 5 + strlen(text))) return false;
deliveries.push_back({timestamp, text, packet->header, packet->path_len, packet->_snr});
return true;
}
void sendAckTo(const ContactInfo&, const uint8_t* hash, uint8_t length) {
assert(length == 4);
std::array<uint8_t, 4> ack;
memcpy(ack.data(), hash, 4);
acks.push_back(ack);
}
void fill() {
const uint8_t frame = 1;
while (offline_queue_len < getOfflineQueueCapacity()) assert(addToOfflineQueue(&frame, 1));
}
void drain() { uint8_t frame[MAX_FRAME_SIZE]; while (getFromOfflineQueue(frame)) {} }
};
@METHODS@
struct Message {
uint32_t timestamp;
std::string text;
mesh::Packet packet;
std::vector<uint8_t> plain;
std::array<uint8_t, 4> ack;
Message(unsigned id, size_t length = 0) : timestamp(id) {
text = length ? std::string(length, 'x') : "message-" + std::to_string(id);
if (length) { text[length - 2] = char(0xC3); text[length - 1] = char(0xA9); }
plain.resize(5 + text.size());
memcpy(plain.data(), &timestamp, 4);
plain[4] = id % 4; // real attempt bits must survive in the exact ACK
memcpy(plain.data() + 5, text.data(), text.size());
packet.header = id % 2 ? ROUTE_TYPE_DIRECT : ROUTE_TYPE_FLOOD;
packet.path_len = id % 2 ? 21 : 0x82;
packet._snr = id % 2 ? -91 : 51;
// Independently concatenate and hash exactly the original plaintext/key.
std::vector<uint8_t> input = plain;
for (unsigned i = 0; i < PUB_KEY_SIZE; ++i) input.push_back(0x42 + i);
uint8_t digest[SHA256_DIGEST_LENGTH];
SHA256(input.data(), input.size(), digest);
memcpy(ack.data(), digest, 4);
}
bool hold(MyMesh& receiver) {
return receiver.onAddressedTextPacket(&packet, 0x42, plain.data(), plain.size());
}
};
int main() {
{
MyMesh receiver;
std::vector<Message> messages;
for (unsigned i = 1; i <= 16; ++i) {
messages.emplace_back(i, i == 16 ? MAX_TEXT_LEN : 0);
assert(messages.back().hold(receiver));
}
assert(receiver.held_dm_count == 15 && receiver.held_dms[0].timestamp == 2);
assert(receiver.held_dms[14].text[MAX_TEXT_LEN] == 0);
auto original_ack = receiver.held_dms[14].ack_hash[0];
messages.back().plain[4] ^= 1; // same text/timestamp, different attempt
assert(messages.back().hold(receiver));
assert(receiver.held_dm_count == 15 && receiver.held_dms[14].ack_hash[0] == original_ack);
// Pending storage stays independent even with every normal slot occupied.
receiver.fill();
assert(receiver.offline_queue_len == 224 && receiver.held_dm_count == 15);
receiver.accepted = true;
receiver.releaseHeldOneKeyDMs();
assert(receiver.verified_pending_count == 0 && receiver.held_dm_count == 15);
assert(receiver.deliveries.empty() && receiver.acks.empty());
receiver.popOfflineQueue();
receiver.releaseHeldOneKeyDMs();
assert(receiver.held_dm_count == 14 && receiver.offline_queue_len == 224);
assert(receiver.deliveries.size() == 1 && receiver.deliveries[0].timestamp == 2);
receiver.drain();
receiver.releaseHeldOneKeyDMs();
assert(receiver.held_dm_count == 0 && receiver.deliveries.size() == 15);
assert(receiver.acks.size() == 15 && receiver.offline_queue_len == 14);
for (unsigned i = 0; i < 15; ++i) {
const Delivery& delivery = receiver.deliveries[i];
const Message& expected = messages[i + 1];
assert(delivery.timestamp == expected.timestamp && delivery.text == expected.text);
assert(delivery.route == expected.packet.header && delivery.snr == expected.packet._snr);
assert(delivery.path == (expected.packet.isRouteFlood() ? expected.packet.path_len : 0));
assert(receiver.acks[i] == expected.ack);
}
}
{
MyMesh receiver;
Message message(7, MAX_TEXT_LEN);
assert(message.hold(receiver));
receiver.fill();
receiver.accepted = true;
receiver.onMessageRecv(receiver.contact, &message.packet, message.timestamp, message.text.c_str());
assert(receiver.deliveries.empty() && receiver.held_dm_count == 1);
receiver.popOfflineQueue();
receiver.onMessageRecv(receiver.contact, &message.packet, message.timestamp, message.text.c_str());
assert(receiver.deliveries.size() == 1 && receiver.offline_queue_len == 224);
receiver.drain();
receiver.releaseHeldOneKeyDMs();
assert(receiver.held_dm_count == 0 && receiver.deliveries.size() == 1);
assert(receiver.acks.size() == 1 && receiver.acks[0] == message.ack);
receiver.onMessageRecv(receiver.contact, &message.packet, message.timestamp, message.text.c_str());
assert(receiver.deliveries.size() == 1); // already delivered retry stays deduplicated
}
{
MyMesh receiver;
Message message(33, MAX_TEXT_LEN);
mesh::Utils::authentic = false;
assert(!message.hold(receiver) && receiver.held_dm_count == 0);
mesh::Utils::authentic = true;
Message oversized(34, MAX_TEXT_LEN + 1);
assert(!oversized.hold(receiver) && receiver.held_dm_count == 0);
assert(!receiver.onAddressedTextPacket(&message.packet, 0x99,
message.plain.data(), message.plain.size()));
message.plain[4] = 4; // non-plain text must not enter the compact FIFO
assert(!message.hold(receiver) && receiver.held_dm_count == 0);
message.plain[4] = 1;
assert(message.hold(receiver));
receiver.fill();
for (unsigned i = 0; i < 96; ++i) receiver.popOfflineQueue();
auto* workspace = receiver.offline_queue.acquire(receiver.offline_queue_len,
receiver.offline_queue_head);
assert(workspace && receiver.getOfflineQueueCapacity() == 128);
memset(workspace->bytes, 0xCC, sizeof workspace->bytes);
assert(!strcmp(receiver.held_dms[0].text, message.text.c_str()));
receiver.offline_queue.release(receiver.offline_queue_head);
assert(receiver.getOfflineQueueCapacity() == 224);
receiver.drain();
receiver.accepted = true;
receiver.releaseHeldOneKeyDMs();
assert(receiver.deliveries.size() == 1 && receiver.deliveries[0].text == message.text);
assert(receiver.acks.size() == 1 && receiver.acks[0] == message.ack);
}
{
MyMesh receiver;
Message first(50), second(50, 24);
assert(first.hold(receiver) && second.hold(receiver));
assert(receiver.held_dm_count == 2); // equal timestamps do not merge different text
assert(first.hold(receiver) && receiver.held_dm_count == 2);
receiver.accepted = true;
receiver.releaseHeldOneKeyDMs();
assert(receiver.deliveries.size() == 2 && receiver.acks.size() == 2);
assert(receiver.deliveries[0].text == first.text && receiver.deliveries[1].text == second.text);
}
}
'''
harness = harness.replace("@HELD@", block(header, "struct HeldOneKeyDM {"))
harness = harness.replace("@DELIVERED@", block(header, "struct DeliveredOneKeyDM {"))
harness = harness.replace("@METHODS@", methods)
with tempfile.TemporaryDirectory(prefix="meshcore-compact-dm-") as temp:
binary = Path(temp) / "compact"
built = subprocess.run([
"c++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-g", "-I", str(ROOT / "src"),
"-x", "c++", "-", "-lcrypto", "-o", str(binary),
], input=harness, text=True, capture_output=True)
self.assertEqual(built.returncode, 0, built.stderr)
result = subprocess.run([str(binary)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()