/* Arduino OBI interface, ported from the Python desktop app (interfaces/arduino_obi.py).
Uses the Web Serial API instead of pyserial. The widget renders the connection
details (port + firmware version); the host page owns the Connect button and
calls connect()/disconnect(). */
(function () {
"use strict";
const {
debug,
hexDump,
modal,
toast,
sleep,
ConnectionError,
SerialConnection,
} = OBI.util;
const INTERFACE_VERSION_CMD = [0x01, 0x00, 0x03, 0x01];
function createWidget(frame, obi, callbacks) {
const onConnect = (callbacks && callbacks.onConnect) || function () {};
frame.innerHTML =
'
' +
'
Serial port' +
'
No port connected
' +
"
";
const portNameEl = frame.querySelector("#port-name");
const versionLabel = document.getElementById("interface-version");
let conn = null; /* SerialConnection */
let connected = false;
const setConnected = (v) => {
connected = v;
onConnect(v);
};
function portName(port) {
const info = port.getInfo ? port.getInfo() : {};
const parts = [];
if (info.usbVendorId) parts.push("VID " + info.usbVendorId.toString(16));
if (info.usbProductId) parts.push("PID " + info.usbProductId.toString(16));
return parts.length ? parts.join(" ") : "serial device";
}
async function connect(resetBoard) {
if (!("serial" in navigator)) {
modal(
"Unsupported Browser",
"Web Serial is not available in this browser.\n\n" +
"Use Chrome, Edge, Opera or Firefox 151+ on desktop over HTTPS (or localhost)."
);
return;
}
let port;
try {
port = await navigator.serial.requestPort();
} catch (e) {
/* user cancelled */
return;
}
try {
await port.open({ baudRate: 9600, bufferSize: 255 });
if (resetBoard) {
/* Classic Arduino reset: pulse DTR to reboot the board. */
await port.setSignals({ dataTerminalReady: false });
await sleep(100);
await port.setSignals({ dataTerminalReady: true });
}
/* Wait for the Uno to boot out of the (opti)bootloader and for
the OBI firmware to initialise its serial port. */
await sleep(2000);
conn = new SerialConnection(port);
await conn.start();
portNameEl.textContent = portName(port) + " (9600 baud)";
debug.info("Opened serial port: " + portNameEl.textContent);
connected = true; /* allow request() while we read the version */
const version = await getVersion();
versionLabel.textContent = "Firmware version: " + version;
debug.info("Arduino OBI interface version " + version);
onConnect(true);
toast("Connected to Arduino OBI (firmware " + version + ")");
} catch (e) {
try {
await port.close();
} catch (err) {
/* ignore */
}
setConnected(false);
portNameEl.textContent = "No port connected";
versionLabel.textContent = "Firmware version: -";
debug.error("Error opening serial port: " + e.message);
modal(
"Connection Error",
"Could not open the serial port:\n\n" +
e.message +
"\n\nCheck that the port is not in use by another application."
);
}
}
async function disconnect() {
if (conn) {
await conn.close();
conn = null;
}
setConnected(false);
versionLabel.textContent = "Firmware version: -";
portNameEl.textContent = "No port connected";
debug.info("Closed serial port");
}
function isConnected() {
return connected;
}
async function getVersion() {
const response = await request(INTERFACE_VERSION_CMD, 5);
return Array.from(response.slice(2)).join(".");
}
async function request(requestBytes, maxAttempts = 5) {
if (!connected || !conn) {
throw new ConnectionError(
"Serial port is not open. Please connect to the Arduino first."
);
}
const expectedLength = requestBytes[2] + 2;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
debug.send(">> " + hexDump(requestBytes.slice(3)));
try {
conn.clear();
await conn.write(requestBytes);
if (requestBytes[2] === 0) {
return undefined;
}
const response = await conn.read(expectedLength, 3000);
debug.recv("<< " + hexDump(response.slice(2)));
if (response.length === 0) {
throw new Error(
"No response received from Arduino (expected " +
expectedLength +
" bytes). Check that a battery is connected."
);
}
if (response.length !== expectedLength) {
throw new Error(
"Incomplete response: received " +
response.length +
" bytes, expected " +
expectedLength +
". The battery may not be seated correctly."
);
}
if (Array.from(response.slice(2)).every((b) => b === 0xff)) {
throw new Error(
"Invalid response: all bytes are 0xFF. The battery may not be communicating correctly."
);
}
return response;
} catch (e) {
if (e instanceof ConnectionError) throw e;
debug.error(
"Attempt " + attempt + "/" + maxAttempts + " failed: " + e.message
);
}
}
throw new ConnectionError(
"Failed to get a valid response after " +
maxAttempts +
" attempts. Ensure the Arduino is connected and a battery is inserted."
);
}
return {
connect,
disconnect,
isConnected,
request,
};
}
OBI.interfaces["arduino_obi"] = {
getDisplayName: () => "Arduino OBI",
createWidget,
};
})();