Created
November 25, 2009 13:44
-
-
Save elia/242708 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# 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 <tt>String#unpack</tt> 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 <tt>String#unpack</tt> 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 <tt>String#unpack</tt> 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 | |
Rails.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 <em>bit field</em> 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;", __FILE__, __LINE__ | |
Rails.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 | |
Rails.logger.debug "Unpacking #{@raw.inspect} with #{unpack_recipe.inspect}" | |
@unpacked = @raw.unpack(unpack_recipe) | |
@attributes ||= {} | |
Rails.logger.debug { "Parsing #{@raw.inspect} with fields #{self.class.fields.inspect}" } | |
self.class.fields.each_with_index do |name, position| | |
Rails.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] | |
Rails.logger.debug { "Parsing value #{attributes[name]} with bit fields #{bit_definitions.inspect}" } | |
bit_value = attributes[name] | |
bit_definitions.reverse.each do |bit_field| | |
Rails.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) | |
Rails.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 method_missing name, *args | |
if @raw.respond_to? name | |
@raw.send name, *args | |
else | |
super | |
end | |
end | |
end | |
extend InstanceMethods | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require File.dirname(__FILE__) + '/../spec_helper' | |
describe BitFields do | |
before :each do | |
@klass = Class.new | |
@klass.class_eval{ | |
extend BitFields | |
field :char_value | |
field :short_value, 'S' | |
field :frame_data_field_status, 'S' 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 | |
} | |
@values = [23, 14, 0b10100111_11111111] | |
@bit_string = @values.pack('CSS') | |
@object = @klass.new @bit_string | |
end | |
it 'should create methods in a class' do | |
@object.char_value.should == 23 | |
@object.secondary_header_flag.should == 0b1 | |
@object.sync_flag.should == 0b0 | |
@object.first_header_pointer.should == 0b111_11111111 | |
end | |
it 'should act as a Hash' do | |
@object.attributes[:char_value].should == 23 | |
@object.attributes[:secondary_header_flag].should == 0b1 | |
@object.attributes[:sync_flag].should == 0b0 | |
@object.attributes[:first_header_pointer].should == 0b111_11111111 | |
@object[:char_value].should == 23 | |
@object[:secondary_header_flag].should == 0b1 | |
@object[:sync_flag].should == 0b0 | |
@object[:first_header_pointer].should == 0b111_11111111 | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment