mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 18:46:17 +00:00
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.
This commit is contained in:
@@ -183,12 +183,14 @@ 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 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, runningProgId: number) => ({
|
||||
(statusFlags: number, runningProgId: number, selectedSlot: number) => ({
|
||||
type: 'blePybricksServiceEvent.action.didReceiveStatusReport',
|
||||
statusFlags,
|
||||
runningProgId,
|
||||
selectedSlot,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -226,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,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -303,18 +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 program ID 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;
|
||||
runningProgId: number;
|
||||
selectedSlot: number;
|
||||
} {
|
||||
assert(msg.getUint8(0) === EventType.StatusReport, 'expecting status report event');
|
||||
return {
|
||||
flags: msg.getUint32(1, true),
|
||||
runningProgId: msg.byteLength > 5 ? msg.getUint8(5) : 0,
|
||||
selectedSlot: msg.byteLength > 6 ? msg.getUint8(6) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('command encoder', () => {
|
||||
describe('event decoder', () => {
|
||||
test.each([
|
||||
[
|
||||
'legacy status report',
|
||||
'v1.3 status report',
|
||||
[
|
||||
0x00, // status report event
|
||||
0x01, // flags count LSB
|
||||
@@ -182,7 +182,19 @@ describe('event decoder', () => {
|
||||
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',
|
||||
@@ -193,8 +205,9 @@ describe('event decoder', () => {
|
||||
0x00, // .
|
||||
0x00, // flags count MSB
|
||||
0x80, // program ID
|
||||
0x02, // selected slot
|
||||
],
|
||||
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL),
|
||||
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL, 2),
|
||||
],
|
||||
[
|
||||
'write stdout',
|
||||
|
||||
@@ -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.runningProgId));
|
||||
yield* put(
|
||||
didReceiveStatusReport(
|
||||
status.flags,
|
||||
status.runningProgId,
|
||||
status.selectedSlot,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case EventType.WriteStdout:
|
||||
|
||||
@@ -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 {
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
@@ -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(
|
||||
|
||||
+11
-2
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+16
-13
@@ -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 {
|
||||
@@ -44,8 +44,10 @@ test('initial state', () => {
|
||||
"hasRepl": false,
|
||||
"maxBleWriteSize": 0,
|
||||
"maxUserProgramSize": 0,
|
||||
"numOfSlots": 0,
|
||||
"preferredFileFormat": null,
|
||||
"runtime": "hub.runtime.disconnected",
|
||||
"selectedSlot": 0,
|
||||
"useLegacyDownload": false,
|
||||
"useLegacyStartUserProgram": false,
|
||||
"useLegacyStdio": false,
|
||||
@@ -158,7 +160,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Disconnected } as State,
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Disconnected);
|
||||
|
||||
@@ -166,7 +168,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Loading } as State,
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Loading);
|
||||
|
||||
@@ -174,7 +176,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Unknown } as State,
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Running);
|
||||
|
||||
@@ -182,7 +184,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Unknown } as State,
|
||||
didReceiveStatusReport(0, 0),
|
||||
didReceiveStatusReport(0, 0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Idle);
|
||||
|
||||
@@ -190,7 +192,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Running } as State,
|
||||
didReceiveStatusReport(0, 0),
|
||||
didReceiveStatusReport(0, 0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Idle);
|
||||
|
||||
@@ -198,7 +200,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.StartingRepl } as State,
|
||||
didReceiveStatusReport(0, 0),
|
||||
didReceiveStatusReport(0, 0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.StartingRepl);
|
||||
|
||||
@@ -206,7 +208,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.StoppingUserProgram } as State,
|
||||
didReceiveStatusReport(0, 0),
|
||||
didReceiveStatusReport(0, 0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.StoppingUserProgram);
|
||||
});
|
||||
@@ -301,7 +303,7 @@ describe('maxBleWriteSize', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ maxBleWriteSize: 0 } as State,
|
||||
blePybricksServiceDidReceiveHubCapabilities(size, 0, 100),
|
||||
blePybricksServiceDidReceiveHubCapabilities(size, 0, 100, 0),
|
||||
).maxBleWriteSize,
|
||||
).toBe(size);
|
||||
});
|
||||
@@ -312,7 +314,7 @@ describe('maxUserProgramSize', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ maxUserProgramSize: 0 } as State,
|
||||
blePybricksServiceDidReceiveHubCapabilities(23, 0, size),
|
||||
blePybricksServiceDidReceiveHubCapabilities(23, 0, size, 0),
|
||||
).maxUserProgramSize,
|
||||
).toBe(size);
|
||||
});
|
||||
@@ -340,7 +342,7 @@ describe('hasRepl', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ hasRepl: true } as State,
|
||||
blePybricksServiceDidReceiveHubCapabilities(23, flag, 100),
|
||||
blePybricksServiceDidReceiveHubCapabilities(23, flag, 100, 0),
|
||||
).hasRepl,
|
||||
).toBe(Boolean(flag & HubCapabilityFlag.HasRepl));
|
||||
},
|
||||
@@ -374,6 +376,7 @@ describe('preferredFileFormat', () => {
|
||||
23,
|
||||
HubCapabilityFlag.UserProgramMultiMpy6,
|
||||
100,
|
||||
0,
|
||||
),
|
||||
).preferredFileFormat,
|
||||
).toBe(FileFormat.MultiMpy6);
|
||||
@@ -383,7 +386,7 @@ describe('preferredFileFormat', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ preferredFileFormat: FileFormat.MultiMpy6 } as State,
|
||||
blePybricksServiceDidReceiveHubCapabilities(23, 0, 100),
|
||||
blePybricksServiceDidReceiveHubCapabilities(23, 0, 100, 0),
|
||||
).preferredFileFormat,
|
||||
).toBeNull();
|
||||
});
|
||||
@@ -403,7 +406,7 @@ describe('useLegacyDownload', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ useLegacyDownload: true } as State,
|
||||
blePybricksServiceDidReceiveHubCapabilities(23, 0, 100),
|
||||
blePybricksServiceDidReceiveHubCapabilities(23, 0, 100, 0),
|
||||
).useLegacyDownload,
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
+25
-1
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2024 The Pybricks Authors
|
||||
// Copyright (c) 2020-2025 The Pybricks Authors
|
||||
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import * as semver from 'semver';
|
||||
@@ -275,6 +275,28 @@ const useLegacyStartUserProgram: Reducer<boolean> = (state = false, action) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns number of available slots or 0 for slots not supported.
|
||||
*/
|
||||
const numOfSlots: Reducer<number> = (state = 0, action) => {
|
||||
if (blePybricksServiceDidReceiveHubCapabilities.matches(action)) {
|
||||
return action.numOfSlots;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns the currently selected slot on a connected hub.
|
||||
*/
|
||||
const selectedSlot: Reducer<number> = (state = 0, action) => {
|
||||
if (didReceiveStatusReport.matches(action)) {
|
||||
return action.selectedSlot;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
runtime,
|
||||
downloadProgress,
|
||||
@@ -285,4 +307,6 @@ export default combineReducers({
|
||||
useLegacyDownload,
|
||||
useLegacyStdio,
|
||||
useLegacyStartUserProgram,
|
||||
numOfSlots,
|
||||
selectedSlot,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2024 The Pybricks Authors
|
||||
// Copyright (c) 2020-2025 The Pybricks Authors
|
||||
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -19,6 +19,7 @@ const RunButton: React.FunctionComponent<RunButtonProps> = ({ id }) => {
|
||||
runtime,
|
||||
useLegacyDownload,
|
||||
useLegacyStartUserProgram,
|
||||
selectedSlot,
|
||||
} = useSelector((s) => s.hub);
|
||||
const activeFile = useSelector((s) => s.editor.activeFileUuid);
|
||||
const keyboardShortcut = 'F5';
|
||||
@@ -48,7 +49,7 @@ const RunButton: React.FunctionComponent<RunButtonProps> = ({ id }) => {
|
||||
preferredFileFormat,
|
||||
useLegacyDownload,
|
||||
useLegacyStartUserProgram,
|
||||
0, // No slot UI yet
|
||||
selectedSlot,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user