paho-mqtt 0.0.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 777754009990c745ded4b02654a54bc26a2e422d
4
+ data.tar.gz: 0e92fba78c0e1938db874d2faed6bee9ef52890c
5
+ SHA512:
6
+ metadata.gz: ee45850a33868e480871d6cf7b50bd8277a9a1d1d81e634341a64548d69c77b5145fa49d4ccd1524be6b4358af7ace9414222f3a074d796b9ebe85ee8d97a7a3
7
+ data.tar.gz: b295155a5598e8f410b61a4591df6024133e2d9125364d27aa47a4aff10fb308d7ac9c4ac1a7cc8161d5afd5c122ee6b964372c692cba3be7820509426d86866
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.11.2
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at p-goudet@ruby-dev.jp. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in PahoMqttRuby.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Pierre Goudet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ # PahoMqtt
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.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'paho-mqtt'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install paho-mqtt
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
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.
30
+
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).
32
+
33
+ ## Contributing
34
+
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.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "PahoMqttRuby"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ require "paho.mqtt/version"
2
+ require "paho.mqtt/paho_client"
3
+ require "paho.mqtt/packet_manager"
4
+
5
+ module PahoMqtt
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,1056 @@
1
+ # encoding: BINARY
2
+
3
+ module PahoMqtt
4
+ # Class representing a MQTT Packet
5
+ # Performs binary encoding and decoding of headers
6
+ class Packet
7
+ # The version number of the MQTT protocol to use (default 3.1.0)
8
+ attr_accessor :version
9
+
10
+ # Identifier to link related control packets together
11
+ attr_accessor :id
12
+
13
+ # Array of 4 bits in the fixed header
14
+ attr_accessor :flags
15
+
16
+ # The length of the parsed packet body
17
+ attr_reader :body_length
18
+
19
+ # Default attribute values
20
+ ATTR_DEFAULTS = {
21
+ :version => '3.1.0',
22
+ :id => 0,
23
+ :body_length => nil
24
+ }
25
+
26
+ # Read in a packet from a socket
27
+ def self.read(socket)
28
+ # Read in the packet header and create a new packet object
29
+ packet = create_from_header(
30
+ read_byte(socket)
31
+ )
32
+ unless packet.nil?
33
+ packet.validate_flags
34
+
35
+ # Read in the packet length
36
+ multiplier = 1
37
+ body_length = 0
38
+ pos = 1
39
+ begin
40
+ digit = read_byte(socket)
41
+ body_length += ((digit & 0x7F) * multiplier)
42
+ multiplier *= 0x80
43
+ pos += 1
44
+ end while ((digit & 0x80) != 0x00) and pos <= 4
45
+
46
+ # Store the expected body length in the packet
47
+ packet.instance_variable_set('@body_length', body_length)
48
+
49
+ # Read in the packet body
50
+ packet.parse_body( socket.read(body_length) )
51
+ end
52
+ return packet
53
+ end
54
+
55
+ # Parse buffer into new packet object
56
+ def self.parse(buffer)
57
+ packet = parse_header(buffer)
58
+ packet.parse_body(buffer)
59
+ return packet
60
+ end
61
+
62
+ # Parse the header and create a new packet object of the correct type
63
+ # The header is removed from the buffer passed into this function
64
+ def self.parse_header(buffer)
65
+ # Check that the packet is a long as the minimum packet size
66
+ if buffer.bytesize < 2
67
+ raise "Invalid packet: less than 2 bytes long"
68
+ end
69
+
70
+ # Create a new packet object
71
+ bytes = buffer.unpack("C5")
72
+ packet = create_from_header(bytes.first)
73
+ packet.validate_flags
74
+
75
+ # Parse the packet length
76
+ body_length = 0
77
+ multiplier = 1
78
+ pos = 1
79
+ begin
80
+ if buffer.bytesize <= pos
81
+ raise "The packet length header is incomplete"
82
+ end
83
+ digit = bytes[pos]
84
+ body_length += ((digit & 0x7F) * multiplier)
85
+ multiplier *= 0x80
86
+ pos += 1
87
+ end while ((digit & 0x80) != 0x00) and pos <= 4
88
+
89
+ # Store the expected body length in the packet
90
+ packet.instance_variable_set('@body_length', body_length)
91
+
92
+ # Delete the fixed header from the raw packet passed in
93
+ buffer.slice!(0...pos)
94
+
95
+ return packet
96
+ end
97
+
98
+ # Create a new packet object from the first byte of a MQTT packet
99
+ def self.create_from_header(byte)
100
+ unless byte.nil?
101
+ # Work out the class
102
+ type_id = ((byte & 0xF0) >> 4)
103
+ packet_class = PahoMqtt::PACKET_TYPES[type_id]
104
+ if packet_class.nil?
105
+ raise "Invalid packet type identifier: #{type_id}"
106
+ end
107
+
108
+ # Convert the last 4 bits of byte into array of true/false
109
+ flags = (0..3).map { |i| byte & (2 ** i) != 0 }
110
+
111
+ # Create a new packet object
112
+ packet_class.new(:flags => flags)
113
+ end
114
+ end
115
+
116
+ # Create a new empty packet
117
+ def initialize(args={})
118
+ # We must set flags before the other values
119
+ @flags = [false, false, false, false]
120
+ update_attributes(ATTR_DEFAULTS.merge(args))
121
+ end
122
+
123
+ # Set packet attributes from a hash of attribute names and values
124
+ def update_attributes(attr={})
125
+ attr.each_pair do |k,v|
126
+ if v.is_a?(Array) or v.is_a?(Hash)
127
+ send("#{k}=", v.dup)
128
+ else
129
+ send("#{k}=", v)
130
+ end
131
+ end
132
+ end
133
+
134
+ # Get the identifer for this packet type
135
+ def type_id
136
+ index = PahoMqtt::PACKET_TYPES.index(self.class)
137
+ if index.nil?
138
+ raise "Invalid packet type: #{self.class}"
139
+ end
140
+ return index
141
+ end
142
+
143
+ # Get the name of the packet type as a string in capitals
144
+ # (like the MQTT specification uses)
145
+ #
146
+ # Example: CONNACK
147
+ def type_name
148
+ self.class.name.split('::').last.upcase
149
+ end
150
+
151
+ # Set the protocol version number
152
+ def version=(arg)
153
+ @version = arg.to_s
154
+ end
155
+
156
+ # Set the length of the packet body
157
+ def body_length=(arg)
158
+ @body_length = arg.to_i
159
+ end
160
+
161
+ # Parse the body (variable header and payload) of a packet
162
+ def parse_body(buffer)
163
+ if buffer.bytesize != body_length
164
+ raise "Failed to parse packet - input buffer (#{buffer.bytesize}) is not the same as the body length header (#{body_length})"
165
+ end
166
+ end
167
+
168
+ # Get serialisation of packet's body (variable header and payload)
169
+ def encode_body
170
+ '' # No body by default
171
+ end
172
+
173
+ # Serialise the packet
174
+ def to_s
175
+ # Encode the fixed header
176
+ header = [
177
+ ((type_id.to_i & 0x0F) << 4) |
178
+ (flags[3] ? 0x8 : 0x0) |
179
+ (flags[2] ? 0x4 : 0x0) |
180
+ (flags[1] ? 0x2 : 0x0) |
181
+ (flags[0] ? 0x1 : 0x0)
182
+ ]
183
+
184
+ # Get the packet's variable header and payload
185
+ body = self.encode_body
186
+
187
+ # Check that that packet isn't too big
188
+ body_length = body.bytesize
189
+ if body_length > 268435455
190
+ raise "Error serialising packet: body is more than 256MB"
191
+ end
192
+
193
+ # Build up the body length field bytes
194
+ begin
195
+ digit = (body_length % 128)
196
+ body_length = (body_length / 128)
197
+ # if there are more digits to encode, set the top bit of this digit
198
+ digit |= 0x80 if (body_length > 0)
199
+ header.push(digit)
200
+ end while (body_length > 0)
201
+
202
+ # Convert header to binary and add on body
203
+ header.pack('C*') + body
204
+ end
205
+
206
+ # Check that fixed header flags are valid for types that don't use the flags
207
+ # @private
208
+ def validate_flags
209
+ if flags != [false, false, false, false]
210
+ raise "Invalid flags in #{type_name} packet header"
211
+ end
212
+ end
213
+
214
+ # Returns a human readable string
215
+ def inspect
216
+ "\#<#{self.class}>"
217
+ end
218
+
219
+ protected
220
+
221
+ # Encode an array of bytes and return them
222
+ def encode_bytes(*bytes)
223
+ bytes.pack('C*')
224
+ end
225
+
226
+ # Encode an array of bits and return them
227
+ def encode_bits(bits)
228
+ [bits.map{|b| b ? '1' : '0'}.join].pack('b*')
229
+ end
230
+
231
+ # Encode a 16-bit unsigned integer and return it
232
+ def encode_short(val)
233
+ [val.to_i].pack('n')
234
+ end
235
+
236
+ # Encode a UTF-8 string and return it
237
+ # (preceded by the length of the string)
238
+ def encode_string(str)
239
+ str = str.to_s.encode('UTF-8')
240
+
241
+ # Force to binary, when assembling the packet
242
+ str.force_encoding('ASCII-8BIT')
243
+ encode_short(str.bytesize) + str
244
+ end
245
+
246
+ # Remove a 16-bit unsigned integer from the front of buffer
247
+ def shift_short(buffer)
248
+ bytes = buffer.slice!(0..1)
249
+ bytes.unpack('n').first
250
+ end
251
+
252
+ # Remove one byte from the front of the string
253
+ def shift_byte(buffer)
254
+ buffer.slice!(0...1).unpack('C').first
255
+ end
256
+
257
+ # Remove 8 bits from the front of buffer
258
+ def shift_bits(buffer)
259
+ buffer.slice!(0...1).unpack('b8').first.split('').map {|b| b == '1'}
260
+ end
261
+
262
+ # Remove n bytes from the front of buffer
263
+ def shift_data(buffer,bytes)
264
+ buffer.slice!(0...bytes)
265
+ end
266
+
267
+ # Remove string from the front of buffer
268
+ def shift_string(buffer)
269
+ len = shift_short(buffer)
270
+ str = shift_data(buffer,len)
271
+ # Strings in MQTT v3.1 are all UTF-8
272
+ str.force_encoding('UTF-8')
273
+ end
274
+
275
+
276
+ private
277
+
278
+ # Read and unpack a single byte from a socket
279
+ def self.read_byte(socket)
280
+ byte = socket.read(1)
281
+ unless byte.nil?
282
+ byte.unpack('C').first
283
+ else
284
+ nil
285
+ end
286
+ end
287
+
288
+ ## PACKET SUBCLASSES ##
289
+
290
+
291
+ # Class representing an MQTT Publish message
292
+ class Publish < PahoMqtt::Packet
293
+
294
+ # Duplicate delivery flag
295
+ attr_accessor :duplicate
296
+
297
+ # Retain flag
298
+ attr_accessor :retain
299
+
300
+ # Quality of Service level (0, 1, 2)
301
+ attr_accessor :qos
302
+
303
+ # The topic name to publish to
304
+ attr_accessor :topic
305
+
306
+ # The data to be published
307
+ attr_accessor :payload
308
+
309
+ # Default attribute values
310
+ ATTR_DEFAULTS = {
311
+ :topic => nil,
312
+ :payload => ''
313
+ }
314
+
315
+ # Create a new Publish packet
316
+ def initialize(args={})
317
+ super(ATTR_DEFAULTS.merge(args))
318
+ end
319
+
320
+ def duplicate
321
+ @flags[3]
322
+ end
323
+
324
+ # Set the DUP flag (true/false)
325
+ def duplicate=(arg)
326
+ if arg.kind_of?(Integer)
327
+ @flags[3] = (arg == 0x1)
328
+ else
329
+ @flags[3] = arg
330
+ end
331
+ end
332
+
333
+ def retain
334
+ @flags[0]
335
+ end
336
+
337
+ # Set the retain flag (true/false)
338
+ def retain=(arg)
339
+ if arg.kind_of?(Integer)
340
+ @flags[0] = (arg == 0x1)
341
+ else
342
+ @flags[0] = arg
343
+ end
344
+ end
345
+
346
+ def qos
347
+ (@flags[1] ? 0x01 : 0x00) | (@flags[2] ? 0x02 : 0x00)
348
+ end
349
+
350
+ # Set the Quality of Service level (0/1/2)
351
+ def qos=(arg)
352
+ @qos = arg.to_i
353
+ if @qos < 0 or @qos > 2
354
+ raise "Invalid QoS value: #{@qos}"
355
+ else
356
+ @flags[1] = (arg & 0x01 == 0x01)
357
+ @flags[2] = (arg & 0x02 == 0x02)
358
+ end
359
+ end
360
+
361
+ # Get serialisation of packet's body
362
+ def encode_body
363
+ body = ''
364
+ if @topic.nil? or @topic.to_s.empty?
365
+ raise "Invalid topic name when serialising packet"
366
+ end
367
+ body += encode_string(@topic)
368
+ body += encode_short(@id) unless qos == 0
369
+ body += payload.to_s.dup.force_encoding('ASCII-8BIT')
370
+ return body
371
+ end
372
+
373
+ # Parse the body (variable header and payload) of a Publish packet
374
+ def parse_body(buffer)
375
+ super(buffer)
376
+ @topic = shift_string(buffer)
377
+ @id = shift_short(buffer) unless qos == 0
378
+ @payload = buffer
379
+ end
380
+
381
+ # Check that fixed header flags are valid for this packet type
382
+ # @private
383
+ def validate_flags
384
+ if qos == 3
385
+ raise "Invalid packet: QoS value of 3 is not allowed"
386
+ end
387
+ if qos == 0 and duplicate
388
+ raise "Invalid packet: DUP cannot be set for QoS 0"
389
+ end
390
+ end
391
+
392
+ # Returns a human readable string, summarising the properties of the packet
393
+ def inspect
394
+ "\#<#{self.class}: " +
395
+ "d#{duplicate ? '1' : '0'}, " +
396
+ "q#{qos}, " +
397
+ "r#{retain ? '1' : '0'}, " +
398
+ "m#{id}, " +
399
+ "'#{topic}', " +
400
+ "#{inspect_payload}>"
401
+ end
402
+
403
+ protected
404
+
405
+ def inspect_payload
406
+ str = payload.to_s
407
+ if str.bytesize < 16 and str =~ /^[ -~]*$/
408
+ "'#{str}'"
409
+ else
410
+ "... (#{str.bytesize} bytes)"
411
+ end
412
+ end
413
+ end
414
+
415
+ # Class representing an MQTT Connect Packet
416
+ class Connect < PahoMqtt::Packet
417
+ # The name of the protocol
418
+ attr_accessor :protocol_name
419
+
420
+ # The version number of the protocol
421
+ attr_accessor :protocol_level
422
+
423
+ # The client identifier string
424
+ attr_accessor :client_id
425
+
426
+ # Set to false to keep a persistent session with the server
427
+ attr_accessor :clean_session
428
+
429
+ # Period the server should keep connection open for between pings
430
+ attr_accessor :keep_alive
431
+
432
+ # The topic name to send the Will message to
433
+ attr_accessor :will_topic
434
+
435
+ # The QoS level to send the Will message as
436
+ attr_accessor :will_qos
437
+
438
+ # Set to true to make the Will message retained
439
+ attr_accessor :will_retain
440
+
441
+ # The payload of the Will message
442
+ attr_accessor :will_payload
443
+
444
+ # The username for authenticating with the server
445
+ attr_accessor :username
446
+
447
+ # The password for authenticating with the server
448
+ attr_accessor :password
449
+
450
+ # Default attribute values
451
+ ATTR_DEFAULTS = {
452
+ :client_id => nil,
453
+ :clean_session => true,
454
+ :keep_alive => 15,
455
+ :will_topic => nil,
456
+ :will_qos => 0,
457
+ :will_retain => false,
458
+ :will_payload => '',
459
+ :username => nil,
460
+ :password => nil,
461
+ }
462
+
463
+ # Create a new Client Connect packet
464
+ def initialize(args={})
465
+ super(ATTR_DEFAULTS.merge(args))
466
+
467
+ if version == '3.1.0' or version == '3.1'
468
+ self.protocol_name ||= 'MQIsdp'
469
+ self.protocol_level ||= 0x03
470
+ elsif version == '3.1.1'
471
+ self.protocol_name ||= 'MQTT'
472
+ self.protocol_level ||= 0x04
473
+ else
474
+ raise ArgumentError.new("Unsupported protocol version: #{version}")
475
+ end
476
+ end
477
+
478
+ # Get serialisation of packet's body
479
+ def encode_body
480
+ body = ''
481
+ if @version == '3.1.0'
482
+ if @client_id.nil? or @client_id.bytesize < 1
483
+ raise "Client identifier too short while serialising packet"
484
+ elsif @client_id.bytesize > 23
485
+ raise "Client identifier too long when serialising packet"
486
+ end
487
+ end
488
+ body += encode_string(@protocol_name)
489
+ body += encode_bytes(@protocol_level.to_i)
490
+ if @keep_alive < 0
491
+ raise "Invalid keep-alive value: cannot be less than 0"
492
+ end
493
+
494
+ # Set the Connect flags
495
+ @connect_flags = 0
496
+ @connect_flags |= 0x02 if @clean_session
497
+ @connect_flags |= 0x04 unless @will_topic.nil?
498
+ @connect_flags |= ((@will_qos & 0x03) << 3)
499
+ @connect_flags |= 0x20 if @will_retain
500
+ @connect_flags |= 0x40 unless @password.nil?
501
+ @connect_flags |= 0x80 unless @username.nil?
502
+ body += encode_bytes(@connect_flags)
503
+ body += encode_short(@keep_alive)
504
+ body += encode_string(@client_id)
505
+ unless will_topic.nil?
506
+ body += encode_string(@will_topic)
507
+ # The MQTT v3.1 specification says that the payload is a UTF-8 string
508
+ body += encode_string(@will_payload)
509
+ end
510
+ body += encode_string(@username) unless @username.nil?
511
+ body += encode_string(@password) unless @password.nil?
512
+ return body
513
+ end
514
+
515
+ # Parse the body (variable header and payload) of a Connect packet
516
+ def parse_body(buffer)
517
+ super(buffer)
518
+ @protocol_name = shift_string(buffer)
519
+ @protocol_level = shift_byte(buffer).to_i
520
+ if @protocol_name == 'MQIsdp' and @protocol_level == 3
521
+ @version = '3.1.0'
522
+ elsif @protocol_name == 'MQTT' and @protocol_level == 4
523
+ @version = '3.1.1'
524
+ else
525
+ raise "Unsupported protocol: #{@protocol_name}/#{@protocol_level}"
526
+ end
527
+
528
+ @connect_flags = shift_byte(buffer)
529
+ @clean_session = ((@connect_flags & 0x02) >> 1) == 0x01
530
+ @keep_alive = shift_short(buffer)
531
+ @client_id = shift_string(buffer)
532
+ if ((@connect_flags & 0x04) >> 2) == 0x01
533
+ # Last Will and Testament
534
+ @will_qos = ((@connect_flags & 0x18) >> 3)
535
+ @will_retain = ((@connect_flags & 0x20) >> 5) == 0x01
536
+ @will_topic = shift_string(buffer)
537
+ # The MQTT v3.1 specification says that the payload is a UTF-8 string
538
+ @will_payload = shift_string(buffer)
539
+ end
540
+ if ((@connect_flags & 0x80) >> 7) == 0x01 and buffer.bytesize > 0
541
+ @username = shift_string(buffer)
542
+ end
543
+ if ((@connect_flags & 0x40) >> 6) == 0x01 and buffer.bytesize > 0
544
+ @password = shift_string(buffer)
545
+ end
546
+ end
547
+
548
+ # Returns a human readable string, summarising the properties of the packet
549
+ def inspect
550
+ str = "\#<#{self.class}: "
551
+ str += "keep_alive=#{keep_alive}"
552
+ str += ", clean" if clean_session
553
+ str += ", client_id='#{client_id}'"
554
+ str += ", username='#{username}'" unless username.nil?
555
+ str += ", password=..." unless password.nil?
556
+ str += ">"
557
+ end
558
+ end
559
+
560
+ # Class representing an MQTT Connect Acknowledgment Packet
561
+ class Connack < PahoMqtt::Packet
562
+ # Session Present flag
563
+ attr_accessor :session_present
564
+
565
+ # The return code (defaults to 0 for connection accepted)
566
+ attr_accessor :return_code
567
+
568
+ # Default attribute values
569
+ ATTR_DEFAULTS = {:return_code => 0x00}
570
+
571
+ # Create a new Client Connect packet
572
+ def initialize(args={})
573
+ # We must set flags before other attributes
574
+ @connack_flags = [false, false, false, false, false, false, false, false]
575
+ super(ATTR_DEFAULTS.merge(args))
576
+ end
577
+
578
+ # Get the Session Present flag
579
+ def session_present
580
+ @connack_flags[0]
581
+ end
582
+
583
+ # Set the Session Present flag
584
+ def session_present=(arg)
585
+ if arg.kind_of?(Integer)
586
+ @connack_flags[0] = (arg == 0x1)
587
+ else
588
+ @connack_flags[0] = arg
589
+ end
590
+ end
591
+
592
+ # Get a string message corresponding to a return code
593
+ def return_msg
594
+ case return_code
595
+ when 0x00
596
+ "Connection Accepted"
597
+ when 0x01
598
+ "Connection refused: unacceptable protocol version"
599
+ when 0x02
600
+ "Connection refused: client identifier rejected"
601
+ when 0x03
602
+ "Connection refused: server unavailable"
603
+ when 0x04
604
+ "Connection refused: bad user name or password"
605
+ when 0x05
606
+ "Connection refused: not authorised"
607
+ else
608
+ "Connection refused: error code #{return_code}"
609
+ end
610
+ end
611
+
612
+ # Get serialisation of packet's body
613
+ def encode_body
614
+ body = ''
615
+ body += encode_bits(@connack_flags)
616
+ body += encode_bytes(@return_code.to_i)
617
+ return body
618
+ end
619
+
620
+ # Parse the body (variable header and payload) of a Connect Acknowledgment packet
621
+ def parse_body(buffer)
622
+ super(buffer)
623
+ @connack_flags = shift_bits(buffer)
624
+ unless @connack_flags[1,7] == [false, false, false, false, false, false, false]
625
+ raise "Invalid flags in Connack variable header"
626
+ end
627
+ @return_code = shift_byte(buffer)
628
+ unless buffer.empty?
629
+ raise "Extra bytes at end of Connect Acknowledgment packet"
630
+ end
631
+ end
632
+
633
+ # Returns a human readable string, summarising the properties of the packet
634
+ def inspect
635
+ "\#<#{self.class}: 0x%2.2X>" % return_code
636
+ end
637
+ end
638
+
639
+ # Class representing an MQTT Publish Acknowledgment packet
640
+ class Puback < PahoMqtt::Packet
641
+ # Get serialisation of packet's body
642
+ def encode_body
643
+ encode_short(@id)
644
+ end
645
+
646
+ # Parse the body (variable header and payload) of a packet
647
+ def parse_body(buffer)
648
+ super(buffer)
649
+ @id = shift_short(buffer)
650
+ unless buffer.empty?
651
+ raise "Extra bytes at end of Publish Acknowledgment packet"
652
+ end
653
+ end
654
+
655
+ # Returns a human readable string, summarising the properties of the packet
656
+ def inspect
657
+ "\#<#{self.class}: 0x%2.2X>" % id
658
+ end
659
+ end
660
+
661
+ # Class representing an MQTT Publish Received packet
662
+ class Pubrec < PahoMqtt::Packet
663
+ # Get serialisation of packet's body
664
+ def encode_body
665
+ encode_short(@id)
666
+ end
667
+
668
+ # Parse the body (variable header and payload) of a packet
669
+ def parse_body(buffer)
670
+ super(buffer)
671
+ @id = shift_short(buffer)
672
+ unless buffer.empty?
673
+ raise "Extra bytes at end of Publish Received packet"
674
+ end
675
+ end
676
+
677
+ # Returns a human readable string, summarising the properties of the packet
678
+ def inspect
679
+ "\#<#{self.class}: 0x%2.2X>" % id
680
+ end
681
+ end
682
+
683
+ # Class representing an MQTT Publish Release packet
684
+ class Pubrel < PahoMqtt::Packet
685
+
686
+ # Default attribute values
687
+ ATTR_DEFAULTS = {
688
+ :flags => [false, true, false, false],
689
+ }
690
+
691
+ # Create a new Pubrel packet
692
+ def initialize(args={})
693
+ super(ATTR_DEFAULTS.merge(args))
694
+ end
695
+
696
+ # Get serialisation of packet's body
697
+ def encode_body
698
+ encode_short(@id)
699
+ end
700
+
701
+ # Parse the body (variable header and payload) of a packet
702
+ def parse_body(buffer)
703
+ super(buffer)
704
+ @id = shift_short(buffer)
705
+ unless buffer.empty?
706
+ raise "Extra bytes at end of Publish Release packet"
707
+ end
708
+ end
709
+
710
+ # Check that fixed header flags are valid for this packet type
711
+ # @private
712
+ def validate_flags
713
+ if @flags != [false, true, false, false]
714
+ raise "Invalid flags in PUBREL packet header"
715
+ end
716
+ end
717
+
718
+ # Returns a human readable string, summarising the properties of the packet
719
+ def inspect
720
+ "\#<#{self.class}: 0x%2.2X>" % id
721
+ end
722
+ end
723
+
724
+ # Class representing an MQTT Publish Complete packet
725
+ class Pubcomp < PahoMqtt::Packet
726
+ # Get serialisation of packet's body
727
+ def encode_body
728
+ encode_short(@id)
729
+ end
730
+
731
+ # Parse the body (variable header and payload) of a packet
732
+ def parse_body(buffer)
733
+ super(buffer)
734
+ @id = shift_short(buffer)
735
+ unless buffer.empty?
736
+ raise "Extra bytes at end of Publish Complete packet"
737
+ end
738
+ end
739
+
740
+ # Returns a human readable string, summarising the properties of the packet
741
+ def inspect
742
+ "\#<#{self.class}: 0x%2.2X>" % id
743
+ end
744
+ end
745
+
746
+ # Class representing an MQTT Client Subscribe packet
747
+ class Subscribe < PahoMqtt::Packet
748
+ # One or more topic filters to subscribe to
749
+ attr_accessor :topics
750
+
751
+ # Default attribute values
752
+ ATTR_DEFAULTS = {
753
+ :topics => [],
754
+ :flags => [false, true, false, false],
755
+ }
756
+
757
+ # Create a new Subscribe packet
758
+ def initialize(args={})
759
+ super(ATTR_DEFAULTS.merge(args))
760
+ end
761
+
762
+ # Set one or more topic filters for the Subscribe packet
763
+ # The topics parameter should be one of the following:
764
+ # * String: subscribe to one topic with QoS 0
765
+ # * Array: subscribe to multiple topics with QoS 0
766
+ # * Hash: subscribe to multiple topics where the key is the topic and the value is the QoS level
767
+ #
768
+ # For example:
769
+ # packet.topics = 'a/b'
770
+ # packet.topics = ['a/b', 'c/d']
771
+ # packet.topics = [['a/b',0], ['c/d',1]]
772
+ # packet.topics = {'a/b' => 0, 'c/d' => 1}
773
+ #
774
+ def topics=(value)
775
+ # Get input into a consistent state
776
+ if value.is_a?(Array)
777
+ input = value.flatten
778
+ else
779
+ input = [value]
780
+ end
781
+
782
+ @topics = []
783
+ while(input.length>0)
784
+ item = input.shift
785
+ if item.is_a?(Hash)
786
+ # Convert hash into an ordered array of arrays
787
+ @topics += item.sort
788
+ elsif item.is_a?(String)
789
+ # Peek at the next item in the array, and remove it if it is an integer
790
+ if input.first.is_a?(Integer)
791
+ qos = input.shift
792
+ @topics << [item,qos]
793
+ else
794
+ @topics << [item,0]
795
+ end
796
+ else
797
+ # Meh?
798
+ raise "Invalid topics input: #{value.inspect}"
799
+ end
800
+ end
801
+ @topics
802
+ end
803
+
804
+ # Get serialisation of packet's body
805
+ def encode_body
806
+ if @topics.empty?
807
+ raise "no topics given when serialising packet"
808
+ end
809
+ body = encode_short(@id)
810
+ topics.each do |item|
811
+ body += encode_string(item[0])
812
+ body += encode_bytes(item[1])
813
+ end
814
+ return body
815
+ end
816
+
817
+ # Parse the body (variable header and payload) of a packet
818
+ def parse_body(buffer)
819
+ super(buffer)
820
+ @id = shift_short(buffer)
821
+ @topics = []
822
+ while(buffer.bytesize>0)
823
+ topic_name = shift_string(buffer)
824
+ topic_qos = shift_byte(buffer)
825
+ @topics << [topic_name,topic_qos]
826
+ end
827
+ end
828
+
829
+ # Check that fixed header flags are valid for this packet type
830
+ # @private
831
+ def validate_flags
832
+ if @flags != [false, true, false, false]
833
+ raise "Invalid flags in SUBSCRIBE packet header"
834
+ end
835
+ end
836
+
837
+ # Returns a human readable string, summarising the properties of the packet
838
+ def inspect
839
+ _str = "\#<#{self.class}: 0x%2.2X, %s>" % [
840
+ id,
841
+ topics.map {|t| "'#{t[0]}':#{t[1]}"}.join(', ')
842
+ ]
843
+ end
844
+ end
845
+
846
+ # Class representing an MQTT Subscribe Acknowledgment packet
847
+ class Suback < PahoMqtt::Packet
848
+ # An array of return codes, ordered by the topics that were subscribed to
849
+ attr_accessor :return_codes
850
+
851
+ # Default attribute values
852
+ ATTR_DEFAULTS = {
853
+ :return_codes => [],
854
+ }
855
+
856
+ # Create a new Subscribe Acknowledgment packet
857
+ def initialize(args={})
858
+ super(ATTR_DEFAULTS.merge(args))
859
+ end
860
+
861
+ # Set the granted QoS value for each of the topics that were subscribed to
862
+ # Can either be an integer or an array or integers.
863
+ def return_codes=(value)
864
+ if value.is_a?(Array)
865
+ @return_codes = value
866
+ elsif value.is_a?(Integer)
867
+ @return_codes = [value]
868
+ else
869
+ raise "return_codes should be an integer or an array of return codes"
870
+ end
871
+ end
872
+
873
+ # Get serialisation of packet's body
874
+ def encode_body
875
+ if @return_codes.empty?
876
+ raise "no granted QoS given when serialising packet"
877
+ end
878
+ body = encode_short(@id)
879
+ return_codes.each { |qos| body += encode_bytes(qos) }
880
+ return body
881
+ end
882
+
883
+ # Parse the body (variable header and payload) of a packet
884
+ def parse_body(buffer)
885
+ super(buffer)
886
+ @id = shift_short(buffer)
887
+ while(buffer.bytesize>0)
888
+ @return_codes << shift_byte(buffer)
889
+ end
890
+ end
891
+
892
+ # Returns a human readable string, summarising the properties of the packet
893
+ def inspect
894
+ "\#<#{self.class}: 0x%2.2X, rc=%s>" % [id, return_codes.map{|rc| "0x%2.2X" % rc}.join(',')]
895
+ end
896
+ end
897
+
898
+ # Class representing an MQTT Client Unsubscribe packet
899
+ class Unsubscribe < PahoMqtt::Packet
900
+ # One or more topic paths to unsubscribe from
901
+ attr_accessor :topics
902
+
903
+ # Default attribute values
904
+ ATTR_DEFAULTS = {
905
+ :topics => [],
906
+ :flags => [false, true, false, false],
907
+ }
908
+
909
+ # Create a new Unsubscribe packet
910
+ def initialize(args={})
911
+ super(ATTR_DEFAULTS.merge(args))
912
+ end
913
+
914
+ # Set one or more topic paths to unsubscribe from
915
+ def topics=(value)
916
+ if value.is_a?(Array)
917
+ @topics = value
918
+ else
919
+ @topics = [value]
920
+ end
921
+ end
922
+
923
+ # Get serialisation of packet's body
924
+ def encode_body
925
+ if @topics.empty?
926
+ raise "no topics given when serialising packet"
927
+ end
928
+ body = encode_short(@id)
929
+ topics.each { |topic| body += encode_string(topic) }
930
+ return body
931
+ end
932
+
933
+ # Parse the body (variable header and payload) of a packet
934
+ def parse_body(buffer)
935
+ super(buffer)
936
+ @id = shift_short(buffer)
937
+ while(buffer.bytesize>0)
938
+ @topics << shift_string(buffer)
939
+ end
940
+ end
941
+
942
+ # Check that fixed header flags are valid for this packet type
943
+ # @private
944
+ def validate_flags
945
+ if @flags != [false, true, false, false]
946
+ raise "Invalid flags in UNSUBSCRIBE packet header"
947
+ end
948
+ end
949
+
950
+ # Returns a human readable string, summarising the properties of the packet
951
+ def inspect
952
+ "\#<#{self.class}: 0x%2.2X, %s>" % [
953
+ id,
954
+ topics.map {|t| "'#{t}'"}.join(', ')
955
+ ]
956
+ end
957
+ end
958
+
959
+ # Class representing an MQTT Unsubscribe Acknowledgment packet
960
+ class Unsuback < PahoMqtt::Packet
961
+ # Create a new Unsubscribe Acknowledgment packet
962
+ def initialize(args={})
963
+ super(args)
964
+ end
965
+
966
+ # Get serialisation of packet's body
967
+ def encode_body
968
+ encode_short(@id)
969
+ end
970
+
971
+ # Parse the body (variable header and payload) of a packet
972
+ def parse_body(buffer)
973
+ super(buffer)
974
+ @id = shift_short(buffer)
975
+ unless buffer.empty?
976
+ raise "Extra bytes at end of Unsubscribe Acknowledgment packet"
977
+ end
978
+ end
979
+
980
+ # Returns a human readable string, summarising the properties of the packet
981
+ def inspect
982
+ "\#<#{self.class}: 0x%2.2X>" % id
983
+ end
984
+ end
985
+
986
+ # Class representing an MQTT Ping Request packet
987
+ class Pingreq < PahoMqtt::Packet
988
+ # Create a new Ping Request packet
989
+ def initialize(args={})
990
+ super(args)
991
+ end
992
+
993
+ # Check the body
994
+ def parse_body(buffer)
995
+ super(buffer)
996
+ unless buffer.empty?
997
+ raise "Extra bytes at end of Ping Request packet"
998
+ end
999
+ end
1000
+ end
1001
+
1002
+ # Class representing an MQTT Ping Response packet
1003
+ class Pingresp < PahoMqtt::Packet
1004
+ # Create a new Ping Response packet
1005
+ def initialize(args={})
1006
+ super(args)
1007
+ end
1008
+
1009
+ # Check the body
1010
+ def parse_body(buffer)
1011
+ super(buffer)
1012
+ unless buffer.empty?
1013
+ raise "Extra bytes at end of Ping Response packet"
1014
+ end
1015
+ end
1016
+ end
1017
+
1018
+ # Class representing an MQTT Client Disconnect packet
1019
+ class Disconnect < PahoMqtt::Packet
1020
+ # Create a new Client Disconnect packet
1021
+ def initialize(args={})
1022
+ super(args)
1023
+ end
1024
+
1025
+ # Check the body
1026
+ def parse_body(buffer)
1027
+ super(buffer)
1028
+ unless buffer.empty?
1029
+ raise "Extra bytes at end of Disconnect packet"
1030
+ end
1031
+ end
1032
+ end
1033
+ end
1034
+
1035
+
1036
+ # An enumeration of the MQTT packet types
1037
+ PACKET_TYPES = [
1038
+ nil,
1039
+ PahoMqtt::Packet::Connect,
1040
+ PahoMqtt::Packet::Connack,
1041
+ PahoMqtt::Packet::Publish,
1042
+ PahoMqtt::Packet::Puback,
1043
+ PahoMqtt::Packet::Pubrec,
1044
+ PahoMqtt::Packet::Pubrel,
1045
+ PahoMqtt::Packet::Pubcomp,
1046
+ PahoMqtt::Packet::Subscribe,
1047
+ PahoMqtt::Packet::Suback,
1048
+ PahoMqtt::Packet::Unsubscribe,
1049
+ PahoMqtt::Packet::Unsuback,
1050
+ PahoMqtt::Packet::Pingreq,
1051
+ PahoMqtt::Packet::Pingresp,
1052
+ PahoMqtt::Packet::Disconnect,
1053
+ nil
1054
+ ]
1055
+
1056
+ end