mqtt-rails 1.0

Sign up to get free protection for your applications and to get access to all the features.
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