mqtt 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Y2MwODVmNmViNWEyM2ViM2NkMDk1ODIzNzg3ZGJiM2EyYTdlZTJlMw==
5
+ data.tar.gz: !binary |-
6
+ OWI1ZGM5Yzk4OWZiZDBkMDY5ZDM5NTllMWQ3YzRhYTk3NThmNDY3Ng==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NDg1ZTMxZTdkNjM2ZDk2ZmU0ZGRiYzcyNWI0ZDk1MWI1MGU3OTRmMmI3Yzkx
10
+ NmRiYzQ0NmE3NDlhNDQ1MDExNmY2OGM2NzNjYWNlMTAwODc0NDRiNzM3MDU1
11
+ YWYxYTYyNDdmZTUxNjI2YjM2ODU2ZTgxMGVhNjM1NjAwOWViY2I=
12
+ data.tar.gz: !binary |-
13
+ YzkwNjYyYzYxODgyYzZjZWU2MGZhMzQ0YWZhNWM3MTVhNzdjNTFjMjEzYWFj
14
+ OGMzN2NlYjNmYTdjYzVmODNhZDZmZGIzMDg4ZjBjYmY5MzljNzRlODljMjI0
15
+ NTI3OGRmZDhhODA0NDA2NDI5ZTBlYzhjZWE3MWJhNGQwYzE4OWQ=
data/NEWS.md CHANGED
@@ -1,6 +1,19 @@
1
1
  Ruby MQTT NEWS
2
2
  ==============
3
3
 
4
+ Ruby MQTT Version 0.4.0 (2016-06-27)
5
+ ------------------------------------
6
+
7
+ * Added puback handling for QoS level 1
8
+ * Low-level MQTT-SN packet parsing support
9
+ * Allow certs to be set directly instead of just by file
10
+ * Allow keyphrase for certs to be passed through
11
+ * Put 'disconnect' inside an 'ensure' block
12
+ * Fix for error on publish with frozen payload
13
+ * Fix for packets always getting id 1
14
+ * Improvements to tests
15
+
16
+
4
17
  Ruby MQTT Version 0.3.1 (2014-10-10)
5
18
  ------------------------------------
6
19
 
data/README.md CHANGED
@@ -5,6 +5,8 @@ ruby-mqtt
5
5
 
6
6
  Pure Ruby gem that implements the [MQTT] protocol, a lightweight protocol for publish/subscribe messaging.
7
7
 
8
+ Also includes a class for parsing and generating [MQTT-SN] packets.
9
+
8
10
 
9
11
  Table of Contents
10
12
  -----------------
@@ -31,22 +33,23 @@ Alternatively, to use a development snapshot from GitHub using [Bundler]:
31
33
  Quick Start
32
34
  -----------
33
35
 
34
- require 'rubygems'
35
- require 'mqtt'
36
-
37
- # Publish example
38
- MQTT::Client.connect('test.mosquitto.org') do |c|
39
- c.publish('topic', 'message')
40
- end
41
-
42
- # Subscribe example
43
- MQTT::Client.connect('test.mosquitto.org') do |c|
44
- # If you pass a block to the get method, then it will loop
45
- c.get('test') do |topic,message|
46
- puts "#{topic}: #{message}"
47
- end
48
- end
36
+ ~~~ ruby
37
+ require 'rubygems'
38
+ require 'mqtt'
49
39
 
40
+ # Publish example
41
+ MQTT::Client.connect('test.mosquitto.org') do |c|
42
+ c.publish('test', 'message')
43
+ end
44
+
45
+ # Subscribe example
46
+ MQTT::Client.connect('test.mosquitto.org') do |c|
47
+ # If you pass a block to the get method, then it will loop
48
+ c.get('test') do |topic,message|
49
+ puts "#{topic}: #{message}"
50
+ end
51
+ end
52
+ ~~~
50
53
 
51
54
 
52
55
  Library Overview
@@ -56,42 +59,52 @@ Library Overview
56
59
 
57
60
  A new client connection can be created by passing either a [MQTT URI], a host and port or by passing a hash of attributes.
58
61
 
59
- client = MQTT::Client.connect('mqtt://myserver.example.com')
60
- client = MQTT::Client.connect('mqtts://user:pass@myserver.example.com')
61
- client = MQTT::Client.connect('myserver.example.com')
62
- client = MQTT::Client.connect('myserver.example.com', 18830)
63
- client = MQTT::Client.connect(:host => 'myserver.example.com', :port => 1883 ... )
62
+ ~~~ ruby
63
+ client = MQTT::Client.connect('mqtt://myserver.example.com')
64
+ client = MQTT::Client.connect('mqtts://user:pass@myserver.example.com')
65
+ client = MQTT::Client.connect('myserver.example.com')
66
+ client = MQTT::Client.connect('myserver.example.com', 18830)
67
+ client = MQTT::Client.connect(:host => 'myserver.example.com', :port => 1883 ... )
68
+ ~~~
64
69
 
