開發與維運

JavaScript使用Modbus協議實現RTU設備連雲

來源 | HaaS技術社區

1、功能簡介

在阿里雲物聯網平臺下發物模型屬性設置數據,HaaS600Kit 接收並解析雲端數據後控制 Modbus 繼電器設備進行開關動作。

2、硬件連接

硬件平臺:HaaS600開發板(EC600S-CN)、TTL轉485串口板、Modbus繼電器設備。

注意本示例中 HaaS600開發板需要5V DC接口供電,否則串口轉接板工作時有可能因為供電不足導致系統重啟。

p286627.jpeg

2.1、HaaS600串口使用介紹

HaaS600 用戶串口只有一個UART2,為了方便開發,板子上提供了USB2、J5和 J6 三處UART2 接口,其中 J5 和 J6 接口電壓是 3.3V。注意使用 J5 和 J6 接口要把開關 K1 撥到絲印 JP 側,開關 K3 用於調整串口TX RX順序。本示例使用的TTL轉485接口板使用J5接口,K3 撥到絲印 MASTER 側。

p286628.png

3、雲端控制

參考阿里雲物聯網平臺幫助文檔登陸阿里雲官網,依次創建產品創建設備後,添加物模型屬性 PowerSwitch

自定義的 PowerSwitch 屬性和應用代碼直接關聯,下面介紹如何單個添加自定義物模型屬性。

單個添加物模型屬性

功能定義

image.png

編輯草稿

image.png

添加自定義功能

image.png

發佈上線

image.png

在線調試

image.png

設置開啟、關閉

image.png

4、串口日誌

cloud onProps, msg_id is 1399004153
cloud onProps, params_len is 17
cloud onProps, params is {"PowerSwitch":1}
turn on relay
write send 1,5,0,0,255,0,140,58
rtn onData
onData: 1,5,0,0,255,0,140,58
 
cloud onProps, msg_id is 1435839973
cloud onProps, params_len is 17
cloud onProps, params is {"PowerSwitch":0}
turn off relay
write send 1,5,0,0,0,0,205,202
rtn onData
onData: 1,5,0,0,0,0,205,202

參考代碼

agriculture_demo.zip

app.js

把已創建設備的 ProductKey、ProductKey 和 DeviceSecret 填入到 app.js 代碼中。

var iot = require('iot');
var network = require('network');
 
var net = network.openNetWorkClient();
 
var productKey = 'xxxx';      /* your productKey */
var deviceName = 'xxxx';      /* your deviceName */
var deviceSecret = 'xxxx';  /* your deviceSecret */
 
var device;
 
var modbus = require('./modbus.js');
 
var deviceAddr = 1;
var timeout = 300;
var modbus_device = modbus(
  { id: 'UART2' },
  { id: 'D13', polarity: 1 },
  deviceAddr
);
 
function createDevice() {
    device = iot.device({
        productKey: productKey,
        deviceName: deviceName,
        deviceSecret: deviceSecret
    });
    var powerSwitch = 0;
    modbus_device.write(deviceAddr, 'c1', powerSwitch, timeout).then(function (data) {
        console.log('write c1 data ' + data);
    });
 
    device.on('connect', function () {
        console.log('(re)connected');
        /* post props */
        device.postProps(JSON.stringify({
            PowerSwitch: powerSwitch
        }));
 
        /* 雲端下發服務事件 */
        device.onService(function (res) {
            console.log('cloud onService, msg_id is ' + res.msg_id);
            console.log('cloud onService, service_id is ' + res.service_id);
            console.log('cloud onService, params_len is ' + res.params_len);
            console.log('cloud onService, params is ' + res.params);
        });
 
        /* 雲端設置屬性事件 */
        device.onProps(function (res) {
            console.log('cloud onProps, msg_id is ' + res.msg_id);
            console.log('cloud onProps, params_len is ' + res.params_len);
            console.log('cloud onProps, params is ' + res.params);
            var payload = JSON.parse(res.params);
            powerSwitch = parseInt(payload.PowerSwitch);
            if(powerSwitch == 1) {
                console.log('turn on relay');
            } else {
                console.log('turn off relay');
            }
            modbus_device.write(deviceAddr, 'c1', powerSwitch, timeout).then(function (data) {
                console.log('write c1 data ' + data);
            });
            device.postProps(JSON.stringify({
                PowerSwitch: powerSwitch
            }));
        });
    });
 
    /* 網絡斷開事件 */
    device.on('disconnect', function () {
        console.log('disconnect ');
    });
 
    /* 關閉連接事件 */
    device.on('end', function () {
        console.log('iot client just closed');
    });
 
    /* 發生錯誤事件 */
    device.on('error', function (err) {
        console.log('error ' + err);
    });
}
 
