Skip to content

Commit

Permalink
Merge pull request #3317 from JoergAtGithub/add_HID_readback
Browse files Browse the repository at this point in the history
HID functions to read back the status of an HID  controller (like MIDI SYSEX)
  • Loading branch information
Be-ing authored Feb 21, 2021
2 parents 5cb823e + 54a1f05 commit 3d16052
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 31 deletions.
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions cmake/modules/Findhidapi.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<quint8>(data.at(i)), 16)
.toUpper()
.rightJustified(2, QChar('0')) +
spacer;
Expand Down
132 changes: 105 additions & 27 deletions src/controllers/hid/hidcontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -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();
Expand All @@ -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<char*>(pCurrentBuffer), bytesRead);

// Execute callback function in JavaScript mapping
// and print to stdout in case of --controllerDebug
receive(incomingData, mixxx::Time::elapsed());
}

QList<int> 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<quint8>(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<int>();
}

// 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<int> 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");

Expand All @@ -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<char*>(pCurrentBuffer), bytesRead);
receive(incomingData, mixxx::Time::elapsed());
processInputReport(bytesRead);
}
}

Expand Down Expand Up @@ -254,3 +293,42 @@ void HidController::sendFeatureReport(
ControllerJSProxy* HidController::jsProxy() {
return new HidControllerJSProxy(this);
}

QList<int> 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<quint8>(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<int> dataList;
dataList.reserve(bytesRead - kReportIdSize);
for (int i = kReportIdSize; i < bytesRead; i++) {
dataList.append(dataRead[i]);
}
return dataList;
}
33 changes: 31 additions & 2 deletions src/controllers/hid/hidcontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,32 @@ 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.
void sendBytes(const QByteArray& data) override;
void sendBytesReport(QByteArray data, unsigned int reportID);
void sendFeatureReport(const QList<int>& 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<int> 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<int> getFeatureReport(unsigned int reportID);

const mixxx::hid::DeviceInfo m_deviceInfo;

hid_device* m_pHidDevice;
Expand All @@ -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;
};
Expand All @@ -77,11 +96,21 @@ class HidControllerJSProxy : public ControllerJSProxy {
m_pHidController->sendReport(data, length, reportID);
}

Q_INVOKABLE QList<int> getInputReport(
unsigned int reportID) {
return m_pHidController->getInputReport(reportID);
}

Q_INVOKABLE void sendFeatureReport(
const QList<int>& dataList, unsigned int reportID) {
m_pHidController->sendFeatureReport(dataList, reportID);
}

Q_INVOKABLE QList<int> getFeatureReport(
unsigned int reportID) {
return m_pHidController->getFeatureReport(reportID);
}

private:
HidController* m_pHidController;
};

0 comments on commit 3d16052

Please sign in to comment.