/*
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 "connexionbackend.h"
#include
#include
#include
#include
#include
namespace {
//The part of 3DconnexionClient's interface QET uses, with the
//values Blender uses (intern/ghost/intern/GHOST_NDOFManagerCocoa.mm).
constexpr quint32 MSG_DEVICE_STATE = 0x33645352; // '3dSR'
constexpr quint16 CMD_HANDLE_BUTTONS = 2;
constexpr quint16 CMD_HANDLE_AXIS = 3;
constexpr quint16 MODE_TAKE_OVER = 1;
constexpr quint32 MASK_ALL = 0x3fff;
constexpr quint32 MASK_ALL_BUTTONS = 0xffffffff;
constexpr quint32 SIGNATURE = 0x51456c54; // 'QElT'
//ConnexionDeviceState is packed to 2 bytes. Offsets of the fields read.
constexpr int STATE_CLIENT = 2;
constexpr int STATE_COMMAND = 4;
constexpr int STATE_AXIS = 30;
constexpr int STATE_BUTTONS = 44;
using MessageHandler = void (*)(quint32, quint32, void *);
using DeviceHandler = void (*)(quint32);
using SetConnexionHandlers = qint16 (*)(MessageHandler, DeviceHandler, DeviceHandler, bool);
using RegisterConnexionClient = quint16 (*)(quint32, const quint8 *, quint16, quint32);
using SetConnexionClientButtonMask = void (*)(quint16, quint32);
using CleanupConnexionHandlers = void (*)();
using UnregisterConnexionClient = void (*)(quint16);
//3DxWare's callbacks carry no context, so the one client lives here.
//The mutex is shared with 3DxWare's thread.
QMutex instance_mutex;
ConnexionBackend *instance = nullptr;
void deviceChanged(quint32) {}
}
const char ConnexionBackend::DEFAULT_LIBRARY[] =
"/Library/Frameworks/3DconnexionClient.framework/3DconnexionClient";
/**
@brief ConnexionBackend::ConnexionBackend
Load 3DxWare's client library and register with it. Not installed, or
installed but not running, leaves isAvailable() false: the ordinary
case, never reported as an error.
@param parent
@param library : the client library; only tests pass another
*/
ConnexionBackend::ConnexionBackend(QObject *parent, const QString &library) :
SpaceMouseBackend(parent),
m_library(library)
{
{
QMutexLocker lock(&instance_mutex);
if (instance) {
return; //3DxWare takes one set of handlers per process
}
}
if (!m_library.load()) {
return;
}
const auto set_handlers = reinterpret_cast(
m_library.resolve("SetConnexionHandlers"));
const auto register_client = reinterpret_cast(
m_library.resolve("RegisterConnexionClient"));
const auto button_mask = reinterpret_cast(
m_library.resolve("SetConnexionClientButtonMask"));
m_cleanup = reinterpret_cast(
m_library.resolve("CleanupConnexionHandlers"));
m_unregister = reinterpret_cast(
m_library.resolve("UnregisterConnexionClient"));
if (!set_handlers || !register_client || !m_cleanup || !m_unregister) {
return;
}
{
QMutexLocker lock(&instance_mutex);
instance = this;
}
//Fails while 3DxWare is installed but its driver is not running.
if (set_handlers(&ConnexionBackend::onMessage, deviceChanged, deviceChanged, true) != 0) {
shutdown();
return;
}
m_handlers_installed = true;
//3DxWare only sends a client its messages while that application
//is in front, and recognises it by its executable's name (a Pascal
//string). Take-over mode stops 3DxWare's own actions in QET, so the
//view never moves twice.
QByteArray name = QFileInfo(QCoreApplication::applicationFilePath())
.fileName().toUtf8().left(255);
name.prepend(char(name.size()));
m_client = register_client(SIGNATURE,
reinterpret_cast(name.constData()),
MODE_TAKE_OVER, MASK_ALL);
if (!m_client) {
shutdown();
return;
}
if (button_mask) {
button_mask(m_client, MASK_ALL_BUTTONS);
}
}
/**
@brief ConnexionBackend::~ConnexionBackend
*/
ConnexionBackend::~ConnexionBackend()
{
shutdown();
}
/**
@brief ConnexionBackend::sampleFromAxes
3DxWare reports y up and z away from the user; the raw USB reports, and
so every other backend, have y towards the user and z down. Derived from
Blender, whose 3DxWare and spacenavd code paths must agree.
@param axis : TX, TY, TZ, RX, RY, RZ as 3DxWare sends them
@return the same movement in QET's convention
*/
SpaceMouseSample ConnexionBackend::sampleFromAxes(const qint16 axis[6])
{
SpaceMouseSample sample;
sample.x = axis[0];
sample.y = -axis[2];
sample.z = -axis[1];
sample.rx = axis[3];
sample.ry = -axis[5];
sample.rz = -axis[4];
return sample;
}
/**
@brief ConnexionBackend::newlyPressed
@param before : the button bitmask of the previous message
@param now : the button bitmask of this one
@return the 0-based buttons pressed since \a before, as HidBackend numbers them
*/
QList ConnexionBackend::newlyPressed(quint32 before, quint32 now)
{
QList pressed;
const quint32 down = now & ~before;
for (int bit = 0; bit < 32; ++bit) {
if (down & (quint32(1) << bit)) {
pressed.append(bit);
}
}
return pressed;
}
/**
@brief ConnexionBackend::onMessage
Runs on 3DxWare's thread. Copies what matters out of the message and
hands it to the main thread.
@param type : the message type
@param argument : a ConnexionDeviceState for MSG_DEVICE_STATE
*/
void ConnexionBackend::onMessage(quint32, quint32 type, void *argument)
{
if (type != MSG_DEVICE_STATE || !argument) {
return;
}
const char *state = static_cast(argument);
quint16 client;
quint16 command;
qint16 axis[6];
quint32 buttons;
std::memcpy(&client, state + STATE_CLIENT, sizeof client);
std::memcpy(&command, state + STATE_COMMAND, sizeof command);
std::memcpy(axis, state + STATE_AXIS, sizeof axis);
std::memcpy(&buttons, state + STATE_BUTTONS, sizeof buttons);
//Queued with the backend as context: if it is deleted before the
//main thread gets to it, Qt drops the call.
QMutexLocker lock(&instance_mutex);
ConnexionBackend *backend = instance;
if (!backend) {
return;
}
QMetaObject::invokeMethod(backend, [=]() {
backend->handleState(client, command, axis, buttons);
}, Qt::QueuedConnection);
}
/**
@brief ConnexionBackend::handleState
On the main thread: turn one device state into signals.
*/
void ConnexionBackend::handleState(quint16 client, quint16 command,
const qint16 axis[6], quint32 buttons)
{
if (client != m_client) {
return; //3DxWare sends every state to every client
}
if (command == CMD_HANDLE_AXIS) {
emit motion(sampleFromAxes(axis));
} else if (command == CMD_HANDLE_BUTTONS) {
//State first: a button can open a dialog whose event loop
//delivers the next message before emit returns.
const QList pressed = newlyPressed(m_buttons, buttons);
m_buttons = buttons;
for (int button : pressed) {
emit buttonPressed(button);
}
}
}
/**
@brief ConnexionBackend::shutdown
Unregister and stop 3DxWare's callbacks. The library stays loaded, since
3DxWare's thread may still be returning from one.
*/
void ConnexionBackend::shutdown()
{
if (m_client && m_unregister) {
m_unregister(m_client);
}
m_client = 0;
if (m_handlers_installed && m_cleanup) {
m_cleanup();
}
m_handlers_installed = false;
QMutexLocker lock(&instance_mutex);
if (instance == this) {
instance = nullptr;
}
}