Skip to content

Commit 116a0c3

Browse files
committed
Add PCCC golden-frame tests for the DF1 command set
Drives the PCCC layer through the scripted transport and asserts the exact DF1 wire bytes (CMD/STS/TNS/FNC, logical ASCII addressing, FLAG byte data descriptors) for typed read/write, word range read, diagnostic status, and echo, plus STS/EXT-STS error mapping and transaction matching. Three tests fail against the current code, documenting known bugs fixed in the next commit: - typedWrite passes the offset ref object instead of the offset number when encoding extended data descriptors, so float writes always throw ERR_INVALID_ARG_TYPE - typedWrite for timer/counter/control/string/long files reaches the same broken path through an undefined type id instead of failing with a clear unsupported-type error https://claude.ai/code/session_01AuAYuP6dTn8ALpkKErQuby
1 parent fd29d05 commit 116a0c3

1 file changed

Lines changed: 208 additions & 0 deletions

File tree

test/unit/pccc.layer.test.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { describe, it } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
4+
import PCCCLayer from '../../src/layers/pccc/index.js';
5+
import ScriptedTransport from '../harness/ScriptedTransport.js';
6+
7+
/**
8+
* Golden-frame tests for the PCCC layer.
9+
*
10+
* Expected byte sequences follow the DF1 protocol reference manual
11+
* (Allen-Bradley publication 1770-6.5.16): commands are
12+
* CMD(1) STS(1) TNS(2, little-endian) FNC(1) followed by
13+
* function-specific data; replies set 0x40 in CMD. Logical ASCII
14+
* addresses are encoded as 0x00 0x24 '<address>' 0x00. Typed data uses
15+
* the FLAG-byte descriptor scheme (type id in the high nibble, size in
16+
* the low nibble, extended forms when a field exceeds 3/4 bits).
17+
*/
18+
19+
function hex(s) {
20+
return Buffer.from(s.replace(/\s+/g, ''), 'hex');
21+
}
22+
23+
function createStack() {
24+
const transport = new ScriptedTransport();
25+
const layer = new PCCCLayer(transport);
26+
return { transport, layer };
27+
}
28+
29+
function withTimeout(promise, label, ms = 1000) {
30+
return new Promise((resolve, reject) => {
31+
const handle = setTimeout(() => {
32+
reject(new Error(`${label} did not settle within ${ms}ms`));
33+
}, ms);
34+
promise.then(
35+
(value) => { clearTimeout(handle); resolve(value); },
36+
(err) => { clearTimeout(handle); reject(err); },
37+
);
38+
});
39+
}
40+
41+
describe('PCCC layer: typed read', () => {
42+
it('reads an integer file element (CMD 0x0F, FNC 0x68)', async () => {
43+
const { transport, layer } = createStack();
44+
transport.reply(hex('4f00 0100 42 0500'));
45+
const value = await withTimeout(layer.typedRead('N7:1'), 'typedRead');
46+
assert.deepEqual(transport.sent, [
47+
hex('0f00 0100 68 0000 0100 0024 4e373a31 00 0100'),
48+
]);
49+
assert.equal(value, 5);
50+
});
51+
52+
it('reads multiple integer elements as an array', async () => {
53+
const { transport, layer } = createStack();
54+
/** array descriptor (0x97 0x09, 7 bytes) containing INT descriptor 0x42 */
55+
transport.reply(hex('4f00 0100 9709 42 0000 feff ff00'));
56+
const value = await withTimeout(layer.typedRead('N7:0', 3), 'typedRead');
57+
assert.deepEqual(transport.sent, [
58+
hex('0f00 0100 68 0000 0300 0024 4e373a30 00 0300'),
59+
]);
60+
assert.deepEqual(value, [0, -2, 255]);
61+
});
62+
63+
it('reads a float file element (extended type id descriptor 0x94 0x08)', async () => {
64+
const { transport, layer } = createStack();
65+
transport.reply(hex('4f00 0100 9408 0000c03f'));
66+
const value = await withTimeout(layer.typedRead('F8:0'), 'typedRead');
67+
assert.deepEqual(transport.sent, [
68+
hex('0f00 0100 68 0000 0100 0024 46383a30 00 0100'),
69+
]);
70+
assert.equal(value, 1.5);
71+
});
72+
73+
it('reads a timer element into a structured value', async () => {
74+
const { transport, layer } = createStack();
75+
/** descriptor 0x56 (Timer, 6 bytes): EN set, PRE 1000, ACC 500 */
76+
transport.reply(hex('4f00 0100 56 0080 e803 f401'));
77+
const value = await withTimeout(layer.typedRead('T4:0'), 'typedRead');
78+
assert.deepEqual(transport.sent, [
79+
hex('0f00 0100 68 0000 0100 0024 54343a30 00 0100'),
80+
]);
81+
assert.deepEqual(value, {
82+
EN: true, TT: false, DN: false, PRE: 1000, ACC: 500,
83+
});
84+
});
85+
});
86+
87+
describe('PCCC layer: typed write', () => {
88+
it('writes an integer file element (CMD 0x0F, FNC 0x67)', async () => {
89+
const { transport, layer } = createStack();
90+
transport.reply(hex('4f00 0100'));
91+
const reply = await withTimeout(layer.typedWrite('N7:3', 5), 'typedWrite');
92+
assert.deepEqual(transport.sent, [
93+
hex('0f00 0100 67 0000 0100 0024 4e373a33 00 42 0500'),
94+
]);
95+
assert.equal(reply.status.code, 0);
96+
});
97+
98+
it('writes a float file element (extended type id descriptor 0x94 0x08)', async () => {
99+
const { transport, layer } = createStack();
100+
transport.reply(hex('4f00 0100'));
101+
const reply = await withTimeout(layer.typedWrite('F8:0', 1.5), 'typedWrite');
102+
assert.deepEqual(transport.sent, [
103+
hex('0f00 0100 67 0000 0100 0024 46383a30 00 9408 0000c03f'),
104+
]);
105+
assert.equal(reply.status.code, 0);
106+
});
107+
108+
it('rejects writes to timer files with a clear error', async () => {
109+
const { layer } = createStack();
110+
await assert.rejects(
111+
withTimeout(layer.typedWrite('T4:0', 1), 'typedWrite'),
112+
/not currently supported/,
113+
);
114+
});
115+
116+
it('rejects writes to long files with a clear error', async () => {
117+
const { layer } = createStack();
118+
await assert.rejects(
119+
withTimeout(layer.typedWrite('L9:0', 1), 'typedWrite'),
120+
/not currently supported/,
121+
);
122+
});
123+
124+
it('rejects unknown address prefixes', async () => {
125+
const { layer } = createStack();
126+
await assert.rejects(
127+
withTimeout(layer.typedWrite('Q2:0', 1), 'typedWrite'),
128+
/Unsupported address/,
129+
);
130+
});
131+
});
132+
133+
describe('PCCC layer: other commands', () => {
134+
it('word range read (CMD 0x0F, FNC 0x01) resolves the raw data', async () => {
135+
const { transport, layer } = createStack();
136+
transport.reply(hex('4f00 0100 3412 7856'));
137+
const value = await withTimeout(layer.wordRangeRead('N7:0', 2), 'wordRangeRead');
138+
assert.deepEqual(transport.sent, [
139+
hex('0f00 0100 01 0000 0200 0024 4e373a30 00 04'),
140+
]);
141+
assert.deepEqual(value, hex('3412 7856'));
142+
});
143+
144+
it('diagnostic status (CMD 0x06, FNC 0x03)', async () => {
145+
const { transport, layer } = createStack();
146+
transport.reply(hex('4600 0100 ee31c0'));
147+
const value = await withTimeout(layer.diagnosticStatus(), 'diagnosticStatus');
148+
assert.deepEqual(transport.sent, [hex('0600 0100 03')]);
149+
assert.deepEqual(value, hex('ee31c0'));
150+
});
151+
152+
it('echo (CMD 0x06, FNC 0x00)', async () => {
153+
const { transport, layer } = createStack();
154+
transport.reply(hex('4600 0100 dead'));
155+
const value = await withTimeout(layer.echo(hex('dead')), 'echo');
156+
assert.deepEqual(transport.sent, [hex('0600 0100 00 dead')]);
157+
assert.deepEqual(value, hex('dead'));
158+
});
159+
});
160+
161+
describe('PCCC layer: error replies', () => {
162+
it('rejects with the STS description', async () => {
163+
const { transport, layer } = createStack();
164+
transport.reply(hex('4f10 0100'));
165+
await assert.rejects(
166+
withTimeout(layer.typedRead('N7:0'), 'typedRead'),
167+
/Illegal command or format/,
168+
);
169+
});
170+
171+
it('rejects with the EXT STS description when STS is 0xF0', async () => {
172+
const { transport, layer } = createStack();
173+
transport.reply(hex('4ff0 0100 11'));
174+
await assert.rejects(
175+
withTimeout(layer.typedRead('N7:0'), 'typedRead'),
176+
/Illegal data type/,
177+
);
178+
});
179+
});
180+
181+
describe('PCCC layer: transactions', () => {
182+
it('increments the transaction number per request', async () => {
183+
const { transport, layer } = createStack();
184+
transport.reply(hex('4f00 0100 42 0100'));
185+
transport.reply(hex('4f00 0200 42 0200'));
186+
await withTimeout(layer.typedRead('N7:0'), 'first typedRead');
187+
await withTimeout(layer.typedRead('N7:1'), 'second typedRead');
188+
assert.deepEqual(transport.sent, [
189+
hex('0f00 0100 68 0000 0100 0024 4e373a30 00 0100'),
190+
hex('0f00 0200 68 0000 0100 0024 4e373a31 00 0100'),
191+
]);
192+
});
193+
194+
it('matches out-of-order replies by transaction number', async () => {
195+
const { transport, layer } = createStack();
196+
transport.ignoreNextRequest();
197+
transport.onNextRequest((request, t) => {
198+
t.deliver(hex('4f00 0200 42 0200'));
199+
t.deliver(hex('4f00 0100 42 0100'));
200+
});
201+
const first = layer.typedRead('N7:0');
202+
const second = layer.typedRead('N7:1');
203+
assert.deepEqual(
204+
await withTimeout(Promise.all([first, second]), 'out-of-order replies'),
205+
[1, 2],
206+
);
207+
});
208+
});

0 commit comments

Comments
 (0)