mqtt 0.5.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,481 +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.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
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
155
144
  end
156
- end
157
145
 
158
- if args.length >= 2
159
- attr.merge!(:port => args[1]) unless args[1].nil?
160
- end
146
+ if args.length >= 2
147
+ attributes[:port] = args[1] unless args[1].nil?
148
+ end
161
149
 
162
- if args.length >= 3
163
- raise ArgumentError, "Unsupported number of arguments"
164
- end
150
+ raise ArgumentError, 'Unsupported number of arguments' if args.length >= 3
165
151
 
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
152
+ # Merge arguments with default values for attributes
153
+ ATTR_DEFAULTS.merge(attributes).each_pair do |k, v|
154
+ send("#{k}=", v)
155
+ end
170
156
 
171
- # Set a default port number
172
- if @port.nil?
173
- @port = @ssl ? MQTT::DEFAULT_SSL_PORT : MQTT::DEFAULT_PORT
174
- end
157
+ # Set a default port number
158
+ if @port.nil?
159
+ @port = @ssl ? MQTT::DEFAULT_SSL_PORT : MQTT::DEFAULT_PORT
160
+ end
175
161
 
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
162
+ if @ssl
163
+ require 'openssl'
164
+ require 'mqtt/openssl_fix'
165
+ end
186
166
 
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
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
176
+ end
191
177
 
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
178
+ # Get the OpenSSL context, that is used if SSL/TLS is enabled
179
+ def ssl_context
180
+ @ssl_context ||= OpenSSL::SSL::SSLContext.new
181
+ end
196
182
 
197
- # PEM-format client certificate
198
- def cert=(cert)
199
- ssl_context.cert = OpenSSL::X509::Certificate.new(cert)
200
- end
183
+ # Set a path to a file containing a PEM-format client certificate
184
+ def cert_file=(path)
185
+ self.cert = File.read(path)
186
+ end
201
187
 
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
188
+ # PEM-format client certificate
189
+ def cert=(cert)
190
+ ssl_context.cert = OpenSSL::X509::Certificate.new(cert)
191
+ end
207
192
 
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
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
213
198
 
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
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)
219
203
  end
220
- end
221
204
 
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
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
232
210
 
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
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
238
220
  end
239
221
 
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'
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?
226
+
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
229
+
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'
248
232
  end
249
- end
250
233
 
251
- if @host.nil?
252
- raise 'No MQTT server host set when attempting to connect'
253
- end
234
+ raise 'No MQTT server host set when attempting to connect' if @host.nil?
254
235
 
255
- if not connected?
256
- # Create network socket
257
- tcp_socket = TCPSocket.new(@host, @port)
236
+ unless connected?
237
+ # Create network socket
238
+ tcp_socket = TCPSocket.new(@host, @port)
258
239
 
259
- if @ssl
260
- # Set the protocol version
261
- if @ssl.is_a?(Symbol)
262
- ssl_context.ssl_version = @ssl
263
- end
240
+ if @ssl
241
+ # Set the protocol version
242
+ ssl_context.ssl_version = @ssl if @ssl.is_a?(Symbol)
264
243
 
265
- @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
266
- @socket.sync_close = true
244
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
245
+ @socket.sync_close = true
267
246
 
268
- # Set hostname on secure socket for Server Name Indication (SNI)
269
- if @socket.respond_to?(:hostname=)
270
- @socket.hostname = @host
271
- end
247
+ # Set hostname on secure socket for Server Name Indication (SNI)
248
+ @socket.hostname = @host if @socket.respond_to?(:hostname=)
272
249
 
273
- @socket.connect
274
- else
275
- @socket = tcp_socket
276
- end
250
+ @socket.connect
251
+ else
252
+ @socket = tcp_socket
253
+ end
277
254
 
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
- )
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
+ )
291
268
 
292
- # Send packet
293
- send_packet(packet)
269
+ # Send packet
270
+ send_packet(packet)
294
271
 
295
- # Receive response
296
- receive_connack
272
+ # Receive response
273
+ receive_connack
297
274
 
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
275
+ # Start packet reading thread
276
+ @read_thread = Thread.new(Thread.current) do |parent|
277
+ Thread.current[:parent] = parent
278
+ receive_packet while connected?
303
279
  end
304
280
  end
305
- end
306
281
 
307
- # If a block is given, then yield and disconnect
308
- if block_given?
282
+ return unless block_given?
283
+
284
+ # If a block is given, then yield and disconnect
309
285
  begin
310
286
  yield(self)
311
287
  ensure
312
288
  disconnect
313
289
  end
314
290
  end
315
- end
316
291
 
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
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
323
298
 
324
- # Close the socket if it is open
325
- if connected?
299
+ return unless connected?
300
+
301
+ # Close the socket if it is open
326
302
  if send_msg
327
303
  packet = MQTT::Packet::Disconnect.new
328
304
  send_packet(packet)
329
305
  end
330
306
  @socket.close unless @socket.nil?
307
+ handle_close
331
308
  @socket = nil
332
309
  end
333
- end
334
310
 
335
- # Checks whether the client is connected to the server.
336
- def connected?
337
- (not @socket.nil?) and (not @socket.closed?)
338
- end
311
+ # Checks whether the client is connected to the server.
312
+ def connected?
313
+ !@socket.nil? && !@socket.closed?
314
+ end
339
315
 
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?
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
+ )
344
328
 
