@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
JavaScript
"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