From 73789a64e849e25eb49cda9444e01426c01ae335 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 23 Oct 2026 11:25:33 -0700 Subject: [PATCH] Pack STM32WL Companion help while retaining every command --- .../stm32_companion_help_2026-10-23.md | 35 +++++ .../companion_radio/CompanionHelpPacked.cpp | 7 +- .../companion_radio/CompanionHelpWriter.h | 10 +- examples/companion_radio/MyMesh.cpp | 3 +- platformio.ini | 1 + scripts/nrf52_flash_trim.py | 2 +- scripts/stm32_companion_help.py | 39 ++++++ test/test_companion_help_build_policy.py | 23 +++- test/test_companion_help_writer.py | 122 ++++++++++++++---- 9 files changed, 211 insertions(+), 31 deletions(-) create mode 100644 docs/test-results/stm32_companion_help_2026-10-23.md create mode 100644 scripts/stm32_companion_help.py diff --git a/docs/test-results/stm32_companion_help_2026-10-23.md b/docs/test-results/stm32_companion_help_2026-10-23.md new file mode 100644 index 000000000..a861a6491 --- /dev/null +++ b/docs/test-results/stm32_companion_help_2026-10-23.md @@ -0,0 +1,35 @@ + + + + +# STM32WL Companion help packing, 23 October 2026 + +Small STM32WL USB Companions now use the existing packed help producer with a +256-byte history. Every command, option and terminal suffix is retained. The +same feature-gated `CompanionHelp.inc` supplies the packed and ordinary output. +nRF52 RAK Full keeps its 2,048-byte history; ESP32 retains its ordinary help +printing path. Contact/channel limits and the application/filesystem boundary +are unchanged. + +Only the selected `CompanionHelpPacked.cpp` is compiled as C++17. A source-role +policy registers that override for STM32WL Companion builds, excludes other +roles and reuses the existing per-object helper without installing nRF52 +trims. Other project sources and framework/library language standards remain +unchanged. The final producer also requires the STM32WL platform and text +terminal capability. + +Native tests compare the STM32 output with the preceding ordinary-printing +bytes, including both USB and network terminal suffixes. They exercise short, +zero and invalid writes, overlapping matches, ring wrap, malformed tokens and +a match farther back than the selected history. The STM32 profile's USB help +remains 1,908 bytes including its terminal suffix. Its SHA-256 is +`9e587ac9f287c8f8fe64fe5c899fe635b6de5d6cf436c4c43a27ffd4add42474`. +C++11 callers link and execute the actual C++17 producer under address and +undefined-behavior sanitizers for both STM32WL and RAK Full. + +The standalone Cortex-M4 object comparison saves 733 bytes including the +decoder and its data, before whole-firmware LTO effects. Emitted producer and +helper frames total 384 bytes; a 512-byte caller/output reserve keeps that +local estimate at 896 bytes within the existing 1,024-byte minimum stack +allowance. This is compiler frame accounting, excluding physical interrupt +high-water. The complete firmware's linked size is a separate build check. diff --git a/examples/companion_radio/CompanionHelpPacked.cpp b/examples/companion_radio/CompanionHelpPacked.cpp index eab3d381b..ee21aad31 100644 --- a/examples/companion_radio/CompanionHelpPacked.cpp +++ b/examples/companion_radio/CompanionHelpPacked.cpp @@ -1,9 +1,11 @@ -#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) -// This is the only Companion translation unit compiled as C++17. Include the +#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) \ + || (defined(STM32_PLATFORM) && defined(STM32WLxx)) +// This is the only selected Companion source compiled as C++17. Include the // same feature/board context as MyMesh before expanding the single help source. #include "MyMesh.h" #include "CompanionHelpWriter.h" +#if !defined(STM32_PLATFORM) || COMPANION_FEATURE_TEXT_TERMINAL namespace mesh { namespace companion { size_t writePackedCompanionHelp(Stream& output) { // Constant evaluation retains only the exact-sized packed help image. @@ -30,3 +32,4 @@ size_t writePackedCompanionHelp(Stream& output) { } } } // namespace mesh::companion #endif +#endif diff --git a/examples/companion_radio/CompanionHelpWriter.h b/examples/companion_radio/CompanionHelpWriter.h index dfac47e77..c62c5d4a9 100644 --- a/examples/companion_radio/CompanionHelpWriter.h +++ b/examples/companion_radio/CompanionHelpWriter.h @@ -7,7 +7,7 @@ class Stream; namespace mesh { namespace companion { -// The implementation is isolated in the RAK Full C++17 packing source. +// The implementation is isolated in one selected C++17 packing source. size_t writePackedCompanionHelp(Stream& output); // Native USB accepts bounded producer writes. Honor partial progress, and stop @@ -33,7 +33,13 @@ namespace help_packing { // ASCII literals retain bit 7 clear. Two-byte references encode a 3..18 byte // match at distance 1..2048. Only the final packed bytes survive evaluation; // the hash chains and original text occupy no runtime storage. +#if defined(STM32_PLATFORM) && defined(STM32WLxx) +// Small STM32WL Companions retain their 1 KiB minimum stack allowance. A +// smaller history preserves every help byte without adding a 2 KiB frame. +constexpr size_t WindowSize = 256; +#else constexpr size_t WindowSize = 2048; +#endif constexpr size_t MaximumMatch = 18; constexpr uint8_t hash(const char* text) { return static_cast((static_cast(text[0]) * 31u + @@ -134,7 +140,7 @@ size_t writePackedHelp(Output& output, const uint8_t* packed, size_t length) { if (position == length) return written; distance = ((static_cast(next & 7) << 8) | packed[position++]) + 1; remaining = ((next >> 3) & 15) + 3; - if (distance > produced) return written; + if (distance > produced || distance > help_packing::WindowSize) return written; continue; } } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a0c5cf82e..e710404f1 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -10404,7 +10404,8 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, FIRMWARE_VERSION, (unsigned)FIRMWARE_VER_CODE, FIRMWARE_BUILD_DATE); } else if (strcmp(command, "help") == 0) { auto& output = terminalOutput(); -#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) +#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) \ + || (defined(STM32_PLATFORM) && defined(STM32WLxx) && COMPANION_FEATURE_TEXT_TERMINAL) mesh::companion::writePackedCompanionHelp(output); #else #define MESH_COMPANION_HELP_BEGIN output.print( diff --git a/platformio.ini b/platformio.ini index 177a86a1b..55b856971 100644 --- a/platformio.ini +++ b/platformio.ini @@ -375,6 +375,7 @@ extends = arduino_base platform = ststm32@19.5.0 extra_scripts = post:arch/stm32/build_hex.py pre:scripts/client_acl_role.py + pre:scripts/stm32_companion_help.py pre:scripts/micronmea_time_fix.py pre:scripts/rtttl_rollover_fix.py post:scripts/check_firmware_ram.py diff --git a/scripts/nrf52_flash_trim.py b/scripts/nrf52_flash_trim.py index 9f84b6146..0f7ba0783 100644 --- a/scripts/nrf52_flash_trim.py +++ b/scripts/nrf52_flash_trim.py @@ -208,7 +208,7 @@ def compile_packed_help(build_env, node): if not str(value).startswith("-std=")] for name in ("CCFLAGS", "CXXFLAGS")} flags["CXXFLAGS"].append("-std=gnu++17") - print("nRF52 Companion help: isolated C++17 source") + print("Companion help: isolated C++17 source") # Builder overrides stay attached to this object even when PlatformIO # collects it through the parent construction environment afterward. return build_env.Object(node, **flags) diff --git a/scripts/stm32_companion_help.py b/scripts/stm32_companion_help.py new file mode 100644 index 000000000..50ab9920a --- /dev/null +++ b/scripts/stm32_companion_help.py @@ -0,0 +1,39 @@ +"""Compile only the selected small STM32WL Companion help producer as C++17. + +All source selection and object overrides are reused without installing any +nRF52 trim, changing a global language standard or changing feature defines. +The C++ producer itself checks the final platform and terminal capabilities. +""" + +import importlib.util +from pathlib import Path + + +SCRIPT_DIR = Path(__file__).resolve().parent if "__file__" in globals() else Path("scripts").resolve() +SPEC = importlib.util.spec_from_file_location("stm32_help_source_policy", SCRIPT_DIR / "nrf52_flash_trim.py") +POLICY = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(POLICY) +COMPANION_SOURCES = ("../examples/companion_radio/main.cpp", + "../examples/companion_radio/MyMesh.cpp") + + +def packed_help_policy(platform, mcu, src_filter): + if platform != "ststm32" or not str(mcu).lower().startswith("stm32wl"): + return False + return (all(POLICY.source_selected(src_filter, source) for source in COMPANION_SOURCES) + and not any(POLICY.source_selected(src_filter, source) + for source in POLICY.ROLE_SOURCES + POLICY.COMPANION_SOURCES[1:])) + + +def install(build_env): + source_filter = build_env.get("SRC_FILTER") + if source_filter is None: + source_filter = build_env.GetProjectOption("build_src_filter", "") + if packed_help_policy(build_env.PioPlatform().name, + build_env.BoardConfig().get("build.mcu", ""), source_filter): + build_env.AddBuildMiddleware(POLICY.compile_packed_help, "*CompanionHelpPacked.cpp") + + +if "Import" in globals(): + Import("env") + install(env) diff --git a/test/test_companion_help_build_policy.py b/test/test_companion_help_build_policy.py index e5ebd6c4c..33472319c 100644 --- a/test/test_companion_help_build_policy.py +++ b/test/test_companion_help_build_policy.py @@ -1,10 +1,11 @@ -"""The C++17 help encoder is one RAK Full source, never a global ABI change.""" +"""Compact Companions isolate one C++17 help source, never a global ABI change.""" from pathlib import Path import copy import importlib.util import unittest ROOT=Path(__file__).resolve().parents[1] spec=importlib.util.spec_from_file_location('help_language_policy',ROOT/'scripts/nrf52_flash_trim.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +spec=importlib.util.spec_from_file_location('stm32_help_language_policy',ROOT/'scripts/stm32_companion_help.py');stm=importlib.util.module_from_spec(spec);spec.loader.exec_module(stm) class HelpBuildPolicy(unittest.TestCase): def test_companion_source_registers_before_late_board_and_profile_flags(self): yes=dict(platform='nordicnrf52',mcu='nrf52840',src_filter='+<*> +<../examples/companion_radio>',flags='-DRAK_4631 -DCOMPANION_RADIO_FULL') @@ -38,4 +39,24 @@ class HelpBuildPolicy(unittest.TestCase): branch=mesh[mesh.index(' auto& output = terminalOutput();',mesh.index('} else if (strcmp(command, "help") == 0)')):] self.assertIn('mesh::companion::writePackedCompanionHelp(output);',branch) self.assertNotIn('constexpr',branch[:branch.index('#else')]) + def test_stm32_registration_is_only_the_small_companion_source_role(self): + selected='+<*> +<../examples/companion_radio/*.cpp>' + for mcu in ('stm32wle5jc','STM32WL55JC','stm32wle5cc'): + self.assertTrue(stm.packed_help_policy('ststm32',mcu,selected)) + for platform,mcu,source in (('nordicnrf52','nrf52840',selected), + ('espressif32','esp32s3',selected),('ststm32','stm32f405rg',selected), + ('ststm32','stm32wle5jc','+<*>'), + ('ststm32','stm32wle5jc',selected+' +<../examples/simple_repeater>'), + ('ststm32','stm32wle5jc',selected+' +<../examples/simple_secure_chat>')): + self.assertFalse(stm.packed_help_policy(platform,mcu,source)) + def test_stm32_installer_registers_one_source_without_installing_nrf_trims(self): + class Env(dict): + def PioPlatform(self):return type('Platform',(),{'name':'ststm32'})() + def BoardConfig(self):return {'build.mcu':'stm32wle5jc'} + def AddBuildMiddleware(self,callback,pattern):self.calls.append((callback,pattern)) + env=Env(SRC_FILTER='+<*> +<../examples/companion_radio/*.cpp>');env.calls=[] + before=copy.deepcopy(dict(env));stm.install(env) + self.assertEqual(dict(env),before) + self.assertEqual(env.calls,[(stm.POLICY.compile_packed_help,'*CompanionHelpPacked.cpp')]) + self.assertIn('pre:scripts/stm32_companion_help.py',(ROOT/'platformio.ini').read_text()) if __name__=='__main__':unittest.main() diff --git a/test/test_companion_help_writer.py b/test/test_companion_help_writer.py index 321c2b360..dd4d8ace1 100644 --- a/test/test_companion_help_writer.py +++ b/test/test_companion_help_writer.py @@ -16,6 +16,7 @@ FEATURES = ['COMPANION_FEATURE_READER=1', 'COMPANION_FEATURE_TEMP_RADIO=1', 'COMPANION_FEATURE_OTA_CLI=1', 'COMPANION_FEATURE_NOTIFICATIONS=1', 'MESH_ENABLE_LOST_REPLY=1', 'MESH_ENABLE_TX_BEACONS=1', 'MESH_ENABLE_MESHTEXT_CHAT=1', 'MESH_ENABLE_ETHERNET_TUNNEL=1'] +PACKED_PROFILES = {'rak_full', 'stm32_wl_usb'} PROFILES = { 'rak_full': ['NRF52_PLATFORM', 'RAK_4631', 'COMPANION_RADIO_FULL', 'EXTRAFS', 'DISPLAY_CLASS', 'OLED_AUTO_DETECT=1', 'BLE_PIN_CODE=123456', @@ -25,6 +26,8 @@ PROFILES = { 'OLED_AUTO_DETECT=1', 'BLE_PIN_CODE=123456', 'MESH_USB_LOGGING_AVAILABLE=1', 'MESH_USB_CONSOLE_COOPERATIVE=1', *FEATURES], 'nrf_minimal': ['NRF52_PLATFORM'], + 'stm32_wl_usb': ['STM32_PLATFORM', 'STM32WLxx', 'ENABLE_USB_INTERFACE', + 'COMPANION_FEATURE_TEXT_TERMINAL=1', 'MESH_RADIO_SWITCH_TUNING=0'], 'esp_full': ['ESP32_PLATFORM', 'ESP32', 'WIFI_SSID', 'COMPANION_RADIO_FULL', 'COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1', 'WITH_WEBCONFIG', 'MESH_USB_LOGGING_AVAILABLE=1', 'MESH_USB_CONSOLE_COOPERATIVE=1', @@ -35,6 +38,8 @@ PROFILES = { # suffix. Non-RAK call hashes bind print/println order as well as all text bytes. GOLDEN = {'rak_full': {'text': '9a68ce5a10905a201c3bddeb19f4819ce03c6dcaff248cff9c140803b70b14a7', 'calls': '56ed09dee7d53881b2c9fcf3f4ec9b6223e1562ce63e110a065e46706702b53b'}, 'nrf_full': {'text': 'b114ac12ae2ea01b7c47609b72f7eafb616c1dbeb7fb796a95b1377465fd9105', 'calls': '4973a7f21887d07b129cce4ae4cc8145f3f35e664bae09be658957210440d52e'}, 'nrf_minimal': {'text': '15fa8f290cd8c75cfde2fd2ac1cb5b5151a6755c68448fb2793dde3f27669033', 'calls': '6063fd1a699f9c0b588252bd8ce5922e0ff31cec6bbd24522789284ab2a37644'}, 'esp_full': {'text': '1e3b6781c81b5603f7092108e5e309543c60cef8ff4d7cb41a891f67396f716f', 'calls': 'b6335df2bd05b9cab44a7933469b76c5b2611c4b81057436a959feaf3bfbaf60'}} +GOLDEN['stm32_wl_usb'] = {'text': '9e587ac9f287c8f8fe64fe5c899fe635b6de5d6cf436c4c43a27ffd4add42474'} + FAST_HELP = (b' get radio.fast / get radio.fast.caps (supported saved controls)\r\n' b' set radio.fast / set radio.fast.spi <0|2|4|8|16>\r\n') @@ -82,13 +87,21 @@ void actualHelp(bool _terminal_mode) { } int main() { actualHelp(true); -#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) +#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) \ + || (defined(STM32_PLATFORM) && defined(STM32WLxx) && COMPANION_FEATURE_TEXT_TERMINAL) assert(!output.writes.empty()); #else assert(output.writes.empty()); #endif fwrite(output.text.data(),1,output.text.size(),stdout); fwrite(output.trace.data(),1,output.trace.size(),stderr); + const std::string usb_text=output.text; + const std::string usb_suffix=" +++MESHCORE-TERM-STOP\r\n"; + assert(usb_text.substr(usb_text.size()-usb_suffix.size())==usb_suffix); + output=Output{}; + actualHelp(false); + assert(output.text==usb_text.substr(0,usb_text.size()-usb_suffix.size()) + +" disconnect (network terminal)\r\n"); std::string text(777,'x'); Output partial;partial.throttle=7; assert(mesh::companion::writeHelpChunks(partial,text.data(),text.size())==text.size()); @@ -151,18 +164,26 @@ int main() { assert(mesh::companion::writePackedHelp(repeated,overlap_packed.bytes, sizeof(overlap_packed))==sizeof(overlap)-1 && repeated.text==overlap); - // Exercise both the 2048-byte distance limit and the ring wrap, independent - // of the encoder's choice of nearer repeated phrases. + // Exercise the selected distance limit and ring wrap on both families, + // independent of the encoder's choice of nearer repeated phrases. + constexpr size_t window=mesh::companion::help_packing::WindowSize; std::vector boundary; - for(size_t i=0;i<2048;++i) boundary.push_back('!'+(i%90)); + for(size_t i=0;i>8));boundary.push_back((window-1)&255); expected+=expected.substr(0,18); boundary.push_back(0xf8);boundary.push_back(0); // length18, distance1 expected+=std::string(18,expected.back()); Output wrapped;wrapped.throttle=5; assert(mesh::companion::writePackedHelp(wrapped,boundary.data(),boundary.size())==expected.size()); assert(wrapped.text==expected); + if (window<2048) { + std::vector too_far(window+64,'q'); + too_far.push_back(0x80 | (window>>8));too_far.push_back(window&255); + Output rejected; + assert(mesh::companion::writePackedHelp(rejected,too_far.data(),too_far.size())==window+64); + assert(rejected.text==std::string(window+64,'q')); + } for(const auto& corrupt : std::vector>{ {0x80}, {0x80,0}, {0xff,0xff}, {'x',0x80,1}}) { Output rejected; @@ -187,7 +208,7 @@ class CompanionHelpWriterTests(unittest.TestCase): source.index('} else if (strcmp(command, "help") == 0)')) end = source.index(' } else if (handleCommand(command, 0, local_reply))', start) implementation = body((ROOT / 'examples/companion_radio/CompanionHelpPacked.cpp').read_text(), 'size_t writePackedCompanionHelp(Stream& output)') - implementation = '#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL)\nnamespace mesh { namespace companion {\n' + implementation + '\n} }\n#endif\n' + implementation = '#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) || (defined(STM32_PLATFORM) && defined(STM32WLxx) && COMPANION_FEATURE_TEXT_TERMINAL)\nnamespace mesh { namespace companion {\n' + implementation + '\n} }\n#endif\n' harness = HARNESS.replace('@ACTUAL_HELP@', source[start:end]).replace('@PACKED_IMPLEMENTATION@', implementation) for name, defines in PROFILES.items(): with self.subTest(profile=name), tempfile.TemporaryDirectory(prefix='companion-help-') as folder: @@ -201,10 +222,10 @@ class CompanionHelpWriterTests(unittest.TestCase): self.assertEqual(result.returncode,0,result.stdout+result.stderr) result = subprocess.run([str(work/'test')],capture_output=True) self.assertEqual(result.returncode,0,result.stderr.decode(errors='replace')) - self.assertEqual(result.stdout.count(FAST_HELP), 1) + self.assertEqual(result.stdout.count(FAST_HELP), 0 if name == 'stm32_wl_usb' else 1) previous_help = result.stdout.replace(FAST_HELP, b'') self.assertEqual(hashlib.sha256(previous_help).hexdigest(),GOLDEN[name]['text']) - if name != 'rak_full': + if name not in PACKED_PROFILES: fast_call = b'P' + len(FAST_HELP).to_bytes(4, 'little') + FAST_HELP self.assertEqual(result.stderr.count(fast_call), 1) previous_calls = result.stderr.replace(fast_call, b'') @@ -222,23 +243,76 @@ class CompanionHelpWriterTests(unittest.TestCase): consumer=consumer[:consumer.index(' constexpr char literal[] =')]+"}\n" stream=HARNESS[HARNESS.index('class Stream {'):HARNESS.index('using Output=Stream;')] headers='#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n' - with tempfile.TemporaryDirectory(prefix='companion-help-language-') as folder: - work=Path(folder) - (work/'MyMesh.h').write_text(headers+stream) - (work/'CompanionHelpPacked.cpp').write_bytes((ROOT/'examples/companion_radio/CompanionHelpPacked.cpp').read_bytes()) - (work/'consumer.cpp').write_text(consumer) - compiler=os.environ.get('CXX','g++') - options=['-Os','-Wall','-Wextra','-Werror','-fsanitize=address,undefined','-fno-sanitize-recover=all','-fno-pie','-no-pie',*['-D'+v for v in PROFILES['rak_full']],'-I'+str(work),'-I'+str(ROOT/'examples/companion_radio')] - for name,standard in [('CompanionHelpPacked','c++17'),('consumer','c++11')]: - result=subprocess.run([compiler,'-std='+standard,*options,'-c',str(work/(name+'.cpp')),'-o',str(work/(name+'.o'))],capture_output=True,text=True) + for profile in ('rak_full', 'stm32_wl_usb'): + with tempfile.TemporaryDirectory(prefix='companion-help-language-') as folder: + work=Path(folder) + (work/'MyMesh.h').write_text(headers+stream) + (work/'CompanionHelpPacked.cpp').write_bytes((ROOT/'examples/companion_radio/CompanionHelpPacked.cpp').read_bytes()) + (work/'consumer.cpp').write_text(consumer) + compiler=os.environ.get('CXX','g++') + options=['-Os','-Wall','-Wextra','-Werror','-fsanitize=address,undefined','-fno-sanitize-recover=all','-fno-pie','-no-pie',*['-D'+v for v in PROFILES[profile]],'-I'+str(work),'-I'+str(ROOT/'examples/companion_radio')] + for name,standard in [('CompanionHelpPacked','c++17'),('consumer','c++11')]: + result=subprocess.run([compiler,'-std='+standard,*options,'-c',str(work/(name+'.cpp')),'-o',str(work/(name+'.o'))],capture_output=True,text=True) + self.assertEqual(result.returncode,0,result.stdout+result.stderr) + result=subprocess.run([compiler,*options,str(work/'consumer.o'),str(work/'CompanionHelpPacked.o'),'-o',str(work/'test')],capture_output=True,text=True) self.assertEqual(result.returncode,0,result.stdout+result.stderr) - result=subprocess.run([compiler,*options,str(work/'consumer.o'),str(work/'CompanionHelpPacked.o'),'-o',str(work/'test')],capture_output=True,text=True) - self.assertEqual(result.returncode,0,result.stdout+result.stderr) - result=subprocess.run([str(work/'test')],capture_output=True) - self.assertEqual(result.returncode,0,result.stderr.decode(errors='replace')) - self.assertEqual(result.stdout.count(FAST_HELP), 1) - self.assertEqual(hashlib.sha256(result.stdout.replace(FAST_HELP, b'')).hexdigest(),GOLDEN['rak_full']['text']) - self.assertNotIn(b'Commands:\r\n stats-core / stats-radio / stats-radio-diag',(work/'test').read_bytes()) + result=subprocess.run([str(work/'test')],capture_output=True) + self.assertEqual(result.returncode,0,result.stderr.decode(errors='replace')) + self.assertEqual(result.stdout.count(FAST_HELP), 0 if profile == 'stm32_wl_usb' else 1) + self.assertEqual(hashlib.sha256(result.stdout.replace(FAST_HELP, b'')).hexdigest(),GOLDEN[profile]['text']) + self.assertNotIn(b'Commands:\r\n stats-core / stats-radio / stats-radio-diag',(work/'test').read_bytes()) + + def test_small_stm32_arm_help_saves_flash_with_a_bounded_frame(self): + tool = shutil.which('arm-none-eabi-g++') + if not tool: + candidate = Path.home()/'.platformio/packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-g++' + if candidate.is_file(): tool = str(candidate) + if not tool: + self.skipTest('ARM compiler unavailable; linked image qualification remains required') + size_tool = str(Path(tool).with_name('arm-none-eabi-size')) + header = ('#include \n#include \n' + 'class Stream { public: virtual size_t write(const uint8_t*,size_t)=0;' + 'size_t print(const char*);size_t println(const char*);};\n') + raw = '''#include "MyMesh.h" +extern "C" void rawHelp(Stream& output) { +#define MESH_COMPANION_HELP_BEGIN output.print( +#define MESH_COMPANION_HELP_END ); +#define MESH_COMPANION_HELP_LINE_BEGIN output.println( +#define MESH_COMPANION_HELP_LINE_END ); +#include "CompanionHelp.inc" +} +''' + with tempfile.TemporaryDirectory(prefix='stm32-help-arm-') as folder: + work=Path(folder) + (work/'MyMesh.h').write_text(header) + (work/'packed.cpp').write_bytes((ROOT/'examples/companion_radio/CompanionHelpPacked.cpp').read_bytes()) + (work/'raw.cpp').write_text(raw) + flags=['-std=gnu++17','-mcpu=cortex-m4','-mthumb','-Oz', + '-fno-inline-small-functions','-fno-rtti','-fno-exceptions', + '-fmerge-all-constants','-fno-tree-dominator-opts','-fstack-usage', + *['-D'+v for v in PROFILES['stm32_wl_usb']], + '-I'+str(work),'-I'+str(ROOT/'examples/companion_radio')] + sizes={} + for name in ('raw','packed'): + result=subprocess.run([tool,*flags,'-c',str(work/(name+'.cpp')), + '-o',str(work/(name+'.o'))],capture_output=True,text=True) + self.assertEqual(result.returncode,0,result.stdout+result.stderr) + result=subprocess.run([size_tool,str(work/(name+'.o'))],capture_output=True,text=True) + self.assertEqual(result.returncode,0,result.stdout+result.stderr) + sizes[name]=int(result.stdout.splitlines()[-1].split()[0]) + self.assertGreaterEqual(sizes['raw']-sizes['packed'],700) + frames=[] + for line in (work/'packed.su').read_text().splitlines(): + function,count,kind=line.rsplit('\t',2) + self.assertEqual(kind,'static',line) + frames.append(int(count)) + # Overbound all emitted producer/helper frames. The reserve covers + # its caller and output sink here, not physical ISR high-water. + self.assertTrue(frames) + self.assertLessEqual(sum(frames),512) + self.assertLessEqual(sum(frames)+512,1024) + print('STM32WL help: object saving %d bytes; producer/helper stack %d + 512 reserve / 1024' + % (sizes['raw']-sizes['packed'],sum(frames))) if __name__ == '__main__': unittest.main()