modbus-serial
Version:
A pure JavaScript implemetation of MODBUS-RTU (Serial and TCP) for NodeJS.
403 lines (318 loc) • 11.2 kB
JavaScript
"use strict";
function getByteLength(type) {
switch (String(type).toLowerCase()) {
case "int16":
case "uint16":
return 2;
case "int32":
case "uint32":
case "float":
return 4;
default:
throw new Error("Unsupported type");
}
}
function send({ fc, unit, address, arg }) {
this._port.setID(unit);
switch (fc) {
case 1: return this._port.readCoils(address, arg);
case 2: return this._port.readDiscreteInputs(address, arg);
case 3: return this._port.readHoldingRegisters(address, arg);
case 4: return this._port.readInputRegisters(address, arg);
case 5: return this._port.writeCoil(address, arg);
case 6: return this._port.writeRegister(address, arg);
case 15: return this._port.writeCoils(address, arg);
case 16: return this._port.writeRegisters(address, arg);
}
return Promise.reject(new Error("Unknown fc code"));
}
const Worker = function(port, options) {
if (typeof(options) === "undefined") options = {};
this.maxConcurrentRequests = 1;
this.debug = false;
this._port = port;
this._queue = [];
this._scheduled = [];
this._running = new Map();
this._nextId = 0;
this.setOptions(options);
};
Worker.prototype.setOptions = function({ maxConcurrentRequests, debug }) {
if(maxConcurrentRequests > 0) {
this.maxConcurrentRequests = maxConcurrentRequests;
}
if(debug !== undefined) {
this.debug = Boolean(debug);
}
};
Worker.prototype.log = function(...args) {
if(this.debug === true) {
args.unshift(new Date());
console.log(...args);
}
};
Worker.prototype.emit = function(name, data) {
this._port.emit(name, data);
};
Worker.prototype.bufferize = function(data, type) {
if(Array.isArray(data) === false) {
data = [data];
}
const quantity = data.length;
const byteLength = getByteLength(type);
const size = quantity * byteLength;
const buffer = Buffer.alloc(size);
for(let i = 0; i < quantity; i++) {
if(type === "int16") {
buffer.writeInt16BE(data[i], i * byteLength);
} else if(type === "uint16") {
buffer.writeUInt16BE(data[i], i * byteLength);
} else if(type === "int32") {
buffer.writeInt32BE(data[i], i * byteLength);
} else if(type === "uint32") {
buffer.writeUInt32BE(data[i], i * byteLength);
} else if(type === "float") {
buffer.writeFloatBE(data[i], i * byteLength);
}
}
return buffer;
};
Worker.prototype.unbufferize = function(buffer, type) {
const byteLength = getByteLength(type);
const quantity = buffer.length / byteLength;
const data = [];
for(let i = 0; i < quantity; i++) {
if(type === "int16") {
data.push(buffer.readInt16BE(i * byteLength));
} else if(type === "uint16") {
data.push(buffer.readUInt16BE(i * byteLength));
} else if(type === "int32") {
data.push(buffer.readInt32BE(i * byteLength));
} else if(type === "uint32") {
data.push(buffer.readUInt32BE(i * byteLength));
} else if(type === "float") {
data.push(buffer.readFloatBE(i * byteLength));
}
}
return data;
};
Worker.prototype.nextId = function() {
this._nextId = this._nextId + 1;
if(this._nextId > 9999) {
this._nextId = 1;
}
return this._nextId;
};
Worker.prototype.send = function({ fc, unit, address, value, quantity, arg, type }) {
const promise = new Promise((resolve, reject) => {
arg = arg || quantity || value;
if(fc === 1 || fc === 2) {
arg = arg || 1;
}
if(fc === 3 || fc === 4) {
type = type || "int16";
arg = (arg || 1) * getByteLength(type) / 2;
}
if(fc === 6 || fc === 16) {
type = type || "int16";
arg = this.bufferize(arg, type);
if(fc === 6 && arg.length > 2) {
fc = 16;
}
}
if(fc === 5 && arg instanceof Array && arg.length > 1) {
fc = 15;
}
const id = this.nextId();
this.log("queue push", `#${id}`, fc, unit, address, arg, type);
this._queue.push({ id, fc, unit, address, arg, type, resolve, reject });
});
this.process();
return promise;
};
Worker.prototype.process = function() {
if(this._port.isOpen === false) {
this._queue = [];
this._scheduled = [];
this._running = new Map();
this._nextId = 0;
return;
}
setTimeout(() => this.run(), 1);
};
Worker.prototype.run = function() {
if(this._running.size >= this.maxConcurrentRequests) {
return;
}
let request = this._queue.shift();
if(!request) {
request = this._scheduled.shift();
}
if(!request) {
return; // Well Done
}
if(typeof request.checkBeforeQueuing === "function") {
if(request.checkBeforeQueuing() === false) {
return this.process(); // Skip current request and go on
}
}
this._running.set(request.id, request);
this.log("send", JSON.stringify(request));
this.emit("request", { request });
send.apply(this, [request])
.then((response) => {
let data = [];
if(request.fc === 1 || request.fc === 2) {
for(let i = 0; i < request.arg; i++) {
data.push(Boolean(response.data[i]));
}
} else if(request.fc === 3 || request.fc === 4) {
data = this.unbufferize(response.buffer, request.type);
} else if(request.arg instanceof Array) {
data = request.arg;
} else if(request.arg instanceof Buffer && request.type) {
data = this.unbufferize(request.arg, request.type);
} else {
data.push(request.arg);
}
this._running.delete(request.id);
this.emit("response", { request, response: data });
request.resolve(data);
this.process();
})
.catch((error) => {
this._running.delete(request.id);
error.request = request;
this.emit("failed", error);
request.reject(error);
this.process();
});
this.process();
};
Worker.prototype._poll_send = function(result, { i, fc, unit, address, arg, items, length, type }, { skipErrors }) {
const promise = new Promise((res, rej) => {
const id = this.nextId();
this.log("scheduled push", "poll #" + result.id, "req #" + i, "#" + id, fc, length, type);
const resolve = function(response) {
const data = items.map((address, index) => ({ address, value: response[index] }));
result._req += 1;
result.done += 1;
result.data = [...result.data, ...data];
res(data);
};
const reject = function(error) {
result._req += 1;
result.error = error;
rej(error);
};
const checkBeforeQueuing = function() {
return result.error === null || skipErrors === true;
};
this._scheduled.push({ id, i, fc, unit, address, arg, items, length, type, result, checkBeforeQueuing, resolve, reject });
});
this.process();
return promise;
};
Worker.prototype.poll = function({ unit, map, onProgress, maxChunkSize, skipErrors, defaultType }) {
maxChunkSize = maxChunkSize || 32;
skipErrors = Boolean(skipErrors);
defaultType = defaultType || "int16";
if(unit < 1 || unit > 250 || isNaN(unit) || unit === undefined) {
throw new Error("invalid unit");
}
this.log("poll", `unit=${unit}`, "map size=" + Object.keys(map).length, `maxChunkSize=${maxChunkSize}`, `skipErrors=${skipErrors}`);
const result = {
id: this.nextId(),
unit,
total: 0,
done: 0,
data: [],
error: null,
dt: Date.now(),
_req: 0
};
const registers = [];
map.forEach(({ fc, address, type }) => {
fc = parseInt(fc);
if(fc === 3 || fc === 4) {
type = type || defaultType;
} else if(fc === 1 || fc === 2) {
type = "bool";
} else {
throw new Error("unsupported fc");
}
if(address instanceof Array) {
address.forEach((item) => {
registers.push({ fc, address: parseInt(item), type: type || null });
});
} else {
address = parseInt(address);
registers.push({ fc, address, type: type || null });
}
});
registers.sort((a, b) => {
if(a.fc === b.fc) {
return a.address - b.address;
}
return a.fc - b.fc;
});
const requests = registers.reduce(function(chunks, register, i, arr) {
let chunk = 0;
if(chunks.length) {
chunk = chunks.length - 1;
}
if(i > 0) {
const lastRegister = arr[i - 1];
if(lastRegister.fc !== register.fc) {
chunk += 1;
} else if(lastRegister.type !== register.type) {
chunk += 1;
} else if([3, 4].indexOf(register.fc) >= 0 && register.address - lastRegister.address !== getByteLength(register.type) / 2) {
chunk += 1;
} else if(chunks[chunk].items.length >= maxChunkSize) {
chunk += 1;
}
}
if(chunks[chunk] === undefined) {
chunks[chunk] = {
fc: register.fc,
items: [],
length: 0,
type: register.type
};
}
chunks[chunk].items.push(register.address);
if ([3, 4].indexOf(register.fc) >= 0) {
chunks[chunk].length += getByteLength(register.type) / 2;
} else {
chunks[chunk].length += 1;
}
return chunks;
}, []);
result.total = requests.length;
return new Promise(((resolve) => {
const check = function() {
if(result._req === result.total) {
result.dt = Date.now() - result.dt;
resolve(result);
} else if(result.error && skipErrors !== true) {
result.dt = Date.now() - result.dt;
resolve(result);
}
};
for(let i = 0; i < requests.length; i++) {
const { fc, items, length, type } = requests[i];
this._poll_send(result, { i, unit, fc, address: parseInt(items[0]), items, arg: length, length, type }, {
skipErrors
})
.then((data) => {
if(typeof onProgress === "function") {
onProgress(result.done / result.total, data);
}
check();
})
.catch(() => check());
}
}));
};
module.exports = Worker;