diff --git a/CMakeLists.txt b/CMakeLists.txt index 06f9abffb8ae..f45521882658 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2530,7 +2530,12 @@ find_package(LibUSB) # USB HID controller support find_package(hidapi) option(HID "USB HID controller support" ON) -cmake_dependent_option(HIDAPI_STATIC "Link HIDAPI library statically" OFF "HIDAPI_FOUND" ON) + +if(hidapi_VERSION VERSION_LESS "0.10.0") + set(HIDAPI_STATIC ON) +else() + cmake_dependent_option(HIDAPI_STATIC "Link HIDAPI library statically" OFF "HIDAPI_FOUND" ON) +endif() if(HID) target_sources(mixxx-lib PRIVATE src/controllers/hid/hidcontroller.cpp diff --git a/cmake/modules/Findhidapi.cmake b/cmake/modules/Findhidapi.cmake index 89847675831b..f1ca6ab36366 100644 --- a/cmake/modules/Findhidapi.cmake +++ b/cmake/modules/Findhidapi.cmake @@ -70,6 +70,18 @@ find_package_handle_standard_args( hidapi_INCLUDE_DIR ) +# Version detection +if (EXISTS "${hidapi_INCLUDE_DIR}/hidapi.h") + file(READ "${hidapi_INCLUDE_DIR}/hidapi.h" hidapi_H_CONTENTS) + string(REGEX MATCH "#define HID_API_VERSION_MAJOR ([0-9]+)" _dummy "${hidapi_H_CONTENTS}") + set(hidapi_VERSION_MAJOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define HID_API_VERSION_MINOR ([0-9]+)" _dummy "${hidapi_H_CONTENTS}") + set(hidapi_VERSION_MINOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define HID_API_VERSION_PATCH ([0-9]+)" _dummy "${hidapi_H_CONTENTS}") + set(hidapi_VERSION_PATCH "${CMAKE_MATCH_1}") + set(hidapi_VERSION "${hidapi_VERSION_MAJOR}.${hidapi_VERSION_MINOR}.${hidapi_VERSION_PATCH}") +endif () + if(hidapi_FOUND) set(hidapi_LIBRARIES "${hidapi_LIBRARY}") set(hidapi_INCLUDE_DIRS "${hidapi_INCLUDE_DIR}") diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index ade00f29a8e4..c48daf2fa140 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -125,7 +125,8 @@ void Controller::receive(const QByteArray& data, mixxx::Duration timestamp) { } else { spacer = QStringLiteral(" "); } - message += QString::number(data.at(i), 16) + // cast to quint8 to avoid that negative chars are for instance displayed as ffffffff instead of the desired ff + message += QString::number(static_cast(data.at(i)), 16) .toUpper() .rightJustified(2, QChar('0')) + spacer; diff --git a/src/controllers/hid/hidcontroller.cpp b/src/controllers/hid/hidcontroller.cpp index 8ac1a2b29a44..9126db1bac83 100644 --- a/src/controllers/hid/hidcontroller.cpp +++ b/src/controllers/hid/hidcontroller.cpp @@ -19,7 +19,7 @@ HidController::HidController( mixxx::hid::DeviceInfo&& deviceInfo) : m_deviceInfo(std::move(deviceInfo)), m_pHidDevice(nullptr), - m_iPollingBufferIndex(0) { + m_pollingBufferIndex(0) { setDeviceCategory(mixxx::hid::DeviceCategory::guessFromDeviceInfo(m_deviceInfo)); setDeviceName(m_deviceInfo.formatName()); @@ -109,7 +109,7 @@ int HidController::open() { for (int i = 0; i < kNumBuffers; i++) { memset(m_pPollData[i], 0, kBufferSize); } - m_iLastPollSize = 0; + m_lastPollSize = 0; setOpen(true); startEngine(); @@ -136,6 +136,67 @@ int HidController::close() { return 0; } +void HidController::processInputReport(int bytesRead) { + Trace process("HidController processInputReport"); + unsigned char* pPreviousBuffer = m_pPollData[(m_pollingBufferIndex + 1) % kNumBuffers]; + unsigned char* pCurrentBuffer = m_pPollData[m_pollingBufferIndex]; + // Some controllers such as the Gemini GMX continuously send input reports even if it + // is identical to the previous send input report. If this loop processed all those redundant + // input report, it would be a big performance problem to run JS code for every input report and + // would be unnecessary. + // This assumes that the redundant input report all use the same report ID. In practice we + // have not encountered any controllers that send redundant input report with different report + // IDs. If any such devices exist, this may be changed to use a separate buffer to store + // the last input report for each report ID. + if (bytesRead == m_lastPollSize && + memcmp(pCurrentBuffer, pPreviousBuffer, bytesRead) == 0) { + return; + } + // Cycle between buffers so the memcmp above does not require deep copying to another buffer. + m_pollingBufferIndex = (m_pollingBufferIndex + 1) % kNumBuffers; + m_lastPollSize = bytesRead; + auto incomingData = QByteArray::fromRawData( + reinterpret_cast(pCurrentBuffer), bytesRead); + + // Execute callback function in JavaScript mapping + // and print to stdout in case of --controllerDebug + receive(incomingData, mixxx::Time::elapsed()); +} + +QList HidController::getInputReport(unsigned int reportID) { + Trace hidRead("HidController getInputReport"); + int bytesRead; + + m_pPollData[m_pollingBufferIndex][0] = reportID; + bytesRead = hid_get_input_report(m_pHidDevice, m_pPollData[m_pollingBufferIndex], kBufferSize); + + controllerDebug(bytesRead + << "bytes received by hid_get_input_report" << getName() + << "serial #" << m_deviceInfo.serialNumber() + << "(including one byte for the report ID:" + << QString::number(static_cast(reportID), 16) + .toUpper() + .rightJustified(2, QChar('0')) + << ")"); + + if (bytesRead <= kReportIdSize) { + // -1 is the only error value according to hidapi documentation. + // Otherwise minimum possible value is 1, because 1 byte is for the reportID, + // the smallest report with data is therefore 2 bytes. + DEBUG_ASSERT(bytesRead <= kReportIdSize); + return QList(); + } + + // Convert array of bytes read in a JavaScript compatible return type + // For compatibilty with the array provided by HidController::poll the reportID is contained as prefix + QList dataList; + dataList.reserve(bytesRead); + for (int i = 0; i < bytesRead; i++) { + dataList.append(m_pPollData[m_pollingBufferIndex][i]); + } + return dataList; +} + bool HidController::poll() { Trace hidRead("HidController poll"); @@ -145,38 +206,16 @@ bool HidController::poll() { // There is no safety net for this because it has not been demonstrated to be // a problem in practice. while (true) { - // Cycle between buffers so the memcmp below does not require deep copying to another buffer. - unsigned char* pPreviousBuffer = m_pPollData[m_iPollingBufferIndex]; - const int currentBufferIndex = (m_iPollingBufferIndex + 1) % kNumBuffers; - unsigned char* pCurrentBuffer = m_pPollData[currentBufferIndex]; - - int bytesRead = hid_read(m_pHidDevice, pCurrentBuffer, kBufferSize); + int bytesRead = hid_read(m_pHidDevice, m_pPollData[m_pollingBufferIndex], kBufferSize); if (bytesRead < 0) { // -1 is the only error value according to hidapi documentation. DEBUG_ASSERT(bytesRead == -1); return false; } else if (bytesRead == 0) { + // No packet was available to be read return true; } - - Trace process("HidController process packet"); - // Some controllers such as the Gemini GMX continuously send input packets even if it - // is identical to the previous packet. If this loop processed all those redundant - // packets, it would be a big performance problem to run JS code for every packet and - // would be unnecessary. - // This assumes that the redundant packets all use the same report ID. In practice we - // have not encountered any controllers that send redundant packets with different report - // IDs. If any such devices exist, this may be changed to use a separate buffer to store - // the last packet for each report ID. - if (bytesRead == m_iLastPollSize && - memcmp(pCurrentBuffer, pPreviousBuffer, bytesRead) == 0) { - continue; - } - m_iLastPollSize = bytesRead; - m_iPollingBufferIndex = currentBufferIndex; - auto incomingData = QByteArray::fromRawData( - reinterpret_cast(pCurrentBuffer), bytesRead); - receive(incomingData, mixxx::Time::elapsed()); + processInputReport(bytesRead); } } @@ -254,3 +293,42 @@ void HidController::sendFeatureReport( ControllerJSProxy* HidController::jsProxy() { return new HidControllerJSProxy(this); } + +QList HidController::getFeatureReport( + unsigned int reportID) { + unsigned char dataRead[kReportIdSize + kBufferSize]; + dataRead[0] = reportID; + + int bytesRead; + bytesRead = hid_get_feature_report(m_pHidDevice, + dataRead, + kReportIdSize + kBufferSize); + if (bytesRead <= kReportIdSize) { + // -1 is the only error value according to hidapi documentation. + // Otherwise minimum possible value is 1, because 1 byte is for the reportID, + // the smallest report with data is therefore 2 bytes. + qWarning() << "getFeatureReport is unable to get data from" << getName() + << "serial #" << m_deviceInfo.serialNumber() << ":" + << mixxx::convertWCStringToQString( + hid_error(m_pHidDevice), + kMaxHidErrorMessageSize); + } else { + controllerDebug(bytesRead + << "bytes received by getFeatureReport from" << getName() + << "serial #" << m_deviceInfo.serialNumber() + << "(including one byte for the report ID:" + << QString::number(static_cast(reportID), 16) + .toUpper() + .rightJustified(2, QChar('0')) + << ")") + } + + // Convert array of bytes read in a JavaScript compatible return type + // For compatibilty with input array HidController::sendFeatureReport, a reportID prefix is not added here + QList dataList; + dataList.reserve(bytesRead - kReportIdSize); + for (int i = kReportIdSize; i < bytesRead; i++) { + dataList.append(dataRead[i]); + } + return dataList; +} diff --git a/src/controllers/hid/hidcontroller.h b/src/controllers/hid/hidcontroller.h index e0ec809cfe24..f84463302b26 100644 --- a/src/controllers/hid/hidcontroller.h +++ b/src/controllers/hid/hidcontroller.h @@ -40,6 +40,7 @@ class HidController final : public Controller { private: bool isPolling() const override; + void processInputReport(int bytesRead); // For devices which only support a single report, reportID must be set to // 0x0. @@ -47,6 +48,24 @@ class HidController final : public Controller { void sendBytesReport(QByteArray data, unsigned int reportID); void sendFeatureReport(const QList& dataList, unsigned int reportID); + // getInputReport receives an input report on request. + // This can be used on startup to initialize the knob positions in Mixxx + // to the physical position of the hardware knobs on the controller. + // The returned data structure for the input reports is the same + // as in the polling functionality (including ReportID in first byte). + // The returned list can be used to call the incomingData + // function of the common-hid-packet-parser. + QList getInputReport(unsigned int reportID); + + // getFeatureReport receives a feature reports on request. + // HID doesn't support polling feature reports, therefore this is the + // only method to get this information. + // Usually, single bits in a feature report need to be set without + // changing the other bits. The returned list matches the input + // format of sendFeatureReport, allowing it to be read, modified + // and sent it back to the controller. + QList getFeatureReport(unsigned int reportID); + const mixxx::hid::DeviceInfo m_deviceInfo; hid_device* m_pHidDevice; @@ -55,8 +74,8 @@ class HidController final : public Controller { static constexpr int kNumBuffers = 2; static constexpr int kBufferSize = 255; unsigned char m_pPollData[kNumBuffers][kBufferSize]; - int m_iLastPollSize; - int m_iPollingBufferIndex; + int m_lastPollSize; + int m_pollingBufferIndex; friend class HidControllerJSProxy; }; @@ -77,11 +96,21 @@ class HidControllerJSProxy : public ControllerJSProxy { m_pHidController->sendReport(data, length, reportID); } + Q_INVOKABLE QList getInputReport( + unsigned int reportID) { + return m_pHidController->getInputReport(reportID); + } + Q_INVOKABLE void sendFeatureReport( const QList& dataList, unsigned int reportID) { m_pHidController->sendFeatureReport(dataList, reportID); } + Q_INVOKABLE QList getFeatureReport( + unsigned int reportID) { + return m_pHidController->getFeatureReport(reportID); + } + private: HidController* m_pHidController; };