基于 TCP 的工业级干扰设备通信协议实现:从帧解析到命令交互全解析(uni-app/TCP通信)

在工业控制领域,基于 TCP 的设备通信协议开发是核心能力之一,尤其是面对需要高可靠性、严格帧格式校验的干扰设备通信场景,协议的精准实现直接决定了设备交互的稳定性。本文将详细拆解一套完整的工业级干扰设备 TCP 通信协议实现方案,涵盖 CRC16 校验、DLE 转义、帧解析、心跳处理、命令构建与发送等核心环节,并提供可直接落地的代码实现与调用指南。

一、需求背景与核心功能

本次实现的核心目标是构建一套与干扰设备通信的 TCP 客户端,具备以下核心能力:

  • 多 IP 连接管理与自动重连机制
  • 严格遵循自定义帧格式的数据包编解码
  • CRC16 校验保证数据完整性
  • DLE 转义 / 去转义处理避免帧边界混淆
  • 心跳帧自动响应与普通数据帧确认
  • 干扰设备的开启 / 关闭命令构建与发送
  • 连接状态监控与异常处理

二、核心技术点拆解

2.1 协议基础定义

2.1.1 帧结构规范

设备通信帧采用固定格式,核心结构如下:

plaintext

bash 复制代码
帧起始(10 02) + 长度字段(2字节) + 序列号(1字节) + 目的地址(1字节) + 源地址(1字节) + 数据内容 + CRC(2字节) + 帧结束(10 03)
  • 长度字段:表示「序列号 + 目的地址 + 源地址 + 数据内容 + CRC」的总字节数
  • 序列号:0-127 循环使用,确认帧需将最高位置 1
  • 地址映射:信号源 1-5 对应 0x01/0x02/0x04/0x08/0x10,"all" 对应 0x1F(全开 / 全停)
2.1.2 关键算法实现
(1)CRC16 校验算法

采用查表法实现高效 CRC16 校验,核心逻辑是通过预定义的校验表(Crc_Ta),逐字节计算数据的校验值,保证传输数据的完整性:

javascript 复制代码
crc16(data) {
	const Crc_Ta = [/* 预定义校验表 */];
	let crc = 0x0000;
	let i = data.length;
	let index = 0;
	while (i-- !== 0) {
		const tmp = (crc >> 8) & 0xFF;
		crc = ((crc << 8) & 0xFFFF) ^ Crc_Ta[tmp ^ data[index]];
		index++;
	}
	return crc & 0xFFFF;
}
(2)DLE 转义 / 去转义

为避免数据中的 0x10(DLE)、0x02(STX)、0x03(ETX)混淆帧边界,需对 DLE 字符进行转义处理:

  • 转义:遇到 0x10 则追加一个 0x10
  • 去转义:遇到连续两个 0x10 则跳过第二个
javascript 复制代码
// DLE转义
escapeDLE(data) {
	const result = [];
	for (let i = 0; i < data.length; i++) {
		result.push(data[i]);
		if (data[i] === 0x10) {
			result.push(0x10);
		}
	}
	return result;
}

// DLE去转义
unescapeDLE(data) {
	const result = [];
	for (let i = 0; i < data.length; i++) {
		result.push(data[i]);
		if (data[i] === 0x10 && i + 1 < data.length && data[i + 1] === 0x10) {
			i++;
		}
	}
	return result;
}

2.2 连接管理模块

2.2.1 连接初始化

支持多 IP 批量连接,为每个 IP 维护独立的连接状态(连接状态、重试次数、最后连接时间等):

javascript 复制代码
initConnections(ipList, port, onMessage) {
	this.port = port || this.port;
	ipList.forEach((ip, index) => {
		this.connectionStatus[ip] = {
			connected: false,
			lastError: null,
			retryCount: 0,
			lastConnectTime: 0
		};
		this.connect(ip, port, onMessage);
	});
	this._startStatusMonitor();
}
2.2.2 自动重连机制

连接失败或断开后,自动触发重连逻辑,重连间隔默认 5 秒:

javascript 复制代码
// 连接失败后触发重连
setTimeout(() => {
	this.connect(ip, port, onMessage);
}, this.reconnectInterval);

// 连接断开后触发重连
_handleDisconnection(ip) {
	// 关闭连接逻辑...
	setTimeout(() => {
		var connection = this.connections[ip];
		if (connection) {
			this.connect(ip, connection.port, null);
		}
	}, this.reconnectInterval);
}
2.2.3 异步数据读取循环

采用非阻塞方式循环读取数据,避免线程阻塞,同时处理读取超时异常:

javascript 复制代码
_readLoop(connection, onMessage) {
	if (!connection || !connection.isConnected) return;
	setTimeout(() => {
		try {
			if (connection.dataInputStream.available() > 0) {
				// 读取数据并处理...
			}
			this._readLoop(connection, onMessage);
		} catch (e) {
			if (e.toString().indexOf("timed out") !== -1) {
				this._readLoop(connection, onMessage);
			} else {
				this._handleDisconnection(connection.ip);
			}
		}
	}, 10);
}

2.3 帧处理模块

2.3.1 帧解析与确认

核心处理逻辑包含帧起始 / 结束符校验、长度验证、心跳帧识别、确认帧构建:

