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