mosq 0.0.1
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/LICENSE +21 -0
- data/README.md +9 -0
- data/ext/mosq/Rakefile +47 -0
- data/lib/mosq.rb +13 -0
- data/lib/mosq/client.rb +324 -0
- data/lib/mosq/client/bucket.rb +76 -0
- data/lib/mosq/ffi.rb +135 -0
- data/lib/mosq/ffi/error.rb +58 -0
- data/lib/mosq/util.rb +69 -0
- metadata +158 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: e3e826b96ac902207ad690a3ea90a49acc266eb0
|
|
4
|
+
data.tar.gz: b0208ffc9b595687d936b704c15ce3dd0b06b864
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7b605dbc96e3bd96b3c75fe2d9ed1f20c8ff187c2e752e1f2ae77ea5971bb1621100ade4a59f1f8fe82a74edff215d5779be7449ed417c91fb170cf79a1eef27
|
|
7
|
+
data.tar.gz: 00d94526260cb7ba5313125ddbfa3aa5e742db6853f072fe52b23b03297885ba334d6f8443c12d8768f6c975280c36028c80bf7cf5b969a315035b162cf49318
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Joe McIlvain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# mosq
|
|
2
|
+
|
|
3
|
+
[](https://circleci.com/gh/jemc/ruby-mosq/tree/master)
|
|
4
|
+
[](http://badge.fury.io/rb/mosq)
|
|
5
|
+
[](https://gitter.im/jemc/ruby-mosq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
6
|
+
|
|
7
|
+
[MQTT](http://mqtt.org/) client library based on [FFI](https://github.com/ffi/ffi/wiki) bindings for [libmosquitto](http://mosquitto.org/man/libmosquitto-3.html).
|
|
8
|
+
|
|
9
|
+
##### `$ gem install mosq`
|
data/ext/mosq/Rakefile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
require 'rake/clean'
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
FILES = {}
|
|
6
|
+
|
|
7
|
+
task :default => [:build, :compact]
|
|
8
|
+
|
|
9
|
+
def self.file_task(filename, opts, &block)
|
|
10
|
+
name, dep = opts.is_a?(Hash) ? opts.to_a.first : [opts, nil]
|
|
11
|
+
|
|
12
|
+
FILES[name] = filename
|
|
13
|
+
CLEAN.include filename
|
|
14
|
+
task name => filename
|
|
15
|
+
|
|
16
|
+
if dep
|
|
17
|
+
file filename => FILES[dep], &block
|
|
18
|
+
else
|
|
19
|
+
file filename, &block
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def cmd(string)
|
|
24
|
+
fail "Command failed: #{string}" unless system(string)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
file_task 'mosquitto.tar.gz', :download_tarball do
|
|
28
|
+
version = "1.4.2"
|
|
29
|
+
release = "http://mosquitto.org/files/source/mosquitto-#{version}.tar.gz"
|
|
30
|
+
cmd "wget -O #{FILES[:download_tarball]} #{release}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
file_task 'mosquitto', :download => :download_tarball do
|
|
34
|
+
cmd "tar -zxf #{FILES[:download_tarball]}"
|
|
35
|
+
cmd "mv mosquitto-* #{FILES[:download]}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
file_task "libmosquitto.#{::FFI::Platform::LIBSUFFIX}", :build => :download do
|
|
39
|
+
cmd "/usr/bin/env sh -c 'cd #{FILES[:download]} && env CFLAGS=-g make'"
|
|
40
|
+
cmd "cp #{FILES[:download]}/lib/#{FILES[:build]}* ./#{FILES[:build]}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
task :compact => FILES[:build] do
|
|
44
|
+
FILES.each do |key, filename|
|
|
45
|
+
cmd "rm -rf #{filename}" unless key == :build
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/mosq.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
require_relative 'mosq/util'
|
|
3
|
+
require_relative 'mosq/ffi'
|
|
4
|
+
require_relative 'mosq/ffi/error'
|
|
5
|
+
|
|
6
|
+
require_relative 'mosq/client'
|
|
7
|
+
|
|
8
|
+
# Call to initialize the library
|
|
9
|
+
Mosq::Util.error_check "initializing the libmosquitto library",
|
|
10
|
+
Mosq::FFI.mosquitto_lib_init
|
|
11
|
+
|
|
12
|
+
# Call cleanup at exit clean up the library
|
|
13
|
+
at_exit { Mosq::FFI.mosquitto_lib_cleanup }
|
data/lib/mosq/client.rb
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
|
|
2
|
+
require 'socket'
|
|
3
|
+
|
|
4
|
+
require_relative 'client/bucket'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
module Mosq
|
|
8
|
+
class Client
|
|
9
|
+
|
|
10
|
+
# Raised when an operation is performed on an already-destroyed {Client}.
|
|
11
|
+
class DestroyedError < RuntimeError; end
|
|
12
|
+
|
|
13
|
+
# Create a new {Client} instance with the given properties.
|
|
14
|
+
def initialize(*args)
|
|
15
|
+
@options = Util.connection_info(*args)
|
|
16
|
+
|
|
17
|
+
@options[:heartbeat] ||= 30 # seconds
|
|
18
|
+
@protocol_timeout = DEFAULT_PROTOCOL_TIMEOUT
|
|
19
|
+
|
|
20
|
+
Util.null_check "creating the client",
|
|
21
|
+
(@ptr = FFI.mosquitto_new(@options[:client_id], true, nil))
|
|
22
|
+
|
|
23
|
+
@bucket = Bucket.new(@ptr)
|
|
24
|
+
@event_handlers = {}
|
|
25
|
+
|
|
26
|
+
@packet_id_ptr = Util.mem_ptr(:int)
|
|
27
|
+
|
|
28
|
+
@finalizer = self.class.create_finalizer_for(@ptr)
|
|
29
|
+
ObjectSpace.define_finalizer(self, @finalizer)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @api private
|
|
33
|
+
def self.create_finalizer_for(ptr)
|
|
34
|
+
Proc.new do
|
|
35
|
+
FFI.mosquitto_destroy(ptr)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def username; @options.fetch(:username); end
|
|
40
|
+
def password; @options.fetch(:password); end
|
|
41
|
+
def host; @options.fetch(:host); end
|
|
42
|
+
def port; @options.fetch(:port); end
|
|
43
|
+
def ssl?; @options.fetch(:ssl); end
|
|
44
|
+
def heartbeat; @options.fetch(:heartbeat); end
|
|
45
|
+
|
|
46
|
+
# The maximum time interval the user application should wait between
|
|
47
|
+
# yielding control back to the client object by calling run_loop!.
|
|
48
|
+
def max_poll_interval
|
|
49
|
+
@options.fetch(:heartbeat) / 2.0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ptr
|
|
53
|
+
raise DestroyedError unless @ptr
|
|
54
|
+
@ptr
|
|
55
|
+
end
|
|
56
|
+
private :ptr
|
|
57
|
+
|
|
58
|
+
# Initiate the connection with the server.
|
|
59
|
+
# It is necessary to call this before any other communication.
|
|
60
|
+
def start
|
|
61
|
+
Util.error_check "configuring the username and password",
|
|
62
|
+
FFI.mosquitto_username_pw_set(ptr, @options[:usernam], @options[:password])
|
|
63
|
+
|
|
64
|
+
Util.error_check "connecting to #{@options[:host]}",
|
|
65
|
+
FFI.mosquitto_connect(ptr, @options[:host], @options[:port], @options[:heartbeat])
|
|
66
|
+
|
|
67
|
+
@ruby_socket = Socket.for_fd(FFI.mosquitto_socket(ptr))
|
|
68
|
+
@ruby_socket.autoclose = false
|
|
69
|
+
|
|
70
|
+
res = fetch_response(:connect, nil)
|
|
71
|
+
raise Mosq::FFI::Error::NoConn, res.fetch(:message) \
|
|
72
|
+
unless res.fetch(:status) == 0
|
|
73
|
+
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Gracefully close the connection with the server.
|
|
78
|
+
def close
|
|
79
|
+
@ruby_socket = nil
|
|
80
|
+
|
|
81
|
+
Util.error_check "closing the connection to #{@options[:host]}",
|
|
82
|
+
FFI.mosquitto_disconnect(ptr)
|
|
83
|
+
|
|
84
|
+
self
|
|
85
|
+
rescue Mosq::FFI::Error::NoConn
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Free the native resources associated with this object. This will
|
|
90
|
+
# be done automatically on garbage collection if not called explicitly.
|
|
91
|
+
def destroy
|
|
92
|
+
if @finalizer
|
|
93
|
+
@finalizer.call
|
|
94
|
+
ObjectSpace.undefine_finalizer(self)
|
|
95
|
+
end
|
|
96
|
+
@ptr = @finalizer = @ruby_socket = @bucket = nil
|
|
97
|
+
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Register a handler for events on the given channel of the given type.
|
|
102
|
+
# Only one handler for each event type may be registered at a time.
|
|
103
|
+
# If no callable or block is given, the handler will be cleared.
|
|
104
|
+
#
|
|
105
|
+
# @param type [Symbol] The type of event to watch for.
|
|
106
|
+
# @param callable [#call,nil] The callable handler if no block is given.
|
|
107
|
+
# @param block [Proc,nil] The handler block to register.
|
|
108
|
+
# @return [Proc,#call,nil] The given block or callable.
|
|
109
|
+
# @yieldparam event [Hash] The event passed to the handler.
|
|
110
|
+
#
|
|
111
|
+
def on_event(type, callable=nil, &block)
|
|
112
|
+
handler = block || callable
|
|
113
|
+
raise ArgumentError, "expected block or callable as the event handler" \
|
|
114
|
+
unless handler.respond_to?(:call)
|
|
115
|
+
|
|
116
|
+
@event_handlers[type.to_sym] = handler
|
|
117
|
+
handler
|
|
118
|
+
end
|
|
119
|
+
alias_method :on, :on_event
|
|
120
|
+
|
|
121
|
+
# Unregister the event handler associated with the given channel and method.
|
|
122
|
+
#
|
|
123
|
+
# @param type [Symbol] The type of protocol method to watch for.
|
|
124
|
+
# @return [Proc,nil] This removed handler, if any.
|
|
125
|
+
#
|
|
126
|
+
def clear_event_handler(type)
|
|
127
|
+
@event_handlers.delete(type.to_sym)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The timeout to use when waiting for protocol events, in seconds.
|
|
131
|
+
# By default, this has the value of {DEFAULT_PROTOCOL_TIMEOUT}.
|
|
132
|
+
# When set, it affects operations like {#run_loop!}.
|
|
133
|
+
attr_accessor :protocol_timeout
|
|
134
|
+
DEFAULT_PROTOCOL_TIMEOUT = 30 # seconds
|
|
135
|
+
|
|
136
|
+
# Subscribe to the given topic. Messages with matching topic will be
|
|
137
|
+
# delivered to the {:message} event handler registered with {on_event}.
|
|
138
|
+
#
|
|
139
|
+
# @param topic [String] The topic patten to subscribe to.
|
|
140
|
+
# @param qos [Integer] The QoS level to expect for received messages.
|
|
141
|
+
# @return [Client] This client.
|
|
142
|
+
#
|
|
143
|
+
def subscribe(topic, qos: 0)
|
|
144
|
+
Util.error_check "subscribing to a topic",
|
|
145
|
+
FFI.mosquitto_subscribe(ptr, @packet_id_ptr, topic, qos)
|
|
146
|
+
|
|
147
|
+
fetch_response(:subscribe, @packet_id_ptr.read_int)
|
|
148
|
+
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Unsubscribe from the given topic.
|
|
153
|
+
#
|
|
154
|
+
# @param topic [String] The topic patten to unsubscribe from.
|
|
155
|
+
# @return [Client] This client.
|
|
156
|
+
#
|
|
157
|
+
def unsubscribe(topic)
|
|
158
|
+
Util.error_check "unsubscribing from a topic",
|
|
159
|
+
FFI.mosquitto_unsubscribe(ptr, @packet_id_ptr, topic)
|
|
160
|
+
|
|
161
|
+
fetch_response(:unsubscribe, @packet_id_ptr.read_int)
|
|
162
|
+
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Publish a message with the given topic and payload.
|
|
167
|
+
#
|
|
168
|
+
# @param topic [String] The topic to publish on.
|
|
169
|
+
# @param payload [String] The payload to publish.
|
|
170
|
+
# @param qos [Integer] The QoS level to use for the publish transaction.
|
|
171
|
+
# @param retain [Boolean] Whether the broker should retain the message.
|
|
172
|
+
# @return [Client] This client.
|
|
173
|
+
#
|
|
174
|
+
def publish(topic, payload, qos: 0, retain: false)
|
|
175
|
+
Util.error_check "publishing a message",
|
|
176
|
+
FFI.mosquitto_publish(ptr, @packet_id_ptr,
|
|
177
|
+
topic, payload.bytesize, payload, qos, retain)
|
|
178
|
+
|
|
179
|
+
fetch_response(:publish, @packet_id_ptr.read_int)
|
|
180
|
+
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Fetch and handle events in a loop that blocks the calling thread.
|
|
185
|
+
# The loop will continue until the {#break!} method is called from within
|
|
186
|
+
# an event handler, or until the given timeout duration has elapsed.
|
|
187
|
+
# Note that this must be called at least as frequently as the heartbeat
|
|
188
|
+
# interval to ensure that the client is not disconnected - if control is
|
|
189
|
+
# not yielded to the client transport heartbeats will not be maintained.
|
|
190
|
+
#
|
|
191
|
+
# @param timeout [Float] the maximum time to run the loop, in seconds;
|
|
192
|
+
# if none is given, the value is {#protocol_timeout} or until {#break!}
|
|
193
|
+
# @param block [Proc,nil] if given, the block will be yielded each
|
|
194
|
+
# non-exception event received on any channel. Other handlers or
|
|
195
|
+
# response fetchings that match the event will still be processed,
|
|
196
|
+
# as the block does not consume the event or replace the handlers.
|
|
197
|
+
# @return [undefined] assume no value - reserved for future use.
|
|
198
|
+
#
|
|
199
|
+
def run_loop!(timeout: protocol_timeout, &block)
|
|
200
|
+
timeout = Float(timeout) if timeout
|
|
201
|
+
fetch_events(timeout, &block)
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Yield control to the client object to do any connection-oriented work
|
|
206
|
+
# that needs to be done, including heartbeating. This is the same as
|
|
207
|
+
# calling {#run_loop!} with no block and a timeout of 0.
|
|
208
|
+
#
|
|
209
|
+
def run_immediate!
|
|
210
|
+
run_loop!(timeout: 0)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Stop iterating from within an execution of the {#run_loop!} method.
|
|
214
|
+
# Call this method only from within an event handler.
|
|
215
|
+
# It will take effect only after the handler finishes running.
|
|
216
|
+
#
|
|
217
|
+
# @return [nil]
|
|
218
|
+
#
|
|
219
|
+
def break!
|
|
220
|
+
@breaking = true
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
# Calculate the amount of the timeout remaining from the given start time
|
|
227
|
+
def remaining_timeout(timeout=0, start=Time.now)
|
|
228
|
+
return nil unless timeout
|
|
229
|
+
timeout = timeout - (Time.now - start)
|
|
230
|
+
timeout < 0 ? 0 : timeout
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Block until there is readable data on the internal ruby socket,
|
|
234
|
+
# returning true if there is readable data, or false if time expired.
|
|
235
|
+
def select(timeout=0)
|
|
236
|
+
return false unless @ruby_socket
|
|
237
|
+
IO.select([@ruby_socket], [], [], timeout) ? true : false
|
|
238
|
+
rescue Errno::EBADF
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Execute the handler for this type of event, if any.
|
|
243
|
+
def handle_incoming_event(event)
|
|
244
|
+
if (handler = (@event_handlers[event.fetch(:type)]))
|
|
245
|
+
handler.call(event)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def connection_housekeeping
|
|
250
|
+
# Do any pending outbound writes.
|
|
251
|
+
while FFI.mosquitto_want_write(ptr)
|
|
252
|
+
Util.error_check "sending outbound packets",
|
|
253
|
+
FFI.mosquitto_loop_write(ptr, 1)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Do any pending stateful protocol packets.
|
|
257
|
+
Util.error_check "handling stateful protocol packets",
|
|
258
|
+
FFI.mosquitto_loop_misc(ptr)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Return the next incoming event as a Hash, or nil if time expired.
|
|
262
|
+
def fetch_next_event(timeout=0, start=Time.now)
|
|
263
|
+
max_timeout = max_poll_interval
|
|
264
|
+
|
|
265
|
+
# Check if any data is immediately available to read
|
|
266
|
+
if select(0)
|
|
267
|
+
Util.error_check "reading immediate inbound packets",
|
|
268
|
+
FFI.mosquitto_loop_read(ptr, 1)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
while true
|
|
272
|
+
connection_housekeeping
|
|
273
|
+
|
|
274
|
+
# Check for an event already waiting in the bucket
|
|
275
|
+
return @bucket.events.shift unless @bucket.events.empty?
|
|
276
|
+
|
|
277
|
+
# Calculate remaining timeout and break if breaking or time expired.
|
|
278
|
+
remaining = remaining_timeout(timeout, start)
|
|
279
|
+
return nil if remaining && remaining <= 0
|
|
280
|
+
|
|
281
|
+
# Wait for data to arrive on the socket.
|
|
282
|
+
select_timeout = remaining ? [remaining, max_timeout].min : nil
|
|
283
|
+
if select(select_timeout)
|
|
284
|
+
Util.error_check "reading inbound packets",
|
|
285
|
+
FFI.mosquitto_loop_read(ptr, 1)
|
|
286
|
+
|
|
287
|
+
unless @bucket.events.empty?
|
|
288
|
+
connection_housekeeping
|
|
289
|
+
return @bucket.events.shift
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Internal implementation of the {#run_loop!} method.
|
|
296
|
+
def fetch_events(timeout=protocol_timeout, start=Time.now)
|
|
297
|
+
while (event = fetch_next_event(timeout, start))
|
|
298
|
+
handle_incoming_event(event)
|
|
299
|
+
yield event if block_given?
|
|
300
|
+
break if @breaking
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Internal implementation of synchronous responses.
|
|
305
|
+
def fetch_response(expected_type, expected_packet_id, timeout=protocol_timeout, start=Time.now)
|
|
306
|
+
unwanted_events = []
|
|
307
|
+
|
|
308
|
+
while (event = fetch_next_event(timeout, start))
|
|
309
|
+
if (event.fetch(:type) == expected_type) && (
|
|
310
|
+
!expected_packet_id ||
|
|
311
|
+
event.fetch(:packet_id) == expected_packet_id
|
|
312
|
+
)
|
|
313
|
+
unwanted_events.reverse_each { |e| @bucket.events.unshift(e) }
|
|
314
|
+
handle_incoming_event(event)
|
|
315
|
+
return event
|
|
316
|
+
else
|
|
317
|
+
unwanted_events.push(event)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
raise FFI::Error::Timeout, "waiting for #{expected_type} response"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
module Mosq
|
|
3
|
+
class Client
|
|
4
|
+
|
|
5
|
+
# @api private
|
|
6
|
+
class Bucket
|
|
7
|
+
def initialize(ptr)
|
|
8
|
+
FFI.mosquitto_connect_callback_set ptr, method(:on_connect)
|
|
9
|
+
# FFI.mosquitto_disconnect_callback_set ptr, method(:on_disconnect)
|
|
10
|
+
FFI.mosquitto_publish_callback_set ptr, method(:on_publish)
|
|
11
|
+
FFI.mosquitto_message_callback_set ptr, method(:on_message)
|
|
12
|
+
FFI.mosquitto_subscribe_callback_set ptr, method(:on_subscribe)
|
|
13
|
+
FFI.mosquitto_unsubscribe_callback_set ptr, method(:on_unsubscribe)
|
|
14
|
+
# FFI.mosquitto_log_callback_set ptr, method(:on_log)
|
|
15
|
+
|
|
16
|
+
@events = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :events
|
|
20
|
+
|
|
21
|
+
def on_connect(ptr, _, status)
|
|
22
|
+
@events << {
|
|
23
|
+
type: :connect,
|
|
24
|
+
status: status,
|
|
25
|
+
message: case status
|
|
26
|
+
when 0; "success"
|
|
27
|
+
when 1; "connection refused (unacceptable protocol version)"
|
|
28
|
+
when 2; "connection refused (identifier rejected)"
|
|
29
|
+
when 3; "connection refused (broker unavailable)"
|
|
30
|
+
else "unknown connection failure"
|
|
31
|
+
end,
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# def on_disconnect(ptr, _, status)
|
|
36
|
+
|
|
37
|
+
# end
|
|
38
|
+
|
|
39
|
+
def on_publish(ptr, _, packet_id)
|
|
40
|
+
@events << {
|
|
41
|
+
type: :publish,
|
|
42
|
+
packet_id: packet_id,
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def on_message(ptr, _, message)
|
|
47
|
+
@events << {
|
|
48
|
+
type: :message,
|
|
49
|
+
topic: message[:topic].read_string,
|
|
50
|
+
payload: message[:payload].read_bytes(message[:payloadlen]),
|
|
51
|
+
retained: message[:retain],
|
|
52
|
+
qos: message[:qos],
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def on_subscribe(ptr, _, packet_id, _, _)
|
|
57
|
+
@events << {
|
|
58
|
+
type: :subscribe,
|
|
59
|
+
packet_id: packet_id,
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def on_unsubscribe(ptr, _, packet_id)
|
|
64
|
+
@events << {
|
|
65
|
+
type: :unsubscribe,
|
|
66
|
+
packet_id: packet_id,
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# def on_log(ptr, _, status, string)
|
|
71
|
+
|
|
72
|
+
# end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/mosq/ffi.rb
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
|
|
2
|
+
require 'ffi'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
module Mosq
|
|
6
|
+
|
|
7
|
+
# Bindings and wrappers for the native functions and structures exposed by
|
|
8
|
+
# the libmosquitto C library. This module is for internal use only so that
|
|
9
|
+
# all dependencies on the implementation of the C library are abstracted.
|
|
10
|
+
# @api private
|
|
11
|
+
module FFI
|
|
12
|
+
extend ::FFI::Library
|
|
13
|
+
|
|
14
|
+
libfile = "libmosquitto.#{::FFI::Platform::LIBSUFFIX}"
|
|
15
|
+
|
|
16
|
+
ffi_lib ::FFI::Library::LIBC
|
|
17
|
+
ffi_lib \
|
|
18
|
+
File.expand_path("../../ext/mosq/#{libfile}", File.dirname(__FILE__))
|
|
19
|
+
|
|
20
|
+
opts = {
|
|
21
|
+
blocking: true # only necessary on MRI to deal with the GIL.
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
attach_function :free, [:pointer], :void, **opts
|
|
25
|
+
attach_function :malloc, [:size_t], :pointer, **opts
|
|
26
|
+
|
|
27
|
+
class Boolean
|
|
28
|
+
extend ::FFI::DataConverter
|
|
29
|
+
native_type ::FFI::TypeDefs[:int]
|
|
30
|
+
def self.to_native val, ctx; val ? 1 : 0; end
|
|
31
|
+
def self.from_native val, ctx; val != 0; end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Message < ::FFI::Struct
|
|
35
|
+
layout :mid, :int,
|
|
36
|
+
:topic, :pointer,
|
|
37
|
+
:payload, :pointer,
|
|
38
|
+
:payloadlen, :int,
|
|
39
|
+
:qos, :int,
|
|
40
|
+
:retain, Boolean
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Status = enum ::FFI::TypeDefs[:int], [
|
|
44
|
+
:conn_pending, -1,
|
|
45
|
+
:success, 0,
|
|
46
|
+
:nomem, 1,
|
|
47
|
+
:protocol, 2,
|
|
48
|
+
:inval, 3,
|
|
49
|
+
:no_conn, 4,
|
|
50
|
+
:conn_refused, 5,
|
|
51
|
+
:not_found, 6,
|
|
52
|
+
:conn_lost, 7,
|
|
53
|
+
:tls, 8,
|
|
54
|
+
:payload_size, 9,
|
|
55
|
+
:not_supported, 10,
|
|
56
|
+
:auth, 11,
|
|
57
|
+
:acl_denied, 12,
|
|
58
|
+
:unknown, 13,
|
|
59
|
+
:errno, 14,
|
|
60
|
+
:eai, 15,
|
|
61
|
+
:proxy, 16,
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
Option = enum [
|
|
65
|
+
:protocol_version, 1,
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
client = :pointer
|
|
69
|
+
|
|
70
|
+
callback :on_connect, [client, :pointer, :int], :void
|
|
71
|
+
callback :on_disconnect, [client, :pointer, :int], :void
|
|
72
|
+
callback :on_publish, [client, :pointer, :int], :void
|
|
73
|
+
callback :on_message, [client, :pointer, Message.ptr], :void
|
|
74
|
+
callback :on_subscribe, [client, :pointer, :int, :int, :pointer], :void
|
|
75
|
+
callback :on_unsubscribe, [client, :pointer, :int], :void
|
|
76
|
+
callback :on_log, [client, :pointer, :int, :string], :void
|
|
77
|
+
|
|
78
|
+
attach_function :mosquitto_lib_version, [:pointer, :pointer, :pointer], Status, **opts
|
|
79
|
+
attach_function :mosquitto_lib_init, [], Status, **opts
|
|
80
|
+
attach_function :mosquitto_lib_cleanup, [], Status, **opts
|
|
81
|
+
attach_function :mosquitto_new, [:string, Boolean, :pointer], client, **opts
|
|
82
|
+
attach_function :mosquitto_destroy, [client], :void, **opts
|
|
83
|
+
attach_function :mosquitto_reinitialise, [client, :string, Boolean, :pointer], Status, **opts
|
|
84
|
+
attach_function :mosquitto_will_set, [client, :string, :int, :pointer, :int, Boolean], Status, **opts
|
|
85
|
+
attach_function :mosquitto_will_clear, [client], Status, **opts
|
|
86
|
+
attach_function :mosquitto_username_pw_set, [client, :string, :string], Status, **opts
|
|
87
|
+
attach_function :mosquitto_connect, [client, :string, :int, :int], Status, **opts
|
|
88
|
+
attach_function :mosquitto_connect_bind, [client, :string, :int, :int, :string], Status, **opts
|
|
89
|
+
attach_function :mosquitto_connect_async, [client, :string, :int, :int], Status, **opts
|
|
90
|
+
attach_function :mosquitto_connect_bind_async, [client, :string, :int, :int, :string], Status, **opts
|
|
91
|
+
attach_function :mosquitto_connect_srv, [client, :string, :int, :string], Status, **opts
|
|
92
|
+
attach_function :mosquitto_reconnect, [client], Status, **opts
|
|
93
|
+
attach_function :mosquitto_reconnect_async, [client], Status, **opts
|
|
94
|
+
attach_function :mosquitto_disconnect, [client], Status, **opts
|
|
95
|
+
attach_function :mosquitto_publish, [client, :pointer, :string, :int, :pointer, :int, Boolean], Status, **opts
|
|
96
|
+
attach_function :mosquitto_subscribe, [client, :pointer, :string, :int], Status, **opts
|
|
97
|
+
attach_function :mosquitto_unsubscribe, [client, :pointer, :string], Status, **opts
|
|
98
|
+
attach_function :mosquitto_message_copy, [Message.ptr, Message.ptr], Status, **opts
|
|
99
|
+
attach_function :mosquitto_message_free, [:pointer], :void, **opts
|
|
100
|
+
attach_function :mosquitto_loop, [client, :int, :int], Status, **opts
|
|
101
|
+
attach_function :mosquitto_loop_forever, [client, :int, :int], Status, **opts
|
|
102
|
+
attach_function :mosquitto_loop_start, [client], Status, **opts
|
|
103
|
+
attach_function :mosquitto_loop_stop, [client, Boolean], Status, **opts
|
|
104
|
+
attach_function :mosquitto_socket, [client], :int, **opts
|
|
105
|
+
attach_function :mosquitto_loop_read, [client, :int], Status, **opts
|
|
106
|
+
attach_function :mosquitto_loop_write, [client, :int], Status, **opts
|
|
107
|
+
attach_function :mosquitto_loop_misc, [client], Status, **opts
|
|
108
|
+
attach_function :mosquitto_want_write, [client], :bool, **opts
|
|
109
|
+
attach_function :mosquitto_threaded_set, [client, Boolean], Status, **opts
|
|
110
|
+
attach_function :mosquitto_opts_set, [client, Option, :pointer], Status, **opts
|
|
111
|
+
attach_function :mosquitto_tls_set, [client], Status, **opts
|
|
112
|
+
attach_function :mosquitto_tls_insecure_set, [client, Boolean], Status, **opts
|
|
113
|
+
attach_function :mosquitto_tls_opts_set, [client, :int, :string, :string], Status, **opts
|
|
114
|
+
attach_function :mosquitto_tls_psk_set, [client, :string, :string, :string], Status, **opts
|
|
115
|
+
attach_function :mosquitto_connect_callback_set, [client, :on_connect], :void, **opts
|
|
116
|
+
attach_function :mosquitto_disconnect_callback_set, [client, :on_disconnect], :void, **opts
|
|
117
|
+
attach_function :mosquitto_publish_callback_set, [client, :on_publish], :void, **opts
|
|
118
|
+
attach_function :mosquitto_message_callback_set, [client, :on_message], :void, **opts
|
|
119
|
+
attach_function :mosquitto_subscribe_callback_set, [client, :on_subscribe], :void, **opts
|
|
120
|
+
attach_function :mosquitto_unsubscribe_callback_set, [client, :on_unsubscribe], :void, **opts
|
|
121
|
+
attach_function :mosquitto_log_callback_set, [client, :on_log], :void, **opts
|
|
122
|
+
attach_function :mosquitto_reconnect_delay_set, [client, :uint, :uint, Boolean], Status, **opts
|
|
123
|
+
attach_function :mosquitto_max_inflight_messages_set, [client, :uint], Status, **opts
|
|
124
|
+
attach_function :mosquitto_message_retry_set, [client, :uint], :void, **opts
|
|
125
|
+
attach_function :mosquitto_user_data_set, [client, :pointer], :void, **opts
|
|
126
|
+
attach_function :mosquitto_socks5_set, [client, :string, :int, :string, :string], Status, **opts
|
|
127
|
+
attach_function :mosquitto_strerror, [Status], :string, **opts
|
|
128
|
+
attach_function :mosquitto_connack_string, [:int], :string, **opts
|
|
129
|
+
attach_function :mosquitto_sub_topic_tokenise, [:string, :pointer, :pointer], Status, **opts
|
|
130
|
+
attach_function :mosquitto_sub_topic_tokens_free, [:pointer, :int], Status, **opts
|
|
131
|
+
attach_function :mosquitto_topic_matches_sub, [:string, :string, :pointer], Status, **opts
|
|
132
|
+
attach_function :mosquitto_pub_topic_check, [:string], Status, **opts
|
|
133
|
+
attach_function :mosquitto_sub_topic_check, [:string], Status, **opts
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
module Mosq
|
|
3
|
+
module FFI
|
|
4
|
+
|
|
5
|
+
class Error < RuntimeError
|
|
6
|
+
def initialize(message=nil)
|
|
7
|
+
@message = message
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def message
|
|
11
|
+
if @message && status_message; "#{status_message} - #{@message}"
|
|
12
|
+
elsif @message; @message
|
|
13
|
+
elsif status_message; status_message
|
|
14
|
+
else; ""
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def status_message
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.lookup status
|
|
23
|
+
if status == :errno
|
|
24
|
+
@errno_lookup_table.fetch(::FFI.errno)
|
|
25
|
+
else
|
|
26
|
+
@lookup_table.fetch(status)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@errno_lookup_table = {}
|
|
31
|
+
|
|
32
|
+
# Populate the errno_lookup_table
|
|
33
|
+
Errno.constants.each do |name|
|
|
34
|
+
kls = Errno.const_get(name)
|
|
35
|
+
begin
|
|
36
|
+
errno = kls.const_get(:Errno)
|
|
37
|
+
@errno_lookup_table[errno] = kls
|
|
38
|
+
rescue NoMethodError, NameError
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@lookup_table = {}
|
|
43
|
+
|
|
44
|
+
# Populate the FFI::Status lookup_table
|
|
45
|
+
(FFI::Status.symbols - [:errno]).each do |status|
|
|
46
|
+
message = FFI.mosquitto_strerror(status)
|
|
47
|
+
message.gsub!(/\.\s*\Z/, '')
|
|
48
|
+
kls = Class.new(Error) { define_method(:status_message) { message } }
|
|
49
|
+
@lookup_table[status] = kls
|
|
50
|
+
const_set Util.const_name(status), kls
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Custom static class to use for timeouts
|
|
54
|
+
Timeout = Class.new(Error) { define_method(:status_message) { "timed out" } }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/mosq/util.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
module Mosq
|
|
3
|
+
|
|
4
|
+
# Helper functions for this library.
|
|
5
|
+
# @api private
|
|
6
|
+
module Util
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def const_name(lowercase_name)
|
|
10
|
+
lowercase_name.to_s.gsub(/((?:\A\w)|(?:_\w))/) { |x| x[-1].upcase }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def error_check(action, status)
|
|
14
|
+
return if status == :success
|
|
15
|
+
raise Mosq::FFI::Error.lookup(status), "while #{action}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def null_check(action, obj)
|
|
19
|
+
return unless obj.nil?
|
|
20
|
+
raise Mosq::FFI::Error, "while #{action} - got unexpected null"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def mem_ptr(size, count: 1, clear: true, release: true)
|
|
24
|
+
ptr = ::FFI::MemoryPointer.new(size, count, clear)
|
|
25
|
+
ptr.autorelease = false unless release
|
|
26
|
+
ptr
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def strdup_ptr(str, **kwargs)
|
|
30
|
+
str = str + "\x00"
|
|
31
|
+
ptr = mem_ptr(str.bytesize, **kwargs)
|
|
32
|
+
ptr.write_string(str)
|
|
33
|
+
ptr
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def strdup_ary_ptr(ary, **kwargs)
|
|
37
|
+
ptr = mem_ptr(:pointer, count: ary.size)
|
|
38
|
+
ary.each_with_index do |str, i|
|
|
39
|
+
cursor = (ptr + i * ::FFI::TypeDefs[:pointer].size)
|
|
40
|
+
cursor.write_pointer(strdup_ptr(str, **kwargs))
|
|
41
|
+
end
|
|
42
|
+
ptr
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def connection_info(uri=nil, **overrides)
|
|
46
|
+
info = {
|
|
47
|
+
ssl: false,
|
|
48
|
+
host: "localhost",
|
|
49
|
+
port: 1883,
|
|
50
|
+
}
|
|
51
|
+
if uri
|
|
52
|
+
# TODO: support IPv6
|
|
53
|
+
pattern = %r{\A(?<schema>mqtts?)://((?<username>[^:]+):(?<password>[^@]+)@)?(?<host>[^:]+)(:(?<port>\d+))?\Z}
|
|
54
|
+
match = pattern.match(uri)
|
|
55
|
+
if match
|
|
56
|
+
info[:ssl] = ("mqtts" == match[:schema])
|
|
57
|
+
info[:host] = match[:host]
|
|
58
|
+
info[:port] = match[:port] ? Integer(match[:port]) : (info[:ssl] ? 8883 : 1883)
|
|
59
|
+
info[:username] = match[:username] if match[:username]
|
|
60
|
+
info[:password] = match[:password] if match[:password]
|
|
61
|
+
else
|
|
62
|
+
info[:host] = uri
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
info.merge(overrides)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mosq
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Joe McIlvain
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-08-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: ffi
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.9'
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 1.9.8
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '1.9'
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.9.8
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: bundler
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.6'
|
|
40
|
+
type: :development
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.6'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: rake
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '10.3'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '10.3'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: pry
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.9'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.9'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: rspec
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.0'
|
|
89
|
+
- !ruby/object:Gem::Dependency
|
|
90
|
+
name: rspec-its
|
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.0'
|
|
96
|
+
type: :development
|
|
97
|
+
prerelease: false
|
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1.0'
|
|
103
|
+
- !ruby/object:Gem::Dependency
|
|
104
|
+
name: fivemat
|
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1.3'
|
|
110
|
+
type: :development
|
|
111
|
+
prerelease: false
|
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '1.3'
|
|
117
|
+
description: A Ruby MQTT client library based on FFI bindings for libmosquitto.
|
|
118
|
+
email: joe.eli.mac@gmail.com
|
|
119
|
+
executables: []
|
|
120
|
+
extensions:
|
|
121
|
+
- ext/mosq/Rakefile
|
|
122
|
+
extra_rdoc_files: []
|
|
123
|
+
files:
|
|
124
|
+
- LICENSE
|
|
125
|
+
- README.md
|
|
126
|
+
- ext/mosq/Rakefile
|
|
127
|
+
- lib/mosq.rb
|
|
128
|
+
- lib/mosq/client.rb
|
|
129
|
+
- lib/mosq/client/bucket.rb
|
|
130
|
+
- lib/mosq/ffi.rb
|
|
131
|
+
- lib/mosq/ffi/error.rb
|
|
132
|
+
- lib/mosq/util.rb
|
|
133
|
+
homepage: https://github.com/jemc/ruby-mosq
|
|
134
|
+
licenses:
|
|
135
|
+
- MIT
|
|
136
|
+
metadata: {}
|
|
137
|
+
post_install_message:
|
|
138
|
+
rdoc_options: []
|
|
139
|
+
require_paths:
|
|
140
|
+
- lib
|
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '0'
|
|
151
|
+
requirements: []
|
|
152
|
+
rubyforge_project:
|
|
153
|
+
rubygems_version: 2.2.2
|
|
154
|
+
signing_key:
|
|
155
|
+
specification_version: 4
|
|
156
|
+
summary: mosq
|
|
157
|
+
test_files: []
|
|
158
|
+
has_rdoc:
|