mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Posix device scanner + app ui improvements
This commit is contained in:
parent
8c4bfc2260
commit
56bd0fd774
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
#include <gps/gps.h>
|
||||
#include <gps/gps_settings.h>
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/log.h>
|
||||
@ -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<Device*> collect_owned_devices() {
|
||||
}
|
||||
|
||||
void gps_ledger_sync() {
|
||||
mutex_lock(&ledger_mutex.mutex);
|
||||
|
||||
std::vector<GpsConfiguration> configurations;
|
||||
gps_settings_for_each_configuration(&configurations, [](const GpsConfiguration* configuration, size_t, void* context) {
|
||||
static_cast<std::vector<GpsConfiguration>*>(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);
|
||||
}
|
||||
|
||||
@ -115,6 +115,11 @@ static error_t write_configurations(const std::vector<GpsConfiguration>& configu
|
||||
error_t gps_settings_add_configuration(const GpsConfiguration* configuration) {
|
||||
std::vector<GpsConfiguration> 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);
|
||||
|
||||
@ -2,18 +2,35 @@
|
||||
#include <gps/module.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/module.h>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
27
Platforms/platform-posix/include/tactility/posix_services.h
Normal file
27
Platforms/platform-posix/include/tactility/posix_services.h
Normal 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
|
||||
311
Platforms/platform-posix/source/posix_device_scanner.cpp
Normal file
311
Platforms/platform-posix/source/posix_device_scanner.cpp
Normal 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,
|
||||
};
|
||||
|
||||
}
|
||||
22
Platforms/platform-posix/source/posix_services.cpp
Normal file
22
Platforms/platform-posix/source/posix_services.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -6,11 +6,7 @@
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/TactilityPrivate.h>
|
||||
|
||||
#include <Tactility/bluetooth/Bluetooth.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/log.h>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -9,6 +9,10 @@
|
||||
|
||||
#include <wifi/module.h>
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
#include <tactility/posix_services.h>
|
||||
#endif
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/audio_stream.h>
|
||||
@ -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);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||
#include <Tactility/app/selectiondialog/SelectionDialog.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
|
||||
#include <app/event.h>
|
||||
@ -9,7 +9,10 @@
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/drivers/uart_controller.h>
|
||||
@ -18,8 +21,11 @@
|
||||
#include <gps/gps.h>
|
||||
#include <gps/gps_settings.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <lvgl.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<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 };
|
||||
const char* baudRatesDropdownValues = "9600\n19200\n28800\n38400\n57600\n115200";
|
||||
};
|
||||
|
||||
|
||||
std::vector<std::string> getModelNames() {
|
||||
std::vector<std::string> result;
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
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) {
|
||||
auto* ctx = static_cast<Context*>(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<std::vector<::Device*>*>(context);
|
||||
vector_ptr->push_back(device);
|
||||
std::vector<std::string> getUartNames() {
|
||||
std::vector<std::string> names;
|
||||
device_for_each_of_type(&UART_CONTROLLER_TYPE, &names, [](auto* device, auto* context) {
|
||||
static_cast<std::vector<std::string>*>(context)->emplace_back(device->name);
|
||||
return true;
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
std::string getUartDropdownNames(Context* ctx) {
|
||||
std::vector<std::string> 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<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[]) {
|
||||
@ -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:
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
@ -26,6 +28,7 @@
|
||||
|
||||
#include <gps/gps.h>
|
||||
#include <gps/gps_settings.h>
|
||||
#include <gps_generic/gps_generic.h>
|
||||
|
||||
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<const GpsConfig*>(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*>(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) {
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/list.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;
|
||||
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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user