Improved QR-code setup, added MCAP screen

This commit is contained in:
Djuri 2023-11-10 19:52:06 +01:00
parent f611d2f5f8
commit 705f27fda9
25 changed files with 1197 additions and 88 deletions

View file

@ -2,10 +2,12 @@
char *wsServer;
esp_websocket_client_handle_t blockNotifyClient = NULL;
unsigned long int currentBlockHeight;
unsigned long int currentBlockHeight = 816000;
void setupBlockNotify()
{
currentBlockHeight = preferences.getULong("blockHeight", 816000);
IPAddress result;
int dnsErr = -1;
@ -87,7 +89,8 @@ void onWebsocketMessage(esp_websocket_event_data_t *event_data)
currentBlockHeight = block["height"].as<long>();
Serial.printf("New block found: %d\r\n", block["height"].as<long>());
Serial.printf("New block found: %d\r\n", block["height"].as<long>());
preferences.putULong("blockHeight", currentBlockHeight);
if (blockUpdateTaskHandle != nullptr) {
xTaskNotifyGive(blockUpdateTaskHandle);

View file

@ -1,8 +1,6 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "shared.hpp"
#include "screen_handler.hpp"

View file

@ -10,15 +10,19 @@ void setup()
{
setupPreferences();
setupHardware();
setupDisplays();
if (preferences.getBool("ledTestOnPower", true))
{
queueLedEffect(LED_POWER_TEST);
}
if (mcp.digitalRead(3) == LOW)
{
preferences.putBool("wifiConfigured", false);
WiFi.eraseAP();
blinkDelay(100, 3);
queueLedEffect(LED_EFFECT_WIFI_ERASE_SETTINGS);
}
setupDisplays();
tryImprovSetup();
setupWebserver();
@ -33,19 +37,23 @@ void setup()
xTaskCreate(setupWebsocketClients, "setupWebsocketClients", 4096, NULL, tskIDLE_PRIORITY, NULL);
setupButtonTask();
Serial.printf("Number of free Preferences entries %d", preferences.freeEntries());
}
void tryImprovSetup()
{
if (!preferences.getBool("wifiConfigured", false))
{
setFgColor(GxEPD_BLACK);
setBgColor(GxEPD_WHITE);
queueLedEffect(LED_EFFECT_WIFI_WAIT_FOR_CONFIG);
uint8_t x_buffer[16];
uint8_t x_position = 0;
// Hold second button to start QR code wifi config
if (!mcp.digitalRead(2) == LOW)
if (mcp.digitalRead(2) == LOW)
{
WiFiManager wm;
@ -65,25 +73,32 @@ void tryImprovSetup()
wifiManager->getConfigPortalSSID().c_str(),
softAP_password.c_str());
// vTaskDelay(pdMS_TO_TICKS(1000));
delay(1000);
delay(6000);
const String qrText = "qrWIFI:S:" + wifiManager->getConfigPortalSSID() + ";T:WPA;P:" + softAP_password.c_str() + ";;";
const String explainText = "*SSID: *\r\n" + wifiManager->getConfigPortalSSID() + "\r\n\r\n*Password:*\r\n" + softAP_password;
std::array<String, NUM_SCREENS> epdContent = {"Welcome!", "", "To setup\r\nscan QR or\r\nconnect\r\nmanually", "", explainText, "", qrText};
setEpdContent(epdContent);
delay(3000);
Serial.println("xTask");
xTaskNotifyGive(epdTaskHandle); });
});
wm.setSaveConfigCallback([]() {
wm.setSaveConfigCallback([]()
{
preferences.putBool("wifiConfigured", true);
});
delay(1000);
// just restart after succes
ESP.restart(); });
bool ac = wm.autoConnect(softAP_SSID.c_str(), softAP_password.c_str());
wm.server->stop();
}
else
{
waitUntilNoneBusy();
std::array<String, NUM_SCREENS> epdContent = {"Welcome!", "", "Use\r\nweb-interface\r\nto configure", "", "Or restart\r\nwhile\r\nholding\r\n2nd button\r\r\nto start\r\n QR-config", "", ""};
setEpdContent(epdContent);
esp_task_wdt_init(30, false);
while (WiFi.status() != WL_CONNECTED)
{
if (Serial.available() > 0)
@ -99,9 +114,12 @@ void tryImprovSetup()
x_position = 0;
}
}
esp_task_wdt_reset();
}
esp_task_wdt_deinit();
esp_task_wdt_reset();
}
setFgColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
setBgColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
}
else
{
@ -112,7 +130,7 @@ void tryImprovSetup()
vTaskDelay(pdMS_TO_TICKS(400));
}
}
queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);
// queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);
}
void setupTime()
@ -134,6 +152,7 @@ void setupPreferences()
{
preferences.begin("btclock", false);
setFgColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
setBgColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
@ -142,6 +161,7 @@ void setupPreferences()
screenNameMap[SCREEN_BTC_TICKER] = "Ticker";
screenNameMap[SCREEN_TIME] = "Time";
screenNameMap[SCREEN_HALVING_COUNTDOWN] = "Halving countdown";
screenNameMap[SCREEN_MARKET_CAP] = "Market Cap";
}
void setupWebsocketClients(void *pvParameters)

