Skip to content

Commit 3d1d6fc

Browse files
authored
RSDK-7046 Add Stream Ticks to Board (#267)
1 parent 7620980 commit 3d1d6fc

File tree

4 files changed

+183
-3
lines changed

4 files changed

+183
-3
lines changed

src/components/board.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { type Board, type Duration, PowerMode } from './board/board';
1+
export { type Board, type Duration, PowerMode, type Tick } from './board/board';
22
export { BoardClient } from './board/client';

src/components/board/board.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ interface Status {
1010
type ValueOf<T> = T[keyof T];
1111
export const { PowerMode } = pb;
1212
export type PowerMode = ValueOf<typeof pb.PowerMode>;
13-
13+
export interface Tick {
14+
pinName: string;
15+
high: boolean;
16+
time: number;
17+
}
1418
export type Duration = PBDuration.AsObject;
1519

1620
/**
@@ -89,6 +93,17 @@ export interface Board extends Resource {
8993
digitalInterruptName: string,
9094
extra?: StructType
9195
): Promise<number>;
96+
/**
97+
* Stream digital interrupt ticks on the board.
98+
*
99+
* @param interrupts - Names of the interrupts to stream.
100+
* @param queue - Array to put the ticks in.
101+
*/
102+
streamTicks(
103+
interrupts: string[],
104+
queue: Tick[],
105+
extra?: StructType
106+
): Promise<void>;
92107
/**
93108
* Set power mode of the board.
94109
*

src/components/board/client.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// @vitest-environment happy-dom
2+
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { BoardClient } from './client';
5+
import { type Tick } from './board';
6+
import { EventDispatcher } from '../../events';
7+
import { type ResponseStream } from '../../gen/robot/v1/robot_pb_service';
8+
import { RobotClient } from '../../robot';
9+
vi.mock('../../robot');
10+
import { StreamTicksResponse } from '../../gen/component/board/v1/board_pb';
11+
12+
import { BoardServiceClient } from '../../gen/component/board/v1/board_pb_service';
13+
vi.mock('../../gen/component/board/v1/board_pb_service');
14+
15+
let board: BoardClient;
16+
17+
export class TestResponseStream<T> extends EventDispatcher {
18+
private stream: ResponseStream<any>;
19+
20+
constructor(stream: ResponseStream<any>) {
21+
super();
22+
this.stream = stream;
23+
}
24+
25+
override on(
26+
type: string,
27+
handler: (message: any) => void
28+
): ResponseStream<T> {
29+
super.on(type, handler);
30+
return this;
31+
}
32+
33+
cancel(): void {
34+
this.listeners = {};
35+
this.stream.cancel();
36+
}
37+
}
38+
39+
let tickStream: ResponseStream<StreamTicksResponse>;
40+
let testTickStream: TestResponseStream<StreamTicksResponse> | undefined;
41+
42+
const tickStreamMock = ():
43+
| TestResponseStream<StreamTicksResponse>
44+
| undefined => {
45+
return testTickStream;
46+
};
47+
48+
beforeEach(() => {
49+
testTickStream = new TestResponseStream(tickStream);
50+
RobotClient.prototype.createServiceClient = vi
51+
.fn()
52+
.mockImplementation(() => new BoardServiceClient('board'));
53+
54+
BoardServiceClient.prototype.streamTicks = vi
55+
.fn()
56+
.mockImplementation(tickStreamMock);
57+
board = new BoardClient(new RobotClient('host'), 'test-board');
58+
});
59+
60+
afterEach(() => {
61+
testTickStream = undefined;
62+
});
63+
64+
describe('streamTicks tests', () => {
65+
it('streamTicks', () => {
66+
const ticks: Tick[] = [];
67+
board.streamTicks(['1', '2'], ticks);
68+
69+
const response1 = new StreamTicksResponse();
70+
response1.setPinName('1');
71+
response1.setHigh(true);
72+
response1.setTime(1000);
73+
testTickStream?.emit('data', response1);
74+
75+
const response2 = new StreamTicksResponse();
76+
response2.setPinName('2');
77+
response2.setHigh(false);
78+
response2.setTime(2000);
79+
testTickStream?.emit('data', response2);
80+
81+
expect(ticks.length).toEqual(2);
82+
83+
const tick1: Tick = ticks[0]!;
84+
expect(tick1.pinName).toEqual('1');
85+
expect(tick1.high).toBe(true);
86+
expect(tick1.time).toEqual(1000);
87+
88+
const tick2: Tick = ticks[1]!;
89+
expect(tick2.pinName).toEqual('2');
90+
expect(tick2.high).toBe(false);
91+
expect(tick2.time).toEqual(2000);
92+
});
93+
94+
it('end streamTicks with an error', () => {
95+
const error = { code: 1, details: 'test', metadata: undefined };
96+
97+
const ticks: Tick[] = [];
98+
const promise1 = board.streamTicks(['1', '2'], ticks);
99+
100+
testTickStream?.emit('end', undefined);
101+
expect(promise1).rejects.toStrictEqual({
102+
message: 'Stream ended without a status code',
103+
});
104+
105+
const promise2 = board.streamTicks(['1', '2'], ticks);
106+
testTickStream?.emit('end', error);
107+
expect(promise2).rejects.toStrictEqual({
108+
code: 1,
109+
message: 'test',
110+
metadata: undefined,
111+
});
112+
113+
const promise3 = board.streamTicks(['1', '2'], ticks);
114+
testTickStream?.emit('status', error);
115+
expect(promise3).rejects.toStrictEqual({
116+
code: 1,
117+
message: 'test',
118+
metadata: undefined,
119+
});
120+
});
121+
});

src/components/board/client.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Options, StructType } from '../../types';
77

88
import pb from '../../gen/component/board/v1/board_pb';
99
import { promisify, doCommandFromClient } from '../../utils';
10-
import type { Board, Duration, PowerMode } from './board';
10+
import type { Board, Duration, PowerMode, Tick } from './board';
1111

1212
/**
1313
* A gRPC-web client for the Board component.
@@ -219,6 +219,50 @@ export class BoardClient implements Board {
219219
return response.getValue();
220220
}
221221

222+
async streamTicks(interrupts: string[], queue: Tick[], extra = {}) {
223+
const request = new pb.StreamTicksRequest();
224+
request.setName(this.name);
225+
request.setPinNamesList(interrupts);
226+
request.setExtra(Struct.fromJavaScript(extra));
227+
this.options.requestLogger?.(request);
228+
const stream = this.client.streamTicks(request);
229+
stream.on('data', (response) => {
230+
const tick: Tick = {
231+
pinName: response.getPinName(),
232+
high: response.getHigh(),
233+
time: response.getTime(),
234+
};
235+
queue.push(tick);
236+
});
237+
238+
return new Promise<void>((resolve, reject) => {
239+
stream.on('status', (status) => {
240+
if (status.code !== 0) {
241+
const error = {
242+
message: status.details,
243+
code: status.code,
244+
metadata: status.metadata,
245+
};
246+
reject(error);
247+
}
248+
});
249+
stream.on('end', (end) => {
250+
if (end === undefined) {
251+
const error = { message: 'Stream ended without a status code' };
252+
reject(error);
253+
} else if (end.code !== 0) {
254+
const error = {
255+
message: end.details,
256+
code: end.code,
257+
metadata: end.metadata,
258+
};
259+
reject(error);
260+
}
261+
resolve();
262+
});
263+
});
264+
}
265+
222266
async setPowerMode(
223267
name: string,
224268
powerMode: PowerMode,

0 commit comments

Comments
 (0)