// net-server.js
|
const net = require('net');
|
const logger = require('./logger');
|
const { sendDataToAliyun } = require('./aliyun-iot');
|
const { loadConfig } = require('./config');
|
|
// 加载配置(端口、主机)
|
const CONFIG = loadConfig();
|
|
|
function getDateString() {
|
const now = new Date();
|
const localTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
|
.toISOString()
|
.slice(0, 19)
|
.replace('T', ' ');
|
|
return localTime;
|
}
|
/**
|
* 计算校验和
|
* @param {Buffer} data - 数据包的前12个字节
|
* @return {number} 校验和
|
*/
|
function calculateChecksum(data) {
|
try {
|
let sum = 0;
|
for (let i = 0; i < data.length; i++) {
|
sum += data[i];
|
}
|
return sum % 65536;
|
} catch (err) {
|
logger.error('计算校验和时发生错误', { 错误: err.message, 数据: data?.toString('hex') });
|
return 0;
|
}
|
}
|
|
/**
|
* 解析报警标志
|
* @param {number} alarmFlags - 报警标志字节
|
* @return {Array} 报警信息数组
|
*/
|
function parseAlarmFlags(alarmFlags) {
|
try {
|
const alarms = {
|
g: '0', // 漏血报警
|
ywbj: '0', // 液位报警
|
f: '0', // 气泡报警
|
p: '0', // 动脉压报警
|
e: '0', // 跨膜压报警
|
c: '0', // 静脉压报警
|
a: '0', // 温度透析液温度
|
b: '0', // 电导度
|
};
|
if (alarmFlags & 0x01) {
|
alarms.g='1'; // 漏血报警
|
}else{
|
alarms.g='0'; // 无漏血报警
|
}
|
if (alarmFlags & 0x02) {
|
alarms.ywbj='1'; // 液位报警
|
}
|
else{
|
alarms.ywbj='0'; // 无液位报警
|
}
|
if (alarmFlags & 0x04) {
|
alarms.f='1';// 气泡报警
|
}else{
|
alarms.f='0'; // 无气泡报警
|
}
|
if (alarmFlags & 0x08) {
|
alarms.p='1'; // 动脉压报警
|
}else{
|
alarms.p='0'; // 无动脉压报警
|
}
|
if (alarmFlags & 0x10) {
|
alarms.e='1'; // 跨膜压报警
|
}else{
|
alarms.e='0'; // 无跨膜压报警
|
}
|
if (alarmFlags & 0x20) {
|
alarms.c='1'; // 静脉压报警
|
};
|
if (alarmFlags & 0x40) {
|
alarms.a='1'; // 温度报警
|
}else{
|
alarms.a='0'; // 无温度报警
|
}
|
if (alarmFlags & 0x80) {
|
alarms.b='1'; // 电导度报警
|
}else{
|
alarms.b='0'; // 无电导度报警
|
}
|
return alarms;
|
} catch (err) {
|
logger.error('解析报警标志时发生错误', { 错误: err.message, alarmFlags });
|
return ['解析报警标志失败'];
|
}
|
}
|
|
/**
|
* 解析血透机数据包
|
* @param {Buffer} data - 数据包
|
*/
|
function parseDialysisData(data) {
|
try {
|
// 检查输入是否有效
|
if (!data || !Buffer.isBuffer(data)) {
|
logger.error("接收到的数据无效,非Buffer类型", { 数据类型: typeof data, 数据值: data });
|
return;
|
}
|
|
if (data.length !== 14) {
|
logger.error("数据长度无效。期望长度为14字节。", { 实际长度: data.length, 数据: data.toString('hex') });
|
return;
|
}
|
|
const offsets = {
|
start: [0, 2],
|
deviceType: [2, 1],
|
deviceId: [3, 3],
|
modeAndValue: [6, 1],
|
mainAlarmFlag: [7, 1],
|
otherAlarmFlag: [8, 1],
|
dataFlag: [9, 1],
|
dataValue: [10, 2],
|
checksum: [12, 2]
|
};
|
|
// 解析开始标志
|
const startMarker = data.slice(0, 2).toString('hex');
|
if (startMarker !== '55aa') {
|
logger.error("起始标志无效。期望值为 '55aa'。", { 起始标志: startMarker, 数据: data.toString('hex') });
|
return;
|
}
|
|
// 提取前12字节计算校验和
|
const dataForChecksum = data.slice(0, 12);
|
const receivedChecksum = data.readUInt16BE(12);
|
const calculatedChecksum = calculateChecksum(dataForChecksum);
|
|
if (receivedChecksum !== calculatedChecksum) {
|
logger.error(`校验和不匹配`, {
|
接收到的校验和: receivedChecksum,
|
计算出的校验和: calculatedChecksum,
|
数据: data.toString('hex')
|
});
|
return;
|
} else {
|
logger.info(`校验和验证成功: ${receivedChecksum}`);
|
}
|
|
// 开始解析字段(每个都加 try 防止崩溃)
|
let deviceType, deviceIdHex, modeAndValue, mainAlarmFlag, mainAlarmFlags,otherAlarmFlag, dataFlag, dataValue;
|
|
try {
|
deviceType = data.readUInt8(2);
|
logger.info(`设备类型: ${deviceType}`);
|
} catch (err) {
|
logger.error('解析设备类型失败', { 错误: err.message });
|
deviceType = null;
|
}
|
|
try {
|
deviceIdHex = data.slice(3, 6).toString('hex');
|
logger.info(`设备ID: ${deviceIdHex}`);
|
} catch (err) {
|
logger.error('解析设备ID失败', { 错误: err.message });
|
deviceIdHex = 'unknown';
|
}
|
|
try {
|
modeAndValue = data.readUInt8(6);
|
logger.info(`运行模式&权值: ${modeAndValue}`);
|
} catch (err) {
|
logger.error('解析运行模式&权值失败', { 错误: err.message });
|
modeAndValue = null;
|
}
|
|
try {
|
mainAlarmFlag = data.readUInt8(7);
|
logger.info(`主要报警标志: ${mainAlarmFlag}`);
|
const alarms = parseAlarmFlags(mainAlarmFlag);
|
mainAlarmFlags = alarms;
|
if (alarms.length > 0) {
|
logger.info(`当前报警信息:${alarms.join(', ')}`, { 报警信息: alarms });
|
} else {
|
logger.info('当前无报警');
|
}
|
} catch (err) {
|
logger.error('解析主要报警标志失败', { 错误: err.message });
|
mainAlarmFlag = 0;
|
}
|
|
try {
|
otherAlarmFlag = data.readUInt8(8);
|
logger.info(`其他报警标志: ${otherAlarmFlag}`);
|
} catch (err) {
|
logger.error('解析其他报警标志失败', { 错误: err.message });
|
otherAlarmFlag = null;
|
}
|
|
try {
|
dataFlag = data.readUInt8(9);
|
logger.info(`数据标识: ${dataFlag}`);
|
} catch (err) {
|
logger.error('解析数据标识失败', { 错误: err.message });
|
dataFlag = null;
|
}
|
|
try {
|
dataValue = data.readUInt16BE(10);
|
logger.info(`数据值: ${dataValue}`);
|
} catch (err) {
|
logger.error('解析数据值失败', { 错误: err.message });
|
dataValue = null;
|
}
|
|
// 构造 payload
|
const payload = {
|
deviceName:'威力生',
|
deviceType:deviceType, //设备类型 0是血透1 是血滤
|
n: deviceIdHex, //设备序列号 也就是id
|
RunningState: modeAndValue, // 运行模式&权值
|
h:otherAlarmFlag.toFixed(0),//其他报警
|
suedtime: getDateString(), // 建议加上时间戳
|
// g:0, // 漏血报警
|
// f:0,//气泡报警
|
// e:0,//跨膜压报警
|
// ywbj:0,//液位报警
|
// c:0,//静脉压报警
|
// p:0,//动脉压报警
|
// b:0,//电导度
|
// a:0,//温度透析液温度,
|
// h:0,//其他报警
|
// A:0,//脱水
|
// B:0,//当前脱水
|
// C:0,//脱水速率
|
// D:0,//血液流速 血流量
|
// L:0,//透析液流量
|
// fzl:0,//辅助泵流量
|
// zsq:0,//注射器
|
// F:0,//透析液温度
|
// G:0,//透析液电导度
|
// H:0,//静脉压
|
// J:0,//跨膜压
|
// o:0,//动脉压,
|
// ytsj:0,//已经透析时间
|
// sysj:0,//剩余时间
|
// ssy:0,//收缩压
|
// szy:0,//舒张压
|
// mb:0,//脉搏
|
|
};
|
|
// 发送数据到阿里云(异步,可能抛错)
|
try {
|
// console.log('报警数据:', mainAlarmFlags);
|
// console.log('其他数据:', otherAlarmFlag);
|
// console.log('运行或者是授权:', modeAndValue);
|
// console.log('数据标识:', dataFlag);
|
// console.log('数据值:', dataValue);
|
// console.log('发送到阿里云:', payload);
|
// 计算是运行模式还是小数点计位
|
if(modeAndValue<=4){
|
// 继位小数点
|
if(modeAndValue){
|
dataValue=(dataValue/10**modeAndValue).toFixed(modeAndValue)
|
}else{
|
dataValue=dataValue.toFixed(0)
|
}
|
if(dataFlag){
|
switch(dataFlag){
|
case 1:
|
payload.A=dataValue
|
break;
|
case 2:
|
payload.B=dataValue
|
break;
|
case 3:
|
payload.C=dataValue
|
break;
|
case 4:
|
payload.D=dataValue
|
break;
|
case 5:
|
payload.fzl=dataValue
|
break;
|
case 6:
|
payload.zsq=dataValue
|
break;
|
case 7:
|
payload.L=dataValue
|
break;
|
case 8:
|
payload.F=dataValue
|
break;
|
case 9:
|
payload.G=dataValue
|
break;
|
case 10:
|
payload.H=(Number(dataValue)-65536).toFixed(0)
|
break;
|
case 11:
|
payload.J=dataValue
|
break;
|
case 12:
|
payload.ytsj=dataValue
|
break;
|
case 13:
|
payload.sysj=dataValue
|
break;
|
case 14:
|
payload.o=dataValue
|
break;
|
case 15:
|
payload.N=dataValue
|
break;
|
case 16:
|
payload.O=dataValue
|
break;
|
case 17:
|
payload.P=dataValue
|
break;
|
default:
|
logger.warn('未知的数据标识', { dataFlag });
|
}
|
}
|
}else if(modeAndValue>=5){
|
payload.RunningState=modeAndValue
|
dataValue=dataValue.toFixed(0)
|
if(dataFlag){
|
switch(dataFlag){
|
case 1:
|
payload.A=dataValue
|
break;
|
case 2:
|
payload.B=dataValue
|
break;
|
case 3:
|
payload.C=dataValue
|
break;
|
case 4:
|
payload.D=dataValue
|
break;
|
case 5:
|
payload.fzl=dataValue
|
break;
|
case 6:
|
payload.zsq=dataValue
|
break;
|
case 7:
|
payload.L=dataValue
|
break;
|
case 8:
|
payload.F=dataValue
|
break;
|
case 9:
|
payload.G=dataValue
|
break;
|
case 10:
|
payload.H=(Number(dataValue)-65536).toFixed(0)
|
break;
|
case 11:
|
payload.J=dataValue
|
break;
|
case 12:
|
payload.ytsj=dataValue
|
break;
|
case 13:
|
payload.sysj=dataValue
|
break;
|
case 14:
|
payload.o=dataValue
|
break;
|
case 15:
|
payload.N=dataValue
|
break;
|
case 16:
|
payload.O=dataValue
|
break;
|
case 17:
|
payload.P=dataValue
|
break;
|
default:
|
logger.warn('未知的数据标识', { dataFlag });
|
}
|
}
|
}
|
|
const payloadData={...payload,...mainAlarmFlags}
|
|
console.log('最终发送到阿里云:', payloadData);
|
|
|
|
sendDataToAliyun(deviceIdHex, payloadData)
|
} catch (err) {
|
logger.error('调用 sendDataToAliyun 时发生同步错误', {
|
错误: err,
|
payload
|
});
|
}
|
|
} catch (err) {
|
logger.error('parseDialysisData 发生未预期错误', {
|
错误: err.message,
|
堆栈: err.stack,
|
数据: data?.toString?.('hex') || 'unknown'
|
});
|
}
|
}
|
|
/**
|
* 创建TCP服务器(带异常防护)
|
*/
|
function createServer() {
|
let server = null;
|
|
try {
|
server = net.createServer((socket) => {
|
const remote = `${socket.remoteAddress}:${socket.remotePort}`;
|
logger.info(`客户端已连接: ${remote}`);
|
|
// 设置超时和错误处理
|
socket.setTimeout(300 * 1000); // 300秒超时
|
socket.setNoDelay(true);
|
|
socket.on('data', (data) => {
|
try {
|
logger.info(`接收数据: ${remote}`, { 数据: data.toString('hex') });
|
parseDialysisData(data);
|
} catch (err) {
|
logger.error('处理客户端数据时发生异常', {
|
错误: err.message,
|
堆栈: err.stack,
|
远程地址: remote
|
});
|
}
|
});
|
|
socket.on('timeout', () => {
|
logger.warn(`客户端连接超时,关闭连接: ${remote}`);
|
socket.destroy();
|
});
|
|
socket.on('end', () => {
|
logger.info(`客户端断开连接: ${remote}`);
|
});
|
|
socket.on('error', (err) => {
|
logger.error(`客户端连接发生错误: ${err.message}`, {
|
远程地址: remote,
|
错误码: err.code
|
});
|
// 错误后 socket 可能已不可用,避免重复 destroy
|
try {
|
socket.destroy();
|
} catch (e) { /* 忽略 */ }
|
});
|
|
});
|
|
const { port: PORT, host: HOST } = CONFIG.server;
|
|
server.listen(PORT, HOST, () => {
|
logger.info(`✅ 血透机TCP服务器已启动,监听 ${HOST}:${PORT}`);
|
});
|
|
server.on('error', (err) => {
|
logger.error('服务器监听错误', {
|
错误: err.message,
|
堆栈: err.stack,
|
端口: PORT,
|
主机: HOST
|
});
|
|
// 如果是端口占用,尝试重启或退出
|
if (err.code === 'EADDRINUSE') {
|
logger.error(`端口 ${PORT} 被占用,请检查是否有其他进程占用。`);
|
// 可在此加入延迟重试逻辑
|
setTimeout(() => {
|
logger.info(`尝试重新启动服务器...`);
|
server.close(() => {
|
createServer(); // 递归重启
|
});
|
}, 5000);
|
}
|
});
|
|
server.on('close', () => {
|
logger.info('服务器已关闭');
|
});
|
|
} catch (err) {
|
logger.error('创建服务器时发生严重错误', {
|
错误: err.message,
|
堆栈: err.stack
|
});
|
}
|
}
|
|
/**
|
* 全局异常处理器(防止崩溃)
|
*/
|
function setupUncaughtExceptionHandler() {
|
// 捕获未捕获的异常
|
process.on('uncaughtException', (err) => {
|
logger.error('【严重】未捕获的异常', {
|
错误: err.message,
|
堆栈: err.stack
|
});
|
|
// 避免进程无限重启
|
setTimeout(() => {
|
logger.info('程序将在 3 秒后退出并尝试重启...');
|
process.exit(1); // 触发外部进程管理器(如 pm2)重启
|
}, 3000);
|
});
|
|
// 捕获未处理的 Promise 拒绝
|
process.on('unhandledRejection', (reason, promise) => {
|
logger.error('【警告】未处理的 Promise 拒绝', {
|
原因: reason?.message || reason,
|
堆栈: reason?.stack,
|
promise
|
});
|
// 不 exit,只记录
|
});
|
|
// SIGTERM 信号处理(优雅关闭)
|
process.on('SIGTERM', () => {
|
logger.info('收到 SIGTERM 信号,正在优雅关闭...');
|
process.exit(0);
|
});
|
|
// SIGINT (Ctrl+C)
|
process.on('SIGINT', () => {
|
logger.info('收到 SIGINT 信号,正在关闭...');
|
process.exit(0);
|
});
|
}
|
|
// 启动服务器(带全局保护)
|
setupUncaughtExceptionHandler();
|
|
// 使用 setInterval 或 PM2 等工具确保进程永生
|
// 例如:用 PM2 启动可自动重启
|
createServer();
|
|
logger.info('血透机服务已初始化,等待客户端连接...');
|