go-mqtt 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.md +21 -0
- data/NEWS.md +150 -0
- data/README.md +237 -0
- data/lib/mqtt/client.rb +755 -0
- data/lib/mqtt/openssl_fix.rb +29 -0
- data/lib/mqtt/packet.rb +1049 -0
- data/lib/mqtt/proxy.rb +115 -0
- data/lib/mqtt/sn/packet.rb +720 -0
- data/lib/mqtt/version.rb +4 -0
- data/lib/mqtt.rb +48 -0
- data/spec/mqtt_client_spec.rb +1124 -0
- data/spec/mqtt_packet_spec.rb +2012 -0
- data/spec/mqtt_proxy_spec.rb +8 -0
- data/spec/mqtt_sn_packet_spec.rb +1721 -0
- data/spec/mqtt_version_spec.rb +23 -0
- data/spec/zz_client_integration_spec.rb +177 -0
- metadata +152 -0
data/lib/mqtt/client.rb
ADDED
@@ -0,0 +1,755 @@
|
|
1
|
+
autoload :OpenSSL, 'openssl'
|
2
|
+
autoload :URI, 'uri'
|
3
|
+
autoload :CGI, 'cgi'
|
4
|
+
|
5
|
+
# Client class for talking to an MQTT server
|
6
|
+
module MQTT
|
7
|
+
class Client
|
8
|
+
# Hostname of the remote server
|
9
|
+
attr_accessor :host
|
10
|
+
|
11
|
+
# Port number of the remote server
|
12
|
+
attr_accessor :port
|
13
|
+
|
14
|
+
# The version number of the MQTT protocol to use (default 3.1.1)
|
15
|
+
attr_accessor :version
|
16
|
+
|
17
|
+
# Set to true to enable SSL/TLS encrypted communication
|
18
|
+
#
|
19
|
+
# Set to a symbol to use a specific variant of SSL/TLS.
|
20
|
+
# Allowed values include:
|
21
|
+
#
|
22
|
+
# @example Using TLS 1.0
|
23
|
+
# client = Client.new('mqtt.example.com', :ssl => :TLSv1)
|
24
|
+
# @see OpenSSL::SSL::SSLContext::METHODS
|
25
|
+
attr_accessor :ssl
|
26
|
+
|
27
|
+
# Set to false to skip tls hostname verification
|
28
|
+
attr_accessor :verify_host
|
29
|
+
|
30
|
+
# Time (in seconds) between pings to remote server (default is 15 seconds)
|
31
|
+
attr_accessor :keep_alive
|
32
|
+
|
33
|
+
# Set the 'Clean Session' flag when connecting? (default is true)
|
34
|
+
attr_accessor :clean_session
|
35
|
+
|
36
|
+
# Client Identifier
|
37
|
+
attr_accessor :client_id
|
38
|
+
|
39
|
+
# Number of seconds to wait for acknowledgement packets (default is 5 seconds)
|
40
|
+
attr_accessor :ack_timeout
|
41
|
+
|
42
|
+
# Number of seconds to connect to the server (default is 90 seconds)
|
43
|
+
attr_accessor :connect_timeout
|
44
|
+
|
45
|
+
# Username to authenticate to the server with
|
46
|
+
attr_accessor :username
|
47
|
+
|
48
|
+
# Password to authenticate to the server with
|
49
|
+
attr_accessor :password
|
50
|
+
|
51
|
+
# The topic that the Will message is published to
|
52
|
+
attr_accessor :will_topic
|
53
|
+
|
54
|
+
# Contents of message that is sent by server when client disconnect
|
55
|
+
attr_accessor :will_payload
|
56
|
+
|
57
|
+
# The QoS level of the will message sent by the server
|
58
|
+
attr_accessor :will_qos
|
59
|
+
|
60
|
+
# If the Will message should be retain by the server after it is sent
|
61
|
+
attr_accessor :will_retain
|
62
|
+
|
63
|
+
# Last ping response time
|
64
|
+
attr_reader :last_ping_response
|
65
|
+
|
66
|
+
# Timeout between select polls (in seconds)
|
67
|
+
SELECT_TIMEOUT = 0.5
|
68
|
+
|
69
|
+
# Default attribute values
|
70
|
+
ATTR_DEFAULTS = {
|
71
|
+
:host => nil,
|
72
|
+
:port => nil,
|
73
|
+
:version => '3.1.1',
|
74
|
+
:keep_alive => 15,
|
75
|
+
:clean_session => true,
|
76
|
+
:client_id => nil,
|
77
|
+
:ack_timeout => 5,
|
78
|
+
:connect_timeout => 90,
|
79
|
+
:username => nil,
|
80
|
+
:password => nil,
|
81
|
+
:will_topic => nil,
|
82
|
+
:will_payload => nil,
|
83
|
+
:will_qos => 0,
|
84
|
+
:will_retain => false,
|
85
|
+
:ssl => false,
|
86
|
+
:verify_host => true
|
87
|
+
}
|
88
|
+
|
89
|
+
# Create and connect a new MQTT Client
|
90
|
+
#
|
91
|
+
# Accepts the same arguments as creating a new client.
|
92
|
+
# If a block is given, then it will be executed before disconnecting again.
|
93
|
+
#
|
94
|
+
# Example:
|
95
|
+
# MQTT::Client.connect('myserver.example.com') do |client|
|
96
|
+
# # do stuff here
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
def self.connect(*args, &block)
|
100
|
+
client = MQTT::Client.new(*args)
|
101
|
+
client.connect(&block)
|
102
|
+
client
|
103
|
+
end
|
104
|
+
|
105
|
+
# Generate a random client identifier
|
106
|
+
# (using the characters 0-9 and a-z)
|
107
|
+
def self.generate_client_id(prefix = 'ruby', length = 16)
|
108
|
+
str = prefix.dup
|
109
|
+
length.times do
|
110
|
+
num = rand(36)
|
111
|
+
# Adjust based on number or letter.
|
112
|
+
num += num < 10 ? 48 : 87
|
113
|
+
str += num.chr
|
114
|
+
end
|
115
|
+
str
|
116
|
+
end
|
117
|
+
|
118
|
+
# Create a new MQTT Client instance
|
119
|
+
#
|
120
|
+
# Accepts one of the following:
|
121
|
+
# - a URI that uses the MQTT scheme
|
122
|
+
# - a hostname and port
|
123
|
+
# - a Hash containing attributes to be set on the new instance
|
124
|
+
#
|
125
|
+
# If no arguments are given then the method will look for a URI
|
126
|
+
# in the MQTT_SERVER environment variable.
|
127
|
+
#
|
128
|
+
# Examples:
|
129
|
+
# client = MQTT::Client.new
|
130
|
+
# client = MQTT::Client.new('mqtt://myserver.example.com')
|
131
|
+
# client = MQTT::Client.new('mqtt://user:pass@myserver.example.com')
|
132
|
+
# client = MQTT::Client.new('myserver.example.com')
|
133
|
+
# client = MQTT::Client.new('myserver.example.com', 18830)
|
134
|
+
# client = MQTT::Client.new(:host => 'myserver.example.com')
|
135
|
+
# client = MQTT::Client.new(:host => 'myserver.example.com', :keep_alive => 30)
|
136
|
+
#
|
137
|
+
def initialize(*args)
|
138
|
+
attributes = args.last.is_a?(Hash) ? args.pop : {}
|
139
|
+
|
140
|
+
# Set server URI from environment if present
|
141
|
+
attributes.merge!(parse_uri(ENV['MQTT_SERVER'])) if args.length.zero? && ENV['MQTT_SERVER']
|
142
|
+
|
143
|
+
if args.length >= 1
|
144
|
+
case args[0]
|
145
|
+
when URI
|
146
|
+
attributes.merge!(parse_uri(args[0]))
|
147
|
+
when %r{^mqtts?://}
|
148
|
+
attributes.merge!(parse_uri(args[0]))
|
149
|
+
else
|
150
|
+
attributes[:host] = args[0]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
if args.length >= 2
|
155
|
+
attributes[:port] = args[1] unless args[1].nil?
|
156
|
+
end
|
157
|
+
|
158
|
+
raise ArgumentError, 'Unsupported number of arguments' if args.length >= 3
|
159
|
+
|
160
|
+
# Merge arguments with default values for attributes
|
161
|
+
ATTR_DEFAULTS.merge(attributes).each_pair do |k, v|
|
162
|
+
send("#{k}=", v)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Set a default port number
|
166
|
+
if @port.nil?
|
167
|
+
@port = @ssl ? MQTT::DEFAULT_SSL_PORT : MQTT::DEFAULT_PORT
|
168
|
+
end
|
169
|
+
|
170
|
+
if @ssl
|
171
|
+
require 'openssl'
|
172
|
+
require 'mqtt/openssl_fix'
|
173
|
+
end
|
174
|
+
|
175
|
+
# Initialise private instance variables
|
176
|
+
@last_ping_request = current_time
|
177
|
+
@last_ping_response = current_time
|
178
|
+
@socket = nil
|
179
|
+
@read_queue = Queue.new
|
180
|
+
@pubacks = {}
|
181
|
+
@pubrecs = {} # For QoS 2 PUBREC packets
|
182
|
+
@pubrels = {} # For QoS 2 PUBREL packets
|
183
|
+
@pubcomps = {} # For QoS 2 PUBCOMP packets
|
184
|
+
@read_thread = nil
|
185
|
+
@write_semaphore = Mutex.new
|
186
|
+
@pubacks_semaphore = Mutex.new
|
187
|
+
@message_id = 0
|
188
|
+
end
|
189
|
+
|
190
|
+
# Get the OpenSSL context, that is used if SSL/TLS is enabled
|
191
|
+
def ssl_context
|
192
|
+
@ssl_context ||= OpenSSL::SSL::SSLContext.new
|
193
|
+
end
|
194
|
+
|
195
|
+
# Set a path to a file containing a PEM-format client certificate
|
196
|
+
def cert_file=(path)
|
197
|
+
self.cert = File.read(path)
|
198
|
+
end
|
199
|
+
|
200
|
+
# PEM-format client certificate
|
201
|
+
def cert=(cert)
|
202
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(cert)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Set a path to a file containing a PEM-format client private key
|
206
|
+
def key_file=(*args)
|
207
|
+
path, passphrase = args.flatten
|
208
|
+
ssl_context.key = OpenSSL::PKey.read(File.binread(path), passphrase)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Set to a PEM-format client private key
|
212
|
+
def key=(*args)
|
213
|
+
cert, passphrase = args.flatten
|
214
|
+
ssl_context.key = OpenSSL::PKey.read(cert, passphrase)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Set a path to a file containing a PEM-format CA certificate and enable peer verification
|
218
|
+
def ca_file=(path)
|
219
|
+
ssl_context.ca_file = path
|
220
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless path.nil?
|
221
|
+
end
|
222
|
+
|
223
|
+
# Set the Will for the client
|
224
|
+
#
|
225
|
+
# The will is a message that will be delivered by the server when the client dies.
|
226
|
+
# The Will must be set before establishing a connection to the server
|
227
|
+
def set_will(topic, payload, retain = false, qos = 0)
|
228
|
+
self.will_topic = topic
|
229
|
+
self.will_payload = payload
|
230
|
+
self.will_retain = retain
|
231
|
+
self.will_qos = qos
|
232
|
+
end
|
233
|
+
|
234
|
+
# Connect to the MQTT server
|
235
|
+
# If a block is given, then yield to that block and then disconnect again.
|
236
|
+
def connect(clientid = nil)
|
237
|
+
@client_id = clientid unless clientid.nil?
|
238
|
+
|
239
|
+
if @client_id.nil? || @client_id.empty?
|
240
|
+
raise 'Must provide a client_id if clean_session is set to false' unless @clean_session
|
241
|
+
|
242
|
+
# Empty client id is not allowed for version 3.1.0
|
243
|
+
@client_id = MQTT::Client.generate_client_id if @version == '3.1.0'
|
244
|
+
end
|
245
|
+
|
246
|
+
raise 'No MQTT server host set when attempting to connect' if @host.nil?
|
247
|
+
|
248
|
+
unless connected?
|
249
|
+
# Create network socket
|
250
|
+
tcp_socket = open_tcp_socket
|
251
|
+
|
252
|
+
if @ssl
|
253
|
+
# Set the protocol version
|
254
|
+
ssl_context.ssl_version = @ssl if @ssl.is_a?(Symbol)
|
255
|
+
|
256
|
+
@socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
257
|
+
@socket.sync_close = true
|
258
|
+
|
259
|
+
# Set hostname on secure socket for Server Name Indication (SNI)
|
260
|
+
@socket.hostname = @host if @socket.respond_to?(:hostname=)
|
261
|
+
|
262
|
+
@socket.connect
|
263
|
+
|
264
|
+
@socket.post_connection_check(@host) if @verify_host
|
265
|
+
else
|
266
|
+
@socket = tcp_socket
|
267
|
+
end
|
268
|
+
|
269
|
+
# Construct a connect packet
|
270
|
+
packet = MQTT::Packet::Connect.new(
|
271
|
+
:version => @version,
|
272
|
+
:clean_session => @clean_session,
|
273
|
+
:keep_alive => @keep_alive,
|
274
|
+
:client_id => @client_id,
|
275
|
+
:username => @username,
|
276
|
+
:password => @password,
|
277
|
+
:will_topic => @will_topic,
|
278
|
+
:will_payload => @will_payload,
|
279
|
+
:will_qos => @will_qos,
|
280
|
+
:will_retain => @will_retain
|
281
|
+
)
|
282
|
+
|
283
|
+
# Send packet
|
284
|
+
send_packet(packet)
|
285
|
+
|
286
|
+
# Receive response
|
287
|
+
receive_connack
|
288
|
+
|
289
|
+
# Start packet reading thread
|
290
|
+
@read_thread = Thread.new(Thread.current) do |parent|
|
291
|
+
Thread.current[:parent] = parent
|
292
|
+
receive_packet while connected?
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
return unless block_given?
|
297
|
+
|
298
|
+
# If a block is given, then yield and disconnect
|
299
|
+
begin
|
300
|
+
yield(self)
|
301
|
+
ensure
|
302
|
+
disconnect
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Disconnect from the MQTT server.
|
307
|
+
# If you don't want to say goodbye to the server, set send_msg to false.
|
308
|
+
def disconnect(send_msg = true)
|
309
|
+
# Stop reading packets from the socket first
|
310
|
+
@read_thread.kill if @read_thread && @read_thread.alive?
|
311
|
+
@read_thread = nil
|
312
|
+
|
313
|
+
return unless connected?
|
314
|
+
|
315
|
+
# Close the socket if it is open
|
316
|
+
if send_msg
|
317
|
+
packet = MQTT::Packet::Disconnect.new
|
318
|
+
send_packet(packet)
|
319
|
+
end
|
320
|
+
@socket.close unless @socket.nil?
|
321
|
+
handle_close
|
322
|
+
@socket = nil
|
323
|
+
end
|
324
|
+
|
325
|
+
# Checks whether the client is connected to the server.
|
326
|
+
def connected?
|
327
|
+
!@socket.nil? && !@socket.closed?
|
328
|
+
end
|
329
|
+
|
330
|
+
# Publish a message on a particular topic to the MQTT server.
|
331
|
+
def publish(topic, payload = '', retain = false, qos = 0)
|
332
|
+
raise ArgumentError, 'Topic name cannot be nil' if topic.nil?
|
333
|
+
raise ArgumentError, 'Topic name cannot be empty' if topic.empty?
|
334
|
+
raise ArgumentError, 'QoS should be 0, 1 or 2' unless (0..2).include?(qos)
|
335
|
+
|
336
|
+
packet = MQTT::Packet::Publish.new(
|
337
|
+
:id => next_packet_id,
|
338
|
+
:qos => qos,
|
339
|
+
:retain => retain,
|
340
|
+
:topic => topic,
|
341
|
+
:payload => payload
|
342
|
+
)
|
343
|
+
|
344
|
+
case qos
|
345
|
+
when 0
|
346
|
+
send_packet(packet)
|
347
|
+
when 1
|
348
|
+
queue = wait_for_puback(packet.id)
|
349
|
+
send_packet(packet)
|
350
|
+
wait_for_ack(queue, packet.id, @pubacks)
|
351
|
+
when 2
|
352
|
+
queue = wait_for_pubrec(packet.id)
|
353
|
+
send_packet(packet)
|
354
|
+
|
355
|
+
if wait_for_ack(queue, packet.id, @pubrecs)
|
356
|
+
# Send PUBREL
|
357
|
+
pubrel = MQTT::Packet::Pubrel.new(:id => packet.id)
|
358
|
+
queue = wait_for_pubcomp(packet.id)
|
359
|
+
send_packet(pubrel)
|
360
|
+
wait_for_ack(queue, packet.id, @pubcomps)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
# Send a subscribe message for one or more topics on the MQTT server.
|
366
|
+
# The topics parameter should be one of the following:
|
367
|
+
# * String: subscribe to one topic with QoS 0
|
368
|
+
# * Array: subscribe to multiple topics with QoS 0
|
369
|
+
# * Hash: subscribe to multiple topics where the key is the topic and the value is the QoS level
|
370
|
+
#
|
371
|
+
# For example:
|
372
|
+
# client.subscribe( 'a/b' )
|
373
|
+
# client.subscribe( 'a/b', 'c/d' )
|
374
|
+
# client.subscribe( ['a/b',0], ['c/d',1] )
|
375
|
+
# client.subscribe( 'a/b' => 0, 'c/d' => 1 )
|
376
|
+
#
|
377
|
+
def subscribe(*topics)
|
378
|
+
packet = MQTT::Packet::Subscribe.new(
|
379
|
+
:id => next_packet_id,
|
380
|
+
:topics => topics
|
381
|
+
)
|
382
|
+
send_packet(packet)
|
383
|
+
end
|
384
|
+
|
385
|
+
# Return the next message received from the MQTT server.
|
386
|
+
# An optional topic can be given to subscribe to.
|
387
|
+
#
|
388
|
+
# The method either returns the topic and message as an array:
|
389
|
+
# topic,message = client.get
|
390
|
+
#
|
391
|
+
# Or can be used with a block to keep processing messages:
|
392
|
+
# client.get('test') do |topic,payload|
|
393
|
+
# # Do stuff here
|
394
|
+
# end
|
395
|
+
#
|
396
|
+
def get(topic = nil, options = {})
|
397
|
+
if block_given?
|
398
|
+
get_packet(topic) do |packet|
|
399
|
+
yield(packet.topic, packet.payload) unless packet.retain && options[:omit_retained]
|
400
|
+
end
|
401
|
+
else
|
402
|
+
loop do
|
403
|
+
# Wait for one packet to be available
|
404
|
+
packet = get_packet(topic)
|
405
|
+
return packet.topic, packet.payload unless packet.retain && options[:omit_retained]
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Return the next packet object received from the MQTT server.
|
411
|
+
# An optional topic can be given to subscribe to.
|
412
|
+
#
|
413
|
+
# The method either returns a single packet:
|
414
|
+
# packet = client.get_packet
|
415
|
+
# puts packet.topic
|
416
|
+
#
|
417
|
+
# Or can be used with a block to keep processing messages:
|
418
|
+
# client.get_packet('test') do |packet|
|
419
|
+
# # Do stuff here
|
420
|
+
# puts packet.topic
|
421
|
+
# end
|
422
|
+
#
|
423
|
+
def get_packet(topic = nil)
|
424
|
+
# Subscribe to a topic, if an argument is given
|
425
|
+
subscribe(topic) unless topic.nil?
|
426
|
+
|
427
|
+
if block_given?
|
428
|
+
# Loop forever!
|
429
|
+
loop do
|
430
|
+
packet = @read_queue.pop
|
431
|
+
yield(packet)
|
432
|
+
case packet.qos
|
433
|
+
when 1
|
434
|
+
puback_packet(packet)
|
435
|
+
when 2
|
436
|
+
pubrec = MQTT::Packet::Pubrec.new(:id => packet.id)
|
437
|
+
send_packet(pubrec)
|
438
|
+
# Wait for PUBREL before delivering
|
439
|
+
queue = wait_for_pubrel(packet.id)
|
440
|
+
if wait_for_ack(queue, packet.id, @pubrels)
|
441
|
+
# Send PUBCOMP
|
442
|
+
pubcomp = MQTT::Packet::Pubcomp.new(:id => packet.id)
|
443
|
+
send_packet(pubcomp)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
447
|
+
else
|
448
|
+
# Wait for one packet to be available
|
449
|
+
packet = @read_queue.pop
|
450
|
+
case packet.qos
|
451
|
+
when 1
|
452
|
+
puback_packet(packet)
|
453
|
+
when 2
|
454
|
+
pubrec = MQTT::Packet::Pubrec.new(:id => packet.id)
|
455
|
+
send_packet(pubrec)
|
456
|
+
# Wait for PUBREL before delivering
|
457
|
+
queue = wait_for_pubrel(packet.id)
|
458
|
+
if wait_for_ack(queue, packet.id, @pubrels)
|
459
|
+
# Send PUBCOMP
|
460
|
+
pubcomp = MQTT::Packet::Pubcomp.new(:id => packet.id)
|
461
|
+
send_packet(pubcomp)
|
462
|
+
end
|
463
|
+
end
|
464
|
+
return packet
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Returns true if the incoming message queue is empty.
|
469
|
+
def queue_empty?
|
470
|
+
@read_queue.empty?
|
471
|
+
end
|
472
|
+
|
473
|
+
# Returns the length of the incoming message queue.
|
474
|
+
def queue_length
|
475
|
+
@read_queue.length
|
476
|
+
end
|
477
|
+
|
478
|
+
# Clear the incoming message queue.
|
479
|
+
def clear_queue
|
480
|
+
@read_queue.clear
|
481
|
+
end
|
482
|
+
|
483
|
+
# Send a unsubscribe message for one or more topics on the MQTT server
|
484
|
+
def unsubscribe(*topics)
|
485
|
+
topics = topics.first if topics.is_a?(Enumerable) && topics.count == 1
|
486
|
+
|
487
|
+
packet = MQTT::Packet::Unsubscribe.new(
|
488
|
+
:topics => topics,
|
489
|
+
:id => next_packet_id
|
490
|
+
)
|
491
|
+
send_packet(packet)
|
492
|
+
end
|
493
|
+
|
494
|
+
private
|
495
|
+
|
496
|
+
# Try to read a packet from the server
|
497
|
+
# Also sends keep-alive ping packets.
|
498
|
+
def receive_packet
|
499
|
+
# Poll socket - is there data waiting?
|
500
|
+
|
501
|
+
return unless @socket && !@socket.closed? && @socket.respond_to?(:fileno)
|
502
|
+
result = IO.select([@socket], [], [], SELECT_TIMEOUT)
|
503
|
+
handle_timeouts
|
504
|
+
unless result.nil?
|
505
|
+
# Yes - read in the packet
|
506
|
+
packet = MQTT::Packet.read(@socket)
|
507
|
+
handle_packet packet
|
508
|
+
end
|
509
|
+
keep_alive!
|
510
|
+
# Pass exceptions up to parent thread
|
511
|
+
rescue ::Exception => exp
|
512
|
+
unless @socket.nil?
|
513
|
+
@socket.close
|
514
|
+
@socket = nil
|
515
|
+
handle_close
|
516
|
+
end
|
517
|
+
Thread.current[:parent].raise(exp)
|
518
|
+
end
|
519
|
+
|
520
|
+
def wait_for_puback(id)
|
521
|
+
@pubacks_semaphore.synchronize do
|
522
|
+
@pubacks[id] = Queue.new
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
def wait_for_pubrec(id)
|
527
|
+
@pubacks_semaphore.synchronize do
|
528
|
+
@pubrecs[id] = Queue.new
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def wait_for_pubrel(id)
|
533
|
+
@pubacks_semaphore.synchronize do
|
534
|
+
@pubrels[id] = Queue.new
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
def wait_for_pubcomp(id)
|
539
|
+
@pubacks_semaphore.synchronize do
|
540
|
+
@pubcomps[id] = Queue.new
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
def wait_for_ack(queue, packet_id, ack_hash)
|
545
|
+
deadline = current_time + @ack_timeout
|
546
|
+
|
547
|
+
begin
|
548
|
+
Timeout.timeout(@ack_timeout) do
|
549
|
+
loop do
|
550
|
+
response = begin
|
551
|
+
queue.pop(true)
|
552
|
+
rescue StandardError
|
553
|
+
nil
|
554
|
+
end
|
555
|
+
case response
|
556
|
+
when :read_timeout
|
557
|
+
return false if current_time > deadline
|
558
|
+
when :close
|
559
|
+
return false
|
560
|
+
when nil
|
561
|
+
# No response yet, sleep briefly and try again
|
562
|
+
sleep(0.01)
|
563
|
+
return false if current_time > deadline
|
564
|
+
else
|
565
|
+
@pubacks_semaphore.synchronize do
|
566
|
+
ack_hash.delete packet_id
|
567
|
+
end
|
568
|
+
return true
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
rescue Timeout::Error
|
573
|
+
return false
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
def handle_packet(packet)
|
578
|
+
case packet
|
579
|
+
when MQTT::Packet::Publish
|
580
|
+
if packet.qos == 2
|
581
|
+
# Send PUBREC
|
582
|
+
pubrec = MQTT::Packet::Pubrec.new(:id => packet.id)
|
583
|
+
send_packet(pubrec)
|
584
|
+
|
585
|
+
# Store the packet for later delivery
|
586
|
+
@read_queue.push(packet)
|
587
|
+
else
|
588
|
+
# QoS 0 or 1
|
589
|
+
@read_queue.push(packet)
|
590
|
+
# Send PUBACK for QoS 1
|
591
|
+
puback_packet(packet) if packet.qos == 1
|
592
|
+
end
|
593
|
+
when MQTT::Packet::Puback
|
594
|
+
@pubacks_semaphore.synchronize do
|
595
|
+
@pubacks[packet.id] << packet if @pubacks[packet.id]
|
596
|
+
end
|
597
|
+
when MQTT::Packet::Pubrec
|
598
|
+
@pubacks_semaphore.synchronize do
|
599
|
+
@pubrecs[packet.id] << packet if @pubrecs[packet.id]
|
600
|
+
end
|
601
|
+
when MQTT::Packet::Pubrel
|
602
|
+
@pubacks_semaphore.synchronize do
|
603
|
+
@pubrels[packet.id] << packet if @pubrels[packet.id]
|
604
|
+
end
|
605
|
+
when MQTT::Packet::Pubcomp
|
606
|
+
@pubacks_semaphore.synchronize do
|
607
|
+
@pubcomps[packet.id] << packet if @pubcomps[packet.id]
|
608
|
+
end
|
609
|
+
when MQTT::Packet::Pingresp
|
610
|
+
@last_ping_response = current_time
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
def handle_timeouts
|
615
|
+
@pubacks_semaphore.synchronize do
|
616
|
+
@pubacks.each_value { |q| q << :read_timeout }
|
617
|
+
@pubrecs.each_value { |q| q << :read_timeout }
|
618
|
+
@pubrels.each_value { |q| q << :read_timeout }
|
619
|
+
@pubcomps.each_value { |q| q << :read_timeout }
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
def handle_close
|
624
|
+
@pubacks_semaphore.synchronize do
|
625
|
+
@pubacks.each_value { |q| q << :close }
|
626
|
+
@pubrecs.each_value { |q| q << :close }
|
627
|
+
@pubrels.each_value { |q| q << :close }
|
628
|
+
@pubcomps.each_value { |q| q << :close }
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
if Process.const_defined? :CLOCK_MONOTONIC
|
633
|
+
def current_time
|
634
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
635
|
+
end
|
636
|
+
else
|
637
|
+
# Support older Ruby
|
638
|
+
def current_time
|
639
|
+
Time.now.to_f
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
def keep_alive!
|
644
|
+
return unless @keep_alive > 0 && connected?
|
645
|
+
|
646
|
+
response_timeout = (@keep_alive * 1.5).ceil
|
647
|
+
if current_time >= @last_ping_request + @keep_alive
|
648
|
+
packet = MQTT::Packet::Pingreq.new
|
649
|
+
send_packet(packet)
|
650
|
+
@last_ping_request = current_time
|
651
|
+
elsif current_time > @last_ping_response + response_timeout
|
652
|
+
raise MQTT::ProtocolException, "No Ping Response received for #{response_timeout} seconds"
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
def puback_packet(packet)
|
657
|
+
send_packet(MQTT::Packet::Puback.new(:id => packet.id))
|
658
|
+
end
|
659
|
+
|
660
|
+
# Read and check a connection acknowledgement packet
|
661
|
+
def receive_connack
|
662
|
+
Timeout.timeout(@ack_timeout) do
|
663
|
+
packet = MQTT::Packet.read(@socket)
|
664
|
+
if packet.class != MQTT::Packet::Connack
|
665
|
+
raise MQTT::ProtocolException, "Response wasn't a connection acknowledgement: #{packet.class}"
|
666
|
+
end
|
667
|
+
|
668
|
+
# Check the return code
|
669
|
+
if packet.return_code != 0x00
|
670
|
+
# 3.2.2.3 If a server sends a CONNACK packet containing a non-zero
|
671
|
+
# return code it MUST then close the Network Connection
|
672
|
+
@socket.close
|
673
|
+
raise MQTT::ProtocolException, packet.return_msg
|
674
|
+
end
|
675
|
+
rescue Timeout::Error => e
|
676
|
+
@socket.close unless @socket.nil?
|
677
|
+
@socket = nil
|
678
|
+
handle_close
|
679
|
+
raise e
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
# Send a packet to server
|
684
|
+
def send_packet(data)
|
685
|
+
# Raise exception if we aren't connected
|
686
|
+
raise MQTT::NotConnectedException unless connected?
|
687
|
+
|
688
|
+
# Only allow one thread to write to socket at a time
|
689
|
+
@write_semaphore.synchronize do
|
690
|
+
@socket.write(data.to_s)
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
def parse_uri(uri)
|
695
|
+
uri = URI.parse(uri) unless uri.is_a?(URI)
|
696
|
+
if uri.scheme == 'mqtt'
|
697
|
+
ssl = false
|
698
|
+
elsif uri.scheme == 'mqtts'
|
699
|
+
ssl = true
|
700
|
+
else
|
701
|
+
raise 'Only the mqtt:// and mqtts:// schemes are supported'
|
702
|
+
end
|
703
|
+
|
704
|
+
{
|
705
|
+
:host => uri.host,
|
706
|
+
:port => uri.port || nil,
|
707
|
+
:username => uri.user ? CGI.unescape(uri.user) : nil,
|
708
|
+
:password => uri.password ? CGI.unescape(uri.password) : nil,
|
709
|
+
:ssl => ssl
|
710
|
+
}
|
711
|
+
end
|
712
|
+
|
713
|
+
def next_packet_id
|
714
|
+
@last_packet_id = (@last_packet_id || 0).next
|
715
|
+
@last_packet_id = 1 if @last_packet_id > 0xffff
|
716
|
+
@last_packet_id
|
717
|
+
end
|
718
|
+
|
719
|
+
def open_tcp_socket
|
720
|
+
return TCPSocket.new @host, @port, connect_timeout: @connect_timeout if RUBY_VERSION.to_f >= 3.0
|
721
|
+
|
722
|
+
begin
|
723
|
+
Timeout.timeout(@connect_timeout) do
|
724
|
+
return TCPSocket.new(@host, @port)
|
725
|
+
end
|
726
|
+
rescue Timeout::Error
|
727
|
+
raise IO::TimeoutError, "Connection timed out for \"#{@host}\" port #{@port}" if defined? IO::TimeoutError
|
728
|
+
raise Errno::ETIMEDOUT, "Connection timed out for \"#{@host}\" port #{@port}"
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
# ---- Deprecated attributes and methods ---- #
|
733
|
+
public
|
734
|
+
|
735
|
+
# @deprecated Please use {#host} instead
|
736
|
+
def remote_host
|
737
|
+
host
|
738
|
+
end
|
739
|
+
|
740
|
+
# @deprecated Please use {#host=} instead
|
741
|
+
def remote_host=(args)
|
742
|
+
self.host = args
|
743
|
+
end
|
744
|
+
|
745
|
+
# @deprecated Please use {#port} instead
|
746
|
+
def remote_port
|
747
|
+
port
|
748
|
+
end
|
749
|
+
|
750
|
+
# @deprecated Please use {#port=} instead
|
751
|
+
def remote_port=(args)
|
752
|
+
self.port = args
|
753
|
+
end
|
754
|
+
end
|
755
|
+
end
|