Skip to content

Latest commit

 

History

History
 
 

modbus-server

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

MODBUS to OPC UA server

This is a sample MODBUS to OPC UA server. It reads and writes coils and registers from a MODBUS slave and exposes them as variables in OPC UA.

Registers are two byte words and coils are discrete on/off values, i.e. bools. For each there is a table which is read-only and another which is read-write.

Each register / coil is addressable by a number and and occupies one of 4 tables.

  • Number 0xxxx - Discrete Output Coil from 1 to 9999 - Read-Write
  • Number 1xxxx - Discrete Input Coil from 10001 to 19999 - Read-Only
  • Number 3xxxx - Input Register from 30001 to 39999 - Read-Only
  • Number 4xxxx - Output Holding Register from 40001 to 49999 - Read-Write

Within each table, each data is addressed 0-9998 or 0000-270E in hex. So addressing an input coil would be 10001 + address, i.e. 10001 + 0, up to 19999 (10001 + 9998). Basically the numbers are 1-indexed on a table but the addresses are 0-indexed within the table. Yes it's weird.

Note: Some MODBUS devices support an extended addressable range 0-65535 but this demo only supports a notation of 0-9998. The table notation could be extended with an additional digit, i.e. 0xxxxx to support the extra range but that is future work.

In MODBUS the the master is expected to know what they are requesting and the meaning of each value returned, e.g. if input register 10001 reports the temperature of a device, then the master is expected to know that because there is no metadata describing it's purpose.

This server is controlled by a configuration file that allows you to define "aliases" to impart meaning onto registers and coils.

Configuration file

The sample reads a modbus.conf which defines its configuration.

The default configuration can be overridden by providing an alternative path via a --config filename option, e.g.

cargo run -- --config ../mymodbus.conf

The configuration defines which coils and registers to read. For example:


---toml
slave_address: "127.0.0.1:502"
read_interval: 1000
output_coils:
    base_address: 0
    count: 0
input_coils:
    base_address: 0
    count: 20
input_registers:
    base_address: 0
    count: 9
output_registers:
    base_address: 0
    count: 0
aliases:
  - name: "Pump #1 Power"
    number: 10001
  - name: "Temperature"
    number: 30001
    data_type: Int32
  • The slave_address which is IP address of the slave device that it will connect to.
  • The read_interval is the duration in milliseconds that values are polled from the slave.
  • Each table has a base_address and a count that describes the range of values that are read or written from that table. If the table has a count of zero it is neither read nor written.
  • Tables may optionally specify an access_mode. If this is set then it affects whether the table will be written or read or both. Input tables can only be ReadOnly. Output tables can be ReadWrite, WriteOnly or ReadOnly. Note that if you disable reading then values will appear to contain the value 0 even if you subsequently write a different value to that register/coil. If you have writable aliases, you will get an error if the table is not also writable.

Aliases

You can also define an alias for a coil or register(s). Aliases appear in a separate folder of the address space under Objects/MODBUS/Aliases.

Each alias consists of a:

  1. name - An alpha numeric name which must be unique from other aliases
  2. number - the number of the register / coil, i.e. 0-9999, 10001-19999, 30001-39999, 40001-49999. The number MUST resolve to a value being captured, i.e. you cannot specify a number which lies outside the base address / count defined for that table.
  3. data_type - optional. For register types ONLY. The type coerces the value in the register(s) to another type. The default type is UInt16.
  4. writable - optional. Indicates the variable is writable. Only works on output tables. The default is false, read-only.

Aliasing will attempt to use bitwise conversions to preserve the original value for some types and casting / coercion for others. Refer to this list to see which applies.

  • Boolean - 1 register. A register with a value of 0 becomes false, otherwise true.
  • Byte - 1 register. Value is clamped 0 to 255, i.e. if the value is > 255, it reports as 255
  • SByte - 1 register bytes treated as a signed 16-bit integer is clamped -127 to 128, i.e. if the value < -127 or > 128 it reports as one of those limits else the real value.
  • UInt16 - 1 register. This is the default register format.
  • Int16 - 1 register. A bitwise conversion of the word, treated as a signed integer.
  • UInt32 - A bitwise conversion of 2 consecutive registers. Affected by endianness.
  • Int32 - A bitwise conversion of 2 consecutive registers. Affected by endianness.
  • UInt64 - A bitwise conversion of 4 consecutive registers. Affected by endianness.
  • Int64 - A bitwise conversion of 4 consecutive registers. Affected by endianness.
  • Float - A bitwise conversion of 2 consecutive registers. Affected by endianness.
  • Double - A bitwise conversion of 4 consecutive registers. Affected by endianness.

If a type uses consecutive registers then the endianness rules are used to resolved the value.

It is an error to alias register numbers, or required consecutive numbers outside of the requested range.

#...
aliases:
  - name: "Pump #1 Power"
    number: 10001
  - name: "Temperature"
    number: 30001
    data_type: Int32
  - name: "Double #1"
    number: 40030
    data_type: Double
    writable: true

If an alias is explicitly marked writable and is in one of the read-write tables, then the value can be changed via OPC UA. Otherwise it is read-only.

Endianness rules

Endianness is a potential head wrecker, so this implementation takes a relatively simple approach:

  1. The MODBUS slave is assumed to return word values big-endian, as per spec.
  2. The MODBUS slave is assumed to return consecutive values for 32-bit or 64-bit values types that are also big-endian, e.g. the number 64-bit number 0x0102030405060708 will be in consecutive register words like so [0x0102],[0x0304],[0x0506],[0x0708].
  3. For 32-bit and 64-bit floating point types, the format is assumed to be consecutive big endian bytes which are bitwise converted to their float equivalents.

In other words, this sample assumes a sane MODBUS slave. It may be that there are broken MODBUS slaves out there which mangle the ordering of words, or double words which require some flipping, but this implementation will not second guess that behaviour for the time being.

Address Space representation

This sample exposes registers / coils into the address space like this.

Objects/
  MODBUS/
    Input Coils
      Input Coil 0
      ...
      Input Coil N - 1
    Input Registers/
      Input Register 0
      ...
      Input Register N - 1
    Aliases/
      Pump #1 Power
      Temperature
      ...

Where Input Register 0 is the first register in the table up to a count of N registers configured when the server was started. Registers are of type UInt16 and coils are of type Boolean.

If the server is configured to reads registers / coils from a non-zero base address, indexing will happen with whatever address was specified, e.g. if the base address for input registers was 1000 then variables will be called Input Register 1000, Input Register 1001 etc.

Any defined aliases are described in the Aliases section as they were set in the configuration file.

Demo MODBUS server

To simplify testing, the demo takes a --run-demo-slave argument. If this flag is given the server will launch its own MODBUS slave on a thread. The demo slave contains some changing and static values to observe the behaviour of the OPC UA.

cd samples/modbus-server
cargo run -- --run-demo-slave

What is going on with Tokio?

This sample uses Tokio 0.1 to interface with the MODBUS library but OPC UA for Rust is internally Tokio 1.x. This may make the sample less than optimal but it should still work.