Android之无线智能家居实现

起源

晚上在家里洗澡的时候,突然想听听歌,自high一把,拿起洗漱柜上的手机放音乐,不过因为手上的水,导致屏幕按钮点击特别烦,结果它掉地上了。这时候突然有一种想法,我用Android能不能实现类似"天猫精灵"这些东西呢?

正文

概述

其实现流程共分3步,分别为寻址授权,局域网通信,外网通信。其总体架构图如下所示:


整体架构图.png

如上所示,整体架构分2个模块,分别为家庭局域网模式和互联网模式,初始状态时,手机通过家庭局域网获取到Android设备的IP信息以及相应的授权任务,从而获得设备操作权限,之后通过局域网通讯的方式进行业务操作,同时该设备会与服务端进行任务同步。当该通过授权的手机在互联网模式下时,可进行任务下发,此时家庭局域网中的Android设备会同步到来自服务端下发的任务,进行相应的业务操作。

寻址授权

该流程为架构实现的第一步,也是实现局域网通讯的前提。因为设备处于无线模式下,可能会导致IP前后出现变化,所以每次局域网通讯之前需要先获取到设备的IP地址,实现该功能的方案有3种,一种是IP轮询检索,其次是蓝牙配对,最后一种是UDP广播。

IP轮询检索:即从1~255进行一个个的socket连接测试,Android设备端进行accept,手机端进行连接尝试,当手机端获取到来自设备端的返回时,说明当前的IP为该设备的IP地址。但是该方法耗时且对性能不足的特点,此案中不引入。

蓝牙配对:通过手机设备的蓝牙进行检索附近的蓝牙设备,然后进行配对授权,因为考虑到该功能的实现需要蓝牙服务,提升了设备成本,其次蓝牙服务只能一对一进行交互服务,当存在多部手机设备时,无法满足该功能。最后因为蓝牙服务差不多为10~15m的覆盖范围,考虑家庭中存在墙面等情况,该方案并不合适。

UDP广播:使用UDP协议进行信息的传输之前不需要建立连接。换句话说就是客户端向服务器发送信息,客户端只需要给出服务器的ip地址和端口号,然后将信息封装到一个待发送的报文中并且发送出去。至于服务器端是否存在,或者能否收到该报文,客户端根本不用管。其中广播UDP与单播UDP的区别就是IP地址不同,广播使用广播地址255.255.255.255,将消息发送到在同一广播网络上的每个主机。该方案也是本案所采用的方案。

ps:androidSDK中在android.net.nsd目录下存在NsdManager一个类,该类能够实现局域网下面的android设备通讯,并且SDK已经提供了相应的封装,使用起来非常方便,实现原理是通过网络服务的发现服务NsdService,其基于苹果的Bonjour服务发现协议,支持远程服务的发现和零配置,相对考虑到IOS的实现,怕出现兼容性问题,所以该方案暂时不进行考虑。

实现流程

寻址授权流程图.png

实现代码

手机设备请求方

public abstract class DeviceSearchWorker extends Thread {

    private static final String TAG = "DeviceSearchWorker";


    private static final int RECEIVE_TIME_OUT = 15000; // 接收超时时间


    private static final byte PACKET_TYPE_FIND_DEVICE_REQ_13 = 0x13; // 搜索请求
    private static final byte PACKET_TYPE_FIND_DEVICE_RSP_14 = 0x14; // 搜索响应
    private static final byte PACKET_TYPE_FIND_DEVICE_CHK_15 = 0x15; // 搜索确认


    private static final int DEVICE_FIND_PORT = 10000;
    private static final int RESPONSE_DEVICE_MAX = 200; //接收消息的最大次数


    private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20;
    private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21;

    private String deviceIP; //发送广播之后,设备返回来的设备ip地址
    DatagramSocket socket = null;

    private Set deviceSet;

    public DeviceSearchWorker(){
        deviceSet = new HashSet<>();
    }


