velxio/test/backend/e2e/test_mpu6050_simulation.mjs

402 lines
16 KiB
JavaScript

/**
* test_mpu6050_simulation.mjs
*
* Full end-to-end test for the ESP32 + MPU-6050 I2C simulation.
* Mirrors exactly what the frontend does:
* 1. POST /api/compile/ → get firmware_b64
* 2. WebSocket /api/simulation/ws/{id}
* 3. send start_esp32 with firmware + sensors:[{sensor_type:'mpu6050',…}]
* 4. Watch all events, log I2C state machine + serial output
*
* Run from the backend/ directory:
* node test_mpu6050_simulation.mjs [--timeout 30]
*
* Prerequisites: Backend running on http://localhost:8001
*/
// ─── Config ───────────────────────────────────────────────────────────────────
const BACKEND = process.env.BACKEND_URL ?? process.argv.find(a => a.startsWith('--backend='))?.slice(10) ?? 'http://localhost:8001';
const WS_BASE = BACKEND.replace(/^https?:/, m => m === 'https:' ? 'wss:' : 'ws:');
const SESSION = `test-mpu6050-${Date.now()}`;
const TIMEOUT_S = parseInt(process.argv.find(a => a.startsWith('--timeout='))?.slice(10) ?? '40');
// ─── MPU-6050 sketch (same as the example in examples.ts) ────────────────────
const SKETCH = `// ESP32 — MPU-6050 Accelerometer & Gyroscope (I2C)
// Requires: Adafruit MPU6050, Adafruit Unified Sensor libraries
// Wiring: SDA → D21 | SCL → D22 | VCC → 3V3 | GND → GND
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
Adafruit_MPU6050 mpu;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA=21, SCL=22
if (!mpu.begin()) {
Serial.println("MPU6050 not found! Check wiring.");
while (true) delay(10);
}
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
Serial.println("MPU6050 ready!");
}
void loop() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
Serial.printf("Accel X=%.2f Y=%.2f Z=%.2f m/s^2\\n",
a.acceleration.x, a.acceleration.y, a.acceleration.z);
Serial.printf("Gyro X=%.2f Y=%.2f Z=%.2f rad/s\\n",
g.gyro.x, g.gyro.y, g.gyro.z);
Serial.printf("Temp: %.1f C\\n---\\n", temp.temperature);
delay(500);
}`;
// ─── I2C event decoder (mirrors Python esp32_i2c_slaves.py constants) ─────────
const I2C_OP = { 0x00: 'STOP', 0x01: 'START', 0x03: 'READ', 0x05: 'WRITE_FIRST', 0x06: 'WRITE_CONT' };
// Local mirror of the MPU6050 slave state machine — tracks the same logic as
// Python's MPU6050Slave so we can annotate WHICH firmware call each event came from.
class MPU6050StateMirror {
constructor() {
this.reg_ptr = 0x75;
this.first_byte = true;
this._who_am_i_count = 0;
this.regs = new Uint8Array(256);
// WHO_AM_I = 0x68
this.regs[0x75] = 0x68;
// ACCEL_Z = +1g = 0x40 (MSB at 0x3F)
this.regs[0x3F] = 0x40;
// TEMP raw for 25°C
const tempRaw = Math.round((25.0 - 36.53) * 340) & 0xFFFF;
this.regs[0x41] = (tempRaw >> 8) & 0xFF;
this.regs[0x42] = tempRaw & 0xFF;
this.eventCount = 0;
this.readCount = 0;
}
handle(event) {
const op = event & 0xFF;
const data = (event >> 8) & 0xFF;
this.eventCount++;
let val = 0;
let note = '';
if (op === 0x01) { // I2C_START
this.first_byte = true;
if (this._who_am_i_count >= 2) {
this.reg_ptr = 0x3B;
note = `DATA_MODE (count=${this._who_am_i_count} ≥ 2)`;
} else {
this.reg_ptr = 0x75;
note = `WHO_AM_I_MODE (count=${this._who_am_i_count} < 2)`;
}
val = 1;
} else if (op === 0x05 || op === 0x06) { // WRITE
if (this.first_byte) {
this.reg_ptr = data;
this.first_byte = false;
note = `set reg_ptr=0x${data.toString(16).padStart(2,'0')} (${REG_NAME[data] ?? '?'})`;
} else {
this.regs[this.reg_ptr] = data;
if (this.reg_ptr === 0x6B) this.regs[0x6B] &= 0x7F; // auto-clear DEVICE_RESET
note = `write 0x${data.toString(16).padStart(2,'0')} → reg[0x${this.reg_ptr.toString(16).padStart(2,'0')}]`;
this.reg_ptr = (this.reg_ptr + 1) & 0xFF;
}
val = 1;
} else if (op === 0x03) { // READ
this.readCount++;
val = this.regs[this.reg_ptr];
if (this.reg_ptr === 0x75 && val === 0x68) {
this._who_am_i_count++;
note = `WHO_AM_I read #${this._who_am_i_count} → 0x68`;
} else {
note = `reg[0x${this.reg_ptr.toString(16).padStart(2,'0')}]=${FRIENDLY_REG(this.reg_ptr, val)}`;
}
this.reg_ptr = (this.reg_ptr + 1) & 0xFF;
} else { // STOP
this.first_byte = true;
note = 'transaction end';
}
return { val, note };
}
/** Guess which firmware function triggered this event sequence */
guessPhase() {
const c = this._who_am_i_count;
const r = this.readCount;
if (c === 0) return 'before_begin / detected()';
if (c === 1) return 'chip_id.read() or reset() read-modify-write';
if (c === 2 && r <= 3) return 'reset() wait loop or setFilterBandwidth';
if (c >= 2) return '_init() config setup OR sketch getEvent() loop';
return '?';
}
}
// Known MPU-6050 register names
const REG_NAME = {
0x19: 'SMPRT_DIV', 0x1A: 'CONFIG', 0x1B: 'GYRO_CONFIG', 0x1C: 'ACCEL_CONFIG',
0x3B: 'ACCEL_XOUT_H', 0x3C: 'ACCEL_XOUT_L', 0x3D: 'ACCEL_YOUT_H', 0x3E: 'ACCEL_YOUT_L',
0x3F: 'ACCEL_ZOUT_H', 0x40: 'ACCEL_ZOUT_L',
0x41: 'TEMP_OUT_H', 0x42: 'TEMP_OUT_L',
0x43: 'GYRO_XOUT_H', 0x44: 'GYRO_XOUT_L', 0x45: 'GYRO_YOUT_H', 0x46: 'GYRO_YOUT_L',
0x47: 'GYRO_ZOUT_H', 0x48: 'GYRO_ZOUT_L',
0x6B: 'PWR_MGMT_1', 0x6C: 'PWR_MGMT_2',
0x68: 'SIGNAL_PATH_RESET',
0x75: 'WHO_AM_I',
};
function FRIENDLY_REG(reg, val) {
const name = REG_NAME[reg];
const hex = `0x${val.toString(16).padStart(2,'0')}`;
if (name) return `${hex} [${name}]`;
return hex;
}
// ─── Logging helpers ───────────────────────────────────────────────────────────
const T0 = Date.now();
const ts = () => `[+${((Date.now() - T0)/1000).toFixed(3)}s]`;
const LOG_LEVELS = { INFO: '\x1b[36m', WARN: '\x1b[33m', ERROR: '\x1b[31m', OK: '\x1b[32m', I2C: '\x1b[35m', SERIAL: '\x1b[32m', RESET: '\x1b[0m' };
const log = (lvl, ...args) => console.log(`${LOG_LEVELS[lvl] ?? ''}${ts()} [${lvl}]${LOG_LEVELS.RESET}`, ...args);
const info = (...a) => log('INFO', ...a);
const warn = (...a) => log('WARN', ...a);
const ok = (...a) => log('OK', ...a);
const err = (...a) => log('ERROR', ...a);
const i2c = (...a) => log('I2C', ...a);
const serial = (...a) => log('SERIAL', ...a);
// ─── Counters & state ─────────────────────────────────────────────────────────
let totalEvents = 0;
let i2cEvents = 0;
let i2cTraceCount = 0;
let serialLines = [];
let foundOK = false;
let foundFail = false;
const mirror = new MPU6050StateMirror();
// ─── Step 1: Compile the sketch ───────────────────────────────────────────────
async function compile() {
info('Compiling MPU6050 sketch via POST /api/compile/ ...');
const res = await fetch(`${BACKEND}/api/compile/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
files: [{ name: 'sketch.ino', content: SKETCH }],
board_fqbn: 'esp32:esp32:esp32',
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Compilation failed HTTP ${res.status}: ${text.slice(0, 300)}`);
}
const body = await res.json();
if (!body.success) {
throw new Error(`Compilation error:\n${(body.error ?? body.stderr ?? 'unknown').slice(0, 500)}`);
}
// API returns firmware as base64 in 'binary_content' (ESP32 flash image)
const firmware_b64 = body.binary_content ?? body.firmware_b64;
if (!firmware_b64) {
throw new Error(`No firmware in response. Keys: ${Object.keys(body).join(', ')}`);
}
const sizeKB = Math.round(firmware_b64.length * 0.75 / 1024);
ok(`Compilation succeeded — ${sizeKB} KB firmware (has_wifi=${body.has_wifi})`);
return firmware_b64;
}
// ─── Step 2: Run simulation via WebSocket ─────────────────────────────────────
function runSimulation(firmware_b64) {
return new Promise((resolve, reject) => {
const wsUrl = `${WS_BASE}/api/simulation/ws/${SESSION}`;
info(`Connecting WebSocket → ${wsUrl}`);
const ws = new WebSocket(wsUrl);
const timer = setTimeout(() => {
info(`Timeout reached (${TIMEOUT_S}s) — stopping simulation`);
ws.close();
resolve({ timedOut: true });
}, TIMEOUT_S * 1000);
ws.addEventListener('open', () => {
ok('WebSocket connected');
const payload = {
type: 'start_esp32',
data: {
board: 'esp32',
firmware_b64,
sensors: [
// Mirror what useSimulatorStore sends for wokwi-mpu6050 component
{ sensor_type: 'mpu6050', pin: 200 + 0x68, addr: 0x68 }
],
wifi_enabled: false,
},
};
info('Sending start_esp32 with sensors:', JSON.stringify(payload.data.sensors));
ws.send(JSON.stringify(payload));
});
ws.addEventListener('message', ev => {
totalEvents++;
let msg;
try { msg = JSON.parse(ev.data); } catch { return; }
const { type, data } = msg;
// ── Serial output ──────────────────────────────────────────────────────
if (type === 'serial_output') {
const text = (data?.data ?? '').trim();
if (!text) return;
for (const line of text.split(/\r?\n/)) {
if (!line.trim()) continue;
serialLines.push(line);
serial(`UART: ${line}`);
if (line.includes('MPU6050 ready!') || line.includes('===BEGIN_OK===')) {
foundOK = true;
}
if (line.includes('not found') || line.includes('===BEGIN_FAILED===')) {
foundFail = true;
warn('begin() returned FALSE — firmware reported "not found"');
}
// If we got sensor data, stop after a few lines
if (foundOK && serialLines.filter(l => l.startsWith('Accel')).length >= 3) {
clearTimeout(timer);
ws.close();
resolve({ timedOut: false });
}
}
// Fail fast: if error already confirmed, still wait for a bit more events
if (foundFail && i2cEvents > 5) {
// Give 5 more seconds to see remaining I2C events
setTimeout(() => { clearTimeout(timer); ws.close(); resolve({ timedOut: false }); }, 5000);
}
return;
}
// ── I2C trace (Python slave handled the event — always emitted for debug) ─
if (type === 'i2c_trace') {
i2cTraceCount++;
const { bus, addr, event, op, result, reg_ptr, wai_count } = data;
const regHex = reg_ptr != null ? `0x${reg_ptr.toString(16).padStart(2,'0')}` : '??';
const resHex = `0x${(result??0).toString(16).padStart(2,'0')}`;
i2c(`[slave] bus=${bus} addr=0x${(addr??0).toString(16).padStart(2,'0')} ` +
`op=${op} result=${resHex} reg_ptr=${regHex} wai=${wai_count}`);
return;
}
// ── I2C event (forwarded from Python worker when addr NOT in _i2c_slaves) ─
if (type === 'i2c_event') {
i2cEvents++;
const { bus, addr, event, response } = data;
const op = event & 0xFF;
const d = (event >> 8) & 0xFF;
const opName = I2C_OP[op] ?? `0x${op.toString(16)}`;
i2c(`[UNHANDLED slave] bus=${bus} addr=0x${(addr??0).toString(16).padStart(2,'0')} ` +
`event=0x${(event??0).toString(16).padStart(4,'0')} op=${opName} data=0x${d.toString(16).padStart(2,'0')} ` +
`resp=0x${(response??0).toString(16).padStart(2,'0')}`);
return;
}
// ── gpio_change — show I2C pin activity ───────────────────────────────
if (type === 'gpio_change') {
const { pin, state } = data ?? {};
if (pin === 21 || pin === 22) {
// SDA=21, SCL=22 — these toggling means I2C is active on the bus
i2c(`I2C pin toggle: GPIO${pin} (${pin===21?'SDA':'SCL'}) → ${state}`);
}
return;
}
// ── system / error ─────────────────────────────────────────────────────
if (type === 'system') {
info(`system event: ${JSON.stringify(data)}`);
return;
}
if (type === 'error') {
err(`simulation error: ${JSON.stringify(data)}`);
return;
}
// ── Everything else ───────────────────────────────────────────────────
if (!['gpio_change'].includes(type)) {
info(`event type=${type} data=${JSON.stringify(data).slice(0,120)}`);
}
});
ws.addEventListener('close', ev => {
clearTimeout(timer);
info(`WebSocket closed (code=${ev.code})`);
resolve({ timedOut: false });
});
ws.addEventListener('error', ev => {
clearTimeout(timer);
err('WebSocket error:', ev.message ?? ev.type);
reject(new Error('WebSocket error'));
});
});
}
// ─── Main ──────────────────────────────────────────────────────────────────────
async function main() {
console.log('\n' + '═'.repeat(60));
console.log(' TEST: ESP32 + MPU-6050 I2C Simulation');
console.log(' Session:', SESSION);
console.log(' Backend:', BACKEND);
console.log(' Timeout:', TIMEOUT_S, 's');
console.log('═'.repeat(60) + '\n');
let firmware_b64;
try {
firmware_b64 = await compile();
} catch (e) {
err('Compilation failed:', e.message);
process.exit(1);
}
console.log('\n' + '─'.repeat(60));
console.log(' Starting simulation...');
console.log('─'.repeat(60) + '\n');
info('NOTE: I2C trace events emitted by backend are shown below with [slave] prefix.');
console.log();
const result = await runSimulation(firmware_b64);
// ─── Summary ────────────────────────────────────────────────────────────────
console.log('\n' + '═'.repeat(60));
console.log(' SUMMARY');
console.log('═'.repeat(60));
console.log(` Total WebSocket events received : ${totalEvents}`);
console.log(` I2C trace events (slave handled): ${i2cTraceCount}`);
console.log(` I2C events (no slave registered): ${i2cEvents}`);
console.log(` Serial lines received : ${serialLines.length}`);
console.log(` Timed out : ${result.timedOut}`);
console.log();
console.log(' Serial output:');
for (const l of serialLines) console.log(` ${l}`);
console.log();
if (foundOK) {
console.log('\x1b[32m ✓ PASS — MPU6050 detected and sensor data flowing\x1b[0m');
process.exit(0);
} else if (foundFail) {
console.log('\x1b[31m ✗ FAIL — mpu.begin() returned false ("not found")\x1b[0m');
console.log('\x1b[33m → Check the backend (uvicorn) terminal for I2C event trace.\x1b[0m');
console.log('\x1b[33m → Look for "I2C bus=0 addr=0x68" lines to see the full sequence.\x1b[0m');
process.exit(1);
} else if (result.timedOut) {
console.log('\x1b[33m ? TIMEOUT — no "ready" or "not found" in serial output\x1b[0m');
process.exit(1);
} else {
console.log('\x1b[33m ? INCONCLUSIVE — WebSocket closed before result determined\x1b[0m');
process.exit(1);
}
}
main().catch(e => { err('Unhandled error:', e); process.exit(1); });