@wandelbots/nova-js 3.2.0-pr.dev-e2e-jogging-test.143.4f02caf → 3.2.0-pr.feat-v2.155.e91b019

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