buttplugrb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7c5d05ba327fa3c5eec041b8115fde4dbfd7ff0b
4
+ data.tar.gz: be8dec539e6079430e47ed6f0514ea74c65b13ed
5
+ SHA512:
6
+ metadata.gz: 986d6327dcb9614b469715eabb5216db660c6f71cf4e2d4afa990465d47ac241d72b72ce74da2fe3ffcb995bcda5b5e5a36b1e0f635bf938e398128f075ff6b2
7
+ data.tar.gz: f19225ad6ac973613fa066b08ff4860b08c79a8c75999fc786f2227c6006539b842e9111f6ad1971d0c2c08e9dfc1a18fd635577b8011b33790825d58e74660d
@@ -0,0 +1,72 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * Trolling, insulting/derogatory comments, and personal or political attacks
26
+ * Public or private harassment
27
+ * Publishing others' private information, such as a physical or electronic
28
+ address, without explicit permission
29
+ * Other conduct which could reasonably be considered inappropriate in a
30
+ professional setting
31
+
32
+ ## Our Responsibilities
33
+
34
+ Project maintainers are responsible for clarifying the standards of acceptable
35
+ behavior and are expected to take appropriate and fair corrective action in
36
+ response to any instances of unacceptable behavior.
37
+
38
+ Project maintainers have the right and responsibility to remove, edit, or
39
+ reject comments, commits, code, wiki edits, issues, and other contributions
40
+ that are not aligned to this Code of Conduct, or to ban temporarily or
41
+ permanently any contributor for other behaviors that they deem inappropriate,
42
+ threatening, offensive, or harmful.
43
+
44
+ ## Scope
45
+
46
+ This Code of Conduct applies both within project spaces and in public spaces
47
+ when an individual is representing the project or its community. Examples of
48
+ representing a project or community include using an official project e-mail
49
+ address, posting via an official social media account, or acting as an appointed
50
+ representative at an online or offline event. Representation of a project may be
51
+ further defined and clarified by project maintainers.
52
+
53
+ ## Enforcement
54
+
55
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
56
+ reported by contacting the project team at eva@rigel.moe. All
57
+ complaints will be reviewed and investigated and will result in a response that
58
+ is deemed necessary and appropriate to the circumstances. The project team is
59
+ obligated to maintain confidentiality with regard to the reporter of an incident.
60
+ Further details of specific enforcement policies may be posted separately.
61
+
62
+ Project maintainers who do not follow or enforce the Code of Conduct in good
63
+ faith may face temporary or permanent repercussions as determined by other
64
+ members of the project's leadership.
65
+
66
+ ## Attribution
67
+
68
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
69
+ available at [http://contributor-covenant.org/version/1/4][version]
70
+
71
+ [homepage]: http://contributor-covenant.org
72
+ [version]: http://contributor-covenant.org/version/1/4/
@@ -0,0 +1,34 @@
1
+ # Buttplugrb - Shove Ruby up your ass!
2
+
3
+ Welp ... You have wandered into probably one of the silliest things I set up ... This is a gem to make your ruby app act as a client for a buttplug.io server ...
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'buttplugrb'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install buttplugrb
20
+
21
+ ## Usage
22
+
23
+ You can get client up and running like this:
24
+
25
+ ```ruby
26
+ require "buttplugrb"
27
+ client=Buttplug::Client.new("wss://localhost:12345/buttplug")
28
+ ```
29
+
30
+ You can view some additional examples in the Examples Folder!
31
+
32
+ ## License
33
+
34
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,56 @@
1
+ require "buttplugrb"
2
+ LOCATION="wss://localhost:12345/buttplug" #where our server is located, more than likely going to be on localhost:12345/buttplug
3
+ def pattern1(controller)
4
+ controller.vibrateAll(0.25)
5
+ sleep (3)
6
+ controller.vibrateAll(1)
7
+ sleep (1)
8
+ controller.vibrateAll(0.3)
9
+ sleep 2
10
+ controller.vibrateAll 1
11
+ sleep 2
12
+ controller.vibrateAll 0.6
13
+ sleep 5
14
+ controller.vibrateAll 0.5
15
+ sleep 0.5
16
+ controller.vibrateAll 1
17
+ sleep 20
18
+ end
19
+ def pulsePattern(controller)
20
+ controller.vibrateAll(0)
21
+ sleep 0.5
22
+ controller.vibrateAll(1)
23
+ sleep 0.5
24
+ end
25
+ def lickPattern(controller)
26
+ controller.vibrateAll 0
27
+ sleep 0.25
28
+ controller.vibrateAll 0.5
29
+ sleep 0.25
30
+ end
31
+ def randomPattern(controller)
32
+ controller.vibrateAll(rand(0.0..1.0)) #Set our vibration somewhere in the range of valid numbers
33
+ sleep(rand(5..60)) #and sleep for a random period of time
34
+ end
35
+ client=Buttplug::Client.new(LOCATION) #initalizing our client
36
+ sleep 0.1 #Giving our client a moment to wake up
37
+ client.startScanning(); #Telling the server to start scanning for new devices, ble or otherwise
38
+ sleep 1 #Giving it a moment to find our device
39
+ devices=[]
40
+ while devices==[] do
41
+ devices=client.listDevices() #Grabing our device list
42
+ sleep 1
43
+ end
44
+ client.stopScanning() #No sense in tying up the server ... not just yet~
45
+ controller=Buttplug::Device.new(client,devices[0]) #and generating a new device from the first client on the list
46
+ #ok we are gonna make a few assumptions here ... primarily that you are using an xbox controller
47
+ if(controller.methods.include? :vibrateAll) #Ok due to metaprogramming bs we have to check to see if the device in question supports vibration
48
+ (0..20000).each{|i|
49
+ (0..5).each{|j|
50
+ lickPattern(controller)
51
+ }
52
+ controller.vibrateAll 1
53
+ sleep 10
54
+ }
55
+ controller.vibrateAll(0.0) #and shutting off the lights~
56
+ end
@@ -0,0 +1,312 @@
1
+ require 'faye/websocket'
2
+ require 'eventmachine'
3
+ require 'json'
4
+
5
+ =begin rdoc
6
+ Our Module for containg the functions and classes relating to the Buttplugrb gem
7
+ =end
8
+ module Buttplug
9
+ =begin rdoc
10
+ Our Client for a buttplug.io server
11
+ =end
12
+ class Client
13
+ =begin rdoc
14
+ Creates a new client for buttplug.io
15
+
16
+ Arguments:
17
+ * serverLocation (string) - Where our buttplug.io server is hosted. this will tend to be: <code>"wss://localhost:12345/buttplug"</code>
18
+
19
+ Returns:
20
+ * A shiney new buttplug client ready for some action
21
+ =end
22
+ def initialize(serverLocation)
23
+ @location=serverLocation
24
+ #Ok Explanation time!
25
+ # * @EventQueue - The events we are triggering on the server, Expected to be an array, with the first element being the message Id, and the second being the message itself!
26
+ @eventQueue=EM::Queue.new
27
+ @eventMachine=Thread.new{EM.run{
28
+ eventQueue=@eventQueue
29
+ messageWatch={}
30
+ ws = Faye::WebSocket::Client.new(@location)
31
+ tickLoop=EM.tick_loop do #Should improve response times~
32
+ eventQueue.pop{|msg|
33
+ ws.send msg[1]
34
+ messageWatch[msg[0]]=msg
35
+ p [Time.now, :message_send, msg[1]]
36
+ }
37
+ end
38
+ ws.on :open do |event|
39
+ p [Time.now, :open]
40
+ ws.send '[{"RequestServerInfo": {"Id": 1, "ClientName": "roboMegumin", "MessageVersion": 1}}]'
41
+ end
42
+ ws.on :message do |event|
43
+ message=JSON::parse(event.data)[0]
44
+ message.each{|key,value|
45
+ #We don't really care about the key just yet ... We are going to just care about finding our ID
46
+ if(messageWatch.keys.include?(value["Id"]))
47
+ messageWatch[value["Id"]]<<{key => value}#And now we care about our key!
48
+ puts messageWatch[value["Id"]].object_id
49
+ messageWatch.delete(value["Id"])
50
+ p [Time.now, :message_recieved, [{key => value}]]
51
+ next
52
+ elsif(key=="ServerInfo")
53
+ p [Time.now, :server_info, value]
54
+ end
55
+ }
56
+ end
57
+ ws.on :close do |event|
58
+ p [Time.now, :close, event.code, event.reason]
59
+ ws = nil
60
+ end
61
+ EM.add_periodic_timer(0.5){
62
+ ws.send "[{\"Ping\": {\"Id\": #{generateID()}}}]"
63
+ }
64
+ }}
65
+ @eventMachine.run
66
+ end
67
+ =begin rdoc
68
+ Tells our server to start scanning for new devices
69
+ =end
70
+ def startScanning()
71
+ id=generateID()
72
+ @eventQueue.push([id,"[{\"StartScanning\":{\"Id\":#{id}}}]"])
73
+ end
74
+ =begin rdoc
75
+ Tells our server to stop scanning for new devices
76
+ =end
77
+ def stopScanning()
78
+ id=generateID()
79
+ @eventQueue.push([id,"[{\"StopScanning\":{\"Id\":#{id}}}]"])
80
+ end
81
+ =begin rdoc
82
+ Lists all devices available to the server
83
+
84
+ Returns:
85
+ * An array of available devices from the server
86
+
87
+ Example:
88
+ client.listDevices()
89
+ [{"DeviceName"=>"XBox Compatible Gamepad (XInput)", "DeviceIndex"=>1, "DeviceMessages"=>{"SingleMotorVibrateCmd"=>{}, "VibrateCmd"=>{"FeatureCount"=>2}, "StopDeviceCmd"=>{}}}]
90
+ =end
91
+ def listDevices()
92
+ id=generateID()
93
+ deviceRequest=[id,"[{\"RequestDeviceList\": {\"Id\":#{id}}}]"]
94
+ @eventQueue.push(deviceRequest)
95
+ while(deviceRequest.length<3) do
96
+ sleep 0.01#Just so we arn't occupying all the time on the system while we are waiting for our device list to come back.
97
+ end
98
+ return deviceRequest[2]["DeviceList"]["Devices"]
99
+ end
100
+ =begin rdoc
101
+ Stops all devices currently controlled by the server
102
+ =end
103
+ def stopAllDevices()
104
+ id=generateID()
105
+ deviceRequest=[id,"[{\"StopAllDevices\": {\"ID\":#{id}}}]"]
106
+ @eventQueue.push(deviceRequest)
107
+ end
108
+ =begin rdoc
109
+ Sends a message to our buttplug server
110
+
111
+ Arguments:
112
+ * message (JSON formatted string) - The message we are sending to our server
113
+
114
+ Returns:
115
+ * the Response from our server
116
+ =end
117
+ def sendMessage(message)
118
+ @eventQueue.push(message)
119
+ while(message.length<3) do
120
+ sleep 0.01
121
+ end
122
+ return message[3]
123
+ end
124
+ =begin rdoc
125
+ Does exactly what it says on the tin, generates a random id for our messages
126
+
127
+ Returns:
128
+ * a number between 2 and 4294967295
129
+ =end
130
+ def generateID()
131
+ return rand(2..4294967295)
132
+ end
133
+ end
134
+ =begin rdoc
135
+ This class creates a Wrapper for your various devices you fetched from listDevices for your controlling pleasure!
136
+ =end
137
+ class Device
138
+ =begin rdoc
139
+ Creates our Device wrapper for our client
140
+
141
+ Note: This does create a few functions on the fly. you should check to see if they are available using .methods.include
142
+
143
+ Arguments:
144
+ * client (Buttplug::Client) - Our buttplugrb client that we are gonna use to control our device
145
+ * deviceInfo (Hash) - Our information that we should have fetched from the list_devices() instance method ... should look like:
146
+ {"DeviceName"=>"XBox Compatible Gamepad (XInput)", "DeviceIndex"=>1, "DeviceMessages"=>{"SingleMotorVibrateCmd"=>{}, "VibrateCmd"=>{"FeatureCount"=>2}, "StopDeviceCmd"=>{}}}
147
+
148
+ Returns:
149
+ * Our nicely bundled up device ready to be domminated~
150
+ =end
151
+ def initialize(client, deviceInfo)
152
+ #Ok we are gonna expect our deviceInfo to be a Hash so we can do some ... fun things ...
153
+ #{"DeviceName"=>"XBox Compatible Gamepad (XInput)", "DeviceIndex"=>1, "DeviceMessages"=>{"SingleMotorVibrateCmd"=>{}, "VibrateCmd"=>{"FeatureCount"=>2}
154
+ @deviceName=deviceInfo["DeviceName"]
155
+ @deviceIndex=deviceInfo["DeviceIndex"]
156
+ @client=client
157
+ #Ok so we are starting our weird metaProgramming BS here
158
+
159
+ if(deviceInfo["DeviceMessages"].keys.include? "VibrateCmd")
160
+ @vibeMotors=deviceInfo["DeviceMessages"]["VibrateCmd"]["FeatureCount"]
161
+ define_singleton_method(:vibrate){|speeds|
162
+ #And now the real fun, we are gonna craft our message!
163
+ id=client.generateID()
164
+ cmd=[{"VibrateCmd"=>{"Id"=>id,"DeviceIndex"=>@deviceIndex,"Speeds"=>[]}}]
165
+ #Ok we arn't gonna really care about how many speeds we are fed in here, we are gonna make sure that our total array isn't empty.
166
+ (0..@vibeMotors-1).each{|i|
167
+ if speeds[i].nil?
168
+ speeds[i]=0
169
+ end
170
+ cmd[0]["VibrateCmd"]["Speeds"]<<{"Index"=>i,"Speed"=>speeds[i]}
171
+ }
172
+ client.sendMessage([id,cmd.to_json])
173
+ }
174
+ generateActivateAllCommand(:vibrate,@vibeMotors,:vibrateAll)
175
+ end
176
+ if(deviceInfo["DeviceMessages"].keys.include? "LinearCmd")
177
+ @linearActuators=deviceInfo["DeviceMessages"]["LinearCmd"]["FeatureCount"]
178
+ generateArrayedHashCommand({"Duration"=>0, "Position"=>0.0},@linearActuators,"LinearCmd","Vectors",:stroke)
179
+ generateActivateAllCommand(:stroke,@linearActuators,:strokeAll)
180
+ end
181
+ if(deviceInfo["DeviceMessages"].keys.include? "RotateCmd")
182
+ @rotationMotors=deviceInfo["DeviceMessages"]["RotateCmd"]["FeatureCount"]
183
+ generateArrayedHashCommand({"Speed"=>0.0,"Clockwise"=>true},@rotationMotors,"RotateCmd","Rotations",:rotate)
184
+ generateActivateAllCommand(:rotate,@rotationMotors,:rotateAll)
185
+ end
186
+ if(deviceInfo["DeviceMessages"].keys.include? "RawCmd")
187
+ #TODO: Do some stuff here with RawCmd? ... Honestly I don't know what devices would support this ... possibly estim but at the moment 🤷 I have no idea. 🤷
188
+ #To implement: https://metafetish.github.io/buttplug/generic.html#rawcmd
189
+ end
190
+ end
191
+ =begin rdoc
192
+ Stops the Device from any current actions that it might be taking.
193
+ =end
194
+ def stopDevice
195
+ id=@client.generateID()
196
+ cmd="[{\"StopDeviceCmd\": {\"ID\":#{id},\"DeviceIndex\":#{@deviceIndex}}}]"
197
+ @client.sendMessage([id,cmd])
198
+ end
199
+ ##
200
+ # :method: vibrate
201
+ #
202
+ # Vibrates the motors on the device! (⁄ ⁄•⁄ω⁄•⁄ ⁄)
203
+ #
204
+ # Arguments:
205
+ # * speeds (Array - Float) - Array of speeds, any extra speeds will be dropped, and any ommitted speeds will be set to 0
206
+ #
207
+ # example:
208
+ # device.vibrate([0.2,0.3,1])
209
+
210
+ ##
211
+ # :method: vibrateAll
212
+ #
213
+ # Vibrates all motors on the device (⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)
214
+ #
215
+ # Arguments:
216
+ # * speed (Float) - The speed that all motors on the device to be set to
217
+ #
218
+ # example:
219
+ # device.vibrateAll(0.2)
220
+
221
+ ##
222
+ # :method: stroke
223
+ # Sends a command to well ... Actuate the linear motors of the device („ಡωಡ„)
224
+ #
225
+ # Arguments:
226
+ # * vectors (Array - Hash) - Array of Vectors, any extra will be dropped, and any ommited will be set to a duration of 0 and a posision of 0.0.
227
+ #
228
+ # example:
229
+ # device.stroke([{"Duration"=>300, "Position"=>0.2},{"Duration"=>1000, "Position"=>0.8}])
230
+
231
+ ##
232
+ # :method: strokeAll
233
+ #
234
+ # Sends a command to all linear actuators to respond to a vector (ง ื▿ ื)ว
235
+ #
236
+ # Arguments:
237
+ # * vector (Hash) - A single vector.
238
+ #
239
+ # example:
240
+ # device.strokeAll({"Duration"=>300, "Position"=>0.2})
241
+
242
+ ##
243
+ # :method: rotate
244
+ # Spins whatever feature rotates right round ... baby right round~ (ノ*°▽°*)
245
+ #
246
+ # Arguments:
247
+ # * rotations (Array - Hash) - Array of Vectors, any extra will be dropped, and any ommited will be set to a duration of 0 and a posision of 0.0.
248
+ #
249
+ # example:
250
+ # device.rotate([{"Speed"=>0.5,"Clockwise"=>true},{"Speed=>1, "Clockwise"=>false}])
251
+
252
+ ##
253
+ # :method: rotateAll
254
+ # Spins All the features Right round like a record, baby Right round round round (*ノωノ)
255
+ #
256
+ # Arguments:
257
+ # * rotation (Hash) - Our single rotation we are sending to all the features
258
+ #
259
+ # example:
260
+ # device.rotateAll({"Speed"=>0.5,"Clockwise"=>true})
261
+
262
+ ##
263
+ #
264
+ protected
265
+ =begin rdoc
266
+ Helper Function to generate Metaprogrammed Methods for various buttplug stuff
267
+
268
+ Arguments:
269
+ * blankHash (Hash) - An example of a Nilled out hash so the newly minted function knows what 0 looks like
270
+ * featureCount (Int) - How many features are we talking about here? I've heard rumors of a dildo with 10 vibrators~
271
+ * controlName (String) - And what command are we exactly sending to our server?
272
+ # arrayName (String) - What Buttplug.io is expecting the array to be called
273
+ * cmdName (Method) - Annnnd what are we gonna call our newly minted method?
274
+
275
+ example:
276
+ generateArrayedHashCommand({"Speed"=>0.0,"Clockwise"=>true},@rotationMotors,"RotateCmd",:rotate)
277
+ =end
278
+ def generateArrayedHashCommand(blankHash, featureCount, controlName, arrayName ,cmdName) #AKA I have a feeling that if we get a dedicated function for estim boxes I feel like I'd have to rewrite this code again... so let's dry it the fuck up!
279
+ define_singleton_method(cmdName){|hash|
280
+ id=@client.generateID()
281
+ cmd=[{controlName=>{"Id"=>id,"DeviceIndex"=>@deviceIndex,arrayName=>[]}}]
282
+ (0..featureCount-1).each{|i|
283
+ if hash[i].nil?
284
+ hash[i]=blankHash
285
+ end
286
+ hash[i]["Index"]=i
287
+ cmd[0][controlName][arrayName]<<hash[i]
288
+ }
289
+ @client.sendMessage([id,cmd.to_json])
290
+ }
291
+ end
292
+ =begin rdoc
293
+ Helper Function to generate Metaprogrammed methods to set all instances of a feature to the same value
294
+
295
+ Arguments:
296
+ * cmdToAll (Method) - The command we are gonna call when we want to send our DO ALL THE THINGS signal
297
+ * featureCount (Int) - How many features are we controlling?
298
+ * cmdName (Method) - Annnnd what are we gonna call our newly minted command?
299
+ =end
300
+ def generateActivateAllCommand(cmdToAll, featureCount, cmdName)
301
+ define_singleton_method(cmdName){|var|
302
+ vars=[]
303
+ (0..featureCount-1).each{|i|
304
+ vars<<var
305
+ }
306
+ self.public_send(cmdToAll, vars)
307
+ }
308
+ end
309
+ end
310
+ end
311
+ #And loading in any other things that might help (including debug ...)
312
+ Dir["#{File.dirname(__FILE__)}/buttplugrb/*.rb"].each {|file| require file }
@@ -0,0 +1,3 @@
1
+ module Buttplug
2
+ VERSION="1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: buttplugrb
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nora Maguire
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faye-websocket
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.10.7
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.10.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: eventmachine
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.2.5
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.0.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.0.2
55
+ description:
56
+ email: eva@rigel.moe
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - CODE_OF_CONDUCT.md
62
+ - README.md
63
+ - Rakefile
64
+ - examples/Vibrate.rb
65
+ - lib/buttplugrb.rb
66
+ - lib/buttplugrb/VERSION
67
+ homepage: http://rubygems.org/gems/buttplugrb
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ - bin
76
+ - examples
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.6.14
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Buttplug Client Library
93
+ test_files: []