Initial commit for tac2100_compteur_mbus2mqtt
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
"""Repl client."""
|
||||
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
+143
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
Reference in New Issue
Block a user