I’ve always had an interest in getting the best fuel economy out of cars that I drove. On June 14, 2006, I blogged about my Toyota Echo and its gas mileage. I’ve often wanted to buy a device like ScanGauge so I could see the instantaneous fuel economy I was getting.
A few weeks ago, the Raspberry.org site’s main item had to do with carputers. That, along with an article about the Quite Rubbish clock got me to thinking, how about a Raspberry Pi MPG reader?
Here’s the final result, well final, there is no case and I’m also thinking of adding various buttons to vary the output of the display. But for the moment…
The top line lists the average of the last 3 seconds. The bottom line the average for the entire trip, followed by the duration of the trip in minutes. Since I’m in Canada, where fuel economy is expressed in litres consumed per 100 km driven, that’s what it is set up for. However, it would be fairly trivial to change the formula and output based on the user’s preference.
The code is based on Martin O’Hanlon’s obd_capture.py. However, I made significant changes in that the data is being captured into a SQLite3 database (which is actually used to compute the 3 second average). Furthermore, I included the output to the LCD in this class as well. (Yeah, I know, you shouldn’t be mixing data with user interface, but hey, I’m not getting paid for this, so I can do whatever I want.)
Every time the car starts, a new database with a unique name is created. It takes about 15 seconds for the Pi to boot and another 5 seconds for the Python script to start running. This database can be retrieved later for further analysis of the car’s performance.
The fuel economy is derived from the values given by the ELM327 (about $12 from China) OBDII reader for MAF (Mass Airflow meter) and VSS (vehicle speed). The formula for litres per 100 km is:
(3600 * MAF)/(9069.90 * VSS)
Credit for this formula should go to Bruce Lightner, who is probably one of the most knowledgeable people on this planet when it comes to OBDII (and moon rocks and more stuff)
The output goes to a Nokia 5110 LCD (Deal Extreme $5.60). In order to get a fairly large readout for driver visibility, I actually created an image in Python and then place that image on the LCD as a whole.
The OBDII connector in my car points downwards, and that might interfere with the driver feet, so I’ve ordered an extension cable from China for a few dollars which will alleviate this problem. This cable exits at right angles from the connector.
If you intend to build this device using the Nokia 5110 LCD, you’ll need to read my previous post regarding this device.
For Martin O’Hanlon excellent code, and related libraries type this at the command prompt:
sudo apt-get install python-serial
sudo apt-get install git-core
cd ~
git clone https://github.com/martinohanlon/pyobd
cd pyobd
python obd_capture.py
Here is my version of obd_capture.py:
1: #!/usr/bin/env python
2:
3: import obd_io
4: import serial
5: import platform
6: import obd_sensors
7: from datetime import datetime
8: from PIL import Image,ImageDraw,ImageFont
9: import ImageOps
10: import nokiaSPI
11: import time
12: import os
13: import sqlite3
14: #from datetime import timedelta
15:
16: from obd_utils import scanSerial
17:
18: class OBD_Capture():
19: def __init__(self):
20: self.port = None
21: localtime = time.localtime(time.time())
22:
23: def connect(self):
24: portnames = scanSerial()
25: print portnames
26: for port in portnames:
27: self.port = obd_io.OBDPort(port, None, 2, 2)
28: if(self.port.State == 0):
29: self.port.close()
30: self.port = None
31: else:
32: break
33:
34: if(self.port):
35: print "Connected to "+self.port.port.name
36:
37: def is_connected(self):
38: return self.port
39:
40: def nokiprint(self,cShortTerm,cLongTerm,cLongTermMinutes):
41: cShortTerm = cShortTerm[:4]
42: cLongTerm = cLongTerm[:4]
43: noki.cls()
44: im = Image.new('1', (84,48))
45: draw = ImageDraw.Draw(im)
46: print cShortTerm
47: draw.text((0,0),cShortTerm, font=font, fill=1)
48: draw.text((0,24),cLongTerm, font=font, fill=1)
49: draw.text((54,7),"3 s", font=fontsmall, fill=1)
50: draw.text((54,31),cLongTermMinutes + " m", font=fontsmall, fill=1)
51: # Copy it to the display
52: noki.show_image(im)
53: #noki.next_row()
54:
55: def ComputeFuelConsumption(self):
56: nCurrentTime = time.time()
57:
58: try:
59: nStart = nCurrentTime - 20
60: cLimit = " and time_read > " + str(nStart) + " order by time_read desc limit 6"
61: cursor.execute('''SELECT maf,speed from SensorReadings where speed > "0" and maf > "0" and rpm != "NODATA" ''' + cLimit)
62: data = cursor.fetchall()
63: except sqlite3.OperationalError,msg:
64: return msg
65: #print len(data)
66: if (len(data) > 0):
67: nFuelConsumption = 0
68: for x in range(0,len(data)):
69: nFuelConsumption += (3600 * float(data[x][0]))/(9069.90 * float(data[x][1]))
70:
71: nAvgFuelConsumption = nFuelConsumption/len(data)
72: print nAvgFuelConsumption
73: print type(nAvgFuelConsumption)
74: #print data[x][0],data[x][1],data[x][2]
75: return "{:5.2f}".format(nAvgFuelConsumption).lstrip()
76: else:
77: return "No data"
78:
79: #print nAvgFuelConsum
80:
81: def is_number(self,DataToTest):
82: try:
83: float(DataToTest)
84: return True
85: except ValueError:
86: return False
87:
88:
89:
90:
91: def capture_data(self):
92: #Creating new database
93: for kounter in range(10000):
94: cKounter = "{0:05d}".format(kounter)
95: cNewDatabase = "obdii" + cKounter + ".db"
96: #print cNewDatabase
97: if not (os.path.exists(cNewDatabase)):
98: #print "New database name: " + cNewDatabase
99: break
100:
101: global conn
102: global cursor
103: conn = sqlite3.connect(cNewDatabase)
104: cursor = conn.cursor()
105:
106: #Find supported sensors - by getting PIDs from OBD
107: # its a string of binary 01010101010101
108: # 1 means the sensor is supported
109: self.supp = self.port.sensor(0)[1]
110: self.supportedSensorList = []
111: self.unsupportedSensorList = []
112:
113:
114: # loop through PIDs binary
115: for i in range(0, len(self.supp)):
116: if self.supp[i] == "1":
117: # store index of sensor and sensor object
118: self.supportedSensorList.append([i+1, obd_sensors.SENSORS[i+1]])
119: else:
120: self.unsupportedSensorList.append([i+1, obd_sensors.SENSORS[i+1]])
121:
122: sqlCreateTable = "CREATE TABLE SensorReadings (time_read real, "
123: sqlInsertTemplate = "INSERT INTO SensorReadings(time_read, "
124:
125: for supportedSensor in self.supportedSensorList:
126: #print "supported sensor index = " + str(supportedSensor[0]) + " " + str(supportedSensor[1].shortname)
127: sqlCreateTable += str(supportedSensor[1].shortname) + " text,"
128: sqlInsertTemplate += str(supportedSensor[1].shortname) + ","
129:
130: sqlCreateTable = sqlCreateTable[:sqlCreateTable.rfind(",")] + ")"
131: #print sqlCreateTable
132: try:
133: cursor.execute(sqlCreateTable)
134: conn.commit()
135: cursor.execute('''CREATE INDEX time_read_index on SensorReadings(time_read)''')
136: cMessage = "Database " + cNewDatabase + " created..."
137: except sqlite3.OperationalError,msg:
138: cMessage = msg
139: noki.cls()
140: noki.text(cMessage,wrap=True)
141:
142: sqlInsertTemplate = sqlInsertTemplate[:sqlInsertTemplate.rfind(",")] + ") VALUES ("
143: #print sqlInsertTemplate
144:
145: time.sleep(3)
146:
147: if(self.port is None):
148: return None
149:
150: #Loop until Ctrl C is pressed
151: try:
152: nRunningTotalFuelConsumption = 0
153: nStartTime = time.time()
154: x = 0
155: while True:
156: current_time = time.time()
157: #current_time = str(localtime.hour)+":"+str(localtime.minute)+":"+str(localtime.second)+"."+str(localtime.microsecond)
158: #log_string = current_time + "\n"
159: sqlInsert = sqlInsertTemplate + '"' + str(current_time) + '",'
160: results = {}
161: for supportedSensor in self.supportedSensorList:
162: sensorIndex = supportedSensor[0]
163: #print sensorIndex
164: (name, value, unit) = self.port.sensor(sensorIndex)
165: #log_string += name + " = " + str(value) + " " + str(unit) + "\n"
166: #print value,type(value)
167: sqlInsert += '"' + str(value) + '",'
168:
169:
170: sqlInsert = sqlInsert[:sqlInsert.rfind(",")] + ")"
171: #print sqlInsert
172:
173: try:
174: cursor.execute(sqlInsert)
175: conn.commit()
176: except sqlite3.OperationalError,msg:
177: noki.cls()
178: noki.text(msg,wrap=True)
179: continue
180:
181: cFuelConsumption = self.ComputeFuelConsumption()
182: if (cFuelConsumption != "No data"):
183: x += 1
184: if (x > 0):
185: if (self.is_number):
186: nRunningTotalFuelConsumption += float(cFuelConsumption)
187: nTripAverage = nRunningTotalFuelConsumption/x
188: cTripAverage = "{:5.2f}".format(nTripAverage).lstrip()
189: else:
190: cTripAverage = "Nodata"
191:
192: cDurationInMinutes = "{:3.0f}".format((current_time - nStartTime)/60).lstrip()
193: self.nokiprint(cFuelConsumption,cTripAverage,cDurationInMinutes)
194:
195: #print log_string,
196: #time.sleep(0.5)
197:
198:
199: except KeyboardInterrupt:
200: self.port.close()
201: print("stopped")
202:
203: if __name__ == "__main__":
204: font = ImageFont.truetype("/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",26)
205: fontsmall = ImageFont.truetype("/usr/share/fonts/truetype/freefont/FreeSansBold.ttf", 16)
206: # New b-w image
207: im = Image.new('1', (84,48))
208: noki = nokiaSPI.NokiaSPI(brightness=268) # create display device
209: noki.cls()
210: noki.text("Initializing..",wrap=True)
211:
212:
213: o = OBD_Capture()
214: o.connect()
215: time.sleep(3)
216: if not o.is_connected():
217: print "Not connected"
218: noki.cls()
219: noki.text("Error: Not connected to OBDII...",wrap=True)
220: time.sleep(10)
221: noki.set_brightness(0)
222: noki.cls()
223: exit()
224: else:
225: o.capture_data()
In order for the Python script to automatically start when the Pi boots up, I added this line to /etc/rc.local :
(cd /home/pi/pythonprogs;python obd_capture.py)&
That line should only be added when you are relatively confident it will all work. First, for testing, you will need to run a network cable to your car (or tap in wirelessly, if you are within range). This is what my setup looked like, during early testing:
The blue cable was networking, the yellow powered the Pi. Later on, I removed the power cable and ran off the car’s battery. It is quite tricky to have to program for something that has to run headless and detached from everything. So when making changes to your code, check, check and double check again.
I’ve also ordered from MausBerryCircuits.com an illuminated LED shutdown switch, which will gracefully shut down the Pi before power is removed, reducing the chance of corrupting the SD card.
That’s it!
Update 2013-08-31:
I’ve optimized the fuel consumption function and made SQLite do the average calculation, alleviating the need do to the calculation in a Python ‘for’ loop. Also, made the SQLite connection and cursor class attributes rather than global variables, which are generally considered undesirable.
No comments:
Post a Comment