paho-mqtt 1.0.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 16c0a069f566b0c91b896cbcc0a4b3ca2eb694f6
4
- data.tar.gz: bd03230d51b7bc37be071caac903ce8537073b6b
3
+ metadata.gz: 81783208a50d429eede136e757bddf982ed2fd83
4
+ data.tar.gz: 4d1c34982623e35713e8af79da00cc6bf14d91eb
5
5
  SHA512:
6
- metadata.gz: 0c3a8a75de74ef2df07b3e6df1abcf694e07dd425e589169a8c7696e8131b55aba92b41daf6525e60ef3210347f33139ea399eb8092877dedc40c6f986b022e5
7
- data.tar.gz: 53e67740ea1e52c1a0e73517079d1b7b0ce1f15802ea22f9a880a00ba2e514b7e715e7a06b293931e1a561cbd2beb2dd8439dee906a6fdfbc183c13f0ce7f87c
6
+ metadata.gz: 02f36654df8e3f0ae3a3e47f64708ca0f770076766c7e593755d8d55dc14950e40b1eab91631f4aab7815e83653f83e2ca05dd02f8565820a3d2b089a2652cee
7
+ data.tar.gz: 50456a5604f7318622cd1809b8859db2845c7c5a0b7b4bae15d09333d957248c40cced139c7c54ac6efff9efda6985aad4c84b50de33880491ad4a369a247ff4
data/lib/paho-mqtt.rb CHANGED
@@ -1,8 +1,27 @@
1
+ # Copyright (c) 2016-2017 Pierre Goudet <p-goudet@ruby-dev.jp>
2
+ #
3
+ # All rights reserved. This program and the accompanying materials
4
+ # are made available under the terms of the Eclipse Public License v1.0
5
+ # and Eclipse Distribution License v1.0 which accompany this distribution.
6
+ #
7
+ # The Eclipse Public License is available at
8
+ # https://eclipse.org/org/documents/epl-v10.php.
9
+ # and the Eclipse Distribution License is available at
10
+ # https://eclipse.org/org/documents/edl-v10.php.
11
+ #
12
+ # Contributors:
13
+ # Pierre Goudet - initial committer
14
+
1
15
  require "paho_mqtt/version"
2
16
  require "paho_mqtt/client"
3
17
  require "paho_mqtt/packet"
18
+ require 'logger'
4
19
 
5
20
  module PahoMqtt
21
+ extend self
22
+
23
+ attr_accessor :logger
24
+
6
25
  # Default connection setup
7
26
  DEFAULT_SSL_PORT = 8883
8
27
  DEFAULT_PORT = 1883
@@ -47,15 +66,67 @@ module PahoMqtt
47
66
  nil
48
67
  ]
49
68
 
69
+ CONNACK_ERROR_MESSAGE = {
70
+ 0x02 => "Client Identifier is correct but not allowed by remote server.",
71
+ 0x03 => "Connection established but MQTT service unvailable on remote server.",
72
+ 0x04 => "User name or user password is malformed.",
73
+ 0x05 => "Client is not authorized to connect to the server."
74
+ }
75
+
76
+ CLIENT_ATTR_DEFAULTS = {
77
+ :host => "",
78
+ :port => nil,
79
+ :mqtt_version => '3.1.1',
80
+ :clean_session => true,
81
+ :persistent => false,
82
+ :blocking => false,
83
+ :client_id => nil,
84
+ :username => nil,
85
+ :password => nil,
86
+ :ssl => false,
87
+ :will_topic => nil,
88
+ :will_payload => nil,
89
+ :will_qos => 0,
90
+ :will_retain => false,
91
+ :keep_alive => 60,
92
+ :ack_timeout => 5,
93
+ :on_connack => nil,
94
+ :on_suback => nil,
95
+ :on_unsuback => nil,
96
+ :on_puback => nil,
97
+ :on_pubrel => nil,
98
+ :on_pubrec => nil,
99
+ :on_pubcomp => nil,
100
+ :on_message => nil,
101
+ }
102
+
50
103
  Thread.abort_on_exception = true
51
104
 
105
+ def logger=(logger_path)
106
+ file = File.open(logger_path, "a+")
107
+ log_file = Logger.new(file)
108
+ log_file.level = Logger::DEBUG
109
+ @action_manager.logger = log_file
110
+ end
111
+
112
+ def logger
113
+ @logger
114
+ end
115
+
116
+ def logger?
117
+ @logger.is_a?(Logger)
118
+ end
119
+
52
120
  class Exception < ::Exception
53
121
  end
54
122
 
55
123
  class ProtocolViolation < PahoMqtt::Exception
56
124
  end
57
125
 
58
- class ParameterException < PahoMqtt::Exception
126
+ class WritingException < PahoMqtt::Exception
127
+ end
128
+
129
+ class ReadingException < PahoMqtt::Exception
59
130
  end
60
131
 
61
132
  class PacketException < PahoMqtt::Exception
@@ -1,12 +1,26 @@
1
- require 'openssl'
2
- require 'socket'
3
- require 'logger'
1
+ # Copyright (c) 2016-2017 Pierre Goudet <p-goudet@ruby-dev.jp>
2
+ #
3
+ # All rights reserved. This program and the accompanying materials
4
+ # are made available under the terms of the Eclipse Public License v1.0
5
+ # and Eclipse Distribution License v1.0 which accompany this distribution.
6
+ #
7
+ # The Eclipse Public License is available at
8
+ # https://eclipse.org/org/documents/epl-v10.php.
9
+ # and the Eclipse Distribution License is available at
10
+ # https://eclipse.org/org/documents/edl-v10.php.
11
+ #
12
+ # Contributors:
13
+ # Pierre Goudet - initial committer
14
+
15
+ require 'paho_mqtt/handler'
16
+ require 'paho_mqtt/connection_helper'
17
+ require 'paho_mqtt/sender'
18
+ require 'paho_mqtt/publisher'
19
+ require 'paho_mqtt/subscriber'
20
+ require 'paho_mqtt/ssl_helper'
4
21
 
