文章大纲
引言
最近刚刚实现了一个与串口通信的小模块,主要的需求背景是,我们嵌入式工程师开发了一块圆形的板子用于控制LED灯,考虑到后期的调试和开发便捷性,便把控制LED灯的各种灯光效果,交给了app端去实现,于是自然落在我的肩上,经过一番努力折腾,终于成功实现了需求,包括一整套的监听和同步框架,特记录下来好好总结一番。此系列相关文章链接:
- Android NDK——实战演练之使用Android Studio引用so库,jar包、module,aar以及导入Eclipse项目并使用JNI的正确姿势(一)
- Android NDK——实战演练之配置NDK及使用Android studio开发Hello JNI并简单打包so库(二)
- Android NDK——实战演练之App端通过串口通信完成实时控制单片机上LED灯的颜色及灯光动画特效(三)
- Android NDK——实战演练之TextureView的应用之调用外接USB摄像头自动对焦并完成隐蔽拍照(四)
- Android NDK—— 实战演练之物联网、车联网必知必会自己动手实现串口通信控制智能家居(五)
一、串口与相关术语介
串口全称串行接口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。常见数据通信方式:并行通信,串行通信,串口参数的配置主要包括:波特率、数据位、停止位、流控协议。计算机中串行通信端口的关键部分是UART(通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),具体实物表现为独立的模块化芯片,或作为集成于微处理器中的周边设备)
其中UART的主要操作:
- 数据发送及接受
- 产生中断
- 产生波特率
- Loopback模式
- 自动流控模式
- 通信协议编辑
UART作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。
其中各位的意义如下:
起始位:先发出一个逻辑”0”的信号,表示传输字符的开始。
资料位:紧接着起始位之后。资料位的个数可以是4、5、6、7、8等,构成一个字符。通常采用ASCII码。从最低位开始传送,靠时钟定位。
奇偶校验位:资料位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验资料传送的正确性。
停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。[2]
空闲位:处于逻辑“1”状态,表示当前线路上没有资料传送。
波特率:是衡量资料传送速率的指标。表示每秒钟传送的符号数(symbol)。一个符号代表的信息量(比特数)与符号的阶数有关。例如资料传送速率为120字符/秒,传输使用256阶符号,每个符号代表8bit,则波特率就是120baud,比特率是120*8=960bit/s。(以上摘自百度百科,在我的实际项目开发中这些协议都是单片机同事去完成的,他封装了个通信协议,我只需要做的就是按照一定的格式发送16进制的数据帧)。
上面介绍了一些串口的基本术语,其实对于我们app端开发来说也并不是一定要了解的(只不过我不喜欢不知其所以然,一点都不懂原理,所以开发的时候去现学了下,不至于一脸懵X),我们app端需要掌握的三点:
- 在linux下所有的硬件都被会被映射到相关目录下,串口通信并不是直接和硬件通信的,而串口设备文件放于/dev/目录下,形如串口一,串口二分别为"/dev/ttyS0","/dev/ttyS1".
- 在linux下操作串口,实质就是操作文件IO流.
- 串口详细配置包括:波特率、数据位、校验位、停止位等,其中波特率对于我们app操作串口尤其重要,这个得硬件工程师调试或者产品说明,否则会造成我们无法操作串口。串口设置由下面的结构体实现:
//c_cflag最为重要,可设置波特率、数据位、校验位、停止位。在设置波特率时需要在数字前加上'B',
struct termios
{
tcflag_t c_iflag; //input flags
tcflag_t c_oflag; //output flags
tcflag_t c_cflag; //control flags
tcflag_t c_lflag; //local flags
cc_t c_cc[NCCS]; //control characters
};
二、串口通信步骤
如前面所言,串口通信起实质就是IO操作,所以逻辑也和我们平常的IO没多大区别,不过由于是和底层硬件通信,java 并没有封装相应的api直接去操作串口,也不应该用java去实现,c语言应该是最佳选项,不管用任何语言逻辑都是不变的。
- 设置正确的串口号和波特率获取串口对象
- 由于串口通信都是IO操作,而且有可能是无限循环发送一些指令,所以最好开辟一个单独的子线程去做发送操作
- 打开对应的串口对象并启动发送线程
三、实现串口通信
这个实例主要是参考Google code上的android-serialport-api,在他的基础上进行封装的,主要是利用他来打开或关闭串口。
1、引入so文件和建立对应的Java本地接口类
这基本是把他项目里的类直接copy过来,使用时候注意下包名,就不会有什么错,如果在引入so这一步骤还出错的话可以参考下 Android NDK——使用Android Studio引用so库,jar包及module并使用JNI的正确姿势
2、 Process
在Google官网上是是这样介绍Process的功能(Tools for managing OS processes)在android中Process进程默认情况下,同一个应用程序中的所有组件运行在同一个进程中,而且绝大多数的应用程序也都是这样的。但是,如果我们想要控制让某个特定的组件属于某个进程,我们可以在manifest文件中进行配置。在每种组件元素(activity、service、receiver、provider)的manifest条目中,都支持一个“android:process”的属性,通过这个属性,我们可以指定某个组件运行的进程。我们可以通过设置这个属性,让每个组件运行在它自己的进程中,也可以只让某些组件共享一个进程。我们要可以通过设置“android:process”属性,让不同应用程序中的组件运行在相同的进程中,这些应用程序共享相同的Linux用户ID,拥有相同的证书。
3、 Android上使用su权限执行命令
//设备不同不一定成功
ProcessBuilder pb = new ProcessBuilder("su");//othre pb setting,such as dir,
envProcess p = pb.start();
OutputStream out;
out = p.getOutputStream();
out.write(cmd.getBytes());
/*
* Copyright 2009 Cedric Priscal
*
* 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 and
* limitations under the License.
*/
package android_serialport_api;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class SerialPort {
private static final String TAG = "SerialPort";
/*
* Do not remove or rename the field mFd: it is used by native method close();
*/
private FileDescriptor mFd;
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;
public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {
/* Check access permission */
if (!device.canRead() || !device.canWrite()) {
try {
/* Missing read/write permission, trying to chmod the file */
Process su;
su = Runtime.getRuntime().exec("/system/bin/su");//有些手机上无法正常运行有可能是路径错误
String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"+ "exit\n";//设置权限
su.getOutputStream().write(cmd.getBytes());
if ((su.waitFor() != 0) || !device.canRead()
|| !device.canWrite()) {
throw new SecurityException();
}
} catch (Exception e) {
e.printStackTrace();
throw new SecurityException();
}
}
mFd = open(device.getAbsolutePath(), baudrate, flags);
if (mFd == null) {
Log.e(TAG, "native open returns null");
throw new IOException();
}
mFileInputStream = new FileInputStream(mFd);
mFileOutputStream = new FileOutputStream(mFd);
}
// Getters and setters
public InputStream getInputStream() {
return mFileInputStream;
}
public OutputStream getOutputStream() {
return mFileOutputStream;
}
// JNI
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
static {
try {
System.loadLibrary("serial_port");
}catch (Exception e){
Log.e(TAG, "static initializer: "+e.toString() );
}
}
}
4、封装自己的基本的串口操作类
因为我在整个app运行的过程中,这个串口操作类是存在全生命周期的,所以没必要重复创建它的对象实例,只需要在application里初始化一次就好,所以有必要写成**单例模式(推荐写法)**的。
package com.xiaoi.app.zkhttpservice.util;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidParameterException;
import android_serialport_api.SerialPort;
/**
* @auther: Crazy.Mo
* Date: 2016/11/9
* Time:10:04
* Des:串口控制类:包含打开、关闭串口,发送指令到串口,开启发送指令线程
*/
public class SerialportControler {
private final String TAG="CrazyMo";
private SerialPort mSerialPort;
private OutputStream mOutputStream;
private InputStream mInputStream;
private SerialportControler.SendThread mSendThread;
/* private SerialportControler.ReadThread mReadThread;
private SerialportControler.AnalyzeThread analyzeThread;*/
private String mPort = "dev/ttySAC3";//默认的你的端口号
private int baudRate = 115200;//默认的波特率
private boolean isOpen = false;
private byte[] loopData = new byte[]{0x30};//用于保存要发送到串口的数据
private int iDelay = 500;//避免长久占用cpu sleep 500ms
private SerialportControler() {
}
private SerialportControler(String port, int baudRate){
this.mPort = port;
this.baudRate = baudRate;
}
public static SerialportControler getInstance() {
return SerialportControlerHolder.serialportControler;
}
private static class SerialportControlerHolder {
private static final SerialportControler serialportControler = new SerialportControler();
}
public boolean setBaudRate(int iBaud) {
if (isOpen) {
return false;
} else {
baudRate = iBaud;
return true;
}
}
public boolean setBaudRate(String sBaud) {
int iBaud = Integer.parseInt(sBaud);
return setBaudRate(iBaud);
}
public String getPort() {
return mPort;
}
public boolean setPort(String port) {
if (isOpen) {
return false;
} else {
this.mPort = port;
return true;
}
}
public boolean isOpen() {
return isOpen;
}
public byte[] getbLoopData() {
return loopData;
}
public void setbLoopData(byte[] bLoopData) {
this.loopData = bLoopData;
}
public void setTxtLoopData(String sTxt) {
this.loopData = sTxt.getBytes();
}
public void setHexLoopData(String sHex) {
this.loopData = SerialCmdUtils.hexToByteArr(sHex);
}
public int getiDelay() {
return iDelay;
}
public void setiDelay(int iDelay) {
this.iDelay = iDelay;
}
/**
* 打开指定的SerialPort,并开启发送线程
* @throws SecurityException
* @throws IOException
* @throws InvalidParameterException
*/
public void open() throws SecurityException, IOException, InvalidParameterException {
mSerialPort = new SerialPort(new File(mPort), baudRate, 0);
mOutputStream = mSerialPort.getOutputStream();
mInputStream = mSerialPort.getInputStream();
//mReadThread = new ReadThread();
//mReadThread.start();
mSendThread = new SerialportControler.SendThread();
mSendThread.setSuspendFlag();
mSendThread.start();
isOpen = true;
Log.e(TAG, "open: 运行的线程名:"+Thread.currentThread().getName() );
}
/**
* 关闭串口
*/
public void close() {
if (mSerialPort != null) {
mSerialPort.close();
mSerialPort = null;
}
isOpen = false;
}
/**
* 发送数据到串口
* @param bOutArray 字节数组
*/
public void send(byte[] bOutArray) {
try {
Log.e("CrazyMo", "send: 把字节写到串口"+"运行的所有线程:"+Thread.currentThread().getName()+SerialCmdUtils.byteArrToHex(bOutArray)+bOutArray.length );
mOutputStream.write(bOutArray);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发送16进制的字符串,形如E2E2C40107FF这样的是被解析成为16进制的整数
* @param sHex
*/
public void sendHex(String sHex) {
byte[] bOutArray = SerialCmdUtils.hexToByteArr(sHex);
send(bOutArray);
}
/**
* 发送字符串,发送E2E2就会当成是字符串"E2E2"
* @param sTxt
*/
public void sendTxt(String sTxt) {
byte[] bOutArray = sTxt.getBytes();
send(bOutArray);
}
/**
* 恢复发送线程
*/
public void startSend() {
if (mSendThread != null) {
mSendThread.setResume();
}
}
/**
* 为了避免浪费cpu资源,再没有必要使用的时候暂停发送线程
*/
public void stopSend() {
if (mSendThread != null) {
mSendThread.setSuspendFlag();
}
}
/**
* 发送指令到串口,可以通过调整
* @param sOut
*/
public synchronized void sendPortData(String sOut) {
//serialHelper.sendTxt(sOut);//以字符串形式发送至串口
try {
Thread.sleep(ConstantUtil.SEND_CMD_THREAD_SLEEP_TIME);//为了避免同时发送太多指令导致丢帧,设置两条指令每间隔10ms才发送
setHexLoopData(sOut);
sendHex(sOut);
Log.e("CrazyMO", "sendPortData: " + "sleep Thread name::" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
public void openComport() {
try {
open();
} catch (SecurityException e) {
Log.e("CrazyMo", "openComport: " + "打开串口失败:没有串口读/写权限!");
} catch (IOException e) {
Log.e("CrazyMo", "openComport: " + "打开串口失败:IO异常!");
} catch (InvalidParameterException e) {
Log.e("CrazyMo", "openComport: " + "打开串口失败:非法参数:波特率或者端口号错误!");
}
}
/**
* 发送指令的线程
*/
private class SendThread extends Thread {
public boolean suspendFlag = true;
@Override
public void run() {
super.run();
while (!Thread.currentThread().isInterrupted()) {
synchronized (this) {
while (suspendFlag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
Thread.sleep(iDelay);
Log.e("CrazyMo", "SendThread is running");
send(getbLoopData());//先得到封装到SerialHelper的数据再执行发送到串口的操作
}catch (Error e){
Log.e("CrazyMo", "run: Erro "+e.toString() );
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
设置线程暂停标志
public void setSuspendFlag() {
this.suspendFlag = true;
}
唤醒线程
public synchronized void setResume() {
this.suspendFlag = false;
notify();
}
}
/**
* 读取串口发送数据的线程
*/
/*private class ReadThread extends Thread {
public boolean suspendFlag = true;
public boolean isSuspendFlag() {
return suspendFlag;
}
public void setSuspendFlag(boolean suspendFlag) {
this.suspendFlag = suspendFlag;
}
@Override
public void run() {
while (!isInterrupted() && this.suspendFlag == true) {
try {
if (mInputStream == null) return;
Log.e("CrazyMo", "ReadThread is running");
byte[] buffer = new byte[64];
int size = mInputStream.read(buffer);
if (size > 0) {
StringBuilder receive = new StringBuilder();
receive.append(byteArrToHex(buffer));
if(ConstantUtil.SERIAL_HOLD_SUCCESS_CMD.equals(receive.toString())){
mReadThread.setSuspendFlag(false);
}else{
mReadThread.setSuspendFlag(true);
//HttpApplication.getSerialControler().sendPortData(ConstantUtil.SERIAL_HOLD_CMD);
}
}
} catch (Throwable e) {
e.printStackTrace();
return;
}
}
try {
sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
protected void onDataReceived(byte[] receiveData) {
StringBuilder receive = new StringBuilder();
receive.append(byteArrToHex(receiveData));
analyzeThread.AddQueue(receive.toString());
}
}
/**
*解析从串口发回的数据帧,,要记住的是,串口并不同于PC通信,即使单片机串口发送的是一串指令——E2E2C40106FFFBFS,你在app端或者pc端也不一定一次性接到完整的字符串,有可能第一次接到的只有E2E2,也有可能接到的是E2E2C4,所以对于接收串口来的数据我们得一直去访问串口,直到接收到完整的数据,再去解析
*/
/* private class AnalyzeThread extends Thread {
private Queue<String> queueList = new LinkedList<String>();
private StringBuilder comdatas=new StringBuilder();//接收到串口返回的数据,由于串口传输问题有可能不是一次性返回完整的数据。得一直读取然后添加到SB里
@Override
public void run() {
super.run();
while (!isInterrupted()) {
String comData;
while ((comData = queueList.poll()) != null) {
isHoldSucess(comData);
}
try {
Thread.sleep(10);//性能高的话,可以把此数值调小。
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
public synchronized void AddQueue(String recevie) {
queueList.add(recevie);
}
protected void isHoldSucess(String comData){
comdatas.append(comData);
if(ConstantUtil.SERIAL_HOLD_SUCCESS_CMD.equals(comdatas.toString())){
mReadThread.setSuspendFlag(false);
}
}
}*/
}
5、封装构造串口指令工具类
由于LED控制模块并不是单独存在的,需要和其他模块对接,并且是在全生命周期都得快速调用,而且根据我们的通信协议,构造指令是有一定算法的,故此处我是根据我的需求来封装的,而且也是方便新人调用我的框架去实现led控制,分享出来只是提供一个面向对象的启发,同时里面也有一些算法可以分享下,注意注释。
package com.xiaoi.app.zkhttpservice.util;
import android.support.annotation.NonNull;
import com.xiaoi.app.zkhttpservice.entity.bean.SerialCmdBean;
/**
* @auther: Crazy.Mo
* Date: 2016/11/9
* Time:10:30
* Des:构造串口控制指令
*/
public class SerialCmdUtils {
private static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6','7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
/**
* 构造控制Led的指令
* @param func 暂时为固定值E2E2C4
* @param lednum 0x01~0x10 调用时只需传入01~10即可
* @param functype
* 简化为只传入00 或者 07 00表示开灯 07表示关灯
* @param rpwm RGB模式下Red的值,0x00~0xFF,也是只需要传入00~FF即可
* @param gpwm
* @param bpwm
* 注意实际上传入的顺序是BRG
* @return 返回指令
*/
public static String buildInstruction(@NonNull String func, @NonNull String lednum,
@NonNull String functype, @NonNull String bpwm, @NonNull String rpwm, @NonNull String gpwm) {
// 计算校验和,功能帧标识位C4固定
String check = toHexString((byte) (hexToInt("C4") + hexToInt(lednum)
+ hexToInt(functype) + hexToInt(rpwm) + hexToInt(gpwm) + hexToInt(bpwm)));// 校验和
StringBuilder instructions = new StringBuilder();
instructions.append(func);
instructions.append(lednum);
instructions.append(functype);
instructions.append(bpwm);
instructions.append(rpwm);
instructions.append(gpwm);
instructions.append(check.toUpperCase());
return instructions.toString();
}
/**
* 构造控制Led的指令
* @param cmdBean
* 注意实际上传入的顺序是BRG
* @return 返回指令
*/
public static String buildInstruction(@NonNull SerialCmdBean cmdBean) {
// 计算校验和,功能帧标识位C4固定
String check = toHexString((byte) (hexToInt("C4") + hexToInt(cmdBean.getWitchled())
+ hexToInt(cmdBean.getFuntype()) + hexToInt(cmdBean.getBpwm()) + hexToInt(cmdBean.getRpwm()) + hexToInt(cmdBean.getGpwm())));// 校验和
StringBuilder instructions = new StringBuilder();
instructions.append(cmdBean.getFunc());
instructions.append(cmdBean.getWitchled());
instructions.append(cmdBean.getFuntype());
instructions.append(cmdBean.getBpwm());
instructions.append(cmdBean.getRpwm());
instructions.append(cmdBean.getGpwm());
instructions.append(check.toUpperCase());
return instructions.toString();
}
/**
* 返回更新的指令,即执行该指令后前面所有的指令才真正被执行,开关灯或者显示颜色变化操作才会显示在Led灯上
* @return
*/
public static String buildUpdateInstruction() {
return ConstantUtil.SERIAL_UPDATE_CMD;
}
/**
* 返回握手指令,每一次启动在application之后执行发送握手指令,发送握手指令成功之后才能进行其他的控制指令操作,这里是模仿“三次”握手的机制
*/
public static String buildHoldInstruction() {
return ConstantUtil.SERIAL_HOLD_CMD;
}
/**
* 关闭Led的指令
* @param witchled 指示要关闭的led
* @return
*/
public static String buildCloseInstruction(String witchled){
// 计算校验和,功能帧标识位C4固定
String check = toHexString((byte) (hexToInt("C4") + hexToInt(witchled)
+ hexToInt("00") + hexToInt("00") + hexToInt("00") + hexToInt("00")));// 校验和
StringBuilder instructions=new StringBuilder();
instructions.append("E2E2C4");
instructions.append(witchled);
instructions.append("00");
instructions.append("00");
instructions.append("00");
instructions.append("00");
instructions.append(check.toUpperCase());
return instructions.toString();
}
/**
* byte类型转为String
* @param b
* @return
*/
public static String toHexString(byte b) {
int u = toUnsigned(b);
return new String(new char[] { DIGITS[u >>> 4], DIGITS[u & 0xf] });
}
/**
* byte tt=0x2d;toHexString(tt).toUpperCase()///2D
* @param bytes
* @return
*/
public static String toHexString(byte... bytes) {
char[] buffer = new char[bytes.length * 2];
for (int i = 0, j = 0; i < bytes.length; ++i) {
int u = toUnsigned(bytes[i]);
buffer[j++] = DIGITS[u >>> 4];
buffer[j++] = DIGITS[u & 0xf];
}
return new String(buffer);
}
private static int toUnsigned(byte b) {
return b < 0 ? b + 256 : b;
}
/**
* 十六进制转整数
* String s="2D";String st="3F";
* toHexString((byte) (HexToInt(s)+HexToInt(st))));//6c
* @param inHex
* @return
*/
public static int hexToInt(String inHex){
return Integer.parseInt(inHex, 16);
}
/**
* 判断奇数或偶数,位运算,最后一位是1则为奇数,为0是偶数
*/
public static int isOdd(int num) {
return num & 0x1;
}
public static byte hexToByte(String inHex) {
return (byte) Integer.parseInt(inHex, 16);
}
public static byte[] hexToByteArr(String inHex) {
int hexlen = inHex.length();
byte[] result;
if (isOdd(hexlen) == 1) {//奇数
hexlen++;
result = new byte[(hexlen / 2)];
inHex = "0" + inHex;
} else {
result = new byte[(hexlen / 2)];
}
int j = 0;
for (int i = 0; i < hexlen; i += 2) {
result[j] = hexToByte(inHex.substring(i, i + 2));
j++;
}
return result;
}
/**
* 字节数组转转hex字符串
* @param inBytArr
* @return
*/
static public String byteArrToHex(byte[] inBytArr){
StringBuilder strBuilder=new StringBuilder();
int j=inBytArr.length;
for (int i = 0; i < j; i++)
{
strBuilder.append(byte2Hex(inBytArr[i]));
strBuilder.append(" ");
}
return strBuilder.toString();
}
/**
* 1字节转2个Hex字符
* @param inByte
* @return
*/
static public String byte2Hex(Byte inByte){
return String.format("%02x", inByte).toUpperCase();
}
}
6、构造发送更新串口指令的工具类
这里使用Timer或者Handler来实现延时效果并不好,后来我采用了另一种方式,这里把Timer和Handler写出来只是为了一个提醒,其实这代码也不是最终的版本,有很多地方可以优化的。
package com.xiaoi.app.zkhttpservice.util;
import android.os.Handler;
import android.os.Looper;
import com.xiaoi.app.zkhttpservice.HttpApplication;
import com.xiaoi.app.zkhttpservice.entity.bean.SerialCmdBean;
import java.util.Timer;
import java.util.TimerTask;
/**
* @auther: Crazy.Mo
* Date: 2016/11/9
* Time:14:52
* Des:用于控制更新指令执行的时间间隔来实现正常速度、慢速、中速和快速
*/
public class SerialControlCmdRun {
public static void sendUpdateNow(){
try {
Thread.sleep(10);
HttpApplication.getSerialControler().sendPortData(SerialCmdUtils.buildUpdateInstruction());
}catch (Exception e){
}
}
public static void sendUpdateAfterMils(long mils){
if(mils>0){
try {
Thread.sleep(mils);
HttpApplication.getSerialControler().sendPortData(SerialCmdUtils.buildUpdateInstruction());
}catch (Exception e){
}
}else{
sendUpdateNow();
}
}
/**
* 延时发送更新指令
* @param time
*/
public static void sendUpdateDelay(long time){
if(time>0) {
Timer timer = new Timer();
timer.schedule(new CmdRunTimer(), time);
}
}
/**
* 延时发送普通指令(同时处理16个灯的时候有bug,1、2号灯会不同步)
* @param time
* @param bean
*/
public static void sendControlDelay(SerialCmdBean bean,long time){
if(time>0) {
Timer timer = new Timer();
timer.schedule(new CmdSendTimer(bean), time);
}
}
/**
* 延时刷新指令
*/
public static class CmdRunTimer extends TimerTask {
@Override
public void run() {
HttpApplication.getSerialControler().sendPortData(SerialCmdUtils.buildUpdateInstruction());
}
}
/**
*延时发送普通指令
*/
public static class CmdSendTimer extends TimerTask{
private SerialCmdBean cmdBean;
public CmdSendTimer(SerialCmdBean bean){
this.cmdBean=bean;
}
@Override
public void run() {
HttpApplication.getSerialControler().sendPortData(SerialCmdUtils.buildInstruction(cmdBean));
}
}
public static void sendUpdateDelayByHandler(long time){
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
HttpApplication.getSerialControler().sendPortData(SerialCmdUtils.buildUpdateInstruction());
}
},time);
}
}
7、构造各种LED模式对应的指令工具类
这个类的作用就是直接构造各种LED灯的酷炫效果,其他模块需要显示LED灯的时候可以直接调用就可以显示了,但是考虑到代码的效率和简洁性以及多线程之间的切换,我后面构造了一个小的框架来实现,总之这个这个类的作用就是发送指令到串口并显示对应的LED灯效果,里面有一些小算法的实现,你就会感觉到数据结构的重要性了,细细体会吧!
package com.xiaoi.app.zkhttpservice.util;
import android.support.annotation.NonNull;
import android.util.Log;
import com.xiaoi.app.zkhttpservice.HttpApplication;
import com.xiaoi.app.zkhttpservice.entity.bean.SerialCmdBean;
import com.xiaoi.app.zkhttpservice.ledcontrol.LedControlService;
import com.xiaoi.app.zkhttpservice.ledcontrol.MessageConstants;
import java.util.LinkedList;
import static com.xiaoi.app.zkhttpservice.util.SerialCmdUtils.isOdd;
/**
* @auther: Crazy.Mo
* Date: 2016/11/9
* Time:15:38
* Des:用于定义各种场景模式下Led的显示效果,这个类是直接给业务类调用的
*/
public class SerialControlLedMode {
//赤橙黄绿青蓝紫淡红的RGB值
private static final String[] startUpColor = {"FF0000", "FF0000", "F65419", "F65419", "EEF60B", "EEF60B", "3C981B", "3C981B", "3CE2F3", "3CE2F3", "0511FB", "0511FB", "AB56EE", "AB56EE", "DA5E4D", "DA5E4D"};
//wifi连接时
private static final String[] wifiConnectingColor = {"0511FB", "0511FB", "0511FB", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF", "FFFFFF"};
/**
* 当中控系统启动时候
*/
public static void startUp() {
int colorLen = startUpColor.length;
SerialCmdBean cmdBean;
String cmd;
for (int i = 1; i < colorLen + 1; i++) {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(i), "07", startUpColor[i - 1]);
cmd = SerialCmdUtils.buildInstruction(cmdBean);
Log.e("CrazyMO", "startUp: 第" + i + "条:::::" + cmd);
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
HttpApplication.getSerialControler().sendPortData(cmd);
SerialControlCmdRun.sendUpdateDelay(100);
}
/*SerialControlCmdRun.sendUpdateDelay(10);
for(int k=0;k<10;k++) {
for (int i = 0; i < colorLen; i++) {
if (i == 0) {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(i), "00", startUpColor[i]);
} else {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(i), "00", startUpColor[i - 1]);
}
cmd = SerialCmdUtils.buildInstruction(cmdBean);
Log.e("CrazyMO", "startUp: 第"+k+"."+i+"条:::::"+cmd );
HttpApplication.getSerialControler().sendPortData(cmd);
//SerialControlCmdRun.sendUpdateDelay(100);
}
}*/
}
/**
* 同时控制16盏LED关闭或者打开
* @param color
* @param functype open 表示打开,其他表示关闭
*/
public static void controlAllLed(String color, @NonNull String functype) {
SerialCmdBean cmdBean;
String cmd;
for (int i = 1; i < ConstantUtil.LED_NUM + 1; i++) {
if ("open".equals(functype)) {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(i), ConstantUtil.OPEN_LED, color);
} else {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(i), ConstantUtil.CLOSE_LED, color);
}
cmd = SerialCmdUtils.buildInstruction(cmdBean);
Log.e("CrazyMO", "controlAllLed: 运行的线程名:"+Thread.currentThread().getName()+" 第" + i + "个灯::::" + cmd);
try {
Thread.sleep(10);//再间隔10ms
HttpApplication.getSerialControler().sendPortData(cmd);//这里在发送指令的线程中已经做了延时10ms的限制
} catch (InterruptedException e) {
e.printStackTrace();
}
catch (Error e){
Log.e("CrazyMo", "controlAllLed: "+e.toString() );
e.printStackTrace();
}
}
try {
Thread.sleep(ConstantUtil.SEND_UPD_CMD_SLEEP_TIME);//每次执行刷新指令时,延时10ms
SerialControlCmdRun.sendUpdateNow();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 当wifi正在连接时
*/
public static void waitingConnecting() {
String color = "EFEC90";
SerialCmdBean cmdBean;
String cmd;
for (int i = 1; i < ConstantUtil.LED_NUM + 1; i++) {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(i), ConstantUtil.OPEN_LED, color);
cmd = SerialCmdUtils.buildInstruction(cmdBean);
Log.e("CrazyMO", "waitingConnecting: 第" + i + "个灯::::" + cmd);
HttpApplication.getSerialControler().sendPortData(cmd);//这里在发送指令的线程中已经做了延时10ms的限制
}
try {
Thread.sleep(ConstantUtil.SEND_UPD_CMD_SLEEP_TIME);//每次执行刷新指令时,延时10ms
SerialControlCmdRun.sendUpdateNow();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 同时关闭16盏Led
*/
public static void waitingConnecting2() {
String color = "EFEC90";
SerialCmdBean cmdBean;
String cmd;
for (int i = 1; i < ConstantUtil.LED_NUM + 1; i++) {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(i), ConstantUtil.CLOSE_LED, color);
cmd = SerialCmdUtils.buildInstruction(cmdBean);
Log.e("CrazyMO", "waitingConnecting2: 第" + i + "个灯::关闭::" + cmd);
HttpApplication.getSerialControler().sendPortData(cmd);
}
try {
Thread.sleep(ConstantUtil.SEND_UPD_CMD_SLEEP_TIME);
SerialControlCmdRun.sendUpdateNow();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
*闪烁效果
* @param color 要显示的颜色值
* @param times 次数
*/
public static void runBreathingLed(String color,int times) {
if(times>0) {
for (int i = 0; i < times; i++) {
if (isOdd(i) == 0) {
controlAllLed(color, "open");
} else {
controlAllLed(color, "close");
}
}
}
}
/**
* 启动跑马灯效果
*/
public static void runMarqueeLed(){
SerialCmdBean cmdBean;
String cmd;
String newhead;
LinkedList<String> list =initMarqueeLinkList(startUpColor);
int colorLen=list.size();
try {
while (true ) {
Log.e("CrazyMo", "runMarqueeLed: 222222" );
if((MessageConstants.MSG_LED_WAITING_WIFI != LedControlService.ledControlBiz)){
Log.e("CrazyMo", "runMarqueeLed: 333" );
break;
}
try {
for (int k = 1; k < colorLen + 1; k++) {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(k), ConstantUtil.OPEN_LED, list.get(k - 1));
cmd = SerialCmdUtils.buildInstruction(cmdBean);
Log.e("CrazyMO", "runMarqueeLed: 第" + k + "个灯::::" + cmd);
HttpApplication.getSerialControler().sendPortData(cmd);//这里在发送指令的线程中已经做了延时10ms的限制
}
newhead = list.getLast();
list.removeLast();
list.addFirst(newhead);
SerialControlCmdRun.sendUpdateNow();
} catch (Exception e) {
e.printStackTrace();
Log.e("CrazyMo", "runMarqueeLed: " + e.toString());
}
}
} catch (Error e){
//很重要,否则无限循环时会异常退出
Log.e("CrazyMo", "runMarqueeLed: "+e.toString());
} catch(Exception e){
Log.e("CrazyMo", "runMarqueeLed: "+e.toString());
}
}
/**
* 初始化跑马灯效果所用的颜色
* @return
*/
public static LinkedList<String> initMarqueeLinkList(String[] colors){
int len=colors.length;
LinkedList<String> list = new LinkedList<String>();
for(int i=0;i<len;i++){
list.add(colors[i]);
}
return list;
}
/**
* 通过数字所以获取对应的Led编号0x01~0x10
* @param no
* @return
*/
public static String getWitchLed(int no) {
if (no < 10) {
return "0" + no;
} else {
switch (no) {
case 10:
return "0A";
case 11:
return "0B";
case 12:
return "0C";
case 13:
return "0D";
case 14:
return "0E";
case 15:
return "0F";
case 16:
return "10";
default:
return "10";
}
}
}
/**
*闪烁灯效果
* @param color 要显示的颜色值
* @param times 次数-1
*/
public static void twinklingLed(String color,int times) {
if(times>0) {
for (int i = 0; i < times; i++) {
//偶数打开
if (isOdd(i) == 0) {
controlAllLed(color, "open");
} else {
controlAllLed(color, "close");
}
}
}
}
/*****以上为通用功能方法,以下方法为具体的情景方法*****/
/**
* 执行Wifi连接之后
* @param failed true 连接成功 false 连接失败
*/
public static void runOnWifiConnect(boolean failed){
HttpApplication.getSerialControler().stopSend();
HttpApplication.getSerialControler().startSend();
controlAllLed("000000","close");
if(failed) {
runBreathingLed("0511FB", 3);
controlAllLed("0511FB", "open");
}else {
runBreathingLed("FF0000", 3);
controlAllLed("FF0000", "open");
}
}
/**
* 当盒子待机时
*/
public static void runOnReady(){
HttpApplication.getSerialControler().stopSend();
HttpApplication.getSerialControler().startSend();
controlAllLed("0511FB","open");
}
/**
* 当盒子等待wifi配置时
*/
public static void runOnWaitConnecting(int times){
HttpApplication.getSerialControler().stopSend();
HttpApplication.getSerialControler().startSend();
String[] colors ={"C3B00F","DFCB25","EDDA39"};//深黄、浅黄、最浅黄
if(times>0) {
while (true) {
try {
for (; ; ) {
controlAllLed(colors[2], "open");
Thread.sleep(50);//每次执行刷新指令时,延时50ms
controlAllLed(colors[1], "open");
Thread.sleep(40);//每次执行刷新指令时,延时50ms
controlAllLed(colors[0], "open");
Thread.sleep(30);//每次执行刷新指令时,延时50ms
}
} catch (Error e) {
//很重要,否则无限循环时会异常退出
Log.e("CrazyMo", "runOnWaitConnecting: " + e.toString());
} catch (Exception e) {
Log.e("CrazyMo", "runOnWaitConnecting: " + e.toString());
}
}
}
}
/**
* 当发送握手包和串口通信成功之后
* 马上进入待机状态呼吸灯效果
*/
public static void runAfterHoldSuccess(){
HttpApplication.getSerialControler().stopSend();
HttpApplication.getSerialControler().startSend();
String[] colors ={"EDDA39","DFCB25","C3B00F"};//最浅黄、中黄、深黄
LinkedList<String> list =initMarqueeLinkList(colors);
String headColor;
while (true) {
//为了避免线程无线调用
if((MessageConstants.MSG_LED_AFTER_HOLD != LedControlService.ledControlBiz)){
Log.e("CrazyMo", "runAfterHoldSuccess: 23" );
break;
}
try {
controlAllLed(list.getFirst(), "open");
headColor=list.removeFirst();
list.addLast(headColor);
SerialControlCmdRun.sendUpdateNow();
Thread.sleep(20);
} catch (Error e) {
//很重要,否则无限循环时会异常退出
Log.e("CrazyMo", "runOnWaitConnecting: " + e.toString());
} catch (Exception e) {
Log.e("CrazyMo", "runOnWaitConnecting: " + e.toString());
}
}
}
/**
* 當盒子正在进行wifi连接的时候
*/
public static void runOnWifiConnecting(){
SerialCmdBean cmdBean;
String cmd;
String newhead;
LinkedList<String> list =initMarqueeLinkList(wifiConnectingColor);
int colorLen=list.size();
try {
while (true ) {
Log.e("CrazyMo", "runOnWifiConnecting: 222222" );
if((MessageConstants.MSG_LED_WIFI_CONNECTING != LedControlService.ledControlBiz)){
Log.e("CrazyMo", "runOnWifiConnecting: 333" );
break;
}
try {
for (int k = 1; k < colorLen + 1; k++) {
cmdBean = new SerialCmdBean("E2E2C4", getWitchLed(k), ConstantUtil.OPEN_LED, list.get(k - 1));
cmd = SerialCmdUtils.buildInstruction(cmdBean);
Log.e("CrazyMO", "runOnWifiConnecting: 第" + k + "个灯::::" + cmd);
HttpApplication.getSerialControler().sendPortData(cmd);//这里在发送指令的线程中已经做了延时10ms的限制
}
newhead = list.getLast();
list.removeLast();
list.addFirst(newhead);
SerialControlCmdRun.sendUpdateNow();
} catch (Exception e) {
e.printStackTrace();
Log.e("CrazyMo", "runOnWifiConnecting: " + e.toString());
}
}
} catch (Error e){
//很重要,否则无限循环时会异常退出
Log.e("CrazyMo", "runOnWifiConnecting: "+e.toString());
} catch(Exception e){
Log.e("CrazyMo", "runOnWifiConnecting: "+e.toString());
}
}
/**
* 當盒子wifi连接成功的时候
* 闪烁蓝色两次然后常亮
*/
public static void runOnWifiConnectedSuccess(){
twinklingLed("0511FB",3);
controlAllLed("0511FB","open");
}
/**
* 当设备(场景)执行失败时
* 16个LED 红色闪一下
*/
public static void runOnVoiceCmdFailed(){
String color="FE0000";
twinklingLed(color,2);
}
}
8、发送指令到串口
下一篇文章再细讲,结合具体的业务模型来总结。
PS
以上所有代码都是demo阶段的,均有很大的优化空间,如果需要使用到的话不要直接使用,实际的项目中我也进行了部分的重构和优化,核心知识是不变的。