Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Each of the responses starts with a single byte (uint8) indicating the length of its payload (len) followed by that many bytes of payload. The payload consists of the raw Modbus response as sent by the Slave Device followed by 3 additional bytes: the first register/coil as uint16 (big endian) and the number of registers/coils as uint8 taken from the executed command. The following tables visualise the message structure. See the Examples Section for some sample data messages explained down to the individual bytes. We also provide a  Reference Decoder decoder in JavaScript that can read the format.

Expand
titleReference decoder

Reference decoder

This is a decoder written in JavaScript that can be used to parse the device's LoRaWAN messages. It can be used as is in The Things Network.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218

function readVersion(bytes) {
    if (bytes.length<3) {
        return null;
    }
    return "v" + bytes[0] + "." + bytes[1] + "." + bytes[2];
}
 
function int40_BE(bytes, idx) {
    bytes = bytes.slice(idx || 0);
    return bytes[0] << 32 |
        bytes[1] << 24 | bytes[2] << 16 | bytes[3] << 8 | bytes[4] << 0;
}
 
function int16_BE(bytes, idx) {
    bytes = bytes.slice(idx || 0);
    return bytes[0] << 8 | bytes[1] << 0;
}
 
function uint16_BE(bytes, idx) {
    bytes = bytes.slice(idx || 0);
    return bytes[0] << 8 | bytes[1] << 0;
}
 
function port1(bytes) {
    return {
        "port":1,
        "version":readVersion(bytes),
        "flags":bytes[3],
        "temp": int16_BE(bytes, 4) / 10,
        "vBat": int16_BE(bytes, 6) / 1000,
        "timestamp": int40_BE(bytes, 8),
        "operationMode": bytes[13],
        "noData": !!(bytes[3] & 0x01)
    };
}
 
function port2(bytes) {
    var regs = [];
    if (bytes.length > 5) {
        // loop through data packs
        var b = bytes.slice(5);
        while (b.length>=4) {
            var r = {
                "device":b[0],
                "register":int16_BE(b, 1),
                "count":b[3] & 0x3f,
                "error":!!(b[3]>>7),
                "data":null
            };
            var dataLen = r["count"]*2;
            if (b.length >= dataLen+4) {
                r["data"] = b.slice(4, 4 + dataLen);
            }
            regs.push(r);
            b = b.slice(4+dataLen);
        }
    }
    return {
        "port":2,
        "timestamp": int40_BE(bytes, 0),
        "registers": regs
    };
}
 
function modbusErrorString(code) {
    // Modbus exception codes
    // see https://en.wikipedia.org/wiki/Modbus#Exception_responses
    switch (code) {
        case 1:
            return "Illegal Function";
        case 2:
            return "Illegal Data Address";
        case 3:
            return "Illegal Data Value";
        case 4:
            return "Slave Device Failure";
        case 5:
            return "Acknowledge";
        case 6:
            return "Slave Device Busy";
        case 7:
            return "Negative Acknowledge";
        case 8:
            return "Memory Parity Error";
        case 10:
            return "Gateway Path Unavailable";
        case 11:
            return "Gateway Target Device Failed to Respond";
        default:
            return "Unknown error code";
    }
}
 
function parseModbusPayloadRegisters(payload) {
    if (payload.length < 1) {
        return null;
    }
    var byteCnt = payload[0];
    if (payload.length !== byteCnt + 1) {
        return null;
    }
    var vals = [];
    for (var i=0; i<byteCnt; i+=2) {
        vals.push([+payload[i+1], +payload[i+2]])
    }
    return vals;
 
}
function parseModbusResponse(raw) {
    var resp = {};
    if (raw.length >= 6) {
        var fun = raw[1] & 0xf;
        var error = !!(raw[1] & 0x80);
        var rawResp = raw.slice(0, raw.length - 3);
        resp["slave"] = raw[0];
        resp["function"] = fun;
        resp["error"] = error;
        resp["start"] = uint16_BE(raw, raw.length - 3);
        resp["cnt"] = raw[raw.length - 1];
        resp["raw"] = rawResp;
        if (error) {
            resp["errorCode"] = raw[2];
            resp["errorText"] = modbusErrorString(raw[2]);
        } else {
            resp["values"] = parseModbusPayloadRegisters(rawResp.slice(2))
            // TODO: coils
        }
    }
    return resp;
}
 
