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
+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()
|
||||
Reference in new issue
Block a user