mqtt-rails 1.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -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/LICENSE +210 -0
  8. data/README.md +323 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/lib/mqtt-rails.rb +144 -0
  13. data/lib/mqtt_rails/client.rb +414 -0
  14. data/lib/mqtt_rails/connection_helper.rb +172 -0
  15. data/lib/mqtt_rails/exception.rb +52 -0
  16. data/lib/mqtt_rails/handler.rb +274 -0
  17. data/lib/mqtt_rails/packet.rb +33 -0
  18. data/lib/mqtt_rails/packet/base.rb +315 -0
  19. data/lib/mqtt_rails/packet/connack.rb +102 -0
  20. data/lib/mqtt_rails/packet/connect.rb +183 -0
  21. data/lib/mqtt_rails/packet/disconnect.rb +38 -0
  22. data/lib/mqtt_rails/packet/pingreq.rb +29 -0
  23. data/lib/mqtt_rails/packet/pingresp.rb +38 -0
  24. data/lib/mqtt_rails/packet/puback.rb +44 -0
  25. data/lib/mqtt_rails/packet/pubcomp.rb +44 -0
  26. data/lib/mqtt_rails/packet/publish.rb +148 -0
  27. data/lib/mqtt_rails/packet/pubrec.rb +44 -0
  28. data/lib/mqtt_rails/packet/pubrel.rb +62 -0
  29. data/lib/mqtt_rails/packet/suback.rb +75 -0
  30. data/lib/mqtt_rails/packet/subscribe.rb +124 -0
  31. data/lib/mqtt_rails/packet/unsuback.rb +49 -0
  32. data/lib/mqtt_rails/packet/unsubscribe.rb +84 -0
  33. data/lib/mqtt_rails/publisher.rb +181 -0
  34. data/lib/mqtt_rails/sender.rb +129 -0
  35. data/lib/mqtt_rails/ssl_helper.rb +61 -0
  36. data/lib/mqtt_rails/subscriber.rb +166 -0
  37. data/lib/mqtt_rails/version.rb +3 -0
  38. data/mqtt-rails.gemspec +33 -0
  39. data/samples/client_blocking(reading).rb +29 -0
  40. data/samples/client_blocking(writing).rb +18 -0
  41. data/samples/getting_started.rb +49 -0
  42. data/samples/test_client.rb +69 -0
  43. metadata +126 -0
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mqtt-rails"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/mqtt-rails.rb ADDED
@@ -0,0 +1,144 @@
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 "mqtt_rails/version"
16
+ require "mqtt_rails/client"
17
+ require "mqtt_rails/exception"
18
+ require "mqtt_rails/packet"
19
+
20
+ module MqttRails
21
+ extend self
22
+
23
+ MAX_PACKET_ID = 65535
24
+
25
+ # Default connection setup
26
+ DEFAULT_SSL_PORT = 8883
27
+ DEFAULT_PORT = 1883
28
+ SELECT_TIMEOUT = 0.002
29
+
30
+ # MAX size of queue
31
+ MAX_SUBACK = 10
32
+ MAX_UNSUBACK = 10
33
+ MAX_PUBLISH = 1000
34
+ MAX_QUEUE = 1000
35
+
36
+ # Connection states values
37
+ MQTT_CS_NEW = 0
38
+ MQTT_CS_CONNECTED = 1
39
+ MQTT_CS_DISCONNECT = 2
40
+
41
+ # Error values
42
+ MQTT_ERR_SUCCESS = 0
43
+ MQTT_ERR_FAIL = 1
44
+
45
+ PACKET_TYPES = [
46
+ nil,
47
+ MqttRails::Packet::Connect,
48
+ MqttRails::Packet::Connack,
49
+ MqttRails::Packet::Publish,
50
+ MqttRails::Packet::Puback,
51
+ MqttRails::Packet::Pubrec,
52
+ MqttRails::Packet::Pubrel,
53
+ MqttRails::Packet::Pubcomp,
54
+ MqttRails::Packet::Subscribe,
55
+ MqttRails::Packet::Suback,
56
+ MqttRails::Packet::Unsubscribe,
57
+ MqttRails::Packet::Unsuback,
58
+ MqttRails::Packet::Pingreq,
59
+ MqttRails::Packet::Pingresp,
60
+ MqttRails::Packet::Disconnect,
61
+ nil
62
+ ]
63
+
64
+ CONNACK_ERROR_MESSAGE = {
65
+ 0x02 => "Client Identifier is correct but not allowed by remote server.",
66
+ 0x03 => "Connection established but MQTT service unvailable on remote server.",
67
+ 0x04 => "User name or user password is malformed.",
68
+ 0x05 => "Client is not authorized to connect to the server."
69
+ }
70
+
71
+ CLIENT_ATTR_DEFAULTS = {
72
+ :host => "",
73
+ :port => nil,
74
+ :mqtt_version => '3.1.1',
75
+ :clean_session => true,
76
+ :persistent => false,
77
+ :blocking => false,
78
+ :client_id => nil,
79
+ :username => nil,
80
+ :password => nil,
81
+ :ssl => false,
82
+ :will_topic => nil,
83
+ :will_payload => nil,
84
+ :will_qos => 0,
85
+ :will_retain => false,
86
+ :keep_alive => 60,
87
+ :ack_timeout => 5,
88
+ :on_connack => nil,
89
+ :on_suback => nil,
90
+ :on_unsuback => nil,
91
+ :on_puback => nil,
92
+ :on_pubrel => nil,
93
+ :on_pubrec => nil,
94
+ :on_pubcomp => nil,
95
+ :on_message => nil,
96
+ }
97
+
98
+ Thread.abort_on_exception = true
99
+
100
+ def match_filter(topics, filters)
101
+ check_topics(topics, filters)
102
+ index = 0
103
+ rc = false
104
+ topic = topics.split('/')
105
+ filter = filters.split('/')
106
+ while index < [topic.length, filter.length].max do
107
+ if is_end?(topic[index], filter[index])
108
+ break
109
+ elsif is_wildcard?(filter[index])
110
+ rc = index == (filter.length - 1)
111
+ break
112
+ elsif keep_running?(filter[index], topic[index])
113
+ index = index + 1
114
+ else
115
+ break
116
+ end
117
+ end
118
+ is_matching?(rc, topic.length, filter.length, index)
119
+ end
120
+
121
+ def keep_running?(filter_part, topic_part)
122
+ filter_part == topic_part || filter_part == '+'
123
+ end
124
+
125
+ def is_wildcard?(filter_part)
126
+ filter_part == '#'
127
+ end
128
+
129
+ def is_end?(topic_part, filter_part)
130
+ topic_part.nil? || filter_part.nil?
131
+ end
132
+
133
+ def is_matching?(rc, topic_length, filter_length, index)
134
+ rc || index == [topic_length, filter_length].max
135
+ end
136
+
137
+ def check_topics(topics, filters)
138
+ if topics.is_a?(String) && filters.is_a?(String)
139
+ else
140
+ Rails.logger.error("Topics or Wildcards are not found as String.")
141
+ raise ArgumentError
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,414 @@
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 'mqtt_rails/handler'
16
+ require 'mqtt_rails/connection_helper'
17
+ require 'mqtt_rails/sender'
18
+ require 'mqtt_rails/publisher'
19
+ require 'mqtt_rails/subscriber'
20
+ require 'mqtt_rails/ssl_helper'
21
+
22
+ module MqttRails
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_packet_id = 0
54
+ @ssl_context = nil
55
+ @sender = nil
56
+ @handler = Handler.new
57
+ @connection_helper = nil
58
+ @connection_state = MQTT_CS_DISCONNECT
59
+ @connection_state_mutex = Mutex.new
60
+ @mqtt_thread = nil
61
+ @reconnect_thread = nil
62
+ @id_mutex = Mutex.new
63
+ @reconnect_limit = 3
64
+ @reconnect_delay = 5
65
+
66
+ if args.last.is_a?(Hash)
67
+ attr = args.pop
68
+ else
69
+ attr = {}
70
+ end
71
+
72
+ CLIENT_ATTR_DEFAULTS.merge(attr).each_pair do |k,v|
73
+ self.send("#{k}=", v)
74
+ end
75
+
76
+ if @ssl
77
+ @ssl_context = OpenSSL::SSL::SSLContext.new
78
+ end
79
+
80
+ if @port.nil?
81
+ if @ssl
82
+ @port = DEFAULT_SSL_PORT
83
+ else
84
+ @port = DEFAULT_PORT
85
+ end
86
+ end
87
+
88
+ if @client_id.nil? || @client_id == ""
89
+ @client_id = generate_client_id
90
+ end
91
+ end
92
+
93
+ def generate_client_id(prefix='paho_ruby', lenght=16)
94
+ charset = Array('A'..'Z') + Array('a'..'z') + Array('0'..'9')
95
+ @client_id = prefix << Array.new(lenght) { charset.sample }.join
96
+ end
97
+
98
+ def config_ssl_context(cert_path, key_path, ca_path=nil)
99
+ @ssl ||= true
100
+ @ssl_context = SSLHelper.config_ssl_context(cert_path, key_path, ca_path)
101
+ end
102
+
103
+ def connect(host=@host, port=@port, keep_alive=@keep_alive, persistent=@persistent, blocking=@blocking)
104
+ @persistent = persistent
105
+ @blocking = blocking
106
+ @host = host
107
+ @port = port.to_i
108
+ @keep_alive = keep_alive
109
+ @connection_state_mutex.synchronize do
110
+ @connection_state = MQTT_CS_NEW
111
+ end
112
+ @mqtt_thread.kill unless @mqtt_thread.nil?
113
+
114
+ init_connection unless reconnect?
115
+ @connection_helper.send_connect(session_params)
116
+ begin
117
+ init_pubsub
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
154
+ begin
155
+ @sender.writing_loop
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
166
+ begin
167
+ MAX_QUEUE.times do
168
+ result = @handler.receive_packet
169
+ break if result.nil?
170
+ end
171
+ rescue FullQueueException
172
+ Rails.logger.warn("Early exit in reading loop. The maximum packets have been reach for #{packet.type_name}")
173
+ rescue ReadingException
174
+ if check_persistence
175
+ reconnect
176
+ else
177
+ raise ReadingException
178
+ end
179
+ end
180
+ end
181
+
182
+ def mqtt_loop
183
+ loop_read
184
+ loop_write
185
+ loop_misc
186
+ end
187
+
188
+ def loop_misc
189
+ if @connection_helper.check_keep_alive(@persistent, @keep_alive) == MQTT_CS_DISCONNECT
190
+ reconnect if check_persistence
191
+ end
192
+ @publisher.check_waiting_publisher
193
+ @subscriber.check_waiting_subscriber
194
+ sleep SELECT_TIMEOUT
195
+ end
196
+
197
+ def reconnect
198
+ @reconnect_thread = Thread.new do
199
+ counter = 0
200
+ while (@reconnect_limit >= counter || @reconnect_limit == -1) do
201
+ counter += 1
202
+ Rails.logger.info("New reconnect attempt...")
203
+ connect
204
+ if connected?
205
+ break
206
+ else
207
+ sleep @reconnect_delay
208
+ end
209
+ end
210
+ unless connected?
211
+ Rails.logger.error("Reconnection attempt counter is over. (#{@reconnect_limit} times)")
212
+ disconnect(false)
213
+ end
214
+ end
215
+ end
216
+
217
+ def disconnect(explicit=true)
218
+ @connection_helper.do_disconnect(@publisher, explicit, @mqtt_thread)
219
+ @connection_state_mutex.synchronize do
220
+ @connection_state = MQTT_CS_DISCONNECT
221
+ end
222
+ if explicit && @clean_session
223
+ @last_packet_id = 0
224
+ @subscriber.clear_queue
225
+ end
226
+ MQTT_ERR_SUCCESS
227
+ end
228
+
229
+ def publish(topic, payload="", retain=false, qos=0)
230
+ if topic == "" || !topic.is_a?(String)
231
+ Rails.logger.error("Publish topics is invalid, not a string or empty.")
232
+ raise ArgumentError
233
+ end
234
+ id = next_packet_id
235
+ @publisher.send_publish(topic, payload, retain, qos, id)
236
+ end
237
+
238
+ def subscribe(*topics)
239
+ begin
240
+ id = next_packet_id
241
+ unless @subscriber.send_subscribe(topics, id) == MqttRails::MQTT_ERR_SUCCESS
242
+ reconnect if check_persistence
243
+ end
244
+ MQTT_ERR_SUCCESS
245
+ rescue ProtocolViolation
246
+ Rails.logger.error("Subscribe topics need one topic or a list of topics.")
247
+ raise ProtocolViolation
248
+ end
249
+ end
250
+
251
+ def unsubscribe(*topics)
252
+ begin
253
+ id = next_packet_id
254
+ unless @subscriber.send_unsubscribe(topics, id) == MQTT_ERR_SUCCESS
255
+ reconnect if check_persistence
256
+ end
257
+ MQTT_ERR_SUCCESS
258
+ rescue ProtocolViolation
259
+ Rails.logger.error("Unsubscribe need at least one topic.")
260
+ raise ProtocolViolation
261
+ end
262
+ end
263
+
264
+ def ping_host
265
+ @sender.send_pingreq
266
+ end
267
+
268
+ def add_topic_callback(topic, callback=nil, &block)
269
+ @handler.register_topic_callback(topic, callback, &block)
270
+ end
271
+
272
+ def remove_topic_callback(topic)
273
+ @handler.clear_topic_callback(topic)
274
+ end
275
+
276
+ def on_connack(&block)
277
+ @handler.on_connack = block if block_given?
278
+ @handler.on_connack
279
+ end
280
+
281
+ def on_suback(&block)
282
+ @handler.on_suback = block if block_given?
283
+ @handler.on_suback
284
+ end
285
+
286
+ def on_unsuback(&block)
287
+ @handler.on_unsuback = block if block_given?
288
+ @handler.on_unsuback
289
+ end
290
+
291
+ def on_puback(&block)
292
+ @handler.on_puback = block if block_given?
293
+ @handler.on_puback
294
+ end
295
+
296
+ def on_pubrec(&block)
297
+ @handler.on_pubrec = block if block_given?
298
+ @handler.on_pubrec
299
+ end
300
+
301
+ def on_pubrel(&block)
302
+ @handler.on_pubrel = block if block_given?
303
+ @handler.on_pubrel
304
+ end
305
+
306
+ def on_pubcomp(&block)
307
+ @handler.on_pubcomp = block if block_given?
308
+ @handler.on_pubcomp
309
+ end
310
+
311
+ def on_message(&block)
312
+ @handler.on_message = block if block_given?
313
+ @handler.on_message
314
+ end
315
+
316
+ def on_connack=(callback)
317
+ @handler.on_connack = callback if callback.is_a?(Proc)
318
+ end
319
+
320
+ def on_suback=(callback)
321
+ @handler.on_suback = callback if callback.is_a?(Proc)
322
+ end
323
+
324
+ def on_unsuback=(callback)
325
+ @handler.on_unsuback = callback if callback.is_a?(Proc)
326
+ end
327
+
328
+ def on_puback=(callback)
329
+ @handler.on_puback = callback if callback.is_a?(Proc)
330
+ end
331
+
332
+ def on_pubrec=(callback)
333
+ @handler.on_pubrec = callback if callback.is_a?(Proc)
334
+ end
335
+
336
+ def on_pubrel=(callback)
337
+ @handler.on_pubrel = callback if callback.is_a?(Proc)
338
+ end
339
+
340
+ def on_pubcomp=(callback)
341
+ @handler.on_pubcomp = callback if callback.is_a?(Proc)
342
+ end
343
+
344
+ def on_message=(callback)
345
+ @handler.on_message = callback if callback.is_a?(Proc)
346
+ end
347
+
348
+ def registered_callback
349
+ @handler.registered_callback
350
+ end
351
+
352
+ def subscribed_topics
353
+ @subscriber.subscribed_topics
354
+ end
355
+
356
+
357
+ private
358
+
359
+ def next_packet_id
360
+ @id_mutex.synchronize do
361
+ @last_packet_id = 0 if @last_packet_id >= MAX_PACKET_ID
362
+ @last_packet_id = @last_packet_id.next
363
+ end
364
+ end
365
+
366
+ def downgrade_version
367
+ Rails.logger.info("Connection refused: unacceptable protocol version #{@mqtt_version}, trying 3.1")
368
+ if @mqtt_version != "3.1"
369
+ @mqtt_version = "3.1"
370
+ connect(@host, @port, @keep_alive)
371
+ else
372
+ raise ProtocolVersionException.new("Unsupported MQTT version")
373
+ end
374
+ end
375
+
376
+ def init_pubsub
377
+ @subscriber.nil? ? @subscriber = Subscriber.new(@sender) : @subscriber.sender = @sender
378
+ @publisher.nil? ? @publisher = Publisher.new(@sender) : @publisher.sender = @sender
379
+ @handler.config_pubsub(@publisher, @subscriber)
380
+ end
381
+
382
+ def build_pubsub
383
+ @subscriber.config_subscription(next_packet_id)
384
+ @sender.flush_waiting_packet
385
+ @publisher.config_all_message_queue
386
+ end
387
+
388
+ def init_connection
389
+ @connection_helper = ConnectionHelper.new(@host, @port, @ssl, @ssl_context, @ack_timeout)
390
+ @connection_helper.handler = @handler
391
+ @sender = @connection_helper.sender
392
+ end
393
+
394
+ def session_params
395
+ {
396
+ :version => @mqtt_version,
397
+ :clean_session => @clean_session,
398
+ :keep_alive => @keep_alive,
399
+ :client_id => @client_id,
400
+ :username => @username,
401
+ :password => @password,
402
+ :will_topic => @will_topic,
403
+ :will_payload => @will_payload,
404
+ :will_qos => @will_qos,
405
+ :will_retain => @will_retain
406
+ }
407
+ end
408
+
409
+ def check_persistence
410
+ disconnect(false)
411
+ @persistent
412
+ end
413
+ end
414
+ end