var status = net.getStatus();
console.log('net status is: ' + status);
 
 
if (status == 'connect') {
    createDevice();
} else {
    net.on('connect', function () {
        console.log('net is connected success!');
        createDevice();
    });
}

modbus.js

function Modbus(uartPort, dirPort) {
    var rtn = {}
    var lastTid = 1
    rtn.readTimeout = 500;
    rtn.readTimeouthandler;
    rtn.packetBufferLength = 100
    rtn.packets = []
 
    rtn.stream = Serial(uartPort, dirPort)
    rtn.protocal = 'rtu'
 
    rtn.read = function (unitId, address, timeout, callback) {
        var parsedAddress = parseAddress(address)
        var funcCode = parsedAddress.fcRead
        var length = parsedAddress.length
        address = parsedAddress.address
 
        rtn.readTimeout = timeout
        rtn.readTimeouthandler = setTimeout(function() {
            rtn.packets[tid].promiseReject('timeout')
        }, rtn.readTimeout)
 
        var tid = getTid()
 
        var buff = makeDataPacket(tid, 0, unitId, funcCode, address, null, length)
        if (rtn.protocal == 'rtu') { buff = buff.rtu }
 
        var packet = {
            onResponce: callback,
            tx: {
                funcCode: funcCode,
                tid: tid,
                address: address,
                hex: buff.toString('hex')
            },
            promiseResolve: null,
            promiseReject: null,
            rx: null
        }
 
        rtn.packets[tid] = packet
 
        rtn.stream.send(buff)
        console.log('read send ' + buff.toString('hex'))
        return new Promise(function (resolve, reject) {
            rtn.packets[tid].promiseResolve = resolve
            rtn.packets[tid].promiseReject = reject
        })
 
    }
    rtn.write = function (unitId, address, value, timeout, callback) {
        var parsedAddress = parseAddress(address)
        var funcCode = parsedAddress.fcWrite
        var length = parsedAddress.length
        address = parsedAddress.address
        var tid = getTid()
 
        rtn.readTimeout = timeout
        rtn.readTimeouthandler = setTimeout(function() {
            rtn.packets[tid].promiseReject('timeout')
        }, rtn.readTimeout)
    
        if (funcCode == 5 && value == true) { value = 65280 } // To force a coil on you send FF00 not 0001
 
        var buff = makeDataPacket(tid, 0, unitId, funcCode, address, value, length)
        if (rtn.protocal == 'rtu') { buff = buff.rtu }
        // console.log('buff ' + buff)
        var packet = {
            onResponce: callback,
            tx: {
                funcCode: funcCode,
                tid: tid,
                address: address,
                hex: buff.toString('hex')
            },
            rx: null
        }
        rtn.packets[tid] = packet
 
        rtn.stream.send(buff)
        console.log('write send ' + buff.toString('hex'))
 
        return new Promise(function (resolve, reject) {
            rtn.packets[tid].promiseResolve = resolve
            rtn.packets[tid].promiseReject = reject
        })
    }
    var getTid = function () {
        if (lastTid > rtn.packetBufferLength) { lastTid = 0 }
        lastTid++
        if (rtn.protocal == 'rtu') { lastTid = 0 }
        return lastTid
    }
 
    rtn.stream.onData = function (buf) {
        console.log('onData: ' + buf.toString('hex'))
 
        var modbusRes
        if (rtn.protocal == "rtu") { modbusRes = parseResponseRtu(buf) }
 
        var value = modbusRes.value
        var tid = modbusRes.tid
        if (rtn.protocal == "rtu") { tid = 0 }
 
        var err = null
        if (modbusRes.exceptionCode) { err = 'Exception Code: 02 - Illegal Data Address' }
 
        rtn.packets[tid].rx = modbusRes
        rtn.packets[tid].rx.hex = buf.toString('hex')
        if (typeof (rtn.packets[tid].onResponce) == "function") {
            rtn.packets[tid].onResponce(err, value)
        }
 
        if (err) {
            rtn.packets[tid].promiseReject(err)
        }
        else {
            // console.log('raw data:', value)
            rtn.packets[tid].promiseResolve(value)
            clearTimeout(rtn.readTimeouthandler)
        }
 
    }
    return rtn
}
 
