Skip to content

Comm

pychilaslasers.comm

Communication class for handling laser driver serial communication.

This module contains the Communication class for handling communication with the laser driver over serial.

It contains the methods for sending commands to the laser, receiving responses, and managing the serial connection.

Authors: RLK, AVR, SDU

pychilaslasers.comm.Communication(com_port: str)

Communication class for handling communication with the laser driver over serial.

This class provides methods for sending commands to the laser, receiving responses, and managing the serial connection. It also handles the prefix mode for the laser driver.

This method sets up the serial connection to the laser driver and initializes the communication parameters. It also registers cleanup functions to ensure the serial connection is properly closed on exit or signal termination. And sets the initial baudrate to the default value. When a connection fails, it will attempt to reconnect using the next supported baudrate until a connection is established as this is one of the most common issues when connecting to the laser driver.

Parameters:

  • com_port (str) –

    The serial port to connect to the laser driver. this can be found by using the pychilaslasers.utils.get_serial_ports() function.

Source code in src/pychilaslasers/comm.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def __init__(self, com_port: str) -> None:
    """Initialize the Communication class with the specified serial port.

    This method sets up the serial connection to the laser driver and initializes
    the communication parameters. It also registers cleanup functions to ensure
    the serial connection is properly closed on exit or signal termination. And
    sets the initial baudrate to the default value. When a connection fails, it will
    attempt to reconnect using the next supported baudrate until a connection is
    established as this is one of the most common issues when connecting to the
    laser driver.

    Args:
        com_port: The serial port to connect to the laser driver.
            this can be found by using the `pychilaslasers.utils.get_serial_ports()`
            function.

    """
    # Validate the com_port input
    if not isinstance(com_port, str):
        raise ValueError(
            "The com_port must be a string representing the serial port."
        )

    # Initialize serial connection to the laser
    self._serial: serial.Serial = serial.Serial(
        port=com_port,
        baudrate=Constants.TLM_INITIAL_BAUDRATE,  # Use the first supported baudrate
        bytesize=serial.EIGHTBITS,
        parity=serial.PARITY_NONE,
        stopbits=serial.STOPBITS_ONE,
        timeout=1.0,
    )
    self._previous_command: str = "None"

    self._prefix_mode: bool = True
    # Attempt to open the serial connection by trying different baudrates
    baudrates: set[int] = (
        Constants.SUPPORTED_BAUDRATES.copy()
    )  # Copy to avoid modifying the original set
    rate = Constants.TLM_INITIAL_BAUDRATE
    while True:
        try:
            self.prefix_mode = True
            break
        except Exception:
            try:
                logger.error(
                    f"Serial connection failed at {rate} baud.Attempting new "
                    f"connection with baudrate {(rate := baudrates.pop())}."
                )
                self.baudrate = rate  # Try next baudrate if the current one fails
            except KeyError:
                logger.critical(
                    "No more supported baudrates available. Cannot establish serial"
                    " connection."
                )
                raise RuntimeError(
                    "Failed to establish serial connection with the laser driver. "
                    + "Please check the connection and supported baudrates."
                ) from None
    self.baudrate = Constants.DEFAULT_BAUDRATE

    # Ensure proper closing of the serial connection on exit or signal
    atexit.register(self.close_connection)
    signal.signal(signal.SIGINT, self.close_connection)
    signal.signal(signal.SIGTERM, self.close_connection)

Attributes

prefix_mode: bool property writable

Gets prefix mode for the laser driver.

The laser can be operated in two different communication modes:

  1. Prefix mode on
  2. Prefix mode off

When prefix mode is on, every message over the serial connection will be replied to by the driver with a response, and every response will be prefixed with a return code (rc), either 0 or 1 for an OK or ERROR respectively.

With prefix mode is off, responses from the laser driver are not prefixed with a return code. This means that in the case for a serial write command without an expected return value, the driver will not send back a reply.

Returns:

  • bool

    whether prefix mode is enabled (True) or disabled (False)

baudrate: int property writable

Gets the baudrate of the serial connection to the driver.

The baudrate can be changed, but does require a serial reconnect

Currently supported baudrates are:
  • 9600
  • 14400
  • 19200
  • 28800
  • 38400
  • 57600 default
  • 115200
  • 230400
  • 460800
  • 912600