5
22
  module PahoMqtt
6
- class Client
7
- # Log file
8
- attr_accessor :logger
9
-
23
+ class Client
10
24
  # Connection related attributes:
11
25
  attr_accessor :host
12
26
  attr_accessor :port
@@ -18,7 +32,7 @@ module PahoMqtt
18
32
  attr_accessor :username
19
33
  attr_accessor :password
20
34
  attr_accessor :ssl
21
-
35
+
22
36
  # Last will attributes:
23
37
  attr_accessor :will_topic
24
38
  attr_accessor :will_payload
@@ -29,100 +43,45 @@ module PahoMqtt
29
43
  attr_accessor :keep_alive
30
44
  attr_accessor :ack_timeout
31
45
 
32
- #Callback attributes
33
- attr_accessor :on_message
34
- attr_accessor :on_connack
35
- attr_accessor :on_suback
36
- attr_accessor :on_unsuback
37
- attr_accessor :on_puback
38
- attr_accessor :on_pubrel
39
- attr_accessor :on_pubrec
40
- attr_accessor :on_pubcomp
41
-
42
46
  #Read Only attribute
43
- attr_reader :registered_callback
44
- attr_reader :subscribed_topics
45
47
  attr_reader :connection_state
48
+ attr_reader :ssl_context
46
49
 
47
- ATTR_DEFAULTS = {
48
- :logger => nil,
49
- :host => "",
50
- :port => nil,
51
- :mqtt_version => '3.1.1',
52
- :clean_session => true,
53
- :persistent => false,
54
- :blocking => false,
55
- :client_id => nil,
56
- :username => nil,
57
- :password => nil,
58
- :ssl => false,
59
- :will_topic => nil,
60
- :will_payload => nil,
61
- :will_qos => 0,
62
- :will_retain => false,
63
- :keep_alive => 60,
64
- :ack_timeout => 5,
65
- :on_connack => nil,
66
- :on_suback => nil,
67
- :on_unsuback => nil,
68
- :on_puback => nil,
69
- :on_pubrel => nil,
70
- :on_pubrec => nil,
71
- :on_pubcomp => nil,
72
- :on_message => nil,
73
- }
74
-
75
50
  def initialize(*args)
51
+ @last_ping_resp = Time.now
52
+ @last_packet_id = 0
53
+ @ssl_context = nil
54
+ @sender = nil
55
+ @handler = Handler.new
56
+ @connection_helper = nil
57
+ @connection_state = MQTT_CS_DISCONNECT
58
+ @connection_state_mutex = Mutex.new
59
+ @mqtt_thread = nil
60
+ @reconnect_thread = nil
61
+ @id_mutex = Mutex.new
62
+
76
63
  if args.last.is_a?(Hash)
77
64
  attr = args.pop
78
65
  else
79
66
  attr = {}
80
67
  end
81
68
 
82
- ATTR_DEFAULTS.merge(attr).each_pair do |k,v|
69
+ CLIENT_ATTR_DEFAULTS.merge(attr).each_pair do |k,v|
83
70
  self.send("#{k}=", v)
84
71
  end
85
72
 
86
73
  if @port.nil?
87
74
  @port
88
75
  if @ssl
89
- @port = PahoMqtt::DEFAULT_SSL_PORT
76
+ @port = DEFAULT_SSL_PORT
90
77
  else
91
- @port = PahoMqtt::DEFAULT_PORT
78
+ @port = DEFAULT_PORT
92
79
  end
93
80
  end
94
81
 
95
82
  if @client_id.nil? || @client_id == ""
96
83
  @client_id = generate_client_id
97
84
  end
98
-
99
- @last_ping_req = Time.now
100
- @last_ping_resp = Time.now
101
- @last_packet_id = 0
102
- @socket = nil
103
- @ssl_context = nil
104
- @writing_mutex = Mutex.new
105
- @writing_queue = []
106
- @connection_state = MQTT_CS_DISCONNECT
107
- @connection_state_mutex = Mutex.new
108
- @subscribed_mutex = Mutex.new
109
- @subscribed_topics = []
110
- @registered_callback = []
111
- @waiting_suback = []
112
- @suback_mutex = Mutex.new
113
- @waiting_unsuback = []
114
- @unsuback_mutex = Mutex.new
115
- @mqtt_thread = nil
116
- @reconnect_thread = nil
117
-
118
- @puback_mutex = Mutex.new
119
- @pubrec_mutex = Mutex.new
120
- @pubrel_mutex = Mutex.new
121
- @pubcomp_mutex = Mutex.new
122
- @waiting_puback = []
123
- @waiting_pubrec = []
124
- @waiting_pubrel = []
125
- @waiting_pubcomp = []
126
85
  end
127
86
 
128
87
  def generate_client_id(prefix='paho_ruby', lenght=16)
@@ -130,25 +89,11 @@ module PahoMqtt
130
89
  @client_id = prefix << Array.new(lenght) { charset.sample }.join
131
90
  end
132
91
 
133
- def ssl_context
134
- @ssl_context ||= OpenSSL::SSL::SSLContext.new
135
- end
136
-
137
92
  def config_ssl_context(cert_path, key_path, ca_path=nil)
138
93
  @ssl ||= true
139
- @ssl_context = ssl_context
140
- self.cert = cert_path
141
- self.key = key_path
142
- self.root_ca = ca_path
94
+ @ssl_context = SSLHelper.config_ssl_context(cert_path, key_path, ca_path)
143
95
  end
144
96
 
