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,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