fix(provisioning): wait for SPAXEL READY before sending payload over serial
The firmware prints 'SPAXEL READY <MAC>' a few seconds after the flash reset,
once the UART driver has initialised. Previously the browser sent the JSON
payload immediately when the port opened — the bytes arrived while the device
was still booting and were lost, causing a 15 s timeout.
New flow:
1. Open port (with retries for USB re-enumeration)
2. Read serial stream waiting for 'SPAXEL READY' line (up to 30 s)
3. Only then send the JSON provisioning payload
4. Wait for firmware's {"ok":true} acknowledgment (up to 10 s)
The MAC extracted from 'SPAXEL READY <MAC>' is used as a fallback in case
the firmware's JSON response is parsed before the mac field is available.
This commit is contained in:
parent
156ac98c37
commit
deaf2b5653
1 changed files with 129 additions and 30 deletions
|
|
@ -758,8 +758,6 @@
|
|||
// Firmware expects {"provision": {...}} format
|
||||
var wrappedPayload = { provision: payload };
|
||||
|
||||
// Prefer the port the user explicitly selected in the Connect step. Fall back to
|
||||
// whatever the browser has previously authorized if state.port was somehow lost.
|
||||
addProvLog('log', 'Looking up serial port (state.port=' + (state.port ? 'set' : 'null') + ')');
|
||||
var port = state.port || await getAuthorizedPort();
|
||||
if (!port) {
|
||||
|
|
@ -768,8 +766,8 @@
|
|||
}
|
||||
addProvLog('log', 'Port found — opening at ' + CONFIG.serialBaudRate + ' baud');
|
||||
|
||||
// The port may be closed (esptool closes it after flashing). Open it with retries
|
||||
// to handle the brief gap while the device reboots and re-enumerates.
|
||||
// The port may be closed (esptool closes it after flashing). Retry while the device
|
||||
// is still in the middle of its USB re-enumeration after the reset.
|
||||
var opened = false;
|
||||
for (var attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
|
|
@ -778,14 +776,12 @@
|
|||
addProvLog('log', 'Port opened on attempt ' + (attempt + 1));
|
||||
break;
|
||||
} catch (e) {
|
||||
// Already open → proceed
|
||||
if (e && (e.message || '').toLowerCase().includes('already open')) {
|
||||
opened = true;
|
||||
addProvLog('log', 'Port was already open — proceeding');
|
||||
break;
|
||||
}
|
||||
addProvLog('warn', 'Open attempt ' + (attempt + 1) + ' failed: ' + (e.message || e));
|
||||
// Device not ready yet — wait and retry
|
||||
if (attempt < 4) {
|
||||
setProvStatus('Waiting for device to boot... (attempt ' + (attempt + 2) + '/5)');
|
||||
await new Promise(function (r) { setTimeout(r, 1000); });
|
||||
|
|
@ -795,36 +791,139 @@
|
|||
|
||||
if (!opened) {
|
||||
addProvLog('error', 'Could not open port after 5 attempts');
|
||||
throw new UserError(
|
||||
'Could not open serial port. Unplug and replug the USB cable, then try again.'
|
||||
);
|
||||
throw new UserError('Could not open serial port. Unplug and replug the USB cable, then try again.');
|
||||
}
|
||||
|
||||
addProvLog('log', 'Sending JSON payload: ' + JSON.stringify(wrappedPayload).substring(0, 120) + '...');
|
||||
setProvStatus('Waiting for device acknowledgment...');
|
||||
var response = await sendSerialJSONAndWaitForResponse(port, wrappedPayload, 15000);
|
||||
addProvLog('log', 'Serial response: ' + (response ? JSON.stringify(response) : '(none — timeout)'));
|
||||
try { await port.close(); } catch (_) {}
|
||||
// Set up bidirectional streams. The reader is opened immediately so we don't
|
||||
// miss the "SPAXEL READY" line that the firmware prints at boot.
|
||||
var decoder = new TextDecoderStream();
|
||||
var readableClosed = port.readable.pipeTo(decoder.writable);
|
||||
var reader = decoder.readable.getReader();
|
||||
|
||||
if (!response) {
|
||||
throw new UserError(
|
||||
'No response from device. Make sure the board finished booting and try again. ' +
|
||||
'The provisioning window is open for 2 minutes after first boot.'
|
||||
);
|
||||
}
|
||||
if (response.ok === false) {
|
||||
var errorMsg = response.error || 'Unknown error';
|
||||
addProvLog('error', 'Device rejected provisioning: ' + errorMsg);
|
||||
if (errorMsg === 'missing_provision_key') {
|
||||
throw new UserError('Firmware communication error. Please try again.');
|
||||
var encoder = new TextEncoderStream();
|
||||
var writableClosed = encoder.readable.pipeTo(port.writable);
|
||||
var writer = encoder.writable.getWriter();
|
||||
|
||||
var mac = null;
|
||||
try {
|
||||
// Phase 1: wait for "SPAXEL READY <MAC>" from the firmware (up to 30 s).
|
||||
// The firmware prints this immediately after its UART driver initialises, a
|
||||
// few seconds after the reset that follows the flash. If we send the payload
|
||||
// before this line appears the bytes are lost because the device UART isn't
|
||||
// ready yet.
|
||||
setProvStatus('Waiting for device to boot...');
|
||||
addProvLog('log', 'Waiting for SPAXEL READY signal (up to 30 s)...');
|
||||
|
||||
var buffer = '';
|
||||
var readyReceived = false;
|
||||
var readyDeadline = Date.now() + 30000;
|
||||
|
||||
outer: while (Date.now() < readyDeadline) {
|
||||
var remaining = readyDeadline - Date.now();
|
||||
var result;
|
||||
try {
|
||||
result = await Promise.race([
|
||||
reader.read(),
|
||||
new Promise(function (_, reject) {
|
||||
setTimeout(function () { reject(new Error('timeout')); }, remaining + 50);
|
||||
})
|
||||
]);
|
||||
} catch (e) {
|
||||
break; // deadline expired
|
||||
}
|
||||
if (result.done) break;
|
||||
buffer += result.value;
|
||||
|
||||
var nl;
|
||||
while ((nl = buffer.indexOf('\n')) !== -1) {
|
||||
var line = buffer.substring(0, nl).trim();
|
||||
buffer = buffer.substring(nl + 1);
|
||||
if (line) addProvLog('log', 'Device: ' + line);
|
||||
if (line.startsWith('SPAXEL READY')) {
|
||||
readyReceived = true;
|
||||
// Extract MAC that the firmware appends after the keyword
|
||||
var parts = line.split(' ');
|
||||
if (parts.length >= 3) mac = parts[parts.length - 1];
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMsg === 'nvs_write_failed') {
|
||||
throw new UserError('Failed to save configuration to device. Please try again.');
|
||||
|
||||
if (!readyReceived) {
|
||||
throw new UserError(
|
||||
'Device did not become ready after flashing. ' +
|
||||
'Unplug and replug the USB cable, then try again.'
|
||||
);
|
||||
}
|
||||
throw new UserError('Provisioning failed: ' + errorMsg);
|
||||
|
||||
addProvLog('log', 'SPAXEL READY received (MAC: ' + (mac || 'unknown') + ') — sending payload');
|
||||
setProvStatus('Sending configuration to device...');
|
||||
addProvLog('log', 'Payload: ' + JSON.stringify(wrappedPayload).substring(0, 120) + '...');
|
||||
|
||||
// Phase 2: send the JSON payload now that the firmware is listening.
|
||||
await writer.write(JSON.stringify(wrappedPayload) + '\n');
|
||||
writer.close();
|
||||
await writableClosed;
|
||||
|
||||
// Phase 3: wait for the firmware's JSON acknowledgment (up to 10 s).
|
||||
setProvStatus('Waiting for device acknowledgment...');
|
||||
var response = null;
|
||||
var respDeadline = Date.now() + 10000;
|
||||
|
||||
outer2: while (Date.now() < respDeadline) {
|
||||
var remaining = respDeadline - Date.now();
|
||||
var result;
|
||||
try {
|
||||
result = await Promise.race([
|
||||
reader.read(),
|
||||
new Promise(function (_, reject) {
|
||||
setTimeout(function () { reject(new Error('timeout')); }, remaining + 50);
|
||||
})
|
||||
]);
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
if (result.done) break;
|
||||
buffer += result.value;
|
||||
|
||||
var nl;
|
||||
while ((nl = buffer.indexOf('\n')) !== -1) {
|
||||
var line = buffer.substring(0, nl).trim();
|
||||
buffer = buffer.substring(nl + 1);
|
||||
if (line) addProvLog('log', 'Device: ' + line);
|
||||
if (line.length > 0) {
|
||||
try {
|
||||
response = JSON.parse(line);
|
||||
break outer2;
|
||||
} catch (e) { /* not JSON — keep reading */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addProvLog('log', 'Serial response: ' + (response ? JSON.stringify(response) : '(none — timeout)'));
|
||||
|
||||
if (!response) {
|
||||
throw new UserError(
|
||||
'No response from device after sending configuration. ' +
|
||||
'The provisioning window is open for 2 minutes after first boot.'
|
||||
);
|
||||
}
|
||||
if (response.ok === false) {
|
||||
var errorMsg = response.error || 'Unknown error';
|
||||
addProvLog('error', 'Device rejected provisioning: ' + errorMsg);
|
||||
if (errorMsg === 'missing_provision_key') throw new UserError('Firmware communication error. Please try again.');
|
||||
if (errorMsg === 'nvs_write_failed') throw new UserError('Failed to save configuration to device. Please try again.');
|
||||
throw new UserError('Provisioning failed: ' + errorMsg);
|
||||
}
|
||||
|
||||
addProvLog('log', 'Provisioning acknowledged — MAC: ' + (response.mac || mac || '(unknown)'));
|
||||
return response.mac || mac;
|
||||
|
||||
} finally {
|
||||
try { reader.cancel(); } catch (_) {}
|
||||
try { await readableClosed.catch(function () {}); } catch (_) {}
|
||||
try { await port.close(); } catch (_) {}
|
||||
}
|
||||
addProvLog('log', 'Provisioning acknowledged by device — MAC: ' + (response.mac || '(unknown)'));
|
||||
return response.mac;
|
||||
}
|
||||
|
||||
function showFormError(id, msg) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue