const net = require('net');
|
const { JhmDecoder } = require('./decoder');
|
|
function normalizeIp(address) {
|
if (!address) {
|
return '';
|
}
|
|
if (address.startsWith('::ffff:')) {
|
return address.slice(7);
|
}
|
|
return address;
|
}
|
|
function formatReceivedTimestamp(date = new Date()) {
|
const year = date.getFullYear();
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
const hour = String(date.getHours()).padStart(2, '0');
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
const second = String(date.getSeconds()).padStart(2, '0');
|
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
}
|
|
class TcpService {
|
constructor(options) {
|
this.tcpConfig = options.tcpConfig;
|
this.devices = options.devices || [];
|
this.alModelPath = options.alModelPath;
|
this.publishBloodPressureTime = options.publishBloodPressureTime !== false;
|
this.logger = options.logger || console;
|
this.onMetric = typeof options.onMetric === 'function' ? options.onMetric : () => {};
|
this.server = null;
|
this.sessions = new Map();
|
this.deviceMap = this.buildDeviceMap(this.devices);
|
}
|
|
buildDeviceMap(devices) {
|
const map = new Map();
|
|
for (const device of devices) {
|
if (!device || !device.ip || !device.deviceId) {
|
continue;
|
}
|
|
map.set(normalizeIp(device.ip), {
|
...device,
|
name: device.name || device['备注'] || device.deviceId,
|
});
|
}
|
|
return map;
|
}
|
|
getReceivedMetricTime() {
|
return formatReceivedTimestamp();
|
}
|
|
async start() {
|
this.server = net.createServer((socket) => {
|
this.handleConnection(socket);
|
});
|
|
this.server.maxConnections = this.tcpConfig.maxConnections || 100;
|
|
this.server.on('error', (error) => {
|
this.logger.error(`[TCP] 服务错误: ${error.message}`);
|
});
|
|
await new Promise((resolve, reject) => {
|
this.server.once('listening', resolve);
|
this.server.once('error', reject);
|
this.server.listen({
|
host: this.tcpConfig.host,
|
port: this.tcpConfig.port,
|
backlog: this.tcpConfig.backlog || 128,
|
});
|
});
|
|
this.logger.info(`[TCP] 已监听 ${this.tcpConfig.host}:${this.tcpConfig.port} devices=${this.deviceMap.size} maxConnections=${this.server.maxConnections}`);
|
}
|
|
handleConnection(socket) {
|
const clientIp = normalizeIp(socket.remoteAddress);
|
const device = this.deviceMap.get(clientIp);
|
|
if (!device) {
|
this.logger.warn(`[TCP] 未配置设备接入,ip=${clientIp} port=${socket.remotePort},连接将被关闭`);
|
socket.destroy();
|
return;
|
}
|
|
const decoder = new JhmDecoder({
|
alModelPath: this.alModelPath,
|
maxBufferBytes: this.tcpConfig.maxBufferBytes || 8192,
|
});
|
|
this.sessions.set(socket, {
|
clientIp,
|
device,
|
decoder,
|
});
|
|
if (this.tcpConfig.keepAlive) {
|
socket.setKeepAlive(true, this.tcpConfig.keepAliveDelayMs || 10000);
|
}
|
|
socket.setNoDelay(Boolean(this.tcpConfig.noDelay));
|
socket.setTimeout(this.tcpConfig.socketTimeoutMs || 120000);
|
|
this.logger.info(`[TCP] 设备已连接 deviceId=${device.deviceId} ip=${clientIp} port=${socket.remotePort} activeSessions=${this.sessions.size}`);
|
|
socket.on('data', (chunk) => {
|
this.handleData(socket, chunk);
|
});
|
|
socket.on('timeout', () => {
|
this.logger.warn(`[TCP] 连接超时 deviceId=${device.deviceId} ip=${clientIp}`);
|
socket.destroy();
|
});
|
|
socket.on('error', (error) => {
|
this.logger.error(`[TCP] 连接异常 deviceId=${device.deviceId} ip=${clientIp}: ${error.message}`);
|
});
|
|
socket.on('close', () => {
|
this.sessions.delete(socket);
|
this.logger.info(`[TCP] 设备已断开 deviceId=${device.deviceId} ip=${clientIp} activeSessions=${this.sessions.size}`);
|
});
|
}
|
|
handleData(socket, chunk) {
|
const session = this.sessions.get(socket);
|
|
if (!session) {
|
return;
|
}
|
|
this.logger.info(`[TCP] 收到原始数据 deviceId=${session.device.deviceId} bytes=${chunk.length}`);
|
|
const results = session.decoder.push(chunk);
|
|
if (results.length === 0) {
|
this.logger.warn(`[TCP] 当前数据片段未形成完整报文 deviceId=${session.device.deviceId} bytes=${chunk.length}`);
|
}
|
|
for (const result of results) {
|
if (!result.publish) {
|
if (!result.ok) {
|
this.logger.warn(`[TCP] 丢弃无效报文 deviceId=${session.device.deviceId} reason=${result.reason}`);
|
} else {
|
this.logger.warn(`[TCP] 跳过未发布报文 deviceId=${session.device.deviceId} reason=${result.reason || 'publish-disabled'} raw=${result.rawHex || ''}`);
|
}
|
continue;
|
}
|
|
if (result.protocol === 'blood-pressure' && this.publishBloodPressureTime) {
|
result.metric = {
|
...result.metric,
|
M: this.getReceivedMetricTime(),
|
};
|
}
|
|
this.logger.info(`[TCP] 收到指标 deviceId=${session.device.deviceId} metric=${JSON.stringify(result.metric)} raw=${result.rawHex}`);
|
this.onMetric(session.device, result.metric, result);
|
}
|
}
|
|
async stop() {
|
this.logger.info(`[TCP] 开始停止 TCP 服务 activeSessions=${this.sessions.size}`);
|
|
for (const socket of this.sessions.keys()) {
|
socket.destroy();
|
}
|
|
this.sessions.clear();
|
|
if (!this.server) {
|
return;
|
}
|
|
await new Promise((resolve) => {
|
this.server.close(resolve);
|
});
|
|
this.server = null;
|
this.logger.info('[TCP] TCP 服务已停止');
|
}
|
}
|
|
module.exports = {
|
formatReceivedTimestamp,
|
TcpService,
|
normalizeIp,
|
};
|