mirror of
https://github.com/usetrmnl/byos_node_lite.git
synced 2026-08-13 23:18:18 -07:00
Minimal BYOS implementation
This commit is contained in:
+3
-1
@@ -1,3 +1,5 @@
|
||||
# example file for .env.local
|
||||
SECRET_KEY=PUT_YOUR_UNIQIE_SECRET_KEY_HERE
|
||||
SECRET_KEY=PUT_YOUR_UNIQUE_SECRET_KEY_HERE
|
||||
URL_ORIGIN=http://localhost:3000
|
||||
# If you want to use BYOS. Mac should be of your real TMNNL device
|
||||
BYOS_DEVICE_MAC=
|
||||
|
||||
+12
-1
@@ -28,7 +28,7 @@ from any source, and design it with HTML, JSX, CSS.
|
||||
**Image** http://localhost:3000/image?secret_key=... <br>
|
||||
-- can be used for preview and [Alias](https://help.usetrmnl.com/en/articles/10701448-alias-plugin) plugin
|
||||
|
||||
**API** http://localhost:3000/api?secret_key=... <br>
|
||||
**API** http://localhost:3000/plugin/redirect?secret_key=... <br>
|
||||
-- can be used for [Redirect](https://help.usetrmnl.com/en/articles/11035846-redirect-plugin) plugin
|
||||
|
||||
## JSX components
|
||||
@@ -74,5 +74,16 @@ Fetch Screen Content as your device (Developer edition):
|
||||
curl https://usetrmnl.com/api/display --header "access-token:xxxxxx"
|
||||
```
|
||||
|
||||
## Bring your own server (BYOS)
|
||||
This repo implements basic BYOS server for one device.<br>
|
||||
You can setup it with those steps:
|
||||
1. Put your device's MAC value to ENV (can be .env.local). If you don't know it: just put anything and check server logs.
|
||||
2. Hold round button on your device for more than5 seconds - you should see connection instructions on screen.
|
||||
3. Connect your phone to wifi called `TRMNL`
|
||||
4. On setup choose `use your own server`
|
||||
5. Check logs of server
|
||||
6. If it still `wrong access-token value from device` - you may need to choose `Soft reset` on setup stage
|
||||
|
||||
|
||||
---
|
||||
Goal of this repo: simple and easy to customize.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import {Router} from 'express';
|
||||
import {BYOS_DEVICE_MAC, PUBLIC_URL_ORIGIN, SECRET_KEY} from '../Config.js';
|
||||
import {screenHash} from "../Utils/Screen.js";
|
||||
import {sha256} from "../Utils/Sha256.js";
|
||||
|
||||
// all routes starts with /api/
|
||||
export const BYOSRoutes = Router();
|
||||
|
||||
|
||||
BYOSRoutes.post('/log', (req, res) => {
|
||||
const macId = getMacId(req);
|
||||
const data = req.body;
|
||||
if (!data['log'] || !data['log']['logs_array']) {
|
||||
res.status(204).send();
|
||||
return;
|
||||
}
|
||||
data['log']['logs_array'].map(record => {
|
||||
let ts = record['creation_timestamp'];
|
||||
if (ts) {
|
||||
ts = new Date(ts * 1000).toISOString();
|
||||
}
|
||||
console.log([
|
||||
`[LOG]`,
|
||||
`[${macId}]`,
|
||||
`EVENT_TIME`,
|
||||
ts,
|
||||
record['log_message'],
|
||||
'file:' + record['log_sourcefile'] + ':' + record['log_codeline']
|
||||
].join(' '))
|
||||
});
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
BYOSRoutes.get('/display', async (req, res) => {
|
||||
const macId = getMacId(req);
|
||||
if (readDeviceKey(req) !== calcProperDeviceApiKey(macId)) {
|
||||
console.error(`[DISPLAY] [${macId}] Wrong access-token value from device: ` + readDeviceKey(req));
|
||||
res.status(403).send();
|
||||
return;
|
||||
}
|
||||
batteryPercentage = calcBattery(req.headers['battery-voltage']);
|
||||
res.json({
|
||||
// screen wouldn't update if data is not changed
|
||||
filename: 'custom-screen-' + await screenHash(),
|
||||
image_url: PUBLIC_URL_ORIGIN + '/image?secret_key=' + SECRET_KEY,
|
||||
refresh_rate: 60, // Seconds. Can be overridden by device settings.
|
||||
});
|
||||
});
|
||||
|
||||
BYOSRoutes.get('/setup', (req, res) => {
|
||||
const macId = getMacId(req);
|
||||
if (!BYOS_DEVICE_MAC || macId !== BYOS_DEVICE_MAC) {
|
||||
console.error(`[SETUP] [${macId}] device is tried to connect with other MAC, that allowed - rejected`);
|
||||
res.status(403).send();
|
||||
return;
|
||||
}
|
||||
console.log(`[SETUP] [${macId}] device is trying to connect.`);
|
||||
res.json({
|
||||
"status": 200,
|
||||
"api_key": calcProperDeviceApiKey(macId),
|
||||
"friendly_id": "TRMNL",
|
||||
"message": "Device successfully registered",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
export let batteryPercentage = 0;
|
||||
|
||||
function calcBattery(voltage) {
|
||||
const minVoltage = 0.45;
|
||||
const maxVoltage = 4.05;
|
||||
const minPercentage = 10;
|
||||
const maxPercentage = 90;
|
||||
if (voltage <= minVoltage) return minPercentage;
|
||||
if (voltage > maxVoltage) return 100;
|
||||
const percentage = (voltage - minVoltage) / (maxVoltage - minVoltage) *
|
||||
(maxPercentage - minPercentage) + minPercentage;
|
||||
return Math.round(percentage);
|
||||
}
|
||||
|
||||
|
||||
function getMacId(req): string {
|
||||
if (typeof req.headers.id !== 'string') {
|
||||
throw new Error('Missing id header');
|
||||
}
|
||||
return req.headers.id;
|
||||
}
|
||||
|
||||
function calcProperDeviceApiKey(macID: string) {
|
||||
return sha256(SECRET_KEY + macID);
|
||||
}
|
||||
|
||||
function readDeviceKey(req): string {
|
||||
if (typeof req.headers['access-token'] !== 'string') {
|
||||
throw new Error('Missing access-token header');
|
||||
}
|
||||
return req.headers['access-token'];
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ export const SERVER_PORT = 3000;
|
||||
export const SERVER_HOST = '127.0.0.1'; // use '0.0.0.0' for access via local router
|
||||
export const TIMEZONE = 'Europe/Warsaw';
|
||||
export const ASSETS_FOLDER = path.join(import.meta.dirname, '..', 'assets');
|
||||
|
||||
export const BYOS_DEVICE_MAC = process.env['BYOS_DEVICE_MAC'] && process.env['BYOS_DEVICE_MAC'].length > 5 ? process.env['BYOS_DEVICE_MAC'] : undefined;
|
||||
|
||||
function readEnvOrFail(key: string): string {
|
||||
const value = process.env[key];
|
||||
|
||||
+40
-47
@@ -1,70 +1,63 @@
|
||||
import {JSXtoPNG} from "./Utils/JSXtoPNG.js";
|
||||
import App from "./Template/App.js";
|
||||
import express from "express";
|
||||
import {prepareData} from "./Data/PrepareData.js";
|
||||
import {SECRET_KEY, SERVER_HOST, SERVER_PORT, PUBLIC_URL_ORIGIN} from "./Config.js";
|
||||
import {PNGto1BIT} from "./Utils/PNGto1BIT.js";
|
||||
import crypto from 'crypto';
|
||||
import {buildScreen, screenHash} from "./Utils/Screen.js";
|
||||
import {BYOSRoutes} from "./BYOS/BYOSRoutes.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path === '/') {
|
||||
return next();
|
||||
}
|
||||
if (req.query['secret_key'] !== SECRET_KEY) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.status(401).send(JSON.stringify('Wrong or missing secret_key'));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/api', async (req, res) => {
|
||||
const data = await prepareData();
|
||||
const image = await JSXtoPNG(App(data));
|
||||
const imageHash = crypto.createHash('sha256').update(image).digest('hex');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify({
|
||||
// screen wouldn't update if data is not changed
|
||||
filename: 'custom-screen-' + imageHash,
|
||||
url: PUBLIC_URL_ORIGIN + '/image?secret_key=' + SECRET_KEY,
|
||||
refresh_rate: 60, // Seconds. Can be overridden by device settings.
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/image', async (req, res) => {
|
||||
const data = await prepareData();
|
||||
const image = await JSXtoPNG(App(data));
|
||||
const image1bit = await PNGto1BIT(image);
|
||||
res.setHeader('Content-Type', 'image/bmp');
|
||||
res.send(image1bit);
|
||||
})
|
||||
app.use(express.json());
|
||||
app.use('/api', BYOSRoutes); // comment this line to disable BYOS
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send();
|
||||
})
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).send({
|
||||
error: 'Not Found',
|
||||
message: 'The requested path could not be found: ' + req.url
|
||||
function isSecretKeyValid(req, res) {
|
||||
if (req.query['secret_key'] !== SECRET_KEY) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.status(401).json('Wrong or missing secret_key');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
app.get('/plugin/redirect', async (req, res) => {
|
||||
if (!isSecretKeyValid(req, res)) {
|
||||
return;
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.json({
|
||||
// screen wouldn't update if data is not changed
|
||||
filename: 'custom-screen-' + await screenHash(),
|
||||
url: PUBLIC_URL_ORIGIN + '/image?secret_key=' + SECRET_KEY,
|
||||
refresh_rate: 60, // Seconds. Can be overridden by device settings.
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/image', async (req, res) => {
|
||||
if (!isSecretKeyValid(req, res)) {
|
||||
return;
|
||||
}
|
||||
const image1bit = await buildScreen();
|
||||
res.setHeader('Content-Type', 'image/bmp');
|
||||
res.send(image1bit);
|
||||
})
|
||||
|
||||
|
||||
app.use((req, res) => {
|
||||
console.log(`[404] ${req.method} ${req.url}`);
|
||||
res.status(404).json({error: 'Not Found', message: 'The requested path could not be found: ' + req.url});
|
||||
});
|
||||
|
||||
app.use((err: Error, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).send({
|
||||
error: 'Internal Server Error',
|
||||
message: 'Something went wrong!'
|
||||
});
|
||||
res.status(500).json({error: 'Internal Server Error', message: 'Something went wrong!'});
|
||||
});
|
||||
|
||||
app.listen(SERVER_PORT, SERVER_HOST, (error) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
} else {
|
||||
console.log(`Server started. Check it http://${SERVER_HOST}:${SERVER_PORT}/api?secret_key=... OR ${PUBLIC_URL_ORIGIN}/api?secret_key=...`);
|
||||
console.log(`Server started. Check it http://${SERVER_HOST}:${SERVER_PORT}/image?secret_key=... OR ${PUBLIC_URL_ORIGIN}/image?secret_key=...`);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {prepareData} from "../Data/PrepareData.js";
|
||||
import {JSXtoPNG} from "./JSXtoPNG.js";
|
||||
import App from "../Template/App.js";
|
||||
import {PNGto1BIT} from "./PNGto1BIT.js";
|
||||
import {sha256} from "./Sha256.js";
|
||||
|
||||
export async function buildScreen() {
|
||||
const data = await prepareData();
|
||||
const image = await JSXtoPNG(App(data));
|
||||
return PNGto1BIT(image);
|
||||
}
|
||||
|
||||
export async function screenHash() {
|
||||
const screen = await buildScreen();
|
||||
return sha256(screen);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
export function sha256(data: string | NodeJS.ArrayBufferView): string {
|
||||
return crypto.createHash('sha256').update(data).digest('hex');
|
||||
}
|
||||
Reference in New Issue
Block a user