/
klischa
/
RuView
Обзор
Документация
Войти
/
klischa
/
RuView
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
scripts/esp32_wasm_test.py
569 строк
19 KB
ruv
feat: add ADR-042 CHCI protocol, 24 new edge modules, README restructure
03 мар 2026, 19:35
03 мар 2026, 19:35
e94c705
Код
Авторство
О чём код?
#!/usr/bin/env python3 """ESP32 WASM Module On-Device Test Suite Uploads WASM edge modules to the ESP32-S3 and captures execution proof. Tests representative modules from each category against the 4 WASM slots. Usage: python scripts/esp32_wasm_test.py --host 192.168.1.71 --port 8032 python scripts/esp32_wasm_test.py --discover # scan subnet for ESP32 """ import argparse import json import struct import sys import time import urllib.request import urllib.error import socket import datetime # ─── WASM Module Generators ───────────────────────────────────────────────── # # Each generator produces a valid MVP WASM binary that: # 1. Imports from "csi" namespace (matching firmware) # 2. Exports on_frame() → i32 (required entry point) # 3. Uses ≤2 memory pages (128 KB) # 4. Contains no bulk-memory ops (MVP only) # 5. Emits events via csi_emit_event(event_id, value) # # The modules are tiny (200-800 bytes) but exercise real host API calls # and produce measurable event output. def leb128_u(val): """Encode unsigned LEB128.""" out = bytearray() while True: b = val & 0x7F val >>= 7 if val: out.append(b | 0x80) else: out.append(b) break return bytes(out) def leb128_s(val): """Encode signed LEB128.""" out = bytearray() while True: b = val & 0x7F val >>= 7 if (val == 0 and not (b & 0x40)) or (val == -1 and (b & 0x40)): out.append(b) break else: out.append(b | 0x80) return bytes(out) def section(section_id, data): """Wrap data in a WASM section.""" return bytes([section_id]) + leb128_u(len(data)) + data def vec(items): """WASM vector: count + items.""" return leb128_u(len(items)) + b"".join(items) def func_type(params, results): """Encode a func type (0x60 params results).""" return b"\x60" + vec([bytes([p]) for p in params]) + vec([bytes([r]) for r in results]) def import_entry(module, name, kind_byte, type_idx): """Encode an import entry.""" mod_enc = leb128_u(len(module)) + module.encode() name_enc = leb128_u(len(name)) + name.encode() return mod_enc + name_enc + bytes([0x00]) + leb128_u(type_idx) # kind=func def export_entry(name, kind, idx): """Encode an export entry.""" return leb128_u(len(name)) + name.encode() + bytes([kind]) + leb128_u(idx) I32 = 0x7F F32 = 0x7D # Opcodes OP_LOCAL_GET = 0x20 OP_I32_CONST = 0x41 OP_F32_CONST = 0x43 OP_CALL = 0x10 OP_DROP = 0x1A OP_END = 0x0B def f32_bytes(val): """Encode f32 constant.""" return struct.pack("<f", val) def build_module(name, event_id, event_value, imports_needed=None): """Build a minimal WASM module that calls csi_emit_event on each frame. The on_frame function: 1. Calls csi_emit_event(event_id, event_value) 2. Returns 1 (success) Args: name: Module name for logging event_id: Event ID to emit (i32) event_value: Event value to emit (f32) imports_needed: List of (name, param_types, result_types) for extra imports """ if imports_needed is None: imports_needed = [] # Type section: define function signatures types = [] # Type 0: (i32, f32) -> void [csi_emit_event] types.append(func_type([I32, F32], [])) # Type 1: () -> i32 [on_frame export] types.append(func_type([], [I32])) # Type 2+: additional import types extra_type_map = {} for imp_name, params, results in imports_needed: sig = (tuple(params), tuple(results)) if sig not in extra_type_map: extra_type_map[sig] = len(types) types.append(func_type(params, results)) type_sec = section(1, vec(types)) # Import section imports = [] # Import 0: csi_emit_event (type 0) imports.append(import_entry("csi", "csi_emit_event", 0, 0)) import_idx = 1 extra_import_indices = {} for imp_name, params, results in imports_needed: sig = (tuple(params), tuple(results)) tidx = extra_type_map[sig] imports.append(import_entry("csi", imp_name, 0, tidx)) extra_import_indices[imp_name] = import_idx import_idx += 1 import_sec = section(2, vec(imports)) # Function section: 1 local function (on_frame) func_sec = section(3, vec([leb128_u(1)])) # type index 1 # Memory section: 1 page (64KB), max 2 pages mem_sec = section(5, b"\x01" + b"\x01\x01\x02") # 1 memory, limits: min=1, max=2 # Export section: export on_frame as "on_frame" (func, idx = import_count) on_frame_idx = len(imports) # local func index offset by imports exports = [export_entry("on_frame", 0, on_frame_idx)] # Also export memory exports.append(export_entry("memory", 2, 0)) export_sec = section(7, vec(exports)) # Code section: on_frame body # Calls csi_emit_event(event_id, event_value), returns 1 body = bytearray() body.append(0x00) # 0 local declarations # Call csi_emit_event(event_id, event_value) body.append(OP_I32_CONST) body.extend(leb128_s(event_id)) body.append(OP_F32_CONST) body.extend(f32_bytes(event_value)) body.append(OP_CALL) body.extend(leb128_u(0)) # call import 0 (csi_emit_event) # Return 1 body.append(OP_I32_CONST) body.extend(leb128_s(1)) body.append(OP_END) body_with_size = leb128_u(len(body)) + bytes(body) code_sec = section(10, vec([body_with_size])) # Assemble wasm = b"\x00asm" + struct.pack("<I", 1) # magic + version wasm += type_sec + import_sec + func_sec + mem_sec + export_sec + code_sec return wasm # ─── Category Module Definitions ──────────────────────────────────────────── CATEGORY_MODULES = [ { "name": "core_gesture", "category": "Core", "event_id": 1, "event_value": 0.85, "description": "Gesture detection event (coherence=0.85)", }, { "name": "med_fall_detect", "category": "Medical & Health", "event_id": 100, "event_value": 0.92, "description": "Fall detection alert (confidence=0.92)", }, { "name": "sec_intrusion", "category": "Security & Safety", "event_id": 200, "event_value": 0.78, "description": "Intrusion detection (score=0.78)", }, { "name": "bld_zone_occupied", "category": "Smart Building", "event_id": 300, "event_value": 3.0, "description": "Zone occupancy (3 persons detected)", }, { "name": "ret_queue_len", "category": "Retail & Hospitality", "event_id": 400, "event_value": 5.0, "description": "Queue length estimate (5 people)", }, { "name": "ind_proximity", "category": "Industrial", "event_id": 500, "event_value": 1.5, "description": "Proximity warning (1.5m distance)", }, { "name": "exo_sleep_stage", "category": "Exotic & Research", "event_id": 600, "event_value": 2.0, "description": "Sleep stage detection (stage 2 = light sleep)", }, { "name": "sig_coherence", "category": "Signal Intelligence", "event_id": 700, "event_value": 0.91, "description": "Coherence gate score (0.91)", }, { "name": "lrn_gesture_learned", "category": "Adaptive Learning", "event_id": 730, "event_value": 0.88, "description": "Gesture learned (DTW score=0.88)", }, { "name": "spt_influence", "category": "Spatial & Temporal", "event_id": 760, "event_value": 0.72, "description": "PageRank influence score (0.72)", }, { "name": "ais_replay_attack", "category": "AI Security", "event_id": 820, "event_value": 0.95, "description": "Replay attack detected (confidence=0.95)", }, { "name": "qnt_entanglement", "category": "Quantum & Autonomous", "event_id": 850, "event_value": 0.67, "description": "Quantum entanglement coherence (0.67)", }, ] # ─── ESP32 Communication ──────────────────────────────────────────────────── def discover_esp32(subnet="192.168.1", port=8032, start=1, end=80): """Scan subnet for ESP32 WASM runtime.""" print(f"Scanning {subnet}.{start}-{end} for WASM runtime on port {port}...") for i in range(start, end + 1): ip = f"{subnet}.{i}" try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.3) if sock.connect_ex((ip, port)) == 0: sock.close() url = f"http://{ip}:{port}/wasm/status" try: resp = urllib.request.urlopen(url, timeout=2) data = json.loads(resp.read()) if "slots" in data: print(f" Found ESP32 at {ip}:{port} — {len(data['slots'])} WASM slots") return ip except Exception: pass else: sock.close() except Exception: pass return None def get_status(host, port): """Get WASM runtime status from ESP32.""" url = f"http://{host}:{port}/wasm/status" resp = urllib.request.urlopen(url, timeout=5) return json.loads(resp.read()) def upload_module(host, port, slot, wasm_bytes, name="test"): """Upload a WASM module to a specific slot.""" url = f"http://{host}:{port}/wasm/upload?slot={slot}" req = urllib.request.Request( url, data=wasm_bytes, headers={"Content-Type": "application/wasm"}, method="POST", ) try: resp = urllib.request.urlopen(req, timeout=10) return json.loads(resp.read()) except urllib.error.HTTPError as e: body = e.read().decode(errors="replace") return {"error": f"HTTP {e.code}: {body}"} except Exception as e: return {"error": str(e)} def get_slot_status(host, port, slot): """Get status for a specific WASM slot.""" status = get_status(host, port) if "slots" in status and slot < len(status["slots"]): return status["slots"][slot] return None def reset_slot(host, port, slot): """Try to reset/unload a WASM slot.""" url = f"http://{host}:{port}/wasm/{slot}" req = urllib.request.Request(url, method="DELETE") try: resp = urllib.request.urlopen(req, timeout=5) return json.loads(resp.read()) except Exception: return None # ─── Test Runner ───────────────────────────────────────────────────────────── def run_test_suite(host, port, wasm_binary_path=None): """Run the full on-device test suite. Tests 12 category modules across 4 WASM slots (3 rounds of 4). Captures event counts and timing as proof of execution. """ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") results = [] print("=" * 70) print(f" ESP32 WASM On-Device Test Suite — {timestamp}") print("=" * 70) print() # 1. Get initial status try: status = get_status(host, port) except Exception as e: print(f"ERROR: Cannot reach ESP32 at {host}:{port}: {e}") return [] n_slots = len(status.get("slots", [])) print(f"ESP32 WASM Runtime: {n_slots} slots available") print(f"Host: {host}:{port}") print() # 2. Test full Rust library if path provided if wasm_binary_path: print("─── Phase 1: Full Rust WASM Library Upload ───") try: with open(wasm_binary_path, "rb") as f: wasm_data = f.read() print(f" Binary: {len(wasm_data)} bytes") result = upload_module(host, port, 0, wasm_data, "edge_library") print(f" Upload result: {json.dumps(result)}") if result.get("started"): time.sleep(2) slot = get_slot_status(host, port, 0) if slot: print(f" Running: {slot.get('frames', 0)} frames, " f"{slot.get('events', 0)} events, " f"mean {slot.get('mean_us', 0)}us") results.append({ "name": "edge_library_full", "category": "Full Library", "size": len(wasm_data), "upload": result, "slot_status": slot, "pass": result.get("started", False), }) else: results.append({ "name": "edge_library_full", "category": "Full Library", "size": len(wasm_data), "upload": result, "pass": False, }) except Exception as e: print(f" Error: {e}") results.append({ "name": "edge_library_full", "category": "Full Library", "error": str(e), "pass": False, }) print() # 3. Test per-category synthetic modules (4 at a time across slots) print("─── Phase 2: Per-Category Module Tests ───") print() modules = CATEGORY_MODULES batch_size = min(n_slots, 4) for batch_start in range(0, len(modules), batch_size): batch = modules[batch_start:batch_start + batch_size] print(f" Batch {batch_start // batch_size + 1}: " f"{', '.join(m['name'] for m in batch)}") # Upload batch for i, mod in enumerate(batch): slot = i % n_slots wasm = build_module(mod["name"], mod["event_id"], mod["event_value"]) print(f" [{slot}] {mod['name']} ({len(wasm)} bytes) — {mod['description']}") result = upload_module(host, port, slot, wasm, mod["name"]) if "error" in result: print(f" FAIL: {result['error']}") results.append({**mod, "size": len(wasm), "upload": result, "pass": False}) continue print(f" Upload: {json.dumps(result, separators=(',', ':'))}") results.append({ **mod, "size": len(wasm), "upload": result, "pass": result.get("started", False), }) # Let modules run for 3 seconds to accumulate frames/events print(f" Waiting 3s for frame processing...") time.sleep(3) # Capture slot status as proof status = get_status(host, port) for i, mod in enumerate(batch): slot = i % n_slots if slot < len(status.get("slots", [])): ss = status["slots"][slot] frames = ss.get("frames", 0) events = ss.get("events", 0) errors = ss.get("errors", 0) mean_us = ss.get("mean_us", 0) max_us = ss.get("max_us", 0) # Find the result and update it for r in results: if r.get("name") == mod["name"] and "slot_proof" not in r: r["slot_proof"] = { "frames": frames, "events": events, "errors": errors, "mean_us": mean_us, "max_us": max_us, } passed = frames > 0 and events > 0 and errors == 0 r["pass"] = r["pass"] and passed status_str = "PASS" if passed else "FAIL" print(f" [{slot}] {mod['name']}: {frames} frames, " f"{events} events, {errors} errors, " f"mean {mean_us}us, max {max_us}us — {status_str}") break print() # 4. Summary print("=" * 70) print(" TEST SUMMARY") print("=" * 70) passed = sum(1 for r in results if r.get("pass")) failed = sum(1 for r in results if not r.get("pass")) print(f" Passed: {passed}/{len(results)}") print(f" Failed: {failed}/{len(results)}") print() for r in results: status_str = "PASS" if r.get("pass") else "FAIL" proof = r.get("slot_proof", {}) frames = proof.get("frames", "?") events = proof.get("events", "?") mean_us = proof.get("mean_us", "?") print(f" [{status_str}] {r.get('category', '?'):24s} {r.get('name', '?'):24s} " f"frames={frames} events={events} latency={mean_us}us") print() print(f" Timestamp: {timestamp}") print(f" ESP32: {host}:{port}") print() # 5. Save proof JSON proof_path = f"docs/edge-modules/esp32_test_proof_{timestamp}.json" try: proof_data = { "timestamp": timestamp, "host": f"{host}:{port}", "results": results, "summary": { "total": len(results), "passed": passed, "failed": failed, }, } import os os.makedirs(os.path.dirname(proof_path), exist_ok=True) with open(proof_path, "w") as f: json.dump(proof_data, f, indent=2) print(f" Proof saved to: {proof_path}") except Exception as e: print(f" Warning: Could not save proof file: {e}") return results # ─── Main ─────────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser(description="ESP32 WASM On-Device Test Suite") parser.add_argument("--host", default="192.168.1.71", help="ESP32 IP address") parser.add_argument("--port", type=int, default=8032, help="WASM HTTP port") parser.add_argument("--discover", action="store_true", help="Scan subnet for ESP32") parser.add_argument("--wasm", help="Path to full Rust WASM binary to test") parser.add_argument("--subnet", default="192.168.1", help="Subnet to scan") args = parser.parse_args() if args.discover: host = discover_esp32(args.subnet, args.port) if not host: print("No ESP32 found. Check that device is powered and connected to WiFi.") sys.exit(1) args.host = host results = run_test_suite(args.host, args.port, args.wasm) sys.exit(0 if all(r.get("pass") for r in results) else 1) if __name__ == "__main__": main()