velxio/test/pi_arduino_serial/avr_runner.js

189 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* avr_runner.js
* -------------
* Node.js ATmega328P (Arduino Uno) emulator using avr8js.
*
* Loads a compiled Intel HEX firmware file and emulates the CPU at
* 16 MHz. The USART (Serial) peripheral is bridged to a TCP socket
* so the Python broker can connect and exchange bytes.
*
* Usage:
* node avr_runner.js <hex_file> [broker_host] [broker_port]
*
* The script acts as a TCP CLIENT. It connects (with retries) to the
* Python broker which acts as the server for the Arduino side.
*
* Data flow:
* Pi --> broker:5556 --> avr_runner --> usart.writeByte() --> Arduino RX
* Arduino TX --> usart.onByteTransmit --> broker:5556 --> Pi
*/
'use strict';
const fs = require('fs');
const net = require('net');
const path = require('path');
// ── Load avr8js from local wokwi-libs ────────────────────────────────────────
const AVR8JS_CJS = path.resolve(
__dirname, '..', '..', 'wokwi-libs', 'avr8js', 'dist', 'cjs', 'index.js'
);
let avr8js;
try {
avr8js = require(AVR8JS_CJS);
} catch (e) {
process.stderr.write(`[avr_runner] FATAL: cannot load avr8js from:\n ${AVR8JS_CJS}\n ${e.message}\n`);
process.exit(1);
}
const {
CPU,
avrInstruction,
AVRUSART, usart0Config,
AVRTimer, timer0Config, timer1Config, timer2Config,
} = avr8js;
// ── CLI arguments ─────────────────────────────────────────────────────────────
const [,, hexFile, brokerHost = '127.0.0.1', brokerPort = '5556'] = process.argv;
if (!hexFile) {
process.stderr.write('Usage: node avr_runner.js <hex_file> [broker_host] [broker_port]\n');
process.exit(1);
}
if (!fs.existsSync(hexFile)) {
process.stderr.write(`[avr_runner] ERROR: hex file not found: ${hexFile}\n`);
process.exit(1);
}
// ── Intel HEX parser ──────────────────────────────────────────────────────────
function parseIntelHex(content) {
// ATmega328P has 32 KB flash → 0x8000 bytes
const flash = new Uint8Array(0x8000);
for (const rawLine of content.split('\n')) {
const line = rawLine.trim();
if (!line.startsWith(':') || line.length < 11) continue;
const bytes = Buffer.from(line.slice(1), 'hex');
const byteCount = bytes[0];
const addr = (bytes[1] << 8) | bytes[2];
const recordType = bytes[3];
if (recordType === 0x00) { // Data record
for (let i = 0; i < byteCount; i++) {
if (addr + i < flash.length) {
flash[addr + i] = bytes[4 + i];
}
}
}
// recordType 0x01 = EOF — nothing to do
}
// AVR instructions are 16-bit little-endian words
return new Uint16Array(flash.buffer);
}
// ── Build the CPU ─────────────────────────────────────────────────────────────
const CLOCK_HZ = 16_000_000;
const hexContent = fs.readFileSync(hexFile, 'utf8');
const program = parseIntelHex(hexContent);
const cpu = new CPU(program);
// Timers are needed for delay() / millis() inside the Arduino sketch
const timers = [
new AVRTimer(cpu, timer0Config),
new AVRTimer(cpu, timer1Config),
new AVRTimer(cpu, timer2Config),
];
const usart = new AVRUSART(cpu, usart0Config, CLOCK_HZ);
// ── TCP bridge state ───────────────────────────────────────────────────────────
let socket = null;
let txBacklog = []; // bytes queued before TCP connects
// Arduino → Pi: forward transmitted bytes
usart.onByteTransmit = (byte) => {
const ch = String.fromCharCode(byte);
process.stdout.write(`[AVR->Pi] ${ch === '\n' ? '\\n\n' : ch}`);
if (socket && !socket.destroyed) {
socket.write(Buffer.from([byte]));
} else {
txBacklog.push(byte); // buffer until connected
}
};
// ── Simulation loop ───────────────────────────────────────────────────────────
// Run ~160 000 instructions per Node.js event-loop tick ≈ 10 ms simulated time.
// setImmediate() yields after each batch so I/O callbacks can fire.
const BATCH = 160_000;
function runBatch() {
for (let i = 0; i < BATCH; i++) {
avrInstruction(cpu);
cpu.tick();
}
setImmediate(runBatch);
}
// ── TCP connection to broker ──────────────────────────────────────────────────
let retryCount = 0;
const MAX_RETRIES = 40; // 40 × 500 ms = 20 s
function connectToBroker() {
if (retryCount >= MAX_RETRIES) {
process.stderr.write('[avr_runner] ERROR: could not connect to broker after max retries\n');
process.exit(1);
}
const s = new net.Socket();
s.connect(parseInt(brokerPort, 10), brokerHost, () => {
retryCount = 0;
socket = s;
process.stdout.write(`[avr_runner] Connected to broker ${brokerHost}:${brokerPort}\n`);
// Flush bytes queued before connection
if (txBacklog.length > 0) {
s.write(Buffer.from(txBacklog));
txBacklog = [];
}
});
// Pi → Arduino: feed received bytes into USART RX
s.on('data', (chunk) => {
for (const byte of chunk) {
const ch = String.fromCharCode(byte);
process.stdout.write(`[Pi->AVR] ${ch === '\n' ? '\\n\n' : ch}`);
// writeByte(value, immediate=true) bypasses baud-rate timing
usart.writeByte(byte, true);
}
});
s.on('close', () => {
process.stdout.write('[avr_runner] Broker connection closed\n');
socket = null;
});
s.on('error', (err) => {
if (err.code === 'ECONNREFUSED') {
retryCount++;
process.stdout.write(`[avr_runner] Broker not ready, retry ${retryCount}/${MAX_RETRIES} ...\n`);
setTimeout(connectToBroker, 500);
} else {
process.stderr.write(`[avr_runner] Socket error: ${err.message}\n`);
}
});
}
// ── Start ─────────────────────────────────────────────────────────────────────
process.stdout.write(`[avr_runner] Loaded: ${path.basename(hexFile)}\n`);
process.stdout.write(`[avr_runner] ATmega328P @ ${CLOCK_HZ / 1e6} MHz — simulation starting\n`);
connectToBroker();
runBatch();