mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 14:32:22 +00:00
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:
1 parent
010374d7db
commit
8a69e73226
44 files changed
+11150
-14
No files matched your search
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
Vendored
+81
@@ -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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -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);
|
||||
}
|
||||
Vendored
+61
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -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)")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user