diff --git a/src/actions/ble.ts b/src/actions/ble.ts index fe9e63e1..c6d2f182 100644 --- a/src/actions/ble.ts +++ b/src/actions/ble.ts @@ -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; +export type BLEConnectAction = Action; /** * 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 { +export interface BLEDataWriteAction extends Action { + value: Uint8Array; +} + +export function write(value: Uint8Array): BLEDataWriteAction { + return { type: BLEDataActionType.Write, value }; +} + +export interface BLEDataNotifyAction extends Action { value: DataView; } -export type BLEThunkAction = ThunkAction, {}, {}, Action>; - -export function connect(): BLEThunkAction { - return async function (dispatch): Promise { - 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 { - 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 { - // 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; + +export function toggleBluetooth(): BLEToggleAction { + return { type: BLEActionType.Toggle }; } + +/** Common type for high-level BLE actions */ +export type BLEAction = BLEToggleAction; diff --git a/src/actions/hub.ts b/src/actions/hub.ts index 49b28713..c541cfca 100644 --- a/src/actions/hub.ts +++ b/src/actions/hub.ts @@ -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, {}, {}, 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 { +export interface HubRuntimeStatusMessageAction + extends Action { readonly newStatus: HubRuntimeStatusType; } -export interface HubChecksumAction extends Action { - 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 { + 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 { - // 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; -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; -export function stop(): HubThunkAction { - return write(stopCommand); +export function stop(): HubStopAction { + return { type: HubActionType.Stop }; } + +export type HubReplAction = Action; + +export function repl(): HubReplAction { + return { type: HubActionType.Repl }; +} + +/** + * Common type for all high-level hub actions. + */ +export type HubAction = HubDownloadAndRunAction | HubStopAction | HubReplAction; diff --git a/src/actions/index.ts b/src/actions/index.ts index 82e55208..3edb50f4 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -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; diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx index 39904db5..838f65ec 100644 --- a/src/components/ActionButton.tsx +++ b/src/components/ActionButton.tsx @@ -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 { +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 { 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 extends React.Component> { +class ActionButton extends React.Component { render(): JSX.Element { return ( extends React.Component> >