javascript 复制代码
handleDataFrameAck(connection, data, ip) {
	try {
		// 1. 查找帧起始
		let startIndex = -1;
		for (let i = 0; i < data.length - 1; i++) {
			if (data[i] === 0x10 && data[i + 1] === 0x02) {
				startIndex = i;
				break;
			}
		}
		if (startIndex === -1) return;

		// 2. 去转义处理
		const unescapedData = this.unescapeDLE(data.slice(startIndex));

		// 3. 帧合法性校验(长度、结束符)
		if (unescapedData.length < 5) return;
		if (unescapedData[unescapedData.length - 2] !== 0x10 || unescapedData[unescapedData.length - 1] !== 0x03) return;

		// 4. 提取序列号并识别心跳帧
		const receivedSequence = unescapedData[4];
		if (unescapedData.length >= 12 && unescapedData[7] === 0x01 && unescapedData[8] === 0x23) {
			this.handleHeartbeat(connection, unescapedData, ip);
			return;
		}

		// 5. 构建并发送确认帧
		const ackFrame = this.buildAckFrame(receivedSequence);
		// 发送确认帧逻辑...
	} catch (e) {
		console.log(`处理数据帧确认失败 ${ip}:`, e.toString());
	}
}
2.3.2 确认帧构建

按照协议要求构建确认帧,将收到的序列号最高位置 1 作为确认帧序列号:

javascript 复制代码
buildAckFrame(receivedSequence) {
	const frame = [];
	frame.push(0x10, 0x02);
	const ackSequence = (receivedSequence | 0x80) & 0xFF;
	const infoField = [];
	const length = 3;
	infoField.push((length >> 8) & 0xFF);
	infoField.push(length & 0xFF);
	infoField.push(ackSequence);
	const crc = this.crc16(infoField);
	infoField.push((crc >> 8) & 0xFF);
	infoField.push(crc & 0xFF);
	const escapedInfoField = this.escapeDLE(infoField);
	frame.push(...escapedInfoField);
	frame.push(0x10, 0x03);
	return frame;
}

2.4 命令发送模块

2.4.1 开启干扰命令

支持多频段参数配置,包含中心频点、衰减、调制方式、扫频速度 / 步进等参数,采用小端序构建数据载荷:

javascript 复制代码
sendTo(ip, bands, src) {
	var connection = this.connections[ip];
	if (!connection || !connection.isConnected) return false;
	try {
		const srcAddr = 0x80;
		const destAddr = this.getSignalSourceAddr(src);
		const dataContent = this.buildChannelOpenData(bands);
		const frame = this.buildFrame(destAddr, srcAddr, dataContent, connection.sequenceNumber);
		connection.sequenceNumber = (connection.sequenceNumber + 1) % 128;
		// 转换为字节数组并发送...
		return true;
	} catch (e) {
		this._handleDisconnection(ip);
		return false;
	}
}
2.4.2 关闭干扰命令

支持指定通道或全通道关闭,通道掩码采用小端序编码:

javascript 复制代码
sendChannelClose(ip, src) {
	var connection = this.connections[ip];
	if (!connection || !connection.isConnected) return false;
	try {
		const srcAddr = 0x80;
		const destAddr = this.getSignalSourceAddr(src);
		const mask = 0x03e0; // 关闭所有通道(bit0~bit9)
		const dataContent = [0x01, 0x29, mask & 0xFF, (mask >> 8) & 0xFF];
		const frame = this.buildFrame(destAddr, srcAddr, dataContent, connection.sequenceNumber);
		connection.sequenceNumber = (connection.sequenceNumber + 1) % 128;
		// 转换为字节数组并发送...
		return true;
	} catch (e) {
		this._handleDisconnection(ip);
		return false;
	}
}

三、完整调用示例

3.1 基础初始化与连接

javascript 复制代码
// 导入通信模块
import tcpClient from './tcpClient.js';

// 初始化连接
const ipList = ['192.168.1.100', '192.168.1.101'];
const port = 2000;
tcpClient.initConnections(ipList, port, (data, ip) => {
	console.log(`收到来自${ip}的原始数据:`, data);
});

// 获取连接状态
const status = tcpClient.getConnectionStatus();
console.log('当前连接状态:', status);

3.2 发送开启干扰命令

javascript 复制代码
// 定义频段参数
const bands = [
	{
		channel: 1,          // 通道1
		frequency: 900,      // 中心频点900MHz
		attenuation: 10,     // 衰减10dB
		modem: 1,            // 调制方式1
		mode: 2,             // 扫频模式2-step
		bandwidth: 20        // 带宽20MHz
	}
];

// 发送开启命令到指定IP,控制信号源1
const sendResult = tcpClient.sendTo('192.168.1.100', bands, 1);
if (sendResult) {
	console.log('开启干扰命令发送成功');
} else {
	console.log('开启干扰命令发送失败');
}

3.3 发送关闭干扰命令

javascript 复制代码
// 发送关闭命令到指定IP,关闭所有信号源
const closeResult = tcpClient.sendChannelClose('192.168.1.100', 'all');
if (closeResult) {
	console.log('关闭干扰命令发送成功');
} else {
	console.log('关闭干扰命令发送失败');
}

