From 4bf31f908a2c05bfd9be5909f0b7c1d3cfe348b3 Mon Sep 17 00:00:00 2001 From: dark98 Date: Thu, 24 Sep 2026 22:34:33 +0100 Subject: [PATCH] Add Serial Terminal App & WebUi Theme --- .vscode/settings.json | 2 +- README.md | 22 +- components/sys_cli/sys_cli.c | 48 +++ components/sys_core/CMakeLists.txt | 4 + components/sys_core/include/sys_uart.h | 79 ++++ components/sys_core/sys_uart.c | 191 ++++++++++ components/sys_wifi/sys_wifi.c | 227 ----------- components/web_server/CMakeLists.txt | 1 + .../web_server/include/web_server_pages.h | 1 + components/web_server/pages/ota.c | 117 +++++- components/web_server/pages/serial.c | 360 ++++++++++++++++++ components/web_server/pages/wifi.c | 112 +++++- components/web_server/web_server.c | 3 + main/CMakeLists.txt | 1 + main/apps/app_serial.c | 97 +++++ main/apps/apps.h | 1 + main/main.c | 8 + 17 files changed, 1040 insertions(+), 234 deletions(-) create mode 100644 components/sys_core/include/sys_uart.h create mode 100644 components/sys_core/sys_uart.c create mode 100644 components/web_server/pages/serial.c create mode 100644 main/apps/app_serial.c diff --git a/.vscode/settings.json b/.vscode/settings.json index cae13e7..db4840b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,4 +16,4 @@ }, "idf.currentSetup": "D:\\SDK\\.espressif\\v6.1\\esp-idf", "idf.portWin": "COM4" -} +} \ No newline at end of file diff --git a/README.md b/README.md index 6437466..08d632e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A modular embedded micro-OS for the ABrobot ESP32-C3 board with a 0.42" monochro - OLED-based app launcher and system UI - Single-button input handling with click, double-click, hold, and home gestures - USB serial/JTAG CLI for runtime system control +- Hardware UART Serial Console bridge (TX: GPIO 21, RX: GPIO 20) with Web Terminal UI at `/serial` - Wi‑Fi station mode with automatic fallback to SoftAP provisioning - Wi‑Fi network scanning from the setup page - Persistent NVS Wi‑Fi credentials @@ -24,7 +25,7 @@ d:/Projects/ESP32C3/OS/ ├── components/ │ ├── sys_app/ # App lifecycle and app registry │ ├── sys_cli/ # USB serial / JTAG shell commands -│ ├── sys_core/ # Global status and event plumbing +│ ├── sys_core/ # Global status, event plumbing & sys_uart driver │ ├── sys_gfx/ # OLED display, fonts, UI compositor, widgets │ ├── sys_input/ # Button input handling and gestures │ ├── sys_wifi/ # Wi‑Fi initialization, station/AP logic, NVS credentials @@ -34,7 +35,8 @@ d:/Projects/ESP32C3/OS/ │ │ └── web_server_pages.h │ ├── pages/ │ │ ├── wifi.c # Root configuration and /scan /configure handlers -│ │ └── ota.c # /ota firmware upload page and OTA logic +│ │ ├── ota.c # /ota firmware upload page and OTA logic +│ │ └── serial.c # /serial Web Serial Terminal and /serial/data REST API │ └── web_server.c # Starts the HTTP server and registers pages ├── main/ │ ├── apps/ @@ -42,6 +44,7 @@ d:/Projects/ESP32C3/OS/ │ │ ├── app_game.c │ │ ├── app_settings.c │ │ ├── app_sysmon.c +│ │ ├── app_serial.c │ │ └── apps.h │ ├── CMakeLists.txt │ ├── font_5x7.h @@ -98,6 +101,20 @@ The web server is also started while the board is connected to Wi‑Fi so the sa - `/scan` — JSON network scan results - `/configure` — save station credentials and connect - `/ota` — OTA upload page and firmware update endpoint +- `/serial` — Web Serial Terminal interface +- `/serial/data` — Real-time streaming JSON endpoint for RX/TX delta bytes +- `/serial/tx` — POST endpoint to transmit text or bytes over UART TX (GPIO 21) +- `/serial/baud` — POST endpoint to dynamically change UART baud rate +- `/serial/clear` — POST endpoint to clear the RX ring buffer + +--- + +## Web Serial Terminal + +Connect over Wi-Fi and open `http://192.168.4.1/serial` (in SoftAP mode) or `http:///serial` (in station mode) to access the interactive web-based serial console: + +- **Pins**: TX on `GPIO 21`, RX on `GPIO 20` +- **Features**: Live streaming terminal, color-coded RX/TX logs, auto-scroll toggle, command history (Up/Down arrow navigation), dynamic baud rate switching (9600 to 921600), macro shortcuts (`PING`, `AT`, `HELP`, etc.), and log file exporting. --- @@ -169,6 +186,7 @@ esp32c3-os> help free Display system heap and RAM status app App management (list, launch , home) input Simulate button input (click, double, long, home) + uart Hardware UART control (send, baud, stats, clear) contrast Set OLED contrast (0-255) reboot Reboot the MCU ``` diff --git a/components/sys_cli/sys_cli.c b/components/sys_cli/sys_cli.c index 690390f..3b823a8 100644 --- a/components/sys_cli/sys_cli.c +++ b/components/sys_cli/sys_cli.c @@ -4,6 +4,7 @@ #include "os_gfx.h" #include "os_oled.h" #include "os_status.h" +#include "sys_uart.h" #include #include #include @@ -15,6 +16,45 @@ static const char *TAG = "SYS_CLI"; +static int cmd_uart(int argc, char **argv) +{ + if (argc < 2) { + printf("Usage: uart | baud | stats | clear>\n"); + return 1; + } + + if (strcmp(argv[1], "send") == 0 && argc >= 3) { + sys_uart_write_str(argv[2]); + sys_uart_write_str("\r\n"); + printf("Sent over UART TX: %s\n", argv[2]); + return 0; + } else if (strcmp(argv[1], "baud") == 0 && argc >= 3) { + uint32_t baud = (uint32_t)strtoul(argv[2], NULL, 10); + if (baud > 0 && sys_uart_set_baud_rate(baud) == ESP_OK) { + printf("UART baud rate changed to %lu\n", (unsigned long)baud); + return 0; + } else { + printf("Failed to set baud rate\n"); + return 1; + } + } else if (strcmp(argv[1], "stats") == 0) { + uint32_t rx = 0, tx = 0; + sys_uart_get_stats(&rx, &tx); + printf("\n=== UART Status (TX: GPIO 21, RX: GPIO 20) ===\n"); + printf("Active Baud Rate: %lu\n", (unsigned long)sys_uart_get_baud_rate()); + printf("Total RX Bytes: %lu\n", (unsigned long)rx); + printf("Total TX Bytes: %lu\n", (unsigned long)tx); + return 0; + } else if (strcmp(argv[1], "clear") == 0) { + sys_uart_clear_rx_buffer(); + printf("UART RX buffer cleared\n"); + return 0; + } + + printf("Unknown uart sub-command\n"); + return 1; +} + static int cmd_ps(int argc, char **argv) { printf("\n=== Running FreeRTOS Tasks ===\n"); @@ -178,6 +218,14 @@ esp_err_t sys_cli_init(void) }; esp_console_cmd_register(&reboot_cmd); + const esp_console_cmd_t uart_cmd = { + .command = "uart", + .help = "Hardware UART control (send, baud, stats, clear)", + .hint = "", + .func = &cmd_uart, + }; + esp_console_cmd_register(&uart_cmd); + err = esp_console_start_repl(repl); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to start USB Serial/JTAG REPL: %s", esp_err_to_name(err)); diff --git a/components/sys_core/CMakeLists.txt b/components/sys_core/CMakeLists.txt index e666d8b..113b2ab 100644 --- a/components/sys_core/CMakeLists.txt +++ b/components/sys_core/CMakeLists.txt @@ -2,6 +2,7 @@ idf_component_register( SRCS "sys_event.c" "sys_status.c" + "sys_uart.c" INCLUDE_DIRS "include" REQUIRES @@ -9,4 +10,7 @@ idf_component_register( esp_timer esp_system log + driver + esp_driver_gpio + esp_driver_uart ) diff --git a/components/sys_core/include/sys_uart.h b/components/sys_core/include/sys_uart.h new file mode 100644 index 0000000..9452ef6 --- /dev/null +++ b/components/sys_core/include/sys_uart.h @@ -0,0 +1,79 @@ +#ifndef SYS_UART_H +#define SYS_UART_H + +#include +#include +#include +#include "esp_err.h" +#include "driver/gpio.h" +#include "driver/uart.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define SYS_UART_NUM UART_NUM_1 +#define SYS_UART_PIN_TX GPIO_NUM_21 +#define SYS_UART_PIN_RX GPIO_NUM_20 +#define SYS_UART_DEFAULT_BAUD 115200 +#define SYS_UART_BUFFER_SIZE 4096 + +/** + * @brief Initialize the hardware UART subsystem and RX background task. + * @param baud_rate Initial baud rate (e.g., 115200). + * @return ESP_OK on success. + */ +esp_err_t sys_uart_init(uint32_t baud_rate); + +/** + * @brief Change the UART baud rate dynamically. + * @param baud_rate New baud rate. + * @return ESP_OK on success. + */ +esp_err_t sys_uart_set_baud_rate(uint32_t baud_rate); + +/** + * @brief Get the currently active baud rate. + */ +uint32_t sys_uart_get_baud_rate(void); + +/** + * @brief Transmit data over UART TX pin. + * @param data Pointer to buffer. + * @param len Number of bytes to send. + * @return ESP_OK on success. + */ +esp_err_t sys_uart_write(const uint8_t *data, size_t len); + +/** + * @brief Transmit a null-terminated string over UART TX. + * @param str Null-terminated string. + * @return ESP_OK on success. + */ +esp_err_t sys_uart_write_str(const char *str); + +/** + * @brief Read new incoming data since a given monotonic sequence index. + * @param from_seq Sequence number previously returned to the caller. + * @param out_buf Buffer to store newly received bytes. + * @param max_len Size of out_buf. + * @param[out] new_seq Pointer to store the updated sequence counter. + * @return Number of bytes copied to out_buf. + */ +size_t sys_uart_read_delta(uint32_t from_seq, uint8_t *out_buf, size_t max_len, uint32_t *new_seq); + +/** + * @brief Get total transmitted and received byte counts. + */ +void sys_uart_get_stats(uint32_t *rx_count, uint32_t *tx_count); + +/** + * @brief Clear the RX circular buffer and reset sequence. + */ +void sys_uart_clear_rx_buffer(void); + +#ifdef __cplusplus +} +#endif + +#endif // SYS_UART_H diff --git a/components/sys_core/sys_uart.c b/components/sys_core/sys_uart.c new file mode 100644 index 0000000..ca06b25 --- /dev/null +++ b/components/sys_core/sys_uart.c @@ -0,0 +1,191 @@ +#include "sys_uart.h" +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" + +static const char *TAG = "SYS_UART"; + +static uint8_t s_rx_ring[SYS_UART_BUFFER_SIZE]; +static uint32_t s_rx_head = 0; /* Total lifetime received bytes (monotonic) */ +static uint32_t s_tx_total = 0; +static uint32_t s_baud_rate = SYS_UART_DEFAULT_BAUD; +static SemaphoreHandle_t s_uart_mutex = NULL; +static bool s_initialized = false; + +static void uart_rx_task(void *pvParameters) +{ + uint8_t tmp_buf[256]; + while (1) { + int len = uart_read_bytes(SYS_UART_NUM, tmp_buf, sizeof(tmp_buf), pdMS_TO_TICKS(20)); + if (len > 0) { + if (xSemaphoreTake(s_uart_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { + for (int i = 0; i < len; i++) { + s_rx_ring[s_rx_head % SYS_UART_BUFFER_SIZE] = tmp_buf[i]; + s_rx_head++; + } + xSemaphoreGive(s_uart_mutex); + } + } + } +} + +esp_err_t sys_uart_init(uint32_t baud_rate) +{ + if (s_initialized) { + return ESP_OK; + } + + if (baud_rate == 0) { + baud_rate = SYS_UART_DEFAULT_BAUD; + } + s_baud_rate = baud_rate; + + s_uart_mutex = xSemaphoreCreateMutex(); + if (!s_uart_mutex) { + ESP_LOGE(TAG, "Failed to create UART mutex"); + return ESP_ERR_NO_MEM; + } + + uart_config_t uart_config = { + .baud_rate = (int)s_baud_rate, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + + esp_err_t err = uart_driver_install(SYS_UART_NUM, 1024, 1024, 0, NULL, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); + return err; + } + + err = uart_param_config(SYS_UART_NUM, &uart_config); + if (err != ESP_OK) { + ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + return err; + } + + err = uart_set_pin(SYS_UART_NUM, SYS_UART_PIN_TX, SYS_UART_PIN_RX, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); + return err; + } + + /* Enable internal pull-up on RX to prevent floating line noise / framing errors */ + gpio_set_pull_mode(SYS_UART_PIN_RX, GPIO_PULLUP_ONLY); + gpio_pullup_en(SYS_UART_PIN_RX); + + memset(s_rx_ring, 0, sizeof(s_rx_ring)); + s_rx_head = 0; + s_tx_total = 0; + s_initialized = true; + + xTaskCreate(uart_rx_task, "uart_rx_task", 3072, NULL, 5, NULL); + ESP_LOGI(TAG, "Hardware UART initialized on TX:GPIO%d, RX:GPIO%d @ %lu baud", + SYS_UART_PIN_TX, SYS_UART_PIN_RX, (unsigned long)s_baud_rate); + + return ESP_OK; +} + +esp_err_t sys_uart_set_baud_rate(uint32_t baud_rate) +{ + if (!s_initialized || baud_rate == 0) return ESP_ERR_INVALID_STATE; + esp_err_t err = uart_set_baudrate(SYS_UART_NUM, baud_rate); + if (err == ESP_OK) { + s_baud_rate = baud_rate; + uart_flush_input(SYS_UART_NUM); + ESP_LOGI(TAG, "Baud rate changed to %lu", (unsigned long)baud_rate); + } + return err; +} + +uint32_t sys_uart_get_baud_rate(void) +{ + return s_baud_rate; +} + +esp_err_t sys_uart_write(const uint8_t *data, size_t len) +{ + if (!s_initialized || !data || len == 0) return ESP_ERR_INVALID_ARG; + int sent = uart_write_bytes(SYS_UART_NUM, data, len); + if (sent > 0) { + if (xSemaphoreTake(s_uart_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { + s_tx_total += (uint32_t)sent; + xSemaphoreGive(s_uart_mutex); + } + return ESP_OK; + } + return ESP_FAIL; +} + +esp_err_t sys_uart_write_str(const char *str) +{ + if (!str) return ESP_ERR_INVALID_ARG; + return sys_uart_write((const uint8_t *)str, strlen(str)); +} + +size_t sys_uart_read_delta(uint32_t from_seq, uint8_t *out_buf, size_t max_len, uint32_t *new_seq) +{ + if (!s_initialized || !out_buf || max_len == 0) { + if (new_seq) *new_seq = from_seq; + return 0; + } + + size_t bytes_copied = 0; + if (xSemaphoreTake(s_uart_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { + uint32_t current_head = s_rx_head; + + if (from_seq > current_head) { + from_seq = current_head; + } + + uint32_t available = current_head - from_seq; + if (available > SYS_UART_BUFFER_SIZE) { + /* Client is too far behind, catch up to buffer start */ + from_seq = current_head - SYS_UART_BUFFER_SIZE; + available = SYS_UART_BUFFER_SIZE; + } + + if (available > max_len) { + available = max_len; + } + + for (size_t i = 0; i < available; i++) { + out_buf[i] = s_rx_ring[(from_seq + i) % SYS_UART_BUFFER_SIZE]; + } + + bytes_copied = available; + if (new_seq) { + *new_seq = from_seq + available; + } + xSemaphoreGive(s_uart_mutex); + } else { + if (new_seq) *new_seq = from_seq; + } + + return bytes_copied; +} + +void sys_uart_get_stats(uint32_t *rx_count, uint32_t *tx_count) +{ + if (xSemaphoreTake(s_uart_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { + if (rx_count) *rx_count = s_rx_head; + if (tx_count) *tx_count = s_tx_total; + xSemaphoreGive(s_uart_mutex); + } else { + if (rx_count) *rx_count = s_rx_head; + if (tx_count) *tx_count = s_tx_total; + } +} + +void sys_uart_clear_rx_buffer(void) +{ + if (xSemaphoreTake(s_uart_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { + memset(s_rx_ring, 0, sizeof(s_rx_ring)); + xSemaphoreGive(s_uart_mutex); + } +} diff --git a/components/sys_wifi/sys_wifi.c b/components/sys_wifi/sys_wifi.c index 70c6702..a660783 100644 --- a/components/sys_wifi/sys_wifi.c +++ b/components/sys_wifi/sys_wifi.c @@ -45,233 +45,6 @@ static esp_err_t load_credentials(void) return err; } -#if 0 -static esp_err_t provisioning_scan(httpd_req_t *request) -{ - wifi_scan_config_t scan_config = { - .show_hidden = true, - .scan_type = WIFI_SCAN_TYPE_ACTIVE, - }; - esp_err_t err = esp_wifi_scan_start(&scan_config, true); - if (err != ESP_OK) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Wi-Fi scan failed"); - return err; - } - - uint16_t count = 0; - esp_wifi_scan_get_ap_num(&count); - if (count > 32) count = 32; - wifi_ap_record_t *records = calloc(count, sizeof(wifi_ap_record_t)); - if (count > 0 && !records) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Not enough memory for scan"); - return ESP_ERR_NO_MEM; - } - if (count > 0) { - err = esp_wifi_scan_get_ap_records(&count, records); - if (err != ESP_OK) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not read scan results"); - free(records); - return err; - } - } - - httpd_resp_set_type(request, "application/json"); - httpd_resp_send_chunk(request, "[", 1); - uint16_t visible_count = 0; - for (uint16_t i = 0; i < count; i++) { - if (records[i].ssid[0] == '\0') continue; - - char prefix[32]; - int prefix_length = snprintf(prefix, sizeof(prefix), "%s{\"ssid\":\"", - visible_count == 0 ? "" : ","); - httpd_resp_send_chunk(request, prefix, prefix_length); - append_json_string(request, records[i].ssid, sizeof(records[i].ssid)); - char suffix[48]; - int suffix_length = snprintf(suffix, sizeof(suffix), "\",\"rssi\":%d}", records[i].rssi); - httpd_resp_send_chunk(request, suffix, suffix_length); - visible_count++; - } - httpd_resp_send_chunk(request, "]", 1); - free(records); - return httpd_resp_send_chunk(request, NULL, 0); -} - -static esp_err_t provisioning_get(httpd_req_t *request) -{ - const char *page = - "" - "ABrobot Wi-Fi

ABrobot Wi-Fi setup

" - "
" - "

" - "" - "

" - "

" - "

" - ""; - httpd_resp_set_type(request, "text/html"); - return httpd_resp_send(request, page, HTTPD_RESP_USE_STRLEN); -} - -static esp_err_t ota_get(httpd_req_t *request) -{ - const char *page = - "" - "ABrobot OTA" - "

Firmware update

" - "
" - "

"; - httpd_resp_set_type(request, "text/html"); - return httpd_resp_send(request, page, HTTPD_RESP_USE_STRLEN); -} - -static esp_err_t ota_post(httpd_req_t *request) -{ - const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL); - if (!update_partition || request->content_len == 0 || request->content_len > update_partition->size) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Firmware image is too large or unavailable"); - return ESP_ERR_INVALID_SIZE; - } - - esp_ota_handle_t ota_handle; - esp_err_t err = esp_ota_begin(update_partition, request->content_len, &ota_handle); - if (err != ESP_OK) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not begin firmware update"); - return err; - } - - uint8_t *buffer = malloc(4096); - if (!buffer) { - esp_ota_abort(ota_handle); - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Not enough memory for firmware update"); - return ESP_ERR_NO_MEM; - } - - size_t remaining = request->content_len; - while (remaining > 0) { - size_t chunk_size = remaining < 4096 ? remaining : 4096; - int received = httpd_req_recv(request, (char *)buffer, chunk_size); - if (received <= 0) { - free(buffer); - esp_ota_abort(ota_handle); - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Firmware upload interrupted"); - return ESP_FAIL; - } - err = esp_ota_write(ota_handle, buffer, received); - if (err != ESP_OK) { - free(buffer); - esp_ota_abort(ota_handle); - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not write firmware image"); - return err; - } - remaining -= (size_t)received; - } - free(buffer); - - err = esp_ota_end(ota_handle); - if (err != ESP_OK) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Invalid firmware image"); - return err; - } - err = esp_ota_set_boot_partition(update_partition); - if (err != ESP_OK) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not select new firmware"); - return err; - } - - httpd_resp_sendstr(request, "Firmware update installed. Rebooting."); - ESP_LOGI(TAG, "OTA image installed to %s; rebooting", update_partition->label); - xTaskCreate(ota_reboot_task, "ota_reboot", 2048, NULL, 5, NULL); - return ESP_OK; -} - -static esp_err_t provisioning_post(httpd_req_t *request) -{ - char body[160] = {0}; - char ssid[sizeof(s_sta_ssid)] = {0}; - char password[sizeof(s_sta_password)] = {0}; - int received = httpd_req_recv(request, body, sizeof(body) - 1); - if (received <= 0 || !form_value(body, "ssid", ssid, sizeof(ssid)) || - !form_value(body, "password", password, sizeof(password))) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "SSID and password are required"); - return ESP_FAIL; - } - - esp_err_t err = save_credentials(ssid, password); - if (err != ESP_OK) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not save credentials"); - return err; - } - - snprintf(s_sta_ssid, sizeof(s_sta_ssid), "%s", ssid); - snprintf(s_sta_password, sizeof(s_sta_password), "%s", password); - httpd_resp_sendstr(request, "Credentials saved. The device is connecting; you can close this page."); - set_status(WIFI_STATE_CONNECTING, 0); - err = esp_wifi_set_mode(WIFI_MODE_STA); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to switch to station mode: %s", esp_err_to_name(err)); - return err; - } - wifi_config_t config = {0}; - copy_wifi_string(config.sta.ssid, sizeof(config.sta.ssid), s_sta_ssid); - copy_wifi_string(config.sta.password, sizeof(config.sta.password), s_sta_password); - err = esp_wifi_set_config(WIFI_IF_STA, &config); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to apply station credentials: %s", esp_err_to_name(err)); - return err; - } - err = esp_wifi_connect(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to start station connection: %s", esp_err_to_name(err)); - return err; - } - ESP_LOGI(TAG, "Station connection started for '%s'", s_sta_ssid); - return ESP_OK; -} - -static void start_provisioning_server(void) -{ - if (s_http_server != NULL) return; - - httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - if (httpd_start(&s_http_server, &config) != ESP_OK) { - ESP_LOGE(TAG, "Failed to start provisioning web server"); - return; - } - const httpd_uri_t get_uri = {.uri = "/", .method = HTTP_GET, .handler = provisioning_get}; - const httpd_uri_t ota_get_uri = {.uri = "/ota", .method = HTTP_GET, .handler = ota_get}; - const httpd_uri_t ota_post_uri = {.uri = "/ota", .method = HTTP_POST, .handler = ota_post}; - const httpd_uri_t scan_uri = {.uri = "/scan", .method = HTTP_GET, .handler = provisioning_scan}; - const httpd_uri_t post_uri = {.uri = "/configure", .method = HTTP_POST, .handler = provisioning_post}; - httpd_register_uri_handler(s_http_server, &get_uri); - httpd_register_uri_handler(s_http_server, &ota_get_uri); - httpd_register_uri_handler(s_http_server, &ota_post_uri); - httpd_register_uri_handler(s_http_server, &scan_uri); - httpd_register_uri_handler(s_http_server, &post_uri); -} - -#endif - static void start_softap(void) { wifi_config_t ap_config = {0}; diff --git a/components/web_server/CMakeLists.txt b/components/web_server/CMakeLists.txt index 33c3df9..8360134 100644 --- a/components/web_server/CMakeLists.txt +++ b/components/web_server/CMakeLists.txt @@ -3,6 +3,7 @@ idf_component_register( "web_server.c" "pages/wifi.c" "pages/ota.c" + "pages/serial.c" INCLUDE_DIRS "include" REQUIRES diff --git a/components/web_server/include/web_server_pages.h b/components/web_server/include/web_server_pages.h index efc3da5..8374a78 100644 --- a/components/web_server/include/web_server_pages.h +++ b/components/web_server/include/web_server_pages.h @@ -5,5 +5,6 @@ void web_server_register_wifi_pages(httpd_handle_t server); void web_server_register_ota_page(httpd_handle_t server); +void web_server_register_serial_pages(httpd_handle_t server); #endif diff --git a/components/web_server/pages/ota.c b/components/web_server/pages/ota.c index eaf2e80..aa57201 100644 --- a/components/web_server/pages/ota.c +++ b/components/web_server/pages/ota.c @@ -14,11 +14,124 @@ static void reboot_task(void *arg) esp_restart(); } +static const char OTA_HTML[] = +"" +"" +"ABrobot OTA Firmware Update" +"" +"
" +"
" +"
" +"
" +"
ESP32-C3 Firmware Update
" +"
" +" " +"
" +"
" +"
Over-The-Air FlasherOTA 2.0 Engine
" +"
" +" " +"
" +"
⚡
" +"
Click or Drag & Drop Firmware Image (.bin)
" +"
Select the compiled application binary file to flash to ESP32-C3
" +"
" +"
" +"
" +" firmware.bin" +" 0 KB" +"
" +" " +"
" +"
" +"
" +" Uploading binary..." +" 0 KB / 0 KB" +"
" +"
" +"
" +"
0%
" +"
" +"
" +" " +"
" +"
" +"
" +"
" +"
OTA Safety Checklist
" +"
" +"
Dual PartitionSafe rollback on boot failure
" +"
Flash VerificationSHA-256 & image headers verified
" +"
Power NoticeDo not remove power during flash
" +"
Auto RestartReboots automatically after install
" +"
" +"
" +"
" +""; + static esp_err_t ota_get(httpd_req_t *request) { - const char *page = "ABrobot OTA

