客户有一个称重的设备,要求要用串口数码管显示对应的重量值,最开始我以为需要在kernel中加驱动,后来看了一下客户提供过来的资料,上电之后通过发串口命令就能显示对应的数据,那接上串口后就不必走kernel驱动,直接在framework里加串口通信就ok了。
既然是在framework中加串口通信,那么给客户的接口就是一个广播,广播中传一个重量值,然后我们接收到广播之后获取广播中的重量值信息,再将重量值信息转化为对应的串口命令,再通过串口发送命令到数码管显示。
在开始写代码之前,我们首先要确认串口发送命令到数码管是能够显示对应的数据的,那么我们可以找一个串口apk去发送串口命令,测试串口发送命令到数码管是正常的。我用的是serialtool.apk,因为这个apk只支持ttyS1--ttyS4,所以我硬件上就接的dev/ttyS3。在测试之前我们要首先保证dev/ttyS3有读写权限,通过adb shell命令进去之后,用ls -al dev/ttyS3查看
如果没有读写权限可以用chmod 0666 dev/ttyS3给串口节点赋读写权限。
打开串口apk还需要关闭selinux权限,用命令setenforce 0关闭selinux,命令getenforce查看当前selinux状态。
打开串口apk,发送串口指令。apk界面如下图所示,我们需要打开Hex Send发送16进制数据,否则会导致数据包错误。
接下来我们看一下串口数码管需要发送的命令。
我们通过apk发送01 06 6A 00 00 01 55 D2和01 06 6A 00 00 02 15 D3,看一下数码管会不会显示对应的值。如果串口数码管没有显示对应的值,那么首先我们要看一下apk发送的命令是否和接收到的一致,可以用一个串口板接到电脑上,用串口工具看一下串口apk发送过来的命令是否一致;检查命令一致之后还是不显示对应的值再交换一下数码管接的rx和tx引脚,排查是否引脚接错了。如果都排查无误数码管还是没有显示,那么就需要打一下串口发送的波形看一下了,对比参照工具为sscom串口工具发送串口指令和apk发送的波形做对比,因为用sscom串口工具发送的命令是可以显示的,最后两位校验位选择modbus-crc16校验。
成功发送的效果如下图所示。
保证串口命令能正常发送显示之后,接下来就分析一下串口指令的构成。我们需要发送什么指令才能实现客户发送过来的数据能显示对应的值?因为是称重,所以我们需要保留两位小数,通过搜索指令我们发现01 10 74 0D 00 02 04 00 02 00 02 45 35(显示0.2)这个指令比较符合我们的要求,那么就以这个指令为标准吧。
01是设备码,10是功能码,74 0D是寄存器地址,00 02是寄存器数量,04是寄存器字节数(寄存器数量*2),00 02表示保留两位小数,00 02表示数据位,45 35表示crc16校验位。此为完整的串口指令构成,接下来我们要写的代码也是基于这个基础上完成的。
我们先实现modbus-crc16的代码,毕竟校验位是串口指令中最重要的,其他的都是基本固定的指令,只有最后两位校验位是需要我们通过前面所以指令计算所得。我先贴上代码
/**
* 获取CRC16校验码
*
* @param data 数据
* @return CRC16校验码
*/
public static String getCRC(String data) {
data = data.replace(" ", "");
int len = data.length();
if (!(len % 2 == 0)) {
return "0000";
}
int num = len / 2;
byte[] para = new byte[num];
for (int i = 0; i < num; i++) {
int value = Integer.valueOf(data.substring(i * 2, 2 * (i + 1)), 16);
para[i] = (byte) value;
}
return getCRC(para);
}
/**
* 计算CRC16校验码
*
* @param bytes 字节数组
* @return 校验码
*/
private static String getCRC(byte[] bytes) {
// CRC寄存器全为1
int CRC = 0x0000ffff;
// 多项式校验值
int POLYNOMIAL = 0x0000a001;
int i, j;
for (i = 0; i < bytes.length; i++) {
CRC ^= ((int) bytes[i] & 0x000000ff);
for (j = 0; j < 8; j++) {
if ((CRC & 0x00000001) != 0) {
CRC >>= 1;
CRC ^= POLYNOMIAL;
} else {
CRC >>= 1;
}
}
}
// 结果转换为16进制
String result = Integer.toHexString(CRC).toUpperCase();
if (result.length() != 4) {
StringBuilder sb = new StringBuilder("0000");
result = sb.replace(4 - result.length(), 4, result).toString();
}
// 交换高低位,低位在前高位在后
return result.substring(2, 4) + " " + result.substring(0, 2);
}
因为传入的数据是float的小数,所以还需要一个float转16进制的函数,代码如下
public static String Hex_conversion(float data){
int float_to_int = (int)(data * 100);
String Hex_high = Integer.toHexString(float_to_int >> 8);
String Hex_low = Integer.toHexString(float_to_int & 0xff);
if((float_to_int & 0xff) < 16)
Hex_low = "0" + Hex_low;
else if((float_to_int >> 8) < 16)
Hex_high = "0" + Hex_high;
Log.d(TAG,"cgl Hex_high = "+Hex_high+" Hex_low = "+Hex_low);
return (Hex_high+" "+Hex_low +" ");
}
串口函数发送的数据格式为byte[],所以串口命令转换完成后还需要将命令转换成对应的byte数组,代码如下。
public static byte[] stringToBytes(String data){
data = data.replace(" ", "");
int len = data.length();
int num = len / 2;
byte[] para = new byte[num];
for (int i = 0; i < num; i++) {
int value = Integer.valueOf(data.substring(i * 2, 2 * (i + 1)), 16);
para[i] = (byte) value;
Log.d(TAG,"stringToBytes para = "+para[i]+" i = "+i);
}
return para;
}
最终传输的数据,通过一个函数封装在一起,代码如下
public byte[] transmitData(float data){
String crcStr = "01 10 74 0D 00 02 04 00 02 ";
String crc_result = getCRC(crcStr + Hex_conversion(data));
String final_data = crcStr + Hex_conversion(data) + crc_result;
byte[] byte_data = stringToBytes(final_data);
Log.d(TAG,"crc_result = "+crc_result+" final_data = "+final_data+" byte_data = "+byte_data);
return byte_data;
}
接下来我们来看一下如何打开串口并发送串口指令,代码如下
framework/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+import android.hardware.SerialPort;
+import android.hardware.SerialManager;
+import java.nio.ByteBuffer;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.io.File;
+ private static final String TTYS3_PATH = "dev/ttyS3";
+ private static final int BAUD_RATE = 9600;
+ private SerialManager mSerialManager;
+ private ByteBuffer mInputBuffer;
+ private ByteBuffer mOutputBuffer;
+ public SerialPort mSerialPort;
+ public InputStream mInputStream;
+ public OutputStream mOutputStream;
//初始化
+ mSerialManager =(SerialManager)mContext.getSystemService(Context.SERIAL_SERVICE);
+
+ try {
+ mSerialPort = mSerialManager.openSerialPort(TTYS3_PATH, 9600);
+ } catch (IOException e) {
+ }
+ mInputBuffer = ByteBuffer.allocate(1024);
+ mOutputBuffer = ByteBuffer.allocate(1024);
//新建发送串口指令函数
+ public void sendSerialPort(byte[] data){
+ Log.d(TAG,"sendSerialPort data = "+data);
+ try {
+ mOutputBuffer.clear();
+ mOutputBuffer.put(data);
+ mSerialPort.write(mOutputBuffer, data.length);
+ } catch (IOException e) {
+ e.printStackTrace();
+ Log.d(TAG, "COM1 OPEN FAILED!");
+ }
+ }
需要注意的是,要在AndroidManifest.xml中添加Serial权限
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -271,6 +271,9 @@
<!-- Permission to make accessibility service access Bubbles -->
<uses-permission android:name="android.permission.ADD_TRUSTED_DISPLAY" />
+ <!-- Permission to serial port -->
+ <uses-permission android:name="android.permission.SERIAL_PORT" />
+
还需要在config.cml中添加对应的串口字符数组,否则会报串口为空的错误,具体代码为
framework/base/core/java/android/hardware/SerialManager.java
/**
* Opens and returns the {@link android.hardware.SerialPort} with the given name.
* The speed of the serial port must be one of:
* 50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, 9600,
* 19200, 38400, 57600, 115200, 230400, 460800, 500000, 576000, 921600, 1000000, 1152000,
* 1500000, 2000000, 2500000, 3000000, 3500000 or 4000000
*
* @param name of the serial port
* @param speed at which to open the serial port
* @return the serial port
*/
@UnsupportedAppUsage
public SerialPort openSerialPort(String name, int speed) throws IOException {
try {
ParcelFileDescriptor pfd = mService.openSerialPort(name);
if (pfd != null) {
SerialPort port = new SerialPort(name);
port.open(pfd, speed);
return port;
} else {
throw new IOException("Could not open serial port " + name);
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
openSerialPort调用mService的openSerialPort方法
framework/base/services/core/java/com/android/server/SerialService.java
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions an
* limitations under the License.
*/
package com.android.server;
import android.content.Context;
import android.hardware.ISerialManager;
import android.os.ParcelFileDescriptor;
import java.io.File;
import java.util.ArrayList;
public class SerialService extends ISerialManager.Stub {
private final Context mContext;
private final String[] mSerialPorts;
public SerialService(Context context) {
mContext = context;
mSerialPorts = context.getResources().getStringArray(
com.android.internal.R.array.config_serialPorts);
}
public String[] getSerialPorts() {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.SERIAL_PORT, null);
ArrayList<String> ports = new ArrayList<String>();
for (int i = 0; i < mSerialPorts.length; i++) {
String path = mSerialPorts[i];
if (new File(path).exists()) {
ports.add(path);
}
}
String[] result = new String[ports.size()];
ports.toArray(result);
return result;
}
public ParcelFileDescriptor openSerialPort(String path) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.SERIAL_PORT, null);
for (int i = 0; i < mSerialPorts.length; i++) {
if (mSerialPorts[i].equals(path)) {
return native_open(path);
}
}
throw new IllegalArgumentException("Invalid serial port " + path);
}
private native ParcelFileDescriptor native_open(String path);
}
在SerialService.java中,openSerialPort通过mSerialPorts[i].equals(path)来比对来打开串口,而mSerialPorts是通过获取config_serialPorts这个String数组,所以我们应该在config_serialPorts中添加dev/ttyS3。
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -990,6 +990,7 @@
for example, /dev/ttyUSB0
-->
<string-array translatable="false" name="config_serialPorts">
+ <item>dev/ttyS3</item>
</string-array>
串口发送函数添加完之后,就需要添加一个广播接口给客户发送数据。广播添加流程如下:
+ private static final String LED_LIGHT_ACTION = "com.android.led_light_action";
@VisibleForTesting
protected void registerBroadcastReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG);
+ filter.addAction(LED_LIGHT_ACTION);
mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, null, UserHandle.ALL);
}
else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
if (mNotificationShadeWindowController != null) {
mNotificationShadeWindowController.setNotTouchable(false);
}
if (mBubbleController.isStackExpanded()) {
mBubbleController.collapseStack();
}
finishBarAnimations();
resetUserExpandedStates();
}
+ else if (LED_LIGHT_ACTION.equals(action)) {
+ Log.d(TAG,"LED_LIGHT_ACTION is send");
+ sendSerialPort(transmitData(intent.getFloatExtra("weight",0.0f)));
+ }
else if (DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG.equals(action)) {
mQSPanel.showDeviceMonitoringDialog();
}
注册完广播之后,可以通过am broadcast命令发送验证,验证结果如下: