通过NettySocket向硬件发送控制指令,并接收数据解析

准备工作

1,搭建netty服务端

@Slf4j
@Component
public class NettyServer {
	
	@Autowired
	private NettyServerProperties nettyServerProperties;
	//编写run方法,处理客户端的请求
	public void run() throws Exception {
		//创建两个线程组
		EventLoopGroup bossGroup = new NioEventLoopGroup(1);
		EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop
		
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.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("decoder", new StringDecoder());
							//向pipeline加入编码器,可以自己定义
							//pipeline.addLast("encoder", new MyEncoder());
							pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
							//加入自己的业务处理handler
							pipeline.addLast(new NettyServerHandler());
							
						}
					});
			log.info("netty服务器启动成功(port:" + nettyServerProperties.getPort() + ")......");
			ChannelFuture channelFuture = b.bind(nettyServerProperties.getPort()).sync();
			//监听关闭
			channelFuture.channel().closeFuture().sync();
		} finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
		
	}

2,NettyServerProperties 配置

/**
 * 读取YML中的服务配置
 *
 */
@Configuration
@ConfigurationProperties(prefix = NettyServerProperties.PREFIX)
@Data
public class NettyServerProperties {

    public static final String PREFIX = "netty.server";

    /**
     * 服务器端口
     */
    private Integer port;

}

3,pom配置

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
        </dependency>
        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>1.7.7</version>
        </dependency>

4,yml配置

#Netty暴露端口
netty.server.port=9021

5,编解码处理程序可以自己定义,也可以用原生的(根据自己需要)(此段出处找不到了,对原作者说声抱歉)

/**
 * @Author: LQY
 * @CreateTime: 2022-09-14  0e:53
 * @Description: 编码器
 */
@Component
public class MyEncoder extends MessageToByteEncoder<String> {
	@Override
	protected void encode(ChannelHandlerContext channelHandlerContext, String s, ByteBuf byteBuf) throws Exception {
		//将16进制字符串转为数组
		byteBuf.writeBytes(hexString2Bytes(s));
	}
	
	/**
	 * 功能描述: 16进制字符串转字节数组
	 *
	 * @param src 16进制字符串
	 * @return byte[]
	 * @Author keLe
	 * @Date 2022/8/26
	 */
	public static byte[] hexString2Bytes(String src) {
		int l = src.length() / 2;
		byte[] ret = new byte[l];
		for (int i = 0; i < l; i++) {
			ret[i] = (byte) Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue();
		}
		return ret;
	}
	
	
	public static String convertStringToHex(String str) {
		
		char[] chars = str.toCharArray();
		
		StringBuffer hex = new StringBuffer();
		
		for (int i = 0; i < chars.length; i++) {
			
			hex.append(Integer.toHexString((int) chars[i]));
			
		}
		
		return hex.toString();
		
	}
	
	//16进制转换10进制
	public static int hex16To10(String strHex) {
		BigInteger lngNum = new BigInteger(strHex, 16);
		return lngNum.intValue();
	}
}

处理程序

1,编写服务端处理程序

@Slf4j
public class NettyServerHandler extends SimpleChannelInboundHandler<String> {
	private static final NettyChannelMng NETTY_CHANNEL_MNG;
	private static final ServiceProperties PROPERTIES;
	
	static {
		PROPERTIES = SpringUtil.getBean("serviceProperties");
		NETTY_CHANNEL_MNG = SpringUtil.getBean("nettyChannelMng");
	}
	
