Skip to content

Commit 22350af

Browse files
committed
🆕 ASAM API for hexfiles
1 parent 6f84ac7 commit 22350af

11 files changed

Lines changed: 571 additions & 213 deletions

File tree

docs/README.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,55 @@ The folling types are supported:
323323

324324
In any case, endianess suffixes **_be** or **_le** are required.
325325

326+
For ASAM workflows there are dedicated helpers with explicit byte-order names:
327+
328+
.. code-block:: python
329+
330+
img0 = Image([Section(0x1000, bytes(64))])
331+
img0.write_asam_numeric(0x1000, 0x11223344, "ULONG", "MSB_FIRST")
332+
img0.write_asam_numeric(0x1004, 0x11223344, "ULONG", "MSB_FIRST_MSW_LAST")
333+
img0.write_asam_numeric(0x1008, 0x11223344, "ULONG", "MSB_LAST_MSW_FIRST")
334+
335+
print(hex(img0.read_asam_numeric(0x1000, "ULONG", "MSB_FIRST")))
336+
print(hex(img0.read_asam_numeric(0x1004, "ULONG", "MSB_FIRST_MSW_LAST")))
337+
print(hex(img0.read_asam_numeric(0x1008, "ULONG", "MSB_LAST_MSW_FIRST")))
338+
339+
# All reads print 0x11223344 again.
340+
341+
Supported ASAM byte orders:
342+
343+
* MSB_FIRST
344+
* MSB_LAST
345+
* MSB_FIRST_MSW_LAST (word-swap)
346+
* MSB_LAST_MSW_FIRST (word-swap)
347+
* LITTLE_ENDIAN (legacy alias for MSB_LAST)
348+
* BIG_ENDIAN (legacy alias for MSB_FIRST)
349+
350+
Supported ASAM numeric datatypes:
351+
352+
* UBYTE, SBYTE
353+
* UWORD, SWORD
354+
* ULONG, SLONG
355+
* A_UINT64, A_INT64
356+
* FLOAT16_IEEE, FLOAT32_IEEE, FLOAT64_IEEE
357+
358+
ASAM string helpers are available, too:
359+
360+
.. code-block:: python
361+
362+
img0.write_asam_string(0x1020, "MOTOR", "ASCII")
363+
img0.write_asam_string(0x1030, "Drehzahl", "UTF8")
364+
365+
print(img0.read_asam_string(0x1020, "ASCII"))
366+
print(img0.read_asam_string(0x1030, "UTF8"))
367+
368+
Supported ASAM string datatypes:
369+
370+
* ASCII
371+
* UTF8
372+
* UTF16
373+
* UTF32
374+
326375
Arrays are also supported.
327376

328377
.. code-block:: python

docs/howto.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ Read/write typed values at absolute addresses
5353
img.write_numeric_array(0x2004, [1, 2, 3, 4], "uint16_le")
5454
img.write_string(0x2010, "hello")
5555
56+
Read/write ASAM values (incl. word-swap byte orders)
57+
-----------------------------------------------------
58+
59+
.. code-block:: python
60+
61+
from objutils import Image, Section
62+
63+
img = Image([Section(0x3000, bytes(64))])
64+
65+
# ASAM numeric helpers
66+
img.write_asam_numeric(0x3000, 0x11223344, "ULONG", "MSB_FIRST")
67+
img.write_asam_numeric(0x3004, 0x11223344, "ULONG", "MSB_FIRST_MSW_LAST")
68+
img.write_asam_numeric(0x3008, 0x11223344, "ULONG", "MSB_LAST_MSW_FIRST")
69+
70+
value0 = img.read_asam_numeric(0x3000, "ULONG", "MSB_FIRST")
71+
value1 = img.read_asam_numeric(0x3004, "ULONG", "MSB_FIRST_MSW_LAST")
72+
value2 = img.read_asam_numeric(0x3008, "ULONG", "MSB_LAST_MSW_FIRST")
73+
74+
# ASAM string helpers
75+
img.write_asam_string(0x3010, "MOTOR", "ASCII")
76+
name = img.read_asam_string(0x3010, "ASCII")
77+
5678
Extract loadable image from ELF
5779
-------------------------------
5880

