Skip to content

Commit 883ddcd

Browse files
committed
updated web flashing tool that asks support universal mmWave beta firmware while preserving the ability to select specifc mmWave firmware for production firmware
1 parent e27bc5c commit 883ddcd

6 files changed

Lines changed: 373 additions & 41 deletions

File tree

docs/javascript/firmware_selector.js

Lines changed: 168 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
(() => {
22
const config = [
33
{
4-
title: 'Select Firmware',
5-
description: 'Choose which firmware you\'ll be using with your device.',
4+
title: 'Select Satellite1 Firmware',
5+
description: 'Choose which firmware you\'ll be using with your Satellite1.',
66
choices: [
77
{
88
id: "stable",
@@ -37,6 +37,110 @@
3737
let stage = 0;
3838
const selections = ['stable', 'default']
3939

40+
function isBetaSelected() {
41+
return selections[0] === 'beta';
42+
}
43+
44+
function activeStepIndexes() {
45+
return isBetaSelected() ? [0] : config.map((_, idx) => idx);
46+
}
47+
48+
function isSummaryStage() {
49+
return !activeStepIndexes().includes(stage);
50+
}
51+
52+
function getPreviousStage() {
53+
const steps = activeStepIndexes();
54+
if (isSummaryStage()) return steps[steps.length - 1];
55+
56+
const idx = steps.indexOf(stage);
57+
return idx > 0 ? steps[idx - 1] : stage;
58+
}
59+
60+
function getNextStage() {
61+
const steps = activeStepIndexes();
62+
const idx = steps.indexOf(stage);
63+
return idx >= 0 && idx < steps.length - 1 ? steps[idx + 1] : config.length;
64+
}
65+
66+
function selectedLabel(idx) {
67+
return idx === 1 ? 'Selected mmWave' : config[idx].title.replace(/^Select\b/, 'Selected');
68+
}
69+
70+
function selectedValue(selected, idx) {
71+
if (idx !== 0) return selected.title;
72+
73+
const version = firmwareVersion(selected);
74+
return `${selected.title}${version ? ` (${version})` : ''}`;
75+
}
76+
77+
function choiceTitle(choice, idx) {
78+
if (idx !== 0) return choice.title;
79+
80+
const version = firmwareVersion(choice);
81+
return `${choice.title}${version ? ` (${version})` : ''}`;
82+
}
83+
84+
function firmwareVersion(choice) {
85+
return window.fphFirmwareVersions?.[choice.id] || '';
86+
}
87+
88+
function firmwareReleaseUrl(choice) {
89+
const version = firmwareVersion(choice);
90+
return window.fphFirmwareReleaseUrls?.[choice.id]
91+
|| (version ? `https://github.com/FutureProofHomes/Satellite1-ESPHome/releases/tag/${version}/` : '');
92+
}
93+
94+
function releaseLink(choice, className = 'firmware-release-link') {
95+
const releaseUrl = firmwareReleaseUrl(choice);
96+
if (!releaseUrl) return null;
97+
98+
return el('a', {
99+
className,
100+
href: releaseUrl,
101+
textContent: 'Read release notes',
102+
attr: {
103+
target: '_blank',
104+
rel: 'noopener',
105+
},
106+
});
107+
}
108+
109+
function summaryRow(label, value) {
110+
const row = el('p');
111+
row.append(
112+
el('strong', { textContent: label }),
113+
document.createTextNode(`: ${value}`)
114+
);
115+
return row;
116+
}
117+
118+
function firmwareSummaryRow(selected) {
119+
const version = firmwareVersion(selected);
120+
const releaseUrl = firmwareReleaseUrl(selected);
121+
const row = el('p');
122+
row.append(
123+
el('strong', { textContent: selectedLabel(0) }),
124+
document.createTextNode(': ')
125+
);
126+
127+
if (releaseUrl) {
128+
row.append(el('a', {
129+
href: releaseUrl,
130+
textContent: selectedValue(selected, 0),
131+
attr: {
132+
target: '_blank',
133+
rel: 'noopener',
134+
'aria-label': `Read release notes for ${selected.title} ${version}`,
135+
},
136+
}));
137+
} else {
138+
row.append(document.createTextNode(selectedValue(selected, 0)));
139+
}
140+
141+
return row;
142+
}
143+
40144
function setAttr(el, obj) {
41145
Object.entries(obj).forEach(([k, v]) => el[k] = v);
42146
return el
@@ -59,32 +163,36 @@
59163
attr: { 'aria-label': 'Progress' }
60164
})
61165
const fragment = document.createDocumentFragment();
62-
config.forEach((_, idx) => {
166+
const steps = activeStepIndexes();
167+
const currentPosition = steps.indexOf(stage);
168+
const completedPosition = isSummaryStage() ? steps.length : currentPosition;
169+
steps.forEach((_, idx) => {
63170
const dot = el('div', {
64171
className: 'dot',
65-
attr: { 'aria-current': idx === stage ? 'step' : 'false' }
172+
attr: { 'aria-current': idx === currentPosition ? 'step' : 'false' }
66173
})
67-
if (idx === stage) dot.classList.add('active');
68-
else if (idx < stage) dot.classList.add('done');
174+
if (idx === currentPosition) dot.classList.add('active');
175+
else if (idx < completedPosition) dot.classList.add('done');
69176
fragment.appendChild(dot);
70177
});
71178
nav.appendChild(fragment);
72179
return nav;
73180
}
74181

75182
function getBackBtn() {
183+
const hasPrevious = isSummaryStage() || activeStepIndexes().indexOf(stage) > 0;
76184
const back = el('button', {
77185
className: 'md-button md-button--secondary',
78-
textContent: 'Back',
79-
disabled: stage <= 0,
186+
textContent: 'Back',
187+
disabled: !hasPrevious,
80188
onclick: (e) => {
81189
e.stopPropagation();
82-
stage -= 1;
190+
stage = getPreviousStage();
83191
render();
84192
}
85193
});
86194

87-
if (stage <= 0) {
195+
if (!hasPrevious) {
88196
back.classList.add('hidden');
89197
}
90198
return back
@@ -168,7 +276,7 @@
168276
textContent: 'Next',
169277
onclick: (e) => {
170278
e.stopPropagation();
171-
stage += 1;
279+
stage = getNextStage();
172280
render();
173281
}
174282
})
@@ -207,20 +315,45 @@
207315
const fragment = document.createDocumentFragment();
208316
config[stage].choices.forEach((choice, i) => {
209317
const checked = selections[stage] === choice.id;
210-
const btn = el('button', {
211-
className: 'md-button md-button--primary choice-btn',
212-
textContent: choice.title,
318+
const isFirmwareStep = stage === 0;
319+
const btn = el(isFirmwareStep ? 'div' : 'button', {
320+
className: 'choice-btn',
213321
dataset: { id: choice.id },
214322
tabIndex: 0,
215323
attr: {
216324
role: 'radio',
217325
'aria-checked': checked
218326
},
219-
onclick: render
327+
onclick: render,
328+
onkeydown: (e) => {
329+
if (!isFirmwareStep || !['Enter', ' '].includes(e.key)) return;
330+
e.preventDefault();
331+
btn.click();
332+
}
220333
})
221334

335+
btn.append(
336+
el('span', {
337+
className: 'choice-check',
338+
textContent: '✓',
339+
attr: { 'aria-hidden': 'true' },
340+
}),
341+
el('span', {
342+
className: 'choice-label',
343+
textContent: choiceTitle(choice, stage),
344+
})
345+
);
346+
222347
if (checked) btn.classList.add('selected');
223-
fragment.appendChild(btn);
348+
349+
const link = isFirmwareStep ? releaseLink(choice) : null;
350+
if (link) {
351+
const choiceWrapper = el('div', { className: 'choice-option' });
352+
choiceWrapper.append(btn, link);
353+
fragment.appendChild(choiceWrapper);
354+
} else {
355+
fragment.appendChild(btn);
356+
}
224357
});
225358

226359
choicesDiv.appendChild(fragment);
@@ -232,15 +365,24 @@
232365
function summary() {
233366
const section = el('section', { className: 'md-typeset' });
234367
const fragment = document.createDocumentFragment();
235-
config.forEach((step, idx) => {
368+
activeStepIndexes().forEach((idx) => {
369+
const step = config[idx];
236370
const selected = step.choices.find(i => i.id === selections[idx]);
237371
if (!selected) return;
238372
fragment.appendChild(
239-
el('p', { textContent: `${step.title}: ${selected.title}` })
373+
idx === 0 ? firmwareSummaryRow(selected) : summaryRow(selectedLabel(idx), selectedValue(selected, idx))
240374
);
241375
});
376+
if (isBetaSelected()) {
377+
fragment.appendChild(
378+
summaryRow(
379+
'Selected mmWave',
380+
'Automatically supports both LD2410 and LD2450 if/when a sensor is attached.'
381+
)
382+
);
383+
}
242384
const firmware = selections[0] === 'stable' ? '' : `-${selections[0]}`;
243-
const mmwave = selections[1] === 'default' ? '' : `.${selections[1]}`;
385+
const mmwave = selections[0] === 'stable' && selections[1] !== 'default' ? `.${selections[1]}` : '';
244386
section.append(
245387
el('h2', { textContent: 'Summary' }),
246388
fragment,
@@ -254,17 +396,21 @@
254396
function handleChoice(e) {
255397
const btn = e.target.closest('.choice-btn');
256398
if (!btn) return;
257-
const group = btn.parentElement;
399+
const group = btn.closest('.choices');
258400
if (group && group.dataset.step) {
259-
selections[Number(group.dataset.step)] = btn.dataset.id;
401+
const step = Number(group.dataset.step);
402+
selections[step] = btn.dataset.id;
403+
if (step === 0 && isBetaSelected()) {
404+
selections[1] = 'default';
405+
}
260406
render();
261407
}
262408
}
263409

264410
function render() {
265411
const el = document.getElementById('firmware-selector');
266412
if (el) {
267-
const renderStep = stage < config.length;
413+
const renderStep = !isSummaryStage();
268414
el.replaceChildren();
269415
el.addEventListener('click', handleChoice);
270416
el.append(renderStep ? step() : summary());

docs/overrides/main.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@
5555
<button class="beta-banner__stable-btn" id="beta-to-stable" type="button">Switch to Stable</button>
5656
</div>
5757
<script>
58+
window.fphFirmwareVersions = {
59+
stable: "{{ config.extra.fw_stable_version }}",
60+
beta: "{{ config.extra.fw_beta_version }}"
61+
};
62+
window.fphFirmwareReleaseUrls = {
63+
stable: "{{ config.extra.fw_stable_release_url }}",
64+
beta: "{{ config.extra.fw_beta_release_url }}"
65+
};
66+
5867
(function() {
5968
function checkBeta() {
6069
var banner = document.getElementById('beta-banner');

docs/satellite1-flash-via-usb-c.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
1. Use a data-capable USB-C cable and connect it directly to your computer.
2-
2. If the CORE is mounted to the HAT, plug into the HAT’s “CORE/ESP32” USB-C port. Otherwise, use the CORE’s USB-C port.
3-
3. Use the form below to flash Production or Beta firmware, with or without mmWave.
1+
1. Use a **data-capable USB-C cable** and connect the Satellite's **CORE/ESP32** port to a computer running a **Chrome-based browser**.
2+
2. Follow the steps below to install your preferred firmware.
43

54
!!! example ""
65
<div class="form-container" id="firmware-selector" role="form" aria-live="polite"></div>
@@ -27,7 +26,7 @@
2726

2827
![Image title](/assets/ESPHome-Erase.png){ loading=lazy }
2928
![Image title](/assets/ESPHome-ConfirmInstall.png){ loading=lazy }
30-
Check **Erase the device** if you want to remove saved WiFi credentials, then click **"Next"** and **"Install"**.
29+
Check **Erase the device** only if you want to remove saved WiFi credentials, then click **"Next"** and **"Install"**.
3130

3231
- :material-numeric-4-circle:{ .lg .middle } __Installation completed__
3332

0 commit comments

Comments
 (0)