mqtt-ccutrer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,772 @@
1
+ # frozen_string_literal: true
2
+
3
+ autoload :OpenSSL, 'openssl'
4
+ autoload :SecureRandom, 'securerandom'
5
+ autoload :URI, 'uri'
6
+
7
+ # Client class for talking to an MQTT server
8
+ module MQTT
9
+ class Client
10
+ # Hostname of the remote server
11
+ attr_accessor :host
12
+
13
+ # Port number of the remote server
14
+ attr_accessor :port
15
+
16
+ # The version number of the MQTT protocol to use (default 3.1.1)
17
+ attr_accessor :version
18
+
19
+ # Set to true to enable SSL/TLS encrypted communication
20
+ #
21
+ # Set to a symbol to use a specific variant of SSL/TLS.
22
+ # Allowed values include:
23
+ #
24
+ # @example Using TLS 1.0
25
+ # client = Client.new('mqtt.example.com', ssl: :TLSv1)
26
+ # @see OpenSSL::SSL::SSLContext::METHODS
27
+ attr_accessor :ssl
28
+
29
+ # Time (in seconds) between pings to remote server (default is 15 seconds)
30
+ attr_accessor :keep_alive
31
+
32
+ # Set the 'Clean Session' flag when connecting? (default is true)
33
+ attr_accessor :clean_session
34
+
35
+ # Client Identifier
36
+ attr_accessor :client_id
37
+
38
+ # Number of seconds to wait for acknowledgement packets (default is 5 seconds)
39
+ attr_accessor :ack_timeout
40
+
41
+ # How many times to attempt re-sending packets that weren't acknowledged
42
+ # (default is 5) before giving up
43
+ attr_accessor :resend_limit
44
+
45
+ # How many attempts to re-establish a connection after it drops before
46
+ # giving up (default 5)
47
+ attr_accessor :reconnect_limit
48
+
49
+ # How long to wait between re-connection attempts (exponential - i.e.
50
+ # immediately after first drop, then 5s, then 25s, then 125s, etc.
51
+ # when theis value defaults to 5)
52
+ attr_accessor :reconnect_backoff
53
+
54
+ # Username to authenticate to the server with
55
+ attr_accessor :username
56
+
57
+ # Password to authenticate to the server with
58
+ attr_accessor :password
59
+
60
+ # The topic that the Will message is published to
61
+ attr_accessor :will_topic
62
+
63
+ # Contents of message that is sent by server when client disconnect
64
+ attr_accessor :will_payload
65
+
66
+ # The QoS level of the will message sent by the server
67
+ attr_accessor :will_qos
68
+
69
+ # If the Will message should be retain by the server after it is sent
70
+ attr_accessor :will_retain
71
+
72
+ # Default attribute values
73
+ ATTR_DEFAULTS = {
74
+ host: nil,
75
+ port: nil,
76
+ version: '3.1.1',
77
+ keep_alive: 15,
78
+ clean_session: true,
79
+ client_id: nil,
80
+ ack_timeout: 5,
81
+ resend_limit: 5,
82
+ reconnect_limit: 5,
83
+ reconnect_backoff: 5,
84
+ username: nil,
85
+ password: nil,
86
+ will_topic: nil,
87
+ will_payload: nil,
88
+ will_qos: 0,
89
+ will_retain: false,
90
+ ssl: false
91
+ }.freeze
92
+
93
+ # Create and connect a new MQTT Client
94
+ #
95
+ # Accepts the same arguments as creating a new client.
96
+ # If a block is given, then it will be executed before disconnecting again.
97
+ #
98
+ # Example:
99
+ # MQTT::Client.connect('myserver.example.com') do |client|
100
+ # # do stuff here
101
+ # end
102
+ #
103
+ def self.connect(*args, &block)
104
+ client = MQTT::Client.new(*args)
105
+ client.connect(&block)
106
+ client
107
+ end
108
+
109
+ # Generate a random client identifier
110
+ # (using the characters 0-9 and a-z)
111
+ def self.generate_client_id(prefix = 'ruby', length = 16)
112
+ "#{prefix}#{SecureRandom.alphanumeric(length).downcase}"
113
+ end
114
+
115
+ # Create a new MQTT Client instance
116
+ #
117
+ # Accepts one of the following:
118
+ # - a URI that uses the MQTT scheme
119
+ # - a hostname and port
120
+ # - a Hash containing attributes to be set on the new instance
121
+ #
122
+ # If no arguments are given then the method will look for a URI
123
+ # in the MQTT_SERVER environment variable.
124
+ #
125
+ # Examples:
126
+ # client = MQTT::Client.new
127
+ # client = MQTT::Client.new('mqtt://myserver.example.com')
128
+ # client = MQTT::Client.new('mqtt://user:pass@myserver.example.com')
129
+ # client = MQTT::Client.new('myserver.example.com')
130
+ # client = MQTT::Client.new('myserver.example.com', 18830)
131
+ # client = MQTT::Client.new(host: 'myserver.example.com')
132
+ # client = MQTT::Client.new(host: 'myserver.example.com', keep_alive: 30)
133
+ #
134
+ def initialize(host = nil, port = nil, **attributes)
135
+ # Set server URI from environment if present
136
+ if host.nil? && port.nil? && attributes.empty? && ENV['MQTT_SERVER']
137
+ attributes.merge!(parse_uri(ENV['MQTT_SERVER']))
138
+ end
139
+
140
+ if host
141
+ case host
142
+ when URI, %r{^mqtts?://}
143
+ attributes.merge!(parse_uri(host))
144
+ else
145
+ attributes[:host] = host
146
+ end
147
+ end
148
+
149
+ attributes[:port] = port unless port.nil?
150
+
151
+ # Merge arguments with default values for attributes
152
+ ATTR_DEFAULTS.merge(attributes).each_pair do |k, v|
153
+ send("#{k}=", v)
154
+ end
155
+
156
+ # Set a default port number
157
+ if @port.nil?
158
+ @port = @ssl ? MQTT::DEFAULT_SSL_PORT : MQTT::DEFAULT_PORT
159
+ end
160
+
161
+ # Initialise private instance variables
162
+ @socket = nil
163
+ @read_queue = Queue.new
164
+ @write_queue = Queue.new
165
+
166
+ @read_thread = nil
167
+ @write_thread = nil
168
+
169
+ @acks = {}
170
+
171
+ @connection_mutex = Mutex.new
172
+ @acks_mutex = Mutex.new
173
+ @wake_up_pipe = IO.pipe
174
+
175
+ @connected = false
176
+ end
177
+
178
+ # Get the OpenSSL context, that is used if SSL/TLS is enabled
179
+ def ssl_context
180
+ @ssl_context ||= OpenSSL::SSL::SSLContext.new
181
+ end
182
+
183
+ # Set a path to a file containing a PEM-format client certificate
184
+ def cert_file=(path)
185
+ self.cert = File.read(path)
186
+ end
187
+
188
+ # PEM-format client certificate
189
+ def cert=(cert)
190
+ ssl_context.cert = OpenSSL::X509::Certificate.new(cert)
191
+ end
192
+
193
+ # Set a path to a file containing a PEM-format client private key
194
+ def key_file=(*args)
195
+ path, passphrase = args.flatten
196
+ ssl_context.key = OpenSSL::PKey::RSA.new(File.open(path), passphrase)
197
+ end
198
+
199
+ # Set to a PEM-format client private key
200
+ def key=(*args)
201
+ cert, passphrase = args.flatten
202
+ ssl_context.key = OpenSSL::PKey::RSA.new(cert, passphrase)
203
+ end
204
+
205
+ # Set a path to a file containing a PEM-format CA certificate and enable peer verification
206
+ def ca_file=(path)
207
+ ssl_context.ca_file = path
208
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless path.nil?
209
+ end
210
+
211
+ # Set the Will for the client
212
+ #
213
+ # The will is a message that will be delivered by the server when the client dies.
214
+ # The Will must be set before establishing a connection to the server
215
+ def set_will(topic, payload, retain: false, qos: 0)
216
+ self.will_topic = topic
217
+ self.will_payload = payload
218
+ self.will_retain = retain
219
+ self.will_qos = qos
220
+ end
221
+
222
+ # Connect to the MQTT server
223
+ #
224
+ # If a block is given, then yield to that block and then disconnect again.
225
+ def connect
226
+ if connected?
227
+ yield(self) if block_given?
228
+ return
229
+ end
230
+
231
+ if @client_id.nil? || @client_id.empty?
232
+ raise 'Must provide a client_id if clean_session is set to false' unless @clean_session
233
+
234
+ # Empty client id is not allowed for version 3.1.0
235
+ @client_id = MQTT::Client.generate_client_id if @version == '3.1.0'
236
+ end
237
+
238
+ raise ArgumentError, 'No MQTT server host set when attempting to connect' if @host.nil?
239
+
240
+ connect_internal
241
+
242
+ return unless block_given?
243
+
244
+ # If a block is given, then yield and disconnect
245
+ begin
246
+ yield(self)
247
+ ensure
248
+ disconnect
249
+ end
250
+ end
251
+
252
+ # wait until all messages have been sent
253
+ def flush
254
+ raise NotConnectedException unless connected?
255
+
256
+ queue = Queue.new
257
+ @write_queue << queue
258
+ queue.pop
259
+ nil
260
+ end
261
+
262
+ # Disconnect from the MQTT server.
263
+ #
264
+ # If you don't want to say goodbye to the server, set send_msg to false.
265
+ def disconnect(send_msg: true)
266
+ return unless connected?
267
+
268
+ @read_queue << [ConnectionClosedException.new, current_time]
269
+ # Stop reading packets from the socket first
270
+ @connection_mutex.synchronize do
271
+ if @write_thread&.alive?
272
+ @write_thread.kill
273
+ @write_thread.join
274
+ end
275
+ @read_thread.kill if @read_thread&.alive?
276
+ @read_thread = @write_thread = nil
277
+
278
+ @connected = false
279
+ end
280
+ @acks_mutex.synchronize do
281
+ @acks.each_value do |pending_ack|
282
+ pending_ack.queue << :close
283
+ end
284
+ @acks.clear
285
+ end
286
+
287
+ return unless @socket
288
+
289
+ if send_msg
290
+ packet = MQTT::Packet::Disconnect.new
291
+ begin
292
+ @socket.write(packet.to_s)
293
+ rescue
294
+ nil
295
+ end
296
+ end
297
+ @socket.close
298
+ @socket = nil
299
+ end
300
+
301
+ # Checks whether the client is connected to the server.
302
+ #
303
+ # Note that this returns true even if the connection is down and we're
304
+ # trying to reconnect
305
+ def connected?
306
+ @connected
307
+ end
308
+
309
+ # registers a callback to be called when a connection is re-established
310
+ #
311
+ # can be used to re-subscribe (if you're not using persistent sessions)
312
+ # to topics, and/or re-publish aliveness (if you set a Will)
313
+ def on_reconnect(&block)
314
+ @on_reconnect = block
315
+ end
316
+
317
+ # yields a block, and after the block returns all messages are
318
+ # published at once, waiting for any necessary PubAcks for QoS 1
319
+ # packets as a batch at the end
320
+ #
321
+ # For example:
322
+ # client.batch_publish do
323
+ # client.publish("topic1", "value1", qos: 1)
324
+ # client.publish("topic2", "value2", qos: 1)
325
+ # end
326
+ def batch_publish
327
+ return yield if @batch_publish
328
+
329
+ @batch_publish = {}
330
+
331
+ begin
332
+ yield
333
+
334
+ batch = @batch_publish
335
+ @batch_publish = nil
336
+ batch.each do |(kwargs, values)|
337
+ publish(values, **kwargs)
338
+ end
339
+ ensure
340
+ @batch_publish = nil
341
+ end
342
+ end
343
+
344
+ # Publish a message on a particular topic to the MQTT server.
345
+ def publish(topics, payload = nil, retain: false, qos: 0)
346
+ if topics.is_a?(Hash) && !payload.nil?
347
+ raise ArgumentError, 'Payload cannot be passed if passing a hash for topics and payloads'
348
+ end
349
+ raise NotConnectedException unless connected?
350
+
351
+ if @batch_publish && qos != 0
352
+ values = @batch_publish[{ retain: retain, qos: qos }] ||= {}
353
+ if topics.is_a?(Hash)
354
+ values.merge!(topics)
355
+ else
356
+ values[topics] = payload
357
+ end
358
+ return
359
+ end
360
+
361
+ pending_acks = []
362
+
363
+ topics = { topics => payload } unless topics.is_a?(Hash)
364
+
365
+ topics.each do |(topic, topic_payload)|
366
+ raise ArgumentError, 'Topic name cannot be nil' if topic.nil?
367
+ raise ArgumentError, 'Topic name cannot be empty' if topic.empty?
368
+
369
+ packet = MQTT::Packet::Publish.new(
370
+ id: next_packet_id,
371
+ qos: qos,
372
+ retain: retain,
373
+ topic: topic,
374
+ payload: topic_payload
375
+ )
376
+
377
+ pending_acks << register_for_ack(packet) unless qos.zero?
378
+
379
+ # Send the packet
380
+ send_packet(packet)
381
+ end
382
+
383
+ return if qos.zero?
384
+
385
+ pending_acks.each do |ack|
386
+ wait_for_ack(ack)
387
+ end
388
+ nil
389
+ end
390
+
391
+ # Send a subscribe message for one or more topics on the MQTT server.
392
+ # The topics parameter should be one of the following:
393
+ # * String: subscribe to one topic with QoS 0
394
+ # * Array: subscribe to multiple topics with QoS 0
395
+ # * Hash: subscribe to multiple topics where the key is the topic and the value is the QoS level
396
+ #
397
+ # For example:
398
+ # client.subscribe( 'a/b' )
399
+ # client.subscribe( 'a/b', 'c/d' )
400
+ # client.subscribe( ['a/b',0], ['c/d',1] )
401
+ # client.subscribe( { 'a/b' => 0, 'c/d' => 1 } )
402
+ #
403
+ def subscribe(*topics, wait_for_ack: false)
404
+ raise NotConnectedException unless connected?
405
+
406
+ packet = MQTT::Packet::Subscribe.new(
407
+ id: next_packet_id,
408
+ topics: topics
409
+ )
410
+ token = register_for_ack(packet) if wait_for_ack
411
+ send_packet(packet)
412
+ wait_for_ack(token) if wait_for_ack
413
+ end
414
+
415
+ # Send a unsubscribe message for one or more topics on the MQTT server
416
+ def unsubscribe(*topics, wait_for_ack: false)
417
+ raise NotConnectedException unless connected?
418
+
419
+ topics = topics.first if topics.is_a?(Enumerable) && topics.count == 1
420
+
421
+ packet = MQTT::Packet::Unsubscribe.new(
422
+ topics: topics,
423
+ id: next_packet_id
424
+ )
425
+ token = register_for_ack(packet) if wait_for_ack
426
+ send_packet(packet)
427
+ wait_for_ack(token) if wait_for_ack
428
+ end
429
+
430
+ # Return the next message received from the MQTT server.
431
+ #
432
+ # The method either returns the Publish packet:
433
+ # packet = client.get
434
+ #
435
+ # Or can be used with a block to keep processing messages:
436
+ # client.get do |packet|
437
+ # # Do stuff here
438
+ # end
439
+ #
440
+ def get
441
+ raise NotConnectedException unless connected?
442
+
443
+ loop_start = current_time
444
+ loop do
445
+ packet = @read_queue.pop
446
+ if packet.is_a?(Array) && packet.last >= loop_start
447
+ e = packet.first
448
+ e.set_backtrace((e.backtrace || []) + ['<from MQTT worker thread>'] + caller)
449
+ raise e
450
+ end
451
+ next unless packet.is_a?(Packet)
452
+
453
+ unless block_given?
454
+ puback_packet(packet) if packet.qos > 0
455
+ return packet
456
+ end
457
+
458
+ yield packet
459
+ puback_packet(packet) if packet.qos > 0
460
+ end
461
+ end
462
+
463
+ # Returns true if the incoming message queue is empty.
464
+ def queue_empty?
465
+ @read_queue.empty?
466
+ end
467
+
468
+ # Returns the length of the incoming message queue.
469
+ def queue_length
470
+ @read_queue.length
471
+ end
472
+
473
+ # Clear the incoming message queue.
474
+ def clear_queue
475
+ @read_queue.clear
476
+ end
477
+
478
+ private
479
+
480
+ PendingAck = Struct.new(:packet, :queue, :timeout_at, :send_count)
481
+
482
+ def connect_internal
483
+ # Create network socket
484
+ tcp_socket = TCPSocket.new(@host, @port)
485
+
486
+ if @ssl
487
+ # Set the protocol version
488
+ ssl_context.ssl_version = @ssl if @ssl.is_a?(Symbol)
489
+
490
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
491
+ @socket.sync_close = true
492
+
493
+ # Set hostname on secure socket for Server Name Indication (SNI)
494
+ @socket.hostname = @host if @socket.respond_to?(:hostname=)
495
+
496
+ @socket.connect
497
+ else
498
+ @socket = tcp_socket
499
+ end
500
+
501
+ # Construct a connect packet
502
+ packet = MQTT::Packet::Connect.new(
503
+ version: @version,
504
+ clean_session: @clean_session,
505
+ keep_alive: @keep_alive,
506
+ client_id: @client_id,
507
+ username: @username,
508
+ password: @password,
509
+ will_topic: @will_topic,
510
+ will_payload: @will_payload,
511
+ will_qos: @will_qos,
512
+ will_retain: @will_retain
513
+ )
514
+
515
+ # Send packet
516
+ @socket.write(packet.to_s)
517
+
518
+ # Receive response
519
+ receive_connack
520
+
521
+ @connected = true
522
+
523
+ @write_thread = Thread.new do
524
+ while (packet = @write_queue.pop)
525
+ # flush command
526
+ if packet.is_a?(Queue)
527
+ packet << :flushed
528
+ next
529
+ end
530
+ @socket.write(packet.to_s)
531
+ end
532
+ rescue => e
533
+ @write_queue << packet if packet
534
+ reconnect(e)
535
+ end
536
+
537
+ @read_thread = Thread.new do
538
+ receive_packet while connected?
539
+ end
540
+ end
541
+
542
+ def reconnect(exception)
543
+ should_exit = nil
544
+ @connection_mutex.synchronize do
545
+ @socket&.close
546
+ @socket = nil
547
+ @read_thread&.kill if Thread.current != @read_thread
548
+ @write_thread&.kill if Thread.current != @write_thread
549
+ should_exit = Thread.current == @read_thread
550
+ @read_thread = @write_thread = nil
551
+
552
+ retries = 0
553
+ begin
554
+ connect_internal unless @reconnect_limit == 0
555
+ rescue
556
+ @socket&.close
557
+ @socket = nil
558
+
559
+ if (retries += 1) < @reconnect_limit
560
+ sleep @reconnect_backoff ** retries
561
+ retry
562
+ end
563
+ end
564
+
565
+ unless @socket
566
+ # couldn't reconnect
567
+ @acks_mutex.synchronize do
568
+ @acks.each_value do |pending_ack|
569
+ pending_ack.queue << :close
570
+ end
571
+ @acks.clear
572
+ end
573
+ @connected = false
574
+ @read_queue << [exception, current_time]
575
+ return
576
+ end
577
+ end
578
+
579
+ begin
580
+ if @on_reconnect&.arity == 0
581
+ @on_reconnect.call
582
+ else
583
+ @on_reconnect&.call(@connack)
584
+ end
585
+ rescue => e
586
+ @read_queue << [e, current_time]
587
+ disconnect
588
+ end
589
+ Thread.exit if should_exit
590
+ end
591
+
592
+ # Try to read a packet from the server
593
+ # Also sends keep-alive ping packets.
594
+ def receive_packet
595
+ # Poll socket - is there data waiting?
596
+ timeout = next_timeout
597
+ read_ready, = IO.select([@socket, @wake_up_pipe[0]], [], [], timeout)
598
+
599
+ # we just needed to break out of our select to set up a new timeout;
600
+ # we can discard the actual contents
601
+ @wake_up_pipe[0].readpartial(4096) if read_ready&.include?(@wake_up_pipe[0])
602
+
603
+ handle_timeouts
604
+
605
+ if read_ready&.include?(@socket)
606
+ packet = MQTT::Packet.read(@socket)
607
+ handle_packet(packet)
608
+ end
609
+
610
+ handle_keep_alives
611
+ rescue => e
612
+ reconnect(e)
613
+ end
614
+
615
+ def register_for_ack(packet)
616
+ queue = Queue.new
617
+
618
+ timeout_at = current_time + @ack_timeout
619
+ @acks_mutex.synchronize do
620
+ if @acks.empty?
621
+ # just need to wake up the read thread to set up the timeout for this packet
622
+ @wake_up_pipe[1].write('z')
623
+ end
624
+ @acks[packet.id] = PendingAck.new(packet, queue, timeout_at, 1)
625
+ end
626
+ end
627
+
628
+ def wait_for_ack(pending_ack)
629
+ response = pending_ack.queue.pop
630
+ case response
631
+ when :close
632
+ raise ConnectionClosedException
633
+ when :resend_limit_exceeded
634
+ raise ResendLimitExceededException
635
+ end
636
+ end
637
+
638
+ def handle_packet(packet)
639
+ @last_packet_received_at = current_time
640
+ @keep_alive_sent = false
641
+ case packet
642
+ when MQTT::Packet::Publish
643
+ # Add to queue
644
+ @read_queue.push(packet)
645
+ when MQTT::Packet::Pingresp
646
+ # do nothing; setting @last_packet_received_at already handled it
647
+ when MQTT::Packet::Puback,
648
+ MQTT::Packet::Suback,
649
+ MQTT::Packet::Unsuback
650
+ @acks_mutex.synchronize do
651
+ pending_ack = @acks[packet.id]
652
+ if pending_ack
653
+ @acks.delete(packet.id)
654
+ pending_ack.queue << packet
655
+ end
656
+ end
657
+ end
658
+ # Ignore all other packets
659
+ # FIXME: implement responses for QoS 2
660
+ end
661
+
662
+ def handle_timeouts
663
+ @acks_mutex.synchronize do
664
+ current_time = self.current_time
665
+ @acks.each_value do |pending_ack|
666
+ break unless pending_ack.timeout_at <= current_time
667
+
668
+ resend(pending_ack)
669
+ end
670
+ end
671
+ end
672
+
673
+ def resend(pending_ack)
674
+ packet = pending_ack.packet
675
+ if (pending_ack.send_count += 1) > @resend_limit
676
+ @acks.delete(packet.id)
677
+ pending_ack.queue << :resend_limit_exceeded
678
+ return
679
+ end
680
+ # timed out, or simple re-send
681
+ @wake_up_pipe[1].write('z') if @acks.first.first == packet.id
682
+ pending_ack.timeout_at = current_time + @ack_timeout
683
+ packet.duplicate = true
684
+ send_packet(packet)
685
+ end
686
+
687
+ def current_time
688
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
689
+ end
690
+
691
+ def next_timeout
692
+ timeout_from_acks = @acks_mutex.synchronize do
693
+ @acks.first&.last&.timeout_at
694
+ end
695
+ return nil if timeout_from_acks.nil? && @keep_alive.nil?
696
+
697
+ next_ping = @last_packet_received_at + @keep_alive if @keep_alive && !@keep_alive_sent
698
+ next_ping = @last_packet_received_at + @keep_alive + @ack_timeout if @keep_alive && @keep_alive_sent
699
+ current_time = self.current_time
700
+ [([timeout_from_acks, next_ping].compact.min || current_time) - current_time, 0].max
701
+ end
702
+
703
+ def handle_keep_alives
704
+ return unless @keep_alive && @keep_alive > 0
705
+
706
+ current_time = self.current_time
707
+ if current_time >= @last_packet_received_at + @keep_alive && !@keep_alive_sent
708
+ packet = MQTT::Packet::Pingreq.new
709
+ send_packet(packet)
710
+ @keep_alive_sent = true
711
+ elsif current_time >= @last_packet_received_at + @keep_alive + @ack_timeout
712
+ raise KeepAliveTimeout
713
+ end
714
+ end
715
+
716
+ def puback_packet(packet)
717
+ send_packet(MQTT::Packet::Puback.new(id: packet.id))
718
+ end
719
+
720
+ # Read and check a connection acknowledgement packet
721
+ def receive_connack
722
+ Timeout.timeout(@ack_timeout) do
723
+ packet = MQTT::Packet.read(@socket)
724
+ if packet.class != MQTT::Packet::Connack
725
+ raise MQTT::ProtocolException, "Response wasn't a connection acknowledgement: #{packet.class}"
726
+ end
727
+
728
+ # Check the return code
729
+ if packet.return_code != 0x00
730
+ # 3.2.2.3 If a server sends a CONNACK packet containing a non-zero
731
+ # return code it MUST then close the Network Connection
732
+ @socket.close
733
+ raise MQTT::ProtocolException, packet.return_msg
734
+ end
735
+ @last_packet_received_at = current_time
736
+ @keep_alive_sent = false
737
+ @connack = packet
738
+ end
739
+ end
740
+
741
+ # Send a packet to server
742
+ def send_packet(packet)
743
+ @write_queue << packet
744
+ end
745
+
746
+ def parse_uri(uri)
747
+ uri = URI.parse(uri) unless uri.is_a?(URI)
748
+ ssl = case uri.scheme
749
+ when 'mqtt'
750
+ false
751
+ when 'mqtts'
752
+ true
753
+ else
754
+ raise 'Only the mqtt:// and mqtts:// schemes are supported'
755
+ end
756
+
757
+ {
758
+ host: uri.host,
759
+ port: uri.port || nil,
760
+ username: uri.user ? URI::Parser.new.unescape(uri.user) : nil,
761
+ password: uri.password ? URI::Parser.new.unescape(uri.password) : nil,
762
+ ssl: ssl
763
+ }
764
+ end
765
+
766
+ def next_packet_id
767
+ @last_packet_id = (@last_packet_id || 0).next
768
+ @last_packet_id = 1 if @last_packet_id > 0xffff
769
+ @last_packet_id
770
+ end
771
+ end
772
+ end