@wandelbots/wandelbots-js-react-components
Version:
React UI toolkit for building applications on top of the Wandelbots platform
448 lines (363 loc) • 12.4 kB
text/typescript
import { tryParseJson } from "@wandelbots/nova-js"
import type {
CoordinateSystem,
JoggerConnection,
MotionGroupSpecification,
RobotTcp,
} from "@wandelbots/nova-js/v1"
import { countBy } from "lodash-es"
import keyBy from "lodash-es/keyBy"
import uniqueId from "lodash-es/uniqueId"
import { autorun, makeAutoObservable, type IReactionDisposer } from "mobx"
const discreteIncrementOptions = [
{ id: "0.1", mm: 0.1, degrees: 0.05 },
{ id: "1", mm: 1, degrees: 0.5 },
{ id: "5", mm: 5, degrees: 2.5 },
{ id: "10", mm: 10, degrees: 5 },
]
const incrementOptions = [
{ id: "continuous" },
...discreteIncrementOptions,
] as const
export type JoggingAxis = "x" | "y" | "z"
export type JoggingDirection = "+" | "-"
export type DiscreteIncrementOption = (typeof discreteIncrementOptions)[number]
export type IncrementOption = (typeof incrementOptions)[number]
export type IncrementOptionId = IncrementOption["id"]
export const ORIENTATION_IDS = ["coordsys", "tool"]
export type OrientationId = (typeof ORIENTATION_IDS)[number]
export type IncrementJogInProgress = {
direction: JoggingDirection
axis: JoggingAxis
}
export class JoggingStore {
selectedTabId: "cartesian" | "joint" | "debug" = "cartesian"
/**
* State of the jogging panel. Starts as "inactive"
*/
activationState: "inactive" | "loading" | "active" = "inactive"
/**
* If an error occurred connecting to the jogging websocket
*/
activationError: unknown | null = null
/** To avoid activation race conditions */
activationCounter: number = 0
/** Locks to prevent UI interactions during certain operations */
locks = new Set<string>()
/**
* Id of selected coordinate system from among those defined on the API side
*/
selectedCoordSystemId: string = "world"
/** Id of selected tool center point from among the options available on the robot */
selectedTcpId: string = ""
/**
* Whether the user is jogging in the coordinate system or tool orientation.
* When in tool orientation, the robot moves in a direction relative to the
* attached tool rotation.
*/
selectedOrientation: OrientationId = "coordsys"
/**
* Id of selected increment amount for jogging. Options are defined by robot pad.
* When non-continuous, jogging moves the robot by a fixed number of mm or degrees
* each time the button is pressed, for extra precision
*/
selectedIncrementId: IncrementOptionId = "continuous"
/**
* When on the cartesian tab, jogging can be either translating or rotating
* around the TCP.
*/
selectedCartesianMotionType: "translate" | "rotate" = "translate"
/**
* If the jogger is busy running an incremental jog, this will be set
* with the information about the motion
*/
incrementJogInProgress: IncrementJogInProgress | null = null
/** How fast the robot goes when doing cartesian translate jogging in mm/s */
translationVelocityMmPerSec: number = 10
/** How fast the robot goes when doing cartesian or joint rotation jogging in °/s */
rotationVelocityDegPerSec: number = 1
/** Minimum translation velocity user can choose on the velocity slider in °/s */
minTranslationVelocityMmPerSec: number = 5
/** Maximum translation velocity user can choose on the velocity slider in °/s */
maxTranslationVelocityMmPerSec: number = 250
/** Minimum rotation velocity user can choose on the velocity slider in °/s */
minRotationVelocityDegPerSec: number = 1
/** Maximum rotation velocity user can choose on the velocity slider in °/s */
maxRotationVelocityDegPerSec: number = 60
/** Whether to show the coordinate system select dropdown in the UI */
showCoordSystemSelect: boolean = true
/** Whether to show the TCP select dropdown in the UI */
showTcpSelect: boolean = true
/** Whether to show the orientation select dropdown in the UI */
showOrientationSelect: boolean = true
/** Whether to show the increment select dropdown in the UI */
showIncrementSelect: boolean = true
/** Whether to show icons in the jogging tabs */
showTabIcons: boolean = false
/** Whether to show the label to the right of the velocity slider */
showVelocitySliderLabel: boolean = true
/** Whether to show the legend before the slider */
showVelocityLegend: boolean = false
/** Whether to show the legend before the joints */
showJointsLegend: boolean = false
disposers: IReactionDisposer[] = []
/**
* Load a jogging store with the relevant data it needs
* from the backend
*/
static async loadFor(jogger: JoggerConnection) {
const { nova } = jogger
// Find out what TCPs this motion group has (we need it for jogging)
const [motionGroupSpec, { coordinatesystems }, { tcps }] =
await Promise.all([
nova.api.motionGroupInfos.getMotionGroupSpecification(
jogger.motionGroupId,
),
// Fetch coord systems so user can select between them
nova.api.coordinateSystems.listCoordinateSystems("ROTATION_VECTOR"),
// Same for TCPs
nova.api.motionGroupInfos.listTcps(
jogger.motionGroupId,
"ROTATION_VECTOR",
),
])
return new JoggingStore(
jogger,
motionGroupSpec,
coordinatesystems || [],
tcps || [],
)
}
constructor(
readonly jogger: JoggerConnection,
readonly motionGroupSpec: MotionGroupSpecification,
readonly coordSystems: CoordinateSystem[],
readonly tcps: RobotTcp[],
) {
// TODO workaround for default coord system on backend having a canonical id
// of empty string. Can remove when fixed on API side
for (const cs of coordSystems) {
if (cs.coordinate_system === "") {
cs.coordinate_system = "world"
break
}
}
this.selectedCoordSystemId = coordSystems[0]?.coordinate_system || "world"
this.selectedTcpId = tcps[0]?.id || ""
makeAutoObservable(this, {}, { autoBind: true })
// Load user settings from local storage if available
this.loadFromLocalStorage()
// Automatically save user settings to local storage when save changes
this.disposers.push(autorun(() => this.saveToLocalStorage()))
;(window as any).joggingStore = this
}
dispose() {
for (const dispose of this.disposers) {
dispose()
}
this.jogger.dispose()
}
get coordSystemCountByName() {
return countBy(this.coordSystems, (cs) => cs.name)
}
async deactivate() {
const websocket = this.jogger.activeWebsocket
this.jogger.setJoggingMode("increment")
if (websocket) {
await websocket.closed()
}
}
/** Activate the jogger with current settings */
async activate() {
if (this.currentTab.id === "cartesian") {
const cartesianJoggingOpts = {
tcpId: this.selectedTcpId,
coordSystemId: this.activeCoordSystemId,
}
if (this.activeDiscreteIncrement) {
this.jogger.setJoggingMode("increment", cartesianJoggingOpts)
} else {
this.jogger.setJoggingMode("cartesian", cartesianJoggingOpts)
}
} else {
this.jogger.setJoggingMode("joint")
}
return this.jogger
}
loadFromLocalStorage() {
const save = tryParseJson(localStorage.getItem("joggingToolStore"))
if (!save) return
if (this.tabsById[save.selectedTabId]) {
this.selectedTabId = save.selectedTabId
}
if (this.coordSystemsById[save.selectedCoordSystemId]) {
this.selectedCoordSystemId = save.selectedCoordSystemId
}
if (this.tcpsById[save.selectedTcpId]) {
this.selectedTcpId = save.selectedTcpId
}
if (this.incrementOptionsById[save.selectedIncrementId]) {
this.selectedIncrementId = save.selectedIncrementId
}
if (["translate", "rotate"].includes(save.selectedCartesianMotionType)) {
this.selectedCartesianMotionType = save.selectedCartesianMotionType
}
if (["coordsys", "tool"].includes(save.selectedOrientation)) {
this.selectedOrientation = save.selectedOrientation
}
}
saveToLocalStorage() {
localStorage.setItem(
"joggingToolStore",
JSON.stringify(this.localStorageSave),
)
}
get isLocked() {
return this.locks.size > 0
}
get localStorageSave() {
return {
selectedTabId: this.selectedTabId,
selectedCoordSystemId: this.selectedCoordSystemId,
selectedTcpId: this.selectedTcpId,
selectedOrientation: this.selectedOrientation,
selectedIncrementId: this.selectedIncrementId,
selectedCartesianMotionType: this.selectedCartesianMotionType,
}
}
get tabs() {
return [
{
id: "cartesian",
label: "cartesian",
},
{
id: "joint",
label: "joint",
},
] as const
}
get incrementOptions() {
return incrementOptions
}
get discreteIncrementOptions() {
return discreteIncrementOptions
}
get incrementOptionsById() {
return keyBy(this.incrementOptions, (o) => o.id)
}
get tabsById() {
return keyBy(this.tabs, (t) => t.id)
}
get currentTab() {
return this.tabsById[this.selectedTabId] || this.tabs[0]!
}
get tabIndex() {
return this.tabs.indexOf(this.currentTab)
}
get coordSystemsById() {
return keyBy(this.coordSystems, (cs) => cs.coordinate_system)
}
get selectedCoordSystem() {
return this.coordSystemsById[this.selectedCoordSystemId]
}
/**
* The id of the coordinate system to use for jogging.
* If in tool orientation, this is set to "tool", not the
* selected coordinate system.
*/
get activeCoordSystemId() {
return this.selectedOrientation === "tool"
? "tool"
: this.selectedCoordSystemId
}
get tcpsById() {
return keyBy(this.tcps, (tcp) => tcp.id)
}
get activeDiscreteIncrement() {
return this.selectedOrientation === "tool"
? undefined
: discreteIncrementOptions.find((d) => d.id === this.selectedIncrementId)
}
/** The selected rotation velocity converted to radians per second */
get rotationVelocityRadsPerSec() {
return (this.rotationVelocityDegPerSec * Math.PI) / 180
}
/** Selected velocity in mm/sec or deg/sec */
get velocityInDisplayUnits() {
return this.currentMotionType === "translate"
? this.translationVelocityMmPerSec
: this.rotationVelocityDegPerSec
}
/** Minimum selectable velocity in mm/sec or deg/sec */
get minVelocityInDisplayUnits() {
return this.currentMotionType === "translate"
? this.minTranslationVelocityMmPerSec
: this.minRotationVelocityDegPerSec
}
/** Maximum selectable velocity in mm/sec or deg/sec */
get maxVelocityInDisplayUnits() {
return this.currentMotionType === "translate"
? this.maxTranslationVelocityMmPerSec
: this.maxRotationVelocityDegPerSec
}
/**
* For velocity unit purposes, joint and cartesian rotation
* are treated as the same type of motion
*/
get currentMotionType() {
if (
this.selectedTabId === "cartesian" &&
this.selectedCartesianMotionType === "translate"
) {
return "translate"
} else {
return "rotate"
}
}
onTabChange(_event: React.SyntheticEvent, newValue: number) {
const tab = this.tabs[newValue] || this.tabs[0]!
this.selectedTabId = tab.id
}
setSelectedCoordSystemId(id: string) {
this.selectedCoordSystemId = id
}
setSelectedTcpId(id: string) {
this.selectedTcpId = id
}
setSelectedOrientation(orientation: OrientationId) {
this.selectedOrientation = orientation
}
setSelectedIncrementId(id: IncrementOptionId) {
this.selectedIncrementId = id
}
setCurrentIncrementJog(incrementJog: IncrementJogInProgress | null) {
this.incrementJogInProgress = incrementJog
}
setVelocityFromSlider(velocity: number) {
if (this.currentMotionType === "translate") {
this.translationVelocityMmPerSec = velocity
} else {
this.rotationVelocityDegPerSec = velocity
}
}
setSelectedCartesianMotionType(type: "translate" | "rotate") {
this.selectedCartesianMotionType = type
}
lock(id: string) {
this.locks.add(id)
}
unlock(id: string) {
this.locks.delete(id)
}
/** Lock the UI until the given async callback resolves */
async withMotionLock(fn: () => Promise<void>) {
const lockId = uniqueId()
this.lock(lockId)
try {
return await fn()
} finally {
this.unlock(lockId)
}
}
}