feat: rough in a firmware update flow

This commit is contained in:
Christopher Willis-Ford committed 2023-06-05 07:48:34 -07:00
1 parent 18407b15af
commit 3b2b013cac
5 files changed
+217 -2

No files matched your search

@@ -11,6 +11,7 @@ import ConnectingStep from './connecting-step.jsx';
import ConnectedStep from './connected-step.jsx';
import ErrorStep from './error-step.jsx';
import UnavailableStep from './unavailable-step.jsx';
import UpdateFirmwareStep from './update-firmware-step.jsx';
import styles from './connection-modal.css';
@@ -19,7 +20,8 @@ const PHASES = keyMirror({
connecting: null,
connected: null,
error: null,
unavailable: null
unavailable: null,
updateFirmware: null
});
const ConnectionModalComponent = props => (
@@ -39,6 +41,7 @@ const ConnectionModalComponent = props => (
{props.phase === PHASES.connected && <ConnectedStep {...props} />}
{props.phase === PHASES.error && <ErrorStep {...props} />}
{props.phase === PHASES.unavailable && <UnavailableStep {...props} />}
{props.phase === PHASES.updateFirmware && <UpdateFirmwareStep {...props} />}
</Box>
</Modal>
);
@@ -51,6 +51,25 @@ const ScanningStep = props => (
description="Text shown when no devices could be found"
id="gui.connection.scanning.noPeripheralsFound"
/>
{
props.onSendFirmware && <p>
<FormattedMessage
defaultMessage="Need to update your device to work with Scratch?"
description="Prompt for updating firmware"
id="gui.connection.scanning.updateFirmwarePrompt"
/>
<button
className={classNames(styles.bottomAreaItem, styles.connectionButton)}
onClick={props.onUpdateFirmware}
>
<FormattedMessage
defaultMessage="Update my Device"
description="Button in prompt for starting a search"
id="gui.connection.scanning.updateFirmwareButton"
/>
</button>
</p>
}
</Box>
)}
</Box>
@@ -89,6 +108,8 @@ ScanningStep.propTypes = {
connectionSmallIconURL: PropTypes.string,
onConnecting: PropTypes.func,
onRefresh: PropTypes.func,
onSendFirmware: PropTypes.func,
onUpdateFirmware: PropTypes.func,
peripheralList: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
rssi: PropTypes.number,
@@ -0,0 +1,157 @@
import 'regenerator-runtime/runtime';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import React from 'react';
import bindAll from 'lodash.bindall';
import keyMirror from 'keymirror';
import Box from '../box/box.jsx';
import Dots from './dots.jsx';
import backIcon from './icons/back.svg';
import styles from './connection-modal.css';
/** @enum{string} UPDATE_ACTIVITY */
const UPDATE_ACTIVITY = keyMirror({
getReady: null,
sendFirmware: null,
results: null
});
class UpdateFirmwareStep extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleUpdateFirmware'
]);
this.state = {
/**
* @type {UPDATE_ACTIVITY}
*/
activity: UPDATE_ACTIVITY.getReady,
progress: 0
};
}
async handleUpdateFirmware () {
this.setState({
activity: UPDATE_ACTIVITY.sendFirmware,
progress: 0
});
const results = await this.props.onSendFirmware(progress => {
this.setState({progress});
});
this.setState({
activity: UPDATE_ACTIVITY.results,
results
});
}
renderGetReady () {
return (<Box className={styles.activityArea}>
<Box className={styles.centeredRow}>
<FormattedMessage
defaultMessage="This will update your {extensionName} to work with Scratch."
description="Introduction to the firmware update process for a peripheral"
id="gui.connection.updateFirmware.introduction"
values={{
extensionName: this.props.name
}}
/>
</Box>
<Box className={styles.centeredRow}>
{
// The instructions for getting the peripheral ready for the update process will vary depending on
// the peripheral. Should we get this from the extension somehow?
}
<FormattedMessage
defaultMessage="Please connect your {extensionName} to this device using a USB cable."
description="Instructions to connect the micro:bit to the computer for the firmware update process"
id="gui.connection.updateFirmware.microBitConnect"
values={{
extensionName: this.props.name
}}
/>
</Box>
<Box className={styles.centeredRow}>
<FormattedMessage
defaultMessage="Please don't disconnect, reset, or turn off your {extensionName} during the update."
description="Notice to not disrupt the firmware update process"
id="gui.connection.updateFirmware.doNotDisconnect"
values={{
extensionName: this.props.name
}}
/>
</Box>
</Box>);
}
renderSendFirmware () {
return (<Box className={styles.activityArea}>
<p>{'Sending update...'}</p>
<progress
max="1"
value={this.state.progress}
/>
</Box>);
}
renderResults () {
return (<Box className={styles.activityArea}>
<p>{'Results'}</p>
</Box>);
}
render () {
return (
<Box className={styles.body}>
{(this.state.activity === UPDATE_ACTIVITY.getReady) && this.renderGetReady()}
{(this.state.activity === UPDATE_ACTIVITY.sendFirmware) && this.renderSendFirmware()}
{(this.state.activity === UPDATE_ACTIVITY.results) && this.renderResults()}
<Box className={styles.bottomArea}>
<Dots
className={styles.bottomAreaItem}
total={3}
/>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
<button
className={styles.connectionButton}
onClick={this.props.onScanning}
disabled={this.state.activity === UPDATE_ACTIVITY.sendFirmware}
>
<img
className={classNames(styles.buttonIconLeft, styles.buttonIconBack)}
src={backIcon}
/>
<FormattedMessage
defaultMessage="Go back"
description="Button to leave the firmware update process"
id="gui.connection.updateFirmware.goBackButton"
/>
</button>
<button
className={styles.connectionButton}
onClick={this.handleUpdateFirmware}
disabled={this.state.activity !== UPDATE_ACTIVITY.getReady}
>
<FormattedMessage
defaultMessage="Update now"
description="Button to start the firmware update"
id="gui.connection.updateFirmware.updateNowButton"
/>
</button>
</Box>
</Box>
</Box>
);
}
}
UpdateFirmwareStep.propTypes = {
name: PropTypes.string.isRequired,
onScanning: PropTypes.func.isRequired,
onSendFirmware: PropTypes.func.isRequired
};
export default UpdateFirmwareStep;
+31 -1
View File
@@ -18,7 +18,9 @@ class ConnectionModal extends React.Component {
'handleConnecting',
'handleDisconnect',
'handleError',
'handleHelp'
'handleHelp',
'handleUpdateFirmware',
'handleSendFirmware'
]);
this.state = {
extension: extensionData.find(ext => ext.extensionId === props.extensionId),
@@ -104,7 +106,33 @@ class ConnectionModal extends React.Component {
label: this.props.extensionId
});
}
handleUpdateFirmware () {
this.setState({
phase: PHASES.updateFirmware
});
analytics.event({
category: 'extensions',
action: 'update firmware',
label: this.props.extensionId
});
}
handleSendFirmware (progressCallback) {
// TODO: get this functionality from the extension
// TODO: actually send firmware to the device
let progress = 0;
return new Promise(resolve => {
const interval = setInterval(() => {
progress += 0.01;
if (progress >= 1) {
clearInterval(interval);
resolve({success: true});
}
progressCallback(progress);
}, 100);
});
}
render () {
const canSendFirmware = this.props.extensionId === 'microbit';
return (
<ConnectionModalComponent
connectingMessage={this.state.extension && this.state.extension.connectingMessage}
@@ -123,6 +151,8 @@ class ConnectionModal extends React.Component {
onDisconnect={this.handleDisconnect}
onHelp={this.handleHelp}
onScanning={this.handleScanning}
onUpdateFirmware={this.handleUpdateFirmware}
onSendFirmware={canSendFirmware ? this.handleSendFirmware : null}
/>
);
}
+4
View File
@@ -62,6 +62,8 @@ class ScanningStep extends React.Component {
onConnected={this.props.onConnected}
onConnecting={this.props.onConnecting}
onRefresh={this.handleRefresh}
onSendFirmware={this.props.onSendFirmware}
onUpdateFirmware={this.props.onUpdateFirmware}
/>
);
}
@@ -72,6 +74,8 @@ ScanningStep.propTypes = {
extensionId: PropTypes.string.isRequired,
onConnected: PropTypes.func.isRequired,
onConnecting: PropTypes.func.isRequired,
onSendFirmware: PropTypes.func,
onUpdateFirmware: PropTypes.func,
vm: PropTypes.instanceOf(VM).isRequired
};