From 039e29b97e9ad75354442c23cddeb2e9216b297b Mon Sep 17 00:00:00 2001 From: JoergAtGithub Date: Mon, 27 Dec 2021 21:59:00 +0100 Subject: [PATCH] Fix performance issues by slow hid_write command of HIDAPI, which can block the controller thread for several milliseconds per OutputReport: -Ensure that the ring-buffer of InputReports is polled between hid_write of two OutputReports -Move HID IO in a dedicated thread, this allows the JavaScript controller mapping to continue, while HIDAPI waits for completion of the data transfer of the OutputReport -Skip sending of OutputReports, if the data didn't changed - as already implemented for polled InputReports -Added timing information to --controllerDebug output, that mapping developers can understand the timing behavior --- CMakeLists.txt | 1 + src/controllers/hid/hidcontroller.cpp | 267 +++++++++----------------- src/controllers/hid/hidcontroller.h | 35 ++-- src/controllers/hid/hidio.cpp | 259 +++++++++++++++++++++++++ src/controllers/hid/hidio.h | 72 +++++++ 5 files changed, 440 insertions(+), 194 deletions(-) create mode 100644 src/controllers/hid/hidio.cpp create mode 100644 src/controllers/hid/hidio.h diff --git a/CMakeLists.txt b/CMakeLists.txt index df6024ec0c5d..3fbcc626a3e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2775,6 +2775,7 @@ if(HID) endif() target_sources(mixxx-lib PRIVATE src/controllers/hid/hidcontroller.cpp + src/controllers/hid/hidio.cpp src/controllers/hid/hiddevice.cpp src/controllers/hid/hidenumerator.cpp src/controllers/hid/legacyhidcontrollermapping.cpp diff --git a/src/controllers/hid/hidcontroller.cpp b/src/controllers/hid/hidcontroller.cpp index 5cd6117f0b9b..e13ba14f272e 100644 --- a/src/controllers/hid/hidcontroller.cpp +++ b/src/controllers/hid/hidcontroller.cpp @@ -9,22 +9,18 @@ #include "util/time.h" #include "util/trace.h" -namespace { -constexpr int kReportIdSize = 1; -constexpr int kMaxHidErrorMessageSize = 512; -} // namespace - HidController::HidController( mixxx::hid::DeviceInfo&& deviceInfo) : Controller(deviceInfo.formatName()), m_deviceInfo(std::move(deviceInfo)), - m_pHidDevice(nullptr), - m_pollingBufferIndex(0) { + m_pHidDevice(nullptr) { setDeviceCategory(mixxx::hid::DeviceCategory::guessFromDeviceInfo(m_deviceInfo)); // All HID devices are full-duplex setInputDevice(true); setOutputDevice(true); + + m_pHidIo = nullptr; } HidController::~HidController() { @@ -104,15 +100,52 @@ int HidController::open() { return -1; } - // This isn't strictly necessary but is good practice. - for (int i = 0; i < kNumBuffers; i++) { - memset(m_pPollData[i], 0, kBufferSize); - } - m_lastPollSize = 0; - setOpen(true); startEngine(); + if (m_pHidIo != nullptr) { + qWarning() << "HidIo already present for" << getName(); + } else { + m_pHidIo = new HidIo(m_pHidDevice, + std::move(m_deviceInfo), + m_logBase, + m_logInput, + m_logOutput); + m_pHidIo->setObjectName(QString("HidIo %1").arg(getName())); + + connect(m_pHidIo, + &HidIo::receive, + this, + &HidController::receive, + Qt::QueuedConnection); + + connect(this, + &HidController::getInputReport, + m_pHidIo, + &HidIo::getInputReport, + Qt::DirectConnection); // Enforces syncronisation of mapping and IO thread + + connect(this, + &HidController::sendOutputReport, + m_pHidIo, + &HidIo::sendOutputReport, + Qt::QueuedConnection); + + connect(this, + &HidController::getFeatureReport, + m_pHidIo, + &HidIo::getFeatureReport, + Qt::DirectConnection); // Enforces syncronisation of mapping and IO thread + connect(this, + &HidController::sendFeatureReport, + m_pHidIo, + &HidIo::sendFeatureReport, + Qt::DirectConnection); // Enforces syncronisation of mapping and IO thread + + // Controller input needs to be prioritized since it can affect the + // audio directly, like when scratching + m_pHidIo->start(QThread::HighPriority); + } return 0; } @@ -124,95 +157,60 @@ int HidController::close() { qCInfo(m_logBase) << "Shutting down HID device" << getName(); + // Stop the reading thread + if (m_pHidIo == nullptr) { + qWarning() << "HidIo not present for" << getName() + << "yet the device is open!"; + } else { + disconnect(m_pHidIo, + &HidIo::receive, + this, + &HidController::receive); + + disconnect(this, + &HidController::getInputReport, + m_pHidIo, + &HidIo::getInputReport); + + disconnect(this, + &HidController::sendOutputReport, + m_pHidIo, + &HidIo::sendOutputReport); + + disconnect(this, + &HidController::getFeatureReport, + m_pHidIo, + &HidIo::getFeatureReport); + disconnect(this, + &HidController::sendFeatureReport, + m_pHidIo, + &HidIo::sendFeatureReport); + + m_pHidIo->stop(); + hid_set_nonblocking(m_pHidDevice, 1); // Quit blocking + qDebug() << "Waiting on IO thread to finish"; + m_pHidIo->wait(); + } + // Stop controller engine here to ensure it's done before the device is closed - // in case it has any final parting messages + // in case it has any final parting messages stopEngine(); + if (m_pHidIo != nullptr) { + delete m_pHidIo; + m_pHidIo = nullptr; + } + // Close device qCInfo(m_logBase) << "Closing device"; + // hid_close is not thread safe + // All communication to this hid_device must be completed, before hid_close is called. hid_close(m_pHidDevice); setOpen(false); 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()); -} - -QByteArray HidController::getInputReport(unsigned int reportID) { - Trace hidRead("HidController getInputReport"); - int bytesRead; - - m_pPollData[m_pollingBufferIndex][0] = reportID; - // FIXME: implement upstream for hidraw backend on Linux - // https://github.com/libusb/hidapi/issues/259 - bytesRead = hid_get_input_report(m_pHidDevice, m_pPollData[m_pollingBufferIndex], kBufferSize); - - qCDebug(m_logInput) << 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 QByteArray(); - } - return QByteArray::fromRawData( - reinterpret_cast(m_pPollData[m_pollingBufferIndex]), bytesRead); -} - -bool HidController::poll() { - Trace hidRead("HidController poll"); - - // This loop risks becoming a high priority endless loop in case processing - // the mapping JS code takes longer than the controller polling rate. - // This could stall other low priority tasks. - // There is no safety net for this because it has not been demonstrated to be - // a problem in practice. - while (true) { - 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; - } - processInputReport(bytesRead); - } -} bool HidController::isPolling() const { return isOpen(); @@ -224,96 +222,13 @@ void HidController::sendReport(QList data, unsigned int length, unsigned in foreach (int datum, data) { temp.append(datum); } - sendBytesReport(temp, reportID); + emit sendOutputReport(temp, reportID); } void HidController::sendBytes(const QByteArray& data) { - sendBytesReport(data, 0); -} - -void HidController::sendBytesReport(QByteArray data, unsigned int reportID) { - // Append the Report ID to the beginning of data[] per the API.. - data.prepend(reportID); - - int result = hid_write(m_pHidDevice, (unsigned char*)data.constData(), data.size()); - if (result == -1) { - qCWarning(m_logOutput) << "Unable to send data to" << getName() << ":" - << mixxx::convertWCStringToQString( - hid_error(m_pHidDevice), - kMaxHidErrorMessageSize); - } else { - qCDebug(m_logOutput) << result << "bytes sent to" << getName() - << "serial #" << m_deviceInfo.serialNumber() - << "(including report ID of" << reportID << ")"; - } -} - -void HidController::sendFeatureReport( - const QByteArray& reportData, unsigned int reportID) { - QByteArray dataArray; - dataArray.reserve(kReportIdSize + reportData.size()); - - // Append the Report ID to the beginning of dataArray[] per the API.. - dataArray.append(reportID); - - for (const int datum : reportData) { - dataArray.append(datum); - } - - int result = hid_send_feature_report(m_pHidDevice, - reinterpret_cast(dataArray.constData()), - dataArray.size()); - if (result == -1) { - qCWarning(m_logOutput) << "sendFeatureReport is unable to send data to" - << getName() << "serial #" << m_deviceInfo.serialNumber() - << ":" - << mixxx::convertWCStringToQString( - hid_error(m_pHidDevice), - kMaxHidErrorMessageSize); - } else { - qCDebug(m_logOutput) << result << "bytes sent by sendFeatureReport to" << getName() - << "serial #" << m_deviceInfo.serialNumber() - << "(including report ID of" << reportID << ")"; - } + emit sendOutputReport(data, 0); } ControllerJSProxy* HidController::jsProxy() { return new HidControllerJSProxy(this); } - -QByteArray 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. - qCWarning(m_logInput) << "getFeatureReport is unable to get data from" << getName() - << "serial #" << m_deviceInfo.serialNumber() << ":" - << mixxx::convertWCStringToQString( - hid_error(m_pHidDevice), - kMaxHidErrorMessageSize); - } else { - qCDebug(m_logInput) << 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 compatibility with input array HidController::sendFeatureReport, a reportID prefix is not added here - QByteArray byteArray; - byteArray.reserve(bytesRead - kReportIdSize); - auto featureReportStart = reinterpret_cast(dataRead + kReportIdSize); - return QByteArray(featureReportStart, bytesRead); -} diff --git a/src/controllers/hid/hidcontroller.h b/src/controllers/hid/hidcontroller.h index 2e7a8f22e6e9..46c3ae1c8781 100644 --- a/src/controllers/hid/hidcontroller.h +++ b/src/controllers/hid/hidcontroller.h @@ -1,7 +1,10 @@ #pragma once +#include + #include "controllers/controller.h" #include "controllers/hid/hiddevice.h" +#include "controllers/hid/hidio.h" #include "controllers/hid/legacyhidcontrollermapping.h" #include "util/duration.h" @@ -36,18 +39,10 @@ class HidController final : public Controller { int open() override; int close() override; - bool poll() override; - private: bool isPolling() const override; - void processInputReport(int bytesRead); - - // For devices which only support a single report, reportID must be set to - // 0x0. - void sendBytes(const QByteArray& data) override; - void sendBytesReport(QByteArray data, unsigned int reportID); - void sendFeatureReport(const QByteArray& reportData, unsigned int reportID); + signals: // 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. @@ -57,6 +52,10 @@ class HidController final : public Controller { // function of the common-hid-packet-parser. QByteArray getInputReport(unsigned int reportID); + void sendOutputReport(const QByteArray& data, unsigned int reportID); + + void sendFeatureReport(const QByteArray& reportData, 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. @@ -66,17 +65,17 @@ class HidController final : public Controller { // and sent it back to the controller. QByteArray getFeatureReport(unsigned int reportID); + private: + // For devices which only support a single report, reportID must be set to + // 0x0. + void sendBytes(const QByteArray& data) override; + const mixxx::hid::DeviceInfo m_deviceInfo; + HidIo* m_pHidIo; hid_device* m_pHidDevice; std::shared_ptr m_pMapping; - static constexpr int kNumBuffers = 2; - static constexpr int kBufferSize = 255; - unsigned char m_pPollData[kNumBuffers][kBufferSize]; - int m_lastPollSize; - int m_pollingBufferIndex; - friend class HidControllerJSProxy; }; @@ -98,17 +97,17 @@ class HidControllerJSProxy : public ControllerJSProxy { Q_INVOKABLE QByteArray getInputReport( unsigned int reportID) { - return m_pHidController->getInputReport(reportID); + return emit m_pHidController->getInputReport(reportID); } Q_INVOKABLE void sendFeatureReport( const QByteArray& reportData, unsigned int reportID) { - m_pHidController->sendFeatureReport(reportData, reportID); + emit m_pHidController->sendFeatureReport(reportData, reportID); } Q_INVOKABLE QByteArray getFeatureReport( unsigned int reportID) { - return m_pHidController->getFeatureReport(reportID); + return emit m_pHidController->getFeatureReport(reportID); } private: diff --git a/src/controllers/hid/hidio.cpp b/src/controllers/hid/hidio.cpp new file mode 100644 index 000000000000..2c5f4b01778a --- /dev/null +++ b/src/controllers/hid/hidio.cpp @@ -0,0 +1,259 @@ +#include "controllers/hid/hidio.h" + +#include + +#include "controllers/defs_controllers.h" +#include "controllers/hid/legacyhidcontrollermappingfilehandler.h" +#include "moc_hidio.cpp" +#include "util/string.h" +#include "util/time.h" +#include "util/trace.h" + +namespace { +constexpr int kReportIdSize = 1; +constexpr int kMaxHidErrorMessageSize = 512; +} // namespace + +HidIoReport::HidIoReport(const unsigned char& reportId, + hid_device* device, + const mixxx::hid::DeviceInfo&& deviceInfo, + const RuntimeLoggingCategory& logOutput) + : m_reportId(reportId), + m_pHidDevice(device), + m_deviceInfo(std::move(deviceInfo)), + m_logOutput(logOutput), + m_lastSentOutputreport("") { +} + +HidIoReport::~HidIoReport() { +} + +void HidIoReport::sendOutputReport(QByteArray data) { + auto startOfHidWrite = mixxx::Time::elapsed(); + if (!m_lastSentOutputreport.compare(data)) { + qCDebug(m_logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() + << " Skipped identical Output Report for" << m_deviceInfo.formatName() + << "serial #" << m_deviceInfo.serialNumberRaw() + << "(Report ID" << m_reportId << ")"; + return; // Same data sent last time + } + m_lastSentOutputreport.clear(); + m_lastSentOutputreport.append(data); + + // Prepend the Report ID to the beginning of data[] per the API.. + data.prepend(m_reportId); + + // hid_write can take several milliseconds, because hidapi synchronizes the asyncron HID communication from the OS + int result = hid_write(m_pHidDevice, (unsigned char*)data.constData(), data.size()); + if (result == -1) { + qCWarning(m_logOutput) << "Unable to send data to" << m_deviceInfo.formatName() << ":" + << mixxx::convertWCStringToQString( + hid_error(m_pHidDevice), + kMaxHidErrorMessageSize); + } else { + qCDebug(m_logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() << " " + << result << "bytes sent to" << m_deviceInfo.formatName() + << "serial #" << m_deviceInfo.serialNumberRaw() + << "(including report ID of" << m_reportId << ") - Needed: " + << (mixxx::Time::elapsed() - startOfHidWrite).formatMicrosWithUnit(); + } +} + +HidIo::HidIo(hid_device* device, + const mixxx::hid::DeviceInfo&& deviceInfo, + const RuntimeLoggingCategory& logBase, + const RuntimeLoggingCategory& logInput, + const RuntimeLoggingCategory& logOutput) + : QThread(), + m_pollingBufferIndex(0), + m_logBase(logBase), + m_logInput(logInput), + m_logOutput(logOutput), + m_pHidDevice(device), + m_deviceInfo(std::move(deviceInfo)) { + // This isn't strictly necessary but is good practice. + for (int i = 0; i < kNumBuffers; i++) { + memset(m_pPollData[i], 0, kBufferSize); + } + m_lastPollSize = 0; +} + +HidIo::~HidIo() { +} + +void HidIo::run() { + m_stop = 0; + while (atomicLoadRelaxed(m_stop) == 0) { + poll(); + usleep(1000); + } +} + +void HidIo::poll() { + Trace hidRead("HidIo poll"); + + // This loop risks becoming a high priority endless loop in case processing + // the mapping JS code takes longer than the controller polling rate. + // This could stall other low priority tasks. + // There is no safety net for this because it has not been demonstrated to be + // a problem in practice. + while (true) { + 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); + break; + } else if (bytesRead == 0) { + // No packet was available to be read + break; + } + processInputReport(bytesRead); + } +} + +void HidIo::processInputReport(int bytesRead) { + Trace process("HidIO 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 + emit receive(incomingData, mixxx::Time::elapsed()); +} + +QByteArray HidIo::getInputReport(unsigned int reportID) { + auto startOfHidGetInputReport = mixxx::Time::elapsed(); + int bytesRead; + + m_pPollData[m_pollingBufferIndex][0] = reportID; + // FIXME: implement upstream for hidraw backend on Linux + // https://github.com/libusb/hidapi/issues/259 + bytesRead = hid_get_input_report(m_pHidDevice, m_pPollData[m_pollingBufferIndex], kBufferSize); + qCDebug(m_logInput) << bytesRead << "bytes received by hid_get_input_report" + << m_deviceInfo.formatName() << "serial #" + << m_deviceInfo.serialNumber() + << "(including one byte for the report ID:" + << QString::number(static_cast(reportID), 16) + .toUpper() + .rightJustified(2, QChar('0')) + << ") - Needed: " + << (mixxx::Time::elapsed() - startOfHidGetInputReport) + .formatMicrosWithUnit(); + + 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 QByteArray(); + } + + return QByteArray::fromRawData( + reinterpret_cast(m_pPollData[m_pollingBufferIndex]), bytesRead); +} + +void HidIo::sendOutputReport(const QByteArray& data, unsigned int reportID) { + if (m_outputReports.find(reportID) == m_outputReports.end()) { + std::unique_ptr pNewOutputReport; + m_outputReports[reportID] = std::make_unique( + reportID, m_pHidDevice, std::move(m_deviceInfo), m_logOutput); + } + // SendOutputReports executes a hardware operation, which take several milliseconds + m_outputReports[reportID]->sendOutputReport(data); + + // Ensure that all InputReports are read from the ring buffer, before the next OutputReport blocks the IO again + poll(); // Polling available Input-Reports is a cheap software only operation, which takes insignificiant time +} + +void HidIo::sendFeatureReport( + const QByteArray& reportData, unsigned int reportID) { + auto startOfHidSendFeatureReport = mixxx::Time::elapsed(); + QByteArray dataArray; + dataArray.reserve(kReportIdSize + reportData.size()); + + // Append the Report ID to the beginning of dataArray[] per the API.. + dataArray.append(reportID); + + for (const int datum : reportData) { + dataArray.append(datum); + } + + int result = hid_send_feature_report(m_pHidDevice, + reinterpret_cast(dataArray.constData()), + dataArray.size()); + if (result == -1) { + qCWarning(m_logOutput) + << "sendFeatureReport is unable to send data to" + << m_deviceInfo.formatName() << "serial #" + << m_deviceInfo.serialNumber() << ":" + << mixxx::convertWCStringToQString( + hid_error(m_pHidDevice), kMaxHidErrorMessageSize); + } else { + qCDebug(m_logOutput) + << result << "bytes sent by sendFeatureReport to" + << m_deviceInfo.formatName() << "serial #" + << m_deviceInfo.serialNumber() << "(including report ID of" + << reportID << ") - Needed: " + << (mixxx::Time::elapsed() - startOfHidSendFeatureReport) + .formatMicrosWithUnit(); + } +} + +QByteArray HidIo::getFeatureReport( + unsigned int reportID) { + auto startOfHidGetFeatureReport = mixxx::Time::elapsed(); + 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. + qCWarning(m_logInput) + << "getFeatureReport is unable to get data from" + << m_deviceInfo.formatName() << "serial #" + << m_deviceInfo.serialNumber() << ":" + << mixxx::convertWCStringToQString( + hid_error(m_pHidDevice), kMaxHidErrorMessageSize); + } else { + qCDebug(m_logInput) + << bytesRead << "bytes received by getFeatureReport from" + << m_deviceInfo.formatName() << "serial #" + << m_deviceInfo.serialNumber() + << "(including one byte for the report ID:" + << QString::number(static_cast(reportID), 16) + .toUpper() + .rightJustified(2, QChar('0')) + << ") - Needed: " + << (mixxx::Time::elapsed() - startOfHidGetFeatureReport) + .formatMicrosWithUnit(); + } + + // Convert array of bytes read in a JavaScript compatible return type + // For compatibility with input array HidController::sendFeatureReport, a reportID prefix is not added here + QByteArray byteArray; + byteArray.reserve(bytesRead - kReportIdSize); + const auto* const featureReportStart = reinterpret_cast(dataRead + kReportIdSize); + return QByteArray(featureReportStart, bytesRead); +} diff --git a/src/controllers/hid/hidio.h b/src/controllers/hid/hidio.h new file mode 100644 index 000000000000..ceda19df2ef0 --- /dev/null +++ b/src/controllers/hid/hidio.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include "controllers/controller.h" +#include "controllers/hid/hiddevice.h" +#include "util/compatibility/qatomic.h" +#include "util/duration.h" + +class HidIoReport : public QObject { + Q_OBJECT + public: + HidIoReport(const unsigned char& reportId, + hid_device* device, + const mixxx::hid::DeviceInfo&& deviceInfo, + const RuntimeLoggingCategory& logOutput); + ~HidIoReport(); + void sendOutputReport(QByteArray data); + + private: + const unsigned char m_reportId; + hid_device* m_pHidDevice; + const mixxx::hid::DeviceInfo m_deviceInfo; + const RuntimeLoggingCategory m_logOutput; + QByteArray m_lastSentOutputreport; +}; + +class HidIo : public QThread { + Q_OBJECT + public: + HidIo(hid_device* device, + const mixxx::hid::DeviceInfo&& deviceInfo, + const RuntimeLoggingCategory& logBase, + const RuntimeLoggingCategory& logInput, + const RuntimeLoggingCategory& logOutput); + ~HidIo(); + + void stop() { + m_stop = 1; + } + + static constexpr int kNumBuffers = 2; + static constexpr int kBufferSize = 255; + unsigned char m_pPollData[kNumBuffers][kBufferSize]; + int m_lastPollSize; + int m_pollingBufferIndex; + const RuntimeLoggingCategory m_logBase; + const RuntimeLoggingCategory m_logInput; + const RuntimeLoggingCategory m_logOutput; + + signals: + /// Signals that a HID InputReport received by Interrupt triggered from HID device + void receive(const QByteArray& data, mixxx::Duration timestamp); + + public slots: + QByteArray getInputReport(unsigned int reportID); + void sendOutputReport(const QByteArray& reportData, unsigned int reportID); + void sendFeatureReport(const QByteArray& reportData, unsigned int reportID); + QByteArray getFeatureReport(unsigned int reportID); + + protected: + void run(); + + private: + void poll(); + void processInputReport(int bytesRead); + hid_device* m_pHidDevice; + const mixxx::hid::DeviceInfo m_deviceInfo; + QAtomicInt m_stop; + std::map> m_outputReports; +};