initial
This commit is contained in:
1
env/lib/python3.11/site-packages/pymodbus/server/reactive/__init__.py
vendored
Normal file
1
env/lib/python3.11/site-packages/pymodbus/server/reactive/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Initialize."""
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/default_config.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/default_config.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/main.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/server/reactive/__pycache__/main.cpython-311.pyc
vendored
Normal file
Binary file not shown.
55
env/lib/python3.11/site-packages/pymodbus/server/reactive/default_config.py
vendored
Normal file
55
env/lib/python3.11/site-packages/pymodbus/server/reactive/default_config.py
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Configuration for Pymodbus REPL Reactive Module."""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"tcp": {
|
||||
"ignore_missing_slaves": False,
|
||||
},
|
||||
"serial": {
|
||||
"stopbits": 1,
|
||||
"bytesize": 8,
|
||||
"parity": "N",
|
||||
"baudrate": 9600,
|
||||
"timeout": 3,
|
||||
"reconnect_delay": 2,
|
||||
},
|
||||
"tls": {
|
||||
"certfile": None,
|
||||
"keyfile": None,
|
||||
"ignore_missing_slaves": False,
|
||||
},
|
||||
"udp": {
|
||||
"ignore_missing_slaves": False,
|
||||
},
|
||||
"data_block_settings": {
|
||||
"min_binary_value": 0, # For coils and DI
|
||||
"max_binary_value": 1, # For coils and DI
|
||||
"min_register_value": 0, # For Holding and input registers
|
||||
"max_register_value": 65535, # For Holding and input registers
|
||||
"data_block": {
|
||||
"discrete_inputs": {
|
||||
"block_start": 0, # Block start
|
||||
"block_size": 100, # Block end
|
||||
"default": 0, # Default value,
|
||||
"sparse": False,
|
||||
},
|
||||
"coils": {
|
||||
"block_start": 0,
|
||||
"block_size": 100,
|
||||
"default": 0,
|
||||
"sparse": False,
|
||||
},
|
||||
"holding_registers": {
|
||||
"block_start": 0,
|
||||
"block_size": 100,
|
||||
"default": 0,
|
||||
"sparse": False,
|
||||
},
|
||||
"input_registers": {
|
||||
"block_start": 0,
|
||||
"block_size": 100,
|
||||
"default": 0,
|
||||
"sparse": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
501
env/lib/python3.11/site-packages/pymodbus/server/reactive/main.py
vendored
Normal file
501
env/lib/python3.11/site-packages/pymodbus/server/reactive/main.py
vendored
Normal file
@@ -0,0 +1,501 @@
|
||||
"""Reactive main."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from enum import Enum
|
||||
|
||||
|
||||
try:
|
||||
from aiohttp import web
|
||||
except ImportError:
|
||||
print(
|
||||
"Reactive server requires aiohttp. "
|
||||
'Please install with "pip install aiohttp" and try again.'
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
from pymodbus import __version__ as pymodbus_version
|
||||
from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext
|
||||
from pymodbus.datastore.store import (
|
||||
BaseModbusDataBlock,
|
||||
ModbusSequentialDataBlock,
|
||||
ModbusSparseDataBlock,
|
||||
)
|
||||
from pymodbus.device import ModbusDeviceIdentification
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.pdu import ExceptionResponse, ModbusExceptions
|
||||
from pymodbus.server.async_io import (
|
||||
ModbusSerialServer,
|
||||
ModbusTcpServer,
|
||||
ModbusTlsServer,
|
||||
ModbusUdpServer,
|
||||
)
|
||||
from pymodbus.transaction import (
|
||||
ModbusAsciiFramer,
|
||||
ModbusBinaryFramer,
|
||||
ModbusRtuFramer,
|
||||
ModbusSocketFramer,
|
||||
ModbusTlsFramer,
|
||||
)
|
||||
|
||||
|
||||
SERVER_MAPPER = {
|
||||
"tcp": ModbusTcpServer,
|
||||
"serial": ModbusSerialServer,
|
||||
"udp": ModbusUdpServer,
|
||||
"tls": ModbusTlsServer,
|
||||
}
|
||||
|
||||
DEFAULT_FRAMER = {
|
||||
"tcp": ModbusSocketFramer,
|
||||
"rtu": ModbusRtuFramer,
|
||||
"tls": ModbusTlsFramer,
|
||||
"udp": ModbusSocketFramer,
|
||||
"ascii": ModbusAsciiFramer,
|
||||
"binary": ModbusBinaryFramer,
|
||||
}
|
||||
|
||||
DEFAULT_MANIPULATOR = {
|
||||
"response_type": "normal", # normal, error, delayed, empty
|
||||
"delay_by": 0,
|
||||
"error_code": ModbusExceptions.IllegalAddress,
|
||||
"clear_after": 5, # request count
|
||||
}
|
||||
DEFAULT_MODBUS_MAP = {
|
||||
"block_start": 0,
|
||||
"block_size": 100,
|
||||
"default": 0,
|
||||
"sparse": False,
|
||||
}
|
||||
DEFAULT_DATA_BLOCK = {
|
||||
"co": DEFAULT_MODBUS_MAP,
|
||||
"di": DEFAULT_MODBUS_MAP,
|
||||
"ir": DEFAULT_MODBUS_MAP,
|
||||
"hr": DEFAULT_MODBUS_MAP,
|
||||
}
|
||||
|
||||
HINT = """
|
||||
Reactive Modbus Server started.
|
||||
{}
|
||||
|
||||
===========================================================================
|
||||
Example Usage:
|
||||
curl -X POST http://{}:{} -d "{{"response_type": "error", "error_code": 4}}"
|
||||
===========================================================================
|
||||
"""
|
||||
|
||||
|
||||
class ReactiveModbusSlaveContext(ModbusSlaveContext):
|
||||
"""Reactive Modbus slave context"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
discrete_inputs: BaseModbusDataBlock = None,
|
||||
coils: BaseModbusDataBlock = None,
|
||||
input_registers: BaseModbusDataBlock = None,
|
||||
holding_registers: BaseModbusDataBlock = None,
|
||||
zero_mode: bool = False,
|
||||
randomize: int = 0,
|
||||
change_rate: int = 0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Reactive Modbus slave context supporting simulating data.
|
||||
|
||||
:param discrete_inputs: Discrete input data block
|
||||
:param coils: Coils data block
|
||||
:param input_registers: Input registers data block
|
||||
:param holding_registers: Holding registers data block
|
||||
:param zero_mode: Enable zero mode for data blocks
|
||||
:param randomize: Randomize reads every <n> reads for DI and IR,
|
||||
default is disabled (0)
|
||||
:param change_rate: Rate in % of registers to change for DI and IR,
|
||||
default is disabled (0)
|
||||
:param min_binary_value: Minimum value for coils and discrete inputs
|
||||
:param max_binary_value: Max value for discrete inputs
|
||||
:param min_register_value: Minimum value for input registers
|
||||
:param max_register_value: Max value for input registers
|
||||
|
||||
"""
|
||||
super().__init__(
|
||||
di=discrete_inputs,
|
||||
co=coils,
|
||||
ir=input_registers,
|
||||
hr=holding_registers,
|
||||
zero_mode=zero_mode,
|
||||
)
|
||||
min_binary_value = kwargs.get("min_binary_value", 0)
|
||||
max_binary_value = kwargs.get("max_binary_value", 1)
|
||||
min_register_value = kwargs.get("min_register_value", 0)
|
||||
max_register_value = kwargs.get("max_register_value", 65535)
|
||||
self._randomize = randomize
|
||||
self._change_rate = change_rate
|
||||
if self._randomize > 0 and self._change_rate > 0:
|
||||
sys.exit(
|
||||
"'randomize' and 'change_rate' is not allowed to use at the same time"
|
||||
)
|
||||
self._lock = threading.Lock()
|
||||
self._read_counter = {"d": 0, "i": 0}
|
||||
self._min_binary_value = min_binary_value
|
||||
self._max_binary_value = max_binary_value
|
||||
self._min_register_value = min_register_value
|
||||
self._max_register_value = max_register_value
|
||||
|
||||
def getValues(self, fc_as_hex, address, count=1):
|
||||
"""Get `count` values from datastore.
|
||||
|
||||
:param fc_as_hex: The function we are working with
|
||||
:param address: The starting address
|
||||
:param count: The number of values to retrieve
|
||||
:returns: The requested values from a:a+c
|
||||
"""
|
||||
if not self.zero_mode:
|
||||
address += 1
|
||||
Log.debug("getValues: fc-[{}] address-{}: count-{}", fc_as_hex, address, count)
|
||||
_block_type = self.decode(fc_as_hex)
|
||||
if self._randomize > 0 and _block_type in {"d", "i"}:
|
||||
with self._lock:
|
||||
if not (self._read_counter.get(_block_type) % self._randomize):
|
||||
# Update values
|
||||
if _block_type == "d":
|
||||
min_val = self._min_binary_value
|
||||
max_val = self._max_binary_value
|
||||
else:
|
||||
min_val = self._min_register_value
|
||||
max_val = self._max_register_value
|
||||
values = [random.randint(min_val, max_val) for _ in range(count)]
|
||||
self.store[_block_type].setValues(address, values)
|
||||
self._read_counter[_block_type] += 1
|
||||
elif self._change_rate > 0 and _block_type in {"d", "i"}:
|
||||
regs_to_changes = round(count * self._change_rate / 100)
|
||||
random_indices = random.sample(range(count), regs_to_changes)
|
||||
for offset in random_indices:
|
||||
with self._lock:
|
||||
# Update values
|
||||
if _block_type == "d":
|
||||
min_val = self._min_binary_value
|
||||
max_val = self._max_binary_value
|
||||
else:
|
||||
min_val = self._min_register_value
|
||||
max_val = self._max_register_value
|
||||
self.store[_block_type].setValues(
|
||||
address + offset, random.randint(min_val, max_val)
|
||||
)
|
||||
values = self.store[_block_type].getValues(address, count)
|
||||
return values
|
||||
|
||||
|
||||
class ReactiveServer:
|
||||
"""Modbus Asynchronous Server which can manipulate the response dynamically.
|
||||
|
||||
Useful for testing
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, modbus_server):
|
||||
"""Initialize."""
|
||||
self._web_app = web.Application()
|
||||
self._runner = web.AppRunner(self._web_app)
|
||||
self._host = host
|
||||
self._port = int(port)
|
||||
self._modbus_server = modbus_server
|
||||
self._add_routes()
|
||||
self._counter = 0
|
||||
self._modbus_server.response_manipulator = self.manipulate_response
|
||||
self._manipulator_config = {**DEFAULT_MANIPULATOR}
|
||||
self._web_app.on_startup.append(self.start_modbus_server)
|
||||
self._web_app.on_shutdown.append(self.stop_modbus_server)
|
||||
|
||||
@property
|
||||
def web_app(self):
|
||||
"""Start web_app."""
|
||||
return self._web_app
|
||||
|
||||
@property
|
||||
def manipulator_config(self):
|
||||
"""Manipulate config."""
|
||||
return self._manipulator_config
|
||||
|
||||
@manipulator_config.setter
|
||||
def manipulator_config(self, value):
|
||||
if isinstance(value, dict):
|
||||
self._manipulator_config.update(**value)
|
||||
|
||||
def _add_routes(self):
|
||||
"""Add routes."""
|
||||
self._web_app.add_routes([web.post("/", self._response_manipulator)])
|
||||
|
||||
async def start_modbus_server(self, app):
|
||||
"""Start Modbus server as asyncio task after startup.
|
||||
|
||||
:param app: Webapp
|
||||
"""
|
||||
try:
|
||||
if hasattr(asyncio, "create_task"):
|
||||
app["modbus_server"] = asyncio.create_task(
|
||||
self._modbus_server.serve_forever()
|
||||
)
|
||||
else:
|
||||
app["modbus_server"] = asyncio.ensure_future(
|
||||
self._modbus_server.serve_forever()
|
||||
)
|
||||
|
||||
Log.info("Modbus server started")
|
||||
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
Log.error("Error starting modbus server {}", exc)
|
||||
|
||||
async def stop_modbus_server(self, app):
|
||||
"""Stop modbus server.
|
||||
|
||||
:param app: Webapp
|
||||
"""
|
||||
Log.info("Stopping modbus server")
|
||||
if isinstance(self._modbus_server, ModbusSerialServer):
|
||||
app["modbus_serial_server"].cancel()
|
||||
app["modbus_server"].cancel()
|
||||
await app["modbus_server"]
|
||||
Log.info("Modbus server Stopped")
|
||||
|
||||
async def _response_manipulator(self, request):
|
||||
"""POST request Handler for response manipulation end point.
|
||||
|
||||
Payload is a dict with following fields
|
||||
:response_type : One among (normal, delayed, error, empty, stray)
|
||||
:error_code: Modbus error code for error response
|
||||
:delay_by: Delay sending response by <n> seconds
|
||||
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = await request.json()
|
||||
self._manipulator_config.update(data)
|
||||
return web.json_response(data=data)
|
||||
|
||||
def update_manipulator_config(self, config):
|
||||
"""Update manipulator config. Resets previous counters.
|
||||
|
||||
:param config: Manipulator config (dict)
|
||||
"""
|
||||
self._counter = 0
|
||||
self._manipulator_config = config
|
||||
|
||||
def manipulate_response(self, response):
|
||||
"""Manipulate the actual response according to the required error state.
|
||||
|
||||
:param response: Modbus response object
|
||||
:return: Modbus response
|
||||
"""
|
||||
skip_encoding = False
|
||||
if not self._manipulator_config:
|
||||
return response
|
||||
|
||||
clear_after = self._manipulator_config.get("clear_after")
|
||||
if clear_after and self._counter > clear_after:
|
||||
Log.info("Resetting manipulator after {} responses", clear_after)
|
||||
self.update_manipulator_config(dict(DEFAULT_MANIPULATOR))
|
||||
return response
|
||||
response_type = self._manipulator_config.get("response_type")
|
||||
if response_type == "error":
|
||||
error_code = self._manipulator_config.get("error_code")
|
||||
Log.warning("Sending error response for all incoming requests")
|
||||
err_response = ExceptionResponse(response.function_code, error_code)
|
||||
err_response.transaction_id = response.transaction_id
|
||||
err_response.slave_id = response.slave_id
|
||||
response = err_response
|
||||
self._counter += 1
|
||||
elif response_type == "delayed":
|
||||
delay_by = self._manipulator_config.get("delay_by")
|
||||
Log.warning("Delaying response by {}s for all incoming requests", delay_by)
|
||||
time.sleep(delay_by) # change to async
|
||||
self._counter += 1
|
||||
elif response_type == "empty":
|
||||
Log.warning("Sending empty response")
|
||||
self._counter += 1
|
||||
response.should_respond = False
|
||||
elif response_type == "stray":
|
||||
if (data_len := self._manipulator_config.get("data_len", 10)) <= 0:
|
||||
Log.warning("Invalid data_len {}, using default 10", data_len)
|
||||
data_len = 10
|
||||
response = os.urandom(data_len)
|
||||
self._counter += 1
|
||||
skip_encoding = True
|
||||
return response, skip_encoding
|
||||
|
||||
async def run_async(self, repl_mode=False):
|
||||
"""Run Web app."""
|
||||
|
||||
try:
|
||||
await self._runner.setup()
|
||||
site = web.TCPSite(self._runner, self._host, self._port)
|
||||
await site.start()
|
||||
if not repl_mode:
|
||||
message = (
|
||||
f"======== Running on http://{self._host}:{self._port} ========"
|
||||
)
|
||||
msg = HINT.format(message, self._host, self._port)
|
||||
print(msg)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
Log.error("Exception {}", exc)
|
||||
|
||||
@classmethod
|
||||
def create_identity(
|
||||
cls,
|
||||
vendor="Pymodbus",
|
||||
product_code="PM",
|
||||
vendor_url="https://github.com/pymodbus-dev/pymodbus/",
|
||||
product_name="Pymodbus Server",
|
||||
model_name="Reactive Server",
|
||||
version=pymodbus_version,
|
||||
):
|
||||
"""Create modbus identity.
|
||||
|
||||
:param vendor:
|
||||
:param product_code:
|
||||
:param vendor_url:
|
||||
:param product_name:
|
||||
:param model_name:
|
||||
:param version:
|
||||
:return: ModbusIdentity object
|
||||
"""
|
||||
identity = ModbusDeviceIdentification(
|
||||
info_name={
|
||||
"VendorName": vendor,
|
||||
"ProductCode": product_code,
|
||||
"VendorUrl": vendor_url,
|
||||
"ProductName": product_name,
|
||||
"ModelName": model_name,
|
||||
"MajorMinorRevision": version,
|
||||
}
|
||||
)
|
||||
|
||||
return identity
|
||||
|
||||
@classmethod
|
||||
def create_context(
|
||||
cls,
|
||||
data_block_settings: dict = {},
|
||||
slave: list[int] | int = [1],
|
||||
single: bool = False,
|
||||
randomize: int = 0,
|
||||
change_rate: int = 0,
|
||||
): # pylint: disable=dangerous-default-value
|
||||
"""Create Modbus context.
|
||||
|
||||
:param data_block_settings: Datablock (dict) Refer DEFAULT_DATA_BLOCK
|
||||
:param slave: Unit id for the slave
|
||||
:param single: To run as a single slave
|
||||
:param randomize: Randomize every <n> reads for DI and IR.
|
||||
:param change_rate: Rate in % of registers to change for DI and IR.
|
||||
:return: ModbusServerContext object
|
||||
"""
|
||||
data_block = data_block_settings.pop("data_block", DEFAULT_DATA_BLOCK)
|
||||
if not isinstance(slave, list):
|
||||
slave = [slave]
|
||||
slaves = {}
|
||||
for i in slave:
|
||||
block = {}
|
||||
for modbus_entity, block_desc in data_block.items():
|
||||
start_address = block_desc.get("block_start", 0)
|
||||
default_count = block_desc.get("block_size", 0)
|
||||
default_value = block_desc.get("default", 0)
|
||||
default_values = [default_value] * default_count
|
||||
sparse = block_desc.get("sparse", False)
|
||||
db = ModbusSequentialDataBlock if not sparse else ModbusSparseDataBlock
|
||||
if sparse:
|
||||
if not (address_map := block_desc.get("address_map")):
|
||||
address_map = random.sample(
|
||||
range(start_address + 1, default_count), default_count - 1
|
||||
)
|
||||
address_map.insert(0, 0)
|
||||
address_map.sort()
|
||||
block[modbus_entity] = db(address_map, default_values)
|
||||
else:
|
||||
block[modbus_entity] = db(start_address, default_values)
|
||||
|
||||
slave_context = ReactiveModbusSlaveContext(
|
||||
**block,
|
||||
randomize=randomize,
|
||||
change_rate=change_rate,
|
||||
zero_mode=True,
|
||||
**data_block_settings,
|
||||
)
|
||||
if not single:
|
||||
slaves[i] = slave_context
|
||||
else:
|
||||
slaves[0] = slave_context
|
||||
server_context = ModbusServerContext(slaves, single=single)
|
||||
return server_context
|
||||
|
||||
@classmethod
|
||||
def factory( # pylint: disable=dangerous-default-value,too-many-arguments
|
||||
cls,
|
||||
server,
|
||||
framer=None,
|
||||
context=None,
|
||||
slave=1,
|
||||
single=False,
|
||||
host="localhost",
|
||||
modbus_port=5020,
|
||||
web_port=8080,
|
||||
data_block_settings={"data_block": DEFAULT_DATA_BLOCK},
|
||||
identity=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Create ReactiveModbusServer.
|
||||
|
||||
:param server: Modbus server type (tcp, rtu, tls, udp)
|
||||
:param framer: Modbus framer (ModbusSocketFramer, ModbusRTUFramer, ModbusTLSFramer)
|
||||
:param context: Modbus server context to use
|
||||
:param slave: Modbus slave id
|
||||
:param single: Run in single mode
|
||||
:param host: Host address to use for both web app and modbus server (default localhost)
|
||||
:param modbus_port: Modbus port for TCP and UDP server(default: 5020)
|
||||
:param web_port: Web App port (default: 8080)
|
||||
:param data_block_settings: Datablock settings (refer DEFAULT_DATA_BLOCK)
|
||||
:param identity: Modbus identity object
|
||||
:param kwargs: Other server specific keyword arguments,
|
||||
: refer corresponding servers documentation
|
||||
:return: ReactiveServer object
|
||||
"""
|
||||
if isinstance(server, Enum):
|
||||
server = server.value
|
||||
|
||||
if server.lower() not in SERVER_MAPPER:
|
||||
Log.error("Invalid server {}", server)
|
||||
sys.exit(1)
|
||||
server = SERVER_MAPPER.get(server)
|
||||
randomize = kwargs.pop("randomize", 0)
|
||||
change_rate = kwargs.pop("change_rate", 0)
|
||||
if not framer:
|
||||
framer = DEFAULT_FRAMER.get(server)
|
||||
if not context:
|
||||
context = cls.create_context(
|
||||
data_block_settings=data_block_settings,
|
||||
slave=slave,
|
||||
single=single,
|
||||
randomize=randomize,
|
||||
change_rate=change_rate,
|
||||
)
|
||||
if not identity:
|
||||
identity = cls.create_identity()
|
||||
if server == ModbusSerialServer:
|
||||
kwargs["port"] = modbus_port
|
||||
server = server(context, framer=framer, identity=identity, **kwargs)
|
||||
else:
|
||||
server = server(
|
||||
context,
|
||||
framer=framer,
|
||||
identity=identity,
|
||||
address=(host, modbus_port),
|
||||
**kwargs,
|
||||
)
|
||||
return ReactiveServer(host, web_port, server)
|
||||
|
||||
|
||||
# __END__
|
||||
Reference in New Issue
Block a user