Returns:

  • int

    baudrate currently in use

Functions

__del__() -> None

Destructor that ensures the serial connection is closed after deletion.

This method is called when the object is garbage collected, providing an additional safety mechanism to ensure the serial connection is properly closed even if the user forgets to call close explicitly or if the program terminates unexpectedly.

Source code in src/pychilaslasers/comm.py
102
103
104
105
106
107
108
109
110
def __del__(self) -> None:
    """Destructor that ensures the serial connection is closed after deletion.

    This method is called when the object is garbage collected, providing an
    additional safety mechanism to ensure the serial connection is properly closed
    even if the user forgets to call close explicitly or if the program terminates
    unexpectedly.
    """
    self.close_connection()

query(data: str) -> str

Send a command to the laser and return its response.

This method sends a command to the laser over the serial connection and returns the response. It also handles the logging of the command and response. The response code of the reply is checked and an error is raised if the response code is not 0. Commands that are sent multiple times may be replaced with a semicolon to speed up communication.

Parameters:

  • data (str) –

    The serial command to be sent to the laser.

Returns:

  • str

    The response from the laser. The response is stripped of any leading or trailing whitespace as well as the return code. Response may be empty if the command does not return a value.

Raises:

  • SerialException

    If there is an error in the serial communication, such as a decoding error or an empty reply.

  • LaserError

    If the response code from the laser is not 0, indicating an error.

Source code in src/pychilaslasers/comm.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def query(self, data: str) -> str:
    """Send a command to the laser and return its response.

    This method sends a command to the laser over the serial connection and returns
    the response. It also handles the logging of the command and response. The
    response code of the reply is checked and an error is raised if the response
    code is not 0. Commands that are sent multiple times may be replaced with a
    semicolon to speed up communication.

    Args:
        data: The serial command to be sent to the laser.

    Returns:
        The response from the laser. The response is stripped of any leading
            or trailing whitespace as well as the return code. Response may be
            empty if the command does not return a value.

    Raises:
        serial.SerialException: If there is an error in the serial communication,
            such as a decoding error or an empty reply.
        LaserError: If the response code from the laser is not 0,
            indicating an error.

    """
    # Write the command to the serial port
    logger.debug(msg=f"W {data}")  # Logs the command being sent
    self._serial.write(f"{self._semicolon_replace(data)}\r\n".encode("ascii"))
    self._serial.flush()

    if not self.prefix_mode:
        return ""  # If prefix mode is off, return empty string immediately

    # Read the response from the laser
    try:
        reply: str = self._serial.readline().decode("ascii").rstrip()
    except UnicodeDecodeError as e:
        logger.error(f"Failed to decode reply from device: {e}")
        raise serial.SerialException(
            f"Failed to decode reply from device: {e}. "
            + "Please check the connection and baudrate settings."
        ) from e

    # Error handling
    if not reply or reply == "":
        logger.error("Empty reply from device")
        raise serial.SerialException(
            "Empty reply from device. Please check the connection and prefix mode."
        )

    if reply[0] != "0":
        logger.error(f"Nonzero return code: {reply[2:]}")
        raise LaserError(
            code=reply[0], message=reply[2:]
        )  # Raise a custom error with the reply message
    else:
        logger.debug(f"R {reply}")

    return reply[2:]

close_connection(signum=None, fname=None) -> None

Close the serial connection to the laser driver safely.

Attempts to reset the baudrate to the initial value before closing the connection.

This method is registered to be called on exit or when a signal is received.

Source code in src/pychilaslasers/comm.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def close_connection(self, signum=None, fname=None) -> None:
    """Close the serial connection to the laser driver safely.

    Attempts to reset the baudrate to the initial value before closing the
    connection.

    This method is registered to be called on exit or when a signal is received.
    """
    if self._serial and self._serial.is_open:
        if signum is not None:
            logger.error(
                f"Received signal {signal.Signals(signum).name} ({signum}):"
                "closing connection"
            )
        else:
            logger.debug("Closing connection")
        self.query("SYST:STAT 0")
        self._serial.write(
            f"SYST:SER:BAUD {Constants.TLM_INITIAL_BAUDRATE}\r\n".encode("ascii")
        )
        logger.debug("Resetting serial baudrate to initial value")
        self._serial.close()