const net = require('net');
|
const iconv = require('iconv-lite');
|
const xml2js = require('xml2js');
|
const fs = require('fs');
|
const path = require('path');
|
const {
|
extractFramesFromUtf16leBuffer,
|
extractFramesFromText,
|
guessTextEncodingFromBuffer,
|
} = require('./framing');
|
|
// ============================================================================
|
// 解析主流程(index.js)
|
// A) 在线模式:TCP 收包 -> buffer 拼接 -> framing 分帧 -> parseAndPrintData 解析
|
// B) 离线模式:读取文件(--decode-file) -> 判定输入编码/形态 -> 分帧 -> 解析
|
// C) 解析结果:控制台打印 + 可选 onFrame 回调(供 gateway.js 做缓存与转发)
|
// ============================================================================
|
|
// ================= 配置区域 =================
|
const CONFIG = {
|
host: '127.0.0.1', // 【重要】请修改为您的透析机 IP 地址
|
port: 3021, // 3021: 自动推送模式
|
reconnectInterval: 5000 // 断线重连间隔 (毫秒)
|
};
|
|
// 允许命令行覆盖连接目标:node .\index.js --host 127.0.0.1 --port 3021
|
const hostIdx = process.argv.indexOf('--host');
|
if (hostIdx !== -1 && process.argv[hostIdx + 1]) {
|
CONFIG.host = process.argv[hostIdx + 1];
|
}
|
const portIdx = process.argv.indexOf('--port');
|
if (portIdx !== -1 && process.argv[portIdx + 1]) {
|
const p = Number(process.argv[portIdx + 1]);
|
if (!Number.isNaN(p) && p > 0) CONFIG.port = p;
|
}
|
|
// 参数映射表 (基于百特 Artis 协议文档)
|
// 如果文档中有新参数,请在此处添加
|
const PARAM_MAP = {
|
'0': '治疗阶段 (Phase)',
|
'1': '剩余时间 (Time Rem)',
|
'2': '超滤量 (UF Vol)',
|
'3': '超滤率 (UF Rate)',
|
'4': '电导率 (Cond)',
|
'5': '碳酸氢盐电导率 (Bic Cond)',
|
'6': '血流速 (Blood Flow)',
|
'7': '温度 (Temp)',
|
'8': '透析液流速 (Dialysate Flow)',
|
'9': '在线置换率 (OnLine Sub Rate)',
|
'10': '累计血容量 (Cum Blood Vol)',
|
'12': '跨膜压 (TMP Actual)',
|
'13': '静脉压 (Venous Pressure)',
|
'14': '动脉压 (Arterial Pressure)',
|
'32': '设定血流速 (Set Blood Flow)',
|
'34': '设定超滤量 (Set UF Vol)',
|
'100': '动脉压 (Art Pressure)', // 示例补充,需核对文档
|
'101': '静脉压 (Ven Pressure)' // 示例补充,需核对文档
|
};
|
|
let bufferCache = Buffer.alloc(0);
|
let client = null;
|
let reconnectTimer = null;
|
let reconnectScheduledAt = null;
|
|
const DEBUG = process.argv.includes('--debug');
|
const PRINT_RX = process.argv.includes('--print-rx');
|
const printRxMaxIdx = process.argv.indexOf('--print-rx-max');
|
const PRINT_RX_MAX = (printRxMaxIdx !== -1 && process.argv[printRxMaxIdx + 1])
|
? Math.max(0, Number(process.argv[printRxMaxIdx + 1]) || 0)
|
: 256;
|
|
const decodeFileIdx = process.argv.indexOf('--decode-file');
|
const DECODE_FILE = (decodeFileIdx !== -1 && process.argv[decodeFileIdx + 1])
|
? path.resolve(process.cwd(), process.argv[decodeFileIdx + 1])
|
: null;
|
|
function toHexPreview(buf, maxBytes = 32) {
|
if (!buf || buf.length === 0) return '';
|
const slice = buf.slice(0, Math.min(buf.length, maxBytes));
|
return slice.toString('hex').match(/../g).join(' ');
|
}
|
|
function printReceivedChunk(data) {
|
// 调试辅助:打印原始 chunk 的 hex + UTF16LE 视图,用于排查乱码/截断/粘包。
|
if (!PRINT_RX || !data || data.length === 0) return;
|
|
const max = PRINT_RX_MAX;
|
const showAll = max === 0;
|
const shown = showAll ? data : data.slice(0, Math.min(data.length, max));
|
const truncated = shown.length < data.length;
|
|
const hex = shown.toString('hex');
|
const hexPretty = hex ? hex.match(/../g).join(' ') : '';
|
|
let utf16Text = '';
|
try {
|
const evenLen = shown.length % 2 === 0 ? shown.length : shown.length - 1;
|
const utf16Buf = shown.slice(0, Math.max(0, evenLen));
|
utf16Text = iconv.decode(utf16Buf, 'utf16le').replace(/^\uFEFF/, '');
|
} catch (_) {
|
utf16Text = '[UTF-16LE 解码失败]';
|
}
|
|
console.log(`\n🧩 收包 chunk: ${data.length} bytes${truncated ? ` (仅显示前 ${shown.length} bytes)` : ''}`);
|
console.log(`HEX: ${hexPretty}`);
|
console.log('UTF16LE:');
|
console.log(utf16Text);
|
}
|
|
function extractLikelyHexBytesFromText(text) {
|
// Prefer long runs like: "3C 00 3F 00 ..." to avoid RTF/header noise.
|
const candidates = text.match(/[0-9A-Fa-f]{2}(?:\s+[0-9A-Fa-f]{2}){40,}/g);
|
if (candidates && candidates.length) {
|
let best = candidates[0];
|
for (const c of candidates) {
|
if (c.length > best.length) best = c;
|
}
|
const hex = best.split(/\s+/g).filter(Boolean).join('');
|
return hex;
|
}
|
|
// Fallback: strip anything not hex.
|
return text
|
.replace(/0x/gi, '')
|
.replace(/\\x/gi, '')
|
.replace(/[^0-9a-fA-F]/g, '');
|
}
|
|
function decodeHexTextToBytes(text) {
|
let cleaned = extractLikelyHexBytesFromText(text);
|
if (!cleaned || cleaned.length < 40) {
|
throw new Error('未在文件中提取到足够的 hex 字节(可能不是 hex 文本/或格式不支持)');
|
}
|
if (cleaned.length % 2 !== 0) {
|
console.warn('⚠️ hex 长度不是偶数(可能复制/截断),已丢弃最后 1 个半字节继续尝试解析');
|
cleaned = cleaned.slice(0, -1);
|
}
|
return Buffer.from(cleaned, 'hex');
|
}
|
|
function decodeFileAndParse(filePath) {
|
// 离线解析入口:支持 hex/bin/base64 文本,自动识别 utf16le/utf8。
|
if (!fs.existsSync(filePath)) {
|
console.error(`文件不存在: ${filePath}`);
|
process.exitCode = 1;
|
return;
|
}
|
|
const args = process.argv;
|
const forcedEncoding = args.includes('--utf16le') ? 'utf16le' : (args.includes('--utf8') ? 'utf8' : null);
|
const modeHex = args.includes('--hex');
|
const modeBin = args.includes('--bin');
|
const modeB64 = args.includes('--b64');
|
|
const raw = fs.readFileSync(filePath);
|
let bytes = raw;
|
let source = 'binary';
|
|
if (modeHex || (!modeBin && !modeB64)) {
|
// Default prefer hex for text-like inputs, especially RTF-wrapped hex dumps.
|
try {
|
const asText = raw.toString('utf8');
|
bytes = decodeHexTextToBytes(asText);
|
source = 'hex';
|
} catch (e) {
|
if (modeHex) throw e;
|
// If not forced, fall back to treating as binary.
|
bytes = raw;
|
source = 'binary';
|
}
|
} else if (modeB64) {
|
const asText = raw.toString('utf8').replace(/\s+/g, '');
|
bytes = Buffer.from(asText, 'base64');
|
source = 'base64';
|
}
|
|
const encoding = forcedEncoding || guessTextEncodingFromBuffer(bytes);
|
console.log(`输入: ${filePath} (source=${source}, bytes=${bytes.length}, encoding=${encoding})`);
|
|
if (encoding === 'utf16le') {
|
const { frames, remainderBuffer } = extractFramesFromUtf16leBuffer(bytes);
|
if (frames.length === 0) {
|
// 兜底:常见“抓包/复制截断”,没拿到 </ORU_R31>,但前面可能已包含完整 OBX 段。
|
let work = bytes;
|
if (work.length % 2 === 1) work = work.slice(0, work.length - 1);
|
const text = iconv.decode(work, 'utf16le').replace(/^\uFEFF/, '');
|
const hasRoot = text.includes('<ORU_R31') || text.includes('<ORF_R04');
|
const obxBlocks = text.match(/<OBX>[\s\S]*?<\/OBX>/g) || [];
|
if (hasRoot && obxBlocks.length > 0) {
|
console.log(`⚠️ 未找到完整帧(疑似截断),尝试从片段提取 ${obxBlocks.length} 个 OBX 解析`);
|
const wrapped = `<?xml version="1.0" encoding="UTF-8"?>\n<ORU_R31>\n <ORU_R31.OBSERVATION>\n${obxBlocks.join('\n')}\n </ORU_R31.OBSERVATION>\n</ORU_R31>`;
|
parseAndPrintData(wrapped);
|
return;
|
}
|
|
console.log('未找到 ORU_R31/ORF_R04 XML 完整帧');
|
if (DEBUG) console.log(`remainder=${remainderBuffer.length} bytes; head=${toHexPreview(bytes)}`);
|
return;
|
}
|
console.log(`找到 ${frames.length} 条 XML 帧`);
|
frames.forEach((xml, idx) => {
|
console.log(`\n===== Frame #${idx + 1} =====`);
|
parseAndPrintData(xml.replace(/^\uFEFF/, '').trim());
|
});
|
if (remainderBuffer && remainderBuffer.length) {
|
console.warn(`⚠️ 末尾还有 ${remainderBuffer.length} bytes 未形成完整帧(可能抓包截断/拼接不完整)`);
|
}
|
return;
|
}
|
|
const text = bytes.toString('utf8');
|
const { frames, remainderText } = extractFramesFromText(text);
|
if (frames.length === 0) {
|
const hasRoot = text.includes('<ORU_R31') || text.includes('<ORF_R04');
|
const obxBlocks = text.match(/<OBX>[\s\S]*?<\/OBX>/g) || [];
|
if (hasRoot && obxBlocks.length > 0) {
|
console.log(`⚠️ 未找到完整帧(疑似截断),尝试从片段提取 ${obxBlocks.length} 个 OBX 解析`);
|
const wrapped = `<?xml version="1.0" encoding="UTF-8"?>\n<ORU_R31>\n <ORU_R31.OBSERVATION>\n${obxBlocks.join('\n')}\n </ORU_R31.OBSERVATION>\n</ORU_R31>`;
|
parseAndPrintData(wrapped);
|
return;
|
}
|
|
console.log('未找到 ORU_R31/ORF_R04 XML 完整帧');
|
if (DEBUG) console.log(`remainderChars=${remainderText.length}`);
|
return;
|
}
|
console.log(`找到 ${frames.length} 条 XML 帧`);
|
frames.forEach((xml, idx) => {
|
console.log(`\n===== Frame #${idx + 1} =====`);
|
parseAndPrintData(xml.replace(/^\uFEFF/, '').trim());
|
});
|
if (remainderText && remainderText.length) {
|
console.warn(`⚠️ 末尾还有 ${remainderText.length} chars 未形成完整帧(可能抓包截断/拼接不完整)`);
|
}
|
}
|
|
// ================= 数据收集(可选) =================
|
const COLLECT = {
|
enabled: process.argv.includes('--collect'),
|
outFile: path.resolve(__dirname, 'artis_frames.ndjson'),
|
};
|
|
const outIdx = process.argv.indexOf('--out');
|
if (outIdx !== -1 && process.argv[outIdx + 1]) {
|
COLLECT.outFile = path.resolve(process.cwd(), process.argv[outIdx + 1]);
|
}
|
|
let collectStream = null;
|
function ensureCollector() {
|
if (!COLLECT.enabled) return;
|
if (collectStream) return;
|
collectStream = fs.createWriteStream(COLLECT.outFile, { flags: 'a' });
|
console.log(`🧾 收集已启用:${COLLECT.outFile}`);
|
}
|
|
function recordFrame(frameObj) {
|
if (!COLLECT.enabled) return;
|
ensureCollector();
|
try {
|
collectStream.write(`${JSON.stringify(frameObj)}\n`);
|
} catch (e) {
|
console.warn('⚠️ 写入收集文件失败:', e && e.message ? e.message : String(e));
|
}
|
}
|
|
// ================= 主逻辑 =================
|
|
function startClient() {
|
// 在线 TCP 入口:连接透析机,接收实时推送并持续解析。
|
if (reconnectTimer) {
|
clearTimeout(reconnectTimer);
|
reconnectTimer = null;
|
reconnectScheduledAt = null;
|
}
|
|
if (client) {
|
client.destroy();
|
}
|
|
client = new net.Socket();
|
client.setKeepAlive(true, 10_000);
|
|
console.log(`\n🔄 正在连接百特 Artis 透析机 (${CONFIG.host}:${CONFIG.port})...`);
|
|
client.connect(CONFIG.port, CONFIG.host, () => {
|
console.log('✅ 连接成功!等待数据推送...\n');
|
|
// 可选:开启收集器
|
ensureCollector();
|
});
|
|
client.on('end', () => {
|
console.log('📴 远端已结束连接 (FIN)');
|
});
|
|
client.on('timeout', () => {
|
console.log('⏱️ Socket 超时');
|
try { client.destroy(); } catch (_) {}
|
});
|
|
// 接收数据
|
client.on('data', (data) => {
|
printReceivedChunk(data);
|
|
// 1. 将新数据拼接到缓存区 (处理 TCP 分包/粘包)
|
bufferCache = Buffer.concat([bufferCache, data]);
|
|
// 2. 从原始字节流中提取出尽可能多的完整 XML 帧(处理粘包/半包/多帧连发)
|
try {
|
const { frames, remainderBuffer } = extractFramesFromUtf16leBuffer(bufferCache);
|
bufferCache = remainderBuffer;
|
|
if (DEBUG) {
|
console.log(`📥 收到 ${data.length} bytes;本次提取 ${frames.length} 帧;缓存剩余 ${bufferCache.length} bytes;head=${toHexPreview(data)}`);
|
}
|
|
frames.forEach((xmlStr) => {
|
const normalized = xmlStr.replace(/^\uFEFF/, '').trim();
|
parseAndPrintData(normalized, { onFrame: recordFrame });
|
});
|
} catch (err) {
|
if (DEBUG) {
|
console.log(`📥 收到 ${data.length} bytes;当前缓存 ${bufferCache.length} bytes;尚未形成完整帧;head=${toHexPreview(data)}`);
|
}
|
// 保持缓存,等待后续数据补齐。
|
}
|
});
|
|
client.on('error', (err) => {
|
console.error(`❌ 连接错误: ${err.message}`);
|
handleDisconnect();
|
});
|
|
client.on('close', (hadError) => {
|
console.log(`🔌 连接已断开${hadError ? ' (伴随错误)' : ''}`);
|
handleDisconnect();
|
});
|
}
|
|
// 处理断开重连
|
function handleDisconnect() {
|
if (reconnectTimer) return;
|
reconnectScheduledAt = Date.now();
|
console.log(`⏳ ${CONFIG.reconnectInterval/1000}秒后尝试重连...`);
|
reconnectTimer = setTimeout(() => {
|
reconnectTimer = null;
|
reconnectScheduledAt = null;
|
startClient();
|
}, CONFIG.reconnectInterval);
|
}
|
|
// 解析 XML 并打印
|
function parseAndPrintData(xmlStr, options = {}) {
|
// 解析策略:
|
// 1) 先走 xml2js 严格解析(结构化准确)
|
// 2) 失败则走 OBX 片段兜底(容忍非严格/损坏 XML)
|
// 3) 最终产出 params + latest,并可通过 onFrame 回调给上层网关
|
const parser = new xml2js.Parser({
|
explicitArray: false, // 如果只有一个节点,不强制转为数组
|
mergeAttrs: false // 属性单独处理
|
});
|
|
const unescapeXmlText = (s) => {
|
if (s == null) return '';
|
return String(s)
|
.replace(/</g, '<')
|
.replace(/>/g, '>')
|
.replace(/&/g, '&')
|
.replace(/"/g, '"')
|
.replace(/'/g, "'");
|
};
|
|
const extractMetaFromXmlText = (text) => {
|
const meta = {
|
device: '',
|
timestamp: '',
|
};
|
|
// MSH.4 is often SW version + SN, e.g. SW_8.60.02_SN_34856
|
const msh4 = pickFirst(text, [
|
/<MSH\.4>[\s\S]*?<HD\.1>([^<]*)<\/HD\.1>/i,
|
/<MSH\.4>\s*([^<\s][^<]*)\s*<\/MSH\.4>/i,
|
]);
|
const msh3 = pickFirst(text, [
|
/<MSH\.3>[\s\S]*?<HD\.1>([^<]*)<\/HD\.1>/i,
|
/<MSH\.3>\s*([^<\s][^<]*)\s*<\/MSH\.3>/i,
|
]);
|
meta.device = msh4 || msh3;
|
|
meta.timestamp = pickFirst(text, [
|
/<MSH\.7>[\s\S]*?<TS\.1>([^<]*)<\/TS\.1>/i,
|
/<MSH\.7>\s*([^<\s][^<]*)\s*<\/MSH\.7>/i,
|
]);
|
|
return meta;
|
};
|
|
const pickFirst = (text, patterns) => {
|
for (const re of patterns) {
|
const m = text.match(re);
|
if (m && m[1] != null) return unescapeXmlText(m[1]).trim();
|
}
|
return '';
|
};
|
|
const parseObxBlockLoosely = (obxXml) => {
|
const paramId = pickFirst(obxXml, [
|
/<OBX\.3>[\s\S]*?<CE\.1>([^<]*)<\/CE\.1>/i,
|
/<OBX\.3>\s*([^<\s][^<]*)\s*<\/OBX\.3>/i,
|
]);
|
if (!paramId) return null;
|
|
const paramNameRaw = pickFirst(obxXml, [
|
/<OBX\.3>[\s\S]*?<CE\.2>([^<]*)<\/CE\.2>/i,
|
]) || null;
|
|
const value = pickFirst(obxXml, [
|
/<OBX\.5>[\s\S]*?<FN\.1>([^<]*)<\/FN\.1>/i,
|
/<OBX\.5>[\s\S]*?<ST>([^<]*)<\/ST>/i,
|
/<OBX\.5>\s*([^<\s][^<]*)\s*<\/OBX\.5>/i,
|
]);
|
|
const unit = pickFirst(obxXml, [
|
/<OBX\.6>[\s\S]*?<CE\.1>([^<]*)<\/CE\.1>/i,
|
/<OBX\.6>[\s\S]*?<CE\.2>([^<]*)<\/CE\.2>/i,
|
/<OBX\.6>\s*([^<\s][^<]*)\s*<\/OBX\.6>/i,
|
]);
|
|
const displayName = PARAM_MAP[String(paramId)] || paramNameRaw || `未知参数(${paramId})`;
|
return {
|
id: String(paramId),
|
name: displayName,
|
rawName: paramNameRaw || undefined,
|
value: value,
|
unit: unit,
|
};
|
};
|
|
const salvageObxAndPrint = () => {
|
// 非严格 XML 兜底解析:直接正则提取 <OBX>...</OBX>,尽可能保留业务数据。
|
const obxBlocks = xmlStr.match(/<OBX>[\s\S]*?<\/OBX>/g) || [];
|
if (obxBlocks.length === 0) return false;
|
|
const meta = extractMetaFromXmlText(xmlStr);
|
|
const receivedAt = new Date().toISOString();
|
const messageType = xmlStr.includes('<ORF_R04') ? 'ORF_R04' : 'ORU_R31';
|
const params = [];
|
const latest = {};
|
|
for (const block of obxBlocks) {
|
const p = parseObxBlockLoosely(block);
|
if (!p) continue;
|
params.push(p);
|
latest[p.id] = p;
|
}
|
|
if (params.length === 0) return false;
|
|
console.warn(`⚠️ XML 非严格/损坏,已从片段提取 ${params.length}/${obxBlocks.length} 个 OBX 兜底解析`);
|
console.log('--- 📊 收到新数据帧 ---');
|
if (meta.device || meta.timestamp) {
|
console.log(` 设备: ${meta.device || '--'} | 时间: ${meta.timestamp || '--'}`);
|
}
|
params.forEach(p => {
|
const unitStr = (p.unit && p.unit !== '-' && p.unit !== 'NULL') ? ` ${p.unit}` : '';
|
console.log(` [${p.id}] ${p.name}: ${p.value}${unitStr}`);
|
});
|
|
if (typeof options.onFrame === 'function') {
|
options.onFrame({
|
receivedAt,
|
messageType,
|
device: meta.device || undefined,
|
timestamp: meta.timestamp || undefined,
|
params,
|
latest,
|
});
|
}
|
console.log('------------------------\n');
|
return true;
|
};
|
|
const asArray = (val) => {
|
if (!val) return [];
|
return Array.isArray(val) ? val : [val];
|
};
|
|
const collectObxFromObservationGroup = (obsGroup, target) => {
|
asArray(obsGroup).forEach(item => {
|
if (!item) return;
|
const obx = item.OBX;
|
if (!obx) return;
|
target.push(...asArray(obx));
|
});
|
};
|
|
parser.parseString(xmlStr, (err, result) => {
|
if (err) {
|
// 兜底:部分设备/抓包数据存在重复/缺失标签,导致不是严格 XML。
|
// 若帧内有完整 OBX 段,则仍可提取参数。
|
if (!options.__salvaged && salvageObxAndPrint()) return;
|
|
console.error('❌ XML 解析失败:', err.message);
|
return;
|
}
|
|
console.log('--- 📊 收到新数据帧 ---');
|
|
// 兼容 ORU (自动推送) 和 ORF (查询响应)
|
const root = result.ORU_R31 || result.ORF_R04;
|
|
if (!root) {
|
console.log('⚠️ 无法识别根节点,可能是非标准消息');
|
return;
|
}
|
|
// 提取 OBX 数据段
|
// 结构路径通常是: MSH -> OBR -> [OBSERVATION] -> OBX
|
// 注意:xml2js 的结构取决于具体 XML 层级,这里做通用提取
|
|
let obxItems = [];
|
|
// 优先:与你的协议文档示例一致(OBSERVATION 为根节点的直接子节点)
|
collectObxFromObservationGroup(root['ORU_R31.OBSERVATION'], obxItems);
|
collectObxFromObservationGroup(root['ORF_R04.OBSERVATION'], obxItems);
|
|
// 尝试从 OBR 下的 OBSERVATION 组获取
|
asArray(root.OBR).forEach(obr => {
|
if (!obr) return;
|
const obsGroup = obr['ORU_R31.OBSERVATION'] || obr['ORF_R04.OBSERVATION'];
|
collectObxFromObservationGroup(obsGroup, obxItems);
|
});
|
|
// 如果没有找到,尝试直接在根节点下找 (某些简化结构)
|
if (obxItems.length === 0 && root.OBX) {
|
obxItems = asArray(root.OBX);
|
}
|
|
if (obxItems.length === 0) {
|
console.log('ℹ️ 消息中未找到 OBX 数据段');
|
return;
|
}
|
|
const receivedAt = new Date().toISOString();
|
const messageType = result.ORU_R31 ? 'ORU_R31' : 'ORF_R04';
|
const msh = root.MSH;
|
const meta = {
|
device: (msh && msh['MSH.4'] && msh['MSH.4']['HD.1']) ? msh['MSH.4']['HD.1']
|
: ((msh && msh['MSH.3'] && msh['MSH.3']['HD.1']) ? msh['MSH.3']['HD.1'] : ''),
|
timestamp: (msh && msh['MSH.7'] && msh['MSH.7']['TS.1']) ? msh['MSH.7']['TS.1'] : '',
|
};
|
|
if (meta.device || meta.timestamp) {
|
console.log(` 设备: ${meta.device || '--'} | 时间: ${meta.timestamp || '--'}`);
|
}
|
const params = [];
|
const latest = {};
|
|
// 遍历并打印每个参数
|
obxItems.forEach(obx => {
|
// 1. 获取参数 ID (OBX.3)
|
// 结构可能是 CE (CE.1=ID, CE.2=Name) 或直接是字符串
|
const idObj = obx['OBX.3'];
|
let paramId = null;
|
let paramNameRaw = null;
|
|
if (idObj && typeof idObj === 'object') {
|
paramId = idObj['CE.1'];
|
paramNameRaw = idObj['CE.2'];
|
} else if (idObj) {
|
paramId = idObj;
|
}
|
|
if (!paramId) return;
|
|
// 2. 获取中文名称 (查表 或 使用原始名)
|
const displayName = PARAM_MAP[paramId] || paramNameRaw || `未知参数(${paramId})`;
|
|
// 3. 获取数值 (OBX.5)
|
// 结构可能是 FN (FN.1=Value), ST, 或直接值
|
const valObj = obx['OBX.5'];
|
let value = '';
|
if (valObj && typeof valObj === 'object') {
|
value = valObj['FN.1'] || valObj['ST'] || JSON.stringify(valObj);
|
} else if (valObj) {
|
value = valObj;
|
}
|
|
// 4. 获取单位 (OBX.6)
|
const unitObj = obx['OBX.6'];
|
let unit = '';
|
if (unitObj && typeof unitObj === 'object') {
|
unit = unitObj['CE.1'] || unitObj['CE.2'];
|
} else if (unitObj) {
|
unit = unitObj;
|
}
|
|
// 格式化输出
|
const unitStr = (unit && unit !== '-' && unit !== 'NULL') ? ` ${unit}` : '';
|
console.log(` [${paramId}] ${displayName}: ${value}${unitStr}`);
|
|
const paramObj = {
|
id: String(paramId),
|
name: displayName,
|
rawName: paramNameRaw || undefined,
|
value: value,
|
unit: unit,
|
};
|
params.push(paramObj);
|
latest[String(paramId)] = paramObj;
|
});
|
|
if (typeof options.onFrame === 'function') {
|
options.onFrame({
|
receivedAt,
|
messageType,
|
device: meta.device || undefined,
|
timestamp: meta.timestamp || undefined,
|
params,
|
latest,
|
});
|
}
|
|
console.log('------------------------\n');
|
});
|
}
|
|
// 启动
|
if (require.main === module) {
|
if (DECODE_FILE) {
|
try {
|
decodeFileAndParse(DECODE_FILE);
|
} catch (e) {
|
console.error(`解码失败: ${e && e.message ? e.message : String(e)}`);
|
process.exitCode = 1;
|
}
|
} else {
|
startClient();
|
}
|
}
|
|
module.exports = {
|
parseAndPrintData,
|
};
|