BallAerospace/COSMOS

View on GitHub
cosmos/lib/cosmos/interfaces/protocols/preidentified_protocol.rb

Summary

Maintainability
C
1 day
Test Coverage
# encoding: ascii-8bit

# Copyright 2022 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# This program may also be used under the terms of a commercial or
# enterprise edition license of COSMOS if purchased from the
# copyright holder

require 'cosmos/interfaces/protocols/burst_protocol'

module Cosmos
  # Delineates packets using the COSMOS preidentification system
  class PreidentifiedProtocol < BurstProtocol
    COSMOS4_STORED_FLAG_MASK = 0x80
    COSMOS4_EXTRA_FLAG_MASK = 0x40

    # @param sync_pattern (see BurstProtocol#initialize)
    # @param max_length [Integer] The maximum allowed value of the length field
    # @param allow_empty_data [true/false/nil] See Protocol#initialize
    def initialize(sync_pattern = nil, max_length = nil, mode = 4, allow_empty_data = nil)
      super(0, sync_pattern, false, allow_empty_data)
      @max_length = ConfigParser.handle_nil(max_length)
      @max_length = Integer(@max_length) if @max_length
      @mode = Integer(mode)
    end

    def reset
      super()
      @reduction_state = :START
    end

    def read_packet(packet)
      packet.received_time = @read_received_time
      packet.target_name = @read_target_name
      packet.packet_name = @read_packet_name
      if @mode == 4 # COSMOS4.3+ Protocol
        packet.stored = @read_stored
        packet.extra = @read_extra
      end
      return packet
    end

    def write_packet(packet)
      received_time = packet.received_time
      received_time = Time.now unless received_time
      @write_time_seconds = [received_time.tv_sec].pack('N') # UINT32
      @write_time_microseconds = [received_time.tv_usec].pack('N') # UINT32
      @write_target_name = packet.target_name
      @write_target_name = 'UNKNOWN' unless @write_target_name
      @write_packet_name = packet.packet_name
      @write_packet_name = 'UNKNOWN' unless @write_packet_name
      if @mode == 4 # COSMOS4.3+ Protocol
        @write_flags = 0
        @write_flags |= COSMOS4_STORED_FLAG_MASK if packet.stored
        @write_extra = nil
        if packet.extra
          @write_flags |= COSMOS4_EXTRA_FLAG_MASK
          @write_extra = packet.extra.to_json
        end
      end
      return packet
    end

    def write_data(data)
      data_length = [data.length].pack('N') # UINT32
      data_to_send = ''
      data_to_send << @sync_pattern if @sync_pattern
      if @mode == 4 # COSMOS4.3+ Protocol
        data_to_send << @write_flags
        if @write_extra
          data_to_send << [@write_extra.length].pack('N')
          data_to_send << @write_extra
        end
      end
      data_to_send << @write_time_seconds
      data_to_send << @write_time_microseconds
      data_to_send << @write_target_name.length
      data_to_send << @write_target_name
      data_to_send << @write_packet_name.length
      data_to_send << @write_packet_name
      data_to_send << data_length
      data_to_send << data
      return data_to_send
    end

    protected

    def read_length_field_followed_by_string(length_num_bytes)
      # Read bytes for string length
      return :STOP if @data.length < length_num_bytes

      string_length = @data[0..(length_num_bytes - 1)]

      case length_num_bytes
      when 1
        string_length = string_length.unpack('C')[0] # UINT8
      when 2
        string_length = string_length.unpack('n')[0] # UINT16
      when 4
        string_length = string_length.unpack('N')[0] # UINT32
        raise "Length value received larger than max_length: #{string_length} > #{@max_length}" if @max_length and string_length > @max_length
      else
        raise "Unsupported length given to read_length_field_followed_by_string: #{length_num_bytes}"
      end

      # Read String
      return :STOP if @data.length < (string_length + length_num_bytes)

      next_index = string_length + length_num_bytes
      string = @data[length_num_bytes..(next_index - 1)]

      # Remove data from current_data
      @data.replace(@data[next_index..-1])

      return string
    end

    def reduce_to_single_packet
      # Discard sync pattern if present
      if @sync_pattern
        if @reduction_state == :START
          return :STOP if @data.length < @sync_pattern.length

          @data.replace(@data[(@sync_pattern.length)..-1])
          @reduction_state = :SYNC_REMOVED
        end
      elsif @reduction_state == :START
        @reduction_state = :SYNC_REMOVED
      end

      if @reduction_state == :SYNC_REMOVED and @mode == 4
        # Read and remove flags
        return :STOP if @data.length < 1

        flags = @data[0].unpack('C')[0] # byte
        @data.replace(@data[1..-1])
        @read_stored = false
        @read_stored = true if (flags & COSMOS4_STORED_FLAG_MASK) != 0
        @read_extra = nil
        if (flags & COSMOS4_EXTRA_FLAG_MASK) != 0
          @reduction_state = :NEED_EXTRA
        else
          @reduction_state = :FLAGS_REMOVED
        end
      end

      if @reduction_state == :NEED_EXTRA
        # Read and remove extra
        @read_extra = read_length_field_followed_by_string(4)
        return :STOP if @read_extra == :STOP

        @read_extra = JSON.parse(@read_extra)
        @reduction_state = :FLAGS_REMOVED
      end

      if @reduction_state == :FLAGS_REMOVED or (@reduction_state == :SYNC_REMOVED and @mode != 4)
        # Read and remove packet received time
        return :STOP if @data.length < 8

        time_seconds = @data[0..3].unpack('N')[0] # UINT32
        time_microseconds = @data[4..7].unpack('N')[0] # UINT32
        @read_received_time = Time.at(time_seconds, time_microseconds).sys
        @data.replace(@data[8..-1])
        @reduction_state = :TIME_REMOVED
      end

      if @reduction_state == :TIME_REMOVED
        # Read and remove the target name
        @read_target_name = read_length_field_followed_by_string(1)
        return :STOP if @read_target_name == :STOP

        @reduction_state = :TARGET_NAME_REMOVED
      end

      if @reduction_state == :TARGET_NAME_REMOVED
        # Read and remove the packet name
        @read_packet_name = read_length_field_followed_by_string(1)
        return :STOP if @read_packet_name == :STOP

        @reduction_state = :PACKET_NAME_REMOVED
      end

      if @reduction_state == :PACKET_NAME_REMOVED
        # Read packet data and return
        packet_data = read_length_field_followed_by_string(4)
        return :STOP if packet_data == :STOP

        @reduction_state = :START
        return packet_data
      end

      raise "Error should never reach end of method #{@reduction_state}"
    end
  end
end