mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
add support for Pybricks Profile v1.3.0
This commit is contained in:
committed by
David Lechner
parent
63a1026dae
commit
337209744c
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
// Copyright (c) 2021-2023 The Pybricks Authors
|
||||
//
|
||||
// Actions for Bluetooth Low Energy Pybricks service
|
||||
|
||||
@@ -109,6 +109,21 @@ export const sendWriteUserRamCommand = createAction(
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Action that requests to write to stdin.
|
||||
* @param id Unique identifier for this transaction.
|
||||
* @param payload The bytes to write.
|
||||
*
|
||||
* @since Pybricks Profile v1.3.0.
|
||||
*/
|
||||
export const sendWriteStdinCommand = createAction(
|
||||
(id: number, payload: ArrayBuffer) => ({
|
||||
type: 'blePybricksServiceCommand.action.sendWriteStdinCommand',
|
||||
id,
|
||||
payload,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Action that indicates that a command was successfully sent.
|
||||
* @param id Unique identifier for the transaction from the corresponding "send" command.
|
||||
@@ -140,6 +155,17 @@ export const didReceiveStatusReport = createAction((statusFlags: number) => ({
|
||||
statusFlags,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Action that represents a status report event received from the hub.
|
||||
* @param statusFlags The status flags.
|
||||
*
|
||||
* @since Pybricks Profile v1.3.0
|
||||
*/
|
||||
export const didReceiveWriteStdout = createAction((payload: ArrayBuffer) => ({
|
||||
type: 'blePybricksServiceEvent.action.didReceiveWriteStdout',
|
||||
payload,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Pseudo-event = actionCreator((not received from hub) indicating that there was a protocol error.
|
||||
* @param error The error that was caught.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
//
|
||||
// Definitions related to the Pybricks Bluetooth low energy GATT service.
|
||||
|
||||
@@ -52,6 +52,12 @@ export enum CommandType {
|
||||
* @since Pybricks Profile v1.2.0
|
||||
*/
|
||||
ResetInUpdateMode = 5,
|
||||
/**
|
||||
* Request to write data to stdin.
|
||||
*
|
||||
* @since Pybricks Profile v1.3.0
|
||||
*/
|
||||
WriteStdin = 6,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +126,20 @@ export function createWriteUserRamCommand(
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CommandType.WriteStdin} message.
|
||||
* @param payload The bytes to write.
|
||||
*
|
||||
* @since Pybricks Profile v1.3.0.
|
||||
*/
|
||||
export function createWriteStdinCommand(payload: ArrayBuffer): Uint8Array {
|
||||
const msg = new Uint8Array(1 + payload.byteLength);
|
||||
const view = new DataView(msg.buffer);
|
||||
view.setUint8(0, CommandType.WriteStdin);
|
||||
msg.set(new Uint8Array(payload), 1);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/** Events are notifications received from the hub. */
|
||||
export enum EventType {
|
||||
/**
|
||||
@@ -130,6 +150,12 @@ export enum EventType {
|
||||
* @since Pybricks Profile v1.0.0
|
||||
*/
|
||||
StatusReport = 0,
|
||||
/**
|
||||
* Hub wrote to stdout event.
|
||||
*
|
||||
* @since Pybricks Profile v1.3.0
|
||||
*/
|
||||
WriteStdout = 1,
|
||||
}
|
||||
|
||||
/** Status indications received by Event.StatusReport */
|
||||
@@ -206,6 +232,18 @@ export function parseStatusReport(msg: DataView): number {
|
||||
return msg.getUint32(1, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the payload of a write stdout.
|
||||
* @param msg The raw message data.
|
||||
* @returns The bytes that were written.
|
||||
*
|
||||
* @since Pybricks Profile v1.3.0
|
||||
*/
|
||||
export function parseWriteStdout(msg: DataView): ArrayBuffer {
|
||||
assert(msg.getUint8(0) === EventType.WriteStdout, 'expecting write stdout event');
|
||||
return msg.buffer.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Protocol error. Thrown e.g. when there is a malformed message.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
// Copyright (c) 2021-2023 The Pybricks Authors
|
||||
|
||||
import { AsyncSaga } from '../../test';
|
||||
import {
|
||||
@@ -7,10 +7,16 @@ import {
|
||||
didFailToWriteCommand,
|
||||
didNotifyEvent,
|
||||
didReceiveStatusReport,
|
||||
didReceiveWriteStdout,
|
||||
didSendCommand,
|
||||
didWriteCommand,
|
||||
eventProtocolError,
|
||||
sendStartReplCommand,
|
||||
sendStartUserProgramCommand,
|
||||
sendStopUserProgramCommand,
|
||||
sendWriteStdinCommand,
|
||||
sendWriteUserProgramMetaCommand,
|
||||
sendWriteUserRamCommand,
|
||||
writeCommand,
|
||||
} from './actions';
|
||||
import { CommandType, ProtocolError } from './protocol';
|
||||
@@ -25,6 +31,57 @@ describe('command encoder', () => {
|
||||
0x00, // stop user program command
|
||||
],
|
||||
],
|
||||
[
|
||||
'start user program',
|
||||
sendStartUserProgramCommand(0),
|
||||
[
|
||||
0x01, // start user program command
|
||||
],
|
||||
],
|
||||
[
|
||||
'start repl',
|
||||
sendStartReplCommand(0),
|
||||
[
|
||||
0x02, // start repl command
|
||||
],
|
||||
],
|
||||
[
|
||||
'write user program meta',
|
||||
sendWriteUserProgramMetaCommand(0, 100),
|
||||
[
|
||||
0x03, // write user program meta command
|
||||
0x64, // program size LSB
|
||||
0x00,
|
||||
0x00,
|
||||
0x00, // program size MSB
|
||||
],
|
||||
],
|
||||
[
|
||||
'write user ram',
|
||||
sendWriteUserRamCommand(0, 100, new Uint8Array([1, 2, 3, 4]).buffer),
|
||||
[
|
||||
0x04, // write user ram command
|
||||
0x64, // offset size LSB
|
||||
0x00,
|
||||
0x00,
|
||||
0x00, // offset size MSB
|
||||
0x01, // payload start
|
||||
0x02,
|
||||
0x03,
|
||||
0x04, // payload end
|
||||
],
|
||||
],
|
||||
[
|
||||
'write stdin',
|
||||
sendWriteStdinCommand(0, new Uint8Array([1, 2, 3, 4]).buffer),
|
||||
[
|
||||
0x06, // write stdin command
|
||||
0x01, // payload start
|
||||
0x02,
|
||||
0x03,
|
||||
0x04, // payload end
|
||||
],
|
||||
],
|
||||
])('encode %s request', async (_n, request, expected) => {
|
||||
const saga = new AsyncSaga(blePybricksService);
|
||||
saga.put(request);
|
||||
@@ -103,6 +160,24 @@ describe('event decoder', () => {
|
||||
],
|
||||
didReceiveStatusReport(0x00000001),
|
||||
],
|
||||
[
|
||||
'write stdout',
|
||||
[
|
||||
0x01, // write stdout event
|
||||
't'.charCodeAt(0), //payload
|
||||
'e'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
],
|
||||
didReceiveWriteStdout(
|
||||
new Uint8Array([
|
||||
't'.charCodeAt(0),
|
||||
'e'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
]).buffer,
|
||||
),
|
||||
],
|
||||
])('decode %s event', async (_n, message, expected) => {
|
||||
const saga = new AsyncSaga(blePybricksService);
|
||||
const notification = new Uint8Array(message);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
// Copyright (c) 2021-2023 The Pybricks Authors
|
||||
//
|
||||
// Handles Pybricks protocol.
|
||||
|
||||
@@ -18,12 +18,14 @@ import {
|
||||
didFailToWriteCommand,
|
||||
didNotifyEvent,
|
||||
didReceiveStatusReport,
|
||||
didReceiveWriteStdout,
|
||||
didSendCommand,
|
||||
didWriteCommand,
|
||||
eventProtocolError,
|
||||
sendStartReplCommand,
|
||||
sendStartUserProgramCommand,
|
||||
sendStopUserProgramCommand,
|
||||
sendWriteStdinCommand,
|
||||
sendWriteUserProgramMetaCommand,
|
||||
sendWriteUserRamCommand,
|
||||
writeCommand,
|
||||
@@ -34,10 +36,12 @@ import {
|
||||
createStartReplCommand,
|
||||
createStartUserProgramCommand,
|
||||
createStopUserProgramCommand,
|
||||
createWriteStdinCommand,
|
||||
createWriteUserProgramMetaCommand,
|
||||
createWriteUserRamCommand,
|
||||
getEventType,
|
||||
parseStatusReport,
|
||||
parseWriteStdout,
|
||||
} from './protocol';
|
||||
|
||||
/**
|
||||
@@ -45,7 +49,7 @@ import {
|
||||
* the bytecodes to to the device.
|
||||
*/
|
||||
function* encodeRequest(): Generator {
|
||||
// Using a while loop to serialize sending data to avoid "busy" errors.
|
||||
// Using a loop to serialize sending data to avoid "busy" errors.
|
||||
|
||||
const chan = yield* actionChannel(
|
||||
(a: AnyAction) =>
|
||||
@@ -53,7 +57,7 @@ function* encodeRequest(): Generator {
|
||||
a.type.startsWith('blePybricksServiceCommand.action.send'),
|
||||
);
|
||||
|
||||
while (true) {
|
||||
for (;;) {
|
||||
const action = yield* take(chan);
|
||||
|
||||
/* istanbul ignore else: should not be possible to reach */
|
||||
@@ -74,6 +78,10 @@ function* encodeRequest(): Generator {
|
||||
createWriteUserRamCommand(action.offset, action.payload),
|
||||
),
|
||||
);
|
||||
} else if (sendWriteStdinCommand.matches(action)) {
|
||||
yield* put(
|
||||
writeCommand(action.id, createWriteStdinCommand(action.payload)),
|
||||
);
|
||||
} else {
|
||||
console.error(`Unknown Pybricks service command ${action.type}`);
|
||||
continue;
|
||||
@@ -103,6 +111,9 @@ function* decodeResponse(action: ReturnType<typeof didNotifyEvent>): Generator {
|
||||
case EventType.StatusReport:
|
||||
yield* put(didReceiveStatusReport(parseStatusReport(action.value)));
|
||||
break;
|
||||
case EventType.WriteStdout:
|
||||
yield* put(didReceiveWriteStdout(parseWriteStdout(action.value)));
|
||||
break;
|
||||
default:
|
||||
throw new ProtocolError(
|
||||
`unknown pybricks event type: ${hex(responseType, 2)}`,
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
bleDidDisconnectPybricks,
|
||||
bleDisconnectPybricks,
|
||||
} from '../ble/actions';
|
||||
import { didReceiveStatusReport } from '../ble-pybricks-service/actions';
|
||||
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
|
||||
import { bleDIServiceDidReceiveSoftwareRevision } from '../ble-device-info-service/actions';
|
||||
import { PnpId } from '../ble-device-info-service/protocol';
|
||||
import { HubType } from '../ble-lwp3-service/protocol';
|
||||
import {
|
||||
@@ -48,6 +47,7 @@ test('initial state', () => {
|
||||
"preferredFileFormat": null,
|
||||
"runtime": "hub.runtime.disconnected",
|
||||
"useLegacyDownload": false,
|
||||
"useLegacyStdio": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
@@ -406,3 +406,22 @@ describe('useLegacyDownload', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLegacyStdio', () => {
|
||||
test('old', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ useLegacyStdio: false } as State,
|
||||
bleDIServiceDidReceiveSoftwareRevision('1.2.0'),
|
||||
).useLegacyStdio,
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('new', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ useLegacyStdio: true } as State,
|
||||
bleDIServiceDidReceiveSoftwareRevision('1.3.0'),
|
||||
).useLegacyStdio,
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
bleDidDisconnectPybricks,
|
||||
bleDisconnectPybricks,
|
||||
} from '../ble/actions';
|
||||
import { bleDIServiceDidReceiveSoftwareRevision } from '../ble-device-info-service/actions';
|
||||
import { HubType } from '../ble-lwp3-service/protocol';
|
||||
import {
|
||||
blePybricksServiceDidNotReceiveHubCapabilities,
|
||||
@@ -250,6 +251,18 @@ const useLegacyDownload: Reducer<boolean> = (state = false, action) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* When true, use NUS for stdio instead of Pybricks control characteristic.
|
||||
*/
|
||||
const useLegacyStdio: Reducer<boolean> = (state = false, action) => {
|
||||
if (bleDIServiceDidReceiveSoftwareRevision.matches(action)) {
|
||||
// Behavior changed starting with Pybricks Profile v1.3.0.
|
||||
return !semver.satisfies(action.version, '^1.3.0');
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
runtime,
|
||||
downloadProgress,
|
||||
@@ -258,4 +271,5 @@ export default combineReducers({
|
||||
hasRepl,
|
||||
preferredFileFormat,
|
||||
useLegacyDownload,
|
||||
useLegacyStdio,
|
||||
});
|
||||
|
||||
+230
-16
@@ -9,6 +9,12 @@ import {
|
||||
didWrite,
|
||||
write,
|
||||
} from '../ble-nordic-uart-service/actions';
|
||||
import {
|
||||
didFailToSendCommand,
|
||||
didReceiveWriteStdout,
|
||||
didSendCommand,
|
||||
sendWriteStdinCommand,
|
||||
} from '../ble-pybricks-service/actions';
|
||||
import { checksum, hubDidStartRepl } from '../hub/actions';
|
||||
import { HubRuntimeState } from '../hub/reducers';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
@@ -17,28 +23,68 @@ import terminal from './sagas';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
describe('Data receiver filters out hub status', () => {
|
||||
test('normal message - no status', async () => {
|
||||
describe('receiving stdout from hub', () => {
|
||||
test('legacy UART message', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({ hub: { useLegacyDownload: true, useLegacyStdio: true } });
|
||||
|
||||
// sending ASCII space character
|
||||
saga.put(didNotify(new DataView(new Uint8Array([0x20]).buffer)));
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toEqual(sendData(' '));
|
||||
await expect(saga.take()).resolves.toEqual(sendData(' '));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('checksum message', async () => {
|
||||
test('legacy download checksum message', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.updateState({ hub: { runtime: HubRuntimeState.Loading } });
|
||||
saga.updateState({
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Loading,
|
||||
useLegacyDownload: true,
|
||||
useLegacyStdio: true,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(didNotify(new DataView(new Uint8Array([0xaa]).buffer)));
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toEqual(checksum(0xaa));
|
||||
await expect(saga.take()).resolves.toEqual(checksum(0xaa));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('Pybricks Profile v1.3.0 ignores UART service', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.updateState({
|
||||
hub: {
|
||||
useLegacyDownload: false,
|
||||
useLegacyStdio: false,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(didNotify(new DataView(new Uint8Array([0x20]).buffer)));
|
||||
|
||||
await delay(50);
|
||||
|
||||
// no further actions should be pending
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('Pybricks Profile v1.3.0 write stdout command', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.updateState({
|
||||
hub: {
|
||||
useLegacyDownload: false,
|
||||
useLegacyStdio: false,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(didReceiveWriteStdout(new Uint8Array([0x20]).buffer));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendData(' '));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
@@ -71,7 +117,161 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
|
||||
test('basic function works', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({ hub: { runtime: HubRuntimeState.Running } });
|
||||
saga.updateState({
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Running,
|
||||
useLegacyStdio: false,
|
||||
maxBleWriteSize: 20,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(0, expected));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('messages are queued until previous has completed', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Running,
|
||||
useLegacyStdio: false,
|
||||
maxBleWriteSize: 20,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
await delay(50); // without delay, messages are combined
|
||||
saga.put(receiveData('test1234'));
|
||||
|
||||
// second message is queued until didSend or didFailToSend
|
||||
expect(saga.numPending()).toBe(1);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(0, expected));
|
||||
|
||||
// second message is queued until didSend or didFailToSend
|
||||
expect(saga.numPending()).toBe(0);
|
||||
|
||||
saga.put(didSendCommand(0));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(1, expected));
|
||||
|
||||
saga.put(didSendCommand(1));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('messages are queued until previous has failed', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Running,
|
||||
useLegacyStdio: false,
|
||||
maxBleWriteSize: 20,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
await delay(50); // without delay, messages are combined
|
||||
saga.put(receiveData('test1234'));
|
||||
|
||||
// second message is queued until didSend or didFailToSend
|
||||
expect(saga.numPending()).toBe(1);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(0, expected));
|
||||
|
||||
// second message is queued until didSend or didFailToSend
|
||||
expect(saga.numPending()).toBe(0);
|
||||
|
||||
saga.put(didFailToSendCommand(0, new Error('test error')));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendWriteStdinCommand(1, expected));
|
||||
|
||||
saga.put(didSendCommand(1));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('small messages are combined', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Running,
|
||||
useLegacyStdio: false,
|
||||
maxBleWriteSize: 20,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
saga.put(receiveData('test1234'));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
sendWriteStdinCommand(0, new Uint8Array([...expected, ...expected])),
|
||||
);
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('long messages are split', async () => {
|
||||
const testData = '012345678901234567890123456789';
|
||||
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Running,
|
||||
useLegacyStdio: false,
|
||||
maxBleWriteSize: 20,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(receiveData(testData));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
sendWriteStdinCommand(0, encoder.encode(testData.slice(0, 20))),
|
||||
);
|
||||
|
||||
saga.put(didSendCommand(0));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
sendWriteStdinCommand(1, encoder.encode(testData.slice(20, 40))),
|
||||
);
|
||||
|
||||
saga.put(didSendCommand(1));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('if user program is not running echo BEL', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Disconnected,
|
||||
useLegacyStdio: false,
|
||||
maxBleWriteSize: 20,
|
||||
},
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
|
||||
await delay(50);
|
||||
|
||||
// sends BEL character on error
|
||||
await expect(saga.take()).resolves.toEqual(sendData('\x07'));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Terminal data source responds to receive data actions (legacy)', () => {
|
||||
const expected = encoder.encode('test1234');
|
||||
|
||||
test('basic function works', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({
|
||||
hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
|
||||
@@ -83,7 +283,9 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
|
||||
test('messages are queued until previous has completed', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({ hub: { runtime: HubRuntimeState.Running } });
|
||||
saga.updateState({
|
||||
hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
await delay(50); // without delay, messages are combined
|
||||
@@ -110,7 +312,9 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
|
||||
test('messages are queued until previous has failed', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({ hub: { runtime: HubRuntimeState.Running } });
|
||||
saga.updateState({
|
||||
hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
await delay(50); // without delay, messages are combined
|
||||
@@ -137,7 +341,9 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
|
||||
test('small messages are combined', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({ hub: { runtime: HubRuntimeState.Running } });
|
||||
saga.updateState({
|
||||
hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
saga.put(receiveData('test1234'));
|
||||
@@ -152,7 +358,9 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
const testData = '012345678901234567890123456789';
|
||||
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({ hub: { runtime: HubRuntimeState.Running } });
|
||||
saga.updateState({
|
||||
hub: { runtime: HubRuntimeState.Running, useLegacyStdio: true },
|
||||
});
|
||||
|
||||
saga.put(receiveData(testData));
|
||||
|
||||
@@ -169,13 +377,19 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('if user program is not running, do not dispatch write', async () => {
|
||||
test('if user program is not running, echo BEL', async () => {
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
saga.updateState({ hub: { runtime: HubRuntimeState.Disconnected } });
|
||||
saga.updateState({
|
||||
hub: { runtime: HubRuntimeState.Disconnected, useLegacyStdio: true },
|
||||
});
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
|
||||
// line below will fail with unhandled pending dispatches if not working correctly
|
||||
await delay(50);
|
||||
|
||||
// sends BEL character on error
|
||||
await expect(saga.take()).resolves.toEqual(sendData('\x07'));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
|
||||
+69
-12
@@ -20,10 +20,16 @@ import {
|
||||
write,
|
||||
} from '../ble-nordic-uart-service/actions';
|
||||
import { nordicUartSafeTxCharLength } from '../ble-nordic-uart-service/protocol';
|
||||
import {
|
||||
didFailToSendCommand,
|
||||
didReceiveWriteStdout,
|
||||
didSendCommand,
|
||||
sendWriteStdinCommand,
|
||||
} from '../ble-pybricks-service/actions';
|
||||
import { checksum, hubDidStartRepl } from '../hub/actions';
|
||||
import { HubRuntimeState } from '../hub/reducers';
|
||||
import { RootState } from '../reducers';
|
||||
import { defined } from '../utils';
|
||||
import { assert, defined } from '../utils';
|
||||
import { TerminalContextValue } from './TerminalContext';
|
||||
import { receiveData, sendData } from './actions';
|
||||
|
||||
@@ -36,7 +42,13 @@ const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
function* receiveUartData(action: ReturnType<typeof didNotify>): Generator {
|
||||
const hubState = yield* select((s: RootState) => s.hub.runtime);
|
||||
const { runtime: hubState, useLegacyStdio } = yield* select(
|
||||
(s: RootState) => s.hub,
|
||||
);
|
||||
|
||||
if (!useLegacyStdio) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hubState === HubRuntimeState.Loading && action.value.buffer.byteLength === 1) {
|
||||
const view = new DataView(action.value.buffer);
|
||||
@@ -48,6 +60,13 @@ function* receiveUartData(action: ReturnType<typeof didNotify>): Generator {
|
||||
yield* put(sendData(value));
|
||||
}
|
||||
|
||||
function* handleReceiveWriteStdout(
|
||||
action: ReturnType<typeof didReceiveWriteStdout>,
|
||||
): Generator {
|
||||
const value = decoder.decode(action.payload);
|
||||
yield* put(sendData(value));
|
||||
}
|
||||
|
||||
function* receiveTerminalData(): Generator {
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
const channel = yield* actionChannel(receiveData);
|
||||
@@ -87,18 +106,55 @@ function* receiveTerminalData(): Generator {
|
||||
// stdin gets piped to BLE connection
|
||||
const data = encoder.encode(value);
|
||||
|
||||
for (let i = 0; i < data.length; i += nordicUartSafeTxCharLength) {
|
||||
const { id } = yield* put(
|
||||
write(nextMessageId(), data.slice(i, i + nordicUartSafeTxCharLength)),
|
||||
);
|
||||
const { useLegacyStdio, maxBleWriteSize } = yield* select(
|
||||
(s: RootState) => s.hub,
|
||||
);
|
||||
|
||||
yield* take(
|
||||
(a: AnyAction) =>
|
||||
(didWrite.matches(a) || didFailToWrite.matches(a)) && a.id === id,
|
||||
);
|
||||
if (useLegacyStdio) {
|
||||
for (let i = 0; i < data.length; i += nordicUartSafeTxCharLength) {
|
||||
const { id } = yield* put(
|
||||
write(
|
||||
nextMessageId(),
|
||||
data.slice(i, i + nordicUartSafeTxCharLength),
|
||||
),
|
||||
);
|
||||
|
||||
// wait for echo so tht we don't overrun the hub with messages
|
||||
yield* race([take(didNotify), delay(100)]);
|
||||
yield* take(
|
||||
(a: AnyAction) =>
|
||||
(didWrite.matches(a) || didFailToWrite.matches(a)) &&
|
||||
a.id === id,
|
||||
);
|
||||
|
||||
// wait for echo so tht we don't overrun the hub with messages
|
||||
yield* race([take(didNotify), delay(100)]);
|
||||
}
|
||||
} else {
|
||||
// maxBleWriteSize should always be set to a valid value when useLegacyStdio is false
|
||||
assert(maxBleWriteSize >= 20, 'bad maxBleWriteSize');
|
||||
|
||||
for (let i = 0; i < data.length; i += maxBleWriteSize) {
|
||||
const { id } = yield* put(
|
||||
sendWriteStdinCommand(
|
||||
nextMessageId(),
|
||||
data.slice(i, i + maxBleWriteSize),
|
||||
),
|
||||
);
|
||||
|
||||
const { didFail } = yield* race({
|
||||
didSucceed: take(didSendCommand.when((a) => a.id === id)),
|
||||
didFail: take(didFailToSendCommand.when((a) => a.id === id)),
|
||||
});
|
||||
|
||||
if (didFail) {
|
||||
// istanbul ignore if
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(didFail.error);
|
||||
}
|
||||
|
||||
// REVISIT: should we provide UI feedback?
|
||||
// could echo BEL character as above
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +171,7 @@ function handleHubDidStartRepl(): void {
|
||||
|
||||
export default function* (): Generator {
|
||||
yield* takeEvery(didNotify, receiveUartData);
|
||||
yield* takeEvery(didReceiveWriteStdout, handleReceiveWriteStdout);
|
||||
yield* fork(receiveTerminalData);
|
||||
yield* takeEvery(sendData, sendTerminalData);
|
||||
yield* takeEvery(hubDidStartRepl, handleHubDidStartRepl);
|
||||
|
||||
Reference in New Issue
Block a user