Skip to content

Commit 9e3eb9b

Browse files
committed
feat: implemented pybricks protocol 1.4
chore: added ble-pybricks-service tests fix: improved ArrayBuffer deep equality check
1 parent 482a554 commit 9e3eb9b

File tree

5 files changed

+205
-14
lines changed

5 files changed

+205
-14
lines changed

src/ble-pybricks-service/actions.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,15 @@ export const sendStopUserProgramCommand = createAction((id: number) => ({
5959
* Action that requests a start user program to be sent.
6060
* @param id Unique identifier for this transaction.
6161
*
62-
* @since Pybricks Profile v1.2.0
62+
* @since Pybricks Profile v1.2.0. Program identifier added in Pybricks Profile v1.4.0.
6363
*/
64-
export const sendStartUserProgramCommand = createAction((id: number) => ({
65-
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
66-
id,
67-
}));
64+
export const sendStartUserProgramCommand = createAction(
65+
(id: number, slot?: number) => ({
66+
type: 'blePybricksServiceCommand.action.sendStartUserProgram',
67+
id,
68+
slot,
69+
}),
70+
);
6871

6972
/**
7073
* Action that requests a start interactive REPL to be sent.
@@ -124,6 +127,23 @@ export const sendWriteStdinCommand = createAction(
124127
}),
125128
);
126129

130+
/**
131+
* Action that requests to write to appdata.
132+
* @param id Unique identifier for this transaction.
133+
* @param offset offset: The offset from the buffer base address
134+
* @param payload The bytes to write.
135+
*
136+
* @since Pybricks Profile v1.4.0.
137+
*/
138+
export const sendWriteAppDataCommand = createAction(
139+
(id: number, offset: number, payload: ArrayBuffer) => ({
140+
type: 'blePybricksServiceCommand.action.sendWriteAppDataCommand',
141+
id,
142+
offset,
143+
payload,
144+
}),
145+
);
146+
127147
/**
128148
* Action that indicates that a command was successfully sent.
129149
* @param id Unique identifier for the transaction from the corresponding "send" command.
@@ -157,7 +177,7 @@ export const didReceiveStatusReport = createAction((statusFlags: number) => ({
157177

158178
/**
159179
* Action that represents a status report event received from the hub.
160-
* @param statusFlags The status flags.
180+
* @param payload The piece of message received.
161181
*
162182
* @since Pybricks Profile v1.3.0
163183
*/
@@ -166,6 +186,17 @@ export const didReceiveWriteStdout = createAction((payload: ArrayBuffer) => ({
166186
payload,
167187
}));
168188

189+
/**
190+
* Action that represents a write to a buffer that is pre-allocated by a user program received from the hub.
191+
* @param payload The piece of message received.
192+
*
193+
* @since Pybricks Profile v1.4.0
194+
*/
195+
export const didReceiveWriteAppData = createAction((payload: ArrayBuffer) => ({
196+
type: 'blePybricksServiceEvent.action.didReceiveWriteAppData',
197+
payload,
198+
}));
199+
169200
/**
170201
* Pseudo-event = actionCreator((not received from hub) indicating that there was a protocol error.
171202
* @param error The error that was caught.

src/ble-pybricks-service/protocol.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ export enum CommandType {
5858
* @since Pybricks Profile v1.3.0
5959
*/
6060
WriteStdin = 6,
61+
/**
62+
* Requests to write to a buffer that is pre-allocated by a user program.
63+
*
64+
* Parameters:
65+
* - offset: The offset from the buffer base address (16-bit little-endian
66+
* unsigned integer).
67+
* - payload: The data to write.
68+
*
69+
* @since Pybricks Profile v1.4.0
70+
*/
71+
WriteAppData = 7,
6172
}
6273

6374
/**
@@ -74,11 +85,22 @@ export function createStopUserProgramCommand(): Uint8Array {
7485
/**
7586
* Creates a {@link CommandType.StartUserProgram} message.
7687
*
77-
* @since Pybricks Profile v1.2.0
88+
* The optional payload parameter was added in Pybricks Profile v1.4.0.
89+
*
90+
* Parameters:
91+
* - payload: Optional program identifier (one byte). Slots 0--127 are
92+
* reserved for downloaded user programs. Slots 128--255 are
93+
* for builtin user programs. If no program identifier is
94+
* given, the currently active program slot will be started.
95+
*
96+
* @since Pybricks Profile v1.2.0. Program identifier added in Pybricks Profile v1.4.0.
7897
*/
79-
export function createStartUserProgramCommand(): Uint8Array {
80-
const msg = new Uint8Array(1);
98+
export function createStartUserProgramCommand(slot: number | undefined): Uint8Array {
99+
const msg = new Uint8Array(slot === undefined ? 1 : 2);
81100
msg[0] = CommandType.StartUserProgram;
101+
if (slot !== undefined) {
102+
msg[1] = slot & 0xff;
103+
}
82104
return msg;
83105
}
84106

@@ -140,6 +162,25 @@ export function createWriteStdinCommand(payload: ArrayBuffer): Uint8Array {
140162
return msg;
141163
}
142164

165+
/**
166+
* Creates a {@link CommandType.WriteAppData} message.
167+
* @param offset The offset from the buffer base address
168+
* @param payload The bytes to write.
169+
*
170+
* @since Pybricks Profile v1.4.0.
171+
*/
172+
export function createWriteAppDataCommand(
173+
offset: number,
174+
payload: ArrayBuffer,
175+
): Uint8Array {
176+
const msg = new Uint8Array(1 + 2 + payload.byteLength);
177+
const view = new DataView(msg.buffer);
178+
view.setUint8(0, CommandType.WriteAppData);
179+
view.setUint16(1, offset & 0xffff);
180+
msg.set(new Uint8Array(payload), 3);
181+
return msg;
182+
}
183+
143184
/** Events are notifications received from the hub. */
144185
export enum EventType {
145186
/**
@@ -156,6 +197,12 @@ export enum EventType {
156197
* @since Pybricks Profile v1.3.0
157198
*/
158199
WriteStdout = 1,
200+
/**
201+
* Hub wrote to appdata event.
202+
*
203+
* @since Pybricks Profile v1.4.0
204+
*/
205+
WriteAppData = 2,
159206
}
160207

161208
/** Status indications received by Event.StatusReport */
@@ -244,6 +291,18 @@ export function parseWriteStdout(msg: DataView): ArrayBuffer {
244291
return msg.buffer.slice(1);
245292
}
246293

294+
/**
295+
* Parses the payload of a app data message.
296+
* @param msg The raw message data.
297+
* @returns The bytes that were written.
298+
*
299+
* @since Pybricks Profile v1.4.0
300+
*/
301+
export function parseWriteAppData(msg: DataView): ArrayBuffer {
302+
assert(msg.getUint8(0) === EventType.WriteAppData, 'expecting write appdata event');
303+
return msg.buffer.slice(1);
304+
}
305+
247306
/**
248307
* Protocol error. Thrown e.g. when there is a malformed message.
249308
*/
@@ -285,6 +344,20 @@ export enum HubCapabilityFlag {
285344
* @since Pybricks Profile v1.3.0
286345
*/
287346
UserProgramMultiMpy6Native6p1 = 1 << 2,
347+
348+
/**
349+
* Hub supports builtin sensor port view monitoring program.
350+
*
351+
* @since Pybricks Profile v1.4.0.
352+
*/
353+
HasPortView = 1 << 3,
354+
355+
/**
356+
* Hub supports builtin IMU calibration program.
357+
*
358+
* @since Pybricks Profile v1.4.0.
359+
*/
360+
HasIMUCalibration = 1 << 4,
288361
}
289362

290363
/** Supported user program file formats. */

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

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import {
77
didFailToWriteCommand,
88
didNotifyEvent,
99
didReceiveStatusReport,
10+
didReceiveWriteAppData,
1011
didReceiveWriteStdout,
1112
didSendCommand,
1213
didWriteCommand,
1314
eventProtocolError,
1415
sendStartReplCommand,
1516
sendStartUserProgramCommand,
1617
sendStopUserProgramCommand,
18+
sendWriteAppDataCommand,
1719
sendWriteStdinCommand,
1820
sendWriteUserProgramMetaCommand,
1921
sendWriteUserRamCommand,
@@ -38,6 +40,14 @@ describe('command encoder', () => {
3840
0x01, // start user program command
3941
],
4042
],
43+
[
44+
'start user program with slot',
45+
sendStartUserProgramCommand(0, 0x2A),
46+
[
47+
0x01, // start user program command
48+
0x2A, // program slot
49+
],
50+
],
4151
[
4252
'start repl',
4353
sendStartReplCommand(0),
@@ -82,6 +92,19 @@ describe('command encoder', () => {
8292
0x04, // payload end
8393
],
8494
],
95+
[
96+
'write appdata',
97+
sendWriteAppDataCommand(0, 0x2A, new Uint8Array([1, 2, 3, 4]).buffer),
98+
[
99+
0x07, // write appdata command
100+
0x00, // offset msb 16bit
101+
0x2A, // offset lsb 16bit
102+
0x01, // payload start
103+
0x02,
104+
0x03,
105+
0x04, // payload end
106+
],
107+
],
85108
])('encode %s request', async (_n, request, expected) => {
86109
const saga = new AsyncSaga(blePybricksService);
87110
saga.put(request);
@@ -178,14 +201,61 @@ describe('event decoder', () => {
178201
]).buffer,
179202
),
180203
],
181-
])('decode %s event', async (_n, message, expected) => {
204+
[
205+
'write appdata',
206+
[
207+
0x02, // write appdata event
208+
't'.charCodeAt(0), //payload
209+
'e'.charCodeAt(0),
210+
't'.charCodeAt(0),
211+
't'.charCodeAt(0),
212+
],
213+
didReceiveWriteAppData(
214+
new Uint8Array([
215+
't'.charCodeAt(0),
216+
'e'.charCodeAt(0),
217+
't'.charCodeAt(0),
218+
't'.charCodeAt(0),
219+
]).buffer,
220+
),
221+
],
222+
[
223+
'write appdata mismatch',
224+
[
225+
0x02, // write appdata event
226+
't'.charCodeAt(0), //payload
227+
'e'.charCodeAt(0),
228+
't'.charCodeAt(0),
229+
't'.charCodeAt(0),
230+
],
231+
didReceiveWriteAppData(
232+
new Uint8Array([
233+
't'.charCodeAt(0),
234+
'e'.charCodeAt(0),
235+
'x'.charCodeAt(0),
236+
't'.charCodeAt(0),
237+
]).buffer,
238+
),
239+
true,
240+
false
241+
],
242+
])('decode %s event', async (_n, message, expected, isEqual = true, isStrictlyEqual = true) => {
182243
const saga = new AsyncSaga(blePybricksService);
183244
const notification = new Uint8Array(message);
184245

185246
saga.put(didNotifyEvent(new DataView(notification.buffer)));
186247

187248
const action = await saga.take();
188-
expect(action).toEqual(expected);
249+
if (isEqual) {
250+
expect(action).toEqual(expected);
251+
} else {
252+
expect(action).not.toEqual(expected);
253+
}
254+
if (isStrictlyEqual) {
255+
expect(action).toStrictEqual(expected);
256+
} else {
257+
expect(action).not.toStrictEqual(expected);
258+
}
189259

190260
await saga.end();
191261
});

src/ble-pybricks-service/sagas.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ import {
1818
didFailToWriteCommand,
1919
didNotifyEvent,
2020
didReceiveStatusReport,
21+
didReceiveWriteAppData,
2122
didReceiveWriteStdout,
2223
didSendCommand,
2324
didWriteCommand,
2425
eventProtocolError,
2526
sendStartReplCommand,
2627
sendStartUserProgramCommand,
2728
sendStopUserProgramCommand,
29+
sendWriteAppDataCommand,
2830
sendWriteStdinCommand,
2931
sendWriteUserProgramMetaCommand,
3032
sendWriteUserRamCommand,
@@ -36,11 +38,13 @@ import {
3638
createStartReplCommand,
3739
createStartUserProgramCommand,
3840
createStopUserProgramCommand,
41+
createWriteAppDataCommand,
3942
createWriteStdinCommand,
4043
createWriteUserProgramMetaCommand,
4144
createWriteUserRamCommand,
4245
getEventType,
4346
parseStatusReport,
47+
parseWriteAppData,
4448
parseWriteStdout,
4549
} from './protocol';
4650

@@ -57,14 +61,17 @@ function* encodeRequest(): Generator {
5761
a.type.startsWith('blePybricksServiceCommand.action.send'),
5862
);
5963

60-
for (;;) {
64+
65+
for (; ;) {
6166
const action = yield* take(chan);
6267

6368
/* istanbul ignore else: should not be possible to reach */
6469
if (sendStopUserProgramCommand.matches(action)) {
6570
yield* put(writeCommand(action.id, createStopUserProgramCommand()));
6671
} else if (sendStartUserProgramCommand.matches(action)) {
67-
yield* put(writeCommand(action.id, createStartUserProgramCommand()));
72+
yield* put(
73+
writeCommand(action.id, createStartUserProgramCommand(action.slot)),
74+
);
6875
} else if (sendStartReplCommand.matches(action)) {
6976
yield* put(writeCommand(action.id, createStartReplCommand()));
7077
} else if (sendWriteUserProgramMetaCommand.matches(action)) {
@@ -82,6 +89,13 @@ function* encodeRequest(): Generator {
8289
yield* put(
8390
writeCommand(action.id, createWriteStdinCommand(action.payload)),
8491
);
92+
} else if (sendWriteAppDataCommand.matches(action)) {
93+
yield* put(
94+
writeCommand(
95+
action.id,
96+
createWriteAppDataCommand(action.offset, action.payload),
97+
),
98+
);
8599
} else {
86100
console.error(`Unknown Pybricks service command ${action.type}`);
87101
continue;
@@ -114,6 +128,9 @@ function* decodeResponse(action: ReturnType<typeof didNotifyEvent>): Generator {
114128
case EventType.WriteStdout:
115129
yield* put(didReceiveWriteStdout(parseWriteStdout(action.value)));
116130
break;
131+
case EventType.WriteAppData:
132+
yield* put(didReceiveWriteAppData(parseWriteAppData(action.value)));
133+
break;
117134
default:
118135
throw new ProtocolError(
119136
`unknown pybricks event type: ${hex(responseType, 2)}`,

src/ble/sagas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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.3.0';
76+
export const supportedPybricksProfileVersion = '1.4.0';
7777

7878
const decoder = new TextDecoder();
7979

0 commit comments

Comments
 (0)