apiotics-paho-mqtt 1.0.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.
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