mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-15 02:54:07 +00:00
Implemented pybricks protocol v1.4.0 (#2317)
* 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 <david@pybricks.com>
This commit is contained in:
co-authored by
David Lechner
parent
41a397a8a3
commit
14f4716dbc
@@ -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
|
||||
|
||||
@@ -59,10 +59,10 @@ 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,
|
||||
}));
|
||||
|
||||
@@ -70,13 +70,27 @@ export const sendStartUserProgramCommand = createAction((id: number) => ({
|
||||
* 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 sendStartReplCommand = createAction((id: number) => ({
|
||||
type: 'blePybricksServiceCommand.action.sendStartRepl',
|
||||
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 sendStartUserProgramCommand = createAction((id: number, slot: number) => ({
|
||||
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
|
||||
id,
|
||||
slot,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Action that requests to write user program metadata.
|
||||
* @param id Unique identifier for this transaction.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
/**
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
/**
|
||||
@@ -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 */
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2023 The Pybricks Authors
|
||||
// Copyright (c) 2021-2024 The Pybricks Authors
|
||||
|
||||
import { AsyncSaga } from '../../test';
|
||||
import {
|
||||
@@ -7,19 +7,22 @@ import {
|
||||
didFailToWriteCommand,
|
||||
didNotifyEvent,
|
||||
didReceiveStatusReport,
|
||||
didReceiveWriteAppData,
|
||||
didReceiveWriteStdout,
|
||||
didSendCommand,
|
||||
didWriteCommand,
|
||||
eventProtocolError,
|
||||
sendStartReplCommand,
|
||||
sendLegacyStartReplCommand,
|
||||
sendLegacyStartUserProgramCommand,
|
||||
sendStartUserProgramCommand,
|
||||
sendStopUserProgramCommand,
|
||||
sendWriteAppDataCommand,
|
||||
sendWriteStdinCommand,
|
||||
sendWriteUserProgramMetaCommand,
|
||||
sendWriteUserRamCommand,
|
||||
writeCommand,
|
||||
} from './actions';
|
||||
import { CommandType, ProtocolError } from './protocol';
|
||||
import { BuiltinProgramId, CommandType, ProtocolError } from './protocol';
|
||||
import blePybricksService from './sagas';
|
||||
|
||||
describe('command encoder', () => {
|
||||
@@ -31,16 +34,24 @@ describe('command encoder', () => {
|
||||
0x00, // stop user program command
|
||||
],
|
||||
],
|
||||
[
|
||||
'start user program with slot',
|
||||
sendStartUserProgramCommand(0, 0x2a),
|
||||
[
|
||||
0x01, // start user program command
|
||||
0x2a, // program slot
|
||||
],
|
||||
],
|
||||
[
|
||||
'start user program',
|
||||
sendStartUserProgramCommand(0),
|
||||
sendLegacyStartUserProgramCommand(0),
|
||||
[
|
||||
0x01, // start user program command
|
||||
],
|
||||
],
|
||||
[
|
||||
'start repl',
|
||||
sendStartReplCommand(0),
|
||||
sendLegacyStartReplCommand(0),
|
||||
[
|
||||
0x02, // start repl command
|
||||
],
|
||||
@@ -82,6 +93,19 @@ describe('command encoder', () => {
|
||||
0x04, // payload end
|
||||
],
|
||||
],
|
||||
[
|
||||
'write AppData',
|
||||
sendWriteAppDataCommand(0, 0x2a, new Uint8Array([1, 2, 3, 4]).buffer),
|
||||
[
|
||||
0x07, // write AppData command
|
||||
0x2a, // offset LSB 16bit
|
||||
0x00, // offset MSB 16bit
|
||||
0x01, // payload start
|
||||
0x02,
|
||||
0x03,
|
||||
0x04, // payload end
|
||||
],
|
||||
],
|
||||
])('encode %s request', async (_n, request, expected) => {
|
||||
const saga = new AsyncSaga(blePybricksService);
|
||||
saga.put(request);
|
||||
@@ -150,7 +174,7 @@ describe('command encoder', () => {
|
||||
describe('event decoder', () => {
|
||||
test.each([
|
||||
[
|
||||
'status report',
|
||||
'legacy status report',
|
||||
[
|
||||
0x00, // status report event
|
||||
0x01, // flags count LSB
|
||||
@@ -158,7 +182,19 @@ describe('event decoder', () => {
|
||||
0x00, // .
|
||||
0x00, // flags count MSB
|
||||
],
|
||||
didReceiveStatusReport(0x00000001),
|
||||
didReceiveStatusReport(0x00000001, 0),
|
||||
],
|
||||
[
|
||||
'status report',
|
||||
[
|
||||
0x00, // status report event
|
||||
0x01, // flags count LSB
|
||||
0x00, // .
|
||||
0x00, // .
|
||||
0x00, // flags count MSB
|
||||
0x80, // slot
|
||||
],
|
||||
didReceiveStatusReport(0x00000001, BuiltinProgramId.REPL),
|
||||
],
|
||||
[
|
||||
'write stdout',
|
||||
@@ -178,17 +214,67 @@ describe('event decoder', () => {
|
||||
]).buffer,
|
||||
),
|
||||
],
|
||||
])('decode %s event', async (_n, message, expected) => {
|
||||
const saga = new AsyncSaga(blePybricksService);
|
||||
const notification = new Uint8Array(message);
|
||||
[
|
||||
'write AppData',
|
||||
[
|
||||
0x02, // write AppData event
|
||||
't'.charCodeAt(0), //payload
|
||||
'e'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
],
|
||||
didReceiveWriteAppData(
|
||||
new Uint8Array([
|
||||
't'.charCodeAt(0),
|
||||
'e'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
]).buffer,
|
||||
),
|
||||
],
|
||||
[
|
||||
'write appdata mismatch',
|
||||
[
|
||||
0x02, // write AppData event
|
||||
't'.charCodeAt(0), //payload
|
||||
'e'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
],
|
||||
didReceiveWriteAppData(
|
||||
new Uint8Array([
|
||||
't'.charCodeAt(0),
|
||||
'e'.charCodeAt(0),
|
||||
'x'.charCodeAt(0),
|
||||
't'.charCodeAt(0),
|
||||
]).buffer,
|
||||
),
|
||||
true,
|
||||
false,
|
||||
],
|
||||
])(
|
||||
'decode %s event',
|
||||
async (_n, message, expected, isEqual = true, isStrictlyEqual = true) => {
|
||||
const saga = new AsyncSaga(blePybricksService);
|
||||
const notification = new Uint8Array(message);
|
||||
|
||||
saga.put(didNotifyEvent(new DataView(notification.buffer)));
|
||||
saga.put(didNotifyEvent(new DataView(notification.buffer)));
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toEqual(expected);
|
||||
const action = await saga.take();
|
||||
if (isEqual) {
|
||||
expect(action).toEqual(expected);
|
||||
} else {
|
||||
expect(action).not.toEqual(expected);
|
||||
}
|
||||
if (isStrictlyEqual) {
|
||||
expect(action).toStrictEqual(expected);
|
||||
} else {
|
||||
expect(action).not.toStrictEqual(expected);
|
||||
}
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
await saga.end();
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
[
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2023 The Pybricks Authors
|
||||
// Copyright (c) 2021-2024 The Pybricks Authors
|
||||
//
|
||||
// Handles Pybricks protocol.
|
||||
|
||||
@@ -18,13 +18,16 @@ import {
|
||||
didFailToWriteCommand,
|
||||
didNotifyEvent,
|
||||
didReceiveStatusReport,
|
||||
didReceiveWriteAppData,
|
||||
didReceiveWriteStdout,
|
||||
didSendCommand,
|
||||
didWriteCommand,
|
||||
eventProtocolError,
|
||||
sendStartReplCommand,
|
||||
sendLegacyStartReplCommand,
|
||||
sendLegacyStartUserProgramCommand,
|
||||
sendStartUserProgramCommand,
|
||||
sendStopUserProgramCommand,
|
||||
sendWriteAppDataCommand,
|
||||
sendWriteStdinCommand,
|
||||
sendWriteUserProgramMetaCommand,
|
||||
sendWriteUserRamCommand,
|
||||
@@ -33,14 +36,17 @@ import {
|
||||
import {
|
||||
EventType,
|
||||
ProtocolError,
|
||||
createStartReplCommand,
|
||||
createLegacyStartReplCommand,
|
||||
createLegacyStartUserProgramCommand,
|
||||
createStartUserProgramCommand,
|
||||
createStopUserProgramCommand,
|
||||
createWriteAppDataCommand,
|
||||
createWriteStdinCommand,
|
||||
createWriteUserProgramMetaCommand,
|
||||
createWriteUserRamCommand,
|
||||
getEventType,
|
||||
parseStatusReport,
|
||||
parseWriteAppData,
|
||||
parseWriteStdout,
|
||||
} from './protocol';
|
||||
|
||||
@@ -63,10 +69,14 @@ function* encodeRequest(): Generator {
|
||||
/* istanbul ignore else: should not be possible to reach */
|
||||
if (sendStopUserProgramCommand.matches(action)) {
|
||||
yield* put(writeCommand(action.id, createStopUserProgramCommand()));
|
||||
} else if (sendLegacyStartUserProgramCommand.matches(action)) {
|
||||
yield* put(writeCommand(action.id, createLegacyStartUserProgramCommand()));
|
||||
} else if (sendLegacyStartReplCommand.matches(action)) {
|
||||
yield* put(writeCommand(action.id, createLegacyStartReplCommand()));
|
||||
} else if (sendStartUserProgramCommand.matches(action)) {
|
||||
yield* put(writeCommand(action.id, createStartUserProgramCommand()));
|
||||
} else if (sendStartReplCommand.matches(action)) {
|
||||
yield* put(writeCommand(action.id, createStartReplCommand()));
|
||||
yield* put(
|
||||
writeCommand(action.id, createStartUserProgramCommand(action.slot)),
|
||||
);
|
||||
} else if (sendWriteUserProgramMetaCommand.matches(action)) {
|
||||
yield* put(
|
||||
writeCommand(action.id, createWriteUserProgramMetaCommand(action.size)),
|
||||
@@ -82,6 +92,13 @@ function* encodeRequest(): Generator {
|
||||
yield* put(
|
||||
writeCommand(action.id, createWriteStdinCommand(action.payload)),
|
||||
);
|
||||
} else if (sendWriteAppDataCommand.matches(action)) {
|
||||
yield* put(
|
||||
writeCommand(
|
||||
action.id,
|
||||
createWriteAppDataCommand(action.offset, action.payload),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.error(`Unknown Pybricks service command ${action.type}`);
|
||||
continue;
|
||||
@@ -108,12 +125,17 @@ function* decodeResponse(action: ReturnType<typeof didNotifyEvent>): Generator {
|
||||
try {
|
||||
const responseType = getEventType(action.value);
|
||||
switch (responseType) {
|
||||
case EventType.StatusReport:
|
||||
yield* put(didReceiveStatusReport(parseStatusReport(action.value)));
|
||||
case EventType.StatusReport: {
|
||||
const status = parseStatusReport(action.value);
|
||||
yield* put(didReceiveStatusReport(status.flags, status.slot));
|
||||
break;
|
||||
}
|
||||
case EventType.WriteStdout:
|
||||
yield* put(didReceiveWriteStdout(parseWriteStdout(action.value)));
|
||||
break;
|
||||
case EventType.WriteAppData:
|
||||
yield* put(didReceiveWriteAppData(parseWriteAppData(action.value)));
|
||||
break;
|
||||
default:
|
||||
throw new ProtocolError(
|
||||
`unknown pybricks event type: ${hex(responseType, 2)}`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
// Copyright (c) 2021-2024 The Pybricks Authors
|
||||
|
||||
import { AnyAction } from 'redux';
|
||||
import {
|
||||
@@ -130,14 +130,14 @@ test('deviceLowBatteryWarning', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ deviceLowBatteryWarning: false } as State,
|
||||
didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning)),
|
||||
didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning), 0),
|
||||
).deviceLowBatteryWarning,
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
reducers(
|
||||
{ deviceLowBatteryWarning: true } as State,
|
||||
didReceiveStatusReport(~statusToFlag(Status.BatteryLowVoltageWarning)),
|
||||
didReceiveStatusReport(~statusToFlag(Status.BatteryLowVoltageWarning), 0),
|
||||
).deviceLowBatteryWarning,
|
||||
).toBeFalsy();
|
||||
|
||||
|
||||
+1
-1
@@ -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.3.0';
|
||||
export const supportedPybricksProfileVersion = '1.4.0';
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
|
||||
+28
-7
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
// Copyright (c) 2020-2024 The Pybricks Authors
|
||||
|
||||
import { createAction } from '../actions';
|
||||
import { FileFormat } from '../ble-pybricks-service/protocol';
|
||||
@@ -14,11 +14,25 @@ export const checksum = createAction((checksum: number) => ({
|
||||
|
||||
// High-level hub actions.
|
||||
|
||||
/**
|
||||
* Request to download and run a program on the hub.
|
||||
* @param fileFormat The format of the file to download.
|
||||
* @param useLegacyDownload Whether to use the legacy NUS download method.
|
||||
* @param useLegacyStartUserProgram Whether to use the legacy start user program method (changed in protocol v1.4.0).
|
||||
* @param slot The slot number to download the program to - only valid on hubs/firmware that support multiple slots.
|
||||
*/
|
||||
export const downloadAndRun = createAction(
|
||||
(fileFormat: FileFormat | null, useLegacyDownload: boolean) => ({
|
||||
(
|
||||
fileFormat: FileFormat | null,
|
||||
useLegacyDownload: boolean,
|
||||
useLegacyStartUserProgram: boolean,
|
||||
slot: number,
|
||||
) => ({
|
||||
type: 'hub.action.downloadAndRun',
|
||||
fileFormat,
|
||||
useLegacyDownload,
|
||||
useLegacyStartUserProgram,
|
||||
slot,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -54,11 +68,18 @@ export const hubDidFailToStopUserProgram = createAction(() => ({
|
||||
type: 'hub.action.didFailToStopUserProgram',
|
||||
}));
|
||||
|
||||
/** Request to send the start repl command to the hub. */
|
||||
export const hubStartRepl = createAction((useLegacyDownload: boolean) => ({
|
||||
type: 'hub.action.startRepl',
|
||||
useLegacyDownload,
|
||||
}));
|
||||
/**
|
||||
* Request to send the start repl command to the hub.
|
||||
* @param useLegacyDownload Whether to use the legacy NUS download method.
|
||||
* @param useLegacyStartUserProgram Whether to use the legacy start user program method (changed in protocol v1.4.0).
|
||||
*/
|
||||
export const hubStartRepl = createAction(
|
||||
(useLegacyDownload: boolean, useLegacyStartUserProgram: boolean) => ({
|
||||
type: 'hub.action.startRepl',
|
||||
useLegacyDownload,
|
||||
useLegacyStartUserProgram,
|
||||
}),
|
||||
);
|
||||
|
||||
/** Indicates the the start repl command was sent to the hub. */
|
||||
export const hubDidStartRepl = createAction(() => ({
|
||||
|
||||
+33
-10
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2023 The Pybricks Authors
|
||||
// Copyright (c) 2021-2024 The Pybricks Authors
|
||||
|
||||
import { AnyAction } from 'redux';
|
||||
import {
|
||||
@@ -47,6 +47,7 @@ test('initial state', () => {
|
||||
"preferredFileFormat": null,
|
||||
"runtime": "hub.runtime.disconnected",
|
||||
"useLegacyDownload": false,
|
||||
"useLegacyStartUserProgram": false,
|
||||
"useLegacyStdio": false,
|
||||
}
|
||||
`);
|
||||
@@ -157,7 +158,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Disconnected } as State,
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning)),
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Disconnected);
|
||||
|
||||
@@ -165,7 +166,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Loading } as State,
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning)),
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Loading);
|
||||
|
||||
@@ -173,7 +174,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Unknown } as State,
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning)),
|
||||
didReceiveStatusReport(statusToFlag(Status.UserProgramRunning), 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Running);
|
||||
|
||||
@@ -181,7 +182,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Unknown } as State,
|
||||
didReceiveStatusReport(0),
|
||||
didReceiveStatusReport(0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Idle);
|
||||
|
||||
@@ -189,7 +190,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Running } as State,
|
||||
didReceiveStatusReport(0),
|
||||
didReceiveStatusReport(0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Idle);
|
||||
|
||||
@@ -197,7 +198,7 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.StartingRepl } as State,
|
||||
didReceiveStatusReport(0),
|
||||
didReceiveStatusReport(0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.StartingRepl);
|
||||
|
||||
@@ -205,15 +206,17 @@ describe('runtime', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.StoppingUserProgram } as State,
|
||||
didReceiveStatusReport(0),
|
||||
didReceiveStatusReport(0, 0),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.StoppingUserProgram);
|
||||
});
|
||||
|
||||
test('hubStartRepl', () => {
|
||||
expect(
|
||||
reducers({ runtime: HubRuntimeState.Running } as State, hubStartRepl(false))
|
||||
.runtime,
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Running } as State,
|
||||
hubStartRepl(false, false),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.StartingRepl);
|
||||
});
|
||||
|
||||
@@ -425,3 +428,23 @@ describe('useLegacyStdio', () => {
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLegacyStartUserProgram', () => {
|
||||
test('Pybricks Profile < v1.4.0', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ useLegacyStartUserProgram: false } as State,
|
||||
bleDIServiceDidReceiveSoftwareRevision('1.3.0'),
|
||||
).useLegacyStartUserProgram,
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Pybricks Profile >= v1.4.0', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ useLegacyStartUserProgram: true } as State,
|
||||
bleDIServiceDidReceiveSoftwareRevision('1.4.0'),
|
||||
).useLegacyStartUserProgram,
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
+14
-1
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
// Copyright (c) 2020-2024 The Pybricks Authors
|
||||
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import * as semver from 'semver';
|
||||
@@ -263,6 +263,18 @@ const useLegacyStdio: Reducer<boolean> = (state = false, action) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* When true, use Legacy StartUserProgram.
|
||||
*/
|
||||
const useLegacyStartUserProgram: Reducer<boolean> = (state = false, action) => {
|
||||
if (bleDIServiceDidReceiveSoftwareRevision.matches(action)) {
|
||||
// Behavior changed starting with Pybricks Profile v1.4.0.
|
||||
return !semver.satisfies(action.version, '^1.4.0');
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
runtime,
|
||||
downloadProgress,
|
||||
@@ -272,4 +284,5 @@ export default combineReducers({
|
||||
preferredFileFormat,
|
||||
useLegacyDownload,
|
||||
useLegacyStdio,
|
||||
useLegacyStartUserProgram,
|
||||
});
|
||||
|
||||
+28
-9
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
// Copyright (c) 2020-2024 The Pybricks Authors
|
||||
|
||||
import { AsyncSaga } from '../../test';
|
||||
import { alertsShowAlert } from '../alerts/actions';
|
||||
@@ -7,10 +7,11 @@ import { didWrite, write } from '../ble-nordic-uart-service/actions';
|
||||
import {
|
||||
didFailToSendCommand,
|
||||
didSendCommand,
|
||||
sendStartReplCommand,
|
||||
sendLegacyStartReplCommand,
|
||||
sendStartUserProgramCommand,
|
||||
sendStopUserProgramCommand,
|
||||
} from '../ble-pybricks-service/actions';
|
||||
import { FileFormat } from '../ble-pybricks-service/protocol';
|
||||
import { BuiltinProgramId, FileFormat } from '../ble-pybricks-service/protocol';
|
||||
import { editorGetValueRequest, editorGetValueResponse } from '../editor/actions';
|
||||
import { compile, didCompile } from '../mpy/actions';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
@@ -37,7 +38,7 @@ describe('downloadAndRun', () => {
|
||||
|
||||
saga.updateState({ editor: { isReady: true } });
|
||||
|
||||
saga.put(downloadAndRun(FileFormat.Mpy5, true));
|
||||
saga.put(downloadAndRun(FileFormat.Mpy5, true, true, 0));
|
||||
|
||||
// first, it gets the value from the current editor
|
||||
const editorValueAction = await saga.take();
|
||||
@@ -94,7 +95,7 @@ describe('hubStartRepl', () => {
|
||||
test('legacy UART download', async () => {
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(hubStartRepl(true));
|
||||
saga.put(hubStartRepl(true, true));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
write(0, new Uint8Array([32, 32, 32, 32])),
|
||||
@@ -105,12 +106,28 @@ describe('hubStartRepl', () => {
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('legacy start REPL command', async () => {
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(hubStartRepl(false, true));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendLegacyStartReplCommand(0));
|
||||
|
||||
saga.put(didSendCommand(0));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(hubDidStartRepl());
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('success', async () => {
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(hubStartRepl(false));
|
||||
saga.put(hubStartRepl(false, false));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendStartReplCommand(0));
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
sendStartUserProgramCommand(0, BuiltinProgramId.REPL),
|
||||
);
|
||||
|
||||
saga.put(didSendCommand(0));
|
||||
|
||||
@@ -122,9 +139,11 @@ describe('hubStartRepl', () => {
|
||||
test('failure due to disconnect', async () => {
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(hubStartRepl(false));
|
||||
saga.put(hubStartRepl(false, false));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendStartReplCommand(0));
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
sendStartUserProgramCommand(0, BuiltinProgramId.REPL),
|
||||
);
|
||||
|
||||
saga.put(didFailToSendCommand(0, new DOMException('test', 'NetworkError')));
|
||||
|
||||
|
||||
+17
-8
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
// Copyright (c) 2020-2024 The Pybricks Authors
|
||||
|
||||
import {
|
||||
SagaGenerator,
|
||||
@@ -19,13 +19,14 @@ import { nordicUartSafeTxCharLength } from '../ble-nordic-uart-service/protocol'
|
||||
import {
|
||||
didFailToSendCommand,
|
||||
didSendCommand,
|
||||
sendStartReplCommand,
|
||||
sendLegacyStartReplCommand,
|
||||
sendLegacyStartUserProgramCommand,
|
||||
sendStartUserProgramCommand,
|
||||
sendStopUserProgramCommand,
|
||||
sendWriteUserProgramMetaCommand,
|
||||
sendWriteUserRamCommand,
|
||||
} from '../ble-pybricks-service/actions';
|
||||
import { FileFormat } from '../ble-pybricks-service/protocol';
|
||||
import { BuiltinProgramId, FileFormat } from '../ble-pybricks-service/protocol';
|
||||
import { editorGetValue } from '../editor/sagaLib';
|
||||
import {
|
||||
compile,
|
||||
@@ -311,7 +312,11 @@ function* handleDownloadAndRun(action: ReturnType<typeof downloadAndRun>): Gener
|
||||
yield* put(didFinishDownload());
|
||||
|
||||
const startUserProgramId = nextMessageId();
|
||||
yield* put(sendStartUserProgramCommand(startUserProgramId));
|
||||
if (action.useLegacyStartUserProgram) {
|
||||
yield* put(sendLegacyStartUserProgramCommand(startUserProgramId));
|
||||
} else {
|
||||
yield* put(sendStartUserProgramCommand(startUserProgramId, action.slot));
|
||||
}
|
||||
|
||||
const { didFailToStart } = yield* race({
|
||||
didStart: take(didSendCommand.when((a) => a.id === startUserProgramId)),
|
||||
@@ -344,22 +349,26 @@ function* handleDownloadAndRun(action: ReturnType<typeof downloadAndRun>): Gener
|
||||
}
|
||||
|
||||
// SPACE, SPACE, SPACE, SPACE
|
||||
const legacyStartReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
|
||||
const legacyNusStartReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
|
||||
|
||||
function* handleHubStartRepl(action: ReturnType<typeof hubStartRepl>): Generator {
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
|
||||
if (action.useLegacyDownload) {
|
||||
yield* put(write(nextMessageId(), legacyStartReplCommand));
|
||||
yield* put(write(nextMessageId(), legacyNusStartReplCommand));
|
||||
yield* put(hubDidStartRepl());
|
||||
return;
|
||||
}
|
||||
|
||||
const id = nextMessageId();
|
||||
yield* put(sendStartReplCommand(id));
|
||||
if (action.useLegacyStartUserProgram) {
|
||||
yield* put(sendLegacyStartReplCommand(id));
|
||||
} else {
|
||||
yield* put(sendStartUserProgramCommand(id, BuiltinProgramId.REPL));
|
||||
}
|
||||
|
||||
const { didFailToSend } = yield* race({
|
||||
didStart: take(didSendCommand.when((a) => a.id === id)),
|
||||
didSend: take(didSendCommand.when((a) => a.id === id)),
|
||||
didFailToSend: take(didFailToSendCommand.when((a) => a.id === id)),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022-2023 The Pybricks Authors
|
||||
// Copyright (c) 2022-2024 The Pybricks Authors
|
||||
|
||||
import { act, cleanup } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
@@ -13,17 +13,27 @@ afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should dispatch action when clicked', async () => {
|
||||
const [user, button, dispatch] = testRender(
|
||||
<FocusScope>
|
||||
<ReplButton id="test-repl-button" />
|
||||
</FocusScope>,
|
||||
{
|
||||
hub: { hasRepl: true, runtime: HubRuntimeState.Idle },
|
||||
},
|
||||
);
|
||||
test.each([[false, false, false, true, true, true]])(
|
||||
'should dispatch action when clicked',
|
||||
async (legacyDownload: boolean, legacyStartUserProgram: boolean) => {
|
||||
const [user, button, dispatch] = testRender(
|
||||
<FocusScope>
|
||||
<ReplButton id="test-repl-button" />
|
||||
</FocusScope>,
|
||||
{
|
||||
hub: {
|
||||
hasRepl: true,
|
||||
runtime: HubRuntimeState.Idle,
|
||||
useLegacyDownload: legacyDownload,
|
||||
useLegacyStartUserProgram: legacyStartUserProgram,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await act(() => user.click(button.getByRole('button', { name: 'REPL' })));
|
||||
await act(() => user.click(button.getByRole('button', { name: 'REPL' })));
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(hubStartRepl(false));
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
hubStartRepl(legacyDownload, legacyStartUserProgram),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
// Copyright (c) 2020-2024 The Pybricks Authors
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -14,13 +14,14 @@ import icon from './icon.svg';
|
||||
type ReplButtonProps = Pick<ActionButtonProps, 'id'>;
|
||||
|
||||
const ReplButton: React.FunctionComponent<ReplButtonProps> = ({ id }) => {
|
||||
const { runtime, useLegacyDownload, hasRepl } = useSelector((s) => s.hub);
|
||||
const { runtime, useLegacyDownload, useLegacyStartUserProgram, hasRepl } =
|
||||
useSelector((s) => s.hub);
|
||||
const i18n = useI18n();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const action = useCallback(
|
||||
() => dispatch(hubStartRepl(useLegacyDownload)),
|
||||
[dispatch, useLegacyDownload],
|
||||
() => dispatch(hubStartRepl(useLegacyDownload, useLegacyStartUserProgram)),
|
||||
[dispatch, useLegacyDownload, useLegacyStartUserProgram],
|
||||
);
|
||||
|
||||
const busy = useDebounce(runtime === HubRuntimeState.StartingRepl, 250);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022-2023 The Pybricks Authors
|
||||
// Copyright (c) 2022-2024 The Pybricks Authors
|
||||
|
||||
import { act, cleanup } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
@@ -14,21 +14,37 @@ afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should dispatch action when clicked', async () => {
|
||||
const [user, button, dispatch] = testRender(
|
||||
<FocusScope>
|
||||
<RunButton id="test-run-button" />
|
||||
</FocusScope>,
|
||||
{
|
||||
editor: { activeFileUuid: uuid(0) },
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Idle,
|
||||
preferredFileFormat: FileFormat.MultiMpy6,
|
||||
test.each([
|
||||
[false, false],
|
||||
[false, true],
|
||||
[true, true],
|
||||
])(
|
||||
'should dispatch action when clicked',
|
||||
async (legacyDownload: boolean, legacyStartUserProgram: boolean) => {
|
||||
const [user, button, dispatch] = testRender(
|
||||
<FocusScope>
|
||||
<RunButton id="test-run-button" />
|
||||
</FocusScope>,
|
||||
{
|
||||
editor: { activeFileUuid: uuid(0) },
|
||||
hub: {
|
||||
runtime: HubRuntimeState.Idle,
|
||||
preferredFileFormat: FileFormat.MultiMpy6,
|
||||
useLegacyDownload: legacyDownload,
|
||||
useLegacyStartUserProgram: legacyStartUserProgram,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
await act(() => user.click(button.getByRole('button', { name: 'Run' })));
|
||||
await act(() => user.click(button.getByRole('button', { name: 'Run' })));
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(downloadAndRun(FileFormat.MultiMpy6, false));
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
downloadAndRun(
|
||||
FileFormat.MultiMpy6,
|
||||
legacyDownload,
|
||||
legacyStartUserProgram,
|
||||
0,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
// Copyright (c) 2020-2024 The Pybricks Authors
|
||||
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -13,8 +13,13 @@ import icon from './icon.svg';
|
||||
type RunButtonProps = Pick<ActionButtonProps, 'id'>;
|
||||
|
||||
const RunButton: React.FunctionComponent<RunButtonProps> = ({ id }) => {
|
||||
const { downloadProgress, preferredFileFormat, runtime, useLegacyDownload } =
|
||||
useSelector((s) => s.hub);
|
||||
const {
|
||||
downloadProgress,
|
||||
preferredFileFormat,
|
||||
runtime,
|
||||
useLegacyDownload,
|
||||
useLegacyStartUserProgram,
|
||||
} = useSelector((s) => s.hub);
|
||||
const activeFile = useSelector((s) => s.editor.activeFileUuid);
|
||||
const keyboardShortcut = 'F5';
|
||||
|
||||
@@ -38,7 +43,14 @@ const RunButton: React.FunctionComponent<RunButtonProps> = ({ id }) => {
|
||||
showProgress={runtime === HubRuntimeState.Loading}
|
||||
progress={downloadProgress === null ? undefined : downloadProgress}
|
||||
onAction={() =>
|
||||
dispatch(downloadAndRun(preferredFileFormat, useLegacyDownload))
|
||||
dispatch(
|
||||
downloadAndRun(
|
||||
preferredFileFormat,
|
||||
useLegacyDownload,
|
||||
useLegacyStartUserProgram,
|
||||
0, // No slot UI yet
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user