diff --git a/src/actions.ts b/src/actions.ts index d055a233..83257588 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -3,6 +3,10 @@ import { Dispatch as ReduxDispatch } from 'redux'; import { AppAction } from './app/actions'; +import { + BlePybricksServiceAction, + BlePybricksServiceEventAction, +} from './ble-pybricks-service/actions'; import { BleUartAction } from './ble-uart/actions'; import { BLEAction, BLEConnectAction } from './ble/actions'; import { EditorAction } from './editor/actions'; @@ -29,6 +33,8 @@ export type Action = | AppAction | BLEAction | BLEConnectAction + | BlePybricksServiceAction + | BlePybricksServiceEventAction | BleUartAction | BootloaderConnectionAction | BootloaderDidRequestAction diff --git a/src/ble-pybricks-service/actions.ts b/src/ble-pybricks-service/actions.ts new file mode 100644 index 00000000..6447b7b8 --- /dev/null +++ b/src/ble-pybricks-service/actions.ts @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors +// actions/blePybricksService.ts: Actions for Bluetooth Low Energy Pybricks service + +import { Action } from 'redux'; + +/** + * BLE Pybricks service actions types. + */ +export enum BlePybricksServiceActionType { + /** + * Write command to control characteristic. + */ + WriteCommand = 'blePybricksService.action.writeCommand', + /** + * Writing command to control characteristic completed successfully. + */ + DidWriteCommand = 'blePybricksService.action.didWriteCommand', + /** + * Writing command to control characteristic failed. + */ + DidFailToWriteCommand = 'blePybricksService.action.didFailToWriteCommand', + /** + * Event notification was received from the control characteristic. + */ + DidNotifyEvent = 'blePybricksService.action.didNotifyEvent', +} + +/** + * Action that request to write a command to the Pybricks service control characteristic. + */ +export type BlePybricksServiceWriteCommandAction = Action & { + id: number; + value: Uint8Array; +}; + +/** + * Action that request to write a command to the Pybricks service control characteristic. + */ +export function writeCommand( + id: number, + value: Uint8Array, +): BlePybricksServiceWriteCommandAction { + return { + type: BlePybricksServiceActionType.WriteCommand, + id, + value, + }; +} + +/** + * Action that indicates sending a command to the Pybricks service control characteristic was successful. + */ +export type BlePybricksServiceDidWriteCommandAction = Action & { + id: number; +}; + +/** + * Action that indicates sending a command to the Pybricks service control characteristic was successful. + */ +export function didWriteCommand(id: number): BlePybricksServiceDidWriteCommandAction { + return { + type: BlePybricksServiceActionType.DidWriteCommand, + id, + }; +} + +/** + * Action that indicates sending a command to the Pybricks service control characteristic failed. + */ +export type BlePybricksServiceDidFailToWriteCommandAction = Action & { + id: number; + err: Error; +}; + +/** + * Action that indicates sending a command to the Pybricks service control characteristic failed. + */ +export function didFailToWriteCommand( + id: number, + err: Error, +): BlePybricksServiceDidFailToWriteCommandAction { + return { + type: BlePybricksServiceActionType.DidFailToWriteCommand, + id, + err, + }; +} + +/** + * Action that indicates an event notification was received on the Pybricks service control characteristic. + */ +export type BlePybricksServiceDidNotifyEventAction = Action & { + value: DataView; +}; + +/** + * Action that indicates an event notification was received on the Pybricks service control characteristic. + */ +export function didNotifyEvent( + value: DataView, +): BlePybricksServiceDidNotifyEventAction { + return { + type: BlePybricksServiceActionType.DidNotifyEvent, + value, + }; +} + +/** Common type for BLE Pybricks service actions. */ +export type BlePybricksServiceAction = + | BlePybricksServiceWriteCommandAction + | BlePybricksServiceDidWriteCommandAction + | BlePybricksServiceDidFailToWriteCommandAction + | BlePybricksServiceDidNotifyEventAction; + +/** Action types for events received from the Pybricks service control characteristic. */ +export enum BlePybricksServiceEventActionType { + /** A status report event. */ + StatusReport = 'blePybricksServiceEvent.action.statusReport', + /** A pseudo-event indicating there was a protocol error (not directly received from the hub). */ + ProtocolError = 'blePybricksServiceEvent.action.protocolError', +} + +/** + * Action that represents a status report event received from the hub. + */ +export type BlePybricksServiceEventStatusReportAction = Action & { + statusFlags: number; +}; + +/** + * Action that represents a status report event received from the hub. + * @param statusFlags The status flags. + */ +export function statusReportEvent( + statusFlags: number, +): BlePybricksServiceEventStatusReportAction { + return { type: BlePybricksServiceEventActionType.StatusReport, statusFlags }; +} + +/** + * Pseudo-event (not received from hub) indicating that there was a protocol error. + */ +export type BlePybricksServiceEventProtocolErrorAction = Action & { + err: Error; +}; + +/** + * Pseudo-event (not received from hub) indicating that there was a protocol error. + * @param err The error that was caught. + */ +export function eventProtocolError( + err: Error, +): BlePybricksServiceEventProtocolErrorAction { + return { type: BlePybricksServiceEventActionType.ProtocolError, err }; +} + +/** Common type for Pybricks control characteristic event actions. */ +export type BlePybricksServiceEventAction = + | BlePybricksServiceEventStatusReportAction + | BlePybricksServiceEventProtocolErrorAction; diff --git a/src/ble-pybricks-service/protocol.ts b/src/ble-pybricks-service/protocol.ts new file mode 100644 index 00000000..fbe5a617 --- /dev/null +++ b/src/ble-pybricks-service/protocol.ts @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020-2021 The Pybricks Authors +// +// Definitions related to the Pybricks Bluetooth low energy GATT service. + +import { assert } from '../utils'; + +/** Pybricks service UUID. */ +export const ServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef'; +/** Pybricks control characteristic UUID. */ +export const ControlCharacteristicUUID = 'c5f50002-8280-46da-89f4-6d8051e4aeef'; + +/** Events are notifications received from the hub. */ +export enum EventType { + /** Status report. Received when notifications are enabled and when status changes. */ + StatusReport = 0, +} + +/** Status indications received by Event.StatusReport */ +export enum Status { + /** Battery voltage is low. */ + BatteryLowVoltageWarning = 0, + /** Battery voltage is critically low. */ + BatteryLowVoltageShutdown = 1, + /** Battery current is too high. */ + BatteryHighCurrent = 2, + /** Bluetooth Low Energy is advertising/discoverable. */ + BLEAdvertising = 3, + /** Bluetooth Low Energy has low signal. */ + BLELowSignal = 4, + /** Power button is currently pressed. */ + PowerButtonPressed = 5, + /** User program is currently running. */ + UserProgramRunning = 6, +} + +/** Converts a Status enum value to a bit flag. */ +export function statusToFlag(status: Status): number { + return 1 << status; +} + +/** Gets the event type from a message. */ +export function getEventType(msg: DataView): EventType { + return msg.getUint8(0) as EventType; +} + +/** + * Parses the payload of a status report message. + * @param msg The raw message data. + * @returns The status as bit flags. + */ +export function parseStatusReport(msg: DataView): number { + assert(msg.getUint8(0) === EventType.StatusReport, 'expecting status report event'); + return msg.getUint32(1, true); +} + +/** + * Protocol error. Thrown e.g. when there is a malformed message. + */ +export class ProtocolError extends Error { + /** + * Creates a new ProtocolError. + * @param message The error message + * @param value The bytecodes that caused the error + */ + constructor(message: string, public value: DataView) { + super(message); + } +} diff --git a/src/ble-pybricks-service/sagas.ts b/src/ble-pybricks-service/sagas.ts new file mode 100644 index 00000000..e3baf6e4 --- /dev/null +++ b/src/ble-pybricks-service/sagas.ts @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors +// +// Handles Pybricks protocol. + +import { put, takeEvery } from 'typed-redux-saga/macro'; +import { hex } from '../utils'; +import { + BlePybricksServiceActionType, + BlePybricksServiceDidNotifyEventAction, + eventProtocolError, + statusReportEvent, +} from './actions'; +import { EventType, ProtocolError, getEventType, parseStatusReport } from './protocol'; + +/** + * Converts an incoming connection message to a response action. + * @param action The received response action. + */ +function* decodeResponse(action: BlePybricksServiceDidNotifyEventAction): Generator { + try { + const responseType = getEventType(action.value); + switch (responseType) { + case EventType.StatusReport: + yield* put(statusReportEvent(parseStatusReport(action.value))); + break; + default: + throw new ProtocolError( + `unknown pybricks event type: ${hex(responseType, 2)}`, + action.value, + ); + } + } catch (err) { + yield* put(eventProtocolError(err)); + } +} + +export default function* (): Generator { + yield* takeEvery(BlePybricksServiceActionType.DidNotifyEvent, decodeResponse); +} diff --git a/src/ble-pybricks/protocol.ts b/src/ble-pybricks/protocol.ts deleted file mode 100644 index 49baab26..00000000 --- a/src/ble-pybricks/protocol.ts +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors -// -// Definitions related to the Pybricks Bluetooth low energy GATT service. - -// Protocol details have not been defined yet. - -/** Pybricks Service UUID. */ -export const ServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef'; diff --git a/src/ble-uart/actions.ts b/src/ble-uart/actions.ts index 32eb07db..71e02f41 100644 --- a/src/ble-uart/actions.ts +++ b/src/ble-uart/actions.ts @@ -11,19 +11,19 @@ export enum BleUartActionType { /** * Write data. */ - Write = 'ble.data.action.write', + Write = 'bleUart.action.write', /** * Writing completed successfully. */ - DidWrite = 'ble.data.didWrite', + DidWrite = 'bleUart.didWrite', /** * Writing failed. */ - DidFailToWrite = 'ble.data.action.didFailToWrite', + DidFailToWrite = 'bleUart.action.didFailToWrite', /** * Notify that data was received. */ - Notify = 'ble.data.action.receive', + DidNotify = 'bleUart.action.didNotify', } export type BleUartWriteAction = Action & { @@ -52,12 +52,12 @@ export function didFailToWrite(id: number, err: Error): BleUartDidFailToWriteAct return { type: BleUartActionType.DidFailToWrite, id, err }; } -export type BleUartNotifyAction = Action & { +export type BleUartDidNotifyAction = Action & { value: DataView; }; -export function notify(value: DataView): BleUartNotifyAction { - return { type: BleUartActionType.Notify, value }; +export function didNotify(value: DataView): BleUartDidNotifyAction { + return { type: BleUartActionType.DidNotify, value }; } /** Common type for low-level BLE data actions. */ @@ -65,4 +65,4 @@ export type BleUartAction = | BleUartWriteAction | BleUartDidWriteAction | BleUartDidFailToWriteAction - | BleUartNotifyAction; + | BleUartDidNotifyAction; diff --git a/src/ble-uart/sagas.ts b/src/ble-uart/sagas.ts index 207cf767..c703016f 100644 --- a/src/ble-uart/sagas.ts +++ b/src/ble-uart/sagas.ts @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors // -// Manages connection to a Bluetooth Low Energy device with the Nordic (nRF) UART service. +// Manages connection to a Bluetooth Low Energy device running Pybricks firmware. + +// TODO: this file needs to be combined with the firmware BLE connection management +// to reduce duplicated code import { END, eventChannel } from 'redux-saga'; import { @@ -12,7 +15,16 @@ import { takeEvery, takeMaybe, } from 'typed-redux-saga/macro'; -import { ServiceUUID as pybricksServiceUUID } from '../ble-pybricks/protocol'; +import { + BlePybricksServiceActionType, + didFailToWriteCommand, + didNotifyEvent, + didWriteCommand, +} from '../ble-pybricks-service/actions'; +import { + ControlCharacteristicUUID as pybricksCommandCharacteristicUUID, + ServiceUUID as pybricksServiceUUID, +} from '../ble-pybricks-service/protocol'; import { BLEActionType, BleDeviceActionType as BLEDeviceActionType, @@ -31,14 +43,14 @@ import { RootState } from '../reducers'; import { BleUartActionType, BleUartWriteAction, - didFailToWrite, - didWrite, - notify, + didFailToWrite as didFailToWriteUart, + didNotify as didNotifyUart, + didWrite as didWriteUart, } from './actions'; import { + RxCharUUID as uartRxCharUUID, ServiceUUID as uartServiceUUID, TxCharUUID as uartTxCharUUID, - RxCharUUID as urtRxCharUUID, } from './protocol'; function disconnect( @@ -48,19 +60,35 @@ function disconnect( server.disconnect(); } -function* handleValueChanged(data: DataView): Generator { - yield* put(notify(data)); +function* handlePybricksControlValueChanged(data: DataView): Generator { + yield* put(didNotifyEvent(data)); } -function* write( - rxChar: BluetoothRemoteGATTCharacteristic, +function* writePybricksCommand( + char: BluetoothRemoteGATTCharacteristic, action: BleUartWriteAction, ): Generator { try { - yield* call(() => rxChar.writeValueWithoutResponse(action.value.buffer)); - yield* put(didWrite(action.id)); + yield* call(() => char.writeValueWithoutResponse(action.value.buffer)); + yield* put(didWriteCommand(action.id)); } catch (err) { - yield* put(didFailToWrite(action.id, err)); + yield* put(didFailToWriteCommand(action.id, err)); + } +} + +function* handleUartValueChanged(data: DataView): Generator { + yield* put(didNotifyUart(data)); +} + +function* writeUart( + char: BluetoothRemoteGATTCharacteristic, + action: BleUartWriteAction, +): Generator { + try { + yield* call(() => char.writeValueWithoutResponse(action.value.buffer)); + yield* put(didWriteUart(action.id)); + } catch (err) { + yield* put(didFailToWriteUart(action.id, err)); } } @@ -81,7 +109,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { device = yield* call(() => navigator.bluetooth.requestDevice({ filters: [{ services: [pybricksServiceUUID] }], - optionalServices: [uartServiceUUID], + optionalServices: [pybricksServiceUUID, uartServiceUUID], }), ); } catch (err) { @@ -117,15 +145,16 @@ function* connect(_action: BleDeviceConnectAction): Generator { yield* takeEvery(BLEDeviceActionType.Disconnect, disconnect, server); - let service: BluetoothRemoteGATTService; + let pybricksService: BluetoothRemoteGATTService; try { - service = yield* call([server, 'getPrimaryService'], uartServiceUUID); + pybricksService = yield* call( + [server, 'getPrimaryService'], + pybricksServiceUUID, + ); } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) { - // Possibly/probably caused by Chrome BlueZ back-end bug - // https://chromium-review.googlesource.com/c/chromium/src/+/2214098 yield* put(didFailToConnect({ reason: Reason.NoService })); } else { yield* put(didFailToConnect({ reason: Reason.Unknown, err })); @@ -133,9 +162,12 @@ function* connect(_action: BleDeviceConnectAction): Generator { return; } - let rxChar: BluetoothRemoteGATTCharacteristic; + let pybricksControlChar: BluetoothRemoteGATTCharacteristic; try { - rxChar = yield* call([service, 'getCharacteristic'], urtRxCharUUID); + pybricksControlChar = yield* call( + [pybricksService, 'getCharacteristic'], + pybricksCommandCharacteristicUUID, + ); } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); @@ -143,26 +175,19 @@ function* connect(_action: BleDeviceConnectAction): Generator { return; } - let txChar: BluetoothRemoteGATTCharacteristic; - try { - txChar = yield* call([service, 'getCharacteristic'], uartTxCharUUID); - } catch (err) { - server.disconnect(); - yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); - return; - } - - const txChannel = eventChannel((emitter) => { + const pybricksControlChannel = eventChannel((emitter) => { const listener = (): void => { - if (!txChar.value) { + if (!pybricksControlChar.value) { return; } - emitter(txChar.value); + emitter(pybricksControlChar.value); }; - txChar.addEventListener('characteristicvaluechanged', listener); + pybricksControlChar.addEventListener('characteristicvaluechanged', listener); return (): void => - txChar.removeEventListener('characteristicvaluechanged', listener); + pybricksControlChar.removeEventListener( + 'characteristicvaluechanged', + listener, + ); }); try { @@ -171,23 +196,98 @@ function* connect(_action: BleDeviceConnectAction): Generator { // and reconnecting unless we stop notifications before we start them // again. Wireshark shows that no enable notification descriptor write // is performed but notifications are received. - yield* call([txChar, 'stopNotifications']); - yield* call([txChar, 'startNotifications']); + yield* call([pybricksControlChar, 'stopNotifications']); + yield* call([pybricksControlChar, 'startNotifications']); } catch (err) { - txChannel.close(); + pybricksControlChannel.close(); server.disconnect(); yield* takeMaybe(disconnectChannel); yield* put(didFailToConnect({ reason: Reason.Unknown, err })); return; } - yield* takeEvery(txChannel, handleValueChanged); - yield* takeEvery(BleUartActionType.Write, write, rxChar); + let uartService: BluetoothRemoteGATTService; + try { + uartService = yield* call([server, 'getPrimaryService'], uartServiceUUID); + } catch (err) { + pybricksControlChannel.close(); + server.disconnect(); + yield* takeMaybe(disconnectChannel); + if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) { + yield* put(didFailToConnect({ reason: Reason.NoService })); + } else { + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + } + return; + } + + let uartRxChar: BluetoothRemoteGATTCharacteristic; + try { + uartRxChar = yield* call([uartService, 'getCharacteristic'], uartRxCharUUID); + } catch (err) { + pybricksControlChannel.close(); + server.disconnect(); + yield* takeMaybe(disconnectChannel); + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + return; + } + + let uartTxChar: BluetoothRemoteGATTCharacteristic; + try { + uartTxChar = yield* call([uartService, 'getCharacteristic'], uartTxCharUUID); + } catch (err) { + pybricksControlChannel.close(); + server.disconnect(); + yield* takeMaybe(disconnectChannel); + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + return; + } + + const uartTxChannel = eventChannel((emitter) => { + const listener = (): void => { + if (!uartTxChar.value) { + return; + } + emitter(uartTxChar.value); + }; + uartTxChar.addEventListener('characteristicvaluechanged', listener); + return (): void => + uartTxChar.removeEventListener('characteristicvaluechanged', listener); + }); + + try { + // REVISIT: possible Pybricks firmware bug (or chromium bug on Linux) + // where 'characteristicvaluechanged' is not called after disconnecting + // and reconnecting unless we stop notifications before we start them + // again. Wireshark shows that no enable notification descriptor write + // is performed but notifications are received. + yield* call([uartTxChar, 'stopNotifications']); + yield* call([uartTxChar, 'startNotifications']); + } catch (err) { + uartTxChannel.close(); + pybricksControlChannel.close(); + server.disconnect(); + yield* takeMaybe(disconnectChannel); + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + return; + } + + yield* takeEvery(pybricksControlChannel, handlePybricksControlValueChanged); + yield* takeEvery( + BlePybricksServiceActionType.WriteCommand, + writePybricksCommand, + pybricksControlChar, + ); + + yield* takeEvery(uartTxChannel, handleUartValueChanged); + yield* takeEvery(BleUartActionType.Write, writeUart, uartRxChar); yield* put(didConnect()); yield* takeMaybe(disconnectChannel); - txChannel.close(); + uartTxChannel.close(); + pybricksControlChannel.close(); + try { yield* cancel(); // have to cancel to stop forked effects } finally { diff --git a/src/error-log/sagas.ts b/src/error-log/sagas.ts index 259764be..c4e40ea3 100644 --- a/src/error-log/sagas.ts +++ b/src/error-log/sagas.ts @@ -1,7 +1,11 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors import { takeEvery } from 'typed-redux-saga/macro'; +import { + BlePybricksServiceEventActionType, + BlePybricksServiceEventProtocolErrorAction, +} from '../ble-pybricks-service/actions'; import { BleUartActionType, BleUartDidFailToWriteAction } from '../ble-uart/actions'; import { BleDeviceActionType, @@ -25,6 +29,12 @@ function bleDeviceDidFailToConnect(action: BleDeviceDidFailToConnectAction): voi } } +function pybricksProtocolError( + action: BlePybricksServiceEventProtocolErrorAction, +): void { + console.error(action.err); +} + function bleDataDidFailToWrite(action: BleUartDidFailToWriteAction): void { console.error(action.err); } @@ -47,6 +57,10 @@ function licenseDidFailToFetch(action: LicenseDidFailToFetchListAction): void { export default function* (): Generator { yield* takeEvery(BleDeviceActionType.DidFailToConnect, bleDeviceDidFailToConnect); + yield* takeEvery( + BlePybricksServiceEventActionType.ProtocolError, + pybricksProtocolError, + ); yield* takeEvery(BleUartActionType.DidFailToWrite, bleDataDidFailToWrite); yield* takeEvery( BootloaderConnectionActionType.DidFailToConnect, diff --git a/src/hub/reducers.ts b/src/hub/reducers.ts index 210ac87e..b0368005 100644 --- a/src/hub/reducers.ts +++ b/src/hub/reducers.ts @@ -1,8 +1,10 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors import { Reducer, combineReducers } from 'redux'; import { Action } from '../actions'; +import { BlePybricksServiceEventActionType } from '../ble-pybricks-service/actions'; +import { Status, statusToFlag } from '../ble-pybricks-service/protocol'; import { BleDeviceActionType } from '../ble/actions'; import { HubMessageActionType, HubRuntimeStatusType } from './actions'; @@ -65,6 +67,10 @@ const runtime: Reducer = ( console.error(`bad action/state: ${action.newStatus}`); return state; } + case BlePybricksServiceEventActionType.StatusReport: + return action.statusFlags & statusToFlag(Status.UserProgramRunning) + ? HubRuntimeState.Running + : HubRuntimeState.Idle; default: return state; } diff --git a/src/sagas.ts b/src/sagas.ts index 1f30f024..2ccf8d5b 100644 --- a/src/sagas.ts +++ b/src/sagas.ts @@ -4,6 +4,7 @@ import { all, put } from 'typed-redux-saga/macro'; import { didStart } from './app/actions'; import app from './app/sagas'; +import blePybricksService from './ble-pybricks-service/sagas'; import bleUart from './ble-uart/sagas'; import editor from './editor/sagas'; import errorLog from './error-log/sagas'; @@ -21,6 +22,7 @@ import terminal from './terminal/sagas'; export default function* (): Generator { yield* all([ app(), + blePybricksService(), bleUart(), lwp3BootloaderBle(), lwp3BootloaderProtocol(), diff --git a/src/terminal/sagas.test.ts b/src/terminal/sagas.test.ts index 06ed88ec..9ef0953c 100644 --- a/src/terminal/sagas.test.ts +++ b/src/terminal/sagas.test.ts @@ -7,15 +7,10 @@ import { BleUartActionType, BleUartWriteAction, didFailToWrite, + didNotify, didWrite, - notify, } from '../ble-uart/actions'; -import { - HubChecksumMessageAction, - HubMessageActionType, - HubRuntimeStatusMessageAction, - HubRuntimeStatusType, -} from '../hub/actions'; +import { HubChecksumMessageAction, HubMessageActionType } from '../hub/actions'; import { HubRuntimeState } from '../hub/reducers'; import { createCountFunc } from '../utils/iter'; import { @@ -35,7 +30,7 @@ describe('Data receiver filters out hub status', () => { ); // sending ASCII space character - saga.put(notify(new DataView(new Uint8Array([0x20]).buffer))); + saga.put(didNotify(new DataView(new Uint8Array([0x20]).buffer))); const action = await saga.take(); expect(action.type).toBe(TerminalActionType.SendData); @@ -51,7 +46,7 @@ describe('Data receiver filters out hub status', () => { { nextMessageId: createCountFunc() }, ); - saga.put(notify(new DataView(new Uint8Array([0xaa]).buffer))); + saga.put(didNotify(new DataView(new Uint8Array([0xaa]).buffer))); const action = await saga.take(); expect(action.type).toBe(HubMessageActionType.Checksum); @@ -59,260 +54,6 @@ describe('Data receiver filters out hub status', () => { await saga.end(); }); - - test('idle message', async () => { - const saga = new AsyncSaga( - terminal, - { hub: { runtime: HubRuntimeState.Unknown } }, - { nextMessageId: createCountFunc() }, - ); - - // '>>>> IDLE' - saga.put( - notify( - new DataView( - new Uint8Array([ - 0x3e, - 0x3e, - 0x3e, - 0x3e, - 0x20, - 0x49, - 0x44, - 0x4c, - 0x45, - ]).buffer, - ), - ), - ); - - const action = await saga.take(); - expect(action.type).toBe(HubMessageActionType.RuntimeStatus); - expect((action as HubRuntimeStatusMessageAction).newStatus).toBe( - HubRuntimeStatusType.Idle, - ); - - await saga.end(); - }); - - test('idle message with extra text', async () => { - const saga = new AsyncSaga( - terminal, - { hub: { runtime: HubRuntimeState.Unknown } }, - { nextMessageId: createCountFunc() }, - ); - - // '0>>>> IDLE1' - saga.put( - notify( - new DataView( - new Uint8Array([ - 0x30, - 0x3e, - 0x3e, - 0x3e, - 0x3e, - 0x20, - 0x49, - 0x44, - 0x4c, - 0x45, - 0x31, - ]).buffer, - ), - ), - ); - - // this should get split into '0', idle status, '1' - - const action1 = await saga.take(); - expect(action1.type).toBe(TerminalActionType.SendData); - expect((action1 as TerminalDataSendDataAction).value).toBe('0'); - - const action2 = await saga.take(); - expect(action2.type).toBe(HubMessageActionType.RuntimeStatus); - expect((action2 as HubRuntimeStatusMessageAction).newStatus).toBe( - HubRuntimeStatusType.Idle, - ); - - const action3 = await saga.take(); - expect(action3.type).toBe(TerminalActionType.SendData); - expect((action3 as TerminalDataSendDataAction).value).toBe('1'); - - await saga.end(); - }); - - test('error message', async () => { - const saga = new AsyncSaga( - terminal, - { hub: { runtime: HubRuntimeState.Unknown } }, - { nextMessageId: createCountFunc() }, - ); - - // '>>>> ERROR' - saga.put( - notify( - new DataView( - new Uint8Array([ - 0x3e, - 0x3e, - 0x3e, - 0x3e, - 0x20, - 0x45, - 0x52, - 0x52, - 0x4f, - 0x52, - ]).buffer, - ), - ), - ); - - const action = await saga.take(); - expect(action.type).toBe(HubMessageActionType.RuntimeStatus); - expect((action as HubRuntimeStatusMessageAction).newStatus).toBe( - HubRuntimeStatusType.Error, - ); - - await saga.end(); - }); - - test('error message with extra text', async () => { - const saga = new AsyncSaga( - terminal, - { hub: { runtime: HubRuntimeState.Unknown } }, - { nextMessageId: createCountFunc() }, - ); - - // '0>>>> ERROR1' - saga.put( - notify( - new DataView( - new Uint8Array([ - 0x30, - 0x3e, - 0x3e, - 0x3e, - 0x3e, - 0x20, - 0x45, - 0x52, - 0x52, - 0x4f, - 0x52, - 0x31, - ]).buffer, - ), - ), - ); - - // this should get split into '0', error status, '1' - - const action1 = await saga.take(); - expect(action1.type).toBe(TerminalActionType.SendData); - expect((action1 as TerminalDataSendDataAction).value).toBe('0'); - - const action2 = await saga.take(); - expect(action2.type).toBe(HubMessageActionType.RuntimeStatus); - expect((action2 as HubRuntimeStatusMessageAction).newStatus).toBe( - HubRuntimeStatusType.Error, - ); - - const action3 = await saga.take(); - expect(action3.type).toBe(TerminalActionType.SendData); - expect((action3 as TerminalDataSendDataAction).value).toBe('1'); - - await saga.end(); - }); - - test('running message', async () => { - const saga = new AsyncSaga( - terminal, - { hub: { runtime: HubRuntimeState.Unknown } }, - { nextMessageId: createCountFunc() }, - ); - - // '>>>> ERROR' - saga.put( - notify( - new DataView( - new Uint8Array([ - 0x3e, - 0x3e, - 0x3e, - 0x3e, - 0x20, - 0x52, - 0x55, - 0x4e, - 0x4e, - 0x49, - 0x4e, - 0x47, - ]).buffer, - ), - ), - ); - - const action = await saga.take(); - expect(action.type).toBe(HubMessageActionType.RuntimeStatus); - expect((action as HubRuntimeStatusMessageAction).newStatus).toBe( - HubRuntimeStatusType.Running, - ); - - await saga.end(); - }); - - test('running message with extra text', async () => { - const saga = new AsyncSaga( - terminal, - { hub: { runtime: HubRuntimeState.Unknown } }, - { nextMessageId: createCountFunc() }, - ); - - // '0>>>> RUNNING1' - saga.put( - notify( - new DataView( - new Uint8Array([ - 0x30, - 0x3e, - 0x3e, - 0x3e, - 0x3e, - 0x20, - 0x52, - 0x55, - 0x4e, - 0x4e, - 0x49, - 0x4e, - 0x47, - 0x31, - ]).buffer, - ), - ), - ); - - // this should get split into '0', running status, '1' - - const action1 = await saga.take(); - expect(action1.type).toBe(TerminalActionType.SendData); - expect((action1 as TerminalDataSendDataAction).value).toBe('0'); - - const action2 = await saga.take(); - expect(action2.type).toBe(HubMessageActionType.RuntimeStatus); - expect((action2 as HubRuntimeStatusMessageAction).newStatus).toBe( - HubRuntimeStatusType.Running, - ); - - const action3 = await saga.take(); - expect(action3.type).toBe(TerminalActionType.SendData); - expect((action3 as TerminalDataSendDataAction).value).toBe('1'); - - await saga.end(); - }); }); test('Terminal data source responds to send data actions', async () => { diff --git a/src/terminal/sagas.ts b/src/terminal/sagas.ts index af1a9663..c3722ffc 100644 --- a/src/terminal/sagas.ts +++ b/src/terminal/sagas.ts @@ -13,9 +13,9 @@ import { takeEvery, } from 'typed-redux-saga/macro'; import { Action } from '../actions'; -import { BleUartActionType, BleUartNotifyAction, write } from '../ble-uart/actions'; +import { BleUartActionType, BleUartDidNotifyAction, write } from '../ble-uart/actions'; import { SafeTxCharLength } from '../ble-uart/protocol'; -import { HubRuntimeStatusType, checksum, updateStatus } from '../hub/actions'; +import { checksum } from '../hub/actions'; import { HubRuntimeState } from '../hub/reducers'; import { RootState } from '../reducers'; import { defined } from '../utils'; @@ -25,28 +25,7 @@ import { TerminalActionType, TerminalDataReceiveDataAction, sendData } from './a const encoder = new TextEncoder(); const decoder = new TextDecoder(); -function* handleMatch( - match: RegExpMatchArray | null, - status: HubRuntimeStatusType, -): Generator { - if (!match) { - return false; - } - - if (match[1]) { - yield* put(sendData(match[1])); - } - - yield* put(updateStatus(status)); - - if (match[2]) { - yield* put(sendData(match[2])); - } - - return true; -} - -function* receiveUartData(action: BleUartNotifyAction): Generator { +function* receiveUartData(action: BleUartDidNotifyAction): Generator { const hubState = yield* select((s: RootState) => s.hub.runtime); if (hubState === HubRuntimeState.Loading && action.value.buffer.byteLength === 1) { @@ -56,31 +35,6 @@ function* receiveUartData(action: BleUartNotifyAction): Generator { } const value = decoder.decode(action.value.buffer); - - if ( - yield* handleMatch(value.match(/(.*)>>>> IDLE(.*)/), HubRuntimeStatusType.Idle) - ) { - return; - } - - if ( - yield* handleMatch( - value.match(/(.*)>>>> ERROR(.*)/), - HubRuntimeStatusType.Error, - ) - ) { - return; - } - - if ( - yield* handleMatch( - value.match(/(.*)>>>> RUNNING(.*)/), - HubRuntimeStatusType.Running, - ) - ) { - return; - } - yield* put(sendData(value)); } @@ -123,7 +77,7 @@ function* receiveTerminalData(): Generator { ); // wait for echo so tht we don't overrun the hub with messages - yield* race([take(BleUartActionType.Notify), delay(100)]); + yield* race([take(BleUartActionType.DidNotify), delay(100)]); } } } @@ -135,7 +89,7 @@ function* sendTerminalData(action: TerminalDataReceiveDataAction): Generator { } export default function* (): Generator { - yield* takeEvery(BleUartActionType.Notify, receiveUartData); + yield* takeEvery(BleUartActionType.DidNotify, receiveUartData); yield* fork(receiveTerminalData); yield* takeEvery(TerminalActionType.SendData, sendTerminalData); }