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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE +210 -0
- data/README.md +323 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/mqtt-rails.rb +144 -0
- data/lib/mqtt_rails/client.rb +414 -0
- data/lib/mqtt_rails/connection_helper.rb +172 -0
- data/lib/mqtt_rails/exception.rb +52 -0
- data/lib/mqtt_rails/handler.rb +274 -0
- data/lib/mqtt_rails/packet.rb +33 -0
- data/lib/mqtt_rails/packet/base.rb +315 -0
- data/lib/mqtt_rails/packet/connack.rb +102 -0
- data/lib/mqtt_rails/packet/connect.rb +183 -0
- data/lib/mqtt_rails/packet/disconnect.rb +38 -0
- data/lib/mqtt_rails/packet/pingreq.rb +29 -0
- data/lib/mqtt_rails/packet/pingresp.rb +38 -0
- data/lib/mqtt_rails/packet/puback.rb +44 -0
- data/lib/mqtt_rails/packet/pubcomp.rb +44 -0
- data/lib/mqtt_rails/packet/publish.rb +148 -0
- data/lib/mqtt_rails/packet/pubrec.rb +44 -0
- data/lib/mqtt_rails/packet/pubrel.rb +62 -0
- data/lib/mqtt_rails/packet/suback.rb +75 -0
- data/lib/mqtt_rails/packet/subscribe.rb +124 -0
- data/lib/mqtt_rails/packet/unsuback.rb +49 -0
- data/lib/mqtt_rails/packet/unsubscribe.rb +84 -0
- data/lib/mqtt_rails/publisher.rb +181 -0
- data/lib/mqtt_rails/sender.rb +129 -0
- data/lib/mqtt_rails/ssl_helper.rb +61 -0
- data/lib/mqtt_rails/subscriber.rb +166 -0
- data/lib/mqtt_rails/version.rb +3 -0
- data/mqtt-rails.gemspec +33 -0
- data/samples/client_blocking(reading).rb +29 -0
- data/samples/client_blocking(writing).rb +18 -0
- data/samples/getting_started.rb +49 -0
- data/samples/test_client.rb +69 -0
- 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
|
data/mqtt-rails.gemspec
ADDED
@@ -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
|