Missing files in commit

This commit is contained in:
Djuri Baars 2023-11-07 21:26:15 +01:00
parent 4538326990
commit 687bc1f60d
23 changed files with 1485 additions and 61 deletions

8
scripts/git_rev.py Normal file
View file

@ -0,0 +1,8 @@
import subprocess
revision = (
subprocess.check_output(["git", "rev-parse", "HEAD"])
.strip()
.decode("utf-8")
)
print("'-DGIT_REV=\"%s\"'" % revision)

6
src/idf_component.yml Normal file
View file

@ -0,0 +1,6 @@
dependencies:
# Required IDF version
idf: ">=4.4"
esp_littlefs:
git: https://github.com/joltwallet/esp_littlefs.git

View file

@ -7,21 +7,41 @@ unsigned long int currentBlockHeight;
void setupBlockNotify() 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 // Get current block height through regular API
HTTPClient *http = new HTTPClient(); 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(); int httpCode = http->GET();
if (httpCode > 0 && httpCode == HTTP_CODE_OK) if (httpCode > 0 && httpCode == HTTP_CODE_OK)
{ {
String blockHeightStr = http->getString(); String blockHeightStr = http->getString();
currentBlockHeight = blockHeightStr.toInt(); currentBlockHeight = blockHeightStr.toInt();
xTaskNotifyGive(blockUpdateTaskHandle);
} }
esp_websocket_client_config_t config = { 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); client = esp_websocket_client_init(&config);
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, onWebsocketEvent, client); esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, onWebsocketEvent, client);
esp_websocket_client_start(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: case WEBSOCKET_EVENT_CONNECTED:
Serial.println("Connected to Mempool.space WebSocket"); 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\"]}"; sub = "{\"action\": \"want\", \"data\":[\"blocks\"]}";
if (esp_websocket_client_send_text(client, sub.c_str(), sub.length(), portMAX_DELAY) == -1) 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; 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 // Handle the received WebSocket message (block notifications) here
break; break;
case WEBSOCKET_EVENT_ERROR: case WEBSOCKET_EVENT_ERROR:
Serial.println("Connnection error"); Serial.println("Mempool.space WS Connnection error");
break; break;
case WEBSOCKET_EVENT_DISCONNECTED: case WEBSOCKET_EVENT_DISCONNECTED:
Serial.println("Connnection Closed"); Serial.println("Mempool.space WS Connnection Closed");
break; break;
} }
} }
void onWebsocketMessage(esp_websocket_event_data_t *event_data) 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); deserializeJson(doc, (char *)event_data->data_ptr);
// serializeJsonPretty(doc, Serial); // serializeJsonPretty(doc, Serial);
@ -82,7 +99,6 @@ void onWebsocketMessage(esp_websocket_event_data_t *event_data)
doc.clear(); doc.clear();
} }
unsigned long getBlockHeight() unsigned long getBlockHeight()
{ {
return currentBlockHeight; return currentBlockHeight;

View file

@ -4,6 +4,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "shared.hpp"
#include "esp_websocket_client.h" #include "esp_websocket_client.h"
#include "screen_handler.hpp" #include "screen_handler.hpp"

View file

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

View file

@ -0,0 +1,12 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "shared.hpp"
extern TaskHandle_t buttonTaskHandle;
void buttonTask(void *pvParameters);
void IRAM_ATTR handleButtonInterrupt();
void setupButtonTask();

377
src/lib/config.cpp Normal file
View file

@ -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<uint8_t> 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<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, std::vector<std::string>{}, 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<std::string> 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<uint8_t> 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<String, NUM_SCREENS> epdContent = {"S", "U", "C", "C", "E", "S", "S"};
// setEpdContent(epdContent);
improv_set_state(improv::STATE_PROVISIONED);
std::vector<uint8_t> 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<std::string> infos = {
// Firmware name
"BTClock",
// Firmware version
"1.0.0",
// Hardware chip/variant
"ESP32S3",
// Device name
"BTClock"};
std::vector<uint8_t> 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<String, NUM_SCREENS> 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<uint8_t> 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<uint8_t> &response)
{
std::vector<uint8_t> 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<uint8_t> 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());
}