function FullResponses(bytes, port) {
    var timestamp = int40_BE(bytes);
    var pos = 5;
    var resps = [];
    while (pos < bytes.length) {
        var respLen = bytes[pos++];
        if (bytes.length >= pos + respLen) {
            var rawResponse = bytes.slice(pos, pos + respLen);
            resps.push(parseModbusResponse(rawResponse));
            pos += respLen;
        } else {
            break;
        }
    }
    return {
        "port": port,
        "timestamp" : timestamp,
        "responses": resps
    };
}
 
function bin2String(array) {
    var result = "";
    for (var i = 0; i < array.length; i++) {
        result += String.fromCharCode(array[i]);
    }
    return result;
}
 
function ConfigResponse(data) {
    var t = bin2String(data);
    return {
        "response" : t,
        "error" : (t.length === 0) || (t[0] === '!')
    }
}
 
/**
 * TTN decoder function.
 */
function Decoder(bytes, port) {
    switch (port) {
        case 1:
            // Status message:
            return port1(bytes);
        case 2:
            // not legacy format:
            return port2(bytes);
        case 3:
        case 4:
            // v1.0.0 format, full modbus responses:
            return FullResponses(bytes, port);
        case 5:
            // continuation of previous response:
            return {};
        case 6:
            // dense format with prefixed timestamp:
            return {};
        case 7:
            // dense format without timestamp:
            return {};
        case 128:
            return ConfigResponse(bytes);
    }
    return {"error":"invalid port", "port":port};
}
 
/**
 * LoRaServer decoder function.
 */
function Decode(fPort, bytes) {
    // wrap TTN Decoder:
    return Decoder(bytes, fPort);
}
function Parse(input) {
    var data = bytes(atob(input.data));
    var port = input.fPort;
    var fcnt = input.fCnt;
    var vals =  Decoder(data, port);
    vals["port"] = port;
    vals["data"] = data;
    vals["fnct"] = fcnt;
    var lastFcnt = Device.getProperty("lastFcnt");
    vals["reset"] = fcnt <= lastFcnt;
    Device.setProperty("lastFcnt", fcnt);
    return vals;
}



The timestamp in the message is the wakeup time when the device was activated by the cron expression in MbCron (using the devices internal clock), so all Uplinks from a single activation will have the same Timestamp. The Modbus Response in the message in addition with the start register/coil and the register/coil count makes it possible to know which registers/coils where exactly read/written, what kind they were, and the address of the device. For Modbus Commands that do not have a register/coil count (like function 5, forcing a single coil), or for those that do not contain a start register/coil (e.g. funtion 7, reading exception status), the contents of the additional fields start register and/or count are undefined. The payload format used only a single byte for the count value, so if you are reading/writing more than 255 coils, the higher byte will be cut off.

...

Expand
titleThe Examples section contains an illustration of a split up Response

Examples

This chapter illustrates with some examples, how working with the Modbus Bridge looks like. The bytes that are sent via LoRaWAN are presented here as hex strings, while on the air they are sent as raw bytes. Modbus Commands and Responses are broken down to their parts in the explanations, but explaining the format used by Modbus in detail is beyond the scope of this manual. You can find a short explanation on Modbus on Wikipedia: https://en.wikipedia.org/wiki/Modbus.

Uplinks in Verbose Payload Format (PlFmt=1)

The following shows some examples of configuration for the automated reading and what the generated Uplinks for that could look like.

Example A1: Read Holding Registers 0, 1, and 2 of device with address 1