145
- def config_will(topic, payload="", retain=false, qos=0)
146
- @will_topic = topic
147
- @will_payload = payload
148
- @will_retain = retain
149
- @will_qos = qos
150
- end
151
-
152
97
  def connect(host=@host, port=@port, keep_alive=@keep_alive, persistent=@persistent, blocking=@blocking)
153
98
  @persistent = persistent
154
99
  @blocking = blocking
@@ -158,11 +103,19 @@ module PahoMqtt
158
103
  @connection_state_mutex.synchronize {
159
104
  @connection_state = MQTT_CS_NEW
160
105
  }
161
- setup_connection
162
- connect_async(host, port, keep_alive) unless @blocking
106
+ @mqtt_thread.kill unless @mqtt_thread.nil?
107
+ init_connection
108
+ @connection_helper.send_connect(session_params)
109
+ begin
110
+ @connection_state = @connection_helper.do_connect(reconnect?)
111
+ rescue LowVersionException
112
+ downgrade_version
113
+ end
114
+ build_pubsub
115
+ daemon_mode unless @blocking || !connected?
163
116
  end
164
117
 
165
- def connect_async(host, port=1883, keep_alive)
118
+ def daemon_mode
166
119
  @mqtt_thread = Thread.new do
167
120
  @reconnect_thread.kill unless @reconnect_thread.nil? || !@reconnect_thread.alive?
168
121
  while connected? do
@@ -174,20 +127,34 @@ module PahoMqtt
174
127
  def connected?
175
128
  @connection_state == MQTT_CS_CONNECTED
176
129
  end
177
-
130
+
131
+ def reconnect?
132
+ Thread.current == @reconnect_thread
133
+ end
134
+
178
135
  def loop_write(max_packet=MAX_WRITING)
179
- @writing_mutex.synchronize {
180
- cnt = 0
181
- while !@writing_queue.empty? && cnt < max_packet do
182
- send_packet(@writing_queue.shift)
183
- cnt += 1
136
+ begin
137
+ @sender.writing_loop(max_packet)
138
+ rescue WritingException
139
+ if check_persistence
140
+ reconnect
141
+ else
142
+ raise WritingException
184
143
  end
185
- }
144
+ end
186
145
  end
187
146
 
188
147
  def loop_read(max_packet=MAX_READ)
189
148
  max_packet.times do
190
- receive_packet
149
+ begin
150
+ @handler.receive_packet
151
+ rescue ReadingException
152
+ if check_persistence
153
+ reconnect
154
+ else
155
+ raise ReadingException
156
+ end
157
+ end
191
158
  end
192
159
  end
193
160
 
@@ -199,19 +166,17 @@ module PahoMqtt
199
166
  end
200
167
 
201
168
  def loop_misc
202
- check_keep_alive
203
- check_ack_alive(@waiting_puback, @puback_mutex, MAX_PUBACK)
204
- check_ack_alive(@waiting_pubrec, @pubrec_mutex, MAX_PUBREC)
205
- check_ack_alive(@waiting_pubrel, @pubrel_mutex, MAX_PUBREL)
206
- check_ack_alive(@waiting_pubcomp, @pubcomp_mutex, MAX_PUBCOMP)
207
- check_ack_alive(@waiting_suback, @suback_mutex, @waiting_suback.length)
208
- check_ack_alive(@waiting_unsuback, @unsuback_mutex, @waiting_unsuback.length)
169
+ if @connection_helper.check_keep_alive(@persistent, @handler.last_ping_resp, @keep_alive) == MQTT_CS_DISCONNECT
170
+ reconnect if check_persistence
171
+ end
172
+ @publisher.check_waiting_publisher
173
+ @subscriber.check_waiting_subscriber
209
174
  end
210
175
 
211
176
  def reconnect(retry_time=RECONNECT_RETRY_TIME, retry_tempo=RECONNECT_RETRY_TEMPO)
212
177
  @reconnect_thread = Thread.new do
213
178
  retry_time.times do
214
- @logger.debug("New reconnect atempt...") if @logger.is_a?(Logger)
179
+ PahoMqtt.logger.debug("New reconnect atempt...") if PahoMqtt.logger?
215
180
  connect
216
181
  if connected?
217
182
  break
@@ -220,7 +185,7 @@ module PahoMqtt
220
185
  end
221
186
  end
222
187
  unless connected?
223
- @logger.error("Reconnection atempt counter is over.(#{RECONNECT_RETRY_TIME} times)") if @logger.is_a?(Logger)
188
+ PahoMqtt.logger.error("Reconnection atempt counter is over.(#{RECONNECT_RETRY_TIME} times)") if PahoMqtt.logger?
224
189
  disconnect(false)
225
190
  exit
226
191
  end
@@ -228,38 +193,8 @@ module PahoMqtt
228
193
  end
229
194
 
230
195
  def disconnect(explicit=true)
231
- @logger.debug("Disconnecting from #{@host}") if @logger.is_a?(Logger)
232
-
233
- if explicit
234
- send_disconnect
235
- @mqtt_thread.kill if @mqtt_thread && @mqtt_thread.alive?
236
- @mqtt_thread.kill if @mqtt_thread.alive?
237
- @last_packet_id = 0
238
-
239
- @writing_mutex.synchronize {
240
- @writing_queue = []
241
- }
242
-
243
- @puback_mutex.synchronize {
244
- @waiting_puback = []
245
- }
246
-
247
- @pubrec_mutex.synchronize {
248
- @waiting_pubrec = []
249
- }
250
-
251
- @pubrel_mutex.synchronize {
252
- @waiting_pubrel = []
253
- }
254
-
255
- @pubcomp_mutex.synchronize {
256
- @waiting_pubcomp = []
257
- }
258
- end
259
-
260
- @socket.close unless @socket.nil? || @socket.closed?
261
- @socket = nil
262
-
196
+ @last_packet_id = 0 if explicit
197
+ @connection_helper.do_disconnect(@publisher, explicit, @mqtt_thread)
263
198
  @connection_state_mutex.synchronize {
264
199
  @connection_state = MQTT_CS_DISCONNECT
265
200
  }
