class IrisCryoValveStreamInterface(StreamInterface): commands = { Cmd("get_status", "^\?$"), Cmd("set_open", "^OPEN$"), Cmd("set_closed", "^CLOSE$"), } in_terminator = "\r" out_terminator = "\r" def get_status(self): status = "OPEN" if self._device.is_open else "CLOSED" return "SOLENOID " + status def set_open(self): self._device.is_open = True return "" def set_closed(self): self._device.is_open = False return "" def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error))
class FlipprpsStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation commands = { Cmd("set_polarity_down", "^dn$"), Cmd("set_polarity_up", "^up$"), Cmd("get_id", "^id$"), } in_terminator = "\r\n" out_terminator = in_terminator @if_connected def get_id(self): return self._device.id @if_connected def set_polarity_down(self): self._device.polarity = 0 return "OK" @if_connected def set_polarity_up(self): self._device.polarity = 1 return "OK" def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error))
class VerySimpleInterface(StreamAdapter): """ A very simple device with TCP-stream interface The device has only one parameter, which can be set to an arbitrary value. The interface consists of two commands which can be invoked via telnet. To connect: $ telnet host port After that, typing either of the commands and pressing enter sends them to the server. """ commands = { Cmd('get_param', '^P$'), Cmd('set_param', '^P=(.+)$'), } in_terminator = '\r\n' out_terminator = '\r\n' def get_param(self): """Returns the device parameter.""" return self._device.param def set_param(self, new_param): """Set the device parameter, does not return anything.""" self._device.param = new_param def handle_error(self, request, error): return 'An error occurred: ' + repr(error)
class Kynctm3KStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation commands = { Cmd("return_data", "MM,1111111111111111$"), Cmd("change_input_mode", "(Q0|R0|R1)$"), Cmd("toggle_autosend", "SW,EA,(1|0)$") } in_terminator = "\r" out_terminator = "\r" def return_data(self): return self._device.format_output_data() def change_input_mode(self, new_state): return self._device.set_input_mode(new_state) def toggle_autosend(self, new_state): return self._device.set_autosend_status(int(new_state)) def handle_error(self, request, error): err = "An error occurred at request " + repr(request) + ": " + repr(error) print(err) self.log.info(err) return str(err)
class CCD100StreamInterface(StreamInterface): commands = { Cmd("get_sp", "^[a-h]" + SP_COMM + "\?$"), Cmd("set_sp", "^([a-h])" + SP_COMM + " ([\-0-9.]+)$", argument_mappings=[str, float]), Cmd("get_units", "^[a-h]" + UNITS_COMM + "\?$"), Cmd("set_units", "^([a-h])" + UNITS_COMM + " ([a-z]+)$"), Cmd("get_reading", "^[a-h]" + READING_COMM + "$"), } in_terminator = "\r\n" good_out_terminator = "\r\r\n" out_echo = "*{}*:{};{}" out_response = "!{}!o!" def create_response(self, command, params=" ", data=None): if self._device.is_giving_errors: self.out_terminator = self._device.out_terminator_in_error return self._device.out_error else: self.out_terminator = self.good_out_terminator out = self.out_echo.format(self._device.address, command, params) + self.out_terminator if data: out += data + self.out_terminator out += self.out_response.format(self._device.address) return out @if_connected def get_sp(self): return self.create_response(SP_COMM + "?", data="SP VALUE: " + str(self._device.setpoint) + " ") @if_connected def set_sp(self, addr, new_sp): if self._device.address == addr: self._device.setpoint = new_sp return self.create_response(SP_COMM) @if_connected def get_units(self): return self.create_response(UNITS_COMM + "?", data="INPUT UNITS STR: " + str(self._device.units)) @if_connected def set_units(self, addr, new_units): if self._device.address == addr: self._device.units = new_units return self.create_response(UNITS_COMM) @if_connected def get_reading(self): min_width = 10 data_str = "READ:" + "{:0.3f}".format(self._device.current_reading).ljust(min_width) + ";" + str(self._device.setpoint_mode) return self.create_response(READING_COMM + " ", data=data_str) def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error))
class ExampleMotorStreamInterface(StreamAdapter): """ TCP-stream based example motor interface This motor simulation can be controlled via telnet: $ telnet host port Where the host and port-parameter are part of the dynamically created documentation for a concrete device instance. The motor starts moving immediately when a new target position is set. Once it's moving, it has to be stopped to receive a new target, otherwise an error is generated. """ commands = { Cmd('get_status', r'^S\?$'), Cmd('get_position', r'^P\?$'), Cmd('get_target', r'^T\?$'), Cmd('set_target', r'^T=([-+]?[0-9]*\.?[0-9]+)$', argument_mappings=(float, )), Cmd('stop', r'^H$', return_mapping=lambda x: 'T={},P={}'.format(x[0], x[1])), } in_terminator = '\r\n' out_terminator = '\r\n' def get_status(self): """Returns the status of the device, which is one of 'idle' or 'moving'.""" return self._device.state def get_position(self): """Returns the current position in mm.""" return self._device.position def get_target(self): """Returns the current target in mm.""" return self._device.target def set_target(self, new_target): """ Sets the new target in mm, the movement starts immediately. If the value is outside the interval [0, 250] or the motor is already moving, an error is returned, otherwise the new target is returned.""" try: self._device.target = new_target return 'T={}'.format(new_target) except RuntimeError: return 'err: not idle' except ValueError: return 'err: not 0<=T<=250'
class ExampleMotorStreamInterface(StreamInterface): """ TCP-stream based example motor interface This motor simulation can be controlled via telnet: $ telnet host port Where the host and port-parameter are part of the dynamically created documentation for a concrete device instance. The motor starts moving immediately when a new target position is set. Once it's moving, it has to be stopped to receive a new target, otherwise an error is generated. """ commands = { Cmd("get_status", regex(r"^S\?$")), # explicit regex Cmd("get_position", r"^P\?$"), # implicit regex Cmd("get_target", r"^T\?$"), Cmd("set_target", scanf("T=%f")), # scanf format specification Cmd("stop", r"^H$", return_mapping=lambda x: "T={},P={}".format(x[0], x[1])), } in_terminator = "\r\n" out_terminator = "\r\n" def get_status(self): """Returns the status of the device, which is one of 'idle' or 'moving'.""" return self.device.state def get_position(self): """Returns the current position in mm.""" return self.device.position def get_target(self): """Returns the current target in mm.""" return self.device.target def set_target(self, new_target): """ Sets the new target in mm, the movement starts immediately. If the value is outside the interval [0, 250] or the motor is already moving, an error is returned, otherwise the new target is returned.""" try: self.device.target = new_target return "T={}".format(new_target) except RuntimeError: return "err: not idle" except ValueError: return "err: not 0<=T<=250"
class IegStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation commands = { Cmd("get_status", "^&STS0$"), Cmd("change_operating_mode", "^&OPM([1-4])$"), Cmd("abort", "^&KILL$"), } in_terminator = "!" # Out terminator is defined in ResponseBuilder instead as we need to add it to two messages. out_terminator = "" def _build_valve_state(self): val = 0 val += 1 if self._device.is_pump_valve_open() else 0 val += 2 if self._device.is_buffer_valve_open() else 0 val += 4 if self._device.is_gas_valve_open() else 0 return val def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error)) return str(error) def get_status(self): return ResponseBuilder() \ .add_data_block("IEG", self._device.get_id()) \ .add_data_block("OPM", self._device.get_operating_mode()) \ .add_data_block("VST", self._build_valve_state()) \ .add_data_block("ERR", self._device.get_error()) \ .add_data_block("BPH", 0 if self._device.is_buffer_pressure_high() else 1) \ .add_data_block("SPL", 1 if self._device.is_sample_pressure_low() else 0) \ .add_data_block("SPH", 1 if self._device.is_sample_pressure_high() else 0) \ .add_data_block("SPR", int(self._device.get_pressure())) \ .build() def change_operating_mode(self, mode): self._device.set_operating_mode(int(mode)) return ResponseBuilder()\ .add_data_block("IEG", self._device.get_id()) \ .add_data_block("OPM", self._device.get_operating_mode())\ .build() def abort(self): self._device.operatingmode = 0 return ResponseBuilder() \ .add_data_block("IEG", self._device.get_id()) \ .add_data_block("KILL") \ .build()
class VerySimpleInterface(StreamInterface): """ A very simple device with TCP-stream interface The device has only one parameter, which can be set to an arbitrary value. The interface consists of five commands which can be invoked via telnet. To connect: $ telnet host port After that, typing either of the commands and pressing enter sends them to the server. The commands are: - ``V``: Returns the parameter as part of a verbose message. - ``V=something``: Sets the parameter to ``something``. - ``P``: Returns the device parameter unmodified. - ``P=something``: Exactly the same as ``V=something``. - ``R`` or ``r``: Returns the number 4. """ commands = { Cmd("get_param", pattern="^V$", return_mapping="The value is {}".format), Cmd("set_param", pattern="^V=(.+)$", argument_mappings=(int, )), Var( "param", read_pattern="^P$", write_pattern="^P=(.+)$", doc="The only parameter.", ), Cmd(lambda: 4, pattern="^R$(?i)", doc='"Random" number (4).'), } in_terminator = "\r\n" out_terminator = "\r\n" def get_param(self): """Returns the device parameter.""" return self.device.param def set_param(self, new_param): """Set the device parameter, does not return anything.""" self.device.param = new_param def handle_error(self, request, error): return "An error occurred: " + repr(error)
class Kynctm3KStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation commands = { Cmd("return_data", "MM,1111111111111111$"), CmdBuilder("change_input_mode").enum("Q0", "R0", "R1").build(), CmdBuilder("toggle_autosend").escape("SW,EA,").enum("1", "0").build() } in_terminator = "\r" out_terminator = "\r" def return_data(self): return_data = self._device.format_output_data() self.log.info("Returning {}".format(return_data)) return return_data def change_input_mode(self, new_state): return self._device.set_input_mode(new_state) def toggle_autosend(self, new_state): return self._device.set_autosend_status(int(new_state)) def handle_error(self, request, error): err = "An error occurred at request {}: {}".format(request, error) self.log.error(err) return str(err)
class TimeTerminatedInterface(StreamInterface): """ A simple device where commands are terminated by a timeout. This demonstrates how to implement devices that do not have standard terminators and where a command is considered terminated after a certain time delay of not receiving more data. To interact with this device, you must switch telnet into char mode, or use netcat with special tty settings: $ telnet host port ^] telnet> mode char [type command and wait] $ stty -icanon && nc host port hello world! foobar! The following commands are available: - ``hello ``: Reply with "world!" - ``foo``: Replay with "bar!" - ``P``: Returns the device parameter - ``P=something``: Set parameter to specified value """ commands = { # Space as \x20 represents a custom 'terminator' for this command only # However, waiting for the timeout still applies Cmd("say_world", pattern=scanf("hello\x20")), Cmd("say_bar", pattern=scanf("foo")), Var("param", read_pattern=scanf("P"), write_pattern=scanf("P=%d")), } # An empty in_terminator triggers "timeout mode" # Otherwise, a ReadTimeout is considered an error. in_terminator = "" out_terminator = "\r\n" # Unusually long, for easier manual entry readtimeout = 2500 def handle_error(self, request, error): return "An error occurred: " + repr(error)
class TTIEX355PStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation commands = { Cmd("catch_all", "^#9.*$"), # Catch-all command for debugging } def catch_all(self): pass def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error))
def build(self, *args, **kwargs): """ Builds the CMd object based on the target and regular expression. :param args: arguments to pass to Cmd constructor :param kwargs: key word arguments to pass to Cmd constructor :return: Cmd object """ if self._ignore_case: pattern = regex(self._reg_ex) pattern.compiled_pattern = re.compile(self._reg_ex.encode(), re.IGNORECASE) else: pattern = self._reg_ex return Cmd(self._target_method, pattern, argument_mappings=self.argument_mappings, *args, **kwargs)
class JulaboStreamInterfaceV2(StreamInterface): """Julabos can have different commands sets depending on the version number of the hardware. This protocol matches that for: FP50-HE (unconfirmed). """ protocol = "julabo-version-2" commands = { Var('temperature', read_pattern='^IN_PV_00$', doc='The bath temperature.'), Var('external_temperature', read_pattern='^IN_PV_01$', doc='The external temperature.'), Var('heating_power', read_pattern='^IN_PV_02$', doc='The heating power.'), Var('set_point_temperature', read_pattern='^IN_SP_00$', doc='The temperature setpoint.'), Cmd('set_set_point', '^OUT_SP_00 ([0-9]*\.?[0-9]+)$', argument_mappings=(float,)), # Read pattern for high limit is different from version 1 Var('temperature_high_limit', read_pattern='^IN_SP_03$', doc='The high limit - usually set in the hardware.'), # Read pattern for low limit is different from version 1 Var('temperature_low_limit', read_pattern='^IN_SP_04$', doc='The low limit - usually set in the hardware.'), Var('version', read_pattern='^VERSION$', doc='The Julabo version.'), Var('status', read_pattern='^STATUS$', doc='The Julabo status.'), Var('is_circulating', read_pattern='^IN_MODE_05$', doc='Whether it is circulating.'), Cmd('set_circulating', '^OUT_MODE_05 (0|1)$', argument_mappings=(int,)), Var('internal_p', read_pattern='^IN_PAR_06$', doc='The internal proportional.'), Cmd('set_internal_p', '^OUT_PAR_06 ([0-9]*\.?[0-9]+)$', argument_mappings=(float,)), Var('internal_i', read_pattern='^IN_PAR_07$', doc='The internal integral.'), Cmd('set_internal_i', '^OUT_PAR_07 ([0-9]*)$', argument_mappings=(int,)), Var('internal_d', read_pattern='^IN_PAR_08$', doc='The internal derivative.'), Cmd('set_internal_d', '^OUT_PAR_08 ([0-9]*)$', argument_mappings=(int,)), Var('external_p', read_pattern='^IN_PAR_09$', doc='The external proportional.'), Cmd('set_external_p', '^OUT_PAR_09 ([0-9]*\.?[0-9]+)$', argument_mappings=(float,)), Var('external_i', read_pattern='^IN_PAR_11$', doc='The external integral.'), Cmd('set_external_i', '^OUT_PAR_11 ([0-9]*)$', argument_mappings=(int,)), Var('external_d', read_pattern='^IN_PAR_12$', doc='The external derivative.'), Cmd('set_external_d', '^OUT_PAR_12 ([0-9]*)$', argument_mappings=(int,)), } in_terminator = '\r' out_terminator = '\n' # Different from version 1
class Keithley2400StreamInterface(StreamInterface): # Commands that we expect via serial during normal operation serial_commands = { Cmd("get_values", "^:READ\?$"), Cmd("get_values", "\:MEAS:VOLT\?$"), Cmd("get_values", "\:MEAS:CURR\?$"), Cmd("get_values", "\:MEAS:RES\?$"), Cmd("reset", "^\*RST$"), Cmd("identify", "^\*IDN?"), Cmd("set_output_mode", "^\:OUTP\s(1|0)$"), Cmd("get_output_mode", "^\:OUTP\?$"), Cmd("set_offset_compensation_mode", "^\:SENS:RES:OCOM\s(1|0)$"), Cmd("get_offset_compensation_mode", "^\:SENS:RES:OCOM\?$"), Cmd("set_resistance_mode", "^\:SENS:RES:MODE\s(AUTO|MAN)$"), Cmd("get_resistance_mode", "^\:SENS:RES:MODE\?$"), Cmd("set_remote_sensing_mode", "^\:SYST:RSEN\s(1|0)$"), Cmd("get_remote_sensing_mode", "^\:SYST:RSEN\?$"), Cmd("set_resistance_range_mode", "^\:SENS:RES:RANG:AUTO\s(0|1)$"), Cmd("get_resistance_range_mode", "^\:SENS:RES:RANG:AUTO\?$"), Cmd("set_resistance_range", "^\:SENS:RES:RANG\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), Cmd("get_resistance_range", "^\:SENS:RES:RANG\?$"), Cmd("set_source_mode", "^\:SOUR:FUNC\s(CURR|VOLT)$"), Cmd("get_source_mode", "^\:SOUR:FUNC\?$"), Cmd("set_current_compliance", "^\:SENS:CURR:PROT\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), Cmd("get_current_compliance", "^\:SENS:CURR:PROT\?$"), Cmd("set_voltage_compliance", "^\:SENS:VOLT:PROT\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), Cmd("get_voltage_compliance", "^\:SENS:VOLT:PROT\?$"), Cmd("set_source_voltage", "^\:SOUR:VOLT:LEV\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), Cmd("get_source_voltage", "^\:SOUR:VOLT:LEV\?$"), Cmd("set_source_current", "^\:SOUR:CURR:LEV\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), Cmd("get_source_current", "^\:SOUR:CURR:LEV\?$"), Cmd("set_source_current_autorange_mode", "^\:SOUR:CURR:RANG:AUTO\s(1|0)$"), Cmd("get_source_current_autorange_mode", "^\:SOUR:CURR:RANG:AUTO\?$"), Cmd("set_source_voltage_autorange_mode", "^\:SOUR:VOLT:RANG:AUTO\s(1|0)$"), Cmd("get_source_voltage_autorange_mode", "^\:SOUR:VOLT:RANG:AUTO\?$"), Cmd("get_source_current_range", "^\:SOUR:CURR:RANG\?$"), Cmd("set_source_current_range", "^\:SOUR:CURR:RANG\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), Cmd("get_source_voltage_range", "^\:SOUR:VOLT:RANG\?$"), Cmd("set_source_voltage_range", "^\:SOUR:VOLT:RANG\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), Cmd("set_measured_voltage_autorange_mode", "^\:SENS:VOLT:RANG:AUTO\s(1|0)$"), Cmd("get_measured_voltage_autorange_mode", "^\:SENS:VOLT:RANG:AUTO\?$"), Cmd("set_measured_current_autorange_mode", "^\:SENS:CURR:RANG:AUTO\s(1|0)$"), Cmd("get_measured_current_autorange_mode", "^\:SENS:CURR:RANG:AUTO\?$"), Cmd("get_measured_current_range", "^\:SENS:CURR:RANG\?$"), Cmd("set_measured_current_range", "^\:SENS:CURR:RANG\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), Cmd("get_measured_voltage_range", "^\:SENS:VOLT:RANG\?$"), Cmd("set_measured_voltage_range", "^\:SENS:VOLT:RANG\s([-+]?[0-9]*\.?[0-9]*e?[-+]?[0-9]+$)"), } # Private control commands that can be used as an alternative to the lewis backdoor control_commands = { Cmd("set_voltage", "^\:_CTRL:VOLT\s([-+]?[0-9]*\.?[0-9]+)$"), Cmd("set_current", "^\:_CTRL:CURR\s([-+]?[0-9]*\.?[0-9]+)$"), } commands = set.union(serial_commands, control_commands) in_terminator = "\r\n" out_terminator = "\r\n" def get_values(self): """ Get the current, voltage and resistance readings :return: A string of 3 doubles: voltage, current, resistance. In that order """ return ", ".join([ self._device.get_voltage(as_string=True), self._device.get_current(as_string=True), self._device.get_resistance(as_string=True) ]) if self._device.get_output_mode() == OutputMode.ON else None def reset(self): """ Resets the device. """ self._device.reset() return "*RST" def identify(self): """ Replies with the device's identity. """ return "Keithley 2400 Source Meter emulator" def set_current(self, value): self._device.set_current(float(value)) return "Current set to: " + str(value) def set_voltage(self, value): self._device.set_voltage(float(value)) return "Voltage set to: " + str(value) def _set_mode(self, set_method, mode, command): """ The generic form of how mode sets are executed and responded to. """ set_method(mode) return command + " " + mode def set_output_mode(self, new_mode): return self._set_mode(self._device.set_output_mode, new_mode, "OUTP:") def get_output_mode(self): return self._device.get_output_mode() def set_offset_compensation_mode(self, new_mode): return self._set_mode(self._device.set_offset_compensation_mode, new_mode, ":SENS:RES:OCOM") def get_offset_compensation_mode(self): return self._device.get_offset_compensation_mode() def set_resistance_mode(self, new_mode): return self._set_mode(self._device.set_resistance_mode, new_mode, ":SENS:RES:MODE") def get_resistance_mode(self): return self._device.get_resistance_mode() def set_remote_sensing_mode(self, new_mode): return self._set_mode(self._device.set_remote_sensing_mode, new_mode, ":SYST:RSEN") def get_remote_sensing_mode(self): return self._device.get_remote_sensing_mode() def set_resistance_range_mode(self, new_mode): return self._set_mode(self._device.set_resistance_range_mode, new_mode, ":SENS:RES:RANG:AUTO") def get_resistance_range_mode(self): return self._device.get_resistance_range_mode() def set_resistance_range(self, value): return self._device.set_resistance_range(float(value)) def get_resistance_range(self): return self._device.get_resistance_range() def set_source_mode(self, new_mode): return self._set_mode(self._device.set_source_mode, new_mode, ":SOUR:FUNC") def get_source_mode(self): return self._device.get_source_mode() def set_current_compliance(self, value): return self._device.set_current_compliance(float(value)) def get_current_compliance(self): return self._device.get_current_compliance() def set_voltage_compliance(self, value): return self._device.set_voltage_compliance(float(value)) def get_voltage_compliance(self): return self._device.get_voltage_compliance() def set_source_voltage(self, value): return self._device.set_source_voltage(float(value)) def get_source_voltage(self): return self._device.get_source_voltage() def set_source_current(self, value): return self._device.set_source_current(float(value)) def get_source_current(self): return self._device.get_source_current() def get_source_current_autorange_mode(self): return self._device.get_source_current_autorange_mode() def set_source_current_autorange_mode(self, value): return self._device.set_source_current_autorange_mode(value) def get_source_voltage_autorange_mode(self): return self._device.get_source_voltage_autorange_mode() def set_source_voltage_autorange_mode(self, value): return self._device.set_source_voltage_autorange_mode(value) @has_log def handle_error(self, request, error): err = "An error occurred at request {}: {}".format(str(request), str(error)) print(err) self.log.info(err) return str(err) def set_source_current_range(self, value): return self._device.set_source_current_range(float(value)) def get_source_current_range(self): return self._device.get_source_current_range() def set_source_voltage_range(self, value): return self._device.set_source_voltage_range(float(value)) def get_source_voltage_range(self): return self._device.get_source_voltage_range() def get_measured_voltage_autorange_mode(self): return self._device.get_measured_voltage_autorange_mode() def set_measured_voltage_autorange_mode(self, value): return self._device.set_measured_voltage_autorange_mode(value) def get_measured_current_autorange_mode(self): return self._device.get_measured_current_autorange_mode() def set_measured_current_autorange_mode(self, value): val = self._device.set_measured_current_autorange_mode(value) return val
class AttocubeANC350StreamInterface(StreamInterface): # Commands that we expect via serial during normal operation. Match anything! commands = { Cmd("any_command", r"^([\s\S]*)$", return_mapping=lambda x:x), } in_terminator = "" out_terminator = b"" # Due to poll rate of the driver this will get individual commands readtimeout = 10 def handle_error(self, request, error): self.log.error("An error occurred at request " + repr(request) + ": " + repr(error)) return str(error) def any_command(self, command): response = '' if not self.device.connected: # Used rather than conditional_reply decorator to improve error message raise ValueError("Device simulating disconnection") while command: # Length doesn't include itself length = raw_bytes_to_int(command[:BYTES_IN_INT]) + BYTES_IN_INT response += self.handle_single_command(command[:length]) command = command[length:] return response def handle_single_command(self, command): length = raw_bytes_to_int(command[:BYTES_IN_INT]) opcode, address, index, correlation_num = convert_to_ints(command, BYTES_IN_INT, HEADER_LENGTH + 1) if length > HEADER_LENGTH: # This is probably a set command data = convert_to_ints(command, HEADER_LENGTH + BYTES_IN_INT, len(command)) # Length should describe command minus itself if len(command) - BYTES_IN_INT != length: raise ValueError("Told I would receive {} bytes but received {}".format(length, len(command) - BYTES_IN_INT)) if opcode == UC_GET: return self.get(address, index, correlation_num) elif opcode == UC_SET: return self.set(address, index, correlation_num, data) else: raise ValueError("Unrecognised opcode {}".format(opcode)) def set(self, address, index, correlation_num, data): self.log.info("Setting address {} with data {}".format(address, data[0])) command_mapping = { ID_ANC_TARGET: partial(self.device.set_position_setpoint, position=data[0]), ID_ANC_RUN_TARGET: self.device.move, ID_ANC_AMPL: partial(self.device.set_amplitude, data[0]), ID_ANC_AXIS_ON: partial(self.device.set_axis_on, data[0]), } try: command_mapping[address]() print("Device amp is {}".format(self.device.amplitude)) except KeyError: pass # Ignore unimplemented commands for now return generate_response(address, index, correlation_num) def get(self, address, index, correlation_num): self.log.info("Getting address {}".format(address)) command_mapping = { ID_ANC_COUNTER: int(self.device.position), ID_ANC_REFCOUNTER: 0, ID_ANC_STATUS: ANC_STATUS_REF_VALID + ANC_STATUS_ENABLE, ID_ANC_UNIT: 0x00, ID_ANC_REGSPD_SETP: self.device.speed, ID_ANC_SENSOR_VOLT: 2000, ID_ANC_MAX_AMP: 60000, ID_ANC_AMPL: self.device.amplitude, ID_ANC_FAST_FREQ: 1000, } try: data = command_mapping[address] except KeyError: data = 0 # Just return 0 for now return generate_response(address, index, correlation_num, data)
class FermichopperStreamInterface(StreamInterface): protocol = "fermi_maps" # Commands that we expect via serial during normal operation commands = { Cmd("get_all_data", "^#00000([0-9A-F]{2})$"), Cmd("execute_command", "^#1([0-9A-F]{4})([0-9A-F]{2})$"), Cmd("set_speed", "^#3([0-9A-F]{4})([0-9A-F]{2})$"), Cmd("set_delay_highword", "^#6([0-9A-F]{4})([0-9A-F]{2})$"), Cmd("set_delay_lowword", "^#5([0-9A-F]{4})([0-9A-F]{2})$"), Cmd("set_gate_width", "^#9([0-9A-F]{4})([0-9A-F]{2})$"), # Cmd("catch_all", "^#9.*$"), # Catch-all command for debugging } in_terminator = "$" out_terminator = "" # Catch all command for debugging if the IOC sends strange characters in the checksum. # def catch_all(self): # pass def build_status_code(self): status = 0 if True: # Microcontroller OK? status += 1 if self._device.get_true_speed() == self._device.get_speed_setpoint(): status += 2 if self._device.magneticbearing: status += 8 if self._device.get_voltage() > 0: status += 16 if self._device.drive: status += 32 if self._device.parameters == ChopperParameters.MERLIN_LARGE: status += 64 if False: # Interlock open? status += 128 if self._device.parameters == ChopperParameters.HET_MARI: status += 256 if self._device.parameters == ChopperParameters.MERLIN_SMALL: status += 512 if self._device.speed > 600: status += 1024 if self._device.speed > 10 and not self._device.magneticbearing: status += 2048 if any(abs(voltage) > 3 for voltage in [self._device.autozero_1_lower, self._device.autozero_2_lower, self._device.autozero_1_upper, self._device.autozero_2_upper]): status += 4096 return status def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error)) return str(error) def get_all_data(self, checksum): JulichChecksum.verify('#0', '0000', checksum) def autozero_calibrate(value): return (value + 7.0) / 0.0137 return JulichChecksum.append( '#1' + self._device.get_last_command()) \ + JulichChecksum.append( "#2{:04X}".format(self.build_status_code())) \ + JulichChecksum.append( "#3{:04X}".format(self._device.get_speed_setpoint() * 60)) \ + JulichChecksum.append( "#4{:04X}".format(int(round(self._device.get_true_speed() * 60)))) \ + JulichChecksum.append( "#5{:04X}".format(int(round((self._device.get_nominal_delay() * TIMING_FREQ_MHZ) % 65536)))) \ + JulichChecksum.append( "#6{:04X}".format(int(round((self._device.get_nominal_delay() * TIMING_FREQ_MHZ) / 65536)))) \ + JulichChecksum.append( "#7{:04X}".format(int(round((self._device.get_actual_delay() * TIMING_FREQ_MHZ) % 65536)))) \ + JulichChecksum.append( "#8{:04X}".format(int(round((self._device.get_actual_delay() * TIMING_FREQ_MHZ) / 65536)))) \ + JulichChecksum.append( "#9{:04X}".format(int(round(self._device.get_gate_width() * TIMING_FREQ_MHZ)))) \ + JulichChecksum.append( "#A{:04X}".format(int(round(self._device.get_current() / 0.00684)))) \ + JulichChecksum.append( "#B{:04X}".format(int(round(autozero_calibrate(self._device.autozero_1_upper))))) \ + JulichChecksum.append( "#C{:04X}".format(int(round(autozero_calibrate(self._device.autozero_2_upper))))) \ + JulichChecksum.append( "#D{:04X}".format(int(round(autozero_calibrate(self._device.autozero_1_lower))))) \ + JulichChecksum.append( "#E{:04X}".format(int(round(autozero_calibrate(self._device.autozero_2_lower))))) \ + "$" def execute_command(self, command, checksum): JulichChecksum.verify('#1', command, checksum) self._device.do_command(command) def set_speed(self, command, checksum): JulichChecksum.verify("#3", command, checksum) self._device.set_speed_setpoint(int(command, 16) / 60) def set_delay_lowword(self, command, checksum): JulichChecksum.verify('#5', command, checksum) self._device.set_delay_lowword(int(command, 16) / TIMING_FREQ_MHZ) def set_delay_highword(self, command, checksum): JulichChecksum.verify('#6', command, checksum) self._device.set_delay_highword(int(command, 16) / TIMING_FREQ_MHZ) def set_gate_width(self, command, checksum): JulichChecksum.verify('#9', command, checksum) self._device.set_gate_width(int(command, 16) / TIMING_FREQ_MHZ)
class Mk2ChopperStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation commands = { Cmd("get_true_frequency", "^RF$"), Cmd("get_demanded_frequency", "^RG$"), Cmd("get_true_phase_delay", "^RP$"), Cmd("get_demanded_phase_delay", "^RQ$"), Cmd("get_true_phase_error", "^RE$"), Cmd("get_demanded_phase_error_window", "^RW$"), Cmd("get_chopper_interlocks", "^RC$"), Cmd("get_spectral_interlocks", "^RS$"), Cmd("get_error_flags", "^RX$"), Cmd("read_all", "^RA$"), Cmd("set_chopper_started", "^WS([0-9]+)$"), Cmd("set_demanded_frequency", "^WM([0-9]+)$"), Cmd("set_demanded_phase_delay", "^WP([0-9]+)$"), Cmd("set_demanded_phase_error_window", "^WR([0-9]+)$") } in_terminator = "\r" out_terminator = "\r" def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error)) return str(error) def get_demanded_frequency(self): return "RG{0}".format( filled_int(self._device.get_demanded_frequency(), 3)) def get_true_frequency(self): return "RF{0}".format(filled_int(self._device.get_true_frequency(), 3)) def get_demanded_phase_delay(self): return "RQ{0}".format( filled_int(self._device.get_demanded_phase_delay(), 5)) def get_true_phase_delay(self): return "RP{0}".format( filled_int(self._device.get_true_phase_delay(), 5)) def get_demanded_phase_error_window(self): return "RW{0}".format( filled_int(self._device.get_demanded_phase_error_window(), 3)) def get_true_phase_error(self): return "RE{0}".format( filled_int(self._device.get_true_phase_error(), 3)) def get_spectral_interlocks(self): bits = [0] * 8 if self._device.get_manufacturer() == ChopperType.CORTINA: bits[0] = 1 if self._device.inverter_ready() else 0 bits[1] = 1 if self._device.motor_running() else 0 bits[2] = 1 if self._device.in_sync() else 0 elif self._device.get_manufacturer() == ChopperType.INDRAMAT: bits[0] = 1 if self._device.motor_running() else 0 bits[1] = 1 if self._device.reg_mode() else 0 bits[2] = 1 if self._device.in_sync() else 0 elif self._device.get_manufacturer() == ChopperType.SPECTRAL: bits[2] = 1 if self._device.external_fault() else 0 return "RS{0:8s}".format( Mk2ChopperStreamInterface._string_from_bits(bits)) def get_chopper_interlocks(self): bits = [0] * 8 bits[0] = 1 if self._device.get_system_frequency() == 50 else 0 bits[1] = 1 if self._device.clock_loss() else 0 bits[2] = 1 if self._device.bearing_1_overheat() else 0 bits[3] = 1 if self._device.bearing_2_overheat() else 0 bits[4] = 1 if self._device.motor_overheat() else 0 bits[5] = 1 if self._device.chopper_overspeed() else 0 return "RC{0:8s}".format( Mk2ChopperStreamInterface._string_from_bits(bits)) def get_error_flags(self): bits = [0] * 8 bits[0] = 1 if self._device.phase_delay_error() else 0 bits[1] = 1 if self._device.phase_delay_correction_error() else 0 bits[2] = 1 if self._device.phase_accuracy_window_error() else 0 return "RX{0:8s}".format( Mk2ChopperStreamInterface._string_from_bits(bits)) def get_manufacturer(self): return self._type.get_manufacturer() def set_chopper_started(self, start_flag_raw): try: start_flag = int(start_flag_raw) except ValueError: pass else: if start_flag == 1: self._device.start() elif start_flag == 2: self._device.stop() return def set_demanded_frequency(self, new_frequency_raw): return Mk2ChopperStreamInterface._set( new_frequency_raw, self.get_demanded_frequency, self._device.set_demanded_frequency) def set_demanded_phase_delay(self, new_phase_delay_raw): return Mk2ChopperStreamInterface._set( new_phase_delay_raw, self.get_demanded_phase_delay, self._device.set_demanded_phase_delay) def set_demanded_phase_error_window(self, new_phase_error_window_raw): return Mk2ChopperStreamInterface._set( new_phase_error_window_raw, self.get_demanded_phase_error_window, self._device.set_demanded_phase_error_window) def read_all(self): return "RA:Don't use, it causes the driver to lock up" @staticmethod def _set(raw, device_get, device_set): try: int_value = int(raw) except ValueError: pass else: device_set(int_value) return device_get() @staticmethod def _string_from_bits(bits): return "".join(str(n) for n in reversed(bits))
class InstronStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation commands = { Cmd("get_control_channel", "^Q300$"), Cmd("set_control_channel", "^C300,([1-3])$"), Cmd("disable_watchdog", "^C904,0$"), Cmd("get_control_mode", "^Q909$"), Cmd("set_control_mode", "^P909,([0-1])$"), Cmd("get_status", "^Q22$"), Cmd("arbitrary_command", "^([a-z]*)$"), Cmd("get_actuator_status", "^Q23$"), Cmd("set_actuator_status", "^C23,([0-1])$"), Cmd("get_movement_type", "^Q1$"), Cmd("set_movement_type", "^C1,([0-3])$"), Cmd("get_step_time", "^Q86,([1-3])$"), Cmd("set_step_time", "^C86,([1-3]),([0-9]*.[0-9]*)$"), # Channel commands Cmd("get_chan_waveform_type", "^Q2,([1-3])$"), Cmd("set_chan_waveform_type", "^C2,([1-3]),([0-5])$"), Cmd("get_ramp_amplitude_setpoint", "^Q4,([1-3])$"), Cmd("set_ramp_amplitude_setpoint", "^C4,([1-3]),([0-9]*.[0-9]*)$"), Cmd("get_single_point_feedback_data", "^Q134,([1-3]),([0-9]+)$"), Cmd("get_chan_scale", "^Q308,([1-3])$"), Cmd("get_strain_channel_length", "^Q340,([1-3])$"), Cmd("get_chan_area", "^Q341,([1-3])$"), Cmd("set_chan_area", "^C341,([1-3]),([0-9]*.[0-9]*)$"), Cmd("get_chan_type", "^Q307,([1-3])$"), # Waveform commands Cmd("get_waveform_status", "^Q200$"), Cmd("abort_waveform_generation", "^C200,0$"), Cmd("start_waveform_generation", "^C200,1$"), Cmd("request_stop_waveform_generation", "^C200,4$"), Cmd("get_waveform_type", "^Q201,([1-3])$"), Cmd("set_waveform_type", "^C201,([1-3]),([0-8])$"), Cmd("get_waveform_amplitude", "^Q203,([1-3])$"), Cmd("set_waveform_amplitude", "^C203,([1-3]),([0-9]*.[0-9]*)$"), Cmd("get_waveform_frequency", "^Q202,([1-3])$"), Cmd("set_waveform_frequency", "^C202,([1-3]),([0-9]*.[0-9]*)$"), Cmd("set_waveform_hold", "^C213,3$"), Cmd("set_waveform_maintain_log", "^C214,0$"), # Waveform (quarter counter event detector) commands Cmd("arm_quarter_counter", "^C212,2$"), Cmd("get_quarter_counts", "^Q210$"), Cmd("get_max_quarter_counts", "^Q209$"), Cmd("set_max_quarter_counts", "^C209,([0-9]+)$"), Cmd("set_quarter_counter_off", "^C212,0$"), Cmd("get_quarter_counter_status", "^Q212$"), } in_terminator = "\r\n" out_terminator = "\r\n" def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error)) return str(error) def get_control_channel(self): return self._device.get_control_channel() def set_control_channel(self, channel): self._device.set_control_channel(int(channel)) def disable_watchdog(self): self._device.disable_watchdog() def get_control_mode(self): return self._device.get_control_mode() def set_control_mode(self, mode): self._device.set_control_mode(int(mode)) def get_status(self): return int(self._device.get_status()) def arbitrary_command(self, command): return "Arb_com_response_" + str(command) def get_actuator_status(self): return self._device.get_actuator_status() def set_actuator_status(self, mode): self._device.set_actuator_status(int(mode)) def get_movement_type(self): return self._device.get_movement_type() def set_movement_type(self, mov_type): self._device.set_movement_type(int(mov_type)) def get_step_time(self, channel): return float(self._device.get_step_time(int(channel))) def set_step_time(self, channel, value): self._device.set_step_time(int(channel), float(value)) def get_chan_waveform_type(self, channel): return int(self._device.get_chan_waveform_type(int(channel))) def set_chan_waveform_type(self, channel, value): self._device.set_chan_waveform_type(int(channel), int(value)) def get_ramp_amplitude_setpoint(self, channel): return float(self._device.get_ramp_amplitude_setpoint(int(channel))) def set_ramp_amplitude_setpoint(self, channel, value): self._device.set_ramp_amplitude_setpoint(int(channel), float(value)) def get_single_point_feedback_data(self, channel, type): return float(self._device.get_chan_value(int(channel), int(type))) def get_chan_scale(self, channel): return self._device.get_chan_scale(int(channel)) def get_strain_channel_length(self, channel): return self._device.get_strain_channel_length(int(channel)) def get_chan_area(self, channel): return self._device.get_chan_area(int(channel)) def set_chan_area(self, channel, value): self._device.set_chan_area(int(channel), float(value)) def get_chan_type(self, channel): transducer_type = self._device.get_chan_transducer_type(int(channel)) chan_type = self._device.get_chan_type(int(channel)) return "{a},{b}".format(a=transducer_type, b=chan_type) # Waveform generation def get_waveform_status(self): return self._device.get_waveform_status() def start_waveform_generation(self): self._device.start_waveform_generation() def abort_waveform_generation(self): self._device.abort_waveform_generation() def request_stop_waveform_generation(self): self._device.finish_waveform_generation() def get_waveform_type(self, channel): return self._device.get_waveform_type(int(channel)) def set_waveform_type(self, channel, type): self._device.set_waveform_type(int(channel), int(type)) def get_waveform_amplitude(self, channel): return self._device.get_waveform_amplitude(int(channel)) def set_waveform_amplitude(self, channel, value): self._device.set_waveform_amplitude(int(channel), float(value)) def get_waveform_frequency(self, channel): return self._device.get_waveform_frequency(int(channel)) def set_waveform_frequency(self, channel, value): self._device.set_waveform_frequency(int(channel), float(value)) def set_waveform_hold(self): self._device.set_waveform_hold() # Waveform quarter counter def arm_quarter_counter(self): self._device.arm_quarter_counter() def get_quarter_counts(self): return self._device.get_quarter_counts() def get_max_quarter_counts(self): return self._device.get_max_quarter_counts() def set_max_quarter_counts(self, val): self._device.set_max_quarter_counts(int(val)) def set_quarter_counter_off(self): self._device.set_quarter_counter_off() def get_quarter_counter_status(self): return self._device.get_quarter_counter_status() def set_waveform_maintain_log(self): self._device.set_waveform_maintain_log()
class LinkamT95StreamInterface(StreamInterface): """ Linkam T95 TCP stream interface This is the interface of a simulated Linkam T95 device. The device listens on a configured host:port-combination, one option to connect to it is via telnet: $ telnet host port Once connected, it's possible to send the specified commands, described in the dynamically generated documentation. Information about host, port and line terminators in the concrete device instance are also generated dynamically. """ out_terminator = b"\r" commands = { Cmd("get_status", "^T$", return_mapping=lambda x: x), Cmd("set_rate", "^R1([0-9]+)$"), Cmd("set_limit", "^L1([0-9]+)$"), Cmd("start", "^S$"), Cmd("stop", "^E$"), Cmd("hold", "^O$"), Cmd("heat", "^H$"), Cmd("cool", "^C$"), Cmd("pump_command", "^P(a0|m0|[0123456789:;<=>?@ABCDEFGHIJKLMN]{1})$"), } def get_status(self): """ Models "T Command" functionality of device. Returns all available status information about the device as single byte array. :return: Byte array consisting of 10 status bytes. """ # "The first command sent must be a 'T' command" from T95 manual self.device.serial_command_mode = True Tarray = [0x80] * 10 # Status byte (SB1) Tarray[0] = { "stopped": 0x01, "heat": 0x10, "cool": 0x20, "hold": 0x30, }.get(self.device._csm.state, 0x01) if Tarray[0] == 0x30 and self.device.hold_commanded: Tarray[0] = 0x50 # Error status byte (EB1) if self.device.pump_overspeed: Tarray[1] |= 0x01 # TODO: Add support for other error conditions? # Pump status byte (PB1) Tarray[2] = 0x80 + self.device.pump_speed # Temperature Tarray[6:10] = [ ord(x) for x in "%04x" % (int(self.device.temperature * 10) & 0xFFFF) ] return bytes(Tarray) def set_rate(self, param): """ Models "Rate Command" functionality of device. Sets the target rate of temperature change. :param param: Rate of temperature change in C/min, multiplied by 100, as a string. Must be positive. :return: Empty string. """ # TODO: Is not having leading zeroes / 4 digits an error? rate = int(param) if 1 <= rate <= 15000: self.device.temperature_rate = rate / 100.0 return b"" def set_limit(self, param): """ Models "Limit Command" functionality of device. Sets the target temperate to be reached. :param param: Target temperature in C, multiplied by 10, as a string. Can be negative. :return: Empty string. """ # TODO: Is not having leading zeroes / 4 digits an error? limit = int(param) if -2000 <= limit <= 6000: self.device.temperature_limit = limit / 10.0 return b"" def start(self): """ Models "Start Command" functionality of device. Tells the T95 unit to start heating or cooling at the rate specified by setRate and to a limit set by setLimit. :return: Empty string. """ self.device.start_commanded = True return b"" def stop(self): """ Models "Stop Command" functionality of device. Tells the T95 unit to stop heating or cooling. :return: Empty string. """ self.device.stop_commanded = True return b"" def hold(self): """ Models "Hold Command" functionality of device. Device will hold current temperature until a heat or cool command is issued. :return: Empty string. """ self.device.hold_commanded = True return b"" def heat(self): """ Models "Heat Command" functionality of device. :return: Empty string. """ # TODO: Is this really all it does? self.device.hold_commanded = False return b"" def cool(self): """ Models "Cool Command" functionality of device. :return: Empty string. """ # TODO: Is this really all it does? self.device.hold_commanded = False return b"" def pump_command(self, param): """ Models "LNP Pump Commands" functionality of device. Switches between automatic or manual pump mode, and adjusts speed when in manual mode. :param param: 'a0' for auto, 'm0' for manual, [0-N] for speed. :return: """ lookup = b"0123456789:;<=>?@ABCDEFGHIJKLMN" if param == b"a0": self.device.pump_manual_mode = False elif param == b"m0": self.device.pump_manual_mode = True elif param in lookup: self.device.manual_target_speed = lookup.index(param) return b"" def handle_error(self, request, error): """ If command is not recognised print and error Args: request: requested string error: problem """ self.log.error( "An error occurred at request " + repr(request) + ": " + repr(error) )
class AG33220AStreamInterface(StreamInterface): commands = { Cmd("get_amplitude", "^VOLT\?$"), Cmd("set_amplitude", "^VOLT " + NUM_MIN_MAX, argument_mappings=[string_arg]), Cmd("get_frequency", "^FREQ\?$"), Cmd("set_frequency", "^FREQ " + NUM_MIN_MAX, argument_mappings=[string_arg]), Cmd("get_offset", "^VOLT:OFFS\?$"), Cmd("set_offset", "^VOLT:OFFS " + NUM_MIN_MAX, argument_mappings=[string_arg]), Cmd("get_units", "^VOLT:UNIT\?$"), Cmd("set_units", "^VOLT:UNIT (VPP|VRMS|DBM)$", argument_mappings=[string_arg]), Cmd("get_function", "^FUNC\?$"), Cmd("set_function", "^FUNC (SIN|SQU|RAMP|PULS|NOIS|DC|USER)$", argument_mappings=[string_arg]), Cmd("get_output", "^OUTP\?$"), Cmd("set_output", "^OUTP (ON|OFF)$", argument_mappings=[string_arg]), Cmd("get_idn", "^\*IDN\?$"), Cmd("get_voltage_high", "^VOLT:HIGH\?$"), Cmd("set_voltage_high", "^VOLT:HIGH " + NUM_MIN_MAX, argument_mappings=[string_arg]), Cmd("get_voltage_low", "^VOLT:LOW\?$"), Cmd("set_voltage_low", "^VOLT:LOW " + NUM_MIN_MAX, argument_mappings=[string_arg]), Cmd("get_voltage_range_auto", "^VOLT:RANG:AUTO\?$"), Cmd("set_voltage_range_auto", "^VOLT:RANG:AUTO (OFF|ON|ONCE)$", argument_mappings=[string_arg]), } in_terminator = "\n" out_terminator = "\n" # Takes in a value and returns a value in the form of x.xxx0000000000Eyy def float_output(self, value): value = float('%s' % float('%.4g' % float(value))) return "{:+.13E}".format(value) @if_connected def get_amplitude(self): return self.float_output(self._device.amplitude) @if_connected def set_amplitude(self, new_amplitude): self._device.set_new_amplitude(new_amplitude) @if_connected def get_frequency(self): return self.float_output(self._device.frequency) @if_connected def set_frequency(self, new_frequency): self._device.set_new_frequency(new_frequency) @if_connected def get_offset(self): return self.float_output(self._device.offset) @if_connected def set_offset(self, new_offset): self._device.set_offs_and_update_voltage(new_offset) @if_connected def get_units(self): return self._device.units @if_connected def set_units(self, new_units): self._device.units = new_units @if_connected def get_function(self): return self._device.function @if_connected def set_function(self, new_function): self._device.set_function(new_function) @if_connected def get_output(self): return self._device.get_output() @if_connected def set_output(self, new_output): self._device.output = new_output @if_connected def get_idn(self): return self._device.idn @if_connected def get_voltage_high(self): return self.float_output(self._device.voltage_high) @if_connected def set_voltage_high(self, new_voltage_high): self._device.set_new_voltage_high(new_voltage_high) @if_connected def get_voltage_low(self): return self.float_output(self._device.voltage_low) @if_connected def set_voltage_low(self, new_voltage_low): self._device.set_new_voltage_low(new_voltage_low) @if_connected def get_voltage_range_auto(self): return self._device.get_range_auto() @if_connected def set_voltage_range_auto(self, range_auto): self._device.range_auto = range_auto def handle_error(self, request, error): print(traceback.format_exc())
class POLARISSampleChangerStreamInterface(StreamInterface): protocol = "POLARIS" commands = { Cmd("get_id", "^id$"), Cmd("get_position", "^po$"), Cmd("get_status", "^st$"), Cmd("go_back", "^bk$"), Cmd("go_fwd", "^fw$"), Cmd("halt", "^ht$"), Cmd("initialise", "^in$"), Cmd("lower_arm", "^lo$"), Cmd("move_to", "^ma(0[1-9]|[1][0-9]|20)$", argument_mappings=[int]), Cmd("move_to_without_lowering", "^mn(0[1-9]|[1][0-9]|20)$", argument_mappings=[int]), Cmd("raise_arm", "^ra$"), Cmd("retrieve_sample", "^rt$") } error_codes = { Errors.NO_ERR: 0, Errors.ERR_INV_DEST: 5, Errors.ERR_NOT_INITIALISED: 6, Errors.ERR_ARM_DROPPED: 7, Errors.ERR_ARM_UP: 8, Errors.ERR_CANT_ROT_IF_NOT_UP: 10 } in_terminator = "\r\n" out_terminator = "\r\n" def get_id(self): return "0001 0001 ISIS Polaris Sample Changer V" def get_position(self): return "Position = {:2d}".format(int(self._device.car_pos)) def get_status(self): lowered = self._device.get_arm_lowered() # Based on testing with actual device, appears to be different than doc return_string = "{0:b}01{1:b}{2:b}{3:b}0{4:b}" return_string = return_string.format(not lowered, self._device.is_car_at_one(), not lowered, lowered, self._device.is_moving()) return_string += "{:1d}".format(int(self._device.current_err)) return_string += " {:2d}".format(int(self._device.car_pos)) return return_string def go_back(self): self._device.go_backward() def go_fwd(self): self._device.go_forward() def halt(self): return "" def initialise(self): self._device.init() def move_to(self, position): self._device.move_to(position, True) def move_to_without_lowering(self, position): self._device.move_to(position, False) def lower_arm(self): self._device.set_arm(True) def raise_arm(self): self._device.set_arm(False) def retrieve_sample(self): self._device.sample_retrieved = True def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error)) return "??"
class FinsPLCStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation. Match anything! commands = { Cmd("any_command", r"^([\s\S]*)$", return_mapping=lambda x: x), } in_terminator = "" out_terminator = b"" do_log = True def handle_error(self, request, error): error_message = "An error occurred at request " + repr(request) + ": " + repr(error) self.log.error(error_message) print(error_message) return str(error) def any_command(self, command): """ Handles all command sent to this emulator. It checks the validity of the command, and raises an error if it finds something invalid. If the command is valid, then it returns a string representing the response to the command. Args: command (bytes): A string where every character represents a byte from the received FINS command frame. Returns: bytes: a string where each character represents a byte from the FINS response frame. """ self._log_fins_frame(command, False) self._check_fins_frame_header_validity(command[:10]) # We extract information necessary for building the FINS response header self.device.network_address = command[3] self.device.unit_address = command[5] client_network_address = command[6] client_node_address = command[7] client_unit_address = command[8] service_id = command[9] if command[10] != 0x01 or command[11] != 0x01: raise ValueError("The command code should be 0x0101, for memory area read command!") if command[12] != 0x82: raise ValueError("The emulator only supports reading words from the DM area, for which the code is 82.") # the address of the starting word from where reading is done. Addresses are stored in two bytes. memory_start_address = raw_bytes_to_int(command[13:15], low_bytes_first=False) # The FINS PLC supports reading either a certain number of words, or can also read individual bits in a word. # The helium recovery memory map implies that that PLC uses word designated reading. When bit designated # reading is not used, the 16th byte of the command is 0x00. if command[15] != 0x00: raise ValueError("The emulator only supports word designated memory reading. The bit address must " "be 0x00.") number_of_words_to_read = raw_bytes_to_int(command[16:18], low_bytes_first=False) # The helium recovery PLC memory map has addresses that store types that take up either one word (16 bits) or # two. Most take up one word, so if the number of words to read is two we check that the client wants to read # from a memory location from where a 32 bit value starts. if number_of_words_to_read == 2 and memory_start_address not in self.device.int32_memory.keys(): raise ValueError("The memory start address {} corresponds to a single word in the memory map, " "not two.".format(memory_start_address)) # The PLC also stores 32 bit floating point numbers, but the asyn device support makes the IOC ask for 4 bytes # instead of two. elif number_of_words_to_read == 4 and memory_start_address not in self.device.float_memory.keys(): raise ValueError("The only commands that should ask for 4 words are for reading memory locations that " "store real numbers, which {} is not.".format(memory_start_address)) elif number_of_words_to_read > 4 or number_of_words_to_read == 3: raise ValueError("The memory map only specifies data types for which commands should ask for one, two or " "four words at most .") self._log_command_contents(client_network_address, client_node_address, client_unit_address, service_id, memory_start_address, number_of_words_to_read) reply = dm_memory_area_read_response_fins_frame(self.device, client_network_address, client_node_address, client_unit_address, service_id, memory_start_address, number_of_words_to_read) self._log_fins_frame(reply, True) return reply def _log_fins_frame(self, fins_frame, is_reply): """ Nicely displays every byte in the command as a hexadecimal number in the emulator log. Args: fins_frame (bytes): The fins frame we want to log. is_reply (bool): Whether we want to log the reply or the command. Returns: None. """ if self.do_log: hex_command = [hex(character) for character in fins_frame] if not is_reply: self.log.info("command is {}".format(hex_command)) else: self.log.info("reply is{}".format(hex_command)) def _log_command_contents(self, client_network_address, client_node_address, client_unit_address, service_id, memory_start_address, number_of_words_to_read): """ Nicely displays the bits of information in the FINS command that will be used for building the reply as numbers. Args: client_network_address (int): The FINS network address of the client. client_node_address (int): The FINS node of the client. client_unit_address (int): The FINS unit address of the client. service_id (int): The service ID of the original command. memory_start_address (int): The memory address from where reading starts. number_of_words_to_read (int): The number of words to be read, starting from the start address, inclusive. Returns: None. """ if self.do_log: self.log.info("Server network address: {}".format(self.device.network_address)) self.log.info("Server Unit address: {}".format(self.device.unit_address)) self.log.info("Client network address: {}".format(client_network_address)) self.log.info("Client node address: {}".format(client_node_address)) self.log.info("Client unit address: {}".format(client_unit_address)) self.log.info("Service id: {}".format(service_id)) self.log.info("Memory start address: {}".format(memory_start_address)) self.log.info("Number of words to read: {}".format(number_of_words_to_read)) @staticmethod def _check_fins_frame_header_validity(fins_frame_header): """ Checks that the FINS frame header part of the command is valid for a command sent from a client to a server (PLC). Args: fins_frame_header (bytes): A string where every character represents a byte from the received FINS frame header. Returns: None. """ # ICF means Information Control Field, it gives information about if the frame is for a command or a response, # and if a response is needed or not. icf = fins_frame_header[0] if icf != 0x80: raise ValueError("ICF value should always be 0x80 for a command sent to the emulator") # Reserved byte. Should always be 0x00 if fins_frame_header[1] != 0x00: raise ValueError("Reserved byte should always be 0x00.") if fins_frame_header[2] != 0x02: raise ValueError("Gate count should always be 0x02.") check_is_byte(fins_frame_header[3]) if fins_frame_header[4] != SimulatedFinsPLC.HELIUM_RECOVERY_NODE: raise ValueError("The node address of the FINS helium recovery PLC should be {}!".format( SimulatedFinsPLC.HELIUM_RECOVERY_NODE)) for i in range(5, 10): check_is_byte(fins_frame_header[i])
class CybamanStreamInterface(StreamInterface): """ Stream interface for the serial port """ FLOAT = "([-+]?[0-9]*\.?[0-9]*)" commands = { Cmd("initialize", "^A$"), Cmd("get_a", "^M101$"), Cmd("get_b", "^M201$"), Cmd("get_c", "^M301$"), Cmd( "set_all", "^OPEN PROG 10 CLEAR\nG1 A " + FLOAT + " B " + FLOAT + " C " + FLOAT + " TM([0-9]*)$"), Cmd("ignore", "^CLOSE$"), Cmd("ignore", "^B10R$"), Cmd("reset", "^\$\$\$$"), Cmd("home_a", "^B9001R$"), Cmd("home_b", "^B9002R$"), Cmd("home_c", "^B9003R$"), Cmd("stop", "^{}$".format(chr(0x01))), } in_terminator = "\r" # ACK character out_terminator = chr(0x06) @has_log def handle_error(self, request, error): """ If command is not recognised print and error. :param request: requested string :param error: problem :return: """ error = "An error occurred at request " + repr(request) + ": " + repr( error) print(error) self.log.debug(error) return error @if_connected def ignore(self): return "" @if_connected def initialize(self): self._device.initialized = True return "" @if_connected def stop(self): self._device.initialized = False @if_connected def reset(self): self._device._initialize_data() return "" @if_connected def get_a(self): return "{}\r".format(self._device.a * 3577) @if_connected def get_b(self): return "{}\r".format(self._device.b * 3663) @if_connected def get_c(self): return "{}\r".format(self._device.c * 3663) @if_connected def set_all(self, a, b, c, tm): self._verify_tm(a, b, c, tm) self._device.a_setpoint = float(a) self._device.b_setpoint = float(b) self._device.c_setpoint = float(c) return "" def _verify_tm(self, a, b, c, tm): tm = int(tm) old_position = (self._device.a, self._device.b, self._device.c) new_position = (float(a), float(b), float(c)) max_difference = max( [abs(a - b) for a, b in zip(old_position, new_position)]) expected_tm = max([int(round(max_difference / 5.0)) * 1000, 4000]) # Allow a difference of 1000 for rounding errors / differences between labview and epics # (error would get multiplied by 1000) if abs(tm - expected_tm) > 1000: assert False, "Wrong TM value! Expected {} but got {}".format( expected_tm, tm) def home_a(self): self._device.home_axis_a() return "" def home_b(self): self._device.home_axis_b() return "" def home_c(self): self._device.home_axis_c() return ""
class HRPDSampleChangerStreamInterface(StreamInterface): protocol = "HRPD" commands = { Cmd("get_id", "^id$"), Cmd("get_position", "^po$"), Cmd("get_status", "^st$"), Cmd("go_back", "^bk$"), Cmd("go_fwd", "^fw$"), Cmd("halt", "^ht$"), Cmd("initialise", "^in$"), Cmd("lower_arm", "^lo$"), Cmd("move_to", "^ma([0-9]{2})$", argument_mappings=[int]), Cmd("move_to_without_lowering", "^mn([0-9]{2})$", argument_mappings=[int]), Cmd("raise_arm", "^ra$"), Cmd("read_variable", "^vr([0-9]{4})$", argument_mappings=[int]), Cmd("retrieve_sample", "^rt$"), } error_codes = { Errors.NO_ERR: 0, Errors.ERR_INV_DEST: 5, Errors.ERR_NOT_INITIALISED: 6, Errors.ERR_ARM_DROPPED: 7, Errors.ERR_ARM_UP: 8, Errors.ERR_CANT_ROT_IF_NOT_UP: 7 } in_terminator = "\r" out_terminator = "\r\n" def _check_error_code(self, code): if code == Errors.NO_ERR: return "ok" else: self._device.current_err = code return "rf-%02d" % code def get_id(self): return "0001 0001 ISIS HRPD Sample Changer V1.00" def get_position(self): return "Position = {:2d}".format(int(self._device.car_pos)) def get_status(self): lowered = self._device.get_arm_lowered() # Based on testing with actual device, appears to be different than doc return_string = "01000{0:b}01{1:b}{2:b}{3:b}00000" return_string = return_string.format(not lowered, self._device.is_car_at_one(), not lowered, lowered) return_string += " 0 {:b}".format(self._device.is_moving()) return_error = int(self.error_codes[self._device.current_err]) return_string += " {:2d}".format(return_error) return_string += " {:2d}".format(int(self._device.car_pos)) return return_string def go_back(self): return self._check_error_code(self._device.go_backward()) def go_fwd(self): return self._check_error_code(self._device.go_forward()) def read_variable(self, variable): return "- VR " + str(variable) + " = 17 hx 11" def halt(self): return "ok" def initialise(self): return self._check_error_code(self._device.init()) def move_to(self, position): return self._check_error_code(self._device.move_to(position, True)) def move_to_without_lowering(self, position): return self._check_error_code(self._device.move_to(position, False)) def lower_arm(self): return self._check_error_code(self._device.set_arm(True)) def raise_arm(self): return self._check_error_code(self._device.set_arm(False)) def retrieve_sample(self): self._device.sample_retrieved = True return "ok" def handle_error(self, request, error): print("An error occurred at request " + repr(request) + ": " + repr(error))
class JulaboStreamInterfaceV2(StreamInterface): """Julabos can have different commands sets depending on the version number of the hardware. This protocol matches that for: FP50-HE (unconfirmed). """ protocol = "julabo-version-2" commands = { Var("temperature", read_pattern="^IN_PV_00$", doc="The bath temperature."), Var( "external_temperature", read_pattern="^IN_PV_01$", doc="The external temperature.", ), Var("heating_power", read_pattern="^IN_PV_02$", doc="The heating power."), Var( "set_point_temperature", read_pattern="^IN_SP_00$", doc="The temperature setpoint.", ), Cmd( "set_set_point", r"^OUT_SP_00 ([0-9]*\.?[0-9]+)$", argument_mappings=(float,), ), # Read pattern for high limit is different from version 1 Var( "temperature_high_limit", read_pattern="^IN_SP_03$", doc="The high limit - usually set in the hardware.", ), # Read pattern for low limit is different from version 1 Var( "temperature_low_limit", read_pattern="^IN_SP_04$", doc="The low limit - usually set in the hardware.", ), Var("version", read_pattern="^VERSION$", doc="The Julabo version."), Var("status", read_pattern="^STATUS$", doc="The Julabo status."), Var( "is_circulating", read_pattern="^IN_MODE_05$", doc="Whether it is circulating.", ), Cmd("set_circulating", "^OUT_MODE_05 (0|1)$", argument_mappings=(int,)), Var("internal_p", read_pattern="^IN_PAR_06$", doc="The internal proportional."), Cmd( "set_internal_p", r"^OUT_PAR_06 ([0-9]*\.?[0-9]+)$", argument_mappings=(float,), ), Var("internal_i", read_pattern="^IN_PAR_07$", doc="The internal integral."), Cmd("set_internal_i", "^OUT_PAR_07 ([0-9]*)$", argument_mappings=(int,)), Var("internal_d", read_pattern="^IN_PAR_08$", doc="The internal derivative."), Cmd("set_internal_d", "^OUT_PAR_08 ([0-9]*)$", argument_mappings=(int,)), Var("external_p", read_pattern="^IN_PAR_09$", doc="The external proportional."), Cmd( "set_external_p", r"^OUT_PAR_09 ([0-9]*\.?[0-9]+)$", argument_mappings=(float,), ), Var("external_i", read_pattern="^IN_PAR_11$", doc="The external integral."), Cmd("set_external_i", "^OUT_PAR_11 ([0-9]*)$", argument_mappings=(int,)), Var("external_d", read_pattern="^IN_PAR_12$", doc="The external derivative."), Cmd("set_external_d", "^OUT_PAR_12 ([0-9]*)$", argument_mappings=(int,)), } in_terminator = "\r" out_terminator = "\n" # Different from version 1
class VolumetricRigStreamInterface(StreamInterface): # The rig typically splits a command by whitespace and then uses the arguments it needs and then ignores the rest # so "IDN" will respond as "IDN BLAH BLAH BLAH" and "BCS 01" would be the same as "BCS 01 02 03". # Some commands that take input will respond with default (often invalid) parameters if not present. For example # "BCS" is the same as "BCS 00" and also "BCS AA". serial_commands = { Cmd("purge", "^(.*)\!$"), Cmd("get_identity", "^IDN(?:\s.*)?$"), Cmd("get_identity", "^\?(?:\s.*)?$"), Cmd("get_buffer_control_and_status", "^BCS(?:\s(\S*))?.*$"), Cmd("get_ethernet_and_hmi_status", "^ETN(?:\s.*)?$"), Cmd("get_gas_control_and_status", "^GCS(?:\s.*)?$"), Cmd("get_gas_mix_matrix", "^GMM(?:\s.*)?$"), Cmd("gas_mix_check", "^GMC(?:\s(\S*))?(?:\s(\S*))?.*$"), Cmd("get_gas_number_available", "^GNA(?:\s.*)?$"), Cmd("get_hmi_status", "^HMI(?:\s.*)?$"), Cmd("get_hmi_count_cycles", "^HMC(?:\s.*)?$"), Cmd("get_memory_location", "^RDM(?:\s(\S*))?.*"), Cmd("get_pressure_and_temperature_status", "^PTS(?:\s.*)?$"), Cmd("get_pressures", "^PMV(?:\s.*)?$"), Cmd("get_temperatures", "^TMV(?:\s.*)?$"), Cmd("get_ports_and_relays_hex", "^PTR(?:\s.*)?$"), Cmd("get_ports_output", "^POT(?:\s.*)?$"), Cmd("get_ports_input", "^PIN(?:\s.*)?$"), Cmd("get_ports_relays", "^PRY(?:\s.*)?$"), Cmd("get_system_status", "^STS(?:\s.*)?$"), Cmd("get_com_activity", "^COM(?:\s.*)?$"), Cmd("get_valve_status", "^VST(?:\s.*)?$"), Cmd("open_valve", "^OPV(?:\s(\S*))?.*$"), Cmd("close_valve", "^CLV(?:\s(\S*))?.*$"), Cmd("halt", "^HLT(?:\s.*)?$"), } # These commands are intended solely as a control mechanism for the emulator. As an alternative, the Lewis # backdoor can be used to modify the device state. control_commands = { Cmd("set_buffer_system_gas", "^_SBG(?:\s(\S*))?(?:\s(\S*))?.*$"), Cmd("set_pressure_cycling", "^_PCY(?:\s(\S*)).*$"), Cmd("set_pressures", "^_SPR(?:\s(\S*)).*$"), Cmd("set_pressure_target", "^_SPT(?:\s(\S*)).*$"), Cmd("enable_valve", "^_ENV(?:\s(\S*)).*$"), Cmd("disable_valve", "^_DIV(?:\s(\S*)).*$"), } commands = set.union(serial_commands, control_commands) # You may need to change these to \r\n if using Telnet" in_terminator = "\r" out_terminator = "\r" # Lots of formatted output for the volumetric rig is based on fixed length strings output_length = 20 def purge(self, chars): """ Responds any current input to the screen without executing it. :param chars: Whatever characters are left over in the buffer :return: Purge message including ignored input """ return " ".join([ "PRG,00,Purge", format_int(len(chars) + 1, True, 5), "Characters", chars + "!" ]) def get_identity(self): """ Responds with the devices identity. :return: Device identity """ return "IDN,00," + self._device.identify() def _build_buffer_control_and_status_string(self, buffer_number): """ Get information about a specific buffer, its valve state and the gases connected to it. :param buffer_number : The index of the buffer :return: Information about the requested buffer """ buff = self._device.buffer(buffer_number) assert buff is not None return " ".join([ "", buff.index(as_string=True), buff.buffer_gas().index(as_string=True), buff.buffer_gas().name(VolumetricRigStreamInterface.output_length, " "), "E" if buff.valve_is_enabled() else "d", "O" if buff.valve_is_open() else "c", buff.system_gas().index(as_string=True), buff.system_gas().name() ]) def get_buffer_control_and_status(self, buffer_number_raw): """ Get information about a specific buffer, its valve state and the gases connected to it. :param buffer_number_raw : The buffer "number" entered by a user. Although a number is expected, the command will accept other types of input :return: Information about the requested buffer """ buffer_number = convert_raw_to_int(buffer_number_raw) message_prefix = "BCS" num_length = 3 error_message_prefix = " ".join([ message_prefix, "Buffer", str(buffer_number)[:num_length].zfill(num_length) ]) buffer_too_low = " ".join([error_message_prefix, "Too Low"]) buffer_too_high = " ".join([error_message_prefix, "Too High"]) if buffer_number <= 0: return buffer_too_low elif buffer_number > len(self._device.buffers()): return buffer_too_high else: return "BCS " + self._build_buffer_control_and_status_string( buffer_number) def get_ethernet_and_hmi_status(self): """ Get information about the rig's hmi and plc ethernet devices. :return: Information about the ethernet devices status. The syntax of the return string is odd: the separators are not consistent """ return " ".join([ "ETN:PLC", self._device.plc().ip() + ",HMI", self._device.hmi().status(), "," + self._device.hmi().ip() ]) def get_gas_control_and_status(self): """ Get a list of information about all the buffers, their associated gases and valve statuses. :return: Buffer information. One line per buffer with a header """ return "\r\n".join(["No No Buffer E O No System"] + [ self._build_buffer_control_and_status_string(b.index()) for b in self._device.buffers() ] + ["GCS"]) def get_gas_mix_matrix(self): """ Get information about which gases can be mixed together. :return: A 2D matrix representation of the ability to mix different gases with column and row titles """ system_gases = self._device.system_gases().gases() column_headers = [ gas.name(VolumetricRigStreamInterface.output_length, '|') for gas in system_gases ] row_titles = [ " ".join([ gas.index(as_string=True), gas.name(VolumetricRigStreamInterface.output_length, ' ') ]) for gas in system_gases ] mixable_chars = [[ "<" if self._device.mixer().can_mix(g1, g2) else "." for g1 in system_gases ] for g2 in system_gases] # Put data in output format lines = list() # Add column headers for i in range(len(max(column_headers, key=len))): words = list() # For the top-left block of white space words.append((len(max(row_titles, key=len)) - 1) * " ") # Vertically aligned gas names for j in range(len(column_headers)): words.append(column_headers[j][i]) lines.append(' '.join(words)) # Add rows assert len(row_titles) == len(mixable_chars) for i in range(len(row_titles)): words = list() words.append(row_titles[i]) words.append(' '.join(mixable_chars[i])) lines.append(''.join(words)) # Add footer lines.append("GMM allowance limit: " + str(self._device.system_gases().gas_count())) return '\r\n'.join(lines) def gas_mix_check(self, gas1_index_raw, gas2_index_raw): """ Query whether two gases can be mixed. :param gas1_index_raw : The index of the first gas. Although a number is expected, the command will accept other types of input :param gas2_index_raw : As above for the 2nd gas :return: An echo of the name and index of the requested gases as well as an ok/NO indicating whether the gases can be mixed """ gas1 = self._device.system_gases().gas_by_index( convert_raw_to_int(gas1_index_raw)) gas2 = self._device.system_gases().gas_by_index( convert_raw_to_int(gas2_index_raw)) if gas1 is None: gas1 = self._device.system_gases().gas_by_index(0) if gas2 is None: gas2 = self._device.system_gases().gas_by_index(0) return ' '.join([ "GMC", gas1.index(as_string=True), gas1.name(VolumetricRigStreamInterface.output_length, '.'), gas2.index(as_string=True), gas2.name(VolumetricRigStreamInterface.output_length, '.'), "ok" if self._device.mixer().can_mix(gas1, gas2) else "NO" ]) def get_gas_number_available(self): """ Get the number of available gases. :return: The number of available gases """ return self._device.system_gases().gas_count() def get_hmi_status(self): """ Get the current status of the HMI. :return: Information about the HMI """ hmi = self._device.hmi() return ",".join([ "HMI " + hmi.status() + " ", hmi.ip(), "B", hmi.base_page(as_string=True, length=3), "S", hmi.sub_page(as_string=True, length=3), "C", hmi.count(as_string=True, length=4), "L", hmi.limit(as_string=True, length=4), "M", hmi.max_grabbed(as_string=True, length=4) ]) def get_hmi_count_cycles(self): """ Get information about how frequently the HMI is disconnected. :return: A list of integers indicating the number of occurrences of a disconnected count cycle of a specific length """ return " ".join(["HMC"] + self._device.hmi().count_cycles()) def get_memory_location(self, location_raw): """ Get the value stored in a particular location in memory. :param location_raw : The memory location to read. Although a number is expected, the command will accept other types of input :return: The memory location and the value stored there """ location = convert_raw_to_int(location_raw) return " ".join([ "RDM", format_int(location, as_string=True, length=4), self._device.memory_location(location, as_string=True, length=6) ]) def get_pressure_and_temperature_status(self): """ Get the status of the temperature and pressure sensors. :return: A letter for each sensor indicating its status. Refer to the spec for the meaning and sensor order """ status_codes = { SensorStatus.DISABLED: "D", SensorStatus.NO_REPLY: "X", SensorStatus.VALUE_IN_RANGE: "O", SensorStatus.VALUE_TOO_LOW: "L", SensorStatus.VALUE_TOO_HIGH: "H", SensorStatus.UNKNOWN: "?" } return "PTS " + \ "".join([status_codes[s.status()] for s in self._device.pressure_sensors(reverse=True)+self._device.temperature_sensors(reverse=True)]) def get_pressures(self): """ Get the current pressure sensor readings, and target pressure. :return: The pressure readings from each of the pressure sensors and the target pressure which, if exceeded, will cause all buffer valves to close and disable """ return " ".join(["PMV"] + [ p.value(as_string=True) for p in self._device.pressure_sensors(reverse=True) ] + ["T", self._device.target_pressure(as_string=True)]) def get_temperatures(self): """ Get the current temperature reading. :return: The current temperature for each of the temperature sensors """ return " ".join(["TMV"] + [ t.value(as_string=True) for t in self._device.temperature_sensors(reverse=True) ]) def get_valve_status(self): """ Get the status of the buffer and system valves. :return: The status of each of the system valves represented by a letter. Refer to the specification for the exact meaning and order """ status_codes = { ValveStatus.OPEN_AND_ENABLED: "O", ValveStatus.CLOSED_AND_ENABLED: "E", ValveStatus.CLOSED_AND_DISABLED: "D", ValveStatus.OPEN_AND_DISABLED: "!" } return "VST Valve Status " + "".join( [status_codes[v] for v in self._device.valves_status()]) @staticmethod def _convert_raw_valve_to_int(raw): """ Get the valve number from its identifier. :param raw: The raw valve identifier :return: An integer indicating the valve number """ if str(raw).lower() == "c": n = 7 elif str(raw).lower() == "v": n = 8 else: n = convert_raw_to_int(raw) return n def _set_valve_status(self, valve_identifier_raw, set_to_open=None, set_to_enabled=None): """ Change the valve status. :param valve_identifier_raw: A raw value that identifies the valve :param set_to_open: Whether to set the valve to open(True)/closed(False)/do noting(None) :param set_to_enabled: Whether to set the valve to enabled(True)/disabled(False)/do noting(None) :return: Indicates the valve number, previous state, and new state """ valve_number = VolumetricRigStreamInterface._convert_raw_valve_to_int( valve_identifier_raw) # We should have exactly one of these arguments if set_to_open is not None: command = "OPV" if set_to_open else "CLV" elif set_to_enabled is not None: command = "_ENV" if set_to_enabled else "_DIV" else: assert False # The command and valve number are always included message_prefix = " ".join([command, "Value", str(valve_number)]) # Select an action based on the input parameters. args = list() enabled = lambda *args: None if self._device.halted(): return command + " Rejected only allowed when running" elif valve_number <= 0: return message_prefix + " Too Low" elif valve_number <= self._device.buffer_count(): if set_to_open is not None: action = self._device.open_buffer_valve if set_to_open else self._device.close_buffer_valve enabled = self._device.buffer_valve_is_enabled current_state = self._device.buffer_valve_is_open else: action = self._device.enable_buffer_valve if set_to_enabled else self._device.disable_buffer_valve current_state = self._device.buffer_valve_is_enabled args.append(valve_number) elif valve_number == self._device.buffer_count() + 1: if set_to_open is not None: action = self._device.open_cell_valve if set_to_open else self._device.close_cell_valve enabled = self._device.cell_valve_is_enabled current_state = self._device.cell_valve_is_open else: action = self._device.enable_cell_valve if set_to_enabled else self._device.disable_cell_valve current_state = self._device.cell_valve_is_enabled elif valve_number == self._device.buffer_count() + 2: if set_to_open is not None: action = self._device.open_vacuum_valve if set_to_open else self._device.close_vacuum_valve enabled = self._device.vacuum_valve_is_enabled current_state = self._device.vacuum_valve_is_open else: action = self._device.enable_vacuum_valve if set_to_enabled else self._device.disable_vacuum_valve current_state = self._device.vacuum_valve_is_enabled else: return message_prefix + " Too High" if set_to_open is not None: if not enabled(*args): return " ".join([ command, "Rejected not enabled", format_int(valve_number, True, 1) ]) status_codes = {True: "open", False: "closed"} else: status_codes = {True: "enabled", False: "disabled"} # Execute the action and get the status before and after original_status = current_state(*args) action(*args) new_status = current_state(*args) return " ".join([ command, "Valve Buffer", str(valve_number), status_codes[new_status], "was", status_codes[original_status], ]) def close_valve(self, valve_number_raw): """ Close a valve. :param valve_number_raw: The number of the valve to close. The first n valves correspond to the buffers where n is the number of buffers. The n+1th valve is the cell valve, the n+2nd valve is for the vacuum. The supply valve cannot be controlled via serial. Although a number is expected, the command will accept other types of input :return: Indicates the valve number, previous state, and new state """ return self._set_valve_status(valve_number_raw, set_to_open=False) def open_valve(self, valve_number_raw): """ Open a valve. :param valve_number_raw : The number of the valve to close. The first n valves correspond to the buffers where n is the number of buffers. The n+1th valve is the cell valve, the n+2nd valve is for the vacuum. The supply valve cannot be controlled via serial. Although a number is expected, the command will accept other types of input :return: Indicates the valve number, previous state, and new state """ return self._set_valve_status(valve_number_raw, set_to_open=True) def enable_valve(self, valve_number_raw): """ Enable a valve. :param valve_number_raw: The number of the valve to close. The first n valves correspond to the buffers where n is the number of buffers. The n+1th valve is the cell valve, the n+2nd valve is for the vacuum. The supply valve cannot be controlled via serial. Although a number is expected, the command will accept other types of input :return: Indicates the valve number, previous state, and new state """ return self._set_valve_status(convert_raw_to_int(valve_number_raw), set_to_enabled=True) def disable_valve(self, valve_number_raw): """ Disable a valve. :param valve_number_raw: The number of the valve to close. The first n valves correspond to the buffers where n is the number of buffers. The n+1th valve is the cell valve, the n+2nd valve is for the vacuum. The supply valve cannot be controlled via serial. Although a number is expected, the command will accept other types of input :return: Indicates the valve number, previous state, and new state """ return self._set_valve_status(convert_raw_to_int(valve_number_raw), set_to_enabled=False) def halt(self): """ Halts the device. No further valve commands will be accepted. :return: Indicates that the system has been, or was already halted """ if self._device.halted(): message = "SYSTEM ALREADY HALTED" else: self._device.halt() assert self._device.halted() message = "SYSTEM NOW HALTED" return "HLT *** " + message + " ***" def get_system_status(self): """ Get information about the current system state. :return: Information about the system. Capitalisation of a particular word indicates an error has occurred in that subsystem. Refer to the specification for the meaning of system codes """ return " ".join([ "STS", self._device.status_code(as_string=True, length=2), "STOP" if self._device.errors().run else "run", "HMI" if self._device.errors().hmi else "hmi", # Spelling error duplicated as on device "GUAGES" if self._device.errors().gauges else "guages", "COMMS" if self._device.errors().comms else "comms", "HLT" if self._device.halted() else "halted", "E-STOP" if self._device.errors().estop else "estop" ]) def get_ports_and_relays_hex(self): """ :return: Information about the ports and relays """ return "PTR I:00 0000 0000 R:0000 0200 0000 O:00 0000 4400" def get_ports_output(self): """ :return: Information about the port output """ return "POT qwertyus vsbbbbbbzyxwvuts aBhecSssvsbbbbbb" def get_ports_input(self): """ :return: Information about the port input """ return "PIN qwertyui zyxwvutsrqponmlk abcdefghijklmneb" def get_ports_relays(self): """ :return: Information about the port relays. """ return "PRY qwertyuiopasdfgh zyxwhmLsrqponmlk abcdefghihlbhace" def get_com_activity(self): """ :return: Information about activity over the COM port """ return "COM ok 0113/0000" def set_buffer_system_gas(self, buffer_index_raw, gas_index_raw): """ Changes the system gas associated with a particular buffer. :param buffer_index_raw: The index of the buffer to update :param gas_index_raw: The index of the gas to update :return: Indicates the buffer changed, the previous system gas and the new system gas """ gas = self._device.system_gases().gas_by_index( convert_raw_to_int(gas_index_raw)) buff = self._device.buffer(convert_raw_to_int(buffer_index_raw)) if gas is not None and buff is not None: original_gas = buff.system_gas() buff.set_system_gas(gas) new_gas = buff.system_gas() return " ".join([ "SBG Buffer", buff.index(as_string=True), "system gas was", original_gas.name(), "now", new_gas.name() ]) else: return "SBG Lookup failed" def set_pressure_cycling(self, on_int_raw): """ Starts a sequence of pressure cycling. The pressure is increased until the target is met. This disables all buffer valves. The system pressure is decreased and the valves are renabled and reopened when the pressure falls below set limits. When the pressure reaches a minimum, the cycle is restarted. This allows simulation of various valve conditions. :param on_int_raw: Whether to switch cycling on(1)/off(other) :return: Indicates whether cycling is enabled """ cycle = convert_raw_to_bool(on_int_raw) self._device.cycle_pressures(cycle) return "_PCY " + str(cycle) def set_pressures(self, value_raw): """ Set the reading for all pressure sensors to a fixed value. :param value_raw: The value to apply to the pressure sensors :return: Echo the new pressure """ value = convert_raw_to_float(value_raw) self._device.set_pressures(value) return "SPR Pressures set to " + str(value) def set_pressure_target(self, value_raw): """ Set the target (limit) pressure for the system. :param value_raw: The new pressure target :return: Echo the new target """ value = convert_raw_to_float(value_raw) self._device.set_pressure_target(value) return "SPT Pressure target set to " + str(value) def handle_error(self, request, error): """ Handle errors during execution. May be an unrecognised command or emulator failure. """ if str(error) == "None of the device's commands matched.": return "URC,04,Unrecognised Command," + str(request) else: print("An error occurred at request " + repr(request) + ": " + repr(error))
class SkfMb350ChopperStreamInterface(StreamInterface): # Commands that we expect via serial during normal operation. Match anything! commands = { Cmd("any_command", "^([\s\S]*)$"), } in_terminator = "\r\n" out_terminator = in_terminator def handle_error(self, request, error): error_message = "An error occurred at request " + repr(request) + ": " + repr(error) print(error_message) self.log.error(error_message) return str(error) def any_command(self, command): command_mapping = { 0x20: self.start, 0x30: self.stop, 0x60: self.set_rotational_speed, 0x81: self.get_rotator_angle, 0x82: self.set_rotator_angle, 0x85: self.get_phase_delay, 0x8E: self.set_gate_width, 0x90: self.set_nominal_phase, 0xC0: self.get_phase_info, } address = ord(command[0]) if not 0 <= address < 16: raise ValueError("Address should be in range 0-15") # Constant function code. Should always be 0x80 if ord(command[1]) != 0x80: raise ValueError("Function code should always be 0x80") command_number = ord(command[2]) if command_number not in command_mapping.keys(): raise ValueError("Command number should be in map") command_data = [c for c in command[3:-2]] if not crc16_matches(command[:-2], command[-2:]): raise ValueError("CRC Checksum didn't match. Expected {} but got {}" .format(crc16(command[:-2]), command[-2:])) return command_mapping[command_number](address, command_data) def start(self, address, data): self._device.start() return general_status_response_packet(address, self.device, 0x20) def stop(self, address, data): self._device.stop() return general_status_response_packet(address, self.device, 0x30) def set_nominal_phase(self, address, data): self.log.info("Setting phase") self.log.info("Data = {}".format(data)) nominal_phase = raw_bytes_to_int(data) / 1000. self.log.info("Setting nominal phase to {}".format(nominal_phase)) self._device.set_nominal_phase(nominal_phase) return general_status_response_packet(address, self.device, 0x90) def set_gate_width(self, address, data): self.log.info("Setting gate width") self.log.info("Data = {}".format(data)) width = raw_bytes_to_int(data) self.log.info("Setting gate width to {}".format(width)) self._device.set_phase_repeatability(width / 10.) return general_status_response_packet(address, self.device, 0x8E) def set_rotational_speed(self, address, data): self.log.info("Setting frequency") self.log.info("Data = {}".format(data)) freq = raw_bytes_to_int(data) self.log.info("Setting frequency to {}".format(freq)) self._device.set_frequency(freq) return general_status_response_packet(address, self.device, 0x60) def set_rotator_angle(self, address, data): self.log.info("Setting rotator angle") self.log.info("Data = {}".format(data)) angle_times_ten = raw_bytes_to_int(data) self.log.info("Setting rotator angle to {}".format(angle_times_ten / 10.)) self._device.set_rotator_angle(angle_times_ten / 10.) return general_status_response_packet(address, self.device, 0x82) def get_phase_info(self, address, data): self.log.info("Getting phase info") return phase_information_response_packet(address, self._device) def get_rotator_angle(self, address, data): self.log.info("Getting rotator angle") return rotator_angle_response_packet(address, self._device) def get_phase_delay(self, address, data): self.log.info("Getting phase time") return phase_time_response_packet(address, self._device)