pychemstation.utils.abc_tables.abc_comm

Module to provide API for the communication with Agilent HPLC systems.

HPLCController sends commands to Chemstation software via a command file. Answers are received via reply file. On the Chemstation side, a custom Macro monitors the command file, executes commands and writes to the reply file. Each command is given a number (cmd_no) to keep track of which commands have been processed.

Authors: Alexander Hammer, Hessam Mehr, Lucy Hao

  1"""
  2Module to provide API for the communication with Agilent HPLC systems.
  3
  4HPLCController sends commands to Chemstation software via a command file.
  5Answers are received via reply file. On the Chemstation side, a custom
  6Macro monitors the command file, executes commands and writes to the reply file.
  7Each command is given a number (cmd_no) to keep track of which commands have
  8been processed.
  9
 10Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
 11"""
 12
 13import abc
 14import os
 15import time
 16from abc import abstractmethod
 17from typing import Union
 18
 19from result import Err, Ok, Result
 20
 21from ..macro import HPLCAvailStatus, Command, Response, Status
 22
 23
 24class ABCCommunicationController(abc.ABC):
 25    """Abstract class representing the communication controller.
 26
 27    :param comm_dir: the complete directory path that was used in the MACRO file, common file that pychemstation and Chemstation use to communicate.
 28    :param cmd_file: name of the write file that pychemstation writes MACROs to, in `comm_dir`
 29    :param reply_file: name of the read file that Chemstation replies to, in `comm_dir
 30    :param offline: whether or not communication with Chemstation is to be established
 31    :param debug: if True, prints all send MACROs to an out.txt file
 32    """
 33
 34    # maximum command number
 35    MAX_CMD_NO = 255
 36
 37    def __init__(
 38        self,
 39        comm_dir: str,
 40        cmd_file: str = "cmd",
 41        reply_file: str = "reply",
 42        offline: bool = False,
 43        debug: bool = False,
 44    ):
 45        if not offline:
 46            self.debug = debug
 47            if os.path.isdir(comm_dir):
 48                self.cmd_file = os.path.join(comm_dir, cmd_file)
 49                self.reply_file = os.path.join(comm_dir, reply_file)
 50                self.cmd_no = 0
 51            else:
 52                raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
 53
 54            # Create files for Chemstation to communicate with Python
 55            open(self.cmd_file, "a").close()
 56            open(self.reply_file, "a").close()
 57
 58            self.reset_cmd_counter()
 59            self._most_recent_hplc_status: Status = self.get_status()
 60
 61    @abstractmethod
 62    def get_num_val(self, cmd: str) -> Union[int, float]:
 63        pass
 64
 65    @abstractmethod
 66    def get_text_val(self, cmd: str) -> str:
 67        pass
 68
 69    @abstractmethod
 70    def get_status(self) -> Status:
 71        pass
 72
 73    @abstractmethod
 74    def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
 75        pass
 76
 77    @abstractmethod
 78    def _receive(self, cmd_no: int, num_attempts=100) -> Result[str, str]:
 79        pass
 80
 81    def set_status(self):
 82        """Updates current status of HPLC machine"""
 83        self._most_recent_hplc_status = self.get_status()
 84
 85    def check_if_not_running(self) -> bool:
 86        """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
 87
 88        :return: whether the HPLC machine is in a safe state to retrieve data back."""
 89        self.set_status()
 90        hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
 91        time.sleep(10)
 92        self.set_status()
 93        hplc_actually_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
 94        time.sleep(10)
 95        self.set_status()
 96        hplc_final_check_avail = isinstance(
 97            self._most_recent_hplc_status, HPLCAvailStatus
 98        )
 99        return hplc_avail and hplc_actually_avail and hplc_final_check_avail
