venv added, updated

This commit is contained in:
Norbert
2024-09-13 09:46:28 +02:00
parent 577596d9f3
commit 82af8c809a
4812 changed files with 640223 additions and 2 deletions

View File

@@ -0,0 +1,42 @@
"""Server.
import external classes, to make them easier to use:
"""
__all__ = [
"get_simulator_commandline",
"ModbusSerialServer",
"ModbusSimulatorServer",
"ModbusTcpServer",
"ModbusTlsServer",
"ModbusUdpServer",
"ServerAsyncStop",
"ServerStop",
"StartAsyncSerialServer",
"StartAsyncTcpServer",
"StartAsyncTlsServer",
"StartAsyncUdpServer",
"StartSerialServer",
"StartTcpServer",
"StartTlsServer",
"StartUdpServer",
]
from pymodbus.server.async_io import (
ModbusSerialServer,
ModbusTcpServer,
ModbusTlsServer,
ModbusUdpServer,
ServerAsyncStop,
ServerStop,
StartAsyncSerialServer,
StartAsyncTcpServer,
StartAsyncTlsServer,
StartAsyncUdpServer,
StartSerialServer,
StartTcpServer,
StartTlsServer,
StartUdpServer,
)
from pymodbus.server.simulator.http_server import ModbusSimulatorServer
from pymodbus.server.simulator.main import get_commandline as get_simulator_commandline

View File

