Hey everyone, this is my first post where I’m not asking for help but rather am looking to share something that others will hopefully find useful.
An upgrade I’ve been looking into is getting a USB game controller to control my XCP in Easel. You usually need a controller mapping software, or you’d have to use UGS, so I watched Steve Good’s neat youtube video where he used xPadder and Autohot key to get his setup to work, and it seems to work well, but there were a couple limitations:
- The window must always be in the same position (e.g. maximized) so the buttons always fall in the correct location on the screen.
- It doesn’t let you take advantage of the controller’s analog thumb stick to jog at different distances/sensitivities based on the analog position, and sometimes I just want to quickly get the spindle out of my way NOW without reaching for the mouse and navigating to the jog distance panel.
I started thinking: javascript natively has an API to communicate with your USB controller (for web gaming), so I decided to write a javascript hack that doesn’t require any external software, is agnostic to the window size / location, and where the inputs from my analog thumb switch determine the jog distance. And then this idea took on a life of its own.
Below is the code, it works perfectly for my $19 Logitech F310 controller, and you’re all obviously welcome to use it, but I highly advise you to be very careful and do lots of testing for your USB controller and machine to make sure you don’t run your machine into walls. I’d imagine the button names and indices differ from one controller to another, so you’ll want to test, and you might even want to update the code.
How do you use it?
Easy way: Inspect, understand, and then copy the below code, then go to your Easel browser window, enter developer mode (usually right click >> Inspect, or key press Ctrl+Shift+I), and paste the below code into the Console.
More advanced way: create a custom chrome extension out of this code. This way the code will load automatically anytime you access your projects in Easel. That’s what I did. happy to show you how if wanted.
Controls:
Left analog thumb stick controls the X and Y jogs (one axis at a time, can’t do both axes simultaneously for diagonal jogging yet).
Right analog thumb stick up/down direction controls the Z jog. Horizontal direction does nothing. I limited this to jog up to 0.1 inch increments, not 1" because I don’t want it going to fa when playing with the analog stick.
Up/down/left/right buttons (not the analog stick) do single step jogs using the currently set jog distance, same as keyboard arrow keys.
If simultaneously holding the right upper trigger button (RB I think), then the up/down buttons will do a single z-jog (similar to holding Shift key and using keyboard arrows), and for this, the jog distance is not limited to 0.1".
‘A’ button manually loops through the X/Y step intervals.
‘B’ button manually loops through the Z step intervals.
‘X’ button opens/closes the “Jog Machine” dialog box, because similar to keyboard controls, the joystick will only work if this dialog box is open.
‘Y’ button while holding the left upper trigger (LB) homes the machine.
I feel like there’s more that could be done, but that’s what I’ve gotten so far. Thank ChatGPT for doing all the leg work, I was just its boss.
let buttonPressStates = new Array(16).fill(false); // Tracks if a press has been registered for each button
let modalOpen = (document.getElementsByClassName("machine-control-modal").length == 1);
const svgButtonMap = {
12: 'y-up', // Up button
13: 'y-down', // Down button
14: 'x-left', // Left button
15: 'x-right', // Right button
};
function hitEscapeKey() {
// Create a KeyboardEvent object for the Escape key
const escapeKey = new KeyboardEvent("keydown", {
key: "Escape",
keyCode: 27, // Deprecated, but included for compatibility with older code
which: 27, // Deprecated, but included for compatibility with older code
bubbles: true, // Allow the event to bubble up through the DOM
cancelable: true // Allow the event to be cancellable
});
// Dispatch the event on the document object
document.dispatchEvent(escapeKey);
}
function cycleThroughButtons(buttons, hasClass) {
const primaryIndex = Array.from(buttons).findIndex(button => button.classList.contains(hasClass));
const nextIndex = (primaryIndex + 1) % buttons.length;
buttons.forEach((btn, index) => {
btn.classList.remove(hasClass);
if (index === nextIndex) {
btn.classList.add(hasClass);
}
});
buttons[nextIndex].click();
}
function handleSingleButtons(index) { //
switch (index) {
case 0: // A button - cycle through XY presets
if (modalOpen) {
const xyButtons = document.querySelectorAll('.step-interval__presets.xy_presets button');
cycleThroughButtons(xyButtons, "btn-primary");
}
break;
case 1: // B button - cycle through Z presets
if (modalOpen) {
const zButtons = document.querySelectorAll('.step-interval__presets.z_presets button');
cycleThroughButtons(zButtons, "btn-primary");
}
break;
case 2: // X button - toggle modal visibility
if (modalOpen) {
hitEscapeKey();
modalOpen = false;
} else {
document.querySelector('.machine-control-widget__button.machine-status-indicator').click();
modalOpen = true;
}
break;
}
}
function handleButtonCombos(combo) {
let elementToClickId, elementToClick;
switch (combo) {
case 1:
elementToClickId = "z-zUp";
elementToClick = document.querySelector(`g[id="${elementToClickId}"].jog-buttons__arrow`);
if (elementToClick) {
const clickEventSVG = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
elementToClick.dispatchEvent(clickEventSVG);
}
break;
case 2:
elementToClickId = "z-zDown";
elementToClick = document.querySelector(`g[id="${elementToClickId}"].jog-buttons__arrow`);
if (elementToClick) {
const clickEventSVG = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
elementToClick.dispatchEvent(clickEventSVG);
}
break;
case 3: // Y button - Home machine (if modal is open)
const buttons = document.querySelectorAll('.btn.btn-primary.m-1');
const homeButton = Array.from(buttons).find(button => button.innerText === 'Home');
if (homeButton) {
// If the button is found, you can now click it or perform any other actions
homeButton.click();
}
break;
}
}
// Mapping of gamepad button indexes to corresponding actions or elements
function updateGamepadState() {
setTimeout(updateGamepadState, 175); // Schedule the next call
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
for (let i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
if (gamepad) {
let currentButtonStates = gamepad.buttons.map(button => button.pressed);
if (currentButtonStates[5] && currentButtonStates[12] && !buttonPressStates[12]) {
handleButtonCombos(1);
buttonPressStates = currentButtonStates;
} else if (currentButtonStates[5] && currentButtonStates[13] && !buttonPressStates[13]) {
handleButtonCombos(2);
buttonPressStates = currentButtonStates;
} else if (currentButtonStates[4] && currentButtonStates[3] && !buttonPressStates[3]){
handleButtonCombos(3);
buttonPressStates = currentButtonStates;
} else {
gamepad.buttons.forEach((button, index) => {
if (button.pressed) {
if ([0, 1, 2, 3].includes(index)) { // Handle special buttons separately
handleSingleButtons(index);
} else if (svgButtonMap.hasOwnProperty(index) && !buttonPressStates[index]) {
// SVG click logic for jog buttons
let elementToClickId = svgButtonMap[index];
let elementToClick = document.querySelector(`g[id="${elementToClickId}"].jog-buttons__arrow`);
if (elementToClick) {
const clickEventSVG = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
elementToClick.dispatchEvent(clickEventSVG);
}
buttonPressStates[index] = true; // Mark as registered
}
} else {
// Reset the registered press flag when the button is released
buttonPressStates[index] = false;
}
});
const XAxis = gamepad.axes[0];
const YAxis = gamepad.axes[1];
const ZAxis = gamepad.axes[3];
const deadZone = 0.05; // Define a small threshold
//Left analog stick:
if (Math.abs(XAxis) > deadZone || Math.abs(YAxis) > deadZone) {
// Determine primary axis and direction
const isXAxisPrimary = Math.abs(XAxis) > Math.abs(YAxis);
const positiveDirection = isXAxisPrimary ? XAxis > 0 : YAxis > 0;
const primaryAxisValue = isXAxisPrimary ? Math.abs(XAxis) : Math.abs(YAxis);
// Determine step size based on custom scale
let index;
if (primaryAxisValue <= 0.37) {
index = 0;
} else if (primaryAxisValue <= 0.63) {
index = 1;
} else if (primaryAxisValue <= 0.99) {
index = 2;
} else {
index = 3;
}
// Click the determined step size button
const buttons = document.querySelectorAll('.step-interval__presets.xy_presets button');
if (index !== undefined & modalOpen) {
buttons[index].click();
}
// Delay to ensure the step size is set before executing the jog command
setTimeout(() => {
// Selecting the SVG element based on axis and direction
let elementToClickId = isXAxisPrimary ? (positiveDirection ? 'x-right' : 'x-left') : (positiveDirection ? 'y-down' : 'y-up');
let elementToClick = document.querySelector(`g[id="${elementToClickId}"].jog-buttons__arrow`);
// Trigger a click event if the SVG element is found
if (elementToClick) {
const clickEventSVG = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
elementToClick.dispatchEvent(clickEventSVG);
}
}, 8); // A short delay to allow sequence of actions
}
//Right analog stick
const ZAxisValue = Math.abs(ZAxis);
if (ZAxisValue > deadZone) {
// Determine direction
const positiveZDirection = ZAxis < 0;
// Determine step size based on custom scale
let indexZ;
if (ZAxisValue <= 0.4) {
indexZ = 0;
} else if (ZAxisValue <= 0.99) {
indexZ = 1;
} else {
indexZ = 2;
}
// Click the determined step size button
const buttons = document.querySelectorAll('.step-interval__presets.z_presets button');
if (indexZ !== undefined & modalOpen) {
buttons[indexZ].click();
}
// Delay to ensure the step size is set before executing the jog command
setTimeout(() => {
// Selecting the SVG element based on axis and direction
let elementZToClickId = (positiveZDirection ? 'z-zUp' : 'z-zDown');
let elementZToClick = document.querySelector(`g[id="${elementZToClickId}"].jog-buttons__arrow`);
// Trigger a click event if the SVG element is found
if (elementZToClick) {
const clickEventSVG = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
elementZToClick.dispatchEvent(clickEventSVG);
}
}, 8); // A short delay to allow sequence of actions
}
}
}
}
}
// Initialize the loop
updateGamepadState();