@mokouliszt/mel-device 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,686 @@
1
+ "use strict";
2
+
3
+ const SERIES = ["iQ-R", "iQ-F", "MX-R", "MX-F", "CR800"];
4
+
5
+ const IQ_R_MODELS = [
6
+ "R00CPU", "R01CPU", "R02CPU", "R04CPU", "R04ENCPU", "R08CPU", "R08ENCPU",
7
+ "R16CPU", "R16ENCPU", "R32CPU", "R32ENCPU", "R120CPU", "R120ENCPU"
8
+ ];
9
+ const IQ_F_MODELS = ["FX5S", "FX5UJ", "FX5U", "FX5UC"];
10
+ const MX_R_MODELS = ["MXR300-16", "MXR300-32", "MXR300-64", "MXR500-128", "MXR500-256"];
11
+ const MX_F_MODELS = [
12
+ "MXF100-8-N32", "MXF100-8-P32", "MXF100-16-N32", "MXF100-16-P32", "MXF100-X32",
13
+ "MXF100-Y32N", "MXF100-Y32P", "MXF100-H32N", "MXF100-H32P", "MXF100-Y16R"
14
+ ];
15
+ const CR800_MODELS = ["CR800-R", "CR800-D", "CR800-Q"];
16
+
17
+ const MODEL_LISTS = {
18
+ "iQ-R": IQ_R_MODELS,
19
+ "iQ-F": IQ_F_MODELS,
20
+ "MX-R": MX_R_MODELS,
21
+ "MX-F": MX_F_MODELS,
22
+ CR800: CR800_MODELS
23
+ };
24
+
25
+ const BIT_PREFIXES = new Set(["X", "DX", "Y", "DY", "M", "L", "B", "F", "SB", "V", "S", "SM", "FX", "FY"]);
26
+ const WORD_BIT_IQF = new Set(["D", "SD", "W", "SW", "R", "G"]);
27
+ const WORD_BIT_R = new Set(["D", "SD", "W", "SW", "R", "ZR", "RD", "G", "JW", "JSW"]);
28
+ const TIMER_ALIAS = {
29
+ TS: ["T", "contact"], TC: ["T", "coil"], TN: ["T", "current"],
30
+ STS: ["ST", "contact"], STC: ["ST", "coil"], STN: ["ST", "current"],
31
+ LTS: ["LT", "contact"], LTC: ["LT", "coil"], LTN: ["LT", "current"],
32
+ LSTS: ["LST", "contact"], LSTC: ["LST", "coil"], LSTN: ["LST", "current"],
33
+ CS: ["C", "contact"], CC: ["C", "coil"], CN: ["C", "current"],
34
+ LCS: ["LC", "contact"], LCC: ["LC", "coil"], LCN: ["LC", "current"]
35
+ };
36
+
37
+ const IQF_MAX_COMMON = {
38
+ X: 0o1777, Y: 0o1777, M: 32767, L: 32767, B: 0x7fff, F: 32767, SB: 0x7fff,
39
+ S: 4095, T: 1023, ST: 1023, C: 1023, LC: 1023, D: 7999, W: 0x7fff,
40
+ SW: 0x7fff, R: 32767, ER: 32767, SM: 9999, SD: 9999, Z: 23, LZ: 11, N: 14
41
+ };
42
+ const IQF_MAX_UJ = {
43
+ ...IQF_MAX_COMMON,
44
+ M: 7679, L: 7679, B: 0x7ff, F: 127, SB: 0x7ff, T: 511, ST: 15,
45
+ C: 255, LC: 63, W: 0x3ff, SW: 0x3ff, Z: 19, LZ: 1
46
+ };
47
+
48
+ const IQR_DEFAULT_LOW = {
49
+ X: 0x1fff, Y: 0x1fff, M: 8191, B: 0x1fff, F: 2047, SB: 0x7ff, V: 2047,
50
+ S: -1, T: 2047, ST: -1, LT: -1, LST: -1, C: 1023, LC: -1, D: 12287,
51
+ W: 0x1fff, SW: 0x7ff, L: 8191, FX: 0xf, FY: 0xf, FD: 4, SM: 4095, SD: 4095,
52
+ Z: 19, LZ: 1, R: -1, ZR: -1, RD: 524287, N: 14, P: 8191, I: 1023, BL: 127, TR: -1
53
+ };
54
+ const IQR_DEFAULT_HIGH = {
55
+ ...IQR_DEFAULT_LOW,
56
+ X: 0x2fff, Y: 0x2fff, M: 12287, T: 1023, LT: 1023, C: 511, LC: 511,
57
+ D: 18431, BL: 319
58
+ };
59
+ const IQR_MAX = {
60
+ X: 0x2fff, Y: 0x2fff, M: 161882111, B: 0x9a61fff, F: 32767, SB: 0x9a61fff,
61
+ V: 32767, S: 16383, T: 8993439, ST: 8993439, LT: 2529407, LST: 2529407,
62
+ C: 8993439, LC: 4761215, D: 10117631, W: 0x9a61ff, SW: 0x9a61ff, L: 32767,
63
+ FX: 0xf, FY: 0xf, FD: 4, SM: 4095, SD: 4095, Z: 23, LZ: 11,
64
+ R: 32767, ZR: 10117631, RD: 1048575, N: 14, P: 32767, I: 1023, BL: 319, TR: 0
65
+ };
66
+
67
+ const MXR_DEFAULT = {
68
+ X: 0x2fff, Y: 0x2fff, M: 65535, B: 0x7fff, F: 2047, SB: 0x7ff, V: 2047,
69
+ T: 1023, ST: -1, LT: 4095, LST: -1, C: 511, LC: 511, D: 65535,
70
+ W: 0x7fff, SW: 0x7ff, L: 8191, FX: 0xf, FY: 0xf, FD: 4, SM: 4495, SD: 4495,
71
+ Z: 23, LZ: 11, R: -1, ZR: -1, RD: 1048575, N: 14, P: 8191, I: 1023
72
+ };
73
+ const MXR_MAX = {
74
+ ...MXR_DEFAULT,
75
+ M: 167747583, B: 0x9ff9fff, F: 32767, SB: 0x9ff9fff, V: 32767,
76
+ T: 9319295, ST: 9319295, LT: 2621055, LST: 2621055, C: 9319295,
77
+ LC: 4933727, D: 10484223, W: 0x9ff9ff, SW: 0x9ff9ff, L: 32767,
78
+ R: 32767, ZR: 10484223, RD: 4194303, P: 16383
79
+ };
80
+
81
+ const MXF_DEFAULT = {
82
+ X: 0x2fff, Y: 0x2fff, M: 65535, B: 0x7fff, F: 2047, SB: 0x7ff, V: 2047,
83
+ T: 1023, ST: -1, LT: 4095, LST: -1, C: 511, LC: 511, D: 65535,
84
+ W: 0x7fff, SW: 0x7ff, L: 8191, FX: 0xf, FY: 0xf, FD: 4, SM: 9999, SD: 9999,
85
+ Z: 23, LZ: 11, R: -1, ZR: -1, RD: 131071, N: 14, P: 8191, I: 255
86
+ };
87
+ const MXF_MAX = {
88
+ ...MXF_DEFAULT,
89
+ M: 67084287, B: 0x3ff9fff, F: 32767, SB: 0x3ff9fff, V: 32767,
90
+ T: 3726879, ST: 3726879, LT: 1048191, LST: 1048191, C: 3726879,
91
+ LC: 1973055, D: 4192767, W: 0x3ff9ff, SW: 0x3ff9ff, L: 32767,
92
+ R: 32767, ZR: 4192767, RD: 262143, P: 16383
93
+ };
94
+
95
+ const RADIX = {
96
+ X: "series-x", DX: "series-x", Y: "series-x", DY: "series-x", B: 16, SB: 16, W: 16, SW: 16, FX: 16, FY: 16,
97
+ M: 10, L: 10, F: 10, V: 10, S: 10, T: 10, ST: 10, LT: 10, LST: 10,
98
+ C: 10, LC: 10, D: 10, R: 10, ER: 10, ZR: 10, RD: 10, SM: 10, SD: 10,
99
+ FD: 10, Z: 10, LZ: 10, N: 10, P: 10, I: 10, BL: 10, TR: 10
100
+ };
101
+
102
+ const CONFIGURABLE = new Set([
103
+ "M", "L", "B", "F", "SB", "V", "S", "T", "ST", "LT", "LST", "C", "LC",
104
+ "D", "W", "SW", "R", "ZR", "RD", "Z", "LZ", "P", "BL", "TR"
105
+ ]);
106
+
107
+ const SOURCES = {
108
+ "iQ-F": { manual: "JY997D54301AF", pages: [57, 69, 70, 76, 89] },
109
+ "iQ-R": { manual: "SH-082487-J", pages: [65, 408, 409, 410, 435, 436] },
110
+ "MX-R": { manual: "SH-082640-D", pages: [350, 351, 352, 380, 381] },
111
+ "MX-F": { manual: "SH-082633-E", pages: [277, 416, 417, 418] },
112
+ CR800: { manual: "BFP-A3477-AB", pages: [489, 490, 491, 618, 619, 654, 655, 656, 657] }
113
+ };
114
+
115
+ const CR800_RULES = {
116
+ "CR800-R": {
117
+ direct: { X: [0, 0xfff], Y: [0, 0xfff], M: [0, 18431], D: [0, 5119], SM: [0, 4095], SD: [0, 4095] },
118
+ cpu: { G: { units: [0, 1, 2, 3], range: [0, 524287] }, HG: { units: [0, 1, 2, 3], range: [0, 12287] } }
119
+ },
120
+ "CR800-D": {
121
+ direct: { X: [0, 0x1fff], Y: [0, 0x1fff], D: [0, 5119], SM: [0, 4095], SD: [0, 4095] },
122
+ cpu: { HG: { units: [0, 1], range: [0, 2047] } }
123
+ },
124
+ "CR800-Q": {
125
+ direct: { X: [0, 0xfff], Y: [0, 0xfff], M: [0, 18431], D: [0, 5119], SM: [0, 2047], SD: [0, 2047] },
126
+ cpu: { G: { units: [0, 1, 2, 3], range: [10000, 24335] } }
127
+ }
128
+ };
129
+
130
+ const CR800_FEATURE_NAMES = new Set([
131
+ "qxyread", "iqmem", "parallelIoUnit", "parallelIoInterface", "gotLink",
132
+ "profibus", "ccLink", "ccLinkIef"
133
+ ]);
134
+
135
+ const CR800_ALLOCATION_LABELS = {
136
+ qxyread: "sequencer I/O unit direct control (QXYREAD)",
137
+ iqmem: "CPU/shared-memory extension (IQMEM)",
138
+ ddevvl: "sequencer device allocation (DDEVVL*)",
139
+ parallelIoUnit: "parallel I/O unit",
140
+ parallelIoInterface: "parallel I/O interface",
141
+ gotLink: "GOT link",
142
+ profibus: "PROFIBUS",
143
+ ccLink: "CC-Link",
144
+ ccLinkIef: "CC-Link IE Field"
145
+ };
146
+
147
+ class MelDeviceError extends Error {
148
+ constructor(message, result) {
149
+ super(message);
150
+ this.name = "MelDeviceError";
151
+ this.code = result.code;
152
+ this.result = result;
153
+ }
154
+ }
155
+
156
+ function canonicalSeries(value) {
157
+ if (typeof value !== "string") return null;
158
+ const compact = value.normalize("NFKC").trim().toUpperCase().replace(/[\s_-]/g, "");
159
+ return ({ IQR: "iQ-R", IQF: "iQ-F", MXR: "MX-R", MXF: "MX-F", CR800: "CR800", CR800R: "CR800", CR800D: "CR800", CR800Q: "CR800" })[compact] || null;
160
+ }
161
+
162
+ function cr800ModelFromSeries(value) {
163
+ if (typeof value !== "string") return null;
164
+ const compact = value.normalize("NFKC").trim().toUpperCase().replace(/[\s_-]/g, "");
165
+ return ({ CR800R: "CR800-R", CR800D: "CR800-D", CR800Q: "CR800-Q" })[compact] || null;
166
+ }
167
+
168
+ function normalizeModel(series, value) {
169
+ const s = canonicalSeries(series);
170
+ if (!s) return null;
171
+ if (s === "CR800" && value == null) return cr800ModelFromSeries(series);
172
+ if (typeof value !== "string") return null;
173
+ const model = value.normalize("NFKC").trim().toUpperCase().replace(/\s+/g, "");
174
+ if (s === "iQ-R") {
175
+ const match = /^R(00|01|02|04|08|16|32|120)(EN)?(?:CPU)?$/.exec(model);
176
+ if (!match) return null;
177
+ const canonical = `R${match[1]}${match[2] || ""}CPU`;
178
+ return IQ_R_MODELS.includes(canonical) ? canonical : null;
179
+ }
180
+ if (s === "iQ-F") {
181
+ for (const family of ["FX5UC", "FX5UJ", "FX5U", "FX5S"]) {
182
+ if (model === family || model.startsWith(`${family}-`)) return family;
183
+ }
184
+ return null;
185
+ }
186
+ if (s === "MX-R") {
187
+ if (model === "MXR300" || model === "MXR500") return model;
188
+ return MX_R_MODELS.includes(model) ? model : null;
189
+ }
190
+ if (s === "CR800") {
191
+ const compact = String(value).normalize("NFKC").trim().toUpperCase().replace(/[\s_-]/g, "");
192
+ return ({ R: "CR800-R", D: "CR800-D", Q: "CR800-Q", CR800R: "CR800-R", CR800D: "CR800-D", CR800Q: "CR800-Q" })[compact] || null;
193
+ }
194
+ if (model === "MXF100") return model;
195
+ return MX_F_MODELS.includes(model) ? model : null;
196
+ }
197
+
198
+ function getSupportedModels(series) {
199
+ const s = canonicalSeries(series);
200
+ const inferred = cr800ModelFromSeries(series);
201
+ if (inferred) return [inferred];
202
+ return s ? [...MODEL_LISTS[s]] : [];
203
+ }
204
+
205
+ function normalizeDevice(value) {
206
+ if (typeof value !== "string") return null;
207
+ return value.normalize("NFKC").trim().replace(/[¥¥]/g, "\\").toUpperCase();
208
+ }
209
+
210
+ function invalid(input, series, model, mode, code, message, extra = {}) {
211
+ return {
212
+ valid: false,
213
+ input,
214
+ normalized: extra.normalized ?? null,
215
+ series: series ?? null,
216
+ model: model ?? null,
217
+ mode,
218
+ code,
219
+ message,
220
+ configurationDependent: false,
221
+ source: series ? SOURCES[series] : null,
222
+ ...extra
223
+ };
224
+ }
225
+
226
+ function radixFor(series, prefix) {
227
+ const radix = RADIX[prefix];
228
+ if (radix === "series-x") return series === "iQ-F" ? 8 : 16;
229
+ return radix || 10;
230
+ }
231
+
232
+ function parseNumber(text, radix) {
233
+ const pattern = radix === 16 ? /^[0-9A-F]+$/ : radix === 8 ? /^[0-7]+$/ : /^\d+$/;
234
+ if (!pattern.test(text)) return null;
235
+ const value = Number.parseInt(text, radix);
236
+ return Number.isSafeInteger(value) ? value : null;
237
+ }
238
+
239
+ function parseDirect(text, series) {
240
+ const timerPrefixes = Object.keys(TIMER_ALIAS).sort((a, b) => b.length - a.length);
241
+ for (const alias of timerPrefixes) {
242
+ if (!text.startsWith(alias)) continue;
243
+ const addressText = text.slice(alias.length);
244
+ const address = parseNumber(addressText, 10);
245
+ if (address !== null) {
246
+ const [prefix, timerPart] = TIMER_ALIAS[alias];
247
+ return { kind: "direct", prefix, notationPrefix: alias, address, addressText, radix: 10, timerPart };
248
+ }
249
+ }
250
+ const prefixes = Object.keys(RADIX).sort((a, b) => b.length - a.length);
251
+ for (const prefix of prefixes) {
252
+ if (!text.startsWith(prefix)) continue;
253
+ const addressText = text.slice(prefix.length);
254
+ const radix = radixFor(series, prefix);
255
+ const address = parseNumber(addressText, radix);
256
+ if (address !== null) return { kind: "direct", prefix, notationPrefix: prefix, address, addressText, radix };
257
+ }
258
+ return null;
259
+ }
260
+
261
+ function parseBase(text, series) {
262
+ let match = /^U3E([0-9A-F]+)\\(HG|G)(\d+)$/.exec(text);
263
+ if (match) return {
264
+ kind: "cpu-buffer", prefix: match[2] === "HG" ? "HG" : "G", cpu: Number.parseInt(match[1], 16),
265
+ address: Number.parseInt(match[3], 10), addressText: match[3], radix: 10
266
+ };
267
+ match = /^U([0-9A-F]+)\\G(\d+)$/.exec(text);
268
+ if (match) return {
269
+ kind: "unit", prefix: "G", unit: Number.parseInt(match[1], 16), address: Number.parseInt(match[2], 10),
270
+ addressText: match[2], radix: 10
271
+ };
272
+ match = /^J(\d+)\\(SB|SW|X|Y|B|W)([0-9A-F]+)$/.exec(text);
273
+ if (match) return {
274
+ kind: "link", prefix: `J${match[2]}`, network: Number.parseInt(match[1], 10),
275
+ address: Number.parseInt(match[3], 16), addressText: match[3], radix: 16
276
+ };
277
+ return parseDirect(text, series);
278
+ }
279
+
280
+ function parseDevice(value, options = {}) {
281
+ const series = canonicalSeries(options.series);
282
+ if (!series || typeof value !== "string") return null;
283
+ const inputMode = options.inputMode || "exact";
284
+ const normalized = inputMode === "friendly" ? normalizeDevice(value) : value;
285
+ if (!normalized || (inputMode === "exact" && normalized !== normalizeDevice(normalized))) return null;
286
+ if (/\s/.test(normalized)) return null;
287
+
288
+ let rest = normalized;
289
+ let local = false;
290
+ let digit = null;
291
+ let indirect = false;
292
+ if (rest.startsWith("#")) { local = true; rest = rest.slice(1); }
293
+ const digitMatch = /^K([1-8])/.exec(rest);
294
+ if (digitMatch) { digit = Number.parseInt(digitMatch[1], 10); rest = rest.slice(2); }
295
+ if (rest.startsWith("@")) { indirect = true; rest = rest.slice(1); }
296
+
297
+ let parsed = parseBase(rest, series);
298
+ let bit = null;
299
+ let index = null;
300
+ let indexOrder = null;
301
+
302
+ if (!parsed) {
303
+ const candidates = [
304
+ [/^(.+?)(LZ|Z)(\d+)\.([0-9A-F])$/, "index-bit"],
305
+ [/^(.+?)\.([0-9A-F])(LZ|Z)(\d+)$/, "bit-index"],
306
+ [/^(.+?)\.([0-9A-F])$/, "bit"],
307
+ [/^(.+?)(LZ|Z)(\d+)$/, "index"]
308
+ ];
309
+ for (const [pattern, order] of candidates) {
310
+ const match = pattern.exec(rest);
311
+ if (!match) continue;
312
+ let baseText;
313
+ if (order === "index-bit") {
314
+ baseText = match[1]; index = { type: match[2], number: Number.parseInt(match[3], 10) }; bit = Number.parseInt(match[4], 16);
315
+ } else if (order === "bit-index") {
316
+ baseText = match[1]; bit = Number.parseInt(match[2], 16); index = { type: match[3], number: Number.parseInt(match[4], 10) };
317
+ } else if (order === "bit") {
318
+ baseText = match[1]; bit = Number.parseInt(match[2], 16);
319
+ } else {
320
+ baseText = match[1]; index = { type: match[2], number: Number.parseInt(match[3], 10) };
321
+ }
322
+ parsed = parseBase(baseText, series);
323
+ if (parsed) { indexOrder = order; break; }
324
+ bit = null; index = null;
325
+ }
326
+ }
327
+ if (!parsed) return null;
328
+ return { ...parsed, input: value, normalized, local, digit, indirect, bit, index, indexOrder };
329
+ }
330
+
331
+ function limitsFor(series, model, mode) {
332
+ if (series === "iQ-F") {
333
+ const max = model === "FX5UJ" ? IQF_MAX_UJ : IQF_MAX_COMMON;
334
+ const defaults = mode === "maximum" ? max : { ...max, Z: 19, LZ: 1 };
335
+ return { limits: defaults, maximum: max, defaultKnown: false };
336
+ }
337
+ if (series === "iQ-R") {
338
+ const low = ["R00CPU", "R01CPU", "R02CPU"].includes(model);
339
+ const maximum = {
340
+ ...IQR_MAX,
341
+ X: low ? 0x1fff : 0x2fff,
342
+ Y: low ? 0x1fff : 0x2fff,
343
+ P: model.startsWith("R120") ? 32767 : 16383
344
+ };
345
+ return { limits: mode === "maximum" ? maximum : (low ? IQR_DEFAULT_LOW : IQR_DEFAULT_HIGH), maximum, defaultKnown: true };
346
+ }
347
+ if (series === "MX-R") {
348
+ const is500 = model === "MXR500" || model.startsWith("MXR500-");
349
+ const defaults = { ...MXR_DEFAULT, P: is500 ? 16383 : 8191 };
350
+ const maximum = { ...MXR_MAX, P: is500 ? 32767 : 16383 };
351
+ return { limits: mode === "maximum" ? maximum : defaults, maximum, defaultKnown: true };
352
+ }
353
+ return { limits: mode === "maximum" ? MXF_MAX : MXF_DEFAULT, maximum: MXF_MAX, defaultKnown: true };
354
+ }
355
+
356
+ function validateModifiers(parsed, series, limits) {
357
+ if (parsed.local && (series === "iQ-F" || !["M", "L", "B", "F", "V", "S", "T", "ST", "LT", "LST", "C", "LC", "D", "W", "R", "ZR", "Z", "LZ", "P"].includes(parsed.prefix))) {
358
+ return ["LOCAL_NOT_SUPPORTED", "This device cannot use the local-device # prefix."];
359
+ }
360
+ if (parsed.digit !== null && (!BIT_PREFIXES.has(parsed.prefix) || parsed.bit !== null || parsed.indirect)) {
361
+ return ["DIGIT_MODIFIER_NOT_SUPPORTED", "K1-K8 digit designation is only valid for a bit device."];
362
+ }
363
+ const wordBitSet = series === "iQ-F" ? WORD_BIT_IQF : WORD_BIT_R;
364
+ if (parsed.bit !== null && !wordBitSet.has(parsed.prefix)) {
365
+ return ["BIT_SELECTION_NOT_SUPPORTED", `.${parsed.bit.toString(16).toUpperCase()} bit selection is not supported for ${parsed.prefix}.`];
366
+ }
367
+ if (parsed.indirect && parsed.digit !== null) return ["MODIFIER_COMBINATION", "Digit and indirect modifiers cannot be combined."];
368
+ if (parsed.indirect && !wordBitSet.has(parsed.prefix)) return ["INDIRECT_NOT_SUPPORTED", "@ indirect addressing requires a supported word device."];
369
+ if (parsed.index) {
370
+ const indexLimit = limits[parsed.index.type];
371
+ if (indexLimit === undefined || parsed.index.number > indexLimit) return ["INDEX_OUT_OF_RANGE", `${parsed.index.type}${parsed.index.number} is outside the selected mode range.`];
372
+ if (series === "iQ-F" && parsed.index.type === "LZ" && !["G"].includes(parsed.prefix)) {
373
+ return ["INDEX_NOT_SUPPORTED", "For iQ-F, LZ indexing is limited to unit access devices (and constants, which this package does not classify as devices)."];
374
+ }
375
+ }
376
+ return null;
377
+ }
378
+
379
+ function configuredLimit(prefix, options) {
380
+ const points = options.configuredPoints && options.configuredPoints[prefix];
381
+ return Number.isInteger(points) && points >= 0 ? points - 1 : null;
382
+ }
383
+
384
+ function validateCr800Features(features) {
385
+ if (features === undefined) return null;
386
+ if (!features || typeof features !== "object" || Array.isArray(features)) {
387
+ return "cr800Features must be an object when supplied.";
388
+ }
389
+ for (const [name, state] of Object.entries(features)) {
390
+ if (name === "ddevvl") {
391
+ if (!["disabled", "program-external", "status", "mixed"].includes(state)) {
392
+ return "cr800Features.ddevvl must be disabled, program-external, status, or mixed.";
393
+ }
394
+ continue;
395
+ }
396
+ if (!CR800_FEATURE_NAMES.has(name)) return `Unknown CR800 feature: ${name}.`;
397
+ if (typeof state !== "boolean") return `cr800Features.${name} must be boolean.`;
398
+ }
399
+ return null;
400
+ }
401
+
402
+ function inRange(address, start, end) {
403
+ return address >= start && address <= end;
404
+ }
405
+
406
+ function cr800Access(parsed, model, features = {}) {
407
+ const rules = [];
408
+ const warnings = [];
409
+ const addReadOnly = (allocation, state) => rules.push({ allocation, state, readOnly: true });
410
+ const addReadWrite = (allocation, state) => rules.push({ allocation, state, readOnly: false });
411
+ const feature = (name, readOnly = true) => {
412
+ const state = features[name];
413
+ (readOnly ? addReadOnly : addReadWrite)(CR800_ALLOCATION_LABELS[name], state);
414
+ };
415
+
416
+ if (parsed.kind === "direct") {
417
+ const { prefix, address } = parsed;
418
+
419
+ if ((model === "CR800-R" || model === "CR800-Q") && ["X", "Y"].includes(prefix)) {
420
+ feature("qxyread");
421
+ }
422
+ if (prefix === "X" && inRange(address, 0x384, 0x38b)) {
423
+ addReadOnly("hand I/O (always active)", true);
424
+ }
425
+
426
+ if (model === "CR800-D") {
427
+ if (["X", "Y"].includes(prefix) && inRange(address, 0, 0xff)) feature("parallelIoUnit");
428
+ if (["X", "Y"].includes(prefix) && inRange(address, 0, 0x3f)) feature("parallelIoInterface");
429
+ if (prefix === "X" && inRange(address, 0, 0xff)) feature("gotLink", false);
430
+ if (prefix === "Y" && inRange(address, 0, 0xff)) feature("gotLink");
431
+ if (prefix === "X" && inRange(address, 0x320, 0x327)) addReadOnly("STOP/SKIP I/O (always active)", true);
432
+ if (["X", "Y"].includes(prefix) && inRange(address, 0x7d0, 0x13cf)) feature("profibus");
433
+ if (["X", "Y"].includes(prefix) && inRange(address, 0x1770, 0x1f6f)) {
434
+ feature("ccLink");
435
+ feature("ccLinkIef");
436
+ }
437
+ if (prefix === "D" && inRange(address, 0, 511)) feature("ccLink");
438
+ if (prefix === "D" && inRange(address, 0, 2047)) feature("ccLinkIef");
439
+ }
440
+
441
+ if (prefix === "D" && inRange(address, 4096, 5119)) {
442
+ const state = features.ddevvl;
443
+ if (state === "status") addReadOnly(CR800_ALLOCATION_LABELS.ddevvl, true);
444
+ else if (state === "program-external") addReadWrite(CR800_ALLOCATION_LABELS.ddevvl, true);
445
+ else if (state === "mixed") {
446
+ rules.push({ allocation: CR800_ALLOCATION_LABELS.ddevvl, state: undefined, readOnly: true });
447
+ warnings.push("DDEVVL* contains mixed variable types; program external variables are read/write and status variables are read-only.");
448
+ } else if (state === undefined) {
449
+ rules.push({ allocation: CR800_ALLOCATION_LABELS.ddevvl, state: undefined, readOnly: true });
450
+ }
451
+ }
452
+ }
453
+
454
+ if (parsed.kind === "cpu-buffer") {
455
+ const { prefix, cpu, address } = parsed;
456
+ if (model === "CR800-R" && prefix === "HG") {
457
+ if (cpu >= 1 && cpu <= 3 && inRange(address, 0, 511)) {
458
+ addReadOnly("sequencer link input (always active)", true);
459
+ }
460
+ if (cpu >= 1 && cpu <= 3 && inRange(address, 512, 1023)) feature("iqmem");
461
+ }
462
+ if (model === "CR800-D" && prefix === "HG" && cpu === 1) {
463
+ if (inRange(address, 0, 511)) addReadOnly("sequencer link input (always active)", true);
464
+ if (inRange(address, 512, 1023)) feature("iqmem");
465
+ }
466
+ if (model === "CR800-Q" && prefix === "G" && cpu >= 1 && cpu <= 3) {
467
+ if (inRange(address, 10000, 10511)) addReadOnly("sequencer link input (always active)", true);
468
+ if (inRange(address, 10512, 11023) && features.iqmem !== false) {
469
+ rules.push({ allocation: CR800_ALLOCATION_LABELS.iqmem, state: undefined, readOnly: true });
470
+ warnings.push("Table 6-16 prints U3En\\G512-G1023 for this IQMEM read-only area, while tables 6-13, 5-3/5-4 and 6-4 use the G10000-based CR800-Q map. Access is therefore reported as configuration-dependent instead of inferred as read-only.");
471
+ }
472
+ }
473
+ }
474
+
475
+ const active = rules.filter(rule => rule.state === true);
476
+ const possible = rules.filter(rule => rule.state === undefined);
477
+ const activeReadOnly = active.filter(rule => rule.readOnly);
478
+ const possibleReadOnly = possible.filter(rule => rule.readOnly);
479
+ let access = "read-write";
480
+ if (activeReadOnly.length) access = "read-only";
481
+ else if (possibleReadOnly.length) access = "configuration-dependent";
482
+
483
+ return {
484
+ access,
485
+ activeAllocations: [...new Set(active.map(rule => rule.allocation))],
486
+ possibleAllocations: [...new Set(possible.map(rule => rule.allocation))],
487
+ warnings
488
+ };
489
+ }
490
+
491
+ function analyzeCr800(value, parsed, series, model, mode, normalized, operation, features) {
492
+ const rules = CR800_RULES[model];
493
+ const hasModifier = parsed.local || parsed.digit !== null || parsed.indirect || parsed.bit !== null || parsed.index !== null;
494
+ if (hasModifier) {
495
+ return invalid(value, series, model, mode, "MODIFIER_NOT_SUPPORTED", "The CR800 device table defines direct device notation without bit, digit, indirect, index, or local modifiers.", { normalized, parsed });
496
+ }
497
+
498
+ if (parsed.kind === "direct") {
499
+ const range = rules.direct[parsed.prefix];
500
+ if (!range) return invalid(value, series, model, mode, "DEVICE_NOT_SUPPORTED", `${parsed.prefix} is not supported by ${model}.`, { normalized, parsed });
501
+ if (mode !== "syntax" && (parsed.address < range[0] || parsed.address > range[1])) {
502
+ return invalid(value, series, model, mode, "ADDRESS_OUT_OF_RANGE", `${parsed.notationPrefix}${parsed.addressText} is outside the fixed ${model} range.`, { normalized, parsed });
503
+ }
504
+ } else {
505
+ if (parsed.kind !== "cpu-buffer") {
506
+ return invalid(value, series, model, mode, "DEVICE_NOT_SUPPORTED", `${parsed.kind} device notation is not listed for ${model}.`, { normalized, parsed });
507
+ }
508
+ const cpuRule = rules.cpu[parsed.prefix];
509
+ if (!cpuRule) return invalid(value, series, model, mode, "DEVICE_NOT_SUPPORTED", `U3En\\${parsed.prefix} is not supported by ${model}.`, { normalized, parsed });
510
+ if (!cpuRule.units.includes(parsed.cpu)) {
511
+ return invalid(value, series, model, mode, "CPU_NUMBER_OUT_OF_RANGE", `U3E${parsed.cpu.toString(16).toUpperCase()} is not a supported CPU selector for ${model}.`, { normalized, parsed });
512
+ }
513
+ if (mode !== "syntax" && (parsed.address < cpuRule.range[0] || parsed.address > cpuRule.range[1])) {
514
+ const qTableConflict = model === "CR800-Q" && parsed.prefix === "G"
515
+ && parsed.cpu >= 1 && parsed.cpu <= 3 && inRange(parsed.address, 512, 1023);
516
+ return invalid(value, series, model, mode, qTableConflict ? "MANUAL_RANGE_CONFLICT" : "ADDRESS_OUT_OF_RANGE",
517
+ qTableConflict
518
+ ? "Table 6-16 prints this IQMEM allocation, but it is outside the fixed CR800-Q range in table 6-13 and conflicts with the G10000-based maps in sections 5.2.1 and 6.2.2; it is conservatively rejected."
519
+ : `G/HG address is outside the fixed ${model} range ${cpuRule.range[0]}-${cpuRule.range[1]}.`,
520
+ { normalized, parsed });
521
+ }
522
+ }
523
+
524
+ if (mode === "syntax") {
525
+ return {
526
+ valid: true, input: value, normalized, series, model, mode, operation, code: "VALID",
527
+ message: "Valid CR800 device syntax; range and access were not evaluated in syntax mode.",
528
+ configurationDependent: false, access: "not-evaluated", operationAllowed: null,
529
+ activeAllocations: [], possibleAllocations: [], warnings: [], parsed, source: SOURCES[series]
530
+ };
531
+ }
532
+
533
+ const allocation = cr800Access(parsed, model, features);
534
+ const operationAllowed = operation === "read"
535
+ ? true
536
+ : operation === "write"
537
+ ? allocation.access === "read-write" ? true : allocation.access === "read-only" ? false : null
538
+ : null;
539
+ const warnings = [...allocation.warnings];
540
+ if (allocation.access === "read-only") {
541
+ warnings.push(`The active allocation is read-only: ${allocation.activeAllocations.join(", ")}. Writes are ignored.`);
542
+ } else if (allocation.access === "configuration-dependent") {
543
+ warnings.push(`Write access depends on the controller configuration: ${allocation.possibleAllocations.join(", ")}.`);
544
+ }
545
+ return {
546
+ valid: true, input: value, normalized, series, model, mode, operation, code: "VALID",
547
+ message: operation === "write" && operationAllowed === false
548
+ ? "Valid device notation and fixed range, but this address is read-only for the active CR800 allocation."
549
+ : operation === "write" && operationAllowed === null
550
+ ? "Valid device notation and fixed range; write access depends on the CR800 feature configuration."
551
+ : "Valid within the fixed CR800 device range.",
552
+ configurationDependent: allocation.access === "configuration-dependent",
553
+ access: allocation.access,
554
+ operationAllowed,
555
+ activeAllocations: allocation.activeAllocations,
556
+ possibleAllocations: allocation.possibleAllocations,
557
+ warnings,
558
+ parsed,
559
+ source: SOURCES[series]
560
+ };
561
+ }
562
+
563
+ function analyzeDevice(value, options = {}) {
564
+ const mode = options.mode || "default";
565
+ const inputMode = options.inputMode || "exact";
566
+ const operation = options.operation || "either";
567
+ if (!["syntax", "default", "maximum", "configured"].includes(mode)) {
568
+ return invalid(value, null, null, mode, "INVALID_MODE", "mode must be syntax, default, maximum, or configured.");
569
+ }
570
+ if (!["exact", "friendly"].includes(inputMode)) {
571
+ return invalid(value, null, null, mode, "INVALID_INPUT_MODE", "inputMode must be exact or friendly.");
572
+ }
573
+ if (!["read", "write", "either"].includes(operation)) {
574
+ return invalid(value, null, null, mode, "INVALID_OPERATION", "operation must be read, write, or either.");
575
+ }
576
+ const series = canonicalSeries(options.series);
577
+ if (!series) return invalid(value, null, null, mode, "UNSUPPORTED_SERIES", `Supported series: ${SERIES.join(", ")}.`);
578
+ const seriesModel = cr800ModelFromSeries(options.series);
579
+ const model = normalizeModel(options.series, options.model ?? seriesModel);
580
+ if (!model) return invalid(value, series, null, mode, "UNSUPPORTED_MODEL", `Unsupported ${series} model: ${String(options.model)}.`);
581
+ if (seriesModel && model !== seriesModel) return invalid(value, series, model, mode, "MODEL_SERIES_MISMATCH", `${String(options.series)} conflicts with model ${model}.`);
582
+ if (series === "CR800") {
583
+ const featureError = validateCr800Features(options.cr800Features);
584
+ if (featureError) return invalid(value, series, model, mode, "INVALID_CR800_FEATURES", featureError);
585
+ }
586
+ const parsed = parseDevice(value, { series, inputMode });
587
+ const normalized = inputMode === "friendly" ? normalizeDevice(value) : (typeof value === "string" ? value : null);
588
+ if (!parsed) return invalid(value, series, model, mode, "INVALID_SYNTAX", "The value is not a supported MELSEC/MELFA device notation.", { normalized });
589
+
590
+ if (series === "CR800") return analyzeCr800(value, parsed, series, model, mode, normalized, operation, options.cr800Features);
591
+
592
+ if (series === "iQ-F" && ["V", "LT", "LST", "ZR", "RD", "FX", "FY", "FD"].includes(parsed.prefix)) {
593
+ return invalid(value, series, model, mode, "DEVICE_NOT_SUPPORTED", `${parsed.prefix} is not listed as an iQ-F device in the attached manual.`, { normalized, parsed });
594
+ }
595
+ if (series === "iQ-F" && model === "FX5S" && parsed.kind === "unit") {
596
+ return invalid(value, series, model, mode, "MODEL_NOT_SUPPORTED", "FX5S does not support the U\\G unit access device.", { normalized, parsed });
597
+ }
598
+ if (series === "iQ-F" && parsed.kind === "cpu-buffer") {
599
+ return invalid(value, series, model, mode, "DEVICE_NOT_SUPPORTED", "CPU buffer access notation is not listed for iQ-F in the attached manual.", { normalized, parsed });
600
+ }
601
+
602
+ const { limits, maximum, defaultKnown } = limitsFor(series, model, mode);
603
+ const modifierError = validateModifiers(parsed, series, limits);
604
+ if (modifierError) return invalid(value, series, model, mode, modifierError[0], modifierError[1], { normalized, parsed });
605
+
606
+ if (parsed.kind === "link") {
607
+ if (series === "iQ-F") return invalid(value, series, model, mode, "DEVICE_NOT_SUPPORTED", "Link direct Jn\\ devices are not listed in the iQ-F device table.", { normalized, parsed });
608
+ if (parsed.network < 1 || parsed.network > 239) return invalid(value, series, model, mode, "NETWORK_OUT_OF_RANGE", "Network number must be 1-239.", { normalized, parsed });
609
+ const maxBySeries = series === "iQ-R"
610
+ ? { JX: 0x27fff, JY: 0x27fff, JB: 0x9ffff, JSB: 0x13ff, JW: 0x27ffff, JSW: 0x13ff }
611
+ : series === "MX-F"
612
+ ? { JX: 0xa175, JY: 0xa175, JB: -1, JSB: 0x51ff, JW: 0xa7ff, JSW: 0x51ff }
613
+ : { JX: 0x27fff, JY: 0x27fff, JB: 0x9ffff, JSB: 0x13ff, JW: 0x27ffff, JSW: 0x13ff };
614
+ if (parsed.address > maxBySeries[parsed.prefix]) return invalid(value, series, model, mode, "ADDRESS_OUT_OF_RANGE", "Link-direct address exceeds the controller-level maximum documented for this series.", { normalized, parsed });
615
+ return { valid: true, input: value, normalized, series, model, mode, code: "VALID", message: "Valid link-direct device notation; the actual range depends on the configured network.", configurationDependent: true, parsed, source: SOURCES[series] };
616
+ }
617
+
618
+ if (parsed.kind === "unit" || parsed.kind === "cpu-buffer") {
619
+ const maxAddress = parsed.kind === "cpu-buffer"
620
+ ? series === "MX-R" ? 524287 : series === "MX-F" ? 16809983 : 268435455
621
+ : 268435455;
622
+ if (parsed.address > maxAddress) return invalid(value, series, model, mode, "ADDRESS_OUT_OF_RANGE", `Buffer-memory address must be 0-${maxAddress}.`, { normalized, parsed });
623
+ return { valid: true, input: value, normalized, series, model, mode, code: "VALID", message: "Valid access-device notation; existence and actual range depend on the installed unit or CPU buffer.", configurationDependent: true, parsed, source: SOURCES[series] };
624
+ }
625
+
626
+ if (mode === "syntax") {
627
+ return { valid: true, input: value, normalized, series, model, mode, code: "VALID", message: "Valid syntax for the selected series and model.", configurationDependent: CONFIGURABLE.has(parsed.prefix), parsed, source: SOURCES[series] };
628
+ }
629
+
630
+ const rangePrefix = parsed.prefix === "DX" ? "X" : parsed.prefix === "DY" ? "Y" : parsed.prefix;
631
+ let limit = limits[rangePrefix];
632
+ if (limit === undefined) return invalid(value, series, model, mode, "DEVICE_NOT_SUPPORTED", `${parsed.prefix} is not supported for ${series}.`, { normalized, parsed });
633
+ if (mode === "configured" && CONFIGURABLE.has(rangePrefix)) {
634
+ const configured = configuredLimit(rangePrefix, options);
635
+ if (configured === null) return invalid(value, series, model, mode, "MISSING_CONFIGURATION", `configuredPoints.${rangePrefix} is required in configured mode.`, { normalized, parsed });
636
+ const absolute = maximum[rangePrefix];
637
+ if (absolute !== undefined && configured > absolute) return invalid(value, series, model, mode, "INVALID_CONFIGURATION", `configuredPoints.${rangePrefix} exceeds the documented maximum.`, { normalized, parsed });
638
+ limit = configured;
639
+ }
640
+ if (parsed.address > limit || limit < 0) {
641
+ const potentiallyConfigurable = mode === "default" && maximum[rangePrefix] >= parsed.address && CONFIGURABLE.has(rangePrefix);
642
+ return invalid(value, series, model, mode, potentiallyConfigurable ? "REQUIRES_CONFIGURATION" : "ADDRESS_OUT_OF_RANGE",
643
+ potentiallyConfigurable
644
+ ? `${parsed.notationPrefix || parsed.prefix}${parsed.addressText} is outside the default range but within the documented configurable maximum.`
645
+ : `${parsed.notationPrefix || parsed.prefix}${parsed.addressText} is outside the selected mode range.`,
646
+ { normalized, parsed, configurationDependent: potentiallyConfigurable, suggestedMode: potentiallyConfigurable ? "maximum" : undefined });
647
+ }
648
+ const configurationDependent = mode === "maximum" && CONFIGURABLE.has(rangePrefix)
649
+ || mode === "default" && series === "iQ-F" && CONFIGURABLE.has(rangePrefix) && !defaultKnown;
650
+ return {
651
+ valid: true,
652
+ input: value,
653
+ normalized,
654
+ series,
655
+ model,
656
+ mode,
657
+ code: "VALID",
658
+ message: configurationDependent
659
+ ? "Valid within the documented model/maximum range; confirm the project device-point configuration."
660
+ : "Valid for the selected mode.",
661
+ configurationDependent,
662
+ parsed,
663
+ source: SOURCES[series]
664
+ };
665
+ }
666
+
667
+ function isValidDevice(value, options) {
668
+ return analyzeDevice(value, options).valid;
669
+ }
670
+
671
+ function assertValidDevice(value, options) {
672
+ const result = analyzeDevice(value, options);
673
+ if (!result.valid) throw new MelDeviceError(result.message, result);
674
+ return result;
675
+ }
676
+
677
+ module.exports = {
678
+ MelDeviceError,
679
+ analyzeDevice,
680
+ assertValidDevice,
681
+ getSupportedModels,
682
+ isValidDevice,
683
+ normalizeDevice,
684
+ normalizeModel,
685
+ parseDevice
686
+ };