├── ELM327.py ├── ELM327_Dash.py ├── README.md ├── full_dash.qml └── full_gauge.py /ELM327.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import datetime 3 | import time 4 | import sys 5 | import os 6 | import re 7 | from openpyxl import Workbook 8 | from openpyxl import load_workbook 9 | import binascii 10 | 11 | IDS=[] 12 | PIDS=[] 13 | PID_LIST=[] 14 | MODES=['01','02','03','09'] 15 | ELM_PROMPT = '>' 16 | root_dir = os.getcwd() 17 | minID=500 18 | maxID=2048 19 | minPID=0 20 | maxPID=255 21 | """CAN-IDS to Loop over""" 22 | for i in range(minID,maxID): 23 | i=(hex(i))[2:] 24 | i=format(i, '>02') 25 | IDS.append(i) 26 | """PIDS to Loop over""" 27 | for i in range(minPID,maxPID): 28 | i=(hex(i))[2:] 29 | i=format(i, '>02') 30 | PIDS.append(i) 31 | 32 | print '\nTool_SW_Vers: ' 33 | version=os.path.basename(__file__) 34 | print version 35 | print 'developed by Sebastian Kienitz' 36 | 37 | #LOGGING----------------------------------- 38 | file_name='INIT Log File_' + version+time.strftime("%Y_%m_%d_%Hh_%Mm") 39 | te = open(file_name + '.txt','w') # File where you need to keep the logs 40 | print 'Log File: ' + file_name + '_' + time.strftime("%Y_%m_%d_%Hh_%Mm") + '.txt' 41 | 42 | #------FUNCTIONS-------------------------------------------------------------------- 43 | def byte_to_binary(n): 44 | return ''.join(str((n & (1 << i)) and 1) for i in reversed(range(8))) 45 | 46 | def hex_to_binary(h): 47 | return ''.join(byte_to_binary(ord(b)) for b in binascii.unhexlify(h)) 48 | 49 | def send_cmd(cmd): 50 | data='' 51 | cmd += "\r" # terminate 52 | s.send(cmd) 53 | i=0 54 | while True: 55 | data=data+s.recv(64) 56 | i=i+1 57 | print i 58 | if (data.endswith(ELM_PROMPT) or len(data)>128 or i>10): 59 | te.write(data) 60 | print data 61 | if len(data)>128: 62 | s.send("\r") 63 | break 64 | # remove the prompt character 65 | data = data[:-1] 66 | # splits into lines while removing empty lines and trailing spaces 67 | data = data.replace('\r','') 68 | data = data [len(cmd)-1:] 69 | """time.sleep(1)""" 70 | return data 71 | 72 | """INIT DONGLE-------------------------------------""" 73 | s = socket.socket() 74 | host = '192.168.0.10' # needs to be in quote 75 | port = 35000 76 | s.connect((host, port)) 77 | send_cmd("\r") 78 | send_cmd("ATSP0") 79 | send_cmd("ATD") 80 | protocol=send_cmd("ATDPN")[1:] 81 | te.write('Protocol: ' + protocol+'\n') 82 | te.write(send_cmd("ATI")+'\n') 83 | send_cmd("0100") 84 | te.write('Headers:') 85 | te.write(send_cmd("ATH0")+'\n') 86 | print send_cmd("ATAT2") 87 | print send_cmd('ATSH' + IDS[len(IDS)-1]) 88 | 89 | #--------------------------MAIN--------------------------------------------------------------------------------------------------------------- 90 | print ('Battery Voltage:' + send_cmd('ATRV')) 91 | te.write('Battery Voltage:' + send_cmd('ATRV')+'\n') 92 | VIN=send_cmd('0902') 93 | te.write('VIN ' + VIN+'\n') 94 | if not (os.path.isfile(root_dir + '/' + 'PID.xlsx')): 95 | #VEHICLE-PRESCAN---------------------------------------------------- 96 | print ('Learning valid PIDs ..................') 97 | for ID in IDS: 98 | print ID 99 | response =send_cmd('ATSH' + ID) 100 | print 'ATSH' + ID 101 | print response 102 | time.sleep(2) 103 | response=send_cmd("0100") 104 | if not('NO DAT' in response) and ('41' in response[:4]): 105 | for MODE in MODES: 106 | for PID in PIDS: 107 | response=send_cmd(MODE + PID) 108 | if not('NO DAT' in response) and ('4'+ MODE[1]in response[:4]): 109 | print 'VALID OBD RESPONSE FOUND' 110 | PID_LIST.append([ID,MODE,PID,response]) 111 | 112 | te.write(str(PID_LIST)) 113 | print 'No PID-LIST stored, saving list into PID.xlsx ..................' 114 | wb = Workbook() 115 | ws = wb.active 116 | print str(PID_LIST) 117 | for PID in PID_LIST: 118 | print 'SAVING LINE TO EXCEL: ' 119 | print PID[0] + '-' + PID[1] + '-' + PID[2] + '-' + PID[3] 120 | ws.append([PID[0], PID[1], PID[2],PID[3]]) 121 | wb.save('PID.xlsx') 122 | else: 123 | print 'PID-LIST FOUND! ..................' 124 | wb=load_workbook(root_dir + '/' + 'PID.xlsx') 125 | first_sheet=wb.get_sheet_names()[0] 126 | ws=wb.get_sheet_by_name(first_sheet) 127 | for row in ws.iter_rows(min_row=1,max_col=3): 128 | line=[] 129 | for cell in row: 130 | if not(str(cell.value)=='None'): 131 | line.append(str(cell.value)) 132 | PID_LIST.append(line) 133 | """delete last row cause it contains nothing""" 134 | PID_LIST=PID_LIST[0:len(PID_LIST)-6] 135 | te.write(str(PID_LIST)) 136 | te.close() 137 | 138 | #LOGGING----------------------------------- 139 | file_name='MEASUREMENT Log File_' + version+time.strftime("%Y_%m_%d_%Hh_%Mm") 140 | te = open(file_name + '.txt','w') # File where you need to keep the logs 141 | print 'MEASUREMENT Log File: ' + file_name + '_' + time.strftime("%Y_%m_%d_%Hh_%Mm") + '.txt' 142 | 143 | print 'Start Measurement of learned PIDs into Excel ..................' 144 | wb = Workbook() 145 | ws = wb.active 146 | xls_lines=0 147 | measurement_number=0 148 | while True: 149 | for PID in PID_LIST: 150 | send_cmd("ATSH" + PID[0]) 151 | response=send_cmd(PID[1]+PID[2]) 152 | ws.append([datetime.datetime.now(),PID[0], PID[1], PID[2],response]) 153 | xls_lines=xls_lines+1 154 | if xls_lines>1000: 155 | wb.save(file_name + '_' + str(measurement_number) +'.xlsx') 156 | measurement_number=measurement_number+1 157 | wb = Workbook() 158 | ws = wb.active 159 | xls_lines=0 160 | te.close() 161 | -------------------------------------------------------------------------------- /ELM327_Dash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import socket 3 | import datetime 4 | import time 5 | import sys 6 | import os 7 | 8 | MODE='01' 9 | ELM_PROMPT = '>' 10 | root_dir = os.getcwd() 11 | strings = [] 12 | 13 | 14 | print '\nTool_SW_Vers: ' 15 | version=os.path.basename(__file__) 16 | print version 17 | print 'developed by Sebastian Kienitz' 18 | 19 | #------FUNCTIONS-------------------------------------------------------------------- 20 | 21 | def send_cmd(cmd): 22 | data='' 23 | cmd += "\r" # terminate 24 | s.send(cmd) 25 | i=0 26 | while True: 27 | data=data+s.recv(64) 28 | i=i+1 29 | if (data.endswith(ELM_PROMPT) or len(data)>128 or i>10): 30 | if len(data)>128: 31 | s.send("\r") 32 | break 33 | # remove the prompt character 34 | data = data[:-1] 35 | # splits into lines while removing empty lines and trailing spaces 36 | data = data.replace('\r','') 37 | data = data [len(cmd)-1:] 38 | """time.sleep(1)""" 39 | return data 40 | 41 | def get_dec(PID): 42 | response=send_cmd(MODE + PID) 43 | response=response.replace('41','') 44 | response=response.replace(PID,'') 45 | response=response.replace(' ','') 46 | if(len(response)>2): 47 | A=response[:-2] 48 | B=response[2:] 49 | else: 50 | A=response 51 | B='00' 52 | return [int(A, 16),int(B,16)] 53 | 54 | """INIT DONGLE-------------------------------------""" 55 | s = socket.socket() 56 | host = '192.168.0.10' # needs to be in quote 57 | port = 35000 58 | s.connect((host, port)) 59 | send_cmd("\r") 60 | send_cmd("ATSP0") 61 | send_cmd("ATD") 62 | protocol=send_cmd("ATDPN")[1:] 63 | send_cmd("0100") 64 | send_cmd("ATAT2") 65 | 66 | 67 | 68 | #--------------------------MAIN--------------------------------------------------------------------------------------------------------------- 69 | 70 | while(True): 71 | strings = [] 72 | strings.append('Battery Voltage:' + send_cmd('ATRV')) 73 | strings.append('Load: ' + str(get_dec('04')[0]/2.55) + '%') 74 | strings.append('Coolant: ' + str(get_dec('05')[0]-40) + ' deg C') 75 | strings.append('RPM: ' + str((256*get_dec('0C')[0] + get_dec('0C')[1])/4) + ' rpm') 76 | strings.append('Timing Adv: ' + str(get_dec('0E')[0]/2-64) + ' deg') 77 | strings.append('Intake Air Temp: ' + str(get_dec('0F')[0]-40) + ' deg C') 78 | strings.append('MAF: ' + str((256*get_dec('10')[0] + get_dec('10')[1])/100) + ' g/sec') 79 | strings.append('Throttle: ' + str(100*get_dec('11')[0]/255) +'%') 80 | for string in strings: 81 | print (string + '\n') 82 | os.system('cls' if os.name == 'nt' else 'clear') 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ELM327-Wifi-OBDII-Excel-Logger-with-Python 2.7 2 | 3 | This a first draft of a ELM327 Wifi OBDII Logger which logs into excel. 4 | 5 | The communication to the device is established with Sockets. 6 | 7 | ## Getting Started 8 | 9 | 1.You need a Wifi ELM327 Adapter. (can be found cheap online) 10 | 2.Connect to its Wifi and run ELM327.py 11 | 3.If it doesnt connect check ports & IP of the Dongle via putty and change them in the script. 12 | (Mine has:host = '192.168.0.10' # needs to be in quote 13 | port = 35000) 14 | 15 | 16 | -------------------------------------------------------------------------------- /full_dash.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.4 3 | import QtQuick.Controls.Styles 1.4 4 | import QtQuick.Extras 1.4 5 | import QtQuick.Extras.Private 1.0 6 | 7 | Rectangle { 8 | width: 1000 9 | height: 1000 10 | color: "#000000" 11 | 12 | CircularGauge { 13 | objectName: "CircularGauge_RPM" 14 | id: circulargauge_rpm 15 | property real gauge_value: 800.0 16 | value: gauge_value 17 | maximumValue: 8000.0 // Largest Value 18 | minimumValue: 0.0 // Smallest Value 19 | style: CircularGaugeStyle { 20 | id: style 21 | tickmarkStepSize: 1000.0 // Tick Marks 22 | 23 | tickmark: Rectangle { 24 | visible: styleData.value < 8000 || styleData.value % 1000 == 0 25 | implicitWidth: outerRadius * 0.02 26 | antialiasing: true 27 | implicitHeight: outerRadius * 0.06 28 | color: styleData.value >= 8000 ? "#ff0000" : "#ff0000" 29 | } 30 | 31 | minorTickmark: Rectangle { 32 | visible: styleData.value < 8000 33 | implicitWidth: outerRadius * 0.01 34 | antialiasing: true 35 | implicitHeight: outerRadius * 0.03 36 | color: "#ff0000" 37 | } 38 | 39 | tickmarkLabel: Text { 40 | font.pixelSize: Math.max(6, outerRadius * 0.1) 41 | text: styleData.value 42 | color: styleData.value >= 8000 ? "#ff0000" : "#ff0000" 43 | antialiasing: true 44 | } 45 | 46 | needle: Rectangle { 47 | y: outerRadius * 0.15 48 | implicitWidth: outerRadius * 0.03 49 | implicitHeight: outerRadius * 1.1 50 | antialiasing: true 51 | color: "#ff0000" 52 | } 53 | 54 | foreground: Item { 55 | Rectangle { 56 | width: outerRadius * 0.2 57 | height: width 58 | radius: width / 2 59 | color: "#b2b2b2" 60 | anchors.centerIn: parent 61 | } 62 | } 63 | } 64 | } 65 | 66 | Text { 67 | text: "RPM [1/min]" 68 | font.family: "Helvetica" 69 | font.pointSize: 18 70 | color: "red" 71 | anchors.top: circulargauge_rpm.bottom 72 | anchors.horizontalCenter: circulargauge_rpm.horizontalCenter 73 | } 74 | 75 | CircularGauge { 76 | objectName: "CircularGauge_Coolant" 77 | id: circulargauge_coolant 78 | property real gauge_value: 90.0 79 | anchors.left: circulargauge_rpm.right 80 | value: gauge_value 81 | maximumValue: 130.0 // Largest Value 82 | minimumValue: 0.0 // Smallest Value 83 | style: CircularGaugeStyle { 84 | id: style 85 | tickmarkStepSize: 10.0 // Tick Marks 86 | 87 | tickmark: Rectangle { 88 | visible: styleData.value < 8000 || styleData.value % 1000 == 0 89 | implicitWidth: outerRadius * 0.02 90 | antialiasing: true 91 | implicitHeight: outerRadius * 0.06 92 | color: styleData.value >= 8000 ? "#ff0000" : "#ff0000" 93 | } 94 | 95 | minorTickmark: Rectangle { 96 | visible: styleData.value < 8000 97 | implicitWidth: outerRadius * 0.01 98 | antialiasing: true 99 | implicitHeight: outerRadius * 0.03 100 | color: "#ff0000" 101 | } 102 | 103 | tickmarkLabel: Text { 104 | font.pixelSize: Math.max(6, outerRadius * 0.1) 105 | text: styleData.value 106 | color: styleData.value >= 8000 ? "#ff0000" : "#ff0000" 107 | antialiasing: true 108 | } 109 | 110 | needle: Rectangle { 111 | y: outerRadius * 0.15 112 | implicitWidth: outerRadius * 0.03 113 | implicitHeight: outerRadius * 1.1 114 | antialiasing: true 115 | color: "#ff0000" 116 | } 117 | 118 | foreground: Item { 119 | Rectangle { 120 | width: outerRadius * 0.2 121 | height: width 122 | radius: width / 2 123 | color: "#b2b2b2" 124 | anchors.centerIn: parent 125 | } 126 | } 127 | } 128 | } 129 | 130 | Text { 131 | text: "Coolant [deg C]" 132 | font.family: "Helvetica" 133 | font.pointSize: 18 134 | color: "red" 135 | anchors.top: circulargauge_coolant.bottom 136 | anchors.horizontalCenter: circulargauge_coolant.horizontalCenter 137 | } 138 | 139 | CircularGauge { 140 | objectName: "CircularGauge_Load" 141 | id: circulargauge_load 142 | property real gauge_value: 0.0 143 | anchors.left: circulargauge_coolant.right 144 | value: gauge_value 145 | maximumValue: 100.0 // Largest Value 146 | minimumValue: 0.0 // Smallest Value 147 | style: CircularGaugeStyle { 148 | id: style 149 | tickmarkStepSize: 10.0 // Tick Marks 150 | 151 | tickmark: Rectangle { 152 | visible: styleData.value < 8000 || styleData.value % 1000 == 0 153 | implicitWidth: outerRadius * 0.02 154 | antialiasing: true 155 | implicitHeight: outerRadius * 0.06 156 | color: styleData.value >= 8000 ? "#ff0000" : "#ff0000" 157 | } 158 | 159 | minorTickmark: Rectangle { 160 | visible: styleData.value < 8000 161 | implicitWidth: outerRadius * 0.01 162 | antialiasing: true 163 | implicitHeight: outerRadius * 0.03 164 | color: "#ff0000" 165 | } 166 | 167 | tickmarkLabel: Text { 168 | font.pixelSize: Math.max(6, outerRadius * 0.1) 169 | text: styleData.value 170 | color: styleData.value >= 8000 ? "#ff0000" : "#ff0000" 171 | antialiasing: true 172 | } 173 | 174 | needle: Rectangle { 175 | y: outerRadius * 0.15 176 | implicitWidth: outerRadius * 0.03 177 | implicitHeight: outerRadius * 1.1 178 | antialiasing: true 179 | color: "#ff0000" 180 | } 181 | 182 | foreground: Item { 183 | Rectangle { 184 | width: outerRadius * 0.2 185 | height: width 186 | radius: width / 2 187 | color: "#b2b2b2" 188 | anchors.centerIn: parent 189 | } 190 | } 191 | } 192 | 193 | } 194 | 195 | Text { 196 | text: "Load [%]" 197 | font.family: "Helvetica" 198 | font.pointSize: 18 199 | color: "red" 200 | anchors.top: circulargauge_load.bottom 201 | anchors.horizontalCenter: circulargauge_load.horizontalCenter 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /full_gauge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import time 4 | from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSignal 5 | from PyQt5.QtWidgets import QApplication 6 | from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType, QQmlEngine, QQmlComponent 7 | from PyQt5 import QtCore, QtGui 8 | from PyQt5.QtQuick import QQuickView 9 | import time 10 | 11 | if __name__ == "__main__": 12 | app = QApplication(sys.argv) 13 | view = QQuickView() 14 | view.setSource(QUrl('full_dash.qml')) 15 | gauge_rpm=view.findChild(QObject,'CircularGauge_RPM') 16 | gauge_coolant=view.findChild(QObject,'CircularGauge_Coolant') 17 | gauge_load=view.findChild(QObject,'CircularGauge_Load') 18 | view.showFullScreen() 19 | gauge_rpm.setProperty('gauge_value',4000) 20 | gauge_coolant.setProperty('gauge_value',90) 21 | gauge_load.setProperty('gauge_value',0) 22 | sys.exit(app.exec_()) 23 | --------------------------------------------------------------------------------