Reserve Full host KISS codec RAM without reducing existing budgets

This commit is contained in:
mikecarper committed 2026-10-26 17:53:39 -07:00
1 parent 1aef6de288
commit ec4aba8a1c
6 files changed
+119 -9

No files matched your search

+10
View File
@@ -77,6 +77,16 @@ Headless and OLED devices use their own smaller totals. The JSON lists each
component and checks the largest available region against the largest planned
single allocation.
Enabled Full host KISS images reserve a separate 2,048-byte
`host_kiss_codec` allocation on ESP32 and nRF52, including while KISS is saved
off. Turning the service on allocates its two host-TX and two encoded-output
frames plus parser state. The source assertion bounds the complete Codec and
allocator headroom to this allowance. WiFi/TCP retains its existing network
allowance; the independent transient margin and all normal-role capacities
remain additive. Explicit feature-off and ESP-NOW-only images omit this
allocation. A passing report proves linked capacity, not physical session or
maximum network-load qualification.
### MQTT startup minimum and runtime qualification
The `mqtt_startup_allowance` component keeps a 24,576-byte internal startup
+11 -1
View File
@@ -314,6 +314,16 @@ def requirements(platform, defines, target):
parts["neighbor_history"] = count * 9 + (16 if count else 0)
else:
parts["core_filesystems_sensors"] = 8192
if (platform in {"ESP32_PLATFORM", "NRF52_PLATFORM"}
and not integer(defines, "MESH_PRIMARY_ESPNOW", 0)
and integer(defines, "MESH_ENABLE_HOST_KISS",
integer(defines, "COMPANION_RADIO_FULL", 0))):
# Full raw KISS is lazy but can coexist with every normal-role owner.
# Its two RX/output frames, two host TX frames and framing state live
# in one Codec allocation; its source assertion retains allocator
# headroom within 2 KiB. Socket/WiFi objects retain their existing
# network allowance. This never replaces the transient margin.
parts["host_kiss_codec"] = 2048
# Packet bytes plus all three queue tables and allocation overhead.
parts["radio_packet_pool"] = 5120 if companion else 10240
# These tables moved out of .bss, so linker heap bounds now include their
@@ -448,7 +458,7 @@ def requirements(platform, defines, target):
"solarxiao_voltage_history",
"ota_context", "screen_objects_and_history", "ota_encoder_workspace",
"ota_generic_source_buffers", "ethernet_or_external_ota_workspaces", "ota_target_name_lookup",
"ethernet_companion_worker"):
"ethernet_companion_worker", "host_kiss_codec"):
largest = max(largest, parts.get(name, 0))
if "room_browser_mailbox" in parts:
largest = max(largest, 8192, parts["room_browser_mailbox"])
+5
View File
@@ -146,4 +146,9 @@ class Codec {
uint32_t droppedFrames() const { return dropped_; }
};
// Lazy Codec allocation is included in check_firmware_ram.py on every
// enabled Full image. Retain allocator metadata room if queues grow later.
static_assert(sizeof(Codec) + 32 <= 2048,
"Update the host KISS Codec runtime RAM allowance");
}} // namespace mesh::hostkiss
+9 -6
View File
@@ -1161,12 +1161,15 @@ int main() {
"BLE_PIN_CODE": 123456, "UI_SMALL_MESSAGE_FONT": 0}
with tempfile.TemporaryDirectory() as temp, contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
path = Path(temp) / "firmware.elf"
for available, accepted in ((54724, False), (73727, False), (73728, True), (74060, True)):
with self.subTest(available=available):
elf32(path, {"__HeapBase": (0x2003F800 - available, 0), "__HeapLimit": (0x2003F800, 0)})
result = ram.check_firmware(path, "NRF52_PLATFORM", "nrf52840", flags,
"Heltec_t096_companion_radio_full_femon")
self.assertEqual(result == 0, accepted)
for host_kiss, boundary in ((0, 73728), (1, 74530)):
selected = {**flags, "MESH_ENABLE_HOST_KISS": host_kiss}
for available, accepted in ((54724, False), (boundary - 1, False),
(boundary, True), (boundary + 332, True)):
with self.subTest(host_kiss=host_kiss, available=available):
elf32(path, {"__HeapBase": (0x2003F800 - available, 0), "__HeapLimit": (0x2003F800, 0)})
result = ram.check_firmware(path, "NRF52_PLATFORM", "nrf52840", selected,
"Heltec_t096_companion_radio_full_femon")
self.assertEqual(result == 0, accepted)
def test_arm_heap_excludes_stack_softdevice_and_mota_arena(self):
with tempfile.TemporaryDirectory() as temp:
+5 -2
View File
@@ -205,10 +205,13 @@ apply_policy() {
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
for enabled, cooperative, lose_owner in ((0, 0, False), (0, 1, False),
(1, 0, False), (1, 1, False),
(1, 1, True)):
(1, 1, True), (1, 1, "late")):
with self.subTest(enabled=enabled, cooperative=cooperative, lose_owner=lose_owner):
candidate = pump
if lose_owner:
if lose_owner == "late":
candidate = candidate.replace(" if (mesh::hostkiss::usbInputReserved(console.peek())) break;\n", "")
self.assertNotEqual(candidate, pump)
elif lose_owner:
candidate = candidate.replace("if (mesh::hostkiss::serviceUsb())",
"if (false && mesh::hostkiss::serviceUsb())")
self.assertNotEqual(candidate, pump)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Bind the optional KISS heap allowance to the real codec and ELF gate."""
import contextlib
import io
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/"scripts"))
import check_firmware_ram as ram
from test_firmware_ram import elf32,esp_fixture
class HostKissRamTest(unittest.TestCase):
def test_enabled_codec_adds_two_kib_without_lowering_other_reserves(self):
for platform in ("ESP32_PLATFORM","NRF52_PLATFORM"):
for target in ("board_companion_radio_full","board_repeater","board_room_server","board_sensor"):
with self.subTest(platform=platform,target=target):
flags={"BLE_PIN_CODE":123456}
off=ram.requirements(platform,{**flags,"MESH_ENABLE_HOST_KISS":0},target)
on=ram.requirements(platform,{**flags,"MESH_ENABLE_HOST_KISS":1},target)
self.assertEqual(on["components"]["host_kiss_codec"],2048)
self.assertEqual(on["required_heap_bytes"],off["required_heap_bytes"]+2048)
self.assertEqual({key:value for key,value in on["components"].items()
if key!="host_kiss_codec"},off["components"])
self.assertEqual(on["required_contiguous_bytes"],off["required_contiguous_bytes"])
default=ram.requirements(platform,{**flags,"COMPANION_RADIO_FULL":1},target)
self.assertEqual(default["components"],on["components"])
for extra in ({"COMPANION_RADIO_FULL":0},
{"COMPANION_RADIO_FULL":1,"MESH_ENABLE_HOST_KISS":0},
{"MESH_ENABLE_HOST_KISS":1,"MESH_PRIMARY_ESPNOW":1}):
self.assertEqual(ram.requirements(platform,{**flags,**extra},target)["components"],off["components"])
for platform in ("STM32_PLATFORM","RP2040_PLATFORM"):
self.assertEqual(ram.requirements(platform,{"MESH_ENABLE_HOST_KISS":1},"board_companion_radio_ble"),
ram.requirements(platform,{},"board_companion_radio_ble"))
def test_real_elf_rejects_unbudgeted_codec_and_honors_exact_new_boundary(self):
for platform in ("ESP32_PLATFORM","NRF52_PLATFORM"):
with self.subTest(platform=platform),tempfile.TemporaryDirectory() as folder:
folder=Path(folder);path=folder/"firmware.elf";report=folder/"memory.json"
target="board_companion_radio_full"
flags={"COMPANION_RADIO_FULL":1,"MESH_ENABLE_HOST_KISS":1,"BLE_PIN_CODE":123456}
required=ram.requirements(platform,flags,target)["required_heap_bytes"]
old=ram.requirements(platform,{**flags,"MESH_ENABLE_HOST_KISS":0},target)["required_heap_bytes"]
self.assertEqual(required,old+2048)
for available,accepted in ((old,False),(required-1,False),(required,True)):
with self.subTest(available=available),contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
if platform=="ESP32_PLATFORM":
esp_fixture(path,internal_length=available-0x10000+0x4000)
else:
elf32(path,{"__HeapBase":(0x20010000,0),"__HeapLimit":(0x20010000+available,0)})
status=ram.check_firmware(path,platform,"esp32" if platform=="ESP32_PLATFORM" else "nrf52840",flags,target,report)
actual=json.loads(report.read_text())
self.assertEqual(status==0,accepted)
self.assertEqual(actual["required_heap_bytes"],required)
self.assertEqual(actual["components"]["host_kiss_codec"],2048)
def test_actual_codec_size_is_source_bounded_and_queue_growth_fails(self):
source=(ROOT/"src/helpers/HostKissCodec.h").read_text()
with tempfile.TemporaryDirectory() as folder:
folder=Path(folder);header=folder/"Codec.h";main=folder/"main.cpp";binary=folder/"test"
main.write_text('#include "Codec.h"\n#include <stdio.h>\nint main(){printf("%zu\\n",sizeof(mesh::hostkiss::Codec));}\n')
for changed,accepted in ((source,True),(source.replace("QueueDepth = 2","QueueDepth = 3"),False)):
header.write_text(changed)
result=subprocess.run(["g++","-std=c++17",str(main),"-o",str(binary)],capture_output=True,text=True)
self.assertEqual(result.returncode==0,accepted,result.stdout+result.stderr)
if accepted:
size=int(subprocess.check_output([str(binary)]))
self.assertEqual(size,1828)
self.assertLessEqual(size+32,2048)
else:
self.assertIn("runtime RAM allowance",result.stderr)
if __name__=="__main__":unittest.main()