Skip to content

Commit 6cd4b06

Browse files
authored
RSDK-10408 Add world-state-store service (#621)
1 parent 32e1d63 commit 6cd4b06

File tree

9 files changed

+368
-0
lines changed

9 files changed

+368
-0
lines changed

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"prettier-plugin-jsdoc": "^1.1.1",
8181
"typedoc": "^0.27.9",
8282
"typescript": "^5.3.3",
83+
"uuid-tool": "^2.0.3",
8384
"vite": "^5.0.10",
8485
"vitest": "^1.1.0",
8586
"yaml": "^2.3.3"

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ export {
407407
*/
408408
export * as visionApi from './gen/service/vision/v1/vision_pb';
409409

410+
export * from './services/world-state-store';
411+
410412
export {
411413
GenericClient as GenericServiceClient,
412414
type Generic as GenericService,

src/robot/client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type { Robot } from './robot';
3636
import SessionManager from './session-manager';
3737
import { MLModelService } from '../gen/service/mlmodel/v1/mlmodel_connect';
3838
import type { AccessToken, Credential } from '../main';
39+
import { WorldStateStoreService } from '../gen/service/worldstatestore/v1/world_state_store_connect';
3940
import { assertExists } from '../assert';
4041

4142
interface ICEServer {
@@ -238,6 +239,10 @@ export class RobotClient extends EventDispatcher implements Robot {
238239

239240
private slamServiceClient: Client<typeof SLAMService> | undefined;
240241

242+
private worldStateStoreServiceClient:
243+
| Client<typeof WorldStateStoreService>
244+
| undefined;
245+
241246
private currentRetryAttempt = 0;
242247

243248
constructor(
@@ -478,6 +483,13 @@ export class RobotClient extends EventDispatcher implements Robot {
478483
return this.slamServiceClient;
479484
}
480485

486+
get worldStateStoreService() {
487+
this.worldStateStoreServiceClient ??= this.createServiceClient(
488+
WorldStateStoreService
489+
);
490+
return this.worldStateStoreServiceClient;
491+
}
492+
481493
createServiceClient<T extends ServiceType>(svcType: T): Client<T> {
482494
assertExists(this.clientTransport, RobotClient.notConnectedYetStr);
483495
return createClient(svcType, this.clientTransport);
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// @vitest-environment happy-dom
2+
3+
import { createClient, createRouterTransport } from '@connectrpc/connect';
4+
import { Struct } from '@bufbuild/protobuf';
5+
import { beforeEach, describe, expect, it, vi } from 'vitest';
6+
import { WorldStateStoreService } from '../../gen/service/worldstatestore/v1/world_state_store_connect';
7+
import {
8+
GetTransformResponse,
9+
ListUUIDsResponse,
10+
StreamTransformChangesResponse,
11+
} from '../../gen/service/worldstatestore/v1/world_state_store_pb';
12+
import { RobotClient } from '../../robot';
13+
import { WorldStateStoreClient } from './client';
14+
import { TransformChangeType } from '../../gen/service/worldstatestore/v1/world_state_store_pb';
15+
import { Transform, PoseInFrame, Pose } from '../../gen/common/v1/common_pb';
16+
import { transformWithUUID, uuidToString } from './world-state-store';
17+
18+
vi.mock('../../robot');
19+
20+
const worldStateStoreClientName = 'test-world-state-store';
21+
22+
let worldStateStore: WorldStateStoreClient;
23+
24+
const mockUuids = [new Uint8Array([1, 2, 3, 4]), new Uint8Array([5, 6, 7, 8])];
25+
const mockTransform = new Transform({
26+
referenceFrame: 'test-frame',
27+
poseInObserverFrame: new PoseInFrame({
28+
referenceFrame: 'observer-frame',
29+
pose: new Pose({
30+
x: 10,
31+
y: 20,
32+
z: 30,
33+
oX: 0,
34+
oY: 0,
35+
oZ: 1,
36+
theta: 90,
37+
}),
38+
}),
39+
uuid: mockUuids[0],
40+
});
41+
42+
describe('WorldStateStoreClient Tests', () => {
43+
beforeEach(() => {
44+
const mockTransport = createRouterTransport(({ service }) => {
45+
service(WorldStateStoreService, {
46+
listUUIDs: () => new ListUUIDsResponse({ uuids: mockUuids }),
47+
getTransform: () =>
48+
new GetTransformResponse({ transform: mockTransform }),
49+
streamTransformChanges: async function* mockStream() {
50+
// Add await to satisfy linter
51+
await Promise.resolve();
52+
yield new StreamTransformChangesResponse({
53+
changeType: TransformChangeType.ADDED,
54+
transform: mockTransform,
55+
});
56+
yield new StreamTransformChangesResponse({
57+
changeType: TransformChangeType.UPDATED,
58+
transform: mockTransform,
59+
updatedFields: { paths: ['pose_in_observer_frame'] },
60+
});
61+
},
62+
doCommand: () => ({ result: Struct.fromJson({ success: true }) }),
63+
});
64+
});
65+
66+
RobotClient.prototype.createServiceClient = vi
67+
.fn()
68+
.mockImplementation(() =>
69+
createClient(WorldStateStoreService, mockTransport)
70+
);
71+
worldStateStore = new WorldStateStoreClient(
72+
new RobotClient('host'),
73+
worldStateStoreClientName
74+
);
75+
});
76+
77+
describe('listUUIDs', () => {
78+
it('returns all transform UUIDs', async () => {
79+
const expected = mockUuids.map((uuid) => uuidToString(uuid));
80+
81+
await expect(worldStateStore.listUUIDs()).resolves.toStrictEqual(
82+
expected
83+
);
84+
});
85+
});
86+
87+
describe('getTransform', () => {
88+
it('returns a transform by UUID', async () => {
89+
const uuid = '01020304';
90+
const expected = mockTransform;
91+
92+
await expect(worldStateStore.getTransform(uuid)).resolves.toStrictEqual({
93+
...expected,
94+
uuidString: uuid,
95+
});
96+
});
97+
});
98+
99+
describe('streamTransformChanges', () => {
100+
it('streams transform changes', async () => {
101+
const stream = worldStateStore.streamTransformChanges();
102+
const results = [];
103+
104+
for await (const result of stream) {
105+
results.push(result);
106+
}
107+
108+
expect(results).toHaveLength(2);
109+
expect(results[0]).toEqual({
110+
changeType: TransformChangeType.ADDED,
111+
transform: transformWithUUID(mockTransform),
112+
updatedFields: undefined,
113+
});
114+
expect(results[1]).toEqual({
115+
changeType: TransformChangeType.UPDATED,
116+
transform: transformWithUUID(mockTransform),
117+
updatedFields: { paths: ['pose_in_observer_frame'] },
118+
});
119+
});
120+
});
121+
122+
describe('doCommand', () => {
123+
it('executes arbitrary commands', async () => {
124+
const command = Struct.fromJson({ test: 'value' });
125+
const expected = { success: true };
126+
127+
await expect(worldStateStore.doCommand(command)).resolves.toStrictEqual(
128+
expected
129+
);
130+
});
131+
});
132+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Struct, type JsonValue } from '@bufbuild/protobuf';
2+
import type { CallOptions, Client } from '@connectrpc/connect';
3+
import { WorldStateStoreService } from '../../gen/service/worldstatestore/v1/world_state_store_connect';
4+
import {
5+
GetTransformRequest,
6+
ListUUIDsRequest,
7+
StreamTransformChangesRequest,
8+
} from '../../gen/service/worldstatestore/v1/world_state_store_pb';
9+
import type { RobotClient } from '../../robot';
10+
import type { Options } from '../../types';
11+
import { doCommandFromClient } from '../../utils';
12+
import type { WorldStateStore } from './world-state-store';
13+
import {
14+
transformWithUUID,
15+
uuidFromString,
16+
uuidToString,
17+
} from './world-state-store';
18+
import type { TransformChangeEvent } from './types';
19+
20+
/**
21+
* A gRPC-web client for a WorldStateStore service.
22+
*
23+
* @group Clients
24+
*/
25+
export class WorldStateStoreClient implements WorldStateStore {
26+
private client: Client<typeof WorldStateStoreService>;
27+
public readonly name: string;
28+
private readonly options: Options;
29+
public callOptions: CallOptions = { headers: {} as Record<string, string> };
30+
31+
constructor(client: RobotClient, name: string, options: Options = {}) {
32+
this.client = client.createServiceClient(WorldStateStoreService);
33+
this.name = name;
34+
this.options = options;
35+
}
36+
37+
async listUUIDs(extra = {}, callOptions = this.callOptions) {
38+
const request = new ListUUIDsRequest({
39+
name: this.name,
40+
extra: Struct.fromJson(extra),
41+
});
42+
43+
this.options.requestLogger?.(request);
44+
45+
const response = await this.client.listUUIDs(request, callOptions);
46+
return response.uuids.map((uuid) => uuidToString(uuid));
47+
}
48+
49+
async getTransform(uuid: string, extra = {}, callOptions = this.callOptions) {
50+
const request = new GetTransformRequest({
51+
name: this.name,
52+
uuid: uuidFromString(uuid),
53+
extra: Struct.fromJson(extra),
54+
});
55+
56+
this.options.requestLogger?.(request);
57+
58+
const response = await this.client.getTransform(request, callOptions);
59+
if (!response.transform) {
60+
throw new Error('No transform returned from server');
61+
}
62+
63+
return transformWithUUID(response.transform);
64+
}
65+
66+
async *streamTransformChanges(
67+
extra = {},
68+
callOptions = this.callOptions
69+
): AsyncGenerator<TransformChangeEvent, void> {
70+
const request = new StreamTransformChangesRequest({
71+
name: this.name,
72+
extra: Struct.fromJson(extra),
73+
});
74+
75+
this.options.requestLogger?.(request);
76+
77+
const stream = this.client.streamTransformChanges(request, callOptions);
78+
79+
for await (const response of stream) {
80+
if (!response.transform) {
81+
continue;
82+
}
83+
84+
yield {
85+
...response,
86+
transform: transformWithUUID(response.transform),
87+
};
88+
}
89+
}
90+
91+
async doCommand(
92+
command: Struct,
93+
callOptions = this.callOptions
94+
): Promise<JsonValue> {
95+
return doCommandFromClient(
96+
this.client.doCommand,
97+
this.name,
98+
command,
99+
this.options,
100+
callOptions
101+
);
102+
}
103+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { WorldStateStoreClient } from './client';
2+
export type { WorldStateStore } from './world-state-store';
3+
export { transformWithUUID } from './world-state-store';
4+
export type { TransformChangeEvent, TransformWithUUID } from './types';
5+
6+
export { TransformChangeType } from '../../gen/service/worldstatestore/v1/world_state_store_pb';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { PlainMessage } from '@bufbuild/protobuf';
2+
import type { Transform } from '../../gen/common/v1/common_pb';
3+
import type { StreamTransformChangesResponse } from '../../gen/service/worldstatestore/v1/world_state_store_pb';
4+
5+
export interface TransformWithUUID extends PlainMessage<Transform> {
6+
uuidString: string;
7+
}
8+
9+
export type TransformChangeEvent = Omit<
10+
PlainMessage<StreamTransformChangesResponse>,
11+
'transform'
12+
> & {
13+
transform: TransformWithUUID | undefined;
14+
};

0 commit comments

Comments
 (0)