implement BLE Pybricks service status notifications

This is the beginning of the BLE Pybricks service implementation, so
changes are made to get a handle to the Pybricks control characteristic
and enable notifications.

Currently, only one event (the status report event) and no commands are
implemented on this characteristic.

The new status report event replaces monitoring the UART for >>>>
strings to get the current hub state.

Also includes some minor cleanup in the ble-uart/actions for consistency.
This commit is contained in:
David Lechner
2021-03-10 10:30:15 -06:00
parent 257b843f38
commit 61c34fc4b1
12 changed files with 459 additions and 375 deletions
+6
View File
@@ -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
+161
View File
@@ -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<BlePybricksServiceActionType.WriteCommand> & {
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<BlePybricksServiceActionType.DidWriteCommand> & {
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<BlePybricksServiceActionType.DidFailToWriteCommand> & {
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<BlePybricksServiceActionType.DidNotifyEvent> & {
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<BlePybricksServiceEventActionType.StatusReport> & {
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<BlePybricksServiceEventActionType.ProtocolError> & {
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;
+69
View File
@@ -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);
}
}
+40
View File
@@ -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);
}
-9
View File
@@ -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';
+8 -8
View File
@@ -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<BleUartActionType.Write> & {
@@ -52,12 +52,12 @@ export function didFailToWrite(id: number, err: Error): BleUartDidFailToWriteAct
return { type: BleUartActionType.DidFailToWrite, id, err };
}
export type BleUartNotifyAction = Action<BleUartActionType.Notify> & {
export type BleUartDidNotifyAction = Action<BleUartActionType.DidNotify> & {
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;
+142 -42
View File
@@ -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<DataView>((emitter) => {
const pybricksControlChannel = eventChannel<DataView>((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<DataView>((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 {
+15 -1
View File
@@ -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,
+7 -1
View File
@@ -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<HubRuntimeState, Action> = (
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;
}
+2
View File
@@ -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(),
+4 -263
View File
@@ -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 () => {
+5 -51
View File
@@ -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<unknown, boolean> {
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);
}