补充总召唤的广播地址,电度召唤+时钟同步业务的实现
This commit is contained in:
@@ -106,6 +106,48 @@ class Master {
|
||||
const signals = [0, 1, 0, 1, 1, 0, 1, 0];
|
||||
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];
|
||||
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];
|
||||
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];
|
||||
let counterSubmitted = 0, counterCompleted = 0;
|
||||
|
||||
async function readCounters(master, oa, qcc = 5, values = initialCounters, sequence = 0) {
|
||||
const group = qcc & 0x3f;
|
||||
const first = group === 2 ? 4 : 0, end = group === 1 ? 4 : 8;
|
||||
const count = 2 + (end - first) / 4;
|
||||
for (let frame = 0; frame < count; frame++) {
|
||||
const asdu = await master.readASDU();
|
||||
if (frame === 0 || frame === count - 1) {
|
||||
assert.deepEqual([...asdu], counterRequest(oa, qcc, frame === 0 ? 7 : 10));
|
||||
} else {
|
||||
assert.equal(asdu.length, 38);
|
||||
assert.deepEqual([...asdu.subarray(0, 6)], [15, 4, group === 5 ? 37 : 37 + group, oa, 7, 0]);
|
||||
for (let point = 0; point < 4; point++) {
|
||||
const offset = 6 + 8 * point, index = first + 4 * (frame - 1) + point;
|
||||
assert.equal(asdu.readUIntLE(offset, 3), 3001 + index);
|
||||
assert.equal(asdu.readInt32LE(offset + 3), values[index]);
|
||||
assert.equal(asdu[offset + 7], sequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
counterSubmitted += count;
|
||||
counterCompleted++;
|
||||
}
|
||||
|
||||
async function counterRead(master, oa, qcc = 5, values = initialCounters, sequence = 0, ca = 7) {
|
||||
await master.send(counterRequest(oa, qcc, 6, false, ca));
|
||||
await readCounters(master, oa, qcc, values, sequence);
|
||||
}
|
||||
|
||||
async function counterReply(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);
|
||||
counterSubmitted++;
|
||||
if (!negative) counterCompleted++;
|
||||
}
|
||||
async function totalCall(master, oa, fragmented = false, qoi = 20, ca = 7) {
|
||||
await master.send(request(oa, qoi, ca), fragmented);
|
||||
await readTotalCall(master, oa, qoi);
|
||||
@@ -157,17 +199,21 @@ async function main() {
|
||||
assert.ifError(help.error);
|
||||
assert.equal(help.status, 0, help.stderr);
|
||||
assert(help.stdout.includes('--run-ms'));
|
||||
assert(help.stdout.includes('--clock simulated|system'));
|
||||
const invalid = spawnSync(executable, ['--ca', '0'], { windowsHide: true, encoding: 'utf8' });
|
||||
assert.ifError(invalid.error);
|
||||
assert.equal(invalid.status, 1);
|
||||
const child = spawn(executable, ['--port', '0', '--ca', '7', '--window', '1', '--run-ms', '4000'],
|
||||
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'],
|
||||
{ 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(); }, 9000);
|
||||
const deadline = setTimeout(() => { child.kill(); }, 15000);
|
||||
try {
|
||||
const port = await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`No listening port: ${output} ${errors}`)), 2000);
|
||||
@@ -186,6 +232,55 @@ async function main() {
|
||||
return master;
|
||||
}
|
||||
const master = await connect();
|
||||
// 一次write内的106延时获得和102读取逐条回显,k=1时第二条由HIGH等待首条ACK。
|
||||
const unsupportedBatch = [
|
||||
[106, 1, 0x86, 0x18, 7, 0, 0, 0, 0, 0x39, 0x30],
|
||||
[102, 1, 5, 0x19, 7, 0, 0xe9, 3, 0]
|
||||
];
|
||||
master.socket.write(Buffer.concat(unsupportedBatch.map(payload => master.frame(payload))));
|
||||
for (const payload of unsupportedBatch) {
|
||||
const expected = [...payload]; expected[2] = (payload[2] & 0x80) | 0x40 | 44;
|
||||
assert.deepEqual([...(await master.readASDU())], expected);
|
||||
}
|
||||
// 默认模拟时钟:成功、TEST拒绝、错误COT及广播实际CA确认都保持请求时标原文。
|
||||
for (const [payload, cot, negative] of [
|
||||
[clockRequest(0x10), 7, false],
|
||||
[clockRequest(0x11, 6, true), 7, true],
|
||||
[clockRequest(0x12, 5), 45, true],
|
||||
[clockRequest(0x13, 6, false, 65535), 7, false]
|
||||
]) {
|
||||
await master.send(payload);
|
||||
const expected = [...payload];
|
||||
expected[2] = (payload[2] & 0x80) | cot | (negative ? 0x40 : 0);
|
||||
expected[4] = 7; expected[5] = 0;
|
||||
assert.deepEqual([...(await master.readASDU())], expected);
|
||||
}
|
||||
// 同批103先保存执行任务,102当场兜底;Try确认必须等已提交的44之后发送。
|
||||
const clockBatch = clockRequest(0x14);
|
||||
const readBatch = [102, 1, 5, 0x15, 7, 0, 0xe9, 3, 0];
|
||||
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);
|
||||
await counterRead(master, 0x20);
|
||||
await counterReply(master, counterRequest(0x21, 0x45), 7);
|
||||
await counterRead(master, 0x22, 5, initialCounters, 1);
|
||||
await counterRead(master, 0x23, 1, initialCounters, 1);
|
||||
await counterRead(master, 0x24, 2, initialCounters, 1);
|
||||
await counterReply(master, counterRequest(0x25, 3), 7, true);
|
||||
await counterReply(master, counterRequest(0x26, 4), 7, true);
|
||||
await counterReply(master, counterRequest(0x27, 5, 8), 45, true);
|
||||
// TEST requests must not freeze or clear real counters.
|
||||
await counterReply(master, counterRequest(0x28, 0x85, 6, true), 7, true);
|
||||
await counterRead(master, 0x29, 5, initialCounters, 1, 65535);
|
||||
await counterReply(master, counterRequest(0x2a, 0x85), 7);
|
||||
await counterRead(master, 0x2b, 5, initialCounters, 2);
|
||||
await counterReply(master, counterRequest(0x2c, 0x45), 7);
|
||||
const zeroCounters = initialCounters.map(() => 0);
|
||||
await counterRead(master, 0x2d, 5, zeroCounters, 3);
|
||||
await counterReply(master, counterRequest(0x2e, 0xc5), 7);
|
||||
await counterRead(master, 0x2f, 5, zeroCounters, 3);
|
||||
await totalCall(master, 0x5a, true);
|
||||
await totalCall(master, 0x5b, false, 21);
|
||||
await totalCall(master, 0x5c, false, 22);
|
||||
@@ -220,18 +315,30 @@ async function main() {
|
||||
assert.deepEqual([...(await master.readASDU())], deactCon);
|
||||
await master.control(0x13, 0x23);
|
||||
await master.control(7, 0x0b);
|
||||
await totalCall(master, 0x6b);
|
||||
// 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++) {
|
||||
const asdu = await master.readASDU();
|
||||
(asdu[0] === 101 || asdu[0] === 15 ? counterFrames : generalFrames).push(asdu);
|
||||
}
|
||||
assert.equal(generalFrames.length, 6);
|
||||
assert.equal(counterFrames.length, 4);
|
||||
await readTotalCall({ readASDU: async () => generalFrames.shift() }, 0x6b);
|
||||
await readCounters({ readASDU: async () => counterFrames.shift() }, 0x60, 5, zeroCounters, 3);
|
||||
await master.close();
|
||||
const next = await connect();
|
||||
await totalCall(next, 0x7c);
|
||||
await next.close();
|
||||
const [code] = await exit;
|
||||
assert.equal(code, 0, `${output}\n${errors}`);
|
||||
assert.match(output, /STOP submitted=62 gi_submitted=11 /);
|
||||
assert.match(output, /STOP submitted=65 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.equal(errors, '');
|
||||
console.log('Slave example loopback: passed (real TCP, station/groups and broadcast CA, busy rejection and retry, broadcast/directed deactivation, fragmented input, k=1 ACK, errors, STOP/START, reconnect, cleanup)');
|
||||
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)');
|
||||
} finally {
|
||||
clearTimeout(deadline);
|
||||
for (const socket of sockets) socket.destroy();
|
||||
|
||||
Reference in New Issue
Block a user