From 3026a75e93166bf4626c282ae4853ebce9c00123 Mon Sep 17 00:00:00 2001 From: Lenucksi Date: Sun, 18 Oct 2026 13:26:40 +0200 Subject: [PATCH] Add Jabra Link 380 (paired headset) support The Link 380 dongle (PID 0x24c7) speaks the same GN Protocol (GNP) as the Link 390, but exposes the GNP vendor collection (0xFF00) on usage 0x01 instead of 0x05. getMetadata() reports the paired headset's name/serial. hid_utility.cpp: try usage_page/usage matching on all platforms, not just Windows. On Linux/macOS the Link 380's three collections all share interface 3, so the previous interface_number-only fallback opened the wrong (Telephony) collection and hid_open_path failed. Verified on hardware: battery (76%), sidetone read-back, lights, voice prompts, inactive time, volume limiter, and metadata (Jabra Evolve2 65). --- README.md | 1 + lib/device_registry.cpp | 2 ++ lib/devices/jabra_link_380.hpp | 42 ++++++++++++++++++++++++++++++++++ lib/hid_utility.cpp | 14 ++++-------- tests/test_device_registry.cpp | 23 +++++++++++++++++++ 5 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 lib/devices/jabra_link_380.hpp diff --git a/README.md b/README.md index 26f30719..0dccd284 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ sudo udevadm control --reload-rules && sudo udevadm trigger | Audeze Maxwell 2 | All | x | x | | | x | x | x | | x | | | | | | | | x | | | Lenovo Wireless VoIP Headset | All | x | x | | | x | | x | x | x | | | x | | x | | | | | | Plantronics Voyager 8200 UC (BT600) | L/W | x | x | | x | | | x | | | | | | | x | | | | | +| Jabra Link 380 (paired headset) | L/M | x | x | | x | x | | x | | | | | | | x | | | | x | | Jabra Link 390 (paired headset) | L/M | x | x | | x | x | | x | | | | | | | x | | | | x | | Jabra Evolve2 65 Flex (USB) | L/M | x | x | | x | x | | x | | | | | | | x | | | | x | | Sony INZONE Buds | All | | x | | | | | | | | | | | | | | | | | diff --git a/lib/device_registry.cpp b/lib/device_registry.cpp index 73124607..ea55e37f 100644 --- a/lib/device_registry.cpp +++ b/lib/device_registry.cpp @@ -57,6 +57,7 @@ // Jabra devices #include "devices/jabra_evolve2_65_flex.hpp" +#include "devices/jabra_link_380.hpp" #include "devices/jabra_link_390.hpp" // Sony devices @@ -163,6 +164,7 @@ void DeviceRegistry::initialize() registerDevice(std::make_unique()); // Jabra devices + registerDevice(std::make_unique()); registerDevice(std::make_unique()); registerDevice(std::make_unique()); diff --git a/lib/devices/jabra_link_380.hpp b/lib/devices/jabra_link_380.hpp new file mode 100644 index 00000000..01541f93 --- /dev/null +++ b/lib/devices/jabra_link_380.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "protocols/jabra_gnp_protocol.hpp" +#include +#include +#include + +using namespace std::string_view_literals; + +namespace headsetcontrol { + +/** + * @brief Jabra Link 380 USB dongle, controlling the Jabra headset paired to it + * + * Same GN Protocol (GNP) as the Link 390; the headset is reached at GNP address + * 0x04 and the dongle's vendor collection (usage page 0xFF00) carries report 5 + * on usage 0x01. Verified on Linux with a Link 380 (0x24c7, fw 1.16) paired + * with a Jabra Evolve2 65. + */ +class JabraLink380 : public protocols::JabraGNPDevice<0x04, 0x01> { +public: + static constexpr std::array PRODUCT_IDS { 0x24C7 }; + + constexpr std::vector getProductIds() const override + { + return { PRODUCT_IDS.begin(), PRODUCT_IDS.end() }; + } + + constexpr std::string_view getDeviceName() const override + { + return "Jabra Link 380 (paired headset)"sv; + } + + // Windows is left out because it is untested, not because the protocol needs + // anything different there: getCapabilityDetail() reports usagepage/usageid. + constexpr uint8_t getSupportedPlatforms() const override + { + return PLATFORM_LINUX | PLATFORM_MACOS; + } +}; + +} // namespace headsetcontrol diff --git a/lib/hid_utility.cpp b/lib/hid_utility.cpp index 4922d53e..d94c5794 100644 --- a/lib/hid_utility.cpp +++ b/lib/hid_utility.cpp @@ -25,11 +25,10 @@ std::optional get_hid_path(uint16_t vid, uint16_t pid, int iid, uin iid = 0; #endif - // usageid is more specific to interface id, so we try it first - // It is a good idea, to do it on all platforms, however googling shows - // that older versions of hidapi have a bug where the value is not correctly - // set on non-Windows. -#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) + // usageid is more specific to interface id, so we try it first. + // Older hidapi versions on non-Windows did not populate usage_page/usage, so + // we only trust the match when both values are actually present; otherwise + // we fall through to the interface_number fallback below. if (usageid && usagepageid) // ignore when one of them 0 { struct hid_device_info* cur_dev = devs; @@ -42,11 +41,6 @@ std::optional get_hid_path(uint16_t vid, uint16_t pid, int iid, uin cur_dev = cur_dev->next; } } -#else - // ignore unused parameter warning - (void)(usageid); - (void)(usagepageid); -#endif if (!result) // only when we didn't yet find something { diff --git a/tests/test_device_registry.cpp b/tests/test_device_registry.cpp index a3466f72..a36724bc 100644 --- a/tests/test_device_registry.cpp +++ b/tests/test_device_registry.cpp @@ -241,6 +241,28 @@ void testLookupJabraLink390() std::cout << " PASS" << std::endl; } +void testLookupJabraLink380() +{ + std::cout << " Testing lookup of Jabra Link 380 (0x24c7)..." << std::endl; + + auto& registry = DeviceRegistry::instance(); + registry.initialize(); + + auto* device = registry.getDevice(0x0b0e, 0x24c7); + ASSERT_NOT_NULL(device, "Jabra Link 380 should be found"); + ASSERT_EQ("Jabra Link 380 (paired headset)", std::string(device->getDeviceName()), "Device name should match"); + ASSERT_TRUE((device->getCapabilities() & B(CAP_SIDETONE)) != 0, "Link 380 should expose sidetone capability"); + ASSERT_TRUE((device->getCapabilities() & B(CAP_BATTERY_STATUS)) != 0, "Link 380 should expose battery capability"); + ASSERT_TRUE((device->getCapabilities() & B(CAP_LIGHTS)) != 0, "Link 380 should expose lights capability"); + ASSERT_TRUE((device->getCapabilities() & B(CAP_INACTIVE_TIME)) != 0, "Link 380 should expose inactive time capability"); + + auto* link390 = registry.getDevice(0x0b0e, 0x2e57); + ASSERT_NOT_NULL(link390, "Jabra Link 390 should be found"); + ASSERT_EQ(device->getCapabilities(), link390->getCapabilities(), "Link 380 and Link 390 share the GNP feature set"); + + std::cout << " PASS" << std::endl; +} + void testLookupPlantronicsBT600() { std::cout << " Testing lookup of Plantronics BT600 (0x02ee)..." << std::endl; @@ -538,6 +560,7 @@ void runAllDeviceRegistryTests() runTest("Lookup Logitech PRO X2 LIGHTSPEED", testLookupLogitechProX2Lightspeed); runTest("Lookup Plantronics BT600", testLookupPlantronicsBT600); runTest("Lookup Jabra Link 390", testLookupJabraLink390); + runTest("Lookup Jabra Link 380", testLookupJabraLink380); runTest("Lookup Non-Existent", testLookupNonExistentDevice); runTest("Lookup Wrong Product ID", testLookupWithWrongProductId); runTest("Lookup Wrong Vendor ID", testLookupWithWrongVendorId);