diff --git a/Modules/gps-module/include/gps/gps_settings.h b/Modules/gps-module/include/gps/gps_settings.h index 23c98f080..32efcf692 100644 --- a/Modules/gps-module/include/gps/gps_settings.h +++ b/Modules/gps-module/include/gps/gps_settings.h @@ -26,6 +26,7 @@ struct GpsConfiguration { * @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 * 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 */ error_t gps_settings_add_configuration(const struct GpsConfiguration* configuration); diff --git a/Modules/gps-module/private/gps/private/gps_ledger.h b/Modules/gps-module/private/gps/private/gps_ledger.h index a0fbc757e..018aa7a02 100644 --- a/Modules/gps-module/private/gps/private/gps_ledger.h +++ b/Modules/gps-module/private/gps/private/gps_ledger.h @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +struct Device; + /** * @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 @@ -18,3 +20,9 @@ void gps_ledger_sync(); * are left untouched. */ 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); diff --git a/Modules/gps-module/source/gps_ledger.cpp b/Modules/gps-module/source/gps_ledger.cpp index 2b94978e6..4b13a2225 100644 --- a/Modules/gps-module/source/gps_ledger.cpp +++ b/Modules/gps-module/source/gps_ledger.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -30,6 +31,15 @@ struct GpsLedgerEntry { 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. 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) { Device* uart = nullptr; 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; } @@ -125,6 +135,8 @@ static std::vector collect_owned_devices() { } void gps_ledger_sync() { + mutex_lock(&ledger_mutex.mutex); + std::vector configurations; gps_settings_for_each_configuration(&configurations, [](const GpsConfiguration* configuration, size_t, void* context) { static_cast*>(context)->push_back(*configuration); @@ -158,10 +170,24 @@ void gps_ledger_sync() { create_device(configurations[i]); } } + + mutex_unlock(&ledger_mutex.mutex); } void gps_ledger_clear() { + mutex_lock(&ledger_mutex.mutex); for (auto* device : collect_owned_devices()) { 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); } diff --git a/Modules/gps-module/source/gps_settings.cpp b/Modules/gps-module/source/gps_settings.cpp index 31489acdf..bb598e790 100644 --- a/Modules/gps-module/source/gps_settings.cpp +++ b/Modules/gps-module/source/gps_settings.cpp @@ -115,6 +115,11 @@ static error_t write_configurations(const std::vector& configu error_t gps_settings_add_configuration(const GpsConfiguration* configuration) { std::vector 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); error_t error = write_configurations(configurations); diff --git a/Modules/gps-module/source/module.cpp b/Modules/gps-module/source/module.cpp index c6935d872..3a89f2879 100644 --- a/Modules/gps-module/source/module.cpp +++ b/Modules/gps-module/source/module.cpp @@ -2,18 +2,35 @@ #include #include +#include +#include +#include #include #include 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() { // Materializes devices for configurations persisted in previous sessions. gps_ledger_sync(); + device_listener_add(on_device_event, nullptr); return ERROR_NONE; } static error_t stop() { + device_listener_remove(on_device_event, nullptr); gps_ledger_clear(); return ERROR_NONE; } diff --git a/Platforms/platform-posix/CMakeLists.txt b/Platforms/platform-posix/CMakeLists.txt index c0bd284d8..22bd6b4ad 100644 --- a/Platforms/platform-posix/CMakeLists.txt +++ b/Platforms/platform-posix/CMakeLists.txt @@ -5,7 +5,7 @@ file(GLOB_RECURSE SOURCES "source/*.c**") add_library(platform-posix OBJECT) target_sources(platform-posix PRIVATE ${SOURCES}) 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, # which no-ops it. see that file for why. diff --git a/Platforms/platform-posix/include/tactility/posix_services.h b/Platforms/platform-posix/include/tactility/posix_services.h new file mode 100644 index 000000000..a46bfb736 --- /dev/null +++ b/Platforms/platform-posix/include/tactility/posix_services.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#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 diff --git a/Platforms/platform-posix/source/posix_device_scanner.cpp b/Platforms/platform-posix/source/posix_device_scanner.cpp new file mode 100644 index 000000000..886f2b582 --- /dev/null +++ b/Platforms/platform-posix/source/posix_device_scanner.cpp @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 devices; + /** Paths that failed to register, so the failure is only logged once */ + std::set 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(&scanned->uart_config) + : static_cast(&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 list_device_files() { + std::set 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(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(service_data); + mutex_destruct(&data->mutex); + delete data; +} + +error_t on_start(ServiceInstance*, void* service_data) { + auto* data = static_cast(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(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, +}; + +} diff --git a/Platforms/platform-posix/source/posix_services.cpp b/Platforms/platform-posix/source/posix_services.cpp new file mode 100644 index 000000000..85b400b64 --- /dev/null +++ b/Platforms/platform-posix/source/posix_services.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include + +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); +} + +} diff --git a/Tactility/Source/Init.cpp b/Tactility/Source/Init.cpp index c9c8b2c91..64864e247 100644 --- a/Tactility/Source/Init.cpp +++ b/Tactility/Source/Init.cpp @@ -6,11 +6,7 @@ #include #include -#include #include -#include -#include -#include #include #include diff --git a/Tactility/Source/InitApps.cpp b/Tactility/Source/InitApps.cpp index 1d5bc4362..4a0921c22 100644 --- a/Tactility/Source/InitApps.cpp +++ b/Tactility/Source/InitApps.cpp @@ -176,7 +176,14 @@ static void registerInternalApps() { 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::gpssettings::manifest); } diff --git a/Tactility/Source/InitServices.cpp b/Tactility/Source/InitServices.cpp index 5b9f20876..9938249fe 100644 --- a/Tactility/Source/InitServices.cpp +++ b/Tactility/Source/InitServices.cpp @@ -9,6 +9,10 @@ #include +#if defined(__linux__) || defined(__APPLE__) +#include +#endif + #include #include #include @@ -41,6 +45,9 @@ void registerAndStartServices() { check(module_ensure_started(&wifi_module) == ERROR_NONE); addService(service::development::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) addService(service::espnow::manifest); diff --git a/Tactility/Source/app/addgps/AddGps.cpp b/Tactility/Source/app/addgps/AddGps.cpp index d9d9293ba..9d03c86c6 100644 --- a/Tactility/Source/app/addgps/AddGps.cpp +++ b/Tactility/Source/app/addgps/AddGps.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include #include @@ -9,7 +9,10 @@ #include +#include #include +#include +#include #include #include #include @@ -18,8 +21,11 @@ #include #include +#include #include #include +#include +#include namespace tt::app::addgps { @@ -31,73 +37,155 @@ namespace { struct Context { uint32_t appInstanceId; - lv_obj_t* uartDropdown = nullptr; - lv_obj_t* modelDropdown = nullptr; - lv_obj_t* baudDropdown = nullptr; + TaskEventGroup* eventGroup = nullptr; + uint32_t selectUartBit = 0; + 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 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 baudRates = { 9600, 19200, 28800, 38400, 57600, 115200 }; - const char* baudRatesDropdownValues = "9600\n19200\n28800\n38400\n57600\n115200"; }; std::vector getModelNames() { std::vector result; for (int model = GpsModel::GPS_MODEL_UNKNOWN; model <= GpsModel::GPS_MODEL_UC6580; model++) { - result.emplace_back(gps_model_to_string(static_cast(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(model))); } return result; } +std::vector getBaudNames(Context* ctx) { + std::vector 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) { auto* ctx = static_cast(lv_event_get_user_data(event)); app_event_emit_close(ctx->appInstanceId); } +void onSelectUartPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + task_event_group_signal(ctx->eventGroup, ctx->selectUartBit); +} + +void onSelectModelPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + task_event_group_signal(ctx->eventGroup, ctx->selectModelBit); +} + +void onSelectBaudPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + task_event_group_signal(ctx->eventGroup, ctx->selectBaudBit); +} + void onAddGpsPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - auto selected_baud_index = lv_dropdown_get_selected(ctx->baudDropdown); GpsConfiguration new_configuration = { .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 - .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) { alertdialog::start(ctx->appInstanceId, "Error", "You must select a bus/uart."); return; } 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"); } else { onBackPressed(event); } } -void updateUartDevices(Context* ctx) { - ctx->devices.clear(); - device_for_each_of_type(&UART_CONTROLLER_TYPE, &ctx->devices, [](auto* device, auto* context) { - auto* vector_ptr = static_cast*>(context); - vector_ptr->push_back(device); +std::vector getUartNames() { + std::vector names; + device_for_each_of_type(&UART_CONTROLLER_TYPE, &names, [](auto* device, auto* context) { + static_cast*>(context)->emplace_back(device->name); return true; }); + return names; } -std::string getUartDropdownNames(Context* ctx) { - std::vector names; - names.push_back(""); - for (auto* device: ctx->devices) { - names.push_back(device->name); - } - return string::join(names, "\n"); +/** + * Creates a row with a title, the selected value below it, and a "Select" button. + * @return the label of the selected value + */ +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 + 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) { @@ -114,86 +202,47 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); 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); 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); - lv_obj_set_size(uart_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_ver(uart_wrapper, 0, 0); - 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 + ctx->uartValueLabel = createSelectRow(card, "Bus", onSelectUartPressed, ctx); + ctx->modelValueLabel = createSelectRow(card, "Model", onSelectModelPressed, ctx); + ctx->baudValueLabel = createSelectRow(card, "Baud", onSelectBaudPressed, ctx); // region Button - auto* button_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - 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"); + auto* add_button = lv_button_create(main_wrapper); + lv_label_set_text(lv_label_create(add_button), "Add"); lv_obj_add_event_cb(add_button, onAddGpsPressed, LV_EVENT_SHORT_CLICKED, ctx); // endregion + + updateViews(ctx); +} + +void destroyWidgets(void* userData) { + auto* ctx = static_cast(userData); + ctx->uartValueLabel = nullptr; + ctx->modelValueLabel = nullptr; + ctx->baudValueLabel = nullptr; +} + +void startSelection(Context* ctx, uint32_t& launchId, const char* title, const std::vector& 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(itemCount)) ? selected : -1; } int32_t appMain(int argc, char* argv[]) { @@ -203,15 +252,35 @@ int32_t appMain(int argc, char* argv[]) { TaskEventGroup 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 {}; 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; 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 {}; while (app_event_poll(&sub, &event) == ERROR_NONE) { @@ -220,6 +289,22 @@ int32_t appMain(int argc, char* argv[]) { shouldClose = true; break; 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); break; default: diff --git a/Tactility/Source/app/gpssettings/GpsSettings.cpp b/Tactility/Source/app/gpssettings/GpsSettings.cpp index d1a2a09b1..cb3e5bdd7 100644 --- a/Tactility/Source/app/gpssettings/GpsSettings.cpp +++ b/Tactility/Source/app/gpssettings/GpsSettings.cpp @@ -1,5 +1,7 @@ +#include #include #include +#include #include #include @@ -26,6 +28,7 @@ #include #include +#include namespace tt::app::addgps { 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(device->config); + result += " " + std::to_string(config->baud_rate) + " bps"; + } + return result; +} + 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_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_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]; if (gps_get_model_name(device, model_name, sizeof(model_name)) == ERROR_NONE) { 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); } + 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); 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_style_border_width(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_bg_opa(actions_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT); auto* button = lv_button_create(actions_wrapper); 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_set_user_data(delete_button, device); 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); row.hasConfiguration = true; @@ -199,6 +240,13 @@ void rebuildDeviceList(Context* ctx) { createDeviceRow(static_cast(context), device); 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) { diff --git a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp index 42313088f..ce403fc48 100644 --- a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp +++ b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -79,9 +80,24 @@ void createWidgets(lv_obj_t* parent, void* userData) { const char* title = (argv[0][0] != '\0') ? argv[0] : DEFAULT_TITLE; lvgl_toolbar_create(parent, title); - auto* list = lvgl_list_create(parent); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); + // Scrolls the card when the items don't fit + auto* wrapper = lv_obj_create(parent); + 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') { LOG_E(TAG, "No items provided");