docs/tutorial.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,57 @@ Supported scalar types:
8686

8787
An endianness suffix (``_be`` or ``_le``) is required.
8888

89+
ASAM byte order and datatype helpers
90+
------------------------------------
91+
92+
For ECU/ASAM style type names and byte orders (including word-swap variants), use the dedicated ASAM helpers:
93+
94+
.. code-block:: python
95+
96+
from objutils import Image, Section
97+
98+
img = Image([Section(0x2000, bytes(64))])
99+
100+
# ASAM numerics
101+
img.write_asam_numeric(0x2000, 0x11223344, "ULONG", "MSB_FIRST")
102+
img.write_asam_numeric(0x2004, 0x11223344, "ULONG", "MSB_FIRST_MSW_LAST")
103+
img.write_asam_numeric(0x2008, 0x11223344, "ULONG", "MSB_LAST_MSW_FIRST")
104+
105+
# Roundtrip reads
106+
a = img.read_asam_numeric(0x2000, "ULONG", "MSB_FIRST")
107+
b = img.read_asam_numeric(0x2004, "ULONG", "MSB_FIRST_MSW_LAST")
108+
c = img.read_asam_numeric(0x2008, "ULONG", "MSB_LAST_MSW_FIRST")
109+
110+
# ASAM strings
111+
img.write_asam_string(0x2010, "MOTOR", "ASCII")
112+
img.write_asam_string(0x2020, "Drehzahl", "UTF8")
113+
s0 = img.read_asam_string(0x2010, "ASCII")
114+
s1 = img.read_asam_string(0x2020, "UTF8")
115+
116+
Supported ASAM byte orders:
117+
118+
- ``MSB_FIRST``
119+
- ``MSB_LAST``
120+
- ``MSB_FIRST_MSW_LAST``
121+
- ``MSB_LAST_MSW_FIRST``
122+
- ``LITTLE_ENDIAN`` (legacy alias for ``MSB_LAST``)
123+
- ``BIG_ENDIAN`` (legacy alias for ``MSB_FIRST``)
124+
125+
Supported ASAM numeric datatypes:
126+
127+
- ``UBYTE``, ``SBYTE``
128+
- ``UWORD``, ``SWORD``
129+
- ``ULONG``, ``SLONG``
130+
- ``A_UINT64``, ``A_INT64``
131+
- ``FLOAT16_IEEE``, ``FLOAT32_IEEE``, ``FLOAT64_IEEE``
132+
133+
Supported ASAM string datatypes:
134+
135+
- ``ASCII``
136+
- ``UTF8``
137+
- ``UTF16``
138+
- ``UTF32``
139+
89140
CLI companions
90141
--------------
91142

objutils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
The first parameter is always the codec name.
1414
"""
1515

16-
__version__ = "0.9.1"
16+
__version__ = "0.10.0"
1717

1818
__all__ = [
1919
"Image",

objutils/image.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,29 @@ def write_numeric(self, addr: int, value: Union[int, float], dtype: str, **kws:
617617
"""
618618
self._call_address_function("write_numeric", addr, value, dtype, **kws)
619619

