Skip to content

Commit

Permalink
Update demo filter param UI
Browse files Browse the repository at this point in the history
Name all parameters in the UI
Add translation and rotation scale params
Add host parameter to make real-robot demos easier
Add button change events
Bump dependencies
  • Loading branch information
nickswalker committed Mar 11, 2024
1 parent 6a13c5e commit de378c9
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 60 deletions.
36 changes: 29 additions & 7 deletions dist/ThreeDMouse.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export class ThreeDMouse {
pitch: 0,
yaw: 0,
buttonsState: {},
buttonsChanged: {},
buttonsValue: 0,
buttonsChanged: false,
controlChangeCount: 0,
};
this._emitRepeatedEventsInterval = null;
Expand Down Expand Up @@ -113,7 +113,8 @@ export class ThreeDMouse {
}

/**
* Emit a repeated event if the last emitted time exceeds a threshold.
* Emit a repeated event if no new data is heard for a while.
* This is useful
*/

_emitRepeatedEvent() {
Expand Down Expand Up @@ -166,6 +167,8 @@ export class ThreeDMouse {
if (e.reportId === chan) {
this._workingState[name] =
(flip * data.getInt16(byte, true)) / this.dataSpecs.axisScale;
// We count the number of packets we've processed and only emit
// when we've seen both XYZ and RPY data
if (name === "roll" || name === "x") {
this._workingState["controlChangeCount"] += 1;
}
Expand All @@ -185,21 +188,26 @@ export class ThreeDMouse {
bit: bit,
} = this.dataSpecs.buttonMapping[button_index];
if (e.reportId === chan) {
this._workingState["buttonsChanged"] = true;
if (newButtonsValue === null) {
newButtonsValue = 0;
}
// update the button vector
const mask = 1 << bit;
const state = data.getUint8(byte) & mask;
this._workingState["buttonsState"][name] = state !== 0;
const state = (data.getUint8(byte) & mask) !== 0;
const previousValue = this._workingState["buttonsState"][name]
const changed = state !== previousValue
if (changed) {
window.dispatchEvent(this._makeButtonEvent(name, state))
}
this._workingState["buttonsChanged"][name] = changed
this._workingState["buttonsState"][name] = state ;
newButtonsValue |= state << (8 * byte);
}
}

// If we haven't seen all the control data don't emit
if (
this._workingState["controlChangeCount"] <= 1 &&
!this._workingState["buttonsChanged"]
this._workingState["controlChangeCount"] <= 1 && newButtonsValue === null
) {
return;
}
Expand Down Expand Up @@ -236,4 +244,18 @@ export class ThreeDMouse {
},
});
}

_makeButtonEvent(name, buttonState) {
let type = "3dmousebuttondown"
if (!buttonState) {
type = "3dmousebuttonup"
}
return new CustomEvent(type, {
bubbles: true,
cancelable: true,
detail: {
name: name
}
})
}
}
5 changes: 5 additions & 0 deletions dist/filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ export class SmoothingDeadbandFilter {
this._previousOutput[1],
this.rotationMultiplier,
);
// Zero out very small (less than epsilon) values. The deadband will
// clamp _inputs_, but because of the temporal smoothing, the output will
// take a long time to settle all the way to zero unless we do this.
transProcessed = transProcessed.map((x) => Math.abs(x) < 1e-6 ? 0 : x);
rotProcessed = rotProcessed.map((x) => Math.abs(x) < 1e-6 ? 0 : x);
this._previousOutput = [transProcessed, rotProcessed];
return [transProcessed, rotProcessed];
}
Expand Down
52 changes: 42 additions & 10 deletions test/configurationPane.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { Pane } from "tweakpane";