3.4 手动重连与连接关闭

javascript 复制代码
// 手动重连指定IP
tcpClient.reconnect('192.168.1.100');

// 关闭指定IP连接
tcpClient.closeConnection('192.168.1.101');

// 关闭所有连接
tcpClient.closeAll();

四、关键优化点与注意事项

4.1 长度验证逻辑

协议中的长度字段是「序列号到 CRC 的长度」,验证时需注意:

  • 先对帧体进行去转义处理
  • 验证公式:声明长度 = 去转义帧体长度 - 2(长度字段本身占 2 字节)

4.2 序列号管理

每个连接维护独立的序列号,范围 0-127 循环,发送帧后递增,避免多连接序列号冲突。

4.3 数据端序处理

所有多字节数据(如频率、扫频速度、通道掩码)均采用小端序编码,需严格遵循协议要求。

4.4 异常处理

  • 读取超时:继续读取循环,不触发重连
  • 连接异常:关闭连接并触发重连
  • 帧格式错误:记录日志并跳过该帧,不影响后续数据处理

五、总结

本文详细拆解了一套工业级干扰设备 TCP 通信协议的完整实现,核心亮点包括:

  1. 高可靠性:实现自动重连、非阻塞读取、异常容错等机制,保证连接稳定性;
  2. 协议合规性:严格遵循自定义帧格式,包含 CRC 校验、DLE 转义、帧确认等核心机制;
  3. 易用性:提供简洁的调用接口,支持多 IP 批量管理、命令一键发送;
  4. 可扩展性:模块化设计,便于后续新增命令类型、扩展协议字段。

该实现方案不仅适用于干扰设备通信,也可作为工业级 TCP 通信协议开发的通用参考模板,只需根据具体设备的协议文档调整帧结构、命令字、地址映射等细节,即可快速适配其他工业设备的通信需求。

六、可使用版本

