mqtt-ccutrer 1.0.0

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,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