Skip to content

Commit

Permalink
Implemented pybricks protocol v1.4.0 (#2317)
Browse files Browse the repository at this point in the history
* Update for changed StartUserProgram command semantics
* Added new capability flags and fixed-purpose slot definitions
* Update for new start REPL command
* Updated StartUserProgram command and Status event for slots
* Added new AppData command and event

---------

Co-authored-by: David Lechner <[email protected]>
  • Loading branch information
afarago and dlech authored Oct 27, 2024
1 parent 41a397a commit 14f4716
Show file tree
Hide file tree
Showing 15 changed files with 514 additions and 123 deletions.
70 changes: 58 additions & 12 deletions src/ble-pybricks-service/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2023 The Pybricks Authors
// Copyright (c) 2021-2024 The Pybricks Authors
//
// Actions for Bluetooth Low Energy Pybricks service

Expand Down Expand Up @@ -59,22 +59,36 @@ 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 - removed in v1.4.0
*/
export const sendStartUserProgramCommand = createAction((id: number) => ({
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
export const sendLegacyStartUserProgramCommand = createAction((id: number) => ({
type: 'blePybricksServiceCommand.action.sendLegacyStartUserProgram',
id,
}));

/**
* Action that requests a start interactive REPL to be sent.
* @param id Unique identifier for this transaction.
*
* @since Pybricks Profile v1.2.0
* @since Pybricks Profile v1.2.0 - removed in v1.4.0
*
*/
export const sendLegacyStartReplCommand = createAction((id: number) => ({
type: 'blePybricksServiceCommand.action.sendLegacyStartRepl',
id,
}));

/**
* Action that requests a start user program to be sent.
* @param id Unique identifier for this transaction.
* @param slot The slot number of the user program to start.
*
* @since Pybricks Profile v1.4.0
*/
export const sendStartReplCommand = createAction((id: number) => ({
type: 'blePybricksServiceCommand.action.sendStartRepl',
export const sendStartUserProgramCommand = createAction((id: number, slot: number) => ({
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
id,
slot,
}));

/**
Expand Down Expand Up @@ -124,6 +138,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 All @@ -149,15 +180,19 @@ export const didFailToSendCommand = createAction((id: number, error: Error) => (
/**
* Action that represents a status report event received from the hub.
* @param statusFlags The status flags.
* @param slot The slot number of the user program that is running.
*/
export const didReceiveStatusReport = createAction((statusFlags: number) => ({
type: 'blePybricksServiceEvent.action.didReceiveStatusReport',
statusFlags,
}));
export const didReceiveStatusReport = createAction(
(statusFlags: number, slot: number) => ({
type: 'blePybricksServiceEvent.action.didReceiveStatusReport',
statusFlags,
slot,
}),
);

/**
* 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 +201,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
139 changes: 126 additions & 13 deletions src/ble-pybricks-service/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2023 The Pybricks Authors
// Copyright (c) 2020-2024 The Pybricks Authors
//
// Definitions related to the Pybricks Bluetooth low energy GATT service.

Expand All @@ -25,13 +25,13 @@ export enum CommandType {
/**
* Request to start the user program.
*
* @since Pybricks Profile v1.2.0
* @since Pybricks Profile v1.2.0 - changed in v1.4.0
*/
StartUserProgram = 1,
/**
* Request to start the interactive REPL.
*
* @since Pybricks Profile v1.2.0
* @since Pybricks Profile v1.2.0 - removed in v1.4.0
*/
StartRepl = 2,
/**
Expand All @@ -58,6 +58,43 @@ 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,
}

/**
* Built-in program ID's for use with {@link CommandType.StartUserProgram}.
*
* @since Pybricks Profile v1.4.0
*/
export enum BuiltinProgramId {
/**
* Requests to start the built-in REPL on stdio.
*
* @since Pybricks Profile v1.4.0
*/
REPL = 0x80,
/**
* Requests to start the built-in sensor port view monitoring program.
*
* @since Pybricks Profile v1.4.0
*/
PortView = 0x81,
/**
* Requests to start the built-in IMU calibration program.
*
* @since Pybricks Profile v1.4.0
*/
IMUCalibration = 0x82,
}

/**
Expand All @@ -74,20 +111,38 @@ export function createStopUserProgramCommand(): Uint8Array {
/**
* Creates a {@link CommandType.StartUserProgram} message.
*
* @since Pybricks Profile v1.2.0
* Parameters:
* - slot: Program identifier (one byte). Slots 0--127 are reserved for
* downloaded user programs. Slots 128--255 are for builtin user programs.
*
* @since Pybricks Profile v1.4.0
*/
export function createStartUserProgramCommand(): Uint8Array {
export function createStartUserProgramCommand(
slot: number | BuiltinProgramId,
): Uint8Array {
const msg = new Uint8Array(2);
msg[0] = CommandType.StartUserProgram;
msg[1] = slot;
return msg;
}

/**
* Creates a legacy {@link CommandType.StartUserProgram} message.
*
* @since Pybricks Profile v1.2.0 - removed in v1.4.0
*/
export function createLegacyStartUserProgramCommand(): Uint8Array {
const msg = new Uint8Array(1);
msg[0] = CommandType.StartUserProgram;
return msg;
}

/**
* Creates a {@link CommandType.StartRepl} message.
* Creates a legacy {@link CommandType.StartRepl} message.
*
* @since Pybricks Profile v1.2.0
* @since Pybricks Profile v1.2.0 - removed in v1.4.0
*/
export function createStartReplCommand(): Uint8Array {
export function createLegacyStartReplCommand(): Uint8Array {
const msg = new Uint8Array(1);
msg[0] = CommandType.StartRepl;
return msg;
Expand Down Expand Up @@ -140,6 +195,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, true);
msg.set(new Uint8Array(payload), 3);
return msg;
}

/** Events are notifications received from the hub. */
export enum EventType {
/**
Expand All @@ -156,6 +230,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 @@ -223,13 +303,16 @@ export function getEventType(msg: DataView): EventType {
/**
* Parses the payload of a status report message.
* @param msg The raw message data.
* @returns The status as bit flags.
* @returns The status as bit flags and the slot number of the running program.
*
* @since Pybricks Profile v1.0.0
* @since Pybricks Profile v1.0.0 - changed in v1.4.0
*/
export function parseStatusReport(msg: DataView): number {
export function parseStatusReport(msg: DataView): { flags: number; slot: number } {
assert(msg.getUint8(0) === EventType.StatusReport, 'expecting status report event');
return msg.getUint32(1, true);
return {
flags: msg.getUint32(1, true),
slot: msg.byteLength > 5 ? msg.getUint8(5) : 0,
};
}

/**
Expand All @@ -244,6 +327,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 All @@ -266,7 +361,9 @@ export class ProtocolError extends Error {
*/
export enum HubCapabilityFlag {
/**
* Hub has an interactive REPL.
* Hub supports {@link CommandType.StartUserProgram} command with
* {@link BuiltinProgramId.REPL} for protocol v1.4.0 and later or hub
* supports {@link CommandType.StartRepl}
*
* @since Pybricks Profile v1.2.0
*/
Expand All @@ -285,6 +382,22 @@ export enum HubCapabilityFlag {
* @since Pybricks Profile v1.3.0
*/
UserProgramMultiMpy6Native6p1 = 1 << 2,

/**
* Hub supports {@link CommandType.StartUserProgram} command with
* {@link BuiltinProgramId.PortView}.
*
* @since Pybricks Profile v1.4.0.
*/
HasPortView = 1 << 3,

/**
* Hub supports {@link CommandType.StartUserProgram} command with
* {@link BuiltinProgramId.IMUCalibration}.
*
* @since Pybricks Profile v1.4.0.
*/
HasIMUCalibration = 1 << 4,
}

/** Supported user program file formats. */
Expand Down
Loading

0 comments on commit 14f4716

Please sign in to comment.