@@ -268,656 +203,195 @@ module PahoMqtt
268
203
 
269
204
  def publish(topic, payload="", retain=false, qos=0)
270
205
  if topic == "" || !topic.is_a?(String)
271
- @logger.error("Publish topics is invalid, not a string or empty.") if @logger.is_a?(Logger)
272
- raise ParameterException
206
+ PahoMqtt.logger.error("Publish topics is invalid, not a string or empty.") if PahoMqtt.logger?
207
+ raise ArgumentError
273
208
  end
274
- send_publish(topic, payload, retain, qos)
209
+ id = next_packet_id
210
+ @publisher.send_publish(topic, payload, retain, qos, id)
211
+ MQTT_ERR_SUCCESS
275
212
  end
276
213
 
277
214
  def subscribe(*topics)
278
- unless valid_topics?(topics) == MQTT_ERR_FAIL
279
- send_subscribe(topics)
280
- else
281
- @logger.error("Subscribe topics need one topic or a list of topics.") if @logger.is_a?(Logger)
215
+ begin
216
+ id = next_packet_id
217
+ unless @subscriber.send_subscribe(topics, id) == PahoMqtt::MQTT_ERR_SUCCESS
218
+ reconnect if check_persistence
219
+ end
220
+ MQTT_ERR_SUCCESS
221
+ rescue ProtocolViolation
222
+ PahoMqtt.logger.error("Subscribe topics need one topic or a list of topics.") if PahoMqtt.logger?
282
223
  disconnect(false)
283
224
  raise ProtocolViolation
284
225
  end
285
226
  end
286
227
 
287
228
  def unsubscribe(*topics)
288
- unless valid_topics?(topics) == MQTT_ERR_FAIL
289
- send_unsubscribe(topics)
290
- else
291
- @logger.error("Unsubscribe need at least one topics.") if @logger.is_a?(Logger)
229
+ begin
230
+ id = next_packet_id
231
+ unless @subscriber.send_unsubscribe(topics, id) == MQTT_ERR_SUCCESS
232
+ reconnect if check_persistence
233
+ end
234
+ MQTT_ERR_SUCCESS
235
+ rescue ProtocolViolation
236
+ PahoMqtt.logger.error("Unsubscribe need at least one topics.") if PahoMqtt.logger?
292
237
  disconnect(false)
293
238
  raise ProtocolViolation
294
239
  end
295
240
  end
296
241
 
297
242
  def ping_host
298
- send_pingreq
243
+ @sender.send_pingreq
299
244
  end
300
245
 
301
246
  def add_topic_callback(topic, callback=nil, &block)
302
- if topic.nil?
303
- @logger.error("The topics where the callback is trying to be registered have been found nil.") if @logger.is_a?(Logger)
304
- raise ParameterException
305
- end
306
- remove_topic_callback(topic)
307
-
308
- if block_given?
309
- @registered_callback.push([topic, block])
310
- elsif !(callback.nil?) && callback.class == Proc
311
- @registered_callback.push([topic, callback])
312
- end
313
- MQTT_ERR_SUCCESS
247
+ @handler.register_topic_callback(topic, callback, &block)
314
248
  end
315
249
 
316
250
  def remove_topic_callback(topic)
317
- if topic.nil?
318
- @logger.error("The topics where the callback is trying to be unregistered have been found nil.") if @logger.is_a?(Logger)
319
- raise ParameterException
320
- end
321
- @registered_callback.delete_if {|pair| pair.first == topic}
322
- MQTT_ERR_SUCCESS
251
+ @handler.clear_topic_callback(topic)
323
252
  end
324
253
 
325
254
  def on_connack(&block)
326
- @on_connack = block if block_given?
327
- @on_connack
255
+ @handler.on_connack = block if block_given?
256
+ @handler.on_connack
328
257
  end
329
258
 
330
259
  def on_suback(&block)
331
- @on_suback = block if block_given?
332
- @on_suback
260
+ @handler.on_suback = block if block_given?
261
+ @handler.on_suback
333
262
  end
334
263
 
335
264
  def on_unsuback(&block)
336
- @on_unsuback = block if block_given?
337
- @on_unsuback
265
+ @handler.on_unsuback = block if block_given?
266
+ @handler.on_unsuback
338
267
  end
339
268
 
340
269
  def on_puback(&block)
341
- @on_puback = block if block_given?
342
- @on_puback
270
+ @handler.on_puback = block if block_given?
271
+ @handler.on_puback
343
272
  end
344
273
 
345
274
  def on_pubrec(&block)
346
- @on_pubrec = block if block_given?
347
- @on_pubrec
275
+ @handler.on_pubrec = block if block_given?
276
+ @handler.on_pubrec
348
277
  end
349
278
 
350
279
  def on_pubrel(&block)
351
- @on_pubrel = block if block_given?
352
- @on_pubrel
280
+ @handler.on_pubrel = block if block_given?
281
+ @handler.on_pubrel
353
282
  end
354
283
 
355
284
  def on_pubcomp(&block)
356
- @on_pubcomp = block if block_given?
357
- @on_pubcomp
285
+ @handler.on_pubcomp = block if block_given?
286
+ @handler.on_pubcomp
358
287
  end
359
288
 
360
289
  def on_message(&block)
