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