Last time I sent my bike to a mechanic it had a faulty oxygen sensor, they charged me $100 by simply reading a check engine light. I still don't know why it was this expensive since they just needed to connect a scanner to the OBD-II port, and probably click the scanner a couple of times to get the code.
Since then, I've tried to do everything on my bike myself. After some research I found out about a micro-controller called "ELM327", which allows software to communicate with the ECU.
To my surprise, this tiny device is only $24 on Amazon. Although this is different from a dedicated scan tool with built-in software, it also meant I had found a unique programming project.
I bought a WiFi-ELM327, this device creates its own network which you can connect to and communicate through a TCP socket. There was a Bluetooth option which did not seem reliable and a Raspberry Pi build which seemed hard to assemble. I stuck to WiFi.
The following script lets you communicate with the ELM327 very easily. Every command sent to the ELM327 ends with a carriage return (\r), which tells the adapter that the command is complete. Without it, the ELM327 keeps waiting for more input and won't process the command.
import socket
IP, PORT = "192.168.0.10", 35000
s = socket.create_connection((IP, PORT), timeout=5)
s.settimeout(2)
def cmd(c):
s.sendall((c + "\r").encode())
buff = b""
try:
while b">" not in buff:
buff += s.recv(1024)
except socket.timeout:
pass
return buff.decode(errors="replace").replace("\r", "\n").split()
while True:
c = input(">").strip()
if c.lower() == "q":
break
print(cmd(c))
s.close()
You can then ask the ECU which PIDs are supported by sending 0100 which will print several hex numbers. In my case, my bike replied with "41 00 BA 3E F0 01". Ignore the first two bytes, which are response headers. Convert the rest to binary, this gives a list of PIDs from 01-20 that are supported.
B A 3 E F 0 0 1
1011 1010 0011 1110 1111 0000 0000 0001
You can consult this Wikipedia article to guide you through the formulas for every PID. The most interesting ones for me were the third and fourth bytes (0011 1110). These bits correspond to PIDs 09–16, which according to the Wikipedia article are::
9. 0 -> Long term fuel trim (LTFT)—Bank 2
10. 0 -> Fuel pressure
11. 1 -> Intake manifold absolute pressure
12. 1 -> Engine speed
13. 1 -> Vehicle speed
14. 1 -> Timing advance
15. 1 -> Intake air temperature
16. 0 -> Mass air flow sensor
A 1 means the ECU supports that PID, while 0 means it doesn't. For example, vehicle speed corresponds to PID 0D. Sending:
Sending:
010D
returns:
41 0D 04
41 is the ECU responding to Mode 01, 0D is the requested PID, and 04 is the current speed value in hexadecimal. According to the article, the formula for vehicle speed is simply A, where A is the returned byte. This would mean that the current velocity is 4 km/h.
Knowing this was possible, the project shifted from reading engine fault codes to a telemetry logger. Currently, it saves RPM, speed, throttle position, and engine load. It saves these every ~1.4 seconds which is slow. I'm still testing to see if it's a WiFi limitation, a hardware one, or the amount of PIDs I'm asking for. This data is uploaded to my website at nezi.xyz/ride.csv and displayed at nezi.xyz/telemetry.
If you ever plan to make your own tool, I recommend you use an ELM emulator. You can find one at Ircama/ELM327-emulator. The exact same code I used in the emulator worked on my first try on my bike.
The logger is available at neizih/telemetry. This is a very early version.