Class: RubyBits::Structure

Inherits:
Object
  • Object
show all
Defined in:
lib/rubybits.rb

Overview

You can subclass RubyBits::Strcuture to define new binary formats. This can be used for lots of purposes: reading binary data, communicating in binary formats (like TCP/IP, http, etc).

Currently, three field types are supported: unsigned, signed and variable. Unsigned and signed fields are big-endian and can be any number of bits in size. Unsigned integers are assumed to be encoded with two’s complement. Variable fields are binary strings with their size defined by the value of another field (given by passing that field’s name to the :length option). This size is assumed to be in bits; if it is in fact in bytes, you should pass :byte to the :unit option (see the example).

Examples:

class NECProjectorFormat < RubyBits::Structure
  unsigned :id1,     8,    "Identification data assigned to each command"
  unsigned :id2,     8,    "Identification data assigned to each command"
  unsigned :p_id,    8,    "Projector ID"
  unsigned :m_code,  4,    "Model code for projector"
  unsigned :len,     12,   "Length of data in bytes"
  variable :data,    8,    "Packet data", :length => :len, :unit => :byte
  unsigned :checksum,8,    "Checksum"

  checksum :checksum do |bytes|
    bytes[0..-2].inject{|sum, byte| sum += byte} & 255
  end
 end

 NECProjectorFormat.parse(buffer)
 # => [[<NECProjectorFormat>, <NECProjectorFormat>], rest]

 NECProjectorFormat.new(:id1 => 0x44, :id2 => 2, :p_id => 0, :m_code => 0, :len => 5, :data => "hello").to_s.bytes.to_a
 # => [0x44, 0x2, 0x05, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x5F]

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(values = {}) ⇒ Structure

Creates a new instance of the class. You can pass in field names to initialize to set their values.

Examples:

MyStructure.new(:field1 => 44, :field2 => 0x70, :field3 => "hello")


221
222
223
224
225
226
# File 'lib/rubybits.rb', line 221

def initialize(values={})
	values.each{|key, value|
		self.send "#{key}=", value
	}
	@_checksum_cached = false
end

Class Method Details

.checksum(field) {|bytes| ... } ⇒ Object

Sets the checksum field. Setting a checksum field alters the functionality in several ways: the checksum is automatically calculated and set, and #parse will only consider a bitstring to be a valid instance of the structure if it has a checksum appropriate to its data.

Parameters:

  • field (Symbol)

    the field that contains the checksum data

Yields:

  • (bytes)

    block that should calculate the checksum given bytes, which is an array of bytes representing the full structure, with the checksum field set to 0



125
126
127
128
129
130
131
132
133
# File 'lib/rubybits.rb', line 125

def checksum field, &block
	@_checksum_field = [field, block]
	self.class_eval %{
		def #{field}
			calculate_checksum unless @_calculating_checksum || @_checksum_cached
			@__#{field}
		end
	}
end

.checksum_fieldObject

The checksum field



139
# File 'lib/rubybits.rb', line 139

def checksum_field; @_checksum_field; end

.fieldsObject

A list of the fields in the class



136
# File 'lib/rubybits.rb', line 136

def fields; @_fields; end

.from_string(string) ⇒ Array<Structure, string>

Parses a message from the binary string assuming that the message starts at the first byte of the string

Parameters:

  • string (String)

    a binary string to be interpreted

Returns:

  • (Array<Structure, string>)

    a pair with the first element being a structure object with the data from the input string (or nil if not a valid structure) and the second being the left-over bytes from the string (those after the message or the entire string if no valid message was found)



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/rubybits.rb', line 155

def from_string(string)
	message = self.new
	iter = 0
	checksum = nil
	fields.each{|field|
		kind, name, size, description, options = field
		options ||= {}
		size = (kind == :variable) ? message.send(options[:length]) : size
		size *= 8 if options[:unit] == :byte
		begin
			value = FIELD_TYPES[kind][:unpack].call(string, iter, size, options)
			message.send("#{name}=", value)
			checksum = value if checksum_field && name == checksum_field[0]
		rescue StopIteration, FieldValueException => e
			return [nil, string]
		end
		iter += size
	}
	# if there's a checksum, make sure the provided one is valid
	return [nil, string] unless message.checksum == checksum if checksum_field
	[message, string[((iter/8.0).ceil)..-1]]
end

.parse(string) ⇒ Array<Array<Structure>, String>

Parses out all of the messages in a given string assuming that the first message starts at the first byte, and there are no bytes between messages (though messages are not allowed to span bytes; i.e., all messages must be byte-aligned).

Parameters:

  • string (String)

    a binary string containing the messages to be parsed

Returns:

  • (Array<Array<Structure>, String>)

    a pair with the first element being an array of messages parsed out of the string and the second being whatever part of the string was left over after parsing.



185
186
187
188
189
190
191
192
193
194
# File 'lib/rubybits.rb', line 185

def parse(string)
	messages = []
	last_message = true
	while last_message
		last_message, string = from_string(string)
		#puts "Found message: #{last_message.to_s.bytes.to_a}, string=#{string.bytes.to_a.inspect}"
		messages << last_message if last_message
	end
	[messages, string]
end

.valid_message?(string) ⇒ Boolean

Determines whether a string is a valid message

Parameters:

  • string (String)

    a binary string to be tested

Returns:

  • (Boolean)

    whether the string is in fact a valid message



144
145
146
# File 'lib/rubybits.rb', line 144

def valid_message? string
	!!from_string(string)[0]
end

Instance Method Details

#calculate_checksumObject

Calculates and sets the checksum bit according to the checksum field defined by #checksum



239
240
241
242
243
244
245
246
247
248
# File 'lib/rubybits.rb', line 239

def calculate_checksum
	if self.class.checksum_field
		@_calculating_checksum = true
		self.send("#{self.class.checksum_field[0]}=", 0)
		checksum = self.class.checksum_field[1].call(self.to_s_without_checksum.bytes.to_a)
		self.send("#{self.class.checksum_field[0]}=", checksum)
		@_checksum_cached = true
		@_calculating_checksum = false
	end
end

#to_sString

Returns a binary string representation of the structure according to the fields defined and their current values.

Returns:

  • (String)

    bit string representing struct



231
232
233
234
235
236
# File 'lib/rubybits.rb', line 231

def to_s
	if self.class.checksum_field && !@_checksum_cached
		self.calculate_checksum
	end
	to_s_without_checksum
end