    private Handler myHandler = new Handler(Looper.getMainLooper());

    @Override
    public void run() {
        super.run();

        try{
            onPushDeviceSearchStartMsg();
            socket = new DatagramSocket();
            socket.setSoTimeout(RECEIVE_TIME_OUT);

            byte[] sendData = new byte[1024];
            InetAddress broadIp = InetAddress.getByName("255.255.255.255");  //来一个广播
            DatagramPacket packet = new DatagramPacket(sendData, sendData.length, broadIp, DEVICE_FIND_PORT);

            for (int i = 0; i < 3; i++){
                packet.setData(packetData(i + 1, PACKET_TYPE_FIND_DEVICE_REQ_13));
                //发送广播
                socket.send(packet);
                // 监听来信
                byte[] receData = new byte[1024];
                DatagramPacket recePacket = new DatagramPacket(receData, receData.length);

                int rspCount = RESPONSE_DEVICE_MAX;
                while (rspCount-- > 0) {
                    LogUtils.i(TAG, "DatagramPacket >>> " + rspCount);
                    recePacket.setData(receData);
                    socket.receive(recePacket);
                    if (recePacket.getLength() > 0) {
                        deviceIP = recePacket.getAddress().getHostAddress();
                        if (parsePack(recePacket)) {
                            LogUtils.i(TAG, "设备上线:" + deviceIP);
                            // 发送一对一的确认信息。使用接收报,因为接收报中有对方的实际IP,发送报时广播IP
                            recePacket.setData(packetData(rspCount, PACKET_TYPE_FIND_DEVICE_CHK_15)); // 注意:设置数据的同时,把recePack.getLength()也改变了
                            socket.send(recePacket);
                            onPushDeviceSearchFinishedMsg();
                        }
                    }
                }
            }

        }catch (SocketException e){
            e.printStackTrace();
            onPushDeviceSearchFailedMsg();
        } catch (UnknownHostException e) {
            e.printStackTrace();
            onPushDeviceSearchFailedMsg();
        } catch (IOException e) {
            e.printStackTrace();
            onPushDeviceSearchFailedMsg();
        } finally {
            if(socket != null){
                socket.close();

            }
        }
    }

    private void onPushDeviceSearchFailedMsg() {
        myHandler.post(new Runnable() {
            @Override
            public void run() {
                onPushDeviceSearchFailedMsg();
            }
        });
    }

    private void onPushDeviceSearchFinishedMsg() {
        myHandler.post(new Runnable() {
            @Override
            public void run() {
                onDeviceSearchFinished(deviceSet);
            }
        });
    }

    private void onPushDeviceSearchStartMsg() {
        myHandler.post(new Runnable() {
            @Override
            public void run() {
                onDeviceSearchStart();
            }
        });
    }


    /**
     * 解析报文
     * 协议:$ + packType(1) + data(n)
     *  data: 由n组数据,每组的组成结构type(1) + length(4) + data(length)
     *  type类型中包含name、room类型,但name必须在最前面
     */
    private boolean parsePack(DatagramPacket pack) {
        if (pack == null || pack.getAddress() == null) {
            return false;
        }


        String ip = pack.getAddress().getHostAddress();
        int port = pack.getPort();
        for (DeviceBean d : deviceSet) {
            if (d.getIp().equals(ip)) {
                return false;
            }
        }
        int dataLen = pack.getLength();
        int offset = 0;
        byte packType;
        byte type;
        int len;
        DeviceBean device = null;

        if (dataLen < 2) {
            return false;
        }
        byte[] data = new byte[dataLen];
        System.arraycopy(pack.getData(), pack.getOffset(), data, 0, dataLen);

        if (data[offset++] != '$') {
            return false;
        }

        packType = data[offset++];
        if (packType != PACKET_TYPE_FIND_DEVICE_RSP_14) {
            return false;
        }

        while (offset + 5 < dataLen) {
            type = data[offset++];
            len = data[offset++] & 0xFF;
            len |= (data[offset++] << 8);
            len |= (data[offset++] << 16);
            len |= (data[offset++] << 24);

            if (offset + len > dataLen) {
                break;
            }
            switch (type) {
                case PACKET_DATA_TYPE_DEVICE_NAME_20:
                    String name = new String(data, offset, len, Charset.forName("UTF-8"));
                    device = new DeviceBean();
                    device.setName(name);
                    device.setIp(ip);
                    device.setPort(port);
                    break;
                case PACKET_DATA_TYPE_DEVICE_ROOM_21:
                    String room = new String(data, offset, len, Charset.forName("UTF-8"));
                    if (device != null) {
                        device.setRoom(room);
                    }
                    break;
                default: break;
            }
            offset += len;
        }
        if (device != null) {
            deviceSet.add(device);
            return true;
        }
        return false;
    }

