Reduce internal RAM use: embedded assets, fonts in flash, PSRAM stacks, file_ops (#701)

- 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
This commit is contained in:
Ken Van Hoeylandt 2026-10-18 17:30:43 +02:00 committed by GitHub
parent 84fd232e29
commit 0ebd471cc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
129 changed files with 2031 additions and 1623 deletions

View File

@ -4,5 +4,7 @@
Code in `Platforms/platform-esp32/` is already ESP-only and does not need guards around ESP-IDF includes. Code in `Platforms/platform-esp32/` is already ESP-only and does not need guards around ESP-IDF includes.
- The `Drivers/` directory contains hardware drivers (display controllers, touch controllers, PMICs, etc.) — each is its own CMake component. - The `Drivers/` directory contains hardware drivers (display controllers, touch controllers, PMICs, etc.) — each is its own CMake component.
- `Modules/` contains cross-cutting modules. e.g.`lvgl-module` (LVGL task management). - `Modules/` contains cross-cutting modules. e.g.`lvgl-module` (LVGL task management).
- `Data/system/` and `Data/data/` are flashed as FAT filesystem images on ESP32 devices that don't have an SD card. - `Data/data/` is flashed as a FAT filesystem image on ESP32 devices that don't have an SD card.
- `Data/system/` is embedded in the Tactility project as generated C byte arrays (`generated_assets.h`, see `Buildscripts/generate-assets.py`).
- The binfont files of the default font size are generated at build time into `generated_fonts.h` (see `Buildscripts/generate-fonts.py`).
- Translations are in `Translations/` as CSV files, generated via `generate.py`. - Translations are in `Translations/` as CSV files, generated via `generate.py`.

View File

@ -0,0 +1,104 @@
// SPDX-License-Identifier: Apache-2.0
// Host tool that rasterizes TrueType fonts into binfont files with the same generator as the firmware.
//
// Usage: binfont_gen <jobs-file> <output-folder>
// Each line of the jobs file has tab separated fields: name, TTF path, size, bpp and a codepoints file.
// The codepoints file has one decimal codepoint per line, "-" means all codepoints of the TTF.
// The result of a job is written to <output-folder>/<name>.bin.
#include <binfont/generator.h>
#include <tactility/memory.h>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
// Normally defined by the kernel's memory.cpp, which depends on more than this tool needs
extern "C" const struct MemoryPolicy MEMORY_POLICY_DEFAULT = { 0, 0, 0 };
namespace {
bool readCodepoints(const std::string& path, std::vector<uint32_t>& codepoints) {
std::ifstream file(path);
if (!file) {
return false;
}
std::string line;
while (std::getline(file, line)) {
if (!line.empty()) {
codepoints.push_back(static_cast<uint32_t>(std::stoul(line)));
}
}
return true;
}
bool runJob(const std::string& line, const std::string& outputFolder) {
std::istringstream fields(line);
std::string name, ttfPath, size, bpp, codepointsPath;
if (!std::getline(fields, name, '\t') || !std::getline(fields, ttfPath, '\t') || !std::getline(fields, size, '\t') ||
!std::getline(fields, bpp, '\t') || !std::getline(fields, codepointsPath, '\t')) {
fprintf(stderr, "Invalid job: %s\n", line.c_str());
return false;
}
std::vector<uint32_t> codepoints;
if (codepointsPath != "-" && !readCodepoints(codepointsPath, codepoints)) {
fprintf(stderr, "Failed to read %s\n", codepointsPath.c_str());
return false;
}
const BinFontGeneratorConfig config = {
.ttf_path = ttfPath.c_str(),
.ttf_data = nullptr,
.ttf_size = 0,
.size = static_cast<uint16_t>(std::stoul(size)),
.bpp = static_cast<uint8_t>(std::stoul(bpp)),
.codepoints = codepoints.empty() ? nullptr : codepoints.data(),
.codepoint_count = codepoints.size(),
};
uint8_t* data = nullptr;
size_t dataSize = 0;
const error_t error = binfont_generate(&config, &data, &dataSize);
if (error != ERROR_NONE) {
fprintf(stderr, "Failed to generate %s (error %d)\n", name.c_str(), static_cast<int>(error));
return false;
}
const std::string outputPath = outputFolder + "/" + name + ".bin";
FILE* output = fopen(outputPath.c_str(), "wb");
const bool written = output != nullptr && fwrite(data, 1, dataSize, output) == dataSize;
if (output != nullptr) {
fclose(output);
}
memory_free(data);
if (!written) {
fprintf(stderr, "Failed to write %s\n", outputPath.c_str());
}
return written;
}
} // namespace
int main(int argc, char** argv) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <jobs-file> <output-folder>\n", argv[0]);
return 1;
}
std::ifstream jobs(argv[1]);
if (!jobs) {
fprintf(stderr, "Failed to read %s\n", argv[1]);
return 1;
}
std::string line;
while (std::getline(jobs, line)) {
if (!line.empty() && !runJob(line, argv[2])) {
return 1;
}
}
return 0;
}

View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""
Embeds every file of an asset folder as a C byte array.
Usage: generate-assets.py <asset-folder> <output-folder>
Writes generated_assets.h and generated_assets.c. A file "fonts/AdwaitaSans.ttf" becomes the symbols
asset_fonts_AdwaitaSans_ttf (the bytes) and asset_fonts_AdwaitaSans_ttf_size (the byte count).
The size is also available as the constant asset_fonts_AdwaitaSans_ttf_SIZE for use in constant initializers.
Text files get an extra terminating 0 byte that is not part of the size, so they can be used as C strings.
Output files are only rewritten when their content changes, to avoid needless recompiles.
"""
import os
import re
import sys
TEXT_EXTENSIONS = {".pem", ".csv", ".html", ".json", ".codepoints", ".i18n", ".txt"}
def symbol_name(relative_path: str) -> str:
return "asset_" + re.sub(r"[^A-Za-z0-9]", "_", relative_path)
def write_if_changed(path: str, content: str):
if os.path.exists(path):
with open(path, "r", encoding="utf-8", newline="") as file:
if file.read() == content:
return
with open(path, "w", encoding="utf-8", newline="") as file:
file.write(content)
def main():
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <asset-folder> <output-folder>")
sys.exit(1)
asset_folder = os.path.abspath(sys.argv[1])
output_folder = os.path.abspath(sys.argv[2])
os.makedirs(output_folder, exist_ok=True)
relative_paths = []
for directory, _, file_names in os.walk(asset_folder):
for file_name in file_names:
relative_paths.append(os.path.relpath(os.path.join(directory, file_name), asset_folder).replace(os.sep, "/"))
relative_paths.sort()
header = [
"// Generated by Buildscripts/generate-assets.py. Do not edit.",
"#pragma once",
"",
"#include <stddef.h>",
"#include <stdint.h>",
"",
"#ifdef __cplusplus",
'extern "C" {',
"#endif",
"",
]
source = [
"// Generated by Buildscripts/generate-assets.py. Do not edit.",
'#include "generated_assets.h"',
"",
]
for relative_path in relative_paths:
with open(os.path.join(asset_folder, relative_path), "rb") as file:
data = file.read()
symbol = symbol_name(relative_path)
size = len(data)
if os.path.splitext(relative_path)[1].lower() in TEXT_EXTENSIONS:
data += b"\0"
header.append(f"// {relative_path}")
header.append(f"extern const uint8_t {symbol}[];")
header.append(f"extern const size_t {symbol}_size;")
header.append(f"#define {symbol}_SIZE {size}")
source.append(f"// {relative_path}")
source.append(f"const uint8_t {symbol}[] = {{")
for offset in range(0, len(data), 24):
source.append(" " + ", ".join(f"0x{byte:02x}" for byte in data[offset:offset + 24]) + ",")
if len(data) == 0:
source.append(" 0")
source.append("};")
source.append(f"const size_t {symbol}_size = {size};")
source.append("")
header += ["", "#ifdef __cplusplus", "}", "#endif", ""]
write_if_changed(os.path.join(output_folder, "generated_assets.h"), "\n".join(header))
write_if_changed(os.path.join(output_folder, "generated_assets.c"), "\n".join(source))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,230 @@
#!/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()

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Generates the spinner and cursor images in Data/system/ui/ from Data/system_sources/:
- spinner_<size>.png from TactilitySpinner.svg
- cursor_<size>.png from cursor.svg
The sizes are in pixels. They must match the ones in Modules/lvgl-module/source/ui_assets.c.
Requires the cairosvg package: pip install cairosvg
"""
import os
import re
import cairosvg
SPINNER_SIZES = [12, 16, 20, 24, 32, 36, 40]
CURSOR_SIZES = [12, 16, 20, 24, 32, 40]
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SOURCES = os.path.join(ROOT, "Data", "system_sources")
OUTPUT = os.path.join(ROOT, "Data", "system", "ui")
def load_svg(svg_name: str) -> bytes:
"""
Inkscape leaves x, y, width and height attributes on paths that came from rectangles.
cairosvg applies them to the path, which displaces and distorts it, so they're removed.
"""
with open(os.path.join(SOURCES, svg_name), "r", encoding="utf-8") as file:
svg = file.read()
svg = re.sub(
r"<path\b.*?>",
lambda tag: re.sub(r'\s+(?:x|y|width|height)="[^"]*"', "", tag[0]),
svg,
flags=re.S,
)
return svg.encode("utf-8")
def render(svg_name: str, output_name: str, size: int):
cairosvg.svg2png(
bytestring=load_svg(svg_name),
write_to=os.path.join(OUTPUT, output_name),
output_width=size,
output_height=size,
)
print(output_name)
def main():
os.makedirs(OUTPUT, exist_ok=True)
for size in SPINNER_SIZES:
render("TactilitySpinner.svg", f"spinner_{size}.png", size)
for size in CURSOR_SIZES:
render("cursor.svg", f"cursor_{size}.png", size)
if __name__ == "__main__":
main()

View File

@ -13,5 +13,4 @@ mkdir -p "$target_path"
cp version.txt "$target_path" cp version.txt "$target_path"
cp "$build_path/Tactility/Tactility" "$target_path/" cp "$build_path/Tactility/Tactility" "$target_path/"
cp -r Data/data "$target_path/" mkdir -p "$target_path/data"
cp -r Data/system "$target_path/"

View File

@ -1,27 +0,0 @@
# Web Server Settings
# WiFi and HTTP server configuration
# WiFi Enable (0=disabled, 1=enabled)
wifiEnabled=0
# WiFi Mode (0=Station/Client, 1=Access Point)
wifiMode=0
# Access Point Mode Settings (create own WiFi network)
# apSsid will be auto-generated as Tactility-XXXX if empty
# apPassword will be auto-generated if empty or insecure (WPA2 requires 8-63 chars)
# apOpenNetwork: if 1, create open network without password (ignores apPassword)
apSsid=
apPassword=
apOpenNetwork=0
apChannel=1
# Web Server Settings
webServerEnabled=0
webServerPort=80
# HTTP Basic Authentication (optional)
# When auth is enabled with empty/insecure credentials, strong random credentials are auto-generated
webServerAuthEnabled=0
webServerUsername=
webServerPassword=

View File

@ -1,7 +0,0 @@
Dutch (Netherlands)
Dutch (Belgium)
English (United States)
English (United Kingdom)
French (France)
Region
Language

View File

@ -1,7 +0,0 @@
Dutch (Netherlands)
Dutch (Belgium)
English (United States)
English (United Kingdom)
French (France)
Region
Language

View File

@ -1,7 +0,0 @@
Néerlandais (Pays-Bas)
Néerlandais (Belgique)
Anglais (États-Unis)
Anglais (Royaume-Uni)
Français (France)
Région
Langue

View File

@ -1,7 +0,0 @@
Nederlands (Nederland)
Nederlands (België)
Engels (Verenigde Staten)
Engels (Verenigd Koninkrijk)
Frans (Frankrijk)
Regio
Taal

View File

@ -1,7 +0,0 @@
Nederlands (Nederland)
Nederlands (België)
Engels (Verenigde Staten)
Engels (Verenigd Koninkrijk)
Frans (Frankrijk)
Regio
Taal

View File

