diff --git a/README.md b/README.md
index 25d8ee3..6ebc155 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
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 e86ae87..bf2f0d9 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -336,14 +336,23 @@ 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
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/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();
+ }
}
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/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..21b1c23 100644
--- a/src/sip/SIPCall.cpp
+++ b/src/sip/SIPCall.cpp
@@ -15,10 +15,11 @@
#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"
+#include "BusylightDeviceManager.h"
#include "pjsua-lib/pjsua.h"
@@ -43,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
@@ -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..109cb6b 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"
@@ -16,9 +17,10 @@
#include "StateManager.h"
#include "ViewHelper.h"
#include "AvatarManager.h"
-#include "HeadsetDevices.h"
+#include "USBDevices.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);
@@ -41,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) {
@@ -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/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 3396fe3..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().devices();
+ auto devs = USBDevices::instance().headsetDevices();
if (m_device) {
m_device = 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/HeadsetDevices.cpp b/src/usb/USBDevices.cpp
similarity index 79%
rename from src/usb/HeadsetDevices.cpp
rename to src/usb/USBDevices.cpp
index 518243b..96e9179 100644
--- a/src/usb/HeadsetDevices.cpp
+++ b/src/usb/USBDevices.cpp
@@ -6,9 +6,11 @@
#include "ReportDescriptorParser.h"
#include "ReportDescriptorEnums.h"
#include "ReportDescriptorStructs.h"
-#include "HeadsetDevices.h"
+#include "USBDevices.h"
#include "HeadsetDevice.h"
#include "HeadsetDeviceProxy.h"
+#include "BusylightDeviceManager.h"
+#include "IBusylightDevice.h"
Q_LOGGING_CATEGORY(lcHeadsets, "gonnect.usb.headsets")
@@ -20,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) {
@@ -67,7 +69,7 @@ HeadsetDevices::HeadsetDevices(QObject *parent) : QObject(parent)
}
}
-void HeadsetDevices::processUsbEvents()
+void USBDevices::processUsbEvents()
{
if (m_hotplugSupported) {
timeval t = { 0, 0 };
@@ -75,7 +77,7 @@ void HeadsetDevices::processUsbEvents()
}
}
-void HeadsetDevices::initialize()
+void USBDevices::initialize()
{
if (!m_initialized) {
refresh();
@@ -83,7 +85,7 @@ void HeadsetDevices::initialize()
}
}
-void HeadsetDevices::shutdown()
+void USBDevices::shutdown()
{
if (!m_ctx) {
return;
@@ -107,11 +109,10 @@ void HeadsetDevices::shutdown()
m_proxy = nullptr;
}
- qDeleteAll(m_devices);
- m_devices.clear();
+ 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);
@@ -140,30 +141,32 @@ int HeadsetDevices::hotplugHandler(libusb_context *, libusb_device *device,
return 0;
};
-void HeadsetDevices::refresh()
+void USBDevices::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,7 +175,15 @@ void HeadsetDevices::refresh()
emit devicesChanged();
}
-HeadsetDevice *HeadsetDevices::parseReportDescriptor(const hid_device_info *deviceInfo)
+void USBDevices::clearDevices()
+{
+ qDeleteAll(m_headsetDevices);
+ m_headsetDevices.clear();
+
+ BusylightDeviceManager::instance().clearDevices();
+}
+
+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);
@@ -188,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);
@@ -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;
@@ -245,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 69%
rename from src/usb/HeadsetDevices.h
rename to src/usb/USBDevices.h
index 50866fc..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();
}
@@ -30,11 +30,11 @@ 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();
+ HeadsetDeviceProxy *getHeadsetDeviceProxy();
- ~HeadsetDevices() { shutdown(); }
+ ~USBDevices() { shutdown(); }
signals:
void devicesChanged();
@@ -43,9 +43,10 @@ private slots:
void processUsbEvents();
private:
- explicit HeadsetDevices(QObject *parent = nullptr);
+ explicit USBDevices(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/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;
+};