diff --git a/.gitmodules b/.gitmodules index 639bc79..90d9f6c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "components/espp"] path = components/espp url = git@github.com:esp-cpp/espp +[submodule "components/esp-protocols"] + path = components/esp-protocols + url = git@github.com:espressif/esp-protocols diff --git a/CMakeLists.txt b/CMakeLists.txt index 11ef4a9..33bc29a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,17 +8,16 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) set(EXTRA_COMPONENT_DIRS "components/" "components/espp/components/" + "components/esp-protocols/components" ) set( COMPONENTS - # TODO: add additional esp-idf and espp components you want to use to the line below: - "main esptool_py logger task" + "main esptool_py driver lwip button display display_drivers input_drivers logger lvgl mdns socket task tt21100 wifi gui" CACHE STRING "List of components to include" ) -# TODO: update this with your project's name -project(template) +project(wireless-debug-display) set(CMAKE_CXX_STANDARD 20) diff --git a/README.md b/README.md index 83e62cb..1cceccf 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,101 @@ -# ESP++ Template +# Wireless Debug Display + +This repository contains an example application designed for either +ESP32-WROVER-KIT or ESP32-S3-BOX (selectable via menuconfig) which listens on a +UDP socket for data. It then parses that data and if it matches a certain +format, it will plot the data in a graph, otherwise it will print the data to a +text log for display. + +https://github.com/esp-cpp/wireless-debug-display/assets/213467/f835922f-e17f-4f76-95ee-5d6585e84656 + +## Configuration + +You'll need to configure the build using `idf.py set-target ` +and then `idf.py menuconfig` to then set the `Wireless Debug Display +Configuration` which allows you to set which hardware you want to run it on, as +well as the WiFi Access Point connection information (ssid/password). It also +allows customization of the port of the UDP server that the debug display is +running. + +## Use + +This code receives string data from a UDP server. It will parse that string data +and determine which of the following three types of data it is: + +* *Commands*: contain the prefix (`+++`) in the string. +* *Plot data*: contain the delimiter (`::`) in the string followed by a + single value which can be converted successfully to a number. If the + conversion fails, the message will be printed as a log. +* *Log / text data*: all data that is not a command and cannot be + plotted. + +They are parsed in that priority order. + +Some example data (no commands) can be found in [test_data.txt](./test_data.txt). + +A couple python scripts are provided for sending data from a computer to your +logger to showcase simple UDP socket sending, as well as automatic service +discovery using mDNS. + +- [./send_to_display.py](./send_to_display.py): Uses simple UDP sockets to send + messages or a file to the debug display. +- [./send_to_display_mdns.py](./send_to_display_mdns.py): Uses python's + `zeroconf` package to discover the wireless display on the network and then + send messages or a file to the debug display. NOTE: zeroconf may not be + installed / accessible within the python environment used by ESP-IDF. + +## Sending Data to the Display + +This display is designed to receive data from any other device on the network, +though it is primarily intended for other embedded wireless devices such as +other ESP-based systems. However, I have provided some scripts to help show how +data can be sent from computers or other systems if you choose. + +Assuming that your computer is also on the network (you'll need to replace the +IP address below with the ip address displayed in the `info` page of the +display if you don't use the mDNS version): + +```console +# this python script uses mDNS to automatically find the display on the network +python ./send_to_display_mdns.py --file +python ./send_to_display_mdns.py --message "" --message "" ... +# e.g. +python ./send_to_display_mdns.py --file test_data.txt +python ./send_to_display_mdns.py --message "Hello world" --message "trace1::0" --message "trace1::1" --message "Goodbye World" + +# this python script uses raw UDP sockets to send data to the display on the network +python ./send_to_display.py --ip --port --file +python ./send_to_display.py --ip --port --message "" --message "" ... +# e.g. +python ./send_to_display.py --ip 192.168.1.23 --file additional_data.txt +python ./send_to_display.py --ip 192.168.1.23 --message "Hello world" --message "trace1::0" --message "trace1::1" --message "Goodbye World" +``` + +### Commands + +There are a limited set of commands in the system, which are +determined by a prefix and the command itself. If the prefix is found +_ANYWHERE_ in the string message (where messages are separated by +newlines), then the message is determined to be a command. -Template repository for building an ESP app with ESP++ (espp) components and -ESP-IDF components. +**PREFIX:** `+++` - three plus characters in a row -## Development +* **Remove Plot:** this command (`RP:` followed by the string plot name) will remove the named plot from the graph. +* **Clear Plots:** this command (`CP`) will remove _all_ plots from the graph. +* **Clear Logs:** this command (`CL`) will remove _all_ logs / text. -This repository is designed to be used as a template repository - so you can -sepcify this as the template repository type when creating a new repository on -GitHub. +### Plotting -After setting this as the template, make sure to update the following: -- [This README](./README.md) to contain the relevant description and images of your project -- The [./CMakeLists.txt](./CMakeLists.txt) file to have the components that you - want to use (and any you may have added to the [components - folder](./components)) as well as to update the project name -- The [./main/main.cpp](./main/main.cpp) To run the main code for your app. The - [main folder](./main) is also where you can put additional header and source - files that you don't think belong in their own components but help keep the - main code clean. +Messages which contain the string `::` and which have a value that +successfully and completely converts into a number are determined to +be a plot. Plots are grouped by their name, which is any string +preceding the `::`. + +### Logging + +All other text is treated as a log and written out to the log +window. Note, we do not wrap lines, so any text that would go off the +edge of the screen is simply not rendered. ## Cloning @@ -51,6 +129,22 @@ See the Getting Started Guide for full steps to configure and use ESP-IDF to bui ## Output -Example screenshot of the console output from this app: +### Console Logs: +![initial output](https://github.com/esp-cpp/wireless-debug-display/assets/213467/c20993a7-9873-4c76-bc8e-1b115f63a5e0) +![receiving more info](https://github.com/esp-cpp/wireless-debug-display/assets/213467/0413e79e-018c-497e-b9d7-511481d17385) + +### Python script: +![python script](https://github.com/esp-cpp/wireless-debug-display/assets/213467/9d5d4899-3074-47b1-8d57-1ef22aa4bfba) + +### ESP32-WROVER-KIT + +https://github.com/esp-cpp/wireless-debug-display/assets/213467/395400f6-e677-464c-a258-df06049cc562 + +### ESP32-S3-BOX + +![image](https://github.com/esp-cpp/wireless-debug-display/assets/213467/5aa28996-4ad7-4dbc-bc00-756ecd7ec736) +![image](https://github.com/esp-cpp/wireless-debug-display/assets/213467/2c75f6dc-4528-4663-ae12-f894ec2bcdc9) +![image](https://github.com/esp-cpp/wireless-debug-display/assets/213467/e59536a1-da8c-40fb-9f37-fdfdfb2d5b52) + +https://github.com/esp-cpp/wireless-debug-display/assets/213467/f835922f-e17f-4f76-95ee-5d6585e84656 -![CleanShot 2023-07-12 at 14 01 21](https://github.com/esp-cpp/template/assets/213467/7f8abeae-121b-4679-86d8-7214a76f1b75) diff --git a/additional_data.txt b/additional_data.txt new file mode 100644 index 0000000..50db92b --- /dev/null +++ b/additional_data.txt @@ -0,0 +1,44 @@ +Some additional logs! +This is some more logging +#FF00FF wow# there really are a lot of logs here +you really can't believe it can you? +#FF0000 Hopefully# at some point +I'll be able to stop typing +and I'll have gotten to +the bottom of the logs... +t0::5 +t0::7 +t0::35 +t0::76 +t0::32 +t0::11 +t0::15 +t0::13 +t0::5 +t0::0 +t0::10 +t0::-31 +t0::-75 +t0::3 +t0::1 +t0::1 +t0::3 +t0::5 +r0::0 +r0::10 +r0::3 +r0::5 +r0::3 +r0::1 +r0::1 +r0::3 +r0::5 +r0::0 +r0::10 +r0::30 +r0::5 +r0::3 +r0::1 +r0::-10 +r0::-30 +r0::5 diff --git a/components/esp-protocols b/components/esp-protocols new file mode 160000 index 0000000..12bacdc --- /dev/null +++ b/components/esp-protocols @@ -0,0 +1 @@ +Subproject commit 12bacdc3a01121ca97eeb3988e2faf38c7563472 diff --git a/components/gui/CMakeLists.txt b/components/gui/CMakeLists.txt new file mode 100644 index 0000000..a083b90 --- /dev/null +++ b/components/gui/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRC_DIRS "src" + INCLUDE_DIRS "include" + REQUIRES task display logger) diff --git a/components/gui/include/converter.hpp b/components/gui/include/converter.hpp new file mode 100644 index 0000000..17806f6 --- /dev/null +++ b/components/gui/include/converter.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include + +class Converter { +public: + enum class Status { Success, Overflow, Underflow, Inconvertible }; + static Status str2int(int &i, char const *s, int base = 0); +}; diff --git a/components/gui/include/graph_window.hpp b/components/gui/include/graph_window.hpp new file mode 100644 index 0000000..9a0ad79 --- /dev/null +++ b/components/gui/include/graph_window.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include "window.hpp" + +class GraphWindow : public Window { +public: + void init(lv_obj_t *parent, size_t width, size_t height) override; + void update() override; + + void clear_plots ( void ); + void add_data ( const std::string& plot_name, int new_data ); + void remove_plot ( const std::string& plot_name ); + +protected: + lv_chart_series_t* create_plot ( const std::string& plotName ); + lv_chart_series_t* get_plot ( const std::string& plotName ); + + void update_ticks ( void ); + +private: + lv_obj_t *chart_; + lv_obj_t *legend_; + std::string y_ticks_; + std::unordered_map plot_map_; +}; diff --git a/components/gui/include/gui.hpp b/components/gui/include/gui.hpp new file mode 100644 index 0000000..53ff7dd --- /dev/null +++ b/components/gui/include/gui.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "converter.hpp" +#include "display.hpp" +#include "task.hpp" +#include "logger.hpp" +#include "text_window.hpp" +#include "graph_window.hpp" + +class Gui { +public: + const std::string delimeter_data = "::"; ///< Delimeter indicating this contains plottable data + const std::string delimeter_command = "+++"; ///< Delimeter indicating this contains a command + const std::string command_remove_plot = "RP:"; ///< Command: remove plot + const std::string command_clear_plots = "CP"; ///< Command: clear plots + const std::string command_clear_logs = "CL"; ///< Command: clear logs + + struct Config { + std::shared_ptr display; + espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; + }; + + explicit Gui(const Config& config) + : display_(config.display) + , logger_({.tag = "Gui", .level = config.log_level}) { + init_ui(); + // now start the gui updater task + using namespace std::placeholders; + task_ = espp::Task::make_unique({ + .name = "Gui Task", + .callback = std::bind(&Gui::update, this, _1, _2), + .stack_size_bytes = 6 * 1024 + }); + task_->start(); + } + + ~Gui() { + task_->stop(); + deinit_ui(); + } + + void switch_tab(); + + void push_data(const std::string& data); + std::string pop_data(); + + void clear_info(); + void add_info(const std::string& info); + + bool handle_data(); + +protected: + void init_ui(); + void deinit_ui(); + + bool update(std::mutex& m, std::condition_variable& cv) { + { + std::lock_guard lk(mutex_); + lv_task_handler(); + } + { + using namespace std::chrono_literals; + std::unique_lock lk(m); + cv.wait_for(lk, 16ms); + } + // don't want to stop the task + return false; + } + + static void event_callback(lv_event_t *e) { + lv_event_code_t event_code = lv_event_get_code(e); + auto user_data = lv_event_get_user_data(e); + auto gui = static_cast(user_data); + if (!gui) { + return; + } + switch (event_code) { + case LV_EVENT_SHORT_CLICKED: + break; + case LV_EVENT_PRESSED: + gui->on_pressed(e); + break; + case LV_EVENT_VALUE_CHANGED: + // gui->on_value_changed(e); + break; + case LV_EVENT_LONG_PRESSED: + break; + case LV_EVENT_KEY: + break; + default: + break; + } + } + + void on_pressed(lv_event_t *e); + + GraphWindow plot_window_; + TextWindow log_window_; + TextWindow info_window_; + lv_obj_t *tabview_; + + std::mutex data_queue_mutex_; + std::queue data_queue_; + + std::shared_ptr display_; + std::unique_ptr task_; + + espp::Logger logger_; + std::recursive_mutex mutex_; +}; diff --git a/components/gui/include/text_window.hpp b/components/gui/include/text_window.hpp new file mode 100644 index 0000000..5e1ebfc --- /dev/null +++ b/components/gui/include/text_window.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include "window.hpp" + +class TextWindow : public Window { +public: + + void init ( lv_obj_t *parent, size_t width, size_t height ) override; + + void clear_logs( void ); + void add_log ( const std::string& log_text ); + +private: + std::string log_text_; + lv_obj_t *log_container_; +}; diff --git a/components/gui/include/window.hpp b/components/gui/include/window.hpp new file mode 100644 index 0000000..3c90cca --- /dev/null +++ b/components/gui/include/window.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "lvgl.h" + +class Window { +public: + virtual void init ( lv_obj_t *parent, size_t width, size_t height ) { + parent_ = parent; + width_ = width; + height_ = height; + } + virtual void clear ( void ) { } + virtual void update ( void ) { } + +protected: + size_t width_; + size_t height_; + lv_obj_t* parent_; +}; diff --git a/components/gui/src/converter.cpp b/components/gui/src/converter.cpp new file mode 100644 index 0000000..38318b3 --- /dev/null +++ b/components/gui/src/converter.cpp @@ -0,0 +1,19 @@ +#include "converter.hpp" + +Converter::Status Converter::str2int(int &i, char const *s, int base) { + char *end; + long l; + errno = 0; + l = strtol(s, &end, base); + if ((errno == ERANGE && l == LONG_MAX) || l > INT_MAX) { + return Status::Overflow; + } + if ((errno == ERANGE && l == LONG_MIN) || l < INT_MIN) { + return Status::Underflow; + } + if (*s == '\0' || *end != '\0') { + return Status::Inconvertible; + } + i = l; + return Status::Success; +} diff --git a/components/gui/src/graph_window.cpp b/components/gui/src/graph_window.cpp new file mode 100644 index 0000000..8013dfa --- /dev/null +++ b/components/gui/src/graph_window.cpp @@ -0,0 +1,139 @@ +#include "graph_window.hpp" + +void GraphWindow::init(lv_obj_t *parent, size_t width, size_t height) { + Window::init(parent, width, height); + // Create a chart + chart_ = lv_chart_create(parent_); + + // we need to make the width of the chart less than the parent full width to + // leave room for tick labels + lv_obj_set_width(chart_, lv_pct(90)); + lv_obj_set_height(chart_, lv_pct(100)); + + // the y axis labels are on the left of the chart, so align the chart on the + // right side of the parent to leave room for tick labels + lv_obj_align(chart_, LV_ALIGN_RIGHT_MID, 0, 0); + + // Show lines and points too + lv_chart_set_type(chart_, LV_CHART_TYPE_LINE); + + // update the tick values + size_t major_tick_length = 5; + size_t minor_tick_length = 2; + size_t major_tick_count = 5; + size_t minor_tick_count = 2; + bool label_enabled = true; + size_t draw_size = 50; + lv_chart_set_axis_tick(chart_, LV_CHART_AXIS_PRIMARY_Y, + major_tick_length, + minor_tick_length, + major_tick_count, + minor_tick_count, + label_enabled, + draw_size); + + // create the legend + legend_ = lv_label_create(chart_); + lv_obj_align(legend_, LV_ALIGN_TOP_RIGHT, - 7, 5); + lv_label_set_text(legend_, ""); + + // create a style for the chart + // give some padding to the chart - especially on the left where we + // have the y axis labels / ticks. + lv_obj_set_style_border_width(chart_, 0, 0); + lv_obj_set_style_pad_left(chart_, 60, 0); + lv_obj_set_style_pad_bottom(chart_, 36, 0); + lv_obj_set_style_pad_right(chart_, 25, 0); + lv_obj_set_style_pad_top(chart_, 20, 0); +} + +void GraphWindow::update_ticks() { + + // get the minimum and maximum + lv_coord_t min=0, max=0; + auto num_points = lv_chart_get_point_count(chart_); + for (auto e : plot_map_) { + auto series = e.second; + for (size_t i=0; i < num_points; i++) { + auto point = series->y_points[i]; + if (point < min) min = point; + if (point > max) max = point; + } + } + + // update the chart range + lv_chart_set_range(chart_, LV_CHART_AXIS_PRIMARY_Y, min, max); +} + +void GraphWindow::update() { + // make sure if we have new data we update the range & tick values + update_ticks(); + // update the chart if we use our own data arrays or modify the data + // ourselves + lv_chart_refresh(chart_); +} + +void GraphWindow::clear_plots( void ) { + // remove all the series from the chart + for (auto e : plot_map_) { + lv_chart_remove_series(chart_, e.second); + } + // now clear the map + plot_map_.clear(); + // clear the legend + lv_label_set_text(legend_, ""); +} + +void GraphWindow::add_data( const std::string& plotName, int newData ) { + auto plot = get_plot( plotName ); + // couldn't find the plot so create a new one + if ( plot == nullptr ) + plot = create_plot( plotName ); + // now add the data + lv_chart_set_next_value(chart_, plot, newData); +} + +lv_chart_series_t* GraphWindow::create_plot( const std::string& plotName ) { + // make a random color + uint8_t red = rand() % 256; + uint8_t green = rand() % 256; + uint8_t blue = rand() % 256; + auto color = lv_color_make(red, green, blue); + // now make the plot + auto plot = lv_chart_add_series(chart_, color, LV_CHART_AXIS_PRIMARY_Y); + + //Add text to legend, #XXX # will be a colored string, if we set the + //recolor property to true on the label + char label_buf[50]; + sprintf(label_buf, "#%02X%02X%02X - %s#\n", red, green, blue, plotName.c_str()); + char *current_legend = lv_label_get_text(legend_); + auto new_text = std::string(current_legend); + new_text += label_buf; + lv_label_set_text(legend_, new_text.c_str()); + lv_label_set_recolor(legend_, true); + + // add it to the map + plot_map_[plotName] = plot; + // and return it + return plot; +} + +void GraphWindow::remove_plot( const std::string& plotName ) { + auto plot = get_plot(plotName); + if (plot != nullptr) { + // we should remove it from the display + lv_chart_remove_series(chart_, plot); + // and delete the value from the map + plot_map_.erase(plotName); + } +} + +lv_chart_series_t* GraphWindow::get_plot( const std::string& plotName ) { + lv_chart_series_t* plot = nullptr; + auto search = plot_map_.find(plotName); + if (search != plot_map_.end()) { + // set the plot to be the value of the element + plot = search->second; + } + return plot; +} diff --git a/components/gui/src/gui.cpp b/components/gui/src/gui.cpp new file mode 100644 index 0000000..2c2e05b --- /dev/null +++ b/components/gui/src/gui.cpp @@ -0,0 +1,158 @@ +#include "gui.hpp" + +using namespace espp; +using namespace std::chrono_literals; + +void Gui::deinit_ui() { + lv_obj_del(tabview_); +} + +void Gui::init_ui() { + // Initialize the GUI + const auto tab_location = LV_DIR_TOP; + const size_t tab_height = 20; + tabview_ = lv_tabview_create(lv_scr_act(), tab_location, tab_height); + + // create the plotting tab and hide the scrollbars + auto plot_tab = lv_tabview_add_tab(tabview_, "Plots"); + // lv_obj_set_scrollbar_mode(plot_tab, LV_SCROLLBAR_MODE_OFF); + plot_window_.init(plot_tab, display_->width(), display_->height()); + + // create the logging tab and hide the scrollbars + auto log_tab = lv_tabview_add_tab(tabview_, "Logs"); + // lv_obj_set_scrollbar_mode(log_tab, LV_SCROLLBAR_MODE_OFF); + log_window_.init(log_tab, display_->width(), display_->height()); + + // create the info tab and hide the scrollbars + auto info_tab = lv_tabview_add_tab(tabview_, "Info"); + // lv_obj_set_scrollbar_mode(info_tab, LV_SCROLLBAR_MODE_OFF); + info_window_.init(info_tab, display_->width(), display_->height()); + + // rom screen navigation + // lv_obj_add_event_cb(ui_settingsbutton, &Gui::event_callback, LV_EVENT_PRESSED, static_cast(this)); + // lv_obj_add_event_cb(ui_playbutton, &Gui::event_callback, LV_EVENT_PRESSED, static_cast(this)); +} + +void Gui::switch_tab() { + std::lock_guard lk{mutex_}; + auto num_tabs = ((lv_tabview_t*)tabview_)->tab_cnt; + auto active_tab = lv_tabview_get_tab_act(tabview_); + auto next_tab = (active_tab + 1) % num_tabs; + lv_tabview_set_act(tabview_, next_tab, LV_ANIM_ON); +} + +void Gui::push_data(const std::string& data) { + std::unique_lock lock{data_queue_mutex_}; + data_queue_.push(data); +} + +std::string Gui::pop_data() { + std::unique_lock lock{data_queue_mutex_}; + std::string data{""}; + if (!data_queue_.empty()) { + data = data_queue_.front(); + data_queue_.pop(); + } + return data; +} + +void Gui::clear_info() { + std::lock_guard lk{mutex_}; + info_window_.clear_logs(); +} + +void Gui::add_info(const std::string& info) { + std::lock_guard lk{mutex_}; + info_window_.add_log(info); +} + +bool Gui::handle_data() { + // lock the display + std::lock_guard lk{mutex_}; + + bool hasNewPlotData = false; + bool hasNewTextData = false; + + std::string newData = pop_data(); + int len = newData.length(); + if(len > 0) { + size_t num_lines = 0; + // logger_.info("parsing input '{}'", newData); + // have data, parse here + std::stringstream ss(newData); + std::string line; + while (std::getline(ss, line, '\n')) { + num_lines++; + size_t pos = 0; + // parse for commands + if ( (pos = line.find(delimeter_command)) != std::string::npos) { + std::string command; + std::string plotName; + command = line.substr(pos + delimeter_command.length(), line.length()); + if (command == command_clear_logs) { + log_window_.clear_logs(); + // make sure we transition to the next state + hasNewTextData = true; + } + else if (command == command_clear_plots) { + plot_window_.clear_plots(); + // make sure we transition to the next state + hasNewPlotData = true; + } + else if ( (pos = line.find(command_remove_plot)) != std::string::npos) { + plotName = line.substr(pos + command_remove_plot.length(), line.length()); + plot_window_.remove_plot( plotName ); + // make sure we transition to the next state + hasNewPlotData = true; + } + } + else { + // parse for data + if ( (pos = line.find(delimeter_data)) != std::string::npos) { + // found "::" so we have a plot data + std::string plotName; + std::string value; + plotName = line.substr(0, pos); + pos = pos + delimeter_data.length(); + if ( pos < line.length() ) { + int iValue; + value = line.substr(pos, line.length()); + if (Converter::str2int(iValue, value.c_str()) == Converter::Status::Success) { + // make sure we transition to the next state + plot_window_.add_data( plotName, iValue ); + hasNewPlotData = true; + } else { + logger_.warn("has '::', but could not convert to number, adding log '{}'", line); + // couldn't find that, so we just have text data + log_window_.add_log( line ); + // make sure we transition to the next state + hasNewTextData = true; + } + } + else { + // couldn't find that, so we just have text data + log_window_.add_log( line ); + // make sure we transition to the next state + hasNewTextData = true; + } + } + else { + // couldn't find that, so we just have text data + log_window_.add_log( line ); + // make sure we transition to the next state + hasNewTextData = true; + } + } + } + // logger_.info("parsed {} lines", num_lines); + } + if (hasNewPlotData) { + plot_window_.update(); + } + return hasNewPlotData || hasNewTextData; +} + +void Gui::on_pressed(lv_event_t *e) { + lv_obj_t * target = lv_event_get_target(e); + logger_.info("PRESSED: {}", fmt::ptr(target)); +} diff --git a/components/gui/src/text_window.cpp b/components/gui/src/text_window.cpp new file mode 100644 index 0000000..04e1115 --- /dev/null +++ b/components/gui/src/text_window.cpp @@ -0,0 +1,30 @@ +#include "text_window.hpp" + +void TextWindow::init( lv_obj_t *parent, size_t width, size_t height ) { + Window::init(parent, width, height); + log_container_ = lv_label_create(parent_); + lv_label_set_recolor(log_container_, true); + + // wrap text on long lines + lv_label_set_long_mode(log_container_, LV_LABEL_LONG_WRAP); + + // log should span the width of the screen + lv_obj_set_width(log_container_, lv_pct(100)); + lv_obj_set_height(log_container_, lv_pct(100)); +} + +void TextWindow::clear_logs( void ) { + // clear all the logs off the page + lv_obj_clean(log_container_); + // now empty the string + log_text_.clear(); +} + +void TextWindow::add_log( const std::string& log_text ) { + // now add to our string for storage + log_text_ += "\n" + log_text; + // set the string to the display + lv_label_set_text(log_container_, log_text_.c_str()); + // make sure the most recent logs are shown + lv_obj_scroll_to_y(log_container_, LV_COORD_MAX, LV_ANIM_OFF); +} diff --git a/components/tt21100/CMakeLists.txt b/components/tt21100/CMakeLists.txt new file mode 100644 index 0000000..17999ce --- /dev/null +++ b/components/tt21100/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + INCLUDE_DIRS "include" + REQUIRES "logger" + ) diff --git a/components/tt21100/include/tt21100.hpp b/components/tt21100/include/tt21100.hpp new file mode 100644 index 0000000..16e9d12 --- /dev/null +++ b/components/tt21100/include/tt21100.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include + +#include "logger.hpp" + +class Tt21100 { +public: + static constexpr uint8_t ADDRESS = (0x24); + + typedef std::function read_fn; + + struct Config { + read_fn read; + espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; /**< Log verbosity for the input driver. */ + }; + + Tt21100(const Config& config) + : read_(config.read), + logger_({.tag = "Tt21100", .level = config.log_level}) { + init(); + } + + bool read() { + static uint16_t data_len; + static uint8_t data[256]; + + read_(ADDRESS, (uint8_t*)&data_len, sizeof(data_len)); + logger_.debug("Data length: {}", data_len); + + if (data_len == 0xff) { + return false; + } + + read_(ADDRESS, data, data_len); + switch (data_len) { + case 2: + // no available data + break; + case 7: + case 17: + case 27: { + // touch event - NOTE: this only gets the first touch record + auto report_data = (TouchReport*) data; + auto touch_data = (TouchRecord*)(&report_data->touch_record[0]); + x_ = touch_data->x; + y_ = touch_data->y; + num_touch_points_ = (data_len - sizeof(TouchReport)) / sizeof(TouchRecord); + logger_.debug("Touch event: #={}, [0]=({}, {})", num_touch_points_, x_, y_); + break; + } + case 14: { + // button event + auto button_data = (ButtonRecord*)data; + home_button_pressed_ = button_data->btn_val; + auto btn_signal = button_data->btn_signal[0]; + logger_.debug("Button event({}): {}, {}", (int)(button_data->length), home_button_pressed_, btn_signal); + break; + } + default: + break; + } + return true; + } + + uint8_t get_num_touch_points() { + return num_touch_points_; + } + + void get_touch_point(uint8_t *num_touch_points, uint16_t *x, uint16_t *y) { + *num_touch_points = get_num_touch_points(); + if (*num_touch_points != 0) { + *x = x_; + *y = y_; + logger_.info("Got touch ({}, {})", *x, *y); + } + } + + uint8_t get_home_button_state() { + return home_button_pressed_; + } + +protected: + void init() { + uint16_t reg_val = 0; + do { + using namespace std::chrono_literals; + read_(ADDRESS, (uint8_t*)®_val, sizeof(reg_val)); + std::this_thread::sleep_for(20ms); + } while (0x0002 != reg_val); + } + + enum class Registers : uint8_t { + TP_NUM = 0x01, + X_POS = 0x02, + Y_POS = 0x03, + }; + + struct TouchRecord { + uint8_t :5; + uint8_t touch_type:3; + uint8_t tip:1; + uint8_t event_id:2; + uint8_t touch_id:5; + uint16_t x; + uint16_t y; + uint8_t pressure; + uint16_t major_axis_length; + uint8_t orientation; + } __attribute__((packed)); + + struct TouchReport { + uint16_t data_len; + uint8_t report_id; + uint16_t time_stamp; + uint8_t :2; + uint8_t large_object : 1; + uint8_t record_num : 5; + uint8_t report_counter:2; + uint8_t :3; + uint8_t noise_efect:3; + TouchRecord touch_record[0]; + } __attribute__((packed)); + + struct ButtonRecord { + uint16_t length; /*!< Always 14(0x000E) */ + uint8_t report_id; /*!< Always 03h */ + uint16_t time_stamp; /*!< Number in units of 100 us */ + uint8_t btn_val; /*!< Only use bit[0..3] */ + uint16_t btn_signal[4]; + } __attribute__((packed)); + + read_fn read_; + std::atomic home_button_pressed_{false}; + std::atomic num_touch_points_; + std::atomic x_; + std::atomic y_; + espp::Logger logger_; +}; diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..5fb266e --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,38 @@ +menu "Wireless Debug Display Configuration" + + choice + prompt "Hardware Configuration" + default HARDWARE_BOX + help + Select the dev-kit / hardware you're using. + config HARDWARE_WROVER_KIT + bool "ESP32 WROVER KIT V4" + config HARDWARE_BOX + bool "ESP BOX" + endchoice + + config DEBUG_SERVER_PORT + int "Debug Display Server Port" + default 5555 + help + The port number of the wireless debug display's udp server + + config ESP_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the camera streamer to connect to. + + config ESP_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the camera streamer to use. + + config ESP_MAXIMUM_RETRY + int "Maximum retry" + default 5 + help + Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent. + +endmenu diff --git a/main/main.cpp b/main/main.cpp index ad45eea..31e07c4 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1,11 +1,173 @@ +#include + #include #include +#include +#include +#include +#if CONFIG_ESP32_WIFI_NVS_ENABLED +#include +#endif + +#include "display.hpp" + +#if CONFIG_HARDWARE_WROVER_KIT +#include "ili9341.hpp" +#include "button.hpp" +static constexpr int DC_PIN_NUM = 21; +#elif CONFIG_HARDWARE_BOX +#include +#include "st7789.hpp" +#include "touchpad_input.hpp" +#include "tt21100.hpp" +static constexpr int DC_PIN_NUM = 4; +#else +#error "Misconfigured hardware!" +#endif + #include "logger.hpp" #include "task.hpp" +#include "gui.hpp" +#include "tcp_socket.hpp" +#include "udp_socket.hpp" +#include "wifi_sta.hpp" using namespace std::chrono_literals; +static spi_device_handle_t spi; +static const int spi_queue_size = 7; +static size_t num_queued_trans = 0; + +// the user flag for the callbacks does two things: +// 1. Provides the GPIO level for the data/command pin, and +// 2. Sets some bits for other signaling (such as LVGL FLUSH) +static constexpr int FLUSH_BIT = (1 << (int)espp::display_drivers::Flags::FLUSH_BIT); +static constexpr int DC_LEVEL_BIT = (1 << (int)espp::display_drivers::Flags::DC_LEVEL_BIT); + +// This function is called (in irq context!) just before a transmission starts. +// It will set the D/C line to the value indicated in the user field +// (DC_LEVEL_BIT). +static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { + uint32_t user_flags = (uint32_t)(t->user); + bool dc_level = user_flags & DC_LEVEL_BIT; + gpio_set_level((gpio_num_t)DC_PIN_NUM, dc_level); +} + +// This function is called (in irq context!) just after a transmission ends. It +// will indicate to lvgl that the next flush is ready to be done if the +// FLUSH_BIT is set. +static void IRAM_ATTR lcd_spi_post_transfer_callback(spi_transaction_t *t) { + uint16_t user_flags = (uint32_t)(t->user); + bool should_flush = user_flags & FLUSH_BIT; + if (should_flush) { + lv_disp_t *disp = _lv_refr_get_disp_refreshing(); + lv_disp_flush_ready(disp->driver); + } +} + +extern "C" void IRAM_ATTR lcd_write(const uint8_t *data, size_t length, uint32_t user_data) { + if (length == 0) { + return; + } + static spi_transaction_t t; + memset(&t, 0, sizeof(t)); + t.length = length * 8; + t.tx_buffer = data; + t.user = (void *)user_data; + spi_device_polling_transmit(spi, &t); +} + +static void lcd_wait_lines() { + spi_transaction_t *rtrans; + esp_err_t ret; + // Wait for all transactions to be done and get back the results. + while (num_queued_trans) { + // fmt::print("Waiting for {} lines\n", num_queued_trans); + ret = spi_device_get_trans_result(spi, &rtrans, portMAX_DELAY); + if (ret != ESP_OK) { + fmt::print("Could not get trans result: {} '{}'\n", ret, esp_err_to_name(ret)); + } + num_queued_trans--; + // We could inspect rtrans now if we received any info back. The LCD is treated as write-only, + // though. + } +} + +void IRAM_ATTR lcd_send_lines(int xs, int ys, int xe, int ye, const uint8_t *data, + uint32_t user_data) { + // if we haven't waited by now, wait here... + lcd_wait_lines(); + esp_err_t ret; + // Transaction descriptors. Declared static so they're not allocated on the stack; we need this + // memory even when this function is finished because the SPI driver needs access to it even while + // we're already calculating the next line. + static spi_transaction_t trans[6]; + // In theory, it's better to initialize trans and data only once and hang on to the initialized + // variables. We allocate them on the stack, so we need to re-init them each call. + for (int i = 0; i < 6; i++) { + memset(&trans[i], 0, sizeof(spi_transaction_t)); + if ((i & 1) == 0) { + // Even transfers are commands + trans[i].length = 8; + trans[i].user = (void *)0; + } else { + // Odd transfers are data + trans[i].length = 8 * 4; + trans[i].user = (void *)DC_LEVEL_BIT; + } + trans[i].flags = SPI_TRANS_USE_TXDATA; + } + size_t length = (xe - xs + 1) * (ye - ys + 1) * 2; +#if CONFIG_HARDWARE_WROVER_KIT + trans[0].tx_data[0] = (uint8_t)espp::Ili9341::Command::caset; +#endif +#if CONFIG_HARDWARE_BOX + trans[0].tx_data[0] = (uint8_t)espp::St7789::Command::caset; +#endif + trans[1].tx_data[0] = (xs) >> 8; + trans[1].tx_data[1] = (xs)&0xff; + trans[1].tx_data[2] = (xe) >> 8; + trans[1].tx_data[3] = (xe)&0xff; +#if CONFIG_HARDWARE_WROVER_KIT + trans[2].tx_data[0] = (uint8_t)espp::Ili9341::Command::raset; +#endif +#if CONFIG_HARDWARE_BOX + trans[2].tx_data[0] = (uint8_t)espp::St7789::Command::raset; +#endif + trans[3].tx_data[0] = (ys) >> 8; + trans[3].tx_data[1] = (ys)&0xff; + trans[3].tx_data[2] = (ye) >> 8; + trans[3].tx_data[3] = (ye)&0xff; +#if CONFIG_HARDWARE_WROVER_KIT + trans[4].tx_data[0] = (uint8_t)espp::Ili9341::Command::ramwr; +#endif +#if CONFIG_HARDWARE_BOX + trans[4].tx_data[0] = (uint8_t)espp::St7789::Command::ramwr; +#endif + trans[5].tx_buffer = data; + trans[5].length = length * 8; + // undo SPI_TRANS_USE_TXDATA flag + trans[5].flags = 0; + // we need to keep the dc bit set, but also add our flags + trans[5].user = (void *)(DC_LEVEL_BIT | user_data); + // Queue all transactions. + for (int i = 0; i < 6; i++) { + ret = spi_device_queue_trans(spi, &trans[i], portMAX_DELAY); + if (ret != ESP_OK) { + fmt::print("Couldn't queue trans: {} '{}'\n", ret, esp_err_to_name(ret)); + } else { + num_queued_trans++; + } + } + // When we are here, the SPI driver is busy (in the background) getting the + // transactions sent. That happens mostly using DMA, so the CPU doesn't have + // much to do here. We're not going to wait for the transaction to finish + // because we may as well spend the time calculating the next line. When that + // is done, we can call send_line_finish, which will wait for the transfers + // to be done and check their status. +} + extern "C" void app_main(void) { static auto start = std::chrono::high_resolution_clock::now(); static auto elapsed = [&]() { @@ -13,27 +175,271 @@ extern "C" void app_main(void) { return std::chrono::duration(now - start).count(); }; - espp::Logger logger({.tag = "Template", .level = espp::Logger::Verbosity::DEBUG}); + espp::Logger logger({.tag = "WirelessDebugDisplay", .level = espp::Logger::Verbosity::DEBUG}); logger.info("Bootup"); - // make a simple task that prints "Hello World!" every second - espp::Task task({ - .name = "Hello World", - .callback = [&](auto &m, auto &cv) -> bool { - logger.debug("[{:.3f}] Hello from the task!", elapsed()); - std::unique_lock lock(m); - cv.wait_for(lock, 1s); - // we don't want to stop the task, so return false - return false; - }, - .stack_size_bytes = 4096, +#if CONFIG_ESP32_WIFI_NVS_ENABLED + // initialize NVS, needed for WiFi + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + logger.warn("Erasing NVS flash..."); + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); +#endif + + // create the display +#if CONFIG_HARDWARE_WROVER_KIT + static constexpr std::string_view dev_kit = "ESP-WROVER-DevKit"; + int clock_speed = 20 * 1000 * 1000; + auto spi_num = SPI2_HOST; + gpio_num_t mosi = GPIO_NUM_23; + gpio_num_t sclk = GPIO_NUM_19; + gpio_num_t spics = GPIO_NUM_22; + gpio_num_t reset = GPIO_NUM_18; + gpio_num_t dc_pin = (gpio_num_t)DC_PIN_NUM; + gpio_num_t backlight = GPIO_NUM_5; + size_t width = 320; + size_t height = 240; + size_t pixel_buffer_size = 16384; + bool invert_colors = false; + auto flush_cb = espp::Ili9341::flush; + auto rotation = espp::Display::Rotation::LANDSCAPE; +#endif +#if CONFIG_HARDWARE_BOX + static constexpr std::string_view dev_kit = "ESP32-S3-BOX"; + int clock_speed = 60 * 1000 * 1000; + auto spi_num = SPI2_HOST; + gpio_num_t mosi = GPIO_NUM_6; + gpio_num_t sclk = GPIO_NUM_7; + gpio_num_t spics = GPIO_NUM_5; + gpio_num_t reset = GPIO_NUM_48; + gpio_num_t dc_pin = (gpio_num_t)DC_PIN_NUM; + gpio_num_t backlight = GPIO_NUM_45; + size_t width = 320; + size_t height = 240; + size_t pixel_buffer_size = width * 50; + bool invert_colors = true; + auto flush_cb = espp::St7789::flush; + auto rotation = espp::Display::Rotation::LANDSCAPE; +#endif + + logger.info("Initializing display drivers for {}", dev_kit); + // create the spi host + spi_bus_config_t buscfg; + memset(&buscfg, 0, sizeof(buscfg)); + buscfg.mosi_io_num = mosi; + buscfg.miso_io_num = -1; + buscfg.sclk_io_num = sclk; + buscfg.quadwp_io_num = -1; + buscfg.quadhd_io_num = -1; + buscfg.max_transfer_sz = (int)(pixel_buffer_size * sizeof(lv_color_t)); + // create the spi device + spi_device_interface_config_t devcfg; + memset(&devcfg, 0, sizeof(devcfg)); + devcfg.mode = 0; + devcfg.clock_speed_hz = clock_speed; + devcfg.input_delay_ns = 0; + devcfg.spics_io_num = spics; + devcfg.queue_size = spi_queue_size; + devcfg.pre_cb = lcd_spi_pre_transfer_callback; + devcfg.post_cb = lcd_spi_post_transfer_callback; + + // Initialize the SPI bus + ret = spi_bus_initialize(spi_num, &buscfg, SPI_DMA_CH_AUTO); + ESP_ERROR_CHECK(ret); + // Attach the LCD to the SPI bus + ret = spi_bus_add_device(spi_num, &devcfg, &spi); + ESP_ERROR_CHECK(ret); +#if CONFIG_HARDWARE_WROVER_KIT + // initialize the controller + espp::Ili9341::initialize(espp::display_drivers::Config{.lcd_write = lcd_write, + .lcd_send_lines = lcd_send_lines, + .reset_pin = reset, + .data_command_pin = dc_pin, + .backlight_pin = backlight, + .invert_colors = invert_colors}); +#endif +#if CONFIG_HARDWARE_BOX + // initialize the controller + espp::St7789::initialize(espp::display_drivers::Config{ + .lcd_write = lcd_write, + .lcd_send_lines = lcd_send_lines, + .reset_pin = reset, + .data_command_pin = dc_pin, + .backlight_pin = backlight, + .backlight_on_value = invert_colors, + .invert_colors = true, + .mirror_x = true, + .mirror_y = true, + }); +#endif + // initialize the display / lvgl + auto display = std::make_shared( + espp::Display::AllocatingConfig{.width = width, + .height = height, + .pixel_buffer_size = pixel_buffer_size, + .flush_callback = flush_cb, + .rotation = rotation, + .software_rotation_enabled = true}); + + // create the gui + Gui gui({ + .display = display, + .log_level = espp::Logger::Verbosity::DEBUG + }); + + // initialize the input system +#if CONFIG_HARDWARE_WROVER_KIT + espp::Button button({ + .gpio_num = GPIO_NUM_0, + .callback = + [&](const espp::Button::Event &event) { + gui.switch_tab(); + }, + .active_level = espp::Button::ActiveLevel::LOW, + .interrupt_type = espp::Button::InterruptType::RISING_EDGE, + .pullup_enabled = false, + .pulldown_enabled = false, + .log_level = espp::Logger::Verbosity::WARN, + }); +#endif +#if CONFIG_HARDWARE_BOX + // initialize the i2c bus to read the touchpad driver (tt21100) + static constexpr auto I2C_PORT = I2C_NUM_0; + static constexpr int I2C_FREQ_HZ = (400*1000); + static constexpr int I2C_TIMEOUT_MS = 10; + logger.info("initializing i2c driver..."); + i2c_config_t i2c_cfg; + memset(&i2c_cfg, 0, sizeof(i2c_cfg)); + i2c_cfg.sda_io_num = GPIO_NUM_8; + i2c_cfg.scl_io_num = GPIO_NUM_18; + i2c_cfg.mode = I2C_MODE_MASTER; + i2c_cfg.sda_pullup_en = GPIO_PULLUP_ENABLE; + i2c_cfg.scl_pullup_en = GPIO_PULLUP_ENABLE; + i2c_cfg.master.clk_speed = I2C_FREQ_HZ; + auto i2c_err = i2c_param_config(I2C_PORT, &i2c_cfg); + if (i2c_err != ESP_OK) logger.error("config i2c failed"); + i2c_err = i2c_driver_install(I2C_PORT, I2C_MODE_MASTER, 0, 0, 0); // buff len (x2), default flags + if (i2c_err != ESP_OK) logger.error("install i2c driver failed"); + + auto i2c_read = [&](uint8_t dev_addr, uint8_t *data, size_t len) { + i2c_master_read_from_device(I2C_PORT, dev_addr, data, len, I2C_TIMEOUT_MS / portTICK_PERIOD_MS); + }; + logger.info("Initializing Tt21100"); + auto tt21100 = Tt21100(Tt21100::Config{ + .read = i2c_read, + .log_level = espp::Logger::Verbosity::WARN + }); + auto touchpad_read = [&](uint8_t* num_touch_points, uint16_t* x, uint16_t* y, uint8_t* btn_state) { + // get the latest data from the device + while (!tt21100.read()) + ; + // now hand it off + tt21100.get_touch_point(num_touch_points, x, y); + *btn_state = tt21100.get_home_button_state(); + }; + + logger.info("Initializing touchpad"); + auto touchpad = espp::TouchpadInput(espp::TouchpadInput::Config{ + .touchpad_read = touchpad_read, + .swap_xy = false, + .invert_x = true, + .invert_y = false, + .log_level = espp::Logger::Verbosity::WARN }); - task.start(); +#endif + + // initialize WiFi + logger.info("Initializing WiFi"); + std::string server_address; + espp::WifiSta wifi_sta({ + .ssid = CONFIG_ESP_WIFI_SSID, + .password = CONFIG_ESP_WIFI_PASSWORD, + .num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY, + .on_connected = nullptr, + .on_disconnected = nullptr, + .on_got_ip = [&server_address, &logger](ip_event_got_ip_t* eventdata) { + server_address = fmt::format("{}.{}.{}.{}", IP2STR(&eventdata->ip_info.ip)); + logger.info("got IP: {}.{}.{}.{}", IP2STR(&eventdata->ip_info.ip)); + } + }); + + // wait for network + while (!wifi_sta.is_connected()) { + logger.info("waiting for wifi connection..."); + std::this_thread::sleep_for(1s); + } + + // create the debug display socket + size_t server_port = CONFIG_DEBUG_SERVER_PORT; + logger.info("Creating debug server at {}:{}", server_address, server_port); + // create the socket + espp::UdpSocket server_socket({.log_level = espp::Logger::Verbosity::WARN}); + auto server_task_config = espp::Task::Config{ + .name = "UdpServer", + .callback = nullptr, + .stack_size_bytes = 6 * 1024, + }; + auto server_config = espp::UdpSocket::ReceiveConfig{ + .port = server_port, + .buffer_size = 1024, + .on_receive_callback = + [&gui](auto &data, auto &source) -> auto { + // turn the vector into a string + std::string data_str(data.begin(), data.end()); + fmt::print("Server received: '{}'\n" + " from source: {}\n", + data_str, source); + gui.push_data(data_str); + gui.handle_data(); + return std::nullopt; + } + }; + server_socket.start_receiving(server_task_config, server_config); + + // initialize mDNS, so that other embedded devices on the network can find us + // without having to be hardcoded / configured with our IP address and port + logger.info("Initializing mDNS"); + auto err = mdns_init(); + if (err != ESP_OK) { + logger.error("Could not initialize mDNS: {}", err); + return; + } + + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + std::string hostname = fmt::format("wireless-debug-display-{:x}{:x}{:x}", mac[3], mac[4], mac[5]); + err = mdns_hostname_set(hostname.c_str()); + if (err != ESP_OK) { + logger.error("Could not set mDNS hostname: {}", err); + return; + } + logger.info("mDNS hostname set to '{}'", hostname); + err = mdns_instance_name_set("Wireless Debug Display"); + if (err != ESP_OK) { + logger.error("Could not set mDNS instance name: {}", err); + return; + } + err = mdns_service_add("Wireless Debug Display", "_debugdisplay", "_udp", server_port, NULL, 0); + if (err != ESP_OK) { + logger.error("Could not add mDNS service: {}", err); + return; + } + logger.info("mDNS initialized"); + + // update the info page + gui.clear_info(); + gui.add_info(std::string("#FF0000 WiFi: #") + + CONFIG_ESP_WIFI_SSID); + gui.add_info(std::string("#00FF00 IP: #") + + server_address + ":" + + std::to_string(server_port)); - // also print in the main thread + // loop forever while (true) { - logger.debug("[{:.3f}] Hello World!", elapsed()); std::this_thread::sleep_for(1s); } } diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..8427228 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,4 @@ +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0x9000, 0x6000 +phy_init, data, phy, 0xf000, 0x1000 +factory, app, factory, 0x10000, 2M diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 5847447..37c1b17 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -5,19 +5,48 @@ # CONFIG_IDF_TARGET="esp32c3" # TODO: uncomment if you want freertos to run at 1 khz instead of the default 100 hz -# CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_HZ=1000 # TODO: uncomment if you want to run the esp at max clock speed (240 mhz) # ESP32-specific # -# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y -# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240 +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240 # TODO: uncomment if you want to update the event and main task stask sizes # Common ESP-related # -# CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 -# CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 # TODO: uncomment if you want to enable exceptions (which may be needed by certain components such as cli) # CONFIG_COMPILER_CXX_EXCEPTIONS=y + +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" + +# +# Partition Table +# +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + +# +# LVGL configuration - # Color settings +# +# CONFIG_LV_COLOR_DEPTH_32 is not set +CONFIG_LV_COLOR_DEPTH_16=y +# CONFIG_LV_COLOR_DEPTH_8 is not set +# CONFIG_LV_COLOR_DEPTH_1 is not set +CONFIG_LV_COLOR_DEPTH=16 +CONFIG_LV_COLOR_16_SWAP=y +CONFIG_LV_COLOR_MIX_ROUND_OFS=128 +CONFIG_LV_COLOR_CHROMA_KEY_HEX=0x00FF00 + +# +# LVGL configuration - # Themes +# +CONFIG_LV_USE_THEME_DEFAULT=y +CONFIG_LV_THEME_DEFAULT_DARK=y +CONFIG_LV_THEME_DEFAULT_GROW=y +CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=80 diff --git a/send_to_display.py b/send_to_display.py new file mode 100644 index 0000000..92a0d6c --- /dev/null +++ b/send_to_display.py @@ -0,0 +1,33 @@ +import sys +import socket +import argparse + +def main(): + parser = argparse.ArgumentParser(description='Send a message to the display.') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--message', type=str, action="append", + help='Messages to send to the display') + group.add_argument('--file', dest='file', type=str, + help='the file to read the message from') + parser.add_argument('--ip', dest='ip', type=str, + help='the ip address of the display') + parser.add_argument('--port', dest='port', type=int, default=5555, + help='the port of the display') + + args = parser.parse_args() + + UDP_IP = args.ip + UDP_PORT = args.port + + if args.message: + MESSAGE = '\n'.join(args.message) + if args.file: + with open(args.file, 'r') as f: + MESSAGE = f.read() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP + sock.sendto(MESSAGE.encode(), (UDP_IP, UDP_PORT)) + print(f"Sent to address: {UDP_IP}:{UDP_PORT}") + +if __name__ == '__main__': + main() diff --git a/send_to_display_mdns.py b/send_to_display_mdns.py new file mode 100644 index 0000000..6969b91 --- /dev/null +++ b/send_to_display_mdns.py @@ -0,0 +1,54 @@ +import sys +import socket +import time +import argparse + +from zeroconf import ServiceBrowser, ServiceListener, Zeroconf + +class MyListener(ServiceListener): + def __init__(self, message): + self.message = message + self.has_sent = False + + def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: + print(f"Service {name} updated") + + def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: + print(f"Service {name} removed") + + def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: + info = zc.get_service_info(type_, name) + print(f"Service {name} added, service info: {info}") + UDP_IP = socket.inet_ntoa(info.addresses[0]) + UDP_PORT = info.port + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP + sock.sendto(self.message.encode(), (UDP_IP, UDP_PORT)) + print(f"Sent to address: {UDP_IP}:{UDP_PORT}") + self.has_sent = True + +def main(): + parser = argparse.ArgumentParser(description='Send a message to the display.') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--message', type=str, action="append", + help='Messages to send to the display') + group.add_argument('--file', dest='file', type=str, + help='the file to read the message from') + + args = parser.parse_args() + + if args.message: + MESSAGE = '\n'.join(args.message) + if args.file: + with open(args.file, 'r') as f: + MESSAGE = f.read() + + zeroconf = Zeroconf() + listener = MyListener(MESSAGE) + browser = ServiceBrowser(zeroconf, "_debugdisplay._udp.local.", listener) + + print("Sending...\n") + while not listener.has_sent: + time.sleep(0.1) + +if __name__ == '__main__': + main() diff --git a/test_data.txt b/test_data.txt new file mode 100644 index 0000000..253e348 --- /dev/null +++ b/test_data.txt @@ -0,0 +1,43 @@ ++++CP ++++CL +hello world! +plots should be of the form: '::' +Some #FF0000 red# text +Some #00FF00 green# text +Some #0000FF blue# text +t0::0 +t0::1 +t0::30 +t0::50 +t0::3 +t0::1 +t0::1 +t0::3 +t0::5 +t0::0 +t0::10 +t0::-30 +t0::-50 +t0::3 +t0::1 +t0::1 +t0::3 +t0::5 +r0::0 +r0::10 +r0::3 +r0::5 +r0::3 +r0::1 +r0::1 +r0::3 +r0::5 +r0::0 +r0::10 +r0::30 +r0::5 +r0::3 +r0::1 +r0::-10 +r0::-30 +r0::5