From 23ab654c835703436285c1eb5784101b804b18a5 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Fri, 8 Aug 2025 12:34:09 -0500 Subject: [PATCH 01/16] initial commit for add a generic HID joystick --- CMakeLists.txt | 4 + include/idf/GenericJoystick.hh | 63 +++++++ include/idf/HidDecoder.hh | 120 +++++++++++++ include/idf/HidDevice.hh | 44 +++++ include/idf/HidGenericJoystick.hh | 43 +++++ source/idf/GenericJoystick.cpp | 26 +++ source/idf/HidDecoder.cpp | 271 ++++++++++++++++++++++++++++++ source/idf/HidDevice.cpp | 38 +++++ source/idf/HidGenericJoystick.cpp | 16 ++ 9 files changed, 625 insertions(+) create mode 100644 include/idf/GenericJoystick.hh create mode 100644 include/idf/HidDecoder.hh create mode 100644 include/idf/HidDevice.hh create mode 100644 include/idf/HidGenericJoystick.hh create mode 100644 source/idf/GenericJoystick.cpp create mode 100644 source/idf/HidDecoder.cpp create mode 100644 source/idf/HidDevice.cpp create mode 100644 source/idf/HidGenericJoystick.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8880866..4c3bd03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,11 @@ set(SRC_IDF source/idf/EthernetWingMan.cpp source/idf/Extreme3dPro.cpp source/idf/FlightController.cpp + source/idf/GenericJoystick.cpp source/idf/Gravis.cpp + source/idf/HidDevice.cpp + source/idf/HidDecoder.cpp + source/idf/HidGenericJoystick.cpp source/idf/HagstromKEUSB36.cpp source/idf/IndustrialProducts2.cpp source/idf/IndustrialProducts3.cpp diff --git a/include/idf/GenericJoystick.hh b/include/idf/GenericJoystick.hh new file mode 100644 index 0000000..d3d82f2 --- /dev/null +++ b/include/idf/GenericJoystick.hh @@ -0,0 +1,63 @@ +/* +PURPOSE: +LIBRARY DEPENDENCIES: ( +(idf/GenericJoystick.cpp) + +) +*/ + +/** + * @trick_parse{everything} + * @trick_link_dependency{idf/GenericJoystick.cpp} + */ + + +#ifndef GENERIC_JOYSTICk_HH +#define GENERIC_JOYSTICk_HH + +#include "idf/InputLayout.hh" +#include "idf/SingleInput.hh" + +namespace idf { + +/** + * @brief Generic joystick layout. Implementing device should guarantee at least: + * Roll axis, Pitch axis and Trigger button. Other Inputs are not guaranteed + * + * @author Philip Kunz + */ +class GenericJoystick : public virtual InputLayout { + + public: + + GenericJoystick(); + + virtual ~GenericJoystick(); + + // forward-backward pivoting + SingleInput forwardBackwardPivot; + + // left-right pivoting + SingleInput leftRightPivot; + + // twisting + SingleInput twist; + + // the trigger button + SingleInput trigger; + + // list of remaining buttons (if any) + std::vectorbuttons; + + // the slider + SingleInput slider; + + protected: + + virtual const std::vector& getConfigurables(); + +}; + +} // namespace idf + +#endif \ No newline at end of file diff --git a/include/idf/HidDecoder.hh b/include/idf/HidDecoder.hh new file mode 100644 index 0000000..95f0999 --- /dev/null +++ b/include/idf/HidDecoder.hh @@ -0,0 +1,120 @@ +/* +PURPOSE: +LIBRARY DEPENDENCIES: ( +(idf/HidDecoder.cpp) +) +*/ + +/** + * @trick_parse{everything} + * @trick_link_dependency{idf/HidDecoder.cpp} + */ + +#ifndef HID_DECODER_HH +#define HID_DECODER_HH + +#include +#include +#include +#include +#include +#include "hidapi/hidapi/hidapi.h" + +namespace idf +{ + +struct HIDInput +{ + std::string name; + int start_bit; + int end_bit; + int logical_min; + int logical_max; + int physical_min; + int physical_max; + int units; + int units_exp; +}; + +struct HIDReport +{ + int id; + std::vector inputs; + int bytes_count; + bool has_report_byte; +}; + +struct HIDDevice +{ + std::string type; + std::vector reports; +}; + +class HIDDecoder +{ +public: + HIDDecoder(); + + HIDDevice parseDescriptor(const std::vector& descriptor); + // std::vector> enumerateDevices(); + +private: + void init(); + int convertDataToInt(const std::vector &data, bool isSigned); + bool interpretSigned(); + void decodeGlobalItem(int tag_code, int data, const std::vector& data_bytes); + void decodeLocalItem(int tag_code, int data); + void decodeMainItem(int tag_code); + + struct HIDState { + uint report_size; + uint report_count; + int usage_page; + int usage_min; + int usage_max; + int logical_min; + int logical_max; + int physical_min; + int physical_max; + int units; + int units_exp; + }; + + int vendorId_; + int productId_; + + std::string device_type_ = "Unknown"; + std::vector reports_; + std::vector inputs_; + int bit_offset_ = 0; + int report_id_ = 0; + std::vector usage_list_; + + HIDState current_; + std::vector state_stack_; + int button_base_ = 1; + + std::map usage_names_ = { + {0x30, "X"}, + {0x31, "Y"}, + {0x32, "Z"}, + {0x33, "Rx"}, + {0x34, "Ry"}, + {0x35, "Rz"}, + {0x36, "Slider"}, + {0x37, "Dial"}, + {0x38, "Wheel"}, + {0x39, "Hat switch"}, + {0x3D, "Start"}, + {0x3E, "Select"}, + {0x01, "Pointer"}, + {0x04, "Joystick"}, + {0x05, "Gamepad"}, + {0x08, "Multi-axis Controller"} + }; + +}; // HIDDecoder + +} // namespace + +#endif \ No newline at end of file diff --git a/include/idf/HidDevice.hh b/include/idf/HidDevice.hh new file mode 100644 index 0000000..231b5fc --- /dev/null +++ b/include/idf/HidDevice.hh @@ -0,0 +1,44 @@ +/* PURPOSE: +LIBRARY DEPENDENCIES: ( +(idf/HidDevice.cpp) +) +*/ + +#ifndef IDF_HID_DEVICE_HH +#define IDF_HID_DEVICE_HH + +#include "idf/UsbDevice.hh" +#include "idf/HidDecoder.hh" + +namespace idf { + +/** + * @brief HidDevice is intended to help make IDF useful with generic + * gaming devices that do not yet have a specific implementation. + * ie joysticks, gamepads and "multi-axis" controllers + * @author Philip Kunz + * @date 2025-08-07 + */ +class HidDevice : public UsbDevice { + public: + + HidDevice(); + + virtual ~HidDevice() {}; + + virtual void open(); + + protected: + + std::vectorhidReports; + + std::vector hidReportDescriptor; + + virtual std::vector getHidReportDescriptor(); + + void parseHidReportDescriptor(); +}; + +} // namespace idf + +#endif diff --git a/include/idf/HidGenericJoystick.hh b/include/idf/HidGenericJoystick.hh new file mode 100644 index 0000000..ab44f06 --- /dev/null +++ b/include/idf/HidGenericJoystick.hh @@ -0,0 +1,43 @@ +/* +PURPOSE: +LIBRARY DEPENDENCIES: ( +(idf/HidGenericJoystick.cpp) + +) +*/ + +/** + * @trick_parse{everything} + * @trick_link_dependency{idf/HidGenericJoystick.cpp} + */ + + +#ifndef HID_GENERIC_JOYSTICK_HH +#define HID_GENERIC_JOYSTICK_HH + +#include "idf/HidDevice.hh" +#include "idf/GenericJoystick.hh" + +namespace idf { + +/** + * @brief Generic class to open any HID device, read the HID Report Descriptor + * and construct a usable controller with at least Roll axis, Pitch axis and + * Trigger button. Attempts will also be made to map, but no guarantee is provided: + * Throttle (Slider), Hat (8 directional) and a std::vector of buttons. + * + * @author Philip Kunz + */ +class HidGenericJoystick : public HidDevice, public virtual GenericJoystick { + + public: + + HidGenericJoystick(); + + void decode(const std::vector& data); + +}; + +} // namespace idf + +#endif \ No newline at end of file diff --git a/source/idf/GenericJoystick.cpp b/source/idf/GenericJoystick.cpp new file mode 100644 index 0000000..8279f42 --- /dev/null +++ b/source/idf/GenericJoystick.cpp @@ -0,0 +1,26 @@ +#include "idf/GenericJoystick.hh" + +namespace idf { + +GenericJoystick::GenericJoystick() : + forwardBackwardPivot(0, 1023, 512), + leftRightPivot(0, 1023, 512), + twist(0, 1023, 512), + trigger(0,1), + slider(0, 1023, 512) { + buttons = {}; + } + +const std::vector& GenericJoystick::getConfigurables() { + static std::vector inputs; + if (inputs.empty()) { + append(InputLayout::getConfigurables(), inputs); + inputs.push_back(Configurable(forwardBackwardPivot, "Forward/Backward Pivot", "forwardBackwardPivot")); + inputs.push_back(Configurable(leftRightPivot, "Left/Right Pivot", "leftRightPivot")); + inputs.push_back(Configurable(twist, "Twist", "twist")); + inputs.push_back(Configurable(slider, "Slider", "slider")); + } + return inputs; +} + +} // namespace idf diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp new file mode 100644 index 0000000..6ec07eb --- /dev/null +++ b/source/idf/HidDecoder.cpp @@ -0,0 +1,271 @@ +#include "idf/HidDecoder.hh" +#include +#include + +namespace idf +{ + + HIDDecoder::HIDDecoder() { + init(); + } + + void HIDDecoder::init() { + reports_.clear(); + inputs_.clear(); + bit_offset_ = 0; + report_id_ = 0; + usage_list_.clear(); + current_.report_size = 0; + current_.report_count = 0; + current_.usage_page = 0; + current_.usage_min = 0; + current_.usage_max = 0; + current_.logical_min = 0; + current_.logical_max = 0; + current_.physical_min = 0; + current_.physical_max = 0; + current_.units = 0; + current_.units_exp = 0; + device_type_ = "Unknown"; + state_stack_.clear(); + button_base_ = 1; + } + + HIDDevice HIDDecoder::parseDescriptor(const std::vector &descriptor) + { + init(); + uint i = 0; + while (i < descriptor.size()) { + unsigned char prefix = descriptor[i]; + int size_code = prefix & 0x03; + int type_code = (prefix >> 2) & 0x03; + int tag_code = (prefix >> 4) & 0x0F; + int data = 0; + std::vector data_bytes; + + int size = size_code; + if (size_code == 3) { + size = 4; + } + + if (size > 0) { + data_bytes = std::vector(descriptor.begin() + i + 1, descriptor.begin() + i + 1 + size); + data = 0; + for (int j = 0; j < size; ++j) { + data |= (data_bytes[j] << (j * 8)); + } + } + + if (type_code == 0) { // Main + decodeMainItem(tag_code); + } + else if (type_code == 1) { // Global + decodeGlobalItem(tag_code, data, data_bytes); + } + else if (type_code == 2) { // Local + decodeLocalItem(tag_code, data); + } + + i += 1 + size; + } + + // add final report to list + reports_.push_back({ + report_id_, + inputs_, + bit_offset_ / 8, + report_id_ == 0 + }); + + return {device_type_, reports_}; + } + + void HIDDecoder::decodeGlobalItem(int tag_code, int data, const std::vector &data_bytes) + { + if (tag_code == 0x0) { // Usage Page + current_.usage_page = data; + } + else if (tag_code == 0x1) { // Logical Minimum + current_.logical_min = convertDataToInt(data_bytes, true); + } + else if (tag_code == 0x2) { // Logical Maximum + current_.logical_max = convertDataToInt(data_bytes, true); + } + else if (tag_code == 0x3) { // Physical Minimum + current_.physical_min = convertDataToInt(data_bytes, interpretSigned()); + } + else if (tag_code == 0x4) { // Physical Maximum + current_.physical_max = convertDataToInt(data_bytes, interpretSigned()); + } + else if (tag_code == 0x5) { // Unit exponent + current_.units_exp = static_cast(data_bytes[0]); + } + else if (tag_code == 0x6) { // Units + current_.units = data; + if (data == 0) + { + current_.units = 0; + current_.units_exp = 0; + current_.physical_min = 0; + current_.physical_max = 0; + } + } + else if (tag_code == 0x7) { // Report Size + current_.report_size = data; + } + else if (tag_code == 0x8) { // Report ID + if (report_id_ != 0) + { // if not the 1st report, save inputs to a report + reports_.push_back({report_id_, + inputs_, + bit_offset_ / 8, + true}); + inputs_.clear(); + } + + bit_offset_ = 8; + report_id_ = data; + } + else if (tag_code == 0x9) { // Report Count + current_.report_count = data; + } + else if (tag_code == 0xA) { // Push + state_stack_.push_back(current_); + } + else if (tag_code == 0xB) { // Pop + if (!state_stack_.empty()) + { + HIDState tmp = state_stack_.back(); + current_ = state_stack_.back(); + current_.usage_page = tmp.usage_page; + current_.usage_min = tmp.usage_min; + current_.usage_max = tmp.usage_max; + current_.report_size = tmp.report_size; + current_.report_count = tmp.report_count; + current_.logical_min = tmp.logical_min; + current_.logical_max = tmp.logical_max; + current_.physical_min = tmp.physical_min; + current_.physical_max = tmp.physical_max; + current_.units = tmp.units; + current_.units_exp = tmp.units_exp; + state_stack_.pop_back(); + } + } + } + + void HIDDecoder::decodeLocalItem(int tag_code, int data) + { + if (tag_code == 0x0) { // Usage + if (data < 0x30) { + if (device_type_ == "Unknown") { + device_type_ = usage_names_[data]; + } + } + else { + usage_list_.push_back(data); + } + } + else if (tag_code == 0x1) { // Usage Minimum + current_.usage_min = data; + } + else if (tag_code == 0x2) { // Usage Maximum + current_.usage_max = data; + } + } + + void HIDDecoder::decodeMainItem(int tag_code) + { + if (tag_code == 0x8) { // Input(s) from current HIDState information + std::vector expanded_usages; + if (current_.usage_min != 0 && current_.usage_max != 0) + { + for (int j = current_.usage_min; j <= current_.usage_max; ++j) + { + expanded_usages.push_back(j); + } + } + else if (!usage_list_.empty()) { + expanded_usages = usage_list_; + } + + for (uint j = 0; j < current_.report_count; ++j) { + std::string name; + if (current_.usage_page == 0x01 && j < expanded_usages.size()) { + int usage = expanded_usages[j]; + name = usage_names_[usage]; + } + else if (current_.usage_page == 0x09 && j < expanded_usages.size()) { + name = "Button " + std::to_string(button_base_++); + } + else if (current_.usage_page == 0x01 && expanded_usages.size() == 1) { + int usage = expanded_usages[0]; + name = usage_names_[usage]; + } + else { + name = "Padding"; + } + + int start_bit = bit_offset_; + int end_bit = bit_offset_ + current_.report_size - 1; + + int tmp_phys_min = current_.physical_min; + int tmp_phys_max = current_.physical_max; + if (current_.physical_min == 0) { + tmp_phys_min = current_.logical_min; + } + if (current_.physical_max == 0) { + tmp_phys_max = current_.logical_max; + } + + inputs_.push_back({name, + start_bit, + end_bit, + current_.logical_min, + current_.logical_max, + tmp_phys_min, + tmp_phys_max, + current_.units, + current_.units_exp}); + + bit_offset_ += current_.report_size; + } + + usage_list_.clear(); + current_.usage_min = 0; + current_.usage_max = 0; + } + else if (tag_code == 0x9 || tag_code == 0x0B) { // Output or Feature + usage_list_.clear(); + current_.usage_min = 0; + current_.usage_max = 0; + } + } + + bool HIDDecoder::interpretSigned() { + return current_.logical_max < 0 || current_.logical_min < 0; + } + + int HIDDecoder::convertDataToInt(const std::vector &data, bool isSigned) { + u_int32_t value = 0; + + for (uint i = 0; i < data.size(); ++i) { + value += (data[i] << 8*i); + } + + if (!isSigned) return value; + + switch (data.size()) { + case 1: + if (value >= pow(2,7)) return static_cast(value); + break; + case 2: + if (value >= pow(2,15)) return static_cast(value); + break; + case 4: + if (value >= pow(2,31)) return static_cast(value); + break; + } + return value; + } + +} // namespace \ No newline at end of file diff --git a/source/idf/HidDevice.cpp b/source/idf/HidDevice.cpp new file mode 100644 index 0000000..10f98a0 --- /dev/null +++ b/source/idf/HidDevice.cpp @@ -0,0 +1,38 @@ +#include "idf/HidDevice.hh" + +namespace idf { + +HidDevice::HidDevice() : + UsbDevice("Generic HID Device", 64) {} + +std::vector HidDevice::getHidReportDescriptor() { + unsigned char buffer[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; + int size = hid_get_report_descriptor(hidDevice, buffer, sizeof(buffer)); + + if (size < 0) { + perror("Unable to read HID Report Descriptor"); + return {}; + } + + std::vector report(buffer, buffer + size); + + printf("\x1b[39;49m\nHID Report Descriptor (%d bytes):\n ", size); + for(int i=0; i < size; ++i) { + printf("%02X ", buffer[i]); + if (i % 16 == 15) printf("\n "); + else if (i % 8 == 7) printf(" "); + } + + return report; +} + +void HidDevice::open() { + UsbDevice::open(); + hidReportDescriptor = getHidReportDescriptor(); + + if (hidReportDescriptor.size() > 0) { + parseHidReportDescriptor(); + } +} + +} //namespace idf \ No newline at end of file diff --git a/source/idf/HidGenericJoystick.cpp b/source/idf/HidGenericJoystick.cpp new file mode 100644 index 0000000..83b0bbb --- /dev/null +++ b/source/idf/HidGenericJoystick.cpp @@ -0,0 +1,16 @@ +#include "idf/HidGenericJoystick.hh" + +namespace idf { + +HidGenericJoystick::HidGenericJoystick() {} + +void HidGenericJoystick::decode(const std::vector& data) { + printf("decode something from:"); + for (int d : data) { + printf("%02X", d); + } + printf("\n"); +} + + +} // namespace idf From 8be5b4c24ff5274c350c330557a3cb363879a1e4 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Fri, 8 Aug 2025 18:18:36 -0500 Subject: [PATCH 02/16] able to instantiate an HidGenericJoystick with vid:pid and print parsed inputs --- include/idf/GenericJoystick.hh | 4 +- include/idf/HidDecoder.hh | 25 +++++----- include/idf/HidDevice.hh | 21 +++++++-- include/idf/HidGenericJoystick.hh | 4 +- include/idf/PythonInterface.hh | 3 ++ source/idf/HidDecoder.cpp | 27 +++++++---- source/idf/HidDevice.cpp | 78 +++++++++++++++++++++++++++---- source/idf/HidGenericJoystick.cpp | 3 +- source/idf/UsbDevice.cpp | 3 -- 9 files changed, 125 insertions(+), 43 deletions(-) diff --git a/include/idf/GenericJoystick.hh b/include/idf/GenericJoystick.hh index d3d82f2..90af1c9 100644 --- a/include/idf/GenericJoystick.hh +++ b/include/idf/GenericJoystick.hh @@ -32,7 +32,7 @@ class GenericJoystick : public virtual InputLayout { GenericJoystick(); - virtual ~GenericJoystick(); + virtual ~GenericJoystick() {}; // forward-backward pivoting SingleInput forwardBackwardPivot; @@ -60,4 +60,4 @@ class GenericJoystick : public virtual InputLayout { } // namespace idf -#endif \ No newline at end of file +#endif diff --git a/include/idf/HidDecoder.hh b/include/idf/HidDecoder.hh index 95f0999..8ea7746 100644 --- a/include/idf/HidDecoder.hh +++ b/include/idf/HidDecoder.hh @@ -23,7 +23,7 @@ LIBRARY DEPENDENCIES: ( namespace idf { -struct HIDInput +struct HidInput { std::string name; int start_bit; @@ -36,26 +36,27 @@ struct HIDInput int units_exp; }; -struct HIDReport +struct HidReport { int id; - std::vector inputs; + std::vector inputs; int bytes_count; bool has_report_byte; }; -struct HIDDevice +struct HidDecoded { std::string type; - std::vector reports; + std::vector reports; + int maxReportLength; }; -class HIDDecoder +class HidDecoder { public: - HIDDecoder(); + HidDecoder(); - HIDDevice parseDescriptor(const std::vector& descriptor); + HidDecoded parseDescriptor(const std::vector& descriptor); // std::vector> enumerateDevices(); private: @@ -84,8 +85,8 @@ private: int productId_; std::string device_type_ = "Unknown"; - std::vector reports_; - std::vector inputs_; + std::vector reports_; + std::vector inputs_; int bit_offset_ = 0; int report_id_ = 0; std::vector usage_list_; @@ -113,8 +114,8 @@ private: {0x08, "Multi-axis Controller"} }; -}; // HIDDecoder +}; // HidDecoder } // namespace -#endif \ No newline at end of file +#endif diff --git a/include/idf/HidDevice.hh b/include/idf/HidDevice.hh index 231b5fc..b826767 100644 --- a/include/idf/HidDevice.hh +++ b/include/idf/HidDevice.hh @@ -22,21 +22,34 @@ namespace idf { class HidDevice : public UsbDevice { public: - HidDevice(); + HidDevice( int vendor, int product ); + + HidDevice( HidDecoded ); virtual ~HidDevice() {}; - virtual void open(); + /** + * @brief open device with @a vendor and @a product, and parse the HID + * report descriptor. + * + * @param vendor USB Vendor ID + * @param product USB Product ID + * @return HIDDecodedDevice struct enumerating the available reports + */ + static HidDecoded decodeDevice( int vendor, int product); + + void printDecodedInfo(); protected: - std::vectorhidReports; + HidDecoded decoded; + + std::vectorhidReports; std::vector hidReportDescriptor; virtual std::vector getHidReportDescriptor(); - void parseHidReportDescriptor(); }; } // namespace idf diff --git a/include/idf/HidGenericJoystick.hh b/include/idf/HidGenericJoystick.hh index ab44f06..4a56795 100644 --- a/include/idf/HidGenericJoystick.hh +++ b/include/idf/HidGenericJoystick.hh @@ -32,7 +32,7 @@ class HidGenericJoystick : public HidDevice, public virtual GenericJoystick { public: - HidGenericJoystick(); + HidGenericJoystick(int vendor, int product); void decode(const std::vector& data); @@ -40,4 +40,4 @@ class HidGenericJoystick : public HidDevice, public virtual GenericJoystick { } // namespace idf -#endif \ No newline at end of file +#endif diff --git a/include/idf/PythonInterface.hh b/include/idf/PythonInterface.hh index 20454bb..027288d 100644 --- a/include/idf/PythonInterface.hh +++ b/include/idf/PythonInterface.hh @@ -72,6 +72,9 @@ #include "idf/UsbXBox.hh" #include "idf/UsbXBoxOne.hh" +// Generic HID Devices +#include "idf/HidGenericJoystick.hh" + // Ethernet Devices #include "idf/EthernetExtreme3dPro.hh" #include "idf/EthernetWingMan.hh" diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index 6ec07eb..64c28ec 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -5,11 +5,11 @@ namespace idf { - HIDDecoder::HIDDecoder() { + HidDecoder::HidDecoder() { init(); } - void HIDDecoder::init() { + void HidDecoder::init() { reports_.clear(); inputs_.clear(); bit_offset_ = 0; @@ -31,7 +31,7 @@ namespace idf button_base_ = 1; } - HIDDevice HIDDecoder::parseDescriptor(const std::vector &descriptor) + HidDecoded HidDecoder::parseDescriptor(const std::vector &descriptor) { init(); uint i = 0; @@ -77,10 +77,17 @@ namespace idf report_id_ == 0 }); - return {device_type_, reports_}; + int maxReport = -1; + for (HidReport rep : reports_) { + if (rep.inputs.size() > 0) { + maxReport = std::max(maxReport, rep.bytes_count); + } + } + + return {device_type_, reports_, maxReport}; } - void HIDDecoder::decodeGlobalItem(int tag_code, int data, const std::vector &data_bytes) + void HidDecoder::decodeGlobalItem(int tag_code, int data, const std::vector &data_bytes) { if (tag_code == 0x0) { // Usage Page current_.usage_page = data; @@ -153,7 +160,7 @@ namespace idf } } - void HIDDecoder::decodeLocalItem(int tag_code, int data) + void HidDecoder::decodeLocalItem(int tag_code, int data) { if (tag_code == 0x0) { // Usage if (data < 0x30) { @@ -173,7 +180,7 @@ namespace idf } } - void HIDDecoder::decodeMainItem(int tag_code) + void HidDecoder::decodeMainItem(int tag_code) { if (tag_code == 0x8) { // Input(s) from current HIDState information std::vector expanded_usages; @@ -241,11 +248,11 @@ namespace idf } } - bool HIDDecoder::interpretSigned() { + bool HidDecoder::interpretSigned() { return current_.logical_max < 0 || current_.logical_min < 0; } - int HIDDecoder::convertDataToInt(const std::vector &data, bool isSigned) { + int HidDecoder::convertDataToInt(const std::vector &data, bool isSigned) { u_int32_t value = 0; for (uint i = 0; i < data.size(); ++i) { @@ -268,4 +275,4 @@ namespace idf return value; } -} // namespace \ No newline at end of file +} // namespace diff --git a/source/idf/HidDevice.cpp b/source/idf/HidDevice.cpp index 10f98a0..5239641 100644 --- a/source/idf/HidDevice.cpp +++ b/source/idf/HidDevice.cpp @@ -1,9 +1,48 @@ #include "idf/HidDevice.hh" +#include "idf/IOException.hh" +#include +#include +#include +#include namespace idf { -HidDevice::HidDevice() : - UsbDevice("Generic HID Device", 64) {} +HidDevice::HidDevice( int vendor, int product) : + HidDevice(HidDevice::decodeDevice(vendor, product)) {} + +HidDevice::HidDevice(HidDecoded decoded_in) : + UsbDevice("Generic " + decoded_in.type, decoded_in.maxReportLength), + decoded(decoded_in) {} + +HidDecoded HidDevice::decodeDevice(int vendor, int product) { + + std::ostringstream ss; + hid_device* hidDevice; + unsigned char buffer[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; + HidDecoder decoder; + HidDecoded decDevice; + + if (!(hidDevice = hid_open(vendor, product, NULL))) { + ss << "unable to open device " << vendor << ":" << product << " : "; + ss << strerror(errno) << std::endl; + throw IOException(ss.str()); + } + + int descSize = hid_get_report_descriptor(hidDevice, buffer, sizeof(buffer)); + + if (descSize < 0) { + ss << "unable to get HID report descriptor from " << vendor << ":" << product << " : "; + ss << strerror(errno) << std::endl; + hid_close(hidDevice); + throw IOException(ss.str()); + } + + std::vector descriptor(buffer, buffer + descSize); + decDevice = decoder.parseDescriptor(descriptor); + + hid_close(hidDevice); + return decDevice; +} std::vector HidDevice::getHidReportDescriptor() { unsigned char buffer[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; @@ -26,13 +65,34 @@ std::vector HidDevice::getHidReportDescriptor() { return report; } -void HidDevice::open() { - UsbDevice::open(); - hidReportDescriptor = getHidReportDescriptor(); - - if (hidReportDescriptor.size() > 0) { - parseHidReportDescriptor(); +void HidDevice::printDecodedInfo() { + std::ostringstream ss; + ss << "Device Type:" << decoded.type << "\n"; + for (HidReport report : decoded.reports) { + ss << "Report: " << report.id << " (" << report.bytes_count << " bytes)\n"; + if (report.id != 0) { + ss << " Report ID bits 0:7 value: " << report.id << "\n"; + } + for(HidInput input : report.inputs) { + ss << " " << std::setfill(' ') << std::setw(15) << std::left << input.name; + if (input.start_bit == input.end_bit) { + ss << " bit " << std::setw(5) << std::right << input.start_bit << " range: "; + ss << std::setw(5) << std::right << input.logical_min << ":"; + ss << std::left << input.logical_max << "\n"; + } + else { + ss << " bits " << std::setw(5) << std::right << input.start_bit << ":"; + ss << std::setw(5) << std::left << input.end_bit; + ss << " range: "; + ss << std::setw(5) << std::right << input.logical_min << ":"; + ss << std::setw(5) << std::left << input.logical_max << " "; + ss << std::setw(5) << std::right << input.physical_min << ":"; + ss << std::setw(5) << std::left << input.physical_max << "\n"; + } + } + ss << "\n"; } + printf(ss.str().c_str()); } -} //namespace idf \ No newline at end of file +} //namespace idf diff --git a/source/idf/HidGenericJoystick.cpp b/source/idf/HidGenericJoystick.cpp index 83b0bbb..ed52980 100644 --- a/source/idf/HidGenericJoystick.cpp +++ b/source/idf/HidGenericJoystick.cpp @@ -2,7 +2,8 @@ namespace idf { -HidGenericJoystick::HidGenericJoystick() {} +HidGenericJoystick::HidGenericJoystick(int vendor, int product) : + HidDevice(vendor, product) {} void HidGenericJoystick::decode(const std::vector& data) { printf("decode something from:"); diff --git a/source/idf/UsbDevice.cpp b/source/idf/UsbDevice.cpp index bb1c186..ce5bb7f 100644 --- a/source/idf/UsbDevice.cpp +++ b/source/idf/UsbDevice.cpp @@ -9,9 +9,6 @@ #include #include -//deleteme -#include - namespace idf { int UsbDevice::instanceCount = 0; From 1d28c92c98a474beacca9ed77a3f5e831792f5f1 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Wed, 13 Aug 2025 15:04:21 -0500 Subject: [PATCH 03/16] HidDecoder: add value extracting #103 --- include/idf/HidDecoder.hh | 30 ++++++++++++++++++++------ include/idf/HidDevice.hh | 2 ++ source/idf/HidDecoder.cpp | 45 +++++++++++++++++++++++++++++++++------ 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/include/idf/HidDecoder.hh b/include/idf/HidDecoder.hh index 8ea7746..8cf2124 100644 --- a/include/idf/HidDecoder.hh +++ b/include/idf/HidDecoder.hh @@ -25,6 +25,7 @@ namespace idf struct HidInput { + u_int8_t usage; std::string name; int start_bit; int end_bit; @@ -56,8 +57,24 @@ class HidDecoder public: HidDecoder(); + /** + * @brief parse a list of @a HidReports with @a HidInputs from an HID + * Report Descriptor. Only @a Input items are kept. @a Output and + * @a feature items are discarded + * + * @param descriptor binary HID report descriptor. + * @return HidDecoded struct with a list of HidReports and some metadata + */ HidDecoded parseDescriptor(const std::vector& descriptor); - // std::vector> enumerateDevices(); + + /** + * @brief extract the appropriate binary value from the data for a given HidInput + * + * @param input HidInput retrieved from parsing HID report descriptor + * @param data data that was read in from device + * @return u_int64_t raw binary data extracted from device. + */ + u_int64_t extractValue(const HidInput& input, const std::vector& data); private: void init(); @@ -95,7 +112,12 @@ private: std::vector state_stack_; int button_base_ = 1; - std::map usage_names_ = { + std::map usage_names_ = { + {0x01, "Pointer"}, + {0x04, "Joystick"}, + {0x05, "Gamepad"}, + {0x09, "Button"}, + {0x08, "Multi-axis Controller"}, {0x30, "X"}, {0x31, "Y"}, {0x32, "Z"}, @@ -108,10 +130,6 @@ private: {0x39, "Hat switch"}, {0x3D, "Start"}, {0x3E, "Select"}, - {0x01, "Pointer"}, - {0x04, "Joystick"}, - {0x05, "Gamepad"}, - {0x08, "Multi-axis Controller"} }; }; // HidDecoder diff --git a/include/idf/HidDevice.hh b/include/idf/HidDevice.hh index b826767..c907e9d 100644 --- a/include/idf/HidDevice.hh +++ b/include/idf/HidDevice.hh @@ -42,6 +42,8 @@ class HidDevice : public UsbDevice { protected: + HidDecoder decoder; + HidDecoded decoded; std::vectorhidReports; diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index 64c28ec..cc8177a 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -35,6 +35,7 @@ namespace idf { init(); uint i = 0; + while (i < descriptor.size()) { unsigned char prefix = descriptor[i]; int size_code = prefix & 0x03; @@ -74,7 +75,7 @@ namespace idf report_id_, inputs_, bit_offset_ / 8, - report_id_ == 0 + report_id_ != 0 }); int maxReport = -1; @@ -165,7 +166,7 @@ namespace idf if (tag_code == 0x0) { // Usage if (data < 0x30) { if (device_type_ == "Unknown") { - device_type_ = usage_names_[data]; + device_type_ = usage_names_.at(data); } } else { @@ -197,16 +198,20 @@ namespace idf for (uint j = 0; j < current_.report_count; ++j) { std::string name; + u_int8_t usage = 0; if (current_.usage_page == 0x01 && j < expanded_usages.size()) { - int usage = expanded_usages[j]; - name = usage_names_[usage]; + usage = expanded_usages[j]; + if (usage_names_.count(usage)) name = usage_names_.at(usage); + else name = "Unknown"; } else if (current_.usage_page == 0x09 && j < expanded_usages.size()) { + usage = 0x09; name = "Button " + std::to_string(button_base_++); } else if (current_.usage_page == 0x01 && expanded_usages.size() == 1) { - int usage = expanded_usages[0]; - name = usage_names_[usage]; + usage = expanded_usages[0]; + if (usage_names_.count(usage)) name = usage_names_.at(usage); + else name = "Unknown"; } else { name = "Padding"; @@ -224,7 +229,8 @@ namespace idf tmp_phys_max = current_.logical_max; } - inputs_.push_back({name, + inputs_.push_back({usage, + name, start_bit, end_bit, current_.logical_min, @@ -275,4 +281,29 @@ namespace idf return value; } + u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector& data){ + int startByte = input.start_bit / 8; + int startBit = input.start_bit % 8; + int endByte = input.end_bit / 8; + int endBit = input.end_bit % 8; + u_int64_t temp = 0; + u_int64_t mask = 1; + + // bitmask + mask = (mask << (input.end_bit - input.start_bit +1)) - 1; + + // stack bytes in reverse to get continuous bits + for(int i = endByte; i >= startByte; --i) { + temp = temp << 8; + temp |= data[i]; + } + + temp = temp >> startBit; + temp = temp & mask; + // testing output + // printf(" %s(%02x) %d[%d] --> %d[%d], mask(%lu), >> %u", input.name.c_str(), input.usage, startByte, startBit, endByte, endBit, mask, startBit); + + return temp; + } + } // namespace From 47c40b60af5b95cd7c02a154cbc32fa3bd8f1b26 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Fri, 15 Aug 2025 16:43:19 -0500 Subject: [PATCH 04/16] generic: add hat to joystick, flesh out decoding --- include/idf/GenericJoystick.hh | 14 +- include/idf/HidDecoder.hh | 73 ++-- include/idf/HidDevice.hh | 14 +- include/idf/HidGenericJoystick.hh | 8 +- source/idf/GenericJoystick.cpp | 30 +- source/idf/HidDecoder.cpp | 564 ++++++++++++++++-------------- source/idf/HidDevice.cpp | 64 ++-- source/idf/HidGenericJoystick.cpp | 72 +++- 8 files changed, 495 insertions(+), 344 deletions(-) diff --git a/include/idf/GenericJoystick.hh b/include/idf/GenericJoystick.hh index 90af1c9..3a7f7a1 100644 --- a/include/idf/GenericJoystick.hh +++ b/include/idf/GenericJoystick.hh @@ -28,7 +28,7 @@ namespace idf { */ class GenericJoystick : public virtual InputLayout { - public: +public: GenericJoystick(); @@ -52,7 +52,17 @@ class GenericJoystick : public virtual InputLayout { // the slider SingleInput slider; - protected: + // Hat Directions + SingleInput HatNorth; + SingleInput HatNorthEast; + SingleInput HatEast; + SingleInput HatSouthEast; + SingleInput HatSouth; + SingleInput HatSouthWest; + SingleInput HatWest; + SingleInput HatNorthWest; + +protected: virtual const std::vector& getConfigurables(); diff --git a/include/idf/HidDecoder.hh b/include/idf/HidDecoder.hh index 8cf2124..1033fd6 100644 --- a/include/idf/HidDecoder.hh +++ b/include/idf/HidDecoder.hh @@ -35,6 +35,7 @@ struct HidInput int physical_max; int units; int units_exp; + int button_num; }; struct HidReport @@ -52,8 +53,29 @@ struct HidDecoded int maxReportLength; }; +enum HidUsages { + USAGE_POINTER = 0x01, + USAGE_JOYSTICK = 0x04, + USAGE_GAMEPAD = 0x05, + USAGE_MULTIAXIS = 0x08, + USAGE_BUTTON = 0x09, + USAGE_X = 0x30, + USAGE_Y = 0x31, + USAGE_Z = 0x32, + USAGE_RX = 0x33, + USAGE_RY = 0x34, + USAGE_RZ = 0x35, + USAGE_SLIDER = 0x36, + USAGE_DIAL = 0x37, + USAGE_WHEEL = 0x38, + USAGE_HAT = 0x39, + USAGE_START = 0x3D, + USAGE_SELECT = 0x3E, +}; + class HidDecoder { + public: HidDecoder(); @@ -67,6 +89,15 @@ public: */ HidDecoded parseDescriptor(const std::vector& descriptor); + + /** + * @brief Print out decoded information to stdout + * + * @param decoded an @a HidDecoded struct + */ + static void printDecodedInfo(HidDecoded decoded); + + /** * @brief extract the appropriate binary value from the data for a given HidInput * @@ -76,6 +107,28 @@ public: */ u_int64_t extractValue(const HidInput& input, const std::vector& data); + + const std::map usage_names_ = { + {USAGE_POINTER, "Pointer"}, + {USAGE_JOYSTICK, "Joystick"}, + {USAGE_GAMEPAD, "Gamepad"}, + {USAGE_MULTIAXIS, "Multi-axis Controller"}, + {USAGE_BUTTON, "Button"}, + {USAGE_X, "X"}, + {USAGE_Y, "Y"}, + {USAGE_Z, "Z"}, + {USAGE_RX, "Rx"}, + {USAGE_RY, "Ry"}, + {USAGE_RZ, "Rz"}, + {USAGE_SLIDER, "Slider"}, + {USAGE_DIAL, "Dial"}, + {USAGE_WHEEL, "Wheel"}, + {USAGE_HAT, "Hat switch"}, + {USAGE_START, "Start"}, + {USAGE_SELECT, "Select"}, + }; + + private: void init(); int convertDataToInt(const std::vector &data, bool isSigned); @@ -112,26 +165,6 @@ private: std::vector state_stack_; int button_base_ = 1; - std::map usage_names_ = { - {0x01, "Pointer"}, - {0x04, "Joystick"}, - {0x05, "Gamepad"}, - {0x09, "Button"}, - {0x08, "Multi-axis Controller"}, - {0x30, "X"}, - {0x31, "Y"}, - {0x32, "Z"}, - {0x33, "Rx"}, - {0x34, "Ry"}, - {0x35, "Rz"}, - {0x36, "Slider"}, - {0x37, "Dial"}, - {0x38, "Wheel"}, - {0x39, "Hat switch"}, - {0x3D, "Start"}, - {0x3E, "Select"}, - }; - }; // HidDecoder } // namespace diff --git a/include/idf/HidDevice.hh b/include/idf/HidDevice.hh index c907e9d..22834e2 100644 --- a/include/idf/HidDevice.hh +++ b/include/idf/HidDevice.hh @@ -20,14 +20,18 @@ namespace idf { * @date 2025-08-07 */ class HidDevice : public UsbDevice { - public: - HidDevice( int vendor, int product ); +public: + HidDevice( int vendor, int product, int interface ); HidDevice( HidDecoded ); virtual ~HidDevice() {}; + void printHidDescriptor(); + + void printDecodedHidInfo(); + /** * @brief open device with @a vendor and @a product, and parse the HID * report descriptor. @@ -38,16 +42,12 @@ class HidDevice : public UsbDevice { */ static HidDecoded decodeDevice( int vendor, int product); - void printDecodedInfo(); - - protected: +protected: HidDecoder decoder; HidDecoded decoded; - std::vectorhidReports; - std::vector hidReportDescriptor; virtual std::vector getHidReportDescriptor(); diff --git a/include/idf/HidGenericJoystick.hh b/include/idf/HidGenericJoystick.hh index 4a56795..34b46d4 100644 --- a/include/idf/HidGenericJoystick.hh +++ b/include/idf/HidGenericJoystick.hh @@ -32,10 +32,16 @@ class HidGenericJoystick : public HidDevice, public virtual GenericJoystick { public: - HidGenericJoystick(int vendor, int product); + HidGenericJoystick(int vendor, int product, int interface); void decode(const std::vector& data); + protected: + + // Flag indicating whether the report descriptor contained a Z axis. If so + // use that for twist. + bool useZForTwist; + }; } // namespace idf diff --git a/source/idf/GenericJoystick.cpp b/source/idf/GenericJoystick.cpp index 8279f42..b8ace98 100644 --- a/source/idf/GenericJoystick.cpp +++ b/source/idf/GenericJoystick.cpp @@ -2,25 +2,35 @@ namespace idf { + GenericJoystick::GenericJoystick() : forwardBackwardPivot(0, 1023, 512), leftRightPivot(0, 1023, 512), twist(0, 1023, 512), trigger(0,1), - slider(0, 1023, 512) { + slider(0, 1023, 512), + HatNorth(0,1), + HatNorthEast(0,1), + HatEast(0,1), + HatSouthEast(0,1), + HatSouth(0,1), + HatSouthWest(0,1), + HatWest(0,1), + HatNorthWest(0,1) { buttons = {}; } + const std::vector& GenericJoystick::getConfigurables() { - static std::vector inputs; - if (inputs.empty()) { - append(InputLayout::getConfigurables(), inputs); - inputs.push_back(Configurable(forwardBackwardPivot, "Forward/Backward Pivot", "forwardBackwardPivot")); - inputs.push_back(Configurable(leftRightPivot, "Left/Right Pivot", "leftRightPivot")); - inputs.push_back(Configurable(twist, "Twist", "twist")); - inputs.push_back(Configurable(slider, "Slider", "slider")); - } - return inputs; + static std::vector inputs; + if (inputs.empty()) { + append(InputLayout::getConfigurables(), inputs); + inputs.push_back(Configurable(forwardBackwardPivot, "Forward/Backward Pivot", "forwardBackwardPivot")); + inputs.push_back(Configurable(leftRightPivot, "Left/Right Pivot", "leftRightPivot")); + inputs.push_back(Configurable(twist, "Twist", "twist")); + inputs.push_back(Configurable(slider, "Slider", "slider")); + } + return inputs; } } // namespace idf diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index cc8177a..0b61fe2 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -1,309 +1,357 @@ #include "idf/HidDecoder.hh" #include #include +#include +#include +#include namespace idf { - HidDecoder::HidDecoder() { - init(); - } - - void HidDecoder::init() { - reports_.clear(); - inputs_.clear(); - bit_offset_ = 0; - report_id_ = 0; - usage_list_.clear(); - current_.report_size = 0; - current_.report_count = 0; - current_.usage_page = 0; - current_.usage_min = 0; - current_.usage_max = 0; - current_.logical_min = 0; - current_.logical_max = 0; - current_.physical_min = 0; - current_.physical_max = 0; - current_.units = 0; - current_.units_exp = 0; - device_type_ = "Unknown"; - state_stack_.clear(); - button_base_ = 1; - } - HidDecoded HidDecoder::parseDescriptor(const std::vector &descriptor) - { - init(); - uint i = 0; - - while (i < descriptor.size()) { - unsigned char prefix = descriptor[i]; - int size_code = prefix & 0x03; - int type_code = (prefix >> 2) & 0x03; - int tag_code = (prefix >> 4) & 0x0F; - int data = 0; - std::vector data_bytes; - - int size = size_code; - if (size_code == 3) { - size = 4; - } - - if (size > 0) { - data_bytes = std::vector(descriptor.begin() + i + 1, descriptor.begin() + i + 1 + size); - data = 0; - for (int j = 0; j < size; ++j) { - data |= (data_bytes[j] << (j * 8)); - } - } - - if (type_code == 0) { // Main - decodeMainItem(tag_code); - } - else if (type_code == 1) { // Global - decodeGlobalItem(tag_code, data, data_bytes); - } - else if (type_code == 2) { // Local - decodeLocalItem(tag_code, data); - } +HidDecoder::HidDecoder() { + init(); +} + + +void HidDecoder::init() { + reports_.clear(); + inputs_.clear(); + bit_offset_ = 0; + report_id_ = 0; + usage_list_.clear(); + current_.report_size = 0; + current_.report_count = 0; + current_.usage_page = 0; + current_.usage_min = 0; + current_.usage_max = 0; + current_.logical_min = 0; + current_.logical_max = 0; + current_.physical_min = 0; + current_.physical_max = 0; + current_.units = 0; + current_.units_exp = 0; + device_type_ = "Unknown"; + state_stack_.clear(); + button_base_ = 1; +} + + +HidDecoded HidDecoder::parseDescriptor(const std::vector &descriptor) +{ + init(); + uint i = 0; + + while (i < descriptor.size()) { + unsigned char prefix = descriptor[i]; + int size_code = prefix & 0x03; + int type_code = (prefix >> 2) & 0x03; + int tag_code = (prefix >> 4) & 0x0F; + int data = 0; + std::vector data_bytes; + + int size = size_code; + if (size_code == 3) { + size = 4; + } - i += 1 + size; + if (size > 0) { + data_bytes = std::vector(descriptor.begin() + i + 1, descriptor.begin() + i + 1 + size); + data = 0; + for (int j = 0; j < size; ++j) { + data |= (data_bytes[j] << (j * 8)); + } } - // add final report to list - reports_.push_back({ - report_id_, - inputs_, - bit_offset_ / 8, - report_id_ != 0 - }); - - int maxReport = -1; - for (HidReport rep : reports_) { - if (rep.inputs.size() > 0) { - maxReport = std::max(maxReport, rep.bytes_count); - } + if (type_code == 0) { // Main + decodeMainItem(tag_code); + } + else if (type_code == 1) { // Global + decodeGlobalItem(tag_code, data, data_bytes); + } + else if (type_code == 2) { // Local + decodeLocalItem(tag_code, data); } - return {device_type_, reports_, maxReport}; + i += 1 + size; } - void HidDecoder::decodeGlobalItem(int tag_code, int data, const std::vector &data_bytes) - { - if (tag_code == 0x0) { // Usage Page - current_.usage_page = data; - } - else if (tag_code == 0x1) { // Logical Minimum - current_.logical_min = convertDataToInt(data_bytes, true); - } - else if (tag_code == 0x2) { // Logical Maximum - current_.logical_max = convertDataToInt(data_bytes, true); + // add final report to list + reports_.push_back({ + report_id_, + inputs_, + bit_offset_ / 8, + report_id_ != 0 + }); + + int maxReport = -1; + for (HidReport rep : reports_) { + if (rep.inputs.size() > 0) { + maxReport = std::max(maxReport, rep.bytes_count); } - else if (tag_code == 0x3) { // Physical Minimum - current_.physical_min = convertDataToInt(data_bytes, interpretSigned()); + } + + return {device_type_, reports_, maxReport}; +} + + +void HidDecoder::decodeGlobalItem(int tag_code, int data, const std::vector &data_bytes) +{ + if (tag_code == 0x0) { // Usage Page + current_.usage_page = data; + } + else if (tag_code == 0x1) { // Logical Minimum + current_.logical_min = convertDataToInt(data_bytes, true); + } + else if (tag_code == 0x2) { // Logical Maximum + current_.logical_max = convertDataToInt(data_bytes, true); + } + else if (tag_code == 0x3) { // Physical Minimum + current_.physical_min = convertDataToInt(data_bytes, interpretSigned()); + } + else if (tag_code == 0x4) { // Physical Maximum + current_.physical_max = convertDataToInt(data_bytes, interpretSigned()); + } + else if (tag_code == 0x5) { // Unit exponent + current_.units_exp = static_cast(data_bytes[0]); + } + else if (tag_code == 0x6) { // Units + current_.units = data; + if (data == 0) + { + current_.units = 0; + current_.units_exp = 0; + current_.physical_min = 0; + current_.physical_max = 0; } - else if (tag_code == 0x4) { // Physical Maximum - current_.physical_max = convertDataToInt(data_bytes, interpretSigned()); + } + else if (tag_code == 0x7) { // Report Size + current_.report_size = data; + } + else if (tag_code == 0x8) { // Report ID + if (report_id_ != 0) + { // if not the 1st report, save inputs to a report + reports_.push_back({report_id_, + inputs_, + bit_offset_ / 8, + true}); + inputs_.clear(); } - else if (tag_code == 0x5) { // Unit exponent - current_.units_exp = static_cast(data_bytes[0]); + + bit_offset_ = 8; + report_id_ = data; + } + else if (tag_code == 0x9) { // Report Count + current_.report_count = data; + } + else if (tag_code == 0xA) { // Push + state_stack_.push_back(current_); + } + else if (tag_code == 0xB) { // Pop + if (!state_stack_.empty()) + { + HIDState tmp = state_stack_.back(); + current_ = state_stack_.back(); + current_.usage_page = tmp.usage_page; + current_.usage_min = tmp.usage_min; + current_.usage_max = tmp.usage_max; + current_.report_size = tmp.report_size; + current_.report_count = tmp.report_count; + current_.logical_min = tmp.logical_min; + current_.logical_max = tmp.logical_max; + current_.physical_min = tmp.physical_min; + current_.physical_max = tmp.physical_max; + current_.units = tmp.units; + current_.units_exp = tmp.units_exp; + state_stack_.pop_back(); } - else if (tag_code == 0x6) { // Units - current_.units = data; - if (data == 0) - { - current_.units = 0; - current_.units_exp = 0; - current_.physical_min = 0; - current_.physical_max = 0; + } +} + + +void HidDecoder::decodeLocalItem(int tag_code, int data) +{ + if (tag_code == 0x0) { // Usage + if (data < USAGE_X) { + if (device_type_ == "Unknown") { + device_type_ = usage_names_.at(data); } } - else if (tag_code == 0x7) { // Report Size - current_.report_size = data; + else { + usage_list_.push_back(data); } - else if (tag_code == 0x8) { // Report ID - if (report_id_ != 0) - { // if not the 1st report, save inputs to a report - reports_.push_back({report_id_, - inputs_, - bit_offset_ / 8, - true}); - inputs_.clear(); - } + } + else if (tag_code == 0x1) { // Usage Minimum + current_.usage_min = data; + } + else if (tag_code == 0x2) { // Usage Maximum + current_.usage_max = data; + } +} - bit_offset_ = 8; - report_id_ = data; - } - else if (tag_code == 0x9) { // Report Count - current_.report_count = data; - } - else if (tag_code == 0xA) { // Push - state_stack_.push_back(current_); - } - else if (tag_code == 0xB) { // Pop - if (!state_stack_.empty()) + +void HidDecoder::decodeMainItem(int tag_code) +{ + if (tag_code == 0x8) { // Create Input(s) from usage list HIDState information + std::vector expanded_usages; + if (current_.usage_min != 0 && current_.usage_max != 0) + { + for (int j = current_.usage_min; j <= current_.usage_max; ++j) { - HIDState tmp = state_stack_.back(); - current_ = state_stack_.back(); - current_.usage_page = tmp.usage_page; - current_.usage_min = tmp.usage_min; - current_.usage_max = tmp.usage_max; - current_.report_size = tmp.report_size; - current_.report_count = tmp.report_count; - current_.logical_min = tmp.logical_min; - current_.logical_max = tmp.logical_max; - current_.physical_min = tmp.physical_min; - current_.physical_max = tmp.physical_max; - current_.units = tmp.units; - current_.units_exp = tmp.units_exp; - state_stack_.pop_back(); + expanded_usages.push_back(j); } } - } + else if (!usage_list_.empty()) { + expanded_usages = usage_list_; + } - void HidDecoder::decodeLocalItem(int tag_code, int data) - { - if (tag_code == 0x0) { // Usage - if (data < 0x30) { - if (device_type_ == "Unknown") { - device_type_ = usage_names_.at(data); - } + for (uint j = 0; j < current_.report_count; ++j) { + std::string name; + u_int8_t usage = 0; + int btn_num = -1; + if (current_.usage_page == 0x01 && j < expanded_usages.size()) { + usage = expanded_usages[j]; + if (usage_names_.count(usage)) name = usage_names_.at(usage); + else name = "Unknown"; + } + else if (current_.usage_page == USAGE_BUTTON && j < expanded_usages.size()) { + usage = USAGE_BUTTON; + btn_num = button_base_; + name = "Button " + std::to_string(button_base_++); + } + else if (current_.usage_page == 0x01 && expanded_usages.size() == 1) { + usage = expanded_usages[0]; + if (usage_names_.count(usage)) name = usage_names_.at(usage); + else name = "Unknown"; } else { - usage_list_.push_back(data); + name = "Padding"; } - } - else if (tag_code == 0x1) { // Usage Minimum - current_.usage_min = data; - } - else if (tag_code == 0x2) { // Usage Maximum - current_.usage_max = data; - } - } - void HidDecoder::decodeMainItem(int tag_code) - { - if (tag_code == 0x8) { // Input(s) from current HIDState information - std::vector expanded_usages; - if (current_.usage_min != 0 && current_.usage_max != 0) - { - for (int j = current_.usage_min; j <= current_.usage_max; ++j) - { - expanded_usages.push_back(j); - } + int start_bit = bit_offset_; + int end_bit = bit_offset_ + current_.report_size - 1; + + int tmp_phys_min = current_.physical_min; + int tmp_phys_max = current_.physical_max; + if (current_.physical_min == 0) { + tmp_phys_min = current_.logical_min; } - else if (!usage_list_.empty()) { - expanded_usages = usage_list_; + if (current_.physical_max == 0) { + tmp_phys_max = current_.logical_max; } - for (uint j = 0; j < current_.report_count; ++j) { - std::string name; - u_int8_t usage = 0; - if (current_.usage_page == 0x01 && j < expanded_usages.size()) { - usage = expanded_usages[j]; - if (usage_names_.count(usage)) name = usage_names_.at(usage); - else name = "Unknown"; - } - else if (current_.usage_page == 0x09 && j < expanded_usages.size()) { - usage = 0x09; - name = "Button " + std::to_string(button_base_++); - } - else if (current_.usage_page == 0x01 && expanded_usages.size() == 1) { - usage = expanded_usages[0]; - if (usage_names_.count(usage)) name = usage_names_.at(usage); - else name = "Unknown"; - } - else { - name = "Padding"; - } + inputs_.push_back({usage, + name, + start_bit, + end_bit, + current_.logical_min, + current_.logical_max, + tmp_phys_min, + tmp_phys_max, + current_.units, + current_.units_exp, + btn_num}); + + bit_offset_ += current_.report_size; + } - int start_bit = bit_offset_; - int end_bit = bit_offset_ + current_.report_size - 1; + usage_list_.clear(); + current_.usage_min = 0; + current_.usage_max = 0; + } + else if (tag_code == 0x9 || tag_code == 0x0B) { // Output or Feature + usage_list_.clear(); + current_.usage_min = 0; + current_.usage_max = 0; + } +} - int tmp_phys_min = current_.physical_min; - int tmp_phys_max = current_.physical_max; - if (current_.physical_min == 0) { - tmp_phys_min = current_.logical_min; - } - if (current_.physical_max == 0) { - tmp_phys_max = current_.logical_max; - } - inputs_.push_back({usage, - name, - start_bit, - end_bit, - current_.logical_min, - current_.logical_max, - tmp_phys_min, - tmp_phys_max, - current_.units, - current_.units_exp}); - - bit_offset_ += current_.report_size; - } +bool HidDecoder::interpretSigned() { + return current_.logical_max < 0 || current_.logical_min < 0; +} - usage_list_.clear(); - current_.usage_min = 0; - current_.usage_max = 0; - } - else if (tag_code == 0x9 || tag_code == 0x0B) { // Output or Feature - usage_list_.clear(); - current_.usage_min = 0; - current_.usage_max = 0; - } - } - bool HidDecoder::interpretSigned() { - return current_.logical_max < 0 || current_.logical_min < 0; +int HidDecoder::convertDataToInt(const std::vector &data, bool isSigned) { + u_int32_t value = 0; + + for (uint i = 0; i < data.size(); ++i) { + value += (data[i] << 8*i); } - int HidDecoder::convertDataToInt(const std::vector &data, bool isSigned) { - u_int32_t value = 0; + if (!isSigned) return value; + + switch (data.size()) { + case 1: + if (value >= pow(2,7)) return static_cast(value); + break; + case 2: + if (value >= pow(2,15)) return static_cast(value); + break; + case 4: + if (value >= pow(2,31)) return static_cast(value); + break; + } + return value; +} - for (uint i = 0; i < data.size(); ++i) { - value += (data[i] << 8*i); - } - if (!isSigned) return value; - - switch (data.size()) { - case 1: - if (value >= pow(2,7)) return static_cast(value); - break; - case 2: - if (value >= pow(2,15)) return static_cast(value); - break; - case 4: - if (value >= pow(2,31)) return static_cast(value); - break; +void HidDecoder::printDecodedInfo(HidDecoded decoded) { + std::ostringstream ss; + ss << "Device Type: " << decoded.type << "\n"; + for (HidReport report : decoded.reports) { + ss << "Report: " << report.id << " (" << report.bytes_count << " bytes)\n"; + if (report.id != 0) { + ss << " Report ID bits 0:7 value: " << report.id << "\n"; + } + for(HidInput input : report.inputs) { + ss << " " << std::setfill(' ') << std::setw(15) << std::left << input.name; + if (input.start_bit == input.end_bit) { + ss << " bit " << std::setw(5) << std::right << input.start_bit << " range: "; + ss << std::setw(5) << std::right << input.logical_min << ":"; + ss << std::left << input.logical_max << "\n"; + } + else { + ss << " bits " << std::setw(5) << std::right << input.start_bit << ":"; + ss << std::setw(5) << std::left << input.end_bit; + ss << " range: "; + ss << std::setw(5) << std::right << input.logical_min << ":"; + ss << std::setw(5) << std::left << input.logical_max << " "; + ss << std::setw(5) << std::right << input.physical_min << ":"; + ss << std::setw(5) << std::left << input.physical_max << "\n"; + } } - return value; + ss << "\n"; } + printf(ss.str().c_str()); +} - u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector& data){ - int startByte = input.start_bit / 8; - int startBit = input.start_bit % 8; - int endByte = input.end_bit / 8; - int endBit = input.end_bit % 8; - u_int64_t temp = 0; - u_int64_t mask = 1; - - // bitmask - mask = (mask << (input.end_bit - input.start_bit +1)) - 1; - - // stack bytes in reverse to get continuous bits - for(int i = endByte; i >= startByte; --i) { - temp = temp << 8; - temp |= data[i]; - } - temp = temp >> startBit; - temp = temp & mask; - // testing output - // printf(" %s(%02x) %d[%d] --> %d[%d], mask(%lu), >> %u", input.name.c_str(), input.usage, startByte, startBit, endByte, endBit, mask, startBit); +u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector& data){ + if (!usage_names_.count(input.usage)) return 0; + int startByte = input.start_bit / 8; + int startBit = input.start_bit % 8; + int endByte = input.end_bit / 8; + u_int64_t temp = 0; + u_int64_t mask = 1; + + // bitmask + mask = (mask << (input.end_bit - input.start_bit +1)) - 1; - return temp; + // stack bytes in reverse to get continuous bits + for(int i = endByte; i >= startByte; --i) { + temp = temp << 8; + temp |= data[i]; } + temp = temp >> startBit; + temp = temp & mask; + + // testing output + // int endBit = input.end_bit % 8; + // printf(" %s(%02x) %d[%d] --> %d[%d], mask(%lu), >> %u = %llu\n", input.name.c_str(), input.usage, startByte, startBit, endByte, endBit, mask, startBit, temp); + + return temp; +} + } // namespace diff --git a/source/idf/HidDevice.cpp b/source/idf/HidDevice.cpp index 5239641..686a76a 100644 --- a/source/idf/HidDevice.cpp +++ b/source/idf/HidDevice.cpp @@ -1,21 +1,26 @@ #include "idf/HidDevice.hh" #include "idf/IOException.hh" -#include #include #include +#include +#include #include namespace idf { -HidDevice::HidDevice( int vendor, int product) : - HidDevice(HidDevice::decodeDevice(vendor, product)) {} + +HidDevice::HidDevice( int vendor, int product, int interface) : + HidDevice(HidDevice::decodeDevice(vendor, product)) { + addIdentification(Identification(vendor, product, interface)); + } + HidDevice::HidDevice(HidDecoded decoded_in) : UsbDevice("Generic " + decoded_in.type, decoded_in.maxReportLength), decoded(decoded_in) {} -HidDecoded HidDevice::decodeDevice(int vendor, int product) { +HidDecoded HidDevice::decodeDevice(int vendor, int product) { std::ostringstream ss; hid_device* hidDevice; unsigned char buffer[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; @@ -23,7 +28,7 @@ HidDecoded HidDevice::decodeDevice(int vendor, int product) { HidDecoded decDevice; if (!(hidDevice = hid_open(vendor, product, NULL))) { - ss << "unable to open device " << vendor << ":" << product << " : "; + ss << "unable to open device " << std::hex << vendor << ":" << product << " : "; ss << strerror(errno) << std::endl; throw IOException(ss.str()); } @@ -31,7 +36,7 @@ HidDecoded HidDevice::decodeDevice(int vendor, int product) { int descSize = hid_get_report_descriptor(hidDevice, buffer, sizeof(buffer)); if (descSize < 0) { - ss << "unable to get HID report descriptor from " << vendor << ":" << product << " : "; + ss << "unable to get HID report descriptor from " << std::hex << std::setw(4) << std::setfill('0') << vendor << ":" << product << " : "; ss << strerror(errno) << std::endl; hid_close(hidDevice); throw IOException(ss.str()); @@ -44,6 +49,7 @@ HidDecoded HidDevice::decodeDevice(int vendor, int product) { return decDevice; } + std::vector HidDevice::getHidReportDescriptor() { unsigned char buffer[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; int size = hid_get_report_descriptor(hidDevice, buffer, sizeof(buffer)); @@ -55,44 +61,24 @@ std::vector HidDevice::getHidReportDescriptor() { std::vector report(buffer, buffer + size); - printf("\x1b[39;49m\nHID Report Descriptor (%d bytes):\n ", size); - for(int i=0; i < size; ++i) { - printf("%02X ", buffer[i]); + return report; +} + + +void HidDevice::printHidDescriptor() { + std::vector report = getHidReportDescriptor(); + + printf("\x1b[39;49m\nHID Report Descriptor (%lu bytes):\n ", report.size()); + for(size_t i=0; i < report.size(); ++i) { + printf("%02X ", report[i]); if (i % 16 == 15) printf("\n "); else if (i % 8 == 7) printf(" "); } - - return report; } -void HidDevice::printDecodedInfo() { - std::ostringstream ss; - ss << "Device Type:" << decoded.type << "\n"; - for (HidReport report : decoded.reports) { - ss << "Report: " << report.id << " (" << report.bytes_count << " bytes)\n"; - if (report.id != 0) { - ss << " Report ID bits 0:7 value: " << report.id << "\n"; - } - for(HidInput input : report.inputs) { - ss << " " << std::setfill(' ') << std::setw(15) << std::left << input.name; - if (input.start_bit == input.end_bit) { - ss << " bit " << std::setw(5) << std::right << input.start_bit << " range: "; - ss << std::setw(5) << std::right << input.logical_min << ":"; - ss << std::left << input.logical_max << "\n"; - } - else { - ss << " bits " << std::setw(5) << std::right << input.start_bit << ":"; - ss << std::setw(5) << std::left << input.end_bit; - ss << " range: "; - ss << std::setw(5) << std::right << input.logical_min << ":"; - ss << std::setw(5) << std::left << input.logical_max << " "; - ss << std::setw(5) << std::right << input.physical_min << ":"; - ss << std::setw(5) << std::left << input.physical_max << "\n"; - } - } - ss << "\n"; - } - printf(ss.str().c_str()); + +void HidDevice::printDecodedHidInfo() { + decoder.printDecodedInfo(decoded); } } //namespace idf diff --git a/source/idf/HidGenericJoystick.cpp b/source/idf/HidGenericJoystick.cpp index ed52980..1c68047 100644 --- a/source/idf/HidGenericJoystick.cpp +++ b/source/idf/HidGenericJoystick.cpp @@ -2,16 +2,74 @@ namespace idf { -HidGenericJoystick::HidGenericJoystick(int vendor, int product) : - HidDevice(vendor, product) {} + +HidGenericJoystick::HidGenericJoystick(int vendor, int product, int interface) : + HidDevice(vendor, product, interface) { + // addIdentification(Identification(vendor, product, interface)); + // configure inputs and instantiate list of buttons + for( HidReport report : decoded.reports) { + for (HidInput input : report.inputs) { + switch(input.usage) { + case USAGE_BUTTON: + if (input.button_num == 1) + buttons.push_back(trigger); + else + buttons.push_back(SingleInput(0,1)); + break; + case USAGE_X: + leftRightPivot.configure(input.logical_min, input.logical_max); + break; + case USAGE_Y: + forwardBackwardPivot.configure(input.logical_min, input.logical_max); + break; + case USAGE_Z: + useZForTwist = true; + twist.configure(input.logical_min, input.logical_max); + break; + } + } + } + } + void HidGenericJoystick::decode(const std::vector& data) { - printf("decode something from:"); - for (int d : data) { - printf("%02X", d); + for (HidReport r : decoded.reports) { + if (!r.has_report_byte || (r.has_report_byte && static_cast(data[0]) == r.id)) { + for( HidInput input : r.inputs) { + u_int64_t value = decoder.extractValue(input, data); + switch (input.usage) { + case USAGE_BUTTON: + try { + buttons.at(input.button_num - 1).setValue(value); + } + catch (std::out_of_range & e) {} + break; + case USAGE_X: + leftRightPivot.setValue(value); break; + case USAGE_Y: + forwardBackwardPivot.setValue(value); break; + case USAGE_Z: + twist.setValue(value); break; + case USAGE_RZ: + if (!useZForTwist) twist.setValue(value); + break; + case USAGE_SLIDER: + slider.setValue(value); break; + case USAGE_HAT: + int hat = value - input.logical_min; + HatNorth.setValue(hat == 0); + HatNorthEast.setValue(hat == 1); + HatEast.setValue(hat == 2); + HatSouthEast.setValue(hat == 3); + HatSouth.setValue(hat == 4); + HatSouthWest.setValue(hat == 5); + HatWest.setValue(hat == 6); + HatNorthWest.setValue(hat == 7); + break; + } + } + } } - printf("\n"); } - } // namespace idf From 0cecf1f4e6f1e8497de70de72d699d577b25c91f Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Wed, 20 Aug 2025 11:57:14 -0500 Subject: [PATCH 05/16] change Generic JS button list to vector of pointers refs #103 --- include/idf/GenericJoystick.hh | 2 +- source/idf/HidGenericJoystick.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/idf/GenericJoystick.hh b/include/idf/GenericJoystick.hh index 3a7f7a1..c470de5 100644 --- a/include/idf/GenericJoystick.hh +++ b/include/idf/GenericJoystick.hh @@ -47,7 +47,7 @@ public: SingleInput trigger; // list of remaining buttons (if any) - std::vectorbuttons; + std::vectorbuttons; // the slider SingleInput slider; diff --git a/source/idf/HidGenericJoystick.cpp b/source/idf/HidGenericJoystick.cpp index 1c68047..15dd3f3 100644 --- a/source/idf/HidGenericJoystick.cpp +++ b/source/idf/HidGenericJoystick.cpp @@ -12,9 +12,9 @@ HidGenericJoystick::HidGenericJoystick(int vendor, int product, int interface) : switch(input.usage) { case USAGE_BUTTON: if (input.button_num == 1) - buttons.push_back(trigger); + buttons.push_back(&trigger); else - buttons.push_back(SingleInput(0,1)); + buttons.push_back(new SingleInput(0,1)); break; case USAGE_X: leftRightPivot.configure(input.logical_min, input.logical_max); @@ -40,7 +40,7 @@ void HidGenericJoystick::decode(const std::vector& data) { switch (input.usage) { case USAGE_BUTTON: try { - buttons.at(input.button_num - 1).setValue(value); + buttons.at(input.button_num - 1)->setValue(value); } catch (std::out_of_range & e) {} break; From f7b9b1aef975ec717dfede5bd55f0a0faba14ab8 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Wed, 20 Aug 2025 12:16:40 -0500 Subject: [PATCH 06/16] add soem comments #103 --- include/idf/HidDevice.hh | 22 +++++++++++++++++++++- include/idf/HidGenericJoystick.hh | 10 ++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/include/idf/HidDevice.hh b/include/idf/HidDevice.hh index 22834e2..b473969 100644 --- a/include/idf/HidDevice.hh +++ b/include/idf/HidDevice.hh @@ -22,19 +22,39 @@ namespace idf { class HidDevice : public UsbDevice { public: + + /** + * @brief Find the specified device, read and decode its HID Report + * Descriptor, then instantiate an HidDevice based on the decoded + * input information + * + * @param vendor HID Vendor ID to find the connected device + * @param product HID product ID to find the connected device + * @param interface specific interface of the connected device. HID + * Scanner @link https://github.com/nasa/IDF/wiki/HID-Scanner can + * help identify the correct interface + */ HidDevice( int vendor, int product, int interface ); HidDevice( HidDecoded ); virtual ~HidDevice() {}; + /** + * @brief convenience method for displaying the Report Descriptor + * in hex + */ void printHidDescriptor(); + /** + * @brief convenience method for displaying the available reports and + * inputs + */ void printDecodedHidInfo(); /** * @brief open device with @a vendor and @a product, and parse the HID - * report descriptor. + * report descriptor. This method exists mainly to ease instantiation * * @param vendor USB Vendor ID * @param product USB Product ID diff --git a/include/idf/HidGenericJoystick.hh b/include/idf/HidGenericJoystick.hh index 34b46d4..9ca4bcc 100644 --- a/include/idf/HidGenericJoystick.hh +++ b/include/idf/HidGenericJoystick.hh @@ -32,6 +32,16 @@ class HidGenericJoystick : public HidDevice, public virtual GenericJoystick { public: + /** + * @brief Construct a Generic Joystick derived from HID Report Descriptor + * of the given device + * + * @param vendor HID Vendor ID to find the connected device + * @param product HID product ID to find the connected device + * @param interface specific interface of the connected device. HID + * Scanner @link https://github.com/nasa/IDF/wiki/HID-Scanner can + * help identify the correct interface + */ HidGenericJoystick(int vendor, int product, int interface); void decode(const std::vector& data); From 13af57097c3d198956dcd906cac8e2f2f972695f Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Wed, 20 Aug 2025 14:05:46 -0500 Subject: [PATCH 07/16] apply const #103 --- include/idf/HidDecoder.hh | 10 +++++----- include/idf/HidDevice.hh | 6 +++--- include/idf/HidGenericJoystick.hh | 11 ++++++++--- source/idf/HidDecoder.cpp | 10 +++++----- source/idf/HidDevice.cpp | 6 +++--- source/idf/HidGenericJoystick.cpp | 2 +- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/include/idf/HidDecoder.hh b/include/idf/HidDecoder.hh index 1033fd6..e00d69f 100644 --- a/include/idf/HidDecoder.hh +++ b/include/idf/HidDecoder.hh @@ -95,7 +95,7 @@ public: * * @param decoded an @a HidDecoded struct */ - static void printDecodedInfo(HidDecoded decoded); + static void printDecodedInfo(const HidDecoded decoded); /** @@ -131,11 +131,11 @@ public: private: void init(); - int convertDataToInt(const std::vector &data, bool isSigned); + int convertDataToInt(const std::vector &data, const bool isSigned); bool interpretSigned(); - void decodeGlobalItem(int tag_code, int data, const std::vector& data_bytes); - void decodeLocalItem(int tag_code, int data); - void decodeMainItem(int tag_code); + void decodeGlobalItem(const int tag_code, const int data, const std::vector& data_bytes); + void decodeLocalItem(const int tag_code, const int data); + void decodeMainItem(const int tag_code); struct HIDState { uint report_size; diff --git a/include/idf/HidDevice.hh b/include/idf/HidDevice.hh index b473969..a528779 100644 --- a/include/idf/HidDevice.hh +++ b/include/idf/HidDevice.hh @@ -34,9 +34,9 @@ public: * Scanner @link https://github.com/nasa/IDF/wiki/HID-Scanner can * help identify the correct interface */ - HidDevice( int vendor, int product, int interface ); + HidDevice(const int vendor, const int product, const int interface); - HidDevice( HidDecoded ); + HidDevice(const HidDecoded); virtual ~HidDevice() {}; @@ -60,7 +60,7 @@ public: * @param product USB Product ID * @return HIDDecodedDevice struct enumerating the available reports */ - static HidDecoded decodeDevice( int vendor, int product); + static HidDecoded decodeDevice(const int vendor, const int product); protected: diff --git a/include/idf/HidGenericJoystick.hh b/include/idf/HidGenericJoystick.hh index 9ca4bcc..c7d8773 100644 --- a/include/idf/HidGenericJoystick.hh +++ b/include/idf/HidGenericJoystick.hh @@ -42,14 +42,19 @@ class HidGenericJoystick : public HidDevice, public virtual GenericJoystick { * Scanner @link https://github.com/nasa/IDF/wiki/HID-Scanner can * help identify the correct interface */ - HidGenericJoystick(int vendor, int product, int interface); + HidGenericJoystick(const int vendor, const int product, const int interface); void decode(const std::vector& data); protected: - // Flag indicating whether the report descriptor contained a Z axis. If so - // use that for twist. + /** + * @brief Flag indicating whether the report descriptor contained a Z axis. If so + use that for twist. Most joysticks will give the 'Rz' usage (0x35) for + the twist axis. But some may have additional inputs and often this + leads to using Z for twist. - This is NOT guaranteed, and may need to be + manually overridden. + */ bool useZForTwist; }; diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index 0b61fe2..144d3b9 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -95,7 +95,7 @@ HidDecoded HidDecoder::parseDescriptor(const std::vector &descrip } -void HidDecoder::decodeGlobalItem(int tag_code, int data, const std::vector &data_bytes) +void HidDecoder::decodeGlobalItem(const int tag_code, int data, const std::vector &data_bytes) { if (tag_code == 0x0) { // Usage Page current_.usage_page = data; @@ -169,7 +169,7 @@ void HidDecoder::decodeGlobalItem(int tag_code, int data, const std::vector expanded_usages; @@ -272,7 +272,7 @@ bool HidDecoder::interpretSigned() { } -int HidDecoder::convertDataToInt(const std::vector &data, bool isSigned) { +int HidDecoder::convertDataToInt(const std::vector &data, const bool isSigned) { u_int32_t value = 0; for (uint i = 0; i < data.size(); ++i) { @@ -296,7 +296,7 @@ int HidDecoder::convertDataToInt(const std::vector &data, bool is } -void HidDecoder::printDecodedInfo(HidDecoded decoded) { +void HidDecoder::printDecodedInfo(const HidDecoded decoded) { std::ostringstream ss; ss << "Device Type: " << decoded.type << "\n"; for (HidReport report : decoded.reports) { diff --git a/source/idf/HidDevice.cpp b/source/idf/HidDevice.cpp index 686a76a..4c953dc 100644 --- a/source/idf/HidDevice.cpp +++ b/source/idf/HidDevice.cpp @@ -9,18 +9,18 @@ namespace idf { -HidDevice::HidDevice( int vendor, int product, int interface) : +HidDevice::HidDevice(const int vendor, const int product, const int interface) : HidDevice(HidDevice::decodeDevice(vendor, product)) { addIdentification(Identification(vendor, product, interface)); } -HidDevice::HidDevice(HidDecoded decoded_in) : +HidDevice::HidDevice(const HidDecoded decoded_in) : UsbDevice("Generic " + decoded_in.type, decoded_in.maxReportLength), decoded(decoded_in) {} -HidDecoded HidDevice::decodeDevice(int vendor, int product) { +HidDecoded HidDevice::decodeDevice(const int vendor, const int product) { std::ostringstream ss; hid_device* hidDevice; unsigned char buffer[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; diff --git a/source/idf/HidGenericJoystick.cpp b/source/idf/HidGenericJoystick.cpp index 15dd3f3..9ba9fa9 100644 --- a/source/idf/HidGenericJoystick.cpp +++ b/source/idf/HidGenericJoystick.cpp @@ -3,7 +3,7 @@ namespace idf { -HidGenericJoystick::HidGenericJoystick(int vendor, int product, int interface) : +HidGenericJoystick::HidGenericJoystick(const int vendor, const int product, const int interface) : HidDevice(vendor, product, interface) { // addIdentification(Identification(vendor, product, interface)); // configure inputs and instantiate list of buttons From cec480bdf09a23c06e5ec2e0f4caffaf618db643 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Fri, 22 Aug 2025 16:52:04 -0500 Subject: [PATCH 08/16] add a cli hid decoder app #103 --- apps/decoder/.gitignore | 1 + apps/decoder/Makefile | 18 +++++ apps/decoder/decoder.cpp | 143 ++++++++++++++++++++++++++++++++++++++ include/idf/HidDecoder.hh | 6 +- source/idf/HidDecoder.cpp | 28 ++++---- 5 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 apps/decoder/.gitignore create mode 100644 apps/decoder/Makefile create mode 100644 apps/decoder/decoder.cpp diff --git a/apps/decoder/.gitignore b/apps/decoder/.gitignore new file mode 100644 index 0000000..541ce64 --- /dev/null +++ b/apps/decoder/.gitignore @@ -0,0 +1 @@ +decoder diff --git a/apps/decoder/Makefile b/apps/decoder/Makefile new file mode 100644 index 0000000..d6b3240 --- /dev/null +++ b/apps/decoder/Makefile @@ -0,0 +1,18 @@ +IDF_HOME := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..) +IDF_CONFIG := $(IDF_HOME)/bin/idf-config +IDF_BUILD_DIR := $(IDF_HOME)/build +IDF_LIB := $(IDF_BUILD_DIR)/libidf.a + +CXXFLAGS += $(shell $(IDF_CONFIG) --cxxflags) +LDLIBS += $(shell $(IDF_CONFIG) --libs) + +decoder: $(IDF_LIB) + +$(IDF_LIB): | $(IDF_BUILD_DIR) + cd $| && cmake .. && $(MAKE) + +$(IDF_BUILD_DIR): + mkdir -p $@ + +clean: + @rm -rf decoder diff --git a/apps/decoder/decoder.cpp b/apps/decoder/decoder.cpp new file mode 100644 index 0000000..71ca3dc --- /dev/null +++ b/apps/decoder/decoder.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "hidapi/hidapi/hidapi.h" +#include "idf/HidDecoder.hh" + +static volatile bool keepReading = true; + +void interruptHandler(int unused){ + keepReading = false; +} + +int main(int argc, char **args) { + + signal(SIGINT, interruptHandler); + + int result = hid_init(); + if (result < 0) { + printf("Failed to initialize HID library.\n"); + return -1; + } + + struct hid_device_info* enumerationHead = hid_enumerate(0, 0); + struct hid_device_info * deviceInfo; + size_t serialLength = std::wcslen(L"Serial #"); + size_t vendorLength = std::wcslen(L"Vendor"); + size_t pathLength = std::strlen("Path"); + for (deviceInfo = enumerationHead; deviceInfo; deviceInfo = deviceInfo->next) { + serialLength = std::max(serialLength, wcslen(deviceInfo->serial_number)); + vendorLength = std::max(vendorLength, wcslen(deviceInfo->manufacturer_string)); + pathLength = std::max(pathLength, strlen(deviceInfo->path)); + } + + printf("\nNOTE: If running as non-root, you must have udev rules in place allowing access to usb devices.\n\n"); + printf("Index %-*s Vendor ID Product ID %-*ls Interface # %-*ls Product\n", static_cast(pathLength), + "Path", static_cast(serialLength), L"Serial #", static_cast(vendorLength), L"Vendor"); + + int count = 0; + for (deviceInfo = enumerationHead; deviceInfo; deviceInfo = deviceInfo->next, ++count) { + printf("%5d %-*s 0x%04hX 0x%04hX %-*ls %11d %-*ls %ls\n", count, + static_cast(pathLength), deviceInfo->path, + deviceInfo->vendor_id, deviceInfo->product_id, + static_cast(serialLength), deviceInfo->serial_number, + deviceInfo->interface_number, + static_cast(vendorLength), deviceInfo->manufacturer_string, + deviceInfo->product_string); + } + + int selection = -1; + printf("\n"); + + while (selection < 0 || selection > count - 1) { + char buffer[1024]; + printf("Select a device to listen to: "); + fgets(buffer, sizeof(buffer), stdin); + sscanf(buffer, "%d", &selection); + } + + deviceInfo = enumerationHead; + for (count = 0; count < selection; ++count) { + deviceInfo = deviceInfo->next; + } + + hid_device* device = hid_open_path(deviceInfo->path); + if (!device) { + perror("Failed to open device"); + return -1; + } + + hid_free_enumeration(enumerationHead); + + hid_set_nonblocking(device, 1); + + unsigned char report[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; + int descriptor_size = hid_get_report_descriptor(device, report, sizeof(report)); + + if (descriptor_size < 0) { + perror("Error reading HID Report Descriptor from device"); + return -1; + } + + std::vector descriptor(report, report + descriptor_size); + std::vector dataVect; + + idf::HidDecoder decoder; + idf::HidDecoded devDecoded = decoder.parseDescriptor(descriptor); + + unsigned char readTest[devDecoded.maxReportLength]; + unsigned char data[devDecoded.maxReportLength]; + + decoder.printDecodedInfo(devDecoded); + + std::cout << "Press to start decoding..." << std::endl; + std::cin.ignore(); + + while (keepReading) { + int bytesRead = 0; + int prevBytesRead = 0; + do { + bytesRead = hid_read(device, readTest, sizeof(readTest)); + if (bytesRead > 0) { + memcpy(&data, &readTest, sizeof(data)); + prevBytesRead = bytesRead; + } + } while (bytesRead > 0); + + if (bytesRead < 0) { + perror("Error reading from device"); + if (errno == EINTR) keepReading = false; + else return -1; + } + else { + + dataVect.clear(); + dataVect.assign(data, data + prevBytesRead); + + // attempt to decode + for (idf::HidReport r : devDecoded.reports) { + if (!r.has_report_byte || (r.has_report_byte && static_cast(data[0]) == r.id)) { + printf("\x1b[39;49mDecode report (%d) values:\n", r.id); + for( idf::HidInput input : r.inputs) { + if (input.name != "Unknown") { + u_int64_t tmp = decoder.extractValue(input, dataVect, true); + } + } + } + } + + // a short sleep prevents the cursor from visually jumping all over the place + usleep(50000); + } + } + + hid_close(device); + + hid_exit(); + return 0; +} diff --git a/include/idf/HidDecoder.hh b/include/idf/HidDecoder.hh index e00d69f..5b495cd 100644 --- a/include/idf/HidDecoder.hh +++ b/include/idf/HidDecoder.hh @@ -105,7 +105,7 @@ public: * @param data data that was read in from device * @return u_int64_t raw binary data extracted from device. */ - u_int64_t extractValue(const HidInput& input, const std::vector& data); + u_int64_t extractValue(const HidInput& input, const std::vector& data, const bool print = false) const; const std::map usage_names_ = { @@ -131,11 +131,11 @@ public: private: void init(); - int convertDataToInt(const std::vector &data, const bool isSigned); - bool interpretSigned(); void decodeGlobalItem(const int tag_code, const int data, const std::vector& data_bytes); void decodeLocalItem(const int tag_code, const int data); void decodeMainItem(const int tag_code); + int convertDataToInt(const std::vector &data, const bool isSigned) const; + bool interpretSigned() const; struct HIDState { uint report_size; diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index 144d3b9..b75fc3c 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -151,7 +151,6 @@ void HidDecoder::decodeGlobalItem(const int tag_code, int data, const std::vecto if (!state_stack_.empty()) { HIDState tmp = state_stack_.back(); - current_ = state_stack_.back(); current_.usage_page = tmp.usage_page; current_.usage_min = tmp.usage_min; current_.usage_max = tmp.usage_max; @@ -267,12 +266,12 @@ void HidDecoder::decodeMainItem(const int tag_code) } -bool HidDecoder::interpretSigned() { +bool HidDecoder::interpretSigned() const { return current_.logical_max < 0 || current_.logical_min < 0; } -int HidDecoder::convertDataToInt(const std::vector &data, const bool isSigned) { +int HidDecoder::convertDataToInt(const std::vector &data, const bool isSigned) const { u_int32_t value = 0; for (uint i = 0; i < data.size(); ++i) { @@ -327,15 +326,14 @@ void HidDecoder::printDecodedInfo(const HidDecoded decoded) { } -u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector& data){ +u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector& data, const bool print) const { if (!usage_names_.count(input.usage)) return 0; int startByte = input.start_bit / 8; int startBit = input.start_bit % 8; int endByte = input.end_bit / 8; - u_int64_t temp = 0; - u_int64_t mask = 1; + u_int64_t temp = 0; // while HID theoretically allows an item to be 255 bytes, it is + u_int64_t mask = 1; // very unlikely that a single HC input will need even 4 bytes - // bitmask mask = (mask << (input.end_bit - input.start_bit +1)) - 1; // stack bytes in reverse to get continuous bits @@ -344,13 +342,19 @@ u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector> startBit; - temp = temp & mask; + temp = (temp >> startBit) & mask; - // testing output - // int endBit = input.end_bit % 8; - // printf(" %s(%02x) %d[%d] --> %d[%d], mask(%lu), >> %u = %llu\n", input.name.c_str(), input.usage, startByte, startBit, endByte, endBit, mask, startBit, temp); + if (print) { + int endBit = input.end_bit % 8; + std::string paddedName = input.name; + paddedName.append(11 - paddedName.length(), ' '); + std::cout << " " << paddedName \ + << std::setw(4) << std::right << startByte << '[' << std::setw(0) << startBit << "]-" \ + << std::left << endByte << '[' << endBit << "] " \ + << "mask(0x" << std::hex << mask << ") = " \ + << std::setw(7) << std::dec << temp << std::endl; + } return temp; } From 4e4c967a574a9d0c16a42ab3a4eafa1c4e4b08d5 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Wed, 27 Aug 2025 16:14:09 -0500 Subject: [PATCH 09/16] add Generic joystick mappings to the standard Controllers --- include/idf/SingleCameraController.hh | 14 +++++++++- include/idf/SingleFlightController.hh | 17 ++++++++++-- include/idf/SingleRoboticsController.hh | 14 +++++++++- source/idf/SingleCameraController.cpp | 21 +++++++++++++++ source/idf/SingleFlightController.cpp | 31 ++++++++++++++++++++++ source/idf/SingleRoboticsController.cpp | 35 +++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 4 deletions(-) diff --git a/include/idf/SingleCameraController.hh b/include/idf/SingleCameraController.hh index 5407cdd..092660e 100644 --- a/include/idf/SingleCameraController.hh +++ b/include/idf/SingleCameraController.hh @@ -22,6 +22,7 @@ LIBRARY DEPENDENCIES: ( #include "idf/DacoThc.hh" #include "idf/DualShock.hh" #include "idf/Extreme3dPro.hh" +#include "idf/GenericJoystick.hh" #include "idf/Gravis.hh" #include "idf/IndustrialProducts.hh" #include "idf/IndustrialProducts2.hh" @@ -274,10 +275,21 @@ class SingleCameraController : public CameraController { * * @param virpil the inputs to use in the default mapping * - * @return a new Virpil Constellation Alhpa based lander controller + * @return a new Virpil Constellation Alhpa based camera controller */ static SingleCameraController* createInstance(const VirpilConstellationAlpha& virpil); + /** + * creates a new SingleCameraController mapped to @a HidGenericJoystick using appropriate + * defaults. Since this is a generic mapping without specific knowledge of the + * devices physical layout, it is merely a best guess according to common practices. Thus, + * some outputs may not be mapped to an real input. + * + * @param js the inputs to use in the default mapping + * + * @return a new Generic HID based camera controller + */ + static SingleCameraController* createInstance(const GenericJoystick& js); }; } diff --git a/include/idf/SingleFlightController.hh b/include/idf/SingleFlightController.hh index f258b41..89e1ab4 100644 --- a/include/idf/SingleFlightController.hh +++ b/include/idf/SingleFlightController.hh @@ -22,6 +22,7 @@ LIBRARY DEPENDENCIES: ( #include "idf/DacoThc.hh" #include "idf/DualShock.hh" #include "idf/Extreme3dPro.hh" +#include "idf/GenericJoystick.hh" #include "idf/Gravis.hh" #include "idf/IndustrialProducts.hh" #include "idf/IndustrialProducts2.hh" @@ -278,7 +279,7 @@ class SingleFlightController : public FlightController { * * @param dacoThc the inputs to use in the default mapping * - * @return a new DacoThc-based camera controller + * @return a new DacoThc-based flight controller */ static SingleFlightController* createInstance(const DacoThc& dacoThc); @@ -288,10 +289,22 @@ class SingleFlightController : public FlightController { * * @param virpil the inputs to use in the default mapping * - * @return a new Virpil Constellation Alhpa based lander controller + * @return a new Virpil Constellation Alhpa based flight controller */ static SingleFlightController* createInstance(const VirpilConstellationAlpha& virpil); + /** + * creates a new SingleFlightController mapped to @a HidGenericJoystick using appropriate + * defaults. Since this is a generic mapping without specific knowledge of the + * devices physical layout, it is merely a best guess according to common practices. Thus, + * some outputs may not be mapped to an real input. + * + * @param js the inputs to use in the default mapping + * + * @return a new Generic HID based flight controller + */ + static SingleFlightController* createInstance(const GenericJoystick& js); + }; } diff --git a/include/idf/SingleRoboticsController.hh b/include/idf/SingleRoboticsController.hh index 2fab376..23c0578 100644 --- a/include/idf/SingleRoboticsController.hh +++ b/include/idf/SingleRoboticsController.hh @@ -22,6 +22,7 @@ LIBRARY DEPENDENCIES: ( #include "idf/DacoThc.hh" #include "idf/DualShock.hh" #include "idf/Extreme3dPro.hh" +#include "idf/GenericJoystick.hh" #include "idf/Gravis.hh" #include "idf/Er7Orion.hh" #include "idf/IndustrialProducts.hh" @@ -352,10 +353,21 @@ class SingleRoboticsController : public RoboticsController { * * @param virpil the inputs to use in the default mapping * - * @return a new Virpil Constellation Alhpa based lander controller + * @return a new Virpil Constellation Alhpa based robotics controller */ static SingleRoboticsController* createInstance(const VirpilConstellationAlpha& virpil); + /** + * creates a new SingleRoboticsController mapped to @a HidGenericJoystick using appropriate + * defaults. Since this is a generic mapping without specific knowledge of the + * devices physical layout, it is merely a best guess according to common practices. Thus, + * some outputs may not be mapped to an real input. + * + * @param js the inputs to use in the default mapping + * + * @return a new Generic HID based robotics controller + */ + static SingleRoboticsController* createInstance(const GenericJoystick& js); }; } diff --git a/source/idf/SingleCameraController.cpp b/source/idf/SingleCameraController.cpp index d9dafe4..ec3c9a6 100644 --- a/source/idf/SingleCameraController.cpp +++ b/source/idf/SingleCameraController.cpp @@ -305,4 +305,25 @@ SingleCameraController* SingleCameraController::createInstance( return controller; } +SingleCameraController* SingleCameraController::createInstance(const GenericJoystick& js) { + + CompositeInput* zoom = new CompositeInput(); + zoom->addInput(*js.buttons.at(0)); + zoom->addInput(*js.buttons.at(1), -1); + + SingleCameraController *controller = + new SingleCameraController( + js.twist, + js.forwardBackwardPivot, + js.leftRightPivot, + *zoom + ); + + controller->pan.setInverted(true); + controller->tilt.setInverted(true); + controller->zoom.setInverted(true); + + return controller; +} + } diff --git a/source/idf/SingleFlightController.cpp b/source/idf/SingleFlightController.cpp index ed4c070..94eb321 100644 --- a/source/idf/SingleFlightController.cpp +++ b/source/idf/SingleFlightController.cpp @@ -389,4 +389,35 @@ SingleFlightController* SingleFlightController::createInstance(const VirpilConst return controller; } +SingleFlightController* SingleFlightController::createInstance(const GenericJoystick& js) { + SingleInput* dummyInput = new SingleInput(-1, 1); + + CompositeInput* x = new CompositeInput(); + x->addInput(js.HatEast); + x->addInput(js.HatWest, -1); + + CompositeInput* y = new CompositeInput(); + y->addInput(js.HatNorth); + y->addInput(js.HatSouth, -1); + + CompositeInput* z = new CompositeInput(); + z->addInput(*js.buttons.at(1)); + z->addInput(*js.buttons.at(2), -1); + + SingleFlightController *controller = + new SingleFlightController( + js.twist, + js.forwardBackwardPivot, + js.leftRightPivot, + *dummyInput, + *dummyInput, + *dummyInput + ); + + controller->pitch.setInverted(true); + controller->yaw.setInverted(true); + + return controller; +} + } diff --git a/source/idf/SingleRoboticsController.cpp b/source/idf/SingleRoboticsController.cpp index 6e566da..c148d23 100644 --- a/source/idf/SingleRoboticsController.cpp +++ b/source/idf/SingleRoboticsController.cpp @@ -537,4 +537,39 @@ SingleRoboticsController* SingleRoboticsController::createInstance(const VirpilC return controller; } +SingleRoboticsController* SingleRoboticsController::createInstance(const GenericJoystick& js) { + + CompositeInput* x = new CompositeInput(); + x->addInput(js.HatEast); + x->addInput(js.HatWest, -1); + + CompositeInput* y = new CompositeInput(); + y->addInput(js.HatNorth); + y->addInput(js.HatSouth, -1); + + CompositeInput* z = new CompositeInput(); + z->addInput(*js.buttons.at(1)); + z->addInput(*js.buttons.at(2), -1); + + SingleRoboticsController *controller = + new SingleRoboticsController( + js.twist, + js.forwardBackwardPivot, + js.leftRightPivot, + *x, + *y, + *z, + js.trigger, + js.slider + ); + + controller->pitch.setInverted(true); + controller->yaw.setInverted(true); + controller->x.setInverted(true); + controller->y.setInverted(true); + controller->z.setInverted(true); + + return controller; +} + } From 34afaaf8c2bf99318c098502371dad42e12d11d9 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Thu, 28 Aug 2025 16:34:06 -0500 Subject: [PATCH 10/16] inproved output for decoder app --- apps/decoder/decoder.cpp | 48 +++++++++++++++++++++++++++++++++++++-- source/idf/HidDecoder.cpp | 10 ++++---- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/apps/decoder/decoder.cpp b/apps/decoder/decoder.cpp index 71ca3dc..5092777 100644 --- a/apps/decoder/decoder.cpp +++ b/apps/decoder/decoder.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "hidapi/hidapi/hidapi.h" #include "idf/HidDecoder.hh" @@ -120,19 +121,62 @@ int main(int argc, char **args) { dataVect.assign(data, data + prevBytesRead); // attempt to decode + + std::string btnStr = ""; + int btnStart = -1; + int btnCnt = 0; + int rows = 0; + for (idf::HidReport r : devDecoded.reports) { if (!r.has_report_byte || (r.has_report_byte && static_cast(data[0]) == r.id)) { printf("\x1b[39;49mDecode report (%d) values:\n", r.id); + ++rows; for( idf::HidInput input : r.inputs) { if (input.name != "Unknown") { - u_int64_t tmp = decoder.extractValue(input, dataVect, true); + + // cluster buttons together for easier reading + // while keeping them in order + if (input.usage == idf::USAGE_BUTTON) { + + if (btnCnt == 0) { + btnStart = input.button_num; + } + else if (btnCnt == 4) btnStr.append(" "); + + u_int64_t tmp = decoder.extractValue(input, dataVect, false); + btnStr.append(tmp & 0x1 ? "1" : "0"); + ++btnCnt; + + if (btnCnt == 8) { + std::cout << " Buttons " << std::setw(3) << std::right << btnStart << "-" << std::left << std::setw(3) << btnStart+btnCnt-1 << " " << btnStr.c_str() << std::endl; + btnCnt = 0; + btnStr.clear(); + rows++; + } + } // everything else + else { + if (btnCnt) {std::cout << " Buttons " << std::setw(3) << std::right << btnStart << "-" << std::left << std::setw(3) << btnStart+btnCnt-1 << " " << btnStr.c_str() << std::endl; + btnCnt = 0; + btnStr.clear(); + rows++; + } + + u_int64_t tmp = decoder.extractValue(input, dataVect, true); + if (decoder.usage_names_.count(input.usage)) rows++; + } } } } } // a short sleep prevents the cursor from visually jumping all over the place - usleep(50000); + usleep(100000); + if (!keepReading) break; + if (rows > 0) { + for (int i = 0; i < rows; ++i) { + std::cout << "\x1b[A"; + } + } } } diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index b75fc3c..90db1fe 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -347,12 +347,14 @@ u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vectortype, decoded_in->maxReportLength), + decoded(*decoded_in) {} -HidDecoded HidDevice::decodeDevice(const int vendor, const int product) { +HidDecoded* HidDevice::decodeDevice(const int vendor, const int product) { std::ostringstream ss; hid_device* hidDevice; unsigned char buffer[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; HidDecoder decoder; - HidDecoded decDevice; if (!(hidDevice = hid_open(vendor, product, NULL))) { ss << "unable to open device " << std::hex << vendor << ":" << product << " : "; @@ -43,7 +42,7 @@ HidDecoded HidDevice::decodeDevice(const int vendor, const int product) { } std::vector descriptor(buffer, buffer + descSize); - decDevice = decoder.parseDescriptor(descriptor); + HidDecoded* decDevice = decoder.parseDescriptor(descriptor); hid_close(hidDevice); return decDevice; diff --git a/source/idf/HidGenericJoystick.cpp b/source/idf/HidGenericJoystick.cpp index 9ba9fa9..829c64f 100644 --- a/source/idf/HidGenericJoystick.cpp +++ b/source/idf/HidGenericJoystick.cpp @@ -5,7 +5,6 @@ namespace idf { HidGenericJoystick::HidGenericJoystick(const int vendor, const int product, const int interface) : HidDevice(vendor, product, interface) { - // addIdentification(Identification(vendor, product, interface)); // configure inputs and instantiate list of buttons for( HidReport report : decoded.reports) { for (HidInput input : report.inputs) { From 62946058043605efa6b49b6fe08b07eb8be1f31a Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Fri, 29 Aug 2025 16:45:13 -0500 Subject: [PATCH 12/16] add button get method for Generic Joystick --- include/idf/GenericJoystick.hh | 8 ++++++++ source/idf/GenericJoystick.cpp | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/include/idf/GenericJoystick.hh b/include/idf/GenericJoystick.hh index c470de5..d4362e6 100644 --- a/include/idf/GenericJoystick.hh +++ b/include/idf/GenericJoystick.hh @@ -62,6 +62,14 @@ public: SingleInput HatWest; SingleInput HatNorthWest; + /** + * @brief Get the Nth Button object + * + * @param number number of button (1-based indexing) + * @return SingleInput* pointer to the appropriate button or null + */ + const SingleInput* getButton(const int number); + protected: virtual const std::vector& getConfigurables(); diff --git a/source/idf/GenericJoystick.cpp b/source/idf/GenericJoystick.cpp index b8ace98..a2387de 100644 --- a/source/idf/GenericJoystick.cpp +++ b/source/idf/GenericJoystick.cpp @@ -1,4 +1,5 @@ #include "idf/GenericJoystick.hh" +#include namespace idf { @@ -20,6 +21,14 @@ GenericJoystick::GenericJoystick() : buttons = {}; } +const SingleInput* GenericJoystick::getButton(const int number) { + try { + return buttons.at(number-1); + } + catch (const std::out_of_range& e) { + return NULL; + } +} const std::vector& GenericJoystick::getConfigurables() { static std::vector inputs; From 92e2d1df172752e036be0e0a9818b36c82679741 Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Fri, 29 Aug 2025 16:46:40 -0500 Subject: [PATCH 13/16] add trick example for using HidGenericJoystick --- .../RUN_test/generic_hc_config.py | 53 ++++++++++ .../SIM_python_generic/RUN_test/input.py | 18 ++++ .../examples/SIM_python_generic/S_define | 99 +++++++++++++++++++ .../SIM_python_generic/S_overrides.mk | 1 + 4 files changed, 171 insertions(+) create mode 100644 3rdParty/trick/examples/SIM_python_generic/RUN_test/generic_hc_config.py create mode 100644 3rdParty/trick/examples/SIM_python_generic/RUN_test/input.py create mode 100644 3rdParty/trick/examples/SIM_python_generic/S_define create mode 100644 3rdParty/trick/examples/SIM_python_generic/S_overrides.mk diff --git a/3rdParty/trick/examples/SIM_python_generic/RUN_test/generic_hc_config.py b/3rdParty/trick/examples/SIM_python_generic/RUN_test/generic_hc_config.py new file mode 100644 index 0000000..f1e2e8f --- /dev/null +++ b/3rdParty/trick/examples/SIM_python_generic/RUN_test/generic_hc_config.py @@ -0,0 +1,53 @@ +import sys +import traceback + +# Instantiate a Generic Joystick using its Vendor ID, Product ID and Interface ID +# Use IDF/apps/hidScanner to assist with getting this information. +# In this example, we are creating a mapping for a +# Thrustmaster AVA Base: https://eshop.thrustmaster.com/en_us/ava-base.html +# with an F16 grip: https://eshop.thrustmaster.com/en_us/viper-hotas-add-on-grip.html +try: + device = trick.HidGenericJoystick( 0x044F, 0x0415, 0 ) + device.thisown = 0 + self.addMasterDevice( device ) +except Exception as e: + print( "IDF init error:", traceback.format_exc() ) + sys.exit(1) + +# Create a SingleFlightController, including compositing X, Y, and Z from buttons +# use IDF/apps/decoder to assist with figuring out which buttons to use + +# this grip does not have a twist so we'll use some buttons +twist = trick.CompositeInput() +twist.thisown = 0 +twist.addInput( device.getButton(14) ) +twist.addInput( device.getButton(12), -1 ) + +x = trick.CompositeInput() +x.thisown = 0 +x.addInput( device.getButton(15) ) # the buttons vector is zero indexed +x.addInput( device.getButton(17), -1 ) + +y = trick.CompositeInput() +y.thisown = 0 +y.addInput( device.getButton(18) ) +y.addInput( device.getButton(16), -1 ) + +z = trick.CompositeInput() +z.thisown = 0 +z.addInput( device.getButton(7) ) +z.addInput( device.getButton(9), -1 ) + +controller = trick.SingleFlightController( + device.leftRightPivot, + device.forwardBackwardPivot, + twist, + x, y, z +) +controller.roll.setInverted( True ) +controller.pitch.setInverted( True ) +controller.thisown = 0 # don't forget this!! + + +# add controller to the InputDeviceManager +self.inputDeviceManager.controller = controller \ No newline at end of file diff --git a/3rdParty/trick/examples/SIM_python_generic/RUN_test/input.py b/3rdParty/trick/examples/SIM_python_generic/RUN_test/input.py new file mode 100644 index 0000000..add26d8 --- /dev/null +++ b/3rdParty/trick/examples/SIM_python_generic/RUN_test/input.py @@ -0,0 +1,18 @@ +trick.real_time_enable() +trick.exec_set_software_frame(0.1) + +# import IDF's modules +import idf.config + +# inherit from IDF's Configurator +class Configurator(idf.config.Configurator) : + + # this method is abstract in the base class, and so we must override it + def addMasterDevice(self, device): + + # Despite being abstract in the base class, this method is nevertheless implemented there. + # (It calls addInputDevice for you). Don't forget to call it! + super(Configurator, self).addMasterDevice(device) + +# Instantiate a Configurator, passing it the IdfInputDeviceManager instance from the S_define. +configurator = Configurator(example, configFile="RUN_test/generic_hc_config.py") diff --git a/3rdParty/trick/examples/SIM_python_generic/S_define b/3rdParty/trick/examples/SIM_python_generic/S_define new file mode 100644 index 0000000..4ca30df --- /dev/null +++ b/3rdParty/trick/examples/SIM_python_generic/S_define @@ -0,0 +1,99 @@ +#include "sim_objects/default_trick_sys.sm" +#include "idf/IdfInputDeviceManager.sm" + +##include +##include + +##include "sim_services/Executive/include/exec_proto.h" +##include "idf/SingleFlightController.hh" +##include "idf/Deadband.hh" + +/** + * Demonstrates the use of IDF within a Trick simulation, using the Python + * class idf.config.Configurator to add a device at run-time. See + * RUN_test/input.py. + */ +class Example : public IdfInputDeviceManager { + + public: + + /** + * While other examples have declared devices here, we want to leave the + * choice of device up to the user this time. In this example, our only + * concern is the Controller. + */ + + /** + * While devices represent physical hardware, a controller represents a + * virtual interface into a control system. This separation allows + * control system developers to program against a domain-specific interface + * independent of any particular device that might service that + * interface. It further ensures that only devices that have been + * specifically mapped to the interface may be assigned to service it. + */ + idf::SingleFlightController *controller; + + Example() : + /** + * Call the base class constructor, specifying the period at which the + * updateDevices() job will be called. You can optionally specify the + * phase and class for this job as well. See IdfInputDeviceManager.sm + * for details. + */ + IdfInputDeviceManager(0.1), + controller(NULL) { + /** + * Nothing to see here! + * All of the configuration is done in the input file. + */ + } + + /** + * If you want to perform some action after all devices have been updated, + * one possibility is overriding IdfInputDeviceManager's updateDevices() + * function. In this example, we just want to print the controller's + * normalized values to the console. Normalized values always fall in the + * range [-1, 1], with the extremes corresponing to the minimum and maximum + * raw values. 0 represents the "neutral" point. If you choose this method, + * be sure to call the base class version of the function or the devices + * will never be updated! + * + * Another option is to just declare another scheduled job and do your post- + * update logic there. This can be useful if you want the updateDevices() + * job and your post-update logic to run at different rates or have + * different phases or job classes. + */ + void updateDevices() { + + // Don't forget to call the base class version! + IdfInputDeviceManager::updateDevices(); + + /** + * The controller pointer is assigned via the input file (see Configurator.addMasterDevice). + * If no such assignment was made, then there is no physical device connected to the + * computer. In this case, IDF would launch the Virtual Hand Controller if the + * idf.config.Configurator was instantiated with support for the VHC. This sim does not + * do so (see SIM_virtual_hand_controller for an example that does), so we terminate. + */ + if (!controller) { + exec_terminate("S_define", "This sim requires a physical device to be connected."); + } + + bool blah = controller->isActive(); + + // Print the values. + std::cout << std::showpos << std::fixed << std::setprecision(2) + + << " Roll: " << controller->getRoll() + << " Pitch: " << controller->getPitch() + << " Yaw: " << controller->getYaw() + << " X: " << controller->getX() + << " Y: " << controller->getY() + << " Z: " << controller->getZ() + + << std::endl; + } + +}; + +Example example; diff --git a/3rdParty/trick/examples/SIM_python_generic/S_overrides.mk b/3rdParty/trick/examples/SIM_python_generic/S_overrides.mk new file mode 100644 index 0000000..0b8a3a9 --- /dev/null +++ b/3rdParty/trick/examples/SIM_python_generic/S_overrides.mk @@ -0,0 +1 @@ +include ../../makefiles/core.mk From ce8ab0a5467e01224b669983af9ec50f8bf3de0b Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Fri, 24 Oct 2025 16:20:29 -0500 Subject: [PATCH 14/16] tidy up HidDecoder --- apps/decoder/decoder.cpp | 3 +- include/idf/HidDecoder.hh | 102 +++++++--- source/idf/HidDecoder.cpp | 418 ++++++++++++++++++++++---------------- 3 files changed, 320 insertions(+), 203 deletions(-) diff --git a/apps/decoder/decoder.cpp b/apps/decoder/decoder.cpp index ca20e64..a178f4e 100644 --- a/apps/decoder/decoder.cpp +++ b/apps/decoder/decoder.cpp @@ -162,7 +162,8 @@ int main(int argc, char **args) { } u_int64_t tmp = decoder.extractValue(input, dataVect, true); - if (decoder.usage_names_.count(input.usage)) rows++; + + if (decoder.isUsageKnown(input.usage)) rows++; } } } diff --git a/include/idf/HidDecoder.hh b/include/idf/HidDecoder.hh index a447240..4673a27 100644 --- a/include/idf/HidDecoder.hh +++ b/include/idf/HidDecoder.hh @@ -73,6 +73,42 @@ enum HidUsages { USAGE_SELECT = 0x3E, }; +enum MainItemTag { + MAIN_INPUT = 0x8, + MAIN_OUTPUT = 0x9, + MAIN_COLLECTION = 0xA, + MAIN_FEATURE = 0xB, + MAIN_END_COLLECTION = 0xC, +}; + +enum GlobalItemTag { + GLOBAL_USAGE_PAGE = 0x0, + GLOBAL_LOGICAL_MINIMUM = 0x1, + GLOBAL_LOGICAL_MAXIMUM = 0x2, + GLOBAL_PHYSICAL_MINIMUM = 0x3, + GLOBAL_PHYSICAL_MAXIMUM = 0x4, + GLOBAL_UNIT_EXPONENT = 0x5, + GLOBAL_UNITS = 0x6, + GLOBAL_REPORT_SIZE = 0x7, + GLOBAL_REPORT_ID = 0x8, + GLOBAL_REPORT_COUNT = 0x9, + GLOBAL_PUSH = 0xA, + GLOBAL_POP = 0xB, +}; + +enum LocalItemTags { + LOCAL_USAGE = 0x0, + LOCAL_MINIMUM = 0x1, + LOCAL_MAXIMUM = 0x2, + LOCAL_DESIGNATOR_IDX = 0x3, + LOCAL_DESIGNATOR_MINIMUM = 0x4, + LOCAL_DESIGNATOR_MAXIMUM = 0x5, + LOCAL_STRING_IDX = 0x7, + LOCAL_STRING_MINIMUM = 0x8, + LOCAL_STRING_MAXIMUM = 0x9, + LOCAL_DELIMITER = 0xA, +}; + class HidDecoder { @@ -107,33 +143,15 @@ public: */ u_int64_t extractValue(const HidInput& input, const std::vector& data, const bool print = false) const; - - const std::map usage_names_ = { - {USAGE_POINTER, "Pointer"}, - {USAGE_JOYSTICK, "Joystick"}, - {USAGE_GAMEPAD, "Gamepad"}, - {USAGE_MULTIAXIS, "Multi-axis Controller"}, - {USAGE_BUTTON, "Button"}, - {USAGE_X, "X"}, - {USAGE_Y, "Y"}, - {USAGE_Z, "Z"}, - {USAGE_RX, "Rx"}, - {USAGE_RY, "Ry"}, - {USAGE_RZ, "Rz"}, - {USAGE_SLIDER, "Slider"}, - {USAGE_DIAL, "Dial"}, - {USAGE_WHEEL, "Wheel"}, - {USAGE_HAT, "Hat switch"}, - {USAGE_START, "Start"}, - {USAGE_SELECT, "Select"}, - }; - + bool isUsageKnown(const uint usage) const; + std::string usageName(const uint usage) const; private: void init(); void decodeGlobalItem(const int tag_code, const int data, const std::vector& data_bytes); void decodeLocalItem(const int tag_code, const int data); void decodeMainItem(const int tag_code); + void createInputs(); int convertDataToInt(const std::vector &data, const bool isSigned) const; bool interpretSigned() const; @@ -151,19 +169,39 @@ private: int units_exp; }; - int vendorId_; - int productId_; + int vendorId; + int productId; + + std::string device_type = "Unknown"; + std::vector reports; + std::vector inputs; + int bit_offset = 0; + int report_id = 0; + std::vector usage_list; - std::string device_type_ = "Unknown"; - std::vector reports_; - std::vector inputs_; - int bit_offset_ = 0; - int report_id_ = 0; - std::vector usage_list_; + HIDState state; + std::vector state_stack; + int next_button = 1; - HIDState current_; - std::vector state_stack_; - int button_base_ = 1; + const std::map usage_names = { + {USAGE_POINTER, "Pointer"}, + {USAGE_JOYSTICK, "Joystick"}, + {USAGE_GAMEPAD, "Gamepad"}, + {USAGE_MULTIAXIS, "Multi-axis Controller"}, + {USAGE_BUTTON, "Button"}, + {USAGE_X, "X"}, + {USAGE_Y, "Y"}, + {USAGE_Z, "Z"}, + {USAGE_RX, "Rx"}, + {USAGE_RY, "Ry"}, + {USAGE_RZ, "Rz"}, + {USAGE_SLIDER, "Slider"}, + {USAGE_DIAL, "Dial"}, + {USAGE_WHEEL, "Wheel"}, + {USAGE_HAT, "Hat switch"}, + {USAGE_START, "Start"}, + {USAGE_SELECT, "Select"}, + }; }; // HidDecoder diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index 6aeda4a..fb832a2 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -9,31 +9,33 @@ namespace idf { -HidDecoder::HidDecoder() { +HidDecoder::HidDecoder() +{ init(); } -void HidDecoder::init() { - reports_.clear(); - inputs_.clear(); - bit_offset_ = 0; - report_id_ = 0; - usage_list_.clear(); - current_.report_size = 0; - current_.report_count = 0; - current_.usage_page = 0; - current_.usage_min = 0; - current_.usage_max = 0; - current_.logical_min = 0; - current_.logical_max = 0; - current_.physical_min = 0; - current_.physical_max = 0; - current_.units = 0; - current_.units_exp = 0; - device_type_ = "Unknown"; - state_stack_.clear(); - button_base_ = 1; +void HidDecoder::init() +{ + reports.clear(); + inputs.clear(); + bit_offset = 0; + report_id = 0; + usage_list.clear(); + state.report_size = 0; + state.report_count = 0; + state.usage_page = 0; + state.usage_min = 0; + state.usage_max = 0; + state.logical_min = 0; + state.logical_max = 0; + state.physical_min = 0; + state.physical_max = 0; + state.units = 0; + state.units_exp = 0; + device_type = "Unknown"; + state_stack.clear(); + next_button = 1; } @@ -77,15 +79,15 @@ HidDecoded* HidDecoder::parseDescriptor(const std::vector &descri } // add final report to list - reports_.push_back({ - report_id_, - inputs_, - bit_offset_ / 8, - report_id_ != 0 + reports.push_back({ + report_id, + inputs, + bit_offset / 8, + report_id != 0 }); int maxReport = -1; - for (HidReport rep : reports_) { + for (HidReport rep : reports) { if (rep.inputs.size() > 0) { maxReport = std::max(maxReport, rep.bytes_count); } @@ -93,8 +95,8 @@ HidDecoded* HidDecoder::parseDescriptor(const std::vector &descri HidDecoded* decoded = new HidDecoded(); - decoded->type = device_type_; - decoded->reports = reports_; + decoded->type = device_type; + decoded->reports = reports; decoded->maxReportLength = maxReport; return decoded; @@ -103,181 +105,222 @@ HidDecoded* HidDecoder::parseDescriptor(const std::vector &descri void HidDecoder::decodeGlobalItem(const int tag_code, int data, const std::vector &data_bytes) { - if (tag_code == 0x0) { // Usage Page - current_.usage_page = data; - } - else if (tag_code == 0x1) { // Logical Minimum - current_.logical_min = convertDataToInt(data_bytes, true); - } - else if (tag_code == 0x2) { // Logical Maximum - current_.logical_max = convertDataToInt(data_bytes, true); - } - else if (tag_code == 0x3) { // Physical Minimum - current_.physical_min = convertDataToInt(data_bytes, interpretSigned()); - } - else if (tag_code == 0x4) { // Physical Maximum - current_.physical_max = convertDataToInt(data_bytes, interpretSigned()); - } - else if (tag_code == 0x5) { // Unit exponent - current_.units_exp = static_cast(data_bytes[0]); - } - else if (tag_code == 0x6) { // Units - current_.units = data; + switch(tag_code) { + case GLOBAL_USAGE_PAGE: + state.usage_page = data; + break; + + case GLOBAL_LOGICAL_MINIMUM: + state.logical_min = convertDataToInt(data_bytes, true); + break; + + case GLOBAL_LOGICAL_MAXIMUM: + state.logical_max = convertDataToInt(data_bytes, true); + break; + + case GLOBAL_PHYSICAL_MINIMUM: + state.physical_min = convertDataToInt(data_bytes, interpretSigned()); + break; + + case GLOBAL_PHYSICAL_MAXIMUM: + state.physical_max = convertDataToInt(data_bytes, interpretSigned()); + break; + + case GLOBAL_UNIT_EXPONENT: + state.units_exp = static_cast(data_bytes[0]); + break; + + case GLOBAL_UNITS: + state.units = data; if (data == 0) { - current_.units = 0; - current_.units_exp = 0; - current_.physical_min = 0; - current_.physical_max = 0; + state.units = 0; + state.units_exp = 0; + state.physical_min = 0; + state.physical_max = 0; } - } - else if (tag_code == 0x7) { // Report Size - current_.report_size = data; - } - else if (tag_code == 0x8) { // Report ID - if (report_id_ != 0) + break; + + case GLOBAL_REPORT_SIZE: + state.report_size = data; + break; + + case GLOBAL_REPORT_ID: + if (report_id != 0) { // if not the 1st report, save inputs to a report - reports_.push_back({report_id_, - inputs_, - bit_offset_ / 8, + reports.push_back({report_id, + inputs, + bit_offset / 8, true}); - inputs_.clear(); + inputs.clear(); } - bit_offset_ = 8; - report_id_ = data; - } - else if (tag_code == 0x9) { // Report Count - current_.report_count = data; - } - else if (tag_code == 0xA) { // Push - state_stack_.push_back(current_); - } - else if (tag_code == 0xB) { // Pop - if (!state_stack_.empty()) + bit_offset = 8; + report_id = data; + break; + + case GLOBAL_REPORT_COUNT: + state.report_count = data; + break; + + case GLOBAL_PUSH: + state_stack.push_back(state); + break; + + case GLOBAL_POP: + if (!state_stack.empty()) { - HIDState tmp = state_stack_.back(); - current_.usage_page = tmp.usage_page; - current_.usage_min = tmp.usage_min; - current_.usage_max = tmp.usage_max; - current_.report_size = tmp.report_size; - current_.report_count = tmp.report_count; - current_.logical_min = tmp.logical_min; - current_.logical_max = tmp.logical_max; - current_.physical_min = tmp.physical_min; - current_.physical_max = tmp.physical_max; - current_.units = tmp.units; - current_.units_exp = tmp.units_exp; - state_stack_.pop_back(); + HIDState tmp = state_stack.back(); + state.usage_page = tmp.usage_page; + state.usage_min = tmp.usage_min; + state.usage_max = tmp.usage_max; + state.report_size = tmp.report_size; + state.report_count = tmp.report_count; + state.logical_min = tmp.logical_min; + state.logical_max = tmp.logical_max; + state.physical_min = tmp.physical_min; + state.physical_max = tmp.physical_max; + state.units = tmp.units; + state.units_exp = tmp.units_exp; + state_stack.pop_back(); } + break; + + default: + break; } } void HidDecoder::decodeLocalItem(const int tag_code, const int data) { - if (tag_code == 0x0) { // Usage - if (data < USAGE_X) { - if (device_type_ == "Unknown") { - device_type_ = usage_names_.at(data); + switch(tag_code) { + case LOCAL_USAGE: + if (data < USAGE_X){ + if (device_type == "Unknown") { + device_type = usageName(data); } } else { - usage_list_.push_back(data); + usage_list.push_back(data); } - } - else if (tag_code == 0x1) { // Usage Minimum - current_.usage_min = data; - } - else if (tag_code == 0x2) { // Usage Maximum - current_.usage_max = data; + break; + + case LOCAL_MINIMUM: + state.usage_min = data; + break; + + case LOCAL_MAXIMUM: + state.usage_max = data; + break; + + case LOCAL_DESIGNATOR_IDX: // not implemented + case LOCAL_DESIGNATOR_MINIMUM: // not implemented + case LOCAL_DESIGNATOR_MAXIMUM: // not implemented + case LOCAL_STRING_IDX: // not implemented + case LOCAL_STRING_MINIMUM: // not implemented + case LOCAL_STRING_MAXIMUM: // not implemented + case LOCAL_DELIMITER: // not implemented + default: break; // items not yet defined } } void HidDecoder::decodeMainItem(const int tag_code) { - if (tag_code == 0x8) { // Create Input(s) from usage list HIDState information - std::vector expanded_usages; - if (current_.usage_min != 0 && current_.usage_max != 0) - { - for (int j = current_.usage_min; j <= current_.usage_max; ++j) - { - expanded_usages.push_back(j); - } - } - else if (!usage_list_.empty()) { - expanded_usages = usage_list_; - } + switch(tag_code) { + case MAIN_INPUT: + createInputs(); + break; - for (uint j = 0; j < current_.report_count; ++j) { - std::string name; - u_int8_t usage = 0; - int btn_num = -1; - if (current_.usage_page == 0x01 && j < expanded_usages.size()) { - usage = expanded_usages[j]; - if (usage_names_.count(usage)) name = usage_names_.at(usage); - else name = "Unknown"; - } - else if (current_.usage_page == USAGE_BUTTON && j < expanded_usages.size()) { - usage = USAGE_BUTTON; - btn_num = button_base_; - name = "Button " + std::to_string(button_base_++); - } - else if (current_.usage_page == 0x01 && expanded_usages.size() == 1) { - usage = expanded_usages[0]; - if (usage_names_.count(usage)) name = usage_names_.at(usage); - else name = "Unknown"; - } - else { - name = "Padding"; - } + case MAIN_OUTPUT: + case MAIN_FEATURE: // Outputs and Features are not supported + usage_list.clear(); + state.usage_min = 0; + state.usage_max = 0; + break; - int start_bit = bit_offset_; - int end_bit = bit_offset_ + current_.report_size - 1; + case MAIN_COLLECTION: // not implemented + case MAIN_END_COLLECTION: // not implemented + default: // items not yet defined + break; + } +} - int tmp_phys_min = current_.physical_min; - int tmp_phys_max = current_.physical_max; - if (current_.physical_min == 0) { - tmp_phys_min = current_.logical_min; - } - if (current_.physical_max == 0) { - tmp_phys_max = current_.logical_max; - } +void HidDecoder::createInputs() +{ + std::vector expanded_usages; - inputs_.push_back({usage, - name, - start_bit, - end_bit, - current_.logical_min, - current_.logical_max, - tmp_phys_min, - tmp_phys_max, - current_.units, - current_.units_exp, - btn_num}); - - bit_offset_ += current_.report_size; + if (state.usage_min != 0 && state.usage_max != 0) + { + for (int j = state.usage_min; j <= state.usage_max; ++j) + { + expanded_usages.push_back(j); } - - usage_list_.clear(); - current_.usage_min = 0; - current_.usage_max = 0; } - else if (tag_code == 0x9 || tag_code == 0x0B) { // Output or Feature - usage_list_.clear(); - current_.usage_min = 0; - current_.usage_max = 0; + else if (!usage_list.empty()) { + expanded_usages = usage_list; } -} + for (uint j = 0; j < state.report_count; ++j) { + std::string name; + u_int8_t usage = 0; + int btn_num = -1; + if (state.usage_page == 0x01 && j < expanded_usages.size()) { + usage = expanded_usages[j]; + name = usageName(usage); + } + else if (state.usage_page == USAGE_BUTTON && j < expanded_usages.size()) { + usage = USAGE_BUTTON; + btn_num = next_button++; + name = "Button " + std::to_string(btn_num); + } + else if (state.usage_page == 0x01 && expanded_usages.size() == 1) { + usage = expanded_usages[0]; + name = usageName(usage); + } + else { + name = "Padding"; + } -bool HidDecoder::interpretSigned() const { - return current_.logical_max < 0 || current_.logical_min < 0; + int start_bit = bit_offset; + int end_bit = bit_offset + state.report_size - 1; + + int tmp_phys_min = state.physical_min; + int tmp_phys_max = state.physical_max; + + // when physical min/max are not set, default to logical + if (state.physical_min == 0) tmp_phys_min = state.logical_min; + if (state.physical_max == 0) tmp_phys_max = state.logical_max; + + inputs.push_back({ usage, + name, + start_bit, + end_bit, + state.logical_min, + state.logical_max, + tmp_phys_min, + tmp_phys_max, + state.units, + state.units_exp, + btn_num}); + + bit_offset += state.report_size; + } + + usage_list.clear(); + state.usage_min = 0; + state.usage_max = 0; } +bool HidDecoder::interpretSigned() const +{ + return state.logical_max < 0 || state.logical_min < 0; +} -int HidDecoder::convertDataToInt(const std::vector &data, const bool isSigned) const { + +int HidDecoder::convertDataToInt(const std::vector &data, const bool isSigned) const +{ u_int32_t value = 0; for (uint i = 0; i < data.size(); ++i) { @@ -301,7 +344,8 @@ int HidDecoder::convertDataToInt(const std::vector &data, const b } -void HidDecoder::printDecodedInfo(const HidDecoded decoded) { +void HidDecoder::printDecodedInfo(const HidDecoded decoded) +{ std::ostringstream ss; ss << "Device Type: " << decoded.type << "\n"; for (HidReport report : decoded.reports) { @@ -332,8 +376,9 @@ void HidDecoder::printDecodedInfo(const HidDecoded decoded) { } -u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector& data, const bool print) const { - if (!usage_names_.count(input.usage)) return 0; +u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector& data, const bool print) const +{ + if (!isUsageKnown(input.usage)) return 0; int startByte = input.start_bit / 8; int startBit = input.start_bit % 8; int endByte = input.end_bit / 8; @@ -366,4 +411,37 @@ u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector Date: Wed, 29 Oct 2025 13:16:22 -0500 Subject: [PATCH 15/16] tidy up some more --- apps/decoder/decoder.cpp | 18 ++++++++++++++++-- source/idf/HidDecoder.cpp | 4 ++-- source/idf/HidDevice.cpp | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/decoder/decoder.cpp b/apps/decoder/decoder.cpp index a178f4e..d9829bf 100644 --- a/apps/decoder/decoder.cpp +++ b/apps/decoder/decoder.cpp @@ -16,6 +16,18 @@ void interruptHandler(int unused){ keepReading = false; } + +void printHidDescriptor(const std::vector report) +{ + printf("\nHID Report Descriptor (%lu bytes):\n ", report.size()); + for(size_t i=0; i < report.size(); ++i) { + printf("%02X ", report[i]); + if (i % 16 == 15) printf("\n "); + else if (i % 8 == 7) printf(" "); + } + printf("\n\n"); +} + int main(int argc, char **args) { signal(SIGINT, interruptHandler); @@ -88,6 +100,8 @@ int main(int argc, char **args) { std::vector descriptor(report, report + descriptor_size); std::vector dataVect; + printHidDescriptor(descriptor); + idf::HidDecoder decoder; idf::HidDecoded devDecoded = *decoder.parseDescriptor(descriptor); @@ -129,7 +143,7 @@ int main(int argc, char **args) { for (idf::HidReport r : devDecoded.reports) { if (!r.has_report_byte || (r.has_report_byte && static_cast(data[0]) == r.id)) { - printf("\x1b[39;49mDecode report (%d) values:\n", r.id); + printf("Decode report (%d) values:\n", r.id); ++rows; for( idf::HidInput input : r.inputs) { if (input.name != "Unknown") { @@ -173,7 +187,7 @@ int main(int argc, char **args) { // a short sleep prevents the cursor from visually jumping all over the place usleep(100000); if (!keepReading) break; - if (rows > 0) { + if (rows > 0) { // move the cursor up appropriate number of lines to visually update the read out for (int i = 0; i < rows; ++i) { std::cout << "\x1b[A"; } diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index fb832a2..31e952a 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -351,8 +351,8 @@ void HidDecoder::printDecodedInfo(const HidDecoded decoded) for (HidReport report : decoded.reports) { ss << "Report: " << report.id << " (" << report.bytes_count << " bytes)\n"; if (report.id != 0) { - ss << " Report ID bits 0:7 value: " << report.id << "\n"; - } + ss << " Report ID bits 0:7 value: " << report.id << "\n"; + } for(HidInput input : report.inputs) { ss << " " << std::setfill(' ') << std::setw(15) << std::left << input.name; if (input.start_bit == input.end_bit) { diff --git a/source/idf/HidDevice.cpp b/source/idf/HidDevice.cpp index 1b42e4d..5e95a60 100644 --- a/source/idf/HidDevice.cpp +++ b/source/idf/HidDevice.cpp @@ -67,7 +67,7 @@ std::vector HidDevice::getHidReportDescriptor() { void HidDevice::printHidDescriptor() { std::vector report = getHidReportDescriptor(); - printf("\x1b[39;49m\nHID Report Descriptor (%lu bytes):\n ", report.size()); + printf("\nHID Report Descriptor (%lu bytes):\n ", report.size()); for(size_t i=0; i < report.size(); ++i) { printf("%02X ", report[i]); if (i % 16 == 15) printf("\n "); From 672cbfda72216f5b7444a371061b67c03c1b607c Mon Sep 17 00:00:00 2001 From: Philip Kunz Date: Fri, 31 Oct 2025 11:16:12 -0500 Subject: [PATCH 16/16] indentation and readble whitespace --- source/idf/HidDecoder.cpp | 6 ++-- source/idf/HidGenericJoystick.cpp | 57 ++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/source/idf/HidDecoder.cpp b/source/idf/HidDecoder.cpp index 31e952a..7d41748 100644 --- a/source/idf/HidDecoder.cpp +++ b/source/idf/HidDecoder.cpp @@ -210,7 +210,7 @@ void HidDecoder::decodeLocalItem(const int tag_code, const int data) state.usage_min = data; break; - case LOCAL_MAXIMUM: + case LOCAL_MAXIMUM: state.usage_max = data; break; @@ -240,7 +240,7 @@ void HidDecoder::decodeMainItem(const int tag_code) state.usage_max = 0; break; - case MAIN_COLLECTION: // not implemented + case MAIN_COLLECTION: // not implemented case MAIN_END_COLLECTION: // not implemented default: // items not yet defined break; @@ -383,7 +383,7 @@ u_int64_t HidDecoder::extractValue(const HidInput& input, const std::vector16 bits mask = (mask << (input.end_bit - input.start_bit +1)) - 1; diff --git a/source/idf/HidGenericJoystick.cpp b/source/idf/HidGenericJoystick.cpp index 829c64f..35474d5 100644 --- a/source/idf/HidGenericJoystick.cpp +++ b/source/idf/HidGenericJoystick.cpp @@ -9,22 +9,29 @@ HidGenericJoystick::HidGenericJoystick(const int vendor, const int product, cons for( HidReport report : decoded.reports) { for (HidInput input : report.inputs) { switch(input.usage) { - case USAGE_BUTTON: - if (input.button_num == 1) - buttons.push_back(&trigger); - else - buttons.push_back(new SingleInput(0,1)); - break; - case USAGE_X: - leftRightPivot.configure(input.logical_min, input.logical_max); - break; - case USAGE_Y: - forwardBackwardPivot.configure(input.logical_min, input.logical_max); - break; - case USAGE_Z: - useZForTwist = true; - twist.configure(input.logical_min, input.logical_max); - break; + case USAGE_BUTTON: + if (input.button_num == 1) + buttons.push_back(&trigger); + else + buttons.push_back(new SingleInput(0,1)); + break; + + case USAGE_X: + // legacy dictates that X is a pivot even though it is a liner axis + leftRightPivot.configure(input.logical_min, input.logical_max); + break; + + case USAGE_Y: + // legacy dictates that Y is a pivot even though it is a liner axis + forwardBackwardPivot.configure(input.logical_min, input.logical_max); + break; + + case USAGE_Z: + // Up to the manufacturer whether twist is Z or RZ, but the + // norm seems to be that if Z exists it is the twist axis + useZForTwist = true; + twist.configure(input.logical_min, input.logical_max); + break; } } } @@ -43,17 +50,27 @@ void HidGenericJoystick::decode(const std::vector& data) { } catch (std::out_of_range & e) {} break; + case USAGE_X: - leftRightPivot.setValue(value); break; + leftRightPivot.setValue(value); + break; + case USAGE_Y: - forwardBackwardPivot.setValue(value); break; + forwardBackwardPivot.setValue(value); + break; + case USAGE_Z: - twist.setValue(value); break; + twist.setValue(value); + break; + case USAGE_RZ: if (!useZForTwist) twist.setValue(value); break; + case USAGE_SLIDER: - slider.setValue(value); break; + slider.setValue(value); + break; + case USAGE_HAT: int hat = value - input.logical_min; HatNorth.setValue(hat == 0);