Verbose Payload Format (Port 3, PlFmt=1)

MbCmd = '010300000003'

# Example resulting Uplink after successful readout
Up, Port 3: '005d1698fd0c0103061234567890ab000003'
 '005d1698fd' -> timestamp = 1561762045 -> 2019-06-28T22:47:25 UTC
 '0c'       -> first Response is 12 bytes long
 '0103061234567890ab000003' 12 bytes modbus response:
   '01' -> slave device with address 1
   '03' -> function 3 = read Holding Register, success
   '06' -> 6 bytes of data in Response following
   '1234567890ab' -> 6 bytes of data
   '0000' -> start reading at register 0
   '03' -> read 3 consecutive registers

# Example resulting Uplink after failing readout
Up, Port 3: '005d1698fd0601830b000003'
 '005d1698fd' -> timestamp = 1561762045 -> 2019-06-28T22:47:25 UTC
 '06'       -> first Response is 6 bytes long
 '01830b000003' 3 bytes modbus response:
   '01' -> slave device with address 1
   '83' -> function 3 with error indicator 80 = read Holding Register, failed
   '0b' -> error code 11: "Gateway Target Device Failed to Respond"
   '0000' -> start reading at register 0
   '03' -> read 3 consecutive registers

Example A2: Read coils 1000-1019 of device 32


Verbose Payload Format (Port 3, PlFmt=1)

MbCmd = '200103e80014'

# Example resulting Uplink
Up, Port 3: '005d1698fd 09 200103f1041a03e814'
 '005d1698fd' -> timestamp = 1561762045 -> 2019-06-28T22:47:25 UTC
 '09' -> first Response is 9 bytes long
 '200103f1041a03e814' 9 bytes of response:
  '20' -> slave device with address 32
  '01' -> read coils, success
  '03' -> 3 bytes of data
  'f1041a' -> 20 bits of data packed into 3 bytes
  '03e8' -> start reading at coil 1000
  '14' -> read 20 consecutive coils

Example A3: Read registers from two devices


Verbose Payload Format (Port 3, PlFmt=1)

MbCmd = '0a0300010005,3001ea600020'

# Example resulting Uplink
Up, Port 3: '005d1698fd100a030a111122223333444455550001050a30010412345678ea6020'
 '005d1698fd' -> timestamp = 1561762045 -> 2019-06-28T22:47:25 UTC
 '10' -> first Response is 16 bytes long
 '0a030a11112222333344445555000105' 16 bytes of Response
  '0a' -> slave device with address 10
  '03' -> read Holding Registers, success
  '0a' -> 10 bytes of data following
  '11112222333344445555' 10 bytes of data
  '0001' -> start reading at register 1
  '05' -> read 5 registers
 '0a' -> second Response is 10 bytes long
 '30010412345678ea6020' 10 bytes of Response
  '30' -> slave device with address 48
  '01' -> read Coils, success
  '04' -> 4 bytes of data following
  '12345678' -> 32 bits of data packed in 4 bytes
  'ea60' -> start at coil 60000
  '20' -> read 32 coils

Example A4: Split uplink message


Verbose Payload Format (Port 3, PlFmt=1)

MbCmd = '010300010020'
# Command reads 32 consecutive registers resulting in 64 bytes payload

# Example resulting Uplinks for a Spreading Factor of 12 with 51 bytes of payload per message
Up 1, Port 3: '005d1698fd46010340000100020003000400050006000700080009000a000b000c000d000e000f001000110012001300140015'
  '005d1698fd' -> timestamp = 1561762045 -> 2019-06-28T22:47:25 UTC
  '46' -> first Response is 70 bytes long since the remainder of the message does not contain 70 bytes,
  you know there must be an additional part coming
Up 2, Port 5: '0016001700180019001a001b001c001d001e001f00200120'
  This contains the rest of the message. Appended to the previous message, it adds up to the correct number of bytes.

