initial
This commit is contained in:
1
env/lib/python3.11/site-packages/pymodbus/repl/__init__.py
vendored
Normal file
1
env/lib/python3.11/site-packages/pymodbus/repl/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Pymodbus REPL Module."""
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/repl/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
1
env/lib/python3.11/site-packages/pymodbus/repl/client/__init__.py
vendored
Normal file
1
env/lib/python3.11/site-packages/pymodbus/repl/client/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Repl client."""
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/completer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/completer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/helper.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/helper.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/main.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/main.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/mclient.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/repl/client/__pycache__/mclient.cpython-311.pyc
vendored
Normal file
Binary file not shown.
143
env/lib/python3.11/site-packages/pymodbus/repl/client/completer.py
vendored
Normal file
143
env/lib/python3.11/site-packages/pymodbus/repl/client/completer.py
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Command Completion for pymodbus REPL."""
|
||||
from prompt_toolkit.application.current import get_app
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.filters import Condition
|
||||
from prompt_toolkit.styles import Style
|
||||
|
||||
from pymodbus.repl.client.helper import get_commands
|
||||
|
||||
|
||||
@Condition
|
||||
def has_selected_completion():
|
||||
"""Check for selected completion."""
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
return complete_state is not None and complete_state.current_completion is not None
|
||||
|
||||
|
||||
style = Style.from_dict(
|
||||
{
|
||||
"completion-menu.completion": "bg:#008888 #ffffff",
|
||||
"completion-menu.completion.current": "bg:#00aaaa #000000",
|
||||
"scrollbar.background": "bg:#88aaaa",
|
||||
"scrollbar.button": "bg:#222222",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CmdCompleter(Completer):
|
||||
"""Completer for Pymodbus REPL."""
|
||||
|
||||
def __init__(self, client=None, commands=None, ignore_case=True):
|
||||
"""Initialize.
|
||||
|
||||
:param client: Modbus Client
|
||||
:param commands: Commands to be added for Completion (list)
|
||||
:param ignore_case: Ignore Case while looking up for commands
|
||||
"""
|
||||
self._commands = commands or get_commands(client)
|
||||
self._commands["help"] = ""
|
||||
self._command_names = self._commands.keys()
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
@property
|
||||
def commands(self):
|
||||
"""Return commands."""
|
||||
return self._commands
|
||||
|
||||
@property
|
||||
def command_names(self):
|
||||
"""Return command names."""
|
||||
return self._commands.keys()
|
||||
|
||||
def completing_command(self, words, word_before_cursor):
|
||||
"""Determine if we are dealing with supported command.
|
||||
|
||||
:param words: Input text broken in to word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return:
|
||||
"""
|
||||
return len(words) == 1 and len(word_before_cursor)
|
||||
|
||||
def completing_arg(self, words, word_before_cursor):
|
||||
"""Determine if we are currently completing an argument.
|
||||
|
||||
:param words: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: Specifies whether we are currently completing an arg.
|
||||
"""
|
||||
return len(words) > 1 and len(word_before_cursor)
|
||||
|
||||
def arg_completions(self, words, _word_before_cursor):
|
||||
"""Generate arguments completions based on the input."""
|
||||
cmd = words[0].strip()
|
||||
cmd = self._commands.get(cmd, None)
|
||||
return cmd if cmd else None
|
||||
|
||||
def _get_completions(self, word, word_before_cursor):
|
||||
"""Get completions."""
|
||||
if self.ignore_case:
|
||||
word_before_cursor = word_before_cursor.lower()
|
||||
return self.word_matches(word, word_before_cursor)
|
||||
|
||||
def word_matches(self, word, word_before_cursor):
|
||||
"""Match the word and word before cursor.
|
||||
|
||||
:param word: The input text broken into word tokens.
|
||||
:param word_before_cursor: The current word before the cursor, \
|
||||
which might be one or more blank spaces.
|
||||
:return: True if matched.
|
||||
|
||||
"""
|
||||
if self.ignore_case:
|
||||
word = word.lower()
|
||||
return word.startswith(word_before_cursor)
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
"""Get completions for the current scope.
|
||||
|
||||
:param document: An instance of `prompt_toolkit.Document`.
|
||||
:param complete_event: (Unused).
|
||||
:return: Yields an instance of `prompt_toolkit.completion.Completion`.
|
||||
"""
|
||||
word_before_cursor = document.get_word_before_cursor(WORD=True)
|
||||
text = document.text_before_cursor.lstrip()
|
||||
words = document.text.strip().split()
|
||||
meta = None
|
||||
commands = []
|
||||
if not words:
|
||||
# yield commands
|
||||
pass
|
||||
if self.completing_command(words, word_before_cursor):
|
||||
commands = self._command_names
|
||||
c_meta = {
|
||||
k: v.help_text if not isinstance(v, str) else v
|
||||
for k, v in self._commands.items()
|
||||
}
|
||||
meta = lambda x: ( # pylint: disable=unnecessary-lambda-assignment
|
||||
x,
|
||||
c_meta.get(x, ""),
|
||||
)
|
||||
else:
|
||||
if not list(
|
||||
filter(lambda cmd: any(x == cmd for x in words), self._command_names)
|
||||
):
|
||||
# yield commands
|
||||
pass
|
||||
|
||||
if " " in text:
|
||||
command = self.arg_completions(words, word_before_cursor)
|
||||
commands = list(command.get_completion())
|
||||
commands = list(
|
||||
filter(lambda cmd: not (any(cmd in x for x in words)), commands)
|
||||
)
|
||||
meta = command.get_meta
|
||||
for command in commands:
|
||||
if self._get_completions(command, word_before_cursor):
|
||||
_, display_meta = meta(command) if meta else ("", "")
|
||||
yield Completion(
|
||||
command, -len(word_before_cursor), display_meta=display_meta
|
||||
)
|
||||
312
env/lib/python3.11/site-packages/pymodbus/repl/client/helper.py
vendored
Normal file
312
env/lib/python3.11/site-packages/pymodbus/repl/client/helper.py
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
"""Helper Module for REPL actions."""
|
||||
import inspect
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
import pygments
|
||||
from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.formatted_text import HTML, PygmentsTokens
|
||||
from pygments.lexers.data import JsonLexer
|
||||
|
||||
from pymodbus.payload import BinaryPayloadDecoder, Endian
|
||||
|
||||
|
||||
predicate = inspect.isfunction
|
||||
argspec = inspect.signature
|
||||
|
||||
|
||||
FORMATTERS = {
|
||||
"int8": "decode_8bit_int",
|
||||
"int16": "decode_16bit_int",
|
||||
"int32": "decode_32bit_int",
|
||||
"int64": "decode_64bit_int",
|
||||
"uint8": "decode_8bit_uint",
|
||||
"uint16": "decode_16bit_uint",
|
||||
"uint32": "decode_32bit_uint",
|
||||
"uint64": "decode_64bit_int",
|
||||
"float16": "decode_16bit_float",
|
||||
"float32": "decode_32bit_float",
|
||||
"float64": "decode_64bit_float",
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_KWARGS = {"slave": "Slave address"}
|
||||
|
||||
OTHER_COMMANDS = {
|
||||
"result.raw": "Show RAW Result",
|
||||
"result.decode": "Decode register response to known formats",
|
||||
}
|
||||
EXCLUDE = ["execute", "recv", "send", "trace", "set_debug"]
|
||||
CLIENT_METHODS = [
|
||||
"connect",
|
||||
"close",
|
||||
"idle_time",
|
||||
"is_socket_open",
|
||||
"get_port",
|
||||
"set_port",
|
||||
"get_stopbits",
|
||||
"set_stopbits",
|
||||
"get_bytesize",
|
||||
"set_bytesize",
|
||||
"get_parity",
|
||||
"set_parity",
|
||||
"get_baudrate",
|
||||
"set_baudrate",
|
||||
"get_timeout",
|
||||
"set_timeout",
|
||||
"get_serial_settings",
|
||||
]
|
||||
CLIENT_ATTRIBUTES: List[str] = []
|
||||
|
||||
|
||||
class Command:
|
||||
"""Class representing Commands to be consumed by Completer."""
|
||||
|
||||
def __init__(self, name, signature, doc, slave=False):
|
||||
"""Initialize.
|
||||
|
||||
:param name: Name of the command
|
||||
:param signature: inspect object
|
||||
:param doc: Doc string for the command
|
||||
:param slave: Use slave as additional argument in the command .
|
||||
"""
|
||||
self.name = name
|
||||
self.doc = doc.split("\n") if doc else " ".join(name.split("_"))
|
||||
self.help_text = self._create_help()
|
||||
self.param_help = self._create_arg_help()
|
||||
if signature:
|
||||
self._params = signature.parameters
|
||||
self.args = self.create_completion()
|
||||
else:
|
||||
self._params = ""
|
||||
|
||||
if self.name.startswith("client.") and slave:
|
||||
self.args.update(**DEFAULT_KWARGS)
|
||||
|
||||
def _create_help(self):
|
||||
"""Create help."""
|
||||
doc = filter(lambda d: d, self.doc)
|
||||
cmd_help = list(
|
||||
filter(
|
||||
lambda x: not x.startswith(":param") and not x.startswith(":return"),
|
||||
doc,
|
||||
)
|
||||
)
|
||||
return " ".join(cmd_help).strip()
|
||||
|
||||
def _create_arg_help(self):
|
||||
"""Create arg help."""
|
||||
param_dict = {}
|
||||
params = list(filter(lambda d: d.strip().startswith(":param"), self.doc))
|
||||
for param in params:
|
||||
param, param_help = param.split(":param")[1].strip().split(":")
|
||||
param_dict[param] = param_help
|
||||
return param_dict
|
||||
|
||||
def create_completion(self):
|
||||
"""Create command completion meta data.
|
||||
|
||||
:return:
|
||||
"""
|
||||
words = {}
|
||||
|
||||
def _create(entry, default):
|
||||
if entry not in ["self", "kwargs"]:
|
||||
if isinstance(default, (int, str)):
|
||||
entry += f"={default}"
|
||||
return entry
|
||||
return None
|
||||
|
||||
for arg in self._params.values():
|
||||
if entry := _create(arg.name, arg.default):
|
||||
entry, meta = self.get_meta(entry)
|
||||
words[entry] = meta
|
||||
|
||||
return words
|
||||
|
||||
def get_completion(self):
|
||||
"""Get a list of completions.
|
||||
|
||||
:return:
|
||||
"""
|
||||
return self.args.keys()
|
||||
|
||||
def get_meta(self, cmd):
|
||||
"""Get Meta info of a given command.
|
||||
|
||||
:param cmd: Name of command.
|
||||
:return: Dict containing meta info.
|
||||
"""
|
||||
cmd = cmd.strip()
|
||||
cmd = cmd.split("=")[0].strip()
|
||||
return cmd, self.param_help.get(cmd, "")
|
||||
|
||||
def __str__(self):
|
||||
"""Return string representation."""
|
||||
if self.doc:
|
||||
return f"Command {self.name:>50}{self.doc:<20}"
|
||||
return f"Command {self.name}"
|
||||
|
||||
|
||||
def _get_requests(members):
|
||||
"""Get requests."""
|
||||
commands = list(
|
||||
filter(
|
||||
lambda x: (
|
||||
x[0] not in EXCLUDE and x[0] not in CLIENT_METHODS and callable(x[1])
|
||||
),
|
||||
members,
|
||||
)
|
||||
)
|
||||
commands = {
|
||||
f"client.{c[0]}": Command(
|
||||
f"client.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]), slave=False
|
||||
)
|
||||
for c in commands
|
||||
if not c[0].startswith("_")
|
||||
}
|
||||
return commands
|
||||
|
||||
|
||||
def _get_client_methods(members):
|
||||
"""Get client methods."""
|
||||
commands = list(
|
||||
filter(lambda x: (x[0] not in EXCLUDE and x[0] in CLIENT_METHODS), members)
|
||||
)
|
||||
commands = {
|
||||
f"client.{c[0]}": Command(
|
||||
f"client.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]), slave=False
|
||||
)
|
||||
for c in commands
|
||||
if not c[0].startswith("_")
|
||||
}
|
||||
return commands
|
||||
|
||||
|
||||
def _get_client_properties(members):
|
||||
"""Get client properties."""
|
||||
global CLIENT_ATTRIBUTES # pylint: disable=global-variable-not-assigned
|
||||
commands = list(filter(lambda x: not callable(x[1]), members))
|
||||
commands = {
|
||||
f"client.{c[0]}": Command(f"client.{c[0]}", None, "Read Only!", slave=False)
|
||||
for c in commands
|
||||
if (not c[0].startswith("_") and isinstance(c[1], (str, int, float)))
|
||||
}
|
||||
CLIENT_ATTRIBUTES.extend(list(commands.keys()))
|
||||
return commands
|
||||
|
||||
|
||||
def get_commands(client):
|
||||
"""Retrieve all required methods and attributes.
|
||||
|
||||
Of a client object and convert it to commands.
|
||||
|
||||
:param client: Modbus Client object.
|
||||
:return:
|
||||
"""
|
||||
commands = {}
|
||||
members = inspect.getmembers(client)
|
||||
requests = _get_requests(members)
|
||||
client_methods = _get_client_methods(members)
|
||||
client_attr = _get_client_properties(members)
|
||||
|
||||
result_commands = inspect.getmembers(Result, predicate=predicate)
|
||||
result_commands = {
|
||||
f"result.{c[0]}": Command(f"result.{c[0]}", argspec(c[1]), inspect.getdoc(c[1]))
|
||||
for c in result_commands
|
||||
if (not c[0].startswith("_") and c[0] != "print_result")
|
||||
}
|
||||
commands.update(requests)
|
||||
commands.update(client_methods)
|
||||
commands.update(client_attr)
|
||||
commands.update(result_commands)
|
||||
return commands
|
||||
|
||||
|
||||
class Result:
|
||||
"""Represent result command."""
|
||||
|
||||
function_code: int = None
|
||||
data: Union[Dict[int, Any], Any] = None
|
||||
|
||||
def __init__(self, result):
|
||||
"""Initialize.
|
||||
|
||||
:param result: Response of a modbus command.
|
||||
"""
|
||||
if isinstance(result, dict): # Modbus response
|
||||
self.function_code = result.pop("function_code", None)
|
||||
self.data = dict(result)
|
||||
else:
|
||||
self.data = result
|
||||
|
||||
def decode(self, formatters, byte_order="big", word_order="big"):
|
||||
"""Decode the register response to known formatters.
|
||||
|
||||
:param formatters: int8/16/32/64, uint8/16/32/64, float32/64
|
||||
:param byte_order: little/big
|
||||
:param word_order: little/big
|
||||
"""
|
||||
# Read Holding Registers (3)
|
||||
# Read Input Registers (4)
|
||||
# Read Write Registers (23)
|
||||
if not isinstance(formatters, (list, tuple)):
|
||||
formatters = [formatters]
|
||||
|
||||
if self.function_code not in [3, 4, 23]:
|
||||
print_formatted_text(HTML("<red>Decoder works only for registers!!</red>"))
|
||||
return
|
||||
byte_order = (
|
||||
Endian.LITTLE if byte_order.strip().lower() == "little" else Endian.BIG
|
||||
)
|
||||
word_order = (
|
||||
Endian.LITTLE if word_order.strip().lower() == "little" else Endian.BIG
|
||||
)
|
||||
decoder = BinaryPayloadDecoder.fromRegisters(
|
||||
self.data.get("registers"), byteorder=byte_order, wordorder=word_order
|
||||
)
|
||||
for formatter in formatters:
|
||||
if not (formatter := FORMATTERS.get(formatter)):
|
||||
print_formatted_text(
|
||||
HTML(f"<red>Invalid Formatter - {formatter}!!</red>")
|
||||
)
|
||||
return
|
||||
decoded = getattr(decoder, formatter)()
|
||||
self.print_result(decoded)
|
||||
|
||||
def raw(self):
|
||||
"""Return raw result dict."""
|
||||
self.print_result()
|
||||
|
||||
def _process_dict(self, use_dict):
|
||||
"""Process dict."""
|
||||
new_dict = OrderedDict()
|
||||
for k, v_item in use_dict.items():
|
||||
if isinstance(v_item, bytes):
|
||||
v_item = v_item.decode("utf-8")
|
||||
elif isinstance(v_item, dict):
|
||||
v_item = self._process_dict(v_item)
|
||||
elif isinstance(v_item, (list, tuple)):
|
||||
v_item = [
|
||||
v1.decode("utf-8") if isinstance(v1, bytes) else v1 for v1 in v_item
|
||||
]
|
||||
new_dict[k] = v_item
|
||||
return new_dict
|
||||
|
||||
def print_result(self, data=None):
|
||||
"""Print result object pretty.
|
||||
|
||||
:param data: Data to be printed.
|
||||
"""
|
||||
data = data or self.data
|
||||
if isinstance(data, dict):
|
||||
data = self._process_dict(data)
|
||||
elif isinstance(data, (list, tuple)):
|
||||
data = [v.decode("utf-8") if isinstance(v, bytes) else v for v in data]
|
||||
elif isinstance(data, bytes):
|
||||
data = data.decode("utf-8")
|
||||
tokens = list(pygments.lex(json.dumps(data, indent=4), lexer=JsonLexer()))
|
||||
print_formatted_text(PygmentsTokens(tokens))
|
||||
437
env/lib/python3.11/site-packages/pymodbus/repl/client/main.py
vendored
Normal file
437
env/lib/python3.11/site-packages/pymodbus/repl/client/main.py
vendored
Normal file
@@ -0,0 +1,437 @@
|
||||
"""Pymodbus REPL Entry point."""
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
from prompt_toolkit import PromptSession, print_formatted_text
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.lexers import PygmentsLexer
|
||||
from prompt_toolkit.styles import Style
|
||||
from pygments.lexers.python import PythonLexer
|
||||
|
||||
from pymodbus import __version__ as pymodbus_version
|
||||
from pymodbus.exceptions import ParameterException
|
||||
from pymodbus.repl.client.completer import (
|
||||
CmdCompleter,
|
||||
has_selected_completion,
|
||||
)
|
||||
from pymodbus.repl.client.helper import CLIENT_ATTRIBUTES, Result
|
||||
from pymodbus.repl.client.mclient import ModbusSerialClient, ModbusTcpClient
|
||||
from pymodbus.transaction import (
|
||||
ModbusAsciiFramer,
|
||||
ModbusBinaryFramer,
|
||||
ModbusRtuFramer,
|
||||
ModbusSocketFramer,
|
||||
)
|
||||
|
||||
|
||||
_logger = logging.getLogger()
|
||||
|
||||
TITLE = rf"""
|
||||
----------------------------------------------------------------------------
|
||||
__________ _____ .___ __________ .__
|
||||
\______ \___.__. / \ ____ __| _/ \______ \ ____ ______ | |
|
||||
| ___< | |/ \ / \ / _ \ / __ | | _// __ \\\____ \| |
|
||||
| | \___ / Y ( <_> ) /_/ | | | \ ___/| |_> > |__
|
||||
|____| / ____\____|__ /\____/\____ | /\ |____|_ /\___ > __/|____/
|
||||
\/ \/ \/ \/ \/ \/|__|
|
||||
v1.3.0 - {pymodbus_version}
|
||||
----------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
|
||||
style = Style.from_dict(
|
||||
{
|
||||
"completion-menu.completion": "bg:#008888 #ffffff",
|
||||
"completion-menu.completion.current": "bg:#00aaaa #000000",
|
||||
"scrollbar.background": "bg:#88aaaa",
|
||||
"scrollbar.button": "bg:#222222",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def bottom_toolbar():
|
||||
"""Do console toolbar.
|
||||
|
||||
:return:
|
||||
"""
|
||||
return HTML(
|
||||
'Press <b><style bg="ansired">CTRL+D or exit </style></b>'
|
||||
' to exit! Type "help" for list of available commands'
|
||||
)
|
||||
|
||||
|
||||
class CaseInsenstiveChoice(click.Choice):
|
||||
"""Do case Insensitive choice for click commands and options."""
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
"""Convert args to uppercase for evaluation."""
|
||||
if value is None:
|
||||
return None
|
||||
return super().convert(value.strip().upper(), param, ctx)
|
||||
|
||||
|
||||
class NumericChoice(click.Choice):
|
||||
"""Do numeric choice for click arguments and options."""
|
||||
|
||||
def __init__(self, choices, typ):
|
||||
"""Initialize."""
|
||||
self.typ = typ
|
||||
super().__init__(choices)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
"""Convert."""
|
||||
# Exact match
|
||||
if value in self.choices:
|
||||
return self.typ(value)
|
||||
|
||||
if ctx is not None and ctx.token_normalize_func is not None:
|
||||
value = ctx.token_normalize_func(value)
|
||||
for choice in self.casted_choices: # pylint: disable=no-member
|
||||
if ctx.token_normalize_func(choice) == value:
|
||||
return choice
|
||||
|
||||
self.fail(
|
||||
f"invalid choice: {value}. (choose from {', '.join(self.choices)})",
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _process_args(args: list, string: bool = True):
|
||||
"""Parse arguments provided on command line.
|
||||
|
||||
:param args: Array of argument values
|
||||
:param string: True if arguments values are strings, false if argument values are integers
|
||||
|
||||
:return Tuple, where the first member is hash of parsed values, and second is boolean flag
|
||||
indicating if parsing succeeded.
|
||||
"""
|
||||
kwargs = {}
|
||||
execute = True
|
||||
skip_index = None
|
||||
|
||||
def _parse_val(arg_name, val):
|
||||
if not string:
|
||||
if "," in val:
|
||||
val = val.split(",")
|
||||
val = [int(v, 0) for v in val]
|
||||
else:
|
||||
val = int(val, 0)
|
||||
kwargs[arg_name] = val
|
||||
|
||||
for i, arg in enumerate(args):
|
||||
if i == skip_index:
|
||||
continue
|
||||
arg = arg.strip()
|
||||
if "=" in arg:
|
||||
arg_name, val = arg.split("=")
|
||||
_parse_val(arg_name, val)
|
||||
else:
|
||||
arg_name, val = arg, args[i + 1]
|
||||
try:
|
||||
_parse_val(arg_name, val)
|
||||
skip_index = i + 1
|
||||
except TypeError:
|
||||
click.secho("Error parsing arguments!", fg="yellow")
|
||||
execute = False
|
||||
break
|
||||
except ValueError:
|
||||
click.secho("Error parsing argument", fg="yellow")
|
||||
execute = False
|
||||
break
|
||||
return kwargs, execute
|
||||
|
||||
|
||||
class CLI: # pylint: disable=too-few-public-methods
|
||||
"""Client definition."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Set up client and keybindings."""
|
||||
|
||||
use_keys = KeyBindings()
|
||||
history_file = pathlib.Path.home().joinpath(".pymodhis")
|
||||
self.client = client
|
||||
|
||||
@use_keys.add("c-space")
|
||||
def _(event):
|
||||
"""Initialize autocompletion, or select the next completion."""
|
||||
buff = event.app.current_buffer
|
||||
if buff.complete_state:
|
||||
buff.complete_next()
|
||||
else:
|
||||
buff.start_completion(select_first=False)
|
||||
|
||||
@use_keys.add("enter", filter=has_selected_completion)
|
||||
def _(event):
|
||||
"""Make the enter key work as the tab key only when showing the menu."""
|
||||
event.current_buffer.complete_state = None
|
||||
buffer = event.cli.current_buffer
|
||||
buffer.complete_state = None
|
||||
|
||||
self.session = PromptSession(
|
||||
lexer=PygmentsLexer(PythonLexer),
|
||||
completer=CmdCompleter(client),
|
||||
style=style,
|
||||
complete_while_typing=True,
|
||||
bottom_toolbar=bottom_toolbar,
|
||||
key_bindings=use_keys,
|
||||
history=FileHistory(history_file),
|
||||
auto_suggest=AutoSuggestFromHistory(),
|
||||
)
|
||||
click.secho(TITLE, fg="green")
|
||||
|
||||
def _print_command_help(self, commands):
|
||||
"""Print a list of commands with help text."""
|
||||
for cmd, obj in sorted(commands.items()):
|
||||
if cmd != "help":
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
f"<skyblue>{cmd:45s}</skyblue>"
|
||||
f"<seagreen>{obj.help_text:100s}"
|
||||
"</seagreen>"
|
||||
)
|
||||
)
|
||||
|
||||
def _process_client(self, text, client) -> Result:
|
||||
"""Process client commands."""
|
||||
text = text.strip().split()
|
||||
cmd = text[0].split(".")[1]
|
||||
args = text[1:]
|
||||
kwargs, execute = _process_args(args, string=False)
|
||||
if execute:
|
||||
if text[0] in CLIENT_ATTRIBUTES:
|
||||
result = Result(getattr(client, cmd))
|
||||
else:
|
||||
result = Result(getattr(client, cmd)(**kwargs))
|
||||
result.print_result()
|
||||
return result
|
||||
|
||||
def _process_result(self, text, result):
|
||||
"""Process result commands."""
|
||||
words = text.split()
|
||||
if words[0] == "result.raw":
|
||||
result.raw()
|
||||
if words[0] == "result.decode":
|
||||
args = words[1:]
|
||||
kwargs, execute = _process_args(args)
|
||||
if execute:
|
||||
result.decode(**kwargs)
|
||||
|
||||
def run(self):
|
||||
"""Run the REPL."""
|
||||
result = None
|
||||
while True:
|
||||
try:
|
||||
text = self.session.prompt("> ", complete_while_typing=True)
|
||||
if text.strip().lower() == "help":
|
||||
print_formatted_text(HTML("<u>Available commands:</u>"))
|
||||
self._print_command_help(self.session.completer.commands)
|
||||
elif text.strip().lower() == "exit":
|
||||
raise EOFError()
|
||||
elif text.strip().lower().startswith("client."):
|
||||
result = self._process_client(text, self.client)
|
||||
elif text.strip().lower().startswith("result.") and result:
|
||||
self._process_result(text, result)
|
||||
except KeyboardInterrupt:
|
||||
continue # Control-C pressed. Try again.
|
||||
except EOFError:
|
||||
break # Control-D pressed.
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
click.secho(str(exc), fg="red")
|
||||
|
||||
click.secho("GoodBye!", fg="blue")
|
||||
|
||||
|
||||
@click.group("pymodbus-repl")
|
||||
@click.version_option(str(pymodbus_version), message=TITLE)
|
||||
@click.option("--verbose", is_flag=True, default=False, help="Verbose logs")
|
||||
@click.option(
|
||||
"--broadcast-support",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Support broadcast messages",
|
||||
)
|
||||
@click.option(
|
||||
"--retry-on-empty", is_flag=True, default=False, help="Retry on empty response"
|
||||
)
|
||||
@click.option(
|
||||
"--retry-on-error", is_flag=True, default=False, help="Retry on error response"
|
||||
)
|
||||
@click.option("--retries", default=3, help="Retry count")
|
||||
@click.pass_context
|
||||
def main(
|
||||
ctx,
|
||||
verbose,
|
||||
broadcast_support,
|
||||
retry_on_empty,
|
||||
retry_on_error,
|
||||
retries,
|
||||
):
|
||||
"""Run Main."""
|
||||
if verbose:
|
||||
use_format = (
|
||||
"%(asctime)-15s %(threadName)-15s "
|
||||
"%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s"
|
||||
)
|
||||
logging.basicConfig(format=use_format)
|
||||
_logger.setLevel(logging.DEBUG)
|
||||
ctx.obj = {
|
||||
"broadcast_enable": broadcast_support,
|
||||
"retry_on_empty": retry_on_empty,
|
||||
"retry_on_invalid": retry_on_error,
|
||||
"retries": retries,
|
||||
}
|
||||
|
||||
|
||||
@main.command("tcp")
|
||||
@click.pass_context
|
||||
@click.option("--host", default="localhost", help="Modbus TCP IP ")
|
||||
@click.option(
|
||||
"--port",
|
||||
default=502,
|
||||
type=int,
|
||||
help="Modbus TCP port",
|
||||
)
|
||||
@click.option(
|
||||
"--framer",
|
||||
default="tcp",
|
||||
type=str,
|
||||
help="Override the default packet framer tcp|rtu",
|
||||
)
|
||||
def tcp(ctx, host, port, framer):
|
||||
"""Define TCP."""
|
||||
kwargs = {"host": host, "port": port}
|
||||
kwargs.update(**ctx.obj)
|
||||
if framer == "rtu":
|
||||
kwargs["framer"] = ModbusRtuFramer
|
||||
client = ModbusTcpClient(**kwargs)
|
||||
cli = CLI(client)
|
||||
cli.run()
|
||||
|
||||
|
||||
@main.command("serial")
|
||||
@click.pass_context
|
||||
@click.option(
|
||||
"--method",
|
||||
default="rtu",
|
||||
type=str,
|
||||
help="Modbus Serial Mode (rtu/ascii)",
|
||||
)
|
||||
@click.option(
|
||||
"--port",
|
||||
default=None,
|
||||
type=str,
|
||||
help="Modbus RTU port",
|
||||
)
|
||||
@click.option(
|
||||
"--baudrate",
|
||||
help="Modbus RTU serial baudrate to use.",
|
||||
default=9600,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--bytesize",
|
||||
help="Modbus RTU serial Number of data bits. "
|
||||
"Possible values: FIVEBITS, SIXBITS, SEVENBITS, "
|
||||
"EIGHTBITS.",
|
||||
type=NumericChoice(["5", "6", "7", "8"], int),
|
||||
default="8",
|
||||
)
|
||||
@click.option(
|
||||
"--parity",
|
||||
help="Modbus RTU serial parity. "
|
||||
" Enable parity checking. Possible values: "
|
||||
"PARITY_NONE, PARITY_EVEN, PARITY_ODD PARITY_MARK, "
|
||||
'PARITY_SPACE. Default to "N"',
|
||||
default="N",
|
||||
type=CaseInsenstiveChoice(["N", "E", "O", "M", "S"]),
|
||||
)
|
||||
@click.option(
|
||||
"--stopbits",
|
||||
help="Modbus RTU serial stop bits. "
|
||||
"Number of stop bits. Possible values: STOPBITS_ONE, "
|
||||
'STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO. Default to "1"',
|
||||
default="1",
|
||||
type=NumericChoice(["1", "1.5", "2"], float),
|
||||
)
|
||||
@click.option(
|
||||
"--xonxoff",
|
||||
help="Modbus RTU serial xonxoff. Enable software flow control.",
|
||||
default=0,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--rtscts",
|
||||
help="Modbus RTU serial rtscts. Enable hardware (RTS/CTS) flow " "control.",
|
||||
default=0,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--dsrdtr",
|
||||
help="Modbus RTU serial dsrdtr. Enable hardware (DSR/DTR) flow " "control.",
|
||||
default=0,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--timeout",
|
||||
help="Modbus RTU serial read timeout.",
|
||||
default=0.25,
|
||||
type=float,
|
||||
)
|
||||
@click.option(
|
||||
"--write-timeout",
|
||||
help="Modbus RTU serial write timeout.",
|
||||
default=2,
|
||||
type=float,
|
||||
)
|
||||
def serial( # pylint: disable=too-many-arguments
|
||||
ctx,
|
||||
method,
|
||||
port,
|
||||
baudrate,
|
||||
bytesize,
|
||||
parity,
|
||||
stopbits,
|
||||
xonxoff,
|
||||
rtscts,
|
||||
dsrdtr,
|
||||
timeout,
|
||||
write_timeout,
|
||||
):
|
||||
"""Define serial communication."""
|
||||
method = method.lower()
|
||||
if method == "ascii":
|
||||
framer = ModbusAsciiFramer
|
||||
elif method == "rtu":
|
||||
framer = ModbusRtuFramer
|
||||
elif method == "binary":
|
||||
framer = ModbusBinaryFramer
|
||||
elif method == "socket":
|
||||
framer = ModbusSocketFramer
|
||||
else:
|
||||
raise ParameterException("Invalid framer method requested")
|
||||
client = ModbusSerialClient(
|
||||
framer=framer,
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=bytesize,
|
||||
parity=parity,
|
||||
stopbits=stopbits,
|
||||
xonxoff=xonxoff,
|
||||
rtscts=rtscts,
|
||||
dsrdtr=dsrdtr,
|
||||
timeout=timeout,
|
||||
write_timeout=write_timeout,
|
||||
**ctx.obj,
|
||||
)
|
||||
cli = CLI(client)
|
||||
cli.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
683
env/lib/python3.11/site-packages/pymodbus/repl/client/mclient.py
vendored
Normal file
683
env/lib/python3.11/site-packages/pymodbus/repl/client/mclient.py
vendored
Normal file
@@ -0,0 +1,683 @@
|
||||
"""Modbus Clients to be used with REPL."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import functools
|
||||
|
||||
from pymodbus.client import ModbusSerialClient as _ModbusSerialClient
|
||||
from pymodbus.client import ModbusTcpClient as _ModbusTcpClient
|
||||
from pymodbus.diag_message import (
|
||||
ChangeAsciiInputDelimiterRequest,
|
||||
ClearCountersRequest,
|
||||
ClearOverrunCountRequest,
|
||||
ForceListenOnlyModeRequest,
|
||||
GetClearModbusPlusRequest,
|
||||
RestartCommunicationsOptionRequest,
|
||||
ReturnBusCommunicationErrorCountRequest,
|
||||
ReturnBusExceptionErrorCountRequest,
|
||||
ReturnBusMessageCountRequest,
|
||||
ReturnDiagnosticRegisterRequest,
|
||||
ReturnIopOverrunCountRequest,
|
||||
ReturnQueryDataRequest,
|
||||
ReturnSlaveBusCharacterOverrunCountRequest,
|
||||
ReturnSlaveBusyCountRequest,
|
||||
ReturnSlaveMessageCountRequest,
|
||||
ReturnSlaveNAKCountRequest,
|
||||
ReturnSlaveNoResponseCountRequest,
|
||||
)
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.mei_message import ReadDeviceInformationRequest
|
||||
from pymodbus.other_message import (
|
||||
GetCommEventCounterRequest,
|
||||
GetCommEventLogRequest,
|
||||
ReadExceptionStatusRequest,
|
||||
ReportSlaveIdRequest,
|
||||
)
|
||||
from pymodbus.pdu import ExceptionResponse, ModbusExceptions
|
||||
|
||||
|
||||
def make_response_dict(resp):
|
||||
"""Make response dict."""
|
||||
resp_dict = {"function_code": resp.function_code, "address": resp.address}
|
||||
if hasattr(resp, "value"):
|
||||
resp_dict["value"] = resp.value
|
||||
elif hasattr(resp, "values"):
|
||||
resp_dict["values"] = resp.values
|
||||
elif hasattr(resp, "count"):
|
||||
resp_dict["count"] = resp.count
|
||||
return resp_dict
|
||||
|
||||
|
||||
def handle_brodcast(func):
|
||||
"""Handle broadcast."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def _wrapper(*args, **kwargs):
|
||||
self = args[0]
|
||||
resp = func(*args, **kwargs)
|
||||
if not kwargs.get("slave") and self.params.broadcast_enable:
|
||||
return {"broadcasted": True}
|
||||
if not resp.isError():
|
||||
return make_response_dict(resp)
|
||||
return ExtendedRequestSupport._process_exception( # pylint: disable=protected-access
|
||||
resp, **kwargs
|
||||
)
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
class ExtendedRequestSupport: # pylint: disable=(too-many-public-methods
|
||||
"""Extended request support."""
|
||||
|
||||
@staticmethod
|
||||
def _process_exception(resp, **kwargs):
|
||||
"""Set internal process exception."""
|
||||
if "slave" not in kwargs:
|
||||
err = {"message": "Broadcast message, ignoring errors!!!"}
|
||||
else:
|
||||
if isinstance(resp, ExceptionResponse): # pylint: disable=else-if-used
|
||||
err = {
|
||||
"original_function_code": f"{resp.original_code} ({hex(resp.original_code)})",
|
||||
"error_function_code": f"{resp.function_code} ({hex(resp.function_code)})",
|
||||
"exception code": resp.exception_code,
|
||||
"message": ModbusExceptions.decode(resp.exception_code),
|
||||
}
|
||||
elif isinstance(resp, ModbusIOException):
|
||||
err = {
|
||||
"original_function_code": f"{resp.fcode} ({hex(resp.fcode)})",
|
||||
"error": resp.message,
|
||||
}
|
||||
else:
|
||||
err = {"error": str(resp)}
|
||||
return err
|
||||
|
||||
def read_coils(self, address, count=1, slave=0, **kwargs):
|
||||
"""Read `count` coils from a given slave starting at `address`.
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of coils to read
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:returns: List of register values
|
||||
"""
|
||||
resp = super().read_coils( # pylint: disable=no-member
|
||||
address, count, slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "bits": resp.bits}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def read_discrete_inputs(self, address, count=1, slave=0, **kwargs):
|
||||
"""Read `count` number of discrete inputs starting at offset `address`.
|
||||
|
||||
:param address: The starting address to read from
|
||||
:param count: The number of coils to read
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return: List of bits
|
||||
"""
|
||||
resp = super().read_discrete_inputs( # pylint: disable=no-member
|
||||
address, count, slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "bits": resp.bits}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
@handle_brodcast
|
||||
def write_coil(self, address, value, slave=0, **kwargs):
|
||||
"""Write `value` to coil at `address`.
|
||||
|
||||
:param address: coil offset to write to
|
||||
:param value: bit value to write
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().write_coil( # pylint: disable=no-member
|
||||
address, value, slave, **kwargs
|
||||
)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_coils(self, address, values, slave=0, **kwargs):
|
||||
"""Write `value` to coil at `address`.
|
||||
|
||||
:param address: coil offset to write to
|
||||
:param values: list of bit values to write (comma separated)
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().write_coils( # pylint: disable=no-member
|
||||
address, values, slave, **kwargs
|
||||
)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_register(self, address, value, slave=0, **kwargs):
|
||||
"""Write `value` to register at `address`.
|
||||
|
||||
:param address: register offset to write to
|
||||
:param value: register value to write
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().write_register( # pylint: disable=no-member
|
||||
address, value, slave, **kwargs
|
||||
)
|
||||
return resp
|
||||
|
||||
@handle_brodcast
|
||||
def write_registers(self, address, values, slave=0, **kwargs):
|
||||
"""Write list of `values` to registers starting at `address`.
|
||||
|
||||
:param address: register offset to write to
|
||||
:param values: list of register value to write (comma separated)
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().write_registers( # pylint: disable=no-member
|
||||
address, values, slave, **kwargs
|
||||
)
|
||||
return resp
|
||||
|
||||
def read_holding_registers(self, address, count=1, slave=0, **kwargs):
|
||||
"""Read `count` number of holding registers starting at `address`.
|
||||
|
||||
:param address: starting register offset to read from
|
||||
:param count: Number of registers to read
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().read_holding_registers( # pylint: disable=no-member
|
||||
address, count, slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "registers": resp.registers}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def read_input_registers(self, address, count=1, slave=0, **kwargs):
|
||||
"""Read `count` number of input registers starting at `address`.
|
||||
|
||||
:param address: starting register offset to read from to
|
||||
:param count: Number of registers to read
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().read_input_registers( # pylint: disable=no-member
|
||||
address, count, slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "registers": resp.registers}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def readwrite_registers(
|
||||
self,
|
||||
read_address=0,
|
||||
read_count=0,
|
||||
write_address=0,
|
||||
values=0,
|
||||
slave=0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Read `read_count` number of holding registers.
|
||||
|
||||
Starting at `read_address`
|
||||
and write `write_registers` starting at `write_address`.
|
||||
|
||||
:param read_address: register offset to read from
|
||||
:param read_count: Number of registers to read
|
||||
:param write_address: register offset to write to
|
||||
:param values: List of register values to write (comma separated)
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().readwrite_registers( # pylint: disable=no-member
|
||||
read_address=read_address,
|
||||
read_count=read_count,
|
||||
write_address=write_address,
|
||||
values=values,
|
||||
slave=slave,
|
||||
**kwargs,
|
||||
)
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "registers": resp.registers}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def mask_write_register(
|
||||
self,
|
||||
address=0x0000,
|
||||
and_mask=0xFFFF,
|
||||
or_mask=0x0000,
|
||||
slave=0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Mask content of holding register at `address` with `and_mask` and `or_mask`.
|
||||
|
||||
:param address: Reference address of register
|
||||
:param and_mask: And Mask
|
||||
:param or_mask: OR Mask
|
||||
:param slave: Modbus slave slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
resp = super().mask_write_register( # pylint: disable=no-member
|
||||
address=address, and_mask=and_mask, or_mask=or_mask, slave=slave, **kwargs
|
||||
)
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"address": resp.address,
|
||||
"and mask": resp.and_mask,
|
||||
"or mask": resp.or_mask,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def read_device_information(self, read_code=None, object_id=0x00, **kwargs):
|
||||
"""Read the identification and additional information of remote slave.
|
||||
|
||||
:param read_code: Read Device ID code (0x01/0x02/0x03/0x04)
|
||||
:param object_id: Identification of the first object to obtain.
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReadDeviceInformationRequest(read_code, object_id, **kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"information": resp.information,
|
||||
"object count": resp.number_of_objects,
|
||||
"conformity": resp.conformity,
|
||||
"next object id": resp.next_object_id,
|
||||
"more follows": resp.more_follows,
|
||||
"space left": resp.space_left,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def report_slave_id(self, slave=0, **kwargs):
|
||||
"""Report information about remote slave ID.
|
||||
|
||||
:param slave: Modbus slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReportSlaveIdRequest(slave, **kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"identifier": resp.identifier.decode("cp1252"),
|
||||
"status": resp.status,
|
||||
"byte count": resp.byte_count,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=slave)
|
||||
|
||||
def read_exception_status(self, slave=0, **kwargs):
|
||||
"""Read contents of eight Exception Status output in a remote device.
|
||||
|
||||
:param slave: Modbus slave ID
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReadExceptionStatusRequest(slave, **kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {"function_code": resp.function_code, "status": resp.status}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def get_com_event_counter(self, **kwargs):
|
||||
"""Read status word and an event count.
|
||||
|
||||
From the remote device's communication event counter.
|
||||
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = GetCommEventCounterRequest(**kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"status": resp.status,
|
||||
"count": resp.count,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def get_com_event_log(self, **kwargs):
|
||||
"""Read status word.
|
||||
|
||||
Event count, message count, and a field of event
|
||||
bytes from the remote device.
|
||||
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = GetCommEventLogRequest(**kwargs)
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function_code": resp.function_code,
|
||||
"status": resp.status,
|
||||
"message count": resp.message_count,
|
||||
"event count": resp.event_count,
|
||||
"events": resp.events,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def _execute_diagnostic_request(self, request):
|
||||
"""Execute diagnostic request."""
|
||||
resp = self.execute(request) # pylint: disable=no-member
|
||||
if not resp.isError():
|
||||
return {
|
||||
"function code": resp.function_code,
|
||||
"sub function code": resp.sub_function_code,
|
||||
"message": resp.message,
|
||||
}
|
||||
return ExtendedRequestSupport._process_exception(resp, slave=request.slave_id)
|
||||
|
||||
def return_query_data(self, message=0, **kwargs):
|
||||
"""Loop back data sent in response.
|
||||
|
||||
:param message: Message to be looped back
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnQueryDataRequest(message, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def restart_comm_option(self, toggle=False, **kwargs):
|
||||
"""Initialize and restart remote devices.
|
||||
|
||||
Serial interface and clear all of its communications event counters.
|
||||
|
||||
:param toggle: Toggle Status [ON(0xff00)/OFF(0x0000]
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = RestartCommunicationsOptionRequest(toggle, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_diagnostic_register(self, data=0, **kwargs):
|
||||
"""Read 16-bit diagnostic register.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnDiagnosticRegisterRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def change_ascii_input_delimiter(self, data=0, **kwargs):
|
||||
"""Change message delimiter for future requests.
|
||||
|
||||
:param data: New delimiter character
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ChangeAsciiInputDelimiterRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def force_listen_only_mode(self, data=0, **kwargs):
|
||||
"""Force addressed remote device to its Listen Only Mode.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ForceListenOnlyModeRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def clear_counters(self, data=0, **kwargs):
|
||||
"""Clear all counters and diag registers.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ClearCountersRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_message_count(self, data=0, **kwargs):
|
||||
"""Return count of message detected on bus by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusMessageCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_com_error_count(self, data=0, **kwargs):
|
||||
"""Return count of CRC errors received by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusCommunicationErrorCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_bus_exception_error_count(self, data=0, **kwargs):
|
||||
"""Return count of Modbus exceptions returned by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnBusExceptionErrorCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_message_count(self, data=0, **kwargs):
|
||||
"""Return count of messages addressed to remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveMessageCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_no_response_count(self, data=0, **kwargs):
|
||||
"""Return count of No responses by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveNoResponseCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_no_ack_count(self, data=0, **kwargs):
|
||||
"""Return count of NO ACK exceptions sent by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveNAKCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_busy_count(self, data=0, **kwargs):
|
||||
"""Return count of server busy exceptions sent by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveBusyCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_slave_bus_char_overrun_count(self, data=0, **kwargs):
|
||||
"""Return count of messages not handled.
|
||||
|
||||
By remote slave due to character overrun condition.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnSlaveBusCharacterOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def return_iop_overrun_count(self, data=0, **kwargs):
|
||||
"""Return count of iop overrun errors by remote slave.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ReturnIopOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def clear_overrun_count(self, data=0, **kwargs):
|
||||
"""Clear over run counter.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = ClearOverrunCountRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
def get_clear_modbus_plus(self, data=0, **kwargs):
|
||||
"""Get/clear stats of remote modbus plus device.
|
||||
|
||||
:param data: Data field (0x0000)
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request = GetClearModbusPlusRequest(data, **kwargs)
|
||||
return self._execute_diagnostic_request(request)
|
||||
|
||||
|
||||
class ModbusSerialClient(ExtendedRequestSupport, _ModbusSerialClient):
|
||||
"""Modbus serial client."""
|
||||
|
||||
def __init__(self, framer, **kwargs):
|
||||
"""Initialize."""
|
||||
super().__init__(framer=framer, **kwargs)
|
||||
|
||||
def get_port(self):
|
||||
"""Get serial Port.
|
||||
|
||||
:return: Current Serial port
|
||||
"""
|
||||
return self.comm_params.port
|
||||
|
||||
def set_port(self, value):
|
||||
"""Set serial Port setter.
|
||||
|
||||
:param value: New port
|
||||
"""
|
||||
self.comm_params.port = value
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_stopbits(self):
|
||||
"""Get number of stop bits.
|
||||
|
||||
:return: Current Stop bits
|
||||
"""
|
||||
return self.params.stopbits
|
||||
|
||||
def set_stopbits(self, value):
|
||||
"""Set stop bit.
|
||||
|
||||
:param value: Possible values (1, 1.5, 2)
|
||||
"""
|
||||
self.params.stopbits = float(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_bytesize(self):
|
||||
"""Get number of data bits.
|
||||
|
||||
:return: Current bytesize
|
||||
"""
|
||||
return self.comm_params.bytesize
|
||||
|
||||
def set_bytesize(self, value):
|
||||
"""Set Byte size.
|
||||
|
||||
:param value: Possible values (5, 6, 7, 8)
|
||||
|
||||
"""
|
||||
self.comm_params.bytesize = int(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_parity(self):
|
||||
"""Enable Parity Checking.
|
||||
|
||||
:return: Current parity setting
|
||||
"""
|
||||
return self.params.parity
|
||||
|
||||
def set_parity(self, value):
|
||||
"""Set parity Setter.
|
||||
|
||||
:param value: Possible values ("N", "E", "O", "M", "S")
|
||||
"""
|
||||
self.params.parity = value
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_baudrate(self):
|
||||
"""Get serial Port baudrate.
|
||||
|
||||
:return: Current baudrate
|
||||
"""
|
||||
return self.comm_params.baudrate
|
||||
|
||||
def set_baudrate(self, value):
|
||||
"""Set baudrate setter.
|
||||
|
||||
:param value: <supported baudrate>
|
||||
"""
|
||||
self.comm_params.baudrate = int(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_timeout(self):
|
||||
"""Get serial Port Read timeout.
|
||||
|
||||
:return: Current read imeout.
|
||||
"""
|
||||
return self.comm_params.timeout_connect
|
||||
|
||||
def set_timeout(self, value):
|
||||
"""Read timeout setter.
|
||||
|
||||
:param value: Read Timeout in seconds
|
||||
"""
|
||||
self.comm_params.timeout_connect = float(value)
|
||||
if self.is_socket_open():
|
||||
self.close()
|
||||
|
||||
def get_serial_settings(self):
|
||||
"""Get Current Serial port settings.
|
||||
|
||||
:return: Current Serial settings as dict.
|
||||
"""
|
||||
return {
|
||||
"baudrate": self.comm_params.baudrate,
|
||||
"port": self.comm_params.port,
|
||||
"parity": self.comm_params.parity,
|
||||
"stopbits": self.comm_params.stopbits,
|
||||
"bytesize": self.comm_params.bytesize,
|
||||
"read timeout": self.comm_params.timeout_connect,
|
||||
"t1.5": self.inter_char_timeout,
|
||||
"t3.5": self.silent_interval,
|
||||
}
|
||||
|
||||
|
||||
class ModbusTcpClient(ExtendedRequestSupport, _ModbusTcpClient):
|
||||
"""TCP client."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize."""
|
||||
super().__init__(**kwargs)
|
||||
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