Posix device scanner + app ui improvements

This commit is contained in:
Ken Van Hoeylandt 2026-10-11 17:45:49 +02:00
parent 8c4bfc2260
commit 56bd0fd774
15 changed files with 687 additions and 111 deletions

View File

@ -26,6 +26,7 @@ struct GpsConfiguration {
* @brief Persists a new GPS configuration and triggers the ledger to materialize a (not started) * @brief Persists a new GPS configuration and triggers the ledger to materialize a (not started)
* GPS_TYPE device for it in the device tree. Use device_start()/device_stop() on the resulting * GPS_TYPE device for it in the device tree. Use device_start()/device_stop() on the resulting
* device to control whether it's actually running. * device to control whether it's actually running.
* @retval ERROR_ALREADY_EXISTS if a configuration for the same UART exists
* @retval ERROR_RESOURCE if the configuration file could not be opened/written * @retval ERROR_RESOURCE if the configuration file could not be opened/written
*/ */
error_t gps_settings_add_configuration(const struct GpsConfiguration* configuration); error_t gps_settings_add_configuration(const struct GpsConfiguration* configuration);

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#pragma once #pragma once
struct Device;
/** /**
* @brief Reconciles the device tree against the persisted GPS configurations (see * @brief Reconciles the device tree against the persisted GPS configurations (see
* gps/gps_settings.h): constructs+adds (but does not start) a GPS_TYPE device for every * gps/gps_settings.h): constructs+adds (but does not start) a GPS_TYPE device for every
@ -18,3 +20,9 @@ void gps_ledger_sync();
* are left untouched. * are left untouched.
*/ */
void gps_ledger_clear(); void gps_ledger_clear();
/**
* @brief Stops+destructs+removes the ledger-owned devices that are children of @a parent.
* The ledger recreates them with gps_ledger_sync() when the parent is available again.
*/
void gps_ledger_remove_children_of(struct Device* parent);

View File

@ -3,6 +3,7 @@
#include <gps/gps.h> #include <gps/gps.h>
#include <gps/gps_settings.h> #include <gps/gps_settings.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
#include <tactility/log.h> #include <tactility/log.h>
@ -30,6 +31,15 @@ struct GpsLedgerEntry {
char name[16] {}; char name[16] {};
}; };
// Syncs run on the app thread when settings change, and on the thread that starts or stops a UART
struct LedgerMutex {
Mutex mutex {};
LedgerMutex() { mutex_construct(&mutex); }
~LedgerMutex() { mutex_destruct(&mutex); }
};
static LedgerMutex ledger_mutex;
// Unique across every device the ledger creates, so device names ("gpsN") never collide. // Unique across every device the ledger creates, so device names ("gpsN") never collide.
static uint32_t next_device_index = 0; static uint32_t next_device_index = 0;
@ -47,7 +57,7 @@ static bool device_matches_configuration(Device* device, const GpsConfiguration&
static bool create_device(const GpsConfiguration& configuration) { static bool create_device(const GpsConfiguration& configuration) {
Device* uart = nullptr; Device* uart = nullptr;
if (device_get_by_name(configuration.uart_name, &uart) != ERROR_NONE) { if (device_get_by_name(configuration.uart_name, &uart) != ERROR_NONE) {
LOG_E(TAG, "Failed to find device %s", configuration.uart_name); LOG_W(TAG, "Device %s is not available", configuration.uart_name);
return false; return false;
} }
@ -125,6 +135,8 @@ static std::vector<Device*> collect_owned_devices() {
} }
void gps_ledger_sync() { void gps_ledger_sync() {
mutex_lock(&ledger_mutex.mutex);
std::vector<GpsConfiguration> configurations; std::vector<GpsConfiguration> configurations;
gps_settings_for_each_configuration(&configurations, [](const GpsConfiguration* configuration, size_t, void* context) { gps_settings_for_each_configuration(&configurations, [](const GpsConfiguration* configuration, size_t, void* context) {
static_cast<std::vector<GpsConfiguration>*>(context)->push_back(*configuration); static_cast<std::vector<GpsConfiguration>*>(context)->push_back(*configuration);
@ -158,10 +170,24 @@ void gps_ledger_sync() {
create_device(configurations[i]); create_device(configurations[i]);
} }
} }
mutex_unlock(&ledger_mutex.mutex);
} }
void gps_ledger_clear() { void gps_ledger_clear() {
mutex_lock(&ledger_mutex.mutex);
for (auto* device : collect_owned_devices()) { for (auto* device : collect_owned_devices()) {
destroy_device(device); destroy_device(device);
} }
mutex_unlock(&ledger_mutex.mutex);
}
void gps_ledger_remove_children_of(Device* parent) {
mutex_lock(&ledger_mutex.mutex);
for (auto* device : collect_owned_devices()) {
if (device_get_parent(device) == parent) {
destroy_device(device);
}
}
mutex_unlock(&ledger_mutex.mutex);
} }

View File

@ -115,6 +115,11 @@ static error_t write_configurations(const std::vector<GpsConfiguration>& configu
error_t gps_settings_add_configuration(const GpsConfiguration* configuration) { error_t gps_settings_add_configuration(const GpsConfiguration* configuration) {
std::vector<GpsConfiguration> configurations; std::vector<GpsConfiguration> configurations;
load_configurations(configurations); load_configurations(configurations);
for (const auto& existing : configurations) {
if (strcmp(existing.uart_name, configuration->uart_name) == 0) {
return ERROR_ALREADY_EXISTS;
}
}
configurations.push_back(*configuration); configurations.push_back(*configuration);
error_t error = write_configurations(configurations); error_t error = write_configurations(configurations);

View File

@ -2,18 +2,35 @@
#include <gps/module.h> #include <gps/module.h>
#include <gps/private/gps_ledger.h> #include <gps/private/gps_ledger.h>
#include <tactility/device.h>
#include <tactility/device_listener.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/error.h> #include <tactility/error.h>
#include <tactility/module.h> #include <tactility/module.h>
extern "C" { extern "C" {
// UARTs can appear and disappear at runtime, so their GPS devices follow them
static void on_device_event(Device* device, DeviceEvent event, void*) {
if (device_get_type(device) != &UART_CONTROLLER_TYPE) {
return;
}
if (event == DEVICE_EVENT_STARTED) {
gps_ledger_sync();
} else if (event == DEVICE_EVENT_STOPPING) {
gps_ledger_remove_children_of(device);
}
}
static error_t start() { static error_t start() {
// Materializes devices for configurations persisted in previous sessions. // Materializes devices for configurations persisted in previous sessions.
gps_ledger_sync(); gps_ledger_sync();
device_listener_add(on_device_event, nullptr);
return ERROR_NONE; return ERROR_NONE;
} }
static error_t stop() { static error_t stop() {
device_listener_remove(on_device_event, nullptr);
gps_ledger_clear(); gps_ledger_clear();
return ERROR_NONE; return ERROR_NONE;
} }

View File

@ -5,7 +5,7 @@ file(GLOB_RECURSE SOURCES "source/*.c**")
add_library(platform-posix OBJECT) add_library(platform-posix OBJECT)
target_sources(platform-posix PRIVATE ${SOURCES}) target_sources(platform-posix PRIVATE ${SOURCES})
target_include_directories(platform-posix PUBLIC include/) target_include_directories(platform-posix PUBLIC include/)
target_link_libraries(platform-posix PUBLIC TactilityKernel) target_link_libraries(platform-posix PUBLIC TactilityKernel service-module)
# Routes every pthread_attr_setstack() call to __wrap_pthread_attr_setstack() in source/pthread_stack_wrap.c, # Routes every pthread_attr_setstack() call to __wrap_pthread_attr_setstack() in source/pthread_stack_wrap.c,
# which no-ops it. see that file for why. # which no-ops it. see that file for why.

View File

@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Registers and starts the POSIX platform services.
* The device scanner service scans /dev every second for serial and i2c-dev device files.
* It registers a kernel device when a file appears and unregisters it when the file disappears.
* Devices that are still in use when their file disappears stay registered until they can be stopped.
* @retval ERROR_NONE on success
*/
error_t posix_services_start(void);
/**
* Stops and unregisters the services that were started by posix_services_start().
* @retval ERROR_NONE on success
*/
error_t posix_services_stop(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,311 @@
// SPDX-License-Identifier: Apache-2.0
#include <service/instance.h>
#include <service/manifest.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/concurrent/timer.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/posix_i2c.h>
#include <tactility/drivers/posix_uart.h>
#include <tactility/log.h>
#include <tactility/time.h>
#include <algorithm>
#include <cstring>
#include <dirent.h>
#include <new>
#include <set>
#include <string>
#include <unistd.h>
#include <vector>
#define TAG "posix_device_scanner"
namespace {
constexpr auto* DEVICE_DIRECTORY = "/dev";
constexpr uint32_t SCAN_INTERVAL_MS = 1000;
constexpr auto* UART_COMPATIBLE = "posix,uart";
constexpr auto* I2C_COMPATIBLE = "posix,i2c";
struct DevicePattern {
const char* prefix;
const char* compatible;
};
constexpr DevicePattern DEVICE_PATTERNS[] = {
{ "ttyUSB", UART_COMPATIBLE },
{ "ttyACM", UART_COMPATIBLE },
{ "cu.usbserial", UART_COMPATIBLE },
{ "cu.usbmodem", UART_COMPATIBLE },
{ "i2c-", I2C_COMPATIBLE },
};
struct ScannedDevice {
std::string path;
std::string name;
PosixUartConfig uart_config {};
PosixI2cConfig i2c_config {};
Device device {};
bool pending_removal = false;
bool busy_logged = false;
};
struct ScannerData {
Mutex mutex {};
Timer* timer = nullptr;
bool stopping = false;
std::vector<ScannedDevice*> devices;
/** Paths that failed to register, so the failure is only logged once */
std::set<std::string> failed_paths;
};
const char* find_compatible(const char* file_name) {
for (const auto& pattern : DEVICE_PATTERNS) {
if (strncmp(file_name, pattern.prefix, strlen(pattern.prefix)) == 0) {
return pattern.compatible;
}
}
return nullptr;
}
bool device_name_exists(const char* name) {
Device* existing = nullptr;
error_t error = device_get_by_name(name, &existing);
if (error == ERROR_NONE) {
device_put(existing);
}
return error != ERROR_NOT_FOUND;
}
/** @return the registered device, or nullptr on failure */
ScannedDevice* register_device(const std::string& file_name, const char* compatible, bool log_failure) {
auto* driver = driver_find_compatible(compatible);
if (driver == nullptr) {
if (log_failure) LOG_E(TAG, "No driver for %s", compatible);
return nullptr;
}
if (device_name_exists(file_name.c_str())) {
if (log_failure) LOG_W(TAG, "Skipping %s: a device with that name already exists", file_name.c_str());
return nullptr;
}
Device* root = nullptr;
if (device_get_by_name("/", &root) != ERROR_NONE) {
return nullptr;
}
auto* scanned = new(std::nothrow) ScannedDevice();
if (scanned == nullptr) {
device_put(root);
return nullptr;
}
scanned->path = std::string(DEVICE_DIRECTORY) + "/" + file_name;
scanned->name = file_name;
scanned->uart_config.path = scanned->path.c_str();
scanned->i2c_config.path = scanned->path.c_str();
scanned->device.name = scanned->name.c_str();
scanned->device.config = (strcmp(compatible, UART_COMPATIBLE) == 0)
? static_cast<const void*>(&scanned->uart_config)
: static_cast<const void*>(&scanned->i2c_config);
Device* device = &scanned->device;
if (device_construct(device) != ERROR_NONE) {
device_put(root);
delete scanned;
return nullptr;
}
device_set_parent(device, root);
device_put(root);
device_set_driver(device, driver);
if (device_add(device) != ERROR_NONE) {
if (log_failure) LOG_E(TAG, "Failed to add %s", scanned->path.c_str());
device_destruct(device);
delete scanned;
return nullptr;
}
error_t start_error = device_start(device);
if (start_error != ERROR_NONE) {
if (log_failure) LOG_E(TAG, "Failed to start %s: %s", scanned->path.c_str(), error_to_string(start_error));
device_remove(device);
device_destruct(device);
delete scanned;
return nullptr;
}
LOG_I(TAG, "Registered %s (%s)", scanned->name.c_str(), compatible);
return scanned;
}
/** @return true when the device was unregistered and freed */
bool unregister_device(ScannedDevice* scanned) {
Device* device = &scanned->device;
// A previous attempt can have removed the device already, when destructing it failed
if (device_is_added(device)) {
error_t stop_error = device_stop(device);
if (stop_error != ERROR_NONE) {
if (!scanned->busy_logged) {
LOG_W(TAG, "%s is in use, retrying removal: %s", scanned->name.c_str(), error_to_string(stop_error));
scanned->busy_logged = true;
}
return false;
}
device_remove(device);
}
// Fails while other devices are still children of this device
error_t destruct_error = device_destruct(device);
if (destruct_error != ERROR_NONE) {
if (!scanned->busy_logged) {
LOG_W(TAG, "%s can't be destructed yet, retrying removal: %s", scanned->name.c_str(), error_to_string(destruct_error));
scanned->busy_logged = true;
}
return false;
}
LOG_I(TAG, "Unregistered %s", scanned->name.c_str());
delete scanned;
return true;
}
std::set<std::string> list_device_files() {
std::set<std::string> result;
DIR* dir = opendir(DEVICE_DIRECTORY);
if (dir == nullptr) return result;
while (dirent* entry = readdir(dir)) {
if (find_compatible(entry->d_name) != nullptr) {
result.insert(entry->d_name);
}
}
closedir(dir);
return result;
}
void scan(ScannerData* data) {
auto files = list_device_files();
// Removals, including retries of devices that were still in use
for (auto* scanned : data->devices) {
bool present = files.contains(scanned->name);
if (present && scanned->pending_removal && device_is_added(&scanned->device)) {
scanned->pending_removal = false;
scanned->busy_logged = false;
} else if (!present) {
scanned->pending_removal = true;
}
}
std::erase_if(data->devices, [](ScannedDevice* scanned) {
return scanned->pending_removal && unregister_device(scanned);
});
// Additions
for (const auto& file_name : files) {
bool tracked = std::ranges::any_of(data->devices, [&file_name](ScannedDevice* scanned) {
return scanned->name == file_name;
});
if (tracked) continue;
bool log_failure = !data->failed_paths.contains(file_name);
std::string path = std::string(DEVICE_DIRECTORY) + "/" + file_name;
if (access(path.c_str(), R_OK | W_OK) != 0) {
if (log_failure) LOG_W(TAG, "Skipping %s: no read/write access", path.c_str());
data->failed_paths.insert(file_name);
continue;
}
auto* scanned = register_device(file_name, find_compatible(file_name.c_str()), log_failure);
if (scanned != nullptr) {
data->devices.push_back(scanned);
data->failed_paths.erase(file_name);
} else {
data->failed_paths.insert(file_name);
}
}
// Forget failures of files that disappeared, so a new file with that name logs again
std::erase_if(data->failed_paths, [&files](const std::string& file_name) {
return !files.contains(file_name);
});
}
void on_timer(void* context) {
auto* data = static_cast<ScannerData*>(context);
mutex_lock(&data->mutex);
if (!data->stopping) {
scan(data);
}
mutex_unlock(&data->mutex);
}
// ---- Service ----
void* create_service(const ServiceManifest*) {
auto* data = new(std::nothrow) ScannerData();
if (data == nullptr) return nullptr;
mutex_construct(&data->mutex);
return data;
}
void destroy_service(const ServiceManifest*, void* service_data) {
auto* data = static_cast<ScannerData*>(service_data);
mutex_destruct(&data->mutex);
delete data;
}
error_t on_start(ServiceInstance*, void* service_data) {
auto* data = static_cast<ScannerData*>(service_data);
data->stopping = false;
data->timer = timer_alloc(TIMER_TYPE_PERIODIC, millis_to_ticks(SCAN_INTERVAL_MS), on_timer, data);
if (data->timer == nullptr) return ERROR_OUT_OF_MEMORY;
on_timer(data);
error_t error = timer_start(data->timer);
if (error != ERROR_NONE) {
timer_free(data->timer);
data->timer = nullptr;
}
return error;
}
void on_stop(ServiceInstance*, void* service_data) {
auto* data = static_cast<ScannerData*>(service_data);
mutex_lock(&data->mutex);
data->stopping = true;
mutex_unlock(&data->mutex);
timer_stop(data->timer);
timer_free(data->timer);
data->timer = nullptr;
mutex_lock(&data->mutex);
for (auto* scanned : data->devices) {
// A device that is still in use stays registered, so its memory must stay valid
if (!unregister_device(scanned)) {
LOG_E(TAG, "%s is still in use and stays registered", scanned->name.c_str());
}
}
data->devices.clear();
data->failed_paths.clear();
mutex_unlock(&data->mutex);
}
} // namespace
extern "C" {
extern const ServiceManifest posix_device_scanner_service_manifest;
const ServiceManifest posix_device_scanner_service_manifest = {
.id = "posix-device-scanner",
.create_service = create_service,
.destroy_service = destroy_service,
.on_start = on_start,
.on_stop = on_stop,
};
}

View File

@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/posix_services.h>
#include <service/manager.h>
extern "C" {
extern const ServiceManifest posix_device_scanner_service_manifest;
error_t posix_services_start(void) {
return service_manager_add(&posix_device_scanner_service_manifest, true);
}
error_t posix_services_stop(void) {
const char* id = posix_device_scanner_service_manifest.id;
error_t error = service_manager_stop(id);
if (error != ERROR_NONE) return error;
return service_manager_remove(id);
}
}

View File

@ -6,11 +6,7 @@
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
#include <Tactility/TactilityPrivate.h> #include <Tactility/TactilityPrivate.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <Tactility/hal/SdCard.h>
#include <Tactility/network/NtpPrivate.h>
#include <Tactility/settings/TimePrivate.h>
#include <tactility/kernel_init.h> #include <tactility/kernel_init.h>
#include <tactility/log.h> #include <tactility/log.h>

View File

@ -176,7 +176,14 @@ static void registerInternalApps() {
app_manager_add(&app::grovesettings::manifest); app_manager_add(&app::grovesettings::manifest);
} }
if (device_exists_of_type(&UART_CONTROLLER_TYPE) || device_exists_of_type(&GROVE_TYPE)) { auto enable_gps =
#if defined(__linux__) || defined(__APPLE__)
true;
#else
device_exists_of_type(&UART_CONTROLLER_TYPE) || device_exists_of_type(&GROVE_TYPE);
#endif
if (enable_gps) {
app_manager_add(&app::addgps::manifest); app_manager_add(&app::addgps::manifest);
app_manager_add(&app::gpssettings::manifest); app_manager_add(&app::gpssettings::manifest);
} }

View File

@ -9,6 +9,10 @@
#include <wifi/module.h> #include <wifi/module.h>
#if defined(__linux__) || defined(__APPLE__)
#include <tactility/posix_services.h>
#endif
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/audio_stream.h> #include <tactility/drivers/audio_stream.h>
@ -41,6 +45,9 @@ void registerAndStartServices() {
check(module_ensure_started(&wifi_module) == ERROR_NONE); check(module_ensure_started(&wifi_module) == ERROR_NONE);
addService(service::development::manifest); addService(service::development::manifest);
addService(service::webserver::manifest); addService(service::webserver::manifest);
#if defined(__linux__) || defined(__APPLE__)
check(posix_services_start() == ERROR_NONE);
#endif
#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_ESP_HOSTED_ENABLED) #if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_ESP_HOSTED_ENABLED)
addService(service::espnow::manifest); addService(service::espnow::manifest);

View File

@ -1,5 +1,5 @@
#include <Tactility/StringUtils.h>
#include <Tactility/app/alertdialog/AlertDialog.h> #include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/app/selectiondialog/SelectionDialog.h>
#include <Tactility/lvgl/Style.h> #include <Tactility/lvgl/Style.h>
#include <app/event.h> #include <app/event.h>
@ -9,7 +9,10 @@
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
#include <lvgl/fonts.h>
#include <lvgl/icons/shared.h> #include <lvgl/icons/shared.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/drivers/uart_controller.h> #include <tactility/drivers/uart_controller.h>
@ -18,8 +21,11 @@
#include <gps/gps.h> #include <gps/gps.h>
#include <gps/gps_settings.h> #include <gps/gps_settings.h>
#include <array>
#include <cstring> #include <cstring>
#include <lvgl.h> #include <lvgl.h>
#include <string>
#include <vector>
namespace tt::app::addgps { namespace tt::app::addgps {
@ -31,73 +37,155 @@ namespace {
struct Context { struct Context {
uint32_t appInstanceId; uint32_t appInstanceId;
lv_obj_t* uartDropdown = nullptr; TaskEventGroup* eventGroup = nullptr;
lv_obj_t* modelDropdown = nullptr; uint32_t selectUartBit = 0;
lv_obj_t* baudDropdown = nullptr; uint32_t selectModelBit = 0;
uint32_t selectBaudBit = 0;
uint32_t uartSelectLaunchId = 0;
uint32_t modelSelectLaunchId = 0;
uint32_t baudSelectLaunchId = 0;
std::vector<::Device*> devices; // Committed state, which survives widget rebuilds
std::string uartName;
size_t modelIndex = 0;
size_t baudIndex = 0;
// Valid while the window is shown
lv_obj_t* uartValueLabel = nullptr;
lv_obj_t* modelValueLabel = nullptr;
lv_obj_t* baudValueLabel = nullptr;
// The bus names that the open bus selection dialog shows
std::vector<std::string> uartNames;
// Store as string instead of int, so app startup doesn't require parsing all entries.
// We only need to parse back to int when adding the new GPS entry
std::array<uint32_t, 6> baudRates = { 9600, 19200, 28800, 38400, 57600, 115200 }; std::array<uint32_t, 6> baudRates = { 9600, 19200, 28800, 38400, 57600, 115200 };
const char* baudRatesDropdownValues = "9600\n19200\n28800\n38400\n57600\n115200";
}; };
std::vector<std::string> getModelNames() { std::vector<std::string> getModelNames() {
std::vector<std::string> result; std::vector<std::string> result;
for (int model = GpsModel::GPS_MODEL_UNKNOWN; model <= GpsModel::GPS_MODEL_UC6580; model++) { for (int model = GpsModel::GPS_MODEL_UNKNOWN; model <= GpsModel::GPS_MODEL_UC6580; model++) {
result.emplace_back(gps_model_to_string(static_cast<GpsModel>(model))); // GPS_MODEL_UNKNOWN makes the driver probe for the model
result.emplace_back(model == GPS_MODEL_UNKNOWN ? "Auto-detect" : gps_model_to_string(static_cast<GpsModel>(model)));
} }
return result; return result;
} }
std::vector<std::string> getBaudNames(Context* ctx) {
std::vector<std::string> result;
for (auto baud_rate : ctx->baudRates) {
result.push_back(std::to_string(baud_rate));
}
return result;
}
/** Requires the LVGL lock */
void updateViews(Context* ctx) {
if (ctx->modelValueLabel == nullptr) {
return;
}
lv_label_set_text(ctx->uartValueLabel, ctx->uartName.empty() ? "None" : ctx->uartName.c_str());
lv_label_set_text(ctx->modelValueLabel, getModelNames()[ctx->modelIndex].c_str());
lv_label_set_text(ctx->baudValueLabel, std::to_string(ctx->baudRates[ctx->baudIndex]).c_str());
}
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));
app_event_emit_close(ctx->appInstanceId); app_event_emit_close(ctx->appInstanceId);
} }
void onSelectUartPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
task_event_group_signal(ctx->eventGroup, ctx->selectUartBit);
}
void onSelectModelPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
task_event_group_signal(ctx->eventGroup, ctx->selectModelBit);
}
void onSelectBaudPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
task_event_group_signal(ctx->eventGroup, ctx->selectBaudBit);
}
void onAddGpsPressed(lv_event_t* event) { void onAddGpsPressed(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));
auto selected_baud_index = lv_dropdown_get_selected(ctx->baudDropdown);
GpsConfiguration new_configuration = { GpsConfiguration new_configuration = {
.uart_name = { 0x00 }, .uart_name = { 0x00 },
.baud_rate = ctx->baudRates[selected_baud_index], .baud_rate = ctx->baudRates[ctx->baudIndex],
// Warning: This assumes that the enum is a regularly indexed one that starts at 0 // Warning: This assumes that the enum is a regularly indexed one that starts at 0
.model = (GpsModel)lv_dropdown_get_selected(ctx->modelDropdown) .model = (GpsModel)ctx->modelIndex
}; };
lv_dropdown_get_selected_str(ctx->uartDropdown, new_configuration.uart_name, sizeof(new_configuration.uart_name)); strncpy(new_configuration.uart_name, ctx->uartName.c_str(), sizeof(new_configuration.uart_name) - 1);
if (new_configuration.uart_name[0] == 0x00) { if (new_configuration.uart_name[0] == 0x00) {
alertdialog::start(ctx->appInstanceId, "Error", "You must select a bus/uart."); alertdialog::start(ctx->appInstanceId, "Error", "You must select a bus/uart.");
return; return;
} }
LOG_I(TAG, "Saving: uart=%s, model=%d, baud=%u", new_configuration.uart_name, (int)new_configuration.model, (unsigned)new_configuration.baud_rate); LOG_I(TAG, "Saving: uart=%s, model=%d, baud=%u", new_configuration.uart_name, (int)new_configuration.model, (unsigned)new_configuration.baud_rate);
if (gps_settings_add_configuration(&new_configuration) != ERROR_NONE) { error_t error = gps_settings_add_configuration(&new_configuration);
if (error == ERROR_ALREADY_EXISTS) {
alertdialog::start(ctx->appInstanceId, "Error", "A GPS is already configured for this bus.");
} else if (error != ERROR_NONE) {
alertdialog::start(ctx->appInstanceId, "Error", "Failed to add configuration"); alertdialog::start(ctx->appInstanceId, "Error", "Failed to add configuration");
} else { } else {
onBackPressed(event); onBackPressed(event);
} }
} }
void updateUartDevices(Context* ctx) { std::vector<std::string> getUartNames() {
ctx->devices.clear(); std::vector<std::string> names;
device_for_each_of_type(&UART_CONTROLLER_TYPE, &ctx->devices, [](auto* device, auto* context) { device_for_each_of_type(&UART_CONTROLLER_TYPE, &names, [](auto* device, auto* context) {
auto* vector_ptr = static_cast<std::vector<::Device*>*>(context); static_cast<std::vector<std::string>*>(context)->emplace_back(device->name);
vector_ptr->push_back(device);
return true; return true;
}); });
return names;
} }
std::string getUartDropdownNames(Context* ctx) { /**
std::vector<std::string> names; * Creates a row with a title, the selected value below it, and a "Select" button.
names.push_back(""); * @return the label of the selected value
for (auto* device: ctx->devices) { */
names.push_back(device->name); lv_obj_t* createSelectRow(lv_obj_t* parent, const char* title, lv_event_cb_t onSelectPressed, Context* ctx) {
} // Transparent, so the card provides the background
return string::join(names, "\n"); auto* row = lv_obj_create(parent);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
lv_obj_set_scrollable(row, false);
// The texts take the width that the button leaves. Texts that don't fit scroll.
auto* texts = lv_obj_create(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);
auto* title_label = lv_label_create(texts);
lv_obj_set_width(title_label, LV_PCT(100));
lv_label_set_long_mode(title_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
lv_label_set_text(title_label, title);
auto* value_label = lv_label_create(texts);
lv_obj_set_width(value_label, LV_PCT(100));
lv_label_set_long_mode(value_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
lv_obj_set_style_text_font(value_label, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
auto* select_button = lv_button_create(row);
lv_label_set_text(lv_label_create(select_button), "Select");
lv_obj_add_event_cb(select_button, onSelectPressed, LV_EVENT_SHORT_CLICKED, ctx);
return value_label;
} }
void createWidgets(lv_obj_t* parent, void* userData) { void createWidgets(lv_obj_t* parent, void* userData) {
@ -114,86 +202,47 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1); lv_obj_set_flex_grow(main_wrapper, 1);
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(main_wrapper, 0, 0); lv_obj_set_flex_align(main_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(main_wrapper, 0, 0); lv_obj_set_style_border_width(main_wrapper, 0, 0);
lvgl::obj_set_style_bg_invisible(main_wrapper); lvgl::obj_set_style_bg_invisible(main_wrapper);
// region Uart 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);
auto* uart_wrapper = lv_obj_create(main_wrapper); ctx->uartValueLabel = createSelectRow(card, "Bus", onSelectUartPressed, ctx);
lv_obj_set_size(uart_wrapper, LV_PCT(100), LV_SIZE_CONTENT); ctx->modelValueLabel = createSelectRow(card, "Model", onSelectModelPressed, ctx);
lv_obj_set_style_pad_ver(uart_wrapper, 0, 0); ctx->baudValueLabel = createSelectRow(card, "Baud", onSelectBaudPressed, ctx);
lv_obj_set_style_border_width(uart_wrapper, 0, 0);
lvgl::obj_set_style_bg_invisible(uart_wrapper);
ctx->uartDropdown = lv_dropdown_create(uart_wrapper);
updateUartDevices(ctx);
auto uart_options = getUartDropdownNames(ctx);
lv_dropdown_set_options(ctx->uartDropdown, uart_options.c_str());
lv_obj_align(ctx->uartDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_width(ctx->uartDropdown, LV_PCT(50));
auto* uart_label = lv_label_create(uart_wrapper);
lv_obj_align(uart_label, LV_ALIGN_TOP_LEFT, 0, 10);
lv_label_set_text(uart_label, "Bus");
// region Model
auto* model_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(model_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_ver(model_wrapper, 0, 0);
lv_obj_set_style_border_width(model_wrapper, 0, 0);
lvgl::obj_set_style_bg_invisible(model_wrapper);
ctx->modelDropdown = lv_dropdown_create(model_wrapper);
auto model_names = getModelNames();
auto model_options = string::join(model_names, "\n");
lv_dropdown_set_options(ctx->modelDropdown, model_options.c_str());
lv_obj_align(ctx->modelDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_width(ctx->modelDropdown, LV_PCT(50));
auto* model_label = lv_label_create(model_wrapper);
lv_obj_align(model_label, LV_ALIGN_TOP_LEFT, 0, 10);
lv_label_set_text(model_label, "Model");
// endregion
// region Baud
auto* baud_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(baud_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_ver(baud_wrapper, 0, 0);
lv_obj_set_style_border_width(baud_wrapper, 0, 0);
lvgl::obj_set_style_bg_invisible(baud_wrapper);
ctx->baudDropdown = lv_dropdown_create(baud_wrapper);
lv_dropdown_set_options(ctx->baudDropdown, ctx->baudRatesDropdownValues);
lv_obj_align(ctx->baudDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_width(ctx->baudDropdown, LV_PCT(50));
auto* baud_rate_label = lv_label_create(baud_wrapper);
lv_obj_align(baud_rate_label, LV_ALIGN_TOP_LEFT, 0, 10);
lv_label_set_text(baud_rate_label, "Baud");
// endregion
// region Button // region Button
auto* button_wrapper = lv_obj_create(main_wrapper); auto* add_button = lv_button_create(main_wrapper);
lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_label_set_text(lv_label_create(add_button), "Add");
lv_obj_set_style_pad_ver(button_wrapper, 0, 0);
lv_obj_set_style_border_width(button_wrapper, 0, 0);
lvgl::obj_set_style_bg_invisible(button_wrapper);
auto* add_button = lv_button_create(button_wrapper);
lv_obj_align(add_button, LV_ALIGN_TOP_MID, 0, 0);
auto* add_label = lv_label_create(add_button);
lv_label_set_text(add_label, "Add");
lv_obj_add_event_cb(add_button, onAddGpsPressed, LV_EVENT_SHORT_CLICKED, ctx); lv_obj_add_event_cb(add_button, onAddGpsPressed, LV_EVENT_SHORT_CLICKED, ctx);
// endregion // endregion
updateViews(ctx);
}
void destroyWidgets(void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->uartValueLabel = nullptr;
ctx->modelValueLabel = nullptr;
ctx->baudValueLabel = nullptr;
}
void startSelection(Context* ctx, uint32_t& launchId, const char* title, const std::vector<std::string>& items) {
if (launchId != 0) {
return;
}
launchId = selectiondialog::start(ctx->appInstanceId, title, items);
}
/** @return the selected index, or -1 when the dialog was dismissed */
int32_t getSelectionResult(const AppEvent& event, size_t itemCount) {
const int32_t selected = event.result.result;
return (selected >= 0 && selected < static_cast<int32_t>(itemCount)) ? selected : -1;
} }
int32_t appMain(int argc, char* argv[]) { int32_t appMain(int argc, char* argv[]) {
@ -203,15 +252,35 @@ int32_t appMain(int argc, char* argv[]) {
TaskEventGroup event_group {}; TaskEventGroup event_group {};
task_event_group_construct(&event_group); task_event_group_construct(&event_group);
ctx.eventGroup = &event_group;
check(task_event_group_claim_bit(&event_group, &ctx.selectUartBit) == ERROR_NONE);
check(task_event_group_claim_bit(&event_group, &ctx.selectModelBit) == ERROR_NONE);
check(task_event_group_claim_bit(&event_group, &ctx.selectBaudBit) == ERROR_NONE);
AppEventSubscription sub {}; AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
bool shouldClose = false; bool shouldClose = false;
while (!shouldClose) { while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); uint32_t flags = 0;
task_event_group_wait_any(&event_group, &flags, portMAX_DELAY);
if (flags & ctx.selectUartBit && ctx.uartSelectLaunchId == 0) {
ctx.uartNames = getUartNames();
if (ctx.uartNames.empty()) {
alertdialog::start(ctx.appInstanceId, "Error", "No bus/uart found.");
} else {
startSelection(&ctx, ctx.uartSelectLaunchId, "Bus", ctx.uartNames);
}
}
if (flags & ctx.selectModelBit) {
startSelection(&ctx, ctx.modelSelectLaunchId, "Model", getModelNames());
}
if (flags & ctx.selectBaudBit) {
startSelection(&ctx, ctx.baudSelectLaunchId, "Baud", getBaudNames(&ctx));
}
AppEvent event {}; AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) { while (app_event_poll(&sub, &event) == ERROR_NONE) {
@ -220,6 +289,22 @@ int32_t appMain(int argc, char* argv[]) {
shouldClose = true; shouldClose = true;
break; break;
case APP_EVENT_RESULT: case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.uartSelectLaunchId) {
ctx.uartSelectLaunchId = 0;
auto selected = getSelectionResult(event, ctx.uartNames.size());
if (selected >= 0) ctx.uartName = ctx.uartNames[selected];
} else if (event.result.launch_id == ctx.modelSelectLaunchId) {
ctx.modelSelectLaunchId = 0;
auto selected = getSelectionResult(event, getModelNames().size());
if (selected >= 0) ctx.modelIndex = selected;
} else if (event.result.launch_id == ctx.baudSelectLaunchId) {
ctx.baudSelectLaunchId = 0;
auto selected = getSelectionResult(event, ctx.baudRates.size());
if (selected >= 0) ctx.baudIndex = selected;
}
lvgl_lock();
updateViews(&ctx);
lvgl_unlock();
app_manager_stop(event.result.launch_id); app_manager_stop(event.result.launch_id);
break; break;
default: default:

View File

@ -1,5 +1,7 @@
#include <lvgl/fonts.h>
#include <lvgl/icons/shared.h> #include <lvgl/icons/shared.h>
#include <lvgl/lvgl.h> #include <lvgl/lvgl.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
@ -26,6 +28,7 @@
#include <gps/gps.h> #include <gps/gps.h>
#include <gps/gps_settings.h> #include <gps/gps_settings.h>
#include <gps_generic/gps_generic.h>
namespace tt::app::addgps { namespace tt::app::addgps {
extern const ::AppManifest manifest; extern const ::AppManifest manifest;
@ -138,15 +141,45 @@ void onDeleteButtonPressed(lv_event_t* event) {
} }
} }
/** @return the bus name and speed, e.g. "ttyACM0 9600 bps" */
std::string getConnectionDescription(Device* device) {
auto* parent = device_get_parent(device);
std::string result = parent != nullptr ? parent->name : "";
if (device_is_compatible(device, "tactility,gps-generic")) {
const auto* config = static_cast<const GpsConfig*>(device->config);
result += " " + std::to_string(config->baud_rate) + " bps";
}
return result;
}
void createDeviceRow(Context* ctx, Device* device) { void createDeviceRow(Context* ctx, Device* device) {
auto* wrapper = lv_obj_create(ctx->deviceListWrapper); auto* card = lvgl_card_create(ctx->deviceListWrapper);
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
// Transparent, so the card provides the background
auto* wrapper = lv_obj_create(card);
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(wrapper, 0, 0); lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_style_pad_all(wrapper, 0, 0); lv_obj_set_style_pad_all(wrapper, 0, 0);
lv_obj_set_style_bg_opa(wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_scrollable(wrapper, false);
auto* name_label = lv_label_create(wrapper); // The texts take the width that the buttons leave. Texts that don't fit scroll.
auto* texts = lv_obj_create(wrapper);
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);
auto* name_label = lv_label_create(texts);
lv_obj_set_width(name_label, LV_PCT(100));
lv_label_set_long_mode(name_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
char model_name[64]; char model_name[64];
if (gps_get_model_name(device, model_name, sizeof(model_name)) == ERROR_NONE) { if (gps_get_model_name(device, model_name, sizeof(model_name)) == ERROR_NONE) {
lv_label_set_text(name_label, model_name); lv_label_set_text(name_label, model_name);
@ -154,12 +187,19 @@ void createDeviceRow(Context* ctx, Device* device) {
lv_label_set_text(name_label, device->name); lv_label_set_text(name_label, device->name);
} }
auto* connection_label = lv_label_create(texts);
lv_obj_set_width(connection_label, LV_PCT(100));
lv_label_set_long_mode(connection_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
lv_obj_set_style_text_font(connection_label, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
lv_label_set_text(connection_label, getConnectionDescription(device).c_str());
auto* actions_wrapper = lv_obj_create(wrapper); auto* actions_wrapper = lv_obj_create(wrapper);
lv_obj_set_size(actions_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_set_size(actions_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(actions_wrapper, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(actions_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_style_border_width(actions_wrapper, 0, 0); lv_obj_set_style_border_width(actions_wrapper, 0, 0);
lv_obj_set_style_pad_all(actions_wrapper, 0, 0); lv_obj_set_style_pad_all(actions_wrapper, 0, 0);
lv_obj_set_style_pad_column(actions_wrapper, 4, 0); lv_obj_set_style_pad_column(actions_wrapper, 4, 0);
lv_obj_set_style_bg_opa(actions_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
auto* button = lv_button_create(actions_wrapper); auto* button = lv_button_create(actions_wrapper);
lv_obj_add_event_cb(button, onDeviceButtonPressed, LV_EVENT_SHORT_CLICKED, ctx); lv_obj_add_event_cb(button, onDeviceButtonPressed, LV_EVENT_SHORT_CLICKED, ctx);
@ -176,6 +216,7 @@ void createDeviceRow(Context* ctx, Device* device) {
lv_obj_add_event_cb(delete_button, onDeleteButtonPressed, LV_EVENT_SHORT_CLICKED, ctx); lv_obj_add_event_cb(delete_button, onDeleteButtonPressed, LV_EVENT_SHORT_CLICKED, ctx);
lv_obj_set_user_data(delete_button, device); lv_obj_set_user_data(delete_button, device);
auto* delete_label = lv_label_create(delete_button); auto* delete_label = lv_label_create(delete_button);
lv_obj_set_style_text_font(delete_label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
lv_label_set_text(delete_label, LVGL_ICON_SHARED_DELETE); lv_label_set_text(delete_label, LVGL_ICON_SHARED_DELETE);
row.hasConfiguration = true; row.hasConfiguration = true;
@ -199,6 +240,13 @@ void rebuildDeviceList(Context* ctx) {
createDeviceRow(static_cast<Context*>(context), device); createDeviceRow(static_cast<Context*>(context), device);
return true; return true;
}); });
if (ctx->deviceRows.empty()) {
auto* card = lvgl_card_create(ctx->deviceListWrapper);
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
auto* label = lv_label_create(card);
lv_label_set_text(label, "No devices configured");
}
} }
void updateDeviceStates(Context* ctx) { void updateDeviceStates(Context* ctx) {

View File

@ -12,6 +12,7 @@
#include <tactility/log.h> #include <tactility/log.h>
#include <lvgl.h> #include <lvgl.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/list.h> #include <lvgl/widgets/list.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
@ -79,9 +80,24 @@ void createWidgets(lv_obj_t* parent, void* userData) {
const char* title = (argv[0][0] != '\0') ? argv[0] : DEFAULT_TITLE; const char* title = (argv[0][0] != '\0') ? argv[0] : DEFAULT_TITLE;
lvgl_toolbar_create(parent, title); lvgl_toolbar_create(parent, title);
auto* list = lvgl_list_create(parent); // Scrolls the card when the items don't fit
lv_obj_set_width(list, LV_PCT(100)); auto* wrapper = lv_obj_create(parent);
lv_obj_set_flex_grow(list, 1); lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
auto* card = lvgl_card_create(wrapper);
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT);
// The list items' pressed and focused backgrounds follow the card's rounded corners
lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT);
// The card provides the background
auto* list = lvgl_list_create(card);
lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT);
if (itemCount <= 0 || argv[1][0] == '\0') { if (itemCount <= 0 || argv[1][0] == '\0') {
LOG_E(TAG, "No items provided"); LOG_E(TAG, "No items provided");