View file

@ -6,6 +6,7 @@
#include <Preferences.h>
#include <Adafruit_MCP23X17.h>
#include <Arduino.h>
#include "shared.hpp"
#include "epd.hpp"
#include "improv.hpp"

View file

@ -80,7 +80,7 @@ void setupDisplays()
int *taskParam = new int;
*taskParam = i;
xTaskCreate(updateDisplay, ("EpdUpd" + String(i)).c_str(), 10000, taskParam, tskIDLE_PRIORITY, &tasks[i]); // create task
xTaskCreate(updateDisplay, ("EpdUpd" + String(i)).c_str(), 4096, taskParam, tskIDLE_PRIORITY, &tasks[i]); // create task
}
xTaskCreate(taskEpd, "epd_task", 2048, NULL, tskIDLE_PRIORITY, &epdTaskHandle);
@ -202,7 +202,11 @@ extern "C" void updateDisplay(void *pvParameters) noexcept
#ifdef PAGED_WRITE
showDigitPaged(epdIndex, epdContent[epdIndex].c_str()[0], updatePartial, &FONT_BIG);
#else
if (epdContent[epdIndex].length() > 1) {
showChars(epdIndex, epdContent[epdIndex], updatePartial, &Antonio_SemiBold30pt7b);
} else {
showDigit(epdIndex, epdContent[epdIndex].c_str()[0], updatePartial, &FONT_BIG);
}
#endif
}
@ -335,6 +339,21 @@ void showDigit(const uint dispNum, char chr, bool partial, const GFXfont *font)
displays[dispNum].print(str);
}
void showChars(const uint dispNum, const String& chars, bool partial, const GFXfont *font) {
displays[dispNum].setRotation(2);
displays[dispNum].setFont(font);
displays[dispNum].setTextColor(getFgColor());
int16_t tbx, tby;
uint16_t tbw, tbh;
displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh);
// center the bounding box by transposition of the origin:
uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx;
uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby;
displays[dispNum].fillScreen(getBgColor());
displays[dispNum].setCursor(x, y);
displays[dispNum].print(chars);
}
void showDigitPaged(const uint dispNum, char chr, bool partial, const GFXfont *font)
{
String str(chr);
@ -426,11 +445,13 @@ void renderText(const uint dispNum, const String &text, bool partial)
}
}
displays[dispNum].display(partial);
//displays[dispNum].display(partial);
}
void renderQr(const uint dispNum, const String &text, bool partial)
{
#ifdef USE_QR
uint8_t tempBuffer[800];
bool ok = qrcodegen_encodeText(text.substring(2).c_str(), tempBuffer, qrcode, qrcodegen_Ecc_LOW,
qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
@ -452,5 +473,17 @@ void renderQr(const uint dispNum, const String &text, bool partial)
displays[dispNum].drawPixel(padding + x, paddingY + y, qrcodegen_getModule(qrcode, floor(float(x) / 4), floor(float(y) / 4)) ? GxEPD_BLACK : GxEPD_WHITE);
}
}
displays[dispNum].display(partial);
//displays[dispNum].display(partial);
//free(tempBuffer);
//free(qrcode);
#endif
}
void waitUntilNoneBusy() {
for (int i = 0; i < NUM_SCREENS; i++) {
while (EPD_BUSY[i].digitalRead()) {
vTaskDelay(10);
}
}
}

