##########################################################################
# Copyright (c) 2010-2022 Robert Bosch GmbH
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# http://www.eclipse.org/legal/epl-2.0.
#
# SPDX-License-Identifier: EPL-2.0
##########################################################################
"""
Instrument Control CLI
**********************
:module: instrument_control_cli
:synopsis: Command Line Interface used to communicate with an instrument using the SCPI protocol.
.. currentmodule:: instrument_control_cli
"""
import enum
import logging
import sys
import click
from pykiso import __version__
from pykiso.lib.auxiliaries.instrument_control_auxiliary import (
InstrumentControlAuxiliary,
)
from pykiso.lib.connectors.cc_tcp_ip import CCTcpip
from pykiso.lib.connectors.cc_visa import VISASerial, VISATcpip
from . import REGISTERED_INSTRUMENTS
[docs]@enum.unique
class ExitCode(enum.IntEnum):
"""List of possible exit codes"""
SUCCESS = 0
INTERFACE_NOT_PROVIDED = 1
INSTRUMENT_OPENING_FAILED = 2
[docs]@enum.unique
class Interface(enum.Enum):
"""List of available interfaces"""
VISA_SERIAL = "VISA_SERIAL"
VISA_TCPIP = "VISA_TCPIP"
SOCKET_TCPIP = "SOCKET_TCPIP"
[docs]def setup_interface(
interface: str,
baud_rate: int = None,
ip_address: str = None,
port: int = None,
protocol: str = None,
name: str = None,
) -> InstrumentControlAuxiliary:
"""Set up the instrument auxiliary with the appropriate interface settings.
The ip address must be provided in the case of TCPIP interfaces, as must
the serial port for VISA_SERIAL interface. The baud rate and the output channel
to use are optional.
:param interface: interface to use
:param baud_rate: baud rate to use
:param ip_address: ip address of the remote instrument (used for remote control only)
:param port: the port of the device to connect to. This is either a serial port for
a VISA_SERIAL interface or an IP port in case of an TCPIP interfaces.
:param protocol: The protocol to use for VISA_TCPIP interfaces.
:param name: instrument name used to adapt the SCPI commands to be sent to the instrument
:return: The created instrument auxiliary.
"""
if interface == Interface.VISA_TCPIP.value:
if ip_address is None:
logging.error("An IP address must be provided!")
raise ConnectionAbortedError()
else:
port = f"::{port}" if port else ""
com_channel = VISATcpip(ip_address=ip_address + port, protocol=protocol)
elif interface == Interface.VISA_SERIAL.value:
if port is None:
logging.error("A serial port must be provided!")
raise ConnectionAbortedError()
else:
com_channel = VISASerial(serial_port=port, baud_rate=baud_rate)
elif interface == Interface.SOCKET_TCPIP.value:
if ip_address is None or port is None:
logging.error("You must provide an IP address and a port (usually 5025).")
raise ConnectionAbortedError()
else:
com_channel = CCTcpip(dest_ip=ip_address, dest_port=port)
else:
logging.error("You must choose an interface! Available interfaces are:")
for interface in Interface:
logging.error(interface.value)
raise ConnectionAbortedError()
instr_aux = InstrumentControlAuxiliary(com=com_channel, instrument=name)
instr_aux.create_instance()
return instr_aux
[docs]def initialize_logging(log_level: str) -> logging.getLogger:
"""Initialize the logging by setting the general log level
:param log_level: any of DEBUG, INFO, WARNING, ERROR
:returns: configured Logger
"""
levels = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
}
kwargs = {
"level": levels[log_level],
}
logging.basicConfig(
format="%(asctime)s [%(levelname)s] %(module)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
**kwargs,
)
return logging.getLogger(__name__)
[docs]def parse_user_command(user_cmd: str) -> dict:
"""Parses the command from user input in interactive mode
:param user_cmd: command provided by the user in interactive mode
:return: a single-item dictionary containing the parsed command as
key the the corresponding payload as value"""
cmd = user_cmd.replace("-", "_").replace("__", "").split()
if len(cmd) == 1:
return {cmd[0]: "get"}
else:
return {cmd[0]: cmd[1]}
@click.command()
@click.option(
"-b",
"--baud-rate",
required=False,
default=9600,
type=click.INT,
help="The value to set the baud rate to",
)
@click.option(
"-i",
"--interface",
required=True,
type=click.Choice(
choices=["VISA_SERIAL", "VISA_TCPIP", "SOCKET_TCPIP"], case_sensitive=False
),
help="""The interface to use for the connection. Available interfaces are:
- VISA_SERIAL - serial communication with pyvisa
- VISA_TCPIP - TCP/IP communication with pyvisa's different protocols
- SOCKET_TCPIP - TCP/IP communication using socket""",
)
@click.option(
"-ip",
"--ip-address",
required=False,
help="The address of the device to connect to (needed with TCPIP interfaces).",
)
@click.option(
"--name",
required=False,
type=click.Choice(REGISTERED_INSTRUMENTS, case_sensitive=False),
default=None,
help="""name of the instrument in use. If registered, the commands
adapted to this instrument will be used instead of the default ones. """,
)
@click.option(
"--log-level",
required=False,
default="INFO",
type=click.Choice(
"DEBUG INFO WARNING ERROR".split(" "),
case_sensitive=False,
),
help="set the verbosity of the logging",
)
@click.option(
"--output-channel",
required=False,
type=click.STRING,
default=None,
help="""The index of the output channel instrument to use.
Use 'get' to receive the currently used output channel.""",
)
@click.option(
"-p",
"--port",
required=False,
type=click.INT,
default=None,
help="""The port of the device to connect to. This is either a serial port for a VISA_SERIAL interface
or an IP port in case of an TCPIP interfaces (required for VISA_SERIAL interface and SOCKET
interface).""",
)
@click.option(
"--protocol",
required=False,
default=None,
type=click.Choice(
"INSTR SOCKET HISLIP".split(" "),
case_sensitive=True,
),
help="""The protocol to use for the VISA_TCPIP interfaces. This usually is the last part of
the complete instrument address, e.g. for 'TCPIP::1.1.1.1::HISLIP' this would be HISLIP.""",
)
# Interactive mode
@click.option(
"--interactive",
flag_value="interactive",
required=False,
default=None,
help="Opens the interactive mode after a connection is established with the instrument",
)
# Use instrument method
@click.option(
"--query",
required=False,
default=None,
type=click.STRING,
help="Send a query with a custom payload to the instrument",
)
@click.option(
"--write",
required=False,
default=None,
type=click.STRING,
help="Send a write with a custom payload to the instrument",
)
# General
@click.option(
"--identification",
flag_value="get",
required=False,
default=None,
help="Get the instrument's identification information",
)
@click.option(
"--reset",
flag_value="set",
required=False,
default=None,
help="Reset the instrument",
)
@click.option(
"--status-byte",
flag_value="get",
required=False,
default=None,
help="Get the instrument's status byte",
)
@click.option(
"--all-errors",
flag_value="get",
required=False,
default=None,
help="Get all errors currently stored in the instrument",
)
@click.option(
"--self-test",
flag_value="get",
required=False,
default=None,
help="Perform a self test of the instrument",
)
# Remote control
@click.option(
"--remote-control",
required=False,
default=None,
type=click.Choice("get on off".split(), case_sensitive=False),
help="Instrument's remote control state",
)
# Output mode
@click.option(
"--output-mode",
required=False,
default=None,
type=click.Choice("get enable disable".split(), case_sensitive=False),
help="Instrument's output mode",
)
# Nominal values
@click.option(
"--voltage-nominal",
flag_value="get",
required=False,
default=None,
help="Instrument's nominal voltage (V)",
)
@click.option(
"--current-nominal",
flag_value="get",
required=False,
default=None,
help="Instrument's nominal current (A)",
)
@click.option(
"--power-nominal",
flag_value="get",
required=False,
default=None,
help="Instrument's nominal power (W)",
)
# Target values
@click.option(
"--voltage-target",
required=False,
default=None,
help="Instrument's target voltage (V)",
)
@click.option(
"--current-target",
required=False,
default=None,
help="Instrument's target current (A)",
)
@click.option(
"--power-target",
required=False,
default=None,
help="Instrument's target power (W)",
)
# Measurements
@click.option(
"--voltage-measure",
flag_value="get",
required=False,
default=None,
help="Measure the instrument's voltage (so argument to be provided)",
)
@click.option(
"--current-measure",
flag_value="get",
required=False,
default=None,
help="Measure the instrument's current (so argument to be provided)",
)
@click.option(
"--power-measure",
flag_value="get",
required=False,
default=None,
help="Measure the instrument's power (so argument to be provided)",
)
# Limit values
@click.option(
"--voltage-limit-low",
required=False,
default=None,
help="Instrument's voltage lower limit",
)
@click.option(
"--voltage-limit-high",
required=False,
default=None,
help="Instrument's voltage higher limit",
)
@click.option(
"--current-limit-low",
required=False,
default=None,
help="Instrument's current lower limit",
)
@click.option(
"--current-limit-high",
required=False,
default=None,
help="Instrument's current higher limit",
)
@click.option(
"--power-limit-high",
required=False,
default=None,
help="Instrument's power higher limit",
)
@click.version_option(__version__)
def main(
ip_address,
baud_rate,
interface,
log_level,
name,
port,
interactive,
protocol,
**kwargs,
) -> int:
"""
Main function run by the CLI
:param argv: The argument list passed via command line
:return: None
"""
# Initialize logging
initialize_logging(log_level)
# Setup and open the interface (SERIAL or TCPIP with VISA or TCPIP socket)
instr_aux = setup_interface(
interface=interface,
baud_rate=baud_rate,
ip_address=ip_address,
port=port,
name=name,
protocol=protocol,
)
# Perform other commands from subparsers
perform_actions(instr_aux=instr_aux, actions=kwargs)
# Opens interactive mode
if interactive:
click.echo(
"Interactive mode enabled. The connection to the instrument is still opened. "
"Type other commands to get or set values, "
"and then type 'exit' to exit the interactive session"
)
while 1:
command = click.prompt("--")
if command == "exit":
click.echo("Exiting interactive session")
break
else:
perform_actions(
instr_aux=instr_aux, actions=parse_user_command(command)
)
# Close the VISA interface and exit
instr_aux.delete_instance()
return ExitCode.SUCCESS.value
if __name__ == "__main__":
sys.exit(main())