lib/mqtt/sn/packet.rb



# 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