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 +4 -4
- data/README.md +289 -10
- data/lib/paho-mqtt.rb +26 -8
- data/lib/{paho.mqtt/paho_client.rb → paho_mqtt/client.rb} +130 -99
- data/lib/paho_mqtt/packet.rb +19 -0
- data/lib/paho_mqtt/packet/base.rb +290 -0
- data/lib/paho_mqtt/packet/connack.rb +84 -0
- data/lib/paho_mqtt/packet/connect.rb +149 -0
- data/lib/paho_mqtt/packet/disconnect.rb +20 -0
- data/lib/paho_mqtt/packet/pingreq.rb +12 -0
- data/lib/paho_mqtt/packet/pingresp.rb +20 -0
- data/lib/paho_mqtt/packet/puback.rb +27 -0
- data/lib/paho_mqtt/packet/pubcomp.rb +26 -0
- data/lib/paho_mqtt/packet/publish.rb +127 -0
- data/lib/paho_mqtt/packet/pubrec.rb +26 -0
- data/lib/paho_mqtt/packet/pubrel.rb +44 -0
- data/lib/paho_mqtt/packet/suback.rb +56 -0
- data/lib/paho_mqtt/packet/subscribe.rb +104 -0
- data/lib/paho_mqtt/packet/unsuback.rb +31 -0
- data/lib/paho_mqtt/packet/unsubscribe.rb +65 -0
- data/lib/{paho.mqtt → paho_mqtt}/version.rb +1 -1
- data/paho-mqtt.gemspec +2 -2
- data/samples/client_blocking(writing).rb +16 -0
- data/samples/client_blocking.rb +34 -0
- data/samples/getting_started.rb +49 -0
- metadata +24 -8
- data/lib/paho.mqtt/packet_manager.rb +0 -1056
- data/paho-mqtt-0.0.1.gem +0 -0
- data/samples/test_aws.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16c0a069f566b0c91b896cbcc0a4b3ca2eb694f6
|
4
|
+
data.tar.gz: bd03230d51b7bc37be071caac903ce8537073b6b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c3a8a75de74ef2df07b3e6df1abcf694e07dd425e589169a8c7696e8131b55aba92b41daf6525e60ef3210347f33139ea399eb8092877dedc40c6f986b022e5
|
7
|
+
data.tar.gz: 53e67740ea1e52c1a0e73517079d1b7b0ce1f15802ea22f9a880a00ba2e514b7e715e7a06b293931e1a561cbd2beb2dd8439dee906a6fdfbc183c13f0ce7f87c
|
data/README.md
CHANGED
@@ -1,8 +1,30 @@
|
|
1
1
|
# PahoMqtt
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 "
|
2
|
-
require "
|
3
|
-
require "
|
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
|
-
|
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
|
-
#
|
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
|
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
|
-
@
|
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 =
|
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=
|
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
|
-
|
219
|
-
if
|
215
|
+
connect
|
216
|
+
if connected?
|
220
217
|
break
|
221
218
|
else
|
222
219
|
sleep retry_tempo
|
223
220
|
end
|
224
221
|
end
|
225
|
-
|
226
|
-
|
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
|
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
|
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
|
-
|
456
|
-
|
467
|
+
@last_ping_req = Time.now
|
468
|
+
@last_ping_resp = Time.now
|
457
469
|
|
458
|
-
|
459
|
-
|
470
|
+
@logger.debug("Atempt to connect to host: #{@host}") if @logger.is_a?(Logger)
|
471
|
+
config_socket
|
460
472
|
|
461
|
-
|
462
|
-
|
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
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
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 &&
|
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,
|
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
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
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
|
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
|
-
|
777
|
-
|
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
|
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('/')
|