UNPKG

@raydotac/mcprotocol

Version:

Mitsubishi MC Protocol implementation for Node.js - TypeScript and JavaScript versions. Inspired by pymcprotocol with support for iQ-R, Q, iQ-L, L, and QnA series PLCs.

420 lines 17.5 kB
"use strict"; /** * MC Protocol TypeScript Implementation * Compatible with Mitsubishi Q Series PLCs using 4E Frame Protocol */ 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.MCProtocol = exports.MCProtocolError = void 0; const net = __importStar(require("net")); class MCProtocolError extends Error { constructor(message, errorCode, plcErrorCode) { super(message); this.errorCode = errorCode; this.plcErrorCode = plcErrorCode; this.name = 'MCProtocolError'; } } exports.MCProtocolError = MCProtocolError; class MCProtocol { constructor(options) { this.socket = null; this.isConnected = false; this.responseBuffer = Buffer.alloc(0); this.pendingRequests = new Map(); this.requestCounter = 0; this.wordSize = 2; // bytes per word in binary mode this.options = { timeout: 5000, plcType: 'Q', frame: '4E', // Default to 4E frame to match JavaScript version ascii: false, ...options }; } async connect() { return new Promise((resolve, reject) => { if (this.isConnected) { resolve(); return; } this.socket = new net.Socket(); const connectTimeout = setTimeout(() => { this.socket?.destroy(); reject(new MCProtocolError('Connection timeout')); }, this.options.timeout); this.socket.connect(this.options.port, this.options.host, () => { clearTimeout(connectTimeout); this.isConnected = true; resolve(); }); this.socket.on('error', (error) => { clearTimeout(connectTimeout); this.isConnected = false; reject(new MCProtocolError(`Connection error: ${error.message}`)); }); this.socket.on('close', () => { this.isConnected = false; this.cleanup(); }); this.socket.on('data', (data) => { this.handleResponse(data); }); }); } async disconnect() { return new Promise((resolve) => { if (!this.socket || !this.isConnected) { resolve(); return; } this.socket.once('close', () => { resolve(); }); this.socket.destroy(); this.cleanup(); }); } getIsConnected() { return this.isConnected && this.socket !== null; } async readRegister(device, address) { const result = await this.batchReadWordUnits([{ device, address }]); const key = `${device}${address}`; if (result[key] === undefined) { throw new MCProtocolError(`No data received for ${key}`); } return result[key]; } async writeRegister(device, address, value) { await this.batchWriteWordUnits([{ device, address, value }]); } async batchReadWordUnits(addresses) { if (!this.isConnected || !this.socket) { throw new MCProtocolError('Not connected to PLC'); } const results = {}; for (const addr of addresses) { const frame = this.create4EReadFrame(addr.device, addr.address, addr.count || 1); const response = await this.sendRequest(frame); const values = this.parseReadResponse(response); for (let i = 0; i < values.length; i++) { const key = `${addr.device}${addr.address + i}`; results[key] = values[i]; } } return results; } async batchWriteWordUnits(data) { if (!this.isConnected || !this.socket) { throw new MCProtocolError('Not connected to PLC'); } for (const item of data) { const frame = this.create4EWriteFrame(item.device, item.address, [item.value]); await this.sendRequest(frame); } } create4EReadFrame(device, address, count) { let requestData = Buffer.alloc(0); // Command and subcommand const command = 0x0401; const subcommand = this.options.plcType === 'iQ-R' ? 0x0002 : 0x0000; requestData = Buffer.concat([requestData, this.encodeValue(command, "short")]); requestData = Buffer.concat([requestData, this.encodeValue(subcommand, "short")]); // Device data requestData = Buffer.concat([requestData, this.makeDeviceData(device, address)]); requestData = Buffer.concat([requestData, this.encodeValue(count, "short")]); // Create 4E frame header let mcData = Buffer.alloc(0); // Subheader (big endian for 4E type - 0x5400) const subheaderBuffer = Buffer.alloc(2); subheaderBuffer.writeUInt16BE(0x5400, 0); mcData = Buffer.concat([mcData, subheaderBuffer]); // Add other header fields mcData = Buffer.concat([mcData, this.encodeValue(0x0000, "short")]); // subheaderserial mcData = Buffer.concat([mcData, this.encodeValue(0, "short")]); mcData = Buffer.concat([mcData, this.encodeValue(0, "byte")]); // network mcData = Buffer.concat([mcData, this.encodeValue(0xFF, "byte")]); // pc mcData = Buffer.concat([mcData, this.encodeValue(0x03FF, "short")]); // dest_moduleio mcData = Buffer.concat([mcData, this.encodeValue(0x00, "byte")]); // dest_modulesta // Add data length + timer size mcData = Buffer.concat([mcData, this.encodeValue(this.wordSize + requestData.length, "short")]); mcData = Buffer.concat([mcData, this.encodeValue(4, "short")]); // timer mcData = Buffer.concat([mcData, requestData]); // Debug: Log the request being sent // Debug: console.log(`Sending request: ${mcData.toString('hex')}`) // Debug: console.log(`Request data length: ${requestData.length}, wordSize: ${this.wordSize}`) return mcData; } create4EWriteFrame(device, address, values) { let requestData = Buffer.alloc(0); // Command and subcommand const command = 0x1401; // Write command const subcommand = this.options.plcType === 'iQ-R' ? 0x0002 : 0x0000; requestData = Buffer.concat([requestData, this.encodeValue(command, "short")]); requestData = Buffer.concat([requestData, this.encodeValue(subcommand, "short")]); // Device data requestData = Buffer.concat([requestData, this.makeDeviceData(device, address)]); requestData = Buffer.concat([requestData, this.encodeValue(values.length, "short")]); // Add values for (const value of values) { requestData = Buffer.concat([requestData, this.encodeValue(value, "short", true)]); } // Create 4E frame header let mcData = Buffer.alloc(0); // Subheader (big endian for 4E type - 0x5400) const subheaderBuffer = Buffer.alloc(2); subheaderBuffer.writeUInt16BE(0x5400, 0); mcData = Buffer.concat([mcData, subheaderBuffer]); // Add other header fields mcData = Buffer.concat([mcData, this.encodeValue(0x0000, "short")]); mcData = Buffer.concat([mcData, this.encodeValue(0, "short")]); mcData = Buffer.concat([mcData, this.encodeValue(0, "byte")]); mcData = Buffer.concat([mcData, this.encodeValue(0xFF, "byte")]); mcData = Buffer.concat([mcData, this.encodeValue(0x03FF, "short")]); mcData = Buffer.concat([mcData, this.encodeValue(0x00, "byte")]); // Add data length + timer size mcData = Buffer.concat([mcData, this.encodeValue(this.wordSize + requestData.length, "short")]); mcData = Buffer.concat([mcData, this.encodeValue(4, "short")]); mcData = Buffer.concat([mcData, requestData]); return mcData; } encodeValue(value, mode = "short", isSigned = false) { let buffer; switch (mode) { case "byte": buffer = Buffer.alloc(1); if (isSigned) { buffer.writeInt8(value, 0); } else { buffer.writeUInt8(value, 0); } break; case "short": buffer = Buffer.alloc(2); if (isSigned) { buffer.writeInt16LE(value, 0); } else { buffer.writeUInt16LE(value, 0); } break; case "long": buffer = Buffer.alloc(4); if (isSigned) { buffer.writeInt32LE(value, 0); } else { buffer.writeUInt32LE(value, 0); } break; default: throw new MCProtocolError(`Unknown encode mode: ${mode}`); } return buffer; } makeDeviceData(device, address) { let deviceData = Buffer.alloc(0); // Extract device type (letters) and number const deviceTypeMatch = device.match(/\D+/); if (!deviceTypeMatch) { throw new MCProtocolError(`Invalid device: ${device}`); } const deviceType = deviceTypeMatch[0]; const deviceCode = MCProtocol.DEVICE_CODES[deviceType]; if (deviceCode === undefined) { throw new MCProtocolError(`Unknown device type: ${deviceType}`); } const deviceBase = this.getDeviceBase(deviceType); const deviceNum = parseInt(address.toString(), deviceBase); if (this.options.plcType === 'iQ-R') { // iQ-R series: 4 bytes for device number + 2 bytes for device code const numBuffer = Buffer.alloc(4); numBuffer.writeUInt32LE(deviceNum, 0); deviceData = Buffer.concat([deviceData, numBuffer]); const codeBuffer = Buffer.alloc(2); codeBuffer.writeUInt16LE(deviceCode, 0); deviceData = Buffer.concat([deviceData, codeBuffer]); } else { // Q series: 3 bytes for device number + 1 byte for device code const numBuffer = Buffer.alloc(3); numBuffer.writeUIntLE(deviceNum, 0, 3); deviceData = Buffer.concat([deviceData, numBuffer]); const codeBuffer = Buffer.alloc(1); codeBuffer.writeUInt8(deviceCode, 0); deviceData = Buffer.concat([deviceData, codeBuffer]); } return deviceData; } getDeviceBase(deviceType) { const hexDevices = ['X', 'Y', 'B', 'W', 'SB', 'SW', 'DX', 'DY', 'ZR']; return hexDevices.includes(deviceType) ? 16 : 10; } async sendRequest(frame) { return new Promise((resolve, reject) => { if (!this.socket || !this.isConnected) { reject(new MCProtocolError('Not connected')); return; } const requestId = ++this.requestCounter; const timeout = setTimeout(() => { this.pendingRequests.delete(requestId); reject(new MCProtocolError('Request timeout')); }, this.options.timeout); this.pendingRequests.set(requestId, { resolve, reject, timeout }); this.socket.write(frame, (error) => { if (error) { this.pendingRequests.delete(requestId); clearTimeout(timeout); reject(new MCProtocolError(`Write error: ${error.message}`)); } }); }); } handleResponse(data) { this.responseBuffer = Buffer.concat([this.responseBuffer, data]); // For 4E frame, process complete responses while (this.responseBuffer.length >= 13) { // Minimum 4E response length with data length field // For 4E frame: subheader(2) + serial(2) + reserved(2) + network(1) + pc(1) + moduleio(2) + modulesta(1) + length(2) = 13 bytes to read length // Data length is at offset 11-12 (after moduleio and modulesta) const dataLength = this.responseBuffer.readUInt16LE(11); const totalLength = 13 + dataLength; // header + data if (this.responseBuffer.length >= totalLength) { const response = this.responseBuffer.subarray(0, totalLength); this.responseBuffer = this.responseBuffer.subarray(totalLength); this.processResponse(response); } else { break; } } } processResponse(response) { if (this.pendingRequests.size === 0) { return; } const requestEntry = this.pendingRequests.entries().next().value; if (!requestEntry) { return; } const [requestId, request] = requestEntry; this.pendingRequests.delete(requestId); clearTimeout(request.timeout); try { this.validateResponse(response); request.resolve(response); } catch (error) { request.reject(error); } } validateResponse(response) { // Debug: console.log(`Response: ${response.toString('hex')}`) // Check subheader const subheader = response.readUInt16BE(0); if (subheader !== 0xD400) { throw new MCProtocolError(`Invalid subheader: 0x${subheader.toString(16)}`); } // Get data length and check error code const dataLength = response.readUInt16LE(9); // For error checking, use JavaScript version logic: error at offset 13 if (response.length >= 15) { const errorCode = response.readUInt16LE(13); if (errorCode !== 0x0000) { throw new MCProtocolError(`PLC error: 0x${errorCode.toString(16)}`, undefined, errorCode); } } } parseReadResponse(response) { this.validateResponse(response); const values = []; // Use JavaScript version offset: data starts at 15 const dataStart = 15; for (let i = dataStart; i < response.length; i += 2) { if (i + 1 < response.length) { const value = response.readInt16LE(i); values.push(value); } } // Debug: console.log(`Parsed ${values.length} values from offset ${dataStart}`) return values; } cleanup() { this.isConnected = false; this.responseBuffer = Buffer.alloc(0); for (const [requestId, request] of this.pendingRequests) { clearTimeout(request.timeout); request.reject(new MCProtocolError('Connection closed')); } this.pendingRequests.clear(); } // Helper method for reading PLC registers (pymcprotocol style) async plcRead(registers) { const addresses = registers.map(reg => { const match = reg.match(/^([A-Z]+)(\d+)$/); if (!match) { throw new MCProtocolError(`Invalid register format: ${reg}`); } return { device: match[1], address: parseInt(match[2]) }; }); const results = await this.batchReadWordUnits(addresses); return registers.map(reg => { const value = results[reg]; if (value === undefined) { throw new MCProtocolError(`No data received for register ${reg}`); } return value; }); } } exports.MCProtocol = MCProtocol; // Device type mappings for MC Protocol MCProtocol.DEVICE_CODES = { 'D': 0xA8, 'R': 0xAF, 'ZR': 0xB0, 'M': 0x90, 'X': 0x9C, 'Y': 0x9D, 'B': 0xA0, 'F': 0x93, 'V': 0x94, 'S': 0x98, 'SS': 0xC9, 'SC': 0xC6, 'SB': 0xA1, 'DX': 0xA2, 'DY': 0xA3, 'T': 0xC2, 'ST': 0xC7, 'C': 0xC5, 'TC': 0xC0, 'TS': 0xC1, 'TN': 0xC2, 'CN': 0xC5, 'CS': 0xC4, 'CC': 0xC3, 'W': 0xB4, 'SW': 0xB5, 'RD': 0x2C, 'SD': 0xA9, 'Z': 0xCC }; //# sourceMappingURL=MCProtocol.js.map