mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-19 11:55:06 +00:00
- The `/system` FAT volume is removed. Its files are embedded in the firmware as generated C arrays. - Default-size fonts are generated at build time and used from flash. - The LVGL task and the Launcher run on PSRAM-preferred stacks. A new kernel `file_ops` API takes the file work off those threads. - `MainDispatcher` is reimplemented as an on-demand task. - More long-lived state is allocated with the PSRAM-preferring helpers. - Other fixes and improvements
231 lines
9.5 KiB
Python
231 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generates the binfont files that a device needs at its default font size, as C byte arrays.
|
|
|
|
Usage: generate-fonts.py <device-folder> <simulator: 0 or 1> <output-folder>
|
|
|
|
The fonts are generated by Buildscripts/BinfontTool, built with the host compiler. It uses the same
|
|
generator as the firmware, so the result is identical to generating the fonts on the device.
|
|
Writes generated_fonts.c and generated_fonts.h to the output folder. The entries are found by the file name that
|
|
the firmware uses for its font cache. The table is empty when no host compiler is available.
|
|
The fonts, sizes and bits per pixel must match Tactility/Source/lvgl/Fonts.cpp, FontSizes.cpp and FontCache.cpp.
|
|
"""
|
|
import os
|
|
import re
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
FONTS = os.path.join(ROOT, "Data", "system", "fonts")
|
|
DEFAULT_FONT_SIZE = 14
|
|
|
|
|
|
def read_text(path: str) -> str:
|
|
with open(path, "r", encoding="utf-8") as file:
|
|
return file.read()
|
|
|
|
|
|
def read_properties(path: str) -> dict:
|
|
properties = {}
|
|
for line in read_text(path).splitlines():
|
|
line = line.strip()
|
|
if line and not line.startswith("#") and "=" in line:
|
|
key, value = line.split("=", 1)
|
|
properties[key.strip()] = value.strip().strip('"')
|
|
return properties
|
|
|
|
|
|
def float32(value: float) -> float:
|
|
return struct.unpack("f", struct.pack("f", value))[0]
|
|
|
|
|
|
def scale(size: int, factor: float) -> int:
|
|
# Same as lround(size * factor) with a float factor, in float arithmetic
|
|
product = float32(size * float32(factor))
|
|
return int(product + 0.5) if product >= 0 else int(product - 0.5)
|
|
|
|
|
|
def scale_double(size: int, factor: float) -> int:
|
|
# Same as lround(size * factor) for a double factor
|
|
product = size * factor
|
|
return int(product + 0.5)
|
|
|
|
|
|
def get_defines(path: str) -> dict:
|
|
return {name: int(value.rstrip("uU"), 0) for name, value in re.findall(r"#define\s+(\w+)\s+(0x[0-9A-Fa-f]+u?|\d+u?)", read_text(path))}
|
|
|
|
|
|
def get_icon_names() -> dict:
|
|
source = read_text(os.path.join(ROOT, "Modules", "lvgl-module", "source", "icon_names.c"))
|
|
names = {}
|
|
for key in ("shared", "statusbar", "launcher"):
|
|
body = re.search(r"lvgl_icon_%s_names\[\]\s*=\s*\{(.*?)\};" % key, source, re.S).group(1)
|
|
names[key] = re.findall(r'"([^"]+)"', body)
|
|
return names
|
|
|
|
|
|
def get_icon_codepoints(names: list) -> list:
|
|
codepoints = {}
|
|
for line in read_text(os.path.join(FONTS, "MaterialSymbolsRounded.codepoints")).splitlines():
|
|
parts = line.split(" ")
|
|
if len(parts) == 2:
|
|
codepoints[parts[0]] = int(parts[1], 16)
|
|
return [codepoints[name] for name in names if name in codepoints]
|
|
|
|
|
|
def get_device_settings(device_folder: str, simulator: bool) -> tuple:
|
|
"""@return the default font size and the bits per pixel"""
|
|
properties = read_properties(os.path.join(device_folder, "device.properties"))
|
|
font_size = int(properties.get("font.defaultSize") or properties.get("lvgl.fontSize") or DEFAULT_FONT_SIZE)
|
|
has_psram = properties.get("hardware.spiRam", "false") == "true" and not simulator
|
|
monochrome = any(
|
|
re.search(r"ssd1306|st7305", read_text(os.path.join(device_folder, name)))
|
|
for name in os.listdir(device_folder) if name.endswith(".dts")
|
|
)
|
|
# Same choice as getGeneratedFontBpp()
|
|
bpp = 1 if monochrome else (4 if has_psram else 2)
|
|
return font_size, bpp
|
|
|
|
|
|
def get_jobs(font_size: int, bpp: int) -> list:
|
|
"""@return a list of (file name, ttf file, size, codepoints or None)"""
|
|
text_version = get_defines(os.path.join(ROOT, "Tactility", "Private", "Tactility", "lvgl", "FontVersions.h"))
|
|
icon_version = get_defines(os.path.join(ROOT, "Modules", "lvgl-module", "include", "lvgl", "icons", "names.h"))["LVGL_ICON_FONT_VERSION"]
|
|
icon_names = get_icon_names()
|
|
|
|
def file_name(prefix: str, name: str, size: int, version: int) -> str:
|
|
return f"{prefix}{name}_{size}_{bpp}bpp_{version:08x}.bin"
|
|
|
|
jobs = []
|
|
sans = os.path.join(FONTS, "AdwaitaSans.ttf")
|
|
text_fonts = [("small", scale(font_size, 0.75)), ("default", font_size), ("large", scale(font_size, 1.25))]
|
|
for name, size in text_fonts:
|
|
jobs.append((file_name("text_", name, size, text_version["TT_TEXT_FONT_VERSION"]), sans, size, None))
|
|
jobs.append((file_name("mono_", "mono", font_size, text_version["TT_MONO_FONT_VERSION"]), os.path.join(FONTS, "AdwaitaMono.ttf"), font_size, None))
|
|
|
|
shared_size = scale_double(font_size, 1.25)
|
|
icon_fonts = [
|
|
("statusbar", scale(font_size, 1.15), icon_names["statusbar"]),
|
|
("launcher", scale(font_size, 2.6), icon_names["launcher"]),
|
|
("shared_default", shared_size, icon_names["shared"]),
|
|
("shared_large", shared_size * 2, icon_names["shared"]),
|
|
]
|
|
for name, size, names in icon_fonts:
|
|
jobs.append((file_name("material_symbols_", name, size, icon_version), os.path.join(FONTS, "MaterialSymbolsRounded.ttf"), size, get_icon_codepoints(names)))
|
|
return jobs
|
|
|
|
|
|
def build_tool(work_folder: str):
|
|
"""@return the path of the tool, or None when there's no host compiler"""
|
|
compiler = next((found for found in (shutil.which(name) for name in ("c++", "g++", "clang++")) if found), None)
|
|
if compiler is None:
|
|
return None
|
|
tool = os.path.join(work_folder, "binfont_gen")
|
|
command = [
|
|
compiler, "-O2", "-std=c++20",
|
|
"-I" + os.path.join(ROOT, "Modules", "binfont-module", "include"),
|
|
"-I" + os.path.join(ROOT, "Modules", "binfont-module", "private"),
|
|
"-I" + os.path.join(ROOT, "TactilityKernel", "include"),
|
|
os.path.join(ROOT, "Buildscripts", "BinfontTool", "binfont_gen.cpp"),
|
|
os.path.join(ROOT, "Modules", "binfont-module", "source", "generator.cpp"),
|
|
os.path.join(ROOT, "TactilityKernel", "source", "memory_posix.cpp"),
|
|
"-o", tool,
|
|
]
|
|
if subprocess.run(command).returncode != 0:
|
|
return None
|
|
return tool
|
|
|
|
|
|
def write_if_changed(path: str, content: str):
|
|
if os.path.exists(path) and read_text(path) == content:
|
|
return
|
|
with open(path, "w", encoding="utf-8", newline="") as file:
|
|
file.write(content)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 4:
|
|
print(f"Usage: {sys.argv[0]} <device-folder> <simulator: 0 or 1> <output-folder>")
|
|
sys.exit(1)
|
|
device_folder, simulator, output_folder = sys.argv[1], sys.argv[2] == "1", sys.argv[3]
|
|
os.makedirs(output_folder, exist_ok=True)
|
|
|
|
font_size, bpp = get_device_settings(device_folder, simulator)
|
|
jobs = get_jobs(font_size, bpp)
|
|
|
|
fonts = []
|
|
with tempfile.TemporaryDirectory() as work_folder:
|
|
tool = build_tool(work_folder)
|
|
if tool is None:
|
|
print("WARNING: no host C++ compiler found, fonts are not embedded in the firmware")
|
|
else:
|
|
lines = []
|
|
for index, (name, ttf, size, codepoints) in enumerate(jobs):
|
|
codepoints_path = "-"
|
|
if codepoints is not None:
|
|
codepoints_path = os.path.join(work_folder, f"{index}.codepoints")
|
|
with open(codepoints_path, "w") as file:
|
|
file.write("".join(f"{codepoint}\n" for codepoint in codepoints))
|
|
lines.append(f"{index}\t{ttf}\t{size}\t{bpp}\t{codepoints_path}\n")
|
|
jobs_path = os.path.join(work_folder, "jobs.txt")
|
|
with open(jobs_path, "w") as file:
|
|
file.writelines(lines)
|
|
if subprocess.run([tool, jobs_path, work_folder]).returncode != 0:
|
|
sys.exit(1)
|
|
for index, (name, _, _, _) in enumerate(jobs):
|
|
with open(os.path.join(work_folder, f"{index}.bin"), "rb") as file:
|
|
fonts.append((name, file.read()))
|
|
|
|
header = [
|
|
"// Generated by Buildscripts/generate-fonts.py. Do not edit.",
|
|
"#pragma once",
|
|
"",
|
|
"#include <stddef.h>",
|
|
"#include <stdint.h>",
|
|
"",
|
|
"#ifdef __cplusplus",
|
|
'extern "C" {',
|
|
"#endif",
|
|
"",
|
|
"/** A binfont file that is part of the firmware, found by the file name that the font cache uses */",
|
|
"struct GeneratedFont {",
|
|
" const char* file_name;",
|
|
" const uint8_t* data;",
|
|
" size_t size;",
|
|
"};",
|
|
"",
|
|
"extern const struct GeneratedFont generated_fonts[];",
|
|
"extern const size_t generated_fonts_count;",
|
|
"",
|
|
"#ifdef __cplusplus",
|
|
"}",
|
|
"#endif",
|
|
"",
|
|
]
|
|
source = ['// Generated by Buildscripts/generate-fonts.py. Do not edit.', '#include "generated_fonts.h"', ""]
|
|
for index, (name, data) in enumerate(fonts):
|
|
source.append(f"static const uint8_t font_{index}[] = {{")
|
|
for offset in range(0, len(data), 24):
|
|
source.append(" " + ", ".join(f"0x{byte:02x}" for byte in data[offset:offset + 24]) + ",")
|
|
source.append("};")
|
|
source.append("")
|
|
source.append("const struct GeneratedFont generated_fonts[] = {")
|
|
for index, (name, data) in enumerate(fonts):
|
|
source.append(f' {{ "{name}", font_{index}, {len(data)} }},')
|
|
if not fonts:
|
|
source.append(" { 0, 0, 0 }")
|
|
source.append("};")
|
|
source.append(f"const size_t generated_fonts_count = {len(fonts)};")
|
|
source.append("")
|
|
|
|
write_if_changed(os.path.join(output_folder, "generated_fonts.h"), "\n".join(header))
|
|
write_if_changed(os.path.join(output_folder, "generated_fonts.c"), "\n".join(source))
|
|
print(f"Generated {len(fonts)} fonts ({sum(len(data) for _, data in fonts)} bytes)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|