From 99723f0fb085495ea32c17089399da26af0b0554 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 17 Jan 2026 21:05:05 +0900 Subject: [PATCH 1/9] feat: implement firmware update for MicrobitMore extension - Update prepublish.mjs to download MicrobitMore hex (V2) during build - Create microbit-more-update.js for firmware writing (V2 only) - Update connection-modal.jsx to support firmware update for microbitMore - Update .gitignore to exclude downloaded hex files Co-Authored-By: Gemini --- .gitignore | 1 + scripts/prepublish.mjs | 36 ++++++++ src/containers/connection-modal.jsx | 12 ++- src/lib/microbit-more-update.js | 126 ++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 src/lib/microbit-more-update.js diff --git a/.gitignore b/.gitignore index b5dec787d9c..e9d76713780 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ npm-* # Downloaded during "npm install" /static/microbit +/static/microbitMore # for act .secrets diff --git a/scripts/prepublish.mjs b/scripts/prepublish.mjs index a2afb4da16a..4db790eece6 100644 --- a/scripts/prepublish.mjs +++ b/scripts/prepublish.mjs @@ -110,8 +110,44 @@ const downloadMicrobitHex = async () => { console.info(`Wrote ${relativeGeneratedFile}`); }; +const downloadMicrobitMoreHex = async () => { + const url = 'https://github.com/microbit-more/pxt-mbit-more-v2/releases/download/0.2.5/microbit-mbit-more-v2-0_2_5.hex'; + console.info(`Downloading ${url}`); + const response = await crossFetch(url); + const hexBuffer = Buffer.from(await response.arrayBuffer()); + const relativeHexDir = path.join('static', 'microbitMore'); + const hexFileName = 'microbit-mbit-more-v2-0_2_5.hex'; + const relativeHexFile = path.join(relativeHexDir, hexFileName); + const absoluteDestDir = path.join(basePath, relativeHexDir); + fs.mkdirSync(absoluteDestDir, {recursive: true}); + const absoluteDestFile = path.join(basePath, relativeHexFile); + fs.writeFileSync(absoluteDestFile, hexBuffer); + + const relativeGeneratedDir = path.join('src', 'generated'); + const relativeGeneratedFile = path.join(relativeGeneratedDir, 'microbit-more-hex-url.cjs'); + const absoluteGeneratedDir = path.join(basePath, relativeGeneratedDir); + fs.mkdirSync(absoluteGeneratedDir, {recursive: true}); + const absoluteGeneratedFile = path.join(basePath, relativeGeneratedFile); + const requirePath = `./${path + .relative(relativeGeneratedDir, relativeHexFile) + .split(path.win32.sep) + .join(path.posix.sep)}`; + fs.writeFileSync( + absoluteGeneratedFile, + [ + '// This file is generated by scripts/prepublish.mjs', + '// Do not edit this file directly', + '// This file relies on a loader to turn this `require` into a URL', + `module.exports = require('${requirePath}');`, + '' // final newline + ].join('\n') + ); + console.info(`Wrote ${relativeGeneratedFile}`); +}; + const prepublish = async () => { await downloadMicrobitHex(); + await downloadMicrobitMoreHex(); }; prepublish().then( diff --git a/src/containers/connection-modal.jsx b/src/containers/connection-modal.jsx index fbb08c4a3b9..3f29bc185c3 100644 --- a/src/containers/connection-modal.jsx +++ b/src/containers/connection-modal.jsx @@ -9,6 +9,10 @@ import {connect} from 'react-redux'; import {closeConnectionModal} from '../reducers/modals'; import {isMicroBitUpdateSupported, selectAndUpdateMicroBit} from '../lib/microbit-update'; +import { + isMicroBitUpdateSupported as isMicroBitMoreUpdateSupported, + selectAndUpdateMicroBit as selectAndUpdateMicroBitMore +} from '../lib/microbit-more-update'; class ConnectionModal extends React.Component { constructor (props) { @@ -141,11 +145,15 @@ class ConnectionModal extends React.Component { label: this.props.extensionId }); - // TODO: get this functionality from the extension + if (this.props.extensionId === 'microbitMore') { + return selectAndUpdateMicroBitMore(progressCallback); + } return selectAndUpdateMicroBit(progressCallback); } render () { - const canUpdatePeripheral = (this.props.extensionId === 'microbit') && isMicroBitUpdateSupported(); + const canUpdatePeripheral = + (this.props.extensionId === 'microbit' && isMicroBitUpdateSupported()) || + (this.props.extensionId === 'microbitMore' && isMicroBitMoreUpdateSupported()); return ( { + const microBitBoardId = device?.serialNumber?.substring(0, 4) ?? ''; + switch (microBitBoardId) { + case '9900': + case '9901': + throw new Error('MicrobitMore only supports micro:bit V2'); + case '9903': + case '9904': + case '9905': + case '9906': + return DeviceVersion.V2; + } + + throw new Error('Could not identify micro:bit board version'); +}; + +/** + * Fetches the hex file and returns a map of micro:bit versions to hex file contents. + * @returns {Promise>} A map of micro:bit versions to hex file contents. + */ +const getHexMap = async () => { + const response = await fetch(hexUrl); + const hex = await response.text(); + + const hexMap = new Map(); + const binary = new TextEncoder().encode(hex); + hexMap.set(DeviceVersion.V2, binary); + + return hexMap; +}; + +/** + * Copy the MicrobitMore-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} 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 MicrobitMore-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} 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 +}; From f4e261a82f6e15424545d718ee7ddf60c62ad416 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 17 Jan 2026 21:33:06 +0900 Subject: [PATCH 2/9] feat: add 'Update my Device' button to Error and Unavailable steps - Update ErrorStep and UnavailableStep to show update button if onUpdatePeripheral is provided - Pass onUpdatePeripheral and onScanning props to these steps in ConnectionModalComponent - Ensures MicrobitMore users can reach update screen even if scanning doesn't timeout Co-Authored-By: Gemini --- .../connection-modal/connection-modal.jsx | 18 +++++++++-- .../connection-modal/error-step.jsx | 29 +++++++++++++++++- .../connection-modal/unavailable-step.jsx | 30 ++++++++++++++++++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/components/connection-modal/connection-modal.jsx b/src/components/connection-modal/connection-modal.jsx index 3a7f09b259d..8ddc7366a9b 100644 --- a/src/components/connection-modal/connection-modal.jsx +++ b/src/components/connection-modal/connection-modal.jsx @@ -39,8 +39,20 @@ const ConnectionModalComponent = props => ( {props.phase === PHASES.scanning && props.useAutoScan && } {props.phase === PHASES.connecting && } {props.phase === PHASES.connected && } - {props.phase === PHASES.error && } - {props.phase === PHASES.unavailable && } + {props.phase === PHASES.error && ( + + )} + {props.phase === PHASES.unavailable && ( + + )} {props.phase === PHASES.updatePeripheral && } @@ -55,6 +67,8 @@ ConnectionModalComponent.propTypes = { name: PropTypes.node, onCancel: PropTypes.func.isRequired, onHelp: PropTypes.func.isRequired, + onScanning: PropTypes.func, + onUpdatePeripheral: PropTypes.func, phase: PropTypes.oneOf(Object.keys(PHASES)).isRequired, title: PropTypes.string.isRequired, useAutoScan: PropTypes.bool.isRequired diff --git a/src/components/connection-modal/error-step.jsx b/src/components/connection-modal/error-step.jsx index f91cb537e53..ec3f6d1a57e 100644 --- a/src/components/connection-modal/error-step.jsx +++ b/src/components/connection-modal/error-step.jsx @@ -7,6 +7,7 @@ import Box from '../box/box.jsx'; import Dots from './dots.jsx'; import helpIcon from './icons/help.svg'; import backIcon from './icons/back.svg'; +import enterUpdateIcon from './icons/enter-update.svg'; import styles from './connection-modal.css'; @@ -30,6 +31,15 @@ const ErrorStep = props => ( id="gui.connection.error.errorMessage" /> + {props.onUpdatePeripheral && ( +
+ +
+ )} ( id="gui.connection.error.tryagainbutton" /> + {props.onUpdatePeripheral && ( + + )} + {props.onUpdatePeripheral && ( + + )}