	/**
	 * 有客户端与服务器发生连接时执行此方法
	 * 1.打印提示信息
	 */
	@Override
	public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
		Channel channel = ctx.channel();
		log.info("有新的客户端与服务器发生连接。客户端地址:" + channel.remoteAddress());
	}
	
	/**
	 * 当有客户端与服务器断开连接时执行此方法,此时会自动将此客户端从 channelGroup 中移除
	 * 1.打印提示信息
	 */
	@Override
	public void handlerRemoved(ChannelHandlerContext ctx) {
		Channel channel = ctx.channel();
		log.info("有客户端与服务器断开连接。客户端地址:" + channel.remoteAddress());
		//设备断联,停止定时任务
//		ExperimentRemoveScheduledTaskCmd cmd = new ExperimentRemoveScheduledTaskCmd();
//		cmd.setJobName(linkedHashMap.get(channel.remoteAddress().toString()).toString());
//		cmd.setGroupName(QuartzGroupEnum.EXPERIMENT.getValue());
//		ExperimentScheduledTaskService experimentScheduledTaskService = SpringUtil.getBean("experimentScheduledTaskService");
//		experimentScheduledTaskService.removeExperimentScheduledTask(cmd);
		
		//更新数表中的数据状态
		HashMap<String, Object> param = new HashMap<>();
		param.put("id", NETTY_CHANNEL_MNG.getDeviceId(channel));
		param.put("deviceRoute", StringUtils.EMPTY);
		param.put("deviceStatus", "0");
		String postResult = ExperimentHttpUtil.postMap(PROPERTIES.getUpdateExperimentDeviceSchool(), param);
		log.info("结果:{}", postResult);
	}
	
	/**
	 * 表示channel 处于活动状态
	 */
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		log.info(ctx.channel().remoteAddress() + " 处于活动状态");
	}
	
	/**
	 * 表示channel 处于不活动状态
	 */
	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		Channel channel = ctx.channel();
		log.info(channel.remoteAddress() + " 处于不活动状态,即将断开连接。");

		HashMap<String, Object> param = new HashMap<>();
		param.put("id", NETTY_CHANNEL_MNG.getDeviceId(channel));
		param.put("deviceRoute", StringUtils.EMPTY);
		param.put("deviceStatus", "0");
		String postResult = ExperimentHttpUtil.postMap(PROPERTIES.getUpdateExperimentDeviceSchool(), param);
		log.info("结果:{}", postResult);
	}
	
	private void regConversation(Channel channel, String msg) {
		if (msg.length() != 10) {
			return;
		}
		// 主动建立连接,消息长度是10
		HashMap<String, Object> paramMap = new HashMap<>();
		paramMap.put("deviceMark", msg);
		String result = ExperimentHttpUtil.postMap(PROPERTIES.getGetExperimentDeviceSchool(), paramMap);
		if (ObjectUtil.isEmpty(result)) {
			return;
		}
		
		JSONObject jsonObject = new JSONObject(result);
		String deviceId = jsonObject.getByPath("$.data.id", String.class);
		if(StringUtils.isEmpty(deviceId)) {
			log.info("设备连接失败,接收到deviceMark:{}, 取到的deviceId:{}", msg, deviceId);
			return;
		}
		
		//数据填入到linkedHashMap
		NETTY_CHANNEL_MNG.addActiveDeviceChannel(deviceId, channel);
		
		HashMap<String,Object> connectParam=new HashMap<>();
		connectParam.put("deviceId",deviceId);
		
		//添加连接日志
		String s = ExperimentHttpUtil.postMap(PROPERTIES.getAddDeviceConnectLog(), connectParam);
		
		HashMap<String, Object> param = new HashMap<>();
		param.put("id", deviceId);
		param.put("deviceRoute", channel.remoteAddress().toString());
		param.put("deviceStatus", "1");
		
		
		//更新数据表状态
		String postResult = ExperimentHttpUtil.postMap(PROPERTIES.getUpdateExperimentDeviceSchool(), param);
		log.info("更新数据表结果:{}", postResult);
		
		//查询当前设备所绑定的实验是否还在继续
		HashMap<String, Object> exParamMap = new HashMap<>();
		exParamMap.put("deviceId", NETTY_CHANNEL_MNG.getDeviceId(channel));
		
		String exStatus = ExperimentHttpUtil.postMap(PROPERTIES.getGetExperimentTask(), exParamMap);
		JSONObject exObject = new JSONObject(exStatus);
		if (!Optional.ofNullable(exObject.getByPath("$.success", Boolean.class)).orElse(false)) {
			log.info("实验已经结束");
			return;
		}
		
		log.info("实验在继续中,定时任务修改连接参数");
		//在继续就获取当前定时一个定时采集任务
		JobDataMap map = new JobDataMap();
		//map.put("address", channel.remoteAddress().toString());
		map.put("deviceId", NETTY_CHANNEL_MNG.getDeviceId(channel));
		map.put("deviceNumber", msg);
		ExperimentScheduledTaskAddCmd cmd = new ExperimentScheduledTaskAddCmd();
		cmd.setJobName(NETTY_CHANNEL_MNG.getDeviceId(channel));
		cmd.setGroupName(QuartzGroupEnum.EXPERIMENT.getValue());
		cmd.setJobClass(CollectDataJob.class);
		//cmd.setCron(FrequencyEnum.FIVE_MINUTE.getCron());
		cmd.setCron(FrequencyEnum.TEN_SECOND.getCron());
		cmd.setJobDataMap(map);
		log.info("开始采集");
		ExperimentScheduledTaskAddCmdExe experimentScheduledTaskAddCmdExe = SpringUtil.getBean("experimentScheduledTaskAddCmdExe");
		experimentScheduledTaskAddCmdExe.execute(cmd);
	}
	
	private void commonConversation(Channel channel, String msg) {
		if (msg.length() == 10) {
			return;
		}
		// 发控制或采集指令,返回数据
		// LQY TODO 2022/9/27 21:17 =>查询实验是否结束,结束则停止收集
		HashMap<String, Object> exParamMap = new HashMap<>();
		exParamMap.put("deviceId", NETTY_CHANNEL_MNG.getDeviceId(channel));
		String exStatus = ExperimentHttpUtil.postMap(PROPERTIES.getGetExperimentTask(), exParamMap);
		JSONObject exObject = new JSONObject(exStatus);
		if (!Optional.ofNullable(exObject.getByPath("$.success", Boolean.class)).orElse(false)) {
			ExperimentRemoveScheduledTaskCmd cmd = new ExperimentRemoveScheduledTaskCmd();
			cmd.setJobName(NETTY_CHANNEL_MNG.getDeviceId(channel));
			cmd.setGroupName(QuartzGroupEnum.EXPERIMENT.getValue());
			ExperimentScheduledTaskService experimentScheduledTaskService = SpringUtil.getBean("experimentScheduledTaskService");
			//移除定时任务
			experimentScheduledTaskService.removeExperimentScheduledTask(cmd);
		} else {
			//实验未停止,继续传输采集数据|发送控制指令
			DataAnalysis dataAnalysis = new DataAnalysis();
			Map<String, String> dataMap = dataAnalysis.dataAnalysis(channel.remoteAddress(), msg);
			if (ObjectUtil.isEmpty(dataMap)) {
				log.info("控制指令------------------");
				return;
			}
			//上传数据
			//数据转为Map集合
			HashMap<String, Object> param = new HashMap<>();
			param.put("deviceId", NETTY_CHANNEL_MNG.getDeviceId(channel));
			param.put("startTime", LocalDateTime.now());
			param.put("dataTypeSAndDataValue", dataMap);
			log.info("传输的数据{}", new JSONObject(param).toJSONString(0));
			
			String postResult = ExperimentHttpUtil.postMap(PROPERTIES.getAddExperimentCollectTask(), param);
			
			log.info("数据传输结果:{}", postResult);
		}
	}
	
	/**
	 * 读取到客户端发来的数据数据
	 */
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, String msg) throws IOException {
		//获取到当前channel
		Channel channel = ctx.channel();
		log.info("接收到客户端(硬件)发来的数据,地址为:" + channel.remoteAddress() + ",数据为:" + msg);
		//记录当前连接的设备数据
		this.regConversation(channel, msg);
		this.commonConversation(channel, msg);
	}
	
	/**
	 * 处理异常
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		log.error("发生异常。异常信息:{}", cause.getMessage());
		//关闭通道
		ctx.close();
	}
	
	/**
	 * 心跳机制
	 */
	 //(本项目所连接的硬件设备,会在断连后,自动重连,所以心跳机制可以不加)
	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//		log.info("心跳事件时触发");
