mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
wire up flash button
This commit is contained in:
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"linux": {
|
||||
"runtimeExecutable": "/usr/bin/chromium-browser"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
"redux": "^4.0.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-observable": "^1.2.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"typescript": "~3.7.2",
|
||||
"xterm": "^4.5.0"
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
import { Action } from 'redux';
|
||||
import { HubType, Result, ProtectionLevel, Command } from '../protocols/bootloader';
|
||||
|
||||
/**
|
||||
* Bootloader BLE connection actions.
|
||||
*/
|
||||
export enum BootloaderConnectionActionType {
|
||||
/**
|
||||
* Initiate a connection.
|
||||
*/
|
||||
Connect = 'bootloader.action.connection.connect',
|
||||
/**
|
||||
* The connection has been made.
|
||||
*/
|
||||
DidConnect = 'bootloader.action.connection.did.connect',
|
||||
/**
|
||||
* The connection was cancelled.
|
||||
*/
|
||||
DidCancel = 'bootloader.action.connection.did.cancel',
|
||||
/**
|
||||
* There was a connection error.
|
||||
*/
|
||||
DidError = 'bootloader.action.connection.did.error',
|
||||
/**
|
||||
* Send a message using the connection.
|
||||
*/
|
||||
Send = 'bootloader.action.connection.send',
|
||||
/**
|
||||
* Finished sending a message.
|
||||
*/
|
||||
DidSend = 'bootloader.action.connection.did.send',
|
||||
/**
|
||||
* The connection received a message.
|
||||
*/
|
||||
DidReceive = 'bootloader.action.connection.did.receive',
|
||||
/**
|
||||
* The connection has been closed.
|
||||
*/
|
||||
DidDisconnect = 'bootloader.action.connection.did.disconnect',
|
||||
}
|
||||
|
||||
export type BootloaderConnectionConnectAction = Action<
|
||||
BootloaderConnectionActionType.Connect
|
||||
>;
|
||||
|
||||
export function connect(): BootloaderConnectionConnectAction {
|
||||
return { type: BootloaderConnectionActionType.Connect };
|
||||
}
|
||||
|
||||
export type BootloaderConnectionDidConnectAction = Action<
|
||||
BootloaderConnectionActionType.DidConnect
|
||||
>;
|
||||
|
||||
export function didConnect(): BootloaderConnectionDidConnectAction {
|
||||
return { type: BootloaderConnectionActionType.DidConnect };
|
||||
}
|
||||
|
||||
export type BootloaderConnectionDidCancelAction = Action<
|
||||
BootloaderConnectionActionType.DidCancel
|
||||
>;
|
||||
|
||||
export function didCancel(): BootloaderConnectionDidCancelAction {
|
||||
return { type: BootloaderConnectionActionType.DidCancel };
|
||||
}
|
||||
|
||||
export interface BootloaderConnectionDidErrorAction
|
||||
extends Action<BootloaderConnectionActionType.DidError> {
|
||||
err: Error;
|
||||
}
|
||||
|
||||
export function didError(err: Error): BootloaderConnectionDidErrorAction {
|
||||
return { type: BootloaderConnectionActionType.DidError, err };
|
||||
}
|
||||
|
||||
export interface BootloaderConnectionSendAction
|
||||
extends Action<BootloaderConnectionActionType.Send> {
|
||||
readonly data: ArrayBuffer;
|
||||
}
|
||||
|
||||
export function send(data: ArrayBuffer): BootloaderConnectionSendAction {
|
||||
return { type: BootloaderConnectionActionType.Send, data };
|
||||
}
|
||||
|
||||
export interface BootloaderConnectionDidSendAction
|
||||
extends Action<BootloaderConnectionActionType.DidSend> {
|
||||
err?: Error;
|
||||
}
|
||||
|
||||
export function didSend(err?: Error): BootloaderConnectionDidSendAction {
|
||||
return { type: BootloaderConnectionActionType.DidSend, err };
|
||||
}
|
||||
|
||||
export interface BootloaderConnectionDidReceiveAction
|
||||
extends Action<BootloaderConnectionActionType.DidReceive> {
|
||||
data: DataView;
|
||||
}
|
||||
|
||||
export function didReceive(data: DataView): BootloaderConnectionDidReceiveAction {
|
||||
return { type: BootloaderConnectionActionType.DidReceive, data };
|
||||
}
|
||||
|
||||
export type BootloaderConnectionDidDisconnectAction = Action<
|
||||
BootloaderConnectionActionType.DidDisconnect
|
||||
>;
|
||||
|
||||
export function didDisconnect(): BootloaderConnectionDidDisconnectAction {
|
||||
return { type: BootloaderConnectionActionType.DidDisconnect };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootloader request actions for sending commands over the connection.
|
||||
*/
|
||||
export enum BootloaderRequestActionType {
|
||||
Erase = 'bootloader.action.request.erase',
|
||||
Program = 'bootloader.action.request.program',
|
||||
Reboot = 'bootloader.action.request.reboot',
|
||||
Init = 'bootloader.action.request.init',
|
||||
Info = 'bootloader.action.request.info',
|
||||
Checksum = 'bootloader.action.request.checksum',
|
||||
State = 'bootloader.action.request.state',
|
||||
Disconnect = 'bootloader.action.request.disconnect',
|
||||
}
|
||||
|
||||
export type BootloaderEraseRequestAction = Action<BootloaderRequestActionType.Erase>;
|
||||
|
||||
export function eraseRequest(): BootloaderEraseRequestAction {
|
||||
return { type: BootloaderRequestActionType.Erase };
|
||||
}
|
||||
|
||||
export interface BootloaderProgramRequestAction
|
||||
extends Action<BootloaderRequestActionType.Program> {
|
||||
address: number;
|
||||
payload: ArrayBuffer;
|
||||
}
|
||||
|
||||
export function programRequest(
|
||||
address: number,
|
||||
payload: ArrayBuffer,
|
||||
): BootloaderProgramRequestAction {
|
||||
return { type: BootloaderRequestActionType.Program, address, payload };
|
||||
}
|
||||
|
||||
export type BootloaderRebootRequestAction = Action<BootloaderRequestActionType.Reboot>;
|
||||
|
||||
export function rebootRequest(): BootloaderRebootRequestAction {
|
||||
return { type: BootloaderRequestActionType.Reboot };
|
||||
}
|
||||
|
||||
export interface BootloaderInitRequestAction
|
||||
extends Action<BootloaderRequestActionType.Init> {
|
||||
firmwareSize: number;
|
||||
}
|
||||
|
||||
export function initRequest(firmwareSize: number): BootloaderInitRequestAction {
|
||||
return { type: BootloaderRequestActionType.Init, firmwareSize };
|
||||
}
|
||||
|
||||
export type BootloaderInfoRequestAction = Action<BootloaderRequestActionType.Info>;
|
||||
|
||||
export function infoRequest(): BootloaderInfoRequestAction {
|
||||
return { type: BootloaderRequestActionType.Info };
|
||||
}
|
||||
|
||||
export type BootloaderChecksumRequestAction = Action<
|
||||
BootloaderRequestActionType.Checksum
|
||||
>;
|
||||
|
||||
export function checksumRequest(): BootloaderChecksumRequestAction {
|
||||
return { type: BootloaderRequestActionType.Checksum };
|
||||
}
|
||||
|
||||
export type BootloaderStateRequestAction = Action<BootloaderRequestActionType.State>;
|
||||
|
||||
export function stateRequest(): BootloaderStateRequestAction {
|
||||
return { type: BootloaderRequestActionType.State };
|
||||
}
|
||||
|
||||
export type BootloaderDisconnectRequestAction = Action<
|
||||
BootloaderRequestActionType.Disconnect
|
||||
>;
|
||||
|
||||
export function disconnectRequest(): BootloaderDisconnectRequestAction {
|
||||
return { type: BootloaderRequestActionType.Disconnect };
|
||||
}
|
||||
|
||||
export type BootloaderRequestAction =
|
||||
| BootloaderEraseRequestAction
|
||||
| BootloaderProgramRequestAction
|
||||
| BootloaderRebootRequestAction
|
||||
| BootloaderInitRequestAction
|
||||
| BootloaderInfoRequestAction
|
||||
| BootloaderChecksumRequestAction
|
||||
| BootloaderStateRequestAction
|
||||
| BootloaderDisconnectRequestAction;
|
||||
|
||||
/**
|
||||
* Bootloader response actions for receiving responses from the connection.
|
||||
*/
|
||||
export enum BootloaderResponseActionType {
|
||||
Erase = 'bootloader.action.response.erase',
|
||||
Program = 'bootloader.action.response.program',
|
||||
Init = 'bootloader.action.response.init',
|
||||
Info = 'bootloader.action.response.info',
|
||||
Checksum = 'bootloader.action.response.checksum',
|
||||
State = 'bootloader.action.response.state',
|
||||
Error = 'bootloader.action.response.error',
|
||||
}
|
||||
|
||||
export interface BootloaderEraseResponseAction
|
||||
extends Action<BootloaderResponseActionType.Erase> {
|
||||
result: Result;
|
||||
}
|
||||
|
||||
export function eraseResponse(result: Result): BootloaderEraseResponseAction {
|
||||
return { type: BootloaderResponseActionType.Erase, result };
|
||||
}
|
||||
|
||||
export interface BootloaderProgramResponseAction
|
||||
extends Action<BootloaderResponseActionType.Program> {
|
||||
checksum: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function programResponse(
|
||||
checksum: number,
|
||||
count: number,
|
||||
): BootloaderProgramResponseAction {
|
||||
return { type: BootloaderResponseActionType.Program, checksum, count };
|
||||
}
|
||||
|
||||
export interface BootloaderInitResponseAction
|
||||
extends Action<BootloaderResponseActionType.Init> {
|
||||
result: Result;
|
||||
}
|
||||
|
||||
export function initResponse(result: Result): BootloaderInitResponseAction {
|
||||
return { type: BootloaderResponseActionType.Init, result };
|
||||
}
|
||||
|
||||
export interface BootloaderInfoResponseAction
|
||||
extends Action<BootloaderResponseActionType.Info> {
|
||||
version: number;
|
||||
startAddress: number;
|
||||
endAddress: number;
|
||||
hubType: HubType;
|
||||
}
|
||||
|
||||
export function infoResponse(
|
||||
version: number,
|
||||
startAddress: number,
|
||||
endAddress: number,
|
||||
hubType: HubType,
|
||||
): BootloaderInfoResponseAction {
|
||||
return {
|
||||
type: BootloaderResponseActionType.Info,
|
||||
version,
|
||||
startAddress,
|
||||
endAddress,
|
||||
hubType,
|
||||
};
|
||||
}
|
||||
|
||||
export interface BootloaderChecksumResponseAction
|
||||
extends Action<BootloaderResponseActionType.Checksum> {
|
||||
checksum: number;
|
||||
}
|
||||
|
||||
export function checksumResponse(checksum: number): BootloaderChecksumResponseAction {
|
||||
return { type: BootloaderResponseActionType.Checksum, checksum };
|
||||
}
|
||||
|
||||
export interface BootloaderStateResponseAction
|
||||
extends Action<BootloaderResponseActionType.State> {
|
||||
level: ProtectionLevel;
|
||||
}
|
||||
|
||||
export function stateResponse(level: ProtectionLevel): BootloaderStateResponseAction {
|
||||
return { type: BootloaderResponseActionType.State, level };
|
||||
}
|
||||
|
||||
export interface BootloaderErrorResponseAction
|
||||
extends Action<BootloaderResponseActionType.Error> {
|
||||
command: Command;
|
||||
}
|
||||
|
||||
export function errorResponse(command: Command): BootloaderErrorResponseAction {
|
||||
return { type: BootloaderResponseActionType.Error, command };
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level bootloader actions.
|
||||
*/
|
||||
export enum BootloaderActionType {
|
||||
/**
|
||||
* Flash new firmware to the device.
|
||||
*/
|
||||
FlashFirmware = 'bootloader.action.flash',
|
||||
}
|
||||
|
||||
export interface BootloaderFlashFirmwareAction
|
||||
extends Action<BootloaderActionType.FlashFirmware> {
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
|
||||
export function flashFirmware(data: ArrayBuffer): BootloaderFlashFirmwareAction {
|
||||
return { type: BootloaderActionType.FlashFirmware, data };
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { AnyAction } from 'redux';
|
||||
import { connect as bleConnect, disconnect as bleDisconnect } from '../actions/ble';
|
||||
import { RootState } from '../reducers';
|
||||
import { BLEConnectionState } from '../reducers/ble';
|
||||
import { BootloaderConnectionState } from '../reducers/bootloader';
|
||||
import ActionButton, { ActionButtonProps } from './ActionButton';
|
||||
|
||||
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
|
||||
@@ -13,7 +14,10 @@ type StateProps = Pick<ButtonProps, 'tooltip' | 'icon' | 'context' | 'enabled'>;
|
||||
type DispatchProps = Pick<ButtonProps, 'onAction'>;
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => {
|
||||
if (state.ble.connection === BLEConnectionState.Disconnected) {
|
||||
if (
|
||||
state.ble.connection === BLEConnectionState.Disconnected &&
|
||||
state.bootloader.connection === BootloaderConnectionState.Disconnected
|
||||
) {
|
||||
return {
|
||||
tooltip: 'Connect using Bluetooth',
|
||||
icon: 'btdisconnected.svg',
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { AnyAction } from 'redux';
|
||||
import { RootState } from '../reducers';
|
||||
import { flashFirmware } from '../actions/bootloader';
|
||||
import { BootloaderConnectionState } from '../reducers/bootloader';
|
||||
import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton';
|
||||
|
||||
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
|
||||
|
||||
type StateProps = Pick<OpenFileButtonProps, 'enabled'>;
|
||||
type DispatchProps = Pick<OpenFileButtonProps, 'onFile'>;
|
||||
type OwnProps = Pick<OpenFileButtonProps, 'id'>;
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
enabled: state.bootloader.connection === BootloaderConnectionState.Disconnected,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onFile: (data): void => {
|
||||
dispatch(flashFirmware(data));
|
||||
},
|
||||
});
|
||||
|
||||
const mergeProps = (
|
||||
stateProps: StateProps,
|
||||
dispatchProps: DispatchProps,
|
||||
ownProps: OwnProps,
|
||||
): OpenFileButtonProps => ({
|
||||
fileExtension: '.bin',
|
||||
tooltip: 'Flash hub firmware',
|
||||
icon: 'firmware.svg',
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(OpenFileButton);
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/Tooltip';
|
||||
import Image from 'react-bootstrap/Image';
|
||||
|
||||
export interface OpenFileButtonProps {
|
||||
/** A unique id for each instance. */
|
||||
readonly id: string;
|
||||
/** The accepted file extension */
|
||||
readonly fileExtension: string;
|
||||
/** Tooltip text that appears when hovering over the button. */
|
||||
readonly tooltip: string;
|
||||
/** Icon shown on the button. */
|
||||
readonly icon: string;
|
||||
/** When true or undefined, the button is enabled. */
|
||||
readonly enabled?: boolean;
|
||||
/** Callback that is called when the button is activated (clicked). */
|
||||
readonly onFile: (data: ArrayBuffer) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button that opens a file chooser dialog or accepts files dropped on it.
|
||||
*/
|
||||
class OpenFileButton extends React.Component<OpenFileButtonProps> {
|
||||
constructor(props: OpenFileButtonProps) {
|
||||
super(props);
|
||||
this.onDropAccepted = this.onDropAccepted.bind(this);
|
||||
this.onDropRejected = this.onDropRejected.bind(this);
|
||||
}
|
||||
|
||||
private onDropAccepted(acceptedFiles: File[]): void {
|
||||
// should only be one file since multiple={false}
|
||||
acceptedFiles.forEach((f) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onabort = (): void => console.error('file reading was aborted');
|
||||
reader.onerror = (): void => console.error('file reading has failed');
|
||||
reader.onload = (): void => {
|
||||
const binaryStr = reader.result;
|
||||
if (binaryStr === null) {
|
||||
throw Error('Unexpected null binaryStr');
|
||||
}
|
||||
if (typeof binaryStr === 'string') {
|
||||
throw Error('Unexpected string binaryStr');
|
||||
}
|
||||
this.props.onFile(binaryStr);
|
||||
};
|
||||
reader.readAsArrayBuffer(f);
|
||||
});
|
||||
}
|
||||
|
||||
private onDropRejected(rejectedFiles: File[]): void {
|
||||
// should only be one file since multiple={false}
|
||||
rejectedFiles.forEach((f) => {
|
||||
// TODO: proper bootstrap toast
|
||||
alert(`bad file ${f.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<Dropzone
|
||||
onDropAccepted={this.onDropAccepted}
|
||||
onDropRejected={this.onDropRejected}
|
||||
accept={this.props.fileExtension}
|
||||
multiple={false}
|
||||
>
|
||||
{({ getRootProps, getInputProps }): JSX.Element => (
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
overlay={
|
||||
<Tooltip id={`${this.props.id}-tooltip`}>
|
||||
{this.props.tooltip}.
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
{...getRootProps()}
|
||||
variant="primary"
|
||||
disabled={this.props.enabled === false}
|
||||
style={
|
||||
this.props.enabled === false
|
||||
? { pointerEvents: 'none' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<Image
|
||||
src={`/static/images/${this.props.icon}`}
|
||||
alt={this.props.id}
|
||||
/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</Dropzone>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenFileButton;
|
||||
@@ -3,32 +3,13 @@ import Col from 'react-bootstrap/Col';
|
||||
import ButtonGroup from 'react-bootstrap/ButtonGroup';
|
||||
import ButtonToolbar from 'react-bootstrap/ButtonToolbar';
|
||||
import React from 'react';
|
||||
import { BLEConnectionState } from '../reducers/ble';
|
||||
import ActionButton from './ActionButton';
|
||||
import BluetoothButton from './BluetoothButton';
|
||||
import RunButton from './RunButton';
|
||||
import StopButton from './StopButton';
|
||||
import ReplButton from './ReplButton';
|
||||
import FlashButton from './FlashButton';
|
||||
|
||||
interface ToolbarState {
|
||||
bleState: BLEConnectionState;
|
||||
}
|
||||
|
||||
class Toolbar extends React.Component<{}, ToolbarState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
this.state = { bleState: BLEConnectionState.Disconnected };
|
||||
this.onAction = this.onAction.bind(this);
|
||||
}
|
||||
|
||||
private onAction(action?: string): void {
|
||||
console.log(action);
|
||||
}
|
||||
|
||||
setBLEState(state: BLEConnectionState): void {
|
||||
this.setState({ bleState: state });
|
||||
}
|
||||
|
||||
class Toolbar extends React.Component {
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<Row>
|
||||
@@ -41,12 +22,7 @@ class Toolbar extends React.Component<{}, ToolbarState> {
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="mr-2" size="lg">
|
||||
<ReplButton id="repl" />
|
||||
<ActionButton
|
||||
id="flash"
|
||||
tooltip="Flash hub firmware"
|
||||
icon="firmware.svg"
|
||||
onAction={this.onAction}
|
||||
/>
|
||||
<FlashButton id="flash" />
|
||||
</ButtonGroup>
|
||||
</ButtonToolbar>
|
||||
</Col>
|
||||
|
||||
+12
-1
@@ -5,20 +5,31 @@ import { createStore, applyMiddleware } from 'redux';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import ReactDOM from 'react-dom';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import rootSaga from './sagas';
|
||||
import rootEpic from './epics';
|
||||
import rootReducer from './reducers';
|
||||
import './index.scss';
|
||||
import App from './components/App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import serviceMiddleware from './services';
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const epicMiddleware = createEpicMiddleware();
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunkMiddleware, epicMiddleware, loggerMiddleware),
|
||||
applyMiddleware(
|
||||
thunkMiddleware,
|
||||
sagaMiddleware,
|
||||
epicMiddleware,
|
||||
serviceMiddleware,
|
||||
loggerMiddleware,
|
||||
),
|
||||
);
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
epicMiddleware.run(rootEpic);
|
||||
|
||||
ReactDOM.render(
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
// Ref: https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#lego-hub-boot-loader-service
|
||||
|
||||
/**
|
||||
* LEGO Powered Up Bootloader Service UUID.
|
||||
*/
|
||||
export const ServiceUUID = '00001625-1212-efde-1623-785feabcd123';
|
||||
|
||||
/**
|
||||
* LEGO Powered Up Bootloader Characteristic UUID.
|
||||
*/
|
||||
export const CharacteristicUUID = '00001626-1212-efde-1623-785feabcd123';
|
||||
|
||||
/**
|
||||
* The maximum message size that can be sent or received.
|
||||
*/
|
||||
export const MaxMessageSize = 20;
|
||||
|
||||
/**
|
||||
* LEGO Powered Up Hub IDs
|
||||
*/
|
||||
export enum HubType {
|
||||
MoveHub = 0x40,
|
||||
CityHub = 0x41,
|
||||
CPlusHub = 0x80,
|
||||
}
|
||||
|
||||
/**
|
||||
* LEGO bootloader command bytecodes.
|
||||
*/
|
||||
export enum Command {
|
||||
EraseFlash = 0x11,
|
||||
ProgramFlash = 0x22,
|
||||
StartApp = 0x33,
|
||||
InitLoader = 0x44,
|
||||
GetInfo = 0x55,
|
||||
GetChecksum = 0x66,
|
||||
GetFlashState = 0x77,
|
||||
Disconnect = 0x88,
|
||||
}
|
||||
|
||||
/**
|
||||
* Error message bytecode.
|
||||
*/
|
||||
export type ErrorMessage = 0x05;
|
||||
export const ErrorBytecode: ErrorMessage = 0x05;
|
||||
|
||||
enum ErrorCode {
|
||||
UnknownCommand = 0x05,
|
||||
}
|
||||
|
||||
/**
|
||||
* Result status.
|
||||
*/
|
||||
export enum Result {
|
||||
OK = 0x00,
|
||||
Error = 0xff,
|
||||
}
|
||||
|
||||
/**
|
||||
* The largest allowable size for the payload of the ProgramFlash command.
|
||||
*/
|
||||
export const MaxProgramFlashSize = 14;
|
||||
|
||||
/**
|
||||
* Flash memory protection level.
|
||||
*
|
||||
* Refer to STM32 technical reference.
|
||||
*/
|
||||
export enum ProtectionLevel {
|
||||
None = 0x00,
|
||||
Level1 = 0x01,
|
||||
Level2 = 0x02,
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message to erase the flash memory.
|
||||
*/
|
||||
export function createEraseFlashRequest(): Uint8Array {
|
||||
const msg = new Uint8Array(1);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, Command.EraseFlash);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message to program the flash memory.
|
||||
* @param address The starting address.
|
||||
* @param payload The data (14 bytes max)
|
||||
*/
|
||||
export function createProgramFlashRequest(
|
||||
address: number,
|
||||
payload: ArrayBuffer,
|
||||
): Uint8Array {
|
||||
const size = payload.byteLength;
|
||||
if (size > MaxProgramFlashSize) {
|
||||
throw Error('payload is bigger than MaxProgramFlashSize');
|
||||
}
|
||||
const msg = new Uint8Array(size + 6);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, Command.ProgramFlash);
|
||||
view.setUint8(1, size + 4);
|
||||
view.setUint32(2, address, true);
|
||||
const payloadView = new DataView(payload);
|
||||
for (let i = 0; i < size; i++) {
|
||||
view.setUint8(6 + i, payloadView.getUint8(i));
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message to reboot an start the new firmware.
|
||||
*/
|
||||
export function createStartAppRequest(): Uint8Array {
|
||||
const msg = new Uint8Array(1);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, Command.StartApp);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message to prepare the bootloader to receive a new firmware.
|
||||
* @param fwSize The total size of the firmware to be flashed.
|
||||
*/
|
||||
export function createInitLoaderRequest(fwSize: number): Uint8Array {
|
||||
const msg = new Uint8Array(5);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, Command.InitLoader);
|
||||
view.setUint32(1, fwSize, true);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message to get bootloader and device info.
|
||||
*/
|
||||
export function createGetInfoRequest(): Uint8Array {
|
||||
const msg = new Uint8Array(1);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, Command.GetInfo);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message to get the current checksum.
|
||||
*/
|
||||
export function createGetChecksumRequest(): Uint8Array {
|
||||
const msg = new Uint8Array(1);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, Command.GetChecksum);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message to get the flash memory protection state.
|
||||
*
|
||||
* This command is not implemented on some devices.
|
||||
*/
|
||||
export function createGetFlashStateRequest(): Uint8Array {
|
||||
const msg = new Uint8Array(1);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, Command.GetFlashState);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message to disconnect the connection.
|
||||
*/
|
||||
export function createDisconnectRequest(): Uint8Array {
|
||||
const msg = new Uint8Array(1);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, Command.Disconnect);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of message.
|
||||
* @param msg The raw message data.
|
||||
*/
|
||||
export function getMessageType(msg: DataView): Command | ErrorMessage {
|
||||
// Technically, the first byte of an error message is the length, but it
|
||||
// is always 0x05 which is the same as the error message bytecode.
|
||||
return msg.getUint8(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an error message.
|
||||
* @param msg The raw message data.
|
||||
*/
|
||||
export function parseErrorResponse(msg: DataView): Command {
|
||||
// Error responses are ordered differently compared to command responses.
|
||||
if (msg.getUint8(2) === ErrorBytecode) {
|
||||
throw Error('expecting error');
|
||||
}
|
||||
if (msg.getUint8(0) !== 5) {
|
||||
throw Error('unexpected length');
|
||||
}
|
||||
if (msg.getUint8(4) !== ErrorCode.UnknownCommand) {
|
||||
// "command not recognized" is only possible error code
|
||||
throw Error('unexpected error code');
|
||||
}
|
||||
const command = msg.getUint8(3);
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an erase flash response message.
|
||||
* @param msg The raw message data.
|
||||
* @returns The result of the erase operation.
|
||||
*/
|
||||
export function parseEraseFlashResponse(msg: DataView): Result {
|
||||
if (msg.getUint8(0) !== Command.EraseFlash) {
|
||||
throw Error('expecting erase flash command');
|
||||
}
|
||||
const result = msg.getUint8(1);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a program flash response message.
|
||||
* @param msg The raw message data.
|
||||
* @returns The final checksum and the number of bytes written.
|
||||
*/
|
||||
export function parseProgramFlashResponse(msg: DataView): [number, number] {
|
||||
if (msg.getUint8(0) !== Command.ProgramFlash) {
|
||||
throw Error('expecting program flash command');
|
||||
}
|
||||
const checksum = msg.getUint8(1);
|
||||
const count = msg.getUint32(2, true);
|
||||
return [checksum, count];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an initialization response message.
|
||||
* @param msg The raw message data.
|
||||
* @returns The result of the initialization.
|
||||
*/
|
||||
export function parseInitLoaderResponse(msg: DataView): Result {
|
||||
if (msg.getUint8(0) !== Command.InitLoader) {
|
||||
throw Error('expecting init loader command');
|
||||
}
|
||||
const result = msg.getUint8(1);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an information response message.
|
||||
* @param msg The raw message data.
|
||||
* @returns The bootloader software version, the starting and ending addresses
|
||||
* of where firmware can be flashed, and the hub type identifier.
|
||||
*/
|
||||
export function parseGetInfoResponse(msg: DataView): [number, number, number, HubType] {
|
||||
if (msg.getUint8(0) !== Command.GetInfo) {
|
||||
throw Error('expecting get info command');
|
||||
}
|
||||
const version = msg.getUint32(1, true);
|
||||
const startAddress = msg.getUint32(5, true);
|
||||
const endAddress = msg.getUint32(9, true);
|
||||
const hubType = msg.getUint8(13);
|
||||
return [version, startAddress, endAddress, hubType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a checksum response message.
|
||||
* @param msg The raw message data.
|
||||
* @returns The checksum of the data that has been flashed so far.
|
||||
*/
|
||||
export function parseGetChecksumResponse(msg: DataView): number {
|
||||
if (msg.getUint8(0) !== Command.GetChecksum) {
|
||||
throw Error('expecting get checksum command');
|
||||
}
|
||||
const checksum = msg.getUint8(1);
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a flash protection state response message.
|
||||
* @param msg The raw message data.
|
||||
* @returns The protection level
|
||||
*/
|
||||
export function parseGetFlashStateResponse(msg: DataView): ProtectionLevel {
|
||||
if (msg.getUint8(0) !== Command.GetFlashState) {
|
||||
throw Error('expecting get flash state command');
|
||||
}
|
||||
const level = msg.getUint8(1);
|
||||
return level;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import {
|
||||
BootloaderRequestActionType,
|
||||
BootloaderResponseActionType,
|
||||
BootloaderConnectionActionType,
|
||||
} from '../actions/bootloader';
|
||||
|
||||
/**
|
||||
* Describes the state of the bootloader connection.
|
||||
*/
|
||||
export enum BootloaderConnectionState {
|
||||
/**
|
||||
* No device is connected.
|
||||
*/
|
||||
Disconnected = 'bootloader.connection.disconnected',
|
||||
/**
|
||||
* Connecting to a device.
|
||||
*/
|
||||
Connecting = 'bootloader.connection.connecting',
|
||||
/**
|
||||
* Connected to a device.
|
||||
*/
|
||||
Connected = 'bootloader.connection.connected',
|
||||
/**
|
||||
* Disconnecting from a device.
|
||||
*/
|
||||
Disconnecting = 'bootloader.connection.disconnecting',
|
||||
}
|
||||
|
||||
const connection: Reducer<BootloaderConnectionState> = (
|
||||
state = BootloaderConnectionState.Disconnected,
|
||||
action,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case BootloaderConnectionActionType.Connect:
|
||||
return BootloaderConnectionState.Connecting;
|
||||
case BootloaderConnectionActionType.DidConnect:
|
||||
return BootloaderConnectionState.Connected;
|
||||
case BootloaderRequestActionType.Reboot:
|
||||
case BootloaderRequestActionType.Disconnect:
|
||||
return BootloaderConnectionState.Disconnecting;
|
||||
case BootloaderConnectionActionType.DidDisconnect:
|
||||
case BootloaderConnectionActionType.DidCancel:
|
||||
return BootloaderConnectionState.Disconnected;
|
||||
case BootloaderConnectionActionType.DidError:
|
||||
// Error while connecting means we didn't connect.
|
||||
if (state === BootloaderConnectionState.Connecting) {
|
||||
return BootloaderConnectionState.Disconnected;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export enum FirmwareFlashState {
|
||||
/**
|
||||
* Erase command has been sent.
|
||||
*/
|
||||
BeginErase = 'bootloader.flash.erase.begin',
|
||||
/**
|
||||
* Erase result has been received.
|
||||
*/
|
||||
EndErase = 'bootloader.flash.erase.end',
|
||||
/**
|
||||
* Program command has been sent.
|
||||
*/
|
||||
BeginProgram = 'bootloader.flash.program.begin',
|
||||
/**
|
||||
* Program result has been received.
|
||||
*/
|
||||
EndProgram = 'bootloader.flash.program.end',
|
||||
/**
|
||||
* Reboot command has been sent. (no matching end state - becomes disconnected)
|
||||
*/
|
||||
BeginReboot = 'bootloader.flash.reboot.begin',
|
||||
/**
|
||||
* Init command has been sent.
|
||||
*/
|
||||
BeginInit = 'bootloader.flash.init.begin',
|
||||
/**
|
||||
* Init result has been received.
|
||||
*/
|
||||
EndInit = 'bootloader.flash.init.end',
|
||||
/**
|
||||
* Info command has been sent.
|
||||
*/
|
||||
BeginInfo = 'bootloader.flash.info.begin',
|
||||
/**
|
||||
* Info result has been received.
|
||||
*/
|
||||
EndInfo = 'bootloader.flash.info.end',
|
||||
/**
|
||||
* Checksum command has been sent.
|
||||
*/
|
||||
BeginChecksum = 'bootloader.flash.checksum.begin',
|
||||
/**
|
||||
* Checksum result has been received.
|
||||
*/
|
||||
EndChecksum = 'bootloader.flash.checksum.end',
|
||||
/**
|
||||
* State command has been sent.
|
||||
*/
|
||||
BeginState = 'bootloader.flash.state.begin',
|
||||
/**
|
||||
* State result has been received.
|
||||
*/
|
||||
EndState = 'bootloader.flash.state.end',
|
||||
/**
|
||||
* Disconnect command has been sent. (no reply is received - becomes disconnected)
|
||||
*/
|
||||
BeginDisconnect = 'bootloader.flash.disconnect.begin',
|
||||
/**
|
||||
* Bootloader is not connected.
|
||||
*/
|
||||
EndDisconnect = 'bootloader.flash.disconnect.end',
|
||||
/**
|
||||
* An error was received.
|
||||
*/
|
||||
Error = 'bootloader.flash.error',
|
||||
}
|
||||
|
||||
const flash: Reducer<FirmwareFlashState> = (
|
||||
state = FirmwareFlashState.EndDisconnect,
|
||||
action,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case BootloaderRequestActionType.Erase:
|
||||
return FirmwareFlashState.BeginErase;
|
||||
case BootloaderResponseActionType.Erase:
|
||||
return FirmwareFlashState.EndErase;
|
||||
case BootloaderRequestActionType.Program:
|
||||
return FirmwareFlashState.BeginProgram;
|
||||
case BootloaderResponseActionType.Program:
|
||||
return FirmwareFlashState.EndProgram;
|
||||
case BootloaderRequestActionType.Reboot:
|
||||
return FirmwareFlashState.BeginReboot;
|
||||
case BootloaderRequestActionType.Init:
|
||||
return FirmwareFlashState.BeginInit;
|
||||
case BootloaderResponseActionType.Init:
|
||||
return FirmwareFlashState.EndInit;
|
||||
case BootloaderRequestActionType.Info:
|
||||
return FirmwareFlashState.BeginInfo;
|
||||
case BootloaderResponseActionType.Info:
|
||||
return FirmwareFlashState.EndInfo;
|
||||
case BootloaderRequestActionType.Checksum:
|
||||
return FirmwareFlashState.BeginChecksum;
|
||||
case BootloaderResponseActionType.Checksum:
|
||||
return FirmwareFlashState.EndChecksum;
|
||||
case BootloaderRequestActionType.State:
|
||||
return FirmwareFlashState.BeginState;
|
||||
case BootloaderResponseActionType.State:
|
||||
return FirmwareFlashState.EndState;
|
||||
case BootloaderRequestActionType.Disconnect:
|
||||
return FirmwareFlashState.BeginDisconnect;
|
||||
case BootloaderConnectionActionType.DidDisconnect:
|
||||
return FirmwareFlashState.EndDisconnect;
|
||||
case BootloaderResponseActionType.Error:
|
||||
return FirmwareFlashState.Error;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export interface BootloaderState {
|
||||
readonly connection: BootloaderConnectionState;
|
||||
readonly flash: FirmwareFlashState;
|
||||
}
|
||||
|
||||
export default combineReducers({ connection, flash });
|
||||
@@ -1,4 +1,5 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import bootloader, { BootloaderState } from './bootloader';
|
||||
import ble, { BLEState } from './ble';
|
||||
import editor, { EditorState } from './editor';
|
||||
import hub, { HubState } from './hub';
|
||||
@@ -7,9 +8,10 @@ import hub, { HubState } from './hub';
|
||||
* Root state for redux store.
|
||||
*/
|
||||
export interface RootState {
|
||||
readonly bootloader: BootloaderState;
|
||||
readonly ble: BLEState;
|
||||
readonly editor: EditorState;
|
||||
readonly hub: HubState;
|
||||
}
|
||||
|
||||
export default combineReducers({ ble, editor, hub });
|
||||
export default combineReducers({ bootloader, ble, editor, hub });
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import { Channel, buffers } from 'redux-saga';
|
||||
import {
|
||||
Effect,
|
||||
race,
|
||||
put,
|
||||
take,
|
||||
takeEvery,
|
||||
delay,
|
||||
fork,
|
||||
actionChannel,
|
||||
} from 'redux-saga/effects';
|
||||
import { Action } from 'redux';
|
||||
import {
|
||||
BootloaderActionType,
|
||||
infoRequest,
|
||||
BootloaderResponseActionType,
|
||||
BootloaderInfoResponseAction,
|
||||
BootloaderEraseResponseAction,
|
||||
BootloaderFlashFirmwareAction,
|
||||
BootloaderInitResponseAction,
|
||||
eraseRequest,
|
||||
initRequest,
|
||||
programRequest,
|
||||
BootloaderProgramResponseAction,
|
||||
rebootRequest,
|
||||
BootloaderErrorResponseAction,
|
||||
BootloaderRequestActionType,
|
||||
BootloaderRequestAction,
|
||||
eraseResponse,
|
||||
programResponse,
|
||||
initResponse,
|
||||
infoResponse,
|
||||
checksumResponse,
|
||||
stateResponse,
|
||||
BootloaderConnectionActionType,
|
||||
connect,
|
||||
BootloaderConnectionDidErrorAction,
|
||||
BootloaderConnectionDidConnectAction,
|
||||
BootloaderConnectionDidCancelAction,
|
||||
send,
|
||||
BootloaderConnectionDidReceiveAction,
|
||||
BootloaderChecksumResponseAction,
|
||||
checksumRequest,
|
||||
} from '../actions/bootloader';
|
||||
import {
|
||||
createEraseFlashRequest,
|
||||
createProgramFlashRequest,
|
||||
createStartAppRequest,
|
||||
createInitLoaderRequest,
|
||||
createGetInfoRequest,
|
||||
createGetChecksumRequest,
|
||||
createGetFlashStateRequest,
|
||||
createDisconnectRequest,
|
||||
getMessageType,
|
||||
parseEraseFlashResponse,
|
||||
Command,
|
||||
parseProgramFlashResponse,
|
||||
parseInitLoaderResponse,
|
||||
parseGetInfoResponse,
|
||||
parseGetChecksumResponse,
|
||||
parseGetFlashStateResponse,
|
||||
ErrorBytecode,
|
||||
MaxProgramFlashSize,
|
||||
} from '../protocols/bootloader';
|
||||
|
||||
/**
|
||||
* Converts a request action into bytecodes and creates a new action to send
|
||||
* the bytecodes to to the device.
|
||||
* @param action The request action that was observed.
|
||||
*/
|
||||
function* encodeRequest(): Generator {
|
||||
// Using a while loop to serialize sending data to avoid "busy" errors.
|
||||
|
||||
const chan = (yield actionChannel(
|
||||
(a: Action) => Object.values(BootloaderRequestActionType).includes(a.type),
|
||||
buffers.expanding(),
|
||||
)) as Channel<BootloaderRequestAction>;
|
||||
while (true) {
|
||||
const action = (yield take(chan)) as BootloaderRequestAction;
|
||||
|
||||
switch (action.type) {
|
||||
case BootloaderRequestActionType.Erase:
|
||||
yield put(send(createEraseFlashRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.Program:
|
||||
yield put(
|
||||
send(createProgramFlashRequest(action.address, action.payload)),
|
||||
);
|
||||
break;
|
||||
case BootloaderRequestActionType.Reboot:
|
||||
yield put(send(createStartAppRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.Init:
|
||||
yield put(send(createInitLoaderRequest(action.firmwareSize)));
|
||||
break;
|
||||
case BootloaderRequestActionType.Info:
|
||||
yield put(send(createGetInfoRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.Checksum:
|
||||
yield put(send(createGetChecksumRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.State:
|
||||
yield put(send(createGetFlashStateRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.Disconnect:
|
||||
yield put(send(createDisconnectRequest()));
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown bootloader request action ${action}`);
|
||||
break;
|
||||
}
|
||||
|
||||
yield take(BootloaderConnectionActionType.DidSend);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an incoming connection message to a response action.
|
||||
* @param action The received response action.
|
||||
*/
|
||||
function* decodeResponse(action: BootloaderConnectionDidReceiveAction): Generator {
|
||||
const responseType = getMessageType(action.data);
|
||||
switch (responseType) {
|
||||
case Command.EraseFlash:
|
||||
yield put(eraseResponse(parseEraseFlashResponse(action.data)));
|
||||
break;
|
||||
case Command.ProgramFlash:
|
||||
yield put(programResponse(...parseProgramFlashResponse(action.data)));
|
||||
break;
|
||||
case Command.InitLoader:
|
||||
yield put(initResponse(parseInitLoaderResponse(action.data)));
|
||||
break;
|
||||
case Command.GetInfo:
|
||||
yield put(infoResponse(...parseGetInfoResponse(action.data)));
|
||||
break;
|
||||
case Command.GetChecksum:
|
||||
yield put(checksumResponse(parseGetChecksumResponse(action.data)));
|
||||
break;
|
||||
case Command.GetFlashState:
|
||||
yield put(stateResponse(parseGetFlashStateResponse(action.data)));
|
||||
break;
|
||||
case ErrorBytecode:
|
||||
yield put(stateResponse(parseGetFlashStateResponse(action.data)));
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown bootloader response action ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type for return value of wait() function.
|
||||
*/
|
||||
type WaitResponse<T extends Action<BootloaderResponseActionType>> = [
|
||||
T,
|
||||
BootloaderErrorResponseAction,
|
||||
boolean,
|
||||
];
|
||||
|
||||
/**
|
||||
* Waits for a response action, an error response or timeout, whichever comes
|
||||
* first.
|
||||
* @param type The action type to wait for.
|
||||
* @param timeout The timeout in milliseconds.
|
||||
*/
|
||||
function wait(type: BootloaderResponseActionType, timeout = 500): Effect {
|
||||
return race([take(type), take(BootloaderResponseActionType.Error), delay(timeout)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flashes firmware to a Powered Up device.
|
||||
* @param action The action that triggered this saga.
|
||||
*/
|
||||
function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator {
|
||||
yield put(connect());
|
||||
const didConnect = (yield take([
|
||||
BootloaderConnectionActionType.DidConnect,
|
||||
BootloaderConnectionActionType.DidCancel,
|
||||
BootloaderConnectionActionType.DidError,
|
||||
])) as
|
||||
| BootloaderConnectionDidConnectAction
|
||||
| BootloaderConnectionDidCancelAction
|
||||
| BootloaderConnectionDidErrorAction;
|
||||
|
||||
if (didConnect.type === BootloaderConnectionActionType.DidCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (didConnect.type === BootloaderConnectionActionType.DidError) {
|
||||
// TODO: proper error handling
|
||||
throw didConnect.err;
|
||||
}
|
||||
|
||||
yield put(infoRequest());
|
||||
const info = (yield wait(BootloaderResponseActionType.Info)) as WaitResponse<
|
||||
BootloaderInfoResponseAction
|
||||
>;
|
||||
if (!info[0]) {
|
||||
throw Error(`failed to get info: ${info}`);
|
||||
}
|
||||
|
||||
// TODO: verify hubType === info.response.hubType
|
||||
|
||||
yield put(eraseRequest());
|
||||
const erase = (yield wait(
|
||||
BootloaderResponseActionType.Erase,
|
||||
5000,
|
||||
)) as WaitResponse<BootloaderEraseResponseAction>;
|
||||
if (!erase[0] || erase[0].result) {
|
||||
// TODO: proper error handling
|
||||
throw Error(`Failed to erase: ${erase}`);
|
||||
}
|
||||
|
||||
yield put(initRequest(action.data.byteLength));
|
||||
const init = (yield wait(BootloaderResponseActionType.Init)) as WaitResponse<
|
||||
BootloaderInitResponseAction
|
||||
>;
|
||||
if (!init[0] || init[0].result) {
|
||||
// TODO: proper error handling
|
||||
throw Error(`Failed to init: ${init}`);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
|
||||
for (
|
||||
let offset = 0;
|
||||
offset < action.data.byteLength;
|
||||
offset += MaxProgramFlashSize
|
||||
) {
|
||||
const payload = action.data.slice(offset, offset + MaxProgramFlashSize);
|
||||
yield put(programRequest(info[0].startAddress + offset, payload));
|
||||
|
||||
// TODO: dispatch progress action
|
||||
|
||||
// request checksum every so often to prevent buffer overrun on the hub
|
||||
// because of sending too much data at once
|
||||
if (++count % 10 === 0) {
|
||||
yield put(checksumRequest());
|
||||
const checksum = (yield wait(
|
||||
BootloaderResponseActionType.Checksum,
|
||||
5000,
|
||||
)) as WaitResponse<BootloaderChecksumResponseAction>;
|
||||
if (!checksum[0]) {
|
||||
// TODO: proper error handling
|
||||
throw Error(`Failed to get checksum: ${checksum}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flash = (yield wait(
|
||||
BootloaderResponseActionType.Program,
|
||||
5000,
|
||||
)) as WaitResponse<BootloaderProgramResponseAction>;
|
||||
if (!flash[0]) {
|
||||
throw Error(`failed to get final response: ${flash}`);
|
||||
}
|
||||
if (flash[0].count !== action.data.byteLength) {
|
||||
// TODO: proper error handling
|
||||
throw Error("Didn't flash all bytes");
|
||||
}
|
||||
|
||||
// this will cause the remote device to disconnect and reboot
|
||||
yield put(rebootRequest());
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield fork(encodeRequest);
|
||||
yield takeEvery(BootloaderConnectionActionType.DidReceive, decodeResponse);
|
||||
yield takeEvery(BootloaderActionType.FlashFirmware, flashFirmware);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
import bootloader from './bootloader';
|
||||
|
||||
export default function* (): Generator {
|
||||
yield all([bootloader()]);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Action, Dispatch } from 'redux';
|
||||
import { ServiceUUID, CharacteristicUUID } from '../protocols/bootloader';
|
||||
import {
|
||||
BootloaderConnectionActionType,
|
||||
didCancel,
|
||||
didError,
|
||||
didReceive,
|
||||
didDisconnect,
|
||||
didConnect,
|
||||
BootloaderConnectionSendAction,
|
||||
didSend,
|
||||
} from '../actions/bootloader';
|
||||
import { combineServices } from '.';
|
||||
|
||||
let device: BluetoothDevice | undefined;
|
||||
let char: BluetoothRemoteGATTCharacteristic | undefined;
|
||||
|
||||
async function connect(action: Action, dispatch: Dispatch): Promise<void> {
|
||||
if (action.type !== BootloaderConnectionActionType.Connect) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (device) {
|
||||
throw Error('already connected');
|
||||
}
|
||||
if (navigator.bluetooth === undefined) {
|
||||
throw Error('No web bluetooth');
|
||||
}
|
||||
try {
|
||||
device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [ServiceUUID] }],
|
||||
optionalServices: [ServiceUUID],
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof DOMException &&
|
||||
err.code === DOMException.NOT_FOUND_ERR
|
||||
) {
|
||||
// this error is received if the user clicks the cancel button in
|
||||
// the bluetooth scan dialog
|
||||
dispatch(didCancel());
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (device.gatt === undefined) {
|
||||
throw Error('Device does not support GATT');
|
||||
}
|
||||
device.addEventListener('gattserverdisconnected', () => {
|
||||
device = undefined;
|
||||
char = undefined;
|
||||
dispatch(didDisconnect());
|
||||
});
|
||||
const server = await device.gatt.connect();
|
||||
try {
|
||||
const service = await server.getPrimaryService(ServiceUUID);
|
||||
char = await service.getCharacteristic(CharacteristicUUID);
|
||||
char.addEventListener('characteristicvaluechanged', () => {
|
||||
if (!char || !char.value) {
|
||||
return;
|
||||
}
|
||||
dispatch(didReceive(char.value));
|
||||
});
|
||||
await char.startNotifications();
|
||||
} catch (err) {
|
||||
device.gatt.disconnect();
|
||||
throw err;
|
||||
}
|
||||
dispatch(didConnect());
|
||||
} catch (err) {
|
||||
dispatch(didError(err));
|
||||
}
|
||||
}
|
||||
|
||||
async function send(action: Action, dispatch: Dispatch): Promise<void> {
|
||||
if (action.type !== BootloaderConnectionActionType.Send) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!char) {
|
||||
throw Error('Not connected');
|
||||
}
|
||||
await char.writeValue((action as BootloaderConnectionSendAction).data);
|
||||
dispatch(didSend());
|
||||
} catch (err) {
|
||||
dispatch(didSend(err));
|
||||
}
|
||||
}
|
||||
|
||||
export default combineServices(connect, send);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Action, Dispatch, Middleware } from 'redux';
|
||||
import bootloader from './bootloader';
|
||||
|
||||
type Service = (action: Action, dispatch: Dispatch) => Promise<void>;
|
||||
|
||||
function runService(service: Service, action: Action, dispatch: Dispatch): void {
|
||||
service(action, dispatch).catch((err) =>
|
||||
console.log(`Unhandled exception in service: ${err}`),
|
||||
);
|
||||
}
|
||||
|
||||
export function combineServices(...services: Service[]): Service {
|
||||
return (a, d): Promise<void> => {
|
||||
services.forEach((s) => runService(s, a, d));
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
const rootService = combineServices(bootloader);
|
||||
|
||||
const serviceMiddleware: Middleware = (store) => (next) => (action): unknown => {
|
||||
runService(rootService, action, store.dispatch);
|
||||
return next(action);
|
||||
};
|
||||
|
||||
export default serviceMiddleware;
|
||||
@@ -1876,6 +1876,50 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.3.3.tgz#8731722aeb7330e8fd9eb5d424be6b98dea7d6da"
|
||||
integrity sha512-yEvVC8RfhRPkD9TUn7cFcLcgoJePgZRAOR7T21rcRY5I8tpuhzeWfGa7We7tB14fe9R7wENdqUABcMdwD4SQLw==
|
||||
|
||||
"@redux-saga/core@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4"
|
||||
integrity sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.3"
|
||||
"@redux-saga/deferred" "^1.1.2"
|
||||
"@redux-saga/delay-p" "^1.1.2"
|
||||
"@redux-saga/is" "^1.1.2"
|
||||
"@redux-saga/symbols" "^1.1.2"
|
||||
"@redux-saga/types" "^1.1.0"
|
||||
redux "^4.0.4"
|
||||
typescript-tuple "^2.2.1"
|
||||
|
||||
"@redux-saga/deferred@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/deferred/-/deferred-1.1.2.tgz#59937a0eba71fff289f1310233bc518117a71888"
|
||||
integrity sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ==
|
||||
|
||||
"@redux-saga/delay-p@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/delay-p/-/delay-p-1.1.2.tgz#8f515f4b009b05b02a37a7c3d0ca9ddc157bb355"
|
||||
integrity sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==
|
||||
dependencies:
|
||||
"@redux-saga/symbols" "^1.1.2"
|
||||
|
||||
"@redux-saga/is@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/is/-/is-1.1.2.tgz#ae6c8421f58fcba80faf7cadb7d65b303b97e58e"
|
||||
integrity sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==
|
||||
dependencies:
|
||||
"@redux-saga/symbols" "^1.1.2"
|
||||
"@redux-saga/types" "^1.1.0"
|
||||
|
||||
"@redux-saga/symbols@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/symbols/-/symbols-1.1.2.tgz#216a672a487fc256872b8034835afc22a2d0595d"
|
||||
integrity sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ==
|
||||
|
||||
"@redux-saga/types@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.1.0.tgz#0e81ce56b4883b4b2a3001ebe1ab298b84237204"
|
||||
integrity sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==
|
||||
|
||||
"@restart/context@^2.1.4":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@restart/context/-/context-2.1.4.tgz#a99d87c299a34c28bd85bb489cb07bfd23149c02"
|
||||
@@ -10279,6 +10323,13 @@ redux-observable@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.2.0.tgz#ff51b6c6be2598e9b5e89fc36639186bb0e669c7"
|
||||
integrity sha512-yeR90RP2WzZzCxxnQPlh2uFzyfFLsfXu8ROh53jGDPXVqj71uNDMmvi/YKQkd9ofiVoO4OYb1snbowO49tCEMg==
|
||||
|
||||
redux-saga@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.1.3.tgz#9f3e6aebd3c994bbc0f6901a625f9a42b51d1112"
|
||||
integrity sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==
|
||||
dependencies:
|
||||
"@redux-saga/core" "^1.1.3"
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
@@ -10294,7 +10345,7 @@ redux@^3.6.0:
|
||||
loose-envify "^1.1.0"
|
||||
symbol-observable "^1.0.3"
|
||||
|
||||
redux@^4.0.0, redux@^4.0.5:
|
||||
redux@^4.0.0, redux@^4.0.4, redux@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
||||
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
||||
@@ -11889,6 +11940,25 @@ typedarray@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript-compare@^0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript-compare/-/typescript-compare-0.0.2.tgz#7ee40a400a406c2ea0a7e551efd3309021d5f425"
|
||||
integrity sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==
|
||||
dependencies:
|
||||
typescript-logic "^0.0.0"
|
||||
|
||||
typescript-logic@^0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-logic/-/typescript-logic-0.0.0.tgz#66ebd82a2548f2b444a43667bec120b496890196"
|
||||
integrity sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==
|
||||
|
||||
typescript-tuple@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-tuple/-/typescript-tuple-2.2.1.tgz#7d9813fb4b355f69ac55032e0363e8bb0f04dad2"
|
||||
integrity sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==
|
||||
dependencies:
|
||||
typescript-compare "^0.0.2"
|
||||
|
||||
typescript@~3.7.2:
|
||||
version "3.7.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
|
||||
|
||||
Reference in New Issue
Block a user