mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
remove use of thunk action in ble and hub
This is a step towards moving these to sagas
This commit is contained in:
committed by
David Lechner
parent
99fd62100d
commit
24acab901c
+39
-119
@@ -2,23 +2,6 @@
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Action } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import {
|
||||
PolyfillBluetoothRemoteGATTCharacteristic,
|
||||
polyfillBluetoothRemoteGATTCharacteristic,
|
||||
} from '../utils/web-bluetooth';
|
||||
import * as notification from './notification';
|
||||
|
||||
const pybricksServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef';
|
||||
|
||||
// nRF UART service (Nus)
|
||||
const bleNusServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusCharRXUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusCharTXUUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusMaxSize = 20;
|
||||
|
||||
let device: BluetoothDevice | undefined;
|
||||
let rxChar: PolyfillBluetoothRemoteGATTCharacteristic | undefined;
|
||||
|
||||
/**
|
||||
* Bluetooth low energy connection action types.
|
||||
@@ -27,7 +10,7 @@ export enum BLEConnectActionType {
|
||||
/**
|
||||
* Connecting to a device has been requested.
|
||||
*/
|
||||
WillConnect = 'ble.action.will.connect',
|
||||
Connect = 'ble.action.connect',
|
||||
/**
|
||||
* The connection completed successfully.
|
||||
*/
|
||||
@@ -35,7 +18,7 @@ export enum BLEConnectActionType {
|
||||
/**
|
||||
* Disconnecting from a device has been requested.
|
||||
*/
|
||||
WillDisconnect = 'ble.action.will.disconnect',
|
||||
Disconnect = 'ble.action.disconnect',
|
||||
/**
|
||||
* End async disconnect (can be sent without sending BeginDisconnect first).
|
||||
*/
|
||||
@@ -45,141 +28,78 @@ export enum BLEConnectActionType {
|
||||
/**
|
||||
* Common type for all BLE connection actions.
|
||||
*/
|
||||
type BLEConnectAction = Action<BLEConnectActionType>;
|
||||
export type BLEConnectAction = Action<BLEConnectActionType>;
|
||||
|
||||
/**
|
||||
* Creates an action that indicates connecting has been requested.
|
||||
*/
|
||||
function willConnect(): BLEConnectAction {
|
||||
return { type: BLEConnectActionType.WillConnect };
|
||||
export function connect(): BLEConnectAction {
|
||||
return { type: BLEConnectActionType.Connect };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action that indicates a device was connected.
|
||||
*/
|
||||
function didConnect(): BLEConnectAction {
|
||||
export function didConnect(): BLEConnectAction {
|
||||
return { type: BLEConnectActionType.DidConnect };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action that indicates disconnecting was requested.
|
||||
*/
|
||||
function willDisconnect(): BLEConnectAction {
|
||||
return { type: BLEConnectActionType.WillDisconnect };
|
||||
export function disconnect(): BLEConnectAction {
|
||||
return { type: BLEConnectActionType.Disconnect };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action that indicates a device was disconnected.
|
||||
*/
|
||||
function didDisconnect(): BLEConnectAction {
|
||||
export function didDisconnect(): BLEConnectAction {
|
||||
return { type: BLEConnectActionType.DidDisconnect };
|
||||
}
|
||||
|
||||
export enum BLEDataActionType {
|
||||
/**
|
||||
* Send data.
|
||||
* Write data.
|
||||
*/
|
||||
SendData = 'ble.data.send',
|
||||
Write = 'ble.data.write',
|
||||
/**
|
||||
* Data was received.
|
||||
* Notify that data was received.
|
||||
*/
|
||||
ReceivedData = 'ble.data.receive',
|
||||
Notify = 'ble.data.receive',
|
||||
}
|
||||
|
||||
export interface BLEDataAction extends Action<BLEDataActionType> {
|
||||
export interface BLEDataWriteAction extends Action<BLEDataActionType.Write> {
|
||||
value: Uint8Array;
|
||||
}
|
||||
|
||||
export function write(value: Uint8Array): BLEDataWriteAction {
|
||||
return { type: BLEDataActionType.Write, value };
|
||||
}
|
||||
|
||||
export interface BLEDataNotifyAction extends Action<BLEDataActionType.Notify> {
|
||||
value: DataView;
|
||||
}
|
||||
|
||||
export type BLEThunkAction = ThunkAction<Promise<void>, {}, {}, Action>;
|
||||
|
||||
export function connect(): BLEThunkAction {
|
||||
return async function (dispatch): Promise<void> {
|
||||
if (device !== undefined) {
|
||||
dispatch(notification.add('error', 'A device is already connected.'));
|
||||
return;
|
||||
}
|
||||
if (navigator.bluetooth === undefined) {
|
||||
dispatch(
|
||||
notification.add(
|
||||
'error',
|
||||
'This web browser does not support Web Bluetooth or it is not enabled.',
|
||||
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// TODO: check navigator.bluetooth.getAvailability()
|
||||
dispatch(willConnect());
|
||||
try {
|
||||
device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [pybricksServiceUUID] }],
|
||||
optionalServices: [bleNusServiceUUID],
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof DOMException &&
|
||||
err.code === DOMException.NOT_FOUND_ERR
|
||||
) {
|
||||
// this can happen if the use cancels the dialog
|
||||
console.debug('User cancelled connect');
|
||||
} else {
|
||||
console.error(err);
|
||||
dispatch(
|
||||
notification.add(
|
||||
'error',
|
||||
'Unexpected error, check developer console for details.',
|
||||
),
|
||||
);
|
||||
}
|
||||
dispatch(didDisconnect());
|
||||
return;
|
||||
}
|
||||
if (device.gatt === undefined) {
|
||||
dispatch(notification.add('error', 'Device does not support GATT.'));
|
||||
dispatch(didDisconnect());
|
||||
return;
|
||||
}
|
||||
device.addEventListener('gattserverdisconnected', () => {
|
||||
device = undefined;
|
||||
rxChar = undefined;
|
||||
dispatch(didDisconnect());
|
||||
});
|
||||
const server = await device.gatt.connect();
|
||||
try {
|
||||
const service = await server.getPrimaryService(bleNusServiceUUID);
|
||||
rxChar = polyfillBluetoothRemoteGATTCharacteristic(
|
||||
await service.getCharacteristic(bleNusCharRXUUID),
|
||||
);
|
||||
const txChar = await service.getCharacteristic(bleNusCharTXUUID);
|
||||
txChar.addEventListener('characteristicvaluechanged', () => {
|
||||
if (!txChar.value) {
|
||||
return;
|
||||
}
|
||||
dispatch({ type: BLEDataActionType.ReceivedData, value: txChar.value });
|
||||
});
|
||||
await txChar.startNotifications();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dispatch(notification.add('error', 'Getting nRF UART service failed.'));
|
||||
device.gatt.disconnect();
|
||||
return;
|
||||
}
|
||||
dispatch(didConnect());
|
||||
};
|
||||
export function notify(value: DataView): BLEDataNotifyAction {
|
||||
return { type: BLEDataActionType.Notify, value };
|
||||
}
|
||||
|
||||
export function disconnect(): BLEThunkAction {
|
||||
return async function (dispatch): Promise<void> {
|
||||
dispatch(willDisconnect());
|
||||
device?.gatt?.disconnect();
|
||||
};
|
||||
/** Common type for low-level BLE data actions. */
|
||||
export type BLEDataAction = BLEDataWriteAction | BLEDataNotifyAction;
|
||||
|
||||
/**
|
||||
* High-level BLE actions.
|
||||
*/
|
||||
export enum BLEActionType {
|
||||
Toggle = 'ble.action.toggle',
|
||||
}
|
||||
|
||||
export function write(value: ArrayBuffer): BLEThunkAction {
|
||||
return async function (): Promise<void> {
|
||||
// TODO: do we need to dispatch any Action<>s here?
|
||||
for (let i = 0; i < value.byteLength; i += bleNusMaxSize) {
|
||||
await rxChar?.xWriteValueWithoutResponse(value.slice(i, i + bleNusMaxSize));
|
||||
}
|
||||
};
|
||||
export type BLEToggleAction = Action<BLEActionType.Toggle>;
|
||||
|
||||
export function toggleBluetooth(): BLEToggleAction {
|
||||
return { type: BLEActionType.Toggle };
|
||||
}
|
||||
|
||||
/** Common type for high-level BLE actions */
|
||||
export type BLEAction = BLEToggleAction;
|
||||
|
||||
+45
-57
@@ -2,11 +2,6 @@
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Action } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { getChecksum } from '../epics/hub';
|
||||
import { write } from './ble';
|
||||
|
||||
export type HubThunkAction = ThunkAction<Promise<void>, {}, {}, HubRuntimeStatusAction>;
|
||||
|
||||
export enum HubRuntimeStatusType {
|
||||
Disconnected = 'disconnected',
|
||||
@@ -17,83 +12,76 @@ export enum HubRuntimeStatusType {
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export enum HubActionType {
|
||||
export enum HubMessageActionType {
|
||||
/**
|
||||
* MicroPython runtime status changed.
|
||||
* The hub has send a message indicating the MicroPython runtime status changed.
|
||||
*/
|
||||
RuntimeStatus = 'hub.runtime.status',
|
||||
RuntimeStatus = 'hub.message.action.runtime.status',
|
||||
/**
|
||||
* The hub has sent a checksum.
|
||||
*/
|
||||
Checksum = 'hub.runtime.checksum',
|
||||
Checksum = 'hub.message.action.runtime.checksum',
|
||||
}
|
||||
|
||||
export interface HubRuntimeStatusAction extends Action<HubActionType.RuntimeStatus> {
|
||||
export interface HubRuntimeStatusMessageAction
|
||||
extends Action<HubMessageActionType.RuntimeStatus> {
|
||||
readonly newStatus: HubRuntimeStatusType;
|
||||
}
|
||||
|
||||
export interface HubChecksumAction extends Action<HubActionType.Checksum> {
|
||||
readonly checksum: number;
|
||||
}
|
||||
|
||||
export function updateStatus(newStatus: HubRuntimeStatusType): HubRuntimeStatusAction {
|
||||
export function updateStatus(
|
||||
newStatus: HubRuntimeStatusType,
|
||||
): HubRuntimeStatusMessageAction {
|
||||
return {
|
||||
type: HubActionType.RuntimeStatus,
|
||||
type: HubMessageActionType.RuntimeStatus,
|
||||
newStatus,
|
||||
};
|
||||
}
|
||||
|
||||
export function checksum(checksum: number): HubChecksumAction {
|
||||
export interface HubChecksumMessageAction
|
||||
extends Action<HubMessageActionType.Checksum> {
|
||||
readonly checksum: number;
|
||||
}
|
||||
|
||||
export function checksum(checksum: number): HubChecksumMessageAction {
|
||||
return {
|
||||
type: HubActionType.Checksum,
|
||||
type: HubMessageActionType.Checksum,
|
||||
checksum,
|
||||
};
|
||||
}
|
||||
|
||||
const downloadChunkSize = 100;
|
||||
/**
|
||||
* Common type for low-level hub message actions.
|
||||
*/
|
||||
export type HubMessageAction = HubRuntimeStatusMessageAction | HubChecksumMessageAction;
|
||||
|
||||
export function downloadAndRun(data: ArrayBuffer): HubThunkAction {
|
||||
return async function (dispatch): Promise<void> {
|
||||
// let everyone know the runtime is busy loading the program
|
||||
dispatch(updateStatus(HubRuntimeStatusType.Loading));
|
||||
|
||||
// TODO: might need to flush checksum queue here
|
||||
|
||||
// first send payload size as big-endian 32-bit integer
|
||||
const checksum = getChecksum();
|
||||
const sizeBuf = new Uint8Array(4);
|
||||
const sizeView = new DataView(sizeBuf.buffer);
|
||||
sizeView.setUint32(0, data.byteLength, true);
|
||||
await dispatch(write(sizeBuf));
|
||||
// TODO: verify checksum
|
||||
console.log(await checksum);
|
||||
|
||||
// Then send payload in 100 byte chunks waiting for checksum after
|
||||
// each chunk
|
||||
for (let i = 0; i < data.byteLength; i += downloadChunkSize) {
|
||||
// need to subscribe to checksum before writing to prevent race condition
|
||||
const checksum = getChecksum();
|
||||
await dispatch(write(data.slice(i, i + downloadChunkSize)));
|
||||
// TODO: verify checksum
|
||||
console.log(await checksum);
|
||||
// TODO: dispatch progress
|
||||
}
|
||||
|
||||
// let everyone know the runtime is done loading the program
|
||||
dispatch(updateStatus(HubRuntimeStatusType.Loaded));
|
||||
};
|
||||
/**
|
||||
* High-level hub actions.
|
||||
*/
|
||||
export enum HubActionType {
|
||||
DownloadAndRun = 'hub.action.downloadAndRun',
|
||||
Stop = 'hub.action.stop',
|
||||
Repl = 'hub.action.repl',
|
||||
}
|
||||
|
||||
// SPACE, SPACE, SPACE, SPACE
|
||||
const startReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
|
||||
export type HubDownloadAndRunAction = Action<HubActionType.DownloadAndRun>;
|
||||
|
||||
export function startRepl(): HubThunkAction {
|
||||
return write(startReplCommand);
|
||||
export function downloadAndRun(): HubDownloadAndRunAction {
|
||||
return { type: HubActionType.DownloadAndRun };
|
||||
}
|
||||
|
||||
// CTRL+C, CTRL+C, CTRL+D
|
||||
const stopCommand = new Uint8Array([0x03, 0x03, 0x04]);
|
||||
export type HubStopAction = Action<HubActionType.Stop>;
|
||||
|
||||
export function stop(): HubThunkAction {
|
||||
return write(stopCommand);
|
||||
export function stop(): HubStopAction {
|
||||
return { type: HubActionType.Stop };
|
||||
}
|
||||
|
||||
export type HubReplAction = Action<HubActionType.Repl>;
|
||||
|
||||
export function repl(): HubReplAction {
|
||||
return { type: HubActionType.Repl };
|
||||
}
|
||||
|
||||
/**
|
||||
* Common type for all high-level hub actions.
|
||||
*/
|
||||
export type HubAction = HubDownloadAndRunAction | HubStopAction | HubReplAction;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Dispatch as ReduxDispatch } from 'redux';
|
||||
import { BLEAction, BLEConnectAction, BLEDataAction } from './ble';
|
||||
import {
|
||||
BootloaderAction,
|
||||
BootloaderConnectionAction,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
BootloaderResponseAction,
|
||||
} from './bootloader';
|
||||
import { EditorAction } from './editor';
|
||||
import { HubAction, HubMessageAction } from './hub';
|
||||
import { NotificationAction } from './notification';
|
||||
import { ServiceWorkerAction } from './service-worker';
|
||||
import { TerminalDataAction } from './terminal';
|
||||
@@ -15,12 +17,17 @@ import { TerminalDataAction } from './terminal';
|
||||
* Common type for all actions.
|
||||
*/
|
||||
export type Action =
|
||||
| BLEConnectAction
|
||||
| BLEDataAction
|
||||
| BLEAction
|
||||
| BootloaderConnectionAction
|
||||
| BootloaderRequestAction
|
||||
| BootloaderDidRequestAction
|
||||
| BootloaderResponseAction
|
||||
| BootloaderAction
|
||||
| EditorAction
|
||||
| HubMessageAction
|
||||
| HubAction
|
||||
| NotificationAction
|
||||
| ServiceWorkerAction
|
||||
| TerminalDataAction;
|
||||
|
||||
@@ -7,7 +7,7 @@ import Image from 'react-bootstrap/Image';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/Tooltip';
|
||||
|
||||
export interface ActionButtonProps<T = undefined> {
|
||||
export interface ActionButtonProps {
|
||||
/** A unique id for each instance. */
|
||||
readonly id: string;
|
||||
/** Tooltip text that appears when hovering over the button. */
|
||||
@@ -16,13 +16,11 @@ export interface ActionButtonProps<T = undefined> {
|
||||
readonly icon: string;
|
||||
/** When true or undefined, the button is enabled. */
|
||||
readonly enabled?: boolean;
|
||||
/** Optional action hint passed to the onAction() callback. */
|
||||
readonly context?: T;
|
||||
/** Callback that is called when the button is activated (clicked). */
|
||||
readonly onAction: (context?: T) => void;
|
||||
readonly onAction: () => void;
|
||||
}
|
||||
|
||||
class ActionButton<T = undefined> extends React.Component<ActionButtonProps<T>> {
|
||||
class ActionButton extends React.Component<ActionButtonProps> {
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
@@ -35,7 +33,7 @@ class ActionButton<T = undefined> extends React.Component<ActionButtonProps<T>>
|
||||
>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={(): void => this.props.onAction(this.props.context)}
|
||||
onClick={(): void => this.props.onAction()}
|
||||
disabled={this.props.enabled === false}
|
||||
style={
|
||||
this.props.enabled === false
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { connect as bleConnect, disconnect as bleDisconnect } from '../actions/ble';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { toggleBluetooth } from '../actions/ble';
|
||||
import { RootState } from '../reducers';
|
||||
import { BLEConnectionState } from '../reducers/ble';
|
||||
import { BootloaderConnectionState } from '../reducers/bootloader';
|
||||
@@ -12,11 +11,8 @@ import ActionButton, { ActionButtonProps } from './ActionButton';
|
||||
import btConnectedIcon from './images/bt-connected.svg';
|
||||
import btDisconnectedIcon from './images/bt-disconnected.svg';
|
||||
|
||||
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
|
||||
|
||||
type ButtonProps = ActionButtonProps<string>;
|
||||
type StateProps = Pick<ButtonProps, 'tooltip' | 'icon' | 'context' | 'enabled'>;
|
||||
type DispatchProps = Pick<ButtonProps, 'onAction'>;
|
||||
type StateProps = Pick<ActionButtonProps, 'tooltip' | 'icon' | 'enabled'>;
|
||||
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => {
|
||||
if (
|
||||
@@ -26,27 +22,19 @@ const mapStateToProps = (state: RootState): StateProps => {
|
||||
return {
|
||||
tooltip: 'Connect using Bluetooth',
|
||||
icon: btDisconnectedIcon,
|
||||
context: 'connect',
|
||||
enabled: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
tooltip: 'Disconnect Bluetooth',
|
||||
icon: btConnectedIcon,
|
||||
context: 'disconnect',
|
||||
enabled: state.ble.connection === BLEConnectionState.Connected,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onAction: (c): void => {
|
||||
if (c === 'connect') {
|
||||
dispatch(bleConnect());
|
||||
} else {
|
||||
dispatch(bleDisconnect());
|
||||
}
|
||||
},
|
||||
onAction: (): Action => dispatch(toggleBluetooth()),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ActionButton);
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { startRepl } from '../actions/hub';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { repl } from '../actions/hub';
|
||||
import { RootState } from '../reducers';
|
||||
import { HubRuntimeState } from '../reducers/hub';
|
||||
import ActionButton, { ActionButtonProps } from './ActionButton';
|
||||
import replIcon from './images/repl.svg';
|
||||
|
||||
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
|
||||
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled' | 'context'>;
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled'>;
|
||||
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
|
||||
type OwnProps = Pick<ActionButtonProps, 'id'>;
|
||||
|
||||
@@ -23,9 +20,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onAction: (): void => {
|
||||
dispatch(startRepl());
|
||||
},
|
||||
onAction: (): Action => dispatch(repl()),
|
||||
});
|
||||
|
||||
const mergeProps = (
|
||||
|
||||
@@ -1,59 +1,32 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Ace } from 'ace-builds';
|
||||
import { connect } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { downloadAndRun } from '../actions/hub';
|
||||
import { compile } from '../actions/mpy';
|
||||
import * as notification from '../actions/notification';
|
||||
import { RootState } from '../reducers';
|
||||
import { HubRuntimeState } from '../reducers/hub';
|
||||
import ActionButton, { ActionButtonProps } from './ActionButton';
|
||||
import runIcon from './images/run.svg';
|
||||
|
||||
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
|
||||
|
||||
type ButtonProps = ActionButtonProps<Ace.EditSession>;
|
||||
type StateProps = Pick<ButtonProps, 'enabled' | 'context'>;
|
||||
type DispatchProps = Pick<ButtonProps, 'onAction'>;
|
||||
type OwnProps = Pick<ButtonProps, 'id'>;
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled'>;
|
||||
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
|
||||
type OwnProps = Pick<ActionButtonProps, 'id'>;
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
enabled:
|
||||
state.editor.current !== null && state.hub.runtime === HubRuntimeState.Idle,
|
||||
context: state.editor.current || undefined,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onAction: (c): void => {
|
||||
if (!c) {
|
||||
console.error('No current editor');
|
||||
return;
|
||||
}
|
||||
const script = c.getValue();
|
||||
// TODO: need to get options from hub because they depend on firmware compile options
|
||||
dispatch(compile(script, ['-mno-unicode']))
|
||||
.then((mpy) => {
|
||||
if (mpy.data) {
|
||||
dispatch(downloadAndRun(mpy.data));
|
||||
} else {
|
||||
dispatch(
|
||||
notification.add('error', mpy.err || 'Unknown compiler error.'),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
},
|
||||
onAction: (): Action => dispatch(downloadAndRun()),
|
||||
});
|
||||
|
||||
const mergeProps = (
|
||||
stateProps: StateProps,
|
||||
dispatchProps: DispatchProps,
|
||||
ownProps: OwnProps,
|
||||
): ButtonProps => ({
|
||||
tooltip: 'Download and run this program',
|
||||
): ActionButtonProps => ({
|
||||
icon: runIcon,
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { RootState } from '../reducers';
|
||||
import ActionButton, { ActionButtonProps } from './ActionButton';
|
||||
import downloadIcon from './images/download.svg';
|
||||
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled' | 'context'>;
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled'>;
|
||||
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
|
||||
type OwnProps = Pick<ActionButtonProps, 'id'>;
|
||||
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { stop } from '../actions/hub';
|
||||
import { RootState } from '../reducers';
|
||||
import { HubRuntimeState } from '../reducers/hub';
|
||||
import ActionButton, { ActionButtonProps } from './ActionButton';
|
||||
import stopIcon from './images/stop.svg';
|
||||
|
||||
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
|
||||
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled' | 'context'>;
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled'>;
|
||||
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
|
||||
type OwnProps = Pick<ActionButtonProps, 'id'>;
|
||||
|
||||
@@ -21,9 +18,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onAction: (): void => {
|
||||
dispatch(stop());
|
||||
},
|
||||
onAction: (): Action => dispatch(stop()),
|
||||
});
|
||||
|
||||
const mergeProps = (
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ const disconnect: Epic = (action$) =>
|
||||
|
||||
const rxUartData: Epic<AnyAction, AnyAction, RootState> = (action$, state$) =>
|
||||
action$.pipe(
|
||||
ofType<AnyAction, BLEDataAction>(BLEDataActionType.ReceivedData),
|
||||
ofType<AnyAction, BLEDataAction>(BLEDataActionType.Notify),
|
||||
map((a) => {
|
||||
if (
|
||||
state$.value.hub.runtime === HubRuntimeState.Loading &&
|
||||
|
||||
+2
-2
@@ -5,13 +5,13 @@ import { AnyAction } from 'redux';
|
||||
import { Epic, combineEpics, ofType } from 'redux-observable';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ignoreElements, take, tap } from 'rxjs/operators';
|
||||
import { HubActionType, HubChecksumAction } from '../actions/hub';
|
||||
import { HubChecksumMessageAction, HubMessageActionType } from '../actions/hub';
|
||||
|
||||
const checksumSubject = new Subject<number>();
|
||||
|
||||
const checksum: Epic = (action$) =>
|
||||
action$.pipe(
|
||||
ofType<AnyAction, HubChecksumAction>(HubActionType.Checksum),
|
||||
ofType<AnyAction, HubChecksumMessageAction>(HubMessageActionType.Checksum),
|
||||
tap((a) => checksumSubject.next(a.checksum)),
|
||||
ignoreElements(),
|
||||
);
|
||||
|
||||
+2
-2
@@ -31,11 +31,11 @@ const connection: Reducer<BLEConnectionState> = (
|
||||
action,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case BLEConnectActionType.WillConnect:
|
||||
case BLEConnectActionType.Connect:
|
||||
return BLEConnectionState.Connecting;
|
||||
case BLEConnectActionType.DidConnect:
|
||||
return BLEConnectionState.Connected;
|
||||
case BLEConnectActionType.WillDisconnect:
|
||||
case BLEConnectActionType.Disconnect:
|
||||
return BLEConnectionState.Disconnecting;
|
||||
case BLEConnectActionType.DidDisconnect:
|
||||
return BLEConnectionState.Disconnected;
|
||||
|
||||
+4
-4
@@ -3,8 +3,8 @@
|
||||
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import {
|
||||
HubActionType,
|
||||
HubRuntimeStatusAction,
|
||||
HubMessageActionType,
|
||||
HubRuntimeStatusMessageAction,
|
||||
HubRuntimeStatusType,
|
||||
} from '../actions/hub';
|
||||
|
||||
@@ -38,12 +38,12 @@ export enum HubRuntimeState {
|
||||
Error = 'hub.runtime.error',
|
||||
}
|
||||
|
||||
const runtime: Reducer<HubRuntimeState, HubRuntimeStatusAction> = (
|
||||
const runtime: Reducer<HubRuntimeState, HubRuntimeStatusMessageAction> = (
|
||||
state = HubRuntimeState.Disconnected,
|
||||
action,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case HubActionType.RuntimeStatus:
|
||||
case HubMessageActionType.RuntimeStatus:
|
||||
switch (action.newStatus) {
|
||||
case HubRuntimeStatusType.Disconnected:
|
||||
return HubRuntimeState.Disconnected;
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import {
|
||||
BLEActionType,
|
||||
BLEConnectActionType,
|
||||
BLEDataActionType,
|
||||
connect as connectAction,
|
||||
didConnect,
|
||||
didDisconnect,
|
||||
disconnect as disconnectAction,
|
||||
notify,
|
||||
} from '../actions/ble';
|
||||
import * as notification from '../actions/notification';
|
||||
import { RootState } from '../reducers';
|
||||
import { BLEConnectionState } from '../reducers/ble';
|
||||
import {
|
||||
PolyfillBluetoothRemoteGATTCharacteristic,
|
||||
polyfillBluetoothRemoteGATTCharacteristic,
|
||||
} from '../utils/web-bluetooth';
|
||||
import { combineServices } from '.';
|
||||
|
||||
const pybricksServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef';
|
||||
|
||||
// nRF UART service (Nus)
|
||||
const bleNusServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusCharRXUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusCharTXUUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusMaxSize = 20;
|
||||
|
||||
let device: BluetoothDevice | undefined;
|
||||
let rxChar: PolyfillBluetoothRemoteGATTCharacteristic | undefined;
|
||||
|
||||
async function connect(action: Action, dispatch: Dispatch): Promise<void> {
|
||||
if (action.type !== BLEConnectActionType.Connect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device !== undefined) {
|
||||
dispatch(notification.add('error', 'A device is already connected.'));
|
||||
return;
|
||||
}
|
||||
if (navigator.bluetooth === undefined) {
|
||||
dispatch(
|
||||
notification.add(
|
||||
'error',
|
||||
'This web browser does not support Web Bluetooth or it is not enabled.',
|
||||
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// TODO: check navigator.bluetooth.getAvailability()
|
||||
try {
|
||||
device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [pybricksServiceUUID] }],
|
||||
optionalServices: [bleNusServiceUUID],
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) {
|
||||
// this can happen if the use cancels the dialog
|
||||
console.debug('User cancelled connect');
|
||||
} else {
|
||||
console.error(err);
|
||||
dispatch(
|
||||
notification.add(
|
||||
'error',
|
||||
'Unexpected error, check developer console for details.',
|
||||
),
|
||||
);
|
||||
}
|
||||
dispatch(didDisconnect());
|
||||
return;
|
||||
}
|
||||
if (device.gatt === undefined) {
|
||||
dispatch(notification.add('error', 'Device does not support GATT.'));
|
||||
dispatch(didDisconnect());
|
||||
return;
|
||||
}
|
||||
device.addEventListener('gattserverdisconnected', () => {
|
||||
device = undefined;
|
||||
rxChar = undefined;
|
||||
dispatch(didDisconnect());
|
||||
});
|
||||
const server = await device.gatt.connect();
|
||||
try {
|
||||
const service = await server.getPrimaryService(bleNusServiceUUID);
|
||||
rxChar = polyfillBluetoothRemoteGATTCharacteristic(
|
||||
await service.getCharacteristic(bleNusCharRXUUID),
|
||||
);
|
||||
const txChar = await service.getCharacteristic(bleNusCharTXUUID);
|
||||
txChar.addEventListener('characteristicvaluechanged', () => {
|
||||
if (!txChar.value) {
|
||||
return;
|
||||
}
|
||||
dispatch(notify(txChar.value));
|
||||
});
|
||||
await txChar.startNotifications();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dispatch(notification.add('error', 'Getting nRF UART service failed.'));
|
||||
device.gatt.disconnect();
|
||||
return;
|
||||
}
|
||||
dispatch(didConnect());
|
||||
}
|
||||
|
||||
function disconnect(action: Action): void {
|
||||
if (action.type !== BLEConnectActionType.Disconnect) {
|
||||
return;
|
||||
}
|
||||
device?.gatt?.disconnect();
|
||||
}
|
||||
|
||||
async function write(action: Action): Promise<void> {
|
||||
if (action.type !== BLEDataActionType.Write) {
|
||||
return;
|
||||
}
|
||||
const value = action.value.buffer;
|
||||
for (let i = 0; i < value.byteLength; i += bleNusMaxSize) {
|
||||
await rxChar?.xWriteValueWithoutResponse(value.slice(i, i + bleNusMaxSize));
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(action: Action, dispatch: Dispatch, state: RootState): void {
|
||||
if (action.type !== BLEActionType.Toggle) {
|
||||
return;
|
||||
}
|
||||
switch (state.ble.connection) {
|
||||
case BLEConnectionState.Connected:
|
||||
dispatch(disconnectAction());
|
||||
break;
|
||||
case BLEConnectionState.Disconnected:
|
||||
dispatch(connectAction());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineServices(connect, disconnect, write, toggle);
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { AnyAction } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { Action } from '../actions';
|
||||
import { write } from '../actions/ble';
|
||||
import { HubActionType, HubRuntimeStatusType, updateStatus } from '../actions/hub';
|
||||
import { compile } from '../actions/mpy';
|
||||
import { getChecksum } from '../epics/hub';
|
||||
import { RootState } from '../reducers';
|
||||
import { combineServices } from '.';
|
||||
|
||||
// TODO: this file needs to be converted to a saga
|
||||
|
||||
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
|
||||
|
||||
const downloadChunkSize = 100;
|
||||
|
||||
async function downloadAndRun(
|
||||
action: Action,
|
||||
dispatch: Dispatch,
|
||||
state: RootState,
|
||||
): Promise<void> {
|
||||
if (action.type !== HubActionType.DownloadAndRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = state.editor.current?.getValue();
|
||||
// istanbul ignore next: it should not be possible to trigger this action without a current editor
|
||||
if (script === undefined) {
|
||||
console.log('no current editor');
|
||||
return;
|
||||
}
|
||||
const mpy = await dispatch(compile(script, ['-mno-unicode']));
|
||||
if (mpy.data === undefined) {
|
||||
console.log(`failed to compile: ${mpy.err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// let everyone know the runtime is busy loading the program
|
||||
dispatch(updateStatus(HubRuntimeStatusType.Loading));
|
||||
|
||||
// TODO: might need to flush checksum queue here
|
||||
|
||||
// first send payload size as big-endian 32-bit integer
|
||||
const checksum = getChecksum();
|
||||
const sizeBuf = new Uint8Array(4);
|
||||
const sizeView = new DataView(sizeBuf.buffer);
|
||||
sizeView.setUint32(0, mpy.data.byteLength, true);
|
||||
await dispatch(write(sizeBuf));
|
||||
// TODO: verify checksum
|
||||
console.log(await checksum);
|
||||
|
||||
// Then send payload in 100 byte chunks waiting for checksum after
|
||||
// each chunk
|
||||
for (let i = 0; i < mpy.data.byteLength; i += downloadChunkSize) {
|
||||
// need to subscribe to checksum before writing to prevent race condition
|
||||
const checksum = getChecksum();
|
||||
await dispatch(write(mpy.data.slice(i, i + downloadChunkSize)));
|
||||
// TODO: verify checksum
|
||||
console.log(await checksum);
|
||||
// TODO: dispatch progress
|
||||
}
|
||||
|
||||
// let everyone know the runtime is done loading the program
|
||||
dispatch(updateStatus(HubRuntimeStatusType.Loaded));
|
||||
}
|
||||
|
||||
// SPACE, SPACE, SPACE, SPACE
|
||||
const startReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
|
||||
|
||||
function startRepl(action: Action, dispatch: Dispatch): void {
|
||||
if (action.type !== HubActionType.Repl) {
|
||||
return;
|
||||
}
|
||||
dispatch(write(startReplCommand));
|
||||
}
|
||||
|
||||
// CTRL+C, CTRL+C, CTRL+D
|
||||
const stopCommand = new Uint8Array([0x03, 0x03, 0x04]);
|
||||
|
||||
function stop(action: Action, dispatch: Dispatch): void {
|
||||
if (action.type !== HubActionType.Stop) {
|
||||
return;
|
||||
}
|
||||
dispatch(write(stopCommand));
|
||||
}
|
||||
|
||||
export default combineServices(downloadAndRun, startRepl, stop);
|
||||
@@ -4,9 +4,11 @@
|
||||
import { Middleware } from 'redux';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { RootState } from '../reducers';
|
||||
import ble from './ble';
|
||||
import bootloader from './bootloader';
|
||||
import editor from './editor';
|
||||
import errorLog from './error-log';
|
||||
import hub from './hub';
|
||||
|
||||
type Service = (
|
||||
action: Action,
|
||||
@@ -37,7 +39,7 @@ export function combineServices(...services: Service[]): Service {
|
||||
};
|
||||
}
|
||||
|
||||
const rootService = combineServices(bootloader, editor, errorLog);
|
||||
const rootService = combineServices(ble, bootloader, editor, errorLog, hub);
|
||||
|
||||
const serviceMiddleware: Middleware = (store) => (next) => (action): unknown => {
|
||||
runService(rootService, action, store.dispatch, store.getState());
|
||||
|
||||
Reference in New Issue
Block a user