View file

@ -10,7 +10,10 @@
#include <Fonts/FreeSans9pt7b.h>
#include <regex>
#ifdef USE_QR
#include "qrcodegen.h"
#endif
extern TaskHandle_t epdTaskHandle;
void setupDisplays();
void taskEpd(void *pvParameters);
@ -19,6 +22,8 @@ void splitText(const uint dispNum, const String& top, const String& bottom, bo
void splitTextPaged(const uint dispNum, String top, String bottom, bool partial);
void showDigit(const uint dispNum, char chr, bool partial, const GFXfont *font);
void showChars(const uint dispNum, const String& chars, bool partial, const GFXfont *font);
void showDigitPaged(const uint dispNum, char chr, bool partial, const GFXfont *font);
extern "C" void updateDisplay(void *pvParameters) noexcept;
@ -34,3 +39,4 @@ void renderQr(const uint dispNum, const String& text, bool partial);
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent);
std::array<String, NUM_SCREENS> getCurrentEpdContent();
void waitUntilNoneBusy();

View file

@ -3,7 +3,7 @@
TaskHandle_t ledTaskHandle = NULL;
QueueHandle_t ledTaskQueue = NULL;
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
unsigned long ledTaskParams;
uint ledTaskParams;
void ledTask(void *parameter)
{
@ -23,6 +23,14 @@ void ledTask(void *parameter)
switch (ledTaskParams)
{
case LED_POWER_TEST:
pixels.setPixelColor(0, pixels.Color(255, 0, 0));
pixels.setPixelColor(1, pixels.Color(0, 255, 0));
pixels.setPixelColor(2, pixels.Color(0, 0, 255));
pixels.setPixelColor(3, pixels.Color(255, 255, 255));
pixels.show();
vTaskDelay(pdMS_TO_TICKS(1000));
break;
case LED_EFFECT_WIFI_CONNECT_ERROR:
case LED_FLASH_ERROR:
blinkDelayColor(250, 3, 255, 0, 0);
@ -39,6 +47,9 @@ void ledTask(void *parameter)
case LED_EFFECT_WIFI_WAIT_FOR_CONFIG:
blinkDelayTwoColor(100, 1, pixels.Color(8, 161, 236), pixels.Color(156, 225, 240));
break;
case LED_EFFECT_WIFI_ERASE_SETTINGS:
blinkDelay(100, 3);
break;
case LED_EFFECT_WIFI_CONNECTING:
for (int i = NEOPIXEL_COUNT; i >= 0; i--)
{
@ -109,13 +120,16 @@ void ledTask(void *parameter)
break;
}
// revert to previous state
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, oldLights[i]);
}
// revert to previous state unless power test
pixels.show();
if (ledTaskParams != LED_POWER_TEST) {
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, oldLights[i]);
}
pixels.show();
}
}
}
}
@ -124,25 +138,24 @@ void ledTask(void *parameter)
void setupLeds()
{
pixels.begin();
if (preferences.getBool("ledTestOnPower", true))
{
pixels.setBrightness(preferences.getUInt("ledBrightness", 128));
pixels.setPixelColor(0, pixels.Color(255, 0, 0));
pixels.setPixelColor(1, pixels.Color(0, 255, 0));
pixels.setPixelColor(2, pixels.Color(0, 0, 255));
pixels.setPixelColor(3, pixels.Color(255, 255, 255));
}
else
{
pixels.clear();
}
pixels.setBrightness(preferences.getUInt("ledBrightness", 128));
pixels.clear();
pixels.show();
setupLedTask();
if (preferences.getBool("ledTestOnPower", true))
{
while (!ledTaskQueue)
{
delay(1);
// wait until queue is available
}
queueLedEffect(LED_POWER_TEST);
}
}
void setupLedTask()
{
ledTaskQueue = xQueueCreate(2, sizeof(char));
ledTaskQueue = xQueueCreate(5, sizeof(uint));
xTaskCreate(ledTask, "LedTask", 2048, NULL, tskIDLE_PRIORITY, &ledTaskHandle);
}
@ -249,6 +262,6 @@ bool queueLedEffect(uint effect)
return false;
}
char flashType = effect;
uint flashType = effect;
xQueueSend(ledTaskQueue, &flashType, portMAX_DELAY);
}

