Implement Serial Console app & more (#239)

- Implemented new app: Serial Console
- `Uart::writeString()`: fixed 2 mutex bugs
- `AlertDialog::start()` with default "OK" button added
- Created `tt::lvgl::defaultLockTime` for re-use
- Removed various usages of deprecated `lvgl::obj_set_style_no_padding()`
- Implemented `hal::uart::getNames()` to list all interface names
This commit is contained in:
Ken Van Hoeylandt 2025-03-07 21:58:52 +01:00 committed by GitHub
parent 83a82be901
commit 13d7e84ef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 669 additions and 65 deletions

View File

@ -33,6 +33,9 @@
- Scanning SD card for external apps and auto-register them (in a temporary register?) - Scanning SD card for external apps and auto-register them (in a temporary register?)
- Support hot-plugging SD card - Support hot-plugging SD card
- All drivers (e.g. display, touch, etc.) should call stop() in their destructor, or at least assert that they should not be running. - All drivers (e.g. display, touch, etc.) should call stop() in their destructor, or at least assert that they should not be running.
- Use GPS time to set/update the current time
- Investigate EEZ Studio
- Remove flex_flow from app_container in Gui.cpp
# Nice-to-haves # Nice-to-haves
- Give external app a different icon. Allow an external app update their id, icon, type and name once they are running(and persist that info?). Loader will need to be able to find app by (external) location. - Give external app a different icon. Allow an external app update their id, icon, type and name once they are running(and persist that info?). Loader will need to be able to find app by (external) location.
@ -66,3 +69,7 @@
- GPS app - GPS app
- Investigate CSI https://stevenmhernandez.github.io/ESP32-CSI-Tool/ - Investigate CSI https://stevenmhernandez.github.io/ESP32-CSI-Tool/
- Compile unix tools to ELF apps? - Compile unix tools to ELF apps?
- Calculator
- Text editor
- Todo list
- Calendar

View File

@ -21,6 +21,13 @@ namespace tt::app::alertdialog {
*/ */
void start(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels); void start(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels);
/**
* Show a dialog with the provided title, message and an OK button
* @param[in] title the title to show in the toolbar
* @param[in] message the message to display
*/
void start(const std::string& title, const std::string& message);
/** /**
* Get the index of the button that the user selected. * Get the index of the button that the user selected.
* *

View File

@ -0,0 +1,143 @@
#pragma once
#include "./View.h"
#include "Tactility/Preferences.h"
#include "Tactility/Tactility.h"
#include "Tactility/app/alertdialog/AlertDialog.h"
#include "Tactility/hal/uart/Uart.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/lvgl/Style.h"
#include "Tactility/service/gui/Gui.h"
#include <Tactility/StringUtils.h>
#include <array>
#include <string>
namespace tt::app::serialconsole {
class ConnectView final : public View {
public:
typedef std::function<void(std::unique_ptr<hal::uart::Uart>)> OnConnectedFunction;
std::vector<std::string> uartNames;
Preferences preferences = Preferences("SerialConsole");
private:
OnConnectedFunction onConnected;
lv_obj_t* busDropdown = nullptr;
lv_obj_t* speedTextarea = nullptr;
int32_t getSpeedInput() const {
auto* speed_text = lv_textarea_get_text(speedTextarea);
return std::stoi(speed_text);
}
void onConnect() {
auto lock = lvgl::getSyncLock()->asScopedLock();
if (!lock.lock(lvgl::defaultLockTime)) {
return;
}
auto selected_uart_index = lv_dropdown_get_selected(busDropdown);
if (selected_uart_index >= uartNames.size()) {
alertdialog::start("Error", "No UART selected");
return;
}
auto uart_name = uartNames[selected_uart_index];
auto uart = hal::uart::open(uart_name);
if (uart == nullptr) {
alertdialog::start("Error", "Failed to connect to UART");
return;
}
int speed = getSpeedInput();
if (speed <= 0) {
alertdialog::start("Error", "Invalid speed");
return;
}
if (!uart->start()) {
alertdialog::start("Error", "Failed to initialize");
return;
}
if (!uart->setBaudRate(speed)) {
uart->stop();
alertdialog::start("Error", "Failed to set baud rate");
return;
}
onConnected(std::move(uart));
}
static void onConnectCallback(lv_event_t* event) {
auto* view = (ConnectView*)lv_event_get_user_data(event);
view->onConnect();
}
public:
explicit ConnectView(OnConnectedFunction onConnected) : onConnected(std::move(onConnected)) {}
void onStart(lv_obj_t* parent) {
uartNames = hal::uart::getNames();
auto* wrapper = lv_obj_create(parent);
lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_pad_ver(wrapper, 0, 0);
lv_obj_set_style_border_width(wrapper, 0, 0);
lvgl::obj_set_style_bg_invisible(wrapper);
busDropdown = lv_dropdown_create(wrapper);
auto bus_options = string::join(uartNames, "\n");
lv_dropdown_set_options(busDropdown, bus_options.c_str());
lv_obj_align(busDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_set_width(busDropdown, LV_PCT(50));
int32_t bus_index = 0;
preferences.optInt32("bus", bus_index);
if (bus_index < uartNames.size()) {
lv_dropdown_set_selected(busDropdown, bus_index);
}
auto* bus_label = lv_label_create(wrapper);
lv_obj_align(bus_label, LV_ALIGN_TOP_LEFT, 0, 10);
lv_label_set_text(bus_label, "Bus");
int32_t speed = 115200;
preferences.optInt32("speed", speed);
speedTextarea = lv_textarea_create(wrapper);
lv_textarea_set_text(speedTextarea, std::to_string(speed).c_str());
lv_textarea_set_one_line(speedTextarea, true);
lv_obj_set_width(speedTextarea, LV_PCT(50));
lv_obj_align(speedTextarea, LV_ALIGN_TOP_RIGHT, 0, 40);
service::gui::keyboardAddTextArea(speedTextarea);
auto* baud_rate_label = lv_label_create(wrapper);
lv_obj_align(baud_rate_label, LV_ALIGN_TOP_LEFT, 0, 50);
lv_label_set_text(baud_rate_label, "Baud");
auto* connect_button = lv_button_create(wrapper);
auto* connect_label = lv_label_create(connect_button);
lv_label_set_text(connect_label, "Connect");
lv_obj_align(connect_button, LV_ALIGN_TOP_MID, 0, 90);
lv_obj_add_event_cb(connect_button, onConnectCallback, LV_EVENT_SHORT_CLICKED, this);
}
void onStop() final {
int speed = getSpeedInput();
if (speed > 0) {
preferences.putInt32("speed", speed);
}
auto bus_index = (int32_t)lv_dropdown_get_selected(busDropdown);
preferences.putInt32("bus", bus_index);
}
};
} // namespace tt::app::serialconsole

View File

@ -0,0 +1,296 @@
#pragma once
#include "./View.h"
#include "Tactility/Timer.h"
#include <cstring>
#include <sstream>
#define TAG "SerialConsole"
namespace tt::app::serialconsole {
constexpr size_t receiveBufferSize = 512;
constexpr size_t renderBufferSize = receiveBufferSize + 2; // Leave space for newline at split and null terminator at the end
class ConsoleView final : public View {
private:
lv_obj_t* _Nullable parent = nullptr;
lv_obj_t* _Nullable logTextarea = nullptr;
lv_obj_t* _Nullable inputTextarea = nullptr;
std::unique_ptr<hal::uart::Uart> _Nullable uart = nullptr;
std::shared_ptr<Thread> uartThread _Nullable = nullptr;
bool uartThreadInterrupted = false;
std::shared_ptr<Thread> viewThread _Nullable = nullptr;
bool viewThreadInterrupted = false;
Mutex mutex = Mutex(Mutex::Type::Recursive);
uint8_t receiveBuffer[receiveBufferSize];
uint8_t renderBuffer[renderBufferSize];
size_t receiveBufferPosition = 0;
std::string terminatorString = "\n";
bool isUartThreadInterrupted() const {
auto lock = mutex.asScopedLock();
lock.lock();
return uartThreadInterrupted;
}
bool isViewThreadInterrupted() const {
auto lock = mutex.asScopedLock();
lock.lock();
return viewThreadInterrupted;
}
void updateViews() {
auto lvgl_lock = lvgl::getSyncLock()->asScopedLock();
if (!lvgl_lock.lock(lvgl::defaultLockTime)) {
return;
}
if (parent == nullptr) {
return;
}
// Updating the view is expensive, so we only want to set the text once:
// Gather all the lines in a single buffer
if (mutex.lock()) {
size_t first_part_size = receiveBufferSize - receiveBufferPosition;
memcpy(renderBuffer, receiveBuffer + receiveBufferPosition, first_part_size);
renderBuffer[receiveBufferPosition] = '\n';
if (receiveBufferPosition > 0) {
memcpy(renderBuffer + first_part_size + 1, receiveBuffer, (receiveBufferSize - first_part_size));
renderBuffer[receiveBufferSize - 1] = 0x00;
}
mutex.unlock();
}
if (lvgl::lock(lvgl::defaultLockTime)) {
lv_textarea_set_text(logTextarea, (const char*)renderBuffer);
lvgl::unlock();
}
}
int32_t viewThreadMain() {
while (!isViewThreadInterrupted()) {
auto start_time = kernel::getTicks();
updateViews();
auto end_time = kernel::getTicks();
auto time_diff = end_time - start_time;
if (time_diff < 500U) {
kernel::delayTicks((500U - time_diff) / portTICK_PERIOD_MS);
}
}
return 0;
}
int32_t uartThreadMain() {
uint8_t byte;
while (!isUartThreadInterrupted()) {
assert(uart != nullptr);
bool success = uart->readByte(&byte, 50 / portTICK_PERIOD_MS);
// Thread might've been interrupted in the meanwhile
if (isUartThreadInterrupted()) {
break;
}
if (success) {
mutex.lock();
receiveBuffer[receiveBufferPosition++] = byte;
if (receiveBufferPosition == receiveBufferSize) {
receiveBufferPosition = 0;
}
mutex.unlock();
}
}
return 0;
}
static int32_t viewThreadMainStatic(void* parameter) {
auto* view = (ConsoleView*)parameter;
return view->viewThreadMain();
}
static int32_t uartThreadMainStatic(void* parameter) {
auto* view = (ConsoleView*)parameter;
return view->uartThreadMain();
}
static void onSendClickedCallback(lv_event_t* event) {
auto* view = (ConsoleView*)lv_event_get_user_data(event);
view->onSendClicked();
}
static void onTerminatorDropdownValueChangedCallback(lv_event_t* event) {
auto* view = (ConsoleView*)lv_event_get_user_data(event);
view->onTerminatorDropDownValueChanged(event);
}
void onTerminatorDropDownValueChanged(lv_event_t* event) {
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
mutex.lock();
switch (lv_dropdown_get_selected(dropdown)) {
case 0:
terminatorString = "\n";
break;
case 1:
terminatorString = "\r\n";
break;
}
mutex.unlock();
}
void onSendClicked() {
mutex.lock();
std::string input_text = lv_textarea_get_text(inputTextarea);
std::string to_send = input_text + terminatorString;
mutex.unlock();
auto* safe_uart = uart.get();
if (safe_uart != nullptr) {
if (!safe_uart->writeString(to_send.c_str(), 100 / portTICK_PERIOD_MS)) {
TT_LOG_E(TAG, "Failed to send \"%s\"", input_text.c_str());
}
}
lv_textarea_set_text(inputTextarea, "");
}
public:
void startLogic(std::unique_ptr<hal::uart::Uart> newUart) {
memset(receiveBuffer, 0, receiveBufferSize);
assert(uartThread == nullptr);
assert(uart == nullptr);
uart = std::move(newUart);
uartThreadInterrupted = false;
uartThread = std::make_unique<Thread>(
"SerConsUart",
4096,
uartThreadMainStatic,
this
);
uartThread->setPriority(tt::Thread::Priority::High);
uartThread->start();
}
void startViews(lv_obj_t* parent) {
this->parent = parent;
lv_obj_set_style_pad_gap(parent, 2, 0);
logTextarea = lv_textarea_create(parent);
lv_textarea_set_placeholder_text(logTextarea, "Waiting for data...");
lv_obj_set_flex_grow(logTextarea, 1);
lv_obj_set_width(logTextarea, LV_PCT(100));
lv_obj_add_state(logTextarea, LV_STATE_DISABLED);
lv_obj_set_style_margin_ver(logTextarea, 0, 0);
service::gui::keyboardAddTextArea(logTextarea);
auto* input_wrapper = lv_obj_create(parent);
lv_obj_set_size(input_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(input_wrapper, 0, 0);
lv_obj_set_style_border_width(input_wrapper, 0, 0);
lv_obj_set_width(input_wrapper, LV_PCT(100));
lv_obj_set_flex_flow(input_wrapper, LV_FLEX_FLOW_ROW);
inputTextarea = lv_textarea_create(input_wrapper);
lv_textarea_set_one_line(inputTextarea, true);
lv_textarea_set_placeholder_text(inputTextarea, "Text to send");
lv_obj_set_width(inputTextarea, LV_PCT(100));
lv_obj_set_flex_grow(inputTextarea, 1);
service::gui::keyboardAddTextArea(inputTextarea);
auto* terminator_dropdown = lv_dropdown_create(input_wrapper);
lv_dropdown_set_options(terminator_dropdown, "\\n\n\\r\\n");
lv_obj_set_width(terminator_dropdown, 70);
lv_obj_add_event_cb(terminator_dropdown, onTerminatorDropdownValueChangedCallback, LV_EVENT_VALUE_CHANGED, this);
auto* button = lv_button_create(input_wrapper);
auto* button_label = lv_label_create(button);
lv_label_set_text(button_label, "Send");
lv_obj_add_event_cb(button, onSendClickedCallback, LV_EVENT_SHORT_CLICKED, this);
viewThreadInterrupted = false;
viewThread = std::make_unique<Thread>(
"SerConsView",
4096,
viewThreadMainStatic,
this
);
viewThread->setPriority(THREAD_PRIORITY_RENDER);
viewThread->start();
}
void stopLogic() {
auto lock = mutex.asScopedLock();
lock.lock();
uartThreadInterrupted = true;
// Detach thread, it will auto-delete when leaving the current scope
auto old_uart_thread = std::move(uartThread);
// Unlock so thread can lock
lock.unlock();
if (old_uart_thread->getState() != Thread::State::Stopped) {
// Wait for thread to finish
old_uart_thread->join();
}
}
void stopViews() {
auto lock = mutex.asScopedLock();
lock.lock();
viewThreadInterrupted = true;
// Detach thread, it will auto-delete when leaving the current scope
auto old_view_thread = std::move(viewThread);
// Unlock so thread can lock
lock.unlock();
if (old_view_thread->getState() != Thread::State::Stopped) {
// Wait for thread to finish
old_view_thread->join();
}
}
void stopUart() {
auto lock = mutex.asScopedLock();
lock.lock();
if (uart != nullptr && uart->isStarted()) {
uart->stop();
uart = nullptr;
}
}
void onStart(lv_obj_t* parent, std::unique_ptr<hal::uart::Uart> newUart) {
auto lock = mutex.asScopedLock();
lock.lock();
startLogic(std::move(newUart));
startViews(parent);
}
void onStop() final {
stopViews();
stopLogic();
stopUart();
}
};
} // namespace tt::app::serialconsole

View File

@ -0,0 +1,12 @@
#pragma once
#include <lvgl.h>
namespace tt::app::serialconsole {
class View {
public:
virtual void onStop() = 0;
};
}

View File

@ -6,6 +6,8 @@
namespace tt::lvgl { namespace tt::lvgl {
constexpr TickType_t defaultLockTime = 500 / portTICK_PERIOD_MS;
/** /**
* LVGL locking function * LVGL locking function
* @param[in] timeoutMillis timeout in milliseconds. waits forever when 0 is passed. * @param[in] timeoutMillis timeout in milliseconds. waits forever when 0 is passed.

View File

@ -45,6 +45,7 @@ namespace app {
namespace log { extern const AppManifest manifest; } namespace log { extern const AppManifest manifest; }
namespace power { extern const AppManifest manifest; } namespace power { extern const AppManifest manifest; }
namespace selectiondialog { extern const AppManifest manifest; } namespace selectiondialog { extern const AppManifest manifest; }
namespace serialconsole { extern const AppManifest manifest; }
namespace settings { extern const AppManifest manifest; } namespace settings { extern const AppManifest manifest; }
namespace systeminfo { extern const AppManifest manifest; } namespace systeminfo { extern const AppManifest manifest; }
namespace textviewer { extern const AppManifest manifest; } namespace textviewer { extern const AppManifest manifest; }
@ -80,6 +81,7 @@ static void registerSystemApps() {
addApp(app::inputdialog::manifest); addApp(app::inputdialog::manifest);
addApp(app::launcher::manifest); addApp(app::launcher::manifest);
addApp(app::log::manifest); addApp(app::log::manifest);
addApp(app::serialconsole::manifest);
addApp(app::settings::manifest); addApp(app::settings::manifest);
addApp(app::selectiondialog::manifest); addApp(app::selectiondialog::manifest);
addApp(app::systeminfo::manifest); addApp(app::systeminfo::manifest);

View File

@ -31,6 +31,14 @@ void start(const std::string& title, const std::string& message, const std::vect
service::loader::startApp(manifest.id, bundle); service::loader::startApp(manifest.id, bundle);
} }
void start(const std::string& title, const std::string& message) {
auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, "OK");
service::loader::startApp(manifest.id, bundle);
}
int32_t getResultIndex(const Bundle& bundle) { int32_t getResultIndex(const Bundle& bundle) {
int32_t index = -1; int32_t index = -1;
bundle.optInt32(RESULT_BUNDLE_KEY_INDEX, index); bundle.optInt32(RESULT_BUNDLE_KEY_INDEX, index);
@ -76,6 +84,7 @@ private:
lv_label_set_text(button_label, text.c_str()); lv_label_set_text(button_label, text.c_str());
lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, (void*)index); lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
} }
public: public:
void onShow(AppContext& app, lv_obj_t* parent) override { void onShow(AppContext& app, lv_obj_t* parent) override {
@ -89,6 +98,7 @@ public:
lv_obj_t* message_label = lv_label_create(parent); lv_obj_t* message_label = lv_label_create(parent);
lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_width(message_label, LV_PCT(80)); lv_obj_set_width(message_label, LV_PCT(80));
lv_obj_set_style_text_align(message_label, LV_TEXT_ALIGN_CENTER, 0);
std::string message; std::string message;
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) { if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {
@ -107,21 +117,9 @@ public:
std::string items_concatenated; std::string items_concatenated;
if (parameters->optString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_concatenated)) { if (parameters->optString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_concatenated)) {
std::vector<std::string> labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); std::vector<std::string> labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN);
if (labels.empty() || labels.front().empty()) { size_t index = 0;
TT_LOG_E(TAG, "No items provided"); for (const auto& label: labels) {
setResult(Result::Error); createButton(button_wrapper, label, index++);
service::loader::stopApp();
} else if (labels.size() == 1) {
auto result_bundle = std::make_unique<Bundle>();
result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0);
setResult(Result::Ok, std::move(result_bundle));
service::loader::stopApp();
TT_LOG_W(TAG, "Auto-selecting single item");
} else {
size_t index = 0;
for (const auto& label: labels) {
createButton(button_wrapper, label, index++);
}
} }
} }
} }

View File

@ -0,0 +1,96 @@
#include "Tactility/app/serialconsole/ConnectView.h"
#include "Tactility/app/serialconsole/ConsoleView.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/lvgl/Style.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/hal/uart/Uart.h>
#include <lvgl.h>
#define TAG "text_viewer"
namespace tt::app::serialconsole {
class SerialConsoleApp final : public App {
private:
lv_obj_t* disconnectButton = nullptr;
lv_obj_t* wrapperWidget = nullptr;
ConnectView connectView = ConnectView([this](auto uart){
showConsoleView(std::move(uart));
});
ConsoleView consoleView;
View* activeView = nullptr;
void stopActiveView() {
if (activeView != nullptr) {
activeView->onStop();
lv_obj_clean(wrapperWidget);
activeView = nullptr;
}
}
void showConsoleView(std::unique_ptr<hal::uart::Uart> uart) {
stopActiveView();
activeView = &consoleView;
consoleView.onStart(wrapperWidget, std::move(uart));
lv_obj_remove_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN);
}
void showConnectView() {
stopActiveView();
activeView = &connectView;
connectView.onStart(wrapperWidget);
lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN);
}
void onDisconnect() {
// Changing views (calling ConsoleView::stop()) also disconnects the UART
showConnectView();
}
static void onDisconnectPressed(lv_event_t* event) {
auto* app = (SerialConsoleApp*)lv_event_get_user_data(event);
app->onDisconnect();
}
public:
SerialConsoleApp() = default;
void onShow(AppContext& app, lv_obj_t* parent) final {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
auto* toolbar = lvgl::toolbar_create(parent, app);
disconnectButton = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_POWER, onDisconnectPressed, this);
lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN);
wrapperWidget = lv_obj_create(parent);
lv_obj_set_width(wrapperWidget, LV_PCT(100));
lv_obj_set_flex_grow(wrapperWidget, 1);
lv_obj_set_flex_flow(wrapperWidget, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(wrapperWidget, 0, 0);
lv_obj_set_style_border_width(wrapperWidget, 0, 0);
lvgl::obj_set_style_bg_invisible(wrapperWidget);
showConnectView();
}
void onHide(AppContext& app) final {
stopActiveView();
}
};
extern const AppManifest manifest = {
.id = "SerialConsole",
.name = "Serial Console",
.icon = LV_SYMBOL_LIST,
.type = Type::System,
.createApp = create<SerialConsoleApp>
};
} // namespace

View File

@ -53,13 +53,16 @@ lv_obj_t* toolbar_create(lv_obj_t* parent, const std::string& title) {
auto* toolbar = (Toolbar*)obj; auto* toolbar = (Toolbar*)obj;
obj_set_style_no_padding(obj); lv_obj_set_style_pad_all(obj, 0, 0);
lv_obj_set_style_pad_gap(obj, 0, 0);
lv_obj_center(obj); lv_obj_center(obj);
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
toolbar->close_button = lv_button_create(obj); toolbar->close_button = lv_button_create(obj);
lv_obj_set_size(toolbar->close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4); lv_obj_set_size(toolbar->close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
obj_set_style_no_padding(toolbar->close_button); lv_obj_set_style_pad_all(toolbar->close_button, 0, 0);
lv_obj_set_style_pad_gap(toolbar->close_button, 0, 0);
toolbar->close_button_image = lv_image_create(toolbar->close_button); toolbar->close_button_image = lv_image_create(toolbar->close_button);
lv_obj_align(toolbar->close_button_image, LV_ALIGN_CENTER, 0, 0); lv_obj_align(toolbar->close_button_image, LV_ALIGN_CENTER, 0, 0);

View File

@ -47,13 +47,14 @@ Gui* gui_alloc() {
lv_obj_t* vertical_container = lv_obj_create(screen_root); lv_obj_t* vertical_container = lv_obj_create(screen_root);
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100)); lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(vertical_container); lv_obj_set_style_pad_all(vertical_container, 0, 0);
lv_obj_set_style_pad_gap(vertical_container, 0, 0);
lvgl::obj_set_style_bg_blacken(vertical_container); lvgl::obj_set_style_bg_blacken(vertical_container);
instance->statusbarWidget = lvgl::statusbar_create(vertical_container); instance->statusbarWidget = lvgl::statusbar_create(vertical_container);
auto* app_container = lv_obj_create(vertical_container); auto* app_container = lv_obj_create(vertical_container);
lvgl::obj_set_style_no_padding(app_container); lv_obj_set_style_pad_all(app_container, 0, 0);
lv_obj_set_style_border_width(app_container, 0, 0); lv_obj_set_style_border_width(app_container, 0, 0);
lvgl::obj_set_style_bg_blacken(app_container); lvgl::obj_set_style_bg_blacken(app_container);
lv_obj_set_width(app_container, LV_PCT(100)); lv_obj_set_width(app_container, LV_PCT(100));

View File

@ -58,14 +58,10 @@ std::basic_string<T> lowercase(const std::basic_string<T>& input) {
return std::move(output); return std::move(output);
} }
/** /** @return true when input only has hex characters: [a-z], [A-Z], [0-9] */
* @return true when input only has hex characters: [a-z], [A-Z], [0-9]
*/
bool isAsciiHexString(const std::string& input); bool isAsciiHexString(const std::string& input);
/** /** @return the first part of a file name right up (and excluding) the first period character. */
* @return the first part of a file name right up (and excluding) the first period character.
*/
std::string removeFileExtension(const std::string& input); std::string removeFileExtension(const std::string& input);
} // namespace } // namespace

View File

@ -50,12 +50,14 @@ void Thread::mainBody(void* context) {
assert(pvTaskGetThreadLocalStoragePointer(nullptr, 0) == nullptr); assert(pvTaskGetThreadLocalStoragePointer(nullptr, 0) == nullptr);
vTaskSetThreadLocalStoragePointer(nullptr, 0, thread); vTaskSetThreadLocalStoragePointer(nullptr, 0, thread);
TT_LOG_I(TAG, "Starting %s", thread->name.c_str());
assert(thread->state == Thread::State::Starting); assert(thread->state == Thread::State::Starting);
thread->setState(Thread::State::Running); thread->setState(Thread::State::Running);
thread->callbackResult = thread->callback(thread->callbackContext); thread->callbackResult = thread->callback(thread->callbackContext);
assert(thread->state == Thread::State::Running); assert(thread->state == Thread::State::Running);
thread->setState(Thread::State::Stopped); thread->setState(Thread::State::Stopped);
TT_LOG_I(TAG, "Stopped %s", thread->name.c_str());
vTaskSetThreadLocalStoragePointer(nullptr, 0, nullptr); vTaskSetThreadLocalStoragePointer(nullptr, 0, nullptr);
thread->taskHandle = nullptr; thread->taskHandle = nullptr;

View File

@ -121,4 +121,6 @@ public:
*/ */
std::unique_ptr<Uart> open(std::string name); std::unique_ptr<Uart> open(std::string name);
std::vector<std::string> getNames();
} // namespace tt::hal::uart } // namespace tt::hal::uart

View File

@ -12,6 +12,7 @@ namespace tt {
bool Preferences::optBool(const std::string& key, bool& out) const { bool Preferences::optBool(const std::string& key, bool& out) const {
nvs_handle_t handle; nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) { if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
return false; return false;
} else { } else {
uint8_t out_number; uint8_t out_number;
@ -27,6 +28,7 @@ bool Preferences::optBool(const std::string& key, bool& out) const {
bool Preferences::optInt32(const std::string& key, int32_t& out) const { bool Preferences::optInt32(const std::string& key, int32_t& out) const {
nvs_handle_t handle; nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) { if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
return false; return false;
} else { } else {
bool success = nvs_get_i32(handle, key.c_str(), &out) == ESP_OK; bool success = nvs_get_i32(handle, key.c_str(), &out) == ESP_OK;
@ -38,6 +40,7 @@ bool Preferences::optInt32(const std::string& key, int32_t& out) const {
bool Preferences::optString(const std::string& key, std::string& out) const { bool Preferences::optString(const std::string& key, std::string& out) const {
nvs_handle_t handle; nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) { if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
return false; return false;
} else { } else {
size_t out_size = 256; size_t out_size = 256;
@ -69,11 +72,11 @@ void Preferences::putBool(const std::string& key, bool value) {
nvs_handle_t handle; nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) == ESP_OK) { if (nvs_open(namespace_, NVS_READWRITE, &handle) == ESP_OK) {
if (nvs_set_u8(handle, key.c_str(), (uint8_t)value) != ESP_OK) { if (nvs_set_u8(handle, key.c_str(), (uint8_t)value) != ESP_OK) {
TT_LOG_E(TAG, "failed to write %s:%s", namespace_, key.c_str()); TT_LOG_E(TAG, "Failed to write %s:%s", namespace_, key.c_str());
} }
nvs_close(handle); nvs_close(handle);
} else { } else {
TT_LOG_E(TAG, "failed to open namespace %s for writing", namespace_); TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
} }
} }
@ -81,11 +84,11 @@ void Preferences::putInt32(const std::string& key, int32_t value) {
nvs_handle_t handle; nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) == ESP_OK) { if (nvs_open(namespace_, NVS_READWRITE, &handle) == ESP_OK) {
if (nvs_set_i32(handle, key.c_str(), value) != ESP_OK) { if (nvs_set_i32(handle, key.c_str(), value) != ESP_OK) {
TT_LOG_E(TAG, "failed to write %s:%s", namespace_, key.c_str()); TT_LOG_E(TAG, "Failed to write %s:%s", namespace_, key.c_str());
} }
nvs_close(handle); nvs_close(handle);
} else { } else {
TT_LOG_E(TAG, "failed to open namespace %s for writing", namespace_); TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
} }
} }
@ -95,7 +98,7 @@ void Preferences::putString(const std::string& key, const std::string& text) {
nvs_set_str(handle, key.c_str(), text.c_str()); nvs_set_str(handle, key.c_str(), text.c_str());
nvs_close(handle); nvs_close(handle);
} else { } else {
TT_LOG_E(TAG, "failed to open namespace %s for writing", namespace_); TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
} }
} }

View File

@ -101,7 +101,7 @@ int32_t GpsDevice::threadMain() {
if (bytes_read > 0U) { if (bytes_read > 0U) {
TT_LOG_I(TAG, "%s", buffer); TT_LOG_D(TAG, "%s", buffer);
switch (minmea_sentence_id((char*)buffer, false)) { switch (minmea_sentence_id((char*)buffer, false)) {
case MINMEA_SENTENCE_RMC: case MINMEA_SENTENCE_RMC:

View File

@ -4,12 +4,15 @@
#include <Tactility/Mutex.h> #include <Tactility/Mutex.h>
#include <ranges> #include <ranges>
#include <cstring>
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <esp_check.h> #include "Tactility/TactilityHeadless.h"
#include "Tactility/hal/uart/UartEsp.h" #include "Tactility/hal/uart/UartEsp.h"
#include <esp_check.h>
#else #else
#include "Tactility/hal/uart/UartPosix.h" #include "Tactility/hal/uart/UartPosix.h"
#include <dirent.h>
#endif #endif
#define TAG "uart" #define TAG "uart"
@ -39,15 +42,8 @@ bool init(const std::vector<uart::Configuration>& configurations) {
} }
bool Uart::writeString(const char* buffer, TickType_t timeout) { bool Uart::writeString(const char* buffer, TickType_t timeout) {
while (*buffer != 0) { auto size = strlen(buffer);
if (writeBytes(reinterpret_cast<const std::byte*>(buffer), 1, timeout)) { writeBytes((std::byte*)buffer, size, timeout);
buffer++;
} else {
TT_LOG_E(TAG, "Failed to write - breaking off");
return false;
}
}
return true; return true;
} }
@ -105,6 +101,8 @@ size_t Uart::readUntil(std::byte* buffer, size_t bufferSize, uint8_t untilByte,
} }
std::unique_ptr<Uart> open(std::string name) { std::unique_ptr<Uart> open(std::string name) {
TT_LOG_I(TAG, "Open %s", name.c_str());
auto result = std::views::filter(uartEntries, [&name](auto& entry) { auto result = std::views::filter(uartEntries, [&name](auto& entry) {
return entry.configuration.name == name; return entry.configuration.name == name;
}); });
@ -123,10 +121,12 @@ std::unique_ptr<Uart> open(std::string name) {
auto uart = create(entry.configuration); auto uart = create(entry.configuration);
assert(uart != nullptr); assert(uart != nullptr);
entry.usageId = uart->getId(); entry.usageId = uart->getId();
TT_LOG_I(TAG, "Opened %lu", entry.usageId);
return uart; return uart;
} }
void close(uint32_t uartId) { void close(uint32_t uartId) {
TT_LOG_I(TAG, "Close %lu", uartId);
auto result = std::views::filter(uartEntries, [&uartId](auto& entry) { auto result = std::views::filter(uartEntries, [&uartId](auto& entry) {
return entry.usageId == uartId; return entry.usageId == uartId;
}); });
@ -139,6 +139,32 @@ void close(uint32_t uartId) {
} }
} }
std::vector<std::string> getNames() {
std::vector<std::string> names;
#ifdef ESP_PLATFORM
for (auto& config : getConfiguration()->uart) {
names.push_back(config.name);
}
#else
DIR* dir = opendir("/dev");
if (dir == nullptr) {
TT_LOG_E(TAG, "Failed to read /dev");
return names;
}
struct dirent* current_entry;
while ((current_entry = readdir(dir)) != nullptr) {
auto name = std::string(current_entry->d_name);
if (name.starts_with("tty")) {
auto path = std::string("/dev/") + name;
names.push_back(path);
}
}
closedir(dir);
#endif
return names;
}
Uart::Uart() : id(++lastUartId) {} Uart::Uart() : id(++lastUartId) {}
Uart::~Uart() { Uart::~Uart() {

View File

@ -13,11 +13,13 @@
namespace tt::hal::uart { namespace tt::hal::uart {
bool UartEsp::start() { bool UartEsp::start() {
TT_LOG_I(TAG, "[%s] Starting", configuration.name.c_str());
auto lock = mutex.asScopedLock(); auto lock = mutex.asScopedLock();
lock.lock(); lock.lock();
if (started) { if (started) {
TT_LOG_E(TAG, "(%d) Starting: Already started", configuration.port); TT_LOG_E(TAG, "[%s] Starting: Already started", configuration.name.c_str());
return false; return false;
} }
@ -30,46 +32,53 @@ bool UartEsp::start() {
esp_err_t result = uart_param_config(configuration.port, &configuration.config); esp_err_t result = uart_param_config(configuration.port, &configuration.config);
if (result != ESP_OK) { if (result != ESP_OK) {
TT_LOG_E(TAG, "(%d) Starting: Failed to configure: %s", configuration.port, esp_err_to_name(result)); TT_LOG_E(TAG, "[%s] Starting: Failed to configure: %s", configuration.name.c_str(), esp_err_to_name(result));
return false; return false;
} }
if (uart_is_driver_installed(configuration.port)) {
TT_LOG_W(TAG, "[%s] Driver was still installed. You probably forgot to stop, or another system uses/used the driver.", configuration.name.c_str());
uart_driver_delete(configuration.port);
}
result = uart_set_pin(configuration.port, configuration.txPin, configuration.rxPin, configuration.rtsPin, configuration.ctsPin); result = uart_set_pin(configuration.port, configuration.txPin, configuration.rxPin, configuration.rtsPin, configuration.ctsPin);
if (result != ESP_OK) { if (result != ESP_OK) {
TT_LOG_E(TAG, "(%d) Starting: Failed set pins: %s", configuration.port, esp_err_to_name(result)); TT_LOG_E(TAG, "[%s] Starting: Failed set pins: %s", configuration.name.c_str(), esp_err_to_name(result));
return false; return false;
} }
result = uart_driver_install(configuration.port, (int)configuration.rxBufferSize, (int)configuration.txBufferSize, 0, nullptr, intr_alloc_flags); result = uart_driver_install(configuration.port, (int)configuration.rxBufferSize, (int)configuration.txBufferSize, 0, nullptr, intr_alloc_flags);
if (result != ESP_OK) { if (result != ESP_OK) {
TT_LOG_E(TAG, "(%d) Starting: Failed to install driver: %s", configuration.port, esp_err_to_name(result)); TT_LOG_E(TAG, "[%s] Starting: Failed to install driver: %s", configuration.name.c_str(), esp_err_to_name(result));
return false; return false;
} }
started = true; started = true;
TT_LOG_I(TAG, "(%d) Started", configuration.port); TT_LOG_I(TAG, "[%s] Started", configuration.name.c_str());
return true; return true;
} }
bool UartEsp::stop() { bool UartEsp::stop() {
TT_LOG_I(TAG, "[%s] Stopping", configuration.name.c_str());
auto lock = mutex.asScopedLock(); auto lock = mutex.asScopedLock();
lock.lock(); lock.lock();
if (!started) { if (!started) {
TT_LOG_E(TAG, "(%d) Stopping: Not started", configuration.port); TT_LOG_E(TAG, "[%s] Stopping: Not started", configuration.name.c_str());
return false; return false;
} }
esp_err_t result = uart_driver_delete(configuration.port); esp_err_t result = uart_driver_delete(configuration.port);
if (result != ESP_OK) { if (result != ESP_OK) {
TT_LOG_E(TAG, "(%d) Stopping: Failed to delete driver: %s", configuration.port, esp_err_to_name(result)); TT_LOG_E(TAG, "[%s] Stopping: Failed to delete driver: %s", configuration.name.c_str(), esp_err_to_name(result));
return false; return false;
} }
started = false; started = false;
TT_LOG_I(TAG, "(%d) Stopped", configuration.port); TT_LOG_I(TAG, "[%s] Stopped", configuration.name.c_str());
return true; return true;
} }
@ -97,7 +106,8 @@ bool UartEsp::readByte(std::byte* output, TickType_t timeout) {
} }
size_t UartEsp::writeBytes(const std::byte* buffer, size_t bufferSize, TickType_t timeout) { size_t UartEsp::writeBytes(const std::byte* buffer, size_t bufferSize, TickType_t timeout) {
if (!mutex.lock(timeout)) { auto lock = mutex.asScopedLock();
if (!lock.lock(timeout)) {
return false; return false;
} }

View File

@ -19,13 +19,13 @@ bool UartPosix::start() {
lock.lock(); lock.lock();
if (device != nullptr) { if (device != nullptr) {
TT_LOG_E(TAG, "(%s) Starting: Already started", configuration.name.c_str()); TT_LOG_E(TAG, "[%s] Starting: Already started", configuration.name.c_str());
return false; return false;
} }
auto file = fopen(configuration.name.c_str(), "w"); auto file = fopen(configuration.name.c_str(), "w");
if (file == nullptr) { if (file == nullptr) {
TT_LOG_E(TAG, "(%s) failed to open", configuration.name.c_str()); TT_LOG_E(TAG, "[%s] Open device failed", configuration.name.c_str());
return false; return false;
} }
@ -33,18 +33,16 @@ bool UartPosix::start() {
struct termios tty; struct termios tty;
if (tcgetattr(fileno(file), &tty) < 0) { if (tcgetattr(fileno(file), &tty) < 0) {
printf("(%s) tcgetattr failed: %s\n", configuration.name.c_str(), strerror(errno)); printf("[%s] tcgetattr failed: %s\n", configuration.name.c_str(), strerror(errno));
return false; return false;
} }
if (cfsetospeed(&tty, (speed_t)configuration.baudRate) == -1) { if (cfsetospeed(&tty, (speed_t)configuration.baudRate) == -1) {
TT_LOG_E(TAG, "(%s) failed to set output speed", configuration.name.c_str()); TT_LOG_E(TAG, "[%s] Setting output speed failed", configuration.name.c_str());
return false;
} }
if (cfsetispeed(&tty, (speed_t)configuration.baudRate) == -1) { if (cfsetispeed(&tty, (speed_t)configuration.baudRate) == -1) {
TT_LOG_E(TAG, "(%s) failed to set input speed", configuration.name.c_str()); TT_LOG_E(TAG, "[%s] Setting input speed failed", configuration.name.c_str());
return false;
} }
tty.c_cflag |= (CLOCAL | CREAD); /* ignore modem controls */ tty.c_cflag |= (CLOCAL | CREAD); /* ignore modem controls */
@ -63,13 +61,13 @@ bool UartPosix::start() {
tty.c_cc[VTIME] = 1; tty.c_cc[VTIME] = 1;
if (tcsetattr(fileno(file), TCSANOW, &tty) != 0) { if (tcsetattr(fileno(file), TCSANOW, &tty) != 0) {
printf("(%s) tcsetattr failed: %s\n", configuration.name.c_str(), strerror(errno)); printf("[%s] tcsetattr failed: %s\n", configuration.name.c_str(), strerror(errno));
return false; return false;
} }
device = std::move(new_device); device = std::move(new_device);
TT_LOG_I(TAG, "(%s) Started", configuration.name.c_str()); TT_LOG_I(TAG, "[%s] Started", configuration.name.c_str());
return true; return true;
} }
@ -78,13 +76,13 @@ bool UartPosix::stop() {
lock.lock(); lock.lock();
if (device == nullptr) { if (device == nullptr) {
TT_LOG_E(TAG, "(%s) Stopping: Not started", configuration.name.c_str()); TT_LOG_E(TAG, "[%s] Stopping: Not started", configuration.name.c_str());
return false; return false;
} }
device = nullptr; device = nullptr;
TT_LOG_I(TAG, "(%s) Stopped", configuration.name.c_str()); TT_LOG_I(TAG, "[%s] Stopped", configuration.name.c_str());
return true; return true;
} }
@ -141,7 +139,7 @@ void UartPosix::flushInput() {
uint32_t UartPosix::getBaudRate() { uint32_t UartPosix::getBaudRate() {
struct termios tty; struct termios tty;
if (tcgetattr(fileno(device.get()), &tty) < 0) { if (tcgetattr(fileno(device.get()), &tty) < 0) {
printf("(%s) tcgetattr failed: %s\n", configuration.name.c_str(), strerror(errno)); printf("[%s] tcgetattr failed: %s\n", configuration.name.c_str(), strerror(errno));
return false; return false;
} else { } else {
return (uint32_t)cfgetispeed(&tty); return (uint32_t)cfgetispeed(&tty);
@ -156,17 +154,17 @@ bool UartPosix::setBaudRate(uint32_t baudRate, TickType_t timeout) {
struct termios tty; struct termios tty;
if (tcgetattr(fileno(device.get()), &tty) < 0) { if (tcgetattr(fileno(device.get()), &tty) < 0) {
printf("(%s) tcgetattr failed: %s\n", configuration.name.c_str(), strerror(errno)); printf("[%s] tcgetattr failed: %s\n", configuration.name.c_str(), strerror(errno));
return false; return false;
} }
if (cfsetospeed(&tty, (speed_t)configuration.baudRate) == -1) { if (cfsetospeed(&tty, (speed_t)configuration.baudRate) == -1) {
TT_LOG_E(TAG, "(%s) failed to set output speed", configuration.name.c_str()); TT_LOG_E(TAG, "[%s] Failed to set output speed", configuration.name.c_str());
return false; return false;
} }
if (cfsetispeed(&tty, (speed_t)configuration.baudRate) == -1) { if (cfsetispeed(&tty, (speed_t)configuration.baudRate) == -1) {
TT_LOG_E(TAG, "(%s) failed to set input speed", configuration.name.c_str()); TT_LOG_E(TAG, "[%s] Failed to set input speed", configuration.name.c_str());
return false; return false;
} }