UNPKG

ethernet-ip

Version:

A feature-complete EtherNet/IP client for Rockwell ControlLogix/CompactLogix PLCs

210 lines 8.43 kB
"use strict"; /** * 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