100
101    def sleepy_send(self, cmd: Union[Command, str]):
102        self.send("Sleep 0.1")
103        self.send(cmd)
104        self.send("Sleep 0.1")
105
106    def send(self, cmd: Union[Command, str]):
107        """Sends a command to Chemstation.
108
109        :param cmd: Command to be sent to HPLC
110        """
111        if self.cmd_no == self.MAX_CMD_NO:
112            self.reset_cmd_counter()
113
114        cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
115        self.cmd_no += 1
116        self._send(cmd_to_send, self.cmd_no)
117        if self.debug:
118            f = open("out.txt", "a")
119            f.write(cmd_to_send + "\n")
120            f.close()
121
122    def receive(self) -> Result[Response, str]:
123        """Returns messages received in reply file.
124
125        :return: ChemStation response
126        """
127        num_response_prefix = "Numerical Responses:"
128        str_response_prefix = "String Responses:"
129        possible_response = self._receive(self.cmd_no)
130        if possible_response.is_ok():
131            lines = possible_response.ok_value.splitlines()
132            for line in lines:
133                if str_response_prefix in line and num_response_prefix in line:
134                    string_responses_dirty, _, numerical_responses = line.partition(
135                        num_response_prefix
136                    )
137                    _, _, string_responses = string_responses_dirty.partition(
138                        str_response_prefix
139                    )
140                    return Ok(
141                        Response(
142                            string_response=string_responses.strip(),
143                            num_response=float(numerical_responses.strip()),
144                        )
145                    )
146            return Err("Could not retrieve HPLC response")
147        else:
148            return Err(f"Could not establish response to HPLC: {possible_response}")
149
150    def reset_cmd_counter(self):
151        """Resets the command counter."""
152        self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
153        self._receive(cmd_no=self.MAX_CMD_NO + 1)
154        self.cmd_no = 0
155
156    def stop_macro(self):
157        """Stops Macro execution. Connection will be lost."""
158        self.send(Command.STOP_MACRO_CMD)
class ABCCommunicationController(abc.ABC):
 25class ABCCommunicationController(abc.ABC):
 26    """Abstract class representing the communication controller.
 27
 28    :param comm_dir: the complete directory path that was used in the MACRO file, common file that pychemstation and Chemstation use to communicate.
 29    :param cmd_file: name of the write file that pychemstation writes MACROs to, in `comm_dir`
 30    :param reply_file: name of the read file that Chemstation replies to, in `comm_dir
 31    :param offline: whether or not communication with Chemstation is to be established
 32    :param debug: if True, prints all send MACROs to an out.txt file
 33    """
 34
 35    # maximum command number
 36    MAX_CMD_NO = 255
 37
 38    def __init__(
 39        self,
 40        comm_dir: str,
 41        cmd_file: str = "cmd",
 42        reply_file: str = "reply",
 43        offline: bool = False,
 44        debug: bool = False,
 45    ):
 46        if not offline:
 47            self.debug = debug
 48            if os.path.isdir(comm_dir):
 49                self.cmd_file = os.path.join(comm_dir, cmd_file)
 50                self.reply_file = os.path.join(comm_dir, reply_file)
 51                self.cmd_no = 0
 52            else:
 53                raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
 54
 55            # Create files for Chemstation to communicate with Python
 56            open(self.cmd_file, "a").close()
 57            open(self.reply_file, "a").close()
 58
 59            self.reset_cmd_counter()
 60            self._most_recent_hplc_status: Status = self.get_status()
 61
 62    @abstractmethod
 63    def get_num_val(self, cmd: str) -> Union[int, float]:
 64        pass
 65
 66    @abstractmethod
 67    def get_text_val(self, cmd: str) -> str:
 68        pass
 69
 70    @abstractmethod
 71    def get_status(self) -> Status:
 72        pass
 73
 74    @abstractmethod
 75    def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
 76        pass
 77
 78    @abstractmethod
 79    def _receive(self, cmd_no: int, num_attempts=100) -> Result[str, str]:
 80        pass
 81
 82    def set_status(self):
 83        """Updates current status of HPLC machine"""
 84        self._most_recent_hplc_status = self.get_status()
 85
 86    def check_if_not_running(self) -> bool:
 87        """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
 88
 89        :return: whether the HPLC machine is in a safe state to retrieve data back."""
 90        self.set_status()
 91        hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
 92        time.sleep(10)
 93        self.set_status()
 94        hplc_actually_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
 95        time.sleep(10)
 96        self.set_status()
 97        hplc_final_check_avail = isinstance(
 98            self._most_recent_hplc_status, HPLCAvailStatus
 99        )
100        return hplc_avail and hplc_actually_avail and hplc_final_check_avail
101
102    def sleepy_send(self, cmd: Union[Command, str]):
103        self.send("Sleep 0.1")
104        self.send(cmd)
105        self.send("Sleep 0.1")
106
107    def send(self, cmd: Union[Command, str]):
108        """Sends a command to Chemstation.
109
110        :param cmd: Command to be sent to HPLC
111        """
112        if self.cmd_no == self.MAX_CMD_NO:
113            self.reset_cmd_counter()
114
115        cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
116        self.cmd_no += 1
117        self._send(cmd_to_send, self.cmd_no)
118        if self.debug:
119            f = open("out.txt", "a")
120            f.write(cmd_to_send + "\n")
121            f.close()
122
123    def receive(self) -> Result[Response, str]:
124        """Returns messages received in reply file.
125
126        :return: ChemStation response
127        """
128        num_response_prefix = "Numerical Responses:"
129        str_response_prefix = "String Responses:"
130        possible_response = self._receive(self.cmd_no)
131        if possible_response.is_ok():
132            lines = possible_response.ok_value.splitlines()
133            for line in lines:
134                if str_response_prefix in line and num_response_prefix in line:
135                    string_responses_dirty, _, numerical_responses = line.partition(
136                        num_response_prefix
137                    )
138                    _, _, string_responses = string_responses_dirty.partition(
139                        str_response_prefix
140                    )
141                    return Ok(
142                        Response(
143                            string_response=string_responses.strip(),
144                            num_response=float(numerical_responses.strip()),
145                        )
146                    )
147            return Err("Could not retrieve HPLC response")
148        else:
149            return Err(f"Could not establish response to HPLC: {possible_response}")
150
151    def reset_cmd_counter(self):
152        """Resets the command counter."""
153        self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
154        self._receive(cmd_no=self.MAX_CMD_NO + 1)
155        self.cmd_no = 0
156
157    def stop_macro(self):
158        """Stops Macro execution. Connection will be lost."""
159        self.send(Command.STOP_MACRO_CMD)

