diff --git a/CMakeLists.txt b/CMakeLists.txt index 6be975ee..2232815f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) add_compile_definitions(LV_CONF_PATH="${LVGL_CONFIG_FULL_PATH}/lv_conf_kconfig.h") idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_panic_handler" APPEND) + idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_log_write" APPEND) file(READ version.txt TACTILITY_VERSION) add_compile_definitions(TT_VERSION="${TACTILITY_VERSION}") diff --git a/Documentation/pics/tactility-devices.webp b/Documentation/pics/tactility-devices.webp index 4dea1a38..17f7b550 100644 Binary files a/Documentation/pics/tactility-devices.webp and b/Documentation/pics/tactility-devices.webp differ diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 19ae635b..aaaaf295 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -43,11 +43,12 @@ namespace app { namespace desktop { extern const AppManifest manifest; } namespace files { extern const AppManifest manifest; } namespace gpio { extern const AppManifest manifest; } - namespace imageviewer { extern const AppManifest manifest; } namespace display { extern const AppManifest manifest; } namespace i2cscanner { extern const AppManifest manifest; } namespace i2csettings { extern const AppManifest manifest; } + namespace imageviewer { extern const AppManifest manifest; } namespace inputdialog { extern const AppManifest manifest; } + namespace log { extern const AppManifest manifest; } namespace power { extern const AppManifest manifest; } namespace selectiondialog { extern const AppManifest manifest; } namespace settings { extern const AppManifest manifest; } @@ -78,8 +79,9 @@ static const std::vector system_apps = { &app::gpio::manifest, &app::i2cscanner::manifest, &app::i2csettings::manifest, - &app::inputdialog::manifest, &app::imageviewer::manifest, + &app::inputdialog::manifest, + &app::log::manifest, &app::settings::manifest, &app::selectiondialog::manifest, &app::systeminfo::manifest, diff --git a/Tactility/Source/app/log/Log.cpp b/Tactility/Source/app/log/Log.cpp new file mode 100644 index 00000000..a518ed37 --- /dev/null +++ b/Tactility/Source/app/log/Log.cpp @@ -0,0 +1,150 @@ +#include +#include +#include +#include "lvgl.h" +#include "lvgl/Style.h" +#include "lvgl/Toolbar.h" +#include "app/selectiondialog/SelectionDialog.h" +#include "service/loader/Loader.h" +#include "lvgl/LvglSync.h" + +#define TAG "text_viewer" + +namespace tt::app::log { + +struct LogAppData { + LogLevel filterLevel = LogLevelInfo; + lv_obj_t* labelWidget = nullptr; +}; + +static bool shouldShowLog(LogLevel filterLevel, LogLevel logLevel) { + if (filterLevel == LogLevelNone || logLevel == LogLevelNone) { + return false; + } else { + return filterLevel >= logLevel; + } +} + +static void setLogEntries(lv_obj_t* label) { + auto app = service::loader::getCurrentApp(); + if (app == nullptr) { + return; + } + auto data = std::static_pointer_cast(app->getData()); + auto filterLevel = data->filterLevel; + + unsigned int index; + auto* entries = copyLogEntries(index); + std::stringstream buffer; + if (entries != nullptr) { + for (unsigned int i = index; i < TT_LOG_ENTRY_COUNT; ++i) { + if (shouldShowLog(filterLevel, entries[i].level)) { + buffer << entries[i].message; + } + } + if (index != 0) { + for (unsigned int i = 0; i < index; ++i) { + if (shouldShowLog(filterLevel, entries[i].level)) { + buffer << entries[i].message; + } + } + } + delete entries; + if (!buffer.str().empty()) { + lv_label_set_text(label, buffer.str().c_str()); + } else { + lv_label_set_text(label, "No logs for the selected log level"); + } + } else { + lv_label_set_text(label, "Failed to load log"); + } +} + +static void onLevelFilterPressed(TT_UNUSED lv_event_t* event) { + std::vector items = { + "Verbose", + "Debug", + "Info", + "Warning", + "Error", + }; + app::selectiondialog::start("Log Level", items); +} + +static void updateViews() { + auto app = service::loader::getCurrentApp(); + if (app == nullptr) { + return; + } + auto data = std::static_pointer_cast(app->getData()); + assert(data != nullptr); + + if (lvgl::lock(100 / portTICK_PERIOD_MS)) { + setLogEntries(data->labelWidget); + lvgl::unlock(); + } +} + +static void onShow(AppContext& app, lv_obj_t* parent) { + auto data = std::static_pointer_cast(app.getData()); + + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + auto* toolbar = lvgl::toolbar_create(parent, app); + lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressed, nullptr); + + auto* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lvgl::obj_set_style_no_padding(wrapper); + lvgl::obj_set_style_bg_invisible(wrapper); + + data->labelWidget = lv_label_create(wrapper); + lv_obj_align(data->labelWidget, LV_ALIGN_CENTER, 0, 0); + setLogEntries(data->labelWidget); +} + +static void onStart(AppContext& app) { + auto data = std::make_shared(); + app.setData(data); +} + +static void onResult(AppContext& app, Result result, const Bundle& bundle) { + auto resultIndex = selectiondialog::getResultIndex(bundle); + auto data = std::static_pointer_cast(app.getData()); + if (result == ResultOk) { + switch (resultIndex) { + case 0: + data->filterLevel = LogLevelVerbose; + break; + case 1: + data->filterLevel = LogLevelDebug; + break; + case 2: + data->filterLevel = LogLevelInfo; + break; + case 3: + data->filterLevel = LogLevelWarning; + break; + case 4: + data->filterLevel = LogLevelError; + break; + default: + break; + } + } + + updateViews(); +} + +extern const AppManifest manifest = { + .id = "Log", + .name = "Log", + .icon = LV_SYMBOL_LIST, + .type = TypeSystem, + .onStart = onStart, + .onShow = onShow, + .onResult = onResult +}; + +} // namespace diff --git a/TactilityCore/Source/Log.cpp b/TactilityCore/Source/Log.cpp index 0b463985..04c9c96b 100644 --- a/TactilityCore/Source/Log.cpp +++ b/TactilityCore/Source/Log.cpp @@ -1,9 +1,60 @@ +#include "Mutex.h" +#include + +namespace tt { + +static LogEntry* logEntries = nullptr; +static unsigned int nextLogEntryIndex; +static Mutex logMutex; + +static void ensureLogEntriesExist() { + if (logEntries == nullptr) { + logEntries = new LogEntry[TT_LOG_ENTRY_COUNT]; + assert(logEntries != nullptr); + nextLogEntryIndex = 0; + } +} + +static void storeLog(LogLevel level, const char* format, va_list args) { + if (logMutex.lock(5 / portTICK_PERIOD_MS)) { + ensureLogEntriesExist(); + + logEntries[nextLogEntryIndex].level = level; + vsnprintf(logEntries[nextLogEntryIndex].message, TT_LOG_MESSAGE_SIZE, format, args); + + nextLogEntryIndex++; + if (nextLogEntryIndex == TT_LOG_ENTRY_COUNT) { + nextLogEntryIndex = 0; + } + + logMutex.unlock(); + } +} + +LogEntry* copyLogEntries(unsigned int& outIndex) { + if (logMutex.lock(5 / portTICK_PERIOD_MS)) { + auto* newEntries = new LogEntry[TT_LOG_ENTRY_COUNT]; + assert(newEntries != nullptr); + for (int i = 0; i < TT_LOG_ENTRY_COUNT; ++i) { + memcpy(&newEntries[i], &logEntries[i], sizeof(LogEntry)); + } + outIndex = nextLogEntryIndex; + logMutex.unlock(); + return newEntries; + } else { + return nullptr; + } +} + +} // namespace tt + #ifndef ESP_PLATFORM #include "Log.h" #include #include +#include namespace tt { @@ -17,7 +68,7 @@ static char toPrefix(LogLevel level) { return 'I'; case LogLevelDebug: return 'D'; - case LogLevelTrace: + case LogLevelVerbose: return 'T'; default: return '?'; @@ -34,7 +85,7 @@ static const char* toColour(LogLevel level) { return "\033[32m"; case LogLevelDebug: return "\033[1;37m"; - case LogLevelTrace: + case LogLevelVerbose: return "\033[37m"; default: return ""; @@ -66,22 +117,38 @@ static uint64_t getTimestamp() { } void log(LogLevel level, const char* tag, const char* format, ...) { - printf( - "%s%c (%lu) %s: ", - toColour(level), - toPrefix(level), - getTimestamp(), - tag - ); + std::stringstream buffer; + buffer << toColour(level) << toPrefix(level) << " (" << getTimestamp() << ") " << tag << " " << format << "\033[0m\n"; va_list args; va_start(args, format); - vprintf(format, args); + vprintf(buffer.str().c_str(), args); va_end(args); - printf("\033[0m\n"); + va_start(args, format); + tt::storeLog(level, buffer.str().c_str(), args); + va_end(args); } } // namespace -#endif \ No newline at end of file +#else // ESP_PLATFORM + +#include + +extern "C" { + +extern void __real_esp_log_write(esp_log_level_t level, const char* tag, const char* format, ...); + +void __wrap_esp_log_write(esp_log_level_t level, const char* tag, const char* format, ...) { + va_list args; + va_start(args, format); + tt::storeLog((tt::LogLevel)level, format, args); + esp_log_writev(level, tag, format, args); + va_end(args); +} + +} + +#endif + diff --git a/TactilityCore/Source/Log.h b/TactilityCore/Source/Log.h index 8a8e354d..be5b5650 100644 --- a/TactilityCore/Source/Log.h +++ b/TactilityCore/Source/Log.h @@ -2,6 +2,34 @@ #include "LogMessages.h" +#if CONFIG_SPIRAM_USE_MALLOC == 1 or not defined(ESP_PLATFORM) +#define TT_LOG_ENTRY_COUNT 200 +#define TT_LOG_MESSAGE_SIZE 128 +#else +#define TT_LOG_ENTRY_COUNT 50 +#define TT_LOG_MESSAGE_SIZE 50 +#endif + +namespace tt { + +enum LogLevel { + LogLevelNone, /*!< No log output */ + LogLevelError, /*!< Critical errors, software module can not recover on its own */ + LogLevelWarning, /*!< Error conditions from which recovery measures have been taken */ + LogLevelInfo, /*!< Information messages which describe normal flow of events */ + LogLevelDebug, /*!< Extra information which is not necessary for normal use (values, pointers, sizes, etc). */ + LogLevelVerbose /*!< Bigger chunks of debugging information, or frequent messages which can potentially flood the output. */ +}; + +struct LogEntry { + LogLevel level = LogLevelNone; + char message[TT_LOG_MESSAGE_SIZE] = { 0 }; +}; + +LogEntry* copyLogEntries(unsigned int& outIndex); + +} // namespace tt + #ifdef ESP_TARGET #include "esp_log.h" #else @@ -26,14 +54,6 @@ namespace tt { -typedef enum { - LogLevelError, - LogLevelWarning, - LogLevelInfo, - LogLevelDebug, - LogLevelTrace -} LogLevel; - void log(LogLevel level, const char* tag, const char* format, ...); } // namespace