//		if (evt instanceof IdleStateEvent) {
//			IdleStateEvent event = (IdleStateEvent) evt;
//			// 当我们长时间没有给服务器发消息时,发送ping消息,告诉服务器我们还活跃
//			if (event.state().equals(IdleState.WRITER_IDLE)) {
//				log.debug("发送心跳");
//				ctx.writeAndFlush("ping");
//			}
//		} else {
//			super.userEventTriggered(ctx, evt);
//		}
	}
}

注解:
NettyChannelMng (通过当前的通道,拿到当前硬件设备配置的数据<此项目中配置的是deviceId>)

@Component
@Slf4j
public class NettyChannelMng {

    //定义一个channle 组,管理所有的channel
    //GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例
    private ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    // 活动的设备id和channelId映射
    private Map<String, ChannelId> activeDeviceIdChannelIdMapping = new ConcurrentHashMap<>();

    private Map<ChannelId, String> channelIdDeviceMapping = new ConcurrentHashMap<>();

    synchronized public void addActiveDeviceChannel(String deviceId, Channel channel) {
        boolean added = this.channelGroup.add(channel);
        if(added) {
            this.activeDeviceIdChannelIdMapping.put(deviceId, channel.id());
            this.channelIdDeviceMapping.put(channel.id(), deviceId);
        }
    }

