Follow-me stair light for the Apollo MTR-1 and WLED
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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
24 files changed
+10874
No files matched your search
@@ -0,0 +1,93 @@
|
||||
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,62 @@
|
||||
// Drives the wizard in headless Chrome over the DevTools protocol and saves
|
||||
// screenshots, for checking changes without a phone or a person on the stairs.
|
||||
// node wizard/tools/drive.mjs <url> <width> <height> <steps.json>
|
||||
// steps.json is a list run in order; each entry may have:
|
||||
// eval JavaScript run in the page (its value is printed)
|
||||
// wait milliseconds to wait afterwards
|
||||
// shot PNG path to save; with h, captures that many pixels tall
|
||||
// Use ?sim=1 in the url for the simulated walker. Real waits are needed:
|
||||
// Chrome's --virtual-time-budget hangs on the page's open event stream.
|
||||
// Needs Node 22+ (built-in WebSocket) and Google Chrome.
|
||||
import {spawn} from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const CHROME = process.env.CHROME || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const [url, w, h, stepsFile] = process.argv.slice(2);
|
||||
if (!stepsFile) { console.error('usage: drive.mjs <url> <width> <height> <steps.json>'); process.exit(2); }
|
||||
const steps = JSON.parse(fs.readFileSync(stepsFile, 'utf8'));
|
||||
const port = 9300 + Math.floor(Math.random() * 600);
|
||||
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'wizard-drive-'));
|
||||
const chrome = spawn(CHROME, ['--headless=new', `--remote-debugging-port=${port}`, `--user-data-dir=${profile}`, 'about:blank'], {stdio: 'ignore'});
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
let tabs;
|
||||
for (let i = 0; i < 50 && !tabs; i++) { try { tabs = await (await fetch(`http://127.0.0.1:${port}/json`)).json(); } catch { await sleep(200); } }
|
||||
const ws = new WebSocket(tabs.find(t => t.type === 'page').webSocketDebuggerUrl);
|
||||
await new Promise(r => { ws.onopen = r; });
|
||||
|
||||
let id = 0;
|
||||
const pending = {}, errors = [];
|
||||
ws.onmessage = e => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.method === 'Runtime.exceptionThrown') errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
if (pending[m.id]) { pending[m.id](m); delete pending[m.id]; }
|
||||
};
|
||||
const send = (method, params = {}) => new Promise(r => { pending[++id] = r; ws.send(JSON.stringify({id, method, params})); });
|
||||
|
||||
await send('Runtime.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', {width: +w, height: +h, deviceScaleFactor: 2, mobile: +w < 600});
|
||||
await send('Page.enable');
|
||||
await send('Page.navigate', {url});
|
||||
await sleep(2500);
|
||||
|
||||
for (const s of steps) {
|
||||
if (s.eval) {
|
||||
const r = await send('Runtime.evaluate', {expression: s.eval, returnByValue: true, awaitPromise: true});
|
||||
if (r.result?.exceptionDetails) console.log('eval error:', r.result.exceptionDetails.exception?.description);
|
||||
else if (r.result?.result?.value !== undefined) console.log('eval:', JSON.stringify(r.result.result.value));
|
||||
}
|
||||
if (s.wait) await sleep(s.wait);
|
||||
if (s.shot) {
|
||||
const r = await send('Page.captureScreenshot', {format: 'png', clip: {x: 0, y: 0, width: +w, height: +(s.h || h), scale: 1}, captureBeyondViewport: !!s.h});
|
||||
fs.writeFileSync(s.shot, Buffer.from(r.result.data, 'base64'));
|
||||
console.log('shot', s.shot);
|
||||
}
|
||||
}
|
||||
console.log(errors.length ? errors.map(e => 'page error: ' + e).join('\n') : 'no page errors');
|
||||
chrome.kill();
|
||||
await sleep(300);
|
||||
fs.rmSync(profile, {recursive: true, force: true, maxRetries: 3});
|
||||
process.exit(0);
|
||||
@@ -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)}`);
|
||||
@@ -0,0 +1,24 @@
|
||||
// Serves the wizard folder on the LAN so a phone can open it.
|
||||
// node wizard/tools/serve.mjs [port]
|
||||
// Then open http://<this machine>:<port>/ on the phone and enter the sensor's
|
||||
// address in the bar that opens (or add ?host=<address>).
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const port = +(process.argv[2] || 8080);
|
||||
const types = {'.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json',
|
||||
'.css': 'text/css', '.woff2': 'font/woff2', '.txt': 'text/plain; charset=utf-8'};
|
||||
|
||||
http.createServer((req, res) => {
|
||||
const p = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
||||
const f = path.join(root, p === '/' ? 'wizard.html' : p);
|
||||
if (!f.startsWith(root + path.sep)) { res.writeHead(403).end(); return; }
|
||||
fs.readFile(f, (err, data) => {
|
||||
if (err) { res.writeHead(404).end('not found'); return; }
|
||||
res.writeHead(200, {'Content-Type': types[path.extname(f)] || 'application/octet-stream', 'Cache-Control': 'no-store'}).end(data);
|
||||
console.log(new Date().toLocaleTimeString(), req.socket.remoteAddress, p);
|
||||
});
|
||||
}).listen(port, '0.0.0.0', () => console.log(`serving ${root} on port ${port}`));
|
||||
@@ -0,0 +1,97 @@
|
||||
[
|
||||
{
|
||||
"eval": "localStorage.clear(); location.reload(); 1",
|
||||
"wait": 2500
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-shape=\"L\"]').click(); 1",
|
||||
"wait": 500
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-inc=\"s0\"]').click(); document.querySelector('[data-inc=\"s1\"]').click(); document.querySelector('[data-inc=\"w\"]').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-side]').click(); document.querySelector('[data-start=\"bottom\"]').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 3000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 1500,
|
||||
"shot": "l-walk.png"
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 600
|
||||
},
|
||||
{
|
||||
"eval": "const h = document.getElementById('wledHost'); h.value = 'wled-stairs.local'; h.dispatchEvent(new Event('change')); 1",
|
||||
"wait": 5000
|
||||
},
|
||||
{
|
||||
"eval": "const e = document.getElementById('ledCount'); e.value = 150; e.dispatchEvent(new Event('change')); 1",
|
||||
"wait": 600
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').textContent + ' | disabled=' + document.getElementById('next').disabled",
|
||||
"wait": 200,
|
||||
"shot": "l-match.png"
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 1500
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('toast').textContent + ' | screen=' + document.getElementById('title').textContent",
|
||||
"wait": 3000,
|
||||
"shot": "l-test.png"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
[
|
||||
{
|
||||
"eval": "localStorage.clear(); location.reload(); 1",
|
||||
"wait": 2500
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-shape=\"straight\"]').click(); 1",
|
||||
"wait": 500
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-inc=\"s0\"]').click(); document.querySelector('[data-inc=\"w\"]').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-side]').click(); document.querySelector('[data-start=\"bottom\"]').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 3000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 1500,
|
||||
"shot": "straight-walk.png"
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 600
|
||||
},
|
||||
{
|
||||
"eval": "const h = document.getElementById('wledHost'); h.value = 'wled-stairs.local'; h.dispatchEvent(new Event('change')); 1",
|
||||
"wait": 5000
|
||||
},
|
||||
{
|
||||
"eval": "const e = document.getElementById('ledCount'); e.value = 150; e.dispatchEvent(new Event('change')); 1",
|
||||
"wait": 600
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').textContent + ' | disabled=' + document.getElementById('next').disabled",
|
||||
"wait": 200,
|
||||
"shot": "straight-match.png"
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 1500
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('toast').textContent + ' | screen=' + document.getElementById('title').textContent",
|
||||
"wait": 3000,
|
||||
"shot": "straight-test.png"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,97 @@
|
||||
[
|
||||
{
|
||||
"eval": "localStorage.clear(); location.reload(); 1",
|
||||
"wait": 2500
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-shape=\"U\"]').click(); 1",
|
||||
"wait": 500
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-inc=\"s0\"]').click(); document.querySelector('[data-inc=\"s1\"]').click(); document.querySelector('[data-inc=\"w\"]').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.querySelector('[data-side]').click(); document.querySelector('[data-start=\"bottom\"]').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 400
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 3000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 9000
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('walkTap').click(); 1",
|
||||
"wait": 1500,
|
||||
"shot": "u-walk.png"
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 600
|
||||
},
|
||||
{
|
||||
"eval": "const h = document.getElementById('wledHost'); h.value = 'wled-stairs.local'; h.dispatchEvent(new Event('change')); 1",
|
||||
"wait": 5000
|
||||
},
|
||||
{
|
||||
"eval": "const e = document.getElementById('ledCount'); e.value = 150; e.dispatchEvent(new Event('change')); 1",
|
||||
"wait": 600
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('pin').click(); 1",
|
||||
"wait": 300
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').textContent + ' | disabled=' + document.getElementById('next').disabled",
|
||||
"wait": 200,
|
||||
"shot": "u-match.png"
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('next').click(); 1",
|
||||
"wait": 1500
|
||||
},
|
||||
{
|
||||
"eval": "document.getElementById('toast').textContent + ' | screen=' + document.getElementById('title').textContent",
|
||||
"wait": 3000,
|
||||
"shot": "u-test.png"
|
||||
}
|
||||
]
|
||||
+1715
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user