buttplugrb 1.0.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.
@@ -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: []