diff --git a/data/src/index.html b/data/src/index.html index 0dd9860..dbb46f2 100644 --- a/data/src/index.html +++ b/data/src/index.html @@ -172,8 +172,8 @@
-
-
+
+
min @@ -181,11 +181,21 @@
A restart is required to apply TZ offset.
+
+ +
+
+ + sec +
+
Short amounts might shorten lifespan.
+
+
- +
diff --git a/data/src/js/script.ts b/data/src/js/script.ts index 1000cf6..8c46796 100644 --- a/data/src/js/script.ts +++ b/data/src/js/script.ts @@ -19,39 +19,65 @@ toTime = (secs) => { return obj; } -getBcStatus = () => { - fetch('/api/status', { - method: 'get' - }) - .then(response => response.json()) - .then(jsonData => { - var source = document.getElementById("entry-template").innerHTML; - var template = Handlebars.compile(source); +let processStatusData = (jsonData) => { + var source = document.getElementById("entry-template").innerHTML; + var template = Handlebars.compile(source); - var context = { - timerRunning: jsonData.timerRunning, - memUsage: Math.round(jsonData.espFreeHeap / jsonData.espHeapSize * 100), - memFree: Math.round(jsonData.espFreeHeap / 1024), - memTotal: Math.round(jsonData.espHeapSize / 1024), - uptime: toTime(jsonData.espUptime), - currentScreen: jsonData.currentScreen, - rendered: jsonData.rendered, - data: jsonData.data, - screens: screens, - ledStatus: jsonData.ledStatus ? jsonData.ledStatus.map((t) => (t).toString(16)) : [], - connectionStatus: jsonData.connectionStatus - }; + var context = { + timerRunning: jsonData.timerRunning, + memUsage: Math.round(jsonData.espFreeHeap / jsonData.espHeapSize * 100), + memFree: Math.round(jsonData.espFreeHeap / 1024), + memTotal: Math.round(jsonData.espHeapSize / 1024), + uptime: toTime(jsonData.espUptime), + currentScreen: jsonData.currentScreen, + rendered: jsonData.data, + data: jsonData.data, + screens: screens, + ledStatus: jsonData.ledStatus ? jsonData.ledStatus.map((t) => (t).toString(16)) : [], + connectionStatus: jsonData.connectionStatus + }; - document.getElementById('output').innerHTML = template(context); - }) - .catch(err => { - //error block - }); + document.getElementById('output').innerHTML = template(context); } -interval = setInterval(getBcStatus, 2500); -getBcStatus(); + +if (!!window.EventSource) { + var source = new EventSource('/events'); + + source.addEventListener('open', function (e) { + console.log("Status EventSource Connected"); + if (e.data) { + processStatusData(JSON.parse(e.data)); + } + }, false); + + source.addEventListener('error', function (e) { + if (e.target.readyState != EventSource.OPEN) { + console.log("Status EventSource Disconnected"); + } + source.close(); + }, false); + + source.addEventListener('status', function (e) { + processStatusData(JSON.parse(e.data)); + }, false); +} + + +// getBcStatus = () => { +// fetch('/api/status', { +// method: 'get' +// }) +// .then(response => response.json()) +// .then() +// .catch(err => { +// //error block +// }); +// } + +// interval = setInterval(getBcStatus, 2500); +// getBcStatus(); fetch('/api/settings', { method: 'get' @@ -77,17 +103,18 @@ fetch('/api/settings', { if (jsonData.useBitcoinNode) document.getElementById('useBitcoinNode').checked = true; - let nodeFields = ["rpcHost", "rpcPort", "rpcUser", "tzOffset"]; + // let nodeFields = ["rpcHost", "rpcPort", "rpcUser", "tzOffset"]; - for (let n of nodeFields) { - document.getElementById(n).value = jsonData[n]; - } + // for (let n of nodeFields) { + // document.getElementById(n).value = jsonData[n]; + // } document.getElementById('timePerScreen').value = jsonData.timerSeconds / 60; document.getElementById('ledBrightness').value = jsonData.ledBrightness; document.getElementById('fullRefreshMin').value = jsonData.fullRefreshMin; - document.getElementById('wpTimeout').value = jsonData.wpTimeout; + document.getElementById('tzOffset').value = jsonData.tzOffset; document.getElementById('mempoolInstance').value = jsonData.mempoolInstance; + document.getElementById('minSecPriceUpd').value = jsonData.minSecPriceUpd; if (jsonData.gitRev) document.getElementById('gitRev').innerHTML = "Version: " + jsonData.gitRev; diff --git a/platformio.ini b/platformio.ini index d946a88..e838636 100644 --- a/platformio.ini +++ b/platformio.ini @@ -24,12 +24,11 @@ board_build.partitions = partition.csv build_flags = !python scripts/git_rev.py -DLAST_BUILD_TIME=$UNIX_TIME - -DASYNCWEBSERVER_REGEX - -D ARDUINO_USB_CDC_ON_BOOT + -DARDUINO_USB_CDC_ON_BOOT -fexceptions build_unflags = - -fno-exceptions -Werror=all + -fno-exceptions lib_deps = bblanchon/ArduinoJson@^6.21.3 esphome/Improv@^1.2.3 @@ -48,4 +47,6 @@ build_flags = -D MCP_INT_PIN=8 -D NEOPIXEL_PIN=34 -D NEOPIXEL_COUNT=4 - -D NUM_SCREENS=7 \ No newline at end of file + -D NUM_SCREENS=7 +build_unflags = + ${btclock_base.build_unflags} diff --git a/src/lib/block_notify.cpp b/src/lib/block_notify.cpp index 6378a50..7df45bf 100644 --- a/src/lib/block_notify.cpp +++ b/src/lib/block_notify.cpp @@ -32,7 +32,7 @@ void setupBlockNotify() { String blockHeightStr = http->getString(); currentBlockHeight = blockHeightStr.toInt(); - xTaskNotifyGive(blockUpdateTaskHandle); + // xTaskNotifyGive(blockUpdateTaskHandle); } // std::strcpy(wsServer, String("wss://" + mempoolInstance + "/api/v1/ws").c_str()); @@ -95,7 +95,10 @@ void onWebsocketMessage(esp_websocket_event_data_t *event_data) if (blockUpdateTaskHandle != nullptr) { xTaskNotifyGive(blockUpdateTaskHandle); - queueLedEffect(LED_FLASH_BLOCK_NOTIFY); + if (preferences.getBool("ledFlashOnUpd", false)) { + vTaskDelay(pdMS_TO_TICKS(250)); // Wait until screens are updated + queueLedEffect(LED_FLASH_BLOCK_NOTIFY); + } } } diff --git a/src/lib/config.cpp b/src/lib/config.cpp index 125c9ab..13b2a0a 100644 --- a/src/lib/config.cpp +++ b/src/lib/config.cpp @@ -10,6 +10,7 @@ std::map screenNameMap; void setup() { + setupPreferences(); setupHardware(); if (mcp.digitalRead(3) == LOW) { @@ -20,7 +21,6 @@ void setup() setupDisplays(); tryImprovSetup(); - setupPreferences(); setupWebserver(); // setupWifi(); @@ -112,7 +112,13 @@ void setupTimers() void finishSetup() { - clearLeds(); + + if (preferences.getBool("ledStatus", false)) { + setLights(preferences.getUInt("ledColor", 0xFFCC00)); + } else { + clearLeds(); + } + } std::map getScreenNameMap() { @@ -122,7 +128,7 @@ std::map getScreenNameMap() { void setupHardware() { setupLeds(); - + WiFi.setHostname(getMyHostname().c_str());; if (psramInit()) { Serial.println(F("PSRAM is correctly initialized")); diff --git a/src/lib/config.hpp b/src/lib/config.hpp index 65b2c17..7eeb0e2 100644 --- a/src/lib/config.hpp +++ b/src/lib/config.hpp @@ -9,6 +9,7 @@ #include #include "epd.hpp" #include "improv.hpp" +#include #include "lib/screen_handler.hpp" #include "lib/webserver.hpp" @@ -22,6 +23,7 @@ #define TIME_OFFSET_SECONDS 3600 #define USER_AGENT "BTClock/2.0" #define MCP_DEV_ADDR 0x20 +#define DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE 30 #define DEFAULT_FG_COLOR GxEPD_WHITE #define DEFAULT_BG_COLOR GxEPD_BLACK diff --git a/src/lib/epd.cpp b/src/lib/epd.cpp index 162799e..d42639f 100644 --- a/src/lib/epd.cpp +++ b/src/lib/epd.cpp @@ -148,7 +148,16 @@ extern "C" void updateDisplay(void *pvParameters) noexcept if (epdContent[epdIndex].compareTo(currentEpdContent[epdIndex]) != 0) { + displays[epdIndex].init(0, false); // Little longer reset duration because of MCP + uint count = 0; + while (EPD_BUSY[epdIndex].digitalRead() == HIGH || count < 10) { + vTaskDelay(pdMS_TO_TICKS(100)); + if (count >= 9) { + displays[epdIndex].init(0, false); + } + count++; + } bool updatePartial = true; diff --git a/src/lib/led_handler.cpp b/src/lib/led_handler.cpp index cb4303e..73eedb7 100644 --- a/src/lib/led_handler.cpp +++ b/src/lib/led_handler.cpp @@ -185,20 +185,30 @@ void blinkDelayTwoColor(int d, int times, uint32_t c1, uint32_t c2) void clearLeds() { + preferences.putBool("ledStatus", false); pixels.clear(); pixels.show(); } void setLights(int r, int g, int b) { + setLights(pixels.Color(r, g, b)); +} + +void setLights(uint32_t color) +{ + preferences.putUInt("ledColor", color); + preferences.putBool("ledStatus", true); + for (int i = 0; i < NEOPIXEL_COUNT; i++) { - pixels.setPixelColor(i, pixels.Color(r, g, b)); + pixels.setPixelColor(i, color); } pixels.show(); } + QueueHandle_t getLedTaskQueue() { return ledTaskQueue; diff --git a/src/lib/led_handler.hpp b/src/lib/led_handler.hpp index 2e57d63..762560c 100644 --- a/src/lib/led_handler.hpp +++ b/src/lib/led_handler.hpp @@ -35,4 +35,5 @@ void blinkDelayTwoColor(int d, int times, uint32_t c1, uint32_t c2); void clearLeds(); QueueHandle_t getLedTaskQueue(); bool queueLedEffect(uint effect); -void setLights(int r, int g, int b); \ No newline at end of file +void setLights(int r, int g, int b); +void setLights(uint32_t color); \ No newline at end of file diff --git a/src/lib/price_notify.cpp b/src/lib/price_notify.cpp index 40c906c..3e2b855 100644 --- a/src/lib/price_notify.cpp +++ b/src/lib/price_notify.cpp @@ -4,6 +4,7 @@ const char *wsServerPrice = "wss://ws.coincap.io/prices?assets=bitcoin"; // WebsocketsClient client; esp_websocket_client_handle_t clientPrice = NULL; unsigned long int currentPrice; +unsigned long int lastPriceUpdate = 0; void setupPriceNotify() { @@ -47,13 +48,19 @@ void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data) if (doc.containsKey("bitcoin")) { if (currentPrice != doc["bitcoin"].as()) { - const unsigned long oldPrice = currentPrice; - currentPrice = doc["bitcoin"].as(); + uint minSecPriceUpd = preferences.getUInt("minSecPriceUpd", DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE); + uint currentTime = esp_timer_get_time() / 1000000; - // if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) { - if (priceUpdateTaskHandle != nullptr && (getCurrentScreen() == SCREEN_BTC_TICKER || getCurrentScreen() == SCREEN_MSCW_TIME)) - xTaskNotifyGive(priceUpdateTaskHandle); - //} + if (lastPriceUpdate == 0 || (currentTime - lastPriceUpdate) > minSecPriceUpd) { + // const unsigned long oldPrice = currentPrice; + currentPrice = doc["bitcoin"].as(); + + lastPriceUpdate = currentTime; + // if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) { + if (priceUpdateTaskHandle != nullptr && (getCurrentScreen() == SCREEN_BTC_TICKER || getCurrentScreen() == SCREEN_MSCW_TIME)) + xTaskNotifyGive(priceUpdateTaskHandle); + //} + } } } } diff --git a/src/lib/screen_handler.cpp b/src/lib/screen_handler.cpp index bec727c..d891ecd 100644 --- a/src/lib/screen_handler.cpp +++ b/src/lib/screen_handler.cpp @@ -180,6 +180,8 @@ void setupTasks() xTaskCreate(taskBlockUpdate, "updateBlock", 2048, NULL, tskIDLE_PRIORITY, &blockUpdateTaskHandle); xTaskCreate(taskTimeUpdate, "updateTime", 4096, NULL, tskIDLE_PRIORITY, &timeUpdateTaskHandle); xTaskCreate(taskScreenRotate, "rotateScreen", 2048, NULL, tskIDLE_PRIORITY, &taskScreenRotateTaskHandle); + + setCurrentScreen(preferences.getUInt("currentScreen", 0)); } void setupTimeUpdateTimer(void *pvParameters) @@ -212,7 +214,10 @@ void setupScreenRotateTimer(void *pvParameters) .name = "screen_rotate_timer"}; esp_timer_create(&screenRotateTimerConfig, &screenRotateTimer); - esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond); + + if (preferences.getBool("timerActive", true)) { + esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond); + } vTaskDelete(NULL); } @@ -233,11 +238,13 @@ void setTimerActive(bool status) { esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond); queueLedEffect(LED_EFFECT_START_TIMER); + preferences.putBool("timerActive", true); } else { esp_timer_stop(screenRotateTimer); queueLedEffect(LED_EFFECT_PAUSE_TIMER); + preferences.putBool("timerActive", false); } } diff --git a/src/lib/utils.cpp b/src/lib/utils.cpp index 8e548f6..4c0944b 100644 --- a/src/lib/utils.cpp +++ b/src/lib/utils.cpp @@ -4,3 +4,9 @@ int modulo(int x, int N) { return (x % N + N) % N; } + +String getMyHostname() { + byte mac[6]; + WiFi.macAddress(mac); + return "btclock" + String(mac[4], 16) = String(mac[5], 16); +} \ No newline at end of file diff --git a/src/lib/utils.hpp b/src/lib/utils.hpp index 9dad3aa..2bdd69d 100644 --- a/src/lib/utils.hpp +++ b/src/lib/utils.hpp @@ -1 +1,7 @@ -int modulo(int x,int N); \ No newline at end of file +#pragma once + +#include +#include "shared.hpp" + +int modulo(int x,int N); +String getMyHostname(); \ No newline at end of file diff --git a/src/lib/webserver.cpp b/src/lib/webserver.cpp index f32fefc..057a59b 100644 --- a/src/lib/webserver.cpp +++ b/src/lib/webserver.cpp @@ -1,6 +1,7 @@ #include "webserver.hpp" AsyncWebServer server(80); +AsyncEventSource events("/events"); void setupWebserver() { @@ -10,6 +11,18 @@ void setupWebserver() return; } + events.onConnect([](AsyncEventSourceClient *client) + { + if (client->lastId()) + { + Serial.printf("Client reconnected! Last message ID that it gat is: %u\n", client->lastId()); + } + // send event with message "hello!", id current millis + // and set reconnect delay to 1 second + eventSourceLoop(); + }); + server.addHandler(&events); + server.serveStatic("/css", LittleFS, "/css/"); server.serveStatic("/js", LittleFS, "/js/"); server.serveStatic("/font", LittleFS, "/font/"); @@ -30,31 +43,38 @@ void setupWebserver() server.on("/api/lights/off", HTTP_GET, onApiLightsOff); server.on("/api/lights/color", HTTP_GET, onApiLightsSetColor); - server.on("^\\/api\\/lights\\/([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", HTTP_GET, onApiLightsSetColor); + + // server.on("^\\/api\\/lights\\/([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", HTTP_GET, onApiLightsSetColor); server.on("/api/restart", HTTP_GET, onApiRestart); - + server.addRewrite(new OneParamRewrite("/api/lights/{color}", "/api/lights/color?c={color}")); server.addRewrite(new OneParamRewrite("/api/show/screen/{s}", "/api/show/screen?s={s}")); server.addRewrite(new OneParamRewrite("/api/show/text/{text}", "/api/show/text?t={text}")); server.addRewrite(new OneParamRewrite("/api/show/number/{number}", "/api/show/text?t={text}")); server.onNotFound(onNotFound); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); + server.begin(); + if (!MDNS.begin(getMyHostname())) + { + Serial.println(F("Error setting up MDNS responder!")); + while (1) + { + delay(1000); + } + } + MDNS.addService("http", "tcp", 80); } -/** - * @Api - * @Path("/api/status") - */ -void onApiStatus(AsyncWebServerRequest *request) +StaticJsonDocument<768> getStatusObject() { - AsyncResponseStream *response = request->beginResponseStream("application/json"); - StaticJsonDocument<512> root; + StaticJsonDocument<768> root; root["currentScreen"] = getCurrentScreen(); root["numScreens"] = NUM_SCREENS; - root["timerRunning"] = isTimerActive();; + root["timerRunning"] = isTimerActive(); root["espUptime"] = esp_timer_get_time() / 1000000; root["currentPrice"] = getPrice(); root["currentBlockHeight"] = getBlockHeight(); @@ -67,6 +87,37 @@ void onApiStatus(AsyncWebServerRequest *request) conStatus["price"] = isPriceNotifyConnected(); conStatus["blocks"] = isBlockNotifyConnected(); + return root; +} + +void eventSourceLoop() +{ + if (!events.count()) return; + StaticJsonDocument<768> root = getStatusObject(); + JsonArray data = root.createNestedArray("data"); + String epdContent[NUM_SCREENS]; + std::array retEpdContent = getCurrentEpdContent(); + std::copy(std::begin(retEpdContent), std::end(retEpdContent), epdContent); + + copyArray(epdContent, data); + + size_t bufSize = measureJson(root); + char buffer[bufSize]; + String bufString; + serializeJson(root, bufString); + + events.send(bufString.c_str(), "status"); +} + +/** + * @Api + * @Path("/api/status") + */ +void onApiStatus(AsyncWebServerRequest *request) +{ + AsyncResponseStream *response = request->beginResponseStream("application/json"); + + StaticJsonDocument<768> root = getStatusObject(); JsonArray data = root.createNestedArray("data"); JsonArray rendered = root.createNestedArray("rendered"); String epdContent[NUM_SCREENS]; @@ -107,7 +158,6 @@ void onApiActionTimerRestart(AsyncWebServerRequest *request) request->send(200); } - void onApiShowScreen(AsyncWebServerRequest *request) { if (request->hasParam("s")) @@ -128,13 +178,14 @@ void onApiShowText(AsyncWebServerRequest *request) t.toUpperCase(); // This is needed as long as lowercase letters are glitchy std::array textEpdContent; - for (uint i = 0; i < NUM_SCREENS; i++) { + for (uint i = 0; i < NUM_SCREENS; i++) + { textEpdContent[i] = t[i]; } setEpdContent(textEpdContent); } - //setCurrentScreen(SCREEN_CUSTOM); + // setCurrentScreen(SCREEN_CUSTOM); request->send(200); } @@ -156,7 +207,8 @@ void onApiSettingsGet(AsyncWebServerRequest *request) root["fgColor"] = getFgColor(); root["bgColor"] = getBgColor(); root["timerSeconds"] = getTimerSeconds(); - root["timerRunning"] = isTimerActive();; + root["timerRunning"] = isTimerActive(); + root["minSecPriceUpd"] = preferences.getUInt("minSecPriceUpd", DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE); root["fullRefreshMin"] = preferences.getUInt("fullRefreshMin", 30); root["wpTimeout"] = preferences.getUInt("wpTimeout", 600); root["tzOffset"] = preferences.getInt("gmtOffset", TIME_OFFSET_SECONDS) / 60; @@ -166,7 +218,6 @@ void onApiSettingsGet(AsyncWebServerRequest *request) root["rpcHost"] = preferences.getString("rpcHost", BITCOIND_HOST); root["mempoolInstance"] = preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE); - root["epdColors"] = 2; root["ledFlashOnUpdate"] = preferences.getBool("ledFlashOnUpd", false); root["ledBrightness"] = preferences.getUInt("ledBrightness", 128); @@ -312,6 +363,16 @@ void onApiSettingsPost(AsyncWebServerRequest *request) settingsChanged = true; } + if (request->hasParam("minSecPriceUpd", true)) + { + AsyncWebParameter *p = request->getParam("minSecPriceUpd", true); + int minSecPriceUpd = p->value().toInt() * 60; + preferences.putInt("minSecPriceUpd", minSecPriceUpd); + Serial.print("Setting minSecPriceUpd "); + Serial.println(minSecPriceUpd); + settingsChanged = true; + } + if (request->hasParam("timePerScreen", true)) { AsyncWebParameter *p = request->getParam("timePerScreen", true); @@ -381,14 +442,16 @@ void onApiLightsOff(AsyncWebServerRequest *request) void onApiLightsSetColor(AsyncWebServerRequest *request) { - String rgbColor = request->pathArg(0); - uint r, g, b; - sscanf(rgbColor.c_str(), "%02x%02x%02x", &r, &g, &b); - setLights(r, g, b); - request->send(200, "text/plain", rgbColor); + if (request->hasParam("c")) + { + String rgbColor = request->getParam("c")->value(); + uint r, g, b; + sscanf(rgbColor.c_str(), "%02x%02x%02x", &r, &g, &b); + setLights(r, g, b); + request->send(200, "text/plain", rgbColor); + } } - void onIndex(AsyncWebServerRequest *request) { request->send(LittleFS, "/index.html", String(), false); } void onNotFound(AsyncWebServerRequest *request) diff --git a/src/lib/webserver.hpp b/src/lib/webserver.hpp index 61ee040..fb470dd 100644 --- a/src/lib/webserver.hpp +++ b/src/lib/webserver.hpp @@ -3,6 +3,7 @@ #include "ESPAsyncWebServer.h" #include #include +#include #include "lib/block_notify.hpp" #include "lib/price_notify.hpp" @@ -32,4 +33,7 @@ void onApiLightsSetColor(AsyncWebServerRequest *request); void onApiRestart(AsyncWebServerRequest *request); void onIndex(AsyncWebServerRequest *request); -void onNotFound(AsyncWebServerRequest *request); \ No newline at end of file +void onNotFound(AsyncWebServerRequest *request); + +StaticJsonDocument<768> getStatusObject(); +void eventSourceLoop(); \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index e333e8e..b807039 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ extern "C" void app_main() while (true) { - vTaskDelay(pdMS_TO_TICKS(5000)); + eventSourceLoop(); + vTaskDelay(pdMS_TO_TICKS(2500)); } } \ No newline at end of file