mqtt 0.5.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,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