[目标]
透过android设备,实现对某一开关量的控制,达到给电子门锁通电打开的目的。
网络开关量控制器如下:
image.png
本质上就是一个把以太网协议转换成对输出开关量的动作的硬件模块。
[设备]
- NR01 网络开关量控制器。含以太网口一个
- 普通家用路由器,型号TP- LINK WR541G+(10年前的型号了)
- 商用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);
}
}
结束
欢迎指出问题,欢迎各种合作联系。
网友评论