ethernet-ip
Version:
A feature-complete EtherNet/IP client for Rockwell ControlLogix/CompactLogix PLCs
210 lines • 8.43 kB
JavaScript
;
/**
* Tag list discovery — Get Instance Attribute List (class 0x6B).
* Per CIP Vol 1, Chapter 7 — Symbol Object
*
* Paginates with status 0x06 (partial transfer).
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseTagType = parseTagType;
exports.isUserTag = isUserTag;
exports.extractProgramNames = extractProgramNames;
exports.discoverAll = discoverAll;
exports.discoverUserTags = discoverUserTags;
const MessageRouter = __importStar(require("../cip/message-router"));
const services_1 = require("../cip/services");
const epath_1 = require("../cip/epath");
const encapsulation_1 = require("../encapsulation/encapsulation");
const header_1 = require("../encapsulation/header");
/** CIP status code for partial transfer (more data available) */
const STATUS_PARTIAL_TRANSFER = 0x06;
/** Fixed overhead per tag entry: id(4) + nameLen(2) + type(2) + dimSizes(12) */
const TAG_ENTRY_OVERHEAD = 20;
/** Offset to CIP data within SendRRData response payload */
const CIP_DATA_OFFSET = 16;
/** Tag type bit-packing per CIP spec */
const TAG_TYPE_STRUCT_FLAG = 0x8000;
const TAG_TYPE_RESERVED_FLAG = 0x1000;
const TAG_TYPE_ARRAY_DIMS_MASK = 0x6000;
const TAG_TYPE_ARRAY_DIMS_SHIFT = 13;
const TAG_TYPE_CODE_MASK = 0x0fff;
/**
* Parse the tag type bit field.
* Bit 15 = struct, bits 14-13 = array dims, bit 12 = reserved, bits 11-0 = type code
*/
function parseTagType(raw) {
return {
code: raw & TAG_TYPE_CODE_MASK,
isStruct: !!(raw & TAG_TYPE_STRUCT_FLAG),
isReserved: !!(raw & TAG_TYPE_RESERVED_FLAG),
arrayDims: (raw & TAG_TYPE_ARRAY_DIMS_MASK) >> TAG_TYPE_ARRAY_DIMS_SHIFT,
dimSizes: [],
};
}
/**
* Build the CIP request for Get Instance Attribute List.
* Requests attributes 1 (Symbol Name) and 2 (Symbol Type).
*/
function buildTagListRequest(instanceId, program) {
const pathBuilder = new epath_1.EPathBuilder();
// Program scope prefix
if (program) {
pathBuilder.symbolic(`Program:${program}`);
}
// Symbol Object class 0x6B
pathBuilder.logical(epath_1.LogicalType.ClassID, 0x6b);
// Start instance (0 = beginning)
if (instanceId === 0) {
// Instance 0: use raw bytes for the zero-instance encoding
pathBuilder.logical(epath_1.LogicalType.InstanceID, 0);
}
else {
pathBuilder.logical(epath_1.LogicalType.InstanceID, instanceId);
}
const path = pathBuilder.build();
// Request data: attribute count(2) + attribute 1(2) + attribute 2(2) + attribute 8(2)
const ATTRIBUTE_COUNT = 3;
const requestData = Buffer.alloc(8);
requestData.writeUInt16LE(ATTRIBUTE_COUNT, 0);
requestData.writeUInt16LE(0x01, 2); // Attribute 1: Symbol Name
requestData.writeUInt16LE(0x02, 4); // Attribute 2: Symbol Type
requestData.writeUInt16LE(0x08, 6); // Attribute 8: Dimension Sizes (3 × UINT32)
return MessageRouter.build(services_1.CIPService.GET_INSTANCE_ATTRIBUTE_LIST, path, requestData);
}
/**
* Parse a tag list response buffer into DiscoveredTag entries.
* Returns the last instance ID for pagination.
*/
function parseTagListResponse(data, program) {
const tags = [];
let lastInstanceId = 0;
let offset = 0;
while (offset + TAG_ENTRY_OVERHEAD <= data.length) {
const id = data.readUInt32LE(offset);
const nameLength = data.readUInt16LE(offset + 4);
if (offset + nameLength + TAG_ENTRY_OVERHEAD > data.length)
break;
const name = data.subarray(offset + 6, offset + 6 + nameLength).toString('ascii');
const rawType = data.readUInt16LE(offset + 6 + nameLength);
const type = parseTagType(rawType);
// Attribute 8: three UINT32LE dimension sizes (zero-padded for unused dims)
const dimBase = offset + 8 + nameLength;
for (let d = 0; d < type.arrayDims; d++) {
type.dimSizes.push(data.readUInt32LE(dimBase + d * 4));
}
tags.push({ id, name, type, program });
lastInstanceId = id;
offset += nameLength + TAG_ENTRY_OVERHEAD;
}
return { tags, lastInstanceId };
}
/**
* Filter rules per Rockwell Data Access manual (1756-PM020D, Step 2):
*
* 1. Discard tags with bit 12 set (isReserved — system tags)
* 2. Discard names starting with "__" (system tags)
* 3. Discard names containing ":" UNLESS the prefix is "Program"
* - "Program:X" entries are program scope markers, not readable tags
* - "Map:", "Task:", "Cxn:", module I/O like "Codesys:I" are discarded
*
* Returns true if the tag should be kept.
*/
function isUserTag(tag) {
if (tag.type.isReserved)
return false;
if (tag.name.startsWith('__'))
return false;
const colonIdx = tag.name.indexOf(':');
if (colonIdx !== -1 && tag.name.substring(0, colonIdx) !== 'Program')
return false;
return true;
}
/**
* Extract program names from discovered tags.
* "Program:MainProgram" → "MainProgram"
* Note: Program entries typically have bit 12 (reserved) set.
*/
function extractProgramNames(tags) {
return tags.filter((t) => t.name.startsWith('Program:')).map((t) => t.name.substring(8));
}
/**
* Discover all tags from the PLC.
* Paginates automatically when status 0x06 is returned.
*/
async function discoverAll(pipeline, sessionId, timeoutMs, program) {
const allTags = [];
let instanceId = 0;
let hasMore = true;
while (hasMore) {
const cipRequest = buildTagListRequest(instanceId, program);
const eipPacket = (0, encapsulation_1.sendRRData)(sessionId, cipRequest);
const response = await pipeline.send(eipPacket, timeoutMs);
const eipParsed = (0, header_1.parseHeader)(response);
const cipData = eipParsed.data.subarray(CIP_DATA_OFFSET);
const mrResponse = MessageRouter.parse(cipData);
if (mrResponse.generalStatusCode !== 0 &&
mrResponse.generalStatusCode !== STATUS_PARTIAL_TRANSFER) {
break; // Error — stop pagination
}
const { tags, lastInstanceId } = parseTagListResponse(mrResponse.data, program ?? null);
allTags.push(...tags);
if (mrResponse.generalStatusCode === STATUS_PARTIAL_TRANSFER) {
instanceId = lastInstanceId + 1;
}
else {
hasMore = false;
}
}
return allTags;
}
/**
* Discover all user tags, including program-scoped tags.
* 1. Discover controller-scope tags
* 2. Extract program names from "Program:X" entries
* 3. Discover tags within each program
* 4. Filter to user-created tags only
*/
async function discoverUserTags(pipeline, sessionId, timeoutMs) {
const controllerTags = await discoverAll(pipeline, sessionId, timeoutMs);
const programs = extractProgramNames(controllerTags);
const programTags = [];
for (const prog of programs) {
const tags = await discoverAll(pipeline, sessionId, timeoutMs, prog);
programTags.push(...tags);
}
return [...controllerTags, ...programTags].filter(isUserTag);
}
//# sourceMappingURL=discovery.js.map