const hint = document.getElementById('fw-hint');
const btn = document.getElementById('fw-download-btn');
const status = document.getElementById('fw-status');
+ const unsupported = document.getElementById('chip-unsupported');
+
+ // Frame-blast parts can't be driven from the browser at all — say so up
+ // front instead of letting the user download an image and then watch the
+ // handshake fail for reasons the UI can't explain.
+ if (chip && needsFrameBlast(chip)) {
+ unsupported.style.display = '';
+ unsupported.innerHTML =
+ `${chip} needs the command-line tool. Its bootrom uses a ` +
+ `frame-blast handshake (an active 0xAA+HEAD blast plus a ` +
+ `PRESTEP0 block) that this browser build does not implement, so the ` +
+ `upload cannot complete here. Install defib and run:` +
+ `
` +
+ `Start it before you power-cycle the camera. ` +
+ `Tracking issue.`;
+ autoDiv.style.display = 'none';
+ hint.textContent = '';
+ // Deliberately leave firmwareData alone. Start is already blocked by
+ // needsFrameBlast() below, and clearing it here would strand a locally
+ // selected file: the file input keeps its selection, so the normal path
+ // won't reload it (it only clears when no file is selected) and the
+ // change handler won't re-fire — leaving Start disabled on the way back
+ // to a supported chip.
+ updateStartButton();
+ return;
+ }
+ unsupported.style.display = 'none';
if (chip && hasFirmware(chip)) {
autoDiv.style.display = '';
@@ -629,8 +664,13 @@
Serial Console
];
const unique = [...new Set(allChips)].sort();
sel.innerHTML = '';
- for (const c of unique) sel.innerHTML += ``;
- document.getElementById('chip-count').textContent = `(${unique.length} supported)`;
+ for (const c of unique) {
+ const suffix = needsFrameBlast(c) ? ' — CLI only' : '';
+ sel.innerHTML += ``;
+ }
+ const usable = unique.filter(c => !needsFrameBlast(c)).length;
+ document.getElementById('chip-count').textContent =
+ `(${usable} of ${unique.length} supported in browser)`;
sel.onchange = onChipChanged;
// Warm the release index so availability reflects what OpenIPC actually
// publishes today rather than the hardcoded fallback list. Refresh only if
@@ -658,7 +698,8 @@
Serial Console
function updateStartButton() {
const chip = document.getElementById('chip-select').value;
- document.getElementById('start-btn').disabled = !(chip && firmwareData && selectedPort);
+ document.getElementById('start-btn').disabled =
+ !(chip && firmwareData && selectedPort) || needsFrameBlast(chip);
}
function setStage(name) {
diff --git a/web/profile-parity.test.js b/web/profile-parity.test.js
new file mode 100644
index 0000000..47ae85c
--- /dev/null
+++ b/web/profile-parity.test.js
@@ -0,0 +1,106 @@
+/**
+ * Parity between the browser build's inline PROFILES blob and the CLI's
+ * profile data under src/defib/profiles/data.
+ *
+ * web/index.html carries its own hand-maintained copy of the SoC profiles so
+ * the page can be deployed as static files. Nothing regenerates it, so it can
+ * drift from the CLI — which is exactly how every chip in it ended up with its
+ * PRESTEP0 stripped while staying selectable in the dropdown (defib#121).
+ *
+ * These tests fail when that happens again.
+ *
+ * Run: node --test web/profile-parity.test.js
+ */
+
+const { describe, it } = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const { FRAME_BLAST_SOCS } = require('./protocol.js');
+
+const REPO_ROOT = path.join(__dirname, '..');
+const DATA_DIR = path.join(REPO_ROOT, 'src', 'defib', 'profiles', 'data');
+const MAX_ALIAS_DEPTH = 23;
+
+/**
+ * Resolve a chip to its profile object, following alias files.
+ *
+ * An alias is a profile file whose entire contents are a single token ending
+ * in `.json` — e.g. hi3516ev300.json contains just "hi3516ev200.json". Missing
+ * this indirection is what made the PRESTEP0 drift invisible: a naive read of
+ * hi3516ev300.json finds no PRESTEP0 because it holds no fields at all.
+ * Mirrors load_profile() in src/defib/profiles/loader.py.
+ */
+function resolveProfile(chip, depth = 0) {
+ if (depth > MAX_ALIAS_DEPTH) return null;
+ const file = path.join(DATA_DIR, `${chip}.json`);
+ if (!fs.existsSync(file)) return null;
+ const raw = fs.readFileSync(file, 'utf8').trim();
+ const tokens = raw.split(/\s+/);
+ if (tokens.length === 1 && tokens[0].endsWith('.json')) {
+ return resolveProfile(tokens[0].slice(0, -5), depth + 1);
+ }
+ try {
+ return JSON.parse(raw);
+ } catch {
+ return null;
+ }
+}
+
+/** Pull the inline `const PROFILES = {...};` blob out of web/index.html. */
+function readWebProfiles() {
+ const html = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
+ // Tolerate CRLF and trailing whitespace — a checkout with core.autocrlf=true
+ // would otherwise fail this on a formatting detail rather than a real drift.
+ const m = html.match(/const PROFILES = (\{[\s\S]*?\});[ \t]*\r?$/m);
+ assert.ok(m, 'could not locate the PROFILES blob in web/index.html');
+ return JSON.parse(m[1]);
+}
+
+describe('web PROFILES vs CLI profile data', () => {
+ const webProfiles = readWebProfiles();
+
+ it('every chip in the web dropdown has a CLI profile behind it', () => {
+ const orphans = Object.keys(webProfiles).filter(c => resolveProfile(c) === null);
+ assert.deepEqual(orphans, [],
+ `web-only chips with no resolvable CLI profile: ${orphans.join(', ')}`);
+ });
+
+ it('FRAME_BLAST_SOCS matches the chips whose resolved profile has PRESTEP0', () => {
+ const expected = Object.keys(webProfiles)
+ .filter(c => {
+ const p = resolveProfile(c);
+ return p && p.PRESTEP0 != null;
+ })
+ .sort();
+ const actual = [...FRAME_BLAST_SOCS].sort();
+
+ const missing = expected.filter(c => !FRAME_BLAST_SOCS.has(c));
+ const extra = actual.filter(c => !expected.includes(c));
+
+ assert.deepEqual(missing, [],
+ `these chips need a frame-blast handshake but are not in FRAME_BLAST_SOCS, ` +
+ `so the UI will let users start a recovery that cannot succeed: ${missing.join(', ')}`);
+ assert.deepEqual(extra, [],
+ `these are listed in FRAME_BLAST_SOCS but their resolved profile has no ` +
+ `PRESTEP0, so they are being blocked for no reason: ${extra.join(', ')}`);
+ });
+
+ it('the web build does not claim to send PRESTEP0 it has no data for', () => {
+ // If a future change starts shipping PRESTEP0 in the web blob, the
+ // frame-blast path must be implemented in protocol.js at the same time —
+ // otherwise the data is inert and the block list above silently wrong.
+ const withPrestep = Object.entries(webProfiles)
+ .filter(([, p]) => p.PRESTEP0 != null)
+ .map(([c]) => c);
+ const protocolSrc = fs.readFileSync(path.join(__dirname, 'protocol.js'), 'utf8');
+ const implemented = /function\s+buildPrestepFrames|sendFrameForStart/.test(protocolSrc);
+ if (withPrestep.length > 0) {
+ assert.ok(implemented,
+ `web PROFILES now carry PRESTEP0 for ${withPrestep.join(', ')} but ` +
+ `protocol.js still has no frame-blast implementation — either implement ` +
+ `it or drop the data`);
+ }
+ });
+});
diff --git a/web/protocol.js b/web/protocol.js
index c52c8ea..e47596d 100644
--- a/web/protocol.js
+++ b/web/protocol.js
@@ -138,6 +138,38 @@ function parseCv6xxBoot(data) {
const V500_SOCS = new Set(["gk7205v500","gk7205v510","gk7205v530","xm7205v500","xm7205v510","xm7205v530"]);
const CV6XX_SOCS = new Set(["hi3516cv608","hi3516cv610","hi3516cv613","hi3516dv500","hi3519dv500"]);
+// Standard-protocol SoCs whose bootrom needs the frame-blast handshake: an
+// active 0xAA+HEAD blast instead of passively waiting for the 0x20 markers,
+// followed by a PRESTEP0 block sent ahead of DDRSTEP0. The CLI drives this
+// from HiSiliconStandard._send_frame_for_start(); this build implements
+// neither, so standardHandshake()/standardSendFirmware() cannot bring these
+// parts up. Refuse them in the UI rather than fail silently at the wire.
+//
+// Derived from the chips whose *resolved* profile under
+// src/defib/profiles/data carries a PRESTEP0 — resolution matters, since
+// e.g. hi3516ev300.json is a one-line alias pointing at hi3516ev200.json.
+// profile-parity.test.js recomputes this and fails if it drifts.
+const FRAME_BLAST_SOCS = new Set([
+ "hi3110ev500", "hi3110ev500-ca", "hi3231v520", "hi3231v530", "hi3251v500",
+ "hi3251v510", "hi3516a", "hi3516av200", "hi3516cv300", "hi3516cv500",
+ "hi3516ev200", "hi3516ev300", "hi3518ev200", "hi3519", "hi3519v101",
+ "hi3520dv400", "hi3521a", "hi3521dv100", "hi3531a", "hi3531dv100",
+ "hi3536", "hi3536c", "hi3536dv100", "hi3556av100", "hi3559av100",
+ "hi3559av100es", "hi3559v100", "hi3559v200", "hi3712v100", "hi3716dv100",
+ "hi3716dv100-ca", "hi3716dv110", "hi3716dv110h", "hi3716mv310", "hi3716mv310-ca",
+ "hi3716mv320", "hi3716mv330", "hi3716mv330-ca", "hi3716mv410", "hi3716mv410-ca",
+ "hi3716mv410-ca-n", "hi3716mv420-ca-n", "hi3716mv430", "hi3731v100", "hi3731v101",
+ "hi3731v201", "hi3731v202", "hi3751v310", "hi3751v320", "hi3751v500-ca",
+ "hi3751v510", "hi3751v530", "hi3751v551", "hi3751v553", "hi3751v600",
+ "hi3751v600-ca", "hi3751v620", "hi3751v810", "hi3751v811", "hi3796mv200",
+ "hi3798cv200", "hi3798mv100", "hi3798mv100-ca", "hi3798mv200", "hi3798mv300",
+ "hi3798mv310",
+]);
+
+function needsFrameBlast(chip) {
+ return FRAME_BLAST_SOCS.has(chip);
+}
+
// ================================================================
// OpenIPC U-Boot asset resolution
//
@@ -308,6 +340,7 @@ if (typeof module !== 'undefined' && module.exports) {
CRC_TABLE, calcCrc, appendCrc, appendCrcLE, verifyCrc,
buildHeadFrame, buildDataFrame, buildTailFrame, chunkData,
parseCv6xxBoot, V500_SOCS, CV6XX_SOCS,
+ FRAME_BLAST_SOCS, needsFrameBlast,
FW_RELEASE_API, FW_DIRECT_BASE, FW_ASSET_RE, FW_PROXIES,
CHIP_FW_ALIAS, fwNameForChip, parseReleaseAssets, parseDigest,
fwSourceUrls, bytesToHex, verifyFirmwareBytes,