@wandelbots/wandelbots-js-react-components 2.54.4 → 2.54.5-pr.feat-upgrade-jogging-to-v2.404.a6b053a

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 (79) hide show
  1. package/README.md +1 -1
  2. package/dist/auth0-spa-js.production.esm-Bb4L9JDU.js +1423 -0
  3. package/dist/auth0-spa-js.production.esm-Bb4L9JDU.js.map +1 -0
  4. package/dist/auth0-spa-js.production.esm-CEI5Uihg.cjs +5 -0
  5. package/dist/auth0-spa-js.production.esm-CEI5Uihg.cjs.map +1 -0
  6. package/dist/components/3d-viewport/CoordinateSystemTransform.d.ts +1 -1
  7. package/dist/components/3d-viewport/CoordinateSystemTransform.d.ts.map +1 -1
  8. package/dist/components/3d-viewport/SafetyZonesRenderer.d.ts.map +1 -1
  9. package/dist/components/3d-viewport/collider/ColliderCollection.d.ts +1 -1
  10. package/dist/components/3d-viewport/collider/ColliderCollection.d.ts.map +1 -1
  11. package/dist/components/3d-viewport/collider/ColliderElement.d.ts +1 -1
  12. package/dist/components/3d-viewport/collider/ColliderElement.d.ts.map +1 -1
  13. package/dist/components/3d-viewport/collider/CollisionSceneRenderer.d.ts +1 -1
  14. package/dist/components/3d-viewport/collider/CollisionSceneRenderer.d.ts.map +1 -1
  15. package/dist/components/3d-viewport/collider/colliderShapeToBufferGeometry.d.ts +1 -1
  16. package/dist/components/3d-viewport/collider/colliderShapeToBufferGeometry.d.ts.map +1 -1
  17. package/dist/components/jogging/JoggingCartesianTab.d.ts.map +1 -1
  18. package/dist/components/jogging/JoggingJointTab.d.ts.map +1 -1
  19. package/dist/components/jogging/JoggingPanel.d.ts +1 -1
  20. package/dist/components/jogging/JoggingPanel.d.ts.map +1 -1
  21. package/dist/components/jogging/JoggingStore.d.ts +6 -5
  22. package/dist/components/jogging/JoggingStore.d.ts.map +1 -1
  23. package/dist/components/jogging/PoseCartesianValues.d.ts +2 -2
  24. package/dist/components/jogging/PoseCartesianValues.d.ts.map +1 -1
  25. package/dist/components/jogging/PoseJointValues.d.ts +1 -2
  26. package/dist/components/jogging/PoseJointValues.d.ts.map +1 -1
  27. package/dist/components/robots/RobotAnimator.d.ts +1 -1
  28. package/dist/components/robots/RobotAnimator.d.ts.map +1 -1
  29. package/dist/components/robots/SupportedRobot.d.ts +1 -1
  30. package/dist/components/robots/SupportedRobot.d.ts.map +1 -1
  31. package/dist/components/robots/manufacturerHomePositions.d.ts +1 -1
  32. package/dist/components/robots/manufacturerHomePositions.d.ts.map +1 -1
  33. package/dist/components/utils/errorHandling.test.d.ts +2 -0
  34. package/dist/components/utils/errorHandling.test.d.ts.map +1 -0
  35. package/dist/index.cjs +48 -50
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +15948 -19568
  38. package/dist/index.js.map +1 -1
  39. package/dist/lib/ConnectedMotionGroup.d.ts +84 -0
  40. package/dist/lib/ConnectedMotionGroup.d.ts.map +1 -0
  41. package/dist/lib/JoggerConnection.d.ts +112 -0
  42. package/dist/lib/JoggerConnection.d.ts.map +1 -0
  43. package/dist/lib/MotionStreamConnection.d.ts +24 -0
  44. package/dist/lib/MotionStreamConnection.d.ts.map +1 -0
  45. package/dist/lib/MotionStreamConnection.test.d.ts +2 -0
  46. package/dist/lib/MotionStreamConnection.test.d.ts.map +1 -0
  47. package/dist/lib/motionStateUpdate.d.ts +5 -0
  48. package/dist/lib/motionStateUpdate.d.ts.map +1 -0
  49. package/dist/lib/motionStateUpdate.test.d.ts +2 -0
  50. package/dist/lib/motionStateUpdate.test.d.ts.map +1 -0
  51. package/package.json +2 -2
  52. package/src/components/3d-viewport/CoordinateSystemTransform.tsx +1 -1
  53. package/src/components/3d-viewport/SafetyZonesRenderer.tsx +1 -2
  54. package/src/components/3d-viewport/collider/ColliderCollection.tsx +1 -1
  55. package/src/components/3d-viewport/collider/ColliderElement.tsx +1 -1
  56. package/src/components/3d-viewport/collider/CollisionSceneRenderer.tsx +1 -1
  57. package/src/components/3d-viewport/collider/colliderShapeToBufferGeometry.ts +1 -1
  58. package/src/components/jogging/JoggingCartesianTab.tsx +13 -11
  59. package/src/components/jogging/JoggingJointLimitDetector.tsx +2 -2
  60. package/src/components/jogging/JoggingJointTab.tsx +4 -3
  61. package/src/components/jogging/JoggingPanel.tsx +3 -2
  62. package/src/components/jogging/JoggingStore.ts +44 -39
  63. package/src/components/jogging/PoseCartesianValues.tsx +3 -4
  64. package/src/components/jogging/PoseJointValues.tsx +3 -4
  65. package/src/components/robots/DHRobot.tsx +1 -1
  66. package/src/components/robots/RobotAnimator.tsx +1 -1
  67. package/src/components/robots/SupportedRobot.tsx +1 -1
  68. package/src/components/robots/manufacturerHomePositions.ts +1 -1
  69. package/src/components/utils/errorHandling.test.ts +41 -0
  70. package/src/lib/ConnectedMotionGroup.ts +426 -0
  71. package/src/lib/JoggerConnection.ts +659 -0
  72. package/src/lib/MotionStreamConnection.test.ts +23 -0
  73. package/src/lib/MotionStreamConnection.ts +221 -0
  74. package/src/lib/motionStateUpdate.test.ts +28 -0
  75. package/src/lib/motionStateUpdate.ts +76 -0
  76. package/dist/auth0-spa-js.production.esm-1QXzndwB.js +0 -950
  77. package/dist/auth0-spa-js.production.esm-1QXzndwB.js.map +0 -1
  78. package/dist/auth0-spa-js.production.esm-BLRAk7Yh.cjs +0 -5
  79. package/dist/auth0-spa-js.production.esm-BLRAk7Yh.cjs.map +0 -1
