Source code for herosdevices.core.bus.telnet.telnet

"""This module provides a class for managing telnet connections and a context manager for handling telnet sessions."""

import time
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Literal, overload

from heros.helper import log

from .telnetlib import IAC, NOP, Telnet

TELNET_DELAYS_DEFAULT: dict = {"write": 0.001, "read_echo": 0.001}


[docs] class TelnetConnection: """A class to manage telnet communication connections. This class provides functionality to handle telnet connections including opening/closing connections, reading data, and writing data. Args: address: The address of the telnet socket, something like /dev/ttyUSB0. port: Port the telnet server is listening on line_termination: character that terminates a line in the communication. keep_alive: Flag indicating whether to keep the connection open between operations. delays: Dictionary containing delay times for in between telnet operations. Default telnet delays for telnet devices. Available keys are: * "write": Time to wait after writing a command to the device. * "read_echo": Time to wait before reading a response from the device. :py:data:`herosdevices.core.bus.telnet.TELNET_DELAYS_DEFAULT` sets the default delays. """ def __init__( self, address: str, port: int = 23, timeout: float = 1.0, line_termination: bytes = b"\n", keep_alive: bool = True, delays: dict | None = None, ) -> None: self.address = address self.port = port self.timeout = timeout self.line_termination = line_termination self.connection = Telnet() self.keep_alive = keep_alive self.delays = TELNET_DELAYS_DEFAULT | delays if delays else TELNET_DELAYS_DEFAULT
[docs] def check_alive(self) -> bool: """Check if the telnet connection is alive.""" try: if self.connection.sock: # this way I've taken care of problem if the .close() was called self.connection.sock.send(IAC + NOP) # notice the use of send instead of sendall return True except OSError: pass return False
[docs] @contextmanager def operation(self) -> Iterator[None]: """Context manager for handling telnet connection operations. Ensures the telnet connection is open before performing operations and closes it afterward if :code:`self.keep_alive` is False. Yields: Yields control back to the caller for performing operations within the context. """ if not self.check_alive(): self.connection.open(self.address, self.port, self.timeout) try: yield finally: if not self.keep_alive: self.connection.close()
[docs] def wait(self, operation: str) -> None: """Introduce a (synchronous) delay based on the specified operation type. Args: operation: The operation type. For possible types see :code:`TELNET_DELAYS_DEFAULT`. """ time.sleep(self.delays[operation])
[docs] def read(self) -> str | None: """ Read all available data from the telnet connection and decodes it into a string. Returns: The decoded data as string, or None if an error occurs. """ with self.operation(): try: read = self.connection.read_very_eager() return read.decode("ascii") except Exception as e: # noqa: BLE001 log.error( "Error reading from telnet connection at %s: %s", self.address, e, ) return None
[docs] def read_line(self) -> str | None: """Read a single line from the telnet connection. Returns: The decoded line as string, or None if an error occurs. """ with self.operation(): try: read = self.connection.read_until(self.line_termination) return read.decode("ascii") except Exception as e: # noqa: BLE001 log.error( "Error reading from telnet connection at %s: %s", self.address, e, ) return None
@overload def write(self, message: str, read_echo: Literal[True] = True, read_line: bool = True) -> str: ... @overload def write(self, message: str, read_echo: Literal[False], read_line: bool = True) -> None: ... @overload def write(self, message: str, read_echo: bool = False, read_line: bool = True) -> str | None: ...
[docs] def write(self, message: str, read_echo: bool = False, read_line: bool = True) -> str | None: """Write a message to the telnet connection. Args: message: The message to be written to the telnet connection. read_echo: If True, reads back the echo after writing. Defaults to False. read_line: If True, data is read until `self.line_termination` occurs in the data. Otherwise all available data is read. Returns: If read_echo is True, returns the echo read from the connection as string; otherwise returns None. """ with self.operation(): self.connection.read_very_lazy() self.connection.write(message.encode("ascii")) self.wait("write") if read_echo: if read_line: read = self.read_line() else: self.wait("read_echo") read = self.read() return read return None