620+
def read_asam_numeric(self, addr: int, dtype: str, byte_order: str = "MSB_LAST", **kws: Any) -> Union[int, float]:
621+
"""Read a numeric ASAM datatype with ECU byte order semantics."""
622+
return self._call_address_function("read_asam_numeric", addr, dtype, byte_order, **kws)
623+
624+
def write_asam_numeric(
625+
self,
626+
addr: int,
627+
value: Union[int, float],
628+
dtype: str,
629+
byte_order: str = "MSB_LAST",
630+
**kws: Any,
631+
) -> None:
632+
"""Write a numeric ASAM datatype with ECU byte order semantics."""
633+
self._call_address_function("write_asam_numeric", addr, value, dtype, byte_order, **kws)
634+
635+
def read_asam_string(self, addr: int, dtype: str, length: int = -1, **kws: Any) -> str:
636+
"""Read an ASAM string datatype (ASCII/UTF8/UTF16/UTF32)."""
637+
return self._call_address_function("read_asam_string", addr, dtype, length, **kws)
638+
639+
def write_asam_string(self, addr: int, value: str, dtype: str, **kws: Any) -> None:
640+
"""Write an ASAM string datatype (ASCII/UTF8/UTF16/UTF32)."""
641+
self._call_address_function("write_asam_string", addr, value, dtype, **kws)
642+
620643
def read_numeric_array(self, addr: int, length: int, dtype: str, **kws: Any) -> list[Union[int, float]]:
621644
"""Read array of numeric values from image.
622645

objutils/section.py

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@
157157
__copyright__ = """
158158
objutils - Object file library for Python.
159159
160-
(C) 2010-2025 by Christoph Schueler <github.com/Christoph2,
160+
(C) 2010-2026 by Christoph Schueler <github.com/Christoph2,
161161
cpu12.gems@googlemail.com>
162162
163163
All Rights Reserved
@@ -218,6 +218,7 @@
218218
"int32": "i",
219219
"uint64": "Q",
220220
"int64": "q",
221+
"float16": "e",
221222
"float32": "f",
222223
"float64": "d",
223224
}
@@ -231,6 +232,7 @@
231232
"int32": 4,
232233
"uint64": 8,
233234
"int64": 8,
235+
"float16": 2,
234236
"float32": 4,
235237
"float64": 8,
236238
}
@@ -241,13 +243,46 @@
241243
"be": ">",
242244
}
243245

246+
ASAM_BYTEORDER_ALIASES = {
247+
"LITTLE_ENDIAN": "MSB_LAST",
248+
"BIG_ENDIAN": "MSB_FIRST",
249+
}
250+
251+
ASAM_STRING_ENCODINGS = {
252+
"ASCII": "ascii",
253+
"UTF8": "utf-8",
254+
"UTF16": "utf-16",
255+
"UTF32": "utf-32",
256+
}
257+
258+
ASAM_NUMERIC_DTYPES = {
259+
"UBYTE": "uint8",
260+
"SBYTE": "int8",
261+
"UWORD": "uint16",
262+
"SWORD": "int16",
263+
"ULONG": "uint32",
264+
"SLONG": "int32",
265+
"A_UINT64": "uint64",
266+
"A_INT64": "int64",
267+
"FLOAT16_IEEE": "float16",
268+
"FLOAT32_IEEE": "float32",
269+
"FLOAT64_IEEE": "float64",
270+
}
271+
272+
ASAM_ENDIAN_FOR_BYTEORDER = {
273+
"MSB_FIRST": "be",
274+
"MSB_LAST": "le",
275+
"MSB_FIRST_MSW_LAST": "be",
276+
"MSB_LAST_MSW_FIRST": "le",
277+
}
278+
244279
TypeInformation = namedtuple("TypeInformation", "type byte_order size")
245280