@@ -0,0 +1,733 @@
"""Implementation of a Threaded Modbus Server."""
# pylint: disable=missing-type-doc
from __future__ import annotations
import asyncio
import os
import traceback
from contextlib import suppress
from pymodbus.datastore import ModbusServerContext
from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification
from pymodbus.exceptions import NoSuchSlaveException
from pymodbus.factory import ServerDecoder
from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer
from pymodbus.logging import Log
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.transport import CommParams, CommType, ModbusProtocol
# --------------------------------------------------------------------------- #
# Protocol Handlers
# --------------------------------------------------------------------------- #
class ModbusServerRequestHandler(ModbusProtocol):
"""Implements modbus slave wire protocol.
This uses the asyncio.Protocol to implement the server protocol.
When a connection is established, a callback is called.
This callback will setup the connection and
create and schedule an asyncio.Task and assign it to running_task.
"""
def __init__(self, owner):
"""Initialize."""
params = CommParams(
comm_name="server",
comm_type=owner.comm_params.comm_type,
reconnect_delay=0.0,
reconnect_delay_max=0.0,
timeout_connect=0.0,
host=owner.comm_params.source_address[0],
port=owner.comm_params.source_address[1],
)
super().__init__(params, False)
self.server = owner
self.running = False
self.receive_queue: asyncio.Queue = asyncio.Queue()
self.handler_task = None # coroutine to be run on asyncio loop
self.framer: ModbusFramer
self.loop = asyncio.get_running_loop()
def _log_exception(self):
"""Show log exception."""
Log.debug(
"Handler for stream [{}] has been canceled", self.comm_params.comm_name
)
def callback_new_connection(self) -> ModbusProtocol:
"""Call when listener receive new connection request."""
Log.debug("callback_new_connection called")
return ModbusServerRequestHandler(self)
def callback_connected(self) -> None:
"""Call when connection is succcesfull."""
try:
self.running = True
self.framer = self.server.framer(
self.server.decoder,
client=None,
)
# schedule the connection handler on the event loop
self.handler_task = asyncio.create_task(self.handle())
self.handler_task.set_name("server connection handler")
except Exception as exc: # pylint: disable=broad-except
Log.error(
"Server callback_connected exception: {}; {}",
exc,
traceback.format_exc(),
)
def callback_disconnected(self, call_exc: Exception | None) -> None:
"""Call when connection is lost."""
try:
if self.handler_task:
self.handler_task.cancel()
if hasattr(self.server, "on_connection_lost"):
self.server.on_connection_lost()
if call_exc is None:
self._log_exception()
else:
Log.debug(
"Client Disconnection {} due to {}",
self.comm_params.comm_name,
call_exc,
)
self.running = False
except Exception as exc: # pylint: disable=broad-except
Log.error(
"Datastore unable to fulfill request: {}; {}",
exc,
traceback.format_exc(),
)
async def inner_handle(self):
"""Handle handler."""
slaves = self.server.context.slaves()
# this is an asyncio.Queue await, it will never fail
data = await self._recv_()
if isinstance(data, tuple):
# addr is populated when talking over UDP
data, *addr = data
else:
addr = [None]
# if broadcast is enabled make sure to
# process requests to address 0
if self.server.broadcast_enable:
if 0 not in slaves:
slaves.append(0)
Log.debug("Handling data: {}", data, ":hex")
single = self.server.context.single
self.framer.processIncomingPacket(
data=data,
callback=lambda x: self.execute(x, *addr),
slave=slaves,
single=single,
)
async def handle(self) -> None:
"""Coroutine which represents a single master <=> slave conversation.
Once the client connection is established, the data chunks will be
fed to this coroutine via the asyncio.Queue object which is fed by
the ModbusServerRequestHandler class's callback Future.
This callback future gets data from either asyncio.BaseProtocol.data_received
or asyncio.DatagramProtocol.datagram_received.
This function will execute without blocking in the while-loop and
yield to the asyncio event loop when the frame is exhausted.
As a result, multiple clients can be interleaved without any
interference between them.
"""
while self.running:
try:
await self.inner_handle()
except asyncio.CancelledError:
# catch and ignore cancellation errors
if self.running:
self._log_exception()
self.running = False
except Exception as exc: # pylint: disable=broad-except
# force TCP socket termination as processIncomingPacket
# should handle application layer errors
Log.error(
'Unknown exception "{}" on stream {} forcing disconnect',
exc,
self.comm_params.comm_name,
)
self.close()
self.callback_disconnected(exc)
def execute(self, request, *addr):
"""Call with the resulting message.
:param request: The decoded request message
:param addr: the address
"""
if self.server.request_tracer:
self.server.request_tracer(request, *addr)
asyncio.run_coroutine_threadsafe(self._async_execute(request, *addr), self.loop)
async def _async_execute(self, request, *addr):
broadcast = False
try:
if self.server.broadcast_enable and not request.slave_id:
broadcast = True
# if broadcasting then execute on all slave contexts,
# note response will be ignored
for slave_id in self.server.context.slaves():
response = await request.execute(self.server.context[slave_id])
else:
context = self.server.context[request.slave_id]
response = await request.execute(context)
except NoSuchSlaveException:
Log.error("requested slave does not exist: {}", request.slave_id)
if self.server.ignore_missing_slaves:
return # the client will simply timeout waiting for a response
response = request.doException(merror.GatewayNoResponse)
except Exception as exc: # pylint: disable=broad-except
Log.error(
"Datastore unable to fulfill request: {}; {}",
exc,
traceback.format_exc(),
)
response = request.doException(merror.SlaveFailure)
# no response when broadcasting
if not broadcast:
response.transaction_id = request.transaction_id
response.slave_id = request.slave_id
skip_encoding = False
if self.server.response_manipulator:
response, skip_encoding = self.server.response_manipulator(response)
self.server_send(response, *addr, skip_encoding=skip_encoding)
def server_send(self, message, addr, **kwargs):
"""Send message."""
if kwargs.get("skip_encoding", False):
self.send(message, addr=addr)
elif message.should_respond:
pdu = self.framer.buildPacket(message)
self.send(pdu, addr=addr)
else:
Log.debug("Skipping sending response!!")
async def _recv_(self):
"""Receive data from the network."""
try:
result = await self.receive_queue.get()
except RuntimeError:
Log.error("Event loop is closed")
result = None
return result
def callback_data(self, data: bytes, addr: tuple | None = ()) -> int:
"""Handle received data."""
if addr != ():
self.receive_queue.put_nowait((data, addr))
else:
self.receive_queue.put_nowait(data)
return len(data)
# --------------------------------------------------------------------------- #
# Server Implementations
# --------------------------------------------------------------------------- #
class ModbusBaseServer(ModbusProtocol):
"""Common functionality for all server classes."""
def __init__(
self,
params: CommParams,
context,
ignore_missing_slaves,
broadcast_enable,
response_manipulator,
request_tracer,
identity,
framer,
) -> None:
"""Initialize base server."""
super().__init__(
params,
True,
)
self.loop = asyncio.get_running_loop()
self.decoder = ServerDecoder()
self.context = context or ModbusServerContext()
self.control = ModbusControlBlock()
self.ignore_missing_slaves = ignore_missing_slaves
self.broadcast_enable = broadcast_enable
self.response_manipulator = response_manipulator
self.request_tracer = request_tracer
self.handle_local_echo = False
if isinstance(identity, ModbusDeviceIdentification):
self.control.Identity.update(identity)
self.framer = FRAMER_NAME_TO_CLASS.get(framer, framer)
self.serving: asyncio.Future = asyncio.Future()
def callback_new_connection(self):
"""Handle incoming connect."""
return ModbusServerRequestHandler(self)
async def shutdown(self):
"""Close server."""
if not self.serving.done():
self.serving.set_result(True)
self.close()
async def serve_forever(self):
"""Start endless loop."""
if self.transport:
raise RuntimeError(
"Can't call serve_forever on an already running server object"
)
await self.listen()
Log.info("Server listening.")
await self.serving
Log.info("Server graceful shutdown.")
def callback_connected(self) -> None:
"""Call when connection is succcesfull."""
def callback_disconnected(self, exc: Exception | None) -> None:
"""Call when connection is lost."""
Log.debug("callback_disconnected called: {}", exc)
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
"""Handle received data."""
Log.debug("callback_data called: {} addr={}", data, ":hex", addr)
return 0
class ModbusTcpServer(ModbusBaseServer):
"""A modbus threaded tcp socket server.
We inherit and overload the socket server so that we
can control the client threads as well as have a single
server context instance.
"""
def __init__(
self,
context,
framer=FramerType.SOCKET,
identity=None,
address=("", 502),
ignore_missing_slaves=False,
broadcast_enable=False,
response_manipulator=None,
request_tracer=None,
):
"""Initialize the socket server.
If the identify structure is not passed in, the ModbusControlBlock
uses its own empty structure.
:param context: The ModbusServerContext datastore
:param framer: The framer strategy to use
:param identity: An optional identify structure
:param address: An optional (interface, port) to bind to.
:param ignore_missing_slaves: True to not send errors on a request
to a missing slave
:param broadcast_enable: True to treat slave_id 0 as broadcast address,
False to treat 0 as any other slave_id
:param response_manipulator: Callback method for manipulating the
response
:param request_tracer: Callback method for tracing
"""
params = getattr(
self,
"tls_setup",
CommParams(
comm_type=CommType.TCP,
comm_name="server_listener",
reconnect_delay=0.0,
reconnect_delay_max=0.0,
timeout_connect=0.0,
),
)
params.source_address = address
super().__init__(
params,
context,
ignore_missing_slaves,
broadcast_enable,
response_manipulator,
request_tracer,
identity,
framer,
)
class ModbusTlsServer(ModbusTcpServer):
"""A modbus threaded tls socket server.
We inherit and overload the socket server so that we
can control the client threads as well as have a single
server context instance.
"""
def __init__( # pylint: disable=too-many-arguments
self,
context,
framer=FramerType.TLS,
identity=None,
address=("", 502),
sslctx=None,
certfile=None,
keyfile=None,
password=None,
ignore_missing_slaves=False,
broadcast_enable=False,
response_manipulator=None,
request_tracer=None,
):
"""Overloaded initializer for the socket server.
If the identify structure is not passed in, the ModbusControlBlock
uses its own empty structure.
:param context: The ModbusServerContext datastore
:param framer: The framer strategy to use
:param identity: An optional identify structure
:param address: An optional (interface, port) to bind to.
:param sslctx: The SSLContext to use for TLS (default None and auto
create)
:param certfile: The cert file path for TLS (used if sslctx is None)
:param keyfile: The key file path for TLS (used if sslctx is None)
:param password: The password for for decrypting the private key file
:param ignore_missing_slaves: True to not send errors on a request
to a missing slave
:param broadcast_enable: True to treat slave_id 0 as broadcast address,
False to treat 0 as any other slave_id
:param response_manipulator: Callback method for
manipulating the response
"""
self.tls_setup = CommParams(
comm_type=CommType.TLS,
comm_name="server_listener",
reconnect_delay=0.0,
reconnect_delay_max=0.0,
timeout_connect=0.0,
sslctx=CommParams.generate_ssl(
True, certfile, keyfile, password, sslctx=sslctx
),
)
super().__init__(
context,
framer=framer,
identity=identity,
address=address,
ignore_missing_slaves=ignore_missing_slaves,
broadcast_enable=broadcast_enable,
response_manipulator=response_manipulator,
request_tracer=request_tracer,
)
class ModbusUdpServer(ModbusBaseServer):
"""A modbus threaded udp socket server.
We inherit and overload the socket server so that we
can control the client threads as well as have a single
server context instance.
"""
def __init__(
self,
context,
framer=FramerType.SOCKET,
identity=None,
address=("", 502),
ignore_missing_slaves=False,
broadcast_enable=False,
response_manipulator=None,
request_tracer=None,
):
"""Overloaded initializer for the socket server.
If the identify structure is not passed in, the ModbusControlBlock
uses its own empty structure.
:param context: The ModbusServerContext datastore
:param framer: The framer strategy to use
:param identity: An optional identify structure
:param address: An optional (interface, port) to bind to.
:param ignore_missing_slaves: True to not send errors on a request
to a missing slave
:param broadcast_enable: True to treat slave_id 0 as broadcast address,
False to treat 0 as any other slave_id
:param response_manipulator: Callback method for
manipulating the response
:param request_tracer: Callback method for tracing
"""
# ----------------
super().__init__(
CommParams(
comm_type=CommType.UDP,
comm_name="server_listener",
source_address=address,
reconnect_delay=0.0,
reconnect_delay_max=0.0,
timeout_connect=0.0,
),
context,
ignore_missing_slaves,
broadcast_enable,
response_manipulator,
request_tracer,
identity,
framer,
)
class ModbusSerialServer(ModbusBaseServer):
"""A modbus threaded serial socket server.
We inherit and overload the socket server so that we
can control the client threads as well as have a single
server context instance.
"""
def __init__(
self, context, framer=FramerType.RTU, identity=None, **kwargs
):
"""Initialize the socket server.
If the identity structure is not passed in, the ModbusControlBlock
uses its own empty structure.
:param context: The ModbusServerContext datastore
:param framer: The framer strategy to use, default ModbusRtuFramer
:param identity: An optional identify structure
:param port: The serial port to attach to
:param stopbits: The number of stop bits to use
:param bytesize: The bytesize of the serial messages
:param parity: Which kind of parity to use
:param baudrate: The baud rate to use for the serial device
:param timeout: The timeout to use for the serial device
:param handle_local_echo: (optional) Discard local echo from dongle.
:param ignore_missing_slaves: True to not send errors on a request
to a missing slave
:param broadcast_enable: True to treat slave_id 0 as broadcast address,
False to treat 0 as any other slave_id
:param reconnect_delay: reconnect delay in seconds
:param response_manipulator: Callback method for
manipulating the response
:param request_tracer: Callback method for tracing
"""
super().__init__(
params=CommParams(
comm_type=CommType.SERIAL,
comm_name="server_listener",
reconnect_delay=kwargs.get("reconnect_delay", 2),
reconnect_delay_max=0.0,
timeout_connect=kwargs.get("timeout", 3),
source_address=(kwargs.get("port", 0), 0),
bytesize=kwargs.get("bytesize", 8),
parity=kwargs.get("parity", "N"),
baudrate=kwargs.get("baudrate", 19200),
stopbits=kwargs.get("stopbits", 1),
),
context=context,
ignore_missing_slaves=kwargs.get("ignore_missing_slaves", False),
broadcast_enable=kwargs.get("broadcast_enable", False),
response_manipulator=kwargs.get("response_manipulator", None),
request_tracer=kwargs.get("request_tracer", None),
identity=kwargs.get("identity", None),
framer=framer,
)
self.handle_local_echo = kwargs.get("handle_local_echo", False)
# --------------------------------------------------------------------------- #
# Creation Factories
# --------------------------------------------------------------------------- #
class _serverList:
"""Maintains information about the active server.
:meta private:
"""
active_server: ModbusTcpServer | ModbusUdpServer | ModbusSerialServer
def __init__(self, server):
"""Register new server."""
self.server = server
self.loop = asyncio.get_event_loop()
@classmethod
async def run(cls, server, custom_functions) -> None:
"""Help starting/stopping server."""
for func in custom_functions:
server.decoder.register(func)
cls.active_server = _serverList(server) # type: ignore[assignment]
with suppress(asyncio.exceptions.CancelledError):
await server.serve_forever()
@classmethod
async def async_stop(cls) -> None:
"""Wait for server stop."""
if not cls.active_server:
raise RuntimeError("ServerAsyncStop called without server task active.")
await cls.active_server.server.shutdown() # type: ignore[union-attr]
cls.active_server = None # type: ignore[assignment]
@classmethod
def stop(cls):
"""Wait for server stop."""
if not cls.active_server:
Log.info("ServerStop called without server task active.")
return
if not cls.active_server.loop.is_running():
Log.info("ServerStop called with loop stopped.")
return
future = asyncio.run_coroutine_threadsafe(cls.async_stop(), cls.active_server.loop)
future.result(timeout=10 if os.name == 'nt' else 0.1)
async def StartAsyncTcpServer( # pylint: disable=invalid-name,dangerous-default-value
context=None,
identity=None,
address=None,
custom_functions=[],
**kwargs,
):
"""Start and run a tcp modbus server.
:param context: The ModbusServerContext datastore
:param identity: An optional identify structure
:param address: An optional (interface, port) to bind to.
:param custom_functions: An optional list of custom function classes
supported by server instance.
:param kwargs: The rest
"""
kwargs.pop("host", None)
server = ModbusTcpServer(
context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs
)
await _serverList.run(server, custom_functions)
async def StartAsyncTlsServer( # pylint: disable=invalid-name,dangerous-default-value
context=None,
identity=None,
address=None,
sslctx=None,
certfile=None,
keyfile=None,
password=None,
custom_functions=[],
**kwargs,
):
"""Start and run a tls modbus server.
:param context: The ModbusServerContext datastore
:param identity: An optional identify structure
:param address: An optional (interface, port) to bind to.
:param sslctx: The SSLContext to use for TLS (default None and auto create)
:param certfile: The cert file path for TLS (used if sslctx is None)
:param keyfile: The key file path for TLS (used if sslctx is None)
:param password: The password for for decrypting the private key file
:param custom_functions: An optional list of custom function classes
supported by server instance.
:param kwargs: The rest
"""
kwargs.pop("host", None)
server = ModbusTlsServer(
context,
kwargs.pop("framer", FramerType.TLS),
identity,
address,
sslctx,
certfile,
keyfile,
password,
**kwargs,
)
await _serverList.run(server, custom_functions)
async def StartAsyncUdpServer( # pylint: disable=invalid-name,dangerous-default-value
context=None,
identity=None,
address=None,
custom_functions=[],
**kwargs,
):
"""Start and run a udp modbus server.
:param context: The ModbusServerContext datastore
:param identity: An optional identify structure
:param address: An optional (interface, port) to bind to.
:param custom_functions: An optional list of custom function classes
supported by server instance.
:param kwargs:
"""
kwargs.pop("host", None)
server = ModbusUdpServer(
context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs
)
await _serverList.run(server, custom_functions)
async def StartAsyncSerialServer( # pylint: disable=invalid-name,dangerous-default-value
context=None,
identity=None,
custom_functions=[],
**kwargs,
):
"""Start and run a serial modbus server.
:param context: The ModbusServerContext datastore
:param identity: An optional identify structure
:param custom_functions: An optional list of custom function classes
supported by server instance.
:param kwargs: The rest
"""
server = ModbusSerialServer(
context, kwargs.pop("framer", FramerType.RTU), identity=identity, **kwargs
)
await _serverList.run(server, custom_functions)
def StartSerialServer(**kwargs): # pylint: disable=invalid-name
"""Start and run a serial modbus server."""
return asyncio.run(StartAsyncSerialServer(**kwargs))
def StartTcpServer(**kwargs): # pylint: disable=invalid-name
"""Start and run a serial modbus server."""
return asyncio.run(StartAsyncTcpServer(**kwargs))
def StartTlsServer(**kwargs): # pylint: disable=invalid-name
"""Start and run a serial modbus server."""
return asyncio.run(StartAsyncTlsServer(**kwargs))
def StartUdpServer(**kwargs): # pylint: disable=invalid-name
"""Start and run a serial modbus server."""
return asyncio.run(StartAsyncUdpServer(**kwargs))
async def ServerAsyncStop(): # pylint: disable=invalid-name
"""Terminate server."""
await _serverList.async_stop()
def ServerStop(): # pylint: disable=invalid-name
"""Terminate server."""
_serverList.stop()

