require 'world_logger'
require 'ruby19' # for string ecoding compatibility
#
# 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 accessors for the attributes hash
def bits_attr_accessor name
class_eval "def #{name}; self.attributes[#{name.inspect}]; end;", __FILE__, __LINE__
class_eval "def #{name}=(val); self.attributes[#{name.inspect}]=val; end;", __FILE__, __LINE__
end
##
# 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
#
# TODO: add a skip bits syntax
#
def field name, unpack_recipe = 'C', &bit_fields_definitions_block
include InstanceMethods # when used we include instance methods
# Setup class "instance" vars
@fields ||= []
@bit_fields ||= {}
@unpack_recipe ||= ""
# Register the field definition
@unpack_recipe << unpack_recipe
@fields << name
# Define the attribute accessor
bits_attr_accessor(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
#
# TODO: add options to enable method aliases
# TODO: add a skip bits syntax
#
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, bit_mask(width)]
# Define the attribute accessor
bits_attr_accessor(name)
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
end
def bit_mask size
2 ** size - 1
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
class ValueOverflow < StandardError
end
# Takes the raw binary string and parses it
def initialize bit_string_or_hash
if bit_string_or_hash.kind_of?(Hash)
@attributes = bit_string_or_hash
pack_bit_fields
else
parse_bit_fields(bit_string_or_hash) #.dup.freeze REMOVED: seems useless
end
end
# Makes defined fields accessible like a +Hash+
def [](name)
self.attributes[name]
end
private
# Parses the raw value extracting the defined bit fields
def parse_bit_fields raw
@raw = raw
# Setup
@unpacked = @raw.unpack( self.class.unpack_recipe )
@attributes ||= {}
self.class.fields.each_with_index do |name, position|
@attributes[name] = @unpacked[position]
if bit_fields = self.class.bit_fields[name]
bit_value = attributes[name]
# We must extract bits from end since
# ruby doesn't have types (and fixed lengths)
bit_fields.reverse.each do |(bit_name, bits_number, bit_mask)|
@attributes[bit_name] = bit_value & bit_mask
bit_value = bit_value >> bits_number
end
end
end
end
public
# Parses the raw value extracting the defined bit fields
def pack_bit_fields
@unpacked = []
self.class.fields.each_with_index do |name, position|
if bit_fields = self.class.bit_fields[name]
bit_value = 0
bit_fields.each do |(bit_name, bits_number, bit_mask)|
masked = @attributes[bit_name] & bit_mask
raise ValueOverflow,
"the value #{@attributes[bit_name]} "+
"is too big for #{bits_number} bits" if masked != @attributes[bit_name]
bit_value = bit_value << bits_number
bit_value |= masked
end
# Value of fields composed by binary fields is always overwritten
# by the composition of the latter
attributes[name] = bit_value
end
@unpacked[position] = @attributes[name] || 0
end
@raw = @unpacked.pack( self.class.unpack_recipe )
end
alias pack pack_bit_fields
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