@@ -0,0 +1,659 @@
1
+ import {
2
+ AutoReconnectingWebsocket,
3
+ tryParseJson,
4
+ XYZ_TO_VECTOR,
5
+ } from "@wandelbots/nova-js"
6
+ import type {
7
+ InitializeMovementResponse,
8
+ MotionCommand,
9
+ NovaClient,
10
+ Pose,
11
+ StartMovementResponse,
12
+ } from "@wandelbots/nova-js/v2"
13
+ import { when } from "mobx"
14
+ import { Vector3 } from "three/src/math/Vector3.js"
15
+ import { MotionStreamConnection } from "./MotionStreamConnection"
16
+
17
+ export type Vector3Simple = [number, number, number]
18
+
19
+ export type JoggerConnectionOptions = {
20
+ // The mode of the jogger connection - see type description of JoggerMode for details
21
+ mode?: JoggerMode
22
+
23
+ // Connection timeout in milliseconds to wait for jogging initialization to complete
24
+ timeout?: number
25
+
26
+ /**
27
+ * When an error message is received from the jogging websocket, it
28
+ * will be passed here. If this handler is not provided, the error will
29
+ * instead be thrown as an unhandled error.
30
+ */
31
+ onError?: (err: unknown) => void
32
+
33
+ // The TCP to use for jogging motions.
34
+ tcp?: string
35
+
36
+ // The coordinate system in which jogging takes place in
37
+ // coordinateSystem?: string,
38
+
39
+ // If set to "tool", jogging TcpVelocityRequest will use `use_tool_coordinate_system` option
40
+ orientation?: JoggerOrientation
41
+ }
42
+
43
+ // Three modes are supported:
44
+ // - "jogging": Continuous jogging mode, where joint velocities or TCP velocities can be commanded. Opens a single websocket connection to jogging endpoint
45
+ // - "trajectory": Incremental jogging mode, where fixed distance motions are planned and executed. Opens a short lived websocket for each motion.
46
+ // - "off": No jogging, all websockets are closed.
47
+ export type JoggerMode = "jogging" | "trajectory" | "off"
48
+
49
+ // If set to "tool", will use `use_tool_coordinate_system` option in movement requests
50
+ export type JoggerOrientation = "coordsys" | "tool"
51
+
52
+ export class JoggerConnection {
53
+ ENDPOINT_JOGGING = "/execution/jogging"
54
+ ENDPOINT_TRAJECTORY = "/execution/trajectory"
55
+ DEFAULT_MODE = "off" as JoggerMode
56
+ DEFAULT_TCP = "Flange"
57
+ // DEFAULT_COORDINATE_SYSTEM = "world"
58
+ DEFAULT_INIT_TIMEOUT = 5000
59
+ DEFAULT_ORIENTATION = "coordsys" as JoggerOrientation
60
+
61
+ mode: JoggerMode = "off"
62
+ joggingSocket: AutoReconnectingWebsocket | null = null
63
+ trajectorySocket: AutoReconnectingWebsocket | null = null
64
+ timeout: number = this.DEFAULT_INIT_TIMEOUT
65
+ tcp: string
66
+ // coordinateSystem?: string
67
+ orientation: JoggerOrientation
68
+ onError?: (err: unknown) => void
69
+
70
+ /**
71
+ * Initialize the jogging connection using jogging endpoint or trajectory endpoint depending on the selected mode.
72
+ *
73
+ * @param nova - The Nova client instance
74
+ * @param motionGroupId - The ID of the motion group to connect to
75
+ * @param options - Configuration options for the jogger connection
76
+ * @param options.mode - The jogging mode to initialize:
77
+ * - "jogging": Continuous jogging mode with persistent websocket for real-time velocity commands
78
+ * - "trajectory": Incremental jogging mode for fixed-distance motions via trajectory planning
79
+ * - "off": No jogging enabled, all websockets closed (default)
80
+ * @param options.timeout - Timeout in milliseconds for websocket initialization (default: 5000ms)
81
+ * @param options.tcp - TCP frame to use for motions (default: from motion group)
82
+ * //param options.coordinateSystem - Coordinate system for jogging commands (default: "world"). Please note: Currently not implemented
83
+ * @param options.orientation - If set to "tool", jogging TcpVelocityRequest will use `use_tool_coordinate_system` option (default: "coordsys")
84
+ * @param options.onError - Error handler for websocket errors
85
+ * @returns Promise resolving to initialized JoggerConnection instance
86
+ */
87
+ static async open(
88
+ nova: NovaClient,
89
+ motionGroupId: string,
90
+ options: JoggerConnectionOptions = {},
91
+ ) {
92
+ // Get matching motion stream
93
+ const motionStream = await MotionStreamConnection.open(nova, motionGroupId)
94
+
95
+ // Initialize jogger with options
96
+ const jogger = new JoggerConnection(motionStream, options)
97
+
98
+ // Initialize the jogging websocket, if necessary (mode is "jogging")
99
+ await jogger.setJoggingMode(jogger.mode)
100
+
101
+ // Return the initialized jogger
102
+ return jogger
103
+ }
104
+
105
+ constructor(
106
+ readonly motionStream: MotionStreamConnection,
107
+ readonly options: JoggerConnectionOptions | undefined = {},
108
+ ) {
109
+ this.tcp = options?.tcp || motionStream.motionGroup.tcp || this.DEFAULT_TCP
110
+ // this.coordinateSystem = options?.coordinateSystem || this.DEFAULT_COORDINATE_SYSTEM
111
+ this.orientation = options?.orientation || this.DEFAULT_ORIENTATION
112
+ this.timeout = options?.timeout || this.DEFAULT_INIT_TIMEOUT
113
+ this.mode = options?.mode || this.DEFAULT_MODE
114
+ this.onError = options?.onError
115
+ }
116
+
117
+ // Set a new tcp or other options. If current mode is jogging, will reinitialize the jogging websocket
118
+ async setOptions(options: Partial<JoggerConnectionOptions>) {
119
+ if (options.tcp) {
120
+ this.tcp = options.tcp
121
+ }
122
+
123
+ if (options.orientation) {
124
+ this.orientation = options.orientation
125
+ }
126
+
127
+ // if (options.coordinateSystem) {
128
+ // this.coordinateSystem = options.coordinateSystem
129
+ // }
130
+
131
+ if (options.timeout) {
132
+ this.timeout = options.timeout
133
+ }
134
+
135
+ if (options.mode) {
136
+ this.mode = options.mode
137
+ }
138
+
139
+ if (options.onError) {
140
+ this.onError = options.onError
141
+ }
142
+
143
+ this.setJoggingMode(this.mode, false)
144
+ }
145
+
146
+ get motionGroupId() {
147
+ return this.motionStream.motionGroupId
148
+ }
149
+
150
+ get nova() {
151
+ return this.motionStream.nova
152
+ }
153
+
154
+ get numJoints() {
155
+ return this.motionStream.joints.length
156
+ }
157
+
158
+ // get activeJoggingMode() {
159
+ // return this.mode
160
+ // }
161
+
162
+ // get activeWebsocket() {
163
+ // return this.mode === "jogging" ? this.joggingSocket : this.trajectorySocket;
164
+ // }
165
+
166
+ // Sends stop movement command to robot
167
+ async stop() {
168
+ if (this.joggingSocket) {
169
+ const velocity = new Array(this.numJoints).fill(0)
170
+ this.joggingSocket.sendJson({
171
+ message_type: "JointVelocityRequest",
172
+ velocity,
173
+ })
174
+ }
175
+
176
+ if (this.trajectorySocket) {
177
+ this.trajectorySocket.sendJson({
178
+ message_type: "PauseMovementRequest",
179
+ })
180
+ }
181
+ }
182
+
183
+ // Dispose the jogger, closing all open websockets
184
+ async dispose() {
185
+ // Collect all initialized sockets
186
+ const sockets = [this.joggingSocket, this.trajectorySocket].filter(
187
+ (s) => s !== null,
188
+ ) as AutoReconnectingWebsocket[]
189
+
190
+ // Call them to dispose
191
+ sockets.forEach((s) => {
192
+ s.dispose()
193
+ })
194
+
195
+ // Remove references
196
+ this.joggingSocket = null
197
+ this.trajectorySocket = null
198
+
199
+ // Return promise that resolves when all sockets are closed
200
+ return Promise.all(sockets.map((s) => s.closed()))
201
+ }
202
+
203
+ // Activate jogger in one of the supported modes
204
+ // Will iniitialize or close websockets as necessary
205
+ // If mode is unchanged, does nothing (unless skipReinitializeIfSameMode is false)
206
+ async setJoggingMode(mode: JoggerMode, skipReinitializeIfSameMode = true) {
207
+ // If not changing mode, do nothing
208
+ if (this.mode === mode && skipReinitializeIfSameMode) {
209
+ return
210
+ }
211
+
212
+ // Close any previous websocket connection
213
+ this.dispose()
214
+
215
+ // Set the new mode
216
+ this.mode = mode
217
+
218
+ // Mode is "jogging" - open jogging websocket
219
+ if (this.mode === "jogging") {
220
+ return this.initializeJoggingWebsocket()
221
+ }
222
+
223
+ // Mode is "trajectory" or "off" - nothing more to do
224
+ return
225
+ }
226
+
227
+ // Initializes continuous jogging websocket, called by setJoggingMode("jogging")
228
+ async initializeJoggingWebsocket() {
229
+ // Wait for initialization with timeout
230
+ return new Promise<void>((resolve, reject) => {
231
+ const connectionFailedTimeout = setTimeout(() => {
232
+ reject(
233
+ new Error(
234
+ `Jogging initialization timeout after ${this.timeout} seconds`,
235
+ ),
236
+ )
237
+ }, this.timeout)
238
+
239
+ this.joggingSocket = this.nova.openReconnectingWebsocket(
240
+ this.ENDPOINT_JOGGING,
241
+ )
242
+ this.joggingSocket.addEventListener("message", (ev: MessageEvent) => {
243
+ const data = tryParseJson(ev.data)
244
+
245
+ if (data?.result?.kind === "INITIALIZE_RECEIVED") {
246
+ clearTimeout(connectionFailedTimeout)
247
+ resolve()
248
+ return
249
+ }
250
+
251
+ if (
252
+ (data && "error" in data) ||
253
+ data?.result?.kind === "MOTION_ERROR"
254
+ ) {
255
+ clearTimeout(connectionFailedTimeout)
256
+ if (this.onError) {
257
+ this.onError(ev.data)
258
+ } else {
259
+ reject(new Error(ev.data))
260
+ }
261
+ }
262
+ })
263
+
264
+ this.joggingSocket.sendJson({
265
+ message_type: "InitializeJoggingRequest",
266
+ motion_group: this.motionGroupId,
267
+ tcp: this.tcp,
268
+ })
269
+ })
270
+ }
271
+
272
+ /**
273
+ * Jogging: Start rotation of a single robot joint at the specified velocity
274
+ */
275
+ async rotateJoints({
276
+ joint,
277
+ direction,
278
+ velocityRadsPerSec,
279
+ }: {
280
+ /** Index of the joint to rotate */
281
+ joint: number
282
+ /** Direction of rotation ("+" or "-") */
283
+ direction: "+" | "-"
284
+ /** Speed of the rotation in radians per second */
285
+ velocityRadsPerSec: number
286
+ }) {
287
+ if (!this.joggingSocket || this.mode !== "jogging") {
288
+ throw new Error(
289
+ "Joint jogging websocket not connected; create one by setting jogging mode to 'jogging'",
290
+ )
291
+ }
292
+
293
+ const velocity = new Array(this.numJoints).fill(0)
294
+
295
+ velocity[joint] =
296
+ direction === "-" ? -velocityRadsPerSec : velocityRadsPerSec
297
+
298
+ this.joggingSocket.sendJson({
299
+ message_type: "JointVelocityRequest",
300
+ velocity,
301
+ })
302
+ }
303
+
304
+ /**
305
+ * Jogging: Start the TCP moving along a specified axis at a given velocity
306
+ */
307
+ async translateTCP({
308
+ axis,
309
+ direction,
310
+ velocityMmPerSec,
311
+ }: {
312
+ axis: "x" | "y" | "z"
313
+ direction: "-" | "+"
314
+ velocityMmPerSec: number
315
+ }) {
316
+ if (!this.joggingSocket || this.mode !== "jogging") {
317
+ throw new Error(
318
+ "Continuous jogging websocket not connected; create one by setting jogging mode to 'jogging'",
319
+ )
320
+ }
321
+ const rotation = [0, 0, 0]
322
+ const translation = [0, 0, 0]
323
+ translation[XYZ_TO_VECTOR[axis]] =
324
+ direction === "-" ? -velocityMmPerSec : velocityMmPerSec
325
+
326
+ this.joggingSocket.sendJson({
327
+ message_type: "TcpVelocityRequest",
328
+ translation,
329
+ rotation,
330
+ use_tool_coordinate_system: this.orientation === "tool",
331
+ })
332
+ }
333
+
334
+ /**
335
+ * Jogging: Start the TCP rotating around a specified axis at a given velocity
336
+ */
337
+ async rotateTCP({
338
+ axis,
339
+ direction,
340
+ velocityRadsPerSec,
341
+ }: {
342
+ axis: "x" | "y" | "z"
343
+ direction: "-" | "+"
344
+ velocityRadsPerSec: number
345
+ }) {
346
+ if (!this.joggingSocket || this.mode !== "jogging") {
347
+ throw new Error(
348
+ "Continuous jogging websocket not connected; create one by setting jogging mode to 'jogging'",
349
+ )
350
+ }
351
+ const rotation = [0, 0, 0]
352
+ const translation = [0, 0, 0]
353
+ rotation[XYZ_TO_VECTOR[axis]] =
354
+ direction === "-" ? -velocityRadsPerSec : velocityRadsPerSec
355
+
356
+ this.joggingSocket.sendJson({
357
+ message_type: "TcpVelocityRequest",
358
+ translation,
359
+ rotation,
360
+ })
361
+ }
362
+
363
+ /**
364
+ * Trajectory jogging:
365
+ *
366
+ * Move the robot by a fixed distance in a single cartesian
367
+ * axis, either rotating or translating relative to the TCP.
368
+ * Promise resolves only after the motion has completed.
369
+ */
370
+ async runIncrementalCartesianMotion({
371
+ currentTcpPose,
372
+ currentJoints,
373
+ coordSystemId,
374
+ velocityInRelevantUnits,
375
+ axis,
376
+ direction,
377
+ motion,
378
+ }: {
379
+ currentTcpPose: Pose
380
+ currentJoints: Vector3Simple
381
+ coordSystemId: string
382
+ velocityInRelevantUnits: number
383
+ axis: "x" | "y" | "z"
384
+ direction: "-" | "+"
385
+ motion:
386
+ | {
387
+ type: "rotate"
388
+ distanceRads: number
389
+ }
390
+ | {
391
+ type: "translate"
392
+ distanceMm: number
393
+ }
394
+ }) {
395
+ const commands: MotionCommand[] = []
396
+
397
+ if (this.mode !== "trajectory") {
398
+ throw new Error(
399
+ "Set jogging mode to 'trajectory' to run incremental cartesian motions",
400
+ )
401
+ }
402
+
403
+ if (motion.type === "translate") {
404
+ if (!currentTcpPose.position) {
405
+ throw new Error(
406
+ "Current pose has no position, cannot perform translation",
407
+ )
408
+ }
409
+
410
+ const targetTcpPosition = [...currentTcpPose.position]
411
+ targetTcpPosition[XYZ_TO_VECTOR[axis]] +=
412
+ motion.distanceMm * (direction === "-" ? -1 : 1)
413
+
414
+ commands.push({
415
+ limits_override: {
416
+ tcp_velocity_limit: velocityInRelevantUnits,
417
+ },
418
+ path: {
419
+ path_definition_name: "PathLine",
420
+ target_pose: {
421
+ position: targetTcpPosition,
422
+ orientation: currentTcpPose.orientation,
423
+ },
424
+ },
425
+ })
426
+ } else if (motion.type === "rotate") {
427
+ // Concatenate rotations expressed by rotation vectors
428
+ // Equations taken from https://physics.stackexchange.com/a/287819
429
+
430
+ if (!currentTcpPose.orientation) {
431
+ throw new Error(
432
+ "Current pose has no orientation, cannot perform rotation",
433
+ )
434
+ }
435
+
436
+ // Compute axis and angle of current rotation vector
437
+ const currentRotationVector = new Vector3(
438
+ currentTcpPose.orientation[0],
439
+ currentTcpPose.orientation[1],
440
+ currentTcpPose.orientation[2],
441
+ )
442
+
443
+ const currentRotationRad = currentRotationVector.length()
444
+ const currentRotationDirection = currentRotationVector.clone().normalize()
445
+
446
+ // Compute axis and angle of difference rotation vector
447
+ const differenceRotationRad =
448
+ motion.distanceRads * (direction === "-" ? -1 : 1)
449
+
450
+ const differenceRotationDirection = new Vector3(0.0, 0.0, 0.0)
451
+ differenceRotationDirection[axis] = 1.0
452
+
453
+ // Some abbreviations to make the following equations more readable
454
+ const f1 =
455
+ Math.cos(0.5 * differenceRotationRad) *
456
+ Math.cos(0.5 * currentRotationRad)
457
+ const f2 =
458
+ Math.sin(0.5 * differenceRotationRad) *
459
+ Math.sin(0.5 * currentRotationRad)
460
+ const f3 =
461
+ Math.sin(0.5 * differenceRotationRad) *
462
+ Math.cos(0.5 * currentRotationRad)
463
+ const f4 =
464
+ Math.cos(0.5 * differenceRotationRad) *
465
+ Math.sin(0.5 * currentRotationRad)
466
+
467
+ const dotProduct = differenceRotationDirection.dot(
468
+ currentRotationDirection,
469
+ )
470
+
471
+ const crossProduct = differenceRotationDirection
472
+ .clone()
473
+ .cross(currentRotationDirection)
474
+
475
+ // Compute angle of concatenated rotation
476
+ const newRotationRad = 2.0 * Math.acos(f1 - f2 * dotProduct)
477
+
478
+ // Compute rotation vector of concatenated rotation
479
+ const f5 = newRotationRad / Math.sin(0.5 * newRotationRad)
480
+
481
+ const targetTcpOrientation = new Vector3()
482
+ .addScaledVector(crossProduct, f2)
483
+ .addScaledVector(differenceRotationDirection, f3)
484
+ .addScaledVector(currentRotationDirection, f4)
485
+ .multiplyScalar(f5)
486
+
487
+ commands.push({
488
+ limits_override: {
489
+ tcp_orientation_velocity_limit: velocityInRelevantUnits,
490
+ },
491
+ path: {
492
+ path_definition_name: "PathLine",
493
+ target_pose: {
494
+ position: currentTcpPose.position,
495
+ orientation: [...targetTcpOrientation],
496
+ },
497
+ },
498
+ })
499
+ }
500
+
501
+ // Plan the motion https://portal.wandelbots.io/docs/api/v2/ui/#/operations/planTrajectory
502
+ const description = this.motionStream.description
503
+ if (description.cycle_time === undefined) {
504
+ console.warn(
505
+ "Current motion group has no cycle time, cannot plan jogging motion",
506
+ )
507
+ return
508
+ }
509
+
510
+ const motion_group_setup = {
511
+ motion_group_model: description.motion_group_model,
512
+ cycle_time: description.cycle_time,
513
+ mounting: description.mounting,
514
+
515
+ // tcp_offset: description.tcp_offset, TODO: implement tcp_offset handling
516
+ // FIXME use proper mode's limits if set
517
+ global: description.operation_limits.auto_limits,
518
+ }
519
+
520
+ const motionPlanRes = await this.nova.api.trajectoryPlanning.planTrajectory(
521
+ {
522
+ motion_group_setup,
523
+ start_joint_position: currentJoints,
524
+ motion_commands: commands,
525
+ },
526
+ )
527
+
528
+ const trajectoryData = motionPlanRes.response
529
+ if (!trajectoryData) {
530
+ throw new Error(
531
+ `Failed to plan jogging increment motion ${JSON.stringify(motionPlanRes)}`,
532
+ )
533
+ }
534
+
535
+ if (this.trajectorySocket) {
536
+ console.warn("Trajectory jogging websocket already open; will close")
537
+ this.trajectorySocket.dispose()
538
+ }
539
+
540
+ // Execute the planned motion https://portal.wandelbots.io/docs/api/v2/ui/#/operations/executeTrajectory
541
+ this.trajectorySocket = this.nova.openReconnectingWebsocket(
542
+ this.ENDPOINT_TRAJECTORY,
543
+ )
544
+
545
+ const messageInitializeMovementResponse = (
546
+ result: InitializeMovementResponse | undefined,
547
+ ) => {
548
+ // Handle errorous response
549
+ if (!result || result.add_trajectory_error || result.message) {
550
+ if (this.onError) {
551
+ this.onError(result)
552
+ } else {
553
+ throw new Error(
554
+ result?.add_trajectory_error?.message ||
555
+ result?.message ||
556
+ "Failed to execute trajectory, unknown error",
557
+ )
558
+ }
559
+ }
560
+
561
+ // Handle socket gone
562
+ if (!this.trajectorySocket) {
563
+ throw new Error(
564
+ `Failed to execute trajectory, websocket not available anymore`,
565
+ )
566
+ }
567
+
568
+ // Trajectory locked, now start movement
569
+ this.trajectorySocket.sendJson({
570
+ message_type: "StartMovementRequest",
571
+ direction: "DIRECTION_FORWARD",
572
+ })
573
+ }
574
+
575
+ const waitForMovementToStartAndFinish = async () => {
576
+ // Wait for robot to start moving (standstill becomes false)
577
+ await when(() => !this.motionStream.rapidlyChangingMotionState.standstill)
578
+
579
+ // Then wait for robot to stop moving (standstill becomes true)
580
+ await when(() => this.motionStream.rapidlyChangingMotionState.standstill)
581
+
582
+ // Close connection and free robot
583
+ this.trajectorySocket?.dispose()
584
+ this.trajectorySocket = null
585
+ }
586
+
587
+ const waitForMovementToFinish = async () => {
588
+ // Wait for robot to stop moving (standstill becomes true)
589
+ await when(() => this.motionStream.rapidlyChangingMotionState.standstill)
590
+
591
+ // Close connection and free robot
592
+ this.trajectorySocket?.dispose()
593
+ this.trajectorySocket = null
594
+ }
595
+
596
+ const messageStartMovementResponse = async (
597
+ data: StartMovementResponse,
598
+ ) => {
599
+ if (data?.message) {
600
+ if (this.onError) {
601
+ this.onError(data)
602
+ return
603
+ } else {
604
+ throw new Error(
605
+ data.message || "Failed to execute trajectory, unknown error",
606
+ )
607
+ }
608
+ }
609
+
610
+ // Movement started we now wait to verify the robot is moving
611
+ // by observing changes to motion state
612
+ if (this.motionStream.rapidlyChangingMotionState.standstill) {
613
+ await waitForMovementToStartAndFinish()
614
+ } else {
615
+ await waitForMovementToFinish()
616
+ }
617
+ }
618
+
619
+ this.trajectorySocket.addEventListener("message", (ev: MessageEvent) => {
620
+ const data = tryParseJson(ev.data)
621
+
622
+ if (!data?.result?.kind) {
623
+ throw new Error(
624
+ `Failed to execute trajectory: Received invalid message ${ev.data}`,
625
+ )
626
+ }
627
+
628
+ if (data.result.kind === "INITIALIZE_RECEIVED") {
629
+ messageInitializeMovementResponse(data.result)
630
+ } else if (data.result.kind === "START_RECEIVED") {
631
+ messageStartMovementResponse(data)
632
+ } else if (data.result.kind === "PAUSE_RECEIVED") {
633
+ // do nothing
634
+ } else if (data.result.kind === "MOTION_ERROR" && data.result.message) {
635
+ if (this.onError) {
636
+ this.onError(data)
637
+ return
638
+ } else {
639
+ throw new Error(data.result.message)
640
+ }
641
+ } else {
642
+ throw new Error(
643
+ `Failed to execute trajectory, cannot handle message type "${data.result.kind}"`,
644
+ )
645
+ }
646
+ })
647
+
648
+ // Send initialization/movement request
649
+ this.trajectorySocket.sendJson({
650
+ message_type: "InitializeMovementRequest",
651
+ trajectory: {
652
+ message_type: "TrajectoryData",
653
+ motion_group: this.motionGroupId,
654
+ data: trajectoryData,
655
+ tcp: this.tcp,
656
+ },
657
+ })
658
+ }
659
+ }
@@ -0,0 +1,23 @@
1
+ import { NovaClient } from "@wandelbots/nova-js/v2"
2
+ import { expect, test } from "vitest"
3
+ import { MotionStreamConnection } from "./MotionStreamConnection"
4
+
5
+ test("motion stream", async () => {
6
+ const nova = new NovaClient({
7
+ instanceUrl: "https://mock.example.com",
8
+ })
9
+
10
+ const motionStream = await MotionStreamConnection.open(nova, "0@mock-ur5e")
11
+ expect(motionStream.joints.length).toBe(6)
12
+
13
+ // Test changing the url
14
+ motionStream.motionStateSocket.changeUrl(
15
+ nova.makeWebsocketURL("/motion-groups/0@mock-ur5e/state-stream?tcp=foo"),
16
+ )
17
+
18
+ await motionStream.motionStateSocket.firstMessage()
19
+
20
+ expect(motionStream.motionStateSocket.url).toBe(
21
+ "wss://mock.example.com/api/v2/cells/cell/motion-groups/0@mock-ur5e/state-stream?tcp=foo",
22
+ )
23
+ })