迅速上手android UDP控制網路開關量,以及常見坑

weixin_34107955發表於2018-08-02

[目標]

透過android裝置,實現對某一開關量的控制,達到給電子門鎖通電開啟的目的。
網路開關量控制器如下:


5117013-338cf34eb9bc56cc.png
image.png

本質上就是一個把乙太網協議轉換成對輸出開關量的動作的硬體模組。

[裝置]

  1. NR01 網路開關量控制器。含乙太網口一個
  2. 普通家用路由器,型號TP- LINK WR541G+(10年前的型號了)
  3. 商用android裝置一臺

[設計思路]

雖然開關量控制器提供ModBus協議,但是給android來控制,有點大材小用了。
直接用它提供的UDP模式,發個包控制繼電器輸出開一下,給門鎖訊號就行了。

根據網路開關量控制器報文,android裝置傳送一個UDP包on1:01 代表1號繼電器開發閉合1秒。
如果網路開發控制器收到了,就回應一個UDP包,內容是on1

分為一個UDP傳送執行緒和一個UDP接收執行緒。 傳送一次,就等待接收一會兒。收到on1就不再傳送UDP開鎖包了。超時收不到就再傳送一次。即使UDP經常丟包,在區域網裡,試3次到5次也夠了。

[實做]

UDP傳送很簡單,網上文章很多。接收也描述的很多,不過好多是描述對UDP支援多麼的不好。
這次用的是Android一個商用裝置,對系統篡改少一些。

[踩坑]

最主要的是DatagramSocket對傳送和接收來講一定只用一個例項
否則程式碼中UDP一個位元組都接收不到。

至於傳送和接收是1個執行緒還是2個執行緒,並不重要。

[程式碼]

ShopDoor.java

/*******************************************************************************
 * Copyright 2018 Stephen Zhu
 *
 * 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 com.xxx.peripheral;

import com.xxx.UniCallBack;
import com.xxx.DebugUtil;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;
import java.util.Date;

/**
 * 門鎖開關
 * <p>
 * 開啟門鎖功能
 * @author zhuguangsheng
 */

public class ShopDoor {
    /*UDP方式向控制器傳送訊號*/

    //傳送IP
    private static String SEND_IP = "";
    //傳送埠號
    private static int SEND_PORT = 5000;

    //傳送重試次數
    private final static int SEND_RETRY_COUNT = 5;
    //每次重試延時
    private final static int SEND_RETRY_INTERVAL_MS = 2000;
    //接收超時
    private final static int RECEIVE_SO_TIMEROUT = 800;

    /**
     * 傳送執行緒的迴圈標識
     */
    private static boolean sendFlag = true;
    /**
     * 接收執行緒的迴圈標識
     */
    private static boolean listenStatus = true;

    private static byte[] receiveInfo;
    private static byte[] SENDBUF = "on1:01".getBytes();
    private static byte[] RECEIVEOK = "on1".getBytes();

    static UniCallBack sCallBack;

    /**
     * 傳送和接收共用一個DatagramSocket,實際試過,如果用2個,那接收不到UDP
     */
    static DatagramSocket mUdpSocket;