345
- packet = MQTT::Packet::Publish.new(
346
- :id => next_packet_id,
347
- :qos => qos,
348
- :retain => retain,
349
- :topic => topic,
350
- :payload => payload
351
- )
329
+ # Send the packet
330
+ res = send_packet(packet)
352
331
 
353
- # Send the packet
354
- res = send_packet(packet)
332
+ return if qos.zero?
355
333
 
356
- if packet.qos > 0
357
- Timeout.timeout(@ack_timeout) do
358
- 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
359
348
  @pubacks_semaphore.synchronize do
360
- return res if @pubacks.delete(packet.id)
349
+ @pubacks.delete packet.id
361
350
  end
362
- # FIXME: make threads communicate with each other, instead of polling
363
- # (using a pipe and select ?)
364
- sleep 0.01
351
+ break
365
352
  end
366
353
  end
367
- return -1
354
+
355
+ res
368
356
  end
369
- end
370
357
 
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
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
390
377
 
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]
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
412
400
  end
413
401
  end
414
- end
415
402
 
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
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
436
429
  packet = @read_queue.pop
437
- yield(packet)
438
430
  puback_packet(packet) if packet.qos > 0
431
+ return packet
439
432
  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
433
  end
446
- end
447
434
 
448
- # Returns true if the incoming message queue is empty.
449
- def queue_empty?
450
- @read_queue.empty?
451
- end
435
+ # Returns true if the incoming message queue is empty.
436
+ def queue_empty?
437
+ @read_queue.empty?
438
+ end
452
439
 
453
- # Returns the length of the incoming message queue.
454
- def queue_length
455
- @read_queue.length
456
- end
440
+ # Returns the length of the incoming message queue.
441
+ def queue_length
442
+ @read_queue.length
443
+ end
457
444
 
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
445
+ # Clear the incoming message queue.
446
+ def clear_queue
447
+ @read_queue.clear
462
448
  end
463
449
 
464
- packet = MQTT::Packet::Unsubscribe.new(
465
- :topics => topics,
466
- :id => next_packet_id
467
- )
468
- send_packet(packet)
469
- 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
470
453
 
471
- 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
472
462
 
473
- # Try to read a packet from the server
474
- # Also sends keep-alive ping packets.
475
- def receive_packet
476
- begin
463
+ # Try to read a packet from the server
464
+ # Also sends keep-alive ping packets.
465
+ def receive_packet
477
466
  # Poll socket - is there data waiting?
478
467
  result = IO.select([@socket], [], [], SELECT_TIMEOUT)
468
+ handle_timeouts
479
469
  unless result.nil?
480
470
  # Yes - read in the packet
481
471
  packet = MQTT::Packet.read(@socket)
@@ -487,121 +477,147 @@ private
487
477
  unless @socket.nil?
488
478
  @socket.close
489
479
  @socket = nil
480
+ handle_close
490
481
  end
491
482
  Thread.current[:parent].raise(exp)
492
483
  end
493
- end
494
484
 
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
485
+ def wait_for_puback(id, queue)
502
486
  @pubacks_semaphore.synchronize do
503
- @pubacks[packet.id] = packet
487
+ @pubacks[id] = queue
504
488
  end
505
489
  end
506
- # Ignore all other packets
507
- # FIXME: implement responses for QoS 2
508
- end
509
490
 
510
- def keep_alive!
511
- if @keep_alive > 0 && connected?
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
+
512
532
  response_timeout = (@keep_alive * 1.5).ceil
513
- if Time.now >= @last_ping_request + @keep_alive
533
+ if current_time >= @last_ping_request + @keep_alive
514
534
  packet = MQTT::Packet::Pingreq.new
515
535
  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
- )
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"
521
539
  end
522
540
  end
523
- end
524
541
 
525
- def puback_packet(packet)
526
- send_packet(MQTT::Packet::Puback.new :id => packet.id)
527
- end
542
+ def puback_packet(packet)
543
+ send_packet(MQTT::Packet::Puback.new(:id => packet.id))
544
+ end
528
545
 
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
- )
537
- 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
538
553
 
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)
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
545
561
  end
546
562
  end
547
- end
548
563
 
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?
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?
553
568
 
554
- # Only allow one thread to write to socket at a time
555
- @write_semaphore.synchronize do
556
- @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
557
573
  end
558
- end
559
574
 
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"
569
- 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
570
584
 
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
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
579
593
 
580
- def next_packet_id
581
- @last_packet_id = ( @last_packet_id || 0 ).next
582
- 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
583
599
 
584
- # ---- Deprecated attributes and methods ---- #
585
- public
600
+ # ---- Deprecated attributes and methods ---- #
601
+ public
586
602
 
587
- # @deprecated Please use {#host} instead
588
- def remote_host
589
- host
590
- end
603
+ # @deprecated Please use {#host} instead
604
+ def remote_host
605
+ host
606
+ end
591
607
 
592
- # @deprecated Please use {#host=} instead
593
- def remote_host=(args)
594
- self.host = args
595
- end
608
+ # @deprecated Please use {#host=} instead
609
+ def remote_host=(args)
610
+ self.host = args
611
+ end
596
612
 
597
- # @deprecated Please use {#port} instead
598
- def remote_port
599
- port
600
- end
613
+ # @deprecated Please use {#port} instead
614
+ def remote_port
615
+ port
616
+ end
601
617
 
602
- # @deprecated Please use {#port=} instead
603
- def remote_port=(args)
604
- self.port = args
618
+ # @deprecated Please use {#port=} instead
619
+ def remote_port=(args)
620
+ self.port = args
621
+ end
605
622
  end
606
-
607
623
  end