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 @@
+
+
View this page on MeshCore Docs.
+
+
+# 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()