From c5088c6fabf00d3c0a03dc4c3da2bd9cf7580c4a Mon Sep 17 00:00:00 2001 From: Markus Bader Date: Fri, 14 Feb 2025 13:56:23 +0100 Subject: [PATCH 1/6] feat: support for usb busylights This commits supports basic states for USB busylights, like blinking on an incoming call, flashing during a call and blinking when a call is on hold or the microphone is muted. Devices support must be implemented individually. This commit supports * Luxafor Flag * kuando Busylight UC Omega --- src/CMakeLists.txt | 10 +++ src/sip/SIPAudioManager.h | 2 + src/sip/SIPCall.cpp | 3 + src/sip/SIPCallManager.cpp | 34 ++++++++ src/sip/SIPCallManager.h | 2 + src/usb/HeadsetDeviceProxy.cpp | 2 +- src/usb/HeadsetDevices.cpp | 36 ++++++--- src/usb/HeadsetDevices.h | 5 +- src/usb/ReportDescriptorEnums.h | 2 + src/usb/ReportDescriptorParser.cpp | 1 + src/usb/busylight/BusylightDeviceManager.cpp | 60 ++++++++++++++ src/usb/busylight/BusylightDeviceManager.h | 36 +++++++++ src/usb/busylight/IBusylightDevice.cpp | 76 ++++++++++++++++++ src/usb/busylight/IBusylightDevice.h | 34 ++++++++ src/usb/busylight/KuandoOmega.cpp | 83 ++++++++++++++++++++ src/usb/busylight/KuandoOmega.h | 23 ++++++ src/usb/busylight/LuxaforFlag.cpp | 26 ++++++ src/usb/busylight/LuxaforFlag.h | 18 +++++ 18 files changed, 439 insertions(+), 14 deletions(-) create mode 100644 src/usb/busylight/BusylightDeviceManager.cpp create mode 100644 src/usb/busylight/BusylightDeviceManager.h create mode 100644 src/usb/busylight/IBusylightDevice.cpp create mode 100644 src/usb/busylight/IBusylightDevice.h create mode 100644 src/usb/busylight/KuandoOmega.cpp create mode 100644 src/usb/busylight/KuandoOmega.h create mode 100644 src/usb/busylight/LuxaforFlag.cpp create mode 100644 src/usb/busylight/LuxaforFlag.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e86ae87..87af6ce 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -344,6 +344,15 @@ qt_add_qml_module(gonnect usb/ReportDescriptorParser.cpp usb/ReportDescriptorStructs.h usb/ReportDescriptorStructs.cpp + + usb/busylight/IBusylightDevice.h + usb/busylight/IBusylightDevice.cpp + usb/busylight/BusylightDeviceManager.h + usb/busylight/BusylightDeviceManager.cpp + usb/busylight/KuandoOmega.h + usb/busylight/KuandoOmega.cpp + usb/busylight/LuxaforFlag.h + usb/busylight/LuxaforFlag.cpp ) set_target_properties(gonnect PROPERTIES @@ -396,6 +405,7 @@ target_include_directories(gonnect contacts ui usb + usb/busylight media ${PJSIP_STATIC_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR} diff --git a/src/sip/SIPAudioManager.h b/src/sip/SIPAudioManager.h index 44476ca..0e9e212 100644 --- a/src/sip/SIPAudioManager.h +++ b/src/sip/SIPAudioManager.h @@ -77,6 +77,8 @@ class SIPAudioManager : public QObject qreal playbackAudioVolume() const; void setPlaybackAudioVolume(qreal volume); + bool isAudioCaptureMuted() const { return m_isAudioCaptureMuted; } + unsigned currentProfile() const { return m_currentAudioProfile; } ~SIPAudioManager() = default; diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 7912fa1..4662d6d 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -19,6 +19,7 @@ #include "Notification.h" #include "NotificationManager.h" #include "AvatarManager.h" +#include "BusylightDeviceManager.h" #include "pjsua-lib/pjsua.h" @@ -156,6 +157,7 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) emit establishedChanged(); m_proxy->setBusyLine(true); + BusylightDeviceManager::instance().switchOn(Qt::GlobalColor::red); m_earlyMediaActive = false; emit earlyMediaActiveChanged(); @@ -186,6 +188,7 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) if (m_isEstablished && SIPCallManager::instance().calls().count() == 1) { m_proxy->setIdle(); + BusylightDeviceManager::instance().switchOff(); } qCInfo(lcSIPCall).nospace() << "Call state disconnected, reason: " << ci.lastReason << ", " diff --git a/src/sip/SIPCallManager.cpp b/src/sip/SIPCallManager.cpp index ba08b13..89fba22 100644 --- a/src/sip/SIPCallManager.cpp +++ b/src/sip/SIPCallManager.cpp @@ -4,6 +4,7 @@ #include "SIPManager.h" #include "SIPCallManager.h" #include "SIPAccountManager.h" +#include "SIPAudioManager.h" #include "ExternalMediaManager.h" #include "Notification.h" #include "NotificationManager.h" @@ -19,6 +20,7 @@ #include "HeadsetDevices.h" #include "HeadsetDeviceProxy.h" #include "AddressBook.h" +#include "BusylightDeviceManager.h" Q_LOGGING_CATEGORY(lcSIPCallManager, "gonnect.sip.callmanager") @@ -28,6 +30,8 @@ SIPCallManager::SIPCallManager(QObject *parent) : QObject(parent) { connect(this, &SIPCallManager::incomingCall, this, &SIPCallManager::onIncomingCall); connect(this, &SIPCallManager::incomingCall, this, &SIPCallManager::updateCallCount); + connect(this, &SIPCallManager::isHoldingChanged, this, &SIPCallManager::updateBusylightState); + connect(&SIPAudioManager::instance(), &SIPAudioManager::isAudioCaptureMutedChanged, this, &SIPCallManager::updateBusylightState); m_dtmfTimer.setInterval(PJSUA_CALL_SEND_DTMF_DURATION_DEFAULT + 10); m_dtmfTimer.callOnTimeout(this, &SIPCallManager::dispatchDtmfBuffer); @@ -171,6 +175,10 @@ void SIPCallManager::onIncomingCall(SIPCall *call) auto ringer = new Ringer(n); ringer->start(); + BusylightDeviceManager::instance().startBlinking(Qt::GlobalColor::green); + connect(n, &QObject::destroyed, this, + []() { BusylightDeviceManager::instance().stopBlinking(); }); + pj::CallOpParam prm; prm.statusCode = PJSIP_SC_RINGING; call->answer(prm); @@ -559,6 +567,16 @@ void SIPCallManager::toggleHold() } } +bool SIPCallManager::isOneCallOnHold() const +{ + for (const auto call : std::as_const(m_calls)) { + if (call->isHolding()) { + return true; + } + } + return false; +} + void SIPCallManager::addCall(SIPCall *call) { m_calls.push_back(call); @@ -844,3 +862,19 @@ void SIPCallManager::updateBlockTimerRunning() m_blockCleanTimer.start(); } } + +void SIPCallManager::updateBusylightState() +{ + auto& busylightDevManager = BusylightDeviceManager::instance(); + + QColor color(Qt::GlobalColor::red); + if (SIPAudioManager::instance().isAudioCaptureMuted()) { + color.setRgb(255, 165, 0); + } + + if (isOneCallOnHold()) { + busylightDevManager.startBlinking(color); + } else { + busylightDevManager.switchOn(color); + } +} diff --git a/src/sip/SIPCallManager.h b/src/sip/SIPCallManager.h index c238e84..4e5eca6 100644 --- a/src/sip/SIPCallManager.h +++ b/src/sip/SIPCallManager.h @@ -78,6 +78,7 @@ class SIPCallManager : public QObject bool isConferenceMode() const { return m_isConferenceMode; } void toggleHold(); + bool isOneCallOnHold() const; Q_INVOKABLE void sendDtmf(const QString &accountId, const int callId, const QString &digit); Q_INVOKABLE void resetMissedCalls(); @@ -122,6 +123,7 @@ private slots: void dispatchDtmfBuffer(); void cleanupBlocks(); void updateBlockTimerRunning(); + void updateBusylightState(); private: SIPCallManager(QObject *parent = nullptr); diff --git a/src/usb/HeadsetDeviceProxy.cpp b/src/usb/HeadsetDeviceProxy.cpp index 3396fe3..8218e28 100644 --- a/src/usb/HeadsetDeviceProxy.cpp +++ b/src/usb/HeadsetDeviceProxy.cpp @@ -24,7 +24,7 @@ HeadsetDeviceProxy::~HeadsetDeviceProxy() bool HeadsetDeviceProxy::refreshDevice() { - auto devs = HeadsetDevices::instance().devices(); + auto devs = HeadsetDevices::instance().headsetDevices(); if (m_device) { m_device = nullptr; diff --git a/src/usb/HeadsetDevices.cpp b/src/usb/HeadsetDevices.cpp index 518243b..9567569 100644 --- a/src/usb/HeadsetDevices.cpp +++ b/src/usb/HeadsetDevices.cpp @@ -9,6 +9,8 @@ #include "HeadsetDevices.h" #include "HeadsetDevice.h" #include "HeadsetDeviceProxy.h" +#include "BusylightDeviceManager.h" +#include "IBusylightDevice.h" Q_LOGGING_CATEGORY(lcHeadsets, "gonnect.usb.headsets") @@ -107,8 +109,7 @@ void HeadsetDevices::shutdown() m_proxy = nullptr; } - qDeleteAll(m_devices); - m_devices.clear(); + clearDevices(); } int HeadsetDevices::hotplugHandler(libusb_context *, libusb_device *device, @@ -144,26 +145,28 @@ void HeadsetDevices::refresh() { QMutexLocker lock(&s_enumerateMutex); QString lastPath; + auto &busylightDeviceManager = BusylightDeviceManager::instance(); - qDeleteAll(m_devices); - m_devices.clear(); + clearDevices(); - struct hid_device_info *devs, *dp; + struct hid_device_info *devs, *deviceInfo; - dp = devs = hid_enumerate(0, 0); + deviceInfo = devs = hid_enumerate(0, 0); - for (; dp; dp = dp->next) { + for (; deviceInfo; deviceInfo = deviceInfo->next) { - QString path = dp->path; + QString path = deviceInfo->path; if (path == lastPath) { continue; } lastPath = path; - HeadsetDevice *hd = parseReportDescriptor(dp); - if (hd) { - m_devices.push_back(hd); + if (!busylightDeviceManager.createBusylightDevice(*deviceInfo)) { + HeadsetDevice *hd = parseReportDescriptor(deviceInfo); + if (hd) { + m_headsetDevices.push_back(hd); + } } } @@ -172,6 +175,14 @@ void HeadsetDevices::refresh() emit devicesChanged(); } +void HeadsetDevices::clearDevices() +{ + qDeleteAll(m_headsetDevices); + m_headsetDevices.clear(); + + BusylightDeviceManager::instance().clearDevices(); +} + HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *deviceInfo) { unsigned char descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; @@ -206,6 +217,8 @@ HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *devi return nullptr; } + qCritical() << "====>" << appCollection.get(); + if (!appCollection) { return nullptr; } @@ -220,6 +233,7 @@ HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *devi UsageId::LED_Mute, UsageId::LED_Ring, UsageId::LED_Hold, + UsageId::Vendor_LEDCommand, }; QHash usageInfos; diff --git a/src/usb/HeadsetDevices.h b/src/usb/HeadsetDevices.h index 50866fc..98fa2c7 100644 --- a/src/usb/HeadsetDevices.h +++ b/src/usb/HeadsetDevices.h @@ -30,7 +30,7 @@ class HeadsetDevices : public QObject int hotplugHandler(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void *user_data); - QList devices() const { return m_devices; } + QList headsetDevices() const { return m_headsetDevices; } HeadsetDeviceProxy *getProxy(); @@ -46,6 +46,7 @@ private slots: explicit HeadsetDevices(QObject *parent = nullptr); void refresh(); + void clearDevices(); HeadsetDevice *parseReportDescriptor(const hid_device_info *deviceInfo); HeadsetDevice *parseReportDescriptor(const hid_device_info *deviceInfo, @@ -54,7 +55,7 @@ private slots: QTimer m_refreshTicker; QTimer m_refreshDebouncer; - QList m_devices; + QList m_headsetDevices; HeadsetDeviceProxy *m_proxy = nullptr; libusb_context *m_ctx = nullptr; diff --git a/src/usb/ReportDescriptorEnums.h b/src/usb/ReportDescriptorEnums.h index 1c388f0..6ec6357 100644 --- a/src/usb/ReportDescriptorEnums.h +++ b/src/usb/ReportDescriptorEnums.h @@ -28,6 +28,8 @@ class ReportDescriptorEnums LED_Microphone = 0x0821, Button_Primary = 0x0901, + + Vendor_LEDCommand = 0xFF01, }; Q_ENUM(UsageId) diff --git a/src/usb/ReportDescriptorParser.cpp b/src/usb/ReportDescriptorParser.cpp index d8c7ad3..b625022 100644 --- a/src/usb/ReportDescriptorParser.cpp +++ b/src/usb/ReportDescriptorParser.cpp @@ -73,6 +73,7 @@ std::shared_ptr ReportDescriptorParser::parse(QByteArray } else if (isGlobal && it->global_tag() == GlobalTag::USAGE_PAGE) { itemStateTable.usagePage = val; } else if (isMain && it->main_tag() == MainTag::COLLECTION) { + if (!collectionLevel && val == 0x01 // Main level application collection && itemStateTable.usagePage == 0x0B // Usage Page Telephony && itemStateTable.usageId == 0x05 // Usage Headset diff --git a/src/usb/busylight/BusylightDeviceManager.cpp b/src/usb/busylight/BusylightDeviceManager.cpp new file mode 100644 index 0000000..15f23ce --- /dev/null +++ b/src/usb/busylight/BusylightDeviceManager.cpp @@ -0,0 +1,60 @@ +#include "BusylightDeviceManager.h" + +#include "LuxaforFlag.h" +#include "KuandoOmega.h" + +BusylightDeviceManager::BusylightDeviceManager(QObject *parent) : QObject{ parent } { } + +bool BusylightDeviceManager::createBusylightDevice(const hid_device_info &deviceInfo) +{ + IBusylightDevice *device = nullptr; + const quint16 vendor = deviceInfo.vendor_id; + const quint16 product = deviceInfo.product_id; + + if (vendor == 0x04D8 && product == 0xF372) { + device = new LuxaforFlag(deviceInfo, this); + + } else if (vendor == 0x27BB && (product == 0x3BCD || product == 0x3BCF)) { + device = new KuandoOmega(deviceInfo, this); + } + + if (device) { + device->open(); + m_devices.append(device); + } + return device; +} + +void BusylightDeviceManager::clearDevices() +{ + qDeleteAll(m_devices); + m_devices.clear(); +} + +void BusylightDeviceManager::switchOn(QColor color) const +{ + for (auto device : std::as_const(m_devices)) { + device->switchOn(color); + } +} + +void BusylightDeviceManager::switchOff() const +{ + for (auto device : std::as_const(m_devices)) { + device->switchOff(); + } +} + +void BusylightDeviceManager::startBlinking(QColor color) const +{ + for (auto device : std::as_const(m_devices)) { + device->startBlinking(color); + } +} + +void BusylightDeviceManager::stopBlinking() const +{ + for (auto device : std::as_const(m_devices)) { + device->stopBlinking(); + } +} diff --git a/src/usb/busylight/BusylightDeviceManager.h b/src/usb/busylight/BusylightDeviceManager.h new file mode 100644 index 0000000..229b641 --- /dev/null +++ b/src/usb/busylight/BusylightDeviceManager.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include "hidapi.h" + +class IBusylightDevice; + +class BusylightDeviceManager : public QObject +{ + Q_OBJECT + +public: + static BusylightDeviceManager &instance() + { + static BusylightDeviceManager *_instance = nullptr; + if (!_instance) { + _instance = new BusylightDeviceManager; + } + return *_instance; + } + + bool createBusylightDevice(const struct hid_device_info &deviceInfo); + void clearDevices(); + + void switchOn(QColor color) const; + void switchOff() const; + + void startBlinking(QColor color) const; + void stopBlinking() const; + +private: + explicit BusylightDeviceManager(QObject *parent = nullptr); + + QList m_devices; +}; diff --git a/src/usb/busylight/IBusylightDevice.cpp b/src/usb/busylight/IBusylightDevice.cpp new file mode 100644 index 0000000..60b3599 --- /dev/null +++ b/src/usb/busylight/IBusylightDevice.cpp @@ -0,0 +1,76 @@ +#include "IBusylightDevice.h" + +#include + +Q_LOGGING_CATEGORY(lcBusylightDevice, "gonnect.usb.busylight.IBusylightDevice") + +IBusylightDevice::IBusylightDevice(const hid_device_info &deviceInfo, QObject *parent) + : QObject{ parent }, m_color(Qt::GlobalColor::red) +{ + m_path = deviceInfo.path; + + m_blinkTimer.setInterval(750); + m_blinkTimer.callOnTimeout(this, [this]() { + m_isOn = !m_isOn; + send(m_isOn); + }); +} + +IBusylightDevice::~IBusylightDevice() +{ + close(); +} + +bool IBusylightDevice::open() +{ + if (m_device) { + return true; + } + + hid_device *device = hid_open_path(m_path.toStdString().c_str()); + if (device) { + m_device = device; + switchOff(); + } else { + qCCritical(lcBusylightDevice) << "Error: cannot open Luxafor Flag busylight device"; + } + + return device; +} + +void IBusylightDevice::close() +{ + if (m_device) { + hid_close(m_device); + m_device = nullptr; + } +} + +void IBusylightDevice::switchOn(QColor color) +{ + m_color = color; + stopBlinking(); + send(true); + m_isOn = true; +} + +void IBusylightDevice::switchOff() +{ + stopBlinking(); + send(false); + m_isOn = false; +} + +void IBusylightDevice::startBlinking(QColor color) +{ + m_color = color; + m_blinkTimer.start(); +} + +void IBusylightDevice::stopBlinking() +{ + if (m_blinkTimer.isActive()) { + m_blinkTimer.stop(); + switchOff(); + } +} diff --git a/src/usb/busylight/IBusylightDevice.h b/src/usb/busylight/IBusylightDevice.h new file mode 100644 index 0000000..b5bae1d --- /dev/null +++ b/src/usb/busylight/IBusylightDevice.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include "hidapi.h" + +class IBusylightDevice : public QObject +{ + Q_OBJECT + +public: + explicit IBusylightDevice(const hid_device_info &deviceInfo, QObject *parent = nullptr); + virtual ~IBusylightDevice(); + + bool open(); + void close(); + + void switchOn(QColor color); + void switchOff(); + void startBlinking(QColor color); + void stopBlinking(); + +protected: + virtual void send(bool on) = 0; + hid_device *m_device = nullptr; + QColor m_color; + +private: + QTimer m_blinkTimer; + QString m_path; + bool m_isOn = false; +}; diff --git a/src/usb/busylight/KuandoOmega.cpp b/src/usb/busylight/KuandoOmega.cpp new file mode 100644 index 0000000..a46988b --- /dev/null +++ b/src/usb/busylight/KuandoOmega.cpp @@ -0,0 +1,83 @@ +#include "KuandoOmega.h" + +#include + +Q_LOGGING_CATEGORY(lcKuandoOmega, "gonnect.usb.busylight.KuandoOmega") + +using namespace std::chrono_literals; + +KuandoOmega::KuandoOmega(const hid_device_info &deviceInfo, QObject *parent) : IBusylightDevice{ deviceInfo, parent } +{ + m_keepAliveTimer.setInterval(10s); + m_keepAliveTimer.callOnTimeout(this, &KuandoOmega::sendKeepAlive); +} + +void KuandoOmega::send(bool on) +{ + if (!on) { + m_keepAliveTimer.stop(); + } + + if (!m_device) { + qCCritical(lcKuandoOmega) << "Error: trying to send data while the USB device is not open"; + return; + } + + if (on) { + m_keepAliveTimer.start(); + } + + unsigned char buf[64] = { 0 }; + + // Padding + buf[59] = 0xFF; + buf[60] = 0xFF; + buf[61] = 0xFF; + + // Jump step 1 + buf[0] = 0x10; // Command: Jump, target: 0 + buf[1] = 0x00; // Repeat + buf[2] = on ? m_color.red() : 0x00; // Red value + buf[3] = on ? m_color.green() : 0x00; // Green value + buf[4] = on ? m_color.blue() : 0x00; // Blue value + + // Checksum + quint16 checksum = 0; + for (int i = 0; i < 62; ++i) { + checksum += buf[i]; + } + + buf[62] = checksum >> 8; + buf[63] = checksum & 0xFF; + + hid_write(m_device, buf, sizeof(buf)); +} + +void KuandoOmega::sendKeepAlive() const +{ + if (!m_device) { + qCCritical(lcKuandoOmega) << "Error: trying to send data while the USB device is not open"; + return; + } + + unsigned char buf[64] = { 0 }; + + // Padding + buf[59] = 0xFF; + buf[60] = 0xFF; + buf[61] = 0xFF; + + // Keppalive step 1 + buf[0] = 0x8F; // Command: Keepalive, timeout: 15 sec + + // Checksum + quint16 checksum = 0; + for (int i = 0; i < 62; ++i) { + checksum += buf[i]; + } + + buf[62] = checksum >> 8; + buf[63] = checksum & 0xFF; + + hid_write(m_device, buf, sizeof(buf)); +} diff --git a/src/usb/busylight/KuandoOmega.h b/src/usb/busylight/KuandoOmega.h new file mode 100644 index 0000000..862e6be --- /dev/null +++ b/src/usb/busylight/KuandoOmega.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "IBusylightDevice.h" +#include "hidapi.h" + +class KuandoOmega : public IBusylightDevice +{ + Q_OBJECT + +public: + explicit KuandoOmega(const hid_device_info &deviceInfo, QObject *parent = nullptr); + + +protected: + virtual void send(bool on) override; + +private: + void sendKeepAlive() const; + + QTimer m_keepAliveTimer; +}; diff --git a/src/usb/busylight/LuxaforFlag.cpp b/src/usb/busylight/LuxaforFlag.cpp new file mode 100644 index 0000000..f754136 --- /dev/null +++ b/src/usb/busylight/LuxaforFlag.cpp @@ -0,0 +1,26 @@ +#include "LuxaforFlag.h" + +#include + +Q_LOGGING_CATEGORY(lcLuxaforFlag, "gonnect.usb.busylight.LuxaforFlag") + +void LuxaforFlag::send(bool on) +{ + if (!m_device) { + qCCritical(lcLuxaforFlag) << "Error: trying to send data while the USB device is not open"; + return; + } + + unsigned char buf[8]; + + buf[0] = 0x01; // Command: Color + buf[1] = 0xFF; // LED selection: All + buf[2] = on ? m_color.red() : 0x00; // Red value + buf[3] = on ? m_color.green() : 0x00; // Green value + buf[4] = on ? m_color.blue() : 0x00; // Blue value + buf[5] = 0x00; // Pad + buf[6] = 0x00; // Pad + buf[7] = 0x00; // Pad + + hid_write(m_device, buf, sizeof(buf)); +} diff --git a/src/usb/busylight/LuxaforFlag.h b/src/usb/busylight/LuxaforFlag.h new file mode 100644 index 0000000..cb94ebc --- /dev/null +++ b/src/usb/busylight/LuxaforFlag.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "IBusylightDevice.h" +#include "hidapi.h" + +class LuxaforFlag : public IBusylightDevice +{ + Q_OBJECT + +public: + LuxaforFlag(const hid_device_info &deviceInfo, QObject *parent = nullptr) + : IBusylightDevice{ deviceInfo, parent } {}; + +protected: + virtual void send(bool on) override; +}; From 56edb2c9d21b24928e1f61ce2b6f5a56aee0cb31 Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Mon, 17 Feb 2025 17:18:28 +0100 Subject: [PATCH 2/6] fix: make mime-type based calls work again --- src/StateManager.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/StateManager.cpp b/src/StateManager.cpp index 9047bfa..3b20a21 100644 --- a/src/StateManager.cpp +++ b/src/StateManager.cpp @@ -198,7 +198,15 @@ void StateManager::ActivateAction(const QString &action_name, const QVariantList } } -void StateManager::Open(const QStringList &, const QVariantMap &) +void StateManager::Open(const QStringList &args, const QVariantMap &) { - qobject_cast(Application::instance())->rootWindow()->show(); + if (args.length()) { + QVariantList vArgs; + for (auto& arg : std::as_const(args)) { + vArgs.push_back(arg); + } + ActivateAction("invoke", vArgs, {}); + } else { + qobject_cast(Application::instance())->rootWindow()->show(); + } } From 70da1ef1a0130d8e478911171cbad19ed4d2aba1 Mon Sep 17 00:00:00 2001 From: Markus Bader Date: Fri, 14 Feb 2025 13:56:23 +0100 Subject: [PATCH 3/6] feat: support for usb busylights This commits supports basic states for USB busylights, like blinking on an incoming call, flashing during a call and blinking when a call is on hold or the microphone is muted. Devices support must be implemented individually. This commit supports * Luxafor Flag * kuando Busylight UC Omega --- src/CMakeLists.txt | 10 +++ src/sip/SIPAudioManager.h | 2 + src/sip/SIPCall.cpp | 3 + src/sip/SIPCallManager.cpp | 34 ++++++++ src/sip/SIPCallManager.h | 2 + src/usb/HeadsetDeviceProxy.cpp | 2 +- src/usb/HeadsetDevices.cpp | 36 ++++++--- src/usb/HeadsetDevices.h | 5 +- src/usb/ReportDescriptorEnums.h | 2 + src/usb/ReportDescriptorParser.cpp | 1 + src/usb/busylight/BusylightDeviceManager.cpp | 60 ++++++++++++++ src/usb/busylight/BusylightDeviceManager.h | 36 +++++++++ src/usb/busylight/IBusylightDevice.cpp | 76 ++++++++++++++++++ src/usb/busylight/IBusylightDevice.h | 34 ++++++++ src/usb/busylight/KuandoOmega.cpp | 83 ++++++++++++++++++++ src/usb/busylight/KuandoOmega.h | 23 ++++++ src/usb/busylight/LuxaforFlag.cpp | 26 ++++++ src/usb/busylight/LuxaforFlag.h | 18 +++++ 18 files changed, 439 insertions(+), 14 deletions(-) create mode 100644 src/usb/busylight/BusylightDeviceManager.cpp create mode 100644 src/usb/busylight/BusylightDeviceManager.h create mode 100644 src/usb/busylight/IBusylightDevice.cpp create mode 100644 src/usb/busylight/IBusylightDevice.h create mode 100644 src/usb/busylight/KuandoOmega.cpp create mode 100644 src/usb/busylight/KuandoOmega.h create mode 100644 src/usb/busylight/LuxaforFlag.cpp create mode 100644 src/usb/busylight/LuxaforFlag.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e86ae87..87af6ce 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -344,6 +344,15 @@ qt_add_qml_module(gonnect usb/ReportDescriptorParser.cpp usb/ReportDescriptorStructs.h usb/ReportDescriptorStructs.cpp + + usb/busylight/IBusylightDevice.h + usb/busylight/IBusylightDevice.cpp + usb/busylight/BusylightDeviceManager.h + usb/busylight/BusylightDeviceManager.cpp + usb/busylight/KuandoOmega.h + usb/busylight/KuandoOmega.cpp + usb/busylight/LuxaforFlag.h + usb/busylight/LuxaforFlag.cpp ) set_target_properties(gonnect PROPERTIES @@ -396,6 +405,7 @@ target_include_directories(gonnect contacts ui usb + usb/busylight media ${PJSIP_STATIC_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR} diff --git a/src/sip/SIPAudioManager.h b/src/sip/SIPAudioManager.h index 44476ca..0e9e212 100644 --- a/src/sip/SIPAudioManager.h +++ b/src/sip/SIPAudioManager.h @@ -77,6 +77,8 @@ class SIPAudioManager : public QObject qreal playbackAudioVolume() const; void setPlaybackAudioVolume(qreal volume); + bool isAudioCaptureMuted() const { return m_isAudioCaptureMuted; } + unsigned currentProfile() const { return m_currentAudioProfile; } ~SIPAudioManager() = default; diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 7912fa1..4662d6d 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -19,6 +19,7 @@ #include "Notification.h" #include "NotificationManager.h" #include "AvatarManager.h" +#include "BusylightDeviceManager.h" #include "pjsua-lib/pjsua.h" @@ -156,6 +157,7 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) emit establishedChanged(); m_proxy->setBusyLine(true); + BusylightDeviceManager::instance().switchOn(Qt::GlobalColor::red); m_earlyMediaActive = false; emit earlyMediaActiveChanged(); @@ -186,6 +188,7 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) if (m_isEstablished && SIPCallManager::instance().calls().count() == 1) { m_proxy->setIdle(); + BusylightDeviceManager::instance().switchOff(); } qCInfo(lcSIPCall).nospace() << "Call state disconnected, reason: " << ci.lastReason << ", " diff --git a/src/sip/SIPCallManager.cpp b/src/sip/SIPCallManager.cpp index ba08b13..89fba22 100644 --- a/src/sip/SIPCallManager.cpp +++ b/src/sip/SIPCallManager.cpp @@ -4,6 +4,7 @@ #include "SIPManager.h" #include "SIPCallManager.h" #include "SIPAccountManager.h" +#include "SIPAudioManager.h" #include "ExternalMediaManager.h" #include "Notification.h" #include "NotificationManager.h" @@ -19,6 +20,7 @@ #include "HeadsetDevices.h" #include "HeadsetDeviceProxy.h" #include "AddressBook.h" +#include "BusylightDeviceManager.h" Q_LOGGING_CATEGORY(lcSIPCallManager, "gonnect.sip.callmanager") @@ -28,6 +30,8 @@ SIPCallManager::SIPCallManager(QObject *parent) : QObject(parent) { connect(this, &SIPCallManager::incomingCall, this, &SIPCallManager::onIncomingCall); connect(this, &SIPCallManager::incomingCall, this, &SIPCallManager::updateCallCount); + connect(this, &SIPCallManager::isHoldingChanged, this, &SIPCallManager::updateBusylightState); + connect(&SIPAudioManager::instance(), &SIPAudioManager::isAudioCaptureMutedChanged, this, &SIPCallManager::updateBusylightState); m_dtmfTimer.setInterval(PJSUA_CALL_SEND_DTMF_DURATION_DEFAULT + 10); m_dtmfTimer.callOnTimeout(this, &SIPCallManager::dispatchDtmfBuffer); @@ -171,6 +175,10 @@ void SIPCallManager::onIncomingCall(SIPCall *call) auto ringer = new Ringer(n); ringer->start(); + BusylightDeviceManager::instance().startBlinking(Qt::GlobalColor::green); + connect(n, &QObject::destroyed, this, + []() { BusylightDeviceManager::instance().stopBlinking(); }); + pj::CallOpParam prm; prm.statusCode = PJSIP_SC_RINGING; call->answer(prm); @@ -559,6 +567,16 @@ void SIPCallManager::toggleHold() } } +bool SIPCallManager::isOneCallOnHold() const +{ + for (const auto call : std::as_const(m_calls)) { + if (call->isHolding()) { + return true; + } + } + return false; +} + void SIPCallManager::addCall(SIPCall *call) { m_calls.push_back(call); @@ -844,3 +862,19 @@ void SIPCallManager::updateBlockTimerRunning() m_blockCleanTimer.start(); } } + +void SIPCallManager::updateBusylightState() +{ + auto& busylightDevManager = BusylightDeviceManager::instance(); + + QColor color(Qt::GlobalColor::red); + if (SIPAudioManager::instance().isAudioCaptureMuted()) { + color.setRgb(255, 165, 0); + } + + if (isOneCallOnHold()) { + busylightDevManager.startBlinking(color); + } else { + busylightDevManager.switchOn(color); + } +} diff --git a/src/sip/SIPCallManager.h b/src/sip/SIPCallManager.h index c238e84..4e5eca6 100644 --- a/src/sip/SIPCallManager.h +++ b/src/sip/SIPCallManager.h @@ -78,6 +78,7 @@ class SIPCallManager : public QObject bool isConferenceMode() const { return m_isConferenceMode; } void toggleHold(); + bool isOneCallOnHold() const; Q_INVOKABLE void sendDtmf(const QString &accountId, const int callId, const QString &digit); Q_INVOKABLE void resetMissedCalls(); @@ -122,6 +123,7 @@ private slots: void dispatchDtmfBuffer(); void cleanupBlocks(); void updateBlockTimerRunning(); + void updateBusylightState(); private: SIPCallManager(QObject *parent = nullptr); diff --git a/src/usb/HeadsetDeviceProxy.cpp b/src/usb/HeadsetDeviceProxy.cpp index 3396fe3..8218e28 100644 --- a/src/usb/HeadsetDeviceProxy.cpp +++ b/src/usb/HeadsetDeviceProxy.cpp @@ -24,7 +24,7 @@ HeadsetDeviceProxy::~HeadsetDeviceProxy() bool HeadsetDeviceProxy::refreshDevice() { - auto devs = HeadsetDevices::instance().devices(); + auto devs = HeadsetDevices::instance().headsetDevices(); if (m_device) { m_device = nullptr; diff --git a/src/usb/HeadsetDevices.cpp b/src/usb/HeadsetDevices.cpp index 518243b..9567569 100644 --- a/src/usb/HeadsetDevices.cpp +++ b/src/usb/HeadsetDevices.cpp @@ -9,6 +9,8 @@ #include "HeadsetDevices.h" #include "HeadsetDevice.h" #include "HeadsetDeviceProxy.h" +#include "BusylightDeviceManager.h" +#include "IBusylightDevice.h" Q_LOGGING_CATEGORY(lcHeadsets, "gonnect.usb.headsets") @@ -107,8 +109,7 @@ void HeadsetDevices::shutdown() m_proxy = nullptr; } - qDeleteAll(m_devices); - m_devices.clear(); + clearDevices(); } int HeadsetDevices::hotplugHandler(libusb_context *, libusb_device *device, @@ -144,26 +145,28 @@ void HeadsetDevices::refresh() { QMutexLocker lock(&s_enumerateMutex); QString lastPath; + auto &busylightDeviceManager = BusylightDeviceManager::instance(); - qDeleteAll(m_devices); - m_devices.clear(); + clearDevices(); - struct hid_device_info *devs, *dp; + struct hid_device_info *devs, *deviceInfo; - dp = devs = hid_enumerate(0, 0); + deviceInfo = devs = hid_enumerate(0, 0); - for (; dp; dp = dp->next) { + for (; deviceInfo; deviceInfo = deviceInfo->next) { - QString path = dp->path; + QString path = deviceInfo->path; if (path == lastPath) { continue; } lastPath = path; - HeadsetDevice *hd = parseReportDescriptor(dp); - if (hd) { - m_devices.push_back(hd); + if (!busylightDeviceManager.createBusylightDevice(*deviceInfo)) { + HeadsetDevice *hd = parseReportDescriptor(deviceInfo); + if (hd) { + m_headsetDevices.push_back(hd); + } } } @@ -172,6 +175,14 @@ void HeadsetDevices::refresh() emit devicesChanged(); } +void HeadsetDevices::clearDevices() +{ + qDeleteAll(m_headsetDevices); + m_headsetDevices.clear(); + + BusylightDeviceManager::instance().clearDevices(); +} + HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *deviceInfo) { unsigned char descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; @@ -206,6 +217,8 @@ HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *devi return nullptr; } + qCritical() << "====>" << appCollection.get(); + if (!appCollection) { return nullptr; } @@ -220,6 +233,7 @@ HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *devi UsageId::LED_Mute, UsageId::LED_Ring, UsageId::LED_Hold, + UsageId::Vendor_LEDCommand, }; QHash usageInfos; diff --git a/src/usb/HeadsetDevices.h b/src/usb/HeadsetDevices.h index 50866fc..98fa2c7 100644 --- a/src/usb/HeadsetDevices.h +++ b/src/usb/HeadsetDevices.h @@ -30,7 +30,7 @@ class HeadsetDevices : public QObject int hotplugHandler(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void *user_data); - QList devices() const { return m_devices; } + QList headsetDevices() const { return m_headsetDevices; } HeadsetDeviceProxy *getProxy(); @@ -46,6 +46,7 @@ private slots: explicit HeadsetDevices(QObject *parent = nullptr); void refresh(); + void clearDevices(); HeadsetDevice *parseReportDescriptor(const hid_device_info *deviceInfo); HeadsetDevice *parseReportDescriptor(const hid_device_info *deviceInfo, @@ -54,7 +55,7 @@ private slots: QTimer m_refreshTicker; QTimer m_refreshDebouncer; - QList m_devices; + QList m_headsetDevices; HeadsetDeviceProxy *m_proxy = nullptr; libusb_context *m_ctx = nullptr; diff --git a/src/usb/ReportDescriptorEnums.h b/src/usb/ReportDescriptorEnums.h index 1c388f0..6ec6357 100644 --- a/src/usb/ReportDescriptorEnums.h +++ b/src/usb/ReportDescriptorEnums.h @@ -28,6 +28,8 @@ class ReportDescriptorEnums LED_Microphone = 0x0821, Button_Primary = 0x0901, + + Vendor_LEDCommand = 0xFF01, }; Q_ENUM(UsageId) diff --git a/src/usb/ReportDescriptorParser.cpp b/src/usb/ReportDescriptorParser.cpp index d8c7ad3..b625022 100644 --- a/src/usb/ReportDescriptorParser.cpp +++ b/src/usb/ReportDescriptorParser.cpp @@ -73,6 +73,7 @@ std::shared_ptr ReportDescriptorParser::parse(QByteArray } else if (isGlobal && it->global_tag() == GlobalTag::USAGE_PAGE) { itemStateTable.usagePage = val; } else if (isMain && it->main_tag() == MainTag::COLLECTION) { + if (!collectionLevel && val == 0x01 // Main level application collection && itemStateTable.usagePage == 0x0B // Usage Page Telephony && itemStateTable.usageId == 0x05 // Usage Headset diff --git a/src/usb/busylight/BusylightDeviceManager.cpp b/src/usb/busylight/BusylightDeviceManager.cpp new file mode 100644 index 0000000..15f23ce --- /dev/null +++ b/src/usb/busylight/BusylightDeviceManager.cpp @@ -0,0 +1,60 @@ +#include "BusylightDeviceManager.h" + +#include "LuxaforFlag.h" +#include "KuandoOmega.h" + +BusylightDeviceManager::BusylightDeviceManager(QObject *parent) : QObject{ parent } { } + +bool BusylightDeviceManager::createBusylightDevice(const hid_device_info &deviceInfo) +{ + IBusylightDevice *device = nullptr; + const quint16 vendor = deviceInfo.vendor_id; + const quint16 product = deviceInfo.product_id; + + if (vendor == 0x04D8 && product == 0xF372) { + device = new LuxaforFlag(deviceInfo, this); + + } else if (vendor == 0x27BB && (product == 0x3BCD || product == 0x3BCF)) { + device = new KuandoOmega(deviceInfo, this); + } + + if (device) { + device->open(); + m_devices.append(device); + } + return device; +} + +void BusylightDeviceManager::clearDevices() +{ + qDeleteAll(m_devices); + m_devices.clear(); +} + +void BusylightDeviceManager::switchOn(QColor color) const +{ + for (auto device : std::as_const(m_devices)) { + device->switchOn(color); + } +} + +void BusylightDeviceManager::switchOff() const +{ + for (auto device : std::as_const(m_devices)) { + device->switchOff(); + } +} + +void BusylightDeviceManager::startBlinking(QColor color) const +{ + for (auto device : std::as_const(m_devices)) { + device->startBlinking(color); + } +} + +void BusylightDeviceManager::stopBlinking() const +{ + for (auto device : std::as_const(m_devices)) { + device->stopBlinking(); + } +} diff --git a/src/usb/busylight/BusylightDeviceManager.h b/src/usb/busylight/BusylightDeviceManager.h new file mode 100644 index 0000000..229b641 --- /dev/null +++ b/src/usb/busylight/BusylightDeviceManager.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include "hidapi.h" + +class IBusylightDevice; + +class BusylightDeviceManager : public QObject +{ + Q_OBJECT + +public: + static BusylightDeviceManager &instance() + { + static BusylightDeviceManager *_instance = nullptr; + if (!_instance) { + _instance = new BusylightDeviceManager; + } + return *_instance; + } + + bool createBusylightDevice(const struct hid_device_info &deviceInfo); + void clearDevices(); + + void switchOn(QColor color) const; + void switchOff() const; + + void startBlinking(QColor color) const; + void stopBlinking() const; + +private: + explicit BusylightDeviceManager(QObject *parent = nullptr); + + QList m_devices; +}; diff --git a/src/usb/busylight/IBusylightDevice.cpp b/src/usb/busylight/IBusylightDevice.cpp new file mode 100644 index 0000000..60b3599 --- /dev/null +++ b/src/usb/busylight/IBusylightDevice.cpp @@ -0,0 +1,76 @@ +#include "IBusylightDevice.h" + +#include + +Q_LOGGING_CATEGORY(lcBusylightDevice, "gonnect.usb.busylight.IBusylightDevice") + +IBusylightDevice::IBusylightDevice(const hid_device_info &deviceInfo, QObject *parent) + : QObject{ parent }, m_color(Qt::GlobalColor::red) +{ + m_path = deviceInfo.path; + + m_blinkTimer.setInterval(750); + m_blinkTimer.callOnTimeout(this, [this]() { + m_isOn = !m_isOn; + send(m_isOn); + }); +} + +IBusylightDevice::~IBusylightDevice() +{ + close(); +} + +bool IBusylightDevice::open() +{ + if (m_device) { + return true; + } + + hid_device *device = hid_open_path(m_path.toStdString().c_str()); + if (device) { + m_device = device; + switchOff(); + } else { + qCCritical(lcBusylightDevice) << "Error: cannot open Luxafor Flag busylight device"; + } + + return device; +} + +void IBusylightDevice::close() +{ + if (m_device) { + hid_close(m_device); + m_device = nullptr; + } +} + +void IBusylightDevice::switchOn(QColor color) +{ + m_color = color; + stopBlinking(); + send(true); + m_isOn = true; +} + +void IBusylightDevice::switchOff() +{ + stopBlinking(); + send(false); + m_isOn = false; +} + +void IBusylightDevice::startBlinking(QColor color) +{ + m_color = color; + m_blinkTimer.start(); +} + +void IBusylightDevice::stopBlinking() +{ + if (m_blinkTimer.isActive()) { + m_blinkTimer.stop(); + switchOff(); + } +} diff --git a/src/usb/busylight/IBusylightDevice.h b/src/usb/busylight/IBusylightDevice.h new file mode 100644 index 0000000..b5bae1d --- /dev/null +++ b/src/usb/busylight/IBusylightDevice.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include "hidapi.h" + +class IBusylightDevice : public QObject +{ + Q_OBJECT + +public: + explicit IBusylightDevice(const hid_device_info &deviceInfo, QObject *parent = nullptr); + virtual ~IBusylightDevice(); + + bool open(); + void close(); + + void switchOn(QColor color); + void switchOff(); + void startBlinking(QColor color); + void stopBlinking(); + +protected: + virtual void send(bool on) = 0; + hid_device *m_device = nullptr; + QColor m_color; + +private: + QTimer m_blinkTimer; + QString m_path; + bool m_isOn = false; +}; diff --git a/src/usb/busylight/KuandoOmega.cpp b/src/usb/busylight/KuandoOmega.cpp new file mode 100644 index 0000000..a46988b --- /dev/null +++ b/src/usb/busylight/KuandoOmega.cpp @@ -0,0 +1,83 @@ +#include "KuandoOmega.h" + +#include + +Q_LOGGING_CATEGORY(lcKuandoOmega, "gonnect.usb.busylight.KuandoOmega") + +using namespace std::chrono_literals; + +KuandoOmega::KuandoOmega(const hid_device_info &deviceInfo, QObject *parent) : IBusylightDevice{ deviceInfo, parent } +{ + m_keepAliveTimer.setInterval(10s); + m_keepAliveTimer.callOnTimeout(this, &KuandoOmega::sendKeepAlive); +} + +void KuandoOmega::send(bool on) +{ + if (!on) { + m_keepAliveTimer.stop(); + } + + if (!m_device) { + qCCritical(lcKuandoOmega) << "Error: trying to send data while the USB device is not open"; + return; + } + + if (on) { + m_keepAliveTimer.start(); + } + + unsigned char buf[64] = { 0 }; + + // Padding + buf[59] = 0xFF; + buf[60] = 0xFF; + buf[61] = 0xFF; + + // Jump step 1 + buf[0] = 0x10; // Command: Jump, target: 0 + buf[1] = 0x00; // Repeat + buf[2] = on ? m_color.red() : 0x00; // Red value + buf[3] = on ? m_color.green() : 0x00; // Green value + buf[4] = on ? m_color.blue() : 0x00; // Blue value + + // Checksum + quint16 checksum = 0; + for (int i = 0; i < 62; ++i) { + checksum += buf[i]; + } + + buf[62] = checksum >> 8; + buf[63] = checksum & 0xFF; + + hid_write(m_device, buf, sizeof(buf)); +} + +void KuandoOmega::sendKeepAlive() const +{ + if (!m_device) { + qCCritical(lcKuandoOmega) << "Error: trying to send data while the USB device is not open"; + return; + } + + unsigned char buf[64] = { 0 }; + + // Padding + buf[59] = 0xFF; + buf[60] = 0xFF; + buf[61] = 0xFF; + + // Keppalive step 1 + buf[0] = 0x8F; // Command: Keepalive, timeout: 15 sec + + // Checksum + quint16 checksum = 0; + for (int i = 0; i < 62; ++i) { + checksum += buf[i]; + } + + buf[62] = checksum >> 8; + buf[63] = checksum & 0xFF; + + hid_write(m_device, buf, sizeof(buf)); +} diff --git a/src/usb/busylight/KuandoOmega.h b/src/usb/busylight/KuandoOmega.h new file mode 100644 index 0000000..862e6be --- /dev/null +++ b/src/usb/busylight/KuandoOmega.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "IBusylightDevice.h" +#include "hidapi.h" + +class KuandoOmega : public IBusylightDevice +{ + Q_OBJECT + +public: + explicit KuandoOmega(const hid_device_info &deviceInfo, QObject *parent = nullptr); + + +protected: + virtual void send(bool on) override; + +private: + void sendKeepAlive() const; + + QTimer m_keepAliveTimer; +}; diff --git a/src/usb/busylight/LuxaforFlag.cpp b/src/usb/busylight/LuxaforFlag.cpp new file mode 100644 index 0000000..f754136 --- /dev/null +++ b/src/usb/busylight/LuxaforFlag.cpp @@ -0,0 +1,26 @@ +#include "LuxaforFlag.h" + +#include + +Q_LOGGING_CATEGORY(lcLuxaforFlag, "gonnect.usb.busylight.LuxaforFlag") + +void LuxaforFlag::send(bool on) +{ + if (!m_device) { + qCCritical(lcLuxaforFlag) << "Error: trying to send data while the USB device is not open"; + return; + } + + unsigned char buf[8]; + + buf[0] = 0x01; // Command: Color + buf[1] = 0xFF; // LED selection: All + buf[2] = on ? m_color.red() : 0x00; // Red value + buf[3] = on ? m_color.green() : 0x00; // Green value + buf[4] = on ? m_color.blue() : 0x00; // Blue value + buf[5] = 0x00; // Pad + buf[6] = 0x00; // Pad + buf[7] = 0x00; // Pad + + hid_write(m_device, buf, sizeof(buf)); +} diff --git a/src/usb/busylight/LuxaforFlag.h b/src/usb/busylight/LuxaforFlag.h new file mode 100644 index 0000000..cb94ebc --- /dev/null +++ b/src/usb/busylight/LuxaforFlag.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "IBusylightDevice.h" +#include "hidapi.h" + +class LuxaforFlag : public IBusylightDevice +{ + Q_OBJECT + +public: + LuxaforFlag(const hid_device_info &deviceInfo, QObject *parent = nullptr) + : IBusylightDevice{ deviceInfo, parent } {}; + +protected: + virtual void send(bool on) override; +}; From fed10c51b2a68644cceb7f0886cdd66fa8bb86b2 Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Mon, 17 Feb 2025 17:18:28 +0100 Subject: [PATCH 4/6] fix: make mime-type based calls work again --- src/StateManager.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/StateManager.cpp b/src/StateManager.cpp index 9047bfa..3b20a21 100644 --- a/src/StateManager.cpp +++ b/src/StateManager.cpp @@ -198,7 +198,15 @@ void StateManager::ActivateAction(const QString &action_name, const QVariantList } } -void StateManager::Open(const QStringList &, const QVariantMap &) +void StateManager::Open(const QStringList &args, const QVariantMap &) { - qobject_cast(Application::instance())->rootWindow()->show(); + if (args.length()) { + QVariantList vArgs; + for (auto& arg : std::as_const(args)) { + vArgs.push_back(arg); + } + ActivateAction("invoke", vArgs, {}); + } else { + qobject_cast(Application::instance())->rootWindow()->show(); + } } From e46c420d9dc1a2e210bd828f0063a548458ecd88 Mon Sep 17 00:00:00 2001 From: Michael Neuendorf Date: Wed, 19 Feb 2025 10:01:57 +0000 Subject: [PATCH 5/6] docs: update docs for busylights --- README.md | 7 +++++++ resources/flatpak/de.gonicus.gonnect.metainfo.xml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index 4e3c2fb..f3291ef 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Here's a short feature list: * Configurable Togglers (i.e. for call queues, CFNL, etc.) * Upgrade call to Jitsi Meet session * Support for various hardware headsets (i.e. Yealink, Jabra) + * Support for various busylights * Custom audio device profiles or managed by your system * [mpris](https://specifications.freedesktop.org/mpris-spec/latest/) for stopping other audio sources on incoming calls @@ -72,6 +73,12 @@ make this list more complete by opening an [issue](https://github.com/gonicus/go | Jabra | EVOLVE LINK | AEMS | | Yealink | WH62 | AEMSLOR | +# Busylights known to be supported + +| Manufacturer | Model | +| ------------ | ------------------ | +| Luxafor | Flag | +| kuando | Busylight UC Omega | # Installing _GOnnect_ diff --git a/resources/flatpak/de.gonicus.gonnect.metainfo.xml b/resources/flatpak/de.gonicus.gonnect.metainfo.xml index 6f9ebb2..c289861 100644 --- a/resources/flatpak/de.gonicus.gonnect.metainfo.xml +++ b/resources/flatpak/de.gonicus.gonnect.metainfo.xml @@ -58,6 +58,8 @@
  • Anruf in Jitsi Meet weiterführen
  • Support for various hardware headsets (i.e. Yealink, Jabra)
  • Unterstützung für diverse Headsets (z.B. Yealink, Jabra)
  • +
  • Support for various busylights
  • +
  • Unterstützung für diverse Busylights
  • Custom audio device profiles or managed by your system
  • Benutzerdefinierte oder vom System verwaltete Audiogeräteprofile
  • Stopping other audio sources on incoming calls
  • From b833c5b82c1f00626c412c61063cb9b2b1d19942 Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Thu, 20 Feb 2025 11:48:47 +0100 Subject: [PATCH 6/6] chore: rename headset devices class This is still not ideal, but as long we have not factured out some "lights" and "headset" libs: lets keep it this way --- src/Application.cpp | 6 ++-- src/CMakeLists.txt | 4 +-- src/sip/Ringer.cpp | 6 ++-- src/sip/SIPAudioManager.cpp | 10 +++---- src/sip/SIPCall.cpp | 4 +-- src/sip/SIPCallManager.cpp | 6 ++-- src/ui/ViewHelper.cpp | 4 +-- src/usb/HeadsetDeviceProxy.cpp | 8 +++--- .../{HeadsetDevices.cpp => USBDevices.cpp} | 28 +++++++++---------- src/usb/{HeadsetDevices.h => USBDevices.h} | 14 +++++----- 10 files changed, 45 insertions(+), 45 deletions(-) rename src/usb/{HeadsetDevices.cpp => USBDevices.cpp} (89%) rename src/usb/{HeadsetDevices.h => USBDevices.h} (79%) diff --git a/src/Application.cpp b/src/Application.cpp index 3a2bac3..d42431f 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -7,7 +7,7 @@ #include "SIPCallManager.h" #include "SystemTrayMenu.h" #include "AddressBookManager.h" -#include "HeadsetDevices.h" +#include "USBDevices.h" #include #include @@ -46,7 +46,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) setQuitOnLastWindowClosed(false); - HeadsetDevices::instance().initialize(); + USBDevices::instance().initialize(); StateManager::instance().setParent(this); SearchProvider::instance().setParent(this); @@ -185,7 +185,7 @@ Application::~Application() void Application::shutdown() { NotificationManager::instance().shutdown(); - HeadsetDevices::instance().shutdown(); + USBDevices::instance().shutdown(); if (m_initialized) { SIPManager::instance().shutdown(); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 87af6ce..bf2f0d9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -336,8 +336,8 @@ qt_add_qml_module(gonnect usb/HeadsetDevice.h usb/HeadsetDeviceProxy.cpp usb/HeadsetDeviceProxy.h - usb/HeadsetDevices.cpp - usb/HeadsetDevices.h + usb/USBDevices.cpp + usb/USBDevices.h usb/ReportDescriptorEnums.h usb/ReportDescriptorEnums.cpp usb/ReportDescriptorParser.h diff --git a/src/sip/Ringer.cpp b/src/sip/Ringer.cpp index 97a5f3a..8bcd2dd 100644 --- a/src/sip/Ringer.cpp +++ b/src/sip/Ringer.cpp @@ -6,7 +6,7 @@ #include "SIPAudioManager.h" #include "Ringer.h" #include "AppSettings.h" -#include "HeadsetDevices.h" +#include "USBDevices.h" #include "HeadsetDeviceProxy.h" #include "SystemTrayMenu.h" @@ -32,7 +32,7 @@ void Ringer::start(qreal customVolume) // Prefer headset ringer? if (settings.value(QString("audio%1/preferExternalRinger").arg(currentProfile), false) .toBool()) { - auto proxy = HeadsetDevices::instance().getProxy(); + auto proxy = USBDevices::instance().getHeadsetDeviceProxy(); if (proxy->available()) { proxy->setRing(true); return; @@ -116,7 +116,7 @@ void Ringer::playbackStateChanged(QMediaPlayer::PlaybackState state) void Ringer::stop() { - auto proxy = HeadsetDevices::instance().getProxy(); + auto proxy = USBDevices::instance().getHeadsetDeviceProxy(); proxy->setRing(false); SystemTrayMenu::instance().setRinging(false); diff --git a/src/sip/SIPAudioManager.cpp b/src/sip/SIPAudioManager.cpp index 71c9caf..96210ee 100644 --- a/src/sip/SIPAudioManager.cpp +++ b/src/sip/SIPAudioManager.cpp @@ -10,7 +10,7 @@ #include "media/AudioPort.h" #include "HeadsetDeviceProxy.h" -#include "HeadsetDevices.h" +#include "USBDevices.h" Q_LOGGING_CATEGORY(lcSIPAudioManager, "gonnect.sip.audio") @@ -44,14 +44,14 @@ SIPAudioManager::SIPAudioManager(QObject *parent) : QObject(parent) } // Headset device mute handling - auto &hds = HeadsetDevices::instance(); - auto dev = hds.getProxy(); + auto &hds = USBDevices::instance(); + auto dev = hds.getHeadsetDeviceProxy(); dev->setMute(m_isAudioCaptureMuted); }); // Headset device mute handling - auto &hds = HeadsetDevices::instance(); - auto dev = hds.getProxy(); + auto &hds = USBDevices::instance(); + auto dev = hds.getHeadsetDeviceProxy(); connect(dev, &HeadsetDeviceProxy::mute, this, [this, dev]() { setProperty("isAudioCaptureMuted", dev->getMute()); }); } diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 4662d6d..21b1c23 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -15,7 +15,7 @@ #include "media/Sniffer.h" #include "ViewHelper.h" #include "HeadsetDeviceProxy.h" -#include "HeadsetDevices.h" +#include "USBDevices.h" #include "Notification.h" #include "NotificationManager.h" #include "AvatarManager.h" @@ -44,7 +44,7 @@ SIPCall::SIPCall(SIPAccount *account, int callId, const QString &contactId, bool emit SIPCallManager::instance().meetingRequested(accountId, callId); }); - m_proxy = HeadsetDevices::instance().getProxy(); + m_proxy = USBDevices::instance().getHeadsetDeviceProxy(); // Initialize basic call info // This can only be done here for incoming calls, because an outgoing call has its infos not set diff --git a/src/sip/SIPCallManager.cpp b/src/sip/SIPCallManager.cpp index 89fba22..109cb6b 100644 --- a/src/sip/SIPCallManager.cpp +++ b/src/sip/SIPCallManager.cpp @@ -17,7 +17,7 @@ #include "StateManager.h" #include "ViewHelper.h" #include "AvatarManager.h" -#include "HeadsetDevices.h" +#include "USBDevices.h" #include "HeadsetDeviceProxy.h" #include "AddressBook.h" #include "BusylightDeviceManager.h" @@ -45,8 +45,8 @@ SIPCallManager::SIPCallManager(QObject *parent) : QObject(parent) }); // React on Headset events - auto &hds = HeadsetDevices::instance(); - auto dev = hds.getProxy(); + auto &hds = USBDevices::instance(); + auto dev = hds.getHeadsetDeviceProxy(); connect(dev, &HeadsetDeviceProxy::hookSwitch, this, [dev, this]() { // Were're busy with one call -> end call if (!dev->getHookSwitch() && m_calls.count() == 1) { diff --git a/src/ui/ViewHelper.cpp b/src/ui/ViewHelper.cpp index aa8be64..9338652 100644 --- a/src/ui/ViewHelper.cpp +++ b/src/ui/ViewHelper.cpp @@ -6,7 +6,7 @@ #include "NumberStats.h" #include "Ringer.h" #include "SIPCallManager.h" -#include "HeadsetDevices.h" +#include "USBDevices.h" #include "HeadsetDeviceProxy.h" #include "SecretPortal.h" #include "SystemTrayMenu.h" @@ -179,7 +179,7 @@ void ViewHelper::testPlayRingTone(qreal volume) HeadsetDeviceProxy *ViewHelper::headsetDeviceProxy() const { - return HeadsetDevices::instance().getProxy(); + return USBDevices::instance().getHeadsetDeviceProxy(); } QString ViewHelper::encryptSecret(const QString &secret) const diff --git a/src/usb/HeadsetDeviceProxy.cpp b/src/usb/HeadsetDeviceProxy.cpp index 8218e28..ef11f34 100644 --- a/src/usb/HeadsetDeviceProxy.cpp +++ b/src/usb/HeadsetDeviceProxy.cpp @@ -1,6 +1,6 @@ #include #include -#include "HeadsetDevices.h" +#include "USBDevices.h" #include "HeadsetDevice.h" #include "HeadsetDeviceProxy.h" @@ -10,8 +10,8 @@ using namespace std::chrono_literals; HeadsetDeviceProxy::HeadsetDeviceProxy(QObject *parent) : IHeadsetDevice(parent) { - auto &devs = HeadsetDevices::instance(); - connect(&devs, &HeadsetDevices::devicesChanged, this, &HeadsetDeviceProxy::refreshDevice); + auto &devs = USBDevices::instance(); + connect(&devs, &USBDevices::devicesChanged, this, &HeadsetDeviceProxy::refreshDevice); refreshDevice(); open(); @@ -24,7 +24,7 @@ HeadsetDeviceProxy::~HeadsetDeviceProxy() bool HeadsetDeviceProxy::refreshDevice() { - auto devs = HeadsetDevices::instance().headsetDevices(); + auto devs = USBDevices::instance().headsetDevices(); if (m_device) { m_device = nullptr; diff --git a/src/usb/HeadsetDevices.cpp b/src/usb/USBDevices.cpp similarity index 89% rename from src/usb/HeadsetDevices.cpp rename to src/usb/USBDevices.cpp index 9567569..96e9179 100644 --- a/src/usb/HeadsetDevices.cpp +++ b/src/usb/USBDevices.cpp @@ -6,7 +6,7 @@ #include "ReportDescriptorParser.h" #include "ReportDescriptorEnums.h" #include "ReportDescriptorStructs.h" -#include "HeadsetDevices.h" +#include "USBDevices.h" #include "HeadsetDevice.h" #include "HeadsetDeviceProxy.h" #include "BusylightDeviceManager.h" @@ -22,22 +22,22 @@ static libusb_hotplug_callback_handle s_hotplugHandle; static int LIBUSB_CALL hotplugCallback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void *user_data) { - return HeadsetDevices::instance().hotplugHandler(ctx, device, event, user_data); + return USBDevices::instance().hotplugHandler(ctx, device, event, user_data); } -HeadsetDevices::HeadsetDevices(QObject *parent) : QObject(parent) +USBDevices::USBDevices(QObject *parent) : QObject(parent) { QThread *t = new QThread(this); m_refreshDebouncer.setSingleShot(true); m_refreshDebouncer.setInterval(2s); - connect(&m_refreshDebouncer, &QTimer::timeout, this, &HeadsetDevices::refresh); + connect(&m_refreshDebouncer, &QTimer::timeout, this, &USBDevices::refresh); m_refreshTicker.setInterval(250ms); m_refreshTicker.moveToThread(t); m_refreshTicker.connect(t, SIGNAL(started()), SLOT(start())); m_refreshTicker.connect(t, SIGNAL(finished()), SLOT(stop())); - connect(&m_refreshTicker, &QTimer::timeout, this, &HeadsetDevices::processUsbEvents); + connect(&m_refreshTicker, &QTimer::timeout, this, &USBDevices::processUsbEvents); int res = libusb_init(&m_ctx); if (res < 0) { @@ -69,7 +69,7 @@ HeadsetDevices::HeadsetDevices(QObject *parent) : QObject(parent) } } -void HeadsetDevices::processUsbEvents() +void USBDevices::processUsbEvents() { if (m_hotplugSupported) { timeval t = { 0, 0 }; @@ -77,7 +77,7 @@ void HeadsetDevices::processUsbEvents() } } -void HeadsetDevices::initialize() +void USBDevices::initialize() { if (!m_initialized) { refresh(); @@ -85,7 +85,7 @@ void HeadsetDevices::initialize() } } -void HeadsetDevices::shutdown() +void USBDevices::shutdown() { if (!m_ctx) { return; @@ -112,7 +112,7 @@ void HeadsetDevices::shutdown() clearDevices(); } -int HeadsetDevices::hotplugHandler(libusb_context *, libusb_device *device, +int USBDevices::hotplugHandler(libusb_context *, libusb_device *device, libusb_hotplug_event event, void *) { quint8 bus = libusb_get_bus_number(device); @@ -141,7 +141,7 @@ int HeadsetDevices::hotplugHandler(libusb_context *, libusb_device *device, return 0; }; -void HeadsetDevices::refresh() +void USBDevices::refresh() { QMutexLocker lock(&s_enumerateMutex); QString lastPath; @@ -175,7 +175,7 @@ void HeadsetDevices::refresh() emit devicesChanged(); } -void HeadsetDevices::clearDevices() +void USBDevices::clearDevices() { qDeleteAll(m_headsetDevices); m_headsetDevices.clear(); @@ -183,7 +183,7 @@ void HeadsetDevices::clearDevices() BusylightDeviceManager::instance().clearDevices(); } -HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *deviceInfo) +HeadsetDevice *USBDevices::parseReportDescriptor(const hid_device_info *deviceInfo) { unsigned char descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; hid_device *device = hid_open_path(deviceInfo->path); @@ -199,7 +199,7 @@ HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *devi return hd; } -HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *deviceInfo, +HeadsetDevice *USBDevices::parseReportDescriptor(const hid_device_info *deviceInfo, unsigned char *descriptor, int len) { const auto byteArr = QByteArray::fromRawData(reinterpret_cast(descriptor), len); @@ -259,7 +259,7 @@ HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *devi return hd; } -HeadsetDeviceProxy *HeadsetDevices::getProxy() +HeadsetDeviceProxy *USBDevices::getHeadsetDeviceProxy() { if (!m_proxy) { m_proxy = new HeadsetDeviceProxy(this); diff --git a/src/usb/HeadsetDevices.h b/src/usb/USBDevices.h similarity index 79% rename from src/usb/HeadsetDevices.h rename to src/usb/USBDevices.h index 98fa2c7..fb77cde 100644 --- a/src/usb/HeadsetDevices.h +++ b/src/usb/USBDevices.h @@ -7,17 +7,17 @@ class HeadsetDevice; class HeadsetDeviceProxy; -class HeadsetDevices : public QObject +class USBDevices : public QObject { Q_OBJECT public: - static HeadsetDevices &instance() + static USBDevices &instance() { - static HeadsetDevices *_instance = nullptr; + static USBDevices *_instance = nullptr; if (_instance == nullptr) { - _instance = new HeadsetDevices(); + _instance = new USBDevices(); _instance->initialize(); } @@ -32,9 +32,9 @@ class HeadsetDevices : public QObject QList headsetDevices() const { return m_headsetDevices; } - HeadsetDeviceProxy *getProxy(); + HeadsetDeviceProxy *getHeadsetDeviceProxy(); - ~HeadsetDevices() { shutdown(); } + ~USBDevices() { shutdown(); } signals: void devicesChanged(); @@ -43,7 +43,7 @@ private slots: void processUsbEvents(); private: - explicit HeadsetDevices(QObject *parent = nullptr); + explicit USBDevices(QObject *parent = nullptr); void refresh(); void clearDevices();