Permalink
Fetching contributors…
Cannot retrieve contributors at this time
443 lines (393 sloc) 15.4 KB
# encoding: ascii-8bit
# Copyright 2014 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 General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
require 'cosmos/packets/structure_item'
require 'cosmos/packets/packet_item_limits'
require 'cosmos/conversions/conversion'
module Cosmos
# Maintains knowledge of an item in a Packet
class PacketItem < StructureItem
# @return [String] Printf-style string used to format the item
attr_reader :format_string
# Conversion instance used when reading the PacketItem
# @return [Conversion] Read conversion
attr_reader :read_conversion
# Conversion instance used when writing the PacketItem
# @return [Conversion] Write conversion
attr_reader :write_conversion
# The id_value type depends on the data_type of the PacketItem
# @return Value used to identify a packet
attr_reader :id_value
# States are used to convert from a numeric value to a String.
# @return [Hash] Item states given as STATE_NAME => VALUE
attr_reader :states
# @return [String] Description of the item
attr_reader :description
# Returns the fully spelled out units of the item. For example,
# if the item represents a voltage, this would return "Voltage".
# @return [String] Units of the item
attr_reader :units_full
# Returns the abbreviated units of the item. For example,
# if the item represents a voltage, this would return "V".
# @return [String] Abbreviated units of the item
attr_reader :units
# The default value type depends on the data_type of the PacketItem
# @return Default value for this item
attr_accessor :default
# The valid range of values for this item. Returns nil for items with
# data_type of :STRING or :BLOCK items.
# @return [Range] Valid range of values or nil
attr_reader :range
# @return [Boolean] Whether this item must be specified or can use its
# default value. true if it must be specified.
attr_accessor :required
# States that are hazardous for this item as well as their descriptions
# @return [Hash] Hazardous states given as STATE_NAME => DESCRIPTION. If no
# description was given the value will be nil.
attr_reader :hazardous
# Colors associated with states
# @return [Hash] State colors given as STATE_NAME => COLOR
attr_reader :state_colors
# The allowable state colors
STATE_COLORS = [:GREEN, :YELLOW, :RED]
# @return [PacketItemLimits] All information regarding limits for this PacketItem
attr_reader :limits
# (see StructureItem#initialize)
# It also initializes the attributes of the PacketItem.
def initialize(name, bit_offset, bit_size, data_type, endianness, array_size = nil, overflow = :ERROR)
super(name, bit_offset, bit_size, data_type, endianness, array_size, overflow)
@format_string = nil
@read_conversion = nil
@write_conversion = nil
@id_value = nil
@states = nil
@description = nil
@units_full = nil
@units = nil
@default = nil
@range = nil
@required = false
@hazardous = nil
@state_colors = nil
@limits = PacketItemLimits.new
@persistence_setting = 1
@persistence_count = 0
@meta = nil
end
def format_string=(format_string)
if format_string
raise ArgumentError, "#{@name}: format_string must be a String but is a #{format_string.class}" unless String === format_string
raise ArgumentError, "#{@name}: format_string invalid '#{format_string}'" unless format_string =~ /%.*(b|B|d|i|o|u|x|X|e|E|f|g|G|a|A|c|p|s|%)/
@format_string = format_string.clone.freeze
else
@format_string = nil
end
end
def read_conversion=(read_conversion)
if read_conversion
raise ArgumentError, "#{@name}: read_conversion must be a Cosmos::Conversion but is a #{read_conversion.class}" unless Cosmos::Conversion === read_conversion
@read_conversion = read_conversion.clone
else
@read_conversion = nil
end
end
def write_conversion=(write_conversion)
if write_conversion
raise ArgumentError, "#{@name}: write_conversion must be a Cosmos::Conversion but is a #{write_conversion.class}" unless Cosmos::Conversion === write_conversion
@write_conversion = write_conversion.clone
else
@write_conversion = nil
end
end
def id_value=(id_value)
if id_value
@id_value = convert(id_value, @data_type)
else
@id_value = nil
end
end
# Assignment operator for states to make sure it is a Hash with uppercase keys
def states=(states)
if states
raise ArgumentError, "#{@name}: states must be a Hash but is a #{states.class}" unless Hash === states
# Make sure all states are in upper case
upcase_states = {}
states.each do |key, value|
upcase_states[key.to_s.upcase] = value
end
@states = upcase_states
@state_colors ||= {}
else
@states = nil
end
end
def description=(description)
if description
raise ArgumentError, "#{@name}: description must be a String but is a #{description.class}" unless String === description
@description = description.clone.freeze
else
@description = nil
end
end
def units_full=(units_full)
if units_full
raise ArgumentError, "#{@name}: units_full must be a String but is a #{units_full.class}" unless String === units_full
@units_full = units_full.clone.freeze
else
@units_full = nil
end
end
def units=(units)
if units
raise ArgumentError, "#{@name}: units must be a String but is a #{units.class}" unless String === units
@units = units.clone.freeze
else
@units = nil
end
end
def check_default_and_range_data_types
if @default and !@write_conversion
if @array_size
raise ArgumentError, "#{@name}: default must be an Array but is a #{default.class}" unless Array === @default
else
case data_type
when :INT, :UINT
raise ArgumentError, "#{@name}: default must be a Integer but is a #{@default.class}" unless Integer === @default
if @range
raise ArgumentError, "#{@name}: minimum must be a Integer but is a #{@range.first.class}" unless Integer === @range.first
raise ArgumentError, "#{@name}: maximum must be a Integer but is a #{@range.last.class}" unless Integer === @range.last
end
when :FLOAT
raise ArgumentError, "#{@name}: default must be a Float but is a #{@default.class}" unless Float === @default or Integer === @default
@default = @default.to_f
if @range
raise ArgumentError, "#{@name}: minimum must be a Float but is a #{@range.first.class}" unless Float === @range.first or Integer === @range.first
raise ArgumentError, "#{@name}: maximum must be a Float but is a #{@range.last.class}" unless Float === @range.last or Integer === @range.last
@range = ((@range.first.to_f)..(@range.last.to_f))
end
when :BLOCK, :STRING
raise ArgumentError, "#{@name}: default must be a String but is a #{@default.class}" unless String === @default
@default = @default.clone.freeze
end
end
end
end
def range=(range)
if range
raise ArgumentError, "#{@name}: range must be a Range but is a #{range.class}" unless Range === range
@range = range.clone.freeze
else
@range = nil
end
end
def hazardous=(hazardous)
if hazardous
raise ArgumentError, "#{@name}: hazardous must be a Hash but is a #{hazardous.class}" unless Hash === hazardous
@hazardous = hazardous.clone
else
@hazardous = nil
end
end
def state_colors=(state_colors)
if state_colors
raise ArgumentError, "#{@name}: state_colors must be a Hash but is a #{state_colors.class}" unless Hash === state_colors
@state_colors = state_colors.clone
else
@state_colors = nil
end
end
def limits=(limits)
if limits
raise ArgumentError, "#{@name}: limits must be a PacketItemLimits but is a #{limits.class}" unless PacketItemLimits === limits
@limits = limits.clone
else
@limits = nil
end
end
def meta
@meta ||= {}
end
def meta=(meta)
if meta
raise ArgumentError, "#{@name}: meta must be a Hash but is a #{meta.class}" unless Hash === meta
@meta = meta.clone
else
@meta = nil
end
end
# Make a light weight clone of this item
def clone
item = super()
item.format_string = self.format_string.clone if self.format_string
item.read_conversion = self.read_conversion.clone if self.read_conversion
item.write_conversion = self.write_conversion.clone if self.write_conversion
item.states = self.states.clone if self.states
item.description = self.description.clone if self.description
item.units_full = self.units_full.clone if self.units_full
item.units = self.units.clone if self.units
item.default = self.default.clone if self.default and String === self.default
item.hazardous = self.hazardous.clone if self.hazardous
item.state_colors = self.state_colors.clone if self.state_colors
item.limits = self.limits.clone if self.limits
item.meta = self.meta.clone if @meta
item
end
alias dup clone
def to_hash
hash = super()
hash['format_string'] = self.format_string
if self.read_conversion
hash['read_conversion'] = self.read_conversion.to_s
else
hash['read_conversion'] = nil
end
if self.write_conversion
hash['write_conversion'] = self.write_conversion.to_s
else
hash['write_conversion'] = nil
end
hash['id_value'] = self.id_value
hash['states'] = self.states
hash['description'] = self.description
hash['units_full'] = self.units_full
hash['units'] = self.units
hash['default'] = self.default
hash['range'] = self.range
hash['required'] = self.required
hash['hazardous'] = self.hazardous
hash['state_colors'] = self.state_colors
hash['limits'] = self.limits.to_hash
hash['meta'] = nil
hash['meta'] = @meta if @meta
hash
end
def calculate_range
first = range.first
last = range.last
if data_type == :FLOAT
if bit_size == 32
if range.first == -3.402823e38
first = 'MIN'
end
if range.last == 3.402823e38
last = 'MAX'
end
else
if range.first == -Float::MAX
first = 'MIN'
end
if range.last == Float::MAX
last = 'MAX'
end
end
end
return [first, last]
end
def to_config(cmd_or_tlm, default_endianness)
config = ''
if cmd_or_tlm == :TELEMETRY
if self.array_size
config << " ARRAY_ITEM #{self.name.to_s.quote_if_necessary} #{self.bit_offset} #{self.bit_size} #{self.data_type} #{self.array_size} \"#{self.description.to_s.gsub("\"", "'")}\""
elsif self.id_value
id_value = self.id_value
if self.data_type == :BLOCK || self.data_type == :STRING
unless self.id_value.is_printable?
id_value = "0x" + self.id_value.simple_formatted
else
id_value = "\"#{self.id_value}\""
end
end
config << " ID_ITEM #{self.name.to_s.quote_if_necessary} #{self.bit_offset} #{self.bit_size} #{self.data_type} #{id_value} \"#{self.description.to_s.gsub("\"", "'")}\""
else
config << " ITEM #{self.name.to_s.quote_if_necessary} #{self.bit_offset} #{self.bit_size} #{self.data_type} \"#{self.description.to_s.gsub("\"", "'")}\""
end
else # :COMMAND
if self.array_size
config << " ARRAY_PARAMETER #{self.name.to_s.quote_if_necessary} #{self.bit_offset} #{self.bit_size} #{self.data_type} #{self.array_size} \"#{self.description.to_s.gsub("\"", "'")}\""
else
config << parameter_config()
end
end
config << " #{self.endianness}" if (self.endianness != default_endianness && self.data_type != :STRING && self.data_type != :BLOCK)
config << "\n"
config << " REQUIRED\n" if self.required
config << " FORMAT_STRING #{self.format_string.to_s.quote_if_necessary}\n" if self.format_string
config << " UNITS #{self.units_full.to_s.quote_if_necessary} #{self.units.to_s.quote_if_necessary}\n" if self.units
config << " OVERFLOW #{self.overflow}\n" if self.overflow != :ERROR
if @states
@states.each do |state_name, state_value|
config << " STATE #{state_name.to_s.quote_if_necessary} #{state_value.to_s.quote_if_necessary}"
if @hazardous and @hazardous[state_name]
config << " HAZARDOUS #{@hazardous[state_name].to_s.quote_if_necessary}"
end
if @state_colors and @state_colors[state_name]
config << " #{@state_colors[state_name]}"
end
config << "\n"
end
end
config << self.read_conversion.to_config(:READ) if self.read_conversion
config << self.write_conversion.to_config(:WRITE) if self.write_conversion
if self.limits
if self.limits.values
self.limits.values.each do |limits_set, limits_values|
config << " LIMITS #{limits_set} #{self.limits.persistence_setting} #{self.limits.enabled ? 'ENABLED' : 'DISABLED'} #{limits_values[0]} #{limits_values[1]} #{limits_values[2]} #{limits_values[3]}"
if limits_values[4] && limits_values[5]
config << " #{limits_values[4]} #{limits_values[5]}\n"
else
config << "\n"
end
end
end
config << self.limits.response.to_config if self.limits.response
end
if @meta
@meta.each do |key, values|
config << " META #{key.to_s.quote_if_necessary} #{values.map {|a| a.to_s.quote_if_necessary}.join(" ")}\n"
end
end
config
end
protected
def parameter_config
if @id_value
value = @id_value
config = " ID_PARAMETER "
else
value = @default
config = " PARAMETER "
end
config << "#{@name.to_s.quote_if_necessary} #{@bit_offset} #{@bit_size} #{@data_type} "
if @data_type == :BLOCK || @data_type == :STRING
unless value.is_printable?
val_string = "0x" + value.simple_formatted
else
val_string = "\"#{value}\""
end
else
first, last = calculate_range()
config << "#{first} #{last} "
val_string = value.to_s
end
config << "#{val_string} \"#{@description.to_s.gsub("\"", "'")}\""
end
# Convert a value into the given data type
def convert(value, data_type)
case data_type
when :INT, :UINT
Integer(value)
when :FLOAT
Float(value)
when :STRING, :BLOCK
value.to_s.freeze
end
rescue
raise ArgumentError, "#{@name}: Invalid value: #{value} for data type: #{data_type}"
end
end
end