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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +202 -0
  3. package/dist/LoginWithAuth0.d.ts +7 -0
  4. package/dist/LoginWithAuth0.d.ts.map +1 -0
  5. package/dist/chunk-V3NJLR6P.js +336 -0
  6. package/dist/chunk-V3NJLR6P.js.map +1 -0
  7. package/dist/index.cjs +390 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +54 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/AutoReconnectingWebsocket.d.ts +43 -0
  14. package/dist/lib/AutoReconnectingWebsocket.d.ts.map +1 -0
  15. package/dist/lib/availableStorage.d.ts +15 -0
  16. package/dist/lib/availableStorage.d.ts.map +1 -0
  17. package/dist/lib/converters.d.ts +26 -0
  18. package/dist/lib/converters.d.ts.map +1 -0
  19. package/dist/lib/errorHandling.d.ts +4 -0
  20. package/dist/lib/errorHandling.d.ts.map +1 -0
  21. package/dist/lib/v1/ConnectedMotionGroup.d.ts +77 -0
  22. package/dist/lib/v1/ConnectedMotionGroup.d.ts.map +1 -0
  23. package/dist/lib/v1/JoggerConnection.d.ts +94 -0
  24. package/dist/lib/v1/JoggerConnection.d.ts.map +1 -0
  25. package/dist/lib/v1/MotionStreamConnection.d.ts +25 -0
  26. package/dist/lib/v1/MotionStreamConnection.d.ts.map +1 -0
  27. package/dist/lib/v1/NovaCellAPIClient.d.ts +66 -0
  28. package/dist/lib/v1/NovaCellAPIClient.d.ts.map +1 -0
  29. package/dist/lib/v1/NovaClient.d.ts +67 -0
  30. package/dist/lib/v1/NovaClient.d.ts.map +1 -0
  31. package/dist/lib/v1/ProgramStateConnection.d.ts +53 -0
  32. package/dist/lib/v1/ProgramStateConnection.d.ts.map +1 -0
  33. package/dist/lib/v1/getLatestTrajectories.d.ts +4 -0
  34. package/dist/lib/v1/getLatestTrajectories.d.ts.map +1 -0
  35. package/dist/lib/v1/index.cjs +3957 -0
  36. package/dist/lib/v1/index.cjs.map +1 -0
  37. package/dist/lib/v1/index.d.ts +9 -0
  38. package/dist/lib/v1/index.d.ts.map +1 -0
  39. package/dist/lib/v1/index.js +3662 -0
  40. package/dist/lib/v1/index.js.map +1 -0
  41. package/dist/lib/v1/mock/MockNovaInstance.d.ts +13 -0
  42. package/dist/lib/v1/mock/MockNovaInstance.d.ts.map +1 -0
  43. package/dist/lib/v1/motionStateUpdate.d.ts +4 -0
  44. package/dist/lib/v1/motionStateUpdate.d.ts.map +1 -0
  45. package/dist/lib/v2/ConnectedMotionGroup.d.ts +41 -0
  46. package/dist/lib/v2/ConnectedMotionGroup.d.ts.map +1 -0
  47. package/dist/lib/v2/JoggerConnection.d.ts +53 -0
  48. package/dist/lib/v2/JoggerConnection.d.ts.map +1 -0
  49. package/dist/lib/v2/MotionStreamConnection.d.ts +25 -0
  50. package/dist/lib/v2/MotionStreamConnection.d.ts.map +1 -0
  51. package/dist/lib/v2/NovaCellAPIClient.d.ts +64 -0
  52. package/dist/lib/v2/NovaCellAPIClient.d.ts.map +1 -0
  53. package/dist/lib/v2/NovaClient.d.ts +67 -0
  54. package/dist/lib/v2/NovaClient.d.ts.map +1 -0
  55. package/dist/lib/v2/ProgramStateConnection.d.ts +53 -0
  56. package/dist/lib/v2/ProgramStateConnection.d.ts.map +1 -0
  57. package/dist/lib/v2/index.cjs +2239 -0
  58. package/dist/lib/v2/index.cjs.map +1 -0
  59. package/dist/lib/v2/index.d.ts +8 -0
  60. package/dist/lib/v2/index.d.ts.map +1 -0
  61. package/dist/lib/v2/index.js +1947 -0
  62. package/dist/lib/v2/index.js.map +1 -0
  63. package/dist/lib/v2/mock/MockNovaInstance.d.ts +13 -0
  64. package/dist/lib/v2/mock/MockNovaInstance.d.ts.map +1 -0
  65. package/dist/lib/v2/motionStateUpdate.d.ts +4 -0
  66. package/dist/lib/v2/motionStateUpdate.d.ts.map +1 -0
  67. package/dist/lib/v2/vectorUtils.d.ts +7 -0
  68. package/dist/lib/v2/vectorUtils.d.ts.map +1 -0
  69. package/package.json +67 -0
  70. package/src/LoginWithAuth0.ts +90 -0
  71. package/src/index.ts +5 -0
  72. package/src/lib/AutoReconnectingWebsocket.ts +163 -0
  73. package/src/lib/availableStorage.ts +46 -0
  74. package/src/lib/converters.ts +74 -0
  75. package/src/lib/errorHandling.ts +26 -0
  76. package/src/lib/v1/ConnectedMotionGroup.ts +419 -0
  77. package/src/lib/v1/JoggerConnection.ts +480 -0
  78. package/src/lib/v1/MotionStreamConnection.ts +202 -0
  79. package/src/lib/v1/NovaCellAPIClient.ts +180 -0
  80. package/src/lib/v1/NovaClient.ts +232 -0
  81. package/src/lib/v1/ProgramStateConnection.ts +267 -0
  82. package/src/lib/v1/getLatestTrajectories.ts +36 -0
  83. package/src/lib/v1/index.ts +8 -0
  84. package/src/lib/v1/mock/MockNovaInstance.ts +1302 -0
  85. package/src/lib/v1/motionStateUpdate.ts +55 -0
  86. package/src/lib/v2/ConnectedMotionGroup.ts +216 -0
  87. package/src/lib/v2/JoggerConnection.ts +207 -0
  88. package/src/lib/v2/MotionStreamConnection.ts +201 -0
  89. package/src/lib/v2/NovaCellAPIClient.ts +174 -0
  90. package/src/lib/v2/NovaClient.ts +230 -0
  91. package/src/lib/v2/ProgramStateConnection.ts +255 -0
  92. package/src/lib/v2/index.ts +7 -0
  93. package/src/lib/v2/mock/MockNovaInstance.ts +982 -0
  94. package/src/lib/v2/motionStateUpdate.ts +55 -0
  95. package/src/lib/v2/vectorUtils.ts +36 -0
