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.
- checksums.yaml +5 -13
- data/NEWS.md +15 -0
- data/README.md +1 -1
- data/lib/mqtt/client.rb +496 -469
- data/lib/mqtt/openssl_fix.rb +29 -0
- data/lib/mqtt/packet.rb +182 -235
- data/lib/mqtt/patches/string_encoding.rb +5 -7
- data/lib/mqtt/proxy.rb +85 -89
- data/lib/mqtt/sn/packet.rb +469 -512
- data/lib/mqtt/version.rb +1 -1
- data/lib/mqtt.rb +1 -3
- data/spec/mqtt_client_spec.rb +148 -16
- data/spec/mqtt_packet_spec.rb +8 -0
- data/spec/zz_client_integration_spec.rb +13 -2
- metadata +40 -27
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
148
|
-
|
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
|
-
|
151
|
-
when %r
|
152
|
-
|
138
|
+
attributes.merge!(parse_uri(args[0]))
|
139
|
+
when %r{^mqtts?://}
|
140
|
+
attributes.merge!(parse_uri(args[0]))
|
153
141
|
else
|
154
|
-
|
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
|
-
|
159
|
-
|
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
|
163
|
-
|
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
|
-
#
|
167
|
-
|
168
|
-
self.
|
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
|
-
#
|
172
|
-
|
173
|
-
|
188
|
+
# PEM-format client certificate
|
189
|
+
def cert=(cert)
|
190
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(cert)
|
174
191
|
end
|
175
192
|
|
176
|
-
#
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
209
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
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
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
252
|
-
|
253
|
-
end
|
244
|
+
@socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
245
|
+
@socket.sync_close = true
|
254
246
|
|
255
|
-
|
256
|
-
|
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
|
-
|
260
|
-
|
261
|
-
|
262
|
-
ssl_context.ssl_version = @ssl
|
250
|
+
@socket.connect
|
251
|
+
else
|
252
|
+
@socket = tcp_socket
|
263
253
|
end
|
264
254
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
-
|
287
|
-
|
269
|
+
# Send packet
|
270
|
+
send_packet(packet)
|
288
271
|
|
289
|
-
|
290
|
-
|
272
|
+
# Receive response
|
273
|
+
receive_connack
|
291
274
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
302
|
-
|
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
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
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
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
311
|
+
# Checks whether the client is connected to the server.
|
312
|
+
def connected?
|
313
|
+
!@socket.nil? && !@socket.closed?
|
314
|
+
end
|
333
315
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
-
|
340
|
-
|
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
|
-
|
348
|
-
res = send_packet(packet)
|
332
|
+
return if qos.zero?
|
349
333
|
|
350
|
-
|
351
|
-
|
352
|
-
|
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
|
-
|
349
|
+
@pubacks.delete packet.id
|
355
350
|
end
|
356
|
-
|
357
|
-
# (using a pipe and select ?)
|
358
|
-
sleep 0.01
|
351
|
+
break
|
359
352
|
end
|
360
353
|
end
|
361
|
-
|
354
|
+
|
355
|
+
res
|
362
356
|
end
|
363
|
-
end
|
364
357
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
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
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
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
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
435
|
+
# Returns true if the incoming message queue is empty.
|
436
|
+
def queue_empty?
|
437
|
+
@read_queue.empty?
|
438
|
+
end
|
444
439
|
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
440
|
+
# Returns the length of the incoming message queue.
|
441
|
+
def queue_length
|
442
|
+
@read_queue.length
|
443
|
+
end
|
449
444
|
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
topics = topics.first
|
445
|
+
# Clear the incoming message queue.
|
446
|
+
def clear_queue
|
447
|
+
@read_queue.clear
|
454
448
|
end
|
455
449
|
|
456
|
-
|
457
|
-
|
458
|
-
|
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
|
-
|
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
|
-
|
466
|
-
|
467
|
-
|
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
|
-
|
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[
|
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
|
-
|
503
|
-
|
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
|
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 =
|
509
|
-
elsif
|
510
|
-
raise MQTT::ProtocolException
|
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
|
-
|
518
|
-
|
519
|
-
|
542
|
+
def puback_packet(packet)
|
543
|
+
send_packet(MQTT::Packet::Puback.new(:id => packet.id))
|
544
|
+
end
|
520
545
|
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|
-
|
532
|
-
|
533
|
-
|
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
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
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
|
-
|
544
|
-
|
545
|
-
|
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
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
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
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
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
|
-
|
570
|
-
|
571
|
-
|
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
|
-
|
574
|
-
|
600
|
+
# ---- Deprecated attributes and methods ---- #
|
601
|
+
public
|
575
602
|
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
603
|
+
# @deprecated Please use {#host} instead
|
604
|
+
def remote_host
|
605
|
+
host
|
606
|
+
end
|
580
607
|
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
608
|
+
# @deprecated Please use {#host=} instead
|
609
|
+
def remote_host=(args)
|
610
|
+
self.host = args
|
611
|
+
end
|
585
612
|
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
613
|
+
# @deprecated Please use {#port} instead
|
614
|
+
def remote_port
|
615
|
+
port
|
616
|
+
end
|
590
617
|
|
591
|
-
|
592
|
-
|
593
|
-
|
618
|
+
# @deprecated Please use {#port=} instead
|
619
|
+
def remote_port=(args)
|
620
|
+
self.port = args
|
621
|
+
end
|
594
622
|
end
|
595
|
-
|
596
623
|
end
|