@andrei-tatar/node-red-contrib-nibetcp
Version:
Node-red integration for Nibe S series heatpumps using modbus over TCP
155 lines (154 loc) • 6.72 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Modbus = void 0;
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const LABELS_BY_CODEID = new Map([
[3, 'READ_HOLDING'],
[4, 'READ_INPUT'],
[6, 'WRITE_SINGLE_REGISTER'],
[16, 'WRITE_MULTIPLE_REGISTERS'],
]);
const CODEID_BY_LABEL = new Map([...LABELS_BY_CODEID.entries()].map(([key, value]) => [value, key]));
class Modbus {
static get transactionId() {
Modbus._transactionId++;
if (Modbus._transactionId > 0xffff) {
Modbus._transactionId = 0;
}
return Modbus._transactionId;
}
constructor(tcp) {
this.tcp = tcp;
this.packages$ = this.tcp.data$.pipe((0, operators_1.scan)(Modbus.handleMessage, { items: [], pending: Buffer.alloc(0) }), (0, operators_1.concatMap)(v => v.items), (0, operators_1.share)({ resetOnRefCountZero: true }));
}
static handleMessage(state, msg) {
state.pending = state.pending
? Buffer.concat([state.pending, msg])
: msg;
state.items = [];
while (state.pending.length > 6) {
const pkg_len = state.pending.readUInt16BE(4) + 6;
if (state.pending.length < 8)
return state; // safety precaution
if (state.pending.length < pkg_len)
return state; // not all data yet
const data = state.pending.subarray(8, pkg_len);
const pkgFunctionCode = state.pending.readUInt8(7);
const functionCode = LABELS_BY_CODEID.get(pkgFunctionCode & 0x7F);
if (!functionCode)
return state;
const pkg = {
transactionId: state.pending.readUInt16BE(0),
protocol: state.pending.readUInt16BE(2),
length: pkg_len,
unitId: state.pending.readUInt8(6),
};
state.pending = state.pending.subarray(pkg_len);
if (pkgFunctionCode & 0x80) {
state.items.push(Object.assign(Object.assign({}, pkg), { functionCode, exception: data.readUInt8(0) }));
return state;
}
switch (functionCode) {
case 'READ_HOLDING':
case 'READ_INPUT':
const cnt = data.readUInt8(0);
const readRegsPackage = {
functionCode,
data: data.subarray(1, 1 + cnt),
};
state.items.push(Object.assign(Object.assign({}, pkg), readRegsPackage));
break;
case 'WRITE_SINGLE_REGISTER':
const writeReg = {
functionCode,
address: data.readUInt16BE(0),
value: Buffer.from([data[2], data[3]]),
};
state.items.push(Object.assign(Object.assign({}, pkg), writeReg));
break;
case 'WRITE_MULTIPLE_REGISTERS':
const from = data.readUInt16BE(0);
const writeMany = {
functionCode,
from: from,
to: data.readUInt16BE(2) + from - 1,
};
state.items.push(Object.assign(Object.assign({}, pkg), writeMany));
break;
}
}
return state;
}
readRegisters(kind, startAddress, count = 1, timeoutMsec = 1000) {
const transactionId = Modbus.transactionId;
const data = Buffer.alloc(4);
data.writeUInt16BE(startAddress, 0);
data.writeUInt16BE(count, 2);
const tx = this.sendPacket(transactionId, kind, data).pipe((0, operators_1.ignoreElements)());
const rx = this.packages$.pipe((0, operators_1.filter)(p => p.transactionId === transactionId && p.functionCode === kind), (0, operators_1.timeout)(timeoutMsec), (0, operators_1.first)(), (0, operators_1.map)(v => {
if ('exception' in v) {
throw new Error(`Error while reading register ${startAddress}: (${v.functionCode} - ${v.exception})`);
}
if (v.functionCode !== kind) {
//we already filter, this shouldn't happen
throw new Error();
}
return v;
}));
return (0, rxjs_1.merge)(rx, tx);
}
writeRegisters(startAddress, data, timeoutMsec = 1000) {
if (data.length % 2 !== 0 || data.length === 0) {
throw new Error(`Invalid data length ${data.length}`);
}
const transactionId = Modbus.transactionId;
const writeCount = data.length / 2;
let pckg;
let writeType;
if (writeCount === 1) {
writeType = 'WRITE_SINGLE_REGISTER';
pckg = Buffer.alloc(2 + data.length);
pckg.writeUInt16BE(startAddress, 0);
data.copy(pckg, 2);
}
else {
writeType = 'WRITE_MULTIPLE_REGISTERS';
pckg = Buffer.alloc(5 + data.length);
let offset = 0;
offset = pckg.writeUInt16BE(startAddress, offset);
offset = pckg.writeUInt16BE(writeCount, offset);
offset = pckg.writeUInt8(writeCount * 2, offset);
data.copy(pckg, offset);
}
const tx = this.sendPacket(transactionId, writeType, pckg).pipe((0, operators_1.ignoreElements)());
const rx = this.packages$.pipe((0, operators_1.filter)(p => p.transactionId === transactionId && p.functionCode === writeType), (0, operators_1.timeout)(timeoutMsec), (0, operators_1.first)(), (0, operators_1.map)(v => {
if ('exception' in v) {
throw new Error(`Error while reading register ${startAddress}: (${v.functionCode} - ${v.exception})`);
}
if (v.functionCode !== writeType) {
//we already filter, this shouldn't happen
throw new Error();
}
return v;
}));
return (0, rxjs_1.merge)(rx, tx);
}
sendPacket(transactionId, label, data) {
const codeId = CODEID_BY_LABEL.get(label);
if (!codeId) {
throw new Error('Invalid code id while sending packet');
}
const req = Buffer.alloc(8 + data.length);
req.writeUInt16BE(transactionId, 0);
req.writeUInt16BE(0, 2); //protocol
req.writeUInt16BE(data.length + 2, 4);
req.writeUInt8(0, 6); //unit id
req.writeUInt8(codeId, 7);
data.copy(req, 8);
return this.tcp.send(req);
}
}
exports.Modbus = Modbus;
Modbus._transactionId = 0;
;