361
- @on_message = block if block_given?
362
- @on_message
363
- end
364
-
365
- private
366
-
367
- def next_packet_id
368
- @last_packet_id = ( @last_packet_id || 0 ).next
369
- end
370
-
371
- def config_subscription
372
- unless @subscribed_topics == []
373
- new_id = next_packet_id
374
- packet = PahoMqtt::Packet::Subscribe.new(
375
- :id => new_id,
376
- :topics => @subscribed_topics
377
- )
378
- @subscribed_mutex.synchronize {
379
- @subscribed_topics = []
380
- }
381
- @suback_mutex.synchronize {
382
- @waiting_suback.push({ :id => new_id, :packet => packet, :timestamp => Time.now })
383
- }
384
- send_packet(packet)
385
- end
386
- MQTT_ERR_SUCCESS
387
- end
388
-
389
- def config_all_message_queue
390
- config_message_queue(@waiting_puback, @puback_mutex, MAX_PUBACK)
391
- config_message_queue(@waiting_pubrec, @pubrec_mutex, MAX_PUBREC)
392
- config_message_queue(@waiting_pubrel, @pubrel_mutex, MAX_PUBREL)
393
- config_message_queue(@waiting_pubcomp, @pubcomp_mutex, MAX_PUBCOMP)
394
- end
395
-
396
- def config_message_queue(queue, mutex, max_packet)
397
- mutex.synchronize {
398
- cnt = 0
399
- queue.each do |pck|
400
- pck[:packet].dup ||= true
401
- if cnt <= max_packet
402
- append_to_writing(pck[:packet])
403
- cnt += 1
404
- end
405
- end
406
- }
407
- end
408
-
409
- def config_socket
410
- unless @socket.nil?
411
- @socket.close unless @socket.closed?
412
- @socket = nil
413
- end
414
-
415
- unless @host.nil? || @port < 0
416
- begin
417
- tcp_socket = TCPSocket.new(@host, @port)
418
- rescue ::Exception => exp
419
- @logger.warn("Could not open a socket with #{@host} on port #{@port}") if @logger.is_a?(Logger)
420
- end
421
- end
422
-
423
- if @ssl
424
- unless @ssl_context.nil?
425
- @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
426
- @socket.sync_close = true
427
- @socket.connect
428
- else
429
- @logger.error("The ssl context was found as nil while the socket's opening.") if @logger.is_a?(Logger)
430
- raise Exception
431
- end
432
- else
433
- @socket = tcp_socket
434
- end
290
+ @handler.on_message = block if block_given?
291
+ @handler.on_message
435
292
  end
436
293
 
437
- def cert=(cert_path)
438
- ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
294
+ def on_connack=(callback)
295
+ @handler.on_connack = callback if callback.is_a?(Proc)
439
296
  end
440
297
 
441
- def key=(key_path)
442
- ssl_context.key = OpenSSL::PKey::RSA.new(File.read(key_path))
298
+ def on_suback=(callback)
299
+ @handler.on_suback = callback if callback.is_a?(Proc)
443
300
  end
444
301
 
445
- def root_ca=(ca_path)
446
- ssl_context.ca_file = ca_path
447
- unless @ca_path.nil?
448
- ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
449
- end
302
+ def on_unsuback=(callback)
303
+ @handler.on_unsuback = callback if callback.is_a?(Proc)
450
304
  end
451
305
 
452
- def setup_connection
453
- @mqtt_thread.kill unless @mqtt_thread.nil?
454
- if @host.nil? || @host == ""
455
- @logger.error("The host was found as nil while the connection setup.") if @logger.is_a?(Logger)
456
- raise ParameterException
457
- end
458
-
459
- if @port.to_i <= 0
460
- @logger.error("The port value is invalid (<= 0). Could not setup the connection.") if @logger.is_a?(Logger)
461
- raise ParameterException
462
- end
463
-
464
- @socket.close unless @socket.nil? || @socket.closed?
465
- @socket = nil
466
-
467
- @last_ping_req = Time.now
468
- @last_ping_resp = Time.now
469
-
470
- @logger.debug("Atempt to connect to host: #{@host}") if @logger.is_a?(Logger)
471
- config_socket
472
-
473
- unless @socket.nil?
474
- send_connect
475
-
476
- # Waiting a Connack packet for "ack_timeout" second from the remote
477
- connect_timeout = Time.now + @ack_timeout
478
- while (Time.now <= connect_timeout) && (!connected?) do
479
- receive_packet
480
- sleep 0.0001
481
- end
482
- end
483
-
484
- unless connected?
485
- @logger.warn("Connection failed. Couldn't recieve a Connack packet from: #{@host}, socket is \"#{@socket}\".") if @logger.is_a?(Logger)
486
- unless Thread.current == @reconnect_thread
487
- raise Exception.new("Connection failed. Check log for more details.")
488
- end
489
- else
490
- config_subscription if Thread.current == @reconnect_thread
491
- end
306
+ def on_puback=(callback)
307
+ @handler.on_puback = callback if callback.is_a?(Proc)
492
308
  end
493
309
 
494
- def check_keep_alive
495
- if @keep_alive >= 0 && connected?
496
- now = Time.now
497
- timeout_req = (@last_ping_req + (@keep_alive * 0.7).ceil)
498
-
499
- if timeout_req <= now && @persistent
500
- send_pingreq
501
- @last_ping_req = now
502
- end
503
-
504
- timeout_resp = @last_ping_resp + (@keep_alive * 1.1).ceil
505
- if timeout_resp <= now
506
- @logger.debug("No activity period over timeout, disconnecting from #{@host}") if @logger.is_a?(Logger)
507
- disconnect(false)
508
- reconnect if @persistent
509
- end
510
- end
310
+ def on_pubrec=(callback)
311
+ @handler.on_pubrec = callback if callback.is_a?(Proc)
511
312
  end
512
313
 
