UNPKG

microbyte

Version:

A wrapper for bluetooth and USB interactivity between browsers and micro:bits

1,085 lines (1,069 loc) 42.8 kB
import { WebUSB, CortexM, DAPLink } from 'dapjs'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * (c) 2023, Center for Computational Thinking and Design at Aarhus University and contributors * * SPDX-License-Identifier: MIT */ /* eslint-disable @typescript-eslint/no-namespace */ /** * References to the Bluetooth Profile UUIDs. */ var MBSpecs; (function (MBSpecs) { /** * The UUIDs of the services available on the micro:bit. */ let Services; (function (Services) { /** * The UUID of the micro:bit's UART service. */ Services.UART_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'; /** * The micro:bits accelerometer service. */ Services.ACCEL_SERVICE = 'e95d0753-251d-470a-a062-fa1922dfa9a8'; /** * The device information service. Exposes information about manufacturer, vendor, and firmware version. */ Services.DEVICE_INFO_SERVICE = '0000180a-0000-1000-8000-00805f9b34fb'; /** * Used for controlling the LEDs on the micro:bit. */ Services.LED_SERVICE = 'e95dd91d-251d-470a-a062-fa1922dfa9a8'; /** * The UUID of the micro:bit's IO service. */ Services.IO_SERVICE = 'e95d127b-251d-470a-a062-fa1922dfa9a8'; /** * Service for buttons on the micro:bit. */ Services.BUTTON_SERVICE = 'e95d9882-251d-470a-a062-fa1922dfa9a8'; })(Services = MBSpecs.Services || (MBSpecs.Services = {})); /** * The UUIDs of the characteristics available on the micro:bit. */ let Characteristics; (function (Characteristics) { /** * Characteristic for the A button. */ Characteristics.BUTTON_A = 'e95dda90-251d-470a-a062-fa1922dfa9a8'; /** * Characteristic for the B button. */ Characteristics.BUTTON_B = 'e95dda91-251d-470a-a062-fa1922dfa9a8'; /** * The accelerometer data characteristic. */ Characteristics.ACCEL_DATA = 'e95dca4b-251d-470a-a062-fa1922dfa9a8'; /** * IO data characteristic. Used for controlling IO pins on the micro:bit. */ Characteristics.IO_DATA = 'e95d8d00-251d-470a-a062-fa1922dfa9a8'; /** * Allows the state of any|all LEDs in the 5x5 grid to be set to on or off with a single GATT operation. * * Octet 0, LED Row 1: bit4 bit3 bit2 bit1 bit0 * * Octet 1, LED Row 2: bit4 bit3 bit2 bit1 bit0 * * Octet 2, LED Row 3: bit4 bit3 bit2 bit1 bit0 * * Octet 3, LED Row 4: bit4 bit3 bit2 bit1 bit0 * * Octet 4, LED Row 5: bit4 bit3 bit2 bit1 bit0 */ Characteristics.LED_MATRIX_STATE = 'e95d7b77-251d-470a-a062-fa1922dfa9a8'; /** * The model number of the micro:bit as a string. */ Characteristics.MODEL_NUMBER = '00002a24-0000-1000-8000-00805f9b34fb'; /** * The UUID of the micro:bit's UART TX characteristic. * Used to listen for data from the micro:bit. */ Characteristics.UART_DATA_TX = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; /** * The UUID of the micro:bit's UART RX characteristic. * Used for sending data to the micro:bit. */ Characteristics.UART_DATA_RX = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'; })(Characteristics = MBSpecs.Characteristics || (MBSpecs.Characteristics = {})); let USBSpecs; (function (USBSpecs) { USBSpecs.PRODUCT_ID = 516; USBSpecs.VENDOR_ID = 3368; USBSpecs.FICR = 0x10000000; USBSpecs.DEVICE_ID_1 = 0x064; USBSpecs.MICROBIT_NAME_LENGTH = 5; USBSpecs.MICROBIT_NAME_CODE_LETTERS = 5; })(USBSpecs = MBSpecs.USBSpecs || (MBSpecs.USBSpecs = {})); (function (Button) { Button[Button["A"] = 0] = "A"; Button[Button["B"] = 1] = "B"; })(MBSpecs.Button || (MBSpecs.Button = {})); (function (ButtonStates) { ButtonStates[ButtonStates["Released"] = 0] = "Released"; ButtonStates[ButtonStates["Pressed"] = 1] = "Pressed"; ButtonStates[ButtonStates["LongPressed"] = 2] = "LongPressed"; })(MBSpecs.ButtonStates || (MBSpecs.ButtonStates = {})); /** * Ordered list of all IO pins. Such as 0, 2, '3V', 'GND', etc. */ MBSpecs.IO_PIN_LAYOUT = [ 3, 0, 4, 5, 6, 7, 1, 8, 9, 10, 11, 12, 2, 13, 14, 15, 16, 17, '3V', 18, 19, 20, 21, 'GND', 24, ]; /** * Utilities for working with the micro:bit's Bluetooth Profile. */ class Utility { /** * Fetches the model number of the micro:bit. * @param {BluetoothRemoteGATTServer} gattServer The GATT server to read from. * @return {Promise<number>} The model number of the micro:bit. 1 for the original, 2 for the new. */ static getModelNumber(gattServer) { return __awaiter(this, void 0, void 0, function* () { try { const deviceInfo = yield gattServer.getPrimaryService(Services.DEVICE_INFO_SERVICE); // TODO: Next line has been observed to fail. Proper error handling needed. // Triage the issue, if no cause can be found please remove this todo. const modelNumber = yield deviceInfo.getCharacteristic(Characteristics.MODEL_NUMBER); // Read the value and convert it to UTF-8 (as specified in the Bluetooth specification). const modelNumberValue = yield modelNumber.readValue(); const decodedModelNumber = new TextDecoder().decode(modelNumberValue); // The model number either reads "BBC micro:bit" or "BBC micro:bit V2.0". Still unsure if those are the only cases. if (decodedModelNumber.toLowerCase() === 'BBC micro:bit'.toLowerCase()) { return 1; } if (decodedModelNumber.toLowerCase().includes('BBC micro:bit v2'.toLowerCase())) { return 2; } } catch (e) { console.log(e); } throw new Error('Could not read model number'); }); } /** * Converts a micro:bit serial number to it's corresponding friendly name * @param {number} serialNo The serial number of the micro:bit * @returns {string} the name of the micro:bit. */ static serialNumberToName(serialNo) { let d = USBSpecs.MICROBIT_NAME_CODE_LETTERS; let ld = 1; let name = ''; for (let i = 0; i < USBSpecs.MICROBIT_NAME_LENGTH; i++) { const h = Math.floor((serialNo % d) / ld); serialNo -= h; d *= USBSpecs.MICROBIT_NAME_CODE_LETTERS; ld *= USBSpecs.MICROBIT_NAME_CODE_LETTERS; name = Utility.CODEBOOK_USB[i][h] + name; } return name; } static messageToDataview(message, delimiter = '#') { if (delimiter.length != 1) { throw new Error('The delimiter must be 1 character long'); } const fullMessage = `${message}${delimiter}`; const view = new DataView(new ArrayBuffer(fullMessage.length)); for (let i = 0; i < fullMessage.length; i++) { view.setUint8(i, fullMessage.charCodeAt(i)); } return view; } /** * Converts a pairing pattern to a name. * See guide on microbit names to understand how a pattern is turned into a name * https://support.microbit.org/support/solutions/articles/19000067679-how-to-find-the-name-of-your-micro-bit * @param {boolean[]} pattern The pattern to convert. * @returns {string} The name of the micro:bit. */ static patternToName(pattern) { const code = [' ', ' ', ' ', ' ', ' ']; for (let col = 0; col < USBSpecs.MICROBIT_NAME_LENGTH; col++) { for (let row = 0; row < USBSpecs.MICROBIT_NAME_LENGTH; row++) { if (pattern[row * USBSpecs.MICROBIT_NAME_LENGTH + col]) { // Find the first vertical on/true in each column code[col] = this.CODEBOOK_BLUETOOTH[row][col]; // Use code-book to find char break; // Rest of column is irrelevant } // If we get to here the pattern is not legal, and the returned name // will not match any microbit. } } return code.join(''); } static isPairingPattermValid(pattern) { for (let col = 0; col < USBSpecs.MICROBIT_NAME_LENGTH; col++) { let isAnyHighlighted = false; for (let row = 0; row < USBSpecs.MICROBIT_NAME_LENGTH; row++) { if (pattern[row * USBSpecs.MICROBIT_NAME_LENGTH + col]) { isAnyHighlighted = true; } } if (!isAnyHighlighted) { return false; } } return true; } /** * Converts a name to a pairing pattern. * IMPORTANT: Assumes correct microbit name. Not enough error handling for * incorrect names. * @param {string} name The name of the micro:bit * @returns {boolean[]} The pairing pattern */ static nameToPattern(name) { const pattern = new Array(25).fill(true); // if wrong name length, return empty pattern if (name.length != USBSpecs.MICROBIT_NAME_LENGTH) { return pattern.map(() => false); } for (let column = 0; column < USBSpecs.MICROBIT_NAME_LENGTH; column++) { for (let row = 0; row < USBSpecs.MICROBIT_NAME_LENGTH; row++) { if (this.CODEBOOK_BLUETOOTH[row][column] === name.charAt(column)) { break; } pattern[5 * row + column] = false; } } return pattern; } /** * Converts a binary number represented as an array of numbers into an octet. * * @param array Bitmap array to convert. * @returns {number} The octet. */ static arrayToOctet(array) { let numArray = array; if (typeof array[0] === 'boolean') { const typedArray = array; numArray = typedArray.map((value) => (value ? 1 : 0)); } let sum = 0; // We track the bit pos because the value of the octet, will just be the decimal value of the bit representation. let bitPos = 0; // Walk in reverse order to match the order of the micro:bit. for (let i = numArray.length - 1; i >= 0; i--) { // Just calculate the decimal value of the bit position and add it to the sum. sum += numArray[i] * Math.pow(2, bitPos); bitPos++; } return sum; } } Utility.CODEBOOK_USB = [ ['z', 'v', 'g', 'p', 't'], ['u', 'o', 'i', 'e', 'a'], ['z', 'v', 'g', 'p', 't'], ['u', 'o', 'i', 'e', 'a'], ['z', 'v', 'g', 'p', 't'], ]; /** * This is a version of the microbit codebook, where the original codebook is transposed * and the rows are flipped. This gives an easier to use version for the bluetooth pattern * connection. * This could be done programatically, but having it typed out hopefully helps * with the understanding of pattern <-> friendly name conversion */ Utility.CODEBOOK_BLUETOOTH = [ ['t', 'a', 't', 'a', 't'], ['p', 'e', 'p', 'e', 'p'], ['g', 'i', 'g', 'i', 'g'], ['v', 'o', 'v', 'o', 'v'], ['z', 'u', 'z', 'u', 'z'], ]; MBSpecs.Utility = Utility; })(MBSpecs || (MBSpecs = {})); var MBSpecs$1 = MBSpecs; /** * The state of the Microbit device */ var MicrobitDeviceState; (function (MicrobitDeviceState) { /** * The device is fully connected */ MicrobitDeviceState["CONNECTED"] = "CONNECTED"; /** * The device is disconnected */ MicrobitDeviceState["DISCONNECTED"] = "DISCONNECTED"; /** * The device is connecting */ MicrobitDeviceState["CONNECTING"] = "CONNECTING"; /** * The device has been connected, and is being initialized */ MicrobitDeviceState["INITIALIZING"] = "INITIALIZING"; /** * The device is reconnecting */ MicrobitDeviceState["RECONNECTING"] = "RECONNECTING"; /** * The device is closed */ MicrobitDeviceState["CLOSED"] = "CLOSED"; })(MicrobitDeviceState || (MicrobitDeviceState = {})); const debugLog = (...message) => { }; class USBController { constructor() { this.webUsb = undefined; this.device = undefined; } connect() { return __awaiter(this, void 0, void 0, function* () { const requestOptions = { filters: [ { vendorId: MBSpecs$1.USBSpecs.VENDOR_ID, productId: MBSpecs$1.USBSpecs.PRODUCT_ID, }, ], }; try { const device = yield navigator.usb.requestDevice(requestOptions); this.device = device; this.webUsb = new WebUSB(device); } catch (e) { return Promise.reject(e); } }); } getSerialNumber() { var _a; return (_a = this.device) === null || _a === void 0 ? void 0 : _a.serialNumber; } getModelNumber() { const serialNumber = this.getSerialNumber(); if (!serialNumber) { throw new Error("Cannot get model number. Cannot read serialnumber. Is it connected?"); } const sernoPrefix = serialNumber.substring(0, 4); if (parseInt(sernoPrefix) < 9903) return 1; else return 2; } getFriendlyName() { return __awaiter(this, void 0, void 0, function* () { let result = ''; let cortex = undefined; try { if (!this.webUsb) { return Promise.reject("WebUSB not available, make sure to connect it first"); } cortex = new CortexM(this.webUsb); yield cortex.connect(); // Microbit only uses MSB of serial number const serial = yield cortex.readMem32(MBSpecs$1.USBSpecs.FICR + MBSpecs$1.USBSpecs.DEVICE_ID_1); result = MBSpecs$1.Utility.serialNumberToName(serial); return Promise.resolve(result); } catch (e) { return Promise.reject(e); } finally { if (cortex) { cortex.disconnect(); } } }); } flashHex(hex, progressCallback) { return __awaiter(this, void 0, void 0, function* () { if (!this.webUsb) { throw new Error("Cannot flash hex, no device connected. Connect it first"); } const target = new DAPLink(this.webUsb); target.on(DAPLink.EVENT_PROGRESS, (progress) => { progressCallback(progress); }); try { yield target.connect(); yield target.flash(hex); yield target.disconnect(); } catch (error) { console.log(error); return Promise.reject(error); } return Promise.resolve(); }); } disconnect() { if (this.webUsb) { this.webUsb.close(); } } } class Microbit { constructor() { this.device = undefined; this.handler = undefined; this.usbController = undefined; } setDevice(device) { this.device = device; } getId() { var _a; return (_a = this.device) === null || _a === void 0 ? void 0 : _a.getId(); } connect() { if (!this.device) { throw new Error("Device not set"); } if ([MicrobitDeviceState.DISCONNECTED, MicrobitDeviceState.CLOSED].includes(this.device.getState())) { this.device.connect(); } } setHandler(handler) { this.handler = handler; if (this.device) { this.device.setHandler(this.handler); } } getHandler() { return this.handler; } sendMessage(message) { return __awaiter(this, void 0, void 0, function* () { if (this.device) { yield this.device.sendMessage(message); } else { console.warn("Cannot send message, there's no device attached!"); } }); } disconnect() { if (this.device) { this.device.setAutoReconnect(false); this.device.disconnect(); } } setAutoReconnect(shouldReconnectAutomatically) { var _a; if (this.device) { (_a = this.device) === null || _a === void 0 ? void 0 : _a.setAutoReconnect(shouldReconnectAutomatically); } } isAutoReconnectEnabled() { var _a, _b; return (_b = (_a = this.device) === null || _a === void 0 ? void 0 : _a.isAutoReconnectEnabled()) !== null && _b !== void 0 ? _b : false; } getDeviceState() { if (this.device) { return this.device.getState(); } return MicrobitDeviceState.CLOSED; } setLEDMatrix(matrix) { return __awaiter(this, void 0, void 0, function* () { if (this.device) { yield this.device.setLEDMatrix(matrix); } }); } setIOPin(pin, on) { return __awaiter(this, void 0, void 0, function* () { if (this.device) { yield this.device.setIOPin(pin, on); } }); } getUsbController() { if (!this.usbController) { this.usbController = new USBController(); } return this.usbController; } getLastVersion() { if (this.device) { return this.device.getLastVersion(); } return undefined; } getLastName() { if (this.device) { return this.device.getLastName(); } return undefined; } getDevice() { return this.device; } } class MicrobitBluetoothDeviceServices { constructor(bluetoothDevice) { this.bluetoothDevice = bluetoothDevice; this.accelerometerListener = undefined; this.buttonAListener = undefined; this.buttonBListener = undefined; this.uartTxListener = undefined; this.uartRxCharacteristic = undefined; this.LEDMatrixCharacteristic = undefined; this.IOPinCharacteristic = undefined; this.accelerometerHandler = undefined; this.buttonAHandler = undefined; this.buttonBHandler = undefined; this.uartHandler = undefined; if (!bluetoothDevice) { throw new Error("Bluetooth device is required"); } } init() { return __awaiter(this, void 0, void 0, function* () { if (!this.bluetoothDevice.gatt) { throw new Error("No GATT server found, this is a critical error, please report it"); } yield this.initAccelerometer(this.bluetoothDevice.gatt); yield this.initButtons(this.bluetoothDevice.gatt); yield this.initUart(this.bluetoothDevice.gatt); yield this.initLED(this.bluetoothDevice.gatt); yield this.initIOService(this.bluetoothDevice.gatt); }); } setAccelerometerHandler(handler) { this.accelerometerHandler = handler; } setButtonAHandler(handler) { this.buttonAHandler = handler; } setButtonBHandler(handler) { this.buttonBHandler = handler; } setUartHandler(handler) { this.uartHandler = handler; } initAccelerometer(gatt) { return __awaiter(this, void 0, void 0, function* () { const accelService = yield gatt.getPrimaryService(MBSpecs$1.Services.ACCEL_SERVICE); const accelCharacteristic = yield accelService.getCharacteristic(MBSpecs$1.Characteristics.ACCEL_DATA); yield accelCharacteristic.startNotifications(); if (this.accelerometerListener) { accelCharacteristic.removeEventListener('characteristicvaluechanged', this.accelerometerListener); } this.accelerometerListener = (event) => { const target = event.target; const x = target.value.getInt16(0, true); const y = target.value.getInt16(2, true); const z = target.value.getInt16(4, true); if (this.accelerometerHandler) { this.accelerometerHandler(x, y, z); } }; accelCharacteristic.addEventListener('characteristicvaluechanged', this.accelerometerListener); }); } initButtons(gatt) { return __awaiter(this, void 0, void 0, function* () { const buttonService = yield gatt.getPrimaryService(MBSpecs$1.Services.BUTTON_SERVICE); const buttonACharacteristic = yield buttonService.getCharacteristic(MBSpecs$1.Characteristics.BUTTON_A); const buttonBCharacteristic = yield buttonService.getCharacteristic(MBSpecs$1.Characteristics.BUTTON_B); yield buttonACharacteristic.startNotifications(); yield buttonBCharacteristic.startNotifications(); const buttonHandler = (event, handler) => { const target = event.target; const stateId = target.value.getUint8(0); let state = MBSpecs$1.ButtonStates.Released; if (stateId === 1) { state = MBSpecs$1.ButtonStates.Pressed; } if (stateId === 2) { state = MBSpecs$1.ButtonStates.LongPressed; } if (handler) { handler(state); } }; if (this.buttonAListener) { buttonACharacteristic.removeEventListener('characteristicvaluechanged', this.buttonAListener); } this.buttonAListener = e => buttonHandler(e, this.buttonAHandler); buttonACharacteristic.addEventListener('characteristicvaluechanged', this.buttonAListener); if (this.buttonBListener) { buttonBCharacteristic.removeEventListener('characteristicvaluechanged', this.buttonBListener); } this.buttonBListener = e => buttonHandler(e, this.buttonBHandler); buttonBCharacteristic.addEventListener('characteristicvaluechanged', this.buttonBListener); }); } initUart(gatt) { return __awaiter(this, void 0, void 0, function* () { // TX is the data that the micro:bit sends to the client const uartService = yield gatt.getPrimaryService(MBSpecs$1.Services.UART_SERVICE); const txCharacteristic = yield uartService.getCharacteristic(MBSpecs$1.Characteristics.UART_DATA_TX); yield txCharacteristic.startNotifications(); if (this.uartTxListener) { txCharacteristic.removeEventListener('characteristicvaluechanged', this.uartTxListener); } this.uartTxListener = e => { // Convert the data to a string. const receivedData = []; const target = e.target; for (let i = 0; i < target.value.byteLength; i += 1) { receivedData[i] = target.value.getUint8(i); } const receivedString = String.fromCharCode.apply(null, receivedData); if (this.uartHandler) { this.uartHandler(receivedString); } }; txCharacteristic.addEventListener('characteristicvaluechanged', this.uartTxListener); // RX is the data that the client sends to the micro:bit this.uartRxCharacteristic = yield uartService.getCharacteristic(MBSpecs$1.Characteristics.UART_DATA_RX); }); } initLED(gatt) { return __awaiter(this, void 0, void 0, function* () { const ledService = yield gatt.getPrimaryService(MBSpecs$1.Services.LED_SERVICE); const ledCharacteristic = yield ledService.getCharacteristic(MBSpecs$1.Characteristics.LED_MATRIX_STATE); this.LEDMatrixCharacteristic = ledCharacteristic; }); } initIOService(gatt) { return __awaiter(this, void 0, void 0, function* () { const ioService = yield gatt.getPrimaryService(MBSpecs$1.Services.IO_SERVICE); const ioCharacteristic = yield ioService.getCharacteristic(MBSpecs$1.Characteristics.IO_DATA); this.IOPinCharacteristic = ioCharacteristic; }); } sendMessage(message) { return __awaiter(this, void 0, void 0, function* () { const dataView = MBSpecs$1.Utility.messageToDataview(message); try { if (!this.uartRxCharacteristic) { throw new Error("UART RX characteristic not initialized"); } yield this.uartRxCharacteristic.writeValue(dataView); } catch (error) { console.error(error); } }); } setLEDMatrix(matrix) { return __awaiter(this, void 0, void 0, function* () { if (matrix.length !== 5 || matrix[0].length !== 5) { throw new Error('Matrix must be 5x5'); } if (!this.LEDMatrixCharacteristic) { throw new Error('LED Matrix characteristic not initialized'); } // To match overloads we must cast the matrix to a number[][] const numMatrix = matrix.map(row => row.map(value => (value ? 1 : 0))); // Create the dataview that will be sent through the bluetooth characteristic. const data = new Uint8Array(5); for (let i = 0; i < 5; i += 1) data[i] = MBSpecs$1.Utility.arrayToOctet(numMatrix[i]); const dataView = new DataView(data.buffer); yield this.LEDMatrixCharacteristic.writeValue(dataView); }); } setIOPin(pin, on) { return __awaiter(this, void 0, void 0, function* () { const dataView = new DataView(new ArrayBuffer(2)); dataView.setInt8(0, pin); dataView.setInt8(1, on ? 1 : 0); if (!this.IOPinCharacteristic) { throw new Error('Cannot send to output pin, have not subscribed to the IO service yet!'); } yield this.IOPinCharacteristic.writeValue(dataView); }); } } class MicrobitBluetoothDevice { constructor(bluetoothDevice) { this.bluetoothDevice = undefined; this.state = MicrobitDeviceState.CLOSED; this.shouldReconnectAutomatically = false; this.disconnectHandler = undefined; this.deviceServices = undefined; this.microbitHandler = undefined; this.microbitVersion = undefined; this.name = undefined; if (bluetoothDevice) { this.bluetoothDevice = bluetoothDevice; } } setLEDMatrix(matrix) { if (this.deviceServices) { return this.deviceServices.setLEDMatrix(matrix); } else { throw new Error("Device services not initialized"); } } setHandler(handler) { this.microbitHandler = handler; if (this.deviceServices) { this.deviceServices.setAccelerometerHandler(handler.onAccelerometerDataReceived); this.deviceServices.setUartHandler(handler.onMessageReceived); this.deviceServices.setButtonAHandler(handler.onButtonAPressed); this.deviceServices.setButtonBHandler(handler.onButtonBPressed); } } sendMessage(message) { return __awaiter(this, void 0, void 0, function* () { if (this.deviceServices) { yield this.deviceServices.sendMessage(message); } else { console.warn("Cannot send message, there are no device services attached to the microbit bluetooth device"); } }); } close() { var _a; if (this.bluetoothDevice) { (_a = this.bluetoothDevice.gatt) === null || _a === void 0 ? void 0 : _a.disconnect(); } this.bluetoothDevice = undefined; this.setState(MicrobitDeviceState.CLOSED); } connect(name) { return __awaiter(this, void 0, void 0, function* () { var _a; if (!this.microbitHandler) { console.warn("micro:bit handler has not been set, some functionality may not work properly"); } let timeout = undefined; if (this.getState() !== MicrobitDeviceState.CLOSED) { timeout = setTimeout(() => { this.unsetBluetoothDevice(new Error("Connection failed, timeout reached")); throw new Error("Connection failed, timeout reached"); }, 10000); } try { // We need to remember the device in case we need to shut it down gracefully const rememberedDevice = Object.assign({}, this.bluetoothDevice); yield this.connectBluetoothDevice(name); this.assignDisconnectHandler(); this.deviceServices = new MicrobitBluetoothDeviceServices(this.bluetoothDevice); if (this.microbitHandler) { // Reassign the handler to ensure it works as before this.setHandler(this.microbitHandler); } this.setState(MicrobitDeviceState.INITIALIZING); yield this.deviceServices.init(); // Reveals if it's a version 1 or 2 micro:bit this.microbitVersion = yield MBSpecs$1.Utility.getModelNumber(this.bluetoothDevice.gatt); debugLog("Micro:bit version", this.microbitVersion); // Due to for example timeout, the state might have changed. if (this.state === MicrobitDeviceState.CLOSED) { debugLog("Connection failed, state is closed"); if (rememberedDevice) { (_a = rememberedDevice.gatt) === null || _a === void 0 ? void 0 : _a.disconnect(); // Shutdown the connection gracefully } return; } this.setState(MicrobitDeviceState.CONNECTED); debugLog("Connected to micro:bit"); } catch (error) { this.unsetBluetoothDevice(error); } finally { if (timeout) { clearTimeout(timeout); } } }); } unsetBluetoothDevice(error) { if (this.microbitHandler) { this.state === MicrobitDeviceState.RECONNECTING && this.microbitHandler.onReconnectError(error); this.state === MicrobitDeviceState.CONNECTING && this.microbitHandler.onConnectError(error); } this.setState(MicrobitDeviceState.CLOSED); if (this.bluetoothDevice) { if (this.bluetoothDevice.gatt) { this.bluetoothDevice.gatt.disconnect(); } this.bluetoothDevice = undefined; } } connectBluetoothDevice(name) { return __awaiter(this, void 0, void 0, function* () { var _a; if (!this.bluetoothDevice) { this.setState(MicrobitDeviceState.CONNECTING); this.bluetoothDevice = yield this.requestDevice(name); } else { this.setState(MicrobitDeviceState.RECONNECTING); } if (this.bluetoothDevice.gatt) { yield ((_a = this.bluetoothDevice.gatt) === null || _a === void 0 ? void 0 : _a.connect()); } }); } assignDisconnectHandler() { var _a; if (!this.bluetoothDevice) { return; } // Removing and adding the event listener to avoid multiple listeners if (this.disconnectHandler) { this.bluetoothDevice.removeEventListener('gattserverdisconnected', this.disconnectHandler); } this.disconnectHandler = () => __awaiter(this, void 0, void 0, function* () { return yield this.handleDisconnectEvent(); }); (_a = this.bluetoothDevice) === null || _a === void 0 ? void 0 : _a.addEventListener('gattserverdisconnected', this.disconnectHandler); } handleDisconnectEvent() { return __awaiter(this, void 0, void 0, function* () { // Some cleanup this.disconnectedCleanup(); if (this.shouldReconnectAutomatically) { this.setState(MicrobitDeviceState.DISCONNECTED); yield this.attemptReconnect(); } else { this.setState(MicrobitDeviceState.CLOSED); } }); } attemptReconnect() { return __awaiter(this, void 0, void 0, function* () { this.setState(MicrobitDeviceState.RECONNECTING); try { debugLog("Reconnecting to micro:bit"); yield this.connect(); } catch (error) { } }); } getState() { var _a; if (this.bluetoothDevice) { if (this.bluetoothDevice.gatt) { if ((_a = this.bluetoothDevice.gatt) === null || _a === void 0 ? void 0 : _a.connected) { return MicrobitDeviceState.CONNECTED; } } else { return MicrobitDeviceState.CLOSED; } } debugLog("getState: STATE->" + this.state); return this.state; } setAutoReconnect(shouldReconnectAutomatically) { this.shouldReconnectAutomatically = shouldReconnectAutomatically; } isAutoReconnectEnabled() { return this.shouldReconnectAutomatically; } disconnect() { this.setState(MicrobitDeviceState.CLOSED); } disconnectedCleanup() { var _a; if (this.bluetoothDevice) { (_a = this.bluetoothDevice.gatt) === null || _a === void 0 ? void 0 : _a.disconnect(); } } setState(state) { return __awaiter(this, void 0, void 0, function* () { this.state = state; debugLog("Setting state to", state, "has handler", this.microbitHandler); if (this.microbitHandler) { switch (state) { case MicrobitDeviceState.CONNECTED: if (!this.microbitVersion) { this.microbitHandler.onConnectError(new Error("Could not determine micro:bit version")); } this.microbitHandler.onConnected(this.microbitVersion); break; case MicrobitDeviceState.DISCONNECTED: this.microbitHandler.onDisconnected(); break; case MicrobitDeviceState.CONNECTING: this.microbitHandler.onConnecting(); break; case MicrobitDeviceState.RECONNECTING: this.microbitHandler.onReconnecting(); break; case MicrobitDeviceState.CLOSED: this.microbitHandler.onClosed(); break; case MicrobitDeviceState.INITIALIZING: this.microbitHandler.onInitializing(); break; } } }); } requestDevice(name) { return __awaiter(this, void 0, void 0, function* () { this.name = name; const filters = name ? [{ namePrefix: `BBC micro:bit [${name}]` }] : [{ namePrefix: `BBC micro:bit` }]; const device = yield navigator.bluetooth.requestDevice({ filters: filters, optionalServices: [ MBSpecs$1.Services.UART_SERVICE, MBSpecs$1.Services.ACCEL_SERVICE, MBSpecs$1.Services.DEVICE_INFO_SERVICE, MBSpecs$1.Services.LED_SERVICE, MBSpecs$1.Services.IO_SERVICE, MBSpecs$1.Services.BUTTON_SERVICE, ], }); return device; }); } setIOPin(pin, on) { return __awaiter(this, void 0, void 0, function* () { if (this.deviceServices) { yield this.deviceServices.setIOPin(pin, on); } else { throw new Error("Device services not initialized"); } }); } getLastVersion() { if (this.microbitVersion) { return this.microbitVersion; } return undefined; } getLastName() { return this.name; } getId() { var _a; return (_a = this.bluetoothDevice) === null || _a === void 0 ? void 0 : _a.id; } } class PairingPattern { /** * Converts a pairing pattern to a name. * See guide on microbit names to understand how a pattern is turned into a name * https://support.microbit.org/support/solutions/articles/19000067679-how-to-find-the-name-of-your-micro-bit * @param {boolean[][]} pattern The pattern to convert. * @returns {string} The name of the micro:bit. */ static patternToName(pattern) { const code = [" ", " ", " ", " ", " "]; const nameLength = PairingPattern.MICROBIT_NAME_LENGTH; for (let col = 0; col < nameLength; col++) { for (let row = 0; row < nameLength; row++) { if (pattern[row][col]) { // Find the first vertical on/true in each column code[col] = PairingPattern.CODEBOOK[row][col]; // Use code-book to find char break; // Rest of column is irrelevant } // If we get to here the pattern is not legal, and the returned name // will not match any microbit. } } return code.join(""); } /** * Converts a name to a pairing pattern. * IMPORTANT: Assumes correct microbit name. Not enough error handling for * incorrect names. * @param {string} name The name of the micro:bit * @returns {boolean[][]} The pairing pattern */ static nameToPattern(name) { const nameLength = PairingPattern.MICROBIT_NAME_LENGTH; const pattern = []; // if wrong name length, return empty pattern if (name.length != nameLength) { throw new Error("Couldn't convert name to pattern. Names provided must be of length 5!"); } for (let i = 0; i < nameLength; i++) { pattern.push([true, true, true, true, true]); } for (let column = 0; column < nameLength; column++) { for (let row = 0; row < nameLength; row++) { if (PairingPattern.CODEBOOK[row][column] === name.charAt(column)) { break; } pattern[row][column] = false; } } return pattern; } } PairingPattern.MICROBIT_NAME_LENGTH = 5; /** * Codebook for computing name from pairing pattern. See * https://support.microbit.org/support/solutions/articles/19000067679-how-to-find-the-name-of-your-micro-bit */ PairingPattern.CODEBOOK = [ ["t", "a", "t", "a", "t"], ["p", "e", "p", "e", "p"], ["g", "i", "g", "i", "g"], ["v", "o", "v", "o", "v"], ["z", "u", "z", "u", "z"], ]; export { MBSpecs$1 as MBSpecs, Microbit, MicrobitBluetoothDevice, MicrobitDeviceState, PairingPattern, USBController };