UNPKG

@wandelbots/nova-js

Version:

Official JS client for the Wandelbots API

202 lines (173 loc) 5.93 kB
import type { Controller, MotionGroupPhysical, MotionGroupState, } from "@wandelbots/nova-api/v2" import { makeAutoObservable, runInAction } from "mobx" import * as THREE from "three" import type { AutoReconnectingWebsocket } from "../AutoReconnectingWebsocket" import { tryParseJson } from "../converters" import { jointValuesEqual, tcpPoseEqual } from "./motionStateUpdate" import type { NovaClient } from "./NovaClient" import { Vector3d, vector3ToArray } from "./vectorUtils" const MOTION_DELTA_THRESHOLD = 0.0001 function unwrapRotationVector( newRotationVectorApi: Vector3d, currentRotationVectorApi: Vector3d, ): Vector3d { const currentRotationVector = new THREE.Vector3( currentRotationVectorApi[0], currentRotationVectorApi[1], currentRotationVectorApi[2], ) const newRotationVector = new THREE.Vector3( newRotationVectorApi[0], newRotationVectorApi[1], newRotationVectorApi[2], ) const currentAngle = currentRotationVector.length() const currentAxis = currentRotationVector.normalize() let newAngle = newRotationVector.length() let newAxis = newRotationVector.normalize() // Align rotation axes if (newAxis.dot(currentAxis) < 0) { newAngle = -newAngle newAxis = newAxis.multiplyScalar(-1.0) } // Shift rotation angle close to previous one to extend domain of rotation angles beyond [0, pi] // - this simplifies interpolation and prevents abruptly changing signs of the rotation angles let angleDifference = newAngle - currentAngle angleDifference -= 2.0 * Math.PI * Math.floor((angleDifference + Math.PI) / (2.0 * Math.PI)) newAngle = currentAngle + angleDifference return vector3ToArray(newAxis.multiplyScalar(newAngle)) } /** * Store representing the current state of a connected motion group. */ export class MotionStreamConnection { static async open(nova: NovaClient, motionGroupId: string) { const { controllers: controllers } = await nova.api.controller.listControllers() const [_motionGroupIndex, controllerId] = motionGroupId.split("@") as [ string, string, ] const controller = controllers.find((c) => c.controller === controllerId) const motionGroup = controller?.motion_groups.find( (mg) => mg.motion_group === motionGroupId, ) if (!controller || !motionGroup) { throw new Error( `Controller ${controllerId} or motion group ${motionGroupId} not found`, ) } const motionStateSocket = nova.openReconnectingWebsocket( `/motion-groups/${motionGroupId}/state-stream`, ) // Wait for the first message to get the initial state const firstMessage = await motionStateSocket.firstMessage() console.log("got first message", firstMessage) const initialMotionState = tryParseJson(firstMessage.data) ?.result as MotionGroupState if (!initialMotionState) { throw new Error( `Unable to parse initial motion state message ${firstMessage.data}`, ) } console.log( `Connected motion state websocket to motion group ${motionGroup.motion_group}. Initial state:\n `, initialMotionState, ) return new MotionStreamConnection( nova, controller, motionGroup, initialMotionState, motionStateSocket, ) } // Not mobx-observable as this changes very fast; should be observed // using animation frames rapidlyChangingMotionState: MotionGroupState constructor( readonly nova: NovaClient, readonly controller: Controller, readonly motionGroup: MotionGroupPhysical, readonly initialMotionState: MotionGroupState, readonly motionStateSocket: AutoReconnectingWebsocket, ) { this.rapidlyChangingMotionState = initialMotionState motionStateSocket.addEventListener("message", (event) => { const motionState = tryParseJson(event.data)?.result as | MotionGroupState | undefined if (!motionState) { throw new Error( `Failed to get motion state for ${this.motionGroupId}: ${event.data}`, ) } // handle motionState message if ( !jointValuesEqual( this.rapidlyChangingMotionState.joint_position.joints, motionState.joint_position.joints, MOTION_DELTA_THRESHOLD, ) ) { runInAction(() => { this.rapidlyChangingMotionState = motionState }) } // handle tcpPose message if ( !tcpPoseEqual( this.rapidlyChangingMotionState.tcp_pose, motionState.tcp_pose, MOTION_DELTA_THRESHOLD, ) ) { runInAction(() => { if (this.rapidlyChangingMotionState.tcp_pose == undefined) { this.rapidlyChangingMotionState.tcp_pose = motionState.tcp_pose } else { this.rapidlyChangingMotionState.tcp_pose = { position: motionState.tcp_pose!.position, orientation: unwrapRotationVector( motionState.tcp_pose!.orientation as Vector3d, this.rapidlyChangingMotionState.tcp_pose! .orientation as Vector3d, ), tcp: motionState.tcp_pose!.tcp, coordinate_system: motionState.tcp_pose!.coordinate_system, } } }) } }) makeAutoObservable(this) } get motionGroupId() { return this.motionGroup.motion_group } get controllerId() { return this.controller.controller } get modelFromController() { return this.motionGroup.model_from_controller } get wandelscriptIdentifier() { const num = this.motionGroupId.split("@")[0] return `${this.controllerId.replaceAll("-", "_")}_${num}` } get joints() { return this.initialMotionState.joint_position.joints.map((_, i) => { return { index: i, } }) } dispose() { this.motionStateSocket.close() } }