385 lines
12 KiB
Python
385 lines
12 KiB
Python
"""
|
|
Pymodbus REPL Entry point.
|
|
|
|
Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved.
|
|
|
|
"""
|
|
from __future__ import absolute_import, unicode_literals
|
|
try:
|
|
import click
|
|
except ImportError:
|
|
print("click not installed!! Install with 'pip install click'")
|
|
exit(1)
|
|
try:
|
|
from prompt_toolkit import PromptSession, print_formatted_text
|
|
except ImportError:
|
|
print("prompt toolkit is not installed!! "
|
|
"Install with 'pip install prompt_toolkit --upgrade'")
|
|
exit(1)
|
|
|
|
from prompt_toolkit.lexers import PygmentsLexer
|
|
from prompt_toolkit.styles import Style
|
|
from prompt_toolkit.key_binding import KeyBindings
|
|
|
|
from pygments.lexers.python import PythonLexer
|
|
from prompt_toolkit.formatted_text import HTML
|
|
from prompt_toolkit.history import FileHistory
|
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
from pymodbus.version import version
|
|
from pymodbus.repl.client.completer import CmdCompleter, has_selected_completion
|
|
from pymodbus.repl.client.helper import Result, CLIENT_ATTRIBUTES
|
|
|
|
click.disable_unicode_literals_warning = True
|
|
|
|
TITLE = """
|
|
----------------------------------------------------------------------------
|
|
__________ _____ .___ __________ .__
|
|
\______ \___.__. / \ ____ __| _/ \______ \ ____ ______ | |
|
|
| ___< | |/ \ / \ / _ \ / __ | | _// __ \\\____ \| |
|
|
| | \___ / Y ( <_> ) /_/ | | | \ ___/| |_> > |__
|
|
|____| / ____\____|__ /\____/\____ | /\ |____|_ /\___ > __/|____/
|
|
\/ \/ \/ \/ \/ \/|__|
|
|
v{} - {}
|
|
----------------------------------------------------------------------------
|
|
""".format("1.3.0", version)
|
|
|
|
log = 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',
|
|
})
|
|
|
|
|
|
def bottom_toolbar():
|
|
"""
|
|
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):
|
|
"""
|
|
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(CaseInsenstiveChoice, self).convert(
|
|
value.strip().upper(), param, ctx)
|
|
|
|
|
|
class NumericChoice(click.Choice):
|
|
"""
|
|
Numeric choice for click arguments and options.
|
|
"""
|
|
def __init__(self, choices, typ):
|
|
self.typ = typ
|
|
super(NumericChoice, self).__init__(choices)
|
|
|
|
def convert(self, value, param, ctx):
|
|
# 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:
|
|
if ctx.token_normalize_func(choice) == value:
|
|
return choice
|
|
|
|
self.fail('invalid choice: %s. (choose from %s)' %
|
|
(value, ', '.join(self.choices)), param, ctx)
|
|
|
|
|
|
def cli(client):
|
|
kb = KeyBindings()
|
|
|
|
@kb.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)
|
|
|
|
@kb.add('enter', filter=has_selected_completion)
|
|
def _(event):
|
|
"""
|
|
Makes the enter key work as the tab key only when showing the menu.
|
|
"""
|
|
|
|
event.current_buffer.complete_state = None
|
|
b = event.cli.current_buffer
|
|
b.complete_state = None
|
|
|
|
def _process_args(args, string=True):
|
|
kwargs = {}
|
|
execute = True
|
|
skip_index = None
|
|
for i, arg in enumerate(args):
|
|
if i == skip_index:
|
|
continue
|
|
arg = arg.strip()
|
|
if "=" in arg:
|
|
a, val = arg.split("=")
|
|
if not string:
|
|
if "," in val:
|
|
val = val.split(",")
|
|
val = [int(v) for v in val]
|
|
else:
|
|
val = int(val)
|
|
kwargs[a] = val
|
|
else:
|
|
a, val = arg, args[i + 1]
|
|
try:
|
|
if not string:
|
|
if "," in val:
|
|
val = val.split(",")
|
|
val = [int(v) for v in val]
|
|
else:
|
|
val = int(val)
|
|
kwargs[a] = 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
|
|
|
|
session = PromptSession(lexer=PygmentsLexer(PythonLexer),
|
|
completer=CmdCompleter(client), style=style,
|
|
complete_while_typing=True,
|
|
bottom_toolbar=bottom_toolbar,
|
|
key_bindings=kb,
|
|
history=FileHistory('../.pymodhis'),
|
|
auto_suggest=AutoSuggestFromHistory())
|
|
click.secho("{}".format(TITLE), fg='green')
|
|
result = None
|
|
while True:
|
|
try:
|
|
|
|
text = session.prompt('> ', complete_while_typing=True)
|
|
if text.strip().lower() == 'help':
|
|
print_formatted_text(HTML("<u>Available commands:</u>"))
|
|
for cmd, obj in sorted(session.completer.commands.items()):
|
|
if cmd != 'help':
|
|
print_formatted_text(
|
|
HTML("<skyblue>{:45s}</skyblue>"
|
|
"<seagreen>{:100s}"
|
|
"</seagreen>".format(cmd, obj.help_text)))
|
|
|
|
continue
|
|
elif text.strip().lower() == 'exit':
|
|
raise EOFError()
|
|
elif text.strip().lower().startswith("client."):
|
|
try:
|
|
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()
|
|
except Exception as e:
|
|
click.secho(repr(e), fg='red')
|
|
elif text.strip().lower().startswith("result."):
|
|
if result:
|
|
words = text.lower().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)
|
|
except KeyboardInterrupt:
|
|
continue # Control-C pressed. Try again.
|
|
except EOFError:
|
|
break # Control-D pressed.
|
|
except Exception as e: # Handle all other exceptions
|
|
click.secho(str(e), fg='red')
|
|
|
|
click.secho('GoodBye!', fg='blue')
|
|
|
|
|
|
@click.group('pymodbus-repl')
|
|
@click.version_option(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.option("--reset-socket/--no-reset-socket", default=True, help="Reset client socket on error")
|
|
@click.pass_context
|
|
def main(ctx, verbose, broadcast_support, retry_on_empty,
|
|
retry_on_error, retries, reset_socket):
|
|
if verbose:
|
|
global log
|
|
import logging
|
|
format = ('%(asctime)-15s %(threadName)-15s '
|
|
'%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
|
|
log = logging.getLogger('pymodbus')
|
|
logging.basicConfig(format=format)
|
|
log.setLevel(logging.DEBUG)
|
|
ctx.obj = {
|
|
"broadcast": broadcast_support,
|
|
"retry_on_empty": retry_on_empty,
|
|
"retry_on_invalid": retry_on_error,
|
|
"retries": retries,
|
|
"reset_socket": reset_socket
|
|
}
|
|
|
|
|
|
@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):
|
|
from pymodbus.repl.client.mclient import ModbusTcpClient
|
|
kwargs = dict(host=host, port=port)
|
|
kwargs.update(**ctx.obj)
|
|
if framer == 'rtu':
|
|
from pymodbus.framer.rtu_framer import ModbusRtuFramer
|
|
kwargs['framer'] = ModbusRtuFramer
|
|
client = ModbusTcpClient(**kwargs)
|
|
cli(client)
|
|
|
|
|
|
@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. Defaults to 9600",
|
|
default=9600,
|
|
type=int
|
|
)
|
|
@click.option(
|
|
"--bytesize",
|
|
help="Modbus RTU serial Number of data bits. "
|
|
"Possible values: FIVEBITS, SIXBITS, SEVENBITS, "
|
|
"EIGHTBITS. Defaults to 8",
|
|
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."
|
|
"Defaults to 0",
|
|
default=0,
|
|
type=int
|
|
)
|
|
@click.option(
|
|
"--rtscts",
|
|
help="Modbus RTU serial rtscts. Enable hardware (RTS/CTS) flow "
|
|
"control. Defaults to 0",
|
|
default=0,
|
|
type=int
|
|
)
|
|
@click.option(
|
|
"--dsrdtr",
|
|
help="Modbus RTU serial dsrdtr. Enable hardware (DSR/DTR) flow "
|
|
"control. Defaults to 0",
|
|
default=0,
|
|
type=int
|
|
)
|
|
@click.option(
|
|
"--timeout",
|
|
help="Modbus RTU serial read timeout. Defaults to 0.025 sec",
|
|
default=0.25,
|
|
type=float
|
|
)
|
|
@click.option(
|
|
"--write-timeout",
|
|
help="Modbus RTU serial write timeout. Defaults to 2 sec",
|
|
default=2,
|
|
type=float
|
|
)
|
|
def serial(ctx, method, port, baudrate, bytesize, parity, stopbits, xonxoff,
|
|
rtscts, dsrdtr, timeout, write_timeout):
|
|
from pymodbus.repl.client.mclient import ModbusSerialClient
|
|
client = ModbusSerialClient(method=method,
|
|
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(client)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|