require 'world_logger'
#
# BitFields provides a simple way to extract a values from bit fields,
# especially if they don't correspond to standard sizes (such as +char+, +int+,
# +long+, etc., see String#unpack for further informations).
#
# For example the Primary Header of the Telemetry Frame in the ESA PSS
# Standard has this specification:
#
#
# | TRANSFER FRAME PRIMARY HEADER |
# ___________|_____________________________________________________________________________________|
# |SYNC MARKER| FRAME IDENTIFICATION | MASTER | VIRTUAL | FRAME DATA FIELD STATUS |
# | |----------------------| CHANNEL | CHANNEL |------------------------------------------|
# | |Ver. |S/C.|Virt.|Op.Co| FRAME | FRAME |2nd head.|Sync|Pkt ord|Seg. |First Header|
# | | no. | ID | Chn.| Flag| COUNT | COUNT | flag |flag|flag |len.ID| Pointer |
# | |_____|____|_____|_____| | |_________|____|_______|______|____________|
# | | 2 | 10 | 3 | 1 | | | 1 | 1 | 1 | 2 | 11 |
# |-----------|----------------------|---------|---------|------------------------------------------|
# | 32 | 16 | 8 | 8 | 16 |
#
# Will become:
#
# class PrimaryHeader
# include BitFields
# field :frame_identification, 'n' do
# bit_field :version, 2
# bit_field :spacecraft_id, 10
# bit_field :virtual_channel, 3
# bit_field :op_control_field_flag, 2
# end
#
# field :master_channel_frame_count
# field :virtual_channel_frame_count
#
# field :frame_data_field_status, 'n' do
# bit_field :secondary_header_flag, 1
# bit_field :sync_flag, 1
# bit_field :packet_order_flag, 1
# bit_field :segment_length_id, 2
# bit_field :first_header_pointer, 11
# end
# end
#
#
# And can be used like:
#
# packed_ph = [0b10100111_11111111, 11, 23, 0b10100111_11111111].pack('nCCn') # => "\247\377\v\027\247\377"
#
# ph = PrimaryHeader.new packed_ph
#
# ph.virtual_channel_frame_count # => 23
# ph.secondary_header_flag # => 0b1
# ph.sync_flag # => 0b0
# ph.first_header_pointer # => 0b111_11111111
#
# ph[:first_header_pointer] # => 0b111_11111111
#
#
#
module BitFields
# Collects the fields definitions for later parsing
attr_reader :fields
# Collects the bit_fields definitions for later parsing
attr_reader :bit_fields
# Collects the full String#unpack directive used to parse the raw value.
attr_reader :unpack_recipe
##
# Defines a field to be extracted with String#unpack from the raw value
#
# +name+ :: the name of the field (that will be used to access it)
# +unpack_recipe+ :: the String#unpack directive corresponding to this field (optional, defaults to char: "C")
# +bit_fields_definitions_block+ :: the block in which +bit_fields+ can be defined (optional)
#
# Also defines the attribute reader method
#
def field name, unpack_recipe = 'C', &bit_fields_definitions_block
include InstanceMethods # when used we include instance methods
logger.debug { self.ancestors.inspect }
# Setup class "instance" vars
@fields ||= []
@bit_fields ||= {}
@unpack_recipe ||= ""
# Register the field definition
@unpack_recipe << unpack_recipe
@fields << name
# Define the attribute reader
class_eval "def #{name}; self.attributes[#{name.inspect}]; end;", __FILE__, __LINE__
# define_method(name) { self.fields[name] }
# There's a bit-structure too?
if block_given?
@_current_bit_fields = []
bit_fields_definitions_block.call
@bit_fields[name] = @_current_bit_fields
@_current_bit_fields = nil
end
end
##
# Defines a bit field to be extracted from a +field+
#
# +name+ :: the name of the bit field (that will be used to access it)
# +width+ :: the number of bits from which this value should be extracted
#
def bit_field name, width
raise "'bit_field' can be used only inside a 'field' block." if @_current_bit_fields.nil?
# Register the bit field definition
@_current_bit_fields << [name, width]
# Define the attribute reader
class_eval "def #{name}; self.attributes[#{name.inspect}]; end\n", __FILE__, __LINE__
if width == 1 or name.to_s =~ /_flag$/
# Define a question mark method if the size is 1 bit
class_eval "def #{name}?; self.attributes[#{name.inspect}] != 0; end\n", __FILE__, __LINE__
# returns nil if no substitution happens...
if flag_method_name = name.to_s.gsub!(/_flag$/, '?')
# Define another question mark method if ends with "_flag"
class_eval "alias #{flag_method_name} #{name}?\n", __FILE__, __LINE__
end
end
logger.debug { @_current_bit_fields.inspect }
end
module InstanceMethods
# Contains the raw string
attr_reader :raw
# caches the bit field values
attr_reader :attributes
# caches the bin string unpacked values
attr_reader :unpacked
# Takes the raw binary string and parses it
def initialize bit_string
parse_bit_fields(bit_string.dup.freeze)
end
# Makes defined fields accessible like a +Hash+
def [](name)
self.attributes[name]
end
private
def eat_right_bits original_value, bits_number
# Filter the original value with the
# proper bitmask to get the rightmost bits
new_value = original_value & bit_mask(bits_number)
# Eat those rightmost bits
# wich we have just consumed
remaning = original_value >> bits_number
# Return also the remaning bits
return new_value, remaning
end
# Parses the raw value extracting the defined bit fields
def parse_bit_fields raw
@raw = raw
# Setup
unpack_recipe = self.class.unpack_recipe
logger.debug "Unpacking #{@raw.inspect} with #{unpack_recipe.inspect}"
@unpacked = @raw.unpack(unpack_recipe)
@attributes ||= {}
logger.debug { "Parsing #{@raw.inspect} with fields #{self.class.fields.inspect}" }
self.class.fields.each_with_index do |name, position|
logger.debug { "Parsing field #{name.inspect}" }
attributes[name] = @unpacked[position]
# We must extract bits from end since
# ruby doesn't have types (and fixed lengths)
if bit_definitions = self.class.bit_fields[name]
logger.debug { "Parsing value #{attributes[name]} with bit fields #{bit_definitions.inspect}" }
bit_value = attributes[name]
bit_definitions.reverse.each do |bit_field|
logger.debug "Parsing bit field: #{bit_field.inspect} current value: #{bit_value} (#{bit_value.to_s 2})"
bit_name, bit_size = *bit_field
attributes[bit_name], bit_value = eat_right_bits(bit_value, bit_size)
logger.debug {
"#{bit_name}: #{attributes[bit_name]} 0b#{attributes[bit_name].to_s(2).rjust(16, '0')}"
}
end
end
end
@parsed = true
end
def bit_mask size
2 ** size - 1
end
def to_s
raw.to_s
end
def method_missing name, *args
if @raw.respond_to? name
@raw.send name, *args
else
super
end
end
end
extend InstanceMethods
end