scratch-gui / src /lib /microbit-update.js
soiz1's picture
Upload folder using huggingface_hub
8fd7a1d verified
import {
isUniversalHex,
separateUniversalHex
} from '@microbit/microbit-universal-hex';
import {WebUSB, DAPLink} from 'dapjs';
import keyMirror from 'keymirror';
import log from './log.js';
import hexUrl from '../generated/microbit-hex-url.cjs';
/**
* @typedef {import('@microbit/microbit-universal-hex').IndividualHex} IndividualHex
*/
/**
* The version of a micro:bit.
* @enum {string}
*/
const DeviceVersion = keyMirror({
V1: null,
V2: null
});
const vendorId = 0x0d28;
const productId = 0x0204;
/**
* Assumes the device is a micro:bit and determines its version.
* @param {USBDevice} device The USB device to check.
* @returns {DeviceVersion} The version of the device.
* @throws {Error} If the device is not a recognized micro:bit.
*/
const getDeviceVersion = device => {
const microBitBoardId = device?.serialNumber?.substring(0, 4) ?? '';
switch (microBitBoardId) {
case '9900':
case '9901':
return DeviceVersion.V1;
case '9903':
case '9904':
case '9905':
case '9906':
return DeviceVersion.V2;
}
throw new Error('Could not identify micro:bit board version');
};
/**
* Checks micro:bit board version targetted by the hex file.
* @param {IndividualHex} hex The hex file to check.
* @returns {DeviceVersion} The version of the hex file.
* @throws {Error} If the hex file does not target a recognized micro:bit version.
*/
const getHexVersion = hex => {
switch (hex.boardId) {
case 0x9900:
case 0x9901:
return DeviceVersion.V1;
case 0x9903:
case 0x9904:
case 0x9905:
case 0x9906:
return DeviceVersion.V2;
}
throw new Error('Could not identify hex version');
};
/**
* Fetches the hex file and returns a map of micro:bit versions to hex file contents.
* @returns {Promise<Map<DeviceVersion, Uint8Array>>} A map of micro:bit versions to hex file contents.
* @throws {Error} If the fetch fails or cannot be interpreted as text.
* @throws {Error} If the hex file is not in universal format.
*/
const getHexMap = async () => {
const response = await fetch(hexUrl);
const hex = await response.text();
if (!isUniversalHex(hex)) {
throw new Error('Hex file must be in universal format');
}
const hexMap = new Map();
for (const hexObj of separateUniversalHex(hex)) {
const version = getHexVersion(hexObj);
const binary = new TextEncoder().encode(hex);
hexMap.set(version, binary);
}
return hexMap;
};
/**
* Copy the Scratch-specific hex file to the specified micro:bit.
* @param {USBDevice} device The micro:bit to update.
* @param {function(number): void} [progress] Optional function to call with progress updates in the range of [0..1].
* @returns {Promise<void>} A Promise that resolves when the update is completed.
* @throws {Error} If anything goes wrong while fetching the hex file or updating the micro:bit.
*/
const updateMicroBit = async (device, progress) => {
log.info(`Connecting to micro:bit`);
const transport = new WebUSB(device);
const target = new DAPLink(transport);
if (progress) {
target.on(DAPLink.EVENT_PROGRESS, progress);
}
log.info(`Checking micro:bit version`);
const version = getDeviceVersion(device);
log.info(`Collecting hex file`);
const hexMap = await getHexMap();
const hexData = hexMap.get(version);
if (!hexData) {
throw new Error(`Could not find hex file for micro:bit ${version}`);
}
log.info(`Connecting to micro:bit ${version}`);
await target.connect();
log.info(`Sending hex file...`);
try {
await target.flash(hexData);
} finally {
log.info('Disconnecting');
if (target.connected) {
await target.disconnect();
} else {
log.info('Already disconnected');
}
}
};
/**
* Requests a micro:bit from the browser then updates it with the Scratch-specific hex file.
* The browser is expected to prompt the user to select a micro:bit.
* @param {function(number): void} [progress] Optional function to call with progress updates in the range of [0..1].
* @returns {Promise<void>} A Promise that resolves when the update is completed.
* @throws {Error} If anything goes wrong while fetching the hex file or updating the micro:bit.
*/
const selectAndUpdateMicroBit = async progress => {
log.info('Selecting micro:bit');
const device = await navigator.usb.requestDevice({
filters: [{vendorId, productId}]
});
if (!device) {
log.info('No device selected');
return;
}
return updateMicroBit(device, progress);
};
/**
* Checks if the browser supports updating a micro:bit.
* @returns {boolean} True if the browser appears to support updating a micro:bit.
*/
const isMicroBitUpdateSupported = () =>
!!(navigator.usb && navigator.usb.requestDevice);
export {
isMicroBitUpdateSupported,
selectAndUpdateMicroBit
};