Examples

These are the Python programs from the repository’s python/examples/ directory. They run against real adapters and ECUs. Select a suitable adapter and ECU address for your setup before running them.

Send a CAN frame

Discover an adapter and send one Classic CAN frame.

"""Send one Classic CAN frame through the first available adapter."""

import asyncio

from automotive.adapters import CanAdapter
from automotive.can import Frame


async def main() -> None:
    """Discover an adapter and send a frame."""
    adapter = CanAdapter()
    frame = Frame(id=0x123, data=b"\x01\x02\x03\x04")
    await adapter.send(frame)
    print("Sent", frame)


if __name__ == "__main__":
    asyncio.run(main())

Send ISO-TP messages

Follow the Rust example’s four explicit exchanges: two short requests, a 33-byte segmented request, and another DID request that can produce a long reply.

"""Exchange short and segmented PDUs with an ECU at 0x7A1/0x7A9."""

import asyncio

from automotive.adapters import CanAdapter
from automotive.transports.isotp import IsoTP, IsoTPConfig


async def main() -> None:
    """Send short and segmented requests, printing each response as hex."""
    adapter = CanAdapter()
    config = IsoTPConfig(tx_id=0x7A1)
    isotp = IsoTP(adapter, config)

    stream = isotp.recv()

    await isotp.send(b"\x3e\x00")
    response = await anext(stream)
    print("RX:", response.hex())

    await isotp.send(b"\x22\xf1\x81")
    response = await anext(stream)
    print("RX:", response.hex())

    long_request = b"\x10" + b"\x00" * 32
    await isotp.send(long_request)
    response = await anext(stream)
    print("RX:", response.hex())

    long_response = b"\x22\xf1\x81"
    await isotp.send(long_response)
    response = await anext(stream)
    print("RX:", response.hex())


if __name__ == "__main__":
    asyncio.run(main())

Query an ECU with UDS

Read diagnostic data through a persistent UDS client.

"""Read software identification from a diagnostic ECU at transmit ID 0x7A1."""

import asyncio

from automotive.adapters import CanAdapter
from automotive.protocols.uds import DataIdentifier, SessionType, UDSClient
from automotive.transports.isotp import IsoTP


async def main() -> None:
    """Use defaults, select a session, and read an ECU data identifier."""
    adapter = CanAdapter()
    isotp = IsoTP.from_id(adapter, 0x7A1)
    uds = UDSClient(isotp)
    await uds.tester_present()
    await uds.diagnostic_session_control(SessionType.EXTENDED_DIAGNOSTIC)

    did = DataIdentifier.APPLICATION_SOFTWARE_IDENTIFICATION
    resp = await uds.read_data_by_identifier(did)

    print(f"{did.name}: {resp!r}")


if __name__ == "__main__":
    asyncio.run(main())

Configure bitrate and sample points

Configure a PEAK PCAN-USB FD adapter with 500 kbit/s nominal and 2 Mbit/s data timing, both with an 80% sample point.

"""Configure a PEAK PCAN-USB FD adapter for 500 kbit/s CAN and 2 Mbit/s CAN FD."""

import asyncio

from automotive.adapters import PeakAdapter, PeakConfig
from automotive.can.bitrate import BitrateSettings


async def main() -> None:
    """Configure nominal and data-phase timing, then print received frames."""
    config = PeakConfig(
        bitrate=BitrateSettings(
            bitrate=500_000,
            sample_point=0.8,
            data_bitrate=2_000_000,
            data_sample_point=0.8,
        ),
    )
    print("Resolved timing:", config.resolved_bitrate)
    adapter = PeakAdapter(config)
    stream = adapter.recv()

    async for frame in stream:
        print(frame)


if __name__ == "__main__":
    asyncio.run(main())

Query firmware versions concurrently

Use independent ECU transports for concurrent diagnostic requests.

"""Query named DIDs across standard and extended ECU addresses."""

import asyncio

from automotive.adapters import CanAdapter
from automotive.errors import AutomotiveError
from automotive.protocols.uds import DataIdentifier, UDSClient, UDSConfig
from automotive.transports.isotp import IsoTP, IsoTPConfig


async def get_version(adapter: CanAdapter, identifier: int) -> None:
    """Query each named DID for one ECU, retaining its configured transport."""
    # Increased timeout for adapters not supporting real ACKs.
    config = IsoTPConfig(tx_id=identifier, timeout=1.0)
    isotp = IsoTP(adapter, config)
    uds = UDSClient(isotp, UDSConfig(p2_timeout=1.0))

    await uds.tester_present()

    for did in DataIdentifier:
        try:
            resp = await uds.read_data_by_identifier(did)
        except (AutomotiveError, OSError):
            continue
        print(f"{identifier:x} 0x{did.value:x} {did.name}: {resp!r}")


async def main() -> None:
    """Scan the Rust example's standard/extended address ranges concurrently."""
    adapter = CanAdapter()

    standard_ids = range(0x700, 0x7F8)
    extended_ids = (0x18DA0000 + (i << 8) + 0xF1 for i in range(0x100))
    ids = [*standard_ids, *extended_ids]

    await asyncio.gather(*(get_version(adapter, identifier) for identifier in ids), return_exceptions=True)


if __name__ == "__main__":
    asyncio.run(main())

Synchronous examples

