cosmos/lib/cosmos/packets/packet.rb
# 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 'digest'
require 'cosmos/packets/structure'
require 'cosmos/packets/packet_item'
require 'cosmos/ext/packet' if RUBY_ENGINE == 'ruby' and !ENV['COSMOS_NO_EXT']
module Cosmos
# Adds features common to all COSMOS packets of data to the Structure class.
# This includes the additional attributes listed below. The primary behavior
# Packet adds is the ability to apply formatting to PacketItem values as well
# as managing PacketItem's limit states.
class Packet < Structure
RESERVED_ITEM_NAMES = ['PACKET_TIMESECONDS'.freeze, 'PACKET_TIMEFORMATTED'.freeze, 'RECEIVED_TIMESECONDS'.freeze, 'RECEIVED_TIMEFORMATTED'.freeze, 'RECEIVED_COUNT'.freeze]
CATCH_ALL_STATE = 'ANY'
# @return [String] Name of the target this packet is associated with
attr_reader :target_name
# @return [String] Name of the packet
attr_reader :packet_name
# @return [String] Description of the packet
attr_reader :description
# @return [Time] Time at which the packet was received
attr_reader :received_time
# @return [Integer] Number of times the packet has been received
attr_reader :received_count
# @return [Boolean] Flag indicating if the packet is hazardous (typically for commands)
attr_accessor :hazardous
# @return [String] Description of why the packet is hazardous
attr_reader :hazardous_description
# Contains the values given by the user for a command (distinguished from defaults)
# These values should be used within command conversions if present because the order
# that values are written into the actual packet can vary
# @return [Hash<Item Name, Value>] Given values when constructing the packet
attr_reader :given_values
# @return [Boolean] Flag indicating if the packet is stale (hasn't been received recently)
attr_reader :stale
# @return [Boolean] Whether or not this is a 'raw' packet
attr_accessor :raw
# @return [Boolean] Whether or not this is a 'hidden' packet
attr_accessor :hidden
# @return [Boolean] Whether or not this is a 'disabled' packet
attr_accessor :disabled
# @return [Boolean] Whether or not messages should be printed for this packet
attr_accessor :messages_disabled
# @return [Boolean] Whether or not this is a 'abstract' packet
attr_accessor :abstract
# @return [Boolean] Whether or not this was a stored packet
attr_accessor :stored
# @return [Hash] Extra data to be logged/transferred with packet
attr_accessor :extra
# @return [Symbol] :CMD or :TLM
attr_accessor :cmd_or_tlm
# Valid format types
VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED, :WITH_UNITS]
if RUBY_ENGINE != 'ruby' or ENV['COSMOS_NO_EXT']
# Creates a new packet by initalizing the attributes.
#
# @param target_name [String] Name of the target this packet is associated with
# @param packet_name [String] Name of the packet
# @param default_endianness [Symbol] One of {BinaryAccessor::ENDIANNESS}
# @param description [String] Description of the packet
# @param buffer [String] String buffer to hold the packet data
# @param item_class [Class] Class used to instantiate items (Must be a
# subclass of PacketItem)
def initialize(target_name, packet_name, default_endianness = :BIG_ENDIAN, description = nil, buffer = nil, item_class = PacketItem)
super(default_endianness, buffer, item_class)
# Explictly call the defined setter methods
self.target_name = target_name
self.packet_name = packet_name
self.description = description
self.received_time = nil
self.received_count = 0
@id_items = nil
@hazardous = false
@hazardous_description = nil
@given_values = nil
@limits_items = nil
@processors = nil
@stale = true
@limits_change_callback = nil
@read_conversion_cache = nil
@raw = nil
@messages_disabled = false
@meta = nil
@hidden = false
@disabled = false
@stored = false
@extra = nil
@cmd_or_tlm = nil
end
# Sets the target name this packet is associated with. Unidentified packets
# will have target name set to nil.
#
# @param target_name [String] Name of the target this packet is associated with
def target_name=(target_name)
if target_name
if !(String === target_name)
raise(ArgumentError, "target_name must be a String but is a #{target_name.class}")
end
@target_name = target_name.upcase.freeze
else
@target_name = nil
end
@target_name
end
# Sets the packet name. Unidentified packets will have packet name set to
# nil.
#
# @param packet_name [String] Name of the packet
def packet_name=(packet_name)
if packet_name
if !(String === packet_name)
raise(ArgumentError, "packet_name must be a String but is a #{packet_name.class}")
end
@packet_name = packet_name.upcase.freeze
else
@packet_name = nil
end
@packet_name
end
# Sets the description of the packet
#
# @param description [String] Description of the packet
def description=(description)
if description
if !(String === description)
raise(ArgumentError, "description must be a String but is a #{description.class}")
end
@description = description.to_utf8.freeze
else
@description = nil
end
@description
end
# Sets the received time of the packet
#
# @param received_time [Time] Time this packet was received
def received_time=(received_time)
if received_time
if !(Time === received_time)
raise(ArgumentError, "received_time must be a Time but is a #{received_time.class}")
end
@received_time = received_time.clone.freeze
else
@received_time = nil
end
@read_conversion_cache.clear if @read_conversion_cache
@received_time
end
# Sets the received count of the packet
#
# @param received_count [Integer] Number of times this packet has been
# received
def received_count=(received_count)
if !(Integer === received_count)
raise(ArgumentError, "received_count must be an Integer but is a #{received_count.class}")
end
@received_count = received_count
@read_conversion_cache.clear if @read_conversion_cache
@received_count
end
end # if RUBY_ENGINE != 'ruby' or ENV['COSMOS_NO_EXT']
# Tries to identify if a buffer represents the currently defined packet. It
# does this by iterating over all the packet items that were created with
# an ID value and checking whether that ID value is present at the correct
# location in the buffer.
#
# Incorrectly sized buffers will still positively identify if there is
# enough data to match the ID values. This is to allow incorrectly sized
# packets to still be processed as well as possible given the incorrectly
# sized data.
#
# @param buffer [String] Raw buffer of binary data
# @return [Boolean] Whether or not the buffer of data is this packet
def identify?(buffer)
return false unless buffer
return true unless @id_items
@id_items.each do |item|
begin
value = read_item(item, :RAW, buffer)
rescue Exception
value = nil
end
return false if item.id_value != value
end
true
end
# Reads the values from a buffer at the position of each id_item defined
# in the packet.
#
# @param buffer [String] Raw buffer of binary data
# @return [Array] Array of read id values in order
def read_id_values(buffer)
return [] unless buffer
return [] unless @id_items
values = []
@id_items.each do |item|
values << read_item(item, :RAW, buffer)
rescue Exception
values << nil
end
values
end
# Returns @received_time unless a packet item called PACKET_TIME exists that returns
# a Ruby Time object that represents a different timestamp for the packet
def packet_time
item = @items['PACKET_TIME'.freeze]
if item
return read_item(item, :CONVERTED, @buffer)
else
return @received_time
end
end
# Calculates a unique hashing sum that changes if the parts of the packet configuration change that could affect
# the "shape" of the packet. This value is cached and that packet should not be changed if this method is being used
def config_name
return @config_name if @config_name
string = "#{@target_name} #{@packet_name}"
@sorted_items.each do |item|
string << " ITEM #{item.name} #{item.bit_offset} #{item.bit_size} #{item.data_type} #{item.array_size} #{item.endianness} #{item.overflow} #{item.states} #{item.read_conversion ? item.read_conversion.class : 'NO_CONVERSION'}"
end
# Use the hashing algorithm established by Cosmos::System
digest = Digest::SHA256.new
digest << string
@config_name = digest.hexdigest
@config_name
end
# (see Structure#buffer=)
def buffer=(buffer)
synchronize() do
begin
internal_buffer_equals(buffer)
rescue RuntimeError
Logger.instance.error "#{@target_name} #{@packet_name} received with actual packet length of #{buffer.length} but defined length of #{@defined_length}"
end
@read_conversion_cache.clear if @read_conversion_cache
process()
end
end
# Sets the received time of the packet (without cloning)
#
# @param received_time [Time] Time this packet was received
def set_received_time_fast(received_time)
@received_time = received_time
@received_time.freeze if @received_time
if @read_conversion_cache
synchronize() do
@read_conversion_cache.clear
end
end
end
# Sets the hazardous description of the packet
#
# @param hazardous_description [String] Hazardous description of the packet
def hazardous_description=(hazardous_description)
if hazardous_description
raise ArgumentError, "hazardous_description must be a String but is a #{hazardous_description.class}" unless String === hazardous_description
@hazardous_description = hazardous_description.to_utf8.freeze
else
@hazardous_description = nil
end
end
# Saves a hash of the values given by a user when constructing a command
#
# @param given_values [Hash<Item Name, Value>] Hash of given command parameters
def given_values=(given_values)
if given_values
raise ArgumentError, "given_values must be a Hash but is a #{given_values.class}" unless Hash === given_values
@given_values = given_values.clone
else
@given_values = nil
end
end
# Sets the callback object called when a limits state changes
#
# @param limits_change_callback [#call] Object must respond to the call
# method and take the following arguments: packet (Packet), item (PacketItem),
# old_limits_state (Symbol), item_value (Object), log_change (Boolean). The
# current item state can be found by querying the item object:
# item.limits.state.
def limits_change_callback=(limits_change_callback)
if limits_change_callback
raise ArgumentError, "limits_change_callback must respond to call" unless limits_change_callback.respond_to?(:call)
@limits_change_callback = limits_change_callback
else
@limits_change_callback = nil
end
end
# Review bit offset to look for overlapping definitions. This will allow
# gaps in the packet, but not allow the same bits to be used for multiple
# variables.
#
# @return [Array<String>] Warning messages for big definition overlaps
def check_bit_offsets
expected_next_offset = nil
previous_item = nil
warnings = []
@sorted_items.each do |item|
if expected_next_offset and (item.bit_offset < expected_next_offset) and !item.overlap
msg = "Bit definition overlap at bit offset #{item.bit_offset} for packet #{@target_name} #{@packet_name} items #{item.name} and #{previous_item.name}"
Logger.instance.warn(msg)
warnings << msg
end
expected_next_offset = Packet.next_bit_offset(item)
previous_item = item
end
warnings
end
# Checks if the packet has any gaps or overlapped items
#
# @return [Boolean] true if the packet has no gaps or overlapped items
def packed?
expected_next_offset = nil
@sorted_items.each do |item|
if expected_next_offset and item.bit_offset != expected_next_offset
return false
end
expected_next_offset = Packet.next_bit_offset(item)
end
true
end
# Returns the bit offset of the next item after the current item if items are packed
#
# @param item [PacketItem] The item to calculate the next offset for
# @return [Integer] Bit Offset of Next Item if Packed
def self.next_bit_offset(item)
if item.array_size
if item.array_size > 0
next_offset = item.bit_offset + item.array_size
else
next_offset = item.array_size
end
else
next_offset = nil
if item.bit_offset > 0
if item.little_endian_bit_field?
# Bit offset always refers to the most significant bit of a bitfield
bits_remaining_in_last_byte = 8 - (item.bit_offset % 8)
if item.bit_size > bits_remaining_in_last_byte
next_offset = item.bit_offset + bits_remaining_in_last_byte
end
end
end
unless next_offset
if item.bit_size > 0
next_offset = item.bit_offset + item.bit_size
else
next_offset = item.bit_size
end
end
end
next_offset
end
# Id items are used by the identify? method to determine if a raw buffer of
# data represents this packet.
# @return [Array<PacketItem>] Packet item identifiers
def id_items
@id_items ||= []
end
# @return [Array<PacketItem>] All items with defined limits
def limits_items
@limits_items ||= []
end
# @return [Hash] Hash of processors associated with this packet
def processors
@processors ||= {}
end
# Returns packet specific metadata
# @return [Hash<Meta Name, Meta Values>]
def meta
@meta ||= {}
end
# Sets packet specific metadata
def meta=(meta)
@meta = meta
end
# Indicates if the packet has been identified
# @return [TrueClass or FalseClass]
def identified?
!@target_name.nil? && !@packet_name.nil?
end
# Define an item in the packet. This creates a new instance of the
# item_class as given in the constructor and adds it to the items hash. It
# also resizes the buffer to accomodate the new item.
#
# @param name [String] Name of the item. Used by the items hash to retrieve
# the item.
# @param bit_offset [Integer] Bit offset of the item in the raw buffer
# @param bit_size [Integer] Bit size of the item in the raw buffer
# @param data_type [Symbol] Type of data contained by the item. This is
# dependant on the item_class but by default see StructureItem.
# @param array_size [Integer] Set to a non nil value if the item is to
# represented as an array.
# @param endianness [Symbol] Endianness of this item. By default the
# endianness as set in the constructure is used.
# @param overflow [Symbol] How to handle value overflows. This is
# dependant on the item_class but by default see StructureItem.
# @param format_string [String] String to pass to Kernel#sprintf
# @param read_conversion [Conversion] Conversion to apply when reading the
# item from the packet buffer
# @param write_conversion [Conversion] Conversion to apply before writing
# the item to the packet buffer
# @param id_value [Object] Set to something other than nil to indicate that
# this item should be used to identify a buffer as this packet. The
# id_value should make sense according to the data_type.
# @return [PacketItem] The new packet item
def define_item(name, bit_offset, bit_size, data_type, array_size = nil, endianness = @default_endianness, overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
item = super(name, bit_offset, bit_size, data_type, array_size, endianness, overflow)
packet_define_item(item, format_string, read_conversion, write_conversion, id_value)
end
# Add an item to the packet by adding it to the items hash. It also
# resizes the buffer to accomodate the new item.
#
# @param item [PacketItem] Item to add to the packet
# @return [PacketItem] The same packet item
def define(item)
item = super(item)
update_id_items(item)
update_limits_items_cache(item)
item
end
# Define an item at the end of the packet. This creates a new instance of the
# item_class as given in the constructor and adds it to the items hash. It
# also resizes the buffer to accomodate the new item.
#
# @param name (see #define_item)
# @param bit_size (see #define_item)
# @param data_type (see #define_item)
# @param array_size (see #define_item)
# @param endianness (see #define_item)
# @param overflow (see #define_item)
# @param format_string (see #define_item)
# @param read_conversion (see #define_item)
# @param write_conversion (see #define_item)
# @param id_value (see #define_item)
# @return (see #define_item)
def append_item(name, bit_size, data_type, array_size = nil, endianness = @default_endianness, overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
item = super(name, bit_size, data_type, array_size, endianness, overflow)
packet_define_item(item, format_string, read_conversion, write_conversion, id_value)
end
# (see Structure#get_item)
def get_item(name)
super(name)
rescue ArgumentError
raise "Packet item '#{@target_name} #{@packet_name} #{name.upcase}' does not exist"
end
# Read an item in the packet
#
# @param item [PacketItem] Instance of PacketItem or one of its subclasses
# @param value_type [Symbol] How to convert the item before returning it.
# Must be one of {VALUE_TYPES}
# @param buffer (see Structure#read_item)
# @return The value. :FORMATTED and :WITH_UNITS values are always returned
# as Strings. :RAW values will match their data_type. :CONVERTED values
# can be any type.
def read_item(item, value_type = :CONVERTED, buffer = @buffer)
value = super(item, :RAW, buffer)
derived_raw = false
if item.data_type == :DERIVED && value_type == :RAW
value_type = :CONVERTED
derived_raw = true
end
case value_type
when :RAW
# Done above
when :CONVERTED, :FORMATTED, :WITH_UNITS
if item.read_conversion
using_cached_value = false
check_cache = buffer.equal?(@buffer)
if check_cache and @read_conversion_cache
synchronize_allow_reads() do
if @read_conversion_cache[item]
value = @read_conversion_cache[item]
# Make sure cached value is not modified by anyone by creating
# a deep copy
if String === value
value = value.clone
elsif Array === value
value = Marshal.load(Marshal.dump(value))
end
using_cached_value = true
end
end
end
unless using_cached_value
if item.array_size
value.map! do |val, index|
item.read_conversion.call(val, self, buffer)
end
else
value = item.read_conversion.call(value, self, buffer)
end
if check_cache
synchronize_allow_reads() do
@read_conversion_cache ||= {}
@read_conversion_cache[item] = value
# Make sure cached value is not modified by anyone by creating
# a deep copy
if String === value
value = value.clone
elsif Array === value
value = Marshal.load(Marshal.dump(value))
end
end
end
end
end
# Derived raw values perform read_conversions but nothing else
return value if derived_raw
# Convert from value to state if possible
if item.states
if Array === value
value = value.map do |val, index|
if item.states.key(val)
item.states.key(val)
elsif item.states.values.include?(CATCH_ALL_STATE)
item.states.key(CATCH_ALL_STATE)
else
apply_format_string_and_units(item, val, value_type)
end
end
else
state_value = item.states.key(value)
if state_value
value = state_value
elsif item.states.values.include?(CATCH_ALL_STATE)
value = item.states.key(CATCH_ALL_STATE)
else
value = apply_format_string_and_units(item, value, value_type)
end
end
else
if Array === value
value = value.map do |val, index|
apply_format_string_and_units(item, val, value_type)
end
else
value = apply_format_string_and_units(item, value, value_type)
end
end
else
raise ArgumentError, "Unknown value type on read: #{value_type}"
end
return value
end
# Write an item in the packet
#
# @param item [PacketItem] Instance of PacketItem or one of its subclasses
# @param value (see Structure#write_item)
# @param value_type (see #read_item)
# @param buffer (see Structure#write_item)
def write_item(item, value, value_type = :CONVERTED, buffer = @buffer)
case value_type
when :RAW
super(item, value, value_type, buffer)
when :CONVERTED
if item.states
# Convert from state to value if possible
state_value = item.states[value.to_s.upcase]
value = state_value if state_value
end
if item.write_conversion
value = item.write_conversion.call(value, self, buffer)
else
raise "Cannot write DERIVED item #{item.name} without a write conversion" if item.data_type == :DERIVED
end
begin
super(item, value, :RAW, buffer) unless item.data_type == :DERIVED
rescue ArgumentError => err
if item.states and String === value and err.message =~ /invalid value for/
raise "Unknown state #{value} for #{item.name}"
else
raise err
end
end
when :FORMATTED, :WITH_UNITS
raise ArgumentError, "Invalid value type on write: #{value_type}"
else
raise ArgumentError, "Unknown value type on write: #{value_type}"
end
if @read_conversion_cache
synchronize() do
@read_conversion_cache.clear
end
end
end
# Read an item in the packet by name
#
# @param name [String] Name of the item to read
# @param value_type (see #read_item)
# @param buffer (see #read_item)
# @return (see #read_item)
def read(name, value_type = :CONVERTED, buffer = @buffer)
return super(name, value_type, buffer)
end
# Write an item in the packet by name
#
# @param name [String] Name of the item to write
# @param value (see #write_item)
# @param value_type (see #write_item)
# @param buffer (see #write_item)
def write(name, value, value_type = :CONVERTED, buffer = @buffer)
super(name, value, value_type, buffer)
end
# Read all items in the packet into an array of arrays
# [[item name, item value], ...]
#
# @param value_type (see #read_item)
# @param buffer (see Structure#read_all)
# @param top (See Structure#read_all)
# @return (see Structure#read_all)
def read_all(value_type = :CONVERTED, buffer = @buffer, top = true)
return super(value_type, buffer, top)
end
# Read all items in the packet into an array of arrays
# [[item name, item value], [item limits state], ...]
#
# @param value_type (see #read_all)
# @param buffer (see #read_all)
# @return [Array<String, Object, Symbol|nil>] Returns an Array consisting
# of [item name, item value, item limits state] where the item limits
# state can be one of {Cosmos::Limits::LIMITS_STATES}
def read_all_with_limits_states(value_type = :CONVERTED, buffer = @buffer)
result = nil
synchronize_allow_reads(true) do
result = read_all(value_type, buffer, false).map! do |array|
array << @items[array[0]].limits.state
end
end
return result
end
# Create a string that shows the name and value of each item in the packet
#
# @param value_type (see #read_item)
# @param indent (see Structure#formatted)
# @param buffer (see Structure#formatted)
# @param ignored (see Structure#ignored)
# @return (see Structure#formatted)
def formatted(value_type = :CONVERTED, indent = 0, buffer = @buffer, ignored = nil)
return super(value_type, indent, buffer, ignored)
end
# Restore all items in the packet to their default value
#
# @param buffer [String] Raw buffer of binary data
# @param skip_item_names [Array] Array of item names to skip
def restore_defaults(buffer = @buffer, skip_item_names = nil)
upcase_skip_item_names = skip_item_names.map(&:upcase) if skip_item_names
@sorted_items.each do |item|
next if RESERVED_ITEM_NAMES.include?(item.name)
write_item(item, item.default, :CONVERTED, buffer) unless skip_item_names and upcase_skip_item_names.include?(item.name)
end
end
# Define the reserved items on the current telemetry packet
def define_reserved_items
item = define_item('PACKET_TIMESECONDS', 0, 0, :DERIVED, nil, @default_endianness,
:ERROR, '%0.6f', PacketTimeSecondsConversion.new)
item.description = 'COSMOS Packet Time (UTC, Floating point, Unix epoch)'
item = define_item('PACKET_TIMEFORMATTED', 0, 0, :DERIVED, nil, @default_endianness,
:ERROR, nil, PacketTimeFormattedConversion.new)
item.description = 'COSMOS Packet Time (Local time zone, Formatted string)'
item = define_item('RECEIVED_TIMESECONDS', 0, 0, :DERIVED, nil, @default_endianness,
:ERROR, '%0.6f', ReceivedTimeSecondsConversion.new)
item.description = 'COSMOS Received Time (UTC, Floating point, Unix epoch)'
item = define_item('RECEIVED_TIMEFORMATTED', 0, 0, :DERIVED, nil, @default_endianness,
:ERROR, nil, ReceivedTimeFormattedConversion.new)
item.description = 'COSMOS Received Time (Local time zone, Formatted string)'
item = define_item('RECEIVED_COUNT', 0, 0, :DERIVED, nil, @default_endianness,
:ERROR, nil, ReceivedCountConversion.new)
item.description = 'COSMOS packet received count'
end
# Enable limits on an item by name
#
# @param name [String] Name of the item to enable limits
def enable_limits(name)
get_item(name).limits.enabled = true
end
# Disable limits on an item by name
#
# @param name [String] Name of the item to disable limits
def disable_limits(name)
item = get_item(name)
item.limits.enabled = false
unless item.limits.state == :STALE
old_limits_state = item.limits.state
item.limits.state = nil
@limits_change_callback.call(self, item, old_limits_state, nil, false) if @limits_change_callback
end
end
# Add an item to the limits items cache if necessary.
# You MUST call this after adding limits to an item
# This is an optimization so we don't have to iterate through all the items when
# checking for limits.
def update_limits_items_cache(item)
if item.limits.values || item.state_colors
@limits_items ||= []
@limits_items_hash ||= {}
unless @limits_items_hash[item]
@limits_items << item
@limits_items_hash[item] = true
end
end
end
# Return an array of arrays indicating all items in the packet that are out of limits
# [[target name, packet name, item name, item limits state], ...]
#
# @return [Array<Array<String, String, String, Symbol>>]
def out_of_limits
items = []
return items unless @limits_items
@limits_items.each do |item|
if item.limits.enabled && item.limits.state &&
PacketItemLimits::OUT_OF_LIMITS_STATES.include?(item.limits.state)
items << [@target_name, @packet_name, item.name, item.limits.state]
end
end
return items
end
# Set the limits state for all items to the given state
#
# @param state [Symbol] Must be one of PacketItemLimits::LIMITS_STATES
def set_all_limits_states(state)
@sorted_items.each { |item| item.limits.state = state }
end
# Check all the items in the packet against their defined limits. Update
# their internal limits state and persistence and call the
# limits_change_callback as necessary.
#
# @param limits_set [Symbol] Which limits set to check the item values
# against.
# @param ignore_persistence [Boolean] Whether to ignore persistence when
# checking for out of limits
def check_limits(limits_set = :DEFAULT, ignore_persistence = false)
# If check_limits is being called, then a new packet has arrived and
# this packet is no longer stale
# Stored telemetry doesn't affect the current value table and such doesn't affect stale
if @stale and !@stored
@stale = false
set_all_limits_states(nil)
end
return unless @limits_items
@limits_items.each do |item|
# Verify limits monitoring is enabled for this item
if item.limits.enabled
value = read_item(item)
# Handle state monitoring and value monitoring differently
if item.states
handle_limits_states(item, value)
elsif item.limits.values
handle_limits_values(item, value, limits_set, ignore_persistence)
end
end
end
end
# Sets the overall packet stale state to true and sets each packet item
# limits state to :STALE.
def set_stale
@stale = true
set_all_limits_states(:STALE)
end
# Reset temporary packet data
# This includes packet received time, received count, and processor state
def reset
# The SYSTEM META packet is a special case that does not get reset
return if @target_name == 'SYSTEM' && @packet_name == 'META'
@received_time = nil
@received_count = 0
@stored = false
@extra = nil
if @read_conversion_cache
synchronize() do
@read_conversion_cache.clear
end
end
return unless @processors
@processors.each do |processor_name, processor|
processor.reset
end
end
# Make a light weight clone of this packet. This only creates a new buffer
# of data and clones the processors. The defined packet items are the same.
#
# @return [Packet] A copy of the current packet with a new underlying
# buffer of data and processors
def clone
packet = super()
if packet.instance_variable_get("@processors".freeze)
packet.instance_variable_set("@processors".freeze, packet.processors.clone)
packet.processors.each do |processor_name, processor|
packet.processors[processor_name] = processor.clone
end
end
packet.instance_variable_set("@read_conversion_cache".freeze, nil)
packet.extra = JSON.parse(packet.extra.to_json) if packet.extra # Deep copy using JSON
packet
end
alias dup clone
def update_id_items(item)
if item.id_value
@id_items ||= []
# Add to Id Items
unless @id_items.empty?
last_item = @id_items[-1]
@id_items << item
# If the current item or last item have a negative offset then we have
# to re-sort. We also re-sort if the current item is less than the last
# item because we are inserting.
if last_item.bit_offset <= 0 or item.bit_offset <= 0 or item.bit_offset < last_item.bit_offset
@id_items = @id_items.sort
end
else
@id_items << item
end
end
item
end
def to_config(cmd_or_tlm)
config = ''
if cmd_or_tlm == :TELEMETRY
config << "TELEMETRY #{@target_name.to_s.quote_if_necessary} #{@packet_name.to_s.quote_if_necessary} #{@default_endianness} \"#{@description}\"\n"
else
config << "COMMAND #{@target_name.to_s.quote_if_necessary} #{@packet_name.to_s.quote_if_necessary} #{@default_endianness} \"#{@description}\"\n"
end
config << " ALLOW_SHORT\n" if @short_buffer_allowed
config << " HAZARDOUS #{@hazardous_description.to_s.quote_if_necessary}\n" if @hazardous
config << " DISABLE_MESSAGES\n" if @messages_disabled
if @disabled
config << " DISABLED\n"
elsif @hidden
config << " HIDDEN\n"
end
if @processors
@processors.each do |processor_name, processor|
config << processor.to_config
end
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
# Items with derived items last
@sorted_items.each do |item|
if item.data_type != :DERIVED
config << item.to_config(cmd_or_tlm, @default_endianness)
end
end
@sorted_items.each do |item|
if item.data_type == :DERIVED
unless RESERVED_ITEM_NAMES.include?(item.name)
config << item.to_config(cmd_or_tlm, @default_endianness)
end
end
end
config
end
def as_json
config = {}
config['target_name'] = @target_name.to_s
config['packet_name'] = @packet_name.to_s
config['endianness'] = @default_endianness.to_s
config['description'] = @description
config['short_buffer_allowed'] = true if @short_buffer_allowed
config['hazardous'] = true if @hazardous
config['hazardous_description'] = @hazardous_description.to_s if @hazardous_description
config['messages_disabled'] = true if @messages_disabled
config['disabled'] = true if @disabled
config['hidden'] = true if @hidden
config['stale'] = true if @stale
if @processors
processors = []
config['processors'] = processors
@processors.each do |processor_name, processor|
processors << processor.as_json
end
end
config['meta'] = @meta if @meta
items = []
config['items'] = items
# Items with derived items last
@sorted_items.each do |item|
if item.data_type != :DERIVED
items << item.as_json
end
end
@sorted_items.each do |item|
if item.data_type == :DERIVED
items << item.as_json
end
end
config
end
def self.from_json(hash)
endianness = hash['endianness'] ? hash['endianness'].intern : nil # Convert to symbol
packet = Packet.new(hash['target_name'], hash['packet_name'], endianness, hash['description'])
packet.short_buffer_allowed = hash['short_buffer_allowed']
packet.hazardous = hash['hazardous']
packet.hazardous_description = hash['hazardous_description']
packet.messages_disabled = hash['messages_disabled']
packet.disabled = hash['disabled']
packet.hidden = hash['hidden']
# packet.stale is read only
packet.meta = hash['meta']
# Can't convert processors
hash['items'].each do |item|
packet.define(PacketItem.from_json(item))
end
packet
end
protected
# Performs packet specific processing on the packet.
# Intended to only be run once for each packet received
def process(buffer = @buffer)
return unless @processors
@processors.each do |processor_name, processor|
processor.call(self, buffer)
end
end
def handle_limits_states(item, value)
# Retrieve limits state for the given value
limits_state = item.state_colors[value]
if item.limits.state != limits_state # PacketItemLimits state has changed
# Save old limits state
old_limits_state = item.limits.state
# Update to new limits state
item.limits.state = limits_state
if old_limits_state == nil # Changing from nil
if limits_state != :GREEN && limits_state != :BLUE # Warnings are needed
@limits_change_callback.call(self, item, old_limits_state, value, true) if @limits_change_callback
end
else # Changing from a state other than nil so always call the callback
if @limits_change_callback
if item.limits.state.nil?
@limits_change_callback.call(self, item, old_limits_state, value, false)
else
@limits_change_callback.call(self, item, old_limits_state, value, true)
end
end
end
end
end
def handle_limits_values(item, value, limits_set, ignore_persistence)
# Retrieve limits settings for the specified limits_set
limits = item.limits.values[limits_set]
# Use the default limits set if limits aren't specified for the
# particular limits set
limits = item.limits.values[:DEFAULT] unless limits
# Extract limits from array
red_low = limits[0]
yellow_low = limits[1]
yellow_high = limits[2]
red_high = limits[3]
green_low = limits[4]
green_high = limits[5]
limits_state = nil
# Determine the limits_state based on the limits values and the current
# value of the item
if value > yellow_low
if value < yellow_high
if green_low
if value < green_high
if value > green_low
limits_state = :BLUE
else
limits_state = :GREEN_LOW
end
else
limits_state = :GREEN_HIGH
end
else
limits_state = :GREEN
end
elsif value < red_high
limits_state = :YELLOW_HIGH
else
limits_state = :RED_HIGH
end
else # value <= yellow_low
if value > red_low
limits_state = :YELLOW_LOW
else
limits_state = :RED_LOW
end
end
if item.limits.state != limits_state # limits state has changed
# Save old limits state for use in the callback
old_limits_state = item.limits.state
item.limits.persistence_count += 1
# Check for item to achieve its persistence which means we
# have to update the state and call the callback
# Note when going back to green (or blue) persistence is ignored
if (item.limits.persistence_count >= item.limits.persistence_setting) || ignore_persistence
item.limits.state = limits_state
# Additional actions for limits change
@limits_change_callback.call(self, item, old_limits_state, value, true) if @limits_change_callback
# Clear persistence since we've entered a new state
item.limits.persistence_count = 0
end
else # limits state has not changed so clear persistence
item.limits.persistence_count = 0
end
end
def apply_format_string_and_units(item, value, value_type)
if value_type == :FORMATTED or value_type == :WITH_UNITS
if item.format_string && value
value = sprintf(item.format_string, value)
else
value = value.to_s
end
end
value << ' ' << item.units if value_type == :WITH_UNITS and item.units
value
end
def packet_define_item(item, format_string, read_conversion, write_conversion, id_value)
item.format_string = format_string
item.read_conversion = read_conversion
item.write_conversion = write_conversion
# Change id_value to the correct type
if id_value
item.id_value = id_value
update_id_items(item)
end
item
end
end
end