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

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 (82) 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 +14010 -18713
  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/JoggerConnection.test.d.ts +2 -0
  44. package/dist/lib/JoggerConnection.test.d.ts.map +1 -0
  45. package/dist/lib/MotionStreamConnection.d.ts +24 -0
  46. package/dist/lib/MotionStreamConnection.d.ts.map +1 -0
  47. package/dist/lib/MotionStreamConnection.test.d.ts +2 -0
  48. package/dist/lib/MotionStreamConnection.test.d.ts.map +1 -0
  49. package/dist/lib/motionStateUpdate.d.ts +5 -0
  50. package/dist/lib/motionStateUpdate.d.ts.map +1 -0
  51. package/dist/lib/motionStateUpdate.test.d.ts +2 -0
  52. package/dist/lib/motionStateUpdate.test.d.ts.map +1 -0
  53. package/package.json +2 -2
  54. package/src/components/3d-viewport/CoordinateSystemTransform.tsx +1 -1
  55. package/src/components/3d-viewport/SafetyZonesRenderer.tsx +1 -2
  56. package/src/components/3d-viewport/collider/ColliderCollection.tsx +1 -1
  57. package/src/components/3d-viewport/collider/ColliderElement.tsx +1 -1
  58. package/src/components/3d-viewport/collider/CollisionSceneRenderer.tsx +1 -1
  59. package/src/components/3d-viewport/collider/colliderShapeToBufferGeometry.ts +1 -1
  60. package/src/components/jogging/JoggingCartesianTab.tsx +13 -11
  61. package/src/components/jogging/JoggingJointLimitDetector.tsx +2 -2
  62. package/src/components/jogging/JoggingJointTab.tsx +4 -3
  63. package/src/components/jogging/JoggingPanel.tsx +3 -2
  64. package/src/components/jogging/JoggingStore.ts +44 -39
  65. package/src/components/jogging/PoseCartesianValues.tsx +3 -4
  66. package/src/components/jogging/PoseJointValues.tsx +3 -4
  67. package/src/components/robots/DHRobot.tsx +1 -1
  68. package/src/components/robots/RobotAnimator.tsx +1 -1
  69. package/src/components/robots/SupportedRobot.tsx +1 -1
  70. package/src/components/robots/manufacturerHomePositions.ts +1 -1
  71. package/src/components/utils/errorHandling.test.ts +41 -0
  72. package/src/lib/ConnectedMotionGroup.ts +426 -0
  73. package/src/lib/JoggerConnection.test.ts +120 -0
  74. package/src/lib/JoggerConnection.ts +659 -0
  75. package/src/lib/MotionStreamConnection.test.ts +23 -0
  76. package/src/lib/MotionStreamConnection.ts +221 -0
  77. package/src/lib/motionStateUpdate.test.ts +28 -0
  78. package/src/lib/motionStateUpdate.ts +76 -0
  79. package/dist/auth0-spa-js.production.esm-1QXzndwB.js +0 -950
  80. package/dist/auth0-spa-js.production.esm-1QXzndwB.js.map +0 -1
  81. package/dist/auth0-spa-js.production.esm-BLRAk7Yh.cjs +0 -5
  82. package/dist/auth0-spa-js.production.esm-BLRAk7Yh.cjs.map +0 -1
@@ -1,13 +1,12 @@
1
1
  import { Button, Stack } from "@mui/material"
2
- import { poseToWandelscriptString } from "@wandelbots/nova-js"
3
- import type { TcpPose } from "@wandelbots/nova-js/v1"
2
+ import { poseToWandelscriptString, type Pose } from "@wandelbots/nova-js/v2"
4
3
  import { observer } from "mobx-react-lite"
5
4
  import { useState } from "react"
6
5
  import { externalizeComponent } from "../../externalizeComponent"
7
6
  import { CopyableText } from "../CopyableText"
8
7
 
9
8
  export type PoseCartesianValuesProps = {
10
- tcpPose: TcpPose
9
+ tcpPose: Pose
11
10
  showCopyButton?: boolean
12
11
  }
13
12
 
