mqtt 0.4.0 → 0.6.0

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