I let an AI talk to my car
Building an OBD-II bridge so a model could read my car directly - and what it taught me about where the intelligence belongs.
Recently, I replaced my car’s battery on a hunch. It felt like the car was struggling to turn over on cold starts, but there was no clear sign because there were no fault codes. I consulted AI and it came to the same conclusion. But this made me think: why am I in the diagnostic loop here, providing indirect, second-hand evidence to the AI? Why can’t the model just talk directly to the car, read the actual voltage data and perform any required tests?
Unfortunately, I didn’t have the tech for that. So I decided to build it.
I decided to build an MCP server that would bridge the gap between the model and the hardware. I started with a cheap Bluetooth OBDII dongle (OBDII is the automotive standard which defines diagnostic services and modes), which provided the bottom of the stack. This would allow serial Bluetooth communication between my laptop and the vehicle, and would implement the low-level CAN framing and request/response for me out of the box. This in turn meant that I wouldn’t need to implement a driver; the ELM327 Bluetooth dongle is the driver, providing the interface with a known and documented command set. I did, however, notice at this stage that these chips have a somewhat limited bandwidth - which would limit how many parameters (and at what frequency) I would be able to stream at the same time, and would shape the architecture.
On top of the ELM327 transport layer, I built out a PID (parameter IDs, such as motor RPM, or battery voltage) querying layer, used by OBDII. I then built a service layer on top of this that would expose tools to the MCP server. “python-OBD” would have handled most of the bottom half, but I’d already written my own by the time I properly looked at it, and since mine worked and I understood it end to end, I kept it. (It’s also GPL, and I wanted this MIT-licensed).
So the architecture stack looked like this:
While building the MCP tools I came to the realisation that there would be no need to implement diagnostic taste or logic here. I watched the model chain PID reads and reason about them without any hardcoded logic from me. Instead of building something like “check_charging_system()”, I designed purposefully thin primitives such as read a PID, list supported PIDs, read DTCs, open a stream. I had answered my own question: the intelligence no longer belongs in the application, but belongs in the model. I just needed a safe toolset for the model, which I provided by building exclusively read-only commands into the PID layer.
In order to make the MCP server testable (without relying on having the hardware in the loop), I built a light Streamlit application which enabled streaming selected PIDs, and built a way to record the stream to a JSON file, then to replay from the stream. I used the streaming feature to visualise the battery voltage, and then applied an electrical load (winding down all four windows at once). I proved the end-to-end integration by seeing the battery voltage dip in the graph at the same time:
I also recorded a session driving around the block, to prove the ability to record and replay the diagnostic session - an invaluable feature for diagnosing actual dynamic driving data.
At this point the project had crossed an important threshold: the AI was no longer just helping me reason about the car from photos and descriptions. It had a structured, read-only, interface into the vehicle itself.
So now I had something that worked well for my specific car, and for the default list of PIDs that I had pulled from online. But to make this project useful for other car models and manufacturers, I thought it would be useful to implement a discovery functionality. Effectively I wanted the LLM to ask the car “which PIDs do you support?”. This would pair nicely with the general MCP design philosophy - allowing auto-discovery rather than hardcoding capability. OBDII already supports this with so called “Mode 01 PID discovery”. So I built support for this for both the MCP client and the Streamlit dashboard, enabling users to connect to the car, ask what PIDs are supported, and then select what they wanted to see from the dynamic list generated in the dashboard (or indeed the LLM could do this itself over MCP).
Clearly there are limitations to this approach - I won’t claim that this tool is the answer to all car troubleshooting: far from it. While working on this project I also had to replace the car’s rear windscreen wiper motor, and when doing so I needed to take physical voltage readings at different points in the wiring loom and feed these into the model. The car doesn’t expose this over OBD. Equally, performing a simple voltage under load test (especially without knowing the load current) doesn’t give you a complete battery health test. But had I had this tool I could certainly have made a much more informed decision, especially by doing a few variations of load tests (combined with AI’s ability to pull datasheets and infer specific load currents).
Reflecting on this build made it clear to me that the reasoning boundary has shifted, and will continue to do so. As AI models get more capable, what real value is there in hardcoding “diagnostic intelligence” into the application? The new approach should be to provide the model with the right tools and harnessing, and then to get out of its way.
I thought back to how early waves of frontier model releases killed early AI companies, which relied on products that were fundamentally little more than an AI wrapper. How should today’s applications be built to prevent them becoming obsolete in six months? One approach appears to be to build with the rate of change in mind, and to fully expect that most of the reasoning should be done in the LLM layer. Thus, if reasoning is no longer the competitive moat, then perhaps it is data, domain expertise, harnessing and tooling.





