Skip to content
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
30 changes: 19 additions & 11 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-2024 The Pybricks Authors
// Copyright (c) 2021-2025 The Pybricks Authors
//
// Actions for Bluetooth Low Energy Pybricks service

Expand Down Expand Up @@ -81,15 +81,17 @@ export const sendLegacyStartReplCommand = createAction((id: number) => ({
/**
* 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.
* @param progId The program ID number of the user program to start.
*
* @since Pybricks Profile v1.4.0
*/
export const sendStartUserProgramCommand = createAction((id: number, slot: number) => ({
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
id,
slot,
}));
export const sendStartUserProgramCommand = createAction(
(id: number, progId: number) => ({
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
id,
progId,
}),
);

/**
* Action that requests to write user program metadata.
Expand Down Expand Up @@ -180,13 +182,15 @@ 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.
* @param progId The ID number of the user program that is running.
* @param selectedSlot The currently selected slot on the hub.
*/
export const didReceiveStatusReport = createAction(
(statusFlags: number, slot: number) => ({
(statusFlags: number, runningProgId: number, selectedSlot: number) => ({
type: 'blePybricksServiceEvent.action.didReceiveStatusReport',
statusFlags,
slot,
runningProgId,
selectedSlot,
}),
);

Expand Down Expand Up @@ -224,13 +228,17 @@ export const eventProtocolError = createAction((error: Error) => ({
/**
* Action that is called when the Pybricks Hub Capbailities characteristic
* is read.
*
* @since Pybricks Profile v1.2.0
* @changed numOfSlots added in v.1.5.0
*/
export const blePybricksServiceDidReceiveHubCapabilities = createAction(
(maxWriteSize: number, flags: number, maxUserProgramSize: number) => ({
(maxWriteSize: number, flags: number, maxUserProgramSize: number, numOfSlots) => ({
type: 'blePybricksServiceEvent.action.didReceiveHubCapabilities',
maxWriteSize,
flags,
maxUserProgramSize,
numOfSlots,
}),
);

Expand Down
24 changes: 16 additions & 8 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-2024 The Pybricks Authors
// Copyright (c) 2020-2025 The Pybricks Authors
//
// Definitions related to the Pybricks Bluetooth low energy GATT service.

Expand Down Expand Up @@ -112,17 +112,17 @@ export function createStopUserProgramCommand(): Uint8Array {
* Creates a {@link CommandType.StartUserProgram} message.
*
* Parameters:
* - slot: Program identifier (one byte). Slots 0--127 are reserved for
* - progId: 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(
slot: number | BuiltinProgramId,
progId: number | BuiltinProgramId,
): Uint8Array {
const msg = new Uint8Array(2);
msg[0] = CommandType.StartUserProgram;
msg[1] = slot;
msg[1] = progId;
return msg;
}

Expand Down Expand Up @@ -303,15 +303,23 @@ 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 and the slot number of the running program.
* @returns The status as bit flags, the program ID number of the running program,
* and the currently selected slot.
*
* @since Pybricks Profile v1.0.0 - changed in v1.4.0
* @since Pybricks Profile v1.0.0
* @changed runningProgId added in v1.4.0
* @changed selectedSlot added in v1.5.0
*/
export function parseStatusReport(msg: DataView): { flags: number; slot: number } {
export function parseStatusReport(msg: DataView): {
flags: number;
runningProgId: number;
selectedSlot: number;
} {
assert(msg.getUint8(0) === EventType.StatusReport, 'expecting status report event');
return {
flags: msg.getUint32(1, true),
slot: msg.byteLength > 5 ? msg.getUint8(5) : 0,
runningProgId: msg.byteLength > 5 ? msg.getUint8(5) : 0,
selectedSlot: msg.byteLength > 6 ? msg.getUint8(6) : 0,
};
}

Expand Down
27 changes: 20 additions & 7 deletions src/ble-pybricks-service/sagas.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2024 The Pybricks Authors
// Copyright (c) 2021-2025 The Pybricks Authors

import { AsyncSaga } from '../../test';
import {
Expand Down Expand Up @@ -35,11 +35,11 @@ describe('command encoder', () => {
],
],
[
'start user program with slot',
'start user program with program id',
sendStartUserProgramCommand(0, 0x2a),
[
0x01, // start user program command
0x2a, // program slot
0x2a, // program ID
],
],
[
Expand Down Expand Up @@ -174,15 +174,27 @@ describe('command encoder', () => {
describe('event decoder', () => {
test.each([
[
'legacy status report',
'v1.3 status report',
[
0x00, // status report event
0x01, // flags count LSB
0x00, // .
0x00, // .
0x00, // flags count MSB
],
didReceiveStatusReport(0x00000001, 0),
didReceiveStatusReport(0x00000001, 0, 0),
],
[
'v1.4 status report',
[
0x00, // status report event
0x01, // flags count LSB
0x00, // .
0x00, // .
0x00, // flags count MSB
0x80, // program ID
],
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL, 0),
],
[
'status report',
Expand All @@ -192,9 +204,10 @@ describe('event decoder', () => {
0x00, // .
0x00, // .
0x00, // flags count MSB
0x80, // slot
0x80, // program ID
0x02, // selected slot
],
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL),
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL, 2),
],
[
'write stdout',
Expand Down
12 changes: 9 additions & 3 deletions src/ble-pybricks-service/sagas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2024 The Pybricks Authors
// Copyright (c) 2021-2025 The Pybricks Authors
//
// Handles Pybricks protocol.

Expand Down Expand Up @@ -75,7 +75,7 @@ function* encodeRequest(): Generator {
yield* put(writeCommand(action.id, createLegacyStartReplCommand()));
} else if (sendStartUserProgramCommand.matches(action)) {
yield* put(
writeCommand(action.id, createStartUserProgramCommand(action.slot)),
writeCommand(action.id, createStartUserProgramCommand(action.progId)),
);
} else if (sendWriteUserProgramMetaCommand.matches(action)) {
yield* put(
Expand Down Expand Up @@ -127,7 +127,13 @@ function* decodeResponse(action: ReturnType<typeof didNotifyEvent>): Generator {
switch (responseType) {
case EventType.StatusReport: {
const status = parseStatusReport(action.value);
yield* put(didReceiveStatusReport(status.flags, status.slot));
yield* put(
didReceiveStatusReport(
status.flags,
status.runningProgId,
status.selectedSlot,
),
);
break;
}
case EventType.WriteStdout:
Expand Down
10 changes: 7 additions & 3 deletions src/ble/reducers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2024 The Pybricks Authors
// Copyright (c) 2021-2025 The Pybricks Authors

import { AnyAction } from 'redux';
import {
Expand Down Expand Up @@ -130,14 +130,18 @@ test('deviceLowBatteryWarning', () => {
expect(
reducers(
{ deviceLowBatteryWarning: false } as State,
didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning), 0),
didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning), 0, 0),
).deviceLowBatteryWarning,
).toBeTruthy();

expect(
reducers(
{ deviceLowBatteryWarning: true } as State,
didReceiveStatusReport(~statusToFlag(Status.BatteryLowVoltageWarning), 0),
didReceiveStatusReport(
~statusToFlag(Status.BatteryLowVoltageWarning),
0,
0,
),
).deviceLowBatteryWarning,
).toBeFalsy();

Expand Down
6 changes: 3 additions & 3 deletions src/ble/sagas.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022-2023 The Pybricks Authors
// Copyright (c) 2022-2025 The Pybricks Authors

import { MockProxy, mock } from 'jest-mock-extended';
import { AsyncSaga } from '../../test';
Expand Down Expand Up @@ -114,7 +114,7 @@ function createMocks(): Mocks {
hubCapabilitiesChar.readValue.mockResolvedValue(
new DataView(
new Uint8Array([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]).buffer,
),
);
Expand Down Expand Up @@ -459,7 +459,7 @@ describe('connect action is dispatched', () => {
);

await expect(saga.take()).resolves.toEqual(
blePybricksServiceDidReceiveHubCapabilities(0, 0, 0),
blePybricksServiceDidReceiveHubCapabilities(0, 0, 0, 0),
);

await expect(saga.take()).resolves.toEqual(
Expand Down
13 changes: 11 additions & 2 deletions src/ble/sagas.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-2025 The Pybricks Authors
//
// Manages connection to a Bluetooth Low Energy device running Pybricks firmware.

Expand Down 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.4.0';
export const supportedPybricksProfileVersion = '1.5.0';

const decoder = new TextDecoder();

Expand Down Expand Up @@ -378,11 +378,20 @@ function* handleBleConnectPybricks(): Generator {
const flags = hubCapabilitiesValue.getUint32(2, true);
const maxUserProgramSize = hubCapabilitiesValue.getUint32(6, true);

const numOfSlots = (() => {
if (semver.satisfies(softwareRevision, '^1.5.0')) {
return hubCapabilitiesValue.getUint8(10);
}

return 0;
})();

yield* put(
blePybricksServiceDidReceiveHubCapabilities(
maxWriteSize,
flags,
maxUserProgramSize,
numOfSlots,
),
);
} else {
Expand Down
Loading
Loading