一、此文目的
- 适用于刚接触智能水电表对接的人员,几十、几百甚至几千台设备的不成问题,但几十万设备的对接尚未涉及。
- 为刚接触此类对接场景的小伙伴提供整体思路及注意点,虽未贴全部代码,但核心代码已到位。
二、涉及问题
(一)协议不同
- 设备通信并非 Java Web 的 HTTP 协议。HTTP 基于 TCP/IP,TCP/IP 可视为 HTTP 上层协议。
- 支持 TCP/IP 协议的设备可借助 Netty 对接 Java Web 项目,Maven 集成 Netty 方法可自行百度。
(二)端口配置
- Java 容器启动时,Netty 监听的端口随之启动,可监听多个端口,如水表可用 8081 端口,电表可用 8082 端口。
- 访问 Java Web 项目地址为 192.160.0.22:8080 时,电表的通信模组配置地址为 192.160.0.22:8082。
- 调试时可下载 NetAssist.exe,设置为 TCP 服务器或客户端。
(三)如何通信
- 智能电表因内置支持 4G/5G 远程通信的模组而具备远程通信功能。
- 水电表通信模组需配置 Java 程序部署的服务器地址加 Netty 监听的端口(非 Java 项目启动端口)。
- 通过 TCP/IP 方式通信,硬件设备通信模组可设置 HEX(16 进制)或 ASCII(10 进制)与 Netty 交互,16 进制字节码对应 Java 里的 ByteBuff,ASCII 对应 String。
- 启动 Netty 监听后,Java 端为服务器端,每个电表为客户端,Netty 中的每个 channel 是与每个设备通信后的渠道产物。
(四)模式选择
本文选择 tcp 透传方式,也可选择其他方式如 MQTT等。
(五)数据获取方式
- 当客户端与服务端建立连接后,客户端会向服务端发送一条消息,内容为 SN (设备唯一识别码),以告知服务端 “我是谁”,服务端便知道需给哪个设备发消息以及这是哪个设备的响应。
- 数据编码:需注意客户端设置的是 HEX 还是 ASCII。DLT645 协议响应的数据可参考:FE FE FE FE 68 62 01 76 00 00 81 68 11 04 33 33 34 33 0C 16。每一帧数据都有具体含义,因篇幅所限不详细列举。
- 厂家一般会提供 PDF/WORD 版本的协议文件,也会有相应的对接人员。
- 调试:有两种办法。第一种是将电表设备放到 PC 旁边,接一堆线(如 220V 以及低压线),相当复杂且危险,调试效率低。第二种是将写好的程序部署到支持外网的机器上,把相应的 IP+Netty 监听的端口提供给客户,让他们配置进去,远程调试。
- 由于写好的代码需要部署至服务器,不能频繁发布频繁测试,效率太低。此时可下载 NetAssist.exe,用它来模拟客户端,本地服务启动后就是服务端,效率极高,先保证自测通过再部署至服务器,减少发布频次,提高效率。这是在快完成时才发现的,之前踩坑无数。
(六)上代码
6.1 netty 服务器(主要用于与客户端通讯)
package com.ape.service.netty;
import com.ape.module.netty.NettyServerHandlerEle;
import com.ape.module.netty.NettyServerHandlerWaterEmf;
import com.ape.module.netty.NettyServerHandlerWaterUw;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* netty服务器,主要用于与客户端通讯
*/
@Slf4j
public class NettyServer {
//监听端口-电表
private final int elePort;
//监听端口-水表-电磁流量计
private final int waterEmfPort;
//监听端口-水表-超声波流量计
private final int waterUwPort;
public NettyServer(int elePort, int waterEmfPort, int waterUwPort) {
this.elePort = elePort;
this.waterUwPort = waterUwPort;
this.waterEmfPort = waterEmfPort;
}
/**
* 编写run方法,处理客户端的请求
*
* @throws Exception
*/
public void run() throws Exception {
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
//8个NioEventLoop
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//电表
ServerBootstrap serverBootstrapEle = new ServerBootstrap();
//[水表]电磁流量计
ServerBootstrap serverBootstrapWaterEmf = new ServerBootstrap();
//[水表]超声波
ServerBootstrap serverBootstrapWaterUw = new ServerBootstrap();
serverBootstrapEle.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
//获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
//向pipeline加入心跳超时机制
pipeline.addLast(new IdleStateHandler(45, 0, 0, TimeUnit.SECONDS));
// //向pipeline加入解码器
pipeline.addLast("decoder", new StringDecoder());
// //向pipeline加入编码器
pipeline.addLast("encoder", new StringEncoder());
//加入自己的业务处理handler
pipeline.addLast(new NettyServerHandlerEle());
}
});
serverBootstrapWaterEmf.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
//向pipeline加入心跳超时机制
pipeline.addLast(new IdleStateHandler(45, 0, 0, TimeUnit.SECONDS));
// //向pipeline加入解码器
pipeline.addLast("decoder", new StringDecoder());
// //向pipeline加入编码器
pipeline.addLast("encoder", new StringEncoder());
//加入自己的业务处理handler
pipeline.addLast(new NettyServerHandlerWaterEmf());
}
});
serverBootstrapWaterUw.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
//向pipeline加入心跳超时机制
pipeline.addLast(new IdleStateHandler(45, 0, 0, TimeUnit.SECONDS));
// //向pipeline加入解码器
pipeline.addLast("decoder", new StringDecoder());
// //向pipeline加入编码器
pipeline.addLast("encoder", new StringEncoder());
//加入自己的业务处理handler
pipeline.addLast(new NettyServerHandlerWaterUw());
}
});
ChannelFuture channelFutureEle = serverBootstrapEle.bind(elePort).sync();
log.info("netty服务器启动成功-电表tcp端口:{}", elePort);
ChannelFuture channelFutureWaterEmf = serverBootstrapWaterEmf.bind(waterEmfPort).sync();
log.info("netty服务器启动成功-水表[电磁流量计]tcp端口:{}", waterEmfPort);
ChannelFuture channelFutureWaterUw = serverBootstrapWaterUw.bind(waterUwPort).sync();
log.info("netty服务器启动成功-水表[超声波流量计]tcp端口:{}", waterUwPort);
//监听关闭
channelFutureEle.channel().closeFuture().sync();
channelFutureWaterEmf.channel().closeFuture().sync();
channelFutureWaterUw.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
注意以下代码,如果是 HEX数据格式,则要把这两行注释掉。
// //向pipeline加入解码器
pipeline.addLast("decoder", new StringDecoder());
// //向pipeline加入编码器
pipeline.addLast("encoder", new StringEncoder());
-
6.2 启动 Netty
-
package com.ape; import com.ape.service.netty.NettyServer; import org.apache.tomcat.util.http.LegacyCookieProcessor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.MultipartConfigFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.stereotype.Indexed; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.util.unit.DataSize; import org.springframework.web.bind.annotation.RestController; import javax.servlet.MultipartConfigElement; import java.util.TimeZone; @Indexed @SpringBootApplication(scanBasePackages = {"com.ape"}) @MapperScan(basePackages = {"com.ape.mapper.*"}) @RestController @EnableScheduling @EnableAsync @EnableTransactionManagement @Configuration public class PowerApplication implements CommandLineRunner { @Value("${tcp.elePort}") private Integer elePort; @Value("${tcp.waterEmfPort}") private Integer waterEmfPort; @Value("${tcp.waterUwPort}") private Integer waterUwPort; public static void main(String[] args) { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); SpringApplication.run(PowerApplication.class); } @Override public void run(String... args) throws Exception { new Thread(() -> { try { new NettyServer(elePort, waterEmfPort, waterUwPort).run(); } catch (Exception e) { e.printStackTrace(); } }).start(); } }
-
6.3 NettyServer 控制器
6.3.1 类声明的泛型
如果是 HEX 格式,则类声明中的泛型为ByteBuffer
;如果是 ASCII 格式,则为String
。6.3.2 响应入口
channelRead0
为真正的响应入口,请注意阅读。6.3.3 注册包说明
以下类里多个地方出现 “注册包”字样,此为智能电表每次和 SERVER 端建立连接后发送的设备的 SN(可以理解为设备唯一识别码,只会发送一次哈,切记),用来告知程序这是哪个设备和你建立的连接。一个连接就是一个channel。每次发送以及响应时就知道是和谁在进行交互。 -
package com.ape.module.netty; import com.ape.common.utils.StringUtil; import com.ape.common.utils.StringUtils; import com.ape.pojo.dto.power.EleAcquisitionDTO; import com.ape.pojo.vo.power.EleAcquisitionVO; import com.ape.pojo.vo.power.EleEquipVO; import com.ape.service.power.EleAcquisitionService; import com.ape.service.power.EleEquipService; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.util.concurrent.GlobalEventExecutor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; /** * 电表 * * @author hwlva */ @Slf4j @Component public class NettyServerHandlerEle extends SimpleChannelInboundHandler<ByteBuffer> { /** * 定义本类的静态对象 */ private static NettyServerHandlerEle nettyServerHandlerEle; /** * 3. 添加 @PostConstruct 注解 自定义初始化方法 */ @PostConstruct public void init() { nettyServerHandlerEle = this; } /** * 定义一个channle 组,管理所有的channel * GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例 */ public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); /** * 使用一个Map来存储每个通道的变量,key为通道,value为 SN */ public static Map<Channel, String> variables = new HashMap<>(); /** * 客户端是否是第一次给服务器发送消息标识 */ private boolean firstMessageReceived = false; @Value("${ele.heartContent}") private String heartContent; @Value("${ele.idleData}") private String idleData; @Autowired private EleEquipService eleEquipService; @Autowired EleAcquisitionService eleAcquisitionService; /** * 有客户端与服务器发生连接时执行此方法 * 1.打印提示信息 * 2.将客户端保存到 channelGroup 中 */ @Override public void handlerAdded(ChannelHandlerContext ctx) { Channel channel = ctx.channel(); log.info("[电表]有新的客户端与服务器发生连接。客户端地址:{}", channel.remoteAddress().toString()); channelGroup.add(channel); } /** * 当有客户端与服务器断开连接时执行此方法,此时会自动将此客户端从 channelGroup 中移除 * 1.打印提示信息 */ @Override public void handlerRemoved(ChannelHandlerContext ctx) { Channel channel = ctx.channel(); log.error("[电表]有客户端与服务器断开连接。客户端地址:{},sn:{}", channel.remoteAddress().toString(), getVariable(channel)); doErrorHandle(ctx); removeVariable(channel); logMethod(); } /** * 超时处理,如果45秒没有收到客户端的心跳,就触发; 如果超过两次,则直接关闭; */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception { log.info("[电表] userEventTriggered.........."); if (obj instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent) obj; // 如果读通道处于空闲状态,说明没有接收到心跳命令 if (IdleState.READER_IDLE.equals(event.state())) { log.error("[电表]超过45秒未收到客户端消息,关闭连接,通道标识:{},sn:{}", ctx.channel().remoteAddress().toString(), getVariable(ctx.channel())); ctx.channel().close(); doErrorHandle(ctx); removeVariable(ctx.channel()); } } else { super.userEventTriggered(ctx, obj); } } /** * 表示channel 处于活动状态 */ @Override public void channelActive(ChannelHandlerContext ctx) { log.info("[电表]channelActive.........." + ctx.channel().remoteAddress()); } /** * 表示channel 处于不活动状态 */ @Override public void channelInactive(ChannelHandlerContext ctx) { log.info("[电表]channelInactive.........."); Channel channel = ctx.channel(); log.error(channel.remoteAddress() + " 处于不活动状态"); } /** * 读取到客户端发来的数据数据 */ @Override protected void channelRead0(ChannelHandlerContext ctx, String stringMsg) { log.info("---[电表] channelRead0 start...StringMsg:{},sn:{}---", stringMsg, getVariable(ctx.channel())); //第一次收到的消息为注册码即SN if (!firstMessageReceived) { // 这是第一次接收到的消息 log.info("[电表]第一次接收到的注册包: " + stringMsg); String sn = getVariable(ctx.channel()); if (StringUtils.isEmpty(sn)) { setVariable(ctx.channel(), stringMsg); } firstMessageReceived = true; } else if (stringMsg.equals(nettyServerHandlerEle.heartContent)) { log.info("[电表]消息类型为 [心跳],[{}]", stringMsg); } else { log.info("[电表]消息类型为 非 心跳,{}", stringMsg); //获取有功和无功的读数 BigDecimal res = StringUtil.analyzingConsumption(stringMsg); EleEquipVO eleEquipVO = nettyServerHandlerEle.eleEquipService.getBySn(getVariable(ctx.channel())); String equipName = eleEquipVO.getName(); String deviceNo = eleEquipVO.getDeviceNo(); //判断类型 String tempMsg = stringMsg.replaceAll(" ", ""); String sb = tempMsg.substring(28, 36); String idleOne = nettyServerHandlerEle.idleData.replaceAll(" ", ""); // String idleOne = "33333633"; EleAcquisitionDTO eleAcquisitionDTO = new EleAcquisitionDTO(); //根据设备id获去最大的值 EleAcquisitionVO acquisitionVO = nettyServerHandlerEle.eleAcquisitionService.getMaxDataByDeviceNo(deviceNo); if (idleOne.equals(sb)) { //获取最新的一条采集数据 EleAcquisitionVO eleAcquisitionVO = nettyServerHandlerEle.eleAcquisitionService.selectByDeviceNoOrNowTime(deviceNo); //判断有没有采集的数据 有采集的数据 则修改最新的一条数据的无功读数 if (eleAcquisitionVO != null) { eleAcquisitionDTO.setId(eleAcquisitionVO.getId()); eleAcquisitionDTO.setFieldValue(eleAcquisitionVO.getFieldValue()); eleAcquisitionDTO.setActualValue(eleAcquisitionVO.getActualValue()); }else { //没有则新增,把有功读数和有功实际读数的最大值给塞进去进行新增 eleAcquisitionDTO.setFieldValue(acquisitionVO.getFieldValue()); eleAcquisitionDTO.setActualValue(acquisitionVO.getActualValue()); } eleAcquisitionDTO.setIdleFieldValue(res); //计算倍数 BigDecimal counts = countMultiple(eleEquipVO, res); eleAcquisitionDTO.setIdleActualValue(counts); } else { eleAcquisitionDTO.setFieldValue(res); //计算倍数 BigDecimal counts = countMultiple(eleEquipVO, res); eleAcquisitionDTO.setActualValue(counts); eleAcquisitionDTO.setIdleFieldValue(acquisitionVO.getIdleFieldValue()); eleAcquisitionDTO.setIdleActualValue(acquisitionVO.getIdleActualValue()); } eleAcquisitionDTO.setDeviceNo(deviceNo); log.info("[电表]消息类型为 非 心跳,内容是:{},设备名称为:{},读取值为:{}kwh", stringMsg, equipName, res); eleAcquisitionDTO.setLocation(equipName); //保存电数据采集并且触发daily生成 log.info("------------deviceno:{}", deviceNo); nettyServerHandlerEle.eleAcquisitionService.createAcqAndDaily(eleAcquisitionDTO); } logMethod(); log.info("-------------------[电表] channelRead0 end.........."); } private static BigDecimal countMultiple(EleEquipVO eleEquipVO, BigDecimal res) { Integer multiple = eleEquipVO.getMultiple(); //乘以倍数 if (multiple != null && multiple > 0) { // 将double转换为BigDecimal BigDecimal numberAsBigDecimal = BigDecimal.valueOf(multiple); BigDecimal actual = numberAsBigDecimal.multiply(res); return actual; } else { return res; } } /** * 处理异常 */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { log.error("[电表]发生异常。异常信息:{},sn:{}", cause.getMessage() + cause.getCause() + ctx.channel().remoteAddress().toString(), getVariable(ctx.channel())); doErrorHandle(ctx); //关闭通道 ctx.close(); removeVariable(ctx.channel()); } private void doErrorHandle(ChannelHandlerContext ctx) { log.error("[电表]doErrorHandle。异常信息:{},sn:{}", ctx.channel().remoteAddress(), getVariable(ctx.channel())); } // 存储变量的方法 public static void setVariable(Channel ctx, String variable) { variables.put(ctx, variable); } // 获取变量的方法 public static String getVariable(Channel ctx) { return variables.get(ctx); } /** * 删除ctx * * @param ctx */ public static void removeVariable(Channel ctx) { if (StringUtils.isNotEmpty(variables.get(ctx))) { variables.remove(ctx); } } public static void logMethod() { log.info("[电表]客户端连接总数量为:{}", channelGroup.size()); log.info("[电表]ctx和SN关系map长度为,{}", variables.size()); variables.forEach((key, value) -> log.info("[电表]" + key.remoteAddress() + ":" + value)); } }
-
6.4 向客户端发送指令以达到采集的目的
发送的入口方法为 sendMessage。package com.ape.service.power.impl; import cn.hutool.core.bean.BeanUtil; import com.ape.common.base.LocalUser; import com.ape.common.base.Response; import com.ape.common.cache.ParamsCacheUtil; import com.ape.common.compont.AsyncDayInfoDailyComponent; import com.ape.common.constant.SysConstants; import com.ape.common.enums.AlarmCodeEnum; import com.ape.common.enums.AlarmLevelEnum; import com.ape.common.enums.AlarmTypeEnum; import com.ape.common.enums.EquipStatusEnum; import com.ape.common.utils.*; import com.ape.mapper.power.EleAcquisitionMapper; import com.ape.module.netty.NettyServerHandlerEle; import com.ape.pojo.dto.power.EleAcquisitionDTO; import com.ape.pojo.entity.power.EleAcquisitionEntity; import com.ape.pojo.entity.power.EleAlarmRecordEntity; import com.ape.pojo.query.power.EleAcquisitionQuery; import com.ape.pojo.vo.power.EleAcquisitionVO; import com.ape.pojo.vo.power.EleEquipVO; import com.ape.pojo.vo.power.EleWarningStrategyVO; import com.ape.service.power.*; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** * 电数据采集Service业务层处理 * * @author yrw * @date 2024-04-15 */ @Slf4j @Service public class EleAcquisitionServiceImpl extends ServiceImpl<EleAcquisitionMapper, EleAcquisitionEntity> implements EleAcquisitionService { @Autowired private EleEquipService eleEquipService; @Autowired private AsyncDayInfoDailyComponent dayInfoDailyComponent; @Autowired private EleSystemDailyService eleSystemDailyService; @Autowired private EleAlarmRecordService eleAlarmRecordService; @Autowired private EleWarningStrategyService eleWarningStrategyService; @Value("${ele.prefix}") private String prefix; @Value("${ele.start}") private String start; @Value("${ele.control}") private String control; @Value("${ele.dataLength}") private String dataLength; @Value("${ele.data}") private String data; @Value("${ele.end}") private String end; @Value("${ele.idleData}") private String idleData; @Override @Transactional(rollbackFor = Exception.class) public Response create(EleAcquisitionDTO dto) { EleAcquisitionEntity entity = new EleAcquisitionEntity(); BeanUtil.copyProperties(dto, entity); try { if (ObjectUtils.isEmpty(dto.getId())) { this.baseMapper.insert(entity); } else { this.baseMapper.updateById(entity); } } catch (Exception e) { log.error("保存出错", e); return Response.fromException(e); } return Response.success(); } @Override public IPage<EleAcquisitionVO> search(EleAcquisitionQuery query) { Page<EleAcquisitionVO> pager = new Page<>(query.getCurrentPage(), query.getPageSize()); return this.baseMapper.search(pager, query); } @Override public Response<EleAcquisitionVO> findById(String id) { return Response.success(this.baseMapper.findById(id)); } @Override public Response deleteById(String id) { String userId = LocalUser.getLocalUser().getUserId(); this.baseMapper.delById(id, userId); return Response.success(); } //------ 设备相关接口 start ----------------- //根据设备编号修改名称 @Override public void updateByDeviceNO(String deviceNo, String name) { this.baseMapper.updateByDeviceNo(deviceNo, name); } //根据设备编号获取采集数据 @Override public List<EleAcquisitionVO> selectByDeviceNo(String deviceNo) { return this.baseMapper.selectByDeviceNo(deviceNo); } //根据设备编号删除采集数据 @Override @Transactional(rollbackFor = Exception.class) public void deleteByDeviceNo(String deviceNo) { if (StringUtils.isBlank(deviceNo)) { new Exception(); } this.baseMapper.deleteByDeviceNo(deviceNo); } //------ 设备相关接口 end ----------------- //------ 定时任务相关接口 start ----------------- //获取电表有功读数 @Override @Transactional(rollbackFor = Exception.class) public void acquisitionEleData() { //[电表]开始批量发送消息到电表客户端 sendMessage(); //更新电表状态(在线/离线) batchHandleEquipStatus(); } // 批量发送消息到电表获取无功读数一 @Override public void acquisitionEleIdleOneData() { //[电表]开始批量发送消息到电表客户端 sendIdleMessage(idleData); //更新电表状态(在线/离线) batchHandleEquipStatus(); } //根据设备编号获取最大的有功和无功 @Override public EleAcquisitionVO getMaxDataByDeviceNo(String deviceNo) { return this.baseMapper.getMaxDataByDeviceNo(deviceNo); } //根据日期和设备编号获取最新一条采集信息 @Override public EleAcquisitionVO selectByDeviceNoOrNowTime(String deviceNo) { return this.baseMapper.selectByDeviceNoOrNowTime(deviceNo); } //保存电数据采集并且触发daily生成 @Override public void createAcqAndDaily(EleAcquisitionDTO dto) { //保存电数据采集 this.create(dto); //触发daily生成 eleSystemDailyService.saveByDeviceNo(dto.getDeviceNo()); } //删除采集表中3个月前的数据 @Override public void delAcqInfoMonth() { String month = ParamsCacheUtil.queryValue("del_acq_info_month", "del_acq_info_month"); LocalDateTime time = LocalDateTime.now().minusMonths(Long.parseLong(month)); this.baseMapper.deleteByTime(time.toString()); } //定时任务已停用 //根据日期制造虚假电采集数据 @Override public Response setEleVirtualData(String date) { try { //判断传入的日期是否有值 没有则获取当前日期 String dateTime = StringUtils.isBlank(date) ? DateUtil.getDatestr(new Date(), DateUtil.FORMAT_DATE) : date; // 要插入的数据列表 List<EleAcquisitionEntity> addList = new ArrayList<>(); // 1、查出现有电表 列表 List<EleEquipVO> equipVOList = eleEquipService.getLists(); // 2、循环电表列表 制造 每个电表 指定日期(当日)的 24条数据 equipVOList.forEach(eleEquipVO -> { // 查询当前电表的最终读数 BigDecimal maxValueByDeviceNo = this.baseMapper.findByDeviceNo(eleEquipVO.getDeviceNo()); if (maxValueByDeviceNo == null) { maxValueByDeviceNo = new BigDecimal("0.000"); } // 获取 24个上报时间点 List<String> todayByHour = DateUtil.getHoursInMonth(dateTime); // 第一个随机读数使用表最终读数生成 BigDecimal bigDecimalRandom = RandomUtils.getBigDecimalRandom(maxValueByDeviceNo.add(new BigDecimal(SysConstants.ELE_VIRTUAL_DATA_RANDOM_START)), maxValueByDeviceNo.add(new BigDecimal(SysConstants.ELE_VIRTUAL_DATA_RANDOM_END))); for (String time : todayByHour) { // 获取24个上报时间点(00:00:00-23:00:00),每隔一小时一条 addList.add(new EleAcquisitionEntity(eleEquipVO, time, bigDecimalRandom)); // 剩余随机读数生成读数随机数 bigDecimalRandom = RandomUtils.getBigDecimalRandom(bigDecimalRandom.add(new BigDecimal(SysConstants.ELE_VIRTUAL_DATA_RANDOM_START)), bigDecimalRandom.add(new BigDecimal(SysConstants.ELE_VIRTUAL_DATA_RANDOM_END))); } }); //生成之前先删除所生成当天的数据 this.baseMapper.deleteByReportedAt(dateTime); // 3、批量出入数据 this.baseMapper.batchInsert(addList); return Response.success(); } catch (Exception e) { log.error("用电定时任务批处理失败", e); return Response.fromException(e); } } //------ 定时任务相关接口 end ----------------- //------ 定时任务相关方法 start ----------------- /** * [电表]开始批量发送消息到电表客户端 获取有功读数 */ private void sendMessage() { Integer size = NettyServerHandlerEle.channelGroup.stream().filter(e -> e.isActive()).collect(Collectors.toList()).size(); if (size <= 0) { log.info("[电表]没有连接的客户端(激活的channel)"); return; } log.info("[电表]开始批量发送消息到电表客户端(激活的channel),{}个", size); AtomicInteger count = new AtomicInteger(); NettyServerHandlerEle.channelGroup.forEach(channel -> { if (channel.isActive()) { String sn = NettyServerHandlerEle.variables.get(channel); if (StringUtil.isNotEmpty(sn)) { EleEquipVO eleEquipVO = eleEquipService.getBySn(sn); if (eleEquipVO != null && StringUtil.isNotEmpty(eleEquipVO.getDeviceNo())) { String command = assembleCommand(eleEquipVO.getDeviceNo(), data); command = StringUtil.replaceAllStr(command); channel.writeAndFlush(command); count.getAndIncrement(); log.info("[电表]开始给设备[{}]发送指令,指令为:{}", eleEquipVO.getName(), command); } else { log.info("[电表]没有SN={}的设备", sn); } } else { log.info("[电表]{}未发送注册码,sn为空", channel.remoteAddress()); } } }); log.info("[电表]总共给[{}]个设备发送了命令", count); } /** * [电表]开始批量发送消息到电表客户端 获取无功读数 * @param sendMessage */ private void sendIdleMessage(String sendMessage) { Integer size = NettyServerHandlerEle.channelGroup.stream().filter(e -> e.isActive()).collect(Collectors.toList()).size(); if (size <= 0) { log.info("[电表]没有连接的客户端(激活的channel)"); return; } log.info("[电表]开始批量发送消息到电表客户端(激活的channel),{}个", size); AtomicInteger count = new AtomicInteger(); NettyServerHandlerEle.channelGroup.forEach(channel -> { if (channel.isActive()) { String sn = NettyServerHandlerEle.variables.get(channel); if (StringUtil.isNotEmpty(sn)) { EleEquipVO eleEquipVO = eleEquipService.getBySn(sn); if (eleEquipVO != null && StringUtil.isNotEmpty(eleEquipVO.getDeviceNo())) { String command = assembleCommand(eleEquipVO.getDeviceNo(), sendMessage); command = StringUtil.replaceAllStr(command); channel.writeAndFlush(command); count.getAndIncrement(); log.info("[电表]开始给设备[{}]发送无功指令,指令为:{}", eleEquipVO.getName(), command); } else { log.info("[电表]没有SN={}的设备", sn); } } else { log.info("[电表]{}未发送注册码,sn为空", channel.remoteAddress()); } } }); log.info("[电表]总共给[{}]个设备发送了命令", count); } /** * 组装发送的命令 * @return */ private String assembleCommand(String deviceNo, String dataDomain) { //计算校验码的值base String base = String.format("%s%s%s%s%s%s", start, assembleAddress(deviceNo), start, control, dataLength, StringUtil.replaceAllStr(dataDomain)); String verifyCode = StringUtil.makeCheck(base); String result = String.format("%s %s %s %s", prefix, base, verifyCode, end); return result; } /** * 12位地址位的 两位倒序 * @return */ private String assembleAddress(String deviceNo) { return StringUtil.addressToString(StringUtil.stringToBytes((deviceNo))); } /** * 给设备发送消息+生成告警记录 */ private void batchHandleEquipStatus() { //更新设备状态(在线or离线),如果离线则触发 // batchUpdateEquipStatus(); //生成告警记录 batchCreateAlarmRecord(); } /** * 生成告警记录(离线) */ private void batchCreateAlarmRecord() { //先从缓存中获取所有的设备信息 List<EleEquipVO> equipList = eleEquipService.getLists(); //获取告警策略里面设备离线的策略信息 EleWarningStrategyVO strategyVO = eleWarningStrategyService.getByCode(AlarmCodeEnum.OFFLINE_CODE.getCode()); if (strategyVO == null) { return; } //离线次数 Integer offLineNum = strategyVO.getOffLineNum(); //获取tcp里面存在的设备sn Collection<String> snList = NettyServerHandlerEle.variables.values(); //告警记录 List<EleAlarmRecordEntity> offLineList = Lists.newArrayList(); //设备离线和在线的ids List List<String> offLineByIds = Lists.newArrayList(); List<String> onLineByIds = Lists.newArrayList(); //设备离线次数的ids List<String> offLineIds = Lists.newArrayList(); // 判断如果tcp里面有没有设备 if (snList.isEmpty()) { equipList.stream().forEach(e -> { //判断如果设备离线的次数大于预计离线的次数,则提示告警 if (offLineNum < e.getOffLineNum()) { //添加告警记录信息 offLineList.add(getEleAlarmRecordEntity(e)); offLineByIds.add(e.getId()); }else { onLineByIds.add(e.getId()); } offLineIds.add(e.getId()); }); } else { equipList.stream().forEach(e -> { if (!snList.contains(e.getSn())) { if (offLineNum < e.getOffLineNum()) { offLineList.add(getEleAlarmRecordEntity(e)); offLineByIds.add(e.getId()); }else { onLineByIds.add(e.getId()); } offLineIds.add(e.getId()); } else { onLineByIds.add(e.getId()); } }); } //修改离线次数 eleEquipService.updateByIds(offLineIds); //修改设备状态 eleEquipService.batchUpdateStatusByIds(onLineByIds, EquipStatusEnum.ONLINE.getCode()); eleEquipService.batchUpdateStatusByIds(offLineByIds, EquipStatusEnum.OFFLINE.getCode()); //如果没有要添加的告警记录 ,则不需要删除和添加 if (!offLineList.isEmpty()) { List<String> recordDeviceNos = offLineList.stream().map(EleAlarmRecordEntity::getDeviceNo).collect(Collectors.toList()); eleAlarmRecordService.deleteByDeviceNos(recordDeviceNos); eleAlarmRecordService.saveBatch(offLineList); } } /** * 构造getEleAlarmRecordEntity对象 * * @param eleEquipVO * @return */ private EleAlarmRecordEntity getEleAlarmRecordEntity(EleEquipVO eleEquipVO) { EleAlarmRecordEntity entity = new EleAlarmRecordEntity(); entity.setId(SysUtil.getUuid()); entity.setDeviceNo(eleEquipVO.getDeviceNo()); entity.setType(AlarmTypeEnum.TYPE_ONE.getCode()); String title = String.format("设备离线[%s]", eleEquipVO.getName()); entity.setTitle(title); entity.setLevel(AlarmLevelEnum.LEVEL_ONE.getCode()); String content = String.format("设备[%s]断开连接", eleEquipVO.getName()); entity.setContent(content); entity.setUpdateTime(LocalDateTime.now()); return entity; } //------ 定时任务相关方法 end ----------------- //------ 用电记录相关接口 start ----------------- //根据设备编号查询 初始,终止,当前耗电 的有功和无功读数 @Override @Transactional(rollbackFor = Exception.class) public EleAcquisitionVO getByDeviceNo(String deviceNo) { return this.baseMapper.getByDeviceNo(deviceNo); } //获取昨日最小的有功读数,如果昨日没有数据,则获取小于昨日的最大有功读数 @Override public BigDecimal getYesterdayOrDeviceNo(String deviceNo, String yesterday) { BigDecimal counts = this.baseMapper.getYesterdayOrDeviceNo(deviceNo, yesterday); if (counts == null) { BigDecimal maxCounts = this.baseMapper.getUnderYesterdayOrDeviceNo(deviceNo, yesterday); return maxCounts; } return counts; } //获取昨日最小的无功读数,如果昨日没有数据,则获取小于昨日的最大无功读数 @Override public BigDecimal getYesterdayOrDeviceNoIdle(String deviceNo, String yesterday) { BigDecimal counts = this.baseMapper.getYesterdayOrDeviceNoIdle(deviceNo, yesterday); if (counts == null) { BigDecimal maxCounts = this.baseMapper.getUnderYesterdayOrDeviceNoIdle(deviceNo, yesterday); return maxCounts; } return counts; } //获取当天有功的最大 最小 和 耗电量 已停用 @Override @Transactional(rollbackFor = Error.class) public List<EleAcquisitionVO> getYesterdayInfo(String day) { return this.baseMapper.getYesterdayInfo(day); } //------ 用电记录相关接口 end ----------------- //批量发送报文 @Override public Response sendMsg(String sendMsg, String sns) { AtomicInteger count = new AtomicInteger(); NettyServerHandlerEle.channelGroup.forEach(channel -> { if (channel.isActive()) { String sn = NettyServerHandlerEle.variables.get(channel); if (StringUtil.isNotEmpty(sn)) { EleEquipVO eleEquipVO = eleEquipService.getBySn(sn); if (eleEquipVO != null && StringUtil.isNotEmpty(eleEquipVO.getDeviceNo())) { String command = sendMsg; command = StringUtil.replaceAllStr(command); channel.writeAndFlush(command); count.getAndIncrement(); log.info("[电表]开始给设备[{}]发送指令,指令为:{}", eleEquipVO.getName(), command); } else { log.info("[电表]没有SN={}的设备", sn); } } else { log.info("[电表]{}未发送注册码,sn为空", channel.remoteAddress()); } } }); log.info("[电表]总共给[{}]个设备发送了命令", count); return Response.success(); } //批量添加初始采集表数据 @Override public Response batSave(String deviceNos) { if (StringUtils.isBlank(deviceNos)) { return new Response(4000001, "设备编号不能为空"); } List<EleAcquisitionEntity> entities = new ArrayList<>(); String[] deviceNoList = deviceNos.split(","); List<String> list = Arrays.asList(deviceNoList); list.stream().forEach(e -> { EleAcquisitionEntity entity = new EleAcquisitionEntity(); EleEquipVO vo = eleEquipService.getByDeviceNo(e); entity.setDeviceNo(e); entity.setLocation(vo.getName()); entity.setFieldValue(new BigDecimal(0)); entity.setActualValue(new BigDecimal(0)); String time = DateUtil.minCurrDay(DateUtil.getNowByFormat(DateUtil.FORMAT_DATE)); entity.setReportedAt(LocalDateTime.parse(time, DateTimeFormatter.ofPattern(DateUtil.FORMAT_DATE_TIME))); entities.add(entity); }); this.saveBatch(entities); return Response.success(); } //批量添加采集数据 @Override public void saveDayInfoForStartOrEnd(String startDay, String endDay) { try { List<String> days = DateUtil.getDaysForStartOrEnd(startDay, endDay); List<List<String>> partition = Lists.partition(days, 5); CountDownLatch countDownLatch = new CountDownLatch(partition.size()); partition.stream().forEach(e -> dayInfoDailyComponent.saveDayInfoAcquisition(e, countDownLatch)); countDownLatch.await(); } catch (Exception e) { e.printStackTrace(); } } }
-
(七)写在最后
以上为核心代码块,最重要的接受和发送代码块集中的6.3和6.4里,如有需要仔细阅读,核心代码已贴出,如果有错误的地方还请指正。