View Javadoc

1   /* Open Log Viewer
2    *
3    * Copyright 2011 Fred Cooke
4    *
5    * This file is part of the OpenLogViewer project.
6    *
7    * OpenLogViewer software is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU General Public License as published by
9    * the Free Software Foundation, either version 3 of the License, or
10   * (at your option) any later version.
11   *
12   * OpenLogViewer software is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Public License for more details.
16   *
17   * You should have received a copy of the GNU General Public License
18   * along with any OpenLogViewer software.  If not, see http://www.gnu.org/licenses/
19   *
20   * I ask that if you make any changes to this file you fork the code on github.com!
21   *
22   */
23  package org.diyefi.openlogviewer.decoder;
24  
25  
26  import java.io.File;
27  import java.io.FileInputStream;
28  import java.io.IOException;
29  import java.util.Arrays;
30  
31  import org.diyefi.openlogviewer.decoder.LogField.types;
32  import org.diyefi.openlogviewer.genericlog.GenericLog;
33  
34  
35  /**
36   * This function takes a binary log file, plucks FreeEMS packets out of it,
37   * filters for standard packets and parses them into fields with appropriate scaling.
38   * 
39   * @todo 
40   */
41  public class FreeEMSBin implements Runnable { // implements runnable to make this class theadable
42  
43  	private final int minimumPacketLength = 3; // Flag byte, payload id word, no payload - defined by protocol
44  	private final int maximumPacketLength = 0x0820; // Buffer size on FreeEMS vanilla, take this from config file eventually
45  	private final int payloadIDToParse = 0x0191; // Default freeems basic log, get this from config file and maybe default to this
46  	// NOTE, standard supported log types require their own structure definition in the code, override-able via files, matched looked for first, then local, then fall back to code
47  
48  	private final short ESCAPE_BYTE = 0xBB;         // Used as an unsigned byte
49  	private final short START_BYTE = 0xAA;          // Used as an unsigned byte
50  	private final short STOP_BYTE = 0xCC;           // Used as an unsigned byte
51  	private final short ESCAPED_ESCAPE_BYTE = 0x44; // Used as an unsigned byte
52  	private final short ESCAPED_START_BYTE = 0x55;  // Used as an unsigned byte
53  	private final short ESCAPED_STOP_BYTE = 0x33;   // Used as an unsigned byte
54  
55  	private boolean startFound;
56  	private File logFile;
57  	private FileInputStream logStream;
58  	private short[] packetBuffer; // For use as an unsigned byte
59  	private GenericLog decodedLog;
60  	private Thread t;
61  	private int packetLength; // Track packet length
62  
63  	String[] coreStatusAFlagNames = {
64  			"CS-FuelPumpPrime",
65  			"CS-unused1",
66  			"CS-unused2",
67  			"CS-unused3",
68  			"CS-unused4",
69  			"CS-unused5",
70  			"CS-unused6",
71  			"CS-unused7"
72  		};
73  
74  	String[] decoderFlagsFlagNames = {
75  			"DF-CombustionSync",
76  			"DF-CrankSync",
77  			"DF-CamSync",
78  			"DF-LAST_TIMESTAMP_VALID",
79  			"DF-LAST_PERIOD_VALID",
80  			"DF-LAST_MATCH_VALID",
81  			"DF-Spare-6",
82  			"DF-Spare-7"
83  		};
84  	
85  	String[] flaggableFlagsNames = {
86  			"FF-callsToUISRs",               // to ensure we aren't accidentally triggering unused ISRs.
87  			"FF-lowVoltageConditions",       // low voltage conditions.
88  			"FF-decoderSyncLosses",          // Number of times cam, crank or combustion sync is lost.
89  			"FF-decoderSyncCorrections",     // Definite decoder syncs found while already synced in a different position.
90  			"FF-decoderSyncStateClears",     // Sync loss called when not synced yet, thus discarding data and preventing sync.
91  			"FF-serialNoiseErrors",          // Incremented when noise is detected
92  			"FF-serialFramingErrors",        // Incremented when a framing error occurs
93  			"FF-serialParityErrors",         // Incremented when a parity error occurs
94  			"FF-serialOverrunErrors",        // Incremented when overrun occurs (duplicated in KeyUserDebug below)
95  			"FF-serialEscapePairMismatches", // Incremented when an escape is found but not followed by an escapee
96  			"FF-serialStartsInsideAPacket",  // Incremented when a start byte is found inside a packet
97  			"FF-serialPacketsOverLength",    // Incremented when the buffer fills up before the end
98  			"FF-serialChecksumMismatches",   // Incremented when calculated checksum did not match the received one
99  			"FF-serialPacketsUnderLength",   // Incremented when a packet is found that is too short
100 			"FF-commsDebugMessagesNotSent",  // Incremented when a debug message can't be sent due to the TX buffer
101 			"FF-commsErrorMessagesNotSent"   // Incremented when an error message can't be sent due to the TX buffer
102 	};
103 
104 	private LogField[] fields = { // This should be read from a file at some point, as it's going to be flexible. See FreeEMS/src/inc/structs.h for definitive answers.
105 		// CoreVars struct contents:
106 		new LogField("IAT",  100),   // Inlet Air Temperature           : 0.0 -   655.35       (0.01 Kelvin (/100))
107 		new LogField("CHT",  100),   // Coolant / Head Temperature      : 0.0 -   655.35       (0.01 Kelvin (/100))
108 		new LogField("TPS",  640),   // Throttle Position Sensor        : 0.0 -   102.398438   (0.0015625 % (/640))
109 		new LogField("EGO",  32768), // Exhaust Gas Oxygen              : 0.0 -     1.99996948 (0.0000305175781 lambda (/32768))
110 		new LogField("MAP",  100),   // Manifold Absolute Pressure      : 0.0 -   655.35       (0.01 kPa (/100))
111 		new LogField("AAP",  100),   // Atmospheric Absolute Pressure   : 0.0 -   655.35       (0.01 kPa (/100))
112 		new LogField("BRV",  1000),  // Battery Reference Voltage       : 0.0 -    65.535      (0.001 Volts (/1000))
113 		new LogField("MAT",  100),   // Manifold Air Temperature        : 0.0 -   655.35       (0.01 Kelvin (/100))
114 		new LogField("EGO2", 32768), // Exhaust Gas Oxygen              : 0.0 -     1.99996948 (0.0000305175781 lambda (/32768))
115 		new LogField("IAP",  100),   // Intercooler Absolute Pressure   : 0.0 -   655.35       (0.01 kPa (/100))
116 		new LogField("MAF"),         // Mass Air Flow                   : 0.0 - 65535.0        (raw units from lookup)
117 		new LogField("DMAP"),        // Delta MAP kPa/second or similar : 0.0 - 65535.0        (raw units from lookup)
118 		new LogField("DTPS"),        // Delta TPS %/second or similar   : 0.0 - 65535.0        (raw units from lookup)
119 		new LogField("RPM", 2),      // Revolutions Per Minute (Calced) : 0.0 - 32767.5        (0.5 RPM (/2))
120 		new LogField("DRPM"),        // Delta RPM (Calced)              : 0.0 - 65535.0        (raw units from lookup)
121 		new LogField("DDRPM"),       // Delta Delta RPM (Calced)        : 0.0 - 65535.0        (raw units from lookup)
122 
123 		// DerivedVars struct contents:
124 		new LogField("LoadMain", 512),        // Configurable unit of load, scale same as map by default
125 		new LogField("VEMain", 512),          // Volumetric Efficiency in 0 - 128%
126 		new LogField("Lambda", 32768),        // Integral Lambda 0 - 2.0
127 		new LogField("AirFlow"),              // raw intermediate, remove
128 		new LogField("densityAndFuel"),       // raw intermediate, remove
129 		new LogField("BasePW", 1250),         // Raw PW before corrections 0 - ~52ms
130 		new LogField("ETE", (100.0/32768.0)), // Engine Temperature Enrichment percentage correction 0 - 200%
131 		new LogField("TFCTotal", 1250),       // Transient fuel correction PW (+/-)  0 - ~52ms
132 		new LogField("EffectivePW", 1250),    // Actual PW of fuel delivery 0 - ~52ms
133 		new LogField("IDT", 1250),            // PW duration before fuel flow begins 0 - ~52ms
134 		new LogField("RefPW", 1250),          // Reference electrical PW 0 - ~52ms
135 		new LogField("Advance", 50),          // Ignition advance (scaled degrees / oneDegree(currently 50) = degrees) 0 - ~64*
136 		new LogField("Dwell", 1250),          // Dwell period, s 0 - ~52ms
137 
138 		// KeyUserDebugs struct contents
139 		new LogField("tempClock"),     // Incremented once per log sent, to be moved to a char TODO
140 		new LogField("coreStatusA",  types.BITS8, coreStatusAFlagNames),    // Duplicated, migrate here, remove global var
141 		new LogField("decoderFlags", types.BITS8, decoderFlagsFlagNames),   // Various decoder state flags
142 		new LogField("flaggableFlags", types.BITS16, flaggableFlagsNames),///< Flags to go with our flaggables struct.
143 		new LogField("currentEvent",             types.UINT8), // Which input event was last to come in
144 		new LogField("syncLostWithThisID",       types.UINT8), // A unique identifier for the reason behind a loss of sync
145 		new LogField("syncLostOnThisEvent",      types.UINT8), // Where in the input pattern it all went very badly wrong
146 		new LogField("syncCaughtOnThisEvent",    types.UINT8), // Where in the input pattern that things started making sense
147 		new LogField("syncResetCalls",           types.UINT8), // Sum of losses, corrections and state clears
148 		new LogField("primaryTeethSeen",         types.UINT8), // Free running counters for number of input events, useful at lower RPM
149 		new LogField("secondaryTeethSeen",       types.UINT8), // Free running counters for number of input events, useful at lower RPM
150 		new LogField("serialOverrunErrors",      types.UINT8), // Incremented when an overrun occurs due to high interrupt load, not a fault, just a fact of life at high RPM
151 		new LogField("serialHardwareErrors",     types.UINT8), // Sum of noise, parity, and framing errors
152 		new LogField("serialAndCommsCodeErrors", types.UINT8), // Sum of checksum, escape mismatches, starts inside, and over/under length
153 		new LogField("inputEventTimeTolerance"),   // Required to tune noise rejection over RPM TODO add to LT1 and MissingTeeth
154 		new LogField("zsp10"), // Spare US variable
155 		new LogField("zsp9"),  // Spare US variable
156 		new LogField("zsp8"),  // Spare US variable
157 		new LogField("zsp7"),  // Spare US variable
158 		new LogField("zsp6"),  // Spare US variable
159 		new LogField("zsp5"),  // Spare US variable
160 		new LogField("zsp4"),  // Spare US variable
161 		new LogField("zsp3"),  // Spare US variable
162 		new LogField("clockInMilliSeconds"),       // Migrate to start of all large datalogs once analysed
163 		new LogField("clock8thMSsInMillis", 8) // Migrate to start of all large datalogs once analysed
164 	};
165 
166 	// NO default constructor, a file or path to a file MUST be given
167 	// Reason: File()'s constructors are ambiguous cannot give a null value
168 	/**
169 	 * FreeEmsBin Constructor: <code>String</code> path to your binary log
170 	 * @param String path
171 	 *
172 	 */
173 	public FreeEMSBin(String path) {
174 		this(new File(path));
175 	}
176 
177 	/**
178 	 * FreeEmsBin Constructor: <code>File</code> object of your Binary log
179 	 * @param File f
180 	 */
181 	public FreeEMSBin(File f) {
182 		logFile = f;
183 		startFound = false;
184 		packetBuffer = new short[6000];
185 		packetLength = 0;
186 		
187 		// Eventually pull this in from files and config etc instead of default, if it makes sense to.
188 		String[] headers = new String[fields.length * 32]; // Hack to make it plenty big, trim afterwards...
189 		int headersPosition = 0;
190 		for(int i=0;i<fields.length;i++){
191 			if((fields[i].type == types.BITS8) || (fields[i].type == types.BITS16) || (fields[i].type == types.BITS32)){
192 				for(int j=0;j<fields[i].bitFieldNames.length;j++){
193 					String flagID = fields[i].bitFieldNames[j] + "-" + fields[i].ID + "-B"+ j;
194 					headers[headersPosition] = flagID;
195 					headersPosition++;
196 				}
197 			}else{
198 				headers[headersPosition] = fields[i].ID;
199 				headersPosition++;
200 			}
201 		}
202 
203 		decodedLog = new GenericLog(Arrays.copyOfRange(headers, 0, headersPosition));
204 
205 		t = new Thread(this, "FreeEMSBin Loading");
206 		t.setPriority(Thread.MAX_PRIORITY);
207 		t.start();
208 	}
209 
210 
211 	/**
212 	 * DecodeLog will use the current <code>logFile</code> parse through it and when required pass the job <br>
213 	 * to the required method of this class such as decodePacket or checksum.
214 	 */
215 	@Override
216 	public void run() {
217 		try {
218 			// file setup
219 			byte[] readByte = new byte[1];
220 			short uByte = 0;
221 
222 			// These can be caused by noise, but if there is no noise, then it's a code issue with the firmware!
223 			int escapePairMismatches = 0; // Incremented when an escape is found but not followed by an escapee
224 			int startsInsideAPacket  = 0; // Incremented when a start byte is found inside a packet
225 			int packetsOverLength    = 0; // Incremented when the buffer fills up before the end
226 			int packetsUnderLength   = 0; // Incremented when a packet is found that is too short
227 			int checksumMismatches   = 0; // Incremented when calculated checksum did not match the received one
228 			int packetsParsedFully   = 0; // Number of packets that matched all requirements and got into the log
229 			int packetLengthWrong    = 0; // The length should match the ID passed in, if not, fail.
230 			int strayBytesLost       = 0; // How many bytes were not in a packet! (should be low, ie, under one packet)
231 			int payloadIDWrong       = 0; // Requests to parse packet as A when packet was of type B
232 
233 			startFound = false;
234 			logStream = new FileInputStream(logFile);
235 			decodedLog.setLogStatus(GenericLog.LOG_LOADING);
236 			while (logStream.read(readByte) != -1) {
237 				uByte = unsignedValueOf(readByte[0]);
238 				if (uByte == START_BYTE) {
239 					if (!startFound) {
240 						startFound = true;
241 					} else {
242 						startsInsideAPacket++;
243 					}
244 					packetLength = 0; // Reset array position, always (discard existing data recorded, if any)
245 				} else if (startFound) { // Skip stray bytes until a start is found
246 					if (uByte == STOP_BYTE) {
247 						if(packetLength < minimumPacketLength){
248 							packetsUnderLength++;
249 						}else if(packetLength > maximumPacketLength){
250 							packetsOverLength++;
251 						}else{
252 							short[] justThePacket = new short[packetLength];
253 							for(int i=0;i<packetLength;i++){
254 								justThePacket[i] = packetBuffer[i];
255 							}
256 							if (checksum(justThePacket)){//, packetLength)) {
257 								if(decodeBasicLogPacket(justThePacket, payloadIDToParse)){
258 									packetsParsedFully++;
259 								}else{
260 									packetLengthWrong++;
261 									payloadIDWrong++; // TODO maybe handle various possibilities post valid packet being parsed
262 								}
263 							}else{
264 								checksumMismatches++;
265 							}
266 						}
267 						startFound = false;
268 					} else if (uByte == ESCAPE_BYTE) {
269 						if (logStream.read(readByte) != -1) { // Read in the byte to be un-escaped
270 							uByte = unEscape(unsignedValueOf(readByte[0])); // un-escape this byte
271 							if (uByte != (short) -1) {
272 								packetBuffer[packetLength] = uByte; // Store the un-escaped data for processing later
273 								packetLength++;
274 							} else {
275 								startFound = false; // The rest of the data should be ignored
276 								escapePairMismatches++;
277 							}
278 						}
279 					} else {
280 						packetBuffer[packetLength] = uByte; // Store the data as-is for processing later
281 						packetLength++;
282 					}
283 				}else{
284 					strayBytesLost++;
285 				}
286 			}
287 			decodedLog.setLogStatus(GenericLog.LOG_LOADED);
288 
289 			System.out.println(System.getProperty("line.separator") + "Binary Parsing Statistics:" + System.getProperty("line.separator"));
290 
291 			System.out.println("Value: " + escapePairMismatches + " Incremented when an escape is found but not followed by an escapee");
292 			System.out.println("Value: " + startsInsideAPacket  + " Incremented when a start byte is found inside a packet");
293 			System.out.println("Value: " + packetsOverLength    + " Incremented when the buffer fills up before the end");
294 			System.out.println("Value: " + packetsUnderLength   + " Incremented when a packet is found that is too short");
295 			System.out.println("Value: " + checksumMismatches   + " Incremented when calculated checksum did not match the received one");
296 			System.out.println("Value: " + packetsParsedFully   + " Number of packets that matched all requirements and got into the log");
297 			System.out.println("Value: " + packetLengthWrong    + " The length should match the ID passed in, if not, fail.");
298 			System.out.println("Value: " + strayBytesLost       + " How many bytes were not in a packet! (should be low, ie, under one packet");
299 			System.out.println("Value: " + payloadIDWrong       + " Requests to parse packet as A when packet was of type B");
300 
301 			System.out.println(System.getProperty("line.separator") + "Thank you for choosing FreeEMS!");
302 
303 		} catch (IOException IOE) {
304 			System.out.println(IOE.getMessage());
305 			// TODO Add code to handle or warn of the error
306 		} 
307 	}
308 
309 	/**
310 	 * This method decodes a packet by splitting up the data into larger data types to keep the unsigned info <br>
311 	 * This method could probably use a little work
312 	 * @param packet is a <code>short</code> array containing 1 full packet
313 	 *
314 	 */
315 	private boolean decodeBasicLogPacket(short[] packet, int payloadIDToParse) {
316 		final int HEADER_HAS_LENGTH_INDEX   = 0;
317 		final int HEADER_IS_NACK_INDEX      = 1;
318 		final int HEADER_HAS_SEQUENCE_INDEX = 2;
319 		final int HEADER_RESERVED_E_INDEX   = 3;
320 		final int HEADER_RESERVED_D_INDEX   = 4;
321 		final int HEADER_RESERVED_C_INDEX   = 5;
322 		final int HEADER_RESERVED_B_INDEX   = 6;
323 		final int HEADER_RESERVED_A_INDEX   = 7;
324 
325 		// Increment post use
326 		int position = 0;
327 
328 		short flags = packet[position];
329 		position++;
330 
331 		short payloadIdUpper = packet[position];
332 		position++;
333 		short payloadIdLower = packet[position];
334 		position++;
335 		int payloadId = (payloadIdUpper * 256) + payloadIdLower;
336 
337 		if(payloadId != payloadIDToParse){
338 			return false; // TODO make this a code, or throw exception, but it's not exceptional at all for this to occur...
339 		}
340 
341 		int[] flagValues = processFlagBytes(flags, 8);
342 
343 		if(flagValues[HEADER_HAS_SEQUENCE_INDEX] == 1){
344 			position++; // Skip this!
345 		}
346 
347 		if(flagValues[HEADER_HAS_LENGTH_INDEX] == 1){
348 			position += 2; // Ignore this for now, it's the payload length, check it vs actual length and check actual vs required.
349 		}
350 
351 		int lengthOfFields = 0; // warn if length in config is not equal, but allow both sides of wrong as it is reasonable to not care about some and also to truncate shorter on the ECU side.
352 		for(int i=0;i<fields.length;i++){
353 			lengthOfFields += fields[i].type.width;
354 		}
355 
356 		int payloadLength = ((packet.length - position) - 1);
357 		if(payloadLength != lengthOfFields){ // First run through to find out what the lengths are.
358 			System.out.print(" Fields length is: " + lengthOfFields);
359 			System.out.print(" Packet length is: " + packet.length);
360 			System.out.println(" Payload length is: " + payloadLength);
361 			return false;
362 		}
363 
364 		for(int i=0;i<fields.length;i++){
365 			LogField field = fields[i];
366 
367 			int rawValue = 0;
368 			for(int j=0;j<field.type.width;j++){
369 				rawValue = (rawValue * 256) + packet[position];
370 				position++;
371 			}
372 
373 			if((field.type == types.UINT8) || (field.type == types.UINT16) || (field.type == types.UINT32)){
374 				double scaledValue = (double)rawValue/field.divBy;
375 				double finalValue = scaledValue + field.addTo;
376 				decodedLog.addValue(field.ID, finalValue);
377 			}else if((field.type == types.BITS8) || (field.type == types.BITS16) || (field.type == types.BITS32)){
378 				int[] processedFlags = processFlagBytes(rawValue, (8 * field.type.width));
379 				for(int j=0;j<processedFlags.length;j++){
380 					String flagID = field.bitFieldNames[j] + "-" + field.ID + "-B"+ j;
381 					decodedLog.addValue(flagID, processedFlags[j]);
382 				}
383 			}else if(field.type == types.SINT8){ //??
384 				decodedLog.addValue(field.ID, rawValue);
385 			}else if(field.type == types.SINT16){
386 				decodedLog.addValue(field.ID, rawValue);
387 			}else if(field.type == types.SINT32){
388 				decodedLog.addValue(field.ID, rawValue);
389 			}
390 		}
391 		return true; // TODO FIXME : Default to all things being good till I attack this!
392 	}
393 
394 	int[] processFlagBytes(long valueOfFlags, int numberOfFlags){
395 		if((numberOfFlags != 8) && (numberOfFlags != 16) && (numberOfFlags != 32)){
396 			throw new IllegalArgumentException("Basic units of computer sciene apply, embedded flags are never " + numberOfFlags + " wide!"); // Unless they are 64, but shhhh...
397 		}
398 
399 		int[] flagValues = new int[numberOfFlags];
400 		int comparison = 1;
401 		for(int i=0;i<numberOfFlags;i++){
402 			if((valueOfFlags % (2 * comparison)) == comparison){
403 				flagValues[i] = 1;
404 				valueOfFlags -= comparison;
405 			}
406 			comparison *= 2;
407 		}
408 
409 		return flagValues;
410 	}
411 
412 	/**
413 	 * performs a check sum based on the packet data <br>
414 	 * the checksum needs to be improved however
415 	 * @param packet
416 	 * @return true or false based on if the checksum passes
417 	 */
418 	private boolean checksum(short[] packet) {
419 		if(packetLength > 0){
420 			short includedSum = packet[packetLength -1]; // sum is last byte
421 			long veryBIGsum = 0;
422 			for(int x=0;x<packetLength-1;x++){
423 				veryBIGsum += packet[x];
424 			}
425 			short calculatedSum = (short)(veryBIGsum % 256);
426 			return (calculatedSum == includedSum);
427 		}else{
428 			return false;
429 		}
430 	}
431 
432 	/**
433 	 * Transforms an escape-encoded byte back into what it was before transmission
434 	 *
435 	 * @param uByte - byte to be Un-escaped
436 	 * @return -1 if bad data or the proper value of the escaped byte
437 	 */
438 	private short unEscape(short uByte) {
439 		if (uByte == ESCAPED_START_BYTE) {
440 			return START_BYTE;
441 		} else if (uByte == ESCAPED_STOP_BYTE) {
442 			return STOP_BYTE;
443 		} else if (uByte == ESCAPED_ESCAPE_BYTE) {
444 			return ESCAPE_BYTE;
445 		} else {
446 			return (short) -1;
447 		}
448 	}
449 
450 	/**
451 	 * Transforms an unsigned char into a format suitable for Java, a short.
452 	 * 
453 	 * @param uInt8 the raw signed byte representation of our raw unsigned char
454 	 * @return the value of the unsigned byte stored in a short
455 	 */
456 	private short unsignedValueOf(byte uInt8){
457 		return (short)(0xFF & uInt8);
458 	}
459 
460 	/**
461 	 *
462 	 * @return getGenericLog() returns the reference to the generic log the binary data has been converted to
463 	 */
464 	/*public GenericLog getGenericLog() {
465 		if (logLoaded) {
466 			return decodedLog;
467 		} else {
468 			return null;
469 		}
470 	}*/
471 
472 
473 	/**
474 	 *
475 	 * @return Misc data about this log
476 	 * <br> to be implemented in full later
477 	 */
478 	@Override
479 	public String toString() {
480 		return super.toString();
481 	}
482 }