From 3ff9a1f270b47a2a4e483713143d5b7059f23023 Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Sun, 23 Jul 2023 17:14:02 +0200 Subject: [PATCH] Add USB condition type It allows users to check if a given USB device is currently connected --- data/locale/en-US.ini | 18 ++ plugins/CMakeLists.txt | 1 + plugins/usb/CMakeLists.txt | 110 ++++++++ plugins/usb/macro-condition-usb.cpp | 381 ++++++++++++++++++++++++++++ plugins/usb/macro-condition-usb.hpp | 97 +++++++ plugins/usb/usb-helpers.cpp | 233 +++++++++++++++++ plugins/usb/usb-helpers.hpp | 27 ++ 7 files changed, 867 insertions(+) create mode 100644 plugins/usb/CMakeLists.txt create mode 100644 plugins/usb/macro-condition-usb.cpp create mode 100644 plugins/usb/macro-condition-usb.hpp create mode 100644 plugins/usb/usb-helpers.cpp create mode 100644 plugins/usb/usb-helpers.hpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index f7c88f0f..31308724 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -703,6 +703,16 @@ AdvSceneSwitcher.condition.folder.condition.folderRemove="A directory was remove AdvSceneSwitcher.condition.folder.entry="{{conditions}}in{{folder}}{{tooltip}}" AdvSceneSwitcher.condition.folder.enableFilter="Only evaluate to true, if the changed path matches a patern" AdvSceneSwitcher.condition.folder.entry.filter="{{filter}}{{regex}}" +AdvSceneSwitcher.condition.usb="USB" +AdvSceneSwitcher.condition.usb.description="A USB device matching the following properties is connected:" +AdvSceneSwitcher.condition.usb.vendorID="Vendor ID:" +AdvSceneSwitcher.condition.usb.productID="Product ID:" +AdvSceneSwitcher.condition.usb.busNumber="Bus Number:" +AdvSceneSwitcher.condition.usb.deviceAddress="Device Address:" +AdvSceneSwitcher.condition.usb.vendorName="Vendor Name:" +AdvSceneSwitcher.condition.usb.productName="Product Name:" +AdvSceneSwitcher.condition.usb.serialNumber="Serial Number:" +AdvSceneSwitcher.condition.noDevicesFoundWarning="No USB devices detected!\nThe plugin might not have the required permissions to check for USB devices." ; Macro Actions AdvSceneSwitcher.action.scene="Switch scene" @@ -1778,6 +1788,14 @@ AdvSceneSwitcher.tempVar.slideShow.path.description="The filesystem path of the AdvSceneSwitcher.tempVar.slideShow.fileName="Slide file name" AdvSceneSwitcher.tempVar.slideShow.fileName.description="The name of file of the slide currently being displayed." +AdvSceneSwitcher.tempVar.usb.vendorID="Vendor ID" +AdvSceneSwitcher.tempVar.usb.productID="Product ID" +AdvSceneSwitcher.tempVar.usb.busNumber="Bus Number" +AdvSceneSwitcher.tempVar.usb.deviceAddress="Device Address" +AdvSceneSwitcher.tempVar.usb.vendorName="Vendor Name" +AdvSceneSwitcher.tempVar.usb.productName="Product Name" +AdvSceneSwitcher.tempVar.usb.serialNumber="Serial Number" + AdvSceneSwitcher.selectScene="--select scene--" AdvSceneSwitcher.selectPreviousScene="Previous Scene" AdvSceneSwitcher.selectCurrentScene="Current Scene" diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 70b9d120..9c5d8371 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -31,4 +31,5 @@ install_advss_plugin_dependency(...) add_plugin(midi) add_plugin(openvr) add_plugin(twitch) +add_plugin(usb) add_plugin(video) diff --git a/plugins/usb/CMakeLists.txt b/plugins/usb/CMakeLists.txt new file mode 100644 index 00000000..86093d3c --- /dev/null +++ b/plugins/usb/CMakeLists.txt @@ -0,0 +1,110 @@ +cmake_minimum_required(VERSION 3.14) +project(advanced-scene-switcher-usb) + +# --- Check libusb requirements --- + +get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR) +set(libusb_DEPS_DIR "${ADVSS_SOURCE_DIR}/deps/libusb") + +if(OS_LINUX) + + find_package(PkgConfig) + if(NOT PKG_CONFIG_FOUND) + message(WARNING "pkg-config not found!\n" "USB condition will be disabled!") + return() + endif() + pkg_check_modules(libusb OPTIONAL libusb) + find_path( + libusb_HEADER_DIR + NAMES libusb.h + PATHS ${libusb_INCLUDEDIR} /usr/include /usr/local/include + /opt/local/include + PATH_SUFFIXES libusb-1.0) + find_library( + libusb_LINK_LIBRARIES + NAMES usb-1.0 usb + PATHS /usr/lib /usr/local/lib /opt/local/lib) + +elseif(OS_MACOS) + + find_path( + libusb_HEADER_DIR + NAMES libusb.h + PATHS ${libusb_DEPS_DIR}/libusb) + + # The NO_DEFAULT_PATH option is set since we have to use the "universal" + # variant of the library, which combines the x86 and arm variants + find_library( + libusb_LINK_LIBRARIES + NAMES usb-1.0.0 usb-1.0 usb + PATHS ${CMAKE_PREFIX_PATH} + PATH_SUFFIXES lib + NO_DEFAULT_PATH) + find_file( + libusb_RUNTIME_LIBRARY + NAMES libusb-1.0.0.dylib + PATHS ${CMAKE_PREFIX_PATH} + PATH_SUFFIXES lib + NO_DEFAULT_PATH) + +elseif(OS_WINDOWS) + + find_path( + libusb_HEADER_DIR + NAMES libusb.h + PATHS ${libusb_DEPS_DIR}/libusb) + + # Intentionally not using find_library since we have to use the libusb-1.0.lib + # file from within the dll folder instead of the lib folder to avoid linking + # issues due to conflicting LIBCMT versions + find_file( + libusb_LINK_LIBRARIES + NAMES libusb-1.0.lib + PATHS ${CMAKE_PREFIX_PATH} + PATH_SUFFIXES dll) + + find_file( + libusb_RUNTIME_LIBRARY + NAMES libusb-1.0.dll + PATHS ${CMAKE_PREFIX_PATH} + PATH_SUFFIXES dll) + +endif() + +if(NOT libusb_HEADER_DIR) + message(WARNING "libusb headers not found. Make sure libusb is installed. " + "USB condition will be disabled!\n\n" + "libusb sources are available under: ${libusb_DEPS_DIR}") + return() +endif() +if(NOT libusb_LINK_LIBRARIES) + message(WARNING "libusb library not found. Make sure libusb is installed. " + "USB condition will be disabled!\n\n" + "libusb sources are available under: ${libusb_DEPS_DIR}") + return() +endif() +if(NOT OS_LINUX AND NOT libusb_RUNTIME_LIBRARY) + message( + WARNING "libusb runtime library not found. Make sure libusb is installed. " + "USB condition will be disabled!\n\n" + "libusb sources are available under: ${libusb_DEPS_DIR}") + return() +endif() + +# --- End of section --- + +add_library(${PROJECT_NAME} MODULE) + +target_sources( + ${PROJECT_NAME} PRIVATE macro-condition-usb.cpp macro-condition-usb.hpp + usb-helpers.cpp usb-helpers.hpp) + +setup_advss_plugin(${PROJECT_NAME}) +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +target_include_directories(${PROJECT_NAME} PRIVATE ${libusb_HEADER_DIR}) +target_link_libraries(${PROJECT_NAME} PRIVATE ${libusb_LINK_LIBRARIES}) +install_advss_plugin(${PROJECT_NAME}) +if(NOT OS_LINUX) + install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES + ${libusb_RUNTIME_LIBRARY}) +endif() diff --git a/plugins/usb/macro-condition-usb.cpp b/plugins/usb/macro-condition-usb.cpp new file mode 100644 index 00000000..270373c6 --- /dev/null +++ b/plugins/usb/macro-condition-usb.cpp @@ -0,0 +1,381 @@ +#include "macro-condition-usb.hpp" +#include "layout-helpers.hpp" + +#include "obs.hpp" + +namespace advss { + +const std::string MacroConditionUSB::id = "usb"; + +bool MacroConditionUSB::_registered = MacroConditionFactory::Register( + MacroConditionUSB::id, + {MacroConditionUSB::Create, MacroConditionUSBEdit::Create, + "AdvSceneSwitcher.condition.usb"}); + +bool MacroConditionUSB::DeviceMatchOption::Matches( + const std::string &value) const +{ + if (!regex.Enabled()) { + return pattern == value; + } + return regex.Matches(value, pattern); +} + +void MacroConditionUSB::DeviceMatchOption::Save(obs_data_t *obj, + const char *name) const +{ + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "pattern", pattern.c_str()); + regex.Save(data); + obs_data_set_obj(obj, name, data); +} + +void MacroConditionUSB::DeviceMatchOption::Load(obs_data_t *obj, + const char *name) +{ + OBSDataAutoRelease data = obs_data_get_obj(obj, name); + pattern = obs_data_get_string(data, "pattern"); + regex.Load(data); +} + +bool MacroConditionUSB::CheckCondition() +{ + const auto devs = GetUSBDevices(); + if (devs.empty()) { + return false; + } + + for (const auto &dev : devs) { + if (_vendorID.Matches(dev.vendorID) && + _productID.Matches(dev.productID) && + _busNumber.Matches(dev.busNumber) && + _deviceAddress.Matches(dev.deviceAddress) && + _vendorName.Matches(dev.vendorName) && + _productName.Matches(dev.productName) && + _serialNumber.Matches(dev.serialNumber)) { + SetTempVarValue("vendorID", dev.vendorID); + SetTempVarValue("productID", dev.productID); + SetTempVarValue("busNumber", dev.busNumber); + SetTempVarValue("deviceAddress", dev.deviceAddress); + SetTempVarValue("vendorName", dev.vendorName); + SetTempVarValue("productName", dev.productName); + SetTempVarValue("serialNumber", dev.serialNumber); + return true; + } + } + return false; +} + +bool MacroConditionUSB::Save(obs_data_t *obj) const +{ + MacroCondition::Save(obj); + _vendorID.Save(obj, "vendorID"); + _productID.Save(obj, "productID"); + _busNumber.Save(obj, "busNumber"); + _deviceAddress.Save(obj, "deviceAddress"); + _vendorName.Save(obj, "vendorName"); + _productName.Save(obj, "productName"); + _serialNumber.Save(obj, "serialNumber"); + return true; +} + +bool MacroConditionUSB::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _vendorID.Load(obj, "vendorID"); + _productID.Load(obj, "productID"); + _busNumber.Load(obj, "busNumber"); + _deviceAddress.Load(obj, "deviceAddress"); + _vendorName.Load(obj, "vendorName"); + _productName.Load(obj, "productName"); + _serialNumber.Load(obj, "serialNumber"); + return true; +} + +void MacroConditionUSB::SetupTempVars() +{ + AddTempvar("vendorName", + obs_module_text("AdvSceneSwitcher.tempVar.usb.vendorName")); + AddTempvar("productName", + obs_module_text("AdvSceneSwitcher.tempVar.usb.productName")); + AddTempvar("vendorID", + obs_module_text("AdvSceneSwitcher.tempVar.usb.vendorID")); + AddTempvar("productID", + obs_module_text("AdvSceneSwitcher.tempVar.usb.productID")); + AddTempvar("busNumber", + obs_module_text("AdvSceneSwitcher.tempVar.usb.busNumber")); + AddTempvar( + "deviceAddress", + obs_module_text("AdvSceneSwitcher.tempVar.usb.deviceAddress")); + AddTempvar( + "serialNumber", + obs_module_text("AdvSceneSwitcher.tempVar.usb.serialNumber")); +} + +static void setupDevicePropertySelection(QComboBox *list, + const QSet &items) +{ + list->setEditable(true); + list->setMaxVisibleItems(20); + list->setDuplicatesEnabled(false); + for (const auto &item : items) { + list->addItem(item); + } + list->model()->sort(0); +} + +void static populateNewLayoutRow(QGridLayout *layout, int &row, + const char *labelText, QComboBox *property, + RegexConfigWidget *regex) +{ + layout->addWidget(new QLabel(obs_module_text(labelText)), row, 0); + auto settingsLayout = new QHBoxLayout(); + property->setSizePolicy(QSizePolicy::MinimumExpanding, + QSizePolicy::MinimumExpanding); + settingsLayout->addWidget(property); + settingsLayout->addWidget(regex); + settingsLayout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(settingsLayout, row, 1); + row++; +} + +MacroConditionUSBEdit::MacroConditionUSBEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _vendorID(new QComboBox()), + _productID(new QComboBox()), + _busNumber(new QComboBox()), + _deviceAddress(new QComboBox()), + _vendorName(new QComboBox()), + _productName(new QComboBox()), + _serialNumber(new QComboBox()), + _vendorIDRegex(new RegexConfigWidget()), + _productIDRegex(new RegexConfigWidget()), + _busNumberRegex(new RegexConfigWidget()), + _deviceAddressRegex(new RegexConfigWidget()), + _vendorNameRegex(new RegexConfigWidget()), + _productNameRegex(new RegexConfigWidget()), + _serialNumberRegex(new RegexConfigWidget()) +{ + QSet vendorIDs; + QSet productIDs; + QSet busNumbers; + QSet deviceAddresses; + QSet vendorNames; + QSet productNames; + QSet serialNumbers; + + const auto devs = GetUSBDevices(); + for (const auto &dev : devs) { + vendorIDs.insert(QString::fromStdString(dev.vendorID)); + productIDs.insert(QString::fromStdString(dev.productID)); + busNumbers.insert(QString::fromStdString(dev.busNumber)); + deviceAddresses.insert( + QString::fromStdString(dev.deviceAddress)); + vendorNames.insert(QString::fromStdString(dev.vendorName)); + productNames.insert(QString::fromStdString(dev.productName)); + serialNumbers.insert(QString::fromStdString(dev.serialNumber)); + } + + setupDevicePropertySelection(_vendorID, vendorIDs); + setupDevicePropertySelection(_productID, productIDs); + setupDevicePropertySelection(_busNumber, busNumbers); + setupDevicePropertySelection(_deviceAddress, deviceAddresses); + setupDevicePropertySelection(_vendorName, vendorNames); + setupDevicePropertySelection(_productName, productNames); + setupDevicePropertySelection(_serialNumber, serialNumbers); + + QWidget::connect(_vendorID, &QComboBox::currentTextChanged, this, + &MacroConditionUSBEdit::VendorIDChanged); + QWidget::connect(_productID, &QComboBox::currentTextChanged, this, + &MacroConditionUSBEdit::ProductIDChanged); + QWidget::connect(_busNumber, &QComboBox::currentTextChanged, this, + &MacroConditionUSBEdit::BusNumberChanged); + QWidget::connect(_deviceAddress, &QComboBox::currentTextChanged, this, + &MacroConditionUSBEdit::DeviceAddressChanged); + QWidget::connect(_vendorName, &QComboBox::currentTextChanged, this, + &MacroConditionUSBEdit::VendorNameChanged); + QWidget::connect(_productName, &QComboBox::currentTextChanged, this, + &MacroConditionUSBEdit::ProductNameChanged); + QWidget::connect(_serialNumber, &QComboBox::currentTextChanged, this, + &MacroConditionUSBEdit::SerialNumberChanged); + QWidget::connect(_vendorIDRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(VendorIDRegexChanged(const RegexConfig &))); + QWidget::connect(_productIDRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(ProductIDRegexChanged(const RegexConfig &))); + QWidget::connect(_busNumberRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(BusNumberRegexChanged(const RegexConfig &))); + QWidget::connect(_deviceAddressRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(DeviceAddressRegexChanged(const RegexConfig &))); + QWidget::connect(_vendorNameRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(VendorNameRegexChanged(const RegexConfig &))); + QWidget::connect(_productNameRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(ProductNameRegexChanged(const RegexConfig &))); + QWidget::connect(_serialNumberRegex, + SIGNAL(RegexConfigChanged(const RegexConfig &)), this, + SLOT(SerialNumberRegexChanged(const RegexConfig &))); + + int row = 0; + auto devicePropertyLayout = new QGridLayout; + populateNewLayoutRow(devicePropertyLayout, row, + "AdvSceneSwitcher.condition.usb.vendorName", + _vendorName, _vendorNameRegex); + populateNewLayoutRow(devicePropertyLayout, row, + "AdvSceneSwitcher.condition.usb.productName", + _productName, _productNameRegex); + populateNewLayoutRow(devicePropertyLayout, row, + "AdvSceneSwitcher.condition.usb.vendorID", + _vendorID, _vendorIDRegex); + populateNewLayoutRow(devicePropertyLayout, row, + "AdvSceneSwitcher.condition.usb.productID", + _productID, _productIDRegex); + populateNewLayoutRow(devicePropertyLayout, row, + "AdvSceneSwitcher.condition.usb.busNumber", + _busNumber, _busNumberRegex); + populateNewLayoutRow(devicePropertyLayout, row, + "AdvSceneSwitcher.condition.usb.deviceAddress", + _deviceAddress, _deviceAddressRegex); + populateNewLayoutRow(devicePropertyLayout, row, + "AdvSceneSwitcher.condition.usb.serialNumber", + _serialNumber, _serialNumberRegex); + MinimizeSizeOfColumn(devicePropertyLayout, 0); + devicePropertyLayout->setContentsMargins(0, 0, 0, 0); + + auto layout = new QVBoxLayout(); + layout->addWidget(new QLabel( + obs_module_text("AdvSceneSwitcher.condition.usb.description"))); + layout->addLayout(devicePropertyLayout); + if (devs.empty()) { + layout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.condition.noDevicesFoundWarning"))); + } + setLayout(layout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionUSBEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _vendorID->setCurrentText( + QString::fromStdString(_entryData->_vendorID.pattern)); + _productID->setCurrentText( + QString::fromStdString(_entryData->_productID.pattern)); + _busNumber->setCurrentText( + QString::fromStdString(_entryData->_busNumber.pattern)); + _deviceAddress->setCurrentText( + QString::fromStdString(_entryData->_deviceAddress.pattern)); + _vendorName->setCurrentText( + QString::fromStdString(_entryData->_vendorName.pattern)); + _productName->setCurrentText( + QString::fromStdString(_entryData->_productName.pattern)); + _serialNumber->setCurrentText( + QString::fromStdString(_entryData->_serialNumber.pattern)); + _vendorIDRegex->SetRegexConfig(_entryData->_vendorID.regex); + _productIDRegex->SetRegexConfig(_entryData->_productID.regex); + _busNumberRegex->SetRegexConfig(_entryData->_busNumber.regex); + _deviceAddressRegex->SetRegexConfig(_entryData->_deviceAddress.regex); + _vendorNameRegex->SetRegexConfig(_entryData->_vendorName.regex); + _productNameRegex->SetRegexConfig(_entryData->_productName.regex); + _serialNumberRegex->SetRegexConfig(_entryData->_serialNumber.regex); + + adjustSize(); + updateGeometry(); +} + +void MacroConditionUSBEdit::VendorIDChanged(const QString &text) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_vendorID.pattern = text.toStdString(); +} + +void MacroConditionUSBEdit::ProductIDChanged(const QString &text) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_productID.pattern = text.toStdString(); +} + +void MacroConditionUSBEdit::BusNumberChanged(const QString &text) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_busNumber.pattern = text.toStdString(); +} + +void MacroConditionUSBEdit::DeviceAddressChanged(const QString &text) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_deviceAddress.pattern = text.toStdString(); +} + +void MacroConditionUSBEdit::VendorNameChanged(const QString &text) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_vendorName.pattern = text.toStdString(); +} + +void MacroConditionUSBEdit::ProductNameChanged(const QString &text) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_productName.pattern = text.toStdString(); +} + +void MacroConditionUSBEdit::SerialNumberChanged(const QString &text) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_serialNumber.pattern = text.toStdString(); +} + +void MacroConditionUSBEdit::VendorIDRegexChanged(const RegexConfig ®ex) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_vendorID.regex = regex; +} + +void MacroConditionUSBEdit::ProductIDRegexChanged(const RegexConfig ®ex) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_productID.regex = regex; +} + +void MacroConditionUSBEdit::BusNumberRegexChanged(const RegexConfig ®ex) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_busNumber.regex = regex; +} + +void MacroConditionUSBEdit::DeviceAddressRegexChanged(const RegexConfig ®ex) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_deviceAddress.regex = regex; +} + +void MacroConditionUSBEdit::VendorNameRegexChanged(const RegexConfig ®ex) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_vendorName.regex = regex; +} + +void MacroConditionUSBEdit::ProductNameRegexChanged(const RegexConfig ®ex) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_productName.regex = regex; +} + +void MacroConditionUSBEdit::SerialNumberRegexChanged(const RegexConfig ®ex) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->_serialNumber.regex = regex; +} + +} // namespace advss diff --git a/plugins/usb/macro-condition-usb.hpp b/plugins/usb/macro-condition-usb.hpp new file mode 100644 index 00000000..d5d5177c --- /dev/null +++ b/plugins/usb/macro-condition-usb.hpp @@ -0,0 +1,97 @@ +#pragma once +#include "macro-condition-edit.hpp" +#include "usb-helpers.hpp" +#include "regex-config.hpp" + +#include + +namespace advss { + +class MacroConditionUSB : public MacroCondition { +public: + MacroConditionUSB(Macro *m) : MacroCondition(m, true) {} + bool CheckCondition(); + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + std::string GetId() const { return id; }; + static std::shared_ptr Create(Macro *m) + { + return std::make_shared(m); + } + + struct DeviceMatchOption { + std::string pattern = ".*"; + RegexConfig regex = RegexConfig(true); + + bool Matches(const std::string &value) const; + void Save(obs_data_t *, const char *) const; + void Load(obs_data_t *, const char *); + }; + + DeviceMatchOption _vendorID; + DeviceMatchOption _productID; + DeviceMatchOption _busNumber; + DeviceMatchOption _deviceAddress; + DeviceMatchOption _vendorName; + DeviceMatchOption _productName; + DeviceMatchOption _serialNumber; + +private: + void SetupTempVars(); + + static bool _registered; + static const std::string id; +}; + +class MacroConditionUSBEdit : public QWidget { + Q_OBJECT + +public: + MacroConditionUSBEdit(QWidget *parent, + std::shared_ptr cond = nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr cond) + { + return new MacroConditionUSBEdit( + parent, + std::dynamic_pointer_cast(cond)); + } + +private slots: + void VendorIDChanged(const QString &text); + void ProductIDChanged(const QString &text); + void BusNumberChanged(const QString &text); + void DeviceAddressChanged(const QString &text); + void VendorNameChanged(const QString &text); + void ProductNameChanged(const QString &text); + void SerialNumberChanged(const QString &text); + void VendorIDRegexChanged(const RegexConfig ®ex); + void ProductIDRegexChanged(const RegexConfig ®ex); + void BusNumberRegexChanged(const RegexConfig ®ex); + void DeviceAddressRegexChanged(const RegexConfig ®ex); + void VendorNameRegexChanged(const RegexConfig ®ex); + void ProductNameRegexChanged(const RegexConfig ®ex); + void SerialNumberRegexChanged(const RegexConfig ®ex); + +private: + QComboBox *_vendorID; + QComboBox *_productID; + QComboBox *_busNumber; + QComboBox *_deviceAddress; + QComboBox *_vendorName; + QComboBox *_productName; + QComboBox *_serialNumber; + RegexConfigWidget *_vendorIDRegex; + RegexConfigWidget *_productIDRegex; + RegexConfigWidget *_busNumberRegex; + RegexConfigWidget *_deviceAddressRegex; + RegexConfigWidget *_vendorNameRegex; + RegexConfigWidget *_productNameRegex; + RegexConfigWidget *_serialNumberRegex; + + std::shared_ptr _entryData; + bool _loading = true; +}; + +} // namespace advss diff --git a/plugins/usb/usb-helpers.cpp b/plugins/usb/usb-helpers.cpp new file mode 100644 index 00000000..80190584 --- /dev/null +++ b/plugins/usb/usb-helpers.cpp @@ -0,0 +1,233 @@ +#include "usb-helpers.hpp" +#include "log-helper.hpp" +#include "plugin-state-helpers.hpp" + +#include +#include +#include + +#define LOG_PREFIX "[usb] " + +namespace advss { + +static bool setup(); +static bool setupDone = setup(); +static bool hotplugsAreSupported = false; +static std::mutex mutex; + +static bool setup() +{ + AddPluginInitStep([]() { + libusb_init(NULL); + hotplugsAreSupported = + libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG); + }); + AddPluginCleanupStep([]() { libusb_exit(NULL); }); + return true; +} + +static void logLibusbError(int value, const char *msg) +{ + if (value >= LIBUSB_SUCCESS) { + return; + } + vblog(LOG_WARNING, LOG_PREFIX "%s: %s", msg, libusb_strerror(value)); +} + +static USBDeviceInfo +getDeviceInfo(libusb_device *device, libusb_device_handle *handle, + const struct libusb_device_descriptor &descriptor) +{ + char vendor_name[256] = {}; + char product_name[256] = {}; + char serial_number[256] = {}; + int ret = libusb_get_string_descriptor_ascii(handle, + descriptor.iManufacturer, + (uint8_t *)vendor_name, + sizeof(vendor_name)); + logLibusbError(ret, "Failed to query vendor name"); + ret = libusb_get_string_descriptor_ascii(handle, descriptor.iProduct, + (uint8_t *)product_name, + sizeof(product_name)); + logLibusbError(ret, "Failed to query product name"); + ret = libusb_get_string_descriptor_ascii(handle, + descriptor.iSerialNumber, + (uint8_t *)serial_number, + sizeof(serial_number)); + logLibusbError(ret, "Failed to query serial number"); + + const USBDeviceInfo deviceInfo = { + std::to_string(descriptor.idVendor), + std::to_string(descriptor.idProduct), + std::to_string(libusb_get_bus_number(device)), + std::to_string(libusb_get_device_address(device)), + vendor_name, + product_name, + serial_number}; + return deviceInfo; +} + +static std::vector pollUSBDevices() +{ + libusb_device **devices; + ssize_t count = libusb_get_device_list(NULL, &devices); + if (count < 0) { + logLibusbError(count, "Failed to query device list"); + return {}; + } + + std::vector result; + + for (int i = 0; i < count; i++) { + libusb_device *device = devices[i]; + struct libusb_device_descriptor descriptor; + + int ret = libusb_get_device_descriptor(device, &descriptor); + if (ret != LIBUSB_SUCCESS) { + logLibusbError(ret, "Error getting device descriptor"); + continue; + } + + libusb_device_handle *handle; + ret = libusb_open(device, &handle); + if (ret != LIBUSB_SUCCESS) { + logLibusbError(ret, "Error opening device"); + continue; + } + result.emplace_back(getDeviceInfo(device, handle, descriptor)); + libusb_close(handle); + } + + libusb_free_device_list(devices, 1); + + return result; +} + +static int hotplugCallback(struct libusb_context *ctx, + struct libusb_device *device, + libusb_hotplug_event event, void *user_data) +{ + auto devices = static_cast *>(user_data); + + static libusb_device_handle *handle = nullptr; + struct libusb_device_descriptor descriptor; + + int ret = libusb_get_device_descriptor(device, &descriptor); + logLibusbError(ret, "Error getting device descriptor"); + + ret = libusb_open(device, &handle); + if (ret != LIBUSB_SUCCESS) { + logLibusbError(ret, "Error opening device"); + return 0; + } + const auto deviceInfo = getDeviceInfo(device, handle, descriptor); + + std::lock_guard lock(mutex); + if (LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED == event) { + devices->emplace_back(deviceInfo); + } else if (LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT == event) { + devices->erase(std::find(devices->begin(), devices->end(), + deviceInfo)); + } + + libusb_close(handle); + + return 0; +} + +static std::vector getHotplugBasedDeviceList() +{ + static std::vector devices; + static bool hotplugSetupDone = false; + + if (!hotplugSetupDone) { + hotplugSetupDone = true; + devices = pollUSBDevices(); + + int ret = libusb_hotplug_register_callback( + nullptr, + LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | + LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, + 0, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, + LIBUSB_HOTPLUG_MATCH_ANY, hotplugCallback, &devices, + nullptr); + if (ret != LIBUSB_SUCCESS) { + hotplugsAreSupported = false; + } else { + blog(LOG_WARNING, LOG_PREFIX "hotplug supported!"); + } + } + + std::lock_guard lock(mutex); + return devices; +} + +static std::vector getPollingBasedDeviceList() +{ + // Polling can be very expensive + // Perform this operation at most once every 10 seconds + static const int timeout = 10; + static std::vector deviceList = {}; + static std::chrono::high_resolution_clock::time_point lastUpdate = {}; + + std::lock_guard lock(mutex); + auto now = std::chrono::high_resolution_clock::now(); + if (std::chrono::duration_cast(now - lastUpdate) + .count() >= timeout) { + deviceList = pollUSBDevices(); + lastUpdate = now; + } + + return deviceList; +} + +std::vector GetUSBDevices() +{ + // Hotplug events do not seem to be firing consistently during testing + // on Linux so for now only rely on polling based functionality instead + return getPollingBasedDeviceList(); + + /* + if (hotplugsAreSupported) { + return getHotplugBasedDeviceList(); + } else { + return getPollingBasedDeviceList(); + } + */ +} + +QStringList GetUSBDevicesStringList() +{ + QStringList result; + const auto devices = GetUSBDevices(); + for (const auto &device : devices) { + result << device.ToQString(); + } + return result; +} + +std::string USBDeviceInfo::ToString() const +{ + return "Vendor ID: " + vendorID + "\nProduct ID: " + productID + + "\nBus Number:" + busNumber + + "\nDevice Address:" + deviceAddress + + "\nVendor Name:" + vendorName + "\nProduct Name:" + productName + + "\nSerial Number:" + serialNumber; +} + +QString USBDeviceInfo::ToQString() const +{ + return QString::fromStdString(ToString()); +} + +bool USBDeviceInfo::operator==(const USBDeviceInfo &other) +{ + return vendorID == other.vendorID && productID == other.productID && + busNumber == other.busNumber && + deviceAddress == other.deviceAddress && + vendorName == other.vendorName && + productName == other.productName && + serialNumber == other.serialNumber; +} + +} // namespace advss diff --git a/plugins/usb/usb-helpers.hpp b/plugins/usb/usb-helpers.hpp new file mode 100644 index 00000000..a4041f43 --- /dev/null +++ b/plugins/usb/usb-helpers.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#include + +namespace advss { + +struct USBDeviceInfo { + std::string vendorID; + std::string productID; + std::string busNumber; + std::string deviceAddress; + std::string vendorName; + std::string productName; + std::string serialNumber; + + std::string ToString() const; + QString ToQString() const; + + bool operator==(const USBDeviceInfo &other); +}; + +std::vector GetUSBDevices(); +QStringList GetUSBDevicesStringList(); + +} // namespace advss