class MettlerToledoDevice(object): ''' This Python package (mettler_toledo_device) creates a class named MettlerToledoDevice, which contains an instance of serial_device2.SerialDevice and adds methods to it to interface to Mettler Toledo balances and scales that use the Mettler Toledo Standard Interface Command Set (MT-SICS). Example Usage: dev = MettlerToledoDevice() # Might automatically find device if one available # if it is not found automatically, specify port directly dev = MettlerToledoDevice(port='/dev/ttyUSB0') # Linux specific port dev = MettlerToledoDevice(port='/dev/tty.usbmodem262471') # Mac OS X specific port dev = MettlerToledoDevice(port='COM3') # Windows specific port dev.get_serial_number() 1126493049 dev.get_balance_data() ['XS204', 'Excellence', '220.0090', 'g'] dev.get_weight_stable() [-0.0082, 'g'] #if weight is stable None #if weight is dynamic dev.get_weight() [-0.6800, 'g', 'S'] #if weight is stable [-0.6800, 'g', 'D'] #if weight is dynamic dev.zero_stable() True #zeros if weight is stable False #does not zero if weight is not stable dev.zero() 'S' #zeros if weight is stable 'D' #zeros if weight is dynamic ''' _TIMEOUT = 0.05 _WRITE_WRITE_DELAY = 0.05 _RESET_DELAY = 2.0 def __init__(self, *args, **kwargs): if 'debug' in kwargs: self.debug = kwargs['debug'] else: kwargs.update({'debug': DEBUG}) self.debug = DEBUG if 'try_ports' in kwargs: try_ports = kwargs.pop('try_ports') else: try_ports = None if 'baudrate' not in kwargs: kwargs.update({'baudrate': BAUDRATE}) elif (kwargs['baudrate'] is None) or (str(kwargs['baudrate']).lower() == 'default'): kwargs.update({'baudrate': BAUDRATE}) if 'timeout' not in kwargs: kwargs.update({'timeout': self._TIMEOUT}) if 'write_write_delay' not in kwargs: kwargs.update({'write_write_delay': self._WRITE_WRITE_DELAY}) if ('port' not in kwargs) or (kwargs['port'] is None): port = find_mettler_toledo_device_port(baudrate=kwargs['baudrate'], try_ports=try_ports, debug=kwargs['debug']) kwargs.update({'port': port}) t_start = time.time() self._serial_device = SerialDevice(*args, **kwargs) atexit.register(self._exit_mettler_toledo_device) time.sleep(self._RESET_DELAY) t_end = time.time() self._debug_print('Initialization time =', (t_end - t_start)) def _debug_print(self, *args): if self.debug: print(*args) def _exit_mettler_toledo_device(self): pass def _args_to_request(self, *args): request = ''.join(map(str, args)) request = request + '\r\n' return request def _send_request(self, *args): '''Sends request to device over serial port and returns number of bytes written''' request = self._args_to_request(*args) self._debug_print('request', request) bytes_written = self._serial_device.write_check_freq(request, delay_write=True) return bytes_written def _send_request_get_response(self, *args): '''Sends request to device over serial port and returns response''' request = self._args_to_request(*args) self._debug_print('request', request) response = self._serial_device.write_read(request, use_readline=True, check_write_freq=True) response = response.replace('"', '') response_list = response.split() if 'ES' in response_list[0]: raise MettlerToledoError('Syntax Error!') elif 'ET' in response_list[0]: raise MettlerToledoError('Transmission Error!') elif 'EL' in response_list[0]: raise MettlerToledoError('Logical Error!') return response_list def close(self): ''' Close the device serial port. ''' self._serial_device.close() def get_port(self): return self._serial_device.port def get_commands(self): ''' Inquiry of all implemented MT-SICS commands. ''' response = self._send_request_get_response('I0') if 'I' in response[1]: raise MettlerToledoError( 'The list cannot be sent at present as another operation is taking place.' ) return response[2:] def get_mtsics_level(self): ''' Inquiry of MT-SICS level and MT-SICS versions. ''' response = self._send_request_get_response('I1') if 'I' in response[1]: raise MettlerToledoError( 'Command understood, not executable at present.') return response[2:] def get_balance_data(self): ''' Inquiry of balance data. ''' response = self._send_request_get_response('I2') if 'I' in response[1]: raise MettlerToledoError( 'Command understood, not executable at present.') return response[2:] def get_software_version(self): ''' Inquiry of balance SW version and type definition number. ''' response = self._send_request_get_response('I3') if 'I' in response[1]: raise MettlerToledoError( 'Command understood, not executable at present.') return response[2:] def get_serial_number(self): ''' Inquiry of serial number. ''' response = self._send_request_get_response('I4') if 'I' in response[1]: raise MettlerToledoError( 'Command understood, not executable at present.') return response[2] def get_software_id(self): ''' Inquiry of SW-Identification number. ''' response = self._send_request_get_response('I5') if 'I' in response[1]: raise MettlerToledoError( 'Command understood, not executable at present.') return response[2] def get_weight_stable(self): ''' Send the current stable net weight value. ''' try: response = self._send_request_get_response('S') if 'I' in response[1]: raise MettlerToledoError( 'Command understood, not executable at present.') elif '+' in response[1]: raise MettlerToledoError('Balance in overload range.') elif '-' in response[1]: raise MettlerToledoError('Balance in underload range.') response[2] = float(response[2]) return response[2:] except: pass def get_weight(self): ''' Send the current net weight value, irrespective of balance stability. ''' response = self._send_request_get_response('SI') if 'I' in response[1]: raise MettlerToledoError( 'Command understood, not executable at present.') elif '+' in response[1]: raise MettlerToledoError('Balance in overload range.') elif '-' in response[1]: raise MettlerToledoError('Balance in underload range.') response.append(response[1]) response[2] = float(response[2]) return response[2:] def zero_stable(self): ''' Zero the balance. ''' try: response = self._send_request_get_response('Z') if 'I' in response[1]: raise MettlerToledoError( 'Zero setting not performed (balance is currently executing another command, e.g. taring, or timeout as stability was not reached).' ) elif '+' in response[1]: raise MettlerToledoError( 'Upper limit of zero setting range exceeded.') elif '-' in response[1]: raise MettlerToledoError( 'Lower limit of zero setting range exceeded.') return True except: return False def zero(self): ''' Zero the balance immediately regardless the stability of the balance. ''' response = self._send_request_get_response('ZI') if 'I' in response[1]: raise MettlerToledoError( 'Zero setting not performed (balance is currently executing another command, e.g. taring, or timeout as stability was not reached).' ) elif '+' in response[1]: raise MettlerToledoError( 'Upper limit of zero setting range exceeded.') elif '-' in response[1]: raise MettlerToledoError( 'Lower limit of zero setting range exceeded.') return response[1] def reset(self): ''' Resets the balance to the condition found after switching on, but without a zero setting being performed. ''' self._send_request('@')
class uPythonHelperUI: # ---------------------------------------------------------------------- # | MainFrame # | --------------------------------------- --------------------------- | # | | TermFrame | | File Frame | | # | | --------------------------------- | | ----------------------- | | # | | | PortFrame | | | | Transfer Frame | | | # | | --------------------------------- | | ----------------------- | | # | | --------------------------------- | | ----------------------- | | # | | | TermView | | | | FileView | | | # | | --------------------------------- | | ----------------------- | | # | --------------------------------------- --------------------------- | # ---------------------------------------------------------------------- def __init__(self): self.dev = None self.pauseUpdate = True self.fileView = None self.uploadButton = None self.downloadButton = None self.deleteButton = None self.connectButton = None self.portName = None self.termArea = None self.progress = None self.connected = False root = Tk() mainFrame = Frame(root) root.title("uPython Helper") mainFrame.pack(padx=10, pady=10) # Terminal Side termFrame = Frame(mainFrame) termFrame.pack(padx=1, pady=1, side=LEFT) portFrame = Frame(termFrame) portFrame.pack(padx=1, pady=1, side=TOP, fill=X) Label(portFrame, text='Port/Device').pack(side=LEFT) self.portName = Entry(portFrame) self.portName.pack(padx=1, pady=1, side=LEFT) #If not Windows, user might try the following: # '/dev/ttyUSB0' # Linux # '/dev/tty.usbmodem262471' # Mac OS X self.portName.insert(0, 'COM3') self.connectButton = Button(portFrame, text='Connect', command=self.onConnect) self.connectButton.pack(padx=1, pady=1, side=LEFT) self.progress = ttk.Progressbar(portFrame, orient="horizontal", length=200, mode="determinate") self.progress.pack() self.termArea = tkst.ScrolledText(master=termFrame, wrap=WORD, width=100, height=30, padx=10, pady=10, takefocus=True, background='lightgrey') self.termArea.bind('<Key>', self.handleInput) self.termArea.bind('<Return>', self.handleInput) self.termArea.pack(padx=2, pady=2, side=BOTTOM) # File area self.fileFrame = Frame(mainFrame) self.fileFrame.pack(padx=2, pady=2, side=RIGHT, fill=Y) transferFrame = Frame(self.fileFrame) transferFrame.pack(padx=1, pady=1, side=TOP, fill=X) self.uploadButton = Button(transferFrame, text='Upload File', command=self.uploadFile, state=DISABLED) self.uploadButton.pack(padx=1, pady=1, side=LEFT) self.downloadButton = Button(transferFrame, text='Download File', command=self.downloadFile, state=DISABLED) self.downloadButton.pack(padx=1, pady=1, side=LEFT) self.deleteButton = Button(transferFrame, text='Delete File', command=self.deleteFile, state=DISABLED) self.deleteButton.pack(padx=1, pady=1, side=LEFT) self.fileView = Treeview(self.fileFrame) self.fileView["columns"] = "bytes" self.fileView.heading("#0", text="filename", anchor=W) self.fileView.column("bytes", width=75) self.fileView.heading("bytes", text="bytes", anchor=W) self.fileView.pack(side=RIGHT, fill=Y) termUpdateTimer = None def updateDataIfNeeded(): global termUpdateTimer if not self.pauseUpdate: if self.dev: if self.dev.in_waiting: resp = self.dev.read_all() self.write(resp) termUpdateTimer = threading.Timer(0.5, updateDataIfNeeded) termUpdateTimer.start() termUpdateTimer = threading.Timer(0.5, updateDataIfNeeded) termUpdateTimer.start() def on_closing(): global termUpdateTimer root.destroy() termUpdateTimer.cancel() def make_menu(w): global the_menu the_menu = Menu(w, tearoff=0) the_menu.add_command(label="Copy") the_menu.add_command(label="Paste") def show_menu(e): w = e.widget the_menu.entryconfigure( "Copy", command=lambda: w.event_generate("<<Copy>>")) the_menu.entryconfigure( "Paste", command=lambda: w.event_generate("<<Paste>>")) the_menu.tk.call("tk_popup", the_menu, e.x_root, e.y_root) make_menu(root) self.termArea.bind("<Button-3><ButtonRelease-3>", show_menu) root.protocol("WM_DELETE_WINDOW", on_closing) root.mainloop() def remote(self, cmd, ignoreResult=False): asc = cmd.encode('ascii', 'ignore') self.pauseUpdate = True self.dev.write(asc) if ignoreResult: self.pauseUpdate = False return sleep(0.5) res = self.dev.read_all() self.pauseUpdate = False return res def write(self, txt): self.termArea.insert(END, txt) def handleInput(self, inputData): if self.connected: self.remote(inputData.char, ignoreResult=True) return "break" @staticmethod def ascii(uni): return uni.encode('ascii', 'ignore') def getDirInfo(self): self.remote("import os\r\n") resp = self.remote("os.listdir()\r\n") resp = resp.decode().split("\r\n")[1] entries = eval(resp) result = [] for entry in entries: resp = self.remote( "statinfo = os.stat('{}')\r\nstatinfo\r\n".format(entry)) resp = resp.decode().split("\r\n")[2] statinfo = eval(resp) result.append({'name': entry, 'size': statinfo[6]}) return result def uploadFile(self): def thrProc(): filename = filedialog.askopenfile(mode='r') if filename is None: return self.progressStart() f = open(filename.name, 'rb') bytesRead = f.read() b64 = base64.b64encode(bytesRead) fn = os.path.split(filename.name)[1] self.remote('import ubinascii\r\n') self.remote('f = open("{}","w")\r\n'.format(fn)) b64Len = len(b64) chunkSize = 1024 for i in range(0, b64Len // chunkSize): sub = b64[chunkSize * i:(chunkSize * (i + 1))].decode() self.remote('s = "{}"\r\n'.format(sub)) self.remote('s2 = ubinascii.a2b_base64(s)\r\n') self.remote('f.write(s2)\r\n', True) if b64Len % chunkSize: sub = b64[b64Len // chunkSize * chunkSize:b64Len].decode() self.remote('s = "{}"\r\n'.format(sub)) self.remote('s2 = ubinascii.a2b_base64(s)\r\n') self.remote('f.write(s2)\r\n', True) self.remote('f.close()\r\n', True) self.clearFileView() self.populateFileView() self.progressStop() t1 = threading.Thread(target=thrProc) t1.start() def downloadFile(self): def thrProc(): selected = self.fileView.selection() if not selected: showwarning("Download", "Please select an item.") return if len(selected) > 1: showwarning("Download", "Please select a single item.") return sel = self.fileView.item(selected[0]) filename = filedialog.asksaveasfile(mode='w', initialfile=sel['text']) if filename is None: return self.progressStart() fLocal = open(filename.name, 'wb') remoteName = sel['text'] remoteSize = sel['values'][0] self.remote('import ubinascii\r\n') self.remote('f = open("{}","r")\r\n'.format(remoteName)) for i in range(0, remoteSize // 1024): self.remote("data = f.read(1024)\r\n") self.remote('s2 = ubinascii.b2a_base64(data)\r\n') resp = self.remote('print(s2)\r\n') resp = resp.decode().split("\r\n")[1][2:].split("\\")[0] chunkBin = base64.b64decode(resp) fLocal.write(chunkBin) remainder = remoteSize % 1024 if remainder: self.remote("data = f.read({})\r\n".format(remainder)) self.remote('s2 = ubinascii.b2a_base64(data)\r\n') resp = self.remote('print(s2)\r\n') resp = resp.decode().split("\r\n")[1][2:].split("\\")[0] tailBin = base64.b64decode(resp) fLocal.write(tailBin) fLocal.close() self.remote('f.close()\r\n') self.progressStop() t1 = threading.Thread(target=thrProc) t1.start() def deleteFile(self): def thrProc(): selected = self.fileView.selection() if not selected: showwarning("Delete", "Please select an item to delete.") return if len(selected) > 1: showwarning("Delete", "Please select a single item.") return sel = self.fileView.item(selected[0]) res = askquestion("Delete", 'Are you sure you want to delete "{}"?'.format( sel['text']), icon='warning') if res != 'yes': return self.progressStart() self.remote('import os\r\n') self.remote('os.remove("{}")\r\n'.format(sel['text'])) self.fileView.delete(selected[0]) self.progressStop() t1 = threading.Thread(target=thrProc) t1.start() def populateFileView(self): info = self.getDirInfo() for item in info: values = item['size'] self.fileView.insert('', 'end', iid=None, text=item['name'], values=values) def clearFileView(self): self.fileView.delete(*self.fileView.get_children()) def progressStart(self): self.progress["mode"] = "indeterminate" self.progress.start(10) def progressStop(self): self.progress.stop() self.progress["mode"] = "determinate" def onConnect(self): def thrproc(): if self.connectButton.cget('relief') == 'raised': self.progressStart() try: port = self.portName.get() self.dev = SerialDevice(port=port) except Exception as e: if self.downloadButton: self.downloadButton.config(state=DISABLED) self.uploadButton.config(state=DISABLED) self.deleteButton.config(state=DISABLED) showwarning("Connect", "Problem opening serial. Details:\n" + str(e)) return self.dev.baudrate = 115200 self.connectButton.config(relief=SUNKEN) # send ctrl-c to break any potentially running python program ctrlc = chr(3) self.remote(ctrlc) self.populateFileView() self.termArea.insert( END, ">>> ") # write out the initial prompt... purely cosmetic self.termArea.config(background='white') self.downloadButton.config(state=NORMAL) self.uploadButton.config(state=NORMAL) self.deleteButton.config(state=NORMAL) self.connected = True else: self.connected = False if self.dev is not None: self.dev.close() self.dev = None self.connectButton.config(relief=RAISED) self.downloadButton.config(state=DISABLED) self.deleteButton.config(state=DISABLED) self.uploadButton.config(state=DISABLED) self.clearFileView() self.termArea.config(background='lightgrey') self.progressStop() t1 = threading.Thread(target=thrproc) t1.start()
class ZaberDevice(object): ''' This Python package (zaber_device) creates a class named ZaberDevice, which contains an instance of serial_device2.SerialDevice and adds methods to it to interface to Zaber motorized linear slides. Example Usage: dev = ZaberDevice() # Might automatically find device if one available # if it is not found automatically, specify port directly dev = ZaberDevice(port='/dev/ttyUSB0') # Linux dev = ZaberDevice(port='/dev/tty.usbmodem262471') # Mac OS X dev = ZaberDevice(port='COM3') # Windows dev.get_actuator_count() 2 dev.get_position() [130000, 160000] dev.home() dev.moving() [True, True] dev.moving() [False, False] dev.get_position() [0, 0] dev.move_relative(10000) dev.get_position() [10000, 10000] dev.move_relative(10000,0) dev.moving() [True, False] dev.get_position() [20000, 10000] dev.store_position(0) dev.get_stored_position(0) [20000, 10000] dev.move_at_speed(1000) dev.stop() dev.get_position() [61679, 51679] dev.move_to_stored_position(0) dev.get_position() [20000, 10000] ''' _TIMEOUT = 0.05 _WRITE_WRITE_DELAY = 0.05 _RESET_DELAY = 2.0 def __init__(self,*args,**kwargs): if 'debug' in kwargs: self.debug = kwargs['debug'] else: kwargs.update({'debug': DEBUG}) self.debug = DEBUG kwargs['debug'] = False if 'try_ports' in kwargs: try_ports = kwargs.pop('try_ports') else: try_ports = None if 'baudrate' not in kwargs: kwargs.update({'baudrate': BAUDRATE}) elif (kwargs['baudrate'] is None) or (str(kwargs['baudrate']).lower() == 'default'): kwargs.update({'baudrate': BAUDRATE}) if 'timeout' not in kwargs: kwargs.update({'timeout': self._TIMEOUT}) if 'write_write_delay' not in kwargs: kwargs.update({'write_write_delay': self._WRITE_WRITE_DELAY}) if ('port' not in kwargs) or (kwargs['port'] is None): port = find_zaber_device_port(baudrate=kwargs['baudrate'], try_ports=try_ports, debug=kwargs['debug']) kwargs.update({'port': port}) t_start = time.time() self._debug_print("port = {0}".format(kwargs['port'])) self._serial_device = SerialDevice(*args,**kwargs) atexit.register(self._exit_zaber_device) time.sleep(self._RESET_DELAY) self._lock = threading.Lock() self._actuator_count = None self._zaber_response = '' t_end = time.time() self._debug_print('Initialization time =', (t_end - t_start)) def _debug_print(self, *args): if self.debug: print(*args) def _exit_zaber_device(self): pass def _args_to_request(self,*args): request = ''.join(map(chr,args)) return request def _data_to_args_list(self,data): if data is None: return [0,0,0,0] # Handle negative data # If Cmd_Data < 0 then Cmd_Data = 256^4 + Cmd_Data # Cmd_Byte_6 = Cmd_Data / 256^3 # Cmd_Data = Cmd_Data - 256^3 * Cmd_Byte_6 # Cmd_Byte_5 = Cmd_Data / 256^2 # Cmd_Data = Cmd_Data - 256^2 * Cmd_Byte_5 # Cmd_Byte_4 = Cmd_Data / 256 # Cmd_Data = Cmd_Data - 256 * Cmd_Byte_4 # Cmd_Byte 3 = Cmd_Data data = int(data) if data < 0: data += pow(256,4) arg3 = data // pow(256,3) data -= pow(256,3)*arg3 arg2 = data // pow(256,2) data -= pow(256,2)*arg2 arg1 = data // 256 data -= 256*arg1 arg0 = data return [arg0,arg1,arg2,arg3] def _response_to_data(self,response): self._debug_print('len(response)',len(response)) actuator_count = len(response) // RESPONSE_LENGTH self._debug_print('actuator_count',actuator_count) if self._actuator_count is not None: if actuator_count != self._actuator_count: self._debug_print("actuator_count != self._actuator_count!!") raise ZaberNumberingError('') elif actuator_count > 0: self._actuator_count = actuator_count data_list = [None for d in range(actuator_count)] for actuator_n in range(actuator_count): actuator = ord(response[0+actuator_n*RESPONSE_LENGTH]) - 1 self._debug_print('response_actuator',actuator) if (actuator >= actuator_count) or (actuator < 0): self._debug_print("invalid actuator number!!") raise ZaberNumberingError('') cmd = ord(response[1+actuator_n*RESPONSE_LENGTH]) self._debug_print('response_command',cmd) response_copy = response[(2+actuator_n*RESPONSE_LENGTH):(6+actuator_n*RESPONSE_LENGTH)] # Reply_Data = 256^3 * Rpl_Byte 6 + 256^2 * Rpl_Byte_5 + 256 * Rpl_Byte_4 + Rpl_Byte_3 # If Rpl_Byte_6 > 127 then Reply_Data = Reply_Data - 256^4 data = pow(256,3)*ord(response_copy[3]) + pow(256,2)*ord(response_copy[2]) + 256*ord(response_copy[1]) + ord(response_copy[0]) data_list[actuator] = data if any([data is None for data in data_list]): raise ZaberNumberingError('') return data_list def _send_request(self,command,actuator=None,data=None): '''Sends request to device over serial port and returns number of bytes written''' with self._lock: if actuator is None: actuator = 0 elif actuator < 0: raise ZaberError('actuator must be >= 0') else: actuator = int(actuator) actuator += 1 args_list = self._data_to_args_list(data) request = self._args_to_request(actuator,command,*args_list) self._debug_print('request', [ord(c) for c in request]) bytes_written = self._serial_device.write_check_freq(request,delay_write=True) self._debug_print('bytes_written', bytes_written) return bytes_written def _send_request_get_response(self,command,actuator=None,data=None): '''Sends request to device over serial port and returns response''' request_successful = False with self._lock: if actuator is None: actuator = 0 elif actuator < 0: raise ZaberError('actuator must be >= 0') else: actuator = int(actuator) actuator += 1 args_list = self._data_to_args_list(data) request = self._args_to_request(actuator,command,*args_list) request_attempt = 0 while (not request_successful) and (request_attempt < REQUEST_ATTEMPTS_MAX): try: self._debug_print('request attempt: {0}'.format(request_attempt)) self._debug_print('request', [ord(c) for c in request]) request_attempt += 1 response = self._serial_device.write_read(request,use_readline=False,match_chars=True) response_array = [ord(c) for c in response] response_str = str(response_array) self._debug_print('response', response_str) self._zaber_response = response_str data = self._response_to_data(response) self._debug_print('data', data) request_successful = True except ZaberNumberingError: self._debug_print("request error!!") if not request_successful: raise ZaberError('Improper actuator response, may need to rearrange zaber cables or use renumber method to fix.') else: return data def close(self): ''' Close the device serial port. ''' self._serial_device.close() def get_port(self): return self._serial_device.port def reset(self,actuator=None): ''' Sets the actuator to its power-up condition. ''' self._send_request(0,actuator) def home(self,actuator=None): ''' Moves to the home position and resets the actuator's internal position. ''' self._send_request(1,actuator) def renumber(self): ''' Assigns new numbers to all the actuators in the order in which they are connected. ''' self._send_request(2,None) def store_position(self,address,actuator=None): ''' Saves the current absolute position of the actuator into the address. ''' address = int(address) if (address < POSITION_ADDRESS_MIN) or (address > POSITION_ADDRESS_MAX): raise ZaberError('address must be between {0} and {1}'.format(POSITION_ADDRESS_MIN,POSITION_ADDRESS_MAX)) self._send_request(16,actuator,address) def get_stored_position(self,address): ''' Gets the current absolute position of the actuator into the address. ''' address = int(address) if (address < POSITION_ADDRESS_MIN) or (address > POSITION_ADDRESS_MAX): raise ZaberError('address must be between {0} and {1}'.format(POSITION_ADDRESS_MIN,POSITION_ADDRESS_MAX)) actuator = None response = self._send_request_get_response(17,actuator,address) return response def move_to_stored_position(self,address,actuator=None): ''' Moves the actuator to the position stored in the specified address. ''' address = int(address) if (address < POSITION_ADDRESS_MIN) or (address > POSITION_ADDRESS_MAX): raise ZaberError('address must be between {0} and {1}'.format(POSITION_ADDRESS_MIN,POSITION_ADDRESS_MAX)) self._send_request(18,actuator,address) def move_absolute(self,position,actuator=None): ''' Moves the actuator to the position specified in microsteps. ''' if position < 0: return self._send_request(20,actuator,position) def get_actuator_count(self): ''' Return the number of Zaber actuators connected in a chain. ''' data = 123 actuator = None response = self._send_request_get_response(55,actuator,data) try: actuator_count = len(response) except TypeError: actuator_count = 1 return actuator_count def move_relative(self,position,actuator=None): ''' Moves the actuator by the positive or negative number of microsteps specified. ''' self._send_request(21,actuator,position) def move_at_speed(self,speed,actuator=None): ''' Moves the actuator at a constant speed until stop is commanded or a limit is reached. ''' self._send_request(22,actuator,speed) def stop(self,actuator=None): ''' Stops the device from moving by preempting any move instruction. ''' self._send_request(23,actuator) def restore_settings(self): ''' Restores the device settings to the factory defaults. ''' self._send_request(36,None) def get_actuator_id(self): ''' Returns the id number for the type of actuator connected. ''' actuator = None response = self._send_request_get_response(50,actuator) return response def _return_setting(self,setting,actuator): ''' Returns the current value of the specified setting. ''' response = self._send_request_get_response(53,actuator,setting) return response def _get_microstep_resolution(self): ''' Returns the number of microsteps per step. ''' actuator = None response = self._return_setting(37,actuator) return response def set_running_current(self,current,actuator=None): ''' Sets the desired current to be used when the actuator is moving. (1-100) ''' if (current < CURRENT_MIN) or (current > CURRENT_MAX): raise ZaberError('current must be between {0} and {1}'.format(CURRENT_MIN,CURRENT_MAX)) zaber_current = self._map(current,CURRENT_MIN,CURRENT_MAX,ZABER_CURRENT_MIN,ZABER_CURRENT_MAX) self._send_request(38,actuator,zaber_current) def get_running_current(self): ''' Returns the desired current to be used when the actuator is moving. (1-100) ''' actuator = None response = self._return_setting(38,actuator) response = self._map_list(response,ZABER_CURRENT_MIN,ZABER_CURRENT_MAX,CURRENT_MIN,CURRENT_MAX) return response def set_hold_current(self,current,actuator=None): ''' Sets the desired current to be used when the actuator is holding its position. (1-100) ''' if (current < CURRENT_MIN) or (current > CURRENT_MAX): raise ZaberError('current must be between {0} and {1}'.format(CURRENT_MIN,CURRENT_MAX)) zaber_current = self._map(current,CURRENT_MIN,CURRENT_MAX,ZABER_CURRENT_MIN,ZABER_CURRENT_MAX) self._send_request(39,actuator,zaber_current) def get_hold_current(self): ''' Returns the desired current to be used when the actuator is holding its position. (1-100) ''' actuator = None response = self._return_setting(39,actuator) response = self._map_list(response,ZABER_CURRENT_MIN,ZABER_CURRENT_MAX,CURRENT_MIN,CURRENT_MAX) return response def _set_actuator_mode(self,mode,actuator=None): ''' Sets the mode for the given actuator. ''' self._send_request(40,actuator,mode) def _get_actuator_mode(self): ''' Returns the mode. ''' actuator = None response = self._return_setting(40,actuator) return response def get_actuator_mode(self): ''' Returns the mode as binary string. ''' actuator = None response = self._return_setting(40,actuator) response = ["{0:b}".format(r) for r in response] return response def _set_actuator_mode_bit(self,bit,actuator=None): ''' Sets the mode bit high, leaving all other mode bits unchanged. ''' mode_list = self._get_actuator_mode() if actuator is None: mode = mode_list[0] else: actuator = int(actuator) mode = mode_list[actuator] mode |= 1 << bit self._set_actuator_mode(mode,actuator) def _clear_actuator_mode_bit(self,bit,actuator=None): ''' Sets the mode bit low, leaving all other mode bits unchanged. ''' mode_list = self._get_actuator_mode() if actuator is None: mode = mode_list[0] else: actuator = int(actuator) mode = mode_list[actuator] mode &= ~(1 << bit) self._set_actuator_mode(mode,actuator) def disable_potentiometer(self,actuator=None): ''' Disables the potentiometer preventing manual adjustment. ''' self._set_actuator_mode_bit(3,actuator) def enable_potentiometer(self,actuator=None): ''' Enables the potentiometer allowing manual adjustment. ''' self._clear_actuator_mode_bit(3,actuator) def disable_power_led(self,actuator=None): ''' Disables the green power LED. ''' self._set_actuator_mode_bit(14,actuator) def enable_power_led(self,actuator=None): ''' Enables the green power LED. ''' self._clear_actuator_mode_bit(14,actuator) def disable_serial_led(self,actuator=None): ''' Disables the green serial LED. ''' self._set_actuator_mode_bit(15,actuator) def enable_serial_led(self,actuator=None): ''' Enables the green serial LED. ''' self._clear_actuator_mode_bit(15,actuator) def homed(self): ''' Returns home status. ''' mode_list = self._get_actuator_mode() home_status_list = [((1 << 7) & mode) for mode in mode_list] return [bool(home_status) for home_status in home_status_list] def set_home_speed(self,speed,actuator=None): ''' Sets the speed at which the actuator moves when using the "Home" command. ''' self._send_request(41,actuator,speed) def get_home_speed(self): ''' Returns the speed at which the actuator moves when using the "Home" command. ''' actuator = None response = self._return_setting(41,actuator) return response def set_target_speed(self,speed,actuator=None): ''' Sets the speed at which the actuator moves when using "move_absolute" or "move_relative" commands. ''' self._send_request(42,actuator,speed) def get_target_speed(self): ''' Returns the speed at which the actuator moves when using "move_absolute" or "move_relative" commands. ''' actuator = None response = self._return_setting(42,actuator) return response def set_acceleration(self,acceleration,actuator=None): ''' Sets the acceleration used by the movement commands. ''' self._send_request(43,actuator,acceleration) def get_acceleration(self): ''' Returns the acceleration used by the movement commands. ''' actuator = None response = self._return_setting(43,actuator) return response def set_home_offset(self,offset,actuator=None): ''' Sets the the new "Home" position which can then be used when the Home command is issued. ''' self._send_request(47,actuator,offset) def get_home_offset(self): ''' Returns the offset to which the actuator moves when using the "Home" command. ''' actuator = None response = self._return_setting(47,actuator) return response def get_alias(self): ''' Returns the alternate device numbers for the actuators. ''' actuator = None response = self._return_setting(48,actuator) response_corrected = [] for r in response: if r > 0: response_corrected.append(r-1) else: response_corrected.append(None) return response_corrected def set_alias(self,actuator,alias): ''' Sets the alternate device numbers for the actuator. ''' actuator_count = self.get_actuator_count() if (actuator < 0) or (actuator > actuator_count): raise ZaberError('actuator must be between {0} and {1}'.format(0,actuator_count)) if (alias < ALIAS_MIN) or (alias > ALIAS_MAX): raise ZaberError('alias must be between {0} and {1}'.format(ALIAS_MIN,ALIAS_MAX)) self._send_request(48,actuator,alias+1) def remove_alias(self,actuator=None): ''' Removes the alternate device number for the actuator. ''' response = self._send_request_get_response(48,actuator,0) return response def moving(self): ''' Returns True if actuator is moving, False otherwise ''' actuator = None response = self._send_request_get_response(54,actuator) response = [bool(r) for r in response] return response def echo_data(self,data): ''' Echoes back the same Command Data that was sent. ''' actuator = None response = self._send_request_get_response(55,actuator,data) try: response = response[0] except TypeError: pass return response def get_position(self): ''' Returns the current absolute position of the actuator in microsteps. ''' actuator = None response = self._send_request_get_response(60,actuator) return response def set_serial_number(self,serial_number): ''' Sets serial number. Useful for talking communicating with ZaberDevices on multiple serial ports. ''' actuator = None # write data = 1 << 7 address = SERIAL_NUMBER_ADDRESS data += address serial_number = int(serial_number) serial_number = serial_number << 8 data += serial_number self._send_request(35,actuator,data) def get_serial_number(self): ''' Gets serial number. Useful for talking communicating with ZaberDevices on multiple serial ports. ''' actuator = None # read data = 0 << 7 address = SERIAL_NUMBER_ADDRESS data += address response = self._send_request_get_response(35,actuator,data) response = response[0] response = response >> 8 return response def get_zaber_response(self): return self._zaber_response def _map_list(self,x_list,in_min,in_max,out_min,out_max): return [int((x-in_min)*(out_max-out_min)/(in_max-in_min)+out_min) for x in x_list] def _map(self,x,in_min,in_max,out_min,out_max): return int((x-in_min)*(out_max-out_min)/(in_max-in_min)+out_min)
class MightexDevice(object): ''' This Python package (mightex_device) creates a class named MightexDevice, which contains an instance of serial_device2.SerialDevice and adds methods to it to interface to Mightex LED controllers. Example Usage: dev = MightexDevice() # Might automatically find device if one available # if it is not found automatically, specify port directly dev = MightexDevice(port='/dev/ttyUSB0') # Linux dev = MightexDevice(port='/dev/tty.usbmodem262471') # Mac OS X dev = MightexDevice(port='COM3') # Windows dev.get_serial_number() '04-150824-007' dev.get_channel_count() 4 channel = 0 # channel numbering starts at 0, not 1! dev.get_mode(channel) 'disable' dev.set_normal_parameters(channel,1000,30) dev.get_normal_parameters(channel) {'current': 30, 'current_max': 1000} dev.set_mode_normal(channel) dev.get_load_voltage(channel) 2622 dev.set_normal_current(channel,200) dev.get_load_voltage(channel) 3054 dev.set_mode_disable(channel) dev.set_strobe_parameters(channel,100,1) dev.get_strobe_parameters(channel) {'current_max': 100, 'repeat': 1} dev.set_strobe_profile_set_point(channel,0,100,1000000) dev.set_strobe_profile_set_point(channel,1,10,500000) dev.set_strobe_profile_set_point(channel,2,0,0) profile = dev.get_strobe_profile(channel) profile [{'current': 100, 'duration': 1000000}, {'current': 10, 'duration': 500000}, {'current': 0, 'duration': 0}] dev.set_mode_strobe(channel) dev.get_strobe_profile(channel+1) [{'current': 20, 'duration': 1000}, {'current': 10, 'duration': 1000}, {'current': 0, 'duration': 0}] dev.set_strobe_profile(channel+1,profile) dev.get_strobe_profile(channel+1) dev.set_mode_strobe(channel+1) dev.set_trigger_parameters(channel,100,True) dev.get_trigger_parameters(channel) {'current_max': 100, 'falling_edge': True} dev.set_trigger_profile_set_point(channel,0,100,1000000) dev.set_trigger_profile_set_point(channel,1,10,500000) dev.set_trigger_profile_set_point(channel,2,0,0) dev.get_trigger_profile(channel) [{'current': 100, 'duration': 1000000}, {'current': 10, 'duration': 500000}, {'current': 0, 'duration': 0}] dev.set_mode_trigger(channel) dev.reset() dev.get_trigger_profile(channel) [{'current': 20, 'duration': 1000}, {'current': 10, 'duration': 1000}, {'current': 0, 'duration': 0}] dev.set_trigger_profile_set_point(channel,0,100,1000000) dev.set_trigger_profile_set_point(channel,1,10,500000) dev.set_trigger_profile_set_point(channel,2,0,0) dev.store_parameters() dev.reset() dev.get_trigger_profile(channel) [{'current': 100, 'duration': 1000000}, {'current': 10, 'duration': 500000}, {'current': 0, 'duration': 0}] dev.restore_factory_defaults() dev.store_parameters() dev.reset() dev.get_trigger_profile(channel) [{'current': 10, 'duration': 20}, {'current': 0, 'duration': 0}] ''' _TIMEOUT = 0.05 _WRITE_WRITE_DELAY = 0.05 _RESET_DELAY = 2.0 def __init__(self,*args,**kwargs): if 'debug' in kwargs: self.debug = kwargs['debug'] else: kwargs.update({'debug': DEBUG}) self.debug = DEBUG if 'try_ports' in kwargs: try_ports = kwargs.pop('try_ports') else: try_ports = None if 'baudrate' not in kwargs: kwargs.update({'baudrate': BAUDRATE}) elif (kwargs['baudrate'] is None) or (str(kwargs['baudrate']).lower() == 'default'): kwargs.update({'baudrate': BAUDRATE}) if 'timeout' not in kwargs: kwargs.update({'timeout': self._TIMEOUT}) if 'write_write_delay' not in kwargs: kwargs.update({'write_write_delay': self._WRITE_WRITE_DELAY}) if ('port' not in kwargs) or (kwargs['port'] is None): port = find_mightex_device_port(baudrate=kwargs['baudrate'], try_ports=try_ports, debug=kwargs['debug']) kwargs.update({'port': port}) t_start = time.time() self._serial_device = SerialDevice(*args,**kwargs) atexit.register(self._exit_mightex_device) self._lock = threading.Lock() self._strict_error = False self._channel_count = -1 time.sleep(self._RESET_DELAY) t_end = time.time() self._debug_print('Initialization time =', (t_end - t_start)) def _debug_print(self, *args): if self.debug: print(*args) def _exit_mightex_device(self): pass def _args_to_request(self,*args): request = ' '.join(map(str,args)) request = request + '\n\r'; return request def _send_request(self,*args): ''' Sends request to device over serial port and returns number of bytes written. ''' lock_acquired = self._lock.acquire() try: request = self._args_to_request(*args) self._debug_print('request', request) bytes_written = self._serial_device.write_check_freq(request,delay_write=True) finally: self._lock.release() return bytes_written def _send_request_get_response(self,*args): ''' Sends request to device over serial port and returns response. ''' lock_acquired = self._lock.acquire() try: request = self._args_to_request(*args) self._debug_print('request', request) response = self._serial_device.write_read(request,use_readline=True,check_write_freq=False,delay_write=True) finally: self._lock.release() response = response.strip() if '#!' in response: if self._strict_error: raise MightexError('The command is valid and executed, but an error occurred during execution.') elif '#?' in response: raise MightexError('The latest command is a valid command but the argument is NOT in valid range.') response = response.replace('#','') return response def close(self): ''' Close the device serial port. ''' self._serial_device.close() def get_port(self): return self._serial_device.port def set_strict_error(self,strict_error): self._strict_error = strict_error def get_device_info(self): ''' Get device_info. ''' request = self._args_to_request('DEVICEINFO') self._debug_print('request', request) response = self._send_request_get_response(request) if 'Mightex' not in response: # try again just in case response = self._send_request_get_response(request) if 'Mightex' not in response: raise MightexError('"Mightex" not in device_info.') return response def get_serial_number(self): ''' Get serial_number. ''' device_info = self.get_device_info() serial_number_str = 'Serial No.:' p = re.compile(serial_number_str+'\d+-?\d+-?\d+') found_list = p.findall(device_info) if len(found_list) != 1: raise MightexError('serial_number not found in device_info.') else: serial_number = found_list[0] serial_number = serial_number.replace(serial_number_str,'') return serial_number def get_mode(self,channel): ''' Get channel mode. Modes = ['DISABLE','NORMAL','STROBE','TRIGGER'] ''' channel = int(channel) + 1 request = self._args_to_request('?MODE',channel) self._debug_print('request', request) response = self._send_request_get_response(request) if response == '0': return 'DISABLE' elif response == '1': return 'NORMAL' elif response == '2': return 'STROBE' elif response == '3': return 'TRIGGER' else: raise MightexError('Unknown response: {0}'.format(response)) def get_all_modes(self): ''' Get all channel modes. ''' modes = [] for channel in range(self.get_channel_count()): mode = self.get_mode(channel) modes.append(mode) return modes def get_channel_count(self): ''' Get channel count. ''' if self._channel_count > 0: return self._channel_count channel_count = 0 while True: try: channel_count += 1 mode = self.get_mode(channel_count) except MightexError: break channel_count -= 1 self._channel_count = channel_count return channel_count def set_mode_disable(self,channel): ''' Set DISABLE mode. ''' channel = int(channel) + 1 request = self._args_to_request('MODE',channel,0) self._debug_print('request', request) self._send_request_get_response(request) def set_all_mode_disable(self): ''' Set DISABLE mode for all channels. ''' for channel in range(self.get_channel_count()): self.set_mode_disable(channel) def set_mode_normal(self,channel): ''' Set NORMAL mode. ''' channel = int(channel) + 1 request = self._args_to_request('MODE',channel,1) self._debug_print('request', request) self._send_request_get_response(request) def set_all_mode_normal(self): ''' Set NORMAL mode for all channels. ''' for channel in range(self.get_channel_count()): self.set_mode_normal(channel) def set_mode_strobe(self,channel): ''' Set STROBE mode. ''' channel = int(channel) + 1 request = self._args_to_request('MODE',channel,2) self._debug_print('request', request) self._send_request_get_response(request) def set_all_mode_strobe(self): ''' Set STROBE mode for all channels. ''' for channel in range(self.get_channel_count()): self.set_mode_strobe(channel) def set_mode_trigger(self,channel): ''' Set TRIGGER mode. ''' channel = int(channel) + 1 request = self._args_to_request('MODE',channel,3) self._debug_print('request', request) self._send_request_get_response(request) def set_all_mode_trigger(self): ''' Set TRIGGER mode for all channels. ''' for channel in range(self.get_channel_count()): self.set_mode_trigger(channel) def set_normal_parameters(self,channel,current_max,current): ''' Set NORMAL mode parameters. current_max is the maximum current allowed for NORMAL mode, in mA. current is the working current for NORMAL mode, in mA. ''' channel = int(channel) + 1 current_max = int(current_max) current = int(current) request = self._args_to_request('NORMAL',channel,current_max,current) self._debug_print('request', request) self._send_request_get_response(request) def set_all_normal_parameters(self,current_max,current): ''' Set normal parameters for all channels. ''' for channel in range(self.get_channel_count()): self.set_normal_parameters(channel,current_max,current) def set_normal_current(self,channel,current): ''' Set the working current for NORMAL mode, in mA. ''' channel = int(channel) + 1 current = int(current) request = self._args_to_request('CURRENT',channel,current) self._debug_print('request', request) self._send_request_get_response(request) def set_all_normal_current(self,current): ''' Set normal current for all channels. ''' for channel in range(self.get_channel_count()): self.set_normal_current(channel,current) def get_normal_parameters(self,channel): ''' Get NORMAL mode parameters. current_max is the maximum current allowed for NORMAL mode, in mA. current is the working current for NORMAL mode, in mA. ''' channel = int(channel) + 1 request = self._args_to_request('?CURRENT',channel) self._debug_print('request', request) response = self._send_request_get_response(request) response_list = response.split(' ') parameters = {} parameters['current_max'] = int(response_list[-2]) parameters['current'] = int(response_list[-1]) return parameters def get_all_normal_parameters(self): ''' get normal parameters for all channels. ''' parameters_list = [] for channel in range(self.get_channel_count()): parameters = self.get_normal_parameters(channel) parameters_list.append(parameters) return parameters_list def set_strobe_parameters(self,channel,current_max,repeat): ''' Set STROBE mode parameters. current_max is the maximum current allowed for STROBE mode, in mA. repeat is the Repeat Count for running the profile. It can be from 0 to 99999999. And the number 9999 is special, it means repeat forever. Note that when it is 0, the programmed wave form will output once, when it is 1, the wave form will be repeated once, which will be output twice and so on. ''' channel = int(channel) + 1 current_max = int(current_max) repeat = int(repeat) request = self._args_to_request('STROBE',channel,current_max,repeat) self._debug_print('request', request) self._send_request_get_response(request) def set_all_strobe_parameters(self,current_max,repeat): ''' Set strobe parameters for all channels. ''' for channel in range(self.get_channel_count()): self.set_strobe_parameters(channel,current_max,current) def set_strobe_profile_set_point(self,channel,set_point,current,duration): ''' Each channel has a programmable profile for STROBE mode. The profile contains 128 set_point values (0-127), and each set_point has current(mA)/duration(us) pair. A ZERO/ZERO pair means it is the end of the profile. If user does not program the profile for a certain channel, the default is all Zero/Zero pairs, which means the channel is always OFF. Use this method over and over to set a customized profile and then enter STROBE mode with the set_mode_strobe method. The profile will be executed (repeatedly) after device enters (or reenters) the STROBE mode. ''' channel = int(channel) + 1 set_point = int(set_point) current = int(current) duration = int(duration) request = self._args_to_request('STRP',channel,set_point,current,duration) self._debug_print('request', request) self._send_request_get_response(request) def get_strobe_parameters(self,channel): ''' Get STROBE mode parameters. current_max is the maximum current allowed for STROBE mode, in mA. repeat is the Repeat Count for running the profile. It can be from 0 to 99999999. And the number 9999 is special, it means repeat forever. Note that when it is 0, the programmed wave form will output once, when it is 1, the wave form will be repeated once, which will be output twice and so on. ''' channel = int(channel) + 1 request = self._args_to_request('?STROBE',channel) self._debug_print('request', request) response = self._send_request_get_response(request) response_list = response.split(' ') parameters = {} parameters['current_max'] = int(response_list[0]) parameters['repeat'] = int(response_list[1]) return parameters def get_all_strobe_parameters(self): ''' get strobe parameters for all channels. ''' parameters_list = [] for channel in range(self.get_channel_count()): parameters = self.get_strobe_parameters(channel) parameters_list.append(parameters) return parameters_list def get_strobe_profile(self,channel): ''' Each channel has a programmable profile for STROBE mode. The profile contains 128 set_point values (0-127), and each set_point has current(mA)/duration(us) pair. A ZERO/ZERO pair means it is the end of the profile. If user does not program the profile for a certain channel, the default is all Zero/Zero pairs, which means the channel is always OFF. ''' request = self._args_to_request('?STRP',channel) self._debug_print('request', request) self._send_request_get_response(request) current = None duration = None profile = [] while not ((current == 0) and (duration == 0)): response = self._serial_device.readline() response = response.strip() response = response.replace('#','') response_list = response.split(' ') self._debug_print('strobe_profile set_point',response_list) try: current = int(response_list[0]) duration = int(response_list[1]) profile_set_point = {} profile_set_point['current'] = current profile_set_point['duration'] = duration profile.append(profile_set_point) except: pass return profile def set_strobe_profile(self,channel,profile): ''' Each channel has a programmable profile for STROBE mode. The profile contains up to 128 set_point values (0-127), and each set_point has current(mA)/duration(us) pair. profile example: [{'current': 100, 'duration': 1000000}, {'current': 10, 'duration': 500000}, {'current': 0, 'duration': 0}] ''' set_point = 0 for set_point_parameters in profile: self.set_strobe_profile_set_point(channel,set_point,**set_point_parameters) set_point += 1 def set_trigger_parameters(self,channel,current_max,falling_edge): ''' Set TRIGGER mode parameters. current_max is the maximum current allowed for TRIGGER mode, in mA. When falling_edge is True, the falling edge of external trigger signal asserts. When falling_edge is False, the rising edge of the external trigger signal asserts. ''' channel = int(channel) + 1 current_max = int(current_max) polarity = int(bool(falling_edge)) request = self._args_to_request('TRIGGER',channel,current_max,polarity) self._debug_print('request', request) self._send_request_get_response(request) def set_all_trigger_parameters(self,current_max,falling_edge): ''' Set trigger parameters for all channels. ''' for channel in range(self.get_channel_count()): self.set_trigger_parameters(channel,current_max,falling_edge) def set_trigger_profile_set_point(self,channel,set_point,current,duration): ''' Each channel has a programmable profile for TRIGGER mode. The profile contains 128 set_point values (0-127), and each set_point has current(mA)/duration(us) pair. A ZERO/ZERO pair means it is the end of the profile. If user does not program the profile for a certain channel, the default is all Zero/Zero pairs, which means the channel is always OFF. Use this method over and over to set a customized profile and then enter TRIGGER mode with the set_mode_trigger method. The profile will be executed while an external trigger occurs and the device is in TRIGGER mode. ''' channel = int(channel) + 1 set_point = int(set_point) current = int(current) duration = int(duration) request = self._args_to_request('TRIGP',channel,set_point,current,duration) self._debug_print('request', request) self._send_request_get_response(request) def get_trigger_parameters(self,channel): ''' Get TRIGGER mode parameters. current_max is the maximum current allowed for TRIGGER mode, in mA. When falling_edge is True, the falling edge of external trigger signal asserts. When falling_edge is False, the rising edge of the external trigger signal asserts. ''' channel = int(channel) + 1 request = self._args_to_request('?TRIGGER',channel) self._debug_print('request', request) response = self._send_request_get_response(request) response_list = response.split(' ') parameters = {} parameters['current_max'] = int(response_list[0]) parameters['falling_edge'] = bool(int(response_list[1])) return parameters def get_all_trigger_parameters(self): ''' get trigger parameters for all channels. ''' parameters_list = [] for channel in range(self.get_channel_count()): parameters = self.get_trigger_parameters(channel) parameters_list.append(parameters) return parameters_list def get_trigger_profile(self,channel): ''' Each channel has a programmable profile for TRIGGER mode. The profile contains 128 set_point values (0-127), and each set_point has current(mA)/duration(us) pair. A ZERO/ZERO pair means it is the end of the profile. If user does not program the profile for a certain channel, the default is all Zero/Zero pairs, which means the channel is always OFF. ''' request = self._args_to_request('?TRIGP',channel) self._debug_print('request', request) self._send_request_get_response(request) current = None duration = None profile = [] while not ((current == 0) and (duration == 0)): response = self._serial_device.readline() response = response.strip() response = response.replace('#','') response_list = response.split(' ') self._debug_print('trigger_profile set_point',response_list) try: current = int(response_list[0]) duration = int(response_list[1]) profile_set_point = {} profile_set_point['current'] = current profile_set_point['duration'] = duration profile.append(profile_set_point) except: pass return profile def get_load_voltage(self,channel): ''' For XV Module (e.g. AV04 or SV04), use this method to get the voltage on the load of the specified channel. It will return the voltage in mV. Note: As the controller polls the load voltage in a 20ms interval, this feature is proper for NORMAL mode or slow STROBE mode only. ''' channel = int(channel) + 1 request = self._args_to_request('LoadVoltage',channel) self._debug_print('request', request) response = self._send_request_get_response(request) channel_str = "{0}:".format(channel) response = response.replace(channel_str,'') response = int(response) return response def get_all_load_voltages(self): ''' get load voltages for all channels. ''' voltages = [] for channel in range(self.get_channel_count()): voltage = self.get_load_voltage(channel) voltages.append(parameters) return voltages def reset(self): ''' Soft reset device. ''' request = self._args_to_request('Reset') self._debug_print('request', request) self._send_request_get_response(request) def restore_factory_defaults(self): ''' This method will reset the device mode and all related parameters to the factory defaults. Note that these parameters become the current device settings in volatile memory, use the "store_parameters" method to save the current settings to non-volatile memory. ''' request = self._args_to_request('RESTOREDEF') self._debug_print('request', request) self._send_request_get_response(request) def store_parameters(self): ''' This method will store the current settings in volatile memory to non-volatile memory. ''' request = self._args_to_request('STORE') self._debug_print('request', request) self._send_request_get_response(request)
class BioshakeDevice(object): ''' This Python package (bioshake_device) creates a class named BioshakeDevice, which contains an instance of serial_device2.SerialDevice and adds methods to it to interface to Q.instruments BioShake devices. Example Usage: dev = BioshakeDevice() # Might automatically find device if one available # if it is not found automatically, specify port directly dev = BioshakeDevice(port='/dev/ttyUSB0') # Linux dev = BioshakeDevice(port='/dev/tty.usbmodem262471') # Mac OS X dev = BioshakeDevice(port='COM3') # Windows dev.get_description() dev.shake_on(speed_target=1000) # speed_target (rpm) dev.get_shake_actual_speed() dev.shake_off() dev.temp_on(temp_target=45) # temp_target (°C) dev.get_temp_actual() dev.temp_off() ''' _TIMEOUT = 0.05 _WRITE_WRITE_DELAY = 0.05 _RESET_DELAY = 2.0 _DEFAULT_SPEED_TARGET = 1000 _SHAKE_STATE_DESCRIPTIONS = { 0: 'Shaking is active', 1: 'Shaker has a stop command detect', 2: 'Shaker in the braking mode', 3: 'Arrived in the home position', 4: 'Manual mode', 5: 'Acceleration', 6: 'Deceleration', 7: 'Deceleration with stopping', 90: 'ECO mode', 99: 'Boot process running', -1: '', } _ELM_STATE_DESCRIPTIONS = { 1: 'Microplate is locked', 3: 'Microplate is unlocked', 9: 'Error', -1: '', } def __init__(self,*args,**kwargs): if 'debug' in kwargs: self.debug = kwargs['debug'] else: kwargs.update({'debug': DEBUG}) self.debug = DEBUG if 'try_ports' in kwargs: try_ports = kwargs.pop('try_ports') else: try_ports = None if 'baudrate' not in kwargs: kwargs.update({'baudrate': BAUDRATE}) elif (kwargs['baudrate'] is None) or (str(kwargs['baudrate']).lower() == 'default'): kwargs.update({'baudrate': BAUDRATE}) if 'timeout' not in kwargs: kwargs.update({'timeout': self._TIMEOUT}) if 'write_write_delay' not in kwargs: kwargs.update({'write_write_delay': self._WRITE_WRITE_DELAY}) if ('port' not in kwargs) or (kwargs['port'] is None): port = find_bioshake_device_port(baudrate=kwargs['baudrate'], try_ports=try_ports, debug=kwargs['debug']) kwargs.update({'port': port}) t_start = time.time() self._serial_device = SerialDevice(*args,**kwargs) atexit.register(self._exit_bioshake_device) time.sleep(self._RESET_DELAY) t_end = time.time() self._debug_print('Initialization time =', (t_end - t_start)) def _debug_print(self, *args): if self.debug: print(*args) def _exit_bioshake_device(self): pass def _args_to_request(self,*args): request = ''.join(map(str,args)) request = request + '\r'; return request def _send_request(self,*args): '''Sends request to bioshake device over serial port and returns number of bytes written''' request = self._args_to_request(*args) self._debug_print('request', request) bytes_written = self._serial_device.write_check_freq(request,delay_write=True) return bytes_written def _send_request_get_response(self,*args): '''Sends request to device over serial port and returns response''' request = self._args_to_request(*args) self._debug_print('request', request) response = self._serial_device.write_read(request,use_readline=True,check_write_freq=True) response = response.strip() if (response == 'e'): request = request.strip() raise BioshakeError(request) return response def close(self): ''' Close the device serial port. ''' self._serial_device.close() def get_port(self): return self._serial_device.port def info(self): ''' Listing of general information. ''' return self._send_request_get_response('info') def get_version(self): ''' Send back the current version number. ''' return self._send_request_get_response('getVersion') def get_description(self): ''' Send back the current model information. ''' return self._send_request_get_response('getDescription') def reset_device(self): ''' Restart the controller. ''' return self._send_request_get_response('resetDevice') def get_error_list(self): ''' Return a semicolon separated list with warnings and errors that occurred. ''' return self._send_request_get_response('getErrorList') def set_eco_mode(self): ''' Switch the shaker into economical mode. It will reduce electricity consumption by switching off the solenoid for the home position and the deactivation of the ELM function. Warning: No home position!!! ELM is in locked position!!! ''' return self._send_request_get_response('setEcoMode') def leave_eco_mode(self): ''' Leave the economical mode and change in the normal operating state with finding the home position. ''' return self._send_request_get_response('leaveEcoMode') def shake_on(self,speed_target=_DEFAULT_SPEED_TARGET): ''' Start the shaking at the target speed (rpm) or with the default speed if no speed_target provided. ''' response = self._set_shake_speed_target(speed_target) if response is not None: return self._send_request_get_response('shakeOn') def shake_on_with_runtime(self,runtime,speed_target=_DEFAULT_SPEED_TARGET): ''' Shake for runtime duration (s) at the speed_target (rpm) or with the default speed_target if none provided. Allowable runtime range: 0 – 99999 seconds. ''' response = self._set_shake_speed_target(speed_target) if response is not None: return self._send_request_get_response('shakeOnWithRuntime'+str(int(runtime))) def get_shake_remaining_time(self): ''' Return the remaining time in seconds. ''' return int(float(self._send_request_get_response('getShakeRemainingTime'))) def shake_off(self): ''' Stop the shaking and return to the home position. ''' return self._send_request_get_response('shakeOff') def shake_emergency_off(self): ''' High-Speed stop for the shaking. Warning: No defined home position !!! ''' return self._send_request_get_response('shakeEmergencyOff') def shake_go_home(self): ''' Shaker goes to the home position and lock in. ''' return self._send_request_get_response('shakeGoHome') def get_shake_state(self): ''' Return the state of shaking. ''' shake_state_value = self._send_request_get_response('getShakeState') if len(shake_state_value) > 0: shake_state_value = int(shake_state_value) else: shake_state_value = -1 return {'value': shake_state_value, 'description': self._SHAKE_STATE_DESCRIPTIONS[shake_state_value]} def get_shake_speed_target(self): ''' Return the target mixing speed. (rpm) ''' return int(float(self._send_request_get_response('getShakeTargetSpeed'))) def _set_shake_speed_target(self,speed_target=_DEFAULT_SPEED_TARGET): ''' Set the target mixing speed. Allowable range: 200 – 3000 rpm ''' if (speed_target >= 200) and (speed_target <= 3000): return self._send_request_get_response('setShakeTargetSpeed'+str(int(speed_target))) else: print(self._set_shake_speed_target.__doc__) def get_default_shake_speed_target(self): ''' Get the default mixing speed. (rpm) ''' return self._DEFAULT_SPEED_TARGET def get_shake_speed_actual(self): ''' Return the current mixing speed. (rpm) ''' return int(float(self._send_request_get_response('getShakeActualSpeed'))) def get_shake_speed_min(self): ''' Return the least shake_speed set point. ''' return int(float(self._send_request_get_response('getShakeMinRpm'))) def get_shake_speed_max(self): ''' Return the biggest shake_speed set point. ''' return int(float(self._send_request_get_response('getShakeMaxRpm'))) def get_shake_acceleration(self): ''' Return the acceleration/deceleration value. (seconds) ''' return int(float(self._send_request_get_response('getShakeAcceleration'))) def set_shake_acceleration(self,acceleration): ''' Set the acceleration/deceleration value in seconds. Allowable range: 0 - 10 seconds ''' if (acceleration >= 0) and (acceleration <= 10): return self._send_request_get_response('setShakeAcceleration'+str(acceleration)) else: print(self.set_shake_acceleration.__doc__) def temp_on(self,temp_target): ''' Activate the temperature control. temp_target allowed range: 0 – 99.0 (°C) ''' response = self._set_temp_target(temp_target) if response is not None: return self._send_request_get_response('tempOn') def temp_off(self): ''' Deactivate the temperature control. ''' return self._send_request_get_response('tempOff') def get_temp_target(self): ''' Return the target temperature. (°C) ''' return float(self._send_request_get_response('getTempTarget')) def _set_temp_target(self,temp_target): ''' Set the target temperature in °C allowed range: 0 – 99.0 in °C ''' if (temp_target >= 0) and (temp_target <= 99): return self._send_request_get_response('setTempTarget'+str(int(round(temp_target*10)))) else: print(self.set_temp_target.__doc__) def get_temp_actual(self): ''' Return the actual temperature. (°C) ''' return float(self._send_request_get_response('getTempActual')) def get_temp_min(self): ''' Return the least set point of temperature. (°C) ''' return float(self._send_request_get_response('getTempMin')) def get_temp_max(self): ''' Return the biggest set point of temperature. (°C) ''' return float(self._send_request_get_response('getTempMax')) def set_elm_lock_pos(self): ''' Close the Edge Locking Mechanism (ELM). The microplate is locked. ''' return self._send_request_get_response('setElmLockPos') def set_elm_unlock_pos(self): ''' Opens the Edge Locking Mechanism (ELM) for gripping of microplates. The microplate is not locked. ''' return self._send_request_get_response('setElmUnlockPos') def get_elm_state(self): ''' Return the state of Edge Locking Mechanism (ELM). ''' elm_state_value = self._send_request_get_response('getElmState') if len(elm_state_value) > 0: elm_state_value = int(elm_state_value) else: elm_state_value = -1 return {'value': elm_state_value, 'description': self._ELM_STATE_DESCRIPTIONS[elm_state_value]}
class SleepAssay(object): ''' ''' def __init__(self,config_file_path,quick_test=False,no_hardware=False,*args,**kwargs): self._TIMEOUT = 0.05 self._WRITE_WRITE_DELAY = 0.05 self._RESET_DELAY = 2.0 self._RELAY_COUNT = 8 self._CAMERA_TRIGGER_DUTY_CYCLE = 50 self._BOARD_INDICATOR_LIGHT_FREQUENCY = 2 self._BOARD_INDICATOR_LIGHT_DUTY_CYCLE = 50 self._quick_test = quick_test if not self._quick_test: self._MILLISECONDS_PER_SECOND = 1000 else: self._MILLISECONDS_PER_SECOND = 1000/3600 self._SECONDS_PER_MINUTE = 60 self._MINUTES_PER_HOUR = 60 self._HOURS_PER_DAY = 24 self._MILLISECONDS_PER_HOUR = self._MILLISECONDS_PER_SECOND*self._SECONDS_PER_MINUTE*self._MINUTES_PER_HOUR self._MILLISECONDS_PER_DAY = self._MILLISECONDS_PER_HOUR*self._HOURS_PER_DAY self._METHOD_ID_START_PWM = 0 self._METHOD_ID_STOP_ALL_PULSES = 1 self._METHOD_ID_GET_POWER = 2 self._METHOD_ID_GET_PWM_STATUS = 3 self._PWM_STOPPED = 0 self._PWM_RUNNING = 1 self._POWER_MAX = 255 self._config_file_path = os.path.abspath(config_file_path) if 'debug' in kwargs: self.debug = kwargs['debug'] else: # kwargs.update({'debug': DEBUG}) self.debug = DEBUG if 'baudrate' not in kwargs: kwargs.update({'baudrate': BAUDRATE}) elif (kwargs['baudrate'] is None) or (str(kwargs['baudrate']).lower() == 'default'): kwargs.update({'baudrate': BAUDRATE}) if 'timeout' not in kwargs: kwargs.update({'timeout': self._TIMEOUT}) if 'write_write_delay' not in kwargs: kwargs.update({'write_write_delay': self._WRITE_WRITE_DELAY}) with open(config_file_path,'r') as config_stream: self._config = yaml.load(config_stream) os_type = platform.system() if os_type == 'Linux': try: kwargs['port'] = self._config['relay_board_serial_port']['linux'] except KeyError: raise RuntimeError('Must specify linux serial port in config file!') elif os_type == 'Windows': try: kwargs['port'] = self._config['relay_board_serial_port']['windows'] except KeyError: raise RuntimeError('Must specify windows serial port in config file!') elif os_type == 'Darwin': try: kwargs['port'] = self._config['relay_board_serial_port']['osx'] except KeyError: raise RuntimeError('Must specify osx serial port in config file!') t_start = time.time() self._no_hardware = no_hardware if not self._no_hardware: self._serial_device = SerialDevice(*args,**kwargs) atexit.register(self._exit_sleep_assay) time.sleep(self._RESET_DELAY) self._csv_file_path = None self._csv_file = None self._csv_writer = None self._video_frame = -1 self._state = 'initialization' self._header = ['video_frame', 'date_time', 'state', 'white_light_power', 'red_light_pwm_status', 'red_light_power'] t_end = time.time() self._debug_print('Initialization time =', (t_end - t_start)) def _debug_print(self, *args): if self.debug: print(*args) def _exit_sleep_assay(self): self.stop() def _flatten(self,l): out = [] for item in l: if isinstance(item, (list, tuple)): out.extend(self._flatten(item)) else: out.append(item) return out def _args_to_request(self,*args): args = self._flatten(args) request = ['[', ','.join(map(str,args)), ']'] request = ''.join(request) request = request + '\n'; return request def _send_request(self,*args): '''Sends request to device over serial port and returns number of bytes written''' request = self._args_to_request(*args) self._debug_print('request', request) bytes_written = 0 if not self._no_hardware: bytes_written = self._serial_device.write_check_freq(request, delay_write=True) return bytes_written def _send_request_get_result(self,*args): ''' Sends request to server over serial port and returns response result ''' request = self._args_to_request(*args) self._debug_print('request', request) successful = False result = None while (not successful) and (not self._no_hardware): try: response = self._serial_device.write_read(request,use_readline=True,check_write_freq=False) self._debug_print('response', response) result = json.loads(response) successful = True except: print('Error!','\nrequest:',request) time.sleep(0.5) return result def _close(self): ''' Close the device serial port. ''' if not self._no_hardware: self._serial_device.close() def _get_port(self): if not self._no_hardware: return self._serial_device.port else: return None def _start_pwm(self, relay, power, delay, count, pwm_level_count, periods, on_durations): ''' ''' if relay < 0: relay = 0 elif relay > (self._RELAY_COUNT - 1): relay = self._RELAY_COUNT power = int(power) delay = int(delay) count = int(count) for pwm_level in range(pwm_level_count): periods[pwm_level] = int(periods[pwm_level]) on_durations[pwm_level] = int(on_durations[pwm_level]) pwm_info = zip(periods,on_durations) self._send_request(self._METHOD_ID_START_PWM, relay, power, delay, count, pwm_level_count, *pwm_info) def _stop_all_pulses(self): ''' ''' self._send_request(self._METHOD_ID_STOP_ALL_PULSES) def _get_power(self): ''' ''' result = self._send_request_get_result(self._METHOD_ID_GET_POWER) return result def _get_pwm_status(self): ''' ''' result = self._send_request_get_result(self._METHOD_ID_GET_PWM_STATUS) return result def _print_datetime(self,dt): print(' {0}-{1}-{2}-{3}-{4}-{5}'.format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)) def _start_to_start_datetime(self,start): now_datetime = datetime.datetime.now() offset = datetime.timedelta(start['offset_days']) offset_datetime = now_datetime + offset start_datetime = datetime.datetime(offset_datetime.year, offset_datetime.month, offset_datetime.day, start['hour'], start['minute']) delta = start_datetime - now_datetime delay = int(1000*delta.total_seconds()) if delay < 0: start_datetime = now_datetime return start_datetime def _start_datetime_to_delay(self,start_datetime): now_datetime = datetime.datetime.now() delta = start_datetime - now_datetime delay = int(1000*delta.total_seconds()) if delay < 0: delay = 0 return delay def _writerow(self,columns): if self._csv_writer is not None: utf8row = [] for col in columns: utf8row.append(str(col).encode('utf8')) self._csv_writer.writerow(utf8row) def _get_date_str(self): today = datetime.date.today() date_str = "{year}-{month}-{day}".format(year=today.year, month=today.month, day=today.day) return date_str def _get_time_str(self): localtime = time.localtime() time_str = "{hour}-{min}-{sec}".format(hour=localtime.tm_hour, min=localtime.tm_min, sec=localtime.tm_sec) return time_str def _get_date_time_str(self): date_str = self._get_date_str() time_str = self._get_time_str() return date_str + '-' + time_str def start_board_indicator_light_cycle(self,relay): period = 1000/self._BOARD_INDICATOR_LIGHT_FREQUENCY on_duration = (self._BOARD_INDICATOR_LIGHT_DUTY_CYCLE/100)*period self._start_pwm(relay, self._POWER_MAX, 0, -1, 1, [period], [on_duration]) def start_camera_trigger(self, relay, frame_rate, delay): print('start_camera_trigger:') print(' relay = {0}'.format(relay)) print(' frame_rate = {0}'.format(frame_rate)) period = 1000/frame_rate on_duration = (self._CAMERA_TRIGGER_DUTY_CYCLE/100)*period self._start_pwm(relay, self._POWER_MAX, delay, -1, 1, [period], [on_duration]) def start_data_writer(self): if self._csv_writer is None: user_home_dir = os.path.expanduser('~') date_str = self._get_date_str() output_dir = os.path.join(user_home_dir,'sleep_assay_data',date_str) if not os.path.exists(output_dir): os.makedirs(output_dir) date_time_str = self._get_date_time_str() self._csv_file_path = os.path.join(output_dir,date_time_str + '-data.txt') self._csv_file = open(self._csv_file_path, 'w') # Create a new csv writer object to use as the output formatter self._csv_writer = csv.writer(self._csv_file,quotechar='\"',quoting=csv.QUOTE_MINIMAL) self._writerow(self._header) return self._csv_file_path def stop(self): self._stop_all_pulses() def _duration_days_to_duration_datetime(self,duration_days): duration_datetime = datetime.timedelta(duration_days/(1000/self._MILLISECONDS_PER_SECOND)) return duration_datetime def start_entrainment(self,start_datetime,config): print('entrainment:') print(' start:') self._print_datetime(start_datetime) duration_days = config['duration_days'] if duration_days <= 0: end_datetime = start_datetime print(' end:') self._print_datetime(end_datetime) return end_datetime relay = self._config['relays']['white_light'] power = config['white_light']['power'] pwm0_on_duration_hours = config['white_light']['pwm0_on_duration_hours'] pwm0_off_duration_hours = config['white_light']['pwm0_off_duration_hours'] pwm0_period_hours = pwm0_on_duration_hours + pwm0_off_duration_hours pwm0_period = pwm0_period_hours*self._MILLISECONDS_PER_HOUR pwm0_on_duration = pwm0_on_duration_hours*self._MILLISECONDS_PER_HOUR pwm0_period_days = pwm0_period_hours/self._HOURS_PER_DAY count = duration_days/pwm0_period_days delay = self._start_datetime_to_delay(start_datetime) self._start_pwm(relay, power, delay, count, 1, [pwm0_period], [pwm0_on_duration]) duration_datetime = self._duration_days_to_duration_datetime(duration_days) end_datetime = start_datetime + duration_datetime print(' end:') self._print_datetime(end_datetime) return end_datetime def start_experiment_run(self,run,start_datetime,config): print('experiment run {0}:'.format(run)) print(' start:') self._print_datetime(start_datetime) duration_days = config['duration_days'] if duration_days <= 0: end_datetime = start_datetime print(' end:') self._print_datetime(end_datetime) return end_datetime # white light if 'white_light' in config: relay = self._config['relays']['white_light'] power = config['white_light']['power'] pwm0_on_duration_hours = config['white_light']['pwm0_on_duration_hours'] pwm0_off_duration_hours = config['white_light']['pwm0_off_duration_hours'] pwm0_period = (pwm0_on_duration_hours + pwm0_off_duration_hours)*self._MILLISECONDS_PER_HOUR pwm0_on_duration = pwm0_on_duration_hours*self._MILLISECONDS_PER_HOUR delay_days = 0 if 'delay_days' in config['white_light']: delay_days = config['white_light']['delay_days'] duration_datetime = self._duration_days_to_duration_datetime(delay_days) white_start_datetime = start_datetime + duration_datetime delay = self._start_datetime_to_delay(white_start_datetime) if ('pwm1_on_duration_days' in config['white_light']) and ('pwm1_off_duration_days' in config['white_light']): pwm1_on_duration_days = config['white_light']['pwm1_on_duration_days'] pwm1_off_duration_days = config['white_light']['pwm1_off_duration_days'] pwm1_period_days = pwm1_on_duration_days + pwm1_off_duration_days pwm1_period = pwm1_period_days*self._MILLISECONDS_PER_DAY pwm1_on_duration = pwm1_on_duration_days*self._MILLISECONDS_PER_DAY count = math.ceil((duration_days - delay_days)/pwm1_period_days) self._start_pwm(relay, power, delay, count, 2, [pwm0_period,pwm1_period], [pwm0_on_duration,pwm1_on_duration]) else: pwm0_period_days = (pwm0_on_duration_hours + pwm0_off_duration_hours)/self._HOURS_PER_DAY count = math.ceil((duration_days - delay_days)/pwm0_period_days) self._start_pwm(relay, power, delay, count, 1, [pwm0_period], [pwm0_on_duration]) # red light if 'red_light' in config: relay = self._config['relays']['red_light'] power = config['red_light']['power'] pwm0_frequency = config['red_light']['pwm0_frequency_hz'] pwm0_duty_cycle = config['red_light']['pwm0_duty_cycle_percent'] pwm0_period = 1000/pwm0_frequency pwm0_on_duration = (pwm0_duty_cycle/100)*pwm0_period pwm1_on_duration_hours = config['red_light']['pwm1_on_duration_hours'] pwm1_off_duration_hours = config['red_light']['pwm1_off_duration_hours'] pwm1_period_hours = pwm1_on_duration_hours + pwm1_off_duration_hours pwm1_period = pwm1_period_hours*self._MILLISECONDS_PER_HOUR pwm1_on_duration = pwm1_on_duration_hours*self._MILLISECONDS_PER_HOUR pwm1_period_days = pwm1_period_hours/self._HOURS_PER_DAY delay_days = 0 if 'delay_days' in config['red_light']: delay_days = config['red_light']['delay_days'] count = math.ceil((duration_days - delay_days)/pwm1_period_days) duration_datetime = self._duration_days_to_duration_datetime(delay_days) red_start_datetime = start_datetime + duration_datetime delay = self._start_datetime_to_delay(red_start_datetime) self._start_pwm(relay, power, delay, count, 2, [pwm0_period,pwm1_period], [pwm0_on_duration,pwm1_on_duration]) duration_datetime = self._duration_days_to_duration_datetime(duration_days) end_datetime = start_datetime + duration_datetime print(' end:') self._print_datetime(end_datetime) return end_datetime def start_recovery(self,start_datetime,config): print('recovery:') print(' start:') self._print_datetime(start_datetime) duration_days = config['duration_days'] if duration_days <= 0: end_datetime = start_datetime print(' end:') self._print_datetime(end_datetime) return end_datetime relay = self._config['relays']['white_light'] power = config['white_light']['power'] pwm0_on_duration_hours = config['white_light']['pwm0_on_duration_hours'] pwm0_off_duration_hours = config['white_light']['pwm0_off_duration_hours'] pwm0_period_hours = pwm0_on_duration_hours + pwm0_off_duration_hours pwm0_period = pwm0_period_hours*self._MILLISECONDS_PER_HOUR pwm0_on_duration = pwm0_on_duration_hours*self._MILLISECONDS_PER_HOUR pwm0_period_days = pwm0_period_hours/self._HOURS_PER_DAY count = duration_days/pwm0_period_days delay = self._start_datetime_to_delay(start_datetime) self._start_pwm(relay, power, delay, count, 1, [pwm0_period], [pwm0_on_duration]) duration_datetime = self._duration_days_to_duration_datetime(duration_days) end_datetime = start_datetime + duration_datetime print(' end:') self._print_datetime(end_datetime) return end_datetime def _write_data(self): if self._no_hardware: time.sleep(1/self._config['camera_trigger']['frame_rate_hz']) return time_start = time.time() pwm_status = self._get_pwm_status() camera_trigger_on = pwm_status[self._config['relays']['camera_trigger']][1] if camera_trigger_on: power = self._get_power() white_light_pwm_status = pwm_status[self._config['relays']['white_light']][0:3] white_light_power = power[self._config['relays']['white_light']] red_light_pwm_status = pwm_status[self._config['relays']['red_light']][1] red_light_power = power[self._config['relays']['red_light']] self._video_frame += 1 date_time = self._get_date_time_str() if ((self._white_light_power_prev != white_light_power) or (self._red_light_pwm_status_prev != red_light_pwm_status) or (self._red_light_power_prev != red_light_power) or (self._state_prev != self._state)): # if ((not self._prev_written) and # (self._white_light_power_prev is not None) and # (self._red_light_pwm_status_prev is not None) and # (self._red_light_power_prev is not None) and # (self._date_time_prev is not None)): # row = [] # row.append(self._video_frame - 1) # row.append(self._date_time_prev) # row.append(self._state_prev) # row.append(self._white_light_power_prev) # row.append(self._red_light_pwm_status_prev) # row.append(self._red_light_power_prev) # self._writerow(row) row = [] row.append(self._video_frame) row.append(date_time) row.append(self._state) row.append(white_light_power) row.append(red_light_pwm_status) row.append(red_light_power) self._writerow(row) self._prev_written = True else: self._prev_written = False self._white_light_power_prev = white_light_power self._red_light_pwm_status_prev = red_light_pwm_status self._red_light_power_prev = red_light_power self._state_prev = self._state self._date_time_prev = date_time time_stop = time.time() time_sleep = 1/self._config['camera_trigger']['frame_rate_hz'] - (time_stop - time_start) if time_sleep > 0: time.sleep(time_sleep) def plot_data(self,data_file_path): if self._no_hardware: return print('data_file_path: {0}'.format(data_file_path)) fig = plt.figure() filename = os.path.split(data_file_path)[1] fig.suptitle(filename, fontsize=14, fontweight='bold') self._data_file_path = os.path.abspath(data_file_path) with open(self._data_file_path,'r') as fid: header = fid.readline().rstrip().split(',') dt = np.dtype({'names':header,'formats':['S25']*len(header)}) self._numpy_data = np.loadtxt(self._data_file_path,dtype=dt,delimiter=",",skiprows=1) scale_factor = 1000/(self._config['camera_trigger']['frame_rate_hz']*self._MILLISECONDS_PER_DAY) t = np.uint32(self._numpy_data['video_frame'])*scale_factor # white light power = np.uint8(self._numpy_data['white_light_power']) y_max = 255 plt.subplot(2, 1, 1) # plt.plot(t, power) plt.step(t, power, where='post') plt.ylim(-0.1, y_max+45) plt.ylabel('white light power') plt.xlabel('days') duration_days = int(t.max()) plt.xticks(np.linspace(0, duration_days+1, 2*duration_days+3, endpoint=True)) plt.grid(True) marker_half_thickness = 0.025 start = 0 entrainment_duration = self._config['entrainment']['duration_days'] stop = entrainment_duration - marker_half_thickness if entrainment_duration > 0: plt.axvspan(start, stop, color='y', alpha=0.5, lw=0) plt.text(start + (stop-start)/2, y_max+25, 'entrainment', fontsize=15, horizontalalignment='center') start = stop stop = start + 2*marker_half_thickness plt.axvspan(start, stop, color='k', alpha=0.5, lw=0) run = 0 for run_config in self._config['experiment']: start = stop run_duration = run_config['duration_days'] stop = start + run_duration - 2*marker_half_thickness if run_duration > 0: plt.axvspan(start, stop, color='g', alpha=0.5, lw=0) plt.text(start + (stop-start)/2, y_max+25, 'experiment run {0}'.format(run), fontsize=15, horizontalalignment='center') start = stop stop = start + 2*marker_half_thickness plt.axvspan(start, stop, color='k', alpha=0.5, lw=0) run += 1 start = stop recovery_duration = self._config['recovery']['duration_days'] stop = start + recovery_duration - marker_half_thickness if recovery_duration > 0: plt.axvspan(start, stop, color='r', alpha=0.5, lw=0) plt.text(start + (stop-start)/2, y_max+25, 'recovery', fontsize=15, horizontalalignment='center') start = stop stop = start + marker_half_thickness plt.axvspan(start, stop, color='k', alpha=0.5, lw=0) # red light red_light_pwm_status = np.uint8(self._numpy_data['red_light_pwm_status']) y_max = 1 plt.subplot(2, 1, 2) # plt.plot(t, red_light_pwm_status) plt.step(t, red_light_pwm_status, where='post') plt.ylim(-0.1, y_max+0.25) plt.ylabel('red light pwm status') plt.xlabel('days') duration_days = int(t.max()) plt.xticks(np.linspace(0, duration_days+1, 2*duration_days+3, endpoint=True)) plt.grid(True) marker_half_thickness = 0.025 start = 0 entrainment_duration = self._config['entrainment']['duration_days'] stop = entrainment_duration - marker_half_thickness if entrainment_duration > 0: plt.axvspan(start, stop, color='y', alpha=0.5, lw=0) plt.text(start + (stop-start)/2, y_max+0.1, 'entrainment', fontsize=15, horizontalalignment='center') start = stop stop = start + 2*marker_half_thickness plt.axvspan(start, stop, color='k', alpha=0.5, lw=0) run = 0 for run_config in self._config['experiment']: start = stop run_duration = run_config['duration_days'] stop = start + run_duration - 2*marker_half_thickness if run_duration > 0: plt.axvspan(start, stop, color='g', alpha=0.5, lw=0) plt.text(start + (stop-start)/2, y_max+0.1, 'experiment run {0}'.format(run), fontsize=15, horizontalalignment='center') start = stop stop = start + 2*marker_half_thickness plt.axvspan(start, stop, color='k', alpha=0.5, lw=0) run += 1 start = stop recovery_duration = self._config['recovery']['duration_days'] stop = start + recovery_duration - marker_half_thickness if recovery_duration > 0: plt.axvspan(start, stop, color='r', alpha=0.5, lw=0) plt.text(start + (stop-start)/2, y_max+0.1, 'recovery', fontsize=15, horizontalalignment='center') start = stop stop = start + marker_half_thickness plt.axvspan(start, stop, color='k', alpha=0.5, lw=0) plt.show() def run(self): self.stop() print('config_file_path:') print(self._config_file_path) print('data_file_path:') data_file_path = self.start_data_writer() print(data_file_path) self.start_board_indicator_light_cycle(self._config['relays']['board_indicator_light']) if self._quick_test: self._config['start']['offset_days'] = -1 if self._config['camera_trigger']['frame_rate_hz'] <= 1: self._config['camera_trigger']['frame_rate_hz'] *= 10 camera_trigger_start_datetime = self._start_to_start_datetime(self._config['start']) entrainment_duration_datetime = self._duration_days_to_duration_datetime(self._config['entrainment']['duration_days']) end_datetime = camera_trigger_start_datetime + entrainment_duration_datetime for run in range(len(self._config['experiment'])): run_duration_datetime = self._duration_days_to_duration_datetime(self._config['experiment'][run]['duration_days']) end_datetime += run_duration_datetime recovery_duration_datetime = self._duration_days_to_duration_datetime(self._config['recovery']['duration_days']) end_datetime += recovery_duration_datetime print('sleep assay will run until:') self._print_datetime(end_datetime) delay = self._start_datetime_to_delay(camera_trigger_start_datetime) self._video_frame = -1 self.start_camera_trigger(self._config['relays']['camera_trigger'], self._config['camera_trigger']['frame_rate_hz'], delay) self._state = 'entrainment' entrainment_end_datetime = self.start_entrainment(camera_trigger_start_datetime, self._config['entrainment']) self._white_light_power_prev = None self._red_light_pwm_status_prev = None self._red_light_power_prev = None self._state_prev = None self._prev_written = False self._date_time_prev = None while(datetime.datetime.now() < entrainment_end_datetime): self._write_data() run_start_datetime = entrainment_end_datetime for run in range(len(self._config['experiment'])): self._state = 'experiment_run{0}'.format(run) run_end_datetime = self.start_experiment_run(run, run_start_datetime, self._config['experiment'][run]) while(datetime.datetime.now() < run_end_datetime): self._write_data() run_start_datetime = run_end_datetime self._state = 'recovery' recovery_start_datetime = run_end_datetime recovery_end_datetime = self.start_recovery(recovery_start_datetime, self._config['recovery']) while(datetime.datetime.now() < recovery_end_datetime): self._write_data() self._csv_file.close() self._stop_all_pulses() self.plot_data(self._csv_file_path)
class MettlerToledoDevice(object): ''' This Python package (mettler_toledo_device) creates a class named MettlerToledoDevice, which contains an instance of serial_device2.SerialDevice and adds methods to it to interface to Mettler Toledo balances and scales that use the Mettler Toledo Standard Interface Command Set (MT-SICS). Example Usage: dev = MettlerToledoDevice() # Might automatically find device if one available # if it is not found automatically, specify port directly dev = MettlerToledoDevice(port='/dev/ttyUSB0') # Linux specific port dev = MettlerToledoDevice(port='/dev/tty.usbmodem262471') # Mac OS X specific port dev = MettlerToledoDevice(port='COM3') # Windows specific port dev.get_serial_number() 1126493049 dev.get_balance_data() ['XS204', 'Excellence', '220.0090', 'g'] dev.get_weight_stable() [-0.0082, 'g'] #if weight is stable None #if weight is dynamic dev.get_weight() [-0.6800, 'g', 'S'] #if weight is stable [-0.6800, 'g', 'D'] #if weight is dynamic dev.zero_stable() True #zeros if weight is stable False #does not zero if weight is not stable dev.zero() 'S' #zeros if weight is stable 'D' #zeros if weight is dynamic ''' _TIMEOUT = 0.05 _WRITE_WRITE_DELAY = 0.05 _RESET_DELAY = 2.0 def __init__(self,*args,**kwargs): if 'debug' in kwargs: self.debug = kwargs['debug'] else: kwargs.update({'debug': DEBUG}) self.debug = DEBUG if 'try_ports' in kwargs: try_ports = kwargs.pop('try_ports') else: try_ports = None if 'baudrate' not in kwargs: kwargs.update({'baudrate': BAUDRATE}) elif (kwargs['baudrate'] is None) or (str(kwargs['baudrate']).lower() == 'default'): kwargs.update({'baudrate': BAUDRATE}) if 'timeout' not in kwargs: kwargs.update({'timeout': self._TIMEOUT}) if 'write_write_delay' not in kwargs: kwargs.update({'write_write_delay': self._WRITE_WRITE_DELAY}) if ('port' not in kwargs) or (kwargs['port'] is None): port = find_mettler_toledo_device_port(baudrate=kwargs['baudrate'], try_ports=try_ports, debug=kwargs['debug']) kwargs.update({'port': port}) t_start = time.time() self._serial_device = SerialDevice(*args,**kwargs) atexit.register(self._exit_mettler_toledo_device) time.sleep(self._RESET_DELAY) t_end = time.time() self._debug_print('Initialization time =', (t_end - t_start)) def _debug_print(self, *args): if self.debug: print(*args) def _exit_mettler_toledo_device(self): pass def _args_to_request(self,*args): request = ''.join(map(str,args)) request = request + '\r\n'; return request def _send_request(self,*args): '''Sends request to device over serial port and returns number of bytes written''' request = self._args_to_request(*args) self._debug_print('request', request) bytes_written = self._serial_device.write_check_freq(request,delay_write=True) return bytes_written def _send_request_get_response(self,*args): '''Sends request to device over serial port and returns response''' request = self._args_to_request(*args) self._debug_print('request', request) response = self._serial_device.write_read(request,use_readline=True,check_write_freq=True) response = response.replace('"','') response_list = response.split() if 'ES' in response_list[0]: raise MettlerToledoError('Syntax Error!') elif 'ET' in response_list[0]: raise MettlerToledoError('Transmission Error!') elif 'EL' in response_list[0]: raise MettlerToledoError('Logical Error!') return response_list def close(self): ''' Close the device serial port. ''' self._serial_device.close() def get_port(self): return self._serial_device.port def get_commands(self): ''' Inquiry of all implemented MT-SICS commands. ''' response = self._send_request_get_response('I0') if 'I' in response[1]: raise MettlerToledoError('The list cannot be sent at present as another operation is taking place.') return response[2:] def get_mtsics_level(self): ''' Inquiry of MT-SICS level and MT-SICS versions. ''' response = self._send_request_get_response('I1') if 'I' in response[1]: raise MettlerToledoError('Command understood, not executable at present.') return response[2:] def get_balance_data(self): ''' Inquiry of balance data. ''' response = self._send_request_get_response('I2') if 'I' in response[1]: raise MettlerToledoError('Command understood, not executable at present.') return response[2:] def get_software_version(self): ''' Inquiry of balance SW version and type definition number. ''' response = self._send_request_get_response('I3') if 'I' in response[1]: raise MettlerToledoError('Command understood, not executable at present.') return response[2:] def get_serial_number(self): ''' Inquiry of serial number. ''' response = self._send_request_get_response('I4') if 'I' in response[1]: raise MettlerToledoError('Command understood, not executable at present.') return response[2] def get_software_id(self): ''' Inquiry of SW-Identification number. ''' response = self._send_request_get_response('I5') if 'I' in response[1]: raise MettlerToledoError('Command understood, not executable at present.') return response[2] def get_weight_stable(self): ''' Send the current stable net weight value. ''' try: response = self._send_request_get_response('S') if 'I' in response[1]: raise MettlerToledoError('Command understood, not executable at present.') elif '+' in response[1]: raise MettlerToledoError('Balance in overload range.') elif '-' in response[1]: raise MettlerToledoError('Balance in underload range.') response[2] = float(response[2]) return response[2:] except: pass def get_weight(self): ''' Send the current net weight value, irrespective of balance stability. ''' response = self._send_request_get_response('SI') if 'I' in response[1]: raise MettlerToledoError('Command understood, not executable at present.') elif '+' in response[1]: raise MettlerToledoError('Balance in overload range.') elif '-' in response[1]: raise MettlerToledoError('Balance in underload range.') response.append(response[1]) response[2] = float(response[2]) return response[2:] def zero_stable(self): ''' Zero the balance. ''' try: response = self._send_request_get_response('Z') if 'I' in response[1]: raise MettlerToledoError('Zero setting not performed (balance is currently executing another command, e.g. taring, or timeout as stability was not reached).') elif '+' in response[1]: raise MettlerToledoError('Upper limit of zero setting range exceeded.') elif '-' in response[1]: raise MettlerToledoError('Lower limit of zero setting range exceeded.') return True except: return False def zero(self): ''' Zero the balance immediately regardless the stability of the balance. ''' response = self._send_request_get_response('ZI') if 'I' in response[1]: raise MettlerToledoError('Zero setting not performed (balance is currently executing another command, e.g. taring, or timeout as stability was not reached).') elif '+' in response[1]: raise MettlerToledoError('Upper limit of zero setting range exceeded.') elif '-' in response[1]: raise MettlerToledoError('Lower limit of zero setting range exceeded.') return response[1] def reset(self): ''' Resets the balance to the condition found after switching on, but without a zero setting being performed. ''' self._send_request('@')
class MettlerToledoDevice(object): """ This Python package (mettler_toledo_device) creates a class named MettlerToledoDevice, which contains an instance of serial_device2.SerialDevice and adds methods to it to interface to Mettler Toledo balances and scales that use the Mettler Toledo Standard Interface Command Set (MT-SICS). Example Usage: dev = MettlerToledoDevice() # Might automatically find device if one available # if it is not found automatically, specify port directly dev = MettlerToledoDevice(port='/dev/ttyUSB0') # Linux specific port dev = MettlerToledoDevice(port='/dev/tty.usbmodem262471') # Mac OS X specific port dev = MettlerToledoDevice(port='COM3') # Windows specific port dev.get_serial_number() 1126493049 dev.get_balance_data() ['XS204', 'Excellence', '220.0090', 'g'] dev.get_weight_stable() [-0.0082, 'g'] #if weight is stable None #if weight is dynamic dev.get_weight() [-0.6800, 'g', 'S'] #if weight is stable [-0.6800, 'g', 'D'] #if weight is dynamic dev.zero_stable() True #zeros if weight is stable False #does not zero if weight is not stable dev.zero() 'S' #zeros if weight is stable 'D' #zeros if weight is dynamic """ _TIMEOUT = 0.05 _WRITE_WRITE_DELAY = 0.05 _RESET_DELAY = 2.0 def __init__(self, *args, **kwargs): if "debug" in kwargs: self.debug = kwargs["debug"] else: kwargs.update({"debug": DEBUG}) self.debug = DEBUG if "try_ports" in kwargs: try_ports = kwargs.pop("try_ports") else: try_ports = None if "baudrate" not in kwargs: kwargs.update({"baudrate": BAUDRATE}) elif (kwargs["baudrate"] is None) or (str(kwargs["baudrate"]).lower() == "default"): kwargs.update({"baudrate": BAUDRATE}) if "timeout" not in kwargs: kwargs.update({"timeout": self._TIMEOUT}) if "write_write_delay" not in kwargs: kwargs.update({"write_write_delay": self._WRITE_WRITE_DELAY}) if ("port" not in kwargs) or (kwargs["port"] is None): port = find_mettler_toledo_device_port( baudrate=kwargs["baudrate"], try_ports=try_ports, debug=kwargs["debug"] ) kwargs.update({"port": port}) t_start = time.time() self._serial_device = SerialDevice(*args, **kwargs) atexit.register(self._exit_mettler_toledo_device) time.sleep(self._RESET_DELAY) t_end = time.time() self._debug_print("Initialization time =", (t_end - t_start)) def _debug_print(self, *args): if self.debug: print(*args) def _exit_mettler_toledo_device(self): pass def _args_to_request(self, *args): request = "".join(map(str, args)) request = request + "\r\n" return request def _send_request(self, *args): """Sends request to device over serial port and returns number of bytes written""" request = self._args_to_request(*args) self._debug_print("request", request) bytes_written = self._serial_device.write_check_freq(request, delay_write=True) return bytes_written def _send_request_get_response(self, *args): """Sends request to device over serial port and returns response""" request = self._args_to_request(*args) self._debug_print("request", request) response = self._serial_device.write_read(request, use_readline=True, check_write_freq=True) response = response.decode().replace('"', "") response_list = response.split() if "ES" in response_list[0]: raise MettlerToledoError("Syntax Error!") elif "ET" in response_list[0]: raise MettlerToledoError("Transmission Error!") elif "EL" in response_list[0]: raise MettlerToledoError("Logical Error!") return response_list def close(self): """ Close the device serial port. """ self._serial_device.close() def get_port(self): return self._serial_device.port def get_commands(self): """ Inquiry of all implemented MT-SICS commands. """ response = self._send_request_get_response("I0") if "I" in response[1]: raise MettlerToledoError("The list cannot be sent at present as another operation is taking place.") return response[2:] def get_mtsics_level(self): """ Inquiry of MT-SICS level and MT-SICS versions. """ response = self._send_request_get_response("I1") if "I" in response[1]: raise MettlerToledoError("Command understood, not executable at present.") return response[2:] def get_balance_data(self): """ Inquiry of balance data. """ response = self._send_request_get_response("I2") if "I" in response[1]: raise MettlerToledoError("Command understood, not executable at present.") return response[2:] def get_software_version(self): """ Inquiry of balance SW version and type definition number. """ response = self._send_request_get_response("I3") if "I" in response[1]: raise MettlerToledoError("Command understood, not executable at present.") return response[2:] def get_serial_number(self): """ Inquiry of serial number. """ response = self._send_request_get_response("I4") if "I" in response[1]: raise MettlerToledoError("Command understood, not executable at present.") return response[2] def get_software_id(self): """ Inquiry of SW-Identification number. """ response = self._send_request_get_response("I5") if "I" in response[1]: raise MettlerToledoError("Command understood, not executable at present.") return response[2] def get_weight_stable(self): """ Send the current stable net weight value. """ try: response = self._send_request_get_response("S") if "I" in response[1]: raise MettlerToledoError("Command understood, not executable at present.") elif "+" in response[1]: raise MettlerToledoError("Balance in overload range.") elif "-" in response[1]: raise MettlerToledoError("Balance in underload range.") response[2] = float(response[2]) return response[2:] except: pass def get_weight(self): """ Send the current net weight value, irrespective of balance stability. """ response = self._send_request_get_response("SI") if "I" in response[1]: raise MettlerToledoError("Command understood, not executable at present.") elif "+" in response[1]: raise MettlerToledoError("Balance in overload range.") elif "-" in response[1]: raise MettlerToledoError("Balance in underload range.") response.append(response[1]) response[2] = float(response[2]) return response[2:] def zero_stable(self): """ Zero the balance. """ try: response = self._send_request_get_response("Z") if "I" in response[1]: raise MettlerToledoError( "Zero setting not performed (balance is currently executing another command, e.g. taring, or timeout as stability was not reached)." ) elif "+" in response[1]: raise MettlerToledoError("Upper limit of zero setting range exceeded.") elif "-" in response[1]: raise MettlerToledoError("Lower limit of zero setting range exceeded.") return True except: return False def zero(self): """ Zero the balance immediately regardless the stability of the balance. """ response = self._send_request_get_response("ZI") if "I" in response[1]: raise MettlerToledoError( "Zero setting not performed (balance is currently executing another command, e.g. taring, or timeout as stability was not reached)." ) elif "+" in response[1]: raise MettlerToledoError("Upper limit of zero setting range exceeded.") elif "-" in response[1]: raise MettlerToledoError("Lower limit of zero setting range exceeded.") return response[1] def reset(self): """ Resets the balance to the condition found after switching on, but without a zero setting being performed. """ self._send_request("@")