mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
implement Pybricks service stop command
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user