Skip to content

Commit 3c4cc20

Browse files
committed
ble-pybricks-service: add support for slots
The firmware has added some additional info for hubs that support slots for user programs. Add these additional parameters accordingly.
1 parent 1a7663a commit 3c4cc20

File tree

10 files changed

+103
-32
lines changed

10 files changed

+103
-32
lines changed

src/ble-pybricks-service/actions.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,14 @@ export const didFailToSendCommand = createAction((id: number, error: Error) => (
183183
* Action that represents a status report event received from the hub.
184184
* @param statusFlags The status flags.
185185
* @param progId The ID number of the user program that is running.
186+
* @param selectedSlot The currently selected slot on the hub.
186187
*/
187188
export const didReceiveStatusReport = createAction(
188-
(statusFlags: number, runningProgId: number) => ({
189+
(statusFlags: number, runningProgId: number, selectedSlot: number) => ({
189190
type: 'blePybricksServiceEvent.action.didReceiveStatusReport',
190191
statusFlags,
191192
runningProgId,
193+
selectedSlot,
192194
}),
193195
);
194196

@@ -226,13 +228,17 @@ export const eventProtocolError = createAction((error: Error) => ({
226228
/**
227229
* Action that is called when the Pybricks Hub Capbailities characteristic
228230
* is read.
231+
*
232+
* @since Pybricks Profile v1.2.0
233+
* @changed numOfSlots added in v.1.5.0
229234
*/
230235
export const blePybricksServiceDidReceiveHubCapabilities = createAction(
231-
(maxWriteSize: number, flags: number, maxUserProgramSize: number) => ({
236+
(maxWriteSize: number, flags: number, maxUserProgramSize: number, numOfSlots) => ({
232237
type: 'blePybricksServiceEvent.action.didReceiveHubCapabilities',
233238
maxWriteSize,
234239
flags,
235240
maxUserProgramSize,
241+
numOfSlots,
236242
}),
237243
);
238244

src/ble-pybricks-service/protocol.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,18 +303,23 @@ export function getEventType(msg: DataView): EventType {
303303
/**
304304
* Parses the payload of a status report message.
305305
* @param msg The raw message data.
306-
* @returns The status as bit flags and the program ID number of the running program.
306+
* @returns The status as bit flags, the program ID number of the running program,
307+
* and the currently selected slot.
307308
*
308-
* @since Pybricks Profile v1.0.0 - changed in v1.4.0
309+
* @since Pybricks Profile v1.0.0
310+
* @changed runningProgId added in v1.4.0
311+
* @changed selectedSlot added in v1.5.0
309312
*/
310313
export function parseStatusReport(msg: DataView): {
311314
flags: number;
312315
runningProgId: number;
316+
selectedSlot: number;
313317
} {
314318
assert(msg.getUint8(0) === EventType.StatusReport, 'expecting status report event');
315319
return {
316320
flags: msg.getUint32(1, true),
317321
runningProgId: msg.byteLength > 5 ? msg.getUint8(5) : 0,
322+
selectedSlot: msg.byteLength > 6 ? msg.getUint8(6) : 0,
318323
};
319324
}
320325

src/ble-pybricks-service/sagas.test.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,27 @@ describe('command encoder', () => {
174174
describe('event decoder', () => {
175175
test.each([
176176
[
177-
'legacy status report',
177+
'v1.3 status report',
178178
[
179179
0x00, // status report event
180180
0x01, // flags count LSB
181181
0x00, // .
182182
0x00, // .
183183
0x00, // flags count MSB
184184
],
185-
didReceiveStatusReport(0x00000001, 0),
185+
didReceiveStatusReport(0x00000001, 0, 0),
186+
],
187+
[
188+
'v1.4 status report',
189+
[
190+
0x00, // status report event
191+
0x01, // flags count LSB
192+
0x00, // .
193+
0x00, // .
194+
0x00, // flags count MSB
195+
0x80, // program ID
196+
],
197+
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL, 0),
186198
],
187199
[
188200
'status report',
@@ -193,8 +205,9 @@ describe('event decoder', () => {
193205
0x00, // .
194206
0x00, // flags count MSB
195207
0x80, // program ID
208+
0x02, // selected slot
196209
],
197-
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL),
210+
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL, 2),
198211
],
199212
[
200213
'write stdout',

src/ble-pybricks-service/sagas.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,13 @@ function* decodeResponse(action: ReturnType<typeof didNotifyEvent>): Generator {
127127
switch (responseType) {
128128
case EventType.StatusReport: {
129129
const status = parseStatusReport(action.value);
130-
yield* put(didReceiveStatusReport(status.flags, status.runningProgId));
130+
yield* put(
131+
didReceiveStatusReport(
132+
status.flags,
133+
status.runningProgId,
134+
status.selectedSlot,
135+
),
136+
);
131137
break;
132138
}
133139
case EventType.WriteStdout:

src/ble/reducers.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2021-2024 The Pybricks Authors
2+
// Copyright (c) 2021-2025 The Pybricks Authors
33

44
import { AnyAction } from 'redux';
55
import {
@@ -130,14 +130,18 @@ test('deviceLowBatteryWarning', () => {
130130
expect(
131131
reducers(
132132
{ deviceLowBatteryWarning: false } as State,
133-
didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning), 0),
133+
didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning), 0, 0),
134134
).deviceLowBatteryWarning,
135135
).toBeTruthy();
136136

137137
expect(
138138
reducers(
139139
{ deviceLowBatteryWarning: true } as State,
140-
didReceiveStatusReport(~statusToFlag(Status.BatteryLowVoltageWarning), 0),
140+
didReceiveStatusReport(
141+
~statusToFlag(Status.BatteryLowVoltageWarning),
142+
0,
143+
0,
144+
),
141145
).deviceLowBatteryWarning,
142146
).toBeFalsy();
143147

src/ble/sagas.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2022-2023 The Pybricks Authors
2+
// Copyright (c) 2022-2025 The Pybricks Authors
33

44
import { MockProxy, mock } from 'jest-mock-extended';
55
import { AsyncSaga } from '../../test';
@@ -114,7 +114,7 @@ function createMocks(): Mocks {
114114
hubCapabilitiesChar.readValue.mockResolvedValue(
115115
new DataView(
116116
new Uint8Array([
117-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
117+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
118118
]).buffer,
119119
),
120120
);
@@ -459,7 +459,7 @@ describe('connect action is dispatched', () => {
459459
);
460460

461461
await expect(saga.take()).resolves.toEqual(
462-
blePybricksServiceDidReceiveHubCapabilities(0, 0, 0),
462+
blePybricksServiceDidReceiveHubCapabilities(0, 0, 0, 0),
463463
);
464464

465465
await expect(saga.take()).resolves.toEqual(

src/ble/sagas.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2020-2023 The Pybricks Authors
2+
// Copyright (c) 2020-2025 The Pybricks Authors
33
//
44
// Manages connection to a Bluetooth Low Energy device running Pybricks firmware.
55

@@ -73,7 +73,7 @@ import {
7373
import { BleConnectionState } from './reducers';
7474

7575
/** The version of the Pybricks Profile version currently implemented by this file. */
76-
export const supportedPybricksProfileVersion = '1.4.0';
76+
export const supportedPybricksProfileVersion = '1.5.0';
7777

7878
const decoder = new TextDecoder();
7979

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

381+
const numOfSlots = (() => {
382+
if (semver.satisfies(softwareRevision, '^1.5.0')) {
383+
return hubCapabilitiesValue.getUint8(10);
384+
}
385+
386+
return 0;
387+
})();
388+
381389
yield* put(
382390
blePybricksServiceDidReceiveHubCapabilities(
383391
maxWriteSize,
384392
flags,
385393
maxUserProgramSize,
394+
numOfSlots,
386395
),
387396
);
388397
} else {

src/hub/reducers.test.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2021-2024 The Pybricks Authors
2+
// Copyright (c) 2021-2025 The Pybricks Authors
33

44
import { AnyAction } from 'redux';
55
import {
@@ -44,8 +44,10 @@ test('initial state', () => {
4444
"hasRepl": false,
4545
"maxBleWriteSize": 0,
4646
"maxUserProgramSize": 0,
47+
"numOfSlots": 0,
4748
"preferredFileFormat": null,
4849
"runtime": "hub.runtime.disconnected",
50+
"selectedSlot": 0,
4951
"useLegacyDownload": false,
5052
"useLegacyStartUserProgram": false,
5153
"useLegacyStdio": false,
@@ -158,55 +160,55 @@ describe('runtime', () => {
158160
expect(
159161
reducers(
160162
{ runtime: HubRuntimeState.Disconnected } as State,
161-
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
163+
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0, 0),
162164
).runtime,
163165
).toBe(HubRuntimeState.Disconnected);
164166

165167
// status update ignored while download not finished
166168
expect(
167169
reducers(
168170
{ runtime: HubRuntimeState.Loading } as State,
169-
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
171+
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0, 0),
170172
).runtime,
171173
).toBe(HubRuntimeState.Loading);
172174

173175
// normal operation - user program started
174176
expect(
175177
reducers(
176178
{ runtime: HubRuntimeState.Unknown } as State,
177-
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
179+
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0, 0),
178180
).runtime,
179181
).toBe(HubRuntimeState.Running);
180182

181183
// really short program run finished before receiving download finished
182184
expect(
183185
reducers(
184186
{ runtime: HubRuntimeState.Unknown } as State,
185-
didReceiveStatusReport(0, 0),
187+
didReceiveStatusReport(0, 0, 0),
186188
).runtime,
187189
).toBe(HubRuntimeState.Idle);
188190

189191
// normal operation - user program stopped
190192
expect(
191193
reducers(
192194
{ runtime: HubRuntimeState.Running } as State,
193-
didReceiveStatusReport(0, 0),
195+
didReceiveStatusReport(0, 0, 0),
194196
).runtime,
195197
).toBe(HubRuntimeState.Idle);
196198

197199
// ignored during start repl command
198200
expect(
199201
reducers(
200202
{ runtime: HubRuntimeState.StartingRepl } as State,
201-
didReceiveStatusReport(0, 0),
203+
didReceiveStatusReport(0, 0, 0),
202204
).runtime,
203205
).toBe(HubRuntimeState.StartingRepl);
204206

205207
// ignored during stop user program command
206208
expect(
207209
reducers(
208210
{ runtime: HubRuntimeState.StoppingUserProgram } as State,
209-
didReceiveStatusReport(0, 0),
211+
didReceiveStatusReport(0, 0, 0),
210212
).runtime,
211213
).toBe(HubRuntimeState.StoppingUserProgram);
212214
});
@@ -301,7 +303,7 @@ describe('maxBleWriteSize', () => {
301303
expect(
302304
reducers(
303305
{ maxBleWriteSize: 0 } as State,
304-
blePybricksServiceDidReceiveHubCapabilities(size, 0, 100),
306+
blePybricksServiceDidReceiveHubCapabilities(size, 0, 100, 0),
305307
).maxBleWriteSize,
306308
).toBe(size);
307309
});
@@ -312,7 +314,7 @@ describe('maxUserProgramSize', () => {
312314
expect(
313315
reducers(
314316
{ maxUserProgramSize: 0 } as State,
315-
blePybricksServiceDidReceiveHubCapabilities(23, 0, size),
317+
blePybricksServiceDidReceiveHubCapabilities(23, 0, size, 0),
316318
).maxUserProgramSize,
317319
).toBe(size);
318320
});
@@ -340,7 +342,7 @@ describe('hasRepl', () => {
340342
expect(
341343
reducers(
342344
{ hasRepl: true } as State,
343-
blePybricksServiceDidReceiveHubCapabilities(23, flag, 100),
345+
blePybricksServiceDidReceiveHubCapabilities(23, flag, 100, 0),
344346
).hasRepl,
345347
).toBe(Boolean(flag & HubCapabilityFlag.HasRepl));
346348
},
@@ -374,6 +376,7 @@ describe('preferredFileFormat', () => {
374376
23,
375377
HubCapabilityFlag.UserProgramMultiMpy6,
376378
100,
379+
0,
377380
),
378381
).preferredFileFormat,
379382
).toBe(FileFormat.MultiMpy6);
@@ -383,7 +386,7 @@ describe('preferredFileFormat', () => {
383386
expect(
384387
reducers(
385388
{ preferredFileFormat: FileFormat.MultiMpy6 } as State,
386-
blePybricksServiceDidReceiveHubCapabilities(23, 0, 100),
389+
blePybricksServiceDidReceiveHubCapabilities(23, 0, 100, 0),
387390
).preferredFileFormat,
388391
).toBeNull();
389392
});
@@ -403,7 +406,7 @@ describe('useLegacyDownload', () => {
403406
expect(
404407
reducers(
405408
{ useLegacyDownload: true } as State,
406-
blePybricksServiceDidReceiveHubCapabilities(23, 0, 100),
409+
blePybricksServiceDidReceiveHubCapabilities(23, 0, 100, 0),
407410
).useLegacyDownload,
408411
).toBeFalsy();
409412
});

src/hub/reducers.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2020-2024 The Pybricks Authors
2+
// Copyright (c) 2020-2025 The Pybricks Authors
33

44
import { Reducer, combineReducers } from 'redux';
55
import * as semver from 'semver';
@@ -275,6 +275,28 @@ const useLegacyStartUserProgram: Reducer<boolean> = (state = false, action) => {
275275
return state;
276276
};
277277

278+
/*
279+
* Returns number of available slots or 0 for slots not supported.
280+
*/
281+
const numOfSlots: Reducer<number> = (state = 0, action) => {
282+
if (blePybricksServiceDidReceiveHubCapabilities.matches(action)) {
283+
return action.numOfSlots;
284+
}
285+
286+
return state;
287+
};
288+
289+
/*
290+
* Returns the currently selected slot on a connected hub.
291+
*/
292+
const selectedSlot: Reducer<number> = (state = 0, action) => {
293+
if (didReceiveStatusReport.matches(action)) {
294+
return action.selectedSlot;
295+
}
296+
297+
return state;
298+
};
299+
278300
export default combineReducers({
279301
runtime,
280302
downloadProgress,
@@ -285,4 +307,6 @@ export default combineReducers({
285307
useLegacyDownload,
286308
useLegacyStdio,
287309
useLegacyStartUserProgram,
310+
numOfSlots,
311+
selectedSlot,
288312
});

0 commit comments

Comments
 (0)