Abstract class representing the communication controller.

Parameters
  • comm_dir: the complete directory path that was used in the MACRO file, common file that pychemstation and Chemstation use to communicate.
  • cmd_file: name of the write file that pychemstation writes MACROs to, in comm_dir
  • reply_file: name of the read file that Chemstation replies to, in `comm_dir
  • offline: whether or not communication with Chemstation is to be established
  • debug: if True, prints all send MACROs to an out.txt file
MAX_CMD_NO = 255
@abstractmethod
def get_num_val(self, cmd: str) -> Union[int, float]:
62    @abstractmethod
63    def get_num_val(self, cmd: str) -> Union[int, float]:
64        pass
@abstractmethod
def get_text_val(self, cmd: str) -> str:
66    @abstractmethod
67    def get_text_val(self, cmd: str) -> str:
68        pass
70    @abstractmethod
71    def get_status(self) -> Status:
72        pass
def set_status(self):
82    def set_status(self):
83        """Updates current status of HPLC machine"""
84        self._most_recent_hplc_status = self.get_status()

Updates current status of HPLC machine

def check_if_not_running(self) -> bool:
 86    def check_if_not_running(self) -> bool:
 87        """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
 88
 89        :return: whether the HPLC machine is in a safe state to retrieve data back."""
 90        self.set_status()
 91        hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
 92        time.sleep(10)
 93        self.set_status()
 94        hplc_actually_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
 95        time.sleep(10)
 96        self.set_status()
 97        hplc_final_check_avail = isinstance(
 98            self._most_recent_hplc_status, HPLCAvailStatus
 99        )
100        return hplc_avail and hplc_actually_avail and hplc_final_check_avail

Checks if HPLC machine is in an available state, meaning a state that data is not being written.

Returns

whether the HPLC machine is in a safe state to retrieve data back.

def sleepy_send(self, cmd: Union[pychemstation.utils.macro.Command, str]):
102    def sleepy_send(self, cmd: Union[Command, str]):
103        self.send("Sleep 0.1")
104        self.send(cmd)
105        self.send("Sleep 0.1")
def send(self, cmd: Union[pychemstation.utils.macro.Command, str]):
107    def send(self, cmd: Union[Command, str]):
108        """Sends a command to Chemstation.
109
110        :param cmd: Command to be sent to HPLC
111        """
112        if self.cmd_no == self.MAX_CMD_NO:
113            self.reset_cmd_counter()
114
115        cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
116        self.cmd_no += 1
117        self._send(cmd_to_send, self.cmd_no)
118        if self.debug:
119            f = open("out.txt", "a")
120            f.write(cmd_to_send + "\n")
121            f.close()

Sends a command to Chemstation.

Parameters
  • cmd: Command to be sent to HPLC
def receive( self) -> Union[result.result.Ok[pychemstation.utils.macro.Response], result.result.Err[str]]:
123    def receive(self) -> Result[Response, str]:
124        """Returns messages received in reply file.
125
126        :return: ChemStation response
127        """
128        num_response_prefix = "Numerical Responses:"
129        str_response_prefix = "String Responses:"
130        possible_response = self._receive(self.cmd_no)
131        if possible_response.is_ok():
132            lines = possible_response.ok_value.splitlines()
133            for line in lines:
134                if str_response_prefix in line and num_response_prefix in line:
135                    string_responses_dirty, _, numerical_responses = line.partition(
136                        num_response_prefix
137                    )
138                    _, _, string_responses = string_responses_dirty.partition(
139                        str_response_prefix
140                    )
141                    return Ok(
142                        Response(
143                            string_response=string_responses.strip(),
144                            num_response=float(numerical_responses.strip()),
145                        )
146                    )
147            return Err("Could not retrieve HPLC response")
148        else:
149            return Err(f"Could not establish response to HPLC: {possible_response}")

Returns messages received in reply file.

Returns

ChemStation response

def reset_cmd_counter(self):
151    def reset_cmd_counter(self):
152        """Resets the command counter."""
153        self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
154        self._receive(cmd_no=self.MAX_CMD_NO + 1)
155        self.cmd_no = 0

Resets the command counter.

def stop_macro(self):
157    def stop_macro(self):
158        """Stops Macro execution. Connection will be lost."""
159        self.send(Command.STOP_MACRO_CMD)

Stops Macro execution. Connection will be lost.