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