View file

@ -1,8 +1,6 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <Adafruit_NeoPixel.h>
#include "shared.hpp"
@ -23,7 +21,8 @@ const int LED_EFFECT_WIFI_WAIT_FOR_CONFIG = 100;
const int LED_EFFECT_WIFI_CONNECTING = 101;
const int LED_EFFECT_WIFI_CONNECT_ERROR = 102;
const int LED_EFFECT_WIFI_CONNECT_SUCCESS = 103;
const int LED_EFFECT_WIFI_ERASE_SETTINGS = 104;
const int LED_POWER_TEST = 999;
extern TaskHandle_t ledTaskHandle;
void ledTask(void *pvParameters);

View file

@ -3,8 +3,8 @@
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;
unsigned long int currentPrice = 30000;
unsigned long int lastPriceUpdate;
void setupPriceNotify()
{
@ -57,7 +57,7 @@ void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data)
lastPriceUpdate = currentTime;
// if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) {
if (priceUpdateTaskHandle != nullptr && (getCurrentScreen() == SCREEN_BTC_TICKER || getCurrentScreen() == SCREEN_MSCW_TIME))
if (priceUpdateTaskHandle != nullptr && (getCurrentScreen() == SCREEN_BTC_TICKER || getCurrentScreen() == SCREEN_MSCW_TIME || getCurrentScreen() == SCREEN_MARKET_CAP))
xTaskNotifyGive(priceUpdateTaskHandle);
//}
}

View file

