From 14401da74d0b8d6f75e57fe3502db9ac2a132c21 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 13 Nov 2020 09:41:27 +0100 Subject: [PATCH 1/8] Move StringCollator into mixxx namespace --- src/library/basetrackcache.h | 2 +- src/util/db/dbconnection.cpp | 4 ++-- src/util/db/dbconnection.h | 2 +- src/util/string.h | 12 +++++++----- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/library/basetrackcache.h b/src/library/basetrackcache.h index 64b346312ed..95a57b689d6 100644 --- a/src/library/basetrackcache.h +++ b/src/library/basetrackcache.h @@ -122,7 +122,7 @@ class BaseTrackCache : public QObject { const std::unique_ptr m_pQueryParser; - const StringCollator m_collator; + const mixxx::StringCollator m_collator; QStringList m_searchColumns; QVector m_searchColumnIndices; diff --git a/src/util/db/dbconnection.cpp b/src/util/db/dbconnection.cpp index 3dfdcca7559..19dd9f1636a 100644 --- a/src/util/db/dbconnection.cpp +++ b/src/util/db/dbconnection.cpp @@ -180,7 +180,7 @@ const QChar kSqlLikeEscapeDefault = '\0'; int sqliteStringCompareUTF16(void* pArg, int len1, const void* data1, int len2, const void* data2) { - StringCollator* pCollator = static_cast(pArg); + const auto* pCollator = static_cast(pArg); // Construct a QString without copy QString string1 = QString::fromRawData(static_cast(data1), len1 / sizeof(QChar)); @@ -233,7 +233,7 @@ void sqliteLike(sqlite3_context *context, #endif // __SQLITE3__ -bool initDatabase(QSqlDatabase database, StringCollator* pCollator) { +bool initDatabase(QSqlDatabase database, mixxx::StringCollator* pCollator) { DEBUG_ASSERT(database.isOpen()); #ifdef __SQLITE3__ QVariant v = database.driver()->handle(); diff --git a/src/util/db/dbconnection.h b/src/util/db/dbconnection.h index 76b6af89287..dfe91dbf860 100644 --- a/src/util/db/dbconnection.h +++ b/src/util/db/dbconnection.h @@ -64,7 +64,7 @@ class DbConnection final { DbConnection(const DbConnection&&) = delete; QSqlDatabase m_sqlDatabase; - StringCollator m_collator; + mixxx::StringCollator m_collator; }; } // namespace mixxx diff --git a/src/util/string.h b/src/util/string.h index 264b006c8b1..08ed29f46f2 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -1,16 +1,17 @@ -#ifndef MIXXX_STRING_H -#define MIXXX_STRING_H +#pragma once -#include #include +#include #include #include +namespace mixxx { + // The default comparison of strings for sorting. class StringCollator { public: explicit StringCollator(QLocale locale = QLocale()) - : m_collator(std::move(locale)) { + : m_collator(std::move(locale)) { m_collator.setCaseSensitivity(Qt::CaseInsensitive); } @@ -26,4 +27,5 @@ class StringCollator { QCollator m_collator; }; -#endif // MIXXX_STRING_H + +} // namespace mixxx From 3c58f6bdedf81879b81a79010b9129753a03afa4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 13 Nov 2020 22:48:44 +0100 Subject: [PATCH 2/8] Extract HID DeviceInfo from HidController/HidEnumerator --- CMakeLists.txt | 1 + build/features.py | 1 + src/controllers/hid/hidcontroller.cpp | 222 ++++++-------------------- src/controllers/hid/hidcontroller.h | 46 +----- src/controllers/hid/hiddevice.cpp | 194 ++++++++++++++++++++++ src/controllers/hid/hiddevice.h | 100 ++++++++++++ src/controllers/hid/hidenumerator.cpp | 65 +++----- src/util/string.h | 26 +++ 8 files changed, 393 insertions(+), 262 deletions(-) create mode 100644 src/controllers/hid/hiddevice.cpp create mode 100644 src/controllers/hid/hiddevice.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9adb398b990..70c59622f14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2381,6 +2381,7 @@ cmake_dependent_option(HIDAPI_STATIC "Link HIDAPI library statically" OFF "HIDAP if(HID) target_sources(mixxx-lib PRIVATE src/controllers/hid/hidcontroller.cpp + src/controllers/hid/hiddevice.cpp src/controllers/hid/hidenumerator.cpp src/controllers/hid/hidcontrollerpreset.cpp src/controllers/hid/hidcontrollerpresetfilehandler.cpp diff --git a/build/features.py b/build/features.py index 31d38dd187a..605da723ab5 100644 --- a/build/features.py +++ b/build/features.py @@ -101,6 +101,7 @@ def configure(self, build, conf): def sources(self, build): sources = ['src/controllers/hid/hidcontroller.cpp', 'src/controllers/hid/hidcontrollerpreset.cpp', + 'src/controllers/hid/hiddevice.cpp', 'src/controllers/hid/hidenumerator.cpp', 'src/controllers/hid/hidcontrollerpresetfilehandler.cpp'] diff --git a/src/controllers/hid/hidcontroller.cpp b/src/controllers/hid/hidcontroller.cpp index bc995688f91..d050dd90ee9 100644 --- a/src/controllers/hid/hidcontroller.cpp +++ b/src/controllers/hid/hidcontroller.cpp @@ -1,86 +1,24 @@ -/** - * @file hidcontroller.cpp - * @author Sean M. Pappalardo spappalardo@mixxx.org - * @date Sun May 1 2011 - * @brief HID controller backend - * - */ - -#include -#include - -#include "util/path.h" // for PATH_MAX on Windows #include "controllers/hid/hidcontroller.h" -#include "controllers/defs_controllers.h" -#include "util/trace.h" + #include "controllers/controllerdebug.h" +#include "controllers/defs_controllers.h" +#include "controllers/hid/hidcontrollerpresetfilehandler.h" +#include "util/string.h" #include "util/time.h" - -ControllerJSProxy* HidController::jsProxy() { - return new HidControllerJSProxy(this); -} +#include "util/trace.h" namespace { constexpr int kReportIdSize = 1; constexpr int kMaxHidErrorMessageSize = 512; } // namespace -HidController::HidController(const hid_device_info& deviceInfo) - : Controller(), +HidController::HidController( + mixxx::hid::DeviceInfo&& deviceInfo) + : m_deviceInfo(std::move(deviceInfo)), m_pHidDevice(nullptr), m_iPollingBufferIndex(0) { - // Copy required variables from deviceInfo, which will be freed after - // this class is initialized by caller. - hid_vendor_id = deviceInfo.vendor_id; - hid_product_id = deviceInfo.product_id; - hid_interface_number = deviceInfo.interface_number; - if (hid_interface_number == -1) { - // OS/X and windows don't use interface numbers, but usage_page/usage - hid_usage_page = deviceInfo.usage_page; - hid_usage = deviceInfo.usage; - } else { - // Linux hidapi does not set value for usage_page or usage and uses - // interface number to identify subdevices - hid_usage_page = 0; - hid_usage = 0; - } - - // Don't trust path to be null terminated. - hid_path = new char[PATH_MAX + 1]; - strncpy(hid_path, deviceInfo.path, PATH_MAX); - hid_path[PATH_MAX] = 0; - - hid_serial_raw = NULL; - if (deviceInfo.serial_number != NULL) { - size_t serial_max_length = 512; - hid_serial_raw = new wchar_t[serial_max_length+1]; - wcsncpy(hid_serial_raw, deviceInfo.serial_number, serial_max_length); - hid_serial_raw[serial_max_length] = 0; - } - - hid_serial = safeDecodeWideString(deviceInfo.serial_number, 512); - hid_manufacturer = safeDecodeWideString(deviceInfo.manufacturer_string, 512); - hid_product = safeDecodeWideString(deviceInfo.product_string, 512); - - guessDeviceCategory(); - - // Set the Unique Identifier to the serial_number - m_sUID = hid_serial; - - //Note: We include the last 4 digits of the serial number and the - // interface number to allow the user (and Mixxx!) to keep track of - // which is which - if (hid_interface_number < 0) { - setDeviceName( - QString("%1 %2").arg(hid_product) - .arg(hid_serial.right(4))); - } else { - setDeviceName( - QString("%1 %2_%3").arg(hid_product) - .arg(hid_serial.right(4)) - .arg(QString::number(hid_interface_number))); - m_sUID.append(QString::number(hid_interface_number)); - } + setDeviceCategory(mixxx::hid::DeviceCategory::guessFromDeviceInfo(m_deviceInfo)); + setDeviceName(m_deviceInfo.formatName()); // All HID devices are full-duplex setInputDevice(true); @@ -91,8 +29,6 @@ HidController::~HidController() { if (isOpen()) { close(); } - delete [] hid_path; - delete [] hid_serial_raw; } QString HidController::presetExtension() { @@ -114,76 +50,11 @@ void HidController::visit(const HidControllerPreset* preset) { bool HidController::matchPreset(const PresetInfo& preset) { const QList& products = preset.getProducts(); for (const auto& product : products) { - if (matchProductInfo(product)) + if (m_deviceInfo.matchProductInfo(product)) { return true; - } - return false; -} - -bool HidController::matchProductInfo(const ProductInfo& product) { - int value; - bool ok; - // Product and vendor match is always required - value = product.vendor_id.toInt(&ok,16); - if (!ok || hid_vendor_id!=value) return false; - value = product.product_id.toInt(&ok,16); - if (!ok || hid_product_id!=value) return false; - - // Optionally check against interface_number / usage_page && usage - if (hid_interface_number!=-1) { - value = product.interface_number.toInt(&ok,16); - if (!ok || hid_interface_number!=value) return false; - } else { - value = product.usage_page.toInt(&ok,16); - if (!ok || hid_usage_page!=value) return false; - - value = product.usage.toInt(&ok,16); - if (!ok || hid_usage!=value) return false; - } - // Match found - return true; -} - -void HidController::guessDeviceCategory() { - // This should be done somehow else, I know. But at least we get started with - // the idea of mapping this information - QString info; - if (hid_interface_number==-1) { - if (hid_usage_page==0x1) { - switch (hid_usage) { - case 0x2: info = tr("Generic HID Mouse"); break; - case 0x4: info = tr("Generic HID Joystick"); break; - case 0x5: info = tr("Generic HID Gamepad"); break; - case 0x6: info = tr("Generic HID Keyboard"); break; - case 0x8: info = tr("Generic HID Multiaxis Controller"); break; - default: info = tr("Unknown HID Desktop Device") + - QStringLiteral(" 0x") + QString::number(hid_usage_page, 16) + - QStringLiteral("/0x") + QString::number(hid_usage, 16); - break; - } - } else if (hid_vendor_id==0x5ac) { - // Apple laptop special HID devices - if (hid_product_id==0x8242) { - info = tr("HID Infrared Control"); - } else { - info = tr("Unknown Apple HID Device") + - QStringLiteral(" 0x") + QString::number(hid_usage_page, 16) + - QStringLiteral("/0x") + QString::number(hid_usage, 16); - } - } else { - // Fill in the usage page and usage fields for debugging info - info = tr("HID Unknown Device") + - QStringLiteral(" 0x") + QString::number(hid_usage_page, 16) + - QStringLiteral("/0x") + QString::number(hid_usage, 16); } - } else { - // Guess linux device types somehow as well. Or maybe just fill in the - // interface number? - info = tr("HID Interface Number") + - QStringLiteral(" 0x") + QString::number(hid_usage_page, 16) + - QStringLiteral("/0x") + QString::number(hid_usage, 16); } - setDeviceCategory(info); + return false; } int HidController::open() { @@ -193,29 +64,36 @@ int HidController::open() { } // Open device by path - controllerDebug("Opening HID device" << getName() << "by HID path" << hid_path); + controllerDebug("Opening HID device" << getName() << "by HID path" + << m_deviceInfo.pathRaw()); - m_pHidDevice = hid_open_path(hid_path); + m_pHidDevice = hid_open_path(m_deviceInfo.pathRaw()); // If that fails, try to open device with vendor/product/serial # - if (m_pHidDevice == NULL) { + if (!m_pHidDevice) { controllerDebug("Failed. Trying to open with make, model & serial no:" - << hid_vendor_id << hid_product_id << hid_serial); - m_pHidDevice = hid_open(hid_vendor_id, hid_product_id, hid_serial_raw); + << m_deviceInfo.vendorId() << m_deviceInfo.productId() + << m_deviceInfo.serialNumber()); + m_pHidDevice = hid_open( + m_deviceInfo.vendorId(), + m_deviceInfo.productId(), + m_deviceInfo.serialNumberRaw()); } // If it does fail, try without serial number WARNING: This will only open // one of multiple identical devices - if (m_pHidDevice == NULL) { + if (!m_pHidDevice) { qWarning() << "Unable to open specific HID device" << getName() << "Trying now with just make and model." << "(This may only open the first of multiple identical devices.)"; - m_pHidDevice = hid_open(hid_vendor_id, hid_product_id, NULL); + m_pHidDevice = hid_open(m_deviceInfo.vendorId(), + m_deviceInfo.productId(), + nullptr); } // If that fails, we give up! - if (m_pHidDevice == NULL) { - qWarning() << "Unable to open HID device" << getName(); + if (!m_pHidDevice) { + qWarning() << "Unable to open HID device" << getName(); return -1; } @@ -325,15 +203,19 @@ void HidController::sendBytesReport(QByteArray data, unsigned int reportID) { if (result == -1) { if (ControllerDebug::enabled()) { qWarning() << "Unable to send data to" << getName() - << "serial #" << hid_serial << ":" - << safeDecodeWideString(hid_error(m_pHidDevice), kMaxHidErrorMessageSize); + << "serial #" << m_deviceInfo.serialNumber() << ":" + << mixxx::convertWCStringToQString( + hid_error(m_pHidDevice), + kMaxHidErrorMessageSize); } else { qWarning() << "Unable to send data to" << getName() << ":" - << safeDecodeWideString(hid_error(m_pHidDevice), kMaxHidErrorMessageSize); + << mixxx::convertWCStringToQString( + hid_error(m_pHidDevice), + kMaxHidErrorMessageSize); } } else { controllerDebug(result << "bytes sent to" << getName() - << "serial #" << hid_serial + << "serial #" << m_deviceInfo.serialNumber() << "(including report ID of" << reportID << ")"); } } @@ -354,33 +236,19 @@ void HidController::sendFeatureReport( reinterpret_cast(dataArray.constData()), dataArray.size()); if (result == -1) { - qWarning() << "sendFeatureReport is unable to send data to" << getName() - << "serial #" << hid_serial << ":" - << safeDecodeWideString(hid_error(m_pHidDevice), kMaxHidErrorMessageSize); + qWarning() << "sendFeatureReport is unable to send data to" + << getName() << "serial #" << m_deviceInfo.serialNumber() + << ":" + << mixxx::convertWCStringToQString( + hid_error(m_pHidDevice), + kMaxHidErrorMessageSize); } else { controllerDebug(result << "bytes sent by sendFeatureReport to" << getName() - << "serial #" << hid_serial + << "serial #" << m_deviceInfo.serialNumber() << "(including report ID of" << reportID << ")"); } } -//static -QString HidController::safeDecodeWideString(const wchar_t* pStr, size_t max_length) { - if (pStr == NULL) { - return QString(); - } - // find a terminating 0 or take all chars - int size = 0; - while ((size < (int)max_length) && (pStr[size] != 0)) { - ++size; - } - // inlining QString::fromWCharArray() - // We cannot use Qts wchar_t functions, since they may work or not - // depending on the '/Zc:wchar_t-' build flag in the Qt configs - // on Windows build - if (sizeof(wchar_t) == sizeof(QChar)) { - return QString::fromUtf16((const ushort *)pStr, size); - } else { - return QString::fromUcs4((uint *)pStr, size); - } +ControllerJSProxy* HidController::jsProxy() { + return new HidControllerJSProxy(this); } diff --git a/src/controllers/hid/hidcontroller.h b/src/controllers/hid/hidcontroller.h index fb6742cadf7..b62fda73077 100644 --- a/src/controllers/hid/hidcontroller.h +++ b/src/controllers/hid/hidcontroller.h @@ -1,26 +1,15 @@ -/** - * @file hidcontroller.h - * @author Sean M. Pappalardo spappalardo@mixxx.org - * @date Sun May 1 2011 - * @brief HID controller backend - */ - -#ifndef HIDCONTROLLER_H -#define HIDCONTROLLER_H - -#include - -#include +#pragma once #include "controllers/controller.h" #include "controllers/hid/hidcontrollerpreset.h" -#include "controllers/hid/hidcontrollerpresetfilehandler.h" +#include "controllers/hid/hiddevice.h" #include "util/duration.h" class HidController final : public Controller { Q_OBJECT public: - HidController(const hid_device_info& deviceInfo); + explicit HidController( + mixxx::hid::DeviceInfo&& deviceInfo); ~HidController() override; ControllerJSProxy* jsProxy() override; @@ -28,9 +17,8 @@ class HidController final : public Controller { QString presetExtension() override; ControllerPresetPointer getPreset() const override { - HidControllerPreset* pClone = new HidControllerPreset(); - *pClone = m_preset; - return ControllerPresetPointer(pClone); + return ControllerPresetPointer( + new HidControllerPreset(m_preset)); } void visit(const MidiControllerPreset* preset) override; @@ -48,8 +36,6 @@ class HidController final : public Controller { bool matchPreset(const PresetInfo& preset) override; - static QString safeDecodeWideString(const wchar_t* pStr, size_t max_length); - protected: void sendReport(QList data, unsigned int length, unsigned int reportID); @@ -73,22 +59,8 @@ class HidController final : public Controller { return &m_preset; } - bool matchProductInfo(const ProductInfo& product); - void guessDeviceCategory(); - - // Local copies of things we need from hid_device_info - int hid_interface_number; - unsigned short hid_vendor_id; - unsigned short hid_product_id; - unsigned short hid_usage_page; - unsigned short hid_usage; - char* hid_path; - wchar_t* hid_serial_raw; - QString hid_serial; - QString hid_manufacturer; - QString hid_product; - - QString m_sUID; + const mixxx::hid::DeviceInfo m_deviceInfo; + hid_device* m_pHidDevice; HidControllerPreset m_preset; @@ -125,5 +97,3 @@ class HidControllerJSProxy : public ControllerJSProxy { private: HidController* m_pHidController; }; - -#endif diff --git a/src/controllers/hid/hiddevice.cpp b/src/controllers/hid/hiddevice.cpp new file mode 100644 index 00000000000..52782fc0d18 --- /dev/null +++ b/src/controllers/hid/hiddevice.cpp @@ -0,0 +1,194 @@ +#include "controllers/hid/hiddevice.h" + +#include +#include +#include + +#include "controllers/controllerpresetinfo.h" +#include "util/path.h" // for PATH_MAX on Windows +#include "util/string.h" + +namespace { + +constexpr unsigned short kGenericDesktopUsagePage = 0x01; + +constexpr unsigned short kGenericDesktopPointerUsage = 0x01; +constexpr unsigned short kGenericDesktopMouseUsage = 0x02; +constexpr unsigned short kGenericDesktopJoystickUsage = 0x04; +constexpr unsigned short kGenericDesktopGamePadUsage = 0x05; +constexpr unsigned short kGenericDesktopKeyboardUsage = 0x06; +constexpr unsigned short kGenericDesktopKeypadUsage = 0x07; +constexpr unsigned short kGenericDesktopMultiaxisControllerUsage = 0x08; + +constexpr unsigned short kAppleInfraredControlProductId = 0x8242; + +constexpr std::size_t kDeviceInfoStringMaxLength = 512; + +} // namespace + +namespace mixxx { + +namespace hid { + +DeviceInfo::DeviceInfo( + const hid_device_info& device_info) + : usage_page(device_info.usage_page), + usage(device_info.usage), + interface_number(device_info.interface_number), + vendor_id(device_info.vendor_id), + product_id(device_info.product_id), + release_number(device_info.release_number), + m_pathRaw(device_info.path, strnlen(device_info.path, PATH_MAX)), + m_serialNumberRaw(device_info.serial_number, + wcsnlen(device_info.serial_number, + kDeviceInfoStringMaxLength)), + m_manufacturerName(mixxx::convertWCStringToQString( + device_info.manufacturer_string, + wcsnlen(device_info.manufacturer_string, + kDeviceInfoStringMaxLength))), + m_productName(mixxx::convertWCStringToQString(device_info.product_string, + wcsnlen(device_info.product_string, + kDeviceInfoStringMaxLength))), + m_serialNumber(mixxx::convertWCStringToQString( + m_serialNumberRaw.data(), m_serialNumberRaw.size())) { +} + +QString DeviceInfo::formatInterface() const { + if (interface_number < 0) { + return QString(); + } + return QChar('#') + QString::number(interface_number); +} + +QString DeviceInfo::formatUsage() const { + if (usage_page == 0 && usage == 0) { + DEBUG_ASSERT(!formatInterface().isEmpty()); + return QString(); + } + return QStringLiteral("0x") + + QString::number(usage_page, 16) + + QStringLiteral("/0x") + + QString::number(usage, 16); +} + +QString DeviceInfo::formatName() const { + // We include the last 4 digits of the serial number and the + // interface number to allow the user (and Mixxx!) to keep + // track of which is which + const auto serialSuffix = serialNumber().right(4); + if (interface_number >= 0) { + return productName() + + QChar(' ') + + serialSuffix + + QChar('_') + + QString::number(interface_number); + } else { + return productName() + + QChar(' ') + + serialSuffix; + } +} + +QDebug operator<<(QDebug dbg, const DeviceInfo& deviceInfo) { + QStringList parts; + parts.reserve(8); + const QString usage = deviceInfo.formatUsage(); + if (!usage.isEmpty()) { + parts.append(QStringLiteral("Usage: ") + usage); + } + const QString interface = deviceInfo.formatInterface(); + if (!interface.isEmpty()) { + parts.append(QStringLiteral("Interface: ") + interface); + } + parts.append(QStringLiteral("Vendor ID: 0x") + QString::number(deviceInfo.vendorId(), 16)); + parts.append(QStringLiteral("Product ID: 0x") + QString::number(deviceInfo.productId(), 16)); + parts.append(QStringLiteral("Release/Version: ") + + QString::number(deviceInfo.releaseNumberBCD(), 16)); + if (!deviceInfo.manufacturerName().isEmpty()) { + parts.append(QStringLiteral("Manufacturer: ") + deviceInfo.manufacturerName()); + } + if (!deviceInfo.productName().isEmpty()) { + parts.append(QStringLiteral("Product: ") + deviceInfo.productName()); + } + if (!deviceInfo.serialNumber().isEmpty()) { + parts.append(QStringLiteral("S/N: ") + deviceInfo.serialNumber()); + } + const auto dbgState = QDebugStateSaver(dbg); + return dbg.nospace().noquote() + << QStringLiteral("{ ") + << parts.join(QStringLiteral(" | ")) + << QStringLiteral(" }"); +} + +QString DeviceCategory::guessFromDeviceInfoImpl( + const DeviceInfo& deviceInfo) const { + // This should be done somehow else, I know. But at least we get started with + // the idea of mapping this information + const QString interface = deviceInfo.formatInterface(); + if (!interface.isEmpty()) { + // TODO: Guess linux device types somehow as well + // or maybe just fill in the interface number? + return tr("HID Interface %1: ").arg(interface) + deviceInfo.formatUsage(); + } + if (deviceInfo.usage_page == kGenericDesktopUsagePage) { + switch (deviceInfo.usage) { + case kGenericDesktopPointerUsage: + return tr("Generic HID Pointer"); + case kGenericDesktopMouseUsage: + return tr("Generic HID Mouse"); + case kGenericDesktopJoystickUsage: + return tr("Generic HID Joystick"); + case kGenericDesktopGamePadUsage: + return tr("Generic HID Game Pad"); + case kGenericDesktopKeyboardUsage: + return tr("Generic HID Keyboard"); + case kGenericDesktopKeypadUsage: + return tr("Generic HID Keypad"); + case kGenericDesktopMultiaxisControllerUsage: + return tr("Generic HID Multi-axis Controller"); + default: + return tr("Unknown HID Desktop Device: ") + deviceInfo.formatUsage(); + } + } else if (deviceInfo.vendor_id == kAppleVendorId) { + // Apple laptop special HID devices + if (deviceInfo.product_id == kAppleInfraredControlProductId) { + return tr("Apple HID Infrared Control"); + } else { + return tr("Unknown Apple HID Device: ") + deviceInfo.formatUsage(); + } + } else { + return tr("Unknown HID Device: ") + deviceInfo.formatUsage(); + } +} + +bool DeviceInfo::matchProductInfo( + const ProductInfo& product) const { + bool ok; + // Product and vendor match is always required + if (vendor_id != product.vendor_id.toInt(&ok, 16) || !ok) { + return false; + } + if (product_id != product.product_id.toInt(&ok, 16) || !ok) { + return false; + } + + // Optionally check against interface_number / usage_page && usage + if (interface_number >= 0) { + if (interface_number != product.interface_number.toInt(&ok, 16) || !ok) { + return false; + } + } else { + if (usage_page != product.usage_page.toInt(&ok, 16) || !ok) { + return false; + } + if (usage != product.usage.toInt(&ok, 16) || !ok) { + return false; + } + } + // Match found + return true; +} + +} // namespace hid + +} // namespace mixxx diff --git a/src/controllers/hid/hiddevice.h b/src/controllers/hid/hiddevice.h new file mode 100644 index 00000000000..1414f28f45b --- /dev/null +++ b/src/controllers/hid/hiddevice.h @@ -0,0 +1,100 @@ +#pragma once + +#include + +#include +#include + +class ProductInfo; + +namespace mixxx { + +namespace hid { + +constexpr unsigned short kAppleVendorId = 0x5ac; + +class DeviceInfo final { + public: + explicit DeviceInfo( + const hid_device_info& device_info); + + unsigned short vendorId() const { + return vendor_id; + } + unsigned short productId() const { + return product_id; + } + /// The release number is stored as a binary-coded decimal (BCD) + unsigned short releaseNumberBCD() const { + return release_number; + } + + const char* pathRaw() const { + return m_pathRaw.c_str(); + } + const wchar_t* serialNumberRaw() const { + return m_serialNumberRaw.c_str(); + } + + const QString& manufacturerName() const { + return m_manufacturerName; + } + const QString& productName() const { + return m_productName; + } + const QString& serialNumber() const { + return m_serialNumber; + } + + bool isValid() const { + return !productName().isNull() && !serialNumber().isNull(); + } + + QString formatInterface() const; + QString formatUsage() const; + QString formatName() const; + + bool matchProductInfo( + const ProductInfo& productInfo) const; + + private: + friend class DeviceCategory; + friend QDebug operator<<( + QDebug dbg, + const DeviceInfo& deviceInfo); + + // members from hid_device_info + unsigned short usage_page; + unsigned short usage; + int interface_number; + unsigned short vendor_id; + unsigned short product_id; + unsigned short release_number; + + std::string m_pathRaw; + std::wstring m_serialNumberRaw; + + QString m_manufacturerName; + QString m_productName; + QString m_serialNumber; +}; + +class DeviceCategory final : public QObject { + // QObject needed for i18n device category + Q_OBJECT + public: + static QString guessFromDeviceInfo( + const DeviceInfo& deviceInfo) { + return DeviceCategory().guessFromDeviceInfoImpl(deviceInfo); + } + + private: + QString guessFromDeviceInfoImpl( + const DeviceInfo& deviceInfo) const; + + DeviceCategory() = default; +}; + +} // namespace hid + +} // namespace mixxx diff --git a/src/controllers/hid/hidenumerator.cpp b/src/controllers/hid/hidenumerator.cpp index be4a150b890..1863b712af2 100644 --- a/src/controllers/hid/hidenumerator.cpp +++ b/src/controllers/hid/hidenumerator.cpp @@ -1,10 +1,3 @@ -/** -* @file hidenumerator.cpp -* @author Sean Pappalardo spappalardo@mixxx.org -* @date Sat Apr 30 2011 -* @brief This class handles discovery and enumeration of DJ controllers that use the USB-HID protocol -*/ - #include "controllers/hid/hidenumerator.h" #include @@ -64,54 +57,32 @@ HidEnumerator::~HidEnumerator() { } QList HidEnumerator::queryDevices() { - qDebug() << "Scanning HID devices:"; - - struct hid_device_info *devs, *cur_dev; - devs = hid_enumerate(0x0, 0x0); + qInfo() << "Scanning USB HID devices"; - for (cur_dev = devs; cur_dev; cur_dev = cur_dev->next) { - if (!recognizeDevice(*cur_dev)) { - // OS/X and windows use usage_page/usage not interface_number - qDebug() - << "Skipping" - << HidController::safeDecodeWideString( - cur_dev->manufacturer_string, 512) - << HidController::safeDecodeWideString( - cur_dev->product_string, 512) - << QString("r%1").arg(cur_dev->release_number) << "S/N" - << HidController::safeDecodeWideString( - cur_dev->serial_number, 512) - << (cur_dev->interface_number == -1 - ? QString("Usage Page %1 Usage %2") - .arg(QString::number( - cur_dev->usage_page), - QString::number( - cur_dev->usage)) - : QString("Interface %1") - .arg(cur_dev->interface_number)); + hid_device_info* device_info_list = hid_enumerate(0x0, 0x0); + for (const auto* device_info = device_info_list; + device_info; + device_info = device_info->next) { + auto deviceInfo = mixxx::hid::DeviceInfo(*device_info); + if (!recognizeDevice(*device_info)) { + qInfo() + << "Excluding USB HID device" + << deviceInfo; continue; } + qInfo() << "Found USB HID device:" + << deviceInfo; - // OS/X and windows use usage_page/usage not interface_number - qDebug() << "Found" - << HidController::safeDecodeWideString(cur_dev->manufacturer_string, 512) - << HidController::safeDecodeWideString(cur_dev->product_string, 512) - << QString("r%1").arg(cur_dev->release_number) - << "S/N" << HidController::safeDecodeWideString(cur_dev->serial_number, 512) - << (cur_dev->interface_number == -1 ? QString("Usage Page %1 Usage %2").arg( - QString::number(cur_dev->usage_page), - QString::number(cur_dev->usage)) : - QString("Interface %1").arg(cur_dev->interface_number)); - - if (!cur_dev->serial_number && !cur_dev->product_string) { - qWarning() << "USB permissions problem (or device error.) Your account needs write access to USB HID controllers."; + if (!deviceInfo.isValid()) { + qWarning() << "USB permissions problem or device error." + << "Your account needs write access to USB HID controllers."; continue; } - HidController* currentDevice = new HidController(*cur_dev); - m_devices.push_back(currentDevice); + HidController* newDevice = new HidController(std::move(deviceInfo)); + m_devices.push_back(newDevice); } - hid_free_enumeration(devs); + hid_free_enumeration(device_info_list); return m_devices; } diff --git a/src/util/string.h b/src/util/string.h index 08ed29f46f2..e81ade5f25b 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -4,6 +4,9 @@ #include #include #include +#include + +#include "util/assert.h" namespace mixxx { @@ -27,5 +30,28 @@ class StringCollator { QCollator m_collator; }; +/// Convert a wide-character C string to QString. +/// +/// We cannot use Qts wchar_t functions, since they may work or not +/// depending on the '/Zc:wchar_t-' build flag in the Qt configs +/// on Windows build. +/// +/// See also: QString::fromWCharArray() +inline QString convertWCStringToQString( + const wchar_t* wcs, + std::size_t len) { + DEBUG_ASSERT(wcsnlen(wcs, len) == len); + const auto ilen = static_cast(len); + DEBUG_ASSERT(ilen >= 0); // unsigned -> signed + switch (sizeof(wchar_t)) { + case sizeof(ushort): + return QString::fromUtf16(reinterpret_cast(wcs), ilen); + case sizeof(uint): + return QString::fromUcs4(reinterpret_cast(wcs), ilen); + default: + DEBUG_ASSERT(!"unsupported character type"); + return QString(); + } +} } // namespace mixxx From cc3733b9b5864b7b853f54dd4498600db1b242d0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 13 Nov 2020 23:55:30 +0100 Subject: [PATCH 3/8] Add special case handling when converting null strings --- src/util/string.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/util/string.h b/src/util/string.h index e81ade5f25b..f91b13499d7 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -40,6 +40,10 @@ class StringCollator { inline QString convertWCStringToQString( const wchar_t* wcs, std::size_t len) { + if (!wcs) { + DEBUG_ASSERT(len == 0); + return QString(); + } DEBUG_ASSERT(wcsnlen(wcs, len) == len); const auto ilen = static_cast(len); DEBUG_ASSERT(ilen >= 0); // unsigned -> signed From 62279c887471cc65d231d2e247f6660867f52554 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 14 Nov 2020 00:03:50 +0100 Subject: [PATCH 4/8] Add nullable string length functions --- src/controllers/hid/hiddevice.cpp | 10 ++++------ src/util/string.h | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/controllers/hid/hiddevice.cpp b/src/controllers/hid/hiddevice.cpp index 52782fc0d18..2e60cfd879a 100644 --- a/src/controllers/hid/hiddevice.cpp +++ b/src/controllers/hid/hiddevice.cpp @@ -1,8 +1,6 @@ #include "controllers/hid/hiddevice.h" #include -#include -#include #include "controllers/controllerpresetinfo.h" #include "util/path.h" // for PATH_MAX on Windows @@ -38,16 +36,16 @@ DeviceInfo::DeviceInfo( vendor_id(device_info.vendor_id), product_id(device_info.product_id), release_number(device_info.release_number), - m_pathRaw(device_info.path, strnlen(device_info.path, PATH_MAX)), + m_pathRaw(device_info.path, mixxx::nullable_strnlen(device_info.path, PATH_MAX)), m_serialNumberRaw(device_info.serial_number, - wcsnlen(device_info.serial_number, + mixxx::nullable_wcsnlen(device_info.serial_number, kDeviceInfoStringMaxLength)), m_manufacturerName(mixxx::convertWCStringToQString( device_info.manufacturer_string, - wcsnlen(device_info.manufacturer_string, + mixxx::nullable_wcsnlen(device_info.manufacturer_string, kDeviceInfoStringMaxLength))), m_productName(mixxx::convertWCStringToQString(device_info.product_string, - wcsnlen(device_info.product_string, + mixxx::nullable_wcsnlen(device_info.product_string, kDeviceInfoStringMaxLength))), m_serialNumber(mixxx::convertWCStringToQString( m_serialNumberRaw.data(), m_serialNumberRaw.size())) { diff --git a/src/util/string.h b/src/util/string.h index f91b13499d7..b56b2b32dfa 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "util/assert.h" @@ -30,6 +31,24 @@ class StringCollator { QCollator m_collator; }; +inline std::size_t nullable_strnlen( + const char* str, + std::size_t len) { + if (!str) { + return 0; + } + return strnlen(str, len); +} + +inline std::size_t nullable_wcsnlen( + const wchar_t* wcs, + std::size_t len) { + if (!wcs) { + return 0; + } + return wcsnlen(wcs, len); +} + /// Convert a wide-character C string to QString. /// /// We cannot use Qts wchar_t functions, since they may work or not From e670ea871861c8abef454135739e54ba85e15d6d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 14 Nov 2020 19:39:58 +0100 Subject: [PATCH 5/8] Rename string length functions and add documentation --- src/controllers/hid/hiddevice.cpp | 8 ++++---- src/util/string.h | 20 ++++++++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/controllers/hid/hiddevice.cpp b/src/controllers/hid/hiddevice.cpp index 2e60cfd879a..e6b7233f789 100644 --- a/src/controllers/hid/hiddevice.cpp +++ b/src/controllers/hid/hiddevice.cpp @@ -36,16 +36,16 @@ DeviceInfo::DeviceInfo( vendor_id(device_info.vendor_id), product_id(device_info.product_id), release_number(device_info.release_number), - m_pathRaw(device_info.path, mixxx::nullable_strnlen(device_info.path, PATH_MAX)), + m_pathRaw(device_info.path, mixxx::strnlen(device_info.path, PATH_MAX)), m_serialNumberRaw(device_info.serial_number, - mixxx::nullable_wcsnlen(device_info.serial_number, + mixxx::wcsnlen(device_info.serial_number, kDeviceInfoStringMaxLength)), m_manufacturerName(mixxx::convertWCStringToQString( device_info.manufacturer_string, - mixxx::nullable_wcsnlen(device_info.manufacturer_string, + mixxx::wcsnlen(device_info.manufacturer_string, kDeviceInfoStringMaxLength))), m_productName(mixxx::convertWCStringToQString(device_info.product_string, - mixxx::nullable_wcsnlen(device_info.product_string, + mixxx::wcsnlen(device_info.product_string, kDeviceInfoStringMaxLength))), m_serialNumber(mixxx::convertWCStringToQString( m_serialNumberRaw.data(), m_serialNumberRaw.size())) { diff --git a/src/util/string.h b/src/util/string.h index b56b2b32dfa..57f2b256614 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -31,22 +31,30 @@ class StringCollator { QCollator m_collator; }; -inline std::size_t nullable_strnlen( +/// A nullptr-safe variant of the corresponding standard C function. +/// +/// Treats nullptr like an empty string and returns 0. +inline std::size_t strnlen( const char* str, std::size_t len) { - if (!str) { + if (str == nullptr) { return 0; } - return strnlen(str, len); + // Invoke the global function + return ::strnlen(str, len); } -inline std::size_t nullable_wcsnlen( +/// A nullptr-safe variant of the corresponding standard C function. +/// +/// Treats nullptr like an empty string and returns 0. +inline std::size_t wcsnlen( const wchar_t* wcs, std::size_t len) { - if (!wcs) { + if (wcs == nullptr) { return 0; } - return wcsnlen(wcs, len); + // Invoke the global function + return ::wcsnlen(wcs, len); } /// Convert a wide-character C string to QString. From 519d46bdb7e64e094acd03bbf847d32ce0f4d8ec Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 15 Nov 2020 14:37:02 +0100 Subject: [PATCH 6/8] Fix forward declaration of ProductInfo --- src/controllers/hid/hiddevice.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/hid/hiddevice.h b/src/controllers/hid/hiddevice.h index 1414f28f45b..616c5c7e49f 100644 --- a/src/controllers/hid/hiddevice.h +++ b/src/controllers/hid/hiddevice.h @@ -5,7 +5,7 @@ #include #include -class ProductInfo; +struct ProductInfo; namespace mixxx { From 5f273476e05023ff3f2fdd3063a2c2a7ee84e2cd Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 16 Nov 2020 16:45:42 +0100 Subject: [PATCH 7/8] Use a compact representation for reporting USB HID devices --- src/controllers/hid/hiddevice.cpp | 53 +++++++++++++++++++------------ src/controllers/hid/hiddevice.h | 23 ++++++++------ 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/controllers/hid/hiddevice.cpp b/src/controllers/hid/hiddevice.cpp index e6b7233f789..bbc8f2a1613 100644 --- a/src/controllers/hid/hiddevice.cpp +++ b/src/controllers/hid/hiddevice.cpp @@ -30,21 +30,21 @@ namespace hid { DeviceInfo::DeviceInfo( const hid_device_info& device_info) - : usage_page(device_info.usage_page), - usage(device_info.usage), - interface_number(device_info.interface_number), - vendor_id(device_info.vendor_id), + : vendor_id(device_info.vendor_id), product_id(device_info.product_id), release_number(device_info.release_number), + usage_page(device_info.usage_page), + usage(device_info.usage), + interface_number(device_info.interface_number), m_pathRaw(device_info.path, mixxx::strnlen(device_info.path, PATH_MAX)), m_serialNumberRaw(device_info.serial_number, mixxx::wcsnlen(device_info.serial_number, kDeviceInfoStringMaxLength)), - m_manufacturerName(mixxx::convertWCStringToQString( + m_manufacturerString(mixxx::convertWCStringToQString( device_info.manufacturer_string, mixxx::wcsnlen(device_info.manufacturer_string, kDeviceInfoStringMaxLength))), - m_productName(mixxx::convertWCStringToQString(device_info.product_string, + m_productString(mixxx::convertWCStringToQString(device_info.product_string, mixxx::wcsnlen(device_info.product_string, kDeviceInfoStringMaxLength))), m_serialNumber(mixxx::convertWCStringToQString( @@ -58,15 +58,26 @@ QString DeviceInfo::formatInterface() const { return QChar('#') + QString::number(interface_number); } +QString DeviceInfo::formatVID() const { + return QStringLiteral("%1").arg(vendor_id, 4, 16, QLatin1Char('0')); +} + +QString DeviceInfo::formatPID() const { + return QStringLiteral("%1").arg(product_id, 4, 16, QLatin1Char('0')); +} + +QString DeviceInfo::formatReleaseNumber() const { + return QString::number(releaseNumberBCD(), 16); +} + QString DeviceInfo::formatUsage() const { if (usage_page == 0 && usage == 0) { DEBUG_ASSERT(!formatInterface().isEmpty()); return QString(); } - return QStringLiteral("0x") + - QString::number(usage_page, 16) + - QStringLiteral("/0x") + - QString::number(usage, 16); + return QStringLiteral("%1").arg(usage_page, 4, 16, QLatin1Char('0')) + + QLatin1Char(':') + + QStringLiteral("%1").arg(usage, 4, 16, QLatin1Char('0')); } QString DeviceInfo::formatName() const { @@ -75,13 +86,13 @@ QString DeviceInfo::formatName() const { // track of which is which const auto serialSuffix = serialNumber().right(4); if (interface_number >= 0) { - return productName() + + return productString() + QChar(' ') + serialSuffix + QChar('_') + QString::number(interface_number); } else { - return productName() + + return productString() + QChar(' ') + serialSuffix; } @@ -90,6 +101,12 @@ QString DeviceInfo::formatName() const { QDebug operator<<(QDebug dbg, const DeviceInfo& deviceInfo) { QStringList parts; parts.reserve(8); + // "VID:PID vReleaseNumber" + parts.append(deviceInfo.formatVID() + + QLatin1Char(':') + + deviceInfo.formatPID() + + QLatin1String(" r") + + deviceInfo.formatReleaseNumber()); const QString usage = deviceInfo.formatUsage(); if (!usage.isEmpty()) { parts.append(QStringLiteral("Usage: ") + usage); @@ -98,15 +115,11 @@ QDebug operator<<(QDebug dbg, const DeviceInfo& deviceInfo) { if (!interface.isEmpty()) { parts.append(QStringLiteral("Interface: ") + interface); } - parts.append(QStringLiteral("Vendor ID: 0x") + QString::number(deviceInfo.vendorId(), 16)); - parts.append(QStringLiteral("Product ID: 0x") + QString::number(deviceInfo.productId(), 16)); - parts.append(QStringLiteral("Release/Version: ") + - QString::number(deviceInfo.releaseNumberBCD(), 16)); - if (!deviceInfo.manufacturerName().isEmpty()) { - parts.append(QStringLiteral("Manufacturer: ") + deviceInfo.manufacturerName()); + if (!deviceInfo.manufacturerString().isEmpty()) { + parts.append(QStringLiteral("Manufacturer: ") + deviceInfo.manufacturerString()); } - if (!deviceInfo.productName().isEmpty()) { - parts.append(QStringLiteral("Product: ") + deviceInfo.productName()); + if (!deviceInfo.productString().isEmpty()) { + parts.append(QStringLiteral("Product: ") + deviceInfo.productString()); } if (!deviceInfo.serialNumber().isEmpty()) { parts.append(QStringLiteral("S/N: ") + deviceInfo.serialNumber()); diff --git a/src/controllers/hid/hiddevice.h b/src/controllers/hid/hiddevice.h index 616c5c7e49f..a20b2955d6c 100644 --- a/src/controllers/hid/hiddevice.h +++ b/src/controllers/hid/hiddevice.h @@ -36,20 +36,23 @@ class DeviceInfo final { return m_serialNumberRaw.c_str(); } - const QString& manufacturerName() const { - return m_manufacturerName; + const QString& manufacturerString() const { + return m_manufacturerString; } - const QString& productName() const { - return m_productName; + const QString& productString() const { + return m_productString; } const QString& serialNumber() const { return m_serialNumber; } bool isValid() const { - return !productName().isNull() && !serialNumber().isNull(); + return !productString().isNull() && !serialNumber().isNull(); } + QString formatVID() const; + QString formatPID() const; + QString formatReleaseNumber() const; QString formatInterface() const; QString formatUsage() const; QString formatName() const; @@ -64,18 +67,18 @@ class DeviceInfo final { const DeviceInfo& deviceInfo); // members from hid_device_info - unsigned short usage_page; - unsigned short usage; - int interface_number; unsigned short vendor_id; unsigned short product_id; unsigned short release_number; + unsigned short usage_page; + unsigned short usage; + int interface_number; std::string m_pathRaw; std::wstring m_serialNumberRaw; - QString m_manufacturerName; - QString m_productName; + QString m_manufacturerString; + QString m_productString; QString m_serialNumber; }; From eea33eb7fc5e633b0e15d5ebe88b01efcbabec67 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 17 Nov 2020 21:36:50 +0100 Subject: [PATCH 8/8] HID DeviceInfo: Add documentation --- src/controllers/hid/hiddevice.h | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/controllers/hid/hiddevice.h b/src/controllers/hid/hiddevice.h index a20b2955d6c..28d1539c347 100644 --- a/src/controllers/hid/hiddevice.h +++ b/src/controllers/hid/hiddevice.h @@ -13,25 +13,41 @@ namespace hid { constexpr unsigned short kAppleVendorId = 0x5ac; +/// Detached copy of `struct hid_device_info`. +/// +/// Stores a detached copy of hid_device_info and its contents. +/// +/// All instances of hid_device_info are returned by the HIDAPI +/// library as a linked list. The memory of both the members +/// of this list as well as their contents are managed by the +/// library and freed immediately after iterating through this +/// list. +/// +/// Includes some basic validations and implicit conversion to +/// QString if needed. class DeviceInfo final { public: explicit DeviceInfo( const hid_device_info& device_info); + // The VID. unsigned short vendorId() const { return vendor_id; } + // The PID. unsigned short productId() const { return product_id; } - /// The release number is stored as a binary-coded decimal (BCD) + /// The release number as a binary-coded decimal (BCD). unsigned short releaseNumberBCD() const { return release_number; } + /// The raw path, needed for subsequent HIDAPI requests. const char* pathRaw() const { return m_pathRaw.c_str(); } + /// The raw serial number, needed for subsequent HIDAPI requests. const wchar_t* serialNumberRaw() const { return m_serialNumberRaw.c_str(); }