HID-Enumeration
Was jetzt kommt, hasse ich: Nämlich ein großer, großer Block Quelltext, den ich nicht durch viel mehr erklären kann als durch
„weil es nunmal so ist“. Man muss eben alle diese Funktionen in dieser Reihenfolge auf diese Art aufrufen weil es keinen anderen Weg gibt, an die HIDs zu kommen. Füllen wir als
HID.cpp:
#include "HID.hpp"
#include <dinput.h>
#include <dinputd.h>
#include <cassert>
Wo wir gerade dabei sind: Bitte fügt den Abhängigkeiten des Projekts
hid.lib aus dem Windows Driver Kit hinzu!
Unser erster Anlaufpunkt ist
GetRawInputDeviceList(). Diese Funktion spuckt uns eine Liste aller Mäuse, Tastaturen, und HIDs aus, die derzeit an das System angeschlossen sind. Ausgabe ist eine Liste von
RAWINPUTDEVICELIST-Datensätzen, die in ihrem
hDevice-Attribut das Raw Input-Handle des Geräts aufführen.
std::vector<HID *> theHIDs;
void enumerateHIDs() {
assert("enumerateHIDs() must be called only once" && theHIDs.empty());
// Erster Schritt: abfragen, wie viele HIDs angeschlossen sind.
UINT expectedNumberOfHIDs = 0;
if(0 != GetRawInputDeviceList(nullptr, &expectedNumberOfHIDs, sizeof(RAWINPUTDEVICELIST))) {
return; // Keine HIDs angeschlossen oder kein Raw Input verfügbar!
}
// Zweiter Schritt: HID-Beschreibungen abfragen.
std::vector<RAWINPUTDEVICELIST> hidDescriptors(expectedNumberOfHIDs);
if(expectedNumberOfHIDs != GetRawInputDeviceList(&*hidDescriptors.begin(), &expectedNumberOfHIDs, sizeof(RAWINPUTDEVICELIST))) {
return; // Fehler!
}
// Die enumerierten HIDs initialisieren. Dabei kann von unbekannten Achsen und Knöpfen über kaputte Registry-Einträge bis
// hin zu Treiberproblemen alles mögliche schiefgehen, darum sollte jeder Durchlauf isoliert stattfinden.
for(auto const & hidDescriptor : hidDescriptors) {
try {
if(RIM_TYPEHID == hidDescriptor.dwType) { // uns interessieren nur Mäuse, Joysticks, usw.
theHIDs.push_back(new HID(hidDescriptor.hDevice));
}
} catch(char const * error) { // So schmeißen wir unsere Fehlermeldungen
OutputDebugStringA(error);
// Mit dem nächsten Gerät fortfahren
} catch(...) { // Pokémon Programming: Gotta catch 'em all!
// Mit dem nächsten Gerät fortfahren
}
}
}
Ein wesentlich dickerer Brocken ist der
HID-Konstruktor. Dort müssen wir folgendes erledigen:
- Das Handle erhalten, über das der NT-Kernel mit dem Gerät kommuniziert.
- Herstellername und Produktbezeichnung des Geräts abfragen.
- Das Input Report Protocol des Geräts abfragen und speichern.
- Alle Achsen enumerieren.
- Namen und DirectInput-Kalibrierungsinformationen für diese Achse aus der Registry laden.
- DirectInput spricht die HIDs nicht – wie wir es tun – über Usage Page und Usage an, sondern über Indizes: Achse 0; Achse 1; …. Es werden sieben Achsen unterstützt (das ist ja Microsofts Argument, auf XInput umzusteigen: Der XBox-Controller hat zu viele Achsen für DirectInput!); und wenn DirectInput in der Kalibrierung von der vierten Achse spricht, wissen wir nicht, ob das die Z-Achse, das Ruder, das Gaspedal, oder die X-Rotationsachse ist. Das ist knifflig, aber ich erkläre es genau, sobald wir am zuständigen Quelltext arbeiten.
- Alle Knöpfe enumerieren. (Auch hier müssen wir wieder DirectInput-Zuordnungen berücksichtigen.)
- Logging, damit Debugging nicht komplett zur Hölle wird.
Also beginnen wir mit dem größten Brocken dieses Tutorials: Dem Konstruktor der
HID-Klasse in
HID.cpp.
HID::HID(
HANDLE const itsRawInputHandle
)
: rawInputHandle(itsRawInputHandle)
{
// Jeder folgende Quelltext kommt hierher …
}
Unter Windows sind auch Geräte als Dateien realisiert, die man öffnen und schließen kann. Wenn wir an das Handle kommen wollen, mit dem der NT-Kernel das HID ansteuert, müssen wir das durch einen Dateipfad tun. Den liefert uns die Raw Input API durch
GetRawInputDeviceInfo() mit
RIDI_DEVICENAME.
Pfade können (inklusive abschließender Null) eine Maximallänge von 260 Buchstaben haben (
MAX_PATH). Da wir die Unicode-Version benutzen, könnte dem Pfad ein
\\?\ vorangestellt sein. Die Maximallänge des Gerätepfads inklusive Null ist also 264 Buchstaben:
wchar_t pathBuffer[260 + 4];
auto pathsLength = UINT(sizeof pathBuffer / sizeof pathBuffer[0]);
pathsLength = GetRawInputDeviceInfoW(itsRawInputHandle, RIDI_DEVICENAME, pathBuffer, &pathsLength);
if(sizeof pathBuffer / sizeof pathBuffer[0] < pathsLength) { // Negative
UINTs werden sehr groß positiv
throw "invalid HID: could not retrieve its path";
}
Jetzt bekommen wir es mit einem Windows XP-spezifischen Problem zu tun: Der zweite Buchstabe des Pfads ist manchmal
? statt
\. Wer sich für die Gründe interessiert, kann sich die StackOverflow-Frage
GetRawInputDeviceInfo returns wrong syntax of USB HID device name in Windows XP durchlesen.
pathBuffer[1] = L'\\';
Dann kann das Kernel-Handle des HIDs geöffnet werden:
ntHandle = CreateFileW(
pathBuffer, 0u,
FILE_SHARE_READ | FILE_SHARE_WRITE, // wir können anderen Prozessen nicht verbieten, das HID zu benutzen
nullptr,
OPEN_EXISTING,
0u, nullptr
);
if(INVALID_HANDLE_VALUE == ntHandle) {
throw "invalid HID: could not open its handle";
}
Jetzt fragen wir die HID API nach dem Namen des Geräts – falls irgendwas schief läuft (und dafür kommen noch genügend Möglichkeiten!), wissen Programmierer und Benutzer zumindest, welches Gerät schuld ist.
Fehler sehe ich dabei als nicht kritisch an: Man kann ein Gerät auch benutzen, wenn man nicht weiß, wie es heißt. Falls ihr das anders seht, werft eine Ausnahme (aber vergesst nicht, vorher
ntHandle freizugeben!).
Wer wissen will, warum ich hier mit einem Array von 255 Buchstaben arbeite, muss die Deklaration von
name im vorherigen Post nachlesen.
{
wchar_t nameBuffer[255];
if(FALSE == HidD_GetManufacturerString(ntHandle, nameBuffer, 127)) {
wcscpy(nameBuffer, L"(unknown)");
} else {
auto manufacturerLength = wcslen(nameBuffer);
nameBuffer[manufacturerLength++] = ' ';
HidD_GetProductString(ntHandle, nameBuffer + manufacturerLength, ULONG(255 - manufacturerLength));
}
name = nameBuffer;
// Hier solltet ihr jetzt den Namen loggen oder so …
}
Okayokay; die leichtesten Sachen haben wir hinter uns. Nun wird es etwas schwieriger. Damit wir im Fehlerfall das bereits offene Kernel-Handle wieder freigeben, beginnen wir nun einen
try-
catch-Block:
try {
// Jeder folgende Quelltext kommt hierher …
} catch(...) {
CloseHandle(ntHandle);
throw;
}
Als nächstes greifen wir uns das Input Report Protocol des HIDs durch
HidD_GetPreparsedData():
if(FALSE == HidD_GetPreparsedData(ntHandle, &toInputReportProtocol)) {
throw "invalid HID: no input report protocol";
}
Weil diese Daten von der HID API allokiert werden und nach ihrer Benutzung wieder freigegeben werden müssen, kommt der Rest in einen weiteren (aber in den letzten!)
try-
catch-Block:
try {
// Jeder folgende Quelltext kommt hierher …
} catch(...) {
HidD_FreePreparsedData(toInputReportProtocol);
throw;
}
Nun können wir endlich
HidP_GetCaps() aufrufen: Das füllt uns eine
HIDP_CAPS-Instanz, die uns mitteilt, wie viele Klassen von Achsen und Knöpfen das HID anbietet; aber auch, wie groß der Datenblock ist, den wir pro Input Report zu erwarten haben:
HIDP_CAPS capabilities;
if(HIDP_STATUS_SUCCESS != HidP_GetCaps(toInputReportProtocol, &capabilities)) {
throw "invalid HID: no capabilities";
}
sizeOfInputReport = sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + capabilities.InputReportByteLength;
Nun holen wir uns alle Klassen von Achsen und Knöpfen:
std::vector<HIDP_BUTTON_CAPS> buttonClasses(capabilities.NumberInputButtonCaps);
if(HIDP_STATUS_SUCCESS != HidP_GetButtonCaps(
HidP_Input,
&*buttonClasses.begin(), &capabilities.NumberInputButtonCaps,
toInputReportProtocol
)) {
throw "invalid HID: could not retrieve its button classes";
}
std::vector<HIDP_VALUE_CAPS> axisClasses(capabilities.NumberInputValueCaps);
if(HIDP_STATUS_SUCCESS != HidP_GetValueCaps(
HidP_Input,
&*axisClasses.begin(), &capabilities.NumberInputValueCaps,
toInputReportProtocol
)) {
throw "invalid HID: could not retrieve its axis classes";
}
Jede dieser Klassen kann eine oder mehrere Usages abdecken. Dafür stellt die HID API zwei getrennte Datenstrukturen bereit:
HIDP_VALUE_CAPS stellt etwa ein Attribut
BOOLEAN IsRange; und falls das
TRUE ist, muss man alles weitere aus der Unterstruktur
Range lesen, sonst aus
NotRange. Wir werden nun über alle Klassen iterieren und alle zu Ranges (Ende inklusiv!) konvertieren. An jeder Stelle, wo wir die Klassen verarbeiten, wird dann aus
if(IsRange) {
for(auto i = Range.UsageMin; i <= Range.UsageMax; ++i) {
doSomething(i);
}
} else {
doSomething(NotRange.Usage);
}
das viel einfachere
for(auto i = Range.UsageMin; i <= Range.UsageMax; ++i) {
doSomething(i);
}
und spart uns eine riesige Menge Quelltext. Wir kombinieren das damit, die tatsächliche Anzahl von Knöpfen und Achsen zu zählen:
size_t numberOfButtons = 0;
for(auto & currentClass : buttonClasses) {
if(currentClass.IsRange) {
numberOfButtons += currentClass.Range.UsageMax - currentClass.Range.UsageMin + 1u;
} else {
currentClass.Range.UsageMin =
currentClass.Range.UsageMax = currentClass.NotRange.Usage;
currentClass.Range.DataIndexMin =
currentClass.Range.DataIndexMax = currentClass.NotRange.DataIndex;
currentClass.IsRange = 1;
++numberOfButtons;
}
}
size_t numberOfAxes = 0;
for(auto & currentClass : axisClasses) {
if(currentClass.IsRange) {
numberOfAxes += currentClass.Range.UsageMax - currentClass.Range.UsageMin + 1u;
} else {
currentClass.Range.UsageMin =
currentClass.Range.UsageMax = currentClass.NotRange.Usage;
currentClass.Range.DataIndexMin =
currentClass.Range.DataIndexMax = currentClass.NotRange.DataIndex;
currentClass.IsRange = 1;
++numberOfAxes;
}
}
Jetzt kommen wir zum härtesten Brocken der Initialisierung: Dem Laden von DirectInputs Kalibrierungsdaten.
DirectInput arbeitet mit einem Satz von acht Achsen: X, Y, Z, RX, RY, RZ, Slider, Slider 2. Davon wird die letzte, Slider 2, ignoriert:
Es verbleiben also sieben Achsen.
Welche Usage Page und Usage an welche Achse gebunden ist, speichert DirectInput in der Registry unter
HKEY_CURRENT_USER\System\CurrentControlSet\Control\MediaProperties\PrivateProperties\Joystick\OEM\VID_xxxx&PID_xxxx\Axes\x (wobei VID die vierstellige hexadezimale Herstellerkennung (
Vendor ID) des HIDs angibt; PID seine Produktkennung; und das letzte
x ist der dezimale Index der Achse). Das Format ist dabei eine
DIOBJECTATTRIBUTES-Struktur.
Jetzt wird es leider noch kniffliger: Falls das HID keine Z-Achse anbietet, aber einen Schieberegler, dann landet letzterer auf dem Platz der Z-Achse.
Beispiel:- Ein Joystick hat die Achsen X, Y, und Slider.
- Der Anwender hat keine Achsen überschrieben.
- Die Kalibrierung für X und Y findet sich unter Index 0 und 1 (ganz einfach).
- DirectInput wird Slider automatisch auf Z verschieben, weil kein Z existiert.
- Die Kalibrierung für Slider (und auch der zugehörige Name) befinden sich nicht am Index 6, sondern am Index 2.
Wenn das HID installiert wird, oder der Anwender den Kalibrierungsbildschirm ausführt, schreibt DirectInput die Kalibrierungsdaten in die Registry. Das Format ist dabei
DIOBJECTCALIBRATION. Implementieren wir also erst einmal die Achszuordnung:
struct MappingAndCalibration {
WORD usagePage; // Null falls unbenutzt
WORD usage;
bool isCalibrated;
DIOBJECTCALIBRATION calibration;
wchar_t name[32];
} dInputAxisMapping[7] = { }; // Nullinitialisierung
for(auto const & currentClass : axisClasses) {
if(HID_USAGE_PAGE_GENERIC != currentClass.UsagePage) {
continue;
}
auto const firstUsage = currentClass.Range.UsageMin;
auto const lastUsage = currentClass.Range.UsageMax;
for(WORD currentUsage = firstUsage; currentUsage <= lastUsage; ++currentUsage) {
auto const index = unsigned(currentUsage - HID_USAGE_GENERIC_X); // 0 für X,
1 für Y, …,
6 für Slider
if(index < 7) { // Nur ein Test nötig weil
unsigned
dInputAxisMapping[index].usagePage = HID_USAGE_PAGE_GENERIC;
dInputAxisMapping[index].usagePage = currentUsage;
}
}
}
if(0 == dInputAxisMapping[2].usagePage) {
// Hier Debug-Ausgabe, dass der Slider nun auf Z gemappt ist …
dInputAxisMapping[2] = dInputAxisMapping[6];
dInputAxisMapping[6].usagePage =
dInputAxisMapping[6].usage = 0;
}
Falls das HID eine bestimmte Achse anbietet, steht sie nun unter ihrer Standard-DirectInput-Index in
dInputAxisMapping. Wir wiederholen das für Knöpfe. Glaubt man dem Wikipedia-Artikel über DirectInput, werden 128 Knöpfe unterstützt (die meisten Controller haben nicht mehr als acht).
struct Mapping {
WORD usagePage; // Null falls unbenutzt
WORD usage;
wchar_t name[32];
} dInputButtonMapping[128] = { }; // Nullinitialisierung
Diese Standardzuordnung wird nun von der in der Registry gespeicherten überschrieben. Weil die Registry als externe Informationsquelle unzuverlässig ist, und eine kaputte Kalibrierung das Spiel nicht abschießen sollte, behandle ich hier wieder alle Fehler als nicht-kritisch.
Um die Registry-Einträge lesen zu können, werden zuerst Herstellerkennung und Produktkennung des HIDs benötigt. Die liefert
HidD_GetAttributes():
HIDD_ATTRIBUTES vendorAndProductID;
if(FALSE != HidD_GetAttributes(ntHandle, &vendorAndProductID)) {
wchar_t path[128] = L"System\\CurrentControlSet\\Control\\MediaProperties\\PrivateProperties\\Joystick\\OEM\\VID_????&PID_????\\Axes\\?";
path[84] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 12) & 0xF]);
path[85] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 8) & 0xF]);
path[86] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 4) & 0xF]);
path[87] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 0) & 0xF]);
path[93] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 12) & 0xF]);
path[94] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 8) & 0xF]);
path[95] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 4) & 0xF]);
path[96] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 0) & 0xF]);
for(size_t i = 0; i < sizeof dInputAxisMapping / sizeof dInputAxisMapping[0]; ++i) {
path[103] = wchar_t('0' + i);
HKEY key = nullptr;
if(0 != RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &key)) {
continue; // Die Achse wurde nicht überschrieben; nächste prüfen!
}
Viele Treiber weisen den HID-Achsen einen menschenlesbaren Namen zu; z.B.
combined pedals oder
throttle. Den nehmen wir direkt mit, wenn wir schonmal hier sind. Beachtet, dass Strings aus der Registry nicht zwingend nullterminiert sind!
{
DWORD valueType = REG_NONE;
DWORD valueSize = 0;
RegQueryValueExW(key, L"", nullptr, &valueType, nullptr, &valueSize);
if(REG_SZ == valueType && sizeof dInputAxisMapping.name > valueSize) {
RegQueryValueExW(key, L"", nullptr, &valueType, LPBYTE(dInputAxisMapping.name), &valueSize);
// Der Name wurde bereits bei der Erzeugung genullt; kein Grund, die Null manuell anzufügen.
}
}
DIOBJECTATTRIBUTES mapping;
DWORD valueType = REG_NONE;
DWORD valueSize = 0;
RegQueryValueExW(key, L"Attributes", nullptr, &valueType, nullptr, &valueSize);
if(REG_BINARY == valueType && sizeof mapping == valueSize) {
RegQueryValueExW(key, L"Attributes", nullptr, &valueType, LPBYTE(&mapping), &valueSize);
if(0x15 > mapping.wUsagePage) { // Gültige Usage Page?
dInputAxisMapping.usagePage = mapping.wUsagePage;
dInputAxisMapping.usage = mapping.wUsage;
}
// Hier solltet ihr Debug-Informationen ausgeben um das Ergebnis zu kontrollieren …
}
RegCloseKey(key);
} // for jede Achse
Und weil das so einfach war, wiederholen wir es direkt mit Knöpfen!
wcscpy(path + 98, L"Buttons\\???");
for(size_t i = 0u; i < sizeof dInputButtonMapping / sizeof dInputButtonMapping[0]; ++i) {
if(i >= 100) { // Dreistelliger Name?
path[106] = wchar_t('0' + i / 100u);
path[107] = wchar_t('0' + i % 100u / 10u);
path[108] = wchar_t('0' + i % 10u);
path[109] = wchar_t('\0');
} else if(i >= 10u) { // Zweistelliger Name?
path[106] = wchar_t('0' + i / 10u);
path[107] = wchar_t('0' + i % 10u);
path[108] = wchar_t('\0');
} else { // Einstelliger Name?
path[106] = wchar_t('0' + i);
path[107] = wchar_t('\0');
}
HKEY key = nullptr;
if(0 != RegOpenKeyExW(HKEY_CURRENT_USER, path, 0u, KEY_READ, &key)) {
continue;
}
{
DWORD valueType = REG_NONE;
DWORD valueSize = 0;
RegQueryValueExW(key, L"", nullptr, &valueType, nullptr, &valueSize);
if(REG_SZ == valueType && sizeof dInputButtonMapping.name > valueSize) {
RegQueryValueExW(key, L"", nullptr, &valueType, LPBYTE(dInputButtonMapping.name), &valueSize);
}
}
DIOBJECTATTRIBUTES mapping;
DWORD valueType = REG_NONE;
DWORD valueSize = 0;
RegQueryValueExW(key, L"Attributes", nullptr, &valueType, nullptr, &valueSize);
if(REG_BINARY == valueType && sizeof mapping == valueSize) {
RegQueryValueExW(key, L"Attributes", nullptr, &valueType, LPBYTE(&mapping), &valueSize);
if(0x15 > mapping.wUsagePage) { // Gültige Usage Page?
dInputButtonMapping.usagePage = mapping.wUsagePage;
dInputButtonMapping.usage = mapping.wUsage;
}
// Nochmal Debug-Informationen!
}
RegCloseKey(key);
} // for jeden Knopf
Und nun der finale Registry-Akt: Die eigentlichen Kalibrierungsdaten. Wir finden sie unter HKEY_CURRENT_USER\System\CurrentControlSet\Control\MediaProperties\PrivateProperties\DirectInput\VID_xxxx&PIDxxxx\Calibration\0\Type\Axes\. Bedenkt: Den ganzen Wirrwarr mit den Achszuordnungen haben wir auf uns genommen, damit wir die Kalibrierungsschlüssel den HID-Achsen zuordnen können. Existieren keine Kalibrierungsdaten für eine Achse, ist die entsprechende Achszuordnung wertlos für uns. In diesem Fall markieren wir sie als ungültig, indem usagePage und usage genullt werden.
wcscpy(path, L"System\\CurrentControlSet\\Control\\MediaProperties\\PrivateProperties\\DirectInput\\VID_????&PID_????\\Calibration\\0\\Type\\Axes\\?");
path[83] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 12) & 0xF]);
path[84] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 8) & 0xF]);
path[85] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 4) & 0xF]);
path[86] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.VendorID >> 0) & 0xF]);
path[92] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 12) & 0xF]);
path[93] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 8) & 0xF]);
path[94] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 4) & 0xF]);
path[95] = wchar_t("0123456789ABCDEF"[(vendorAndProductID.ProductID >> 0) & 0xF]);
for(size_t i = 0; i < sizeof dInputAxisMapping / sizeof dInputAxisMapping[0]; ++i) {
path[121] = wchar_t('0' + i);
bool success = false;
HKEY key = nullptr;
if(0 == RegOpenKeyExW(HKEY_CURRENT_USER, path, 0u, KEY_READ, &key)) {
auto & calibration = dInputAxisMapping.calibration;
DWORD valueType = REG_NONE;
DWORD valueSize = 0;
RegQueryValueExW(key, L"Calibration", nullptr, &valueType, nullptr, &valueSize);
if(REG_BINARY == valueType && sizeof calibration == valueSize) {
if(0 == RegQueryValueExW(key, L"Calibration", nullptr, &valueType, LPBYTE(&calibration), &valueSize)) {
dInputAxisMapping.isCalibrated = true;
}
}
RegCloseKey(key);
}
} // for jede Achse
} // if Herstellerkennung und Produktkennung gefunden
Jetzt haben wir alle Daten über das Gerät, die wir brauchen. Der Konstruktor wird damit abgeschlossen, dass wir die Informationen über Achsen und Knöpfe in den Attributen axes und buttons des HIDs speichern, damit sie beim Verarbeiten von Eingaben abgerufen werden können:
for(auto const & currentClass : axisClasses) {
auto const firstUsage = currentClass.Range.UsageMin;
auto const lastUsage = currentClass.Range.UsageMax;
for(WORD currentUsage = firstUsage, currentIndex = currentClass.Range.DataIndexMin;
currentUsage <= lastUsage;
++currentUsage, ++currentIndex
) {
bool isCalibrated = false;
int32_t calibratedMinimum;
int32_t calibratedMaximum;
int32_t calibratedCenter;
wchar_t const * toName = L"";
// Wurden Kalibrierungsdaten oder Name überschrieben?
for(auto & mapping : dInputAxisMapping) {
if(currentClass.UsagePage == mapping.usagePage && currentUsage == mapping.usage) {
toName = mapping.name;
isCalibrated = mapping.isCalibrated;
if(mapping.isCalibrated) {
calibratedMinimum = mapping.calibration.lMin;
calibratedCenter = mapping.calibration.lCenter;
calibratedMaximum = mapping.calibration.lMax;
}
mapping.usage = 0; // Optimierung: bei zukünfigen Durchläufen überspringen
break;
}
}
Axis axis;
axis.usagePage = currentClass.UsagePage;
axis.usage = currentUsage;
axis.index = currentIndex;
axis.logicalMinimum = currentClass.LogicalMin;
axis.logicalMaximum = currentClass.LogicalMax;
axis.isCalibrated = isCalibrated;
if(isCalibrated) {
axis.logicalCalibratedMinimum = calibratedMinimum;
axis.logicalCalibratedMaximum = calibratedMaximum;
axis.logicalCalibratedCenter = calibratedCenter;
}
axis.physicalMinimum = float(currentClass.PhysicalMin);
axis.physicalMaximum = float(currentClass.PhysicalMax);
axis.name = toName;
axes.push_back(axis);
}
}
Ähnlich für die Knöpfe; nur, dass es dafür keine Kalibrierungsinformationen gibt und die Namen das einzige sind, was wir überschreiben:
for(auto const & currentClass : buttonClasses) {
auto const firstUsage = currentClass.Range.UsageMin;
auto const lastUsage = currentClass.Range.UsageMax;
for(WORD currentUsage = firstUsage, currentIndex = currentClass.Range.DataIndexMin;
currentUsage <= lastUsage;
++currentUsage, ++currentIndex
) {
// Wurde der Name von DirectInput oder dem Treiber überschrieben?
wchar_t const * toName = L"";
for(auto & mapping : dInputButtonMapping) {
if(currentClass.UsagePage == mapping.usagePage && currentUsage == mapping.usage) {
toName = mapping.name;
mapping.usage = 0; // Optimierung: bei zukünfigen Durchläufen überspringen
break;
}
}
Button button;
button.usagePage = currentClass.UsagePage;
button.usage = currentUsage;
button.index = currentIndex;
button.name = toName;
buttons.push_back(button);
}
}
Fertig! Ich empfehle aber dringend, dem Konstruktor am Ende noch Logging-Funktionalität hinzuzufügen, damit ihr die Ergebnisse einfach kontrollieren könnt. Bei mir sieht die beispielsweise so aus:
std::cout << name << '\n';
for(auto const & axis : axes) {
std::wcout << "Achse " << std::hex << axis.usagePage << "/" << axis.usage;
if(false == axis.name.empty()) {
std::wcout << " (" << axis.name << ')';
}
if(axis.isCalibrated) {
std::wcout << " kalibriert von " << std::dec << axis.logicalCalibratedMinimum << " ueber "
<< axis.logicalCalibratedCenter << " zu " << axis.logicalCalibratedMaximum << '\n';
} else {
std::wcout << " nicht kalibriert von " << std::dec << axis.logicalMinimum << " bis " << axis.logicalMaximum << '\n';
}
}
for(auto const & button : buttons) {
std::wcout << "Knopf " << std::hex << button.usagePage << "/" << button.usage;
if(button.name.empty()) {
std::wcout << '\n';
} else {
std::wcout << " (" << button.name << ")\n";
}
}
und ich erhalte für meinen No-Name-Joystick folgende Ausgabe:
Mega World USB Game Controllers
Achse 1/32 kalibriert von 0 ueber 127 zu 255
Achse 1/31 kalibriert von 0 ueber 128 zu 255
Achse 1/30 kalibriert von 0 ueber 128 zu 255
Achse 1/39 nicht kalibriert von 1 bis 8
Knopf 9/1
Knopf 9/2
Knopf 9/3
Knopf 9/4
Für meinen PlayStation-zu-USB-Adapter:
GreenAsia Inc. USB Joystick
Achse 1/0 nicht kalibriert von 0 bis 255
Achse 1/31 (LY axis) kalibriert von 0 ueber 128 zu 255
Achse 1/30 (LX axis) kalibriert von 0 ueber 128 zu 255
Achse 1/35 (RY axis) kalibriert von 0 ueber 128 zu 255
Achse 1/32 (RX axis) kalibriert von 0 ueber 128 zu 255
Achse 1/39 nicht kalibriert von 0 bis 7
Knopf 9/1 (Triangle)
Knopf 9/2 (Circle)
Knopf 9/3 (Cross)
Knopf 9/4 (Square)
Knopf 9/5 (L2)
Knopf 9/6 (R2)
Knopf 9/7 (L1)
Knopf 9/8 (R1)
Knopf 9/9 (Select)
Knopf 9/a (Start)
Knopf 9/b (L3)
Knopf 9/c (R3)
Knopf ff00/1
Noch schnell den Destruktor hinklatschen:
HID::~HID() {
HidD_FreePreparsedData(toInputReportProtocol);
CloseHandle(ntHandle);
}
Ich versichere damit hoch und heilig, dass das Schwierigste erledigt ist! Beim nächsten Mal geht es darum, endlich die Eingaben der angeschlossenen HIDs zu verarbeiten.