ethernet-ip
Version:
A feature-complete EtherNet/IP client for Rockwell ControlLogix/CompactLogix PLCs
178 lines • 8.2 kB
JavaScript
;
/**
* Batch Builder — packs multiple CIP service requests into
* Multi-Service Packet (0x0A) batches that fit within the
* negotiated connection size.
*
* Tracks BOTH request and response sizes to avoid overflow.
* Per CIP Vol 1, Appendix A-7 — Multiple Service Packet
*
* Request packet structure (connected):
* EIP Header (24) + InterfaceHandle (4) + Timeout (2) + CPF (varies)
* └─ Connected Data Item: SequenceCount (2) + CIP data
* └─ MR header: Service (1) + PathSize (1) + Path (4)
* └─ Multi-Service: Count (2) + Offsets (2×N) + ServiceData[]
*
* Response packet structure (connected):
* EIP Header (24) + InterfaceHandle (4) + Timeout (2) + CPF (varies)
* └─ Connected Data Item: SequenceCount (2) + CIP data
* └─ MR header: Service (1) + Reserved (1) + Status (1) + ExtSize (1)
* └─ Multi-Service: Count (2) + Offsets (2×N) + Replies[]
* └─ Each reply: Service (1) + Reserved (1) + Status (1) + ExtSize (1) + Data
*
* The connectionSize limits the Connected Data Item payload (seq + CIP).
* For unconnected (UCMM), there is no sequence count — CIP data goes directly.
*/
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.buildBatches = buildBatches;
exports.parseMultiServiceResponse = parseMultiServiceResponse;
const MessageRouter = __importStar(require("./message-router"));
const services_1 = require("./services");
const logical_1 = require("./epath/segments/logical");
/** Message Router path: Class 0x02, Instance 0x01 */
const MESSAGE_ROUTER_PATH = Buffer.concat([
(0, logical_1.buildLogicalSegment)(logical_1.LogicalType.ClassID, 0x02),
(0, logical_1.buildLogicalSegment)(logical_1.LogicalType.InstanceID, 0x01),
]);
/**
* MR wrapper overhead added by assembleBatch:
* service(1) + pathSize(1) + path(4 for class 0x02 instance 0x01) = 6 bytes
*/
const MR_REQUEST_OVERHEAD = 2 + MESSAGE_ROUTER_PATH.length;
/**
* MR response header: service(1) + reserved(1) + status(1) + extStatusSize(1) = 4 bytes
*/
const MR_RESPONSE_OVERHEAD = 4;
/**
* Connected transport overhead: sequence count (2 bytes) prepended to CIP data.
* The connection size limits sequenceCount + CIP data, so usable CIP space
* is connectionSize - CONNECTED_TRANSPORT_OVERHEAD.
*/
const CONNECTED_TRANSPORT_OVERHEAD = 2;
/** Overhead bytes in the request multi-service packet (before services) */
const REQUEST_BASE_OVERHEAD = 2; // service count (UINT16LE)
/** Per-service overhead in request: offset entry (UINT16LE) */
const REQUEST_PER_SERVICE_OVERHEAD = 2;
/** Overhead bytes in the response multi-service packet */
const RESPONSE_BASE_OVERHEAD = 2; // service count
/** Per-service overhead in response: offset(2) + reply header(4: service, reserved, status, extStatusLen) */
const RESPONSE_PER_SERVICE_OVERHEAD = 6;
/**
* Split an array of CIP service requests into optimally-packed
* Multi-Service Packet batches.
*
* Accounts for all protocol overhead:
* Connected: connectionSize ≥ seqCount(2) + MR(6) + multiSvc header + services
* Unconnected: connectionSize ≥ MR(6) + multiSvc header + services
*
* @param requests - Individual CIP service requests with size estimates
* @param connectionSize - Max packet size (4002 Large, 504 Small, 508 UCMM)
* @param connected - True for connected messaging (sequence count overhead)
*/
function buildBatches(requests, connectionSize, connected) {
if (requests.length === 0)
return [];
if (requests.length === 1) {
// Single request — no need for multi-service wrapping
return [{ requests, data: requests[0].serviceData }];
}
const transportOverhead = connected ? CONNECTED_TRANSPORT_OVERHEAD : 0;
const maxCipPayload = connectionSize - transportOverhead;
// Fixed overhead per batch: MR wrapper + multi-service count field
const requestFixedOverhead = MR_REQUEST_OVERHEAD + REQUEST_BASE_OVERHEAD;
const responseFixedOverhead = MR_RESPONSE_OVERHEAD + RESPONSE_BASE_OVERHEAD;
const batches = [];
let currentRequests = [];
let requestSize = requestFixedOverhead;
let responseSize = responseFixedOverhead;
for (const req of requests) {
const nextRequestSize = requestSize + REQUEST_PER_SERVICE_OVERHEAD + req.serviceData.length;
const nextResponseSize = responseSize + RESPONSE_PER_SERVICE_OVERHEAD + req.estimatedResponseSize;
if (currentRequests.length > 0 &&
(nextRequestSize > maxCipPayload || nextResponseSize > maxCipPayload)) {
batches.push(assembleBatch(currentRequests));
currentRequests = [];
requestSize = requestFixedOverhead;
responseSize = responseFixedOverhead;
}
currentRequests.push(req);
requestSize += REQUEST_PER_SERVICE_OVERHEAD + req.serviceData.length;
responseSize += RESPONSE_PER_SERVICE_OVERHEAD + req.estimatedResponseSize;
}
batches.push(assembleBatch(currentRequests));
return batches;
}
/**
* Assemble a batch of requests into a Multi-Service Packet.
*/
function assembleBatch(requests) {
const serviceCount = requests.length;
// Service count (2 bytes) + offset table (2 bytes per service)
const headerSize = REQUEST_BASE_OVERHEAD + serviceCount * REQUEST_PER_SERVICE_OVERHEAD;
// Build offset table
const header = Buffer.alloc(headerSize);
header.writeUInt16LE(serviceCount, 0); // Service count
let currentOffset = headerSize; // First service starts after header
for (let i = 0; i < serviceCount; i++) {
header.writeUInt16LE(currentOffset, REQUEST_BASE_OVERHEAD + i * 2); // Offset for service i
currentOffset += requests[i].serviceData.length;
}
// Concatenate header + all service data
const payload = Buffer.concat([header, ...requests.map((r) => r.serviceData)]);
// Wrap in Message Router targeting Message Router object
const data = MessageRouter.build(services_1.CIPService.MULTIPLE_SERVICE_PACKET, MESSAGE_ROUTER_PATH, payload);
return { requests, data };
}
/**
* Parse a Multi-Service Packet response into individual MR responses.
*
* Response layout (after MR header is stripped):
* [count(2), offset0(2), offset1(2), ..., reply0, reply1, ...]
*
* Offsets are relative to the count field.
* Each reply is a standard Message Router response.
*/
function parseMultiServiceResponse(data) {
const count = data.readUInt16LE(0);
const replies = [];
for (let i = 0; i < count; i++) {
const offset = data.readUInt16LE(2 + i * 2);
const nextOffset = i + 1 < count ? data.readUInt16LE(2 + (i + 1) * 2) : data.length;
replies.push(MessageRouter.parse(data.subarray(offset, nextOffset)));
}
return replies;
}
//# sourceMappingURL=batch-builder.js.map