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 all commits
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
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
Loading