Skip to content

Implemented pybricks protocol v1.4.0 #2317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions src/ble-pybricks-service/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ export const sendStopUserProgramCommand = createAction((id: number) => ({
* Action that requests a start user program to be sent.
* @param id Unique identifier for this transaction.
*
* @since Pybricks Profile v1.2.0
* @since Pybricks Profile v1.2.0. Program identifier added in Pybricks Profile v1.4.0.
*/
export const sendStartUserProgramCommand = createAction((id: number) => ({
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
id,
}));
export const sendStartUserProgramCommand = createAction(
(id: number, slot?: number) => ({
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
id,
slot,
}),
);

/**
* Action that requests a start interactive REPL to be sent.
Expand Down Expand Up @@ -124,6 +127,23 @@ export const sendWriteStdinCommand = createAction(
}),
);

/**
* Action that requests to write to appdata.
* @param id Unique identifier for this transaction.
* @param offset offset: The offset from the buffer base address
* @param payload The bytes to write.
*
* @since Pybricks Profile v1.4.0.
*/
export const sendWriteAppDataCommand = createAction(
(id: number, offset: number, payload: ArrayBuffer) => ({
type: 'blePybricksServiceCommand.action.sendWriteAppDataCommand',
id,
offset,
payload,
}),
);

/**
* Action that indicates that a command was successfully sent.
* @param id Unique identifier for the transaction from the corresponding "send" command.
Expand Down Expand Up @@ -157,7 +177,7 @@ export const didReceiveStatusReport = createAction((statusFlags: number) => ({

/**
* Action that represents a status report event received from the hub.
* @param statusFlags The status flags.
* @param payload The piece of message received.
*
* @since Pybricks Profile v1.3.0
*/
Expand All @@ -166,6 +186,17 @@ export const didReceiveWriteStdout = createAction((payload: ArrayBuffer) => ({
payload,
}));

/**
* Action that represents a write to a buffer that is pre-allocated by a user program received from the hub.
* @param payload The piece of message received.
*
* @since Pybricks Profile v1.4.0
*/
export const didReceiveWriteAppData = createAction((payload: ArrayBuffer) => ({
type: 'blePybricksServiceEvent.action.didReceiveWriteAppData',
payload,
}));

/**
* Pseudo-event = actionCreator((not received from hub) indicating that there was a protocol error.
* @param error The error that was caught.
Expand Down
79 changes: 76 additions & 3 deletions src/ble-pybricks-service/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ export enum CommandType {
* @since Pybricks Profile v1.3.0
*/
WriteStdin = 6,
/**
* Requests to write to a buffer that is pre-allocated by a user program.
*
* Parameters:
* - offset: The offset from the buffer base address (16-bit little-endian
* unsigned integer).
* - payload: The data to write.
*
* @since Pybricks Profile v1.4.0
*/
WriteAppData = 7,
}

