Pack RAK Full help text at compile time without changing output

This commit is contained in:
mikecarper committed 2026-10-23 09:32:02 -07:00
1 parent 0f8cd1e019
commit 4deff5b687
3 files changed
+162 -5

No files matched your search

@@ -22,4 +22,117 @@ size_t writeHelpChunks(Output& output, const char* text, size_t length) {
return written;
}
namespace help_packing {
// Help is ASCII. Bytes 0x80..0x8f encode these common fragments; the original
// text exists only during constant evaluation, never alongside the packed data.
constexpr size_t TokenCount = 16;
constexpr char Dictionary[] =
" get/set \0" " get \0" " set \0" "get/set \0"
"bluetooth.\0" "pwrmgt.\0" "radio.\0" "default\0"
"preamble\0" "on|off\0" " (saved; \0" " \0"
"get \0" "set \0" "\r\n\0" " / \0";
struct TokenOffsets { uint8_t value[TokenCount + 1] = {}; };
constexpr TokenOffsets tokenOffsets() {
TokenOffsets result{};
size_t offset = 0;
for (size_t token = 0; token < TokenCount; ++token) {
result.value[token] = static_cast<uint8_t>(offset);
while (Dictionary[offset]) ++offset;
++offset;
}
result.value[TokenCount] = static_cast<uint8_t>(offset);
return result;
}
static_assert(sizeof(Dictionary) < 256, "Help dictionary offsets need one byte");
constexpr TokenOffsets Offsets = tokenOffsets();
constexpr size_t tokenLength(size_t token) {
return Offsets.value[token + 1] - Offsets.value[token] - 1;
}
constexpr size_t matchToken(const char* text, size_t remaining) {
for (size_t token = 0; token < TokenCount; ++token) {
const char* fragment = Dictionary + Offsets.value[token];
const size_t length = tokenLength(token);
if (length > remaining || *text != *fragment) continue;
size_t i = 1;
while (i < length && text[i] == fragment[i]) ++i;
if (i == length) return token;
}
return TokenCount;
}
} // namespace help_packing
template<size_t N>
constexpr bool validHelpText(const char (&text)[N]) {
if (text[N - 1] != 0) return false;
for (size_t i = 0; i + 1 < N; ++i) {
if (text[i] == 0 || static_cast<uint8_t>(text[i]) >= 0x80) return false;
}
return true;
}
template<size_t N>
constexpr size_t packedHelpSize(const char (&text)[N]) {
size_t packed = 0;
for (size_t i = 0; i + 1 < N; ++packed) {
const size_t token = help_packing::matchToken(text + i, N - i - 1);
i += token < help_packing::TokenCount ? help_packing::tokenLength(token) : 1;
}
return packed;
}
template<size_t N> struct PackedHelp {
static_assert(N > 0, "A help image must contain text");
uint8_t bytes[N] = {};
};
template<size_t PackedSize, size_t N>
constexpr PackedHelp<PackedSize> packHelp(const char (&text)[N]) {
PackedHelp<PackedSize> result{};
size_t packed = 0;
for (size_t i = 0; i + 1 < N;) {
const size_t token = help_packing::matchToken(text + i, N - i - 1);
if (token < help_packing::TokenCount) {
result.bytes[packed++] = static_cast<uint8_t>(0x80 + token);
i += help_packing::tokenLength(token);
} else {
result.bytes[packed++] = static_cast<uint8_t>(text[i++]);
}
}
return result;
}
template<typename Output>
size_t writePackedHelp(Output& output, const uint8_t* packed, size_t length) {
if (!packed) return 0;
size_t position = 0, written = 0;
const char* expansion = nullptr;
while (position < length || expansion) {
char chunk[64];
size_t used = 0;
while (used < sizeof(chunk) && (position < length || expansion)) {
if (expansion) {
const char next = *expansion++;
if (next) chunk[used++] = next;
else expansion = nullptr;
} else {
const uint8_t next = packed[position++];
if (next >= 0x80) {
const size_t token = next - 0x80;
if (token >= help_packing::TokenCount) return written;
expansion = help_packing::Dictionary + help_packing::Offsets.value[token];
} else {
chunk[used++] = static_cast<char>(next);
}
}
}
const size_t accepted = writeHelpChunks(output, chunk, used);
written += accepted;
if (accepted != used) break;
}
return written;
}
} } // namespace mesh::companion
+12 -4
View File
@@ -10405,8 +10405,10 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt,
} else if (strcmp(command, "help") == 0) {
auto& output = terminalOutput();
#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL)
// Keep every help byte and feature gate while sharing the printing code.
static const char help_text[] =
// Constant evaluation retains only the exact-sized packed help image.
// The source fragments and every feature gate remain shared below.
static constexpr auto packed_help = []() constexpr {
constexpr char help_text[] =
#define MESH_COMPANION_HELP_BEGIN
#define MESH_COMPANION_HELP_END
#define MESH_COMPANION_HELP_LINE_BEGIN
@@ -10416,8 +10418,14 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt,
#undef MESH_COMPANION_HELP_END
#undef MESH_COMPANION_HELP_LINE_BEGIN
#undef MESH_COMPANION_HELP_LINE_END
;
mesh::companion::writeHelpChunks(output, help_text, sizeof(help_text) - 1);
;
static_assert(mesh::companion::validHelpText(help_text),
"Packed help requires ASCII without embedded NUL bytes");
return mesh::companion::packHelp<
mesh::companion::packedHelpSize(help_text)>(help_text);
}();
mesh::companion::writePackedHelp(output, packed_help.bytes,
sizeof(packed_help.bytes));
#else
#define MESH_COMPANION_HELP_BEGIN output.print(
#define MESH_COMPANION_HELP_END );
+37 -1
View File
@@ -97,6 +97,39 @@ int main() {
Output empty;
assert(mesh::companion::writeHelpChunks(empty,nullptr,1)==0);
assert(mesh::companion::writeHelpChunks(empty,"",0)==0 && empty.writes.empty());
constexpr char literal[] =
" get/set radio.preamble on|off\r\n"
" get bluetooth.name / get pwrmgt.source\r\n"
" set radio.cad on (saved; default on)\r\n";
static_assert(mesh::companion::validHelpText(literal), "ASCII accepted");
static_assert(!mesh::companion::validHelpText("bad\x80"), "Token bytes reserved");
static_assert(!mesh::companion::validHelpText("a\0b"), "Embedded NUL rejected");
static constexpr auto packed = mesh::companion::packHelp<
mesh::companion::packedHelpSize(literal)>(literal);
static_assert(sizeof(packed) == mesh::companion::packedHelpSize(literal),
"No raw-size padding retained");
static_assert(sizeof(packed) + 40 < sizeof(literal), "Fragments compress");
Output decoded;
assert(mesh::companion::writePackedHelp(decoded,packed.bytes,sizeof(packed))==sizeof(literal)-1);
assert(decoded.text==literal);
Output slow;slow.throttle=3;
assert(mesh::companion::writePackedHelp(slow,packed.bytes,sizeof(packed))==sizeof(literal)-1);
assert(slow.text==literal && slow.writes.size()<=sizeof(literal)-1);
for (size_t credit : {size_t(0),size_t(1),size_t(13),size_t(63),size_t(64),size_t(65)}) {
Output blocked;blocked.credit=credit;
assert(mesh::companion::writePackedHelp(blocked,packed.bytes,sizeof(packed))==credit);
assert(blocked.text==std::string(literal,credit));
assert(blocked.writes.size()<=credit+1);
}
Output bad;bad.overreport=true;
assert(mesh::companion::writePackedHelp(bad,packed.bytes,sizeof(packed))==0);
assert(bad.writes.size()==1 && bad.text.empty());
Output unused;
assert(mesh::companion::writePackedHelp(unused,nullptr,1)==0);
assert(mesh::companion::writePackedHelp(unused,packed.bytes,0)==0 && unused.writes.empty());
const uint8_t invalid_token[]={0xff};
assert(mesh::companion::writePackedHelp(unused,invalid_token,1)==0 && unused.writes.empty());
}
'''
@@ -116,7 +149,7 @@ class CompanionHelpWriterTests(unittest.TestCase):
work = Path(folder);(work / 'test.cpp').write_text(harness)
sanitizer = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie'] if sys.platform.startswith('linux') else []
result = subprocess.run([compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror',
result = subprocess.run([compiler, '-std=c++17', '-Os', '-Wall', '-Wextra', '-Werror',
*sanitizer, *['-D'+value for value in defines],
'-I'+str(ROOT/'examples/companion_radio'), str(work/'test.cpp'),
'-o', str(work/'test')], capture_output=True, text=True)
@@ -126,6 +159,9 @@ class CompanionHelpWriterTests(unittest.TestCase):
self.assertEqual(hashlib.sha256(result.stdout).hexdigest(),GOLDEN[name]['text'])
if name != 'rak_full':
self.assertEqual(hashlib.sha256(result.stderr).hexdigest(),GOLDEN[name]['calls'])
else:
image = (work / 'test').read_bytes()
self.assertNotIn(b'Commands:\r\n stats-core / stats-radio / stats-radio-diag',image)
if __name__ == '__main__':