hidaping 1.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/.clang-format +5 -0
- data/.github/workflows/gem-push.yml +50 -0
- data/.github/workflows/test.yml +53 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +18 -0
- data/ext/hidaping/common.h +17 -0
- data/ext/hidaping/extconf.rb +27 -0
- data/ext/hidaping/hidaping.c +57 -0
- data/ext/hidaping/hidaping.h +6 -0
- data/ext/hidaping/hidaping_device.c +159 -0
- data/ext/hidaping/hidaping_device.h +9 -0
- data/ext/hidaping/hidaping_util.h +19 -0
- data/lib/hidaping/version.rb +5 -0
- data/lib/hidaping.rb +26 -0
- data/sample/kbprog.rb +190 -0
- data/sample/set_hsv.rb +21 -0
- data/sig/myhidapi.rbs +4 -0
- data/test/joycon.rb +16 -0
- data/test/test.rb +14 -0
- data/test/test_myhidapi.rb +33 -0
- data/test/xbox360.rb +15 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c38b9b9d15828de26fea00975aa9744b1ab4351cc52bba779c3daf257b987efc
|
|
4
|
+
data.tar.gz: d65edf49a8fe4b072f3e68ad038d31d4a349c4a4331f2f38d24cfc20157780c4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7212e791d8fcc018e825b6ed54a2add5c8c2096e7aea2b8ce9375379a3710f25d8268d973e67b1fa691d928c5cd42e878580f40745fbeeac39890b07ddc8f5ea
|
|
7
|
+
data.tar.gz: c8a7bf89640c6962fed10d48747cc3d024bc285889afe83fdfe2ea1221dd053265a4fdc70ba9b863eed961172cf557f2cb4e84e4ee7c900cb4b73e56b3076d90
|
data/.clang-format
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Ruby Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "master" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "master" ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: Build + Publish
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
pages: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Clone
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
- name: Set up Ruby 3.4.5
|
|
22
|
+
uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: 3.4.5
|
|
25
|
+
- name: Build
|
|
26
|
+
run: |
|
|
27
|
+
bundle install
|
|
28
|
+
bundle exec rake gem
|
|
29
|
+
|
|
30
|
+
mkdir -p ~/.gem
|
|
31
|
+
cat <<AAA > ~/.gem/.mirrorrc
|
|
32
|
+
---
|
|
33
|
+
- from: https://repy.github.io/hidaping
|
|
34
|
+
to: /home/runner/gemrepos
|
|
35
|
+
AAA
|
|
36
|
+
mkdir -p /home/runner/gemrepos/gems
|
|
37
|
+
gem install rubygems-mirror
|
|
38
|
+
gem mirror
|
|
39
|
+
|
|
40
|
+
cp ./pkg/hidaping-*.gem /home/runner/gemrepos/gems/
|
|
41
|
+
cd /home/runner/gemrepos
|
|
42
|
+
gem generate_index
|
|
43
|
+
- name: Setup Pages
|
|
44
|
+
uses: actions/configure-pages@v5
|
|
45
|
+
- name: Upload artifact
|
|
46
|
+
uses: actions/upload-pages-artifact@v3
|
|
47
|
+
with:
|
|
48
|
+
path: /home/runner/gemrepos
|
|
49
|
+
- name: Deploy to GitHub Pages
|
|
50
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Test on All OS
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["master"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["master"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
os:
|
|
15
|
+
- ubuntu-latest
|
|
16
|
+
- windows-latest
|
|
17
|
+
- macos-latest
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Clone
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
- name: Set up Ruby 3.4.5
|
|
23
|
+
uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: 3.4.5
|
|
26
|
+
|
|
27
|
+
- name: Run tests (Ubuntu)
|
|
28
|
+
if: runner.os == 'Linux'
|
|
29
|
+
run: |
|
|
30
|
+
sudo apt-get update
|
|
31
|
+
sudo apt install -y libhidapi-dev
|
|
32
|
+
gem install bundler
|
|
33
|
+
bundle install
|
|
34
|
+
bundle exec rake compile
|
|
35
|
+
bundle exec ruby test/test.rb
|
|
36
|
+
|
|
37
|
+
- name: Run tests (Windows)
|
|
38
|
+
if: runner.os == 'Windows'
|
|
39
|
+
run: |
|
|
40
|
+
pacman --noconfirm -S mingw-w64-ucrt-x86_64-hidapi
|
|
41
|
+
gem install bundler
|
|
42
|
+
bundle install
|
|
43
|
+
bundle exec rake compile
|
|
44
|
+
bundle exec ruby test/test.rb
|
|
45
|
+
|
|
46
|
+
- name: Run tests (macOS)
|
|
47
|
+
if: runner.os == 'macOS'
|
|
48
|
+
run: |
|
|
49
|
+
brew install hidapi
|
|
50
|
+
gem install bundler
|
|
51
|
+
bundle install
|
|
52
|
+
bundle exec rake compile
|
|
53
|
+
sudo bundle exec ruby test/test.rb
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
=== 1.1.0 / 2024-05-24
|
|
2
|
+
|
|
3
|
+
* Forked from https://github.com/tenderlove/myhidapi and updated project metadata.
|
|
4
|
+
* Improved the build process to use `pkg-config` for locating the `hidapi` library. This simplifies installation on systems where `hidapi` is installed via a package manager (e.g., Homebrew, apt).
|
|
5
|
+
|
|
6
|
+
=== 1.0.0 / 2019-02-15
|
|
7
|
+
|
|
8
|
+
* 1 major enhancement
|
|
9
|
+
* Birthday!
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Repy
|
|
4
|
+
Copyright (c) 2019 Aaron Patterson
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# HIDAPING
|
|
2
|
+
|
|
3
|
+
Next generation HIDAPI wrapper for Ruby
|
|
4
|
+
|
|
5
|
+
## DESCRIPTION
|
|
6
|
+
|
|
7
|
+
This is a small wrapper around HIDAPI.
|
|
8
|
+
I couldn't get other HIDAPI wrappers to work, so I wrote this one.
|
|
9
|
+
I'm using it to communicate with my keyboard, so it really only supports enough of the HIDAPI to do that.
|
|
10
|
+
|
|
11
|
+
## DESCRIPTION
|
|
12
|
+
|
|
13
|
+
* https://github.com/Repy/hidaping
|
|
14
|
+
|
|
15
|
+
This gem is a fork from the original work by Aaron Patterson ([@tenderlove](https://github.com/tenderlove)).
|
|
16
|
+
|
|
17
|
+
* Original Repository: https://github.com/tenderlove/myhidapi
|
|
18
|
+
|
|
19
|
+
## SUPPORTS
|
|
20
|
+
|
|
21
|
+
This gem works on the following platforms, leveraging the capabilities of the underlying HIDAPI library.
|
|
22
|
+
|
|
23
|
+
* **Platforms**:
|
|
24
|
+
* Windows
|
|
25
|
+
* macOS
|
|
26
|
+
* Linux
|
|
27
|
+
|
|
28
|
+
* **Device Types**:
|
|
29
|
+
* USB
|
|
30
|
+
* Bluetooth
|
|
31
|
+
|
|
32
|
+
## FEATURES/PROBLEMS
|
|
33
|
+
|
|
34
|
+
* Incomplete
|
|
35
|
+
* No tests
|
|
36
|
+
* Seems to work (for me)
|
|
37
|
+
|
|
38
|
+
## SYNOPSIS
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require 'hidaping'
|
|
42
|
+
|
|
43
|
+
devices = HIDAPING.enumerate(0x0, 0x0)
|
|
44
|
+
|
|
45
|
+
for d in devices
|
|
46
|
+
printf("0x%04x, 0x%04x, %s\n", d.vendor_id, d.product_id, d.product_string)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
handle = HIDAPING.open(0x045e, 0x028e)
|
|
50
|
+
|
|
51
|
+
while true
|
|
52
|
+
report_raw = handle.read_timeout(64, 500)
|
|
53
|
+
data = report_raw.unpack("vvvvvb*")
|
|
54
|
+
p data
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## REQUIREMENTS
|
|
59
|
+
|
|
60
|
+
This depends on **hidapi**.
|
|
61
|
+
|
|
62
|
+
## INSTALL
|
|
63
|
+
|
|
64
|
+
First, install the **hidapi** library. Here are instructions for common platforms:
|
|
65
|
+
|
|
66
|
+
* **Windows (using RubyInstaller with Devkit)**
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
ridk exec pacman -S mingw-w64-ucrt-x86_64-hidapi
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
* **macOS (using Homebrew)**
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
brew install hidapi
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
* **Ubuntu / Debian**
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
sudo apt update
|
|
82
|
+
sudo apt install libhidapi-dev
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Once `hidapi` is installed, you can install this gem:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
gem install hidaping
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## LICENSE
|
|
92
|
+
|
|
93
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/extensiontask"
|
|
5
|
+
require 'rake/packagetask'
|
|
6
|
+
|
|
7
|
+
task build: :compile
|
|
8
|
+
|
|
9
|
+
GEMSPEC = Gem::Specification.load("hidaping.gemspec")
|
|
10
|
+
|
|
11
|
+
Rake::ExtensionTask.new("hidaping", GEMSPEC) do |ext|
|
|
12
|
+
ext.lib_dir = "lib/hidaping"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Gem::PackageTask.new(GEMSPEC) do |pkg|
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task default: %i[clobber compile]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#ifndef MY_HIDAPI_COMMON_H
|
|
2
|
+
#define MY_HIDAPI_COMMON_H
|
|
3
|
+
|
|
4
|
+
#include "extconf.h"
|
|
5
|
+
|
|
6
|
+
#if defined(HAVE_HIDAPI_H)
|
|
7
|
+
#include <hidapi.h>
|
|
8
|
+
#elif defined(HAVE_HIDAPI_HIDAPI_H)
|
|
9
|
+
#include <hidapi/hidapi.h>
|
|
10
|
+
#else
|
|
11
|
+
#error "No suitable header found"
|
|
12
|
+
#endif
|
|
13
|
+
|
|
14
|
+
#include <ruby.h>
|
|
15
|
+
#include <stdlib.h>
|
|
16
|
+
|
|
17
|
+
#endif // MY_HIDAPI_COMMON_H
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
|
|
5
|
+
dir_config("hidapi")
|
|
6
|
+
pkg_config('hidapi')
|
|
7
|
+
|
|
8
|
+
if have_library('hidapi')
|
|
9
|
+
libname = 'hidapi'
|
|
10
|
+
elsif have_library('hidapi-hidraw')
|
|
11
|
+
libname = 'hidapi-hidraw'
|
|
12
|
+
elsif have_library('hidapi-libusb')
|
|
13
|
+
libname = 'hidapi-libusb'
|
|
14
|
+
else
|
|
15
|
+
abort 'you must have "libhidapi".'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if have_header('hidapi/hidapi.h')
|
|
19
|
+
header = 'hidapi/hidapi.h'
|
|
20
|
+
elsif have_header('hidapi.h')
|
|
21
|
+
header = 'hidapi.h'
|
|
22
|
+
else
|
|
23
|
+
abort 'you must have "hidapi.h".'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
create_header
|
|
27
|
+
create_makefile("hidaping/hidaping")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#include "hidaping.h"
|
|
2
|
+
#include "hidaping_device.h"
|
|
3
|
+
#include "hidaping_util.h"
|
|
4
|
+
|
|
5
|
+
VALUE mHIDAPING;
|
|
6
|
+
|
|
7
|
+
static VALUE enumerate(VALUE mod, VALUE vendor_id, VALUE product_id) {
|
|
8
|
+
VALUE devices;
|
|
9
|
+
struct hid_device_info *devs, *cur_dev;
|
|
10
|
+
|
|
11
|
+
VALUE device_info_class = rb_path2class("HIDAPING::DeviceInfo");
|
|
12
|
+
|
|
13
|
+
devices = rb_ary_new();
|
|
14
|
+
devs = hid_enumerate(NUM2USHORT(vendor_id), NUM2USHORT(product_id));
|
|
15
|
+
cur_dev = devs;
|
|
16
|
+
while (cur_dev) {
|
|
17
|
+
VALUE args[] = {INT2NUM(cur_dev->vendor_id),
|
|
18
|
+
INT2NUM(cur_dev->product_id),
|
|
19
|
+
rb_str_new2(cur_dev->path),
|
|
20
|
+
hidaping_wcstombs(cur_dev->serial_number),
|
|
21
|
+
hidaping_wcstombs(cur_dev->manufacturer_string),
|
|
22
|
+
hidaping_wcstombs(cur_dev->product_string),
|
|
23
|
+
INT2NUM(cur_dev->usage),
|
|
24
|
+
INT2NUM(cur_dev->interface_number)};
|
|
25
|
+
rb_ary_push(devices, rb_class_new_instance(8, args, device_info_class));
|
|
26
|
+
cur_dev = cur_dev->next;
|
|
27
|
+
}
|
|
28
|
+
hid_free_enumeration(devs);
|
|
29
|
+
|
|
30
|
+
return devices;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static VALUE rb_hid_open(VALUE mod, VALUE vid, VALUE pid) {
|
|
34
|
+
hid_device *handle = hid_open(NUM2USHORT(vid), NUM2USHORT(pid), NULL);
|
|
35
|
+
if (handle) {
|
|
36
|
+
return create_hidaping_handle(handle);
|
|
37
|
+
} else {
|
|
38
|
+
return Qfalse;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static VALUE rb_hid_open_path(VALUE mod, VALUE path) {
|
|
43
|
+
hid_device *handle = hid_open_path(StringValueCStr(path));
|
|
44
|
+
if (handle) {
|
|
45
|
+
return create_hidaping_handle(handle);
|
|
46
|
+
} else {
|
|
47
|
+
return Qfalse;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
extern void Init_hidaping(void) {
|
|
52
|
+
mHIDAPING = rb_define_module("HIDAPING");
|
|
53
|
+
rb_define_singleton_method(mHIDAPING, "enumerate", enumerate, 2);
|
|
54
|
+
rb_define_singleton_method(mHIDAPING, "open", rb_hid_open, 2);
|
|
55
|
+
rb_define_singleton_method(mHIDAPING, "open_path", rb_hid_open_path, 1);
|
|
56
|
+
Init_hidaping_device(mHIDAPING);
|
|
57
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#include "hidaping_device.h"
|
|
2
|
+
#include "hidaping.h"
|
|
3
|
+
#include "hidaping_util.h"
|
|
4
|
+
|
|
5
|
+
#define BUF_SIZE 4096
|
|
6
|
+
|
|
7
|
+
VALUE cHIDAPINGHandle;
|
|
8
|
+
|
|
9
|
+
static void dealloc(void *ptr) {
|
|
10
|
+
hid_device *handle = (hid_device *)ptr;
|
|
11
|
+
hid_close(handle);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static const rb_data_type_t hidaping_handle_type = {
|
|
15
|
+
"HIDAPING/Handle",
|
|
16
|
+
{
|
|
17
|
+
0,
|
|
18
|
+
dealloc,
|
|
19
|
+
0,
|
|
20
|
+
},
|
|
21
|
+
0,
|
|
22
|
+
0,
|
|
23
|
+
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
|
|
24
|
+
RUBY_TYPED_FREE_IMMEDIATELY,
|
|
25
|
+
#endif
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
VALUE create_hidaping_handle(hid_device *handle) {
|
|
29
|
+
return TypedData_Wrap_Struct(cHIDAPINGHandle, &hidaping_handle_type, handle);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static VALUE rb_hid_write(VALUE self, VALUE str) {
|
|
33
|
+
hid_device *handle;
|
|
34
|
+
TypedData_Get_Struct(self, hid_device, &hidaping_handle_type, handle);
|
|
35
|
+
|
|
36
|
+
int written = hid_write(handle, (unsigned char *)StringValuePtr(str), RSTRING_LEN(str));
|
|
37
|
+
|
|
38
|
+
if (written >= 0) {
|
|
39
|
+
return INT2NUM(written);
|
|
40
|
+
} else {
|
|
41
|
+
return Qfalse;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static VALUE rb_hid_set_nonblocking(VALUE self, VALUE val) {
|
|
46
|
+
hid_device *handle;
|
|
47
|
+
TypedData_Get_Struct(self, hid_device, &hidaping_handle_type, handle);
|
|
48
|
+
|
|
49
|
+
if (!hid_set_nonblocking(handle, NUM2INT(val))) {
|
|
50
|
+
return Qtrue;
|
|
51
|
+
} else {
|
|
52
|
+
return Qnil;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static VALUE rb_hid_read(VALUE self, VALUE size) {
|
|
57
|
+
hid_device *handle;
|
|
58
|
+
TypedData_Get_Struct(self, hid_device, &hidaping_handle_type, handle);
|
|
59
|
+
|
|
60
|
+
unsigned char *buf = xcalloc(NUM2SIZET(size), sizeof(unsigned char));
|
|
61
|
+
int read = hid_read(handle, buf, NUM2SIZET(size));
|
|
62
|
+
|
|
63
|
+
VALUE ret;
|
|
64
|
+
if (read > 0) {
|
|
65
|
+
ret = rb_str_new((char *)buf, read);
|
|
66
|
+
} else {
|
|
67
|
+
ret = Qnil;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
xfree(buf);
|
|
71
|
+
return ret;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static VALUE rb_hid_read_timeout(VALUE self, VALUE size, VALUE timeout_ms) {
|
|
75
|
+
hid_device *handle;
|
|
76
|
+
unsigned char *buf;
|
|
77
|
+
int read;
|
|
78
|
+
VALUE ret;
|
|
79
|
+
|
|
80
|
+
TypedData_Get_Struct(self, hid_device, &hidaping_handle_type, handle);
|
|
81
|
+
|
|
82
|
+
buf = xcalloc(NUM2SIZET(size), sizeof(unsigned char));
|
|
83
|
+
|
|
84
|
+
read = hid_read_timeout(handle, buf, NUM2SIZET(size), NUM2INT(timeout_ms));
|
|
85
|
+
|
|
86
|
+
if (read > 0) {
|
|
87
|
+
ret = rb_str_new((char *)buf, read);
|
|
88
|
+
} else {
|
|
89
|
+
ret = Qnil;
|
|
90
|
+
}
|
|
91
|
+
xfree(buf);
|
|
92
|
+
return ret;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static VALUE rb_hid_send_feature_report(VALUE self, VALUE str) {
|
|
96
|
+
hid_device *handle;
|
|
97
|
+
TypedData_Get_Struct(self, hid_device, &hidaping_handle_type, handle);
|
|
98
|
+
|
|
99
|
+
int written =
|
|
100
|
+
hid_send_feature_report(handle, (unsigned char *)StringValuePtr(str), RSTRING_LEN(str));
|
|
101
|
+
|
|
102
|
+
if (written >= 0) {
|
|
103
|
+
return INT2NUM(written);
|
|
104
|
+
} else {
|
|
105
|
+
return Qfalse;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static VALUE rb_hid_get_feature_report(VALUE self, VALUE report_id, VALUE size) {
|
|
110
|
+
hid_device *handle;
|
|
111
|
+
TypedData_Get_Struct(self, hid_device, &hidaping_handle_type, handle);
|
|
112
|
+
|
|
113
|
+
int buf_size = NUM2SIZET(size);
|
|
114
|
+
unsigned char *buf = xcalloc(buf_size, sizeof(unsigned char));
|
|
115
|
+
int read = hid_get_feature_report(handle, buf, buf_size);
|
|
116
|
+
|
|
117
|
+
VALUE ret;
|
|
118
|
+
if (read > 0) {
|
|
119
|
+
ret = rb_str_new((char *)buf, read);
|
|
120
|
+
} else {
|
|
121
|
+
ret = Qnil;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
xfree(buf);
|
|
125
|
+
return ret;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static VALUE rb_manufacturer(VALUE self) {
|
|
129
|
+
hid_device *handle;
|
|
130
|
+
TypedData_Get_Struct(self, hid_device, &hidaping_handle_type, handle);
|
|
131
|
+
|
|
132
|
+
wchar_t buffer[BUF_SIZE];
|
|
133
|
+
hid_get_manufacturer_string(handle, buffer, BUF_SIZE);
|
|
134
|
+
|
|
135
|
+
return hidaping_wcstombs(buffer);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static VALUE rb_product(VALUE self) {
|
|
139
|
+
hid_device *handle;
|
|
140
|
+
TypedData_Get_Struct(self, hid_device, &hidaping_handle_type, handle);
|
|
141
|
+
|
|
142
|
+
wchar_t buffer[BUF_SIZE];
|
|
143
|
+
hid_get_product_string(handle, buffer, BUF_SIZE);
|
|
144
|
+
|
|
145
|
+
return hidaping_wcstombs(buffer);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
void Init_hidaping_device(VALUE mHIDAPING) {
|
|
149
|
+
cHIDAPINGHandle = rb_define_class_under(mHIDAPING, "Handle", rb_cObject);
|
|
150
|
+
rb_undef_alloc_func(cHIDAPINGHandle);
|
|
151
|
+
rb_define_method(cHIDAPINGHandle, "write", rb_hid_write, 1);
|
|
152
|
+
rb_define_method(cHIDAPINGHandle, "read", rb_hid_read, 1);
|
|
153
|
+
rb_define_method(cHIDAPINGHandle, "read_timeout", rb_hid_read_timeout, 2);
|
|
154
|
+
rb_define_method(cHIDAPINGHandle, "set_nonblocking", rb_hid_set_nonblocking, 1);
|
|
155
|
+
rb_define_method(cHIDAPINGHandle, "send_feature_report", rb_hid_send_feature_report, 1);
|
|
156
|
+
rb_define_method(cHIDAPINGHandle, "get_feature_report", rb_hid_get_feature_report, 2);
|
|
157
|
+
rb_define_method(cHIDAPINGHandle, "manufacturer", rb_manufacturer, 0);
|
|
158
|
+
rb_define_method(cHIDAPINGHandle, "product", rb_product, 0);
|
|
159
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#ifndef MY_HIDAPI_UTIL_H
|
|
2
|
+
#define MY_HIDAPI_UTIL_H
|
|
3
|
+
|
|
4
|
+
#include "common.h"
|
|
5
|
+
|
|
6
|
+
#define BUF_SIZE 4096
|
|
7
|
+
|
|
8
|
+
static VALUE hidaping_wcstombs(wchar_t *str) {
|
|
9
|
+
char buf[BUF_SIZE];
|
|
10
|
+
int len;
|
|
11
|
+
len = wcstombs(buf, str, BUF_SIZE);
|
|
12
|
+
if (len > 0) {
|
|
13
|
+
return rb_str_new(buf, len);
|
|
14
|
+
} else {
|
|
15
|
+
return Qnil;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#endif // MY_HIDAPI_UTIL_H
|
data/lib/hidaping.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hidaping/hidaping.so"
|
|
4
|
+
|
|
5
|
+
module HIDAPING
|
|
6
|
+
|
|
7
|
+
class DeviceInfo
|
|
8
|
+
attr_reader :vendor_id, :product_id, :path, :serial_number, :manufacturer_string, :product_string, :usage, :interface_number
|
|
9
|
+
|
|
10
|
+
def initialize vendor_id, product_id, path, serial_number, manufacturer_string, product_string, usage, interface_number
|
|
11
|
+
@vendor_id = vendor_id
|
|
12
|
+
@product_id = product_id
|
|
13
|
+
@path = path
|
|
14
|
+
@serial_number = serial_number
|
|
15
|
+
@manufacturer_string = manufacturer_string
|
|
16
|
+
@product_string = product_string
|
|
17
|
+
@usage = usage
|
|
18
|
+
@interface_number = interface_number
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def open
|
|
22
|
+
HIDAPING.open vendor_id, product_id
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
data/sample/kbprog.rb
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require 'hidaping'
|
|
2
|
+
require 'optparse'
|
|
3
|
+
|
|
4
|
+
class TeeloKB
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class CouldNotReadError < Error; end
|
|
7
|
+
class KeyboardNotFoundError < Error; end
|
|
8
|
+
|
|
9
|
+
COMMAND_VERSIONS = { 1 => {} }
|
|
10
|
+
|
|
11
|
+
%i{
|
|
12
|
+
version
|
|
13
|
+
rgblight_enable
|
|
14
|
+
rgblight_disable
|
|
15
|
+
rgblight_toggle
|
|
16
|
+
rgblight_mode
|
|
17
|
+
rgblight_sethsv
|
|
18
|
+
rgblight_get_mode
|
|
19
|
+
rgblight_get_hue
|
|
20
|
+
rgblight_get_sat
|
|
21
|
+
rgblight_get_val
|
|
22
|
+
}.each_with_index do |command, index|
|
|
23
|
+
COMMAND_VERSIONS[1][command] = index + 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.find
|
|
27
|
+
HIDAPING.enumerate(0x0, 0x0).find { |dev| dev.product_string == "ErgoDox EZ" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.open
|
|
31
|
+
dev = self.find
|
|
32
|
+
|
|
33
|
+
retries = 0
|
|
34
|
+
handle = dev.open
|
|
35
|
+
while !handle
|
|
36
|
+
retries += 1
|
|
37
|
+
raise KeyboardNotFoundError, "Couldn't find keyboard" if retries > 10
|
|
38
|
+
handle = dev.open
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
new handle
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize handle
|
|
45
|
+
@handle = handle
|
|
46
|
+
@version = protocol_version
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def protocol_version
|
|
50
|
+
write [0x0, 0x1]
|
|
51
|
+
read(2).inject(:+)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def solid
|
|
55
|
+
rgblight_mode 0x1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def enable
|
|
59
|
+
write [0x0, _(:rgblight_enable)]
|
|
60
|
+
read 10
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def disable
|
|
64
|
+
write [0x0, _(:rgblight_disable)]
|
|
65
|
+
read 10
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def toggle
|
|
69
|
+
write [0x0, _(:rgblight_toggle)]
|
|
70
|
+
read 10
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def mode
|
|
74
|
+
write [0x0, _(:rgblight_get_mode)]
|
|
75
|
+
read(2).last
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def rainbow style = 0x6
|
|
79
|
+
rgblight_mode style
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rgblight_mode value
|
|
83
|
+
write [0x0, _(:rgblight_mode), value & 0xFF]
|
|
84
|
+
read 10
|
|
85
|
+
end
|
|
86
|
+
alias :mode= :rgblight_mode
|
|
87
|
+
|
|
88
|
+
def rgblight_sethsv h, s, v
|
|
89
|
+
write([0x0, _(:rgblight_sethsv)] + [(h >> 8) & 0xFF, h & 0xFF, s & 0xFF, v & 0xFF])
|
|
90
|
+
read 10
|
|
91
|
+
end
|
|
92
|
+
alias :sethsv :rgblight_sethsv
|
|
93
|
+
|
|
94
|
+
def setrgb r, g, b
|
|
95
|
+
sethsv *rgb2hsv(r, g, b)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def hue
|
|
99
|
+
write [0x0, _(:rgblight_get_hue)]
|
|
100
|
+
_, upper, lower = read 3
|
|
101
|
+
(upper << 8) | lower
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def saturation
|
|
105
|
+
write [0x0, _(:rgblight_get_sat)]
|
|
106
|
+
read(2)[1]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def value
|
|
110
|
+
write [0x0, _(:rgblight_get_val)]
|
|
111
|
+
read(2)[1]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def hsv
|
|
115
|
+
[hue, saturation, value]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def rgb2hsv r, g, b
|
|
121
|
+
v = [r, g, b].max
|
|
122
|
+
delta = v - [r, g, b].min
|
|
123
|
+
|
|
124
|
+
s = v == 0 ? 0 : (delta / v.to_f)
|
|
125
|
+
h = delta == 0 ? 0 : (case v
|
|
126
|
+
when r then ((g - b) / delta) % 6
|
|
127
|
+
when g then ((b - r) / delta) + 2
|
|
128
|
+
when b then ((r - g) / delta) + 4
|
|
129
|
+
end) * 60
|
|
130
|
+
|
|
131
|
+
[h.round, (s * 255).round, v]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def write buf
|
|
135
|
+
loop do
|
|
136
|
+
break if @handle.write buf.pack('C*')
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def read size
|
|
141
|
+
buf = @handle.read_timeout size, 300 # 300 ms timeout
|
|
142
|
+
if buf
|
|
143
|
+
buf.unpack('C*')
|
|
144
|
+
else
|
|
145
|
+
# Unfortunately, this seems to happen frequently.
|
|
146
|
+
raise CouldNotReadError, "could not read from device"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def _ command
|
|
151
|
+
COMMAND_VERSIONS.fetch(@version).fetch(command)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
commands = []
|
|
156
|
+
responses = []
|
|
157
|
+
|
|
158
|
+
OptionParser.new do |opts|
|
|
159
|
+
opts.banner = "Usage: kbprog.rb [options]"
|
|
160
|
+
|
|
161
|
+
opts.on("--hsv=[HSV]", "Set or get HSV (comma separated)") do |v|
|
|
162
|
+
if v
|
|
163
|
+
commands << lambda { |kb| kb.sethsv(*v.split(",").map(&:to_i)) }
|
|
164
|
+
else
|
|
165
|
+
commands << lambda { |kb| responses << { hsv: kb.hsv } }
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
opts.on("--mode=[HSV]", "Set or get mode") do |v|
|
|
169
|
+
if v
|
|
170
|
+
commands << lambda { |kb| kb.mode = v.to_i }
|
|
171
|
+
else
|
|
172
|
+
commands << lambda { |kb| responses << { mode: kb.mode } }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end.parse!
|
|
176
|
+
|
|
177
|
+
kb = TeeloKB.open
|
|
178
|
+
commands.each { |cmd| cmd.call kb }
|
|
179
|
+
responses.each do |res|
|
|
180
|
+
p res
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
#p ez.mode
|
|
184
|
+
#ez.mode = 5
|
|
185
|
+
#ez.sethsv 300, 250, 255
|
|
186
|
+
#p ez.hsv
|
|
187
|
+
|
|
188
|
+
#rescue TeeloKB::Error => e
|
|
189
|
+
# exec "ruby --disable-gems -I lib:test #{__FILE__}"
|
|
190
|
+
#end
|
data/sample/set_hsv.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'hidaping'
|
|
2
|
+
|
|
3
|
+
def try_block times
|
|
4
|
+
times.times { x = yield; return x if x }
|
|
5
|
+
raise "Couldn't do it"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
dev = HIDAPING.enumerate(0x0, 0x0).find { |dev| dev.product_string == "ErgoDox EZ" }
|
|
9
|
+
|
|
10
|
+
handle = try_block(10) { dev.open }
|
|
11
|
+
p handle
|
|
12
|
+
|
|
13
|
+
h = 0 # 0x0 to 0x168 (which is 360 base 10)
|
|
14
|
+
s = 0xFF # 0x0 to 0xFF
|
|
15
|
+
v = 0xFF # 0x0 to 0xFF
|
|
16
|
+
|
|
17
|
+
try_block(10) do
|
|
18
|
+
handle.write [0x0, 0x6, (h >> 8) & 0xFF, h & 0xFF, s, v].pack('C*')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
handle.read_timeout 10, 300 # 300 ms timeout
|
data/sig/myhidapi.rbs
ADDED
data/test/joycon.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'hidaping'
|
|
2
|
+
|
|
3
|
+
devices = HIDAPING.enumerate(0x0, 0x0)
|
|
4
|
+
|
|
5
|
+
for d in devices
|
|
6
|
+
printf("0x%04x, 0x%04x, %s\n", d.vendor_id, d.product_id, d.product_string)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
handle = HIDAPING.open(0x057e, 0x2007)
|
|
10
|
+
handle.send_feature_report("\x01\x48\x00\x01\x40\x40\x00\x01\x40\x40\x00")
|
|
11
|
+
|
|
12
|
+
while true
|
|
13
|
+
report_raw = handle.read_timeout(64, 500)
|
|
14
|
+
data = report_raw.unpack("a17va*")
|
|
15
|
+
p data[1]
|
|
16
|
+
end
|
data/test/test.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'hidaping'
|
|
2
|
+
|
|
3
|
+
devices = HIDAPING.enumerate(0x0, 0x0)
|
|
4
|
+
|
|
5
|
+
for d in devices
|
|
6
|
+
printf("0x%04x, 0x%04x, %s\n", d.vendor_id, d.product_id, d.product_string)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
if devices.length > 0
|
|
10
|
+
handle = devices[0].open()
|
|
11
|
+
|
|
12
|
+
report_raw = handle.read_timeout(64, 500)
|
|
13
|
+
p report_raw
|
|
14
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'hidaping'
|
|
3
|
+
|
|
4
|
+
class TestHIDAPING < MiniTest::Test
|
|
5
|
+
def test_it_works
|
|
6
|
+
devices = HIDAPING.enumerate 0x0, 0x0
|
|
7
|
+
dev = devices.find { |dev| dev.product_string == "ErgoDox EZ" }
|
|
8
|
+
|
|
9
|
+
handle = dev.open
|
|
10
|
+
|
|
11
|
+
while !handle
|
|
12
|
+
p "retry"
|
|
13
|
+
handle = dev.open
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
p handle
|
|
17
|
+
|
|
18
|
+
buf = [0x0, 0x3, 35]
|
|
19
|
+
loop do
|
|
20
|
+
break if handle.write buf.pack('C*')
|
|
21
|
+
end
|
|
22
|
+
puts "done writing"
|
|
23
|
+
|
|
24
|
+
buf = handle.read_timeout 1, 500
|
|
25
|
+
p buf
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_can_enumerate_by_ids
|
|
29
|
+
devices = HIDAPING.enumerate(0xfeed, 0x1307)
|
|
30
|
+
|
|
31
|
+
refute_predicate devices, :empty?
|
|
32
|
+
end
|
|
33
|
+
end
|
data/test/xbox360.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'hidaping'
|
|
2
|
+
|
|
3
|
+
devices = HIDAPING.enumerate(0x0, 0x0)
|
|
4
|
+
|
|
5
|
+
for d in devices
|
|
6
|
+
printf("0x%04x, 0x%04x, %s\n", d.vendor_id, d.product_id, d.product_string)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
handle = HIDAPING.open(0x045e, 0x028e)
|
|
10
|
+
|
|
11
|
+
while true
|
|
12
|
+
report_raw = handle.read_timeout(64, 500)
|
|
13
|
+
data = report_raw.unpack("vvvvvSb*")
|
|
14
|
+
p data
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hidaping
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Repy
|
|
8
|
+
- Aaron Patterson
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
This is a small wrapper around HIDAPI.
|
|
15
|
+
I couldn't get other HIDAPI wrappers to work, so I wrote this one.
|
|
16
|
+
I'm using it to communicate with my keyboard, so it really only supports enough of the HIDAPI to do that.
|
|
17
|
+
|
|
18
|
+
This gem is a fork from the original work by Aaron Patterson (@tenderlove).
|
|
19
|
+
email:
|
|
20
|
+
- gamecube02+git@gmail.com
|
|
21
|
+
- tenderlove@ruby-lang.org
|
|
22
|
+
executables: []
|
|
23
|
+
extensions:
|
|
24
|
+
- ext/hidaping/extconf.rb
|
|
25
|
+
extra_rdoc_files: []
|
|
26
|
+
files:
|
|
27
|
+
- ".clang-format"
|
|
28
|
+
- ".github/workflows/gem-push.yml"
|
|
29
|
+
- ".github/workflows/test.yml"
|
|
30
|
+
- ".ruby-version"
|
|
31
|
+
- CHANGELOG.md
|
|
32
|
+
- LICENSE.txt
|
|
33
|
+
- README.md
|
|
34
|
+
- Rakefile
|
|
35
|
+
- ext/hidaping/common.h
|
|
36
|
+
- ext/hidaping/extconf.rb
|
|
37
|
+
- ext/hidaping/hidaping.c
|
|
38
|
+
- ext/hidaping/hidaping.h
|
|
39
|
+
- ext/hidaping/hidaping_device.c
|
|
40
|
+
- ext/hidaping/hidaping_device.h
|
|
41
|
+
- ext/hidaping/hidaping_util.h
|
|
42
|
+
- lib/hidaping.rb
|
|
43
|
+
- lib/hidaping/version.rb
|
|
44
|
+
- sample/kbprog.rb
|
|
45
|
+
- sample/set_hsv.rb
|
|
46
|
+
- sig/myhidapi.rbs
|
|
47
|
+
- test/joycon.rb
|
|
48
|
+
- test/test.rb
|
|
49
|
+
- test/test_myhidapi.rb
|
|
50
|
+
- test/xbox360.rb
|
|
51
|
+
homepage: https://github.com/Repy/hidaping
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
homepage_uri: https://github.com/Repy/hidaping
|
|
56
|
+
source_code_uri: https://github.com/Repy/hidaping
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 3.2.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.7.1
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: A wrapper around hidapi for Ruby
|
|
74
|
+
test_files: []
|