initial
This commit is contained in:
1
env/lib/python3.11/site-packages/pymodbus/repl/server/__init__.py
vendored
Normal file
1
env/lib/python3.11/site-packages/pymodbus/repl/server/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Repl server."""
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/cli.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/cli.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/main.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/server/__pycache__/main.cpython-311.pyc
vendored
Normal file
Binary file not shown.
211
env/lib/python3.11/site-packages/pymodbus/repl/server/cli.py
vendored
Normal file
211
env/lib/python3.11/site-packages/pymodbus/repl/server/cli.py
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
"""Repl server cli."""
|
||||
import shutil
|
||||
|
||||
import click
|
||||
from prompt_toolkit import PromptSession, print_formatted_text
|
||||
from prompt_toolkit.completion import NestedCompleter
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.shortcuts import clear
|
||||
from prompt_toolkit.shortcuts.progress_bar import formatters
|
||||
from prompt_toolkit.styles import Style
|
||||
|
||||
|
||||
TITLE = r"""
|
||||
__________ .______. _________
|
||||
\______ \___.__. _____ ____ __| _/\_ |__ __ __ ______ / _____/ ______________ __ ___________
|
||||
| ___< | |/ \ / _ \ / __ | | __ \| | \/ ___/ \_____ \_/ __ \_ __ \ \/ // __ \_ __ \\
|
||||
| | \___ | Y Y ( <_> ) /_/ | | \_\ \ | /\___ \ / \ ___/| | \/\ /\ ___/| | \/
|
||||
|____| / ____|__|_| /\____/\____ | |___ /____//____ > /_______ /\___ >__| \_/ \___ >__|
|
||||
\/ \/ \/ \/ \/ \/ \/ \/
|
||||
"""
|
||||
|
||||
|
||||
SMALL_TITLE = "Pymodbus server..."
|
||||
BOTTOM_TOOLBAR = HTML(
|
||||
'(MODBUS SERVER) <b><style bg="ansired">Press Ctrl+C or '
|
||||
'type "exit" to quit</style></b> Type "help" '
|
||||
"for list of available commands"
|
||||
)
|
||||
COMMAND_ARGS = ["response_type", "error_code", "delay_by", "clear_after", "data_len"]
|
||||
RESPONSE_TYPES = ["normal", "error", "delayed", "empty", "stray"]
|
||||
COMMANDS = {
|
||||
"manipulator": {
|
||||
"response_type": None,
|
||||
"error_code": None,
|
||||
"delay_by": None,
|
||||
"clear_after": None,
|
||||
},
|
||||
"exit": None,
|
||||
"help": None,
|
||||
"clear": None,
|
||||
}
|
||||
USAGE = (
|
||||
"manipulator response_type=|normal|error|delayed|empty|stray \n"
|
||||
"\tAdditional parameters\n"
|
||||
"\t\terror_code=<int> \n\t\tdelay_by=<in seconds> \n\t\t"
|
||||
"clear_after=<clear after n messages int>"
|
||||
"\n\t\tdata_len=<length of stray data (int)>\n"
|
||||
"\n\tExample usage: \n\t"
|
||||
"1. Send error response 3 for 4 requests\n\t"
|
||||
" <ansiblue>manipulator response_type=error error_code=3 clear_after=4</ansiblue>\n\t"
|
||||
"2. Delay outgoing response by 5 seconds indefinitely\n\t"
|
||||
" <ansiblue>manipulator response_type=delayed delay_by=5</ansiblue>\n\t"
|
||||
"3. Send empty response\n\t"
|
||||
" <ansiblue>manipulator response_type=empty</ansiblue>\n\t"
|
||||
"4. Send stray response of length 12 and revert to normal after 2 responses\n\t"
|
||||
" <ansiblue>manipulator response_type=stray data_len=11 clear_after=2</ansiblue>\n\t"
|
||||
"5. To disable response manipulation\n\t"
|
||||
" <ansiblue>manipulator response_type=normal</ansiblue>"
|
||||
)
|
||||
COMMAND_HELPS = {
|
||||
"manipulator": f"Manipulate response from server.\nUsage: {USAGE}",
|
||||
"clear": "Clears screen",
|
||||
}
|
||||
|
||||
|
||||
STYLE = Style.from_dict({"": "cyan"})
|
||||
CUSTOM_FORMATTERS = [
|
||||
formatters.Label(suffix=": "),
|
||||
formatters.Bar(start="|", end="|", sym_a="#", sym_b="#", sym_c="-"),
|
||||
formatters.Text(" "),
|
||||
formatters.Text(" "),
|
||||
formatters.TimeElapsed(),
|
||||
formatters.Text(" "),
|
||||
]
|
||||
|
||||
|
||||
def info(message):
|
||||
"""Show info."""
|
||||
if not isinstance(message, str):
|
||||
message = str(message)
|
||||
click.secho(message, fg="green")
|
||||
|
||||
|
||||
def warning(message):
|
||||
"""Show warning."""
|
||||
click.secho(str(message), fg="yellow")
|
||||
|
||||
|
||||
def error(message):
|
||||
"""Show error."""
|
||||
click.secho(str(message), fg="red")
|
||||
|
||||
|
||||
def get_terminal_width():
|
||||
"""Get terminal width."""
|
||||
return shutil.get_terminal_size()[0]
|
||||
|
||||
|
||||
def print_help():
|
||||
"""Print help."""
|
||||
print_formatted_text(HTML("<u>Available commands:</u>"))
|
||||
for cmd, hlp in sorted(COMMAND_HELPS.items()):
|
||||
print_formatted_text(
|
||||
HTML(f"<skyblue>{cmd:45s}</skyblue><seagreen>{hlp:100s}</seagreen>")
|
||||
)
|
||||
|
||||
|
||||
def print_title():
|
||||
"""Print title - large if there are sufficient columns, otherwise small."""
|
||||
col = get_terminal_width()
|
||||
max_len = max( # pylint: disable=consider-using-generator
|
||||
[len(t) for t in TITLE.split("\n")]
|
||||
)
|
||||
if col > max_len:
|
||||
info(TITLE)
|
||||
else:
|
||||
print_formatted_text(
|
||||
HTML(f'<u><b><style color="green">{SMALL_TITLE}</style></b></u>')
|
||||
)
|
||||
|
||||
|
||||
async def interactive_shell(server):
|
||||
"""Run CLI interactive shell."""
|
||||
print_title()
|
||||
info("")
|
||||
completer = NestedCompleter.from_nested_dict(COMMANDS)
|
||||
session = PromptSession(
|
||||
"SERVER > ", completer=completer, bottom_toolbar=BOTTOM_TOOLBAR
|
||||
)
|
||||
|
||||
# Run echo loop. Read text from stdin, and reply it back.
|
||||
while True:
|
||||
try:
|
||||
result = await session.prompt_async()
|
||||
if result == "exit":
|
||||
await server.web_app.shutdown()
|
||||
break
|
||||
if result == "help":
|
||||
print_help()
|
||||
continue
|
||||
if result == "clear":
|
||||
clear()
|
||||
continue
|
||||
if command := result.split():
|
||||
if command[0] not in COMMANDS:
|
||||
warning(f"Invalid command or invalid usage of command - {command}")
|
||||
continue
|
||||
if len(command) == 1:
|
||||
warning(f'Usage: "{USAGE}"')
|
||||
else:
|
||||
val_dict = _process_args(command[1:])
|
||||
if val_dict: # pylint: disable=consider-using-assignment-expr
|
||||
server.update_manipulator_config(val_dict)
|
||||
# server.manipulator_config = val_dict
|
||||
# result = await run_command(tester, *command)
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return
|
||||
|
||||
|
||||
def _process_args(args) -> dict:
|
||||
"""Process arguments passed to CLI."""
|
||||
skip_next = False
|
||||
val_dict = {}
|
||||
for index, arg in enumerate(args):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if "=" in arg:
|
||||
arg, value = arg.split("=")
|
||||
elif arg in COMMAND_ARGS:
|
||||
try:
|
||||
value = args[index + 1]
|
||||
skip_next = True
|
||||
except IndexError:
|
||||
error(f"Missing value for argument - {arg}")
|
||||
warning('Usage: "{USAGE}"')
|
||||
break
|
||||
if arg == "response_type":
|
||||
if value not in RESPONSE_TYPES:
|
||||
warning(f"Invalid response type request - {value}")
|
||||
warning(f"Choose from {RESPONSE_TYPES}")
|
||||
continue
|
||||
elif arg in { # pylint: disable=confusing-consecutive-elif
|
||||
"error_code",
|
||||
"delay_by",
|
||||
"clear_after",
|
||||
"data_len",
|
||||
}:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
warning(f"Expected integer value for {arg}")
|
||||
continue
|
||||
val_dict[arg] = value
|
||||
return val_dict
|
||||
|
||||
|
||||
async def main(server):
|
||||
"""Run main."""
|
||||
# with patch_stdout():
|
||||
try:
|
||||
await interactive_shell(server)
|
||||
finally:
|
||||
pass
|
||||
warning("Bye Bye!!!")
|
||||
|
||||
|
||||
async def run_repl(server):
|
||||
"""Run repl server."""
|
||||
await main(server)
|
||||
209
env/lib/python3.11/site-packages/pymodbus/repl/server/main.py
vendored
Normal file
209
env/lib/python3.11/site-packages/pymodbus/repl/server/main.py
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Repl server main."""
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import typer
|
||||
|
||||
from pymodbus import pymodbus_apply_logging_config
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.repl.server.cli import run_repl
|
||||
from pymodbus.server.reactive.default_config import DEFAULT_CONFIG
|
||||
from pymodbus.server.reactive.main import (
|
||||
DEFAULT_FRAMER,
|
||||
ReactiveServer,
|
||||
)
|
||||
|
||||
|
||||
CANCELLED_ERROR = asyncio.exceptions.CancelledError
|
||||
CONTEXT_SETTING = {"allow_extra_args": True, "ignore_unknown_options": True}
|
||||
|
||||
|
||||
# TBD class ModbusServerConfig:
|
||||
|
||||
|
||||
class ModbusServerTypes(str, Enum):
|
||||
"""Server types."""
|
||||
|
||||
# ["tcp", "serial", "tls", "udp"]
|
||||
tcp = "tcp" # pylint: disable=invalid-name
|
||||
serial = "serial" # pylint: disable=invalid-name
|
||||
tls = "tls" # pylint: disable=invalid-name
|
||||
udp = "udp" # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class ModbusFramerTypes(str, Enum):
|
||||
"""Framer types."""
|
||||
|
||||
# ["socket", "rtu", "tls", "ascii", "binary"]
|
||||
socket = "socket" # pylint: disable=invalid-name
|
||||
rtu = "rtu" # pylint: disable=invalid-name
|
||||
tls = "tls" # pylint: disable=invalid-name
|
||||
ascii = "ascii" # pylint: disable=invalid-name
|
||||
binary = "binary" # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _completer(incomplete: str, valid_values: List[str]) -> List[str]:
|
||||
"""Complete value."""
|
||||
completion = []
|
||||
for name in valid_values:
|
||||
if name.startswith(incomplete):
|
||||
completion.append(name)
|
||||
return completion
|
||||
|
||||
|
||||
def framers(incomplete: str) -> List[str]:
|
||||
"""Return an autocompleted list of supported clouds."""
|
||||
_framers = ["socket", "rtu", "tls", "ascii", "binary"]
|
||||
return _completer(incomplete, _framers)
|
||||
|
||||
|
||||
def servers(incomplete: str) -> List[str]:
|
||||
"""Return an autocompleted list of supported clouds."""
|
||||
_servers = ["tcp", "serial", "tls", "udp"]
|
||||
return _completer(incomplete, _servers)
|
||||
|
||||
|
||||
def process_extra_args(extra_args: List[str], modbus_config: dict) -> dict:
|
||||
"""Process extra args passed to server."""
|
||||
options_stripped = [x.strip().replace("--", "") for x in extra_args[::2]]
|
||||
extra_args_dict = dict(list(zip(options_stripped, extra_args[1::2])))
|
||||
for option, value in extra_args_dict.items():
|
||||
if option in modbus_config:
|
||||
try:
|
||||
modbus_config[option] = type(modbus_config[option])(value)
|
||||
except ValueError as err:
|
||||
Log.error(
|
||||
"Error parsing extra arg {} with value '{}'. {}", option, value, err
|
||||
)
|
||||
sys.exit(1)
|
||||
return modbus_config
|
||||
|
||||
|
||||
app = typer.Typer(
|
||||
no_args_is_help=True,
|
||||
context_settings=CONTEXT_SETTING,
|
||||
help="Reactive Modbus server",
|
||||
)
|
||||
|
||||
|
||||
@app.callback()
|
||||
def server(
|
||||
ctx: typer.Context,
|
||||
host: str = typer.Option("localhost", "--host", help="Host address"),
|
||||
web_port: int = typer.Option(8080, "--web-port", help="Web app port"),
|
||||
broadcast_support: bool = typer.Option(
|
||||
False, "-b", help="Support broadcast messages"
|
||||
),
|
||||
repl: bool = typer.Option(True, help="Enable/Disable repl for server"),
|
||||
verbose: bool = typer.Option(
|
||||
False, help="Run with debug logs enabled for pymodbus"
|
||||
),
|
||||
):
|
||||
"""Run server code."""
|
||||
log_level = logging.DEBUG if verbose else logging.ERROR
|
||||
pymodbus_apply_logging_config(log_level)
|
||||
|
||||
ctx.obj = {
|
||||
"repl": repl,
|
||||
"host": host,
|
||||
"web_port": web_port,
|
||||
"broadcast_enable": broadcast_support,
|
||||
}
|
||||
|
||||
|
||||
@app.command("run", context_settings=CONTEXT_SETTING)
|
||||
def run(
|
||||
ctx: typer.Context,
|
||||
modbus_server: str = typer.Option(
|
||||
ModbusServerTypes.tcp.value,
|
||||
"--modbus-server",
|
||||
"-s",
|
||||
case_sensitive=False,
|
||||
autocompletion=servers,
|
||||
help="Modbus Server",
|
||||
),
|
||||
modbus_framer: str = typer.Option(
|
||||
ModbusFramerTypes.socket.value,
|
||||
"--framer",
|
||||
"-f",
|
||||
case_sensitive=False,
|
||||
autocompletion=framers,
|
||||
help="Modbus framer to use",
|
||||
),
|
||||
modbus_port: str = typer.Option("5020", "--modbus-port", "-p", help="Modbus port"),
|
||||
modbus_slave_id: List[int] = typer.Option(
|
||||
[1], "--slave-id", "-u", help="Supported Modbus slave id's"
|
||||
),
|
||||
modbus_config_path: Path = typer.Option(
|
||||
None, help="Path to additional modbus server config"
|
||||
),
|
||||
randomize: int = typer.Option(
|
||||
0,
|
||||
"--random",
|
||||
"-r",
|
||||
help="Randomize every `r` reads. 0=never, 1=always,2=every-second-read"
|
||||
", and so on. Applicable IR and DI.",
|
||||
),
|
||||
change_rate: int = typer.Option(
|
||||
0,
|
||||
"--change-rate",
|
||||
"-c",
|
||||
help="Rate in % registers to change. 0=none, 100=all, 12=12% of registers"
|
||||
", and so on. Applicable IR and DI.",
|
||||
),
|
||||
):
|
||||
"""Run Reactive Modbus server.
|
||||
|
||||
Exposing REST endpoint for response manipulation.
|
||||
"""
|
||||
repl = ctx.obj.pop("repl")
|
||||
# TBD extra_args = ctx.args
|
||||
web_app_config = ctx.obj
|
||||
loop = asyncio.get_event_loop()
|
||||
framer = DEFAULT_FRAMER.get(modbus_framer, ModbusSocketFramer)
|
||||
if modbus_config_path:
|
||||
with open(modbus_config_path, encoding="utf-8") as my_file:
|
||||
modbus_config = json.load(my_file)
|
||||
else:
|
||||
modbus_config = DEFAULT_CONFIG
|
||||
|
||||
extra_args = ctx.args
|
||||
data_block_settings = modbus_config.pop("data_block_settings", {})
|
||||
modbus_config = modbus_config.get(modbus_server, {})
|
||||
modbus_config = process_extra_args(extra_args, modbus_config)
|
||||
|
||||
modbus_config["randomize"] = randomize
|
||||
modbus_config["change_rate"] = change_rate
|
||||
|
||||
async def _wrapper():
|
||||
app = ReactiveServer.factory(
|
||||
modbus_server,
|
||||
framer,
|
||||
modbus_port=modbus_port,
|
||||
slave=modbus_slave_id,
|
||||
single=False,
|
||||
data_block_settings=data_block_settings,
|
||||
**web_app_config,
|
||||
**modbus_config,
|
||||
)
|
||||
await app.run_async(repl)
|
||||
return app
|
||||
|
||||
app = loop.run_until_complete(_wrapper())
|
||||
if repl:
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
loop.run_until_complete(run_repl(app))
|
||||
|
||||
else:
|
||||
loop.run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
Reference in New Issue
Block a user