@@ -0,0 +1,180 @@
1
+ import type { Configuration as BaseConfiguration } from "@wandelbots/nova-api/v1"
2
+ import {
3
+ ApplicationApi,
4
+ CellApi,
5
+ ControllerApi,
6
+ ControllerIOsApi,
7
+ CoordinateSystemsApi,
8
+ DeviceConfigurationApi,
9
+ LibraryProgramApi,
10
+ LibraryProgramMetadataApi,
11
+ LibraryRecipeApi,
12
+ LibraryRecipeMetadataApi,
13
+ MotionApi,
14
+ MotionGroupApi,
15
+ MotionGroupInfosApi,
16
+ MotionGroupJoggingApi,
17
+ MotionGroupKinematicApi,
18
+ ProgramApi,
19
+ ProgramValuesApi,
20
+ StoreCollisionComponentsApi,
21
+ StoreCollisionScenesApi,
22
+ StoreObjectApi,
23
+ SystemApi,
24
+ VirtualRobotApi,
25
+ VirtualRobotBehaviorApi,
26
+ VirtualRobotModeApi,
27
+ VirtualRobotSetupApi,
28
+ } from "@wandelbots/nova-api/v1"
29
+ import type { BaseAPI } from "@wandelbots/nova-api/v1/base"
30
+ import type { AxiosInstance } from "axios"
31
+ import axios from "axios"
32
+
33
+ type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => infer R
34
+ ? (...args: P) => R
35
+ : never
36
+
37
+ type UnwrapAxiosResponseReturn<T> = T extends (...a: any) => any
38
+ ? (
39
+ ...a: Parameters<T>
40
+ ) => Promise<Awaited<ReturnType<T>> extends { data: infer D } ? D : never>
41
+ : never
42
+
43
+ export type WithCellId<T> = {
44
+ [P in keyof T]: UnwrapAxiosResponseReturn<OmitFirstArg<T[P]>>
45
+ }
46
+
47
+ export type WithUnwrappedAxiosResponse<T> = {
48
+ [P in keyof T]: UnwrapAxiosResponseReturn<T[P]>
49
+ }
50
+
51
+ /**
52
+ * API client providing type-safe access to all the Nova API REST endpoints
53
+ * associated with a specific cell id.
54
+ */
55
+ export class NovaCellAPIClient {
56
+ constructor(
57
+ readonly cellId: string,
58
+ readonly opts: BaseConfiguration & {
59
+ axiosInstance?: AxiosInstance
60
+ mock?: boolean
61
+ },
62
+ ) {}
63
+
64
+ /**
65
+ * Some TypeScript sorcery which alters the API class methods so you don't
66
+ * have to pass the cell id to every single one, and de-encapsulates the
67
+ * response data
68
+ */
69
+ private withCellId<T extends BaseAPI>(
70
+ ApiConstructor: new (
71
+ config: BaseConfiguration,
72
+ basePath: string,
73
+ axios: AxiosInstance,
74
+ ) => T,
75
+ ) {
76
+ const apiClient = new ApiConstructor(
77
+ {
78
+ ...this.opts,
79
+ isJsonMime: (mime: string) => {
80
+ return mime === "application/json"
81
+ },
82
+ },
83
+ this.opts.basePath ?? "",
84
+ this.opts.axiosInstance ?? axios.create(),
85
+ ) as {
86
+ [key: string | symbol]: any
87
+ }
88
+
89
+ for (const key of Reflect.ownKeys(Reflect.getPrototypeOf(apiClient)!)) {
90
+ if (key !== "constructor" && typeof apiClient[key] === "function") {
91
+ const originalFunction = apiClient[key]
92
+ apiClient[key] = (...args: any[]) => {
93
+ return originalFunction
94
+ .apply(apiClient, [this.cellId, ...args])
95
+ .then((res: any) => res.data)
96
+ }
97
+ }
98
+ }
99
+
100
+ return apiClient as WithCellId<T>
101
+ }
102
+
103
+ /**
104
+ * As withCellId, but only does the response unwrapping
105
+ */
106
+ private withUnwrappedResponsesOnly<T extends BaseAPI>(
107
+ ApiConstructor: new (
108
+ config: BaseConfiguration,
109
+ basePath: string,
110
+ axios: AxiosInstance,
111
+ ) => T,
112
+ ) {
113
+ const apiClient = new ApiConstructor(
114
+ {
115
+ ...this.opts,
116
+ isJsonMime: (mime: string) => {
117
+ return mime === "application/json"
118
+ },
119
+ },
120
+ this.opts.basePath ?? "",
121
+ this.opts.axiosInstance ?? axios.create(),
122
+ ) as {
123
+ [key: string | symbol]: any
124
+ }
125
+
126
+ for (const key of Reflect.ownKeys(Reflect.getPrototypeOf(apiClient)!)) {
127
+ if (key !== "constructor" && typeof apiClient[key] === "function") {
128
+ const originalFunction = apiClient[key]
129
+ apiClient[key] = (...args: any[]) => {
130
+ return originalFunction
131
+ .apply(apiClient, args)
132
+ .then((res: any) => res.data)
133
+ }
134
+ }
135
+ }
136
+
137
+ return apiClient as WithUnwrappedAxiosResponse<T>
138
+ }
139
+
140
+ readonly system = this.withUnwrappedResponsesOnly(SystemApi)
141
+ readonly cell = this.withUnwrappedResponsesOnly(CellApi)
142
+
143
+ readonly deviceConfig = this.withCellId(DeviceConfigurationApi)
144
+
145
+ readonly motionGroup = this.withCellId(MotionGroupApi)
146
+ readonly motionGroupInfos = this.withCellId(MotionGroupInfosApi)
147
+
148
+ readonly controller = this.withCellId(ControllerApi)
149
+
150
+ readonly program = this.withCellId(ProgramApi)
151
+ readonly programValues = this.withCellId(ProgramValuesApi)
152
+
153
+ readonly controllerIOs = this.withCellId(ControllerIOsApi)
154
+
155
+ readonly motionGroupKinematic = this.withCellId(MotionGroupKinematicApi)
156
+ readonly motion = this.withCellId(MotionApi)
157
+
158
+ readonly coordinateSystems = this.withCellId(CoordinateSystemsApi)
159
+
160
+ readonly application = this.withCellId(ApplicationApi)
161
+ readonly applicationGlobal = this.withUnwrappedResponsesOnly(ApplicationApi)
162
+
163
+ readonly motionGroupJogging = this.withCellId(MotionGroupJoggingApi)
164
+
165
+ readonly virtualRobot = this.withCellId(VirtualRobotApi)
166
+ readonly virtualRobotSetup = this.withCellId(VirtualRobotSetupApi)
167
+ readonly virtualRobotMode = this.withCellId(VirtualRobotModeApi)
168
+ readonly virtualRobotBehavior = this.withCellId(VirtualRobotBehaviorApi)
169
+
170
+ readonly libraryProgramMetadata = this.withCellId(LibraryProgramMetadataApi)
171
+ readonly libraryProgram = this.withCellId(LibraryProgramApi)
172
+ readonly libraryRecipeMetadata = this.withCellId(LibraryRecipeMetadataApi)
173
+ readonly libraryRecipe = this.withCellId(LibraryRecipeApi)
174
+
175
+ readonly storeObject = this.withCellId(StoreObjectApi)
176
+ readonly storeCollisionComponents = this.withCellId(
177
+ StoreCollisionComponentsApi,
178
+ )
179
+ readonly storeCollisionScenes = this.withCellId(StoreCollisionScenesApi)
180
+ }
@@ -0,0 +1,232 @@
1
+ import type { Configuration as BaseConfiguration } from "@wandelbots/nova-api/v1"
2
+ import type { AxiosRequestConfig } from "axios"
3
+ import axios, { isAxiosError } from "axios"
4
+ import urlJoin from "url-join"
5
+ import { loginWithAuth0 } from "../../LoginWithAuth0.js"
6
+ import { AutoReconnectingWebsocket } from "../AutoReconnectingWebsocket.js"
7
+ import { availableStorage } from "../availableStorage.js"
8
+ import { ConnectedMotionGroup } from "./ConnectedMotionGroup.js"
9
+ import { JoggerConnection } from "./JoggerConnection.js"
10
+ import { MotionStreamConnection } from "./MotionStreamConnection.js"
11
+ import { NovaCellAPIClient } from "./NovaCellAPIClient.js"
12
+ import { MockNovaInstance } from "./mock/MockNovaInstance.js"
13
+
14
+ export type NovaClientConfig = {
15
+ /**
16
+ * Url of the deployed Nova instance to connect to
17
+ * e.g. https://saeattii.instance.wandelbots.io
18
+ */
19
+ instanceUrl: string | "https://mock.example.com"
20
+
21
+ /**
22
+ * Identifier of the cell on the Nova instance to connect this client to.
23
+ * If omitted, the default identifier "cell" is used.
24
+ **/
25
+ cellId?: string
26
+
27
+ /**
28
+ * Username for basic auth to the Nova instance.
29
+ * @deprecated use accessToken instead
30
+ */
31
+ username?: string
32
+
33
+ /**
34
+ * Password for basic auth to the Nova instance.
35
+ * @deprecated use accessToken instead
36
+ */
37
+ password?: string
38
+
39
+ /**
40
+ * Access token for Bearer authentication.
41
+ */
42
+ accessToken?: string
43
+ } & Omit<BaseConfiguration, "isJsonMime" | "basePath">
44
+
45
+ type NovaClientConfigWithDefaults = NovaClientConfig & { cellId: string }
46
+
47
+ /**
48
+ * Client for connecting to a Nova instance and controlling robots.
49
+ */
50
+ export class NovaClient {
51
+ readonly api: NovaCellAPIClient
52
+ readonly config: NovaClientConfigWithDefaults
53
+ readonly mock?: MockNovaInstance
54
+ authPromise: Promise<string | null> | null = null
55
+ accessToken: string | null = null
56
+
57
+ constructor(config: NovaClientConfig) {
58
+ const cellId = config.cellId ?? "cell"
59
+ this.config = {
60
+ cellId,
61
+ ...config,
62
+ }
63
+ this.accessToken =
64
+ config.accessToken ||
65
+ availableStorage.getString("wbjs.access_token") ||
66
+ null
67
+
68
+ if (this.config.instanceUrl === "https://mock.example.com") {
69
+ this.mock = new MockNovaInstance()
70
+ }
71
+
72
+ // Set up Axios instance with interceptor for token fetching
73
+ const axiosInstance = axios.create({
74
+ baseURL: urlJoin(this.config.instanceUrl, "/api/v1"),
75
+ })
76
+
77
+ axiosInstance.interceptors.request.use(async (request) => {
78
+ if (!request.headers.Authorization) {
79
+ if (this.accessToken) {
80
+ request.headers.Authorization = `Bearer ${this.accessToken}`
81
+ } else if (this.config.username && this.config.password) {
82
+ request.headers.Authorization = `Basic ${btoa(config.username + ":" + config.password)}`
83
+ }
84
+ }
85
+ return request
86
+ })
87
+
88
+ if (typeof window !== "undefined") {
89
+ axiosInstance.interceptors.response.use(
90
+ (r) => r,
91
+ async (error) => {
92
+ if (isAxiosError(error)) {
93
+ if (error.response?.status === 401) {
94
+ // If we hit a 401, attempt to login the user and retry with
95
+ // a new access token
96
+ try {
97
+ await this.renewAuthentication()
98
+
99
+ if (error.config) {
100
+ if (this.accessToken) {
101
+ error.config.headers.Authorization = `Bearer ${this.accessToken}`
102
+ } else {
103
+ delete error.config.headers.Authorization
104
+ }
105
+ return axiosInstance.request(error.config)
106
+ }
107
+ } catch (err) {
108
+ return Promise.reject(err)
109
+ }
110
+ } else if (error.response?.status === 503) {
111
+ // Check if the server as a whole is down
112
+ const res = await fetch(window.location.href)
113
+ if (res.status === 503) {
114
+ // Go to 503 page
115
+ window.location.reload()
116
+ }
117
+ }
118
+ }
119
+
120
+ return Promise.reject(error)
121
+ },
122
+ )
123
+ }
124
+
125
+ this.api = new NovaCellAPIClient(cellId, {
126
+ ...config,
127
+ basePath: urlJoin(this.config.instanceUrl, "/api/v1"),
128
+ isJsonMime: (mime: string) => {
129
+ return mime === "application/json"
130
+ },
131
+ baseOptions: {
132
+ ...(this.mock
133
+ ? ({
134
+ adapter: (config) => {
135
+ return this.mock!.handleAPIRequest(config)
136
+ },
137
+ } satisfies AxiosRequestConfig)
138
+ : {}),
139
+ ...config.baseOptions,
140
+ },
141
+ axiosInstance,
142
+ })
143
+ }
144
+
145
+ async renewAuthentication(): Promise<void> {
146
+ if (this.authPromise) {
147
+ // Don't double up
148
+ return
149
+ }
150
+
151
+ this.authPromise = loginWithAuth0(this.config.instanceUrl)
152
+ try {
153
+ this.accessToken = await this.authPromise
154
+ if (this.accessToken) {
155
+ // Cache access token so we don't need to log in every refresh
156
+ availableStorage.setString("wbjs.access_token", this.accessToken)
157
+ } else {
158
+ availableStorage.delete("wbjs.access_token")
159
+ }
160
+ } finally {
161
+ this.authPromise = null
162
+ }
163
+ }
164
+
165
+ makeWebsocketURL(path: string): string {
166
+ const url = new URL(
167
+ urlJoin(
168
+ this.config.instanceUrl,
169
+ `/api/v1/cells/${this.config.cellId}`,
170
+ path,
171
+ ),
172
+ )
173
+ url.protocol = url.protocol.replace("http", "ws")
174
+ url.protocol = url.protocol.replace("https", "wss")
175
+
176
+ // If provided, add basic auth credentials to the URL
177
+ // NOTE - basic auth is deprecated on websockets and doesn't work in Safari
178
+ // use tokens instead
179
+ if (this.accessToken) {
180
+ url.searchParams.append("token", this.accessToken)
181
+ } else if (this.config.username && this.config.password) {
182
+ url.username = this.config.username
183
+ url.password = this.config.password
184
+ }
185
+
186
+ return url.toString()
187
+ }
188
+
189
+ /**
190
+ * Retrieve an AutoReconnectingWebsocket to the given path on the Nova instance.
191
+ * If you explicitly want to reconnect an existing websocket, call `reconnect`
192
+ * on the returned object.
193
+ */
194
+ openReconnectingWebsocket(path: string) {
195
+ return new AutoReconnectingWebsocket(this.makeWebsocketURL(path), {
196
+ mock: this.mock,
197
+ })
198
+ }
199
+
200
+ /**
201
+ * Connect to the motion state websocket(s) for a given motion group
202
+ */
203
+ async connectMotionStream(motionGroupId: string) {
204
+ return await MotionStreamConnection.open(this, motionGroupId)
205
+ }
206
+
207
+ /**
208
+ * Connect to the jogging websocket(s) for a given motion group
209
+ */
210
+ async connectJogger(motionGroupId: string) {
211
+ return await JoggerConnection.open(this, motionGroupId)
212
+ }
213
+
214
+ async connectMotionGroups(
215
+ motionGroupIds: string[],
216
+ ): Promise<ConnectedMotionGroup[]> {
217
+ const { instances } = await this.api.controller.listControllers()
218
+
219
+ return Promise.all(
220
+ motionGroupIds.map((motionGroupId) =>
221
+ ConnectedMotionGroup.connect(this, motionGroupId, instances),
222
+ ),
223
+ )
224
+ }
225
+
226
+ async connectMotionGroup(
227
+ motionGroupId: string,
228
+ ): Promise<ConnectedMotionGroup> {
229
+ const motionGroups = await this.connectMotionGroups([motionGroupId])
230
+ return motionGroups[0]!
231
+ }
232
+ }
@@ -0,0 +1,267 @@
1
+ import { AxiosError } from "axios"
2
+ import { makeAutoObservable, runInAction } from "mobx"
3
+ import { AutoReconnectingWebsocket } from "../AutoReconnectingWebsocket"
4
+ import { tryParseJson } from "../converters"
5
+ import type { MotionStreamConnection } from "./MotionStreamConnection"
6
+ import type { NovaClient } from "./NovaClient"
7
+
8
+ export type ProgramRunnerLogEntry = {
9
+ timestamp: number
10
+ message: string
11
+ level?: "warn" | "error"
12
+ }
13
+
14
+ export enum ProgramState {
15
+ NotStarted = "not started",
16
+ Running = "running",
17
+ Stopped = "stopped",
18
+ Failed = "failed",
19
+ Completed = "completed",
20
+ }
21
+
22
+ export type CurrentProgram = {
23
+ id?: string
24
+ wandelscript?: string
25
+ state?: ProgramState
26
+ }
27
+
28
+ type ProgramStateMessage = {
29
+ type: string
30
+ runner: {
31
+ id: string
32
+ state: ProgramState
33
+ start_time?: number | null
34
+ execution_time?: number | null
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Interface for running Wandelscript programs on the Nova instance and
40
+ * tracking their progress and output
41
+ */
42
+ export class ProgramStateConnection {
43
+ currentProgram: CurrentProgram = {}
44
+ logs: ProgramRunnerLogEntry[] = []
45
+
46
+ executionState = "idle" as "idle" | "starting" | "executing" | "stopping"
47
+ currentlyExecutingProgramRunnerId = null as string | null
48
+
49
+ programStateSocket: AutoReconnectingWebsocket
50
+
51
+ constructor(readonly nova: NovaClient) {
52
+ makeAutoObservable(this, {}, { autoBind: true })
53
+
54
+ this.programStateSocket = nova.openReconnectingWebsocket(`/programs/state`)
55
+
56
+ this.programStateSocket.addEventListener("message", (ev) => {
57
+ const msg = tryParseJson(ev.data)
58
+
59
+ if (!msg) {
60
+ console.error("Failed to parse program state message", ev.data)
61
+ return
62
+ }
63
+ if (msg.type === "update") {
64
+ this.handleProgramStateMessage(msg)
65
+ }
66
+ })
67
+ }
68
+
69
+ /** Handle a program state update from the backend */
70
+ async handleProgramStateMessage(msg: ProgramStateMessage) {
71
+ const { runner } = msg
72
+
73
+ // Ignoring other programs for now
74
+ // TODO - show if execution state is busy from another source
75
+ if (runner.id !== this.currentlyExecutingProgramRunnerId) return
76
+
77
+ if (runner.state === ProgramState.Failed) {
78
+ try {
79
+ const runnerState = await this.nova.api.program.getProgramRunner(
80
+ runner.id,
81
+ )
82
+
83
+ // TODO - wandelengine should send print statements in real time over
84
+ // websocket as well, rather than at the end
85
+ const stdout = (runnerState as any).stdout
86
+ if (stdout) {
87
+ this.log(stdout)
88
+ }
89
+ this.logError(
90
+ `Program runner ${runner.id} failed with error: ${runnerState.error}\n${runnerState.traceback}`,
91
+ )
92
+ } catch (err) {
93
+ this.logError(
94
+ `Failed to retrieve results for program ${runner.id}: ${err}`,
95
+ )
96
+ }
97
+
98
+ this.currentProgram.state = ProgramState.Failed
99
+
100
+ this.gotoIdleState()
101
+ } else if (runner.state === ProgramState.Stopped) {
102
+ try {
103
+ const runnerState = await this.nova.api.program.getProgramRunner(
104
+ runner.id,
105
+ )
106
+
107
+ const stdout = (runnerState as any).stdout
108
+ if (stdout) {
109
+ this.log(stdout)
110
+ }
111
+
112
+ this.currentProgram.state = ProgramState.Stopped
113
+ this.log(`Program runner ${runner.id} stopped`)
114
+ } catch (err) {
115
+ this.logError(
116
+ `Failed to retrieve results for program ${runner.id}: ${err}`,
117
+ )
118
+ }
119
+
120
+ this.gotoIdleState()
121
+ } else if (runner.state === ProgramState.Completed) {
122
+ try {
123
+ const runnerState = await this.nova.api.program.getProgramRunner(
124
+ runner.id,
125
+ )
126
+
127
+ const stdout = (runnerState as any).stdout
128
+ if (stdout) {
129
+ this.log(stdout)
130
+ }
131
+ this.log(
132
+ `Program runner ${runner.id} finished successfully in ${runner.execution_time?.toFixed(2)} seconds`,
133
+ )
134
+
135
+ this.currentProgram.state = ProgramState.Completed
136
+ } catch (err) {
137
+ this.logError(
138
+ `Failed to retrieve results for program ${runner.id}: ${err}`,
139
+ )
140
+ }
141
+
142
+ this.gotoIdleState()
143
+ } else if (runner.state === ProgramState.Running) {
144
+ this.currentProgram.state = ProgramState.Running
145
+ this.log(`Program runner ${runner.id} now running`)
146
+ } else if (runner.state !== ProgramState.NotStarted) {
147
+ console.error(runner)
148
+ this.logError(
149
+ `Program runner ${runner.id} entered unexpected state: ${runner.state}`,
150
+ )
151
+ this.currentProgram.state = ProgramState.NotStarted
152
+ this.gotoIdleState()
153
+ }
154
+ }
155
+
156
+ /** Call when a program is no longer executing */
157
+ gotoIdleState() {
158
+ runInAction(() => {
159
+ this.executionState = "idle"
160
+ })
161
+ this.currentlyExecutingProgramRunnerId = null
162
+ }
163
+
164
+ async executeProgram(
165
+ wandelscript: string,
166
+ initial_state?: Object,
167
+ activeRobot?: MotionStreamConnection,
168
+ ) {
169
+ this.currentProgram = {
170
+ wandelscript: wandelscript,
171
+ state: ProgramState.NotStarted,
172
+ }
173
+
174
+ const { currentProgram: openProgram } = this
175
+ if (!openProgram) return
176
+ runInAction(() => {
177
+ this.executionState = "starting"
178
+ })
179
+
180
+ // Jogging can cause program execution to fail for some time after
181
+ // So we need to explicitly stop jogging before running a program
182
+ if (activeRobot) {
183
+ try {
184
+ await this.nova.api.motionGroupJogging.stopJogging(
185
+ activeRobot.motionGroupId,
186
+ )
187
+ } catch (err) {
188
+ console.error(err)
189
+ }
190
+ }
191
+
192
+ // WOS-1539: Wandelengine parser currently breaks if there are empty lines with indentation
193
+ const trimmedCode = openProgram.wandelscript!.replaceAll(/^\s*$/gm, "")
194
+
195
+ try {
196
+ const programRunnerRef = await this.nova.api.program.createProgramRunner(
197
+ {
198
+ code: trimmedCode,
199
+ initial_state: initial_state,
200
+ default_robot: activeRobot?.wandelscriptIdentifier,
201
+ } as any,
202
+ {
203
+ headers: {
204
+ "Content-Type": "application/json",
205
+ },
206
+ },
207
+ )
208
+
209
+ this.log(`Created program runner ${programRunnerRef.id}"`)
210
+ runInAction(() => {
211
+ this.executionState = "executing"
212
+ })
213
+ this.currentlyExecutingProgramRunnerId = programRunnerRef.id
214
+ } catch (error) {
215
+ if (error instanceof AxiosError && error.response && error.request) {
216
+ this.logError(
217
+ `${error.response.status} ${error.response.statusText} from ${error.response.config.url} ${JSON.stringify(error.response.data)}`,
218
+ )
219
+ } else {
220
+ this.logError(JSON.stringify(error))
221
+ }
222
+ runInAction(() => {
223
+ this.executionState = "idle"
224
+ })
225
+ }
226
+ }
227
+
228
+ async stopProgram() {
229
+ if (!this.currentlyExecutingProgramRunnerId) return
230
+ runInAction(() => {
231
+ this.executionState = "stopping"
232
+ })
233
+
234
+ try {
235
+ await this.nova.api.program.stopProgramRunner(
236
+ this.currentlyExecutingProgramRunnerId,
237
+ )
238
+ } catch (err) {
239
+ // Reactivate the stop button so user can try again
240
+ runInAction(() => {
241
+ this.executionState = "executing"
242
+ })
243
+ throw err
244
+ }
245
+ }
246
+
247
+ reset() {
248
+ this.currentProgram = {}
249
+ }
250
+
251
+ log(message: string) {
252
+ console.log(message)
253
+ this.logs.push({
254
+ timestamp: Date.now(),
255
+ message,
256
+ })
257
+ }
258
+
259
+ logError(message: string) {
260
+ console.log(message)
261
+ this.logs.push({
262
+ timestamp: Date.now(),
263
+ message,
264
+ level: "error",
265
+ })
266
+ }
267
+ }
@@ -0,0 +1,36 @@
1
+ import type { GetTrajectoryResponse } from "@wandelbots/nova-api/v1"
2
+ import type { NovaCellAPIClient } from "./NovaCellAPIClient"
3
+
4
+ let lastMotionIds: Set<string> = new Set()
5
+
6
+ export async function getLatestTrajectories(
7
+ apiClient: NovaCellAPIClient,
8
+ sampleTime: number = 50,
9
+ responsesCoordinateSystem?: string,
10
+ ): Promise<GetTrajectoryResponse[]> {
11
+ const newTrajectories: GetTrajectoryResponse[] = []
12
+
13
+ try {
14
+ const motions = await apiClient.motion.listMotions()
15
+ const currentMotionIds = new Set(motions.motions)
16
+
17
+ const newMotionIds = Array.from(currentMotionIds).filter(
18
+ (id) => !lastMotionIds.has(id),
19
+ )
20
+
21
+ for (const motionId of newMotionIds) {
22
+ const trajectory = await apiClient.motion.getMotionTrajectory(
23
+ motionId,
24
+ sampleTime,
25
+ responsesCoordinateSystem,
26
+ )
27
+ newTrajectories.push(trajectory)
28
+ }
29
+
30
+ lastMotionIds = currentMotionIds
31
+ } catch (error) {
32
+ console.error("Failed to get latest trajectories:", error)
33
+ }
34
+
35
+ return newTrajectories
36
+ }