apiotics-paho-mqtt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/Gemfile +4 -0
  7. data/README.md +322 -0
  8. data/Rakefile +6 -0
  9. data/apiotics-paho-mqtt.gemspec +33 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/lib/paho-mqtt.rb +165 -0
  13. data/lib/paho_mqtt/client.rb +417 -0
  14. data/lib/paho_mqtt/connection_helper.rb +169 -0
  15. data/lib/paho_mqtt/exception.rb +43 -0
  16. data/lib/paho_mqtt/handler.rb +273 -0
  17. data/lib/paho_mqtt/packet/base.rb +315 -0
  18. data/lib/paho_mqtt/packet/connack.rb +102 -0
  19. data/lib/paho_mqtt/packet/connect.rb +183 -0
  20. data/lib/paho_mqtt/packet/disconnect.rb +38 -0
  21. data/lib/paho_mqtt/packet/pingreq.rb +29 -0
  22. data/lib/paho_mqtt/packet/pingresp.rb +38 -0
  23. data/lib/paho_mqtt/packet/puback.rb +44 -0
  24. data/lib/paho_mqtt/packet/pubcomp.rb +44 -0
  25. data/lib/paho_mqtt/packet/publish.rb +148 -0
  26. data/lib/paho_mqtt/packet/pubrec.rb +44 -0
  27. data/lib/paho_mqtt/packet/pubrel.rb +62 -0
  28. data/lib/paho_mqtt/packet/suback.rb +75 -0
  29. data/lib/paho_mqtt/packet/subscribe.rb +124 -0
  30. data/lib/paho_mqtt/packet/unsuback.rb +49 -0
  31. data/lib/paho_mqtt/packet/unsubscribe.rb +84 -0
  32. data/lib/paho_mqtt/packet.rb +33 -0
  33. data/lib/paho_mqtt/publisher.rb +191 -0
  34. data/lib/paho_mqtt/sender.rb +86 -0
  35. data/lib/paho_mqtt/ssl_helper.rb +42 -0
  36. data/lib/paho_mqtt/subscriber.rb +163 -0
  37. data/lib/paho_mqtt/version.rb +3 -0
  38. data/samples/client_blocking(reading).rb +30 -0
  39. data/samples/client_blocking(writing).rb +18 -0
  40. data/samples/getting_started.rb +49 -0
  41. data/samples/test_client.rb +70 -0
  42. metadata +127 -0
