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,172 @@
|
|
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 MqttRails
|
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
|
+
end
|
45
|
+
unless is_connected?
|
46
|
+
Rails.logger.warn("Connection failed. Couldn't recieve a Connack packet from: #{@host}.")
|
47
|
+
raise Exception.new("Connection failed. Check log for more details.") unless reconnection
|
48
|
+
end
|
49
|
+
@cs
|
50
|
+
end
|
51
|
+
|
52
|
+
def is_connected?
|
53
|
+
@cs == MQTT_CS_CONNECTED
|
54
|
+
end
|
55
|
+
|
56
|
+
def do_disconnect(publisher, explicit, mqtt_thread)
|
57
|
+
Rails.logger.info("Disconnecting from #{@host}.")
|
58
|
+
if explicit
|
59
|
+
explicit_disconnect(publisher, mqtt_thread)
|
60
|
+
end
|
61
|
+
@socket.close unless @socket.nil? || @socket.closed?
|
62
|
+
@socket = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def explicit_disconnect(publisher, mqtt_thread)
|
66
|
+
@sender.flush_waiting_packet(false)
|
67
|
+
send_disconnect
|
68
|
+
mqtt_thread.kill if mqtt_thread && mqtt_thread.alive?
|
69
|
+
publisher.flush_publisher unless publisher.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
def setup_connection
|
73
|
+
clean_start(@host, @port)
|
74
|
+
config_socket
|
75
|
+
unless @socket.nil?
|
76
|
+
@sender.socket = @socket
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def config_socket
|
81
|
+
Rails.logger.info("Attempt to connect to host: #{@host}...")
|
82
|
+
begin
|
83
|
+
tcp_socket = TCPSocket.new(@host, @port)
|
84
|
+
if @ssl
|
85
|
+
encrypted_socket(tcp_socket, @ssl_context)
|
86
|
+
else
|
87
|
+
@socket = tcp_socket
|
88
|
+
end
|
89
|
+
rescue StandardError
|
90
|
+
Rails.logger.warn("Could not open a socket with #{@host} on port #{@port}.")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def encrypted_socket(tcp_socket, ssl_context)
|
95
|
+
unless ssl_context.nil?
|
96
|
+
@socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
97
|
+
@socket.sync_close = true
|
98
|
+
@socket.connect
|
99
|
+
else
|
100
|
+
Rails.logger.error("The SSL context was found as nil while the socket's opening.")
|
101
|
+
raise Exception
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def clean_start(host, port)
|
106
|
+
self.host = host
|
107
|
+
self.port = port
|
108
|
+
unless @socket.nil?
|
109
|
+
@socket.close unless @socket.closed?
|
110
|
+
@socket = nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def host=(host)
|
115
|
+
if host.nil? || host == ""
|
116
|
+
Rails.logger.error("The host was found as nil while the connection setup.")
|
117
|
+
raise ArgumentError
|
118
|
+
else
|
119
|
+
@host = host
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def port=(port)
|
124
|
+
if port.to_i <= 0
|
125
|
+
Rails.logger.error("The port value is invalid (<= 0). Could not setup the connection.")
|
126
|
+
raise ArgumentError
|
127
|
+
else
|
128
|
+
@port = port
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def send_connect(session_params)
|
133
|
+
setup_connection
|
134
|
+
packet = MqttRails::Packet::Connect.new(session_params)
|
135
|
+
@handler.clean_session = session_params[:clean_session]
|
136
|
+
@sender.send_packet(packet)
|
137
|
+
MQTT_ERR_SUCCESS
|
138
|
+
end
|
139
|
+
|
140
|
+
def send_disconnect
|
141
|
+
packet = MqttRails::Packet::Disconnect.new
|
142
|
+
@sender.send_packet(packet)
|
143
|
+
MQTT_ERR_SUCCESS
|
144
|
+
end
|
145
|
+
|
146
|
+
# Would return 'true' if ping requset should be sent and 'nil' if not
|
147
|
+
def should_send_ping?(now, keep_alive, last_packet_received_at)
|
148
|
+
last_pingreq_sent_at = @sender.last_pingreq_sent_at
|
149
|
+
last_pingresp_received_at = @handler.last_pingresp_received_at
|
150
|
+
if !last_pingreq_sent_at || (last_pingresp_received_at && (last_pingreq_sent_at <= last_pingresp_received_at))
|
151
|
+
next_pingreq_at = [@sender.last_packet_sent_at, last_packet_received_at].min + (keep_alive * 0.7).ceil
|
152
|
+
return next_pingreq_at <= now
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def check_keep_alive(persistent, keep_alive)
|
157
|
+
now = Time.now
|
158
|
+
last_packet_received_at = @handler.last_packet_received_at
|
159
|
+
# send a PINGREQ only if we don't already wait for a PINGRESP
|
160
|
+
if persistent && should_send_ping?(now, keep_alive, last_packet_received_at)
|
161
|
+
Rails.logger.info("Checking if server is still alive...")
|
162
|
+
@sender.send_pingreq
|
163
|
+
end
|
164
|
+
disconnect_timeout_at = last_packet_received_at + (keep_alive * 1.1).ceil
|
165
|
+
if disconnect_timeout_at <= now
|
166
|
+
Rails.logger.info("No activity is over timeout, disconnecting from #{@host}.")
|
167
|
+
@cs = MQTT_CS_DISCONNECT
|
168
|
+
end
|
169
|
+
@cs
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,52 @@
|
|
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 MqttRails
|
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
|
+
|
44
|
+
class FullWritingException < Exception
|
45
|
+
end
|
46
|
+
|
47
|
+
class FullQueueException < Exception
|
48
|
+
end
|
49
|
+
|
50
|
+
class NotSupportedEncryptionException < Exception
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,274 @@
|
|
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 Handler
|
17
|
+
|
18
|
+
attr_reader :registered_callback
|
19
|
+
attr_reader :last_packet_received_at
|
20
|
+
attr_reader :last_pingresp_received_at
|
21
|
+
attr_accessor :clean_session
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@registered_callback = []
|
25
|
+
@publisher = nil
|
26
|
+
@subscriber = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def config_pubsub(publisher, subscriber)
|
30
|
+
@publisher = publisher
|
31
|
+
@subscriber = subscriber
|
32
|
+
end
|
33
|
+
|
34
|
+
def socket=(socket)
|
35
|
+
@socket = socket
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive_packet
|
39
|
+
result = IO.select([@socket], nil, nil, SELECT_TIMEOUT) unless @socket.nil? || @socket.closed?
|
40
|
+
unless result.nil?
|
41
|
+
packet = MqttRails::Packet::Base.read(@socket)
|
42
|
+
unless packet.nil?
|
43
|
+
@last_packet_received_at = Time.now
|
44
|
+
if packet.is_a?(MqttRails::Packet::Connack)
|
45
|
+
return handle_connack(packet)
|
46
|
+
else
|
47
|
+
handle_packet(packet)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle_packet(packet)
|
55
|
+
Rails.logger.info("New packet #{packet.class} received.")
|
56
|
+
type = packet_type(packet)
|
57
|
+
self.send("handle_#{type}", packet)
|
58
|
+
end
|
59
|
+
|
60
|
+
def register_topic_callback(topic, callback, &block)
|
61
|
+
if topic.nil?
|
62
|
+
Rails.logger.error("The topics where the callback is trying to be registered have been found nil.")
|
63
|
+
raise ArgumentError
|
64
|
+
end
|
65
|
+
clear_topic_callback(topic)
|
66
|
+
if block_given?
|
67
|
+
@registered_callback.push([topic, block])
|
68
|
+
elsif !(callback.nil?) && callback.is_a?(Proc)
|
69
|
+
@registered_callback.push([topic, callback])
|
70
|
+
end
|
71
|
+
MQTT_ERR_SUCCESS
|
72
|
+
end
|
73
|
+
|
74
|
+
def clear_topic_callback(topic)
|
75
|
+
if topic.nil?
|
76
|
+
Rails.logger.error("The topics where the callback is trying to be unregistered have been found nil.")
|
77
|
+
raise ArgumentError
|
78
|
+
end
|
79
|
+
@registered_callback.delete_if { |pair| pair.first == topic }
|
80
|
+
MQTT_ERR_SUCCESS
|
81
|
+
end
|
82
|
+
|
83
|
+
def handle_connack(packet)
|
84
|
+
if packet.return_code == 0x00
|
85
|
+
Rails.logger.info(packet.return_msg)
|
86
|
+
@last_pingresp_received_at = Time.now
|
87
|
+
handle_connack_accepted(packet.session_present)
|
88
|
+
else
|
89
|
+
Rails.logger.warn(packet.return_msg)
|
90
|
+
return MQTT_CS_DISCONNECT
|
91
|
+
end
|
92
|
+
@on_connack.call(packet) unless @on_connack.nil?
|
93
|
+
MQTT_CS_CONNECTED
|
94
|
+
end
|
95
|
+
|
96
|
+
def handle_connack_accepted(session_flag)
|
97
|
+
clean_session?(session_flag)
|
98
|
+
new_session?(session_flag)
|
99
|
+
old_session?(session_flag)
|
100
|
+
end
|
101
|
+
|
102
|
+
def new_session?(session_flag)
|
103
|
+
if !@clean_session && !session_flag
|
104
|
+
Rails.logger.info("New session created for the client.")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def clean_session?(session_flag)
|
109
|
+
if @clean_session && !session_flag
|
110
|
+
Rails.logger.info("No previous session found by server, starting a new one.")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def old_session?(session_flag)
|
115
|
+
if !@clean_session && session_flag
|
116
|
+
Rails.logger.info("Previous session restored by the server.")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def handle_pingresp(_packet)
|
121
|
+
@last_pingresp_received_at = Time.now
|
122
|
+
end
|
123
|
+
|
124
|
+
def handle_suback(packet)
|
125
|
+
max_qos = packet.return_codes
|
126
|
+
id = packet.id
|
127
|
+
topics = []
|
128
|
+
topics = @subscriber.add_subscription(max_qos, id, topics)
|
129
|
+
unless topics.empty?
|
130
|
+
@on_suback.call(topics) unless @on_suback.nil?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def handle_unsuback(packet)
|
135
|
+
id = packet.id
|
136
|
+
topics = []
|
137
|
+
topics = @subscriber.remove_subscription(id, topics)
|
138
|
+
unless topics.empty?
|
139
|
+
@on_unsuback.call(topics) unless @on_unsuback.nil?
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def handle_publish(packet)
|
144
|
+
id = packet.id
|
145
|
+
qos = packet.qos
|
146
|
+
if @publisher.do_publish(qos, id) == MQTT_ERR_SUCCESS
|
147
|
+
@on_message.call(packet) unless @on_message.nil?
|
148
|
+
check_callback(packet)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def handle_puback(packet)
|
153
|
+
id = packet.id
|
154
|
+
if @publisher.do_puback(id) == MQTT_ERR_SUCCESS
|
155
|
+
@on_puback.call(packet) unless @on_puback.nil?
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def handle_pubrec(packet)
|
160
|
+
id = packet.id
|
161
|
+
if @publisher.do_pubrec(id) == MQTT_ERR_SUCCESS
|
162
|
+
@on_pubrec.call(packet) unless @on_pubrec.nil?
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def handle_pubrel(packet)
|
167
|
+
id = packet.id
|
168
|
+
if @publisher.do_pubrel(id) == MQTT_ERR_SUCCESS
|
169
|
+
@on_pubrel.call(packet) unless @on_pubrel.nil?
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def handle_pubcomp(packet)
|
174
|
+
id = packet.id
|
175
|
+
if @publisher.do_pubcomp(id) == MQTT_ERR_SUCCESS
|
176
|
+
@on_pubcomp.call(packet) unless @on_pubcomp.nil?
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def on_connack(&block)
|
181
|
+
@on_connack = block if block_given?
|
182
|
+
@on_connack
|
183
|
+
end
|
184
|
+
|
185
|
+
def on_suback(&block)
|
186
|
+
@on_suback = block if block_given?
|
187
|
+
@on_suback
|
188
|
+
end
|
189
|
+
|
190
|
+
def on_unsuback(&block)
|
191
|
+
@on_unsuback = block if block_given?
|
192
|
+
@on_unsuback
|
193
|
+
end
|
194
|
+
|
195
|
+
def on_puback(&block)
|
196
|
+
@on_puback = block if block_given?
|
197
|
+
@on_puback
|
198
|
+
end
|
199
|
+
|
200
|
+
def on_pubrec(&block)
|
201
|
+
@on_pubrec = block if block_given?
|
202
|
+
@on_pubrec
|
203
|
+
end
|
204
|
+
|
205
|
+
def on_pubrel(&block)
|
206
|
+
@on_pubrel = block if block_given?
|
207
|
+
@on_pubrel
|
208
|
+
end
|
209
|
+
|
210
|
+
def on_pubcomp(&block)
|
211
|
+
@on_pubcomp = block if block_given?
|
212
|
+
@on_pubcomp
|
213
|
+
end
|
214
|
+
|
215
|
+
def on_message(&block)
|
216
|
+
@on_message = block if block_given?
|
217
|
+
@on_message
|
218
|
+
end
|
219
|
+
|
220
|
+
def on_connack=(callback)
|
221
|
+
@on_connack = callback if callback.is_a?(Proc)
|
222
|
+
end
|
223
|
+
|
224
|
+
def on_suback=(callback)
|
225
|
+
@on_suback = callback if callback.is_a?(Proc)
|
226
|
+
end
|
227
|
+
|
228
|
+
def on_unsuback=(callback)
|
229
|
+
@on_unsuback = callback if callback.is_a?(Proc)
|
230
|
+
end
|
231
|
+
|
232
|
+
def on_puback=(callback)
|
233
|
+
@on_puback = callback if callback.is_a?(Proc)
|
234
|
+
end
|
235
|
+
|
236
|
+
def on_pubrec=(callback)
|
237
|
+
@on_pubrec = callback if callback.is_a?(Proc)
|
238
|
+
end
|
239
|
+
|
240
|
+
def on_pubrel=(callback)
|
241
|
+
@on_pubrel = callback if callback.is_a?(Proc)
|
242
|
+
end
|
243
|
+
|
244
|
+
def on_pubcomp=(callback)
|
245
|
+
@on_pubcomp = callback if callback.is_a?(Proc)
|
246
|
+
end
|
247
|
+
|
248
|
+
def on_message=(callback)
|
249
|
+
@on_message = callback if callback.is_a?(Proc)
|
250
|
+
end
|
251
|
+
|
252
|
+
def packet_type(packet)
|
253
|
+
type = packet.class
|
254
|
+
if MqttRails::PACKET_TYPES[3..13].include?(type)
|
255
|
+
type.to_s.split('::').last.downcase
|
256
|
+
else
|
257
|
+
Rails.logger.error("Received an unexpeceted packet: #{packet}.")
|
258
|
+
raise PacketException.new('Invalid packet type id')
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def check_callback(packet)
|
263
|
+
callbacks = []
|
264
|
+
@registered_callback.each do |reccord|
|
265
|
+
callbacks.push(reccord.last) if MqttRails.match_filter(packet.topic, reccord.first)
|
266
|
+
end
|
267
|
+
unless callbacks.empty?
|
268
|
+
callbacks.each do |callback|
|
269
|
+
callback.call(packet)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|