| 421 | } |
| 422 | |
| 423 | bool HidEncoderManager::open(uint16_t vid, uint16_t pid) |
| 424 | { |
| 425 | if (m_device) close(); |
| 426 | |
| 427 | // Block if more than one RC-28-compatible device is connected — interleaved |
| 428 | // events from two encoders would produce unpredictable tuning behaviour. |
| 429 | // macOS reports each HID usage collection as a separate enumeration entry, so |
| 430 | // counting raw entries would over-count a single device. We group by a stable |
| 431 | // physical-device key: the USB serial number when present, otherwise the |
| 432 | // hidapi path. The RC-28 exposes no serial, but all usage-collection entries |
| 433 | // of one physical interface share the same path, while two separate devices |
| 434 | // get distinct paths — so the path fallback distinguishes them correctly. |
| 435 | if (isRC28CompatibleId(vid, pid)) { |
| 436 | bool multiplePhysical = false; |
| 437 | QString firstKey; |
| 438 | bool firstSeen = false; |
| 439 | if (auto* info = hid_enumerate(vid, pid)) { |
| 440 | for (auto* cur = info; cur; cur = cur->next) { |
| 441 | const QString key = (cur->serial_number && cur->serial_number[0] != L'\0') |
| 442 | ? QStringLiteral("sn:") + QString::fromWCharArray(cur->serial_number) |
| 443 | : QStringLiteral("path:") + QString::fromLatin1(cur->path ? cur->path : ""); |
| 444 | if (!firstSeen) { |
| 445 | firstKey = key; |
| 446 | firstSeen = true; |
| 447 | } else if (key != firstKey) { |
| 448 | multiplePhysical = true; |
| 449 | break; |
| 450 | } |
| 451 | } |
| 452 | hid_free_enumeration(info); |
| 453 | } |
| 454 | if (multiplePhysical) { |
| 455 | const auto* devices = HidDeviceParser::supportedDevices(); |
| 456 | int count = HidDeviceParser::supportedDeviceCount(); |
| 457 | QString name; |
| 458 | for (int i = 0; i < count; ++i) { |
| 459 | if (devices[i].vid == vid && devices[i].pid == pid) { |
| 460 | name = devices[i].name; |
| 461 | break; |
| 462 | } |
| 463 | } |
| 464 | // open() is retried every hotplug tick while two devices remain, so |
| 465 | // warn + emit only on the transition into the blocked state. |
| 466 | if (!m_multipleDetected.load(std::memory_order_acquire)) { |
| 467 | // Write the name before the release store so the main thread |
| 468 | // sees a valid QString when it reads m_multipleDetected as true. |
| 469 | m_blockedDeviceName = name; |
| 470 | m_multipleDetected.store(true, std::memory_order_release); |
| 471 | qCWarning(lcDevices) << "HidEncoderManager: multiple" << name |
| 472 | << "devices detected — blocking until only one is present"; |
| 473 | emit multipleDevicesDetected(name); |
| 474 | } |
| 475 | return false; |
| 476 | } |
| 477 | m_blockedDeviceName.clear(); |
| 478 | m_multipleDetected.store(false, std::memory_order_release); |
| 479 | } |
| 480 | |