@wandelbots/nova-js 1.17.1-pr.feat-added-v2-client.64.9ac2247

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +202 -0
  3. package/dist/LoginWithAuth0.d.ts +7 -0
  4. package/dist/LoginWithAuth0.d.ts.map +1 -0
  5. package/dist/chunk-V3NJLR6P.js +336 -0
  6. package/dist/chunk-V3NJLR6P.js.map +1 -0
  7. package/dist/index.cjs +390 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +54 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/AutoReconnectingWebsocket.d.ts +43 -0
  14. package/dist/lib/AutoReconnectingWebsocket.d.ts.map +1 -0
  15. package/dist/lib/availableStorage.d.ts +15 -0
  16. package/dist/lib/availableStorage.d.ts.map +1 -0
  17. package/dist/lib/converters.d.ts +26 -0
  18. package/dist/lib/converters.d.ts.map +1 -0
  19. package/dist/lib/errorHandling.d.ts +4 -0
  20. package/dist/lib/errorHandling.d.ts.map +1 -0
  21. package/dist/lib/v1/ConnectedMotionGroup.d.ts +77 -0
  22. package/dist/lib/v1/ConnectedMotionGroup.d.ts.map +1 -0
  23. package/dist/lib/v1/JoggerConnection.d.ts +94 -0
  24. package/dist/lib/v1/JoggerConnection.d.ts.map +1 -0
  25. package/dist/lib/v1/MotionStreamConnection.d.ts +25 -0
  26. package/dist/lib/v1/MotionStreamConnection.d.ts.map +1 -0
  27. package/dist/lib/v1/NovaCellAPIClient.d.ts +66 -0
  28. package/dist/lib/v1/NovaCellAPIClient.d.ts.map +1 -0
  29. package/dist/lib/v1/NovaClient.d.ts +67 -0
  30. package/dist/lib/v1/NovaClient.d.ts.map +1 -0
  31. package/dist/lib/v1/ProgramStateConnection.d.ts +53 -0
  32. package/dist/lib/v1/ProgramStateConnection.d.ts.map +1 -0
  33. package/dist/lib/v1/getLatestTrajectories.d.ts +4 -0
  34. package/dist/lib/v1/getLatestTrajectories.d.ts.map +1 -0
  35. package/dist/lib/v1/index.cjs +3957 -0
  36. package/dist/lib/v1/index.cjs.map +1 -0
  37. package/dist/lib/v1/index.d.ts +9 -0
  38. package/dist/lib/v1/index.d.ts.map +1 -0
  39. package/dist/lib/v1/index.js +3662 -0
  40. package/dist/lib/v1/index.js.map +1 -0
  41. package/dist/lib/v1/mock/MockNovaInstance.d.ts +13 -0
  42. package/dist/lib/v1/mock/MockNovaInstance.d.ts.map +1 -0
  43. package/dist/lib/v1/motionStateUpdate.d.ts +4 -0
  44. package/dist/lib/v1/motionStateUpdate.d.ts.map +1 -0
  45. package/dist/lib/v2/ConnectedMotionGroup.d.ts +41 -0
  46. package/dist/lib/v2/ConnectedMotionGroup.d.ts.map +1 -0
  47. package/dist/lib/v2/JoggerConnection.d.ts +53 -0
  48. package/dist/lib/v2/JoggerConnection.d.ts.map +1 -0
  49. package/dist/lib/v2/MotionStreamConnection.d.ts +25 -0
  50. package/dist/lib/v2/MotionStreamConnection.d.ts.map +1 -0
  51. package/dist/lib/v2/NovaCellAPIClient.d.ts +64 -0
  52. package/dist/lib/v2/NovaCellAPIClient.d.ts.map +1 -0
  53. package/dist/lib/v2/NovaClient.d.ts +67 -0
  54. package/dist/lib/v2/NovaClient.d.ts.map +1 -0
  55. package/dist/lib/v2/ProgramStateConnection.d.ts +53 -0
  56. package/dist/lib/v2/ProgramStateConnection.d.ts.map +1 -0
  57. package/dist/lib/v2/index.cjs +2239 -0
  58. package/dist/lib/v2/index.cjs.map +1 -0
  59. package/dist/lib/v2/index.d.ts +8 -0
  60. package/dist/lib/v2/index.d.ts.map +1 -0
  61. package/dist/lib/v2/index.js +1947 -0
  62. package/dist/lib/v2/index.js.map +1 -0
  63. package/dist/lib/v2/mock/MockNovaInstance.d.ts +13 -0
  64. package/dist/lib/v2/mock/MockNovaInstance.d.ts.map +1 -0
  65. package/dist/lib/v2/motionStateUpdate.d.ts +4 -0
  66. package/dist/lib/v2/motionStateUpdate.d.ts.map +1 -0
  67. package/dist/lib/v2/vectorUtils.d.ts +7 -0
  68. package/dist/lib/v2/vectorUtils.d.ts.map +1 -0
  69. package/package.json +67 -0
  70. package/src/LoginWithAuth0.ts +90 -0
  71. package/src/index.ts +5 -0
  72. package/src/lib/AutoReconnectingWebsocket.ts +163 -0
  73. package/src/lib/availableStorage.ts +46 -0
  74. package/src/lib/converters.ts +74 -0
  75. package/src/lib/errorHandling.ts +26 -0
  76. package/src/lib/v1/ConnectedMotionGroup.ts +419 -0
  77. package/src/lib/v1/JoggerConnection.ts +480 -0
  78. package/src/lib/v1/MotionStreamConnection.ts +202 -0
  79. package/src/lib/v1/NovaCellAPIClient.ts +180 -0
  80. package/src/lib/v1/NovaClient.ts +232 -0
  81. package/src/lib/v1/ProgramStateConnection.ts +267 -0
  82. package/src/lib/v1/getLatestTrajectories.ts +36 -0
  83. package/src/lib/v1/index.ts +8 -0
  84. package/src/lib/v1/mock/MockNovaInstance.ts +1302 -0
  85. package/src/lib/v1/motionStateUpdate.ts +55 -0
  86. package/src/lib/v2/ConnectedMotionGroup.ts +216 -0
  87. package/src/lib/v2/JoggerConnection.ts +207 -0
  88. package/src/lib/v2/MotionStreamConnection.ts +201 -0
  89. package/src/lib/v2/NovaCellAPIClient.ts +174 -0
  90. package/src/lib/v2/NovaClient.ts +230 -0
  91. package/src/lib/v2/ProgramStateConnection.ts +255 -0
  92. package/src/lib/v2/index.ts +7 -0
  93. package/src/lib/v2/mock/MockNovaInstance.ts +982 -0
  94. package/src/lib/v2/motionStateUpdate.ts +55 -0
  95. package/src/lib/v2/vectorUtils.ts +36 -0
