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