@ -34,7 +34,7 @@ void taskPriceUpdate(void *pvParameters)
firstIndex = 1;
}
}
else
else if (getCurrentScreen() == SCREEN_MSCW_TIME)
{
priceString = String(int(round(1 / float(price) * 10e7))).c_str();
@ -45,10 +45,39 @@ void taskPriceUpdate(void *pvParameters)
firstIndex = 1;
}
}
for (uint i = firstIndex; i < NUM_SCREENS; i++)
else
{
taskEpdContent[i] = priceString[i];
double supply = getSupplyAtBlock(getBlockHeight());
int64_t marketCap = static_cast<std::int64_t>(supply * double(price));
std::string stringValue = std::to_string(marketCap);
size_t mcLength = stringValue.length();
size_t leadingSpaces = (3 - mcLength % 3) % 3;
stringValue = std::string(leadingSpaces, ' ') + stringValue;
taskEpdContent[0] = "USD/MCAP";
uint groups = (mcLength + leadingSpaces) / 3;
if (groups < NUM_SCREENS) {
firstIndex = 1;
}
for (int i = firstIndex; i < NUM_SCREENS-groups-1; i++) {
taskEpdContent[i] = "";
}
taskEpdContent[NUM_SCREENS-groups-1] = " $ ";
for (uint i = 0; i < groups; i ++)
{
taskEpdContent[(NUM_SCREENS-groups+i)] = stringValue.substr(i*3, 3).c_str();
}
}
if (getCurrentScreen() != SCREEN_MARKET_CAP) {
for (uint i = firstIndex; i < NUM_SCREENS; i++)
{
taskEpdContent[i] = priceString[i];
}
}
setEpdContent(taskEpdContent);
@ -179,7 +208,7 @@ void IRAM_ATTR screenRotateTimerISR(void *arg)
void setupTasks()
{
xTaskCreate(taskPriceUpdate, "updatePrice", 2048, NULL, tskIDLE_PRIORITY, &priceUpdateTaskHandle);
xTaskCreate(taskPriceUpdate, "updatePrice", 3072, NULL, tskIDLE_PRIORITY, &priceUpdateTaskHandle);
xTaskCreate(taskBlockUpdate, "updateBlock", 2048, NULL, tskIDLE_PRIORITY, &blockUpdateTaskHandle);
xTaskCreate(taskTimeUpdate, "updateTime", 4096, NULL, tskIDLE_PRIORITY, &timeUpdateTaskHandle);
xTaskCreate(taskScreenRotate, "rotateScreen", 2048, NULL, tskIDLE_PRIORITY, &taskScreenRotateTaskHandle);
@ -283,6 +312,7 @@ void setCurrentScreen(uint newScreen)
case SCREEN_BLOCK_HEIGHT:
xTaskNotifyGive(blockUpdateTaskHandle);
break;
case SCREEN_MARKET_CAP:
case SCREEN_MSCW_TIME:
case SCREEN_BTC_TICKER:
xTaskNotifyGive(priceUpdateTaskHandle);
@ -308,6 +338,7 @@ void nextScreen()
void previousScreen()
{
int newCurrentScreen = modulo(getCurrentScreen() - 1, SCREEN_COUNT);
String key = "screen" + String(newCurrentScreen) + "Visible";

View file

@ -5,8 +5,7 @@
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "lib/block_notify.hpp"
#include "lib/price_notify.hpp"
#include "shared.hpp"
#include "lib/epd.hpp"
extern TaskHandle_t priceUpdateTaskHandle;

View file

@ -1,5 +1,7 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <Adafruit_MCP23X17.h>
#include <ArduinoJson.h>
#include <Preferences.h>
@ -13,10 +15,12 @@ const PROGMEM int SCREEN_MSCW_TIME = 1;
const PROGMEM int SCREEN_BTC_TICKER = 2;
const PROGMEM int SCREEN_TIME = 3;
const PROGMEM int SCREEN_HALVING_COUNTDOWN = 4;
const PROGMEM int SCREEN_MARKET_CAP = 5;
const PROGMEM int SCREEN_COUNTDOWN = 98;
const PROGMEM int SCREEN_CUSTOM = 99;
const PROGMEM int screens[5] = { SCREEN_BLOCK_HEIGHT, SCREEN_MSCW_TIME, SCREEN_BTC_TICKER, SCREEN_TIME, SCREEN_HALVING_COUNTDOWN };
const int SCREEN_COUNT = 5;
const int SCREEN_COUNT = 6;
const PROGMEM int screens[SCREEN_COUNT] = { SCREEN_BLOCK_HEIGHT, SCREEN_MSCW_TIME, SCREEN_BTC_TICKER, SCREEN_TIME, SCREEN_HALVING_COUNTDOWN, SCREEN_MARKET_CAP };
struct SpiRamAllocator {
void* allocate(size_t size) {

View file

@ -9,4 +9,24 @@ String getMyHostname() {
byte mac[6];
WiFi.macAddress(mac);
return "btclock" + String(mac[4], 16) = String(mac[5], 16);
}
double getSupplyAtBlock(uint blockNr) {
if (blockNr >= 33 * 210000) {
return 20999999.9769;
}
const int initialBlockReward = 50; // Initial block reward
const int halvingInterval = 210000; // Number of blocks before halving
int halvingCount = blockNr / halvingInterval;
double totalBitcoinInCirculation = 0;
for (int i = 0; i < halvingCount; ++i) {
totalBitcoinInCirculation += halvingInterval * initialBlockReward * std::pow(0.5, i);
}
totalBitcoinInCirculation += (blockNr % halvingInterval) * initialBlockReward * std::pow(0.5, halvingCount);
return totalBitcoinInCirculation;
}

View file

@ -4,4 +4,7 @@
#include "shared.hpp"
int modulo(int x,int N);
double getSupplyAtBlock(uint blockNr);
String getMyHostname();

View file

@ -186,7 +186,7 @@ void onApiShowText(AsyncWebServerRequest *request)
setEpdContent(textEpdContent);
}
// setCurrentScreen(SCREEN_CUSTOM);
setCurrentScreen(SCREEN_CUSTOM);
request->send(200);
}
@ -203,7 +203,7 @@ void onApiRestart(AsyncWebServerRequest *request)
*/
void onApiSettingsGet(AsyncWebServerRequest *request)
{
StaticJsonDocument<768> root;
StaticJsonDocument<1536> root;
root["numScreens"] = NUM_SCREENS;
root["fgColor"] = getFgColor();
root["bgColor"] = getBgColor();
@ -214,11 +214,11 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
root["wpTimeout"] = preferences.getUInt("wpTimeout", 600);
root["tzOffset"] = preferences.getInt("gmtOffset", TIME_OFFSET_SECONDS) / 60;
root["useBitcoinNode"] = preferences.getBool("useNode", false);
root["rpcPort"] = preferences.getUInt("rpcPort", BITCOIND_PORT);
root["rpcUser"] = preferences.getString("rpcUser", BITCOIND_RPC_USER);
root["rpcHost"] = preferences.getString("rpcHost", BITCOIND_HOST);
// root["rpcPort"] = preferences.getUInt("rpcPort", BITCOIND_PORT);
// root["rpcUser"] = preferences.getString("rpcUser", BITCOIND_RPC_USER);
// root["rpcHost"] = preferences.getString("rpcHost", BITCOIND_HOST);
root["mempoolInstance"] = preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
root["ledTestOnPower"] = preferences.getBool("ledTestOnPower", true);
root["ledFlashOnUpdate"] = preferences.getBool("ledFlashOnUpd", false);
root["ledBrightness"] = preferences.getUInt("ledBrightness", 128);
@ -374,33 +374,33 @@ void onApiSettingsPost(AsyncWebServerRequest *request)
settingsChanged = true;
}
if (request->hasParam("useBitcoinNode", true))
{
AsyncWebParameter *p = request->getParam("useBitcoinNode", true);
bool useBitcoinNode = p->value().toInt();
preferences.putBool("useNode", useBitcoinNode);
settingsChanged = true;
// if (request->hasParam("useBitcoinNode", true))
// {
// AsyncWebParameter *p = request->getParam("useBitcoinNode", true);
// bool useBitcoinNode = p->value().toInt();
// preferences.putBool("useNode", useBitcoinNode);
// settingsChanged = true;
String rpcVars[] = {"rpcHost", "rpcPort", "rpcUser", "rpcPass"};
// String rpcVars[] = {"rpcHost", "rpcPort", "rpcUser", "rpcPass"};
for (String v : rpcVars)
{
if (request->hasParam(v, true))
{
AsyncWebParameter *pv = request->getParam(v, true);
// Don't store an empty password, probably new settings save
if (!(v.equals("rpcPass") && pv->value().length() == 0))
{
preferences.putString(v.c_str(), pv->value().c_str());
}
}
}
}
else
{
preferences.putBool("useNode", false);
settingsChanged = true;
}
// for (String v : rpcVars)
// {
// if (request->hasParam(v, true))
// {
// AsyncWebParameter *pv = request->getParam(v, true);
// // Don't store an empty password, probably new settings save
// if (!(v.equals("rpcPass") && pv->value().length() == 0))
// {
// preferences.putString(v.c_str(), pv->value().c_str());
// }
// }
// }
// }
// else
// {
// preferences.putBool("useNode", false);
// settingsChanged = true;
// }
request->send(200);
if (settingsChanged)