You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

612 lines
23 KiB

'use strict';
const common = require(__dirname + '/common.js');
const Modbus = require('./jsmodbus');
function Master(options, adapter) {
let modbusClient;
let connected;
let connectTimer;
let nextPoll;
let pollTime;
let errorCount = 0;
let ackObjects = {};
let objects = options.objects;
let isStop = false;
let pulseList = {};
let sendBuffer = {};
let devices = options.devices;
let deviceIds = Object.keys(devices);
let reconnectTimeout = null;
function reconnect(isImmediately) {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
if (nextPoll) {
clearTimeout(nextPoll);
nextPoll = null;
}
try {
if (modbusClient) {
modbusClient.close();
}
} catch (e) {
adapter.log.error('Cannot close master: ' + e);
}
if (connected) {
if (options.config.tcp) {
adapter.log.info('Disconnected from slave ' + options.config.tcp.bind);
} else {
adapter.log.info('Disconnected from slave');
}
connected = false;
adapter.setState('info.connection', false, true);
}
if (!connectTimer) {
connectTimer = setTimeout(function () {
connectTimer = null;
modbusClient.connect();
}, isImmediately ? 1000 : options.config.recon);
}
}
function pollBinariesBlock(device, regType, func, block, callback) {
let regs = device[regType];
if (block >= regs.blocks.length) {
return callback();
}
const regBlock = regs.blocks[block];
if (regBlock.startIndex === regBlock.endIndex) {
regBlock.endIndex++;
}
adapter.log.debug(`Poll ${regType} DevID(${regs.deviceId}) address ${regBlock.start} - ${regBlock.count} bits`);
if (modbusClient) {
modbusClient[func](regs.deviceId, regBlock.start, regBlock.count).then(response => {
if (response.data && response.data.length) {
for (let n = regBlock.startIndex; n < regBlock.endIndex; n++) {
let id = regs.config[n].id;
let val = response.data[regs.config[n].address - regBlock.start];
if (options.config.alwaysUpdate || ackObjects[id] === undefined || ackObjects[id].val !== val) {
ackObjects[id] = {val: val};
adapter.setState(id, !!val, true, err => {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
}
}
} else {
adapter.log.warn(`Null buffer length ${func} for ${regType} ${regBlock.start}`);
}
setImmediate(() => pollBinariesBlock(device, regType, func, block + 1, callback));
}).catch(err => {
callback(err);
});
} else {
adapter.log.debug(`Poll canceled, because no connection`);
callback('No connection');
}
}
function pollBinariesBlocks(device, regType, func, callback) {
let regs = device[regType];
if (regs.length) {
pollBinariesBlock(device, regType, func, 0, err => {
callback(err);
});
} else {
callback();
}
}
function pollFloatBlock(device, regType, func, block, callback) {
let regs = device[regType];
if (block >= regs.blocks.length) {
return callback();
}
const regBlock = regs.blocks[block];
if (regBlock.startIndex === regBlock.endIndex) {
regBlock.endIndex++;
}
adapter.log.debug(`Poll ${regType} DevID(${regs.deviceId}) address ${regBlock.start} - ${regBlock.count} bytes`);
if (modbusClient) {
modbusClient[func](regs.deviceId, regBlock.start, regBlock.count).then(response => {
if (response.payload && response.payload.length) {
for (let n = regBlock.startIndex; n < regBlock.endIndex; n++) {
let id = regs.config[n].id;
let val = common.extractValue(regs.config[n].type, regs.config[n].len, response.payload, regs.config[n].address - regBlock.start);
if (regs.config[n].type !== 'string') {
val = val * regs.config[n].factor + regs.config[n].offset;
val = Math.round(val * options.config.round) / options.config.round;
}
if (options.config.alwaysUpdate || ackObjects[id] === undefined || ackObjects[id].val !== val) {
ackObjects[id] = {val: val};
adapter.setState(id, val, true, err => {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error(`Can not set state ${id}: ${err}`);
});
}
}
} else {
adapter.log.warn(`Null buffer length ${func} for ${regType} ${regBlock.start}`);
}
// special case
if (options.config.maxBlock < 2 && regs.config[regBlock.startIndex].cw) {
// write immediately the current value
writeFloatsReg(device, regType, objects[regs.config[regBlock.startIndex].fullId], () => {
pollFloatBlock(device, regType, func, block + 1, callback);
});
} else {
setImmediate(() => pollFloatBlock(device, regType, func, block + 1, callback));
}
}).catch(err => {
callback(err);
});
} else {
adapter.log.debug(`Poll canceled, because no connection`);
callback('No connection');
}
}
function pollFloatsBlocks(device, regType, func, callback) {
let regs = device[regType];
if (regs.length) {
pollFloatBlock(device, regType, func, 0, err => {
if (!err && regs.cyclicWrite && regs.cyclicWrite.length && options.config.maxBlock >= 2) {
writeFloatsRegs(device, regType, 0, callback);
} else {
callback(err);
}
});
} else {
callback();
}
}
function writeFloatsReg(device, regType, obj, callback) {
let regs = device[regType];
if (obj.native.len > 1) {
let buffer = new Buffer(obj.native.len * 2);
for (let b = 0; b < buffer.length; b++) {
buffer[b] = regs.config[(obj.native.address - regs.addressLow) * 2 + b];
}
modbusClient.writeMultipleRegisters(regs.deviceId, obj.native.address, buffer)
.then(response => callback())
.catch(err => {
adapter.log.error('Cannot write: ' + JSON.stringify(err));
callback(err);
});
} else {
callback();
}
}
function writeFloatsRegs(device, regType, i, callback) {
let regs = device[regType];
if (i >= regs.cyclicWrite.length) {
return callback();
}
let id = regs.cyclicWrite[i];
writeFloatsReg(device, regType, objects[id], () => {
writeFloatsRegs(device, regType, i + 1, callback);
});
}
function pollResult(startTime, err, cb) {
if (err) {
errorCount++;
adapter.log.warn(`Poll error count: ${errorCount} code: ${JSON.stringify(err)}`);
adapter.setState('info.connection', false, true);
if (errorCount > 12 * deviceIds.length) { // 2 reconnects did not help, restart adapter
throw new Error('Reconnect did not help, restart adapter');
} else if (errorCount < 6 * deviceIds.length && connected) {
cb && cb();
} else {
cb && cb('disconnect');
}
} else {
let currentPollTime = (new Date()).valueOf() - startTime;
if (pollTime !== null && pollTime !== undefined) {
if (Math.abs(pollTime - currentPollTime) > 100) {
pollTime = currentPollTime;
adapter.setState('info.pollTime', currentPollTime, true);
}
} else {
pollTime = currentPollTime;
adapter.setState('info.pollTime', currentPollTime, true);
}
if (errorCount > 0) {
adapter.setState('info.connection', true, true);
errorCount = 0;
}
cb && cb();
}
}
function pollDevice(device, callback) {
adapter.log.debug(`Poll device ${device.coils.deviceId}`);
let startTime = new Date().valueOf();
let requestTimer = setTimeout(() => {
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, 'App Timeout', callback);
}
}, options.config.timeout + 200);
// TODO: use promises here
pollBinariesBlocks(device, 'disInputs', 'readDiscreteInputs', err => {
if (err) {
if (requestTimer) {
clearTimeout(requestTimer);
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, err, callback);
}
}
} else {
pollBinariesBlocks(device, 'coils', 'readCoils', err => {
if (err) {
if (requestTimer) {
clearTimeout(requestTimer);
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, err, callback);
}
}
} else {
pollFloatsBlocks(device, 'inputRegs', 'readInputRegisters', err => {
if (err) {
if (requestTimer) {
clearTimeout(requestTimer);
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, err, callback);
}
}
} else {
pollFloatsBlocks(device, 'holdingRegs', 'readHoldingRegisters', err => {
if (requestTimer) {
clearTimeout(requestTimer);
requestTimer = null;
if (connected && !isStop) {
pollResult(startTime, err, callback);
}
}
});
}
});
}
});
}
});
}
function poll(i, cb) {
if (typeof i === 'function') {
cb = i;
i = 0;
}
i = i || 0;
if (i >= deviceIds.length) {
if (deviceIds.find(id => !devices[id].err)) {
nextPoll = setTimeout(function () {
nextPoll = null;
poll();
}, options.config.poll);
} else {
!reconnectTimeout && reconnect();
}
cb && cb();
} else {
pollDevice(devices[deviceIds[i]], err => {
devices[deviceIds[i]].err = err;
setImmediate(poll, i + 1, cb);
});
}
}
function send() {
if (!modbusClient) {
adapter.log.error('Client not connected');
return;
}
let id = Object.keys(sendBuffer)[0];
let type = objects[id].native.regType;
let val = sendBuffer[id];
if (type === 'coils') {
if (val === 'true' || val === true) val = 1;
if (val === 'false' || val === false) val = 0;
val = parseFloat(val);
modbusClient.writeSingleCoil(objects[id].native.deviceId, objects[id].native.address, !!val).then(response => {
adapter.log.debug('Write successfully [' + objects[id].native.address + ']: ' + val);
}).catch(err => {
adapter.log.error('Cannot write [' + objects[id].native.address + ']: ' + JSON.stringify(err));
// still keep on communication
if (!isStop) {
!reconnectTimeout && reconnect(true);
}
});
} else if (type === 'holdingRegs') {
if (objects[id].native.float === undefined) {
objects[id].native.float =
objects[id].native.type === 'floatle' || objects[id].native.type === 'floatbe' || objects[id].native.type === 'floatsw' ||
objects[id].native.type === 'doublele' || objects[id].native.type === 'doublebe' || objects[id].native.type === 'floatsb';
}
if (objects[id].native.type !== 'string') {
val = parseFloat(val);
val = (val - objects[id].native.offset) / objects[id].native.factor;
if (!objects[id].native.float) val = Math.round(val);
}
if (objects[id].native.len > 1) {
let hrBuffer = common.writeValue(objects[id].native.type, val, objects[id].native.len);
modbusClient.writeMultipleRegisters(objects[id].native.deviceId, objects[id].native.address, hrBuffer, function (err, response) {
adapter.log.debug('Write successfully [' + objects[id].native.address + ']: ' + val);
}).catch(err => {
adapter.log.error('Cannot write [' + objects[id].native.address + ']: ' + JSON.stringify(err));
// still keep on communication
if (!isStop) {
!reconnectTimeout && reconnect(true);
}
});
} else {
if (!modbusClient) {
adapter.log.error('Client not connected');
return;
}
let buffer = common.writeValue(objects[id].native.type, val, objects[id].native.len);
modbusClient.writeSingleRegister(objects[id].native.deviceId, objects[id].native.address, buffer).then(function (response) {
adapter.log.debug('Write successfully [' + objects[id].native.address + ': ' + val);
}).catch(err => {
adapter.log.error('Cannot write [' + objects[id].native.address + ']: ' + JSON.stringify(err));
// still keep on communication
if (!isStop) {
!reconnectTimeout && reconnect(true);
}
});
}
}
delete(sendBuffer[id]);
if (Object.keys(sendBuffer).length) {
setTimeout(send, 0);
}
}
function writeHelper(id, state) {
sendBuffer[id] = state.val;
if (Object.keys(sendBuffer).length === 1) {
send();
}
}
this.write = (id, state) => {
if (!objects[id] || !objects[id].native) {
adapter.log.error('Can not set state ' + id + ': unknown object');
return;
}
if (objects[id].native.regType === 'coils' || objects[id].native.regType === 'holdingRegs') {
if (!objects[id].native.wp) {
writeHelper(id, state);
setTimeout(function () {
let _id = id.substring(adapter.namespace.length + 1);
adapter.setState(id, ackObjects[_id] ? ackObjects[_id].val : null, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
}, options.config.poll * 1.5);
} else {
if (pulseList[id] === undefined) {
let _id = id.substring(adapter.namespace.length + 1);
pulseList[id] = ackObjects[_id] ? ackObjects[_id].val : !state.val;
setTimeout(function () {
writeHelper(id, {val: pulseList[id]});
setTimeout(function () {
if (ackObjects[_id]) {
adapter.setState(id, ackObjects[_id].val, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
}
delete pulseList[id];
}, options.config.poll * 1.5);
}, options.config.pulsetime);
writeHelper(id, state);
}
}
} else {
setImmediate(() => {
let _id = id.substring(adapter.namespace.length + 1);
adapter.setState(id, ackObjects[_id] ? ackObjects[_id].val : null, true, function (err) {
// analyse if the state could be set (because of permissions)
if (err) adapter.log.error('Can not set state ' + id + ': ' + err);
});
});
}
};
this.start = () => {
if (modbusClient && typeof modbusClient.connect === 'function') {
modbusClient.connect();
}
};
this.close = () => {
isStop = true;
if (nextPoll) {
clearTimeout(nextPoll);
nextPoll = null;
}
if (modbusClient) {
try {
modbusClient.close();
} catch (e) {
}
modbusClient = null;
}
};
(function _constructor () {
adapter.setState('info.connection', false, true);
if (options.config.type === 'tcp') {
try {
modbusClient = Modbus('client', 'tcp')({
options: {
tcp: {
host: tcp.bind || "127.0.0.1",
port: parseInt(tcp.port, 10) || 502,
autoReconnect: false,
},
log: adapter.log,
timeout: options.config.timeout,
unitId: options.config.defaultDeviceId
}
});
} catch (e) {
adapter.log.error('Cannot connect to "' + tcp.bind + ':' + (parseInt(tcp.port, 10) || 502) + '": ' + e);
}
} else if (options.config.type === 'tcprtu') {
const tcp = options.config.tcp;
if (!tcp || !tcp.bind || tcp.bind === '0.0.0.0') {
adapter.log.error('IP address is not defined');
return;
}
try {
modbusClient = Modbus('client', 'tcp-rtu')({
options: {
tcp: {
host: tcp.bind,
port: parseInt(tcp.port, 10) || 502,
autoReconnect: false,
},
log: adapter.log,
timeout: options.config.timeout,
unitId: options.config.defaultDeviceId
}
});
} catch (e) {
adapter.log.error('Cannot connect to "' + tcp.bind + ':' + (parseInt(tcp.port, 10) || 502) + '": ' + e);
}
} else if (options.config.type === 'serial') {
const serial = options.config.serial;
if (!serial || !serial.comName) {
adapter.log.error('Serial devicename is not defined');
return;
}
try {
modbusClient = Modbus('client', 'serial')({
options: {
serial: {
portName: serial.comName,
baudRate: parseInt(serial.baudRate, 10) || 9600,
dataBits: parseInt(serial.dataBits, 10) || 8,
stopBits: parseInt(serial.stopBits, 10) || 1,
parity: serial.parity || 'none',
},
log: adapter.log,
timeout: options.config.timeout,
unitId: options.config.defaultDeviceId
}
});
} catch (e) {
adapter.log.error('Cannot open port "' + serial.comName + '" [' + (parseInt(serial.baudRate, 10) || 9600) + ']: ' + e);
}
} else {
adapter.log.error(`Unsupported type ${options.config.type}"`);
return;
}
if (!modbusClient) {
adapter.log.error('Cannot create modbus master!');
return;
}
modbusClient.on('connect', function () {
if (!connected) {
if (options.config.type === 'tcp') {
adapter.log.info('Connected to slave ' + options.config.tcp.bind);
} else {
adapter.log.info('Connected to slave');
}
connected = true;
adapter.setState('info.connection', true, true);
}
if (nextPoll) {
clearTimeout(nextPoll);
nextPoll = null;
}
poll();
}).on('disconnect', () => {
if (isStop) return;
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(reconnect, 1000);
}
});
modbusClient.on('close', () => {
if (isStop) return;
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(reconnect, 1000);
}
});
modbusClient.on('error', err => {
if (isStop) return;
adapter.log.warn('On error: ' + JSON.stringify(err));
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(reconnect, 1000);
}
});
modbusClient.on('trashCurrentRequest', err => {
if (isStop) return;
adapter.log.warn('Error: ' + JSON.stringify(err));
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(reconnect, 1000);
}
});
})();
return this;
}
module.exports = Master;