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
@ -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`.
|
||||||
|
|||||||
104
Buildscripts/BinfontTool/binfont_gen.cpp
Normal 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;
|
||||||
|
}
|
||||||
91
Buildscripts/generate-assets.py
Normal 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()
|
||||||
230
Buildscripts/generate-fonts.py
Normal 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()
|
||||||
59
Buildscripts/generate-ui-assets.py
Normal 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()
|
||||||
@ -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/"
|
|
||||||
|
|||||||
@ -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=
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
Dutch (Netherlands)
|
|
||||||
Dutch (Belgium)
|
|
||||||
English (United States)
|
|
||||||
English (United Kingdom)
|
|
||||||
French (France)
|
|
||||||
Region
|
|
||||||
Language
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
Dutch (Netherlands)
|
|
||||||
Dutch (Belgium)
|
|
||||||
English (United States)
|
|
||||||
English (United Kingdom)
|
|
||||||
French (France)
|
|
||||||
Region
|
|
||||||
Language
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
Néerlandais (Pays-Bas)
|
|
||||||
Néerlandais (Belgique)
|
|
||||||
Anglais (États-Unis)
|
|
||||||
Anglais (Royaume-Uni)
|
|
||||||
Français (France)
|
|
||||||
Région
|
|
||||||
Langue
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
Nederlands (Nederland)
|
|
||||||
Nederlands (België)
|
|
||||||
Engels (Verenigde Staten)
|
|
||||||
Engels (Verenigd Koninkrijk)
|
|
||||||
Frans (Frankrijk)
|
|
||||||
Regio
|
|
||||||
Taal
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
Nederlands (Nederland)
|
|
||||||
Nederlands (België)
|
|
||||||
Engels (Verenigde Staten)
|
|
||||||
Engels (Verenigd Koninkrijk)
|
|
||||||
Frans (Frankrijk)
|
|
||||||
Regio
|
|
||||||
Taal
|
|
||||||
@ -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');
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 546 B |
@ -1,7 +0,0 @@
|
|||||||
OK
|
|
||||||
Yes
|
|
||||||
No
|
|
||||||
Cancel
|
|
||||||
Retry
|
|
||||||
Close
|
|
||||||
Open
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
OK
|
|
||||||
Yes
|
|
||||||
No
|
|
||||||
Cancel
|
|
||||||
Retry
|
|
||||||
Close
|
|
||||||
Open
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
OK
|
|
||||||
Oui
|
|
||||||
Non
|
|
||||||
Annuler
|
|
||||||
Réessayer
|
|
||||||
Fermer
|
|
||||||
Ouvrir
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
OK
|
|
||||||
Ja
|
|
||||||
Nee
|
|
||||||
Annuleren
|
|
||||||
Opnieuw
|
|
||||||
Sluiten
|
|
||||||
Openen
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
OK
|
|
||||||
Ja
|
|
||||||
Nee
|
|
||||||
Annuleren
|
|
||||||
Opnieuw
|
|
||||||
Sluiten
|
|
||||||
Openen
|
|
||||||
|
Before Width: | Height: | Size: 384 B |
BIN
Data/system/ui/cursor_12.png
Normal file
|
After Width: | Height: | Size: 387 B |
BIN
Data/system/ui/cursor_16.png
Normal file
|
After Width: | Height: | Size: 501 B |
BIN
Data/system/ui/cursor_20.png
Normal file
|
After Width: | Height: | Size: 599 B |
BIN
Data/system/ui/cursor_24.png
Normal file
|
After Width: | Height: | Size: 677 B |
BIN
Data/system/ui/cursor_32.png
Normal file
|
After Width: | Height: | Size: 869 B |
BIN
Data/system/ui/cursor_40.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Data/system/ui/spinner_12.png
Normal file
|
After Width: | Height: | Size: 232 B |
BIN
Data/system/ui/spinner_16.png
Normal file
|
After Width: | Height: | Size: 351 B |
BIN
Data/system/ui/spinner_20.png
Normal file
|
After Width: | Height: | Size: 401 B |
BIN
Data/system/ui/spinner_24.png
Normal file
|
After Width: | Height: | Size: 341 B |
BIN
Data/system/ui/spinner_32.png
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
Data/system/ui/spinner_36.png
Normal file
|
After Width: | Height: | Size: 347 B |
BIN
Data/system/ui/spinner_40.png
Normal file
|
After Width: | Height: | Size: 570 B |
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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" {
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
45
Modules/lvgl-module/include/lvgl/ui_assets.h
Normal 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
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
44
Modules/lvgl-module/source/ui_assets.c
Normal 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)];
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
#
|
#
|
||||||
|
|||||||
@ -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")
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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>()));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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 "";
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
|
||||||
@ -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,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 },
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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},
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||