diff --git a/scripts/git_rev.py b/scripts/git_rev.py new file mode 100644 index 0000000..9594475 --- /dev/null +++ b/scripts/git_rev.py @@ -0,0 +1,8 @@ +import subprocess + +revision = ( + subprocess.check_output(["git", "rev-parse", "HEAD"]) + .strip() + .decode("utf-8") +) +print("'-DGIT_REV=\"%s\"'" % revision) \ No newline at end of file diff --git a/src/idf_component.yml b/src/idf_component.yml new file mode 100644 index 0000000..8bff6b8 --- /dev/null +++ b/src/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + # Required IDF version + idf: ">=4.4" + + esp_littlefs: + git: https://github.com/joltwallet/esp_littlefs.git \ No newline at end of file diff --git a/src/lib/block_notify.cpp b/src/lib/block_notify.cpp index cd6a96a..c11e6d5 100644 --- a/src/lib/block_notify.cpp +++ b/src/lib/block_notify.cpp @@ -7,21 +7,41 @@ unsigned long int currentBlockHeight; void setupBlockNotify() { + IPAddress result; + + int dnsErr = -1; + String mempoolInstance = preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE); + + while (dnsErr != 1) { + dnsErr = WiFi.hostByName(mempoolInstance.c_str(), result); + + if (dnsErr != 1) { + Serial.print(mempoolInstance); + Serial.println("mempool DNS could not be resolved"); + WiFi.reconnect(); + vTaskDelay(pdMS_TO_TICKS(1000)); + } + } + Serial.println("mempool DNS can be resolved"); + // Get current block height through regular API HTTPClient *http = new HTTPClient(); - http->begin("https://mempool.space/api/blocks/tip/height"); + http->begin("https://" + mempoolInstance + "/api/blocks/tip/height"); int httpCode = http->GET(); if (httpCode > 0 && httpCode == HTTP_CODE_OK) { String blockHeightStr = http->getString(); currentBlockHeight = blockHeightStr.toInt(); + xTaskNotifyGive(blockUpdateTaskHandle); } esp_websocket_client_config_t config = { - .uri = "wss://mempool.space/api/v1/ws", + .uri = "wss://mempool.bitcoin.nl/api/v1/ws", }; + Serial.printf("Connecting to %s\r\n", config.uri); + client = esp_websocket_client_init(&config); esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, onWebsocketEvent, client); esp_websocket_client_start(client); @@ -36,14 +56,11 @@ void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_i { case WEBSOCKET_EVENT_CONNECTED: Serial.println("Connected to Mempool.space WebSocket"); - // init = "{\"action\": \"init\"}"; - // if(esp_websocket_client_send_text(client, init.c_str(), init.length(), portMAX_DELAY) == -1) { - // Serial.println("Error init"); - // } + sub = "{\"action\": \"want\", \"data\":[\"blocks\"]}"; if (esp_websocket_client_send_text(client, sub.c_str(), sub.length(), portMAX_DELAY) == -1) { - Serial.println("Error want"); + Serial.println("Mempool.space WS Block Subscribe Error"); } break; @@ -52,17 +69,17 @@ void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_i // Handle the received WebSocket message (block notifications) here break; case WEBSOCKET_EVENT_ERROR: - Serial.println("Connnection error"); + Serial.println("Mempool.space WS Connnection error"); break; case WEBSOCKET_EVENT_DISCONNECTED: - Serial.println("Connnection Closed"); + Serial.println("Mempool.space WS Connnection Closed"); break; } } void onWebsocketMessage(esp_websocket_event_data_t *event_data) { - DynamicJsonDocument doc(event_data->data_len); + SpiRamJsonDocument doc(event_data->data_len); deserializeJson(doc, (char *)event_data->data_ptr); // serializeJsonPretty(doc, Serial); @@ -82,7 +99,6 @@ void onWebsocketMessage(esp_websocket_event_data_t *event_data) doc.clear(); } - unsigned long getBlockHeight() { return currentBlockHeight; diff --git a/src/lib/block_notify.hpp b/src/lib/block_notify.hpp index a6816a5..b0a70aa 100644 --- a/src/lib/block_notify.hpp +++ b/src/lib/block_notify.hpp @@ -4,6 +4,7 @@ #include #include #include +#include "shared.hpp" #include "esp_websocket_client.h" #include "screen_handler.hpp" diff --git a/src/lib/button_handler.cpp b/src/lib/button_handler.cpp new file mode 100644 index 0000000..4d287ce --- /dev/null +++ b/src/lib/button_handler.cpp @@ -0,0 +1,63 @@ +#include "button_handler.hpp" + +TaskHandle_t buttonTaskHandle = NULL; +const TickType_t debounceDelay = pdMS_TO_TICKS(50); +TickType_t lastDebounceTime = 0; + +void buttonTask(void *parameter) +{ + while (1) + { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + TickType_t currentTime = xTaskGetTickCount(); + if ((currentTime - lastDebounceTime) >= debounceDelay) + { + lastDebounceTime = currentTime; + + if (!digitalRead(MCP_INT_PIN)) + { + uint pin = mcp.getLastInterruptPin(); + + Serial.printf("Button pressed: %d", pin); + // switch (pin) + // { + // case 3: + // toggleScreenTimer(); + // break; + // case 2: + // nextScreen(); + // break; + // case 1: + // previousScreen(); + // break; + // case 0: + // showNetworkSettings(); + // break; + // } + } + mcp.clearInterrupts(); + // Very ugly, but for some reason this is necessary + while (!digitalRead(MCP_INT_PIN)) + { + mcp.clearInterrupts(); + } + } + } +} + +void IRAM_ATTR handleButtonInterrupt() +{ + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + xTaskNotifyFromISR(buttonTaskHandle, 0, eNoAction, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) + { + portYIELD_FROM_ISR(); + } +} + +void setupButtonTask() +{ + xTaskCreate(buttonTask, "ButtonTask", 4096, NULL, tskIDLE_PRIORITY, &buttonTaskHandle); // Create the FreeRTOS task + // Use interrupt instead of task + attachInterrupt(MCP_INT_PIN, handleButtonInterrupt, CHANGE); +} diff --git a/src/lib/button_handler.hpp b/src/lib/button_handler.hpp new file mode 100644 index 0000000..02849c2 --- /dev/null +++ b/src/lib/button_handler.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include +#include "shared.hpp" + +extern TaskHandle_t buttonTaskHandle; + +void buttonTask(void *pvParameters); +void IRAM_ATTR handleButtonInterrupt(); +void setupButtonTask(); diff --git a/src/lib/config.cpp b/src/lib/config.cpp new file mode 100644 index 0000000..f09005b --- /dev/null +++ b/src/lib/config.cpp @@ -0,0 +1,377 @@ +#include "config.hpp" + +#ifndef NEOPIXEL_PIN +#define NEOPIXEL_PIN 34 +#endif +#ifndef NEOPIXEL_COUNT +#define NEOPIXEL_COUNT 4 +#endif + +#define MAX_ATTEMPTS_WIFI_CONNECTION 20 + +Preferences preferences; +Adafruit_MCP23X17 mcp; +Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); + +void setup() +{ + setupHardware(); + if (mcp.digitalRead(3) == LOW) { + WiFi.eraseAP(); + blinkDelay(100, 3); + } + + tryImprovSetup(); + + setupDisplays(); + setupPreferences(); + setupWebserver(); + + // setupWifi(); + setupTime(); + finishSetup(); + + setupTasks(); + setupTimers(); + setupWebsocketClients(); + + setupButtonTask(); +} + +void tryImprovSetup() +{ + // if (mcp.digitalRead(3) == LOW) + // { + // WiFi.persistent(false); + // blinkDelay(100, 3); + // } + // else + // { + // WiFi.begin(); + // } + + uint8_t x_buffer[16]; + uint8_t x_position = 0; + + while (WiFi.status() != WL_CONNECTED) + { + if (Serial.available() > 0) + { + uint8_t b = Serial.read(); + + if (parse_improv_serial_byte(x_position, b, x_buffer, onImprovCommandCallback, onImprovErrorCallback)) + { + x_buffer[x_position++] = b; + } + else + { + x_position = 0; + } + } + vTaskDelay(1); + } +} + +void setupTime() +{ + configTime(preferences.getInt("gmtOffset", TIME_OFFSET_SECONDS), 0, NTP_SERVER); + struct tm timeinfo; + + while (!getLocalTime(&timeinfo)) + { + configTime(preferences.getInt("gmtOffset", TIME_OFFSET_SECONDS), 0, NTP_SERVER); + delay(500); + Serial.println(F("Retry set time")); + } + + Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S"); +} + +void setupPreferences() +{ + preferences.begin("btclock", false); +} + +void setupWebsocketClients() +{ + setupBlockNotify(); + setupPriceNotify(); +} + +void setupTimers() +{ + xTaskCreate(setupTimeUpdateTimer, "setupTimeUpdateTimer", 4096, NULL, 1, NULL); + xTaskCreate(setupScreenRotateTimer, "setupScreenRotateTimer", 4096, NULL, 1, NULL); +} + +void finishSetup() +{ + pixels.clear(); + pixels.show(); +} + +void setupHardware() +{ + pixels.begin(); + 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(); + + if (psramInit()) + { + Serial.println(F("PSRAM is correctly initialized")); + } + else + { + Serial.println(F("PSRAM not available")); + } + + + Wire.begin(35, 36, 400000); + + if (!mcp.begin_I2C(0x20)) + { + Serial.println(F("Error MCP23017")); + + // while (1) + // ; + } + else + { + pinMode(MCP_INT_PIN, INPUT_PULLUP); + mcp.setupInterrupts(false, false, LOW); + + for (int i = 0; i < 4; i++) + { + mcp.pinMode(i, INPUT_PULLUP); + mcp.setupInterruptPin(i, LOW); + } + for (int i = 8; i <= 14; i++) + { + mcp.pinMode(i, OUTPUT); + } + + } +} + +void improvGetAvailableWifiNetworks() +{ + int networkNum = WiFi.scanNetworks(); + + for (int id = 0; id < networkNum; ++id) + { + std::vector data = improv::build_rpc_response( + improv::GET_WIFI_NETWORKS, {WiFi.SSID(id), String(WiFi.RSSI(id)), (WiFi.encryptionType(id) == WIFI_AUTH_OPEN ? "NO" : "YES")}, false); + improv_send_response(data); + } + // final response + std::vector data = + improv::build_rpc_response(improv::GET_WIFI_NETWORKS, std::vector{}, false); + improv_send_response(data); +} + +bool improv_connectWifi(std::string ssid, std::string password) +{ + uint8_t count = 0; + + WiFi.begin(ssid.c_str(), password.c_str()); + + while (WiFi.status() != WL_CONNECTED) + { + blinkDelay(500, 2); + + if (count > MAX_ATTEMPTS_WIFI_CONNECTION) + { + WiFi.disconnect(); + return false; + } + count++; + } + + return true; +} + +void blinkDelay(int d, int times) +{ + for (int j = 0; j < times; j++) + { + + pixels.setPixelColor(0, pixels.Color(255, 0, 0)); + pixels.setPixelColor(1, pixels.Color(0, 255, 0)); + pixels.setPixelColor(2, pixels.Color(255, 0, 0)); + pixels.setPixelColor(3, pixels.Color(0, 255, 0)); + pixels.show(); + vTaskDelay(pdMS_TO_TICKS(d)); + + pixels.setPixelColor(0, pixels.Color(255, 255, 0)); + pixels.setPixelColor(1, pixels.Color(0, 255, 255)); + pixels.setPixelColor(2, pixels.Color(255, 255, 0)); + pixels.setPixelColor(3, pixels.Color(0, 255, 255)); + pixels.show(); + vTaskDelay(pdMS_TO_TICKS(d)); + } + pixels.clear(); + pixels.show(); +} + +void onImprovErrorCallback(improv::Error err) +{ + pixels.setPixelColor(0, pixels.Color(255, 0, 0)); + pixels.setPixelColor(1, pixels.Color(255, 0, 0)); + pixels.setPixelColor(2, pixels.Color(255, 0, 0)); + pixels.setPixelColor(3, pixels.Color(255, 0, 0)); + pixels.show(); + vTaskDelay(pdMS_TO_TICKS(100)); + + pixels.clear(); + pixels.show(); + vTaskDelay(pdMS_TO_TICKS(100)); +} + +std::vector getLocalUrl() +{ + return { + // URL where user can finish onboarding or use device + // Recommended to use website hosted by device + String("http://" + WiFi.localIP().toString()).c_str()}; +} + +bool onImprovCommandCallback(improv::ImprovCommand cmd) +{ + + switch (cmd.command) + { + case improv::Command::GET_CURRENT_STATE: + { + if ((WiFi.status() == WL_CONNECTED)) + { + improv_set_state(improv::State::STATE_PROVISIONED); + std::vector data = improv::build_rpc_response(improv::GET_CURRENT_STATE, getLocalUrl(), false); + improv_send_response(data); + } + else + { + improv_set_state(improv::State::STATE_AUTHORIZED); + } + + break; + } + + case improv::Command::WIFI_SETTINGS: + { + if (cmd.ssid.length() == 0) + { + improv_set_error(improv::Error::ERROR_INVALID_RPC); + break; + } + + improv_set_state(improv::STATE_PROVISIONING); + + if (improv_connectWifi(cmd.ssid, cmd.password)) + { + + blinkDelay(100, 3); + // std::array epdContent = {"S", "U", "C", "C", "E", "S", "S"}; + // setEpdContent(epdContent); + + improv_set_state(improv::STATE_PROVISIONED); + std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, getLocalUrl(), false); + improv_send_response(data); + setupWebserver(); + } + else + { + improv_set_state(improv::STATE_STOPPED); + improv_set_error(improv::Error::ERROR_UNABLE_TO_CONNECT); + } + + break; + } + + case improv::Command::GET_DEVICE_INFO: + { + std::vector infos = { + // Firmware name + "BTClock", + // Firmware version + "1.0.0", + // Hardware chip/variant + "ESP32S3", + // Device name + "BTClock"}; + std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); + improv_send_response(data); + break; + } + + case improv::Command::GET_WIFI_NETWORKS: + { + improvGetAvailableWifiNetworks(); + // std::array epdContent = {"W", "E", "B", "W", "I", "F", "I"}; + // setEpdContent(epdContent); + break; + } + + default: + { + improv_set_error(improv::ERROR_UNKNOWN_RPC); + return false; + } + } + + return true; +} + +void improv_set_state(improv::State state) +{ + + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(11); + data[6] = improv::IMPROV_SERIAL_VERSION; + data[7] = improv::TYPE_CURRENT_STATE; + data[8] = 1; + data[9] = state; + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data[10] = checksum; + + Serial.write(data.data(), data.size()); +} + +void improv_send_response(std::vector &response) +{ + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(9); + data[6] = improv::IMPROV_SERIAL_VERSION; + data[7] = improv::TYPE_RPC_RESPONSE; + data[8] = response.size(); + data.insert(data.end(), response.begin(), response.end()); + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data.push_back(checksum); + + Serial.write(data.data(), data.size()); +} + +void improv_set_error(improv::Error error) +{ + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(11); + data[6] = improv::IMPROV_SERIAL_VERSION; + data[7] = improv::TYPE_ERROR_STATE; + data[8] = 1; + data[9] = error; + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data[10] = checksum; + + Serial.write(data.data(), data.size()); +} \ No newline at end of file diff --git a/src/lib/config.hpp b/src/lib/config.hpp index 454f25d..72204f7 100644 --- a/src/lib/config.hpp +++ b/src/lib/config.hpp @@ -2,22 +2,47 @@ #include #include #include +#include #include "shared.hpp" #include #include #include #include "epd.hpp" +#include "improv.hpp" #include "lib/screen_handler.hpp" #include "lib/webserver.hpp" #include "lib/block_notify.hpp" #include "lib/price_notify.hpp" +#include "lib/button_handler.hpp" +#define NTP_SERVER "pool.ntp.org" +#define DEFAULT_MEMPOOL_INSTANCE "mempool.space" +#define TIME_OFFSET_SECONDS 3600 +#define USER_AGENT "BTClock/2.0" +#define MCP_DEV_ADDR 0x20 + +#define BITCOIND_HOST "" +#define BITCOIND_PORT 8332 +#define BITCOIND_RPC_USER "" +#define BITCOIND_RPC_PASS "" void setup(); void setupTime(); void setupPreferences(); -void setupWifi(); void setupWebsocketClients(); -void setupHardware(); \ No newline at end of file +void setupHardware(); +void tryImprovSetup(); +void setupTimers(); +void finishSetup(); + +std::vector getLocalUrl(); +bool improv_connectWifi(std::string ssid, std::string password); +void improvGetAvailableWifiNetworks(); +bool onImprovCommandCallback(improv::ImprovCommand cmd); +void onImprovErrorCallback(improv::Error err); +void improv_set_state(improv::State state); +void improv_send_response(std::vector &response); +void improv_set_error(improv::Error error); +void blinkDelay(int d, int times); \ No newline at end of file diff --git a/src/lib/epd.cpp b/src/lib/epd.cpp index c65405a..96486a0 100644 --- a/src/lib/epd.cpp +++ b/src/lib/epd.cpp @@ -169,7 +169,6 @@ extern "C" void updateDisplay(void *pvParameters) noexcept showDigit(epdIndex, epdContent[epdIndex].c_str()[0], updatePartial, &FONT_BIG); } -#ifdef USE_UNIVERSAL_PIN char tries = 0; while (tries < 3) { @@ -180,14 +179,9 @@ extern "C" void updateDisplay(void *pvParameters) noexcept break; } - delay(100); + vTaskDelay(pdMS_TO_TICKS(100)); tries++; } -#else - displays[epdIndex].display(updatePartial); - displays[epdIndex].hibernate(); - currentEpdContent[epdIndex] = epdContent[epdIndex]; -#endif } xSemaphoreGive(epdUpdateSemaphore[epdIndex]); } @@ -265,3 +259,13 @@ void setFgColor(int color) { fgColor = color; } + +std::array getCurrentEpdContent() +{ + // Serial.println("currentEpdContent"); + + // for (int i = 0; i < NUM_SCREENS; i++) { + // Serial.printf("%d = %s", i, currentEpdContent[i]); + // } + return currentEpdContent; +} diff --git a/src/lib/epd.hpp b/src/lib/epd.hpp index ca6e7fa..6a31013 100644 --- a/src/lib/epd.hpp +++ b/src/lib/epd.hpp @@ -20,3 +20,4 @@ void setBgColor(int color); void setFgColor(int color); void setEpdContent(std::array newEpdContent); +std::array getCurrentEpdContent(); diff --git a/src/lib/improv.cpp b/src/lib/improv.cpp new file mode 100644 index 0000000..2a41a42 --- /dev/null +++ b/src/lib/improv.cpp @@ -0,0 +1,145 @@ +#include "improv.h" + +namespace improv { + +ImprovCommand parse_improv_data(const std::vector &data, bool check_checksum) { + return parse_improv_data(data.data(), data.size(), check_checksum); +} + +ImprovCommand parse_improv_data(const uint8_t *data, size_t length, bool check_checksum) { + ImprovCommand improv_command; + Command command = (Command) data[0]; + uint8_t data_length = data[1]; + + if (data_length != length - 2 - check_checksum) { + improv_command.command = UNKNOWN; + return improv_command; + } + + if (check_checksum) { + uint8_t checksum = data[length - 1]; + + uint32_t calculated_checksum = 0; + for (uint8_t i = 0; i < length - 1; i++) { + calculated_checksum += data[i]; + } + + if ((uint8_t) calculated_checksum != checksum) { + improv_command.command = BAD_CHECKSUM; + return improv_command; + } + } + + if (command == WIFI_SETTINGS) { + uint8_t ssid_length = data[2]; + uint8_t ssid_start = 3; + size_t ssid_end = ssid_start + ssid_length; + + uint8_t pass_length = data[ssid_end]; + size_t pass_start = ssid_end + 1; + size_t pass_end = pass_start + pass_length; + + std::string ssid(data + ssid_start, data + ssid_end); + std::string password(data + pass_start, data + pass_end); + return {.command = command, .ssid = ssid, .password = password}; + } + + improv_command.command = command; + return improv_command; +} + +bool parse_improv_serial_byte(size_t position, uint8_t byte, const uint8_t *buffer, + std::function &&callback, std::function &&on_error) { + if (position == 0) + return byte == 'I'; + if (position == 1) + return byte == 'M'; + if (position == 2) + return byte == 'P'; + if (position == 3) + return byte == 'R'; + if (position == 4) + return byte == 'O'; + if (position == 5) + return byte == 'V'; + + if (position == 6) + return byte == IMPROV_SERIAL_VERSION; + + if (position <= 8) + return true; + + uint8_t type = buffer[7]; + uint8_t data_len = buffer[8]; + + if (position <= 8 + data_len) + return true; + + if (position == 8 + data_len + 1) { + uint8_t checksum = 0x00; + for (size_t i = 0; i < position; i++) + checksum += buffer[i]; + + if (checksum != byte) { + on_error(ERROR_INVALID_RPC); + return false; + } + + if (type == TYPE_RPC) { + auto command = parse_improv_data(&buffer[9], data_len, false); + return callback(command); + } + } + + return false; +} + +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum) { + std::vector out; + uint32_t length = 0; + out.push_back(command); + for (const auto &str : datum) { + uint8_t len = str.length(); + length += len + 1; + out.push_back(len); + out.insert(out.end(), str.begin(), str.end()); + } + out.insert(out.begin() + 1, length); + + if (add_checksum) { + uint32_t calculated_checksum = 0; + + for (uint8_t byte : out) { + calculated_checksum += byte; + } + out.push_back(calculated_checksum); + } + return out; +} + +#ifdef ARDUINO +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum) { + std::vector out; + uint32_t length = 0; + out.push_back(command); + for (const auto &str : datum) { + uint8_t len = str.length(); + length += len; + out.push_back(len); + out.insert(out.end(), str.begin(), str.end()); + } + out.insert(out.begin() + 1, length); + + if (add_checksum) { + uint32_t calculated_checksum = 0; + + for (uint8_t byte : out) { + calculated_checksum += byte; + } + out.push_back(calculated_checksum); + } + return out; +} +#endif // ARDUINO + +} // namespace improv \ No newline at end of file diff --git a/src/lib/improv.hpp b/src/lib/improv.hpp new file mode 100644 index 0000000..00a3785 --- /dev/null +++ b/src/lib/improv.hpp @@ -0,0 +1,76 @@ +#pragma once + +#ifdef ARDUINO +#include +#endif // ARDUINO + +#include +#include +#include +#include + +namespace improv { + +static const char *const SERVICE_UUID = "00467768-6228-2272-4663-277478268000"; +static const char *const STATUS_UUID = "00467768-6228-2272-4663-277478268001"; +static const char *const ERROR_UUID = "00467768-6228-2272-4663-277478268002"; +static const char *const RPC_COMMAND_UUID = "00467768-6228-2272-4663-277478268003"; +static const char *const RPC_RESULT_UUID = "00467768-6228-2272-4663-277478268004"; +static const char *const CAPABILITIES_UUID = "00467768-6228-2272-4663-277478268005"; + +enum Error : uint8_t { + ERROR_NONE = 0x00, + ERROR_INVALID_RPC = 0x01, + ERROR_UNKNOWN_RPC = 0x02, + ERROR_UNABLE_TO_CONNECT = 0x03, + ERROR_NOT_AUTHORIZED = 0x04, + ERROR_UNKNOWN = 0xFF, +}; + +enum State : uint8_t { + STATE_STOPPED = 0x00, + STATE_AWAITING_AUTHORIZATION = 0x01, + STATE_AUTHORIZED = 0x02, + STATE_PROVISIONING = 0x03, + STATE_PROVISIONED = 0x04, +}; + +enum Command : uint8_t { + UNKNOWN = 0x00, + WIFI_SETTINGS = 0x01, + IDENTIFY = 0x02, + GET_CURRENT_STATE = 0x02, + GET_DEVICE_INFO = 0x03, + GET_WIFI_NETWORKS = 0x04, + BAD_CHECKSUM = 0xFF, +}; + +static const uint8_t CAPABILITY_IDENTIFY = 0x01; +static const uint8_t IMPROV_SERIAL_VERSION = 1; + +enum ImprovSerialType : uint8_t { + TYPE_CURRENT_STATE = 0x01, + TYPE_ERROR_STATE = 0x02, + TYPE_RPC = 0x03, + TYPE_RPC_RESPONSE = 0x04 +}; + +struct ImprovCommand { + Command command; + std::string ssid; + std::string password; +}; + +ImprovCommand parse_improv_data(const std::vector &data, bool check_checksum = true); +ImprovCommand parse_improv_data(const uint8_t *data, size_t length, bool check_checksum = true); + +bool parse_improv_serial_byte(size_t position, uint8_t byte, const uint8_t *buffer, + std::function &&callback, std::function &&on_error); + +std::vector build_rpc_response(Command command, const std::vector &datum, + bool add_checksum = true); +#ifdef ARDUINO +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum = true); +#endif // ARDUINO + +} // namespace improv \ No newline at end of file diff --git a/src/lib/led_handler.cpp b/src/lib/led_handler.cpp new file mode 100644 index 0000000..77fd7aa --- /dev/null +++ b/src/lib/led_handler.cpp @@ -0,0 +1,18 @@ +#include "led_handler.hpp" + +TaskHandle_t ledTaskHandle = NULL; +const TickType_t debounceDelay = pdMS_TO_TICKS(50); + +void ledTask(void *parameter) +{ + while (1) + { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + } +} + +void setupLedTask() +{ + xTaskCreate(ledTask, "LedTask", 4096, NULL, tskIDLE_PRIORITY, &ledTaskHandle); // Create the FreeRTOS task +} diff --git a/src/lib/led_handler.hpp b/src/lib/led_handler.hpp new file mode 100644 index 0000000..b7d41c7 --- /dev/null +++ b/src/lib/led_handler.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include +#include +#include "shared.hpp" + +extern TaskHandle_t ledTaskHandle; + +void ledTask(void *pvParameters); +void setupLedTask(); diff --git a/src/lib/price_notify.cpp b/src/lib/price_notify.cpp index 891bc94..73a83be 100644 --- a/src/lib/price_notify.cpp +++ b/src/lib/price_notify.cpp @@ -40,19 +40,19 @@ void onWebsocketPriceEvent(void *handler_args, esp_event_base_t base, int32_t ev void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data) { - DynamicJsonDocument doc(event_data->data_len); + SpiRamJsonDocument doc(event_data->data_len); deserializeJson(doc, (char *)event_data->data_ptr); if (doc.containsKey("bitcoin")) { if (currentPrice != doc["bitcoin"].as()) { - Serial.printf("New price %lu\r\n", currentPrice); + // Serial.printf("New price %lu\r\n", currentPrice); const unsigned long oldPrice = currentPrice; currentPrice = doc["bitcoin"].as(); // if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) { - if (priceUpdateTaskHandle != nullptr) + 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 484a363..d545e20 100644 --- a/src/lib/screen_handler.cpp +++ b/src/lib/screen_handler.cpp @@ -2,9 +2,18 @@ TaskHandle_t priceUpdateTaskHandle; TaskHandle_t blockUpdateTaskHandle; +TaskHandle_t timeUpdateTaskHandle; +TaskHandle_t taskScreenRotateTaskHandle; +esp_timer_handle_t screenRotateTimer; +esp_timer_handle_t minuteTimer; std::array taskEpdContent = {"", "", "", "", "", "", ""}; std::string priceString; +const int usPerSecond = 1000000; +const int usPerMinute = 60 * usPerSecond; +int64_t next_callback_time = 0; + +uint currentScreen; void taskPriceUpdate(void *pvParameters) { @@ -14,18 +23,23 @@ void taskPriceUpdate(void *pvParameters) unsigned long price = getPrice(); uint firstIndex = 0; - if (false) { + if (getCurrentScreen() == SCREEN_BTC_TICKER) + { priceString = ("$" + String(price)).c_str(); - - if (priceString.length() < (NUM_SCREENS)) { + + if (priceString.length() < (NUM_SCREENS)) + { priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' '); taskEpdContent[0] = "BTC/USD"; firstIndex = 1; } - } else { + } + else + { priceString = String(int(round(1 / float(price) * 10e7))).c_str(); - if (priceString.length() < (NUM_SCREENS)) { + if (priceString.length() < (NUM_SCREENS)) + { priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' '); taskEpdContent[0] = "MSCW/TIME"; firstIndex = 1; @@ -41,6 +55,16 @@ void taskPriceUpdate(void *pvParameters) } } +void taskScreenRotate(void *pvParameters) +{ + for (;;) + { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + setCurrentScreen((currentScreen+1) % 5); + } +} + void taskBlockUpdate(void *pvParameters) { for (;;) @@ -49,24 +73,206 @@ void taskBlockUpdate(void *pvParameters) std::string blockNrString = String(getBlockHeight()).c_str(); uint firstIndex = 0; - if (blockNrString.length() < NUM_SCREENS) { - blockNrString.insert(blockNrString.begin(), NUM_SCREENS - blockNrString.length(), ' '); - taskEpdContent[0] = "BLOCK/HEIGHT"; - firstIndex = 1; - } - for (uint i = firstIndex; i < NUM_SCREENS; i++) + if (getCurrentScreen() != SCREEN_HALVING_COUNTDOWN) { - taskEpdContent[i] = blockNrString[i]; + if (blockNrString.length() < NUM_SCREENS) + { + blockNrString.insert(blockNrString.begin(), NUM_SCREENS - blockNrString.length(), ' '); + taskEpdContent[0] = "BLOCK/HEIGHT"; + firstIndex = 1; + } + + for (uint i = firstIndex; i < NUM_SCREENS; i++) + { + taskEpdContent[i] = blockNrString[i]; + } + } + else + { + const uint nextHalvingBlock = 210000 - (getBlockHeight() % 210000); + const uint minutesToHalving = nextHalvingBlock * 10; + + const int years = floor(minutesToHalving / 525600); + const int days = floor((minutesToHalving - (years * 525600)) / (24 * 60)); + const int hours = floor((minutesToHalving - (years * 525600) - (days * (24 * 60))) / 60); + const int mins = floor(minutesToHalving - (years * 525600) - (days * (24 * 60)) - (hours * 60)); + taskEpdContent[0] = "BIT/COIN"; + taskEpdContent[1] = "HALV/ING"; + taskEpdContent[(NUM_SCREENS - 5)] = String(years) + "/YRS"; + taskEpdContent[(NUM_SCREENS - 4)] = String(days) + "/DAYS"; + taskEpdContent[(NUM_SCREENS - 3)] = String(days) + "/HRS"; + taskEpdContent[(NUM_SCREENS - 2)] = String(mins) + "/MINS"; + taskEpdContent[(NUM_SCREENS - 1)] = "TO/GO"; } setEpdContent(taskEpdContent); } } +void taskTimeUpdate(void *pvParameters) +{ + for (;;) + { + if (getCurrentScreen() == SCREEN_TIME) + { + time_t currentTime; + struct tm timeinfo; + time(¤tTime); + localtime_r(¤tTime, &timeinfo); + std::string timeString; + + String minute = String(timeinfo.tm_min); + if (minute.length() < 2) + { + minute = "0" + minute; + } + + timeString = std::to_string(timeinfo.tm_hour) + ":" + minute.c_str(); + timeString.insert(timeString.begin(), NUM_SCREENS - timeString.length(), ' '); + taskEpdContent[0] = String(timeinfo.tm_mday) + "/" + String(timeinfo.tm_mon + 1); + + for (uint i = 1; i < NUM_SCREENS; i++) + { + taskEpdContent[i] = timeString[i]; + } + setEpdContent(taskEpdContent); + } + + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + } +} + +const char* int64_to_iso8601(int64_t timestamp) { + time_t seconds = timestamp / 1000000; // Convert microseconds to seconds + struct tm timeinfo; + gmtime_r(&seconds, &timeinfo); + + // Define a buffer to store the formatted time string + static char iso8601[21]; // ISO 8601 time string has the format "YYYY-MM-DDTHH:MM:SSZ" + + // Format the time into the buffer + strftime(iso8601, sizeof(iso8601), "%Y-%m-%dT%H:%M:%SZ", &timeinfo); + + return iso8601; +} + +void IRAM_ATTR minuteTimerISR(void *arg) +{ + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(timeUpdateTaskHandle, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) + { + portYIELD_FROM_ISR(); + } + int64_t current_time = esp_timer_get_time(); + next_callback_time = current_time + usPerMinute; +} + +void IRAM_ATTR screenRotateTimerISR(void *arg) +{ + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(taskScreenRotateTaskHandle, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) + { + portYIELD_FROM_ISR(); + } +} void setupTasks() { - xTaskCreate(taskPriceUpdate, "updatePrice", 1024, NULL, 1, &priceUpdateTaskHandle); - xTaskCreate(taskBlockUpdate, "updateBlock", 1024, NULL, 1, &blockUpdateTaskHandle); + xTaskCreate(taskPriceUpdate, "updatePrice", 2048, 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); +} + +void setupTimeUpdateTimer(void *pvParameters) +{ + const esp_timer_create_args_t minuteTimerConfig = { + .callback = &minuteTimerISR, + .name = "minute_timer"}; + + esp_timer_create(&minuteTimerConfig, &minuteTimer); + + time_t currentTime; + struct tm timeinfo; + time(¤tTime); + localtime_r(¤tTime, &timeinfo); + uint32_t secondsUntilNextMinute = 60 - timeinfo.tm_sec; + + if (secondsUntilNextMinute > 0) + vTaskDelay(pdMS_TO_TICKS((secondsUntilNextMinute * 1000))); + + esp_timer_start_periodic(minuteTimer, usPerMinute); + xTaskNotifyGive(timeUpdateTaskHandle); + + vTaskDelete(NULL); +} + +void setupScreenRotateTimer(void *pvParameters) +{ + const esp_timer_create_args_t screenRotateTimerConfig = { + .callback = &screenRotateTimerISR, + .name = "screen_rotate_timer"}; + + esp_timer_create(&screenRotateTimerConfig, &screenRotateTimer); + + esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond); + + Serial.println("Set up Screen Rotate Timer"); + + vTaskDelete(NULL); +} + +uint getTimerSeconds() +{ + return preferences.getUInt("timerSeconds", 1800); +} + +bool isTimerActive() +{ + return esp_timer_is_active(screenRotateTimer); +} + +void setTimerActive(bool status) +{ + if (status) + { + esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond); + } + else + { + esp_timer_stop(screenRotateTimer); + } +} + +uint getCurrentScreen() +{ + return currentScreen; +} + +void setCurrentScreen(uint newScreen) +{ + if (newScreen != SCREEN_CUSTOM) + { + preferences.putUInt("currentScreen", newScreen); + } + + currentScreen = newScreen; + + switch (currentScreen) + { + case SCREEN_TIME: + xTaskNotifyGive(timeUpdateTaskHandle); + break; + case SCREEN_HALVING_COUNTDOWN: + case SCREEN_BLOCK_HEIGHT: + xTaskNotifyGive(blockUpdateTaskHandle); + break; + case SCREEN_MSCW_TIME: + case SCREEN_BTC_TICKER: + xTaskNotifyGive(priceUpdateTaskHandle); + break; + } } \ No newline at end of file diff --git a/src/lib/screen_handler.hpp b/src/lib/screen_handler.hpp index feb31ce..a7c1d14 100644 --- a/src/lib/screen_handler.hpp +++ b/src/lib/screen_handler.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -9,8 +11,27 @@ extern TaskHandle_t priceUpdateTaskHandle; extern TaskHandle_t blockUpdateTaskHandle; +extern TaskHandle_t timeUpdateTaskHandle; +extern TaskHandle_t taskScreenRotateTaskHandle; + +uint getCurrentScreen(); +void setCurrentScreen(uint newScreen); + +void setupTimeUpdateTimer(void *pvParameters); +void setupScreenRotateTimer(void *pvParameters); + +void IRAM_ATTR minuteTimerISR(void* arg); +void IRAM_ATTR screenRotateTimerISR(void* arg); void taskPriceUpdate(void *pvParameters); void taskBlockUpdate(void *pvParameters); +void taskTimeUpdate(void *pvParameters); +void taskScreenRotate(void *pvParameters); -void setupTasks(); \ No newline at end of file +uint getTimerSeconds(); +bool isTimerActive(); +void setTimerActive(bool status); + + +void setupTasks(); +const char* int64_to_iso8601(int64_t timestamp); \ No newline at end of file diff --git a/src/lib/shared.hpp b/src/lib/shared.hpp index 97705c7..697f390 100644 --- a/src/lib/shared.hpp +++ b/src/lib/shared.hpp @@ -1,7 +1,33 @@ #pragma once #include +#include #include extern Adafruit_MCP23X17 mcp; extern Preferences preferences; + +const PROGMEM int SCREEN_BLOCK_HEIGHT = 0; +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_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 }; + +struct SpiRamAllocator { + void* allocate(size_t size) { + return heap_caps_malloc(size, MALLOC_CAP_SPIRAM); + } + + void deallocate(void* pointer) { + heap_caps_free(pointer); + } + + void* reallocate(void* ptr, size_t new_size) { + return heap_caps_realloc(ptr, new_size, MALLOC_CAP_SPIRAM); + } +}; + +using SpiRamJsonDocument = BasicJsonDocument; diff --git a/src/lib/webserver.cpp b/src/lib/webserver.cpp index b2b3991..cd90697 100644 --- a/src/lib/webserver.cpp +++ b/src/lib/webserver.cpp @@ -4,32 +4,365 @@ AsyncWebServer server(80); void setupWebserver() { - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ - request->send(200, "text/plain", "Hello, world"); - }); + if (!LittleFS.begin(true)) + { + Serial.println(F("An Error has occurred while mounting LittleFS")); + return; + } - server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request){ - AsyncResponseStream *response = request->beginResponseStream("application/json"); - StaticJsonDocument<128> root; - - root["currentPrice"] = getPrice(); - root["currentBlockHeight"] = getBlockHeight(); - root["espFreeHeap"] = ESP.getFreeHeap(); - root["espHeapSize"] = ESP.getHeapSize(); - root["espFreePsram"] = ESP.getFreePsram(); - root["espPsramSize"] = ESP.getPsramSize(); - - serializeJson(root, *response); + server.serveStatic("/css", LittleFS, "/css/"); + server.serveStatic("/js", LittleFS, "/js/"); + server.serveStatic("/font", LittleFS, "/font/"); + server.on("/", HTTP_GET, onIndex); - request->send(response); - }); + server.on("/api/status", HTTP_GET, onApiStatus); + server.on("/api/system_status", HTTP_GET, onApiSystemStatus); + + server.on("/api/action/pause", HTTP_GET, onApiActionPause); + server.on("/api/action/timer_restart", HTTP_GET, onApiActionTimerRestart); + + server.on("/api/settings", HTTP_GET, onApiSettingsGet); + server.on("/api/settings", HTTP_POST, onApiSettingsPost); + + server.on("/api/show/screen", HTTP_GET, onApiShowScreen); + server.on("/api/show/text", HTTP_GET, onApiShowText); + + server.on("/api/restart", HTTP_GET, onApiRestart); + + 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); server.begin(); } +/** + * @Api + * @Path("/api/status") + */ +void onApiStatus(AsyncWebServerRequest *request) +{ + AsyncResponseStream *response = request->beginResponseStream("application/json"); + StaticJsonDocument<512> root; + + root["currentScreen"] = getCurrentScreen(); + root["numScreens"] = NUM_SCREENS; + root["timerRunning"] = isTimerActive();; + root["espUptime"] = esp_timer_get_time() / 1000000; + root["currentPrice"] = getPrice(); + root["currentBlockHeight"] = getBlockHeight(); + root["espFreeHeap"] = ESP.getFreeHeap(); + root["espHeapSize"] = ESP.getHeapSize(); + root["espFreePsram"] = ESP.getFreePsram(); + root["espPsramSize"] = ESP.getPsramSize(); + + JsonArray data = root.createNestedArray("data"); + JsonArray rendered = root.createNestedArray("rendered"); + String epdContent[NUM_SCREENS]; + + std::array retEpdContent = getCurrentEpdContent(); + + std::copy(std::begin(retEpdContent), std::end(retEpdContent), epdContent); + + copyArray(epdContent, data); + copyArray(epdContent, rendered); + serializeJson(root, *response); + + request->send(response); +} + +/** + * @Api + * @Path("/api/action/pause") + */ +void onApiActionPause(AsyncWebServerRequest *request) +{ + setTimerActive(false); + Serial.println(F("Update timer paused")); + + request->send(200); +}; + +/** + * @Api + * @Path("/api/action/timer_restart") + */ +void onApiActionTimerRestart(AsyncWebServerRequest *request) +{ + // moment = millis(); + setTimerActive(true); + Serial.println(F("Update timer restarted")); + + request->send(200); +} + + +void onApiShowScreen(AsyncWebServerRequest *request) +{ + if (request->hasParam("s")) + { + AsyncWebParameter *p = request->getParam("s"); + uint currentScreen = p->value().toInt(); + setCurrentScreen(currentScreen); + } + request->send(200); +} + +void onApiShowText(AsyncWebServerRequest *request) +{ + if (request->hasParam("t")) + { + AsyncWebParameter *p = request->getParam("t"); + String t = p->value(); + t.toUpperCase(); // This is needed as long as lowercase letters are glitchy + + std::array textEpdContent; + for (uint i = 0; i < NUM_SCREENS; i++) { + textEpdContent[i] = t[i]; + } + + setEpdContent(textEpdContent); + } + //setCurrentScreen(SCREEN_CUSTOM); + request->send(200); +} + +void onApiRestart(AsyncWebServerRequest *request) +{ + request->send(200); + esp_restart(); +} + +/** + * @Api + * @Method GET + * @Path("/api/settings") + */ +void onApiSettingsGet(AsyncWebServerRequest *request) +{ + StaticJsonDocument<768> root; + root["numScreens"] = NUM_SCREENS; + root["fgColor"] = getFgColor(); + root["bgColor"] = getBgColor(); + root["timerSeconds"] = getTimerSeconds(); + root["timerRunning"] = isTimerActive();; + root["fullRefreshMin"] = preferences.getUInt("fullRefreshMin", 30); + 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["mempoolInstance"] = preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE); + + root["epdColors"] = 2; + root["ledFlashOnUpdate"] = preferences.getBool("ledFlashOnUpd", false); + root["ledBrightness"] = preferences.getUInt("ledBrightness", 128); + +#ifdef GIT_REV + root["gitRev"] = String(GIT_REV); +#endif +#ifdef LAST_BUILD_TIME + root["lastBuildTime"] = String(LAST_BUILD_TIME); +#endif + JsonArray screens = root.createNestedArray("screens"); + + // for (int i = 0; i < screenNameMap.size(); i++) + // { + // JsonObject o = screens.createNestedObject(); + // String key = "screen" + String(i) + "Visible"; + // o["id"] = i; + // o["name"] = screenNameMap[i]; + // o["enabled"] = preferences.getBool(key.c_str(), true); + // } + + AsyncResponseStream *response = request->beginResponseStream("application/json"); + serializeJson(root, *response); + + request->send(response); +} + +bool processEpdColorSettings(AsyncWebServerRequest *request) +{ + bool settingsChanged = false; + if (request->hasParam("fgColor", true)) + { + AsyncWebParameter *fgColor = request->getParam("fgColor", true); + preferences.putUInt("fgColor", strtol(fgColor->value().c_str(), NULL, 16)); + setFgColor(int(strtol(fgColor->value().c_str(), NULL, 16))); + Serial.print(F("Setting foreground color to ")); + Serial.println(fgColor->value().c_str()); + settingsChanged = true; + } + if (request->hasParam("bgColor", true)) + { + AsyncWebParameter *bgColor = request->getParam("bgColor", true); + + preferences.putUInt("bgColor", strtol(bgColor->value().c_str(), NULL, 16)); + setBgColor(int(strtol(bgColor->value().c_str(), NULL, 16))); + Serial.print(F("Setting background color to ")); + Serial.println(bgColor->value().c_str()); + settingsChanged = true; + } + + return settingsChanged; +} + +void onApiSettingsPost(AsyncWebServerRequest *request) +{ + int params = request->params(); + bool settingsChanged = false; + + settingsChanged = processEpdColorSettings(request); + + if (request->hasParam("ledFlashOnUpd", true)) + { + AsyncWebParameter *ledFlashOnUpdate = request->getParam("ledFlashOnUpd", true); + + preferences.putBool("ledFlashOnUpd", ledFlashOnUpdate->value().toInt()); + Serial.print("Setting led flash on update to "); + Serial.println(ledFlashOnUpdate->value().c_str()); + settingsChanged = true; + } + else + { + preferences.putBool("ledFlashOnUpd", 0); + Serial.print("Setting led flash on update to false"); + settingsChanged = true; + } + + if (request->hasParam("mempoolInstance", true)) + { + AsyncWebParameter *mempoolInstance = request->getParam("mempoolInstance", true); + + preferences.putString("mempoolInstance", mempoolInstance->value().c_str()); + Serial.print("Setting mempool instance to "); + Serial.println(mempoolInstance->value().c_str()); + settingsChanged = true; + } + + if (request->hasParam("ledBrightness", true)) + { + AsyncWebParameter *ledBrightness = request->getParam("ledBrightness", true); + + preferences.putUInt("ledBrightness", ledBrightness->value().toInt()); + Serial.print("Setting brightness to "); + Serial.println(ledBrightness->value().c_str()); + settingsChanged = true; + } + + if (request->hasParam("fullRefreshMin", true)) + { + AsyncWebParameter *fullRefreshMin = request->getParam("fullRefreshMin", true); + + preferences.putUInt("fullRefreshMin", fullRefreshMin->value().toInt()); + Serial.print("Set full refresh minutes to "); + Serial.println(fullRefreshMin->value().c_str()); + settingsChanged = true; + } + + if (request->hasParam("wpTimeout", true)) + { + AsyncWebParameter *wpTimeout = request->getParam("wpTimeout", true); + + preferences.putUInt("wpTimeout", wpTimeout->value().toInt()); + Serial.print("Set WiFi portal timeout seconds to "); + Serial.println(wpTimeout->value().c_str()); + settingsChanged = true; + } + + // for (int i = 0; i < screenNameMap.size(); i++) + // { + // String key = "screen[" + String(i) + "]"; + // String prefKey = "screen" + String(i) + "Visible"; + // bool visible = false; + // if (request->hasParam(key, true)) + // { + // AsyncWebParameter *screenParam = request->getParam(key, true); + // visible = screenParam->value().toInt(); + // } + // Serial.print("Setting screen " + String(i) + " to "); + // Serial.println(visible); + + // preferences.putBool(prefKey.c_str(), visible); + // } + + if (request->hasParam("tzOffset", true)) + { + AsyncWebParameter *p = request->getParam("tzOffset", true); + int tzOffsetSeconds = p->value().toInt() * 60; + preferences.putInt("gmtOffset", tzOffsetSeconds); + Serial.print("Setting tz offset to "); + Serial.println(tzOffsetSeconds); + settingsChanged = true; + } + + if (request->hasParam("timePerScreen", true)) + { + AsyncWebParameter *p = request->getParam("timePerScreen", true); + uint timerSeconds = p->value().toInt() * 60; + preferences.putUInt("timerSeconds", timerSeconds); + 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"}; + + 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) + { + //flashTemporaryLights(0, 255, 0); + + Serial.println(F("Settings changed")); + } +} + +void onApiSystemStatus(AsyncWebServerRequest *request) +{ + AsyncResponseStream *response = request->beginResponseStream("application/json"); + + StaticJsonDocument<128> root; + + root["espFreeHeap"] = ESP.getFreeHeap(); + root["espHeapSize"] = ESP.getHeapSize(); + root["espFreePsram"] = ESP.getFreePsram(); + root["espPsramSize"] = ESP.getPsramSize(); + + serializeJson(root, *response); + + request->send(response); +} + +void onIndex(AsyncWebServerRequest *request) { request->send(LittleFS, "/index.html", String(), false); } + void onNotFound(AsyncWebServerRequest *request) { if (request->method() == HTTP_OPTIONS) diff --git a/src/lib/webserver.hpp b/src/lib/webserver.hpp index bab3f63..5de2e94 100644 --- a/src/lib/webserver.hpp +++ b/src/lib/webserver.hpp @@ -2,9 +2,30 @@ #include "ESPAsyncWebServer.h" #include +#include #include "lib/block_notify.hpp" #include "lib/price_notify.hpp" +#include "lib/screen_handler.hpp" + + +#include "webserver/OneParamRewrite.hpp" void setupWebserver(); +bool processEpdColorSettings(AsyncWebServerRequest *request); + +void onApiStatus(AsyncWebServerRequest *request); +void onApiSystemStatus(AsyncWebServerRequest *request); + +void onApiShowScreen(AsyncWebServerRequest *request); +void onApiShowText(AsyncWebServerRequest *request); + +void onApiActionPause(AsyncWebServerRequest *request); +void onApiActionTimerRestart(AsyncWebServerRequest *request); +void onApiSettingsGet(AsyncWebServerRequest *request); +void onApiSettingsPost(AsyncWebServerRequest *request); + +void onApiRestart(AsyncWebServerRequest *request); + +void onIndex(AsyncWebServerRequest *request); void onNotFound(AsyncWebServerRequest *request); \ No newline at end of file diff --git a/src/lib/webserver/OneParamRewrite.cpp b/src/lib/webserver/OneParamRewrite.cpp new file mode 100644 index 0000000..b7ed85e --- /dev/null +++ b/src/lib/webserver/OneParamRewrite.cpp @@ -0,0 +1,43 @@ +#include "OneParamRewrite.hpp" + +OneParamRewrite::OneParamRewrite(const char *from, const char *to) + : AsyncWebRewrite(from, to) +{ + + _paramIndex = _from.indexOf('{'); + + if (_paramIndex >= 0 && _from.endsWith("}")) + { + _urlPrefix = _from.substring(0, _paramIndex); + int index = _params.indexOf('{'); + if (index >= 0) + { + _params = _params.substring(0, index); + } + } + else + { + _urlPrefix = _from; + } + _paramsBackup = _params; +} + +bool OneParamRewrite::match(AsyncWebServerRequest *request) +{ + if (request->url().startsWith(_urlPrefix)) + { + if (_paramIndex >= 0) + { + _params = _paramsBackup + request->url().substring(_paramIndex); + } + else + { + _params = _paramsBackup; + } + return true; + } + else + { + return false; + } +}; diff --git a/src/lib/webserver/OneParamRewrite.hpp b/src/lib/webserver/OneParamRewrite.hpp new file mode 100644 index 0000000..01d5943 --- /dev/null +++ b/src/lib/webserver/OneParamRewrite.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "ESPAsyncWebServer.h" + +class OneParamRewrite : public AsyncWebRewrite +{ +protected: + String _urlPrefix; + int _paramIndex; + String _paramsBackup; + +public: + OneParamRewrite(const char *from, const char *to); + bool match(AsyncWebServerRequest *request) override; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 87b373e..e333e8e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,20 +1,15 @@ #include "Arduino.h" #include "lib/config.hpp" - extern "C" void app_main() { initArduino(); - setup(); Serial.begin(115200); - static char sBuffer[240]; + setup(); while (true) { - // Serial.println("-------"); - // vTaskGetRunTimeStats((char *)sBuffer); - // Serial.println(sBuffer); vTaskDelay(pdMS_TO_TICKS(5000)); } } \ No newline at end of file