From 1c6cd234c65d7e8c2f9967bf606ad2c62275a85a Mon Sep 17 00:00:00 2001 From: George Emanuel Date: Thu, 18 Jul 2019 16:42:04 -0400 Subject: [PATCH] Implemented fluidics interface for syringe pump. --- .../fluidics/default_config_syringe.xml | 59 +++++ storm_control/fluidics/kilroy.py | 20 +- storm_control/fluidics/kilroyProtocols.py | 9 +- storm_control/fluidics/pumps/gilson_mp3.py | 3 + storm_control/fluidics/pumps/hamilton_psd6.py | 147 +++++++++++++ storm_control/fluidics/pumps/pumpCommands.py | 62 ++++-- storm_control/fluidics/pumps/pumpControl.py | 206 +++++++++++++++--- .../scope_settings/kilroy_syringe_test.xml | 24 ++ storm_control/fluidics/valves/idex.py | 2 +- 9 files changed, 475 insertions(+), 57 deletions(-) create mode 100644 storm_control/fluidics/default_config_syringe.xml create mode 100644 storm_control/fluidics/pumps/hamilton_psd6.py create mode 100644 storm_control/fluidics/scope_settings/kilroy_syringe_test.xml diff --git a/storm_control/fluidics/default_config_syringe.xml b/storm_control/fluidics/default_config_syringe.xml new file mode 100644 index 000000000..7d4e28361 --- /dev/null +++ b/storm_control/fluidics/default_config_syringe.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Flow Wash + Normal Flow + Stop Flow + + + Normal Flow + Set Hyb 1 + Flow Hybridization + Flow Wash + Flow STORM Buffer + Stop Flow + + + Normal Flow + Set Hyb 2 + Flow Hybridization + Flow Wash + Flow STORM Buffer + Stop Flow + + + diff --git a/storm_control/fluidics/kilroy.py b/storm_control/fluidics/kilroy.py index 2b8e069b3..c67c8d589 100644 --- a/storm_control/fluidics/kilroy.py +++ b/storm_control/fluidics/kilroy.py @@ -16,7 +16,8 @@ import time from PyQt5 import QtCore, QtGui, QtWidgets from storm_control.fluidics.valves.valveChain import ValveChain -from storm_control.fluidics.pumps.pumpControl import PumpControl +from storm_control.fluidics.pumps.pumpControl import PeristalticPumpControl +from storm_control.fluidics.pumps.pumpControl import SyringePumpControl from storm_control.fluidics.kilroyProtocols import KilroyProtocols from storm_control.sc_library.tcpServer import TCPServer import storm_control.sc_library.parameters as params @@ -34,7 +35,12 @@ def __init__(self, parameters): self.tcp_port = parameters.get("tcp_port") self.pump_com_port = parameters.get("pump_com_port") self.pump_ID = parameters.get("pump_ID") - + + if not parameters.has("pump_type"): + self.pump_type = 'peristaltic' + else: + self.pump_type = parameters.get('pump_type') + if not parameters.has("num_simulated_valves"): self.num_simulated_valves = 0 else: @@ -70,12 +76,16 @@ def __init__(self, parameters): verbose = self.verbose) # Create PumpControl instance - self.pumpControl = PumpControl(parameters = parameters) - + if self.pump_type == 'peristaltic': + self.pumpControl = PeristalticPumpControl(parameters=parameters) + elif self.pump_type == 'syringe': + self.pumpControl = SyringePumpControl(parameters=parameters) + # Create KilroyProtocols instance and connect signals self.kilroyProtocols = KilroyProtocols(protocol_xml_path = self.protocols_file, command_xml_path = self.commands_file, - verbose = self.verbose) + verbose = self.verbose, + pumpType = self.pump_type) self.kilroyProtocols.command_ready_signal.connect(self.sendCommand) self.kilroyProtocols.status_change_signal.connect(self.handleProtocolStatusChange) diff --git a/storm_control/fluidics/kilroyProtocols.py b/storm_control/fluidics/kilroyProtocols.py index a280923c3..3b6494300 100644 --- a/storm_control/fluidics/kilroyProtocols.py +++ b/storm_control/fluidics/kilroyProtocols.py @@ -8,6 +8,8 @@ # Jeff Moffitt # 2/15/14 # jeffmoffitt@gmail.com +# +# Updated 7/2019 by George Emanuel # ---------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------- @@ -33,7 +35,8 @@ class KilroyProtocols(QtWidgets.QMainWindow): def __init__(self, protocol_xml_path = "default_config.xml", command_xml_path = "default_config.xml", - verbose = False): + verbose = False, + pumpType='peristaltic'): super(KilroyProtocols, self).__init__() # Initialize internal attributes @@ -47,6 +50,7 @@ def __init__(self, self.status = [-1, -1] # Protocol ID, command ID within protocol self.issued_command = [] self.received_message = None + self.pumpType = pumpType print("----------------------------------------------------------------------") @@ -59,7 +63,8 @@ def __init__(self, # Create instance of PumpCommands class self.pumpCommands = PumpCommands(xml_file_path = self.command_xml_path, - verbose = self.verbose) + verbose = self.verbose, + pumpType = pumpType) # Connect pump commands issue signal self.pumpCommands.change_command_signal.connect(self.issuePumpCommand) diff --git a/storm_control/fluidics/pumps/gilson_mp3.py b/storm_control/fluidics/pumps/gilson_mp3.py index ac1e0fd07..b526e636b 100644 --- a/storm_control/fluidics/pumps/gilson_mp3.py +++ b/storm_control/fluidics/pumps/gilson_mp3.py @@ -50,6 +50,9 @@ def __init__(self, self.startFlow(self.speed, self.direction) self.identification = self.getIdentification() + def pumpType(self): + return 'peristaltic' + def getIdentification(self): return self.sendImmediate(self.pump_ID, "%") diff --git a/storm_control/fluidics/pumps/hamilton_psd6.py b/storm_control/fluidics/pumps/hamilton_psd6.py new file mode 100644 index 000000000..cc25f5784 --- /dev/null +++ b/storm_control/fluidics/pumps/hamilton_psd6.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# ---------------------------------------------------------------------------------------- +# The basic I/O class for a Hamilton syringe pump +# ---------------------------------------------------------------------------------------- +# George Emanuel +# 6/28/19 +# ---------------------------------------------------------------------------------------- + +import serial + +acknowledge = '\x06' +start = '\x0A' +stop = '\x0D' + + +# ---------------------------------------------------------------------------------------- +# HamiltonPSD6 Syringe Pump Class Definition +# ---------------------------------------------------------------------------------------- +class APump(): + def __init__(self, parameters=False): + + print('Initializing pump') + + # Define attributes + self.com_port = parameters.get("pump_com_port", "COM3") + self.pump_ID = parameters.get("pump_ID", 30) + self.verbose = parameters.get("verbose", True) + self.simulate = parameters.get("simulate_pump", True) + self.serial_verbose = parameters.get("serial_verbose", False) + self.flip_flow_direction = parameters.get("flip_flow_direction", False) + + # Create serial port + self.serial = serial.Serial( + port=self.com_port, baudrate=9600, timeout=0.1) + + # enable h commands + self.sendString('/1h30001R\r') + + # TODO Confirm that this initialization only pushes to waste + self.sendString('/1OZ1R\r') + + # Define initial pump status + self.flow_status = "Stopped" + self.speed = 1000 + self.direction = "Forward" + + self.disconnect() + self.setSpeed(self.speed) + self.identification = 'HamiltonSyringe' + + def pumpType(self): + return 'syringe' + + def getIdentification(self): + return self.sendImmediate(self.pump_ID, "%") + + def getPumpPosition(self): + return int(self.sendString('/1?R\r').decode() + .split('`')[1].split('\x03')[0]) + + def getValvePosition(self): + positionDict = { + 0: 'Moving', + 1: 'Input', + 2: 'Output', + 3: 'Wash', + 4: 'Return', + 5: 'Bypass', + 6: 'Extra' + } + return positionDict[int(self.sendString('/1?23000\r').decode() + .split('`')[1].split('\x03')[0])] + + def getStatus(self): + return (self.getPumpPosition(), self.getValvePosition()) + + message = self.readDisplay() + + if self.flip_flow_direction: + direction = {" ": "Not Running", "-": "Forward", "+": "Reverse"}. \ + get(message[0], "Unknown") + else: + direction = {" ": "Not Running", "+": "Forward", "-": "Reverse"}. \ + get(message[0], "Unknown") + + status = "Stopped" if direction == "Not Running" else "Flowing" + + control = {"K": "Keypad", "R": "Remote"}.get(message[-1], "Unknown") + + auto_start = "Disabled" + + speed = float(message[1:len(message) - 1]) + + return (status, self.getPumpPosition(), direction, control, auto_start, "No Error") + + def close(self): + pass + + def setValvePosition(self, valvePosition): + # valve position is either 'input' or 'output' + if valvePosition == 'Input': + self.sendString('/1IR\r') + elif valvePosition == 'Output': + self.sendString('/1OR\r') + + def setSyringePosition(self, position, valvePosition=None, speed=500, + emptyFirst=False): + commandString = '/1' + + if emptyFirst: + commandString += 'OV5000A0' + + if valvePosition is not None: + if valvePosition == 'Input': + commandString += 'I' + else: + commandString += 'O' + + commandString += 'V' + str(int(speed)) + commandString += 'A' + str(int(position)) + + self.sendString(commandString + 'R\r') + + def emptySyringe(self): + self.sendString('/1OV5000A0R\r') + + def stopSyringe(self): + self.sendString('/1TR\r') + + def resetSyringe(self): + self.sendString('/1OZ1R\r') + + def setSpeed(self, speed): + if 2 <= speed <= 5800: + self.sendString('/1V%iR\r' % speed) + + def stopFlow(self): + self.setSpeed(0.0) + return True + + def disconnect(self): + pass + #self.sendAndAcknowledge('\xff') + + def sendString(self, string): + self.serial.write(string.encode()) + return self.serial.readline() diff --git a/storm_control/fluidics/pumps/pumpCommands.py b/storm_control/fluidics/pumps/pumpCommands.py index bd7ad2b7b..f23f8910e 100644 --- a/storm_control/fluidics/pumps/pumpCommands.py +++ b/storm_control/fluidics/pumps/pumpCommands.py @@ -5,6 +5,9 @@ # Jeff Moffitt # 2/16/14 # jeffmoffitt@gmail.com +# +# +# Updated 7/3/2019 by George Emanuel for syringe pump # ---------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------- @@ -25,7 +28,8 @@ class PumpCommands(QtWidgets.QMainWindow): def __init__(self, xml_file_path="default_config.xml", - verbose = False): + verbose = False, + pumpType = 'peristaltic'): super(PumpCommands, self).__init__() # Initialize internal attributes @@ -35,6 +39,7 @@ def __init__(self, self.commands = [] self.num_commands = 0 self.num_pumps = 0 + self.pumpType = pumpType # Create GUI self.createGUI() @@ -171,17 +176,25 @@ def parseCommandXML(self): for pump_command in self.kilroy_configuration.findall("pump_commands"): command_list = pump_command.findall("pump_cmd") for command in command_list: - for pump_config in command.findall("pump_config"): - speed = float(pump_config.get("speed")) - direction = pump_config.get("direction") - if speed < 0.00 or speed > 48.0: - speed = 0.0 - direction = "Stopped" # Flag for stopped flow - direction = {"Forward": "Forward", "Reverse": "Reverse"}.get(direction, "Stopped") - - # Add command - self.commands.append([direction, speed]) - self.command_names.append(command.get("name")) + if self.pumpType == 'peristaltic': + for pump_config in command.findall("pump_config"): + speed = float(pump_config.get("speed")) + direction = pump_config.get("direction") + if speed < 0.00 or speed > 48.0: + speed = 0.0 + direction = "Stopped" # Flag for stopped flow + direction = {"Forward": "Forward", "Reverse": "Reverse"}.get(direction, "Stopped") + + # Add command + self.commands.append([direction, speed]) + self.command_names.append(command.get("name")) + + elif self.pumpType == 'syringe': + for pump_config in command.findall("pump_config"): + position = int(pump_config.get('position')) + speed = int(pump_config.get('speed')) + self.commands.append([position, speed]) + self.command_names.append(command.get('name')) # Record number of configs self.num_commands = len(self.command_names) @@ -193,11 +206,18 @@ def printCommands(self): print("Current commands:") for command_ID in range(self.num_commands): print(self.command_names[command_ID]) - direction = self.commands[command_ID][0] - speed = self.commands[command_ID][1] - text_string = " " + "Flow Direction: " + direction + "\n" - text_string += " " + "Speed: " + str(speed) +"\n" - print(text_string) + if self.pumpType == 'peristaltic': + direction = self.commands[command_ID][0] + speed = self.commands[command_ID][1] + text_string = " " + "Flow Direction: " + direction + "\n" + text_string += " " + "Speed: " + str(speed) +"\n" + print(text_string) + elif self.pumpType == 'syringe': + position = self.commands[command_ID][0] + speed = self.commands[command_ID][1] + text_string = " " + "Position: " + str(position) + "\n" + text_string += " " + "Speed: " + str(speed) +"\n" + print(text_string) # ------------------------------------------------------------------------------------ # Update active command on GUI @@ -229,8 +249,12 @@ def updateCommandDisplay(self): current_command = self.commands[current_ID] text_string = current_command_name + "\n" - text_string += "Flow Direction: " + current_command[0] + "\n" - text_string += "Flow Speed: " + str(current_command[1]) + "\n" + if self.pumpType == 'peristaltic': + text_string += "Flow Direction: " + current_command[0] + "\n" + text_string += "Flow Speed: " + str(current_command[1]) + "\n" + elif self.pumpType == 'syringe': + text_string += "Targe position: " + str(current_command[0]) + "\n" + text_string += "Speed: " + str(current_command[1]) + "\n" self.currentCommandLabel.setText(text_string) # ------------------------------------------------------------------------------------ diff --git a/storm_control/fluidics/pumps/pumpControl.py b/storm_control/fluidics/pumps/pumpControl.py index 638461727..011dc700f 100644 --- a/storm_control/fluidics/pumps/pumpControl.py +++ b/storm_control/fluidics/pumps/pumpControl.py @@ -5,6 +5,8 @@ # Jeff Moffitt # 2/15/14 # jeffmoffitt@gmail.com +# +# Updated 7/2019 by George Emanuel for syringe pump # ---------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------- @@ -13,50 +15,200 @@ import importlib import sys import time +from abc import abstractmethod from PyQt5 import QtCore, QtGui, QtWidgets -# ---------------------------------------------------------------------------------------- -# PumpControl Class Definition -# ---------------------------------------------------------------------------------------- -class PumpControl(QtWidgets.QWidget): - def __init__(self, - parameters = False, - parent = None): - #Initialize parent class +class PumpControl(QtWidgets.QWidget): + def __init__(self, parameters, parent=None): + # Initialize parent class QtWidgets.QWidget.__init__(self, parent) # Define internal attributes self.com_port = parameters.get("pump_com_port") self.pump_ID = parameters.get("pump_id", 30) - self.simulate = parameters.get("simulate_pump", True) self.verbose = parameters.get("verbose", True) self.status_repeat_time = 2000 - self.speed_units = "rpm" + self.speed_units = "ml/min" # Dynamic import of pump class - pump_module = importlib.import_module(parameters.get("pump_class", "storm_control.fluidics.pumps.rainin_rp1")) + pump_module = importlib.import_module(parameters.get( + "pump_class", "storm_control.fluidics.pumps.rainin_rp1")) # Create Instance of Pump - self.pump = pump_module.APump(parameters = parameters) + self.pump = pump_module.APump(parameters=parameters) # Create GUI Elements self.createGUI() self.pollPumpStatus() - + # Define timer for periodic polling of pump status - self.status_timer = QtCore.QTimer() + self.status_timer = QtCore.QTimer() self.status_timer.setInterval(self.status_repeat_time) self.status_timer.timeout.connect(self.pollPumpStatus) self.status_timer.start() - # ------------------------------------------------------------------------------------ - # Close class - # ------------------------------------------------------------------------------------ + @abstractmethod + def createGUI(self): + pass + + @abstractmethod + def pollPumpStatus(self): + pass + def close(self): if self.verbose: "Print closing pump" self.pump.close() + def setEnabled(self, enabled): + # This control is always enabled to allow emergency control over the flow + pass + + +class SyringePumpControl(PumpControl): + + def __init__(self, parameters, parent=None): + super().__init__(parameters, parent) + self.speed_units = "ml/min" + + def createGUI(self): + self.mainWidget = QtWidgets.QGroupBox() + self.mainWidget.setTitle("Pump Controls") + self.mainWidgetLayout = QtWidgets.QVBoxLayout(self.mainWidget) + + # Add individual widgets + self.pump_identification_label = QtWidgets.QLabel() + self.pump_identification_label.setText("No Pump Attached") + + self.flow_status_label = QtWidgets.QLabel() + self.flow_status_label.setText("Flow Status:") + self.flow_status_display = QtWidgets.QLabel() + self.flow_status_display.setText("Unknown") + font = QtGui.QFont() + font.setPointSize(20) + self.flow_status_display.setFont(font) + + self.speed_label = QtWidgets.QLabel() + self.speed_label.setText("Flow Rate:") + self.speed_display = QtWidgets.QLabel() + self.speed_display.setText("Unknown") + font = QtGui.QFont() + font.setPointSize(20) + self.speed_display.setFont(font) + + self.speed_control_label = QtWidgets.QLabel() + self.speed_control_label.setText("Set Syringe Speed") + self.speed_control_entry_box = QtWidgets.QLineEdit() + self.speed_control_entry_box.setText("1000") + self.speed_control_entry_box.editingFinished.connect(self.coerceSpeed) + + self.direction_control_label = QtWidgets.QLabel() + self.direction_control_label.setText("Set Valve Position") + self.direction_control = QtWidgets.QComboBox() + self.direction_control.addItem("Input") + self.direction_control.addItem("Output") + + self.position_control_label = QtWidgets.QLabel() + self.position_control_label.setText("Set Syringe Position") + self.position_control_entry_box = QtWidgets.QLineEdit() + self.position_control_entry_box.setText("1000") + self.position_control_entry_box.editingFinished.connect(self.coercePosition) + + self.start_flow_button = QtWidgets.QPushButton() + self.start_flow_button.setText("Move Syringe") + self.start_flow_button.clicked.connect(self.handleMoveSyringe) + + self.empty_button = QtWidgets.QPushButton() + self.empty_button.setText("Empty Syringe") + self.empty_button.clicked.connect(self.handleEmptySyringe) + + self.stop_button = QtWidgets.QPushButton() + self.stop_button.setText("Stop Syringe") + self.stop_button.clicked.connect(self.handleStopSyringe) + + self.mainWidgetLayout.addWidget(self.flow_status_display) + self.mainWidgetLayout.addWidget(self.speed_display) + self.mainWidgetLayout.addWidget(self.direction_control_label) + self.mainWidgetLayout.addWidget(self.direction_control) + self.mainWidgetLayout.addWidget(self.position_control_label) + self.mainWidgetLayout.addWidget(self.position_control_entry_box) + self.mainWidgetLayout.addWidget(self.speed_control_label) + self.mainWidgetLayout.addWidget(self.speed_control_entry_box) + self.mainWidgetLayout.addWidget(self.start_flow_button) + self.mainWidgetLayout.addWidget(self.empty_button) + self.mainWidgetLayout.addWidget(self.stop_button) + self.mainWidgetLayout.addStretch(1) + + def pollPumpStatus(self): + status = self.pump.getStatus() + self.flow_status_display.setText(str(status[0])) + self.speed_display.setText(status[1]) + + def pumpType(self): + return 'syringe' + + def coercePosition(self): + current_speed_text = self.position_control_entry_box.displayText() + try: + position_value = int(current_speed_text) + if position_value < 0: + self.position_control_entry_box.setText("0") + elif position_value > 6000: + self.position_control_entry_box.setText("6000") + else: + self.position_control_entry_box.setText('%i' % position_value) + except: + self.position_control_entry_box.setText("1000") + + def coerceSpeed(self): + current_speed_text = self.speed_control_entry_box.displayText() + try: + speed_value = int(current_speed_text) + if speed_value < 2: + self.speed_control_entry_box.setText("2") + elif speed_value > 5800: + self.speed_control_entry_box.setText("5800") + else: + self.speed_control_entry_box.setText('%i' % speed_value) + except: + self.speed_control_entry_box.setText("1000") + + def handleMoveSyringe(self): + self.pump.setSyringePosition( + int(self.position_control_entry_box.displayText()), + self.direction_control.currentText(), + int(self.speed_control_entry_box.displayText())) + + #time.sleep(0.1) + #self.pollPumpStatus() + + def handleEmptySyringe(self): + self.pump.emptySyringe() + time.sleep(0.1) + self.pollPumpStatus() + + def handleStopSyringe(self): + self.pump.stopSyringe() + time.sleep(0.1) + self.pollPumpStatus() + + def receiveCommand(self, command): + print(command) + self.pump.setSyringePosition( + int(command[0]), 'Input', int(command[1]), True) + +# ---------------------------------------------------------------------------------------- +# PeristalticPumpControl Class Definition +# ---------------------------------------------------------------------------------------- +class PeristalticPumpControl(PumpControl): + + def __init__(self, parameters=False, parent=None): + super().__init__(parameters, parent) + self.speed_units = "rpm" + + def pumpType(self): + return 'peristaltic' + # ------------------------------------------------------------------------------------ # Coerce Speed Entry to Acceptable Range # ------------------------------------------------------------------------------------ @@ -193,12 +345,6 @@ def receiveCommand(self, command): else: self.pump.startFlow(speed, direction) - # ------------------------------------------------------------------------------------ - # Determine Enabled State - # ------------------------------------------------------------------------------------ - def setEnabled(self, enabled): - # This control is always enabled to allow emergency control over the flow - pass # ---------------------------------------------------------------------------------------- # Stand Alone Test Class @@ -208,14 +354,14 @@ def __init__(self, parent = None): super(StandAlone, self).__init__(parent) # scroll area widget contents - layout - self.pump = PumpControl(com_port = 4, - pump_ID = 30, - simulate = False, - verbose = False) + self.pump = SyringePumpControl( + parameters=dict(pump_com_port='COM9', pump_ID=30, simulate=False, + verbose=False, + pump_class='hamilton_psd6')) # central widget - self.centralWidget = QtGui.QWidget() - self.mainLayout = QtGui.QVBoxLayout(self.centralWidget) + self.centralWidget = QtWidgets.QWidget() + self.mainLayout = QtWidgets.QVBoxLayout(self.centralWidget) self.mainLayout.addWidget(self.pump.mainWidget) # set central widget @@ -231,7 +377,7 @@ def __init__(self, parent = None): menubar = self.menuBar() file_menu = menubar.addMenu("File") - exit_action = QtGui.QAction("Exit", self) + exit_action = QtWidgets.QAction("Exit", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.closeEvent) diff --git a/storm_control/fluidics/scope_settings/kilroy_syringe_test.xml b/storm_control/fluidics/scope_settings/kilroy_syringe_test.xml new file mode 100644 index 000000000..feede6570 --- /dev/null +++ b/storm_control/fluidics/scope_settings/kilroy_syringe_test.xml @@ -0,0 +1,24 @@ + + + + Hamilton + COM37 + 0 + + + pumps.gilson_mp3 + COM36 + 30 + False + False + + + True + True + 9500 + default_config.xml + default_config.xml + + + \ No newline at end of file diff --git a/storm_control/fluidics/valves/idex.py b/storm_control/fluidics/valves/idex.py index 8307fa2e8..3c08302ef 100644 --- a/storm_control/fluidics/valves/idex.py +++ b/storm_control/fluidics/valves/idex.py @@ -22,7 +22,7 @@ def __init__(self, com_port=2, verbose= False): baudrate = 9600, timeout=0.5) #give the arduino time to initialize - time.sleep(2) + time.sleep(10) self.port_count = self.getPortCount() self.updateValveStatus()