LEDs and buttons working

This commit is contained in:
Djuri Baars 2023-11-08 12:18:59 +01:00
parent 4f2fbd8a36
commit 91fd921e2e
33 changed files with 3877 additions and 136 deletions

8
.gitignore vendored
View file

@ -4,4 +4,10 @@
.vscode/launch.json .vscode/launch.json
.vscode/ipch .vscode/ipch
managed_components managed_components
data/build/* data/build/*
data/build
data/.yarn
data/node_modules
node_modules
.DS_Store
*.bin

1
data/.yarnrc.yml Normal file
View file

@ -0,0 +1 @@
nodeLinker: node-modules

50
data/esbuild.mjs Normal file
View file

@ -0,0 +1,50 @@
import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import htmlPlugin from '@chialab/esbuild-plugin-html';
import handlebarsPlugin from "esbuild-plugin-handlebars";
import { clean } from 'esbuild-plugin-clean';
import postcss from "postcss";
import autoprefixer from "autoprefixer";
const hbsOptions = {
additionalHelpers: { splitText: "helpers.js" },
additionalPartials: {},
precompileOptions: {}
}
esbuild
.build({
entryPoints: ["src/css/style.scss", "src/js/script.ts", "src/index.html", "src/js/helpers.js"],
outdir: "build",
bundle: true,
loader: {
".png": "dataurl",
".woff": "dataurl",
".woff2": "dataurl",
".eot": "dataurl",
".ttf": "dataurl",
".svg": "dataurl",
},
plugins: [
clean({
patterns: ['./build/*']
}),
htmlPlugin(),
sassPlugin({
async transform(source) {
const { css } = await postcss([autoprefixer]).process(
source
, { from: undefined });
return css;
},
}),
handlebarsPlugin(hbsOptions),
],
minify: true,
metafile: false,
sourcemap: false
})
.then(() => console.log("⚡ Build complete! ⚡"))
.catch(() => process.exit(1));

23
data/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "data",
"packageManager": "yarn@3.2.1",
"scripts": {
"build": "node esbuild.mjs"
},
"dependencies": {
"esbuild": "0.19.4"
},
"devDependencies": {
"@chialab/esbuild-plugin-html": "^0.17.2",
"@craftamap/esbuild-plugin-html": "^0.5.0",
"@esbuilder/html": "^0.0.6",
"autoprefixer": "^10.4.16",
"bootstrap": "^5.3.2",
"esbuild-plugin-clean": "^1.0.1",
"esbuild-plugin-handlebars": "^1.0.2",
"esbuild-sass-plugin": "^2.16.0",
"handlebars": "^4.7.7",
"postcss": "^8.4.31",
"typescript": "^5.1.6"
}
}

BIN
data/src/css/oswald.woff Normal file

Binary file not shown.

BIN
data/src/css/oswald.woff2 Normal file

Binary file not shown.

121
data/src/css/style.scss Normal file
View file

@ -0,0 +1,121 @@
// @import "../node_modules/bootstrap/scss/bootstrap";
@import "../node_modules/bootstrap/scss/functions";
@import "../node_modules/bootstrap/scss/variables";
@import "../node_modules/bootstrap/scss/variables-dark";
$form-range-track-bg: #fff;
@import "../node_modules/bootstrap/scss/mixins";
@import "../node_modules/bootstrap/scss/maps";
@import "../node_modules/bootstrap/scss/utilities";
@import "../node_modules/bootstrap/scss/root";
@import "../node_modules/bootstrap/scss/reboot";
@import "../node_modules/bootstrap/scss/type";
@import "../node_modules/bootstrap/scss/containers";
@import "../node_modules/bootstrap/scss/grid";
@import "../node_modules/bootstrap/scss/forms";
@import "../node_modules/bootstrap/scss/buttons";
@import "../node_modules/bootstrap/scss/navbar";
@import "../node_modules/bootstrap/scss/nav";
@import "../node_modules/bootstrap/scss/card";
@import "../node_modules/bootstrap/scss/progress";
@import "../node_modules/bootstrap/scss/helpers";
@import "../node_modules/bootstrap/scss/utilities/api";
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Oswald';
font-style: normal;
font-weight: 400;
src: url('oswald.woff2') format('woff2'),
/* Chrome 36+, Opera 23+, Firefox 39+ */
url('oswald.woff') format('woff');
/* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
nav {
margin-bottom: 15px;
}
.splitText div:first-child::after {
display: block;
content: '';
margin-top: 0px;
border-bottom: 2px solid;
margin-bottom: 3px;
}
#btcclock-wrapper {
margin: 0 auto;
}
.btclock {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
align-content: stretch;
font-family: 'Oswald', sans-serif;
}
.btclock>div {
padding: 5px;
}
.fg-ffff .btclock>div {
color: #fff;
border-color: #fff;
}
.bg-ffff .btclock>div {
background: #fff;
}
.fg-f800 .btclock>div {
color: #f00;
border-color: #f00;
}
.bg-f800 .btclock>div {
background: #f00;
}
.fg-0 .btclock>div {
color: #000;
border-color: #000;
}
.bg-0 .btclock>div {
background: #000;
}
.splitText {
font-size: 2.2rem;
padding-top: 5px;
padding-bottom: 5px;
text-align: center;
}
.digit {
font-size: 5rem;
padding-left: 10px;
padding-right: 10px;
}
.digit-blank {
content: "abc";
}
#customText {
text-transform: uppercase;
}
#toggleTimerArea {
cursor: pointer;
}

0
data/src/font/oswald.css Normal file
View file

235
data/src/index.html Normal file
View file

@ -0,0 +1,235 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<link href="/css/style.css" rel="stylesheet">
<title>&#8383;TClock</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.7/handlebars.min.js"
integrity="sha512-RNLkV3d+aLtfcpEyFG8jRbnWHxUqVZozacROI4J2F1sTaDqo1dPQYs01OMi1t1w9Y2FdbSCDSQ2ZVdAC8bzgAg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<nav class="navbar navbar-light bg-light">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">&#8383;TClock</span>
</div>
</nav>
<script id="entry-template" type="text/x-handlebars-template">
<div class="entry">
<h1>Status</h1>
<div class="body">
<div class="btn-group" role="group" aria-label="Basic radio toggle button group">
{{#each screens }}
<input type="radio" class="btn-check" name="btnradio" id="btnradio{{ @index }}" autocomplete="off" {{#ifEquals @index ../currentScreen }} checked {{/ifEquals}} onclick="changeScreen({{ @index }})">
<label class="btn btn-outline-primary" for="btnradio{{ @index }}">{{ this }}</label>
{{/each}}
</div>
<p>Rendered:</p>
{{#if rendered }}
<div class="btcclock-wrapper" id="btcclock-wrapper">
<div class="btclock">
{{#each data }}
{{{splitText this}}}
{{/each}}
</div></div>
{{/if}}
{{#if ledStatus }}
<p>LED status:</p>
{{#each ledStatus }}
<div style="background: #{{ this }}">&nbsp;</div>
{{/each}}
{{/if}}
<div>
<p>Screen cycle:
<span onclick="toggleTimer({{ timerRunning }})" id="toggleTimerArea">
{{#if timerRunning}}
&#9205;
{{else}}
&#9208;
{{/if}}
</span>
</p>
</div>
<hr>
<div>
<div class="progress" role="progressbar" aria-label="Memory usage" aria-valuenow="{{ memUsage }}" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar progress-bar-striped" style="width: {{ memUsage }}%">{{ memUsage }}%</div>
</div>
<div class="d-flex justify-content-between">
<div>Memory usage</div>
<div>{{ memFree }} / {{ memTotal }} KiB</div>
</div>
</div>
<hr>
<div>
<p>Uptime: {{#if uptime.h }}{{ uptime.h }}h {{/if}}{{ uptime.m }}m {{ uptime.s }}s</p>
<p>
Price connection:
<span>
{{#if connectionStatus.price}}
&#9989;
{{else}}
&#10060;
{{/if}}
</span>
-
Mempool.space connection:
<span>
{{#if connectionStatus.price}}
&#9989;
{{else}}
&#10060;
{{/if}}
</span>
</p>
</div>
</div>
</div>
</script>
<div class="container-fluid">
<div class="row">
<div class="col">
<div class="h-100 p-3 border bg-light">
<h1>Custom text</h1>
<form name="customText" id="customTextForm">
<div class="row">
<label for="customText" class="col-sm-4 col-form-label">Text</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="customText" name="customText" maxlength="7">
</div>
</div>
<footer>
<button type="submit" class="btn btn-primary">Show Text</button>
<button type="button" class="btn btn-secondary" id="restartBtn">Restart</button>
</footer>
</form>
<hr>
<h2>LEDs</h2>
<form id="ledsForm" name="ledsForm">
<div class="row">
<label for="ledColorPicker" class="col-sm-6 col-form-label">LEDs color</label>
<div class="col-sm-6">
<input type="color" id="ledColorPicker" name="pickedColor" value="#ff8800">
</div>
</div>
<button type="button" class="btn btn-secondary" id="turnOffLedsBtn">Turn off</button>
<button type="submit" class="btn btn-primary">Set color</button>
</form>
</div>
</div>
<div class="col">
<div id="output" class="p-3 border bg-light"></div>
</div>
<div class="col">
<div class="h-100 p-3 border bg-light">
<h1>Settings</h1>
<form method="post" action="/api/settings" name="settings" id="settingsForm">
<div class="row">
<label for="fgColor" class="col-sm-6 col-form-label">Text color</label>
<div class="col-sm-6">
<select class="form-select" id="fgColor" name="fgColor">
<option value="0xF800">Red</option>
<option value="0xFFFF">White</option>
<option value="0x0">Black</option>
</select>
</div>
</div>
<div class="row">
<label for="bgColor" class="col-sm-6 col-form-label">Background color</label>
<div class="col-sm-6">
<select class="form-select" id="bgColor" name="bgColor">
<option value="0xF800">Red</option>
<option value="0xFFFF">White</option>
<option value="0x0">Black</option>
</select>
</div>
</div>
<div class="row">
<label for="timePerScreen" class="col-sm-6 col-form-label">Time per screen</label>
<div class="col-sm-6">
<div class="input-group mb-3">
<input type="text" name="timePerScreen" id="timePerScreen" class="form-control">
<span class="input-group-text">minutes</span>
</div>
</div>
</div>
<div class="row">
<label for="fullRefreshMin" class="col-sm-6 col-form-label">Full refresh every</label>
<div class="col-sm-6">
<div class="input-group mb-3">
<input type="text" name="fullRefreshMin" id="fullRefreshMin" class="form-control">
<span class="input-group-text">minutes</span>
</div>
</div>
</div>
<div class="row">
<label for="tzOffset" class="col-sm-6 col-form-label">Timezone offset</label>
<div class="col-sm-6">
<div class="input-group mb-3">
<input type="number" name="tzOffset" id="tzOffset" class="form-control">
<span class="input-group-text">min</span>
<button class="btn btn-outline-secondary" type="button" id="getTzOffsetBtn">Auto</button>
</div>
<div class="form-text">A restart is required to apply TZ offset.</div>
</div>
</div>
<div class="row">
<div class=" col-sm-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="ledFlashOnUpdate" name="ledFlashOnUpd" value="1">
<label class="form-check-label" for="ledFlashOnUpdate">LED flash on update</label>
</div>
</div>
</div>
<div class="row">
<label class="col-sm-6 col-form-label" for="ledBrightness">LED brightness</label>
<div class="col-sm-6">
<input type="range" class="form-range" id="ledBrightness" name="ledBrightness" value="128" min="0"
max="255">
</div>
</div>
<div class="row">
<label for="mempoolInstance" class="col-sm-6 col-form-label">Mempool Instance</label>
<div class="col-sm-6">
<input type="text" name="mempoolInstance" id="mempoolInstance" class="form-control">
</div>
</div>
<script id="screens-template" type="text/x-handlebars-template">
{{#each screens }}
<div class="row">
<div class="col-sm-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="screen{{id}}" name="screen[{{id}}]" value="1" {{#if enabled}}checked{{/if}}>
<label class="form-check-label" for="screen{{id}}">{{name}}</label>
</div>
</div>
{{/each}}
</script>
<h3>Screens</h3>
<div id="outputScreens"></div>
<button type="submit" class="btn btn-secondary">Reset</button>
<button type="submit" class="btn btn-primary" id="saveSettingsBtn">Save</button>
</form>
</div>
</div>
</div>
</div>
<footer>
<small>
<span id="gitRev"></span>
<span id="lastBuildTime"></span>
</small>
</footer>
<script src="/js/script.js"></script>
</body>
</html>

17
data/src/js/helpers.js Normal file
View file

@ -0,0 +1,17 @@
//import "handlebars/dist/handlebars.js";
Handlebars.registerHelper('splitText', function (aString) {
if (aString.includes("/")) {
var c = aString.split("/").map((el) => { return "<div class=\"flex-items\">" + el + "</div>"; }).join('');
return "<div class=\"splitText\">" + c + "</div>";
}
if (aString.length == 0 || aString === " ") {
aString = "&nbsp;&nbsp;";
}
//return aString;
return "<div class=\"digit\">" + aString + "</div>";
});
Handlebars.registerHelper('ifEquals', function(arg1, arg2, options) {
return (arg1 == arg2) ? options.fn(this) : options.inverse(this);
});

187
data/src/js/script.ts Normal file
View file

@ -0,0 +1,187 @@
import './helpers.js';
var screens = ["Block Height", "Moscow Time", "Ticker", "Time", "Halving countdown"];
toTime = (secs) => {
var hours = Math.floor(secs / (60 * 60));
var divisor_for_minutes = secs % (60 * 60);
var minutes = Math.floor(divisor_for_minutes / 60);
var divisor_for_seconds = divisor_for_minutes % 60;
var seconds = Math.ceil(divisor_for_seconds);
var obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
}
getBcStatus = () => {
fetch('/api/status', {
method: 'get'
})
.then(response => response.json())
.then(jsonData => {
var source = document.getElementById("entry-template").innerHTML;
var template = Handlebars.compile(source);
var context = {
timerRunning: jsonData.timerRunning,
memUsage: Math.round(jsonData.espFreeHeap / jsonData.espHeapSize * 100),
memFree: Math.round(jsonData.espFreeHeap / 1024),
memTotal: Math.round(jsonData.espHeapSize / 1024),
uptime: toTime(jsonData.espUptime),
currentScreen: jsonData.currentScreen,
rendered: jsonData.rendered,
data: jsonData.data,
screens: screens,
ledStatus: jsonData.ledStatus ? jsonData.ledStatus.map((t) => (t).toString(16)) : [],
connectionStatus: jsonData.connectionStatus
};
document.getElementById('output').innerHTML = template(context);
})
.catch(err => {
//error block
});
}
interval = setInterval(getBcStatus, 2500);
getBcStatus();
fetch('/api/settings', {
method: 'get'
})
.then(response => response.json())
.then(jsonData => {
var fgColor = ("0x" + jsonData.fgColor.toString(16).toUpperCase());
if (jsonData.epdColors == 2) {
document.getElementById('fgColor').querySelector('[value="0xF800"]').remove();
document.getElementById('bgColor').querySelector('[value="0xF800"]').remove();
}
document.getElementById('customText').setAttribute('maxlength', jsonData.numScreens);
document.getElementById('output').classList.add("fg-" + jsonData.fgColor.toString(16));
document.getElementById('output').classList.add("bg-" + jsonData.bgColor.toString(16));
document.getElementById('fgColor').value = fgColor;
document.getElementById('bgColor').value = "0x" + jsonData.bgColor.toString(16).toUpperCase();
if (jsonData.ledFlashOnUpdate)
document.getElementById('ledFlashOnUpdate').checked = true;
if (jsonData.useBitcoinNode)
document.getElementById('useBitcoinNode').checked = true;
let nodeFields = ["rpcHost", "rpcPort", "rpcUser", "tzOffset"];
for (let n of nodeFields) {
document.getElementById(n).value = jsonData[n];
}
document.getElementById('timePerScreen').value = jsonData.timerSeconds / 60;
document.getElementById('ledBrightness').value = jsonData.ledBrightness;
document.getElementById('fullRefreshMin').value = jsonData.fullRefreshMin;
document.getElementById('wpTimeout').value = jsonData.wpTimeout;
document.getElementById('mempoolInstance').value = jsonData.mempoolInstance;
if (jsonData.gitRev)
document.getElementById('gitRev').innerHTML = "Version: " + jsonData.gitRev;
if (jsonData.lastBuildTime)
document.getElementById('lastBuildTime').innerHTML = " / " + new Date((jsonData.lastBuildTime * 1000)).toLocaleString();
var source = document.getElementById("screens-template").innerHTML;
var template = Handlebars.compile(source);
var context = { screens: jsonData.screens };
document.getElementById('outputScreens').innerHTML = template(context);
})
.catch(err => {
console.log('error', err);
});
var settingsForm = document.querySelector('#settingsForm');
settingsForm.onsubmit = (event) => {
var formData = new FormData(settingsForm);
fetch("/api/settings",
{
body: formData,
method: "post"
}).then(() => {
console.log('Submitted');
document.getElementById('saveSettingsBtn')?.classList.add('btn-success');
});
return false;
}
document.getElementById('restartBtn').onclick = (event) => {
fetch('/api/restart');
return false;
}
var ledsForm = document.querySelector('#ledsForm');
ledsForm.onsubmit = (event) => {
var formData = new FormData(ledsForm);
fetch('/api/lights/' + encodeURIComponent(formData.get('pickedColor').substring(1)), {
method: 'get'
})
return false;
}
turnOffLedsBtn.onclick = (event) => {
fetch('/api/lights/off', {
method: 'get'
})
return false;
}
let tzOffsetBtn = document.getElementById('getTzOffsetBtn');
if (tzOffsetBtn)
tzOffsetBtn.onclick = (event) => {
document.getElementById("tzOffset").value = new Date(new Date().getFullYear(), 0, 1).getTimezoneOffset() * -1;
return false;
};
var textForm = document.querySelector('#customTextForm');
textForm.onsubmit = (event) => {
var formData = new FormData(textForm);
fetch('/api/show/text/' + encodeURIComponent(formData.get('customText')), {
method: 'get'
})
.then(response => response.json())
.catch(err => {
//error block
});
return false;
}
changeScreen = (id) => {
fetch('/api/show/screen/' + encodeURIComponent(id), {
method: 'get'
})
.then(response => response.json())
.catch(err => {
//error block
});
}
toggleTimer = (currentStatus) => {
if (currentStatus) {
fetch('/api/action/pause');
} else {
fetch('/api/action/timer_restart');
}
}

48
data/src/wifi.html Normal file
View file

@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<link href="/css/style.css" rel="stylesheet">
<title>&#8383;TClock WiFi Settings</title>
</head>
<body>
<nav class="navbar navbar-light bg-light">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">&#8383;TClock WiFi Settings</span>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col">
<div class="h-100 p-3 border bg-light">
<h1>WiFi Settings</h1>
<form name="customText" id="customTextForm" method="post" action="/setup/wifi">
<div class="row">
<label for="ssid" class="col-sm-4 col-form-label">SSID</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="ssid" name="ssid" required>
</div>
</div>
<div class="row">
<label for="password" class="col-sm-4 col-form-label">Password</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="password" name="password" required>
</div>
</div>
<footer>
<button type="submit" class="btn btn-primary">Save and connect</button>
<p><small>The BTClock will restart and connect to your network. If it doesn't, reset to factory settings by holding the red button while booting to retry.</small></p>
</footer>
</form>
</div>
</div>
</div>
</div>
</body>
</html>

6
data/tsconfig.json Normal file
View file

@ -0,0 +1,6 @@
{
"files": ["src/js/**.ts"],
"compilerOptions": {
"noImplicitAny": true,
}
}

2728
data/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,4 +4,4 @@ Import("env")
def before_buildfs(source, target, env): def before_buildfs(source, target, env):
env.Execute("cd data && yarn && yarn build") env.Execute("cd data && yarn && yarn build")
env.AddPreAction("$BUILD_DIR/spiffs.bin", before_buildfs) env.AddPreAction("$BUILD_DIR/littlefs.bin", before_buildfs)

View file

@ -1,8 +1,7 @@
#include "block_notify.hpp" #include "block_notify.hpp"
const char *wsServer = "wss://mempool.space/api/v1/ws"; char *wsServer;
// WebsocketsClient client; esp_websocket_client_handle_t blockNotifyClient = NULL;
esp_websocket_client_handle_t client;
unsigned long int currentBlockHeight; unsigned long int currentBlockHeight;
void setupBlockNotify() void setupBlockNotify()
@ -36,15 +35,17 @@ void setupBlockNotify()
xTaskNotifyGive(blockUpdateTaskHandle); xTaskNotifyGive(blockUpdateTaskHandle);
} }
// std::strcpy(wsServer, String("wss://" + mempoolInstance + "/api/v1/ws").c_str());
esp_websocket_client_config_t config = { esp_websocket_client_config_t config = {
.uri = "wss://mempool.bitcoin.nl/api/v1/ws", .uri = "wss://mempool.bitcoin.nl/api/v1/ws",
}; };
Serial.printf("Connecting to %s\r\n", config.uri); Serial.printf("Connecting to %s\r\n", config.uri);
client = esp_websocket_client_init(&config); blockNotifyClient = esp_websocket_client_init(&config);
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, onWebsocketEvent, client); esp_websocket_register_events(blockNotifyClient, WEBSOCKET_EVENT_ANY, onWebsocketEvent, blockNotifyClient);
esp_websocket_client_start(client); esp_websocket_client_start(blockNotifyClient);
} }
void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
@ -58,7 +59,7 @@ void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_i
Serial.println("Connected to Mempool.space WebSocket"); Serial.println("Connected to Mempool.space WebSocket");
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(blockNotifyClient, sub.c_str(), sub.length(), portMAX_DELAY) == -1)
{ {
Serial.println("Mempool.space WS Block Subscribe Error"); Serial.println("Mempool.space WS Block Subscribe Error");
} }
@ -92,8 +93,10 @@ void onWebsocketMessage(esp_websocket_event_data_t *event_data)
Serial.print("New block found: "); Serial.print("New block found: ");
Serial.println(block["height"].as<long>()); Serial.println(block["height"].as<long>());
if (blockUpdateTaskHandle != nullptr) if (blockUpdateTaskHandle != nullptr) {
xTaskNotifyGive(blockUpdateTaskHandle); xTaskNotifyGive(blockUpdateTaskHandle);
queueLedEffect(LED_FLASH_BLOCK_NOTIFY);
}
} }
doc.clear(); doc.clear();
@ -102,4 +105,10 @@ void onWebsocketMessage(esp_websocket_event_data_t *event_data)
unsigned long getBlockHeight() unsigned long getBlockHeight()
{ {
return currentBlockHeight; return currentBlockHeight;
}
bool isBlockNotifyConnected() {
if (blockNotifyClient == NULL)
return false;
return esp_websocket_client_is_connected(blockNotifyClient);
} }

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstring>
#include <string> #include <string>
#include <Arduino.h> #include <Arduino.h>
#include <HTTPClient.h> #include <HTTPClient.h>
@ -8,6 +9,7 @@
#include "esp_websocket_client.h" #include "esp_websocket_client.h"
#include "screen_handler.hpp" #include "screen_handler.hpp"
#include "led_handler.hpp"
//using namespace websockets; //using namespace websockets;
@ -16,4 +18,5 @@ void setupBlockNotify();
void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
void onWebsocketMessage(esp_websocket_event_data_t* event_data); void onWebsocketMessage(esp_websocket_event_data_t* event_data);
unsigned long getBlockHeight(); unsigned long getBlockHeight();
bool isBlockNotifyConnected();

View file

@ -18,22 +18,21 @@ void buttonTask(void *parameter)
{ {
uint pin = mcp.getLastInterruptPin(); uint pin = mcp.getLastInterruptPin();
Serial.printf("Button pressed: %d", pin); switch (pin)
// switch (pin) {
// { case 3:
// case 3: toggleTimerActive();
// toggleScreenTimer(); break;
// break; case 2:
// case 2: nextScreen();
// nextScreen(); break;
// break; case 1:
// case 1: previousScreen();
// previousScreen(); break;
// break; case 0:
// case 0: showSystemStatusScreen();
// showNetworkSettings(); break;
// break; }
// }
} }
mcp.clearInterrupts(); mcp.clearInterrupts();
// Very ugly, but for some reason this is necessary // Very ugly, but for some reason this is necessary

View file

@ -4,6 +4,7 @@
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include "shared.hpp" #include "shared.hpp"
#include "screen_handler.hpp"
extern TaskHandle_t buttonTaskHandle; extern TaskHandle_t buttonTaskHandle;

View file

@ -1,27 +1,22 @@
#include "config.hpp" #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 #define MAX_ATTEMPTS_WIFI_CONNECTION 20
Preferences preferences; Preferences preferences;
Adafruit_MCP23X17 mcp; Adafruit_MCP23X17 mcp;
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); std::map<int, std::string> screenNameMap;
void setup() void setup()
{ {
setupHardware(); setupHardware();
if (mcp.digitalRead(3) == LOW) { if (mcp.digitalRead(3) == LOW)
{
WiFi.eraseAP(); WiFi.eraseAP();
blinkDelay(100, 3); blinkDelay(100, 3);
} }
setupDisplays(); setupDisplays();
tryImprovSetup(); tryImprovSetup();
@ -47,9 +42,9 @@ void tryImprovSetup()
// blinkDelay(100, 3); // blinkDelay(100, 3);
// } // }
// else // else
// { {
// WiFi.begin(); WiFi.begin();
// } }
uint8_t x_buffer[16]; uint8_t x_buffer[16];
uint8_t x_position = 0; uint8_t x_position = 0;
@ -69,7 +64,8 @@ void tryImprovSetup()
x_position = 0; x_position = 0;
} }
} }
// vTaskDelay(1);
vTaskDelay(1 / portTICK_PERIOD_MS);
} }
} }
@ -91,6 +87,15 @@ void setupTime()
void setupPreferences() void setupPreferences()
{ {
preferences.begin("btclock", false); preferences.begin("btclock", false);
setFgColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
setBgColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
screenNameMap = {{SCREEN_BLOCK_HEIGHT, "Block Height"},
{SCREEN_MSCW_TIME, "Sats per dollar"},
{SCREEN_BTC_TICKER, "Ticker"},
{SCREEN_TIME, "Time"},
{SCREEN_HALVING_COUNTDOWN, "Halving countdown"}};
} }
void setupWebsocketClients() void setupWebsocketClients()
@ -107,18 +112,16 @@ void setupTimers()
void finishSetup() void finishSetup()
{ {
pixels.clear(); clearLeds();
pixels.show(); }
std::map<int, std::string> getScreenNameMap() {
return screenNameMap;
} }
void setupHardware() void setupHardware()
{ {
pixels.begin(); setupLeds();
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()) if (psramInit())
{ {
@ -129,7 +132,6 @@ void setupHardware()
Serial.println(F("PSRAM not available")); Serial.println(F("PSRAM not available"));
} }
Wire.begin(35, 36, 400000); Wire.begin(35, 36, 400000);
if (!mcp.begin_I2C(0x20)) if (!mcp.begin_I2C(0x20))
@ -153,7 +155,6 @@ void setupHardware()
{ {
mcp.pinMode(i, OUTPUT); mcp.pinMode(i, OUTPUT);
} }
} }
} }
@ -194,41 +195,20 @@ bool improv_connectWifi(std::string ssid, std::string password)
return true; 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) void onImprovErrorCallback(improv::Error err)
{ {
pixels.setPixelColor(0, pixels.Color(255, 0, 0)); blinkDelayColor(100, 1, 255,0,0);
pixels.setPixelColor(1, pixels.Color(255, 0, 0)); // pixels.setPixelColor(0, pixels.Color(255, 0, 0));
pixels.setPixelColor(2, pixels.Color(255, 0, 0)); // pixels.setPixelColor(1, pixels.Color(255, 0, 0));
pixels.setPixelColor(3, pixels.Color(255, 0, 0)); // pixels.setPixelColor(2, pixels.Color(255, 0, 0));
pixels.show(); // pixels.setPixelColor(3, pixels.Color(255, 0, 0));
vTaskDelay(pdMS_TO_TICKS(100)); // pixels.show();
// vTaskDelay(pdMS_TO_TICKS(100));
pixels.clear(); // pixels.clear();
pixels.show(); // pixels.show();
vTaskDelay(pdMS_TO_TICKS(100)); // vTaskDelay(pdMS_TO_TICKS(100));
} }
std::vector<std::string> getLocalUrl() std::vector<std::string> getLocalUrl()
@ -272,7 +252,7 @@ bool onImprovCommandCallback(improv::ImprovCommand cmd)
if (improv_connectWifi(cmd.ssid, cmd.password)) if (improv_connectWifi(cmd.ssid, cmd.password))
{ {
blinkDelay(100, 3); blinkDelay(100, 3);
// std::array<String, NUM_SCREENS> epdContent = {"S", "U", "C", "C", "E", "S", "S"}; // std::array<String, NUM_SCREENS> epdContent = {"S", "U", "C", "C", "E", "S", "S"};
// setEpdContent(epdContent); // setEpdContent(epdContent);

View file

@ -2,7 +2,6 @@
#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>
@ -16,6 +15,7 @@
#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" #include "lib/button_handler.hpp"
#include "lib/led_handler.hpp"
#define NTP_SERVER "pool.ntp.org" #define NTP_SERVER "pool.ntp.org"
#define DEFAULT_MEMPOOL_INSTANCE "mempool.space" #define DEFAULT_MEMPOOL_INSTANCE "mempool.space"
@ -23,6 +23,9 @@
#define USER_AGENT "BTClock/2.0" #define USER_AGENT "BTClock/2.0"
#define MCP_DEV_ADDR 0x20 #define MCP_DEV_ADDR 0x20
#define DEFAULT_FG_COLOR GxEPD_WHITE
#define DEFAULT_BG_COLOR GxEPD_BLACK
#define BITCOIND_HOST "" #define BITCOIND_HOST ""
#define BITCOIND_PORT 8332 #define BITCOIND_PORT 8332
#define BITCOIND_RPC_USER "" #define BITCOIND_RPC_USER ""
@ -36,6 +39,7 @@ void setupHardware();
void tryImprovSetup(); void tryImprovSetup();
void setupTimers(); void setupTimers();
void finishSetup(); void finishSetup();
std::map<int, std::string> getScreenNameMap();
std::vector<std::string> getLocalUrl(); std::vector<std::string> getLocalUrl();
bool improv_connectWifi(std::string ssid, std::string password); bool improv_connectWifi(std::string ssid, std::string password);
@ -45,4 +49,3 @@ void onImprovErrorCallback(improv::Error err);
void improv_set_state(improv::State state); void improv_set_state(improv::State state);
void improv_send_response(std::vector<uint8_t> &response); void improv_send_response(std::vector<uint8_t> &response);
void improv_set_error(improv::Error error); void improv_set_error(improv::Error error);
void blinkDelay(int d, int times);

View file

@ -76,7 +76,7 @@ void setupDisplays()
int *taskParam = new int; int *taskParam = new int;
*taskParam = i; *taskParam = i;
xTaskCreate(updateDisplay, "EpdUpd" + char(i), 4096, taskParam, 1, &tasks[i]); // create task xTaskCreate(updateDisplay, "EpdUpd" + char(i), 4096, taskParam, tskIDLE_PRIORITY, &tasks[i]); // create task
} }
epdContent = {"B", epdContent = {"B",
@ -91,7 +91,7 @@ void setupDisplays()
xTaskNotifyGive(tasks[i]); xTaskNotifyGive(tasks[i]);
} }
xTaskCreate(taskEpd, "epd_task", 2048, NULL, 1, NULL); xTaskCreate(taskEpd, "epd_task", 2048, NULL, tskIDLE_PRIORITY, NULL);
} }
void taskEpd(void *pvParameters) void taskEpd(void *pvParameters)

View file

@ -1,18 +1,216 @@
#include "led_handler.hpp" #include "led_handler.hpp"
TaskHandle_t ledTaskHandle = NULL; TaskHandle_t ledTaskHandle = NULL;
QueueHandle_t ledTaskQueue = NULL;
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
const TickType_t debounceDelay = pdMS_TO_TICKS(50); const TickType_t debounceDelay = pdMS_TO_TICKS(50);
uint32_t notificationValue;
unsigned long ledTaskParams;
void ledTask(void *parameter) void ledTask(void *parameter)
{ {
while (1) while (1)
{ {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if (ledTaskQueue != NULL)
{
if (xQueueReceive(ledTaskQueue, &ledTaskParams, portMAX_DELAY) == pdPASS)
{
uint32_t oldLights[NEOPIXEL_COUNT];
// get current state
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
oldLights[i] = pixels.getPixelColor(i);
}
switch (ledTaskParams)
{
case LED_FLASH_ERROR:
blinkDelayColor(250, 3, 255, 0, 0);
break;
case LED_FLASH_SUCCESS:
blinkDelayColor(250, 3, 0, 255, 0);
break;
case LED_FLASH_UPDATE:
break;
case LED_FLASH_BLOCK_NOTIFY:
blinkDelayTwoColor(250, 3, pixels.Color(224, 67, 0), pixels.Color(8, 2, 0));
break;
case LED_EFFECT_PAUSE_TIMER:
for (int i = NEOPIXEL_COUNT; i >= 0; i--)
{
for (int j = NEOPIXEL_COUNT; j >= 0; j--)
{
uint32_t c = pixels.Color(0, 0, 0);
if (i == j)
c = pixels.Color(0, 255, 0);
pixels.setPixelColor(j, c);
}
pixels.show();
delay(100);
}
delay(900);
pixels.clear();
pixels.show();
break;
case LED_EFFECT_START_TIMER:
pixels.clear();
pixels.setPixelColor(NEOPIXEL_COUNT, pixels.Color(0, 255, 0));
pixels.show();
delay(900);
for (int i = NEOPIXEL_COUNT; i--; i > 0)
{
for (int j = NEOPIXEL_COUNT; j--; j > 0)
{
uint32_t c = pixels.Color(0, 0, 0);
if (i == j)
c = pixels.Color(0, 255, 0);
pixels.setPixelColor(j, c);
}
pixels.show();
delay(100);
}
pixels.clear();
pixels.show();
break;
}
// revert to previous state
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, oldLights[i]);
}
pixels.show();
}
}
} }
} }
void setupLeds()
{
pixels.begin();
pixels.setBrightness(preferences.getUInt("ledBrightness", 128));
pixels.setPixelColor(0, pixels.Color(255, 0, 0));
pixels.setPixelColor(1, pixels.Color(0, 255, 0));
pixels.setPixelColor(2, pixels.Color(0, 0, 255));
pixels.setPixelColor(3, pixels.Color(255, 255, 255));
pixels.show();
setupLedTask();
}
void setupLedTask() void setupLedTask()
{ {
xTaskCreate(ledTask, "LedTask", 4096, NULL, tskIDLE_PRIORITY, &ledTaskHandle); // Create the FreeRTOS task ledTaskQueue = xQueueCreate(10, sizeof(unsigned long));
xTaskCreate(ledTask, "LedTask", 4096, NULL, tskIDLE_PRIORITY, &ledTaskHandle);
} }
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 blinkDelayColor(int d, int times, uint r, uint g, uint b)
{
for (int j = 0; j < times; j++)
{
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, pixels.Color(r, g, b));
}
pixels.show();
vTaskDelay(pdMS_TO_TICKS(d));
pixels.clear();
pixels.show();
vTaskDelay(pdMS_TO_TICKS(d));
}
pixels.clear();
pixels.show();
}
void blinkDelayTwoColor(int d, int times, uint32_t c1, uint32_t c2)
{
for (int j = 0; j < times; j++)
{
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, c1);
}
pixels.show();
vTaskDelay(pdMS_TO_TICKS(d));
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, c2);
}
pixels.show();
vTaskDelay(pdMS_TO_TICKS(d));
}
pixels.clear();
pixels.show();
}
void clearLeds()
{
pixels.clear();
pixels.show();
}
void setLights(int r, int g, int b)
{
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, pixels.Color(r, g, b));
}
pixels.show();
}
QueueHandle_t getLedTaskQueue()
{
return ledTaskQueue;
}
bool queueLedEffect(uint effect)
{
if (ledTaskQueue == NULL)
{
return false;
}
unsigned long flashType = effect;
xQueueSend(ledTaskQueue, &flashType, portMAX_DELAY);
}

View file

@ -6,7 +6,33 @@
#include <Adafruit_NeoPixel.h> #include <Adafruit_NeoPixel.h>
#include "shared.hpp" #include "shared.hpp"
#ifndef NEOPIXEL_PIN
#define NEOPIXEL_PIN 34
#endif
#ifndef NEOPIXEL_COUNT
#define NEOPIXEL_COUNT 4
#endif
typedef struct {
int flashType;
} LedTaskParameters;
const int LED_FLASH_ERROR = 0;
const int LED_FLASH_SUCCESS = 1;
const int LED_FLASH_UPDATE = 2;
const int LED_FLASH_BLOCK_NOTIFY = 3;
const int LED_EFFECT_START_TIMER = 4;
const int LED_EFFECT_PAUSE_TIMER = 5;
extern TaskHandle_t ledTaskHandle; extern TaskHandle_t ledTaskHandle;
void ledTask(void *pvParameters); void ledTask(void *pvParameters);
void setupLeds();
void setupLedTask(); void setupLedTask();
void blinkDelay(int d, int times);
void blinkDelayColor(int d, int times, uint r, uint g, uint b);
void blinkDelayTwoColor(int d, int times, uint32_t c1, uint32_t c2);
void clearLeds();
QueueHandle_t getLedTaskQueue();
bool queueLedEffect(uint effect);
void setLights(int r, int g, int b);

View file

@ -2,7 +2,7 @@
const char *wsServerPrice = "wss://ws.coincap.io/prices?assets=bitcoin"; const char *wsServerPrice = "wss://ws.coincap.io/prices?assets=bitcoin";
// WebsocketsClient client; // WebsocketsClient client;
esp_websocket_client_handle_t clientPrice; esp_websocket_client_handle_t clientPrice = NULL;
unsigned long int currentPrice; unsigned long int currentPrice;
void setupPriceNotify() void setupPriceNotify()
@ -46,7 +46,6 @@ void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data)
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);
const unsigned long oldPrice = currentPrice; const unsigned long oldPrice = currentPrice;
currentPrice = doc["bitcoin"].as<long>(); currentPrice = doc["bitcoin"].as<long>();
@ -61,4 +60,10 @@ void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data)
unsigned long getPrice() { unsigned long getPrice() {
return currentPrice; return currentPrice;
}
bool isPriceNotifyConnected() {
if (clientPrice == NULL)
return false;
return esp_websocket_client_is_connected(clientPrice);
} }

View file

@ -14,4 +14,5 @@ void setupPriceNotify();
void onWebsocketPriceEvent(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); void onWebsocketPriceEvent(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data); void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data);
unsigned long getPrice(); unsigned long getPrice();
bool isPriceNotifyConnected();

View file

@ -61,7 +61,16 @@ void taskScreenRotate(void *pvParameters)
{ {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
setCurrentScreen((currentScreen+1) % 5); int nextScreen = (currentScreen+ 1) % 5;
String key = "screen" + String(nextScreen) + "Visible";
while (!preferences.getBool(key.c_str(), true))
{
nextScreen = (nextScreen + 1) % 5;
key = "screen" + String(nextScreen) + "Visible";
}
setCurrentScreen(nextScreen);
} }
} }
@ -143,20 +152,6 @@ void taskTimeUpdate(void *pvParameters)
} }
} }
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) void IRAM_ATTR minuteTimerISR(void *arg)
{ {
BaseType_t xHigherPriorityTaskWoken = pdFALSE; BaseType_t xHigherPriorityTaskWoken = pdFALSE;
@ -217,11 +212,8 @@ void setupScreenRotateTimer(void *pvParameters)
.name = "screen_rotate_timer"}; .name = "screen_rotate_timer"};
esp_timer_create(&screenRotateTimerConfig, &screenRotateTimer); esp_timer_create(&screenRotateTimerConfig, &screenRotateTimer);
esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond); esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond);
Serial.println("Set up Screen Rotate Timer");
vTaskDelete(NULL); vTaskDelete(NULL);
} }
@ -240,13 +232,19 @@ void setTimerActive(bool status)
if (status) if (status)
{ {
esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond); esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond);
queueLedEffect(LED_EFFECT_START_TIMER);
} }
else else
{ {
esp_timer_stop(screenRotateTimer); esp_timer_stop(screenRotateTimer);
queueLedEffect(LED_EFFECT_PAUSE_TIMER);
} }
} }
void toggleTimerActive() {
setTimerActive(!isTimerActive());
}
uint getCurrentScreen() uint getCurrentScreen()
{ {
return currentScreen; return currentScreen;
@ -275,4 +273,56 @@ void setCurrentScreen(uint newScreen)
xTaskNotifyGive(priceUpdateTaskHandle); xTaskNotifyGive(priceUpdateTaskHandle);
break; break;
} }
}
void nextScreen()
{
int newCurrentScreen = (getCurrentScreen() + 1) % SCREEN_COUNT;
String key = "screen" + String(newCurrentScreen) + "Visible";
while (!preferences.getBool(key.c_str(), true))
{
newCurrentScreen = (newCurrentScreen + 1) % SCREEN_COUNT;
key = "screen" + String(newCurrentScreen) + "Visible";
}
setCurrentScreen(newCurrentScreen);
}
void previousScreen()
{
int newCurrentScreen = modulo(getCurrentScreen() - 1, SCREEN_COUNT);
String key = "screen" + String(newCurrentScreen) + "Visible";
while (!preferences.getBool(key.c_str(), true))
{
newCurrentScreen = modulo(newCurrentScreen - 1, SCREEN_COUNT);
key = "screen" + String(newCurrentScreen) + "Visible";
}
setCurrentScreen(newCurrentScreen);
}
void showSystemStatusScreen()
{
std::array<String, NUM_SCREENS> sysStatusEpdContent = {"", "", "", "", "", "", ""};
String ipAddr = WiFi.localIP().toString();
String subNet = WiFi.subnetMask().toString();
sysStatusEpdContent[0] = "IP/Subnet";
int ipAddrPos = 0;
int subnetPos = 0;
for (int i = 0; i < 4; i++)
{
sysStatusEpdContent[1 + i] = ipAddr.substring(0, ipAddr.indexOf('.')) + "/" + subNet.substring(0, subNet.indexOf('.'));
ipAddrPos = ipAddr.indexOf('.') + 1;
subnetPos = subNet.indexOf('.') + 1;
ipAddr = ipAddr.substring(ipAddrPos);
subNet = subNet.substring(subnetPos);
}
sysStatusEpdContent[NUM_SCREENS-2] = "RAM/Status";
sysStatusEpdContent[NUM_SCREENS-1] = String((int)round(ESP.getFreeHeap()/1024)) + "/" + (int)round(ESP.getHeapSize()/1024);
setCurrentScreen(SCREEN_CUSTOM);
setEpdContent(sysStatusEpdContent);
} }

View file

@ -16,6 +16,10 @@ extern TaskHandle_t taskScreenRotateTaskHandle;
uint getCurrentScreen(); uint getCurrentScreen();
void setCurrentScreen(uint newScreen); void setCurrentScreen(uint newScreen);
void nextScreen();
void previousScreen();
void showSystemStatusScreen();
void setupTimeUpdateTimer(void *pvParameters); void setupTimeUpdateTimer(void *pvParameters);
void setupScreenRotateTimer(void *pvParameters); void setupScreenRotateTimer(void *pvParameters);
@ -31,7 +35,6 @@ void taskScreenRotate(void *pvParameters);
uint getTimerSeconds(); uint getTimerSeconds();
bool isTimerActive(); bool isTimerActive();
void setTimerActive(bool status); void setTimerActive(bool status);
void toggleTimerActive();
void setupTasks(); void setupTasks();
const char* int64_to_iso8601(int64_t timestamp);

View file

@ -3,6 +3,7 @@
#include <Adafruit_MCP23X17.h> #include <Adafruit_MCP23X17.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <Preferences.h> #include <Preferences.h>
#include "utils.hpp"
extern Adafruit_MCP23X17 mcp; extern Adafruit_MCP23X17 mcp;
extern Preferences preferences; extern Preferences preferences;
@ -15,6 +16,7 @@ const PROGMEM int SCREEN_HALVING_COUNTDOWN = 4;
const PROGMEM int SCREEN_COUNTDOWN = 98; const PROGMEM int SCREEN_COUNTDOWN = 98;
const PROGMEM int SCREEN_CUSTOM = 99; const PROGMEM int SCREEN_CUSTOM = 99;
const PROGMEM int screens[5] = { SCREEN_BLOCK_HEIGHT, SCREEN_MSCW_TIME, SCREEN_BTC_TICKER, SCREEN_TIME, SCREEN_HALVING_COUNTDOWN }; const PROGMEM int screens[5] = { SCREEN_BLOCK_HEIGHT, SCREEN_MSCW_TIME, SCREEN_BTC_TICKER, SCREEN_TIME, SCREEN_HALVING_COUNTDOWN };
const int SCREEN_COUNT = 5;
struct SpiRamAllocator { struct SpiRamAllocator {
void* allocate(size_t size) { void* allocate(size_t size) {

6
src/lib/utils.cpp Normal file
View file

@ -0,0 +1,6 @@
#include "utils.hpp"
int modulo(int x, int N)
{
return (x % N + N) % N;
}

1
src/lib/utils.hpp Normal file
View file

@ -0,0 +1 @@
int modulo(int x,int N);

View file

@ -28,6 +28,10 @@ void setupWebserver()
server.on("/api/show/screen", HTTP_GET, onApiShowScreen); server.on("/api/show/screen", HTTP_GET, onApiShowScreen);
server.on("/api/show/text", HTTP_GET, onApiShowText); server.on("/api/show/text", HTTP_GET, onApiShowText);
server.on("/api/lights/off", HTTP_GET, onApiLightsOff);
server.on("/api/lights/color", HTTP_GET, onApiLightsSetColor);
server.on("^\\/api\\/lights\\/([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", HTTP_GET, onApiLightsSetColor);
server.on("/api/restart", HTTP_GET, onApiRestart); 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/screen/{s}", "/api/show/screen?s={s}"));
@ -59,6 +63,10 @@ void onApiStatus(AsyncWebServerRequest *request)
root["espFreePsram"] = ESP.getFreePsram(); root["espFreePsram"] = ESP.getFreePsram();
root["espPsramSize"] = ESP.getPsramSize(); root["espPsramSize"] = ESP.getPsramSize();
JsonObject conStatus = root.createNestedObject("connectionStatus");
conStatus["price"] = isPriceNotifyConnected();
conStatus["blocks"] = isBlockNotifyConnected();
JsonArray data = root.createNestedArray("data"); JsonArray data = root.createNestedArray("data");
JsonArray rendered = root.createNestedArray("rendered"); JsonArray rendered = root.createNestedArray("rendered");
String epdContent[NUM_SCREENS]; String epdContent[NUM_SCREENS];
@ -170,14 +178,16 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
#endif #endif
JsonArray screens = root.createNestedArray("screens"); JsonArray screens = root.createNestedArray("screens");
// for (int i = 0; i < screenNameMap.size(); i++) std::map<int, std::string> screenNameMap = getScreenNameMap();
// {
// JsonObject o = screens.createNestedObject(); for (int i = 0; i < screenNameMap.size(); i++)
// String key = "screen" + String(i) + "Visible"; {
// o["id"] = i; JsonObject o = screens.createNestedObject();
// o["name"] = screenNameMap[i]; String key = "screen" + String(i) + "Visible";
// o["enabled"] = preferences.getBool(key.c_str(), true); o["id"] = i;
// } o["name"] = screenNameMap[i];
o["enabled"] = preferences.getBool(key.c_str(), true);
}
AsyncResponseStream *response = request->beginResponseStream("application/json"); AsyncResponseStream *response = request->beginResponseStream("application/json");
serializeJson(root, *response); serializeJson(root, *response);
@ -274,21 +284,23 @@ void onApiSettingsPost(AsyncWebServerRequest *request)
settingsChanged = true; settingsChanged = true;
} }
// for (int i = 0; i < screenNameMap.size(); i++) std::map<int, std::string> screenNameMap = getScreenNameMap();
// {
// 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); 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)) if (request->hasParam("tzOffset", true))
{ {
@ -339,7 +351,7 @@ void onApiSettingsPost(AsyncWebServerRequest *request)
request->send(200); request->send(200);
if (settingsChanged) if (settingsChanged)
{ {
//flashTemporaryLights(0, 255, 0); queueLedEffect(LED_FLASH_SUCCESS);
Serial.println(F("Settings changed")); Serial.println(F("Settings changed"));
} }
@ -361,6 +373,22 @@ void onApiSystemStatus(AsyncWebServerRequest *request)
request->send(response); request->send(response);
} }
void onApiLightsOff(AsyncWebServerRequest *request)
{
setLights(0, 0, 0);
request->send(200);
}
void onApiLightsSetColor(AsyncWebServerRequest *request)
{
String rgbColor = request->pathArg(0);
uint r, g, b;
sscanf(rgbColor.c_str(), "%02x%02x%02x", &r, &g, &b);
setLights(r, g, b);
request->send(200, "text/plain", rgbColor);
}
void onIndex(AsyncWebServerRequest *request) { request->send(LittleFS, "/index.html", String(), false); } void onIndex(AsyncWebServerRequest *request) { request->send(LittleFS, "/index.html", String(), false); }
void onNotFound(AsyncWebServerRequest *request) void onNotFound(AsyncWebServerRequest *request)

View file

@ -7,7 +7,7 @@
#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 "lib/screen_handler.hpp"
#include "lib/led_handler.hpp"
#include "webserver/OneParamRewrite.hpp" #include "webserver/OneParamRewrite.hpp"
@ -25,6 +25,10 @@ void onApiActionTimerRestart(AsyncWebServerRequest *request);
void onApiSettingsGet(AsyncWebServerRequest *request); void onApiSettingsGet(AsyncWebServerRequest *request);
void onApiSettingsPost(AsyncWebServerRequest *request); void onApiSettingsPost(AsyncWebServerRequest *request);
void onApiLightsOff(AsyncWebServerRequest *request);
void onApiLightsSetColor(AsyncWebServerRequest *request);
void onApiRestart(AsyncWebServerRequest *request); void onApiRestart(AsyncWebServerRequest *request);
void onIndex(AsyncWebServerRequest *request); void onIndex(AsyncWebServerRequest *request);