mosq 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://circleci.com/gh/jemc/ruby-mosq/tree/master.svg?style=svg)](https://circleci.com/gh/jemc/ruby-mosq/tree/master)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/mosq.png)](http://badge.fury.io/rb/mosq)
|
5
|
+
[![Join the chat at https://gitter.im/jemc/ruby-mosq](https://badges.gitter.im/Join%20Chat.svg)](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:
|