From 621e8cd18c3ae93b031d84a0a2c5446012160d17 Mon Sep 17 00:00:00 2001 From: gilex-dev Date: Fri, 22 Aug 2025 19:14:01 +0200 Subject: [PATCH] Add ESP32 support, WiFi setup page and initial HTTPS support for ESP32 & ESP8266 --- .prettierrc | 2 +- README.md | 3 + TODO.md | 7 +- boards/esp32c3-mini.json | 59 +++++++------- data/web/add.html | 74 ++++++++++++++++++ data/web/app.js | 51 ++++++++++++ data/web/index.html | 70 +++++++++++++++++ data/web/logo.png | Bin 0 -> 27014 bytes data/web/style.css | 116 ++++++++++++++++++++++++++++ lib/WiFiManager/src/WiFiManager.cpp | 91 +++++++++++++++++----- lib/WiFiManager/src/WiFiManager.h | 11 ++- platformio.ini | 35 ++++++++- src/main.cpp | 51 +++++++++++- src/main.h | 3 + 14 files changed, 512 insertions(+), 61 deletions(-) create mode 100644 data/web/add.html create mode 100644 data/web/app.js create mode 100644 data/web/index.html create mode 100644 data/web/logo.png create mode 100644 data/web/style.css diff --git a/.prettierrc b/.prettierrc index 9510464..97b367e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { - "semi": false, + "semi": true, "singleQuote": true, "quoteProps": "as-needed", "jsxSingleQuote": true, diff --git a/README.md b/README.md index 9e21750..3a61023 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ A template for developing NOT[^1]-devices using micro controllers like the ESP8266 or ESP32 (support pending) using [PlatformIO](https://platformio.org/) and the [Arduino environment](https://github.com/arduino/ArduinoCore-API). +The WebGUI is tested on `Android Browser 2.3.5` (yikes!) and should work with +other outdated browsers, so you can use your old devices to control your NOT! + [^1]: Network Of Things (a.k.a. IOT / marketing buzzword for network / internet enabled devices) diff --git a/TODO.md b/TODO.md index e909923..0a56f5a 100644 --- a/TODO.md +++ b/TODO.md @@ -4,14 +4,15 @@ ## Planned -- [ ] load WiFi configuration from filesystem +- [x] load WiFi STA configuration from filesystem +- [x] HTTPS +- [ ] load WiFi AP configuration from filesystem - [ ] edit WiFi configuration from browser -- [ ] HTTPS - [ ] API & Console configuration - [ ] IPv4 & IPv6 configuration, DNS, captive portal - [ ] physical access validation for reset / recovery - [ ] MQTT / HomeAssistant integration -- [ ] ESP32 support +- [x] ESP32 support - [ ] SPA frontend, live - [ ] Change peripheral / pin configuration - [ ] SD card logging diff --git a/boards/esp32c3-mini.json b/boards/esp32c3-mini.json index b1155f9..c744416 100644 --- a/boards/esp32c3-mini.json +++ b/boards/esp32c3-mini.json @@ -1,35 +1,30 @@ { - "build": { - "arduino":{ - "ldscript": "esp32c3_out.ld" + "build": { + "arduino": { + "ldscript": "esp32c3_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1", + "f_cpu": "160000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "mcu": "esp32c3", + "variant": "esp32c3-mini", + "variants_dir": "variants" }, - "core": "esp32", - "extra_flags": "-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1", - "f_cpu": "160000000L", - "f_flash": "80000000L", - "flash_mode": "qio", - "mcu": "esp32c3", - "variant": "esp32c3-mini", - "variants_dir": "variants" - }, - "connectivity": [ - "wifi" - ], - "debug": { - "openocd_target": "esp32c3.cfg" - }, - "frameworks": [ - "arduino", - "espidf" - ], - "name": "Espressif ESP32-C3-MINI", - "upload": { - "flash_size": "4MB", - "maximum_ram_size": 327680, - "maximum_size": 4194304, - "require_upload_port": true, - "speed": 460800 - }, - "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html", - "vendor": "Espressif" + "connectivity": ["wifi"], + "debug": { + "openocd_target": "esp32c3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Espressif ESP32-C3 Mini", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html", + "vendor": "Espressif" } diff --git a/data/web/add.html b/data/web/add.html new file mode 100644 index 0000000..153a0d0 --- /dev/null +++ b/data/web/add.html @@ -0,0 +1,74 @@ + + + + + + + + Tiny NOT Add WiFi + + +
+

WiFi configuration

+ +
+

Add configuration

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ IPv4 Configuration + + + + +
+
+
+
+
+
+ + + + diff --git a/data/web/app.js b/data/web/app.js new file mode 100644 index 0000000..7d1af05 --- /dev/null +++ b/data/web/app.js @@ -0,0 +1,51 @@ +var cssPropertySupported = (function () { + var mem = {}; // save test results for efficient future calls with same parameters + + return function cssPropertySupported(prop, values) { + if (mem[prop + values]) return mem[prop + values]; + + var element = document.createElement('p'), + index = values.length, + value, + result = false; + + try { + while (index--) { + value = values[index]; + element.style.setProperty(prop, value); + + if (element.style.getPropertyValue(prop) === value) { + result = true; + break; + } + } + } catch (pError) {} + + mem[prop + values] = result; + return result; + }; +})(); + +if (!cssPropertySupported('display', ['flex'])) { + fillViewport(); +} + +function fillViewport() { + var vh = window.innerHeight * 0.01; + + var main = document.querySelector('main'); + var footer = document.querySelector('footer'); + main.style.minHeight = window.innerHeight - footer.offsetHeight * 2 + 'px'; + + window.addEventListener('resize', function () { + main.style.minHeight = + window.innerHeight - footer.offsetHeight * 2 + 'px'; + }); + + // also hide early android browser's zoom controls + var meta = document.createElement('meta'); + meta.name = 'viewport'; + meta.content = + 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'; + document.getElementsByTagName('head')[0].appendChild(meta); +} diff --git a/data/web/index.html b/data/web/index.html new file mode 100644 index 0000000..45f1546 --- /dev/null +++ b/data/web/index.html @@ -0,0 +1,70 @@ + + + + + + + + Tiny NOT setup + + +
+

Start

+ +
+

Status

+ + + + + + + + + + + +
FieldValue
Uptime + +
+
+ +
+

Stored

+
+
    +
  1. + stored SSID - stored BSSID - RSSI + +
  2. +
+
+
+ +
+

Scan results

+ +
+
    +
  1. SSID - BSSID - RSSI +
    + Add + +
    +
  2. +
+
+
+
+ + + + diff --git a/data/web/logo.png b/data/web/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..628655f84944c49e5aa3c3432da7663628008ff9 GIT binary patch literal 27014 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 A;s5{u literal 0 HcmV?d00001 diff --git a/data/web/style.css b/data/web/style.css new file mode 100644 index 0000000..df84f76 --- /dev/null +++ b/data/web/style.css @@ -0,0 +1,116 @@ +* { + box-sizing: border-box; + margin: 0; + font-family: Arial, Helvetica, sans-serif; + color: white; +} + +html { + /* min-height: 100%; */ + background-color: #232323; +} + +h1, +h2 { + text-align: center; + margin-top: 0.5rem; + margin-bottom: 1rem; +} + +[data-symbol]::before { + content: attr(data-symbol); + padding-right: 0.5rem; +} + +input[type='submit'], +a { + background-color: #233c54; +} + +input[type='submit']:hover, +input[type='submit']:focus-visible, +a:hover, +a:focus-visible { + background-color: #61a8ea; +} + +input, +a { + padding: 0.25rem; + border-radius: 4px; + display: inline-block; +} + +ol { + /* list-style-position: inside; */ + display: inline-block; + text-align: left; +} + +table { + display: inline-table; +} + +section { + margin-bottom: 1rem; +} + +.list-actions { + display: inline-block; +} + +.section-actions { + margin: 0.5rem 0; +} + +input { + border: 1px solid grey; + background-color: rgb(40, 40, 40); +} + +body { + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +main { + flex-grow: 1; + padding-bottom: 40px; + display: block; +} + +input { + display: block; + margin: 0.5rem auto; +} + +input[required] { + border-color: red; +} + +nav, +.align-center { + text-align: center; +} + +form { + background-color: #181818; + border-radius: 8px; + padding: 1rem; + margin: auto; + width: fit-content; + display: inline-block; +} + +form div { + position: relative; +} + +footer { + background-color: #233c54; + text-align: center; + padding: 1rem 0; + border-radius: 8px 8px 0 0; +} diff --git a/lib/WiFiManager/src/WiFiManager.cpp b/lib/WiFiManager/src/WiFiManager.cpp index 3147525..12f1508 100644 --- a/lib/WiFiManager/src/WiFiManager.cpp +++ b/lib/WiFiManager/src/WiFiManager.cpp @@ -10,19 +10,23 @@ #endif #ifdef ESP32 -#include #include #include +#include WiFiMulti wifiMulti; +WiFiEventCb disconnectedEventHandler; +typedef wifi_ap_record_t bss_info; +int scanned_ap_num; #elif defined(ESP8266) #include ESP8266WiFiMulti wifiMulti; +WiFiEventHandler disconnectedEventHandler; +typedef nullptr_t WiFiEventInfo_t; #endif -WiFiEventHandler disconnectedEventHandler; std::vector *connection_queue; JsonDocument doc; -const uint8 *bssid; +const uint8_t *bssid; // keep track of current connection uint currentConfig; @@ -38,7 +42,7 @@ int sortByRSSI(const void *a, const void *b) { bool matches_ssid(const std::vector &networks, JsonVariant config) { for (auto &&network : networks) { - if (memcmp((char *)network->ssid, config["ssid"].as().c_str(), network->ssid_len) == 0) { + if (memcmp((char *)network->ssid, config["ssid"].as().c_str(), strlen((const char *)network->ssid)) == 0) { return true; } } @@ -60,11 +64,23 @@ bool matches_bssid(const std::vector &networks, JsonVariant co return false; } -void disconnectHandler(const WiFiEventStationModeDisconnected &event = {}) { +void startRecoveryAP() { + inRecovery = true; + WiFi.disconnect(); + WiFi.softAP("ESP reached recovery state", NULL); +} + +void disconnectHandler(WiFiEvent_t event, WiFiEventInfo_t info) { if (inRecovery) { return; } + if (connection_queue->size() == 0) { + Log.noticeln("No configurations found to try. Starting recovery sequence"); + startRecoveryAP(); + return; + } + Log.noticeln("In disc-handler"); JsonVariant config = connection_queue->at(currentConfig); if (currentTry < config["retries"]) { @@ -73,9 +89,7 @@ void disconnectHandler(const WiFiEventStationModeDisconnected &event = {}) { } else { if (currentConfig + 1 == connection_queue->size()) { Log.noticeln("Station disconnected. No more configurations to try. Starting recovery sequence"); - inRecovery = true; - WiFi.disconnect(); - WiFi.softAP("ESP reached recovery state", "tempPW12345"); + startRecoveryAP(); return; } else { currentTry = 0; @@ -102,13 +116,22 @@ void disconnectHandler(const WiFiEventStationModeDisconnected &event = {}) { tryStartMillis = millis(); if (!config["hostname"].isNull()) { Log.noticeln("Set hostname %s", config["hostname"].as().c_str()); +#ifdef ESP32 + WiFi.mode(WIFI_MODE_NULL); +#endif WiFi.setHostname(config["hostname"].as().c_str()); } - WiFi.begin(config["ssid"].as(), psk, 0); - Log.noticeln("Set Wifi to ssid: '%s' psk: '%s' bssid: %s", WiFi.SSID().c_str(), WiFi.psk().c_str(), WiFi.BSSIDstr()); + WiFi.begin(config["ssid"].as(), psk, 0, bssid); + Log.noticeln("Set Wifi to ssid: '%s' psk: '%s' bssid: %s", WiFi.SSID().c_str(), WiFi.psk().c_str(), WiFi.BSSIDstr().c_str()); } +#ifdef ESP8266 +void disconnectHandler_ESP8266(const WiFiEventStationModeDisconnected &event = {}) { + disconnectHandler(WiFiEvent_t::WIFI_EVENT_ANY, nullptr); +} +#endif + void getScanResults(int count) { Log.noticeln("Scan done. Found %i radios", count); @@ -116,10 +139,14 @@ void getScanResults(int count) { std::vector networks; networks.resize(count); - uint8 max_ssid_length = 0; + size_t max_ssid_length = 0; for (int i = 0; i < count; i++) { +#ifdef ESP32 + networks.at(i) = (bss_info *)WiFiScanClass::getScanInfoByIndex(i); +#elif defined(ESP8266) networks.at(i) = WiFi.getScanInfoByIndex(i); - max_ssid_length = max(networks.at(i)->ssid_len, max_ssid_length); +#endif + max_ssid_length = max(strlen((const char *)networks.at(i)->ssid), max_ssid_length); } std::sort(networks.begin(), networks.end(), sortByRSSI); @@ -129,7 +156,14 @@ void getScanResults(int count) { for (auto &network : networks) { bssid = network->bssid; snprintf_P(bssid_char, sizeof(bssid_char), "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); - snprintf_P(buffer, sizeof(buffer), "SSID: %-*s, RSSI: %i, BSSID: %s, hidden: %i", max_ssid_length, network->ssid, network->rssi, bssid_char, network->is_hidden); +#ifdef ESP32 + const bool ssid_is_hidden = strlen((const char *)network->ssid) == 0; + const uint8_t channel = network->primary; +#elif defined(ESP8266) + const bool ssid_is_hidden = network->is_hidden; + const uint8_t channel = network->channel; +#endif + snprintf_P(buffer, sizeof(buffer), "SSID: %-*s, RSSI: %i, BSSID: %s, hidden: %i, channel: %i", max_ssid_length, network->ssid, network->rssi, bssid_char, ssid_is_hidden, channel); Log.noticeln("%s", buffer); } @@ -145,7 +179,17 @@ void getScanResults(int count) { Log.noticeln("Adding %s to queue", config["name"].as().c_str()); } } - disconnectHandler(); // start connecting to configurations + +#ifdef ESP32 + disconnectHandler(WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_START, arduino_event_info_t{}); // start connecting to configurations +#endif +#ifdef ESP8266 + disconnectHandler(WiFiEvent_t::WIFI_EVENT_ANY, nullptr); // start connecting to configurations +#endif +} + +void getScanResultsESP32(WiFiEvent_t event, WiFiEventInfo_t info) { + getScanResults(WiFi.scanComplete()); // assume scan always completed since we are in scan complete CB } wl_status_t WiFiManager::tick(unsigned long update_interval = 100) { @@ -198,15 +242,19 @@ bool WiFiManager::loadFromJson(File file) { setupAP = doc["SetupAP"].as(); for (JsonVariant i : doc["WiFiSta"].as()) { - Serial.println(i["name"].as() + i["ssid"].as() + i["psk"].as()); Log.noticeln("Name: %s", i["name"].as().c_str()); Log.noticeln("SSID: %s", i["ssid"].as().c_str()); Log.noticeln("PSK: %s", i["psk"].as().c_str()); } - disconnectedEventHandler = WiFi.onStationModeDisconnected(disconnectHandler); - +#ifdef ESP32 + WiFi.onEvent(disconnectHandler, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + WiFi.onEvent(getScanResultsESP32, WiFiEvent_t::ARDUINO_EVENT_WIFI_SCAN_DONE); + WiFi.scanNetworks(true, true); +#elif defined(ESP8266) + disconnectedEventHandler = WiFi.onStationModeDisconnected(disconnectHandler_ESP8266); WiFi.scanNetworksAsync(getScanResults, true); +#endif loadConnections(); @@ -216,7 +264,7 @@ bool WiFiManager::loadFromJson(File file) { bool WiFiManager::loadConnections() { WiFi.persistent(false); - WiFi.mode(WIFI_AP_STA); + WiFi.mode(WIFI_STA); IPAddress local_ip, gateway, subnet; local_ip.fromString("10.0.0.1"); @@ -225,8 +273,9 @@ bool WiFiManager::loadConnections() { uint8_t newMACAddress[WL_MAC_ADDR_LENGTH] = {0x32, 0xAE, 0xA4, 0x07, 0x0D, 0x66}; - const bool ap = true; + const bool ap = false; + // TODO: can't set AP channel, can't scan AP channel -> set ap, then start async scan seems to work if (ap) { if (setApMac(newMACAddress)) @@ -243,7 +292,11 @@ bool WiFiManager::loadConnections() { } bool WiFiManager::setApMac(uint8_t mac[WL_MAC_ADDR_LENGTH]) { +#ifdef ESP32 + esp_wifi_set_mac(WIFI_IF_AP, &mac[0]); +#elif defined(ESP8266) wifi_set_macaddr(SOFTAP_IF, &mac[0]); +#endif char charMacAddress[(sizeof(*mac) * 3)]; snprintf_P(charMacAddress, sizeof(charMacAddress), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); diff --git a/lib/WiFiManager/src/WiFiManager.h b/lib/WiFiManager/src/WiFiManager.h index c3a7c6b..7c0409c 100644 --- a/lib/WiFiManager/src/WiFiManager.h +++ b/lib/WiFiManager/src/WiFiManager.h @@ -1,15 +1,22 @@ #ifdef ESP32 -#include +#include #include -#include #elif defined(ESP8266) #include #endif +#ifndef WL_IPV4_LENGTH +#define WL_IPV4_LENGTH 4 +#endif + #ifndef WL_IPV6_LENGTH #define WL_IPV6_LENGTH 16 #endif +#ifndef WL_MAC_ADDR_LENGTH +#define WL_MAC_ADDR_LENGTH 6 +#endif + struct WiFiConnection { char *ssid; char *bssid; diff --git a/platformio.ini b/platformio.ini index 31b146b..4431208 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,6 +15,13 @@ default_envs = nodemcuv2 build_flags = '-D BUILD_VERSION="0.0.1"' -D DEBUG_ESP_PORT=Serial -D NDEBUG extra_scripts = ./extra_scripts/reset_board.py +lib_deps = miguel5612/MQUnifiedsensor @ ^3.0.0 + adafruit/DHT sensor library @ ^1.4.6 + dawidchyrzynski/home-assistant-integration @ ^2.1.0 + arduinogetstarted/ezLED @ ^1.0.1 + https://github.com/gilex-dev/rc-switch.git#SKL-W1B + thijse/ArduinoLog @ ^1.1.1 + bblanchon/ArduinoJson @ ^7.4.2 [env:nodemcuv2] platform = espressif8266 @@ -28,6 +35,28 @@ build_flags = ${common_env_data.build_flags} -D SERIAL_BAUD_RATE=${this.monitor_ extra_scripts = ${common_env_data.extra_scripts} lib_compat_mode = strict lib_ldf_mode = chain -lib_deps = - thijse/ArduinoLog @ ^1.1.1 - bblanchon/ArduinoJson @ ^7.4.2 +lib_deps = ${common_env_data.lib_deps} + +[env:nodemcuv2_ota] +extends = env:nodemcuv2 +upload_protocol = espota +[env:esp32-dev] +upload_port = /dev/ttyUSB0 +framework = arduino +platform = espressif32 +board = esp32dev +upload_speed = 921600 +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +board_build.filesystem = littlefs +build_flags = ${common_env_data.build_flags} -D SERIAL_BAUD_RATE=${this.monitor_speed} -D BUILD_DEBUG=false +extra_scripts = ${common_env_data.extra_scripts} +lib_compat_mode = strict +lib_ldf_mode = chain +lib_deps = ${common_env_data.lib_deps} + hoeken/PsychicHttp @ ^1.2.1 + +[env:esp32c3-mini] +platform = espressif32 +extends = env:esp32dev +board = esp32c3-mini diff --git a/src/main.cpp b/src/main.cpp index 57b1b4a..4ef216a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,32 @@ #include #include +static const char serverCert[] PROGMEM = R"EOF( +-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- +)EOF"; + +static const char serverKey[] PROGMEM = R"EOF( +-----BEGIN RSA PRIVATE KEY----- +-----END RSA PRIVATE KEY----- +)EOF"; + +#ifdef ESP32 +#include +#include +#include +#ifdef PSY_ENABLE_SSL +PsychicHttpsServer server; +#else +PsychicHttpServer server; +#endif + +#elif defined(ESP8266) +#include +BearSSL::ESP8266WebServerSecure server(443); +BearSSL::ServerSessions serverCache(5); +#endif + #ifndef SERIAL_BAUD_RATE #define SERIAL_BAUD_RATE 115200 #endif @@ -16,6 +42,25 @@ WiFiManager manager; unsigned long currentMillis, oldMillis; void setup() { + WiFi.persistent(false); +#ifdef ESP8266 + server.getServer().setRSACert(new BearSSL::X509List(serverCert), new BearSSL::PrivateKey(serverKey)); + + // Cache SSL sessions to accelerate the TLS handshake. + server.getServer().setCache(&serverCache); + + server.on("/", []() { + server.send(200, "text/plain", "Hello from esp8266 over HTTPS!"); + }); + server.begin(); +#elif ESP32 + WiFi.mode(WIFI_MODE_STA); // required by PsychicHttp + server.listen(443, serverCert, serverKey); + server.on("/", [](PsychicRequest *request) { + return request->reply(200, "text/plain", "Hello from esp32 over HTTPS!"); + }); +#endif + Serial.begin(SERIAL_BAUD_RATE); Log.setPrefix(printPrefix); Log.setSuffix(printSuffix); @@ -42,7 +87,6 @@ void setup() { } void loop() { - currentMillis = millis(); if (currentMillis - oldMillis >= 1000) { @@ -52,5 +96,10 @@ void loop() { if (manager.tick(1000) == WL_CONNECTED) { // regularOperationStuff(); } + +#ifdef ESP8266 + esp_yield(); + server.handleClient(); +#endif // importantStuff(); } diff --git a/src/main.h b/src/main.h index e69de29..5367fac 100644 --- a/src/main.h +++ b/src/main.h @@ -0,0 +1,3 @@ +#ifndef WL_MAC_ADDR_LENGTH +#define WL_MAC_ADDR_LENGTH 6 +#endif