node-modbustcp
Version:
A ModbusTcp driver based on modbus-serial
721 lines (684 loc) • 30.5 kB
text/typescript
/**
* 引用 modbus-serial 模块
* @link https://github.com/yaacov/node-modbus-serial
*/
import ModbusSerial from 'modbus-serial';
import { ModbusTCPClientInterface, Register, RegisterAll, Package, PackageItem, ResultInterface } from './interface';
import { EnumRegisterType, EnumDataType, RegisterName } from './enum';
import { ReadCoilResult, ReadRegisterResult } from 'modbus-serial/ModbusRTU';
export default class ModbusTCPClient {
/** 创建一个新的 client */
private client: ModbusSerial = new ModbusSerial();
/** 主机地址 */
host: string;
/** 端口号 */
port: number;
/** 设备ID */
deviceID: number;
/** 连接超时时间,单位:毫秒 */
timeout: number = 3000;
/** length of one package */
packageLength: number = 100;
/** 最大重连时长,单位:小时,0为无限时长 */
maxReconnectTime: number = 0;
/** 尝试重连时间,单位:秒 */
tryReconnectTime: number = 3;
/** 所有寄存器 */
registerMap: Map<string, RegisterAll> = new Map();
/** 寄存器包 */
private registerPackage: Package = { Coil: [], DisInput: [], Input: [], Holding: [] };
/** 轮询间隔时间,单位:毫秒 */
pollingInterval: number = 1000;
/** 初次连接失败时间戳 */
private firstConnFailTime: number = 0;
constructor(option: ModbusTCPClientInterface) {
this.host = option.host;
this.port = option.port;
this.deviceID = option.deviceID;
this.timeout = option.timeout === undefined ? this.timeout : option.timeout;
this.packageLength = option.packageLength === undefined ? this.packageLength : option.packageLength;
this.maxReconnectTime = option.maxReconnectTime === undefined ? this.maxReconnectTime : option.maxReconnectTime;
this.tryReconnectTime = option.tryReconnectTime === undefined ? this.tryReconnectTime : option.tryReconnectTime;
}
/**
* 连接设备
*/
private async connect() {
return await this.client.connectTCP(this.host, { port: this.port })
.then(() => {
this.client.setID(this.deviceID);
this.client.setTimeout(this.timeout);
console.log(`connect ${this.host}:${this.port} ${this.deviceID} success ${new Date()}`);
return this.client;
})
.catch(e => {
return e.message
});
}
/**
* 断开连接
*/
disconnect() {
this.client.close((closeRes: any) => { return closeRes });
}
/**
* 将设置的寄存器数组进行解析整合并排序,然后存入 Map 中
* @param registerArr 寄存器数组
*/
private sortRegisters(registerArr: Register[]) {
let sortedRegisterArr = [];
for (let i = 0; i < registerArr.length; i++) {
let tempRegister: RegisterAll = {
register: registerArr[i].register,
registerType: EnumRegisterType.Coil,
startAddr: 0,
endAddr: 0,
dataType: EnumDataType.Bit,
packageIndex: 0,
bufferAddr: [],
value: null,
timestamp: new Date(),
quality: ''
};
// 寄存器类型
let registerType = registerArr[i].register.substr(0, 1);
tempRegister.registerType = RegisterName[registerType];
// 起始地址
tempRegister.startAddr = +registerArr[i].register.substr(1);
// 结束地址
switch (registerArr[i].dataType) {
case EnumDataType.Bit:
case EnumDataType.Binary:
case EnumDataType.Decimal:
case EnumDataType.Int:
case EnumDataType.Hex:
tempRegister.endAddr = tempRegister.startAddr;
break;
case EnumDataType.Float:
case EnumDataType.SwappedFloat:
case EnumDataType.Long:
case EnumDataType.SwappedLong:
tempRegister.endAddr = tempRegister.startAddr + 1;
break;
case EnumDataType.Double:
case EnumDataType.SwappedDouble:
tempRegister.endAddr = tempRegister.startAddr + 3;
break;
default:
tempRegister.endAddr = tempRegister.startAddr;
}
// 数据类型
tempRegister.dataType = registerArr[i].dataType;
sortedRegisterArr.push(tempRegister);
}
// 按照寄存器的其实地址从小到大排序
sortedRegisterArr = sortedRegisterArr.sort(sortRegister);
for (let i = 0; i < sortedRegisterArr.length; i++) {
this.registerMap.set(sortedRegisterArr[i].register, sortedRegisterArr[i]);
}
function sortRegister(r1: RegisterAll, r2: RegisterAll) {
return r1.startAddr - r2.startAddr;
}
}
/**
* 根据排序后的寄存器数组,创建打包信息 this.registerPackage 和一部分寄存器信息,准备读取
*/
private createPack() {
for (let register of this.registerMap.values()) {
let registerType = register.registerType;
let packageItemArr: PackageItem[];
let packageLength: number;
switch (registerType) {
case EnumRegisterType.Coil:
packageItemArr = this.registerPackage.Coil;
packageLength = this.packageLength * 16;
break;
case EnumRegisterType.DiscreteInput:
packageItemArr = this.registerPackage.DisInput;
packageLength = this.packageLength * 16;
break;
case EnumRegisterType.Input:
packageItemArr = this.registerPackage.Input;
packageLength = this.packageLength;
break;
case EnumRegisterType.Holding:
packageItemArr = this.registerPackage.Holding;
packageLength = this.packageLength;
break;
default:
packageItemArr = this.registerPackage.Coil;
packageLength = this.packageLength * 16;
}
// 当前寄存器开始地址
let startAddr = register.startAddr;
// 当前寄存器结束地址
let endAddr = register.endAddr;
if (packageItemArr.length === 0) {
// 包中还没项
let packageItem: PackageItem = {
startAddr: startAddr,
endAddr: endAddr,
length: endAddr - startAddr + 1,
datas: null,
timestamp: new Date(),
quality: ''
}
packageItemArr.push(packageItem);
} else {
// 已经有项了
// 获取最后一个包的索引号
let packageIndex = packageItemArr.length - 1;
if (endAddr - packageItemArr[packageIndex].startAddr >= packageLength) {
// 如果当前寄存器的结束地址 - 包的开始地址 >= 包的长度
// 直接新开一个包
let packageItem: PackageItem = {
startAddr: startAddr,
endAddr: endAddr,
length: endAddr - startAddr + 1,
datas: null,
timestamp: new Date(),
quality: ''
}
packageItemArr.push(packageItem);
} else {
// 否则
if (endAddr > packageItemArr[packageIndex].endAddr) {
// 如果当前寄存器的结束地址 > 当前包的结束地址
// 把当前寄存器的结束地址作为当前包的结束地址
packageItemArr[packageIndex].endAddr = endAddr;
packageItemArr[packageIndex].length = endAddr - packageItemArr[packageIndex].startAddr + 1;
}
}
}
// 包索引号生成
register.packageIndex = packageItemArr.length - 1;
// 数据长度,单位:字 Word
let dataLength = 1;
switch (register.dataType) {
case EnumDataType.Bit:
case EnumDataType.Binary:
case EnumDataType.Decimal:
case EnumDataType.Hex:
dataLength = 0;
break;
case EnumDataType.Int:
dataLength = 1;
break;
case EnumDataType.Float:
case EnumDataType.SwappedFloat:
case EnumDataType.Long:
case EnumDataType.SwappedLong:
dataLength = 2;
break;
case EnumDataType.Double:
case EnumDataType.SwappedDouble:
dataLength = 4;
break;
default:
dataLength = 0;
}
// buffer 地址是按字节 Byte 算的
// buffer 起始地址、结束地址生成
let bufferStartAddr: number, bufferEndAddr: number;
if (dataLength !== 0) {
bufferStartAddr = (startAddr - packageItemArr[register.packageIndex].startAddr) * 2;
bufferEndAddr = bufferStartAddr + dataLength * 2 - 1;
} else {
bufferStartAddr = startAddr - packageItemArr[register.packageIndex].startAddr;
bufferEndAddr = bufferStartAddr;
}
// bufferAddr 生成
register.bufferAddr.push(bufferStartAddr, bufferEndAddr);
}
}
/**
* 设置准备要进行数据读取的寄存器
* @param registerArr 准备读取的寄存器数据
*/
setRegisters(registerArr: Register[]) {
this.sortRegisters(registerArr);
this.createPack();
return this;
}
/**
* 对数据包进行读取
*/
private async readData() {
let promiseArr: Promise<ReadCoilResult | ReadRegisterResult | void>[] = [];
let coilPakArr = this.registerPackage.Coil;
let disInputPakArr = this.registerPackage.DisInput;
let inputPakArr = this.registerPackage.Input;
let holdingPakArr = this.registerPackage.Holding;
// 读取线圈包
for (let i = 0; i < coilPakArr.length; i++) {
// 数据包起始地址
let startAddr = coilPakArr[i].startAddr - 1;
// 数据包长度
let length = coilPakArr[i].length;
let readPromise: Promise<ReadCoilResult | void> = this.client.readCoils(startAddr, length)
.then(datas => {
this.registerPackage.Coil[i].datas = datas;
coilPakArr[i].timestamp = new Date();
coilPakArr[i].quality = 'GOOD';
return datas;
})
.catch(e => {
coilPakArr[i].timestamp = new Date();
coilPakArr[i].quality = e.message;
});
promiseArr.push(readPromise);
}
// 读取离散输入包
for (let i = 0; i < disInputPakArr.length; i++) {
// 数据包起始地址
let startAddr = disInputPakArr[i].startAddr - 1;
// 数据包长度
let length = disInputPakArr[i].length;
let readPromise: Promise<ReadCoilResult | void> = this.client.readDiscreteInputs(startAddr, length)
.then(datas => {
this.registerPackage.DisInput[i].datas = datas;
disInputPakArr[i].timestamp = new Date();
coilPakArr[i].quality = 'GOOD';
})
.catch(e => {
disInputPakArr[i].timestamp = new Date();
disInputPakArr[i].quality = e.message;
});
promiseArr.push(readPromise);
}
// 读取输入包
for (let i = 0; i < inputPakArr.length; i++) {
// 数据包起始地址
let startAddr = inputPakArr[i].startAddr - 1;
// 数据包长度
let length = inputPakArr[i].length;
let readPromise: Promise<ReadRegisterResult | void> = this.client.readInputRegisters(startAddr, length)
.then(datas => {
this.registerPackage.Input[i].datas = datas;
inputPakArr[i].timestamp = new Date();
inputPakArr[i].quality = 'GOOD';
})
.catch(e => {
inputPakArr[i].timestamp = new Date();
inputPakArr[i].quality = e.message;
});
promiseArr.push(readPromise);
}
// 读取保持包
for (let i = 0; i < holdingPakArr.length; i++) {
// 数据包起始地址
let startAddr = holdingPakArr[i].startAddr - 1;
// 数据包长度
let length = holdingPakArr[i].length;
let readPromise: Promise<ReadRegisterResult | void> = this.client.readHoldingRegisters(startAddr, length)
.then(datas => {
this.registerPackage.Holding[i].datas = datas;
holdingPakArr[i].timestamp = new Date();
holdingPakArr[i].quality = 'GOOD';
})
.catch(e => {
holdingPakArr[i].timestamp = new Date();
holdingPakArr[i].quality = e.message;
});
promiseArr.push(readPromise);
}
// 当所有数据读取完毕之后对数据进行派发,派发到结果集中
await Promise.all(promiseArr).then(() => { this.distribute() });
}
/**
* 分派数据值到 registerMap 的 value、timestamp、quality 中,并返回数据
*/
private distribute() {
for (let register of this.registerMap.values()) {
// 数据类型
let dataType = register.dataType;
// buffer 地址数组
let bufferAddr = register.bufferAddr;
// 包索引号
let packageIndex = register.packageIndex;
// 寄存器类型
let registerType = '';
switch (register.registerType) {
case EnumRegisterType.Coil:
registerType = "Coil";
break;
case EnumRegisterType.DiscreteInput:
registerType = "DisInput";
break;
case EnumRegisterType.Input:
registerType = "Input";
break;
case EnumRegisterType.Holding:
registerType = "Holding";
break;
default:
registerType = "Coil";
}
register.timestamp = this.registerPackage[registerType][packageIndex].timestamp;
register.quality = this.registerPackage[registerType][packageIndex].quality;
// 包读取完成之后生成的 buffer 数据
let bufferDatas = this.registerPackage[registerType][packageIndex].datas;
// 一些中间变量
let bufferArr: number[], sortedBufferArr: number[], newBuffer: Buffer;
// 根据数据类型生成最终要返回的数据
if (bufferDatas) {
switch (dataType) {
case EnumDataType.Bit:
register.value = bufferDatas.data[bufferAddr[0]];
break;
case EnumDataType.Binary:
register.value = bufferDatas.data[bufferAddr[0]].toString(2);
break;
case EnumDataType.Decimal:
register.value = bufferDatas.data[bufferAddr[0]];
break;
case EnumDataType.Int:
register.value = bufferDatas.buffer.readInt16BE(bufferAddr[0]);
break;
case EnumDataType.Hex:
register.value = bufferDatas.data[bufferAddr[0]].toString(16);
break;
case EnumDataType.Float:
bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
// 改变顺序 0123 => 2301
sortedBufferArr = [bufferArr[2], bufferArr[3], bufferArr[0], bufferArr[1]];
newBuffer = Buffer.from(sortedBufferArr);
register.value = newBuffer.readFloatBE(0);
break;
case EnumDataType.SwappedFloat:
bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
// 顺序 0123 即可
newBuffer = Buffer.from(bufferArr);
register.value = newBuffer.readFloatBE(0);
break;
case EnumDataType.Long:
bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
// 改变顺序 0123 => 2301
sortedBufferArr = [bufferArr[2], bufferArr[3], bufferArr[0], bufferArr[1]];
newBuffer = Buffer.from(sortedBufferArr);
register.value = newBuffer.readInt32BE(0);
break;
case EnumDataType.SwappedLong:
bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
// 顺序 0123 即可
newBuffer = Buffer.from(bufferArr);
register.value = newBuffer.readInt32BE(0);
break;
case EnumDataType.Double:
bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
// 改变顺序 0123 4567 => 6745 2301
sortedBufferArr = [bufferArr[6], bufferArr[7], bufferArr[4], bufferArr[5], bufferArr[2], bufferArr[3], bufferArr[0], bufferArr[1]];
newBuffer = Buffer.from(sortedBufferArr);
register.value = newBuffer.readDoubleBE(0);
break;
case EnumDataType.SwappedDouble:
bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
// 顺序 0123 4567 即可
newBuffer = Buffer.from(bufferArr);
register.value = newBuffer.readDoubleBE(0);
break;
default:
register.value = bufferDatas.data[bufferAddr[0]];
}
}
}
}
/**
* 读取数据一次,然后断开
*/
async readOnce() {
let connRes = await this.connect();
if (this.client.isOpen === true) {
await this.readData();
this.disconnect();
return this.registerMap;
} else {
return connRes;
}
}
/**
* 轮询,外部调用
* @param {Number} interval 轮询间隔,单位:毫秒
*/
async polling(interval?: number) {
let res: ResultInterface = {
result: false,
info: null
}
if (interval !== undefined) {
// 如果未传入数字 Interval,那么使用默认值
this.pollingInterval = interval;
}
if (this.client.isOpen === true) {
await this.readData();
setTimeout(() => { this.polling() }, this.pollingInterval);
res.result = true;
res.info = this.registerMap;
return res;
} else {
let connRes = await this.connect();
if (typeof connRes !== 'string') {
await this.readData();
setTimeout(() => { this.polling() }, this.pollingInterval);
res.result = true;
res.info = this.registerMap;
return res;
} else {
await this.reconnect();
res.info = 'reconnecting';
return res;
}
}
}
/**
* 重连
*/
private async reconnect() {
console.log(`try to reconnect ${this.host}:${this.port} ${this.deviceID} ${new Date()}`);
const connRes = await this.connect();
if (typeof connRes !== 'string') {
// 重连成功
this.firstConnFailTime = 0;
this.polling();
} else {
// 重连失败
if (this.firstConnFailTime === 0) {
this.firstConnFailTime = new Date().getTime();
}
if (this.maxReconnectTime !== 0) {
// 最大尝试重连时长不为无限
if (new Date().getTime() - this.firstConnFailTime <= this.maxReconnectTime * 60 * 60 * 1000) {
// 未超过最大尝试重连时间,继续重连
setTimeout(async () => {
await this.reconnect();
}, this.tryReconnectTime * 1000);
} else {
console.log(`${this.host}:${this.port} ${this.deviceID} ${new Date()} max reconnect time has come, connetion closed`);
}
} else {
// 最大尝试重连时长为无限
setTimeout(async () => {
await this.reconnect();
}, this.tryReconnectTime * 1000);
}
}
}
/**
* 写数据
* @param register 寄存器
* @param value 要写的值
*/
async writeData(register: string, value: boolean | number | string | null) {
let reg = this.registerMap.get(register);
let result: ResultInterface = {
result: true,
info: '',
};
if (reg === undefined) {
// 从设置的寄存器表中也没有找到此寄存器信息
result.result = false;
result.info = 'register is not exist';
} else {
// 找到了寄存器
let regAddr = reg.startAddr - 1;
let regType = reg.registerType;
let val: any;
if (regType === EnumRegisterType.Coil) {
if (typeof value !== 'boolean') {
result.result = false;
result.info = 'wrong value type';
} else {
val = value;
}
} else if (reg.registerType === EnumRegisterType.Holding) {
switch (reg.dataType) {
case EnumDataType.Decimal:
case EnumDataType.Int:
if (typeof value !== 'number' || value % 1 !== 0) {
// 不是整数
result.result = false;
result.info = 'value is not an integer';
} else {
val = [value % 65536];
}
break;
case EnumDataType.Binary:
if (/^[0-1]{1,16}$/.test(value + '')) {
val = parseInt(value + '', 2);
} else {
result.result = false;
result.info = 'value is illegal';
}
break;
case EnumDataType.Hex:
if (/^[0-9A-F]{1,4}$/.test(value + '')) {
val = parseInt(value + '', 16);
} else {
result.result = false;
result.info = 'value is illegal';
}
break;
case EnumDataType.Float:
try {
if (typeof value === 'number') {
let buffer = Buffer.alloc(4);
buffer.writeFloatBE(value, 0);
val = [buffer.readUInt16BE(2), buffer.readUInt16BE(0)];
} else {
result.result = false;
result.info = 'value is not a number';
}
} catch (e) {
result.result = false;
result.info = e.message;
}
break;
case EnumDataType.SwappedFloat:
try {
if (typeof value === 'number') {
let buffer = Buffer.alloc(4);
buffer.writeFloatBE(value, 0);
val = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
} else {
result.result = false;
result.info = 'value is not a number';
}
} catch (e) {
result.result = false;
result.info = e.message;
}
break;
case EnumDataType.Long:
try {
if (typeof value === 'number') {
let buffer = Buffer.alloc(4);
buffer.writeInt32BE(value, 0);
val = [buffer.readUInt16BE(2), buffer.readUInt16BE(0)];
} else {
result.result = false;
result.info = 'value is not a number';
}
} catch (e) {
result.result = false;
result.info = e.message;
}
break;
case EnumDataType.SwappedLong:
try {
if (typeof value === 'number') {
let buffer = Buffer.alloc(4);
buffer.writeInt32BE(value, 0);
val = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
} else {
result.result = false;
result.info = 'value is not a number';
}
} catch (e) {
result.result = false;
result.info = e.message;
}
break;
case EnumDataType.Double:
try {
if (typeof value === 'number') {
let buffer = Buffer.alloc(8);
buffer.writeDoubleBE(value, 0);
val = [buffer.readUInt16BE(6), buffer.readUInt16BE(4), buffer.readUInt16BE(2), buffer.readUInt16BE(0)];
} else {
result.result = false;
result.info = 'value is not a number';
}
} catch (e) {
result.result = false;
result.info = e.message;
}
break;
case EnumDataType.SwappedDouble:
try {
if (typeof value === 'number') {
let buffer = Buffer.alloc(8);
buffer.writeDoubleBE(value, 0);
val = [buffer.readUInt16BE(0), buffer.readUInt16BE(2), buffer.readUInt16BE(4), buffer.readUInt16BE(6)];
} else {
result.result = false;
result.info = 'value is not a number';
}
} catch (e) {
result.result = false;
result.info = e.message;
}
break;
}
} else {
result.result = false;
result.info = 'wrong register type';
}
if (result.result) {
if (this.client.isOpen) {
// 已连接
if (regType === EnumRegisterType.Coil) {
result.info = await this.client.writeCoil(regAddr, val);
} else if (regType === EnumRegisterType.Holding) {
result.info = await this.client.writeRegisters(regAddr, val);
}
} else {
// 未连接
const connRes = await this.connect();
if (typeof connRes !== 'string') {
// 连接成功
if (regType === EnumRegisterType.Coil) {
result.info = await this.client.writeCoil(regAddr, val);
} else if (regType === EnumRegisterType.Holding) {
result.info = await this.client.writeRegisters(regAddr, val);
}
} else {
// 连接失败
result.result = false;
result.info = 'connect failed';
}
}
}
return result;
}
}
}