    synchronized public void unconnectDevice(String deviceId) {
        this.activeDeviceIdChannelIdMapping.remove(deviceId);
        Iterator<Map.Entry<ChannelId, String>> iterator = this.channelIdDeviceMapping.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<ChannelId, String> entry = iterator.next();
            if(Objects.equals(entry.getValue(), deviceId)) {
                iterator.remove();
            }
        }
     }

    synchronized public Channel getActiveDeviceChannel(String deviceId) {
        ChannelId channelId = this.activeDeviceIdChannelIdMapping.getOrDefault(deviceId, null);
        if(Objects.isNull(channelId)) {
            return null;
        }

        Channel channel = this.channelGroup.find(channelId);
        return channel;
    }

    synchronized public String getDeviceId(Channel channel) {
        String deviceId = this.channelIdDeviceMapping.getOrDefault(channel.id(), null);
        return deviceId;
    }

    public List<String> listAddress() {
        List<String> addresses = this.channelGroup.stream().map(e->e.remoteAddress().toString()).collect(Collectors.toList());
        return addresses;
    }
}

数据处理

1,commonConversation(Channel channel, String msg)
此接口处理客户端(硬件)推送过来的数据(第一次是连接发送设备的编号,长度为10,后续发送的则为控制指令结果,和数据采集结果),自定义定时任务不再解释,后续会贴出链接。

private void commonConversation(Channel channel, String msg) {
		if (msg.length() == 10) {
			return;
		}
		// 发控制或采集指令,返回数据
		// LQY TODO 2022/9/27 21:17 =>查询实验是否结束,结束则停止收集
		HashMap<String, Object> exParamMap = new HashMap<>();
		exParamMap.put("deviceId", NETTY_CHANNEL_MNG.getDeviceId(channel));
		String exStatus = ExperimentHttpUtil.postMap(PROPERTIES.getGetExperimentTask(), exParamMap);
		JSONObject exObject = new JSONObject(exStatus);
		if (!Optional.ofNullable(exObject.getByPath("$.success", Boolean.class)).orElse(false)) {
			ExperimentRemoveScheduledTaskCmd cmd = new ExperimentRemoveScheduledTaskCmd();
			cmd.setJobName(NETTY_CHANNEL_MNG.getDeviceId(channel));
			cmd.setGroupName(QuartzGroupEnum.EXPERIMENT.getValue());
			ExperimentScheduledTaskService experimentScheduledTaskService = SpringUtil.getBean("experimentScheduledTaskService");
			//移除定时任务
			experimentScheduledTaskService.removeExperimentScheduledTask(cmd);
		} else {
			//实验未停止,继续传输采集数据|发送控制指令
			DataAnalysis dataAnalysis = new DataAnalysis();
			Map<String, String> dataMap = dataAnalysis.dataAnalysis(channel.remoteAddress(), msg);
			if (ObjectUtil.isEmpty(dataMap)) {
				log.info("控制指令------------------");
				return;
			}
			//上传数据
			//数据转为Map集合
			HashMap<String, Object> param = new HashMap<>();
			param.put("deviceId", NETTY_CHANNEL_MNG.getDeviceId(channel));
			param.put("startTime", LocalDateTime.now());
			param.put("dataTypeSAndDataValue", dataMap);
			log.info("传输的数据{}", new JSONObject(param).toJSONString(0));
			
			String postResult = ExperimentHttpUtil.postMap(PROPERTIES.getAddExperimentCollectTask(), param);
			
			log.info("数据传输结果:{}", postResult);
		}
	}

2,数据分析(DataAnalysis)

**
 * @Author: LQY
 * @CreateTime: 2022-09-22  20:00
 * @Description: 数据解析
 */
@Slf4j
public class DataAnalysis {

