import { CIP } from "../enip"; const { LOGICAL } = CIP.EPATH.segments; const { MessageRouter } = CIP; const { MULTIPLE_SERVICE_PACKET } = MessageRouter.services; import equals from "deep-equal"; import type Tag from "../tag"; type tagGroupState = { tags: object, path: Buffer, timestamp: Date } type readMessageRequests = { data: Buffer, tag_ids: string[] } type writeTagMessageRequests = { data: Buffer, tag: Tag, tag_ids: string[] } class TagGroup { state: tagGroupState; /** * Tag Group Class used for reading and writing multiple tags at once */ constructor() { const pathBuf = Buffer.concat([ LOGICAL.build(LOGICAL.types.ClassID, 0x02), // Message Router Class ID (0x02) LOGICAL.build(LOGICAL.types.InstanceID, 0x01) // Instance ID (0x01) ]); this.state = { tags: {}, path: pathBuf, timestamp: new Date() }; } /** * Fetches the Number of Tags * * @readonly * @returns Number of tags */ get length(): number { return Object.keys(this.state.tags).length; } // endregion /** * Adds Tag to Group * * @param tag - Tag to Add to Group */ add(tag: Tag): void { if (!this.state.tags[tag.instance_id]) this.state.tags[tag.instance_id] = tag; } /** * Removes Tag from Group * * @param tag - Tag to be Removed from Group */ remove(tag: Tag) { if (this.state.tags[tag.instance_id]) delete this.state.tags[tag.instance_id]; } /** * Iterable, Allows user to Iterate of each Tag in Group * * @param callback - Accepts Tag Class */ forEach(callback: (tag: Tag) => {}) { for (let key of Object.keys(this.state.tags)) callback(this.state.tags[key]); } /** * Generates Array of Messages to Compile into a Multiple * Service Request * * @returns Array of Read Tag Message Services */ generateReadMessageRequests(): readMessageRequests[] { const { tags } = this.state; // Initialize Variables let messages = []; let msgArr = []; let tagIds = []; let messageLength = 0; // Loop Over Tags in List for (let key of Object.keys(tags)) { const tag = tags[key]; // Build Current Message let msg = tag.generateReadMessageRequest(); messageLength += msg.length + 2; tagIds.push(tag.instance_id); msgArr.push(msg); // If Current Message Length is > 350 Bytes then Assemble Message and Move to Next Message if (messageLength >= 300) { let buf = Buffer.alloc(2 + 2 * msgArr.length); buf.writeUInt16LE(msgArr.length, 0); let ptr = 2; let offset = buf.length; for (let i = 0; i < msgArr.length; i++) { buf.writeUInt16LE(offset, ptr); ptr += 2; offset += msgArr[i].length; } buf = Buffer.concat([buf, ...msgArr]); buf = MessageRouter.build(MULTIPLE_SERVICE_PACKET, this.state.path, buf); messages.push({ data: buf, tag_ids: tagIds }); messageLength = 0; msgArr = []; tagIds = []; } } // Assemble and Push Last Message if (msgArr.length > 0) { let buf = Buffer.alloc(2 + 2 * msgArr.length); buf.writeUInt16LE(msgArr.length, 0); let ptr = 2; let offset = buf.length; for (let i = 0; i < msgArr.length; i++) { buf.writeUInt16LE(offset, ptr); ptr += 2; offset += msgArr[i].length; } buf = Buffer.concat([buf, ...msgArr]); buf = MessageRouter.build(MULTIPLE_SERVICE_PACKET, this.state.path, buf); messages.push({ data: buf, tag_ids: tagIds }); } return messages; } /** * Parse Incoming Multi Service Request Messages * * @param responses - response from controller * @param ids - Tag ids */ parseReadMessageResponses(responses: any[], ids: string[]) { for (let i = 0; i < ids.length; i++) { if(responses[i].generalStatusCode === 0) this.state.tags[ids[i]].parseReadMessageResponse(responses[i].data); if(responses[i].generalStatusCode === 4) this.state.tags[ids[i]].unknownTag(); } } /** * Generates Array of Messages to Compile into a Multiple * Service Request * * @returns Array of Write Tag Message Services */ generateWriteMessageRequests(): writeTagMessageRequests[] { const { tags } = this.state; // Initialize Variables let messages = []; let msgArr = []; let tagIds = []; let messageLength = 0; // Loop Over Tags in List for (let key of Object.keys(tags)) { const tag = tags[key]; if (tag.value !== null && tag.type === "STRUCT") tag.writeObjToValue(); if (tag.value !== null && !equals(tag.state.tag.value, tag.controller_value)) { // Build Current Message let msg = tag.generateWriteMessageRequest(); if (tag.type !== "STRUCT") { messageLength += msg.length + 2; tagIds.push(tag.instance_id); msgArr.push(msg); // If Current Message Length is > 350 Bytes then Assemble Message and Move to Next Message if (messageLength >= 350) { let buf = Buffer.alloc(2 + 2 * msgArr.length); buf.writeUInt16LE(msgArr.length, 0); let ptr = 2; let offset = buf.length; for (let i = 0; i < msgArr.length; i++) { buf.writeUInt16LE(offset, ptr); ptr += 2; offset += msgArr[i].length; } buf = Buffer.concat([buf, ...msgArr]); buf = MessageRouter.build(MULTIPLE_SERVICE_PACKET, this.state.path, buf); messages.push({ data: buf, tag: null, tag_ids: tagIds }); messageLength = 0; msgArr = []; tagIds = []; } } else { messages.push({data: null, tag: tag, tagIds: null}); // Single tag pushed to indicate need to write single STRUCT tag } } } // Assemble and Push Last Message if (msgArr.length > 0) { let buf = Buffer.alloc(2 + 2 * msgArr.length); buf.writeUInt16LE(msgArr.length, 0); let ptr = 2; let offset = buf.length; for (let i = 0; i < msgArr.length; i++) { buf.writeUInt16LE(offset, ptr); ptr += 2; offset += msgArr[i].length; } buf = Buffer.concat([buf, ...msgArr]); buf = MessageRouter.build(MULTIPLE_SERVICE_PACKET, this.state.path, buf); messages.push({ data: buf, tag: null, tag_ids: tagIds }); } return messages; } /** * Parse Incoming Multi Service Request Messages * * @param ids */ parseWriteMessageRequests(ids: string[]) { for (let id of ids) { this.state.tags[id].unstageWriteRequest(); } } // endregion // region Private Methods // endregion // region Event Handlers // endregion } export default TagGroup;