paho-mqtt 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5f334ef3126b1f0def40579e6f202200d3dacfc3
4
- data.tar.gz: 9ef4eb0748507a6b744f82011a9da894f909624e
3
+ metadata.gz: 16c0a069f566b0c91b896cbcc0a4b3ca2eb694f6
4
+ data.tar.gz: bd03230d51b7bc37be071caac903ce8537073b6b
5
5
  SHA512:
6
- metadata.gz: 412649a90228d742ba6812d77924f74559d759199ed1258f9095a9b1e80d759979bc0ceb64065cf11d73a6c4b1b9294102ccd690b7c9ca2d42f695d3c29b86b5
7
- data.tar.gz: 3533ef2b3774a816a66827341a14aebd4d70534cf08888881ac619f072b9aa09ab7700cc28ba406a8bcffe0a9f3bdf02e66d8c045289a386d31ef2930b6734a3
6
+ metadata.gz: 0c3a8a75de74ef2df07b3e6df1abcf694e07dd425e589169a8c7696e8131b55aba92b41daf6525e60ef3210347f33139ea399eb8092877dedc40c6f986b022e5
7
+ data.tar.gz: 53e67740ea1e52c1a0e73517079d1b7b0ce1f15802ea22f9a880a00ba2e514b7e715e7a06b293931e1a561cbd2beb2dd8439dee906a6fdfbc183c13f0ce7f87c
data/README.md CHANGED
@@ -1,8 +1,30 @@
1
1
  # PahoMqtt
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/PahoMqttRuby`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ The following file describes the Paho Mqtt client API for the ruby programming language. It enables applications to connect to an MQTT message broker threw the [MQTT](http://mqtt.org/) protocol (versions 3.1.1). MQTT is a lightweight protocol designed for IoT/M2M. A Mqtt client can connect to a message broker in order to publish and received data contained in short messages. The messages are exchanged on topics where the client has to subscribe for receiving message.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ ## Contents
6
+ * [Installation](#installation)
7
+ * [Usage](#usage)
8
+ * [Getting started](#getting-started)
9
+ * [Client](#client)
10
+ * [Initialization](#initialization)
11
+ * [Client's parameters](#clients-parameter)
12
+ * [Subscription](#subscription)
13
+ * [Publishing](#publishing)
14
+ * [Connection configuration](#connection-configuration)
15
+ * [Unencrypted mode](#unencrypted-mode)
16
+ * [Encrypted mode](#encrypted-mode)
17
+ * [Persistence](#persistence)
18
+ * [Foreground and Daemon](#foreground-and-daemon)
19
+ * [Control loops](#control-loops)
20
+ * [Reading loop](#reading-loop)
21
+ * [Writing loop](#writing-loop)
22
+ * [Miscellaneous loop](#miscellaneous-loop)
23
+ * [Handlers and Callbacks](#handlers-and-callbacks)
24
+ * [Handlers](#handlers)
25
+ * [Callbacks](#callbacks)
26
+ * [Mosquitto (message broker)](#mosquitto-message-broker)
27
+ * [Thanks](#thanks)
6
28
 
7
29
  ## Installation
8
30
 
@@ -22,20 +44,277 @@ Or install it yourself as:
22
44
 
23
45
  ## Usage
24
46
 
25
- TODO: Write usage instructions here
47
+ ### Getting started
48
+ The following samples files cover the main features of the client:
49
+ ```ruby
50
+ require 'paho-mqtt'
51
+
52
+ ### Create a simple client with default attributes
53
+ client = PahoMqtt::Client.new
54
+
55
+ ### Register a callback on message event to display messages
56
+ message_counter = 0
57
+ client.on_message do |message|
58
+ puts "Message recieved on topic: #{message.topic}\n>>> #{message.payload}"
59
+ message_counter += 1
60
+ end
61
+
62
+ ### Register a callback on suback to assert the subcription
63
+ waiting_suback = true
64
+ client.on_suback do
65
+ waiting_suback = false
66
+ puts "Subscribed"
67
+ end
68
+
69
+ ### Register a callback for puback event when receiving a puback
70
+ waiting_puback = true
71
+ client.on_puback do
72
+ waiting_puback = false
73
+ puts "Message Acknowledged"
74
+ end
75
+
76
+ ### Connect to the eclipse test server on port 1883 (Unencrypted mode)
77
+ client.connect('iot.eclipse.org', 1883)
78
+
79
+ ### Subscribe to a topic
80
+ client.subscribe(['/paho/ruby/test', 2])
81
+
82
+ ### Waiting for the suback answer and excute the previously set on_suback callback
83
+ while waiting_suback do
84
+ sleep 0.001
85
+ end
86
+
87
+ ### Publlish a message on the topic "/paho/ruby/test" with "retain == false" and "qos == 1"
88
+ client.publish("/paho/ruby/test", "Hello there!", false, 1)
89
+
90
+ while waiting_puback do
91
+ sleep 0.001
92
+ end
93
+
94
+ ### Waiting to assert that the message is displayed by on_message callback
95
+ sleep 1
96
+
97
+ ### Calling an explicit disconnect
98
+ client.disconnect
99
+ ```
100
+
101
+ ## Client
102
+ ### Initialization
103
+ The client may be initialized without paramaeters or with a hash of parameters. The list of client's accessor is details in the next parts. A client id would be generated if not provided, a default port would be also set (8883 if ssl set, else 1883).
104
+ ```ruby
105
+ client = PahoMqtt::Client.new
106
+ # Or
107
+ client = PahoMqtt::Client.new({host: "iot.eclispe.org", port: 1883, ssl: false})
108
+ ```
109
+
110
+ ### Client's parameters
111
+ The client has many accessors which help to configure the client depending on user's need. The different accessors could be splited in four roles, connection setup, last will setup, time-out setup and callback setup.
112
+ Connection setup:
113
+ ```
114
+ * host : The endpoint where the client would try to connect (defaut "")
115
+ * port : The port on the remote host where the socket would try to connect (default 1883)
116
+ * mqtt_version : The version of MQTT protocol used to communication (default 3.1.1)
117
+ * clean_session : If set to false, ask the message broker to try to restore the previous session (default true)
118
+ * persistent : Keep the client connected even after keep alive timer run out, automatically try to reconnect on failure (default false)
119
+ * client_id : The identifier of the client (default nil)
120
+ * username : The username if the server require authentication (default nil)
121
+ * password : The password of the user if authentication required (default nil)
122
+ * ssl : Requiring the encryption for the communication (default false)
123
+ ```
26
124
 
27
- ## Development
125
+ Last Will:
126
+ ```
127
+ * will_topic : The topic where to publish the last will (default nil)
128
+ * will_payload : The message of the last will (default "")
129
+ * will_qos : The qos of the last will (default 0)
130
+ * will_retain : The retain status of the last will (default false)
131
+ ```
28
132
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
133
+ Timers:
134
+ ```
135
+ * keep_alive : The reference timer after which the client should decide to keep the connection alive or not
136
+ * ack_timeout : The timer after which a non-acknowledged packet is considered as a failure
137
+ ```
30
138
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
139
+ The description of the callback accessor is detailed in the section dedicated to the callbacks. The client also have three read only attributes which provide information on the client state.
140
+ ```
141
+ * registered_callback : The list of topics where callback have been registred which the associated callback
142
+ * subscribed_topics : The list of the topics where the client is currentely receiving publish.
143
+ * connection_state : The current state of the connection between the message broker and the client
144
+ ```
32
145
 
33
- ## Contributing
146
+ ### Subscription
147
+ In order to read a message sent on a topic, the client should subscribe to this topic. The client enables to subscribe to several topics in the same subscribe request. The subscription could also be done by using a wild-card, see more details on [MQTT protocol specifications](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html). Each topic is subscribed with a maximum qos level, only message with a qos level lower or equal to this value would be forwarded to the client. The subscribe command accepts one or several pair, each pair is composed by the topic (or wild-card) and the maximum qos level.
148
+ ```ruby
149
+ ### Subscribe to two topics with maximum qos associated
150
+ client.subscribe(["/foo/bar", 1], ["/foo/foo/", 2])
151
+ ```
34
152
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/PahoMqttRuby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
153
+ The subscription is persistent, in case of an unexpected disconnecting, the current subscription state is saved and a new subscribe request is sent to the message broker.
36
154
 
155
+ ### Publishing
156
+ User data could be sent to the message broker with the publish operation. A publish operation requires a topic, and payload (user data), two other parameters may be configured, retain and qos. The retain flag tell to the message broker to keep the current publish packet, see the [MQTT protocol specifications](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) for more details about retain. The qos enable different levels of control on the transmission of publish package. The PahoMqtt client supports the three levels of qos (0, 1 and 2), see the [MQTT protocol specifications](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) for qos level details. The default retain value is False and the qos level is 0.
157
+ ```ruby
158
+ ### Publish to the topics "/foo/bar", with qos = 1 and no retain
159
+ client.publish("/foo/bar", "Hello Wourld!", false, 1)
160
+ ```
161
+
162
+ ## Connection configuration
163
+ ### Unencrypted mode
164
+ The most simple connection way is the unencrypted mode. All data would be sent clearly to the message broker, also it might not be safe for sensitive data. The connect method may set up or override some parameters of the client, the host, the port, the keep_alive timer, the persistence mode and blocking mode.
165
+ ```ruby
166
+ ### Simply connect to the message broker with default value or pre-set value
167
+ client.connect
168
+ # Or
169
+ ### Connect to the message broker with all parameter
170
+ client.connect("iot.eclipse.org", 1883, client.keep_alive, client.persistent, client.blocking)
171
+ ```
172
+
173
+ ### Encrypted mode
174
+ The client supports the encrypted connection threw tls-ssl socket. In order to use encrypted mode, the ssl flag of the client should be set to True.
175
+ ``` ruby
176
+ ### Set the encryption mode to True
177
+ client.ssl = true
178
+ ### Configure the user SSL key and the certificate
179
+ client.config_ssl_context(certificate_path, key_path)
180
+ client.connect("test.mosquitto.org", 8883)
181
+ ### Or if rootCA is needed
182
+ client.config_ssl_context(certificate_path, key_path, rootCA_path)
183
+ client.connect("test.mosquitto.org", 8884)
184
+ ```
185
+
186
+ ### Persistence
187
+ The client holds a keep_alive timer is the reference time that the connection should be held. The timer is reset every time a new valid packet is received from the message broker. The persistence flag, when set to True, enables the client to be more independent from the keep_alive timer. Just before the keep_alive run out, the client sends a ping request to tell to the message broker that the connection should be kept. The persistent mode also enables the client to automatically reconnect to the message broker after an unexpected failure.
188
+ When the client's persistence flag is set to False, it just simply disconnects when the keep_alive timer runs out.
189
+
190
+ ```ruby
191
+ ### This will connect to the message broker, keep connected and automatically reconnect on failure
192
+ client.connect('iot.eclipse.org', 1883, client.keep_alive, true, client.blocking)
193
+ #Or
194
+ ### This only connect to the message broker, disconnect after keep_alive or on failure
195
+ client.connect('iot.eclipse.org', 1883, client.keep_alive, false, client.blocking)
196
+ ```
37
197
 
38
- ## License
198
+ ### Foreground and Daemon
199
+ The client could be connected to the message broker using the main thread in foreground or as a daemon in a separate thread. The default mode is daemon mode, the daemon would run in the background the read/write operation as well as the control of the timers. If the client is connected using the main thread, all control operations are left to the user, using the different control loops. There are four different loop roles is detailed in the next part.
200
+
201
+ ```ruby
202
+ ### Connect to the message broker executing the mqtt_loop (socket reading/writing) in the background
203
+ client.connect('iot.eclipse.org', 1883, client.keep_alive, client.persistence, true)
204
+ #Or
205
+ ### This only connect to the message broker, nothing more
206
+ client.connect('iot.eclipse.org', 1883, client.keep_alive, client.persistence, false)
207
+ ```
208
+
209
+ ## Control loops
210
+ /!\ The control loops should not be used in a daemon mode.
211
+ They are automatically run in separate thread and execute the necessary operations for reading, writing and checking the connection state.
212
+
213
+ ### Reading loop
214
+ The reading loop provides access to the socket in a reading mode. Periodically, the socket would be inspected to try to find a mqtt packet. The read loop accepts a parameter, which is the number of loop's turn. The default value is five turns.
215
+ The default value is defined in the PahoMqtt module as the constant PahoMqtt::MAX_READ, another module constant could be modified to control the socket inspection period. The referring constant is SELECT_TIMEOUT (PahoMqtt::SELECT_TIMEOUT) and its default value is 0.
216
+ ```ruby
217
+ ### Trying to read 'max_packet' packets from the client socket
218
+ client.loop_read(max_packet)
219
+ ```
220
+
221
+ ### Writing loop
222
+ The writing loop send the packets which have previously been stacked by MQTT operations. This loop also accepts a parameter, which is the maximum packets number that could be written as the MAX_WRITING constant (PahoMqtt::MAX_WRITING). The writing loop exit if the maximum number of packet have been sent or if the waiting packet queue is empty.
223
+ ```ruby
224
+ ### Writing 'max_packet' packets to the client socket
225
+ client.loop_write(max_packet)
226
+ ```
227
+
228
+ ### Miscellaneous loop
229
+ The misc loop performs different control operations, modifying the packets states and the connection state. The misc loop parses the different queue of packet that are waiting for an acknowledgement. If the ack_timeout of a packet had run out, the packet is re-sent. The size of the different waiting queues is defined as module constants. This loop also asserts that the connection is still available by checking the keep_alive timer.
230
+ ```ruby
231
+ ### Perfom control operations on packets queues and connection
232
+ client.loop_misc
233
+ ```
234
+
235
+ ## Handlers and Callbacks
236
+ ### Handlers
237
+ When a packet is received and inspected, an appropriate handler is called. The handler performs different control operation such as update the connection state, update the subscribed topics, and send publish control packets. Each packet has a specific handler, except the pingreq/pingresp packet. Before returning the handler executes a callback, if the user has configured one for this type of packet. The publish handler may execute sequentially two callbacks. One callback for the reception of a generic publish packet and another one, if the user has configured a callback for the topic where the publish packet has been received.
238
+
239
+ ### Callbacks
240
+ The callbacks could be defined in a three different ways, as block, as Proc or as Lambda. The callback has access to the packet which triggered it.
241
+ ```ruby
242
+ ### Register a callback trigger on the reception of a CONNACK packet
243
+ client.on_connack = proc { puts "Successfully Connected" }
244
+
245
+ ### Register a callback trigger on the reception of PUBLISH packet
246
+ client.on_message do |packet|
247
+ puts "New message received on topic: #{packet.topic}\n>>>#{packet.payload}"
248
+ end
249
+ ```
250
+
251
+ A callback could be configured for every specific topics. The list of topics where a callbacks have been registered could be read at any time, threw the registered_callback variable. The following example details how to manage callbacks for specific topics.
252
+ ```ruby
253
+ ### Add a callback for every message received on /foo/bar
254
+ specific_callback = lambda { |packet| puts "Specific callback for #{packet.topic}" }
255
+ client.add_topic_callback("/foo/bar", specific_callback)
256
+ # Or
257
+ client.add_topic_callback("/foo/bar") do |packet|
258
+ puts "Specific callback for #{packet.topic}"
259
+ end
260
+
261
+ ### To remove a callback form a topic
262
+ client.remove_topic_callback("/foo/bar")
263
+ ```
264
+
265
+ ## Mosquitto (message broker)
266
+ Mosquitto is a message broker support by Eclipse, which is quite easy-going. In order to run spec or samples files, a message broker is needed. Mosquitto enable to run locally a message broker, it could be configured with the mosquitto.conf files.
267
+ ### Install mosquitto
268
+ #### OSX (homebrew)
269
+ ```
270
+ $ brew install mosquitto
271
+ ```
272
+ ### Run mosquitto
273
+ #### Default mode
274
+ The default mode of mosquitto is unencrypted, listening on the port 1883.
275
+ ```
276
+ $ mosquitto
277
+ ```
278
+
279
+ #### Encrypted mode
280
+ In order to successfully pass the spec, or for testing in encrypted mode, some configurations are needed on mosquitto. Private keys and certificates should be set on both client side and server side. The [mosquitto-tls](https://mosquitto.org/man/mosquitto-tls-7.html) page might help you create all the required credentials. Once the credentials are created, the mosquitto's config files should be updated as following.
281
+
282
+ ```
283
+ $ cp mosquitto.conf samples-mosquitto.conf
284
+ $ nano mosquitto.conf
285
+ ```
286
+
287
+ The following file enables the broker to support the unencrypted mode (default) on port 1883, and the encrypted mode on port 8883. Update the path variable with the file's location on your environment.
288
+ ```
289
+ ### mosquitto.conf
290
+ # =================================================================
291
+ # General configuration
292
+ # =================================================================
293
+ .
294
+ .
295
+ .
296
+ # =================================================================
297
+ # Extra listeners
298
+ # =================================================================
299
+ .
300
+ .
301
+ listener 8883
302
+ .
303
+ .
304
+ cafile "Path to the certificate authorithy certificate file"
305
+ certfile "Path to the server certificate file"
306
+ keyfile "Path to the server private keys file"
307
+ .
308
+ .
309
+ .
310
+ ```
311
+ Finally run the server with the updated configuration file.
312
+
313
+ ```
314
+ $ mosquitto -c mosquitto.conf
315
+ ```
39
316
 
40
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
317
+ See [Mosquitto message broker page](https://mosquitto.org/) for more details.
41
318
 
319
+ ## Thanks
320
+ Special thanks to [Nicholas Humfrey](https://github.com/njh) for providing a great help with the packet serializer/deserializer.
data/lib/paho-mqtt.rb CHANGED
@@ -1,9 +1,8 @@
1
- require "paho.mqtt/version"
2
- require "paho.mqtt/paho_client"
3
- require "paho.mqtt/packet_manager"
1
+ require "paho_mqtt/version"
2
+ require "paho_mqtt/client"
3
+ require "paho_mqtt/packet"
4
4
 
5
5
  module PahoMqtt
6
-
7
6
  # Default connection setup
8
7
  DEFAULT_SSL_PORT = 8883
9
8
  DEFAULT_PORT = 1883
@@ -13,24 +12,43 @@ module PahoMqtt
13
12
  RECONNECT_RETRY_TEMPO = 5
14
13
 
15
14
  # MAX size of queue
15
+ MAX_READ = 10
16
16
  MAX_PUBACK = 20
17
17
  MAX_PUBREC = 20
18
18
  MAX_PUBREL = 20
19
19
  MAX_PUBCOMP = 20
20
20
  MAX_WRITING = MAX_PUBACK + MAX_PUBREC + MAX_PUBREL + MAX_PUBCOMP
21
-
21
+
22
22
  # Connection states values
23
23
  MQTT_CS_NEW = 0
24
24
  MQTT_CS_CONNECTED = 1
25
25
  MQTT_CS_DISCONNECT = 2
26
- MQTT_CS_CONNECT_ASYNC = 3
27
-
26
+
28
27
  # Error values
29
28
  MQTT_ERR_SUCCESS = 0
30
29
  MQTT_ERR_FAIL = 1
31
30
 
31
+ PACKET_TYPES = [
32
+ nil,
33
+ PahoMqtt::Packet::Connect,
34
+ PahoMqtt::Packet::Connack,
35
+ PahoMqtt::Packet::Publish,
36
+ PahoMqtt::Packet::Puback,
37
+ PahoMqtt::Packet::Pubrec,
38
+ PahoMqtt::Packet::Pubrel,
39
+ PahoMqtt::Packet::Pubcomp,
40
+ PahoMqtt::Packet::Subscribe,
41
+ PahoMqtt::Packet::Suback,
42
+ PahoMqtt::Packet::Unsubscribe,
43
+ PahoMqtt::Packet::Unsuback,
44
+ PahoMqtt::Packet::Pingreq,
45
+ PahoMqtt::Packet::Pingresp,
46
+ PahoMqtt::Packet::Disconnect,
47
+ nil
48
+ ]
49
+
32
50
  Thread.abort_on_exception = true
33
-
51
+
34
52
  class Exception < ::Exception
35
53
  end
36
54
 
@@ -12,6 +12,8 @@ module PahoMqtt
12
12
  attr_accessor :port
13
13
  attr_accessor :mqtt_version
14
14
  attr_accessor :clean_session
15
+ attr_accessor :persistent
16
+ attr_accessor :blocking
15
17
  attr_accessor :client_id
16
18
  attr_accessor :username
17
19
  attr_accessor :password
@@ -23,10 +25,9 @@ module PahoMqtt
23
25
  attr_accessor :will_qos
24
26
  attr_accessor :will_retain
25
27
 
26
- # Setting attributes:
28
+ # Timeout attributes:
27
29
  attr_accessor :keep_alive
28
30
  attr_accessor :ack_timeout
29
- attr_accessor :persistent
30
31
 
31
32
  #Callback attributes
32
33
  attr_accessor :on_message
@@ -50,6 +51,7 @@ module PahoMqtt
50
51
  :mqtt_version => '3.1.1',
51
52
  :clean_session => true,
52
53
  :persistent => false,
54
+ :blocking => false,
53
55
  :client_id => nil,
54
56
  :username => nil,
55
57
  :password => nil,
@@ -82,7 +84,12 @@ module PahoMqtt
82
84
  end
83
85
 
84
86
  if @port.nil?
85
- @port = @ssl ? PahoMqtt::DEFAULT_SSL_PORT : PahoMqtt::DEFAULT_PORT
87
+ @port
88
+ if @ssl
89
+ @port = PahoMqtt::DEFAULT_SSL_PORT
90
+ else
91
+ @port = PahoMqtt::DEFAULT_PORT
92
+ end
86
93
  end
87
94
 
88
95
  if @client_id.nil? || @client_id == ""
@@ -123,6 +130,10 @@ module PahoMqtt
123
130
  @client_id = prefix << Array.new(lenght) { charset.sample }.join
124
131
  end
125
132
 
133
+ def ssl_context
134
+ @ssl_context ||= OpenSSL::SSL::SSLContext.new
135
+ end
136
+
126
137
  def config_ssl_context(cert_path, key_path, ca_path=nil)
127
138
  @ssl ||= true
128
139
  @ssl_context = ssl_context
@@ -130,25 +141,6 @@ module PahoMqtt
130
141
  self.key = key_path
131
142
  self.root_ca = ca_path
132
143
  end
133
-
134
- def ssl_context
135
- @ssl_context ||= OpenSSL::SSL::SSLContext.new
136
- end
137
-
138
- def cert=(cert_path)
139
- ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
140
- end
141
-
142
- def key=(key_path, passphrase=nil)
143
- ssl_context.key = OpenSSL::PKey::RSA.new(File.read(key_path), passphrase)
144
- end
145
-
146
- def root_ca=(ca_path)
147
- ssl_context.ca_file = ca_path
148
- unless @ca_path.nil?
149
- ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
150
- end
151
- end
152
144
 
153
145
  def config_will(topic, payload="", retain=false, qos=0)
154
146
  @will_topic = topic
@@ -157,25 +149,31 @@ module PahoMqtt
157
149
  @will_qos = qos
158
150
  end
159
151
 
160
- def connect(host=@host, port=@port, keep_alive=@keep_alive, persistent=@persistent)
152
+ def connect(host=@host, port=@port, keep_alive=@keep_alive, persistent=@persistent, blocking=@blocking)
161
153
  @persistent = persistent
162
- @connection_state_mutex.synchronize {
163
- @connection_state = MQTT_CS_NEW
164
- }
165
- connect_async(host, port, keep_alive)
166
- end
167
-
168
- def connect_async(host, port=1883, keep_alive)
154
+ @blocking = blocking
169
155
  @host = host
170
156
  @port = port.to_i
171
157
  @keep_alive = keep_alive
172
-
173
158
  @connection_state_mutex.synchronize {
174
- @connection_state = MQTT_CS_CONNECT_ASYNC
159
+ @connection_state = MQTT_CS_NEW
175
160
  }
176
161
  setup_connection
162
+ connect_async(host, port, keep_alive) unless @blocking
177
163
  end
178
164
 
165
+ def connect_async(host, port=1883, keep_alive)
166
+ @mqtt_thread = Thread.new do
167
+ @reconnect_thread.kill unless @reconnect_thread.nil? || !@reconnect_thread.alive?
168
+ while connected? do
169
+ mqtt_loop
170
+ end
171
+ end
172
+ end
173
+
174
+ def connected?
175
+ @connection_state == MQTT_CS_CONNECTED
176
+ end
179
177
 
180
178
  def loop_write(max_packet=MAX_WRITING)
181
179
  @writing_mutex.synchronize {
@@ -187,12 +185,12 @@ module PahoMqtt
187
185
  }
188
186
  end
189
187
 
190
- def loop_read(max_packet=5)
188
+ def loop_read(max_packet=MAX_READ)
191
189
  max_packet.times do
192
190
  receive_packet
193
191
  end
194
192
  end
195
-
193
+
196
194
  def mqtt_loop
197
195
  loop_read
198
196
  loop_write
@@ -210,20 +208,19 @@ module PahoMqtt
210
208
  check_ack_alive(@waiting_unsuback, @unsuback_mutex, @waiting_unsuback.length)
211
209
  end
212
210
 
213
-
214
211
  def reconnect(retry_time=RECONNECT_RETRY_TIME, retry_tempo=RECONNECT_RETRY_TEMPO)
215
212
  @reconnect_thread = Thread.new do
216
213
  retry_time.times do
217
214
  @logger.debug("New reconnect atempt...") if @logger.is_a?(Logger)
218
- setup_connection
219
- if @connection_state == MQTT_CS_CONNECTED
215
+ connect
216
+ if connected?
220
217
  break
221
218
  else
222
219
  sleep retry_tempo
223
220
  end
224
221
  end
225
- if @connection_state != MQTT_CS_CONNECTED
226
- @logger.error("Reconnection atempt counter is over.(#{RECONNECT_RETRY_TIME} times)") if @logger.is_a?(Logger)
222
+ unless connected?
223
+ @logger.error("Reconnection atempt counter is over.(#{RECONNECT_RETRY_TIME} times)") if @logger.is_a?(Logger)
227
224
  disconnect(false)
228
225
  exit
229
226
  end
@@ -260,7 +257,7 @@ module PahoMqtt
260
257
  }
261
258
  end
262
259
 
263
- @socket.close unless @socket.nil?
260
+ @socket.close unless @socket.nil? || @socket.closed?
264
261
  @socket = nil
265
262
 
266
263
  @connection_state_mutex.synchronize {
@@ -268,7 +265,7 @@ module PahoMqtt
268
265
  }
269
266
  MQTT_ERR_SUCCESS
270
267
  end
271
-
268
+
272
269
  def publish(topic, payload="", retain=false, qos=0)
273
270
  if topic == "" || !topic.is_a?(String)
274
271
  @logger.error("Publish topics is invalid, not a string or empty.") if @logger.is_a?(Logger)
@@ -278,7 +275,7 @@ module PahoMqtt
278
275
  end
279
276
 
280
277
  def subscribe(*topics)
281
- unless topics.length == 0
278
+ unless valid_topics?(topics) == MQTT_ERR_FAIL
282
279
  send_subscribe(topics)
283
280
  else
284
281
  @logger.error("Subscribe topics need one topic or a list of topics.") if @logger.is_a?(Logger)
@@ -288,7 +285,7 @@ module PahoMqtt
288
285
  end
289
286
 
290
287
  def unsubscribe(*topics)
291
- unless topics.length == 0
288
+ unless valid_topics?(topics) == MQTT_ERR_FAIL
292
289
  send_unsubscribe(topics)
293
290
  else
294
291
  @logger.error("Unsubscribe need at least one topics.") if @logger.is_a?(Logger)
@@ -300,7 +297,7 @@ module PahoMqtt
300
297
  def ping_host
301
298
  send_pingreq
302
299
  end
303
-
300
+
304
301
  def add_topic_callback(topic, callback=nil, &block)
305
302
  if topic.nil?
306
303
  @logger.error("The topics where the callback is trying to be registered have been found nil.") if @logger.is_a?(Logger)
@@ -411,7 +408,7 @@ module PahoMqtt
411
408
 
412
409
  def config_socket
413
410
  unless @socket.nil?
414
- @socket.close
411
+ @socket.close unless @socket.closed?
415
412
  @socket = nil
416
413
  end
417
414
 
@@ -437,6 +434,21 @@ module PahoMqtt
437
434
  end
438
435
  end
439
436
 
437
+ def cert=(cert_path)
438
+ ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
439
+ end
440
+
441
+ def key=(key_path)
442
+ ssl_context.key = OpenSSL::PKey::RSA.new(File.read(key_path))
443
+ end
444
+
445
+ def root_ca=(ca_path)
446
+ ssl_context.ca_file = ca_path
447
+ unless @ca_path.nil?
448
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
449
+ end
450
+ end
451
+
440
452
  def setup_connection
441
453
  @mqtt_thread.kill unless @mqtt_thread.nil?
442
454
  if @host.nil? || @host == ""
@@ -449,44 +461,38 @@ module PahoMqtt
449
461
  raise ParameterException
450
462
  end
451
463
 
452
- @socket.close unless @socket.nil?
464
+ @socket.close unless @socket.nil? || @socket.closed?
453
465
  @socket = nil
454
466
 
455
- @last_ping_req = Time.now
456
- @last_ping_resp = Time.now
467
+ @last_ping_req = Time.now
468
+ @last_ping_resp = Time.now
457
469
 
458
- @logger.debug("Atempt to connect to host: #{@host}") if @logger.is_a?(Logger)
459
- config_socket
470
+ @logger.debug("Atempt to connect to host: #{@host}") if @logger.is_a?(Logger)
471
+ config_socket
460
472
 
461
- unless @socket.nil?
462
- send_connect
463
- # Waiting a Connack packet for "ack_timeout" second from the remote
464
- connect_timeout = Time.now + @ack_timeout
465
- while (Time.now <= connect_timeout) && (@connection_state != MQTT_CS_CONNECTED) do
466
- receive_packet
467
- sleep 0.0001
468
- end
469
- end
473
+ unless @socket.nil?
474
+ send_connect
470
475
 
471
- if @connection_state != MQTT_CS_CONNECTED
472
- @logger.warn("Connection failed. Couldn't recieve a Connack packet from: #{@host}, socket is \"#{@socket}\".") if @logger.is_a?(Logger)
473
-
474
- unless Thread.current == @reconnect_thread
475
- raise Exception.new("Connection failed. Check log for more details.")
476
- end
477
- else
478
- config_subscription
479
- @mqtt_thread = Thread.new do
480
- @reconnect_thread.kill unless @reconnect_thread.nil? || !@reconnect_thread.alive?
481
- while @connection_state == MQTT_CS_CONNECTED do
482
- mqtt_loop
483
- end
484
- end
485
- end
476
+ # Waiting a Connack packet for "ack_timeout" second from the remote
477
+ connect_timeout = Time.now + @ack_timeout
478
+ while (Time.now <= connect_timeout) && (!connected?) do
479
+ receive_packet
480
+ sleep 0.0001
481
+ end
482
+ end
483
+
484
+ unless connected?
485
+ @logger.warn("Connection failed. Couldn't recieve a Connack packet from: #{@host}, socket is \"#{@socket}\".") if @logger.is_a?(Logger)
486
+ unless Thread.current == @reconnect_thread
487
+ raise Exception.new("Connection failed. Check log for more details.")
488
+ end
489
+ else
490
+ config_subscription if Thread.current == @reconnect_thread
491
+ end
486
492
  end
487
493
 
488
494
  def check_keep_alive
489
- if @keep_alive >= 0 && @connection_state == MQTT_CS_CONNECTED
495
+ if @keep_alive >= 0 && connected?
490
496
  now = Time.now
491
497
  timeout_req = (@last_ping_req + (@keep_alive * 0.7).ceil)
492
498
 
@@ -497,7 +503,7 @@ module PahoMqtt
497
503
 
498
504
  timeout_resp = @last_ping_resp + (@keep_alive * 1.1).ceil
499
505
  if timeout_resp <= now
500
- @logger.debug("No activity over timeout, trying to reconnect to #{@host}") if @logger.is_a?(Logger)
506
+ @logger.debug("No activity period over timeout, disconnecting from #{@host}") if @logger.is_a?(Logger)
501
507
  disconnect(false)
502
508
  reconnect if @persistent
503
509
  end
@@ -523,9 +529,9 @@ module PahoMqtt
523
529
 
524
530
  def receive_packet
525
531
  begin
526
- result = IO.select([@socket], [], [], SELECT_TIMEOUT) unless @socket.nil?
532
+ result = IO.select([@socket], [], [], SELECT_TIMEOUT) unless @socket.nil? || @socket.closed?
527
533
  unless result.nil?
528
- packet = PahoMqtt::Packet.read(@socket)
534
+ packet = PahoMqtt::Packet::Base.read(@socket)
529
535
  unless packet.nil?
530
536
  handle_packet packet
531
537
  @last_ping_resp = Time.now
@@ -591,7 +597,7 @@ module PahoMqtt
591
597
  send_packet(m)
592
598
  end
593
599
  }
594
- @on_connack.call unless @on_connack.nil?
600
+ @on_connack.call(packet) unless @on_connack.nil?
595
601
  end
596
602
 
597
603
  def handle_pingresp
@@ -623,7 +629,7 @@ module PahoMqtt
623
629
  @subscribed_mutex.synchronize {
624
630
  @subscribed_topics.concat(adjust_qos)
625
631
  }
626
- @on_suback.call unless @on_suback.nil?
632
+ @on_suback.call(adjust_qos) unless @on_suback.nil?
627
633
  end
628
634
 
629
635
  def handle_unsuback(packet)
@@ -641,10 +647,10 @@ module PahoMqtt
641
647
 
642
648
  @subscribed_mutex.synchronize {
643
649
  to_unsub.each do |filter|
644
- @subscribed_topics.delete_if { |topic| match_filter(topic.first, filter) }
650
+ @subscribed_topics.delete_if { |topic| match_filter(topic.first, filter.first) }
645
651
  end
646
652
  }
647
- @on_unsuback.call unless @on_unsuback.nil?
653
+ @on_unsuback.call(to_unsub) unless @on_unsuback.nil?
648
654
  end
649
655
 
650
656
  def handle_publish(packet)
@@ -660,14 +666,14 @@ module PahoMqtt
660
666
  end
661
667
 
662
668
  @on_message.call(packet) unless @on_message.nil?
663
- @registered_callback.assoc(packet.topic).last.call if @registered_callback.any? { |pair| pair.first == packet.topic}
669
+ @registered_callback.assoc(packet.topic).last.call(packet) if @registered_callback.any? { |pair| pair.first == packet.topic}
664
670
  end
665
671
 
666
672
  def handle_puback(packet)
667
673
  @puback_mutex.synchronize{
668
674
  @waiting_puback.delete_if { |pck| pck[:id] == packet.id }
669
675
  }
670
- @on_puback.call unless @on_puback.nil?
676
+ @on_puback.call(packet) unless @on_puback.nil?
671
677
  end
672
678
 
673
679
  def handle_pubrec(packet)
@@ -675,7 +681,7 @@ module PahoMqtt
675
681
  @waiting_pubrec.delete_if { |pck| pck[:id] == packet.id }
676
682
  }
677
683
  send_pubrel(packet.id)
678
- @on_pubrec.call unless @on_pubrec.nil?
684
+ @on_pubrec.call(packet) unless @on_pubrec.nil?
679
685
  end
680
686
 
681
687
  def handle_pubrel(packet)
@@ -683,14 +689,14 @@ module PahoMqtt
683
689
  @waiting_pubrel.delete_if { |pck| pck[:id] == packet.id }
684
690
  }
685
691
  send_pubcomp(packet.id)
686
- @on_pubrel.call unless @on_pubrel.nil?
692
+ @on_pubrel.call(packet) unless @on_pubrel.nil?
687
693
  end
688
694
 
689
695
  def handle_pubcomp(packet)
690
696
  @pubcomp_mutex.synchronize {
691
697
  @waiting_pubcomp.delete_if { |pck| pck[:id] == packet.id }
692
698
  }
693
- @on_pubcomp.call unless @on_pubcomp.nil?
699
+ @on_pubcomp.call(packet) unless @on_pubcomp.nil?
694
700
  end
695
701
 
696
702
  def handle_connack_error(return_code)
@@ -715,15 +721,24 @@ module PahoMqtt
715
721
  MQTT_ERR_FAIL
716
722
  end
717
723
  end
718
-
724
+
719
725
  def send_packet(packet)
720
- @socket.write(packet.to_s)
721
- @logger.info("A packet #{packet.class} have been sent.") if @logger.is_a?(Logger)
722
- @last_ping_req = Time.now
723
- MQTT_ERR_SUCCESS
726
+ begin
727
+ @socket.write(packet.to_s) unless @socket.nil? || @socket.closed?
728
+ @logger.info("A packet #{packet.class} have been sent.") if @logger.is_a?(Logger)
729
+ @last_ping_req = Time.now
730
+ MQTT_ERR_SUCCESS
731
+ rescue ::Exception => exp
732
+ disconnect(false)
733
+ if @persistent
734
+ reconnect
735
+ else
736
+ @logger.error("Trying to right a packet on a nil socket.") if @logger.is_a?(Logger)
737
+ raise(exp)
738
+ end
739
+ end
724
740
  end
725
741
 
726
-
727
742
  def append_to_writing(packet)
728
743
  @writing_mutex.synchronize {
729
744
  @writing_queue.push(packet)
@@ -762,23 +777,25 @@ module PahoMqtt
762
777
  end
763
778
 
764
779
  def send_subscribe(topics)
765
- unless topics.length == 0
780
+ unless valid_topics?(topics) == MQTT_ERR_FAIL
766
781
  new_id = next_packet_id
767
782
  packet = PahoMqtt::Packet::Subscribe.new(
768
783
  :id => new_id,
769
784
  :topics => topics
770
785
  )
771
-
772
786
  append_to_writing(packet)
773
787
  @suback_mutex.synchronize {
774
788
  @waiting_suback.push({ :id => new_id, :packet => packet, :timestamp => Time.now })
775
789
  }
776
- end
777
- MQTT_ERR_SUCCESS
790
+ MQTT_ERR_SUCCESS
791
+ else
792
+ disconnect(false)
793
+ raise ProtocolViolation
794
+ end
778
795
  end
779
796
 
780
797
  def send_unsubscribe(topics)
781
- unless topics.length == 0
798
+ unless valid_topics?(topics) == MQTT_ERR_FAIL
782
799
  new_id = next_packet_id
783
800
  packet = PahoMqtt::Packet::Unsubscribe.new(
784
801
  :id => new_id,
@@ -789,8 +806,11 @@ module PahoMqtt
789
806
  @unsuback_mutex.synchronize {
790
807
  @waiting_unsuback.push({:id => new_id, :packet => packet, :timestamp => Time.now})
791
808
  }
809
+ MQTT_ERR_SUCCESS
810
+ else
811
+ disconnect(false)
812
+ raise ProtocolViolation
792
813
  end
793
- MQTT_ERR_SUCCESS
794
814
  end
795
815
 
796
816
  def send_publish(topic, payload, retain, qos)
@@ -862,6 +882,17 @@ module PahoMqtt
862
882
  MQTT_ERR_SUCCESS
863
883
  end
864
884
 
885
+ def valid_topics?(topics)
886
+ unless topics.length == 0
887
+ topics.map do |topic|
888
+ return MQTT_ERR_FAIL if topic.first == ""
889
+ end
890
+ else
891
+ MQTT_ERR_FAIL
892
+ end
893
+ MQTT_ERR_SUCCESS
894
+ end
895
+
865
896
  def match_filter(topics, filters)
866
897
  if topics.is_a?(String) && filters.is_a?(String)
867
898
  topic = topics.split('/')