	public Map<String, String> dataAnalysis(SocketAddress remoteAddress, String msg) {
		Map<String, String> dataMap = new HashMap<>();
		//表示客户端回传的数据为硬件采集/控制数据
		String ip = remoteAddress.toString();
		String crc = msg.substring(msg.length() - 4);
		//数据校验
		String newCRC = InstructionTransformation.getCRC16(msg.substring(0, msg.length() - 4));
		if(!StringUtils.equals(crc, newCRC)) {
			log.error("校验值不正确");
			MemoryCacheUtil.put(ip, false);
			return dataMap;
		}

		String deviceNum = msg.substring(0, 6);
		String code = msg.substring(6, 8);
		switch (code) {
			case "03"://流速传感器数据、土壤温湿度数据、角度传感器数据
				String mark = msg.substring(8, 10);
				switch (mark) {
					case "30":
						double flowMeter_1 = MyEncoder.hex16To10(msg.substring(10, 14)) / 10.0;//1号流量计
						double flowMeter_2 = MyEncoder.hex16To10(msg.substring(14, 18)) / 10.0;//2号流量计
						double flowMeter_3 = MyEncoder.hex16To10(msg.substring(18, 22)) / 10.0;//3号流量计
						double flowMeter_4 = MyEncoder.hex16To10(msg.substring(22, 26)) / 10.0;//4号流量计
						double totalFlow_1 = MyEncoder.hex16To10(msg.substring(26, 30));//1号流量计总流量
						double totalFlow_2 = MyEncoder.hex16To10(msg.substring(30, 34));//2号流量计总流量
						double totalFlow_3 = MyEncoder.hex16To10(msg.substring(34, 38));//3号流量计总流量
						double totalFlow_4 = MyEncoder.hex16To10(msg.substring(38, 42));//4号流量计总流量
						double tem_1 = MyEncoder.hex16To10(msg.substring(42, 46)) / 100.0;//1号温度
						double hum_1 = MyEncoder.hex16To10(msg.substring(46, 50)) / 100.0;//1号湿度
						double ec_1 = MyEncoder.hex16To10(msg.substring(50, 54));//1号盐度
						double tem_2 = MyEncoder.hex16To10(msg.substring(54, 58)) / 100.0;//2号温度
						double hum_2 = MyEncoder.hex16To10(msg.substring(58, 62)) / 100.0;//2号湿度
						double ec_2 = MyEncoder.hex16To10(msg.substring(62, 66));//2号盐度
						double tem_3 = MyEncoder.hex16To10(msg.substring(66, 70)) / 100.0;//3号温度
						double hum_3 = MyEncoder.hex16To10(msg.substring(70, 74)) / 100.0;//3号湿度
						double ec_3 = MyEncoder.hex16To10(msg.substring(74, 78));//3号盐度
						Integer proximitySwitch_0 = MyEncoder.hex16To10(msg.substring(78, 82));//0度传感器是否触发,1触发0未触发
						Integer proximitySwitch_2 = MyEncoder.hex16To10(msg.substring(82, 86));//2度传感器是否触发,1触发0未触发
						Integer proximitySwitch_4 = MyEncoder.hex16To10(msg.substring(86, 90));//4度传感器是否触发,1触发0未触发
						Integer proximitySwitch_6 = MyEncoder.hex16To10(msg.substring(90, 94));//6度传感器是否触发,1触发0未触发
						Integer proximitySwitch_8 = MyEncoder.hex16To10(msg.substring(94, 98));//8度传感器是否触发,1触发0未触发
						Integer proximitySwitch_10 = MyEncoder.hex16To10(msg.substring(98, 102));//10度传感器是否触发,1触发0未触发
						Integer bathHeater_1 = MyEncoder.hex16To10(msg.substring(105, 106));//1号浴霸状态,1开0关
						Integer bathHeater_2 = MyEncoder.hex16To10(msg.substring(104, 105));//2号浴霸状态,1开0关
						Integer bathHeater_3 = MyEncoder.hex16To10(msg.substring(103, 104));//3号浴霸状态,1开0关

						log.info("1号流量计:" + flowMeter_1 + ",2号流量计:" + flowMeter_2 + ",3号流量计:" + flowMeter_3 + ",4号流量计:" + flowMeter_4);
						log.info("1号流量计总流量:" + totalFlow_1 + ",2号流量计总流量:" + totalFlow_2 + ",3号流量计总流量:" + totalFlow_3 + ",4号流量计总流量:" + totalFlow_4);
						log.info("1号温度:" + tem_1 + ",1号湿度:" + hum_1 + ",1号盐度:" + ec_1);
						log.info("2号温度:" + tem_2 + ",2号湿度:" + hum_2 + ",2号盐度:" + ec_2);
						log.info("3号温度:" + tem_3 + ",3号湿度:" + hum_3 + ",3号盐度:" + ec_3);
						log.info("1号浴霸状态:" + bathHeater_1 + ",2号浴霸状态:" + bathHeater_2 + ",3号浴霸状态:" + bathHeater_3);
						log.info("0度传感器是否触发:" + proximitySwitch_0 + ",2度传感器是否触发:" + proximitySwitch_2 + ",4度传感器是否触发:" + proximitySwitch_4
								+ ",6度传感器是否触发:" + proximitySwitch_6 + ",8度传感器是否触发:" + proximitySwitch_8 + ",10度传感器是否触发:" + proximitySwitch_10);
						dataMap.put("flowMeter_1", String.valueOf(flowMeter_1));
						dataMap.put("flowMeter_2", String.valueOf(flowMeter_2));
						dataMap.put("flowMeter_3", String.valueOf(flowMeter_3));
						dataMap.put("flowMeter_4", String.valueOf(flowMeter_4));
						dataMap.put("totalFlow_1", String.valueOf(totalFlow_1));
						dataMap.put("totalFlow_2", String.valueOf(totalFlow_2));
						dataMap.put("totalFlow_3", String.valueOf(totalFlow_3));
						dataMap.put("totalFlow_4", String.valueOf(totalFlow_4));
						dataMap.put("tem_1", String.valueOf(tem_1));
						dataMap.put("hum_1", String.valueOf(hum_1));
						dataMap.put("ec_1", String.valueOf(ec_1));
						dataMap.put("tem_2", String.valueOf(tem_2));
						dataMap.put("hum_2", String.valueOf(hum_2));
						dataMap.put("ec_2", String.valueOf(ec_2));
						dataMap.put("tem_3", String.valueOf(tem_3));
						dataMap.put("hum_3", String.valueOf(hum_3));
						dataMap.put("ec_3", String.valueOf(ec_3));
//						MemoryCacheUtil.put(ip, dataMap.isEmpty());
						break;
				}
				break;
			case "10":
				mark = msg.substring(10, 12);
				switch (mark) {
					case "19":
						log.info("控制坡度成功");
						break;
					case "1A":
						log.info("控制浴霸1、 2、 3成功");
						break;
					case "15":    // L.D. TODO 2022/9/27 15:03 : 这个暂时没用到
						log.info("清空流量计1、2、3、4累计流量成功");
						break;
				}
				break;
		}
		
		return dataMap;
	}
	
}