Uplinks triggered by Downlink Commands


Verbose Payload Format (Port 4, Downlink Response)

Down, Port 4: '06180401000001'
 '06' -> first Command is 6 bytes long
 '180401000001' 6 bytes of Modbus Command
  '18' -> slave device with address 24
  '04' -> function 4, read Input Register
  '0100' -> start at register 256 '0001' -> read 1 register

# Example resulting Uplink
Up, Port4: '004b3dd67508180402abcd010001'
 '004b3dd675' -> timestamp = 1262343797 -> 2010-01-01T11:03:17 UTC
 '08' -> first Response is 8 bytes long
 '180404abcd010001' 8 bytes of Response
  '18' -> slave device with address 24
  '04' -> read Input Register, success
  '02' -> 2 bytes of data following
  'abcd' -> 2 bytes of data
  '0100' -> start at register 256
  '01' -> read 1 register

Example B2: Writing holding registers on multiple devices


Verbose Payload Format (Port 4, Downlink Response)

Down, Port 4: '06a106aabb12340fa210a0010004081122334455667788'
 '06' -> first Command is 6 bytes long
 'a106aabb1234' 6 bytes of Modbus Command
  'a1' -> slave device with address 161
  '06' -> function 6, write single Holding Register
  'aabb' -> address of Register to write = 43707
  '1234' -> two bytes of data
 '0f' -> second Command is 15 bytes long
 'a210a0010004081122334455667788' 15 byte of Modbus Command
  'a2' -> slave device with address 162
  '10' -> function 16, write multiple Holding Registers
  'a001' -> start at register 40961
  '0004' -> 4 registers to write
  '08' -> 8 bytes of data follow
  '1122334455667788' -> 8 bytes of data

# Example resulting Uplink Up, Port 4: '004b3dd67506a1860200000006a210a0010004'
 '004b3dd675' -> timestamp = 1262343797 -> 2010-01-01T11:03:17 UTC
 '06' -> first Response is 3 bytes long
 'a18602000000' 3 bytes of Modbus Response
  'a1' -> slave device address 161 '86' -> write single Holding Regsiter, failed
  '02' -> error code 2: "Illegal Data Address"
  '0000' -> start register not used (undefined)
  '00' -> count not used (undefined)
 '06' - second Response is 6 bytes long
 'a210a0010004' 6 bytes of Modbus Response
  'a2' -> slave device address 162
  '10' -> write multiple Holding Registers, success
  'a001' -> start at register 40961
  '0004' -> 4 registers to write

Uplinks in Compact Payload Format

Example C1: Single Modbus Command, PlFmt 4

# Attached device: B+G E-Tech power Meter
 
# Config:
MbCmd = 010300000003  -> Read registers 0 to 3 from Slave 1
PlFmt = 4             -> Compact Format with Timestamp
PlMax = 51            -> Max 51 Bytes per Uplink
PlId  = 0             -> Payload Id = 0
 
# Info from Log
APP| Number of commands to be executed on cron: 1
APP|   01 03 00 00 00 03
APP| Compact format definition, id=0, max size=51
APP|   Port 20:
APP|     000-000=error&fmt-id
APP|     001-005=timestamp
APP|     006-011=resp(010300000003)
 
# Successful readout, leading to Uplink on Port 20: '00005fd8bf08000000010033'
  '00' -> '0' no error, '00' -> PlId = 0
  '005fd8bf08' -> timestamp = 1608040200 -> 2020-12-15T13:50:00 UTC
  '000000010033' -> 6 Bytes data from 3 Registers -> 3.07 kWh
 
# Failed readout, leading to Uplink on Port 20: '80005fd8c7caffffffffffff'
  '80' -> '1' error in any of the responses, '00' -> PlId = 0
  '005fd8c7ca' -> timestamp = 1608042442 -> 2020-12-15T14:27:22 UTC
  'ffffffffffff' -> 6 Bytes set to 0xff, indicating error

