Files
uarep-ctl/modbus/pymodbus/repl/server/cli.py

209 lines
7.5 KiB
Python

"""
Copyright (c) 2020 by RiptideIO
All rights reserved.
"""
import json
import click
import shutil
import logging
from prompt_toolkit.shortcuts import clear
from prompt_toolkit.shortcuts.progress_bar import formatters
from prompt_toolkit.styles import Style
from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit.formatted_text import HTML
logger = logging.getLogger(__name__)
TITLE = """
__________ .______. _________
\______ \___.__. _____ ____ __| _/\_ |__ __ __ ______ / _____/ ______________ __ ___________
| ___< | |/ \ / _ \ / __ | | __ \| | \/ ___/ \_____ \_/ __ \_ __ \ \/ // __ \_ __ \\
| | \___ | 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=&lt;int&gt; \n\t\tdelay_by=&lt;in seconds&gt; \n\t\t" \
"clear_after=&lt;clear after n messages int&gt;" \
"\n\t\tdata_len=&lt;length of stray data (int)&gt;\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 lenght 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": "Manipulate response from server.\nUsage: {}".format(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):
if not isinstance(message, str):
message = str(message)
click.secho(message, fg="green")
def warning(message):
click.secho(str(message), fg="yellow")
def error(message):
click.secho(str(message), fg="red")
def get_terminal_width():
return shutil.get_terminal_size()[0]
def print_help():
print_formatted_text(HTML("<u>Available commands:</u>"))
for cmd, hlp in sorted(COMMAND_HELPS.items()):
print_formatted_text(
HTML("<skyblue>{:45s}</skyblue><seagreen>{:100s}</seagreen>".format(cmd, hlp))
)
async def interactive_shell(server):
"""
CLI interactive shell
"""
col = get_terminal_width()
max_len = max([len(t) for t in TITLE.split("\n")])
if col > max_len:
info(TITLE)
else:
print_formatted_text(HTML('<u><b><style color="green">{}'
'</style></b></u>'.format(SMALL_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:
invalid_command = False
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
command = result.split()
if command:
if command[0] not in COMMANDS:
invalid_command = True
if invalid_command:
warning("Invalid command or invalid usage of command - {}".format(command))
continue
if len(command) == 1:
warning("Usage: '{}'".format(USAGE))
else:
args = command[1:]
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("=")
else:
if arg in COMMAND_ARGS:
try:
value = args[index+1]
skip_next = True
except IndexError:
error("Missing value "
"for argument - {}".format(arg))
warning("Usage: '{}'".format(USAGE))
break
valid = True
if arg == "response_type":
if value not in RESPONSE_TYPES:
warning("Invalid response "
"type request - {}".format(value))
warning("Choose from {}".format(RESPONSE_TYPES))
valid = False
elif arg in ["error_code", "delay_by",
"clear_after", "data_len"]:
try:
value = int(value)
except ValueError:
warning("Expected integer "
"value for {}".format(arg))
valid = False
if valid:
val_dict[arg] = value
if val_dict:
server.update_manipulator_config(val_dict)
# server.manipulator_config = val_dict
# result = await run_command(tester, *command)
except (EOFError, KeyboardInterrupt):
return
async def main(server):
# with patch_stdout():
try:
await interactive_shell(server)
finally:
pass
warning("Bye Bye!!!")
async def run_repl(server):
await main(server)