blob: 9691f9ceba3edfb42a52085f64e6fa5f004c150c [file] [log] [blame]
/*
* WebI2C, a web interface for flashing I2C EEPROMS, notably FRU EEPROMs for
* the HBJ11.
*/
import { FRUParser, HBJ11FRUAssembler } from './fru.js';
import { Status, StatusFromU8, USBI2CClassInterface } from './i2c.js';
/**
* I2CDevice is an I2C device (eg. EEPROM) on the I2C bus, attached via a
* Programmer.
*/
class I2CDevice {
constructor(programmer, addr) {
this.programmer = programmer;
this.addr = addr;
this.dump = "";
}
/**
* Treat this device as an I2C EEPROM (eg. 24C02) and read its content.
* @param {number} addr - The address in the EEPROM to start reading at.
* @param {number} length - Count of bytes to read starting at address.
* @returns {Promise<Uint8Array>} Contents of the EEPROM.
*/
async readFlash(addr, length) {
// Always send a non-zero seek, otherwise 24C02 sometimes NACKs?
await this.programmer.writeI2C(this.addr, new Uint8Array([1]));
await this.programmer.readI2C(this.addr, 1);
// Chunk up reads into 128 bytes.
let i = 0;
const max_chunk_size = 128;
let flash = new Uint8Array(length);
while (i < length) {
await this.programmer.writeI2C(this.addr, new Uint8Array([i]));
let chunk_size = length - i;
if (chunk_size > max_chunk_size) {
chunk_size = max_chunk_size;
}
let res = await this.programmer.readI2C(this.addr, chunk_size);
flash.set(new Uint8Array(res.buffer), i);
i += chunk_size;
}
return flash;
}
/**
* Threat this devices as an I2C EEPROM on a HBJ11 and flash it with a given
* serial nyumber.
* @param {string} serial - The serial number of the HJB11 to brand it with.
* @param {HTMLButtonelement} button - Button used to trigger this action,
* will be disabled while the flashing is
* performed.
*/
async writeHBJ11(serial, button) {
// Always send a non-zero seek, otherwise 24C02 sometimes NACKs?
await this.programmer.writeI2C(this.addr, new Uint8Array([1]));
await this.programmer.readI2C(this.addr, 1);
let text = button.innerText;
button.disabled = true;
button.innerText = "Flashing...";
let data = new HBJ11FRUAssembler(serial).assemble();
// Chunk up writes into 16 bytes.
let chunks = [];
for (let i = 0; i < data.length; i+= 16) {
chunks.push([i].concat(Array.from(data.slice(i, i+16))));
}
for (const chunk of chunks) {
await this.programmer.writeI2C(this.addr, new Uint8Array(chunk));
}
button.disabled = false;
button.innerText = text;
}
render(div) {
div.innerHTML = "";
let deviceName = document.createElement("div");
deviceName.className = "deviceName";
deviceName.appendChild(document.createTextNode(`Device 0x${this.addr.toString(16)}`));
div.appendChild(deviceName);
let deviceOptions = document.createElement("div");
deviceOptions.className = "deviceOptions";
let readButton = document.createElement("button");
readButton.appendChild(document.createTextNode("Read flash"));
readButton.onclick = async () => {
let res = await this.readFlash(this.addr, 256);
this.dump = "";
const hex = "0123456789ABCDEF";
for (let i = 0; i < res.length; i += 16) {
let block = res.slice(i, Math.min(i+16, res.length));
let addr = ("0000" + i.toString(16)).slice(-4);
let codes = Array.from(block.values()).map((code) => {
return " " + hex[(0xF0 & code) >> 4] + hex[0x0f & code];
}).join("");
codes += " ".repeat(16 - block.length);
let chars = Array.from(block.values()).map((code) => {
if (code < 0x20 || code > 0x7e) {
return ".";
}
return String.fromCharCode(code);
}).join("");
codes += " ".repeat(16 - block.length);
this.dump += (addr + " " + codes + " " + chars + "\n");
}
let p = new FRUParser(res);
try {
p.parse();
this.dump += "\nFRU EEPROM:\n";
this.dump += p.stringify();
} catch(err) {
this.dump += "\nNot an FRU EEPROM: " + err;
}
console.log(this.dump);
this.render(div);
};
deviceOptions.appendChild(readButton);
let makeButton = document.createElement("button");
makeButton.appendChild(document.createTextNode("Make HBJ11"));
makeButton.onclick = async () => {
await this.writeHBJ11(window.prompt("Enter HBJ11 Serial", "A0000"), makeButton);
};
deviceOptions.appendChild(makeButton);
deviceName.appendChild(deviceOptions);
if (this.dump.length > 0) {
let deviceDump = document.createElement("pre");
deviceDump.className = "deviceDump";
deviceDump.innerText = this.dump;
div.appendChild(deviceDump);
}
}
}
/**
* A list of I2CDevices, eg. EEPROMs. Used for DOM rendering.
*/
class I2CDeviceList {
constructor() {
this.list = [];
}
set(devices) {
this.list = devices;
}
render(div) {
if (this.list.length === 0) {
div.innerHTML = "<i>No devices...</i>";
return;
}
for (const device of this.list) {
let deviceDiv = document.createElement("div");
deviceDiv.className = "device";
device.render(deviceDiv);
div.appendChild(deviceDiv);
}
}
}
/**
* A WebI2C compatible programmer accessed over USB.
*/
class Programmer {
/**
* @param {USBDevice} usb - The WebUSB device that backs this programmer.
*/
constructor(usb) {
this.usb = usb;
this.i2c = new USBI2CClassInterface(usb);
this.devices = new I2CDeviceList();
}
/**
* Get programmer manufacturer name.
* @returns {string} The name.
*/
get manufacturerName() {
return this.usb.manufacturerName;
}
/**
* Get programmer product name.
* @returns {string} The name.
*/
get productName() {
return this.usb.productName;
}
/**
* Get programmer serial number.
* @returns {string} The name.
*/
get serialNumber() {
return this.usb.serialNumber;
}
/**
* Compares two Programmers and checks if they're using the same WebUSB
* device underneath. This is used for housekeeping of the ProgrammerList.
*/
equal(other) {
let one = this.usb;
let two = other.usb;
return (one.vendorId == two.vendorId)
&& (one.productId == two.productId)
&& (one.serialNumber == two.serialNumber);
}
/**
* Performs an I2C read on the bus of the programmer and reads the resulting
* data from the buffer. The readout is performed in chunks over multiple
* Bulk transfer.
* @param {number} addr - Address of the I2C device to read from.
* @param {number} length - Number of bytes to read from I2C (not larger than
* BUFFER_SIZE).
* @returns {object} Object with status and bufer keys. TODO(q3k): declare type.
*/
async readI2C(addr, length) {
await this.i2c.readI2C(addr, length);
let status = await this.i2c.getStatus();
if (status !== Status.Ack) {
return {status: status, buffer: null};
}
let buffer = new Uint8Array(length);
let i = 0;
while (i < length) {
let chunkSize = length - i;
if (chunkSize > this.i2c.PACKET_SIZE) {
chunkSize = this.i2c.PACKET_SIZE;
}
let chunk = await this.readBuffer(i, chunkSize);
buffer.set(new Uint8Array(chunk.buffer), i);
i += chunkSize;
}
return {status: status, buffer: buffer};
}
/**
* Transfers data to internal buffer of programmers and performs an I2C write
* with the given data.
* @param {number} addr - Address of the I2C to write data to.
* @param {ArrayBuffer} data - Data to write to device.
*/
async writeI2C(addr, data) {
let i = 0;
while (i < data.length) {
let end = i + this.i2c.PACKET_SIZE;
if (end > data.length) {
end = data.length;
}
let chunk = data.slice(i, end);
await this.writeBuffer(i, chunk);
i = end;
}
await this.i2c.writeI2C(addr, data.length);
}
/**
* Performs a scan of the I2C bus for all connected devices and upgrades the
* internal I2CDeviceList with found I2CDevices.
* @param {HTMLButtonElement} button - Button that will be disabled when the
* Scan is performed.
*/
async scan(button) {
let text = button.innerText;
button.innerText = "Scanning...";
button.disabled = true;
let present = [];
for (let i = 0; i < 127; i++) {
let res = await this.readI2C(i, 1);
switch (res.status) {
case Status.Ack:
present.push(new I2CDevice(this, i));
break;
case Status.Nack:
break;
default:
throw new Error(`When scanning ${i}: ${StatusFromU8(res.status)}`);
}
}
this.devices.set(present);
button.disabled = false;
button.innerText = text;
}
/**
* Blinks the programmer's LED.
* @param {HTMLButtonElement} button - Button that will be disabled when the
* LED blinks.
*/
async blink(button) {
let on = true;
button.disabled = true;
let text = button.innerText;
button.innerText = "Blinking...";
for (let i = 0; i < 20; i++) {
await this.i2c.setLED(on);
await new Promise(r => setTimeout(r, 100));
on = !on;
}
button.disabled = false;
button.innerText = text;
}
/**
* Requests buffer readout from device via ReadBuffer control transfer and
* then performs a single read via the Bulk IN endpoint.
* @param {number} addr - Address within the buffer to start read at.
* @param {number} length - Number of bytes to read (not larger than
* PACKET_SIZE).
* @returns {ArrayBuffer} Data read from buffer.
*/
async readBuffer(addr, length) {
await this.i2c.readBuffer(addr, length);
let status = await this.i2c.getStatus();
if (status !== Status.Idle) {
throw new Error(`When requesting buffer: ${StatusFromU8(res.status)}`);
}
let res = await this.i2c.bulkIn(length);
return res.data;
}
/**
* Writes bytes to internal buffer.
* @param {number} addr - Address within the buffer to start write at.
* @param {ArrayBuffer} data - Data to write to buffer (must not be longer
* than PACKET_SIZE).
*/
async writeBuffer(addr, data) {
await this.i2c.setWritePointer(addr);
let status = await this.i2c.getStatus();
if (status !== Status.Idle) {
throw new Error(`When setting pointer: ${StatusFromU8(res.status)}`);
}
await this.i2c.bulkOut(data);
}
render(div) {
let programmer = document.createElement("div");
programmer.className = "programmer";
let programmerName = document.createElement("div");
programmerName.className = "programmerName";
programmerName.appendChild(document.createTextNode(this.manufacturerName));
programmerName.appendChild(document.createTextNode(" "));
let b = document.createElement("b");
b.textContent = this.productName;
programmerName.appendChild(b);
let programmerOptions = document.createElement("div");
programmerOptions.className = "programmerOptions";
let blinkButton = document.createElement("button");
blinkButton.appendChild(document.createTextNode("Blink LED"));
blinkButton.onclick = async () => {
await this.blink(blinkButton);
};
programmerOptions.appendChild(blinkButton);
let devices = document.createElement("div");
devices.className = "devices";
let scanButton = document.createElement("button");
scanButton.appendChild(document.createTextNode("Scan I2C Bus"));
scanButton.onclick = async () => {
await this.scan(scanButton);
devices.innerText = "";
this.devices.render(devices)
};
programmerOptions.appendChild(scanButton);
programmerName.appendChild(programmerOptions);
programmer.appendChild(programmerName);
this.devices.render(devices)
programmer.append(devices);
div.appendChild(programmer);
}
}
/**
* List of Programmers, used for rendering to DOM.
*/
class ProgrammerList {
constructor(list) {
this.list = [];
for (const l of list) {
this.list.push(l);
}
this.status = {};
}
async addProgrammer(programmer) {
let existing = this.list.filter(d => d.equal(programmer));
if (existing.length == 0) {
this.list.push(programmer);
await programmer.i2c.open();
}
}
removeProgrammer(programmer) {
this.list = this.list.filter(d => !d.equal(programmer));
}
render() {
let div = document.querySelector("#programmers");
div.innerText = "";
for (const programmer of this.list) {
programmer.render(div);
}
}
}
if (navigator.usb === undefined || navigator.usb.requestDevice === undefined) {
alert("No WebUSB support! Please use a Chromium-based browser.");
}
// 'global' ProgrammerList, modified by document/USB events.
let list = null;
document.addEventListener('DOMContentLoaded', async () => {
let programmers = (await navigator.usb.getDevices()).map(d => new Programmer(d));
for (const programmer of programmers) {
await programmer.i2c.open();
}
list = new ProgrammerList(programmers);
list.render();
});
navigator.usb.addEventListener('connect', async event => {
await list.addProgrammer(new Programmer(event.device));
list.render();
});
navigator.usb.addEventListener('disconnect', event => {
list.removeProgrammer(new Programmer(event.device));
list.render();
});
document.getElementById("connect").onclick = async () => {
let device;
try {
device = await navigator.usb.requestDevice({
filters: [{
vendorId: 0x16c0,
productId: 0x27d8,
}]
});
} catch (err) {
return;
};
if (device !== undefined) {
await list.addProgrammer(new Programmer(device));
list.render();
}
};