View file

@ -2,22 +2,47 @@
#include <WiFiClientSecure.h> #include <WiFiClientSecure.h>
#include <Preferences.h> #include <Preferences.h>
#include <Adafruit_MCP23X17.h> #include <Adafruit_MCP23X17.h>
#include <Adafruit_NeoPixel.h>
#include "shared.hpp" #include "shared.hpp"
#include <esp_system.h> #include <esp_system.h>
#include <esp_netif.h> #include <esp_netif.h>
#include <esp_sntp.h> #include <esp_sntp.h>
#include "epd.hpp" #include "epd.hpp"
#include "improv.hpp"
#include "lib/screen_handler.hpp" #include "lib/screen_handler.hpp"
#include "lib/webserver.hpp" #include "lib/webserver.hpp"
#include "lib/block_notify.hpp" #include "lib/block_notify.hpp"
#include "lib/price_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 setup();
void setupTime(); void setupTime();
void setupPreferences(); void setupPreferences();
void setupWifi();
void setupWebsocketClients(); void setupWebsocketClients();
void setupHardware(); void setupHardware();
void tryImprovSetup();
void setupTimers();
void finishSetup();
std::vector<std::string> 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<uint8_t> &response);
void improv_set_error(improv::Error error);
void blinkDelay(int d, int times);

View file

@ -169,7 +169,6 @@ extern "C" void updateDisplay(void *pvParameters) noexcept
showDigit(epdIndex, epdContent[epdIndex].c_str()[0], updatePartial, &FONT_BIG); showDigit(epdIndex, epdContent[epdIndex].c_str()[0], updatePartial, &FONT_BIG);
} }
#ifdef USE_UNIVERSAL_PIN
char tries = 0; char tries = 0;
while (tries < 3) while (tries < 3)
{ {
@ -180,14 +179,9 @@ extern "C" void updateDisplay(void *pvParameters) noexcept
break; break;
} }
delay(100); vTaskDelay(pdMS_TO_TICKS(100));
tries++; tries++;
} }
#else
displays[epdIndex].display(updatePartial);
displays[epdIndex].hibernate();
currentEpdContent[epdIndex] = epdContent[epdIndex];
#endif
} }
xSemaphoreGive(epdUpdateSemaphore[epdIndex]); xSemaphoreGive(epdUpdateSemaphore[epdIndex]);
} }
@ -265,3 +259,13 @@ void setFgColor(int color)
{ {
fgColor = color; fgColor = color;
} }
std::array<String, NUM_SCREENS> getCurrentEpdContent()
{
// Serial.println("currentEpdContent");
// for (int i = 0; i < NUM_SCREENS; i++) {
// Serial.printf("%d = %s", i, currentEpdContent[i]);
// }
return currentEpdContent;
}

View file

@ -20,3 +20,4 @@ void setBgColor(int color);
void setFgColor(int color); void setFgColor(int color);
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent); void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent);
std::array<String, NUM_SCREENS> getCurrentEpdContent();

145
src/lib/improv.cpp Normal file
View file