@ -811,26 +811,6 @@ async function loadWifiStatus() {
} }
} }
async function syncAssets(btn) {
const status = document.getElementById('actionStatus');
btn.disabled = true;
btn.textContent = 'Syncing...';
try {
const r = await fetch('/admin/sync', { method: 'POST' });
if (status) {
status.textContent = r.ok ? 'Assets synchronized!' : 'Sync failed';
status.style.color = r.ok ? '#2ecc71' : '#e74c3c';
}
} catch (e) {
if (status) {
status.textContent = 'Error: ' + e.message;
status.style.color = '#e74c3c';
}
}
btn.disabled = false;
btn.textContent = 'Sync Assets';
}
async function captureScreenshot(btn) { async function captureScreenshot(btn) {
const status = document.getElementById('actionStatus'); const status = document.getElementById('actionStatus');
const container = document.getElementById('screenshotContainer'); const container = document.getElementById('screenshotContainer');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

View File

@ -1,7 +0,0 @@
OK
Yes
No
Cancel
Retry
Close
Open

View File

@ -1,7 +0,0 @@
OK
Yes
No
Cancel
Retry
Close
Open

View File

@ -1,7 +0,0 @@
OK
Oui
Non
Annuler
Réessayer
Fermer
Ouvrir

View File

@ -1,7 +0,0 @@
OK
Ja
Nee
Annuleren
Opnieuw
Sluiten
Openen

View File

@ -1,7 +0,0 @@
OK
Ja
Nee
Annuleren
Opnieuw
Sluiten
Openen

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

View File

@ -5,6 +5,7 @@
#include <csignal> #include <csignal>
#include <pthread.h> #include <pthread.h>
#include <sys/stat.h>
#include <thread> #include <thread>
namespace simulator { namespace simulator {
@ -19,6 +20,9 @@ void app_main(); // ESP-IDF's main function, implemented in the application
} }
int main() { int main() {
// The folder for the "data" file system is not in the repository
mkdir("data", 0755);
// The FreeRTOS POSIX port arms a process-wide SIGALRM timer for its tick and expects every one // The FreeRTOS POSIX port arms a process-wide SIGALRM timer for its tick and expects every one
// of its task pthreads to have all signals but SIGINT blocked. // of its task pthreads to have all signals but SIGINT blocked.
// (see prvSetupSignalsAndSchedulerPolicy() in FreeRTOS-Kernel's Posix port.c) // (see prvSetupSignalsAndSchedulerPolicy() in FreeRTOS-Kernel's Posix port.c)

View File

@ -7,11 +7,9 @@
## Higher Priority ## Higher Priority
- Experiment with LVGL stack in SPIRAM - LVGL shouldn't do file IO anyway. - Move EspNowRadio etc to Platforms/platform-esp32/
- Experiment with default malloc() to SPIRAM
- GPIO app: fix for monochrome display by using filled vs outline box in secondary colour - GPIO app: fix for monochrome display by using filled vs outline box in secondary colour
- BlueTooth as radio backend for MeshCore/Meshtastic? - BlueTooth as radio backend for MeshCore/Meshtastic?
- ESP-NOW as radio backend. Use it for MeshCore/Meshtastic?
- Find out why the first build of an app with a clean build cache folder always fails - Find out why the first build of an app with a clean build cache folder always fails
- Fix BT: an esp32 device connecting to a peripheral isn't working - Fix BT: an esp32 device connecting to a peripheral isn't working
- "update all" functionality for app hub module, including commandline app. add button to app hub toolbar if there are 1 or more apps to update (does "update all"). - "update all" functionality for app hub module, including commandline app. add button to app hub toolbar if there are 1 or more apps to update (does "update all").
@ -22,7 +20,6 @@
- Cursor is hard-coded, needs size based on DPI - Cursor is hard-coded, needs size based on DPI
- Move Bluetooth code from kernel to Modules/bluetooth-module, and have includes in path bluetooth/ then make a bluetooth_command.cpp like wifi_command.cpp - Move Bluetooth code from kernel to Modules/bluetooth-module, and have includes in path bluetooth/ then make a bluetooth_command.cpp like wifi_command.cpp
- Inspect code for memory improvements: replace `std::*` with `tt::std::*` - Inspect code for memory improvements: replace `std::*` with `tt::std::*`
- esp_lvgl_port settings has a large stack size (~9kB) to fix stackoverflow when LVGL events (e.g. button click) do actions like file operations do actions like file operations. Can we reduce the callstack?
- Improve posix support: use user home path and system /tmp folder. - Improve posix support: use user home path and system /tmp folder.
- NimBLE looses pairing key after reboot - NimBLE looses pairing key after reboot
- Pick different font to improve monochrome screen readability (or change the weight). - Pick different font to improve monochrome screen readability (or change the weight).
@ -37,6 +34,8 @@
## Medium Priority ## Medium Priority
- Tactility/Source/app/imageviewer/ImageViewer.cpp: Limit memory used to open an image.
loadFile() now resizes fileData to the entire file size before displaying the image. start accepts a file path without a size limit, so opening a large image can exhaust memory on an embedded device before LVGL decodes it. Reject files above a supported limit or retain a bounded file-backed decoding path.
- A device profile selection screen for Simulator: select device profiles such as T-Lora Pager, T-Deck Plus, CL-32, Cardputer and T-Embed which changes the resolution and theming defaults after restarting. - A device profile selection screen for Simulator: select device profiles such as T-Lora Pager, T-Deck Plus, CL-32, Cardputer and T-Embed which changes the resolution and theming defaults after restarting.
- A fullscreen tiny keyboard that works with single stroke gestures (similar to Palm OS Graffiti) - A fullscreen tiny keyboard that works with single stroke gestures (similar to Palm OS Graffiti)
- CL-32 display driver performance - CL-32 display driver performance

View File

@ -9,6 +9,12 @@ extern "C" {
extern struct Module app_hub_module; extern struct Module app_hub_module;
/**
* @brief Sets the root certificate that verifies the app catalog server.
* @param[in] certificate_pem PEM text that must stay valid forever
*/
void app_hub_configure_certificate(const char* certificate_pem);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -56,8 +56,8 @@ const char* app_hub_catalog_get_platform();
/** @return true when this device can install and run the entry's app (target platform, device and RAM requirements) */ /** @return true when this device can install and run the entry's app (target platform, device and RAM requirements) */
bool app_hub_catalog_is_compatible(const AppHubCatalogEntry& entry); bool app_hub_catalog_is_compatible(const AppHubCatalogEntry& entry);
/** @return the path of the certificate that verifies the catalog server, in the "system" file system, or an empty string when it isn't available */ /** @return the PEM text of the certificate that verifies the catalog server, or an empty string when it isn't configured */
std::string app_hub_catalog_get_certificate_path(); const char* app_hub_catalog_get_certificate();
std::string app_hub_catalog_get_url(); std::string app_hub_catalog_get_url();

View File

@ -1,9 +1,9 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#include <app_hub/app_hub_catalog.h> #include <app_hub/app_hub_catalog.h>
#include <app_hub/module.h>
#include <app/package_manifest.h> #include <app/package_manifest.h>
#include <tactility/filesystem/file_system.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/paths.h> #include <tactility/paths.h>
@ -32,14 +32,14 @@ static std::string get_version_without_postfix() {
return (index == std::string::npos) ? version : version.substr(0, index); return (index == std::string::npos) ? version : version.substr(0, index);
} }
std::string app_hub_catalog_get_certificate_path() { static const char* certificate_pem = "";
// The system file system is mounted at "/system" on ESP32, but it's a folder elsewhere on the simulator
FileSystem* system = file_system_find_by_name("system"); void app_hub_configure_certificate(const char* pem) {
char path[FILE_MAX_PATH_STRING_LENGTH]; certificate_pem = pem;
if (system == nullptr || file_system_get_path(system, path, sizeof(path)) != ERROR_NONE) { }
return "";
} const char* app_hub_catalog_get_certificate() {
return std::string(path) + "/certificates/GTS_Root_R4.pem"; return certificate_pem;
} }
std::string app_hub_catalog_get_url() { std::string app_hub_catalog_get_url() {

View File

@ -19,17 +19,17 @@
#include <tactility/paths.h> #include <tactility/paths.h>
#include <tactility/time.h> #include <tactility/time.h>
#include <TactilityCpp/Allocator.h> #include <TactilityCpp/std/deque.h>
#include <TactilityCpp/std/string.h>
#include <TactilityCpp/std/vector.h>
#include <algorithm> #include <algorithm>
#include <new> #include <new>
#include <cctype> #include <cctype>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <deque>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector>
namespace { namespace {
@ -53,7 +53,7 @@ struct InstalledPackage {
AppId first_app_id; AppId first_app_id;
}; };
using AppList = std::vector<AppHubApp, tt::OptExternalAllocator<AppHubApp>>; using AppList = tt::std::vector<AppHubApp>;
struct GlobalMutex { struct GlobalMutex {
Mutex handle {}; Mutex handle {};
@ -69,7 +69,7 @@ void unlock() { mutex_unlock(&globalMutex.handle); }
// region State, guarded by the global lock // region State, guarded by the global lock
struct State { struct State {
std::deque<Operation, tt::OptExternalAllocator<Operation>> queue; tt::std::deque<Operation> queue;
Operation runningOperation {}; Operation runningOperation {};
bool operationRunning = false; bool operationRunning = false;
uint32_t nextOperationId = 1; uint32_t nextOperationId = 1;
@ -191,10 +191,10 @@ void copyString(char* target, size_t targetSize, const char* source) {
} }
/** Copies the installed packages, as app_manager_for_each_package() holds the app manager's lock */ /** Copies the installed packages, as app_manager_for_each_package() holds the app manager's lock */
std::vector<InstalledPackage> getInstalledPackages() { tt::std::vector<InstalledPackage> getInstalledPackages() {
std::vector<InstalledPackage> packages; tt::std::vector<InstalledPackage> packages;
app_manager_for_each_package([](const AppPackage* package, void* context) { app_manager_for_each_package([](const AppPackage* package, void* context) {
auto& packages = *static_cast<std::vector<InstalledPackage>*>(context); auto& packages = *static_cast<tt::std::vector<InstalledPackage>*>(context);
InstalledPackage installed {}; InstalledPackage installed {};
installed.package = package->package; installed.package = package->package;
if (package->app_id_count > 0) { if (package->app_id_count > 0) {
@ -232,7 +232,7 @@ const Operation* findPendingOperation(const char* id) {
} }
/** Requires the global lock */ /** Requires the global lock */
AppList collectApps(const std::vector<InstalledPackage>& installedPackages) { AppList collectApps(const tt::std::vector<InstalledPackage>& installedPackages) {
AppList apps; AppList apps;
char catalogPath[FILE_MAX_PATH_STRING_LENGTH]; char catalogPath[FILE_MAX_PATH_STRING_LENGTH];
if (getCatalogFile(catalogPath, sizeof(catalogPath))) { if (getCatalogFile(catalogPath, sizeof(catalogPath))) {
@ -308,7 +308,7 @@ error_t download(const char* url, const char* targetPath) {
if (http_download_subscribe(&sub, &state->workerEvents) != ERROR_NONE) { if (http_download_subscribe(&sub, &state->workerEvents) != ERROR_NONE) {
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
if (http_download_start(url, app_hub_catalog_get_certificate_path().c_str(), targetPath, &sub) != ERROR_NONE) { if (http_download_start(url, app_hub_catalog_get_certificate(), targetPath, &sub) != ERROR_NONE) {
http_download_unsubscribe(&sub); http_download_unsubscribe(&sub);
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
@ -395,8 +395,8 @@ error_t executeInstall(const char* id) {
return error; return error;
} }
const auto separator = file.find_last_of('/'); const auto separator = file.find_last_of('/');
const std::string fileName = (separator == std::string::npos) ? file : file.substr(separator + 1); const tt::std::string fileName = (separator == std::string::npos) ? tt::std::string(file.c_str()) : tt::std::string(file.c_str() + separator + 1);
const std::string tempPath = std::string(tempDirectory) + "/" + fileName; const tt::std::string tempPath = tt::std::string(tempDirectory) + "/" + fileName;
error = download(app_hub_catalog_get_download_url(file).c_str(), tempPath.c_str()); error = download(app_hub_catalog_get_download_url(file).c_str(), tempPath.c_str());
if (error == ERROR_NONE) { if (error == ERROR_NONE) {
@ -413,7 +413,7 @@ error_t executeInstall(const char* id) {
error_t executeUpdateAll(const Operation& operation) { error_t executeUpdateAll(const Operation& operation) {
const auto installed = getInstalledPackages(); const auto installed = getInstalledPackages();
std::vector<std::string> ids; tt::std::vector<tt::std::string> ids;
lock(); lock();
for (const auto& app : collectApps(installed)) { for (const auto& app : collectApps(installed)) {
if (app.update_available) { if (app.update_available) {

View File

@ -4,6 +4,8 @@
#include <app/private/scheduler.h> #include <app/private/scheduler.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <TactilityCpp/Memory.h>
#include <tactility/delay.h> #include <tactility/delay.h>
#include <tactility/log.h> #include <tactility/log.h>
@ -12,7 +14,6 @@
#include <atomic> #include <atomic>
#include <cstdlib> #include <cstdlib>
#include <mutex> #include <mutex>
#include <new>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
@ -85,7 +86,7 @@ size_t task_count(AppInstanceId app_instance_id) {
} // namespace } // namespace
error_t app_resources_register(AppInstanceId app_instance_id) { error_t app_resources_register(AppInstanceId app_instance_id) {
auto* tracker = new (std::nothrow) Tracker(); auto* tracker = tt::newObject<Tracker>();
if (tracker == nullptr) { if (tracker == nullptr) {
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }
@ -165,7 +166,7 @@ void app_resources_release(AppInstanceId app_instance_id) {
static_cast<unsigned>(tracker->allocations.size())); static_cast<unsigned>(tracker->allocations.size()));
} }
delete tracker; tt::deleteObject(tracker);
} }
extern "C" { extern "C" {

View File

@ -11,8 +11,12 @@ extern "C" {
#endif #endif
typedef struct BinFontGeneratorConfig { typedef struct BinFontGeneratorConfig {
/** Path to a TrueType (.ttf) file */ /** Path to a TrueType (.ttf) file. Ignored when ttf_data is set */
const char* ttf_path; const char* ttf_path;
/** A TrueType (.ttf) file in memory that must stay valid during the call, or NULL to read ttf_path */
const uint8_t* ttf_data;
/** Size of ttf_data in bytes */
size_t ttf_size;
/** Font size in pixels (em height) */ /** Font size in pixels (em height) */
uint16_t size; uint16_t size;
/** Bits per pixel: 1, 2, 3 or 4 */ /** Bits per pixel: 1, 2, 3 or 4 */

View File

@ -538,20 +538,26 @@ error_t generate(const BinFontGeneratorConfig* config, const uint32_t* codepoint
extern "C" { extern "C" {
error_t binfont_generate(const BinFontGeneratorConfig* config, uint8_t** out_data, size_t* out_size) { error_t binfont_generate(const BinFontGeneratorConfig* config, uint8_t** out_data, size_t* out_size) {
if (config->ttf_path == nullptr || config->size == 0 || config->bpp < 1 || config->bpp > 4 || (config->codepoints == nullptr) != (config->codepoint_count == 0)) { const bool has_ttf_data = config->ttf_data != nullptr && config->ttf_size > 0;
if ((config->ttf_path == nullptr && !has_ttf_data) || config->size == 0 || config->bpp < 1 || config->bpp > 4 || (config->codepoints == nullptr) != (config->codepoint_count == 0)) {
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
size_t ttf_size = 0; size_t ttf_size = config->ttf_size;
uint8_t* ttf = read_file(config->ttf_path, ttf_size); const uint8_t* ttf = config->ttf_data;
if (ttf == nullptr) { uint8_t* owned_ttf = nullptr;
if (!has_ttf_data) {
owned_ttf = read_file(config->ttf_path, ttf_size);
if (owned_ttf == nullptr) {
return ERROR_NOT_FOUND; return ERROR_NOT_FOUND;
} }
ttf = owned_ttf;
}
stbtt_fontinfo info; stbtt_fontinfo info;
const int font_offset = stbtt_GetFontOffsetForIndex(ttf, 0); const int font_offset = stbtt_GetFontOffsetForIndex(ttf, 0);
if (font_offset < 0 || stbtt_InitFont(&info, ttf, font_offset) == 0) { if (font_offset < 0 || stbtt_InitFont(&info, ttf, font_offset) == 0) {
memory_free(ttf); memory_free(owned_ttf);
return ERROR_NOT_SUPPORTED; return ERROR_NOT_SUPPORTED;
} }
AllocationState allocation = { .failed = false }; AllocationState allocation = { .failed = false };
@ -562,7 +568,7 @@ error_t binfont_generate(const BinFontGeneratorConfig* config, uint8_t** out_dat
CodepointList all_codepoints; CodepointList all_codepoints;
if (codepoints == nullptr) { if (codepoints == nullptr) {
if (!collect_codepoints(info, ttf_size, all_codepoints)) { if (!collect_codepoints(info, ttf_size, all_codepoints)) {
memory_free(ttf); memory_free(owned_ttf);
return all_codepoints.hasFailed() ? ERROR_OUT_OF_MEMORY : ERROR_NOT_SUPPORTED; return all_codepoints.hasFailed() ? ERROR_OUT_OF_MEMORY : ERROR_NOT_SUPPORTED;
} }
codepoints = all_codepoints.getData(); codepoints = all_codepoints.getData();
@ -571,14 +577,14 @@ error_t binfont_generate(const BinFontGeneratorConfig* config, uint8_t** out_dat
auto* glyphs = static_cast<GlyphInfo*>(memory_alloc(codepoint_count * sizeof(GlyphInfo))); auto* glyphs = static_cast<GlyphInfo*>(memory_alloc(codepoint_count * sizeof(GlyphInfo)));
if (glyphs == nullptr) { if (glyphs == nullptr) {
memory_free(ttf); memory_free(owned_ttf);
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }
ByteBuffer out; ByteBuffer out;
const error_t error = generate(config, codepoints, codepoint_count, info, glyphs, out); const error_t error = generate(config, codepoints, codepoint_count, info, glyphs, out);
memory_free(glyphs); memory_free(glyphs);
memory_free(ttf); memory_free(owned_ttf);
if (error != ERROR_NONE) { if (error != ERROR_NONE) {
return error; return error;
} }

View File

@ -106,10 +106,10 @@ error_t http_download_poll(struct HttpDownloadSubscription* sub, struct HttpDown
/** /**
* Starts a download on its own dedicated task: issues a GET request for @a url, verifying the * Starts a download on its own dedicated task: issues a GET request for @a url, verifying the
* server against the PEM certificate at @a cert_path, and writes the response body to * server against the PEM certificate @a cert_pem, and writes the response body to
* @a target_path. * @a target_path.
* @param[in] url the URL to download * @param[in] url the URL to download
* @param[in] cert_path path to a PEM certificate file used to verify the server * @param[in] cert_pem PEM certificate text used to verify the server, copied before this returns
* @param[in] target_path path to write the downloaded file to * @param[in] target_path path to write the downloaded file to
* @param[in] sub subscription to notify on completion, already registered via * @param[in] sub subscription to notify on completion, already registered via
* http_download_subscribe(); must stay alive (and stationary) at least until * http_download_subscribe(); must stay alive (and stationary) at least until
@ -120,7 +120,7 @@ error_t http_download_poll(struct HttpDownloadSubscription* sub, struct HttpDown
*/ */
error_t http_download_start( error_t http_download_start(
const char* url, const char* url,
const char* cert_path, const char* cert_pem,
const char* target_path, const char* target_path,
struct HttpDownloadSubscription* sub struct HttpDownloadSubscription* sub
); );

View File

@ -61,7 +61,7 @@ inline bool http_download_is_cancelled(HttpDownloadLink* link) {
/** Implemented in download_esp.cpp (ESP_PLATFORM) or download_mock.cpp (otherwise). */ /** Implemented in download_esp.cpp (ESP_PLATFORM) or download_mock.cpp (otherwise). */
HttpDownloadEvent http_download_run( HttpDownloadEvent http_download_run(
const std::string& url, const std::string& url,
const std::string& certPath, const std::string& certPem,
const std::string& targetPath, const std::string& targetPath,
HttpDownloadLink* link HttpDownloadLink* link
); );

View File

@ -16,7 +16,7 @@ constexpr size_t DOWNLOAD_TASK_STACK_DEPTH = 5120;
struct DownloadContext { struct DownloadContext {
std::string url; std::string url;
std::string certPath; std::string certPem;
std::string targetPath; std::string targetPath;
HttpDownloadSubscription* subscription; HttpDownloadSubscription* subscription;
HttpDownloadLink* link; HttpDownloadLink* link;
@ -43,7 +43,7 @@ void download_task_main(void* raw_context) {
auto* context = static_cast<DownloadContext*>(raw_context); auto* context = static_cast<DownloadContext*>(raw_context);
LOG_I(TAG, "Downloading %s to %s", context->url.c_str(), context->targetPath.c_str()); LOG_I(TAG, "Downloading %s to %s", context->url.c_str(), context->targetPath.c_str());
auto event = http_download_run(context->url, context->certPath, context->targetPath, context->link); auto event = http_download_run(context->url, context->certPem, context->targetPath, context->link);
if (event.type == HTTP_DOWNLOAD_EVENT_ERROR) { if (event.type == HTTP_DOWNLOAD_EVENT_ERROR) {
LOG_E(TAG, "Download of %s failed: %s", context->url.c_str(), event.error.message); LOG_E(TAG, "Download of %s failed: %s", context->url.c_str(), event.error.message);
} else if (event.type == HTTP_DOWNLOAD_EVENT_CANCELLED) { } else if (event.type == HTTP_DOWNLOAD_EVENT_CANCELLED) {
@ -133,7 +133,7 @@ error_t http_download_poll(HttpDownloadSubscription* sub, HttpDownloadEvent* out
error_t http_download_start( error_t http_download_start(
const char* url, const char* url,
const char* cert_path, const char* cert_pem,
const char* target_path, const char* target_path,
HttpDownloadSubscription* sub HttpDownloadSubscription* sub
) { ) {
@ -144,7 +144,7 @@ error_t http_download_start(
auto* context = new(std::nothrow) DownloadContext { auto* context = new(std::nothrow) DownloadContext {
.url = url, .url = url,
.certPath = cert_path, .certPem = cert_pem,
.targetPath = target_path, .targetPath = target_path,
.subscription = sub, .subscription = sub,
.link = link .link = link

View File

@ -50,44 +50,18 @@ public:
bool isComplete() const { return esp_http_client_is_complete_data_received(client); } bool isComplete() const { return esp_http_client_is_complete_data_received(client); }
}; };
// esp_http_client_config_t::cert_pem needs a NUL-terminated buffer.
bool read_certificate(const std::string& certPath, std::string& outCertificate) {
auto* file = fopen(certPath.c_str(), "rb");
if (file == nullptr) {
return false;
}
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
if (size <= 0) {
fclose(file);
return false;
}
outCertificate.resize(static_cast<size_t>(size));
size_t read_bytes = fread(outCertificate.data(), 1, static_cast<size_t>(size), file);
fclose(file);
return read_bytes == static_cast<size_t>(size);
} }
} HttpDownloadEvent http_download_run(const std::string& url, const std::string& certPem, const std::string& targetPath, HttpDownloadLink* link) {
HttpDownloadEvent http_download_run(const std::string& url, const std::string& certPath, const std::string& targetPath, HttpDownloadLink* link) {
if (http_download_is_cancelled(link)) { if (http_download_is_cancelled(link)) {
return http_download_make_cancelled_event(); return http_download_make_cancelled_event();
} }
std::string certificate;
if (!read_certificate(certPath, certificate)) {
return http_download_make_error_event("Failed to read certificate file");
}
esp_http_client_config_t config {}; esp_http_client_config_t config {};
config.url = url.c_str(); config.url = url.c_str();
config.auth_type = HTTP_AUTH_TYPE_NONE; config.auth_type = HTTP_AUTH_TYPE_NONE;
config.cert_pem = certificate.c_str(); config.cert_pem = certPem.c_str();
config.cert_len = certificate.size() + 1; config.cert_len = certPem.size() + 1;
config.tls_version = ESP_HTTP_CLIENT_TLS_VER_TLS_1_2; config.tls_version = ESP_HTTP_CLIENT_TLS_VER_TLS_1_2;
config.method = HTTP_METHOD_GET; config.method = HTTP_METHOD_GET;
config.timeout_ms = 5000; config.timeout_ms = 5000;

View File

@ -83,26 +83,9 @@ public:
mbedtls_x509_crt_free(&ca); mbedtls_x509_crt_free(&ca);
} }
bool loadCertificate(const std::string& certPath) { bool loadCertificate(const std::string& certPem) {
auto* file = fopen(certPath.c_str(), "rb");
if (file == nullptr) {
return false;
}
fseek(file, 0, SEEK_END);
const long size = ftell(file);
fseek(file, 0, SEEK_SET);
if (size <= 0) {
fclose(file);
return false;
}
// A PEM certificate is parsed as a NUL-terminated string // A PEM certificate is parsed as a NUL-terminated string
std::string certificate(static_cast<size_t>(size) + 1, '\0'); return mbedtls_x509_crt_parse(&ca, reinterpret_cast<const unsigned char*>(certPem.c_str()), certPem.size() + 1) == 0;
const size_t readBytes = fread(certificate.data(), 1, static_cast<size_t>(size), file);
fclose(file);
if (readBytes != static_cast<size_t>(size)) {
return false;
}
return mbedtls_x509_crt_parse(&ca, reinterpret_cast<const unsigned char*>(certificate.data()), certificate.size()) == 0;
} }
bool connectSocket(const HttpUrl& url) { bool connectSocket(const HttpUrl& url) {
@ -259,7 +242,7 @@ public:
} }
HttpDownloadEvent http_download_run(const std::string& url, const std::string& certPath, const std::string& targetPath, HttpDownloadLink* link) { HttpDownloadEvent http_download_run(const std::string& url, const std::string& certPem, const std::string& targetPath, HttpDownloadLink* link) {
if (http_download_is_cancelled(link)) { if (http_download_is_cancelled(link)) {
return http_download_make_cancelled_event(); return http_download_make_cancelled_event();
} }
@ -273,8 +256,8 @@ HttpDownloadEvent http_download_run(const std::string& url, const std::string& c
} }
TlsConnection connection; TlsConnection connection;
if (!connection.loadCertificate(certPath)) { if (!connection.loadCertificate(certPem)) {
return http_download_make_error_event("Failed to read certificate file"); return http_download_make_error_event("Failed to parse certificate");
} }
if (!connection.connectSocket(parsedUrl) || !connection.handshake(parsedUrl, link)) { if (!connection.connectSocket(parsedUrl) || !connection.handshake(parsedUrl, link)) {
return http_download_make_error_event("Failed to open connection"); return http_download_make_error_event("Failed to open connection");

View File

@ -4,7 +4,7 @@
TEST_CASE("http_download_start requires a subscribed subscription") { TEST_CASE("http_download_start requires a subscribed subscription") {
HttpDownloadSubscription sub {}; HttpDownloadSubscription sub {};
CHECK_EQ(http_download_start("https://example.com/file", "/nonexistent/cert.pem", "/target.bin", &sub), ERROR_INVALID_ARGUMENT); CHECK_EQ(http_download_start("https://example.com/file", "not a certificate", "/target.bin", &sub), ERROR_INVALID_ARGUMENT);
} }
TEST_CASE("http_download_poll times out before the download finishes") { TEST_CASE("http_download_poll times out before the download finishes") {
@ -21,13 +21,13 @@ TEST_CASE("http_download_poll times out before the download finishes") {
task_event_group_destruct(&event_group); task_event_group_destruct(&event_group);
} }
TEST_CASE("http_download_start fails the download when the certificate can't be read") { TEST_CASE("http_download_start fails the download when the certificate can't be parsed") {
TaskEventGroup event_group {}; TaskEventGroup event_group {};
task_event_group_construct(&event_group); task_event_group_construct(&event_group);
HttpDownloadSubscription sub {}; HttpDownloadSubscription sub {};
CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE); CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE);
CHECK_EQ(http_download_start("https://example.com/file", "/nonexistent/cert.pem", "/target.bin", &sub), ERROR_NONE); CHECK_EQ(http_download_start("https://example.com/file", "not a certificate", "/target.bin", &sub), ERROR_NONE);
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
@ -55,7 +55,7 @@ TEST_CASE("http_download_unsubscribe is safe to call right after start, without
HttpDownloadSubscription sub {}; HttpDownloadSubscription sub {};
CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE); CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE);
CHECK_EQ(http_download_start("https://example.com/file", "/nonexistent/cert.pem", "/target.bin", &sub), ERROR_NONE); CHECK_EQ(http_download_start("https://example.com/file", "not a certificate", "/target.bin", &sub), ERROR_NONE);
// No cancel, no poll for the terminal event - just unsubscribe and tear down immediately, // No cancel, no poll for the terminal event - just unsubscribe and tear down immediately,
// racing whatever the download task is doing. // racing whatever the download task is doing.
@ -77,7 +77,7 @@ TEST_CASE("http_download_cancel before start makes the download finish as cancel
HttpDownloadSubscription sub {}; HttpDownloadSubscription sub {};
CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE); CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE);
CHECK_EQ(http_download_cancel(&sub), ERROR_NONE); CHECK_EQ(http_download_cancel(&sub), ERROR_NONE);
CHECK_EQ(http_download_start("https://example.com/file", "/nonexistent/cert.pem", "/target.bin", &sub), ERROR_NONE); CHECK_EQ(http_download_start("https://example.com/file", "not a certificate", "/target.bin", &sub), ERROR_NONE);
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);

View File

@ -9,6 +9,12 @@ extern "C" {
extern struct Module location_module; extern struct Module location_module;
/**
* @brief Sets the root certificate that verifies the IP lookup server.
* @param[in] certificate_pem PEM text that must stay valid forever
*/
void location_configure_certificate(const char* certificate_pem);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -1,5 +1,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#include <location/ip_provider.h> #include <location/ip_provider.h>
#include <location/module.h>
#include <http/download.h> #include <http/download.h>
@ -26,11 +27,16 @@
#define TAG "location_ip" #define TAG "location_ip"
// The root certificate of the API's certificate chain
static const char* certificate_pem = "";
void location_configure_certificate(const char* pem) {
certificate_pem = pem;
}
namespace { namespace {
constexpr auto* API_URL = "https://json.geoiplookup.io/"; constexpr auto* API_URL = "https://json.geoiplookup.io/";
// The root certificate of the API's certificate chain
constexpr auto* API_CERTIFICATE = "certificates/GTS_Root_R4.pem";
constexpr uint64_t RETRY_INTERVAL_MS = 60 * 1000; constexpr uint64_t RETRY_INTERVAL_MS = 60 * 1000;
// Bounds the request rate when the network connects repeatedly // Bounds the request rate when the network connects repeatedly
constexpr uint64_t MIN_INTERVAL_MS = 10 * 1000; constexpr uint64_t MIN_INTERVAL_MS = 10 * 1000;
@ -81,17 +87,6 @@ uint64_t getUptimeMs() {
return get_micros_since_boot() / 1000; return get_micros_since_boot() / 1000;
} }
bool getCertificatePath(char* out_path, size_t out_size) {
// The system file system is mounted at "/system" on ESP32, but it's a folder elsewhere on the simulator
FileSystem* system = file_system_find_by_name("system");
char system_path[FILE_MAX_PATH_STRING_LENGTH];
if (system == nullptr || file_system_get_path(system, system_path, sizeof(system_path)) != ERROR_NONE) {
return false;
}
const int written = snprintf(out_path, out_size, "%s/%s", system_path, API_CERTIFICATE);
return written > 0 && static_cast<size_t>(written) < out_size;
}
/** /**
* Downloads the lookup result into a temp file named @a file_name and parses it * Downloads the lookup result into a temp file named @a file_name and parses it
* @param[in] group the event group to wait on, with a free bit for the download * @param[in] group the event group to wait on, with a free bit for the download
@ -99,10 +94,9 @@ bool getCertificatePath(char* out_path, size_t out_size) {
* @return false when the lookup failed or was cancelled * @return false when the lookup failed or was cancelled
*/ */
bool fetchLookup(const char* file_name, TaskEventGroup* group, const std::atomic<bool>& running, IpLookupResponse* out) { bool fetchLookup(const char* file_name, TaskEventGroup* group, const std::atomic<bool>& running, IpLookupResponse* out) {
char certificate_path[FILE_MAX_PATH_STRING_LENGTH];
char temp_path[FILE_MAX_PATH_STRING_LENGTH]; char temp_path[FILE_MAX_PATH_STRING_LENGTH];
char target_path[FILE_MAX_PATH_STRING_LENGTH]; char target_path[FILE_MAX_PATH_STRING_LENGTH];
if (!getCertificatePath(certificate_path, sizeof(certificate_path)) || paths_get_temp_path(temp_path, sizeof(temp_path)) != ERROR_NONE) { if (paths_get_temp_path(temp_path, sizeof(temp_path)) != ERROR_NONE) {
return false; return false;
} }
if (!directory_exists(temp_path)) { if (!directory_exists(temp_path)) {
@ -120,7 +114,7 @@ bool fetchLookup(const char* file_name, TaskEventGroup* group, const std::atomic
if (http_download_subscribe(&subscription, group) != ERROR_NONE) { if (http_download_subscribe(&subscription, group) != ERROR_NONE) {
return false; return false;
} }
if (http_download_start(API_URL, certificate_path, target_path, &subscription) != ERROR_NONE) { if (http_download_start(API_URL, certificate_pem, target_path, &subscription) != ERROR_NONE) {
http_download_unsubscribe(&subscription); http_download_unsubscribe(&subscription);
return false; return false;
} }

View File

@ -35,7 +35,11 @@ Icons are provided by the **Material Symbols** font, divided into three usage-sp
All font sizes derive from one default text size: `font.defaultSize` in `device.properties` (`CONFIG_TT_FONT_DEFAULT_SIZE`, 14 on the simulator). All font sizes derive from one default text size: `font.defaultSize` in `device.properties` (`CONFIG_TT_FONT_DEFAULT_SIZE`, 14 on the simulator).
Tactility calculates the other text and icon sizes from it in `Tactility/Source/lvgl/FontSizes.cpp`. Tactility calculates the other text and icon sizes from it in `Tactility/Source/lvgl/FontSizes.cpp`.
Text fonts are rasterized on device at startup from `Data/system/fonts/AdwaitaSans.ttf` and cached as `.bin` files in the data path. The fonts of the device's default size are generated at build time by `Buildscripts/generate-fonts.py` (using a host built tool, `Buildscripts/BinfontTool`) and embedded in the firmware as `generated_fonts.c`.
They are opened in place and take no RAM. Other sizes and custom fonts use the cache below, which needs at least 2 MB of total memory: without it only the embedded fonts are used and the Appearance app hides the font settings.
When there's no host C++ compiler at build time, no fonts are embedded and the cache is used for all of them.
Text fonts that aren't embedded are rasterized on device at startup from the embedded `Data/system/fonts/AdwaitaSans.ttf` and cached as `.bin` files in the data path.
`Tactility/Fonts/generate.py` creates that TTF subset from `Tactility/Fonts/AdwaitaSans-Regular.ttf` (requires `pip install fonttools`). `Tactility/Fonts/generate.py` creates that TTF subset from `Tactility/Fonts/AdwaitaSans-Regular.ttf` (requires `pip install fonttools`).
Icon fonts are rasterized on device at startup from `Data/system/fonts/MaterialSymbolsRounded.ttf` (a subset with only the used icons) and cached as `.bin` files in the data path, so any icon size works. Icon fonts are rasterized on device at startup from `Data/system/fonts/MaterialSymbolsRounded.ttf` (a subset with only the used icons) and cached as `.bin` files in the data path, so any icon size works.

View File

@ -42,12 +42,9 @@ bool lvgl_try_lock(uint32_t timeout);
void lvgl_unlock(void); void lvgl_unlock(void);
/** /**
* @brief Runs a function with the LVGL lock held, on the LVGL task when the calling task can't do the work itself. * @brief Runs a function with the LVGL lock held, on the calling task.
* *
* Code that creates widgets can read files, e.g. to decode an image. Reading from flash needs a stack in * The function must not read files. The LVGL task can have its stack in external RAM.
* internal RAM, which tasks with a stack in external RAM don't have. Such tasks have the function run on the LVGL
* task, and wait for it to finish. Other tasks run it themselves.
* Don't call this while holding a lock that LVGL callbacks can wait for.
* @param[in] function the function to run * @param[in] function the function to run
* @param[in] argument passed to @a function * @param[in] argument passed to @a function
*/ */

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Amount of spinner images. The sizes in pixels are 12, 16, 20, 24, 32, 36 and 40 */
#define LVGL_UI_SPINNER_COUNT 7
/** Amount of cursor images. The sizes in pixels are 12, 16, 20, 24, 32 and 40 */
#define LVGL_UI_CURSOR_COUNT 6
/**
* @brief PNG images for the spinner and the mouse cursor, ordered by size.
* The arrays have LVGL_UI_SPINNER_COUNT and LVGL_UI_CURSOR_COUNT entries and must stay valid forever.
*/
struct LvglUiAssets {
const lv_image_dsc_t* spinner;
const lv_image_dsc_t* cursor;
};
/**
* @brief Sets the images that the spinner and cursor getters pick from.
* @param[in] assets the images, copied by reference
*/
void lvgl_ui_assets_configure(const struct LvglUiAssets* assets);
/**
* @brief Gets the spinner image that fits the default text size best.
* @return an image for lv_image_set_src(), or NULL when not configured
*/
const lv_image_dsc_t* lvgl_get_spinner_image(void);
/**
* @brief Gets the mouse cursor image that fits the default text size best.
* @return an image for lv_image_set_src(), or NULL when not configured
*/
const lv_image_dsc_t* lvgl_get_cursor_image(void);
#ifdef __cplusplus
}
#endif

View File

@ -43,54 +43,19 @@ void lvgl_unlock(void) {
lvgl_port_unlock(); lvgl_port_unlock();
} }
struct RunOnTaskRequest {
void (*function)(void*);
void* argument;
SemaphoreHandle_t done;
};
// Runs inside the LVGL task, which holds the LVGL lock
static void run_on_task_callback(void* data) {
struct RunOnTaskRequest* request = data;
request->function(request->argument);
xSemaphoreGive(request->done);
}
void lvgl_run_on_task(void (*function)(void*), void* argument) { void lvgl_run_on_task(void (*function)(void*), void* argument) {
const int stack_marker = 0;
const bool is_lvgl_task = strcmp(pcTaskGetName(NULL), "taskLVGL") == 0;
SemaphoreHandle_t done = NULL;
// A stack in internal RAM can read files, and the LVGL task itself can always run the function
if (initialized && !is_lvgl_task && !esp_ptr_internal(&stack_marker)) {
done = xSemaphoreCreateBinary();
}
if (done == NULL) {
lvgl_lock(); lvgl_lock();
function(argument); function(argument);
lvgl_unlock(); lvgl_unlock();
return;
}
struct RunOnTaskRequest request = { .function = function, .argument = argument, .done = done };
lvgl_lock();
lv_async_call(run_on_task_callback, &request);
lvgl_unlock();
// The LVGL task may be asleep for a while otherwise
lvgl_port_task_wake(LVGL_PORT_EVENT_USER, NULL);
while (xSemaphoreTake(done, pdMS_TO_TICKS(1000)) != pdTRUE) {
if (!initialized) {
// LVGL stopped before it ran the function
lv_async_call_cancel(run_on_task_callback, &request);
break;
}
}
vSemaphoreDelete(done);
} }
error_t lvgl_arch_start() { error_t lvgl_arch_start() {
// The LVGL task must not read files: its stack in external RAM can't be used while the flash cache is off
const bool has_external_ram = heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0;
const lvgl_port_cfg_t lvgl_cfg = { const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = lvgl_module_config.task_priority, .task_priority = lvgl_module_config.task_priority,
.task_stack = lvgl_module_config.task_stack_size, .task_stack = lvgl_module_config.task_stack_size,
.task_stack_caps = has_external_ram ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : (MALLOC_CAP_INTERNAL | MALLOC_CAP_DEFAULT),
.task_affinity = lvgl_module_config.task_affinity, .task_affinity = lvgl_module_config.task_affinity,
.task_max_sleep_ms = 500, .task_max_sleep_ms = 500,
.timer_period_ms = 5 .timer_period_ms = 5

View File

@ -24,6 +24,7 @@
#include <lvgl/widgets/page_indicator.h> #include <lvgl/widgets/page_indicator.h>
#include <lvgl/widgets/step_indicator.h> #include <lvgl/widgets/step_indicator.h>
#include <lvgl/widgets/qr.h> #include <lvgl/widgets/qr.h>
#include <lvgl/ui_assets.h>
#include <lvgl/widgets/spinner.h> #include <lvgl/widgets/spinner.h>
#include <lvgl/widgets/statusbar.h> #include <lvgl/widgets/statusbar.h>
#include <lvgl/widgets/text_button.h> #include <lvgl/widgets/text_button.h>
@ -155,6 +156,11 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_qr_set_data), DEFINE_MODULE_SYMBOL(lvgl_qr_set_data),
DEFINE_MODULE_SYMBOL(lvgl_qr_set_colors), DEFINE_MODULE_SYMBOL(lvgl_qr_set_colors),
// ui_assets
DEFINE_MODULE_SYMBOL(lvgl_ui_assets_configure),
DEFINE_MODULE_SYMBOL(lvgl_get_spinner_image),
DEFINE_MODULE_SYMBOL(lvgl_get_cursor_image),
// lvgl_spinner // lvgl_spinner
DEFINE_MODULE_SYMBOL(lvgl_spinner_create), DEFINE_MODULE_SYMBOL(lvgl_spinner_create),
// lvgl_toolbar // lvgl_toolbar

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
#include <lvgl/ui_assets.h>
#include <lvgl/fonts.h>
#include <lvgl.h>
#include <stdlib.h>
// Must match the sizes in Buildscripts/generate-ui-assets.py
static const int32_t spinner_sizes[LVGL_UI_SPINNER_COUNT] = { 12, 16, 20, 24, 32, 36, 40 };
static const int32_t cursor_sizes[LVGL_UI_CURSOR_COUNT] = { 12, 16, 20, 24, 32, 40 };
static struct LvglUiAssets ui_assets;
void lvgl_ui_assets_configure(const struct LvglUiAssets* assets) {
ui_assets = *assets;
}
/** @return the index of the size that is the closest to target_size */
static int nearest_size(const int32_t* sizes, int count, int32_t target_size) {
int nearest = 0;
for (int i = 1; i < count; i++) {
if (abs(sizes[i] - target_size) < abs(sizes[nearest] - target_size)) {
nearest = i;
}
}
return nearest;
}
const lv_image_dsc_t* lvgl_get_spinner_image(void) {
if (ui_assets.spinner == NULL) {
return NULL;
}
const int32_t text_size = (int32_t)lvgl_get_text_font_height(FONT_SIZE_DEFAULT);
return &ui_assets.spinner[nearest_size(spinner_sizes, LVGL_UI_SPINNER_COUNT, text_size * 3 / 2)];
}
const lv_image_dsc_t* lvgl_get_cursor_image(void) {
if (ui_assets.cursor == NULL) {
return NULL;
}
const int32_t text_size = (int32_t)lvgl_get_text_font_height(FONT_SIZE_DEFAULT);
return &ui_assets.cursor[nearest_size(cursor_sizes, LVGL_UI_CURSOR_COUNT, text_size * 3 / 2)];
}

View File

@ -1,11 +1,9 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration #define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
#include <lvgl/ui_assets.h>
#include <lvgl/widgets/spinner.h> #include <lvgl/widgets/spinner.h>
// TODO: Don't depend on asset from a different project
#define SPINNER_ASSET "A:/system/spinner.png"
static void spinner_constructor(const lv_obj_class_t* object_class, lv_obj_t* object); static void spinner_constructor(const lv_obj_class_t* object_class, lv_obj_t* object);
static const lv_obj_class_t tt_spinner_class = { static const lv_obj_class_t tt_spinner_class = {
@ -27,7 +25,7 @@ lv_obj_t* lvgl_spinner_create(lv_obj_t* parent) {
lv_obj_t* obj = lv_obj_class_create_obj(&tt_spinner_class, parent); lv_obj_t* obj = lv_obj_class_create_obj(&tt_spinner_class, parent);
lv_obj_class_init_obj(obj); lv_obj_class_init_obj(obj);
lv_image_set_src(obj, SPINNER_ASSET); lv_image_set_src(obj, lvgl_get_spinner_image());
return obj; return obj;
} }

View File

@ -172,7 +172,6 @@ lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_wi
if (content == nullptr) { if (content == nullptr) {
return nullptr; return nullptr;
} }
// The widgets can load files, e.g. images, so tasks without a stack for that have the LVGL task build them
BuildRequest request { content, create_widgets, user_data }; BuildRequest request { content, create_widgets, user_data };
lvgl_run_on_task(build_window_widget_locked, &request); lvgl_run_on_task(build_window_widget_locked, &request);
return request.widget; return request.widget;

View File

@ -8,4 +8,5 @@ tactility_add_module(service-module
SRCS ${SOURCE_FILES} SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/ INCLUDE_DIRS include/
REQUIRES TactilityKernel REQUIRES TactilityKernel
PRIV_REQUIRES TactilityKernelCpp
) )

View File

@ -2,14 +2,15 @@
#include <service/manager.h> #include <service/manager.h>
#include <TactilityCpp/Memory.h>
#include <TactilityCpp/std/string.h>
#include <TactilityCpp/std/unordered_map.h>
#include <tactility/check.h>
#include <tactility/concurrent/mutex.h> #include <tactility/concurrent/mutex.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/system_event.h> #include <tactility/system_event.h>
#include <new>
#include <string>
#include <unordered_map>
#define TAG "service_registration" #define TAG "service_registration"
// Defined in service_instance.cpp. Internal-only: lets the registry drive state // Defined in service_instance.cpp. Internal-only: lets the registry drive state
@ -17,7 +18,7 @@
extern "C" void service_instance_set_state(ServiceInstance* instance, ServiceState state); extern "C" void service_instance_set_state(ServiceInstance* instance, ServiceState state);
struct ManifestLedger { struct ManifestLedger {
std::unordered_map<std::string, const ServiceManifest*> manifests; tt::std::unordered_map<tt::std::string, const ServiceManifest*> manifests;
Mutex mutex {}; Mutex mutex {};
ManifestLedger() { mutex_construct(&mutex); } ManifestLedger() { mutex_construct(&mutex); }
@ -25,7 +26,7 @@ struct ManifestLedger {
}; };
struct InstanceLedger { struct InstanceLedger {
std::unordered_map<std::string, ServiceInstance*> instances; tt::std::unordered_map<tt::std::string, ServiceInstance*> instances;
Mutex mutex {}; Mutex mutex {};
InstanceLedger() { mutex_construct(&mutex); } InstanceLedger() { mutex_construct(&mutex); }
@ -33,13 +34,15 @@ struct InstanceLedger {
}; };
static ManifestLedger& get_manifest_ledger() { static ManifestLedger& get_manifest_ledger() {
static ManifestLedger ledger; static ManifestLedger* ledger = tt::newObject<ManifestLedger>();
return ledger; check(ledger != nullptr);
return *ledger;
} }
static InstanceLedger& get_instance_ledger() { static InstanceLedger& get_instance_ledger() {
static InstanceLedger ledger; static InstanceLedger* ledger = tt::newObject<InstanceLedger>();
return ledger; check(ledger != nullptr);
return *ledger;
} }
#define manifest_ledger get_manifest_ledger() #define manifest_ledger get_manifest_ledger()
@ -100,7 +103,7 @@ error_t service_manager_start(const char* id) {
return ERROR_INVALID_STATE; return ERROR_INVALID_STATE;
} }
auto* instance = new(std::nothrow) ServiceInstance { .manifest = nullptr, .data = nullptr, .internal = nullptr }; auto* instance = tt::newObject<ServiceInstance>(ServiceInstance { .manifest = nullptr, .data = nullptr, .internal = nullptr });
if (instance == nullptr) { if (instance == nullptr) {
mutex_unlock(&instance_ledger.mutex); mutex_unlock(&instance_ledger.mutex);
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
@ -109,7 +112,7 @@ error_t service_manager_start(const char* id) {
error_t error = service_instance_construct(instance, manifest); error_t error = service_instance_construct(instance, manifest);
if (error != ERROR_NONE) { if (error != ERROR_NONE) {
mutex_unlock(&instance_ledger.mutex); mutex_unlock(&instance_ledger.mutex);
delete instance; tt::deleteObject(instance);
return error; return error;
} }
@ -137,7 +140,7 @@ error_t service_manager_start(const char* id) {
mutex_unlock(&instance_ledger.mutex); mutex_unlock(&instance_ledger.mutex);
service_instance_destruct(instance); service_instance_destruct(instance);
delete instance; tt::deleteObject(instance);
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
@ -167,7 +170,7 @@ error_t service_manager_stop(const char* id) {
mutex_unlock(&instance_ledger.mutex); mutex_unlock(&instance_ledger.mutex);
service_instance_destruct(instance); service_instance_destruct(instance);
delete instance; tt::deleteObject(instance);
ServiceStoppedEvent stop_event = { .id = id }; ServiceStoppedEvent stop_event = { .id = id };
system_event_emit(KERNEL_EVENT_SERVICE_STOPPED, &stop_event, sizeof(stop_event)); system_event_emit(KERNEL_EVENT_SERVICE_STOPPED, &stop_event, sizeof(stop_event));

View File

@ -18,6 +18,8 @@ extern "C" {
* need (WIFI_MODE_STA or WIFI_MODE_AP); the radio is brought up on the first acquire (merging into * need (WIFI_MODE_STA or WIFI_MODE_AP); the radio is brought up on the first acquire (merging into
* WIFI_MODE_APSTA if another owner is already active) and only torn down once every owner has * WIFI_MODE_APSTA if another owner is already active) and only torn down once every owner has
* released. Several owners can hold the same mode bit, and the bit stays active until its last owner released. * released. Several owners can hold the same mode bit, and the bit stays active until its last owner released.
* The default station netif is created before the radio starts when the station bit is acquired, and destroyed with the last
* station owner. Creating it after the radio started leaves it without an IP address.
* *
* @param mode_bit WIFI_MODE_STA or WIFI_MODE_AP (not WIFI_MODE_APSTA/WIFI_MODE_NULL). * @param mode_bit WIFI_MODE_STA or WIFI_MODE_AP (not WIFI_MODE_APSTA/WIFI_MODE_NULL).
* @retval true the mode bit is now active (radio is initialized, started, and its current mode * @retval true the mode bit is now active (radio is initialized, started, and its current mode

View File

@ -51,7 +51,6 @@ struct Esp32WifiCtx {
wifi_ap_record_t scanResults[WIFI_SCAN_RECORD_LIMIT] = {}; wifi_ap_record_t scanResults[WIFI_SCAN_RECORD_LIMIT] = {};
uint16_t scanResultCount = 0; uint16_t scanResultCount = 0;
esp_netif_t* netif = nullptr;
esp_event_handler_instance_t wifiEventHandler = nullptr; esp_event_handler_instance_t wifiEventHandler = nullptr;
esp_event_handler_instance_t ipEventHandler = nullptr; esp_event_handler_instance_t ipEventHandler = nullptr;
@ -80,6 +79,9 @@ wifi_mode_t g_radioMode = WIFI_MODE_NULL;
// Owners per mode bit, as several owners can share one: a bit leaves the mode when its last owner released // Owners per mode bit, as several owners can share one: a bit leaves the mode when its last owner released
int g_staOwnerCount = 0; int g_staOwnerCount = 0;
int g_apOwnerCount = 0; int g_apOwnerCount = 0;
// The station netif exists for as long as the station bit is active, and it's created before the radio starts,
// so the netif always handles the station's start event. A netif that is created after the radio started doesn't get an IP address.
esp_netif_t* g_staNetif = nullptr;
// Netifs that were started and not stopped or destroyed since // Netifs that were started and not stopped or destroyed since
std::mutex g_startedNetifMutex; std::mutex g_startedNetifMutex;
@ -111,6 +113,15 @@ void mark_netif_stopped(const void* netif) {
} }
} }
// Must be called with the radio mutex held
void destroy_station_netif_if(bool destroy) {
if (destroy && g_staNetif != nullptr) {
esp_wifi_clear_default_wifi_driver_and_handlers(g_staNetif);
esp_netif_destroy(g_staNetif);
g_staNetif = nullptr;
}
}
int& modeOwnerCount(wifi_mode_t mode_bit) { int& modeOwnerCount(wifi_mode_t mode_bit) {
return mode_bit == WIFI_MODE_AP ? g_apOwnerCount : g_staOwnerCount; return mode_bit == WIFI_MODE_AP ? g_apOwnerCount : g_staOwnerCount;
} }
@ -229,22 +240,10 @@ void on_wifi_or_ip_event(void* arg, esp_event_base_t event_base, int32_t event_i
// ---- Work, always run inline on the caller (see note at the top) ---- // ---- Work, always run inline on the caller (see note at the top) ----
error_t bring_up_wifi(Esp32WifiCtx* ctx) { error_t bring_up_wifi(Esp32WifiCtx* ctx) {
ctx->netif = esp_netif_create_default_wifi_sta();
if (ctx->netif == nullptr) {
LOG_E(TAG, "Failed to create default STA netif");
return ERROR_RESOURCE;
}
// Another owner (e.g. ESP-NOW) can have the radio running already. Its WIFI_EVENT_STA_START has been
// delivered by then, or is still on its way. If it was delivered, nothing starts the new netif, so it's started below.
bool radio_already_started = false;
// Warning: this is the memory-intensive operation. It uses over 100kB of // Warning: this is the memory-intensive operation. It uses over 100kB of
// RAM with default settings. // RAM with default settings. It also creates the station netif.
if (!esp32_wifi_radio_acquire_ex(WIFI_MODE_STA, &radio_already_started)) { if (!esp32_wifi_radio_acquire(WIFI_MODE_STA)) {
LOG_E(TAG, "Failed to acquire WiFi radio for station mode"); LOG_E(TAG, "Failed to acquire WiFi radio for station mode");
esp_netif_destroy(ctx->netif);
ctx->netif = nullptr;
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
@ -254,8 +253,6 @@ error_t bring_up_wifi(Esp32WifiCtx* ctx) {
if (err != ESP_OK) { if (err != ESP_OK) {
LOG_E(TAG, "Failed to register WIFI_EVENT handler: %s", esp_err_to_name(err)); LOG_E(TAG, "Failed to register WIFI_EVENT handler: %s", esp_err_to_name(err));
esp32_wifi_radio_release(WIFI_MODE_STA); esp32_wifi_radio_release(WIFI_MODE_STA);
esp_netif_destroy(ctx->netif);
ctx->netif = nullptr;
return esp_err_to_error(err); return esp_err_to_error(err);
} }
@ -265,16 +262,9 @@ error_t bring_up_wifi(Esp32WifiCtx* ctx) {
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, ctx->wifiEventHandler); esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, ctx->wifiEventHandler);
ctx->wifiEventHandler = nullptr; ctx->wifiEventHandler = nullptr;
esp32_wifi_radio_release(WIFI_MODE_STA); esp32_wifi_radio_release(WIFI_MODE_STA);
esp_netif_destroy(ctx->netif);
ctx->netif = nullptr;
return esp_err_to_error(err); return esp_err_to_error(err);
} }
// A start event that is still on its way starts the netif too, which __wrap_esp_netif_action_start() ignores
if (radio_already_started) {
esp_netif_action_start(ctx->netif, WIFI_EVENT, WIFI_EVENT_STA_START, nullptr);
}
mutex_lock(&ctx->mutex); mutex_lock(&ctx->mutex);
ctx->radioState = WIFI_RADIO_STATE_ON; ctx->radioState = WIFI_RADIO_STATE_ON;
mutex_unlock(&ctx->mutex); mutex_unlock(&ctx->mutex);
@ -303,13 +293,6 @@ void bring_down_wifi(Esp32WifiCtx* ctx) {
esp_wifi_disconnect(); esp_wifi_disconnect();
} }
// Detach netif from the internal WiFi event handlers before stopping,
// otherwise esp_netif_destroy() can race with esp_wifi_stop()'s own
// netif teardown (see esp32_ble/WifiEsp.cpp for the same issue).
if (ctx->netif != nullptr) {
esp_wifi_clear_default_wifi_driver_and_handlers(ctx->netif);
}
if (ctx->wifiEventHandler != nullptr) { if (ctx->wifiEventHandler != nullptr) {
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, ctx->wifiEventHandler); esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, ctx->wifiEventHandler);
ctx->wifiEventHandler = nullptr; ctx->wifiEventHandler = nullptr;
@ -319,13 +302,9 @@ void bring_down_wifi(Esp32WifiCtx* ctx) {
ctx->ipEventHandler = nullptr; ctx->ipEventHandler = nullptr;
} }
// The station netif is destroyed with the last station owner of the radio
esp32_wifi_radio_release(WIFI_MODE_STA); esp32_wifi_radio_release(WIFI_MODE_STA);
if (ctx->netif != nullptr) {
esp_netif_destroy(ctx->netif);
ctx->netif = nullptr;
}
mutex_lock(&ctx->mutex); mutex_lock(&ctx->mutex);
ctx->radioState = WIFI_RADIO_STATE_OFF; ctx->radioState = WIFI_RADIO_STATE_OFF;
mutex_unlock(&ctx->mutex); mutex_unlock(&ctx->mutex);
@ -703,11 +682,25 @@ bool esp32_wifi_radio_acquire_ex(wifi_mode_t mode_bit, bool* was_started) {
bool first_owner = g_radioOwnerCount == 0; bool first_owner = g_radioOwnerCount == 0;
*was_started = !first_owner; *was_started = !first_owner;
// Before the radio starts, so that the netif handles the station's start event
bool created_netif = false;
if (mode_bit == WIFI_MODE_STA && g_staNetif == nullptr) {
g_staNetif = esp_netif_create_default_wifi_sta();
if (g_staNetif == nullptr) {
LOG_E(TAG, "esp32_wifi_radio_acquire: failed to create the station netif");
mutex_unlock(&g_radioMutex);
return false;
}
created_netif = true;
}
if (first_owner) { if (first_owner) {
wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT();
esp_err_t err = esp_wifi_init(&init_config); esp_err_t err = esp_wifi_init(&init_config);
if (err != ESP_OK) { if (err != ESP_OK) {
LOG_E(TAG, "esp32_wifi_radio_acquire: esp_wifi_init failed: %s", esp_err_to_name(err)); LOG_E(TAG, "esp32_wifi_radio_acquire: esp_wifi_init failed: %s", esp_err_to_name(err));
destroy_station_netif_if(created_netif);
mutex_unlock(&g_radioMutex); mutex_unlock(&g_radioMutex);
return false; return false;
} }
@ -721,6 +714,7 @@ bool esp32_wifi_radio_acquire_ex(wifi_mode_t mode_bit, bool* was_started) {
if (first_owner) { if (first_owner) {
esp_wifi_deinit(); esp_wifi_deinit();
} }
destroy_station_netif_if(created_netif);
mutex_unlock(&g_radioMutex); mutex_unlock(&g_radioMutex);
return false; return false;
} }
@ -731,6 +725,7 @@ bool esp32_wifi_radio_acquire_ex(wifi_mode_t mode_bit, bool* was_started) {
LOG_E(TAG, "esp32_wifi_radio_acquire: esp_wifi_start failed: %s", esp_err_to_name(err)); LOG_E(TAG, "esp32_wifi_radio_acquire: esp_wifi_start failed: %s", esp_err_to_name(err));
esp_wifi_set_mode(WIFI_MODE_NULL); esp_wifi_set_mode(WIFI_MODE_NULL);
esp_wifi_deinit(); esp_wifi_deinit();
destroy_station_netif_if(created_netif);
mutex_unlock(&g_radioMutex); mutex_unlock(&g_radioMutex);
return false; return false;
} }
@ -765,6 +760,13 @@ void esp32_wifi_radio_release(wifi_mode_t mode_bit) {
} }
g_radioOwnerCount--; g_radioOwnerCount--;
// Detach the station netif from the internal WiFi event handlers before stopping,
// otherwise esp_netif_destroy() can race with esp_wifi_stop()'s own netif teardown
const bool release_netif = mode_bit == WIFI_MODE_STA && g_staOwnerCount <= 0 && g_staNetif != nullptr;
if (release_netif) {
esp_wifi_clear_default_wifi_driver_and_handlers(g_staNetif);
}
if (g_radioOwnerCount <= 0) { if (g_radioOwnerCount <= 0) {
esp_wifi_stop(); esp_wifi_stop();
esp_wifi_set_mode(WIFI_MODE_NULL); esp_wifi_set_mode(WIFI_MODE_NULL);
@ -777,6 +779,11 @@ void esp32_wifi_radio_release(wifi_mode_t mode_bit) {
esp_wifi_set_mode(g_radioMode); esp_wifi_set_mode(g_radioMode);
} }
if (release_netif) {
esp_netif_destroy(g_staNetif);
g_staNetif = nullptr;
}
mutex_unlock(&g_radioMutex); mutex_unlock(&g_radioMutex);
} }
@ -810,9 +817,10 @@ Driver esp32_wifi_driver = {
.compatible = (const char*[]) { "espressif,esp32-wifi", nullptr }, .compatible = (const char*[]) { "espressif,esp32-wifi", nullptr },
.start_device = start_device, .start_device = start_device,
.stop_device = stop_device, .stop_device = stop_device,
.api = (const void*)&esp32_wifi_api, .probe = nullptr,
.api = static_cast<const void*>(&esp32_wifi_api),
.device_type = &WIFI_TYPE, .device_type = &WIFI_TYPE,
.internal = nullptr .internal = nullptr,
}; };
} // extern "C" } // extern "C"

View File

@ -60,6 +60,7 @@ esp_err_t initNvsFlashSafely() {
wl_handle_t data_wl_handle = WL_INVALID_HANDLE; wl_handle_t data_wl_handle = WL_INVALID_HANDLE;
#ifdef CONFIG_TT_USER_DATA_LOCATION_INTERNAL
size_t getSectorSize() { size_t getSectorSize() {
#if defined(CONFIG_FATFS_SECTOR_512) #if defined(CONFIG_FATFS_SECTOR_512)
return 512; return 512;
@ -73,6 +74,7 @@ size_t getSectorSize() {
#error Not implemented #error Not implemented
#endif #endif
} }
#endif
} // namespace } // namespace
@ -84,6 +86,7 @@ error_t platform_esp32_start_partitions() {
LOG_I(TAG, "Init partitions"); LOG_I(TAG, "Init partitions");
ESP_ERROR_CHECK(initNvsFlashSafely()); ESP_ERROR_CHECK(initNvsFlashSafely());
#ifdef CONFIG_TT_USER_DATA_LOCATION_INTERNAL
const esp_vfs_fat_mount_config_t mount_config = { const esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = false, .format_if_mount_failed = false,
.max_files = 4, .max_files = 4,
@ -92,16 +95,6 @@ error_t platform_esp32_start_partitions() {
.use_one_fat = true, .use_one_fat = true,
}; };
auto system_result = esp_vfs_fat_spiflash_mount_ro("/system", "system", &mount_config);
if (system_result != ESP_OK) {
LOG_E(TAG, "Failed to mount /system (%s)", esp_err_to_name(system_result));
return ERROR_RESOURCE;
}
LOG_I(TAG, "Mounted /system");
static auto system_fs_data = PartitionFsData("/system");
file_system_add("system", &partition_fs_api, &system_fs_data);
#ifdef CONFIG_TT_USER_DATA_LOCATION_INTERNAL
auto data_result = esp_vfs_fat_spiflash_mount_rw_wl("/data", "data", &mount_config, &data_wl_handle); auto data_result = esp_vfs_fat_spiflash_mount_rw_wl("/data", "data", &mount_config, &data_wl_handle);
if (data_result != ESP_OK) { if (data_result != ESP_OK) {
LOG_E(TAG, "Failed to mount /data (%s)", esp_err_to_name(data_result)); LOG_E(TAG, "Failed to mount /data (%s)", esp_err_to_name(data_result));

View File

@ -21,7 +21,6 @@ struct DirectoryFsData {
bool mounted; bool mounted;
}; };
DirectoryFsData system_fs_data;
DirectoryFsData data_fs_data; DirectoryFsData data_fs_data;
DirectoryFsData root_fs_data; DirectoryFsData root_fs_data;
@ -56,7 +55,7 @@ const FileSystemApi directory_fs_api = {
}; };
// relative_path is resolved against the process' current working directory. // relative_path is resolved against the process' current working directory.
// The simulator is expected to run with Data/ as its working directory, so "data"/"system" here match the real on-disk folder names. // The simulator is expected to run with Data/ as its working directory, so "data" here matches the real on-disk folder names.
// realpath() converts it to a real absolute path once, up front. // realpath() converts it to a real absolute path once, up front.
// Callers elsewhere (e.g. the shell) that ask for this filesystem's path via file_system_get_path() then // Callers elsewhere (e.g. the shell) that ask for this filesystem's path via file_system_get_path() then
// get something meaningful regardless of their own notion of "current directory", real or virtual. // get something meaningful regardless of their own notion of "current directory", real or virtual.
@ -72,9 +71,8 @@ FileSystem* registerDirectoryFs(const char* name, const char* relativePath, Dire
} // namespace } // namespace
error_t platform_posix_start_partitions() { error_t platform_posix_start_partitions() {
registerDirectoryFs("system", "system", &system_fs_data);
registerDirectoryFs("data", "data", &data_fs_data); registerDirectoryFs("data", "data", &data_fs_data);
// "data"/"system" live somewhere under here (wherever the process was launched from). U // "data" lives somewhere under here (wherever the process was launched from). U
// Unlike ESP32, POSIX genuinely has a real "/", so it gets registered too. // Unlike ESP32, POSIX genuinely has a real "/", so it gets registered too.
registerDirectoryFs("root", "/", &root_fs_data); registerDirectoryFs("root", "/", &root_fs_data);
return ERROR_NONE; return ERROR_NONE;

View File

@ -166,7 +166,7 @@ endif ()
if (DEFINED ENV{ESP_IDF_VERSION}) if (DEFINED ENV{ESP_IDF_VERSION})
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} "${GENERATED_DIR}/devicetree.c" SRCS ${SOURCE_FILES} "${GENERATED_DIR}/devicetree.c" "${GENERATED_DIR}/generated_assets.c" "${GENERATED_DIR}/generated_fonts.c"
INCLUDE_DIRS Include/ INCLUDE_DIRS Include/
PRIV_INCLUDE_DIRS Private/ PRIV_INCLUDE_DIRS Private/
REQUIRES ${TACTILITY_REQUIRES_LIST} REQUIRES ${TACTILITY_REQUIRES_LIST}
@ -179,10 +179,10 @@ if (DEFINED ENV{ESP_IDF_VERSION})
endif () endif ()
if (NOT DEFINED TACTILITY_SKIP_SPIFFS) if (NOT DEFINED TACTILITY_SKIP_SPIFFS)
# Read-only
fatfs_create_rawflash_image(system "${CMAKE_CURRENT_SOURCE_DIR}/../Data/system" FLASH_IN_PROJECT PRESERVE_TIME)
# Read-write (skipped when user data lives on the SD card instead of internal flash) # Read-write (skipped when user data lives on the SD card instead of internal flash)
if (NOT CONFIG_TT_USER_DATA_LOCATION_SD) if (NOT CONFIG_TT_USER_DATA_LOCATION_SD)
# The folder is not in the repository
file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../Data/data")
fatfs_create_spiflash_image(data "${CMAKE_CURRENT_SOURCE_DIR}/../Data/data" FLASH_IN_PROJECT PRESERVE_TIME) fatfs_create_spiflash_image(data "${CMAKE_CURRENT_SOURCE_DIR}/../Data/data" FLASH_IN_PROJECT PRESERVE_TIME)
endif () endif ()
endif () endif ()
@ -224,6 +224,46 @@ target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/devicetree.c")
target_include_directories(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}") target_include_directories(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}")
add_dependencies(${COMPONENT_LIB} Generated) add_dependencies(${COMPONENT_LIB} Generated)
#
# Assets: Data/system embedded as C byte arrays
#
# Rewrites its outputs only when the content changes. The assets are the whole Data/system tree,
# so no fixed DEPENDS list is kept.
add_custom_target(GeneratedAssets ALL
COMMAND python "${CMAKE_SOURCE_DIR}/Buildscripts/generate-assets.py"
"${CMAKE_SOURCE_DIR}/Data/system" "${GENERATED_DIR}"
BYPRODUCTS "${GENERATED_DIR}/generated_assets.c" "${GENERATED_DIR}/generated_assets.h"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generating asset source files..."
)
set_source_files_properties("${GENERATED_DIR}/generated_assets.c" PROPERTIES GENERATED TRUE)
set_source_files_properties("${GENERATED_DIR}/generated_assets.h" PROPERTIES GENERATED TRUE)
target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/generated_assets.c")
add_dependencies(${COMPONENT_LIB} GeneratedAssets)
#
# Fonts: the binfont files of the device's default font size, embedded as C byte arrays
#
# The fonts are rasterized by a tool that is built with the host compiler. Without one the table stays empty.
if (DEFINED ENV{ESP_IDF_VERSION})
set(fonts_simulator 0)
else ()
set(fonts_simulator 1)
endif ()
add_custom_target(GeneratedFonts ALL
COMMAND python "${CMAKE_SOURCE_DIR}/Buildscripts/generate-fonts.py"
"${DEVICETREE_LOCATION}" ${fonts_simulator} "${GENERATED_DIR}"
BYPRODUCTS "${GENERATED_DIR}/generated_fonts.c" "${GENERATED_DIR}/generated_fonts.h"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generating font source files..."
)
set_source_files_properties("${GENERATED_DIR}/generated_fonts.c" PROPERTIES GENERATED TRUE)
set_source_files_properties("${GENERATED_DIR}/generated_fonts.h" PROPERTIES GENERATED TRUE)
target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/generated_fonts.c")
add_dependencies(${COMPONENT_LIB} GeneratedFonts)
# #
# Fonts # Fonts
# #

View File

@ -1,13 +0,0 @@
#pragma once
/**
* Deprecated list of LVGL asset folder paths>
*/
#define TT_ASSET_FOLDER "A:/system/"
#define TT_ASSET(file) TT_ASSET_FOLDER file
// UI
#define TT_ASSETS_UI_SPINNER TT_ASSET("spinner.png")
#define TT_ASSETS_UI_CURSOR TT_ASSET("cursor.png")

View File

@ -1,14 +1,16 @@
#pragma once #pragma once
#include <tactility/concurrent/dispatcher.h> #include <tactility/concurrent/mutex.h>
#include <tactility/concurrent/thread.h>
#include <functional> #include <functional>
namespace tt { namespace tt {
/** /**
* A thin C++ wrapper around the TactilityKernel dispatcher, allowing * Runs functions in order on a task that is pinned to the main dispatcher's CPU core.
* std::function (and therefore capturing lambdas) to be dispatched. * The task is created when a function is queued and no task is running, and it ends when the queue is empty.
* Its stack is in internal RAM, so the functions can access flash.
*/ */
class [[deprecated("Use an app task's stack or a custom task with its own stack")]] MainDispatcher final { class [[deprecated("Use an app task's stack or a custom task with its own stack")]] MainDispatcher final {
@ -16,26 +18,42 @@ public:
using Function = std::function<void()>; using Function = std::function<void()>;
explicit MainDispatcher(DispatcherHandle_t handle) : handle(handle) {}
/**
* Queue a function to be consumed on the main task.
* @param[in] function the function to execute elsewhere
* @param[in] timeout lock acquisition timeout
* @return true if dispatching was successful (timeout not reached)
*/
bool dispatch(Function function, TickType_t timeout = portMAX_DELAY) const;
private: private:
DispatcherHandle_t handle; struct Node;
::Mutex mutex;
Node* head = nullptr;
Node* tail = nullptr;
bool running = false;
// The task that has ended or is ending, as it can't free itself
::Thread* stale = nullptr;
static int32_t taskMain(void* context);
Node* popOrStop();
/** Must be called with the mutex held and no task running */
bool startTask();
public:
MainDispatcher();
~MainDispatcher() { mutex_destruct(&mutex); }
MainDispatcher(const MainDispatcher&) = delete;
MainDispatcher& operator=(const MainDispatcher&) = delete;
/**
* Queue a function to be run on the main dispatcher's task.
* @param[in] function the function to execute elsewhere
* @return true if the function was queued
*/
bool dispatch(Function function);
}; };
/** Provides access to the dispatcher that runs on the main task. /** Provides access to the dispatcher that runs on the main dispatcher's task.
* @warning This dispatcher is used for WiFi and might block for some time during WiFi connection. * @warning This dispatcher is used for WiFi and might block for some time during WiFi connection.
* @return the dispatcher * @return the one dispatcher instance
*/ */
[[deprecated("Use an app task's stack or a custom task with its own stack")]] [[deprecated("Use an app task's stack or a custom task with its own stack")]]
MainDispatcher getMainDispatcher(); MainDispatcher& getMainDispatcher();
} // namespace tt } // namespace tt

View File

@ -5,14 +5,6 @@
namespace tt::file { namespace tt::file {
constexpr auto* SYSTEM_PARTITION_NAME = "system";
#ifdef ESP_PLATFORM
constexpr auto* MOUNT_POINT_SYSTEM = "/system";
#else
constexpr auto* MOUNT_POINT_SYSTEM = "system";
#endif
constexpr auto* DATA_PARTITION_NAME = "data"; constexpr auto* DATA_PARTITION_NAME = "data";
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM

View File

@ -11,9 +11,3 @@
#else // Sim #else // Sim
#define TT_FEATURE_SCREENSHOT_ENABLED true #define TT_FEATURE_SCREENSHOT_ENABLED true
#endif #endif
namespace tt::config {
constexpr auto SHOW_SYSTEM_PARTITION = false;
}

View File

@ -2,6 +2,9 @@
#include <service/instance.h> #include <service/instance.h>
#include <TactilityCpp/Allocator.h>
#include <TactilityCpp/Memory.h>
#include <memory> #include <memory>
namespace tt::service { namespace tt::service {
@ -22,7 +25,10 @@ public:
virtual void onStop(ServiceContext& serviceContext) {} virtual void onStop(ServiceContext& serviceContext) {}
}; };
/** Creates a service for ServiceManifest::createService. The result is released with deleteObject(), see ServiceManifest. */
template<typename T> template<typename T>
void* create(const ::ServiceManifest* /*manifest*/) { return new std::shared_ptr<Service>(std::shared_ptr<T>(new T)); } void* create(const ::ServiceManifest* /*manifest*/) {
return newObject<::std::shared_ptr<Service>>(::std::allocate_shared<T>(OptExternalAllocator<T>()));
}
} }

View File

@ -16,7 +16,11 @@ struct ServiceManifest {
/** The identifier by which the app is launched by the system and other apps. */ /** The identifier by which the app is launched by the system and other apps. */
std::string id {}; std::string id {};
/** Create the instance of the app */ /**
* Creates the instance of the service: use create<T>(), or return a std::shared_ptr<Service> that is allocated with tt::newObject().
* The registration releases the returned pointer with tt::deleteObject() when the service is removed,
* so a pointer from plain new is released incorrectly.
*/
CreateService createService = nullptr; CreateService createService = nullptr;
}; };

View File

@ -1,72 +0,0 @@
#pragma once
#include <cstdint>
namespace tt::service::webserver {
/**
* @brief Asset version tracking for web server content synchronization
*
* Manages version.json files in both Data partition and SD card to ensure
* proper synchronization and recovery of web assets.
*/
struct AssetVersion {
uint32_t version; // Integer version number
AssetVersion() : version(0) {}
explicit AssetVersion(uint32_t v) : version(v) {}
};
/**
* @brief Load asset version from Data partition
* @param[out] version The version structure to populate
* @return true if version was loaded successfully
*/
bool loadDataVersion(AssetVersion& version);
/**
* @brief Load asset version from SD card
* @param[out] version The version structure to populate
* @return true if version was loaded successfully
*/
bool loadSdVersion(AssetVersion& version);
/**
* @brief Save asset version to Data partition
* @param[in] version The version to save
* @return true if version was saved successfully
*/
bool saveDataVersion(const AssetVersion& version);
/**
* @brief Save asset version to SD card
* @param[in] version The version to save
* @return true if version was saved successfully
*/
bool saveSdVersion(const AssetVersion& version);
/**
* @brief Check if Data partition has any web assets
* @return true if Data partition contains web assets
*/
bool hasDataAssets();
/**
* @brief Check if SD card has any web assets
* @return true if SD card contains web assets backup
*/
bool hasSdAssets();
/**
* @brief Synchronize assets between Data partition and SD card based on version
*
* Logic:
* - If Data is empty: Copy from SD card (recovery mode)
* - If SD version > Data version: Copy SD -> Data (firmware update)
* - If Data version > SD version: Copy Data -> SD (backup customizations)
*
* @return true if sync completed successfully
*/
bool syncAssets();
} // namespace tt::service::webserver

View File

@ -1,11 +1,10 @@
#pragma once #pragma once
#include <tactility/concurrent/dispatcher.h>
namespace tt { namespace tt {
/** @return the handle of the dispatcher that getMainDispatcher() wraps */ /** Queues a function on the main dispatcher's task without using the deprecated MainDispatcher class.
DispatcherHandle_t getMainDispatcherHandle(); * @return true if the function was queued */
bool dispatchOnMainDispatcher(void (*function)(void* context), void* context);
void prepareFileSystems(); void prepareFileSystems();

View File

@ -11,6 +11,12 @@ struct PixelBuffer;
namespace tt::app::boot { namespace tt::app::boot {
/** A PNG image in memory */
struct BootLogo {
const uint8_t* data = nullptr;
size_t size = 0;
};
/** /**
* Draws boot screens directly to the display, for use while LVGL isn't running. * Draws boot screens directly to the display, for use while LVGL isn't running.
* Content is drawn in the orientation from the display settings. * Content is drawn in the orientation from the display settings.
@ -53,10 +59,10 @@ public:
/** /**
* Shows a logo with text lines below it, centered on a black background. * Shows a logo with text lines below it, centered on a black background.
* @param[in] logoPath path to a PNG file, or empty for no logo * @param[in] logo a PNG image, or empty for no logo
* @param[in] lines text lines, wrapped when they're too long for the display * @param[in] lines text lines, wrapped when they're too long for the display
*/ */
void show(const std::string& logoPath, const std::vector<std::string>& lines); void show(const BootLogo& logo, const std::vector<std::string>& lines);
/** /**
* Shows a QR code with text lines below it, centered on a black background. The set modules are white. * Shows a QR code with text lines below it, centered on a black background. The set modules are white.

View File

@ -2,49 +2,109 @@
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <TactilityCpp/Memory.h>
#include <TactilityCpp/std/string.h>
#include <TactilityCpp/std/vector.h>
#include <app/paths.h> #include <app/paths.h>
#include <tactility/check.h>
#include <tactility/concurrent/mutex.h>
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
#include <string>
#include <vector>
namespace tt::app::launcher { namespace tt::app::launcher {
/** Favourited app ids, persisted one per line under the data root. */ /**
* Favourited app ids, persisted one per line under the data root.
* The ids are cached in memory: ids(), isFavourite() and toggle() never touch files and can be used from any task.
* load() and save() do file operations, so they must run on the file operations task, see <tactility/file_ops.h>.
*/
class Favourites final { class Favourites final {
struct State {
Mutex mutex;
std::vector<std::string> ids;
/** Toggling before the first load() would save a list without the favourites in the file */
bool loaded = false;
State() { mutex_construct(&mutex); }
~State() { mutex_destruct(&mutex); }
};
static State& state() {
static State* instance = tt::newObject<State>();
check(instance != nullptr);
return *instance;
}
public: public:
std::vector<std::string> load() const { /** Replaces the cached ids with the ones in the file. File operation. */
std::vector<std::string> ids; void load() const {
const std::string path = filePath(); std::vector<std::string> loaded;
const ::std::string path = filePath();
if (!path.empty()) { if (!path.empty()) {
file::readLines(path, /* stripNewLine */ true, [&ids](const char* line) { file::readLines(path, /* stripNewLine */ true, [&loaded](const char* line) {
if (line[0] != '\0') { if (line[0] != '\0') {
ids.emplace_back(line); loaded.emplace_back(line);
} }
}); });
} }
return ids; auto& s = state();
mutex_lock(&s.mutex);
s.ids = ::std::move(loaded);
s.loaded = true;
mutex_unlock(&s.mutex);
} }
/** @return false if the data root is unavailable or the write itself failed - the previous /** @return a copy of the cached ids */
std::vector<std::string> ids() const {
auto& s = state();
mutex_lock(&s.mutex);
std::vector<std::string> result(s.ids.begin(), s.ids.end());
mutex_unlock(&s.mutex);
return result;
}
/** Adds the id to the cached ids when it's not in there yet, otherwise removes it.
* @param[out] result the new cached ids, to be passed to save()
* @return false when load() hasn't completed yet, in which case nothing changed */
bool toggle(const char* id, std::vector<std::string>& result) const {
auto& s = state();
mutex_lock(&s.mutex);
if (!s.loaded) {
mutex_unlock(&s.mutex);
return false;
}
auto it = ::std::ranges::find(s.ids, id);
if (it != s.ids.end()) {
s.ids.erase(it);
} else {
s.ids.emplace_back(id);
}
result.assign(s.ids.begin(), s.ids.end());
mutex_unlock(&s.mutex);
return true;
}
/** Writes the ids to the file. File operation.
* @return false if the data root is unavailable or the write itself failed. The previous
* file, if any, is left untouched either way. */ * file, if any, is left untouched either way. */
bool save(const std::vector<std::string>& ids) const { bool save(const std::vector<std::string>& ids) const {
const std::string path = filePath(); const ::std::string path = filePath();
if (path.empty()) { if (path.empty()) {
return false; return false;
} }
file::findOrCreateParentDirectory(path, 0777); file::findOrCreateParentDirectory(path, 0777);
std::string content; ::std::string content;
for (const auto& id : ids) { for (const auto& id : ids) {
content += id; content += id;
content += '\n'; content += '\n';
} }
// Written to a temp file and renamed into place, so a write that fails partway through // Written to a temp file and renamed into place, so a write that fails partway through
// can't truncate or corrupt the existing file. // can't truncate or corrupt the existing file.
const std::string tempPath = path + ".tmp"; const ::std::string tempPath = path + ".tmp";
if (!file::writeString(tempPath, content)) { if (!file::writeString(tempPath, content)) {
remove(tempPath.c_str()); remove(tempPath.c_str());
return false; return false;
@ -61,25 +121,12 @@ public:
} }
static bool contains(const std::vector<std::string>& ids, const char* id) { static bool contains(const std::vector<std::string>& ids, const char* id) {
return std::ranges::find(ids, id) != ids.end(); return ::std::ranges::find(ids, id) != ids.end();
}
/** @return false if the new list failed to persist (see save()) - the caller should tell the
* user, since the next load() will not reflect this toggle. */
bool toggle(const char* id) const {
auto ids = load();
auto it = std::ranges::find(ids, id);
if (it != ids.end()) {
ids.erase(it);
} else {
ids.emplace_back(id);
}
return save(ids);
} }
private: private:
static std::string filePath() { static ::std::string filePath() {
char path[128]; char path[128];
if (app_paths_get_user_data_path("tactility.launcher", "favourites", path, sizeof(path)) != ERROR_NONE) { if (app_paths_get_user_data_path("tactility.launcher", "favourites", path, sizeof(path)) != ERROR_NONE) {
return ""; return "";

View File

@ -21,6 +21,9 @@ struct LauncherMode {
void (*onKey)(TileGrid& grid, const TileGridItem& item, uint32_t key); void (*onKey)(TileGrid& grid, const TileGridItem& item, uint32_t key);
}; };
/** Reads the favourites from their file into memory. File operation, see <tactility/file_ops.h>. */
void loadFavourites(void* context);
extern const LauncherMode APPS_MODE; extern const LauncherMode APPS_MODE;
extern const LauncherMode SETTINGS_MODE; extern const LauncherMode SETTINGS_MODE;

View File

@ -1,5 +0,0 @@
#pragma once
// Intentionally empty: LocaleSettings.cpp has no external callers (verified via repo-wide
// grep during its thread-per-app conversion), so it no longer exposes a start() wrapper. This
// header is kept as a placeholder in case that changes; nothing currently includes it.

View File

@ -1,19 +0,0 @@
#pragma once
#include <Tactility/i18n/TextResources.h>
// WARNING: This file is auto-generated. Do not edit manually.
namespace tt::app::localesettings::i18n {
enum class Text {
NL_NL = 0,
NL_BE = 1,
EN_US = 2,
EN_GB = 3,
FR_FR = 4,
REGION = 5,
LANGUAGE = 6,
};
}

View File

@ -9,8 +9,24 @@ struct BinFont;
namespace tt::lvgl { namespace tt::lvgl {
/** @return the path of a font file in the system partition */ /** A TrueType font in a file, or in memory when data is set (the memory must stay valid forever) */
std::string getSystemFontPath(const char* fileName); struct TtfSource {
std::string path;
const uint8_t* data = nullptr;
size_t size = 0;
};
/** @return true when the firmware has any fonts, which is not the case when the build had no host compiler */
bool hasGeneratedFonts();
/**
* Finds a font that is part of the firmware, which doesn't use RAM when it's opened with binfont_open_memory() without ownership.
* @param[in] fileName the cache file name, see getCachedFontFileName()
* @param[out] data the font file contents, which are valid forever
* @param[out] size the size of data in bytes
* @return true when the firmware has the font
*/
bool findGeneratedFont(const std::string& fileName, const uint8_t*& data, size_t& size);
/** @return 1 for monochrome displays, otherwise 4 when PSRAM is available, otherwise 2 */ /** @return 1 for monochrome displays, otherwise 4 when PSRAM is available, otherwise 2 */
uint8_t getGeneratedFontBpp(); uint8_t getGeneratedFontBpp();
@ -25,21 +41,21 @@ void deleteStaleCachedFonts(const char* prefix, const std::vector<std::string>&
* Opens a font from the font cache in the data path. When it's not cached, it's rasterized from * Opens a font from the font cache in the data path. When it's not cached, it's rasterized from
* the TTF and written to the cache. Without a data path, the font is only kept in memory. * the TTF and written to the cache. Without a data path, the font is only kept in memory.
* @param[in] fileName the cache file name, see getCachedFontFileName() * @param[in] fileName the cache file name, see getCachedFontFileName()
* @param[in] ttfPath the TrueType font to rasterize from * @param[in] ttf the TrueType font to rasterize from
* @param[in] size font size in pixels * @param[in] size font size in pixels
* @param[in] bpp bits per pixel * @param[in] bpp bits per pixel
* @param[in] getCodepoints provides the codepoints to rasterize, only called when the font is generated. * @param[in] getCodepoints provides the codepoints to rasterize, only called when the font is generated.
* An empty list rasterizes every codepoint in the TTF. * An empty list rasterizes every codepoint in the TTF.
* @return the font, or nullptr when it couldn't be loaded or generated * @return the font, or nullptr when it couldn't be loaded or generated
*/ */
BinFont* loadOrGenerateFont(const std::string& fileName, const std::string& ttfPath, uint16_t size, uint8_t bpp, const std::function<std::vector<uint32_t>()>& getCodepoints); BinFont* loadOrGenerateFont(const std::string& fileName, const TtfSource& ttf, uint16_t size, uint8_t bpp, const std::function<std::vector<uint32_t>()>& getCodepoints);
/** /**
* Generates a font and writes it to the font cache, unless it's cached already. * Generates a font and writes it to the font cache, unless it's cached already.
* Without a data path there is no cache: the font is generated and discarded, to verify that it can be. * Without a data path there is no cache: the font is generated and discarded, to verify that it can be.
* @return false when the font couldn't be generated or written * @return false when the font couldn't be generated or written
*/ */
bool ensureCachedFont(const std::string& fileName, const std::string& ttfPath, uint16_t size, uint8_t bpp, const std::function<std::vector<uint32_t>()>& getCodepoints); bool ensureCachedFont(const std::string& fileName, const TtfSource& ttf, uint16_t size, uint8_t bpp, const std::function<std::vector<uint32_t>()>& getCodepoints);
/** @return a version for a font file that changes when the file is replaced, or 0 when it doesn't exist */ /** @return a version for a font file that changes when the file is replaced, or 0 when it doesn't exist */
uint32_t getFontFileVersion(const std::string& path); uint32_t getFontFileVersion(const std::string& path);

View File

@ -29,6 +29,12 @@ constexpr size_t LARGE_FONT_CHARACTER_COUNT = 1000;
/** @return the number of characters in a TrueType font, or 0 when it can't be read */ /** @return the number of characters in a TrueType font, or 0 when it can't be read */
size_t getTtfCharacterCount(const std::string& path); size_t getTtfCharacterCount(const std::string& path);
/**
* Changing fonts generates them on the device, which needs at least 2 MB of memory in total.
* Otherwise only the fonts in the firmware are used.
*/
bool isFontChangeSupported();
/** @return the configuration from the appearance settings, with the device defaults for unset values */ /** @return the configuration from the appearance settings, with the device defaults for unset values */
FontConfiguration loadFontConfiguration(); FontConfiguration loadFontConfiguration();

View File

@ -52,7 +52,6 @@ private:
// Core HTML endpoints (hardcoded in firmware) // Core HTML endpoints (hardcoded in firmware)
static error_t handleRoot(struct HttpServerRequest* request, void* user_ctx); static error_t handleRoot(struct HttpServerRequest* request, void* user_ctx);
static error_t handleSync(struct HttpServerRequest* request, void* user_ctx);
static error_t handleReboot(struct HttpServerRequest* request, void* user_ctx); static error_t handleReboot(struct HttpServerRequest* request, void* user_ctx);
// File browser endpoints // File browser endpoints

View File

@ -55,7 +55,6 @@ namespace app {
namespace inputdialog { extern const ::AppManifest manifest; } namespace inputdialog { extern const ::AppManifest manifest; }
namespace launcher { extern const ::AppManifest manifest; } namespace launcher { extern const ::AppManifest manifest; }
namespace ledstripsettings { extern const ::AppManifest manifest; } namespace ledstripsettings { extern const ::AppManifest manifest; }
namespace localesettings { extern const ::AppManifest manifest; }
namespace locationsettings { extern const ::AppManifest manifest; } namespace locationsettings { extern const ::AppManifest manifest; }
namespace notes { extern const ::AppManifest manifest; } namespace notes { extern const ::AppManifest manifest; }
namespace power { extern const ::AppManifest manifest; } namespace power { extern const ::AppManifest manifest; }
@ -126,7 +125,6 @@ static void registerInternalApps() {
app_manager_add(&app::imageviewer::manifest); app_manager_add(&app::imageviewer::manifest);
app_manager_add(&app::inputdialog::manifest); app_manager_add(&app::inputdialog::manifest);
app_manager_add(&app::launcher::manifest); app_manager_add(&app::launcher::manifest);
app_manager_add(&app::localesettings::manifest);
app_manager_add(&app::locationsettings::manifest); app_manager_add(&app::locationsettings::manifest);
app_manager_add(&app::notes::manifest); app_manager_add(&app::notes::manifest);
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) { if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
@ -215,7 +213,7 @@ static void registerInstalledAppsFromFileSystems() {
char path[128]; char path[128];
if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true; if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
const auto app_path = std::format("{}/tactility/app", path); const auto app_path = std::format("{}/tactility/app", path);
if (!app_path.starts_with(file::MOUNT_POINT_SYSTEM) && file::isDirectory(app_path)) { if (file::isDirectory(app_path)) {
LOG_I(TAG, "Registering install path %s", app_path.c_str()); LOG_I(TAG, "Registering install path %s", app_path.c_str());
app_manager_install_path_add(app_path.c_str()); app_manager_install_path_add(app_path.c_str());
} }

View File

@ -9,6 +9,7 @@
#include <Tactility/service/espnow/EspNowRadio.h> #include <Tactility/service/espnow/EspNowRadio.h>
#include <Tactility/settings/RadioSettings.h> #include <Tactility/settings/RadioSettings.h>
#include <generated_assets.h>
#include <location/module.h> #include <location/module.h>
#include <app_hub/module.h> #include <app_hub/module.h>
#include <development/module.h> #include <development/module.h>
@ -50,6 +51,8 @@ void registerAndStartServices() {
addService(service::audio::manifest); addService(service::audio::manifest);
} }
check(module_ensure_started(&wifi_module) == ERROR_NONE); check(module_ensure_started(&wifi_module) == ERROR_NONE);
location_configure_certificate(reinterpret_cast<const char*>(asset_certificates_GTS_Root_R4_pem));
app_hub_configure_certificate(reinterpret_cast<const char*>(asset_certificates_GTS_Root_R4_pem));
check(module_ensure_started(&location_module) == ERROR_NONE); check(module_ensure_started(&location_module) == ERROR_NONE);
check(module_ensure_started(&app_hub_module) == ERROR_NONE); check(module_ensure_started(&app_hub_module) == ERROR_NONE);
// Before the mesh service starts, as it needs the region to turn on the radio // Before the mesh service starts, as it needs the region to turn on the radio

View File

@ -1,28 +1,17 @@
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
#include <Tactility/TactilityPrivate.h>
#include <devicetree.h> #include <devicetree.h>
#include <tactility/log.h>
#ifndef ESP_PLATFORM #ifndef ESP_PLATFORM
#include <Simulator.h> #include <Simulator.h>
#endif #endif
constexpr auto* TAG = "Main";
extern "C" { extern "C" {
void app_main() { void app_main() {
if (!tt::init(dts_modules, dts_devices)) { tt::init(dts_modules, dts_devices);
return; // The main task is only there to init. Delete it task afterwards to free up memory.
} vTaskDelete(nullptr);
LOG_I(TAG, "Main dispatcher ready");
const auto dispatcher = tt::getMainDispatcherHandle();
while (true) {
dispatcher_consume(dispatcher);
}
} }
} // extern } // extern

View File

@ -1,39 +1,114 @@
#include <Tactility/CpuAffinity.h>
#include <Tactility/MainDispatcher.h> #include <Tactility/MainDispatcher.h>
#include <Tactility/TactilityPrivate.h> #include <Tactility/TactilityPrivate.h>
#include <TactilityCpp/Memory.h>
#include <tactility/check.h>
#include <tactility/log.h>
// Implements the deprecated MainDispatcher API // Implements the deprecated MainDispatcher API
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations" #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
namespace tt { namespace tt {
static DispatcherHandle_t mainDispatcherHandle = dispatcher_alloc(); constexpr auto* TAG = "MainDispatcher";
namespace { // Internal RAM, so that the functions can access flash
constexpr size_t TASK_STACK_SIZE = 8192;
void mainDispatcherTrampoline(void* context) { struct MainDispatcher::Node {
auto* function = static_cast<MainDispatcher::Function*>(context); Function function;
(*function)(); Node* next = nullptr;
delete function; };
MainDispatcher::MainDispatcher() {
mutex_construct(&mutex);
} }
} // namespace MainDispatcher::Node* MainDispatcher::popOrStop() {
mutex_lock(&mutex);
Node* node = head;
if (node != nullptr) {
head = node->next;
if (head == nullptr) {
tail = nullptr;
}
} else {
running = false;
}
mutex_unlock(&mutex);
return node;
}
bool MainDispatcher::dispatch(Function function, TickType_t timeout) const { int32_t MainDispatcher::taskMain(void* context) {
auto* boxed = new Function(std::move(function)); auto* dispatcher = static_cast<MainDispatcher*>(context);
if (dispatcher_dispatch_timed(handle, boxed, mainDispatcherTrampoline, timeout) != ERROR_NONE) { while (Node* node = dispatcher->popOrStop()) {
delete boxed; node->function();
deleteObject(node);
}
return 0;
}
bool MainDispatcher::startTask() {
if (stale != nullptr) {
thread_join(stale, portMAX_DELAY, 1);
thread_free(stale);
stale = nullptr;
}
::Thread* thread = thread_alloc_full("main_dispatcher", TASK_STACK_SIZE, taskMain, this, getCpuAffinityConfiguration().mainDispatcher);
if (thread == nullptr) {
return false; return false;
} }
running = true;
if (thread_start(thread) != ERROR_NONE) {
running = false;
thread_free(thread);
return false;
}
stale = thread;
return true; return true;
} }
MainDispatcher getMainDispatcher() { bool MainDispatcher::dispatch(Function function) {
return MainDispatcher(mainDispatcherHandle); auto* node = newObject<Node>();
if (node == nullptr) {
return false;
}
node->function = std::move(function);
mutex_lock(&mutex);
if (tail != nullptr) {
tail->next = node;
} else {
head = node;
}
tail = node;
if (!running && !startTask()) {
// The queue was empty before this node, because no task was running
head = nullptr;
tail = nullptr;
mutex_unlock(&mutex);
LOG_E(TAG, "Failed to start the task");
deleteObject(node);
return false;
}
mutex_unlock(&mutex);
return true;
} }
DispatcherHandle_t getMainDispatcherHandle() { MainDispatcher& getMainDispatcher() {
return mainDispatcherHandle; // Allocated once and never freed
static MainDispatcher* instance = newObject<MainDispatcher>();
check(instance != nullptr);
return *instance;
}
bool dispatchOnMainDispatcher(void (*function)(void* context), void* context) {
return getMainDispatcher().dispatch([function, context] { function(context); });
} }
} // namespace } // namespace

View File

@ -18,12 +18,11 @@ std::vector<dirent> getFileSystemDirents() {
if (!file_system_is_mounted(fs)) return true; if (!file_system_is_mounted(fs)) return true;
char path[128]; char path[128];
if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true; if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
// ESP32 mount paths are short names ("/system"); POSIX file systems can return a full // ESP32 mount paths are short names ("/data"); POSIX file systems can return a full
// absolute host path instead, so take the last path component either way. // absolute host path instead, so take the last path component either way.
auto path_str = std::string(path); auto path_str = std::string(path);
auto slash_pos = path_str.find_last_of('/'); auto slash_pos = path_str.find_last_of('/');
auto mount_name = slash_pos == std::string::npos ? path_str : path_str.substr(slash_pos + 1); auto mount_name = slash_pos == std::string::npos ? path_str : path_str.substr(slash_pos + 1);
if (!config::SHOW_SYSTEM_PARTITION && mount_name.starts_with(SYSTEM_PARTITION_NAME)) return true;
auto dir_entry = dirent { auto dir_entry = dirent {
.d_ino = 2, .d_ino = 2,
.d_type = TT_DT_DIR, .d_type = TT_DT_DIR,

View File

@ -179,7 +179,8 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 3072, .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL }, // Requires 3072 is not enough on certain devices
.stack = { .depth = 4096, .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL },
}; };
} }

View File

@ -163,6 +163,10 @@ struct Context {
}; };
lvgl::FontConfiguration toFontConfiguration(const settings::appearance::AppearanceSettings& settings) { lvgl::FontConfiguration toFontConfiguration(const settings::appearance::AppearanceSettings& settings) {
// The font settings aren't shown without enough memory to generate fonts, so saved values are ignored like at boot
if (!lvgl::isFontChangeSupported()) {
return { .defaultSize = static_cast<uint16_t>(TT_FONT_DEFAULT_SIZE), .regularFontPath = "", .monoFontPath = "" };
}
return { return {
.defaultSize = settings.fontSize != 0 ? settings.fontSize : static_cast<uint16_t>(TT_FONT_DEFAULT_SIZE), .defaultSize = settings.fontSize != 0 ? settings.fontSize : static_cast<uint16_t>(TT_FONT_DEFAULT_SIZE),
.regularFontPath = settings.regularFontPath, .regularFontPath = settings.regularFontPath,
@ -286,9 +290,11 @@ void updateWidgets(Context* ctx) {
const bool applying = ctx->applying; const bool applying = ctx->applying;
setHidden(ctx->applyButton, applying || ctx->pendingSettings == ctx->savedSettings); setHidden(ctx->applyButton, applying || ctx->pendingSettings == ctx->savedSettings);
setHidden(ctx->spinner, !applying); setHidden(ctx->spinner, !applying);
if (ctx->errorLabel != nullptr) {
updateFontRow(ctx->regularRow, ctx->pendingSettings.regularFontPath, ctx->regularCharacterCount); updateFontRow(ctx->regularRow, ctx->pendingSettings.regularFontPath, ctx->regularCharacterCount);
updateFontRow(ctx->monoRow, ctx->pendingSettings.monoFontPath, ctx->monoCharacterCount); updateFontRow(ctx->monoRow, ctx->pendingSettings.monoFontPath, ctx->monoCharacterCount);
setHidden(ctx->errorLabel, !ctx->applyFailed); setHidden(ctx->errorLabel, !ctx->applyFailed);
}
updateThemeWidgets(ctx); updateThemeWidgets(ctx);
} }
@ -727,6 +733,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
} }
createSystemBarsCard(content, ctx); createSystemBarsCard(content, ctx);
if (lvgl::isFontChangeSupported()) {
auto* fonts_card = createSection(content, "Fonts"); auto* fonts_card = createSection(content, "Fonts");
auto* font_size_row = createRow(fonts_card); auto* font_size_row = createRow(fonts_card);
auto* font_size_label = lv_label_create(font_size_row); auto* font_size_label = lv_label_create(font_size_row);
@ -764,6 +771,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_label_set_long_mode(ctx->errorLabel, LV_LABEL_LONG_MODE_WRAP); lv_label_set_long_mode(ctx->errorLabel, LV_LABEL_LONG_MODE_WRAP);
lv_label_set_text(ctx->errorLabel, "Failed to create the fonts. Check that the selected files are TrueType fonts."); lv_label_set_text(ctx->errorLabel, "Failed to create the fonts. Check that the selected files are TrueType fonts.");
lv_obj_set_style_text_color(ctx->errorLabel, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT); lv_obj_set_style_text_color(ctx->errorLabel, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT);
}
updateWidgets(ctx); updateWidgets(ctx);

View File

@ -20,6 +20,7 @@
#include <Tactility/MountPoints.h> #include <Tactility/MountPoints.h>
#include <Tactility/TactilityPrivate.h> #include <Tactility/TactilityPrivate.h>
#include <Tactility/app/boot/BootScreen.h> #include <Tactility/app/boot/BootScreen.h>
#include <generated_assets.h>
#include <Tactility/hal/usb/Usb.h> #include <Tactility/hal/usb/Usb.h>
#include <Tactility/hal/usb/UsbHost.h> #include <Tactility/hal/usb/UsbHost.h>
#include <Tactility/lvgl/Fonts.h> #include <Tactility/lvgl/Fonts.h>
@ -29,7 +30,6 @@
#include <Tactility/settings/BootSettings.h> #include <Tactility/settings/BootSettings.h>
#include <Tactility/settings/DisplaySettings.h> #include <Tactility/settings/DisplaySettings.h>
#include <format>
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <Tactility/app/crashdiagnostics/CrashDiagnostics.h> #include <Tactility/app/crashdiagnostics/CrashDiagnostics.h>
@ -46,36 +46,32 @@ constexpr auto* DEFAULT_LAUNCHER_ID = "tactility.launcher";
namespace { namespace {
#ifdef ESP_PLATFORM
constexpr auto PARTITION_PREFIX = std::string("/");
#else
constexpr auto PARTITION_PREFIX = std::string("");
#endif
// Equivalent of AppPaths::getAssetsPath() for the internal "Boot" app id, without needing a
// live AppContext (which this app no longer has under the new app-module model).
std::string getBootAssetsPath(const std::string& childPath) {
return std::format("{}{}/app/Boot/assets/{}", PARTITION_PREFIX, file::SYSTEM_PARTITION_NAME, childPath);
}
// TODO: Replace with automatic asset buckets like on Android // TODO: Replace with automatic asset buckets like on Android
std::string getSizedAssetPath(const BootScreen& screen, const char* small, const char* medium, const char* large) { BootLogo getSizedLogo(const BootScreen& screen, const BootLogo& small, const BootLogo& medium, const BootLogo& large) {
const int smallest_dimension = screen.getSmallestDimension(); const int smallest_dimension = screen.getSmallestDimension();
if (smallest_dimension < 150) { // e.g. Cardputer if (smallest_dimension < 150) { // e.g. Cardputer
return getBootAssetsPath(small); return small;
} else if (smallest_dimension >= 320) { } else if (smallest_dimension >= 320) {
return getBootAssetsPath(large); return large;
} else { } else {
return getBootAssetsPath(medium); return medium;
} }
} }
std::string getLogoPath(const BootScreen& screen) { BootLogo getLogo(const BootScreen& screen) {
return getSizedAssetPath(screen, "logo_small.png", "logo.png", "logo_large.png"); return getSizedLogo(screen,
{ asset_app_Boot_assets_logo_small_png, asset_app_Boot_assets_logo_small_png_SIZE },
{ asset_app_Boot_assets_logo_png, asset_app_Boot_assets_logo_png_SIZE },
{ asset_app_Boot_assets_logo_large_png, asset_app_Boot_assets_logo_large_png_SIZE }
);
} }
std::string getUsbLogoPath(const BootScreen& screen) { BootLogo getUsbLogo(const BootScreen& screen) {
return getSizedAssetPath(screen, "logo_usb_small.png", "logo_usb.png", "logo_usb_large.png"); return getSizedLogo(screen,
{ asset_app_Boot_assets_logo_usb_small_png, asset_app_Boot_assets_logo_usb_small_png_SIZE },
{ asset_app_Boot_assets_logo_usb_png, asset_app_Boot_assets_logo_usb_png_SIZE },
{ asset_app_Boot_assets_logo_usb_large_png, asset_app_Boot_assets_logo_usb_large_png_SIZE }
);
} }
void setupDisplay() { void setupDisplay() {
@ -207,7 +203,7 @@ bool bootInit(TickType_t startTime) {
BootScreen screen; BootScreen screen;
if (screen.begin()) { if (screen.begin()) {
screen.show(is_usb_boot ? getUsbLogoPath(screen) : getLogoPath(screen), {}); screen.show(is_usb_boot ? getUsbLogo(screen) : getLogo(screen), {});
} }
LOG_I(TAG, "Init display"); LOG_I(TAG, "Init display");
@ -249,14 +245,14 @@ bool bootInit(TickType_t startTime) {
break; break;
case UsbBootResult::Started: case UsbBootResult::Started:
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
screen.show(getUsbLogoPath(screen), { getInputPrompt("return to OS") }); screen.show(getUsbLogo(screen), { getInputPrompt("return to OS") });
waitForInput(); waitForInput();
hal::usb::stop(); hal::usb::stop();
esp_restart(); esp_restart();
#endif #endif
return false; return false;
case UsbBootResult::Failed: case UsbBootResult::Failed:
screen.show("", { "Failed to start USB mass storage:", hal::usb::getLastError(), getInputPrompt("reboot") }); screen.show({}, { "Failed to start USB mass storage:", hal::usb::getLastError(), getInputPrompt("reboot") });
waitForInputAndReboot(); waitForInputAndReboot();
return false; return false;
} }
@ -264,7 +260,7 @@ bool bootInit(TickType_t startTime) {
registerApps(); registerApps();
if (sd_card_missing) { if (sd_card_missing) {
screen.show("", { "SD card not found.", "Please insert one and reboot.", getInputPrompt("reboot") }); screen.show({}, { "SD card not found.", "Please insert one and reboot.", getInputPrompt("reboot") });
waitForInputAndReboot(); waitForInputAndReboot();
return false; return false;
} }

View File

@ -137,16 +137,16 @@ int BootScreen::getSmallestDimension() const {
return std::min(panelWidth, panelHeight); return std::min(panelWidth, panelHeight);
} }
void BootScreen::show(const std::string& logoPath, const std::vector<std::string>& lines) { void BootScreen::show(const BootLogo& bootLogo, const std::vector<std::string>& lines) {
if (target == nullptr) { if (target == nullptr) {
return; return;
} }
Logo logo; Logo logo;
if (!logoPath.empty()) { if (bootLogo.data != nullptr) {
const unsigned error = lodepng_decode32_file(&logo.rgba, &logo.width, &logo.height, logoPath.c_str()); const unsigned error = lodepng_decode32(&logo.rgba, &logo.width, &logo.height, bootLogo.data, bootLogo.size);
if (error != 0) { if (error != 0) {
LOG_E(TAG, "Failed to load %s: %s", logoPath.c_str(), lodepng_error_text(error)); LOG_E(TAG, "Failed to decode logo: %s", lodepng_error_text(error));
lodepng_free(logo.rgba); lodepng_free(logo.rgba);
logo.rgba = nullptr; logo.rgba = nullptr;
logo.width = 0; logo.width = 0;

View File

@ -122,7 +122,7 @@ void showCrashScreen(boot::BootScreen& screen) {
lines.push_back(std::string("Only the cause is known: ") + crashCauseToString(crash_data.cause)); lines.push_back(std::string("Only the cause is known: ") + crashCauseToString(crash_data.cause));
} }
lines.push_back(prompt); lines.push_back(prompt);
screen.show("", lines); screen.show({}, lines);
waitForInputOrForever(has_input); waitForInputOrForever(has_input);
return; return;
} }
@ -154,7 +154,7 @@ void showCrashScreen(boot::BootScreen& screen) {
return qrcode_getModule(&qr_code, static_cast<uint8_t>(x), static_cast<uint8_t>(y)); return qrcode_getModule(&qr_code, static_cast<uint8_t>(x), static_cast<uint8_t>(y));
}, lines); }, lines);
} else { } else {
screen.show("", lines); screen.show({}, lines);
} }
waitForInputOrForever(has_input); waitForInputOrForever(has_input);

View File

@ -15,6 +15,9 @@
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <lvgl.h> #include <lvgl.h>
#include <TactilityCpp/std/vector.h>
#include <cstdio>
#include <string> #include <string>
namespace tt::app::imageviewer { namespace tt::app::imageviewer {
@ -27,9 +30,35 @@ namespace {
struct Context { struct Context {
uint32_t appInstanceId; uint32_t appInstanceId;
std::string filePath; ::std::string filePath;
// File contents are read in the app task, never on the LVGL task
tt::std::vector<uint8_t> fileData;
lv_image_dsc_t imageDsc {};
}; };
void loadFile(Context& ctx) {
FILE* file = fopen(ctx.filePath.c_str(), "rb");
if (file == nullptr) return;
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
if (size > 0) {
ctx.fileData.resize(static_cast<size_t>(size));
if (fread(ctx.fileData.data(), 1, ctx.fileData.size(), file) != ctx.fileData.size()) {
ctx.fileData.clear();
}
}
fclose(file);
if (!ctx.fileData.empty()) {
ctx.imageDsc.header.magic = LV_IMAGE_HEADER_MAGIC;
ctx.imageDsc.header.cf = LV_COLOR_FORMAT_RAW;
ctx.imageDsc.data_size = ctx.fileData.size();
ctx.imageDsc.data = ctx.fileData.data();
}
}
void onBackPressed(lv_event_t* event) { void onBackPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event)); auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
@ -68,10 +97,9 @@ void createWidgets(lv_obj_t* parent, void* userData) {
auto* file_label = lv_label_create(wrapper); auto* file_label = lv_label_create(wrapper);
lv_obj_align_to(file_label, wrapper, LV_ALIGN_BOTTOM_LEFT, 0, 0); lv_obj_align_to(file_label, wrapper, LV_ALIGN_BOTTOM_LEFT, 0, 0);
if (!ctx->filePath.empty()) { if (!ctx->fileData.empty()) {
std::string prefixed_path = lvgl::PATH_PREFIX + ctx->filePath; LOG_I(TAG, "Opening %s", ctx->filePath.c_str());
LOG_I(TAG, "Opening %s", prefixed_path.c_str()); lv_image_set_src(image, &ctx->imageDsc);
lv_img_set_src(image, prefixed_path.c_str());
auto path = string::getLastPathSegment(ctx->filePath); auto path = string::getLastPathSegment(ctx->filePath);
lv_label_set_text(file_label, path.c_str()); lv_label_set_text(file_label, path.c_str());
} else { } else {
@ -86,6 +114,7 @@ int32_t appMain(int argc, char* argv[]) {
Context ctx {}; Context ctx {};
ctx.appInstanceId = appInstanceId; ctx.appInstanceId = appInstanceId;
ctx.filePath = argv[0]; ctx.filePath = argv[0];
loadFile(ctx);
TaskEventGroup event_group {}; TaskEventGroup event_group {};
task_event_group_construct(&event_group); task_event_group_construct(&event_group);
@ -121,7 +150,7 @@ int32_t appMain(int argc, char* argv[]) {
} // namespace } // namespace
void start(const std::string& file) { void start(const ::std::string& file) {
const char* argv[] = { file.c_str() }; const char* argv[] = { file.c_str() };
uint32_t instanceId = 0; uint32_t instanceId = 0;
AppStartContext context = app_start_context_for_manifest(&manifest); AppStartContext context = app_start_context_for_manifest(&manifest);

View File

@ -6,10 +6,12 @@
#include <app/package_manifest.h> #include <app/package_manifest.h>
#include <lvgl/icons/shared.h> #include <lvgl/icons/shared.h>
#include <tactility/file_ops.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <new>
#include <string> #include <string>
namespace tt::app::launcher { namespace tt::app::launcher {
@ -20,6 +22,19 @@ constexpr auto* TAG = "Launcher";
const Favourites favourites; const Favourites favourites;
struct SaveFavouritesJob {
std::vector<std::string> ids;
};
// File operation: runs on the file operations task, which frees the job
void saveFavourites(void* context) {
auto* job = static_cast<SaveFavouritesJob*>(context);
if (!favourites.save(job->ids)) {
LOG_E(TAG, "Failed to save favourites");
}
delete job;
}
struct IconEntry { struct IconEntry {
const char* id; const char* id;
const char* icon; const char* icon;
@ -77,14 +92,14 @@ void collectIncompatibleAppIds(const AppPackage* package, void* context) {
} }
std::vector<TileGridItem> collectItems() { std::vector<TileGridItem> collectItems() {
const std::vector<std::string> favouriteIds = favourites.load(); const std::vector<std::string> favouriteIds = favourites.ids();
std::vector<::AppManifest> collected; std::vector<::AppManifest> collected;
app_manager_for_each_manifest(collectManifest, &collected); app_manager_for_each_manifest(collectManifest, &collected);
std::vector<std::string> incompatibleIds; std::vector<std::string> incompatibleIds;
app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds); app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds);
std::erase_if(collected, [&](const ::AppManifest& manifest) { ::std::erase_if(collected, [&](const ::AppManifest& manifest) {
return !isShown(manifest) || std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end(); return !isShown(manifest) || ::std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end();
}); });
std::vector<TileGridItem> items; std::vector<TileGridItem> items;
@ -92,7 +107,7 @@ std::vector<TileGridItem> collectItems() {
for (const auto& manifest : collected) { for (const auto& manifest : collected) {
items.push_back({ manifest.id, manifest.name, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) }); items.push_back({ manifest.id, manifest.name, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) });
} }
std::ranges::sort(items, [](const TileGridItem& a, const TileGridItem& b) { ::std::ranges::sort(items, [](const TileGridItem& a, const TileGridItem& b) {
if (a.highlighted != b.highlighted) { if (a.highlighted != b.highlighted) {
return a.highlighted; return a.highlighted;
} }
@ -102,8 +117,20 @@ std::vector<TileGridItem> collectItems() {
} }
void toggleFavourite(TileGrid& grid, const char* appId, bool keepSelection) { void toggleFavourite(TileGrid& grid, const char* appId, bool keepSelection) {
if (!favourites.toggle(appId)) { auto* job = new (::std::nothrow) SaveFavouritesJob();
LOG_E(TAG, "Failed to save favourites"); if (job == nullptr) {
LOG_E(TAG, "Failed to queue saving favourites");
return;
}
if (!favourites.toggle(appId, job->ids)) {
// The favourites are still being loaded, so toggling would overwrite the file with an incomplete list
LOG_W(TAG, "Favourites aren't loaded yet");
delete job;
return;
}
if (file_ops_queue(saveFavourites, job, nullptr) != ERROR_NONE) {
LOG_E(TAG, "Failed to queue saving favourites");
delete job;
} }
grid.requestRepopulate(keepSelection); grid.requestRepopulate(keepSelection);
} }
@ -120,6 +147,10 @@ void onKey(TileGrid& grid, const TileGridItem& item, uint32_t key) {
} // namespace } // namespace
void loadFavourites(void*) {
favourites.load();
}
const LauncherMode APPS_MODE = { const LauncherMode APPS_MODE = {
.buttonIcon = LVGL_ICON_SHARED_APPS, .buttonIcon = LVGL_ICON_SHARED_APPS,
.iconColor = TileGrid::IconColor::Primary, .iconColor = TileGrid::IconColor::Primary,

View File

@ -5,13 +5,17 @@
#include <app/start.h> #include <app/start.h>
#include <cstring> #include <cstring>
#include <new>
#include <string>
#include <vector> #include <vector>
#include <lvgl/lvgl.h>
#include <lvgl/theme.h> #include <lvgl/theme.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/file_ops.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/memory.h> #include <tactility/memory.h>
@ -35,7 +39,7 @@ void onAppKey(const TileGridItem& item, uint32_t key, void* userData);
enum class Mode { enum class Mode {
Apps, Apps,
Settings Settings,
}; };
const LauncherMode& getLauncherMode(Mode mode) { const LauncherMode& getLauncherMode(Mode mode) {
@ -57,7 +61,7 @@ struct Context {
.onKey = onAppKey, .onKey = onAppKey,
.resolveIcon = nullptr, .resolveIcon = nullptr,
.onPageChanged = nullptr, .onPageChanged = nullptr,
.userData = this .userData = this,
} }; } };
}; };
@ -65,16 +69,27 @@ std::vector<TileGridItem> collectItems(void* userData) {
return getLauncherMode(static_cast<Context*>(userData)->mode).collect(); return getLauncherMode(static_cast<Context*>(userData)->mode).collect();
} }
void startApp(const char* appId) { struct StartAppJob {
std::string appId;
};
// Starting an app can load it from a file, so it runs on the file operations task, which frees the job
void startApp(void* context) {
auto* job = static_cast<StartAppJob*>(context);
uint32_t instance_id = 0; uint32_t instance_id = 0;
AppStartContext context; AppStartContext start_context;
if (app_start_context_from_id(appId, &context) == ERROR_NONE) { if (app_start_context_from_id(job->appId.c_str(), &start_context) == ERROR_NONE) {
app_start_with_context(&context, &instance_id); app_start_with_context(&start_context, &instance_id);
} }
delete job;
} }
void onAppClicked(const TileGridItem& item, void*) { void onAppClicked(const TileGridItem& item, void*) {
startApp(item.id.c_str()); auto* job = new (::std::nothrow) StartAppJob { item.id };
if (job == nullptr || file_ops_queue(startApp, job, nullptr) != ERROR_NONE) {
LOG_E(TAG, "Failed to queue starting %s", item.id.c_str());
delete job;
}
} }
void onAppLongPressed(const TileGridItem& item, void* userData) { void onAppLongPressed(const TileGridItem& item, void* userData) {
@ -135,7 +150,8 @@ void destroyWidgets(void* userData) {
ctx->grid.destroyWidgets(); ctx->grid.destroyWidgets();
} }
void runAutoStart() { // Reads settings files, so it runs on the file operations task
void runAutoStart(void*) {
settings::BootSettings boot_properties; settings::BootSettings boot_properties;
AppStartContext context; AppStartContext context;
if ( if (
@ -172,16 +188,28 @@ int32_t appMain(int argc, char* argv[]) {
AppEventSubscription sub {}; AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
FileOpsSubscription file_ops_sub {};
check(file_ops_subscribe(&file_ops_sub, &event_group) == ERROR_NONE);
Context ctx; Context ctx;
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
runAutoStart(); // The file operations run in order: the favourites are available when auto-start runs
check(file_ops_queue(loadFavourites, nullptr, &file_ops_sub) == ERROR_NONE);
check(file_ops_queue(runAutoStart, nullptr, nullptr) == ERROR_NONE);
// The launcher is meant to stay resident (it's the home screen) - it only gives up its // The launcher is meant to stay resident (it's the home screen) - it only gives up its
// thread when app-module's scheduler asks it to (e.g. another new-model app is started). // thread when app-module's scheduler asks it to (e.g. another new-model app is started).
while (true) { while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
if (file_ops_poll(&file_ops_sub) == ERROR_NONE) {
// The favourites were loaded
lvgl_lock();
ctx.grid.requestRepopulate(false);
lvgl_unlock();
}
bool shouldClose = false; bool shouldClose = false;
AppEvent event {}; AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) { while (app_event_poll(&sub, &event) == ERROR_NONE) {
@ -194,6 +222,7 @@ int32_t appMain(int argc, char* argv[]) {
} }
window_manager_remove(window); window_manager_remove(window);
file_ops_unsubscribe(&file_ops_sub);
check(app_event_unsubscribe(&sub) == ERROR_NONE); check(app_event_unsubscribe(&sub) == ERROR_NONE);
task_event_group_destruct(&event_group); task_event_group_destruct(&event_group);
return 0; return 0;
@ -207,7 +236,8 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 5120, .desired_memory_capability = 0 }, // All file operations run on the file operations task, so the stack can be in external RAM
.stack = { .depth = 3072, .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL },
}; };
} // namespace } // namespace

View File

@ -26,7 +26,6 @@ constexpr IconEntry ICONS[] = {
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE}, {"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT}, {"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP}, {"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
{"tactility.locationsettings", LVGL_ICON_SHARED_LOCATION_ON}, {"tactility.locationsettings", LVGL_ICON_SHARED_LOCATION_ON},
{"tactility.meshsettings", LVGL_ICON_SHARED_HUB}, {"tactility.meshsettings", LVGL_ICON_SHARED_HUB},
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW}, {"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},

View File

@ -1,256 +0,0 @@
#include <Tactility/Tactility.h>
#include <Tactility/RecursiveMutex.h>
#include <Tactility/app/localesettings/TextResources.h>
#include <Tactility/app/selectiondialog/SelectionDialog.h>
#include <Tactility/settings/Language.h>
#include <Tactility/settings/SystemSettings.h>
#include <app/event.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <tactility/concurrent/task_event_group.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/toolbar.h>
#include <lvgl.h>
#include <lvgl/fonts.h>
#include <lvgl/icons/shared.h>
#include <lvgl/lvgl.h>
#include <map>
#include <string>
#include <vector>
namespace tt::app::localesettings {
constexpr auto* TAG = "LocaleSettings";
#ifdef ESP_PLATFORM
constexpr auto* TEXT_RESOURCE_PATH = "/system/app/LocaleSettings/i18n";
#else
constexpr auto* TEXT_RESOURCE_PATH = "system/app/LocaleSettings/i18n";
#endif
extern const ::AppManifest manifest;
namespace {
struct Context {
uint32_t appInstanceId;
tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH);
RecursiveMutex mutex;
TaskEventGroup* eventGroup = nullptr;
uint32_t selectLanguageBit = 0;
uint32_t languageSelectLaunchId = 0;
// Valid while the window is shown
lv_obj_t* languageLabel = nullptr;
lv_obj_t* languageValueLabel = nullptr;
bool settingsUpdated = false;
std::map<settings::Language, std::string> languageMap;
};
std::vector<std::string> getLanguageNames(Context* ctx) {
std::vector<std::string> items;
for (int i = 0; i < static_cast<int>(settings::Language::count); i++) {
switch (static_cast<settings::Language>(i)) {
case settings::Language::en_GB:
items.push_back(ctx->textResources[i18n::Text::EN_GB]);
break;
case settings::Language::en_US:
items.push_back(ctx->textResources[i18n::Text::EN_US]);
break;
case settings::Language::fr_FR:
items.push_back(ctx->textResources[i18n::Text::FR_FR]);
break;
case settings::Language::nl_BE:
items.push_back(ctx->textResources[i18n::Text::NL_BE]);
break;
case settings::Language::nl_NL:
items.push_back(ctx->textResources[i18n::Text::NL_NL]);
break;
case settings::Language::count:
break;
}
}
return items;
}
/** Requires the LVGL lock */
void updateViews(Context* ctx) {
if (ctx->languageLabel == nullptr) {
return;
}
lv_label_set_text(ctx->languageLabel, ctx->textResources[i18n::Text::LANGUAGE].c_str());
const auto names = getLanguageNames(ctx);
const auto index = static_cast<size_t>(settings::getLanguage());
lv_label_set_text(ctx->languageValueLabel, index < names.size() ? names[index].c_str() : "");
}
void onSelectLanguagePressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
task_event_group_signal(ctx->eventGroup, ctx->selectLanguageBit);
}
void startLanguageSelection(Context* ctx) {
if (ctx->languageSelectLaunchId != 0) {
return;
}
ctx->languageSelectLaunchId = selectiondialog::start(ctx->appInstanceId, ctx->textResources[i18n::Text::LANGUAGE], getLanguageNames(ctx));
}
void onLanguageSelectionResult(Context* ctx, const AppEvent& event) {
ctx->languageSelectLaunchId = 0;
// Other results mean that the dialog was dismissed
const int32_t selected = event.result.result;
if (selected >= 0 && selected < static_cast<int32_t>(settings::Language::count)) {
settings::setLanguage(static_cast<settings::Language>(selected));
ctx->textResources.load();
lvgl_lock();
updateViews(ctx);
lvgl_unlock();
}
app_manager_stop(event.result.launch_id);
}
// Preserved from the pre-conversion code as-is: declared but never wired to any widget there
// either, so this has always been dead code (kept verbatim rather than dropped, since removing
// it would be a functional judgment call outside the scope of this lifecycle-only conversion).
[[maybe_unused]] void onRegionChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
ctx->settingsUpdated = true;
}
void onBackPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
app_event_emit_close(ctx->appInstanceId);
}
void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->textResources.load();
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl_toolbar_create(parent, "Locale");
// The global toolbar nav callback only knows how to stop old-model apps.
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
auto* main_wrapper = lv_obj_create(parent);
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1);
// "Language English [Select]"
auto* card = lvgl_card_create(main_wrapper);
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
// Transparent, so the card provides the background
auto* language_row = lv_obj_create(card);
lv_obj_set_size(language_row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(language_row, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(language_row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(language_row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(language_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(language_row, 0, LV_STATE_DEFAULT);
lv_obj_set_scrollable(language_row, false);
// The title with the language below it, which take the width that the button leaves. Texts that don't fit scroll.
auto* texts = lv_obj_create(language_row);
lv_obj_set_height(texts, LV_SIZE_CONTENT);
lv_obj_set_flex_grow(texts, 1);
lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(texts, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(texts, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(texts, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_scrollable(texts, false);
ctx->languageLabel = lv_label_create(texts);
lv_obj_set_width(ctx->languageLabel, LV_PCT(100));
lv_label_set_long_mode(ctx->languageLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
ctx->languageValueLabel = lv_label_create(texts);
lv_obj_set_width(ctx->languageValueLabel, LV_PCT(100));
lv_label_set_long_mode(ctx->languageValueLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
lv_obj_set_style_text_font(ctx->languageValueLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
auto* select_button = lv_button_create(language_row);
auto* select_icon = lv_label_create(select_button);
lv_obj_set_style_text_font(select_icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
lv_label_set_text(select_icon, LVGL_ICON_SHARED_LIST_ALT);
lv_obj_add_event_cb(select_button, onSelectLanguagePressed, LV_EVENT_SHORT_CLICKED, ctx);
updateViews(ctx);
}
void destroyWidgets(void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->languageLabel = nullptr;
ctx->languageValueLabel = nullptr;
}
int32_t appMain(int argc, char* argv[]) {
uint32_t appInstanceId = app_scheduler_current_app_id();
Context ctx;
ctx.appInstanceId = appInstanceId;
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
ctx.eventGroup = &event_group;
check(task_event_group_claim_bit(&event_group, &ctx.selectLanguageBit) == ERROR_NONE);
AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
uint32_t flags = 0;
task_event_group_wait_any(&event_group, &flags, portMAX_DELAY);
if (flags & ctx.selectLanguageBit) {
startLanguageSelection(&ctx);
}
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
} else if (event.type == APP_EVENT_RESULT && event.result.launch_id == ctx.languageSelectLaunchId) {
onLanguageSelectionResult(&ctx, event);
}
}
}
window_manager_remove(window);
check(app_event_unsubscribe(&sub) == ERROR_NONE);
task_event_group_destruct(&event_group);
return 0;
}
} // namespace
extern const ::AppManifest manifest = {
.id = "tactility.localesettings",
.name = "Locale",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
.flags = 0,
.stack = {}
};
} // namespace tt::app::localesettings

View File

@ -82,7 +82,7 @@ struct Context {
// Valid while the window is shown // Valid while the window is shown
lv_obj_t* serviceLabel = nullptr; lv_obj_t* serviceLabel = nullptr;
lv_obj_t* nameLabel = nullptr; lv_obj_t* nameLabel = nullptr;
lv_obj_t* radioRow = nullptr; lv_obj_t* radioButton = nullptr;
lv_obj_t* radioLabel = nullptr; lv_obj_t* radioLabel = nullptr;
lv_obj_t* regionLabel = nullptr; lv_obj_t* regionLabel = nullptr;
lv_obj_t* presetRow = nullptr; lv_obj_t* presetRow = nullptr;
@ -121,7 +121,12 @@ void updateLabels(Context* ctx) {
} }
lv_label_set_text(ctx->serviceLabel, ctx->serviceText.c_str()); lv_label_set_text(ctx->serviceLabel, ctx->serviceText.c_str());
lv_label_set_text(ctx->nameLabel, ctx->nameText.c_str()); lv_label_set_text(ctx->nameLabel, ctx->nameText.c_str());
lv_obj_set_hidden(ctx->radioRow, !ctx->radioChoice); // The radio is always shown, but it can only be changed when there are several
if (ctx->radioChoice) {
lv_obj_remove_state(ctx->radioButton, LV_STATE_DISABLED);
} else {
lv_obj_add_state(ctx->radioButton, LV_STATE_DISABLED);
}
lv_label_set_text(ctx->radioLabel, ctx->radioText.c_str()); lv_label_set_text(ctx->radioLabel, ctx->radioText.c_str());
lv_label_set_text(ctx->regionLabel, ctx->regionText.c_str()); lv_label_set_text(ctx->regionLabel, ctx->regionText.c_str());
lv_obj_set_hidden(ctx->presetRow, !ctx->hasPresets); lv_obj_set_hidden(ctx->presetRow, !ctx->hasPresets);
@ -175,7 +180,7 @@ void loadState(Context* ctx) {
char name[MESH_NAME_MAX_LENGTH + 1]; char name[MESH_NAME_MAX_LENGTH + 1];
mesh_get_node_name(name, sizeof(name)); mesh_get_node_name(name, sizeof(name));
ctx->nameText = name; ctx->nameText = name;
// A choice only matters when there is more than one radio // A choice is only possible when there is more than one radio
char radio[MESH_NAME_MAX_LENGTH + 1] = "-"; char radio[MESH_NAME_MAX_LENGTH + 1] = "-";
mesh_get_radio(radio, sizeof(radio)); mesh_get_radio(radio, sizeof(radio));
std::vector<std::string> radioNames; std::vector<std::string> radioNames;
@ -415,12 +420,13 @@ lv_obj_t* createTitleAndValue(lv_obj_t* row, const char* title) {
return valueLabel; return valueLabel;
} }
void createChangeButton(lv_obj_t* row, lv_event_cb_t callback, Context* ctx) { lv_obj_t* createChangeButton(lv_obj_t* row, lv_event_cb_t callback, Context* ctx) {
auto* button = lv_button_create(row); auto* button = lv_button_create(row);
auto* icon = lv_label_create(button); auto* icon = lv_label_create(button);
lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT); lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
lv_label_set_text(icon, LVGL_ICON_SHARED_LIST_ALT); lv_label_set_text(icon, LVGL_ICON_SHARED_LIST_ALT);
lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, ctx); lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, ctx);
return button;
} }
void createWidgets(lv_obj_t* parent, void* userData) { void createWidgets(lv_obj_t* parent, void* userData) {
@ -450,9 +456,9 @@ void createWidgets(lv_obj_t* parent, void* userData) {
ctx->nameLabel = createTitleAndValue(nameRow, "Name"); ctx->nameLabel = createTitleAndValue(nameRow, "Name");
createChangeButton(nameRow, onChangeNamePressed, ctx); createChangeButton(nameRow, onChangeNamePressed, ctx);
ctx->radioRow = createRow(card); auto* radioRow = createRow(card);
ctx->radioLabel = createTitleAndValue(ctx->radioRow, "Radio"); ctx->radioLabel = createTitleAndValue(radioRow, "Radio");
createChangeButton(ctx->radioRow, onSelectRadioPressed, ctx); ctx->radioButton = createChangeButton(radioRow, onSelectRadioPressed, ctx);
auto* regionRow = createRow(card); auto* regionRow = createRow(card);
ctx->regionLabel = createTitleAndValue(regionRow, "Region"); ctx->regionLabel = createTitleAndValue(regionRow, "Region");
@ -478,7 +484,7 @@ void destroyWidgets(void* userData) {
auto* ctx = static_cast<Context*>(userData); auto* ctx = static_cast<Context*>(userData);
ctx->serviceLabel = nullptr; ctx->serviceLabel = nullptr;
ctx->nameLabel = nullptr; ctx->nameLabel = nullptr;
ctx->radioRow = nullptr; ctx->radioButton = nullptr;
ctx->radioLabel = nullptr; ctx->radioLabel = nullptr;
ctx->regionLabel = nullptr; ctx->regionLabel = nullptr;
ctx->presetRow = nullptr; ctx->presetRow = nullptr;

View File

@ -24,8 +24,11 @@
#include <lvgl/widgets/icon_button.h> #include <lvgl/widgets/icon_button.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/file_ops.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <new>
namespace tt::app::notes { namespace tt::app::notes {
constexpr auto* TAG = "Notes"; constexpr auto* TAG = "Notes";
@ -85,6 +88,22 @@ bool saveFile(Context* ctx, const std::string& path) {
return result; return result;
} }
struct SaveJob {
std::string path;
std::string content;
};
/** File operation: it runs on the file operations task, which frees the job */
void saveInBackground(void* context) {
auto* job = static_cast<SaveJob*>(context);
if (file::writeString(job->path, job->content.c_str())) {
LOG_I(TAG, "Saved to %s", job->path.c_str());
} else {
LOG_E(TAG, "Failed to save to %s", job->path.c_str());
}
delete job;
}
void hideOverlay(Context* ctx) { void hideOverlay(Context* ctx) {
if (lv_obj_is_hidden(ctx->uiOverlay)) { if (lv_obj_is_hidden(ctx->uiOverlay)) {
return; return;
@ -108,8 +127,12 @@ void onNewPressed(lv_event_t* e) {
void onSavePressed(lv_event_t* e) { void onSavePressed(lv_event_t* e) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e)); auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
hideOverlay(ctx); hideOverlay(ctx);
ctx->saveBuffer = lv_textarea_get_text(ctx->uiNoteText); // Runs on the LVGL task, which must not do file operations
saveFile(ctx, ctx->filePath); auto* job = new (::std::nothrow) SaveJob { ctx->filePath, lv_textarea_get_text(ctx->uiNoteText) };
if (job == nullptr || file_ops_queue(saveInBackground, job, nullptr) != ERROR_NONE) {
LOG_E(TAG, "Failed to queue saving %s", ctx->filePath.c_str());
delete job;
}
} }
void onSaveAsPressed(lv_event_t* e) { void onSaveAsPressed(lv_event_t* e) {

View File

@ -294,7 +294,6 @@ struct Context {
MemoryBarWidgets externalMemBar; MemoryBarWidgets externalMemBar;
MemoryBarWidgets dataStorageBar; MemoryBarWidgets dataStorageBar;
MemoryBarWidgets sdcardStorageBar; MemoryBarWidgets sdcardStorageBar;
MemoryBarWidgets systemStorageBar;
lv_obj_t* tasksContainer = nullptr; lv_obj_t* tasksContainer = nullptr;
/** The task name of each row in tasksContainer */ /** The task name of each row in tasksContainer */
@ -303,7 +302,6 @@ struct Context {
bool hasExternalMem = false; bool hasExternalMem = false;
bool hasDataStorage = false; bool hasDataStorage = false;
bool hasSystemStorage = false;
}; };
@ -331,11 +329,6 @@ void updateStorage(Context* ctx) {
updateMemoryBar(ctx->sdcardStorageBar, storage_free, storage_total); updateMemoryBar(ctx->sdcardStorageBar, storage_free, storage_total);
} }
if (ctx->hasSystemStorage) {
if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) {
updateMemoryBar(ctx->systemStorageBar, storage_free, storage_total);
}
}
#endif #endif
} }
@ -428,13 +421,6 @@ void createWidgets(lv_obj_t* parent, void* userData) {
if (findFirstMountedSdCardPath(sdcard_path) && esp_vfs_fat_info(sdcard_path.c_str(), &storage_total, &storage_free) == ESP_OK) { if (findFirstMountedSdCardPath(sdcard_path) && esp_vfs_fat_info(sdcard_path.c_str(), &storage_total, &storage_free) == ESP_OK) {
ctx->sdcardStorageBar = createMemoryBar(storage_tab, sdcard_path.c_str()); ctx->sdcardStorageBar = createMemoryBar(storage_tab, sdcard_path.c_str());
} }
if (config::SHOW_SYSTEM_PARTITION) {
ctx->hasSystemStorage = (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK);
if (ctx->hasSystemStorage) {
ctx->systemStorageBar = createMemoryBar(storage_tab, file::MOUNT_POINT_SYSTEM);
}
}
#endif #endif
#if configUSE_TRACE_FACILITY #if configUSE_TRACE_FACILITY

View File

@ -1,6 +1,5 @@
#include <Tactility/app/timezone/TimeZoneView.h> #include <Tactility/app/timezone/TimeZoneView.h>
#include <Tactility/MountPoints.h>
#include <Tactility/StringUtils.h> #include <Tactility/StringUtils.h>
#include <location/location.h> #include <location/location.h>
@ -15,7 +14,9 @@
#include <lvgl/widgets/list.h> #include <lvgl/widgets/list.h>
#include <lvgl/widgets/spinner.h> #include <lvgl/widgets/spinner.h>
#include <cstdio> #include <generated_assets.h>
#include <string_view>
namespace tt::app::timezone { namespace tt::app::timezone {
@ -27,10 +28,6 @@ namespace {
constexpr size_t MAX_ENTRIES = 50; constexpr size_t MAX_ENTRIES = 50;
constexpr TickType_t FILTER_DELAY = pdMS_TO_TICKS(500); constexpr TickType_t FILTER_DELAY = pdMS_TO_TICKS(500);
std::string getTimeZonesPath() {
return std::string(file::MOUNT_POINT_SYSTEM) + "/timezones.csv";
}
/** Parses a line like "Europe/Amsterdam","CET-1CEST,M3.5.0,M10.5.0/3" */ /** Parses a line like "Europe/Amsterdam","CET-1CEST,M3.5.0,M10.5.0/3" */
bool parseEntry(const std::string& input, std::string& outName, std::string& outCode) { bool parseEntry(const std::string& input, std::string& outName, std::string& outCode) {
if (input.size() < 3) { if (input.size() < 3) {
@ -48,20 +45,18 @@ bool parseEntry(const std::string& input, std::string& outName, std::string& out
/** Calls @a visitor for each time zone until it returns false */ /** Calls @a visitor for each time zone until it returns false */
void forEachTimeZone(const std::function<bool(const std::string& name, const std::string& code)>& visitor) { void forEachTimeZone(const std::function<bool(const std::string& name, const std::string& code)>& visitor) {
auto* file = fopen(getTimeZonesPath().c_str(), "rb"); const std::string_view csv(reinterpret_cast<const char*>(asset_timezones_csv), asset_timezones_csv_size);
if (file == nullptr) {
LOG_E(TAG, "Failed to open %s", getTimeZonesPath().c_str());
return;
}
char line[96];
std::string name; std::string name;
std::string code; std::string code;
while (fgets(line, sizeof(line), file)) { size_t start = 0;
if (parseEntry(line, name, code) && !visitor(name, code)) { while (start < csv.size()) {
size_t end = csv.find('\n', start);
end = end == std::string_view::npos ? csv.size() : end + 1;
if (parseEntry(std::string(csv.substr(start, end - start)), name, code) && !visitor(name, code)) {
break; break;
} }
start = end;
} }
fclose(file);
} }
} // namespace } // namespace

Some files were not shown because too many files have changed in this diff Show More