65
70
  TLS/SSL is not enabled by default, to enabled it, pass ```:ssl => true```:
66
71
 
67
- client = MQTT::Client.connect(
68
- :host => 'test.mosquitto.org',
69
- :port => 8883
70
- :ssl => true
71
- )
72
+ ~~~ ruby
73
+ client = MQTT::Client.connect(
74
+ :host => 'test.mosquitto.org',
75
+ :port => 8883,
76
+ :ssl => true
77
+ )
78
+ ~~~
72
79
 
73
80
  Alternatively you can create a new Client object and then configure it by setting attributes. This example shows setting up client certificate based authentication:
74
81
 
75
- client = MQTT::Client.new
76
- client.host = 'myserver.example.com'
77
- client.ssl = true
78
- client.cert_file = path_to('client.pem')
79
- client.key_file = path_to('client.key')
80
- client.ca_file = path_to('root-ca.pem')
81
- client.connect()
82
+ ~~~ ruby
83
+ client = MQTT::Client.new
84
+ client.host = 'myserver.example.com'
85
+ client.ssl = true
86
+ client.cert_file = path_to('client.pem')
87
+ client.key_file = path_to('client.key')
88
+ client.ca_file = path_to('root-ca.pem')
89
+ client.connect()
90
+ ~~~
82
91
 
83
92
  The connection can either be made without the use of a block:
84
93
 
85
- client = MQTT::Client.connect('test.mosquitto.org')
86
- # perform operations
87
- client.disconnect()
94
+ ~~~ ruby
95
+ client = MQTT::Client.connect('test.mosquitto.org')
96
+ # perform operations
97
+ client.disconnect()
98
+ ~~~
88
99
 
89
100
  Or, if using a block, with an implicit disconnection at the end of the block.
90
101
 
91
- MQTT::Client.connect('test.mosquitto.org') do |client|
92
- # perform operations
93
- end
94
-
102
+ ~~~ ruby
103
+ MQTT::Client.connect('test.mosquitto.org') do |client|
104
+ # perform operations
105
+ end
106
+ ~~~
107
+
95
108
  For more information, see and list of attributes for the [MQTT::Client] class and the [MQTT::Client.connect] method.
96
109
 
97
110
 
@@ -99,7 +112,9 @@ For more information, see and list of attributes for the [MQTT::Client] class an
99
112
 
100
113
  To send a message to a topic, use the ```publish``` method:
101
114
 
102
- client.publish(topic, payload, retain=false)
115
+ ~~~ ruby
116
+ client.publish(topic, payload, retain=false)
117
+ ~~~
103
118
 
104
119
  The method will return once the message has been sent to the MQTT server.
105
120
 
