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 +7 -0
- data/LICENCE +9 -0
- data/README.md +109 -0
- data/lib/simple_hid/version.rb +3 -0
- data/lib/simple_hid.rb +151 -0
- metadata +107 -0
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
|
+
```
|
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: []
|