    /**
     * 协议:$ + packType(1) + sendSeq(4) + [deviceIP(n<=15)]
     * @param seq 发送序列
     * @param packetType 报文类型
     * @return
     */
    private byte[] packetData(int seq, byte packetType) {
        byte[] data = new byte[1024];
        int offset = 0;

        data[offset++] = '$';

        data[offset++] = packetType;

        seq = seq == 3 ? 1 : ++seq; // can't use findSeq++
        data[offset++] = (byte) seq;
        data[offset++] = (byte) (seq >> 8 );
        data[offset++] = (byte) (seq >> 16);
        data[offset++] = (byte) (seq >> 24);

        if (packetType == PACKET_TYPE_FIND_DEVICE_CHK_15) {
            byte[] ips = deviceIP.getBytes(Charset.forName("UTF-8"));
            System.arraycopy(ips, 0, data, offset, ips.length);
            offset += ips.length;
        }

        byte[] result = new byte[offset];
        System.arraycopy(data, 0, result, 0, offset);
        return result;



    }


    public abstract void onDeviceSearchStart();


    public abstract void onDeviceSearchFinished(Set deviceSet);


    public abstract void onDeviceSearchFailed();


    public void close(){
        try{
            if(socket != null){
                socket.close();
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        this.interrupt();
    }
}

Android设备接收方

public abstract class DeviceClientWorker extends Thread {

    /**
     * 设备对应的port
     */
    private static final int DEVICE_FIND_PORT = 10000;

    private static final String TAG = "DeviceClientWorker";

    private static final int RECEIVE_TIME_OUT = 10000; // 接收超时时间,应小于等于主机的超时时间10000

    private static final byte PACKET_TYPE_FIND_DEVICE_REQ_13 = 0x13; // 搜索请求
    private static final byte PACKET_TYPE_FIND_DEVICE_RSP_14 = 0x14; // 搜索响应
    private static final byte PACKET_TYPE_FIND_DEVICE_CHK_15 = 0x15; // 搜索确认

    private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20; //设备名称
    private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21; //设备所处的房间名

    private static final int RESPONSE_DEVICE_MAX = 200; // 响应设备的最大个数,防止UDP广播攻击


    private static Handler workHandler = new Handler(Looper.getMainLooper());


    /**
     * 设备名称
     */
    private String deviceName;
    /**
     * 房间名称
     */
    private String room;

    private boolean isRunning;

    DatagramSocket socket = null;


    public DeviceClientWorker(String deviceName, String room){
        this.deviceName = deviceName;
        this.room = room;
        isRunning = true;
    }



    @Override
    public void run() {
        super.run();

        try {
            socket = new DatagramSocket(DEVICE_FIND_PORT);
            byte[] data = new byte[1024];
            DatagramPacket packet = new DatagramPacket(data, data.length);
            while (isRunning){
                LogUtils.i(TAG, "waitting receive data");
                socket.receive(packet);  //等待接收数据
                LogUtils.i(TAG, "data received");
                if(verifySearchData(packet)){
                    byte[] backData = packData();
                    LogUtils.i(TAG, "back device info");
                    DatagramPacket sendPacket = new DatagramPacket(backData, backData.length, packet.getAddress(), packet.getPort());
                    socket.send(sendPacket);
                    socket.setSoTimeout(RECEIVE_TIME_OUT);
                    LogUtils.i(TAG, "waitting for server veritify again");
                    socket.receive(packet);
                    if(verifyCheckData(packet)){ //验证确认信息
                        pushDeviceClientSearchedMsg((InetSocketAddress)packet.getSocketAddress());
                    }
                }

                socket.setSoTimeout(0); // 连接超时还原成无穷大,阻塞式接收
            }
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(socket != null){
                socket.close();
            }
        }
    }

    private void pushDeviceClientSearchedMsg(final InetSocketAddress socketAddress) {
        workHandler.post(new Runnable() {
            @Override
            public void run() {
                onDeviceSearchedCallBack(socketAddress);
            }
        });
    }

    /**
     * 当设备被发现时执行
     */
    public abstract void onDeviceSearchedCallBack(InetSocketAddress socketAddr);

    /**
     * 验证再次确认信息
     * 协议:$ + packType(1) + sendSeq(4) + deviceIP(n<=15)
     *  packType - 报文类型
     *  sendSeq - 发送序列
     *  deviceIP - 设备IP,仅确认时携带
     * @param packet
     * @return
     */
    private boolean verifyCheckData(DatagramPacket packet) {
        if(packet.getLength() < 6){
            return false; //前面的$ + packType(1) + sendSeq(4)   共占6位
        }

        byte[] data = packet.getData();
        int offset = packet.getOffset();
        int sendSeq;
        if (data[offset++] != '$' || data[offset++] != PACKET_TYPE_FIND_DEVICE_CHK_15) {
            return false;
        }

        sendSeq = data[offset++] & 0xFF;
        sendSeq |= (data[offset++] << 8 );
        sendSeq |= (data[offset++] << 16);
        sendSeq |= (data[offset++] << 24);
        if (sendSeq < 1 || sendSeq > RESPONSE_DEVICE_MAX) {
            return false;
        }

        String ip = new String(data, offset, packet.getLength() - offset, Charset.forName("UTF-8"));
        LogUtils.i(TAG, "ip from host : " + ip);
        return ip.equals(DeviceUtils.getOwnWifiIP());
    }

    /**
     * 搜索响应
     * 组装搜索反馈信息
     * 协议:$ + packType(1) + data(n)
     * data: 由n组数据,每组的组成结构type(1) + length(4) + data(length)
     * type类型中包含name、room类型,但name必须在最前面
     * @return
     */
    private byte[] packData() {
        byte[] data = new byte[1024];
        int offset = 0;
        data[offset++] = '$';
        data[offset++] = PACKET_TYPE_FIND_DEVICE_RSP_14;
        //追加设备名称信息
        byte[] temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_NAME_20, deviceName);
        System.arraycopy(temp, 0, data, offset, temp.length);
        offset += temp.length;

        temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_ROOM_21, room);
        System.arraycopy(temp, 0, data, offset, temp.length);
        offset += temp.length;

        byte[] retVal = new byte[offset];
        System.arraycopy(data, 0, retVal, 0, offset);

        return retVal;
    }

