Android NDK——实战演练之App端通过串口通信完成实时控制单片机上LED灯的颜色及灯光动画特效(三)

引言

最近刚刚实现了一个与串口通信的小模块,主要的需求背景是,我们嵌入式工程师开发了一块圆形的板子用于控制LED灯,考虑到后期的调试和开发便捷性,便把控制LED灯的各种灯光效果,交给了app端去实现,于是自然落在我的肩上,经过一番努力折腾,终于成功实现了需求,包括一整套的监听和同步框架,特记录下来好好总结一番。此系列相关文章链接:

一、串口与相关术语介

串口全称串行接口,也称串行通信接口或串行通讯接口(通常指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阶段的,均有很大的优化空间,如果需要使用到的话不要直接使用,实际的项目中我也进行了部分的重构和优化,核心知识是不变的。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CrazyMo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值