@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,174 @@
|
|
|
1
|
+
import type { Configuration as BaseConfiguration } from "@wandelbots/nova-api/v2"
|
|
2
|
+
import {
|
|
3
|
+
ApplicationApi,
|
|
4
|
+
CellApi,
|
|
5
|
+
ControllerApi,
|
|
6
|
+
ControllerInputsOutputsApi,
|
|
7
|
+
CoordinateSystemsApi,
|
|
8
|
+
JoggingApi,
|
|
9
|
+
MotionGroupApi,
|
|
10
|
+
MotionGroupInfoApi,
|
|
11
|
+
MotionGroupKinematicsApi,
|
|
12
|
+
ProgramApi,
|
|
13
|
+
ProgramLibraryApi,
|
|
14
|
+
ProgramLibraryMetadataApi,
|
|
15
|
+
ProgramOperatorApi,
|
|
16
|
+
StoreCollisionComponentsApi,
|
|
17
|
+
StoreCollisionScenesApi,
|
|
18
|
+
StoreObjectApi,
|
|
19
|
+
SystemApi,
|
|
20
|
+
TrajectoryExecutionApi,
|
|
21
|
+
TrajectoryPlanningApi,
|
|
22
|
+
VirtualRobotApi,
|
|
23
|
+
VirtualRobotBehaviorApi,
|
|
24
|
+
VirtualRobotModeApi,
|
|
25
|
+
VirtualRobotSetupApi,
|
|
26
|
+
} from "@wandelbots/nova-api/v2"
|
|
27
|
+
import type { BaseAPI } from "@wandelbots/nova-api/v2/base"
|
|
28
|
+
import type { AxiosInstance } from "axios"
|
|
29
|
+
import axios from "axios"
|
|
30
|
+
|
|
31
|
+
type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => infer R
|
|
32
|
+
? (...args: P) => R
|
|
33
|
+
: never
|
|
34
|
+
|
|
35
|
+
type UnwrapAxiosResponseReturn<T> = T extends (...a: any) => any
|
|
36
|
+
? (
|
|
37
|
+
...a: Parameters<T>
|
|
38
|
+
) => Promise<Awaited<ReturnType<T>> extends { data: infer D } ? D : never>
|
|
39
|
+
: never
|
|
40
|
+
|
|
41
|
+
export type WithCellId<T> = {
|
|
42
|
+
[P in keyof T]: UnwrapAxiosResponseReturn<OmitFirstArg<T[P]>>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type WithUnwrappedAxiosResponse<T> = {
|
|
46
|
+
[P in keyof T]: UnwrapAxiosResponseReturn<T[P]>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* API client providing type-safe access to all the Nova API REST endpoints
|
|
51
|
+
* associated with a specific cell id.
|
|
52
|
+
*/
|
|
53
|
+
export class NovaCellAPIClient {
|
|
54
|
+
constructor(
|
|
55
|
+
readonly cellId: string,
|
|
56
|
+
readonly opts: BaseConfiguration & {
|
|
57
|
+
axiosInstance?: AxiosInstance
|
|
58
|
+
mock?: boolean
|
|
59
|
+
},
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Some TypeScript sorcery which alters the API class methods so you don't
|
|
64
|
+
* have to pass the cell id to every single one, and de-encapsulates the
|
|
65
|
+
* response data
|
|
66
|
+
*/
|
|
67
|
+
private withCellId<T extends BaseAPI>(
|
|
68
|
+
ApiConstructor: new (
|
|
69
|
+
config: BaseConfiguration,
|
|
70
|
+
basePath: string,
|
|
71
|
+
axios: AxiosInstance,
|
|
72
|
+
) => T,
|
|
73
|
+
) {
|
|
74
|
+
const apiClient = new ApiConstructor(
|
|
75
|
+
{
|
|
76
|
+
...this.opts,
|
|
77
|
+
isJsonMime: (mime: string) => {
|
|
78
|
+
return mime === "application/json"
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
this.opts.basePath ?? "",
|
|
82
|
+
this.opts.axiosInstance ?? axios.create(),
|
|
83
|
+
) as {
|
|
84
|
+
[key: string | symbol]: any
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const key of Reflect.ownKeys(Reflect.getPrototypeOf(apiClient)!)) {
|
|
88
|
+
if (key !== "constructor" && typeof apiClient[key] === "function") {
|
|
89
|
+
const originalFunction = apiClient[key]
|
|
90
|
+
apiClient[key] = (...args: any[]) => {
|
|
91
|
+
return originalFunction
|
|
92
|
+
.apply(apiClient, [this.cellId, ...args])
|
|
93
|
+
.then((res: any) => res.data)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return apiClient as WithCellId<T>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* As withCellId, but only does the response unwrapping
|
|
103
|
+
*/
|
|
104
|
+
private withUnwrappedResponsesOnly<T extends BaseAPI>(
|
|
105
|
+
ApiConstructor: new (
|
|
106
|
+
config: BaseConfiguration,
|
|
107
|
+
basePath: string,
|
|
108
|
+
axios: AxiosInstance,
|
|
109
|
+
) => T,
|
|
110
|
+
) {
|
|
111
|
+
const apiClient = new ApiConstructor(
|
|
112
|
+
{
|
|
113
|
+
...this.opts,
|
|
114
|
+
isJsonMime: (mime: string) => {
|
|
115
|
+
return mime === "application/json"
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
this.opts.basePath ?? "",
|
|
119
|
+
this.opts.axiosInstance ?? axios.create(),
|
|
120
|
+
) as {
|
|
121
|
+
[key: string | symbol]: any
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const key of Reflect.ownKeys(Reflect.getPrototypeOf(apiClient)!)) {
|
|
125
|
+
if (key !== "constructor" && typeof apiClient[key] === "function") {
|
|
126
|
+
const originalFunction = apiClient[key]
|
|
127
|
+
apiClient[key] = (...args: any[]) => {
|
|
128
|
+
return originalFunction
|
|
129
|
+
.apply(apiClient, args)
|
|
130
|
+
.then((res: any) => res.data)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return apiClient as WithUnwrappedAxiosResponse<T>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
readonly system = this.withUnwrappedResponsesOnly(SystemApi)
|
|
139
|
+
readonly cell = this.withUnwrappedResponsesOnly(CellApi)
|
|
140
|
+
|
|
141
|
+
readonly motionGroup = this.withCellId(MotionGroupApi)
|
|
142
|
+
readonly motionGroupInfos = this.withCellId(MotionGroupInfoApi)
|
|
143
|
+
|
|
144
|
+
readonly controller = this.withCellId(ControllerApi)
|
|
145
|
+
|
|
146
|
+
readonly program = this.withCellId(ProgramApi)
|
|
147
|
+
readonly programOperator = this.withCellId(ProgramOperatorApi)
|
|
148
|
+
readonly programLibraryMetadata = this.withCellId(ProgramLibraryMetadataApi)
|
|
149
|
+
readonly programLibrary = this.withCellId(ProgramLibraryApi)
|
|
150
|
+
|
|
151
|
+
readonly controllerIOs = this.withCellId(ControllerInputsOutputsApi)
|
|
152
|
+
|
|
153
|
+
readonly motionGroupKinematic = this.withCellId(MotionGroupKinematicsApi)
|
|
154
|
+
readonly trajectoryPlanning = this.withCellId(TrajectoryPlanningApi)
|
|
155
|
+
readonly trajectoryExecution = this.withCellId(TrajectoryExecutionApi)
|
|
156
|
+
|
|
157
|
+
readonly coordinateSystems = this.withCellId(CoordinateSystemsApi)
|
|
158
|
+
|
|
159
|
+
readonly application = this.withCellId(ApplicationApi)
|
|
160
|
+
readonly applicationGlobal = this.withUnwrappedResponsesOnly(ApplicationApi)
|
|
161
|
+
|
|
162
|
+
readonly jogging = this.withCellId(JoggingApi)
|
|
163
|
+
|
|
164
|
+
readonly virtualRobot = this.withCellId(VirtualRobotApi)
|
|
165
|
+
readonly virtualRobotSetup = this.withCellId(VirtualRobotSetupApi)
|
|
166
|
+
readonly virtualRobotMode = this.withCellId(VirtualRobotModeApi)
|
|
167
|
+
readonly virtualRobotBehavior = this.withCellId(VirtualRobotBehaviorApi)
|
|
168
|
+
|
|
169
|
+
readonly storeObject = this.withCellId(StoreObjectApi)
|
|
170
|
+
readonly storeCollisionComponents = this.withCellId(
|
|
171
|
+
StoreCollisionComponentsApi,
|
|
172
|
+
)
|
|
173
|
+
readonly storeCollisionScenes = this.withCellId(StoreCollisionScenesApi)
|
|
174
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { Configuration as BaseConfiguration } from "@wandelbots/nova-api/v2"
|
|
2
|
+
import type { AxiosRequestConfig } from "axios"
|
|
3
|
+
import axios, { isAxiosError } from "axios"
|
|
4
|
+
import urlJoin from "url-join"
|
|
5
|
+
import { loginWithAuth0 } from "../../LoginWithAuth0"
|
|
6
|
+
import { AutoReconnectingWebsocket } from "../AutoReconnectingWebsocket"
|
|
7
|
+
import { availableStorage } from "../availableStorage"
|
|
8
|
+
import { ConnectedMotionGroup } from "./ConnectedMotionGroup"
|
|
9
|
+
import { MockNovaInstance } from "./mock/MockNovaInstance"
|
|
10
|
+
import { MotionStreamConnection } from "./MotionStreamConnection"
|
|
11
|
+
import { NovaCellAPIClient } from "./NovaCellAPIClient"
|
|
12
|
+
|
|
13
|
+
export type NovaClientConfig = {
|
|
14
|
+
/**
|
|
15
|
+
* Url of the deployed Nova instance to connect to
|
|
16
|
+
* e.g. https://saeattii.instance.wandelbots.io
|
|
17
|
+
*/
|
|
18
|
+
instanceUrl: string | "https://mock.example.com"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Identifier of the cell on the Nova instance to connect this client to.
|
|
22
|
+
* If omitted, the default identifier "cell" is used.
|
|
23
|
+
**/
|
|
24
|
+
cellId?: string
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Username for basic auth to the Nova instance.
|
|
28
|
+
* @deprecated use accessToken instead
|
|
29
|
+
*/
|
|
30
|
+
username?: string
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Password for basic auth to the Nova instance.
|
|
34
|
+
* @deprecated use accessToken instead
|
|
35
|
+
*/
|
|
36
|
+
password?: string
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Access token for Bearer authentication.
|
|
40
|
+
*/
|
|
41
|
+
accessToken?: string
|
|
42
|
+
} & Omit<BaseConfiguration, "isJsonMime" | "basePath">
|
|
43
|
+
|
|
44
|
+
type NovaClientConfigWithDefaults = NovaClientConfig & { cellId: string }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* EXPERIMENTAL
|
|
48
|
+
*
|
|
49
|
+
* This client provides a starting point to migrate NOVA api v2.
|
|
50
|
+
* As v2 is still in development, this client has to be considered unstable
|
|
51
|
+
*
|
|
52
|
+
* Client for connecting to a Nova instance and controlling robots.
|
|
53
|
+
*/
|
|
54
|
+
export class NovaClient {
|
|
55
|
+
readonly api: NovaCellAPIClient
|
|
56
|
+
readonly config: NovaClientConfigWithDefaults
|
|
57
|
+
readonly mock?: MockNovaInstance
|
|
58
|
+
authPromise: Promise<string | null> | null = null
|
|
59
|
+
accessToken: string | null = null
|
|
60
|
+
|
|
61
|
+
constructor(config: NovaClientConfig) {
|
|
62
|
+
console.warn("Using experimental NOVA v2 client")
|
|
63
|
+
const cellId = config.cellId ?? "cell"
|
|
64
|
+
this.config = {
|
|
65
|
+
cellId,
|
|
66
|
+
...config,
|
|
67
|
+
}
|
|
68
|
+
this.accessToken =
|
|
69
|
+
config.accessToken ||
|
|
70
|
+
availableStorage.getString("wbjs.access_token") ||
|
|
71
|
+
null
|
|
72
|
+
|
|
73
|
+
if (this.config.instanceUrl === "https://mock.example.com") {
|
|
74
|
+
this.mock = new MockNovaInstance()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Set up Axios instance with interceptor for token fetching
|
|
78
|
+
const axiosInstance = axios.create({
|
|
79
|
+
baseURL: urlJoin(this.config.instanceUrl, "/api/v2"),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
axiosInstance.interceptors.request.use(async (request) => {
|
|
83
|
+
if (!request.headers.Authorization) {
|
|
84
|
+
if (this.accessToken) {
|
|
85
|
+
request.headers.Authorization = `Bearer ${this.accessToken}`
|
|
86
|
+
} else if (this.config.username && this.config.password) {
|
|
87
|
+
request.headers.Authorization = `Basic ${btoa(config.username + ":" + config.password)}`
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return request
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (typeof window !== "undefined") {
|
|
94
|
+
axiosInstance.interceptors.response.use(
|
|
95
|
+
(r) => r,
|
|
96
|
+
async (error) => {
|
|
97
|
+
if (isAxiosError(error)) {
|
|
98
|
+
if (error.response?.status === 401) {
|
|
99
|
+
// If we hit a 401, attempt to login the user and retry with
|
|
100
|
+
// a new access token
|
|
101
|
+
try {
|
|
102
|
+
await this.renewAuthentication()
|
|
103
|
+
|
|
104
|
+
if (error.config) {
|
|
105
|
+
if (this.accessToken) {
|
|
106
|
+
error.config.headers.Authorization = `Bearer ${this.accessToken}`
|
|
107
|
+
} else {
|
|
108
|
+
delete error.config.headers.Authorization
|
|
109
|
+
}
|
|
110
|
+
return axiosInstance.request(error.config)
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return Promise.reject(err)
|
|
114
|
+
}
|
|
115
|
+
} else if (error.response?.status === 503) {
|
|
116
|
+
// Check if the server as a whole is down
|
|
117
|
+
const res = await fetch(window.location.href)
|
|
118
|
+
if (res.status === 503) {
|
|
119
|
+
// Go to 503 page
|
|
120
|
+
window.location.reload()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Promise.reject(error)
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.api = new NovaCellAPIClient(cellId, {
|
|
131
|
+
...config,
|
|
132
|
+
basePath: urlJoin(this.config.instanceUrl, "/api/v1"),
|
|
133
|
+
isJsonMime: (mime: string) => {
|
|
134
|
+
return mime === "application/json"
|
|
135
|
+
},
|
|
136
|
+
baseOptions: {
|
|
137
|
+
...(this.mock
|
|
138
|
+
? ({
|
|
139
|
+
adapter: (config) => {
|
|
140
|
+
return this.mock!.handleAPIRequest(config)
|
|
141
|
+
},
|
|
142
|
+
} satisfies AxiosRequestConfig)
|
|
143
|
+
: {}),
|
|
144
|
+
...config.baseOptions,
|
|
145
|
+
},
|
|
146
|
+
axiosInstance,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async renewAuthentication(): Promise<void> {
|
|
151
|
+
if (this.authPromise) {
|
|
152
|
+
// Don't double up
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.authPromise = loginWithAuth0(this.config.instanceUrl)
|
|
157
|
+
try {
|
|
158
|
+
this.accessToken = await this.authPromise
|
|
159
|
+
if (this.accessToken) {
|
|
160
|
+
// Cache access token so we don't need to log in every refresh
|
|
161
|
+
availableStorage.setString("wbjs.access_token", this.accessToken)
|
|
162
|
+
} else {
|
|
163
|
+
availableStorage.delete("wbjs.access_token")
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
this.authPromise = null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
makeWebsocketURL(path: string): string {
|
|
171
|
+
const url = new URL(
|
|
172
|
+
urlJoin(
|
|
173
|
+
this.config.instanceUrl,
|
|
174
|
+
`/api/v1/cells/${this.config.cellId}`,
|
|
175
|
+
path,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
url.protocol = url.protocol.replace("http", "ws")
|
|
179
|
+
url.protocol = url.protocol.replace("https", "wss")
|
|
180
|
+
|
|
181
|
+
// If provided, add basic auth credentials to the URL
|
|
182
|
+
// NOTE - basic auth is deprecated on websockets and doesn't work in Safari
|
|
183
|
+
// use tokens instead
|
|
184
|
+
if (this.accessToken) {
|
|
185
|
+
url.searchParams.append("token", this.accessToken)
|
|
186
|
+
} else if (this.config.username && this.config.password) {
|
|
187
|
+
url.username = this.config.username
|
|
188
|
+
url.password = this.config.password
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return url.toString()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Retrieve an AutoReconnectingWebsocket to the given path on the Nova instance.
|
|
196
|
+
* If you explicitly want to reconnect an existing websocket, call `reconnect`
|
|
197
|
+
* on the returned object.
|
|
198
|
+
*/
|
|
199
|
+
openReconnectingWebsocket(path: string) {
|
|
200
|
+
return new AutoReconnectingWebsocket(this.makeWebsocketURL(path), {
|
|
201
|
+
mock: this.mock,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Connect to the motion state websocket(s) for a given motion group
|
|
207
|
+
*/
|
|
208
|
+
async connectMotionStream(motionGroupId: string) {
|
|
209
|
+
return await MotionStreamConnection.open(this, motionGroupId)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async connectMotionGroups(
|
|
213
|
+
motionGroupIds: string[],
|
|
214
|
+
): Promise<ConnectedMotionGroup[]> {
|
|
215
|
+
const { controllers } = await this.api.controller.listControllers()
|
|
216
|
+
|
|
217
|
+
return Promise.all(
|
|
218
|
+
motionGroupIds.map((motionGroupId) =>
|
|
219
|
+
ConnectedMotionGroup.connect(this, motionGroupId, controllers),
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async connectMotionGroup(
|
|
225
|
+
motionGroupId: string,
|
|
226
|
+
): Promise<ConnectedMotionGroup> {
|
|
227
|
+
const motionGroups = await this.connectMotionGroups([motionGroupId])
|
|
228
|
+
return motionGroups[0]!
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
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
|
+
// WOS-1539: Wandelengine parser currently breaks if there are empty lines with indentation
|
|
181
|
+
const trimmedCode = openProgram.wandelscript!.replaceAll(/^\s*$/gm, "")
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const programRunnerRef = await this.nova.api.program.createProgramRunner(
|
|
185
|
+
{
|
|
186
|
+
code: trimmedCode,
|
|
187
|
+
initial_state: initial_state,
|
|
188
|
+
default_robot: activeRobot?.wandelscriptIdentifier,
|
|
189
|
+
} as any,
|
|
190
|
+
{
|
|
191
|
+
headers: {
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
this.log(`Created program runner ${programRunnerRef.id}"`)
|
|
198
|
+
runInAction(() => {
|
|
199
|
+
this.executionState = "executing"
|
|
200
|
+
})
|
|
201
|
+
this.currentlyExecutingProgramRunnerId = programRunnerRef.id
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof AxiosError && error.response && error.request) {
|
|
204
|
+
this.logError(
|
|
205
|
+
`${error.response.status} ${error.response.statusText} from ${error.response.config.url} ${JSON.stringify(error.response.data)}`,
|
|
206
|
+
)
|
|
207
|
+
} else {
|
|
208
|
+
this.logError(JSON.stringify(error))
|
|
209
|
+
}
|
|
210
|
+
runInAction(() => {
|
|
211
|
+
this.executionState = "idle"
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async stopProgram() {
|
|
217
|
+
if (!this.currentlyExecutingProgramRunnerId) return
|
|
218
|
+
runInAction(() => {
|
|
219
|
+
this.executionState = "stopping"
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await this.nova.api.program.stopProgramRunner(
|
|
224
|
+
this.currentlyExecutingProgramRunnerId,
|
|
225
|
+
)
|
|
226
|
+
} catch (err) {
|
|
227
|
+
// Reactivate the stop button so user can try again
|
|
228
|
+
runInAction(() => {
|
|
229
|
+
this.executionState = "executing"
|
|
230
|
+
})
|
|
231
|
+
throw err
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
reset() {
|
|
236
|
+
this.currentProgram = {}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
log(message: string) {
|
|
240
|
+
console.log(message)
|
|
241
|
+
this.logs.push({
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
message,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
logError(message: string) {
|
|
248
|
+
console.log(message)
|
|
249
|
+
this.logs.push({
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
message,
|
|
252
|
+
level: "error",
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "@wandelbots/nova-api/v2"
|
|
2
|
+
export * from "./ConnectedMotionGroup"
|
|
3
|
+
export * from "./JoggerConnection"
|
|
4
|
+
export * from "./MotionStreamConnection"
|
|
5
|
+
export * from "./NovaCellAPIClient"
|
|
6
|
+
export * from "./NovaClient"
|
|
7
|
+
export * from "./ProgramStateConnection"
|