246281
DTYPE = re.compile(
247282
r"""
248283
(?:(?P<uns>u)?int(?P<len>8 | 16 | 32 | 64)(?P<sep>[-/_:])(?P<end> be | le))
249284
| (?P<byte>byte)
250-
| (?P<flt>float)(?P<flen>32 | 64)""",
285+
| (?P<flt>float)(?P<flen>16 | 32 | 64)""",
251286
re.IGNORECASE | re.VERBOSE,
252287
)
253288

@@ -512,6 +547,50 @@ def _getformat(self, dtype: str, length: int = 1) -> str:
512547
else:
513548
return f"{BYTEORDER.get(bo)}{FORMATS.get(fmt)}"
514549

550+
def _resolve_asam_byteorder(self, byte_order: str) -> str:
551+
normalized = byte_order.strip().upper()
552+
normalized = ASAM_BYTEORDER_ALIASES.get(normalized, normalized)
553+
if normalized not in ASAM_ENDIAN_FOR_BYTEORDER:
554+
raise ValueError(f"Unsupported ASAM byte order {byte_order!r}")
555+
return normalized
556+
557+
def _permute_asam_bytes_for_read(self, data: bytes, byte_order: str) -> bytes:
558+
size = len(data)
559+
if size <= 1:
560+
return data
561+
if byte_order == "MSB_FIRST_MSW_LAST":
562+
if size % 2 != 0:
563+
raise ValueError("MSB_FIRST_MSW_LAST requires even-sized numeric types")
564+
return b"".join(data[idx + 1 : idx + 2] + data[idx : idx + 1] for idx in range(0, size, 2))
565+
if byte_order == "MSB_LAST_MSW_FIRST":
566+
if size % 2 != 0:
567+
raise ValueError("MSB_LAST_MSW_FIRST requires even-sized numeric types")
568+
return b"".join(data[idx + 1 : idx + 2] + data[idx : idx + 1] for idx in range(0, size, 2))
569+
return data
570+
571+
def _permute_asam_bytes_for_write(self, data: bytes, byte_order: str) -> bytes:
572+
size = len(data)
573+
if size <= 1:
574+
return data
575+
if byte_order == "MSB_FIRST_MSW_LAST":
576+
if size % 2 != 0:
577+
raise ValueError("MSB_FIRST_MSW_LAST requires even-sized numeric types")
578+
return b"".join(data[idx + 1 : idx + 2] + data[idx : idx + 1] for idx in range(0, size, 2))
579+
if byte_order == "MSB_LAST_MSW_FIRST":
580+
if size % 2 != 0:
581+
raise ValueError("MSB_LAST_MSW_FIRST requires even-sized numeric types")
582+
return b"".join(data[idx + 1 : idx + 2] + data[idx : idx + 1] for idx in range(0, size, 2))
583+
return data
584+
585+
def _asam_numeric_dtype_to_internal(self, asam_dtype: str, asam_byte_order: str) -> str:
586+
normalized_dtype = asam_dtype.strip().upper()
587+
internal_dtype = ASAM_NUMERIC_DTYPES.get(normalized_dtype)
588+
if internal_dtype is None:
589+
raise TypeError(f"Unsupported ASAM datatype {asam_dtype!r}")
590+
if internal_dtype in ("uint8", "int8"):
591+
return internal_dtype
592+
return f"{internal_dtype}_{ASAM_ENDIAN_FOR_BYTEORDER[asam_byte_order]}"
593+
515594
def read(self, addr: int, length: int, **kws) -> bytes:
516595
"""Read raw bytes from section at specified address.
517596
@@ -675,6 +754,72 @@ def read_numeric_array(self, addr: int, length: int, dtype: str, **kws) -> Union
675754
data = self.data[offset : offset + data_size]
676755
return struct.unpack(fmt, data)
677756

757+
def read_asam_numeric(self, addr: int, dtype: str, byte_order: str = "MSB_LAST", **kws) -> Union[int, float]:
758+
asam_byte_order = self._resolve_asam_byteorder(byte_order)
759+
internal_dtype = self._asam_numeric_dtype_to_internal(dtype, asam_byte_order)
760+
value = self.read_numeric(addr, internal_dtype, **kws)
761+
if TYPE_SIZES.get(internal_dtype.split("_")[0], 0) <= 1:
762+
return value
763+
764+
fmt = self._getformat(internal_dtype)
765+
packed = struct.pack(fmt, value)
766+
permuted = self._permute_asam_bytes_for_read(packed, asam_byte_order)
767+
return struct.unpack(fmt, permuted)[0]
768+
769+
def write_asam_numeric(
770+
self,
771+
addr: int,
772+
value: Union[int, float],
773+
dtype: str,
774+
byte_order: str = "MSB_LAST",
775+
**kws,
776+
) -> None:
777+
asam_byte_order = self._resolve_asam_byteorder(byte_order)
778+
internal_dtype = self._asam_numeric_dtype_to_internal(dtype, asam_byte_order)
779+
if TYPE_SIZES.get(internal_dtype.split("_")[0], 0) <= 1:
780+
self.write_numeric(addr, value, internal_dtype, **kws)
781+
return
782+
783+
fmt = self._getformat(internal_dtype)
784+
packed = struct.pack(fmt, value)
785+
permuted = self._permute_asam_bytes_for_write(packed, asam_byte_order)
786+
self.write(addr, permuted)
787+
788+
def read_asam_string(self, addr: int, dtype: str, length: int = -1, **kws) -> str:
789+
encoding = ASAM_STRING_ENCODINGS.get(dtype.strip().upper())
790+
if encoding is None:
791+
raise TypeError(f"Unsupported ASAM string datatype {dtype!r}")
792+
if length == -1 and encoding in ("utf-8", "utf-16", "utf-32"):
793+
offset = addr - self.start_address
794+
if offset < 0:
795+
raise InvalidAddressError(f"read_asam_string(0x{addr:08x}) access out of bounds.")
796+
tail = self.data[offset:]
797+
terminator = "\x00".encode(encoding=encoding)
798+
pos = tail.find(terminator)
799+
if pos != -1:
800+
return tail[:pos].decode(encoding=encoding)
801+
raise TypeError("Unterminated String!!!")
802+
return self.read_string(addr, encoding=encoding, length=length, **kws)
803+
804+
def write_asam_string(self, addr: int, value: str, dtype: str, **kws) -> None:
805+
encoding = ASAM_STRING_ENCODINGS.get(dtype.strip().upper())
806+
if encoding is None:
807+
raise TypeError(f"Unsupported ASAM string datatype {dtype!r}")
808+
if encoding == "ascii":
809+
self.write_string(addr, value, encoding=encoding, **kws)
810+
return
811+
812+
offset = addr - self.start_address
813+
if offset < 0:
814+
raise InvalidAddressError(f"write_asam_string(0x{addr:08x}) access out of bounds.")
815+
encoded = value.encode(encoding=encoding)
816+
terminator = "\x00".encode(encoding=encoding)
817+
total_length = len(encoded) + len(terminator)
818+
if offset + total_length > self.length:
819+
raise InvalidAddressError(f"write_asam_string(0x{addr:08x}) access out of bounds.")
820+
self.data[offset : offset + len(encoded)] = encoded
821+
self.data[offset + len(encoded) : offset + total_length] = terminator
822+
678823
def write_numeric_array(self, addr: int, data: Union[list[int], list[float]], dtype: str, **kws) -> None:
679824
if not hasattr(data, "__iter__"):
680825
raise TypeError("data must be iterable")

objutils/tests/test_image.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,5 +1330,18 @@ def test_write_uint8_array_boundary_case3_1():
13301330
img.write_numeric_array(0x0FFF, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "uint8_be")
13311331

13321332

1333+
def test_image_write_read_asam_numeric_word_swap_32bit():
1334+
img = Image(Section(data=bytearray(16), start_address=0x1000))
1335+
img.write_asam_numeric(0x1000, 0x11223344, "ULONG", byte_order="MSB_LAST_MSW_FIRST")
1336+
assert img.read(0x1000, 4) == b"\x33\x44\x11\x22"
1337+
assert img.read_asam_numeric(0x1000, "ULONG", byte_order="MSB_LAST_MSW_FIRST") == 0x11223344
1338+
1339+
1340+
def test_image_asam_string_ascii_roundtrip():
1341+
img = Image(Section(data=bytearray(16), start_address=0x1000))
1342+
img.write_asam_string(0x1000, "ABC", "ASCII")
1343+
assert img.read_asam_string(0x1000, "ASCII") == "ABC"
1344+
1345+
13331346
if __name__ == "__main__":
13341347
unittest.main()

0 commit comments

Comments
 (0)