在工业控制领域,基于 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 通信协议的完整实现,核心亮点包括:
- 高可靠性:实现自动重连、非阻塞读取、异常容错等机制,保证连接稳定性;
- 协议合规性:严格遵循自定义帧格式,包含 CRC 校验、DLE 转义、帧确认等核心机制;
- 易用性:提供简洁的调用接口,支持多 IP 批量管理、命令一键发送;
- 可扩展性:模块化设计,便于后续新增命令类型、扩展协议字段。
该实现方案不仅适用于干扰设备通信,也可作为工业级 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}` : '');
}
};