添加遥控业务代码
This commit is contained in:
@@ -104,8 +104,36 @@ class Master {
|
||||
}
|
||||
|
||||
const signals = [0, 1, 0, 1, 1, 0, 1, 0];
|
||||
const doubleSignals = Array(8).fill(1);
|
||||
const measures = [2300, 2310, 2290, 5000, -125, 0, 1234, 42];
|
||||
const request = (oa, qoi = 20, ca = 7) => [100, 1, 0x86, oa, ca & 0xff, ca >> 8, 0, 0, 0, qoi];
|
||||
// 构造单对象45/46命令原文:CA/IOA按小端编码,QU固定为1(短脉冲)。
|
||||
// type/oa/ioa/state来自用例,select决定S/E;cot/ca默认激活原因6和示例站点7。
|
||||
// 返回普通字节数组,也用于故意构造不支持类型的兜底请求,不在此执行业务校验。
|
||||
const commandRequest = (type, oa, ioa, state, select, cot = 6, ca = 7) =>
|
||||
[type, 1, cot, oa, ca & 0xff, ca >> 8, ioa & 0xff, (ioa >> 8) & 0xff, ioa >> 16,
|
||||
state | 4 | (select ? 0x80 : 0)];
|
||||
|
||||
// 发送payload并断言紧随其后的应答;除COT/P/N外须回显原请求,TEST位也须保留。
|
||||
// master负责链路收发及确认,cot/negative描述预期应答;Promise完成表示该帧已核对。
|
||||
async function commandReply(master, payload, cot, negative = false) {
|
||||
await master.send(payload);
|
||||
const expected = [...payload]; expected[2] = (payload[2] & 0x80) | cot | (negative ? 0x40 : 0);
|
||||
assert.deepEqual([...(await master.readASDU())], expected);
|
||||
}
|
||||
|
||||
// 验证一次选择后执行的真实TCP全流程;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);
|
||||
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]);
|
||||
const terminated = [...execute]; terminated[2] = 10;
|
||||
assert.deepEqual([...(await master.readASDU())], terminated);
|
||||
}
|
||||
const initialCounters = [120000, 345600, 7890, 0, 1, 65535, 2147483647, 42];
|
||||
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];
|
||||
@@ -155,7 +183,10 @@ async function totalCall(master, oa, fragmented = false, qoi = 20, ca = 7) {
|
||||
|
||||
async function readTotalCall(master, oa, qoi = 20, busyRequests = []) {
|
||||
const original = request(oa, qoi);
|
||||
const count = qoi === 20 ? 6 : 4;
|
||||
// 站总召依次返回单点、双点、遥测;遥信分组含前两类,遥测分组只含最后一类。
|
||||
// 每类8点、每帧4点,首尾另有ACT_CON与ACT_TERM。
|
||||
const types = qoi === 20 ? [1, 3, 11] : qoi === 21 ? [1, 3] : [11];
|
||||
const count = 2 + types.length * 2;
|
||||
const busyReplies = busyRequests.map(payload => {
|
||||
const expected = [...payload]; expected[2] = 0xc7;
|
||||
if (expected[4] === 0xff && expected[5] === 0xff) { expected[4] = 7; expected[5] = 0; }
|
||||
@@ -175,17 +206,20 @@ async function readTotalCall(master, oa, qoi = 20, busyRequests = []) {
|
||||
expected[2] = f === 0 ? 0x87 : 0x8a;
|
||||
assert.deepEqual([...asdu], expected);
|
||||
} else {
|
||||
const digital = qoi === 20 ? f < 3 : qoi === 21;
|
||||
const type = types[Math.floor((f - 1) / 2)];
|
||||
const digital = type !== 11;
|
||||
const values = type === 1 ? signals : type === 3 ? doubleSignals : measures;
|
||||
const baseIoa = type === 1 ? 1001 : type === 3 ? 1101 : 2001;
|
||||
const width = digital ? 4 : 6;
|
||||
const first = (qoi === 20 && !digital ? f - 3 : f - 1) * 4;
|
||||
const first = ((f - 1) % 2) * 4;
|
||||
assert.equal(asdu.length, 6 + width * 4);
|
||||
assert.deepEqual([...asdu.subarray(0, 6)], [digital ? 1 : 11, 4, 0x80 | qoi, oa, 7, 0]);
|
||||
assert.deepEqual([...asdu.subarray(0, 6)], [type, 4, 0x80 | qoi, oa, 7, 0]);
|
||||
for (let p = 0; p < 4; p++) {
|
||||
const offset = 6 + p * width;
|
||||
assert.equal(asdu.readUIntLE(offset, 3), (digital ? 1001 : 2001) + first + p);
|
||||
if (digital) assert.equal(asdu[offset + 3], signals[first + p]);
|
||||
assert.equal(asdu.readUIntLE(offset, 3), baseIoa + first + p);
|
||||
if (digital) assert.equal(asdu[offset + 3], values[first + p]);
|
||||
else {
|
||||
assert.equal(asdu.readInt16LE(offset + 3), measures[first + p]);
|
||||
assert.equal(asdu.readInt16LE(offset + 3), values[first + p]);
|
||||
assert.equal(asdu[offset + 5], 0);
|
||||
}
|
||||
}
|
||||
@@ -200,20 +234,22 @@ async function main() {
|
||||
assert.equal(help.status, 0, help.stderr);
|
||||
assert(help.stdout.includes('--run-ms'));
|
||||
assert(help.stdout.includes('--clock simulated|system'));
|
||||
assert(help.stdout.includes('Type45 IOA6001..6008'));
|
||||
assert(help.stdout.includes('Type46 IOA6101..6108'));
|
||||
const invalid = spawnSync(executable, ['--ca', '0'], { windowsHide: true, encoding: 'utf8' });
|
||||
assert.ifError(invalid.error);
|
||||
assert.equal(invalid.status, 1);
|
||||
const invalidClock = spawnSync(executable, ['--clock', 'invalid'], { windowsHide: true, encoding: 'utf8' });
|
||||
assert.ifError(invalidClock.error);
|
||||
assert.equal(invalidClock.status, 1);
|
||||
const child = spawn(executable, ['--port', '0', '--ca', '7', '--window', '1', '--run-ms', '8000'],
|
||||
const child = spawn(executable, ['--port', '0', '--ca', '7', '--window', '1', '--run-ms', '12000'],
|
||||
{ windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let output = '', errors = '';
|
||||
const sockets = [];
|
||||
child.stdout.on('data', data => { output += data.toString(); });
|
||||
child.stderr.on('data', data => { errors += data.toString(); });
|
||||
const exit = once(child, 'exit');
|
||||
const deadline = setTimeout(() => { child.kill(); }, 15000);
|
||||
const deadline = setTimeout(() => { child.kill(); }, 20000);
|
||||
try {
|
||||
const port = await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`No listening port: ${output} ${errors}`)), 2000);
|
||||
@@ -263,6 +299,28 @@ async function main() {
|
||||
assert.deepEqual([...(await master.readASDU())], readRejected);
|
||||
const clockConfirmed = [...clockBatch]; clockConfirmed[2] = 7;
|
||||
assert.deepEqual([...(await master.readASDU())], clockConfirmed);
|
||||
// 遥控默认先选择;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;
|
||||
// 撤销故意使用不同状态值:匹配只要求类型/站点/IOA/OA,不比较命令状态。
|
||||
await commandReply(master, commandRequest(45, 0x73, 6002, 0, true), 7);
|
||||
await commandReply(master, commandRequest(45, 0x73, 6002, 1, true, 8), 9);
|
||||
// 选择已撤销,默认不允许直接执行,因此后续执行必须被否定。
|
||||
await commandReply(master, commandRequest(45, 0x73, 6002, 0, false), 7, true);
|
||||
// 公共地址错误由模块判定;遥信地址1001不是Type45命令地址,由应用点表返回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兜底。
|
||||
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 counterRead(master, 0x20);
|
||||
await counterReply(master, counterRequest(0x21, 0x45), 7);
|
||||
await counterRead(master, 0x22, 5, initialCounters, 1);
|
||||
@@ -318,11 +376,11 @@ async function main() {
|
||||
// k=1: both instances must make progress, preserving their own response order.
|
||||
master.socket.write(Buffer.concat([master.frame(request(0x6b)), master.frame(counterRequest(0x60))]));
|
||||
const generalFrames = [], counterFrames = [];
|
||||
for (let index = 0; index < 10; index++) {
|
||||
for (let index = 0; index < 12; index++) {
|
||||
const asdu = await master.readASDU();
|
||||
(asdu[0] === 101 || asdu[0] === 15 ? counterFrames : generalFrames).push(asdu);
|
||||
}
|
||||
assert.equal(generalFrames.length, 6);
|
||||
assert.equal(generalFrames.length, 8);
|
||||
assert.equal(counterFrames.length, 4);
|
||||
await readTotalCall({ readASDU: async () => generalFrames.shift() }, 0x6b);
|
||||
await readCounters({ readASDU: async () => counterFrames.shift() }, 0x60, 5, zeroCounters, 3);
|
||||
@@ -332,13 +390,15 @@ async function main() {
|
||||
await next.close();
|
||||
const [code] = await exit;
|
||||
assert.equal(code, 0, `${output}\n${errors}`);
|
||||
assert.match(output, /STOP submitted=65 gi_submitted=11 /);
|
||||
assert.match(output, /STOP submitted=83 gi_submitted=11 /);
|
||||
assert.match(output, /deactivated=1 timeouts=0/);
|
||||
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/);
|
||||
assert.equal(errors, '');
|
||||
console.log('Slave example loopback: passed (real TCP, batched 106+102 and 103+102, simulated clock/TEST/COT/broadcast, GI and counter groups/broadcast, freeze/reset/TEST, independent k=1 progress, busy/deactivation/errors, STOP/START, reconnect, cleanup)');
|
||||
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)');
|
||||
} finally {
|
||||
clearTimeout(deadline);
|
||||
for (const socket of sockets) socket.destroy();
|
||||
|
||||
Reference in New Issue
Block a user