diff --git a/docs/README.md b/docs/README.md index 93805f7d..1e5c90b3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3683,12 +3683,12 @@ Usage example: redirecting logs to syslog. ```c static void mylog(uint8_t ch) { - static char buf[128]; - static struct mg_iobuf log = { .buf = buf, .size = sizeof(buf), .len = 0}; - log.buf[log.len++] = ch; - if (ch == '\n' || log.len >= log.size) { - syslog(LOG_INFO, "%.*s", (int) log.len, log.buf); - log.len = 0; + static char buf[256]; + static size_t len; + buf[len++] = ch; + if (ch == '\n' || len >= sizeof(buf)) { + syslog(LOG_INFO, "%.*s", (int) len, buf); // Send logs + len = 0; } } ... diff --git a/examples/esp32/uart-bridge/CMakeLists.txt b/examples/esp32/uart-bridge/CMakeLists.txt new file mode 100644 index 00000000..88ab9137 --- /dev/null +++ b/examples/esp32/uart-bridge/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(mongoose-esp32-example) diff --git a/examples/esp32/uart-bridge/Makefile b/examples/esp32/uart-bridge/Makefile new file mode 100644 index 00000000..e9909d35 --- /dev/null +++ b/examples/esp32/uart-bridge/Makefile @@ -0,0 +1,29 @@ +CWD = $(realpath $(CURDIR)) +MNT = $(realpath $(CURDIR)/../../..) +PORT ?= /dev/ttyUSB0 +CMD ?= build + +all: example + +example: + true + +build: Makefile $(wildcard main/*) + docker run --rm $(DA) -v $(MNT):$(MNT) -w $(CWD) espressif/idf idf.py $(CMD) + +flash: build +flash: CMD = flash monitor +flash: DA = --device $(PORT) + +bridge.hex: build + esputil mkhex \ + 0x8000 build/partition_table/partition-table.bin \ + 0x1000 build/bootloader/bootloader.bin \ + 0x100000 build/mongoose-esp32-example.bin > $@ + +flash2: bridge.hex + esputil -b 921600 -fp 0x220 flash bridge.hex + esputil monitor + +clean: + rm -rf build diff --git a/examples/esp32/uart-bridge/README.md b/examples/esp32/uart-bridge/README.md new file mode 100644 index 00000000..aa30f0a7 --- /dev/null +++ b/examples/esp32/uart-bridge/README.md @@ -0,0 +1,61 @@ +# A UART to network bridge for ESP32 + +This example is a demonstration of how Mongoose Library could be integrated +into an embedded device and provide a UART-to-Network bridge capability: + +- A device opens listening TCP port and Websocket port and waits for connections +- When a client connects, data is exchanged with the device's UART +- Everything that client send, is sent to the UART +- Everything that is read from the UART, gets sent to the client +- Multiple clients are allowed +- Live UART console allows to talk to the UART from the web page +- Web UI is hardcoded into the binary and does not need a filesystem + +# Screenshots + +![](../../uart-bridge/screenshots/dashboard.png) + +# Build and flash + +Build requires Docker installed, and uses Espressif's ESP-IDF docker image: + +```sh +make build +make flash PORT=/dev/YOURSERIAL +``` + +# Flash pre-built firmware + +You can flash a pre-built firmware to the ESP32 device using the following +instructions: + +1. Connect your ESP32 device to the workstation. It should be accessible + via a serial port +2. Download and unzip ESP32 flashing tool from https://mongoose.ws/downloads/esputil.zip +3. Download a prebuilt firmware https://mongoose.ws/downloads/uart-bridge.hex into the unzipped directory +4. Start command prompt (or terminal on Mac/Linux). Run `cd + PATH/TO/esputil` to go into the unzipped `esputil/` directory. After that, run + the following command (change `COMPORT` to the board's serial port): + + | OS | Command | + | ------- | ------- | + | Windows |
.\windows\esputil -p COMPORT flash uart-bridge.hex
| + | Linux |
./linux/esputil -p COMPORT flash uart-bridge.hex
| + | MacOS |
./macos/esputil -p COMPORT flash uart-bridge.hex
| + +Next step is to monitor and follow the instructions. + +```sh +esputil -p COMPORT monitor +``` + +Note: if monitor command shows constant restarts, the flash parameters +settings can be wrong. Reflash your device with `-fp ...` flash parameters +settings. For example, WROOM-32 based boards use `-fp 0x220`: + +```sh +esputil -p COMPORT -fp 0x220 flash uart-bridge.hex +``` + +For more on possible options for flash parameters, see +https://github.com/cpq/mdk#flash-parameters diff --git a/examples/esp32/uart-bridge/main/CMakeLists.txt b/examples/esp32/uart-bridge/main/CMakeLists.txt new file mode 100644 index 00000000..f8c1bbe1 --- /dev/null +++ b/examples/esp32/uart-bridge/main/CMakeLists.txt @@ -0,0 +1,10 @@ +idf_component_register(SRCS "main.c" + "wifi.c" + "uart.c" + "cli.c" + "net.c" + "packed_fs.c" + "mongoose.c") +component_compile_options(-DMG_ENABLE_LINES=1) +component_compile_options(-DMG_ENABLE_PACKED_FS=1) +component_compile_options(-DUART_API_IMPLEMENTED=1) diff --git a/examples/esp32/uart-bridge/main/cli.c b/examples/esp32/uart-bridge/main/cli.c new file mode 100644 index 00000000..74cd220d --- /dev/null +++ b/examples/esp32/uart-bridge/main/cli.c @@ -0,0 +1,89 @@ +#include "main.h" + +static void cli_wifi(const char *ssid, const char *pass) { + if (wifi_init(ssid, pass)) { + mg_file_printf(&mg_fs_posix, WIFI_FILE, "{%Q:%Q,%Q:%Q}\n", "ssid", ssid, + "pass", pass); + MG_INFO(("Reboot now")); + } +} + +static void cli_ls(void) { + DIR *dirp = opendir(FS_ROOT); + struct dirent *dp; + if (dirp == NULL) { + MG_ERROR(("Cannot open FS: %d", errno)); + } else { + while ((dp = readdir(dirp)) != NULL) { + /* Do not show current and parent dirs */ + if (strcmp((const char *) dp->d_name, ".") == 0 || + strcmp((const char *) dp->d_name, "..") == 0) { + continue; + } else { + printf("%s\n", dp->d_name); + } + } + closedir(dirp); + } +} + +static void cli_cat(const char *fname) { + char path[MG_PATH_MAX]; + snprintf(path, sizeof(path), "%s/%s", FS_ROOT, fname); + FILE *fp = fopen(path, "r"); + if (fp != NULL) { + int ch; + while ((ch = fgetc(fp)) != EOF) putchar(ch); + fclose(fp); + } +} + +static void cli_rm(const char *fname) { + char path[100]; + snprintf(path, sizeof(path), "%s/%s", FS_ROOT, fname); + remove(path); +} + +void cli(uint8_t input_byte) { + static struct mg_iobuf in; + + if (input_byte == 0 || input_byte == 0xff) return; + if (in.len >= 128) in.len = 0; + mg_iobuf_add(&in, in.len, &input_byte, sizeof(input_byte), 32); + + if (input_byte == '\n') { + const char *arrow = "---"; + char buf0[10], buf1[50], buf2[250]; + + in.buf[in.len] = '\0'; + buf0[0] = buf1[0] = buf2[0] = '\0'; + sscanf((char *) in.buf, "%9s %49s %249[^\r\n]", buf0, buf1, buf2); + + printf("%s CLI command: '%s'\n", arrow, buf0); + if (strcmp(buf0, "reboot") == 0) { + esp_restart(); + } else if (strcmp(buf0, "ls") == 0) { + cli_ls(); + } else if (strcmp(buf0, "cat") == 0) { + cli_cat(buf1); + } else if (strcmp(buf0, "rm") == 0) { + cli_rm(buf1); + } else if (strcmp(buf0, "reboot") == 0) { + esp_restart(); + } else if (strcmp(buf0, "ll") == 0) { + mg_log_set(buf1); + } else if (strcmp(buf0, "wifi") == 0) { + cli_wifi(buf1, buf2); + } else { + printf("%s %s\n", arrow, "Unknown command. Usage:"); + printf("%s %s\n", arrow, " set NAME VALUE"); + printf("%s %s\n", arrow, " rm FILENAME"); + printf("%s %s\n", arrow, " cat FILENAME"); + printf("%s %s\n", arrow, " ls"); + printf("%s %s\n", arrow, " reboot"); + printf("%s %s\n", arrow, " wifi WIFI_NET WIFI_PASS"); + } + printf("%s %s\n", arrow, "CLI output end"); + in.len = 0; + } +} diff --git a/examples/esp32/uart-bridge/main/main.c b/examples/esp32/uart-bridge/main/main.c new file mode 100644 index 00000000..a38e125d --- /dev/null +++ b/examples/esp32/uart-bridge/main/main.c @@ -0,0 +1,49 @@ +// Copyright (c) 2020 Cesanta Software Limited +// All rights reserved + +#include "main.h" + +const char *s_listening_url = "http://0.0.0.0:80"; + +char *config_read(void) { + return mg_file_read(&mg_fs_posix, FS_ROOT "/config.json", NULL); +} + +void config_write(struct mg_str config) { + mg_file_write(&mg_fs_posix, FS_ROOT "/config.json", config.ptr, config.len); +} + +void app_main(void) { + // Mount filesystem + esp_vfs_spiffs_conf_t conf = { + .base_path = FS_ROOT, .max_files = 20, .format_if_mount_failed = true}; + int res = esp_vfs_spiffs_register(&conf); + MG_INFO(("FS at %s initialised, status: %d", conf.base_path, res)); + + // Try to connect to wifi by using saved WiFi credentials + char *json = mg_file_read(&mg_fs_posix, WIFI_FILE, NULL); + if (json != NULL) { + char *ssid = mg_json_get_str(mg_str(json), "$.ssid"); + char *pass = mg_json_get_str(mg_str(json), "$.pass"); + while (!wifi_init(ssid, pass)) (void) 0; + free(ssid); + free(pass); + free(json); + } else { + // If WiFi is not configured, run CLI until configured + MG_INFO(("WiFi is not configured, running CLI. Press enter")); + for (;;) { + uint8_t ch = getchar(); + cli(ch); + usleep(10000); + } + } + + // Connected to WiFi, now start HTTP server + struct mg_mgr mgr; + mg_mgr_init(&mgr); + mg_log_set("3"); + MG_INFO(("Mongoose v%s on %s", MG_VERSION, s_listening_url)); + mg_http_listen(&mgr, s_listening_url, uart_bridge_fn, &mgr); + for (;;) mg_mgr_poll(&mgr, 10); // Infinite event loop +} diff --git a/examples/esp32/uart-bridge/main/main.h b/examples/esp32/uart-bridge/main/main.h new file mode 100644 index 00000000..458e0503 --- /dev/null +++ b/examples/esp32/uart-bridge/main/main.h @@ -0,0 +1,17 @@ +#pragma once + +#include "mongoose.h" + +#include "driver/gpio.h" +#include "driver/uart.h" +#include "esp_spiffs.h" +#include "freertos/FreeRTOS.h" + +#define FS_ROOT "/spiffs" +#define WIFI_FILE FS_ROOT "/wifi.json" +#define UART_NO 1 + +void uart_bridge_fn(struct mg_connection *, int, void *, void *); +int uart_read(void *buf, size_t len); +bool wifi_init(const char *ssid, const char *pass); +void cli(uint8_t ch); diff --git a/examples/esp32/uart-bridge/main/mongoose.c b/examples/esp32/uart-bridge/main/mongoose.c new file mode 120000 index 00000000..7a2752cb --- /dev/null +++ b/examples/esp32/uart-bridge/main/mongoose.c @@ -0,0 +1 @@ +../../../../mongoose.c \ No newline at end of file diff --git a/examples/esp32/uart-bridge/main/mongoose.h b/examples/esp32/uart-bridge/main/mongoose.h new file mode 120000 index 00000000..daff1633 --- /dev/null +++ b/examples/esp32/uart-bridge/main/mongoose.h @@ -0,0 +1 @@ +../../../../mongoose.h \ No newline at end of file diff --git a/examples/esp32/uart-bridge/main/net.c b/examples/esp32/uart-bridge/main/net.c new file mode 120000 index 00000000..11b4a308 --- /dev/null +++ b/examples/esp32/uart-bridge/main/net.c @@ -0,0 +1 @@ +../../../uart-bridge/net.c \ No newline at end of file diff --git a/examples/esp32/uart-bridge/main/packed_fs.c b/examples/esp32/uart-bridge/main/packed_fs.c new file mode 120000 index 00000000..2ad10a44 --- /dev/null +++ b/examples/esp32/uart-bridge/main/packed_fs.c @@ -0,0 +1 @@ +../../../uart-bridge/packed_fs.c \ No newline at end of file diff --git a/examples/esp32/uart-bridge/main/uart.c b/examples/esp32/uart-bridge/main/uart.c new file mode 100644 index 00000000..f0cc7e8a --- /dev/null +++ b/examples/esp32/uart-bridge/main/uart.c @@ -0,0 +1,44 @@ +#include "main.h" + +int uart_close(int no) { + return uart_driver_delete(no); +} + +int uart_open(int no, int rx, int tx, int cts, int rts, int baud) { + uart_config_t uart_config = { + .baud_rate = baud, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = cts > 0 && rts > 0 ? UART_HW_FLOWCTRL_CTS_RTS + : cts > 0 ? UART_HW_FLOWCTRL_CTS + : rts > 0 ? UART_HW_FLOWCTRL_RTS + : UART_HW_FLOWCTRL_DISABLE, + }; + int e1 = uart_param_config(no, &uart_config); + int e2 = uart_set_pin(no, tx, rx, rts, cts); + int e3 = + uart_driver_install(no, UART_FIFO_LEN * 2, UART_FIFO_LEN * 2, 0, NULL, 0); + MG_INFO(("%d: %d/%d/%d, %d %d %d", no, rx, tx, baud, e1, e2, e3)); + if (e1 != ESP_OK || e2 != ESP_OK || e3 != ESP_OK) return -1; + return no; +} + +void uart_init(int tx, int rx, int baud) { + uart_open(UART_NO, rx, tx, -1, -1, baud); +} + +int uart_read(void *buf, size_t len) { + size_t x = 0; + int no = UART_NO; + if (uart_get_buffered_data_len(no, &x) != ESP_OK || x == 0) return 0; + int n = uart_read_bytes(no, buf, len, 10 / portTICK_PERIOD_MS); + MG_DEBUG(("%d bytes: [%.*s]", n, n, (char *) buf)); + return n; +} + +int uart_write(const void *buf, int len) { + int no = UART_NO; + MG_DEBUG(("%d bytes: [%.*s]", len, len, (char *) buf)); + return uart_write_bytes(no, (const char *) buf, len); +} diff --git a/examples/esp32/uart-bridge/main/wifi.c b/examples/esp32/uart-bridge/main/wifi.c new file mode 100644 index 00000000..b4a7e2db --- /dev/null +++ b/examples/esp32/uart-bridge/main/wifi.c @@ -0,0 +1,108 @@ +// Code taken from the ESP32 IDF WiFi station Example + +#include +#include "esp_event.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/task.h" +#include "nvs_flash.h" + +#include "lwip/err.h" +#include "lwip/sys.h" + +#include "mongoose.h" + +static EventGroupHandle_t s_wifi_event_group; + +/* The event group allows multiple bits for each event, but we only care about + * two events: + * - we are connected to the AP with an IP + * - we failed to connect after the maximum amount of retries */ +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static int s_retry_num = 0; + +static void event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) { + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && + event_id == WIFI_EVENT_STA_DISCONNECTED) { + if (s_retry_num < 3) { + esp_wifi_connect(); + s_retry_num++; + MG_INFO(("retry to connect to the AP")); + } else { + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + } + MG_ERROR(("connect to the AP fail")); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; + MG_INFO(("IP ADDRESS: " IPSTR ". Go to:", IP2STR(&event->ip_info.ip))); + MG_INFO(("http://" IPSTR, IP2STR(&event->ip_info.ip))); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +bool wifi_init(const char *ssid, const char *pass) { + bool result = false; + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || + ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + s_wifi_event_group = xEventGroupCreate(); + + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + esp_event_handler_instance_t instance_any_id; + esp_event_handler_instance_t instance_got_ip; + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id)); + ESP_ERROR_CHECK(esp_event_handler_instance_register( + IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip)); + + wifi_config_t c = {.sta = {.threshold = {.authmode = WIFI_AUTH_WPA2_PSK}, + .pmf_cfg = {.capable = true, .required = false}}}; + snprintf((char *) c.sta.ssid, sizeof(c.sta.ssid), "%s", ssid); + snprintf((char *) c.sta.password, sizeof(c.sta.password), "%s", pass); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &c)); + ESP_ERROR_CHECK(esp_wifi_start()); + MG_DEBUG(("wifi_init_sta finished.")); + + EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, pdFALSE, portMAX_DELAY); + + if (bits & WIFI_CONNECTED_BIT) { + MG_INFO(("connected to ap SSID:%s", ssid)); + result = true; + } else if (bits & WIFI_FAIL_BIT) { + MG_ERROR(("Failed to connect to SSID:%s, password:%s", ssid, pass)); + } else { + MG_ERROR(("UNEXPECTED EVENT")); + } + + /* The event will not be processed after unregister */ + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip)); + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id)); + vEventGroupDelete(s_wifi_event_group); + return result; +} diff --git a/examples/esp32/uart-bridge/make b/examples/esp32/uart-bridge/make new file mode 100644 index 00000000..e69de29b diff --git a/examples/esp32/uart-bridge/partitions.csv b/examples/esp32/uart-bridge/partitions.csv new file mode 100644 index 00000000..8ab9ab77 --- /dev/null +++ b/examples/esp32/uart-bridge/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +storage, data, spiffs, 0x10000, 0x10000, +factory, app, factory, 0x100000, 1M, diff --git a/examples/esp32/uart-bridge/sdkconfig.defaults b/examples/esp32/uart-bridge/sdkconfig.defaults new file mode 100644 index 00000000..607180e5 --- /dev/null +++ b/examples/esp32/uart-bridge/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +#CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 diff --git a/examples/uart-bridge/Makefile b/examples/uart-bridge/Makefile index 5a16e952..ea0f27d5 100644 --- a/examples/uart-bridge/Makefile +++ b/examples/uart-bridge/Makefile @@ -16,4 +16,4 @@ $(PROG): $(SOURCES) $(FILES_TO_EMBED) $(CC) -W -Wall -Wextra -O0 -g3 $(CFLAGS) -o $(PROG) $(SOURCES) clean: - rm -rf $(PROG) *.o *.dSYM *.gcov *.gcno *.gcda *.obj *.exe *.ilk *.pdb log.txt pack + rm -rf $(PROG) *.o *.dSYM *.gcov *.gcno *.gcda *.obj *.exe *.ilk *.pdb log.txt pack config.json diff --git a/examples/uart-bridge/net.c b/examples/uart-bridge/net.c index c54aa27f..86dfd9bd 100644 --- a/examples/uart-bridge/net.c +++ b/examples/uart-bridge/net.c @@ -18,7 +18,7 @@ struct state { int tx, rx, baud; } s_state = {.tcp = {.enable = true}, .websocket = {.enable = true}, - .mqtt = {.enable = true}, + .mqtt = {.enable = false}, .tx = 5, .rx = 4, .baud = 115200}; diff --git a/examples/uart-bridge/screenshots/dashboard.png b/examples/uart-bridge/screenshots/dashboard.png index 0ae0f04e..02f344fe 100644 Binary files a/examples/uart-bridge/screenshots/dashboard.png and b/examples/uart-bridge/screenshots/dashboard.png differ