3,数据校验(InstructionTransformation)

**
 * @Author: LQY
 * @CreateTime: 2022-09-21  16:35
 * @Description: 指令转换,转为hex
 */

public class InstructionTransformation {
	public static String getCRC16(String data) {
		data = data.replace(" ", "");
		int len = data.length();
		if (!(len % 2 == 0)) {
			return "0000";
		}
		int num = len / 2;
		byte[] para = new byte[num];
		for (int i = 0; i < num; i++) {
			int value = Integer.valueOf(data.substring(i * 2, 2 * (i + 1)), 16);
			para[i] = (byte) value;
		}
		return getCRC16(para);
	}
	
	/**
	 * 计算CRC16校验码
	 *
	 * @param bytes 字节数组
	 * @return 校验码
	 */
	public static String getCRC16(byte[] bytes) {
		// CRC寄存器全为1
		int CRC = 0x0000ffff;
		// 多项式校验值
		int POLYNOMIAL = 0x0000a001;
		int i, j;
		for (i = 0; i < bytes.length; i++) {
			CRC ^= ((int) bytes[i] & 0x000000ff);
			for (j = 0; j < 8; j++) {
				if ((CRC & 0x00000001) != 0) {
					CRC >>= 1;
					CRC ^= POLYNOMIAL;
				} else {
					CRC >>= 1;
				}
			}
		}
		// 结果转换为16进制
		String result = Integer.toHexString(CRC).toUpperCase();
		if (result.length() != 4) {
			StringBuffer sb = new StringBuffer("0000");
			result = sb.replace(4 - result.length(), 4, result).toString();
		}
		//高位在前地位在后
		//String str = result.substring(0, 2) + result.substring(2, 4);
		// 交换高低位,低位在前高位在后
		String str = result.substring(2, 4) + result.substring(0, 2);
		return str;
	}
}

控制层

1,nettyController

/**
 * @Author: LQY
 * @CreateTime: 2022-09-23  09:38
 * @Description: 硬件控制
 */
@RestController
@RequestMapping("/control")
@Api(tags = "硬件控制类", produces = "application/json", consumes = "application/json")
@Slf4j
public class NettyController {

	@Autowired
	private DataCollectionService dataCollectionService;

	@Autowired
	private CommandControlService commandControlService;

	@Autowired
	private RemoveFlowService removeFlowService;

	/**
	 * @Description: 数据采集(单次)
	 * @Author LQY
	 * @Date 2022/9/24 9:50
	 * @Return com.alibaba.cola.dto.Response
	 */
	@PostMapping("/dataCollect")
	@ApiOperation(value = "数据收集")
	public Response dataCollect(@RequestBody @Validated NettyRequestDto nettyRequestDto) {
		return dataCollectionService.dataCollection(nettyRequestDto);
	}

