Lars Knudsen 🇩🇰
Posted on June 9, 2021
Finding gold!
For many years, I've been saving different odd hardware devices, that I would find use for SomeDay(TM).
In reality, that day rarely comes, so a few months back, I decided to get rid of all the unused stuff.
One item I initially put in the 'out' pile was a 3Dconnexion Spaceball 5000, because surely there would be little to no support for it in any OS.
However, I tried to connect it to my Ubuntu machine to see what would happen.
Running lsusb
gives us the following:
ID 046d:c621 Logitech, Inc. 3Dconnexion Spaceball 5000
... adding the -v
switch, tells us that there is one exposed HID interface:
...
bInterfaceClass 3 Human Interface Device
...
NOTE: Using (Ubuntu) Linux, in order to get access from user space, add the following, using the USB Vendor ID returned from lsusb
to a udev rules file (e.g. /etc/udev/rules/50-webhid.rules
):
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", MODE:="0666", GROUP="input"
And run sudo udevadm control --reload-rules && sudo udevadm trigger
On other systems, this is not necessary.
WebHID
Looking at the Fugu API tracker, we see that WebHID was released in Chrome M89, so let's try to hook it up and see what kind of data the Spaceball sends out.
We will start with something very simple, just to read all the data coming from the device.
<button id="scan">SCAN</button>
<script>
function scan() {
navigator.hid.requestDevice({filters: [{ vendorId: 0x046d }]}).then(devices => {
if (!devices.length) return;
const device = devices[0];
device.open().then(() => {
console.log('Opened device: ' + device.productName);
device.addEventListener('inputreport', e => {
console.log('Report ID', e.reportId);
console.log('Data', new Int8Array(e.data.buffer));
});
});
});
}
document.querySelector('#scan').addEventListener('click', scan);
</script>
Running this in Chrome and clicking the SCAN
button with the device attached should show a dialog, where we can now select the Spaceball 5000:
After a successful connection, you should see the following in the console:
Opened device: 3Dconnexion SpaceBall 5000 USB
YAY!
All the data!
Manipulating the Spaceball sends a lot of data packets of 6 bytes to the console, which seems to be pairs with report ID 1 and 2:
Report ID 1
Data Int8Array(6)Â [101, -1, -2, -1, 95, 0]
Report ID 2
Data Int8Array(6)Â [-27, 0, 35, 1, -126, -1]
After searching a bit for a protocol description, I read a small post from 2006, that the values are 16-bit signed X, Y, Z translation values, followed by the same for rotation.
Let's make a small change to the code:
...
device.addEventListener('inputreport', e => {
// First attempt: Print all the data
// console.log('Report ID', e.reportId);
// console.log('Data', new Int8Array(e.data.buffer));
// Second attempt: extract translation and rotation:
if (e.reportId === 1) console.log('T', new Int16Array(e.data.buffer));
if (e.reportId === 2) console.log('R', new Int16Array(e.data.buffer));
});
...
Now the data starts to make a bit more sense:
T Int16Array(3)Â [33, -18, 132]
R Int16Array(3)Â [-230, 67, 0]
T Int16Array(3)Â [39, -15, 124]
R Int16Array(3)Â [-208, 67, 0]
and when releasing the Spaceball, it seems to go to zero:
T Int16Array(3)Â [0, 0, 0]
R Int16Array(3)Â [0, 0, 0]
Making a simple driver
Now that all the basics are in place, let's write a small web driver for the device.
Extending EventTarget gives the driver the ability to send the parsed packets on to any listener as proper events.
export const SpaceDriver = new class extends EventTarget {
#device // Just allow one device, for now
constructor() {
super();
this.handleInputReport = this.handleInputReport.bind(this);
// See if a paired device is already connected
navigator.hid.getDevices().then((devices) => {
devices.filter(d => d.vendorId === deviceFilter.vendorId).forEach(this.openDevice.bind(this));
});
navigator.hid.addEventListener('disconnect', evt => {
const device = evt.device;
console.log('disconnected', device);
if (device === this.#device) {
this.disconnect();
}
});
}
openDevice(device) {
this.disconnect(); // If another device is connected - close it
device.open().then(() => {
console.log('Opened device: ' + device.productName);
device.addEventListener('inputreport', this.handleInputReport);
this.#device = device;
this.dispatchEvent(new CustomEvent('connect', {detail: { device }}));
});
}
disconnect() {
this.#device?.close();
this.#device = undefined;
this.dispatchEvent(new Event('disconnect'));
}
scan() {
navigator.hid.requestDevice(requestParams).then(devices => {
if (devices.length == 0) return;
this.openDevice(devices[0]);
});
}
handleInputReport(e) {
switch(e.reportId) {
case 1: // x, y, z
this.handleTranslation(new Int16Array(e.data.buffer));
break;
case 2: // yaw, pitch, roll
this.handleRotation(new Int16Array(e.data.buffer));
break;
}
}
handleTranslation(val) {
this.dispatchEvent(new CustomEvent('translate', {
detail: {
x: val[0],
y: val[1],
z: val[2]
}
}));
}
handleRotation(val) {
this.dispatchEvent(new CustomEvent('rotate', {
detail: {
rx: -val[0],
ry: -val[1],
rz: val[2]
}
}));
}
}
Visualizing with Web Components and CSS3D
Just writing numbers in the console is no fun, and as we are using a modern browser with native CSS3D and Web Component support, why not make a silly 3D object in a component, we can manipulate with the Spaceball:
export class Demo3DObj extends HTMLElement {
#objtranslate
#objrotate
#obj
constructor() {
super();
this.#objtranslate = '';
this.#objrotate = '';
}
connectedCallback() {
this.innerHTML = `
<style>
.scene {
width: 200px;
height: 200px;
margin: 200px;
perspective: 500px;
}
.obj {
width: 200px;
height: 200px;
position: relative;
transform-style: preserve-3d;
transform: translateZ(-1000px);
transition: transform 100ms;
}
.plane {
position: absolute;
width: 200px;
height: 200px;
border: 5px solid black;
border-radius: 50%;
}
.red {
background: rgba(255,0,0,0.5);
transform: rotateY(-90deg)
}
.green {
background: rgba(0,255,0,0.5);
transform: rotateX( 90deg)
}
.blue {
background: rgba(0,0,255,0.5);
}
</style>
<div class="scene">
<div class="obj">
<div class="plane red"></div>
<div class="plane green"></div>
<div class="plane blue"></div>
</div>
</div>
`;
this.#obj = this.querySelector('.obj');
}
setTranslation(x, y, z) {
this.#objtranslate = `translateX(${x}px) translateY(${y}px) translateZ(${z}px) `;
this.#obj.style.transform = this.#objtranslate + this.#objrotate;
}
setRotation(rx, ry, rz) {
this.#objrotate = `rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg) `;
this.#obj.style.transform = this.#objtranslate + this.#objrotate;
}
}
customElements.define('demo-3dobj', Demo3DObj);
All that is left is to combine the SpaceDriver
and demo-3dobj
to see the direct manipulation of a 3D object using the WebHID connected Spaceball 5000:
Live demo and source code on GitHub
ENJOY!
Posted on June 9, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.