@@ -44,7 +43,7 @@ export const PoseCartesianValues = externalizeComponent(
44
43
  onClick={handleCopy}
45
44
  sx={{ flexShrink: 0 }}
46
45
  >
47
- { copyMessage ? copyMessage : "Copy"}
46
+ {copyMessage ? copyMessage : "Copy"}
48
47
  </Button>
49
48
  )}
50
49
  </Stack>
@@ -1,19 +1,18 @@
1
1
  import { Button, Stack } from "@mui/material"
2
- import type { Joints } from "@wandelbots/nova-api/v1"
3
2
  import { observer } from "mobx-react-lite"
4
3
  import { useState } from "react"
5
4
  import { externalizeComponent } from "../../externalizeComponent"
6
5
  import { CopyableText } from "../CopyableText"
7
6
 
8
7
  export type PoseJointValuesProps = {
9
- joints: Joints
8
+ joints: Array<number>
10
9
  showCopyButton?: boolean
11
10
  }
12
11
 
13
12
  export const PoseJointValues = externalizeComponent(
14
13
  observer(({ joints, showCopyButton = false }: PoseJointValuesProps) => {
15
14
  const [copyMessage, setCopyMessage] = useState<string | null>(null)
16
- const poseString = `[${joints.joints.map((j: number) => parseFloat(j.toFixed(4))).join(", ")}]`
15
+ const poseString = `[${joints.map((j: number) => parseFloat(j.toFixed(4))).join(", ")}]`
17
16
 
18
17
  const handleCopy = async () => {
19
18
  try {
@@ -43,7 +42,7 @@ export const PoseJointValues = externalizeComponent(
43
42
  onClick={handleCopy}
44
43
  sx={{ flexShrink: 0 }}
45
44
  >
46
- { copyMessage ? copyMessage : "Copy"}
45
+ {copyMessage ? copyMessage : "Copy"}
47
46
  </Button>
48
47
  )}
49
48
  </Stack>
@@ -1,5 +1,5 @@
1
1
  import { Line } from "@react-three/drei"
2
- import type { DHParameter } from "@wandelbots/nova-api/v1"
2
+ import type { DHParameter } from "@wandelbots/nova-js/v1"
3
3
  import React, { useRef } from "react"
4
4
  import type * as THREE from "three"
5
5
  import { Matrix4, Quaternion, Vector3 } from "three"
@@ -2,7 +2,7 @@ import { useFrame, useThree } from "@react-three/fiber"
2
2
  import type {
3
3
  DHParameter,
4
4
  MotionGroupStateResponse,
5
- } from "@wandelbots/nova-api/v1"
5
+ } from "@wandelbots/nova-js/v1"
6
6
  import React, { useEffect, useRef } from "react"
7
7
  import type { Group, Object3D } from "three"
8
8
  import { useAutorun } from "../utils/hooks"
@@ -2,7 +2,7 @@ import type { ThreeElements } from "@react-three/fiber"
2
2
  import type {
3
3
  DHParameter,
4
4
  MotionGroupStateResponse,
5
- } from "@wandelbots/nova-api/v1"
5
+ } from "@wandelbots/nova-js/v1"
6
6
  import { Suspense, useCallback, useEffect, useState } from "react"
7
7
  import { DHRobot } from "./DHRobot"
8
8
 
@@ -1,4 +1,4 @@
1
- import { Manufacturer } from "@wandelbots/nova-api/v1"
1
+ import { Manufacturer } from "@wandelbots/nova-js/v1"
2
2
 
3
3
  /**
4
4
  * Default home configs for different robot manufacturers.
@@ -0,0 +1,41 @@
1
+ import axios, { AxiosError, AxiosHeaders } from "axios"
2
+ import { expect, test } from "vitest"
3
+ import { makeErrorMessage } from "./errorHandling"
4
+
5
+ test("making useful error messages", async () => {
6
+ // Error objects take the message
7
+ const someCustomError = new Error("some custom error")
8
+ expect(makeErrorMessage(someCustomError)).toEqual("some custom error")
9
+
10
+ // Strings go through prefixed
11
+ expect(makeErrorMessage("some string")).toEqual(
12
+ 'Unexpected error: "some string"',
13
+ )
14
+
15
+ // Random objects get serialized
16
+ expect(makeErrorMessage({ some: "object" })).toEqual(
17
+ 'Unexpected error: {"some":"object"}',
18
+ )
19
+
20
+ // Axios errors with a response should include the response code
21
+ // and url - but not for 404, we use a friendly message for that
22
+ try {
23
+ await axios.get("http://example.com/doesnt-exist")
24
+ expect(true).toBe(false)
25
+ } catch (err) {
26
+ expect(makeErrorMessage(err)).toMatch(
27
+ "Failed to connect to the server. Please check your internet connection.",
28
+ )
29
+ }
30
+
31
+ // Not sure how to reproduce CORS errors naturally in vitest environment
32
+ // so let's create it manually
33
+ const networkError = new AxiosError("Network Error", "ERR_NETWORK", {
34
+ url: "http://example.com/some-cors-thing",
35
+ method: "post",
36
+ headers: new AxiosHeaders(),
37
+ })
38
+ expect(makeErrorMessage(networkError)).toEqual(
39
+ "Failed to connect to the server. Please check your internet connection.",
40
+ )
41
+ })
@@ -0,0 +1,426 @@
1
+ import {
2
+ tryParseJson,
3
+ type AutoReconnectingWebsocket,
4
+ } from "@wandelbots/nova-js"
5
+ import type {
6
+ MotionGroupDescription,
7
+ MotionGroupState,
8
+ NovaClient,
9
+ OperationMode,
10
+ RobotControllerState,
11
+ SafetyStateType,
12
+ } from "@wandelbots/nova-js/v2"
13
+ import { makeAutoObservable, runInAction } from "mobx"
14
+ import * as THREE from "three"
15
+ import type { Vector3Simple } from "./JoggerConnection"
16
+ import { jointValuesEqual, tcpMotionEqual } from "./motionStateUpdate"
17
+
18
+ const MOTION_DELTA_THRESHOLD = 0.0001
19
+
20
+ export type RobotTcpLike = {
21
+ id: string
22
+ readable_name: string
23
+ position: Vector3Simple
24
+ orientation: Vector3Simple
25
+ }
26
+
27
+ export type MotionGroupOption = {
28
+ selectionId: string
29
+ }
30
+
31
+ /**
32
+ * Store representing the current state of a connected motion group.
33
+ * API v2 version, not used yet in the components.
34
+ */
35
+ export class ConnectedMotionGroup {
36
+ static async connectMultiple(nova: NovaClient, motionGroupIds: string[]) {
37
+ return Promise.all(
38
+ motionGroupIds.map((motionGroupId) =>
39
+ ConnectedMotionGroup.connect(nova, motionGroupId),
40
+ ),
41
+ )
42
+ }
43
+
44
+ static async connect(nova: NovaClient, motionGroupId: string) {
45
+ const [_motionGroupIndex, controllerId] = motionGroupId.split("@") as [
46
+ string,
47
+ string,
48
+ ]
49
+
50
+ const controller =
51
+ await nova.api.controller.getCurrentRobotControllerState(controllerId)
52
+ const motionGroup = controller?.motion_groups.find(
53
+ (mg) => mg.motion_group === motionGroupId,
54
+ )
55
+ if (!controller || !motionGroup) {
56
+ throw new Error(
57
+ `Controller ${controllerId} or motion group ${motionGroupId} not found`,
58
+ )
59
+ }
60
+
61
+ const motionStateSocket = nova.openReconnectingWebsocket(
62
+ `/controllers/${controllerId}/motion-groups/${motionGroupId}/state-stream`,
63
+ )
64
+
65
+ // Wait for the first message to get the initial state
66
+ const firstMessage = await motionStateSocket.firstMessage()
67
+ const initialMotionState = tryParseJson(firstMessage.data)
68
+ ?.result as MotionGroupState
69
+
70
+ if (!initialMotionState) {
71
+ throw new Error(
72
+ `Unable to parse initial motion state message ${firstMessage.data}`,
73
+ )
74
+ }
75
+
76
+ console.log(
77
+ `Connected motion state websocket to motion group ${motionGroup.motion_group}. Initial state:\n `,
78
+ initialMotionState,
79
+ )
80
+
81
+ // Check if robot is virtual or physical
82
+ const config = await nova.api.controller.getRobotController(
83
+ controller.controller,
84
+ )
85
+ const isVirtual = config.configuration.kind === "VirtualController"
86
+
87
+ // If there's a configured mounting, we need it to show the right
88
+ // position of the robot model
89
+ const description = await nova.api.motionGroup.getMotionGroupDescription(
90
+ controllerId,
91
+ motionGroup.motion_group,
92
+ )
93
+
94
+ // Find out what TCPs this motion group has (we need it for jogging)
95
+ // There are converted into a RobotTcpLike for easier use in the UI
96
+ const tcps: RobotTcpLike[] = Object.entries(description.tcps || {}).map(
97
+ ([id, tcp]) => ({
98
+ id,
99
+ readable_name: tcp.name,
100
+ position: tcp.pose.position as Vector3Simple,
101
+ orientation: tcp.pose.orientation as Vector3Simple,
102
+ }),
103
+ )
104
+
105
+ // Open the websocket to monitor controller state for e.g. e-stop
106
+ const controllerStateSocket = nova.openReconnectingWebsocket(
107
+ `/controllers/${controller.controller}/state-stream?response_rate=1000`,
108
+ )
109
+
110
+ // Wait for the first message to get the initial state
111
+ const firstControllerMessage = await controllerStateSocket.firstMessage()
112
+ const initialControllerState = tryParseJson(firstControllerMessage.data)
113
+ ?.result as RobotControllerState
114
+
115
+ if (!initialControllerState) {
116
+ throw new Error(
117
+ `Unable to parse initial controller state message ${firstControllerMessage.data}`,
118
+ )
119
+ }
120
+
121
+ console.log(
122
+ `Connected controller state websocket to controller ${controller.controller}. Initial state:\n `,
123
+ initialControllerState,
124
+ )
125
+
126
+ return new ConnectedMotionGroup(
127
+ nova,
128
+ controller,
129
+ motionGroup,
130
+ initialMotionState,
131
+ motionStateSocket,
132
+ isVirtual,
133
+ tcps,
134
+ description,
135
+ initialControllerState,
136
+ controllerStateSocket,
137
+ )
138
+ }
139
+
140
+ connectedJoggingSocket: WebSocket | null = null
141
+ // biome-ignore lint/suspicious/noExplicitAny: legacy code
142
+ planData: any | null // tmp
143
+ joggingVelocity: number = 10
144
+
145
+ // Not mobx-observable as this changes very fast; should be observed
146
+ // using animation frames
147
+ rapidlyChangingMotionState: MotionGroupState
148
+
149
+ // Response rate on the websocket should be a bit slower on this one since
150
+ // we don't use the motion data
151
+ controllerState: RobotControllerState
152
+
153
+ /**
154
+ * Reflects activation state of the motion group / robot servos. The
155
+ * movement controls in the UI should only be enabled in the "active" state
156
+ */
157
+ activationState: "inactive" | "activating" | "deactivating" | "active" =
158
+ "inactive"
159
+
160
+ constructor(
161
+ readonly nova: NovaClient,
162
+ readonly controller: RobotControllerState,
163
+ readonly motionGroup: MotionGroupState,
164
+ readonly initialMotionState: MotionGroupState,
165
+ readonly motionStateSocket: AutoReconnectingWebsocket,
166
+ readonly isVirtual: boolean,
167
+ readonly tcps: RobotTcpLike[],
168
+ readonly description: MotionGroupDescription,
169
+ readonly initialControllerState: RobotControllerState,
170
+ readonly controllerStateSocket: AutoReconnectingWebsocket,
171
+ ) {
172
+ this.rapidlyChangingMotionState = initialMotionState
173
+ this.controllerState = initialControllerState
174
+
175
+ // Track controller state updates (e.g. safety state and operation mode)
176
+ controllerStateSocket.addEventListener("message", (event) => {
177
+ const data = tryParseJson(event.data)?.result
178
+
179
+ if (!data) {
180
+ return
181
+ }
182
+
183
+ runInAction(() => {
184
+ this.controllerState = data
185
+ })
186
+ })
187
+
188
+ motionStateSocket.addEventListener("message", (event) => {
189
+ const latestMotionState = tryParseJson(event.data)?.result as
190
+ | MotionGroupState
191
+ | undefined
192
+
193
+ if (!latestMotionState) {
194
+ throw new Error(
195
+ `Failed to get motion state for ${this.motionGroupId}: ${event.data}`,
196
+ )
197
+ }
198
+
199
+ // handle motionState message
200
+ if (
201
+ !jointValuesEqual(
202
+ this.rapidlyChangingMotionState.joint_position,
203
+ latestMotionState.joint_position,
204
+ MOTION_DELTA_THRESHOLD,
205
+ )
206
+ ) {
207
+ runInAction(() => {
208
+ this.rapidlyChangingMotionState = latestMotionState
209
+ })
210
+ }
211
+
212
+ // handle tcpPose message
213
+ if (
214
+ !tcpMotionEqual(
215
+ this.rapidlyChangingMotionState,
216
+ latestMotionState,
217
+ MOTION_DELTA_THRESHOLD,
218
+ )
219
+ ) {
220
+ runInAction(() => {
221
+ this.rapidlyChangingMotionState.tcp_pose = latestMotionState.tcp_pose
222
+ })
223
+ }
224
+
225
+ // handle standstill changes
226
+ if (
227
+ this.rapidlyChangingMotionState.standstill !==
228
+ latestMotionState.standstill
229
+ ) {
230
+ runInAction(() => {
231
+ this.rapidlyChangingMotionState.standstill =
232
+ latestMotionState.standstill
233
+ })
234
+ }
235
+ })
236
+ makeAutoObservable(this)
237
+ }
238
+
239
+ get motionGroupId() {
240
+ return this.motionGroup.motion_group
241
+ }
242
+
243
+ get controllerId() {
244
+ return this.controller.controller
245
+ }
246
+
247
+ get modelFromController() {
248
+ return this.description.motion_group_model
249
+ }
250
+
251
+ get wandelscriptIdentifier() {
252
+ const num = this.motionGroupId.split("@")[0]
253
+ return `${this.controllerId.replace(/-/g, "_")}_${num}`
254
+ }
255
+
256
+ /** Jogging velocity in radians for rotation and joint movement */
257
+ get joggingVelocityRads() {
258
+ return (this.joggingVelocity * Math.PI) / 180
259
+ }
260
+
261
+ get joints() {
262
+ return this.initialMotionState.joint_position.map((_, i) => {
263
+ return {
264
+ index: i,
265
+ }
266
+ })
267
+ }
268
+
269
+ get dhParameters() {
270
+ return this.description.dh_parameters
271
+ }
272
+
273
+ get safetyZones() {
274
+ return this.description.safety_zones
275
+ }
276
+
277
+ /** Gets the robot mounting position offset in 3D viz coordinates */
278
+ get mountingPosition(): [number, number, number] {
279
+ if (!this.description.mounting) {
280
+ return [0, 0, 0]
281
+ }
282
+
283
+ return [
284
+ (this.description.mounting.position?.[0] || 0) / 1000,
285
+ (this.description.mounting.position?.[1] || 0) / 1000,
286
+ (this.description.mounting.position?.[2] || 0) / 1000,
287
+ ]
288
+ }
289
+
290
+ /** Gets the robot mounting position rotation in 3D viz coordinates */
291
+ get mountingQuaternion() {
292
+ const rotationVector = new THREE.Vector3(
293
+ this.description.mounting?.orientation?.[0] || 0,
294
+ this.description.mounting?.orientation?.[1] || 0,
295
+ this.description.mounting?.orientation?.[2] || 0,
296
+ )
297
+
298
+ const magnitude = rotationVector.length()
299
+ const axis = rotationVector.normalize()
300
+
301
+ return new THREE.Quaternion().setFromAxisAngle(axis, magnitude)
302
+ }
303
+
304
+ /**
305
+ * Whether the controller is currently in a safety state
306
+ * corresponding to an emergency stop
307
+ */
308
+ get isEstopActive() {
309
+ const estopStates: SafetyStateType[] = [
310
+ "SAFETY_STATE_ROBOT_EMERGENCY_STOP",
311
+ "SAFETY_STATE_DEVICE_EMERGENCY_STOP",
312
+ ]
313
+
314
+ return estopStates.includes(this.controllerState.safety_state)
315
+ }
316
+
317
+ /**
318
+ * Whether the controller is in a safety state
319
+ * that may be non-functional for robot pad purposes
320
+ */
321
+ get isMoveableSafetyState() {
322
+ const goodSafetyStates: SafetyStateType[] = [
323
+ "SAFETY_STATE_NORMAL",
324
+ "SAFETY_STATE_REDUCED",
325
+ ]
326
+
327
+ return goodSafetyStates.includes(this.controllerState.safety_state)
328
+ }
329
+
330
+ /**
331
+ * Whether the controller is in an operation mode that allows movement
332
+ */
333
+ get isMoveableOperationMode() {
334
+ const goodOperationModes: OperationMode[] = [
335
+ "OPERATION_MODE_AUTO",
336
+ "OPERATION_MODE_MANUAL",
337
+ "OPERATION_MODE_MANUAL_T1",
338
+ "OPERATION_MODE_MANUAL_T2",
339
+ ]
340
+
341
+ return goodOperationModes.includes(this.controllerState.operation_mode)
342
+ }
343
+
344
+ /**
345
+ * Whether the robot is currently active and can be moved, based on the
346
+ * safety state, operation mode and servo toggle activation state.
347
+ */
348
+ get canBeMoved() {
349
+ return (
350
+ this.isMoveableSafetyState &&
351
+ this.isMoveableOperationMode &&
352
+ this.activationState === "active"
353
+ )
354
+ }
355
+
356
+ async deactivate() {
357
+ if (this.activationState !== "active") {
358
+ console.error("Tried to deactivate while already deactivating")
359
+ return
360
+ }
361
+
362
+ runInAction(() => {
363
+ this.activationState = "deactivating"
364
+ })
365
+
366
+ try {
367
+ await this.nova.api.controller.setDefaultMode(
368
+ this.controllerId,
369
+ "ROBOT_SYSTEM_MODE_MONITOR",
370
+ )
371
+
372
+ runInAction(() => {
373
+ this.activationState = "inactive"
374
+ })
375
+ } catch (err) {
376
+ runInAction(() => {
377
+ this.activationState = "active"
378
+ })
379
+ throw err
380
+ }
381
+ }
382
+
383
+ async activate() {
384
+ if (this.activationState !== "inactive") {
385
+ console.error("Tried to activate while already activating")
386
+ return
387
+ }
388
+
389
+ runInAction(() => {
390
+ this.activationState = "activating"
391
+ })
392
+
393
+ try {
394
+ await this.nova.api.controller.setDefaultMode(
395
+ this.controllerId,
396
+ "ROBOT_SYSTEM_MODE_CONTROL",
397
+ )
398
+
399
+ runInAction(() => {
400
+ this.activationState = "active"
401
+ })
402
+ } catch (err) {
403
+ runInAction(() => {
404
+ this.activationState = "inactive"
405
+ })
406
+ throw err
407
+ }
408
+ }
409
+
410
+ toggleActivation() {
411
+ if (this.activationState === "inactive") {
412
+ this.activate()
413
+ } else if (this.activationState === "active") {
414
+ this.deactivate()
415
+ }
416
+ }
417
+
418
+ dispose() {
419
+ this.motionStateSocket.close()
420
+ if (this.connectedJoggingSocket) this.connectedJoggingSocket.close()
421
+ }
422
+
423
+ setJoggingVelocity(velocity: number) {
424
+ this.joggingVelocity = velocity
425
+ }
426
+ }
@@ -0,0 +1,120 @@
1
+ import { NovaClient } from "@wandelbots/nova-js/v2"
2
+ import { expect, test, vi } from "vitest"
3
+ import { delay } from "../components/utils/errorHandling"
4
+ import { JoggerConnection } from "./JoggerConnection"
5
+ import { jointValuesEqual } from "./motionStateUpdate"
6
+
7
+ // Notes:
8
+ // - In mock mode, no joint comparison takes place (always successful)
9
+ // - To test with real (virtual or physical) instance, set an INSTANCE_URL
10
+
11
+ const INSTANCE_URL = undefined // This could be read from ENV in future
12
+ const MOCK = !INSTANCE_URL
13
+
14
+ test("jog a robot somewhat", async () => {
15
+ const nova = new NovaClient({
16
+ instanceUrl: MOCK ? "https://mock.example.com" : INSTANCE_URL,
17
+ })
18
+
19
+ // Find a virtual robot we can jog
20
+ const controllerNames = await nova.api.controller.listRobotControllers()
21
+ const firstControllerName = controllerNames[0]
22
+
23
+ if (!firstControllerName) {
24
+ throw new Error("No robot controllers found on instance")
25
+ }
26
+
27
+ const controllerConfig =
28
+ await nova.api.controller.getRobotController(firstControllerName)
29
+ const controllerState =
30
+ await nova.api.controller.getCurrentRobotControllerState(
31
+ firstControllerName,
32
+ )
33
+ console.log("verify, got controller config and state", {
34
+ controllerConfig,
35
+ controllerState,
36
+ })
37
+
38
+ if (!controllerConfig || !controllerState) {
39
+ throw new Error(
40
+ `Could not get controller config and state for ${firstControllerName}`,
41
+ )
42
+ }
43
+
44
+ if (controllerConfig.configuration.kind !== "VirtualController") {
45
+ throw new Error(
46
+ `Controller ${firstControllerName} is not a VirtualController, it's a ${controllerConfig.configuration.kind}`,
47
+ )
48
+ }
49
+
50
+ if (controllerState.last_error?.[0]) {
51
+ throw new Error(
52
+ `Controller ${firstControllerName} has error: ${controllerState.last_error[0]}`,
53
+ )
54
+ }
55
+
56
+ const virtualMotionGroup = controllerState.motion_groups[0]
57
+
58
+ if (!virtualMotionGroup) {
59
+ throw new Error(
60
+ `Could not find a joggable motion group. Saw controller: ${firstControllerName}`,
61
+ )
62
+ }
63
+
64
+ const jogger = await JoggerConnection.open(
65
+ nova,
66
+ virtualMotionGroup.motion_group,
67
+ )
68
+
69
+ function getJoints() {
70
+ return jogger.motionStream.rapidlyChangingMotionState.joint_position
71
+ }
72
+
73
+ let joints = getJoints()
74
+
75
+ await jogger.setJoggingMode("jogging")
76
+
77
+ await jogger.rotateJoints({
78
+ joint: 0,
79
+ direction: "+",
80
+ velocityRadsPerSec: 0.1,
81
+ })
82
+
83
+ await delay(500)
84
+ await jogger.stop()
85
+
86
+ if (!MOCK) {
87
+ // Only verify joint movement in non-mock mode
88
+ await expect.poll(() => getJoints()[0]).toBeGreaterThan(joints[0] + 0.01)
89
+ expect(getJoints()[1]).toBeCloseTo(joints[1])
90
+ }
91
+
92
+ joints = getJoints()
93
+
94
+ await jogger.rotateJoints({
95
+ joint: 0,
96
+ direction: "-",
97
+ velocityRadsPerSec: 0.1,
98
+ })
99
+ await delay(500)
100
+ await jogger.stop()
101
+
102
+ await expect.poll(() => getJoints()[0]).toBeLessThan(joints[0] + 0.01)
103
+ expect(getJoints()[1]).toBeCloseTo(joints[1])
104
+
105
+ if (MOCK) {
106
+ return
107
+ }
108
+
109
+ // Wait for motion to stop
110
+ await vi.waitUntil(
111
+ async () => {
112
+ const joints = getJoints()
113
+ await jogger.motionStream.motionStateSocket.nextMessage()
114
+ return jointValuesEqual(joints, getJoints(), 0.0001)
115
+ },
116
+ {
117
+ timeout: 3000,
118
+ },
119
+ )
120
+ })