diff --git a/data/index.html b/data/index.html index 95c7a4b..cf1c43e 100644 --- a/data/index.html +++ b/data/index.html @@ -6,221 +6,46 @@ Owie - Status - + @@ -288,40 +113,11 @@

Owie Status

Unlock Board -
-
- -
-

Owie %OWIE_version%

- \ No newline at end of file diff --git a/data/monitor.html b/data/monitor.html index 76a4cff..1be039b 100644 --- a/data/monitor.html +++ b/data/monitor.html @@ -1,80 +1,104 @@ - - - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/data/settings.html b/data/settings.html index cb61a09..a9ee08a 100644 --- a/data/settings.html +++ b/data/settings.html @@ -1,62 +1,99 @@ -
-
- WiFi options: -

Owie WiFi network name override:

-

Password:

-

Power (dBm):
- -

-
- Board locking: -

- -
-
- -
- \ No newline at end of file + + + + + \ No newline at end of file diff --git a/data/styles.css b/data/styles.css new file mode 100644 index 0000000..67a1756 --- /dev/null +++ b/data/styles.css @@ -0,0 +1,123 @@ +div, +fieldset, +input, +select { + padding: 5px; + font-size: 1em; +} + +fieldset { + background: #4f4f4f; +} + +p { + margin: 0.5em 0; +} + +input { + width: 100%; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + background: #dddddd; + color: #000000; +} + +input[type=checkbox], +input[type=radio] { + width: 1em; + margin-right: 6px; + vertical-align: -1px; +} + +input[type=range] { + width: 99%; +} + +select { + width: 100%; + background: #dddddd; + color: #000000; +} + +textarea { + resize: vertical; + width: 98%; + height: 318px; + padding: 5px; + overflow: auto; + background: #1f1f1f; + color: #65c115; +} + +body { + text-align: center; + font-family: verdana, sans-serif; + background: #252525; +} + +td { + padding: 0px; +} + +button { + border: 0; + border-radius: 0.3rem; + background: #1fa3ec; + color: #faffff; + line-height: 2.4rem; + font-size: 1.2rem; + width: 100%; + -webkit-transition-duration: 0.4s; + transition-duration: 0.4s; + cursor: pointer; +} + +button:hover { + background: #0e70a4; +} + +.bgrn { + background: #47c266; +} + +.bgrn:hover { + background: #5aaf6f; +} + +a { + color: #1fa3ec; + text-decoration: none; +} + +.p { + float: left; + text-align: left; +} + +.q { + float: right; + text-align: right; +} + +.r { + border-radius: 0.3em; + padding: 2px; + margin: 6px 2px; +} + +.hf { + display: none; +} + +.cell-voltages td { + padding: 2px; + background: #354b4d; + border: 0px; + border-radius: 0.2rem; +} + +h2 { + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/data/update.html b/data/update.html new file mode 100644 index 0000000..7dea25f --- /dev/null +++ b/data/update.html @@ -0,0 +1,27 @@ + + + + + + + + Owie - Firmware Update + + + + +
+
+

Owie Update

+
+
+
+ Upload your firmware: + +
+
+
+
+ + + \ No newline at end of file diff --git a/data/update_failed_template.html b/data/update_failed_template.html new file mode 100644 index 0000000..8234d90 --- /dev/null +++ b/data/update_failed_template.html @@ -0,0 +1,23 @@ + + + + + + + + Owie Failed + + + + +
+
+

Update Failed: %UPDATE_ERROR%

+

+ +

+
+
+ + + \ No newline at end of file diff --git a/data/update_successful_response.html b/data/update_successful_response.html new file mode 100644 index 0000000..d8d0f31 --- /dev/null +++ b/data/update_successful_response.html @@ -0,0 +1,22 @@ + + + + + + + + + Owie update + + + + +
+
+

Success - rebooting...

+

IMPORTANT: keep the board powered on until Owie WiFi becomes available again!

+
+
+ + + \ No newline at end of file diff --git a/data/wifi.html b/data/wifi.html index 7bd29d9..aa51636 100644 --- a/data/wifi.html +++ b/data/wifi.html @@ -1,7 +1,31 @@ -
-

WiFi Network to connect to

-


- -
\ No newline at end of file + + + + + + + + Owie - Settings + + + + +
+
+

Owie Settings

+ (%DISPLAY_AP_NAME%) +
+
+

WiFi Network to connect to

+


+ +

+ +

+
+
+ + + \ No newline at end of file diff --git a/include/arduino_ota.h b/include/arduino_ota.h deleted file mode 100644 index 529682f..0000000 --- a/include/arduino_ota.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef OWIE_ARDUINO_OTA_H -#define OWIE_ARDUINO_OTA_H - -void setupArduinoOTA(); - -#endif /* OWIE_ARDUINO_OTA_H */ \ No newline at end of file diff --git a/include/async_ota.h b/include/async_ota.h new file mode 100644 index 0000000..7c64411 --- /dev/null +++ b/include/async_ota.h @@ -0,0 +1,46 @@ +#ifndef ASYNC_OTA_H +#define ASYNC_OTA_H +#include + +// Based on AsyncElegantOTA. + +class AsyncWebServer; +class AsyncWebServerRequest; + +class AsyncOtaClass { + private: + const uint8_t* landingPage_ = nullptr; + size_t landingPageLen_ = 0; + const uint8_t* updateSuccessfuleResponse_ = nullptr; + size_t updateSuccessfuleResponseLen_ = 0; + const uint8_t* updateFailedTemplate_ = nullptr; + size_t updateFailedTemplateLen_ = 0; + + std::function startCallback_; + std::function endCallback_; + + void respondToOtaPostRequest(AsyncWebServerRequest *request); + + public: + AsyncOtaClass(const uint8_t* landingPage, size_t landingPageLen, + const uint8_t* updateSuccessfuleResponse, + size_t updateSuccessfuleResponseLen, + const uint8_t* updateFailedTemplate, + size_t updateFailedTemplateLen, + const std::function& startCallback, + const std::function& endCallback) + : landingPage_(landingPage), + landingPageLen_(landingPageLen), + updateSuccessfuleResponse_(updateSuccessfuleResponse), + updateSuccessfuleResponseLen_(updateSuccessfuleResponseLen), + updateFailedTemplate_(updateFailedTemplate), + updateFailedTemplateLen_(updateFailedTemplateLen), + startCallback_(startCallback), + endCallback_(endCallback) {}; + + void listen(AsyncWebServer* server); +}; + +extern AsyncOtaClass AsyncOta; + +#endif \ No newline at end of file diff --git a/include/global_instances.h b/include/global_instances.h deleted file mode 100644 index 073c1cd..0000000 --- a/include/global_instances.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef GLOBAL_INSTANCES_H -#define GLOBAL_INSTANCES_H - -#include "ArduinoOTA.h" -#include "HardwareSerial.h" - -extern HardwareSerial Serial; -extern ArduinoOTAClass ArduinoOTA; - -#endif // GLOBAL_INSTANCES_H \ No newline at end of file diff --git a/include/web_ota.h b/include/web_ota.h deleted file mode 100644 index b4586f3..0000000 --- a/include/web_ota.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef WEB_OTA_H -#define WEB_OTA_H - -class AsyncWebServer; - -class WebOta { - public: - static void begin(AsyncWebServer* server); -}; - -#endif \ No newline at end of file diff --git a/pio_tools/gen_data.py b/pio_tools/gen_data.py index 11285c0..5e55c4b 100644 --- a/pio_tools/gen_data.py +++ b/pio_tools/gen_data.py @@ -9,7 +9,8 @@ def ReadAndMaybeMinifyFiles(fullPath): - if not fullPath.endswith('.html'): + _, extension = os.path.splitext(fullPath) + if not extension in ['.html', '.js', '.css']: with open(fullPath, "rb") as f: return f.read() originalSize = os.stat(fullPath).st_size @@ -28,9 +29,9 @@ def ReadAndMaybeMinifyFiles(fullPath): def GenData(): dataDir = os.path.join(env["PROJECT_DIR"], "data") - print("dataDir = %s\n" % dataDir) + print("dataDir = %s" % dataDir) genDir = os.path.join(env.subst("$BUILD_DIR"), 'inline_data') - print("genDir = %s\n" % genDir) + print("genDir = %s" % genDir) if not os.path.exists(dataDir): return if not os.path.exists(genDir): diff --git a/pio_tools/platformio_upload.py b/pio_tools/platformio_upload.py new file mode 100644 index 0000000..02e735d --- /dev/null +++ b/pio_tools/platformio_upload.py @@ -0,0 +1,53 @@ +# Allows PlatformIO to upload directly to AsyncElegantOTA +# +# To use: +# - copy this script into the same folder as your platformio.ini +# - set the following for your project in platformio.ini: +# +# extra_scripts = platformio_upload.py +# upload_protocol = custom +# upload_url = +# +# An example of an upload URL: +# upload_URL = http://192.168.1.123/update + +import requests +import hashlib +Import('env') + +try: + from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor + from tqdm import tqdm +except ImportError: + env.Execute("$PYTHONEXE -m pip install requests_toolbelt") + env.Execute("$PYTHONEXE -m pip install tqdm") + from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor + from tqdm import tqdm + +def on_upload(source, target, env): + firmware_path = str(source[0]) + upload_url = env.GetProjectOption('upload_url') + + with open(firmware_path, 'rb') as firmware: + md5 = hashlib.md5(firmware.read()).hexdigest() + firmware.seek(0) + encoder = MultipartEncoder(fields={ + 'MD5': md5, + 'firmware': ('firmware', firmware, 'application/octet-stream')} + ) + + bar = tqdm(desc='Upload Progress', + total=encoder.len, + dynamic_ncols=True, + unit='B', + unit_scale=True, + unit_divisor=1024 + ) + + monitor = MultipartEncoderMonitor(encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n)) + + response = requests.post(upload_url, data=monitor, headers={'Content-Type': monitor.content_type}) + bar.close() + print(response,response.text) + +env.Replace(UPLOADCMD=on_upload) \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 6f27c72..598133b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,7 +4,7 @@ data_dir = nonexistent [env:d1_mini_lite_clone] platform = espressif8266 -upload_speed = 115200 +upload_speed = 524288 monitor_speed = 115200 board = d1_mini # Following is necessary for cheap Wemos D1 Lite clones. @@ -28,14 +28,12 @@ build_flags = ;-DDEBUG_EEPROM_ROTATE_PORT=Serial ;-DDEBUG_ESP_CORE ;-DDEBUG_ESP_WIFI - ;-DDEBUG_ESP_HTTP_UPDATE - ;-DEBUG_ESP_OTA - ;-DEBUG_UPDATER ;-DDEBUG_ESP_UPDATER + ;-DDEBUG_ESP_PORT=Serial + ;-DDEBUG_UPDATER=Serial lib_deps = - ayushsharma82/AsyncElegantOTA@^2.2.7 xoseperez/EEPROM_Rotate@^0.9.2 ottowinter/ESPAsyncWebServer-esphome@^2.1.0 nanopb/Nanopb@^0.4.6 @@ -43,9 +41,12 @@ lib_deps = [env:ota] extends = env:d1_mini_lite_clone -upload_protocol = espota +extra_scripts = + pre:pio_tools/gen_data.py + pio_tools/platformio_upload.py +upload_url = http://owie-c131.lan/update +upload_protocol = custom ;board_build.gzip_fw = true -upload_port = 192.168.1.161 [env:native] platform = native diff --git a/src/arduino_ota.cpp b/src/arduino_ota.cpp deleted file mode 100644 index 8c43c40..0000000 --- a/src/arduino_ota.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "arduino_ota.h" - -#include "global_instances.h" -#include "settings.h" -#include "task_queue.h" - -void setupArduinoOTA() { - ArduinoOTA.begin(false /* useMDNS */); - ArduinoOTA.onStart([]() { - disableFlashPageRotation(); - saveSettings(); - }); - TaskQueue.postRecurringTask([]() { ArduinoOTA.handle(); }); -} \ No newline at end of file diff --git a/src/async_ota.cpp b/src/async_ota.cpp new file mode 100644 index 0000000..3d1b197 --- /dev/null +++ b/src/async_ota.cpp @@ -0,0 +1,116 @@ +#include "async_ota.h" + +#include + +#include "ESPAsyncWebServer.h" +#include "data.h" +#include "flash_hal.h" +#include "settings.h" + +namespace { +class StringPrint : public Print { + private: + String s; + + public: + String getString() const { return s; } + virtual size_t write(uint8_t c) override { + s.concat((char)c); + return 1; + } +}; + +String getUpdateError() { + StringPrint p; + Update.printError(p); + return p.getString(); +} + +} // namespace + +void AsyncOtaClass::respondToOtaPostRequest(AsyncWebServerRequest *request) { + // the request handler is triggered after the upload has finished + int responseCode; + const uint8_t *reponse; + size_t responseLen; + boolean error = Update.hasError(); + if (error) { + responseCode = 500; + reponse = this->updateFailedTemplate_; + responseLen = this->updateFailedTemplateLen_; + } else { + responseCode = 200; + reponse = this->updateSuccessfuleResponse_; + responseLen = this->updateSuccessfuleResponseLen_; + } + + AsyncWebServerResponse *response = + request->beginResponse_P(responseCode, "text/html", reponse, responseLen, + [&](const String &varName) { + if (varName == "UPDATE_ERROR") { + return getUpdateError(); + } + return String("wat"); + }); + response->addHeader("Connection", "close"); + response->addHeader("Access-Control-Allow-Origin", "*"); + request->send(response); + if (!error) { + this->endCallback_(); + } +} + +void AsyncOtaClass::listen(AsyncWebServer *server) { + server->on("/update", HTTP_GET, [&](AsyncWebServerRequest *request) { + request->send(request->beginResponse_P(200, "text/html", this->landingPage_, + this->landingPageLen_)); + }); + server->on( + "/update", HTTP_POST, + [&](AsyncWebServerRequest *request) { + this->respondToOtaPostRequest(request); + }, + [&](AsyncWebServerRequest *request, String filename, size_t index, + uint8_t *data, size_t len, bool final) { + // Upload handles chunks in data + if (!index) { + if (request->hasParam("MD5", true) && + !Update.setMD5(request->getParam("MD5", true)->value().c_str())) { + return request->send(400, "text/plain", "MD5 parameter invalid"); + } + + Update.runAsync(true); + uint32_t maxSketchSpace = + (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + if (!Update.begin(maxSketchSpace, + U_FLASH)) { // Start with max available size + respondToOtaPostRequest(request); + } + this->startCallback_(); + } + + // Write chunked data to the free sketch space + if (len && Update.write(data, len) != len) { + respondToOtaPostRequest(request); + } + + if (final) { // if the final flag is set then this is the last frame of + // data + if (!Update.end( + true)) { // true to set the size to the current progress + respondToOtaPostRequest(request); + } + } + }); +} + +AsyncOtaClass AsyncOta( + UPDATE_HTML_PROGMEM_ARRAY, UPDATE_HTML_SIZE, + UPDATE_SUCCESSFUL_RESPONSE_HTML_PROGMEM_ARRAY, + UPDATE_SUCCESSFUL_RESPONSE_HTML_SIZE, + UPDATE_FAILED_TEMPLATE_HTML_PROGMEM_ARRAY, UPDATE_FAILED_TEMPLATE_HTML_SIZE, + []() { + disableFlashPageRotation(); + saveSettings(); + }, + saveSettingsAndRestartSoon); diff --git a/src/bms_main.cpp b/src/bms_main.cpp index 742fd21..7c9e035 100644 --- a/src/bms_main.cpp +++ b/src/bms_main.cpp @@ -1,9 +1,7 @@ #include -#include "arduino_ota.h" #include "bms_relay.h" #include "charging_tracker.h" -#include "global_instances.h" #include "network.h" #include "packet.h" #include "settings.h" @@ -22,6 +20,8 @@ namespace { // of the TX A line. void IRAM_ATTR txPinRiseInterrupt() { digitalWrite(TX_INVERSE_OUT_PIN, 0); } void IRAM_ATTR txPinFallInterrupt() { digitalWrite(TX_INVERSE_OUT_PIN, 1); } + +HardwareSerial Serial(0); } // namespace BmsRelay *relay; @@ -105,6 +105,5 @@ void bms_setup() { setupWifi(); setupWebServer(relay); - setupArduinoOTA(); TaskQueue.postRecurringTask([]() { relay->loop(); }); } diff --git a/src/global_instances.cpp b/src/global_instances.cpp deleted file mode 100644 index ead8576..0000000 --- a/src/global_instances.cpp +++ /dev/null @@ -1,4 +0,0 @@ -#include "global_instances.h" - -HardwareSerial Serial(0); -ArduinoOTAClass ArduinoOTA; diff --git a/src/network.cpp b/src/network.cpp index 4bc6c7a..e4275d7 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -3,6 +3,7 @@ #include #include #include + #include #include "ArduinoJson.h" @@ -11,7 +12,7 @@ #include "data.h" #include "settings.h" #include "task_queue.h" -#include "web_ota.h" +#include "async_ota.h" namespace { DNSServer dnsServer; @@ -21,11 +22,11 @@ AsyncWebSocket ws("/rawdata"); const String defaultPass("****"); BmsRelay *relay; -const String owie_version = "1.3.0"; +const String owie_version = "1.3.1"; String dumpChargingPointsFromSettings() { String val; - const ChargingDataMsg& proto = Settings->charging_data; + const ChargingDataMsg &proto = Settings->charging_data; val.reserve(proto.voltage_offsets_count * 10 + 100); val.concat("tracked_cell_index = "); val.concat(proto.tracked_cell_index); @@ -87,7 +88,9 @@ String generateOwieStatusJson() { return jsonOutput; } -bool lockingPreconditionsMet() { return strlen(Settings->ap_self_password) > 0; } +bool lockingPreconditionsMet() { + return strlen(Settings->ap_self_password) > 0; +} const char *lockedStatusDataAttrValue() { return Settings->is_locked ? "1" : ""; }; @@ -202,19 +205,17 @@ void setupWifi() { void setupWebServer(BmsRelay *bmsRelay) { relay = bmsRelay; - WebOta::begin(&webServer); + AsyncOta.listen(&webServer); webServer.addHandler(&ws); webServer.onNotFound([](AsyncWebServerRequest *request) { - if (request->host().indexOf("owie.local") >= 0) { - request->send(404); - return; - } - request->redirect("http://" + WiFi.softAPIP().toString() + "/"); - }); - - webServer.on("/charging_status", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send(200, "text/plain", dumpChargingPointsFromSettings()); + request->redirect("http://" + request->client()->localIP().toString() + "/"); }); + webServer.on("/favicon.ico", HTTP_GET, + [](AsyncWebServerRequest *request) { request->send(404); }); + webServer.on( + "/charging_status", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", dumpChargingPointsFromSettings()); + }); webServer.on("/autoupdate", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "application/json", generateOwieStatusJson()); @@ -224,6 +225,12 @@ void setupWebServer(BmsRelay *bmsRelay) { request->send_P(200, "text/html", INDEX_HTML_PROGMEM_ARRAY, INDEX_HTML_SIZE, templateProcessor); }); + webServer.on("/styles.css", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P( + 200, "text/css", STYLES_CSS_PROGMEM_ARRAY, STYLES_CSS_SIZE); + response->addHeader("Cache-Control", "max-age=3600"); + request->send(response); + }); webServer.on("/wifi", HTTP_ANY, [](AsyncWebServerRequest *request) { switch (request->method()) { case HTTP_GET: @@ -270,7 +277,8 @@ void setupWebServer(BmsRelay *bmsRelay) { apSelfPassword->value().length() > 0)) { // this check is necessary so the user can't set a too // small password and thus the network wont' show up - request->send(400, "text/html", "AP password must be between 8 and 31 characters"); + request->send(400, "text/html", + "AP password must be between 8 and 31 characters"); return; } if (apSelfName == nullptr || diff --git a/src/recovery.cpp b/src/recovery.cpp index c84aa87..940077f 100644 --- a/src/recovery.cpp +++ b/src/recovery.cpp @@ -6,10 +6,10 @@ #include #include +#include "async_ota.h" +#include "data.h" #include "settings.h" #include "task_queue.h" -#include "web_ota.h" -#include "global_instances.h" #define SSID_NAME ("Owie-recovery") @@ -22,22 +22,19 @@ void recovery_setup() { WiFi.setOutputPower(0); WiFi.mode(WIFI_AP); WiFi.softAP(SSID_NAME); - ArduinoOTA.setHostname("owie"); - ArduinoOTA.begin(false /** useMDNS */); - ArduinoOTA.onStart([]() { - disableFlashPageRotation(); - saveSettings(); - }); dnsServer.start(53, "*", WiFi.softAPIP()); // DNS spoofing. dnsServer.setErrorReplyCode(DNSReplyCode::NoError); nukeSettings(); webServer.onNotFound([&](AsyncWebServerRequest *request) { request->redirect("http://" + WiFi.softAPIP().toString() + "/update"); }); - WebOta::begin(&webServer); - webServer.begin(); - TaskQueue.postRecurringTask([&]() { - dnsServer.processNextRequest(); - ArduinoOTA.handle(); + webServer.on("/styles.css", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P( + 200, "text/css", STYLES_CSS_PROGMEM_ARRAY, STYLES_CSS_SIZE); + response->addHeader("Cache-Control", "max-age=3600"); + request->send(response); }); + AsyncOta.listen(&webServer); + webServer.begin(); + TaskQueue.postRecurringTask([&]() { dnsServer.processNextRequest(); }); } diff --git a/src/settings.cpp b/src/settings.cpp index c0d03dc..9b34d01 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -73,7 +73,7 @@ int32_t saveSettings() { int32_t saveSettingsAndRestartSoon() { int32_t code = saveSettings(); - TaskQueue.postOneShotTask([]() { ESP.restart(); }, 2000L); + TaskQueue.postOneShotTask([]() { ESP.restart(); }, 1000L); return code; } diff --git a/src/web_ota.cpp b/src/web_ota.cpp deleted file mode 100644 index e2161b8..0000000 --- a/src/web_ota.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "web_ota.h" - -#include - -#include "global_instances.h" -#include "AsyncElegantOTA.h" - -void WebOta::begin(AsyncWebServer* server) { AsyncElegantOTA.begin(server); }