function parseResponseRtu(buffer) {
    var res = {}
    res.tid = 1
    var buf = new Buffer(buffer)
    res.unitId = buf.readInt8(0)                     //Unit Id        - Byte 6
    res.funcCode = buf.readInt8(1)                     //Function Code  - Byte 7
    res.byteCount = Math.abs(buf.readInt8(2))           //Byte Count     - Byte 8
    if (buf.length > 3) {
        // res.value = buf.readIntBE(3, buf.length - 5)       //Data           - Bytes 9+
        // var arr = Array.prototype.slice.call(new Uint8Array(buf))
        // res.value = new Buffer(arr.slice(3, buf.length - 5))
        var dataBuffer = buf.subarray(3, buf.length - 2)
        res.value = Array.prototype.slice.call(new Uint8Array(buf))
    }
 
    return res
}
 
function crc16modbus(buf, previous) {
    var TABLE = [0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240, 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040];
 
    var crc = typeof previous !== 'undefined' ? ~~previous : 0xffff;
 
    for (var index = 0; index < buf.length; index++) {
        var byte = buf[index];
        crc = (TABLE[(crc ^ byte) & 0xff] ^ crc >> 8) & 0xffff;
    }
 
    return crc;
}
 
function makeDataPacket(transId, protoId, unitId, funcCode, address, data, length) {
 
    if (typeof (data) == "boolean" && data) { data = 1 }
    if (typeof (data) == "boolean" && !data) { data = 0 }
 
 
    if (address == 0) { address = 65535 }
    else { address = address - 1 }
 
    var dataBytes = 0
    if (funcCode == 15) { dataBytes = length }
    if (funcCode == 16) { dataBytes = length * 2 }
 
    var bufferLength = 12
    if (funcCode == 15 || funcCode == 16) { bufferLength = 13 + dataBytes }
 
    var byteCount = bufferLength - 6
 
    var buf = new Buffer(bufferLength)
 
    buf.writeUInt16BE(transId, 0)
    buf.writeUInt16BE(protoId, 2)
    buf.writeUInt16BE(byteCount, 4)
    buf.writeUInt8(unitId, 6)
    buf.writeUInt8(funcCode, 7)
    buf.writeUInt16BE(address, 8)
 
    if (funcCode == 1 || funcCode == 2 || funcCode == 3 || funcCode == 4) {
        buf.writeUInt16BE(length, 10)
    }
    if (funcCode == 5 || funcCode == 6) {
        buf.writeInt16BE(data, 10)
    }
    if (funcCode == 15 || funcCode == 16) {
        buf.writeInt16BE(length, 10)
        buf.writeUInt8(dataBytes, 12)
        buf.writeInt32BE(data, 13)
    }
 
    var makeCrc = crc16modbus
 
    var bufRtu = buf.slice(6, bufferLength)
    var crc = makeCrc(bufRtu)
    var arr = Array.prototype.slice.call(new Uint8Array(bufRtu))
    arr.push(0);
    arr.push(0);
    var buffRtu = new Buffer(arr);
    buffRtu.writeUInt16LE(crc, buffRtu.length - 2)
    //console.log('buffRtu:', buffRtu);
    return { rtu: Array.prototype.slice.call(new Uint8Array(buffRtu)) }
}
 
