gx
chenyc
5 天以前 747a86bb94006aaca721cfc0c0ce7061643a9ea6
tcp-simulator.js
@@ -21,6 +21,7 @@
  },
};
// 模拟器支持三种方式:文件回放、单独血压、文件加血压混发。
function parseArgs(argv) {
  const options = {
    host: '127.0.0.1',
@@ -31,6 +32,7 @@
    repeat: 0,
    localAddress: '',
    mode: 'file',
    includeBloodPressure: false,
    bpSystolic: 120,
    bpDiastolic: 80,
    bpPulse: 89,
@@ -66,6 +68,8 @@
    } else if (arg === '--mode' && value) {
      options.mode = value.trim().toLowerCase();
      index += 1;
    } else if (arg === '--with-blood-pressure' || arg === '--with-bp') {
      options.includeBloodPressure = true;
    } else if (arg === '--blood-pressure' || arg === '--bp') {
      options.mode = 'blood-pressure';
    } else if (arg === '--bp-systolic' && value) {
@@ -91,30 +95,33 @@
}
function printHelp() {
  console.log(`JHM TCP simulator
  console.log(`JHM TCP 模拟客户端
Usage:
用法:
  node tcp-simulator.js --host 127.0.0.1 --port 9000
  node tcp-simulator.js --scenario jihua20260414 --host 127.0.0.1 --port 9000 --repeat 1
  node tcp-simulator.js --host 127.0.0.1 --port 9000 --with-bp
  node tcp-simulator.js --bp --host 127.0.0.1 --port 9000 --bp-systolic 120 --bp-diastolic 80 --bp-pulse 89
  node tcp-simulator.js --bp --host 127.0.0.1 --port 9000 --bp-time "2026-04-15 09:30"
Options:
  --host <ip>             target TCP server, default 127.0.0.1
  --port <number>         target TCP port, default 9000
  --interval <ms>         interval between frames, default 1000ms
  --frames <path>         hex frame file, default ./数据模拟.md
  --scenario <name>       built-in scenario: ${Object.keys(BUILTIN_SCENARIOS).join(', ')}
  --repeat <number>       send rounds, 0 means infinite loop, default 0
  --local-address <ip>    bind local IP to simulate source device IP
参数:
  --host <ip>             目标 TCP 服务地址,默认 127.0.0.1
  --port <number>         目标 TCP 服务端口,默认 9000
  --interval <ms>         报文发送间隔,默认 1000ms
  --frames <path>         十六进制报文文件,默认 ./数据模拟.md
  --scenario <name>       内置场景:${Object.keys(BUILTIN_SCENARIOS).join(', ')}
  --repeat <number>       发送轮数,0 表示无限循环,默认 0
  --local-address <ip>    绑定本地 IP,用于模拟设备来源 IP
  --mode <file|blood-pressure>
  --blood-pressure, --bp  shorthand for --mode blood-pressure
  --bp-systolic <n>       blood pressure systolic value, default 120
  --bp-diastolic <n>      blood pressure diastolic value, default 80
  --bp-pulse <n>          blood pressure pulse value, default 89
  --bp-time "<text>"      custom time, format YYYY-MM-DD HH:mm, default current local time
  --bp-no-time            send zeroed time bytes
  --help, -h              show help
  --with-blood-pressure, --with-bp
                          在每轮文件或场景回放后追加 1 条血压报文
  --blood-pressure, --bp  等同于 --mode blood-pressure
  --bp-systolic <n>       收缩压,默认 120
  --bp-diastolic <n>      舒张压,默认 80
  --bp-pulse <n>          脉搏,默认 89
  --bp-time "<text>"      自定义时间,格式 YYYY-MM-DD HH:mm,默认当前本地时间
  --bp-no-time            发送全 0 时间字节
  --help, -h              显示帮助
`);
}
@@ -126,7 +133,7 @@
  const scenarioConfig = BUILTIN_SCENARIOS[options.scenario];
  if (!scenarioConfig) {
    throw new Error(`unknown scenario: ${options.scenario}`);
    throw new Error(`未知场景: ${options.scenario}`);
  }
  return {
@@ -137,45 +144,49 @@
  };
}
function validateOptions(options) {
  if (!options.host) {
    throw new Error('host is required');
  }
  if (!Number.isInteger(options.port) || options.port <= 0) {
    throw new Error('port must be a positive integer');
  }
  if (!Number.isInteger(options.intervalMs) || options.intervalMs <= 0) {
    throw new Error('interval must be a positive integer in ms');
  }
  if (!Number.isInteger(options.repeat) || options.repeat < 0) {
    throw new Error('repeat must be an integer >= 0');
  }
  if (!['file', 'blood-pressure'].includes(options.mode)) {
    throw new Error(`unsupported mode: ${options.mode}`);
  }
  if (options.mode === 'file' && !fs.existsSync(options.framesFile)) {
    throw new Error(`frames file not found: ${options.framesFile}`);
  }
  if (options.mode === 'blood-pressure') {
function validateBloodPressureOptions(options) {
    for (const [name, value] of [
      ['bp-systolic', options.bpSystolic],
      ['bp-diastolic', options.bpDiastolic],
      ['bp-pulse', options.bpPulse],
    ]) {
      if (!Number.isInteger(value) || value < 0 || value > 0xFFFF) {
        throw new Error(`${name} must be an integer between 0 and 65535`);
      throw new Error(`${name} 必须是 0 到 65535 之间的整数`);
      }
    }
    if (options.bpIncludeTime && options.bpTime) {
      parseBloodPressureTimeText(options.bpTime);
    }
}
function validateOptions(options) {
  if (!options.host) {
    throw new Error('必须指定 host');
  }
  if (!Number.isInteger(options.port) || options.port <= 0) {
    throw new Error('port 必须是正整数');
  }
  if (!Number.isInteger(options.intervalMs) || options.intervalMs <= 0) {
    throw new Error('interval 必须是大于 0 的毫秒整数');
  }
  if (!Number.isInteger(options.repeat) || options.repeat < 0) {
    throw new Error('repeat 必须是大于等于 0 的整数');
  }
  if (!['file', 'blood-pressure'].includes(options.mode)) {
    throw new Error(`不支持的模式: ${options.mode}`);
  }
  if (options.mode === 'file' && !fs.existsSync(options.framesFile)) {
    throw new Error(`未找到报文文件: ${options.framesFile}`);
  }
  if (options.mode === 'blood-pressure' || options.includeBloodPressure) {
    validateBloodPressureOptions(options);
  }
}
@@ -193,7 +204,7 @@
  return Buffer.from(hexPairs.map((pair) => {
    if (pair.length > 2) {
      throw new Error(`invalid byte: ${pair}`);
      throw new Error(`非法字节: ${pair}`);
    }
    return Number.parseInt(pair, 16);
@@ -264,7 +275,7 @@
    };
  }
  throw new Error('no sendable hex frames found in frames file');
  throw new Error('报文文件中没有找到可发送的十六进制报文');
}
function analyzeFrames(frames, scenarioConfig) {
@@ -329,7 +340,7 @@
  const match = /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})$/.exec(value);
  if (!match) {
    throw new Error(`invalid --bp-time format: ${value}. Expected YYYY-MM-DD HH:mm`);
    throw new Error(`--bp-time 格式不正确: ${value},应为 YYYY-MM-DD HH:mm`);
  }
  const [, year, month, day, hour, minute] = match;
@@ -393,10 +404,13 @@
  }
  const loadResult = loadFrames(options.framesFile);
  const analysis = analyzeFrames(loadResult.frames, options.scenarioConfig);
  const frames = options.includeBloodPressure
    ? [...loadResult.frames, buildBloodPressureFrame(options)]
    : loadResult.frames;
  const analysis = analyzeFrames(frames, options.scenarioConfig);
  return {
    frames: loadResult.frames,
    frames,
    analysis,
    loadMode: loadResult.mode,
    scenarioConfig: options.scenarioConfig,
@@ -444,7 +458,7 @@
  function sendNextFrame() {
    const frame = prepared.frames[frameIndex];
    socket.write(frame.buffer);
    console.log(`[SIM] sent round=${round + 1} frame=${frameIndex + 1} -> ${frame.raw}`);
    console.log(`[SIM] 已发送 round=${round + 1} frame=${frameIndex + 1} -> ${frame.raw}`);
    frameIndex += 1;
@@ -453,43 +467,48 @@
      round += 1;
      if (options.repeat > 0 && round >= options.repeat) {
        stop(`[SIM] finished ${options.repeat} round(s)`);
        stop(`[SIM] 已完成 ${options.repeat} 轮发送`);
      }
    }
  }
  socket.on('connect', () => {
    console.log(`[SIM] connected to ${options.host}:${options.port}`);
    console.log(`[SIM] 已连接到 ${options.host}:${options.port}`);
    if (options.localAddress) {
      console.log(`[SIM] local bind address ${options.localAddress}`);
      console.log(`[SIM] 本地绑定地址 ${options.localAddress}`);
    }
    if (prepared.scenarioConfig) {
      console.log(`[SIM] scenario ${options.scenario} -> ${prepared.scenarioConfig.name}`);
      console.log(`[SIM] scenario description: ${prepared.scenarioConfig.description}`);
      console.log(`[SIM] 场景 ${options.scenario} -> ${prepared.scenarioConfig.name}`);
      console.log(`[SIM] 场景说明: ${prepared.scenarioConfig.description}`);
    } else if (options.mode === 'blood-pressure') {
      console.log(`[SIM] mode blood-pressure systolic=${options.bpSystolic} diastolic=${options.bpDiastolic} pulse=${options.bpPulse}`);
      console.log(`[SIM] blood-pressure time ${options.bpIncludeTime ? (options.bpTime || 'current local time') : 'disabled / zero bytes'}`);
      console.log(`[SIM] 当前模式为血压报文 systolic=${options.bpSystolic} diastolic=${options.bpDiastolic} pulse=${options.bpPulse}`);
      console.log(`[SIM] 血压时间 ${options.bpIncludeTime ? (options.bpTime || '当前本地时间') : '已禁用 / 发送全 0 字节'}`);
    } else {
      console.log(`[SIM] loaded frames file ${path.basename(options.framesFile)}`);
      console.log(`[SIM] 已加载报文文件 ${path.basename(options.framesFile)}`);
    }
    console.log(`[SIM] data mode ${prepared.loadMode} frames=${prepared.analysis.totalFrames} publishable=${prepared.analysis.publishableFrames} unsupported=${prepared.analysis.unsupportedFrames}`);
    if (options.mode === 'file' && options.includeBloodPressure) {
      console.log(`[SIM] 每轮追加血压报文 systolic=${options.bpSystolic} diastolic=${options.bpDiastolic} pulse=${options.bpPulse}`);
      console.log(`[SIM] 追加血压时间 ${options.bpIncludeTime ? (options.bpTime || '当前本地时间') : '已禁用 / 发送全 0 字节'}`);
    }
    console.log(`[SIM] 数据模式 ${prepared.loadMode} frames=${prepared.analysis.totalFrames} publishable=${prepared.analysis.publishableFrames} unsupported=${prepared.analysis.unsupportedFrames}`);
    if (prepared.analysis.fullCycles > 0 || prepared.analysis.extraFrames > 0) {
      console.log(`[SIM] scenario cycles fullCycles=${prepared.analysis.fullCycles} extraFrames=${prepared.analysis.extraFrames}`);
      console.log(`[SIM] 场景轮次统计 fullCycles=${prepared.analysis.fullCycles} extraFrames=${prepared.analysis.extraFrames}`);
    }
    if (prepared.analysis.stateTransitions.length > 0) {
      const transitionText = prepared.analysis.stateTransitions
        .map((item) => `frame#${item.frameIndex}:o=${item.value}`)
        .join(' -> ');
      console.log(`[SIM] state transitions ${transitionText}`);
      console.log(`[SIM] 状态变化 ${transitionText}`);
    }
    if (Object.keys(prepared.analysis.snapshot).length > 0) {
      console.log(`[SIM] snapshot ${formatSnapshot(prepared.analysis.snapshot)}`);
      console.log(`[SIM] 指标快照 ${formatSnapshot(prepared.analysis.snapshot)}`);
    }
    sendNextFrame();
@@ -497,23 +516,23 @@
  });
  socket.on('error', (error) => {
    stop(`[SIM] connection error: ${error.message}`);
    stop(`[SIM] 连接异常: ${error.message}`);
    process.exitCode = 1;
  });
  socket.on('close', () => {
    if (!stopped) {
      stop('[SIM] connection closed');
      stop('[SIM] 连接已关闭');
    }
  });
  process.on('SIGINT', () => {
    stop('[SIM] received SIGINT, stopping');
    stop('[SIM] 收到 SIGINT,停止发送');
    process.exit(0);
  });
  process.on('SIGTERM', () => {
    stop('[SIM] received SIGTERM, stopping');
    stop('[SIM] 收到 SIGTERM,停止发送');
    process.exit(0);
  });
}