513
- def check_ack_alive(queue, mutex, max_packet)
514
- mutex.synchronize {
515
- now = Time.now
516
- cnt = 0
517
- queue.each do |pck|
518
- if now >= pck[:timestamp] + @ack_timeout
519
- pck[:packet].dup ||= true unless pck[:packet].class == PahoMqtt::Packet::Subscribe || pck[:packet].class == PahoMqtt::Packet::Unsubscribe
520
- unless cnt > max_packet
521
- append_to_writing(pck[:packet])
522
- pck[:timestamp] = now
523
- cnt += 1
524
- end
525
- end
526
- end
527
- }
314
+ def on_pubrel=(callback)
315
+ @handler.on_pubrel = callback if callback.is_a?(Proc)
528
316
  end
529
317
 
530
- def receive_packet
531
- begin
532
- result = IO.select([@socket], [], [], SELECT_TIMEOUT) unless @socket.nil? || @socket.closed?
533
- unless result.nil?
534
- packet = PahoMqtt::Packet::Base.read(@socket)
535
- unless packet.nil?
536
- handle_packet packet
537
- @last_ping_resp = Time.now
538
- end
539
- end
540
- rescue ::Exception => exp
541
- disconnect(false)
542
- if @persistent
543
- reconnect
544
- else
545
- @logger.error("The packet reading have failed.") if @logger.is_a?(Logger)
546
- raise(exp)
547
- end
548
- end
318
+ def on_pubcomp=(callback)
319
+ @handler.on_pubcomp = callback if callback.is_a?(Proc)
549
320
  end
550
321
 
551
- def handle_packet(packet)
552
- @logger.info("New packet #{packet.class} recieved.") if @logger.is_a?(Logger)
553
- if packet.class == PahoMqtt::Packet::Connack
554
- handle_connack(packet)
555
- elsif packet.class == PahoMqtt::Packet::Suback
556
- handle_suback(packet)
557
- elsif packet.class == PahoMqtt::Packet::Unsuback
558
- handle_unsuback(packet)
559
- elsif packet.class == PahoMqtt::Packet::Publish
560
- handle_publish(packet)
561
- elsif packet.class == PahoMqtt::Packet::Puback
562
- handle_puback(packet)
563
- elsif packet.class == PahoMqtt::Packet::Pubrec
564
- handle_pubrec(packet)
565
- elsif packet.class == PahoMqtt::Packet::Pubrel
566
- handle_pubrel(packet)
567
- elsif packet.class == PahoMqtt::Packet::Pubcomp
568
- handle_pubcomp(packet)
569
- elsif packet.class == PahoMqtt::Packet::Pingresp
570
- handle_pingresp
571
- else
572
- @logger.error("The packets header is invalid for packet: #{packet}") if @logger.is_a?(Logger)
573
- raise PacketException
574
- end
322
+ def on_message=(callback)
323
+ @handler.on_message = callback if callback.is_a?(Proc)
575
324
  end
576
325
 
577
- def handle_connack(packet)
578
- if packet.return_code == 0x00
579
- @logger.debug("Connack receive and connection accepted.") if @logger.is_a?(Logger)
580
- if @clean_session && !packet.session_present
581
- @logger.debug("New session created for the client") if @logger.is_a?(Logger)
582
- elsif !@clean_session && !packet.session_present
583
- @logger.debug("No previous session found by server, starting a new one.") if @logger.is_a?(Logger)
584
- elsif !@clean_session && packet.session_present
585
- @logger.debug("Previous session restored by the server.") if @logger.is_a?(Logger)
586
- end
587
- @connection_state_mutex.synchronize{
588
- @connection_state = MQTT_CS_CONNECTED
589
- }
590
- else
591
- disconnect(false)
592
- handle_connack_error(packet.return_code)
593
- end
594
- config_all_message_queue
595
- @writing_mutex.synchronize {
596
- @writing_queue.each do |m|
597
- send_packet(m)
598
- end
599
- }
600
- @on_connack.call(packet) unless @on_connack.nil?
326
+ def registered_callback
327
+ @handler.registered_callback
601
328
  end
602
329
 
603
- def handle_pingresp
604
- @last_ping_resp = Time.now
330
+ def subscribed_topics
331
+ @subscriber.subscribed_topics
605
332
  end
606
333
 
607
- def handle_suback(packet)
608
- adjust_qos = []
609
- max_qos = packet.return_codes
610
- @suback_mutex.synchronize {
611
- adjust_qos, @waiting_suback = @waiting_suback.partition { |pck| pck[:id] == packet.id }
612
- }
613
- if adjust_qos.length == 1
614
- adjust_qos = adjust_qos.first[:packet].topics
615
- adjust_qos.each do |t|
616
- if [0, 1, 2].include?(max_qos[0])
617
- t[1] = max_qos.shift
618
- elsif max_qos[0] == 128
619
- adjust_qos.delete(t)
620
- else
621
- @logger.error("The qos value is invalid in subscribe.") if @logger.is_a?(Logger)
622
- raise PacketException
623
- end
624
- end
625
- else
626
- @logger.error("The packet id is invalid, already used.") if @logger.is_a?(Logger)
627
- raise PacketException
628
- end
629
- @subscribed_mutex.synchronize {
630
- @subscribed_topics.concat(adjust_qos)
631
- }
632
- @on_suback.call(adjust_qos) unless @on_suback.nil?
633
- end
634
334
 
635
- def handle_unsuback(packet)
636
- to_unsub = nil
637
- @unsuback_mutex.synchronize {
638
- to_unsub, @waiting_unsuback = @waiting_unsuback.partition { |pck| pck[:id] == packet.id }
639
- }
640
-
641
- if to_unsub.length == 1
642
- to_unsub = to_unsub.first[:packet].topics
643
- else
644
- @logger.error("The packet id is invalid, already used.") if @logger.is_a?(Logger)
645
- raise PacketException
646
- end
335
+ private
647
336
 
