paho-mqtt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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