├── .gitignore ├── ELM327DS.pdf ├── README.md ├── pom.xml └── src ├── lib └── jssc.jar ├── main ├── assembly │ └── runnable.xml ├── java │ └── cardiag │ │ ├── Main.java │ │ ├── obd2 │ │ ├── AirStatus.java │ │ ├── EcuCompatibility.java │ │ ├── Fault.java │ │ ├── FuelStatus.java │ │ ├── Mode.java │ │ ├── MonitorStatus.java │ │ ├── OBD2Exception.java │ │ ├── OBD2Standard.java │ │ ├── PID.java │ │ ├── Report.java │ │ ├── Response.java │ │ └── ResponseWithNoData.java │ │ ├── output │ │ ├── OutputFileWriter.java │ │ └── ReportFileWriter.java │ │ ├── serial │ │ ├── PortCommunication.java │ │ ├── PortCommunicationException.java │ │ ├── PortConfiguration.java │ │ └── SerialUtils.java │ │ └── user │ │ ├── Action.java │ │ ├── ConsoleCommunication.java │ │ ├── ConsoleWrapper.java │ │ └── UserCommunication.java └── resources │ └── log4j.properties └── test ├── java └── cardiag │ ├── obd2 │ └── OBD2StandardITest.java │ ├── serial │ ├── PortCommunicationIT.java │ └── SerialUtilsIT.java │ ├── test │ └── TestConfiguration.java │ └── user │ ├── ConsoleMock.java │ └── UserCommunicationTest.java └── resources ├── log4j.properties └── test.properties /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .settings 3 | .project 4 | .classpath 5 | 6 | -------------------------------------------------------------------------------- /ELM327DS.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmatej/java-cardiag/6d5d2200be24aa5a8fa3532ac1cfc16450295d0b/ELM327DS.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | java-cardiag 2 | ============ 3 | 4 | Description 5 | ----------- 6 | Car diagnostic software for vehicles with OBD2 compatible interface. 7 | 8 | Example 9 | ------- 10 | - now we can run two actions. Program looks for serial interfaces and lets the user to select one. 11 | Then it runs the action. 12 | - report creates a text file in the current directory containing all implemented values. 13 | - clear_trouble_codes erases the saved errors. 14 | 15 | ``` 16 | sudo java -jar ./java-cardiag-0.0.1-SNAPSHOT-jar-with-dependencies.jar report; 17 | sudo java -jar ./java-cardiag-0.0.1-SNAPSHOT-jar-with-dependencies.jar CLEAR_TROUBLE_CODES; 18 | ``` 19 | 20 | Targets 21 | ------- 22 | - support for most of today's operating systems 23 | - communication via USB ELM327/OBD2 car interface, probably other in future 24 | - stability 25 | - simple startup, no system modifications needed (only JRE6 and higher) 26 | - pretty maintainable code covered by tests, both unit (without real serial interface) 27 | and integration (with interface and car) 28 | - open source free code 29 | 30 | Problems 31 | --------- 32 | - how 33 | - only available hardware for testing - ELM327 clone (1.5a) 34 | - only few available cars for testing - Lada Kalina 1.6 8V 2007, Škoda Fabia 2010, Ford Focus 2007, Seat Ibiza 2001. 35 | - need help with a selection of the license ... GNU GPL v3? EPL? BSD? MIT? 36 | 37 | Troubleshooting 38 | --------------- 39 | Port name - /dev/ttyUSB0; Method name - openPort(); Exception type - Permission denied. (Linux) 40 | - sudo gpasswd --add ${USER} dialout 41 | - or run the program with sudo. 42 | 43 | Current stage 44 | ------------- 45 | - early development. 46 | - can reset error codes 47 | - can produce a report file with the current values 48 | - version 0.0.1 will be released after I will fix my problem with my car (P300, P303, P304) ... or later, in 2017 ;) 49 | - if someone will start implementing GUI, many people will be finally happy ;) 50 | 51 | Useful links 52 | ------------ 53 | - http://code.google.com/p/java-simple-serial-connector/ 54 | - http://en.wikipedia.org/wiki/OBD-II_PIDs#Bitwise_encoded_PIDs 55 | - http://www.obd-codes.com/trouble_codes/ 56 | - http://www.obd-codes.com/faq/obd2-codes-explained.php 57 | - http://www.multitronics.ru/kody_vaz/ 58 | - http://www.multitronics.ru/kody_obd2/ 59 | - http://www.multitronics.ru/terminy_obd2/ 60 | - http://www.obdii.com/obdii_library.asp 61 | - http://www.outilsobdfacile.com/obd-mode-pid.php 62 | - http://www.genisysotc.com/pdfs/DriveabilityDiagnostics.pdf 63 | 64 | Maybe useful links 65 | ------------------ 66 | These libraries were not selected due to license or incompatibilities or other reason: 67 | - http://www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloads-misc-419423.html 68 | - http://www.oracle.com/technetwork/java/index-139971.html 69 | - http://rxtx.qbang.org/wiki/index.php/Download 70 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | java-cardiag 4 | java-cardiag 5 | 0.0.1-SNAPSHOT 6 | Car diagnostic software 7 | https://github.com/dmatej/java-cardiag 8 | 9 | 10 | scm:git:git://github.com/dmatej/java-cardiag.git 11 | git@github.com:dmatej/java-cardiag.git 12 | https://github.com/dmatej/java-cardiag.git 13 | 14 | 15 | 16 | cardiag.Main 17 | src/main/run 18 | UTF-8 19 | 1.8 20 | 1.8 21 | true 22 | 23 | /dev/ttyUSB0 24 | 25 | 26 | 27 | 28 | 29 | src/test/resources 30 | 31 | **/*.properties 32 | 33 | true 34 | 35 | 36 | 37 | 38 | 39 | org.apache.maven.plugins 40 | maven-resources-plugin 41 | 2.5 42 | 43 | 44 | compile-run-scripts 45 | compile 46 | 47 | copy-resources 48 | 49 | 50 | ${project.build.directory}/run 51 | 52 | 53 | ${run.srcDir} 54 | 55 | **/* 56 | 57 | false 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.apache.maven.plugins 67 | maven-surefire-plugin 68 | 2.18.1 69 | 70 | false 71 | false 72 | true 73 | true 74 | 75 | **/*ITest.class 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-failsafe-plugin 83 | 2.18.1 84 | 85 | false 86 | false 87 | true 88 | true 89 | 90 | 91 | 92 | integration-test 93 | 94 | integration-test 95 | 96 | 97 | 98 | verify 99 | 100 | verify 101 | 102 | 103 | 104 | 105 | 106 | 107 | maven-jar-plugin 108 | 2.3.1 109 | 110 | 111 | 112 | true 113 | lib/ 114 | ${mainClass} 115 | 116 | 117 | 118 | 119 | 120 | 121 | maven-assembly-plugin 122 | 2.3 123 | 124 | 125 | make-assembly 126 | package 127 | 128 | single 129 | 130 | 131 | 132 | 133 | 134 | 135 | ${mainClass} 136 | 137 | 138 | 139 | jar-with-dependencies 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | org.scream3r 149 | jssc 150 | 2.8.0 151 | compile 152 | 153 | 154 | org.apache.commons 155 | commons-lang3 156 | 3.5 157 | compile 158 | 159 | 160 | commons-io 161 | commons-io 162 | 2.4 163 | compile 164 | 165 | 166 | org.slf4j 167 | slf4j-api 168 | 1.7.25 169 | compile 170 | 171 | 172 | org.slf4j 173 | slf4j-log4j12 174 | 1.7.25 175 | runtime 176 | 177 | 178 | log4j 179 | log4j 180 | 1.2.17 181 | compile 182 | 183 | 184 | junit 185 | junit 186 | 4.11 187 | test 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /src/lib/jssc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmatej/java-cardiag/6d5d2200be24aa5a8fa3532ac1cfc16450295d0b/src/lib/jssc.jar -------------------------------------------------------------------------------- /src/main/assembly/runnable.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | runnable-cardiag 9 | 10 | 11 | zip 12 | 13 | false 14 | 15 | 16 | 17 | ${project.build.directory}/${project.build.finalName}.one-jar.jar 18 | ${project.artifactId}.jar 19 | 664 20 | 21 | 22 | 23 | 24 | 25 | 26 | ${project.build.directory}/run 27 | 28 | 29 | **/*.sh 30 | 31 | false 32 | 33 | 34 | ${project.build.directory}/run 35 | 36 | 37 | **/*.sh 38 | 39 | false 40 | 774 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/cardiag/Main.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag; 5 | 6 | import java.io.File; 7 | import java.text.SimpleDateFormat; 8 | import java.util.Date; 9 | import java.util.List; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import cardiag.obd2.OBD2Standard; 15 | import cardiag.obd2.Report; 16 | import cardiag.output.ReportFileWriter; 17 | import cardiag.serial.PortConfiguration; 18 | import cardiag.serial.SerialUtils; 19 | import cardiag.user.Action; 20 | import cardiag.user.ConsoleCommunication; 21 | import cardiag.user.ConsoleWrapper; 22 | import cardiag.user.UserCommunication; 23 | 24 | /** 25 | * The main program. Initiates the user interface. 26 | * 27 | * @author David Matějček 28 | */ 29 | public class Main { 30 | 31 | private static final Logger LOG = LoggerFactory.getLogger(Main.class); 32 | 33 | 34 | /** 35 | * The main method. 36 | * 37 | * @param args 38 | * @throws Exception 39 | */ 40 | public static void main(final String... args) throws Exception { 41 | LOG.debug("main(args={})", (Object) args); 42 | 43 | final ConsoleCommunication console = new ConsoleWrapper(System.console()); 44 | final UserCommunication user = new UserCommunication(console); 45 | 46 | final List portNames = SerialUtils.getPortNames(); 47 | final PortConfiguration cfg = user.readPortConfiguration(portNames); 48 | 49 | final Action action = parseAction(args); 50 | final File homeDir = parseHomeDir(args); 51 | 52 | final OBD2Standard obd2 = new OBD2Standard(cfg); 53 | try { 54 | if (action == Action.WATCH) { 55 | watch(obd2, homeDir, 1000); 56 | } else if (action == Action.REPORT) { 57 | watch(obd2, homeDir, 1); 58 | } else if (action == Action.CLEAR_TROUBLE_CODES) { 59 | obd2.clearTroubleCodes(); 60 | } 61 | } finally { 62 | obd2.close(); 63 | } 64 | } 65 | 66 | 67 | private static void watch(final OBD2Standard obd2, final File homeDir, final int iterations) { 68 | LOG.info("watch(obd2={}, homeDir={}, iterations={})", obd2, homeDir, iterations); 69 | final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss.SSS"); 70 | if (!homeDir.exists()) { 71 | homeDir.mkdirs(); 72 | } 73 | final File file = new File(homeDir, "report" + sdf.format(new Date()) + ".txt"); 74 | final ReportFileWriter writer = new ReportFileWriter(file); 75 | int i = iterations; 76 | while (i-- > 0) { 77 | final Report report = obd2.createReport(); 78 | writer.write(report); 79 | } 80 | } 81 | 82 | 83 | private static Action parseAction(final String... args) { 84 | LOG.trace("parseAction(args={})", (Object[]) args); 85 | 86 | if (args == null || args.length == 0) { 87 | return Action.REPORT; 88 | } 89 | return Action.parse(args[0]); 90 | } 91 | 92 | 93 | private static File parseHomeDir(final String... args) { 94 | LOG.trace("parseHomeDir(args={})", (Object[]) args); 95 | 96 | if (args == null || args.length < 2) { 97 | final File tmpDir = new File(System.getProperty("java.io.tmpdir")); 98 | return new File(tmpDir, "java-cardiag"); 99 | } 100 | return new File(args[1]); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/AirStatus.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | /** 7 | * @author David Matějček 8 | */ 9 | public enum AirStatus { 10 | /***/ 11 | UPSTREAM(0x01, "Upstream of catalytic converter"), 12 | /***/ 13 | DOWNSTREAM(0x02, "Downstream of catalytic converter"), 14 | /***/ 15 | OFF_OR_OUTSIDE(0x04, "From the outside atmosphere or off"), 16 | /***/ 17 | PUMP_COMMANDED_ON_FOR_DIAGNOSTIC(0x08, "Pump commanded on for diagnostics"); 18 | 19 | final int code; 20 | final String description; 21 | 22 | 23 | AirStatus(int value, String description) { 24 | this.code = value; 25 | this.description = description; 26 | } 27 | 28 | 29 | /** 30 | * @return the code. 31 | */ 32 | public int getCode() { 33 | return code; 34 | } 35 | 36 | 37 | /** 38 | * @return the description. 39 | */ 40 | public String getDescription() { 41 | return description; 42 | } 43 | 44 | 45 | /** 46 | * @return code and description 47 | */ 48 | @Override 49 | public String toString() { 50 | return this.description; 51 | } 52 | 53 | 54 | public static AirStatus parseHex(final String hex) { 55 | if (hex == null) { 56 | throw new IllegalArgumentException("Invalid hex code: " + hex); 57 | } 58 | 59 | int code1 = Integer.parseInt(hex, 16); 60 | for (AirStatus status : AirStatus.values()) { 61 | if (status.code == code1) { 62 | return status; 63 | } 64 | } 65 | 66 | throw new IllegalArgumentException("Unknown status: " + hex); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/EcuCompatibility.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | /** 7 | * @author David Matějček 8 | */ 9 | public enum EcuCompatibility { 10 | CARB(0x01, "OBD-II as defined by the CARB"), EPA(0x02, "OBD as defined by the EPA"), OBD_OBDII(0x03, "OBD and OBD-II"), OBDI( 11 | 0x04, "OBD-I"), INCOMPATIBLE(0x05, "Not meant to comply with any OBD standard"), EOBD_OBDII(0x06, 12 | "EOBD and OBD-II"), EOBD_OBD(0x08, "EOBD and OBD"), EOBD_OBD_OBDII(0x09, "EOBD, OBD and OBD II"), JOBD(0x0A, 13 | "JOBD (Japan)"), JOBD_OBDII(0x0B, "JOBD and OBD II"), JOBD_EOBD(0x0C, "JOBD and EOBD"), JOBD_EOBD_OBDII(0x0D, 14 | "JOBD, EOBD, and OBD II"); 15 | 16 | private int code; 17 | private String description; 18 | 19 | 20 | EcuCompatibility(int code, String description) { 21 | this.code = code; 22 | this.description = description; 23 | } 24 | 25 | 26 | /** 27 | * @return the code. 28 | */ 29 | public int getCode() { 30 | return code; 31 | } 32 | 33 | 34 | /** 35 | * @return the description. 36 | */ 37 | public String getDescription() { 38 | return description; 39 | } 40 | 41 | 42 | /** 43 | * @return code and description 44 | */ 45 | @Override 46 | public String toString() { 47 | return this.description; 48 | } 49 | 50 | 51 | public static EcuCompatibility parseHex(final String hex) { 52 | if (hex == null) { 53 | throw new IllegalArgumentException("Invalid hex code: " + hex); 54 | } 55 | 56 | int code1 = Integer.parseInt(hex, 16); 57 | for (EcuCompatibility status : EcuCompatibility.values()) { 58 | if (status.code == code1) { 59 | return status; 60 | } 61 | } 62 | 63 | throw new IllegalArgumentException("Unknown status: " + hex); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/Fault.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | import cardiag.serial.SerialUtils; 7 | 8 | 9 | /** 10 | * @author David Matějček 11 | * 12 | */ 13 | public class Fault { 14 | 15 | private final String code; 16 | 17 | 18 | /** 19 | * @param string 20 | */ 21 | public Fault(final String code) { 22 | this.code = code; 23 | } 24 | 25 | 26 | public String getCode() { 27 | return this.code; 28 | } 29 | 30 | 31 | @Override 32 | public String toString() { 33 | return getCode(); 34 | } 35 | 36 | 37 | /** 38 | * @param a 39 | * @param b 40 | * @return 41 | */ 42 | public static Fault decode(final boolean[] a, final boolean[] b) { 43 | final StringBuilder parsed = new StringBuilder(); 44 | final char first; 45 | if (a[0]) { 46 | if (a[1]) { 47 | first = 'U'; 48 | } else { 49 | first = 'B'; 50 | } 51 | } else { 52 | if (a[1]) { 53 | first = 'C'; 54 | } else { 55 | first = 'P'; 56 | } 57 | } 58 | parsed.append(first); 59 | 60 | final char second; 61 | if (a[2]) { 62 | if (a[3]) { 63 | second = '3'; 64 | } else { 65 | second = '2'; 66 | } 67 | } else { 68 | if (a[3]) { 69 | second = '1'; 70 | } else { 71 | second = '0'; 72 | } 73 | } 74 | parsed.append(second); 75 | 76 | parsed.append(toHex(a[4], a[5], a[6], a[7])); 77 | parsed.append(toHex(b[0], b[1], b[2], b[3])); 78 | parsed.append(toHex(b[4], b[5], b[6], b[7])); 79 | 80 | return new Fault(parsed.toString()); 81 | } 82 | 83 | 84 | private static String toHex(boolean... bools) { 85 | return Integer.toHexString(SerialUtils.toInteger(bools)); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/FuelStatus.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | /** 7 | * @author David Matějček 8 | */ 9 | public enum FuelStatus { 10 | 11 | OPEN_LOW_TEMP(1, "Open loop due to insufficient engine temperature"), // 12 | CLOSED_USING_OXYGEN(2, "Closed loop, using oxygen sensor feedback to determine fuel mix"), // 13 | OPEN_LOAD(4, "Open loop due to engine load OR fuel cut due to deceleration"), // 14 | OPEN_FAILURE(8, "Open loop due to system failure"), // 15 | CLOSED_FAILURE_FEEDBACK(16, 16 | "Closed loop, using at least one oxygen sensor but there is a fault in the feedback system"), // 17 | ZERO5(32, "Always zero"), ZERO6(0x06, "Always zero"), ZERO7(0x07, "Always zero"), ; 18 | 19 | final int code; 20 | final String description; 21 | 22 | 23 | FuelStatus(int value, String description) { 24 | this.code = value; 25 | this.description = description; 26 | } 27 | 28 | 29 | /** 30 | * @return the code. 31 | */ 32 | public int getCode() { 33 | return code; 34 | } 35 | 36 | 37 | /** 38 | * @return the description. 39 | */ 40 | public String getDescription() { 41 | return description; 42 | } 43 | 44 | 45 | /** 46 | * @return code and description 47 | */ 48 | @Override 49 | public String toString() { 50 | return this.description; 51 | } 52 | 53 | 54 | public static FuelStatus parseHex(final String hex) { 55 | if (hex == null) { 56 | throw new IllegalArgumentException("Invalid hex code: " + hex); 57 | } 58 | 59 | int code1 = Integer.parseInt(hex, 16); 60 | for (FuelStatus status : FuelStatus.values()) { 61 | if (status.code == code1) { 62 | return status; 63 | } 64 | } 65 | 66 | throw new IllegalArgumentException("Unknown status: " + hex); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/Mode.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | /** 9 | * @author David Matějček 10 | */ 11 | public enum Mode { 12 | CURRENT_DATA(1), FREEZE_FRAME_DATA(2), DIAGNOSTIC(3), CLEAR_TROUBLE_CODES(4), VEHICLE_INFO(9); 13 | 14 | private int code; 15 | 16 | 17 | Mode(int code) { 18 | this.code = code; 19 | } 20 | 21 | 22 | public String hex() { 23 | return StringUtils.leftPad(Integer.toHexString(code), 2, '0'); 24 | } 25 | 26 | 27 | public static Mode parseHex(final String hexMode) { 28 | if (hexMode == null) { 29 | throw new IllegalArgumentException("Invalid mode: " + hexMode); 30 | } 31 | int code = Integer.parseInt(hexMode, 16); 32 | for (Mode mode : Mode.values()) { 33 | if (mode.code == code) { 34 | return mode; 35 | } 36 | } 37 | throw new IllegalArgumentException("Invalid mode: " + hexMode); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/MonitorStatus.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | import org.apache.commons.lang3.builder.ReflectionToStringBuilder; 7 | 8 | 9 | /** 10 | * @author David Matějček 11 | * 12 | */ 13 | public class MonitorStatus { 14 | 15 | private boolean mil; 16 | private int emmissionRelatedDTCs; 17 | private boolean comporessionIgnited; 18 | private boolean missfireAvailable; 19 | private boolean missfireIncomplete; 20 | private boolean fuelAvailable; 21 | private boolean fuelIncomplete; 22 | private boolean componentsAvailable; 23 | private boolean componentsIncomplete; 24 | private boolean reservedBitB7; 25 | 26 | public void setMIL(boolean mil) { 27 | this.mil = mil; 28 | } 29 | 30 | public void setEmissionRelatedDTCs(int dtcs) { 31 | this.emmissionRelatedDTCs = dtcs; 32 | } 33 | 34 | /** 35 | * @param compression (or spark) 36 | */ 37 | public void setIgnition(boolean compression) { 38 | this.comporessionIgnited = compression; 39 | } 40 | 41 | public void setMissfire(boolean available, boolean incomplete) { 42 | this.missfireAvailable = available; 43 | this.missfireIncomplete = incomplete; 44 | } 45 | 46 | public void setFuel(boolean available, boolean incomplete) { 47 | this.fuelAvailable = available; 48 | this.fuelIncomplete = incomplete; 49 | } 50 | 51 | 52 | public void setComponents(boolean available, boolean incomplete) { 53 | this.componentsAvailable = available; 54 | this.componentsIncomplete = incomplete; 55 | } 56 | 57 | public void setReservedBitB7(boolean bit) { 58 | this.reservedBitB7 = bit; 59 | } 60 | 61 | 62 | 63 | @Override 64 | public String toString() { 65 | return ReflectionToStringBuilder.toString(this); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/OBD2Exception.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | 7 | /** 8 | * Port communication exception. Command was not successfully executed. 9 | * 10 | * @author David Matějček 11 | */ 12 | public class OBD2Exception extends Exception { 13 | private static final long serialVersionUID = -3794508502469587712L; 14 | 15 | 16 | /** 17 | * @param message - a short description of exception 18 | */ 19 | public OBD2Exception(final String message) { 20 | super(message); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/OBD2Standard.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | import java.io.Closeable; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.apache.commons.lang3.builder.ReflectionToStringBuilder; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import cardiag.serial.PortCommunication; 18 | import cardiag.serial.PortCommunicationException; 19 | import cardiag.serial.PortConfiguration; 20 | import cardiag.serial.SerialUtils; 21 | 22 | /** 23 | * Standard OBD-II functions. 24 | * 25 | * @author David Matějček 26 | */ 27 | public class OBD2Standard implements Closeable { 28 | 29 | private static final Logger LOG = LoggerFactory.getLogger(OBD2Standard.class); 30 | 31 | private final PortCommunication comm; 32 | 33 | 34 | /** 35 | * Starts the port communication with some initial conversation. 36 | * 37 | * @param cfg 38 | * @throws PortCommunicationException 39 | */ 40 | public OBD2Standard(final PortConfiguration cfg) throws PortCommunicationException { 41 | LOG.debug("OBD2Standard(cfg={})", cfg); 42 | this.comm = new PortCommunication(cfg); 43 | this.reset(); 44 | } 45 | 46 | 47 | /** 48 | * Resets the communication. 49 | * 50 | * @throws PortCommunicationException 51 | */ 52 | public void reset() throws PortCommunicationException { 53 | LOG.debug("reset()"); 54 | // TODO: why is it here twice??? 55 | this.comm.reset(); 56 | this.comm.reset(); 57 | this.comm.setEcho(false); 58 | this.comm.setLineTermination(false); 59 | } 60 | 61 | 62 | /** 63 | * Closes the communication and port. 64 | */ 65 | public void close() { 66 | LOG.debug("close()"); 67 | this.comm.close(); 68 | } 69 | 70 | 71 | /** 72 | * @return a {@link Report} 73 | */ 74 | public Report createReport() { 75 | LOG.trace("createReport()"); 76 | final Report report = new Report(); 77 | report.setSupportedPIDS(getSupportedPIDs()); 78 | report.setEcuCompatibility(getEcuCompatibility()); 79 | report.setMonitorStatus(getMonitorStatus()); 80 | report.setFaults(getErrorReport()); 81 | 82 | report.setDistanceSinceErrorCodesCleared(getDistanceSinceCodesCleared(false)); 83 | report.setDistanceWithMalfunction(getDistanceWithMalfunction(false)); 84 | 85 | report.setAmbientAirTemperature(getAmbientAirTemperature(false)); 86 | report.setEngineOilTemperature(getEngineOilTemperature(false)); 87 | report.setEngineCoolantTemperature(getEngineCoolantTemperature(false)); 88 | report.setManifoldSurfaceTemperature(getManifoldSurfaceTemperature(false)); 89 | 90 | report.setEngineLoad(getEngineLoad(false)); 91 | report.setExhaustGasRecirculationTemperature(getExhaustGasRecirculationTemperature(false)); 92 | report.setFuelInjectionTiming(getFuelInjectionTiming(false)); 93 | report.setFuelLevelInput(getFuelLevelInput(false)); 94 | report.setFuelRate(getFuelRate(false)); 95 | report.setFuelStatus(getFuelStatus(false)); 96 | report.setIntakeAirTemperature(getIntakeAirTemperature(false)); 97 | report.setIntakeAirTemperatureSensor(getIntakeAirTemperatureSensor(false)); 98 | report.setCatalystTemperatureSensor1(getCatalystTemperature(false, 2, 1)); 99 | report.setCatalystTemperatureSensor2(getCatalystTemperature(false, 2, 2)); 100 | report.setSecondaryAirStatus(getSecondaryAirStatus(false)); 101 | report.setCommandedEgr(getCommandedEgr(false)); 102 | report.setEgrError(getEgrError(false)); 103 | 104 | // only bank 1 105 | report.setFuelTrimPercentShortTerm(getFuelTrimPercent(false, false, 1)); 106 | report.setFuelTrimPercentLongTerm(getFuelTrimPercent(false, true, 1)); 107 | report.setEthanolFuel(getEthanolFuel(false)); 108 | 109 | return report; 110 | } 111 | 112 | 113 | /** 114 | * Sends the request to OBD II unit and returns a multiline response. 115 | * 116 | * @param mode 117 | * @param pid 118 | * @param hexParams 119 | * @return a list of responses. Never null, but may be empty. 120 | */ 121 | protected List ask(final Mode mode, final PID pid, final String... hexParams) { 122 | LOG.debug("ask(mode={}, pid={}, hexParams={})", mode, pid, hexParams); 123 | final String[] params; 124 | if (hexParams == null) { 125 | params = new String[2]; 126 | } else { 127 | params = new String[hexParams.length + 2]; 128 | } 129 | params[0] = mode.hex(); 130 | params[1] = pid.hex(); 131 | if (hexParams != null) { 132 | for (int i = 0; i < hexParams.length; i++) { 133 | params[i + 2] = hexParams[i]; 134 | } 135 | } 136 | comm.writeln(params); 137 | final String responseString = removeIgnoredText(comm.readResponse(StringUtils.join(params))); 138 | final List responses = new ArrayList<>(); 139 | // Ford Focus 1.4 140 | if ("NO DATA".equals(responseString)) { 141 | responses.add(new ResponseWithNoData(mode, pid)); 142 | return responses; 143 | } 144 | 145 | final String[] vals = split(responseString); 146 | LOG.trace("vals: \n {}", Arrays.toString(vals)); 147 | final String firstWord = vals[0]; 148 | if (firstWord.length() != 2) { 149 | throw new PortCommunicationException("Invalid response: '" + responseString + "'"); 150 | } 151 | if ("7F".equals(firstWord)) { 152 | LOG.warn("Error response: '{}'", responseString); 153 | final Response response = new Response(true, mode, pid, vals); 154 | responses.add(response); 155 | return responses; 156 | } 157 | final String[] data; 158 | final PID responsePID; 159 | final int dataOffset; 160 | if (mode == Mode.DIAGNOSTIC || mode == Mode.CLEAR_TROUBLE_CODES) { 161 | dataOffset = 1; 162 | responsePID = null; 163 | } else { 164 | dataOffset = 2; 165 | responsePID = PID.parseHex(vals[1], mode); 166 | } 167 | if (vals.length > dataOffset) { 168 | data = Arrays.copyOfRange(vals, dataOffset, vals.length); 169 | } else { 170 | data = null; 171 | } 172 | final Response response = new Response(false, mode, responsePID, data); 173 | responses.add(response); 174 | return responses; 175 | } 176 | 177 | 178 | private String[] split(final String responseString) { 179 | final ArrayList response = new ArrayList<>(); 180 | Character first = null; 181 | for (int i = 0; i < responseString.length(); i++) { 182 | final char c = responseString.charAt(i); 183 | if (Character.isWhitespace(c)) { 184 | continue; 185 | } 186 | if (first == null) { 187 | first = Character.valueOf(c); 188 | } else { 189 | response.add(StringUtils.join(first, c)); 190 | first = null; 191 | } 192 | 193 | } 194 | return response.toArray(new String[response.size()]); 195 | } 196 | 197 | 198 | private String removeIgnoredText(final String responseString) { 199 | LOG.trace("removeIgnoredText(responseString={})", responseString); 200 | final String fixed = StringUtils.removeIgnoreCase(responseString, "SEARCHING..."); 201 | if (fixed.length() != responseString.length()) { 202 | LOG.warn("Removed ignored text from the response: '{}'. Result: '{}'", responseString, fixed); 203 | } 204 | return fixed; 205 | } 206 | 207 | 208 | /** 209 | * Sends the request to OBD II unit and returns a singleline response. 210 | * 211 | * @param mode 212 | * @param pid 213 | * @param hexParams 214 | * @return a response, may be null. 215 | */ 216 | protected Response askOneLine(final Mode mode, final PID pid, final String... hexParams) { 217 | LOG.trace("askOneLine(mode={}, pid={}, hexParams={})", mode, pid, hexParams); 218 | final List responses = ask(mode, pid, hexParams); 219 | if (responses.size() > 1) { 220 | // bug or something weird. 221 | throw new IllegalStateException( 222 | "Too many responses for this command: " + ReflectionToStringBuilder.toString(responses)); 223 | } 224 | final Response response = responses.get(0); 225 | return response; 226 | } 227 | 228 | 229 | private Mode getMode(final boolean freezed) { 230 | return freezed ? Mode.FREEZE_FRAME_DATA : Mode.CURRENT_DATA; 231 | } 232 | 233 | 234 | /** 235 | * @return the VIN code of the car. 236 | */ 237 | public String getVIN() { 238 | LOG.debug("getVIN()"); 239 | final Response responseBytes = askOneLine(Mode.VEHICLE_INFO, PID.VIN_COUNT_OF_BYTES); 240 | LOG.info("Count of VIN bytes: {}", Integer.parseInt(responseBytes.getData()[0]), 16); 241 | 242 | // TODO: Kalina did not returned any. 243 | final List response = ask(Mode.VEHICLE_INFO, PID.VIN); 244 | return response.toString(); 245 | } 246 | 247 | 248 | public boolean[] getSupportedPIDs() { 249 | LOG.debug("getSupportedPIDs()"); 250 | final Response line = askOneLine(Mode.CURRENT_DATA, PID.PIDS_SUPPORTED); 251 | if (line.isError()) { 252 | return null; 253 | } 254 | return SerialUtils.convertHexToBooleanArray(line.getData()); 255 | } 256 | 257 | 258 | public FuelStatus getFuelStatus(final boolean freezed) { 259 | LOG.debug("getFuelStatus(freezed={})", freezed); 260 | final Response line = askOneLine(getMode(freezed), PID.FUEL_STATUS); 261 | if (line.isError()) { 262 | return null; 263 | } 264 | // TODO: two bytes = two systems 265 | return FuelStatus.parseHex(line.getData()[0]); 266 | } 267 | 268 | 269 | public EcuCompatibility getEcuCompatibility() { 270 | LOG.debug("getEcuCompatibility()"); 271 | final Response response = askOneLine(Mode.CURRENT_DATA, PID.ECU_COMPATIBILITY); 272 | if (response.isError()) { 273 | return null; 274 | } 275 | return EcuCompatibility.parseHex(response.getData()[0]); 276 | } 277 | 278 | 279 | public void clearTroubleCodes() { 280 | LOG.debug("clearTroubleCodes()"); 281 | askOneLine(Mode.CLEAR_TROUBLE_CODES, PID.CLEAR_TROUBLE_CODES); 282 | } 283 | 284 | 285 | public MonitorStatus getMonitorStatus() { 286 | LOG.debug("getMonitorStatus()"); 287 | final Response response = askOneLine(Mode.CURRENT_DATA, PID.MONITOR_STATUS); 288 | if (response.isError()) { 289 | return null; 290 | } 291 | final String[] data = response.getData(); 292 | final boolean[] a = SerialUtils.convertHexToBooleanArray(data[0]); 293 | final boolean[] b = SerialUtils.convertHexToBooleanArray(data[1]); 294 | final boolean[] c = SerialUtils.convertHexToBooleanArray(data[2]); 295 | final boolean[] d = SerialUtils.convertHexToBooleanArray(data[3]); 296 | final MonitorStatus status = new MonitorStatus(); 297 | status.setMIL(a[0]); 298 | status.setEmissionRelatedDTCs(SerialUtils.toInteger(Arrays.copyOfRange(a, 1, 8))); 299 | status.setMissfire(b[7], b[3]); 300 | status.setFuel(b[6], b[2]); 301 | status.setComponents(b[5], b[1]); 302 | status.setIgnition(b[4]); 303 | status.setReservedBitB7(b[0]); 304 | // TODO: C and D 305 | return status; 306 | } 307 | 308 | 309 | public List getErrorReport() { 310 | LOG.debug("getErrorReport()"); 311 | final List responses = ask(Mode.DIAGNOSTIC, PID.DIAGNOSTIC_CODES); 312 | if (responses.isEmpty()) { 313 | return Collections.emptyList(); 314 | } 315 | if (responses.get(0).isError()) { 316 | return null; 317 | } 318 | final List faults = new ArrayList(); 319 | if (responses.get(0) instanceof ResponseWithNoData) { 320 | return faults; 321 | } 322 | for (final Response response : responses) { 323 | final String[] data = response.getData(); 324 | final boolean[] a1 = SerialUtils.convertHexToBooleanArray(data[0]); 325 | final boolean[] b1 = SerialUtils.convertHexToBooleanArray(data[1]); 326 | final Fault fault1 = Fault.decode(a1, b1); 327 | LOG.info("Parsed fault code1: {}", fault1.getCode()); 328 | if (!"P0000".equals(fault1.getCode())) { 329 | faults.add(fault1); 330 | } 331 | final boolean[] a2 = SerialUtils.convertHexToBooleanArray(data[2]); 332 | final boolean[] b2 = SerialUtils.convertHexToBooleanArray(data[3]); 333 | final Fault fault2 = Fault.decode(a2, b2); 334 | LOG.info("Parsed fault code2: {}", fault2.getCode()); 335 | if (!"P0000".equals(fault2.getCode())) { 336 | faults.add(fault2); 337 | } 338 | final boolean[] a3 = SerialUtils.convertHexToBooleanArray(data[4]); 339 | final boolean[] b3 = SerialUtils.convertHexToBooleanArray(data[5]); 340 | final Fault fault3 = Fault.decode(a3, b3); 341 | LOG.info("Parsed fault code3: {}", fault3.getCode()); 342 | if (!"P0000".equals(fault3.getCode())) { 343 | faults.add(fault3); 344 | } 345 | } 346 | 347 | return faults; 348 | } 349 | 350 | 351 | /** 352 | * @param freezed 353 | * @return engine load in percents. 354 | * @throws OBD2Exception 355 | */ 356 | public Double getEngineLoad(final boolean freezed) { 357 | LOG.debug("getEngineLoad(freezed={})", freezed); 358 | final Response line = askOneLine(getMode(freezed), PID.ENGINE_LOAD); 359 | if (line.isError()) { 360 | return null; 361 | } 362 | final int encoded = Integer.parseInt(line.getData()[0], 16); 363 | return (encoded * 100.0) / 255.0; 364 | } 365 | 366 | 367 | public Integer getEngineCoolantTemperature(final boolean freezed) { 368 | LOG.debug("getEngineCoolantTemperature(freezed={})", freezed); 369 | final Response line = askOneLine(getMode(freezed), PID.ENGINE_COOLANT_TEMPERATURE); 370 | if (line.isError()) { 371 | return null; 372 | } 373 | // TODO: three bytes?! 374 | final int encoded = Integer.parseInt(line.getData()[0], 16); 375 | return encoded - 40; 376 | } 377 | 378 | 379 | public Integer getEngineOilTemperature(final boolean freezed) { 380 | LOG.debug("getEngineOilTemperature(freezed={})", freezed); 381 | final Response line = askOneLine(getMode(freezed), PID.ENGINE_OIL_TEMPERATURE); 382 | if (line.isError()) { 383 | return null; 384 | } 385 | final int encoded = Integer.parseInt(line.getData()[0], 16); 386 | return encoded - 40; 387 | } 388 | 389 | 390 | public Integer getIntakeAirTemperature(final boolean freezed) { 391 | LOG.debug("getIntakeAirTemperature(freezed={})", freezed); 392 | final Response line = askOneLine(getMode(freezed), PID.INTAKE_AIR_TEMPERATURE); 393 | if (line.isError()) { 394 | return null; 395 | } 396 | final int encoded = Integer.parseInt(line.getData()[0], 16); 397 | return encoded - 40; 398 | } 399 | 400 | 401 | public Integer getIntakeAirTemperatureSensor(final boolean freezed) { 402 | LOG.debug("getIntakeAirTemperatureSensor(freezed={})", freezed); 403 | final Response line = askOneLine(getMode(freezed), PID.INTAKE_AIR_TEMPERATURE_SENSOR); 404 | if (line.isError()) { 405 | return null; 406 | } 407 | // TODO: what is it? 408 | final int encodedA = Integer.parseInt(line.getData()[0], 16); 409 | // final int encodedB = Integer.parseInt(line.getData()[0], 16); 410 | // final int encodedC = Integer.parseInt(line.getData()[0], 16); 411 | // final int encodedD = Integer.parseInt(line.getData()[0], 16); 412 | // final int encodedE = Integer.parseInt(line.getData()[0], 16); 413 | // final int encodedF = Integer.parseInt(line.getData()[0], 16); 414 | // final int encodedG = Integer.parseInt(line.getData()[0], 16); 415 | return encodedA - 40; 416 | } 417 | 418 | 419 | public Integer getExhaustGasRecirculationTemperature(final boolean freezed) { 420 | LOG.debug("getExhaustGasRecirculationTemperature(freezed={})", freezed); 421 | 422 | final Response line = askOneLine(getMode(freezed), PID.EXHAUST_GAS_RECIRCULATION_TEMPERATURE); 423 | if (line.isError()) { 424 | return null; 425 | } 426 | // TODO: what is it? 427 | final int encodedA = Integer.parseInt(line.getData()[0], 16); 428 | // final int encodedB = Integer.parseInt(line.getData()[0], 16); 429 | // final int encodedC = Integer.parseInt(line.getData()[0], 16); 430 | // final int encodedD = Integer.parseInt(line.getData()[0], 16); 431 | // final int encodedE = Integer.parseInt(line.getData()[0], 16); 432 | return encodedA - 40; 433 | 434 | } 435 | 436 | 437 | public Integer getAmbientAirTemperature(final boolean freezed) { 438 | LOG.debug("getAmbientAirTemperature(freezed={})", freezed); 439 | final Response line = askOneLine(getMode(freezed), PID.AMBIENT_AIR_TEMPERATURE); 440 | if (line.isError()) { 441 | return null; 442 | } 443 | final int encoded = Integer.parseInt(line.getData()[0], 16); 444 | return encoded - 40; 445 | } 446 | 447 | 448 | public Integer getManifoldSurfaceTemperature(final boolean freezed) { 449 | LOG.debug("getManifoldSurfaceTemperature(freezed={})", freezed); 450 | final Response line = askOneLine(getMode(freezed), PID.MANIFOLD_SURFACE_TEMPERATURE); 451 | if (line.isError()) { 452 | return null; 453 | } 454 | // TODO: how many bytes? 455 | final int encoded = Integer.parseInt(line.getData()[0], 16); 456 | return encoded - 40; 457 | } 458 | 459 | 460 | public Double getFuelTrimPercent(final boolean freezed, final boolean longTerm, final int bank) { 461 | LOG.debug("getFuelTrimPercent(freezed={}, longTerm={}, bank={})", freezed, longTerm, bank); 462 | final PID pid; 463 | if (bank == 1) { 464 | pid = longTerm ? PID.FUEL_TRIM_PERCENT_LONG_BANK1 : PID.FUEL_TRIM_PERCENT_SHORT_BANK1; 465 | } else if (bank == 2) { 466 | pid = longTerm ? PID.FUEL_TRIM_PERCENT_LONG_BANK2 : PID.FUEL_TRIM_PERCENT_SHORT_BANK2; 467 | } else { 468 | throw new IllegalArgumentException("Invalid bank: " + bank); 469 | } 470 | final Response line = askOneLine(getMode(freezed), pid); 471 | if (line.isError()) { 472 | return null; 473 | } 474 | final int encoded = Integer.parseInt(line.getData()[0], 16); 475 | return ((encoded - 128.0) * 100.0) / 128.0; 476 | } 477 | 478 | 479 | public Double getCatalystTemperature(final boolean freezed, final int bank, final int sensor) { 480 | LOG.debug("getCatalystTemperature(freezed={}, bank={}, sensor={})", freezed, bank, sensor); 481 | final PID pid; 482 | if (sensor != 1 && sensor != 2) { 483 | throw new IllegalArgumentException("Invalid sensor: " + sensor); 484 | } 485 | if (bank == 1) { 486 | if (sensor == 1) { 487 | pid = PID.CATALYST_TEMPERATURE_BANK1_SENSOR1; 488 | } else { 489 | pid = PID.CATALYST_TEMPERATURE_BANK1_SENSOR2; 490 | } 491 | } else if (bank == 2) { 492 | if (sensor == 1) { 493 | pid = PID.CATALYST_TEMPERATURE_BANK2_SENSOR1; 494 | } else { 495 | pid = PID.CATALYST_TEMPERATURE_BANK2_SENSOR2; 496 | } 497 | } else { 498 | throw new IllegalArgumentException("Invalid bank: " + bank); 499 | } 500 | 501 | final Response response = askOneLine(getMode(freezed), pid); 502 | if (response.isError()) { 503 | return null; 504 | } 505 | final int encodedA = Integer.parseInt(response.getData()[0], 16); 506 | final int encodedB = Integer.parseInt(response.getData()[1], 16); 507 | return ((encodedA * 256.0 + encodedB) / 10.0) - 40.0; 508 | } 509 | 510 | 511 | public Integer getDistanceWithMalfunction(final boolean freezed) { 512 | LOG.debug("getDistanceWithMalfunction(freezed={})", freezed); 513 | final Response line = askOneLine(getMode(freezed), PID.DISTANCE_WITH_MALFUNCTION); 514 | if (line.isError()) { 515 | return null; 516 | } 517 | final int encodedA = Integer.parseInt(line.getData()[0], 16); 518 | final int encodedB = Integer.parseInt(line.getData()[1], 16); 519 | return encodedA * 256 + encodedB; 520 | } 521 | 522 | 523 | public Integer getDistanceSinceCodesCleared(final boolean freezed) { 524 | LOG.debug("getDistanceSinceCodesCleared(freezed={})", freezed); 525 | final Response line = askOneLine(getMode(freezed), PID.DISTANCE_FROM_CODES_CLEARED); 526 | if (line.isError()) { 527 | return null; 528 | } 529 | final int encodedA = Integer.parseInt(line.getData()[0], 16); 530 | final int encodedB = Integer.parseInt(line.getData()[1], 16); 531 | return encodedA * 256 + encodedB; 532 | } 533 | 534 | 535 | public Double getEthanolFuel(final boolean freezed) { 536 | LOG.debug("getEthanolFuel(freezed={})", freezed); 537 | final Response line = askOneLine(getMode(freezed), PID.ETHANOL_FUEL); 538 | if (line.isError()) { 539 | return null; 540 | } 541 | final int encoded = Integer.parseInt(line.getData()[0], 16); 542 | return (encoded * 100.0) / 255.0; 543 | } 544 | 545 | 546 | public Double getFuelLevelInput(final boolean freezed) { 547 | LOG.debug("getFuelLevelInput(freezed={})", freezed); 548 | final Response line = askOneLine(getMode(freezed), PID.FUEL_LEVEL_INPUT); 549 | if (line.isError()) { 550 | return null; 551 | } 552 | final int encoded = Integer.parseInt(line.getData()[0], 16); 553 | return (encoded * 100.0) / 255.0; 554 | } 555 | 556 | 557 | public Double getFuelInjectionTiming(final boolean freezed) { 558 | LOG.debug("getFuelInjectionTiming(freezed={})", freezed); 559 | final Response line = askOneLine(getMode(freezed), PID.FUEL_INJECTION_TIMING); 560 | if (line.isError()) { 561 | return null; 562 | } 563 | final int encodedA = Integer.parseInt(line.getData()[0], 16); 564 | final int encodedB = Integer.parseInt(line.getData()[1], 16); 565 | return (encodedA * 256.0 + encodedB - 26880.0) / 128.0; 566 | } 567 | 568 | 569 | public Double getFuelRate(final boolean freezed) { 570 | LOG.debug("getFuelRate(freezed={})", freezed); 571 | final Response line = askOneLine(getMode(freezed), PID.FUEL_RATE); 572 | if (line.isError()) { 573 | return null; 574 | } 575 | final int encodedA = Integer.parseInt(line.getData()[0], 16); 576 | final int encodedB = Integer.parseInt(line.getData()[1], 16); 577 | return (encodedA * 256.0 + encodedB) * 0.05; 578 | } 579 | 580 | 581 | public AirStatus getSecondaryAirStatus(final boolean freezed) { 582 | LOG.debug("getSecondaryAirStatus(freezed={})", freezed); 583 | final Response line = askOneLine(getMode(freezed), PID.SECONDARY_AIR_STATUS); 584 | if (line.isError()) { 585 | return null; 586 | } 587 | try { 588 | return AirStatus.parseHex(line.getData()[0]); 589 | } catch (final IllegalArgumentException e) { 590 | // see Issue #11 591 | LOG.warn(e.getMessage()); 592 | return null; 593 | } 594 | } 595 | 596 | 597 | public Double getCommandedEgr(final boolean freezed) { 598 | LOG.debug("getCommandedEgr(freezed={})", freezed); 599 | final Response line = askOneLine(getMode(freezed), PID.COMMANDED_EGR); 600 | if (line.isError()) { 601 | return null; 602 | } 603 | final int encoded = Integer.parseInt(line.getData()[0], 16); 604 | return (encoded * 100.0) / 255.0; 605 | } 606 | 607 | 608 | public Double getEgrError(final boolean freezed) { 609 | LOG.debug("getEgrError(freezed={})", freezed); 610 | final Response line = askOneLine(getMode(freezed), PID.EGR_ERROR); 611 | if (line.isError()) { 612 | return null; 613 | } 614 | final int encoded = Integer.parseInt(line.getData()[0], 16); 615 | return ((encoded - 128.0) * 100.0) / 128.0; 616 | } 617 | } 618 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/PID.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | import static cardiag.obd2.Mode.CURRENT_DATA; 7 | import static cardiag.obd2.Mode.DIAGNOSTIC; 8 | import static cardiag.obd2.Mode.FREEZE_FRAME_DATA; 9 | import static cardiag.obd2.Mode.VEHICLE_INFO; 10 | 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.apache.commons.lang3.builder.ReflectionToStringBuilder; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | /** 17 | * @author David Matějček 18 | */ 19 | public class PID { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(PID.class); 22 | 23 | public static final PID DIAGNOSTIC_CODES = new PID(0, DIAGNOSTIC); 24 | public static final PID CLEAR_TROUBLE_CODES = new PID(0, Mode.CLEAR_TROUBLE_CODES); 25 | public static final PID VIN_COUNT_OF_BYTES = new PID(1, VEHICLE_INFO); 26 | public static final PID VIN = new PID(2, VEHICLE_INFO); 27 | 28 | public static final PID PIDS_SUPPORTED = new PID(0, CURRENT_DATA); 29 | public static final PID MONITOR_STATUS = new PID(1, FREEZE_FRAME_DATA, CURRENT_DATA); 30 | public static final PID FUEL_STATUS = new PID(3, FREEZE_FRAME_DATA, CURRENT_DATA); 31 | public static final PID ENGINE_LOAD = new PID(4, FREEZE_FRAME_DATA, CURRENT_DATA); 32 | public static final PID ENGINE_COOLANT_TEMPERATURE = new PID(5, FREEZE_FRAME_DATA, CURRENT_DATA); 33 | public static final PID FUEL_TRIM_PERCENT_SHORT_BANK1 = new PID(6, FREEZE_FRAME_DATA, CURRENT_DATA); 34 | public static final PID FUEL_TRIM_PERCENT_LONG_BANK1 = new PID(7, FREEZE_FRAME_DATA, CURRENT_DATA); 35 | public static final PID FUEL_TRIM_PERCENT_SHORT_BANK2 = new PID(8, FREEZE_FRAME_DATA, CURRENT_DATA); 36 | public static final PID FUEL_TRIM_PERCENT_LONG_BANK2 = new PID(9, FREEZE_FRAME_DATA, CURRENT_DATA); 37 | 38 | public static final PID INTAKE_AIR_TEMPERATURE = new PID(0x0F, FREEZE_FRAME_DATA, CURRENT_DATA); 39 | public static final PID INTAKE_AIR_TEMPERATURE_SENSOR = new PID(0x68, FREEZE_FRAME_DATA, CURRENT_DATA); 40 | 41 | public static final PID SECONDARY_AIR_STATUS = new PID(0x12, FREEZE_FRAME_DATA, CURRENT_DATA); 42 | public static final PID ECU_COMPATIBILITY = new PID(0x1c, CURRENT_DATA); 43 | 44 | public static final PID DISTANCE_WITH_MALFUNCTION = new PID(0x21, FREEZE_FRAME_DATA, CURRENT_DATA); 45 | public static final PID COMMANDED_EGR = new PID(0x2c, FREEZE_FRAME_DATA, CURRENT_DATA); 46 | public static final PID EGR_ERROR = new PID(0x2d, FREEZE_FRAME_DATA, CURRENT_DATA); 47 | public static final PID FUEL_LEVEL_INPUT = new PID(0x2F, FREEZE_FRAME_DATA, CURRENT_DATA); 48 | 49 | public static final PID DISTANCE_FROM_CODES_CLEARED = new PID(0x31, FREEZE_FRAME_DATA, CURRENT_DATA); 50 | public static final PID CATALYST_TEMPERATURE_BANK1_SENSOR1 = new PID(0x3c, FREEZE_FRAME_DATA, CURRENT_DATA); 51 | public static final PID CATALYST_TEMPERATURE_BANK2_SENSOR1 = new PID(0x3d, FREEZE_FRAME_DATA, CURRENT_DATA); 52 | public static final PID CATALYST_TEMPERATURE_BANK1_SENSOR2 = new PID(0x3e, FREEZE_FRAME_DATA, CURRENT_DATA); 53 | public static final PID CATALYST_TEMPERATURE_BANK2_SENSOR2 = new PID(0x3f, FREEZE_FRAME_DATA, CURRENT_DATA); 54 | 55 | public static final PID AMBIENT_AIR_TEMPERATURE = new PID(0x46, FREEZE_FRAME_DATA, CURRENT_DATA); 56 | 57 | public static final PID ETHANOL_FUEL = new PID(0x51, FREEZE_FRAME_DATA, CURRENT_DATA); 58 | public static final PID ENGINE_OIL_TEMPERATURE = new PID(0x5c, FREEZE_FRAME_DATA, CURRENT_DATA); 59 | public static final PID FUEL_INJECTION_TIMING = new PID(0x5d, FREEZE_FRAME_DATA, CURRENT_DATA); 60 | public static final PID FUEL_RATE = new PID(0x5e, FREEZE_FRAME_DATA, CURRENT_DATA); 61 | 62 | public static final PID EXHAUST_GAS_RECIRCULATION_TEMPERATURE = new PID(0x6b, FREEZE_FRAME_DATA, CURRENT_DATA); 63 | 64 | public static final PID MANIFOLD_SURFACE_TEMPERATURE = new PID(0x84, FREEZE_FRAME_DATA, CURRENT_DATA); 65 | 66 | 67 | private final Mode[] allowedModes; 68 | private final int code; 69 | 70 | 71 | protected PID(final int pidCode, final Mode... modesAllowed) { 72 | this.code = pidCode; 73 | this.allowedModes = modesAllowed; 74 | } 75 | 76 | 77 | public String hex() { 78 | return StringUtils.leftPad(Integer.toHexString(code), 2, '0'); 79 | } 80 | 81 | 82 | @Override 83 | public String toString() { 84 | return ReflectionToStringBuilder.toString(this); 85 | } 86 | 87 | 88 | // the problem is that some pids have very different meaning in different modes. 89 | public static PID parseHex(final String hex, final Mode mode) { 90 | LOG.debug("parseHex(hex={}, mode={})", hex, mode); 91 | if (hex == null) { 92 | throw new IllegalArgumentException("Invalid PID: " + hex); 93 | } 94 | final int code = Integer.parseInt(hex, 16); 95 | switch (code) { 96 | case 0: 97 | switch (mode) { 98 | case CURRENT_DATA: 99 | return valid(mode, PIDS_SUPPORTED); 100 | case DIAGNOSTIC: 101 | return valid(mode, DIAGNOSTIC_CODES); 102 | case CLEAR_TROUBLE_CODES: 103 | return valid(mode, CLEAR_TROUBLE_CODES); 104 | default: 105 | break; 106 | } 107 | case 1: 108 | switch (mode) { 109 | case VEHICLE_INFO: 110 | return valid(mode, VIN_COUNT_OF_BYTES); 111 | default: 112 | return valid(mode, MONITOR_STATUS); 113 | } 114 | case 2: 115 | return valid(mode, VIN); 116 | case 3: 117 | return valid(mode, FUEL_STATUS); 118 | case 4: 119 | return valid(mode, ENGINE_LOAD); 120 | case 5: 121 | return valid(mode, ENGINE_COOLANT_TEMPERATURE); 122 | case 6: 123 | return valid(mode, FUEL_TRIM_PERCENT_SHORT_BANK1); 124 | case 7: 125 | return valid(mode, FUEL_TRIM_PERCENT_LONG_BANK2); 126 | case 8: 127 | return valid(mode, FUEL_TRIM_PERCENT_SHORT_BANK1); 128 | case 9: 129 | return valid(mode, FUEL_TRIM_PERCENT_LONG_BANK2); 130 | case 0x0F: 131 | return valid(mode, INTAKE_AIR_TEMPERATURE); 132 | case 0x12: 133 | return valid(mode, SECONDARY_AIR_STATUS); 134 | case 0x1C: 135 | return valid(mode, ECU_COMPATIBILITY); 136 | case 0x21: 137 | return valid(mode, DISTANCE_WITH_MALFUNCTION); 138 | case 0x2c: 139 | return valid(mode, COMMANDED_EGR); 140 | case 0x2d: 141 | return valid(mode, EGR_ERROR); 142 | case 0x2F: 143 | return valid(mode, FUEL_LEVEL_INPUT); 144 | case 0x31: 145 | return valid(mode, DISTANCE_FROM_CODES_CLEARED); 146 | case 0x3c: 147 | return valid(mode, CATALYST_TEMPERATURE_BANK1_SENSOR1); 148 | case 0x3d: 149 | return valid(mode, CATALYST_TEMPERATURE_BANK2_SENSOR1); 150 | case 0x3e: 151 | return valid(mode, CATALYST_TEMPERATURE_BANK1_SENSOR2); 152 | case 0x3f: 153 | return valid(mode, CATALYST_TEMPERATURE_BANK2_SENSOR2); 154 | case 0x46: 155 | return valid(mode, AMBIENT_AIR_TEMPERATURE); 156 | case 0x51: 157 | return valid(mode, ETHANOL_FUEL); 158 | case 0x5c: 159 | return valid(mode, ENGINE_OIL_TEMPERATURE); 160 | case 0x5d: 161 | return valid(mode, FUEL_INJECTION_TIMING); 162 | case 0x5e: 163 | return valid(mode, FUEL_RATE); 164 | case 0x68: 165 | return valid(mode, INTAKE_AIR_TEMPERATURE_SENSOR); 166 | case 0x6b: 167 | return valid(mode, EXHAUST_GAS_RECIRCULATION_TEMPERATURE); 168 | case 0x84: 169 | return valid(mode, MANIFOLD_SURFACE_TEMPERATURE); 170 | default: 171 | throw new IllegalArgumentException("Invalid PID: " + hex); 172 | } 173 | } 174 | 175 | 176 | /** 177 | * @param mode 178 | * @param pidsSupported 179 | * @return 180 | */ 181 | private static PID valid(Mode mode, PID pid) { 182 | for (Mode allowed : pid.allowedModes) { 183 | if (allowed == mode) { 184 | return pid; 185 | } 186 | } 187 | throw new IllegalArgumentException(String.format("The pid %s is not allowed to run in mode %s.", pid, mode)); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/Report.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.Date; 9 | import java.util.List; 10 | 11 | /** 12 | * @author David Matějček 13 | */ 14 | public class Report { 15 | 16 | private Date timestamp = new Date(); 17 | private boolean[] supportedPIDS; 18 | private MonitorStatus monitorStatus; 19 | private List faults; 20 | private Integer distanceSinceErrorCodesCleared; 21 | private Integer distanceWithMalfunction; 22 | private Integer engineCoolantTemperature; 23 | private Double engineLoad; 24 | private Double fuelInjectionTiming; 25 | private Double fuelLevelInput; 26 | private Double fuelRate; 27 | private FuelStatus fuelStatus; 28 | private Double fuelTrimPercentShortTerm; 29 | private Double fuelTrimPercentLongTerm; 30 | private Integer intakeAirTemperature; 31 | private AirStatus secondaryAirStatus; 32 | private EcuCompatibility ecuCompatibility; 33 | private Double catalystTemperatureSensor1; 34 | private Double catalystTemperatureSensor2; 35 | private Integer ambientAirTemperature; 36 | private Integer engineOilTemperature; 37 | private Integer intakeAirTemperatureSensor; 38 | private Integer exhaustGasRecirculationTemperature; 39 | private Integer manifoldSurfaceTemperature; 40 | private Double commandedEgr; 41 | private Double egrError; 42 | private Double ethanolFuel; 43 | 44 | 45 | public Date getTimestamp() { 46 | return timestamp; 47 | } 48 | 49 | public boolean[] getSupportedPIDS() { 50 | return Arrays.copyOf(supportedPIDS, supportedPIDS.length); 51 | } 52 | 53 | 54 | public void setSupportedPIDS(final boolean[] supportedPIDS) { 55 | if (supportedPIDS == null) { 56 | this.supportedPIDS = new boolean[0]; 57 | } else { 58 | this.supportedPIDS = Arrays.copyOf(supportedPIDS, supportedPIDS.length); 59 | } 60 | } 61 | 62 | 63 | public MonitorStatus getMonitorStatus() { 64 | return monitorStatus; 65 | } 66 | 67 | 68 | public void setMonitorStatus(final MonitorStatus monitorStatus) { 69 | this.monitorStatus = monitorStatus; 70 | } 71 | 72 | 73 | public Integer getDistanceSinceErrorCodesCleared() { 74 | return distanceSinceErrorCodesCleared; 75 | } 76 | 77 | 78 | public void setDistanceSinceErrorCodesCleared(final Integer distanceSinceErrorCodesCleared) { 79 | this.distanceSinceErrorCodesCleared = distanceSinceErrorCodesCleared; 80 | } 81 | 82 | 83 | public Integer getDistanceWithMalfunction() { 84 | return distanceWithMalfunction; 85 | } 86 | 87 | 88 | public void setDistanceWithMalfunction(final Integer distanceWithMalfunction) { 89 | this.distanceWithMalfunction = distanceWithMalfunction; 90 | } 91 | 92 | 93 | public Integer getEngineCoolantTemperature() { 94 | return engineCoolantTemperature; 95 | } 96 | 97 | 98 | public void setEngineCoolantTemperature(final Integer engineCoolantTemperature) { 99 | this.engineCoolantTemperature = engineCoolantTemperature; 100 | } 101 | 102 | 103 | public Double getEngineLoad() { 104 | return engineLoad; 105 | } 106 | 107 | 108 | public void setEngineLoad(final Double engineLoad) { 109 | this.engineLoad = engineLoad; 110 | } 111 | 112 | 113 | public Double getFuelInjectionTiming() { 114 | return fuelInjectionTiming; 115 | } 116 | 117 | 118 | public void setFuelInjectionTiming(final Double fuelInjectionTiming) { 119 | this.fuelInjectionTiming = fuelInjectionTiming; 120 | } 121 | 122 | 123 | public Double getFuelLevelInput() { 124 | return fuelLevelInput; 125 | } 126 | 127 | 128 | public void setFuelLevelInput(final Double fuelLevelInput) { 129 | this.fuelLevelInput = fuelLevelInput; 130 | } 131 | 132 | 133 | public Double getFuelRate() { 134 | return fuelRate; 135 | } 136 | 137 | 138 | public void setFuelRate(final Double fuelRate) { 139 | this.fuelRate = fuelRate; 140 | } 141 | 142 | 143 | public FuelStatus getFuelStatus() { 144 | return fuelStatus; 145 | } 146 | 147 | 148 | public void setFuelStatus(final FuelStatus fuelStatus) { 149 | this.fuelStatus = fuelStatus; 150 | } 151 | 152 | 153 | public Double getFuelTrimPercentShortTerm() { 154 | return fuelTrimPercentShortTerm; 155 | } 156 | 157 | 158 | public void setFuelTrimPercentShortTerm(final Double fuelTrimPercentShortTerm) { 159 | this.fuelTrimPercentShortTerm = fuelTrimPercentShortTerm; 160 | } 161 | 162 | 163 | public Double getFuelTrimPercentLongTerm() { 164 | return fuelTrimPercentLongTerm; 165 | } 166 | 167 | 168 | public void setFuelTrimPercentLongTerm(final Double fuelTrimPercentLongTerm) { 169 | this.fuelTrimPercentLongTerm = fuelTrimPercentLongTerm; 170 | } 171 | 172 | 173 | public Integer getIntakeAirTemperature() { 174 | return intakeAirTemperature; 175 | } 176 | 177 | 178 | public void setIntakeAirTemperature(final Integer intakeAirTemperature) { 179 | this.intakeAirTemperature = intakeAirTemperature; 180 | } 181 | 182 | 183 | public void setFaults(final List faults) { 184 | this.faults = faults; 185 | } 186 | 187 | 188 | public List getFaults() { 189 | if (this.faults == null) { 190 | return null; 191 | } 192 | return Collections.unmodifiableList(this.faults); 193 | } 194 | 195 | 196 | public AirStatus getSecondaryAirStatus() { 197 | return secondaryAirStatus; 198 | } 199 | 200 | 201 | public void setSecondaryAirStatus(final AirStatus secondaryAirStatus) { 202 | this.secondaryAirStatus = secondaryAirStatus; 203 | } 204 | 205 | 206 | public EcuCompatibility getEcuCompatibility() { 207 | return ecuCompatibility; 208 | } 209 | 210 | 211 | public void setEcuCompatibility(final EcuCompatibility ecuCompatibility) { 212 | this.ecuCompatibility = ecuCompatibility; 213 | } 214 | 215 | 216 | public Double getCatalystTemperatureSensor1() { 217 | return catalystTemperatureSensor1; 218 | } 219 | 220 | 221 | public void setCatalystTemperatureSensor1(final Double catalystTemperatureSensor1) { 222 | this.catalystTemperatureSensor1 = catalystTemperatureSensor1; 223 | } 224 | 225 | 226 | public Double getCatalystTemperatureSensor2() { 227 | return catalystTemperatureSensor2; 228 | } 229 | 230 | 231 | public void setCatalystTemperatureSensor2(final Double catalystTemperatureSensor2) { 232 | this.catalystTemperatureSensor2 = catalystTemperatureSensor2; 233 | } 234 | 235 | 236 | public Integer getAmbientAirTemperature() { 237 | return ambientAirTemperature; 238 | } 239 | 240 | 241 | public void setAmbientAirTemperature(final Integer ambientAirTemperature) { 242 | this.ambientAirTemperature = ambientAirTemperature; 243 | } 244 | 245 | 246 | public Integer getEngineOilTemperature() { 247 | return engineOilTemperature; 248 | } 249 | 250 | 251 | public void setEngineOilTemperature(final Integer engineOilTemperature) { 252 | this.engineOilTemperature = engineOilTemperature; 253 | } 254 | 255 | 256 | public Integer getIntakeAirTemperatureSensor() { 257 | return intakeAirTemperatureSensor; 258 | } 259 | 260 | 261 | public void setIntakeAirTemperatureSensor(final Integer intakeAirTemperatureSensor) { 262 | this.intakeAirTemperatureSensor = intakeAirTemperatureSensor; 263 | } 264 | 265 | 266 | public Integer getExhaustGasRecirculationTemperature() { 267 | return exhaustGasRecirculationTemperature; 268 | } 269 | 270 | 271 | public void setExhaustGasRecirculationTemperature(final Integer exhaustGasRecirculationTemperature) { 272 | this.exhaustGasRecirculationTemperature = exhaustGasRecirculationTemperature; 273 | } 274 | 275 | 276 | public Integer getManifoldSurfaceTemperature() { 277 | return manifoldSurfaceTemperature; 278 | } 279 | 280 | 281 | public void setManifoldSurfaceTemperature(final Integer manifoldSurfaceTemperature) { 282 | this.manifoldSurfaceTemperature = manifoldSurfaceTemperature; 283 | } 284 | 285 | 286 | public Double getCommandedEgr() { 287 | return commandedEgr; 288 | } 289 | 290 | 291 | public void setCommandedEgr(final Double commandedEgr) { 292 | this.commandedEgr = commandedEgr; 293 | } 294 | 295 | 296 | public Double getEgrError() { 297 | return egrError; 298 | } 299 | 300 | 301 | public void setEgrError(final Double egrError) { 302 | this.egrError = egrError; 303 | } 304 | 305 | 306 | public Double getEthanolFuel() { 307 | return ethanolFuel; 308 | } 309 | 310 | 311 | public void setEthanolFuel(final Double ethanolFuel) { 312 | this.ethanolFuel = ethanolFuel; 313 | } 314 | 315 | } 316 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/Response.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | /** 7 | * OBD2 protocol response. 8 | * 9 | * @author David Matějček 10 | */ 11 | public class Response { 12 | 13 | private final Mode mode; 14 | private final PID pid; 15 | private final String[] data; 16 | private boolean error; 17 | 18 | 19 | /** 20 | * Only a simple constructor. 21 | * 22 | * @param error - if true, data are an error message 23 | * @param mode - same as the mode of the request. 24 | * @param pid - same as the pid of the request. 25 | * @param data - returned data. 26 | */ 27 | public Response(final boolean error, final Mode mode, final PID pid, final String... data) { 28 | this.mode = mode; 29 | this.pid = pid; 30 | this.data = data == null ? new String[1] : data; 31 | this.error = error; 32 | } 33 | 34 | 35 | /** 36 | * @return a data part of the response - several lines of hex numbers separated by a space. 37 | */ 38 | public String[] getData() { 39 | return data; 40 | } 41 | 42 | 43 | /** 44 | * @return true if the data contains only error message. 45 | */ 46 | public boolean isError() { 47 | return error; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/cardiag/obd2/ResponseWithNoData.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | 7 | /** 8 | * @author David Matějček 9 | * 10 | */ 11 | public class ResponseWithNoData extends Response { 12 | 13 | /** 14 | * @param mode 15 | * @param pid 16 | */ 17 | public ResponseWithNoData(Mode mode, PID pid) { 18 | super(true, mode, pid); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/cardiag/output/OutputFileWriter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.output; 5 | 6 | import java.io.Closeable; 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.io.OutputStreamWriter; 11 | import java.nio.charset.Charset; 12 | 13 | import org.apache.commons.io.IOUtils; 14 | import org.apache.commons.lang3.StringUtils; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | /** 19 | * @author David Matějček 20 | */ 21 | public class OutputFileWriter implements Closeable { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(OutputFileWriter.class); 24 | 25 | private final OutputStreamWriter writer; 26 | private final File outputFile; 27 | 28 | 29 | /** 30 | * Initializes the output. 31 | * 32 | * @param outputFile 33 | */ 34 | public OutputFileWriter(final File outputFile) { 35 | this.outputFile = outputFile; 36 | this.writer = openWriter(outputFile); 37 | } 38 | 39 | 40 | private OutputStreamWriter openWriter(final File file) { 41 | LOG.trace("openWriter(outputFile={})", file); 42 | 43 | FileOutputStream stream = null; 44 | try { 45 | stream = new FileOutputStream(file, true); 46 | return new OutputStreamWriter(stream, Charset.forName("UTF-8")); 47 | } catch (final IOException e) { 48 | IOUtils.closeQuietly(stream); 49 | throw new IllegalArgumentException("Cannot write the report to the outputFile: " + file, e); 50 | } 51 | } 52 | 53 | 54 | /** 55 | * Writes a header. 56 | * 57 | * @param header 58 | */ 59 | public void writeHeader(final String header) { 60 | LOG.debug("writeHeader(header={})", header); 61 | try { 62 | this.writer.write('\n'); 63 | final String lineSeparator = StringUtils.repeat("-", header.length()) + '\n'; 64 | this.writer.write(lineSeparator); 65 | this.writer.write(header + '\n'); 66 | this.writer.write(lineSeparator); 67 | } catch (IOException e) { 68 | throw new IllegalStateException("Cannot write to the outputFile: " + outputFile, e); 69 | } 70 | } 71 | 72 | 73 | /** 74 | * Writes parameters into the single line in the outputFile. 75 | * 76 | * @param label 77 | * @param value - may be null, then -- are written. 78 | * @param unit - may be null, then not written. 79 | */ 80 | public void writeData(final String label, final Object value, final String unit) { 81 | LOG.debug("writeData(label={}, value={}, unit={})", label, value, unit); 82 | 83 | final StringBuilder string = new StringBuilder(256); 84 | string.append(label).append(": "); 85 | if (value == null) { 86 | string.append("--"); 87 | } else { 88 | string.append(value); 89 | } 90 | if (unit != null) { 91 | string.append(' ').append(unit); 92 | } 93 | string.append('\n'); 94 | 95 | try { 96 | this.writer.write(string.toString()); 97 | } catch (IOException e) { 98 | throw new IllegalStateException("Cannot write to the outputFile: " + outputFile, e); 99 | } 100 | } 101 | 102 | 103 | /** 104 | * Silently closes the outputFile. 105 | */ 106 | public void close() { 107 | IOUtils.closeQuietly(writer); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/cardiag/output/ReportFileWriter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.output; 5 | 6 | import java.io.File; 7 | import java.text.SimpleDateFormat; 8 | 9 | import org.apache.commons.io.IOUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import cardiag.obd2.Report; 14 | 15 | /** 16 | * @author David Matějček 17 | */ 18 | public class ReportFileWriter { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(ReportFileWriter.class); 21 | 22 | private static final String DEGREES = "°"; 23 | private static final String DEGREES_OF_CENTIGRADE = "°C"; 24 | private static final String KILOMETERS = "km"; 25 | private static final String PERCENTS = "%"; 26 | 27 | private final File outputFile; 28 | 29 | 30 | /** 31 | * @param outputFile 32 | */ 33 | public ReportFileWriter(final File outputFile) { 34 | this.outputFile = outputFile; 35 | } 36 | 37 | 38 | /** 39 | * Opens the file, writes the report and closes the file. 40 | * 41 | * @param report 42 | */ 43 | public void write(final Report report) { 44 | final OutputFileWriter writer = new OutputFileWriter(outputFile); 45 | try { 46 | write(writer, report); 47 | } finally { 48 | IOUtils.closeQuietly(writer); 49 | } 50 | } 51 | 52 | 53 | private void write(final OutputFileWriter writer, final Report report) { 54 | LOG.trace("write(writer, report)"); 55 | 56 | writer.writeHeader("Basic info"); 57 | 58 | final SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy, HH:mm:ss.SSS"); 59 | writer.writeData("Time:", sdf.format(report.getTimestamp()), null); 60 | writer.writeData("ECU compatibility", report.getEcuCompatibility(), null); 61 | writer.writeData("Supported PIDs", report.getSupportedPIDS(), null); 62 | writer.writeData("Monitor status", report.getMonitorStatus(), null); 63 | writer.writeData("Ambient temperature", report.getAmbientAirTemperature(), DEGREES_OF_CENTIGRADE); 64 | 65 | writer.writeHeader("Errors"); 66 | writer.writeData("Distance since errors cleared", report.getDistanceSinceErrorCodesCleared(), KILOMETERS); 67 | writer.writeData("Distance with malfunction", report.getDistanceWithMalfunction(), KILOMETERS); 68 | writer.writeData("Reported faults", report.getFaults(), null); 69 | 70 | writer.writeHeader("Engine Temperatures"); 71 | writer.writeData("Engine coolant temperature", report.getEngineCoolantTemperature(), DEGREES_OF_CENTIGRADE); 72 | writer.writeData("Engine oil temperature", report.getEngineOilTemperature(), DEGREES_OF_CENTIGRADE); 73 | writer.writeData("Intake air temperature", report.getIntakeAirTemperature(), DEGREES_OF_CENTIGRADE); 74 | writer.writeData("Intake air temperature sensor", report.getIntakeAirTemperatureSensor(), DEGREES_OF_CENTIGRADE); 75 | writer.writeData("Catalyst temperature - sensor1", report.getCatalystTemperatureSensor1(), DEGREES_OF_CENTIGRADE); 76 | writer.writeData("Catalyst temperature - sensor2", report.getCatalystTemperatureSensor2(), DEGREES_OF_CENTIGRADE); 77 | writer.writeData("Exhaust gas recirculation temperature", report.getExhaustGasRecirculationTemperature(), 78 | DEGREES_OF_CENTIGRADE); 79 | writer.writeData("Manifold surface temperature", report.getManifoldSurfaceTemperature(), DEGREES_OF_CENTIGRADE); 80 | 81 | writer.writeHeader("Engine timing and fuel"); 82 | writer.writeData("Engine load", report.getEngineLoad(), PERCENTS); 83 | writer.writeData("Fuel injection timing", report.getFuelInjectionTiming(), DEGREES); 84 | writer.writeData("Secondary air status", report.getSecondaryAirStatus(), null); 85 | writer.writeData("Fuel status", report.getFuelStatus(), null); 86 | writer.writeData("Fuel rate", report.getFuelRate(), "L/h"); 87 | writer.writeData("Fuel level input", report.getFuelLevelInput(), PERCENTS); 88 | writer.writeData("Fuel trim pecent in long term", report.getFuelTrimPercentLongTerm(), PERCENTS); 89 | writer.writeData("Fuel trim pecent in short term", report.getFuelTrimPercentShortTerm(), PERCENTS); 90 | writer.writeData("Commanded EGR", report.getCommandedEgr(), PERCENTS); 91 | writer.writeData("EGR error", report.getEgrError(), PERCENTS); 92 | writer.writeData("Ethanol fuel", report.getEthanolFuel(), PERCENTS); 93 | 94 | // writer.writeData("", report.get, ""); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/cardiag/serial/PortCommunication.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.serial; 5 | 6 | import java.io.Closeable; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import jssc.SerialPort; 16 | import jssc.SerialPortException; 17 | 18 | /** 19 | * Communication with the serial port device. 20 | * 21 | * @author David Matějček 22 | */ 23 | public class PortCommunication implements Closeable { 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(PortCommunication.class); 26 | private static final String RESPONSE_OK = "OK"; 27 | private static final String RESPONSE_TERMINALCHAR = ">"; 28 | 29 | private final PortConfiguration cfg; 30 | private final SerialPort port; 31 | 32 | 33 | /** 34 | * Initializes and opens the port. 35 | * 36 | * @param cfg - a port configuration 37 | * @throws PortCommunicationException - cannot initialize the communication. 38 | */ 39 | public PortCommunication(final PortConfiguration cfg) throws PortCommunicationException { 40 | LOG.debug("PortCommunication(cfg={})", cfg); 41 | try { 42 | this.cfg = cfg; 43 | this.port = new SerialPort(cfg.getPortName()); 44 | if (!this.port.openPort()) { 45 | throw new PortCommunicationException("Cannot open port!"); 46 | } 47 | // see page 7 in elm327.pdf - do not change! 48 | if (!this.port.setParams(38400, 8, 1, 0, true, true)) { 49 | throw new PortCommunicationException("Setting parameters was unsuccessful!"); 50 | } 51 | } catch (final SerialPortException e) { 52 | throw new PortCommunicationException(e); 53 | } 54 | } 55 | 56 | 57 | /** 58 | * Reads responses line by line from the buffer until {@value #RESPONSE_TERMINALCHAR} comes or 59 | * timeout occurs. 60 | * 61 | * @return a response, never null and never empty. 62 | * @throws PortCommunicationException - if there was no response or prompt was missing. 63 | */ 64 | public String readResponse(final String request) throws PortCommunicationException { 65 | LOG.debug("readResponse(request={})", request); 66 | 67 | try { 68 | final String response = readAll(request); 69 | LOG.debug("Received response: {}", response); 70 | if (response == null) { 71 | throw new PortCommunicationException("Retrieved no response from the port."); 72 | } 73 | return response; 74 | } catch (final SerialPortException e) { 75 | throw new PortCommunicationException(e); 76 | } 77 | } 78 | 79 | 80 | private String readAll(final String request) throws SerialPortException { 81 | LOG.trace("readAll(request={})", request); 82 | final StringBuilder buffer = new StringBuilder(256); 83 | final long start = System.currentTimeMillis(); 84 | final long timeout = cfg.getCommandTimeout(); 85 | while (true) { 86 | if (start + timeout < System.currentTimeMillis()) { 87 | throw new PortCommunicationException(String.format("Timeout %d ms occured.", timeout)); 88 | } 89 | if (this.port.getInputBufferBytesCount() == 0) { 90 | Thread.yield(); 91 | continue; 92 | } 93 | final String string = StringUtils.trimToNull(this.port.readString()); 94 | LOG.trace("response string={}", string); 95 | if (string == null) { 96 | sleep(100L); 97 | continue; 98 | } 99 | buffer.append(string); 100 | if (StringUtils.endsWith(string, RESPONSE_TERMINALCHAR)) { 101 | LOG.trace("Response terminated with the character {}. Character deleted.", RESPONSE_TERMINALCHAR); 102 | buffer.setLength(buffer.length() - 1); 103 | if (buffer.indexOf(request) == 0) { 104 | buffer.replace(0, request.length(), ""); 105 | } 106 | return buffer.toString().trim(); 107 | } 108 | } 109 | } 110 | 111 | 112 | private void sleep(final long timeInMillis) { 113 | try { 114 | Thread.sleep(100L); 115 | } catch (final InterruptedException e) { 116 | LOG.warn("Interrupted."); 117 | } 118 | } 119 | 120 | 121 | /** 122 | * Writes command and arguments to the port. 123 | * 124 | * @param command - command and it's arguments. 125 | * @throws PortCommunicationException 126 | */ 127 | public void writeln(final String... command) throws PortCommunicationException { 128 | LOG.debug("writeln(command={})", Arrays.toString(command)); 129 | try { 130 | for (final String commandPart : command) { 131 | this.port.writeString(commandPart); 132 | } 133 | this.port.writeString("\r\n"); 134 | } catch (final SerialPortException e) { 135 | throw new PortCommunicationException(e); 136 | } 137 | } 138 | 139 | 140 | /** 141 | * @param value 142 | * @return 1 for true, 0 for false. 143 | * @throws PortCommunicationException 144 | */ 145 | protected final String translate(final boolean value) throws PortCommunicationException { 146 | LOG.trace("translate(value={})", value); 147 | return value ? "1" : "0"; 148 | } 149 | 150 | 151 | /** 152 | * Checks for OK in port response. 153 | * @param request 154 | * 155 | * @throws PortCommunicationException 156 | */ 157 | protected void checkOkResponse(final String request) throws PortCommunicationException { 158 | LOG.trace("checkOkResponse(request={})", request); 159 | final String response = readResponse(request); 160 | if (response == null || response.isEmpty()) { 161 | throw new PortCommunicationException("No response."); 162 | } 163 | if (!RESPONSE_OK.equalsIgnoreCase(response)) { 164 | throw new PortCommunicationException("Command unsuccessful! Response: '" + response + "'"); 165 | } 166 | } 167 | 168 | 169 | /** 170 | * Executes an AT command and returns an answer. 171 | * 172 | * @param command - an AT command to execute. 173 | * @return an answer of the command. 174 | * @throws PortCommunicationException 175 | */ 176 | public String at(final String command) throws PortCommunicationException { 177 | LOG.info("at(command={})", command); 178 | writeln("AT", command); 179 | return readResponse("AT".concat(command)); 180 | } 181 | 182 | 183 | /** 184 | * Sends ATE signal and sets the command echo on/off 185 | * 186 | * @param on 187 | * @throws PortCommunicationException 188 | */ 189 | public void setEcho(final boolean on) throws PortCommunicationException { 190 | LOG.debug("setEcho(on={})", on); 191 | final String onTranslated = translate(on); 192 | writeln("ATE", onTranslated); 193 | checkOkResponse("ATE".concat(onTranslated)); 194 | } 195 | 196 | 197 | /** 198 | * Sends ATL signal and sets the line termination on/off 199 | * 200 | * @param on 201 | * @throws PortCommunicationException 202 | */ 203 | public void setLineTermination(final boolean on) throws PortCommunicationException { 204 | LOG.debug("setLineTermination(on={})", on); 205 | final String onTranslated = translate(on); 206 | writeln("ATL", onTranslated); 207 | checkOkResponse("ATL".concat(onTranslated)); 208 | } 209 | 210 | 211 | /** 212 | * Sends AT Z command, resets the communication. 213 | * 214 | * @throws PortCommunicationException 215 | */ 216 | public void reset() throws PortCommunicationException { 217 | LOG.debug("reset()"); 218 | final String response = at("Z"); 219 | // with echo on the first line will be ATZ (sent command) 220 | // another line will be a device type identification. 221 | if (response == null) { 222 | throw new PortCommunicationException("Command unsuccessful! Response: " + response); 223 | } 224 | } 225 | 226 | 227 | /** 228 | * Closes the port. 229 | */ 230 | @Override 231 | public void close() { 232 | try { 233 | this.port.closePort(); 234 | } catch (final SerialPortException e) { 235 | throw new IllegalStateException("Cannot close the port.", e); 236 | } 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /src/main/java/cardiag/serial/PortCommunicationException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.serial; 5 | 6 | import jssc.SerialPortException; 7 | 8 | /** 9 | * Port communication exception. Command was not successfully executed. 10 | * 11 | * @author David Matějček 12 | */ 13 | public class PortCommunicationException extends RuntimeException { 14 | 15 | private static final long serialVersionUID = 5530145486386126281L; 16 | 17 | 18 | /** 19 | * @param message - a short description of exception 20 | */ 21 | public PortCommunicationException(final String message) { 22 | super(message); 23 | } 24 | 25 | 26 | /** 27 | * @param cause - a cause of the exception. 28 | */ 29 | public PortCommunicationException(final SerialPortException cause) { 30 | super(cause); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/cardiag/serial/PortConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.serial; 5 | 6 | /** 7 | * @author David Matějček 8 | */ 9 | public class PortConfiguration { 10 | 11 | private String portName; 12 | private Long commandTimeout; 13 | 14 | 15 | public String getPortName() { 16 | return portName; 17 | } 18 | 19 | 20 | public void setPortName(final String portName) { 21 | this.portName = portName; 22 | } 23 | 24 | 25 | public Long getCommandTimeout() { 26 | return commandTimeout; 27 | } 28 | 29 | 30 | /** 31 | * @param commandTimeout - time in millis, must not be null. 32 | */ 33 | public void setCommandTimeout(final Long commandTimeout) { 34 | this.commandTimeout = commandTimeout; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/cardiag/serial/SerialUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.serial; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import jssc.SerialPortList; 15 | 16 | 17 | /** 18 | * @author David Matějček 19 | */ 20 | public class SerialUtils { 21 | private static final Logger LOG = LoggerFactory.getLogger(SerialUtils.class); 22 | 23 | public static List getPortNames() { 24 | final String[] portNames = SerialPortList.getPortNames(); 25 | if (portNames == null || portNames.length == 0) { 26 | return Collections.emptyList(); 27 | } 28 | 29 | return Arrays.asList(portNames); 30 | } 31 | 32 | /** 33 | * @param line 34 | * @return 35 | */ 36 | public static boolean[] convertHexToBooleanArray(final String... line) { 37 | LOG.debug("convertHexToBooleanArray(line={})", (Object) line); 38 | final boolean[] bools = new boolean[line.length * 8]; 39 | int k = 0; 40 | for (int i = 0; i < line.length; i++) { 41 | String hex = line[i]; 42 | int val = Integer.parseInt(hex, 16); 43 | String binary = StringUtils.leftPad(Integer.toBinaryString(val), 8, '0'); 44 | for (int j = 0; j < binary.length(); j++) { 45 | bools[k++] = binary.charAt(j) == '1'; 46 | } 47 | } 48 | return bools; 49 | } 50 | 51 | 52 | public static int toInteger(boolean... bools) { 53 | int base = 1; 54 | int index = bools.length - 1; 55 | int value = 0; 56 | while(index >= 0) { 57 | value += bools[index] ? base : 0; 58 | base *= 2; 59 | index--; 60 | } 61 | return value; 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/cardiag/user/Action.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.user; 5 | 6 | 7 | /** 8 | * @author David Matějček 9 | */ 10 | public enum Action { 11 | 12 | REPORT, 13 | WATCH, 14 | CLEAR_TROUBLE_CODES; 15 | 16 | 17 | public static Action parse(final String str) { 18 | for (Action action : Action.values()) { 19 | if (action.name().equalsIgnoreCase(str)) { 20 | return action; 21 | } 22 | } 23 | throw new IllegalArgumentException("Invalid action: " + str); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/cardiag/user/ConsoleCommunication.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.user; 5 | 6 | 7 | /** 8 | * @author David Matějček 9 | * 10 | */ 11 | public interface ConsoleCommunication { 12 | 13 | void format(String fmt, Object... args); 14 | void flush(); 15 | String readLine(); 16 | String readLine(String fmt, Object... args); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/cardiag/user/ConsoleWrapper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.user; 5 | 6 | import java.io.Console; 7 | 8 | /** 9 | * @author David Matějček 10 | */ 11 | public class ConsoleWrapper implements ConsoleCommunication { 12 | 13 | private final Console console; 14 | 15 | 16 | public ConsoleWrapper(final Console console) { 17 | if (console == null) { 18 | throw new IllegalArgumentException("Console not present, program cannot interact with user!"); 19 | } 20 | this.console = console; 21 | } 22 | 23 | 24 | @Override 25 | public void format(String fmt, Object... args) { 26 | this.console.format(fmt, args); 27 | } 28 | 29 | 30 | @Override 31 | public void flush() { 32 | this.console.flush(); 33 | } 34 | 35 | 36 | @Override 37 | public String readLine() { 38 | return this.console.readLine(); 39 | } 40 | 41 | 42 | @Override 43 | public String readLine(String fmt, Object... args) { 44 | return this.console.readLine(fmt, args); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cardiag/user/UserCommunication.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.user; 5 | 6 | import java.util.List; 7 | 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import cardiag.serial.PortConfiguration; 13 | 14 | /** 15 | * @author David Matějček 16 | */ 17 | public class UserCommunication { 18 | 19 | private static final Logger LOG = LoggerFactory.getLogger(UserCommunication.class); 20 | private static final String EXIT = "X"; 21 | 22 | private final ConsoleCommunication console; 23 | 24 | 25 | /** 26 | * @param console 27 | */ 28 | public UserCommunication(final ConsoleCommunication console) { 29 | LOG.trace("UserCommunication(console={})", console); 30 | if (console == null) { 31 | throw new IllegalArgumentException("Console not present, program cannot interact with user!"); 32 | } 33 | this.console = console; 34 | } 35 | 36 | 37 | /** 38 | * @param portNames 39 | * @return 40 | */ 41 | public PortConfiguration readPortConfiguration(final List portNames) { 42 | LOG.debug("readPortConfiguration(portNames={})", portNames); 43 | final PortConfiguration cfg = new PortConfiguration(); 44 | final String selectedPortName = selectPort(portNames); 45 | cfg.setPortName(selectedPortName); 46 | cfg.setCommandTimeout(10000L); 47 | return cfg; 48 | } 49 | 50 | 51 | protected String selectPort(final List portNames) { 52 | LOG.debug("selectPort(portNames={})", portNames); 53 | 54 | // FIXME: program should not end in the future. User maybe forgot to connect the cable. 55 | if (portNames.isEmpty()) { 56 | throw new IllegalStateException("No serial port to select."); 57 | } 58 | while (true) { 59 | console.format("----\nSelect the port (type number):\n"); 60 | for (int i = 0; i < portNames.size(); i++) { 61 | final String portName = portNames.get(i); 62 | console.format("%d: %s\n", i, portName); 63 | } 64 | console.format("X: Exit\n"); 65 | console.flush(); 66 | final String selectedPort = StringUtils.trimToNull(console.readLine("Port: ")); 67 | if (StringUtils.isNumeric(selectedPort)) { 68 | final String portName = portNames.get(Integer.valueOf(selectedPort)); 69 | if (portName != null) { 70 | return portName; 71 | } 72 | } 73 | if (EXIT.equalsIgnoreCase(selectedPort)) { 74 | return null; 75 | } 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.appender.FILEdevelop=org.apache.log4j.FileAppender 2 | log4j.appender.FILEdevelop.File=development.log 3 | log4j.appender.FILEdevelop.encoding=utf-8 4 | log4j.appender.FILEdevelop.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.FILEdevelop.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%t] %25c{3}:%-4L: %m%n 6 | log4j.appender.FILEdevelop.Append=true 7 | log4j.appender.FILEdevelop.Threshold=TRACE 8 | 9 | log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender 10 | log4j.appender.STDOUT.Target=System.out 11 | log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout 12 | log4j.appender.STDOUT.layout.ConversionPattern=%m%n 13 | log4j.appender.STDOUT.Threshold=INFO 14 | 15 | log4j.rootLogger=INFO, STDOUT, FILEdevelop 16 | log4j.logger.cardiag=TRACE 17 | #log4j.additivity.cardiag=false 18 | 19 | #log4j.debug=on 20 | -------------------------------------------------------------------------------- /src/test/java/cardiag/obd2/OBD2StandardITest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.obd2; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertNotNull; 8 | 9 | import org.junit.AfterClass; 10 | import org.junit.BeforeClass; 11 | import org.junit.Ignore; 12 | import org.junit.Test; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import cardiag.test.TestConfiguration; 17 | 18 | /** 19 | * @author David Matějček 20 | */ 21 | public class OBD2StandardITest { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(OBD2StandardITest.class); 24 | 25 | private static final TestConfiguration testConfig = new TestConfiguration(); 26 | private static OBD2Standard obd2; 27 | 28 | 29 | @BeforeClass 30 | public static void initCommunication() throws OBD2Exception { 31 | obd2 = new OBD2Standard(testConfig.getPortConfiguration()); 32 | } 33 | 34 | 35 | @AfterClass 36 | public static void close() { 37 | if (obd2 != null) { 38 | obd2.close(); 39 | } 40 | } 41 | 42 | 43 | /** 44 | * Received response: [41 03 01 00] 45 | * 46 | * @throws OBD2Exception 47 | */ 48 | @Test 49 | public void testSupportedPIDs() throws OBD2Exception { 50 | final boolean[] pids = obd2.getSupportedPIDs(); 51 | LOG.debug("pids={}", pids); 52 | assertNotNull(pids); 53 | assertEquals(32, pids.length); 54 | } 55 | 56 | 57 | @Test 58 | public void testFuelStatus() throws OBD2Exception { 59 | assertEquals(FuelStatus.CLOSED_USING_OXYGEN, obd2.getFuelStatus(false)); 60 | // assertEquals(FuelStatus.CLOSED_USING_OXYGEN, obd2.getFuelStatus(true)); 61 | } 62 | 63 | 64 | @Test 65 | public void testGetErrorReport() throws OBD2Exception { 66 | LOG.info("Error report: {}", obd2.getErrorReport()); 67 | } 68 | 69 | 70 | // @Test 71 | // public void testClearTroubleCodes() throws OBD2Exception { 72 | // obd2.clearTroubleCodes(); 73 | // LOG.warn("Trouble codes cleared!"); 74 | // } 75 | 76 | /** 77 | * [49 02 01 00 00 00 FF, 49 02 02 FF FF FF FF, 49 02 03 FF FF FF FF, 49 02 04 FF FF FF FF, 49 02 78 | * 05 FF FF FF FF] 79 | * 80 | * @throws OBD2Exception 81 | */ 82 | @Test 83 | public void testGetVIN() throws OBD2Exception { 84 | LOG.info("VIN={}", obd2.getVIN()); 85 | } 86 | 87 | 88 | /** 89 | * [41 04 00] = 0.0% 90 | * 91 | * @throws OBD2Exception 92 | */ 93 | @Test 94 | public void testGetEngineLoad() throws OBD2Exception { 95 | LOG.info("engine load: {}%", obd2.getEngineLoad(false)); 96 | // obd2.getEngineLoad(true); 97 | } 98 | 99 | 100 | /** 101 | * [41 05 37] = 15°C 102 | * 103 | * @throws OBD2Exception 104 | */ 105 | @Test 106 | public void testGetEngineCoolantTemp() throws OBD2Exception { 107 | LOG.info("engine coolant temperature: {}°C", obd2.getEngineCoolantTemperature(false)); 108 | // obd2.getEngineCoolantTemperature(true); 109 | } 110 | 111 | 112 | /** 113 | * [41 0F 36] = 14°C 114 | * 115 | * @throws OBD2Exception 116 | */ 117 | @Test 118 | public void testGetIntakeAirTemp() throws OBD2Exception { 119 | LOG.info("intake air temperature: {}°C", obd2.getIntakeAirTemperature(false)); 120 | } 121 | 122 | 123 | /** 124 | * [41 06 80] = 0.0% 125 | * [41 07 80] = 0.0% 126 | * 127 | * @throws OBD2Exception 128 | */ 129 | @Test 130 | public void testGetFuelTrimPercent() throws OBD2Exception { 131 | LOG.info("fuel trim bank1, short term: {}%", obd2.getFuelTrimPercent(false, false, 1)); 132 | LOG.info("fuel trim bank1, long term: {}%", obd2.getFuelTrimPercent(false, true, 1)); 133 | // LOG.info("fuel trim bank2, short term: {}%", obd2.getFuelTrimPercent(false, false, 2)); 134 | // LOG.info("fuel trim bank2, long term: {}%", obd2.getFuelTrimPercent(false, true, 2)); 135 | } 136 | 137 | 138 | /** 139 | * [41 21 00 00] 140 | * 141 | * @throws OBD2Exception 142 | */ 143 | @Test 144 | public void testGetDistanceWithMalfunction() throws OBD2Exception { 145 | LOG.info("distance with malfunction: {}", obd2.getDistanceWithMalfunction(false)); 146 | } 147 | 148 | 149 | @Test 150 | @Ignore("For Kalina causes error response 7F 01 12") 151 | public void testGetDistanceSinceCodesCleared() throws OBD2Exception { 152 | LOG.info("distance since codes cleared: {}", obd2.getDistanceSinceCodesCleared(false)); 153 | } 154 | 155 | 156 | @Test 157 | @Ignore("For Kalina causes error response 7F 01 12") 158 | public void testGetFuelLevelInput() throws OBD2Exception { 159 | LOG.info("fuel level input: {}%", obd2.getFuelLevelInput(false)); 160 | } 161 | 162 | 163 | @Test 164 | @Ignore("For Kalina causes error response 7F 01 12") 165 | public void testGetFuelInjectionTiming() throws OBD2Exception { 166 | LOG.info("fuel injection timing: {}°", obd2.getFuelInjectionTiming(false)); 167 | } 168 | 169 | 170 | @Test 171 | // @Ignore("For Kalina causes error response 7F 01 12") 172 | public void testGetFuelRate() throws OBD2Exception { 173 | LOG.info("fuel rate: {} L/h", obd2.getFuelRate(false)); 174 | } 175 | 176 | 177 | @Test 178 | /** 179 | * [41 01 03 07 65 00] 180 | * [41 01 00 07 65 65] 181 | * @throws OBD2Exception 182 | */ 183 | public void testGetMonitorStatus() throws OBD2Exception { 184 | LOG.info("monitor status: {}", obd2.getMonitorStatus()); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/test/java/cardiag/serial/PortCommunicationIT.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.serial; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | 8 | import org.junit.After; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | 12 | import cardiag.test.TestConfiguration; 13 | 14 | /** 15 | * @author David Matějček 16 | */ 17 | public class PortCommunicationIT { 18 | 19 | private final TestConfiguration testConfig = new TestConfiguration(); 20 | private PortCommunication comm; 21 | 22 | 23 | @Before 24 | public void initCommunication() throws PortCommunicationException { 25 | this.comm = new PortCommunication(testConfig.getPortConfiguration()); 26 | this.comm.reset(); 27 | this.comm.setEcho(false); 28 | this.comm.setLineTermination(false); 29 | } 30 | 31 | 32 | @After 33 | public void close() { 34 | this.comm.close(); 35 | } 36 | 37 | 38 | @Test 39 | public void testAt() throws PortCommunicationException { 40 | // FIXME: should be parametrized - other tester can have another hardware. 41 | assertEquals("ELM327 v1.5", comm.at("I")); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/cardiag/serial/SerialUtilsIT.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.serial; 5 | 6 | import static org.junit.Assert.*; 7 | 8 | import java.util.List; 9 | 10 | import org.junit.Test; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | 15 | /** 16 | * @author David Matějček 17 | */ 18 | public class SerialUtilsIT { 19 | private static final Logger LOG = LoggerFactory.getLogger(SerialUtilsIT.class); 20 | 21 | @Test 22 | public void testGetPortNames() { 23 | final List names = SerialUtils.getPortNames(); 24 | LOG.info("port names={}", names); 25 | assertNotNull(names); 26 | assertFalse("You must have at last one serial port. No serial port found.", names.isEmpty()); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/cardiag/test/TestConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.test; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Properties; 9 | 10 | import org.apache.commons.io.IOUtils; 11 | import org.junit.Assert; 12 | 13 | import cardiag.serial.PortConfiguration; 14 | 15 | 16 | /** 17 | * @author David Matějček 18 | */ 19 | public class TestConfiguration { 20 | 21 | private Properties properties; 22 | 23 | 24 | public TestConfiguration() { 25 | final InputStream stream = TestConfiguration.class.getClassLoader().getResourceAsStream("test.properties"); 26 | try { 27 | Properties properties = new Properties(); 28 | properties.load(stream); 29 | this.properties = properties; 30 | } catch (IOException e) { 31 | Assert.fail("Cannot load test.properties file"); 32 | } finally { 33 | IOUtils.closeQuietly(stream); 34 | } 35 | } 36 | 37 | 38 | public String getPortName() { 39 | return properties.getProperty("serialPort"); 40 | } 41 | 42 | 43 | public PortConfiguration getPortConfiguration() { 44 | final PortConfiguration cfg = new PortConfiguration(); 45 | cfg.setPortName(getPortName()); 46 | cfg.setCommandTimeout(10000L); 47 | return cfg; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/cardiag/user/ConsoleMock.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.user; 5 | 6 | import java.util.LinkedList; 7 | 8 | /** 9 | * @author David Matějček 10 | */ 11 | public class ConsoleMock implements ConsoleCommunication { 12 | 13 | private LinkedList lines = new LinkedList(); 14 | 15 | 16 | @Override 17 | public void format(String fmt, Object... args) { 18 | System.out.format(fmt, args); 19 | } 20 | 21 | 22 | @Override 23 | public void flush() { 24 | System.out.flush(); 25 | } 26 | 27 | 28 | public void appendLine(final String line) { 29 | this.lines.add(line); 30 | } 31 | 32 | 33 | @Override 34 | public String readLine() { 35 | final String line = this.lines.pollFirst(); 36 | System.out.println(line); 37 | return line; 38 | } 39 | 40 | 41 | @Override 42 | public String readLine(final String fmt, final Object... args) { 43 | format(fmt, args); 44 | return readLine(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/cardiag/user/UserCommunicationTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package cardiag.user; 5 | 6 | import static org.junit.Assert.*; 7 | import java.util.Arrays; 8 | 9 | import org.junit.Test; 10 | 11 | /** 12 | * @author David Matějček 13 | */ 14 | public class UserCommunicationTest { 15 | 16 | @Test 17 | public void testSelectPort() { 18 | final ConsoleMock console = new ConsoleMock(); 19 | final UserCommunication comm = new UserCommunication(console); 20 | final String[] ports = {"/dev/ttyUSB0", "/dev/ttyS10"}; 21 | console.appendLine("0"); 22 | String selectedPort = comm.selectPort(Arrays.asList(ports)); 23 | assertEquals(ports[0], selectedPort); 24 | console.appendLine("1"); 25 | selectedPort = comm.selectPort(Arrays.asList(ports)); 26 | assertEquals(ports[1], selectedPort); 27 | console.appendLine("X"); 28 | selectedPort = comm.selectPort(Arrays.asList(ports)); 29 | assertNull(selectedPort); 30 | console.appendLine("SOMETHING STUPID"); 31 | console.appendLine("X"); 32 | selectedPort = comm.selectPort(Arrays.asList(ports)); 33 | assertNull(selectedPort); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender 2 | log4j.appender.STDOUT.Target=System.out 3 | log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout 4 | log4j.appender.STDOUT.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%t] %25c{3}:%-4L: %m%n 5 | log4j.appender.STDOUT.Threshold=TRACE 6 | 7 | log4j.rootLogger=TRACE, STDOUT 8 | log4j.logger.cardiag=INFO 9 | #log4j.additivity.cardiag=false 10 | -------------------------------------------------------------------------------- /src/test/resources/test.properties: -------------------------------------------------------------------------------- 1 | serialPort=${cardiag.test.serialPort} 2 | --------------------------------------------------------------------------------