@@ -0,0 +1,480 @@
1
+ import type { Command, Joints, TcpPose } from "@wandelbots/nova-api/v1"
2
+ import isEqual from "lodash-es/isEqual"
3
+ import { Vector3 } from "three/src/math/Vector3.js"
4
+ import type { AutoReconnectingWebsocket } from "../AutoReconnectingWebsocket"
5
+ import { isSameCoordinateSystem, tryParseJson } from "../converters"
6
+ import type { MotionStreamConnection } from "./MotionStreamConnection"
7
+ import type { NovaClient } from "./NovaClient"
8
+
9
+ export type JoggerConnectionOpts = {
10
+ /**
11
+ * When an error message is received from the jogging websocket, it
12
+ * will be passed here. If this handler is not provided, the error will
13
+ * instead be thrown as an unhandled error.
14
+ */
15
+ onError?: (err: unknown) => void
16
+ }
17
+
18
+ export class JoggerConnection {
19
+ // Currently a separate websocket is needed for each mode, pester API people
20
+ // to merge these for simplicity
21
+ cartesianWebsocket: AutoReconnectingWebsocket | null = null
22
+ jointWebsocket: AutoReconnectingWebsocket | null = null
23
+ cartesianJoggingOpts: {
24
+ tcpId?: string
25
+ coordSystemId?: string
26
+ } = {}
27
+
28
+ static async open(
29
+ nova: NovaClient,
30
+ motionGroupId: string,
31
+ opts: JoggerConnectionOpts = {},
32
+ ) {
33
+ const motionStream = await nova.connectMotionStream(motionGroupId)
34
+
35
+ return new JoggerConnection(motionStream, opts)
36
+ }
37
+
38
+ constructor(
39
+ readonly motionStream: MotionStreamConnection,
40
+ readonly opts: JoggerConnectionOpts = {},
41
+ ) {}
42
+
43
+ get motionGroupId() {
44
+ return this.motionStream.motionGroupId
45
+ }
46
+
47
+ get nova() {
48
+ return this.motionStream.nova
49
+ }
50
+
51
+ get numJoints() {
52
+ return this.motionStream.joints.length
53
+ }
54
+
55
+ get activeJoggingMode() {
56
+ if (this.cartesianWebsocket) return "cartesian"
57
+ if (this.jointWebsocket) return "joint"
58
+ return "increment"
59
+ }
60
+
61
+ get activeWebsocket() {
62
+ return this.cartesianWebsocket || this.jointWebsocket
63
+ }
64
+
65
+ async stop() {
66
+ // Why not call the stopJogging API endpoint?
67
+ // Because this results in the websocket closing and we
68
+ // would like to keep it open for now.
69
+
70
+ if (this.cartesianWebsocket) {
71
+ this.cartesianWebsocket.sendJson({
72
+ motion_group: this.motionGroupId,
73
+ position_direction: { x: 0, y: 0, z: 0 },
74
+ rotation_direction: { x: 0, y: 0, z: 0 },
75
+ position_velocity: 0,
76
+ rotation_velocity: 0,
77
+ tcp: this.cartesianJoggingOpts.tcpId,
78
+ coordinate_system: this.cartesianJoggingOpts.coordSystemId,
79
+ })
80
+ }
81
+
82
+ if (this.jointWebsocket) {
83
+ this.jointWebsocket.sendJson({
84
+ motion_group: this.motionGroupId,
85
+ joint_velocities: new Array(this.numJoints).fill(0),
86
+ })
87
+ }
88
+ }
89
+
90
+ dispose() {
91
+ if (this.cartesianWebsocket) {
92
+ this.cartesianWebsocket.dispose()
93
+ }
94
+
95
+ if (this.jointWebsocket) {
96
+ this.jointWebsocket.dispose()
97
+ }
98
+ }
99
+
100
+ setJoggingMode(
101
+ mode: "cartesian" | "joint" | "increment",
102
+ cartesianJoggingOpts?: {
103
+ tcpId?: string
104
+ coordSystemId?: string
105
+ },
106
+ ) {
107
+ console.log("Setting jogging mode to", mode)
108
+ if (cartesianJoggingOpts) {
109
+ // Websocket needs to be reopened to change options
110
+ if (!isEqual(this.cartesianJoggingOpts, cartesianJoggingOpts)) {
111
+ if (this.cartesianWebsocket) {
112
+ this.cartesianWebsocket.dispose()
113
+ this.cartesianWebsocket = null
114
+ }
115
+ }
116
+
117
+ this.cartesianJoggingOpts = cartesianJoggingOpts
118
+ }
119
+
120
+ if (mode !== "cartesian" && this.cartesianWebsocket) {
121
+ this.cartesianWebsocket.dispose()
122
+ this.cartesianWebsocket = null
123
+ }
124
+
125
+ if (mode !== "joint" && this.jointWebsocket) {
126
+ this.jointWebsocket.dispose()
127
+ this.jointWebsocket = null
128
+ }
129
+
130
+ if (mode === "cartesian" && !this.cartesianWebsocket) {
131
+ this.cartesianWebsocket = this.nova.openReconnectingWebsocket(
132
+ `/motion-groups/move-tcp`,
133
+ )
134
+
135
+ this.cartesianWebsocket.addEventListener(
136
+ "message",
137
+ (ev: MessageEvent) => {
138
+ const data = tryParseJson(ev.data)
139
+ if (data && "error" in data) {
140
+ if (this.opts.onError) {
141
+ this.opts.onError(ev.data)
142
+ } else {
143
+ throw new Error(ev.data)
144
+ }
145
+ }
146
+ },
147
+ )
148
+ }
149
+
150
+ if (mode === "joint" && !this.jointWebsocket) {
151
+ this.jointWebsocket = this.nova.openReconnectingWebsocket(
152
+ `/motion-groups/move-joint`,
153
+ )
154
+
155
+ this.jointWebsocket.addEventListener("message", (ev: MessageEvent) => {
156
+ const data = tryParseJson(ev.data)
157
+ if (data && "error" in data) {
158
+ if (this.opts.onError) {
159
+ this.opts.onError(ev.data)
160
+ } else {
161
+ throw new Error(ev.data)
162
+ }
163
+ }
164
+ })
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Start rotation of a single robot joint at the specified velocity
170
+ */
171
+ async startJointRotation({
172
+ joint,
173
+ direction,
174
+ velocityRadsPerSec,
175
+ }: {
176
+ /** Index of the joint to rotate */
177
+ joint: number
178
+ /** Direction of rotation ("+" or "-") */
179
+ direction: "+" | "-"
180
+ /** Speed of the rotation in radians per second */
181
+ velocityRadsPerSec: number
182
+ }) {
183
+ if (!this.jointWebsocket) {
184
+ throw new Error(
185
+ "Joint jogging websocket not connected; call setJoggingMode first",
186
+ )
187
+ }
188
+
189
+ const jointVelocities = new Array(this.numJoints).fill(0)
190
+
191
+ jointVelocities[joint] =
192
+ direction === "-" ? -velocityRadsPerSec : velocityRadsPerSec
193
+
194
+ this.jointWebsocket.sendJson({
195
+ motion_group: this.motionGroupId,
196
+ joint_velocities: jointVelocities,
197
+ })
198
+ }
199
+
200
+ /**
201
+ * Start the TCP moving along a specified axis at a given velocity
202
+ */
203
+ async startTCPTranslation({
204
+ axis,
205
+ direction,
206
+ velocityMmPerSec,
207
+ }: {
208
+ axis: "x" | "y" | "z"
209
+ direction: "-" | "+"
210
+ velocityMmPerSec: number
211
+ }) {
212
+ if (!this.cartesianWebsocket) {
213
+ throw new Error(
214
+ "Cartesian jogging websocket not connected; call setJoggingMode first",
215
+ )
216
+ }
217
+
218
+ const zeroVector = { x: 0, y: 0, z: 0 }
219
+ const joggingVector = Object.assign({}, zeroVector)
220
+ joggingVector[axis] = direction === "-" ? -1 : 1
221
+
222
+ this.cartesianWebsocket.sendJson({
223
+ motion_group: this.motionGroupId,
224
+ position_direction: joggingVector,
225
+ rotation_direction: zeroVector,
226
+ position_velocity: velocityMmPerSec,
227
+ rotation_velocity: 0,
228
+ tcp: this.cartesianJoggingOpts.tcpId,
229
+ coordinate_system: this.cartesianJoggingOpts.coordSystemId,
230
+ })
231
+ }
232
+
233
+ /**
234
+ * Start the TCP rotating around a specified axis at a given velocity
235
+ */
236
+ async startTCPRotation({
237
+ axis,
238
+ direction,
239
+ velocityRadsPerSec,
240
+ }: {
241
+ axis: "x" | "y" | "z"
242
+ direction: "-" | "+"
243
+ velocityRadsPerSec: number
244
+ }) {
245
+ if (!this.cartesianWebsocket) {
246
+ throw new Error(
247
+ "Cartesian jogging websocket not connected; call setJoggingMode first",
248
+ )
249
+ }
250
+
251
+ const zeroVector = { x: 0, y: 0, z: 0 }
252
+ const joggingVector = Object.assign({}, zeroVector)
253
+ joggingVector[axis] = direction === "-" ? -1 : 1
254
+
255
+ this.cartesianWebsocket.sendJson({
256
+ motion_group: this.motionGroupId,
257
+ position_direction: zeroVector,
258
+ rotation_direction: joggingVector,
259
+ position_velocity: 0,
260
+ rotation_velocity: velocityRadsPerSec,
261
+ tcp: this.cartesianJoggingOpts.tcpId,
262
+ coordinate_system: this.cartesianJoggingOpts.coordSystemId,
263
+ })
264
+ }
265
+
266
+ /**
267
+ * Move the robot by a fixed distance in a single cartesian
268
+ * axis, either rotating or translating relative to the TCP.
269
+ * Promise resolves only after the motion has completed.
270
+ */
271
+ async runIncrementalCartesianMotion({
272
+ currentTcpPose,
273
+ currentJoints,
274
+ coordSystemId,
275
+ velocityInRelevantUnits,
276
+ axis,
277
+ direction,
278
+ motion,
279
+ }: {
280
+ currentTcpPose: TcpPose
281
+ currentJoints: Joints
282
+ coordSystemId: string
283
+ velocityInRelevantUnits: number
284
+ axis: "x" | "y" | "z"
285
+ direction: "-" | "+"
286
+ motion:
287
+ | {
288
+ type: "rotate"
289
+ distanceRads: number
290
+ }
291
+ | {
292
+ type: "translate"
293
+ distanceMm: number
294
+ }
295
+ }) {
296
+ const commands: Command[] = []
297
+
298
+ if (
299
+ !isSameCoordinateSystem(currentTcpPose.coordinate_system, coordSystemId)
300
+ ) {
301
+ throw new Error(
302
+ `Current TCP pose coordinate system ${currentTcpPose.coordinate_system} does not match target coordinate system ${coordSystemId}`,
303
+ )
304
+ }
305
+
306
+ if (motion.type === "translate") {
307
+ const targetTcpPosition = Object.assign({}, currentTcpPose.position)
308
+ targetTcpPosition[axis] +=
309
+ motion.distanceMm * (direction === "-" ? -1 : 1)
310
+
311
+ commands.push({
312
+ settings: {
313
+ limits_override: {
314
+ tcp_velocity_limit: velocityInRelevantUnits,
315
+ },
316
+ },
317
+ line: {
318
+ position: targetTcpPosition,
319
+ orientation: currentTcpPose.orientation,
320
+ coordinate_system: coordSystemId,
321
+ },
322
+ })
323
+ } else if (motion.type === "rotate") {
324
+ // Concatenate rotations expressed by rotation vectors
325
+ // Equations taken from https://physics.stackexchange.com/a/287819
326
+
327
+ // Compute axis and angle of current rotation vector
328
+ const currentRotationVector = new Vector3(
329
+ currentTcpPose.orientation["x"],
330
+ currentTcpPose.orientation["y"],
331
+ currentTcpPose.orientation["z"],
332
+ )
333
+
334
+ const currentRotationRad = currentRotationVector.length()
335
+ const currentRotationDirection = currentRotationVector.clone().normalize()
336
+
337
+ // Compute axis and angle of difference rotation vector
338
+ const differenceRotationRad =
339
+ motion.distanceRads * (direction === "-" ? -1 : 1)
340
+
341
+ const differenceRotationDirection = new Vector3(0.0, 0.0, 0.0)
342
+ differenceRotationDirection[axis] = 1.0
343
+
344
+ // Some abbreviations to make the following equations more readable
345
+ const f1 =
346
+ Math.cos(0.5 * differenceRotationRad) *
347
+ Math.cos(0.5 * currentRotationRad)
348
+ const f2 =
349
+ Math.sin(0.5 * differenceRotationRad) *
350
+ Math.sin(0.5 * currentRotationRad)
351
+ const f3 =
352
+ Math.sin(0.5 * differenceRotationRad) *
353
+ Math.cos(0.5 * currentRotationRad)
354
+ const f4 =
355
+ Math.cos(0.5 * differenceRotationRad) *
356
+ Math.sin(0.5 * currentRotationRad)
357
+
358
+ const dotProduct = differenceRotationDirection.dot(
359
+ currentRotationDirection,
360
+ )
361
+
362
+ const crossProduct = differenceRotationDirection
363
+ .clone()
364
+ .cross(currentRotationDirection)
365
+
366
+ // Compute angle of concatenated rotation
367
+ const newRotationRad = 2.0 * Math.acos(f1 - f2 * dotProduct)
368
+
369
+ // Compute rotation vector of concatenated rotation
370
+ const f5 = newRotationRad / Math.sin(0.5 * newRotationRad)
371
+
372
+ const targetTcpOrientation = new Vector3()
373
+ .addScaledVector(crossProduct, f2)
374
+ .addScaledVector(differenceRotationDirection, f3)
375
+ .addScaledVector(currentRotationDirection, f4)
376
+ .multiplyScalar(f5)
377
+
378
+ commands.push({
379
+ settings: {
380
+ limits_override: {
381
+ tcp_orientation_velocity_limit: velocityInRelevantUnits,
382
+ },
383
+ },
384
+ line: {
385
+ position: currentTcpPose.position,
386
+ orientation: targetTcpOrientation,
387
+ coordinate_system: coordSystemId,
388
+ },
389
+ })
390
+ }
391
+
392
+ const motionPlanRes = await this.nova.api.motion.planMotion({
393
+ motion_group: this.motionGroupId,
394
+ start_joint_position: currentJoints,
395
+ tcp: this.cartesianJoggingOpts.tcpId,
396
+ commands,
397
+ })
398
+
399
+ const plannedMotion = motionPlanRes.plan_successful_response?.motion
400
+ if (!plannedMotion) {
401
+ throw new Error(
402
+ `Failed to plan jogging increment motion ${JSON.stringify(motionPlanRes)}`,
403
+ )
404
+ }
405
+
406
+ await this.nova.api.motion.streamMoveForward(
407
+ plannedMotion,
408
+ 100,
409
+ undefined,
410
+ undefined,
411
+ undefined,
412
+ {
413
+ // Might take a while at low velocity
414
+ timeout: 1000 * 60,
415
+ },
416
+ )
417
+ }
418
+
419
+ /**
420
+ * Rotate a single robot joint by a fixed number of radians
421
+ * Promise resolves only after the motion has completed.
422
+ */
423
+ async runIncrementalJointRotation({
424
+ joint,
425
+ currentJoints,
426
+ velocityRadsPerSec,
427
+ direction,
428
+ distanceRads,
429
+ }: {
430
+ joint: number
431
+ currentJoints: Joints
432
+ velocityRadsPerSec: number
433
+ direction: "-" | "+"
434
+ distanceRads: number
435
+ }) {
436
+ const targetJoints = [...currentJoints.joints]
437
+ targetJoints[joint]! += distanceRads * (direction === "-" ? -1 : 1)
438
+
439
+ const jointVelocityLimits: number[] = new Array(
440
+ currentJoints.joints.length,
441
+ ).fill(velocityRadsPerSec)
442
+
443
+ const motionPlanRes = await this.nova.api.motion.planMotion({
444
+ motion_group: this.motionGroupId,
445
+ start_joint_position: currentJoints,
446
+ commands: [
447
+ {
448
+ settings: {
449
+ limits_override: {
450
+ joint_velocity_limits: {
451
+ joints: jointVelocityLimits,
452
+ },
453
+ },
454
+ },
455
+ joint_ptp: {
456
+ joints: targetJoints,
457
+ },
458
+ },
459
+ ],
460
+ })
461
+
462
+ const plannedMotion = motionPlanRes.plan_successful_response?.motion
463
+ if (!plannedMotion) {
464
+ console.error("Failed to plan jogging increment motion", motionPlanRes)
465
+ return
466
+ }
467
+
468
+ await this.nova.api.motion.streamMoveForward(
469
+ plannedMotion,
470
+ 100,
471
+ undefined,
472
+ undefined,
473
+ undefined,
474
+ {
475
+ // Might take a while at low velocity
476
+ timeout: 1000 * 60,
477
+ },
478
+ )
479
+ }
480
+ }
@@ -0,0 +1,202 @@
1
+ import type {
2
+ ControllerInstance,
3
+ MotionGroupPhysical,
4
+ MotionGroupStateResponse,
5
+ Vector3d,
6
+ } from "@wandelbots/nova-api/v1"
7
+ import { makeAutoObservable, runInAction } from "mobx"
8
+ import { Vector3 } from "three"
9
+ import type { AutoReconnectingWebsocket } from "../AutoReconnectingWebsocket"
10
+ import { tryParseJson } from "../converters"
11
+ import { jointValuesEqual, tcpPoseEqual } from "./motionStateUpdate"
12
+ import type { NovaClient } from "./NovaClient"
13
+
14
+ const MOTION_DELTA_THRESHOLD = 0.0001
15
+
16
+ function unwrapRotationVector(
17
+ newRotationVectorApi: Vector3d,
18
+ currentRotationVectorApi: Vector3d,
19
+ ): Vector3d {
20
+ const currentRotationVector = new Vector3(
21
+ currentRotationVectorApi.x,
22
+ currentRotationVectorApi.y,
23
+ currentRotationVectorApi.z,
24
+ )
25
+
26
+ const newRotationVector = new Vector3(
27
+ newRotationVectorApi.x,
28
+ newRotationVectorApi.y,
29
+ newRotationVectorApi.z,
30
+ )
31
+
32
+ const currentAngle = currentRotationVector.length()
33
+ const currentAxis = currentRotationVector.normalize()
34
+
35
+ let newAngle = newRotationVector.length()
36
+ let newAxis = newRotationVector.normalize()
37
+
38
+ // Align rotation axes
39
+ if (newAxis.dot(currentAxis) < 0) {
40
+ newAngle = -newAngle
41
+ newAxis = newAxis.multiplyScalar(-1.0)
42
+ }
43
+
44
+ // Shift rotation angle close to previous one to extend domain of rotation angles beyond [0, pi]
45
+ // - this simplifies interpolation and prevents abruptly changing signs of the rotation angles
46
+ let angleDifference = newAngle - currentAngle
47
+ angleDifference -=
48
+ 2.0 * Math.PI * Math.floor((angleDifference + Math.PI) / (2.0 * Math.PI))
49
+
50
+ newAngle = currentAngle + angleDifference
51
+
52
+ return newAxis.multiplyScalar(newAngle)
53
+ }
54
+
55
+ /**
56
+ * Store representing the current state of a connected motion group.
57
+ */
58
+ export class MotionStreamConnection {
59
+ static async open(nova: NovaClient, motionGroupId: string) {
60
+ const { instances: controllers } =
61
+ await nova.api.controller.listControllers()
62
+
63
+ const [_motionGroupIndex, controllerId] = motionGroupId.split("@") as [
64
+ string,
65
+ string,
66
+ ]
67
+ const controller = controllers.find((c) => c.controller === controllerId)
68
+ const motionGroup = controller?.physical_motion_groups.find(
69
+ (mg) => mg.motion_group === motionGroupId,
70
+ )
71
+ if (!controller || !motionGroup) {
72
+ throw new Error(
73
+ `Controller ${controllerId} or motion group ${motionGroupId} not found`,
74
+ )
75
+ }
76
+
77
+ const motionStateSocket = nova.openReconnectingWebsocket(
78
+ `/motion-groups/${motionGroupId}/state-stream`,
79
+ )
80
+
81
+ // Wait for the first message to get the initial state
82
+ const firstMessage = await motionStateSocket.firstMessage()
83
+ console.log("got first message", firstMessage)
84
+ const initialMotionState = tryParseJson(firstMessage.data)
85
+ ?.result as MotionGroupStateResponse
86
+
87
+ if (!initialMotionState) {
88
+ throw new Error(
89
+ `Unable to parse initial motion state message ${firstMessage.data}`,
90
+ )
91
+ }
92
+
93
+ console.log(
94
+ `Connected motion state websocket to motion group ${motionGroup.motion_group}. Initial state:\n `,
95
+ initialMotionState,
96
+ )
97
+
98
+ return new MotionStreamConnection(
99
+ nova,
100
+ controller,
101
+ motionGroup,
102
+ initialMotionState,
103
+ motionStateSocket,
104
+ )
105
+ }
106
+
107
+ // Not mobx-observable as this changes very fast; should be observed
108
+ // using animation frames
109
+ rapidlyChangingMotionState: MotionGroupStateResponse
110
+
111
+ constructor(
112
+ readonly nova: NovaClient,
113
+ readonly controller: ControllerInstance,
114
+ readonly motionGroup: MotionGroupPhysical,
115
+ readonly initialMotionState: MotionGroupStateResponse,
116
+ readonly motionStateSocket: AutoReconnectingWebsocket,
117
+ ) {
118
+ this.rapidlyChangingMotionState = initialMotionState
119
+
120
+ motionStateSocket.addEventListener("message", (event) => {
121
+ const motionStateResponse = tryParseJson(event.data)?.result as
122
+ | MotionGroupStateResponse
123
+ | undefined
124
+
125
+ if (!motionStateResponse) {
126
+ throw new Error(
127
+ `Failed to get motion state for ${this.motionGroupId}: ${event.data}`,
128
+ )
129
+ }
130
+
131
+ // handle motionState message
132
+ if (
133
+ !jointValuesEqual(
134
+ this.rapidlyChangingMotionState.state.joint_position.joints,
135
+ motionStateResponse.state.joint_position.joints,
136
+ MOTION_DELTA_THRESHOLD,
137
+ )
138
+ ) {
139
+ runInAction(() => {
140
+ this.rapidlyChangingMotionState.state = motionStateResponse.state
141
+ })
142
+ }
143
+
144
+ // handle tcpPose message
145
+ if (
146
+ !tcpPoseEqual(
147
+ this.rapidlyChangingMotionState.tcp_pose,
148
+ motionStateResponse.tcp_pose,
149
+ MOTION_DELTA_THRESHOLD,
150
+ )
151
+ ) {
152
+ runInAction(() => {
153
+ if (this.rapidlyChangingMotionState.tcp_pose == undefined) {
154
+ this.rapidlyChangingMotionState.tcp_pose =
155
+ motionStateResponse.tcp_pose
156
+ } else {
157
+ this.rapidlyChangingMotionState.tcp_pose = {
158
+ position: motionStateResponse.tcp_pose!.position,
159
+ orientation: unwrapRotationVector(
160
+ motionStateResponse.tcp_pose!.orientation,
161
+ this.rapidlyChangingMotionState.tcp_pose!.orientation,
162
+ ),
163
+ tcp: motionStateResponse.tcp_pose!.tcp,
164
+ coordinate_system:
165
+ motionStateResponse.tcp_pose!.coordinate_system,
166
+ }
167
+ }
168
+ })
169
+ }
170
+ })
171
+ makeAutoObservable(this)
172
+ }
173
+
174
+ get motionGroupId() {
175
+ return this.motionGroup.motion_group
176
+ }
177
+
178
+ get controllerId() {
179
+ return this.controller.controller
180
+ }
181
+
182
+ get modelFromController() {
183
+ return this.motionGroup.model_from_controller
184
+ }
185
+
186
+ get wandelscriptIdentifier() {
187
+ const num = this.motionGroupId.split("@")[0]
188
+ return `${this.controllerId.replaceAll("-", "_")}_${num}`
189
+ }
190
+
191
+ get joints() {
192
+ return this.initialMotionState.state.joint_position.joints.map((_, i) => {
193
+ return {
194
+ index: i,
195
+ }
196
+ })
197
+ }
198
+
199
+ dispose() {
200
+ this.motionStateSocket.close()
201
+ }
202
+ }