美文网首页
Android之无线智能家居实现

Android之无线智能家居实现

作者: Zac程序员的日常生活 | 来源:发表于2019-04-08 17:41 被阅读0次

    起源

    晚上在家里洗澡的时候,突然想听听歌,自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<DeviceBean> 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<DeviceBean> 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之无线智能家居实现

          本文链接:https://www.haomeiwen.com/subject/jsppiqtx.html