simple_hid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 65df517271a4f786b5f536c3c25c423d50cc3b99f328385964c5b4457ca027b2
4
+ data.tar.gz: 61550584ab55cd3017dc45f095a571701eec7d920d2ac0f5d7f4c1df06c17c7e
5
+ SHA512:
6
+ metadata.gz: 26ab7a91eed2083794eb66537c8106fbe02fbe5fc98f049d07ea912fff88d579ef654d47834624aeb088b394097b83cf7d653eb1bd23fb749bb056deeecfc3b1
7
+ data.tar.gz: cc06048dd9ab119217168d8109d18aa0ecb86f24b404fc9bb7c4a6c10df8593cbceaab14fde9c92e0f6ce6e8f54d0ff257b6e105fbbd09f7a3a755bac8358dba
data/LICENCE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License (MIT)
2
+
3
+ Copyright © 2025 Jesse Fullam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # SimpleHID: A Python-Inspired HIDAPI Wrapper for Ruby
2
+
3
+ This project is a minimal FFI-based Ruby wrapper for the `hidapi` library. It was created to provide a simple interface HID communication, closely mirroring the design and ease of use of the Python `pyhidapi` library.
4
+
5
+ ## Motivation
6
+
7
+ While other HID libraries for Ruby exist, I found they lacked certain features for advanced device discovery. In particular, the ability to filter devices by `usage_page` and `usage` was not readily available. This functionality is critical for precisely identifying a specific device interface when a single piece of hardware exposes multiple HID interfaces.
8
+
9
+ ## Features & Current Status
10
+
11
+ The implementation in its current form, is intentionally lightweight and purpose-built. It provides functionality for:
12
+
13
+ * **Advanced Device Discovery:** Find devices by VID, PID, `usage_page`, and `usage`.
14
+ * **Write-Only Operations:** Send command packets to any connected HID device.
15
+
16
+ It is perfectly suited for projects involving custom peripheral control, especially those that require reverse-engineering a device's USB protocol. The library handles the low-level device connection, allowing you to focus on crafting and sending the correct byte commands.
17
+
18
+ ## Installation
19
+
20
+ - Ensure the system `hidapi` library is installed:
21
+ - Arch: `sudo pacman -S hidapi`
22
+ - Debian/Ubuntu: `sudo apt install libhidapi-hidraw0`
23
+ - macOS (Homebrew): `brew install hidapi`
24
+ - Install the Ruby gem: `gem install simple_hid`
25
+
26
+ ## Usage
27
+
28
+ First, require the library in your script:
29
+
30
+ ```ruby
31
+ require 'simple_hid'
32
+ ```
33
+
34
+ ### 1. Finding a Specific Device
35
+
36
+ The primary feature of the library is finding a device by its specific attributes. The `find_path` method will return the system path as a string if a device is found, or `nil` if not.
37
+
38
+ ```ruby
39
+ # Define the attributes of the device you're looking for
40
+ VENDOR_ID = 0x0CF2
41
+ PRODUCT_ID = 0xA102
42
+ USAGE_PAGE = 0xFF72
43
+ USAGE = 0x00A1
44
+
45
+ # Search for the device path
46
+ path = SimpleHID.find_path(
47
+ vid: VENDOR_ID,
48
+ pid: PRODUCT_ID,
49
+ usage_page: USAGE_PAGE,
50
+ usage: USAGE
51
+ )
52
+
53
+ unless path
54
+ puts "Error: Device not found."
55
+ exit
56
+ end
57
+
58
+ puts "Found device at: #{path}"
59
+ ```
60
+
61
+ ### 2. Listing All Connected HID Devices
62
+
63
+ You can also enumerate all HID devices connected to the system, which is useful for discovery. The `enumerate` method returns an array of hashes, with each hash representing one device.
64
+
65
+ ```ruby
66
+ # Get a list of all HID devices
67
+ all_devices = SimpleHID.enumerate
68
+
69
+ # Print the details for each device found
70
+ all_devices.each do |device_info|
71
+ puts "Path: #{device_info[:path]}"
72
+ puts " Vendor ID: 0x#{device_info[:vendor_id].to_s(16)}"
73
+ puts " Product ID: 0x#{device_info[:product_id].to_s(16)}"
74
+ puts " Usage Page: 0x#{device_info[:usage_page].to_s(16)}"
75
+ puts "-" * 20
76
+ end
77
+ ```
78
+
79
+ ### 3. Opening a Device and Writing Data
80
+
81
+ Once you have the device path, you can open it, send commands, and ensure it's closed safely using a `begin...ensure` block.
82
+
83
+ ```ruby
84
+ # (Continuing from the 'find_path' example above...)
85
+
86
+ begin
87
+ # 1. Open the device using its path
88
+ device = SimpleHID::Device.new(path)
89
+ puts "Device opened successfully."
90
+
91
+ # 2. Prepare your command as an array of bytes
92
+ # This command must match the protocol of your specific device.
93
+ # Note: The underlying HID report may require padding to a specific length.
94
+ # This library sends exactly what you provide.
95
+ command = [0xE0, 0x10, 0x60, 0x01, 0x03, 0x00, 0x00, 0x00]
96
+
97
+ # 3. Write the command to the device
98
+ device.write(command)
99
+ puts "Command sent."
100
+
101
+ ensure
102
+ # 4. The 'ensure' block guarantees the device is closed,
103
+ # even if an error occurs during the write operation.
104
+ if device
105
+ device.close
106
+ puts "Device closed cleanly."
107
+ end
108
+ end
109
+ ```
@@ -0,0 +1,3 @@
1
+ module SimpleHID
2
+ VERSION = "0.1.0"
3
+ end
data/lib/simple_hid.rb ADDED
@@ -0,0 +1,151 @@
1
+ require "ffi"
2
+ require "simple_hid/version"
3
+
4
+ module SimpleHID
5
+ extend FFI::Library
6
+
7
+ begin
8
+ # Try for common library names across platforms -> the first found will be used
9
+ ffi_lib [
10
+ "hidapi-hidraw",
11
+ "hidapi-libusb",
12
+ "hidapi",
13
+ "libhidapi-hidraw.so.0",
14
+ "libhidapi-hidraw.so"
15
+ ]
16
+ rescue LoadError => e
17
+ raise LoadError, (
18
+ "Could not load the hidapi shared library. " \
19
+ "Original error: #{e.message}"
20
+ )
21
+ end
22
+
23
+ enum :hid_bus_type, [
24
+ :unknown, 0x00,
25
+ :usb, 0x01,
26
+ :bluetooth, 0x02,
27
+ :i2c, 0x03,
28
+ :spi, 0x04,
29
+ :virtual, 0x05
30
+ ]
31
+ # Define the C structure for DeviceInfo
32
+ class DeviceInfo < FFI::Struct
33
+ layout :path, :string,
34
+ :vendor_id, :ushort,
35
+ :product_id, :ushort,
36
+ :serial_number, :pointer,
37
+ :release_number, :ushort,
38
+ :manufacturer_string, :pointer,
39
+ :product_string, :pointer,
40
+ :usage_page, :ushort,
41
+ :usage, :ushort,
42
+ :interface_number, :int,
43
+ :next, :pointer,
44
+ :bus_type, :hid_bus_type
45
+ end
46
+
47
+ # Attach the raw C functions from the loaded library
48
+ attach_function :hid_init, [], :int
49
+ attach_function :hid_exit, [], :void
50
+ attach_function :hid_enumerate, [:ushort, :ushort], :pointer
51
+ attach_function :hid_free_enumeration, [:pointer], :void
52
+ attach_function :hid_open_path, [:string], :pointer
53
+ attach_function :hid_write, [:pointer, :pointer, :size_t], :int
54
+ attach_function :hid_close, [:pointer], :void
55
+ # wchar_t handling
56
+ attach_function :hid_get_manufacturer_string, [:pointer, :pointer, :size_t], :int
57
+ attach_function :hid_get_product_string, [:pointer, :pointer, :size_t], :int
58
+ attach_function :hid_get_serial_number_string,[:pointer, :pointer, :size_t], :int
59
+
60
+ # Enumeration method
61
+ def self.enumerate(vid: 0, pid: 0)
62
+ devices = []
63
+ head = hid_enumerate(vid, pid)
64
+
65
+ begin
66
+ return [] if head.null?
67
+ current_ptr = head
68
+ until current_ptr.null?
69
+ info_struct = DeviceInfo.new(current_ptr)
70
+
71
+ # Convert the FFI::Struct to a plain Ruby Hash
72
+ device_info = {
73
+ path: info_struct[:path].dup,
74
+ vendor_id: info_struct[:vendor_id],
75
+ product_id: info_struct[:product_id],
76
+ release_number: info_struct[:release_number],
77
+ usage_page: info_struct[:usage_page],
78
+ usage: info_struct[:usage],
79
+ interface_number: info_struct[:interface_number],
80
+ bus_type: info_struct[:bus_type]
81
+ }
82
+ devices << device_info
83
+ current_ptr = info_struct[:next]
84
+ end
85
+ ensure
86
+ hid_free_enumeration(head) unless head.null?
87
+ end
88
+
89
+ devices
90
+ end
91
+
92
+ # Helper method
93
+ def self.find_path(vid: 0, pid: 0, usage_page: nil, usage: nil)
94
+ all_devices = self.enumerate(vid: vid, pid: pid)
95
+ found_device = all_devices.find do |dev|
96
+ match_usage_page = usage_page.nil? || dev[:usage_page] == usage_page
97
+ match_usage = usage.nil? || dev[:usage] == usage
98
+ match_usage_page && match_usage
99
+ end
100
+ found_device ? found_device[:path] : nil
101
+ end
102
+
103
+ class Device
104
+
105
+ MAX_STR_LEN = 255
106
+
107
+ def initialize(path)
108
+ @handle = SimpleHID.hid_open_path(path)
109
+ raise "Unable to open HID device at path: #{path}" if @handle.null?
110
+ end
111
+
112
+ def write(byte_array)
113
+ raise "Device is closed." if @handle.nil?
114
+ data_string = byte_array.pack('C*')
115
+ data_ptr = FFI::MemoryPointer.from_string(data_string)
116
+ SimpleHID.hid_write(@handle, data_ptr, data_string.bytesize)
117
+ end
118
+
119
+ def close
120
+ return if @handle.nil?
121
+ SimpleHID.hid_close(@handle)
122
+ @handle = nil
123
+ end
124
+
125
+ def manufacturer_string
126
+ read_wide_string_with(:hid_get_manufacturer_string)
127
+ end
128
+
129
+ def product_string
130
+ read_wide_string_with(:hid_get_product_string)
131
+ end
132
+
133
+ def serial_number_string
134
+ read_wide_string_with(:hid_get_serial_number_string)
135
+ end
136
+
137
+ private
138
+
139
+ def read_wide_string_with(c_function_name)
140
+ raise "Device is closed." if @handle.nil?
141
+ buffer = FFI::MemoryPointer.new(:wchar_t, MAX_STR_LEN)
142
+ result = SimpleHID.send(c_function_name, @handle, buffer, MAX_STR_LEN)
143
+ raise "HIDAPI error while reading string." if result == -1
144
+
145
+ return buffer.read_wstring
146
+ end
147
+ end
148
+
149
+ self.hid_init
150
+ at_exit { self.hid_exit }
151
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_hid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jesse Fullam
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.15'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rake
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '13'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '14'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '13'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '14'
52
+ - !ruby/object:Gem::Dependency
53
+ name: bundler
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '2.3'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '2.3'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '3.0'
72
+ description: Provides simple device enumeration by VID/PID/usage and write-only communication
73
+ for HID devices.
74
+ email: []
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - LICENCE
80
+ - README.md
81
+ - lib/simple_hid.rb
82
+ - lib/simple_hid/version.rb
83
+ homepage: https://github.com/bell-arch/Simple_HID
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ source_code_uri: https://github.com/bell-arch/Simple_HID
88
+ changelog_uri: https://github.com/bell-arch/Simple_HID/releases
89
+ rubygems_mfa_required: 'true'
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '2.6'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 3.7.2
105
+ specification_version: 4
106
+ summary: Minimal FFI-based HIDAPI wrapper for Ruby, inspired by Python's hid library.
107
+ test_files: []