TA的每日心情 | 开心 2016-8-15 09:30 |
---|
签到天数: 162 天 连续签到: 1 天 [LV.7]常住居民III
|
楼主 |
发表于 2013-6-17 09:00:40
|
显示全部楼层
本帖最后由 haothree 于 2013-6-17 09:17 编辑
基于树莓派 JavaFx 车载系统(第二部分)
在我的 上一篇文章 (按计划,这篇文章早就应该写了) 中我描述了关于车载 Raspberry Pi JavaFX 系统的想法。现在开始做技术方面的事情吧。
首先,我们先简单回顾一下现代汽车电子方面的事。我的第一辆车是1971年的 Mini Clubman. 这车基本没有电子产品(收音机除外),所有的东西基本都是电子机械结合的 (参见 distributor),现在的情况当然有了很大改观。如今,对于汽车,类似防抱死系统(ABS),电子平衡系统(ESC)等等需要用到复杂感应器和电子元件的系统,都是法律要求安装的,至少在欧洲是这样。同时,从2001年起,所有使用汽油的汽车,都必须安装EOBD(欧洲汽车诊断系统)接口。这样就满足了OBD-II标准,我上篇文章里面提到的 ELM327 接口就有了用武之地。
作为一个标准,OBD-II 规定了一些必需的部分,其他的都是可选的。那个就可以确保基本的部件都会安装好(主要是衡量废气排放性能的部件),汽车制造商可以根据各自的需要来制造和安装可选部件。
OBD-II 接口包含了5种信号协议:
SAE J1850 PWM (脉冲宽度调制, Ford使用)
SAE J1850 VPW (可变脉冲宽度, General Motors使用)
ISO 9141-2 (有点像 RS-232)
ISO 14230
ISO 15765 (也称为 Controller Area Network, 或 CAN 总线)
你可以把这个看作传输层,汽车制造商可以根据自己需要进行更改。使用信号协议的消息协议在 OBD-II标准中定义,这些命令的格式都是直接接受一系列的十六进制数字对。第一对数字标明了 'mode' (这里是 10); 第二对(有可能还有第三对)数字是 'parameter identification' 也即要发送的PID。mode和PID组合在一起定义了要发给车辆的命令。结果是string格式的字节序列,包含了十六进制数字对的数据编码。
对于我的汽车 Audi S3,协议是 ISO 15765,汽车有多个 CAN 总线,用于不同的控制单元之间的通信(之后会对此详细介绍)。
接下来从哪开始呢?
首先,需要在Java程序和ELM327之间建立通信。对于开发这样的Java程序来说,很不错的地方是可以在笔记本上轻松的开发出程序,然后同样轻松的将代码迁移到目标硬件上。这个过程根本不需要跨平台编译的工具链。
我的ELM327接口通过802.11 (Wi-Fi)来通信。我的接口的地址是192.168.0.11 (对于这类设备这很常见),并且使用端口35000 来进行通信。为了验证地址有效,我在我的MacBook上设置了WiFi网络的静态IP地址,然后直接与在WiFi的设备列表中的ELM327连接。在IP层建立连接好连接后,我就可以通过telnet与ELM327连接。如果你要试一下,建议最好看一下 这份文档,写的很好也很全。ELM327 大致会用到两种通信模式:
AT命令,用来与接口自身通信
OBD命令,遵循上面的说明。ELM327为你做了所有麻烦的事情,会把这些转换成必要的数据包格式,添加数据头和数据校验,然后组装成返回的数据。
为了着手工作,我只是用了返回接口版本的AT I命令和返回当前汽车电瓶电压的AT RV命令。他们通过telnet运行良好,因此是时候开始 开发Java代码了。
为了始终保持简单,我编写了封装到ELM327连接的类。初始化连接的代码见下面,这样我们可以根据需要进行字节的读写了。- /*版权所有归Oracle和或它的分公司所有© 2013,保留所有权利。*/
- private static final String ELM327_IP_ADDRESS = "192.168.0.10";
- private static final int ELM327_IP_PORT = 35000;
- private static final byte OBD_RESPONSE = (byte)0x40;
- private static final String CR = "\n";
- private static final String LF = "\r";
- private static final String CR_LF = "\n\r";
- private static final String PROMPT = ">";
- private Socket elmSocket;
- private OutputStream elmOutput;
- private InputStream elmInput;
- private boolean debugOn = false;
- private int debugLevel = 5;
- private byte[] rawResponse = new byte[1024];
- protected byte[] responseData = new byte[1024];
- /**
- * 常见的初始化代码
- *
- * @如果通讯有问题就抛出异常
- */
- private void init() throws IOException {
- /* 建立一个到ELM327端口的套接字,然后创建到这个套接字
- * 的输入和输出流
- */
- try {
- elmSocket = new Socket(ELM327_IP_ADDRESS, ELM327_IP_PORT);
- elmOutput = elmSocket.getOutputStream();
- elmInput = elmSocket.getInputStream();
- } catch (UnknownHostException ex) {
- System.out.println("ELM327: Unknown host, [" + ELM327_IP_ADDRESS + "]");
- System.exit(1);
- } catch (IOException ex) {
- System.out.println("ELM327: IO error talking to car");
- System.out.println(ex.getMessage());
- System.exit(2);
- }
- /* 确保我们已经拥有了输入和输出流 */
- if (elmInput == null || elmOutput == null) {
- System.out.println("ELM327: input or output to device is null");
- System.exit(1);
- }
- /* 最后,发送重置命令道这个套接字,以关闭字符回显
- * (关闭字符回显起什么作用不是很清楚)
- */
- resetInterface();
- sendATCommand("E0");
- debug("ELM327: Connection established.", 1);
- }
复制代码 获得连接后,我们需要一些方法,它能提供用来发送命令和获得返回结果的简单接口。下面是发送信息的常见方法。- /**
- * Send an AT command to control the ELM327 interface
- *
- * @param command The command string to send
- * @return The response from the ELM327
- * @throws IOException If there is a communication error
- */
- protected String sendATCommand(String command) throws IOException {
- /* Construct the full command string to send. We must remember to
- * include a carriage return (ASCII 0x0D)
- */
- String atCommand = "AT " + command + CR_LF;
- debug("ELM327: Sending AT command [AT " + command + "]", 1);
-
- /* Send it to the interface */
- elmOutput.write(atCommand.getBytes());
- debug("ELM327: Command sent", 1);
- String response = getResponse();
-
- /* Delete the command, which may be echoed back */
- response = response.replace("AT " + command, "");
- return response;
- }
-
- /**
- * Send an OBD command to the car via the ELM327.
- *
- * @param command The command as a string of hexadecimal values
- * @return The number of bytes returned by the command
- * @throws IOException If there is a problem communicating
- */
- protected int sendOBDCommand(String command)
- throws IOException, ELM327Exception {
- byte[] commandBytes = byteStringToArray(command);
-
- /* A valid OBD command must be at least two bytes to indicate the mode
- * and then the information request
- */
- if (commandBytes.length < 2)
- throw new ELM327Exception("ELM327: OBD command must be at least 2 bytes");
-
- byte obdMode = commandBytes[0];
-
- /* Send the command to the ELM327 */
- debug("ELM327: sendOBDCommand: [" + command + "], mode = " + obdMode, 1);
- elmOutput.write((command + CR_LF).getBytes());
- debug("ELM327: Command sent", 1);
-
- /* Read the response */
- String response = getResponse();
-
- /* Remove the original command in case that gets echoed back */
- response = response.replace(command, "");
- debug("ELM327: OBD response = " + response, 1);
-
- /* If there is NO DATA, there is no data */
- if (response.compareTo("NO DATA") == 0)
- return 0;
-
- /* Trap error message from CAN bus */
- if (response.compareTo("CAN ERROR") == 0)
- throw new ELM327Exception("ELM327: CAN ERROR detected");
-
- rawResponse = byteStringToArray(response);
- int responseDataLength = rawResponse.length;
-
- /* The first byte indicates a response for the request mode and the
- * second byte is a repeat of the PID. We test these to ensure that
- * the response is of the correct format
- */
- if (responseDataLength < 2)
- throw new ELM327Exception("ELM327: Response was too short");
-
- if (rawResponse[0] != (byte)(obdMode + OBD_RESPONSE))
- throw new ELM327Exception("ELM327: Incorrect response [" +
- String.format("%02X", responseData[0]) + " != " +
- String.format("%02X", (byte)(obdMode + OBD_RESPONSE)) + "]");
-
- if (rawResponse[1] != commandBytes[1])
- throw new ELM327Exception("ELM327: Incorrect command response [" +
- String.format("%02X", responseData[1]) + " != " +
- String.format("%02X", commandBytes[1]));
-
- debug("ELM327: byte count = " + responseDataLength, 1);
-
- for (int i = 0; i < responseDataLength; i++)
- debug(String.format("ELM327: byte %d = %02X", i, rawResponse[i]), 1);
-
- responseData = Arrays.copyOfRange(rawResponse, 2, responseDataLength);
-
- return responseDataLength - 2;
- }
-
- /**
- * Send an OBD command to the car via the ELM327. Test the length of the
- * response to see if it matches an expected value
- *
- * @param command The command as a string of hexadecimal values
- * @param expectedLength The expected length of the response
- * @return The length of the response
- * @throws IOException If there is a communication error or wrong length
- */
- protected int sendOBDCommand(String command, int expectedLength)
- throws IOException, ELM327Exception {
- int responseLength = this.sendOBDCommand(command);
-
- if (responseLength != expectedLength)
- throw new IOException("ELM327: sendOBDCommand: bad reply length ["
- + responseLength + " != " + expectedLength + "]");
-
- return responseLength;
- }
-
- and the method for reading back the results.
-
- /**
- * Get the response to a command, having first cleaned it up so it only
- * contains the data we're interested in.
- *
- * @return The response data
- * @throws IOException If there is a communications problem
- */
- private String getResponse() throws IOException {
- boolean readComplete = false;
- StringBuilder responseBuilder = new StringBuilder();
-
- /* 读取响应。 Sometimes timing issues mean we only get part of
- * the message in the first read. To ensure we always get all the intended
- * data (and therefore do not get confused on the the next read) we keep
- * reading until we see a prompt character in the data. That way we know
- * we have definitely got all the response.
- */
- while (!readComplete) {
- int readLength = elmInput.read(rawResponse);
- debug("ELM327: Response received, length = " + readLength, 1);
-
- String data = new String(Arrays.copyOfRange(rawResponse, 0, readLength));
- responseBuilder.append(data);
-
- /* 检查提示符 */
- if (data.contains(PROMPT)) {
- debug("ELM327: Got a prompt", 1);
- break;
- }
- }
-
- /* 去掉换行符、回车符和提示符 */
- String response = responseBuilder.toString();
- response = response.replace(CR, "");
- response = response.replace(LF, "");
- response = response.replace(PROMPT, "");
- return response;
- }
复制代码 使用这些方法将使得开始使用OBD协议的实现方法非常简单。例如为了获得接口的版本信息,我们只需要下面简单的方法:- /**
- * 获得所连接的ELM327的版本号
- *
- * @返回版本号字符串
- * @如果通信有问题就抛出异常
- */
- public String getInterfaceVersionNumber() throws IOException {
- return sendATCommand("I");
- }
复制代码 另一个非常有用的方法是返回支持给定模式的所有PID的详细信息的方法:- /**
- * Determine which PIDs for OBDII are supported. The OBD standards docs are
- * required for a fuller explanation of these.
- *
- * @参数pid确定汇报的是哪个范围pid
- * @返回一个指明所支持的PID的数组。
- * @如果通信有错误就抛出异常
- */
- public boolean[] getPIDSupport(byte pid) throws IOException, ELM327Exception {
- int dataLength = sendOBDCommand("01 " + String.format("%02X", pid));
-
- /* 如果零个字节返回,那么我们假设请求的范围没有支持的PID
- */
- if (dataLength == 0)
- return null;
-
- int pidCount = dataLength * 8;
- debug("ELM327: pid count = " + pidCount, 1);
- boolean[] pidList = new boolean[pidCount];
- int p = 0;
-
- /*现在解码支持的PID所对应的位图 */
- for (int i = 2; i < dataLength; i++)
- for (int j = 0; j < 8; j++) {
- if ((responseData[i] & (1 << j)) != 0)
- pidList[p++] = true;
- else
- pidList[p++] = false;
- }
-
- return pidList;
- }
复制代码 对于mode为1,PID为0x00, 0x20, 0x40, 0x60, 0x80, 0xA0 和 0xC0,会返回接下来的31个数值是否支持的PID,返回值是4字节的bitmap(译者注:例如,0x20会返回0x21到0x3f这31个PID是否支持,结果放在bitmap的32个bits)。在规范中我只找到截止到0x87的命令。
下一个部分我们将开始用这个类去从汽车获取一些真实的数据。
转自:http://www.oschina.net/translate/the_raspberry_pi_javafx_in-car-system-part2
|
|