Reduce nRF52 infrastructure flash usage without trimming sensors

Keep Companion and Terminal Chat profiles unchanged; retain full-sensor and reduced-sensor OTA builds. Add compact crypto and OTA names, qualified display asset compression, font license packaging, and scope regression tests.
This commit is contained in:
mikecarper committed 2026-10-02 15:52:23 -07:00
1 parent 010374d7db
commit 8a69e73226
44 files changed
+11150 -14

No files matched your search

+14
View File
@@ -260,6 +260,20 @@ jobs:
python3 -B test/test_nrf52_usb_power.py
python3 -B test/test_shared_mota_queue.py
- name: Verify nRF52 infrastructure trims and unchanged Companion scope
run: |
python3 -B test/test_nrf52_flash_trim.py -v
python3 -B test/test_nrf52_tft_font_trim.py -v
python3 -B test/test_ota_target_names.py -v
- name: Verify compact same-size fonts and lossless rendering
run: |
python3 -B test/test_compact_arial.py -v
python3 -B test/test_compact_noto.py -v
python3 -B test/test_nrf52_font_license.py -v
python3 -B test/test_oled_font_experiments.py -v
python3 -B test/test_font_family_trials.py -v
- name: Verify Companion Bluetooth settings and WebConfig
run: |
python3 -B test/test_nimble_companion.py
+2
View File
@@ -4304,6 +4304,8 @@ collect_nrf52_artifacts() {
if [ -f "${build_output_dir}/firmware.zip" ]; then
copy_build_output "${build_output_dir}/firmware.zip" "${OUTPUT_DIR}/${firmware_filename}.zip" || return $?
fi
python3 scripts/package_nrf52_font_license.py collect "$build_output_dir" \
--stem "${OUTPUT_DIR}/${firmware_filename}" || return $?
}
collect_stm32_artifacts() {
+101
View File
@@ -0,0 +1,101 @@
MeshFontProduction9 (MeshNotoCondensed bitmap-font derivative)
This notice applies only to the embedded bitmap-font data, not to the
firmware's other software components or their licenses.
The font contains 16/24-size bitmap assets, derived from Noto Sans Condensed
and stored losslessly with Google Zopfli. The derivative's primary name is
MeshFontProduction9; the upstream name below identifies its source only.
Source: Noto Sans Condensed
Version: 2.000;GOOG;noto-source:20170915:90ef993387c0; ttfautohint (v1.7)
Source SHA256: 4efc2789b9fcaa87b6b55f19c25ac9a2919e4f9ca31d6b5b97bea7977d729d4f
Upstream: https://github.com/notofonts/noto-fonts
SPDX-License-Identifier: OFL-1.1
Copyright 2015 Google Inc. All Rights Reserved.
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font Software,
subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+2
View File
@@ -160,6 +160,8 @@ extra_scripts = create-uf2.py
pre:scripts/nrf52_internal_bootloader_link.py
; Nordic's builder otherwise overwrites the LTO-aware archive tools.
pre:scripts/nrf52_lto_toolchain.py
; Infrastructure-only trims; all Companion/Terminal Chat profiles excluded.
pre:scripts/nrf52_flash_trim.py
post:scripts/check_firmware_ram.py
post:scripts/check_nrf52_ble_dfu_handoff.py
; Nordic's framework defaults to -Ofast. Prefer the size-oriented profile for
+9 -2
View File
@@ -6,6 +6,10 @@ import hashlib
import json
from pathlib import Path
import re
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
from package_nrf52_font_license import validate_artifact_notice
def digest(path):
@@ -37,15 +41,17 @@ def validate_build(directory):
def artifact_files(stem):
stem = Path(stem)
candidates = [stem.parent / (stem.name + suffix)
for suffix in (".bin", "-merged.bin", ".uf2", ".zip", ".hex", ".capabilities.json")]
for suffix in (".bin", "-merged.bin", ".uf2", ".zip", ".hex", ".capabilities.json",
".font-license.txt")]
return [p for p in candidates if p.is_file()]
def package_report(directory, stem):
stem = Path(stem)
report = validate_build(directory)
validate_artifact_notice(stem)
files = artifact_files(stem)
if len(files) < 2 or not any(p.suffix != ".json" for p in files):
if len(files) < 2 or not any(p.suffix in (".bin", ".uf2", ".zip", ".hex") for p in files):
raise ValueError("cannot qualify an empty firmware package")
manifest = json.loads((stem.parent / (stem.name + ".capabilities.json")).read_text())
report.update(target=manifest["target"], artifact_target=manifest["artifact_target"],
@@ -57,6 +63,7 @@ def package_report(directory, stem):
def validate_package(stem):
stem = Path(stem)
validate_artifact_notice(stem)
report = passing_report(stem.parent / (stem.name + ".memory.json"))
actual = {p.name: digest(p) for p in artifact_files(stem)}
if len(actual) < 2 or actual != report.get("files"):
+329
View File
@@ -0,0 +1,329 @@
"""Generate opt-in, lossless Arial font storage experiments.
The checked-in source font remains authoritative. Every one of its 224 slots,
including blank slots and their advance widths, survives these experiments.
Mode 2 uses per-glyph byte RLE; mode 3 independently inflates <=512-byte glyph
groups. Whole-font compression is measured only, not instantiated on the device.
Google Zopfli is used at 1,000 iterations; no compressor runs on the device.
"""
from __future__ import annotations
import argparse
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
import re
from zopfli_compress import MAXIMUM_ITERATIONS, raw_deflate_compress
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "src/helpers/ui/OLEDDisplayFonts.cpp"
OUTPUT = ROOT / "src/helpers/ui/CompactArialFonts.h"
CHUNK_LIMIT = 512
@dataclass(frozen=True)
class Glyph:
width: int
bitmap: bytes
@dataclass(frozen=True)
class Font:
name: str
original: bytes
glyphs: tuple[Glyph, ...]
@property
def header(self) -> bytes:
return self.original[:4]
def read_fonts(path: Path = SOURCE) -> tuple[Font, ...]:
source = path.read_text(encoding="utf-8")
fonts = []
for name, body in re.findall(
r"const uint8_t (ArialMT_Plain_\d+)\[\] PROGMEM = \{(.*?)\};", source, re.S
):
body = re.sub(r"//[^\n]*", "", body)
data = bytes(int(value, 16) for value in re.findall(r"0x([0-9a-fA-F]+)", body))
if len(data) < 4:
raise ValueError(f"{name}: incomplete header")
base = 4 + 4 * data[3]
if base > len(data) or data[2] + data[3] > 256:
raise ValueError(f"{name}: invalid character table")
glyphs = []
consumed = 0
for index in range(data[3]):
entry = data[4 + 4 * index:8 + 4 * index]
offset = int.from_bytes(entry[:2], "big")
length, width = entry[2:]
if offset == 0xffff:
if length:
raise ValueError(f"{name}: blank slot contains bitmap bytes")
bitmap = b""
else:
if not length or offset != consumed or base + offset + length > len(data):
raise ValueError(f"{name}: invalid or non-contiguous bitmap")
bitmap = data[base + offset:base + offset + length]
consumed += length
glyphs.append(Glyph(width, bitmap))
if base + consumed != len(data):
raise ValueError(f"{name}: trailing bitmap bytes")
fonts.append(Font(name, data, tuple(glyphs)))
if [font.name for font in fonts] != [f"ArialMT_Plain_{size}" for size in (10, 16, 24)]:
raise ValueError("Expected the original Arial 10, 16 and 24 fonts")
return tuple(fonts)
def rle_encode(data: bytes) -> bytes:
"""PackBits-style literals (1..128) and repeated bytes (3..130)."""
output = bytearray()
position = 0
while position < len(data):
run = 1
while position + run < len(data) and data[position + run] == data[position] and run < 130:
run += 1
if run >= 3:
output.extend((0x80 | (run - 3), data[position]))
position += run
continue
first = position
position += run
while position < len(data) and position - first < 128:
run = 1
while position + run < len(data) and data[position + run] == data[position] and run < 3:
run += 1
if run >= 3:
break
position += min(run, 128 - (position - first))
output.append(position - first - 1)
output.extend(data[first:position])
return bytes(output)
def rle_font(font: Font) -> tuple[bytes, tuple[tuple[int, int, int], ...]]:
output = bytearray()
records = []
for glyph in font.glyphs:
if not glyph.bitmap:
records.append((0xffff, 0, glyph.width))
continue
compressed = rle_encode(glyph.bitmap)
raw = len(compressed) >= len(glyph.bitmap)
location = len(output) | (0x8000 if raw else 0)
output.extend(glyph.bitmap if raw else compressed)
records.append((location, len(glyph.bitmap), glyph.width))
if len(output) >= 0x7fff:
raise ValueError("RLE offsets no longer fit the 15-bit experiment format")
return bytes(output), tuple(records)
@lru_cache(maxsize=None)
def chunk_font(font: Font) -> tuple[bytes, tuple[tuple[int, int, int], ...], tuple[int, ...], tuple[int, ...]]:
chunks = []
current = bytearray()
records = []
for glyph in font.glyphs:
if not glyph.bitmap:
records.append((0xffff, 0, glyph.width))
continue
if len(glyph.bitmap) > CHUNK_LIMIT:
raise ValueError("A glyph exceeds the bounded font scratch buffer")
if len(current) + len(glyph.bitmap) > CHUNK_LIMIT:
chunks.append(bytes(current))
current.clear()
records.append(((len(chunks) << 9) | len(current), len(glyph.bitmap), glyph.width))
current.extend(glyph.bitmap)
if current:
chunks.append(bytes(current))
if len(chunks) >= 128:
raise ValueError("Chunk identifiers exceed the packed 7-bit format")
output = bytearray()
offsets = [0]
for chunk in chunks:
output.extend(raw_deflate_compress(chunk, numiterations=MAXIMUM_ITERATIONS))
offsets.append(len(output))
if len(output) > 0xffff:
raise ValueError("Compressed font offsets exceed 16 bits")
return bytes(output), tuple(records), tuple(offsets), tuple(map(len, chunks))
@lru_cache(maxsize=None)
def whole_font(font: Font) -> bytes:
return raw_deflate_compress(font.original, numiterations=MAXIMUM_ITERATIONS)
def array(name: str, data, ctype: str = "uint8_t", columns: int = 16) -> str:
values = [f"0x{value:02x}" if ctype == "uint8_t" else str(value) for value in data]
lines = [f"static const {ctype} {name}[] = {{"]
for first in range(0, len(values), columns):
lines.append(" " + ", ".join(values[first:first + columns]) + ",")
lines.append("};")
return "\n".join(lines)
def descriptor(font: Font, rows) -> bytes:
output = bytearray(font.header)
for location, length, width in rows:
output.extend(location.to_bytes(2, "big"))
output.extend((length, width))
return bytes(output)
def generate_header(fonts: tuple[Font, ...]) -> str:
output = [HEADER_START]
for mode in (2, 3):
output.append(f"#if MESH_ARIAL_EXPERIMENT == {mode}")
for index, font in enumerate(fonts[1:], 1):
prefix = f"FONT_{index}"
# Inline function-local statics have one address across translation
# units. Namespace-static arrays would silently break identify().
output.append(f"inline const Font& fontStorage{index}() {{")
if mode == 2:
data, rows = rle_font(font)
offsets, lengths = (), ()
elif mode == 3:
data, rows, offsets, lengths = chunk_font(font)
output.append(array(prefix + "_DATA", data))
output.append(array(prefix + "_DESCRIPTOR", descriptor(font, rows)))
if offsets:
output.append(array(prefix + "_OFFSETS", offsets, "uint16_t"))
output.append(array(prefix + "_LENGTHS", lengths, "uint16_t"))
output.append(
f"static const Font {prefix} = {{ " + prefix + "_DESCRIPTOR, " + prefix + "_DATA, "
+ (prefix + "_OFFSETS, " + prefix + "_LENGTHS" if offsets else "nullptr, nullptr")
+ f", {len(lengths)}, {len(data)} }};"
)
output.append(f"return {prefix};\n}}")
output.append("#endif")
output.append(HEADER_END)
return "\n\n".join(output) + "\n"
HEADER_START = r'''// Generated by scripts/generate_compact_arial.py; do not edit.
// Experiments are LOSSLESS: same Arial pixels, all 224 slots and advance widths.
// 2 = glyph RLE (<=91 scratch bytes); 3 = Zopfli 1000 chunks (<=512 scratch).
// Only Arial16 and Arial24 are instantiated; the unused initial Arial10 is not.
#pragma once
#include <stddef.h>
#include <stdint.h>
#if MESH_ARIAL_EXPERIMENT == 3
#include "../ota/tinf/tinf.h"
#endif
#if MESH_ARIAL_EXPERIMENT != 2 && MESH_ARIAL_EXPERIMENT != 3
#error "CompactArialFonts.h is an opt-in experiment for modes 2 and 3 only"
#endif
namespace mesh { namespace ui { namespace arial {
static constexpr size_t kScratchSize = MESH_ARIAL_EXPERIMENT == 2 ? 91 : 512;
struct Font {
const uint8_t* descriptor;
const uint8_t* data;
const uint16_t* offsets;
const uint16_t* rawLengths;
uint16_t chunkCount;
uint16_t dataLength;
};
struct Glyph {
const uint8_t* data;
uint8_t width;
uint8_t length;
bool drawable;
bool valid;
};'''
HEADER_END = r'''inline const uint8_t* font(unsigned id) {
return id == 1 ? fontStorage1().descriptor : id == 2 ? fontStorage2().descriptor : nullptr;
}
inline int identify(const uint8_t* descriptor) {
return descriptor == fontStorage1().descriptor ? 1 : descriptor == fontStorage2().descriptor ? 2 : -1;
}
inline const Font* storage(const uint8_t* descriptor) {
const int id = identify(descriptor);
return id == 1 ? &fontStorage1() : id == 2 ? &fontStorage2() : nullptr;
}
// Writes only to caller scratch. No global cache, heap allocation, or mutable state.
inline bool decodeRle(const uint8_t* input, size_t inputLength,
uint8_t* output, size_t outputLength) {
if ((!input && inputLength) || (!output && outputLength)) return false;
size_t source = 0, dest = 0;
while (source < inputLength) {
const uint8_t token = input[source++];
const size_t count = token < 128 ? size_t(token) + 1 : size_t(token & 127) + 3;
if (count > outputLength - dest) return false;
if (token < 128) {
if (count > inputLength - source) return false;
for (size_t j = 0; j < count; ++j) output[dest++] = input[source++];
} else {
if (source == inputLength) return false;
const uint8_t value = input[source++];
for (size_t j = 0; j < count; ++j) output[dest++] = value;
}
}
return dest == outputLength;
}
inline Glyph glyph(const uint8_t* descriptor, uint8_t code, uint8_t* scratch, size_t capacity) {
const Font* f = storage(descriptor);
if (!f || code < descriptor[2] || unsigned(code - descriptor[2]) >= descriptor[3])
return { nullptr, 0, 0, false, false };
const unsigned slot = code - descriptor[2];
const uint8_t* record = descriptor + 4 + 4 * slot;
const unsigned location = (unsigned(record[0]) << 8) | record[1];
const uint8_t length = record[2], width = record[3];
if (!length) return { nullptr, width, 0, false, location == 0xffff };
#if MESH_ARIAL_EXPERIMENT == 2
const unsigned first = location & 0x7fff;
unsigned end = f->dataLength;
for (unsigned next = slot + 1; next < descriptor[3]; ++next) {
const uint8_t* nextRecord = descriptor + 4 + 4 * next;
if (nextRecord[2]) { end = ((unsigned(nextRecord[0]) << 8) | nextRecord[1]) & 0x7fff; break; }
}
if (first > end || end > f->dataLength) return { nullptr, width, 0, false, false };
if (location & 0x8000) {
if (end - first != length) return { nullptr, width, 0, false, false };
return { f->data + first, width, length, true, true };
}
if (!scratch || capacity < length || !decodeRle(f->data + first, end - first, scratch, length))
return { nullptr, width, 0, false, false };
return { scratch, width, length, true, true };
#elif MESH_ARIAL_EXPERIMENT == 3
const unsigned group = location >> 9;
const unsigned offset = location & 511;
if (group >= f->chunkCount) return { nullptr, width, 0, false, false };
const unsigned first = f->offsets[group], end = f->offsets[group + 1];
const unsigned expected = f->rawLengths[group];
if (!scratch || expected > kScratchSize || capacity < expected || offset + length > expected || first > end || end > f->dataLength)
return { nullptr, width, 0, false, false };
unsigned int produced = expected;
if (tinf_uncompress_exact(scratch, &produced, f->data + first, end - first) != TINF_OK || produced != expected)
return { nullptr, width, 0, false, false };
return { scratch + offset, width, length, true, true };
#endif
}
}}} // namespace mesh::ui::arial'''
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="Check the generated header without writing")
args = parser.parse_args()
fonts = read_fonts()
header = generate_header(fonts)
if args.check:
if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != header:
raise SystemExit("CompactArialFonts.h is stale; regenerate the font experiments")
else:
OUTPUT.write_text(header, encoding="utf-8", newline="\n")
print("font original RLE+table chunks+table wholefont chunks")
for font in fonts:
rle, _ = rle_font(font)
chunk, _, offsets, lengths = chunk_font(font)
print(f"{font.name:20} {len(font.original):5} {len(rle) + 4 * len(font.glyphs) + 4:9} "
f"{len(chunk) + 4 * len(font.glyphs) + 2 * (len(offsets) + len(lengths)) + 4:12} "
f"{len(whole_font(font)):9} {len(lengths):6}")
if __name__ == "__main__":
main()
+154
View File
@@ -0,0 +1,154 @@
"""Losslessly compress the selected Noto Condensed production bitmap fonts.
The checked-in, licensed mode7 assets in FontFamilyTrials.h are authoritative;
regeneration needs Google Zopfli but no Pillow, FreeType or installed font file.
Every glyph's pixels, advance width, blank slot and font metric are preserved.
Normal firmware builds use the checked-in header without running this script.
"""
from __future__ import annotations
import argparse
import hashlib
from pathlib import Path
import re
from generate_compact_arial import (
Font, Glyph, HEADER_END as ARIAL_DECODER, ROOT, array, chunk_font, descriptor,
)
SOURCE = ROOT / "src/helpers/ui/FontFamilyTrials.h"
OUTPUT = ROOT / "src/helpers/ui/CompactNotoFonts.h"
def read_fonts(path: Path = SOURCE) -> tuple[tuple[Font, ...], str]:
source = path.read_text(encoding="utf-8")
match = re.search(r"#if MESH_ARIAL_EXPERIMENT == 7\s*(.*?)\n#endif", source, re.S)
if not match:
raise ValueError("Authoritative raw Noto Condensed mode7 assets are missing")
body = match[1]
fonts = []
for id, encoded in re.findall(r"static const uint8_t TRIAL_([12])\[\] = \{(.*?)\};", body, re.S):
data = bytes(int(value, 16) for value in re.findall(r"0x([0-9a-f]{2})", encoded))
index = int(id)
expected_header = bytes((16, 19, 32, 224)) if index == 1 else bytes((24, 28, 32, 224))
if data[:4] != expected_header:
raise ValueError("Production Noto dimensions/character coverage changed")
base = 4 + 4 * data[3]
if len(data) < base:
raise ValueError("Incomplete raw Noto character table")
glyphs = []
consumed = 0
for slot in range(data[3]):
record = data[4 + 4 * slot:8 + 4 * slot]
offset = int.from_bytes(record[:2], "big")
length, width = record[2:]
if offset == 0xffff:
if length:
raise ValueError("A blank Noto slot contains bitmap bytes")
bitmap = b""
else:
if not length or offset != consumed or base + offset + length > len(data):
raise ValueError("Invalid or non-contiguous raw Noto glyph")
bitmap = data[base + offset:base + offset + length]
consumed += length
glyphs.append(Glyph(width, bitmap))
if base + consumed != len(data) or sum(bool(glyph.bitmap) for glyph in glyphs) != 189:
raise ValueError("Raw Noto glyph coverage/trailing bytes changed")
fonts.append(Font(f"MeshFontProduction9_{16 if index == 1 else 24}", data, tuple(glyphs)))
if len(fonts) != 2 or fonts[0].header[0] != 16 or fonts[1].header[0] != 24:
raise ValueError("Expected ordered Noto 16/24 assets")
license = re.search(r"/\*(.*?)\*/", body, re.S)
if not license or "SIL OPEN FONT LICENSE Version 1.1" not in license[1]:
raise ValueError("Required source Noto copyright/OFL notice is missing")
return tuple(fonts), license[1].strip()
def chunk_decoder() -> str:
"""Reuse the tested Arial codec's exact C++ DEFLATE branch and pointer API.
Extract at generation time, not with firmware macros: production contains
neither the RLE decoder nor an experimental-mode dependency. Tests compile
the result in isolation and check every authoritative raw Noto glyph.
"""
api = ARIAL_DECODER[:ARIAL_DECODER.index("// Writes only to caller scratch.")]
start = ARIAL_DECODER.index("inline Glyph glyph(")
common = ARIAL_DECODER[start:ARIAL_DECODER.index("#if MESH_ARIAL_EXPERIMENT == 2", start)]
deflate = ARIAL_DECODER.split("#elif MESH_ARIAL_EXPERIMENT == 3\n", 1)[1].split("#endif\n", 1)[0]
return (api + "// Caller-owned 512-byte scratch; no cache, heap or mutable global state.\n"
+ common + deflate + "}\n}}} // namespace mesh::ui::noto")
HEADER_START = '''// Generated by scripts/generate_compact_noto.py; do not edit.
// MeshFontProduction9: lossless storage of the licensed MeshFontTrial7 bitmaps.
// All 224 slots/189 drawable glyphs preserved at 12/17 px H cap height;
// H tops 3/5 px, line boxes 19/28 px. The unused initial Arial10 is not stored.
// Google Zopfli 1000; glyph-aligned chunks decode to at most 512 bytes.
// Source rasterizer: Pillow 12.3.0 / FreeType 2.14.3, threshold 128.
// Nominal source sizes 16/24 px, baselines 15/22 px; existing mode7 placement.
// Normal builds need no compressor, rasterizer or source TrueType font.
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "../ota/tinf/tinf.h"
namespace mesh { namespace ui { namespace noto {
static constexpr size_t kScratchSize = 512;
struct Font {
const uint8_t* descriptor;
const uint8_t* data;
const uint16_t* offsets;
const uint16_t* rawLengths;
uint16_t chunkCount;
uint16_t dataLength;
};
struct Glyph {
const uint8_t* data;
uint8_t width;
uint8_t length;
bool drawable;
bool valid;
};'''
def generate_header(fonts: tuple[Font, ...], license: str) -> str:
output = [HEADER_START, "/*\nCompression derivative named MeshFontProduction9.\n"
"The following notice identifies the upstream rasterized font source.\n"
+ "\n".join(line.rstrip() for line in license.splitlines()) + "\n*/"]
for id, font in enumerate(fonts, 1):
packed, rows, offsets, lengths = chunk_font(font)
prefix = f"FONT_{id}"
output.append(f"// Authoritative raw bitmap SHA256: {hashlib.sha256(font.original).hexdigest()}")
output.append(f"inline const Font& fontStorage{id}() {{")
output.append(array(prefix + "_DATA", packed))
output.append(array(prefix + "_DESCRIPTOR", descriptor(font, rows)))
output.append(array(prefix + "_OFFSETS", offsets, "uint16_t"))
output.append(array(prefix + "_LENGTHS", lengths, "uint16_t"))
output.append(f"static const Font {prefix} = {{ {prefix}_DESCRIPTOR, {prefix}_DATA, "
f"{prefix}_OFFSETS, {prefix}_LENGTHS, {len(lengths)}, {len(packed)} }};")
output.append(f"return {prefix};\n}}")
output.append(chunk_decoder())
return "\n\n".join(output) + "\n"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="Verify the generated header without writing")
args = parser.parse_args()
fonts, license = read_fonts()
header = generate_header(fonts, license)
if args.check:
if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != header:
raise SystemExit("CompactNotoFonts.h is stale; regenerate the production font assets")
else:
OUTPUT.write_text(header, encoding="utf-8", newline="\n")
total = 0
for font in fonts:
packed, records, offsets, lengths = chunk_font(font)
size = len(packed) + len(descriptor(font, records)) + 2 * (len(offsets) + len(lengths))
total += size
print(f"{font.name}: raw={len(font.original)}, compressed+metadata={size}, chunks={len(lengths)}")
print(f"Combined storage={total} bytes (runtime accessor code excluded)")
if __name__ == "__main__":
main()
+279
View File
@@ -0,0 +1,279 @@
"""Generate optional sans-serif family trials at the existing Arial ink sizes.
This is not part of normal firmware builds: checked-in bitmap arrays need no
Pillow or TrueType fonts on the build host. Regeneration requires Pillow 12.3.0
with FreeType 2.14.3 and the SHA-pinned upstream font files below. The derived
font names are MeshFontTrial4..8, not their sources' reserved font names.
"""
from __future__ import annotations
import argparse
from dataclasses import dataclass
import hashlib
import math
from pathlib import Path
import struct
from generate_compact_arial import Font, Glyph, ROOT, array, chunk_font, read_fonts, whole_font
OUTPUT = ROOT / "src/helpers/ui/FontFamilyTrials.h"
PILLOW_VERSION = "12.3.0"
FREETYPE_VERSION = "2.14.3"
@dataclass(frozen=True)
class Family:
mode: int
source: str
filename: str
sha256: str
sizes: tuple[int, int]
spdx: str
upstream: str
FAMILIES = (
Family(4, "Liberation Sans", "LiberationSans-Regular.ttf",
"597589a852868f782c68e25112bab7785b34bc726f6160dca1519198c6e8c762",
(16, 24), "OFL-1.1", "https://github.com/liberationfonts/liberation-fonts"),
Family(5, "Noto Sans", "NotoSans-Regular.ttf",
"6b04c8dd65af6b73eb4279472ed1580b29102d6496a377340e80a40cdb3b22c9",
(16, 24), "OFL-1.1", "https://github.com/notofonts/noto-fonts"),
Family(6, "DejaVu Sans", "DejaVuSans.ttf",
"7da195a74c55bef988d0d48f9508bd5d849425c1770dba5d7bfc6ce9ed848954",
(16, 23), "Bitstream-Vera AND LicenseRef-Arev", "https://github.com/dejavu-fonts/dejavu-fonts"),
Family(7, "Noto Sans Condensed", "NotoSans-Condensed.ttf",
"4efc2789b9fcaa87b6b55f19c25ac9a2919e4f9ca31d6b5b97bea7977d729d4f",
(16, 24), "OFL-1.1", "https://github.com/notofonts/noto-fonts"),
Family(8, "DejaVu Sans Condensed", "DejaVuSansCondensed.ttf",
"8550cd5ca1acb65a8fc7877c46939cd0b4d909f8e7bc1e24716873d918c5e549",
(16, 23), "Bitstream-Vera AND LicenseRef-Arev", "https://github.com/dejavu-fonts/dejavu-fonts"),
)
def sfnt_tables(data: bytes) -> dict[bytes, tuple[int, int]]:
count = struct.unpack_from(">H", data, 4)[0]
result = {}
for index in range(count):
tag, _checksum, offset, length = struct.unpack_from(">4sIII", data, 12 + 16 * index)
if offset + length > len(data):
raise ValueError("Truncated TrueType table")
result[tag] = offset, length
return result
def name_records(data: bytes) -> dict[int, str]:
offset, _length = sfnt_tables(data)[b"name"]
_format, count, strings = struct.unpack_from(">HHH", data, offset)
records = {}
for index in range(count):
platform, _encoding, _language, name, length, relative = struct.unpack_from(
">HHHHHH", data, offset + 6 + 12 * index)
raw = data[offset + strings + relative:offset + strings + relative + length]
if platform in (0, 3):
records[name] = raw.decode("utf-16-be")
elif platform == 1 and name not in records:
records[name] = raw.decode("mac_roman")
return records
def unicode_coverage(data: bytes) -> set[int]:
"""Read supported cmap4/cmap12 codepoints; never silently use .notdef."""
offset, _length = sfnt_tables(data)[b"cmap"]
_version, count = struct.unpack_from(">HH", data, offset)
coverage = set()
for index in range(count):
platform, encoding, relative = struct.unpack_from(">HHI", data, offset + 4 + 8 * index)
if platform != 0 and not (platform == 3 and encoding in (1, 10)):
continue
table = offset + relative
format = struct.unpack_from(">H", data, table)[0]
if format == 12:
groups = struct.unpack_from(">I", data, table + 12)[0]
for group in range(groups):
start, end, first = struct.unpack_from(">III", data, table + 16 + 12 * group)
coverage.update(code for code in range(start, min(end, 255) + 1) if first + code - start)
elif format == 4:
segments = struct.unpack_from(">H", data, table + 6)[0] // 2
end_base = table + 14
start_base = end_base + 2 * segments + 2
delta_base = start_base + 2 * segments
range_base = delta_base + 2 * segments
for segment in range(segments):
start = struct.unpack_from(">H", data, start_base + 2 * segment)[0]
end = struct.unpack_from(">H", data, end_base + 2 * segment)[0]
delta = struct.unpack_from(">H", data, delta_base + 2 * segment)[0]
relative_glyph = struct.unpack_from(">H", data, range_base + 2 * segment)[0]
for code in range(start, min(end, 255) + 1):
if relative_glyph:
glyph_offset = range_base + 2 * segment + relative_glyph + 2 * (code - start)
glyph = struct.unpack_from(">H", data, glyph_offset)[0]
glyph = ((glyph + delta) & 0xffff) if glyph else 0
else:
glyph = (code + delta) & 0xffff
if glyph:
coverage.add(code)
return coverage
def make_font(name: str, header: bytes, glyphs: list[Glyph]) -> Font:
table = bytearray(header)
bitmap = bytearray()
for glyph in glyphs:
if len(glyph.bitmap) > 255 or glyph.width > 255:
raise ValueError("Rasterized glyph exceeds ThingPulse byte metadata")
table.extend((len(bitmap) if glyph.bitmap else 0xffff).to_bytes(2, "big"))
table.extend((len(glyph.bitmap), glyph.width))
bitmap.extend(glyph.bitmap)
return Font(name, bytes(table + bitmap), tuple(glyphs))
def rasterize(family: Family, source: Path, baseline_fonts: tuple[Font, ...]):
from PIL import Image, ImageDraw, ImageFont, features
if Image.__version__ != PILLOW_VERSION or features.version("freetype2") != FREETYPE_VERSION:
raise RuntimeError(f"Require Pillow{PILLOW_VERSION}/FreeType{FREETYPE_VERSION} for deterministic font trials")
data = source.read_bytes()
if hashlib.sha256(data).hexdigest() != family.sha256:
raise ValueError(f"{source.name}: source font hash differs from the pinned trial")
names = name_records(data)
coverage = unicode_coverage(data)
fonts = []
metrics = []
for id, pixel_size in enumerate(family.sizes, 1):
base = baseline_fonts[id]
baseline = 15 if id == 1 else 22
font = ImageFont.truetype(str(source), pixel_size, layout_engine=ImageFont.Layout.BASIC)
glyphs = []
shifted_x, shifted_y = [], []
for slot, original in enumerate(base.glyphs):
code = base.header[2] + slot
character = chr(code)
advance = int(math.floor(font.getlength(character) + 0.5)) if code in coverage else original.width
if not original.bitmap:
# Control slots stay blank, not .notdef boxes. Spaces use the
# candidate font's own spacing; other blanks retain zero width.
glyphs.append(Glyph(advance if code in (32, 160) else original.width, b""))
continue
if code not in coverage:
raise ValueError(f"{family.source}: U+{code:04X} missing from Unicode cmap")
image = Image.new("L", (128, 128), 0)
ImageDraw.Draw(image).text((48, 48), character, font=font, fill=255, anchor="ls")
image = image.point(lambda value: 255 if value >= 128 else 0)
bbox = image.getbbox()
if bbox is None:
raise ValueError(f"{family.source}: drawable U+{code:04X} rasterized blank")
left, top, right, bottom = (value - 48 for value in bbox)
if bottom - top > base.header[1]:
raise ValueError(f"{family.source}: U+{code:04X} is too tall for the unchanged line box")
x_shift = max(0, -left)
y_shift = max(0, -(baseline + top))
y_shift -= max(0, baseline + bottom + y_shift - base.header[1])
width = max(advance + x_shift, right + x_shift)
if baseline + top + y_shift < 0 or baseline + bottom + y_shift > base.header[1]:
raise ValueError("Glyph placement would clip pixels")
if x_shift:
shifted_x.append(code)
if y_shift:
shifted_y.append((code, y_shift))
raster_height = (base.header[1] + 7) // 8
bitmap = bytearray(width * raster_height)
for y in range(bbox[1], bbox[3]):
for x in range(bbox[0], bbox[2]):
if image.getpixel((x, y)):
dest_x = x - 48 + x_shift
dest_y = y - 48 + baseline + y_shift
bitmap[dest_x * raster_height + dest_y // 8] |= 1 << (dest_y & 7)
while bitmap and not bitmap[-1]:
bitmap.pop()
glyphs.append(Glyph(width, bytes(bitmap)))
made = make_font(f"MeshFontTrial{family.mode}_{16 if id == 1 else 24}", base.header, glyphs)
top, bottom = ink_y_bounds(made, ord("H"))
if (top, bottom) != ((3, 14) if id == 1 else (5, 21)):
raise ValueError("The H baseline/top differs from the original Arial placement")
fonts.append(made)
metrics.append({"nominal": pixel_size, "cap": bottom - top + 1, "H_top": top,
"baseline": baseline, "line_box": base.header[1],
"bearing_shifts": shifted_x, "vertical_shifts": shifted_y})
return tuple(fonts), metrics, names
def ink_y_bounds(font: Font, code: int) -> tuple[int, int]:
glyph = font.glyphs[code - font.header[2]]
stride = (font.header[1] + 7) // 8
rows = [8 * (index % stride) + bit for index, value in enumerate(glyph.bitmap)
for bit in range(8) if value & (1 << bit)]
return min(rows), max(rows)
def license_notice(family: Family, names: dict[int, str]) -> str:
if family.mode in (4, 5, 7):
text = (ROOT / "tools/sensecap_indicator_font/OFL-1.1.txt").read_text(encoding="utf-8")
text = text[text.index("SIL OPEN FONT LICENSE Version 1.1"):]
notice = names[0] + "\n"
if family.mode == 4:
notice += "Reserved Font Names: Arimo, Tinos, Cousine, Liberation.\n"
return notice + "\n" + text
return names[0] + "\n\n" + names[13]
def generate_header(trials) -> str:
output = ["// Generated by scripts/generate_font_family_trials.py; do not edit.\n"
"// Opt-in derived fonts named MeshFontTrial4..8. Same visible cap sizes\n"
"// (12/17 px) and line boxes (19/28 px) as Arial16/24; all 189 drawable\n"
"// Latin-1 slots preserved. These are appearance trials, not defaults.\n"
"// Rasterizer: Pillow 12.3.0, FreeType 2.14.3; grayscale threshold 128.\n"
"#pragma once\n#include <stdint.h>\n"
"#if MESH_ARIAL_EXPERIMENT < 4 || MESH_ARIAL_EXPERIMENT > 8\n"
'#error "FontFamilyTrials.h is only for font experiment modes 4 through 8"\n#endif\n'
"namespace mesh { namespace ui { namespace font_trials {"]
for family, fonts, metrics, names in trials:
output.append(f"#if MESH_ARIAL_EXPERIMENT == {family.mode}")
comment = (f"Derived MeshFontTrial{family.mode}; source {family.source}, {names[5]}\n"
f"Source SHA256: {family.sha256}\nUpstream: {family.upstream}\n"
f"SPDX-License-Identifier: {family.spdx}\n"
"Full license/copyright follows; it applies to the bitmap font data.\n"
+ license_notice(family, names))
# Font metadata/license text may carry trailing spaces. Normalize only
# whitespace, never the required copyright/license wording.
comment = "\n".join(line.rstrip() for line in comment.splitlines())
output.append("/*\n" + comment.replace("*/", "* /") + "\n*/")
for id, (font, metric) in enumerate(zip(fonts, metrics), 1):
output.append("// " + "; ".join(f"{key}={value}" for key, value in metric.items()))
output.append(f"inline const uint8_t* fontStorage{id}() {{")
output.append(array(f"TRIAL_{id}", font.original))
output.append(f"return TRIAL_{id};\n}}")
output.append("#endif")
output.append("inline const uint8_t* font(unsigned id) {\n"
" return id == 1 ? fontStorage1() : id == 2 ? fontStorage2() : nullptr;\n"
"}\n}}} // namespace mesh::ui::font_trials")
return "\n\n".join(output) + "\n"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--font-dir", type=Path, default=Path("C:/Windows/Fonts"))
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
baseline_fonts = read_fonts()
trials = []
for family in FAMILIES:
fonts, metrics, names = rasterize(family, args.font_dir / family.filename, baseline_fonts)
trials.append((family, fonts, metrics, names))
if not args.check:
print(f"mode {family.mode} {family.source}: raw={sum(len(font.original) for font in fonts)} "
f"whole-font Zopfli={sum(len(whole_font(font)) for font in fonts)}")
for font, metric in zip(fonts, metrics):
chunk, records, offsets, lengths = chunk_font(font)
compressed_size = len(chunk) + 4 + 4 * len(records) + 2 * (len(offsets) + len(lengths))
print(f" {font.name}: raw={len(font.original)} chunked Zopfli={compressed_size} {metric}")
header = generate_header(trials)
if args.check:
if OUTPUT.read_text(encoding="utf-8") != header:
raise SystemExit("FontFamilyTrials.h is stale")
else:
OUTPUT.write_text(header, encoding="utf-8", newline="\n")
if __name__ == "__main__":
main()
+190
View File
@@ -0,0 +1,190 @@
"""Feature-preserving flash trim for nRF52 infrastructure, never Companions.
Decide from the effective source filter as well as the environment/OTA alias
and flags: build.sh can synthesize profiles over another PlatformIO recipe.
Unknown roles fail closed. No shared framework or downloaded library is edited.
CDC, USB host, sensor/GPS support, BLE P-256, and stored Ed25519 keys remain.
The compact Ed25519 implementation trades more signing time/stack for flash;
the nRF52 base keeps an 8 KiB loop stack for this and the existing OTA path.
"""
import fnmatch
import hashlib
import importlib.util
import re
import shlex
from pathlib import Path
ROLE_SOURCES = (
"../examples/simple_repeater/main.cpp",
"../examples/simple_room_server/main.cpp",
"../examples/simple_sensor/main.cpp",
"../examples/kiss_modem/main.cpp",
)
COMPANION_SOURCES = (
"../examples/companion_radio/MyMesh.cpp",
"../examples/simple_secure_chat/main.cpp",
)
TRIM_DEFINES = {
"MESH_NRF52_FLASH_TRIM": 1,
"ED25519_COMPACT_BASE": 1,
"ED25519_COMPACT_SHA512": 1,
# No infrastructure role instantiates these USB *device* classes. Do not
# touch CDC (including dual CDC logging), DFU, or the MAX3421 USB host.
"CFG_TUD_MSC": 0,
"CFG_TUD_HID": 0,
"CFG_TUD_MIDI": 0,
"CFG_TUD_VENDOR": 0,
"CFG_TUD_VIDEO": 0,
"CFG_TUD_VIDEO_STREAMING": 0,
# MeshCore uses UBX navigation, not SparkFun's automatic NMEA cache. This
# does not disable navigation, the UBX parser, or MeshCore NMEA logging.
"SFE_UBLOX_DISABLE_AUTO_NMEA": 1,
"SFE_UBLOX_REDUCED_PROG_MEM": 1,
# SSD1306Display clears the vendor splash before its first panel transfer.
# Keep MeshCore's own startup logo; only omit the invisible library bitmap.
"SSD1306_NO_SPLASH": 1,
}
CC310_ARCHIVE_SHA256 = "97dc648d44520e47252f62c402a71f976e36d7b3aef99235bbdc4de79a928577"
CC310_ARCHIVE = "src/cortex-m4/fpv4-sp-d16-hard/libnrf_cc310_0.9.13-no-interrupts.a"
def flag_defines(flags=(), cppdefines=()):
"""Read effective defines without importing PlatformIO/SCons for tests."""
values = {}
for define in cppdefines:
if isinstance(define, (tuple, list)):
values[str(define[0])] = str(define[1]) if len(define) > 1 else "1"
else:
values[str(define)] = "1"
text = flags if isinstance(flags, str) else " ".join(map(str, flags))
words = shlex.split(text)
index = 0
while index < len(words):
word = words[index]
if word in ("-D", "-U"):
index += 1
word += words[index] if index < len(words) else ""
if word.startswith("-D"):
name, _, value = word[2:].partition("=")
values[name] = value if "=" in word else "1"
elif word.startswith("-U"):
values[word[2:]] = None
index += 1
return values
def _enabled(value):
return value is not None and str(value).strip("\"'() ") not in ("", "0")
def source_selected(src_filter, source):
"""Resolve ordered includes/excludes for the explicit outside-src roles."""
text = src_filter if isinstance(src_filter, str) else " ".join(map(str, src_filter))
selected = False
for sign, pattern in re.findall(r"([+-])<([^>]+)>", text.replace("\\", "/")):
# +<*.cpp> and +<*> scan PROJECT_SRC_DIR, not ../examples. They do
# not imply that any example role is part of the image.
if source.startswith("../examples/") and "examples/" not in pattern:
continue
pattern = pattern.rstrip("/")
if fnmatch.fnmatchcase(source, pattern) or source.startswith(pattern + "/"):
selected = sign == "+"
return selected
def trim_policy(platform, mcu, env_name, src_filter, flags=(), cppdefines=(),
ota_name_mode="front-coded"):
"""Return defaults for a qualified non-Companion role, otherwise nothing.
The name policy is explicit so the profile can use a different verified
representation later without changing role selection or hardware trims.
"""
if platform != "nordicnrf52" or not str(mcu).lower().startswith("nrf52"):
return {}
values = flag_defines(flags, cppdefines)
names = (str(env_name), str(values.get("OTA_VARIANT", "")))
if any(any(tag in name.lower() for tag in (
"companion", "comp_radio", "terminal_chat", "term_chat", "terminal-chat", "secure_chat")) for name in names):
return {}
if any(_enabled(value) for name, value in values.items()
if name.startswith("COMPANION_")):
return {}
if any(source_selected(src_filter, source) for source in COMPANION_SOURCES):
return {}
if not any(source_selected(src_filter, source) for source in ROLE_SOURCES):
return {}
if "MESH_NRF52_FLASH_TRIM" in values and not _enabled(values["MESH_NRF52_FLASH_TRIM"]):
return {}
if ota_name_mode != "front-coded":
raise ValueError("unsupported nRF52 OTA name trim policy: " + ota_name_mode)
definitions = dict(TRIM_DEFINES, OTA_TARGET_NAME_FRONT_CODED=1)
# Explicit board/user flags retain their value. In particular never undo
# an opt-out or a board-specific USB configuration by re-defining it.
return {name: value for name, value in definitions.items() if name not in values}
def supported_cc310(framework_dir, mcu, defines):
"""Only wrap the exact inspected archive; future SDKs retain their getter."""
if str(mcu).lower() != "nrf52840" or not _enabled(defines.get("USE_CC310_HW_CRYPTO")):
return False
archive = Path(framework_dir) / "libraries/Adafruit_nRFCrypto" / CC310_ARCHIVE
return archive.is_file() and hashlib.sha256(archive.read_bytes()).hexdigest() == CC310_ARCHIVE_SHA256
def trim_tft_library(build_env, node):
# PlatformIO invokes this on TFT_eSPI's library builder, which is already
# a private clone. Do not add a global forced include to the project: its
# User_Setup defines are not appropriate for unrelated sources. The
# matching ST7735Display translation unit includes this header directly.
if not build_env.get("MESH_NRF52_TFT_FONT_TRIM_APPLIED", False):
build_env["MESH_NRF52_TFT_FONT_TRIM_APPLIED"] = True
build_env.Append(CCFLAGS=[
"-include", build_env.subst("$PROJECT_DIR/src/helpers/ui/Nrf52TftFontTrim.h"),
])
return node
def install(build_env):
platform = build_env.PioPlatform()
mcu = build_env.BoardConfig().get("build.mcu", "")
flags = build_env.get("BUILD_FLAGS", ())
cppdefines = build_env.get("CPPDEFINES", ())
definitions = trim_policy(platform.name, mcu, build_env["PIOENV"],
build_env.get("SRC_FILTER", ()), flags, cppdefines)
if not definitions:
return
build_env["MESH_NRF52_FLASH_TRIM_ACTIVE"] = True
build_env.AppendUnique(CPPDEFINES=list(definitions.items()))
print("nRF52 infrastructure flash trim: compact crypto, unused USB device classes/GPS cache/vendor splash, compact OTA target names")
framework = platform.get_package_dir("framework-arduinoadafruitnrf52")
values = flag_defines(flags, cppdefines)
if values.get("ST7789") is not None:
# Keep the font license with this qualified display build, including
# cached DFU packages. The hook independently checks the effective
# font override and excludes every Companion transport/profile.
hook_path = build_env.subst("$PROJECT_DIR/scripts/package_nrf52_font_license.py")
hook_spec = importlib.util.spec_from_file_location("mesh_nrf52_font_license", hook_path)
font_license = importlib.util.module_from_spec(hook_spec)
hook_spec.loader.exec_module(font_license)
font_license.install(build_env)
if (values.get("DISPLAY_CLASS") == "ST7735Display"
and source_selected(build_env.get("SRC_FILTER", ()), "helpers/ui/ST7735Display.cpp")):
build_env.AddBuildMiddleware(trim_tft_library, "*TFT_eSPI*/*.cpp")
if framework and supported_cc310(framework, mcu, values):
build_env.AppendUnique(LINKFLAGS=["-Wl,--wrap=CRYS_ECPKI_GetEcDomain"])
build_env.AppendUnique(CPPPATH=[str(Path(framework) / "libraries/Adafruit_nRFCrypto/src/nrf_cc310/include")])
# BuildSources is called on this selected nRF52 environment only. The
# wrapper is not in the common source filter and cannot leak to other
# platforms or an excluded Companion role.
build_env.BuildSources("$BUILD_DIR/nrf52-flash-trim",
"$PROJECT_DIR/src/helpers/nrf52",
"+<Cc310DomainTrim.c>")
else:
print("nRF52 CC310 trim: unsupported/unused SDK; ordinary domain lookup retained")
if "Import" in globals():
Import("env")
install(env)
+4
View File
@@ -16,6 +16,7 @@ from urllib.parse import quote, urljoin
sys.path.insert(0, str(Path(__file__).resolve().parent))
from firmware_memory_manifest import validate_package
from check_nrf52_uf2 import check_uf2
from package_nrf52_font_license import validate_artifact_notice
ROOT = Path(__file__).resolve().parents[1]
@@ -69,6 +70,9 @@ def collect_artifacts(directory, version):
if any(item.stat().st_size == 0 for item in files):
raise ValueError(f"{stem}: empty firmware artifact")
memory = validate_package(directory / stem)
font_notice = validate_artifact_notice(directory / stem)
if font_notice is not None:
files.append(font_notice)
manifest["runtime_ram"] = {key: memory[key] for key in (
"passed", "available_internal_bytes", "required_heap_bytes", "elf_sha256")}
accounted.update(files)
+211
View File
@@ -0,0 +1,211 @@
"""Carry the production bitmap-font OFL notice alongside affected firmware.
Only the qualified nRF52 infrastructure/ST7789 font mode 9 installs this hook.
The DFU manifest and all application/init-packet entries stay byte-identical;
the extra text entry is not selected by Nordic/Adafruit DFU loaders. UF2 files
are unchanged and travel with an identically named .font-license.txt sidecar.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import shlex
from zipfile import BadZipFile, ZIP_STORED, ZipFile, ZipInfo
ROOT = Path(__file__).resolve().parents[1]
NOTICE_PATH = ROOT / "licenses/MeshNotoCondensed-OFL.txt"
NOTICE_ENTRY = "FONT-LICENSE.txt"
SIDECAR_SUFFIX = ".font-license.txt"
def notice_bytes():
# Explicit UTF-8/LF also makes ZIP contents identical on Windows and Linux.
return NOTICE_PATH.read_text(encoding="utf-8").replace("\r\n", "\n").encode("utf-8")
def _defines(flags=(), cppdefines=()):
"""Read effective -D/-U values without requiring PlatformIO in host tests."""
values = {}
for define in cppdefines:
if isinstance(define, (tuple, list)):
values[str(define[0])] = str(define[1]) if len(define) > 1 else "1"
else:
name, separator, value = str(define).partition("=")
values[name] = value if separator else "1"
words = shlex.split(flags if isinstance(flags, str) else " ".join(map(str, flags)))
index = 0
while index < len(words):
word = words[index]
if word in ("-D", "-U"):
index += 1
word += words[index] if index < len(words) else ""
if word.startswith("-D"):
name, separator, value = word[2:].partition("=")
values[name] = value if separator else "1"
elif word.startswith("-U"):
values[word[2:]] = None
index += 1
return values
def _enabled(value):
return value is not None and str(value).strip("\"'() ") not in ("", "0")
def qualifies(build_env):
"""Match Nrf52FontConfig.h after the infrastructure trim policy qualifies."""
if not build_env.get("MESH_NRF52_FLASH_TRIM_ACTIVE", False):
return False
values = _defines(build_env.get("BUILD_FLAGS", ()), build_env.get("CPPDEFINES", ()))
if (values.get("NRF52_PLATFORM") is None or values.get("ST7789") is None
or not _enabled(values.get("MESH_NRF52_FLASH_TRIM"))
or values.get("COMPANION_RADIO_FULL") is not None):
return False
names = (str(build_env.get("PIOENV", "")), str(values.get("OTA_VARIANT", "")))
if any(any(tag in name.lower() for tag in (
"companion", "comp_radio", "terminal_chat", "term_chat", "terminal-chat", "secure_chat"))
for name in names):
return False
if any(_enabled(value) for name, value in values.items() if name.startswith("COMPANION_")):
return False
mode = values.get("MESH_ARIAL_EXPERIMENT")
if mode is None:
mode = values.get("MESH_NRF52_FONT_MODE")
if mode is None:
mode = "9"
try:
return int(str(mode).strip("\"'() "), 0) == 9
except ValueError:
return False # Unknown/expression overrides must not claim this font.
def package_notice(package):
"""Return the canonical notice, or None for an unaffected DFU package."""
with ZipFile(package) as archive:
notices = [info for info in archive.infolist() if info.filename == NOTICE_ENTRY]
if not notices:
return None
if len(notices) != 1 or archive.read(notices[0]) != notice_bytes():
raise ValueError(f"{package}: unexpected or duplicate font-license entry")
return archive.read(notices[0])
def _check_existing(path, data):
if path.exists() and (not path.is_file() or path.read_bytes() != data):
raise ValueError(f"{path}: refusing to overwrite a different existing font notice")
def _write_new_or_identical(path, data):
_check_existing(path, data)
if not path.exists():
# Never replace an unrelated file, and preserve mtimes on cached builds.
with path.open("xb") as stream:
stream.write(data)
def package_font_license(package, sidecar=None):
"""Append one fixed, deterministic text entry; never regenerate DFU data."""
package = Path(package)
sidecar = Path(sidecar) if sidecar is not None else package.with_suffix(SIDECAR_SUFFIX)
data = notice_bytes()
_check_existing(sidecar, data)
existing = package_notice(package)
with ZipFile(package) as archive:
# Ensure this is an application DFU package before touching anything.
manifest = json.loads(archive.read("manifest.json"))
application = manifest["manifest"]["application"]
for key in ("bin_file", "dat_file"):
selected = application[key]
if sum(info.filename == selected for info in archive.infolist()) != 1:
raise ValueError(f"{package}: invalid DFU application entry {selected}")
if archive.testzip() is not None:
raise ValueError(f"{package}: corrupt DFU package")
if existing is None:
entry = ZipInfo(NOTICE_ENTRY, date_time=(1980, 1, 1, 0, 0, 0))
entry.compress_type = ZIP_STORED
entry.create_system = 3
entry.external_attr = 0o100644 << 16
with ZipFile(package, "a") as archive:
archive.writestr(entry, data)
_write_new_or_identical(sidecar, data)
return sidecar
def _sidecar(stem):
stem = Path(stem)
return stem.parent / (stem.name + SIDECAR_SUFFIX)
def validate_artifact_notice(stem):
"""Require the notice alongside licensed ZIP/UF2 pairs before hashing."""
stem = Path(stem)
package = stem.parent / (stem.name + ".zip")
sidecar = _sidecar(stem)
data = package_notice(package) if package.is_file() else None
if data is None:
if sidecar.exists():
raise ValueError(f"{stem}: font sidecar without a licensed DFU package")
return None
if not sidecar.is_file() or sidecar.read_bytes() != data:
raise ValueError(f"{stem}: missing or mismatched font-license sidecar")
return sidecar
def collect_build_notice(directory, stem):
"""Copy only a notice proved by this build's ZIP, never a stale sidecar."""
directory = Path(directory)
package = directory / "firmware.zip"
data = package_notice(package) if package.is_file() else None
output = _sidecar(stem)
if data is None:
# A prior mode-9 build may leave its source sidecar when opting out.
# Ignore it; do not delete it or accidentally publish it for mode 0.
if output.exists():
raise ValueError(f"{output}: stale output notice for an unaffected build")
return None
build_notice = directory / ("firmware" + SIDECAR_SUFFIX)
if not build_notice.is_file() or build_notice.read_bytes() != data:
raise ValueError(f"{build_notice}: missing or mismatched build font notice")
_write_new_or_identical(output, data)
return output
def install(build_env):
"""Install after nrf52_flash_trim.install has appended effective defines."""
if not qualifies(build_env) or build_env.get("MESH_NRF52_FONT_LICENSE_INSTALLED", False):
return False
build_env["MESH_NRF52_FONT_LICENSE_INSTALLED"] = True
package = "$BUILD_DIR/${PROGNAME}.zip"
build_env.Depends(package, [str(NOTICE_PATH), str(Path(__file__).resolve())])
def attach_notice(source, target, env):
# buildprog is deliberately idempotent: a cached ZIP still needs its
# sidecar validated/emitted. Its alias depends on the ZIP builder.
package_font_license(env.subst(package), env.subst("$BUILD_DIR/${PROGNAME}" + SIDECAR_SUFFIX))
build_env.AddPostAction(package, attach_notice)
build_env.AddPostAction("buildprog", attach_notice)
return True
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=("collect", "validate"))
parser.add_argument("path", type=Path)
parser.add_argument("--stem", type=Path)
args = parser.parse_args()
try:
if args.action == "collect":
if args.stem is None:
parser.error("collect requires --stem")
collect_build_notice(args.path, args.stem)
else:
validate_artifact_notice(args.path)
except (OSError, ValueError, KeyError, BadZipFile) as error:
parser.exit(1, f"Firmware font notice qualification failed: {error}\n")
if __name__ == "__main__":
main()
+18
View File
@@ -0,0 +1,18 @@
/* Keep the exact P-256 domain used by Bluefruit Secure Connections. The
* pinned CC310 archive's generic getter otherwise retains every legacy EC
* curve table. MeshCore identity signatures use Edwards25519 via a different
* API and are unaffected. Build-local linker wrapping is qualified by the
* SDK archive hash in scripts/nrf52_flash_trim.py, never by MCU name alone.
*/
#if defined(MESH_NRF52_FLASH_TRIM) && MESH_NRF52_FLASH_TRIM
#include <stddef.h>
#include "crys_ecpki_domain.h"
extern const CRYS_ECPKI_Domain_t *SaSi_ECPKI_GetSecp256r1DomainP(void);
const CRYS_ECPKI_Domain_t *__wrap_CRYS_ECPKI_GetEcDomain(
CRYS_ECPKI_DomainID_t domain_id) {
if (domain_id != CRYS_ECPKI_DomainID_secp256r1) return NULL;
return SaSi_ECPKI_GetSecp256r1DomainP();
}
#endif
+10
View File
@@ -242,7 +242,12 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
state_word(fs), have, tot, pct, midhx, age);
}
const char* hw = (c.hw_id[0]) ? c.hw_id : "?";
#if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED
char target_name[OTA_TARGET_ENV_NAME_CAPACITY];
const char* tenv = ota_target_env_name(c.manager.target(), target_name, sizeof target_name);
#else
const char* tenv = ota_target_env_name(c.manager.target()); // env name, or "?" if not in the table
#endif
#if defined(NRF52_PLATFORM)
// nRF52 applies via the bootloader - show (cached) whether it can, so `ota get`/`install` won't surprise.
// blrc = the bootloader's last apply code (diagnostic; 0xB8=success, see ota_delta.c).
@@ -390,7 +395,12 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
// and flag the explicit legacy rescue path when an app-only internal-flash nRF52 has no valid EndF.
char hwbuf[16];
const char* fit;
#if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED
char target_name[OTA_TARGET_ENV_NAME_CAPACITY];
const char* env = ota_target_env_name(h->target_id, target_name, sizeof target_name);
#else
const char* env = ota_target_env_name(h->target_id);
#endif
const bool boot_package = (h->flags & MFLAG_BOOTLOADER) != 0;
if (boot_package) {
bool installable = false;
+424 -2
View File
@@ -1,5 +1,6 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand.
// 634 OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32)
@@ -8,12 +9,428 @@
// Size-constrained receivers can set OTA_TARGET_NAME_TABLE=0. They still match targets by ID;
// OTA_LOCAL_TARGET_ID + OTA_LOCAL_TARGET_NAME optionally retain the local human-readable name.
namespace mesh { namespace ota {
#ifndef OTA_TARGET_NAME_TABLE
#define OTA_TARGET_NAME_TABLE 1
#endif
#ifndef OTA_TARGET_NAME_FRONT_CODED
#define OTA_TARGET_NAME_FRONT_CODED 0
#endif
#if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED
#include "tinf/tinf.h"
#endif
namespace mesh { namespace ota {
#if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED
// All names retained. Front coding + Google Zopfli (1,000 iterations), raw DEFLATE.
// Reuses the existing OTA tinf decoder; local chunk/caller buffers, never the live OTA context.
// This is firmware-only storage compression: target IDs and the LoRa protocol are unchanged.
enum { OTA_TARGET_ENV_NAME_CAPACITY = 68 };
inline const char* ota_target_env_name(uint32_t target_id, char* buffer, size_t capacity) {
if (!buffer || !capacity) return nullptr;
buffer[0] = 0;
static const uint32_t IDS[] = {
0x149bcb23, 0x30d4e442, 0x263a8d80, 0x9070ae2c, 0xf190d834, 0x32890982, 0xacb599d5, 0x82d3b273,
0xe96a6af8, 0x180bbe1f, 0xf9986af5, 0x98519a1d, 0x06b71719, 0x6384719d, 0x4c0461c8, 0x4fa88992,
0xcd6604d0, 0xc54ea258, 0x217958b4, 0x03d9704a, 0x8456f753, 0xc8104197, 0xf5eb61ec, 0x70d2d590,
0xd5e04824, 0xa416bb20, 0x875f3744, 0xdaf349be, 0x93b93199, 0x710abb59, 0x5293da13, 0xc6dd7723,
0xf2eec98c, 0x48a986f1, 0xd2cf978c, 0x53626334, 0xefe4f19f, 0xfcaf3730, 0xd90deef7, 0x2d2a4089,
0x89ea5c4e, 0xc0eb712a, 0x67293f43, 0x3f364910, 0xac4b4eb5, 0x822e96ca, 0xe077fb8d, 0x2bbe7fd8,
0x9e40157c, 0xb12e6e01, 0xf6f835ec, 0x7fab017f, 0x9e23928a, 0xf310a4d1, 0xdd5953ec, 0x97574bc2,
0x2d918a3c, 0xd1755faa, 0x8a6ceaf0, 0x57f69746, 0xbc05d6a7, 0x5bad80f1, 0xb4f727a8, 0x112d34c4,
0xf10a26df, 0xc5409ced, 0xf9022545, 0x2023036d, 0x4abd24af, 0x35d42b35, 0x9f736ca7, 0x3fbb1498,
0x42b0818c, 0x80dc72a9, 0xafeed10c, 0x98411ac6, 0xa14d9f4a, 0x6494fda0, 0x32bcdd14, 0xfe937f14,
0x5b9b1933, 0x1b4399de, 0xf7e47568, 0xb1153422, 0x388f2804, 0x075ca3f9, 0x2f9f8e7d, 0xc9dc5d8c,
0x34b0e5f3, 0xb4d0ad97, 0xcfa0e233, 0x17d5e1ff, 0xc99313c6, 0xffb3854f, 0x21a73973, 0x3067f656,
0xfdb25057, 0x836e5931, 0x0f1767cd, 0x57172fcc, 0xa529b4f5, 0x85a8c944, 0x3c60586d, 0x0a9dbbf0,
0xb519a89f, 0xa3e8d8ce, 0xb16009a0, 0xdc780e80, 0x28b8007c, 0x4aa63180, 0x6fddcbf1, 0xf4fb5484,
0x2e5384e2, 0xb62a1e75, 0x9e64845d, 0x19232e8b, 0xfaf164f4, 0x38b948b4, 0x462ddb9b, 0xed21ca4f,
0x9f5f39a7, 0x081d2219, 0x7c498bc4, 0xc283ad60, 0x4fbdc1e1, 0xc797fd73, 0xab9871c8, 0x2075687c,
0x5d726067, 0x9969bbc9, 0x804f7a3a, 0x0823a21a, 0x22f900de, 0xaca0e153, 0x516c9c13, 0xcb1c2e65,
0xd1b29b18, 0x644bb68d, 0xd19a9759, 0xde40f56f, 0x6dc15ab3, 0xc59d294e, 0xceeddfaf, 0x21519537,
0x382bb181, 0xad1bcd63, 0x5a2980e6, 0x90581c83, 0x90a6a854, 0x356df17c, 0x99a084fc, 0x64c9dfc9,
0x2a2ffc92, 0x2c13b8e9, 0x1ee7d867, 0xb016979a, 0xb49b6b55, 0x01d8f124, 0x86fb85f0, 0xd33b0986,
0x1a4d0096, 0xa8f5940f, 0x1cf5e14c, 0x2707d3d1, 0x86ac3b1b, 0x7af848da, 0xd522a14f, 0x76657d76,
0x21ee03bd, 0x5977d941, 0xe2a1907b, 0xbb0ca8db, 0xf75feb3e, 0xb0a1e71f, 0xbc9cdc70, 0x19e31fc6,
0xa01b330e, 0x36c78d86, 0xdf722326, 0xfa901c87, 0xeebe3469, 0xb0f5aff6, 0x5729ad07, 0xaa2aef42,
0xb4cb035a, 0xbb429dde, 0x883e3edc, 0x4a9d1c6c, 0xa0965a6b, 0x59ea25fa, 0x1758bdb9, 0x1313f16d,
0x0372b0f1, 0xa2fb4192, 0xdd39457b, 0x7964461c, 0x1d1c0d92, 0x5fc52bcb, 0x0dacb683, 0xe792a051,
0x2d5ea842, 0x30c59b1d, 0xab74db9e, 0xeed78ce4, 0x7778b7e1, 0x0b6847ed, 0xab3e1ad3, 0x04a8b763,
0xc456437e, 0xb3ecafca, 0xf3afcde6, 0x87513d56, 0xca812b41, 0x18594231, 0x188f3ac1, 0x402055ec,
0x12393f1f, 0x16fbab07, 0xed3ef5b8, 0x8ea86381, 0x09f57289, 0x148aeaab, 0x6e9f7fec, 0x6a25d024,
0xc5a2cb41, 0xc4a11710, 0x753947a4, 0xe6fa98b4, 0xf8c27cf4, 0x90e227d9, 0x812f5e52, 0x61f961c5,
0x1ddd15c8, 0x49d67d0d, 0xbc003322, 0x835b70c0, 0xd7460575, 0x36acced5, 0x33e2c171, 0x5be91148,
0xa165ae99, 0x04c5c2b7, 0x6c1ca659, 0xa295fb04, 0x61c405ca, 0xe893fe91, 0x2b599d31, 0xf1442d25,
0x15082ede, 0x6ff350d1, 0xe03d0cd9, 0x82e1eb2c, 0x0ee19b12, 0x6c67ed89, 0xd1c05d0d, 0xb90150b0,
0x7285bdb3, 0x77cadc99, 0xc177e6cd, 0xf7a38993, 0x83762f33, 0x86cadda9, 0x7e49da49, 0x305d6f92,
0xbc6b870b, 0x78845fdd, 0x6d35f434, 0xa52d76a8, 0xd8de9639, 0x8ab5c51b, 0x80097676, 0xffe70df5,
0x3da80417, 0x47ca3c13, 0x28b499c7, 0xf1ab4c28, 0xe5331f02, 0xfba7177b, 0xde7dc472, 0x3625bb80,
0x69df7765, 0xe9fbc9be, 0x40818863, 0x65deac33, 0xd86cf67a, 0xed73ac03, 0xab0d550a, 0x1bd2a266,
0x3655c748, 0x9a5ae8fc, 0x1e9eb97a, 0x139536f4, 0x0716c724, 0x8de23fa7, 0xb92ec96b, 0x420ebc63,
0x967ccaee, 0x8021afa2, 0x5f7c7688, 0xbd04e6e7, 0x1aeac884, 0x07849209, 0x0c79b100, 0xbfb60e7c,
0x8a63de3a, 0x96036a68, 0xb76a745c, 0x43023193, 0xa7d97db7, 0x468a133e, 0xb59ea2ed, 0xbe372c6d,
0x6215e9eb, 0x07907102, 0x3898ea56, 0x2fb7e218, 0x8c61d4c8, 0x4f722e70, 0x694eed25, 0x85b8b9d7,
0xe8421e36, 0x82eaaf9e, 0x9f9caba4, 0xe4a2de74, 0x582c7826, 0xaeb637f1, 0x86813271, 0x67b2ff84,
0xd0e2e616, 0x3f9edfe3, 0xc23f8082, 0x0e74ae16, 0x8f256e56, 0xf1407f39, 0xe90a3181, 0x3b1b4237,
0x21c6d08d, 0x34302c14, 0x268d6fc1, 0xeabe3b40, 0x3c172d27, 0xf7c5d584, 0x81960cc0, 0x504020ea,
0x9d8b60ae, 0xa2ce002f, 0xba4f47f3, 0x6f16479e, 0x1018e5d1, 0xc0ab5040, 0xa2bf90bd, 0xe6bff8f9,
0xb9cd26cb, 0x9bc34bd5, 0x144504b0, 0xdbfe2675, 0xb00241d0, 0x88e77fee, 0xae2d9caa, 0xa0043e00,
0x26bbf4ed, 0xdcc0f158, 0xe3320463, 0x852b928a, 0x0ec870e9, 0x8f00c1f2, 0xcea95e20, 0xd45277ff,
0x2cad9a9d, 0x02a4a8c1, 0x4a709348, 0x35d960bd, 0x62977023, 0x17a7ed8c, 0x07f5f5b5, 0xfd0d4b8f,
0xf5afb7dd, 0x6ebf28cb, 0x2d589527, 0x77d0810c, 0x11b131e7, 0x54850763, 0x0fa61805, 0x8039574f,
0xa65555d6, 0x22b1804c, 0xa9f631b1, 0x49d11eca, 0xd41f0638, 0xe3105eb8, 0xf8259880, 0x063f1f00,
0x70d71f92, 0xa8a79da5, 0xc0d3815b, 0x966ac88e, 0x104b3f8b, 0x23b49348, 0xc42cc90d, 0xc24791c3,
0x15e11067, 0x2dbb2669, 0x90799c32, 0x881a6a48, 0xf26af51c, 0x8174cbdc, 0x0bf0f061, 0x2509dd80,
0x56293b24, 0x8c81078c, 0xbd6a1c97, 0xf4d98c12, 0xccac4283, 0xa68c09e9, 0x5f606274, 0x84923735,
0x2c0b8b05, 0xdba69401, 0xcb6555ee, 0xd60daf25, 0x410bd800, 0x9aca380e, 0xd8641710, 0x4d0b1601,
0xab2ac09e, 0xa17fc4f7, 0xe4b2bd22, 0x90c4eb3f, 0x19d9ce91, 0x121cc72f, 0xfa8510fa, 0xd34d8461,
0x98077550, 0x11628e29, 0x2ed959aa, 0xf1e7cadb, 0xced7e127, 0x6925aaf0, 0x2fa509c1, 0xa52ba3c8,
0xaf0b81dd, 0x9f4d58ac, 0x7a55dec3, 0x5e69738a, 0x48d3b5d4, 0x14e6965d, 0x76f3c984, 0x41e33a97,
0x29a0da19, 0x29bc38dc, 0x05f5ffae, 0xc62da371, 0xf1d3c5a8, 0xc6d55752, 0x60683191, 0x8a7cb79b,
0x4fa4ae55, 0xd1d61a8b, 0x9cb0b232, 0x51fa1a49, 0xdb73638b, 0xd904c2d5, 0x3a14aac7, 0x620569f4,
0x08483f6b, 0x0db48f35, 0x20532f6c, 0x293ced32, 0xb38ebf22, 0x7c89c73d, 0x2d7c11e8, 0x61b32037,
0xad1cd53d, 0x0e18b360, 0x3caf966e, 0xcb59ea4e, 0xe3a28284, 0x05747eb6, 0x75587f24, 0xf2128c81,
0x1b34e7b4, 0x8e549407, 0x6ca142f4, 0x1deac038, 0x2a2f303b, 0x0ba4453d, 0xf8d1958f, 0x75e85141,
0xd62ed92e, 0x7abaf0d5, 0x87edc08b, 0x3c49caf8, 0x58689ef9, 0x91878dd8, 0x72a93c45, 0x4dbf035e,
0xff4102d6, 0x2f9a9185, 0x000fc519, 0x7b071fa0, 0x3e55e34d, 0x54606a4f, 0x16482630, 0xce0f07ad,
0x01c43d49, 0x51fe3801, 0x857149e5, 0xd9d64938, 0xcb7da3f5, 0x9952fbe3, 0x3729cd74, 0xdc27c37c,
0x9b9f8775, 0xb27514a3, 0xe9ce038c, 0xe295a7a4, 0x189660b3, 0x99907475, 0x2082078d, 0x24a94c81,
0x3750e80d, 0xe4b49973, 0xe1372b52, 0x5a79bac1, 0x6f339514, 0x8eaa289e, 0x70e91bd9, 0x5a7e9c2d,
0x1211b151, 0x0a32044b, 0xa13fe853, 0x79b9ff01, 0xc3decea2, 0x3c012018, 0x6afd6f75, 0x7517d400,
0xd7598668, 0x61bdb52a, 0x413287d8, 0xe8cd2377, 0x3309bdaf, 0x7330afbb, 0x9df48ce6, 0xa5cb783a,
0x4b9f8283, 0x311846c9, 0x8b6b29b7, 0x37928233, 0x88ac45da, 0x087f99c0, 0xdf41affe, 0x2afe6c63,
0xf2d61a1c, 0x14b1b097, 0xec334f3f, 0x1da30414, 0xae8fe1c9, 0x67acd73f, 0xfb118ed8, 0xe4213729,
0x1298038d, 0x2c47acff, 0x7d641646, 0x51efdbfe, 0xf2aada5b, 0xe59c9373, 0x236f605c, 0x93994fed,
0x66c47cfd, 0x48061a01, 0x7ed7137d, 0xa01688a7, 0xa5f4a562, 0xef83222b, 0xc36ec867, 0xb932238e,
0x92f5bbea, 0x26d91ab2, 0x708ac4dc, 0xdc51bf13, 0xc27f8304, 0x7190df40, 0x609d50b3, 0x5718c7ea,
0x37443a5b, 0x603384a7, 0xce3ef40f, 0x475a2a6a, 0xf4175d2c, 0x3d831006, 0x556eb1bc, 0xb67370b3,
0x0c689b90, 0x91eac424, 0x7a84a8c4, 0x67843a5f, 0x0e83fd51, 0xfad357ab, 0x16af5c67, 0x8c105104,
0xa80042a0, 0x4bff0748, 0x77c8f2b5, 0x71ee0e15, 0x8824cd0c, 0xe5439198, 0xe90a6e74, 0x833e5cdc,
0x93145fbb, 0x8d3dd6d5, 0x33a154ff, 0x6658b418, 0x0cfb9c00, 0x964f4f5c, 0x0a5dc760, 0xd08496ce,
0x293ac2c8, 0x60ebfa87, 0x60f5fdc2, 0xe8f79d22, 0xb97bbb15, 0xd466590c, 0x9f27be15, 0x396fecc7,
0xf94e3407, 0xe1930103, 0xa4740508, 0x9f19bcd1, 0xfcebff61, 0xac5c1405, 0xccb734e2, 0x634ff632,
0xf967a300, 0xa0cc1c36,
};
static const uint16_t OFFSETS[] = {
0, 232, 452, 649, 874, 1085, 1312, 1549, 1773, 1968, 2187, 2428,
2687, 2908, 3130, 3377, 3609, 3823, 4039, 4264, 4347,
};
static const uint16_t RAW_LENGTHS[] = {
511, 505, 509, 512, 502, 500, 512, 510, 505, 489, 507, 507,
490, 502, 507, 511, 509, 510, 498, 102,
};
static const uint16_t FIRSTS[] = {
0, 28, 65, 94, 122, 156, 192, 226, 261, 293, 329, 361,
394, 425, 453, 484, 517, 552, 591, 628, 634,
};
// 4347 encoded bytes; 20 independent chunks, at most 512 decoded bytes.
static const uint8_t NAMES[] = {
0x8d, 0x90, 0x03, 0x6f, 0x40, 0x41, 0x0c, 0xc7, 0xbb, 0x60, 0xb6, 0x1d, 0xcc, 0x36, 0xe2, 0xd9,
0x66, 0xdc, 0x1c, 0x9a, 0x3c, 0xf7, 0xa5, 0x77, 0xd3, 0xa7, 0x9f, 0xed, 0xe8, 0xd8, 0x3f, 0x7e,
0xb0, 0xaa, 0x6f, 0x3c, 0xe1, 0x2a, 0x1f, 0xa9, 0x91, 0xe3, 0x29, 0x34, 0x9c, 0xe6, 0x2a, 0x0b,
0x39, 0x43, 0x51, 0x36, 0x64, 0xd4, 0x09, 0x41, 0xe7, 0x85, 0xd3, 0x50, 0x19, 0x87, 0xce, 0x61,
0xca, 0x96, 0x52, 0xa8, 0x3c, 0xa2, 0x9c, 0x94, 0x27, 0x81, 0x4a, 0x61, 0x4e, 0xd1, 0x91, 0x5c,
0x3e, 0x1c, 0x3c, 0x49, 0x1a, 0x66, 0x2a, 0x41, 0x13, 0x28, 0x0f, 0xb0, 0xbe, 0x78, 0x32, 0x33,
0x3b, 0x89, 0x53, 0xe3, 0xc7, 0xb8, 0x4b, 0x2e, 0xc0, 0xed, 0xd0, 0x7f, 0xab, 0xdf, 0xfd, 0xa0,
0x5f, 0xff, 0x4e, 0xbf, 0x5e, 0x9e, 0xf5, 0x31, 0x61, 0x51, 0xc8, 0x5e, 0x61, 0xc6, 0x48, 0xd7,
0x9e, 0xe4, 0x41, 0xdd, 0x51, 0xe6, 0x58, 0x1c, 0x34, 0xbc, 0xf3, 0x2e, 0x7a, 0x74, 0x58, 0x3d,
0x5b, 0xc2, 0x03, 0x61, 0xfc, 0x4e, 0x0b, 0xda, 0xff, 0xaf, 0x56, 0x7e, 0x22, 0xca, 0xc4, 0x24,
0x8f, 0x62, 0xdf, 0x45, 0xee, 0x7f, 0x88, 0xdc, 0xfc, 0xce, 0xa6, 0xf9, 0x7f, 0x91, 0x5b, 0xde,
0x9b, 0x9c, 0x2b, 0x6f, 0x82, 0x89, 0x6f, 0x99, 0x7f, 0xc0, 0x51, 0x40, 0x19, 0x49, 0x68, 0x70,
0x75, 0x72, 0xf2, 0x7d, 0xb3, 0x72, 0x77, 0x3d, 0x31, 0x79, 0xcf, 0xf7, 0xb5, 0x60, 0xcd, 0xfc,
0xdb, 0xbe, 0xe4, 0xf8, 0x60, 0x6f, 0xff, 0xfc, 0x51, 0xfa, 0x59, 0xf5, 0x21, 0x71, 0xd5, 0xd3,
0xbb, 0x40, 0xf5, 0x63, 0x8e, 0x4b, 0x81, 0x3b, 0x94, 0x51, 0xb5, 0x56, 0x05, 0x41, 0x0c, 0x1d,
0x1a, 0x74, 0xa8, 0x70, 0xff, 0x04, 0xdc, 0x5a, 0xdc, 0x5d, 0xca, 0xac, 0xe5, 0xb1, 0x39, 0x6f,
0xec, 0x24, 0xb3, 0xec, 0xe7, 0xe3, 0xee, 0x54, 0xa3, 0xb9, 0xaa, 0x36, 0xd1, 0x21, 0x53, 0x0e,
0xeb, 0xa7, 0x47, 0x07, 0x87, 0x97, 0x10, 0x91, 0x2d, 0xb9, 0xd4, 0x40, 0x5e, 0xa6, 0x51, 0xb5,
0xac, 0x1f, 0x9d, 0xac, 0xc2, 0x1e, 0xb9, 0x66, 0xc3, 0xf8, 0x1a, 0x96, 0xa7, 0xa6, 0x80, 0x31,
0x60, 0x1a, 0x91, 0x95, 0xda, 0x42, 0x13, 0x31, 0x87, 0x3c, 0x2e, 0xcc, 0x40, 0xee, 0x6d, 0x48,
0x1d, 0x79, 0x07, 0x9c, 0x16, 0xe4, 0x21, 0x33, 0xa8, 0x26, 0x20, 0x88, 0x1a, 0xad, 0x24, 0x53,
0xba, 0x49, 0x22, 0x60, 0x7d, 0x81, 0x56, 0xe9, 0x17, 0x80, 0x5e, 0xc8, 0x98, 0x8a, 0x2b, 0x04,
0x94, 0xe0, 0x7c, 0xad, 0xb4, 0xa0, 0x13, 0xcf, 0xaa, 0x6d, 0x7d, 0x66, 0x7a, 0xf6, 0x2b, 0xc4,
0xff, 0x80, 0x75, 0x7b, 0x6f, 0x41, 0x90, 0xaf, 0xef, 0xde, 0x3a, 0x96, 0xa7, 0x5e, 0xe1, 0x1e,
0x80, 0xfa, 0xef, 0x81, 0x86, 0x1e, 0xb6, 0xff, 0x87, 0x6b, 0xb3, 0x28, 0x25, 0x88, 0x37, 0x29,
0xbf, 0x49, 0xa3, 0x7c, 0x4c, 0x83, 0xf3, 0xd9, 0x99, 0xaf, 0xb5, 0x37, 0x2a, 0x63, 0x1e, 0x1d,
0x8c, 0xd6, 0xd4, 0xa0, 0xff, 0x13, 0xbf, 0x04, 0xa4, 0xdf, 0x97, 0xa4, 0x6b, 0x8a, 0xa5, 0xaf,
0x22, 0x14, 0x24, 0xc1, 0xa4, 0xb7, 0xab, 0xc4, 0x6a, 0xbd, 0x0e, 0xd8, 0x7a, 0x1d, 0x90, 0xf5,
0x3a, 0x60, 0xeb, 0x01, 0x4c, 0x4e, 0xc5, 0x95, 0xc3, 0x40, 0x0c, 0x55, 0x01, 0x2e, 0xc1, 0x7b,
0xf3, 0x7d, 0x99, 0x3a, 0x48, 0x17, 0xff, 0x0d, 0xc8, 0x31, 0x8e, 0xe6, 0x49, 0x13, 0xaa, 0x3e,
0x4c, 0x37, 0xe9, 0x33, 0x75, 0x3c, 0x15, 0x0e, 0xd0, 0xf0, 0xf9, 0x81, 0x4d, 0x5f, 0x3a, 0x59,
0x15, 0xc4, 0xde, 0xf2, 0xe4, 0x76, 0x50, 0xce, 0xec, 0x0a, 0x2b, 0x35, 0xf0, 0xda, 0xc7, 0x25,
0x83, 0x2d, 0x27, 0xd9, 0xd0, 0x8b, 0xc8, 0x0c, 0x63, 0x5d, 0xb3, 0x52, 0x6d, 0x9c, 0x4c, 0x94,
0x68, 0x71, 0x49, 0x2a, 0x6f, 0xff, 0x3f, 0x08, 0x32, 0x67, 0x97, 0x7a, 0x49, 0x50, 0x17, 0x7b,
0x81, 0x9f, 0x18, 0x2d, 0xcf, 0xd2, 0xb6, 0xd4, 0x24, 0xaa, 0x57, 0xe6, 0x9f, 0xde, 0x6a, 0xec,
0xcd, 0x30, 0x4b, 0xe4, 0x99, 0xaa, 0x5b, 0xe7, 0xad, 0x51, 0xed, 0xe3, 0xb8, 0x6c, 0x12, 0x75,
0x90, 0xe2, 0x90, 0x04, 0xbc, 0x2d, 0xac, 0xc9, 0x4d, 0xb8, 0x14, 0x1b, 0x1d, 0x26, 0x8a, 0x57,
0x96, 0x17, 0xc9, 0xad, 0x3c, 0x50, 0xb7, 0x72, 0x18, 0x1a, 0x9a, 0x60, 0x73, 0x23, 0xd8, 0x65,
0x44, 0xb8, 0x87, 0x0c, 0x2b, 0x79, 0xd0, 0x02, 0x17, 0xab, 0xf5, 0x3a, 0x20, 0xeb, 0x65, 0x10,
0xc1, 0x8e, 0x6a, 0xa9, 0x2a, 0x3e, 0x4b, 0x51, 0xe2, 0x84, 0x3d, 0xc4, 0xd0, 0xd2, 0x00, 0x6b,
0x24, 0x40, 0xc2, 0x9e, 0x41, 0x01, 0xcc, 0x06, 0x00, 0x62, 0xf0, 0x00, 0x34, 0x46, 0xd5, 0xe8,
0x0d, 0xc5, 0x30, 0x58, 0x53, 0x99, 0x99, 0x69, 0x2b, 0xc3, 0xd6, 0x7b, 0x74, 0xd7, 0x67, 0xd0,
0x6b, 0xfc, 0xc0, 0x8a, 0x21, 0x70, 0xfb, 0x30, 0xf3, 0x64, 0x8b, 0x7f, 0xa0, 0x3c, 0x92, 0xc2,
0xbf, 0xef, 0xdf, 0x2f, 0xcc, 0x4c, 0x08, 0x58, 0xb0, 0xa6, 0x02, 0x76, 0x3c, 0x95, 0x49, 0x44,
0xf2, 0x08, 0xa7, 0xd2, 0x1b, 0xfd, 0x4f, 0x48, 0xa1, 0x6c, 0xb9, 0xd6, 0x8a, 0x59, 0x06, 0xf2,
0xd5, 0x56, 0xa9, 0x70, 0x31, 0xc2, 0x2e, 0x73, 0x81, 0xbd, 0x04, 0x5c, 0x8c, 0xd7, 0xd6, 0xe3,
0x37, 0x0e, 0x16, 0xe5, 0xec, 0x05, 0x72, 0x14, 0x68, 0x19, 0xa9, 0xde, 0xca, 0x58, 0x91, 0x63,
0x20, 0x1b, 0xd8, 0x07, 0xd8, 0xe0, 0x1a, 0x79, 0xac, 0xfe, 0x0c, 0xda, 0xe1, 0x62, 0xf1, 0xc0,
0x41, 0xd0, 0x4a, 0x78, 0xbd, 0xe2, 0x7a, 0x28, 0x75, 0x69, 0x46, 0x2f, 0x54, 0xd6, 0x3e, 0xf4,
0x8d, 0x23, 0xc8, 0xc6, 0x41, 0x9f, 0x0e, 0x08, 0x4d, 0x56, 0x0e, 0x7e, 0x50, 0x71, 0x51, 0x16,
0xd6, 0xb0, 0x45, 0x2f, 0xb4, 0x61, 0x94, 0x39, 0x61, 0x42, 0x05, 0x27, 0x09, 0xbc, 0x58, 0x78,
0x4a, 0x2a, 0x79, 0xde, 0x89, 0x5b, 0xff, 0x4a, 0x90, 0xa3, 0xa5, 0x9a, 0x49, 0x4c, 0x3f, 0x7e,
0xb5, 0x70, 0x34, 0x22, 0xf6, 0xd1, 0x80, 0xf4, 0x15, 0x8e, 0x89, 0x0d, 0xd7, 0xe3, 0x00, 0x8e,
0x87, 0xd0, 0xe0, 0x76, 0x02, 0xdc, 0x51, 0x97, 0x2b, 0x1c, 0x45, 0xf2, 0x85, 0x69, 0x93, 0x57,
0x25, 0x11, 0xa1, 0x6f, 0x70, 0x75, 0x26, 0x76, 0x68, 0x02, 0x62, 0xf0, 0x00, 0x14, 0x42, 0x0d,
0xc8, 0x0e, 0x06, 0x31, 0x38, 0xc3, 0x72, 0xf0, 0x6c, 0x1d, 0xa1, 0xb8, 0x4f, 0x7e, 0x65, 0x5f,
0x33, 0xfd, 0x17, 0xdd, 0xa4, 0xb8, 0x7d, 0x6d, 0xae, 0xf9, 0x89, 0x4a, 0xa5, 0x1c, 0x47, 0x6d,
0xcc, 0xbd, 0x0d, 0xa9, 0x63, 0xef, 0x30, 0xa6, 0x05, 0x7b, 0xcc, 0x4a, 0xc2, 0x20, 0xf0, 0x3e,
0x94, 0x0c, 0xde, 0xc7, 0x6c, 0x18, 0xea, 0x7d, 0x16, 0x41, 0xeb, 0x0b, 0xb2, 0x50, 0x8f, 0x14,
0x28, 0x55, 0x8a, 0xd0, 0xf0, 0xde, 0xa2, 0x50, 0x1c, 0x51, 0x84, 0xba, 0x52, 0xb4, 0xec, 0xd2,
0x12, 0xf3, 0x5e, 0xaa, 0x50, 0xed, 0x9c, 0x43, 0x85, 0xaf, 0x25, 0xae, 0x19, 0x96, 0xe5, 0x3e,
0xfa, 0x37, 0xda, 0x81, 0xea, 0x05, 0x92, 0x7b, 0xcc, 0x22, 0x17, 0xff, 0x84, 0x24, 0xc1, 0xf9,
0x31, 0xbc, 0x47, 0x69, 0x77, 0xda, 0xf0, 0xe0, 0xb3, 0x15, 0xf3, 0xea, 0xef, 0x2f, 0x0a, 0xdb,
0x03, 0x41, 0x4f, 0x78, 0xf8, 0xa2, 0x2e, 0xe4, 0xc4, 0x9f, 0x08, 0x85, 0xde, 0x3a, 0x86, 0x2e,
0xb6, 0x17, 0xc3, 0xa9, 0xee, 0x85, 0x5a, 0x34, 0x64, 0xbd, 0x83, 0xfa, 0x59, 0x57, 0xcb, 0x4b,
0x63, 0xe0, 0x37, 0xc8, 0x66, 0xf9, 0xb9, 0xfd, 0xb4, 0xd8, 0xcc, 0x9d, 0x6e, 0xd7, 0x0b, 0xc3,
0x9b, 0xcd, 0xdf, 0x42, 0xd8, 0x66, 0xd3, 0xa4, 0xc9, 0x1c, 0x58, 0xd8, 0xbb, 0x3e, 0x2b, 0xaa,
0xd1, 0x6b, 0x54, 0xcd, 0x1b, 0xf7, 0xbf, 0xfb, 0xfc, 0xbf, 0x7b, 0xfc, 0x33, 0x6d, 0x90, 0x85,
0x8e, 0x04, 0x21, 0x0c, 0x86, 0x1b, 0xcf, 0xb9, 0xbb, 0x5b, 0xe4, 0xdc, 0xef, 0xde, 0xa6, 0x19,
0x29, 0x59, 0xc6, 0xe0, 0x4a, 0x57, 0x1e, 0xff, 0x06, 0xd6, 0xc9, 0xc6, 0x90, 0x9f, 0x7e, 0xfd,
0x0a, 0x74, 0xa8, 0x12, 0xca, 0xb0, 0xf7, 0x89, 0x1f, 0x28, 0x4a, 0x30, 0x33, 0xb5, 0x4d, 0x1a,
0x6d, 0x1a, 0xe4, 0x24, 0xd7, 0x06, 0xfb, 0x5a, 0x69, 0x54, 0x54, 0x1b, 0xa5, 0x60, 0x39, 0x0e,
0xd3, 0x8a, 0xe0, 0x2c, 0xa4, 0x0d, 0x9c, 0x5b, 0x37, 0xda, 0x1d, 0x77, 0x5d, 0x1a, 0x5f, 0xfb,
0xf2, 0xbb, 0x36, 0xf2, 0xb8, 0x51, 0x72, 0x51, 0xff, 0x89, 0x8c, 0xf6, 0xcb, 0x34, 0x68, 0xc1,
0xae, 0x25, 0x97, 0x5a, 0x90, 0xc9, 0x52, 0x22, 0xc4, 0x70, 0x81, 0x26, 0x75, 0xc4, 0x3d, 0x62,
0xf4, 0x8f, 0xe1, 0xd0, 0x98, 0x1a, 0x87, 0x17, 0x51, 0x72, 0x10, 0xbb, 0x8f, 0xf4, 0x46, 0xfc,
0x17, 0xeb, 0xe0, 0x41, 0x75, 0xab, 0x6a, 0xdc, 0xaf, 0xd4, 0xce, 0x61, 0x6d, 0x72, 0xaa, 0x61,
0xd9, 0x26, 0x2c, 0x5a, 0x7c, 0x59, 0xb0, 0xc8, 0x89, 0x61, 0x99, 0x7f, 0x17, 0xd1, 0xe0, 0x12,
0x5b, 0xd0, 0x99, 0x07, 0xc1, 0x59, 0x98, 0xd2, 0xcf, 0x03, 0x6b, 0x33, 0xb4, 0xb5, 0x89, 0xfc,
0x5e, 0xa4, 0xb8, 0x3e, 0x95, 0x87, 0xc3, 0x28, 0x5b, 0x73, 0xd4, 0x38, 0xc3, 0xb0, 0x26, 0xc4,
0xb5, 0x6e, 0x92, 0x0a, 0xb3, 0x4e, 0xd2, 0x96, 0x2c, 0x1e, 0x0a, 0xae, 0x82, 0xc1, 0x95, 0x37,
0xb8, 0x0a, 0x06, 0x5b, 0x33, 0x06, 0x5b, 0xd6, 0xb0, 0x70, 0x32, 0xf3, 0x8f, 0x51, 0xb3, 0x7f,
0x6c, 0x50, 0x35, 0x76, 0x04, 0x31, 0x0c, 0x55, 0x1b, 0x66, 0x86, 0x2e, 0x54, 0x07, 0x8f, 0x90,
0x2e, 0x9c, 0x94, 0x7a, 0x06, 0xcd, 0xae, 0x87, 0xec, 0xc8, 0x0a, 0x9d, 0x7e, 0x99, 0xc6, 0x2f,
0x95, 0x58, 0x1f, 0xa0, 0x4d, 0xa5, 0x90, 0xc1, 0xef, 0x2b, 0xe4, 0x3b, 0x94, 0x4c, 0x30, 0x78,
0x16, 0x56, 0x4e, 0x90, 0xbd, 0xaf, 0x30, 0x12, 0x7f, 0x13, 0xa3, 0xd7, 0xa3, 0xa4, 0xfa, 0x14,
0x81, 0x35, 0xa6, 0x40, 0x4a, 0x88, 0x61, 0x2f, 0x99, 0xac, 0x4f, 0x6f, 0xe0, 0x30, 0xbd, 0x8a,
0x54, 0x47, 0xcf, 0xb0, 0xd6, 0x3b, 0xac, 0x5c, 0xad, 0x4a, 0x34, 0x6d, 0x25, 0xb0, 0x30, 0xfe,
0xb5, 0x8e, 0x9a, 0x9d, 0x6d, 0x11, 0x52, 0x0c, 0xb5, 0xff, 0x81, 0x8d, 0x92, 0x5a, 0xca, 0xfc,
0x61, 0x50, 0x2c, 0x4e, 0x9c, 0xaf, 0x51, 0x28, 0x0a, 0x6c, 0x34, 0xbf, 0x2e, 0xcc, 0x20, 0x6e,
0x25, 0x88, 0xf3, 0x23, 0xc4, 0xf9, 0x04, 0xb1, 0xa7, 0xd2, 0xf8, 0x2a, 0xa8, 0xba, 0xff, 0x94,
0x95, 0x75, 0x1e, 0x75, 0x49, 0x98, 0x51, 0xe5, 0x6b, 0x38, 0xce, 0xbe, 0xca, 0x72, 0x9c, 0x7f,
0x45, 0x3d, 0x4e, 0x7f, 0x5c, 0xe6, 0x46, 0xf9, 0xf2, 0x44, 0xff, 0x76, 0xc2, 0x79, 0x65, 0x86,
0xcd, 0xf2, 0x08, 0x7d, 0xb9, 0x89, 0x0e, 0xf7, 0x43, 0xc7, 0x3f, 0x1c, 0x53, 0x49, 0x31, 0xe2,
0xa3, 0x0a, 0xc4, 0xff, 0x11, 0x82, 0x0b, 0x0c, 0x11, 0x4e, 0x7b, 0x1c, 0x60, 0xbb, 0x70, 0xbd,
0xcd, 0xca, 0x5b, 0xaa, 0x60, 0x7b, 0x82, 0x7e, 0x9c, 0xa0, 0xef, 0xcc, 0xa0, 0xaf, 0xbe, 0xb2,
0x32, 0x45, 0x77, 0x38, 0x0c, 0xd6, 0x06, 0x19, 0xac, 0x01, 0x32, 0x18, 0x00, 0x5c, 0x4d, 0x35,
0x42, 0xc4, 0x00, 0x10, 0xdc, 0x0e, 0xaf, 0x70, 0xe7, 0x0d, 0xc9, 0x23, 0xd0, 0x0a, 0x2d, 0x27,
0x36, 0xc7, 0x6d, 0x6c, 0xc3, 0x6e, 0xe0, 0x9e, 0x8f, 0x7b, 0x37, 0x3e, 0x72, 0xc2, 0x76, 0x64,
0x89, 0x3b, 0x75, 0xb6, 0x8c, 0xc0, 0xb5, 0xe7, 0x65, 0x43, 0x47, 0xa3, 0xaf, 0xa4, 0xb3, 0x8a,
0x9d, 0x6c, 0x39, 0x07, 0xe6, 0x23, 0x5d, 0x8e, 0x50, 0xb8, 0x56, 0xf7, 0x04, 0x63, 0xe8, 0x6d,
0x26, 0xdb, 0x66, 0x1d, 0x82, 0xfe, 0x44, 0x97, 0xf9, 0xab, 0x8b, 0x14, 0xa5, 0x75, 0x43, 0xde,
0xab, 0xf5, 0xf0, 0xbc, 0x52, 0x43, 0xd1, 0x52, 0x0e, 0x31, 0x84, 0xec, 0x3d, 0x46, 0x21, 0x7b,
0x33, 0x9d, 0xa8, 0x2c, 0x7f, 0xcf, 0xad, 0xfd, 0x9b, 0xdb, 0xf3, 0x48, 0xd2, 0x44, 0xd6, 0xad,
0xf8, 0xd8, 0x44, 0xf7, 0x30, 0x8e, 0xb2, 0xf2, 0xeb, 0x64, 0x13, 0x7f, 0xbd, 0xe5, 0x60, 0x1f,
0xe6, 0x22, 0xda, 0x58, 0x93, 0x63, 0x9a, 0xf7, 0xd5, 0x94, 0x6d, 0x85, 0xde, 0x27, 0x60, 0x92,
0x20, 0x7d, 0x99, 0x41, 0x4a, 0x52, 0x6e, 0xbc, 0x81, 0xa5, 0x19, 0x56, 0x97, 0x19, 0x82, 0x1c,
0xa5, 0x58, 0x94, 0x5f, 0x02, 0x74, 0x4d, 0x0a, 0x56, 0x15, 0x96, 0x20, 0x15, 0xb2, 0x70, 0x07,
0xcb, 0x21, 0x39, 0x45, 0x18, 0x29, 0x84, 0xd8, 0xf2, 0x12, 0xf3, 0xf2, 0xc1, 0x96, 0x1a, 0x19,
0x01, 0x2d, 0xc4, 0x6a, 0x94, 0x2a, 0xc8, 0x28, 0x51, 0x24, 0x4d, 0xa2, 0x70, 0x63, 0xc5, 0x90,
0x8c, 0xe5, 0x37, 0x36, 0xa0, 0xd0, 0x04, 0x01, 0x63, 0xa2, 0x0d, 0x00, 0x00, 0xa5, 0x4f, 0xb5,
0x56, 0x44, 0x41, 0x0c, 0x4d, 0x45, 0x85, 0x6b, 0x85, 0x4b, 0x85, 0xf3, 0x07, 0xb8, 0x95, 0xdb,
0xe7, 0x8c, 0x84, 0xb3, 0xf3, 0x24, 0x79, 0x27, 0x19, 0xec, 0xeb, 0x71, 0x67, 0xba, 0x6d, 0xaf,
0x5f, 0x48, 0xb5, 0xd4, 0x0e, 0xd9, 0xb1, 0x20, 0xeb, 0x0d, 0x1e, 0x1c, 0x44, 0xdf, 0xa2, 0x52,
0x47, 0x2e, 0x93, 0xc2, 0xac, 0x48, 0x8b, 0x46, 0x7a, 0x47, 0x0a, 0x43, 0x96, 0x53, 0xa8, 0xdf,
0x54, 0xfb, 0xfb, 0xaf, 0xaa, 0x20, 0x6d, 0xe7, 0x38, 0x09, 0xa3, 0xba, 0x98, 0x04, 0x7d, 0x43,
0xb0, 0x71, 0x6b, 0x1e, 0x66, 0xeb, 0x64, 0x86, 0xad, 0x44, 0x6a, 0x61, 0xf6, 0x2b, 0x6b, 0xee,
0x47, 0xd6, 0xf8, 0xc1, 0xee, 0x80, 0x09, 0x13, 0x07, 0x03, 0x06, 0xc0, 0x15, 0x51, 0x97, 0x89,
0xf8, 0xba, 0xb7, 0xf7, 0x7d, 0x78, 0x1a, 0x1b, 0x51, 0x87, 0x92, 0x1d, 0xb2, 0x20, 0x3d, 0x64,
0x52, 0x76, 0x0d, 0x1a, 0xb1, 0x89, 0x1a, 0xc0, 0x75, 0x6a, 0x1e, 0xcf, 0x04, 0x7b, 0x5b, 0x27,
0xa1, 0x2f, 0x5b, 0xd7, 0x29, 0x53, 0x71, 0xc3, 0xd4, 0x8f, 0xfa, 0x29, 0x7e, 0xa1, 0xac, 0x4f,
0x4d, 0x53, 0x94, 0x6e, 0xbd, 0xce, 0x9d, 0xfa, 0x1e, 0xf0, 0x63, 0xe2, 0x08, 0x1e, 0x39, 0x8d,
0x45, 0xd7, 0xea, 0x9b, 0xeb, 0x67, 0x4b, 0x31, 0x61, 0xb4, 0xe4, 0x5d, 0x78, 0xf5, 0x3e, 0x03,
0x8d, 0x8f, 0xb5, 0x7a, 0x03, 0x31, 0x10, 0x06, 0xb7, 0x0b, 0x33, 0x33, 0xa7, 0x0a, 0x63, 0x1d,
0x66, 0xa6, 0x72, 0x7d, 0xb0, 0x8e, 0xf5, 0x59, 0x74, 0x82, 0xc0, 0xd3, 0x87, 0xc1, 0x6c, 0xb7,
0x82, 0x7f, 0x66, 0xe0, 0x94, 0xf1, 0xd7, 0x03, 0x85, 0xb7, 0xf3, 0x7b, 0x51, 0x46, 0x61, 0x96,
0x59, 0x8b, 0x42, 0xc5, 0x24, 0xa0, 0xd5, 0x90, 0xa6, 0xc0, 0x91, 0x81, 0x36, 0xa5, 0x04, 0x5a,
0x32, 0x4f, 0x64, 0xa0, 0x7e, 0xf5, 0x66, 0x15, 0xed, 0xcb, 0xf2, 0xca, 0xc6, 0x0a, 0x46, 0x4a,
0xe8, 0x40, 0x32, 0x25, 0xd1, 0x04, 0x31, 0x53, 0x18, 0x72, 0x82, 0x59, 0xd4, 0x16, 0xa6, 0xbc,
0x0d, 0xa1, 0x2b, 0x67, 0xab, 0xeb, 0x6f, 0x6b, 0x08, 0x43, 0xc3, 0xe2, 0x47, 0x42, 0xb2, 0x5a,
0xaa, 0x67, 0x18, 0x56, 0xe1, 0xf7, 0x32, 0x8a, 0xc4, 0x39, 0xe8, 0xce, 0x41, 0x8d, 0x62, 0xfe,
0x5d, 0x97, 0x23, 0x23, 0x98, 0x0c, 0x38, 0x46, 0x99, 0xc0, 0x41, 0xfb, 0xe6, 0x46, 0x49, 0x83,
0xda, 0xe1, 0xb9, 0xb4, 0xc2, 0xf5, 0x7a, 0x3c, 0x12, 0xda, 0x73, 0x4b, 0x78, 0xc9, 0xbd, 0xc5,
0xbf, 0x89, 0xfa, 0x6d, 0x0a, 0x04, 0x2e, 0x3f, 0x94, 0x46, 0x7f, 0xc6, 0x8f, 0xa7, 0x3d, 0xe7,
0x30, 0xfe, 0x69, 0x31, 0xfe, 0xcc, 0xd2, 0x0c, 0xda, 0x73, 0x5c, 0xda, 0xff, 0x86, 0xfa, 0x0b,
0x5c, 0x06, 0xf2, 0x63, 0x3b, 0x72, 0xd4, 0x86, 0x30, 0xff, 0xae, 0x7e, 0x97, 0xa2, 0x6c, 0x49,
0x81, 0x91, 0x4f, 0x6a, 0x4b, 0x0e, 0xaf, 0xe5, 0x8f, 0xf7, 0x0e, 0x6c, 0x50, 0x83, 0x6e, 0x04,
0x41, 0x18, 0xfe, 0xeb, 0x86, 0xb5, 0xed, 0x36, 0xac, 0x1f, 0xa0, 0x66, 0x50, 0xc5, 0xf9, 0xb2,
0xf8, 0x9b, 0x9b, 0xd5, 0xec, 0xe0, 0xf4, 0xf4, 0x67, 0xdf, 0x8e, 0xf9, 0x89, 0x3e, 0x44, 0x54,
0x7e, 0x96, 0xf8, 0x79, 0xfc, 0x79, 0xc1, 0x63, 0x24, 0x2c, 0xc3, 0x94, 0x2e, 0x2e, 0x6f, 0x2f,
0xe1, 0xc9, 0x38, 0x75, 0x12, 0x21, 0x13, 0x68, 0xc7, 0x17, 0x12, 0x6e, 0xc4, 0x74, 0x96, 0x37,
0x2e, 0xad, 0x85, 0xc2, 0x18, 0xc4, 0xd2, 0xe7, 0x98, 0xd6, 0x34, 0xa7, 0xec, 0x58, 0xd6, 0xb4,
0x2e, 0x65, 0x0c, 0xc3, 0xba, 0x50, 0x5b, 0x4f, 0x47, 0x52, 0x3b, 0xb8, 0xbf, 0xcd, 0xc2, 0x00,
0xcd, 0xf6, 0xfc, 0x9f, 0x6d, 0xff, 0x07, 0xcd, 0x75, 0x01, 0x40, 0xd3, 0x1f, 0x75, 0x84, 0xbf,
0x4b, 0x5c, 0xd4, 0xea, 0x6d, 0xb6, 0x16, 0xa4, 0x86, 0x8e, 0xeb, 0x82, 0x8e, 0x8b, 0xe2, 0x5f,
0xd0, 0x72, 0x0f, 0xec, 0x72, 0x47, 0xd6, 0x36, 0xa4, 0xdb, 0x02, 0x8d, 0x95, 0xb5, 0xa0, 0x95,
0x1e, 0x9d, 0x7b, 0x83, 0x97, 0xcb, 0x96, 0x75, 0x2c, 0x12, 0x27, 0x82, 0x97, 0x73, 0x2c, 0xd1,
0xe7, 0xcd, 0xb7, 0x75, 0xbc, 0x10, 0xbf, 0x89, 0xb0, 0x35, 0x3f, 0x1f, 0x99, 0x42, 0xf6, 0xea,
0x1a, 0xe6, 0x7a, 0xd8, 0xe7, 0x3a, 0xec, 0xf3, 0x3d, 0x64, 0x23, 0x6c, 0x72, 0x48, 0xa5, 0x17,
0xb2, 0x45, 0xe7, 0xc1, 0x22, 0x1a, 0x51, 0x49, 0xeb, 0x20, 0x91, 0xe0, 0x52, 0xed, 0xac, 0xce,
0x6e, 0x38, 0x31, 0x52, 0x1b, 0x1a, 0x77, 0xfc, 0x02, 0x27, 0x36, 0xaf, 0x59, 0xa3, 0x3a, 0xdc,
0xd1, 0x02, 0x0e, 0x05, 0xec, 0xfe, 0x47, 0xf6, 0x2e, 0xba, 0xef, 0x00, 0x6d, 0xd0, 0xb5, 0x72,
0x03, 0x41, 0x0c, 0x06, 0x60, 0x85, 0xd3, 0x5d, 0x9a, 0x30, 0x75, 0x6e, 0xcd, 0xf0, 0x00, 0x61,
0xea, 0x52, 0x6b, 0x0e, 0x64, 0x7b, 0x27, 0x0b, 0x9e, 0xd5, 0x9a, 0xde, 0x3e, 0x3a, 0x33, 0x35,
0xcb, 0xfb, 0xeb, 0x1b, 0xc1, 0x37, 0x71, 0x37, 0xce, 0x06, 0x64, 0x43, 0xdf, 0x93, 0x47, 0x1e,
0x95, 0x2b, 0x8d, 0x16, 0xa6, 0xce, 0xf4, 0x62, 0xab, 0x9c, 0x45, 0x1f, 0x67, 0xca, 0x61, 0xa2,
0x09, 0x0a, 0x7d, 0x4e, 0xe0, 0xe2, 0x4f, 0x31, 0xa3, 0x71, 0x19, 0x19, 0xb8, 0xf0, 0xd4, 0xa3,
0x38, 0x90, 0x87, 0x4b, 0xe7, 0x0c, 0x32, 0xf9, 0x81, 0xac, 0x2f, 0xe4, 0xc0, 0x28, 0x1b, 0x6b,
0x4c, 0xbb, 0x71, 0x80, 0x43, 0x65, 0xd4, 0xae, 0x38, 0x84, 0xd3, 0xf9, 0x77, 0x04, 0x30, 0xa2,
0xb0, 0x4e, 0xbb, 0xce, 0x18, 0x87, 0xe5, 0xca, 0xce, 0xf2, 0x8f, 0xed, 0xbe, 0xd6, 0xf0, 0x98,
0x23, 0x1e, 0x87, 0xaa, 0xad, 0x20, 0x5a, 0xa1, 0x44, 0x4b, 0x0a, 0x26, 0x5e, 0x65, 0x1d, 0x42,
0xe2, 0x9e, 0x75, 0x43, 0x38, 0x5b, 0xa1, 0x45, 0x4c, 0x96, 0x9d, 0xcc, 0xeb, 0x44, 0xf8, 0x56,
0x96, 0x86, 0x4c, 0x02, 0x35, 0x54, 0xa9, 0x6b, 0x2e, 0x95, 0x71, 0x91, 0x77, 0x8b, 0xda, 0xf9,
0x18, 0x5d, 0x88, 0xd1, 0x3a, 0xa4, 0x91, 0x9c, 0xe5, 0x1f, 0xa7, 0x49, 0x0c, 0x7b, 0x9f, 0xcf,
0xc8, 0xd5, 0x9d, 0xe0, 0xcb, 0x9c, 0x7a, 0x39, 0xa1, 0x9e, 0x2c, 0xe2, 0xa2, 0x0d, 0xde, 0xa5,
0xe7, 0x4a, 0xb5, 0x02, 0xa7, 0x2b, 0xca, 0x93, 0x99, 0xf2, 0x64, 0x43, 0xf9, 0x13, 0x0b, 0xe0,
0xb5, 0x82, 0xbf, 0x3a, 0x08, 0x68, 0x91, 0x08, 0x56, 0x25, 0x52, 0x0e, 0x39, 0xf5, 0x44, 0x56,
0x24, 0xd6, 0x52, 0x1a, 0x76, 0xb7, 0xbc, 0x90, 0xf7, 0x50, 0x26, 0x91, 0x21, 0xfc, 0x03, 0x7d,
0xcf, 0x85, 0x71, 0xc5, 0x30, 0x0c, 0x06, 0x60, 0x1d, 0x94, 0x99, 0x99, 0x3b, 0x40, 0x61, 0x8d,
0xf2, 0x02, 0xba, 0x80, 0x5e, 0xeb, 0x80, 0x94, 0x93, 0x52, 0x9c, 0xbe, 0xf6, 0x63, 0x0e, 0x98,
0xed, 0xef, 0x37, 0xb0, 0x8b, 0xe3, 0x82, 0xd0, 0x12, 0x25, 0x62, 0x4c, 0x84, 0x99, 0x92, 0xda,
0xd7, 0x65, 0x15, 0xb1, 0x13, 0x46, 0x8d, 0x52, 0x27, 0xf8, 0xed, 0x1a, 0x0e, 0x61, 0x37, 0x77,
0x66, 0x58, 0x4a, 0x4a, 0xa5, 0xef, 0x28, 0x55, 0x14, 0xd5, 0xa4, 0x08, 0x67, 0xb1, 0xba, 0xf4,
0x9d, 0x90, 0xac, 0x62, 0xf9, 0x46, 0xd8, 0x13, 0x29, 0xd1, 0x48, 0xbf, 0xc2, 0xe4, 0xae, 0x5f,
0x52, 0x3a, 0x8e, 0x0a, 0x4c, 0x3e, 0xa2, 0x1a, 0x61, 0xee, 0x8f, 0x54, 0x26, 0x42, 0x21, 0x0b,
0x5c, 0x37, 0x3e, 0x8b, 0xc2, 0x57, 0x9f, 0x16, 0xfb, 0xb2, 0x65, 0x6f, 0xf7, 0xb8, 0x93, 0x21,
0x6e, 0xa7, 0x9f, 0xdb, 0x1e, 0xe2, 0xe0, 0x45, 0xe5, 0xd1, 0x25, 0x2a, 0xe3, 0x28, 0x38, 0xf0,
0x04, 0x2c, 0xf4, 0xae, 0x05, 0x0b, 0x1d, 0x06, 0x36, 0xb0, 0xcd, 0xa8, 0xdd, 0xdd, 0xdf, 0x85,
0xf3, 0x5d, 0x54, 0xdc, 0xc2, 0x66, 0x21, 0x1a, 0xa1, 0xd4, 0x11, 0xb2, 0x20, 0xfd, 0xf8, 0x95,
0xc1, 0x32, 0x62, 0x13, 0x35, 0x58, 0xec, 0x45, 0x81, 0x85, 0xd6, 0x20, 0x2c, 0x0c, 0x24, 0x02,
0x78, 0xbb, 0x7d, 0xa2, 0xf1, 0x69, 0x76, 0x43, 0x9a, 0xd9, 0xbe, 0x34, 0xb3, 0xdd, 0x34, 0xab,
0x38, 0xd5, 0x9d, 0xeb, 0x73, 0x67, 0xdb, 0xee, 0xec, 0xa0, 0xfb, 0x0f, 0x62, 0x08, 0x72, 0xf4,
0x06, 0x14, 0x42, 0x17, 0xe9, 0x0d, 0x03, 0x31, 0x14, 0x80, 0x55, 0xae, 0xbb, 0x2a, 0xc3, 0x01,
0xba, 0x36, 0xfb, 0x3a, 0xfa, 0x0c, 0x72, 0x33, 0x86, 0x91, 0x23, 0x29, 0x70, 0xfb, 0x32, 0x53,
0x96, 0x03, 0x82, 0xff, 0x61, 0x1a, 0xc7, 0x09, 0xd6, 0x3c, 0x4e, 0xa5, 0x77, 0xec, 0x51, 0xca,
0xc6, 0x31, 0x56, 0x03, 0xc1, 0x4d, 0xbb, 0x18, 0x06, 0xb8, 0x59, 0x68, 0x05, 0x37, 0x2b, 0xd7,
0x3a, 0x08, 0x7a, 0xa7, 0x8a, 0x23, 0x37, 0x34, 0x42, 0x20, 0x34, 0x51, 0x69, 0x24, 0x70, 0x82,
0x95, 0xb8, 0xe6, 0x8e, 0x90, 0x74, 0xf2, 0xbc, 0x82, 0x1b, 0xd1, 0x24, 0x4d, 0xe0, 0x94, 0x2b,
0x25, 0x59, 0x92, 0xe0, 0x38, 0x37, 0x83, 0x23, 0xe6, 0x11, 0x5f, 0x2e, 0xe0, 0x1c, 0xbf, 0xbe,
0x05, 0x4a, 0x5e, 0x59, 0x20, 0x30, 0x92, 0xd1, 0xf9, 0x72, 0xc0, 0x7a, 0x56, 0x1a, 0xec, 0x65,
0x51, 0x8c, 0x6f, 0x63, 0x70, 0x60, 0x29, 0x91, 0xad, 0x44, 0xcf, 0x48, 0x6b, 0x23, 0x79, 0xfa,
0xf7, 0x52, 0xa8, 0x70, 0x2a, 0x65, 0x1f, 0xa7, 0x69, 0x94, 0xe0, 0x2a, 0xc9, 0xe7, 0x71, 0xf1,
0xfe, 0x1b, 0x4e, 0x17, 0xde, 0xb5, 0x8e, 0x9a, 0x8f, 0x9b, 0xdd, 0xac, 0x48, 0xe3, 0xdf, 0xc1,
0x64, 0x33, 0x12, 0x4f, 0xf6, 0x89, 0xfe, 0x3b, 0xfa, 0x8d, 0xfc, 0x4c, 0x7d, 0x62, 0xb9, 0x72,
0x88, 0xff, 0x5f, 0xf1, 0x36, 0xd9, 0x40, 0xd8, 0x0c, 0xcc, 0xa3, 0x28, 0x46, 0x1d, 0xd8, 0xb0,
0xfe, 0x0f, 0x78, 0xfa, 0x2d, 0x83, 0xef, 0xb9, 0x3e, 0x00, 0xad, 0x90, 0x35, 0x77, 0xc3, 0x40,
0x0c, 0x80, 0xb5, 0x95, 0x99, 0x99, 0xb7, 0x62, 0x68, 0x2e, 0x33, 0xe3, 0xa8, 0x27, 0xc7, 0x17,
0xf7, 0x0c, 0x27, 0xbf, 0x3b, 0xa7, 0xce, 0xbf, 0x0f, 0x73, 0x32, 0x76, 0x11, 0xc3, 0x27, 0xc1,
0xfb, 0xd9, 0x03, 0xfe, 0x48, 0xf3, 0x24, 0xcc, 0x2f, 0x7e, 0x92, 0x83, 0x59, 0x0e, 0x42, 0x52,
0x92, 0x15, 0x6a, 0xb2, 0x25, 0xa3, 0xe5, 0x0b, 0xd8, 0xcc, 0x1b, 0x0b, 0x26, 0x3d, 0x69, 0x0c,
0x06, 0x6c, 0x8b, 0x00, 0x26, 0xb5, 0x08, 0x05, 0x45, 0x42, 0xc3, 0x22, 0xfa, 0xac, 0x09, 0x39,
0x22, 0x54, 0x8c, 0xa2, 0x50, 0x8e, 0x29, 0xf2, 0xd1, 0x08, 0x65, 0x58, 0x1b, 0x98, 0x62, 0x0e,
0xca, 0x8e, 0xfe, 0x2b, 0x97, 0x4e, 0xd6, 0x82, 0x00, 0x1f, 0x65, 0x2d, 0x2e, 0x28, 0xc4, 0x0f,
0xf6, 0x49, 0x63, 0x73, 0xd6, 0xe0, 0x9d, 0xb2, 0x65, 0x96, 0x22, 0xd6, 0x07, 0x57, 0x1f, 0xaf,
0xcf, 0x1c, 0x57, 0x61, 0xea, 0x1c, 0x15, 0x84, 0xe9, 0x47, 0x7e, 0xa7, 0x83, 0xe7, 0x93, 0xcc,
0x7b, 0xa2, 0x2b, 0x85, 0xb1, 0xcc, 0x49, 0x98, 0xef, 0x6e, 0xd8, 0xab, 0xc5, 0xc1, 0x54, 0x16,
0x15, 0x24, 0x31, 0x26, 0x8f, 0x3f, 0xfa, 0x9e, 0xb8, 0x5e, 0x29, 0x1f, 0x6f, 0x3b, 0x71, 0xbc,
0x89, 0x35, 0x8f, 0x96, 0x96, 0xb6, 0x23, 0x50, 0x9b, 0x44, 0x32, 0x01, 0x13, 0x6d, 0x27, 0x8d,
0x24, 0xff, 0x73, 0x1a, 0x7c, 0x44, 0x14, 0x55, 0x06, 0xdd, 0x24, 0xca, 0x5f, 0x75, 0x1c, 0xa9,
0x9c, 0xd6, 0x73, 0x56, 0x1a, 0x7d, 0xc2, 0x84, 0x8a, 0x63, 0x18, 0x69, 0x66, 0x66, 0xba, 0x32,
0x43, 0x49, 0x2c, 0xbf, 0x2f, 0x99, 0xe8, 0x4b, 0xb6, 0x95, 0xcb, 0xfb, 0x3e, 0x6c, 0x55, 0xf8,
0x4a, 0x7d, 0x50, 0x03, 0x6f, 0x05, 0x41, 0x10, 0x9e, 0x06, 0xb5, 0x6d, 0xdb, 0x8e, 0x51, 0xc7,
0x35, 0xc2, 0xc9, 0x61, 0x7a, 0x39, 0xec, 0xce, 0xea, 0xe1, 0xdf, 0xd7, 0x78, 0x0e, 0x87, 0x9f,
0xe0, 0xde, 0x79, 0x2e, 0x66, 0x89, 0x57, 0x07, 0x78, 0x71, 0x7f, 0x73, 0xb0, 0x8f, 0x01, 0x0b,
0xe5, 0xc9, 0xcf, 0x91, 0xf1, 0xc2, 0x98, 0xb1, 0x10, 0xbf, 0xc6, 0xd0, 0x97, 0xc6, 0xd6, 0xa2,
0xe0, 0x90, 0x04, 0xf4, 0x65, 0x1c, 0x45, 0xb1, 0x8c, 0xd0, 0x90, 0x22, 0xcf, 0x91, 0x81, 0x3e,
0xb3, 0xff, 0xdf, 0x4c, 0x22, 0xfb, 0x96, 0x4c, 0x9e, 0x0c, 0x0a, 0xed, 0x1c, 0x0c, 0x31, 0x0b,
0xfc, 0x1e, 0xc0, 0x4c, 0xc5, 0xae, 0xff, 0xf7, 0x69, 0xac, 0x72, 0xf1, 0xf7, 0x54, 0x0d, 0x08,
0x6e, 0x6f, 0x77, 0x77, 0x97, 0xaa, 0x84, 0xfa, 0x19, 0xc1, 0x48, 0xce, 0xfa, 0xd0, 0x52, 0x22,
0xb6, 0xe5, 0x4f, 0x57, 0x0f, 0x66, 0x6c, 0x3c, 0x64, 0xe7, 0xa1, 0x64, 0xa4, 0xe2, 0xc7, 0x4c,
0x7a, 0x19, 0x5a, 0x92, 0x96, 0x8d, 0x85, 0xd6, 0x12, 0xc6, 0x96, 0xef, 0x21, 0xc0, 0x03, 0x9e,
0x92, 0x27, 0xf0, 0xfe, 0x00, 0xef, 0x73, 0xca, 0x90, 0x20, 0xbc, 0x7f, 0xd9, 0xdb, 0x3f, 0xde,
0xaf, 0xc9, 0xbd, 0x89, 0xca, 0xc2, 0xda, 0x57, 0x5c, 0x63, 0x25, 0x0a, 0xc6, 0xfe, 0x14, 0xcc,
0xa1, 0x6f, 0xe2, 0x30, 0x22, 0x24, 0xab, 0x24, 0x17, 0x60, 0xbe, 0xdc, 0xd7, 0x78, 0x89, 0x82,
0xc5, 0x0a, 0xcf, 0x4d, 0xfe, 0xa7, 0x8e, 0x06, 0xe4, 0xb3, 0x9f, 0xe4, 0x5d, 0x25, 0xb4, 0x5d,
0x7f, 0xb4, 0x43, 0x15, 0x58, 0xdd, 0x25, 0x3c, 0xef, 0x95, 0x50, 0x35, 0x7a, 0x43, 0x31, 0x0c,
0x56, 0xc6, 0x32, 0x33, 0x1e, 0xa1, 0xdc, 0xee, 0x5d, 0xcb, 0xbc, 0xe9, 0x33, 0x28, 0x8d, 0x1f,
0x58, 0xaf, 0x92, 0x03, 0xb7, 0x2f, 0x33, 0x67, 0xb2, 0x45, 0x3f, 0xc1, 0x99, 0x25, 0x53, 0xe2,
0xe9, 0xd5, 0xea, 0xda, 0xd6, 0x1a, 0x0a, 0x73, 0x89, 0x4a, 0xd2, 0x22, 0x41, 0xb6, 0xcf, 0x9f,
0xf2, 0x26, 0x25, 0xe8, 0xdd, 0xde, 0x42, 0xc7, 0x65, 0x65, 0x62, 0xe0, 0x88, 0x62, 0x7c, 0x60,
0xb4, 0x05, 0xc1, 0x32, 0x56, 0x0a, 0x03, 0x79, 0x50, 0xc5, 0x92, 0x3d, 0x95, 0x30, 0x20, 0x54,
0x91, 0x49, 0x24, 0x30, 0xf1, 0x09, 0x62, 0xf0, 0x0d, 0x1c, 0xa6, 0x3f, 0xcd, 0x6a, 0x14, 0x35,
0x19, 0xc1, 0xdd, 0x75, 0xd4, 0xce, 0xa3, 0x94, 0x77, 0x90, 0xa3, 0xaf, 0x90, 0xb3, 0x68, 0x25,
0xf8, 0x6b, 0x42, 0xd2, 0x2a, 0x72, 0x1b, 0x86, 0x77, 0xfe, 0xb7, 0x57, 0x6b, 0x84, 0x98, 0x1f,
0xb0, 0x27, 0xdc, 0x5f, 0xfd, 0xd6, 0xc6, 0x42, 0x53, 0xed, 0xf7, 0x36, 0x3e, 0xc8, 0xee, 0x5b,
0xfb, 0xfe, 0xba, 0xde, 0x2c, 0x0a, 0x58, 0x50, 0x92, 0x60, 0x8a, 0x27, 0xac, 0x85, 0x76, 0xa8,
0x87, 0x8f, 0x88, 0x27, 0x6f, 0xc1, 0x7c, 0x54, 0x37, 0x20, 0xef, 0x28, 0x06, 0x12, 0x49, 0x19,
0xa2, 0x29, 0xd0, 0x35, 0x4c, 0x82, 0xbe, 0xf5, 0x6e, 0xf5, 0x4e, 0x60, 0xc1, 0x62, 0x90, 0x93,
0xc1, 0xc8, 0x48, 0x9d, 0x44, 0xf2, 0x80, 0xa6, 0x14, 0x95, 0x45, 0x3f, 0xd8, 0xb9, 0x03, 0x74,
0x8e, 0x45, 0x76, 0xc4, 0x30, 0x0c, 0x40, 0xb5, 0x1e, 0x5a, 0x95, 0xf9, 0x06, 0x65, 0x3a, 0x41,
0x99, 0x79, 0xa5, 0x17, 0xdb, 0x9a, 0x89, 0x42, 0x4a, 0x65, 0x4f, 0x73, 0xfc, 0x32, 0xd7, 0xdd,
0x98, 0xfd, 0xff, 0x87, 0xcb, 0x94, 0xab, 0xfc, 0x58, 0x1c, 0xe1, 0xd1, 0x3a, 0x5a, 0x29, 0xeb,
0xa4, 0x62, 0xa9, 0x50, 0x13, 0xc7, 0x82, 0xa6, 0x20, 0x58, 0xc0, 0xda, 0xc3, 0x6c, 0x7f, 0x58,
0x14, 0x30, 0xeb, 0x49, 0x39, 0x79, 0x9e, 0x87, 0xde, 0xc0, 0x6c, 0xc3, 0x7d, 0x86, 0x6e, 0xce,
0xde, 0x63, 0x29, 0x8e, 0x4a, 0xe8, 0x9e, 0x53, 0x4d, 0x49, 0x20, 0x85, 0x51, 0x34, 0xca, 0x6e,
0x40, 0x48, 0xbe, 0xae, 0xa4, 0x81, 0xae, 0x8a, 0x94, 0xe8, 0x49, 0x1f, 0x48, 0xa1, 0x1b, 0x48,
0x4b, 0xae, 0x92, 0x02, 0x6d, 0x9a, 0x04, 0x68, 0x6f, 0x44, 0xb5, 0x2f, 0x8e, 0x9f, 0x74, 0xfd,
0xa0, 0xf7, 0xbe, 0xc1, 0xda, 0x9b, 0xf1, 0xdf, 0x14, 0x52, 0xd2, 0x8a, 0xc2, 0x6b, 0xf9, 0xff,
0xc5, 0xfa, 0x55, 0x2c, 0xe6, 0x0d, 0x89, 0xe5, 0x7d, 0x08, 0x3f, 0x1c, 0x13, 0xbf, 0xee, 0xda,
0xdb, 0x31, 0x27, 0xbe, 0x4a, 0xf0, 0xcd, 0x82, 0x71, 0x0d, 0x7e, 0xe7, 0x22, 0xc0, 0xcd, 0xee,
0xdd, 0xca, 0xd2, 0xca, 0x3a, 0x9a, 0xc4, 0x0d, 0x28, 0x0e, 0xed, 0x7d, 0xfd, 0x85, 0x86, 0x05,
0x9b, 0x72, 0x79, 0x79, 0x79, 0x09, 0x3f, 0xc3, 0xe1, 0x86, 0xe5, 0x52, 0x13, 0x9b, 0x93, 0x3e,
0xcd, 0xc7, 0x90, 0xd4, 0x90, 0x04, 0x00, 0x85, 0x51, 0x03, 0x92, 0x44, 0x31, 0x10, 0xed, 0xb5,
0x6d, 0x5f, 0x61, 0x7d, 0x80, 0xb5, 0xad, 0x62, 0xd7, 0x47, 0xcf, 0x56, 0x3e, 0xd2, 0xa9, 0xce,
0x1f, 0xdc, 0x7e, 0x6c, 0x16, 0xe2, 0xc7, 0xc0, 0xaf, 0xe2, 0x2f, 0x71, 0xbc, 0x90, 0xe4, 0xe9,
0x08, 0x85, 0x0c, 0x39, 0x09, 0x09, 0x46, 0x2c, 0x0e, 0x72, 0xe2, 0xa0, 0x66, 0xa4, 0x5c, 0x42,
0xa2, 0x9d, 0x08, 0x2d, 0x69, 0xcb, 0x62, 0x61, 0x9e, 0x39, 0x2e, 0x6e, 0x24, 0x43, 0x02, 0xb3,
0xd7, 0x4a, 0x87, 0xe8, 0x71, 0x6c, 0x1c, 0xad, 0x58, 0xa3, 0x38, 0xbe, 0x62, 0x74, 0x23, 0x82,
0xa5, 0x50, 0x59, 0x8b, 0x31, 0xfb, 0x14, 0x03, 0xfc, 0x29, 0x87, 0xf1, 0xf2, 0xa4, 0xeb, 0xc3,
0x1d, 0x34, 0x16, 0x36, 0xd3, 0xd6, 0x85, 0xcd, 0xac, 0x4a, 0x29, 0x98, 0x6c, 0x02, 0x4e, 0xd6,
0x1c, 0xc1, 0x54, 0x93, 0xe8, 0xf8, 0x79, 0x37, 0x22, 0xec, 0x8e, 0x44, 0x18, 0xd3, 0x92, 0x3a,
0x3b, 0xee, 0xae, 0x5d, 0x92, 0x9d, 0x69, 0x82, 0xcd, 0xd4, 0x05, 0x57, 0xd0, 0x15, 0xe5, 0xff,
0x13, 0x8a, 0x3d, 0x3e, 0x39, 0x86, 0xd9, 0x26, 0xfd, 0xb1, 0xcf, 0x41, 0x49, 0xba, 0x67, 0x58,
0xac, 0x51, 0x92, 0x35, 0x9a, 0xb3, 0x2d, 0x99, 0x26, 0x2b, 0xed, 0xc2, 0xe4, 0xef, 0xfd, 0x6b,
0x57, 0xf2, 0x83, 0x12, 0xf9, 0x6e, 0x2a, 0x1d, 0x45, 0xb0, 0x5b, 0xc4, 0x28, 0xa7, 0x38, 0x97,
0xa4, 0x76, 0xcb, 0xa5, 0xcd, 0x36, 0x09, 0xce, 0x1a, 0x47, 0x12, 0x95, 0x94, 0xe0, 0x94, 0x33,
0x8e, 0xf6, 0x4b, 0xff, 0x54, 0x37, 0x51, 0x00, 0x62, 0x88, 0xc8, 0x4c, 0xcc, 0x8f, 0x0f, 0x36,
0x8e, 0x0f, 0xf7, 0xf4, 0x8f, 0x07, 0x54, 0x25, 0xd7, 0x06, 0x08, 0xc0, 0x40, 0x00, 0x00, 0x7f,
0x80, 0xd0, 0x21, 0xdb, 0xc5, 0xdd, 0x3d, 0xe3, 0xa3, 0x15, 0xed, 0x5d, 0x57, 0x55, 0xb1, 0xa9,
0x3a, 0xe1, 0xdd, 0x49, 0xa3, 0x88, 0x1a, 0x35, 0x97, 0x03, 0xb7, 0xc2, 0x87, 0xea, 0xfb, 0xed,
0xa9, 0xcd, 0x09, 0x97, 0x52, 0x12, 0xf9, 0x01, 0x3c, 0xc8, 0xff, 0xa1, 0xa1, 0xf2, 0x28, 0x1d,
0xd0, 0x54, 0x3d, 0xb9, 0xcc, 0x22, 0x11, 0x96, 0x4d, 0x78, 0x01,
};
for (unsigned i = 0; i < sizeof(IDS) / sizeof(IDS[0]); i++) {
if (IDS[i] != target_id) continue;
unsigned group = 0;
while (i >= FIRSTS[group + 1]) group++;
uint8_t raw[512];
unsigned int produced = sizeof raw;
if (tinf_uncompress_exact(raw, &produced, NAMES + OFFSETS[group],
OFFSETS[group + 1] - OFFSETS[group]) != TINF_OK ||
produced != RAW_LENGTHS[group]) return nullptr;
const uint8_t* record = raw;
const uint8_t* end = raw + produced;
size_t previous_length = 0;
for (unsigned row = FIRSTS[group]; row <= i; row++) {
if (record == end) { buffer[0] = 0; return nullptr; }
size_t length = *record++;
if (length > previous_length) { buffer[0] = 0; return nullptr; }
while (record < end && *record) {
if (length + 1 < capacity) buffer[length] = (char)*record;
length++; record++;
}
if (record == end) { buffer[0] = 0; return nullptr; }
record++;
buffer[length < capacity ? length : capacity - 1] = 0;
if (row == i && length >= capacity) { buffer[0] = 0; return nullptr; }
previous_length = length;
}
return buffer;
}
return nullptr;
}
#else
// target_id -> env name, or nullptr if unknown. Linear scan; lookups are rare.
inline const char* ota_target_env_name(uint32_t target_id) {
#if OTA_TARGET_NAME_TABLE
@@ -662,5 +1079,10 @@ inline const char* ota_target_env_name(uint32_t target_id) {
#endif
return nullptr;
}
// The original flash-string API remains unchanged when compression is disabled.
inline const char* ota_target_env_name(uint32_t target_id, char*, size_t) {
return ota_target_env_name(target_id);
}
#endif
} } // namespace mesh::ota
+3 -1
View File
@@ -3,7 +3,9 @@
* several extra kilobytes with the embedded toolchains used by MeshCore.
*/
#include "../CompanionReaderConfig.h"
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST) || COMPANION_FEATURE_READER
#include "../ui/Nrf52FontConfig.h"
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST) || COMPANION_FEATURE_READER \
|| MESHCORE_FONT_DEFLATE
#if defined(__GNUC__) && !defined(__clang__)
/* nRF52's Arduino recipe defaults to -Ofast, which more than doubles this
* decoder. Keep the size policy local to tinf instead of changing MeshCore. */
File diff suppressed because it is too large. Load diff
+626
View File
@@ -0,0 +1,626 @@
// Generated by scripts/generate_compact_noto.py; do not edit.
// MeshFontProduction9: lossless storage of the licensed MeshFontTrial7 bitmaps.
// All 224 slots/189 drawable glyphs preserved at 12/17 px H cap height;
// H tops 3/5 px, line boxes 19/28 px. The unused initial Arial10 is not stored.
// Google Zopfli 1000; glyph-aligned chunks decode to at most 512 bytes.
// Source rasterizer: Pillow 12.3.0 / FreeType 2.14.3, threshold 128.
// Nominal source sizes 16/24 px, baselines 15/22 px; existing mode7 placement.
// Normal builds need no compressor, rasterizer or source TrueType font.
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "../ota/tinf/tinf.h"
namespace mesh { namespace ui { namespace noto {
static constexpr size_t kScratchSize = 512;
struct Font {
const uint8_t* descriptor;
const uint8_t* data;
const uint16_t* offsets;
const uint16_t* rawLengths;
uint16_t chunkCount;
uint16_t dataLength;
};
struct Glyph {
const uint8_t* data;
uint8_t width;
uint8_t length;
bool drawable;
bool valid;
};
/*
Compression derivative named MeshFontProduction9.
The following notice identifies the upstream rasterized font source.
Derived MeshFontTrial7; source Noto Sans Condensed, Version 2.000;GOOG;noto-source:20170915:90ef993387c0; ttfautohint (v1.7)
Source SHA256: 4efc2789b9fcaa87b6b55f19c25ac9a2919e4f9ca31d6b5b97bea7977d729d4f
Upstream: https://github.com/notofonts/noto-fonts
SPDX-License-Identifier: OFL-1.1
Full license/copyright follows; it applies to the bitmap font data.
Copyright 2015 Google Inc. All Rights Reserved.
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font Software,
subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
*/
// Authoritative raw bitmap SHA256: 0027b835b49dbf1745ad063814a2f3ea23aac9797f157a1ecc48adf09591668d
inline const Font& fontStorage1() {
static const uint8_t FONT_1_DATA[] = {
0x5d, 0xcb, 0x81, 0x86, 0x1b, 0x51, 0x14, 0x80, 0xe1, 0x6f, 0x92, 0xac, 0xdc, 0xc2, 0x76, 0x01,
0x17, 0xd1, 0xcc, 0xee, 0x13, 0x04, 0x54, 0x10, 0xc9, 0x4c, 0xb7, 0x5a, 0xe8, 0x43, 0xcc, 0x9a,
0x6a, 0x81, 0xda, 0x42, 0x0d, 0xb0, 0x03, 0xc8, 0x23, 0xe4, 0x51, 0x0a, 0xb8, 0x8f, 0x51, 0xc0,
0x3c, 0xc2, 0x2d, 0x30, 0x40, 0x7b, 0x11, 0x4a, 0xf9, 0x1c, 0xbf, 0x73, 0x1c, 0xcc, 0x5f, 0xcd,
0x4f, 0x18, 0x00, 0x06, 0x2b, 0xe3, 0x7b, 0xe3, 0xc9, 0x5c, 0xe2, 0xb3, 0xb4, 0xf5, 0xb3, 0xc4,
0x0a, 0xd3, 0xbd, 0xfa, 0xde, 0xfc, 0xc7, 0xee, 0x41, 0xfd, 0x96, 0x0d, 0xf2, 0x52, 0x58, 0x88,
0xbd, 0x29, 0xb2, 0x96, 0xd8, 0x1f, 0x69, 0xe9, 0xd9, 0xc0, 0xd1, 0xdc, 0x3b, 0x3f, 0xfa, 0xf5,
0xc1, 0xf3, 0x81, 0x3d, 0xdf, 0x68, 0x30, 0x20, 0xbd, 0x78, 0x4e, 0x55, 0x50, 0x21, 0xaa, 0xf2,
0x6f, 0xe3, 0x09, 0x35, 0x53, 0x65, 0x50, 0xa6, 0x1a, 0x16, 0x85, 0x69, 0x7b, 0x8d, 0xc2, 0xf5,
0xb6, 0xf2, 0x0f, 0x5d, 0xa1, 0x61, 0xe0, 0x56, 0xaa, 0x0c, 0x04, 0xe4, 0x93, 0xd8, 0x09, 0x4d,
0x99, 0xa5, 0xa5, 0xd7, 0xa8, 0xd9, 0x11, 0x99, 0x5f, 0x70, 0x57, 0xce, 0x83, 0xf0, 0x49, 0xfc,
0x28, 0xb7, 0x34, 0xd7, 0x5d, 0x5b, 0xb8, 0xf4, 0xf2, 0x91, 0x8d, 0xc0, 0xad, 0xf1, 0x95, 0x2e,
0xd8, 0x87, 0xf2, 0x48, 0xc0, 0xfc, 0x24, 0xb6, 0x42, 0xd1, 0x0b, 0x47, 0x36, 0x98, 0xb6, 0x76,
0xdf, 0x5d, 0x1a, 0xe7, 0xe2, 0xc4, 0x1b, 0x84, 0xa2, 0x13, 0x0e, 0xce, 0x6b, 0x33, 0x11, 0xf9,
0xe0, 0xd2, 0x0b, 0x8f, 0x85, 0xfc, 0x43, 0xb7, 0x41, 0x6e, 0xc5, 0xb2, 0x78, 0x27, 0x3e, 0xc8,
0x5b, 0x69, 0x8d, 0xd4, 0x15, 0x48, 0x53, 0x95, 0x6a, 0x58, 0xb2, 0x36, 0xde, 0x18, 0x83, 0x14,
0x34, 0x77, 0x18, 0x57, 0xff, 0xa3, 0x89, 0xc6, 0x50, 0x92, 0x1b, 0x96, 0x2c, 0x10, 0x8a, 0x2f,
0x2e, 0x95, 0xfc, 0x17, 0x65, 0x8f, 0x31, 0x6e, 0x83, 0x40, 0x10, 0x00, 0xe7, 0x4c, 0x14, 0xae,
0x0a, 0x94, 0x5b, 0x58, 0xe1, 0x0b, 0x2e, 0xa9, 0x30, 0x4e, 0x1a, 0x3f, 0x63, 0x95, 0x7c, 0x80,
0x74, 0x54, 0x0e, 0x4f, 0x48, 0x49, 0xe7, 0xaf, 0xdc, 0x33, 0x52, 0xf2, 0x04, 0xca, 0xeb, 0x1c,
0xaf, 0x40, 0x27, 0x11, 0x4b, 0xd3, 0xec, 0xdc, 0x68, 0x4f, 0x0b, 0x84, 0x23, 0xaa, 0x94, 0x81,
0xf1, 0xca, 0xf9, 0x87, 0xf3, 0xc8, 0xef, 0x95, 0xb2, 0x44, 0x85, 0x21, 0x47, 0xe1, 0x42, 0xc8,
0xe9, 0x9f, 0x8c, 0x90, 0x43, 0x83, 0x49, 0xe2, 0xb7, 0xe1, 0x4f, 0x86, 0x7c, 0x32, 0x37, 0xb0,
0x07, 0x42, 0x41, 0x57, 0x23, 0x8a, 0x6f, 0x0d, 0x69, 0x53, 0xba, 0x0a, 0xe5, 0x70, 0x60, 0xaa,
0xb6, 0x1b, 0x8c, 0x24, 0x76, 0x0b, 0x69, 0x59, 0xb9, 0x2c, 0x7b, 0x33, 0xe4, 0x02, 0x4d, 0x4a,
0x71, 0x24, 0x6c, 0xb4, 0x6e, 0xf9, 0x89, 0xdd, 0x9d, 0x78, 0xcb, 0xe2, 0x8d, 0x14, 0x67, 0x84,
0x9c, 0x6e, 0x8f, 0x74, 0x56, 0x24, 0xdd, 0x2e, 0x24, 0x31, 0x63, 0x77, 0xd3, 0x43, 0x07, 0xaf,
0x4c, 0x8e, 0x1e, 0xf3, 0xeb, 0xbb, 0xd9, 0xe0, 0xe0, 0x05, 0xea, 0xd5, 0x60, 0xf7, 0x6c, 0xae,
0x56, 0x1b, 0x43, 0xb1, 0xbd, 0x09, 0xc9, 0x98, 0x1d, 0x8f, 0xf5, 0xe4, 0xba, 0xda, 0xfd, 0xab,
0x9f, 0x91, 0x8a, 0xf9, 0x0b, 0x5a, 0x60, 0x56, 0xc6, 0x13, 0xfe, 0x1d, 0xff, 0x81, 0x34, 0xe0,
0x3d, 0x2c, 0x58, 0xcd, 0x0a, 0xb1, 0x22, 0x1e, 0x41, 0xa1, 0x35, 0xd0, 0xfb, 0x28, 0x10, 0x1d,
0x43, 0x05, 0xbd, 0x31, 0x14, 0x44, 0xf0, 0x7f, 0x6d, 0x8f, 0x81, 0x66, 0xc4, 0x60, 0x10, 0x84,
0xbf, 0x34, 0xe7, 0xf2, 0xa3, 0xae, 0x00, 0x41, 0xab, 0xd0, 0x07, 0xe8, 0x01, 0xa1, 0xb8, 0xe5,
0x4e, 0x5b, 0xf4, 0x19, 0x2a, 0xe8, 0x03, 0x04, 0x10, 0x94, 0x7f, 0x01, 0x7d, 0xac, 0x7d, 0x8c,
0x03, 0xe4, 0x31, 0x7e, 0x20, 0xed, 0x08, 0x41, 0x94, 0xb1, 0xec, 0xec, 0xec, 0x98, 0x69, 0xa1,
0xd4, 0x70, 0x82, 0x41, 0xb3, 0x54, 0x8c, 0x10, 0x0d, 0x7c, 0x0b, 0x71, 0xc7, 0x88, 0xd1, 0x0d,
0x4c, 0xf7, 0x78, 0x83, 0xf6, 0x8e, 0xd4, 0x27, 0x90, 0xac, 0x82, 0x4c, 0xd4, 0x8c, 0xd0, 0x62,
0xa4, 0x81, 0xf4, 0xc9, 0xcf, 0x99, 0x62, 0x74, 0x06, 0x94, 0xb9, 0x6a, 0xa9, 0xd2, 0x82, 0xf5,
0xe5, 0x00, 0x23, 0x98, 0x38, 0x74, 0xff, 0x53, 0x01, 0xd4, 0x4c, 0xf0, 0x0c, 0x83, 0x54, 0x5a,
0xa9, 0xfe, 0x03, 0xb7, 0xd0, 0x02, 0x0a, 0xf6, 0x81, 0x5d, 0x88, 0x27, 0x3c, 0x03, 0x25, 0x13,
0x2f, 0x98, 0x09, 0xd1, 0xe3, 0x27, 0xd0, 0x88, 0x5e, 0x84, 0xb0, 0xd9, 0xfd, 0xa8, 0x97, 0x85,
0xfb, 0x42, 0x46, 0x17, 0xfc, 0x0d, 0xf6, 0x06, 0x53, 0xd6, 0xed, 0x1d, 0x12, 0x12, 0xdc, 0x44,
0xbf, 0x33, 0x13, 0xfc, 0x58, 0x87, 0xe2, 0x52, 0x54, 0x1c, 0x43, 0x08, 0x96, 0x04, 0xd7, 0x0c,
0xbb, 0xeb, 0xdc, 0x2c, 0x67, 0x0e, 0xb0, 0xc7, 0x1f, 0x88, 0x01, 0x6c, 0xe1, 0x20, 0x32, 0xbe,
0xfe, 0x65, 0xfd, 0x05, 0xab, 0xc7, 0xf6, 0x2e, 0x6e, 0x93, 0x5a, 0x66, 0x8f, 0x40, 0xcc, 0x8d,
0x6f, 0xda, 0x6e, 0xda, 0x49, 0xb2, 0x1a, 0x06, 0x18, 0xe0, 0x67, 0xec, 0x55, 0x5d, 0xad, 0xfb,
0x05, 0x64, 0xcd, 0x25, 0xd4, 0x14, 0x51, 0x14, 0x00, 0xe0, 0x6f, 0x04, 0x77, 0x77, 0x6b, 0x34,
0xdc, 0xed, 0xe1, 0x14, 0xa4, 0xe1, 0x9a, 0x88, 0xd3, 0x70, 0x66, 0xe2, 0xf6, 0x44, 0x4f, 0xf4,
0x1e, 0xee, 0x21, 0x6d, 0x4f, 0x1b, 0x87, 0x46, 0x1c, 0x12, 0x6e, 0x0f, 0x87, 0xff, 0x9c, 0xef,
0xe9, 0xb5, 0x44, 0x7f, 0xc4, 0xd0, 0x4a, 0x09, 0x71, 0x84, 0x5b, 0x24, 0xd9, 0x56, 0xd1, 0x06,
0x31, 0x83, 0xc7, 0x34, 0xac, 0x17, 0x55, 0x10, 0x0b, 0xb8, 0x9f, 0xc5, 0x0c, 0x51, 0x70, 0x84,
0x86, 0xc3, 0xa2, 0x92, 0xc4, 0x2d, 0xdd, 0x06, 0xe6, 0xeb, 0x0e, 0x88, 0x5b, 0xa1, 0x8e, 0x19,
0xb5, 0x83, 0x95, 0xa1, 0x90, 0xcb, 0x25, 0xa9, 0x91, 0xae, 0x89, 0xd3, 0x22, 0x41, 0xad, 0xff,
0xe2, 0xed, 0xeb, 0x62, 0xa6, 0x02, 0xc0, 0xdb, 0x2f, 0x33, 0x30, 0x53, 0x31, 0x7c, 0x28, 0xfa,
0x96, 0x1a, 0x0a, 0x59, 0x45, 0x29, 0xab, 0x10, 0x1f, 0xab, 0x18, 0x2a, 0x74, 0x33, 0xf4, 0x1b,
0x6c, 0x5c, 0x68, 0x6f, 0x93, 0x77, 0xdc, 0x31, 0xb4, 0x56, 0x1e, 0x33, 0x33, 0x23, 0x21, 0x2d,
0xd7, 0xad, 0x12, 0x0b, 0xa5, 0x85, 0x79, 0x8f, 0x75, 0xd2, 0x62, 0xec, 0x5d, 0xac, 0x5f, 0x4c,
0x2b, 0x76, 0xb8, 0xbf, 0xd8, 0x4c, 0x00, 0x6f, 0x5f, 0xce, 0xc0, 0x70, 0xdc, 0xe4, 0x84, 0xd1,
0x37, 0x4f, 0xd8, 0x01, 0xcc, 0xe4, 0xcf, 0x25, 0x16, 0xe4, 0x91, 0x0b, 0x37, 0x9a, 0x9c, 0x77,
0xf7, 0x92, 0xcd, 0x17, 0xb2, 0xbd, 0x17, 0xf2, 0xcf, 0xd6, 0xad, 0x62, 0x2d, 0x5e, 0xf1, 0x9c,
0xb7, 0x30, 0x9f, 0x35, 0x6c, 0x62, 0x2e, 0xf9, 0x02, 0xe5, 0xbf, 0xd6, 0xab, 0xfd, 0xf1, 0xa7,
0xff, 0xca, 0x64, 0x7c, 0xde, 0x99, 0x63, 0xc6, 0xe7, 0x3c, 0xbb, 0xfc, 0xbb, 0x7f, 0xc9, 0x54,
0x1a, 0x46, 0xb2, 0x01, 0x8e, 0x66, 0xfd, 0xcd, 0xbc, 0xcb, 0x3a, 0x5e, 0xf0, 0x75, 0x8b, 0x18,
0x18, 0xe6, 0x01, 0x00, 0x45, 0xcd, 0x01, 0xa7, 0x14, 0x51, 0x14, 0xc0, 0xf1, 0xdf, 0xec, 0xdc,
0xde, 0xdc, 0x97, 0x65, 0x97, 0x92, 0x41, 0x12, 0x3d, 0x40, 0xd8, 0x80, 0x82, 0xcc, 0x0a, 0x02,
0x02, 0x10, 0x30, 0xe9, 0x0b, 0x0c, 0xa0, 0xf2, 0xbc, 0x5d, 0xfb, 0xa0, 0x8f, 0x75, 0xab, 0x3e,
0xd0, 0x10, 0x99, 0xf2, 0xb4, 0xed, 0x61, 0x58, 0xfe, 0xc6, 0x7f, 0xfe, 0xe7, 0x38, 0x17, 0x57,
0x55, 0x70, 0x57, 0x61, 0x4d, 0x46, 0x39, 0x36, 0x06, 0x3a, 0xc1, 0x46, 0xd9, 0x61, 0xac, 0x4d,
0x8d, 0xe0, 0x58, 0x67, 0xf1, 0x85, 0x3a, 0x90, 0xec, 0x53, 0x48, 0x98, 0x0b, 0xfe, 0x61, 0xe2,
0x6b, 0x60, 0x80, 0x17, 0x3c, 0x21, 0xcd, 0x62, 0x89, 0x35, 0x53, 0x4d, 0x4f, 0xcb, 0x85, 0x7d,
0xa5, 0xdf, 0x68, 0xaf, 0xd8, 0xf1, 0x74, 0x9e, 0xbf, 0x61, 0xe0, 0x31, 0x8d, 0x52, 0x19, 0x3a,
0xed, 0x07, 0xde, 0xf3, 0xce, 0xc2, 0xdb, 0xc0, 0xf4, 0x91, 0x57, 0x2c, 0xa9, 0x95, 0x8d, 0xcd,
0x73, 0xf9, 0xd9, 0x7c, 0xa2, 0x30, 0xd6, 0xda, 0x45, 0x59, 0x2e, 0xc4, 0x94, 0x3b, 0xb7, 0x2b,
0xbf, 0xb3, 0x31, 0xdb, 0xaf, 0xb8, 0xa6, 0x88, 0x1c, 0x7f, 0x63, 0x8e, 0xc9, 0xed, 0x9c, 0xf5,
0xdc, 0xf8, 0xde, 0xf8, 0x9c, 0x4e, 0x84, 0x78, 0x4d, 0x2f, 0xb2, 0x1f, 0x91, 0x7d, 0x89, 0xac,
0x3a, 0xe7, 0x6f, 0x8d, 0x4f, 0x29, 0x38, 0x89, 0x73, 0xfe, 0xd9, 0xb8, 0x89, 0xed, 0x10, 0x73,
0x76, 0x2d, 0x9e, 0xbc, 0xa7, 0x4d, 0xa6, 0x5d, 0x90, 0xb7, 0x33, 0x94, 0x95, 0xe1, 0xa5, 0xb6,
0x4f, 0xb9, 0xa4, 0xdc, 0x35, 0x6d, 0x87, 0xf1, 0xe8, 0xd7, 0xd1, 0xa3, 0x83, 0x75, 0x20, 0x42,
0xf0, 0xf0, 0x10, 0xed, 0xc1, 0x81, 0xb8, 0xf1, 0x77, 0xe7, 0x72, 0x1b, 0xdc, 0xdf, 0x12, 0xe1,
0xcf, 0x7c, 0x37, 0x1a, 0x97, 0x5d, 0x6c, 0xe4, 0x0e, 0x39, 0xf4, 0x14, 0xfe, 0x03, 0x6d, 0x8d,
0x81, 0x66, 0xc3, 0x50, 0x18, 0x46, 0x4f, 0x92, 0x6d, 0xbd, 0x9d, 0xda, 0x62, 0xa8, 0xb6, 0x43,
0x86, 0x3e, 0x40, 0x28, 0x04, 0xe8, 0xad, 0x45, 0x15, 0xc1, 0x30, 0x14, 0xd8, 0x7d, 0x84, 0x32,
0xb8, 0x18, 0xf9, 0x01, 0x05, 0x0c, 0xb0, 0xc7, 0xb9, 0x05, 0x0a, 0xd8, 0x03, 0x0c, 0x79, 0x84,
0x32, 0xc8, 0xc0, 0xe6, 0x67, 0x11, 0x63, 0x1c, 0xe1, 0x3b, 0x27, 0x7f, 0x72, 0x06, 0x37, 0xc2,
0xe7, 0x37, 0x63, 0x89, 0x60, 0x68, 0x69, 0x6b, 0x7d, 0x46, 0xb4, 0xb5, 0x62, 0x56, 0xca, 0xc4,
0x91, 0xe7, 0x34, 0x19, 0xfc, 0xea, 0x67, 0x38, 0x44, 0xc4, 0x57, 0xc4, 0x05, 0x5f, 0x6a, 0x80,
0xb0, 0xa4, 0xd9, 0xb1, 0x08, 0x8c, 0x85, 0x54, 0xc8, 0x83, 0x4e, 0xc9, 0xba, 0xa2, 0x22, 0x15,
0x8d, 0x8b, 0xbe, 0x34, 0x19, 0xbb, 0x82, 0x99, 0x63, 0x68, 0x95, 0x99, 0xd3, 0x19, 0xae, 0xbb,
0xc2, 0xad, 0x16, 0x2e, 0xff, 0x29, 0x53, 0x87, 0xb1, 0xca, 0xb4, 0x2f, 0x62, 0x90, 0x73, 0x48,
0x60, 0x80, 0x8c, 0xc0, 0x00, 0xcd, 0x13, 0x3b, 0xcf, 0xc4, 0x63, 0xd6, 0xec, 0x57, 0x78, 0x87,
0x2f, 0x38, 0xea, 0xdb, 0xa7, 0x25, 0xa7, 0x9a, 0x24, 0x70, 0x21, 0x20, 0x10, 0x74, 0x42, 0x9b,
0xd1, 0x2e, 0xc1, 0x11, 0x5b, 0x12, 0x0b, 0x4e, 0x67, 0xa7, 0x13, 0x47, 0x64, 0x95, 0xf8, 0x8f,
0x8e, 0x1c, 0x58, 0xb4, 0xa8, 0x36, 0xe0, 0x21, 0x44, 0xc4, 0x35, 0x87, 0x04, 0x0f, 0x13, 0xa0,
0xad, 0x95, 0x3b, 0xa3, 0xb8, 0x11, 0x61, 0x00, 0xfa, 0xc3, 0x02, 0x8c, 0x65, 0x7f, 0x4f, 0xbb,
0x26, 0x7f, 0x81, 0x14, 0xc0, 0x53, 0x55, 0x6c, 0x4b, 0xde, 0xe7, 0x48, 0x8d, 0x0a, 0x6c, 0xc5,
0x43, 0xc9, 0xb1, 0x17, 0xdb, 0x8a, 0x8d, 0x0a, 0x5e, 0x55, 0xa4, 0x9e, 0x4d, 0x77, 0xf2, 0xa6,
0x02, 0x15, 0xd8, 0x92, 0x30, 0x67, 0xdf, 0x9f, 0x3c, 0x96, 0x7c, 0xe8, 0x37, 0x7e, 0x00, 0x55,
0xce, 0x21, 0x6c, 0x83, 0x40, 0x14, 0x87, 0xf1, 0x8f, 0x40, 0xc6, 0x89, 0x09, 0x64, 0x67, 0x36,
0x33, 0x9f, 0xf9, 0x2d, 0xe1, 0x09, 0xb2, 0x21, 0x2a, 0x90, 0x95, 0x47, 0x82, 0xcf, 0xe4, 0xdc,
0x21, 0x2b, 0x2b, 0xeb, 0xbd, 0xd7, 0x4f, 0x9e, 0x98, 0x40, 0xe2, 0xf5, 0x44, 0xe5, 0x5c, 0x9b,
0x77, 0x29, 0x21, 0x24, 0x7f, 0xc3, 0x8f, 0x8f, 0x17, 0x00, 0x7e, 0x90, 0x3d, 0xd2, 0xa0, 0xaf,
0x8c, 0x35, 0x3a, 0x20, 0x8d, 0x6d, 0xfc, 0x82, 0x07, 0x48, 0xe6, 0x0b, 0xd1, 0x42, 0xa4, 0x14,
0xd2, 0xf3, 0xef, 0xc0, 0xa1, 0x61, 0xda, 0x36, 0x03, 0x5d, 0x43, 0xdc, 0xd8, 0x3c, 0xd0, 0x26,
0x3b, 0xad, 0x16, 0xd3, 0x7d, 0x6d, 0x38, 0x9a, 0x39, 0x98, 0x03, 0x15, 0x30, 0x05, 0x5c, 0x05,
0x31, 0xe0, 0xa0, 0x72, 0xa0, 0x01, 0xc0, 0x01, 0xd4, 0x9c, 0x7a, 0xce, 0x62, 0xfb, 0xeb, 0xed,
0x48, 0x7a, 0x7b, 0xce, 0x69, 0xa1, 0x83, 0x09, 0x8e, 0x61, 0xf9, 0x53, 0x5a, 0xa1, 0x13, 0xf4,
0x1b, 0x5e, 0x16, 0x33, 0x38, 0x6c, 0x6c, 0xb2, 0xce, 0x36, 0xad, 0x36, 0x2f, 0xdf, 0xce, 0xab,
0x45, 0x8f, 0x88, 0x2d, 0xde, 0x8d, 0xcc, 0xe6, 0x9f, 0x20, 0x5b, 0xc6, 0x78, 0xb5, 0xd3, 0xb2,
0x47, 0x3e, 0xd1, 0x1e, 0xb1, 0x4e, 0x6b, 0xf0, 0x38, 0xa1, 0x12, 0x78, 0x43, 0xc3, 0x62, 0x06,
0x3b, 0xc1, 0xad, 0x56, 0x59, 0x97, 0xd2, 0xd5, 0x9c, 0x07, 0x81, 0x7b, 0xa7, 0x14, 0x5a, 0x16,
0x7c, 0xe4, 0xbb, 0x4b, 0xf6, 0xf8, 0x8c, 0xe6, 0xc0, 0xff, 0xb5, 0xd4, 0x77, 0x44, 0x6c, 0xea,
0x19, 0x6b, 0xa5, 0x88, 0x29, 0xe2, 0x92, 0x39, 0x8b, 0x6e,
};
static const uint8_t FONT_1_DESCRIPTOR[] = {
0x10, 0x13, 0x20, 0xe0, 0xff, 0xff, 0x00, 0x03, 0x00, 0x00, 0x08, 0x04, 0x00, 0x08, 0x0d, 0x06,
0x00, 0x15, 0x17, 0x08, 0x00, 0x2c, 0x14, 0x08, 0x00, 0x40, 0x20, 0x0b, 0x00, 0x60, 0x1a, 0x09,
0x00, 0x7a, 0x04, 0x03, 0x00, 0x7e, 0x0c, 0x05, 0x00, 0x8a, 0x0b, 0x05, 0x00, 0x95, 0x10, 0x07,
0x00, 0xa5, 0x14, 0x08, 0x00, 0xb9, 0x08, 0x04, 0x00, 0xc1, 0x0b, 0x05, 0x00, 0xcc, 0x08, 0x04,
0x00, 0xd4, 0x10, 0x06, 0x00, 0xe4, 0x14, 0x08, 0x00, 0xf8, 0x0e, 0x08, 0x01, 0x06, 0x14, 0x08,
0x01, 0x1a, 0x14, 0x08, 0x01, 0x2e, 0x14, 0x08, 0x01, 0x42, 0x14, 0x08, 0x01, 0x56, 0x14, 0x08,
0x01, 0x6a, 0x13, 0x08, 0x01, 0x7d, 0x14, 0x08, 0x01, 0x91, 0x14, 0x08, 0x01, 0xa5, 0x08, 0x04,
0x01, 0xad, 0x08, 0x04, 0x01, 0xb5, 0x14, 0x08, 0x01, 0xc9, 0x14, 0x08, 0x01, 0xdd, 0x14, 0x08,
0x01, 0xf1, 0x0d, 0x06, 0x02, 0x00, 0x20, 0x0c, 0x02, 0x20, 0x17, 0x08, 0x02, 0x37, 0x17, 0x08,
0x02, 0x4e, 0x14, 0x08, 0x02, 0x62, 0x17, 0x09, 0x02, 0x79, 0x11, 0x07, 0x02, 0x8a, 0x11, 0x07,
0x02, 0x9b, 0x1a, 0x0a, 0x02, 0xb5, 0x17, 0x0a, 0x02, 0xcc, 0x0b, 0x05, 0x02, 0xd7, 0x0b, 0x05,
0x02, 0xe2, 0x17, 0x08, 0x02, 0xf9, 0x11, 0x07, 0x03, 0x0a, 0x20, 0x0c, 0x03, 0x2a, 0x1a, 0x0a,
0x03, 0x44, 0x1a, 0x0a, 0x03, 0x5e, 0x14, 0x08, 0x03, 0x72, 0x1a, 0x0a, 0x03, 0x8c, 0x17, 0x08,
0x03, 0xa3, 0x14, 0x07, 0x03, 0xb7, 0x13, 0x07, 0x03, 0xca, 0x17, 0x09, 0x03, 0xe1, 0x16, 0x08,
0x04, 0x00, 0x22, 0x0d, 0x04, 0x22, 0x14, 0x07, 0x04, 0x36, 0x13, 0x07, 0x04, 0x49, 0x11, 0x07,
0x04, 0x5a, 0x0f, 0x05, 0x04, 0x69, 0x11, 0x06, 0x04, 0x7a, 0x0c, 0x05, 0x04, 0x86, 0x14, 0x08,
0x04, 0x9a, 0x15, 0x07, 0x04, 0xaf, 0x07, 0x04, 0x04, 0xb6, 0x11, 0x07, 0x04, 0xc7, 0x14, 0x08,
0x04, 0xdb, 0x10, 0x06, 0x04, 0xeb, 0x14, 0x08, 0x04, 0xff, 0x14, 0x07, 0x05, 0x13, 0x0d, 0x05,
0x05, 0x20, 0x15, 0x08, 0x05, 0x35, 0x14, 0x08, 0x05, 0x49, 0x05, 0x04, 0x05, 0x4e, 0x06, 0x04,
0x05, 0x54, 0x14, 0x07, 0x05, 0x68, 0x05, 0x04, 0x05, 0x6d, 0x20, 0x0c, 0x05, 0x8d, 0x14, 0x08,
0x05, 0xa1, 0x14, 0x08, 0x05, 0xb5, 0x14, 0x08, 0x05, 0xc9, 0x15, 0x08, 0x05, 0xde, 0x0d, 0x05,
0x05, 0xeb, 0x0e, 0x06, 0x06, 0x00, 0x0b, 0x05, 0x06, 0x0b, 0x14, 0x08, 0x06, 0x1f, 0x11, 0x07,
0x06, 0x30, 0x1d, 0x0b, 0x06, 0x4d, 0x11, 0x07, 0x06, 0x5e, 0x11, 0x07, 0x06, 0x6f, 0x0e, 0x06,
0x06, 0x7d, 0x0f, 0x06, 0x06, 0x8c, 0x0c, 0x07, 0x06, 0x98, 0x0e, 0x06, 0x06, 0xa6, 0x14, 0x08,
0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10,
0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10,
0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10,
0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10,
0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10,
0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10,
0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10,
0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x10,
0xff, 0xff, 0x00, 0x10, 0xff, 0xff, 0x00, 0x03, 0x06, 0xba, 0x09, 0x04, 0x06, 0xc3, 0x11, 0x08,
0x06, 0xd4, 0x14, 0x08, 0x06, 0xe8, 0x17, 0x09, 0x06, 0xff, 0x13, 0x08, 0x07, 0x12, 0x0c, 0x07,
0x07, 0x1e, 0x11, 0x07, 0x07, 0x2f, 0x10, 0x09, 0x07, 0x3f, 0x23, 0x0d, 0x07, 0x62, 0x0a, 0x05,
0x07, 0x6c, 0x14, 0x08, 0x07, 0x80, 0x14, 0x08, 0x07, 0x94, 0x0b, 0x05, 0x07, 0x9f, 0x23, 0x0d,
0x07, 0xc2, 0x13, 0x07, 0x07, 0xd5, 0x0d, 0x06, 0x07, 0xe2, 0x14, 0x08, 0x07, 0xf6, 0x0a, 0x05,
0x08, 0x00, 0x0b, 0x05, 0x08, 0x0b, 0x07, 0x04, 0x08, 0x12, 0x14, 0x08, 0x08, 0x26, 0x15, 0x09,
0x08, 0x3b, 0x08, 0x04, 0x08, 0x43, 0x09, 0x03, 0x08, 0x4c, 0x0a, 0x05, 0x08, 0x56, 0x0d, 0x05,
0x08, 0x63, 0x14, 0x08, 0x08, 0x77, 0x20, 0x0c, 0x08, 0x97, 0x20, 0x0c, 0x08, 0xb7, 0x23, 0x0c,
0x08, 0xda, 0x0f, 0x06, 0x08, 0xe9, 0x17, 0x08, 0x09, 0x00, 0x17, 0x08, 0x09, 0x17, 0x17, 0x08,
0x09, 0x2e, 0x17, 0x08, 0x09, 0x45, 0x17, 0x08, 0x09, 0x5c, 0x17, 0x08, 0x09, 0x73, 0x1d, 0x0b,
0x09, 0x90, 0x14, 0x08, 0x09, 0xa4, 0x11, 0x07, 0x09, 0xb5, 0x11, 0x07, 0x09, 0xc6, 0x11, 0x07,
0x09, 0xd7, 0x11, 0x07, 0x09, 0xe8, 0x0b, 0x05, 0x09, 0xf3, 0x0b, 0x05, 0x0a, 0x00, 0x0b, 0x05,
0x0a, 0x0b, 0x0b, 0x05, 0x0a, 0x16, 0x17, 0x09, 0x0a, 0x2d, 0x1a, 0x0a, 0x0a, 0x47, 0x1a, 0x0a,
0x0a, 0x61, 0x1a, 0x0a, 0x0a, 0x7b, 0x1a, 0x0a, 0x0a, 0x95, 0x1a, 0x0a, 0x0a, 0xaf, 0x1a, 0x0a,
0x0a, 0xc9, 0x14, 0x08, 0x0a, 0xdd, 0x1a, 0x0a, 0x0a, 0xf7, 0x17, 0x09, 0x0b, 0x0e, 0x17, 0x09,
0x0b, 0x25, 0x17, 0x09, 0x0b, 0x3c, 0x17, 0x09, 0x0b, 0x53, 0x13, 0x07, 0x0b, 0x66, 0x14, 0x08,
0x0b, 0x7a, 0x17, 0x08, 0x0b, 0x91, 0x11, 0x07, 0x0b, 0xa2, 0x11, 0x07, 0x0b, 0xb3, 0x11, 0x07,
0x0b, 0xc4, 0x11, 0x07, 0x0b, 0xd5, 0x11, 0x07, 0x0b, 0xe6, 0x11, 0x07, 0x0c, 0x00, 0x20, 0x0b,
0x0c, 0x20, 0x10, 0x06, 0x0c, 0x30, 0x14, 0x07, 0x0c, 0x44, 0x14, 0x07, 0x0c, 0x58, 0x14, 0x07,
0x0c, 0x6c, 0x14, 0x07, 0x0c, 0x80, 0x07, 0x04, 0x0c, 0x87, 0x07, 0x04, 0x0c, 0x8e, 0x0a, 0x04,
0x0c, 0x98, 0x0a, 0x04, 0x0c, 0xa2, 0x14, 0x08, 0x0c, 0xb6, 0x14, 0x08, 0x0c, 0xca, 0x14, 0x08,
0x0c, 0xde, 0x14, 0x08, 0x0c, 0xf2, 0x14, 0x08, 0x0d, 0x06, 0x14, 0x08, 0x0d, 0x1a, 0x14, 0x08,
0x0d, 0x2e, 0x14, 0x08, 0x0d, 0x42, 0x14, 0x08, 0x0d, 0x56, 0x14, 0x08, 0x0d, 0x6a, 0x14, 0x08,
0x0d, 0x7e, 0x14, 0x08, 0x0d, 0x92, 0x14, 0x08, 0x0d, 0xa6, 0x11, 0x07, 0x0d, 0xb7, 0x14, 0x08,
0x0d, 0xcb, 0x11, 0x07,
};
static const uint16_t FONT_1_OFFSETS[] = {
0, 276, 504, 737, 1012, 1278, 1551, 1786,
};
static const uint16_t FONT_1_LENGTHS[] = {
510, 503, 505, 512, 510, 503, 476,
};
static const Font FONT_1 = { FONT_1_DESCRIPTOR, FONT_1_DATA, FONT_1_OFFSETS, FONT_1_LENGTHS, 7, 1786 };
return FONT_1;
}
// Authoritative raw bitmap SHA256: de86a8dff7e9e25bed0bd0c782c9db31868b052b02001408a85e9d01bef3ab7f
inline const Font& fontStorage2() {
static const uint8_t FONT_2_DATA[] = {
0x65, 0xcf, 0x01, 0xc6, 0x15, 0x51, 0x18, 0x06, 0xe0, 0x67, 0x9a, 0x38, 0xc5, 0x5f, 0x07, 0xe0,
0xa6, 0x6a, 0xa6, 0x1d, 0x4c, 0xc0, 0xfc, 0xc4, 0x6c, 0x65, 0x76, 0x50, 0x3b, 0xb8, 0x07, 0x01,
0xca, 0xbf, 0x95, 0x96, 0x70, 0x08, 0x41, 0x8b, 0x38, 0x4b, 0x18, 0xc0, 0x45, 0xee, 0xf4, 0x5d,
0x18, 0x74, 0xf1, 0xf8, 0xde, 0xf7, 0x05, 0x3e, 0x80, 0xb6, 0x3f, 0x0a, 0x30, 0xa2, 0xa5, 0x00,
0x40, 0x74, 0x81, 0x8e, 0x6f, 0xe1, 0xfb, 0x23, 0x7f, 0x17, 0x6d, 0xef, 0xb4, 0x1f, 0xb7, 0xfe,
0x91, 0x3f, 0x8b, 0xb2, 0xf7, 0xda, 0xd3, 0xad, 0x87, 0xe7, 0x20, 0x3f, 0x28, 0xc3, 0x0b, 0xf5,
0xcd, 0x49, 0x9d, 0x4e, 0xb6, 0xfd, 0xac, 0xae, 0xa7, 0xf0, 0xa0, 0xd6, 0xac, 0x94, 0x04, 0x65,
0xa1, 0x9e, 0x59, 0xab, 0x30, 0x46, 0x9e, 0x63, 0xcb, 0xd4, 0x9e, 0x0d, 0x1f, 0x28, 0xbf, 0xb3,
0xb6, 0x0d, 0xc6, 0x69, 0x22, 0xd8, 0x16, 0x6a, 0x06, 0x25, 0x2b, 0xbf, 0x06, 0xf5, 0xcb, 0x6c,
0xfd, 0x3c, 0x59, 0xbf, 0x86, 0xcb, 0xa4, 0x3e, 0xbd, 0x55, 0xd2, 0x80, 0xd7, 0xd4, 0x25, 0x7c,
0xc2, 0x78, 0xff, 0xe4, 0x96, 0xb8, 0x2e, 0x4a, 0xbe, 0x68, 0x5d, 0xe9, 0x47, 0x9e, 0xc1, 0xed,
0x36, 0xa5, 0x8f, 0x9d, 0xeb, 0x99, 0x2d, 0x41, 0xe9, 0xc2, 0xcb, 0xf0, 0x9e, 0x9f, 0x89, 0xad,
0xa7, 0xe4, 0xf0, 0x2a, 0x74, 0x01, 0x4c, 0xff, 0xd9, 0x93, 0xc0, 0xe4, 0x00, 0xb5, 0xe3, 0x12,
0x66, 0x50, 0xba, 0x7b, 0x00, 0xb3, 0x70, 0x44, 0x03, 0x35, 0x71, 0x39, 0xa2, 0x06, 0xae, 0xbd,
0xb2, 0x67, 0xb5, 0x7b, 0x67, 0x35, 0x1d, 0x9a, 0x59, 0xdd, 0x07, 0xf6, 0xf4, 0x0f, 0x85, 0xcf,
0x01, 0x66, 0x03, 0x41, 0x18, 0x05, 0xe0, 0x6f, 0x3b, 0x24, 0x40, 0x36, 0x80, 0xa5, 0xd5, 0xf4,
0x08, 0x43, 0xd0, 0x05, 0x49, 0x8e, 0x32, 0x47, 0x08, 0x20, 0x28, 0x5d, 0x8a, 0x42, 0x0f, 0x35,
0x37, 0x28, 0x40, 0x00, 0x73, 0x84, 0x02, 0x16, 0xd5, 0x6d, 0x87, 0x15, 0x0b, 0x14, 0x1f, 0x3c,
0x8f, 0xf7, 0xff, 0x00, 0x02, 0x43, 0x43, 0x46, 0xf9, 0x39, 0x2a, 0xd3, 0x11, 0x4e, 0xa2, 0xac,
0x97, 0x1c, 0x24, 0xcf, 0xd2, 0xb0, 0x97, 0x4a, 0x54, 0x2e, 0x51, 0x3e, 0x46, 0xd6, 0x11, 0x4e,
0xb6, 0xb2, 0x28, 0xc5, 0x85, 0x73, 0x54, 0x5e, 0x3a, 0xf9, 0x73, 0xc7, 0x5b, 0x4b, 0x20, 0x07,
0x4a, 0xa0, 0x0f, 0x6c, 0x82, 0x21, 0x04, 0x59, 0x50, 0xc7, 0x2a, 0xc2, 0x0c, 0x5b, 0x65, 0x17,
0x95, 0xfb, 0x28, 0x75, 0x0b, 0xb1, 0x93, 0xbe, 0x76, 0xe4, 0x35, 0x18, 0x03, 0x53, 0x2b, 0xef,
0x1f, 0xe4, 0xed, 0xa2, 0xd6, 0xf7, 0xb5, 0x46, 0x69, 0x21, 0xdd, 0xcc, 0xaf, 0xe4, 0x56, 0x1a,
0x1b, 0xe9, 0x50, 0x1b, 0x94, 0x06, 0xf5, 0xca, 0x7a, 0xad, 0xf2, 0xdd, 0x4b, 0x97, 0xc5, 0x17,
0x63, 0xac, 0xb9, 0xe1, 0xbd, 0x05, 0x75, 0xfd, 0x35, 0x2a, 0x29, 0x4a, 0x79, 0x76, 0xea, 0xe4,
0xf4, 0x28, 0x4f, 0x2d, 0x53, 0x03, 0x60, 0xd3, 0x57, 0x40, 0x6e, 0x6c, 0xc6, 0x3f, 0x3d, 0x78,
0xc2, 0x79, 0x76, 0xc5, 0x47, 0xd5, 0xb0, 0xaa, 0x02, 0xe1, 0x0e, 0x35, 0xfa, 0x1f, 0xc2, 0x8a,
0x55, 0xa8, 0x6e, 0xb1, 0xab, 0x6a, 0x5e, 0xf9, 0x05, 0x65, 0xce, 0x01, 0x66, 0xc3, 0x50, 0x18,
0xc0, 0xf1, 0x5f, 0x94, 0x04, 0x58, 0x7a, 0x80, 0x91, 0x1e, 0xe1, 0x01, 0x32, 0x58, 0xc3, 0x2e,
0xf2, 0x76, 0x83, 0x02, 0x06, 0xc8, 0xec, 0x00, 0x3b, 0x42, 0xaf, 0xf2, 0x66, 0x0c, 0xa0, 0x47,
0xd8, 0x3b, 0x42, 0x01, 0x45, 0x35, 0x4b, 0x84, 0x2d, 0x16, 0xfc, 0x78, 0x92, 0xef, 0xfb, 0xfc,
0x3b, 0xc4, 0x89, 0xad, 0x98, 0x1f, 0xc4, 0xc3, 0x56, 0x6e, 0x49, 0x0d, 0x7d, 0x05, 0xce, 0x1b,
0x6e, 0x0d, 0x55, 0xab, 0x2f, 0x0e, 0xd2, 0xf9, 0x59, 0x77, 0xfd, 0x14, 0xef, 0xbe, 0xc4, 0x72,
0x32, 0xbe, 0x6f, 0x1f, 0xa3, 0x27, 0x49, 0xa9, 0x2f, 0x4a, 0xde, 0xa7, 0x9d, 0x82, 0x80, 0x47,
0x72, 0xc3, 0xb5, 0x90, 0x8e, 0x85, 0xfc, 0x36, 0x7a, 0x9d, 0xdf, 0xd3, 0x88, 0x5c, 0x63, 0x8f,
0x00, 0x90, 0x87, 0xbd, 0x49, 0x0c, 0xc1, 0x52, 0x7e, 0x09, 0xd2, 0xd0, 0xe8, 0x4f, 0x35, 0x36,
0xe0, 0x82, 0xa1, 0xd2, 0x1f, 0x6b, 0xa9, 0xb8, 0x97, 0xb5, 0xa2, 0x60, 0xa9, 0xb3, 0x5d, 0x5d,
0x16, 0x2c, 0x65, 0xad, 0x69, 0xbd, 0xff, 0xae, 0x19, 0x2a, 0x2e, 0xd6, 0x2d, 0x2b, 0x3b, 0xbb,
0xd5, 0x58, 0xb4, 0xb2, 0xb3, 0x0c, 0x3d, 0xcd, 0xa1, 0x69, 0x19, 0x1a, 0xff, 0xa4, 0xbc, 0xd7,
0xe5, 0xe6, 0xff, 0x5d, 0x81, 0xb5, 0xc5, 0x7f, 0x53, 0x8d, 0x28, 0xfc, 0x7e, 0x9a, 0xeb, 0xca,
0x99, 0x2a, 0x0f, 0xc3, 0x66, 0x54, 0xfc, 0x00, 0x84, 0xd0, 0x01, 0x66, 0x04, 0x31, 0x14, 0x80,
0xe1, 0x6f, 0x1a, 0x04, 0x90, 0x02, 0x8a, 0x65, 0x7a, 0x84, 0x01, 0x2c, 0x6a, 0xe7, 0x2a, 0x39,
0xc2, 0x9c, 0xa0, 0xe6, 0x06, 0xbd, 0x4a, 0x8f, 0x10, 0xa0, 0x97, 0x28, 0x72, 0x84, 0x05, 0x2c,
0xca, 0x34, 0x0c, 0x42, 0x83, 0xe2, 0x83, 0xe7, 0xe5, 0x7f, 0x04, 0xa0, 0x1e, 0xab, 0x86, 0x8c,
0x2b, 0x7e, 0xf0, 0x15, 0xec, 0x21, 0xa9, 0x6e, 0xb2, 0x2b, 0x5e, 0x87, 0x75, 0xcb, 0x68, 0x5c,
0xab, 0x81, 0x7d, 0x76, 0x56, 0x6b, 0xc4, 0x8a, 0xb7, 0x73, 0x78, 0x0f, 0xbc, 0x53, 0x22, 0x75,
0xd2, 0xd3, 0xc6, 0x44, 0xc2, 0x0d, 0x0f, 0xf4, 0x41, 0x7f, 0x82, 0x07, 0x8e, 0xa8, 0x7c, 0x24,
0xc5, 0x8b, 0x6c, 0x19, 0x94, 0xe9, 0x62, 0x3f, 0x12, 0x47, 0x64, 0xfb, 0x7b, 0x28, 0x17, 0x5d,
0xa6, 0x6c, 0xcd, 0x8a, 0xf9, 0xff, 0xfe, 0xd6, 0xdc, 0xa7, 0x32, 0x7d, 0x87, 0xd6, 0x0f, 0x8e,
0xf8, 0x64, 0xec, 0x67, 0xa7, 0xaa, 0x09, 0xca, 0x67, 0x52, 0xe6, 0x1b, 0xa9, 0xff, 0x70, 0x7c,
0x6e, 0xa3, 0x45, 0xb9, 0x2c, 0xf2, 0xd2, 0x6c, 0x4d, 0x6e, 0x6a, 0x3b, 0x58, 0x66, 0xf6, 0x98,
0xd1, 0xe9, 0xf5, 0xdf, 0x21, 0x8b, 0x03, 0x00, 0x8d, 0xcc, 0x01, 0x66, 0xc4, 0x40, 0x14, 0x80,
0xe1, 0x2f, 0x82, 0x00, 0x66, 0x4f, 0xd0, 0xe4, 0x08, 0x59, 0xd0, 0x60, 0x6d, 0x7a, 0x94, 0x39,
0xc2, 0x02, 0x8a, 0xa5, 0x41, 0xb5, 0x80, 0x5e, 0x69, 0x40, 0xcf, 0x31, 0x47, 0x28, 0x20, 0x58,
0xbb, 0x7d, 0x74, 0x5b, 0x01, 0x14, 0x9f, 0x91, 0xbc, 0xff, 0x3d, 0x80, 0x7a, 0x4b, 0xa1, 0xc7,
0x84, 0x11, 0x5b, 0x93, 0x98, 0x89, 0x26, 0xa3, 0xb6, 0x94, 0x19, 0x97, 0x96, 0xd2, 0xe3, 0x80,
0x23, 0x35, 0x71, 0x6d, 0xd4, 0x9e, 0xda, 0x30, 0x64, 0xd4, 0x8e, 0xf2, 0x82, 0x35, 0xb1, 0xcc,
0x38, 0x88, 0xe2, 0x27, 0x4c, 0x22, 0xdc, 0x1e, 0xbb, 0xcf, 0xe3, 0x75, 0xe9, 0xd4, 0x99, 0xda,
0x32, 0x30, 0xc8, 0xa6, 0x88, 0x8f, 0x96, 0xf7, 0x4e, 0x2c, 0xb3, 0x12, 0xaf, 0xe5, 0x23, 0x89,
0xff, 0x62, 0x8e, 0x61, 0x70, 0xbf, 0xd9, 0x61, 0xc6, 0x3a, 0x53, 0xc3, 0x19, 0x3d, 0xb5, 0x0d,
0x10, 0x7d, 0x88, 0xbd, 0xf2, 0x28, 0x7f, 0xed, 0xe5, 0xf3, 0x28, 0xf7, 0xa3, 0xda, 0x05, 0x23,
0x06, 0x80, 0x7a, 0xbb, 0xb5, 0x35, 0x64, 0xfe, 0x40, 0x45, 0xe9, 0xd0, 0x63, 0xdd, 0x7e, 0x4c,
0xb0, 0xcd, 0x7f, 0x4f, 0x80, 0x8c, 0x67, 0x3c, 0xb0, 0x74, 0x94, 0x26, 0xb4, 0x48, 0x38, 0xe0,
0x84, 0x27, 0x96, 0xe6, 0x7f, 0x04, 0x76, 0x18, 0x71, 0x42, 0x01, 0x12, 0xaf, 0x33, 0x6f, 0x7b,
0x3e, 0xc7, 0xbb, 0x1d, 0xd7, 0xc4, 0x65, 0xfe, 0x06, 0x8d, 0xd1, 0x01, 0x66, 0xc4, 0x40, 0x14,
0x80, 0xe1, 0xaf, 0x4d, 0x24, 0x10, 0x69, 0x00, 0xc5, 0xd8, 0xb9, 0x42, 0x00, 0x45, 0xd8, 0x2b,
0xed, 0x01, 0x5a, 0x3d, 0x4a, 0xaf, 0xb2, 0xa0, 0xa0, 0x7a, 0x86, 0x1c, 0x21, 0x80, 0x20, 0x92,
0x0e, 0x83, 0x41, 0x16, 0xc5, 0x07, 0x79, 0x2f, 0xef, 0x27, 0x01, 0x58, 0x8e, 0x6b, 0x12, 0xe9,
0x5e, 0x69, 0xc6, 0xa2, 0x7f, 0x63, 0x8b, 0x2c, 0x2d, 0x98, 0x5b, 0xd6, 0x48, 0x08, 0x79, 0x5c,
0x60, 0x40, 0xde, 0xdc, 0x22, 0x97, 0x89, 0x32, 0xa6, 0x1e, 0x2c, 0x1f, 0x31, 0x97, 0x12, 0x98,
0x2b, 0xd6, 0x17, 0x7e, 0x02, 0xdf, 0x63, 0xf1, 0x9b, 0x6c, 0xc9, 0x32, 0xa8, 0xd1, 0x70, 0xcf,
0x6f, 0x19, 0x1b, 0x99, 0x92, 0xaa, 0x85, 0xa9, 0x4b, 0x89, 0xa2, 0x1e, 0x7a, 0xef, 0x5f, 0xad,
0xfd, 0xa8, 0xec, 0x9f, 0x80, 0x52, 0xee, 0xd0, 0x14, 0xf6, 0x2b, 0x5b, 0x02, 0xdc, 0xf6, 0x94,
0x49, 0xe8, 0x32, 0xfd, 0x6d, 0x3f, 0xda, 0x31, 0x1d, 0x3b, 0x5d, 0xba, 0x63, 0x7e, 0x62, 0x6d,
0x09, 0x91, 0x66, 0xe2, 0x79, 0x3c, 0xad, 0x41, 0xe9, 0x3c, 0xec, 0xaf, 0xe7, 0xe7, 0x2e, 0xf2,
0xfe, 0xfc, 0xbf, 0x13, 0x5b, 0x82, 0x25, 0x7f, 0xd4, 0x87, 0xbf, 0x28, 0x84, 0x3c, 0x9b, 0xab,
0x3f, 0x45, 0xcf, 0x81, 0x06, 0xdb, 0x50, 0x14, 0x06, 0xe0, 0x2f, 0x49, 0xa5, 0x10, 0x4b, 0x01,
0xc3, 0x34, 0xaf, 0xd0, 0x82, 0x15, 0x91, 0xbc, 0xca, 0x1e, 0xa3, 0x98, 0xb6, 0xa0, 0xaf, 0xb1,
0x57, 0xb9, 0x60, 0xcf, 0x11, 0x30, 0x00, 0x05, 0x14, 0x91, 0xbb, 0xdb, 0x5d, 0x73, 0xf1, 0x39,
0x72, 0xce, 0xc9, 0x9f, 0x13, 0x00, 0x5b, 0xec, 0xad, 0x49, 0xf7, 0x95, 0xf6, 0x54, 0x7c, 0xb9,
0xb0, 0x0e, 0xbc, 0xf6, 0x90, 0x4a, 0x7e, 0xfc, 0x36, 0x52, 0xd6, 0xd8, 0x1d, 0xf8, 0xf9, 0xab,
0xb7, 0xc5, 0x0c, 0x60, 0x9b, 0x79, 0x27, 0x1d, 0xda, 0x02, 0xe3, 0xe7, 0x8d, 0x13, 0xbf, 0x33,
0xcf, 0x73, 0x32, 0xb0, 0x4b, 0xd0, 0x12, 0xe2, 0x90, 0xcc, 0xe5, 0x23, 0x80, 0x6d, 0xc8, 0xb9,
0x4e, 0x85, 0x03, 0xb9, 0xaf, 0xc6, 0x84, 0xb5, 0x22, 0x0c, 0x18, 0x31, 0x11, 0xfa, 0xdc, 0x9b,
0x50, 0xab, 0x71, 0xc3, 0xba, 0xe7, 0x31, 0x63, 0xcc, 0x75, 0x6d, 0x38, 0x62, 0xc2, 0xbb, 0xcc,
0x84, 0x99, 0xad, 0x61, 0x42, 0xbe, 0xe6, 0x38, 0x72, 0xed, 0x59, 0x9a, 0xec, 0xda, 0xe7, 0x5e,
0x9a, 0xd5, 0x3a, 0x53, 0xb2, 0x56, 0x9d, 0xd0, 0xef, 0x73, 0xea, 0x96, 0xcf, 0x29, 0x27, 0x40,
0x7b, 0x49, 0x26, 0x9e, 0xdf, 0xf9, 0x73, 0xe6, 0xf6, 0x09, 0x2e, 0xbf, 0x1b, 0x32, 0xf1, 0x2e,
0xcc, 0xb1, 0x5a, 0x3c, 0x9a, 0x1f, 0xfc, 0x03, 0x00, 0xf0, 0x8a, 0xb1, 0xff, 0x80, 0xff, 0x3b,
0xcb, 0xb0, 0x55, 0xe1, 0x1e, 0x2b, 0x4b, 0x25, 0x67, 0xfd, 0x05, 0x85, 0xc8, 0x01, 0x06, 0x14,
0x51, 0x1c, 0xc7, 0xf1, 0xcf, 0xf4, 0xc6, 0xdb, 0x8d, 0xd1, 0x06, 0xd8, 0xc0, 0xce, 0x11, 0x5e,
0x45, 0x2d, 0xc5, 0x2e, 0x1d, 0xa0, 0x23, 0xf4, 0x8e, 0x10, 0x20, 0xa0, 0x01, 0x81, 0x74, 0x86,
0x8e, 0x32, 0xa0, 0x20, 0x9d, 0x61, 0x8e, 0xb0, 0x20, 0x83, 0xd1, 0xf4, 0x06, 0xa3, 0x35, 0xa2,
0x2f, 0x1f, 0x7f, 0xff, 0x1f, 0xc8, 0x48, 0x77, 0x5a, 0xe4, 0x8d, 0xd6, 0xda, 0xcf, 0x39, 0x58,
0xac, 0x4d, 0x15, 0xf3, 0x8e, 0x10, 0x75, 0x55, 0x63, 0xa8, 0x5e, 0x2f, 0xb7, 0xa8, 0x41, 0x4e,
0xc5, 0x59, 0x37, 0x5f, 0xf4, 0xf3, 0x4b, 0x39, 0xa7, 0xbf, 0x24, 0xd7, 0x62, 0xed, 0x57, 0x64,
0x0a, 0x34, 0x45, 0x8c, 0x77, 0x02, 0x9f, 0x8b, 0xdf, 0x3b, 0x1e, 0xd4, 0x90, 0x31, 0xbc, 0x89,
0xba, 0xb7, 0x91, 0xa9, 0xb8, 0x5d, 0x18, 0x8b, 0x77, 0xb1, 0xec, 0xc5, 0xb5, 0xb6, 0xe9, 0x76,
0x1a, 0x0f, 0x0b, 0xe8, 0xcf, 0x8f, 0x0d, 0x1f, 0x93, 0xf4, 0xbd, 0xf8, 0x56, 0x7c, 0x7a, 0x2a,
0xfd, 0x78, 0x25, 0x8d, 0x27, 0x52, 0x6d, 0x53, 0xfe, 0xe7, 0x3f, 0x62, 0x0e, 0xba, 0x2a, 0xea,
0x35, 0xb2, 0xa3, 0x3c, 0x3d, 0xd1, 0x7e, 0x79, 0x2e, 0x85, 0xc5, 0x8b, 0x95, 0x36, 0x3c, 0x93,
0x1d, 0x5d, 0x8b, 0x5e, 0xa3, 0x0b, 0x91, 0x39, 0xf0, 0x01, 0x1c, 0xf8, 0xfa, 0xb0, 0xd8, 0x17,
0x35, 0xc3, 0x01, 0x74, 0x15, 0x43, 0xe0, 0xfd, 0x8e, 0x73, 0xc3, 0x7e, 0x4f, 0xbf, 0x6e, 0x8f,
0x38, 0x36, 0x20, 0xfd, 0xc7, 0x2d, 0x80, 0xae, 0xda, 0xfa, 0x03, 0x8d, 0xc8, 0x01, 0x46, 0x04,
0x51, 0x00, 0x80, 0xe1, 0x6f, 0xcc, 0x9a, 0x9d, 0xf4, 0xcc, 0x0c, 0x60, 0x04, 0xed, 0x11, 0x5e,
0x16, 0x05, 0x2a, 0x74, 0x90, 0xa1, 0x0b, 0x2c, 0x20, 0x44, 0x73, 0xb0, 0xf0, 0x0e, 0xd0, 0x21,
0xe6, 0x00, 0x61, 0x81, 0x42, 0xf6, 0x65, 0x3c, 0xd8, 0x80, 0xf0, 0xf1, 0xfb, 0xc1, 0x37, 0x72,
0x6d, 0xae, 0x1a, 0x49, 0x30, 0x19, 0xad, 0x76, 0x79, 0x2f, 0xae, 0x9e, 0x63, 0xf1, 0x75, 0x63,
0x77, 0xbf, 0x37, 0xb9, 0xf2, 0x68, 0x94, 0x04, 0x73, 0xdd, 0x90, 0x6b, 0x5e, 0x02, 0xfe, 0x4b,
0xaa, 0x58, 0x6a, 0x62, 0x43, 0xc1, 0x54, 0x97, 0x8f, 0x18, 0xff, 0xca, 0xb7, 0xc5, 0xf9, 0xa3,
0x0d, 0x84, 0x8e, 0xcd, 0x25, 0x1f, 0x17, 0xbc, 0xb6, 0xc4, 0x16, 0x42, 0xcb, 0x53, 0xe1, 0xb3,
0xe5, 0xbd, 0x67, 0xd8, 0xc2, 0x8c, 0x05, 0x07, 0x44, 0x00, 0xa7, 0xdc, 0x5b, 0x89, 0x28, 0x18,
0x38, 0x5d, 0x73, 0x7a, 0x00, 0x1d, 0xcb, 0x1b, 0xc7, 0x7c, 0x2e, 0x57, 0x11, 0x51, 0xe9, 0x15,
0x80, 0x83, 0x62, 0x83, 0x86, 0xd4, 0x30, 0x6f, 0x51, 0xc1, 0x80, 0x11, 0x01, 0x3f, 0x3d, 0xa4,
0x9e, 0x69, 0x64, 0x37, 0x14, 0x6b, 0xaf, 0x0f, 0x6d, 0xcb, 0x5d, 0xc7, 0x71, 0x4b, 0xaa, 0x99,
0x31, 0x06, 0x0e, 0x1d, 0x4b, 0x4d, 0xaa, 0x7e, 0x01, 0x6d, 0x8e, 0x01, 0x46, 0x05, 0x41, 0x1c,
0x87, 0xbf, 0x31, 0x6c, 0x93, 0x9e, 0xdd, 0x07, 0x08, 0xa9, 0x47, 0x17, 0x98, 0x14, 0x06, 0x54,
0x47, 0xd9, 0x23, 0xec, 0x05, 0xd2, 0xac, 0x3d, 0x48, 0x57, 0x19, 0xf0, 0x1e, 0xa4, 0x33, 0x0c,
0x91, 0x45, 0x12, 0x20, 0xac, 0xf4, 0x63, 0x27, 0xf6, 0xb1, 0xf8, 0xf8, 0xfd, 0xf9, 0xe6, 0x33,
0x00, 0x51, 0x24, 0xd1, 0x8a, 0xfc, 0xec, 0x81, 0x0b, 0x51, 0x43, 0xb4, 0x90, 0x81, 0x80, 0xce,
0x8d, 0xce, 0x86, 0xd4, 0x9f, 0xd2, 0xb6, 0x0e, 0xbe, 0x1f, 0x66, 0x70, 0x8b, 0x84, 0x9e, 0xef,
0x44, 0x28, 0x89, 0x13, 0x48, 0x06, 0xba, 0x72, 0x36, 0x10, 0xaf, 0x3d, 0xd9, 0x07, 0x76, 0xdb,
0x3b, 0xf0, 0xb7, 0x4a, 0xdc, 0x40, 0xf6, 0x00, 0xed, 0x23, 0xf8, 0x7d, 0xe1, 0xa0, 0xcc, 0x53,
0x20, 0x86, 0x4b, 0x88, 0xca, 0xe4, 0xff, 0x8c, 0x53, 0xb5, 0x56, 0xf5, 0x8c, 0x2e, 0x39, 0xb6,
0xdd, 0xf9, 0xe2, 0x17, 0x02, 0xe0, 0xc7, 0xc0, 0x64, 0xa1, 0xb2, 0xbc, 0x59, 0x81, 0x36, 0x02,
0x03, 0x1e, 0xb8, 0x87, 0x7c, 0x85, 0x9d, 0x0c, 0x87, 0x17, 0xc3, 0xd7, 0x60, 0x18, 0x7b, 0x43,
0xd2, 0xe6, 0x57, 0xe4, 0x06, 0x50, 0x12, 0xbf, 0xd4, 0x99, 0x66, 0x65, 0x94, 0xfe, 0xd9, 0xcf,
0x4f, 0xad, 0x74, 0xb3, 0xae, 0x3b, 0xe9, 0x7b, 0x29, 0xef, 0x83, 0xe8, 0xe7, 0xbd, 0x59, 0xaf,
0x3b, 0xe9, 0x75, 0xd1, 0x3f, 0x4a, 0xfd, 0x55, 0xbb, 0x96, 0x5e, 0xad, 0xd7, 0xab, 0xa2, 0xe7,
0x41, 0x94, 0x7a, 0x75, 0x5c, 0xff, 0x03, 0x6d, 0x90, 0x35, 0x5a, 0x03, 0x40, 0x14, 0x06, 0x27,
0xee, 0xd2, 0xe3, 0xee, 0xb0, 0x1d, 0x2e, 0x47, 0x89, 0x27, 0x15, 0x35, 0x1d, 0x4e, 0xcb, 0x11,
0x72, 0x05, 0x8e, 0x90, 0x93, 0xe0, 0xee, 0x1d, 0x2e, 0xef, 0xfb, 0x71, 0x29, 0x66, 0x75, 0xfe,
0xb7, 0x02, 0x0e, 0x98, 0x82, 0xed, 0x26, 0x78, 0xf4, 0xf0, 0x58, 0xf5, 0xb0, 0xbb, 0x6c, 0x2c,
0x7a, 0x78, 0xb6, 0x71, 0xf8, 0xd9, 0x03, 0xdb, 0x29, 0x60, 0xda, 0x70, 0x48, 0x9f, 0x80, 0x05,
0xd3, 0xaf, 0x7d, 0x60, 0xdb, 0xb5, 0x75, 0x0f, 0xdb, 0xa6, 0x67, 0x16, 0xac, 0x7f, 0x99, 0x16,
0x19, 0xe7, 0xfe, 0x00, 0x70, 0x6b, 0xbc, 0x84, 0x58, 0xa8, 0xa6, 0xa8, 0x79, 0xea, 0xd9, 0x66,
0x24, 0x98, 0xe1, 0xfa, 0x9d, 0x8d, 0x50, 0x06, 0xc7, 0x0c, 0x69, 0x00, 0x40, 0x95, 0xf6, 0x8c,
0xb2, 0xa5, 0x67, 0x8d, 0xca, 0xb7, 0x6a, 0xcd, 0x34, 0x7f, 0xd3, 0x44, 0x46, 0x8a, 0x54, 0x45,
0xb2, 0x7f, 0xb5, 0x73, 0xa3, 0x64, 0xcb, 0xf9, 0x37, 0x34, 0xae, 0xfc, 0xd5, 0x0e, 0x8d, 0xdc,
0xb7, 0xc3, 0x4a, 0x42, 0x9a, 0x07, 0x68, 0xb3, 0x41, 0x19, 0xa7, 0x6a, 0xc7, 0x86, 0xd2, 0xa8,
0xcb, 0xe0, 0xb4, 0x74, 0x69, 0x74, 0xd9, 0xdc, 0x17, 0x06, 0xfa, 0x91, 0xaf, 0xb7, 0x1c, 0x19,
0x36, 0x27, 0x0c, 0xd0, 0x89, 0x12, 0x1f, 0x2f, 0xd0, 0xdc, 0x0b, 0x80, 0xe3, 0xdf, 0xbf, 0x94,
0xcc, 0x88, 0x3e, 0x6f, 0x61, 0x2b, 0xa5, 0xcf, 0x64, 0xee, 0xf7, 0x47, 0x9c, 0xfb, 0x60, 0x2d,
0x05, 0xc1, 0x29, 0xf0, 0xdf, 0x42, 0x7c, 0xdb, 0x47, 0x7c, 0x21, 0x45, 0x90, 0xa9, 0x0f, 0xe7,
0x15, 0x7d, 0x8f, 0x01, 0x46, 0xc7, 0x60, 0x1c, 0x86, 0x9f, 0xb5, 0xec, 0x3f, 0x64, 0x03, 0x08,
0x53, 0x1d, 0x61, 0x09, 0x02, 0xba, 0xca, 0xc7, 0xd2, 0x20, 0xec, 0x08, 0x1d, 0x20, 0xba, 0x40,
0x87, 0xf9, 0x06, 0x19, 0x3a, 0x40, 0x80, 0x75, 0x83, 0x00, 0x01, 0xeb, 0xc1, 0xc7, 0x32, 0x33,
0x3c, 0xf8, 0xde, 0xdf, 0xfb, 0xbc, 0x33, 0x00, 0x7e, 0x65, 0x39, 0x11, 0xdf, 0x6a, 0x46, 0x2e,
0xe9, 0x68, 0xe9, 0xe5, 0x59, 0x42, 0x22, 0x66, 0x0d, 0x2f, 0x4b, 0x8d, 0x3d, 0x18, 0xb6, 0x56,
0xd4, 0x4a, 0x4d, 0xb4, 0x78, 0x92, 0x4e, 0xc6, 0x43, 0x6b, 0xd2, 0xb2, 0x4d, 0x2f, 0xff, 0xbf,
0x39, 0x1d, 0x5a, 0x9f, 0x5a, 0xee, 0x23, 0x3c, 0xa2, 0x91, 0xf8, 0x38, 0xb4, 0x62, 0xb2, 0xc2,
0x8a, 0xef, 0xdd, 0xff, 0x3a, 0x3f, 0x83, 0x2a, 0x87, 0xf7, 0xcc, 0x05, 0x4c, 0xe5, 0x47, 0xbe,
0x7c, 0xbf, 0x9a, 0x17, 0x42, 0x1a, 0xef, 0x1d, 0x7f, 0x20, 0x72, 0x45, 0xe0, 0x8e, 0x10, 0x6f,
0x09, 0x83, 0xe3, 0x4d, 0xcb, 0x5c, 0x39, 0x9e, 0x37, 0xcc, 0x8e, 0xdf, 0xac, 0xe3, 0x80, 0x89,
0x5c, 0x93, 0x71, 0xcf, 0x89, 0x96, 0x4a, 0x4a, 0x41, 0x30, 0xf3, 0x26, 0xf5, 0xb6, 0xee, 0x09,
0x52, 0xf5, 0x42, 0x54, 0xc9, 0xcd, 0xbe, 0xf7, 0xeb, 0xa5, 0xa7, 0x02, 0x2b, 0x89, 0x02, 0xb5,
0x75, 0xfd, 0x0f, 0x6d, 0xce, 0x01, 0x66, 0xc4, 0x40, 0x00, 0x46, 0xe1, 0xb7, 0x52, 0x09, 0x94,
0x0c, 0xa0, 0x08, 0xd9, 0x23, 0x4c, 0x81, 0xa0, 0x4c, 0xd1, 0x83, 0xe4, 0x00, 0xc5, 0x02, 0x8a,
0x22, 0xa5, 0x7a, 0x9e, 0x1e, 0x61, 0x40, 0x0a, 0xf4, 0x04, 0x45, 0x7a, 0x83, 0x05, 0x2c, 0x56,
0xda, 0x87, 0x21, 0x5d, 0x9a, 0xf8, 0x18, 0xf9, 0x9f, 0x11, 0xca, 0xb3, 0xfc, 0xb4, 0xea, 0xa9,
0x19, 0x14, 0xa1, 0xf0, 0xac, 0x01, 0x37, 0xb5, 0x7b, 0x4c, 0x77, 0x90, 0x1b, 0x20, 0xe9, 0x94,
0xb8, 0x5e, 0x12, 0xcd, 0x33, 0x54, 0x3d, 0x7c, 0x57, 0xee, 0xdb, 0x9d, 0x09, 0xb9, 0x54, 0x97,
0xe6, 0x1d, 0xac, 0x7a, 0x2a, 0x21, 0xb9, 0x94, 0x11, 0xe4, 0x7b, 0xa7, 0x35, 0xb2, 0x7c, 0xe8,
0x25, 0x01, 0xbd, 0x80, 0x16, 0x5e, 0x13, 0xe1, 0xed, 0x96, 0xc3, 0xec, 0xa4, 0x69, 0x0e, 0xb0,
0xb6, 0x70, 0x36, 0xdb, 0x12, 0x4c, 0x70, 0xce, 0x3a, 0x98, 0x44, 0x93, 0xb0, 0x25, 0x93, 0x49,
0x2e, 0xb7, 0xc4, 0x92, 0x64, 0x93, 0x69, 0x4b, 0xb2, 0xc9, 0x68, 0x32, 0x3a, 0xdf, 0x2b, 0x97,
0x64, 0xdc, 0x12, 0x4a, 0x82, 0xb3, 0x14, 0x18, 0x4d, 0xf6, 0x97, 0xc9, 0x60, 0xf2, 0xe8, 0xfc,
0x30, 0xcb, 0x64, 0xf8, 0xe7, 0x77, 0xe7, 0xbf, 0xb7, 0xdc, 0x38, 0xb7, 0xd2, 0x57, 0x57, 0xbe,
0x15, 0x9f, 0x3a, 0xeb, 0x18, 0x00, 0x58, 0x1a, 0x38, 0xf5, 0xd0, 0x75, 0x57, 0xd4, 0xb1, 0xa6,
0x3e, 0xea, 0xbd, 0xf1, 0x5c, 0x41, 0xf8, 0x05, 0x6d, 0xd0, 0x01, 0x66, 0xc4, 0x40, 0x14, 0x80,
0xe1, 0x9f, 0xb0, 0x85, 0x92, 0x00, 0x94, 0x20, 0x47, 0x18, 0x40, 0x81, 0x5c, 0x65, 0x8e, 0x30,
0x47, 0x98, 0x23, 0xf4, 0x48, 0x0f, 0x14, 0x54, 0xcf, 0x30, 0x47, 0x18, 0x40, 0xb1, 0xaa, 0x3f,
0xfb, 0xb0, 0xb5, 0x3b, 0x7c, 0x24, 0x6f, 0x7e, 0x2f, 0x04, 0x80, 0xb1, 0xc0, 0xcf, 0xca, 0xf6,
0xb5, 0xd3, 0x3e, 0x0b, 0x43, 0x5d, 0x7c, 0xeb, 0xaa, 0xb9, 0xdd, 0x67, 0x98, 0xe1, 0x75, 0xa8,
0xa9, 0x98, 0x6d, 0xff, 0xb3, 0x6e, 0x16, 0xb9, 0xad, 0x64, 0x16, 0x66, 0xfd, 0x71, 0x5b, 0xcd,
0x6d, 0xa4, 0x6a, 0x76, 0xdc, 0x32, 0x3b, 0x9a, 0xc6, 0xef, 0x49, 0x57, 0x9e, 0xf0, 0xb1, 0xc9,
0x86, 0xad, 0x03, 0x91, 0x5d, 0xc9, 0x8b, 0x00, 0x7a, 0x05, 0x0e, 0xe1, 0x00, 0x55, 0x40, 0xc0,
0x78, 0x81, 0xb9, 0xd2, 0xdf, 0x77, 0xe6, 0x6b, 0xa1, 0xa9, 0x6a, 0xbe, 0x15, 0x3e, 0xce, 0xdd,
0x78, 0x85, 0xb9, 0x90, 0xc7, 0x0f, 0x33, 0xae, 0xa7, 0x09, 0xd4, 0x0b, 0x44, 0x0a, 0xe7, 0xd5,
0x39, 0x58, 0xe7, 0xff, 0xdb, 0x77, 0xca, 0xc5, 0x8d, 0x0a, 0x21, 0x9c, 0xe1, 0x1d, 0x63, 0xb9,
0x4f, 0x71, 0x4c, 0x66, 0x55, 0x45, 0xdb, 0xf3, 0x34, 0x1c, 0x67, 0x22, 0x73, 0xf5, 0xc7, 0x34,
0x7c, 0xad, 0x99, 0x8a, 0x48, 0xc3, 0xd9, 0x71, 0x4b, 0xff, 0x00, 0x65, 0xcc, 0x01, 0x66, 0x03,
0x41, 0x14, 0x87, 0xf1, 0x5f, 0x12, 0x12, 0x88, 0x66, 0x00, 0xc5, 0xb2, 0x57, 0x18, 0x2d, 0x5a,
0xc4, 0xf6, 0x2a, 0xef, 0x08, 0xbd, 0x41, 0x03, 0x7a, 0xae, 0x3d, 0x4a, 0x40, 0x01, 0x2c, 0xa0,
0x58, 0xbb, 0x7d, 0x65, 0x0d, 0xc4, 0xf8, 0xf0, 0xfe, 0xf3, 0xf9, 0xc0, 0x74, 0xe0, 0xf7, 0x22,
0xba, 0x4e, 0x1c, 0x2b, 0x1b, 0x91, 0xe4, 0x26, 0xff, 0xb8, 0x1f, 0x40, 0x6d, 0xf0, 0xb2, 0xe7,
0xed, 0x24, 0xa1, 0xed, 0x5a, 0xf0, 0x8b, 0xee, 0xca, 0x71, 0xe0, 0xfb, 0x95, 0x9f, 0xca, 0x90,
0x5c, 0x3b, 0xd6, 0x16, 0x04, 0x4b, 0xaf, 0x2c, 0x1f, 0xaa, 0xea, 0x33, 0x19, 0x13, 0x8a, 0xdc,
0xc9, 0x1d, 0x68, 0x27, 0xd5, 0x2d, 0xb9, 0x4b, 0x5d, 0x51, 0x1e, 0xb5, 0x5b, 0x9e, 0xa1, 0xb6,
0x17, 0x8a, 0xf1, 0x51, 0x8b, 0x4d, 0x23, 0xd9, 0xb4, 0xd8, 0xb4, 0xbd, 0xb3, 0x21, 0x99, 0x77,
0x67, 0xe3, 0xe5, 0x34, 0x9a, 0x0f, 0x61, 0x51, 0xc7, 0x5e, 0x99, 0x77, 0x0c, 0xd8, 0x03, 0x4c,
0xeb, 0x7a, 0xf9, 0xc7, 0xf9, 0x99, 0x63, 0xd5, 0x78, 0x7a, 0x67, 0xee, 0x99, 0x4e, 0xb6, 0x60,
0x64, 0x30, 0x32, 0x28, 0x83, 0x2c, 0x22, 0x83, 0xd1, 0x82, 0x7f,
};
static const uint8_t FONT_2_DESCRIPTOR[] = {
0x18, 0x1c, 0x20, 0xe0, 0xff, 0xff, 0x00, 0x05, 0x00, 0x00, 0x0f, 0x06, 0x00, 0x0f, 0x1e, 0x09,
0x00, 0x2d, 0x2e, 0x0c, 0x00, 0x5b, 0x27, 0x0b, 0x00, 0x82, 0x3f, 0x11, 0x00, 0xc1, 0x33, 0x0d,
0x00, 0xf4, 0x0d, 0x05, 0x01, 0x01, 0x18, 0x07, 0x01, 0x19, 0x17, 0x07, 0x01, 0x30, 0x25, 0x0b,
0x01, 0x55, 0x26, 0x0b, 0x01, 0x7b, 0x0f, 0x06, 0x01, 0x8a, 0x17, 0x07, 0x01, 0xa1, 0x0f, 0x06,
0x01, 0xb0, 0x1d, 0x09, 0x01, 0xcd, 0x27, 0x0b, 0x02, 0x00, 0x1b, 0x0b, 0x02, 0x1b, 0x27, 0x0b,
0x02, 0x42, 0x27, 0x0b, 0x02, 0x69, 0x2b, 0x0b, 0x02, 0x94, 0x27, 0x0b, 0x02, 0xbb, 0x27, 0x0b,
0x02, 0xe2, 0x26, 0x0b, 0x03, 0x08, 0x27, 0x0b, 0x03, 0x2f, 0x27, 0x0b, 0x03, 0x56, 0x0f, 0x06,
0x03, 0x65, 0x0f, 0x06, 0x03, 0x74, 0x27, 0x0b, 0x03, 0x9b, 0x27, 0x0b, 0x03, 0xc2, 0x26, 0x0b,
0x04, 0x00, 0x1e, 0x08, 0x04, 0x1e, 0x3f, 0x11, 0x04, 0x5d, 0x2f, 0x0c, 0x04, 0x8c, 0x2f, 0x0d,
0x04, 0xbb, 0x2b, 0x0c, 0x04, 0xe6, 0x32, 0x0e, 0x05, 0x18, 0x27, 0x0b, 0x05, 0x3f, 0x25, 0x0a,
0x05, 0x64, 0x33, 0x0e, 0x05, 0x97, 0x2f, 0x0e, 0x05, 0xc6, 0x17, 0x07, 0x05, 0xdd, 0x14, 0x07,
0x06, 0x00, 0x2f, 0x0c, 0x06, 0x2f, 0x27, 0x0a, 0x06, 0x56, 0x43, 0x13, 0x06, 0x99, 0x33, 0x0f,
0x06, 0xcc, 0x36, 0x0f, 0x07, 0x02, 0x2a, 0x0c, 0x07, 0x2c, 0x36, 0x0f, 0x07, 0x62, 0x2f, 0x0c,
0x07, 0x91, 0x27, 0x0b, 0x07, 0xb8, 0x25, 0x0a, 0x08, 0x00, 0x2f, 0x0e, 0x08, 0x2f, 0x2d, 0x0c,
0x08, 0x5c, 0x49, 0x13, 0x08, 0xa5, 0x2b, 0x0b, 0x08, 0xd0, 0x25, 0x0b, 0x08, 0xf5, 0x27, 0x0a,
0x09, 0x1c, 0x1c, 0x08, 0x09, 0x38, 0x1f, 0x09, 0x09, 0x57, 0x18, 0x08, 0x09, 0x6f, 0x2a, 0x0c,
0x09, 0x99, 0x2c, 0x0b, 0x09, 0xc5, 0x11, 0x06, 0x09, 0xd6, 0x23, 0x0b, 0x0a, 0x00, 0x2b, 0x0c,
0x0a, 0x2b, 0x23, 0x09, 0x0a, 0x4e, 0x2b, 0x0c, 0x0a, 0x79, 0x27, 0x0b, 0x0a, 0xa0, 0x19, 0x07,
0x0a, 0xb9, 0x2b, 0x0c, 0x0a, 0xe4, 0x27, 0x0c, 0x0b, 0x0b, 0x0f, 0x05, 0x0b, 0x1a, 0x14, 0x06,
0x0b, 0x2e, 0x27, 0x0b, 0x0b, 0x55, 0x0f, 0x05, 0x0b, 0x64, 0x43, 0x12, 0x0b, 0xa7, 0x27, 0x0c,
0x0b, 0xce, 0x2b, 0x0c, 0x0c, 0x00, 0x2b, 0x0c, 0x0c, 0x2b, 0x2c, 0x0c, 0x0c, 0x57, 0x1e, 0x08,
0x0c, 0x75, 0x1f, 0x09, 0x0c, 0x94, 0x17, 0x07, 0x0c, 0xab, 0x27, 0x0c, 0x0c, 0xd2, 0x26, 0x0a,
0x0c, 0xf8, 0x3a, 0x10, 0x0d, 0x32, 0x23, 0x0a, 0x0d, 0x55, 0x26, 0x0a, 0x0d, 0x7b, 0x1f, 0x08,
0x0d, 0x9a, 0x20, 0x09, 0x0d, 0xba, 0x18, 0x0b, 0x0d, 0xd2, 0x1e, 0x09, 0x0e, 0x00, 0x2a, 0x0b,
0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18,
0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18,
0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18,
0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18,
0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18,
0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18,
0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18,
0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x18,
0xff, 0xff, 0x00, 0x18, 0xff, 0xff, 0x00, 0x05, 0x0e, 0x2a, 0x10, 0x06, 0x0e, 0x3a, 0x23, 0x0b,
0x0e, 0x5d, 0x27, 0x0b, 0x0e, 0x84, 0x2f, 0x0d, 0x0e, 0xb3, 0x27, 0x0b, 0x0e, 0xda, 0x18, 0x0b,
0x0e, 0xf2, 0x23, 0x0a, 0x0f, 0x15, 0x21, 0x0d, 0x0f, 0x36, 0x4a, 0x14, 0x0f, 0x80, 0x16, 0x08,
0x0f, 0x96, 0x27, 0x0b, 0x0f, 0xbd, 0x27, 0x0b, 0x0f, 0xe4, 0x17, 0x07, 0x10, 0x00, 0x4a, 0x14,
0x10, 0x4a, 0x29, 0x0b, 0x10, 0x73, 0x1e, 0x09, 0x10, 0x91, 0x27, 0x0b, 0x10, 0xb8, 0x1a, 0x08,
0x10, 0xd2, 0x1a, 0x08, 0x10, 0xec, 0x11, 0x06, 0x10, 0xfd, 0x27, 0x0c, 0x11, 0x24, 0x2c, 0x0d,
0x11, 0x50, 0x0e, 0x06, 0x11, 0x5e, 0x14, 0x05, 0x11, 0x72, 0x12, 0x08, 0x11, 0x84, 0x1a, 0x08,
0x11, 0x9e, 0x27, 0x0b, 0x12, 0x00, 0x43, 0x11, 0x12, 0x43, 0x43, 0x12, 0x12, 0x86, 0x47, 0x12,
0x12, 0xcd, 0x20, 0x08, 0x12, 0xed, 0x2f, 0x0c, 0x13, 0x1c, 0x2f, 0x0c, 0x13, 0x4b, 0x2f, 0x0c,
0x13, 0x7a, 0x2f, 0x0c, 0x13, 0xa9, 0x2f, 0x0c, 0x14, 0x00, 0x2f, 0x0c, 0x14, 0x2f, 0x3b, 0x11,
0x14, 0x6a, 0x2b, 0x0c, 0x14, 0x95, 0x27, 0x0b, 0x14, 0xbc, 0x27, 0x0b, 0x14, 0xe3, 0x27, 0x0b,
0x15, 0x0a, 0x27, 0x0b, 0x15, 0x31, 0x17, 0x07, 0x15, 0x48, 0x19, 0x07, 0x15, 0x61, 0x19, 0x07,
0x15, 0x7a, 0x19, 0x07, 0x15, 0x93, 0x32, 0x0e, 0x15, 0xc5, 0x33, 0x0f, 0x16, 0x00, 0x36, 0x0f,
0x16, 0x36, 0x36, 0x0f, 0x16, 0x6c, 0x36, 0x0f, 0x16, 0xa2, 0x36, 0x0f, 0x16, 0xd8, 0x36, 0x0f,
0x17, 0x0e, 0x27, 0x0b, 0x17, 0x35, 0x36, 0x0f, 0x17, 0x6b, 0x2f, 0x0e, 0x17, 0x9a, 0x2f, 0x0e,
0x17, 0xc9, 0x2f, 0x0e, 0x18, 0x00, 0x2f, 0x0e, 0x18, 0x2f, 0x25, 0x0b, 0x18, 0x54, 0x2a, 0x0c,
0x18, 0x7e, 0x2b, 0x0c, 0x18, 0xa9, 0x23, 0x0b, 0x18, 0xcc, 0x23, 0x0b, 0x18, 0xef, 0x23, 0x0b,
0x19, 0x12, 0x23, 0x0b, 0x19, 0x35, 0x23, 0x0b, 0x19, 0x58, 0x23, 0x0b, 0x19, 0x7b, 0x3f, 0x11,
0x19, 0xba, 0x23, 0x09, 0x1a, 0x00, 0x27, 0x0b, 0x1a, 0x27, 0x27, 0x0b, 0x1a, 0x4e, 0x27, 0x0b,
0x1a, 0x75, 0x27, 0x0b, 0x1a, 0x9c, 0x0f, 0x05, 0x1a, 0xab, 0x15, 0x06, 0x1a, 0xc0, 0x19, 0x07,
0x1a, 0xd9, 0x15, 0x06, 0x1a, 0xee, 0x2b, 0x0c, 0x1b, 0x19, 0x27, 0x0c, 0x1b, 0x40, 0x2b, 0x0c,
0x1b, 0x6b, 0x2b, 0x0c, 0x1b, 0x96, 0x2b, 0x0c, 0x1b, 0xc1, 0x2b, 0x0c, 0x1c, 0x00, 0x2b, 0x0c,
0x1c, 0x2b, 0x26, 0x0b, 0x1c, 0x51, 0x2b, 0x0c, 0x1c, 0x7c, 0x27, 0x0c, 0x1c, 0xa3, 0x27, 0x0c,
0x1c, 0xca, 0x27, 0x0c, 0x1c, 0xf1, 0x27, 0x0c, 0x1d, 0x18, 0x26, 0x0a, 0x1d, 0x3e, 0x2b, 0x0c,
0x1d, 0x69, 0x26, 0x0a,
};
static const uint16_t FONT_2_OFFSETS[] = {
0, 238, 457, 664, 840, 1049, 1233, 1451, 1675, 1865, 2087, 2321, 2515, 2728, 2923, 3099,
};
static const uint16_t FONT_2_LENGTHS[] = {
500, 488, 497, 477, 505, 505, 496, 507, 453, 472, 504, 504, 477, 492, 399,
};
static const Font FONT_2 = { FONT_2_DESCRIPTOR, FONT_2_DATA, FONT_2_OFFSETS, FONT_2_LENGTHS, 15, 3099 };
return FONT_2;
}
inline const uint8_t* font(unsigned id) {
return id == 1 ? fontStorage1().descriptor : id == 2 ? fontStorage2().descriptor : nullptr;
}
inline int identify(const uint8_t* descriptor) {
return descriptor == fontStorage1().descriptor ? 1 : descriptor == fontStorage2().descriptor ? 2 : -1;
}
inline const Font* storage(const uint8_t* descriptor) {
const int id = identify(descriptor);
return id == 1 ? &fontStorage1() : id == 2 ? &fontStorage2() : nullptr;
}
// Caller-owned 512-byte scratch; no cache, heap or mutable global state.
inline Glyph glyph(const uint8_t* descriptor, uint8_t code, uint8_t* scratch, size_t capacity) {
const Font* f = storage(descriptor);
if (!f || code < descriptor[2] || unsigned(code - descriptor[2]) >= descriptor[3])
return { nullptr, 0, 0, false, false };
const unsigned slot = code - descriptor[2];
const uint8_t* record = descriptor + 4 + 4 * slot;
const unsigned location = (unsigned(record[0]) << 8) | record[1];
const uint8_t length = record[2], width = record[3];
if (!length) return { nullptr, width, 0, false, location == 0xffff };
const unsigned group = location >> 9;
const unsigned offset = location & 511;
if (group >= f->chunkCount) return { nullptr, width, 0, false, false };
const unsigned first = f->offsets[group], end = f->offsets[group + 1];
const unsigned expected = f->rawLengths[group];
if (!scratch || expected > kScratchSize || capacity < expected || offset + length > expected || first > end || end > f->dataLength)
return { nullptr, width, 0, false, false };
unsigned int produced = expected;
if (tinf_uncompress_exact(scratch, &produced, f->data + first, end - first) != TINF_OK || produced != expected)
return { nullptr, width, 0, false, false };
return { scratch + offset, width, length, true, true };
}
}}} // namespace mesh::ui::noto
File diff suppressed because it is too large. Load diff
+30
View File
@@ -0,0 +1,30 @@
#pragma once
// The infrastructure pre-script sets MESH_NRF52_FLASH_TRIM only for known
// non-Companion nRF52 roles. Other display drivers and platforms stay unchanged.
// This header is also read by the C tinf wrapper, so keep it preprocessor-only.
// 9 = Noto Sans Condensed 16/24, Zopfli 1000, no unused initial Arial10.
// MESH_NRF52_FONT_MODE=0 opts out of the font change without disabling other
// infrastructure trims. The old experiment switch remains for comparisons.
#if defined(NRF52_PLATFORM) && defined(MESH_NRF52_FLASH_TRIM) \
&& MESH_NRF52_FLASH_TRIM && defined(ST7789) \
&& !defined(COMPANION_RADIO_FULL)
#if defined(MESH_ARIAL_EXPERIMENT)
#define MESHCORE_ARIAL_FONT_MODE MESH_ARIAL_EXPERIMENT
#elif defined(MESH_NRF52_FONT_MODE)
#define MESHCORE_ARIAL_FONT_MODE MESH_NRF52_FONT_MODE
#else
#define MESHCORE_ARIAL_FONT_MODE 9
#endif
#else
#define MESHCORE_ARIAL_FONT_MODE 0
#endif
#if MESHCORE_ARIAL_FONT_MODE < 0 || MESHCORE_ARIAL_FONT_MODE > 9
#error "Unsupported nRF52 font mode (expected 0..9)"
#endif
#define MESHCORE_FONT_DEFLATE \
(MESHCORE_ARIAL_FONT_MODE == 3 || MESHCORE_ARIAL_FONT_MODE == 9)
#define MESHCORE_FONT_COMPRESSED \
(MESHCORE_ARIAL_FONT_MODE == 2 || MESHCORE_FONT_DEFLATE)
+21
View File
@@ -0,0 +1,21 @@
#pragma once
// Only the non-Companion nRF52 ST7735 renderer includes this configuration.
// Load the selected library setup first so its driver, bus and pin settings
// remain unchanged, then prevent TFT_eSPI from loading those settings again.
#if defined(NRF52_PLATFORM) && defined(MESH_NRF52_FLASH_TRIM) && MESH_NRF52_FLASH_TRIM
#include <User_Setup_Select.h>
#ifndef USER_SETUP_LOADED
#define USER_SETUP_LOADED
#endif
// ST7735Display uses the normal GLCD font and integer scaling. Keep GFXFF's
// no-argument setFreeFont() API; the numbered and smooth fonts are never used.
#undef LOAD_FONT2
#undef LOAD_FONT4
#undef LOAD_FONT6
#undef LOAD_FONT7
#undef LOAD_FONT8
#undef LOAD_FONT8N
#undef SMOOTH_FONT
#endif
+42 -2
View File
@@ -45,7 +45,11 @@ OLEDDisplay::OLEDDisplay() {
color = WHITE;
geometry = GEOMETRY_128_64;
textAlignment = TEXT_ALIGN_LEFT;
#if MESHCORE_ARIAL_FONT_MODE >= 1
fontData = ArialMT_Plain_16;
#else
fontData = ArialMT_Plain_10;
#endif
fontTableLookupFunction = DefaultFontTableLookup;
buffer = NULL;
#ifdef OLEDDISPLAY_DOUBLE_BUFFER
@@ -557,6 +561,10 @@ uint16_t OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, const cha
uint8_t textHeight = pgm_read_byte(fontData + HEIGHT_POS);
uint8_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS);
uint16_t sizeOfJumpTable = pgm_read_byte(fontData + CHAR_NUM_POS) * JUMPTABLE_BYTES;
#if MESHCORE_FONT_COMPRESSED
const bool compactFont = mesh::ui::active_compact_font::identify(fontData) >= 0;
uint8_t glyphScratch[mesh::ui::active_compact_font::kScratchSize];
#endif
uint16_t cursorX = 0;
uint16_t cursorY = 0;
@@ -594,7 +602,11 @@ uint16_t OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, const cha
continue;
} else
code = text[j];
#if MESHCORE_ARIAL_FONT_MODE >= 1
if (code >= firstChar && code - firstChar < pgm_read_byte(fontData + CHAR_NUM_POS)) {
#else
if (code >= firstChar) {
#endif
uint8_t charCode = code - firstChar;
// 4 Bytes per char code
@@ -605,9 +617,23 @@ uint16_t OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, const cha
// Test if the char is drawable
if (!(msbJumpToChar == 255 && lsbJumpToChar == 255)) {
#if MESHCORE_FONT_COMPRESSED
if (compactFont) {
const mesh::ui::active_compact_font::Glyph glyph = mesh::ui::active_compact_font::glyph(
fontData, code, glyphScratch, sizeof(glyphScratch));
// A zero byte count has a different meaning to drawInternal().
// Failed/blank glyphs must not enter the generic bitmap renderer.
if (glyph.valid && glyph.drawable && glyph.data && glyph.length)
drawInternal(xPos, yPos, currentCharWidth, textHeight,
glyph.data, 0, glyph.length);
} else {
#endif
// Get the position of the char data
uint16_t charDataPosition = JUMPTABLE_START + sizeOfJumpTable + ((msbJumpToChar << 8) + lsbJumpToChar);
drawInternal(xPos, yPos, currentCharWidth, textHeight, fontData, charDataPosition, charByteSize);
#if MESHCORE_FONT_COMPRESSED
}
#endif
}
cursorX += currentCharWidth;
@@ -678,10 +704,17 @@ uint16_t OLEDDisplay::drawStringMaxWidth(int16_t xMove, int16_t yMove, uint16_t
uint16_t drawStringResult = 1; // later tested for 0 == error, so initialize to 1
for (uint16_t i = 0; i < length; i++) {
#if MESHCORE_ARIAL_FONT_MODE >= 1
const uint8_t c = (this->fontTableLookupFunction)(text[i]);
#else
char c = (this->fontTableLookupFunction)(text[i]);
#endif
if (c == 0)
continue;
strWidth += pgm_read_byte(fontData + JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH);
#if MESHCORE_ARIAL_FONT_MODE >= 1
if (c >= firstChar && c - firstChar < pgm_read_byte(fontData + CHAR_NUM_POS))
#endif
strWidth += pgm_read_byte(fontData + JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH);
// Always break on newline
if (text[i] == '\n') {
@@ -737,13 +770,20 @@ uint16_t OLEDDisplay::getStringWidth(const char* text, uint16_t length, bool utf
uint16_t maxWidth = 0;
for (uint16_t i = 0; i < length; i++) {
#if MESHCORE_ARIAL_FONT_MODE >= 1
uint8_t c = text[i];
#else
char c = text[i];
#endif
if (utf8) {
c = (this->fontTableLookupFunction)(c);
if (c == 0)
continue;
}
stringWidth += pgm_read_byte(fontData + JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH);
#if MESHCORE_ARIAL_FONT_MODE >= 1
if (c >= firstChar && c - firstChar < pgm_read_byte(fontData + CHAR_NUM_POS))
#endif
stringWidth += pgm_read_byte(fontData + JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH);
if (c == 10) {
maxWidth = max(maxWidth, stringWidth);
stringWidth = 0;
+4
View File
@@ -1,5 +1,7 @@
#include "OLEDDisplayFonts.h"
#if MESHCORE_ARIAL_FONT_MODE == 0 || MESHCORE_ARIAL_FONT_MODE == 1
#if MESHCORE_ARIAL_FONT_MODE == 0
const uint8_t ArialMT_Plain_10[] PROGMEM = {
0x0A, // Width: 10
0x0D, // Height: 13
@@ -424,6 +426,7 @@ const uint8_t ArialMT_Plain_10[] PROGMEM = {
0x20,0x00,0xC8,0x09,0x00,0x06,0xC8,0x01,0x20 // 255
};
#endif
const uint8_t ArialMT_Plain_16[] PROGMEM = {
0x10, // Width: 16
0x13, // Height: 19
@@ -1271,3 +1274,4 @@ const uint8_t ArialMT_Plain_24[] PROGMEM = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x00,0x1C,0x18,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x03, // 254
0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x06,0xC0,0xF0,0x01,0x06,0xC0,0x80,0x0F,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xFC,0x00,0xC0,0xC0,0x1F,0x00,0xC0,0xF8,0x03,0x00,0x00,0x3E,0x00,0x00,0x00,0x06 // 255
};
#endif // raw font storage
+24
View File
@@ -1,6 +1,13 @@
#ifndef OLEDDISPLAYFONTS_h
#define OLEDDISPLAYFONTS_h
// Qualified nRF52 infrastructure defaults to mode 9; Companions remain mode 0.
// Optional comparisons: 0 = original, 1 = omit unused Arial10, 2 = per-glyph RLE,
// 3 = Zopfli-1000 chunks, 4 = Liberation Sans, 5 = Noto Sans,
// 6 = DejaVu Sans, 7 = Noto Sans Condensed, 8 = DejaVu Sans Condensed.
// Family trials preserve the visible 12/17px cap heights.
#include "Nrf52FontConfig.h"
#ifdef ARDUINO
#include <Arduino.h>
#elif __MBED__
@@ -8,6 +15,23 @@
#endif
extern const uint8_t ArialMT_Plain_10[] PROGMEM;
#if MESHCORE_ARIAL_FONT_MODE == 9
#include "CompactNotoFonts.h"
namespace mesh { namespace ui { namespace active_compact_font = noto; } }
// Keep the display driver's existing font-selection API, with Noto glyphs.
#define ArialMT_Plain_16 (::mesh::ui::noto::font(1))
#define ArialMT_Plain_24 (::mesh::ui::noto::font(2))
#elif MESHCORE_ARIAL_FONT_MODE == 2 || MESHCORE_ARIAL_FONT_MODE == 3
#include "CompactArialFonts.h"
namespace mesh { namespace ui { namespace active_compact_font = arial; } }
#define ArialMT_Plain_16 (::mesh::ui::arial::font(1))
#define ArialMT_Plain_24 (::mesh::ui::arial::font(2))
#elif MESHCORE_ARIAL_FONT_MODE >= 4
#include "FontFamilyTrials.h"
#define ArialMT_Plain_16 (::mesh::ui::font_trials::font(1))
#define ArialMT_Plain_24 (::mesh::ui::font_trials::font(2))
#else
extern const uint8_t ArialMT_Plain_16[] PROGMEM;
extern const uint8_t ArialMT_Plain_24[] PROGMEM;
#endif
#endif
+3
View File
@@ -1,5 +1,8 @@
#include "ST7735Display.h"
#include "../UsbLogging.h"
#if defined(NRF52_PLATFORM) && defined(MESH_NRF52_FLASH_TRIM) && MESH_NRF52_FLASH_TRIM
#include "Nrf52TftFontTrim.h"
#endif
#include <TFT_eSPI.h>
#if defined(NRF52_PLATFORM)
+9
View File
@@ -0,0 +1,9 @@
#include "helpers/ui/CompactArialFonts.h"
extern "C" const uint8_t* fontFromOtherTranslationUnit(unsigned id) {
return mesh::ui::arial::font(id);
}
extern "C" int identifyInOtherTranslationUnit(const uint8_t* descriptor) {
return mesh::ui::arial::identify(descriptor);
}
+81
View File
@@ -0,0 +1,81 @@
#include "helpers/ui/CompactArialFonts.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
using namespace mesh::ui::arial;
extern "C" const uint8_t* fontFromOtherTranslationUnit(unsigned id);
extern "C" int identifyInOtherTranslationUnit(const uint8_t* descriptor);
static void rleFailures() {
uint8_t guarded[5] = { 0x5a, 0, 0, 0, 0xa5 };
const uint8_t literal[] = { 2, 1, 2, 3 };
assert(decodeRle(literal, sizeof(literal), guarded + 1, 3));
assert(guarded[1] == 1 && guarded[2] == 2 && guarded[3] == 3);
assert(!decodeRle(literal, sizeof(literal), guarded + 1, 2));
assert(!decodeRle(literal, sizeof(literal) - 1, guarded + 1, 3));
const uint8_t repeat[] = { 0x80, 7 };
assert(decodeRle(repeat, sizeof(repeat), guarded + 1, 3));
assert(guarded[1] == 7 && guarded[2] == 7 && guarded[3] == 7);
assert(!decodeRle(repeat, 1, guarded + 1, 3));
assert(!decodeRle(repeat, sizeof(repeat), guarded + 1, 2));
assert(!decodeRle(repeat, sizeof(repeat), guarded + 1, 4));
assert(!decodeRle(nullptr, 1, guarded + 1, 3));
assert(!decodeRle(repeat, sizeof(repeat), nullptr, 3));
assert(decodeRle(nullptr, 0, nullptr, 0));
assert(guarded[0] == 0x5a && guarded[4] == 0xa5);
}
int main(int argc, char**) {
uint8_t scratch[kScratchSize + 2];
if (argc > 1) {
// Python alters the descriptor or first stream in a private header copy.
const Glyph decoded = glyph(font(1), 33, scratch + 1, kScratchSize);
assert(!decoded.valid);
return 0;
}
rleFailures();
assert(font(0) == nullptr && font(3) == nullptr && font(100) == nullptr);
assert(identify(nullptr) == -1);
assert(!glyph(nullptr, 65, scratch + 1, kScratchSize).valid);
for (unsigned id = 1; id <= 2; ++id) {
const uint8_t* descriptor = font(id);
assert(identify(descriptor) == int(id));
assert(fontFromOtherTranslationUnit(id) == descriptor);
assert(identifyInOtherTranslationUnit(descriptor) == int(id));
uint8_t copy[900];
memcpy(copy, descriptor, sizeof(copy));
assert(identify(copy) == -1);
assert(!glyph(copy, 65, scratch + 1, kScratchSize).valid);
assert(!glyph(descriptor, 31, scratch + 1, kScratchSize).valid);
printf("F %u %u %u %u %u\n", id, descriptor[0], descriptor[1], descriptor[2], descriptor[3]);
for (unsigned code = descriptor[2]; code < unsigned(descriptor[2]) + descriptor[3]; ++code) {
memset(scratch, 0xcc, sizeof(scratch));
scratch[0] = 0x5a;
scratch[sizeof(scratch) - 1] = 0xa5;
const Glyph decoded = glyph(descriptor, uint8_t(code), scratch + 1, kScratchSize);
assert(decoded.valid);
assert(decoded.drawable == (decoded.length > 0));
assert(scratch[0] == 0x5a && scratch[sizeof(scratch) - 1] == 0xa5);
const uint8_t* record = descriptor + 4 + 4 * (code - descriptor[2]);
assert(record[2] == decoded.length && record[3] == decoded.width);
if (decoded.drawable) {
#if MESH_ARIAL_EXPERIMENT == 2
if (!(record[0] & 0x80)) {
assert(!glyph(descriptor, uint8_t(code), nullptr, kScratchSize).valid);
assert(!glyph(descriptor, uint8_t(code), scratch + 1, decoded.length - 1).valid);
}
#else
assert(!glyph(descriptor, uint8_t(code), nullptr, kScratchSize).valid);
assert(!glyph(descriptor, uint8_t(code), scratch + 1, 0).valid);
#endif
} else {
assert(decoded.data == nullptr);
assert(glyph(descriptor, uint8_t(code), nullptr, 0).valid);
}
printf("G %u %u %u %u ", id, code, decoded.width, decoded.length);
for (unsigned index = 0; index < decoded.length; ++index) printf("%02x", decoded.data[index]);
puts("");
}
}
}
+9
View File
@@ -0,0 +1,9 @@
#include "helpers/ui/CompactNotoFonts.h"
extern "C" const uint8_t* fontFromOtherTranslationUnit(unsigned id) {
return mesh::ui::noto::font(id);
}
extern "C" int identifyInOtherTranslationUnit(const uint8_t* descriptor) {
return mesh::ui::noto::identify(descriptor);
}
+61
View File
@@ -0,0 +1,61 @@
#include "helpers/ui/CompactNotoFonts.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
using namespace mesh::ui::noto;
extern "C" const uint8_t* fontFromOtherTranslationUnit(unsigned id);
extern "C" int identifyInOtherTranslationUnit(const uint8_t* descriptor);
int main(int argc, char**) {
uint8_t scratch[kScratchSize + 2];
if (argc > 1) {
const Glyph decoded = glyph(font(1), 33, scratch + 1, kScratchSize);
assert(!decoded.valid);
return 0;
}
assert(kScratchSize == 512);
assert(font(0) == nullptr && font(3) == nullptr && font(100) == nullptr);
assert(identify(nullptr) == -1);
assert(!glyph(nullptr, 65, scratch + 1, kScratchSize).valid);
for (unsigned id = 1; id <= 2; ++id) {
const uint8_t* descriptor = font(id);
assert(identify(descriptor) == int(id));
assert(fontFromOtherTranslationUnit(id) == descriptor);
assert(identifyInOtherTranslationUnit(descriptor) == int(id));
uint8_t copy[900];
memcpy(copy, descriptor, sizeof(copy));
assert(identify(copy) == -1);
assert(!glyph(copy, 65, scratch + 1, kScratchSize).valid);
assert(!glyph(descriptor, 31, scratch + 1, kScratchSize).valid);
printf("F %u %u %u %u %u\n", id, descriptor[0], descriptor[1], descriptor[2], descriptor[3]);
for (unsigned code = descriptor[2]; code < unsigned(descriptor[2]) + descriptor[3]; ++code) {
memset(scratch, 0xcc, sizeof(scratch));
scratch[0] = 0x5a;
scratch[sizeof(scratch) - 1] = 0xa5;
const Glyph decoded = glyph(descriptor, uint8_t(code), scratch + 1, kScratchSize);
assert(decoded.valid);
assert(decoded.drawable == (decoded.length > 0));
assert(scratch[0] == 0x5a && scratch[sizeof(scratch) - 1] == 0xa5);
const uint8_t* record = descriptor + 4 + 4 * (code - descriptor[2]);
assert(record[2] == decoded.length && record[3] == decoded.width);
if (decoded.drawable) {
assert(!glyph(descriptor, uint8_t(code), nullptr, kScratchSize).valid);
assert(!glyph(descriptor, uint8_t(code), scratch + 1, 0).valid);
} else {
assert(decoded.data == nullptr);
assert(glyph(descriptor, uint8_t(code), nullptr, 0).valid);
}
printf("G %u %u %u %u ", id, code, decoded.width, decoded.length);
for (unsigned index = 0; index < decoded.length; ++index) printf("%02x", decoded.data[index]);
puts("");
}
}
uint8_t first[kScratchSize], second[kScratchSize];
const Glyph a = glyph(font(1), 'A', first, sizeof(first));
uint8_t snapshot[kScratchSize];
memcpy(snapshot, first, sizeof(first));
const Glyph b = glyph(font(2), 'W', second, sizeof(second));
assert(a.valid && b.valid);
assert(memcmp(snapshot, first, sizeof(first)) == 0);
}
+7
View File
@@ -0,0 +1,7 @@
/* Standalone ABI stub for a feature-preservation unit test, not firmware. */
typedef struct { unsigned marker; } CRYS_ECPKI_Domain_t;
typedef enum {
CRYS_ECPKI_DomainID_secp192r1 = 0,
CRYS_ECPKI_DomainID_secp256r1 = 8,
CRYS_ECPKI_DomainID_Invalid = 255
} CRYS_ECPKI_DomainID_t;
+22
View File
@@ -0,0 +1,22 @@
#include <assert.h>
#include <stddef.h>
#include "crys_ecpki_domain.h"
static const CRYS_ECPKI_Domain_t p256 = {256};
static unsigned calls;
const CRYS_ECPKI_Domain_t *SaSi_ECPKI_GetSecp256r1DomainP(void) {
++calls;
return &p256;
}
extern const CRYS_ECPKI_Domain_t *__wrap_CRYS_ECPKI_GetEcDomain(CRYS_ECPKI_DomainID_t);
int main(void) {
assert(__wrap_CRYS_ECPKI_GetEcDomain(CRYS_ECPKI_DomainID_secp256r1) == &p256);
assert(calls == 1);
assert(__wrap_CRYS_ECPKI_GetEcDomain(CRYS_ECPKI_DomainID_secp192r1) == NULL);
assert(__wrap_CRYS_ECPKI_GetEcDomain(CRYS_ECPKI_DomainID_Invalid) == NULL);
assert(calls == 1);
return 0;
}
+15
View File
@@ -0,0 +1,15 @@
// Controlled library setup used to check that the trim preserves hardware.
#ifndef USER_SETUP_LOADED
#define ILI9341_DRIVER
#define TFT_CS 42
#define SPI_FREQUENCY 40000000
#define LOAD_GLCD
#define LOAD_GFXFF
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_FONT8N
#define SMOOTH_FONT
#endif
+28
View File
@@ -0,0 +1,28 @@
#include "helpers/ui/Nrf52TftFontTrim.h"
#include <User_Setup_Select.h>
#if !defined(ILI9341_DRIVER) || TFT_CS != 42 || SPI_FREQUENCY != 40000000
#error "The trim changed the selected hardware setup"
#endif
#if !defined(LOAD_GLCD) || !defined(LOAD_GFXFF)
#error "The current bitmap font or setFreeFont API was removed"
#endif
#if EXPECT_TRIM
#if defined(LOAD_FONT2) || defined(LOAD_FONT4) || defined(LOAD_FONT6) || \
defined(LOAD_FONT7) || defined(LOAD_FONT8) || defined(LOAD_FONT8N) || \
defined(SMOOTH_FONT)
#error "Unused fonts are still enabled"
#endif
#ifndef USER_SETUP_LOADED
#error "The selected setup could reload the unused fonts"
#endif
#else
#if !defined(LOAD_FONT2) || !defined(LOAD_FONT4) || !defined(LOAD_FONT6) || \
!defined(LOAD_FONT7) || !defined(LOAD_FONT8) || !defined(LOAD_FONT8N) || \
!defined(SMOOTH_FONT)
#error "An excluded build lost its original fonts"
#endif
#endif
int main() { return 0; }
+41
View File
@@ -0,0 +1,41 @@
/* Minimal host-only Arduino surface for exercising the real OLED renderer. */
#pragma once
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#define PROGMEM
#define pgm_read_byte(address) (*reinterpret_cast<const uint8_t *>(address))
template <typename A, typename B>
inline auto min(A a, B b) -> decltype(a + b) { return a < b ? a : b; }
template <typename A, typename B>
inline auto max(A a, B b) -> decltype(a + b) { return a > b ? a : b; }
inline void yield() {}
class String {
std::string value;
public:
String(const char *text) : value(text ? text : "") {}
const char *c_str() const { return value.c_str(); }
unsigned int length() const { return static_cast<unsigned int>(value.size()); }
void toCharArray(char *target, unsigned int capacity, unsigned int index = 0) const {
if (!capacity) return;
const size_t available = index < value.size() ? value.size() - index : 0;
const size_t copied = std::min<size_t>(capacity - 1, available);
std::memcpy(target, value.data() + std::min<size_t>(index, value.size()), copied);
target[copied] = 0;
}
};
class Print {
public:
virtual ~Print() {}
virtual size_t write(uint8_t byte) = 0;
};
+229
View File
@@ -0,0 +1,229 @@
#include "helpers/ui/OLEDDisplay.h"
#include <cassert>
#include <iostream>
#include <string>
#ifndef EXPECT_MODE
#define EXPECT_MODE 0
#endif
#ifndef MESHCORE_ARIAL_FONT_MODE
#define MESHCORE_ARIAL_FONT_MODE 0
#endif
static_assert(MESHCORE_ARIAL_FONT_MODE == EXPECT_MODE, "font experiment escaped its scope");
class TestDisplay : public OLEDDisplay {
public:
TestDisplay() { setGeometry(GEOMETRY_RAWMODE, 96, 64); assert(init()); }
void display() override {}
int getBufferOffset() override { return 0; }
bool connect() override { return true; }
uint8_t initialFontHeight() const { return pgm_read_byte(fontData + HEIGHT_POS); }
size_t framebufferSize() const { return displayBufferSize; }
uint16_t drawRaw(int x, int y, const char *text, uint16_t count) {
return drawStringInternal(x, y, text, count, getStringWidth(text, count, false), false);
}
uint16_t drawControl(const char *text) {
return drawStringInternal(0, 0, text, 1, 0, false);
}
};
struct Digest {
uint64_t hash = UINT64_C(14695981039346656037);
size_t cases = 0;
void byte(uint8_t value) { hash = (hash ^ value) * UINT64_C(1099511628211); }
void number(uint16_t value) { byte(value & 255); byte(value >> 8); }
void framebuffer(const TestDisplay &display) {
for (size_t i = 0; i < display.framebufferSize(); ++i) byte(display.buffer[i]);
++cases;
}
void print(const char *name) const {
std::cout << name << ' ' << cases << ' ' << std::hex << hash << std::dec << '\n';
}
};
static char identityLookup(uint8_t code) { return static_cast<char>(code); }
static void fillPattern(TestDisplay &display, unsigned seed) {
for (size_t i = 0; i < display.framebufferSize(); ++i)
display.buffer[i] = static_cast<uint8_t>(seed + 31 * i);
}
// Independent pixel-at-a-time reference for the uncompressed family trials.
// This does not reuse drawInternal's byte/shift implementation.
static void assertRawFamilyGlyph(TestDisplay &display, const uint8_t *font,
unsigned code, unsigned font_index) {
const unsigned line_height = font[HEIGHT_POS];
const unsigned count = font[CHAR_NUM_POS];
assert(font[FIRST_CHAR_POS] == 32 && count == 224);
assert(line_height == (font_index == 0 ? 19 : 28));
const unsigned table_index = 4 + 4 * (code - font[FIRST_CHAR_POS]);
const unsigned offset = (unsigned(font[table_index]) << 8) | font[table_index + 1];
const unsigned length = font[table_index + 2], advance = font[table_index + 3];
char text[] = {static_cast<char>(code), 0};
assert(display.getStringWidth(text, 1, false) == advance);
uint8_t expected[96 * 64 / 8] = {};
int top = 255, bottom = -1;
const unsigned stride = (line_height + 7) / 8;
if (offset == 0xffff) {
assert(length == 0);
} else {
assert(length && advance);
const auto *bitmap = font + 4 + 4 * count + offset;
assert(length <= advance * stride);
for (unsigned byte = 0; byte < length; ++byte)
for (unsigned bit = 0; bit < 8; ++bit)
if (bitmap[byte] & (1 << bit)) {
const unsigned x = byte / stride, y = 8 * (byte % stride) + bit;
assert(x < advance && y < line_height); // No clipped/padding-row ink.
top = min(top, int(y)); bottom = max(bottom, int(y));
if (x < 96) expected[x + 96 * (y / 8)] |= 1 << (y & 7);
}
assert(bottom >= top); // Every drawable Latin-1 glyph still has pixels.
}
if (code == 'H') {
assert(top == (font_index == 0 ? 3 : 5));
assert(bottom == (font_index == 0 ? 14 : 21));
assert(bottom - top + 1 == (font_index == 0 ? 12 : 17));
}
assert(std::memcmp(expected, display.buffer, sizeof(expected)) == 0);
}
int main() {
TestDisplay display;
const uint8_t initial_height = display.initialFontHeight();
assert(initial_height == (EXPECT_MODE == 0 ? 13 : 19));
std::cout << "initial " << static_cast<unsigned>(initial_height) << '\n';
const uint8_t *fonts[] = {ArialMT_Plain_16, ArialMT_Plain_24};
Digest glyphs, clipping, utf8, wrapping, controls, bitmaps, logs, custom;
unsigned font_index = 0;
for (const auto *font : fonts) {
display.setFont(font);
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFontTableLookupFunction(identityLookup);
for (unsigned code = 32; code <= 255; ++code) {
char text[] = {static_cast<char>(code), 0};
display.clear();
display.setColor(WHITE);
glyphs.number(display.getStringWidth(text, 1, false));
glyphs.number(display.drawRaw(0, 0, text, 1));
if (EXPECT_MODE >= 4 && EXPECT_MODE <= 8)
assertRawFamilyGlyph(display, font, code, font_index);
glyphs.framebuffer(display);
}
if (EXPECT_MODE >= 4) {
unsigned drawable = 0;
for (unsigned slot = 0; slot < 224; ++slot)
if (font[4 + 4 * slot] != 255 || font[5 + 4 * slot] != 255) ++drawable;
assert(drawable == 189);
}
const char samples[] = {'A', 'W', 'j', static_cast<char>(0xc0), static_cast<char>(0xff)};
for (char sample : samples)
for (int color = BLACK; color <= INVERSE; ++color)
for (int offset = 0; offset < 8; ++offset)
for (int edge = 0; edge < 4; ++edge) {
char text[] = {sample, 0};
const int x[] = {-9, 4, 93, 4};
const int y[] = {offset, -19 + offset, offset, 60 + offset};
fillPattern(display, 0x53);
display.setColor(static_cast<OLEDDISPLAY_COLOR>(color));
clipping.number(display.drawRaw(x[edge], y[edge], text, 1));
clipping.framebuffer(display);
}
// Control codes must never index before the font's first character.
for (unsigned code = 1; code < 32; ++code) {
if (code == '\n') continue;
char text[] = {static_cast<char>(code), 0};
display.clear();
// Original mode 0 has no guard for control-code width lookup. Compare
// against its safe drawing behavior, not its out-of-bounds width read.
const auto width = EXPECT_MODE ? display.getStringWidth(text, 1, false) : 0;
assert(width == 0);
controls.number(width);
controls.number(display.drawControl(text));
for (size_t i = 0; i < display.framebufferSize(); ++i) assert(display.buffer[i] == 0);
controls.framebuffer(display);
}
display.setFontTableLookupFunction(DefaultFontTableLookup);
const char *strings[] = {
"MeshCore repeater 909.5 MHz",
"\xc3\x80\xc3\x89\xc3\xa9 \xc2\xb0\xc2\xb5\xc3\xbf",
"\xc2\xa0\xc2\xa3\xc2\xa9 \xe2\x82\xac",
"Line one / Line two",
"unsupported \xf0\x9f\x93\xbb emoji",
};
for (const char *text : strings)
for (int alignment = TEXT_ALIGN_LEFT; alignment <= TEXT_ALIGN_CENTER_BOTH; ++alignment) {
DefaultFontTableLookup(' ');
fillPattern(display, 0x62);
display.setColor(INVERSE);
display.setTextAlignment(static_cast<OLEDDISPLAY_TEXT_ALIGNMENT>(alignment));
utf8.number(display.getStringWidth(text, std::strlen(text), true));
utf8.number(display.drawString(48, 28, String(text)));
utf8.framebuffer(display);
}
display.setTextAlignment(TEXT_ALIGN_LEFT);
for (const char *text : strings)
for (unsigned width : {18, 47, 96}) {
DefaultFontTableLookup(' ');
display.clear();
display.setColor(WHITE);
wrapping.number(display.drawStringMaxWidth(-2, 3, width, String(text)));
wrapping.framebuffer(display);
}
if (EXPECT_MODE) {
// Newline width lookup is independently checked for the experiment;
// baseline font code indexes before its header for '\n'.
const char a[] = {'A', 0}, w[] = {'W', 0};
const auto expected = max(display.getStringWidth(a, 1), display.getStringWidth(w, 1));
assert(display.getStringWidth("A\nW", 3) == expected);
display.clear();
assert(display.drawStringMaxWidth(0, 0, 96, String("A\nW")) == 0);
}
assert(display.setLogBuffer(4, 64));
display.write("One\nTwo\nThree");
display.clear();
display.drawLogBuffer(0, 0);
logs.framebuffer(display);
++font_index;
}
// The font decoder must not accidentally intercept regular bitmap drawing.
const uint8_t bitmap[] = {0x5a, 0xff, 0x01, 0x80, 0x55, 0xaa, 0x00, 0x20};
display.clear();
display.drawFastImage(4, 4, 8, 8, bitmap);
bitmaps.framebuffer(display);
// Public setFont() remains compatible with user-supplied raw ThingPulse
// fonts even when the built-in Arial glyph storage is compressed.
static const uint8_t custom_font[] = {
4, 8, 'A', 2,
0, 0, 4, 4, 0, 4, 3, 3,
0x3c, 0x42, 0x42, 0x3c, 0x7e, 0x4a, 0x34,
};
display.setFont(custom_font);
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFontTableLookupFunction(identityLookup);
display.setColor(WHITE);
display.clear();
assert(display.getStringWidth("AB", 2, false) == 7);
assert(display.drawRaw(0, 0, "AB", 2) == 2);
assert(std::memcmp(display.buffer, custom_font + 12, 7) == 0);
for (size_t i = 7; i < display.framebufferSize(); ++i) assert(display.buffer[i] == 0);
custom.framebuffer(display);
if (EXPECT_MODE) {
// Unsupported codes in a shorter external font cannot index its bitmaps
// as if they were extra metadata rows.
display.clear();
assert(display.getStringWidth("C", 1, false) == 0);
display.drawRaw(0, 0, "C", 1);
for (size_t i = 0; i < display.framebufferSize(); ++i) assert(display.buffer[i] == 0);
}
glyphs.print("glyphs"); clipping.print("clipping"); utf8.print("utf8");
wrapping.print("wrapping"); controls.print("controls"); bitmaps.print("bitmaps");
logs.print("logs"); custom.print("custom");
}
+39
View File
@@ -0,0 +1,39 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "helpers/ota/OtaTargets.h"
using mesh::ota::ota_target_env_name;
int main() {
unsigned id;
while (scanf("%x", &id) == 1) {
#if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED
char buffer[mesh::ota::OTA_TARGET_ENV_NAME_CAPACITY];
const char* name = ota_target_env_name(id, buffer, sizeof buffer);
if (name) {
const size_t length = strlen(name);
char exact[mesh::ota::OTA_TARGET_ENV_NAME_CAPACITY];
assert(ota_target_env_name(id, exact, length + 1) == exact);
assert(strcmp(name, exact) == 0);
// A failed or unrelated lookup must not clobber another caller's result.
char short_buffer[mesh::ota::OTA_TARGET_ENV_NAME_CAPACITY];
memset(short_buffer, 'X', sizeof short_buffer);
assert(ota_target_env_name(id, short_buffer, length) == nullptr);
assert(short_buffer[0] == 0);
assert(short_buffer[length] == 'X');
assert(ota_target_env_name(id, nullptr, sizeof buffer) == nullptr);
assert(ota_target_env_name(id, short_buffer, 0) == nullptr);
assert(ota_target_env_name(0, short_buffer, sizeof short_buffer) == nullptr);
assert(short_buffer[0] == 0);
assert(strcmp(name, exact) == 0);
}
#else
// Keep the original one-argument API, including table-disabled/local fallback.
const char* name = ota_target_env_name(id);
assert(ota_target_env_name(id, nullptr, 0) == name);
#endif
puts(name ? name : "?");
}
return 0;
}
+151
View File
@@ -0,0 +1,151 @@
"""Lossless experimental Arial compression with the existing C tinf decoder."""
from pathlib import Path
import random
import re
import subprocess
import sys
import tempfile
import unittest
import zlib
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import generate_compact_arial as generate
HEADER = ROOT / "src/helpers/ui/CompactArialFonts.h"
HARNESS = ROOT / "test/fixtures/compact_arial/test.cpp"
def unpack_rle(data):
output = bytearray()
index = 0
while index < len(data):
token = data[index]
index += 1
if token < 128:
count = token + 1
output.extend(data[index:index + count])
index += count
else:
output.extend(bytes((data[index],)) * ((token & 127) + 3))
index += 1
return bytes(output)
class CompactArialTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.fonts = generate.read_fonts()
cls.header = HEADER.read_text(encoding="utf-8")
def compile_and_run(self, mode, header=None, invalid=False):
with tempfile.TemporaryDirectory() as directory:
folder = Path(directory)
if header is not None:
include = folder / "helpers/ui"
include.mkdir(parents=True)
(include / "CompactArialFonts.h").write_text(header, encoding="utf-8")
(folder / "helpers/ota/tinf").mkdir(parents=True)
(folder / "helpers/ota/tinf/tinf.h").write_text(
(ROOT / "src/helpers/ota/tinf/tinf.h").read_text(encoding="utf-8"), encoding="utf-8")
decoder = folder / "tinf.o"
subprocess.run([
"cc", "-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c",
str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(decoder),
], check=True)
executable = folder / "fonts"
subprocess.run([
"c++", "-std=c++11", "-O2", "-Wall", "-Wextra", "-Werror",
f"-DMESH_ARIAL_EXPERIMENT={mode}", f"-I{folder}", f"-I{ROOT / 'src'}",
str(HARNESS), str(HARNESS.with_name("other.cpp")), str(decoder), "-o", str(executable),
], check=True)
return subprocess.run(
[str(executable)] + (["invalid"] if invalid else []),
text=True, capture_output=True, check=True).stdout.splitlines()
def test_generated_header_is_reproducible_and_omits_unused_10(self):
self.assertEqual(self.header, generate.generate_header(self.fonts))
self.assertNotIn("FONT_0", self.header)
self.assertEqual([len(font.original) for font in self.fonts], [2731, 5049, 9643])
self.assertEqual(max(len(glyph.bitmap) for font in self.fonts for glyph in font.glyphs), 91)
def test_rle_and_raw_bypass_roundtrip_every_glyph_and_metric(self):
for font in self.fonts:
packed, rows = generate.rle_font(font)
descriptor = generate.descriptor(font, rows)
self.assertEqual(descriptor[:4], font.header)
for index, (glyph, record) in enumerate(zip(font.glyphs, rows)):
location, length, width = record
self.assertEqual((length, width), (len(glyph.bitmap), glyph.width))
self.assertEqual(descriptor[6 + 4 * index:8 + 4 * index], font.original[6 + 4 * index:8 + 4 * index])
if not length:
self.assertEqual(location, 0xffff)
continue
first = location & 0x7fff
end = next((next_row[0] & 0x7fff for next_row in rows[index + 1:] if next_row[1]), len(packed))
encoded = packed[first:end]
self.assertEqual(encoded if location & 0x8000 else unpack_rle(encoded), glyph.bitmap)
def test_rle_literal_and_run_boundaries(self):
cases = [bytes(range(256)), b"x" * 130, b"x" * 131, b"x" * 260,
b"abc" + b"x" * 130 + b"def", b"", b"x", b"xx"]
randomizer = random.Random(1234)
cases += [bytes(randomizer.randrange(8) for _ in range(length)) for length in range(300)]
for data in cases:
self.assertEqual(unpack_rle(generate.rle_encode(data)), data)
def test_zopfli_chunks_roundtrip_every_glyph_with_512_byte_cap(self):
original_total = 0
compressed_total = 0
for font in self.fonts[1:]:
packed, rows, offsets, lengths = generate.chunk_font(font)
raw = [zlib.decompress(packed[offsets[index]:offsets[index + 1]], -15) for index in range(len(lengths))]
self.assertEqual(list(map(len, raw)), list(lengths))
self.assertLessEqual(max(lengths), 512)
for glyph, (location, length, width) in zip(font.glyphs, rows):
self.assertEqual((length, width), (len(glyph.bitmap), glyph.width))
if length:
self.assertEqual(raw[location >> 9][(location & 511):(location & 511) + length], glyph.bitmap)
else:
self.assertEqual(location, 0xffff)
original_total += len(font.original)
compressed_total += len(packed) + len(generate.descriptor(font, rows)) + 2 * (len(offsets) + len(lengths))
self.assertEqual(original_total - compressed_total, 7555)
def test_google_zopfli_uses_1000_iterations(self):
from unittest.mock import patch
generate.chunk_font.cache_clear()
original = generate.raw_deflate_compress
with patch.object(generate, "raw_deflate_compress", wraps=original) as compressor:
generate.chunk_font(self.fonts[0])
self.assertEqual(compressor.call_count, 4)
self.assertTrue(all(call.kwargs == {"numiterations": 1000} for call in compressor.call_args_list))
def test_cpp_both_variants_preserve_all_448_glyphs_and_metadata(self):
expected = []
for id, font in enumerate(self.fonts[1:], 1):
expected.append("F " + " ".join(map(str, (id, *font.header))))
for slot, glyph in enumerate(font.glyphs):
expected.append(f"G {id} {slot + font.header[2]} {glyph.width} {len(glyph.bitmap)} {glyph.bitmap.hex()}")
for mode in (2, 3):
with self.subTest(mode=mode):
self.assertEqual(self.compile_and_run(mode), expected)
def test_cpp_corrupt_descriptor_or_deflate_stream_fails_closed(self):
# Glyph 33 is the first bitmap; glyph32 is a blank space.
for mode in (2, 3):
bad = re.sub(
r"(static const uint8_t FONT_1_DESCRIPTOR\[\] = \{\s*"
r"(?:0x[0-9a-f]{2},\s*){8})0x[0-9a-f]{2},\s*0x[0-9a-f]{2}",
lambda match: match[1] + "0xff, 0xfe", self.header)
self.assertNotEqual(bad, self.header)
self.compile_and_run(mode, bad, invalid=True)
bad = re.sub(r"(static const uint8_t FONT_1_DATA\[\] = \{\s*)0x[0-9a-f]{2}",
lambda match: match[1] + "0x07", self.header)
self.compile_and_run(3, bad, invalid=True)
if __name__ == "__main__":
unittest.main()
+129
View File
@@ -0,0 +1,129 @@
"""Production Noto compression preserves all selected font pixels and metrics."""
from pathlib import Path
import re
import subprocess
import sys
import tempfile
import unittest
import zlib
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import generate_compact_noto as generate
import generate_compact_arial as codec
HEADER = ROOT / "src/helpers/ui/CompactNotoFonts.h"
HARNESS = ROOT / "test/fixtures/compact_noto/test.cpp"
class CompactNotoTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.fonts, cls.license = generate.read_fonts()
cls.header = HEADER.read_text(encoding="utf-8")
def compile_and_run(self, header=None, invalid=False):
with tempfile.TemporaryDirectory() as directory:
folder = Path(directory)
if header is not None:
include = folder / "helpers/ui"
include.mkdir(parents=True)
(include / "CompactNotoFonts.h").write_text(header, encoding="utf-8")
(folder / "helpers/ota/tinf").mkdir(parents=True)
(folder / "helpers/ota/tinf/tinf.h").write_text(
(ROOT / "src/helpers/ota/tinf/tinf.h").read_text(encoding="utf-8"), encoding="utf-8")
decoder = folder / "tinf.o"
subprocess.run([
"cc", "-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c",
str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(decoder),
], check=True)
executable = folder / "fonts"
subprocess.run([
"c++", "-std=c++11", "-O2", "-Wall", "-Wextra", "-Werror",
f"-I{folder}", f"-I{ROOT / 'src'}", str(HARNESS),
str(HARNESS.with_name("other.cpp")), str(decoder), "-o", str(executable),
], check=True)
return subprocess.run(
[str(executable)] + (["invalid"] if invalid else []),
text=True, capture_output=True, check=True).stdout.splitlines()
def test_generated_header_matches_authoritative_mode7_and_has_required_notice(self):
self.assertEqual(self.header, generate.generate_header(self.fonts, self.license))
self.assertEqual([len(font.original) for font in self.fonts], [4419, 8176])
self.assertNotIn("FONT_0", self.header)
self.assertNotIn("MESH_ARIAL_EXPERIMENT", self.header)
self.assertNotIn("decodeRle", self.header)
self.assertIn("SIL OPEN FONT LICENSE Version 1.1", self.header)
self.assertIn("Copyright 2015 Google Inc. All Rights Reserved.", self.header)
self.assertIn("4efc2789b9fcaa87b6b55f19c25ac9a2919e4f9ca31d6b5b97bea7977d729d4f", self.header)
self.assertIn("Pillow 12.3.0 / FreeType 2.14.3", self.header)
def test_all_glyph_chunks_roundtrip_in_at_most_512_decoded_bytes(self):
total = 0
for font in self.fonts:
packed, rows, offsets, lengths = codec.chunk_font(font)
raw = [zlib.decompress(packed[offsets[group]:offsets[group + 1]], -15)
for group in range(len(lengths))]
self.assertEqual(list(map(len, raw)), list(lengths))
self.assertLessEqual(max(lengths), 512)
self.assertEqual(len(rows), 224)
self.assertEqual(sum(bool(glyph.bitmap) for glyph in font.glyphs), 189)
for glyph, (location, length, width) in zip(font.glyphs, rows):
self.assertEqual((length, width), (len(glyph.bitmap), glyph.width))
if length:
self.assertEqual(raw[location >> 9][(location & 511):(location & 511) + length], glyph.bitmap)
else:
self.assertEqual(location, 0xffff)
total += len(packed) + len(codec.descriptor(font, rows)) + 2 * (len(offsets) + len(lengths))
self.assertEqual(total, 6777)
def test_google_zopfli_requires_1000_iterations(self):
from unittest.mock import patch
codec.chunk_font.cache_clear()
original = codec.raw_deflate_compress
with patch.object(codec, "raw_deflate_compress", wraps=original) as compressor:
generate.generate_header(self.fonts, self.license)
self.assertEqual(compressor.call_count, 22)
self.assertTrue(all(call.kwargs == {"numiterations": 1000} for call in compressor.call_args_list))
def test_cpp_all448_glyphs_match_raw_mode7_without_experimental_macro(self):
expected = []
for id, font in enumerate(self.fonts, 1):
expected.append("F " + " ".join(map(str, (id, *font.header))))
for slot, glyph in enumerate(font.glyphs):
expected.append(f"G {id} {slot + font.header[2]} {glyph.width} {len(glyph.bitmap)} {glyph.bitmap.hex()}")
self.assertEqual(self.compile_and_run(), expected)
def test_cpp_rejects_corrupt_descriptor_deflate_truncation_and_trailing_bytes(self):
invalid_headers = [re.sub(
r"(static const uint8_t FONT_1_DESCRIPTOR\[\] = \{\s*"
r"(?:0x[0-9a-f]{2},\s*){8})0x[0-9a-f]{2},\s*0x[0-9a-f]{2}",
lambda match: match[1] + "0xff, 0xfe", self.header)]
invalid_headers.append(re.sub(
r"(static const uint8_t FONT_1_DATA\[\] = \{\s*)0x[0-9a-f]{2}",
lambda match: match[1] + "0x07", self.header))
offsets = re.search(r"static const uint16_t FONT_1_OFFSETS\[\] = \{\s*0, (\d+)", self.header)
for end in (1, int(offsets[1]) + 1):
invalid_headers.append(re.sub(
r"(static const uint16_t FONT_1_OFFSETS\[\] = \{\s*0, )\d+",
lambda match: match[1] + str(end), self.header))
for header in invalid_headers:
self.assertNotEqual(header, self.header)
self.compile_and_run(header, invalid=True)
def test_reader_rejects_missing_source_coverage_or_license(self):
source = generate.SOURCE.read_text(encoding="utf-8")
invalid = [source.replace("#if MESH_ARIAL_EXPERIMENT == 7", "#if MESH_ARIAL_EXPERIMENT == 17"),
source.replace("SIL OPEN FONT LICENSE Version 1.1", "removed license")]
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "source.h"
for content in invalid:
path.write_text(content, encoding="utf-8")
with self.assertRaises(ValueError):
generate.read_fonts(path)
if __name__ == "__main__":
unittest.main()
+106
View File
@@ -0,0 +1,106 @@
"""Same-size libre font trials preserve the complete existing character coverage."""
from pathlib import Path
import re
import sys
import unittest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import generate_font_family_trials as generate
from generate_compact_arial import Font, Glyph, read_fonts
HEADER = ROOT / "src/helpers/ui/FontFamilyTrials.h"
def parse_font(name, data):
count = data[3]
base = 4 + 4 * count
glyphs = []
consumed = 0
for index in range(count):
offset = int.from_bytes(data[4 + 4 * index:6 + 4 * index], "big")
length, width = data[6 + 4 * index:8 + 4 * index]
if offset == 0xffff:
if length:
raise ValueError("Blank glyph has bitmap bytes")
bitmap = b""
else:
if not length or offset != consumed or base + offset + length > len(data):
raise ValueError("Glyph data is not contiguous/in bounds")
bitmap = data[base + offset:base + offset + length]
consumed += length
glyphs.append(Glyph(width, bitmap))
if base + consumed != len(data):
raise ValueError("Trailing font bytes")
return Font(name, data, tuple(glyphs))
class FontFamilyTrialsTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.text = HEADER.read_text(encoding="utf-8")
cls.baseline = read_fonts()
cls.trials = {}
for mode, body in re.findall(r"#if MESH_ARIAL_EXPERIMENT == ([45678])\s*(.*?)\n#endif", cls.text, re.S):
fonts = []
for id, encoded in re.findall(r"static const uint8_t TRIAL_([12])\[\] = \{(.*?)\};", body, re.S):
data = bytes(int(value, 16) for value in re.findall(r"0x([0-9a-f]{2})", encoded))
fonts.append(parse_font(f"MeshFontTrial{mode}_{id}", data))
cls.trials[int(mode)] = tuple(fonts)
def test_all_five_trials_cover_exact_original_drawable_slots(self):
self.assertEqual(set(self.trials), {4, 5, 6, 7, 8})
for mode, fonts in self.trials.items():
self.assertEqual(len(fonts), 2)
for id, font in enumerate(fonts, 1):
with self.subTest(mode=mode, id=id):
self.assertEqual(font.header, self.baseline[id].header)
self.assertEqual(len(font.glyphs), 224)
self.assertEqual([bool(glyph.bitmap) for glyph in font.glyphs],
[bool(glyph.bitmap) for glyph in self.baseline[id].glyphs])
self.assertEqual(sum(bool(glyph.bitmap) for glyph in font.glyphs), 189)
def test_H_visible_size_top_and_line_height_are_unchanged(self):
for mode, fonts in self.trials.items():
for id, font in enumerate(fonts, 1):
with self.subTest(mode=mode, id=id):
self.assertEqual(generate.ink_y_bounds(font, ord("H")), (3, 14) if id == 1 else (5, 21))
self.assertEqual(font.header[1], 19 if id == 1 else 28)
def test_every_glyph_pixel_fits_its_advance_and_line_box(self):
for mode, fonts in self.trials.items():
for id, font in enumerate(fonts, 1):
stride = (font.header[1] + 7) // 8
for code, glyph in enumerate(font.glyphs, 32):
with self.subTest(mode=mode, id=id, code=code):
self.assertLessEqual(len(glyph.bitmap), glyph.width * stride)
for index, value in enumerate(glyph.bitmap):
for bit in range(8):
if value & (1 << bit):
self.assertLess(index // stride, glyph.width)
self.assertLess((index % stride) * 8 + bit, font.header[1])
def test_raw_byte_measurements_and_source_notices_are_exact(self):
self.assertEqual({mode: sum(len(font.original) for font in fonts)
for mode, fonts in self.trials.items()},
{4: 14979, 5: 14871, 6: 15701, 7: 12595, 8: 14230})
for family in generate.FAMILIES:
self.assertIn(family.sha256, self.text)
self.assertIn(family.upstream, self.text)
self.assertIn(f"SPDX-License-Identifier: {family.spdx}", self.text)
for notice in ("SIL OPEN FONT LICENSE Version 1.1", "Copyright (c) 2012 Red Hat, Inc.",
"Copyright 2015 Google Inc. All Rights Reserved.", "Bitstream Vera Fonts Copyright",
"Arev Fonts Copyright", "Copyright (c) 2006 by Tavmjong Bah."):
self.assertIn(notice, self.text)
self.assertNotIn("FONT_0", self.text)
self.assertNotIn("LiberationSansNarrow", self.text)
def test_serializer_preserves_glyph_bytes_and_missing_slot_width(self):
base = self.baseline[1]
self.assertEqual(generate.make_font(base.name, base.header, list(base.glyphs)), base)
if __name__ == "__main__":
unittest.main()
+165
View File
@@ -0,0 +1,165 @@
import importlib.util
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("nrf52_flash_trim", ROOT / "scripts/nrf52_flash_trim.py")
TRIM = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(TRIM)
class Nrf52FlashTrimTest(unittest.TestCase):
def policy(self, name="RAK_4631_repeater", role="simple_repeater", **kwargs):
options = dict(platform="nordicnrf52", mcu="nrf52840", env_name=name,
src_filter="+<*.cpp> +<../examples/%s>" % role)
options.update(kwargs)
return TRIM.trim_policy(**options)
def test_every_infrastructure_role_keeps_compact_crypto_and_names(self):
for role in ("simple_repeater", "simple_room_server", "simple_sensor", "kiss_modem"):
with self.subTest(role=role):
defines = self.policy(role=role)
for name in ("MESH_NRF52_FLASH_TRIM", "ED25519_COMPACT_BASE",
"ED25519_COMPACT_SHA512", "OTA_TARGET_NAME_FRONT_CODED",
"SSD1306_NO_SPLASH"):
self.assertEqual(defines[name], 1)
self.assertEqual(defines["CFG_TUD_HID"], 0)
self.assertNotIn("CFG_TUD_CDC", defines)
self.assertFalse(any(name.startswith("CFG_TUH") for name in defines))
def test_every_companion_alias_is_excluded(self):
for suffix in ("companion_radio_full", "companion_radio_usb", "companion_radio_ble",
"companion_radio_wifi", "companion_radio_serial", "companion_radio_ethernet",
"companion_usb", "companion_ble", "comp_radio_usb", "terminal_chat", "term_chat"):
with self.subTest(suffix=suffix):
self.assertEqual(self.policy(name="RAK_4631_" + suffix), {})
def test_source_filter_and_synthetic_alias_also_exclude_companion(self):
for role in ("companion_radio/*.cpp", "simple_secure_chat/main.cpp"):
self.assertEqual(self.policy(name="custom", role=role), {})
for flags in ('-DCOMPANION_RADIO_FULL=1', '-D COMPANION_FEATURE_USB_MOTA_SOURCE=1',
'-DOTA_VARIANT=\'"RAK_4631_terminal_chat"\'',
'-DOTA_VARIANT=\'"RAK_4631_companion_radio_full"\''):
with self.subTest(flags=flags):
self.assertEqual(self.policy(flags=flags), {})
self.assertEqual(self.policy(cppdefines=[("COMPANION_RADIO_FULL", 1)]), {})
def test_unknown_roles_other_platforms_and_explicit_optout_are_unchanged(self):
for options in (dict(role="custom"), dict(platform="espressif32", mcu="esp32"),
dict(platform="ststm32", mcu="stm32f411"),
dict(flags="-DMESH_NRF52_FLASH_TRIM=0")):
self.assertEqual(self.policy(**options), {})
def test_effective_filter_is_ordered_and_does_not_include_outside_source_by_wildcard(self):
self.assertEqual(self.policy(src_filter="+<*> +<../examples/simple_repeater> -<../examples/simple_repeater>"), {})
self.assertEqual(self.policy(src_filter="+<*>"), {})
self.assertTrue(self.policy(src_filter="+<../examples/companion_radio> -<../examples/companion_radio> +<../examples/simple_repeater>"))
def test_board_and_user_defines_are_not_overridden(self):
defines = self.policy(flags="-D CFG_TUD_HID=1 -UED25519_COMPACT_BASE -USSD1306_NO_SPLASH")
self.assertNotIn("CFG_TUD_HID", defines)
self.assertNotIn("ED25519_COMPACT_BASE", defines)
self.assertNotIn("SSD1306_NO_SPLASH", defines)
self.assertEqual(TRIM.flag_defines("-DMESH_NRF52_FLASH_TRIM=1 -UMESH_NRF52_FLASH_TRIM")["MESH_NRF52_FLASH_TRIM"], None)
def test_cc310_archive_is_exact_and_requires_supported_mcu_and_enabled_crypto(self):
with tempfile.TemporaryDirectory() as temp_dir:
archive = Path(temp_dir) / "libraries/Adafruit_nRFCrypto" / TRIM.CC310_ARCHIVE
archive.parent.mkdir(parents=True)
archive.write_bytes(b"future-sdk")
enabled = {"USE_CC310_HW_CRYPTO": "1"}
self.assertFalse(TRIM.supported_cc310(temp_dir, "nrf52840", enabled))
with patch.object(TRIM, "CC310_ARCHIVE_SHA256", TRIM.hashlib.sha256(b"future-sdk").hexdigest()):
self.assertTrue(TRIM.supported_cc310(temp_dir, "nrf52840", enabled))
self.assertFalse(TRIM.supported_cc310(temp_dir, "nrf52832", enabled))
self.assertFalse(TRIM.supported_cc310(temp_dir, "nrf52840", {"USE_CC310_HW_CRYPTO": "0"}))
self.assertFalse(TRIM.supported_cc310(temp_dir, "nrf52840", {}))
def test_profile_wiring_is_nrf52_only_and_companion_common_flags_unchanged(self):
config = (ROOT / "platformio.ini").read_text()
nrf = config.split("[nrf52_base]", 1)[1].split("\n[", 1)[0]
self.assertIn("pre:scripts/nrf52_flash_trim.py", nrf)
self.assertEqual(config.count("pre:scripts/nrf52_flash_trim.py"), 1)
self.assertNotIn("ED25519_COMPACT", nrf)
self.assertNotIn("CFG_TUD_HID", nrf)
self.assertNotIn("SSD1306_NO_SPLASH", nrf)
self.assertIn("-D MESH_NRF52_LOOP_STACK_WORDS=2048", nrf)
def test_tft_force_include_is_per_library_and_preserves_existing_include_pair(self):
self.assertTrue(TRIM.source_selected("+<helpers/ui/ST7735Display.cpp>",
"helpers/ui/ST7735Display.cpp"))
self.assertFalse(TRIM.source_selected("+<helpers/ui/*.cpp> -<helpers/ui/ST7735Display.cpp>",
"helpers/ui/ST7735Display.cpp"))
class LibraryEnvironment(dict):
def Append(self, **kwargs):
for key, value in kwargs.items():
self.setdefault(key, []).extend(value)
def subst(self, text):
return text.replace("$PROJECT_DIR", str(ROOT))
library = LibraryEnvironment(CCFLAGS=["-include", "SoftDeviceSvcCompat.h"])
source = object()
self.assertIs(TRIM.trim_tft_library(library, source), source)
self.assertEqual(library["CCFLAGS"].count("-include"), 2)
before = list(library["CCFLAGS"])
TRIM.trim_tft_library(library, source)
self.assertEqual(library["CCFLAGS"], before)
def test_install_does_not_mutate_excluded_roles_and_scopes_wrapper_build(self):
class Platform:
name = "nordicnrf52"
def get_package_dir(self, _name):
return "sdk"
class BuildEnvironment(dict):
def PioPlatform(self):
return Platform()
def BoardConfig(self):
return {"build.mcu": "nrf52840"}
def AppendUnique(self, **kwargs):
for name, values in kwargs.items():
for value in values:
if value not in self.setdefault(name, []):
self[name].append(value)
def BuildSources(self, *args):
self.setdefault("BUILT_SOURCES", []).append(args)
base = dict(BUILD_FLAGS=["-DUSE_CC310_HW_CRYPTO=1"],
SRC_FILTER=["+<../examples/simple_repeater>"], CPPDEFINES=[])
excluded = BuildEnvironment(**base, PIOENV="RAK_4631_companion_radio_full")
before = dict(excluded)
TRIM.install(excluded)
self.assertEqual(excluded, before)
active = BuildEnvironment(BUILD_FLAGS=list(base["BUILD_FLAGS"]),
SRC_FILTER=list(base["SRC_FILTER"]), CPPDEFINES=[],
PIOENV="RAK_4631_repeater")
with patch.object(TRIM, "supported_cc310", return_value=True):
TRIM.install(active)
self.assertTrue(active["MESH_NRF52_FLASH_TRIM_ACTIVE"])
self.assertIn("-Wl,--wrap=CRYS_ECPKI_GetEcDomain", active["LINKFLAGS"])
self.assertEqual(active["BUILT_SOURCES"][0][-1], "+<Cc310DomainTrim.c>")
@unittest.skipUnless(shutil.which("cc"), "a native C compiler is required")
def test_wrapped_domain_preserves_p256_and_rejects_unused_curves(self):
fixture = ROOT / "test/fixtures/nrf52_flash_trim"
with tempfile.TemporaryDirectory() as temp_dir:
executable = Path(temp_dir) / "cc310-trim-test"
subprocess.run(["cc", "-std=c99", "-Wall", "-Wextra", "-Werror",
"-DMESH_NRF52_FLASH_TRIM=1", "-I" + str(fixture),
str(ROOT / "src/helpers/nrf52/Cc310DomainTrim.c"),
str(fixture / "test_domain_trim.c"), "-o", str(executable)], check=True)
subprocess.run([str(executable)], check=True)
if __name__ == "__main__":
unittest.main()
+318
View File
@@ -0,0 +1,318 @@
"""Font notices travel with licensed firmware without changing DFU payloads."""
import hashlib
import json
from pathlib import Path
import shutil
import subprocess
import struct
import sys
import tempfile
import unittest
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import package_nrf52_font_license as font_license
import firmware_memory_manifest as memory
import package_cascade_release as release
import nrf52_flash_trim as trim
class BuildEnvironment(dict):
def __init__(self, **changes):
super().__init__(MESH_NRF52_FLASH_TRIM_ACTIVE=True,
PIOENV="Heltec_t114_repeater", BUILD_FLAGS=[],
CPPDEFINES=["NRF52_PLATFORM", "ST7789", ("MESH_NRF52_FLASH_TRIM", 1)])
self.update(changes)
self.actions = []
self.dependencies = []
def Depends(self, target, sources):
self.dependencies.append((target, sources))
def AddPostAction(self, target, action):
self.actions.append((target, action))
def subst(self, value):
return (value.replace("$PROJECT_DIR", str(ROOT))
.replace("$BUILD_DIR", str(self.get("BUILD_DIR", "build")))
.replace("${PROGNAME}", "firmware"))
def PioPlatform(self):
class Platform:
name = "nordicnrf52"
def get_package_dir(self, _name):
return None
return Platform()
def BoardConfig(self):
return {"build.mcu": "nrf52840"}
def AppendUnique(self, **changes):
for name, values in changes.items():
for value in values:
if value not in self.setdefault(name, []):
self[name].append(value)
class Nrf52FontLicenseTest(unittest.TestCase):
def dfu(self, path):
manifest = {"manifest": {"application": {"bin_file": "firmware.bin",
"dat_file": "firmware.dat"}}}
with ZipFile(path, "w") as archive:
for name, data in (("manifest.json", json.dumps(manifest).encode()),
("firmware.bin", bytes(range(256)) * 8),
("firmware.dat", b"original-init-packet")):
info = ZipInfo(name, (2026, 1, 2, 3, 4, 6))
info.compress_type = ZIP_DEFLATED
archive.writestr(info, data)
return manifest
def test_notice_matches_generated_source_copyright_license_and_provenance(self):
notice = font_license.notice_bytes().decode()
generated = (ROOT / "src/helpers/ui/CompactNotoFonts.h").read_text(encoding="utf-8")
copyright = "Copyright 2015 Google Inc. All Rights Reserved."
end = "OTHER DEALINGS IN THE FONT SOFTWARE."
source_license = generated.split(copyright, 1)[1].split(end, 1)[0]
packaged_license = notice.split(copyright, 1)[1].split(end, 1)[0]
self.assertEqual(source_license, packaged_license)
for item in (copyright, "MeshFontProduction9", "OFL-1.1",
"4efc2789b9fcaa87b6b55f19c25ac9a2919e4f9ca31d6b5b97bea7977d729d4f",
"2.000;GOOG;noto-source:20170915:90ef993387c0"):
self.assertIn(item, notice)
self.assertIn(item, generated)
self.assertNotIn(b"\r", font_license.notice_bytes())
def test_only_effective_production_mode_installs_for_qualified_infrastructure(self):
for role in ("repeater", "room_server", "sensor", "kiss"):
self.assertTrue(font_license.qualifies(BuildEnvironment(PIOENV="custom_" + role)))
self.assertTrue(font_license.qualifies(BuildEnvironment(BUILD_FLAGS="-DMESH_NRF52_FONT_MODE=9")))
self.assertTrue(font_license.qualifies(BuildEnvironment(
BUILD_FLAGS="-DMESH_NRF52_FONT_MODE=0 -DMESH_ARIAL_EXPERIMENT=9")))
exclusions = (
dict(MESH_NRF52_FLASH_TRIM_ACTIVE=False),
dict(PIOENV="Heltec_t114_companion_radio_ble"),
dict(BUILD_FLAGS='-DOTA_VARIANT=\'"custom_companion_radio_full"\''),
dict(BUILD_FLAGS="-DCOMPANION_FEATURE_USB_MOTA_SOURCE=1"),
dict(BUILD_FLAGS="-DCOMPANION_RADIO_FULL=0"),
dict(BUILD_FLAGS="-UMESH_NRF52_FLASH_TRIM"),
dict(BUILD_FLAGS="-DMESH_NRF52_FLASH_TRIM=0"),
dict(BUILD_FLAGS="-UST7789"), dict(BUILD_FLAGS="-UNRF52_PLATFORM"),
dict(BUILD_FLAGS="-DMESH_NRF52_FONT_MODE=0"),
dict(BUILD_FLAGS="-DMESH_NRF52_FONT_MODE=9 -DMESH_ARIAL_EXPERIMENT=7"),
dict(BUILD_FLAGS="-DMESH_ARIAL_EXPERIMENT=unknown"),
dict(CPPDEFINES=["NRF52_PLATFORM", ("MESH_NRF52_FLASH_TRIM", 1)]),
)
for changes in exclusions:
with self.subTest(changes=changes):
env = BuildEnvironment(**changes)
before = dict(env)
self.assertFalse(font_license.install(env))
self.assertEqual(env, before)
self.assertEqual(env.actions, [])
self.assertEqual(env.dependencies, [])
def test_install_dependencies_and_cached_build_callback(self):
with tempfile.TemporaryDirectory() as temp:
directory = Path(temp)
self.dfu(directory / "firmware.zip")
env = BuildEnvironment(BUILD_DIR=directory)
self.assertTrue(font_license.install(env))
self.assertFalse(font_license.install(env))
self.assertEqual([target for target, _action in env.actions],
["$BUILD_DIR/${PROGNAME}.zip", "buildprog"])
self.assertIn(str(font_license.NOTICE_PATH), env.dependencies[0][1])
self.assertIn(str(ROOT / "scripts/package_nrf52_font_license.py"), env.dependencies[0][1])
env.actions[1][1]([], [], env)
self.assertEqual((directory / "firmware.font-license.txt").read_bytes(), font_license.notice_bytes())
before = (directory / "firmware.zip").read_bytes()
(directory / "firmware.font-license.txt").unlink()
env.actions[1][1]([], [], env)
self.assertEqual((directory / "firmware.zip").read_bytes(), before)
self.assertTrue((directory / "firmware.font-license.txt").is_file())
def test_trim_install_integrates_notice_for_display_infrastructure_only(self):
for role in ("simple_repeater", "simple_room_server", "simple_sensor", "kiss_modem"):
env = BuildEnvironment(MESH_NRF52_FLASH_TRIM_ACTIVE=False,
CPPDEFINES=["NRF52_PLATFORM", "ST7789"],
SRC_FILTER=["+<../examples/" + role + ">"])
trim.install(env)
self.assertTrue(env["MESH_NRF52_FONT_LICENSE_INSTALLED"])
self.assertEqual(len(env.actions), 2)
for changes in (
dict(CPPDEFINES=["NRF52_PLATFORM"]),
dict(PIOENV="Heltec_t114_companion_radio_usb"),
dict(SRC_FILTER=["+<../examples/companion_radio>"]),
dict(BUILD_FLAGS="-DMESH_NRF52_FONT_MODE=0")):
env = BuildEnvironment(MESH_NRF52_FLASH_TRIM_ACTIVE=False,
SRC_FILTER=["+<../examples/simple_repeater>"])
env.update(changes)
trim.install(env)
self.assertNotIn("MESH_NRF52_FONT_LICENSE_INSTALLED", env)
self.assertEqual(env.actions, [])
def test_zip_original_entries_manifest_and_physical_payload_bytes_are_unchanged(self):
with tempfile.TemporaryDirectory() as temp:
directory = Path(temp)
package = directory / "firmware.zip"
self.dfu(package)
with ZipFile(package) as archive:
entries = {info.filename: (archive.read(info), vars(info).copy()
if hasattr(info, "__dict__") else
(info.CRC, info.compress_size, info.file_size, info.date_time, info.header_offset,
info.compress_type, info.external_attr)) for info in archive.infolist()}
original_payload_prefix = package.read_bytes()[:archive.start_dir]
sidecar = font_license.package_font_license(package)
self.assertEqual(package.read_bytes()[:len(original_payload_prefix)], original_payload_prefix)
with ZipFile(package) as archive:
self.assertEqual(set(archive.namelist()), set(entries) | {"FONT-LICENSE.txt"})
for name, (data, attributes) in entries.items():
info = archive.getinfo(name)
actual = vars(info).copy() if hasattr(info, "__dict__") else (
info.CRC, info.compress_size, info.file_size, info.date_time, info.header_offset,
info.compress_type, info.external_attr)
self.assertEqual(archive.read(name), data)
self.assertEqual(actual, attributes)
self.assertIsNone(archive.testzip())
self.assertEqual(archive.read("FONT-LICENSE.txt"), sidecar.read_bytes())
self.assertEqual(archive.getinfo("FONT-LICENSE.txt").date_time, (1980, 1, 1, 0, 0, 0))
def test_deterministic_and_idempotent_without_rewriting_cached_files(self):
with tempfile.TemporaryDirectory() as temp:
directory = Path(temp)
first, second = directory / "one.zip", directory / "two.zip"
self.dfu(first)
shutil.copyfile(first, second)
sidecar = font_license.package_font_license(first)
font_license.package_font_license(second)
self.assertEqual(first.read_bytes(), second.read_bytes())
before = (first.read_bytes(), first.stat().st_mtime_ns, sidecar.stat().st_mtime_ns)
font_license.package_font_license(first)
self.assertEqual((first.read_bytes(), first.stat().st_mtime_ns, sidecar.stat().st_mtime_ns), before)
def test_refuses_foreign_entries_and_sidecars_before_mutating_zip(self):
with tempfile.TemporaryDirectory() as temp:
directory = Path(temp)
package = directory / "firmware.zip"
self.dfu(package)
sidecar = directory / "firmware.font-license.txt"
sidecar.write_bytes(b"user's unrelated text")
before = package.read_bytes()
with self.assertRaisesRegex(ValueError, "refusing to overwrite"):
font_license.package_font_license(package)
self.assertEqual(package.read_bytes(), before)
self.assertEqual(sidecar.read_bytes(), b"user's unrelated text")
sidecar.unlink()
with ZipFile(package, "a") as archive:
archive.writestr("FONT-LICENSE.txt", b"another license")
before = package.read_bytes()
with self.assertRaisesRegex(ValueError, "unexpected or duplicate"):
font_license.package_font_license(package)
self.assertEqual(package.read_bytes(), before)
self.assertFalse(sidecar.exists())
def test_collection_ignores_stale_source_notice_and_rejects_stale_output(self):
with tempfile.TemporaryDirectory() as temp:
directory = Path(temp)
build = directory / "build"
output = directory / "out"
build.mkdir()
output.mkdir()
self.dfu(build / "firmware.zip")
source_sidecar = build / "firmware.font-license.txt"
source_sidecar.write_bytes(font_license.notice_bytes())
stem = output / "unaffected"
self.assertIsNone(font_license.collect_build_notice(build, stem))
self.assertTrue(source_sidecar.exists())
stale = output / "unaffected.font-license.txt"
stale.write_bytes(font_license.notice_bytes())
with self.assertRaisesRegex(ValueError, "stale output"):
font_license.collect_build_notice(build, stem)
self.assertEqual(stale.read_bytes(), font_license.notice_bytes())
def test_notice_is_staged_hashed_and_tampering_or_removal_is_rejected(self):
with tempfile.TemporaryDirectory() as temp:
directory = Path(temp)
build, output = directory / "build", directory / "out"
build.mkdir()
output.mkdir()
self.dfu(build / "firmware.zip")
font_license.package_font_license(build / "firmware.zip")
label = "v1.17.1.5-halo-keymind-cascade-dev"
version = label + "-deadbeef"
stem = output / ("Heltec_t114_repeater-" + version)
shutil.copyfile(build / "firmware.zip", output / (stem.name + ".zip"))
uf2_data = (struct.pack("<8I", 0x0A324655, 0x9E5D5157, 0x2000, 0x26000,
256, 0, 1, 0xADA52840)
+ b"u" * 256 + b"\0" * 220 + struct.pack("<I", 0x0AB16F30))
(output / (stem.name + ".uf2")).write_bytes(uf2_data)
notice = font_license.collect_build_notice(build, stem)
manifest = dict(schema_version=2, target="Heltec_t114_repeater",
artifact_target="Heltec_t114_repeater", platform="NRF52_PLATFORM",
verified=True, ota_update_verified=True, verification=[], build_profile="standard")
(output / (stem.name + ".capabilities.json")).write_text(json.dumps(manifest))
elf = build / "firmware.elf"
elf.write_bytes(b"qualified-ELF")
report = dict(schema_version=1, passed=True, available_internal_bytes=80000,
required_heap_bytes=50000, largest_internal_region_bytes=80000,
required_contiguous_bytes=5120,
elf_sha256=hashlib.sha256(elf.read_bytes()).hexdigest(), target="build_env")
(build / "firmware.memory.json").write_text(json.dumps(report))
memory.package_report(build, stem)
proof = memory.validate_package(stem)
self.assertEqual(proof["files"][notice.name], hashlib.sha256(notice.read_bytes()).hexdigest())
records = release.collect_artifacts(output, version)
self.assertIn(notice, records[0]["files"])
self.assertEqual(sum(path == notice for path in records[0]["files"]), 1)
self.assertEqual((output / (stem.name + ".uf2")).read_bytes(), uf2_data)
commit = "deadbeef" + "0" * 32
status = directory / "status"
status.write_text("state=completed\nexit_code=0\n"
f"working_directory={directory}\noutput_directory=out\n"
f"source_commit={commit}\nfirmware_version={label}\nfirmware_profile=cascade\n"
"radio_frequency=909.5\nradio_bandwidth=500\nradio_sf=5\nradio_cr=5\n")
result = subprocess.run([sys.executable, str(ROOT / "scripts/package_cascade_release.py"),
"--input", str(output), "--output", str(directory / "stage"),
"--build-status", str(status), "--commit", commit],
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
staged = directory / "stage/repeater-room"
self.assertEqual((staged / notice.name).read_bytes(), notice.read_bytes())
self.assertIn(notice.name, (staged / "TARGET-MANIFEST.json").read_text())
checksums = dict(line.split(" ", 1)[::-1] for line in
(staged / "SHA256SUMS.txt").read_text().splitlines())
self.assertEqual(checksums[notice.name], hashlib.sha256(notice.read_bytes()).hexdigest())
notice.write_bytes(b"tampered")
with self.assertRaisesRegex(ValueError, "mismatched font-license"):
memory.validate_package(stem)
notice.unlink()
with self.assertRaisesRegex(ValueError, "missing or mismatched"):
memory.validate_package(stem)
def test_adafruit_loader_accepts_extra_notice_when_installed(self):
site_packages = (Path.home() / ".platformio/packages/tool-adafruit-nrfutil/site-packages")
if not (site_packages / "nordicsemi/dfu/package.py").is_file():
self.skipTest("local Adafruit nRF DFU utility is not installed")
with tempfile.TemporaryDirectory() as temp:
directory = Path(temp)
package = directory / "firmware.zip"
self.dfu(package)
font_license.package_font_license(package)
code = (
"import sys; from pathlib import Path; "
"sys.path.insert(0,sys.argv[1]); "
"from nordicsemi.dfu.package import Package; "
"m=Package.unpack_package(sys.argv[2],sys.argv[3]); "
"assert m.application.bin_file=='firmware.bin'; "
"assert m.application.dat_file=='firmware.dat'; "
"assert (Path(sys.argv[3])/'FONT-LICENSE.txt').is_file()"
)
result = subprocess.run([sys.executable, "-c", code, str(site_packages), str(package),
str(directory / "unpacked")], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()
+34
View File
@@ -0,0 +1,34 @@
"""Unused TFT fonts are omitted without altering the selected hardware setup."""
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "test" / "fixtures" / "nrf52_tft_trim"
class Nrf52TftFontTrimTest(unittest.TestCase):
def test_selected_setup_and_current_font_survive(self):
cases = (
([], False),
(["-DNRF52_PLATFORM=1"], False),
(["-DMESH_NRF52_FLASH_TRIM=1"], False),
(["-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=0"], False),
(["-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1"], True),
)
with tempfile.TemporaryDirectory() as temp_dir:
for flags, trimmed in cases:
with self.subTest(flags=flags):
executable = Path(temp_dir) / "font_scope"
subprocess.run([
"c++", "-std=c++11", "-Wall", "-Wextra", "-Werror",
*flags, f"-DEXPECT_TRIM={int(trimmed)}",
f"-I{FIXTURES}", f"-I{ROOT / 'src'}",
str(FIXTURES / "test.cpp"), "-o", str(executable),
], check=True)
subprocess.run([str(executable)], check=True)
if __name__ == "__main__":
unittest.main()
+247
View File
@@ -0,0 +1,247 @@
"""nRF52 font storage choices preserve renderer behavior and stay in scope."""
import hashlib
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "test" / "fixtures" / "oled_font_experiments"
class OledFontExperimentsTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temp = tempfile.TemporaryDirectory()
cls.directory = Path(cls.temp.name)
cls.decoder = cls.directory / "tinf.o"
subprocess.run([
"cc", "-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c",
str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(cls.decoder),
], check=True)
cls.results = {}
@classmethod
def tearDownClass(cls):
cls.temp.cleanup()
def run_renderer(self, mode, flags=(), signed_char=False, sanitized=False, explicit=True):
key = (mode, tuple(flags), signed_char, sanitized, explicit)
if key in self.results:
return self.results[key]
executable = self.directory / f"font_{len(self.results)}"
subprocess.run([
"c++", "-std=c++11", "-O2", "-Wall", "-Wextra",
"-Wno-implicit-fallthrough", "-DARDUINO=1",
"-fsigned-char" if signed_char else "-funsigned-char",
*( ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] if sanitized else [] ),
"-DOLEDDISPLAY_REDUCE_MEMORY=1",
*([f"-DMESH_ARIAL_EXPERIMENT={mode}"] if explicit else []),
f"-DEXPECT_MODE={mode if not flags else 0}",
*(flags or ("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1")),
f"-I{FIXTURES}", f"-I{ROOT / 'src'}",
str(FIXTURES / "test.cpp"),
str(ROOT / "src/helpers/ui/OLEDDisplay.cpp"),
str(ROOT / "src/helpers/ui/OLEDDisplayFonts.cpp"),
str(self.decoder), "-o", str(executable),
], check=True)
symbols = subprocess.run(["nm", "-C", "--defined-only", str(executable)],
text=True, capture_output=True, check=True).stdout
if not flags and mode != 0:
self.assertNotIn("ArialMT_Plain_10", symbols, "Unused initial Arial10 is still linked")
result = subprocess.run([str(executable)], text=True, capture_output=True, check=True)
self.results[key] = result.stdout.splitlines()
return self.results[key]
def test_all_glyphs_metrics_and_framebuffers_match(self):
baseline = self.run_renderer(0)
self.assertEqual(baseline[0], "initial 13")
self.assertEqual([line.split()[0] for line in baseline[1:]], [
"glyphs", "clipping", "utf8", "wrapping", "controls", "bitmaps", "logs", "custom"])
self.assertTrue(baseline[1].startswith("glyphs 448 "))
self.assertTrue(baseline[2].startswith("clipping 960 "))
for mode in (1, 2, 3):
with self.subTest(mode=mode):
actual = self.run_renderer(mode)
self.assertEqual(actual[0], "initial 19")
self.assertEqual(actual[1:], baseline[1:])
def test_experiment_guards_signed_char_latin1_indices(self):
baseline = self.run_renderer(0)
for mode in (1, 2, 3):
with self.subTest(mode=mode):
self.assertEqual(self.run_renderer(mode, signed_char=True)[1:], baseline[1:])
def test_experimental_renderer_passes_memory_and_undefined_behavior_checks(self):
baseline = self.run_renderer(0)
for mode in (1, 2, 3):
with self.subTest(mode=mode):
actual = self.run_renderer(mode, signed_char=True, sanitized=True)
self.assertEqual(actual[1:], baseline[1:])
def test_tinf_is_enabled_only_for_effective_zopfli_font_or_existing_features(self):
qualified = ("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1")
cases = [(*qualified, f"-DMESH_ARIAL_EXPERIMENT={mode}") for mode in range(10)]
expected = [mode in (3, 9) for mode in range(10)]
cases.append(qualified) # No override: production compressed Noto font.
expected.append(True)
cases.extend([
(*qualified, "-DMESH_NRF52_FONT_MODE=0"),
(*qualified, "-DMESH_NRF52_FONT_MODE=9"),
(*qualified, "-DMESH_NRF52_FONT_MODE=9", "-DMESH_ARIAL_EXPERIMENT=0"),
])
expected.extend((False, True, False))
excluded = (
("-DNRF52_PLATFORM=1", "-DST7789=1"),
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=0", "-DST7789=1"),
("-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1"),
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1"),
(*qualified, "-DCOMPANION_RADIO_FULL=1"),
)
cases.extend((*flags, "-DMESH_ARIAL_EXPERIMENT=3") for flags in excluded)
expected.extend(False for _ in excluded)
cases.extend((*flags, "-DMESH_ARIAL_EXPERIMENT=9") for flags in excluded)
expected.extend(False for _ in excluded)
cases.extend(excluded) # Production default remains off outside scope.
expected.extend(False for _ in excluded)
cases.extend((*flags, "-DMESH_NRF52_FONT_MODE=9") for flags in excluded)
expected.extend(False for _ in excluded)
cases.extend([
("-DENABLE_OTA=1",), ("-DOTA_TRANSPORT_DEFLATE_TEST=1",),
("-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"),
])
expected.extend((True, True, True))
for flags, decoder_enabled in zip(cases, expected):
with self.subTest(flags=flags):
output = self.directory / "guard.o"
subprocess.run([
"cc", "-Os", *flags, "-c", str(ROOT / "src/helpers/ota/OtaTinf.c"),
"-o", str(output),
], check=True)
symbols = subprocess.run(
["nm", "-g", "--defined-only", str(output)],
text=True, capture_output=True, check=True).stdout
self.assertEqual("tinf_uncompress_exact" in symbols, decoder_enabled)
def test_similar_size_family_trials_match_raw_pixels_and_keep_full_coverage(self):
families = []
for mode in (4, 5, 6, 7, 8):
with self.subTest(mode=mode):
unsigned = self.run_renderer(mode)
self.assertEqual(unsigned[0], "initial 19")
self.assertTrue(unsigned[1].startswith("glyphs 448 "))
self.assertTrue(unsigned[2].startswith("clipping 960 "))
# The fixture compares every glyph against a separate
# pixel-at-a-time reference and checks 189 drawable slots,
# exact 19/28 line boxes and 12/17-pixel H cap heights.
self.assertEqual(self.run_renderer(mode, signed_char=True), unsigned)
self.assertEqual(self.run_renderer(mode, signed_char=True, sanitized=True), unsigned)
families.append(unsigned[1])
self.assertEqual(len(set(families)), 5, "family trials unexpectedly contain the same pixels")
def test_production_compressed_noto_default_matches_raw_family_exactly(self):
raw_noto = self.run_renderer(7)
compressed = self.run_renderer(9)
self.assertEqual(compressed, raw_noto)
self.assertEqual(self.run_renderer(9, explicit=False), raw_noto)
self.assertEqual(self.run_renderer(9, signed_char=True), raw_noto)
self.assertEqual(self.run_renderer(9, signed_char=True, sanitized=True), raw_noto)
self.assertEqual(self.run_renderer(9, signed_char=True, sanitized=True, explicit=False), raw_noto)
def test_production_font_optout_restores_original_renderer(self):
qualified = ("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1")
baseline = self.run_renderer(0)
self.assertEqual(self.run_renderer(
9, (*qualified, "-DMESH_NRF52_FONT_MODE=0"), explicit=False), baseline)
self.assertEqual(self.run_renderer(
0, (*qualified, "-DMESH_NRF52_FONT_MODE=9")), baseline)
def test_experiment_is_disabled_outside_nrf52_infrastructure_st7789(self):
baseline = self.run_renderer(0)
excluded = (
("-DNRF52_PLATFORM=1", "-DST7789=1"), # Companion: no trim macro
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=0", "-DST7789=1"),
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1",
"-DCOMPANION_RADIO_FULL=1"),
("-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1"), # not nRF52
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1"), # another display
)
for mode in (3, 8, 9):
for flags in excluded:
with self.subTest(mode=mode, flags=flags):
self.assertEqual(self.run_renderer(mode, flags), baseline)
for flags in excluded:
with self.subTest(default=True, flags=flags):
self.assertEqual(self.run_renderer(9, flags, explicit=False), baseline)
with self.subTest(override=True, flags=flags):
self.assertEqual(self.run_renderer(
9, (*flags, "-DMESH_NRF52_FONT_MODE=9"), explicit=False), baseline)
class FontTrialSourceAuditTest(unittest.TestCase):
def test_pinned_source_glyphs_keep_all_ink_pixels_when_sources_are_available(self):
# Normal firmware builds/CI need no TrueType files or rasterizer. The
# optional regeneration audit runs on the host that owns pinned fonts.
try:
from PIL import Image, ImageDraw, ImageFont, features
except ImportError:
self.skipTest("Pinned rasterizer is not installed")
sys.path.insert(0, str(ROOT / "scripts"))
import generate_font_family_trials as generator
if Image.__version__ != generator.PILLOW_VERSION or features.version("freetype2") != generator.FREETYPE_VERSION:
self.skipTest("Pinned Pillow/FreeType versions are not installed")
font_directory = Path("C:/Windows/Fonts")
if not font_directory.exists():
font_directory = Path("/mnt/c/Windows/Fonts")
if not all((font_directory / family.filename).exists() for family in generator.FAMILIES):
self.skipTest("Pinned source font files are not available")
header = (ROOT / "src/helpers/ui/FontFamilyTrials.h").read_text(encoding="utf-8")
for family in generator.FAMILIES:
source = font_directory / family.filename
self.assertEqual(hashlib.sha256(source.read_bytes()).hexdigest(), family.sha256)
section_match = re.search(
rf"#if MESH_ARIAL_EXPERIMENT == {family.mode}\b(.*?)#endif", header, re.S)
self.assertIsNotNone(section_match, "Generated header lacks a configured font family")
section = section_match[1]
for font_id, size in enumerate(family.sizes, 1):
body = re.search(rf"static const uint8_t TRIAL_{font_id}\[\] = \{{(.*?)\}};", section, re.S)[1]
data = bytes(int(value, 16) for value in re.findall(r"0x([0-9a-fA-F]+)", body))
first, count, height = data[2], data[3], data[1]
self.assertEqual((first, count, height), (32, 224, 19 if font_id == 1 else 28))
source_font = ImageFont.truetype(str(source), size, layout_engine=ImageFont.Layout.BASIC)
drawable = 0
for slot in range(count):
entry = data[4 + 4 * slot:8 + 4 * slot]
offset = int.from_bytes(entry[:2], "big")
length = entry[2]
if offset == 0xffff:
self.assertEqual(length, 0)
continue
drawable += 1
bitmap = data[4 + 4 * count + offset:4 + 4 * count + offset + length]
stride = (height + 7) // 8
encoded = {(index // stride, 8 * (index % stride) + bit)
for index, value in enumerate(bitmap)
for bit in range(8) if value & (1 << bit)}
image = Image.new("L", (256, 256), 0)
ImageDraw.Draw(image).text((96, 96), chr(first + slot),
font=source_font, fill=255, anchor="ls")
image = image.point(lambda value: 255 if value >= 128 else 0)
box = image.getbbox()
self.assertIsNotNone(box)
original = {(x, y) for y in range(box[1], box[3])
for x in range(box[0], box[2]) if image.getpixel((x, y))}
def normalize(pixels):
left = min(x for x, _ in pixels)
top = min(y for _, y in pixels)
return {(x - left, y - top) for x, y in pixels}
with self.subTest(mode=family.mode, size=size, code=first + slot):
self.assertEqual(normalize(encoded), normalize(original),
"Source ink was cropped or altered during bitmap placement")
self.assertEqual(drawable, 189)
if __name__ == "__main__":
unittest.main()
+139
View File
@@ -0,0 +1,139 @@
"""All OTA names survive front coding; original Companion API stays unchanged."""
import re
import subprocess
import sys
import tempfile
import unittest
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(ROOT / "tools" / "mota"), str(ROOT / "scripts")]
import gen_targets
HEADER = ROOT / "src" / "helpers" / "ota" / "OtaTargets.h"
HARNESS = ROOT / "test" / "fixtures" / "ota_target_names" / "test.cpp"
class OtaTargetNameTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.text = HEADER.read_text(encoding="utf-8")
cls.rows = [(int(target, 16), name) for target, name in re.findall(
r'\{ 0x([0-9a-fA-F]{8}), "([^"]+)" \},', cls.text)]
def compile_and_lookup(self, flags, ids, header=None):
with tempfile.TemporaryDirectory() as temp_dir:
executable = Path(temp_dir) / "target_names"
if header is not None:
alternate = Path(temp_dir) / "helpers/ota"
alternate.mkdir(parents=True)
(alternate / "OtaTargets.h").write_text(header, encoding="utf-8")
(alternate / "tinf").mkdir()
(alternate / "tinf/tinf.h").write_text(
(ROOT / "src/helpers/ota/tinf/tinf.h").read_text(), encoding="utf-8")
decoder = Path(temp_dir) / "tinf.o"
subprocess.run([
"cc", "-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c",
str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(decoder),
], check=True)
subprocess.run([
"c++", "-std=c++11", "-O2", "-Wall", "-Wextra", "-Werror",
*flags, f"-I{temp_dir}", f"-I{ROOT / 'src'}", str(HARNESS), str(decoder), "-o", str(executable),
], check=True)
result = subprocess.run(
[str(executable)], input="\n".join(f"{target:08x}" for target in ids),
text=True, capture_output=True, check=True)
return result.stdout.splitlines()
def test_generator_reproduces_checked_in_header_and_all_ids(self):
self.assertGreater(len(self.rows), 600)
self.assertEqual(self.rows, gen_targets.target_rows(name for _, name in self.rows))
self.assertEqual(self.text, gen_targets.generate_header(self.rows))
def test_zopfli_chunks_retain_every_name_and_have_bounded_output(self):
data, offsets, raw_lengths, firsts = gen_targets.compressed_name_chunks(self.rows)
actual = []
for group, raw_length in enumerate(raw_lengths):
raw = zlib.decompress(data[offsets[group]:offsets[group + 1]], -15)
self.assertEqual(len(raw), raw_length)
self.assertLessEqual(raw_length, 512)
position = 0
previous = b""
for row in range(firsts[group], firsts[group + 1]):
prefix = raw[position]
self.assertLessEqual(prefix, len(previous))
end = raw.index(0, position + 1)
previous = previous[:prefix] + raw[position + 1:end]
position = end + 1
actual.append(previous.decode("utf-8"))
self.assertEqual(position, len(raw))
self.assertEqual(actual, [name for _, name in self.rows])
original_size = sum(8 + len(name.encode("utf-8")) + 1 for _, name in self.rows)
compressed_size = 4 * len(self.rows) + 2 * (len(offsets) + len(raw_lengths) + len(firsts)) + len(data)
self.assertGreater(original_size - compressed_size, 19000)
def test_generator_requires_google_zopfli_1000(self):
from unittest.mock import patch
sys.path.insert(0, str(ROOT / "scripts"))
import zopfli_compress
gen_targets._compressed_name_chunks.cache_clear()
original = zopfli_compress.raw_deflate_compress
with patch.object(zopfli_compress, "raw_deflate_compress", wraps=original) as compress:
gen_targets.compressed_name_chunks([(1, "unique_test_target")])
self.assertEqual(compress.call_count, 1)
self.assertEqual(compress.call_args.kwargs, {"numiterations": 1000})
def test_cpp_standard_and_compact_match_all_names(self):
ids = [target for target, _ in self.rows] + [0, 0xffffffff]
expected = [name for _, name in self.rows] + ["?", "?"]
for flags in ([], ["-DOTA_TARGET_NAME_FRONT_CODED=1"]):
with self.subTest(flags=flags):
self.assertEqual(self.compile_and_lookup(flags, ids), expected)
def test_table_disabled_preserves_optional_local_name(self):
local_id, local_name = self.rows[0]
ids = [local_id, self.rows[1][0], 0]
for compressed in (0, 1):
flags = ["-DOTA_TARGET_NAME_TABLE=0", f"-DOTA_TARGET_NAME_FRONT_CODED={compressed}"]
with self.subTest(compressed=compressed):
self.assertEqual(self.compile_and_lookup(flags, ids), ["?", "?", "?"])
self.assertEqual(self.compile_and_lookup(flags + [
f"-DOTA_LOCAL_TARGET_ID=0x{local_id:08x}",
f'-DOTA_LOCAL_TARGET_NAME="{local_name}"',
], ids), [local_name, "?", "?"])
def test_cpp_decoder_fails_closed_on_invalid_records_and_trailing_bytes(self):
from zopfli_compress import raw_deflate_compress
rows = [(1, "hello")]
header = gen_targets.generate_header(rows)
original, _, _, _ = gen_targets.compressed_name_chunks(rows)
invalid = [
raw_deflate_compress(b"\xffhello\0", numiterations=1000), # invalid restart prefix
raw_deflate_compress(b"\0hello!", numiterations=1000), # missing terminator
original + b"\0", # trailing whole byte
original[:-1], # truncated input
]
for stream in invalid:
altered = re.sub(
r"(static const uint8_t NAMES\[\] = \{).*?(\n \};)",
lambda match: match[1] + "\n " + ", ".join(f"0x{byte:02x}" for byte in stream) + "," + match[2],
header, flags=re.S)
altered = re.sub(
r"(static const uint16_t OFFSETS\[\] = \{).*?(\n \};)",
lambda match: match[1] + f"\n 0, {len(stream)}," + match[2],
altered, flags=re.S)
with self.subTest(stream=stream.hex()):
self.assertEqual(self.compile_and_lookup(
["-DOTA_TARGET_NAME_FRONT_CODED=1"], [1], altered), ["?"])
def test_generator_rejects_unencodable_names(self):
for name in ("x\0y", "x" * 256):
with self.subTest(name=name):
with self.assertRaises(ValueError):
gen_targets.compressed_name_chunks([(1, name)])
if __name__ == "__main__":
unittest.main()
+149 -7
View File
@@ -17,6 +17,7 @@ import json
import os
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
import motalib as ml
@@ -24,6 +25,7 @@ import motalib as ml
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "src" / "helpers" / "ota" / "OtaTargets.h"
INTERNAL_BOOTLOADER_TARGETS = ROOT / "tools" / "mota" / "nrf52_internal_bootloader_targets.txt"
NAME_DECODE_CHUNK_SIZE = 512
def resolved_config():
@@ -58,10 +60,8 @@ def release_aliases():
})
def main():
envs = sorted(set(ota_envs(resolved_config())) | set(release_aliases()))
rows = [(ml.target_id_for_env(e), e) for e in envs]
def target_rows(envs):
rows = [(ml.target_id_for_env(e), e) for e in sorted(set(envs))]
seen = {}
for tid, e in rows: # sha2-256:4 over all names - verify no collisions
if tid in seen and seen[tid] != e:
@@ -70,10 +70,69 @@ def main():
)
seen[tid] = e
rows.sort(key=lambda r: r[1].lower()) # by env name (readable, deterministic)
return rows
def compressed_name_chunks(rows, max_chunk_size=NAME_DECODE_CHUNK_SIZE):
"""Front-code all names into bounded, independent Zopfli-1000 streams.
No OTA wire format or target ID changes. Each record is a one-byte common
prefix length followed by a NUL-terminated UTF-8 suffix. Chunk boundaries
reset the prefix, so lookup needs at most one small tinf output buffer.
"""
return _compressed_name_chunks(tuple(rows), max_chunk_size)
@lru_cache(maxsize=8)
def _compressed_name_chunks(rows, max_chunk_size):
sys.path.insert(0, str(ROOT / "scripts"))
from zopfli_compress import MAXIMUM_ITERATIONS, raw_deflate_compress
if not rows or max_chunk_size < 2 or max_chunk_size > 65535:
raise ValueError("OTA target-name chunk size or inventory is invalid")
raw_chunks = []
firsts = [0]
current = bytearray()
previous = b""
for index, (_, name) in enumerate(rows):
encoded = name.encode("utf-8")
if b"\0" in encoded or len(encoded) > 255:
raise ValueError(f"OTA target name is not encodable: {name!r}")
prefix = 0
while (prefix < min(len(previous), len(encoded)) and
previous[prefix] == encoded[prefix]):
prefix += 1
record = bytes([prefix]) + encoded[prefix:] + b"\0"
if len(current) + len(record) > max_chunk_size:
raw_chunks.append(bytes(current))
firsts.append(index)
current.clear()
prefix = 0
record = b"\0" + encoded + b"\0"
if len(record) > max_chunk_size:
raise ValueError(f"OTA target name exceeds the decode chunk: {name!r}")
current.extend(record)
previous = encoded
raw_chunks.append(bytes(current))
firsts.append(len(rows))
data = bytearray()
offsets = [0]
for raw in raw_chunks:
data.extend(raw_deflate_compress(raw, numiterations=MAXIMUM_ITERATIONS))
offsets.append(len(data))
if len(data) > 65535 or len(rows) > 65535:
raise ValueError("OTA target-name data exceeds uint16_t restart offsets")
return bytes(data), tuple(offsets), tuple(len(raw) for raw in raw_chunks), tuple(firsts)
def generate_header(rows):
data, offsets, raw_lengths, firsts = compressed_name_chunks(rows)
capacity = max((len(name.encode("utf-8")) + 1 for _, name in rows), default=1)
out = [
"#pragma once",
"#include <stdint.h>",
"#include <stddef.h>",
"",
"// AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand.",
f"// {len(rows)} OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32)",
@@ -82,12 +141,84 @@ def main():
"// Size-constrained receivers can set OTA_TARGET_NAME_TABLE=0. They still match targets by ID;",
"// OTA_LOCAL_TARGET_ID + OTA_LOCAL_TARGET_NAME optionally retain the local human-readable name.",
"",
"namespace mesh { namespace ota {",
"",
"#ifndef OTA_TARGET_NAME_TABLE",
" #define OTA_TARGET_NAME_TABLE 1",
"#endif",
"",
"#ifndef OTA_TARGET_NAME_FRONT_CODED",
" #define OTA_TARGET_NAME_FRONT_CODED 0",
"#endif",
"",
"#if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED",
' #include "tinf/tinf.h"',
"#endif",
"",
"namespace mesh { namespace ota {",
"",
"#if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED",
"// All names retained. Front coding + Google Zopfli (1,000 iterations), raw DEFLATE.",
"// Reuses the existing OTA tinf decoder; local chunk/caller buffers, never the live OTA context.",
"// This is firmware-only storage compression: target IDs and the LoRa protocol are unchanged.",
f"enum {{ OTA_TARGET_ENV_NAME_CAPACITY = {capacity} }};",
"inline const char* ota_target_env_name(uint32_t target_id, char* buffer, size_t capacity) {",
" if (!buffer || !capacity) return nullptr;",
" buffer[0] = 0;",
" static const uint32_t IDS[] = {",
]
out += [" " + ", ".join(f"0x{tid:08x}" for tid, _ in rows[i:i + 8]) + ","
for i in range(0, len(rows), 8)]
out += [
" };",
" static const uint16_t OFFSETS[] = {",
]
out += [" " + ", ".join(str(offset) for offset in offsets[i:i + 12]) + ","
for i in range(0, len(offsets), 12)]
out += [" };", " static const uint16_t RAW_LENGTHS[] = {"]
out += [" " + ", ".join(str(length) for length in raw_lengths[i:i + 12]) + ","
for i in range(0, len(raw_lengths), 12)]
out += [" };", " static const uint16_t FIRSTS[] = {"]
out += [" " + ", ".join(str(first) for first in firsts[i:i + 12]) + ","
for i in range(0, len(firsts), 12)]
out += [
" };",
f" // {len(data)} encoded bytes; {len(raw_lengths)} independent chunks, at most {NAME_DECODE_CHUNK_SIZE} decoded bytes.",
" static const uint8_t NAMES[] = {",
]
out += [" " + ", ".join(f"0x{byte:02x}" for byte in data[i:i + 16]) + ","
for i in range(0, len(data), 16)]
out += [
" };",
" for (unsigned i = 0; i < sizeof(IDS) / sizeof(IDS[0]); i++) {",
" if (IDS[i] != target_id) continue;",
" unsigned group = 0;",
" while (i >= FIRSTS[group + 1]) group++;",
f" uint8_t raw[{NAME_DECODE_CHUNK_SIZE}];",
" unsigned int produced = sizeof raw;",
" if (tinf_uncompress_exact(raw, &produced, NAMES + OFFSETS[group],",
" OFFSETS[group + 1] - OFFSETS[group]) != TINF_OK ||",
" produced != RAW_LENGTHS[group]) return nullptr;",
" const uint8_t* record = raw;",
" const uint8_t* end = raw + produced;",
" size_t previous_length = 0;",
" for (unsigned row = FIRSTS[group]; row <= i; row++) {",
" if (record == end) { buffer[0] = 0; return nullptr; }",
" size_t length = *record++;",
" if (length > previous_length) { buffer[0] = 0; return nullptr; }",
" while (record < end && *record) {",
" if (length + 1 < capacity) buffer[length] = (char)*record;",
" length++; record++;",
" }",
" if (record == end) { buffer[0] = 0; return nullptr; }",
" record++;",
" buffer[length < capacity ? length : capacity - 1] = 0;",
" if (row == i && length >= capacity) { buffer[0] = 0; return nullptr; }",
" previous_length = length;",
" }",
" return buffer;",
" }",
" return nullptr;",
"}",
"#else",
"// target_id -> env name, or nullptr if unknown. Linear scan; lookups are rare.",
"inline const char* ota_target_env_name(uint32_t target_id) {",
"#if OTA_TARGET_NAME_TABLE",
@@ -105,11 +236,22 @@ def main():
"#endif",
" return nullptr;",
"}",
"// The original flash-string API remains unchanged when compression is disabled.",
"inline const char* ota_target_env_name(uint32_t target_id, char*, size_t) {",
" return ota_target_env_name(target_id);",
"}",
"#endif",
"",
"} } // namespace mesh::ota",
]
return "\n".join(out) + "\n"
def main():
envs = sorted(set(ota_envs(resolved_config())) | set(release_aliases()))
rows = target_rows(envs)
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text("\n".join(out) + "\n")
OUT.write_text(generate_header(rows), encoding="utf-8")
print(f"wrote {OUT} ({len(rows)} OTA targets)")