mqtt 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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