mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-06 11:14:14 +02:00
Merge pull request #1025 from ispyisail/feature/spacemouse-hid
Add a 3D mouse backend that reads the device over USB (hidapi)
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "hidbackend.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#include <hidapi.h>
|
||||
|
||||
namespace {
|
||||
constexpr int SCAN_INTERVAL_MS = 3000;
|
||||
constexpr int FAST_POLL_MS = 8; // devices report at up to ~125 Hz
|
||||
constexpr int SLOW_POLL_MS = 50;
|
||||
//Fast polls in a row with nothing to read before slowing down.
|
||||
constexpr int IDLE_POLLS_BEFORE_SLOW = 125;
|
||||
constexpr int MAX_REPORT_SIZE = 64;
|
||||
constexpr int MAX_DESCRIPTOR_SIZE = 4096;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief HidBackend::HidBackend
|
||||
@param parent
|
||||
*/
|
||||
HidBackend::HidBackend(QObject *parent) :
|
||||
SpaceMouseBackend(parent)
|
||||
{
|
||||
if (hid_init() != 0) {
|
||||
return;
|
||||
}
|
||||
m_hid_initialised = true;
|
||||
|
||||
m_read_timer = new QTimer(this);
|
||||
connect(m_read_timer, &QTimer::timeout, this, &HidBackend::readReports);
|
||||
|
||||
m_scan_timer = new QTimer(this);
|
||||
m_scan_timer->setInterval(SCAN_INTERVAL_MS);
|
||||
connect(m_scan_timer, &QTimer::timeout, this, &HidBackend::scan);
|
||||
|
||||
scan();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief HidBackend::~HidBackend
|
||||
*/
|
||||
HidBackend::~HidBackend()
|
||||
{
|
||||
close();
|
||||
if (m_hid_initialised) {
|
||||
hid_exit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief HidBackend::scan
|
||||
Open the first 3D mouse found, or keep looking every few seconds.
|
||||
*/
|
||||
void HidBackend::scan()
|
||||
{
|
||||
if (m_device) {
|
||||
return;
|
||||
}
|
||||
|
||||
hid_device_info *list = hid_enumerate(0, 0);
|
||||
for (hid_device_info *info = list; info && !m_device; info = info->next)
|
||||
{
|
||||
if (!SpaceMouseHid::isSpaceMouse(info->vendor_id, info->product_id,
|
||||
info->usage_page, info->usage)) {
|
||||
continue;
|
||||
}
|
||||
m_device = hid_open_path(info->path);
|
||||
}
|
||||
hid_free_enumeration(list);
|
||||
|
||||
if (!m_device) {
|
||||
m_scan_timer->start();
|
||||
return;
|
||||
}
|
||||
m_scan_timer->stop();
|
||||
hid_set_nonblocking(m_device, 1);
|
||||
|
||||
SpaceMouseHid::Layout layout;
|
||||
#if HID_API_VERSION >= HID_API_MAKE_VERSION(0, 14, 0)
|
||||
unsigned char descriptor[MAX_DESCRIPTOR_SIZE];
|
||||
const int size = hid_get_report_descriptor(m_device, descriptor, sizeof descriptor);
|
||||
if (size > 0) {
|
||||
layout = SpaceMouseHid::parseDescriptor(
|
||||
QByteArray(reinterpret_cast<const char *>(descriptor), size));
|
||||
}
|
||||
#endif
|
||||
if (!layout.hasAxes()) {
|
||||
layout = SpaceMouseHid::fallbackLayout();
|
||||
}
|
||||
m_decoder = SpaceMouseHid::Decoder(layout);
|
||||
|
||||
m_idle_polls = 0;
|
||||
m_read_timer->start(FAST_POLL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief HidBackend::readReports
|
||||
Drain every report waiting, emit what they mean, and adjust the polling
|
||||
rate. A read error means the device went away (unplugged, or the
|
||||
wireless receiver lost it): go back to looking for one.
|
||||
*/
|
||||
void HidBackend::readReports()
|
||||
{
|
||||
//A button can trigger an action that opens a dialog, whose event
|
||||
//loop fires this timer again before the signal returns. That is
|
||||
//fine -- the device keeps working in the dialog -- because the
|
||||
//signals are emitted last, after every use of m_device below; this
|
||||
//guard only keeps two reads from ever overlapping.
|
||||
if (m_reading || !m_device) {
|
||||
return;
|
||||
}
|
||||
m_reading = true;
|
||||
|
||||
unsigned char buffer[MAX_REPORT_SIZE];
|
||||
bool got_any = false;
|
||||
//Everything read in one poll is one moment: a device that sends
|
||||
//translation and rotation as two reports, or two samples that
|
||||
//queued up, gives one sample here -- the latest state -- so the
|
||||
//listener's time scaling sees one sample per real interval.
|
||||
bool moved = false;
|
||||
SpaceMouseSample latest;
|
||||
QList<int> pressed;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
const int size = hid_read(m_device, buffer, sizeof buffer);
|
||||
if (size == 0) {
|
||||
break;
|
||||
}
|
||||
if (size < 0) {
|
||||
close();
|
||||
m_scan_timer->start();
|
||||
break;
|
||||
}
|
||||
got_any = true;
|
||||
|
||||
const SpaceMouseHid::Decoder::Result result = m_decoder.feed(
|
||||
QByteArray(reinterpret_cast<const char *>(buffer), size));
|
||||
if (result.motion) {
|
||||
moved = true;
|
||||
latest = result.sample;
|
||||
}
|
||||
pressed += result.pressed;
|
||||
}
|
||||
|
||||
if (m_device) {
|
||||
if (got_any) {
|
||||
m_idle_polls = 0;
|
||||
if (m_read_timer->interval() != FAST_POLL_MS) {
|
||||
m_read_timer->setInterval(FAST_POLL_MS);
|
||||
}
|
||||
} else if (++m_idle_polls == IDLE_POLLS_BEFORE_SLOW) {
|
||||
m_read_timer->setInterval(SLOW_POLL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
//Signals last: whatever they trigger (even the device going away
|
||||
//during a dialog), nothing after them touches the device.
|
||||
m_reading = false;
|
||||
if (moved) {
|
||||
emit motion(latest);
|
||||
}
|
||||
for (int button : pressed) {
|
||||
emit buttonPressed(button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief HidBackend::close
|
||||
*/
|
||||
void HidBackend::close()
|
||||
{
|
||||
if (m_read_timer) {
|
||||
m_read_timer->stop();
|
||||
}
|
||||
if (m_device) {
|
||||
hid_close(m_device);
|
||||
m_device = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef HIDBACKEND_H
|
||||
#define HIDBACKEND_H
|
||||
|
||||
#include "spacemousebackend.h"
|
||||
#include "spacemousehid.h"
|
||||
|
||||
class QTimer;
|
||||
struct hid_device_;
|
||||
|
||||
/**
|
||||
@brief The HidBackend class
|
||||
SpaceMouseBackend that reads the device directly over USB through
|
||||
hidapi, with no 3Dconnexion driver or SDK: the backend for Windows and
|
||||
macOS, and for Linux without spacenavd. Only compiled in when
|
||||
QET_SPACEMOUSE_BACKEND_HID is defined (cmake/find_spacemouse.cmake).
|
||||
|
||||
The raw reports are decoded by SpaceMouseHid, from the device's own
|
||||
report descriptor, into the same values spacenavd would give.
|
||||
|
||||
hidapi has no event to wait on, so the device is polled from the main
|
||||
thread: fast while it is moving, slowly once it has been still for a
|
||||
moment. With no device, it looks for one every few seconds, so plugging
|
||||
one in works without restarting QElectroTech. As with every backend,
|
||||
no device is the normal case and is never reported as an error.
|
||||
*/
|
||||
class HidBackend : public SpaceMouseBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HidBackend(QObject *parent = nullptr);
|
||||
~HidBackend() override;
|
||||
|
||||
bool isAvailable() const override { return m_device != nullptr; }
|
||||
|
||||
private slots:
|
||||
void scan();
|
||||
void readReports();
|
||||
|
||||
private:
|
||||
void close();
|
||||
|
||||
bool m_hid_initialised = false;
|
||||
hid_device_ *m_device = nullptr;
|
||||
SpaceMouseHid::Decoder m_decoder;
|
||||
QTimer *m_scan_timer = nullptr;
|
||||
QTimer *m_read_timer = nullptr;
|
||||
int m_idle_polls = 0;
|
||||
bool m_reading = false;
|
||||
};
|
||||
|
||||
#endif // HIDBACKEND_H
|
||||
@@ -27,23 +27,16 @@
|
||||
Platform seam for discussion #599's 3D mouse support. A backend owns
|
||||
one platform's connection to the actual 6-DOF device driver -- opening
|
||||
it, pumping whatever event source that platform uses, closing it -- and
|
||||
reports motion through the one signal below. Everything platform-
|
||||
independent (which DiagramView to apply motion to, the pan/zoom
|
||||
primitives to call, the Z-to-zoom-factor mapping) lives in
|
||||
SpaceMouseListener instead, once, so it does not have to be duplicated
|
||||
reports motion through the signals below. Everything platform-
|
||||
independent (which view to apply motion to, the pan/zoom primitives to
|
||||
call, the sample-to-motion mapping) lives in SpaceMouseListener and
|
||||
SpaceMouseMotion instead, once, so it does not have to be duplicated
|
||||
or re-verified per backend.
|
||||
|
||||
SpnavBackend (Linux, spacenavd/libspnav) is the only implementation so
|
||||
far -- built, linked, and its "no daemon/device present" path actually
|
||||
run in the environment this was written in. A Windows/macOS backend
|
||||
(3Dconnexion's proprietary 3DxWare SDK) would implement this same
|
||||
interface and be selected in SpaceMouseListener's constructor, without
|
||||
changing SpnavBackend or anything downstream of the motion signal.
|
||||
Deliberately not attempted here: this was written on Linux with no
|
||||
3DxWare SDK and no Windows/macOS toolchain available to compile,
|
||||
link, or run a single line of it against, and shipping platform code
|
||||
that has never even built would be a materially different, weaker
|
||||
thing than everything else in this feature.
|
||||
Two implementations, chosen at build time (cmake/find_spacemouse.cmake):
|
||||
SpnavBackend (Linux, through spacenavd/libspnav) and HidBackend (any
|
||||
platform, directly over USB through hidapi, with no 3Dconnexion driver
|
||||
or SDK). Both report the same values for the same movement.
|
||||
*/
|
||||
class SpaceMouseBackend : public QObject
|
||||
{
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "spacemousehid.h"
|
||||
|
||||
#include <QHash>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<unsigned short> 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<quint8>(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<qint32>(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<int>((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<Globals> stack;
|
||||
QList<quint32> usages; // full 32-bit usages (page << 16 | id)
|
||||
quint32 usage_min = 0, usage_max = 0;
|
||||
bool have_range = false;
|
||||
QHash<quint32, int> 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<quint8>(descriptor.at(i));
|
||||
if (prefix == 0xfe) { // long item: never used by these devices
|
||||
if (i + 1 >= n) break;
|
||||
i += 3 + static_cast<quint8>(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<quint8>(descriptor.at(i + 1 + b))) << (8 * b);
|
||||
}
|
||||
qint32 svalue = static_cast<qint32>(uvalue);
|
||||
if (size > 0 && size < 4 && (uvalue & (1u << (8 * size - 1)))) {
|
||||
svalue = static_cast<qint32>(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<quint32>(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<int>(k, usages.size() - 1));
|
||||
}
|
||||
|
||||
Field field;
|
||||
field.report_id = static_cast<int>(globals.report_id);
|
||||
field.bit_offset = bit + static_cast<int>(k * globals.report_size);
|
||||
field.bit_size = static_cast<int>(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<int>(qMin<qint64>(
|
||||
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<quint8>(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<int> 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<quint8>(payload.at(offset))
|
||||
| static_cast<quint8>(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;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef SPACEMOUSEHID_H
|
||||
#define SPACEMOUSEHID_H
|
||||
|
||||
#include "spacemousemotion.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
|
||||
/**
|
||||
Decoding of a 3D mouse's raw USB HID reports, for the backends that read
|
||||
the device directly (HidBackend) rather than through a driver that has
|
||||
already decoded them (spacenavd). No I/O here, so it is unit-tested from
|
||||
recorded bytes (tests/qttest/tst_spacemousehid).
|
||||
*/
|
||||
namespace SpaceMouseHid
|
||||
{
|
||||
/// USB vendor ids 3Dconnexion devices use: Logitech's for the older
|
||||
/// ones, 3Dconnexion's own for everything since about 2011.
|
||||
constexpr unsigned short VENDOR_LOGITECH = 0x046d;
|
||||
constexpr unsigned short VENDOR_3DCONNEXION = 0x256f;
|
||||
|
||||
/// @return whether a HID interface is a 3D mouse: a 3Dconnexion
|
||||
/// vendor id and the Generic Desktop "multi-axis controller" usage.
|
||||
/// Logitech devices that do not report a usage are matched by
|
||||
/// product id instead, from the list of their 3D mice.
|
||||
bool isSpaceMouse(unsigned short vendor, unsigned short product,
|
||||
unsigned short usage_page, unsigned short usage);
|
||||
|
||||
/// Where one value sits in the reports, per the report descriptor.
|
||||
struct Field
|
||||
{
|
||||
int report_id = 0; ///< 0 when the device does not number its reports
|
||||
int bit_offset = 0; ///< after the report id byte, if any
|
||||
int bit_size = 0;
|
||||
int logical_min = 0;
|
||||
int logical_max = 0;
|
||||
bool relative = false;
|
||||
|
||||
bool isValid() const { return bit_size > 0; }
|
||||
};
|
||||
|
||||
/// What the report descriptor says about the six axes and the buttons.
|
||||
struct Layout
|
||||
{
|
||||
Field axes[6]; ///< x, y, z, rx, ry, rz
|
||||
QVector<Field> buttons; ///< button n (0-based) is buttons[n]
|
||||
bool numbered_reports = false;
|
||||
|
||||
bool hasAxes() const;
|
||||
};
|
||||
|
||||
/// @return the layout described by \a descriptor, or one where
|
||||
/// hasAxes() is false if it describes no axes (or is empty)
|
||||
Layout parseDescriptor(const QByteArray &descriptor);
|
||||
|
||||
/// @return the fixed layout of the classic 3Dconnexion reports
|
||||
/// (1 = translation, 2 = rotation, 3 = button bitmask), used when
|
||||
/// the descriptor cannot be read
|
||||
Layout fallbackLayout();
|
||||
|
||||
/**
|
||||
@brief The Decoder class
|
||||
Turns a stream of raw reports into samples and button presses.
|
||||
A device can send translation and rotation in separate reports, so
|
||||
the decoder keeps the last value of every axis and reports all six
|
||||
each time any of them changes.
|
||||
*/
|
||||
class Decoder
|
||||
{
|
||||
public:
|
||||
struct Result
|
||||
{
|
||||
bool motion = false; ///< sample is new
|
||||
SpaceMouseSample sample;
|
||||
QList<int> pressed; ///< buttons pressed since the previous report
|
||||
};
|
||||
|
||||
explicit Decoder(const Layout &layout = fallbackLayout());
|
||||
Result feed(const QByteArray &report);
|
||||
|
||||
private:
|
||||
Layout m_layout;
|
||||
int m_axes[6] = {0, 0, 0, 0, 0, 0};
|
||||
QSet<int> m_down; ///< buttons currently held
|
||||
};
|
||||
}
|
||||
|
||||
#endif // SPACEMOUSEHID_H
|
||||
@@ -22,6 +22,9 @@
|
||||
#ifdef QET_SPACEMOUSE_BACKEND_SPNAV
|
||||
# include "spnavbackend.h"
|
||||
#endif
|
||||
#ifdef QET_SPACEMOUSE_BACKEND_HID
|
||||
# include "hidbackend.h"
|
||||
#endif
|
||||
|
||||
#include "../diagramview.h"
|
||||
#include "../editor/elementview.h"
|
||||
@@ -45,12 +48,11 @@ SpaceMouseListener::SpaceMouseListener(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_settings(SpaceMouseSettings::load())
|
||||
{
|
||||
#ifdef QET_SPACEMOUSE_BACKEND_SPNAV
|
||||
#if defined(QET_SPACEMOUSE_BACKEND_SPNAV)
|
||||
m_backend = new SpnavBackend(this);
|
||||
#elif defined(QET_SPACEMOUSE_BACKEND_HID)
|
||||
m_backend = new HidBackend(this);
|
||||
#endif
|
||||
//A future Windows/macOS backend (3Dconnexion's proprietary 3DxWare
|
||||
//SDK) slots in here behind its own QET_SPACEMOUSE_BACKEND_3DXWARE
|
||||
//guard, without changing anything below this constructor.
|
||||
|
||||
if (m_backend) {
|
||||
connect(m_backend, &SpaceMouseBackend::motion,
|
||||
|
||||
@@ -91,7 +91,7 @@ bool SpaceMouseSettings::operator==(const SpaceMouseSettings &other) const
|
||||
*/
|
||||
qreal SpaceMouseMotion::stepFor(qint64 elapsed_ms)
|
||||
{
|
||||
if (elapsed_ms <= 0 || elapsed_ms > MAX_PERIOD_MS) {
|
||||
if (elapsed_ms < 0 || elapsed_ms > MAX_PERIOD_MS) {
|
||||
return 1.0;
|
||||
}
|
||||
return elapsed_ms / NOMINAL_PERIOD_MS;
|
||||
|
||||
@@ -85,7 +85,9 @@ namespace SpaceMouseMotion
|
||||
constexpr qint64 MAX_PERIOD_MS = 100;
|
||||
|
||||
/// @return how many nominal periods a sample covers, given the
|
||||
/// milliseconds since the previous one (negative = no previous one)
|
||||
/// milliseconds since the previous one: none for a sample in the
|
||||
/// same millisecond (it covers no time), one for the first sample
|
||||
/// or the first after a rest (elapsed negative or over the maximum)
|
||||
qreal stepFor(qint64 elapsed_ms);
|
||||
|
||||
/// @return \a value with \a dead_zone removed: 0 inside it, and
|
||||
|
||||
Reference in New Issue
Block a user