Files
uarep-ctl/modbus/pymodbus/repl/client/main.py

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()