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.
@@ -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