/**
Expand All @@ -74,11 +85,22 @@ export function createStopUserProgramCommand(): Uint8Array {
/**
* Creates a {@link CommandType.StartUserProgram} message.
*
* @since Pybricks Profile v1.2.0
* The optional payload parameter was added in Pybricks Profile v1.4.0.
*
* Parameters:
* - payload: Optional program identifier (one byte). Slots 0--127 are
* reserved for downloaded user programs. Slots 128--255 are
* for builtin user programs. If no program identifier is
* given, the currently active program slot will be started.
*
* @since Pybricks Profile v1.2.0. Program identifier added in Pybricks Profile v1.4.0.
*/
export function createStartUserProgramCommand(): Uint8Array {
const msg = new Uint8Array(1);
export function createStartUserProgramCommand(slot: number | undefined): Uint8Array {
const msg = new Uint8Array(slot === undefined ? 1 : 2);
msg[0] = CommandType.StartUserProgram;
if (slot !== undefined) {
msg[1] = slot & 0xff;
}
return msg;
}

Expand Down Expand Up @@ -140,6 +162,25 @@ export function createWriteStdinCommand(payload: ArrayBuffer): Uint8Array {
return msg;
}

/**
* Creates a {@link CommandType.WriteAppData} message.
* @param offset The offset from the buffer base address
* @param payload The bytes to write.
*
* @since Pybricks Profile v1.4.0.
*/
export function createWriteAppDataCommand(
offset: number,
payload: ArrayBuffer,
): Uint8Array {
const msg = new Uint8Array(1 + 2 + payload.byteLength);
const view = new DataView(msg.buffer);
view.setUint8(0, CommandType.WriteAppData);
view.setUint16(1, offset & 0xffff);
msg.set(new Uint8Array(payload), 3);
return msg;
}

/** Events are notifications received from the hub. */
export enum EventType {
/**
Expand All @@ -156,6 +197,12 @@ export enum EventType {
* @since Pybricks Profile v1.3.0
*/
WriteStdout = 1,
/**
* Hub wrote to appdata event.
*
* @since Pybricks Profile v1.4.0
*/
WriteAppData = 2,
}

/** Status indications received by Event.StatusReport */
Expand Down Expand Up @@ -244,6 +291,18 @@ export function parseWriteStdout(msg: DataView): ArrayBuffer {
return msg.buffer.slice(1);
}

/**
* Parses the payload of a app data message.
* @param msg The raw message data.
* @returns The bytes that were written.
*
* @since Pybricks Profile v1.4.0
*/
export function parseWriteAppData(msg: DataView): ArrayBuffer {
assert(msg.getUint8(0) === EventType.WriteAppData, 'expecting write appdata event');
return msg.buffer.slice(1);
}

/**
* Protocol error. Thrown e.g. when there is a malformed message.
*/
Expand Down Expand Up @@ -285,6 +344,20 @@ export enum HubCapabilityFlag {
* @since Pybricks Profile v1.3.0
*/
UserProgramMultiMpy6Native6p1 = 1 << 2,

/**
* Hub supports builtin sensor port view monitoring program.
*
* @since Pybricks Profile v1.4.0.
*/
HasPortView = 1 << 3,

/**
* Hub supports builtin IMU calibration program.
*
* @since Pybricks Profile v1.4.0.
*/
HasIMUCalibration = 1 << 4,
}

/** Supported user program file formats. */
Expand Down
74 changes: 72 additions & 2 deletions src/ble-pybricks-service/sagas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
didFailToWriteCommand,
didNotifyEvent,
didReceiveStatusReport,
didReceiveWriteAppData,
didReceiveWriteStdout,
didSendCommand,
didWriteCommand,
eventProtocolError,
sendStartReplCommand,
sendStartUserProgramCommand,
sendStopUserProgramCommand,
sendWriteAppDataCommand,
sendWriteStdinCommand,
sendWriteUserProgramMetaCommand,
sendWriteUserRamCommand,
Expand All @@ -38,6 +40,14 @@ describe('command encoder', () => {
0x01, // start user program command
],
],
[
'start user program with slot',
sendStartUserProgramCommand(0, 0x2A),
[
0x01, // start user program command
0x2A, // program slot
],
],
[
'start repl',
sendStartReplCommand(0),
Expand Down Expand Up @@ -82,6 +92,19 @@ describe('command encoder', () => {
0x04, // payload end
],
],
[
'write appdata',
sendWriteAppDataCommand(0, 0x2A, new Uint8Array([1, 2, 3, 4]).buffer),
[
0x07, // write appdata command
0x00, // offset msb 16bit
0x2A, // offset lsb 16bit
0x01, // payload start
0x02,
0x03,
0x04, // payload end
],
],
])('encode %s request', async (_n, request, expected) => {
const saga = new AsyncSaga(blePybricksService);
saga.put(request);
Expand Down Expand Up @@ -178,14 +201,61 @@ describe('event decoder', () => {
]).buffer,
),
],
])('decode %s event', async (_n, message, expected) => {
[
'write appdata',
[
0x02, // write appdata event
't'.charCodeAt(0), //payload
'e'.charCodeAt(0),
't'.charCodeAt(0),
't'.charCodeAt(0),
],
didReceiveWriteAppData(
new Uint8Array([
't'.charCodeAt(0),
'e'.charCodeAt(0),
't'.charCodeAt(0),
't'.charCodeAt(0),
]).buffer,
),
],
[
'write appdata mismatch',
[
0x02, // write appdata event
't'.charCodeAt(0), //payload
'e'.charCodeAt(0),
't'.charCodeAt(0),
't'.charCodeAt(0),
],
didReceiveWriteAppData(
new Uint8Array([
't'.charCodeAt(0),
'e'.charCodeAt(0),
'x'.charCodeAt(0),
't'.charCodeAt(0),
]).buffer,
),
true,
false
],
])('decode %s event', async (_n, message, expected, isEqual = true, isStrictlyEqual = true) => {
const saga = new AsyncSaga(blePybricksService);
const notification = new Uint8Array(message);

saga.put(didNotifyEvent(new DataView(notification.buffer)));

const action = await saga.take();
expect(action).toEqual(expected);
if (isEqual) {
expect(action).toEqual(expected);
} else {
expect(action).not.toEqual(expected);
}
if (isStrictlyEqual) {
expect(action).toStrictEqual(expected);
} else {
expect(action).not.toStrictEqual(expected);
}

await saga.end();
});
Expand Down
21 changes: 19 additions & 2 deletions src/ble-pybricks-service/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import {
didFailToWriteCommand,
didNotifyEvent,
didReceiveStatusReport,
didReceiveWriteAppData,
didReceiveWriteStdout,
didSendCommand,
didWriteCommand,
eventProtocolError,
sendStartReplCommand,
sendStartUserProgramCommand,
sendStopUserProgramCommand,
sendWriteAppDataCommand,
sendWriteStdinCommand,
sendWriteUserProgramMetaCommand,
sendWriteUserRamCommand,
Expand All @@ -36,11 +38,13 @@ import {
createStartReplCommand,
createStartUserProgramCommand,
createStopUserProgramCommand,
createWriteAppDataCommand,
createWriteStdinCommand,
createWriteUserProgramMetaCommand,
createWriteUserRamCommand,
getEventType,
parseStatusReport,
parseWriteAppData,
parseWriteStdout,
} from './protocol';

Expand All @@ -57,14 +61,17 @@ function* encodeRequest(): Generator {
a.type.startsWith('blePybricksServiceCommand.action.send'),
);

for (;;) {

for (; ;) {
const action = yield* take(chan);

/* istanbul ignore else: should not be possible to reach */
if (sendStopUserProgramCommand.matches(action)) {
yield* put(writeCommand(action.id, createStopUserProgramCommand()));
} else if (sendStartUserProgramCommand.matches(action)) {
yield* put(writeCommand(action.id, createStartUserProgramCommand()));
yield* put(
writeCommand(action.id, createStartUserProgramCommand(action.slot)),
);
} else if (sendStartReplCommand.matches(action)) {
yield* put(writeCommand(action.id, createStartReplCommand()));
} else if (sendWriteUserProgramMetaCommand.matches(action)) {
Expand All @@ -82,6 +89,13 @@ function* encodeRequest(): Generator {
yield* put(
writeCommand(action.id, createWriteStdinCommand(action.payload)),
);
} else if (sendWriteAppDataCommand.matches(action)) {
yield* put(
writeCommand(
action.id,
createWriteAppDataCommand(action.offset, action.payload),
),
);
} else {
console.error(`Unknown Pybricks service command ${action.type}`);
continue;
Expand Down Expand Up @@ -114,6 +128,9 @@ function* decodeResponse(action: ReturnType<typeof didNotifyEvent>): Generator {
case EventType.WriteStdout:
yield* put(didReceiveWriteStdout(parseWriteStdout(action.value)));
break;
case EventType.WriteAppData:
yield* put(didReceiveWriteAppData(parseWriteAppData(action.value)));
break;
default:
throw new ProtocolError(
`unknown pybricks event type: ${hex(responseType, 2)}`,
Expand Down
2 changes: 1 addition & 1 deletion src/ble/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import {
import { BleConnectionState } from './reducers';

/** The version of the Pybricks Profile version currently implemented by this file. */
export const supportedPybricksProfileVersion = '1.3.0';
export const supportedPybricksProfileVersion = '1.4.0';

const decoder = new TextDecoder();

Expand Down