@esp-vision/device-core 0.4.2 → 0.5.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 +25 -2
- package/README.md +23 -13
- package/package.json +1 -5
- package/schemas/device-actions-v1.json +1 -1
- package/src/device.js +17 -6
- package/src/device_files.js +5 -4
- package/src/device_python.js +4 -3
- package/src/device_session.js +127 -43
- package/src/evrepl.js +196 -22
- package/src/node.js +10 -0
- package/src/node_inventory.js +7 -4
- package/src/node_serial.py +9 -1
- package/src/node_transport.js +36 -7
- package/tests/check-package.mjs +6 -9
- package/tests/device-checks.mjs +2 -2
- package/tests/fixtures/device-checks.py +205 -1
- package/tests/test-device-api.mjs +96 -6
- package/tests/test-device-execution.mjs +235 -16
- package/tests/test-device-files-api.mjs +85 -1
- package/tests/test-device-functional-checks.mjs +9 -0
- package/tests/test-device-hardware-cli.mjs +8 -0
- package/tests/test-device-python-api.mjs +29 -2
- package/tests/test-firmware-install.mjs +0 -17
- package/tests/test-node-adapter-assets.mjs +16 -0
- package/tests/test-node-inventory.mjs +136 -0
- package/tests/test-node-serial.mjs +104 -0
- package/src/cli.js +0 -185
- package/tests/test-cli-output.mjs +0 -31
- package/tests/test-cli-ports.mjs +0 -50
- package/tests/test-cli.mjs +0 -98
|
@@ -286,6 +286,205 @@ def display_stream(opts):
|
|
|
286
286
|
lcd.deinit()
|
|
287
287
|
|
|
288
288
|
|
|
289
|
+
def camera_display_lifecycle(opts):
|
|
290
|
+
"""Exercise camera/LCD coexistence, camera shutdown recovery, and restart."""
|
|
291
|
+
import gc, esp32, sensor, display, image
|
|
292
|
+
|
|
293
|
+
def dma_heaps():
|
|
294
|
+
return esp32.idf_heap_info(1 << 3) # ESP-IDF MALLOC_CAP_DMA.
|
|
295
|
+
|
|
296
|
+
lcd = None
|
|
297
|
+
frame = None
|
|
298
|
+
canvas = None
|
|
299
|
+
try:
|
|
300
|
+
sensor.shutdown()
|
|
301
|
+
gc.collect()
|
|
302
|
+
lcd = display.Display(backlight=100)
|
|
303
|
+
panel_width, panel_height = lcd.width(), lcd.height()
|
|
304
|
+
lcd.deinit()
|
|
305
|
+
lcd = None
|
|
306
|
+
gc.collect()
|
|
307
|
+
|
|
308
|
+
sensor.reset()
|
|
309
|
+
sensor.set_pixformat(sensor.RGB565)
|
|
310
|
+
sensor.set_framesize(sensor.QVGA)
|
|
311
|
+
sensor.skip_frames(n=3)
|
|
312
|
+
frame = sensor.snapshot()
|
|
313
|
+
dma_before = dma_heaps()
|
|
314
|
+
lcd = display.Display(backlight=100)
|
|
315
|
+
lcd.write(frame, fit=True)
|
|
316
|
+
dma_active = dma_heaps()
|
|
317
|
+
lcd.deinit()
|
|
318
|
+
lcd = None
|
|
319
|
+
frame = None
|
|
320
|
+
gc.collect()
|
|
321
|
+
|
|
322
|
+
sensor.shutdown()
|
|
323
|
+
gc.collect()
|
|
324
|
+
dma_shutdown = dma_heaps()
|
|
325
|
+
lcd = display.Display(backlight=100)
|
|
326
|
+
canvas = image.Image(32, 32, image.RGB565)
|
|
327
|
+
lcd.write(canvas, fit=True)
|
|
328
|
+
lcd.deinit()
|
|
329
|
+
lcd = None
|
|
330
|
+
canvas = None
|
|
331
|
+
gc.collect()
|
|
332
|
+
|
|
333
|
+
sensor.reset()
|
|
334
|
+
sensor.set_pixformat(sensor.RGB565)
|
|
335
|
+
sensor.set_framesize(sensor.QVGA)
|
|
336
|
+
sensor.skip_frames(n=3)
|
|
337
|
+
frame = sensor.snapshot()
|
|
338
|
+
restarted = (frame.width(), frame.height())
|
|
339
|
+
dma_restarted = dma_heaps()
|
|
340
|
+
return dict(width=panel_width, height=panel_height,
|
|
341
|
+
cameraWidth=restarted[0], cameraHeight=restarted[1],
|
|
342
|
+
dmaBefore=dma_before, dmaActive=dma_active,
|
|
343
|
+
dmaAfterShutdown=dma_shutdown, dmaAfterRestart=dma_restarted)
|
|
344
|
+
finally:
|
|
345
|
+
if lcd is not None:
|
|
346
|
+
lcd.deinit()
|
|
347
|
+
frame = None
|
|
348
|
+
canvas = None
|
|
349
|
+
sensor.shutdown()
|
|
350
|
+
gc.collect()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def image_copy_to_fb(opts):
|
|
354
|
+
"""The unsupported copy_to_fb keyword must be rejected consistently."""
|
|
355
|
+
import gc, image
|
|
356
|
+
|
|
357
|
+
canvas = None
|
|
358
|
+
try:
|
|
359
|
+
try:
|
|
360
|
+
image.Image(32, 32, image.RGB565, copy_to_fb=True)
|
|
361
|
+
except TypeError:
|
|
362
|
+
pass
|
|
363
|
+
else:
|
|
364
|
+
raise AssertionError('Image accepted unsupported copy_to_fb')
|
|
365
|
+
|
|
366
|
+
canvas = image.Image(32, 32, image.RGB565)
|
|
367
|
+
try:
|
|
368
|
+
canvas.to_rgb565(copy_to_fb=True)
|
|
369
|
+
except TypeError:
|
|
370
|
+
pass
|
|
371
|
+
else:
|
|
372
|
+
raise AssertionError('to_rgb565 accepted unsupported copy_to_fb')
|
|
373
|
+
canvas.draw_rectangle(0, 0, 32, 32, color=(255, 0, 0), fill=True)
|
|
374
|
+
assert canvas.size() == 32 * 32 * 2, 'unexpected RGB565 buffer size'
|
|
375
|
+
return dict(width=canvas.width(), height=canvas.height(), size=canvas.size())
|
|
376
|
+
finally:
|
|
377
|
+
canvas = None
|
|
378
|
+
gc.collect()
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def image_transform_argument(opts):
|
|
382
|
+
"""Transform is not part of the ESP-VISION Image API."""
|
|
383
|
+
import image
|
|
384
|
+
|
|
385
|
+
canvas = image.Image(32, 32, image.RGB565)
|
|
386
|
+
source = image.Image(8, 8, image.RGB565)
|
|
387
|
+
matrix = ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
|
|
388
|
+
for operation in (
|
|
389
|
+
lambda: canvas.copy(transform=matrix),
|
|
390
|
+
lambda: canvas.draw_image(source, transform=matrix),
|
|
391
|
+
):
|
|
392
|
+
try:
|
|
393
|
+
operation()
|
|
394
|
+
except TypeError as exc:
|
|
395
|
+
assert 'keyword' in str(exc), 'transform is accepted but unsupported'
|
|
396
|
+
else:
|
|
397
|
+
raise AssertionError('Image accepted unsupported transform keyword')
|
|
398
|
+
return dict(operations=2)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def sensor_snapshot_buffer(opts):
|
|
402
|
+
"""Validate the caller-owned writable buffer contract for sensor.snapshot."""
|
|
403
|
+
import gc, sensor
|
|
404
|
+
|
|
405
|
+
buffer = None
|
|
406
|
+
frame = None
|
|
407
|
+
try:
|
|
408
|
+
sensor.shutdown()
|
|
409
|
+
sensor.reset()
|
|
410
|
+
sensor.set_pixformat(sensor.RGB565)
|
|
411
|
+
sensor.set_framesize(sensor.QVGA)
|
|
412
|
+
sensor.skip_frames(n=3)
|
|
413
|
+
try:
|
|
414
|
+
sensor.snapshot(buffer=bytearray(16))
|
|
415
|
+
except TypeError:
|
|
416
|
+
pass
|
|
417
|
+
else:
|
|
418
|
+
raise AssertionError('sensor.snapshot unexpectedly accepted buffer keyword')
|
|
419
|
+
try:
|
|
420
|
+
sensor.snapshot(bytearray(16))
|
|
421
|
+
except ValueError as exc:
|
|
422
|
+
assert 'snapshot buffer too small' in str(exc), 'unexpected undersized buffer error: ' + str(exc)
|
|
423
|
+
else:
|
|
424
|
+
raise AssertionError('sensor.snapshot accepted an undersized buffer')
|
|
425
|
+
frame_bytes = 320 * 240 * 2
|
|
426
|
+
buffer = bytearray(frame_bytes)
|
|
427
|
+
frame = sensor.snapshot(buffer)
|
|
428
|
+
assert (frame.width(), frame.height(), frame.size()) == (320, 240, frame_bytes), 'unexpected caller-buffer frame'
|
|
429
|
+
buffer = None
|
|
430
|
+
gc.collect()
|
|
431
|
+
assert (frame.width(), frame.height()) == (320, 240), 'Image did not retain caller buffer after GC'
|
|
432
|
+
return dict(width=frame.width(), height=frame.height(), size=frame.size())
|
|
433
|
+
finally:
|
|
434
|
+
frame = None
|
|
435
|
+
buffer = None
|
|
436
|
+
sensor.shutdown()
|
|
437
|
+
gc.collect()
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def memory_policy(opts):
|
|
441
|
+
"""Large Image storage follows MicroPython GC; camera snapshots use PSRAM."""
|
|
442
|
+
import gc, esp32, image, sensor
|
|
443
|
+
|
|
444
|
+
def free_bytes(caps):
|
|
445
|
+
return sum(heap[1] for heap in esp32.idf_heap_info(caps))
|
|
446
|
+
|
|
447
|
+
def measure_image(width, height):
|
|
448
|
+
gc.collect()
|
|
449
|
+
before = gc.mem_alloc()
|
|
450
|
+
canvas = image.Image(width, height, image.RGB565)
|
|
451
|
+
gc.collect()
|
|
452
|
+
allocated = gc.mem_alloc() - before
|
|
453
|
+
canvas = None
|
|
454
|
+
gc.collect()
|
|
455
|
+
return width * height * 2, allocated
|
|
456
|
+
|
|
457
|
+
frame = None
|
|
458
|
+
try:
|
|
459
|
+
small_bytes, small_allocated = measure_image(32, 32)
|
|
460
|
+
assert small_bytes - 512 <= small_allocated <= small_bytes + 8192, 'small Image allocation did not use GC heap'
|
|
461
|
+
large_bytes, large_allocated = measure_image(640, 480)
|
|
462
|
+
assert large_bytes - 4096 <= large_allocated <= large_bytes + 16384, 'large Image allocation did not use GC heap'
|
|
463
|
+
|
|
464
|
+
sensor.shutdown()
|
|
465
|
+
gc.collect()
|
|
466
|
+
sensor.reset()
|
|
467
|
+
sensor.set_pixformat(sensor.RGB565)
|
|
468
|
+
sensor.set_framesize(sensor.QVGA)
|
|
469
|
+
psram_before = free_bytes(1 << 10) # ESP-IDF MALLOC_CAP_SPIRAM.
|
|
470
|
+
if psram_before <= 0:
|
|
471
|
+
raise RuntimeError('ESP_ERR_NOT_SUPPORTED: PSRAM heap unavailable')
|
|
472
|
+
frame = sensor.snapshot()
|
|
473
|
+
gc.collect()
|
|
474
|
+
psram_after = free_bytes(1 << 10)
|
|
475
|
+
frame_bytes = frame.width() * frame.height() * 2
|
|
476
|
+
psram_used = psram_before - psram_after
|
|
477
|
+
assert psram_used >= frame_bytes - 4096, 'camera snapshot buffer was not allocated from PSRAM'
|
|
478
|
+
return dict(smallBytes=small_bytes, smallAllocated=small_allocated,
|
|
479
|
+
largeBytes=large_bytes, largeAllocated=large_allocated,
|
|
480
|
+
width=frame.width(), height=frame.height(), frameBytes=frame_bytes,
|
|
481
|
+
psramUsed=psram_used)
|
|
482
|
+
finally:
|
|
483
|
+
frame = None
|
|
484
|
+
sensor.shutdown()
|
|
485
|
+
gc.collect()
|
|
486
|
+
|
|
487
|
+
|
|
289
488
|
|
|
290
489
|
def memory(opts):
|
|
291
490
|
import esp32
|
|
@@ -314,7 +513,12 @@ def run_check(name, opts):
|
|
|
314
513
|
functions = dict(memory=memory, sensor_state=sensor_state, framesizes=framesizes,
|
|
315
514
|
pixformats=pixformats, orientation=orientation, ipa_stats=ipa_stats,
|
|
316
515
|
snapshot_rate=snapshot_rate, preview=preview, image_ops=image_ops,
|
|
317
|
-
display=display, display_stream=display_stream
|
|
516
|
+
display=display, display_stream=display_stream,
|
|
517
|
+
camera_display_lifecycle=camera_display_lifecycle,
|
|
518
|
+
image_copy_to_fb=image_copy_to_fb,
|
|
519
|
+
image_transform_argument=image_transform_argument,
|
|
520
|
+
sensor_snapshot_buffer=sensor_snapshot_buffer,
|
|
521
|
+
memory_policy=memory_policy)
|
|
318
522
|
try:
|
|
319
523
|
data = functions[name](opts)
|
|
320
524
|
result = dict(name=name, status='PASS', data=data)
|
|
@@ -11,18 +11,36 @@ const capabilities = { ok: true, protocol: { evMuxVersion: 4 },
|
|
|
11
11
|
features: [...EV_MUX_REQUIRED_FEATURES, 'debug.capture_frame', 'script.write', 'script.run', 'device.control'],
|
|
12
12
|
channels: EV_MUX_REQUIRED_CHANNELS, limits: { ...EV_MUX_DEFAULT_LIMITS, txLargePayloadMax: 262144 },
|
|
13
13
|
session: { policy: 'firstCome', leaseMs: 15000, keepaliveMs: 5000 } }
|
|
14
|
-
function peer({ silent = false, screen = false } = {}) {
|
|
14
|
+
function peer({ silent = false, screen = false, vmReady = true, interactiveScripts = false } = {}) {
|
|
15
15
|
let handlers, subscriptions = 0, disconnects = 0
|
|
16
16
|
const requests = [], order = [], id = 'a'.repeat(32)
|
|
17
|
+
const requestWaiters = []
|
|
17
18
|
const stdout = payload => handlers?.data(makeEvMuxFrame({ channel: 'repl.stdout', type: 'data', sessionId: id, vmEpoch: 1, payload }))
|
|
19
|
+
const recordRequest = request => {
|
|
20
|
+
requests.push(request)
|
|
21
|
+
for (let i = requestWaiters.length - 1; i >= 0; i -= 1) {
|
|
22
|
+
if (requestWaiters[i].predicate(request)) {
|
|
23
|
+
requestWaiters.splice(i, 1)[0].resolve(request)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const waitForRequest = predicate => {
|
|
28
|
+
const existing = requests.find(predicate)
|
|
29
|
+
return existing ? Promise.resolve(existing) : new Promise(resolve => requestWaiters.push({ predicate, resolve }))
|
|
30
|
+
}
|
|
18
31
|
const parser = new EvMuxParser({ filterFrame(metadata, bytes) {
|
|
19
32
|
if (silent) return false
|
|
20
33
|
const payload = metadata.channel.endsWith('.rpc') ? JSON.parse(new TextDecoder().decode(bytes)) : new TextDecoder().decode(bytes)
|
|
21
|
-
|
|
22
|
-
if (metadata.channel === 'repl.stdin') {
|
|
34
|
+
recordRequest({ metadata, payload })
|
|
35
|
+
if (metadata.channel === 'repl.stdin') {
|
|
36
|
+
const startMarker = interactiveScripts && /~~EVSCRIPT-START:[A-Za-z0-9-]+~~/.exec(payload)?.[0]
|
|
37
|
+
if (startMarker) stdout(`${payload}\r\n${startMarker}\r\n`)
|
|
38
|
+
else stdout(`${payload}\r\n${payload.includes('EVIDE_BOOT_OK') ? 'EVIDE_BOOT_OK' : 'result'}\r\n>>> `)
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
23
41
|
if (!metadata.channel.endsWith('.rpc')) return false
|
|
24
42
|
let result = { ok: true }
|
|
25
|
-
if (metadata.method === 'hello') result = { ok: true, evMuxVersion: 4, deviceId: 'e8f60ae75642', hostname: 'esp-vision-test', board: 'TEST', firmware: { version: 'test' }, vmReady
|
|
43
|
+
if (metadata.method === 'hello') result = { ok: true, evMuxVersion: 4, deviceId: 'e8f60ae75642', hostname: 'esp-vision-test', board: 'TEST', firmware: { version: 'test' }, vmReady, session: { id, vmEpoch: 1, leaseMs: 15000, keepaliveMs: 5000 } }
|
|
26
44
|
if (metadata.method === 'capabilities') result = { ...capabilities, features: [...capabilities.features, ...(screen ? ['debug.capture_screen'] : [])] }
|
|
27
45
|
if (metadata.method === 'session.keepalive') result = { ok: true, sessionId: id, vmEpoch: 1 }
|
|
28
46
|
if (metadata.method === 'debug.capture_frame' || (screen && metadata.method === 'debug.capture_screen')) {
|
|
@@ -37,7 +55,7 @@ function peer({ silent = false, screen = false } = {}) {
|
|
|
37
55
|
async disconnect() { disconnects++; order.push('disconnect') },
|
|
38
56
|
async writeBytes(bytes) { parser.feedBytes(bytes) },
|
|
39
57
|
}
|
|
40
|
-
return { transport, requests, order, stdout, close: () => handlers.close(), get subscriptions() { return subscriptions }, get disconnects() { return disconnects } }
|
|
58
|
+
return { transport, requests, order, stdout, waitForRequest, close: () => handlers.close(), get subscriptions() { return subscriptions }, get disconnects() { return disconnects } }
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
test('root exports only the application API; protocols remain explicit adapters', () => {
|
|
@@ -57,6 +75,8 @@ test('facade subscribes before connection, negotiates, streams input and dispose
|
|
|
57
75
|
assert.equal(ready.identity.deviceId, 'e8f60ae75642')
|
|
58
76
|
assert.deepEqual(p.order.slice(0, 2), ['subscribe','connect'])
|
|
59
77
|
assert.equal(p.subscriptions, 1)
|
|
78
|
+
assert.ok(p.requests.some(x => x.metadata.channel === 'repl.signal'), 'default connection retains the REPL readiness probe')
|
|
79
|
+
assert.ok(p.requests.some(x => x.metadata.channel === 'repl.stdin' && x.payload.includes('EVIDE_BOOT_OK')))
|
|
60
80
|
await d.python.input('print(2)\r')
|
|
61
81
|
assert.ok(output.some(text => text.includes('result')))
|
|
62
82
|
unsubscribe(); const count = output.length; p.stdout('ignored\r\n'); assert.equal(output.length,count)
|
|
@@ -66,6 +86,28 @@ test('facade subscribes before connection, negotiates, streams input and dispose
|
|
|
66
86
|
await assert.rejects(d.files.read({path:'/x'}), {code:'DEVICE_NOT_READY'})
|
|
67
87
|
})
|
|
68
88
|
|
|
89
|
+
test('connect can acquire and inspect a session without interrupting the REPL', async () => {
|
|
90
|
+
const p = peer({ vmReady: false }), d = api.createDevice({ transport: p.transport })
|
|
91
|
+
try {
|
|
92
|
+
const ready = await d.connect({ role: 'tcp', probeRepl: false })
|
|
93
|
+
assert.equal(ready.runtime.vmReady, false, 'passive negotiation preserves the device readiness report')
|
|
94
|
+
assert.ok(p.requests.some(x => x.metadata.method === 'hello' && x.payload.acquire === true))
|
|
95
|
+
assert.ok(p.requests.some(x => x.metadata.method === 'capabilities'))
|
|
96
|
+
assert.equal(p.requests.some(x => x.metadata.channel === 'repl.signal' || x.metadata.channel === 'repl.stdin'), false)
|
|
97
|
+
await assert.rejects(d.python.execute({ source: 'print(1)' }), /requires a ready VM/)
|
|
98
|
+
assert.equal(p.requests.some(x => x.metadata.channel === 'repl.signal' || x.metadata.channel === 'repl.stdin'), false)
|
|
99
|
+
} finally { await d.disconnect() }
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('connect validates probeRepl before opening the transport', async () => {
|
|
103
|
+
const p = peer(), d = api.createDevice({ transport: p.transport })
|
|
104
|
+
for (const probeRepl of [null, 'false', 0]) {
|
|
105
|
+
await assert.rejects(d.connect({ role: 'tcp', probeRepl }), { name: 'TypeError' })
|
|
106
|
+
}
|
|
107
|
+
assert.deepEqual(p.order, [])
|
|
108
|
+
assert.deepEqual(p.requests, [])
|
|
109
|
+
})
|
|
110
|
+
|
|
69
111
|
test('capture returns binary bytes and unsupported LCD capture never captures the camera', async () => {
|
|
70
112
|
const p = peer(), d = api.createDevice({transport:p.transport})
|
|
71
113
|
try {
|
|
@@ -102,6 +144,53 @@ test('transport loss clears the connection and permits reconnection',async()=>{
|
|
|
102
144
|
await d.connect({role:'tcp'});await d.disconnect();assert.equal(p.disconnects,2)
|
|
103
145
|
})
|
|
104
146
|
|
|
147
|
+
test('disconnect releases the session without interrupting a publicly started run', async () => {
|
|
148
|
+
const p = peer({ interactiveScripts: true }), d = api.createDevice({ transport: p.transport })
|
|
149
|
+
try {
|
|
150
|
+
await d.connect({ role: 'tcp', probeRepl: false })
|
|
151
|
+
const run = await d.python.execute({ path: '/main.py', completion: 'start' })
|
|
152
|
+
const doneRejected = assert.rejects(run.done)
|
|
153
|
+
const beforeDisconnect = p.requests.length
|
|
154
|
+
await d.disconnect()
|
|
155
|
+
await doneRejected
|
|
156
|
+
const released = p.requests.slice(beforeDisconnect)
|
|
157
|
+
assert.ok(released.some(request => request.metadata.method === 'session.release'))
|
|
158
|
+
assert.equal(released.some(request => request.metadata.channel === 'repl.signal'
|
|
159
|
+
|| (request.metadata.method === 'device.control' && request.payload.action === 'stop')), false)
|
|
160
|
+
} finally { await d.disconnect() }
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('caller AbortSignal still interrupts a publicly started run', async () => {
|
|
164
|
+
const p = peer({ interactiveScripts: true }), d = api.createDevice({ transport: p.transport })
|
|
165
|
+
const controller = new AbortController()
|
|
166
|
+
try {
|
|
167
|
+
await d.connect({ role: 'tcp', probeRepl: false })
|
|
168
|
+
const run = await d.python.execute({ path: '/main.py', completion: 'start', signal: controller.signal })
|
|
169
|
+
const doneRejected = assert.rejects(run.done, { name: 'AbortError' })
|
|
170
|
+
const beforeAbort = p.requests.length
|
|
171
|
+
controller.abort()
|
|
172
|
+
await doneRejected
|
|
173
|
+
assert.ok(p.requests.slice(beforeAbort).some(request => request.metadata.method === 'device.control'
|
|
174
|
+
&& request.payload.action === 'stop'))
|
|
175
|
+
} finally { await d.disconnect() }
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('disconnect still interrupts Python execution in completion-wait mode', async () => {
|
|
179
|
+
const p = peer(), d = api.createDevice({ transport: p.transport })
|
|
180
|
+
try {
|
|
181
|
+
await d.connect({ role: 'tcp', probeRepl: false })
|
|
182
|
+
const execution = d.python.execute({ path: '/main.py' })
|
|
183
|
+
await p.waitForRequest(request => request.metadata.channel === 'repl.stdin'
|
|
184
|
+
&& String(request.payload).includes('~~EVSCRIPT-END:'))
|
|
185
|
+
const failed = assert.rejects(execution)
|
|
186
|
+
const beforeDisconnect = p.requests.length
|
|
187
|
+
await d.disconnect()
|
|
188
|
+
await failed
|
|
189
|
+
assert.ok(p.requests.slice(beforeDisconnect).some(request => request.metadata.method === 'device.control'
|
|
190
|
+
&& request.payload.action === 'stop'))
|
|
191
|
+
} finally { await d.disconnect() }
|
|
192
|
+
})
|
|
193
|
+
|
|
105
194
|
test('discovery works before any transport is connected',async()=>{
|
|
106
195
|
const d=api.createDevice({discovery:{async list(){return [{deviceId:'e8f60ae75642',hostname:'esp-vision-test',board:'TEST',firmwareVersion:'1',evMuxVersion:4,port:3232,addresses:['192.168.1.2']}]}}})
|
|
107
196
|
assert.equal((await d.discover({timeout:50}))[0].compatible,true)
|
|
@@ -126,8 +215,9 @@ test('long Unicode paths stay within REPL budget for directory reads and inline
|
|
|
126
215
|
repl.execPy=async source=>{commands.push(source);assert.ok(repl.execInputBytes(source)<=511);return source.includes('__EVLIST__')?'__EVLIST__[]\n':''}
|
|
127
216
|
assert.deepEqual(await repl.listFs(path),[])
|
|
128
217
|
assert.equal(commands.length,2,'Long escaped paths are assigned separately from directory code')
|
|
129
|
-
const command=repl.createScriptRunCommand(path)
|
|
218
|
+
const command=repl.createScriptRunCommand(path,{acknowledgeStart:true})
|
|
130
219
|
assert.ok(new TextEncoder().encode(command).length<=511)
|
|
220
|
+
assert.match(command,/EVSCRIPT-START:/)
|
|
131
221
|
if(repl.scriptRunPrelude)assert.ok(repl.execInputBytes(repl.scriptRunPrelude)<=511)
|
|
132
222
|
repl.cancelScriptEndSentinel()
|
|
133
223
|
})
|