Pack STM32WL Companion help while retaining every command

This commit is contained in:
mikecarper committed 2026-10-23 11:25:33 -07:00
1 parent 9d973d353a
commit 73789a64e8
9 files changed
+211 -31

No files matched your search

@@ -0,0 +1,35 @@
<!-- meshcore-hosted-doc-link:start -->
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/stm32_companion_help_2026-10-23/">View this page on MeshCore Docs</a>.</p>
<!-- meshcore-hosted-doc-link:end -->
# 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.
@@ -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
@@ -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<uint8_t>((static_cast<uint8_t>(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<size_t>(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;
}
}
+2 -1
View File
@@ -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(
+1
View File
@@ -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
+1 -1
View File
@@ -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)
+39
View File
@@ -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)
+22 -1
View File
@@ -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()
+98 -24
View File
@@ -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 <on|off> / 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<uint8_t> boundary;
for(size_t i=0;i<2048;++i) boundary.push_back('!'+(i%90));
for(size_t i=0;i<window;++i) boundary.push_back('!'+(i%90));
std::string expected(boundary.begin(),boundary.end());
boundary.push_back(0xff);boundary.push_back(0xff); // length18, distance2048
boundary.push_back(0xf8 | ((window-1)>>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<uint8_t> 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<std::vector<uint8_t>>{
{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 <cstdint>\n#include <algorithm>\n#include <cassert>\n#include <cstdio>\n#include <cstring>\n#include <string>\n#include <vector>\n#include <utility>\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 <stdint.h>\n#include <stddef.h>\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()