@@ -0,0 +1,417 @@
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'
21
+
22
+ module PahoMqtt
23
+ class Client
24
+ # Connection related attributes:
25
+ attr_accessor :host
26
+ attr_accessor :port
27
+ attr_accessor :mqtt_version
28
+ attr_accessor :clean_session
29
+ attr_accessor :persistent
30
+ attr_accessor :reconnect_limit
31
+ attr_accessor :reconnect_delay
32
+ attr_accessor :blocking
33
+ attr_accessor :client_id
34
+ attr_accessor :username
35
+ attr_accessor :password
36
+ attr_accessor :ssl
37
+
38
+ # Last will attributes:
39
+ attr_accessor :will_topic
40
+ attr_accessor :will_payload
41
+ attr_accessor :will_qos
42
+ attr_accessor :will_retain
43
+
44
+ # Timeout attributes:
45
+ attr_accessor :keep_alive
46
+ attr_accessor :ack_timeout
47
+
48
+ #Read Only attribute
49
+ attr_reader :connection_state
50
+ attr_reader :ssl_context
51
+
52
+ def initialize(*args)
53
+ @last_ping_resp = Time.now
54
+ @last_packet_id = 0
55
+ @ssl_context = nil
56
+ @sender = nil
57
+ @handler = Handler.new
58
+ @connection_helper = nil
59
+ @connection_state = MQTT_CS_DISCONNECT
60
+ @connection_state_mutex = Mutex.new
61
+ @mqtt_thread = nil
62
+ @reconnect_thread = nil
63
+ @id_mutex = Mutex.new
64
+ @reconnect_limit = 3
65
+ @reconnect_delay = 5
66
+
67
+ if args.last.is_a?(Hash)
68
+ attr = args.pop
69
+ else
70
+ attr = {}
71
+ end
72
+
73
+ CLIENT_ATTR_DEFAULTS.merge(attr).each_pair do |k,v|
74
+ self.send("#{k}=", v)
75
+ end
76
+
77
+ if @ssl
78
+ @ssl_context = OpenSSL::SSL::SSLContext.new
79
+ end
80
+
81
+ if @port.nil?
82
+ if @ssl
83
+ @port = DEFAULT_SSL_PORT
84
+ else
85
+ @port = DEFAULT_PORT
86
+ end
87
+ end
88
+
89
+ if @client_id.nil? || @client_id == ""
90
+ @client_id = generate_client_id
91
+ end
92
+ end
93
+
94
+ def generate_client_id(prefix='paho_ruby', lenght=16)
95
+ charset = Array('A'..'Z') + Array('a'..'z') + Array('0'..'9')
96
+ @client_id = prefix << Array.new(lenght) { charset.sample }.join
97
+ end
98
+
99
+ def config_ssl_context(cert_path, key_path, ca_path=nil)
100
+ @ssl ||= true
101
+ @ssl_context = SSLHelper.config_ssl_context(cert_path, key_path, ca_path)
102
+ end
103
+
104
+ def connect(host=@host, port=@port, keep_alive=@keep_alive, persistent=@persistent, blocking=@blocking)
105
+ @persistent = persistent
106
+ @blocking = blocking
107
+ @host = host
108
+ @port = port.to_i
109
+ @keep_alive = keep_alive
110
+ @connection_state_mutex.synchronize do
111
+ @connection_state = MQTT_CS_NEW
112
+ end
113
+ @mqtt_thread.kill unless @mqtt_thread.nil?
114
+
115
+ init_connection
116
+ @connection_helper.send_connect(session_params)
117
+ begin
118
+ @connection_state = @connection_helper.do_connect(reconnect?)
119
+ if connected?
120
+ build_pubsub
121
+ daemon_mode unless @blocking
122
+ end
123
+ rescue LowVersionException
124
+ downgrade_version
125
+ end
126
+ end
127
+
128
+ def daemon_mode
129
+ @mqtt_thread = Thread.new do
130
+ @reconnect_thread.kill unless @reconnect_thread.nil? || !@reconnect_thread.alive?
131
+ begin
132
+ while connected? do
133
+ mqtt_loop
134
+ end
135
+ rescue SystemCallError => e
136
+ if @persistent
137
+ reconnect
138
+ else
139
+ raise e
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ def connected?
146
+ @connection_state == MQTT_CS_CONNECTED
147
+ end
148
+
149
+ def reconnect?
150
+ Thread.current == @reconnect_thread
151
+ end
152
+
153
+ def loop_write(max_packet=MAX_WRITING)
154
+ begin
155
+ @sender.writing_loop(max_packet)
156
+ rescue WritingException
157
+ if check_persistence
158
+ reconnect
159
+ else
160
+ raise WritingException
161
+ end
162
+ end
163
+ end
164
+
165
+ def loop_read(max_packet=MAX_READ)
166
+ max_packet.times do
167
+ begin
168
+ @handler.receive_packet
169
+ rescue ReadingException
170
+ if check_persistence
171
+ reconnect
172
+ else
173
+ raise ReadingException
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ def mqtt_loop
180
+ loop_read
181
+ loop_write
182
+ loop_misc
183
+ sleep LOOP_TEMPO
184
+ end
185
+
186
+ def loop_misc
187
+ if @connection_helper.check_keep_alive(@persistent, @handler.last_ping_resp, @keep_alive) == MQTT_CS_DISCONNECT
188
+ reconnect if check_persistence
189
+ end
190
+ @publisher.check_waiting_publisher
191
+ @subscriber.check_waiting_subscriber
192
+ end
193
+
194
+ def reconnect
195
+ @reconnect_thread = Thread.new do
196
+ counter = 0
197
+ while (@reconnect_limit >= counter || @reconnect_limit == -1) do
198
+ counter += 1
199
+ PahoMqtt.logger.debug("New reconnect attempt...") if PahoMqtt.logger?
200
+ connect
201
+ if connected?
202
+ break
203
+ else
204
+ sleep @reconnect_delay
205
+ end
206
+ end
207
+ unless connected?
208
+ PahoMqtt.logger.error("Reconnection attempt counter is over. (#{@reconnect_limit} times)") if PahoMqtt.logger?
209
+ disconnect(false)
210
+ end
211
+ end
212
+ end
213
+
214
+ def disconnect(explicit=true)
215
+ @last_packet_id = 0 if explicit
216
+ @connection_helper.do_disconnect(@publisher, explicit, @mqtt_thread)
217
+ @connection_state_mutex.synchronize do
218
+ @connection_state = MQTT_CS_DISCONNECT
219
+ end
220
+ MQTT_ERR_SUCCESS
221
+ end
222
+
223
+ def publish(topic, payload="", retain=false, qos=0)
224
+ if topic == "" || !topic.is_a?(String)
225
+ PahoMqtt.logger.error("Publish topics is invalid, not a string or empty.") if PahoMqtt.logger?
226
+ raise ArgumentError
227
+ end
228
+ id = next_packet_id
229
+ @publisher.send_publish(topic, payload, retain, qos, id)
230
+ end
231
+
232
+ def subscribe(*topics)
233
+ begin
234
+ id = next_packet_id
235
+ unless @subscriber.send_subscribe(topics, id) == PahoMqtt::MQTT_ERR_SUCCESS
236
+ reconnect if check_persistence
237
+ end
238
+ MQTT_ERR_SUCCESS
239
+ rescue ProtocolViolation
240
+ PahoMqtt.logger.error("Subscribe topics need one topic or a list of topics.") if PahoMqtt.logger?
241
+ disconnect(false)
242
+ raise ProtocolViolation
243
+ end
244
+ end
245
+
246
+ def unsubscribe(*topics)
247
+ begin
248
+ id = next_packet_id
249
+ unless @subscriber.send_unsubscribe(topics, id) == MQTT_ERR_SUCCESS
250
+ reconnect if check_persistence
251
+ end
252
+ MQTT_ERR_SUCCESS
253
+ rescue ProtocolViolation
254
+ PahoMqtt.logger.error("Unsubscribe need at least one topic.") if PahoMqtt.logger?
255
+ disconnect(false)
256
+ raise ProtocolViolation
257
+ end
258
+ end
259
+
260
+ def ping_host
261
+ @sender.send_pingreq
262
+ end
263
+
264
+ def add_topic_callback(topic, callback=nil, &block)
265
+ @handler.register_topic_callback(topic, callback, &block)
266
+ end
267
+
268
+ def remove_topic_callback(topic)
269
+ @handler.clear_topic_callback(topic)
270
+ end
271
+
272
+ def on_connack(&block)
273
+ @handler.on_connack = block if block_given?
274
+ @handler.on_connack
275
+ end
276
+
277
+ def on_suback(&block)
278
+ @handler.on_suback = block if block_given?
279
+ @handler.on_suback
280
+ end
281
+
282
+ def on_unsuback(&block)
283
+ @handler.on_unsuback = block if block_given?
284
+ @handler.on_unsuback
285
+ end
286
+
287
+ def on_puback(&block)
288
+ @handler.on_puback = block if block_given?
289
+ @handler.on_puback
290
+ end
291
+
292
+ def on_pubrec(&block)
293
+ @handler.on_pubrec = block if block_given?
294
+ @handler.on_pubrec
295
+ end
296
+
297
+ def on_pubrel(&block)
298
+ @handler.on_pubrel = block if block_given?
299
+ @handler.on_pubrel
300
+ end
301
+
302
+ def on_pubcomp(&block)
303
+ @handler.on_pubcomp = block if block_given?
304
+ @handler.on_pubcomp
305
+ end
306
+
307
+ def on_message(&block)
308
+ @handler.on_message = block if block_given?
309
+ @handler.on_message
310
+ end
311
+
312
+ def on_connack=(callback)
313
+ @handler.on_connack = callback if callback.is_a?(Proc)
314
+ end
315
+
316
+ def on_suback=(callback)
317
+ @handler.on_suback = callback if callback.is_a?(Proc)
318
+ end
319
+
320
+ def on_unsuback=(callback)
321
+ @handler.on_unsuback = callback if callback.is_a?(Proc)
322
+ end
323
+
324
+ def on_puback=(callback)
325
+ @handler.on_puback = callback if callback.is_a?(Proc)
326
+ end
327
+
328
+ def on_pubrec=(callback)
329
+ @handler.on_pubrec = callback if callback.is_a?(Proc)
330
+ end
331
+
332
+ def on_pubrel=(callback)
333
+ @handler.on_pubrel = callback if callback.is_a?(Proc)
334
+ end
335
+
336
+ def on_pubcomp=(callback)
337
+ @handler.on_pubcomp = callback if callback.is_a?(Proc)
338
+ end
339
+
340
+ def on_message=(callback)
341
+ @handler.on_message = callback if callback.is_a?(Proc)
342
+ end
343
+
344
+ def registered_callback
345
+ @handler.registered_callback
346
+ end
347
+
348
+ def subscribed_topics
349
+ @subscriber.subscribed_topics
350
+ end
351
+
352
+
353
+ private
354
+
355
+ def next_packet_id
356
+ @id_mutex.synchronize do
357
+ @last_packet_id = (@last_packet_id || 0).next
358
+ end
359
+ end
360
+
361
+ def downgrade_version
362
+ PahoMqtt.logger.debug("Connection refused: unacceptable protocol version #{@mqtt_version}, trying 3.1") if PahoMqtt.logger?
363
+ if @mqtt_version != "3.1"
364
+ @mqtt_version = "3.1"
365
+ connect(@host, @port, @keep_alive)
366
+ else
367
+ raise ProtocolVersionException.new("Unsupported MQTT version")
368
+ end
369
+ end
370
+
371
+ def build_pubsub
372
+ if @subscriber.nil?
373
+ @subscriber = Subscriber.new(@sender)
374
+ else
375
+ @subscriber.sender = @sender
376
+ @subscriber.config_subscription(next_packet_id)
377
+ end
378
+ if @publisher.nil?
379
+ @publisher = Publisher.new(@sender)
380
+ else
381
+ @publisher.sender = @sender
382
+ @sender.flush_waiting_packet
383
+ @publisher.config_all_message_queue
384
+ end
385
+ @handler.config_pubsub(@publisher, @subscriber)
386
+ end
387
+
388
+ def init_connection
389
+ unless reconnect?
390
+ @connection_helper = ConnectionHelper.new(@host, @port, @ssl, @ssl_context, @ack_timeout)
391
+ @connection_helper.handler = @handler
392
+ @sender = @connection_helper.sender
393
+ end
394
+ @connection_helper.setup_connection
395
+ end
396
+
397
+ def session_params
398
+ {
399
+ :version => @mqtt_version,
400
+ :clean_session => @clean_session,
401
+ :keep_alive => @keep_alive,
402
+ :client_id => @client_id,
403
+ :username => @username,
404
+ :password => @password,
405
+ :will_topic => @will_topic,
406
+ :will_payload => @will_payload,
407
+ :will_qos => @will_qos,
408
+ :will_retain => @will_retain
409
+ }
410
+ end
411
+
412
+ def check_persistence
413
+ disconnect(false)
414
+ @persistent
415
+ end
416
+ end
417
+ end
@@ -0,0 +1,169 @@
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 'socket'
16
+
17
+ module PahoMqtt
18
+ class ConnectionHelper
19
+
20
+ attr_accessor :sender
21
+
22
+ def initialize(host, port, ssl, ssl_context, ack_timeout)
23
+ @cs = MQTT_CS_DISCONNECT
24
+ @socket = nil
25
+ @host = host
26
+ @port = port
27
+ @ssl = ssl
28
+ @ssl_context = ssl_context
29
+ @ack_timeout = ack_timeout
30
+ @sender = Sender.new(ack_timeout)
31
+ end
32
+
33
+ def handler=(handler)
34
+ @handler = handler
35
+ end
36
+
37
+ def do_connect(reconnection=false)
38
+ @cs = MQTT_CS_NEW
39
+ @handler.socket = @socket
40
+ # Waiting a Connack packet for "ack_timeout" second from the remote
41
+ connect_timeout = Time.now + @ack_timeout
42
+ while (Time.now <= connect_timeout) && !is_connected? do
43
+ @cs = @handler.receive_packet
44
+ sleep 0.0001
45
+ end
46
+ unless is_connected?
47
+ PahoMqtt.logger.warn("Connection failed. Couldn't recieve a Connack packet from: #{@host}.") if PahoMqtt.logger?
48
+ raise Exception.new("Connection failed. Check log for more details.") unless reconnection
49
+ end
50
+ @cs
51
+ end
52
+
53
+ def is_connected?
54
+ @cs == MQTT_CS_CONNECTED
55
+ end
56
+
57
+ def do_disconnect(publisher, explicit, mqtt_thread)
58
+ PahoMqtt.logger.debug("Disconnecting from #{@host}.") if PahoMqtt.logger?
59
+ if explicit
60
+ explicit_disconnect(publisher, mqtt_thread)
61
+ end
62
+ @socket.close unless @socket.nil? || @socket.closed?
63
+ @socket = nil
64
+ end
65
+
66
+ def explicit_disconnect(publisher, mqtt_thread)
67
+ @sender.flush_waiting_packet
68
+ send_disconnect
69
+ mqtt_thread.kill if mqtt_thread && mqtt_thread.alive?
70
+ publisher.flush_publisher unless publisher.nil?
71
+ end
72
+
73
+ def setup_connection
74
+ clean_start(@host, @port)
75
+ config_socket
76
+ unless @socket.nil?
77
+ @sender.socket = @socket
78
+ end
79
+ end
80
+
81
+ def config_socket
82
+ PahoMqtt.logger.debug("Attempt to connect to host: #{@host}...") if PahoMqtt.logger?
83
+ begin
84
+ tcp_socket = TCPSocket.new(@host, @port)
85
+ rescue StandardError
86
+ PahoMqtt.logger.warn("Could not open a socket with #{@host} on port #{@port}.") if PahoMqtt.logger?
87
+ end
88
+ if @ssl
89
+ encrypted_socket(tcp_socket, @ssl_context)
90
+ else
91
+ @socket = tcp_socket
92
+ end
93
+ end
94
+
95
+ def encrypted_socket(tcp_socket, ssl_context)
96
+ unless ssl_context.nil?
97
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
98
+ @socket.sync_close = true
99
+ @socket.connect
100
+ else
101
+ PahoMqtt.logger.error("The SSL context was found as nil while the socket's opening.") if PahoMqtt.logger?
102
+ raise Exception
103
+ end
104
+ end
105
+
106
+ def clean_start(host, port)
107
+ self.host = host
108
+ self.port = port
109
+ unless @socket.nil?
110
+ @socket.close unless @socket.closed?
111
+ @socket = nil
112
+ end
113
+ end
114
+
115
+ def host=(host)
116
+ if host.nil? || host == ""
117
+ PahoMqtt.logger.error("The host was found as nil while the connection setup.") if PahoMqtt.logger?
118
+ raise ArgumentError
119
+ else
120
+ @host = host
121
+ end
122
+ end
123
+
124
+ def port=(port)
125
+ if port.to_i <= 0
126
+ PahoMqtt.logger.error("The port value is invalid (<= 0). Could not setup the connection.") if PahoMqtt.logger?
127
+ raise ArgumentError
128
+ else
129
+ @port = port
130
+ end
131
+ end
132
+
133
+ def send_connect(session_params)
134
+ setup_connection
135
+ packet = PahoMqtt::Packet::Connect.new(session_params)
136
+ @handler.clean_session = session_params[:clean_session]
137
+ @sender.send_packet(packet)
138
+ MQTT_ERR_SUCCESS
139
+ end
140
+
141
+ def send_disconnect
142
+ packet = PahoMqtt::Packet::Disconnect.new
143
+ @sender.send_packet(packet)
144
+ MQTT_ERR_SUCCESS
145
+ end
146
+
147
+ def send_pingreq
148
+ packet = PahoMqtt::Packet::Pingreq.new
149
+ @sender.send_packet(packet)
150
+ MQTT_ERR_SUCCESS
151
+ end
152
+
153
+ def check_keep_alive(persistent, last_ping_resp, keep_alive)
154
+ now = Time.now
155
+ timeout_req = (@sender.last_ping_req + (keep_alive * 0.7).ceil)
156
+ if timeout_req <= now && persistent
157
+ PahoMqtt.logger.debug("Checking if server is still alive...") if PahoMqtt.logger?
158
+ send_pingreq
159
+ end
160
+ timeout_resp = last_ping_resp + (keep_alive * 1.1).ceil
161
+ #if timeout_resp <= now
162
+ if false #dirty hack to disable the ping timeout. we are using a ping to a topic every 30 secs instead of this.
163
+ PahoMqtt.logger.debug("No activity period over timeout, disconnecting from #{@host}.") if PahoMqtt.logger?
164
+ @cs = MQTT_CS_DISCONNECT
165
+ end
166
+ @cs
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,43 @@
1
+ # Copyright (c) 2016-2018 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
+
16
+ module PahoMqtt
17
+ class Exception < ::StandardError
18
+ def initialize(msg="")
19
+ super
20
+ end
21
+ end
22
+
23
+ class ProtocolViolation < Exception
24
+ end
25
+
26
+ class WritingException < Exception
27
+ end
28
+
29
+ class ReadingException < Exception
30
+ end
31
+
32
+ class PacketException < Exception
33
+ end
34
+
35
+ class PacketFormatException < Exception
36
+ end
37
+
38
+ class ProtocolVersionException < Exception
39
+ end
40
+
41
+ class LowVersionException < Exception
42
+ end
43
+ end