View File

@@ -0,0 +1 @@
"""Initialize."""

View File

@@ -0,0 +1,10 @@
"""Datastore simulator, custom actions."""
def device_reset(_registers, _inx, _cell):
"""Use example custom action."""
custom_actions_dict = {
"umg804_reset": device_reset,
}

View File

@@ -0,0 +1,810 @@
"""HTTP server for modbus simulator."""
from __future__ import annotations
import asyncio
import contextlib
import dataclasses
import importlib
import json
import os
from time import sleep
from typing import TYPE_CHECKING
try:
from aiohttp import web
AIOHTTP_MISSING = False
except ImportError:
AIOHTTP_MISSING = True
if TYPE_CHECKING: # always False at runtime
# type checkers do not understand the Raise RuntimeError in __init__()
from aiohttp import web
from pymodbus.datastore import ModbusServerContext, ModbusSimulatorContext
from pymodbus.datastore.simulator import Label
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.factory import ServerDecoder
from pymodbus.logging import Log
from pymodbus.pdu import ExceptionResponse
from pymodbus.server.async_io import (
ModbusSerialServer,
ModbusTcpServer,
ModbusTlsServer,
ModbusUdpServer,
)
MAX_FILTER = 1000
RESPONSE_INACTIVE = -1
RESPONSE_NORMAL = 0
RESPONSE_ERROR = 1
RESPONSE_EMPTY = 2
RESPONSE_JUNK = 3
@dataclasses.dataclass()
class CallTracer:
"""Define call/response traces."""
call: bool = False
fc: int = -1
address: int = -1
count: int = -1
data: bytes = b""
@dataclasses.dataclass()
class CallTypeMonitor:
"""Define Request/Response monitor."""
active: bool = False
trace_response: bool = False
range_start: int = -1
range_stop: int = -1
function: int = -1
hex: bool = False
decode: bool = False
@dataclasses.dataclass()
class CallTypeResponse:
"""Define Response manipulation."""
active: int = RESPONSE_INACTIVE
split: int = 0
delay: int = 0
junk_len: int = 10
error_response: int = 0
change_rate: int = 0
clear_after: int = 1
class ModbusSimulatorServer:
"""**ModbusSimulatorServer**.
:param modbus_server: Server name in json file (default: "server")
:param modbus_device: Device name in json file (default: "client")
:param http_host: TCP host for HTTP (default: "localhost")
:param http_port: TCP port for HTTP (default: 8080)
:param json_file: setup file (default: "setup.json")
:param custom_actions_module: python module with custom actions (default: none)
if either http_port or http_host is none, HTTP will not be started.
This class starts a http server, that serves a couple of endpoints:
- **"<addr>/"** static files
- **"<addr>/api/log"** log handling, HTML with GET, REST-API with post
- **"<addr>/api/registers"** register handling, HTML with GET, REST-API with post
- **"<addr>/api/calls"** call (function code / message) handling, HTML with GET, REST-API with post
- **"<addr>/api/server"** server handling, HTML with GET, REST-API with post
Example::
from pymodbus.server import ModbusSimulatorServer
async def run():
simulator = ModbusSimulatorServer(
modbus_server="my server",
modbus_device="my device",
http_host="localhost",
http_port=8080)
await simulator.run_forever(only_start=True)
...
await simulator.stop()
"""
def __init__(
self,
modbus_server: str = "server",
modbus_device: str = "device",
http_host: str = "0.0.0.0",
http_port: int = 8080,
log_file: str = "server.log",
json_file: str = "setup.json",
custom_actions_module: str | None = None,
):
"""Initialize http interface."""
if AIOHTTP_MISSING:
raise RuntimeError(
"Simulator server requires aiohttp. "
'Please install with "pip install aiohttp" and try again.'
)
with open(json_file, encoding="utf-8") as file:
setup = json.load(file)
comm_class = {
"serial": ModbusSerialServer,
"tcp": ModbusTcpServer,
"tls": ModbusTlsServer,
"udp": ModbusUdpServer,
}
if custom_actions_module:
actions_module = importlib.import_module(custom_actions_module)
custom_actions_dict = actions_module.custom_actions_dict
else:
custom_actions_dict = {}
server = setup["server_list"][modbus_server]
if server["comm"] != "serial":
server["address"] = (server["host"], server["port"])
del server["host"]
del server["port"]
device = setup["device_list"][modbus_device]
self.datastore_context = ModbusSimulatorContext(
device, custom_actions_dict or {}
)
datastore = None
if "device_id" in server:
# Designated ModBus unit address. Will only serve data if the address matches
datastore = ModbusServerContext(slaves={int(server["device_id"]): self.datastore_context}, single=False)
else:
# Will server any request regardless of addressing
datastore = ModbusServerContext(slaves=self.datastore_context, single=True)
comm = comm_class[server.pop("comm")]
framer = server.pop("framer")
if "identity" in server:
server["identity"] = ModbusDeviceIdentification(
info_name=server["identity"]
)
self.modbus_server = comm(framer=framer, context=datastore, **server)
self.serving: asyncio.Future = asyncio.Future()
self.log_file = log_file
self.site: web.TCPSite | None = None
self.runner: web.AppRunner
self.http_host = http_host
self.http_port = http_port
self.web_path = os.path.join(os.path.dirname(__file__), "web")
self.web_app = web.Application()
self.web_app.add_routes(
[
web.get("/api/{tail:[a-z]*}", self.handle_html),
web.post("/restapi/{tail:[a-z]*}", self.handle_json),
web.get("/{tail:[a-z0-9.]*}", self.handle_html_static),
web.get("/", self.handle_html_static),
]
)
self.web_app.on_startup.append(self.start_modbus_server)
self.web_app.on_shutdown.append(self.stop_modbus_server)
self.generator_html: dict[str, list] = {
"log": ["", self.build_html_log],
"registers": ["", self.build_html_registers],
"calls": ["", self.build_html_calls],
"server": ["", self.build_html_server],
}
self.generator_json = {
"log": self.build_json_log,
"registers": self.build_json_registers,
"calls": self.build_json_calls,
"server": self.build_json_server,
}
self.submit_html = {
"Clear": self.action_clear,
"Stop": self.action_stop,
"Reset": self.action_reset,
"Add": self.action_add,
"Monitor": self.action_monitor,
"Set": self.action_set,
"Simulate": self.action_simulate,
}
for entry in self.generator_html: # pylint: disable=consider-using-dict-items
html_file = os.path.join(self.web_path, "generator", entry)
with open(html_file, encoding="utf-8") as handle:
self.generator_html[entry][0] = handle.read()
self.refresh_rate = 0
self.register_filter: list[int] = []
self.call_list: list[CallTracer] = []
self.request_lookup = ServerDecoder.getFCdict()
self.call_monitor = CallTypeMonitor()
self.call_response = CallTypeResponse()
app_key = getattr(web, 'AppKey', str) # fall back to str for aiohttp < 3.9.0
self.api_key = app_key("modbus_server")
async def start_modbus_server(self, app):
"""Start Modbus server as asyncio task."""
try:
if getattr(self.modbus_server, "start", None):
await self.modbus_server.start()
app[self.api_key] = asyncio.create_task(
self.modbus_server.serve_forever()
)
app[self.api_key].set_name("simulator modbus server")
except Exception as exc:
Log.error("Error starting modbus server, reason: {}", exc)
raise exc
Log.info(
"Modbus server started on {}", self.modbus_server.comm_params.source_address
)
async def stop_modbus_server(self, app):
"""Stop modbus server."""
Log.info("Stopping modbus server")
await self.modbus_server.shutdown()
app[self.api_key].cancel()
with contextlib.suppress(asyncio.exceptions.CancelledError):
await app[self.api_key]
Log.info("Modbus server Stopped")
async def run_forever(self, only_start=False):
"""Start modbus and http servers."""
try:
self.runner = web.AppRunner(self.web_app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.http_host, self.http_port)
await self.site.start()
except Exception as exc:
Log.error("Error starting http server, reason: {}", exc)
raise exc
Log.info("HTTP server started on ({}:{})", self.http_host, self.http_port)
if only_start:
return
await self.serving
async def stop(self):
"""Stop modbus and http servers."""
await self.runner.cleanup()
self.site = None
if not self.serving.done():
self.serving.set_result(True)
await asyncio.sleep(0)
async def handle_html_static(self, request):
"""Handle static html."""
if not (page := request.path[1:]):
page = "index.html"
file = os.path.normpath(os.path.join(self.web_path, page))
if not file.startswith(self.web_path):
raise ValueError(f"File access outside {self.web_path} not permitted.")
try:
with open(file, encoding="utf-8"):
return web.FileResponse(file)
except (FileNotFoundError, IsADirectoryError) as exc:
raise web.HTTPNotFound(reason="File not found") from exc
async def handle_html(self, request):
"""Handle html."""
page_type = request.path.split("/")[-1]
params = dict(request.query)
if refresh := params.pop("refresh", None):
self.refresh_rate = int(refresh)
if self.refresh_rate > 0:
html = self.generator_html[page_type][0].replace(
"<!--REFRESH-->",
f'<meta http-equiv="refresh" content="{self.refresh_rate}">',
)
else:
html = self.generator_html[page_type][0].replace("<!--REFRESH-->", "")
new_page = self.generator_html[page_type][1](params, html)
return web.Response(text=new_page, content_type="text/html")
async def handle_json(self, request):
"""Handle api registers."""
command = request.path.split("/")[-1]
params = await request.json()
try:
result = self.generator_json[command](params)
except (KeyError, ValueError, TypeError, IndexError) as exc:
Log.error("Unhandled error during json request: {}", exc)
return web.json_response({"result": "error", "error": f"Unhandled error Error: {exc}"})
return web.json_response(result)
def build_html_registers(self, params, html):
"""Build html registers page."""
result_txt, foot = self.helper_handle_submit(params, self.submit_html)
if not result_txt:
result_txt = "ok"
if not foot:
if self.register_filter:
foot = f"{len(self.register_filter)} register(s) monitored"
else:
foot = "Nothing selected"
register_types = "".join(
f"<option value={reg_id}>{name}</option>"
for name, reg_id in self.datastore_context.registerType_name_to_id.items()
)
register_actions = "".join(
f"<option value={action_id}>{name}</option>"
for name, action_id in self.datastore_context.action_name_to_id.items()
)
rows = ""
for i in self.register_filter:
inx, reg = self.datastore_context.get_text_register(i)
if reg.type == Label.next:
continue
row = "".join(
f"<td>{entry}</td>"
for entry in (
inx,
reg.type,
reg.access,
reg.action,
reg.value,
reg.count_read,
reg.count_write,
)
)
rows += f"<tr>{row}</tr>"
new_html = (
html.replace("<!--REGISTER_ACTIONS-->", register_actions)
.replace("<!--REGISTER_TYPES-->", register_types)
.replace("<!--REGISTER_FOOT-->", foot)
.replace("<!--REGISTER_ROWS-->", rows)
.replace("<!--RESULT-->", result_txt)
)
return new_html
def build_html_calls(self, params: dict, html: str) -> str:
"""Build html calls page."""
result_txt, foot = self.helper_handle_submit(params, self.submit_html)
if not foot:
foot = "Montitoring active" if self.call_monitor.active else "not active"
if not result_txt:
result_txt = "ok"
function_error = ""
for i, txt in (
(1, "IllegalFunction"),
(2, "IllegalAddress"),
(3, "IllegalValue"),
(4, "SlaveFailure"),
(5, "Acknowledge"),
(6, "SlaveBusy"),
(7, "MemoryParityError"),
(10, "GatewayPathUnavailable"),
(11, "GatewayNoResponse"),
):
selected = "selected" if i == self.call_response.error_response else ""
function_error += f"<option value={i} {selected}>{txt}</option>"
range_start_html = (
str(self.call_monitor.range_start)
if self.call_monitor.range_start != -1
else ""
)
range_stop_html = (
str(self.call_monitor.range_stop)
if self.call_monitor.range_stop != -1
else ""
)
function_codes = ""
for function in self.request_lookup.values():
selected = (
"selected"
if function.function_code == self.call_monitor.function #type: ignore[attr-defined]
else ""
)
function_codes += f"<option value={function.function_code} {selected}>{function.function_code_name}</option>" #type: ignore[attr-defined]
simulation_action = (
"ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else ""
)
max_len = MAX_FILTER if self.call_monitor.active else 0
while len(self.call_list) > max_len:
del self.call_list[0]
call_rows = ""
for entry in reversed(self.call_list):
# req_obj = self.request_lookup[entry[1]]
call_rows += f"<tr><td>{entry.call} - {entry.fc}</td><td>{entry.address}</td><td>{entry.count}</td><td>{entry.data.decode()}</td></tr>"
# line += req_obj.funcion_code_name
new_html = (
html.replace("<!--SIMULATION_ACTIVE-->", simulation_action)
.replace("FUNCTION_RANGE_START", range_start_html)
.replace("FUNCTION_RANGE_STOP", range_stop_html)
.replace("<!--FUNCTION_CODES-->", function_codes)
.replace(
"FUNCTION_SHOW_HEX_CHECKED", "checked" if self.call_monitor.hex else ""
)
.replace(
"FUNCTION_SHOW_DECODED_CHECKED",
"checked" if self.call_monitor.decode else "",
)
.replace(
"FUNCTION_RESPONSE_NORMAL_CHECKED",
"checked" if self.call_response.active == RESPONSE_NORMAL else "",
)
.replace(
"FUNCTION_RESPONSE_ERROR_CHECKED",
"checked" if self.call_response.active == RESPONSE_ERROR else "",
)
.replace(
"FUNCTION_RESPONSE_EMPTY_CHECKED",
"checked" if self.call_response.active == RESPONSE_EMPTY else "",
)
.replace(
"FUNCTION_RESPONSE_JUNK_CHECKED",
"checked" if self.call_response.active == RESPONSE_JUNK else "",
)
.replace(
"FUNCTION_RESPONSE_SPLIT_CHECKED",
"checked" if self.call_response.split > 0 else "",
)
.replace("FUNCTION_RESPONSE_SPLIT_DELAY", str(self.call_response.split))
.replace(
"FUNCTION_RESPONSE_CR_CHECKED",
"checked" if self.call_response.change_rate > 0 else "",
)
.replace("FUNCTION_RESPONSE_CR_PCT", str(self.call_response.change_rate))
.replace("FUNCTION_RESPONSE_DELAY", str(self.call_response.delay))
.replace("FUNCTION_RESPONSE_JUNK", str(self.call_response.junk_len))
.replace("<!--FUNCTION_ERROR-->", function_error)
.replace(
"FUNCTION_RESPONSE_CLEAR_AFTER", str(self.call_response.clear_after)
)
.replace("<!--FC_ROWS-->", call_rows)
.replace("<!--FC_FOOT-->", foot)
)
return new_html
def build_html_log(self, _params, html):
"""Build html log page."""
return html
def build_html_server(self, _params, html):
"""Build html server page."""
return html
def build_json_registers(self, params):
"""Build json registers response."""
# Process params using the helper function
result_txt, foot = self.helper_handle_submit(params, {
"Set": self.action_set,
})
if not result_txt:
result_txt = "ok"
if not foot:
foot = "Operation completed successfully"
# Extract necessary parameters
try:
range_start = int(params.get("range_start", 0))
range_stop = int(params.get("range_stop", range_start))
except ValueError:
return {"result": "error", "error": "Invalid range parameters"}
# Retrieve register details
register_rows = []
for i in range(range_start, range_stop + 1):
inx, reg = self.datastore_context.get_text_register(i)
row = {
"index": inx,
"type": reg.type,
"access": reg.access,
"action": reg.action,
"value": reg.value,
"count_read": reg.count_read,
"count_write": reg.count_write
}
register_rows.append(row)
# Generate register types and actions (assume these are predefined mappings)
register_types = dict(self.datastore_context.registerType_name_to_id)
register_actions = dict(self.datastore_context.action_name_to_id)
# Build the JSON response
json_response = {
"result": result_txt,
"footer": foot,
"register_types": register_types,
"register_actions": register_actions,
"register_rows": register_rows,
}
return json_response
def build_json_calls(self, params: dict) -> dict:
"""Build json calls response."""
result_txt, foot = self.helper_handle_submit(params, {
"Reset": self.action_reset,
"Add": self.action_add,
"Simulate": self.action_simulate,
})
if not foot:
foot = "Monitoring active" if self.call_monitor.active else "not active"
if not result_txt:
result_txt = "ok"
function_error = []
for i, txt in (
(1, "IllegalFunction"),
(2, "IllegalAddress"),
(3, "IllegalValue"),
(4, "SlaveFailure"),
(5, "Acknowledge"),
(6, "SlaveBusy"),
(7, "MemoryParityError"),
(10, "GatewayPathUnavailable"),
(11, "GatewayNoResponse"),
):
function_error.append({
"value": i,
"text": txt,
"selected": i == self.call_response.error_response
})
range_start = (
self.call_monitor.range_start
if self.call_monitor.range_start != -1
else None
)
range_stop = (
self.call_monitor.range_stop
if self.call_monitor.range_stop != -1
else None
)
function_codes = []
for function in self.request_lookup.values():
function_codes.append({
"value": function.function_code, # type: ignore[attr-defined]
"text": function.function_code_name, # type: ignore[attr-defined]
"selected": function.function_code == self.call_monitor.function # type: ignore[attr-defined]
})
simulation_action = "ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else ""
max_len = MAX_FILTER if self.call_monitor.active else 0
while len(self.call_list) > max_len:
del self.call_list[0]
call_rows = []
for entry in reversed(self.call_list):
call_rows.append({
"call": entry.call,
"fc": entry.fc,
"address": entry.address,
"count": entry.count,
"data": entry.data.decode()
})
json_response = {
"simulation_action": simulation_action,
"range_start": range_start,
"range_stop": range_stop,
"function_codes": function_codes,
"function_show_hex_checked": self.call_monitor.hex,
"function_show_decoded_checked": self.call_monitor.decode,
"function_response_normal_checked": self.call_response.active == RESPONSE_NORMAL,
"function_response_error_checked": self.call_response.active == RESPONSE_ERROR,
"function_response_empty_checked": self.call_response.active == RESPONSE_EMPTY,
"function_response_junk_checked": self.call_response.active == RESPONSE_JUNK,
"function_response_split_checked": self.call_response.split > 0,
"function_response_split_delay": self.call_response.split,
"function_response_cr_checked": self.call_response.change_rate > 0,
"function_response_cr_pct": self.call_response.change_rate,
"function_response_delay": self.call_response.delay,
"function_response_junk": self.call_response.junk_len,
"function_error": function_error,
"function_response_clear_after": self.call_response.clear_after,
"call_rows": call_rows,
"foot": foot,
"result": result_txt
}
return json_response
def build_json_log(self, params):
"""Build json log page."""
return {"result": "error", "error": "log endpoint not implemented", "params": params}
def build_json_server(self, params):
"""Build html server page."""
return {"result": "error", "error": "server endpoint not implemented", "params": params}
def helper_handle_submit(self, params, submit_actions):
"""Build html register submit."""
try:
range_start = int(params.get("range_start", -1))
except ValueError:
range_start = -1
try:
range_stop = int(params.get("range_stop", range_start))
except ValueError:
range_stop = -1
if (submit := params["submit"]) not in submit_actions:
return None, None
return submit_actions[submit](params, range_start, range_stop)
def action_clear(self, _params, _range_start, _range_stop):
"""Clear register filter."""
self.register_filter = []
return None, None
def action_stop(self, _params, _range_start, _range_stop):
"""Stop call monitoring."""
self.call_monitor = CallTypeMonitor()
self.modbus_server.response_manipulator = None
self.modbus_server.request_tracer = None
return None, "Stopped monitoring"
def action_reset(self, _params, _range_start, _range_stop):
"""Reset call simulation."""
self.call_response = CallTypeResponse()
if not self.call_monitor.active:
self.modbus_server.response_manipulator = self.server_response_manipulator
return None, None
def action_add(self, params, range_start, range_stop):
"""Build list of registers matching filter."""
reg_action = int(params.get("action", -1))
reg_writeable = "writeable" in params
reg_type = int(params.get("type", -1))
filter_updated = 0
if range_start != -1:
steps = range(range_start, range_stop + 1)
else:
steps = range(1, self.datastore_context.register_count)
for i in steps:
if range_start != -1 and (i < range_start or i > range_stop):
continue
reg = self.datastore_context.registers[i]
skip_filter = reg_writeable and not reg.access
skip_filter |= reg_type not in (-1, reg.type)
skip_filter |= reg_action not in (-1, reg.action)
skip_filter |= i in self.register_filter
if skip_filter:
continue
self.register_filter.append(i)
filter_updated += 1
if len(self.register_filter) >= MAX_FILTER:
self.register_filter.sort()
return None, f"Max. filter size {MAX_FILTER} exceeded!"
self.register_filter.sort()
return None, None
def action_monitor(self, params, range_start, range_stop):
"""Start monitoring calls."""
self.call_monitor.range_start = range_start
self.call_monitor.range_stop = range_stop
self.call_monitor.function = (
int(params["function"]) if params["function"] else -1
)
self.call_monitor.hex = "show_hex" in params
self.call_monitor.decode = "show_decode" in params
self.call_monitor.active = True
self.modbus_server.response_manipulator = self.server_response_manipulator
self.modbus_server.request_tracer = self.server_request_tracer
return None, None
def action_set(self, params, _range_start, _range_stop):
"""Set register value."""
if not (register := params["register"]):
return "Missing register", None
register = int(register)
if value := params["value"]:
self.datastore_context.registers[register].value = int(value)
if bool(params.get("writeable", False)):
self.datastore_context.registers[register].access = True
return None, None
def action_simulate(self, params, _range_start, _range_stop):
"""Simulate responses."""
self.call_response.active = int(params["response_type"])
if "response_split" in params:
if params["split_delay"]:
self.call_response.split = int(params["split_delay"])
else:
self.call_response.split = 1
else:
self.call_response.split = 0
if "response_cr" in params:
if params["response_cr_pct"]:
self.call_response.change_rate = int(params["response_cr_pct"])
else:
self.call_response.change_rate = 0
else:
self.call_response.change_rate = 0
if params["response_delay"]:
self.call_response.delay = int(params["response_delay"])
else:
self.call_response.delay = 0
if params["response_junk_datalen"]:
self.call_response.junk_len = int(params["response_junk_datalen"])
else:
self.call_response.junk_len = 0
self.call_response.error_response = int(params["response_error"])
if params["response_clear_after"]:
self.call_response.clear_after = int(params["response_clear_after"])
else:
self.call_response.clear_after = 1
self.modbus_server.response_manipulator = self.server_response_manipulator
return None, None
def server_response_manipulator(self, response):
"""Manipulate responses.
All server responses passes this filter before being sent.
The filter returns:
- response, either original or modified
- skip_encoding, signals whether or not to encode the response
"""
if self.call_monitor.trace_response:
tracer = CallTracer(
call=False,
fc=response.function_code,
address=response.address if hasattr(response, "address") else -1,
count=response.count if hasattr(response, "count") else -1,
data=b"-",
)
self.call_list.append(tracer)
self.call_monitor.trace_response = False
if self.call_response.active != RESPONSE_INACTIVE:
return response, False
skip_encoding = False
if self.call_response.active == RESPONSE_EMPTY:
Log.warning("Sending empty response")
response.should_respond = False
elif self.call_response.active == RESPONSE_NORMAL:
if self.call_response.delay:
Log.warning(
"Delaying response by {}s for all incoming requests",
self.call_response.delay,
)
sleep(self.call_response.delay) # change to async
else:
pass
# self.call_response.change_rate
# self.call_response.split
elif self.call_response.active == RESPONSE_ERROR:
Log.warning("Sending error response for all incoming requests")
err_response = ExceptionResponse(
response.function_code, self.call_response.error_response
)
err_response.transaction_id = response.transaction_id
err_response.slave_id = response.slave_id
elif self.call_response.active == RESPONSE_JUNK:
response = os.urandom(self.call_response.junk_len)
skip_encoding = True
self.call_response.clear_after -= 1
if self.call_response.clear_after <= 0:
Log.info("Resetting manipulator due to clear_after")
self.call_response.active = RESPONSE_EMPTY
return response, skip_encoding
def server_request_tracer(self, request, *_addr):
"""Trace requests.
All server requests passes this filter before being handled.
"""
if self.call_monitor.function not in {-1, request.function_code}:
return
address = request.address if hasattr(request, "address") else -1
if self.call_monitor.range_start != -1 and address != -1:
if (
self.call_monitor.range_start > address
or self.call_monitor.range_stop < address
):
return
tracer = CallTracer(
call=True,
fc=request.function_code,
address=address,
count=request.count if hasattr(request, "count") else -1,
data=b"-",
)
self.call_list.append(tracer)
self.call_monitor.trace_response = True

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""HTTP server for modbus simulator.
The modbus simulator contain 3 distinct parts:
- Datastore simulator, to define registers and their behaviour including actions: (simulator)(../../datastore/simulator.py)
- Modbus server: (server)(./http_server.py)
- HTTP server with REST API and web pages providing an online console in your browser
Multiple setups for different server types and/or devices are prepared in a (json file)(./setup.json), the detailed configuration is explained in (doc)(README.rst)
The command line parameters are kept to a minimum:
usage: main.py [-h] [--modbus_server MODBUS_SERVER]
[--modbus_device MODBUS_DEVICE] [--http_host HTTP_HOST]
[--http_port HTTP_PORT]
[--log {critical,error,warning,info,debug}]
[--json_file JSON_FILE]
[--custom_actions_module CUSTOM_ACTIONS_MODULE]
Modbus server with REST-API and web server
options:
-h, --help show this help message and exit
--modbus_server MODBUS_SERVER
use <modbus_server> from server_list in json file
--modbus_device MODBUS_DEVICE
use <modbus_device> from device_list in json file
--http_host HTTP_HOST
use <http_host> as host to bind http listen
--http_port HTTP_PORT
use <http_port> as port to bind http listen
--log {critical,error,warning,info,debug}
set log level, default is info
--log_file LOG_FILE
name of server log file, default is "server.log"
--json_file JSON_FILE
name of json_file, default is "setup.json"
--custom_actions_module CUSTOM_ACTIONS_MODULE
python file with custom actions, default is none
"""
import argparse
import asyncio
import os
from pymodbus import pymodbus_apply_logging_config
from pymodbus.logging import Log
from pymodbus.server.simulator.http_server import ModbusSimulatorServer
def get_commandline(extras=None, cmdline=None):
"""Get command line arguments."""
parser = argparse.ArgumentParser(
description="Modbus server with REST-API and web server"
)
parser.add_argument(
"--modbus_server",
help="use <modbus_server> from server_list in json file",
type=str,
)
parser.add_argument(
"--modbus_device",
help="use <modbus_device> from device_list in json file",
type=str,
)
parser.add_argument(
"--http_host",
help="use <http_host> as host to bind http listen",
type=str,
)
parser.add_argument(
"--http_port",
help="use <http_port> as port to bind http listen",
type=str,
default=8081,
)
parser.add_argument(
"--log",
choices=["critical", "error", "warning", "info", "debug"],
help="set log level, default is info",
default="info",
type=str,
)
parser.add_argument(
"--json_file",
help='name of json file, default is "setup.json"',
type=str,
default=os.path.join(os.path.dirname(__file__), "setup.json"),
)
parser.add_argument(
"--log_file",
help='name of server log file, default is "server.log"',
type=str,
)
parser.add_argument(
"--custom_actions_module",
help="python file with custom actions, default is none",
type=str,
)
if extras:
for extra in extras:
parser.add_argument(extra[0], **extra[1])
args = parser.parse_args(cmdline)
pymodbus_apply_logging_config(args.log.upper())
Log.info("Start simulator")
cmd_args = {}
for argument in args.__dict__:
if argument == "log":
continue
if args.__dict__[argument] is not None:
cmd_args[argument] = args.__dict__[argument]
return cmd_args
async def run_main():
"""Run server async."""
cmd_args = get_commandline()
task = ModbusSimulatorServer(**cmd_args)
await task.run_forever()
def main():
"""Run server."""
asyncio.run(run_main(), debug=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,228 @@
{
"server_list": {
"server": {
"comm": "tcp",
"host": "0.0.0.0",
"port": 5020,
"ignore_missing_slaves": false,
"framer": "socket",
"identity": {
"VendorName": "pymodbus",
"ProductCode": "PM",
"VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
"ProductName": "pymodbus Server",
"ModelName": "pymodbus Server",
"MajorMinorRevision": "3.1.0"
}
},
"server_try_serial": {
"comm": "serial",
"port": "/dev/tty0",
"stopbits": 1,
"bytesize": 8,
"parity": "N",
"baudrate": 9600,
"timeout": 3,
"reconnect_delay": 2,
"framer": "rtu",
"identity": {
"VendorName": "pymodbus",
"ProductCode": "PM",
"VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
"ProductName": "pymodbus Server",
"ModelName": "pymodbus Server",
"MajorMinorRevision": "3.1.0"
}
},
"server_try_tls": {
"comm": "tls",
"host": "0.0.0.0",
"port": 5020,
"certfile": "certificates/pymodbus.crt",
"keyfile": "certificates/pymodbus.key",
"ignore_missing_slaves": false,
"framer": "tls",
"identity": {
"VendorName": "pymodbus",
"ProductCode": "PM",
"VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
"ProductName": "pymodbus Server",
"ModelName": "pymodbus Server",
"MajorMinorRevision": "3.1.0"
}
},
"server_test_try_udp": {
"comm": "udp",
"host": "0.0.0.0",
"port": 5020,
"ignore_missing_slaves": false,
"framer": "socket",
"identity": {
"VendorName": "pymodbus",
"ProductCode": "PM",
"VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
"ProductName": "pymodbus Server",
"ModelName": "pymodbus Server",
"MajorMinorRevision": "3.1.0"
}
}
},
"device_list": {
"device": {
"setup": {
"co size": 63000,
"di size": 63000,
"hr size": 63000,
"ir size": 63000,
"shared blocks": true,
"type exception": true,
"defaults": {
"value": {
"bits": 0,
"uint16": 0,
"uint32": 0,
"float32": 0.0,
"string": " "
},
"action": {
"bits": null,
"uint16": "increment",
"uint32": "increment",
"float32": "increment",
"string": null
}
}
},
"invalid": [
1
],
"write": [
3
],
"bits": [
{"addr": 2, "value": 7}
],
"uint16": [
{"addr": 3, "value": 17001, "action": null},
2100
],
"uint32": [
{"addr": [4, 5], "value": 617001, "action": null},
[3037, 3038]
],
"float32": [
{"addr": [6, 7], "value": 404.17},
[4100, 4101]
],
"string": [
5047,
{"addr": [16, 20], "value": "A_B_C_D_E_"}
],
"repeat": [
]
},
"device_try": {
"setup": {
"co size": 63000,
"di size": 63000,
"hr size": 63000,
"ir size": 63000,
"shared blocks": true,
"type exception": true,
"defaults": {
"value": {
"bits": 0,
"uint16": 0,
"uint32": 0,
"float32": 0.0,
"string": " "
},
"action": {
"bits": null,
"uint16": null,
"uint32": null,
"float32": null,
"string": null
}
}
},
"invalid": [
[0, 5],
77
],
"write": [
10
],
"bits": [
10,
1009,
[1116, 1119],
{"addr": 1144, "value": 1},
{"addr": [1148,1149], "value": 32117},
{"addr": [1208, 1306], "action": "random"}
],
"uint16": [
11,
2027,
[2126, 2129],
{"addr": 2164, "value": 1},
{"addr": [2168,2169], "value": 32117},
{"addr": [2208, 2304], "action": "increment"},
{"addr": 2305,
"value": 50,
"action": "increment",
"parameters": {"minval": 45, "maxval": 155}
},
{"addr": 2306,
"value": 50,
"action": "random",
"parameters": {"minval": 45, "maxval": 55}
}
],
"uint32": [
[12, 13],
[3037, 3038],
[3136, 3139],
{"addr": [3174, 3175], "value": 1},
{"addr": [3188,3189], "value": 32514},
{"addr": [3308, 3407], "action": null},
{"addr": [3688, 3875], "value": 115, "action": "increment"},
{"addr": [3876, 3877],
"value": 50000,
"action": "increment",
"parameters": {"minval": 45000, "maxval": 55000}
},
{"addr": [3878, 3879],
"value": 50000,
"action": "random",
"parameters": {"minval": 45000, "maxval": 55000}
}
],
"float32": [
[14, 15],
[4047, 4048],
[4146, 4149],
{"addr": [4184, 4185], "value": 1},
{"addr": [4188, 4191], "value": 32514.2},
{"addr": [4308, 4407], "action": null},
{"addr": [4688, 4875], "value": 115.7, "action": "increment"},
{"addr": [4876, 4877],
"value": 50000.0,
"action": "increment",
"parameters": {"minval": 45000.0, "maxval": 55000.0}
},
{"addr": [4878, 48779],
"value": 50000.0,
"action": "random",
"parameters": {"minval": 45000.0, "maxval": 55000.0}
}
],
"string": [
{"addr": [16, 20], "value": "A_B_C_D_E_"},
{"addr": [529, 544], "value": "Brand name, 32 bytes...........X"}
],
"repeat": [
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Modbus simulator</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple60.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
<link rel="stylesheet" type="text/css" href="/pymodbus.css">
<!--REFRESH-->
</head>
<body>
<h1><center>Calls</center></h1>
<table width="80%" class="listbox">
<thead>
<tr>
<th width="20%">Call/Response</th>
<th width="10%">Address</th>
<th width="10%">Count</th>
<th width="60%">Data</th>
</tr>
</thead>
<tbody>
<!--FC_ROWS-->
</tbody>
<tfoot>
<tr>
<th colspan="4"><!--FC_FOOT--></th>
</tr>
</tfoot>
</table>
<fieldset class="tools_fieldset">
<legend>Monitor</legend>
<form action="/api/calls" method="get">
<table>
<tr>
<td><label>Register range</label></td>
<td>
<input type="number" value="FUNCTION_RANGE_START" name="range_start" />
<input type="number" value="FUNCTION_RANGE_STOP" name="range_stop" />
</td>
</tr>
<tr>
<td><label>Function</label></td>
<td>
<select name="function">
<option value=-1 selected>Any</option>
<!--FUNCTION_CODES-->
</select>
</td>
</tr>
<tr>
<td><label>Show as</label></td>
<td>
<input type="checkbox" FUNCTION_SHOW_HEX_CHECKED name="show_hex">Hex</input>
<input type="checkbox" FUNCTION_SHOW_DECODED_CHECKED name="show_decode">Decoded</input>
</td>
</tr>
</table>
<input type="submit" value="Monitor" name="submit" />
<input type="submit" value="Stop" name="submit" />
</form>
</fieldset>
<fieldset class="tools_fieldset">
<legend>Simulate <b><!--SIMULATION_ACTIVE--></b></legend>
<form action="/api/calls" method="get">
<table>
<tr>
<td>
<input type="radio" name="response_type" value="2" FUNCTION_RESPONSE_EMPTY_CHECKED>Empty</input>
</td>
<td></td>
<td></td>
</tr>
<tr>
<td>
<input type="radio" name="response_type" value="0" FUNCTION_RESPONSE_NORMAL_CHECKED>Normal</input>
</td>
<td><Label>split response</Label></td>
<td>
<input type="checkbox" name="response_split" FUNCTION_RESPONSE_SPLIT_CHECKED/>
<input type="number" name="split_delay" value="FUNCTION_RESPONSE_SPLIT_DELAY"/>seconds delay
</td>
</tr>
<tr>
<td></td>
<td><Label>Change rate</Label></td>
<td>
<input type="checkbox" name="response_cr" FUNCTION_RESPONSE_CR_CHECKED/>
<input type="number" name="response_cr_pct" value="FUNCTION_RESPONSE_CR_PCT"/>%
</td>
</tr>
<tr>
<td></td>
<td><Label>Delay response</Label></td>
<td><input type="number" name="response_delay" value="FUNCTION_RESPONSE_DELAY"/>seconds</td>
</tr>
<tr>
<td>
<input type="radio" name="response_type" value="1" FUNCTION_RESPONSE_ERROR_CHECKED>Error</input>
</td>
<td></td>
<td>
<select name="response_error">
<!--FUNCTION_ERROR-->
</select>
</td>
</tr>
<tr>
<td>
<input type="radio" name="response_type" value="3" FUNCTION_RESPONSE_JUNK_CHECKED>Junk</input>
</td>
<td><Label>Datalength</Label></td>
<td><input type="number" name="response_junk_datalen" value="FUNCTION_RESPONSE_JUNK" />bytes</td>
</tr>
<tr>
<td colspan="2"><Label>Clear after</Label></td>
<td><input type="number" name="response_clear_after" value="FUNCTION_RESPONSE_CLEAR_AFTER" />requests</td>
</tr>
</table>
<input type="submit" value="Simulate" name="submit" />
<input type="submit" value="Reset" name="submit" />
</form>
</fieldset>
</body>
</html>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Modbus simulator.</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple60.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
<link rel="stylesheet" type="text/css" href="/pymodbus.css">
<!--REFRESH-->
</head>
<body>
<center><h1>Log</h1></center>
<table width="80%" class="listbox">
<thead>
<tr>
<th width="10%">Log entries</th>
</tr>
</thead>
<tbody>
<!--LOG_ROWS-->
</tbody>
<tfoot>
<tr>
<th colspan="7"><!--LOG_FOOT--></th>
</tr>
</tfoot>
</table>
<fieldset class="tools_fieldset" width="30%">
<legend>Log</legend>
<form action="/api/log" method="get">
<input type="submit" value="Download" name="submit" />
<input type="submit" value="Monitor" name="submit" />
<input type="submit" value="Clear" name="submit" />
</form>
</fieldset>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Modbus simulator</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple60.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
<link rel="stylesheet" type="text/css" href="/pymodbus.css">
<!--REFRESH-->
</head>
<body>
<h1><center>Registers</center></h1>
<table width="80%" class="listbox">
<thead>
<tr>
<th width="10%">Register</th>
<th width="10%">Type</th>
<th width="10%">Write</th>
<th width="10%">Action</th>
<th width="10%">Value</th>
<th width="10%"># read</th>
<th width="10%"># write</th>
</tr>
</thead>
<tbody>
<!--REGISTER_ROWS-->
</tbody>
<tfoot>
<tr>
<th colspan="7"><!--REGISTER_FOOT--></th>
</tr>
</tfoot>
</table>
<fieldset class="tools_fieldset" width="40%">
<legend>Filter registers</legend>
<form action="/api/registers" method="get">
<table>
<tr>
<td><label>Start/end</label></td>
<td>
<input type="number" name="range_start" />
<input type="number" name="range_stop" />
</td>
</tr>
<tr>
<td><label>Type</label></td>
<td>
<select name="type">
<option value=-1 selected>Any</option>
<!--REGISTER_TYPES-->
</select>
</td>
</tr>
<tr>
<td><label>Action</label></td>
<td>
<select name="action">
<option value=-1 selected>Any</option>
<!--REGISTER_ACTIONS-->
</select>
</td>
</tr>
<tr>
<td><label>Writeable</label></td>
<td><input type="checkbox" name="writeable" /><br></td>
</tr>
</table>
<input type="submit" value="Add" name="submit" />
<input type="submit" value="Clear" name="submit" />
</form>
</fieldset>
<fieldset class="tools_fieldset" width="20%">
<legend>Set</legend>
<form action="/api/registers" method="get">
<table>
<tr>
<td><label>Register</label></td>
<td><input type="number" name="register" /></td>
</tr>
<tr>
<td><label>Value</label></td>
<td><input type="text" name="value" /></td>
</tr>
</table>
<input type="submit" value="Set" name="submit" />
</form>
</fieldset><br>
<p>Result of last command: <!--RESULT--></p>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Modbus simulator.</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple60.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
<link rel="stylesheet" type="text/css" href="/pymodbus.css">
<!--REFRESH-->
</head>
<body>
<body>
<center><h1>Server</h1></center>
<fieldset class="tools_fieldset" width="30%">
<legend>Status</legend>
Uptime: <!--UPTIME-->
</fieldset>
<fieldset class="tools_fieldset" width="30%">
<legend>Status</legend>
<form action="/api/server" method="get">
<input type="submit" value="Restart" name="submit" />
</form>
</fieldset>
</body>
</html>

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Modbus simulator.</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple60.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple76.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple120.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple152.png">
<link rel="stylesheet" type="text/css" href="/pymodbus.css">
<style rel="stylesheet" type="text/css" media="screen">
.sidenav {
height: 100%;
width: 160px;
position: fixed;
z-index: 1;
top: 0;
left: 0;
background-color: gray;
overflow-x: hidden;
padding-top: 5px;
}
.main {
margin-left: 160px;
top: 0;
left: 0;
font-size: 28px;
padding: 0px 0px;
width: 100% - 160px;
height: 100%;
}
.sidenav legend {
color: white
}
</style>
</head>
<body>
<div class="sidenav">
<a href="welcome.html" target="editor">Welcome</a>
<form action="/api" method="get" target="editor">
<fieldset>
<legend>Refresh rate</legend>
<input type="number" style="width: 60%;" value=0 name="refresh">
</fieldset>
<fieldset>
<legend>View</legend>
<input type="submit" formaction="/api/registers" value="Registers" name="submit" /><br>
<input type="submit" formaction="/api/calls" value="Calls" name="submit" /><br>
<input type="submit" formaction="/api/log" value="Log" name="submit" /><br>
<input type="submit" formaction="/api/server" value="Server" name="submit" />
</fieldset>
</form>
<p>Powered by:
<a href="https://github.com/pymodbus-dev/pymodbus"><b>pymodbus</b></a> an open source project, patches are welcome.
</p>
</div>
<div class="main">
<iframe name="editor" title="Simulator" src="welcome.html"/>
</div>
</body>
</html>

View File

@@ -0,0 +1,62 @@
html {
height: 100%;
width: 100%;
}
body {
height: 100%;
width: 100%;
background-color: bisque;
}
table.listbox {
border-collapse: collapse;
border: 1px solid black;
}
table.listbox th {
background-color: lightgray;
border: 1px solid black;
padding: 5px
}
table.listbox td {
border: 1px solid black;
text-align: right;
background-color: #f1f1f1;
padding: 5px
}
legend {
font-size: 18px;
font-weight: bold;
position: relative;
color: black;
padding: 5px 5px;
}
a {
padding: 2px 4px 2px 4px;
text-decoration: none;
font-size: 18px;
display: block;
}
a:hover {
color: #f1f1f1;
}
p {
padding: 2px 4px 6px 4px;
display: block;
}
iframe {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
border: 0;
}
input[type="submit"] {
font-size: 14px;
background-color: lightblue;
margin-top: 10px;
}
.tools_fieldset {
display: inline;
vertical-align:top
}

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Modbus simulator.</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
</style>
</head>
<body>
<center><h1>Welcome to the pymodbus simulator</h1></center>
<p>Thanks for using pymodbus.</p>
<p>the pymodbus development team</p>
<br><br>
The <b>View</b> to the left, are used to control the simulator.
<ul>
<li><b>Registers</b> are used to monitor and/or change registers in the configuration (non-resistent),</li>
<li><b>Calls</b> are used to show and/or modify call from clients,</li>
<li><b>Log</b> are used to show the server log,</li>
<li><b>Server</b> are used to control the server.</li>
</ul>
</body>
</html>