@esp-vision/device-core 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/NOTICE +8 -0
- package/README.md +25 -8
- package/package.json +4 -3
- package/schemas/device-actions-v1.json +1 -1
- package/scripts/generate-action-schema.mjs +2 -1
- package/scripts/npm-command.mjs +17 -0
- package/scripts/release-npm.mjs +18 -1
- package/scripts/version-policy.mjs +71 -0
- package/src/browser.js +6 -2
- package/src/browser_rom.js +208 -4
- package/src/browser_rom_compat.js +86 -59
- package/src/browser_serial.js +130 -0
- package/src/device.js +5 -4
- package/src/device_bindings.js +5 -4
- package/src/device_capture.js +11 -0
- package/src/device_operations.js +3 -1
- package/src/device_session.js +263 -10
- package/src/evmux.js +43 -4
- package/src/firmware_flash.js +1 -1
- package/src/jpeg_validate.js +132 -0
- package/src/node_inventory.js +1 -1
- package/src/node_rom.js +92 -29
- package/src/node_rom.py +134 -6
- package/src/node_rom_port.js +219 -0
- package/src/node_serial.py +122 -13
- package/src/node_transport.js +136 -21
- package/src/rom_md5.js +30 -0
- package/tests/check-browser.mjs +76 -123
- package/tests/check-package.mjs +8 -7
- package/tests/device-checks.mjs +6 -2
- package/tests/fixtures/device-checks.py +11 -1
- package/tests/jpeg-fixtures.mjs +39 -0
- package/tests/test-browser-rom.mjs +204 -67
- package/tests/test-browser-serial.mjs +143 -0
- package/tests/test-device-api.mjs +40 -7
- package/tests/test-device-functional-checks.mjs +12 -2
- package/tests/test-device-operations-api.mjs +4 -3
- package/tests/test-device-reset.mjs +292 -0
- package/tests/test-firmware-flash.mjs +1 -1
- package/tests/test-host-checks.mjs +47 -0
- package/tests/test-jpeg-validation.mjs +233 -0
- package/tests/test-node-rom-port.mjs +385 -0
- package/tests/test-node-rom.mjs +231 -0
- package/tests/test-node-serial.mjs +436 -1
- package/tests/test-npm-release-policy.mjs +56 -0
- package/tests/test-observed-capture.mjs +131 -0
- package/tests/test-uart-baud.mjs +180 -0
- package/tests/test-version-policy.mjs +37 -0
package/src/node_serial.py
CHANGED
|
@@ -4,12 +4,64 @@
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import signal
|
|
7
|
+
import struct
|
|
7
8
|
import sys
|
|
8
9
|
import threading
|
|
9
10
|
|
|
10
11
|
import serial
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def _report_serial_error(stage, error):
|
|
15
|
+
"""Emit bounded OS diagnostics, never exception text or transferred bytes."""
|
|
16
|
+
number = getattr(error, 'errno', None)
|
|
17
|
+
winerror = getattr(error, 'winerror', None)
|
|
18
|
+
cause = error.__cause__ or error.__context__
|
|
19
|
+
if not isinstance(number, int) and cause is not None:
|
|
20
|
+
number = getattr(cause, 'errno', None)
|
|
21
|
+
if not isinstance(winerror, int) and cause is not None:
|
|
22
|
+
winerror = getattr(cause, 'winerror', None)
|
|
23
|
+
def bounded_number(value):
|
|
24
|
+
if isinstance(value, int) and not isinstance(value, bool) and -0x80000000 <= value <= 0xffffffff:
|
|
25
|
+
return value
|
|
26
|
+
return None
|
|
27
|
+
number = bounded_number(number)
|
|
28
|
+
winerror = bounded_number(winerror)
|
|
29
|
+
# Pyserial sometimes wraps a Windows API failure without preserving errno.
|
|
30
|
+
# Only recognize fixed API names/messages; arbitrary exception text may
|
|
31
|
+
# contain application bytes and must not cross this diagnostic channel.
|
|
32
|
+
text = str(error)[:512]
|
|
33
|
+
message = 'Serial ' + stage + ' failed'
|
|
34
|
+
if number is not None and 0 < number < 4096:
|
|
35
|
+
message = os.strerror(number)
|
|
36
|
+
else:
|
|
37
|
+
for needle, safe in (
|
|
38
|
+
('ClearCommError', 'ClearCommError failed'),
|
|
39
|
+
('GetOverlappedResult', 'GetOverlappedResult failed'),
|
|
40
|
+
('ReadFile', 'ReadFile failed'),
|
|
41
|
+
('WriteFile', 'WriteFile failed'),
|
|
42
|
+
('readiness to read but returned no data', 'Device reports readiness to read but returned no data'),
|
|
43
|
+
('write timeout', 'Serial write timed out'),
|
|
44
|
+
('Zero-byte serial write', 'Serial write returned zero bytes'),
|
|
45
|
+
):
|
|
46
|
+
if needle.lower() in text.lower():
|
|
47
|
+
message = safe
|
|
48
|
+
break
|
|
49
|
+
record = {'kind': 'serial.error', 'stage': stage, 'type': type(error).__name__[:64],
|
|
50
|
+
'errno': number, 'winerror': winerror, 'message': message[:160]}
|
|
51
|
+
# One small line keeps READY/control records intact when the reader thread
|
|
52
|
+
# fails concurrently with the main thread. stderr failure is terminal too.
|
|
53
|
+
line = (json.dumps(record, ensure_ascii=True) + '\n').encode()
|
|
54
|
+
try:
|
|
55
|
+
offset = 0
|
|
56
|
+
while offset < len(line):
|
|
57
|
+
count = os.write(2, line[offset:])
|
|
58
|
+
if count <= 0:
|
|
59
|
+
break
|
|
60
|
+
offset += count
|
|
61
|
+
except OSError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
13
65
|
def _configure_serial_exclusive(port, platform=os.name):
|
|
14
66
|
# pyserial implements this as a non-blocking advisory lock on POSIX. The
|
|
15
67
|
# Windows backend does not support this option, so leave its behavior alone.
|
|
@@ -17,6 +69,18 @@ def _configure_serial_exclusive(port, platform=os.name):
|
|
|
17
69
|
port.exclusive = True
|
|
18
70
|
|
|
19
71
|
|
|
72
|
+
def _read_exact(stream, size):
|
|
73
|
+
data = bytearray()
|
|
74
|
+
while len(data) < size:
|
|
75
|
+
chunk = stream.read(size - len(data))
|
|
76
|
+
if not chunk:
|
|
77
|
+
if data:
|
|
78
|
+
raise OSError('Truncated serial bridge record')
|
|
79
|
+
return None
|
|
80
|
+
data.extend(chunk)
|
|
81
|
+
return bytes(data)
|
|
82
|
+
|
|
83
|
+
|
|
20
84
|
def main():
|
|
21
85
|
if sys.argv[1] == '--list':
|
|
22
86
|
from serial.tools.list_ports import comports
|
|
@@ -37,16 +101,24 @@ def main():
|
|
|
37
101
|
raise SystemExit(0)
|
|
38
102
|
|
|
39
103
|
def receive():
|
|
40
|
-
|
|
41
|
-
|
|
104
|
+
while not stopped.is_set():
|
|
105
|
+
try:
|
|
42
106
|
data = port.read(8192)
|
|
43
|
-
|
|
107
|
+
except (OSError, serial.SerialException) as error:
|
|
108
|
+
if not stopped.is_set():
|
|
109
|
+
_report_serial_error('read', error)
|
|
110
|
+
os._exit(2)
|
|
111
|
+
return
|
|
112
|
+
if data:
|
|
113
|
+
try:
|
|
44
114
|
offset = 0
|
|
45
115
|
while offset < len(data):
|
|
46
116
|
offset += os.write(1, data[offset:])
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
117
|
+
except OSError as error:
|
|
118
|
+
if not stopped.is_set():
|
|
119
|
+
_report_serial_error('stdout', error)
|
|
120
|
+
os._exit(2)
|
|
121
|
+
return
|
|
50
122
|
|
|
51
123
|
signal.signal(signal.SIGTERM, terminate)
|
|
52
124
|
signal.signal(signal.SIGINT, terminate)
|
|
@@ -55,13 +127,50 @@ def main():
|
|
|
55
127
|
thread = threading.Thread(target=receive, daemon=True)
|
|
56
128
|
thread.start()
|
|
57
129
|
print('READY', file=sys.stderr, flush=True)
|
|
58
|
-
while
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
130
|
+
while header := _read_exact(sys.stdin.buffer, 5):
|
|
131
|
+
kind, size = struct.unpack('>BI', header)
|
|
132
|
+
if kind not in (0, 1, 2) or size > 1048576:
|
|
133
|
+
raise OSError('Invalid serial bridge record')
|
|
134
|
+
data = _read_exact(sys.stdin.buffer, size)
|
|
135
|
+
if data is None:
|
|
136
|
+
raise OSError('Truncated serial bridge record')
|
|
137
|
+
if kind == 1:
|
|
138
|
+
request = json.loads(data)
|
|
139
|
+
try:
|
|
140
|
+
baud = request['baudRate']
|
|
141
|
+
if not isinstance(baud, int) or isinstance(baud, bool) or baud <= 0:
|
|
142
|
+
raise ValueError('Invalid baudrate')
|
|
143
|
+
port.flush()
|
|
144
|
+
port.baudrate = baud
|
|
145
|
+
response = {'id': request['id'], 'ok': True, 'baudRate': baud}
|
|
146
|
+
except (KeyError, ValueError, OSError, serial.SerialException):
|
|
147
|
+
response = {'id': request.get('id'), 'ok': False}
|
|
148
|
+
print(json.dumps(response), file=sys.stderr, flush=True)
|
|
149
|
+
continue
|
|
150
|
+
if kind == 2:
|
|
151
|
+
request = json.loads(data)
|
|
152
|
+
try:
|
|
153
|
+
request_id = request['id']
|
|
154
|
+
if not isinstance(request_id, int) or isinstance(request_id, bool) or request_id <= 0:
|
|
155
|
+
raise ValueError('Invalid drain request ID')
|
|
156
|
+
# stdin records are processed in order. All preceding byte
|
|
157
|
+
# records have completed port.write before this drain ACK.
|
|
158
|
+
port.flush()
|
|
159
|
+
response = {'id': request_id, 'ok': True, 'drained': True}
|
|
160
|
+
except (KeyError, ValueError, OSError, serial.SerialException):
|
|
161
|
+
response = {'id': request.get('id'), 'ok': False}
|
|
162
|
+
print(json.dumps(response), file=sys.stderr, flush=True)
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
offset = 0
|
|
166
|
+
while offset < len(data):
|
|
167
|
+
written = port.write(data[offset:])
|
|
168
|
+
if not written:
|
|
169
|
+
raise OSError('Zero-byte serial write')
|
|
170
|
+
offset += written
|
|
171
|
+
except (OSError, serial.SerialException) as error:
|
|
172
|
+
_report_serial_error('write', error)
|
|
173
|
+
raise SystemExit(2) from None
|
|
65
174
|
finally:
|
|
66
175
|
stopped.set()
|
|
67
176
|
port.close()
|
package/src/node_transport.js
CHANGED
|
@@ -39,62 +39,177 @@ export function tcpTransport(options) {
|
|
|
39
39
|
|
|
40
40
|
export function serialTransport(options) {
|
|
41
41
|
const events = subscribers()
|
|
42
|
-
let child
|
|
42
|
+
let child, connection, lastError = null, controlId = 0, baudRate = 115200
|
|
43
|
+
const pending = new Map()
|
|
44
|
+
const rejectControls = error => {
|
|
45
|
+
for (const request of pending.values()) request.reject(error)
|
|
46
|
+
pending.clear()
|
|
47
|
+
}
|
|
48
|
+
const send = (kind, bytes) => new Promise((resolveWrite, reject) => {
|
|
49
|
+
if (!child?.stdin.writable) return reject(fail('SERIAL_CLOSED', 'Serial adapter closed'))
|
|
50
|
+
const record = Buffer.allocUnsafe(5 + bytes.byteLength)
|
|
51
|
+
record.writeUInt8(kind, 0)
|
|
52
|
+
record.writeUInt32BE(bytes.byteLength, 1)
|
|
53
|
+
record.set(bytes, 5)
|
|
54
|
+
child.stdin.write(record, error => error ? reject(fail('SERIAL_WRITE', 'Serial write failed')) : resolveWrite())
|
|
55
|
+
})
|
|
43
56
|
return {
|
|
44
57
|
subscribe: events.subscribe,
|
|
45
58
|
connect({ signal }) {
|
|
46
59
|
return new Promise((resolveConnect, reject) => {
|
|
47
60
|
const adapter = fileURLToPath(new URL('./node_serial.py', import.meta.url))
|
|
48
|
-
child = spawn(options.python, ['-u', adapter, options.serial], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
49
|
-
|
|
61
|
+
const bridge = child = spawn(options.python, ['-u', adapter, options.serial], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
62
|
+
const current = connection = { intentional: false }
|
|
63
|
+
lastError = null
|
|
64
|
+
let adapterCause = null, processCause = null
|
|
65
|
+
const safeCause = value => {
|
|
66
|
+
if (value?.kind !== 'serial.error' || !['read', 'write', 'stdout'].includes(value.stage)
|
|
67
|
+
|| typeof value.type !== 'string' || typeof value.message !== 'string') return null
|
|
68
|
+
const integer = n => Number.isSafeInteger(n) && n >= -0x80000000 && n <= 0xffffffff ? n : null
|
|
69
|
+
return { stage: value.stage, type: value.type.slice(0, 64), errno: integer(value.errno),
|
|
70
|
+
winerror: integer(value.winerror), message: value.message.replace(/[\r\n\x00-\x1f\x7f]/g, ' ').slice(0, 160) }
|
|
71
|
+
}
|
|
72
|
+
const errorDetails = (exitCode = null, signal = null) => ({
|
|
73
|
+
...(adapterCause ?? processCause ?? { stage: 'process', type: 'ProcessExit', errno: null, winerror: null,
|
|
74
|
+
message: 'Serial adapter exited unexpectedly' }),
|
|
75
|
+
exitCode: Number.isInteger(exitCode) ? exitCode : null,
|
|
76
|
+
signal: typeof signal === 'string' ? signal.slice(0, 32) : null,
|
|
77
|
+
})
|
|
78
|
+
let ready = false, settled = false, runtimeErrorReported = false, stderr = '', lines = ''
|
|
50
79
|
const abort = () => {
|
|
51
|
-
|
|
80
|
+
current.intentional = true
|
|
81
|
+
bridge.kill('SIGTERM')
|
|
52
82
|
if (!settled) {
|
|
53
83
|
settled = true
|
|
54
84
|
reject(fail('CANCELLED', 'Serial connection cancelled'))
|
|
55
85
|
}
|
|
56
86
|
}
|
|
57
87
|
const cleanup = () => signal.removeEventListener('abort', abort)
|
|
58
|
-
const serialError =
|
|
59
|
-
const code = ['SERIAL_ACCESS_DENIED', 'SERIAL_BUSY'].find(code => stderr.includes(code)) ?? 'SERIAL_ERROR'
|
|
60
|
-
|
|
88
|
+
const serialError = details => {
|
|
89
|
+
const code = ready ? 'SERIAL_ERROR' : ['SERIAL_ACCESS_DENIED', 'SERIAL_BUSY'].find(code => stderr.includes(code)) ?? 'SERIAL_ERROR'
|
|
90
|
+
const message = ready && details?.stage !== 'process'
|
|
91
|
+
? `Serial adapter ${details.stage} failed: ${details.message}`
|
|
92
|
+
: 'Serial adapter failed; ensure Python, pyserial and port access are available'
|
|
93
|
+
const error = fail(code, message)
|
|
94
|
+
error.details = details ?? errorDetails()
|
|
95
|
+
error.cause = Object.assign(new Error(error.details.message), { ...error.details, name: error.details.type })
|
|
96
|
+
return error
|
|
61
97
|
}
|
|
62
|
-
const failBeforeReady =
|
|
98
|
+
const failBeforeReady = details => {
|
|
63
99
|
if (settled) return
|
|
64
100
|
settled = true
|
|
65
101
|
cleanup()
|
|
66
|
-
const error = serialError()
|
|
102
|
+
const error = serialError(details)
|
|
67
103
|
reject(error)
|
|
68
104
|
events.emit('error', error)
|
|
69
105
|
}
|
|
70
|
-
const reportRuntimeError =
|
|
106
|
+
const reportRuntimeError = details => {
|
|
71
107
|
if (runtimeErrorReported) return
|
|
72
108
|
runtimeErrorReported = true
|
|
73
|
-
|
|
109
|
+
const error = serialError(details)
|
|
110
|
+
lastError = { ...error.details }
|
|
111
|
+
events.emit('error', error)
|
|
74
112
|
}
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
const recordProcessError = (stage, error) => {
|
|
114
|
+
processCause = { stage, type: 'ProcessError', errno: null, winerror: null,
|
|
115
|
+
message: typeof error?.code === 'string' && /^[A-Z0-9_]{1,32}$/.test(error.code)
|
|
116
|
+
? `Serial adapter ${stage} failed (${error.code})` : `Serial adapter ${stage} failed` }
|
|
117
|
+
}
|
|
118
|
+
child.on('error', error => recordProcessError('process', error))
|
|
119
|
+
child.stdin.on('error', error => recordProcessError('stdin', error))
|
|
77
120
|
child.stdout.on('data', bytes => events.emit('data', bytes))
|
|
78
121
|
child.stderr.on('data', bytes => {
|
|
79
|
-
if (ready) return
|
|
80
122
|
// Do not print Python errors: they can contain host paths or device data.
|
|
81
123
|
stderr = (stderr + bytes.toString()).slice(-128)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
124
|
+
lines = (lines + bytes.toString()).slice(-8192)
|
|
125
|
+
for (let newline; (newline = lines.indexOf('\n')) !== -1;) {
|
|
126
|
+
const line = lines.slice(0, newline).trim()
|
|
127
|
+
lines = lines.slice(newline + 1)
|
|
128
|
+
if (line === 'READY' && !ready) {
|
|
129
|
+
ready = true
|
|
130
|
+
settled = true
|
|
131
|
+
baudRate = 115200
|
|
132
|
+
cleanup()
|
|
133
|
+
resolveConnect()
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
let response
|
|
137
|
+
try { response = JSON.parse(line) } catch { continue }
|
|
138
|
+
const diagnostic = safeCause(response)
|
|
139
|
+
if (diagnostic) { adapterCause = diagnostic; continue }
|
|
140
|
+
const request = pending.get(response.id)
|
|
141
|
+
if (!request) continue
|
|
142
|
+
pending.delete(response.id)
|
|
143
|
+
if (request.kind === 'drain') {
|
|
144
|
+
if (response.ok === true && response.drained === true) request.resolve()
|
|
145
|
+
else request.reject(fail('SERIAL_DRAIN_FAILED', 'Serial adapter could not drain pending output'))
|
|
146
|
+
} else if (response.ok && response.baudRate === request.baudRate) {
|
|
147
|
+
baudRate = response.baudRate
|
|
148
|
+
request.resolve()
|
|
149
|
+
} else request.reject(fail('SERIAL_BAUD_UNSUPPORTED', 'Serial adapter rejected the requested baudrate'))
|
|
87
150
|
}
|
|
88
151
|
})
|
|
89
152
|
// Wait for close rather than exit: stdout/stderr may still have buffered
|
|
90
153
|
// data after exit, including the adapter's stable SERIAL_BUSY marker.
|
|
91
|
-
child.on('close', () => {
|
|
154
|
+
child.on('close', (code, signal) => {
|
|
155
|
+
cleanup()
|
|
156
|
+
if (connection !== current) return
|
|
157
|
+
const details = errorDetails(code, signal)
|
|
158
|
+
if (current.intentional) {
|
|
159
|
+
const error = fail('SERIAL_CLOSED', 'Serial adapter closed')
|
|
160
|
+
rejectControls(error)
|
|
161
|
+
if (!settled) { settled = true; reject(error) }
|
|
162
|
+
} else {
|
|
163
|
+
lastError = { ...details }
|
|
164
|
+
const error = serialError(details)
|
|
165
|
+
rejectControls(error)
|
|
166
|
+
if (!ready) failBeforeReady(details)
|
|
167
|
+
else reportRuntimeError(details)
|
|
168
|
+
}
|
|
169
|
+
events.emit('close')
|
|
170
|
+
})
|
|
92
171
|
signal.addEventListener('abort', abort, { once: true })
|
|
93
172
|
if (signal.aborted) abort()
|
|
94
173
|
})
|
|
95
174
|
},
|
|
96
|
-
writeBytes(bytes) { return
|
|
175
|
+
writeBytes(bytes) { return send(0, bytes) },
|
|
176
|
+
drain({ signal } = {}) {
|
|
177
|
+
if (signal?.aborted) return Promise.reject(signal.reason ?? fail('CANCELLED', 'Serial drain cancelled'))
|
|
178
|
+
const id = ++controlId
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener('abort', abort); pending.delete(id) }
|
|
181
|
+
const abort = () => { cleanup(); reject(signal.reason ?? fail('CANCELLED', 'Serial drain cancelled')) }
|
|
182
|
+
const timer = setTimeout(() => { cleanup(); reject(fail('SERIAL_DRAIN_TIMEOUT', 'Serial drain timed out')) }, 2000)
|
|
183
|
+
pending.set(id, {
|
|
184
|
+
kind: 'drain',
|
|
185
|
+
resolve() { cleanup(); resolve() },
|
|
186
|
+
reject(error) { cleanup(); reject(error) },
|
|
187
|
+
})
|
|
188
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
189
|
+
send(2, Buffer.from(JSON.stringify({ id }))).catch(error => pending.get(id)?.reject(error))
|
|
190
|
+
})
|
|
191
|
+
},
|
|
192
|
+
setBaudRate(speed, { signal } = {}) {
|
|
193
|
+
if (!Number.isSafeInteger(speed) || speed <= 0) return Promise.reject(new RangeError('baudRate must be a positive integer'))
|
|
194
|
+
if (signal?.aborted) return Promise.reject(signal.reason ?? fail('CANCELLED', 'Serial speed change cancelled'))
|
|
195
|
+
const id = ++controlId
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener('abort', abort); pending.delete(id) }
|
|
198
|
+
const abort = () => { cleanup(); reject(signal.reason ?? fail('CANCELLED', 'Serial speed change cancelled')) }
|
|
199
|
+
const timer = setTimeout(() => { cleanup(); reject(fail('SERIAL_BAUD_TIMEOUT', 'Serial speed change timed out')) }, 2000)
|
|
200
|
+
pending.set(id, {
|
|
201
|
+
baudRate: speed,
|
|
202
|
+
resolve() { cleanup(); resolve() },
|
|
203
|
+
reject(error) { cleanup(); reject(error) },
|
|
204
|
+
})
|
|
205
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
206
|
+
send(1, Buffer.from(JSON.stringify({ id, baudRate: speed }))).catch(error => pending.get(id)?.reject(error))
|
|
207
|
+
})
|
|
208
|
+
},
|
|
209
|
+
getDiagnostics() { return { baudRate, ...(lastError ? { lastError: { ...lastError } } : {}) } },
|
|
97
210
|
async disconnect() {
|
|
211
|
+
if (connection) connection.intentional = true
|
|
212
|
+
rejectControls(fail('SERIAL_CLOSED', 'Serial adapter closed'))
|
|
98
213
|
if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return
|
|
99
214
|
await new Promise(resolveExit => {
|
|
100
215
|
const timer = setTimeout(() => { child.kill('SIGKILL') }, 2000)
|
package/src/rom_md5.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// MD5 verifies the flasher stub's transport digest; approved firmware still uses
|
|
4
|
+
// full SHA-256. Web Crypto intentionally does not offer the legacy MD5 algorithm.
|
|
5
|
+
export function romMd5(bytes) {
|
|
6
|
+
const input = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64)
|
|
7
|
+
input.set(bytes); input[bytes.length] = 0x80
|
|
8
|
+
const view = new DataView(input.buffer)
|
|
9
|
+
view.setUint32(input.length - 8, (bytes.length * 8) >>> 0, true)
|
|
10
|
+
view.setUint32(input.length - 4, Math.floor(bytes.length / 0x20000000), true)
|
|
11
|
+
const rotations = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]]
|
|
12
|
+
const state = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
|
|
13
|
+
for (let offset = 0; offset < input.length; offset += 64) {
|
|
14
|
+
let [a, b, c, d] = state
|
|
15
|
+
for (let i = 0; i < 64; i++) {
|
|
16
|
+
const round = i >>> 4
|
|
17
|
+
const f = round === 0 ? (b & c) | (~b & d) : round === 1 ? (d & b) | (~d & c) : round === 2 ? b ^ c ^ d : c ^ (b | ~d)
|
|
18
|
+
const index = round === 0 ? i : round === 1 ? (5 * i + 1) % 16 : round === 2 ? (3 * i + 5) % 16 : (7 * i) % 16
|
|
19
|
+
const value = (a + f + Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000) + view.getUint32(offset + index * 4, true)) | 0
|
|
20
|
+
const shift = rotations[round][i % 4]
|
|
21
|
+
const next = (b + ((value << shift) | (value >>> (32 - shift)))) | 0
|
|
22
|
+
a = d; d = c; c = b; b = next
|
|
23
|
+
}
|
|
24
|
+
state[0] = (state[0] + a) >>> 0; state[1] = (state[1] + b) >>> 0
|
|
25
|
+
state[2] = (state[2] + c) >>> 0; state[3] = (state[3] + d) >>> 0
|
|
26
|
+
}
|
|
27
|
+
const output = new Uint8Array(16), result = new DataView(output.buffer)
|
|
28
|
+
state.forEach((value, index) => result.setUint32(index * 4, value, true))
|
|
29
|
+
return output
|
|
30
|
+
}
|