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?)
- 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.
- Use GPS time to set/update the current time
- Investigate EEZ Studio
- Remove flex_flow from app_container in Gui.cpp
# 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.
@ -66,3 +69,7 @@
- GPS app
- Investigate CSI https://stevenmhernandez.github.io/ESP32-CSI-Tool/
- 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);
/**
* 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.
*

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 {
constexpr TickType_t defaultLockTime = 500 / portTICK_PERIOD_MS;
/**
* LVGL locking function
* @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 power { extern const AppManifest manifest; }
namespace selectiondialog { extern const AppManifest manifest; }
namespace serialconsole { extern const AppManifest manifest; }
namespace settings { extern const AppManifest manifest; }
namespace systeminfo { extern const AppManifest manifest; }
namespace textviewer { extern const AppManifest manifest; }
@ -80,6 +81,7 @@ static void registerSystemApps() {
addApp(app::inputdialog::manifest);
addApp(app::launcher::manifest);
addApp(app::log::manifest);
addApp(app::serialconsole::manifest);
addApp(app::settings::manifest);
addApp(app::selectiondialog::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);
}
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 index = -1;
bundle.optInt32(RESULT_BUNDLE_KEY_INDEX, index);
@ -76,6 +84,7 @@ private:
lv_label_set_text(button_label, text.c_str());
lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
}
public:
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_align(message_label, LV_ALIGN_CENTER, 0, 0);
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;
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {
@ -107,21 +117,9 @@ public:
std::string 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);
if (labels.empty() || labels.front().empty()) {
TT_LOG_E(TAG, "No items provided");
setResult(Result::Error);
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++);
}
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;
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_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
toolbar->close_button = lv_button_create(obj);
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);
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_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
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);
instance->statusbarWidget = lvgl::statusbar_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);
lvgl::obj_set_style_bg_blacken(app_container);
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 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);
/**
* @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);
} // namespace

View File

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

View File

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

View File

@ -12,6 +12,7 @@ namespace tt {
bool Preferences::optBool(const std::string& key, bool& out) const {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
return false;
} else {
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 {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
return false;
} else {
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 {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
return false;
} else {
size_t out_size = 256;
@ -69,11 +72,11 @@ void Preferences::putBool(const std::string& key, bool value) {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) == 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);
} 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;
if (nvs_open(namespace_, NVS_READWRITE, &handle) == 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);
} 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_close(handle);
} 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) {
TT_LOG_I(TAG, "%s", buffer);
TT_LOG_D(TAG, "%s", buffer);
switch (minmea_sentence_id((char*)buffer, false)) {
case MINMEA_SENTENCE_RMC:

View File

@ -4,12 +4,15 @@
#include <Tactility/Mutex.h>
#include <ranges>
#include <cstring>
#ifdef ESP_PLATFORM
#include <esp_check.h>
#include "Tactility/TactilityHeadless.h"
#include "Tactility/hal/uart/UartEsp.h"
#include <esp_check.h>
#else
#include "Tactility/hal/uart/UartPosix.h"
#include <dirent.h>
#endif
#define TAG "uart"
@ -39,15 +42,8 @@ bool init(const std::vector<uart::Configuration>& configurations) {
}
bool Uart::writeString(const char* buffer, TickType_t timeout) {
while (*buffer != 0) {
if (writeBytes(reinterpret_cast<const std::byte*>(buffer), 1, timeout)) {
buffer++;
} else {
TT_LOG_E(TAG, "Failed to write - breaking off");
return false;
}
}
auto size = strlen(buffer);
writeBytes((std::byte*)buffer, size, timeout);
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) {
TT_LOG_I(TAG, "Open %s", name.c_str());
auto result = std::views::filter(uartEntries, [&name](auto& entry) {
return entry.configuration.name == name;
});
@ -123,10 +121,12 @@ std::unique_ptr<Uart> open(std::string name) {
auto uart = create(entry.configuration);
assert(uart != nullptr);
entry.usageId = uart->getId();
TT_LOG_I(TAG, "Opened %lu", entry.usageId);
return uart;
}
void close(uint32_t uartId) {
TT_LOG_I(TAG, "Close %lu", uartId);
auto result = std::views::filter(uartEntries, [&uartId](auto& entry) {
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() {

View File

@ -13,11 +13,13 @@
namespace tt::hal::uart {
bool UartEsp::start() {
TT_LOG_I(TAG, "[%s] Starting", configuration.name.c_str());
auto lock = mutex.asScopedLock();
lock.lock();
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;
}
@ -30,46 +32,53 @@ bool UartEsp::start() {
esp_err_t result = uart_param_config(configuration.port, &configuration.config);
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;
}
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);
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;
}
result = uart_driver_install(configuration.port, (int)configuration.rxBufferSize, (int)configuration.txBufferSize, 0, nullptr, intr_alloc_flags);
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;
}
started = true;
TT_LOG_I(TAG, "(%d) Started", configuration.port);
TT_LOG_I(TAG, "[%s] Started", configuration.name.c_str());
return true;
}
bool UartEsp::stop() {
TT_LOG_I(TAG, "[%s] Stopping", configuration.name.c_str());
auto lock = mutex.asScopedLock();
lock.lock();
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;
}
esp_err_t result = uart_driver_delete(configuration.port);
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;
}
started = false;
TT_LOG_I(TAG, "(%d) Stopped", configuration.port);
TT_LOG_I(TAG, "[%s] Stopped", configuration.name.c_str());
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) {
if (!mutex.lock(timeout)) {
auto lock = mutex.asScopedLock();
if (!lock.lock(timeout)) {
return false;
}

View File

@ -19,13 +19,13 @@ bool UartPosix::start() {
lock.lock();
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;
}
auto file = fopen(configuration.name.c_str(), "w");
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;
}
@ -33,18 +33,16 @@ bool UartPosix::start() {
struct termios tty;
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;
}
if (cfsetospeed(&tty, (speed_t)configuration.baudRate) == -1) {
TT_LOG_E(TAG, "(%s) failed to set output speed", configuration.name.c_str());
return false;
TT_LOG_E(TAG, "[%s] Setting output speed failed", configuration.name.c_str());
}
if (cfsetispeed(&tty, (speed_t)configuration.baudRate) == -1) {
TT_LOG_E(TAG, "(%s) failed to set input speed", configuration.name.c_str());
return false;
TT_LOG_E(TAG, "[%s] Setting input speed failed", configuration.name.c_str());
}
tty.c_cflag |= (CLOCAL | CREAD); /* ignore modem controls */
@ -63,13 +61,13 @@ bool UartPosix::start() {
tty.c_cc[VTIME] = 1;
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;
}
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;
}
@ -78,13 +76,13 @@ bool UartPosix::stop() {
lock.lock();
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;
}
device = nullptr;
TT_LOG_I(TAG, "(%s) Stopped", configuration.name.c_str());
TT_LOG_I(TAG, "[%s] Stopped", configuration.name.c_str());
return true;
}
@ -141,7 +139,7 @@ void UartPosix::flushInput() {
uint32_t UartPosix::getBaudRate() {
struct termios tty;
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;
} else {
return (uint32_t)cfgetispeed(&tty);
@ -156,17 +154,17 @@ bool UartPosix::setBaudRate(uint32_t baudRate, TickType_t timeout) {
struct termios tty;
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;
}
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;
}
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;
}