mqtt 0.4.0 → 0.6.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,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