@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.
- package/LICENSE +201 -0
- package/README.md +202 -0
- package/dist/LoginWithAuth0.d.ts +7 -0
- package/dist/LoginWithAuth0.d.ts.map +1 -0
- package/dist/chunk-V3NJLR6P.js +336 -0
- package/dist/chunk-V3NJLR6P.js.map +1 -0
- package/dist/index.cjs +390 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/AutoReconnectingWebsocket.d.ts +43 -0
- package/dist/lib/AutoReconnectingWebsocket.d.ts.map +1 -0
- package/dist/lib/availableStorage.d.ts +15 -0
- package/dist/lib/availableStorage.d.ts.map +1 -0
- package/dist/lib/converters.d.ts +26 -0
- package/dist/lib/converters.d.ts.map +1 -0
- package/dist/lib/errorHandling.d.ts +4 -0
- package/dist/lib/errorHandling.d.ts.map +1 -0
- package/dist/lib/v1/ConnectedMotionGroup.d.ts +77 -0
- package/dist/lib/v1/ConnectedMotionGroup.d.ts.map +1 -0
- package/dist/lib/v1/JoggerConnection.d.ts +94 -0
- package/dist/lib/v1/JoggerConnection.d.ts.map +1 -0
- package/dist/lib/v1/MotionStreamConnection.d.ts +25 -0
- package/dist/lib/v1/MotionStreamConnection.d.ts.map +1 -0
- package/dist/lib/v1/NovaCellAPIClient.d.ts +66 -0
- package/dist/lib/v1/NovaCellAPIClient.d.ts.map +1 -0
- package/dist/lib/v1/NovaClient.d.ts +67 -0
- package/dist/lib/v1/NovaClient.d.ts.map +1 -0
- package/dist/lib/v1/ProgramStateConnection.d.ts +53 -0
- package/dist/lib/v1/ProgramStateConnection.d.ts.map +1 -0
- package/dist/lib/v1/getLatestTrajectories.d.ts +4 -0
- package/dist/lib/v1/getLatestTrajectories.d.ts.map +1 -0
- package/dist/lib/v1/index.cjs +3957 -0
- package/dist/lib/v1/index.cjs.map +1 -0
- package/dist/lib/v1/index.d.ts +9 -0
- package/dist/lib/v1/index.d.ts.map +1 -0
- package/dist/lib/v1/index.js +3662 -0
- package/dist/lib/v1/index.js.map +1 -0
- package/dist/lib/v1/mock/MockNovaInstance.d.ts +13 -0
- package/dist/lib/v1/mock/MockNovaInstance.d.ts.map +1 -0
- package/dist/lib/v1/motionStateUpdate.d.ts +4 -0
- package/dist/lib/v1/motionStateUpdate.d.ts.map +1 -0
- package/dist/lib/v2/ConnectedMotionGroup.d.ts +41 -0
- package/dist/lib/v2/ConnectedMotionGroup.d.ts.map +1 -0
- package/dist/lib/v2/JoggerConnection.d.ts +53 -0
- package/dist/lib/v2/JoggerConnection.d.ts.map +1 -0
- package/dist/lib/v2/MotionStreamConnection.d.ts +25 -0
- package/dist/lib/v2/MotionStreamConnection.d.ts.map +1 -0
- package/dist/lib/v2/NovaCellAPIClient.d.ts +64 -0
- package/dist/lib/v2/NovaCellAPIClient.d.ts.map +1 -0
- package/dist/lib/v2/NovaClient.d.ts +67 -0
- package/dist/lib/v2/NovaClient.d.ts.map +1 -0
- package/dist/lib/v2/ProgramStateConnection.d.ts +53 -0
- package/dist/lib/v2/ProgramStateConnection.d.ts.map +1 -0
- package/dist/lib/v2/index.cjs +2239 -0
- package/dist/lib/v2/index.cjs.map +1 -0
- package/dist/lib/v2/index.d.ts +8 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/index.js +1947 -0
- package/dist/lib/v2/index.js.map +1 -0
- package/dist/lib/v2/mock/MockNovaInstance.d.ts +13 -0
- package/dist/lib/v2/mock/MockNovaInstance.d.ts.map +1 -0
- package/dist/lib/v2/motionStateUpdate.d.ts +4 -0
- package/dist/lib/v2/motionStateUpdate.d.ts.map +1 -0
- package/dist/lib/v2/vectorUtils.d.ts +7 -0
- package/dist/lib/v2/vectorUtils.d.ts.map +1 -0
- package/package.json +67 -0
- package/src/LoginWithAuth0.ts +90 -0
- package/src/index.ts +5 -0
- package/src/lib/AutoReconnectingWebsocket.ts +163 -0
- package/src/lib/availableStorage.ts +46 -0
- package/src/lib/converters.ts +74 -0
- package/src/lib/errorHandling.ts +26 -0
- package/src/lib/v1/ConnectedMotionGroup.ts +419 -0
- package/src/lib/v1/JoggerConnection.ts +480 -0
- package/src/lib/v1/MotionStreamConnection.ts +202 -0
- package/src/lib/v1/NovaCellAPIClient.ts +180 -0
- package/src/lib/v1/NovaClient.ts +232 -0
- package/src/lib/v1/ProgramStateConnection.ts +267 -0
- package/src/lib/v1/getLatestTrajectories.ts +36 -0
- package/src/lib/v1/index.ts +8 -0
- package/src/lib/v1/mock/MockNovaInstance.ts +1302 -0
- package/src/lib/v1/motionStateUpdate.ts +55 -0
- package/src/lib/v2/ConnectedMotionGroup.ts +216 -0
- package/src/lib/v2/JoggerConnection.ts +207 -0
- package/src/lib/v2/MotionStreamConnection.ts +201 -0
- package/src/lib/v2/NovaCellAPIClient.ts +174 -0
- package/src/lib/v2/NovaClient.ts +230 -0
- package/src/lib/v2/ProgramStateConnection.ts +255 -0
- package/src/lib/v2/index.ts +7 -0
- package/src/lib/v2/mock/MockNovaInstance.ts +982 -0
- package/src/lib/v2/motionStateUpdate.ts +55 -0
- 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
|
+
}
|