paho-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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/paho-mqtt.rb +7 -0
- data/lib/paho.mqtt/packet_manager.rb +1056 -0
- data/lib/paho.mqtt/paho_client.rb +888 -0
- data/lib/paho.mqtt/version.rb +3 -0
- data/paho-mqtt.gemspec +33 -0
- data/samples/test_aws.rb +31 -0
- data/samples/test_init.rb +64 -0
- metadata +103 -0
@@ -0,0 +1,888 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module PahoMqtt
|
5
|
+
DEFAULT_SSL_PORT = 8883
|
6
|
+
DEFAULT_PORT = 1883
|
7
|
+
SELECT_TIMEOUT = 0
|
8
|
+
LOOP_TEMPO = 0.005
|
9
|
+
RECONNECT_RETRY_TIME = 3
|
10
|
+
RECONNECT_RETRY_TEMPO = 5
|
11
|
+
|
12
|
+
class Client
|
13
|
+
# MAX size of queue
|
14
|
+
MAX_PUBACK = 20
|
15
|
+
MAX_PUBREC = 20
|
16
|
+
MAX_PUBREL = 20
|
17
|
+
MAX_PUBCOMP = 20
|
18
|
+
MAX_WRITING = MAX_PUBACK + MAX_PUBREC + MAX_PUBREL + MAX_PUBCOMP
|
19
|
+
|
20
|
+
# Connection states values
|
21
|
+
MQTT_CS_NEW = 0
|
22
|
+
MQTT_CS_CONNECTED = 1
|
23
|
+
MQTT_CS_DISCONNECT = 2
|
24
|
+
MQTT_CS_CONNECT_ASYNC = 3
|
25
|
+
|
26
|
+
# Error values
|
27
|
+
MQTT_ERR_AGAIN = -1
|
28
|
+
MQTT_ERR_SUCCESS = 0
|
29
|
+
MQTT_ERR_NOMEM = 1
|
30
|
+
MQTT_ERR_PROTOCOL = 2
|
31
|
+
MQTT_ERR_INVAL = 3
|
32
|
+
MQTT_ERR_NO_CONN = 4
|
33
|
+
MQTT_ERR_CONN_REFUSED = 5
|
34
|
+
MQTT_ERR_NOT_FOUND = 6
|
35
|
+
MQTT_ERR_CONN_LOST = 7
|
36
|
+
MQTT_ERR_TLS = 8
|
37
|
+
MQTT_ERR_PAYLOAD_SIZE = 9
|
38
|
+
MQTT_ERR_NOT_SUPPORTED = 10
|
39
|
+
MQTT_ERR_AUTH = 11
|
40
|
+
MQTT_ERR_ACL_DENIED = 12
|
41
|
+
MQTT_ERR_UNKNOWN = 13
|
42
|
+
MQTT_ERR_ERRNO = 14
|
43
|
+
|
44
|
+
# Connection related attributes:
|
45
|
+
attr_accessor :host
|
46
|
+
attr_accessor :port
|
47
|
+
attr_accessor :mqtt_version
|
48
|
+
attr_accessor :clean_session
|
49
|
+
attr_accessor :client_id
|
50
|
+
attr_accessor :username
|
51
|
+
attr_accessor :password
|
52
|
+
attr_accessor :ssl
|
53
|
+
|
54
|
+
# Last will attributes:
|
55
|
+
attr_accessor :will_topic
|
56
|
+
attr_accessor :will_payload
|
57
|
+
attr_accessor :will_qos
|
58
|
+
attr_accessor :will_retain
|
59
|
+
|
60
|
+
# Setting attributes:
|
61
|
+
attr_accessor :keep_alive
|
62
|
+
attr_accessor :ack_timeout
|
63
|
+
attr_accessor :persistent
|
64
|
+
|
65
|
+
#Callback attributes
|
66
|
+
attr_accessor :on_message
|
67
|
+
attr_accessor :on_connack
|
68
|
+
attr_accessor :on_suback
|
69
|
+
attr_accessor :on_unsuback
|
70
|
+
attr_accessor :on_puback
|
71
|
+
attr_accessor :on_pubrel
|
72
|
+
attr_accessor :on_pubrec
|
73
|
+
attr_accessor :on_pubcomp
|
74
|
+
|
75
|
+
#Read Only attribute
|
76
|
+
attr_reader :registered_callback
|
77
|
+
attr_reader :subscribed_topics
|
78
|
+
attr_reader :connection_state
|
79
|
+
|
80
|
+
ATTR_DEFAULTS = {
|
81
|
+
:host => "",
|
82
|
+
:port => nil,
|
83
|
+
:mqtt_version => '3.1.1',
|
84
|
+
:clean_session => true,
|
85
|
+
:persistent => false,
|
86
|
+
:client_id => nil,
|
87
|
+
:username => nil,
|
88
|
+
:password => nil,
|
89
|
+
:ssl => false,
|
90
|
+
:will_topic => nil,
|
91
|
+
:will_payload => nil,
|
92
|
+
:will_qos => 0,
|
93
|
+
:will_retain => false,
|
94
|
+
:keep_alive => 10,
|
95
|
+
:ack_timeout => 5,
|
96
|
+
:on_connack => nil,
|
97
|
+
:on_suback => nil,
|
98
|
+
:on_unsuback => nil,
|
99
|
+
:on_puback => nil,
|
100
|
+
:on_pubrel => nil,
|
101
|
+
:on_pubrec => nil,
|
102
|
+
:on_pubcomp => nil,
|
103
|
+
:on_message => nil,
|
104
|
+
}
|
105
|
+
|
106
|
+
def initialize(*args)
|
107
|
+
if args.last.is_a?(Hash)
|
108
|
+
attr = args.pop
|
109
|
+
else
|
110
|
+
attr = {}
|
111
|
+
end
|
112
|
+
|
113
|
+
ATTR_DEFAULTS.merge(attr).each_pair do |k,v|
|
114
|
+
self.send("#{k}=", v)
|
115
|
+
end
|
116
|
+
|
117
|
+
if @port.nil?
|
118
|
+
@port = @ssl ? PahoMqtt::DEFAULT_SSL_PORT : PahoMqtt::DEFAULT_PORT
|
119
|
+
end
|
120
|
+
|
121
|
+
if @client_id.nil? || @client_id == ""
|
122
|
+
@client_id = generate_client_id
|
123
|
+
end
|
124
|
+
|
125
|
+
@last_ping_req = Time.now
|
126
|
+
@last_ping_resp = Time.now
|
127
|
+
@last_packet_id = 0
|
128
|
+
@socket = nil
|
129
|
+
@ssl_context = nil
|
130
|
+
@writing_mutex = Mutex.new
|
131
|
+
@writing_queue = []
|
132
|
+
@connection_state = MQTT_CS_DISCONNECT
|
133
|
+
@connection_state_mutex = Mutex.new
|
134
|
+
@subscribed_mutex = Mutex.new
|
135
|
+
@subscribed_topics = []
|
136
|
+
@registered_callback = []
|
137
|
+
@waiting_suback = []
|
138
|
+
@suback_mutex = Mutex.new
|
139
|
+
@waiting_unsuback = []
|
140
|
+
@unsuback_mutex = Mutex.new
|
141
|
+
@mqtt_thread = nil
|
142
|
+
@reconnect_thread = nil
|
143
|
+
|
144
|
+
@puback_mutex = Mutex.new
|
145
|
+
@pubrec_mutex = Mutex.new
|
146
|
+
@pubrel_mutex = Mutex.new
|
147
|
+
@pubcomp_mutex = Mutex.new
|
148
|
+
@waiting_puback = []
|
149
|
+
@waiting_pubrec = []
|
150
|
+
@waiting_pubrel = []
|
151
|
+
@waiting_pubcomp = []
|
152
|
+
end
|
153
|
+
|
154
|
+
def generate_client_id(prefix='paho_ruby', lenght=16)
|
155
|
+
charset = Array('A'..'Z') + Array('a'..'z') + Array('0'..'9')
|
156
|
+
@client_id = prefix << Array.new(lenght) { charset.sample }.join
|
157
|
+
end
|
158
|
+
|
159
|
+
def next_packet_id
|
160
|
+
@last_packet_id = ( @last_packet_id || 0 ).next
|
161
|
+
end
|
162
|
+
|
163
|
+
def config_ssl_context(cert_path, key_path, ca_path=nil)
|
164
|
+
@ssl ||= true
|
165
|
+
@ssl_context = ssl_context
|
166
|
+
self.cert = cert_path
|
167
|
+
self.key = key_path
|
168
|
+
self.root_ca = ca_path
|
169
|
+
end
|
170
|
+
|
171
|
+
def config_socket
|
172
|
+
unless @socket.nil?
|
173
|
+
@socket.close
|
174
|
+
@socket = nil
|
175
|
+
end
|
176
|
+
|
177
|
+
unless @host.nil? || @port < 0
|
178
|
+
tcp_socket = TCPSocket.new(@host, @port)
|
179
|
+
end
|
180
|
+
|
181
|
+
if @ssl
|
182
|
+
unless @ssl_context.nil?
|
183
|
+
@socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
184
|
+
@socket.sync_close = true
|
185
|
+
@socket.connect
|
186
|
+
else
|
187
|
+
raise "SSL context should be defined and set to open SSLSocket"
|
188
|
+
end
|
189
|
+
else
|
190
|
+
@socket = tcp_socket
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def ssl_context
|
195
|
+
@ssl_context ||= OpenSSL::SSL::SSLContext.new
|
196
|
+
end
|
197
|
+
|
198
|
+
def cert=(cert_path)
|
199
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
|
200
|
+
end
|
201
|
+
|
202
|
+
def key=(key_path, passphrase=nil)
|
203
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(File.read(key_path), passphrase)
|
204
|
+
end
|
205
|
+
|
206
|
+
def root_ca=(ca_path)
|
207
|
+
ssl_context.ca_file = ca_path
|
208
|
+
unless @ca_path.nil?
|
209
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def config_will(topic, payload="", retain=false, qos=0)
|
214
|
+
@will_topic = topic
|
215
|
+
@will_payload = payload
|
216
|
+
@will_retain = retain
|
217
|
+
@will_qos = qos
|
218
|
+
end
|
219
|
+
|
220
|
+
def connect(host=@host, port=@port, keep_alive=@keep_alive, persistent=@persistent)
|
221
|
+
@persistent = persistent
|
222
|
+
@connection_state_mutex.synchronize {
|
223
|
+
@connection_state = MQTT_CS_NEW
|
224
|
+
}
|
225
|
+
connect_async(host, port, keep_alive)
|
226
|
+
end
|
227
|
+
|
228
|
+
def connect_async(host, port=1883, keep_alive)
|
229
|
+
@host = host
|
230
|
+
@port = port.to_i
|
231
|
+
@keep_alive = keep_alive
|
232
|
+
|
233
|
+
@connection_state_mutex.synchronize {
|
234
|
+
@connection_state = MQTT_CS_CONNECT_ASYNC
|
235
|
+
}
|
236
|
+
setup_connection
|
237
|
+
end
|
238
|
+
|
239
|
+
def setup_connection
|
240
|
+
@mqtt_thread.kill unless @mqtt_thread.nil?
|
241
|
+
if @host.nil? || @host == ""
|
242
|
+
raise "Connection Failed, host cannot be nil or empty"
|
243
|
+
end
|
244
|
+
|
245
|
+
if @port.to_i <= 0
|
246
|
+
raise "Connection Failed port cannot be 0 >="
|
247
|
+
end
|
248
|
+
|
249
|
+
@socket.close unless @socket.nil?
|
250
|
+
@socket = nil
|
251
|
+
|
252
|
+
@last_ping_req = Time.now
|
253
|
+
@last_ping_resp = Time.now
|
254
|
+
|
255
|
+
# TODO => MOVE TO LOGGER
|
256
|
+
# puts "Try to connect to #{@host}"
|
257
|
+
config_socket
|
258
|
+
send_connect
|
259
|
+
|
260
|
+
# Waiting a Connack packet for "ack_timeout" second from the remote
|
261
|
+
connect_timeout = Time.now + @ack_timeout
|
262
|
+
while (Time.now <= connect_timeout) && (@connection_state != MQTT_CS_CONNECTED) do
|
263
|
+
receive_packet
|
264
|
+
end
|
265
|
+
|
266
|
+
if @connection_state != MQTT_CS_CONNECTED
|
267
|
+
# TODO => MOVE TO LOGGER
|
268
|
+
# puts "Didn't receive Connack answer from server #{@host}"
|
269
|
+
else
|
270
|
+
config_subscription
|
271
|
+
config_all_message_queue
|
272
|
+
@mqtt_thread = Thread.new do
|
273
|
+
@reconnect_thread.kill unless @reconnect_thread.nil? || !@reconnect_thread.alive?
|
274
|
+
while @connection_state == MQTT_CS_CONNECTED do
|
275
|
+
mqtt_loop
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def loop_write(max_packet=MAX_WRITING)
|
282
|
+
@writing_mutex.synchronize {
|
283
|
+
cnt = 0
|
284
|
+
while !@writing_queue.empty? && cnt < max_packet do
|
285
|
+
send_packet(@writing_queue.shift)
|
286
|
+
cnt += 1
|
287
|
+
end
|
288
|
+
}
|
289
|
+
end
|
290
|
+
|
291
|
+
def loop_read(max_packet=5)
|
292
|
+
max_packet.times do
|
293
|
+
receive_packet
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def mqtt_loop
|
298
|
+
loop_read
|
299
|
+
loop_write
|
300
|
+
loop_misc
|
301
|
+
sleep LOOP_TEMPO
|
302
|
+
end
|
303
|
+
|
304
|
+
def loop_misc
|
305
|
+
check_keep_alive
|
306
|
+
check_ack_alive(@waiting_puback, @puback_mutex, MAX_PUBACK)
|
307
|
+
check_ack_alive(@waiting_pubrec, @pubrec_mutex, MAX_PUBREC)
|
308
|
+
check_ack_alive(@waiting_pubrel, @pubrel_mutex, MAX_PUBREL)
|
309
|
+
check_ack_alive(@waiting_pubcomp, @pubcomp_mutex, MAX_PUBCOMP)
|
310
|
+
check_ack_alive(@waiting_suback, @suback_mutex, @waiting_suback.length)
|
311
|
+
check_ack_alive(@waiting_unsuback, @unsuback_mutex, @waiting_unsuback.length)
|
312
|
+
end
|
313
|
+
|
314
|
+
def check_keep_alive
|
315
|
+
if @keep_alive >= 0 && @connection_state == MQTT_CS_CONNECTED
|
316
|
+
now = Time.now
|
317
|
+
timeout_req = (@last_ping_req + (@keep_alive * 0.7).ceil)
|
318
|
+
|
319
|
+
if timeout_req <= now && @persistent
|
320
|
+
send_pingreq
|
321
|
+
@last_ping_req = now
|
322
|
+
end
|
323
|
+
|
324
|
+
timeout_resp = @last_ping_resp + (@keep_alive * 1.1).ceil
|
325
|
+
if timeout_resp <= now
|
326
|
+
# TODO => MOVE TO LOGGER
|
327
|
+
#puts "Didn't get answer from server for a long time, trying to reconnect."
|
328
|
+
disconnect(false)
|
329
|
+
reconnect(RECONNECT_RETRY_TIME, RECONNECT_RETRY_TEMPO) if @persistent
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def reconnect(retry_time=3, retry_tempo=3)
|
335
|
+
@reconnect_thread = Thread.new do
|
336
|
+
retry_time.times do
|
337
|
+
# TODO => MOVE TO LOGGER
|
338
|
+
#puts "Retrying to connect"
|
339
|
+
setup_connection
|
340
|
+
if @connection_state == MQTT_CS_CONNECTED
|
341
|
+
break
|
342
|
+
else
|
343
|
+
sleep retry_tempo
|
344
|
+
end
|
345
|
+
end
|
346
|
+
raise "Reconnection retry counter is over (#{RECONNECT_RETRY_TIME}), could not reconnect to the server."
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def check_ack_alive(queue, mutex, max_packet)
|
351
|
+
mutex.synchronize {
|
352
|
+
now = Time.now
|
353
|
+
cnt = 0
|
354
|
+
queue.each do |pck|
|
355
|
+
if now >= pck[:timestamp] + @ack_timeout
|
356
|
+
pck[:packet].dup ||= true unless pck[:packet].class == PahoMqtt::Packet::Subscribe || pck[:packet].class == PahoMqtt::Packet::Unsubscribe
|
357
|
+
unless cnt > max_packet
|
358
|
+
append_to_writing(pck[:packet])
|
359
|
+
pck[:timestamp] = now
|
360
|
+
cnt += 1
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
}
|
365
|
+
end
|
366
|
+
|
367
|
+
def append_to_writing(packet)
|
368
|
+
@writing_mutex.synchronize {
|
369
|
+
@writing_queue.push(packet)
|
370
|
+
}
|
371
|
+
MQTT_ERR_SUCCESS
|
372
|
+
end
|
373
|
+
|
374
|
+
def config_subscription
|
375
|
+
unless @subscribed_topics == []
|
376
|
+
new_id = next_packet_id
|
377
|
+
packet = PahoMqtt::Packet::Subscribe.new(
|
378
|
+
:id => new_id,
|
379
|
+
:topics => @subscribed_topics
|
380
|
+
)
|
381
|
+
@subscribed_mutex.synchronize {
|
382
|
+
@subscribed_topics = []
|
383
|
+
}
|
384
|
+
@suback_mutex.synchronize {
|
385
|
+
@waiting_suback.push({ :id => new_id, :packet => packet, :timestamp => Time.now })
|
386
|
+
}
|
387
|
+
send_packet(packet)
|
388
|
+
end
|
389
|
+
MQTT_ERR_SUCCESS
|
390
|
+
end
|
391
|
+
|
392
|
+
def config_all_message_queue
|
393
|
+
config_message_queue(@waiting_puback, @puback_mutex, MAX_PUBACK)
|
394
|
+
config_message_queue(@waiting_pubrec, @pubrec_mutex, MAX_PUBREC)
|
395
|
+
config_message_queue(@waiting_pubrel, @pubrel_mutex, MAX_PUBREL)
|
396
|
+
config_message_queue(@waiting_pubcomp, @pubcomp_mutex, MAX_PUBCOMP)
|
397
|
+
end
|
398
|
+
|
399
|
+
def config_message_queue(queue, mutex, max_packet)
|
400
|
+
mutex.synchronize {
|
401
|
+
cnt = 0
|
402
|
+
queue.each do |pck|
|
403
|
+
pck[:packet].dup ||= true
|
404
|
+
if cnt <= max_packet
|
405
|
+
append_to_writing(pck)
|
406
|
+
cnt += 1
|
407
|
+
end
|
408
|
+
end
|
409
|
+
}
|
410
|
+
end
|
411
|
+
|
412
|
+
def disconnect(explicit=true)
|
413
|
+
# TODO => MOVE TO LOGGER
|
414
|
+
# puts "Disconnecting"
|
415
|
+
|
416
|
+
if explicit
|
417
|
+
send_disconnect
|
418
|
+
@mqtt_thread.kill if @mqtt_thread && @mqtt_thread.alive?
|
419
|
+
@mqtt_thread.kill if @mqtt_thread.alive?
|
420
|
+
|
421
|
+
@socket.close unless @socket.nil?
|
422
|
+
@socket = nil
|
423
|
+
end
|
424
|
+
|
425
|
+
@connection_state_mutex.synchronize {
|
426
|
+
@connection_state = MQTT_CS_DISCONNECT
|
427
|
+
}
|
428
|
+
|
429
|
+
@writing_mutex.synchronize {
|
430
|
+
@writing_queue = []
|
431
|
+
}
|
432
|
+
|
433
|
+
@puback_mutex.synchronize {
|
434
|
+
@waiting_puback = []
|
435
|
+
}
|
436
|
+
|
437
|
+
@pubrec_mutex.synchronize {
|
438
|
+
@waiting_pubrec = []
|
439
|
+
}
|
440
|
+
|
441
|
+
@pubrel_mutex.synchronize {
|
442
|
+
@waiting_pubrel = []
|
443
|
+
}
|
444
|
+
|
445
|
+
@pubcomp_mutex.synchronize {
|
446
|
+
@waiting_pubcomp = []
|
447
|
+
}
|
448
|
+
|
449
|
+
@last_packet_id = 0
|
450
|
+
MQTT_ERR_SUCCESS
|
451
|
+
end
|
452
|
+
|
453
|
+
def publish(topic, payload="", retain=false, qos=0)
|
454
|
+
if topic == "" || !topic.is_a?(String)
|
455
|
+
raise "Publish error, topic is empty or invalid"
|
456
|
+
end
|
457
|
+
send_publish(topic, payload, retain, qos)
|
458
|
+
end
|
459
|
+
|
460
|
+
def subscribe(*topics)
|
461
|
+
unless topics.length == 0
|
462
|
+
send_subscribe(topics)
|
463
|
+
else
|
464
|
+
raise "Protocol Violation, subscribe topics list must not be empty."
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
def unsubscribe(topics)
|
469
|
+
unless topics.length == 0
|
470
|
+
send_unsubscribe(topics)
|
471
|
+
else
|
472
|
+
raise "Protocol Violation, unsubscribe topics list must not be empty."
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
private
|
477
|
+
|
478
|
+
def receive_packet
|
479
|
+
begin
|
480
|
+
result = IO.select([@socket], [], [], SELECT_TIMEOUT)
|
481
|
+
unless result.nil?
|
482
|
+
packet = PahoMqtt::Packet.read(@socket)
|
483
|
+
unless packet.nil?
|
484
|
+
handle_packet packet
|
485
|
+
@last_ping_resp = Time.now
|
486
|
+
end
|
487
|
+
end
|
488
|
+
rescue Exception => exp
|
489
|
+
unless @socket.nil?
|
490
|
+
@socket.close
|
491
|
+
@socket = nil
|
492
|
+
end
|
493
|
+
raise(exp)
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
def handle_packet(packet)
|
498
|
+
if packet.class == PahoMqtt::Packet::Connack
|
499
|
+
handle_connack(packet)
|
500
|
+
elsif packet.class == PahoMqtt::Packet::Suback
|
501
|
+
handle_suback(packet)
|
502
|
+
elsif packet.class == PahoMqtt::Packet::Unsuback
|
503
|
+
handle_unsuback(packet)
|
504
|
+
elsif packet.class == PahoMqtt::Packet::Publish
|
505
|
+
handle_publish(packet)
|
506
|
+
elsif packet.class == PahoMqtt::Packet::Puback
|
507
|
+
handle_puback(packet)
|
508
|
+
elsif packet.class == PahoMqtt::Packet::Pubrec
|
509
|
+
handle_pubrec(packet)
|
510
|
+
elsif packet.class == PahoMqtt::Packet::Pubrel
|
511
|
+
handle_pubrel(packet)
|
512
|
+
elsif packet.class == PahoMqtt::Packet::Pubcomp
|
513
|
+
handle_pubcomp(packet)
|
514
|
+
elsif packet.class ==PahoMqtt::Packet::Pingresp
|
515
|
+
handle_pingresp
|
516
|
+
else
|
517
|
+
raise "Unknow packet received"
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
def handle_connack(packet)
|
522
|
+
if packet.return_code == 0x00
|
523
|
+
# TODO => MOVE TO LOGGER
|
524
|
+
# puts "Connection accepted, ready to process"
|
525
|
+
if @clean_session && !packet.session_present
|
526
|
+
# puts "New session created"
|
527
|
+
elsif !@clean_session && !packet.session_present
|
528
|
+
# puts "Could not find session on server side, starting a new one."
|
529
|
+
elsif !@clean_session && packet.session_present
|
530
|
+
# puts "Retrieving previous session on server side."
|
531
|
+
end
|
532
|
+
@connection_state_mutex.synchronize{
|
533
|
+
@connection_state = MQTT_CS_CONNECTED
|
534
|
+
}
|
535
|
+
else
|
536
|
+
handle_connack_error(packet.return_code)
|
537
|
+
end
|
538
|
+
config_all_message_queue
|
539
|
+
@writing_mutex.synchronize {
|
540
|
+
@writing_queue.each do |m|
|
541
|
+
send_packet(m)
|
542
|
+
end
|
543
|
+
}
|
544
|
+
@on_connack.call unless @on_connack.nil?
|
545
|
+
end
|
546
|
+
|
547
|
+
def handle_pingresp
|
548
|
+
@last_ping_resp = Time.now
|
549
|
+
end
|
550
|
+
|
551
|
+
def handle_suback(packet)
|
552
|
+
adjust_qos = []
|
553
|
+
max_qos = packet.return_codes
|
554
|
+
@suback_mutex.synchronize {
|
555
|
+
adjust_qos, @waiting_suback = @waiting_suback.partition { |pck| pck[:id] == packet.id }
|
556
|
+
}
|
557
|
+
if adjust_qos.length == 1
|
558
|
+
adjust_qos = adjust_qos.first[:packet].topics
|
559
|
+
adjust_qos.each do |t|
|
560
|
+
if [0, 1, 2].include?(max_qos[0])
|
561
|
+
t[1] = max_qos.shift
|
562
|
+
elsif max_qos[0] == 128
|
563
|
+
adjust_qos.delete(t)
|
564
|
+
else
|
565
|
+
raise "Invalid qos value used."
|
566
|
+
end
|
567
|
+
end
|
568
|
+
else
|
569
|
+
raise "Two packet subscribe packet cannot have the same id"
|
570
|
+
end
|
571
|
+
@subscribed_mutex.synchronize {
|
572
|
+
@subscribed_topics.concat(adjust_qos)
|
573
|
+
}
|
574
|
+
@on_suback.call unless @on_suback.nil?
|
575
|
+
end
|
576
|
+
|
577
|
+
def handle_unsuback(packet)
|
578
|
+
to_unsub = nil
|
579
|
+
@unsuback_mutex.synchronize {
|
580
|
+
to_unsub, @waiting_unsuback = @waiting_unsuback.partition { |pck| pck[:id] == packet.id }
|
581
|
+
}
|
582
|
+
|
583
|
+
if to_unsub.length == 1
|
584
|
+
to_unsub = to_unsub.first[:packet].topics
|
585
|
+
else
|
586
|
+
raise "Two packet unsubscribe cannot have the same id"
|
587
|
+
end
|
588
|
+
|
589
|
+
@subscribed_mutex.synchronize {
|
590
|
+
to_unsub.each do |filter|
|
591
|
+
@subscribed_topics.delete_if { |topic| match_filter(topic.first, filter) }
|
592
|
+
end
|
593
|
+
}
|
594
|
+
@on_unsuback.call unless @on_unsuback.nil?
|
595
|
+
end
|
596
|
+
|
597
|
+
def handle_publish(packet)
|
598
|
+
case packet.qos
|
599
|
+
when 0
|
600
|
+
when 1
|
601
|
+
send_puback(packet.id)
|
602
|
+
when 2
|
603
|
+
send_pubrec(packet.id)
|
604
|
+
else
|
605
|
+
raise "Unknow qos level for a publish packet"
|
606
|
+
end
|
607
|
+
|
608
|
+
@on_message.call(packet) unless @on_message.nil?
|
609
|
+
@registered_callback.assoc(packet.topic).last.call if @registered_callback.any? { |pair| pair.first == packet.topic}
|
610
|
+
end
|
611
|
+
|
612
|
+
def handle_puback(packet)
|
613
|
+
@puback_mutex.synchronize{
|
614
|
+
@waiting_puback.delete_if { |pck| pck[:id] == packet.id }
|
615
|
+
}
|
616
|
+
@on_puback.call unless @on_puback.nil?
|
617
|
+
end
|
618
|
+
|
619
|
+
def handle_pubrec(packet)
|
620
|
+
@pubrec_mutex.synchronize {
|
621
|
+
@waiting_pubrec.delete_if { |pck| pck[:id] == packet.id }
|
622
|
+
}
|
623
|
+
send_pubrel(packet.id)
|
624
|
+
@on_pubrec.call unless @on_pubrec.nil?
|
625
|
+
end
|
626
|
+
|
627
|
+
def handle_pubrel(packet)
|
628
|
+
@pubrel_mutex.synchronize {
|
629
|
+
@waiting_pubrel.delete_if { |pck| pck[:id] == packet.id }
|
630
|
+
}
|
631
|
+
send_pubcomp(packet.id)
|
632
|
+
@on_pubrel.call unless @on_pubrel.nil?
|
633
|
+
end
|
634
|
+
|
635
|
+
def handle_pubcomp(packet)
|
636
|
+
@pubcomp_mutex.synchronize {
|
637
|
+
@waiting_pubcomp.delete_if { |pck| pck[:id] == packet.id }
|
638
|
+
}
|
639
|
+
@on_pubcomp.call unless @on_pubcomp.nil?
|
640
|
+
end
|
641
|
+
|
642
|
+
### MOVE TO ERROR HANDLER CLASS
|
643
|
+
def handle_connack_error(return_code)
|
644
|
+
case return_code
|
645
|
+
when 0x01
|
646
|
+
# TODO => MOVE TO LOGGER
|
647
|
+
# puts "Unable to connect with this version #{@mqtt_version}"
|
648
|
+
if @mqtt_version == "3.1.1"
|
649
|
+
@mqtt_version = "3.1"
|
650
|
+
connect(@host, @port, @keep_alive)
|
651
|
+
end
|
652
|
+
when 0x02
|
653
|
+
|
654
|
+
when 0x03
|
655
|
+
|
656
|
+
when 0x04
|
657
|
+
|
658
|
+
when 0x05
|
659
|
+
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
def send_packet(packet)
|
664
|
+
@socket.write(packet.to_s)
|
665
|
+
@last_ping_req = Time.now
|
666
|
+
MQTT_ERR_SUCCESS
|
667
|
+
end
|
668
|
+
|
669
|
+
def send_connect
|
670
|
+
packet = PahoMqtt::Packet::Connect.new(
|
671
|
+
:version => @mqtt_version,
|
672
|
+
:clean_session => @clean_session,
|
673
|
+
:keep_alive => @keep_alive,
|
674
|
+
:client_id => @client_id,
|
675
|
+
:username => @username,
|
676
|
+
:password => @password,
|
677
|
+
:will_topic => @will_topic,
|
678
|
+
:will_payload => @will_payload,
|
679
|
+
:will_qos => @will_qos,
|
680
|
+
:will_retain => @will_retain
|
681
|
+
)
|
682
|
+
send_packet(packet)
|
683
|
+
end
|
684
|
+
|
685
|
+
def send_disconnect
|
686
|
+
packet = PahoMqtt::Packet::Disconnect.new
|
687
|
+
send_packet(packet)
|
688
|
+
end
|
689
|
+
|
690
|
+
def send_pingreq
|
691
|
+
packet = PahoMqtt::Packet::Pingreq.new
|
692
|
+
# TODO => MOVE TO LOGGER
|
693
|
+
# puts "Check if the connection is still alive."
|
694
|
+
send_packet(packet)
|
695
|
+
end
|
696
|
+
|
697
|
+
def send_subscribe(topics)
|
698
|
+
unless topics.length == 0
|
699
|
+
new_id = next_packet_id
|
700
|
+
packet = PahoMqtt::Packet::Subscribe.new(
|
701
|
+
:id => new_id,
|
702
|
+
:topics => topics
|
703
|
+
)
|
704
|
+
|
705
|
+
append_to_writing(packet)
|
706
|
+
@suback_mutex.synchronize {
|
707
|
+
@waiting_suback.push({ :id => new_id, :packet => packet, :timestamp => Time.now })
|
708
|
+
}
|
709
|
+
else
|
710
|
+
raise "Protocol Violation, subscribe topics list must not be empty."
|
711
|
+
end
|
712
|
+
MQTT_ERR_SUCCESS
|
713
|
+
end
|
714
|
+
|
715
|
+
def send_unsubscribe(topics)
|
716
|
+
unless topics.length == 0
|
717
|
+
new_id = next_packet_id
|
718
|
+
packet = PahoMqtt::Packet::Unsubscribe.new(
|
719
|
+
:id => new_id,
|
720
|
+
:topics => topics
|
721
|
+
)
|
722
|
+
|
723
|
+
append_to_writing(packet)
|
724
|
+
@unsuback_mutex.synchronize {
|
725
|
+
@waiting_unsuback.push({:id => new_id, :packet => packet, :timestamp => Time.now})
|
726
|
+
}
|
727
|
+
else
|
728
|
+
raise "Protocol Violation, unsubscribe topics list must not be empty."
|
729
|
+
end
|
730
|
+
MQTT_ERR_SUCCESS
|
731
|
+
end
|
732
|
+
|
733
|
+
def send_publish(topic, payload, retain, qos)
|
734
|
+
new_id = next_packet_id
|
735
|
+
packet = PahoMqtt::Packet::Publish.new(
|
736
|
+
:id => new_id,
|
737
|
+
:topic => topic,
|
738
|
+
:payload => payload,
|
739
|
+
:retain => retain,
|
740
|
+
:qos => qos
|
741
|
+
)
|
742
|
+
|
743
|
+
append_to_writing(packet)
|
744
|
+
|
745
|
+
case qos
|
746
|
+
when 1
|
747
|
+
@puback_mutex.synchronize{
|
748
|
+
@waiting_puback.push({:id => new_id, :packet => packet, :timestamp => Time.now})
|
749
|
+
}
|
750
|
+
when 2
|
751
|
+
@pubrec_mutex.synchronize{
|
752
|
+
@waiting_pubrec.push({:id => new_id, :packet => packet, :timestamp => Time.now})
|
753
|
+
}
|
754
|
+
end
|
755
|
+
MQTT_ERR_SUCCESS
|
756
|
+
end
|
757
|
+
|
758
|
+
def send_puback(packet_id)
|
759
|
+
packet = PahoMqtt::Packet::Puback.new(
|
760
|
+
:id => packet_id
|
761
|
+
)
|
762
|
+
|
763
|
+
append_to_writing(packet)
|
764
|
+
MQTT_ERR_SUCCESS
|
765
|
+
end
|
766
|
+
|
767
|
+
def send_pubrec(packet_id)
|
768
|
+
packet = PahoMqtt::Packet::Pubrec.new(
|
769
|
+
:id => packet_id
|
770
|
+
)
|
771
|
+
|
772
|
+
append_to_writing(packet)
|
773
|
+
|
774
|
+
@pubrel_mutex.synchronize{
|
775
|
+
@waiting_pubrel.push({:id => packet_id , :packet => packet, :timestamp => Time.now})
|
776
|
+
}
|
777
|
+
MQTT_ERR_SUCCESS
|
778
|
+
end
|
779
|
+
|
780
|
+
def send_pubrel(packet_id)
|
781
|
+
packet = PahoMqtt::Packet::Pubrel.new(
|
782
|
+
:id => packet_id
|
783
|
+
)
|
784
|
+
|
785
|
+
append_to_writing(packet)
|
786
|
+
|
787
|
+
@pubcomp_mutex.synchronize{
|
788
|
+
@waiting_pubcomp.push({:id => packet_id, :packet => packet, :timestamp => Time.now})
|
789
|
+
}
|
790
|
+
MQTT_ERR_SUCCESS
|
791
|
+
end
|
792
|
+
|
793
|
+
def send_pubcomp(packet_id)
|
794
|
+
packet = PahoMqtt::Packet::Pubcomp.new(
|
795
|
+
:id => packet_id
|
796
|
+
)
|
797
|
+
|
798
|
+
append_to_writing(packet)
|
799
|
+
MQTT_ERR_SUCCESS
|
800
|
+
end
|
801
|
+
|
802
|
+
def add_topic_callback(topic, callback=nil, &block)
|
803
|
+
raise "Trying to register a callback for an undefined topic" if topic.nil?
|
804
|
+
|
805
|
+
remove_topic_callback(topic)
|
806
|
+
|
807
|
+
if block_given?
|
808
|
+
@registered_callback.push([topic, block])
|
809
|
+
elsif !(callback.nil?) && callback.class == Proc
|
810
|
+
@registered_callback.push([topic, callback])
|
811
|
+
end
|
812
|
+
MQTT_ERR_SUCCESS
|
813
|
+
end
|
814
|
+
|
815
|
+
def remove_topic_callback(topic)
|
816
|
+
raise "Trying to unregister a callback for an undefined topic" if topic.nil?
|
817
|
+
|
818
|
+
@registered_callback.delete_if {|pair| pair.first == topic}
|
819
|
+
MQTT_ERR_SUCCESS
|
820
|
+
end
|
821
|
+
|
822
|
+
def on_connack(&block)
|
823
|
+
@on_connack = block if block_given?
|
824
|
+
@on_connack
|
825
|
+
end
|
826
|
+
|
827
|
+
def on_suback(&block)
|
828
|
+
@on_suback = block if block_given?
|
829
|
+
@on_suback
|
830
|
+
end
|
831
|
+
|
832
|
+
def on_unsuback(&block)
|
833
|
+
@on_unsuback = block if block_given?
|
834
|
+
@on_unsuback
|
835
|
+
end
|
836
|
+
|
837
|
+
def on_puback(&block)
|
838
|
+
@on_puback = block if block_given?
|
839
|
+
@on_puback
|
840
|
+
end
|
841
|
+
|
842
|
+
def on_pubrec(&block)
|
843
|
+
@on_pubrec = block if block_given?
|
844
|
+
@on_pubrec
|
845
|
+
end
|
846
|
+
|
847
|
+
def on_pubrel(&block)
|
848
|
+
@on_pubrel = block if block_given?
|
849
|
+
@on_pubrel
|
850
|
+
end
|
851
|
+
|
852
|
+
def on_pubcomp(&block)
|
853
|
+
@on_pubcomp = block if block_given?
|
854
|
+
@on_pubcomp
|
855
|
+
end
|
856
|
+
|
857
|
+
def on_message(&block)
|
858
|
+
@on_message = block if block_given?
|
859
|
+
@on_message
|
860
|
+
end
|
861
|
+
|
862
|
+
def match_filter(topics, filters)
|
863
|
+
if topics.is_a?(String) && filters.is_a?(String)
|
864
|
+
topic = topics.split('/')
|
865
|
+
filter = filters.split('/')
|
866
|
+
else
|
867
|
+
raise "Invalid parameter type #{topics.class} and #{filters.class}"
|
868
|
+
end
|
869
|
+
|
870
|
+
rc = false
|
871
|
+
index = 0
|
872
|
+
|
873
|
+
while index < [topic.length, filter.length].max do
|
874
|
+
if topic[index].nil? || filter[index].nil?
|
875
|
+
break
|
876
|
+
elsif filter[index] == '#' && index == (filter.length - 1)
|
877
|
+
rc = true
|
878
|
+
break
|
879
|
+
elsif filter[index] == topic[index] || filter[index] == '+'
|
880
|
+
index = index + 1
|
881
|
+
else
|
882
|
+
break
|
883
|
+
end
|
884
|
+
end
|
885
|
+
rc ||= (index == [topic.length, filter.length].max)
|
886
|
+
end
|
887
|
+
end
|
888
|
+
end
|