Skip to content

can

How AI Tools Cut Hardware Integration from Weeks to Hours

Integrating hardware has always been hard.. And it used to be expensive.  The 'hard' part is not going to change any time soon, but the "expensive" part is not true with modern AI tools. If you’re not using them yet, you’re missing out.

It used to take me about a week to integrate a device — sometimes more. That meant reading long documentation, writing prototypes, building logging tools, and wrapping everything in a driver.

I say “used to” because AI tools have completely changed that. Now I can do the same work in just a few hours. And you can too, once you know what needs to be done.

Here’s a real example from a customer project: I had to integrate motion controllers that claimed to be “CANopen” — but weren’t really. The manufacturer only implemented parts of the stack and didn’t provide .eds files.

In about six hours, using Claude CLI, I managed to:

  • convert the documentation into Markdown (so it’s machine-readable)
  • write a message decoder and a replay script for CAN messages
  • build a small dashboard to visualize decoded messages per drive
  • put together a driver based on an earlier project

So what does this mean?

For software developers, it means creating custom tools is no longer a big project. A single developer with AI can now do what used to take a small team. The hard part isn’t how anymore — it’s what and why

For machine builders, it means custom integrations can be done much cheaper, without relying on external vendors or getting locked into licensing models.

I've written this post primarily because I wanted to share my excitement about what these tools make possible. If you're dealing with similar hardware integration challenges, feel free to reach out — always happy to discuss approaches that work (and the ones that don't).

For nerds, an example of decoder in generated protocol code:

@dataclass
class RPDO1Command:
    """RPDO1 power management command (CAN ID: 0x200 + node_id, 8 bytes)."""

    CAN_ID_BASE: ClassVar[int] = 0x200

    mains_state: int  # U8: Mains state request (0x00=Off, 0x08=Power Ready Direct)
    power_limit: int = 0  # U8: Power limit (0-100%)

    @classmethod
    def can_id(cls, node_id: int) -> int:
        """Get CAN arbitration ID for this PDO and node."""
        return cls.CAN_ID_BASE + node_id

    def __str__(self) -> str:
        """Return formatted command string for display."""
        return f"Mains=0x{self.mains_state:02X}, Power_Limit={self.power_limit}%"

    @classmethod
    def decode(cls, data: bytes) -> "RPDO1Command":
        """Decode from 8-byte CAN message (raises ValueError if wrong length)."""
        if len(data) != 8:
            raise ValueError(f"RPDO1 data must be exactly 8 bytes, got {len(data)}")

        mains_state, power_limit = struct.unpack("<BB6x", data)
        return cls(mains_state=mains_state, power_limit=power_limit)

    def encode(self) -> bytes:
        """Encode to 8-byte CAN message."""
        # Pack as little-endian: U8, U8, 6 padding bytes
        return struct.pack("<BB6x", self.mains_state, self.power_limit)

    def send(self, bus: BusABC, node_id: int) -> None:
        """Send command to CAN bus."""
        data = self.encode()
        msg = can.Message(
            arbitration_id=self.can_id(node_id), data=data, is_extended_id=False
        )
        bus.send(msg)

Plotting live data with Plotjuggler

Containerization is essential for a reliable CI/CD workflow but can be tricky when live data visualization is needed, such as for motion control tuning.

A simple solution is to send data to Plotjuggler over a UDP socket. The example code below shows how to set this up.

For quick data analysis, use the provided UdpClient class. An example of sending test data is incuded as demo().

Plotjuggler