/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see .
*/
#include "spacemousehid.h"
#include
#include
namespace {
constexpr unsigned short PAGE_GENERIC_DESKTOP = 0x01;
constexpr unsigned short PAGE_BUTTON = 0x09;
constexpr unsigned short USAGE_MULTI_AXIS = 0x08;
constexpr unsigned short USAGE_X = 0x30; // X, Y, Z, Rx, Ry, Rz follow
//Newer devices (SpaceMouse Enterprise, the wireless ones) report the
//buttons held as a list of 16-bit button numbers in this report,
//outside the Button usage page.
constexpr int REPORT_BUTTON_LIST = 0x1c;
//spacenavd rescales absolute axes to this range, so doing the same
//makes both backends hand QET the same numbers.
constexpr int SCALED_MIN = -500;
constexpr int SCALED_MAX = 500;
//Limits on what a descriptor can make the parser walk.
constexpr quint32 MAX_FIELDS = 1024;
constexpr qint64 MAX_BITS = 1 << 20;
//3D mice sold under Logitech's vendor id, for the ones whose
//interface does not report a usage.
const QSet LOGITECH_3D_MICE = {
0xc603, 0xc605, 0xc606, 0xc621, 0xc623, 0xc625,
0xc626, 0xc627, 0xc628, 0xc629, 0xc62b
};
/// Read \a size bits at \a offset, little-endian as HID packs them.
bool extract(const QByteArray &payload, int offset, int size, bool is_signed, int *value)
{
if (size <= 0 || size > 32 || offset < 0
|| (offset + size + 7) / 8 > payload.size()) {
return false;
}
quint32 raw = 0;
for (int b = 0; b < size; ++b) {
const int bit = offset + b;
if (static_cast(payload.at(bit / 8)) & (1u << (bit % 8))) {
raw |= 1u << b;
}
}
if (is_signed && size < 32 && (raw & (1u << (size - 1)))) {
raw |= ~((1u << size) - 1);
}
*value = static_cast(raw);
return true;
}
int scaled(const SpaceMouseHid::Field &field, int value)
{
if (field.relative || field.logical_max <= field.logical_min) {
return value;
}
const qint64 range = qint64(field.logical_max) - field.logical_min;
return static_cast((qint64(value) - field.logical_min)
* (SCALED_MAX - SCALED_MIN) / range + SCALED_MIN);
}
}
bool SpaceMouseHid::isSpaceMouse(unsigned short vendor, unsigned short product,
unsigned short usage_page, unsigned short usage)
{
const bool multi_axis = usage_page == PAGE_GENERIC_DESKTOP && usage == USAGE_MULTI_AXIS;
const bool usage_unknown = usage_page == 0 && usage == 0;
if (vendor == VENDOR_3DCONNEXION) {
return multi_axis || usage_unknown;
}
if (vendor == VENDOR_LOGITECH) {
return multi_axis || (usage_unknown && LOGITECH_3D_MICE.contains(product));
}
return false;
}
bool SpaceMouseHid::Layout::hasAxes() const
{
for (const Field &f : axes) {
if (f.isValid()) {
return true;
}
}
return false;
}
/**
@brief SpaceMouseHid::parseDescriptor
Walks the short items of a HID report descriptor (HID 1.11, 6.2.2),
keeping only what the decoder needs: the Input fields carrying the six
Generic Desktop axes and the Button page.
@param descriptor
@return see the declaration
*/
SpaceMouseHid::Layout SpaceMouseHid::parseDescriptor(const QByteArray &descriptor)
{
struct Globals {
quint32 usage_page = 0;
qint32 logical_min = 0;
qint32 logical_max = 0;
quint32 report_size = 0;
quint32 report_count = 0;
quint32 report_id = 0;
};
Layout layout;
Globals globals;
QList stack;
QList usages; // full 32-bit usages (page << 16 | id)
quint32 usage_min = 0, usage_max = 0;
bool have_range = false;
QHash next_bit; // per report id
auto clearLocals = [&]() {
usages.clear();
have_range = false;
usage_min = usage_max = 0;
};
auto fullUsage = [&](quint32 value, int size) {
return size == 4 ? value : (globals.usage_page << 16) | value;
};
int i = 0;
const int n = descriptor.size();
while (i < n)
{
const quint8 prefix = static_cast(descriptor.at(i));
if (prefix == 0xfe) { // long item: never used by these devices
if (i + 1 >= n) break;
i += 3 + static_cast(descriptor.at(i + 1));
continue;
}
const int size = (prefix & 0x03) == 3 ? 4 : (prefix & 0x03);
const int type = (prefix >> 2) & 0x03;
const int tag = prefix >> 4;
if (i + 1 + size > n) break;
quint32 uvalue = 0;
for (int b = 0; b < size; ++b) {
uvalue |= quint32(static_cast(descriptor.at(i + 1 + b))) << (8 * b);
}
qint32 svalue = static_cast(uvalue);
if (size > 0 && size < 4 && (uvalue & (1u << (8 * size - 1)))) {
svalue = static_cast(uvalue | ~((1u << (8 * size)) - 1));
}
i += 1 + size;
if (type == 1) // global
{
switch (tag) {
case 0: globals.usage_page = uvalue; break;
case 1: globals.logical_min = svalue; break;
case 2: globals.logical_max = svalue; break;
case 7: globals.report_size = uvalue; break;
case 8: globals.report_id = uvalue; layout.numbered_reports = true; break;
case 9: globals.report_count = uvalue; break;
case 10: stack.append(globals); break;
case 11: if (!stack.isEmpty()) globals = stack.takeLast(); break;
}
}
else if (type == 2) // local
{
switch (tag) {
case 0: usages.append(fullUsage(uvalue, size)); break;
case 1: usage_min = fullUsage(uvalue, size); have_range = true; break;
case 2: usage_max = fullUsage(uvalue, size); have_range = true; break;
}
}
else if (type == 0) // main
{
if (tag == 8) // Input
{
const bool constant = uvalue & 0x01;
const bool variable = uvalue & 0x02;
const bool relative = uvalue & 0x04;
int &bit = next_bit[globals.report_id];
//A real report is at most a few hundred bytes; a count
//beyond that is a broken (or hostile) descriptor, and
//walking it field by field would stall the application.
const quint32 count = qMin(globals.report_count, MAX_FIELDS);
for (quint32 k = 0; k < count; ++k)
{
quint32 usage = 0;
if (have_range && usage_max >= usage_min) {
usage = qMin(usage_min + k, usage_max);
} else if (!usages.isEmpty()) {
usage = usages.at(qMin(k, usages.size() - 1));
}
Field field;
field.report_id = static_cast(globals.report_id);
field.bit_offset = bit + static_cast(k * globals.report_size);
field.bit_size = static_cast(globals.report_size);
field.logical_min = globals.logical_min;
field.logical_max = globals.logical_max;
field.relative = relative;
if (!constant && variable) {
const quint32 page = usage >> 16, id = usage & 0xffff;
if (page == PAGE_GENERIC_DESKTOP && id >= USAGE_X && id < USAGE_X + 6) {
layout.axes[id - USAGE_X] = field;
} else if (page == PAGE_BUTTON && id >= 1 && id <= 64) {
if (layout.buttons.size() < int(id)) {
layout.buttons.resize(id);
}
layout.buttons[id - 1] = field;
}
}
}
bit = static_cast(qMin(
bit + qint64(count) * globals.report_size, MAX_BITS));
}
clearLocals(); // every main item (Input, Collection, ...) ends the locals
}
}
return layout;
}
SpaceMouseHid::Layout SpaceMouseHid::fallbackLayout()
{
Layout layout;
layout.numbered_reports = true;
for (int a = 0; a < 6; ++a) {
Field &f = layout.axes[a];
f.report_id = a < 3 ? 1 : 2;
f.bit_offset = (a % 3) * 16;
f.bit_size = 16;
f.logical_min = -32768; // signed, and relative: passed through as sent
f.logical_max = 32767;
f.relative = true;
}
layout.buttons.resize(32);
for (int b = 0; b < 32; ++b) {
Field &f = layout.buttons[b];
f.report_id = 3;
f.bit_offset = b;
f.bit_size = 1;
f.logical_max = 1;
}
return layout;
}
SpaceMouseHid::Decoder::Decoder(const Layout &layout) :
m_layout(layout)
{}
/**
@brief SpaceMouseHid::Decoder::feed
@param report : one report as read from the device, report id first
when the device numbers its reports
@return see Result
*/
SpaceMouseHid::Decoder::Result SpaceMouseHid::Decoder::feed(const QByteArray &report)
{
Result result;
if (report.isEmpty()) {
return result;
}
const int id = m_layout.numbered_reports ? static_cast(report.at(0)) : 0;
const QByteArray payload = m_layout.numbered_reports ? report.mid(1) : report;
for (int a = 0; a < 6; ++a)
{
const Field &f = m_layout.axes[a];
int value = 0;
if (f.isValid() && f.report_id == id
&& extract(payload, f.bit_offset, f.bit_size, f.logical_min < 0, &value)) {
m_axes[a] = scaled(f, value);
result.motion = true;
}
}
//Some devices send rotation after translation in one longer report
//1, while their layout (or the fallback, which has no descriptor)
//puts rotation in a report of its own: read it from report 1 too.
if (id == 1 && payload.size() >= 12
&& m_layout.axes[3].isValid() && m_layout.axes[3].report_id != 1) {
for (int a = 3; a < 6; ++a) {
const Field &f = m_layout.axes[a];
int value = 0;
if (extract(payload, 48 + f.bit_offset, f.bit_size, f.logical_min < 0, &value)) {
m_axes[a] = scaled(f, value);
}
}
}
QSet down;
bool buttons_in_report = false;
for (int b = 0; b < m_layout.buttons.size(); ++b)
{
const Field &f = m_layout.buttons.at(b);
int value = 0;
if (f.isValid() && f.report_id == id
&& extract(payload, f.bit_offset, f.bit_size, false, &value)) {
buttons_in_report = true;
if (value) {
down.insert(b);
}
}
}
if (!buttons_in_report && m_layout.numbered_reports && id == REPORT_BUTTON_LIST)
{
buttons_in_report = true;
for (int offset = 0; offset + 1 < payload.size(); offset += 2) {
const int number = static_cast(payload.at(offset))
| static_cast(payload.at(offset + 1)) << 8;
//0 means no button, so the numbers start at 1: count from
//0 like the Button page and spacenavd do.
if (number) {
down.insert(number - 1);
}
}
}
if (buttons_in_report)
{
for (int b : down) {
if (!m_down.contains(b)) {
result.pressed.append(b);
}
}
std::sort(result.pressed.begin(), result.pressed.end());
m_down = down;
}
if (result.motion) {
result.sample.x = m_axes[0];
result.sample.y = m_axes[1];
result.sample.z = m_axes[2];
result.sample.rx = m_axes[3];
result.sample.ry = m_axes[4];
result.sample.rz = m_axes[5];
}
return result;
}