补充遥调业务代码
This commit is contained in:
@@ -122,15 +122,37 @@ async function commandReply(master, payload, cot, negative = false) {
|
||||
assert.deepEqual([...(await master.readASDU())], expected);
|
||||
}
|
||||
|
||||
// 验证一次选择后执行的真实TCP全流程;monitorIoa为该命令对应的监视方向地址。
|
||||
// 顺序逐帧读取:选择ACT_CON、执行ACT_CON、COT=11返回信息、执行原文ACT_TERM。
|
||||
// 验证真实TCP全流程;selectBeforeExecute区分SBO和直接点,monitorIoa为返回信息地址。
|
||||
// 顺序逐帧读取:可选的选择ACT_CON、执行ACT_CON、COT=11返回信息、执行原文ACT_TERM。
|
||||
// 链路窗口k=1,只有前一帧处理完才能推进下一帧,可暴露返回信息/终止抢先的问题。
|
||||
async function commandExecute(master, type, oa, ioa, state, monitorIoa) {
|
||||
await commandReply(master, commandRequest(type, oa, ioa, state, true), 7);
|
||||
const execute = commandRequest(type, oa, ioa, state, false);
|
||||
async function commandExecute(master, type, oa, ioa, state, monitorIoa, timeTag = null, selectBeforeExecute = true) {
|
||||
if (selectBeforeExecute) {
|
||||
const selection = commandRequest(type, oa, ioa, state, true).concat(timeTag ? timeTag() : []);
|
||||
await commandReply(master, selection, 7);
|
||||
}
|
||||
const execute = commandRequest(type, oa, ioa, state, false).concat(timeTag ? timeTag() : []);
|
||||
await commandReply(master, execute, 7);
|
||||
assert.deepEqual([...(await master.readASDU())],
|
||||
[type === 45 ? 1 : 3, 1, 11, oa, 7, 0, monitorIoa & 0xff, (monitorIoa >> 8) & 0xff, 0, state]);
|
||||
[type === 45 || type === 58 ? 1 : 3, 1, 11, oa, 7, 0, monitorIoa & 0xff, (monitorIoa >> 8) & 0xff, 0, state]);
|
||||
const terminated = [...execute]; terminated[2] = 10;
|
||||
assert.deepEqual([...(await master.readASDU())], terminated);
|
||||
}
|
||||
|
||||
// 独立构造Type49/62的基础命令元素:有符号16位设定值及QOS,QL固定为0。
|
||||
const scaledCommandRequest = (oa, ioa, value, select, type = 49) =>
|
||||
[type, 1, 6, oa, 7, 0, ioa & 0xff, (ioa >> 8) & 0xff, ioa >> 16,
|
||||
value & 0xff, (value >> 8) & 0xff, select ? 0x80 : 0];
|
||||
|
||||
async function scaledCommandExecute(master, oa, ioa, value, monitorIoa, type = 49, timeTag = null, selectBeforeExecute = true) {
|
||||
if (selectBeforeExecute) {
|
||||
const selection = scaledCommandRequest(oa, ioa, value, true, type).concat(timeTag ? timeTag() : []);
|
||||
await commandReply(master, selection, 7);
|
||||
}
|
||||
const execute = scaledCommandRequest(oa, ioa, value, false, type).concat(timeTag ? timeTag() : []);
|
||||
await commandReply(master, execute, 7);
|
||||
assert.deepEqual([...(await master.readASDU())],
|
||||
[11, 1, 11, oa, 7, 0, monitorIoa & 0xff, (monitorIoa >> 8) & 0xff, 0,
|
||||
value & 0xff, (value >> 8) & 0xff, 0]);
|
||||
const terminated = [...execute]; terminated[2] = 10;
|
||||
assert.deepEqual([...(await master.readASDU())], terminated);
|
||||
}
|
||||
@@ -139,6 +161,21 @@ const counterRequest = (oa, qcc = 5, cot = 6, test = false, ca = 7) =>
|
||||
[101, 1, cot | (test ? 0x80 : 0), oa, ca & 0xff, ca >> 8, 0, 0, 0, qcc];
|
||||
const clockRequest = (oa, cot = 6, test = false, ca = 7) =>
|
||||
[103, 1, cot | (test ? 0x80 : 0), oa, ca & 0xff, ca >> 8, 0, 0, 0, 0x39, 0x30, 34, 12, 28, 10, 26];
|
||||
|
||||
// 主站独立计算UTC基准,逐次按单调经过时间生成UTC+8/无SU的CP56,选择与执行不复用旧时标。
|
||||
function clockTagFactory(payload, baseNs) {
|
||||
const tag = payload.slice(-7), milliseconds = tag[0] | (tag[1] << 8);
|
||||
const utcMs = Date.UTC(2000 + tag[6], (tag[5] & 15) - 1, tag[4] & 31,
|
||||
(tag[3] & 31) - 8 - ((tag[3] >> 7) & 1), tag[2] & 63,
|
||||
Math.floor(milliseconds / 1000), milliseconds % 1000);
|
||||
return (adjustMs = 0) => {
|
||||
const elapsedMs = Number((process.hrtime.bigint() - baseNs) / 1000000n);
|
||||
const local = new Date(utcMs + elapsedMs + adjustMs + 8 * 60 * 60 * 1000);
|
||||
const minuteMs = local.getUTCSeconds() * 1000 + local.getUTCMilliseconds();
|
||||
return [minuteMs & 0xff, minuteMs >> 8, local.getUTCMinutes(), local.getUTCHours(),
|
||||
local.getUTCDate(), local.getUTCMonth() + 1, local.getUTCFullYear() - 2000];
|
||||
};
|
||||
}
|
||||
let counterSubmitted = 0, counterCompleted = 0;
|
||||
|
||||
async function readCounters(master, oa, qcc = 5, values = initialCounters, sequence = 0) {
|
||||
@@ -236,6 +273,10 @@ async function main() {
|
||||
assert(help.stdout.includes('--clock simulated|system'));
|
||||
assert(help.stdout.includes('Type45 IOA6001..6008'));
|
||||
assert(help.stdout.includes('Type46 IOA6101..6108'));
|
||||
assert(help.stdout.includes('Type49 IOA6301..6308'));
|
||||
assert(help.stdout.includes('Type58/59/62'));
|
||||
assert(help.stdout.includes('Select/execute: IOA6001..6007, 6101..6107, 6301..6307'));
|
||||
assert(help.stdout.includes('Direct execute: IOA6008, 6108, 6308'));
|
||||
const invalid = spawnSync(executable, ['--ca', '0'], { windowsHide: true, encoding: 'utf8' });
|
||||
assert.ifError(invalid.error);
|
||||
assert.equal(invalid.status, 1);
|
||||
@@ -294,33 +335,62 @@ async function main() {
|
||||
// 同批103先保存执行任务,102当场兜底;Try确认必须等已提交的44之后发送。
|
||||
const clockBatch = clockRequest(0x14);
|
||||
const readBatch = [102, 1, 5, 0x15, 7, 0, 0xe9, 3, 0];
|
||||
const clockBaseNs = process.hrtime.bigint();
|
||||
master.socket.write(Buffer.concat([master.frame(clockBatch), master.frame(readBatch)]));
|
||||
const readRejected = [...readBatch]; readRejected[2] = 0x40 | 44;
|
||||
assert.deepEqual([...(await master.readASDU())], readRejected);
|
||||
const clockConfirmed = [...clockBatch]; clockConfirmed[2] = 7;
|
||||
assert.deepEqual([...(await master.readASDU())], clockConfirmed);
|
||||
// 遥控默认先选择;k=1验证确认、返回信息和终止逐帧交接,后续总召必须读到新值。
|
||||
const currentClockTag = clockTagFactory(clockBatch, clockBaseNs);
|
||||
// 同类型按点选择模式:直接点拒绝SELECT,SBO点拒绝未选择执行;点的类型掩码独立于全局启用。
|
||||
await commandReply(master, commandRequest(45, 0x80, 6008, 1, true), 7, true);
|
||||
await commandReply(master, commandRequest(46, 0x81, 6108, 2, true), 7, true);
|
||||
await commandReply(master, scaledCommandRequest(0x82, 6308, -888, true), 7, true);
|
||||
await commandReply(master, commandRequest(45, 0x83, 6007, 0, false), 7, true);
|
||||
await commandReply(master, commandRequest(46, 0x84, 6008, 2, false), 7, true);
|
||||
await commandExecute(master, 45, 0x85, 6008, 1, 1008, null, false);
|
||||
signals[7] = 1;
|
||||
await commandExecute(master, 46, 0x86, 6108, 2, 1108, null, false);
|
||||
doubleSignals[7] = 2;
|
||||
await scaledCommandExecute(master, 0x87, 6308, -888, 2008, 49, null, false);
|
||||
measures[7] = -888;
|
||||
// 同类型其余点保持SBO;k=1验证确认、返回信息和终止逐帧交接,后续总召必须读到新值。
|
||||
await commandExecute(master, 45, 0x71, 6001, 1, 1001);
|
||||
// 同步后续总召的预期值,验证遥控修改的是示例应用点表而非仅构造一次返回帧。
|
||||
signals[0] = 1;
|
||||
await commandExecute(master, 46, 0x72, 6103, 2, 1103);
|
||||
// 站总召和遥信分组均须读回双点1103的新位置,其余双点仍为初值1。
|
||||
doubleSignals[2] = 2;
|
||||
// 设定值先验证应用限值拒绝,再走完整事务;后面的站总召和QOI=22必须读到新运行值。
|
||||
await commandReply(master, scaledCommandRequest(0x79, 6301, 10001, true), 7, true);
|
||||
await scaledCommandExecute(master, 0x7a, 6301, -1234, 2001);
|
||||
measures[0] = -1234;
|
||||
// 撤销故意使用不同状态值:匹配只要求类型/站点/IOA/OA,不比较命令状态。
|
||||
await commandReply(master, commandRequest(45, 0x73, 6002, 0, true), 7);
|
||||
await commandReply(master, commandRequest(45, 0x73, 6002, 1, true, 8), 9);
|
||||
// 选择已撤销,默认不允许直接执行,因此后续执行必须被否定。
|
||||
// 选择已撤销,该点配置为SBO,因此后续执行必须被否定。
|
||||
await commandReply(master, commandRequest(45, 0x73, 6002, 0, false), 7, true);
|
||||
// 公共地址错误由模块判定;遥信地址1001不是Type45命令地址,由应用点表返回47。
|
||||
// 公共地址错误由模块判定;遥信地址1001不在命令点表内,由库直接返回47。
|
||||
await commandReply(master, commandRequest(46, 0x74, 6101, 1, true, 6, 65535), 46, true);
|
||||
await commandReply(master, commandRequest(45, 0x75, 1001, 1, true), 47, true);
|
||||
// TEST命令须原样保留TEST位并否定,不能启动模拟输出。
|
||||
const commandTest = commandRequest(45, 0x76, 6001, 0, true); commandTest[2] |= 0x80;
|
||||
await commandReply(master, commandTest, 7, true);
|
||||
// 本版仅接手45/46;47及附完整CP56字节的58须继续交给44兜底。
|
||||
// 示例仍不启用47;58/59/62使用新鲜时标,确认和终止逐字节保留各自原CP56。
|
||||
await commandReply(master, commandRequest(47, 0x77, 1001, 1, false), 44, true);
|
||||
const timedCommand = commandRequest(58, 0x78, 1001, 0, true).concat([0, 0, 0, 0, 1, 1, 26]);
|
||||
await commandReply(master, timedCommand, 44, true);
|
||||
await commandExecute(master, 58, 0x78, 6002, 0, 1002, currentClockTag);
|
||||
signals[1] = 0;
|
||||
await commandExecute(master, 59, 0x7b, 6104, 2, 1104, currentClockTag);
|
||||
doubleSignals[3] = 2;
|
||||
await scaledCommandExecute(master, 0x7d, 6302, -4321, 2002, 62, currentClockTag);
|
||||
measures[1] = -4321;
|
||||
// 过期58与106同批:先断言106,再严格读取完整计数量流程,任何迟到58帧都会失败。
|
||||
// STOP统计另证明过期选择未执行应用动作;旧58兜底替换为106兜底,总召统计保持原值。
|
||||
const expiredCommand = commandRequest(58, 0x7e, 6003, 1, true).concat(currentClockTag(-60000));
|
||||
const barrier = [106, 1, 6, 0x7f, 7, 0, 0, 0, 0, 0, 0];
|
||||
master.socket.write(Buffer.concat([master.frame(expiredCommand), master.frame(barrier)]));
|
||||
const barrierReply = [...barrier]; barrierReply[2] = 0x40 | 44;
|
||||
assert.deepEqual([...(await master.readASDU())], barrierReply);
|
||||
await counterRead(master, 0x20);
|
||||
await counterReply(master, counterRequest(0x21, 0x45), 7);
|
||||
await counterRead(master, 0x22, 5, initialCounters, 1);
|
||||
@@ -395,10 +465,10 @@ async function main() {
|
||||
assert.match(output, /negative_submitted=\d+ dropped_requests=0/);
|
||||
assert.match(output, new RegExp(`counter_submitted=${counterSubmitted} counter_completed=${counterCompleted}`));
|
||||
assert.match(output, /clock_executed=3 clock_synchronized=3 clock_submitted=5/);
|
||||
// 三次选择含一次成功撤销;只有前两次启动输出并产生各一帧返回信息。
|
||||
assert.match(output, /command_selected=3 command_executed=2 command_completed=2 command_submitted=\d+ command_returns=2/);
|
||||
// 七次成功选择含一次撤销;六个SBO和三个直接执行均返回值,模式拒绝/越限/过期不启动动作。
|
||||
assert.match(output, /command_selected=7 command_executed=9 command_completed=9 command_submitted=\d+ command_returns=9 command_time_expired=1 command_time_rejected=0/);
|
||||
assert.equal(errors, '');
|
||||
console.log('Slave example loopback: passed (real TCP, Type45/46 select/execute/return/termination, command cancel/TEST/address/unsupported types, changed single/double GI values, batched 106+102 and 103+102, simulated clock, counters, k=1, STOP/START, reconnect, cleanup)');
|
||||
console.log('Slave example loopback: passed (real TCP, per-point direct/SBO modes and type masks, Type45/46/49/58/59/62 execute/return/termination, scaled limits, fresh timestamps and silent expiry, command cancel/TEST/address/unsupported types, changed single/double/scaled GI values, batched 106+102 and 103+102, simulated clock, counters, k=1, STOP/START, reconnect, cleanup)');
|
||||
} finally {
|
||||
clearTimeout(deadline);
|
||||
for (const socket of sockets) socket.destroy();
|
||||
|
||||
Reference in New Issue
Block a user