mirror of
https://github.com/btclock/btclock_v3.git
synced 2024-11-19 00:21:06 +01:00
LEDs and buttons working
This commit is contained in:
parent
4f2fbd8a36
commit
91fd921e2e
6
.gitignore
vendored
6
.gitignore
vendored
@ -5,3 +5,9 @@
|
||||
.vscode/ipch
|
||||
managed_components
|
||||
data/build/*
|
||||
data/build
|
||||
data/.yarn
|
||||
data/node_modules
|
||||
node_modules
|
||||
.DS_Store
|
||||
*.bin
|
1
data/.yarnrc.yml
Normal file
1
data/.yarnrc.yml
Normal file
@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
50
data/esbuild.mjs
Normal file
50
data/esbuild.mjs
Normal 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
23
data/package.json
Normal 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
BIN
data/src/css/oswald.woff
Normal file
Binary file not shown.
BIN
data/src/css/oswald.woff2
Normal file
BIN
data/src/css/oswald.woff2
Normal file
Binary file not shown.
121
data/src/css/style.scss
Normal file
121
data/src/css/style.scss
Normal 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
0
data/src/font/oswald.css
Normal file
235
data/src/index.html
Normal file
235
data/src/index.html
Normal 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>₿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">₿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 }}"> </div>
|
||||
{{/each}}
|
||||
|
||||
{{/if}}
|
||||
<div>
|
||||
<p>Screen cycle:
|
||||
<span onclick="toggleTimer({{ timerRunning }})" id="toggleTimerArea">
|
||||
{{#if timerRunning}}
|
||||
⏵
|
||||
{{else}}
|
||||
⏸
|
||||
{{/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}}
|
||||
✅
|
||||
{{else}}
|
||||
❌
|
||||
{{/if}}
|
||||
</span>
|
||||
-
|
||||
Mempool.space connection:
|
||||
<span>
|
||||
{{#if connectionStatus.price}}
|
||||
✅
|
||||
{{else}}
|
||||
❌
|
||||
{{/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
17
data/src/js/helpers.js
Normal 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 = " ";
|
||||
}
|
||||
//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
187
data/src/js/script.ts
Normal 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
48
data/src/wifi.html
Normal 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>₿TClock WiFi Settings</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">₿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
6
data/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"files": ["src/js/**.ts"],
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true,
|
||||
}
|
||||
}
|
2728
data/yarn.lock
Normal file
2728
data/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,4 +4,4 @@ Import("env")
|
||||
def before_buildfs(source, target, env):
|
||||
env.Execute("cd data && yarn && yarn build")
|
||||
|
||||
env.AddPreAction("$BUILD_DIR/spiffs.bin", before_buildfs)
|
||||
env.AddPreAction("$BUILD_DIR/littlefs.bin", before_buildfs)
|
||||
|
@ -1,8 +1,7 @@
|
||||
#include "block_notify.hpp"
|
||||
|
||||
const char *wsServer = "wss://mempool.space/api/v1/ws";
|
||||
// WebsocketsClient client;
|
||||
esp_websocket_client_handle_t client;
|
||||
char *wsServer;
|
||||
esp_websocket_client_handle_t blockNotifyClient = NULL;
|
||||
unsigned long int currentBlockHeight;
|
||||
|
||||
void setupBlockNotify()
|
||||
@ -36,15 +35,17 @@ void setupBlockNotify()
|
||||
xTaskNotifyGive(blockUpdateTaskHandle);
|
||||
}
|
||||
|
||||
// std::strcpy(wsServer, String("wss://" + mempoolInstance + "/api/v1/ws").c_str());
|
||||
|
||||
esp_websocket_client_config_t config = {
|
||||
.uri = "wss://mempool.bitcoin.nl/api/v1/ws",
|
||||
};
|
||||
|
||||
Serial.printf("Connecting to %s\r\n", config.uri);
|
||||
|
||||
client = esp_websocket_client_init(&config);
|
||||
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, onWebsocketEvent, client);
|
||||
esp_websocket_client_start(client);
|
||||
blockNotifyClient = esp_websocket_client_init(&config);
|
||||
esp_websocket_register_events(blockNotifyClient, WEBSOCKET_EVENT_ANY, onWebsocketEvent, blockNotifyClient);
|
||||
esp_websocket_client_start(blockNotifyClient);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
}
|
||||
@ -92,8 +93,10 @@ void onWebsocketMessage(esp_websocket_event_data_t *event_data)
|
||||
Serial.print("New block found: ");
|
||||
Serial.println(block["height"].as<long>());
|
||||
|
||||
if (blockUpdateTaskHandle != nullptr)
|
||||
if (blockUpdateTaskHandle != nullptr) {
|
||||
xTaskNotifyGive(blockUpdateTaskHandle);
|
||||
queueLedEffect(LED_FLASH_BLOCK_NOTIFY);
|
||||
}
|
||||
}
|
||||
|
||||
doc.clear();
|
||||
@ -103,3 +106,9 @@ unsigned long getBlockHeight()
|
||||
{
|
||||
return currentBlockHeight;
|
||||
}
|
||||
|
||||
bool isBlockNotifyConnected() {
|
||||
if (blockNotifyClient == NULL)
|
||||
return false;
|
||||
return esp_websocket_client_is_connected(blockNotifyClient);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <Arduino.h>
|
||||
#include <HTTPClient.h>
|
||||
@ -8,6 +9,7 @@
|
||||
|
||||
#include "esp_websocket_client.h"
|
||||
#include "screen_handler.hpp"
|
||||
#include "led_handler.hpp"
|
||||
|
||||
//using namespace websockets;
|
||||
|
||||
@ -17,3 +19,4 @@ void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_i
|
||||
void onWebsocketMessage(esp_websocket_event_data_t* event_data);
|
||||
|
||||
unsigned long getBlockHeight();
|
||||
bool isBlockNotifyConnected();
|
||||
|
@ -18,22 +18,21 @@ void buttonTask(void *parameter)
|
||||
{
|
||||
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;
|
||||
// }
|
||||
switch (pin)
|
||||
{
|
||||
case 3:
|
||||
toggleTimerActive();
|
||||
break;
|
||||
case 2:
|
||||
nextScreen();
|
||||
break;
|
||||
case 1:
|
||||
previousScreen();
|
||||
break;
|
||||
case 0:
|
||||
showSystemStatusScreen();
|
||||
break;
|
||||
}
|
||||
}
|
||||
mcp.clearInterrupts();
|
||||
// Very ugly, but for some reason this is necessary
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include "shared.hpp"
|
||||
#include "screen_handler.hpp"
|
||||
|
||||
extern TaskHandle_t buttonTaskHandle;
|
||||
|
||||
|
@ -1,27 +1,22 @@
|
||||
#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);
|
||||
std::map<int, std::string> screenNameMap;
|
||||
|
||||
void setup()
|
||||
{
|
||||
setupHardware();
|
||||
if (mcp.digitalRead(3) == LOW) {
|
||||
if (mcp.digitalRead(3) == LOW)
|
||||
{
|
||||
WiFi.eraseAP();
|
||||
blinkDelay(100, 3);
|
||||
}
|
||||
|
||||
|
||||
setupDisplays();
|
||||
tryImprovSetup();
|
||||
|
||||
@ -47,9 +42,9 @@ void tryImprovSetup()
|
||||
// blinkDelay(100, 3);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// WiFi.begin();
|
||||
// }
|
||||
{
|
||||
WiFi.begin();
|
||||
}
|
||||
|
||||
uint8_t x_buffer[16];
|
||||
uint8_t x_position = 0;
|
||||
@ -69,7 +64,8 @@ void tryImprovSetup()
|
||||
x_position = 0;
|
||||
}
|
||||
}
|
||||
// vTaskDelay(1);
|
||||
|
||||
vTaskDelay(1 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +87,15 @@ void setupTime()
|
||||
void setupPreferences()
|
||||
{
|
||||
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()
|
||||
@ -107,18 +112,16 @@ void setupTimers()
|
||||
|
||||
void finishSetup()
|
||||
{
|
||||
pixels.clear();
|
||||
pixels.show();
|
||||
clearLeds();
|
||||
}
|
||||
|
||||
std::map<int, std::string> getScreenNameMap() {
|
||||
return screenNameMap;
|
||||
}
|
||||
|
||||
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();
|
||||
setupLeds();
|
||||
|
||||
if (psramInit())
|
||||
{
|
||||
@ -129,7 +132,6 @@ void setupHardware()
|
||||
Serial.println(F("PSRAM not available"));
|
||||
}
|
||||
|
||||
|
||||
Wire.begin(35, 36, 400000);
|
||||
|
||||
if (!mcp.begin_I2C(0x20))
|
||||
@ -153,7 +155,6 @@ void setupHardware()
|
||||
{
|
||||
mcp.pinMode(i, OUTPUT);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,41 +195,20 @@ bool improv_connectWifi(std::string ssid, std::string password)
|
||||
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));
|
||||
blinkDelayColor(100, 1, 255,0,0);
|
||||
// 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));
|
||||
// pixels.clear();
|
||||
// pixels.show();
|
||||
// vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
std::vector<std::string> getLocalUrl()
|
||||
|
@ -2,7 +2,6 @@
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <Preferences.h>
|
||||
#include <Adafruit_MCP23X17.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
|
||||
#include "shared.hpp"
|
||||
#include <esp_system.h>
|
||||
@ -16,6 +15,7 @@
|
||||
#include "lib/block_notify.hpp"
|
||||
#include "lib/price_notify.hpp"
|
||||
#include "lib/button_handler.hpp"
|
||||
#include "lib/led_handler.hpp"
|
||||
|
||||
#define NTP_SERVER "pool.ntp.org"
|
||||
#define DEFAULT_MEMPOOL_INSTANCE "mempool.space"
|
||||
@ -23,6 +23,9 @@
|
||||
#define USER_AGENT "BTClock/2.0"
|
||||
#define MCP_DEV_ADDR 0x20
|
||||
|
||||
#define DEFAULT_FG_COLOR GxEPD_WHITE
|
||||
#define DEFAULT_BG_COLOR GxEPD_BLACK
|
||||
|
||||
#define BITCOIND_HOST ""
|
||||
#define BITCOIND_PORT 8332
|
||||
#define BITCOIND_RPC_USER ""
|
||||
@ -36,6 +39,7 @@ void setupHardware();
|
||||
void tryImprovSetup();
|
||||
void setupTimers();
|
||||
void finishSetup();
|
||||
std::map<int, std::string> getScreenNameMap();
|
||||
|
||||
std::vector<std::string> getLocalUrl();
|
||||
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_send_response(std::vector<uint8_t> &response);
|
||||
void improv_set_error(improv::Error error);
|
||||
void blinkDelay(int d, int times);
|
@ -76,7 +76,7 @@ void setupDisplays()
|
||||
int *taskParam = new int;
|
||||
*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",
|
||||
@ -91,7 +91,7 @@ void setupDisplays()
|
||||
xTaskNotifyGive(tasks[i]);
|
||||
}
|
||||
|
||||
xTaskCreate(taskEpd, "epd_task", 2048, NULL, 1, NULL);
|
||||
xTaskCreate(taskEpd, "epd_task", 2048, NULL, tskIDLE_PRIORITY, NULL);
|
||||
}
|
||||
|
||||
void taskEpd(void *pvParameters)
|
||||
|
@ -1,18 +1,216 @@
|
||||
#include "led_handler.hpp"
|
||||
|
||||
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);
|
||||
uint32_t notificationValue;
|
||||
unsigned long ledTaskParams;
|
||||
|
||||
void ledTask(void *parameter)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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);
|
||||
}
|
@ -6,7 +6,33 @@
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#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;
|
||||
|
||||
void ledTask(void *pvParameters);
|
||||
void setupLeds();
|
||||
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);
|
@ -2,7 +2,7 @@
|
||||
|
||||
const char *wsServerPrice = "wss://ws.coincap.io/prices?assets=bitcoin";
|
||||
// WebsocketsClient client;
|
||||
esp_websocket_client_handle_t clientPrice;
|
||||
esp_websocket_client_handle_t clientPrice = NULL;
|
||||
unsigned long int currentPrice;
|
||||
|
||||
void setupPriceNotify()
|
||||
@ -46,7 +46,6 @@ void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data)
|
||||
|
||||
if (doc.containsKey("bitcoin")) {
|
||||
if (currentPrice != doc["bitcoin"].as<long>()) {
|
||||
// Serial.printf("New price %lu\r\n", currentPrice);
|
||||
|
||||
const unsigned long oldPrice = currentPrice;
|
||||
currentPrice = doc["bitcoin"].as<long>();
|
||||
@ -62,3 +61,9 @@ void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data)
|
||||
unsigned long getPrice() {
|
||||
return currentPrice;
|
||||
}
|
||||
|
||||
bool isPriceNotifyConnected() {
|
||||
if (clientPrice == NULL)
|
||||
return false;
|
||||
return esp_websocket_client_is_connected(clientPrice);
|
||||
}
|
@ -15,3 +15,4 @@ void onWebsocketPriceEvent(void *handler_args, esp_event_base_t base, int32_t ev
|
||||
void onWebsocketPriceMessage(esp_websocket_event_data_t* event_data);
|
||||
|
||||
unsigned long getPrice();
|
||||
bool isPriceNotifyConnected();
|
@ -61,7 +61,16 @@ void taskScreenRotate(void *pvParameters)
|
||||
{
|
||||
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)
|
||||
{
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
@ -217,11 +212,8 @@ void setupScreenRotateTimer(void *pvParameters)
|
||||
.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);
|
||||
}
|
||||
|
||||
@ -240,13 +232,19 @@ void setTimerActive(bool status)
|
||||
if (status)
|
||||
{
|
||||
esp_timer_start_periodic(screenRotateTimer, getTimerSeconds() * usPerSecond);
|
||||
queueLedEffect(LED_EFFECT_START_TIMER);
|
||||
}
|
||||
else
|
||||
{
|
||||
esp_timer_stop(screenRotateTimer);
|
||||
queueLedEffect(LED_EFFECT_PAUSE_TIMER);
|
||||
}
|
||||
}
|
||||
|
||||
void toggleTimerActive() {
|
||||
setTimerActive(!isTimerActive());
|
||||
}
|
||||
|
||||
uint getCurrentScreen()
|
||||
{
|
||||
return currentScreen;
|
||||
@ -276,3 +274,55 @@ void setCurrentScreen(uint newScreen)
|
||||
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);
|
||||
}
|
@ -16,6 +16,10 @@ extern TaskHandle_t taskScreenRotateTaskHandle;
|
||||
|
||||
uint getCurrentScreen();
|
||||
void setCurrentScreen(uint newScreen);
|
||||
void nextScreen();
|
||||
void previousScreen();
|
||||
|
||||
void showSystemStatusScreen();
|
||||
|
||||
void setupTimeUpdateTimer(void *pvParameters);
|
||||
void setupScreenRotateTimer(void *pvParameters);
|
||||
@ -31,7 +35,6 @@ void taskScreenRotate(void *pvParameters);
|
||||
uint getTimerSeconds();
|
||||
bool isTimerActive();
|
||||
void setTimerActive(bool status);
|
||||
|
||||
void toggleTimerActive();
|
||||
|
||||
void setupTasks();
|
||||
const char* int64_to_iso8601(int64_t timestamp);
|
@ -3,6 +3,7 @@
|
||||
#include <Adafruit_MCP23X17.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <Preferences.h>
|
||||
#include "utils.hpp"
|
||||
|
||||
extern Adafruit_MCP23X17 mcp;
|
||||
extern Preferences preferences;
|
||||
@ -15,6 +16,7 @@ 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 };
|
||||
const int SCREEN_COUNT = 5;
|
||||
|
||||
struct SpiRamAllocator {
|
||||
void* allocate(size_t size) {
|
||||
|
6
src/lib/utils.cpp
Normal file
6
src/lib/utils.cpp
Normal 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
1
src/lib/utils.hpp
Normal file
@ -0,0 +1 @@
|
||||
int modulo(int x,int N);
|
@ -28,6 +28,10 @@ void setupWebserver()
|
||||
server.on("/api/show/screen", HTTP_GET, onApiShowScreen);
|
||||
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.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["espPsramSize"] = ESP.getPsramSize();
|
||||
|
||||
JsonObject conStatus = root.createNestedObject("connectionStatus");
|
||||
conStatus["price"] = isPriceNotifyConnected();
|
||||
conStatus["blocks"] = isBlockNotifyConnected();
|
||||
|
||||
JsonArray data = root.createNestedArray("data");
|
||||
JsonArray rendered = root.createNestedArray("rendered");
|
||||
String epdContent[NUM_SCREENS];
|
||||
@ -170,14 +178,16 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
|
||||
#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);
|
||||
// }
|
||||
std::map<int, std::string> screenNameMap = getScreenNameMap();
|
||||
|
||||
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);
|
||||
@ -274,21 +284,23 @@ void onApiSettingsPost(AsyncWebServerRequest *request)
|
||||
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);
|
||||
std::map<int, std::string> screenNameMap = getScreenNameMap();
|
||||
|
||||
// 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))
|
||||
{
|
||||
@ -339,7 +351,7 @@ void onApiSettingsPost(AsyncWebServerRequest *request)
|
||||
request->send(200);
|
||||
if (settingsChanged)
|
||||
{
|
||||
//flashTemporaryLights(0, 255, 0);
|
||||
queueLedEffect(LED_FLASH_SUCCESS);
|
||||
|
||||
Serial.println(F("Settings changed"));
|
||||
}
|
||||
@ -361,6 +373,22 @@ void onApiSystemStatus(AsyncWebServerRequest *request)
|
||||
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 onNotFound(AsyncWebServerRequest *request)
|
||||
|
@ -7,7 +7,7 @@
|
||||
#include "lib/block_notify.hpp"
|
||||
#include "lib/price_notify.hpp"
|
||||
#include "lib/screen_handler.hpp"
|
||||
|
||||
#include "lib/led_handler.hpp"
|
||||
|
||||
#include "webserver/OneParamRewrite.hpp"
|
||||
|
||||
@ -25,6 +25,10 @@ void onApiActionTimerRestart(AsyncWebServerRequest *request);
|
||||
void onApiSettingsGet(AsyncWebServerRequest *request);
|
||||
void onApiSettingsPost(AsyncWebServerRequest *request);
|
||||
|
||||
void onApiLightsOff(AsyncWebServerRequest *request);
|
||||
void onApiLightsSetColor(AsyncWebServerRequest *request);
|
||||
|
||||
|
||||
void onApiRestart(AsyncWebServerRequest *request);
|
||||
|
||||
void onIndex(AsyncWebServerRequest *request);
|
||||
|
Loading…
Reference in New Issue
Block a user