Modbus

This sections covers htf’s modbus features.

You can find more information in the Modbus documentation.

Warning

This Modbus demo is not usable with the htf-community version. Please contact us for a demo license.

Simulating Modbus-Slave

The Modbus demo to simulate a slave device and to query it using a Modbus client is located in modbus/test_modbus.py.

#
# Copyright (c) 2023, HILSTER - https://hilster.io
# All rights reserved.
#

from typing import Generator

import htf
from htf.modbus import ModbusTCPDevice, ModbusTCPClient

"""
This tests shows how to simulate a modbus device and how to use the client to query it via TCP/IP.
"""

ADDRESS = "localhost"
PORT = 10502  # must be able to run for non-root users


@htf.fixture("session")
def device() -> Generator[ModbusTCPDevice, None, None]:
    """
    Modbus device simulator fixture.
    """
    device = ModbusTCPDevice(ADDRESS, port=PORT)
    yield device
    device.close()


@htf.fixture("test")
def client(
    device: ModbusTCPDevice,
) -> Generator[ModbusTCPClient, None, None]:  # Client requires device to be running already
    """
    Modbus client fixture.
    """
    client = ModbusTCPClient(
        ADDRESS, port=PORT, references_start_at_one=False, auto_disconnect=False, debuglevel=0
    )  # set debuglevel to 1 or 2
    yield client
    client.close()


def test_holding_registers(client: ModbusTCPDevice, device: ModbusTCPDevice) -> None:
    registers = client.read_multiple_holding_registers(0, 8)
    htf.assert_equal(registers, [0] * 8)

    for address in range(10):
        client.write_multiple_holding_registers(address, [0, 1, 0, 1, 1, 0, 1, 0])
        registers = client.read_multiple_holding_registers(address, 8)
        htf.assert_equal(registers, [0, 1, 0, 1, 1, 0, 1, 0])

        device.write_multiple_holding_registers(address, [1, 1, 0, 0, 1, 0, 1, 0])
        registers = client.read_multiple_holding_registers(address, 8)
        htf.assert_equal(registers, [1, 1, 0, 0, 1, 0, 1, 0])


def test_input_registers(client: ModbusTCPDevice, device: ModbusTCPDevice) -> None:
    device.write_multiple_input_registers(0, list(range(10)))

    registers = client.read_multiple_input_registers(0, 10)
    htf.assert_equal(registers, list(range(10)))

    registers = device.read_multiple_input_registers(0, 10)
    htf.assert_equal(registers, list(range(10)))

    device.write_multiple_input_registers(10, list(range(20, 31)))

    registers = client.read_multiple_input_registers(5, 15)
    htf.assert_equal(registers, list(range(5, 10)) + list(range(20, 30)))

    for i in range(10):
        r = client.read_input_register(i)
        htf.assert_equal(r, i)

        r = device.read_input_register(i)
        htf.assert_equal(r, i)

    for i in range(10, 20):
        r = client.read_input_register(i)
        htf.assert_equal(r, i + 10)

        r = device.read_input_register(i)
        htf.assert_equal(r, i + 10)

    device.write_input_register(2323, 1)
    r = client.read_input_register(2323)
    htf.assert_equal(r, 1)


if __name__ == "__main__":
    htf.main()

To execute the demo, run

htf -o modbus/test_modbus.py

Special Modbus Data Structures

Sometimes you’ll need to add special data structures that are not specified by Modbus itself. Using htf makes it easy to do so.

Special data types can be described using oser and can also be read and written directly with the Modbus convenience functions.

The demo for special Modbus data structures is located in modbus/test_modbus_data_structure.py.

#
# Copyright (c) 2023, HILSTER - https://hilster.io
# All rights reserved.
#

from typing import Optional, Generator

import htf
import oser  # type: ignore
from datetime import datetime
from htf.modbus import ModbusTCPDevice, ModbusTCPClient


ADDRESS = "localhost"
PORT = 10502  # must be able to run for non-root users


class DateTime(oser.ByteStruct):
    def __init__(self, value: Optional[datetime] = None) -> None:
        """
        ``DateTime`` data type.

        Args:
            value: a datetime instance to be used
        """
        super(DateTime, self).__init__()

        self.year = oser.UBInt16()
        self.month = oser.UBInt8()
        self.day = oser.UBInt8()

        self.hour = oser.UBInt8()
        self.minute = oser.UBInt8()
        self.second = oser.UBInt8()

        if value is not None:
            self.set(value)

    def set(self, value: datetime) -> None:
        """
        Set values from a ``datetime`` instance.

        Args:
            value: a ``datetime`` instance to be used
        """
        if not isinstance(value, datetime):
            raise ValueError("value must be of type datetime")

        self.year.set(value.year)
        self.month.set(value.month)
        self.day.set(value.day)

        self.hour.set(value.hour)
        self.minute.set(value.minute)
        self.second.set(value.second)

    def get(self) -> datetime:
        """
        Get values as datetime.

        Returns:
            datetime: an instance of ``datetime`` containing the current values
        """
        return datetime(
            year=self.year.get(),
            month=self.month.get(),
            day=self.day.get(),
            hour=self.hour.get(),
            minute=self.minute.get(),
            second=self.second.get(),
        )


@htf.fixture("test")
def device() -> Generator[ModbusTCPDevice, None, None]:
    device = ModbusTCPDevice(ADDRESS, port=PORT)
    yield device
    device.close()


@htf.fixture("test")
def client(device: ModbusTCPDevice) -> Generator[ModbusTCPClient, None, None]:  # require the simulated device running
    client = ModbusTCPClient(
        ADDRESS, port=PORT, references_start_at_one=False, auto_disconnect=False, debuglevel=0
    )  # set debuglevel to 1 or 2
    yield client
    client.close()


"""
This test shows how to read and write own data structures using Modbus.
"""


def test_datetime_data_structure(client: ModbusTCPClient, device: ModbusTCPDevice) -> None:
    """
    A DateTime data type can be read from input registers at address 23.
    """

    now = datetime.now()
    device.write_input_register_data(23, DateTime(now))

    current_date_struct: DateTime = client.read_input_register_data(23, DateTime)  # type: ignore

    htf.assert_equal(current_date_struct.year, now.year)
    htf.assert_equal(current_date_struct.month, now.month)
    htf.assert_equal(current_date_struct.day, now.day)

    htf.assert_equal(current_date_struct.hour, now.hour)
    htf.assert_equal(current_date_struct.minute, now.minute)
    htf.assert_equal(current_date_struct.second, now.second)

    print("device date time is:", current_date_struct.get())


if __name__ == "__main__":
    htf.main()

To execute the demo, run

htf -o modbus/test_modbus_data_structure.py