├── README.md ├── digital_dash.py ├── gallons.txt └── logo.png /README.md: -------------------------------------------------------------------------------- 1 | # digital_dash 2 | A digital dashboard using Python, a Raspberry Pi Zero W, and vehicle's On Board Diagnostics (OBD-II) Port. 3 | 4 | ## Motivation 5 | My personal reason for creating this was because my car's fuel sensor broke. The cost for repair was astronomical, so I figured creating a program to calculate the fuel level and milage would be much cheaper. 6 | 7 | Really, though, this is easily made into just a fun digital dashboard for anyone who wants to experiment with 'carputers.' 8 | 9 | ## How It Works 10 | The real guts of this operation is in the OBD-II port of pretty much every car since the 1996. OBD (On Board Diagnostics) lets you access the car's computer and retrieve information from it. Scroll down to see my supplies list. 11 | 12 | Every tenth of a second, the program queries the OBD port for the data I want through the obd python library. Dependencies are also listed below. 13 | 14 | ## Hardware 15 | 1. Raspberry Pi Zero W (Note: if you use the display I chose, you will need the GPIO header pins on the Pi. It makes a very compact and easily mountable assembly.) 16 | 2. Touchscreen Display of your choice. I like the hyperpixel4 from Pimoroni. (https://shop.pimoroni.com/products/hyperpixel-4?variant=12569485443155) 17 | 3. Bluetooth ELM327 OBD-II Device (https://www.amazon.com/gp/product/B011NSX27A/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1) 18 | 4. OBD-II port splitter (https://www.amazon.com/gp/product/B0711LGRGQ/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1) 19 | 5. OBD-II to micro USB power supply (https://www.amazon.com/gp/product/B074M4XMBX/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1) 20 | 21 | ## Dependencies 22 | 1. OBD `pip install obd` 23 | 2. PyQt5 `sudo apt-get install python3-pyqt5` 24 | 3. BlueZ Protocol `sudo apt-get install bluez` 25 | 26 | ## Hyperpixel4.0 Display Setup 27 | Run these commands in the terminal 28 | 1. `curl -sSL https://get.pimoroni.com/hyperpixel4 | bash` 29 | 2. `hyperpixel4-rotate right` 30 | 3. `reboot` 31 | 32 | ## Bluetooth Setup 33 | There have been some issues connecting the Raspberry Pi to ELM327 Devices. You might get the error `this device has no services which can be used with Raspberry Pi` when trying to connect the adapter. Here is how to successfully set it up. 34 | 35 | 1. Use the Bluetooth GUI or `bluetoothctl` 36 | 2. `sudo nano /etc/systemd/system/dbus-org.bluez.service` 37 | 3. Add/edit these lines 38 | ``` 39 | ExecStart=/usr/lib/bluetooth/bluetoothd -C 40 | ExecStartPost=/usr/bin/sdptool add SP 41 | ``` 42 | 4. `reboot` 43 | 5. `sudo rfcomm connect 0 1` You can get the MAC Address by using `bluetoothctl`'s `devices` command. 44 | 6. `sudo rfcomm bind hci0` 45 | 7. `reboot` 46 | 47 | Many thanks to this website for helping me out with this part: https://www.lukinotes.com/2018/10/raspberry-pi-bluetooth-connection.html 48 | 49 | That should do the trick for setting up bluetooth. To double check, try opening python in the terminal, and run this: 50 | ``` 51 | from obd import * 52 | connection = obd.OBD() 53 | connection.status() 54 | ``` 55 | It will return a string telling you the status of the connection. Go here for more information: https://python-obd.readthedocs.io/en/latest/Connections/. 56 | 57 | **NOTE:** If you still have issues, try forgetting the bluetooth device and then reconnecting, and running the `rfcomm bind` command again. I had some issues the first try around, but this worked for me. 58 | 59 | ## Code Setup 60 | Run these commands in the terminal 61 | 1. `cd /home/pi/` 62 | 2. `mkdir Dashboard` 63 | 3. `cd Dashboard` 64 | 4. `git clone https://github.com/murrasource/digital_dash.git` 65 | 5. `chmod +x digital_dash.py` 66 | 6. `cp /home/pi/Dashboard/digital_dash.py /home/pi/Desktop` 67 | Now you can double click on the digital_dash.py folder on your Desktop to execute the file. 68 | 69 | ## Usage and Customization 70 | As previously stated, my motivation for coding this was because my gas sensor broke and I didn't want to pay for the $600-$800 repair. You will see in my code that I calculate the amount of fuel left in the tank based on some simple stoichiometry. This would be incredibly simple to replace with OBD-II queries if your car's gas sensor works. Take a look here (https://python-obd.readthedocs.io/en/latest/Command%20Tables/) for a list of all the different queries the OBD library supports. 71 | 72 | Also, feel free to have fun and customize what you want for the logo.png file. It should autoscale to the correct fit. I did my car's name in Tesla font and it looks pretty cool, if I may say so myself. The example I have on the GitHub is the Lucid Motors logo from the company's Twitter post. 73 | 74 | **IMPORTANT TIP:** To exit fullscreen, just double click the refill button. 75 | -------------------------------------------------------------------------------- /digital_dash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import subprocess as sp 6 | import time 7 | from PyQt5 import QtCore, QtGui, QtWidgets 8 | from obd import * 9 | 10 | # make our own double-click enabled button 11 | class QDoublePushButton(QtWidgets.QPushButton): 12 | doubleClicked = QtCore.pyqtSignal() 13 | clicked = QtCore.pyqtSignal() 14 | 15 | def __init__(self, *args, **kwargs): 16 | QtWidgets.QPushButton.__init__(self, *args, **kwargs) 17 | self.timer = QtCore.QTimer() 18 | self.timer.setSingleShot(True) 19 | self.timer.timeout.connect(self.clicked.emit) 20 | super().clicked.connect(self.checkDoubleClick) 21 | 22 | @QtCore.pyqtSlot() 23 | def checkDoubleClick(self): 24 | if self.timer.isActive(): 25 | self.doubleClicked.emit() 26 | self.timer.stop() 27 | else: 28 | self.timer.start(250) 29 | 30 | 31 | # the real meat of the operation 32 | class Ui_MainWindow(object): 33 | # graphics and design with PyQt5 34 | def setupUi(self, MainWindow): 35 | MainWindow.setObjectName("MainWindow") 36 | MainWindow.resize(800, 480) 37 | palette = QtGui.QPalette() 38 | MainWindow.setPalette(palette) 39 | font = QtGui.QFont() 40 | font.setFamily("High Tower Text") 41 | MainWindow.setFont(font) 42 | #MainWindow.setCursor(QtGui.QCursor(QtCore.Qt.BlankCursor)) 43 | MainWindow.setStyleSheet("background-color: rgb(29, 29, 29); border-color: rgb(232, 232, 232);") 44 | self.centralwidget = QtWidgets.QWidget(MainWindow) 45 | self.centralwidget.setObjectName("centralwidget") 46 | 47 | # FRIZ LOGO 48 | self.friz = QtWidgets.QLabel(self.centralwidget) 49 | self.friz.setGeometry(QtCore.QRect(225, 30, 350, 50)) 50 | self.friz.setPixmap(QtGui.QPixmap("/home/pi/Dashboard/logo.png")) 51 | self.friz.setScaledContents(True) 52 | self.friz.setObjectName("friz") 53 | 54 | 55 | # SPEED WIDGET 56 | self.spedometer = QtWidgets.QLabel(self.centralwidget) 57 | self.spedometer.setGeometry(QtCore.QRect(347, 140, 200, 200)) 58 | font = QtGui.QFont() 59 | font.setFamily("Javanese Text") 60 | font.setPointSize(62) 61 | self.spedometer.setFont(font) 62 | self.spedometer.setStyleSheet("color: rgb(248, 248, 248);") 63 | self.spedometer.setObjectName("speed") 64 | 65 | # RPM LABEL 66 | self.rpm_label = QtWidgets.QLabel(self.centralwidget) 67 | self.rpm_label.setGeometry(QtCore.QRect(10, 10, 81, 51)) 68 | font = QtGui.QFont() 69 | font.setFamily("Javanese Text") 70 | font.setPointSize(18) 71 | self.rpm_label.setFont(font) 72 | self.rpm_label.setStyleSheet("color: rgb(222, 222, 222);") 73 | self.rpm_label.setObjectName("rpm_label") 74 | 75 | # LOAD LABEL 76 | self.load_label = QtWidgets.QLabel(self.centralwidget) 77 | self.load_label.setGeometry(QtCore.QRect(689, 10, 101, 51)) 78 | font = QtGui.QFont() 79 | font.setFamily("Javanese Text") 80 | font.setPointSize(18) 81 | self.load_label.setFont(font) 82 | self.load_label.setStyleSheet("color: rgb(222, 222, 222);") 83 | self.load_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) 84 | self.load_label.setObjectName("load_label") 85 | 86 | # GAL LABEL 87 | self.gal_label = QtWidgets.QLabel(self.centralwidget) 88 | self.gal_label.setGeometry(QtCore.QRect(10, 400, 81, 51)) 89 | font = QtGui.QFont() 90 | font.setFamily("Javanese Text") 91 | font.setPointSize(18) 92 | self.gal_label.setFont(font) 93 | self.gal_label.setStyleSheet("color: rgb(222, 222, 222);") 94 | self.gal_label.setObjectName("gal_label") 95 | 96 | # MI LABEL 97 | self.mi_label = QtWidgets.QLabel(self.centralwidget) 98 | self.mi_label.setGeometry(QtCore.QRect(710, 400, 81, 51)) 99 | font = QtGui.QFont() 100 | font.setFamily("Javanese Text") 101 | font.setPointSize(18) 102 | self.mi_label.setFont(font) 103 | self.mi_label.setStyleSheet("color: rgb(222, 222, 222);") 104 | self.mi_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) 105 | self.mi_label.setObjectName("mi_label") 106 | 107 | # RPM WIDGET 108 | self.tachometer = QtWidgets.QLabel(self.centralwidget) 109 | self.tachometer.setGeometry(QtCore.QRect(10, 60, 171, 61)) 110 | font = QtGui.QFont() 111 | font.setFamily("Lucida Calligraphy") 112 | font.setPointSize(32) 113 | self.tachometer.setFont(font) 114 | self.tachometer.setStyleSheet("color: rgb(248, 248, 248);") 115 | self.tachometer.setObjectName("rpm") 116 | 117 | #LOAD WIDGET 118 | self.load_meter = QtWidgets.QLabel(self.centralwidget) 119 | self.load_meter.setGeometry(QtCore.QRect(620, 60, 171, 61)) 120 | font = QtGui.QFont() 121 | font.setFamily("Lucida Calligraphy") 122 | font.setPointSize(32) 123 | self.load_meter.setFont(font) 124 | self.load_meter.setLayoutDirection(QtCore.Qt.LeftToRight) 125 | self.load_meter.setStyleSheet("color: rgb(248, 248, 248);") 126 | self.load_meter.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) 127 | self.load_meter.setObjectName("load") 128 | 129 | # MI WIDGET 130 | self.mi = QtWidgets.QLabel(self.centralwidget) 131 | self.mi.setGeometry(QtCore.QRect(620, 335, 171, 61)) 132 | font = QtGui.QFont() 133 | font.setFamily("Lucida Calligraphy") 134 | font.setPointSize(32) 135 | self.mi.setFont(font) 136 | self.mi.setLayoutDirection(QtCore.Qt.LeftToRight) 137 | self.mi.setStyleSheet("color: rgb(248, 248, 248);") 138 | self.mi.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) 139 | self.mi.setObjectName("mi") 140 | 141 | # GAL WIDGET 142 | self.gal = QtWidgets.QLabel(self.centralwidget) 143 | self.gal.setGeometry(QtCore.QRect(10, 335, 180, 61)) 144 | font = QtGui.QFont() 145 | font.setFamily("Lucida Calligraphy") 146 | font.setPointSize(32) 147 | self.gal.setFont(font) 148 | self.gal.setStyleSheet("color: rgb(248, 248, 248);") 149 | self.gal.setObjectName("gal") 150 | 151 | # REFILL BUTTON 152 | self.refill_button = QDoublePushButton(self.centralwidget) 153 | self.refill_button.setGeometry(QtCore.QRect(334, 385, 132, 45)) 154 | font = QtGui.QFont() 155 | font.setFamily("Javanese Text") 156 | font.setPointSize(14) 157 | self.refill_button.setFont(font) 158 | self.refill_button.setStyleSheet("color: rgb(248, 248, 248);") 159 | self.refill_button.setAutoDefault(False) 160 | self.refill_button.setDefault(False) 161 | self.refill_button.setFlat(False) 162 | self.refill_button.setObjectName("refill_button") 163 | # click once to refill 164 | self.refill_button.clicked.connect(self.refill) 165 | # double click to exit application 166 | self.refill_button.doubleClicked.connect(self.fullscreen) 167 | 168 | 169 | # general settings 170 | MainWindow.setCentralWidget(self.centralwidget) 171 | self.menubar = QtWidgets.QMenuBar(MainWindow) 172 | self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22)) 173 | self.menubar.setObjectName("menubar") 174 | MainWindow.setMenuBar(self.menubar) 175 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 176 | self.statusbar.setObjectName("statusbar") 177 | MainWindow.setStatusBar(self.statusbar) 178 | 179 | self.retranslateUi(MainWindow) 180 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 181 | 182 | # set initial values before receiving OBD signals. Essentially the "booting state" 183 | def retranslateUi(self, MainWindow): 184 | global gallons 185 | _translate = QtCore.QCoreApplication.translate 186 | MainWindow.setWindowTitle(_translate("MainWindow", "Dashboard")) 187 | self.spedometer.setText(_translate("MainWindow", "...")) 188 | self.rpm_label.setText(_translate("MainWindow", "RPM")) 189 | self.load_label.setText(_translate("MainWindow", "LOAD")) 190 | self.gal_label.setText(_translate("MainWindow", "GAL")) 191 | self.mi_label.setText(_translate("MainWindow", "MI")) 192 | self.tachometer.setText(_translate("MainWindow", "...")) 193 | self.load_meter.setText(_translate("MainWindow", "...")) 194 | self.mi.setText(_translate("MainWindow", "...")) 195 | self.gal.setText(_translate("MainWindow", "...")) 196 | self.refill_button.setText(_translate("MainWindow", "REFILL")) 197 | 198 | # warn when fuel level is low by changing the color of the refill button 199 | def warning(self): 200 | text = open("/home/pi/Dashboard/gallons.txt", "r") 201 | gallons = float(text.read()) 202 | text.close() 203 | if gallons <= 3.5: 204 | return "color: rgb(248, 248, 248); background-color: rgb(245, 27, 27);" 205 | else: 206 | return "color: rgb(248,248,248); background-color: rgb(29, 29, 29);" 207 | 208 | # set fuel level to max (not necessary if your fuel indicator is functional) 209 | def refill(self): 210 | gallons = 26.0 211 | text = open("/home/pi/Dashboard/gallons.txt", "w") 212 | text.write(str(gallons)) 213 | text.close() 214 | 215 | # query speed from OBD 216 | def speed(self): 217 | cmd = obd.commands.SPEED 218 | response = connection.query(cmd) 219 | return str(response.value.to('mph').magnitude).split(".")[0] 220 | 221 | # query RPM from OBD 222 | def rpm(self): 223 | cmd = obd.commands.RPM 224 | response = connection.query(cmd) 225 | return str(response.value.magnitude).split(".")[0] 226 | 227 | # calculate estimated fuel range 228 | def distance(self): 229 | text = open("/home/pi/Dashboard/gallons.txt", "r") 230 | gallons = float(text.read()) 231 | text.close() 232 | t = str(gallons * 12.12) 233 | return t.split(".")[0] 234 | 235 | # query engine load from OBD 236 | def load(self): 237 | cmd = obd.commands.ENGINE_LOAD 238 | response = connection.query(cmd) 239 | return str(response.value.magnitude).split(".")[0] + "%" 240 | 241 | # calculate gasoline usage and return new fuel level 242 | def gas(self): 243 | # fetch the global gallons variable we read from the text document 244 | text = open("/home/pi/Dashboard/gallons.txt", "r+") 245 | g = float(text.read()) 246 | 247 | # speed-based burn function 248 | def burn(speed): 249 | gpm = 0.0825 250 | miles_traveled = speed / 36000 251 | if miles_traveled == 0: 252 | miles_traveled = 0.000001 253 | fuel_burnt = gpm * miles_traveled 254 | return float(fuel_burnt) 255 | 256 | 257 | burnt = burn(float(self.speed())) 258 | 259 | # update text document storing fuel level 260 | g -= burnt 261 | g = str(g) 262 | text.seek(0) 263 | text.write(g) 264 | text.close() 265 | 266 | # rounding for the display (and not for the calculations) ensures more accuracy 267 | integer = g.split(".")[0] 268 | decimal = g.split(".")[1][0:2] 269 | g = integer + "." + decimal 270 | 271 | return g 272 | 273 | # enter or exit fullscreen 274 | def fullscreen(self): 275 | sys.exit() 276 | 277 | # update all values 278 | def update(self): 279 | try: 280 | gallons = self.gas() 281 | speed = self.speed() 282 | rpm = self.rpm() 283 | distance = self.distance() 284 | load = self.load() 285 | warning = self.warning() 286 | self.spedometer.setText(speed) 287 | self.gal.setText(gallons) 288 | self.tachometer.setText(rpm) 289 | self.mi.setText(distance) 290 | self.load_meter.setText(load) 291 | self.refill_button.setStyleSheet(warning) 292 | os.system("xset dpms force on") 293 | except: 294 | print("Car is turned off") 295 | os.system("xset dpms force off") 296 | time.sleep(3) 297 | global connection 298 | connection = obd.OBD() 299 | # connection = obd.OBD(portstr='/dev/pts/2') 300 | 301 | 302 | # call our main loop 303 | if __name__ == "__main__": 304 | # make sure bluetooth ELM327 device is connected 305 | stdoutdata = sp.getoutput('hcitool con') 306 | while '00:1D:A5:06:25:63' not in stdoutdata.split(): 307 | print('ELM327 Device Not Yet Paired...') 308 | os.system('sudo systemctl start bluetooth && sudo rfkill unblock bluetooth && sudo bluetoothctl pair 00:1D:A5:06:25:63 && sudo bluetoothctl trust 00:1D:A5:06:25:63 && echo Paired') 309 | cmd = "sudo rfcomm connect /dev/rfcomm0 {} {} &".format('00:1D:A5:06:25:63', 1) 310 | conn = sp.Popen(cmd, shell=True) 311 | stdoutdata = sp.getoutput('hcitool con') 312 | else: 313 | pass 314 | 315 | connection = obd.OBD() 316 | 317 | while connection.status() not in [OBDStatus.CAR_CONNECTED, OBDStatus.OBD_CONNECTED, OBDStatus.ELM_CONNECTED]: 318 | print('Waiting for OBD-II Connection to be Established...') 319 | time.sleep(3) 320 | connection = obd.OBD() 321 | # connection = obd.OBD(portstr='/dev/pts/2') 322 | else: 323 | pass 324 | 325 | app = QtWidgets.QApplication(sys.argv) 326 | MainWindow = QtWidgets.QMainWindow() 327 | ui = Ui_MainWindow() 328 | ui.setupUi(MainWindow) 329 | MainWindow.showFullScreen() 330 | 331 | timer = QtCore.QTimer() 332 | timer.timeout.connect(ui.update) 333 | timer.start(100) 334 | 335 | sys.exit(app.exec_()) 336 | -------------------------------------------------------------------------------- /gallons.txt: -------------------------------------------------------------------------------- 1 | 26.0 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/murrasource/digital_dash/9ac97dbef1885620e5f37b1ee5ad96297f581a7e/logo.png --------------------------------------------------------------------------------