| /* |
| * 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(); |
| } |
| }; |