These programs use the matching modules under automotive.sync, including automotive.sync.adapters, automotive.sync.transports.isotp, and automotive.sync.protocols.uds, with the same configuration and value types. They use ordinary constructors, blocking calls, and receive iterators. Both the async and sync examples let ownership release resources when they go out of scope. Their setup, requests, output, and flow follow the corresponding Rust programs.

Send a CAN frame

"""Send one Classic CAN frame through the first available adapter."""

from automotive.sync.adapters import CanAdapter
from automotive.sync.can import Frame


def main() -> None:
    """Discover an adapter and send a frame."""
    adapter = CanAdapter()
    frame = Frame(id=0x123, data=b"\x01\x02\x03\x04")
    adapter.send(frame)
    print("Sent", frame)


if __name__ == "__main__":
    main()

Print CAN frames

"""Print frames from the first available adapter. Requires CAN hardware or vcan0."""

from automotive.sync.adapters import CanAdapter


def main() -> None:
    """Discover an adapter and print frames until interrupted."""
    adapter = CanAdapter()
    stream = adapter.recv()

    for frame in stream:
        print(frame)


if __name__ == "__main__":
    main()

Send ISO-TP messages

"""Exchange short and segmented PDUs with an ECU at 0x7A1/0x7A9."""

from automotive.sync.adapters import CanAdapter
from automotive.sync.transports.isotp import IsoTP, IsoTPConfig


def main() -> None:
    """Send short and segmented requests, printing each response as hex."""
    adapter = CanAdapter()
    config = IsoTPConfig(tx_id=0x7A1)
    isotp = IsoTP(adapter, config)

    stream = isotp.recv()

    isotp.send(b"\x3e\x00")
    response = next(stream)
    print("RX:", response.hex())

    isotp.send(b"\x22\xf1\x81")
    response = next(stream)
    print("RX:", response.hex())

    long_request = b"\x10" + b"\x00" * 32
    isotp.send(long_request)
    response = next(stream)
    print("RX:", response.hex())

    long_response = b"\x22\xf1\x81"
    isotp.send(long_response)
    response = next(stream)
    print("RX:", response.hex())


if __name__ == "__main__":
    main()

Query an ECU with UDS

"""Read software identification from a diagnostic ECU at transmit ID 0x7A1."""

from automotive.sync.adapters import CanAdapter
from automotive.sync.protocols.uds import DataIdentifier, SessionType, UDSClient
from automotive.sync.transports.isotp import IsoTP


def main() -> None:
    """Use defaults, select a session, and read an ECU data identifier."""
    adapter = CanAdapter()
    isotp = IsoTP.from_id(adapter, 0x7A1)
    uds = UDSClient(isotp)
    uds.tester_present()
    uds.diagnostic_session_control(SessionType.EXTENDED_DIAGNOSTIC)

    did = DataIdentifier.APPLICATION_SOFTWARE_IDENTIFICATION
    resp = uds.read_data_by_identifier(did)

    print(f"{did.name}: {resp!r}")


if __name__ == "__main__":
    main()

Configure bitrate and sample points

Configure the same PEAK timing through the synchronous API.

"""Configure a PEAK PCAN-USB FD adapter for 500 kbit/s CAN and 2 Mbit/s CAN FD."""

from automotive.sync.adapters import PeakAdapter, PeakConfig
from automotive.sync.can.bitrate import BitrateSettings


def main() -> None:
    """Configure nominal and data-phase timing, then print received frames."""
    config = PeakConfig(
        bitrate=BitrateSettings(
            bitrate=500_000,
            sample_point=0.8,
            data_bitrate=2_000_000,
            data_sample_point=0.8,
        ),
    )
    print("Resolved timing:", config.resolved_bitrate)
    adapter = PeakAdapter(config)
    stream = adapter.recv()

    for frame in stream:
        print(frame)


if __name__ == "__main__":
    main()

Query firmware versions

Query the same DIDs and ECU address ranges as the Rust and async Python examples. The synchronous version visits ECUs sequentially; the async version queries them concurrently.

"""Query named DIDs across standard and extended ECU addresses."""

from automotive.sync.adapters import CanAdapter
from automotive.sync.errors import AutomotiveError
from automotive.sync.protocols.uds import DataIdentifier, UDSClient, UDSConfig
from automotive.sync.transports.isotp import IsoTP, IsoTPConfig


def get_version(adapter: CanAdapter, identifier: int) -> None:
    """Query each named DID for one ECU, retaining its configured transport."""
    # Lower timeout for the sync version since we scan each ECU individually and bus load is low
    config = IsoTPConfig(tx_id=identifier, timeout=0.05)
    isotp = IsoTP(adapter, config)
    uds = UDSClient(isotp, UDSConfig(p2_timeout=0.05))

    uds.tester_present()

    for did in DataIdentifier:
        try:
            resp = uds.read_data_by_identifier(did)
        except (AutomotiveError, OSError):
            continue
        print(f"{identifier:x} 0x{did.value:x} {did.name}: {resp!r}")


def main() -> None:
    """Scan the Rust example's standard/extended address ranges sequentially."""
    adapter = CanAdapter()

    standard_ids = range(0x700, 0x7F8)
    extended_ids = (0x18DA0000 + (i << 8) + 0xF1 for i in range(0x100))
    ids = [*standard_ids, *extended_ids]

    for identifier in ids:
        try:
            get_version(adapter, identifier)
        except (AutomotiveError, OSError):
            continue


if __name__ == "__main__":
    main()