paho-mqtt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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