File size: 4,985 Bytes
8fd7a1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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
};