Example C2: Multiple Commands, PlFmt 5

# Attached devices: Multiple B+G E-Tech power Meters
 
# Config:
MbCmd = 010300000003,020300000003 -> Read registers 0 to 3 from Slave 1
PlFmt = 5                         -> Compact Format with Timestamp
PlMax = 51                        -> Max 51 Bytes per Uplink
PlId  = 13                        -> Payload Id = 13
 
# Info from Log
APP| Number of commands to be executed on cron: 2
APP|   01 03 00 00 00 03
APP|   02 03 00 00 00 03
APP| Compact format definition, id=29, max size=51
APP|   Port 20:
APP|     000-000=error&fmt-id
APP|     001-006=resp(010300000003)
APP|     007-012=resp(020300000003)
 
# Successful readout of both, leading to Uplink on Port 20: '1d0000000100330000001a0040'
  '1d' -> '0' no error, '1d' -> PlId = 13
  '000000010033' -> 6 Bytes data from 3 Registers, Meter 1 ->  3.07 kWh
  '0000001a0040' -> 6 Bytes data from 3 Registers, Meter 2 -> 67.20 kWh
 
# Partly successful readout, leading to Uplink on Port 20: '9d000000010033ffffffffffff'
  '9d' -> '1' error (in any of the values in message), '1d' -> PlId = 13
  '000000010033' -> 6 Bytes data from 3 Registers, Meter 1 -> 3.07 kWh
  'ffffffffffff' -> 6 Bytes set to 0xff to indicate error, Meter 2
 
# Failed readout, leading to Uplink on Port 20: '9dffffffffffffffffffffffff'
  '9d' -> '1' error (in any of the values in message), '1d' -> PlId = 13
  'ffffffffffff' -> 6 Bytes set to 0xff to indicate error, Meter 1
  'ffffffffffff' -> 6 Bytes set to 0xff to indicate error, Meter 2

Example C3: Multimple Commands, leading in multiple Upoads, PlFmt 5

# Config
MbCmd = 010300000010,010301000004,0103020a000c,010300800008
PlFmt = 5                         -> Compact Format with Timestamp
PlMax = 40                        -> Max 40 Bytes per Uplink
PlId  = 10                        -> Payload Id = 10
 
# Info from Log
APP| Number of commands to be executed on cron: 4
APP|   01 03 00 00 00 10
APP|   01 03 01 00 00 04
APP|   01 03 02 0A 00 0C
APP|   01 03 00 80 00 08
APP| Compact format definition, id=10, max size=40
APP|   Port 20:
APP|     000-000=error&fmt-id
APP|     001-032=resp(010300000010)
APP|   Port 21:
APP|     000-000=error&fmt-id
APP|     001-008=resp(010301000004)
APP|     009-032=resp(0103020a000c)
APP|   Port 22:
APP|     000-000=error&fmt-id
APP|     001-016=resp(010300800008)
 
# Resulting in 3 consecutive uploads on Port 20-22:
# Port 20: 0a0000000100020003000400050006000700080009000a000b000c000d000e000f
  '0a' -> '0' no error, '0a' -> PlId 10
  '0000000100020003000400050006000700080009000a000b000c000d000e000f' -> 32 Bytes of Data from Registers 0x0000-0x000f
# Port 21: 8affffffffffffffff111122223333444455556666777788889999aaaabbbbcccc
  '8a' -> '1' error occured, '0a' -> PlId 10
  'ffffffffffffffff' -> 8 Bytes set to 0xff indicating error reading Registers 0x0100-0x0103
  '111122223333444455556666777788889999aaaabbbbcccc' -> 24 Bytes of Data from Registers 0x020a-0x0215
# Port 22: 0a01010202030304040505060607070808
  '0a' -> '0' no error, '0a' -> PlId 10
  '01010202030304040505060607070808' -> 16 Bytes of Data from Registers 0x0080-0x0087



Remote Config (Port 128 (129-131))

...