    /**
     * 開門
     * 非同步非阻塞呼叫,靠callback返回撥用成功或失敗
     *
     * @param sendIp
     * @param sendPort
     * @param callBack 回撥,成功或失敗
     */
    public static void OpenDoor(String sendIp, int sendPort, final UniCallBack callBack) {
        SEND_IP = sendIp;
        SEND_PORT = sendPort;
        sCallBack = callBack;

        //讓傳送能迴圈
        sendFlag = true;
        //讓接收能迴圈
        listenStatus = true;

        //啟動接收執行緒
        new UdpReceiveThread().start();

        //稍等一會,讓接收執行緒穩定
        try {
            Thread.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new UdpSendThread().start();

    }

    /**
     * UDP資料傳送執行緒
     */
    public static class UdpSendThread extends Thread {
        @Override
        public void run() {
            try {
                for (int i = 0; i < SEND_RETRY_COUNT; i++) {
                    if(!sendFlag){
                        break;
                    }

                    InetAddress serverAddr;

                    //mUdpSocket = new DatagramSocket();
                    if(mUdpSocket==null){
                        mUdpSocket = new DatagramSocket(null);
                        mUdpSocket.setReuseAddress(true);
                        mUdpSocket.bind(new InetSocketAddress(SEND_PORT));
                    }
                    serverAddr = InetAddress.getByName(SEND_IP);
                    DatagramPacket outPacket = new DatagramPacket(SENDBUF, SENDBUF.length, serverAddr, SEND_PORT);
                    mUdpSocket.send(outPacket);
                    //在傳送這裡不close了,要不然接收的也被close了。統一到接收結束的地方close
                    //mUdpSocket.close();

                    Thread.sleep(SEND_RETRY_INTERVAL_MS);
                }
                //n次之後,還沒結果也別收了,算失敗。接收執行緒也停了算了
                listenStatus = false;
            } catch (Exception e) {
                e.printStackTrace();
                try {
                    sCallBack.onError("open door signal send error 開門傳送過程錯誤");
                }catch (Exception e1){
                    DebugUtil.log("sCallBack exception");
                }
            }
        }
    }

    /**
     * UDP資料接收執行緒
     */
    public static class UdpReceiveThread extends Thread {
        @Override
        public void run() {
            while (listenStatus) {
                try {
                    DatagramChannel channel = DatagramChannel.open();

                    if (mUdpSocket == null) {
                        mUdpSocket = channel.socket();
                        mUdpSocket.setReuseAddress(true);
                    }
                    //serverAddr = InetAddress.getByName(SEND_IP);

                    byte[] inBuf = new byte[1024];
                    DatagramPacket inPacket = new DatagramPacket(inBuf, inBuf.length);
                    mUdpSocket.setSoTimeout((int) (RECEIVE_SO_TIMEROUT));
                    DebugUtil.log("before receive start time " + System.currentTimeMillis());
                    mUdpSocket.receive(inPacket);

                    /**
                    if (!inPacket.getAddress().equals(serverAddr)) {
                        //throw new IOException("未知名的報文");
                        DebugUtil.log("未知地址的報文");
                    }
                    */

                    receiveInfo = inPacket.getData();
                    String receiveInfoStr = new String(receiveInfo);
                    String receiveOk = new String(RECEIVEOK);
                    DebugUtil.log("receiveInfo=" + receiveInfoStr);
                    if (receiveInfoStr.contains(new String(receiveOk))) {
                        sendFlag = false;
                        listenStatus = false;

                        try {
                            sCallBack.onSuccess(receiveOk);
                        }catch (Exception e1){
                            DebugUtil.log("sCallBack exception");
                        }
                        //不再接收了
                    } else {
                        //可能是別的指令的回覆,忽略它行了,繼續迴圈
                        continue;
                    }
                    Thread.sleep(100);

                } catch (Exception e) {
                    e.printStackTrace();
                    DebugUtil.log("after receive exception time " + System.currentTimeMillis());
                    try {
                        sCallBack.onError("接收過程錯誤" + e.toString());
                    }catch (Exception e1){
                        DebugUtil.log("sCallBack exception");
                    }
                }
            }

            //嘗試清除工作
            try{
                mUdpSocket.close();
                mUdpSocket = null;
            }catch (Exception e){
                e.printStackTrace();
            }
            //接收執行緒結束
        }
    }
}

另:用到的UniCallBack只有以下幾行,是自定義的介面
UniCallBack.java

public interface UniCallBack {
    public void onSuccess(String msg);
    public void onError(String err);
}

而DebugUtil.java也可以簡寫為以下內容:

public class DebugUtil {
    
    /**
     * 輸出一行log
     * @param level
     * @param msg
     */
    private static void log(Level level, String msg){
        Log.i("debuglog", msg);
    }
}

結束

歡迎指出問題,歡迎各種合作聯絡。

相關文章