javascript 复制代码
export default {
	connections: {}, // 存储所有连接对象
	connectionStatus: {}, // 存储连接状态
	port: 2000, // 默认端口,可以根据需要修改
	reconnectInterval: 5000, // 重连间隔5秒

	// CRC16校验算法
	crc16(data) {
		const Crc_Ta = [
			0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
			0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
			0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
			0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
			0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
			0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
			0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
			0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
			0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
			0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
			0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
			0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
			0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
			0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
			0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
			0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
			0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
			0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
			0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
			0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
			0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
			0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
			0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
			0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
			0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
			0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
			0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
			0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
			0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
			0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
			0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
			0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
		];
		let crc = 0x0000; // 对应 C: crc = 0
		let i = data.length;
		let index = 0;

		while (i-- !== 0) {
			const tmp = (crc >> 8) & 0xFF;
			crc = ((crc << 8) & 0xFFFF) ^ Crc_Ta[tmp ^ data[index]];
			index++;
		}

		return crc & 0xFFFF;
	},

	// DLE转义处理
	escapeDLE(data) {
		const result = [];
		for (let i = 0; i < data.length; i++) {
			result.push(data[i]);
			if (data[i] === 0x10) { // DLE字符
				result.push(0x10); // 再加一个DLE
			}
		}
		return result;
	},

	// 解析收到的帧(去除DLE转义)
	unescapeDLE(data) {
		const result = [];
		for (let i = 0; i < data.length; i++) {
			result.push(data[i]);
			if (data[i] === 0x10 && i + 1 < data.length && data[i + 1] === 0x10) {
				i++; // 跳过第二个DLE
			}
		}
		return result;
	},

	// 构建确认帧(根据文档2.2)- 修改后
	buildAckFrame(receivedSequence) {
		const frame = [];

		// 帧起始
		frame.push(0x10, 0x02);

		// 确认帧序列号最高位置1,低7位为接收到的帧序号
		// 注意:确认帧的序列号就是收到的序列号,只是最高位置1
		const ackSequence = (receivedSequence | 0x80) & 0xFF;

		// 构建信息字段: 长度 + 序列号
		const infoField = [];
		const length = 3; // 从序列号开始到CRC结束的字节数 = 序列号(1) + CRC(2) = 3

		infoField.push((length >> 8) & 0xFF); // 长度高字节
		infoField.push(length & 0xFF);        // 长度低字节
		infoField.push(ackSequence);          // 确认序列号

		// 计算CRC (从长度开始到序列号结束)
		const crc = this.crc16(infoField);

		// CRC高字节在前,低字节在后
		infoField.push((crc >> 8) & 0xFF); // CRC高字节
		infoField.push(crc & 0xFF);        // CRC低字节

		// DLE转义
		const escapedInfoField = this.escapeDLE(infoField);

		// 组合完整帧
		frame.push(...escapedInfoField);
		frame.push(0x10, 0x03); // 帧结束

		return frame;
	},

	// 处理心跳帧(CmdType=0x01, CmdCode=0x23)- 修改后
	handleHeartbeat(connection, data, ip) {
		console.log(`收到心跳帧 from ${ip}`);

		// 根据文档,构建心跳确认帧
		try {
			// 心跳帧结构:10 02 长度 序列号 目的地址 源地址 01 23 状态信息 CRC 10 03
			// 序列号在数据中的位置:跳过10 02和长度(2字节),所以索引4是序列号
			const receivedSequence = data[4];

			console.log(`心跳帧序列号: ${receivedSequence} from ${ip}`);

			// 构建确认帧
			const ackFrame = this.buildAckFrame(receivedSequence);

			// 转换为字节数组发送
			var ByteArray = plus.android.importClass("java.io.ByteArrayOutputStream");
			var byteArrayStream = new ByteArray();

			for (let i = 0; i < ackFrame.length; i++) {
				byteArrayStream.write(ackFrame[i]);
			}

			var bytes = byteArrayStream.toByteArray();

			console.log(`发送心跳确认帧到 ${ip},帧数据:`, ackFrame.map(b => b.toString(16).padStart(2, '0')).join(' '));

			connection.dataOutputStream.write(bytes);
			connection.dataOutputStream.flush();
			console.log(`心跳确认帧发送成功到 ${ip}`);

		} catch (e) {
			console.log(`心跳确认帧发送失败 ${ip}:`, e.toString());
		}
	},

	// 处理数据帧(通用确认)- 修改后
	handleDataFrameAck(connection, data, ip) {
		try {
			// 查找帧起始
			let startIndex = -1;
			for (let i = 0; i < data.length - 1; i++) {
				if (data[i] === 0x10 && data[i + 1] === 0x02) {
					startIndex = i;
					break;
				}
			}

			if (startIndex === -1) {
				console.log(`未找到有效帧起始 from ${ip}`);
				return;
			}

			// 去除DLE转义
			const unescapedData = this.unescapeDLE(data.slice(startIndex));

			// 检查帧长度
			if (unescapedData.length < 5) {
				console.log(`帧长度不足 from ${ip}: ${unescapedData.length}`);
				return;
			}

			// 验证帧结束符
			if (unescapedData[unescapedData.length - 2] !== 0x10 || unescapedData[unescapedData.length - 1] !== 0x03) {
				console.log(`无效的帧结束符 from ${ip}`);
				return;
			}

			// 提取长度字段(位置2-3)
			const length = (unescapedData[2] << 8) | unescapedData[3];

			// 验证长度
			// if (unescapedData.length !== length + 4) { // 长度+帧起始(2)+帧结束(2)
			//   console.log(`帧长度不匹配 from ${ip}: 期望 ${length + 4}, 实际 ${unescapedData.length}`);
			//   return;
			// }

			// 提取序列号(位置4)
			const receivedSequence = unescapedData[4];

			console.log(`收到帧 from ${ip},序列号: ${receivedSequence},长度: ${length}`);

			// 检查是否是心跳帧(CmdType=0x01, CmdCode=0x23)
			// 心跳帧结构:10 02 长度 序列号 目的地址 源地址 01 23 状态信息 CRC 10 03
			// 检查是否有足够的数据,并且是心跳命令
			if (unescapedData.length >= 12 && unescapedData[7] === 0x01 && unescapedData[8] === 0x23) {
				// 心跳帧,特殊处理
				this.handleHeartbeat(connection, unescapedData, ip);
				return; // 心跳帧已处理,直接返回
			}

			// 普通数据帧,发送确认
			console.log(`收到普通数据帧 from ${ip},序列号:`, receivedSequence);

			// 构建确认帧
			const ackFrame = this.buildAckFrame(receivedSequence);

			// 转换为字节数组发送
			var ByteArray = plus.android.importClass("java.io.ByteArrayOutputStream");
			var byteArrayStream = new ByteArray();

			for (let i = 0; i < ackFrame.length; i++) {
				byteArrayStream.write(ackFrame[i]);
			}

			var bytes = byteArrayStream.toByteArray();

			console.log(`发送确认帧到 ${ip},序列号: ${receivedSequence}`);

			connection.dataOutputStream.write(bytes);
			connection.dataOutputStream.flush();
			console.log(`确认帧发送成功到 ${ip}`);

		} catch (e) {
			console.log(`处理数据帧确认失败 ${ip}:`, e.toString());
		}
	},

	// 根据信号源编号获取地址
	getSignalSourceAddr(src) {
		// 根据文档地址规划映射
		const addrMap = {
			1: 0x01,   // 信号源1
			2: 0x02,   // 信号源2
			3: 0x04,   // 信号源3
			4: 0x08,   // 信号源4
			5: 0x10,   // 信号源5
			'all': 0x1F // 全开/全停
		};

		// 如果src是数字1-5,则映射
		if (typeof src === 'number' && src >= 1 && src <= 5) {
			return addrMap[src];
		}

		// 如果src是字符串"1"-"5",也映射
		if (typeof src === 'string' && /^[1-5]$/.test(src)) {
			return addrMap[parseInt(src)];
		}

		// 如果src是'all',返回全开/全停地址
		if (src === 'all') {
			return addrMap['all'];
		}

		// 否则按十六进制解析
		let addr = parseInt(src);
		if (isNaN(addr)) {
			console.warn(`无法解析信号源地址: ${src},使用默认地址0x01`);
			return 0x01;
		}

		return addr;
	},

	// 计算扫频速度和步进
	calculateSweepAndStep(frequencyMHz) {
		if (frequencyMHz < 2000) { // 2G以下
			return {
				sweep: 600000,  // Hz
				step: 700000    // Hz
			};
		} else { // 2G以上
			return {
				sweep: 500000,  // Hz
				step: 1000000   // Hz
			};
		}
	},

	// 构建开启干扰命令的数据载荷(小端序)
	buildChannelOpenData(bands) {
		// 命令类型和命令字 (根据文档 CmdType=0x01, CmdCode=0x17)
		const data = [0x01, 0x17];

		// 通道数量
		data.push(bands.length);

		// 通道参数
		bands.forEach(band => {
			// 通道 (1字节)
			data.push(band.channel || 5);

			// 中心频点(频率)(4字节 float, MHz) - 小端序
			const freq = band.frequency || 0;
			const freqBuffer = new ArrayBuffer(4);
			const freqView = new DataView(freqBuffer);
			freqView.setFloat32(0, freq, true); // true表示小端序
			for (let i = 0; i < 4; i++) {
				data.push(freqView.getUint8(i));
			}

			// 衰减 (1字节, 0~31dB)
			data.push(band.attenuation || 0);

			// 调制方式 (1字节, 默认1)
			data.push(band.modem || 1);

			// 参数模板数量 (4字节 int, 默认1) - 改为4字节int小端序
			const templateCount = 1;
			data.push(templateCount & 0xFF);
			data.push((templateCount >> 8) & 0xFF);
			data.push((templateCount >> 16) & 0xFF);
			data.push((templateCount >> 24) & 0xFF);

			// 子频段数量 (4字节 int, 默认1) - 改为4字节int小端序
			const subbandCount = 1;
			data.push(subbandCount & 0xFF);
			data.push((subbandCount >> 8) & 0xFF);
			data.push((subbandCount >> 16) & 0xFF);
			data.push((subbandCount >> 24) & 0xFF);

			// 计算扫频速度和步进
			const { sweep, step } = this.calculateSweepAndStep(freq);

			// 参数模板0
			// 模式 (1字节, 默认2-step)
			data.push(band.mode || 2);

			// 扫频速度 (4字节 int, Hz) - 小端序
			data.push(sweep & 0xFF);
			data.push((sweep >> 8) & 0xFF);
			data.push((sweep >> 16) & 0xFF);
			data.push((sweep >> 24) & 0xFF);

			// 步进 (4字节 int, Hz) - 小端序
			data.push(step & 0xFF);
			data.push((step >> 8) & 0xFF);
			data.push((step >> 16) & 0xFF);
			data.push((step >> 24) & 0xFF);
			// 保留 (16字节)
			for (let i = 0; i < 16; i++) {
				data.push(0x00);
			}

			// 计算子频段偏移 (带宽转换为Hz,再除以2得到偏移)
			const bandwidthHz = (band.bandwidth || 0) * 1000000; // MHz -> Hz
			const startOffset = -Math.floor(bandwidthHz / 2);
			const endOffset = Math.floor(bandwidthHz / 2);

			// 子频段0 - 开始偏移 (4字节 int, Hz) - 小端序
			data.push(startOffset & 0xFF);
			data.push((startOffset >> 8) & 0xFF);
			data.push((startOffset >> 16) & 0xFF);
			data.push((startOffset >> 24) & 0xFF);

			// 子频段0 - 结束偏移 (4字节 int, Hz) - 小端序
			data.push(endOffset & 0xFF);
			data.push((endOffset >> 8) & 0xFF);
			data.push((endOffset >> 16) & 0xFF);
			data.push((endOffset >> 24) & 0xFF);

			// 信号参数模板索引 (2字节 unsigned short, 默认0) - 小端序
			data.push(0x00);
			data.push(0x00);
		});

		return data;
	},

	// 构建完整帧(使用连接的序列号)- 修改后
	buildFrame(destAddr, srcAddr, dataContent, sequenceNumber) {
		const frame = [];

		// 帧起始
		frame.push(0x10, 0x02);

		// 使用传入的序列号(取模128,确保在0-127范围内)
		const sequence = sequenceNumber % 128;

		// 构建信息字段: 长度 + 序列号 + 目的地址 + 源地址 + 数据内容
		const infoField = [];

		// 先添加长度占位符,稍后计算
		infoField.push(0x00, 0x00);

		// 添加序列号、目的地址、源地址、数据内容
		infoField.push(sequence);
		infoField.push(destAddr);
		infoField.push(srcAddr);
		infoField.push(...dataContent);

		// 计算长度: 从序列号到CRC结束的字节数
		// 序列号(1) + 目的地址(1) + 源地址(1) + 数据内容长度 + CRC(2)
		const length = 1 + 1 + 1 + dataContent.length + 2;

		// 更新长度字段
		infoField[0] = (length >> 8) & 0xFF; // 高字节
		infoField[1] = length & 0xFF;        // 低字节

		// 计算CRC (从长度开始到数据内容结束)
		const crc = this.crc16(infoField);

		// 添加CRC(高字节在前)
		infoField.push((crc >> 8) & 0xFF); // CRC高字节
		infoField.push(crc & 0xFF);        // CRC低字节

		// DLE转义
		const escapedInfoField = this.escapeDLE(infoField);

		// 组合完整帧
		frame.push(...escapedInfoField);
		frame.push(0x10, 0x03); // 帧结束

		console.log(`构建帧: 序列号=${sequence}, 长度=${length}, CRC=${crc.toString(16).toUpperCase()}`);

		return frame;
	},

	// 初始化所有连接
	initConnections(ipList, port, onMessage) {
		this.port = port || this.port;

		// 为每个IP创建连接
		ipList.forEach((ip, index) => {
			this.connectionStatus[ip] = {
				connected: false,
				lastError: null,
				retryCount: 0,
				lastConnectTime: 0
			};
			// 创建连接
			this.connect(ip, port, onMessage);
		});
		// 启动状态监控
		this._startStatusMonitor();
	},

	// 连接单个TCP
	connect(ip, port, onMessage) {
		try {
			// 如果已有连接,先关闭
			if (this.connections[ip]) {
				this.closeConnection(ip);
			}
			// 更新连接状态
			this.connectionStatus[ip] = {
				...this.connectionStatus[ip],
				connecting: true,
				lastConnectTime: Date.now()
			};

			// 导入Java类
			var Socket = plus.android.importClass("java.net.Socket");
			var DataInputStream = plus.android.importClass("java.io.DataInputStream");
			var DataOutputStream = plus.android.importClass("java.io.DataOutputStream");
			const InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress");

			console.log(`正在连接服务器 ${ip}:${port}...`);

			// 创建Socket并设置连接超时
			const socket = new Socket();
			const address = new InetSocketAddress(ip, port);
			socket.connect(address, 5000); // 5秒连接超时
			socket.setSoTimeout(30000); // 30秒读取超时

			console.log(`Socket连接成功: ${ip}`);

			uni.showToast({
				title: `${ip}Подключение успешно...`,
				icon: 'none', // 可选值 'success', 'loading', 'none'
				duration: 2000,// 持续时间,单位ms
				position: 'top'
			})

			// 创建读写流
			var dataInputStream = new DataInputStream(socket.getInputStream());
			var dataOutputStream = new DataOutputStream(socket.getOutputStream());

			// 创建连接对象(包含序列号,从1开始)
			var connection = {
				ip: ip,
				port: port,
				socket: socket,
				dataInputStream: dataInputStream,
				dataOutputStream: dataOutputStream,
				isConnected: true,
				reconnectTimer: null,
				sequenceNumber: 1 // 每个连接独立的序列号,从1开始
			};

			// 存储连接对象
			this.connections[ip] = connection;

			// 更新连接状态
			this.connectionStatus[ip] = {
				connected: true,
				connecting: false,
				lastError: null,
				retryCount: 0,
				lastConnectTime: Date.now()
			};

			// 触发状态变化回调
			this._onConnectionStatusChange(ip, 'connected');

			// 启动读取循环
			this._readLoop(connection, onMessage);

			return connection;

		} catch (e) {
			console.log(`连接失败 ${ip}:`, e.toString());

			uni.showToast({
				title: `${ip}Сбой соединения...`,
				icon: 'none', // 可选值 'success', 'loading', 'none'
				duration: 2000,// 持续时间,单位ms
				position: 'top'
			})

			// 更新连接状态
			this.connectionStatus[ip] = {
				...this.connectionStatus[ip],
				connected: false,
				connecting: false,
				lastError: e.toString(),
				retryCount: (this.connectionStatus[ip]?.retryCount || 0) + 1,
				lastConnectTime: Date.now()
			};

			// 触发状态变化回调
			this._onConnectionStatusChange(ip, 'disconnected', e.toString());

			// 5秒后重试
			setTimeout(() => {
				this.connect(ip, port, onMessage);
			}, this.reconnectInterval);

			return null;
		}
	},

	// 读取循环(修正为使用DataInputStream)
	_readLoop(connection, onMessage) {
		if (!connection || !connection.isConnected) return;

		// 使用setTimeout异步读取
		setTimeout(() => {
			try {
				// 检查是否有数据
				if (connection.dataInputStream.available() > 0) {
					// 读取数据
					var data = [];
					var byte;
					while (connection.dataInputStream.available() > 0) {
						byte = connection.dataInputStream.read();
						if (byte === -1) break;
						data.push(byte);
					}

					console.log(`收到数据 from ${connection.ip}:`, data.map(b => b.toString(16).padStart(2, '0')).join(' '));

					// 处理数据帧确认(包括心跳)
					this.handleDataFrameAck(connection, data, connection.ip);

					// 调用回调,并传入来源IP
					if (onMessage) onMessage(data, connection.ip);
				}

				// 继续下一次读取
				this._readLoop(connection, onMessage);

			} catch (e) {
				console.log(`读取异常 ${connection.ip}:`, e.toString());

				// 如果是超时异常,继续读取
				if (e.toString().indexOf("timed out") !== -1) {
					this._readLoop(connection, onMessage);
				} else {
					// 其他异常,断开连接
					this._handleDisconnection(connection.ip);
				}
			}
		}, 10); // 10ms后再次尝试
	},

	// 处理断开连接
	_handleDisconnection(ip) {
		console.log(`连接断开: ${ip}`);

		// 关闭连接
		this.closeConnection(ip);

		// 更新状态
		this.connectionStatus[ip] = {
			...this.connectionStatus[ip],
			connected: false,
			lastError: 'Connection lost',
			lastConnectTime: Date.now()
		};

		// 触发状态变化回调
		this._onConnectionStatusChange(ip, 'disconnected', 'Connection lost');

		// 5秒后重连
		setTimeout(() => {
			// 获取原来的端口和回调
			var connection = this.connections[ip];
			if (connection) {
				uni.showToast({
					title: `${ip}Восстановление соединения...`,
					icon: 'none', // 可选值 'success', 'loading', 'none'
					duration: 2000,// 持续时间,单位ms
					position: 'top'
				})
				this.connect(ip, connection.port, null); // onMessage会在重连后由_readLoop重新绑定
			}
		}, this.reconnectInterval);
	},

	// 发送数据到指定IP
	sendTo(ip, bands, src) {
		var connection = this.connections[ip];
		if (!connection || !connection.isConnected || !connection.dataOutputStream) {
			console.log(`发送失败 ${ip}: 未连接`);
			return false;
		}

		try {
			// 源地址为上位机 (0x80)
			const srcAddr = 0x80;

			// 目的地址根据信号源编号获取
			const destAddr = this.getSignalSourceAddr(src);

			console.log(`构建开启干扰命令,目标地址: 0x${destAddr.toString(16).padStart(2, '0')}, 源地址: 0x${srcAddr.toString(16).padStart(2, '0')}, 信号源: ${src}`);
			console.log('频段参数:', bands);

			// 构建数据内容(小端序)
			const dataContent = this.buildChannelOpenData(bands);

			// 使用连接的序列号构建完整帧
			const frame = this.buildFrame(destAddr, srcAddr, dataContent, connection.sequenceNumber);

			// 发送前递增序列号
			connection.sequenceNumber = (connection.sequenceNumber + 1) % 128;

			// 转换为字节数组发送
			var ByteArray = plus.android.importClass("java.io.ByteArrayOutputStream");
			var byteArrayStream = new ByteArray();

			for (let i = 0; i < frame.length; i++) {
				byteArrayStream.write(frame[i]);
			}

			var bytes = byteArrayStream.toByteArray();

			console.log(`发送消息到 ${ip},帧长度:`, frame.length, "字节");
			console.log("帧数据:", frame.map(b => b.toString(16).padStart(2, '0')).join(' '));

			connection.dataOutputStream.write(bytes);
			connection.dataOutputStream.flush();
			console.log(`发送成功到 ${ip},下一序列号: ${connection.sequenceNumber}`);
			return true;
		} catch (e) {
			console.log(`发送失败 ${ip}:`, e.toString());
			// 发送失败,可能是连接已断开
			this._handleDisconnection(ip);
			return false;
		}
	},

	// 发送通道关命令到指定IP(根据新文档格式)- 修正版
	// sendChannelClose(ip, src) {
	//   var connection = this.connections[ip];

	//   if (!connection || !connection.isConnected || !connection.dataOutputStream) {
	//     console.log(`发送失败 ${ip}: 未连接`);
	//     return false;
	//   }

	//   try {
	//     // 源地址为上位机 (0x80)
	//     const srcAddr = 0x80;

	//     // 目的地址根据信号源编号获取
	//     // 根据正确帧,目的地址是 0x07 (对应信号源1+2+3)
	//     const destAddr = this.getSignalSourceAddr(src);

	//     console.log(`构建关闭干扰命令,目标地址: 0x${destAddr.toString(16).padStart(2, '0')}, 信号源: ${src}`);

	//     // 构建数据内容(根据文档6.4格式)
	//     // 命令类型: 0x01, 命令字: 0x10
	//     const dataContent = [0x01, 0x10];

	//     // 通道掩码: 2字节 unsigned short
	//     // 根据正确帧,掩码是 0xFF10(小端序: 0x10, 0xFF)
	//     // 注意:正确帧中掩码是 0xFF10,表示关闭某些通道
	//     const mask = 0xFF10; // 修改为正确值

	//     // 小端序:低字节在前,高字节在后
	//     dataContent.push(mask & 0xFF);        // 低字节: 0x10
	//     dataContent.push((mask >> 8) & 0xFF); // 高字节: 0xFF

	//     // 根据正确帧,后面还有30个0x00的填充(总共34字节数据内容)
	//     // 正确帧中数据内容总共34字节:01 10 10 FF + 30个00
	//     for (let i = 0; i < 30; i++) {
	//       dataContent.push(0x00);
	//     }

	//     console.log(`数据内容: ${dataContent.map(b => b.toString(16).padStart(2, '0')).join(' ')}`);
	//     console.log(`数据内容长度: ${dataContent.length} 字节`);

	//     // 使用连接的序列号构建完整帧
	//     // 注意:正确帧的序列号是 0x02,我们使用自己的序列号
	//     const frame = this.buildFrame(destAddr, srcAddr, dataContent, connection.sequenceNumber);

	//     // 发送前递增序列号
	//     connection.sequenceNumber = (connection.sequenceNumber + 1) % 128;

	//     // 转换为字节数组发送
	//     var ByteArray = plus.android.importClass("java.io.ByteArrayOutputStream");
	//     var byteArrayStream = new ByteArray();

	//     for (let i = 0; i < frame.length; i++) {
	//       byteArrayStream.write(frame[i]);
	//     }

	//     var bytes = byteArrayStream.toByteArray();

	//     console.log(`发送消息到 ${ip},帧长度:`, frame.length, "字节");
	//     console.log("帧数据:", frame.map(b => b.toString(16).padStart(2, '0')).join(' '));
	//     console.log(`通道掩码: 0x${mask.toString(16).padStart(4, '0')} (十进制: ${mask})`);

	//     connection.dataOutputStream.write(bytes);
	//     connection.dataOutputStream.flush();
	//     console.log(`发送成功到 ${ip},下一序列号: ${connection.sequenceNumber}`);
	//     return true;
	//   } catch (e) {
	//     console.log(`发送失败 ${ip}:`, e.toString());
	//     // 发送失败,可能是连接已断开
	//     this._handleDisconnection(ip);
	//     return false;
	//   }
	// },
	// 发送通道关命令到指定IP(根据新文档格式)
	sendChannelClose(ip, src) {
		var connection = this.connections[ip];

		if (!connection || !connection.isConnected || !connection.dataOutputStream) {
			console.log(`发送失败 ${ip}: 未连接`);
			return false;
		}

		try {
			// 源地址为上位机 (0x80)
			const srcAddr = 0x80;

			// 目的地址根据信号源编号获取
			const destAddr = this.getSignalSourceAddr(src);

			console.log(`构建关闭干扰命令,目标地址: 0x${destAddr.toString(16).padStart(2, '0')}, 信号源: ${src}`);

			// 构建数据内容 - 小端序
			const mask = 0x03e0; // 关闭所有通道(bit0~bit9)
			const dataContent = [0x01, 0x29, mask & 0xFF, (mask >> 8) & 0xFF];

			// 使用连接的序列号构建完整帧
			const frame = this.buildFrame(destAddr, srcAddr, dataContent, connection.sequenceNumber);
			connection.sequenceNumber = (connection.sequenceNumber + 1) % 128; // 递增序列号

			// 转换为字节数组发送
			var ByteArray = plus.android.importClass("java.io.ByteArrayOutputStream");
			var byteArrayStream = new ByteArray();

			for (let i = 0; i < frame.length; i++) {
				byteArrayStream.write(frame[i]);
			}

			var bytes = byteArrayStream.toByteArray();

			console.log(`发送消息到 ${ip},帧长度:`, frame.length, "字节");
			console.log("帧数据:", frame.map(b => b.toString(16).padStart(2, '0')).join(' '));
			console.log(`通道掩码: 0x${mask.toString(16).padStart(4, '0')} (十进制: ${mask})`);

			connection.dataOutputStream.write(bytes);
			connection.dataOutputStream.flush();
			console.log(`发送成功到 ${ip}`);
			return true;
		} catch (e) {
			console.log(`发送失败 ${ip}:`, e.toString());
			// 发送失败,可能是连接已断开
			this._handleDisconnection(ip);
			return false;
		}
	},

	// 关闭指定连接
	closeConnection(ip) {
		var connection = this.connections[ip];

		if (!connection) return;

		connection.isConnected = false;

		try {
			if (connection.dataOutputStream) {
				connection.dataOutputStream.close();
				connection.dataOutputStream = null;
			}
			if (connection.dataInputStream) {
				connection.dataInputStream.close();
				connection.dataInputStream = null;
			}
			if (connection.socket) {
				connection.socket.close();
				connection.socket = null;
			}

			console.log(`TCP连接已关闭: ${ip}`);
		} catch (e) {
			console.log(`关闭异常 ${ip}:`, e.toString());
		}

		// 清理定时器
		if (connection.reconnectTimer) {
			clearTimeout(connection.reconnectTimer);
			connection.reconnectTimer = null;
		}
	},

	// 关闭所有连接
	closeAll() {
		Object.keys(this.connections).forEach(ip => {
			this.closeConnection(ip);
		});

		this.connections = {};
		console.log("所有TCP连接已关闭");
	},

	// 获取连接状态
	getConnectionStatus() {
		return this.connectionStatus;
	},

	// 手动重连指定IP
	reconnect(ip) {
		var connection = this.connections[ip];
		if (connection) {
			this.connect(ip, connection.port, null);
		}
	},

	// 状态监控
	_startStatusMonitor() {
		// 可以定期检查连接状态
	},

	// 连接状态变化回调
	_onConnectionStatusChange(ip, status, error) {
		console.log(`连接状态变化: ${ip} - ${status}`, error ? `错误: ${error}` : '');
	}
};
相关推荐
荒诞硬汉5 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国5 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882485 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
han_5 小时前
从一道前端面试题,谈 JS 对象存储特点和运算符执行顺序
前端·javascript·面试
aPurpleBerry5 小时前
React 01 目录结构、tsx 语法
前端·react.js
jayaccc6 小时前
微前端架构实战全解析
前端·架构
華勳全栈6 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_996 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹6 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理
专注_每天进步一点点6 小时前
【java开发】写接口文档的札记
java·开发语言