@wandelbots/nova-js
Version:
Official JS client for the Wandelbots API
422 lines (361 loc) • 11.9 kB
text/typescript
import type {
ControllerInstance,
MotionGroupPhysical,
MotionGroupSpecification,
MotionGroupStateResponse,
Mounting,
RobotControllerState,
RobotControllerStateOperationModeEnum,
RobotControllerStateSafetyStateEnum,
RobotTcp,
SafetySetup,
} from "@wandelbots/nova-api/v1"
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"
const MOTION_DELTA_THRESHOLD = 0.0001
export type MotionGroupOption = {
selectionId: string
} & MotionGroupPhysical
/**
* Store representing the current state of a connected motion group.
*/
export class ConnectedMotionGroup {
static async connect(
nova: NovaClient,
motionGroupId: string,
controllers: ControllerInstance[],
) {
const [_motionGroupIndex, controllerId] = motionGroupId.split("@") as [
string,
string,
]
const controller = controllers.find((c) => c.controller === controllerId)
const motionGroup = controller?.physical_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()
const initialMotionState = tryParseJson(firstMessage.data)
?.result as MotionGroupStateResponse
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,
)
// Check if robot is virtual or physical
const config = await nova.api.controller.getRobotController(
controller.controller,
)
const isVirtual = config.configuration.kind === "VirtualController"
// If there's a configured mounting, we need it to show the right
// position of the robot model
const mounting = await (async () => {
try {
const mounting = await nova.api.motionGroupInfos.getMounting(
motionGroup.motion_group,
)
return mounting
} catch (err) {
console.error(
`Error fetching mounting for ${motionGroup.motion_group}`,
err,
)
return null
}
})()
// Open the websocket to monitor controller state for e.g. e-stop
const controllerStateSocket = nova.openReconnectingWebsocket(
`/controllers/${controller.controller}/state-stream?response_rate=1000`,
)
// Wait for the first message to get the initial state
const firstControllerMessage = await controllerStateSocket.firstMessage()
const initialControllerState = tryParseJson(firstControllerMessage.data)
?.result as RobotControllerState
if (!initialControllerState) {
throw new Error(
`Unable to parse initial controller state message ${firstControllerMessage.data}`,
)
}
console.log(
`Connected controller state websocket to controller ${controller.controller}. Initial state:\n `,
initialControllerState,
)
// Find out what TCPs this motion group has (we need it for jogging)
const { tcps } = await nova.api.motionGroupInfos.listTcps(motionGroupId)
const motionGroupSpecification =
await nova.api.motionGroupInfos.getMotionGroupSpecification(motionGroupId)
const safetySetup =
await nova.api.motionGroupInfos.getSafetySetup(motionGroupId)
return new ConnectedMotionGroup(
nova,
controller,
motionGroup,
initialMotionState,
motionStateSocket,
isVirtual,
// biome-ignore lint/style/noNonNullAssertion: legacy code
tcps!,
motionGroupSpecification,
safetySetup,
mounting,
initialControllerState,
controllerStateSocket,
)
}
connectedJoggingCartesianSocket: WebSocket | null = null
connectedJoggingJointsSocket: WebSocket | null = null
// biome-ignore lint/suspicious/noExplicitAny: legacy code
planData: any | null // tmp
joggingVelocity: number = 10
// Not mobx-observable as this changes very fast; should be observed
// using animation frames
rapidlyChangingMotionState: MotionGroupStateResponse
// Response rate on the websocket should be a bit slower on this one since
// we don't use the motion data
controllerState: RobotControllerState
/**
* Reflects activation state of the motion group / robot servos. The
* movement controls in the UI should only be enabled in the "active" state
*/
activationState: "inactive" | "activating" | "deactivating" | "active" =
"inactive"
constructor(
readonly nova: NovaClient,
readonly controller: ControllerInstance,
readonly motionGroup: MotionGroupPhysical,
readonly initialMotionState: MotionGroupStateResponse,
readonly motionStateSocket: AutoReconnectingWebsocket,
readonly isVirtual: boolean,
readonly tcps: RobotTcp[],
readonly motionGroupSpecification: MotionGroupSpecification,
readonly safetySetup: SafetySetup,
readonly mounting: Mounting | null,
readonly initialControllerState: RobotControllerState,
readonly controllerStateSocket: AutoReconnectingWebsocket,
) {
this.rapidlyChangingMotionState = initialMotionState
this.controllerState = initialControllerState
// Track controller state updates (e.g. safety state and operation mode)
controllerStateSocket.addEventListener("message", (event) => {
const data = tryParseJson(event.data)?.result
if (!data) {
return
}
runInAction(() => {
this.controllerState = data
})
})
motionStateSocket.addEventListener("message", (event) => {
const motionStateResponse = tryParseJson(event.data)?.result as
| MotionGroupStateResponse
| undefined
if (!motionStateResponse) {
throw new Error(
`Failed to get motion state for ${this.motionGroupId}: ${event.data}`,
)
}
// handle motionState message
if (
!jointValuesEqual(
this.rapidlyChangingMotionState.state.joint_position.joints,
motionStateResponse.state.joint_position.joints,
MOTION_DELTA_THRESHOLD,
)
) {
runInAction(() => {
this.rapidlyChangingMotionState.state = motionStateResponse.state
})
}
// handle tcpPose message
if (
!tcpPoseEqual(
this.rapidlyChangingMotionState.tcp_pose,
motionStateResponse.tcp_pose,
MOTION_DELTA_THRESHOLD,
)
) {
runInAction(() => {
this.rapidlyChangingMotionState.tcp_pose =
motionStateResponse.tcp_pose
})
}
})
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}`
}
/** Jogging velocity in radians for rotation and joint movement */
get joggingVelocityRads() {
return (this.joggingVelocity * Math.PI) / 180
}
get joints() {
return this.initialMotionState.state.joint_position.joints.map((_, i) => {
return {
index: i,
}
})
}
get dhParameters() {
return this.motionGroupSpecification.dh_parameters
}
get safetyZones() {
return this.safetySetup.safety_zones
}
/** Gets the robot mounting position offset in 3D viz coordinates */
get mountingPosition(): [number, number, number] {
if (!this.mounting) {
return [0, 0, 0]
}
return [
this.mounting.pose.position.x / 1000,
this.mounting.pose.position.y / 1000,
this.mounting.pose.position.z / 1000,
]
}
/** Gets the robot mounting position rotation in 3D viz coordinates */
get mountingQuaternion() {
const rotationVector = new THREE.Vector3(
this.mounting?.pose.orientation?.x || 0,
this.mounting?.pose.orientation?.y || 0,
this.mounting?.pose.orientation?.z || 0,
)
const magnitude = rotationVector.length()
const axis = rotationVector.normalize()
return new THREE.Quaternion().setFromAxisAngle(axis, magnitude)
}
/**
* Whether the controller is currently in a safety state
* corresponding to an emergency stop
*/
get isEstopActive() {
const estopStates: RobotControllerStateSafetyStateEnum[] = [
"SAFETY_STATE_ROBOT_EMERGENCY_STOP",
"SAFETY_STATE_DEVICE_EMERGENCY_STOP",
]
return estopStates.includes(this.controllerState.safety_state)
}
/**
* Whether the controller is in a safety state
* that may be non-functional for robot pad purposes
*/
get isMoveableSafetyState() {
const goodSafetyStates: RobotControllerStateSafetyStateEnum[] = [
"SAFETY_STATE_NORMAL",
"SAFETY_STATE_REDUCED",
]
return goodSafetyStates.includes(this.controllerState.safety_state)
}
/**
* Whether the controller is in an operation mode that allows movement
*/
get isMoveableOperationMode() {
const goodOperationModes: RobotControllerStateOperationModeEnum[] = [
"OPERATION_MODE_AUTO",
"OPERATION_MODE_MANUAL",
"OPERATION_MODE_MANUAL_T1",
"OPERATION_MODE_MANUAL_T2",
]
return goodOperationModes.includes(this.controllerState.operation_mode)
}
/**
* Whether the robot is currently active and can be moved, based on the
* safety state, operation mode and servo toggle activation state.
*/
get canBeMoved() {
return (
this.isMoveableSafetyState &&
this.isMoveableOperationMode &&
this.activationState === "active"
)
}
async deactivate() {
if (this.activationState !== "active") {
console.error("Tried to deactivate while already deactivating")
return
}
runInAction(() => {
this.activationState = "deactivating"
})
try {
await this.nova.api.controller.setDefaultMode(
this.controllerId,
"MODE_MONITOR",
)
runInAction(() => {
this.activationState = "inactive"
})
} catch (err) {
runInAction(() => {
this.activationState = "active"
})
throw err
}
}
async activate() {
if (this.activationState !== "inactive") {
console.error("Tried to activate while already activating")
return
}
runInAction(() => {
this.activationState = "activating"
})
try {
await this.nova.api.controller.setDefaultMode(
this.controllerId,
"MODE_CONTROL",
)
runInAction(() => {
this.activationState = "active"
})
} catch (err) {
runInAction(() => {
this.activationState = "inactive"
})
throw err
}
}
toggleActivation() {
if (this.activationState === "inactive") {
this.activate()
} else if (this.activationState === "active") {
this.deactivate()
}
}
dispose() {
this.motionStateSocket.close()
if (this.connectedJoggingCartesianSocket)
this.connectedJoggingCartesianSocket.close()
if (this.connectedJoggingJointsSocket)
this.connectedJoggingJointsSocket.close()
}
setJoggingVelocity(velocity: number) {
this.joggingVelocity = velocity
}
}