	/**
	 * @Description: 数据采集(定时) // L.D. TODO 2022/9/27 14:37 : 开始实验调这个接口
	 * @Author LQY
	 * @Date 2022/9/25 13:40
	 * @Return com.alibaba.cola.dto.Response
	 */
	@PostMapping("/dataCollectTiming")
	@ApiOperation(value = "数据收集(定时)")
	public Response dataCollectTiming(@RequestBody @Validated NettyRequestDto nettyRequestDto) {
		JobDataMap map = new JobDataMap();
		//map.put("address", nettyRequestDto.getAddress());
		map.put("deviceId", nettyRequestDto.getDeviceId());
		map.put("deviceNumber", nettyRequestDto.getDeviceNumber());
		ExperimentScheduledTaskAddCmd cmd = new ExperimentScheduledTaskAddCmd();
		cmd.setJobName(nettyRequestDto.getDeviceId());
		cmd.setGroupName(QuartzGroupEnum.EXPERIMENT.getValue());
		cmd.setJobClass(CollectDataJob.class);
		//默认5分钟采集一次
		String cron = FrequencyEnum.getValueByName(nettyRequestDto.getFrequency());
		if(StringUtils.isEmpty(cron)) {
			//cron = FrequencyEnum.FIVE_MINUTE.getCron();
			// LQY TODO 2022/11/9 16:55 =>演示用
			cron = FrequencyEnum.TEN_SECOND.getCron();
			
		}
		cmd.setCron(cron);
		cmd.setJobDataMap(map);
		log.info("开始采集");

		return dataCollectionService.dataCollectTiming(cmd);
	}

	@PostMapping("/stopCollect")
	@ApiOperation(value = "实验停止停止收集数据")
	public Response stopCollect(@RequestBody @Validated NettyRequestDto nettyRequestDto) {
//		log.error("测试测试测试测试");
		ExperimentRemoveScheduledTaskCmd cmd = new ExperimentRemoveScheduledTaskCmd();
		cmd.setJobName(nettyRequestDto.getDeviceId());
		cmd.setGroupName(QuartzGroupEnum.EXPERIMENT.getValue());
		ExperimentScheduledTaskService experimentScheduledTaskService = SpringUtil.getBean("experimentScheduledTaskService");
		return experimentScheduledTaskService.removeExperimentScheduledTask(cmd);

	}

	/**
	 * @Description: 控制指令
	 * @Author LQY
	 * @Date 2022/9/24 9:50
	 * @Return com.alibaba.cola.dto.Response
	 */
	@PostMapping("/sendInstructionToDevice")
	@ApiOperation(value = "发送控制指令到设备")
	public Response sendInstructionToDevice(@RequestBody @Validated NettyRequestDto nettyRequestDto) throws InterruptedException {
		Response result = commandControlService.commandControl(nettyRequestDto);
//		Boolean sendResult = MemoryCacheUtil.get(nettyRequestDto.getAddress());
//		if (Objects.nonNull(sendResult) && !sendResult) {
//			MemoryCacheUtil.remove(nettyRequestDto.getAddress());
//			return Response.buildFailure(ErrorCode.ERROR_SERVICE.getErrCode(), "操作失败");
//		}

		return result;
	}

	/**
	 * @Description: 清除累计流量
	 * @Author LQY
	 * @Date 2022/9/24 9:49
	 * @Return com.alibaba.cola.dto.Response
	 */
	@PostMapping("/clearAccumulatedFlow")
	@ApiOperation(value = "清除累计流量")
	public Response clearAccumulatedFlow(@RequestBody @Validated NettyRequestDto nettyRequestDto) {
		return removeFlowService.removeFlow(nettyRequestDto);
	}
}

2,重点(sendInstructionToDevice)(发送控制指令到指定设备)
之前是靠前端传输过来的具体设备的路由地址(通道地址channel),使用循环查询是否存在该设备,存在就能发送控制指令,后续改为设备连接后,会通过全局存储当前连接的设备数据(包含接口查询数据表的相关数据),前端传参只需要传设备的deviceMark与deviceId,传入控制参数即可操控硬件设备。

**
 * @Author: LQY
 * @CreateTime: 2022-09-23  09:16
 * @Description: 控制指令
 */

@Data
public class NettyRequestDto {

	private String address;
	/**
	 * 设备编号
	 */
	
	private String deviceNumber;
	/**
	 * 操作类型
	 */
	private String type;
	
	/** 采集频率 */
	
	private String frequency;
	
	/** 设备id */
	private String deviceId;
}
/**
	 * @Description: 控制指令
	 * @Author LQY
	 * @Date 2022/9/24 9:50
	 * @Return com.alibaba.cola.dto.Response
	 */
	@PostMapping("/sendInstructionToDevice")
	@ApiOperation(value = "发送控制指令到设备")
	public Response sendInstructionToDevice(@RequestBody @Validated NettyRequestDto nettyRequestDto) throws InterruptedException {
		Response result = commandControlService.commandControl(nettyRequestDto);
//		Boolean sendResult = MemoryCacheUtil.get(nettyRequestDto.getAddress());
//		if (Objects.nonNull(sendResult) && !sendResult) {
//			MemoryCacheUtil.remove(nettyRequestDto.getAddress());
//			return Response.buildFailure(ErrorCode.ERROR_SERVICE.getErrCode(), "操作失败");
//		}

		return result;
	}

