Source code for instruments.dressler.cesar_1312

"""Support for Dressler Cesar 1312 RF generator."""

# IMPORTS #####################################################################

from enum import IntEnum
from typing import List, Tuple, Union

from instruments.abstract_instruments import Instrument
from instruments.units import ureg as u
from instruments.util_fns import assume_units

# CLASSES #####################################################################


[docs] class Cesar1312(Instrument): """Communicate with the Dressler Cesar 1312 RF generator. Various connection options are available for different models. This driver has been tested using the RS-232 option. The instrument for which this driver was tested required odd parity mode. You must provide the correct parity for your device when opening the serial connection. Note that you must set the control mode to `ControlMode.Host` in order to send any commands from the computer to the device. Example: >>> import serial >>> import instruments as ik >>> port = '/dev/ttyUSB0' >>> baud = 115200 >>> inst = ik.dressler.Cesar1312.open_serial(port, baud, parity=serial.PARITY_ODD) >>> inst.control_mode = inst.ControlMode.Host >>> inst.rf # query RF state False >>> inst.rf = True # turn on RF """
[docs] class ControlMode(IntEnum): """Control modes of the Cesar 1312 RF generator.""" Host = 2 UserPort = 4 FrontPanel = 6
[docs] class RegulationMode(IntEnum): """Regulation modes of the Cesar 1312 RF generator.""" ForwardPower = 6 LoadPower = 7 ExternalPower = 8
def __init__(self, filelike): super().__init__(filelike) self._retries = 3 self._csr_codes = { 0: "OK", 1: "Command rejected because unit is in wrong control mode.", 2: "Command rejected because RF output is on.", 4: "Command rejected because data sent is out of range.", 5: "Command rejected because User Port RF signal is off.", 7: "Command rejected because active fault(s) exist in generator.", 9: "Command rejected because the data byte count is incorrect.", 19: "Command rejected because the recipe mode is active", 50: "Command rejected because the frequency is out of range.", 51: "Command rejected because the duty cycle is out of range.", 99: "Command not implemented.", } self._address = 0x01 self._ack = bytes([0x06]) self._nak = bytes([0x15]) # CLASS PROPERTIES # @property def address(self) -> int: """Set/get the address of the device. Note that an address of 0 is the broadcast address. Most likely, you can leave this at the default of 1. :return: The set address. :rtype: int """ return self._address @address.setter def address(self, value: int) -> None: if value < 0 or value > 31: raise ValueError("Address must be in the range 0-31.") self._address = value @property def retries(self) -> int: """Set/get the number of retries if a command fails. :return: The number of retries as an integer. :rtype: int """ return self._retries @retries.setter def retries(self, value: int) -> tuple[int, int, bytes]: if value < 0: raise ValueError("Retries must be greater than or equal to 0.") self._retries = value # INSTRUMENT PROPERTIES # @property def control_mode(self) -> ControlMode: """Set/get the active control of the RF generator. Possible values are given in the `ControlMode` enum. For computer control, you likely want to set this to `ControlMode.Host`. ..note:: If you set the control mode at any time back to `ControlMode.FrontPanel`, the RF will turn off. :return: The current control mode. :rtype: ControlMode Example: >>> inst.control_mode = ik.dressler.Cesar1312.ControlMode.Host >>> inst.control_mode <ControlMode.Host: 2> """ data = self.query(self._make_pkg(155)) return self.ControlMode(data[0]) @control_mode.setter def control_mode(self, value: ControlMode) -> None: self.sendcmd(self._make_pkg(14, self._make_data(1, value.value))) @property def name(self) -> str: """Get the supply type and size of the RF generator. :return: The supply type and size. :rtype: str Example: >>> inst.name 'CESAR_1312' """ name_type = self.query(self._make_pkg(128)).decode("utf-8") name_size = self.query(self._make_pkg(129)).decode("utf-8") return f"{name_type}{name_size}" @property def output_power(self) -> u.Quantity: """Set/get the output power of the device in W. :return: The output power in W for the defined mode. :rtype: u.Quantity Example: >>> inst.output_power = 10 * u.W >>> inst.output_power <Quantity(10, 'watt')> """ ret_data = self.query(self._make_pkg(164))[:2] return u.Quantity(int.from_bytes(ret_data, "little"), u.W) @output_power.setter def output_power(self, value: u.Quantity) -> None: value = assume_units(value, u.W).to(u.W) data = self._make_data(2, int(value.magnitude)) self.sendcmd(self._make_pkg(8, data)) @property def reflected_power(self) -> u.Quantity: """Get the reflected power in W. :return: The reflected power in W. :rtype: u.Quantity Example: >>> inst.reflected_power <Quantity(0, 'watt')> """ ret_data = self.query(self._make_pkg(166)) return u.Quantity(int.from_bytes(ret_data, "little"), u.W) @property def regulation_mode(self) -> RegulationMode: """Set/get the regulation mode. Possible values are given in the `RegulationMode` enum. :return: The current regulation mode. :rtype: RegulationMode Example: >>> inst.regulation_mode = ik.dressler.Cesar1312.RegulationMode.ForwardPower >>> inst.regulation_mode <RegulationMode.ForwardPower: 6> """ data = self.query(self._make_pkg(154)) return self.RegulationMode(data[0]) @regulation_mode.setter def regulation_mode(self, value: RegulationMode) -> None: self.sendcmd(self._make_pkg(3, self._make_data(1, value.value))) @property def rf(self) -> bool: """Set/get the RF output state of the device. RF on will be `True` while RF off will be `False`. :return: The current RF state. :rtype: bool Example: >>> inst.rf = True >>> inst.rf True """ data = self.query(self._make_pkg(162)) return bool(data[0] & 0b00100000) @rf.setter def rf(self, value: bool) -> None: cmd = 2 if value else 1 self.sendcmd(self._make_pkg(cmd)) # METHODS #
[docs] def query(self, package: bytes, len_data=1) -> bytes: """Send a package to the instrument, assert it's all good, and return answer. This sends the package and checks the response. If the response is NAK, it retries until an ACK is received or the number of retries is reached. Once an ACK is received, it listens for the response of the instrument parsed the header, command, and optinally the data length (if > 6), then listens to the data and checksum and ensures that the overallc hecksum is zero. If not, it will send a NAK and retry reading until the checksum is zero. Then it will send an ACK to finish the communication. :param bytes package: The package to send. :return: The data received from the device. :rtype: bytes """ tries = 0 got_ack = False while tries < self.retries + 1: self._file.write_raw(package) response = self.read_raw(1) if response == self._ack: got_ack = True break else: tries += 1 if not got_ack: raise OSError("Failed to get ACK from device after sending the command.") tries = 0 got_pkg = False while tries < self.retries: header = self.read_raw(1) cmd = self.read_raw(1) adr, dlength = self._unpack_header(header) optional_data_length = None if dlength == 0b111: optional_data_length = self.read_raw(1) dlength = int(optional_data_length.hex(), 16) data = self.read_raw(dlength) if dlength > 0 else None checksum = self.read_raw(1) pkg = header + cmd if optional_data_length: pkg += optional_data_length if data: pkg += data pkg += checksum if self._calculate_checksum(pkg) == bytes([0x0]): self._file.write_raw(self._ack) got_pkg = True break else: tries += 1 self._file.write_raw(self._nak) if not got_pkg: raise OSError("Failed to get a valid package from the device.") return data
[docs] def sendcmd(self, pkg: bytes) -> None: """Send a package to the instrument and assert it's all good. Uses the query routine and interprets the data, which should be one byte, as a CSR. If the CSR is not OK (0), will print a warning with the message. :param bytes pkg: The package to send. """ data = self.query(pkg) if data: csr = int(data.hex(), 16) if csr != 0: raise OSError( f"{self._csr_codes.get(csr, 'Unknown error')} (CSR={csr})" ) else: raise ValueError("No data received from the device.")
def _make_data( self, length: Union[int, list[int]], data: Union[int, list[int]] ) -> bytes: """Create the data bytes for the package. If only one number is given, provide the length and the actual value as integers (or list). If more than one number is given, provide both as lists. :param Union[int, list[int]] length: The length of the data. :param Union[int, list[int]] data: The data to send. :return: Data in appropriate order. :rtype: bytes """ if isinstance(length, int): length = [length] if isinstance(data, int): data = [data] data_bytes = b"" for ll, dd in zip(length, data): data_bytes += dd.to_bytes(ll, byteorder="little", signed=False) return data_bytes def _make_pkg(self, cmd_number: int, data: Union[None, bytes] = None) -> bytes: """Make a package and return it packed as bytes. :param int cmd_number: The command number. :param bytes data: The data to send, already in proper order as bytes, or None. Defaults to None, which makes it a query command. """ data_length = len(data) if data else 0 header = self._pack_header(data_length) if data_length > 255: raise ValueError("Data length too long, must be <= 255.") if cmd_number > 255: raise ValueError("Command number too long, must be <= 255.") if data_length > 6: pkg = [header, cmd_number, data_length] else: pkg = [header, cmd_number] pkg = bytes(pkg) if data is not None: pkg += data pkg = pkg + self._calculate_checksum(pkg) return pkg @staticmethod def _calculate_checksum(data: bytes) -> bytes: """Calculate the checksum of the data. :param bytes data: The data to calculate the checksum for. :return: Checksum. :rtype: bytes """ checksum = data[0] for it, bt in enumerate(data): if it > 0: checksum ^= bt return bytes([checksum]) def _pack_header(self, data_length: int): """Make the header of the package. :param int data_length: The length of the data. If > 6, will be set to 7. :return: The header as an integer. """ if data_length > 6: data_length = 7 # need an extra byte for data length return (self._address << 3) + data_length @staticmethod def _unpack_header(hdr: bytes) -> tuple[int]: """Parse the header and return address and data length. :param bytes hdr: The header byte. :return: The address and data length as integers. :rtype: tuple[int] """ addr = hdr[0] >> 3 data_length = hdr[0] & 0b00000111 return addr, data_length