go-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,1049 @@
1
+ # encoding: BINARY
2
+
3
+ module MQTT
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
+ packet.validate_flags
33
+
34
+ # Read in the packet length
35
+ multiplier = 1
36
+ body_length = 0
37
+ pos = 1
38
+
39
+ loop do
40
+ digit = read_byte(socket)
41
+ body_length += ((digit & 0x7F) * multiplier)
42
+ multiplier *= 0x80
43
+ pos += 1
44
+ break if (digit & 0x80).zero? || pos > 4
45
+ end
46
+
47
+ # Store the expected body length in the packet
48
+ packet.instance_variable_set('@body_length', body_length)
49
+
50
+ # Read in the packet body
51
+ packet.parse_body(socket.read(body_length))
52
+
53
+ packet
54
+ end
55
+
56
+ # Parse buffer into new packet object
57
+ def self.parse(buffer)
58
+ packet = parse_header(buffer)
59
+ packet.parse_body(buffer)
60
+ packet
61
+ end
62
+
63
+ # Parse the header and create a new packet object of the correct type
64
+ # The header is removed from the buffer passed into this function
65
+ def self.parse_header(buffer)
66
+ # Check that the packet is a long as the minimum packet size
67
+ if buffer.bytesize < 2
68
+ raise ProtocolException, 'Invalid packet: less than 2 bytes long'
69
+ end
70
+
71
+ # Create a new packet object
72
+ bytes = buffer.unpack('C5')
73
+ packet = create_from_header(bytes.first)
74
+ packet.validate_flags
75
+
76
+ # Parse the packet length
77
+ body_length = 0
78
+ multiplier = 1
79
+ pos = 1
80
+
81
+ loop do
82
+ if buffer.bytesize <= pos
83
+ raise ProtocolException, 'The packet length header is incomplete'
84
+ end
85
+
86
+ digit = bytes[pos]
87
+ body_length += ((digit & 0x7F) * multiplier)
88
+ multiplier *= 0x80
89
+ pos += 1
90
+ break if (digit & 0x80).zero? || pos > 4
91
+ end
92
+
93
+ # Store the expected body length in the packet
94
+ packet.instance_variable_set('@body_length', body_length)
95
+
96
+ # Delete the fixed header from the raw packet passed in
97
+ buffer.slice!(0...pos)
98
+
99
+ packet
100
+ end
101
+
102
+ # Create a new packet object from the first byte of a MQTT packet
103
+ def self.create_from_header(byte)
104
+ # Work out the class
105
+ type_id = ((byte & 0xF0) >> 4)
106
+ packet_class = MQTT::PACKET_TYPES[type_id]
107
+ if packet_class.nil?
108
+ raise ProtocolException, "Invalid packet type identifier: #{type_id}"
109
+ end
110
+
111
+ # Convert the last 4 bits of byte into array of true/false
112
+ flags = (0..3).map { |i| byte & (2**i) != 0 }
113
+
114
+ # Create a new packet object
115
+ packet_class.new(:flags => flags)
116
+ end
117
+
118
+ # Create a new empty packet
119
+ def initialize(args = {})
120
+ # We must set flags before the other values
121
+ @flags = [false, false, false, false]
122
+ update_attributes(ATTR_DEFAULTS.merge(args))
123
+ end
124
+
125
+ # Set packet attributes from a hash of attribute names and values
126
+ def update_attributes(attr = {})
127
+ attr.each_pair do |k, v|
128
+ if v.is_a?(Array) || v.is_a?(Hash)
129
+ send("#{k}=", v.dup)
130
+ else
131
+ send("#{k}=", v)
132
+ end
133
+ end
134
+ end
135
+
136
+ # Get the identifer for this packet type
137
+ def type_id
138
+ index = MQTT::PACKET_TYPES.index(self.class)
139
+ raise "Invalid packet type: #{self.class}" if index.nil?
140
+ 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
+ return if buffer.bytesize == body_length
164
+
165
+ raise ProtocolException, "Failed to parse packet - input buffer (#{buffer.bytesize}) is not the same as the body length header (#{body_length})"
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 = encode_body
186
+
187
+ # Check that that packet isn't too big
188
+ body_length = body.bytesize
189
+ if body_length > 268_435_455
190
+ raise 'Error serialising packet: body is more than 256MB'
191
+ end
192
+
193
+ # Build up the body length field bytes
194
+ loop do
195
+ digit = (body_length % 128)
196
+ body_length = body_length.div(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
+ break if body_length <= 0
201
+ end
202
+
203
+ # Convert header to binary and add on body
204
+ header.pack('C*') + body
205
+ end
206
+
207
+ # Check that fixed header flags are valid for types that don't use the flags
208
+ # @private
209
+ def validate_flags
210
+ return if flags == [false, false, false, false]
211
+
212
+ raise ProtocolException, "Invalid flags in #{type_name} packet header"
213
+ end
214
+
215
+ # Returns a human readable string
216
+ def inspect
217
+ "\#<#{self.class}>"
218
+ end
219
+
220
+ # Read and unpack a single byte from a socket
221
+ def self.read_byte(socket)
222
+ byte = socket.getbyte
223
+ raise ProtocolException, 'Failed to read byte from socket' if byte.nil?
224
+
225
+ byte
226
+ end
227
+
228
+ protected
229
+
230
+ # Encode an array of bytes and return them
231
+ def encode_bytes(*bytes)
232
+ bytes.pack('C*')
233
+ end
234
+
235
+ # Encode an array of bits and return them
236
+ def encode_bits(bits)
237
+ [bits.map { |b| b ? '1' : '0' }.join].pack('b*')
238
+ end
239
+
240
+ # Encode a 16-bit unsigned integer and return it
241
+ def encode_short(val)
242
+ raise 'Value too big for short' if val > 0xffff
243
+ [val.to_i].pack('n')
244
+ end
245
+
246
+ # Encode a UTF-8 string and return it
247
+ # (preceded by the length of the string)
248
+ def encode_string(str)
249
+ str = str.to_s.encode('UTF-8')
250
+
251
+ # Force to binary, when assembling the packet
252
+ str.force_encoding('ASCII-8BIT')
253
+ encode_short(str.bytesize) + str
254
+ end
255
+
256
+ # Remove a 16-bit unsigned integer from the front of buffer
257
+ def shift_short(buffer)
258
+ bytes = buffer.slice!(0..1)
259
+ bytes.unpack('n').first
260
+ end
261
+
262
+ # Remove one byte from the front of the string
263
+ def shift_byte(buffer)
264
+ buffer.slice!(0...1).unpack('C').first
265
+ end
266
+
267
+ # Remove 8 bits from the front of buffer
268
+ def shift_bits(buffer)
269
+ buffer.slice!(0...1).unpack('b8').first.split('').map { |b| b == '1' }
270
+ end
271
+
272
+ # Remove n bytes from the front of buffer
273
+ def shift_data(buffer, bytes)
274
+ buffer.slice!(0...bytes)
275
+ end
276
+
277
+ # Remove string from the front of buffer
278
+ def shift_string(buffer)
279
+ len = shift_short(buffer)
280
+ str = shift_data(buffer, len)
281
+ # Strings in MQTT v3.1 are all UTF-8
282
+ str.force_encoding('UTF-8')
283
+ end
284
+
285
+ ## PACKET SUBCLASSES ##
286
+
287
+ # Class representing an MQTT Publish message
288
+ class Publish < MQTT::Packet
289
+ # Duplicate delivery flag
290
+ attr_accessor :duplicate
291
+
292
+ # Retain flag
293
+ attr_accessor :retain
294
+
295
+ # Quality of Service level (0, 1, 2)
296
+ attr_accessor :qos
297
+
298
+ # The topic name to publish to
299
+ attr_accessor :topic
300
+
301
+ # The data to be published
302
+ attr_accessor :payload
303
+
304
+ # Default attribute values
305
+ ATTR_DEFAULTS = {
306
+ :topic => nil,
307
+ :payload => ''
308
+ }
309
+
310
+ # Create a new Publish packet
311
+ def initialize(args = {})
312
+ super(ATTR_DEFAULTS.merge(args))
313
+ end
314
+
315
+ def duplicate
316
+ @flags[3]
317
+ end
318
+
319
+ # Set the DUP flag (true/false)
320
+ def duplicate=(arg)
321
+ @flags[3] = arg.is_a?(Integer) ? (arg == 0x1) : arg
322
+ end
323
+
324
+ def retain
325
+ @flags[0]
326
+ end
327
+
328
+ # Set the retain flag (true/false)
329
+ def retain=(arg)
330
+ @flags[0] = arg.is_a?(Integer) ? (arg == 0x1) : arg
331
+ end
332
+
333
+ def qos
334
+ (@flags[1] ? 0x01 : 0x00) | (@flags[2] ? 0x02 : 0x00)
335
+ end
336
+
337
+ # Set the Quality of Service level (0/1/2)
338
+ def qos=(arg)
339
+ @qos = arg.to_i
340
+ raise "Invalid QoS value: #{@qos}" if @qos < 0 || @qos > 2
341
+
342
+ @flags[1] = (arg & 0x01 == 0x01)
343
+ @flags[2] = (arg & 0x02 == 0x02)
344
+ end
345
+
346
+ # Get serialisation of packet's body
347
+ def encode_body
348
+ body = ''
349
+ if @topic.nil? || @topic.to_s.empty?
350
+ raise 'Invalid topic name when serialising packet'
351
+ end
352
+ body += encode_string(@topic)
353
+ body += encode_short(@id) unless qos.zero?
354
+ body += payload.to_s.dup.force_encoding('ASCII-8BIT')
355
+ body
356
+ end
357
+
358
+ # Parse the body (variable header and payload) of a Publish packet
359
+ def parse_body(buffer)
360
+ super(buffer)
361
+ @topic = shift_string(buffer)
362
+ @id = shift_short(buffer) unless qos.zero?
363
+ @payload = buffer
364
+ end
365
+
366
+ # Check that fixed header flags are valid for this packet type
367
+ # @private
368
+ def validate_flags
369
+ raise ProtocolException, 'Invalid packet: QoS value of 3 is not allowed' if qos == 3
370
+ raise ProtocolException, 'Invalid packet: DUP cannot be set for QoS 0' if qos.zero? && duplicate
371
+ end
372
+
373
+ # Returns a human readable string, summarising the properties of the packet
374
+ def inspect
375
+ "\#<#{self.class}: " \
376
+ "d#{duplicate ? '1' : '0'}, " \
377
+ "q#{qos}, " \
378
+ "r#{retain ? '1' : '0'}, " \
379
+ "m#{id}, " \
380
+ "'#{topic}', " \
381
+ "#{inspect_payload}>"
382
+ end
383
+
384
+ protected
385
+
386
+ def inspect_payload
387
+ str = payload.to_s
388
+ if str.bytesize < 16 && str =~ /^[ -~]*$/
389
+ "'#{str}'"
390
+ else
391
+ "... (#{str.bytesize} bytes)"
392
+ end
393
+ end
394
+ end
395
+
396
+ # Class representing an MQTT Connect Packet
397
+ class Connect < MQTT::Packet
398
+ # The name of the protocol
399
+ attr_accessor :protocol_name
400
+
401
+ # The version number of the protocol
402
+ attr_accessor :protocol_level
403
+
404
+ # The client identifier string
405
+ attr_accessor :client_id
406
+
407
+ # Set to false to keep a persistent session with the server
408
+ attr_accessor :clean_session
409
+
410
+ # Period the server should keep connection open for between pings
411
+ attr_accessor :keep_alive
412
+
413
+ # The topic name to send the Will message to
414
+ attr_accessor :will_topic
415
+
416
+ # The QoS level to send the Will message as
417
+ attr_accessor :will_qos
418
+
419
+ # Set to true to make the Will message retained
420
+ attr_accessor :will_retain
421
+
422
+ # The payload of the Will message
423
+ attr_accessor :will_payload
424
+
425
+ # The username for authenticating with the server
426
+ attr_accessor :username
427
+
428
+ # The password for authenticating with the server
429
+ attr_accessor :password
430
+
431
+ # Default attribute values
432
+ ATTR_DEFAULTS = {
433
+ :client_id => nil,
434
+ :clean_session => true,
435
+ :keep_alive => 15,
436
+ :will_topic => nil,
437
+ :will_qos => 0,
438
+ :will_retain => false,
439
+ :will_payload => '',
440
+ :username => nil,
441
+ :password => nil
442
+ }
443
+
444
+ # Create a new Client Connect packet
445
+ def initialize(args = {})
446
+ super(ATTR_DEFAULTS.merge(args))
447
+
448
+ if ['3.1.0', '3.1'].include?(version)
449
+ self.protocol_name ||= 'MQIsdp'
450
+ self.protocol_level ||= 0x03
451
+ elsif version == '3.1.1'
452
+ self.protocol_name ||= 'MQTT'
453
+ self.protocol_level ||= 0x04
454
+ else
455
+ raise ArgumentError, "Unsupported protocol version: #{version}"
456
+ end
457
+ end
458
+
459
+ # Get serialisation of packet's body
460
+ def encode_body
461
+ body = ''
462
+
463
+ if @version == '3.1.0'
464
+ raise 'Client identifier too short while serialising packet' if @client_id.nil? || @client_id.bytesize < 1
465
+ raise 'Client identifier too long when serialising packet' if @client_id.bytesize > 23
466
+ end
467
+
468
+ body += encode_string(@protocol_name)
469
+ body += encode_bytes(@protocol_level.to_i)
470
+
471
+ if @keep_alive < 0
472
+ raise 'Invalid keep-alive value: cannot be less than 0'
473
+ end
474
+
475
+ # Set the Connect flags
476
+ @connect_flags = 0
477
+ @connect_flags |= 0x02 if @clean_session
478
+ @connect_flags |= 0x04 unless @will_topic.nil?
479
+ @connect_flags |= ((@will_qos & 0x03) << 3)
480
+ @connect_flags |= 0x20 if @will_retain
481
+ @connect_flags |= 0x40 unless @password.nil?
482
+ @connect_flags |= 0x80 unless @username.nil?
483
+ body += encode_bytes(@connect_flags)
484
+
485
+ body += encode_short(@keep_alive)
486
+ body += encode_string(@client_id)
487
+ unless will_topic.nil?
488
+ body += encode_string(@will_topic)
489
+ # The MQTT v3.1 specification says that the payload is a UTF-8 string
490
+ body += encode_string(@will_payload)
491
+ end
492
+ body += encode_string(@username) unless @username.nil?
493
+ body += encode_string(@password) unless @password.nil?
494
+ body
495
+ end
496
+
497
+ # Parse the body (variable header and payload) of a Connect packet
498
+ def parse_body(buffer)
499
+ super(buffer)
500
+ @protocol_name = shift_string(buffer)
501
+ @protocol_level = shift_byte(buffer).to_i
502
+ if @protocol_name == 'MQIsdp' && @protocol_level == 3
503
+ @version = '3.1.0'
504
+ elsif @protocol_name == 'MQTT' && @protocol_level == 4
505
+ @version = '3.1.1'
506
+ else
507
+ raise ProtocolException, "Unsupported protocol: #{@protocol_name}/#{@protocol_level}"
508
+ end
509
+
510
+ @connect_flags = shift_byte(buffer)
511
+ @clean_session = ((@connect_flags & 0x02) >> 1) == 0x01
512
+ @keep_alive = shift_short(buffer)
513
+ @client_id = shift_string(buffer)
514
+ if ((@connect_flags & 0x04) >> 2) == 0x01
515
+ # Last Will and Testament
516
+ @will_qos = ((@connect_flags & 0x18) >> 3)
517
+ @will_retain = ((@connect_flags & 0x20) >> 5) == 0x01
518
+ @will_topic = shift_string(buffer)
519
+ # The MQTT v3.1 specification says that the payload is a UTF-8 string
520
+ @will_payload = shift_string(buffer)
521
+ end
522
+ if ((@connect_flags & 0x80) >> 7) == 0x01 && buffer.bytesize > 0
523
+ @username = shift_string(buffer)
524
+ end
525
+ if ((@connect_flags & 0x40) >> 6) == 0x01 && buffer.bytesize > 0 # rubocop: disable Style/GuardClause
526
+ @password = shift_string(buffer)
527
+ end
528
+ end
529
+
530
+ # Returns a human readable string, summarising the properties of the packet
531
+ def inspect
532
+ str = "\#<#{self.class}: " \
533
+ "keep_alive=#{keep_alive}"
534
+ str += ', clean' if clean_session
535
+ str += ", client_id='#{client_id}'"
536
+ str += ", username='#{username}'" unless username.nil?
537
+ str += ', password=...' unless password.nil?
538
+ str + '>'
539
+ end
540
+
541
+ # ---- Deprecated attributes and methods ---- #
542
+
543
+ # @deprecated Please use {#protocol_level} instead
544
+ def protocol_version
545
+ protocol_level
546
+ end
547
+
548
+ # @deprecated Please use {#protocol_level=} instead
549
+ def protocol_version=(args)
550
+ self.protocol_level = args
551
+ end
552
+ end
553
+
554
+ # Class representing an MQTT Connect Acknowledgment Packet
555
+ class Connack < MQTT::Packet
556
+ # Session Present flag
557
+ attr_accessor :session_present
558
+
559
+ # The return code (defaults to 0 for connection accepted)
560
+ attr_accessor :return_code
561
+
562
+ # Default attribute values
563
+ ATTR_DEFAULTS = { :return_code => 0x00 }
564
+
565
+ # Create a new Client Connect packet
566
+ def initialize(args = {})
567
+ # We must set flags before other attributes
568
+ @connack_flags = [false, false, false, false, false, false, false, false]
569
+ super(ATTR_DEFAULTS.merge(args))
570
+ end
571
+
572
+ # Get the Session Present flag
573
+ def session_present
574
+ @connack_flags[0]
575
+ end
576
+
577
+ # Set the Session Present flag
578
+ def session_present=(arg)
579
+ @connack_flags[0] = arg.is_a?(Integer) ? (arg == 0x1) : arg
580
+ end
581
+
582
+ # Get a string message corresponding to a return code
583
+ def return_msg
584
+ case return_code
585
+ when 0x00
586
+ 'Connection Accepted'
587
+ when 0x01
588
+ 'Connection refused: unacceptable protocol version'
589
+ when 0x02
590
+ 'Connection refused: client identifier rejected'
591
+ when 0x03
592
+ 'Connection refused: server unavailable'
593
+ when 0x04
594
+ 'Connection refused: bad user name or password'
595
+ when 0x05
596
+ 'Connection refused: not authorised'
597
+ else
598
+ "Connection refused: error code #{return_code}"
599
+ end
600
+ end
601
+
602
+ # Get serialisation of packet's body
603
+ def encode_body
604
+ body = ''
605
+ body += encode_bits(@connack_flags)
606
+ body += encode_bytes(@return_code.to_i)
607
+ body
608
+ end
609
+
610
+ # Parse the body (variable header and payload) of a Connect Acknowledgment packet
611
+ def parse_body(buffer)
612
+ super(buffer)
613
+ @connack_flags = shift_bits(buffer)
614
+ unless @connack_flags[1, 7] == [false, false, false, false, false, false, false]
615
+ raise ProtocolException, 'Invalid flags in Connack variable header'
616
+ end
617
+ @return_code = shift_byte(buffer)
618
+
619
+ return if buffer.empty?
620
+ raise ProtocolException, 'Extra bytes at end of Connect Acknowledgment packet'
621
+ end
622
+
623
+ # Returns a human readable string, summarising the properties of the packet
624
+ def inspect
625
+ "\#<#{self.class}: 0x%2.2X>" % return_code
626
+ end
627
+ end
628
+
629
+ # Class representing an MQTT Publish Acknowledgment packet
630
+ class Puback < MQTT::Packet
631
+ # Get serialisation of packet's body
632
+ def encode_body
633
+ encode_short(@id)
634
+ end
635
+
636
+ # Parse the body (variable header and payload) of a packet
637
+ def parse_body(buffer)
638
+ super(buffer)
639
+ @id = shift_short(buffer)
640
+
641
+ return if buffer.empty?
642
+ raise ProtocolException, 'Extra bytes at end of Publish Acknowledgment packet'
643
+ end
644
+
645
+ # Returns a human readable string, summarising the properties of the packet
646
+ def inspect
647
+ "\#<#{self.class}: 0x%2.2X>" % id
648
+ end
649
+ end
650
+
651
+ # Class representing an MQTT Publish Received packet
652
+ class Pubrec < MQTT::Packet
653
+ # Get serialisation of packet's body
654
+ def encode_body
655
+ encode_short(@id)
656
+ end
657
+
658
+ # Parse the body (variable header and payload) of a packet
659
+ def parse_body(buffer)
660
+ super(buffer)
661
+ @id = shift_short(buffer)
662
+
663
+ return if buffer.empty?
664
+ raise ProtocolException, 'Extra bytes at end of Publish Received packet'
665
+ end
666
+
667
+ # Returns a human readable string, summarising the properties of the packet
668
+ def inspect
669
+ "\#<#{self.class}: 0x%2.2X>" % id
670
+ end
671
+ end
672
+
673
+ # Class representing an MQTT Publish Release packet
674
+ class Pubrel < MQTT::Packet
675
+ # Default attribute values
676
+ ATTR_DEFAULTS = {
677
+ :flags => [false, true, false, false]
678
+ }
679
+
680
+ # Create a new Pubrel packet
681
+ def initialize(args = {})
682
+ super(ATTR_DEFAULTS.merge(args))
683
+ end
684
+
685
+ # Get serialisation of packet's body
686
+ def encode_body
687
+ encode_short(@id)
688
+ end
689
+
690
+ # Parse the body (variable header and payload) of a packet
691
+ def parse_body(buffer)
692
+ super(buffer)
693
+ @id = shift_short(buffer)
694
+
695
+ return if buffer.empty?
696
+ raise ProtocolException, 'Extra bytes at end of Publish Release packet'
697
+ end
698
+
699
+ # Check that fixed header flags are valid for this packet type
700
+ # @private
701
+ def validate_flags
702
+ return if @flags == [false, true, false, false]
703
+ raise ProtocolException, 'Invalid flags in PUBREL packet header'
704
+ end
705
+
706
+ # Returns a human readable string, summarising the properties of the packet
707
+ def inspect
708
+ "\#<#{self.class}: 0x%2.2X>" % id
709
+ end
710
+ end
711
+
712
+ # Class representing an MQTT Publish Complete packet
713
+ class Pubcomp < MQTT::Packet
714
+ # Get serialisation of packet's body
715
+ def encode_body
716
+ encode_short(@id)
717
+ end
718
+
719
+ # Parse the body (variable header and payload) of a packet
720
+ def parse_body(buffer)
721
+ super(buffer)
722
+ @id = shift_short(buffer)
723
+
724
+ return if buffer.empty?
725
+ raise ProtocolException, 'Extra bytes at end of Publish Complete packet'
726
+ end
727
+
728
+ # Returns a human readable string, summarising the properties of the packet
729
+ def inspect
730
+ "\#<#{self.class}: 0x%2.2X>" % id
731
+ end
732
+ end
733
+
734
+ # Class representing an MQTT Client Subscribe packet
735
+ class Subscribe < MQTT::Packet
736
+ # One or more topic filters to subscribe to
737
+ attr_accessor :topics
738
+
739
+ # Default attribute values
740
+ ATTR_DEFAULTS = {
741
+ :topics => [],
742
+ :flags => [false, true, false, false]
743
+ }
744
+
745
+ # Create a new Subscribe packet
746
+ def initialize(args = {})
747
+ super(ATTR_DEFAULTS.merge(args))
748
+ end
749
+
750
+ # Set one or more topic filters for the Subscribe packet
751
+ # The topics parameter should be one of the following:
752
+ # * String: subscribe to one topic with QoS 0
753
+ # * Array: subscribe to multiple topics with QoS 0
754
+ # * Hash: subscribe to multiple topics where the key is the topic and the value is the QoS level
755
+ #
756
+ # For example:
757
+ # packet.topics = 'a/b'
758
+ # packet.topics = ['a/b', 'c/d']
759
+ # packet.topics = [['a/b',0], ['c/d',1]]
760
+ # packet.topics = {'a/b' => 0, 'c/d' => 1}
761
+ #
762
+ def topics=(value)
763
+ # Get input into a consistent state
764
+ input = value.is_a?(Array) ? value.flatten : [value]
765
+
766
+ @topics = []
767
+ until input.empty?
768
+ item = input.shift
769
+ if item.is_a?(Hash)
770
+ # Convert hash into an ordered array of arrays
771
+ @topics += item.sort
772
+ elsif item.is_a?(String)
773
+ # Peek at the next item in the array, and remove it if it is an integer
774
+ if input.first.is_a?(Integer)
775
+ qos = input.shift
776
+ @topics << [item, qos]
777
+ else
778
+ @topics << [item, 0]
779
+ end
780
+ else
781
+ # Meh?
782
+ raise "Invalid topics input: #{value.inspect}"
783
+ end
784
+ end
785
+ @topics
786
+ end
787
+
788
+ # Get serialisation of packet's body
789
+ def encode_body
790
+ raise 'no topics given when serialising packet' if @topics.empty?
791
+ body = encode_short(@id)
792
+ topics.each do |item|
793
+ body += encode_string(item[0])
794
+ body += encode_bytes(item[1])
795
+ end
796
+ body
797
+ end
798
+
799
+ # Parse the body (variable header and payload) of a packet
800
+ def parse_body(buffer)
801
+ super(buffer)
802
+ @id = shift_short(buffer)
803
+ @topics = []
804
+ while buffer.bytesize > 0
805
+ topic_name = shift_string(buffer)
806
+ topic_qos = shift_byte(buffer)
807
+ @topics << [topic_name, topic_qos]
808
+ end
809
+ end
810
+
811
+ # Check that fixed header flags are valid for this packet type
812
+ # @private
813
+ def validate_flags
814
+ return if @flags == [false, true, false, false]
815
+ raise ProtocolException, 'Invalid flags in SUBSCRIBE packet header'
816
+ end
817
+
818
+ # Returns a human readable string, summarising the properties of the packet
819
+ def inspect
820
+ _str = "\#<#{self.class}: 0x%2.2X, %s>" % [
821
+ id,
822
+ topics.map { |t| "'#{t[0]}':#{t[1]}" }.join(', ')
823
+ ]
824
+ end
825
+ end
826
+
827
+ # Class representing an MQTT Subscribe Acknowledgment packet
828
+ class Suback < MQTT::Packet
829
+ # An array of return codes, ordered by the topics that were subscribed to
830
+ attr_accessor :return_codes
831
+
832
+ # Default attribute values
833
+ ATTR_DEFAULTS = {
834
+ :return_codes => []
835
+ }
836
+
837
+ # Create a new Subscribe Acknowledgment packet
838
+ def initialize(args = {})
839
+ super(ATTR_DEFAULTS.merge(args))
840
+ end
841
+
842
+ # Set the granted QoS value for each of the topics that were subscribed to
843
+ # Can either be an integer or an array or integers.
844
+ def return_codes=(value)
845
+ if value.is_a?(Array)
846
+ @return_codes = value
847
+ elsif value.is_a?(Integer)
848
+ @return_codes = [value]
849
+ else
850
+ raise 'return_codes should be an integer or an array of return codes'
851
+ end
852
+ end
853
+
854
+ # Get serialisation of packet's body
855
+ def encode_body
856
+ if @return_codes.empty?
857
+ raise 'no granted QoS given when serialising packet'
858
+ end
859
+ body = encode_short(@id)
860
+ return_codes.each { |qos| body += encode_bytes(qos) }
861
+ body
862
+ end
863
+
864
+ # Parse the body (variable header and payload) of a packet
865
+ def parse_body(buffer)
866
+ super(buffer)
867
+ @id = shift_short(buffer)
868
+ @return_codes << shift_byte(buffer) while buffer.bytesize > 0
869
+ end
870
+
871
+ # Returns a human readable string, summarising the properties of the packet
872
+ def inspect
873
+ "\#<#{self.class}: 0x%2.2X, rc=%s>" % [id, return_codes.map { |rc| '0x%2.2X' % rc }.join(',')]
874
+ end
875
+
876
+ # ---- Deprecated attributes and methods ---- #
877
+
878
+ # @deprecated Please use {#return_codes} instead
879
+ def granted_qos
880
+ return_codes
881
+ end
882
+
883
+ # @deprecated Please use {#return_codes=} instead
884
+ def granted_qos=(args)
885
+ self.return_codes = args
886
+ end
887
+ end
888
+
889
+ # Class representing an MQTT Client Unsubscribe packet
890
+ class Unsubscribe < MQTT::Packet
891
+ # One or more topic paths to unsubscribe from
892
+ attr_accessor :topics
893
+
894
+ # Default attribute values
895
+ ATTR_DEFAULTS = {
896
+ :topics => [],
897
+ :flags => [false, true, false, false]
898
+ }
899
+
900
+ # Create a new Unsubscribe packet
901
+ def initialize(args = {})
902
+ super(ATTR_DEFAULTS.merge(args))
903
+ end
904
+
905
+ # Set one or more topic paths to unsubscribe from
906
+ def topics=(value)
907
+ @topics = value.is_a?(Array) ? value : [value]
908
+ end
909
+
910
+ # Get serialisation of packet's body
911
+ def encode_body
912
+ raise 'no topics given when serialising packet' if @topics.empty?
913
+ body = encode_short(@id)
914
+ topics.each { |topic| body += encode_string(topic) }
915
+ body
916
+ end
917
+
918
+ # Parse the body (variable header and payload) of a packet
919
+ def parse_body(buffer)
920
+ super(buffer)
921
+ @id = shift_short(buffer)
922
+ @topics << shift_string(buffer) while buffer.bytesize > 0
923
+ end
924
+
925
+ # Check that fixed header flags are valid for this packet type
926
+ # @private
927
+ def validate_flags
928
+ return if @flags == [false, true, false, false]
929
+ raise ProtocolException, 'Invalid flags in UNSUBSCRIBE packet header'
930
+ end
931
+
932
+ # Returns a human readable string, summarising the properties of the packet
933
+ def inspect
934
+ "\#<#{self.class}: 0x%2.2X, %s>" % [
935
+ id,
936
+ topics.map { |t| "'#{t}'" }.join(', ')
937
+ ]
938
+ end
939
+ end
940
+
941
+ # Class representing an MQTT Unsubscribe Acknowledgment packet
942
+ class Unsuback < MQTT::Packet
943
+ # Create a new Unsubscribe Acknowledgment packet
944
+ def initialize(args = {})
945
+ super(args)
946
+ end
947
+
948
+ # Get serialisation of packet's body
949
+ def encode_body
950
+ encode_short(@id)
951
+ end
952
+
953
+ # Parse the body (variable header and payload) of a packet
954
+ def parse_body(buffer)
955
+ super(buffer)
956
+ @id = shift_short(buffer)
957
+
958
+ return if buffer.empty?
959
+ raise ProtocolException, 'Extra bytes at end of Unsubscribe Acknowledgment packet'
960
+ end
961
+
962
+ # Returns a human readable string, summarising the properties of the packet
963
+ def inspect
964
+ "\#<#{self.class}: 0x%2.2X>" % id
965
+ end
966
+ end
967
+
968
+ # Class representing an MQTT Ping Request packet
969
+ class Pingreq < MQTT::Packet
970
+ # Create a new Ping Request packet
971
+ def initialize(args = {})
972
+ super(args)
973
+ end
974
+
975
+ # Check the body
976
+ def parse_body(buffer)
977
+ super(buffer)
978
+
979
+ return if buffer.empty?
980
+ raise ProtocolException, 'Extra bytes at end of Ping Request packet'
981
+ end
982
+ end
983
+
984
+ # Class representing an MQTT Ping Response packet
985
+ class Pingresp < MQTT::Packet
986
+ # Create a new Ping Response packet
987
+ def initialize(args = {})
988
+ super(args)
989
+ end
990
+
991
+ # Check the body
992
+ def parse_body(buffer)
993
+ super(buffer)
994
+
995
+ return if buffer.empty?
996
+ raise ProtocolException, 'Extra bytes at end of Ping Response packet'
997
+ end
998
+ end
999
+
1000
+ # Class representing an MQTT Client Disconnect packet
1001
+ class Disconnect < MQTT::Packet
1002
+ # Create a new Client Disconnect packet
1003
+ def initialize(args = {})
1004
+ super(args)
1005
+ end
1006
+
1007
+ # Check the body
1008
+ def parse_body(buffer)
1009
+ super(buffer)
1010
+
1011
+ return if buffer.empty?
1012
+ raise ProtocolException, 'Extra bytes at end of Disconnect packet'
1013
+ end
1014
+ end
1015
+
1016
+ # ---- Deprecated attributes and methods ---- #
1017
+ public
1018
+
1019
+ # @deprecated Please use {#id} instead
1020
+ def message_id
1021
+ id
1022
+ end
1023
+
1024
+ # @deprecated Please use {#id=} instead
1025
+ def message_id=(args)
1026
+ self.id = args
1027
+ end
1028
+ end
1029
+
1030
+ # An enumeration of the MQTT packet types
1031
+ PACKET_TYPES = [
1032
+ nil,
1033
+ MQTT::Packet::Connect,
1034
+ MQTT::Packet::Connack,
1035
+ MQTT::Packet::Publish,
1036
+ MQTT::Packet::Puback,
1037
+ MQTT::Packet::Pubrec,
1038
+ MQTT::Packet::Pubrel,
1039
+ MQTT::Packet::Pubcomp,
1040
+ MQTT::Packet::Subscribe,
1041
+ MQTT::Packet::Suback,
1042
+ MQTT::Packet::Unsubscribe,
1043
+ MQTT::Packet::Unsuback,
1044
+ MQTT::Packet::Pingreq,
1045
+ MQTT::Packet::Pingresp,
1046
+ MQTT::Packet::Disconnect,
1047
+ nil
1048
+ ]
1049
+ end