3,控制指令

/**
 * @Author: LQY
 * @CreateTime: 2022-09-24  09:31
 * @Description:指令控制操作
 */
@Component
@Slf4j
public class CommandControlCmdExe {

	@Resource
	NettyChannelMng nettyChannelMng;
	/**
	 * @Description: 发控制指令
	 * @Author LQY
	 * @Date 2022/9/24 9:50
	 * @Return com.alibaba.cola.dto.Response
	 */
	public Response execute(NettyRequestDto nettyRequestDto) {
		Channel channel = nettyChannelMng.getActiveDeviceChannel(nettyRequestDto.getDeviceId());
		if(Objects.isNull(channel)) {
			return Response.buildFailure(ErrorCode.ERROR_SERVICE.getErrCode(), "设备断开连接,请稍后再试");
		}

		String deviceNum = nettyRequestDto.getDeviceNumber().substring(4);
		switch (nettyRequestDto.getType()) {
			case "1"://获取流速传感器数据、土壤温湿度数据、角度传感器数据
				String msg = deviceNum + "0300000018";
				String CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				channel.writeAndFlush(msg);
				break;
			case "2"://控制坡度为0度
				msg = deviceNum + "1000190001020000";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				channel.writeAndFlush(msg);
				break;
			case "3"://控制坡度为2度
				msg = deviceNum + "1000190001020001";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				channel.writeAndFlush(msg);
				break;
			case "4"://控制坡度为4度
				msg = deviceNum + "1000190001020002";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				channel.writeAndFlush(msg);
				break;
			case "5"://控制坡度为6度
				msg = deviceNum + "1000190001020003";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				channel.writeAndFlush(msg);
				break;
			case "15"://控制坡度为8度
				msg = deviceNum + "1000190001020004";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				channel.writeAndFlush(msg);
				break;
			case "16"://控制坡度为10度
				msg = deviceNum + "1000190001020005";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				channel.writeAndFlush(msg);
				break;
			case "6"://控制浴霸1开
				//msg = deviceNum + "10001A000306000000000000";
				msg = deviceNum + "10001A0001020001";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				log.error("1开指令:{}",msg);
				channel.writeAndFlush(msg);
				break;
			case "7"://控制浴霸1关
				//msg = deviceNum + "10001A000306000100000000";
				msg = deviceNum + "10001A0001020000";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				log.error("1关指令:{}",msg);
				channel.writeAndFlush(msg);
				break;
			case "8"://控制浴霸2开
				//msg = deviceNum + "10001A000306000100010000";
				msg = deviceNum + "10001B0001020001";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				log.error("2开指令:{}",msg);
				channel.writeAndFlush(msg);
				break;
			case "9"://控制浴霸2关
				//msg = deviceNum + "10001A000306000100010001";
				msg = deviceNum + "10001B0001020000";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				log.error("2关指令:{}",msg);
				channel.writeAndFlush(msg);
				break;
			case "10"://控制浴霸3开
				//msg = deviceNum + "10001A000306000000010000";
				msg = deviceNum + "10001C0001020001";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				log.error("3开指令:{}",msg);
				channel.writeAndFlush(msg);
				break;
			case "11"://控制浴霸3关
				//msg = deviceNum + "10001A000306000000010001";
				msg = deviceNum + "10001C0001020000";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				log.error("3关指令:{}",msg);
				channel.writeAndFlush(msg);
				break;
			//case "12"://控制浴霸1关2关3开
			//	msg = deviceNum + "10001A000306000000000001";
			//	CRC = InstructionTransformation.getCRC16(msg);
			//	msg = msg + CRC;
			//	channel.writeAndFlush(msg);
			//	break;
			//case "13"://控制浴霸1开2关3开
			//	msg = deviceNum + "10001A000306000100000001";
			//	CRC = InstructionTransformation.getCRC16(msg);
			//	msg = msg + CRC;
			//	channel.writeAndFlush(msg);
			//	break;
			case "14"://清空流量计1、2、3、4累计流量cc
				msg = deviceNum + "1000180001021111";
				CRC = InstructionTransformation.getCRC16(msg);
				msg = msg + CRC;
				channel.writeAndFlush(msg);
				break;
		}
		return Response.buildSuccess();
	}
}