implement Pybricks service stop command

This commit is contained in:
David Lechner
2021-03-10 10:30:15 -06:00
parent 61c34fc4b1
commit 430147487e
6 changed files with 194 additions and 10 deletions
+2
View File
@@ -5,6 +5,7 @@ import { Dispatch as ReduxDispatch } from 'redux';
import { AppAction } from './app/actions';
import {
BlePybricksServiceAction,
BlePybricksServiceCommandAction,
BlePybricksServiceEventAction,
} from './ble-pybricks-service/actions';
import { BleUartAction } from './ble-uart/actions';
@@ -34,6 +35,7 @@ export type Action =
| BLEAction
| BLEConnectAction
| BlePybricksServiceAction
| BlePybricksServiceCommandAction
| BlePybricksServiceEventAction
| BleUartAction
| BootloaderConnectionAction
+67
View File
@@ -113,6 +113,73 @@ export type BlePybricksServiceAction =
| BlePybricksServiceDidFailToWriteCommandAction
| BlePybricksServiceDidNotifyEventAction;
/** Action types for commands sent via the Pybricks service control characteristic. */
export enum BlePybricksServiceCommandActionType {
SendStopUserProgram = 'blePybricksServiceCommand.action.sendStopUserProgram',
DidSend = 'blePybricksServiceCommand.action.didSend',
DidFailToSend = 'blePybricksServiceCommand.action.didFailToSend',
}
type TransactionId = {
/** Unique identifier for the transaction set in the "send" command. */
id: number;
};
/** Action that requests a stop user program to be sent. */
export type BlePybricksServiceCommandSendStopUserProgram = Action<BlePybricksServiceCommandActionType.SendStopUserProgram> &
TransactionId;
/**
* Action that requests a stop user program to be sent.
* @param id Unique identifier for this transaction.
*/
export function sendStopUserProgramCommand(
id: number,
): BlePybricksServiceCommandSendStopUserProgram {
return { type: BlePybricksServiceCommandActionType.SendStopUserProgram, id };
}
/**
* Action that indicates that a command was successfully sent.
*/
export type BlePybricksServiceCommandDidSendAction = Action<BlePybricksServiceCommandActionType.DidSend> &
TransactionId;
/**
* Action that indicates that a command was successfully sent.
* @param id Unique identifier for the transaction from the corresponding "send" command.
*/
export function didSendCommand(id: number): BlePybricksServiceCommandDidSendAction {
return { type: BlePybricksServiceCommandActionType.DidSend, id };
}
/**
* Action that indicates that a command was not sent.
*/
export type BlePybricksServiceCommandDidFailToSendAction = Action<BlePybricksServiceCommandActionType.DidFailToSend> &
TransactionId & {
/** The error that was raised. */
err: Error;
};
/**
* Action that indicates that a command was not sent.
* @param id Unique identifier for the transaction from the corresponding "send" command.
* @param err The error that was raised.
*/
export function didFailToSendCommand(
id: number,
err: Error,
): BlePybricksServiceCommandDidFailToSendAction {
return { type: BlePybricksServiceCommandActionType.DidFailToSend, id, err };
}
/** Common type for Pybricks control characteristic send command actions. */
export type BlePybricksServiceCommandAction =
| BlePybricksServiceCommandSendStopUserProgram
| BlePybricksServiceCommandDidSendAction
| BlePybricksServiceCommandDidFailToSendAction;
/** Action types for events received from the Pybricks service control characteristic. */
export enum BlePybricksServiceEventActionType {
/** A status report event. */
+15
View File
@@ -10,6 +10,21 @@ export const ServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef';
/** Pybricks control characteristic UUID. */
export const ControlCharacteristicUUID = 'c5f50002-8280-46da-89f4-6d8051e4aeef';
/** Commands are instructions sent to the hub. */
export enum CommandType {
/** Request to stop the user program, if it is running. */
StopUserProgram = 0,
}
/**
* Creates a stop user program command message.
*/
export function createStopUserProgramCommand(): Uint8Array {
const msg = new Uint8Array(1);
msg[0] = CommandType.StopUserProgram;
return msg;
}
/** Events are notifications received from the hub. */
export enum EventType {
/** Status report. Received when notifications are enabled and when status changes. */
+67 -2
View File
@@ -3,15 +3,79 @@
//
// Handles Pybricks protocol.
import { put, takeEvery } from 'typed-redux-saga/macro';
import {
actionChannel,
fork,
put,
race,
take,
takeEvery,
} from 'typed-redux-saga/macro';
import { Action } from '../actions';
import { hex } from '../utils';
import {
BlePybricksServiceActionType,
BlePybricksServiceCommandAction,
BlePybricksServiceCommandActionType,
BlePybricksServiceDidFailToWriteCommandAction,
BlePybricksServiceDidNotifyEventAction,
BlePybricksServiceDidWriteCommandAction,
didFailToSendCommand,
didSendCommand,
eventProtocolError,
statusReportEvent,
writeCommand,
} from './actions';
import { EventType, ProtocolError, getEventType, parseStatusReport } from './protocol';
import {
EventType,
ProtocolError,
createStopUserProgramCommand,
getEventType,
parseStatusReport,
} from './protocol';
/**
* Converts a request action into bytecodes and creates a new action to send
* the bytecodes to to the device.
*/
function* encodeRequest(): Generator {
// Using a while loop to serialize sending data to avoid "busy" errors.
const chan = yield* actionChannel<BlePybricksServiceCommandAction>((a: Action) =>
Object.values(BlePybricksServiceCommandActionType).includes(
a.type as BlePybricksServiceCommandActionType,
),
);
while (true) {
const action = yield* take(chan);
switch (action.type) {
case BlePybricksServiceCommandActionType.SendStopUserProgram:
yield* put(writeCommand(action.id, createStopUserProgramCommand()));
break;
/* istanbul ignore next: should not be possible to reach */
default:
console.error(`Unknown Pybricks service command ${action}`);
continue;
}
const { failedToSend } = yield* race({
sent: take<BlePybricksServiceDidWriteCommandAction>(
BlePybricksServiceActionType.DidWriteCommand,
),
failedToSend: take<BlePybricksServiceDidFailToWriteCommandAction>(
BlePybricksServiceActionType.DidFailToWriteCommand,
),
});
if (failedToSend) {
yield* put(didFailToSendCommand(action.id, failedToSend.err));
} else {
yield* put(didSendCommand(action.id));
}
}
}
/**
* Converts an incoming connection message to a response action.
@@ -36,5 +100,6 @@ function* decodeResponse(action: BlePybricksServiceDidNotifyEventAction): Genera
}
export default function* (): Generator {
yield* fork(encodeRequest);
yield* takeEvery(BlePybricksServiceActionType.DidNotifyEvent, decodeResponse);
}
+17 -4
View File
@@ -4,6 +4,11 @@
import { Ace } from 'ace-builds';
import { mock } from 'jest-mock-extended';
import { AsyncSaga } from '../../test';
import {
BlePybricksServiceCommandActionType,
BlePybricksServiceCommandSendStopUserProgram,
didSendCommand,
} from '../ble-pybricks-service/actions';
import { BleUartActionType, BleUartWriteAction, didWrite } from '../ble-uart/actions';
import { MpyActionType, didCompile } from '../mpy/actions';
import { createCountFunc } from '../utils/iter';
@@ -82,8 +87,8 @@ test('repl', async () => {
saga.put(repl());
const compileAction = await saga.take();
expect(compileAction.type).toBe(BleUartActionType.Write);
const action = await saga.take();
expect(action.type).toBe(BleUartActionType.Write);
await saga.end();
});
@@ -93,8 +98,16 @@ test('stop', async () => {
saga.put(stop());
const compileAction = await saga.take();
expect(compileAction.type).toBe(BleUartActionType.Write);
const pybricksServiceAction = await saga.take();
expect(pybricksServiceAction.type).toBe(
BlePybricksServiceCommandActionType.SendStopUserProgram,
);
saga.put(
didSendCommand(
(pybricksServiceAction as BlePybricksServiceCommandSendStopUserProgram).id,
),
);
await saga.end();
});
+26 -4
View File
@@ -12,6 +12,12 @@ import {
takeEvery,
} from 'typed-redux-saga/macro';
import { Action } from '../actions';
import {
BlePybricksServiceCommandActionType,
BlePybricksServiceCommandDidFailToSendAction,
BlePybricksServiceCommandDidSendAction,
sendStopUserProgramCommand,
} from '../ble-pybricks-service/actions';
import {
BleUartActionType,
BleUartDidFailToWriteAction,
@@ -151,12 +157,28 @@ function* startRepl(_action: HubReplAction): Generator {
yield* put(write(nextMessageId(), startReplCommand));
}
// CTRL+C, CTRL+C, CTRL+D
const stopCommand = new Uint8Array([0x03, 0x03, 0x04]);
function* stop(_action: HubStopAction): Generator {
const nextMessageId = yield* getContext<() => number>('nextMessageId');
yield* put(write(nextMessageId(), stopCommand));
const id = nextMessageId();
yield* put(sendStopUserProgramCommand(id));
// REVISIT: may want to disable button while attempting to send command
// this would mean didSendStop() and didFailToSendStop() actions here
const { failedToSend } = yield* race({
sent: take<BlePybricksServiceCommandDidSendAction>(
(a: Action) =>
a.type === BlePybricksServiceCommandActionType.DidSend && a.id === id,
),
failedToSend: take<BlePybricksServiceCommandDidFailToSendAction>(
(a: Action) =>
a.type === BlePybricksServiceCommandActionType.DidFailToSend &&
a.id === id,
),
});
if (failedToSend) {
// TODO: probably want to check error. If hub disconnected, ignore error
// otherwise indicate error to user
console.error(failedToSend.err);
}
}
export default function* (): Generator {