Firmware update

"; httpd_resp_set_type(request, "text/html"); - return httpd_resp_send(request, page, HTTPD_RESP_USE_STRLEN); + return httpd_resp_send(request, OTA_HTML, HTTPD_RESP_USE_STRLEN); } static esp_err_t ota_post(httpd_req_t *request) diff --git a/components/web_server/pages/serial.c b/components/web_server/pages/serial.c new file mode 100644 index 0000000..fbd60dc --- /dev/null +++ b/components/web_server/pages/serial.c @@ -0,0 +1,360 @@ +#include "web_server_pages.h" +#include "sys_uart.h" +#include "esp_log.h" +#include "esp_http_server.h" +#include +#include +#include + +static const char *TAG = "SYS_WEB_SERIAL"; + +static const char SERIAL_HTML[] = +"" +"" +"ABrobot Web Serial Terminal" +"" +"
" +"
" +"
" +"
" +"
ESP32-C3 Web Serial Terminal
" +"
" +" " +"
" +"
" +"
" +" " +" " +" " +" " +" " +" " +"
" +"
" +" RX: 0 B" +" TX: 0 B" +" " +"
" +"
" +"
" +"
" +" " +" " +" " +" " +"
" +"
" +" Quick:" +" " +" " +" " +" " +" " +"
" +"
" +""; + +static esp_err_t serial_page_get(httpd_req_t *request) +{ + httpd_resp_set_type(request, "text/html"); + return httpd_resp_send(request, SERIAL_HTML, HTTPD_RESP_USE_STRLEN); +} + +static esp_err_t serial_data_get(httpd_req_t *request) +{ + char query[64] = {0}; + uint32_t from_seq = 0; + if (httpd_req_get_url_query_str(request, query, sizeof(query)) == ESP_OK) { + char val[16] = {0}; + if (httpd_query_key_value(query, "seq", val, sizeof(val)) == ESP_OK) { + from_seq = (uint32_t)strtoul(val, NULL, 10); + } + } + + uint8_t rx_delta[1024]; + uint32_t new_seq = from_seq; + size_t len = sys_uart_read_delta(from_seq, rx_delta, sizeof(rx_delta) - 1, &new_seq); + rx_delta[len] = '\0'; + + uint32_t total_rx = 0, total_tx = 0; + sys_uart_get_stats(&total_rx, &total_tx); + uint32_t baud = sys_uart_get_baud_rate(); + + httpd_resp_set_type(request, "application/json"); + + /* Begin JSON response */ + char prefix[128]; + int p_len = snprintf(prefix, sizeof(prefix), "{\"seq\":%lu,\"rx\":%lu,\"tx\":%lu,\"baud\":%lu,\"text\":\"", + (unsigned long)new_seq, (unsigned long)total_rx, (unsigned long)total_tx, (unsigned long)baud); + httpd_resp_send_chunk(request, prefix, p_len); + + /* Stream escaped JSON text */ + for (size_t i = 0; i < len; i++) { + uint8_t c = rx_delta[i]; + if (c == '"') { + httpd_resp_send_chunk(request, "\\\"", 2); + } else if (c == '\\') { + httpd_resp_send_chunk(request, "\\\\", 2); + } else if (c == '\n') { + httpd_resp_send_chunk(request, "\\n", 2); + } else if (c == '\r') { + httpd_resp_send_chunk(request, "\\r", 2); + } else if (c == '\t') { + httpd_resp_send_chunk(request, "\\t", 2); + } else if (c < 32 || c > 126) { + /* Replace non-printable ASCII in text view with dot or hex escape */ + char hex_esc[8]; + int h_len = snprintf(hex_esc, sizeof(hex_esc), "\\u%04x", c); + httpd_resp_send_chunk(request, hex_esc, h_len); + } else { + char ch[2] = {(char)c, '\0'}; + httpd_resp_send_chunk(request, ch, 1); + } + } + + /* Stream raw byte array for HEX dump view */ + httpd_resp_send_chunk(request, "\",\"hex\":[", 9); + for (size_t i = 0; i < len; i++) { + char num[8]; + int n_len = snprintf(num, sizeof(num), "%s%u", (i > 0 ? "," : ""), (unsigned int)rx_delta[i]); + httpd_resp_send_chunk(request, num, n_len); + } + httpd_resp_send_chunk(request, "]}", 2); + + return httpd_resp_send_chunk(request, NULL, 0); +} + +static esp_err_t serial_tx_post(httpd_req_t *request) +{ + char body[512] = {0}; + int total_len = request->content_len; + if (total_len >= sizeof(body)) { + total_len = sizeof(body) - 1; + } + + int received = httpd_req_recv(request, body, total_len); + if (received > 0) { + sys_uart_write((const uint8_t *)body, (size_t)received); + httpd_resp_sendstr(request, "OK"); + return ESP_OK; + } + + return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "No TX data received"); +} + +static esp_err_t serial_baud_post(httpd_req_t *request) +{ + char body[32] = {0}; + int received = httpd_req_recv(request, body, sizeof(body) - 1); + if (received > 0) { + body[received] = '\0'; + uint32_t baud = (uint32_t)strtoul(body, NULL, 10); + if (baud > 0) { + esp_err_t err = sys_uart_set_baud_rate(baud); + if (err == ESP_OK) { + httpd_resp_sendstr(request, "OK"); + return ESP_OK; + } + } + } + return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Invalid baud rate"); +} + +static esp_err_t serial_clear_post(httpd_req_t *request) +{ + sys_uart_clear_rx_buffer(); + httpd_resp_sendstr(request, "OK"); + return ESP_OK; +} + +void web_server_register_serial_pages(httpd_handle_t server) +{ + const httpd_uri_t page = {.uri = "/serial", .method = HTTP_GET, .handler = serial_page_get}; + const httpd_uri_t data = {.uri = "/serial/data", .method = HTTP_GET, .handler = serial_data_get}; + const httpd_uri_t tx = {.uri = "/serial/tx", .method = HTTP_POST, .handler = serial_tx_post}; + const httpd_uri_t baud = {.uri = "/serial/baud", .method = HTTP_POST, .handler = serial_baud_post}; + const httpd_uri_t clear = {.uri = "/serial/clear", .method = HTTP_POST, .handler = serial_clear_post}; + + httpd_register_uri_handler(server, &page); + httpd_register_uri_handler(server, &data); + httpd_register_uri_handler(server, &tx); + httpd_register_uri_handler(server, &baud); + httpd_register_uri_handler(server, &clear); + ESP_LOGI(TAG, "Web Serial Terminal endpoints registered at /serial"); +} diff --git a/components/web_server/pages/wifi.c b/components/web_server/pages/wifi.c index d340f55..9f63051 100644 --- a/components/web_server/pages/wifi.c +++ b/components/web_server/pages/wifi.c @@ -11,6 +11,115 @@ static const char *TAG = "SYS_WEB_WIFI"; static char s_ssid[33]; static char s_password[65]; +static const char WIFI_HTML[] = +"" +"" +"ABrobot Wi-Fi Manager" +"" +"
" +"
" +"
" +"
" +"
ESP32-C3 Wi-Fi Setup
" +"
" +" " +"
" +"
" +"
Wireless Network Configuration
" +"
" +"
" +" " +"
" +" " +" " +"
" +"
" +" " +"
" +" " +" " +"
" +"
" +" " +"
" +" " +" " +"
" +"
" +" " +"
" +"
" +"
" +"
" +"
Device Information
" +"
" +"
Provisioning APABrobot-OS
" +"
Default AP IP192.168.4.1
" +"
Hardware PortTX:GPIO21 / RX:GPIO20
" +"
StatusOperational
" +"
" +"
" +"
" +""; + static void copy_string(uint8_t *destination, size_t size, const char *source) { size_t length = strlen(source); @@ -64,9 +173,8 @@ static void append_json_string(httpd_req_t *request, const uint8_t *value, size_ static esp_err_t root_get(httpd_req_t *request) { - const char *page = "ABrobot Wi-Fi

ABrobot Wi-Fi setup





"; httpd_resp_set_type(request, "text/html"); - return httpd_resp_send(request, page, HTTPD_RESP_USE_STRLEN); + return httpd_resp_send(request, WIFI_HTML, HTTPD_RESP_USE_STRLEN); } static esp_err_t scan_get(httpd_req_t *request) diff --git a/components/web_server/web_server.c b/components/web_server/web_server.c index 2341b88..7a13eed 100644 --- a/components/web_server/web_server.c +++ b/components/web_server/web_server.c @@ -11,11 +11,14 @@ esp_err_t web_server_start(void) if (s_server) return ESP_OK; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.max_uri_handlers = 16; + config.stack_size = 8192; esp_err_t err = httpd_start(&s_server, &config); if (err != ESP_OK) return err; web_server_register_wifi_pages(s_server); web_server_register_ota_page(s_server); + web_server_register_serial_pages(s_server); ESP_LOGI(TAG, "Web server started"); return ESP_OK; } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 77c2377..9b190a7 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -5,6 +5,7 @@ idf_component_register( "apps/app_sysmon.c" "apps/app_settings.c" "apps/app_game.c" + "apps/app_serial.c" INCLUDE_DIRS "." "apps" diff --git a/main/apps/app_serial.c b/main/apps/app_serial.c new file mode 100644 index 0000000..cc6a4e7 --- /dev/null +++ b/main/apps/app_serial.c @@ -0,0 +1,97 @@ +#include "apps.h" +#include "os_gfx.h" +#include "os_status.h" +#include "sys_uart.h" +#include +#include +#include "esp_netif.h" +#include "esp_log.h" + +static const uint32_t s_baud_presets[] = {9600, 19200, 38400, 57600, 115200, 230400}; +#define BAUD_PRESETS_COUNT (sizeof(s_baud_presets) / sizeof(s_baud_presets[0])) + +static void serial_init(void) +{ + sys_uart_init(SYS_UART_DEFAULT_BAUD); +} + +static void serial_enter(void) +{ +} + +static void serial_update(uint32_t delta_ms) +{ + (void)delta_ms; + sys_status_t status; + sys_status_get(&status); + sys_gfx_draw_status_bar(&status); + + uint32_t rx_count = 0, tx_count = 0; + sys_uart_get_stats(&rx_count, &tx_count); + uint32_t baud = sys_uart_get_baud_rate(); + + /* Title */ + sys_gfx_draw_str_3x5(3, 9, "SERIAL CONSOLE", COLOR_WHITE); + + /* Web URL / IP Info */ + char ip_str[24] = "192.168.4.1"; + if (status.wifi_state == WIFI_STATE_CONNECTED) { + esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (netif) { + esp_netif_ip_info_t ip_info; + if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) { + esp_ip4addr_ntoa(&ip_info.ip, ip_str, sizeof(ip_str)); + } + } + } + + char url_buf[32]; + snprintf(url_buf, sizeof(url_buf), "IP:%s", ip_str); + sys_gfx_draw_str_3x5(3, 16, url_buf, COLOR_WHITE); + + /* Stats Line: Baud, RX, TX */ + char stats_buf[32]; + snprintf(stats_buf, sizeof(stats_buf), "B:%lu R:%lu T:%lu", + (unsigned long)baud, (unsigned long)rx_count, (unsigned long)tx_count); + sys_gfx_draw_str_3x5(3, 23, stats_buf, COLOR_WHITE); + + sys_gfx_draw_action_bar("C:PING D:BAUD", "H:HOME"); +} + +static void serial_handle_input(input_action_t action) +{ + if (action == INPUT_ACTION_CLICK) { + sys_uart_write_str("ESP32C3-OS PING\r\n"); + sys_gfx_show_toast("TX: PING SENT", 1000); + } else if (action == INPUT_ACTION_DOUBLE_CLICK) { + uint32_t current_baud = sys_uart_get_baud_rate(); + size_t next_idx = 0; + for (size_t i = 0; i < BAUD_PRESETS_COUNT; i++) { + if (s_baud_presets[i] == current_baud) { + next_idx = (i + 1) % BAUD_PRESETS_COUNT; + break; + } + } + sys_uart_set_baud_rate(s_baud_presets[next_idx]); + char toast[24]; + snprintf(toast, sizeof(toast), "BAUD:%lu", (unsigned long)s_baud_presets[next_idx]); + sys_gfx_show_toast(toast, 1200); + } else if (action == INPUT_ACTION_LONG_PRESS) { + sys_uart_clear_rx_buffer(); + sys_gfx_show_toast("BUF CLEARED", 1000); + } +} + +static void serial_exit(void) +{ +} + +const sys_app_t g_app_serial = { + .id = "serial", + .name = "Web Serial", + .init = serial_init, + .enter = serial_enter, + .update = serial_update, + .handle_input = serial_handle_input, + .exit = serial_exit, +}; diff --git a/main/apps/apps.h b/main/apps/apps.h index 429ef30..d26ce1a 100644 --- a/main/apps/apps.h +++ b/main/apps/apps.h @@ -11,6 +11,7 @@ extern const sys_app_t g_app_clock; extern const sys_app_t g_app_sysmon; extern const sys_app_t g_app_settings; extern const sys_app_t g_app_game; +extern const sys_app_t g_app_serial; #ifdef __cplusplus } diff --git a/main/main.c b/main/main.c index 527f706..d2b90c3 100644 --- a/main/main.c +++ b/main/main.c @@ -17,6 +17,7 @@ #include "os_app.h" #include "os_cli.h" #include "sys_wifi.h" +#include "sys_uart.h" #include "apps.h" static const char *TAG = "MICRO_OS"; @@ -94,6 +95,12 @@ void app_main(void) ESP_LOGE(TAG, "Failed to initialize Wi-Fi subsystem: %s", esp_err_to_name(err)); } + // 1c. Hardware UART Subsystem (TX: GPIO 21, RX: GPIO 20) + err = sys_uart_init(115200); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize UART subsystem: %s", esp_err_to_name(err)); + } + // 2. Display & Graphics Subsystem err = sys_gfx_init(); if (err != ESP_OK) { @@ -108,6 +115,7 @@ void app_main(void) sys_app_init(); sys_app_register(&g_app_clock); sys_app_register(&g_app_sysmon); + sys_app_register(&g_app_serial); sys_app_register(&g_app_game); sys_app_register(&g_app_settings);