@ -0,0 +1,145 @@
#include "improv.h"
namespace improv {
ImprovCommand parse_improv_data(const std::vector<uint8_t> &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<bool(ImprovCommand)> &&callback, std::function<void(Error)> &&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<uint8_t> build_rpc_response(Command command, const std::vector<std::string> &datum, bool add_checksum) {
std::vector<uint8_t> 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<uint8_t> build_rpc_response(Command command, const std::vector<String> &datum, bool add_checksum) {
std::vector<uint8_t> 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

76
src/lib/improv.hpp Normal file
View file

@ -0,0 +1,76 @@
#pragma once
#ifdef ARDUINO
#include <Arduino.h>
#endif // ARDUINO
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
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<uint8_t> &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<bool(ImprovCommand)> &&callback, std::function<void(Error)> &&on_error);
std::vector<uint8_t> build_rpc_response(Command command, const std::vector<std::string> &datum,
bool add_checksum = true);
#ifdef ARDUINO
std::vector<uint8_t> build_rpc_response(Command command, const std::vector<String> &datum, bool add_checksum = true);
#endif // ARDUINO
} // namespace improv

18
src/lib/led_handler.cpp Normal file
View file

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

12
src/lib/led_handler.hpp Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <Adafruit_NeoPixel.h>
#include "shared.hpp"
extern TaskHandle_t ledTaskHandle;
void ledTask(void *pvParameters);
void setupLedTask();

View file

@ -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) 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); deserializeJson(doc, (char *)event_data->data_ptr);
if (doc.containsKey("bitcoin")) { if (doc.containsKey("bitcoin")) {
if (currentPrice != doc["bitcoin"].as<long>()) { if (currentPrice != doc["bitcoin"].as<long>()) {
Serial.printf("New price %lu\r\n", currentPrice); // Serial.printf("New price %lu\r\n", currentPrice);
const unsigned long oldPrice = currentPrice; const unsigned long oldPrice = currentPrice;
currentPrice = doc["bitcoin"].as<long>(); currentPrice = doc["bitcoin"].as<long>();
// if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) { // 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); xTaskNotifyGive(priceUpdateTaskHandle);
//} //}
} }

View file

@ -2,9 +2,18 @@
TaskHandle_t priceUpdateTaskHandle; TaskHandle_t priceUpdateTaskHandle;
TaskHandle_t blockUpdateTaskHandle; TaskHandle_t blockUpdateTaskHandle;
TaskHandle_t timeUpdateTaskHandle;
TaskHandle_t taskScreenRotateTaskHandle;
esp_timer_handle_t screenRotateTimer;
esp_timer_handle_t minuteTimer;
std::array<String, NUM_SCREENS> taskEpdContent = {"", "", "", "", "", "", ""}; std::array<String, NUM_SCREENS> taskEpdContent = {"", "", "", "", "", "", ""};
std::string priceString; std::string priceString;
const int usPerSecond = 1000000;
const int usPerMinute = 60 * usPerSecond;
int64_t next_callback_time = 0;
uint currentScreen;
void taskPriceUpdate(void *pvParameters) void taskPriceUpdate(void *pvParameters)
{ {
@ -14,18 +23,23 @@ void taskPriceUpdate(void *pvParameters)
unsigned long price = getPrice(); unsigned long price = getPrice();
uint firstIndex = 0; uint firstIndex = 0;
if (false) { if (getCurrentScreen() == SCREEN_BTC_TICKER)
{
priceString = ("$" + String(price)).c_str(); priceString = ("$" + String(price)).c_str();
if (priceString.length() < (NUM_SCREENS)) { if (priceString.length() < (NUM_SCREENS))
{
priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' '); priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' ');
taskEpdContent[0] = "BTC/USD"; taskEpdContent[0] = "BTC/USD";
firstIndex = 1; firstIndex = 1;
} }
} else { }
else
{
priceString = String(int(round(1 / float(price) * 10e7))).c_str(); 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(), ' '); priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' ');
taskEpdContent[0] = "MSCW/TIME"; taskEpdContent[0] = "MSCW/TIME";
firstIndex = 1; 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) void taskBlockUpdate(void *pvParameters)
{ {
for (;;) for (;;)
@ -49,24 +73,206 @@ void taskBlockUpdate(void *pvParameters)
std::string blockNrString = String(getBlockHeight()).c_str(); std::string blockNrString = String(getBlockHeight()).c_str();
uint firstIndex = 0; 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); setEpdContent(taskEpdContent);
} }
} }
void taskTimeUpdate(void *pvParameters)
{
for (;;)
{
if (getCurrentScreen() == SCREEN_TIME)
{
time_t currentTime;
struct tm timeinfo;
time(&currentTime);
localtime_r(&currentTime, &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() void setupTasks()
{ {
xTaskCreate(taskPriceUpdate, "updatePrice", 1024, NULL, 1, &priceUpdateTaskHandle); xTaskCreate(taskPriceUpdate, "updatePrice", 2048, NULL, tskIDLE_PRIORITY, &priceUpdateTaskHandle);
xTaskCreate(taskBlockUpdate, "updateBlock", 1024, NULL, 1, &blockUpdateTaskHandle); 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(&currentTime);
localtime_r(&currentTime, &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;
}
} }

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <esp_timer.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
@ -9,8 +11,27 @@
extern TaskHandle_t priceUpdateTaskHandle; extern TaskHandle_t priceUpdateTaskHandle;
extern TaskHandle_t blockUpdateTaskHandle; 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 taskPriceUpdate(void *pvParameters);
void taskBlockUpdate(void *pvParameters); void taskBlockUpdate(void *pvParameters);
void taskTimeUpdate(void *pvParameters);
void taskScreenRotate(void *pvParameters);
void setupTasks(); uint getTimerSeconds();
bool isTimerActive();
void setTimerActive(bool status);
void setupTasks();
const char* int64_to_iso8601(int64_t timestamp);

View file

@ -1,7 +1,33 @@
#pragma once #pragma once
#include <Adafruit_MCP23X17.h> #include <Adafruit_MCP23X17.h>
#include <ArduinoJson.h>
#include <Preferences.h> #include <Preferences.h>
extern Adafruit_MCP23X17 mcp; extern Adafruit_MCP23X17 mcp;
extern Preferences preferences; 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<SpiRamAllocator>;

View file

@ -4,32 +4,365 @@ AsyncWebServer server(80);
void setupWebserver() void setupWebserver()
{ {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ if (!LittleFS.begin(true))
request->send(200, "text/plain", "Hello, world"); {
}); Serial.println(F("An Error has occurred while mounting LittleFS"));
return;
}
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request){ server.serveStatic("/css", LittleFS, "/css/");
AsyncResponseStream *response = request->beginResponseStream("application/json"); server.serveStatic("/js", LittleFS, "/js/");
StaticJsonDocument<128> root; server.serveStatic("/font", LittleFS, "/font/");
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.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.onNotFound(onNotFound);
server.begin(); 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<String, NUM_SCREENS> 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<String, NUM_SCREENS> 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) void onNotFound(AsyncWebServerRequest *request)
{ {
if (request->method() == HTTP_OPTIONS) if (request->method() == HTTP_OPTIONS)

View file

@ -2,9 +2,30 @@
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h>
#include "lib/block_notify.hpp" #include "lib/block_notify.hpp"
#include "lib/price_notify.hpp" #include "lib/price_notify.hpp"
#include "lib/screen_handler.hpp"
#include "webserver/OneParamRewrite.hpp"
void setupWebserver(); 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); void onNotFound(AsyncWebServerRequest *request);

View file

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

View file

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

View file

@ -1,20 +1,15 @@
#include "Arduino.h" #include "Arduino.h"
#include "lib/config.hpp" #include "lib/config.hpp"
extern "C" void app_main() extern "C" void app_main()
{ {
initArduino(); initArduino();
setup();
Serial.begin(115200); Serial.begin(115200);
static char sBuffer[240]; setup();
while (true) while (true)
{ {
// Serial.println("-------");
// vTaskGetRunTimeStats((char *)sBuffer);
// Serial.println(sBuffer);
vTaskDelay(pdMS_TO_TICKS(5000)); vTaskDelay(pdMS_TO_TICKS(5000));
} }
} }