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
@@ -0,0 +1,129 @@
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
+ module MqttRails
16
+ class Sender
17
+
18
+ attr_reader :last_packet_sent_at
19
+ attr_reader :last_pingreq_sent_at
20
+
21
+ def initialize(ack_timeout)
22
+ @socket = nil
23
+ @writing_queue = []
24
+ @publish_queue = []
25
+ @publish_mutex = Mutex.new
26
+ @writing_mutex = Mutex.new
27
+ @ack_timeout = ack_timeout
28
+ end
29
+
30
+ def socket=(socket)
31
+ @socket = socket
32
+ end
33
+
34
+ def send_packet(packet)
35
+ begin
36
+ unless @socket.nil? || @socket.closed?
37
+ @socket.write(packet.to_s)
38
+ @last_packet_sent_at = Time.now
39
+ MQTT_ERR_SUCCESS
40
+ else
41
+ MQTT_ERR_FAIL
42
+ end
43
+ end
44
+ rescue StandardError
45
+ raise WritingException
46
+ rescue IO::WaitWritable
47
+ IO.select(nil, [@socket], nil, SELECT_TIMEOUT)
48
+ retry
49
+ end
50
+
51
+ def send_pingreq
52
+ @last_pingreq_sent_at = Time.now if send_packet(MqttRails::Packet::Pingreq.new) == MQTT_ERR_SUCCESS
53
+ end
54
+
55
+ def prepare_sending(queue, mutex, max_packet, packet)
56
+ if queue.length < max_packet
57
+ mutex.synchronize do
58
+ queue.push(packet)
59
+ end
60
+ else
61
+ Rails.logger.error('Writing queue is full, slowing down')
62
+ raise FullWritingException
63
+ end
64
+ end
65
+
66
+ def append_to_writing(packet)
67
+ begin
68
+ if packet.is_a?(MqttRails::Packet::Publish)
69
+ prepare_sending(@publish_queue, @publish_mutex, MAX_PUBLISH, packet)
70
+ else
71
+ prepare_sending(@writing_queue, @writing_mutex, MAX_QUEUE, packet)
72
+ end
73
+ rescue FullWritingException
74
+ sleep SELECT_TIMEOUT
75
+ retry
76
+ end
77
+ MQTT_ERR_SUCCESS
78
+ end
79
+
80
+ def writing_loop
81
+ @writing_mutex.synchronize do
82
+ MAX_QUEUE.times do
83
+ break if @writing_queue.empty?
84
+ packet = @writing_queue.shift
85
+ send_packet(packet)
86
+ end
87
+ end
88
+ @publish_mutex.synchronize do
89
+ MAX_PUBLISH.times do
90
+ break if @publish_queue.empty?
91
+ packet = @publish_queue.shift
92
+ send_packet(packet)
93
+ end
94
+ end
95
+ MQTT_ERR_SUCCESS
96
+ end
97
+
98
+ def flush_waiting_packet(sending=true)
99
+ if sending
100
+ @writing_mutex.synchronize do
101
+ @writing_queue.each do |packet|
102
+ send_packet(packet)
103
+ end
104
+ end
105
+ @publish_mutex.synchronize do
106
+ @publish_queue.each do |packet|
107
+ send_packet(packet)
108
+ end
109
+ end
110
+ end
111
+ @writing_queue = []
112
+ @publish_queue = []
113
+ end
114
+
115
+ def check_ack_alive(queue, mutex)
116
+ mutex.synchronize do
117
+ now = Time.now
118
+ queue.each do |pck|
119
+ if now >= pck[:timestamp] + @ack_timeout
120
+ pck[:packet].dup ||= true unless pck[:packet].class == MqttRails::Packet::Subscribe || pck[:packet].class == MqttRails::Packet::Unsubscribe
121
+ Rails.logger.info("Acknowledgement timeout is over, resending #{pck[:packet].inspect}")
122
+ send_packet(pck[:packet])
123
+ pck[:timestamp] = now
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,61 @@
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 'openssl'
16
+
17
+ module MqttRails
18
+ module SSLHelper
19
+ extend self
20
+
21
+ def config_ssl_context(cert_path=nil, key_path=nil, ca_path=nil)
22
+ ssl_context = OpenSSL::SSL::SSLContext.new
23
+ set_cert(cert_path, ssl_context)
24
+ set_key(key_path, ssl_context)
25
+ set_root_ca(ca_path, ssl_context)
26
+ # ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless ca_path.nil?
27
+ ssl_context
28
+ end
29
+
30
+ def set_cert(cert_path=nil, ssl_context)
31
+ unless cert_path.nil?
32
+ ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
33
+ end
34
+ end
35
+
36
+ def set_key(key_path=nil, ssl_context)
37
+ unless key_path.nil?
38
+ return MQTT_ERR_SUCCESS if try_rsa_key(key_path, ssl_context) == MQTT_ERR_SUCCESS
39
+ begin
40
+ ssl_context.key = OpenSSL::PKey::EC.new(File.read(key_path))
41
+ return MQTT_ERR_SUCCESS
42
+ rescue OpenSSL::PKey::ECError
43
+ raise NotSupportedEncryptionException.new("Could not support the type of the provided key (supported: RSA and EC)")
44
+ end
45
+ end
46
+ end
47
+
48
+ def try_rsa_key(key_path, ssl_context)
49
+ begin
50
+ ssl_context.key = OpenSSL::PKey::RSA.new(File.read(key_path))
51
+ return MQTT_ERR_SUCCESS
52
+ rescue OpenSSL::PKey::RSAError
53
+ return MQTT_ERR_FAIL
54
+ end
55
+ end
56
+
57
+ def set_root_ca(ca_path, ssl_context)
58
+ ssl_context.ca_file = ca_path
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,166 @@
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
+ module MqttRails
16
+ class Subscriber
17
+
18
+ attr_reader :subscribed_topics
19
+
20
+ def initialize(sender)
21
+ @waiting_suback = []
22
+ @waiting_unsuback = []
23
+ @subscribed_mutex = Mutex.new
24
+ @subscribed_topics = []
25
+ @suback_mutex = Mutex.new
26
+ @unsuback_mutex = Mutex.new
27
+ @sender = sender
28
+ end
29
+
30
+ def sender=(sender)
31
+ @sender = sender
32
+ end
33
+
34
+ def config_subscription(new_id)
35
+ unless @subscribed_topics == [] || @subscribed_topics.nil?
36
+ packet = MqttRails::Packet::Subscribe.new(
37
+ :id => new_id,
38
+ :topics => @subscribed_topics
39
+ )
40
+ @subscribed_mutex.synchronize do
41
+ @subscribed_topics = []
42
+ end
43
+ @suback_mutex.synchronize do
44
+ if @waiting_suback.length >= MAX_SUBACK
45
+ Rails.logger.error('SUBACK queue is full, could not send subscribe')
46
+ return MQTT_ERR_FAILURE
47
+ end
48
+ @waiting_suback.push(:id => new_id, :packet => packet, :timestamp => Time.now)
49
+ end
50
+ @sender.append_to_writing(packet)
51
+ end
52
+ MQTT_ERR_SUCCESS
53
+ end
54
+
55
+ def add_subscription(max_qos, packet_id, adjust_qos)
56
+ @suback_mutex.synchronize do
57
+ adjust_qos, @waiting_suback = @waiting_suback.partition { |pck| pck[:id] == packet_id }
58
+ end
59
+ if adjust_qos.length == 1
60
+ adjust_qos = adjust_qos.first[:packet].topics
61
+ adjust_qos.each do |t|
62
+ if [0, 1, 2].include?(max_qos[0])
63
+ t[1] = max_qos.shift
64
+ elsif max_qos[0] == 128
65
+ adjust_qos.delete(t)
66
+ else
67
+ Rails.logger.error("The QoS value is invalid in subscribe.")
68
+ raise PacketException.new('Invalid suback QoS value')
69
+ end
70
+ end
71
+ else
72
+ Rails.logger.error("The packet id is invalid, already used.")
73
+ raise PacketException.new("Invalid suback packet id: #{packet_id}")
74
+ end
75
+ @subscribed_mutex.synchronize do
76
+ @subscribed_topics.concat(adjust_qos)
77
+ end
78
+ return adjust_qos
79
+ end
80
+
81
+ def remove_subscription(packet_id, to_unsub)
82
+ @unsuback_mutex.synchronize do
83
+ to_unsub, @waiting_unsuback = @waiting_unsuback.partition { |pck| pck[:id] == packet_id }
84
+ end
85
+
86
+ if to_unsub.length == 1
87
+ to_unsub = to_unsub.first[:packet].topics
88
+ else
89
+ Rails.logger.error("The packet id is invalid, already used.")
90
+ raise PacketException.new("Invalid unsuback packet id: #{packet_id}")
91
+ end
92
+
93
+ @subscribed_mutex.synchronize do
94
+ to_unsub.each do |filter|
95
+ @subscribed_topics.delete_if { |topic| MqttRails.match_filter(topic.first, filter) }
96
+ end
97
+ end
98
+ return to_unsub
99
+ end
100
+
101
+ def send_subscribe(topics, new_id)
102
+ unless valid_topics?(topics) == MQTT_ERR_FAIL
103
+ packet = MqttRails::Packet::Subscribe.new(
104
+ :id => new_id,
105
+ :topics => topics
106
+ )
107
+ @sender.append_to_writing(packet)
108
+ @suback_mutex.synchronize do
109
+ if @waiting_suback.length >= MAX_SUBACK
110
+ Rails.logger.error('SUBACK queue is full, could not send subscribe')
111
+ return MQTT_ERR_FAILURE
112
+ end
113
+ @waiting_suback.push(:id => new_id, :packet => packet, :timestamp => Time.now)
114
+ end
115
+ MQTT_ERR_SUCCESS
116
+ else
117
+ raise ProtocolViolation
118
+ end
119
+ end
120
+
121
+ def send_unsubscribe(topics, new_id)
122
+ unless valid_topics?(topics) == MQTT_ERR_FAIL
123
+ packet = MqttRails::Packet::Unsubscribe.new(
124
+ :id => new_id,
125
+ :topics => topics
126
+ )
127
+ @sender.append_to_writing(packet)
128
+ @unsuback_mutex.synchronize do
129
+ if @waiting_suback.length >= MAX_UNSUBACK
130
+ Rails.logger.error('UNSUBACK queue is full, could not send unbsubscribe')
131
+ return MQTT_ERR_FAIL
132
+ end
133
+ @waiting_unsuback.push(:id => new_id, :packet => packet, :timestamp => Time.now)
134
+ end
135
+ MQTT_ERR_SUCCESS
136
+ else
137
+ raise ProtocolViolation
138
+ end
139
+ end
140
+
141
+ def check_waiting_subscriber
142
+ @sender.check_ack_alive(@waiting_suback, @suback_mutex)
143
+ @sender.check_ack_alive(@waiting_unsuback, @unsuback_mutex)
144
+ end
145
+
146
+ def clear_queue
147
+ @waiting_suback = []
148
+ end
149
+
150
+ def valid_topics?(topics)
151
+ unless topics.length == 0
152
+ topics.map do |topic|
153
+ case topic
154
+ when Array
155
+ return MQTT_ERR_FAIL if topic.first == ""
156
+ when String
157
+ return MQTT_ERR_FAIL if topic == ""
158
+ end
159
+ end
160
+ else
161
+ MQTT_ERR_FAIL
162
+ end
163
+ MQTT_ERR_SUCCESS
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,3 @@
1
+ module MqttRails
2
+ VERSION = "1.0"
3
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mqtt_rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mqtt-rails"
8
+ spec.version = MqttRails::VERSION
9
+ spec.authors = ["Nicolas KOVACS"]
10
+ spec.email = ["pro.nkovacs@gmail.com"]
11
+
12
+ spec.summary = %q{A simple rails mqtt client gem}
13
+ spec.description = %q{A simple rails mqtt client gem}
14
+ spec.homepage = "https://github.com/nicovak/mqtt.rails.git"
15
+ spec.license = "EPL-1.0"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.11"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ end
@@ -0,0 +1,29 @@
1
+ require 'mqtt-rails'
2
+
3
+ client = MqttRails::Client.new()
4
+
5
+ client.on_message do |pck|
6
+ puts "New Message: #{pck.topic}\n>>> #{pck.payload}"
7
+ end
8
+
9
+ wait_suback = true
10
+ client.on_suback do |pck|
11
+ wait_suback = false
12
+ end
13
+
14
+ client.connect('localhost', 1883, client.keep_alive, true, true)
15
+
16
+ Thread.new do
17
+ while wait_suback do
18
+ client.loop_read
19
+ sleep 0.001
20
+ end
21
+ end
22
+
23
+ client.subscribe(["topic_test", 2])
24
+ client.loop_write
25
+
26
+ loop do
27
+ client.loop_read
28
+ sleep 0.01
29
+ end
@@ -0,0 +1,18 @@
1
+ require 'mqtt-rails'
2
+
3
+ Rails.logger = ('mqtt_rails.log')
4
+
5
+ client = MqttRails::Client.new()
6
+
7
+ client.on_message = lambda { |p| puts ">>>>> This is the callback for a message event <<<<<\nTopic: #{p.topic}\nPayload: #{p.payload}\nQoS: #{p.qos}" }
8
+
9
+
10
+ client.connect('localhost', 1883, client.keep_alive, true, true)
11
+ client.subscribe(["topic_test", 2])
12
+
13
+ loop do
14
+ client.publish("topic_test", "Hello, Are you there?", false, 1)
15
+ client.loop_write
16
+ client.loop_read
17
+ sleep 1
18
+ end