diff --git a/docs/example.py b/docs/example.py new file mode 100644 index 0000000..a49d3ee --- /dev/null +++ b/docs/example.py @@ -0,0 +1,10 @@ +from vbftool.vbf import Vbf +import bincopy + +bin = bincopy.BinFile() +bin.add_ihex_file('test.hex') + +vbf = Vbf(0x1000000, 0x3C0000, bin.as_binary()) + +with open('reference.vbf', 'wb') as fp: + vbf.dump(fp) diff --git a/tests/reference.vbf b/tests/reference.vbf index e8b512b..d542344 100644 Binary files a/tests/reference.vbf and b/tests/reference.vbf differ diff --git a/tests/test_options.py b/tests/test_options.py new file mode 100644 index 0000000..5192cd3 --- /dev/null +++ b/tests/test_options.py @@ -0,0 +1,91 @@ +from io import BytesIO + +import vbftool.options as opts +from vbftool import SwPartType, Network, FrameFormat + + +def assert_desc(desc, expected): + with BytesIO() as fp: + desc.dump(fp) + fp.seek(0) + assert fp.read() == expected + + +def test_description_single_line(): + desc = opts.Description('description') + assert_desc(desc, b'\t// Description\r\n\tdescription = { "description" };\r\n\r\n') + + +def test_description_multi_line(): + desc = opts.Description(['description1', 'description2']) + assert_desc(desc, b'\t// Description\r\n\tdescription = { "description1", "description2" };\r\n\r\n') + + +def test_sw_part_number_wers_or_eniva(): + desc = opts.SwPartNumber('WERS number') + assert_desc(desc, b'\t// Software part number\r\n\tsw_part_number = "WERS number";\r\n\r\n') + + +def test_sw_part_number_wers_and_eniva(): + desc = opts.SwPartNumber(['WERS number', "ENOVIA number"]) + assert_desc(desc, b'\t// Software part number\r\n\tsw_part_number = { "WERS number", "ENOVIA number" };\r\n\r\n') + + +def test_sw_part_number_DID(): + desc = opts.SwPartNumberDID(0xF188) + assert_desc(desc, b'\t// DID to read software part number\r\n\tsw_part_number_DID = 0xF188;\r\n\r\n') + + +def test_sw_part_type(): + desc = opts.SwPartType(SwPartType.EXE) + assert_desc(desc, b'\t// Software part type\r\n\tsw_part_type = EXE;\r\n\r\n') + + +def test_data_format_identifier_none(): + desc = opts.DataFormatIdentifier(0, 0) + assert_desc(desc, b'\t// Format identifier\r\n\tdata_format_identifier = 0x00;\r\n\r\n') + + +def test_data_format_identifier_compressed(): + desc = opts.DataFormatIdentifier(1, 0) + assert_desc(desc, b'\t// Format identifier\r\n\tdata_format_identifier = 0x10;\r\n\r\n') + + +def test_network_single(): + desc = opts.Network(Network.CAN_HS) + assert_desc(desc, b'\t// Network type or list\r\n\tnetwork = CAN_HS;\r\n\r\n') + + +def test_network_subnet(): + desc = opts.Network([Network.CAN_HS, Network.SUB_CAN1]) + assert_desc(desc, b'\t// Network type or list\r\n\tnetwork = { CAN_HS, SUB_CAN1 };\r\n\r\n') + + +def test_ecu_address_single(): + desc = opts.EcuAddress(0x723) + assert_desc(desc, b'\t// ECU address or list\r\n\tecu_address = 0x723;\r\n\r\n') + + +def test_ecu_address_subnet(): + desc = opts.EcuAddress([0x723, 0x740]) + assert_desc(desc, b'\t// ECU address or list\r\n\tecu_address = { 0x723, 0x740 };\r\n\r\n') + + +def test_frame_format(): + desc = opts.FrameFormat(FrameFormat.CAN_STANDARD) + assert_desc(desc, b'\t// Format frame\r\n\tframe_format = CAN_STANDARD;\r\n\r\n') + + +def test_erase_single_block(): + desc = opts.Erase([[0x10000, 0x20000]]) + assert_desc(desc, b'\t// Erase block\r\n\terase = { { 0x10000, 0x20000 } };\r\n\r\n') + + +def test_erase_multi_block(): + desc = opts.Erase([[0x10000, 0x20000], [0x50000, 0x1000]]) + assert_desc(desc, b'\t// Erase block\r\n\terase = { { 0x10000, 0x20000 }, { 0x50000, 0x1000 } };\r\n\r\n') + + +def test_call(): + desc = opts.Call(0x10000) + assert_desc(desc, b'\t// Call address\r\n\tcall = 0x10000;\r\n\r\n') diff --git a/tests/test_vbf.py b/tests/test_vbf.py index 3edb722..217e7d9 100644 --- a/tests/test_vbf.py +++ b/tests/test_vbf.py @@ -6,6 +6,11 @@ import vbftool.options as opts +def test_block(): + block = Vbf.create_data_block(0x1200, b'\x33\x22\x55\xAA\xBB\xCC\xDD\xEE\xFF') + assert block == b'\x00\x00\x12\x00\x00\x00\x00\x09\x33\x22\x55\xaa\xbb\xcc\xdd\xee\xff\xf5\x3f' + + def test_reference(): data = array('B', range(1, 255)) start_addr = 64 * 1024 # 64 KB @@ -16,17 +21,14 @@ def test_reference(): vbf.add_option(opts.SwPartNumber('318-08832-AB')) vbf.add_option(opts.SwPartNumberDID(0xF188)) vbf.add_option(opts.SwPartType(SwPartType.EXE)) - vbf.add_option(opts.DataFormatIdentifier(0x00)) + vbf.add_option(opts.DataFormatIdentifier(0, 0)) vbf.add_option(opts.Network(Network.CAN_HS)) vbf.add_option(opts.EcuAddress(0x723)) vbf.add_option(opts.FrameFormat(FrameFormat.CAN_STANDARD)) vbf.add_option(opts.Erase([(start_addr, length)])) vbf.add_option(opts.Call(start_addr)) - ref_filename = os.path.join( - os.path.dirname(os.path.realpath(__file__)), - 'reference.vbf', - ) + ref_filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'reference.vbf') with open(ref_filename, 'rb') as fp: reference = fp.read() diff --git a/vbftool/options.py b/vbftool/options.py index c9b6980..a32c939 100644 --- a/vbftool/options.py +++ b/vbftool/options.py @@ -1,5 +1,5 @@ from enum import Enum -from vbftool.vbf import writeline, newline +from vbftool.output import writeline, newline # Allowed identifiers: @@ -21,30 +21,31 @@ class Option: - def __init__(self, name, description, value): + def __init__(self, name, description, value, number_format='0x%x'): self._name = name self._desc = description self._value = value + self._number_format = number_format - def format_value(self, value): + def _format_value(self, value): if isinstance(value, Enum): value = value.value elif isinstance(value, int): - value = '0x%x' % value + value = self._number_format % value elif isinstance(value, str): value = '"%s"' % value elif isinstance(value, tuple): - x = [self.format_value(s) for s in value] + x = [self._format_value(s) for s in value] value = '{ %s }' % ', '.join(x) elif isinstance(value, list): - x = [self.format_value(s) for s in value] + x = [self._format_value(s) for s in value] value = '{ %s }' % ', '.join(x) return value def dump(self, fp): - writeline(fp, '\t//%s' % self._desc) + writeline(fp, '\t// %s' % self._desc) - value = self.format_value(self._value) + value = self._format_value(self._value) writeline(fp, '\t%s = %s;' % (self._name, value)) newline(fp) @@ -52,6 +53,8 @@ def dump(self, fp): class Description(Option): def __init__(self, value): + if not isinstance(value, list): + value = [value] super().__init__('description', 'Description', value) @@ -63,7 +66,7 @@ def __init__(self, value): class SwPartNumberDID(Option): def __init__(self, value): super().__init__('sw_part_number_DID', - 'DID to read software part number', value) + 'DID to read software part number', value, number_format='0x%04X') class SwPartType(Option): @@ -72,8 +75,9 @@ def __init__(self, value): class DataFormatIdentifier(Option): - def __init__(self, value): - super().__init__('data_format_identifier', 'Format identifier', value) + def __init__(self, compression_method, encryption_method): + super().__init__('data_format_identifier', 'Format identifier', compression_method << 4 | encryption_method, + number_format='0x%02x') class Network(Option): @@ -83,7 +87,7 @@ def __init__(self, value): class EcuAddress(Option): def __init__(self, value): - super().__init__('ecu_address', 'ecu_address or list', value) + super().__init__('ecu_address', 'ECU address or list', value) class FrameFormat(Option): @@ -93,9 +97,9 @@ def __init__(self, value): class Erase(Option): def __init__(self, value): - super().__init__('erase', 'erase block', value) + super().__init__('erase', 'Erase block', value) class Call(Option): def __init__(self, value): - super().__init__('call', 'call address', value) + super().__init__('call', 'Call address', value) diff --git a/vbftool/output.py b/vbftool/output.py new file mode 100644 index 0000000..b97f0a4 --- /dev/null +++ b/vbftool/output.py @@ -0,0 +1,6 @@ +def writeline(fp, s): + fp.write(b'%s\r\n' % s.encode('utf-8')) + + +def newline(fp): + fp.write(b'\r\n') \ No newline at end of file diff --git a/vbftool/vbf.py b/vbftool/vbf.py index 6808743..beeccda 100644 --- a/vbftool/vbf.py +++ b/vbftool/vbf.py @@ -2,14 +2,8 @@ from enum import Enum from vbftool.checksum import crc16, crc32 - - -def writeline(fp, s): - fp.write(b'%s\r\n' % s.encode('utf-8')) - - -def newline(fp): - fp.write(b'\r\n') +from vbftool.options import Option +from vbftool.output import writeline, newline class VbfVersion(Enum): @@ -54,6 +48,11 @@ class SwPartType(Enum): TEST = 'TEST' # Test program, (i.e. production test, diagnostics) +class _FileChecksum(Option): + def __init__(self, value): + super().__init__('file_checksum', 'file checksum', value, number_format='0x%08x') + + class Vbf: def __init__(self, version, start_addr, memory_size, data): self.version = version @@ -63,11 +62,8 @@ def __init__(self, version, start_addr, memory_size, data): self._options = [] def dump(self, fp): - data_checksum = crc16(self.data) - content = struct.pack('>II', self.start_addr, len(self.data)) - content += self.data - content += struct.pack('>H', data_checksum) - file_checksum = crc32(content) + content = self.create_data_block(self.start_addr, self.data) + file_checksum = _FileChecksum(crc32(content)) writeline(fp, 'vbf_version = %s;' % self.version.value) newline(fp) @@ -77,13 +73,19 @@ def dump(self, fp): for option in self._options: option.dump(fp) - writeline(fp, '\t//file checksum') - writeline(fp, '\t%s = 0x%08x;' % ('file_checksum', file_checksum)) - newline(fp) + file_checksum.dump(fp) fp.write(b'}') fp.write(content) + @staticmethod + def create_data_block(start_addr, data): + data_checksum = crc16(data) + content = struct.pack('>II', start_addr, len(data)) + content += data + content += struct.pack('>H', data_checksum) + return content + def add_option(self, option): self._options.append(option)