# encoding: BINARY module MQTT module SN # Class representing a MQTT::SN Packet # Performs binary encoding and decoding of headers class Packet attr_accessor :duplicate # Duplicate delivery flag attr_accessor :qos # Quality of Service level attr_accessor :retain # Retain flag attr_accessor :request_will # Request that gateway prompts for Will attr_accessor :clean_session # When true, subscriptions are deleted after disconnect attr_accessor :topic_id_type # One of :normal, :predefined or :short DEFAULTS = {} # Parse buffer into new packet object def self.parse(buffer) # Parse the fixed header (length and type) length, type_id, body = buffer.unpack('CCa*') length, type_id, body = buffer.unpack('xnCa*') if length == 1 # Double-check the length if buffer.length != length raise ProtocolException, 'Length of packet is not the same as the length header' end packet_class = PACKET_TYPES[type_id] if packet_class.nil? raise ProtocolException, "Invalid packet type identifier: #{type_id}" end # Create a new packet object packet = packet_class.new packet.parse_body(body) packet end # Create a new empty packet def initialize(args = {}) update_attributes(self.class::DEFAULTS.merge(args)) end def update_attributes(attr = {}) attr.each_pair do |k, v| send("#{k}=", v) end end # Get the identifer for this packet type def type_id PACKET_TYPES.each_pair do |key, value| return key if self.class == value end raise "Invalid packet type: #{self.class}" end # Serialise the packet def to_s # Get the packet's variable header and payload body = encode_body # Build up the body length field bytes body_length = body.length raise 'MQTT-SN Packet is too big, maximum packet body size is 65531' if body_length > 65_531 if body_length > 253 [0x01, body_length + 4, type_id].pack('CnC') + body else [body_length + 2, type_id].pack('CC') + body end end def parse_body(buffer); end protected def parse_flags(flags) self.duplicate = ((flags & 0x80) >> 7) == 0x01 self.qos = (flags & 0x60) >> 5 self.qos = -1 if qos == 3 self.retain = ((flags & 0x10) >> 4) == 0x01 self.request_will = ((flags & 0x08) >> 3) == 0x01 self.clean_session = ((flags & 0x04) >> 2) == 0x01 self.topic_id_type = case (flags & 0x03) when 0x0 :normal when 0x1 :predefined when 0x2 :short end end # Get serialisation of packet's body (variable header and payload) def encode_body '' # No body by default end def encode_flags flags = 0x00 flags += 0x80 if duplicate case qos when -1 flags += 0x60 when 1 flags += 0x20 when 2 flags += 0x40 end flags += 0x10 if retain flags += 0x08 if request_will flags += 0x04 if clean_session case topic_id_type when :normal flags += 0x0 when :predefined flags += 0x1 when :short flags += 0x2 end flags end def encode_topic_id if topic_id_type == :short unless topic_id.is_a?(String) raise "topic_id must be an String for type #{topic_id_type}" end (topic_id[0].ord << 8) + topic_id[1].ord else unless topic_id.is_a?(Integer) raise "topic_id must be an Integer for type #{topic_id_type}" end topic_id end end def parse_topic_id(topic_id) if topic_id_type == :short int = topic_id.to_i self.topic_id = [(int >> 8) & 0xFF, int & 0xFF].pack('CC') else self.topic_id = topic_id end end # Used where a field can either be a Topic Id or a Topic Name # (the Subscribe and Unsubscribe packet types) def encode_topic case topic_id_type when :normal topic_name when :short if topic_name.nil? topic_id else topic_name end when :predefined [topic_id].pack('n') end end # Used where a field can either be a Topic Id or a Topic Name # (the Subscribe and Unsubscribe packet types) def parse_topic(topic) case topic_id_type when :normal self.topic_name = topic when :short self.topic_name = topic self.topic_id = topic when :predefined self.topic_id = topic.unpack('n').first end end class Advertise < Packet attr_accessor :gateway_id attr_accessor :duration DEFAULTS = { :gateway_id => 0x00, :duration => 0 } def encode_body [gateway_id, duration].pack('Cn') end def parse_body(buffer) self.gateway_id, self.duration = buffer.unpack('Cn') end end class Searchgw < Packet attr_accessor :radius DEFAULTS = { :radius => 1 } def encode_body [radius].pack('C') end def parse_body(buffer) self.radius, _ignore = buffer.unpack('C') end end class Gwinfo < Packet attr_accessor :gateway_id attr_accessor :gateway_address DEFAULTS = { :gateway_id => 0, :gateway_address => nil } def encode_body [gateway_id, gateway_address].pack('Ca*') end def parse_body(buffer) if buffer.length > 1 self.gateway_id, self.gateway_address = buffer.unpack('Ca*') else self.gateway_id, _ignore = buffer.unpack('C') self.gateway_address = nil end end end class Connect < Packet attr_accessor :keep_alive attr_accessor :client_id DEFAULTS = { :request_will => false, :clean_session => true, :keep_alive => 15 } # Get serialisation of packet's body def encode_body if @client_id.nil? || @client_id.empty? || @client_id.length > 23 raise 'Invalid client identifier when serialising packet' end [encode_flags, 0x01, keep_alive, client_id].pack('CCna*') end def parse_body(buffer) flags, protocol_id, self.keep_alive, self.client_id = buffer.unpack('CCna*') if protocol_id != 0x01 raise ProtocolException, "Unsupported protocol ID number: #{protocol_id}" end parse_flags(flags) end end class Connack < Packet attr_accessor :return_code # Get a string message corresponding to a return code def return_msg case return_code when 0x00 'Accepted' when 0x01 'Rejected: congestion' when 0x02 'Rejected: invalid topic ID' when 0x03 'Rejected: not supported' else "Rejected: error code #{return_code}" end end def encode_body raise 'return_code must be an Integer' unless return_code.is_a?(Integer) [return_code].pack('C') end def parse_body(buffer) self.return_code = buffer.unpack('C')[0] end end class Willtopicreq < Packet # No attributes end class Willtopic < Packet attr_accessor :topic_name DEFAULTS = { :qos => 0, :retain => false, :topic_name => nil } def encode_body if topic_name.nil? || topic_name.empty? '' else [encode_flags, topic_name].pack('Ca*') end end def parse_body(buffer) if buffer.length > 1 flags, self.topic_name = buffer.unpack('Ca*') else flags, _ignore = buffer.unpack('C') self.topic_name = nil end parse_flags(flags) end end class Willmsgreq < Packet # No attributes end class Willmsg < Packet attr_accessor :data def encode_body data end def parse_body(buffer) self.data = buffer end end class Register < Packet attr_accessor :id attr_accessor :topic_id attr_accessor :topic_name DEFAULTS = { :id => 0x00, :topic_id_type => :normal } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) raise 'topic_id must be an Integer' unless topic_id.is_a?(Integer) [topic_id, id, topic_name].pack('nna*') end def parse_body(buffer) self.topic_id, self.id, self.topic_name = buffer.unpack('nna*') end end class Regack < Packet attr_accessor :id attr_accessor :topic_id attr_accessor :return_code DEFAULTS = { :id => 0x00, :topic_id => 0x00, :topic_id_type => :normal } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) raise 'topic_id must be an Integer' unless topic_id.is_a?(Integer) [topic_id, id, return_code].pack('nnC') end def parse_body(buffer) self.topic_id, self.id, self.return_code = buffer.unpack('nnC') end end class Publish < Packet attr_accessor :topic_id attr_accessor :id attr_accessor :data DEFAULTS = { :id => 0x00, :duplicate => false, :qos => 0, :retain => false, :topic_id_type => :normal } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) [encode_flags, encode_topic_id, id, data].pack('Cnna*') end def parse_body(buffer) flags, topic_id, self.id, self.data = buffer.unpack('Cnna*') parse_flags(flags) parse_topic_id(topic_id) end end class Puback < Packet attr_accessor :topic_id attr_accessor :id attr_accessor :return_code DEFAULTS = { :id => 0x00, :topic_id => nil, :return_code => 0x00 } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) raise 'topic_id must be an Integer' unless topic_id.is_a?(Integer) [topic_id, id, return_code].pack('nnC') end def parse_body(buffer) self.topic_id, self.id, self.return_code = buffer.unpack('nnC') end end class Pubcomp < Packet attr_accessor :id DEFAULTS = { :id => 0x00 } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) [id].pack('n') end def parse_body(buffer) self.id, _ignore = buffer.unpack('n') end end class Pubrec < Packet attr_accessor :id DEFAULTS = { :id => 0x00 } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) [id].pack('n') end def parse_body(buffer) self.id, _ignore = buffer.unpack('n') end end class Pubrel < Packet attr_accessor :id DEFAULTS = { :id => 0x00 } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) [id].pack('n') end def parse_body(buffer) self.id, _ignore = buffer.unpack('n') end end class Subscribe < Packet attr_accessor :id attr_accessor :topic_id attr_accessor :topic_name DEFAULTS = { :id => 0x00, :topic_id_type => :normal } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) [encode_flags, id, encode_topic].pack('Cna*') end def parse_body(buffer) flags, self.id, topic = buffer.unpack('Cna*') parse_flags(flags) parse_topic(topic) end end class Suback < Packet attr_accessor :id attr_accessor :topic_id attr_accessor :return_code DEFAULTS = { :qos => 0, :id => 0x00, :topic_id => 0x00, :topic_id_type => :normal } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) [encode_flags, encode_topic_id, id, return_code].pack('CnnC') end def parse_body(buffer) flags, topic_id, self.id, self.return_code = buffer.unpack('CnnC') parse_flags(flags) parse_topic_id(topic_id) end end class Unsubscribe < Packet attr_accessor :id attr_accessor :topic_id attr_accessor :topic_name DEFAULTS = { :id => 0x00, :topic_id_type => :normal } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) [encode_flags, id, encode_topic].pack('Cna*') end def parse_body(buffer) flags, self.id, topic = buffer.unpack('Cna*') parse_flags(flags) parse_topic(topic) end end class Unsuback < Packet attr_accessor :id DEFAULTS = { :id => 0x00 } def encode_body raise 'id must be an Integer' unless id.is_a?(Integer) [id].pack('n') end def parse_body(buffer) self.id = buffer.unpack('n').first end end class Pingreq < Packet # No attributes end class Pingresp < Packet # No attributes end class Disconnect < Packet attr_accessor :duration DEFAULTS = { :duration => nil } def encode_body if duration.nil? || duration.zero? '' else [duration].pack('n') end end def parse_body(buffer) self.duration = buffer.length == 2 ? buffer.unpack('n').first : nil end end class Willtopicupd < Packet attr_accessor :topic_name DEFAULTS = { :qos => 0, :retain => false, :topic_name => nil } def encode_body if topic_name.nil? || topic_name.empty? '' else [encode_flags, topic_name].pack('Ca*') end end def parse_body(buffer) if buffer.length > 1 flags, self.topic_name = buffer.unpack('Ca*') parse_flags(flags) else self.topic_name = nil end end end class Willtopicresp < Packet attr_accessor :return_code DEFAULTS = { :return_code => 0x00 } def encode_body raise 'return_code must be an Integer' unless return_code.is_a?(Integer) [return_code].pack('C') end def parse_body(buffer) self.return_code, _ignore = buffer.unpack('C') end end class Willmsgupd < Packet attr_accessor :data def encode_body data end def parse_body(buffer) self.data = buffer end end class Willmsgresp < Packet attr_accessor :return_code DEFAULTS = { :return_code => 0x00 } def encode_body raise 'return_code must be an Integer' unless return_code.is_a?(Integer) [return_code].pack('C') end def parse_body(buffer) self.return_code, _ignore = buffer.unpack('C') end end end # An enumeration of the MQTT-SN packet types PACKET_TYPES = { 0x00 => MQTT::SN::Packet::Advertise, 0x01 => MQTT::SN::Packet::Searchgw, 0x02 => MQTT::SN::Packet::Gwinfo, 0x04 => MQTT::SN::Packet::Connect, 0x05 => MQTT::SN::Packet::Connack, 0x06 => MQTT::SN::Packet::Willtopicreq, 0x07 => MQTT::SN::Packet::Willtopic, 0x08 => MQTT::SN::Packet::Willmsgreq, 0x09 => MQTT::SN::Packet::Willmsg, 0x0a => MQTT::SN::Packet::Register, 0x0b => MQTT::SN::Packet::Regack, 0x0c => MQTT::SN::Packet::Publish, 0x0d => MQTT::SN::Packet::Puback, 0x0e => MQTT::SN::Packet::Pubcomp, 0x0f => MQTT::SN::Packet::Pubrec, 0x10 => MQTT::SN::Packet::Pubrel, 0x12 => MQTT::SN::Packet::Subscribe, 0x13 => MQTT::SN::Packet::Suback, 0x14 => MQTT::SN::Packet::Unsubscribe, 0x15 => MQTT::SN::Packet::Unsuback, 0x16 => MQTT::SN::Packet::Pingreq, 0x17 => MQTT::SN::Packet::Pingresp, 0x18 => MQTT::SN::Packet::Disconnect, 0x1a => MQTT::SN::Packet::Willtopicupd, 0x1b => MQTT::SN::Packet::Willtopicresp, 0x1c => MQTT::SN::Packet::Willmsgupd, 0x1d => MQTT::SN::Packet::Willmsgresp } end end