/**
* Demo helper UI with sliders and checkboxes for tuning filtering parameters,
* and interactive line plots for component values
* @param container
* @param {any?} status
* @param {Object?} hostParam
* @param filter input filtering object controls will modify
* @param {Object?} filterParams
* @param lastValue
* @returns {{input: {}, folders: *[], pane: Pane, connectButton: *}}
*/
export function createConfigurationPane({
container: container,
status: status,
hostParam: hostParam,
filter: filter,
filterParams: filterParams,
lastValue: lastValue,
Expand All @@ -18,6 +30,10 @@ export function createConfigurationPane({
multiline: true,
bufferSize: 5,
});
// Allow user to point to a roslibjs websocket of their choice
if (hostParam) {
pane.addBinding(hostParam, "host", { label: "Host" });
}
for (let key of Object.keys(status)) {
if (key === "message") continue;
pane.addBinding(status, key, { readonly: true });
Expand All @@ -31,33 +47,49 @@ export function createConfigurationPane({
title: "Filtering",
disabled: true,
});
let translationScaleBinding = null;
let rotationScaleBinding = null;
// Having separate checkboxes for toggling translation and rotation allows the
// user to specify a scale for each, then disable the component without losing the scale.
filteringFolder
.addBinding(filterParams, "translationEnabled")
.addBinding(filterParams, "translationEnabled", { label: "Translation" })
.on("change", (e) => {
filter.translationMultiplier = e.value ? 1 : 0;
filter.translationMultiplier = e.value ? filterParams["translationScale"] : 0;
translationScaleBinding.disabled = !e.value
});
filteringFolder
.addBinding(filterParams, "rotationEnabled")
.addBinding(filterParams, "rotationEnabled", { label: "Rotation" })
.on("change", (e) => {
filter.rotationMultiplier = e.value ? 1 : 0;
filter.rotationMultiplier = e.value ? filterParams["rotationScale"] : 0;
rotationScaleBinding.disabled = !e.value
});
translationScaleBinding = filteringFolder
.addBinding(filterParams, "translationScale", { label: "Translation Scale", min: 0.0, max: 3.0 })
.on("change", (e) => {
filter.translationMultiplier = e.value;
});
rotationScaleBinding = filteringFolder
.addBinding(filterParams, "rotationScale", { label: "Rotation Scale", min: 0.0, max: 3.0 })
.on("change", (e) => {
filter.rotationMultiplier = e.value;
});
filteringFolder
.addBinding(filterParams, "smoothing", { min: 0.0, max: 1.0 })
.addBinding(filterParams, "smoothing", { label:"Temporal Smoothing", min: 0.0, max: 1.0 })
.on("change", (e) => {
filter.smoothing = e.value;
});
filteringFolder
.addBinding(filterParams, "softmaxTemperature", { min: 0.01, max: 100 })
.addBinding(filterParams, "softmaxTemperature", { label: "Softmax Temp", min: 0.01, max: 100 })
.on("change", (e) => {
filter.softmaxTemperature = e.value;
});
filteringFolder
.addBinding(filterParams, "deadbandSize", { min: 0, max: 1.0 })
.addBinding(filterParams, "deadbandSize", { label: "Deadband Size", min: 0, max: 1.0 })
.on("change", (e) => {
filter.deadband = e.value;
});
filteringFolder
.addBinding(filterParams, "deadbandWeight", { min: 0, max: 1.0 })
.addBinding(filterParams, "deadbandWeight", { label: "Sensitivity (beta)", min: 0, max: 1.0 })
.on("change", (e) => {
filter.cubicDeadbandWeight = e.value;
});
Expand All @@ -66,7 +98,7 @@ export function createConfigurationPane({

if (lastValue) {
const graphFolder = pane.addFolder({
title: "Graphs",
title: "Filtered Graphs",
disabled: true,
expanded: false,
});
Expand All @@ -82,7 +114,7 @@ export function createConfigurationPane({
graphFolder.addBinding(lastValue, key, GRAPH_OPTIONS);
}
const valueFolder = pane.addFolder({
title: "Values",
title: "Raw Values",
disabled: true,
});

Expand Down
21 changes: 17 additions & 4 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.156.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.156.1/examples/jsm/",
"tweakpane": "https://cdn.jsdelivr.net/npm/[email protected].1/dist/tweakpane.min.js"
"three": "https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/",
"tweakpane": "https://cdn.jsdelivr.net/npm/[email protected].3/dist/tweakpane.min.js"
}
}
</script>
Expand Down Expand Up @@ -125,6 +125,9 @@

let ros = null;
let robot = null;
const hostParam = {
host: "ws://127.0.0.1:9090"
};
let threeDMouse = null;
let filter = new SmoothingDeadbandFilter({ smoothing: 0.5, deadband: 0.1 });
let twistVizOverlays = null;
Expand All @@ -142,6 +145,8 @@
deadbandWeight: 0.4,
translationEnabled: true,
rotationEnabled: true,
translationScale: 1.0,
rotationScale: 1.0,
deadbandSize: 0.1,
};
const lastValue = {
Expand Down Expand Up @@ -217,6 +222,7 @@
let ui = createConfigurationPane({
container: document.getElementById("configuration"),
status: status,
hostParam: hostParam,
filter: filter,
filterParams: filterParams,
lastValue: lastValue,
Expand All @@ -241,7 +247,7 @@
if (!device) {
return;
}
initializeRos()
initializeRos(hostParam["host"])
.then((rosConnection) => {
ros = rosConnection;
instructionsContainer.style.display = "none";
Expand Down Expand Up @@ -276,6 +282,12 @@
status["message"] = "Error: ROS webbridge connection failed";
});

window.addEventListener("3dmousebuttonup", (event) => {
robot.forwardButtonChange(event.detail.name, "up");
});
window.addEventListener("3dmousebuttondown", (event) => {
robot.forwardButtonChange(event.detail.name, "down");
});
window.addEventListener("3dmouseinput", (event) => {
let values = event.detail.filteredInput;
if (twistVizOverlays) {
Expand All @@ -289,6 +301,7 @@
}
}
}
robot.forwardButtonStates(event.detail.buttons);
updateLastValues(event, lastValue);
uiFolders["values"].refresh();
});
Expand Down
51 changes: 25 additions & 26 deletions test/robot.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ensureVector3 } from "../dist/linAlg.js";

/**
* Represents a Robot interface for ROS.
* A class representing a robot connecting via ROS
*/

export class Robot {
Expand All @@ -14,9 +14,27 @@ export class Robot {
this.ros = ros;
this.twistTopic = new ROSLIB.Topic({
ros: this.ros,
//name: "/threedmouse/twist",
name: "/servo_node/delta_twist_cmds",
messageType: "geometry_msgs/msg/TwistStamped",
});

// JSON string of button state dictionary
this.buttonsTopic = new ROSLIB.Topic({
ros: this.ros,
name: "/threedmouse/buttons",
messageType: "std_msgs/msg/String",
});

// We have avoided defining any ROS message types because
// we don't want the main package to depend on ROS, and because
// we don't want to require building some other package just to use the demo.
// So we'll string-ly type the button up/down events
this.buttonChangeTopic = new ROSLIB.Topic({
ros: this.ros,
name: "/threedmouse/button_change",
messageType: "std_msgs/msg/String",
});
}

/**
Expand Down Expand Up @@ -60,43 +78,24 @@ export class Robot {
this.twistTopic.publish(twistMsg);
}

/** Open the robot's gripper. */
openGripper() {
console.log("Open gripper");
const message = new ROSLIB.Message({});
this.openTopic.publish(message);
forwardButtonStates(states) {
this.buttonsTopic.publish(new ROSLIB.Message({ data: JSON.stringify(states) }));
}

/** Close the robot's gripper. */
closeGripper() {
console.log("Close gripper");
const message = new ROSLIB.Message({});
this.closeTopic.publish(message);
forwardButtonChange(name, state) {
this.buttonChangeTopic.publish(new ROSLIB.Message({ data: `${name},${state}`}));
}

/**
* Control the robot's gripper based on the provided value.
* @param {number} value - 1 to open the gripper, 2 to close it.
*/

controlGripper(value) {
if (value === 1) {
this.openGripper();
} else if (value === 2) {
this.closeGripper();
}
}
}

/**
* Initialize and connect to the ROS system.
* @returns {Promise} Resolves with the ROSLIB Ros instance if successful, rejects with an error otherwise.
*/

export function initializeRos() {
export function initializeRos(host="ws://127.0.0.1:9090") {
return new Promise((resolve, reject) => {
let ros = new ROSLIB.Ros({
url: "ws://127.0.0.1:9090",
url: host,
});

ros.on("connection", () => {
Expand Down
Loading

0 comments on commit de378c9

Please sign in to comment.