function parseAddress(address) {
    var rtn = {}
    address = address.toLowerCase()
 
    // 提取地址
    // Data Type                  Short Hand   Size        Accessibility
    // Descrite Input             i            1 Bit       Read Only
    // Coil                       c            1 Bit       Read / Write
    // Input Register             ir           16 Bits     Read Only
    // Holding Register           hr           16 Bits     Read / Write
    var isRegister = address.replace('r', '').length !== address.length;
    if (isRegister) {
        rtn.address = address.substr(2)
        rtn.type = address.substr(0, 2)
    }
    if (!isRegister) {
        rtn.address = address.substr(1)
        rtn.type = address.substr(0, 1)
    }
 
    var isRange = rtn.address.replace('-', '').length !== rtn.address.length
    if (isRange) {
        var range = rtn.address.split('-')
        rtn.length = range[0] - range[1]
        if (rtn.length < 0) { rtn.address = range[0] }
        if (rtn.length > 0) { rtn.address = range[1] }
        rtn.length = Math.abs(rtn.length) + 1
    }
    if (!isRange) {
        rtn.length = 1
    }
 
    rtn.address = parseInt(rtn.address)
 
    // 獲取類型
    // FC1 - Read Coil
    // FC2 - Read Input
    // FC3 - Read Holding Registers
    // FC4 - Read Input Registers
    // FC5 - Write Single Coil
    // FC6 - Write Single Register
    // FC15 - Write Multiple Coils
    // FC16 - Write Multiple Registers
    if (rtn.type == 'c') {
        rtn.fcRead = 1
        rtn.fcWrite = 5
    }
    if (rtn.type == 'i') {
        rtn.fcRead = 2
    }
    if (rtn.type == 'hr' && !isRange) {
        rtn.fcRead = 3
        rtn.fcWrite = 6
    }
    if (rtn.type == 'hr' && isRange) {
        rtn.fcRead = 3
        rtn.fcWrite = 16
    }
    if (rtn.type == 'ir') {
        rtn.fcRead = 4
    }
 
    return rtn
}
 
function Serial(uartPort, dirPort) {
    var rtn = {}
    var Uart = require('uart')
 
    rtn.port = Uart.open(uartPort)
 
    if (dirPort) {
        var events = require('events');
        var event = new events();
        var Gpio = require('gpio')
        var option = {};
        option.id = dirPort.id;
        rtn.io = Gpio.open(option);
    }
 
    rtn.send = function (data) {
        rtn.io.writeValue(dirPort.polarity === 0 ? 0 : 1);
        rtn.port.write(data)
        event.emit('send')
    }
 
    rtn.onData = function () { }
 
    rtn.port.on('data', function (data) {
        console.log("rtn onData");
        rtn.onData(data);
    })
 
    event.on('send', function () {
        rtn.io.writeValue(dirPort.polarity === 0 ? 1 : 0);
    })
 
    return rtn
}
 
module.exports = Modbus;
app.json
 
{
    "version": "1.0.0",
    "io": {
      "UART2": {
        "type": "UART",
        "port": 2,
        "dataWidth": 8,
        "baudRate": 9600,
        "stopBits": 1,
        "flowControl": "disable",
        "parity": "none"
      },
      "D13": {
        "type": "GPIO",
        "port": 33,
        "dir": "output",
        "pull": "pulldown"
      }
    },
    "debugLevel": "DEBUG",
    "repl": "disable"
  }

Leave a Reply

Your email address will not be published. Required fields are marked *