Added EventSource for webUI updates, completed more features

This commit is contained in:
Djuri Baars 2023-11-08 15:27:22 +01:00
parent 91fd921e2e
commit 280764a2fa
16 changed files with 241 additions and 78 deletions

View File

@ -172,8 +172,8 @@
</div>
<div class="row">
<label for="tzOffset" class="col-sm-6 col-form-label">Timezone offset</label>
<div class="col-sm-6">
<div class="input-group mb-3">
<div class="col-sm-6 mb-3">
<div class="input-group">
<input type="number" name="tzOffset" id="tzOffset" class="form-control">
<span class="input-group-text">min</span>
<button class="btn btn-outline-secondary" type="button" id="getTzOffsetBtn">Auto</button>
@ -181,11 +181,21 @@
<div class="form-text">A restart is required to apply TZ offset.</div>
</div>
</div>
<div class="row">
<label for="minSecPriceUpd" class="col-sm-6 col-form-label">Time between price updates</label>
<div class="col-sm-6 mb-3">
<div class="input-group">
<input type="number" name="minSecPriceUpd" id="minSecPriceUpd" class="form-control">
<span class="input-group-text">sec</span>
</div>
<div class="form-text">Short amounts might shorten lifespan.</div>
</div>
</div>
<div class="row">
<div class=" col-sm-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="ledFlashOnUpdate" name="ledFlashOnUpd" value="1">
<label class="form-check-label" for="ledFlashOnUpdate">LED flash on update</label>
<label class="form-check-label" for="ledFlashOnUpdate">LED flash on new block</label>
</div>
</div>
</div>

View File

@ -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;

View File

@ -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
-D NUM_SCREENS=7
build_unflags =
${btclock_base.build_unflags}

View File

@ -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);
}
}
}

View File

@ -10,6 +10,7 @@ std::map<int, std::string> 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<int, std::string> getScreenNameMap() {
@ -122,7 +128,7 @@ std::map<int, std::string> getScreenNameMap() {
void setupHardware()
{
setupLeds();
WiFi.setHostname(getMyHostname().c_str());;
if (psramInit())
{
Serial.println(F("PSRAM is correctly initialized"));

View File

@ -9,6 +9,7 @@
#include <esp_sntp.h>
#include "epd.hpp"
#include "improv.hpp"
#include <map>
#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

View File

@ -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;

View File

@ -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;

View File

@ -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);
void setLights(int r, int g, int b);
void setLights(uint32_t color);

View File

@ -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<long>()) {
const unsigned long oldPrice = currentPrice;
currentPrice = doc["bitcoin"].as<long>();
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<long>();
lastPriceUpdate = currentTime;
// if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) {
if (priceUpdateTaskHandle != nullptr && (getCurrentScreen() == SCREEN_BTC_TICKER || getCurrentScreen() == SCREEN_MSCW_TIME))
xTaskNotifyGive(priceUpdateTaskHandle);
//}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -1 +1,7 @@
int modulo(int x,int N);
#pragma once
#include <WiFi.h>
#include "shared.hpp"
int modulo(int x,int N);
String getMyHostname();

View File

@ -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<String, NUM_SCREENS> 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<String, NUM_SCREENS> 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)

View File

@ -3,6 +3,7 @@
#include "ESPAsyncWebServer.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <ESPmDNS.h>
#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);
void onNotFound(AsyncWebServerRequest *request);
StaticJsonDocument<768> getStatusObject();
void eventSourceLoop();

View File

@ -10,6 +10,7 @@ extern "C" void app_main()
while (true)
{
vTaskDelay(pdMS_TO_TICKS(5000));
eventSourceLoop();
vTaskDelay(pdMS_TO_TICKS(2500));
}
}