    /**
     * 根据类型 追加数据
     * @param dataType
     * @param data
     * @return
     */
    private byte[] getBytesFromType(byte dataType, String data) {
        byte[] retVal = new byte[0];
        if(data != null){
            byte[] tmpData = data.getBytes(Charset.forName("utf-8"));
            retVal = new byte[5 + tmpData.length]; //5来源于  type(1) + length(4)
            retVal[0] = dataType;
            retVal[1] = (byte) tmpData.length;
            retVal[2] = (byte) (tmpData.length << 8 );
            retVal[3] = (byte) (tmpData.length << 16);
            retVal[4] = (byte) (tmpData.length << 24);
            System.arraycopy(tmpData, 0, retVal, 5, tmpData.length);
        }
        return retVal;
    }

    /**
     * 验证接收到的数据是否为约定的合法搜索数据
     * 协议:$ + packType(1) + sendSeq(4)
     * @param packet
     * @return
     */
    private boolean verifySearchData(DatagramPacket packet) {
        if(packet.getLength() != 6){
            return false;
        }

        byte[] data = packet.getData();
        int offset = packet.getOffset();
        int sendReq = 0;
        //校验格式是否正确
        if (data[offset++] != '$' || data[offset++] != PACKET_TYPE_FIND_DEVICE_REQ_13) {
            return false;
        }

        sendReq = data[offset++] & 0xFF;
        sendReq |= (data[offset++] << 8 );
        sendReq |= (data[offset++] << 16);
        sendReq |= (data[offset++] << 24);
        return sendReq >= 1 && sendReq <= 3;
    }

