Follow-me stair light for the Apollo MTR-1 and WLED
Installer / Build (push) Failing after 2s
Installer / Publish (push) Skipped

ESPHome package for the Apollo MTR-1: tracks people on the stairs with the
LD2450 radar and streams a lit window to a WLED controller over DDP. The
sensor serves a setup wizard that measures the stairs with one walk and
stores the setup on the device. installer/ holds the browser installer,
published to GitHub Pages on every version tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jackson AndresandClaude Opus 5.5 committed 2026-10-14 20:08:40 -04:00
commit 87a0abca57
27 files changed
+11100

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
// Builds firmware/follow-me-wizard.h: the setup wizard as one gzipped page
// for the sensor to serve at /followme.
//
// node wizard/tools/embed.mjs
//
// Inlines every url(fonts/<file>) in wizard/wizard.html as a data: URI,
// drops <link> tags to Google Fonts (unreachable offline), marks the page as
// served by the device (<meta name="followme-on-device">), gzips it at level
// 9 and writes it as a C++ byte array. Exits non-zero if a referenced font
// file is missing or a Google Fonts reference remains.
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gzipSync, constants } from 'node:zlib';
const WIZARD_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
const ROOT = join(WIZARD_DIR, '..');
const SRC = join(WIZARD_DIR, 'wizard.html');
const OUT = join(ROOT, 'firmware', 'follow-me-wizard.h');
const MIME = { woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf', otf: 'font/otf' };
function fail(msg) {
console.error(`embed: ${msg}`);
process.exit(1);
}
let html = readFileSync(SRC, 'utf8');
// Google Fonts <link> tags (stylesheet and preconnect).
html = html.replace(/[ \t]*<link\b[^>]*fonts\.(?:googleapis|gstatic)\.com[^>]*>[ \t]*\r?\n?/gi, '');
if (/fonts\.(?:googleapis|gstatic)\.com/i.test(html)) fail('wizard.html still references Google Fonts outside a <link> tag');
// url(fonts/<file>), quoted or not.
const fonts = new Set();
html = html.replace(/url\(\s*(['"]?)(?:\.\/)?fonts\/([^'")\s]+)\1\s*\)/g, (_, _q, file) => {
const path = join(WIZARD_DIR, 'fonts', file);
if (!existsSync(path)) fail(`font file missing: ${relative(ROOT, path)}`);
const ext = file.split('.').pop().toLowerCase();
if (!MIME[ext]) fail(`unknown font type: ${file}`);
fonts.add(file);
return `url(data:${MIME[ext]};base64,${readFileSync(path).toString('base64')})`;
});
const META = '<meta name="followme-on-device" content="1">';
const charset = /<meta charset="utf-8">/i;
if (!charset.test(html)) fail('wizard.html has no <meta charset="utf-8">');
html = html.replace(charset, (m) => `${m}\n${META}`);
const raw = Buffer.from(html, 'utf8');
const gz = gzipSync(raw, { level: constants.Z_BEST_COMPRESSION });
const lines = [];
for (let i = 0; i < gz.length; i += 16)
lines.push(' ' + [...gz.subarray(i, i + 16)].map((b) => '0x' + b.toString(16).padStart(2, '0')).join(', ') + ',');
const header = `#pragma once
// Generated by wizard/tools/embed.mjs; do not edit. Regenerate after changing
// wizard/wizard.html or wizard/fonts: node wizard/tools/embed.mjs
//
// The setup wizard page with its fonts inlined, gzipped (${raw.length} bytes raw).
#include <cstddef>
#include <cstdint>
namespace follow_me {
static const uint8_t WIZARD_HTML_GZ[] = {
${lines.join('\n')}
};
static const size_t WIZARD_HTML_GZ_LEN = ${gz.length};
} // namespace follow_me
`;
writeFileSync(OUT, header);
console.log(`fonts inlined: ${fonts.size ? [...fonts].join(', ') : 'none'}`);
console.log(`raw ${raw.length} bytes, gzipped ${gz.length} bytes -> ${relative(ROOT, OUT)}`);