@wandelbots/nova-js
Version:
Official JS client for the Wandelbots API
738 lines (732 loc) • 20.5 kB
JavaScript
import { n as availableStorage, r as AutoReconnectingWebsocket, t as loginWithAuth0 } from "../../LoginWithAuth0-wQB-Sol1.mjs";
import axios, { AxiosError, isAxiosError } from "axios";
import urlJoin from "url-join";
import * as pathToRegexp from "path-to-regexp";
import { ApplicationApi, BUSInputsOutputsApi, CellApi, ControllerApi, ControllerInputsOutputsApi, JoggingApi, KinematicsApi, MotionGroupApi, MotionGroupModelsApi, StoreCollisionComponentsApi, StoreCollisionSetupsApi, StoreObjectApi, SystemApi, TrajectoryCachingApi, TrajectoryExecutionApi, TrajectoryPlanningApi, VirtualControllerApi, VirtualControllerBehaviorApi, VirtualControllerInputsOutputsApi } from "@wandelbots/nova-api/v2";
export * from "@wandelbots/nova-api/v2"
//#region src/lib/v2/NovaCellAPIClient.ts
/**
* API client providing type-safe access to all the Nova API REST endpoints
* associated with a specific cell id.
*/
var NovaCellAPIClient = class {
constructor(cellId, opts) {
this.cellId = cellId;
this.opts = opts;
this.system = this.withUnwrappedResponsesOnly(SystemApi);
this.cell = this.withUnwrappedResponsesOnly(CellApi);
this.motionGroup = this.withCellId(MotionGroupApi);
this.motionGroupModels = this.withCellId(MotionGroupModelsApi);
this.controller = this.withCellId(ControllerApi);
this.controllerIOs = this.withCellId(ControllerInputsOutputsApi);
this.trajectoryPlanning = this.withCellId(TrajectoryPlanningApi);
this.trajectoryExecution = this.withCellId(TrajectoryExecutionApi);
this.trajectoryCaching = this.withCellId(TrajectoryCachingApi);
this.application = this.withCellId(ApplicationApi);
this.applicationGlobal = this.withUnwrappedResponsesOnly(ApplicationApi);
this.jogging = this.withCellId(JoggingApi);
this.kinematics = this.withCellId(KinematicsApi);
this.busInputsOutputs = this.withCellId(BUSInputsOutputsApi);
this.virtualController = this.withCellId(VirtualControllerApi);
this.virtualControllerBehavior = this.withCellId(VirtualControllerBehaviorApi);
this.virtualControllerIOs = this.withCellId(VirtualControllerInputsOutputsApi);
this.storeObject = this.withCellId(StoreObjectApi);
this.storeCollisionComponents = this.withCellId(StoreCollisionComponentsApi);
this.storeCollisionSetups = this.withCellId(StoreCollisionSetupsApi);
}
/**
* Some TypeScript sorcery which alters the API class methods so you don't
* have to pass the cell id to every single one, and de-encapsulates the
* response data
*/
withCellId(ApiConstructor) {
const apiClient = new ApiConstructor({
...this.opts,
isJsonMime: (mime) => {
return mime === "application/json";
}
}, this.opts.basePath ?? "", this.opts.axiosInstance ?? axios.create());
for (const key of Reflect.ownKeys(Reflect.getPrototypeOf(apiClient))) if (key !== "constructor" && typeof apiClient[key] === "function") {
const originalFunction = apiClient[key];
apiClient[key] = (...args) => {
return originalFunction.apply(apiClient, [this.cellId, ...args]).then((res) => res.data);
};
}
return apiClient;
}
/**
* As withCellId, but only does the response unwrapping
*/
withUnwrappedResponsesOnly(ApiConstructor) {
const apiClient = new ApiConstructor({
...this.opts,
isJsonMime: (mime) => {
return mime === "application/json";
}
}, this.opts.basePath ?? "", this.opts.axiosInstance ?? axios.create());
for (const key of Reflect.ownKeys(Reflect.getPrototypeOf(apiClient))) if (key !== "constructor" && typeof apiClient[key] === "function") {
const originalFunction = apiClient[key];
apiClient[key] = (...args) => {
return originalFunction.apply(apiClient, args).then((res) => res.data);
};
}
return apiClient;
}
};
//#endregion
//#region src/lib/v2/mock/MockNovaInstance.ts
/**
* Ultra-simplified mock Nova server for testing stuff
*/
var MockNovaInstance = class {
constructor() {
this.connections = [];
}
async handleAPIRequest(config) {
const apiHandlers = [
{
method: "GET",
path: "/cells/:cellId/controllers",
handle() {
return ["mock-ur5e"];
}
},
{
method: "GET",
path: "/cells/:cellId/controllers/:controllerId",
handle() {
return {
configuration: {
initial_joint_position: "[0,-1.571,-1.571,-1.571,1.571,-1.571,0]",
kind: "VirtualController",
manufacturer: "universalrobots",
type: "universalrobots-ur5e"
},
name: "mock-ur5"
};
}
},
{
method: "GET",
path: "/cells/:cellId/controllers/:controllerId/state",
handle() {
return {
mode: "MODE_CONTROL",
last_error: [],
timestamp: "2025-10-16T09:19:26.634534092Z",
sequence_number: 1054764,
controller: "mock-ur5e",
operation_mode: "OPERATION_MODE_AUTO",
safety_state: "SAFETY_STATE_NORMAL",
velocity_override: 100,
motion_groups: [{
timestamp: "2025-10-16T09:19:26.634534092Z",
sequence_number: 1054764,
motion_group: "0@mock-ur5e",
controller: "mock-ur5e",
joint_position: [
1.487959623336792,
-1.8501918315887451,
1.8003005981445312,
6.034560203552246,
1.4921919107437134,
1.593459963798523
],
joint_limit_reached: { limit_reached: [
false,
false,
false,
false,
false,
false
] },
joint_torque: [],
joint_current: [
0,
0,
0,
0,
0,
0
],
flange_pose: {
position: [
107.6452433732927,
-409.0402987746852,
524.2402132330305
],
orientation: [
.9874434028353319,
-.986571714997442,
1.3336589451098142
]
},
tcp: "Flange",
tcp_pose: {
position: [
107.6452433732927,
-409.0402987746852,
524.2402132330305
],
orientation: [
.9874434028353319,
-.986571714997442,
1.3336589451098142
]
},
payload: "",
coordinate_system: "",
standstill: true
}]
};
}
},
{
method: "GET",
path: "/cells/:cellId/controllers/:controllerId/motion-groups/:motionGroupId/description",
handle() {
return {
motion_group_model: "UniversalRobots_UR5e",
mounting: {
position: [
0,
0,
0
],
orientation: [
0,
0,
0
]
},
tcps: { Flange: {
name: "Default-Flange",
pose: {
position: [
0,
0,
0
],
orientation: [
0,
0,
0
]
}
} },
payloads: { "FPay-0": {
name: "FPay-0",
payload: 0,
center_of_mass: [
0,
0,
0
],
moment_of_inertia: [
0,
0,
0
]
} },
cycle_time: 8,
dh_parameters: [
{
alpha: 1.5707963267948966,
d: 162.25
},
{ a: -425 },
{ a: -392.2 },
{
alpha: 1.5707963267948966,
d: 133.3
},
{
alpha: -1.5707963267948966,
d: 99.7
},
{ d: 99.6 }
],
operation_limits: {
auto_limits: {
joints: [
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
}
],
tcp: { velocity: 5e3 },
elbow: { velocity: 5e3 },
flange: { velocity: 5e3 }
},
manual_limits: {
joints: [
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
}
],
tcp: { velocity: 5e3 }
},
manual_t1_limits: {
joints: [
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
}
],
tcp: { velocity: 5e3 }
},
manual_t2_limits: {
joints: [
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 150
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
},
{
position: {
lower_limit: -6.283185307179586,
upper_limit: 6.283185307179586
},
velocity: 3.34159255027771,
acceleration: 40,
torque: 28
}
],
tcp: { velocity: 5e3 }
}
},
serial_number: "WBVirtualRobot"
};
}
},
{
method: "GET",
path: "/cells/:cellId/controllers/:controllerId/coordinate-systems",
handle() {
return [{
coordinate_system: "",
name: "world",
reference_coordinate_system: "",
position: [
0,
0,
0
],
orientation: [
0,
0,
0
],
orientation_type: "ROTATION_VECTOR"
}, {
coordinate_system: "CS-0",
name: "Default-CS",
reference_coordinate_system: "",
position: [
0,
0,
0
],
orientation: [
0,
0,
0
],
orientation_type: "ROTATION_VECTOR"
}];
}
}
];
const method = config.method?.toUpperCase() || "GET";
const path = `/cells${config.url?.split("/cells")[1]?.split("?")[0]}`;
for (const handler of apiHandlers) {
const match = pathToRegexp.match(handler.path)(path || "");
if (method === handler.method && match) {
const json = handler.handle();
return {
status: 200,
statusText: "Success",
data: JSON.stringify(json),
headers: {},
config,
request: { responseURL: config.url }
};
}
}
throw new AxiosError(`No mock handler matched this request: ${method} ${path}`, "404", config);
}
handleWebsocketConnection(socket) {
this.connections.push(socket);
setTimeout(() => {
socket.dispatchEvent(new Event("open"));
console.log("Websocket connection opened from", socket.url);
if (socket.url.includes("/state-stream")) socket.dispatchEvent(new MessageEvent("message", { data: JSON.stringify(defaultMotionState) }));
if (socket.url.includes("/execution/jogging")) socket.dispatchEvent(new MessageEvent("message", { data: JSON.stringify({ result: {
message: "string",
kind: "INITIALIZE_RECEIVED"
} }) }));
}, 10);
}
handleWebsocketMessage(socket, message) {
console.log(`Received message on ${socket.url}`, message);
}
};
const defaultMotionState = { result: {
motion_group: "0@universalrobots-ur5e",
controller: "universalrobots-ur5e",
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
sequence_number: 1,
joint_position: [
1.1699999570846558,
-1.5700000524520874,
1.3600000143051147,
1.0299999713897705,
1.2899999618530273,
1.2799999713897705
],
joint_limit_reached: { limit_reached: [
false,
false,
false,
false,
false,
false
] },
standstill: false,
flange_pose: {
position: [
1.3300010259703043,
-409.2680714682808,
531.0203477065281
],
orientation: [
1.7564919306270736,
-1.7542521568325058,
.7326972590614671
]
},
tcp_pose: {
position: [
1.3300010259703043,
-409.2680714682808,
531.0203477065281
],
orientation: [
1.7564919306270736,
-1.7542521568325058,
.7326972590614671
]
}
} };
//#endregion
//#region src/lib/v2/NovaClient.ts
function permissiveInstanceUrlParse(url) {
if (!url.startsWith("http")) url = `http://${url}`;
return new URL(url).toString();
}
/**
*
* Client for connecting to a Nova instance and controlling robots.
*/
var NovaClient = class {
constructor(config) {
this.authPromise = null;
this.accessToken = null;
const cellId = config.cellId ?? "cell";
this.config = {
cellId,
...config
};
this.accessToken = config.accessToken || availableStorage.getString("wbjs.access_token") || null;
if (this.config.instanceUrl === "https://mock.example.com") this.mock = new MockNovaInstance();
else this.config.instanceUrl = permissiveInstanceUrlParse(this.config.instanceUrl);
const axiosInstance = axios.create({
baseURL: urlJoin(this.config.instanceUrl, "/api/v2"),
headers: typeof window !== "undefined" && window.location.origin.includes("localhost") ? {} : { "X-Wandelbots-Client": "Wandelbots-Nova-JS-SDK" }
});
axiosInstance.interceptors.request.use(async (request) => {
if (!request.headers.Authorization) {
if (this.accessToken) request.headers.Authorization = `Bearer ${this.accessToken}`;
else if (this.config.username && this.config.password) request.headers.Authorization = `Basic ${btoa(`${config.username}:${config.password}`)}`;
}
return request;
});
if (typeof window !== "undefined") axiosInstance.interceptors.response.use((r) => r, async (error) => {
if (isAxiosError(error)) {
if (error.response?.status === 401) try {
await this.renewAuthentication();
if (error.config) {
if (this.accessToken) error.config.headers.Authorization = `Bearer ${this.accessToken}`;
else delete error.config.headers.Authorization;
return axiosInstance.request(error.config);
}
} catch (err) {
return Promise.reject(err);
}
else if (error.response?.status === 503) {
if ((await fetch(window.location.href)).status === 503) window.location.reload();
}
}
return Promise.reject(error);
});
this.api = new NovaCellAPIClient(cellId, {
...config,
basePath: urlJoin(this.config.instanceUrl, "/api/v2"),
isJsonMime: (mime) => {
return mime === "application/json";
},
baseOptions: {
...this.mock ? { adapter: (config$1) => {
return this.mock.handleAPIRequest(config$1);
} } : {},
...config.baseOptions
},
axiosInstance
});
}
async renewAuthentication() {
if (this.authPromise) return;
this.authPromise = loginWithAuth0(this.config.instanceUrl);
try {
this.accessToken = await this.authPromise;
if (this.accessToken) availableStorage.setString("wbjs.access_token", this.accessToken);
else availableStorage.delete("wbjs.access_token");
} finally {
this.authPromise = null;
}
}
makeWebsocketURL(path) {
const url = new URL(urlJoin(this.config.instanceUrl, `/api/v2/cells/${this.config.cellId}`, path));
url.protocol = url.protocol.replace("http", "ws");
url.protocol = url.protocol.replace("https", "wss");
if (this.accessToken) url.searchParams.append("token", this.accessToken);
else if (this.config.username && this.config.password) {
url.username = this.config.username;
url.password = this.config.password;
}
return url.toString();
}
/**
* Retrieve an AutoReconnectingWebsocket to the given path on the Nova instance.
* If you explicitly want to reconnect an existing websocket, call `reconnect`
* on the returned object.
*/
openReconnectingWebsocket(path) {
return new AutoReconnectingWebsocket(this.makeWebsocketURL(path), { mock: this.mock });
}
};
//#endregion
//#region src/lib/v2/wandelscriptUtils.ts
/**
* Convert a Pose object representing a motion group position
* into a string which represents that pose in Wandelscript.
*/
function poseToWandelscriptString(pose) {
const position = [
pose.position?.[0] ?? 0,
pose.position?.[1] ?? 0,
pose.position?.[2] ?? 0
];
const orientation = [
pose.orientation?.[0] ?? 0,
pose.orientation?.[1] ?? 0,
pose.orientation?.[2] ?? 0
];
const positionValues = position.map((v) => v.toFixed(1));
const rotationValues = orientation.map((v) => v.toFixed(4));
return `(${positionValues.concat(rotationValues).join(", ")})`;
}
//#endregion
export { NovaCellAPIClient, NovaClient, poseToWandelscriptString };
//# sourceMappingURL=index.mjs.map