648
- @subscribed_mutex.synchronize {
649
- to_unsub.each do |filter|
650
- @subscribed_topics.delete_if { |topic| match_filter(topic.first, filter.first) }
651
- end
337
+ def next_packet_id
338
+ @id_mutex.synchronize {
339
+ @last_packet_id = ( @last_packet_id || 0 ).next
652
340
  }
653
- @on_unsuback.call(to_unsub) unless @on_unsuback.nil?
654
341
  end
655
342
 
656
- def handle_publish(packet)
657
- case packet.qos
658
- when 0
659
- when 1
660
- send_puback(packet.id)
661
- when 2
662
- send_pubrec(packet.id)
343
+ def downgrade_version
344
+ PahoMqtt.logger.debug("Unable to connect to the server with the version #{@mqtt_version}, trying 3.1") if PahoMqtt.logger?
345
+ if @mqtt_version != "3.1"
346
+ @mqtt_version = "3.1"
347
+ connect(@host, @port, @keep_alive)
663
348
  else
664
- @logger.error("The packet qos value is invalid in publish.") if @logger.is_a?(Logger)
665
- raise PacketException
349
+ raise "Unsupported MQTT version"
666
350
  end
667
-
668
- @on_message.call(packet) unless @on_message.nil?
669
- @registered_callback.assoc(packet.topic).last.call(packet) if @registered_callback.any? { |pair| pair.first == packet.topic}
670
- end
671
-
672
- def handle_puback(packet)
673
- @puback_mutex.synchronize{
674
- @waiting_puback.delete_if { |pck| pck[:id] == packet.id }
675
- }
676
- @on_puback.call(packet) unless @on_puback.nil?
677
- end
678
-
679
- def handle_pubrec(packet)
680
- @pubrec_mutex.synchronize {
681
- @waiting_pubrec.delete_if { |pck| pck[:id] == packet.id }
682
- }
683
- send_pubrel(packet.id)
684
- @on_pubrec.call(packet) unless @on_pubrec.nil?
685
351
  end
686
352
 
687
- def handle_pubrel(packet)
688
- @pubrel_mutex.synchronize {
689
- @waiting_pubrel.delete_if { |pck| pck[:id] == packet.id }
690
- }
691
- send_pubcomp(packet.id)
692
- @on_pubrel.call(packet) unless @on_pubrel.nil?
693
- end
694
-
695
- def handle_pubcomp(packet)
696
- @pubcomp_mutex.synchronize {
697
- @waiting_pubcomp.delete_if { |pck| pck[:id] == packet.id }
698
- }
699
- @on_pubcomp.call(packet) unless @on_pubcomp.nil?
700
- end
701
-
702
- def handle_connack_error(return_code)
703
- case return_code
704
- when 0x01
705
- @logger.debug("Unable to connect to the server with the version #{@mqtt_version}, trying 3.1") if @logger.is_a?(Logger)
706
- if @mqtt_version != "3.1"
707
- @mqtt_version = "3.1"
708
- connect(@host, @port, @keep_alive)
709
- end
710
- when 0x02
711
- @logger.warn("Client Identifier is correct but not allowed by remote server.") if @logger.is_a?(Logger)
712
- MQTT_ERR_FAIL
713
- when 0x03
714
- @logger.warn("Connection established but MQTT service unvailable on remote server.") if @logger.is_a?(Logger)
715
- MQTT_ERR_FAIL
716
- when 0x04
717
- @logger.warn("User name or user password is malformed.") if @logger.is_a?(Logger)
718
- MQTT_ERR_FAIL
719
- when 0x05
720
- @logger.warn("Client is not authorized to connect to the server.") if @logger.is_a?(Logger)
721
- MQTT_ERR_FAIL
722
- end
723
- end
724
-
725
- def send_packet(packet)
726
- begin
727
- @socket.write(packet.to_s) unless @socket.nil? || @socket.closed?
728
- @logger.info("A packet #{packet.class} have been sent.") if @logger.is_a?(Logger)
729
- @last_ping_req = Time.now
730
- MQTT_ERR_SUCCESS
731
- rescue ::Exception => exp
732
- disconnect(false)
733
- if @persistent
734
- reconnect
735
- else
736
- @logger.error("Trying to right a packet on a nil socket.") if @logger.is_a?(Logger)
737
- raise(exp)
738
- end
739
- end
740
- end
741
-
742
- def append_to_writing(packet)
743
- @writing_mutex.synchronize {
744
- @writing_queue.push(packet)
745
- }
746
- MQTT_ERR_SUCCESS
747
- end
748
-
749
- def send_connect
750
- packet = PahoMqtt::Packet::Connect.new(
751
- :version => @mqtt_version,
752
- :clean_session => @clean_session,
753
- :keep_alive => @keep_alive,
754
- :client_id => @client_id,
755
- :username => @username,
756
- :password => @password,
757
- :will_topic => @will_topic,
758
- :will_payload => @will_payload,
759
- :will_qos => @will_qos,
760
- :will_retain => @will_retain
761
- )
762
- send_packet(packet)
763
- MQTT_ERR_SUCCESS
764
- end
765
-
766
- def send_disconnect
767
- packet = PahoMqtt::Packet::Disconnect.new
768
- send_packet(packet)
769
- MQTT_ERR_SUCCESS
770
- end
771
-
772
- def send_pingreq
773
- packet = PahoMqtt::Packet::Pingreq.new
774
- @logger.debug("Checking if server is still alive.") if @logger.is_a?(Logger)
775
- send_packet(packet)
776
- MQTT_ERR_SUCCESS
777
- end
778
-
779
- def send_subscribe(topics)
780
- unless valid_topics?(topics) == MQTT_ERR_FAIL
781
- new_id = next_packet_id
782
- packet = PahoMqtt::Packet::Subscribe.new(
783
- :id => new_id,
784
- :topics => topics
785
- )
786
- append_to_writing(packet)
787
- @suback_mutex.synchronize {
788
- @waiting_suback.push({ :id => new_id, :packet => packet, :timestamp => Time.now })
789
- }
790
- MQTT_ERR_SUCCESS
353
+ def build_pubsub
354
+ if @subscriber.nil?
355
+ @subscriber = Subscriber.new(@sender)
791
356
  else
792
- disconnect(false)
793
- raise ProtocolViolation
357
+ @subscriber.sender = @sender
358
+ @subscriber.config_subscription(next_packet_id)
794
359
  end
795
- end
796
-
797
- def send_unsubscribe(topics)
798
- unless valid_topics?(topics) == MQTT_ERR_FAIL
799
- new_id = next_packet_id
800
- packet = PahoMqtt::Packet::Unsubscribe.new(
801
- :id => new_id,
802
- :topics => topics
803
- )
804
-
805
- append_to_writing(packet)
806
- @unsuback_mutex.synchronize {
807
- @waiting_unsuback.push({:id => new_id, :packet => packet, :timestamp => Time.now})
808
- }
809
- MQTT_ERR_SUCCESS
360
+ if @publisher.nil?
361
+ @publisher = Publisher.new(@sender)
810
362
  else
811
- disconnect(false)
812
- raise ProtocolViolation
363
+ @publisher.sender = @sender
364
+ @publisher.config_all_message_queue
813
365
  end
366
+ @handler.config_pubsub(@publisher, @subscriber)
367
+ @sender.flush_waiting_packet(true)
814
368
  end
815
369
 
816
- def send_publish(topic, payload, retain, qos)
817
- new_id = next_packet_id
818
- packet = PahoMqtt::Packet::Publish.new(
819
- :id => new_id,
820
- :topic => topic,
821
- :payload => payload,
822
- :retain => retain,
823
- :qos => qos
824
- )
825
-
826
- append_to_writing(packet)
827
-
828
- case qos
829
- when 1
830
- @puback_mutex.synchronize{
831
- @waiting_puback.push({:id => new_id, :packet => packet, :timestamp => Time.now})
832
- }
833
- when 2
834
- @pubrec_mutex.synchronize{
835
- @waiting_pubrec.push({:id => new_id, :packet => packet, :timestamp => Time.now})
836
- }
370
+ def init_connection
371
+ unless reconnect?
372
+ @connection_helper = ConnectionHelper.new(@host, @port, @ssl, @ssl_context, @ack_timeout)
373
+ @connection_helper.handler = @handler
374
+ @sender = @connection_helper.sender
837
375
  end
838
- MQTT_ERR_SUCCESS
376
+ @connection_helper.setup_connection
839
377
  end
840
378
 
841
- def send_puback(packet_id)
842
- packet = PahoMqtt::Packet::Puback.new(
843
- :id => packet_id
844
- )
845
-
846
- append_to_writing(packet)
847
- MQTT_ERR_SUCCESS
848
- end
849
-
850
- def send_pubrec(packet_id)
851
- packet = PahoMqtt::Packet::Pubrec.new(
852
- :id => packet_id
853
- )
854
-
855
- append_to_writing(packet)
856
-
857
- @pubrel_mutex.synchronize{
858
- @waiting_pubrel.push({:id => packet_id , :packet => packet, :timestamp => Time.now})
859
- }
860
- MQTT_ERR_SUCCESS
379
+ def session_params
380
+ {:version => @mqtt_version,
381
+ :clean_session => @clean_session,
382
+ :keep_alive => @keep_alive,
383
+ :client_id => @client_id,
384
+ :username => @username,
385
+ :password => @password,
386
+ :will_topic => @will_topic,
387
+ :will_payload => @will_payload,
388
+ :will_qos => @will_qos,
389
+ :will_retain => @will_retain}
861
390
  end
862
-
863
- def send_pubrel(packet_id)
864
- packet = PahoMqtt::Packet::Pubrel.new(
865
- :id => packet_id
866
- )
867
-
868
- append_to_writing(packet)
869
-
870
- @pubcomp_mutex.synchronize{
871
- @waiting_pubcomp.push({:id => packet_id, :packet => packet, :timestamp => Time.now})
872
- }
873
- MQTT_ERR_SUCCESS
874
- end
875
-
876
- def send_pubcomp(packet_id)
877
- packet = PahoMqtt::Packet::Pubcomp.new(
878
- :id => packet_id
879
- )
880
391
 
881
- append_to_writing(packet)
882
- MQTT_ERR_SUCCESS
883
- end
884
-
885
- def valid_topics?(topics)
886
- unless topics.length == 0
887
- topics.map do |topic|
888
- return MQTT_ERR_FAIL if topic.first == ""
889
- end
890
- else
891
- MQTT_ERR_FAIL
892
- end
893
- MQTT_ERR_SUCCESS
894
- end
895
-
896
- def match_filter(topics, filters)
897
- if topics.is_a?(String) && filters.is_a?(String)
898
- topic = topics.split('/')
899
- filter = filters.split('/')
900
- else
901
- @logger.error("Topics and filters are not found as String while matching topics to filter.") if @logger.is_a?(Logger)
902
- raise ParameterException
903
- end
904
-
905
- rc = false
906
- index = 0
907
-
908
- while index < [topic.length, filter.length].max do
909
- if topic[index].nil? || filter[index].nil?
910
- break
911
- elsif filter[index] == '#' && index == (filter.length - 1)
912
- rc = true
913
- break
914
- elsif filter[index] == topic[index] || filter[index] == '+'
915
- index = index + 1
916
- else
917
- break
918
- end
919
- end
920
- rc ||= (index == [topic.length, filter.length].max)
392
+ def check_persistence
393
+ disconnect(false)
394
+ @persistent
921
395
  end
922
396
  end
923
397
  end