- 整体思路:DTU是数据透传模式,将服务器的指令通过DTU直接传到电表。返回的电表数据通过DTU的物联网卡直接传给服务器。
- 获取电表数据方式:首先正确连接设备(DTU和电表),注意DTU需要安装物联网卡,物联网卡需要查看是否激活。
- 初步可以通过测试工具先查看。正确配置测试工具,需要内网穿透工具。本次使用的是【花生壳】。TCP服务调试工具使用的是【wltszs4.3.29.exe】(网络调试助手)。辅助工具【ComMonitor.exe】(串口调试软件)主要用于生成报文里的【帧校验和CS】。
- 完成以上配置后,在网络调试助手中发送正确的报文就可以获取到电表数据。以下图片仅供效果参考,我使用的DTU是定制产品,具体报文不具备参考性质。 解析报文协议可以参考:水(CJ/T188)电(DL/T645)抄表数据解析示例_cjt188水表数据解析-CSDN博客国网376.1-2013协议解析 - 哔哩哔哩
5、java代码设计思路: 1、注册端口:定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息 3、开始创建独立线程处理socket。
@Component
public class TCPServer {
public static void startServerMethod() {
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("===服务端启动成功===");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(???);
// a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
while (true) {
// 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(simpleDateFormat.format(new Date())+"["+socket.getRemoteSocketAddress()+ "]它来了,上线了!");
// 3、开始创建独立线程处理socket
new ServerReaderThread(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4、独立线程代码:
/**
*开始创建独立线程处理socket
**/
public class ServerReaderThread extends Thread{
//业务service 手动注入bean
private xxxService xxxService = BeanContext.getBean(xxxService.class);
private InputStream inputStream;
private OutputStream outputStream;
private Socket socket;
public ServerReaderThread(Socket socketPara){
try{
this.socket=socketPara;
inputStream=socketPara.getInputStream();
outputStream=socketPara.getOutputStream();
}
catch(IOException e){
e.printStackTrace();
}
}
@Override
public void run() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
// 在死循环中处理与客户的交流
while(true) {
byte[] buffer = new byte[1024];
int length = inputStream.read(buffer);
// 5、按照行读取消息
String hexString = "";
for (int i = 0; i < length; i++) {
String hex = Integer.toHexString(buffer[i] & 0xFF);
if (hex.length() == 1) {
hex = "0" + hex;
}
hexString += " "+hex;
}
System.out.println(simpleDateFormat.format(new Date())+"---收到报文:" + hexString);
String a = hexString.substring(4,6);
//设备信息
if(a.equals("32")){
//DTU登录确认
loginDtu(hexString);
}else if(a.equals("4a")){
//回复心跳
sendMsg(LOGIN_CONFIRM_STR);
}else {
//存储报文中的电表数据
saveDate(hexString);
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
}
}
/**
* DTU登录确认
* @param hexString
*/
private void loginDtu(String hexString) throws IOException {
//登录确认报文
LOGIN_CONFIRM_STR
//登录确认报文(主站 -> 终端)
sendMsg(LOGIN_CONFIRM_STR);
//开启定时任务
startTimeTask();
}
/**
* 给DTU发送请求报文
* @param msg
*/
public void sendMsg(String msg) throws IOException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(new Date())+"---发送报文到["+socket.getRemoteSocketAddress()+"]:" + msg);
// 将16进制字符串转换为字节数组并发送
byte[] data = new byte[msg.length() / 2];
for (int i = 0; i < data.length; i++) {
int index = i * 2;
int value = Integer.parseInt(msg.substring(index, index + 2), 16);
data[i] = (byte) value;
}
outputStream.write(data);
}
/**
* 定时请求任务
*/
private void startTimeTask() {
//启动定时
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
// 这里是要定时执行的代码
System.out.println("==============定时任务执行...");
// 执行你的方法
try {
//业务需求 五分整点时间执行请求
checkTimeTaskMethod();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
// 定时器每隔1分钟执行一次
timer.scheduleAtFixedRate(task, 0, 1*60*1000);
}
/**
* 判断是否是五分的整数时间 是的话请求电表数据
*/
private void checkTimeTaskMethod() throws IOException {
// 创建一个DateTimeFormatter实例来定义时间的格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println("=============开始判断五分整数时间点");
LocalDateTime now = LocalDateTime.now();
Boolean flag = isFiveMinuteTime(now);
if(flag){
// 使用formatter格式化LocalTime
String formattedTime = now.format(formatter);
System.out.println(formattedTime+":是五分整数时间点,开始请求电表数据。");
sendMsg(f225_ONE_STR);
}
}
/**
* 判断是否是五分的整数时间 10:00 10:05 10:10...
* @param time
* @return
*/
public static boolean isFiveMinuteTime(LocalDateTime time) {
int minuteOfDay = time.getMinute();
return minuteOfDay % 5 == 0;
}
}
手动获取bean。因为独立线程不能直接注入service。
/**
* 手动获取Bean
* https://blog.csdn.net/tiger0709/article/details/78270768
* https://blog.csdn.net/u011493599/article/details/78522315
*/
@Component
public class BeanContext implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
BeanContext.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
return (T) applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> clz) throws BeansException {
return (T) applicationContext.getBean(clz);
}
}
6、报文解析:
1.报文格式
其中校验和CS就是从控制域到链路用户数据的报文校验总加和(出来的是三位就截取最后两位):
public static String calculateChecksum(String hexString) {
// 将十六进制字符串转换为字节数组
byte[] bytes = new byte[hexString.length() / 2];
System.out.println("字节长度:"+bytes.length);
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) Integer.parseInt(hexString.substring(2 * i, 2 * i + 2), 16);
}
// 对字节数组求和
int checksum = 0;
for (byte b : bytes) {
checksum += b & 0xFF;
}
// 将总和转换为十六进制字符串
return Integer.toHexString(checksum).toUpperCase();
}