@@ -110,9 +125,11 @@ For more information see the [MQTT::Client#publish] method.
110
125
 
111
126
  You can send a subscription request to the MQTT server using the subscribe method. One or more [Topic Filters] may be passed in:
112
127
 
113
- client.subscribe( 'topic1' )
114
- client.subscribe( 'topic1', 'topic2' )
115
- client.subscribe( 'foo/#' )
128
+ ~~~ ruby
129
+ client.subscribe( 'topic1' )
130
+ client.subscribe( 'topic1', 'topic2' )
131
+ client.subscribe( 'foo/#' )
132
+ ~~~
116
133
 
117
134
  For more information see the [MQTT::Client#subscribe] method.
118
135
 
@@ -121,23 +138,52 @@ For more information see the [MQTT::Client#subscribe] method.
121
138
 
122
139
  To receive a message, use the get method. This method will block until a message is available. The topic is the name of the topic the message was sent to. The message is a string:
123
140
 
124
- topic,message = client.get
141
+ ~~~ ruby
142
+ topic,message = client.get
143
+ ~~~
125
144
 
126
145
  Alternatively, you can give the get method a block, which will be called for every message received and loop forever:
127
146
 
128
- client.get do |topic,message|
129
- # Block is executed for every message received
130
- end
147
+ ~~~ ruby
148
+ client.get do |topic,message|
149
+ # Block is executed for every message received
150
+ end
151
+ ~~~
131
152
 
132
153
  For more information see the [MQTT::Client#get] method.
133
154
 
134
155
 
156
+ ### Parsing and serialising of packets ###
157
+
158
+ The parsing and serialising of MQTT and MQTT-SN packets is a separate lower-level API.
159
+ You can use it to build your own clients and servers, without using any of the rest of the
160
+ code in this gem.
161
+
162
+ ~~~ ruby
163
+ # Parse a string containing a binary packet into an object
164
+ packet_obj = MQTT::Packet.parse(binary_packet)
165
+
166
+ # Write a PUBACK packet to an IO handle
167
+ ios << MQTT::Packet::Puback(:id => 20)
168
+
169
+ # Write an MQTT-SN Publish packet with QoS -1 to a UDP socket
170
+ socket = UDPSocket.new
171
+ socket.connect('localhost', MQTT::SN::DEFAULT_PORT)
172
+ socket << MQTT::SN::Packet::Publish.new(
173
+ :topic_id => 'TT',
174
+ :topic_id_type => :short,
175
+ :data => "The time is: #{Time.now}",
176
+ :qos => -1
177
+ )
178
+ socket.close
179
+ ~~~
135
180
 
136
181
  Limitations
137
182
  -----------
138
183
 
139
- * Only QOS 0 currently supported
184
+ * QoS 2 is not currently supported by client
140
185
  * Automatic re-connects to the server are not supported
186
+ * No local persistence for packets
141
187
 
142
188
 
143
189
  Resources
@@ -146,6 +192,7 @@ Resources
146
192
  * API Documentation: http://rubydoc.info/gems/mqtt
147
193
  * Protocol Specification v3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html
148
194
  * Protocol Specification v3.1: http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html
195
+ * MQTT-SN Protocol Specification v1.2: http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf
149
196
  * MQTT Homepage: http://www.mqtt.org/
150
197
  * GitHub Project: http://github.com/njh/ruby-mqtt
151
198
 
@@ -153,7 +200,7 @@ Resources
153
200
  License
154
201
  -------
155
202
 
156
- The ruby-mqtt gem is licensed under the terms of the MIT license.
203
+ The mqtt ruby gem is licensed under the terms of the MIT license.
157
204
  See the file LICENSE for details.
158
205
 
159
206
 
@@ -162,11 +209,14 @@ Contact
162
209
 
163
210
  * Author: Nicholas J Humfrey
164
211
  * Email: njh@aelius.com
212
+ * Twitter: [@njh]
165
213
  * Home Page: http://www.aelius.com/njh/
166
214
 
167
215
 
168
216
 
217
+ [@njh]: http://twitter.com/njh
169
218
  [MQTT]: http://www.mqtt.org/
219
+ [MQTT-SN]: http://mqtt.org/2013/12/mqtt-for-sensor-networks-mqtt-sn
170
220
  [Rubygems]: http://rubygems.org/
171
221
  [Bundler]: http://bundler.io/
172
222
  [MQTT URI]: https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme
@@ -21,7 +21,7 @@ module MQTT
21
21
  DEFAULT_SSL_PORT = 8883
22
22
 
23
23
  # Super-class for other MQTT related exceptions
24
- class Exception < Exception
24
+ class Exception < ::Exception
25
25
  end
26
26
 
27
27
  # A ProtocolException will be raised if there is a
@@ -39,4 +39,17 @@ module MQTT
39
39
  autoload :Packet, 'mqtt/packet'
40
40
  autoload :Proxy, 'mqtt/proxy'
41
41
 
42
+ # MQTT-SN
43
+ module SN
44
+
45
+ # Default port number for unencrypted connections
46
+ DEFAULT_PORT = 1883
47
+
48
+ # A ProtocolException will be raised if there is a
49
+ # problem with data received from a remote host
50
+ class ProtocolException < MQTT::Exception
51
+ end
52
+
53
+ autoload :Packet, 'mqtt/sn/packet'
54
+ end
42
55
  end
@@ -53,7 +53,7 @@ class MQTT::Client
53
53
  # If the Will message should be retain by the server after it is sent
54
54
  attr_accessor :will_retain
55
55
 
56
- #Last ping response time
56
+ # Last ping response time
57
57
  attr_reader :last_ping_response
58
58
 
59
59
 
@@ -174,13 +174,14 @@ class MQTT::Client
174
174
  end
175
175
 
176
176
  # Initialise private instance variables
177
- @packet_id = 0
178
- @last_pingreq = Time.now
177
+ @last_ping_request = Time.now
179
178
  @last_ping_response = Time.now
180
179
  @socket = nil
181
180
  @read_queue = Queue.new
181
+ @pubacks = {}
182
182
  @read_thread = nil
183
183
  @write_semaphore = Mutex.new
184
+ @pubacks_semaphore = Mutex.new
184
185
  end
185
186
 
186
187
  # Get the OpenSSL context, that is used if SSL/TLS is enabled
@@ -190,12 +191,24 @@ class MQTT::Client
190
191
 
191
192
  # Set a path to a file containing a PEM-format client certificate
192
193
  def cert_file=(path)
193
- ssl_context.cert = OpenSSL::X509::Certificate.new(File.open(path))
194
+ self.cert = File.read(path)
195
+ end
196
+
197
+ # PEM-format client certificate
198
+ def cert=(cert)
199
+ ssl_context.cert = OpenSSL::X509::Certificate.new(cert)
194
200
  end
195
201
 
196
202
  # Set a path to a file containing a PEM-format client private key
197
- def key_file=(path)
198
- ssl_context.key = OpenSSL::PKey::RSA.new(File.open(path))
203
+ def key_file=(*args)
204
+ path, passphrase = args.flatten
205
+ ssl_context.key = OpenSSL::PKey::RSA.new(File.open(path), passphrase)
206
+ end
207
+
208
+ # Set to a PEM-format client private key
209
+ def key=(*args)
210
+ cert, passphrase = args.flatten
211
+ ssl_context.key = OpenSSL::PKey::RSA.new(cert, passphrase)
199
212
  end
200
213
 
201
214
  # Set a path to a file containing a PEM-format CA certificate and enable peer verification
@@ -287,8 +300,11 @@ class MQTT::Client
287
300
 
288
301
  # If a block is given, then yield and disconnect
289
302
  if block_given?
290
- yield(self)
291
- disconnect
303
+ begin
304
+ yield(self)
305
+ ensure
306
+ disconnect
307
+ end
292
308
  end
293
309
  end
294
310
 
@@ -315,23 +331,13 @@ class MQTT::Client
315
331
  (not @socket.nil?) and (not @socket.closed?)
316
332
  end
317
333
 
318
- # Send a MQTT ping message to indicate that the MQTT client is alive.
319
- #
320
- # Note that you will not normally need to call this method
321
- # as it is called automatically
322
- def ping
323
- packet = MQTT::Packet::Pingreq.new
324
- send_packet(packet)
325
- @last_pingreq = Time.now
326
- end
327
-
328
334
  # Publish a message on a particular topic to the MQTT server.
329
335
  def publish(topic, payload='', retain=false, qos=0)
330
336
  raise ArgumentError.new("Topic name cannot be nil") if topic.nil?
331
337
  raise ArgumentError.new("Topic name cannot be empty") if topic.empty?
332
338
 
333
339
  packet = MQTT::Packet::Publish.new(
334
- :id => @packet_id.next,
340
+ :id => next_packet_id,
335
341
  :qos => qos,
336
342
  :retain => retain,
337
343
  :topic => topic,
@@ -339,14 +345,28 @@ class MQTT::Client
339
345
  )
340
346
 
341
347
  # Send the packet
342
- send_packet(packet)
348
+ res = send_packet(packet)
349
+
350
+ if packet.qos > 0
351
+ Timeout.timeout(@ack_timeout) do
352
+ while connected? do
353
+ @pubacks_semaphore.synchronize do
354
+ return res if @pubacks.delete(packet.id)
355
+ end
356
+ # FIXME: make threads communicate with each other, instead of polling
357
+ # (using a pipe and select ?)
358
+ sleep 0.01
359
+ end
360
+ end
361
+ return -1
362
+ end
343
363
  end
344
364
 
345
365
  # Send a subscribe message for one or more topics on the MQTT server.
346
366
  # The topics parameter should be one of the following:
347
- # * String: subscribe to one topic with QOS 0
348
- # * Array: subscribe to multiple topics with QOS 0
349
- # * Hash: subscribe to multiple topics where the key is the topic and the value is the QOS level
367
+ # * String: subscribe to one topic with QoS 0
368
+ # * Array: subscribe to multiple topics with QoS 0
369
+ # * Hash: subscribe to multiple topics where the key is the topic and the value is the QoS level
350
370
  #
351
371
  # For example:
352
372
  # client.subscribe( 'a/b' )
@@ -356,7 +376,7 @@ class MQTT::Client
356
376
  #
357
377
  def subscribe(*topics)
358
378
  packet = MQTT::Packet::Subscribe.new(
359
- :id => @packet_id.next,
379
+ :id => next_packet_id,
360
380
  :topics => topics
361
381
  )
362
382
  send_packet(packet)
@@ -374,18 +394,13 @@ class MQTT::Client
374
394
  # end
375
395
  #
376
396
  def get(topic=nil)
377
- # Subscribe to a topic, if an argument is given
378
- subscribe(topic) unless topic.nil?
379
-
380
397
  if block_given?
381
- # Loop forever!
382
- loop do
383
- packet = @read_queue.pop
398
+ get_packet(topic) do |packet|
384
399
  yield(packet.topic, packet.payload)
385
400
  end
386
401
  else
387
402
  # Wait for one packet to be available
388
- packet = @read_queue.pop
403
+ packet = get_packet(topic)
389
404
  return packet.topic, packet.payload
390
405
  end
391
406
  end
@@ -410,11 +425,15 @@ class MQTT::Client
410
425
  if block_given?
411
426
  # Loop forever!
412
427
  loop do
413
- yield(@read_queue.pop)
428
+ packet = @read_queue.pop
429
+ yield(packet)
430
+ puback_packet(packet) if packet.qos > 0
414
431
  end
415
432
  else
416
433
  # Wait for one packet to be available
417
- return @read_queue.pop
434
+ packet = @read_queue.pop
435
+ puback_packet(packet) if packet.qos > 0
436
+ return packet
418
437
  end
419
438
  end
420
439
 
@@ -436,7 +455,7 @@ class MQTT::Client
436
455
 
437
456
  packet = MQTT::Packet::Unsubscribe.new(
438
457
  :topics => topics,
439
- :id => @packet_id.next
458
+ :id => next_packet_id
440
459
  )
441
460
  send_packet(packet)
442
461
  end
@@ -452,25 +471,9 @@ private
452
471
  unless result.nil?
453
472
  # Yes - read in the packet
454
473
  packet = MQTT::Packet.read(@socket)
455
- if packet.class == MQTT::Packet::Publish
456
- # Add to queue
457
- @read_queue.push(packet)
458
- elsif packet.class == MQTT::Packet::Pingresp
459
- @last_ping_response = Time.now
460
- else
461
- # Ignore all other packets
462
- nil
463
- # FIXME: implement responses for QOS 1 and 2
464
- end
465
- end
466
-
467
- # Time to send a keep-alive ping request?
468
- if @keep_alive > 0 and Time.now > @last_pingreq + @keep_alive
469
- ping
474
+ handle_packet packet
470
475
  end
471
-
472
- # FIXME: check we received a ping response recently?
473
-
476
+ keep_alive!
474
477
  # Pass exceptions up to parent thread
475
478
  rescue Exception => exp
476
479
  unless @socket.nil?
@@ -481,6 +484,40 @@ private
481
484
  end
482
485
  end
483
486
 
487
+ def handle_packet(packet)
488
+ if packet.class == MQTT::Packet::Publish
489
+ # Add to queue
490
+ @read_queue.push(packet)
491
+ elsif packet.class == MQTT::Packet::Pingresp
492
+ @last_ping_response = Time.now
493
+ elsif packet.class == MQTT::Packet::Puback
494
+ @pubacks_semaphore.synchronize do
495
+ @pubacks[packet.id] = packet
496
+ end
497
+ end
498
+ # Ignore all other packets
499
+ # FIXME: implement responses for QoS 2
500
+ end
501
+
502
+ def keep_alive!
503
+ if @keep_alive > 0
504
+ response_timeout = (@keep_alive * 1.5).ceil
505
+ if Time.now >= @last_ping_request + @keep_alive
506
+ packet = MQTT::Packet::Pingreq.new
507
+ send_packet(packet)
508
+ @last_ping_request = Time.now
509
+ elsif Time.now > @last_ping_response + response_timeout
510
+ raise MQTT::ProtocolException.new(
511
+ "No Ping Response received for #{response_timeout} seconds"
512
+ )
513
+ end
514
+ end
515
+ end
516
+
517
+ def puback_packet(packet)
518
+ send_packet(MQTT::Packet::Puback.new :id => packet.id)
519
+ end
520
+
484
521
  # Read and check a connection acknowledgement packet
485
522
  def receive_connack
486
523
  Timeout.timeout(@ack_timeout) do
@@ -500,7 +537,7 @@ private
500
537
 
501
538
  # Send a packet to server
502
539
  def send_packet(data)
503
- # Throw exception if we aren't connected
540
+ # Raise exception if we aren't connected
504
541
  raise MQTT::NotConnectedException if not connected?
505
542
 
506
543
  # Only allow one thread to write to socket at a time
@@ -529,6 +566,9 @@ private
529
566
  }
530
567
  end
531
568
 
569
+ def next_packet_id
570
+ @last_packet_id = ( @last_packet_id || 0 ).next
571
+ end
532
572
 
533
573
  # ---- Deprecated attributes and methods ---- #
534
574
  public