    public void close() {
        try{
            if(socket != null){
                socket.close();
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        isRunning = false;
        this.interrupt();
    }
}

考虑到安全性交互,可进行多个不同的协议进行设备认证,以上代码并不包含token验证机制,可后续进行追加

局域网通信

该环节需要将Android设备当做服务器,来处理接收客户端的业务请求。因为在寻址授权那一环节时,使用UDP来实现功能。当时想到的是后续的索性全部使用UDP来实现好了,全部使用自定义协议。但是这样一来,发现后续所有的业务需求的实现都无法借鉴我们平时通用的方法了,此时就想,是不是能模拟Http请求,使用Android设备实现后台服务?

Android设备服务端实现
通常我们搭建后台Server所使用的是Tomcat容器,翻阅了一下资料,发现Apache官方提供了一个叫HttpCore这个包,可以用它来建立客户端、代理、服务端Http服务同时支持同步异步服务,相关链接地址,除此之外,还需要模拟HttpServlet,并对其进行Controller、Service、Dao三层模块划分,这里介绍一个Android的开源框架:AndServer

其实现的系统流程图如下所示:


system_flow_chat.gif

应用层运行时流程图如下所示:


framework_flow_chat.gif

该框架模拟了SpringMVC的注解方式来实现,最后关于Dao层数据库的实现,使用LitePal数据库框架进行实现。

手机局域网客户端实现

相对来说,客户端的实现非常简单,在通过UDP授权之后,将会获取到来自Android设备的token,以及IP地址,后续的业务请求只需要通过Http请求服务器的方式请求局域网中的Android设备,这里不再进行详细介绍

外网通信

该环节产生的场景来自于当我们身处在非局域网覆盖范围时,但又想要进行Android设备操作,如:迅雷下载


外网同步.png

手机实现

判断当前设备是否进行过授权操作,如存在多个,可进行Android设备的选择,同时进行业务请求操作给服务端

服务端实现

接收来自手机设备的业务请求,每个请求中会包含需要操作的Android设备ID信息以及业务行为,并对该操作进行SyncKey的自增。

接收来自Android设备的每隔10s的轮询同步请求,返回该ID设备对应的业务行为。

Android设备实现

当同步到新的SyncKey信息时,更新本地Job数据库,并对新的业务进行流程处理

扩展

  1. 将家庭电视机集成Android设备的应用,实现媒体资源下载,迅雷下载,室内视频遥控控制
  2. 在树莓派3中烧入AndroidThings,并在应用中集成科大讯飞语音功能,实现室内无线投屏,窗帘、灯泡开关控制、音乐播放等等
  3. Android设备集成相机功能,实现视频流RTSP实时传输远程查看家庭监控
  4. Android设备集成Face++,实现门锁开关报警
  5. AndroidThings集成一氧化碳传感器,实现家庭燃气检测报警
  6. 等等等等

你可能感兴趣的:(Android之无线智能家居实现)