Abstract
There are lots of pre-made car computers you can buy that plug into the OBD-II port. But most are expensive or don’t work very well. Also, you can’t add anything to them or change them. I want to make a better car computer using a Raspberry Pi. It will display info from my 1997 BMW M3 computer on a touch screen. This lets me easily see important things like my engine while tracking or racing the car.
My computer will be cheap to build, last a long time, and be easy to add more things to. It will also come out of the car quickly so I can make the BMW normal again if needed. With small changes, it should work for other cars too. I can also add GPS or sensors to know more about how the car is performing.
Turn your Raspberry Pi into obd2
I. Project Overview
This project is all about creating a computer that will interface with my 1997 BMW M3. I am actually converting the BMW to a track car therefore I have to be able to see such things like the temperatures, pressures, error codes etc. I also need to be able to log all the data so that I am able to see my performance after racing. The finished product fits into my car’s center console area. I 3D printed a cover that the touchscreen mounts into. The screen lets me control and see the data. Behind the cover, a Raspberry Pi connects to the BMW’s computer through the OBD port using a USB adapter. Python programs on the Pi get the data and show it on the 3.5″ touchscreen display. It saves all the info into a CSV file every second. This allows me to look at everything later.
The Pi gets power from a custom circuit board with a microcontroller. When I start the car, it turns on the Pi. Even when I turn off the ignition, it keeps the Pi on for 45 seconds so it can shut down safely. With some tweaks, this should work for other cars too.
II. Background
They are present in any vehicle with model year 1996 and newer, sold in the United States, and are required to have a diagnostic link interface, usually an OBD II port. OBD-II refers to On Board Diagnostic systems that are of the second generation. Currently, there is adoption of a single 16-pin connection plug as illustrated in Fig. 1 and this is used in all the car brands plying the Port. However, each manufacturer can choose which of the five allowed OBD-II communication protocols to use through that connector. So while the plug is uniform, the language the car speaks digitally can be one of five options picked by the automaker.
There are five OBD-II protocols that automakers can choose from:
- SAE J1850 PWM (mainly used by Ford)
- SAE J1850 VPW (primarily used by GM)
- ISO 14230-4
- ISO 15765-4 (required for all US vehicles 2008 and newer)
- ISO 9141-2 (mostly in older European cars)
My development vehicle uses the ISO 9141-2 protocol, which communicates at 115,200 baud.
Regarding these protocols, we transfer hexadecimal codes known as parameter IDs or PIDs to the car’s computer known as the ECU. PIDs are usually made up of two or more pairs of hexadecimal numbers. Sending different PIDs tells the ECU which type of data we want it to send back, like engine RPM, coolant temperature, or diagnostic trouble codes. So by using the right PID codes, we can request specific real-time data from the car’s computer through the OBD-II port.
- “OBD2 protocols – OBDTester.” http://www.obdtester.com/obd2_protocols
- “OBDII pinout – 0xicf ” 4 Mar. 2015, https://0xicf.wordpress.com/tag/obdii-pinout/
“SAE J1850- Interfacebus.com.” - http://www.interfacebus.com/Automotive_SAE_J1850_Bus.html
- “ISO 14230-4:2000 – ISO.org.” https://www.iso.org/standard/28826.html
- “ISO 15765-4:2016 – ISO.org.” https://www.iso.org/standard/67245.html
- “ISO 9141-2:1994 – ISO.org.” https://www.iso.org/standard/16738.html
The PID codes have two sets of hexadecimal numbers. The first set represents the OBD mode, like whether we want live data or diagnostic trouble codes (as shown in Table 1). The second set identifies the exact data parameter within that mode, things like engine RPM, coolant temp, or which diagnostic trouble code we want. So together, the two hexadecimal numbers tell the car’s computer precisely what kind of information we’re asking it to send back through the OBD-II port.
Mode Number | Mode Description |
01 | Current Data |
02 | Freeze Frame Data |
03 | Diagnostic Trouble Codes |
04 | Clear Trouble Code |
05 | Test Results/Oxygen Sensors |
06 | Test Results/Non-Continuous Testing |
07 | Show Pending Trouble Codes |
08 | Special Control Mode |
09 | Request Vehicle Information |
0A | Request Permanent Trouble Codes |
For example, to get a list of all the supported PIDs for mode 1, we would send the code “0100” to the ECU. It would then respond with all the supported mode 1 PIDs in hexadecimal format. While some PIDs are standard, car companies can add their own proprietary ones too as allowed.
The most common way to interface with the OBD port is with a dedicated OBD reader or USB adapter connecting to a computer. Many USB adapters use an ELM Electronics ELM327 chip to interpret the OBD data. I chose a ScanTool OBDLink SX because it had decent reviews for the price. This adapter actually uses an OBD Solutions STN1110 microcontroller that’s fully compatible with the ELM327 commands but has upgraded processing, memory, and speed. It translates the raw OBD data into a format that can be sent over a UART serial connection. An FTDI FT230XQ USB-UART chip then transmits the data through the USB cable to the computer.
7. “J1979_201408 – SAE International.” 11 Aug. 2014, http://standards.sae.org/j1979_201408/
8. “OBD – Elm Electronics.” https://www.elmelectronics.com/products/ics/obd/
9. “OBDLink® SX USB | OBDLink®.” http://www.obdlink.com/sxusb/
10. “STN1110 – Multiprotocol OBD Interpreter IC.” http://www.obdsol.com/solutions/chips/stn1110/
11. “FT230X USB Bridge | UART – FTDIChip.” http://www.ftdichip.com/Products/ICs/FT230X.html
III. Design
Before building anything, I sketched out how it would fit in my M3. The car has a little storage spot in the center console where people often mount gauges (Figure 2). I knew aftermarket companies make products for this spot. But I wanted to design mine to be easy to remove. It also needed space behind the screen to house a small touchscreen display plus room for the Raspberry Pi circuit board. That way everything could neatly fit inside the storage area with the screen portion easily accessible in the front.
At first, I considered modifying an existing gauge mounting kit, but they were too expensive (over $100). Instead, I decided to design my faceplate that would cover the storage area. I wanted it to be securely attached but also easy to remove.
Next, I focused on the graphical user interface (GUI) for the touchscreen. Readability and simplicity of use were key goals. Since the screen is out of the driver’s direct view, the data needs to be big, clear, and easily glanceable. Things like the tachometer should show RPM visually rather than just text. Interaction was also important – I didn’t want anything distracting while driving. So the GUI is designed with minimal touch controls during normal use.
12. “BMW 3 Gauge Cluster Console with Gauges (E36) – BMP Design.com.”
http://www.bmpdesign.com/product-exec/product_id/884
After laying the foundation for the GUI design, I began focusing on the software side. From the start, I knew I wanted the code to be very modular so new features could be added easily. I also decided to use Python since I’d never learned it in school. This project seemed like a great way for me to pick up the basics of Python.
I started with some rough sketches of how the code might be structured. Eventually, I settled on a final block diagram design (Figure 3). Having that high-level overview helped me plan out how the different pieces – like getting data from the car, processing it with Python modules, and displaying on the screen – would all fit and work together.
I decided to put all shared config variables and user settings in config.py. The main controller m3_pi.py would handle the GUI and coordinate the other modules. ecu.py connects to the car’s computer and writes the live data to variables in config.py so all modules can access it. log.py records everything in a CSV file.
Also needed was shutdown.py, a simple separate script to safely power down the Pi. Spending time on the design upfront really paid off – it would have been a mess trying to lump everything into one module.
The hardware was more challenging. I chose a Raspberry Pi 3 for its power and low cost, but my BMW didn’t have USB ports. So I had to use the constant 12V from the battery, requiring a DC-DC converter. However, the 12V is always on, which could drain the battery over time.
I realized I could use the “accessory” circuit that’s live only when the car is running. But losing power immediately on shutdown risked corrupting the SD card. The solution was adding a microcontroller to provide 12V power from the accessory circuit and keep the Pi running for 45 seconds after shutdown to safely power off.
Finally, I needed a touchscreen. I originally tried an Adafruit capacitive screen but damaged it. So I switched to their larger resistive screen which worked better despite being resistive instead of capacitive. Bringing it all together took some problem-solving!
IV. Development
Once the major components were designed, I started by setting up the Raspberry Pi 3. I chose Raspbian as the OS since it’s officially supported. Specifically, I went with the minimal Raspbian Lite version which is command line only.
I flashed the image to an SD card and booted up the Pi. From there, I configured the hostname and password and enabled SSH for remote access. Then I updated everything and installed the necessary kernel files to support the touchscreen using Adafruit’s script.
Once that was done, I plugged in the screen to test if it was working properly. A few more Python libraries were installed and the Pi was ready to go. More details on exactly how I set up the Raspberry Pi are in Section VI below. But getting the basics sorted first laid the foundation for the rest of the project.
Detailed Setup Instructions.
After getting the basics set up, I moved on to creating the graphical user interface (GUI). The problem was, that Python itself doesn’t have any tools for building graphical apps. But I knew there were library options to add those capabilities.
I did some digging into the different frameworks available. In the end, I chose Pygame. It’s good for building simple GUIs, which is what I needed for this project with its smaller touchscreen. Getting Pygame installed and figuring out the basics took a bit of time. But it has functions for drawing graphics, text, and handling touch input. Perfect for displaying data and interactions.
Mocking up early prototypes in Pygame helped me visualize exactly how each screen would look and work before writing any code. This made the actual programming part a lot smoother down the road.
So while Python may lack built-in GUI tools, libraries like Pygame fill that void and let you design and construct the on-screen display.
13. “Adafruit PiTFT – 320×240 2.8 TFT.” https://www.adafruit.com/product/1601
14. “Adafruit PiTFT – 480×320 3.5 TFT.” https://www.adafruit.com/product/2097
15. “GitHub – adafruit/Adafruit-PiTFT-Helper” https://github.com/adafruit/Adafruit-PiTFT-Helper
16. “Pygame.org.” https://www.pygame.org/
The library I chose was Pygame. It’s mainly meant for making simple video games, but also works great for GUIs. Compared to other options like PyQt and wxPython, Pygame seemed easier to use with more documentation available online.
The library I chose was Pygame. It’s mainly meant for making simple video games, but also works great for GUIs. Compared to other options like PyQt and wxPython, Pygame seemed easier to use with more documentation available online.
Getting a basic “Hello World” GUI running in Pygame didn’t take long. So as a proof of concept, I started working on an early very simple GUI (Figure 4). This allowed me to test that my approach was viable before developing the full interface. Pygame handled drawing graphics and inputs well, solidifying that it was the right pick for building out the display screens.
Overall Pygame provided an accessible way to prototype GUIs in Python, even though its main purpose is gaming. The documentation and community support helped overcome any learning curve. And proving a basic GUI was functional gave me confidence to flesh out the full user experience.
Getting live data from the car ended up being tricky. At first, I tried using a Bluetooth OBD adapter to minimize wiring, but couldn’t get it to reliably pair and communicate with the Pi.
So I switched to a USB adapter instead. Then used an open-source Python OBD library called python-OBD to write a small module for grabbing speed, RPM, and engine load from the vehicle.
The problem though was needing the car running to test it. So to develop remotely, I bought a Freematics OBD Emulator. It takes regular 12V power and can simulate sensor data through a simple Windows GUI over USB.
This let me virtually test everything without the car. I configured the emulator to use the same OBD protocol as my BMW. Being able to develop untethered from the vehicle made the process much smoother.
17. “Riverbank | Software | PyQt | What is PyQt?.” https://riverbankcomputing.com/software/pyqt/
18. “wxPython.” https://wxpython.org/
19. “python-OBD.” http://python-obd.readthedocs.io/
20. “Freematics OBD-II Emulator MK2.” http://freematics.com/store/?route=product/product&product_id=71
With the emulator working, I was able to get data from the ECU to display on the basic GUI.
However, I noticed that the GUI was extremely slow to update. After a fair amount of
debugging, I realized that the issue was that the ECU queries that I was making were
synchronous, and thus were blocking the UI. This meant that every time a query was made, the
program would block waiting for data and not update the GUI until the query finished. Luckily, I
was able to switch over to python-OBD’s asynchronous connection functionality which allowed
the UI to be updated on-the-fly. This asynchronous mode uses a threaded update loop that will
fire callbacks for the different queries as soon as new data is available from the ECU.
Now that I had the asynchronous communication established, I queried the M3’s ECU for a list
of supported PIDs using the command “0100”. Based on the response, I picked the ones that I
thought would be most useful to an end user (speed, RPM, MAF, engine load, throttle position,
coolant temperature, and intake temperature). However, there was one data point that I was
really hoping for that I was unable to get through OBD. I really wanted to have the current gear
that the car was in, since it would be a really useful data point to have for track use. To remedy
this, I decided to build a lookup table that would tell me what gear the car was theoretically in,
given some RPM and speed. To do this, I used the below formula (Figure 5) to populate the
table with the theoretical speeds the vehicle would be at a given RPM and gear.
(Axle Ratio x Vehicle Speed x Transmission Ratio x 336.13) / Tire Diameter
Figure 5 – Formula for calculating theoretical engine RPM
In the case of the M3, the axle ratio is the gear ratio of the LSD (limited slip differential) that is
located on the rear axle. The transmission ratio is the gear ratio of the current gear the vehicle is
in. The M3 features a 5-speed transmission, with the final gear using a straight 1:1 ratio. The
336.13 is used to convert the numerator to RPM (63360 inches per mile / 60 minutes per hour x
Pi.). Finally, the tire diameter is simply the diameter of the wheels, which for the M3 is 17”. This
approach ended up being surprisingly accurate, especially once combined with some logic to
detect when the car is theoretically in neutral (e.g The car is stopped at 0 MPH but the engine is
still idling at around 800 RPM).
I then moved on to finalizing the GUI design. I drew some inspiration from my internship with
Porsche at the beginning of this academic year, where I got to test a wide range of different next
generation infotainment systems. I was really inspired by Audi’s “Virtual Cockpit”, 12.3” high
resolution display that completely replaces the traditional instrument cluster. After a few
revisions, I settled on a final design (Figure 6) that displays all of the desired ECU data in a
beautiful yet readable font, has a tachometer that combines both text and visual elements, and
has a M3 logo. Tapping the screen loads a separate view that displays any DTCs stored on the
ECU (Figure 7).
The tachometer consists of 42 different images that were designed in Adobe Illustrator. Each line
on the tachometer represents 200 RPM, so each image builds on the next by highlighting another
line. This functions as the “needle” for the tachometer, but with a much simpler design. I also
made the entire tachometer turn red as the RPM approaches redline (around 7000 RPM). The
design makes it really easy to quickly glance at the screen and see what approximate RPM
you’re at, without actually needing to read the exact RPM number.
Once the GUI was finished, logging was a simple last step since Python has built-in CSV functions. When connecting to the ECU, the logger creates a new CSV file named with the current time.
It then appends new rows with each second’s worth of logged data. Only saving once per second worked well since OBD doesn’t update that frequently.
For testing without the actual car, I made a way to feed past log files into the GUI instead of live data. This was handy when working on the display away from the vehicle.
With the software complete, I focused on the power supply next. Based on my research, an ATtiny85 microcontroller was needed to turn the Pi on and off at the right times based on the accessory circuit.
Circuit design isn’t my strong suit, so I had my Electrical Engineer dad help design the circuit board around the ATtiny85 to make it work properly. Getting hands-on with another microcontroller was exciting too.
After designing the circuit schematic with my dad, we then built it out on a small prototype board. This could mount next to the Pi. It takes Ground, ACC, and 12V pins as inputs.
The board powers the Pi as soon as ACC turns on, then keeps it running for 45 seconds after ACC shuts off. A DC-DC converter steps the 12V to stable 5V for the Pi. And another regulator drops that to 3.3V for the ATtiny85 microcontroller.
When ACC cuts, the board sends a signal through another GPIO pin on the Pi. Seeing this high pin, the Pi executes a full shutdown script. That takes about 10 seconds, allowing it to power off before it loses power itself.
Having the physical circuit board laid out and functioning tied all the hardware aspects together safely and reliably. Now the software and hardware were fully integrated for real-world testing in the car.
To actually mount everything in the car, I used a right-angle OBD connector so the port wouldn’t stick out awkwardly into the footwell. Then routed the USB cable behind the panel to the hidden Pi. This made the install really clean – you can’t even see the OBD connection. I tapped into the 12V, ACC, and ground wires going to the stereo and ran new lines to the power board. Since I wanted everything hidden, I secured the board and Pi inside the storage cubby using standoffs.
A long ribbon cable connected the touchscreen to the Pi GPIO pins. The screen doesn’t use all the pins, leaving some free. And I ran jumper wires from the power board signals to screen GPIOs. This tied the hardware control signals together neatly in the dash compartment.A diagram (Figure 10) shows how all the components installed and interfaced within the center console area. The hidden wiring and integration made for a seamless cockpit upgrade.
For the faceplate, I had some leftover ABS plastic sheets that were perfect for rough prototyping. I cut out test shapes to cover the cubby with a cutout for the screen. ABS made iterating designs quick. Once happy, I moved to a thicker material. My job at iFixit has an awesome laser cutter, perfect for cutting wood, plastics, or thin metals precisely. I took my favorite ABS design into Illustrator to create a wood faceplate template. But then the smaller capacitive screen I was using broke during a test fit.
Luckily, I found a larger resistive screen that just barely fit if I redesigned the plate. More screen area was great for the GUI. This screen is mounted directly to the plate easier too. So I reworked the Illustrator file for the new screen size and added mounting holes (Figure 11). The iterative prototyping process paid off in refining the installation.
The wood faceplate turned out great, but then I realized 3D printing would allow for even more design freedom. Lucky for me, a coworker has a 3D printer willing to lend a hand. I quickly modeled a new design in CAD (Figure 12). The update – shown in green – was the screen would now screw directly into threaded holes in the plate itself. No exposed hardware is needed on the backside for a much sleeker look. The second change was adding a lip around the screen bezel. This made it appear like the display was seamlessly built into the plate rather than mounted on top. Having the ability to 3D print prototypes unlocked way more possibilities to refine small details. The tweaked integration gave an even cleaner finished appearance than wood could achieve. Glad I was able to tap that 3D printing resource!
Mounting the faceplate securely but removably took some brainstorming. In the end, I landed on using L-shaped metal brackets inside the left and right cubby walls. Then the key part – magnets on the backside of the plate. With strong neodymium magnets in just the right spots, the plate would “snap” firmly onto the brackets without any other hardware. Test fitting it, this magnetic mount worked amazingly well (Figure 13). The pic shows an early print but it gives the idea. A final high-quality print is next to make everything perfect. Overall I’m really happy with how this installation aspect came together. The magnetic attachment provides a solid in-car mount with easy removal for maintenance or upgrades down the road.
The last step was making the GUI launch automatically on startup. I edited the RC. local file for this. rc. local runs any commands in it after the Pi finishes booting. So I added two lines – one calling the shutdown script and the other launching the main program file. The key thing is putting an & at the end of each line. This tells the OS to run the commands simultaneously in the background as separate processes.
Without the &, they’d run sequentially and slow everything down.
Now with those two lines added to rc.local, the GUI loads up and has live data within around 25 seconds of starting the car. The system startup was fully automated.
V. Conclusion/Future Additions
I worked hard on this project through week 7, and since installing it in my car months ago, it’s been rock solid. I drive it daily without any major issues popping up.
That said, there’s always more to do. I’ve started putting a GPS module to use, but some threading kinks need working out first. Once integrated though, a new screen will show live location data and sync timestamps for more accurate logging.
Plus an IMU is on the horizon. Combining its gyro, accelerometer, and compass will let me track cool stats like lateral G’s for tracking. Another screen will display that live data too. My goal is to get everything uploaded to a home server over WiFi for easy access and analysis. I’m thinking of heat maps merging GPS with performance data to visualize driving patterns. Hardware-wise, the tiny Pi Zero W is calling my name for its ultra-portability. Directly connecting to the CAN bus instead of OBD might speed things up. That also opens doors to controlling car functions remotely. Overall I’m stoked to keep expanding on this in my free time. The journey of constant improvement and discovery is what makes projects like these so rewarding.
21″GlobalSat BU-353-S4 is – USGlobalsat Corporate.” http://usglobalsat.com/p-688-bu-353-s4.aspx
To wrap things up, I want to open-source my code and upload it to GitHub. There’s some BMW-specific stuff I’ll need to make more universal, like the gear calculator and tachometer graphics. But overall it should be doable to generalize it for any car. I sunk a ton of time into building this out, so sharing it publicly through GitHub would be super rewarding. I hope that other gearheads and tinkerers can take the codebase and use it as a starting point to add an OBD display to their rides. If even a few people find it helpful, all those hours I devoted will feel truly worthwhile. Open sourcing is the goal so that anyone can benefit from and build upon this project going forward. That’s really what it’s all about – advancing each other’s work through collaboration and the free exchange of ideas.
VI. Detailed Setup Instructions
I made sure to thoroughly document every configuration step taken to transform a standard Raspbian Lite image on a Raspberry Pi 3 into the finished product now installed in my car. Please note these instructions assume a basic familiarity with using Linux. It’s also important to call out that “sudo apt-get update” and “sudo apt-get upgrade” should never be run, as that would break the customized kernel enabling the touchscreen. Since open-sourcing my entire codebase is the goal, taking extensive notes on installing and setting up the system software is key. Providing a clear guide roadmap for anyone wanting to replicate or expand upon the project is important for transparency. My focus on documentation now aims to empower fruitful community contributions down the line. Thoroughly detailing how everything was put together pays off by enabling others to easily pick up and continue advancing this open-source dashboard initiative.
- Flash the latest Raspbian image to an 8GB+ SD card using Etcher, the open source imager.
- Connect the Pi to a monitor, keyboard, and power. Debug text should scroll by on a successful boot.
- Open the terminal and configure WiFi by editing the wpa_supplicant file.
- Optionally run raspi-config to set things like the hostname, password, enable SSH.
- Make sure everything’s updated with the terminal apt-get commands.
- Install a custom kernel from Adafruit to enable the touchscreen – check their site for script details.
- Plugging in the screen should now show the command line interface.
- Install required Python tools and libraries with the terminal apt-get and pip commands.
- Transfer project files to the Pi folder. Code is provided but some extras can’t be included.
- We’ll make the code auto-launch at startup by editing rc.local to run the files at boot.
- All set! The detailed instructions aim to clearly guide setting up the system software.
VII. Appendices
config.py
#!/usr/bin/python
import ecu
import datetime
import numpy as np
# Globals.
logLength = 0
dtc_iter = 0
time_elapsed_since_last_action = 0
gui_test_time = 0
logIter = 1
ecuReady = False
debugFlag = False
settingsFlag = False
piTFT = True
startTime = datetime.datetime.today().strftime('%Y%m%d%H%M%S')
RESOLUTION = (480, 320)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
# LUT representing the speeds at each of the five gears. Each entry is +200 RPM, and
is directly linked to rpmList.
speedArr = np.array([[4, 7, 10, 14, 17], [5, 9, 14, 18, 23], [7, 11, 17, 23, 28], [8,
14, 21, 28, 34], [9, 16, 24, 32, 40], [11, 18, 27, 37, 46], [12, 21, 31, 41, 51],
[14, 23, 34, 46, 57], [15, 25, 38, 50, 63], [16, 27, 41, 55, 68], [18, 30, 45, 60,
74], [19, 32, 48, 64, 80], [20, 34, 51, 69, 85], [22, 37, 55, 73, 91], [23, 39, 58,78, 97],
[24, 41, 62, 83, 102], [26, 43, 65, 87, 108], [27, 46, 69, 92, 114], [28,
48, 72, 96, 120], [30, 50, 75, 101, 125], [31, 53, 79, 106, 131], [33, 55, 82, 110,
137], [34, 57, 86, 115, 142], [35, 59, 89, 119, 148], [37, 62, 93, 124, 154], [38,
64, 96, 129, 159]])
# List of RPM values for above LUT.
rpmList = np.array([750, 1000, 1250, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3250,
3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500, 5750, 6000, 6250, 6500, 6750,
7000])
m3_pi.py
#!/usr/bin/python
import config, ecu, log, time, datetime, sys
import pygame, time, os, csv
from pygame.locals import *
# Helper function to draw the given string at coordinate x,y, relative to center.
def drawText(string, x, y, font):
if font == “readout” :
text = readoutFont.render(string, True, config.WHITE)
elif font == “label”:
text = labelFont.render(string, True, config.WHITE)
elif font == “fps”:
text = fpsFont.render(string, True, config.WHITE)
textRect = text.get_rect()
textRect.centerx = windowSurface.get_rect().centerx + x
textRect.centery = windowSurface.get_rect().centery + y
windowSurface.blit(text, textRect)
# Connect to the ECU and GPS.
if not config.debugFlag:
ecu.ecuThread()
# Give time for the ECU to connect before we start the GUI.
while not config.ecuReady:
time.sleep(.01)
# Load all of our tach images into an array so we can easily access them.
background_dir = ‘tach/’
background_files = [‘%i.png’ % i for i in range(0, 42)]
ground = [pygame.image.load(os.path.join(“/home/pi/tach/”, file)) for file in
background_files]
# Load the M3 PI image.
img = pygame.image.load(“/home/pi/images/m3_logo.png”)
img_button = img.get_rect(topleft=(135, 220))
# Set up the window. If piTFT flag is set, set up the window for the screen. Else
create it normally for use on normal monitor.
if config.piTFT:
os.putenv(‘SDL_FBDEV’, ‘/dev/fb1’)
pygame.init()
pygame.mouse.set_visible(0)
windowSurface = pygame.display.set_mode(config.RESOLUTION)
else:
windowSurface = pygame.display.set_mode(config.RESOLUTION, 0, 32)
# Set up fonts
readoutFont = pygame.font.Font(“/home/pi/font/ASL_light.ttf”,40)
labelFont = pygame.font.Font(“/home/pi/font/ASL_light.ttf”,30)
fpsFont = pygame.font.Font(“/home/pi/font/ASL_light.ttf”,20)
# Set the caption.
pygame.display.set_caption(‘M3 PI’)
# Create a clock object to use so we can log every second.
clock = pygame.time.Clock()
# Create the csv log file with the specified header.
log.createLog([“TIME”, “RPM”, “SPEED”, “COOLANT_TEMP”, “INTAKE_TEMP”, “MAF”,
“THROTTLE_POS”, “ENGINE_LOAD”])
# Debug: Instead of reading from the ECU, read from a log file.
if config.debugFlag:
# Read the log file into memory.
list = log.readLog(‘/logs/debug_log.csv’)
# Get the length of the log.
logLength = len(list)
# Run the game loop
while True:
for event in pygame.event.get():
if event.type == QUIT:
# Rename our CSV to include end time.
log.closeLog()
# Close the connection to the ECU.
ecu.connection.close()
pygame.quit()
sys.exit()
if event.type == MOUSEBUTTONDOWN:
# Toggle the settings flag when the screen is touched.
config.settingsFlag = not config.settingsFlag
if not config.debugFlag:
# Figure out what tach image should be.
ecu.getTach()
# Figure out what gear we’re *theoretically* in.
ecu.calcGear(int(float(ecu.rpm)), int(ecu.speed))
# Clear the screen
windowSurface.fill(config.BLACK)
# Load the M3 logo
windowSurface.blit(img, (windowSurface.get_rect().centerx – 105,
windowSurface.get_rect().centery + 60))
# If the settings button has been pressed:
if (config.settingsFlag):
drawText(“Settings”, -160,-145, “readout”)
# Print all the DTCs
if ecu.dtc:
for code,desc in ecu.dtc:
drawText(code, 0, -80 + (dtc_iter * 50), “label”)
dtc_iter += 1
if dtc_iter == len(dtc):
dtc_iter = 0
else:
drawText(“No DTCs found”, 0, -80, “label”)
else:
# Calculate coordinates so tachometer is in middle of screen.
coords = (windowSurface.get_rect().centerx – 200,
windowSurface.get_rect().centery – 200)
# Load the tach image
windowSurface.blit(ground[ecu.tach_iter], coords)
# Draw the RPM readout and label.
drawText(str(ecu.rpm), 0, 0, “readout”)
drawText(“RPM”, 0, 50, “label”)
# Draw the intake temp readout and label.
drawText(str(ecu.intakeTemp) + “\xb0C”, 190, 105, “readout”)
drawText(“Intake”, 190, 140, “label”)
# Draw the coolant temp readout and label.
drawText(str(ecu.coolantTemp) + “\xb0C”, -160, 105, “readout”)
drawText(“Coolant”, -170, 140, “label”)
# Draw the gear readout and label.
drawText(str(ecu.gear), -190, 0, “readout”)
drawText(“Gear”, -190, 50, “label”)
# Draw the speed readout and label.
drawText(str(ecu.speed) + ” mph”, 170, 0, “readout”)
drawText(“Speed”, 180, 50, “label”)
# Draw the throttle position readout and label.
drawText(str(ecu.throttlePosition) + ” %”, 190, -145, “readout”)
drawText(“Throttle”, 190, -110, “label”)
# Draw the MAF readout and label.
drawText(str(ecu.MAF) + ” g/s”, -150, -145, “readout”)
drawText(“MAF”, -190, -110, “label”)
# Draw the engine load readout and label.
drawText(str(ecu.engineLoad) + ” %”, 0, -145, “readout”)
drawText(“Load”, 0, -110, “label”)
# If debug flag is set, feed fake data so we can test the GUI.
if config.debugFlag:
# Debug gui display refresh 10 times a second.
if config.gui_test_time > 500:
log.getLogValues(list)
ecu.calcGear(ecu.rpm, ecu.speed)
ecu.getTach()
config.gui_test_time = 0
# Update the clock.
dt = clock.tick()
config.time_elapsed_since_last_action += dt
config.gui_test_time += dt
# We only want to log once a second.
if config.time_elapsed_since_last_action > 1000:
# Log all of our data.
data = [datetime.datetime.today().strftime(‘%Y%m%d%H%M%S’), ecu.rpm,
ecu.speed, ecu.coolantTemp, ecu.intakeTemp, ecu.MAF, ecu.throttlePosition,
ecu.engineLoad]
log.updateLog(data)
# Reset time.
config.time_elapsed_since_last_action = 0
# draw the window onto the screen
pygame.display.update()
ecu.py
#!/usr/bin/python
import config
from threading import Thread
import obd
import numpy as np
# Globals
rpm = 0
speed = 0
coolantTemp = 0
intakeTemp = 0
MAF = 0
throttlePosition = 0
timingAdvance = 0
engineLoad = 0
tach_iter = 0
gear = 0
connection = None
dtc = None
# Function to figure out what tach image we should display based on the RPM.
def getTach():
global tach_iter
if rpm == 0:
tach_iter = 0
elif (rpm >= 0) & (rpm < 200):
tach_iter = 1
elif (rpm >= 200) & (rpm < 400):
tach_iter = 2
elif (rpm >= 400) & (rpm < 600):
tach_iter = 3
elif (rpm >= 600) & (rpm < 800):
tach_iter = 4
elif (rpm >= 800) & (rpm < 1000):
tach_iter = 5
elif (rpm >= 1000) & (rpm < 1200):
tach_iter = 6
elif (rpm >= 1200) & (rpm < 1400):
tach_iter = 7
elif (rpm >= 1400) & (rpm < 1600):
tach_iter = 8
elif (rpm >= 1600) & (rpm < 1800):
tach_iter = 9
elif (rpm >= 1800) & (rpm < 2000):
tach_iter = 10
elif (rpm >= 2000) & (rpm < 2200):
tach_iter = 11
elif (rpm >= 2200) & (rpm < 2400):
tach_iter = 12
elif (rpm >= 2400) & (rpm < 2600):
tach_iter=13elif (rpm>=2600)&(rpm<2800):tach_iter=14elif (rpm>=2800)&(rpm<3000):tach_iter=15elif (rpm>=3000)&(rpm<3200):tach_iter=16elif (rpm>=3200)&(rpm<3400):tach_iter=17elif (rpm>=3400)&(rpm<3600):tach_iter=18elif (rpm>=3600)&(rpm<3800):tach_iter=19elif (rpm>=3800)&(rpm<4000):tach_iter=20elif (rpm>=4000)&(rpm<4200):tach_iter=21elif (rpm>=4200)&(rpm<4400):tach_iter=22elif (rpm>=4400)&(rpm<4600):tach_iter=23elif (rpm>=4600)&(rpm<4800):tach_iter=24elif (rpm>=4800)&(rpm<5000):tach_iter=25elif (rpm>=5000)&(rpm<5200):tach_iter=26elif (rpm>=5200)&(rpm<5400):tach_iter=27elif (rpm>=5400)&(rpm<5600):tach_iter=28elif (rpm>=5600)&(rpm<5800):tach_iter=29elif (rpm>=5800)&(rpm<6000):tach_iter=30elif (rpm>=6000)&(rpm<6200):tach_iter=31elif (rpm>=6200)&(rpm<6400):tach_iter=32elif (rpm>=6400)&(rpm<6600):tach_iter=33elif (rpm>=6600)&(rpm<6800):tach_iter=34elif (rpm>=6800)&(rpm<7000):tach_iter=35elif (rpm>=7000)&(rpm<7200):tach_iter=36elif (rpm>=7200)&(rpm<7400):
tach_iter = 37elif (rpm >= 7400) & (rpm < 7600):
tach_iter = 38
elif (rpm >= 7600) & (rpm < 7800):
tach_iter = 39
elif (rpm >= 7800) & (rpm < 8000):
tach_iter = 40
elif (rpm >= 8000):
tach_iter = 41
# Given an array and a value, find what array value our value is closest to and
return the index of it.
def find_nearest(array, value):
idx = (np.abs(array-value)).argmin()
return idx
# Given RPM and speed, calculate what gear we’re probably in.
def calcGear(rpm, speed):
global gear
# We’re stopped, so we’re obviously in neutral.
if speed == 0:
gear = ‘N’
# We’re moving but the RPM is really low, so we must be in neutral.
# M3 seems to idle at around 800 rpm
elif (rpm < 875) & (speed > 0):
gear = ‘N’
# We must be in gear.
else:
# Find the index of the closest RPM to our current RPM.
closestRPMIndx = find_nearest(config.rpmList, rpm)
# Find the index of the closest speed to our speed.
closestSpeedIndx = find_nearest(config.speedArr[closestRPMIndx], speed)
gear = str (closestSpeedIndx + 1)
class ecuThread(Thread):
def __init__(self):
Thread.__init__(self)
self.daemon = True
self.start()
def run(self):
global connection
ports = obd.scan_serial()
print ports
# DEBUG: Set debug logging so we can see everything that is happening.
obd.logger.setLevel(obd.logging.DEBUG)
# Connect to the ECU.
connection = obd.Async(“/dev/ttyUSB0″, 115200, “3”, fast=False)
# Watch everything we care about.
connection.watch(obd.commands.RPM, callback=self.new_rpm)
connection.watch(obd.commands.SPEED, callback=self.new_speed)
connection.watch(obd.commands.COOLANT_TEMP,
callback=self.new_coolant_temp)
connection.watch(obd.commands.INTAKE_TEMP,
callback=self.new_intake_temp)
connection.watch(obd.commands.MAF, callback=self.new_MAF)
connection.watch(obd.commands.THROTTLE_POS,
callback=self.new_throttle_position)
connection.watch(obd.commands.ENGINE_LOAD,
callback=self.new_engine_load)
connection.watch(obd.commands.GET_DTC, callback=self.new_dtc)
# Start the connection.
connection.start()
# Set the ready flag so we can boot the GUI.
config.ecuReady = True
def new_rpm(self, r):
global rpm
rpm = int(r.value.magnitude)
def new_speed(self, r):
global speed
speed = r.value.to(“mph”)
speed = int(round(speed.magnitude))
def new_coolant_temp(self, r):
global coolantTemp
coolantTemp = r.value.magnitude
def new_intake_temp(self, r):
global intakeTemp
intakeTemp = r.value.magnitude
def new_MAF(self, r):
global MAF
MAF = r.value.magnitude
def new_throttle_position(self, r):
global throttlePosition
throttlePosition = int(round(r.value.magnitude))
def new_timing_advance(self, r):
global timingAdvance
timingAdvance = int(round(r.value.magnitude))
def new_engine_load(self, r):
global engineLoad
engineLoad = int(round(r.value.magnitude))
def new_dtc(self, r):
global dtc
dtc = r.value
log.py
#!/usr/bin/python
import csv, os
from config import *
# Function to create a csv with the specified header.
def createLog(header):
# Write the header of the csv file.
with open(‘/home/pi/logs/’ + startTime + ‘.csv’, ‘wb’) as f:
w = csv.writer(f)
w.writerow(header)
# Function to append to the current log file.
def updateLog(data):
with open(‘/home/pi/logs/’ + startTime + ‘.csv’, ‘a’) as f:
w = csv.writer(f)
w.writerow(data)
# Function to close the log and rename it to include end time.
def closeLog():
endTime = datetime.datetime.today().strftime(‘%Y%m%d%H%M%S’)
os.rename(‘home/pi/logs/’ + startTime + ‘.csv’, ‘logs/’ + startTime + “_” +
endTime + ‘.csv’)
# Debug function to read from log file for GUI testing.
def readLog(logFile):
with open(logFile, ‘rb’) as f:
reader = csv.reader(f)
logList = list(reader)
return logList
# Debug function that reads from log file and assigns to global values.
def getLogValues(logFile):
global logIter
global rpm
global speed
global coolantTemp
global intakeTemp
global MAF
global throttlePosition
global engineLoad
rpm = int(logFile[logIter][1])
speed = int(logFile[logIter][2])
coolantTemp = logFile[logIter][3]
intakeTemp = logFile[logIter][4]
MAF = logFile[logIter][5]
# Cludgy fix for issue where MAF was logged as really log float, causing
clipping when displayed on GUI.
MAF = format(float(MAF), ‘.2f’)
throttlePosition = logFile[logIter][6]
engineLoad = logFile[logIter][7]
logIter += 1
# Reset iterator.
if logIter == logLength
shutdown.py
#!/usr/bin/python
import RPi.GPIO as GPIO
import os, time
# Set GPIO pin 17 as input for shutdown signal.
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.IN)
# Print message to console.
print("Running shutdown script...")
while True:
if (GPIO.input(17)):
os.system("sudo shutdown -h now")
break
Bill of Materials
Component Price
Raspberry Pi 3 $35
ScanTool OBDLink SX $30
Adafruit PiTFT 3.5 $45
3D-Printed Faceplate $10
Power Supply Components $8
Various wires and cables $2
Total: $130
Development Vehicle Supported PIDs
This is a list of the 53 OBD parameters that the M3 supports. Note that this list is very short
compared to what a modern car supports, most likely because OBD-II had only been around for
less than a year before the M3 was manufactured.
0112: Secondary Air Status
0113: O2 Sensors Present
0110: Air Flow Rate (MAF)
0111: Throttle Position
0114: O2: Bank 1 – Sensor 1 Voltage
0115: O2: Bank 1 – Sensor 2 Voltage
0118: O2: Bank 2 – Sensor 1 Voltage
0119: O2: Bank 2 – Sensor 2 Voltage
021C: DTC OBD Standards Compliance
ATI: ELM327 version string
0140: Supported PIDs [41-60]
020C: DTC Engine RPM
03: Get DTCs
07: Get DTCs from the current/last driving cycle
04: Clear DTCs and Freeze data
011C: OBD Standards Compliance
0213: DTC O2 Sensors Present
0206: DTC Short Term Fuel Trim – Bank 1
0207: DTC Long Term Fuel Trim – Bank 1
0204: DTC Calculated Engine Load
0205: DTC Engine Coolant Temperature
0203: DTC Fuel System Status
0201: DTC Status since DTCs cleared
010E: Timing Advance
010D: Vehicle Speed
010F: Intake Air Temp
0208: DTC Short Term Fuel Trim – Bank 2
0209: DTC Long Term Fuel Trim – Bank 2
020F: DTC Intake Air Temp
020D: DTC Vehicle Speed
020E: DTC Timing Advance
0109: Long Term Fuel Trim – Bank 2
0108: Short Term Fuel Trim – Bank 2
0120: Supported PIDs [21-40]
0105: Engine Coolant Temperature
0104: Calculated Engine Load
0107: Long Term Fuel Trim – Bank 1
0106: Short Term Fuel Trim – Bank 1
0101: Status since DTCs cleared
0100: Supported PIDs [01-20]
0103: Fuel System Status
0214: DTC O2: Bank 1 – Sensor 1 Voltage
0220: DTC Supported PIDs [21-40]
ATRV: Voltage detected by OBD-II adapter
0211: DTC Throttle Position
0210: DTC Air Flow Rate (MAF)
0600: Supported MIDs [01-20]
0212: DTC Secondary Air Status
0215: DTC O2: Bank 1 – Sensor 2 Voltage
010C: Engine RPM
0219: DTC O2: Bank 2 – Sensor 2 Voltage
0218: DTC O2: Bank 2 – Sensor 1 Voltage
0240: DTC Supported PIDs [41-60]