blob: 9691f9ceba3edfb42a52085f64e6fa5f004c150c [file] [log] [blame]
Serge Bazanski81981362021-03-06 13:08:00 +01001/*
2 * WebI2C, a web interface for flashing I2C EEPROMS, notably FRU EEPROMs for
3 * the HBJ11.
4 */
5
6import { FRUParser, HBJ11FRUAssembler } from './fru.js';
7import { Status, StatusFromU8, USBI2CClassInterface } from './i2c.js';
8
9/**
10 * I2CDevice is an I2C device (eg. EEPROM) on the I2C bus, attached via a
11 * Programmer.
12 */
13class I2CDevice {
14 constructor(programmer, addr) {
15 this.programmer = programmer;
16 this.addr = addr;
17 this.dump = "";
18 }
19
20 /**
21 * Treat this device as an I2C EEPROM (eg. 24C02) and read its content.
22 * @param {number} addr - The address in the EEPROM to start reading at.
23 * @param {number} length - Count of bytes to read starting at address.
24 * @returns {Promise<Uint8Array>} Contents of the EEPROM.
25 */
26 async readFlash(addr, length) {
27 // Always send a non-zero seek, otherwise 24C02 sometimes NACKs?
28 await this.programmer.writeI2C(this.addr, new Uint8Array([1]));
29 await this.programmer.readI2C(this.addr, 1);
30
31 // Chunk up reads into 128 bytes.
32 let i = 0;
33 const max_chunk_size = 128;
34 let flash = new Uint8Array(length);
35 while (i < length) {
36 await this.programmer.writeI2C(this.addr, new Uint8Array([i]));
37 let chunk_size = length - i;
38 if (chunk_size > max_chunk_size) {
39 chunk_size = max_chunk_size;
40 }
41 let res = await this.programmer.readI2C(this.addr, chunk_size);
42 flash.set(new Uint8Array(res.buffer), i);
43 i += chunk_size;
44 }
45 return flash;
46 }
47
48 /**
49 * Threat this devices as an I2C EEPROM on a HBJ11 and flash it with a given
50 * serial nyumber.
51 * @param {string} serial - The serial number of the HJB11 to brand it with.
52 * @param {HTMLButtonelement} button - Button used to trigger this action,
53 * will be disabled while the flashing is
54 * performed.
55 */
56 async writeHBJ11(serial, button) {
57 // Always send a non-zero seek, otherwise 24C02 sometimes NACKs?
58 await this.programmer.writeI2C(this.addr, new Uint8Array([1]));
59 await this.programmer.readI2C(this.addr, 1);
60
61 let text = button.innerText;
62 button.disabled = true;
63 button.innerText = "Flashing...";
64
65 let data = new HBJ11FRUAssembler(serial).assemble();
66
67 // Chunk up writes into 16 bytes.
68 let chunks = [];
69 for (let i = 0; i < data.length; i+= 16) {
70 chunks.push([i].concat(Array.from(data.slice(i, i+16))));
71 }
72
73 for (const chunk of chunks) {
74 await this.programmer.writeI2C(this.addr, new Uint8Array(chunk));
75 }
76
77 button.disabled = false;
78 button.innerText = text;
79 }
80
81 render(div) {
82 div.innerHTML = "";
83 let deviceName = document.createElement("div");
84 deviceName.className = "deviceName";
85 deviceName.appendChild(document.createTextNode(`Device 0x${this.addr.toString(16)}`));
86
87 div.appendChild(deviceName);
88
89 let deviceOptions = document.createElement("div");
90 deviceOptions.className = "deviceOptions";
91
92 let readButton = document.createElement("button");
93 readButton.appendChild(document.createTextNode("Read flash"));
94 readButton.onclick = async () => {
95 let res = await this.readFlash(this.addr, 256);
96
97 this.dump = "";
98 const hex = "0123456789ABCDEF";
99 for (let i = 0; i < res.length; i += 16) {
100 let block = res.slice(i, Math.min(i+16, res.length));
101 let addr = ("0000" + i.toString(16)).slice(-4);
102 let codes = Array.from(block.values()).map((code) => {
103 return " " + hex[(0xF0 & code) >> 4] + hex[0x0f & code];
104 }).join("");
105 codes += " ".repeat(16 - block.length);
106 let chars = Array.from(block.values()).map((code) => {
107 if (code < 0x20 || code > 0x7e) {
108 return ".";
109 }
110 return String.fromCharCode(code);
111 }).join("");
112 codes += " ".repeat(16 - block.length);
113 this.dump += (addr + " " + codes + " " + chars + "\n");
114 }
115
116 let p = new FRUParser(res);
117 try {
118 p.parse();
119 this.dump += "\nFRU EEPROM:\n";
120 this.dump += p.stringify();
121 } catch(err) {
122 this.dump += "\nNot an FRU EEPROM: " + err;
123 }
124
125 console.log(this.dump);
126
127 this.render(div);
128 };
129 deviceOptions.appendChild(readButton);
130
131 let makeButton = document.createElement("button");
132 makeButton.appendChild(document.createTextNode("Make HBJ11"));
133 makeButton.onclick = async () => {
134 await this.writeHBJ11(window.prompt("Enter HBJ11 Serial", "A0000"), makeButton);
135 };
136 deviceOptions.appendChild(makeButton);
137
138 deviceName.appendChild(deviceOptions);
139
140 if (this.dump.length > 0) {
141 let deviceDump = document.createElement("pre");
142
143 deviceDump.className = "deviceDump";
144 deviceDump.innerText = this.dump;
145 div.appendChild(deviceDump);
146 }
147 }
148}
149
150/**
151 * A list of I2CDevices, eg. EEPROMs. Used for DOM rendering.
152 */
153class I2CDeviceList {
154 constructor() {
155 this.list = [];
156 }
157
158 set(devices) {
159 this.list = devices;
160 }
161
162 render(div) {
163 if (this.list.length === 0) {
164 div.innerHTML = "<i>No devices...</i>";
165 return;
166 }
167
168 for (const device of this.list) {
169 let deviceDiv = document.createElement("div");
170 deviceDiv.className = "device";
171 device.render(deviceDiv);
172 div.appendChild(deviceDiv);
173 }
174 }
175}
176
177/**
178 * A WebI2C compatible programmer accessed over USB.
179 */
180class Programmer {
181 /**
182 * @param {USBDevice} usb - The WebUSB device that backs this programmer.
183 */
184 constructor(usb) {
185 this.usb = usb;
186 this.i2c = new USBI2CClassInterface(usb);
187 this.devices = new I2CDeviceList();
188 }
189 /**
190 * Get programmer manufacturer name.
191 * @returns {string} The name.
192 */
193 get manufacturerName() {
194 return this.usb.manufacturerName;
195 }
196
197 /**
198 * Get programmer product name.
199 * @returns {string} The name.
200 */
201 get productName() {
202 return this.usb.productName;
203 }
204
205 /**
206 * Get programmer serial number.
207 * @returns {string} The name.
208 */
209 get serialNumber() {
210 return this.usb.serialNumber;
211 }
212
213 /**
214 * Compares two Programmers and checks if they're using the same WebUSB
215 * device underneath. This is used for housekeeping of the ProgrammerList.
216 */
217 equal(other) {
218 let one = this.usb;
219 let two = other.usb;
220 return (one.vendorId == two.vendorId)
221 && (one.productId == two.productId)
222 && (one.serialNumber == two.serialNumber);
223 }
224
225 /**
226 * Performs an I2C read on the bus of the programmer and reads the resulting
227 * data from the buffer. The readout is performed in chunks over multiple
228 * Bulk transfer.
229 * @param {number} addr - Address of the I2C device to read from.
230 * @param {number} length - Number of bytes to read from I2C (not larger than
231 * BUFFER_SIZE).
232 * @returns {object} Object with status and bufer keys. TODO(q3k): declare type.
233 */
234 async readI2C(addr, length) {
235 await this.i2c.readI2C(addr, length);
236 let status = await this.i2c.getStatus();
237 if (status !== Status.Ack) {
238 return {status: status, buffer: null};
239 }
240 let buffer = new Uint8Array(length);
241 let i = 0;
242 while (i < length) {
243 let chunkSize = length - i;
244 if (chunkSize > this.i2c.PACKET_SIZE) {
245 chunkSize = this.i2c.PACKET_SIZE;
246 }
247 let chunk = await this.readBuffer(i, chunkSize);
248 buffer.set(new Uint8Array(chunk.buffer), i);
249 i += chunkSize;
250 }
251 return {status: status, buffer: buffer};
252 }
253
254 /**
255 * Transfers data to internal buffer of programmers and performs an I2C write
256 * with the given data.
257 * @param {number} addr - Address of the I2C to write data to.
258 * @param {ArrayBuffer} data - Data to write to device.
259 */
260 async writeI2C(addr, data) {
261 let i = 0;
262 while (i < data.length) {
263 let end = i + this.i2c.PACKET_SIZE;
264 if (end > data.length) {
265 end = data.length;
266 }
267 let chunk = data.slice(i, end);
268 await this.writeBuffer(i, chunk);
269 i = end;
270 }
271 await this.i2c.writeI2C(addr, data.length);
272 }
273
274 /**
275 * Performs a scan of the I2C bus for all connected devices and upgrades the
276 * internal I2CDeviceList with found I2CDevices.
277 * @param {HTMLButtonElement} button - Button that will be disabled when the
278 * Scan is performed.
279 */
280 async scan(button) {
281 let text = button.innerText;
282 button.innerText = "Scanning...";
283 button.disabled = true;
284
285 let present = [];
286 for (let i = 0; i < 127; i++) {
287 let res = await this.readI2C(i, 1);
288 switch (res.status) {
289 case Status.Ack:
290 present.push(new I2CDevice(this, i));
291 break;
292 case Status.Nack:
293 break;
294 default:
295 throw new Error(`When scanning ${i}: ${StatusFromU8(res.status)}`);
296 }
297 }
298 this.devices.set(present);
299
300 button.disabled = false;
301 button.innerText = text;
302 }
303
304 /**
305 * Blinks the programmer's LED.
306 * @param {HTMLButtonElement} button - Button that will be disabled when the
307 * LED blinks.
308 */
309 async blink(button) {
310 let on = true;
311 button.disabled = true;
312 let text = button.innerText;
313 button.innerText = "Blinking...";
314 for (let i = 0; i < 20; i++) {
315 await this.i2c.setLED(on);
316 await new Promise(r => setTimeout(r, 100));
317 on = !on;
318 }
319 button.disabled = false;
320 button.innerText = text;
321 }
322
323 /**
324 * Requests buffer readout from device via ReadBuffer control transfer and
325 * then performs a single read via the Bulk IN endpoint.
326 * @param {number} addr - Address within the buffer to start read at.
327 * @param {number} length - Number of bytes to read (not larger than
328 * PACKET_SIZE).
329 * @returns {ArrayBuffer} Data read from buffer.
330 */
331 async readBuffer(addr, length) {
332 await this.i2c.readBuffer(addr, length);
333 let status = await this.i2c.getStatus();
334 if (status !== Status.Idle) {
335 throw new Error(`When requesting buffer: ${StatusFromU8(res.status)}`);
336 }
337 let res = await this.i2c.bulkIn(length);
338 return res.data;
339 }
340
341 /**
342 * Writes bytes to internal buffer.
343 * @param {number} addr - Address within the buffer to start write at.
344 * @param {ArrayBuffer} data - Data to write to buffer (must not be longer
345 * than PACKET_SIZE).
346 */
347 async writeBuffer(addr, data) {
348 await this.i2c.setWritePointer(addr);
349 let status = await this.i2c.getStatus();
350 if (status !== Status.Idle) {
351 throw new Error(`When setting pointer: ${StatusFromU8(res.status)}`);
352 }
353 await this.i2c.bulkOut(data);
354 }
355
356
357 render(div) {
358 let programmer = document.createElement("div");
359 programmer.className = "programmer";
360 let programmerName = document.createElement("div");
361 programmerName.className = "programmerName";
362 programmerName.appendChild(document.createTextNode(this.manufacturerName));
363 programmerName.appendChild(document.createTextNode(" "));
364 let b = document.createElement("b");
365 b.textContent = this.productName;
366 programmerName.appendChild(b);
367
368 let programmerOptions = document.createElement("div");
369 programmerOptions.className = "programmerOptions";
370
371 let blinkButton = document.createElement("button");
372 blinkButton.appendChild(document.createTextNode("Blink LED"));
373 blinkButton.onclick = async () => {
374 await this.blink(blinkButton);
375 };
376 programmerOptions.appendChild(blinkButton);
377
378 let devices = document.createElement("div");
379 devices.className = "devices";
380
381 let scanButton = document.createElement("button");
382 scanButton.appendChild(document.createTextNode("Scan I2C Bus"));
383 scanButton.onclick = async () => {
384 await this.scan(scanButton);
385 devices.innerText = "";
386 this.devices.render(devices)
387 };
388 programmerOptions.appendChild(scanButton);
389
390 programmerName.appendChild(programmerOptions);
391 programmer.appendChild(programmerName);
392
393 this.devices.render(devices)
394 programmer.append(devices);
395
396 div.appendChild(programmer);
397 }
398}
399
400/**
401 * List of Programmers, used for rendering to DOM.
402 */
403class ProgrammerList {
404 constructor(list) {
405 this.list = [];
406 for (const l of list) {
407 this.list.push(l);
408 }
409 this.status = {};
410 }
411 async addProgrammer(programmer) {
412 let existing = this.list.filter(d => d.equal(programmer));
413 if (existing.length == 0) {
414 this.list.push(programmer);
415 await programmer.i2c.open();
416 }
417 }
418 removeProgrammer(programmer) {
419 this.list = this.list.filter(d => !d.equal(programmer));
420 }
421 render() {
422 let div = document.querySelector("#programmers");
423 div.innerText = "";
424 for (const programmer of this.list) {
425 programmer.render(div);
426 }
427 }
428}
429
430if (navigator.usb === undefined || navigator.usb.requestDevice === undefined) {
431 alert("No WebUSB support! Please use a Chromium-based browser.");
432}
433
434// 'global' ProgrammerList, modified by document/USB events.
435let list = null;
436
437document.addEventListener('DOMContentLoaded', async () => {
438 let programmers = (await navigator.usb.getDevices()).map(d => new Programmer(d));
439 for (const programmer of programmers) {
440 await programmer.i2c.open();
441 }
442 list = new ProgrammerList(programmers);
443 list.render();
444});
445
446navigator.usb.addEventListener('connect', async event => {
447 await list.addProgrammer(new Programmer(event.device));
448 list.render();
449});
450
451navigator.usb.addEventListener('disconnect', event => {
452 list.removeProgrammer(new Programmer(event.device));
453 list.render();
454});
455
456document.getElementById("connect").onclick = async () => {
457 let device;
458 try {
459 device = await navigator.usb.requestDevice({
460 filters: [{
461 vendorId: 0x16c0,
462 productId: 0x27d8,
463 }]
464 });
465 } catch (err) {
466 return;
467 };
468 if (device !== undefined) {
469 await list.addProgrammer(new Programmer(device));
470 list.render();
471 }
472};