Repository: BYUCamachoLab/pyrolab (language: python)

Issue:
Merge branch 'master' into develop
Catch all in case there's anything I missed when I accidentally merged the first time. Might as well go all in now.


Discussion / hints:

Fix patch:
diff --git a/examples/903.motion/kcube_jogging.py b/examples/903.motion/kcube_jogging.py
index 95110b2..e6d6db3 100644
--- a/examples/903.motion/kcube_jogging.py
+++ b/examples/903.motion/kcube_jogging.py
@@ -1,11 +1,12 @@
+# Example of how to use the linear stage (Z825B) with software limits locally
+
+# import the Kinesis Library and make sure the path is correct
 import os
 os.add_dll_directory("C:\\Program Files\\Thorlabs\\Kinesis")
 
-from ctypes import c_int, c_double, byref, pointer
-import time
-
 from pyrolab.drivers.motion.z825b import Z825B
-linear = Z825B("27003497", home=True)
+# Look at the connected KCube for correct serial num
+linear = Z825B("27504851", home=True)
 
 #Set and read a few of the parameters pertaining to jogging
 print(f"Jog mode: {linear.jog_mode}")
diff --git a/examples/903.motion/kcube_limits.py b/examples/903.motion/kcube_limits.py
index dad8d78..d724575 100644
--- a/examples/903.motion/kcube_limits.py
+++ b/examples/903.motion/kcube_limits.py
@@ -1,11 +1,15 @@
+# Example of how to use the linear stage (Z825B) with software limits locally
+
+# import the Kinesis Library and make sure the path is correct
 import os
 os.add_dll_directory("C:\\Program Files\\Thorlabs\\Kinesis")
 
-from ctypes import c_int, c_double, byref, pointer
-import time
-
 from pyrolab.drivers.motion.z825b import Z825B
-linear = Z825B("27003497", home=True)
+# Look at the connected KCube for correct serial num
+linear = Z825B("27504851", home=True)
+
+# The stage defaults to the "disallow" limit mode
+# and defaults to software limits of 0 and 25 mm 
 
 # read and set all the different types of software limits modes
 print(linear.soft_limits_mode)
@@ -21,20 +25,25 @@
 linear.max_pos = 10
 print(f"max pos: {linear.max_pos} mm")
 
-
+# We will now test the limit mode "disallow"
+# it should error if we try to move it outside the limits set
 print(f"Before Move: {linear.get_position()} mm")
-linear.move_to(5)
+linear.move_to(5) # Within the limits so no error
 print(f"Moved to 5: {linear.get_position()} mm")
 try:
-    linear.move_to(15)
+    linear.move_to(15) # outside the limits so we must catch the error
 except RuntimeError:
     pass
 print(f"Should have dissalowed move: {linear.get_position()} mm")
+
+# We will now test the limit mode "partial"
 linear.soft_limits_mode = "partial"
-linear.move_to(15)
+linear.move_to(15) # It will move but only until it has reached it's limit
 print(f"Should have truncated move to {linear.max_pos}: {linear.get_position()} mm")
+
+# We will now test the limit mode "all"
 linear.soft_limits_mode = "all"
-linear.move_to(15)
+linear.move_to(15) # It will simply ignore any software limit set
 print(f"Should have ignored max pos at {linear.max_pos}: {linear.get_position()} mm")
 linear.move_to(0)
 
diff --git a/examples/903.motion/kcube_rotation.py b/examples/903.motion/kcube_rotation.py
new file mode 100644
index 0000000..aecd239
--- /dev/null
+++ b/examples/903.motion/kcube_rotation.py
@@ -0,0 +1,28 @@
+# Example of how to use the rotation stage (PRM1Z8) locally
+
+# import the Kinesis Library and make sure the path is correct
+import os
+os.add_dll_directory("C:\\Program Files\\Thorlabs\\Kinesis")
+
+from pyrolab.drivers.motion.prm1z8 import PRM1Z8
+# Look at the connected KCube for correct serial num
+rot = PRM1Z8("27003366", home=False) 
+
+# Prints out the starting postition to get an idea of where it currently is
+starting_pos = rot.get_position()
+print(f"Stating position: {starting_pos} degrees")
+
+# A simple script to test moving the stage
+while True:
+    # Input a position in degrees
+    move_pos = float(input("Rotation Position (deg):"))
+    # Input a 0 to go back to the starting position and exit the program
+    if move_pos == 0: 
+        break
+    pos = rot.get_position()
+    print(f"Before Move: {pos} degrees")
+    rot.move_to(move_pos)
+    pos = rot.get_position()
+    print(f"After Move: {pos} degrees")
+rot.move_to(starting_pos)
+rot.close()
\ No newline at end of file
diff --git a/examples/903.motion/kcubes_linear.py b/examples/903.motion/kcubes_linear.py
new file mode 100644
index 0000000..b21860a
--- /dev/null
+++ b/examples/903.motion/kcubes_linear.py
@@ -0,0 +1,28 @@
+# Example of how to use the linear stage (Z825B) locally
+
+# import the Kinesis Library and make sure the path is correct
+import os
+os.add_dll_directory("C:\\Program Files\\Thorlabs\\Kinesis")
+
+from pyrolab.drivers.motion.z825b import Z825B
+# Look at the connected KCube for correct serial num
+linear = Z825B("27504851", home=False) 
+
+# Prints out the starting postition to get an idea of where it currently is
+starting_pos = linear.get_position()
+print(f"Stating position: {starting_pos} mm")
+
+# A simple script to test moving the stage
+while True:
+    # Input a position in degrees
+    move_pos = float(input("linear Position (mm):"))
+    # Input a 0 to go back to the starting position and exit the program
+    if move_pos == 0: 
+        break
+    pos = linear.get_position()
+    print(f"Before Move: {pos} mm")
+    linear.move_to(move_pos)
+    pos = linear.get_position()
+    print(f"After Move: {pos} mm")
+linear.move_to(starting_pos)
+linear.close()
diff --git a/examples/904.sacher-tec/sachertec_sweep.py b/examples/904.sacher-tec/sachertec_sweep.py
new file mode 100644
index 0000000..a04bafd
--- /dev/null
+++ b/examples/904.sacher-tec/sachertec_sweep.py
@@ -0,0 +1,181 @@
+import os
+import time
+import math
+import keyboard
+
+from ctypes import *
+
+from sacher_tec._utils import (
+    c_word,
+    c_dword,
+    bind,
+    not_implemented,
+)
+
+os.environ['PATH'] = ("C:\\Program Files\\SacherLasertechnik\\MotorMotion 2.1\\data"
++ ";" + os.environ['PATH'])
+
+epos = cdll.LoadLibrary("EposCmd64.dll")
+
+from sacher_tec import epos_motor as epm
+
+ST_DISABLED = c_word(0)
+ST_ENABLED = c_word(1)
+A_COEF = -3.499E-12
+B_COEF = 7.74529E-5
+C_COEF = 960.024
+WL_MAX = 985.5
+WL_MIN = 919.5
+
+def du_to_wl(du):
+    wl = A_COEF*(math.pow(du,2)) + B_COEF*du + C_COEF
+    if(wl < WL_MIN):
+        return WL_MIN
+    elif(wl > WL_MAX):
+        return WL_MAX
+    else:
+        return wl
+
+def wl_to_du(wl):
+    if(wl < WL_MIN):
+        wl = WL_MIN
+    elif(wl > WL_MAX):
+        wl = WL_MAX
+    du = -1*B_COEF/(2*A_COEF) - math.sqrt((wl - C_COEF)/A_COEF + math.pow((B_COEF/(2*A_COEF)),2))
+    return round(du)
+
+def vel_to_du(vel):
+    if(vel > 30):
+        vel = 30
+    if(vel < 0.01):
+        vel = 0.01
+    du = round(vel*380)
+    return du
+
+def du_to_vel(du):
+    vel = du/3
+    if(vel > 30):
+        vel = 30
+    if(vel < 0.01):
+        vel = 0.01
+    return vel
+
+def acc_to_du(acc):
+    if(acc > 25):
+        acc = 25
+    if(acc < 0.01):
+        acc = 0.01
+    du = round(acc*380)
+    return du
+
+def du_to_acc(du):
+    acc = du/380
+    if(acc > 25):
+        acc = 25
+    if(acc < 0.01):
+        acc = 0.01
+    return acc
+
+def flush_input():
+    try:
+        import msvcrt
+        while msvcrt.kbhit():
+            msvcrt.getch()
+    except ImportError:
+        import sys, termios
+        termios.tcflush(sys.stdin, termios.TCIOFLUSH)
+
+def keyDebounce():
+    time.sleep(0.2) # sleep 200 ms 
+
+name = create_string_buffer(("EPOS2").encode('utf-8'))
+protocol = create_string_buffer(("MAXON SERIAL V2").encode('utf-8'))
+interface = create_string_buffer(("USB").encode('utf-8'))
+port = create_string_buffer(("USB0").encode('utf-8'))
+NodeID = 1
+pNbOfBytesRead=c_uint()
+pData=c_uint()
+pPositionIs=c_long()
+pErrorCode=c_int()
+pMode=c_byte()
+pState=c_word()
+pTargetReached=c_bool()
+pProfileVelocity=c_dword()
+pProfileAcceleration=c_dword()
+pProfileDeceleration=c_dword()
+
+print("what is the low wavelength (nm)?")
+wl_low = float(input())
+
+print("what is the high wavelength (nm)?")
+wl_high = float(input())
+
+print("what velocity would you like it to move at (nm/s)?")
+vel = vel_to_du(float(input()))
+
+acc = acc_to_du(1.0)
+
+keyhandle = epm.VCS_OpenDevice(name,protocol,interface,port,byref(pErrorCode))
+
+ret=epm.VCS_SetState(keyhandle, NodeID, ST_ENABLED, byref(pErrorCode))
+
+ret=epm.VCS_SetPositionProfile(keyhandle, NodeID, c_dword(vel), c_dword(acc), c_dword(acc), byref(pErrorCode))
+
+ret=epm.VCS_MoveToPosition(keyhandle, NodeID, wl_to_du(wl_low), 1, 0, byref(pErrorCode))
+
+print("Sweeping")
+print("Press 'q' to quit")
+
+comp = False
+while(comp == False):
+    ret=epm.VCS_GetMovementState(keyhandle, NodeID, byref(pTargetReached), byref(pErrorCode))
+    comp = pTargetReached.value
+    if keyboard.is_pressed('q'):
+        keyDebounce() # key 'debouncer'
+        flush_input()
+        comp = True
+        epm.VCS_GetPositionIs(keyhandle, NodeID, byref(pPositionIs), byref(pErrorCode))
+        print(f"Position: {du_to_wl(pPositionIs.value)}")
+
+sweep_done = False
+init_time = time.time()
+while(sweep_done == False):
+    ret=epm.VCS_MoveToPosition(keyhandle, NodeID, wl_to_du(wl_high), 1, 0, byref(pErrorCode))
+    comp = False
+    while(comp == False):
+        ret=epm.VCS_GetMovementState(keyhandle, NodeID, byref(pTargetReached), byref(pErrorCode))
+        comp = pTargetReached.value
+        if keyboard.is_pressed('q'):
+            keyDebounce() # key 'debouncer'
+            flush_input()
+            comp = True
+            sweep_done = True
+        if(time.time() - init_time > 1):
+            init_time = time.time()
+            epm.VCS_GetPositionIs(keyhandle, NodeID, byref(pPositionIs), byref(pErrorCode))
+            print(f"Position: {du_to_wl(pPositionIs.value)}")
+
+    if(sweep_done):
+        continue
+
+    ret=epm.VCS_MoveToPosition(keyhandle, NodeID, wl_to_du(wl_low), 1, 0, byref(pErrorCode))
+    comp = False
+    while(comp == False):
+        ret=epm.VCS_GetMovementState(keyhandle, NodeID, byref(pTargetReached), byref(pErrorCode))
+        comp = pTargetReached.value
+        if keyboard.is_pressed('q'):
+            keyDebounce() # key 'debouncer'
+            flush_input()
+            comp = True
+            sweep_done = True
+        if(time.time() - init_time > 1):
+            init_time = time.time()
+            epm.VCS_GetPositionIs(keyhandle, NodeID, byref(pPositionIs), byref(pErrorCode))
+            print(f"Position: {du_to_wl(pPositionIs.value)}")
+    
+epm.VCS_GetPositionIs(keyhandle, NodeID, byref(pPositionIs), byref(pErrorCode))
+print(f"Position: {du_to_wl(pPositionIs.value)}")
+
+ret=epm.VCS_SetState(keyhandle, NodeID, ST_DISABLED, byref(pErrorCode))
+ret=epm.VCS_CloseAllDevices(byref(pErrorCode))
+print("laser closed")
\ No newline at end of file
diff --git a/examples/905.pp-laser/ppcl550_example.py b/examples/905.pp-laser/ppcl550_example.py
new file mode 100644
index 0000000..f8f73fd
--- /dev/null
+++ b/examples/905.pp-laser/ppcl550_example.py
@@ -0,0 +1,31 @@
+# Example of how to use the Pure Photonics pp550 laser locally
+
+# import the the pyrolab class
+from pyrolab.drivers.lasers.ppcl550 import PPCL55x
+
+import time
+
+# The 550 laser has a range of 1515-1570 nm, power range of 6-13.5 dBm
+# choose the correct COM port 
+laser = PPCL55x(port="COM6")
+
+# set the power wavelength and channel
+laser.setPower(13.5)
+laser.setWavelength(1550)
+laser.setChannel(1)
+
+# turn the laser on
+laser.on()
+time.sleep(10)
+
+# adjust the power to 10 dBm
+print("setting the power to 10 dBm")
+laser.setPower(10)
+time.sleep(10)
+
+# change the wavelength to 1570 nm
+print("setting wavelength to 1570 nm")
+laser.setWavelength(1570)
+time.sleep(10)
+laser.off()
+laser.close()
\ No newline at end of file
diff --git a/examples/905.pp-laser/ppcl551_example.py b/examples/905.pp-laser/ppcl551_example.py
new file mode 100644
index 0000000..4159593
--- /dev/null
+++ b/examples/905.pp-laser/ppcl551_example.py
@@ -0,0 +1,31 @@
+# Example of how to use the Pure Photonics pp551 laser locally
+
+# import the the pyrolab class
+from pyrolab.drivers.lasers.ppcl550 import PPCL55x
+
+import time
+
+# The 551 laser has a range of 1569-1625 nm, power range of 6-13.5 dBm
+# choose the correct COM port 
+laser = PPCL55x(minWL=1569, maxWL=1625, port="COM5")
+
+# set the power wavelength and channel
+laser.setPower(13.5)
+laser.setWavelength(1570)
+laser.setChannel(1)
+
+# turn the laser on
+laser.on()
+time.sleep(10)
+
+# adjust the power to 10 dBm
+print("setting the power to 10 dBm")
+laser.setPower(10)
+time.sleep(10)
+
+# change the wavelength to 1600 nm
+print("setting wavelength to 1600 nm")
+laser.setWavelength(1600)
+time.sleep(10)
+laser.off()
+laser.close()
\ No newline at end of file
diff --git a/examples/906.uc480-camera/gui_dev.py b/examples/906.uc480-camera/gui_dev.py
new file mode 100644
index 0000000..656c854
--- /dev/null
+++ b/examples/906.uc480-camera/gui_dev.py
@@ -0,0 +1,192 @@
+# import the necessary packages
+from __future__ import print_function
+from PIL import Image
+from PIL import ImageTk
+import tkinter as tki
+import threading
+import datetime
+import imutils
+import cv2
+import os
+from pyrolab.api import locate_ns, Proxy
+import numpy as np
+import socket
+import pickle
+import time
+from datetime import datetime
+
+HEADERSIZE = 10
+BRIGHTNESS = 5
+PORT = 2222
+SER_NUMBER = 4103257229
+COLOR = True
+
+class PhotoBoothApp:
+	def __init__(self):
+		# store the video stream object and output path, then initialize
+		# the most recently read frame, thread for reading frames, and
+		# the thread stop event
+		self.outputPath = "C:/Users/hilld/Desktop"
+		self.frame = None
+		self.thread = None
+		self.stopEvent = None
+
+		# initialize the root window and image panel
+		self.root = tki.Tk()
+		self.panel = None
+
+        # create a button, that when pressed, will take the current
+		# frame and save it to file
+		btn = tki.Button(self.root, text="Snapshot!",
+			command=self.takeSnapshot)
+		btn.pack(side="bottom", fill="both", expand="yes", padx=10,
+			pady=10)
+
+		# start a thread that constantly pools the video sensor for
+		# the most recently read frame
+		self.init_camera()
+		self.stopEvent = threading.Event()
+		self.thread = threading.Thread(target=self.videoLoop, args=())
+		self.thread.start()
+
+		# set a callback to handle when the window is closed
+		self.root.wm_title("PyImageSearch PhotoBooth")
+		self.root.wm_protocol("WM_DELETE_WINDOW", self.onClose)
+	
+	def clear(self):
+		for widget in self.root.winfo_children():
+			widget.destroy()
+
+	def bayer_convert(self,bayer):
+		if(COLOR):
+			ow = (bayer.shape[0]//4) * 4
+			oh = (bayer.shape[1]//4) * 4
+
+			R  = bayer[0::2, 0::2]
+			B  = bayer[1::2, 1::2]
+			G0 = bayer[0::2, 1::2]
+			G1 = bayer[1::2, 0::2]
+			G = G0[:oh,:ow]//2 + G1[:oh,:ow]//2
+			bayer_R = np.array(R, dtype=np.uint8).reshape(512, 640)
+			bayer_G = np.array(G, dtype=np.uint8).reshape(512, 640)
+			bayer_B = np.array(B, dtype=np.uint8).reshape(512, 640)
+
+			dStack = np.clip(np.dstack((bayer_B*(BRIGHTNESS/5),bayer_G*(BRIGHTNESS/5),bayer_R*(BRIGHTNESS/5))),0,255).astype('uint8')
+		else:
+			bayer_T = np.array(bayer, dtype=np.uint8).reshape(512, 640)
+			dStack = np.clip((np.dstack(((0.469 + bayer_T*0.75 - (bayer_T^2)*0.003)*(BRIGHTNESS/5),(bayer_T*0.95)*(BRIGHTNESS/5),(0.389 + bayer_T*1.34 - (bayer_T^2)*0.004)*(BRIGHTNESS/5)))),0,255).astype('uint8')
+		#dStack = np.clip(np.dstack((bayer,bayer,bayer)),0,255).astype('uint8')
+		return dStack
+
+	def init_camera(self):
+		ns = locate_ns(host="camacholab.ee.byu.edu")
+		objs = str(ns.list())
+		while(True):
+			temp_str = objs[objs.find('\'')+1:-1]
+			temp_obj = temp_str[0:temp_str.find('\'')]
+			if(temp_obj[0:5] ==  "UC480"):
+				temp_ser_no = int(temp_obj[6:len(temp_obj)])
+				if(temp_ser_no == SER_NUMBER):
+					cam_str = temp_obj 
+			if(objs.find(',') == -1):
+				break
+			objs = objs[objs.find(',')+1:-1]
+		try:
+			cam_str
+		except NameError:
+			raise Exception("Camera with serial number " + str(SER_NUMBER) + " could not be found")
+
+		self.cam = Proxy(ns.lookup(cam_str))
+
+		self.cam.start(exposure=65)
+		ip_address = self.cam.start_capture(COLOR)
+		print("IP: " + str(ip_address))
+		self.clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+		self.clientsocket.connect((str(ip_address), PORT))
+
+		now = datetime.now()
+		dt_string = now.strftime("rec_"+str(SER_NUMBER)+"/%Y-%m-%d_%H-%M-%S.avi")
+		fourcc = cv2.VideoWriter_fourcc(*'XVID')
+		self.out = cv2.VideoWriter(dt_string,fourcc, 4.0, (640,512))
+
+		self.time_s = 0
+		self.frame_rate = 0
+		self.count = -1
+		self.vid = cv2.VideoCapture("test.mp4")
+
+	def videoLoop(self):
+		root = tki.Tk()
+		while(True):
+			self.clear()
+			if(self.count >= 0):
+				t = time.time() - self.time_s
+				self.frame_rate = (self.frame_rate*self.count + 1/t)/(self.count + 1)   
+
+			self.time_s = time.time()
+			self.count = self.count + 1
+
+			msg = b''
+			new_msg = True
+			msg_len = None
+			imList = None
+			while True:
+				if new_msg:
+					sub_msg = self.clientsocket.recv(HEADERSIZE)
+					msg_len = int((sub_msg[:HEADERSIZE]))
+					new_msg = False
+				else:
+					sub_msg = self.clientsocket.recv(32800)
+					msg += sub_msg
+					if len(msg) == msg_len:
+						imList = pickle.loads(msg)
+						break
+						
+			dStack = self.bayer_convert(imList)
+
+			#frame = Image.fromarray(dStack)
+			self.out.write(dStack)
+			
+			ret, frame = self.vid.read()
+			print(ret)
+
+			img_1 = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
+			image_mid = Image.fromarray(img_1)
+			image = ImageTk.PhotoImage(image=image_mid)
+			image.image=image_mid
+
+			# if the panel is not None, we need to initialize it
+			if self.panel is None:
+				self.panel = tki.Label(image=image)
+				self.panel.image = image
+				self.panel.pack(side="left", padx=10, pady=10)
+	
+			# otherwise, simply update the panel
+			else:
+				self.panel.configure(image=image)
+				self.panel.image = image
+
+			cv2.imshow('scope',dStack)
+			keyCode = cv2.waitKey(1)
+			
+			if cv2.getWindowProperty('scope',cv2.WND_PROP_VISIBLE) < 1:
+				self.clientsocket.send(b'b')     
+				break
+			self.clientsocket.send(b'g')
+    
+	def takeSnapshot(self):
+		# grab the current timestamp and use it to construct the
+		# output path
+		ts = datetime.datetime.now()
+		filename = "{}.jpg".format(ts.strftime("%Y-%m-%d_%H-%M-%S"))
+		p = os.path.sep.join((self.outputPath, filename))
+
+		# save the file
+		cv2.imwrite(p, self.frame.copy())
+		print("[INFO] saved {}".format(filename))
+    
+	def onClose(self):
+		# set the stop event, cleanup the camera, and allow the rest of
+		# the quit process to continue
+		print("[INFO] closing...")
+		self.stopEvent.set()
+		self.root.quit()
\ No newline at end of file
diff --git a/examples/906.uc480-camera/gui_dev2.py b/examples/906.uc480-camera/gui_dev2.py
new file mode 100644
index 0000000..3ff73a8
--- /dev/null
+++ b/examples/906.uc480-camera/gui_dev2.py
@@ -0,0 +1,9 @@
+# import the necessary packages
+from __future__ import print_function
+from gui_dev import PhotoBoothApp
+from imutils.video import VideoStream
+import argparse
+import time
+
+# start the app
+pba = PhotoBoothApp()
\ No newline at end of file
diff --git a/examples/906.uc480-camera/uc480_local.py b/examples/906.uc480-camera/uc480_local.py
new file mode 100644
index 0000000..f3c2088
--- /dev/null
+++ b/examples/906.uc480-camera/uc480_local.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © PyroLab Project Contributors
+# Licensed under the terms of the GNU GPLv3+ License
+# (see pyrolab/__init__.py for details)
+
+"""
+-------------------------------------------------------------------------------
+Example to connect to and stream a live video from a Thorlabs UC480 microscope
+locally.
+Author: David Hill (https://github.com/hillda3141)
+Modified: 5/4/2021
+Repo: https://github.com/BYUCamachoLab/pyrolab/examples/camera-live
+"""
+
+from pyrolab.drivers.cameras.uc480 import UC480
+import numpy as np
+import cv2
+import time
+import pickle
+
+BRIGHTNESS = 5  #be able to make the recieved video brighter or darker, this
+                #does nothing to the raw data, it is post-processing,
+                #range: 0-10, 5 is no change
+PIXEL_MAX = 255 #the maximum number that a pixel can hold (255 at bit depth 8)
+EXPOSURE = 65   #the exposure that the camera uses, this actually changes the
+                #raw camera data by changing the exposure time of the sensor
+SER_NUMBER = 4103247225 #serial number of the Thorlabs camera
+COLOR = True   #should the camera transmit colored data or grayscale?
+MASK_ON = True
+mask = np.arange(0)
+
+def _generate_mask(roi,num_cir=4):
+    max_rx = roi[0]/4
+    max_ry = roi[1]/4
+    max_r = min(max_rx,max_ry)
+
+    x, y = np.indices((int(roi[0]/2), int(roi[1]/2)))
+    array = np.zeros((int(roi[0]/2), int(roi[1]/2)))
+
+    rads = (max_r/num_cir*np.arange(1,num_cir+1)).astype(int)
+
+    for r in rads:
+        array = np.logical_or(array,np.abs(np.hypot(max_rx - x, max_ry - y)-r) < 0.5)
+    array = np.logical_or(array,x == round(max_rx))
+    array = np.logical_or(array,y == round(max_ry))
+    
+    return array.astype(int)
+
+def bayer_convert(bayer):
+    """
+    Coverts the raw data to something that can be displayed on-screen.
+
+    Parameters
+    ----------
+    bayer : numpy array
+        this is the raw data that is recieved from the camera    
+    """
+    global mask
+
+    if(COLOR):  #if the data is in color, put the numpy array into BGR form
+        if(not mask.any()):
+            mask = _generate_mask(bayer.shape,4)
+
+        frame_height = bayer.shape[0]//2
+        frame_width = bayer.shape[1]//2
+
+        ow = (bayer.shape[0]//4) * 4
+        oh = (bayer.shape[1]//4) * 4
+
+        R  = bayer[0::2, 0::2]
+        B  = bayer[1::2, 1::2]
+        G0 = bayer[0::2, 1::2]
+        G1 = bayer[1::2, 0::2]
+        G = G0[:oh,:ow]//2 + G1[:oh,:ow]//2
+
+        bayer_R = np.array(R, dtype=np.uint8).reshape(frame_height, frame_width)
+        bayer_G = np.array(G, dtype=np.uint8).reshape(frame_height, frame_width)
+        bayer_B = np.array(B, dtype=np.uint8).reshape(frame_height, frame_width)
+
+        if(MASK_ON == True):
+            idx=(mask==1)
+            bayer_R[idx]=255
+            bayer_G[idx]=255
+            bayer_B[idx]=255
+
+        dStack = np.clip(np.dstack((bayer_B*(BRIGHTNESS/5),bayer_G*(BRIGHTNESS/5),
+                         bayer_R*(BRIGHTNESS/5))),0,PIXEL_MAX).astype('uint8')
+    else:   #if the data is grayscale, put it into BGR form
+        if(not mask.any()):
+            mask = _generate_mask((int(bayer.shape[0]*2),int(bayer.shape[1]*2)))
+
+        frame_height = bayer.shape[0]
+        frame_width = bayer.shape[1]
+        bayer_T = np.array(bayer, dtype=np.uint8).reshape(frame_height,
+                           frame_width)
+
+        if(MASK_ON == True):
+            idx=(mask==1)
+            bayer_T[idx]=255
+
+        dStack = np.clip((np.dstack(((bayer_T)*(BRIGHTNESS/5),
+                         (bayer_T)*(BRIGHTNESS/5),(bayer_T)*(BRIGHTNESS/5)))),0,
+                         PIXEL_MAX).astype('uint8')
+    return dStack
+
+    
+cam = UC480(ser_no=SER_NUMBER,exposure=EXPOSURE)    #start a UC480 object with
+                                                    #the correct serial number
+                                                    #and exposure time
+
+cam.start_capture(COLOR,True)   #start pulling frames from the camera
+time.sleep(0.5) #wait for the camera to start the capture
+
+while(True):    #loop to get frames
+    t_init = time.time()
+    dStack = bayer_convert(cam.get_frame()) #get most recent frame
+    cv2.imshow('UC480_1',dStack)  #paint it to the cv2 window
+    keyCode = cv2.waitKey(1)
+    
+    #if window is exited, break from the loop
+    if cv2.getWindowProperty('UC480_1',cv2.WND_PROP_VISIBLE) < 1:
+        break
+    # time.sleep(0.5)
+    pfs = 1.0/(time.time()-t_init)
+    print("pfs: " + str(pfs))
+
+cam.close() #close the camera connection
diff --git a/pyrolab/drivers/cameras/__init__.py b/pyrolab/drivers/cameras/__init__.py
new file mode 100644
index 0000000..307ce10
--- /dev/null
+++ b/pyrolab/drivers/cameras/__init__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © PyroLab Project Contributors
+# Licensed under the terms of the GNU GPLv3+ License
+# (see pyrolab/__init__.py for details)
+
+"""
+Cameras
+-------
+
+Submodule containing drivers for each supported camera type.
+"""
+
+from pyrolab.drivers import Instrument
+
+class Camera(Instrument):
+    pass
diff --git a/pyrolab/drivers/cameras/uc480.py b/pyrolab/drivers/cameras/uc480.py
new file mode 100644
index 0000000..5e4cd83
--- /dev/null
+++ b/pyrolab/drivers/cameras/uc480.py
@@ -0,0 +1,389 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © PyroLab Project Contributors
+# Licensed under the terms of the GNU GPLv3+ License
+# (see pyrolab/__init__.py for details)
+
+"""
+-------------------------------------------------------------------------------
+Driver for a Thorlabs Microscope.
+Author: David Hill (https://github.com/hillda3141)
+Repo: https://github.com/BYUCamachoLab/pyrolab/pyrolab/drivers/cameras
+"""
+
+from Pyro5.errors import PyroError
+from pyrolab.api import expose
+from thorlabs_kinesis import thor_camera as tc
+import socket
+import pickle
+import threading
+import numpy as np
+from ctypes import *
+
+@expose
+class UC480:
+    """
+    Attributes
+    ----------
+    HEADERSIZE : int
+        the size of the header used to communicate the size of the message
+        (10 bytes is a safe size)
+    """
+    HEADERSIZE = 10
+
+    def __init__(self, ser_no, port=2222, bit_depth=8, camera="ThorCam FS",
+                 pixel_clock=24, color_mode=11, roi_shape=(1024, 1280), 
+                 roi_pos=(0,0), framerate=15, exposure=90, pixelbytes=8):
+        """
+        Opens the serial communication with the Thorlabs camera and sets
+        some low-level values, including the bit depth and camera name.
+
+        Parameters
+        ----------
+        ser_no : long
+            the serial number of the camera that should be initiated
+        port : int
+            the port on which the socket transmits the video feed to the client
+        bit_depth : int
+            the number of bits used for each pixel (usually is 8)
+        camera: string
+            camera name
+        pixel_clock: int
+            clock speed of the camera
+        color_mode: int
+            mode of color that the camera returns data in. 11 returns raw format:
+            | R  G0 |...
+            | G1  B |...
+              .   .
+              .   .
+              .   .
+        roi_shape : array(int)
+            dimentions of the image that is taken by the camera (usually 1024 x 1280)
+        roi_pos : array(int)
+            position of the top left corner of the roi (region of interest) in
+            relation to the sensor array (usaually 0,0)
+        framerate : int
+            the framerate of the camera in frames per second
+        exposure: int
+            in milliseconds, the time the shutter is open on the camera (90 default)
+        pixelbytes: int
+            the amount of memory space allocated per pixel in bytes
+        """
+        num = c_int(0)
+        tc.GetNumberOfCameras(byref(num))
+        print(num)
+        uci_format = tc.UC480_CAMERA_INFO * 2
+        uci = uci_format(tc.UC480_CAMERA_INFO(), tc.UC480_CAMERA_INFO())
+        dwCount = c_int(num.value)
+        cam_list = tc.UC480_CAMERA_LIST(dwCount=dwCount,uci=uci)
+        tc.GetCameraList(byref(cam_list))
+        for i in range(num.value):
+            handle = c_int(cam_list.uci[i].dwCameraID)
+            temp = tc.InitCamera(byref(handle))
+            if(temp != 0):
+                continue
+            info = tc.CAMINFO()
+            out = tc.GetCameraInfo(handle,byref(info))
+            if(int(info.SerNo) == ser_no):
+                self.handle = handle
+                break
+            elif(i == num.value - 1):
+                raise PyroError("Camera not found")
+            else:
+                i = tc.ExitCamera(handle)
+                if i != 0:
+                    raise PyroError("Closing ThorCam failed with error code "+str(i))
+
+        self.bit_depth = bit_depth
+        self.camera = camera
+
+        #print("Trigger Mode: " + str(tc.SetTrigger(handle,tc.IS_SET_TRIGGER_SOFTWARE)))
+        print("Trigger Mode: " + str(tc.SetTrigger(self.handle,tc.IS_GET_EXTERNALTRIGGER)))
+   
+        tc.SetDisplayMode(self.handle, c_int(32768))
+
+        # if i  0:
+        #     print("shee")
+        #     raise PyroError("Opening the ThorCam failed with error code "+str(i))
+
+        self.meminfo = None
+
+        self.set_pixel_clock(pixel_clock)
+        self.set_color_mode(color_mode)
+        self.set_roi_shape(roi_shape)
+        self.set_roi_pos(roi_pos)
+        self.set_framerate(framerate)
+        self.set_exposure(exposure)
+        self.initialize_memory(pixelbytes)  
+        self.port = port
+        print("initialized")
+
+    def _get_image(self):
+        """
+        Retrieves the last frame from the memory buffer, processes
+        it into a gray-scale image, and serializes it using the pickle
+        library. The header is then added to inform the client how long
+        the message is. This should not be called from the client. It
+        will be called from the function _video_loop() which is on a
+        parrallel thread with Pyro5.
+        """
+        bayer = np.frombuffer(self.meminfo[0], c_ubyte).reshape(self.roi_shape[1],
+        self.roi_shape[0])
+        
+        if(self.color == False):
+            ow = (bayer.shape[0]//4) * 4
+            oh = (bayer.shape[1]//4) * 4
+
+            R  = bayer[0::2, 0::2]
+            B  = bayer[1::2, 1::2]
+            G0 = bayer[0::2, 1::2]
+            G1 = bayer[1::2, 0::2]
+
+            GRAY = R[:oh,:ow]//3 + B[:oh,:ow]//3 + (G0[:oh,:ow]//2 + G1[:oh,:ow]//2)//3
+
+            if(self.local == True):
+                return GRAY
+            else:
+                msg = pickle.dumps(GRAY)
+                msg = bytes(f'{len(msg):<{self.HEADERSIZE}}', "utf-8") + msg
+                return msg
+        else:
+            if(self.local == True):
+                return bayer
+            else:
+                msg = pickle.dumps(bayer)
+                msg = bytes(f'{len(msg):<{self.HEADERSIZE}}', "utf-8") + msg
+                return msg
+            
+
+    def _video_loop(self):
+        """
+        This function is called as a seperate thread when streaming is initiated.
+        It will loop, sending frame by frame accross the socket connection,
+        until the threading.Event() stop_video is triggered.
+        """
+        while not self.stop_video.is_set():
+            bad = False
+            if(self.local == False):
+                if bad == False:
+                    if self.start_socket==True:
+                        while True:
+                            self.serversocket = socket.socket(socket.AF_INET,
+                                                              socket.SOCK_STREAM)
+                            self.serversocket.bind((socket.gethostname(), self.port))
+                            self.serversocket.listen(5)
+                            self.clientsocket, address = self.serversocket.accept()
+                            self.start_socket = False
+                            break
+                    msg = self._get_image()
+                    self.clientsocket.send(msg)
+                    while True:
+                        check_msg = self.clientsocket.recv(2)
+                        if(check_msg == b'b'):
+                            bad = True
+                        break
+            else:
+                break
+            
+    def get_frame(self):
+        """
+        Returns the most recently updated frame from the camera. Only use if the
+        program is connecting to a local camera.   
+        """
+        if(self.local == True):
+            return self._get_image()
+
+        
+    def set_pixel_clock(self, clockspeed):
+        """
+        Sets the clockspeed of the camera, usually in the range of 24.
+
+        Parameters
+        ----------
+        clockspeed : int
+            clock speed of the camera       
+        """
+
+        pixelclock = c_uint(clockspeed)
+        i = tc.PixelClock(self.handle, 6, byref(pixelclock), sizeof(pixelclock))
+
+    def start_capture(self,color=False,local=True):
+        """
+        This starts the capture from the camera to the allocated
+        memory location as well as starts a new parallel thread
+        for the socket server to stream from memory to the client.
+
+        Parameters
+        ----------
+        color : boolean
+            whether the video should be sent in full color or grayscale
+        """
+        self.color = color
+        self.local = local
+        tc.StartCapture(self.handle, tc.IS_DONT_WAIT)
+        ip_address = socket.gethostbyname(socket.gethostname())
+        self.start_socket = True
+        self.stop_video = threading.Event()
+        self.video_thread = threading.Thread(target=self._video_loop, args=())
+        self.video_thread.start()
+        return ip_address
+    
+    def color_gray(self,color=False):
+        """
+        This function switches whether the socket transmits color or grayscale.
+        Parameters
+        ----------
+        color : boolean
+            whether the video should be sent in full color or grayscale
+        """
+        self.color = color
+
+    def stop_capture(self):
+        """
+        This frees the memory used for storing the frames then triggers
+        the stop_video event which will end the parrallel socket thread.
+        """
+        
+        tc.FreeMemory(self.handle, self.meminfo[0], self.meminfo[1])
+        tc.StopCapture(self.handle, 1)
+        if(self.local == False):
+            self.clientsocket.close()
+        self.stop_video.set()
+        
+    def initialize_memory(self, pixelbytes=8):
+        """
+        Initializes the memory for holding the most recent frame from the camera.
+
+        Parameters
+        ----------
+        pixelbytes: int
+            the amount of memory space allocated per pixel in bytes
+        """
+        
+        if self.meminfo != None:
+            tc.FreeMemory(self.handle, self.meminfo[0], self.meminfo[1])
+        
+        xdim = self.roi_shape[0]
+        ydim = self.roi_shape[1]
+        imagesize = xdim*ydim
+            
+        memid = c_int(0)
+        c_buf = (c_ubyte * imagesize)(0)
+
+        tc.AllocateMemory(self.handle, xdim, ydim, c_int(pixelbytes), c_buf,
+                          byref(memid))
+        tc.SetImageMemory(self.handle, c_buf, memid)
+        self.meminfo = [c_buf, memid]
+    
+    def set_exposure(self, exposure):
+        """
+        This sets the exposure of the camera.
+
+        Parameters
+        ----------
+        exposure: int
+            in milliseconds, the time the shutter is open on the camera (90 is
+            a good exposure value)
+        """
+        
+        exposure_c = c_double(exposure)
+        tc.SetExposure(self.handle, 12 , exposure_c, sizeof(exposure_c))
+        self.exposure = exposure_c.value
+    
+    def set_framerate(self, framerate):
+        """
+        Sets the framerate of the camera (fps). After calling
+        this function you must reset the exposure.
+
+        Parameters
+        ----------
+        framerate: int
+            the framerate of the camera in frames per second
+        """
+            
+        s_framerate = c_double(0)
+        tc.SetFrameRate(self.handle, c_double(framerate), byref(s_framerate))
+        self.framerate = s_framerate.value
+
+    def set_color_mode(self, mode=11):
+        """
+        This sets the mode of image that is taken, almost always
+        use 11 which will give you the raw photosensor data in the format:
+            | R  G0 |...
+            | G1  B |...
+              .   .
+              .   .
+              .   .
+        This data is interpreted in the _get_image() function.
+        Parameters
+        ----------
+        mode: int
+            the color mode of the pixel data
+        """
+        
+        i = tc.SetColorMode(self.handle, mode) #11 means raw 8-bit, 6 means
+                                               #gray 8-bit
+
+    def set_roi_shape(self, roi_shape):
+        """
+        Sets the dimmentions of the region of interest (roi)
+
+        Parameters
+        ----------
+        roi_shape : (int,int)
+            dimentions of the image that is taken by the camera (usually 1024 x 1280)
+        """
+        
+        AOI_size = tc.IS_2D(roi_shape[0], roi_shape[1]) #Width and Height
+            
+        i = tc.AOI(self.handle, 5, byref(AOI_size), 8)#5 for setting size,
+                                                      #3 for setting position
+        tc.AOI(self.handle, 6, byref(AOI_size), 8)#6 for getting size
+                                                  #4 for getting position
+        self.roi_shape = [AOI_size.s32X, AOI_size.s32Y]
+
+    def set_roi_pos(self, roi_pos):
+        """
+        Sets the origin position of the region of interest
+
+        Parameters
+        ----------
+        roi_pos : int x int
+            position of the top left corner of the roi (region of interest) in
+            relation to the sensor array (usaually 0,0)
+        """
+        
+        AOI_pos = tc.IS_2D(roi_pos[0], roi_pos[1]) #Width and Height
+            
+        i = tc.AOI(self.handle, 3, byref(AOI_pos), 8 )#5 for setting size
+                                                      #3 for setting position
+        tc.AOI(self.handle, 4, byref(AOI_pos), 8 )#6 for getting size
+                                                  #4 for getting position
+        self.roi_pos = [AOI_pos.s32X, AOI_pos.s32Y]
+
+    def close(self):
+        """
+        Calls self.stop_capture to free memory and end the socket server
+        and then closes serial communication with the camera.
+
+        Raises
+        ------
+        PyroError("Closing ThorCam failed with error code "+str(i))
+            Error to signal that the connection to the camera was closed abruptly
+            or another error was thrown upon closing (usually is ignorable)
+        """
+        
+        try:
+            self.handle
+        except AttributeError:
+            return
+        self.stop_capture()
+        i = tc.ExitCamera(self.handle) 
+        if i != 0:
+            raise PyroError("Closing ThorCam failed with error code "+str(i))
+
+    def __del__(self):
+        """"
+        Function called when Proxy connection is lost.
+        """
+        self.close()
diff --git a/pyrolab/drivers/firmata/__init__.py b/pyrolab/drivers/firmata/__init__.py
new file mode 100644
index 0000000..08a1386
--- /dev/null
+++ b/pyrolab/drivers/firmata/__init__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © PyroLab Project Contributors
+# Licensed under the terms of the GNU GPLv3+ License
+# (see pyrolab/__init__.py for details)
+
+"""
+Firmata
+-------
+
+Submodule containing drivers for each supported arduino type.
+"""
+
+from pyrolab.drivers import Instrument
+
+class Firmata(Instrument):
+    pass
diff --git a/pyrolab/drivers/lasers/ppcl550.py b/pyrolab/drivers/lasers/ppcl550.py
new file mode 100644
index 0000000..0837a82
--- /dev/null
+++ b/pyrolab/drivers/lasers/ppcl550.py
@@ -0,0 +1,435 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © PyroLab Project Contributors
+# Licensed under the terms of the GNU GPLv3+ License
+# (see pyrolab/__init__.py for details)
+
+"""
+Pure Photonics Tunable Laser 5xx (specifically designed for PPCL550 and PPCL551)
+-----------------------------------------------
+Driver for the Santec PPCL-5xx Tunable Laser.
+Author: David Hill (https://github.com/hillda3141)
+Repo: https://github.com/BYUCamachoLab/pyrolab/pyrolab/drivers/lasers
+Functions
+---------
+    __init__(self,minWL=1515,maxWL=1570,minPow=7,maxPow=13.5,port="COM4",
+            baudrate=9600)
+    setPower(self,power)
+    setChannel(self,channel=1)
+    setMode(self,mode)
+    setWavelength(self,wavelength,jump=0)
+    on(self,pin=13)
+    off(self,pin=13)
+    _communicate(self,register,data,rw)
+    _send(self,msg)
+    _recieve(self)
+    _checksum(self,msg)
+    _wl_freq(self,unit)
+    close(self)
+    __del__(self)
+"""
+
+import serial
+import time
+import threading
+import array
+from Pyro5.errors import PyroError
+from Pyro5.api import expose
+import pyrolab.api
+
+C_SPEED = 299792458
+
+ITLA_NOERROR=0x00
+ITLA_EXERROR=0x01
+ITLA_AEERROR=0x02
+ITLA_CPERROR=0x03
+ITLA_NRERROR=0x04
+ITLA_CSERROR=0x05
+ITLA_ERROR_SERPORT=0x01
+ITLA_ERROR_SERBAUD=0x02
+
+REG_Nop=0x00
+REG_Mfgr=0x02
+REG_Model=0x03
+REG_Serial=0x04
+REG_Release=0x06
+REG_Gencfg=0x08
+REG_AeaEar=0x0B
+REG_Iocap=0x0D
+REG_Ear=0x10
+REG_Dlconfig=0x14
+REG_Dlstatus=0x15
+REG_Channel=0x30
+REG_Power=0x31
+REG_Resena=0x32
+REG_Grid=0x34
+REG_Fcf1=0x35
+REG_Fcf2=0x36
+REG_Oop=0x42
+REG_Opsl=0x50
+REG_Opsh=0x51
+REG_Lfl1=0x52
+REG_Lfl2=0x53
+REG_Lfh1=0x54
+REG_Lfh2=0x55
+REG_Currents=0x57
+REG_Temps=0x58
+REG_Ftf=0x62
+REG_Mode=0x90
+REG_PW=0xE0
+REG_Csweepsena=0xE5
+REG_Csweepamp=0xE4
+REG_Cscanamp=0xE4
+REG_Cscanon=0xE5
+REG_Csweepon=0xE5
+REG_Csweepoffset=0xE6
+REG_Cscanoffset=0xE6
+REG_Cscansled=0xF0
+REG_Cscanf1=0xF1
+REG_Cscanf2=0xF2
+REG_CjumpTHz=0xEA
+REG_CjumpGHz=0xEB
+REG_CjumpSled=0xEC
+REG_Cjumpon=0xED
+REG_Cjumpoffset=0xE6
+
+READ=0
+WRITE=1
+
+@expose
+class PPCL55x:
+
+    def __init__(self,minWL=1515,maxWL=1570,minPow=6,maxPow=13.5,port="COM4",
+            baudrate=9600):
+        """"
+        Initialize limiting values for the laser.
+
+        Parameters
+        ----------
+        minWL : double
+            Minimum wavelength the laser will produce in nanometers.
+        maxWL : double
+            Maximum wavelength the laser will produce in nanometers.
+        minPow : double
+            Minimum power level of the laser in dBm
+        maxPow : double
+            Maximum power level of the laser in dBm
+        port : str
+            COM port the laser is connected to (e.g. "COM4")
+        baudrate : int
+            baudrate of the serial connection default is 9600
+        """
+        self.minWavelength = minWL
+        self.maxWavelength = maxWL
+        self.minPower = minPow
+        self.maxPower = maxPow
+        self.port = port
+        self._error=ITLA_NOERROR
+        self.latestregister = 0
+        self.queue = []
+        self.maxrowticket = 0
+        self.powerState = 0
+
+        try:
+            self.lasercom = serial.Serial(self.port,baudrate,timeout=1,
+                parity=serial.PARITY_NONE) #attempt connection with given baudrate
+        except serial.SerialException:
+            raise IOError("Serial Connection Error")
+        baudrate2=4800
+        #if the initial connection doesn't work try different baudrates
+        while baudrate2<115200: 
+            back = self._communicate(REG_Nop,0,0)
+            if back != ITLA_NOERROR:
+                #go to next baudrate
+                if baudrate2==4800: baudrate2=9600
+                elif baudrate2==9600: baudrate2=19200
+                elif baudrate2==19200: baudrate2=38400
+                elif baudrate2==38400: baudrate2=57600
+                elif baudrate2==57600: baudrate2=115200
+                self.lasercom.close()
+                self.lasercom = serial.Serial(self.port,baudrate2,timeout=None,
+                    parity=serial.PARITY_NONE)            
+            else:
+                return
+        print(baudrate2)
+        self.lasercom.close()
+        raise IOError("Serial Connection Error")
+
+    def setPower(self,power):
+        """"
+        Set the power on the laser.
+
+        Parameters
+        ----------
+        power : double
+            Power that the laser will be set to in dBm
+        """
+
+        sendPower = int(power*100)  #scale the power inputed
+        #on the REG_Power register, send the power
+        back = self._communicate(REG_Power,sendPower,1)  
+        return back
+
+
+    def setChannel(self,channel=1):
+        """"
+        Set the channel (should always be 1)
+
+        Parameters
+        ----------
+        channel : int
+            channel that the laser is on
+        """
+        #on the REG_Channel register, send the channel
+        back = self._communicate(REG_Channel,channel,1)  
+        return back
+
+    
+    def setMode(self,mode):
+        """
+        Set the mode of operation for the laser
+
+        Parameters
+        ----------
+        mode : int
+            Mode for the laser:
+            0 - regular mode
+            1 - no dither mode
+            2 - clean mode
+        """
+        #on the REG_Mode register, send the mode
+        back = self._communicate(REG_Mode,mode,1)
+        return back
+
+
+    def setWavelength(self,wavelength):
+        """
+        Set the wavelength of the laser. Laser must be off in order to set the 
+        wavelength. If laser is not off this function will turn it off and then 
+        back on.
+
+        Parameters
+        ----------
+        wavelength : double
+            Wavelength of the laser
+        """
+
+        init_time = time.time()
+        #if the wavelength is not in the allowed range
+        if(wavelength < self.minWavelength or wavelength > self.maxWavelength):
+                return "wavelength not in range"
+        freq = self._wl_freq(wavelength)
+        freq_t = int(freq/1000)
+        #convert the wavelength to frequency for each register
+        freq_g = int(freq*10) - freq_t*10000    
+        # print(freq_t)
+        # print(freq_g)
+
+        if self.powerState:   #if the laser is currently on
+            self.off()
+            back = self._communicate(REG_Fcf1,freq_t,1)
+            if(back == ITLA_NOERROR):
+                #write the new wavelength to the REG_Fcf2 register
+                back = self._communicate(REG_Fcf2,freq_g,1)  
+            time_diff = time.time() - init_time
+            # print(time_diff)
+            self.on()
+            return back
+        else:
+            back = self._communicate(REG_Fcf1,freq_t,1)
+            if(back == ITLA_NOERROR):
+                #write the new wavelength to the REG_Fcf2 register
+                back = self._communicate(REG_Fcf2,freq_g,1)
+            time_diff = time.time() - init_time
+            # print(time_diff)
+            return back
+    
+
+    def on(self):
+        """
+        Turn on the laser
+        """
+        #start communication by sending 8 to REG_Resena register
+        back = self._communicate(REG_Resena,8,1) 
+        for x in range(10):
+            #send 0 to REG_Nop register to wait for a "ready" response
+            back = self._communicate(REG_Nop,0,0)   
+        self.powerState = 1
+        return back
+    
+
+    def off(self):
+        """
+        Turn off the laser
+        """
+        #stop communication by sending 0 to REG_Resena register
+        back = self._communicate(REG_Resena,0,1) 
+        self.powerState = 0
+        return back
+
+
+    def _communicate(self,register,data,rw):
+        """
+        Function that implements the commmunication with the laser. It will 
+        first send a message then recieve a response.
+
+        Parameters
+        ----------
+        register : byte
+            Register to which will be written. Each laser function has its own 
+            register.
+        data : byte
+            User-specific data that will be sent to the laser
+        rw : int
+            Defines if the communication is read-write or only write
+            0 : write only
+            1 : write then read
+        """
+
+        lock = threading.Lock()
+        lock.acquire()
+        rowticket = self.maxrowticket + 1
+        self.maxrowticket = self.maxrowticket + 1
+        self.queue.append(rowticket)
+        lock.release()
+        while self.queue[0] != rowticket:
+            rowticket=rowticket
+        if rw == 0:     #if write and then read
+            byte2 = int(data/256)
+            byte3 = int(data - byte2*256)
+            self.latestregister = register      #modify bytes for sending
+            msg = [rw,register,byte2,byte3]
+            msg[0] = msg[0] | int(self._checksum(msg))*16    #calculate checksum
+            self._send(msg)  #send the message
+            recvmsg = self._recieve()    #recieve the response from the laser
+            #print(recvmsg)
+            datamsg = recvmsg[2]*256 + recvmsg[3]
+            #if the message is larger than 4 bytes, read it using AEA method (not implemented)
+            if (recvmsg[0] & 0x03) == 0x02:
+                extmsg = self.AEA(datamsg)  #not implemented
+                lock.acquire()
+                self.queue.pop(0)
+                lock.release()
+                return extmsg
+            lock.acquire()
+            self.queue.pop(0)
+            lock.release()
+            errorMsg = int(recvmsg[0] & 0x03)
+            return(errorMsg)
+        else:   #if only write
+            byte2=int(data/256)
+            byte3=int(data - byte2*256)
+            msg = [rw,register,byte2,byte3]
+            msg[0] = msg[0] | int(self._checksum(msg))*16    #construct message and send
+            self._send(msg)
+            recvmsg = self._recieve()    #recieve message
+            # print("recieved")
+            lock.acquire()
+            self.queue.pop(0)
+            lock.release()
+            errorMsg = int(recvmsg[0] & 0x03)
+            return(errorMsg)
+
+    """
+    Function sends message of four bytes to the laser.
+    """
+    def _send(self,msg):
+        """
+        Sends message of four bytes to the laser
+
+        Parameters
+        ----------
+        msg : 4 x 1 bytes
+            Message that will be sent to the laser
+        """
+
+        self.lasercom.flush()
+        #construct the bytes from the inputed message
+        sendBytes = array.array('B',msg).tobytes()  
+        self.lasercom.write(sendBytes)  #write the bytes on the serial connection
+
+
+    def _recieve(self):
+        """
+        Recieves, decifers, and certifies message from the laser
+        
+        Raises
+        ------
+        PyroError(f"SerialCommunicationFailure queue[0] = {self.queue[0]}")
+            Error to signal that the laser did not respond or the response was 
+            not long enough.
+        PyroError("ChecksumError")
+            Error to signal that the recieved message had an irregular checksum.
+        """
+
+        reftime = time.time()
+        while self.lasercom.inWaiting()<4:  #wait until 4 bytes are recieved
+            if(time.time() > reftime + 0.5): #if it takes longer than 0.25 seconds break
+                self._error=ITLA_NRERROR
+                return(0xFF,0xFF,0xFF,0xFF)
+            time.sleep(0.0001)
+        try:
+            num = self.lasercom.inWaiting() #get number of bytes for debugging purposes
+            msg = []
+            bAll = self.lasercom.read(num)  #read the four bytes from serial
+            for b in bAll:
+                msg.append(b)   #construct array of bytes from the message
+            msg = msg[0:4]
+        except:
+            raise PyroError(f"SerialCommunicationFailure queue[0] = {self.queue[0]}")
+            msg = [0xFF,0xFF,0xFF,0xFF]
+        if self._checksum(msg) == msg[0] >> 4: # ensure the checksum is correct
+            self._error = msg[0] & 0x03
+            return(msg) #return the message recieved
+        else:
+            #if the checksum is wrong, log a CS error
+            raise PyroError("ChecksumError")  
+            self._error=ITLA_CSERROR
+            return(msg)  
+
+    """
+    Function calculates the checksum.
+    """
+    def _checksum(self,msg):     #calculate checksum
+        """
+        Calculate the checksum of a message
+
+        Parameters
+        ----------
+        msg : 4 x 1 bytes
+            Four-byte message that will be used to produce a checksum
+        """
+
+        bip8=(msg[0] & 0x0f) ^ msg[1] ^ msg[2] ^ msg[3]
+        bip4=((bip8 & 0xf0) >> 4) ^ (bip8 & 0x0f)
+        return bip4
+
+    """
+    Convert from wavelength to frequency
+    """
+    def _wl_freq(self,wl):
+        """
+        Convert from wavelength to frequency using propagation velocity of c
+
+        Parameters
+        ----------
+        wl : double
+            Wavelength in nanometers
+        """
+
+        return C_SPEED/wl
+    
+
+    def close(self):
+        """
+        Disconnect from the laser
+        """
+
+        self.lasercom.close()
+        return 0
+
+    def __del__(self):
+        """"
+        Function called when Proxy connection is lost.
+        """
+        self.close()
\ No newline at end of file
diff --git a/pyrolab/drivers/lasers/sachertec.py b/pyrolab/drivers/lasers/sachertec.py
new file mode 100644
index 0000000..9e1e8bf
--- /dev/null
+++ b/pyrolab/drivers/lasers/sachertec.py
@@ -0,0 +1,42 @@
+import os
+os.environ['PATH'] = ("C:\\Program Files\\SacherLasertechnik\\MotorMotion 2.1\\data"
++ ";" + os.environ['PATH'])
+
+from sacher_tec import epos_motor as epm
+from ctypes import *
+from sacher_tec._utils import c_word, c_dword
+
+name = create_string_buffer(("EPOS2").encode('utf-8'))
+protocol = create_string_buffer(("MAXON SERIAL V2").encode('utf-8'))
+interface = create_string_buffer(("USB").encode('utf-8'))
+port = create_string_buffer(("USB0").encode('utf-8'))
+name_size = c_word(10)
+baudrate = c_dword(0)
+timeout = c_dword(0)
+node_id = c_word(0)
+dialog = c_int(3)
+error = c_dword(0)
+
+out = epm.VCS_OpenDeviceDlg(byref(error))
+
+print(out)
+print(error)
+
+handle = epm.VCS_OpenDevice(name,protocol,interface,port,byref(error))
+print(handle)
+print(error)
+
+out = epm.VCS_SetProtocolStackSettings(handle,baudrate,timeout,byref(error))
+print(out)
+print(error)
+
+out = epm.VCS_FindDeviceCommunicationSettings(byref(handle),byref(name),byref(protocol),byref(interface),byref(port),name_size,byref(baudrate),byref(timeout),byref(node_id),dialog,byref(error))
+                                            # [c_int, c_char_p, c_char_p, c_char_p, c_char_p, c_word, POINTER(c_dword), POINTER(c_dword), POINTER(c_word), POINTER(c_int), POINTER(c_dword)], c_bool)
+print(out)
+print(error)
+print(node_id)
+
+out = epm.VCS_CloseAllDevices(byref(error))
+
+print(out)
+print(error)
\ No newline at end of file
diff --git a/pyrolab/drivers/motion/__init__.py b/pyrolab/drivers/motion/__init__.py
index 430c39e..bbff7d7 100644
--- a/pyrolab/drivers/motion/__init__.py
+++ b/pyrolab/drivers/motion/__init__.py
@@ -5,10 +5,10 @@
 # (see pyrolab/__init__.py for details)
 
 """
-Lasers
+Motion
 ------
 
-Submodule containing drivers for each supported laser type.
+Submodule containing drivers for each supported motor type.
 """
 
 from pyrolab.drivers import Instrument
diff --git a/pyrolab/drivers/scopes/__init__.py b/pyrolab/drivers/scopes/__init__.py
index c2c3722..4747129 100644
--- a/pyrolab/drivers/scopes/__init__.py
+++ b/pyrolab/drivers/scopes/__init__.py
@@ -5,10 +5,10 @@
 # (see pyrolab/__init__.py for details)
 
 """
-Lasers
-------
+Oscilloscopes
+-------------
 
-Submodule containing drivers for each supported laser type.
+Submodule containing drivers for each supported scope type.
 """
 
 from pyrolab.drivers import Instrument
diff --git a/pyrolab/drivers/utils/driverLocker.py b/pyrolab/drivers/utils/driverLocker.py
new file mode 100644
index 0000000..3227ad8
--- /dev/null
+++ b/pyrolab/drivers/utils/driverLocker.py
@@ -0,0 +1,57 @@
+# TODO: DEPRECATED! REMOVE AFTER TESTING COMPATABILITY OF NEW LOCKER.
+
+import os
+from os import path
+import pyfirmata
+import time
+from Pyro5.api import expose
+import pyrolab.api
+
+@expose
+class Locker():
+
+    fileName = ""
+    status = True
+    MASTER_PWD = "override"
+
+    def __init__(self,deviceName):
+        self.fileName = "C:\\LockedDevices\\" + deviceName + "_LOCK.txt"
+
+    def lock(self,pwd=""):
+        exists = self.get_status()
+        if exists == True:
+            return 1
+        else:
+            self.password = pwd
+            f = open(self.fileName,"w")
+            f.write(pwd)
+            f.close()
+            return 1
+
+    def release(self,name=""):
+        exists = self.get_status()
+        if exists == True:
+            f = open(self.fileName,"r")
+            pwd = f.read()
+            f.close()
+            if (name == pwd or name == self.MASTER_PWD):
+                os.remove(self.fileName)
+                return 1
+            else:
+                return 0
+        else:
+            return 1
+
+    def get_status(self):
+        self.status = os.path.exists(self.fileName)
+        return self.status
+    
+    def get_user(self):
+        exists = self.get_status()
+        if exists == True:
+            f = open(self.fileName,"r")
+            name = f.read()
+            f.close()
+            return name
+        else:
+            return ""
\ No newline at end of file
diff --git a/pyrolab/drivers/utils/locker.py b/pyrolab/drivers/utils/locker.py
new file mode 100644
index 0000000..79df591
--- /dev/null
+++ b/pyrolab/drivers/utils/locker.py
@@ -0,0 +1,105 @@
+# TODO: DEPRECATED! REMOVE AFTER TESTING COMPATABILITY OF NEW LOCKER.
+
+# -*- coding: utf-8 -*-
+#
+# Copyright © PyroLab Project Contributors
+# Licensed under the terms of the GNU GPLv3+ License
+# (see pyrolab/__init__.py for details)
+
+"""
+Pyrolab Locker Class
+-----------------------------------------------
+Driver for the Santec TSL-550 Tunable Laser.
+Contributors
+ * David Hill (https://github.com/hillda3141)
+ * Sequoia Ploeg (https://github.com/sequoiap)
+"""
+
+import os
+from os import path
+import time
+from Pyro5.api import expose
+import pyrolab.api
+import tempfile
+
+@expose
+class Locker():
+
+    """
+    This class "locks" a device by creating a text document named after it in
+    the director C:\\LockedDevices. This directory must exist before exectuing
+    this code. The txt file that it creates can be empty (no username given)
+    or have a username as its content. Adding a name is often usefull so others
+    can see who is using the device.
+    """
+
+    fileName = ""
+    MASTER_PWD = "override"
+
+    def __init__(self,deviceName):
+        """
+        Initialize the file name that we are dealing with, depending on the
+        device name.
+        """
+
+        self.file_name = deviceName + "_LOCK"
+
+    def lock(self,user=""):
+        """
+        Create the txt file and write the password inside, unless the file
+        already exhists.
+        """
+        exists = self.get_status()
+        if exists == True:
+            return 1
+        else:
+            temp = tempfile.NamedTemporaryFile(prefix = self.file_name,
+            suffix = user, delete=False)
+            temp.close()
+            return 1
+
+    def release(self,user=""):
+        """
+        If the password that is inputed matches the content of the txt file,
+        delete the file to unlock.
+        """
+        exists = self.get_status()
+        if exists == True:
+            directory = tempfile.gettempdir()
+            prefixed = [filename for filename in os.listdir(tempfile.gettempdir())
+            if filename.startswith(self.file_name)]
+            file_full = prefixed[0]
+            pwd = file_full[len(self.file_name)+8:len(file_full)]
+            if (user == pwd or user == self.MASTER_PWD):
+                del_file = directory + "\\" + file_full
+                os.remove(del_file)
+                return 1
+            else:
+                return 0
+        else:
+            return 1
+
+    def get_status(self):
+        """
+        Return True if the file exists, false otherwise.
+        """
+        lock_status = False
+        prefixed = [filename for filename in os.listdir(tempfile.gettempdir())
+        if filename.startswith(self.file_name)]
+        if(len(prefixed) > 0):
+            lock_status = True
+        return lock_status
+    
+    def get_user(self):
+        """
+        Return the username or contents of the txt file.
+        """
+        exists = self.get_status()
+        prefixed = [filename for filename in os.listdir(tempfile.gettempdir())
+        if filename.startswith(self.file_name)]
+        if(len(prefixed) > 0):
+            file_full = prefixed[0]
+            name = file_full[len(self.file_name)+8:len(file_full)]
+            return name
+        else:
+            return ""
diff --git a/pyrolab/server/locker.py b/pyrolab/server/locker.py
index 893578b..f5558f6 100644
--- a/pyrolab/server/locker.py
+++ b/pyrolab/server/locker.py
@@ -89,10 +89,3 @@ def create_lockable(cls) -> Instrument:
         {}
     )
     return DynamicLockable
-
-
-# if __name__ == "__main__":
-#     from pyrolab.drivers.sample import SampleService
-
-#     ss = create_lockable(SampleService)
-#     l = ss()


Test patch:
diff --git a/examples/904.sacher-tec/st_test.py b/examples/904.sacher-tec/st_test.py
new file mode 100644
index 0000000..2cab7dc
--- /dev/null
+++ b/examples/904.sacher-tec/st_test.py
@@ -0,0 +1,218 @@
+import os
+import time
+import math
+
+from ctypes import *
+
+from sacher_tec._utils import (
+    c_word,
+    c_dword,
+    bind,
+    not_implemented,
+)
+
+os.environ['PATH'] = ("C:\\Program Files\\SacherLasertechnik\\MotorMotion 2.1\\data"
++ ";" + os.environ['PATH'])
+
+epos = cdll.LoadLibrary("EposCmd64.dll")
+
+from sacher_tec import epos_motor as epm
+
+ST_DISABLED = c_word(0)
+ST_ENABLED = c_word(1)
+A_COEF = -3.499E-12
+B_COEF = 7.74529E-5
+C_COEF = 960.024
+WL_MAX = 985.5
+WL_MIN = 919.5
+
+def du_to_wl(du):
+    wl = A_COEF*(math.pow(du,2)) + B_COEF*du + C_COEF
+    if(wl < WL_MIN):
+        return WL_MIN
+    elif(wl > WL_MAX):
+        return WL_MAX
+    else:
+        return wl
+
+def wl_to_du(wl):
+    if(wl < WL_MIN):
+        wl = WL_MIN
+    elif(wl > WL_MAX):
+        wl = WL_MAX
+    du = -1*B_COEF/(2*A_COEF) - math.sqrt((wl - C_COEF)/A_COEF + math.pow((B_COEF/(2*A_COEF)),2))
+    return round(du)
+
+def vel_to_du(vel):
+    if(vel > 30):
+        vel = 30
+    if(vel < 0.01):
+        vel = 0.01
+    du = round(vel*380)
+    return du
+
+def du_to_vel(du):
+    vel = du/3
+    if(vel > 30):
+        vel = 30
+    if(vel < 0.01):
+        vel = 0.01
+    return vel
+
+def acc_to_du(acc):
+    if(acc > 25):
+        acc = 25
+    if(acc < 0.01):
+        acc = 0.01
+    du = round(acc*380)
+    return du
+
+def du_to_acc(du):
+    acc = du/380
+    if(acc > 25):
+        acc = 25
+    if(acc < 0.01):
+        acc = 0.01
+    return acc
+
+name = create_string_buffer(("EPOS2").encode('utf-8'))
+protocol = create_string_buffer(("MAXON SERIAL V2").encode('utf-8'))
+interface = create_string_buffer(("USB").encode('utf-8'))
+port = create_string_buffer(("USB0").encode('utf-8'))
+NodeID = 1
+pNbOfBytesRead=c_uint()
+pData=c_uint()
+pPositionIs=c_long()
+pErrorCode=c_int()
+pMode=c_byte()
+pState=c_word()
+pTargetReached=c_bool()
+pProfileVelocity=c_dword()
+pProfileAcceleration=c_dword()
+pProfileDeceleration=c_dword()
+
+print("what wavelength would you like (nm)?")
+wl = float(input())
+
+print("what velocity would you like it to move at (nm/s)?")
+vel = vel_to_du(float(input()))
+
+print("what acceleration would you like it to move at (nm/s^2)?")
+acc = acc_to_du(float(input()))
+
+print("initialize")
+keyhandle = epm.VCS_OpenDevice(name,protocol,interface,port,byref(pErrorCode))
+print(pErrorCode)
+print(keyhandle)
+
+print("get operation mode")
+ret=epm.VCS_GetOperationMode(keyhandle, NodeID, byref(pMode), byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+print(pMode)
+
+print("get state")
+ret=epm.VCS_GetState(keyhandle, NodeID, byref(pState), byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+print(pState)
+
+print("set state")
+ret=epm.VCS_SetState(keyhandle, NodeID, ST_ENABLED, byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+
+print("get state")
+ret=epm.VCS_GetState(keyhandle, NodeID, byref(pState), byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+print(pState)
+
+print("get params")
+ret=epm.VCS_GetPositionProfile(keyhandle, NodeID, byref(pProfileVelocity), byref(pProfileAcceleration), byref(pProfileDeceleration),byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+print(pProfileVelocity)
+print(pProfileAcceleration)
+print(pProfileDeceleration)
+
+print("set params")
+ret=epm.VCS_SetPositionProfile(keyhandle, NodeID, c_dword(vel), c_dword(acc), c_dword(acc), byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+
+print("get params")
+ret=epm.VCS_GetPositionProfile(keyhandle, NodeID, byref(pProfileVelocity), byref(pProfileAcceleration), byref(pProfileDeceleration),byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+print(pProfileVelocity)
+print(pProfileAcceleration)
+print(pProfileDeceleration)
+
+print("get position")
+ret = epm.VCS_GetPositionIs(keyhandle, NodeID, byref(pPositionIs), byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+print(du_to_wl(pPositionIs.value))
+init_pos = pPositionIs.value
+
+print("start time:")
+init_time = time.time()
+print(init_time)
+
+print("set position")
+ret=epm.VCS_MoveToPosition(keyhandle, NodeID, wl_to_du(wl), 1, 0, byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+
+comp = False
+while(comp == False):
+    # print("check if complete")
+    ret=epm.VCS_GetMovementState(keyhandle, NodeID, byref(pTargetReached), byref(pErrorCode))
+    # print(pErrorCode)
+    # print(ret)
+    # print(pTargetReached)
+    comp = pTargetReached.value
+    epm.VCS_GetPositionIs(keyhandle, NodeID, byref(pPositionIs), byref(pErrorCode))
+    print(du_to_wl(pPositionIs.value))
+    time.sleep(0.1)
+
+print("end time:")
+end_time = time.time()
+print(end_time)
+
+print("difference:")
+diff = end_time - init_time
+print(diff)
+
+print("get position")
+ret=epm.VCS_GetPositionIs(keyhandle, NodeID, byref(pPositionIs), byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+print(du_to_wl(pPositionIs.value))
+
+end_pos = pPositionIs.value
+
+print("distance traveled:")
+dist = abs(end_pos - init_pos)
+print(dist)
+
+print("velocity:")
+vel = dist/diff
+print(vel)
+
+print("set state")
+ret=epm.VCS_SetState(keyhandle, NodeID, ST_DISABLED, byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+
+print("get state")
+ret=epm.VCS_GetState(keyhandle, NodeID, byref(pState), byref(pErrorCode))
+print(pErrorCode)
+print(ret)
+print(pState)
+
+print("close device")
+ret=epm.VCS_CloseAllDevices(byref(pErrorCode))
+print(pErrorCode)
+print(ret)
\ No newline at end of file
diff --git a/examples/906.uc480-camera/gui_test3.py b/examples/906.uc480-camera/gui_test3.py
new file mode 100644
index 0000000..a642736
--- /dev/null
+++ b/examples/906.uc480-camera/gui_test3.py
@@ -0,0 +1,415 @@
+import tkinter
+from tkinter import filedialog
+from tkinter import colorchooser
+import tkinter.ttk
+import cv2
+import PIL.Image, PIL.ImageTk
+import time
+from pyrolab.api import locate_ns, Proxy
+import numpy as np
+import socket
+import pickle
+import time
+from datetime import datetime
+import threading
+import os
+
+HEADERSIZE = 10
+BRIGHTNESS = 5
+PORT = 2222
+SER_NUMBER_1 = 4103257229
+SER_NUMBER_2 = 4103238947
+
+class App:
+    def __init__(self, window, window_title, video_source=0):
+        self.window = window
+        self.window.title(window_title)
+        self.video_source = video_source
+        self.window.protocol("WM_DELETE_WINDOW", self.close_window)
+        #self.window.geometry("700x700")
+        self.directory = "C:/UC480Camera"
+
+        # open video source (by default this will try to open the computer webcam)
+        self.vid_1 = MyVideoCapture(self.video_source,SER_NUMBER_1)
+        self.vid_2 = MyVideoCapture(self.video_source,SER_NUMBER_2)
+
+        # Create a canvas that can fit the above video source size
+
+        # Button that lets the user take a snapshot
+
+        self.frame_1 = tkinter.Frame()
+        #self.frame_1.pack(fill=tkinter.BOTH, expand=True)
+        self.frame_1.grid(row=0, column=0)
+
+        self.frame_1.columnconfigure(1, weight=1)
+        self.frame_1.columnconfigure(3, pad=7)
+        self.frame_1.rowconfigure(6, weight=1)
+        self.frame_1.rowconfigure(8, pad=7)
+
+        self.canvas_1 = tkinter.Canvas(self.frame_1, width = self.vid_1.width, height = self.vid_1.height)
+        #self.canvas.pack()
+        self.canvas_1.grid(row=0, column=0, columnspan=2, rowspan=7,
+            padx=5, sticky=tkinter.E+tkinter.W+tkinter.S+tkinter.N)
+
+        self.pic_btn_1 = tkinter.Button(self.frame_1, text="Snapshot", command=self.snapshot_1)
+        self.pic_btn_1.grid(row=0, column=3, pady=4, sticky=tkinter.W)
+
+        self.rec_btn_1 = tkinter.Button(self.frame_1, text="Record", command=self.record_1)
+        self.rec_btn_1.grid(row=1, column=3, pady=4, sticky=tkinter.W)
+
+        self.expo_lbl_1 = tkinter.Label(self.frame_1, text="Exposure:")
+        self.expo_lbl_1.grid(row=4, column=3, pady=4)
+
+        self.expo_slider_1 = tkinter.Scale(self.frame_1, from_=0, to=30,command=self.set_exposure_1)
+        self.expo_slider_1.set(15)
+        self.expo_slider_1.grid(row=5, column=3, pady=4, sticky=tkinter.W)
+
+        self.color_1 = tkinter.IntVar()
+        self.color_check_1 = tkinter.Checkbutton(self.frame_1, text='Color',variable=self.color_1, onvalue=1, offvalue=0, command=self.set_color_1)
+        self.color_check_1.grid(row=2,column=3,pady=4)
+
+        self.col_btn_1 = tkinter.Button(self.frame_1, text="Filter", command=self.select_filter_1)
+        self.col_btn_1.grid(row=3, column=3, pady=4, sticky=tkinter.W)
+
+        self.brow_btn_1 = tkinter.Button(self.frame_1, text="Browse", command=self.browse_files)
+        self.brow_btn_1.grid(row=8, column=0, padx=5)
+
+        self.file_lbl_1 = tkinter.Label(self.frame_1, text="File Selected:")
+        self.file_lbl_1.grid(row=8,column=1,padx=5,sticky=tkinter.W)
+
+        #self.sep = tkinter.ttk.Separator(self.frame, orient=tkinter.VERTICAL).grid(column=4, row=0, rowspan=8, sticky='ns')
+
+        self.frame_2 = tkinter.Frame()
+        #self.frame_2.pack(fill=tkinter.BOTH, expand=True)
+        self.frame_2.grid(row=0, column=1)
+
+        self.frame_2.columnconfigure(1, weight=1)
+        self.frame_2.columnconfigure(3, pad=7)
+        self.frame_2.rowconfigure(6, weight=1)
+        self.frame_2.rowconfigure(7, pad=7)
+
+        self.canvas_2 = tkinter.Canvas(self.frame_2, width = self.vid_2.width, height = self.vid_2.height)
+        #self.canvas.pack()
+        self.canvas_2.grid(row=0, column=0, columnspan=2, rowspan=7,
+            padx=5, sticky=tkinter.E+tkinter.W+tkinter.S+tkinter.N)
+
+        self.pic_btn_2 = tkinter.Button(self.frame_2, text="Snapshot", command=self.snapshot_2)
+        self.pic_btn_2.grid(row=0, column=3, pady=4, sticky=tkinter.W)
+
+        self.rec_btn_2 = tkinter.Button(self.frame_2, text="Record", command=self.record_2)
+        self.rec_btn_2.grid(row=1, column=3, pady=4, sticky=tkinter.W)
+
+        self.expo_lbl_2 = tkinter.Label(self.frame_2, text="Exposure:")
+        self.expo_lbl_2.grid(row=4, column=3, pady=4)
+
+        self.expo_slider_2 = tkinter.Scale(self.frame_2, from_=0, to=30,command=self.set_exposure_2)
+        self.expo_slider_2.set(15)
+        self.expo_slider_2.grid(row=5, column=3, pady=4, sticky=tkinter.W)
+
+        self.color_2 = tkinter.IntVar()
+        self.color_check_2 = tkinter.Checkbutton(self.frame_2, text='Color',variable=self.color_2, onvalue=1, offvalue=0, command=self.set_color_2)
+        self.color_check_2.grid(row=2,column=3,pady=4)
+
+        self.col_btn_2 = tkinter.Button(self.frame_2, text="Filter", command=self.select_filter_2)
+        self.col_btn_2.grid(row=3, column=3, pady=4, sticky=tkinter.W)
+
+        self.brow_btn_2 = tkinter.Button(self.frame_2, text="Browse", command=self.browse_files)
+        self.brow_btn_2.grid(row=8, column=0, padx=5)
+
+        self.file_lbl_2 = tkinter.Label(self.frame_2, text="File Selected:")
+        self.file_lbl_2.grid(row=8,column=1,padx=5,sticky=tkinter.W)
+
+        self.frame_3 = tkinter.Frame()
+        #self.frame_3.pack(fill=tkinter.BOTH, expand=True)
+        self.frame_3.grid(row=3, column=0,columnspan=1,rowspan=1)
+
+        self.left_btn = tkinter.Button(self.frame_3, text="Left")
+        self.left_btn.grid(row=0, column=1, pady=4)
+
+        self.right_btn = tkinter.Button(self.frame_3, text="Right")
+        self.right_btn.grid(row=0, column=2, pady=4)
+
+        # After it is called once, the update method will be automatically called every delay milliseconds
+        self.delay = 15
+        self.update()
+
+        self.window.mainloop()
+
+    def select_filter_1(self):
+        color_code = colorchooser.askcolor(title ="Choose color")
+        self.filter = color_code[0]
+        self.vid_1.set_filter(self.filter)
+
+    def set_color_1(self):
+        col = False
+        if(self.color_1.get() == 1):
+            col = True
+        self.vid_1.set_color(col)
+
+    def set_exposure_1(self,value):
+        self.exposure_1 = int(value)
+        self.vid_1.set_exposure(self.exposure_1)
+
+    def browse_files(self):
+        self.directory = filedialog.askdirectory()
+      
+        # Change label contents
+        self.file_lbl_1.configure(text="File Selected: "+self.directory)
+        self.file_lbl_2.configure(text="File Selected: "+self.directory)
+
+    def record_1(self):
+        if(self.rec_btn_1['text'] == "Record"):
+            self.vid_1.set_rec_state(1,self.directory)
+            self.rec_btn_1.configure(text="Stop")
+        else:
+            self.vid_1.set_rec_state(0,self.directory)
+            self.rec_btn_1.configure(text="Record")
+
+    def snapshot_1(self):
+        # Get a frame from the video source
+        self.vid_1.capture(self.directory)
+    
+    def select_filter_2(self):
+        color_code = colorchooser.askcolor(title ="Choose color")
+        self.filter = color_code[0]
+        self.vid_2.set_filter(self.filter)
+
+    def set_color_2(self):
+        col = False
+        if(self.color_2.get() == 1):
+            col = True
+        self.vid_2.set_color(col)
+
+    def set_exposure_2(self,value):
+        self.exposure_2 = int(value)
+        self.vid_2.set_exposure(self.exposure_2)
+
+    def record_2(self):
+        if(self.rec_btn_2['text'] == "Record"):
+            self.vid_2.set_rec_state(1,self.directory)
+            self.rec_btn_2.configure(text="Stop")
+        else:
+            self.vid_2.set_rec_state(0,self.directory)
+            self.rec_btn_2.configure(text="Record")
+
+    def snapshot_2(self):
+        # Get a frame from the video source
+        self.vid_2.capture(self.directory)
+
+    def update(self):
+        # Get a frame from the video source
+        ret_1, frame_1 = self.vid_1.get_frame()
+        ret_2, frame_2 = self.vid_2.get_frame()
+
+        if ret_1:
+            self.photo_1 = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(frame_1))
+            self.canvas_1.create_image(0, 0, image = self.photo_1, anchor = tkinter.NW)
+        if ret_2:
+            self.photo_2 = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(frame_2))
+            self.canvas_2.create_image(0, 0, image = self.photo_2, anchor = tkinter.NW)
+
+        self.window.after(self.delay, self.update)
+    
+    def close_window(self):
+        self.window.destroy()
+        self.vid_1.__del__()
+        self.vid_2.__del__()
+
+
+class MyVideoCapture:
+    def __init__(self, video_source=0,ser_num=0):
+        self.vs = video_source
+        if(video_source == 0):
+            # Open the video source
+            self.vid = cv2.VideoCapture(video_source)
+            if not self.vid.isOpened():
+                raise ValueError("Unable to open video source", video_source)
+
+            # Get video source width and height
+            self.width = self.vid.get(cv2.CAP_PROP_FRAME_WIDTH)
+            self.height = self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)
+        else:
+            self.live = False
+            self.color = False
+            self.stop_video = threading.Event()
+            self.ser_num = ser_num
+            self.exposure = 3.16
+            self.filter = (255,255,255)
+            self.width = 640
+            self.height = 512
+            self.terminate = False
+            self.rec_state = 0
+            self.cap_state = 0
+            self.init_camera()
+            self.stopEvent = threading.Event()
+            self.thread = threading.Thread(target=self.videoLoop, args=())
+            self.thread.start()
+            while(True):
+                time.sleep(0.001)
+                if(self.live == True):
+                    break
+    def set_filter(self,filter):
+        if(filter != None):
+            self.filter = filter
+    def set_color(self,color):
+        self.cam.color_gray(color)
+
+    def set_exposure(self,exposure):
+        self.exposure = (pow(10.0,(exposure+20.0)/10.0))/1000.0
+        self.cam.set_exposure(self.exposure)
+    
+    def capture(self,directory):
+        if not os.path.exists(directory+"/images"):
+            os.makedirs(directory+"/images")
+        self.cap_state = 1
+        self.directory = directory
+
+    def set_rec_state(self,state,directory):
+        if(state == 1):
+            if not os.path.exists(directory+"/videos"):
+                os.makedirs(directory+"/videos")
+            fourcc = cv2.VideoWriter_fourcc(*'XVID')
+            self.out = cv2.VideoWriter(directory+"/videos/video-" + time.strftime("%d-%m-%Y-%H-%M-%S") + ".avi",fourcc, self.frame_rate, (640,512))
+        else:
+            self.out.release()
+        self.rec_state = state
+
+    
+    def init_camera(self):
+        ns = locate_ns(host="camacholab.ee.byu.edu")
+        objs = str(ns.list())
+        while(True):
+            temp_str = objs[objs.find('\'')+1:-1]
+            temp_obj = temp_str[0:temp_str.find('\'')]
+            if(temp_obj[0:5] ==  "UC480"):
+                temp_ser_no = int(temp_obj[6:len(temp_obj)])
+                if(temp_ser_no == self.ser_num):
+                    cam_str = temp_obj
+            if(objs.find(',') == -1):
+                break
+            objs = objs[objs.find(',')+1:-1]
+        try:
+            cam_str
+        except NameError:
+            raise Exception("Camera with serial number " + str(self.ser_num) + " could not be found")
+        self.cam = Proxy(ns.lookup(cam_str))
+
+        self.cam.start(exposure=65)
+        ip_address = self.cam.start_capture(self.color)
+        self.clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.clientsocket.connect((str(ip_address), PORT))
+
+        self.time_s = 0
+        self.frame_rate = 0
+        self.count = -1
+
+    def bayer_convert(self,bayer):
+        if(self.color):
+            ow = (bayer.shape[0]//4) * 4
+            oh = (bayer.shape[1]//4) * 4
+
+            R  = bayer[0::2, 0::2]
+            B  = bayer[1::2, 1::2]
+            G0 = bayer[0::2, 1::2]
+            G1 = bayer[1::2, 0::2]
+            G = G0[:oh,:ow]//2 + G1[:oh,:ow]//2
+            bayer_R = np.array(R, dtype=np.uint8).reshape(512, 640)
+            bayer_G = np.array(G, dtype=np.uint8).reshape(512, 640)
+            bayer_B = np.array(B, dtype=np.uint8).reshape(512, 640)
+
+            dStack = np.clip(np.dstack((bayer_B*(BRIGHTNESS/5),bayer_G*(BRIGHTNESS/5),bayer_R*(BRIGHTNESS/5))),0,255).astype('uint8')
+        else:
+            bayer_T = np.array(bayer, dtype=np.uint8).reshape(512, 640)
+            dStack = np.clip((np.dstack(((bayer_T*(self.filter[2]/255))*(BRIGHTNESS/5),(bayer_T*(self.filter[1]/255))*(BRIGHTNESS/5),(bayer_T*(self.filter[0]/255))*(BRIGHTNESS/5)))),0,255).astype('uint8')
+            #dStack = np.clip((np.dstack(((0.469 + bayer_T*0.75 - (bayer_T^2)*0.003)*(BRIGHTNESS/5),(bayer_T*0.95)*(BRIGHTNESS/5),(0.389 + bayer_T*1.34 - (bayer_T^2)*0.004)*(BRIGHTNESS/5)))),0,255).astype('uint8')
+        #dStack = np.clip(np.dstack((bayer,bayer,bayer)),0,255).astype('uint8')
+        return dStack
+
+    def videoLoop(self):
+        while not self.stop_video.is_set():
+            if(self.count >= 0):
+                t = time.time() - self.time_s
+                self.frame_rate = (self.frame_rate*self.count + 1/t)/(self.count + 1)  
+
+            self.time_s = time.time()
+            if(self.count < 50):
+                self.count = self.count + 1
+            else:
+                self.count = 1
+
+            msg = b''
+            new_msg = True
+            msg_len = None
+            imList = None
+            while True:
+                if new_msg:
+                    sub_msg = self.clientsocket.recv(HEADERSIZE)
+                    msg_len = int((sub_msg[:HEADERSIZE]))
+                    new_msg = False
+                else:
+                    sub_msg = self.clientsocket.recv(32800)
+                    msg += sub_msg
+                    if len(msg) == msg_len:
+                        imList = pickle.loads(msg)
+                        break
+
+            if(imList.size == 327680):
+                self.color = False
+                self.count = 0
+            else:
+                self.color = True  
+                self.count = 0                
+            dStack = self.bayer_convert(imList)
+
+            if(self.cap_state == 1):
+                cv2.imwrite(self.directory+"/images/frame-" + time.strftime("%d-%m-%Y-%H-%M-%S") + ".jpg", dStack)
+                self.cap_state = 0
+
+            if(self.rec_state == 1):
+                self.out.write(dStack)
+
+            self.frame = cv2.cvtColor(dStack, cv2.COLOR_BGR2RGB)
+            
+            if(self.terminate == True):
+                self.clientsocket.send(b'b')  
+                self.live = False   
+                break
+            self.clientsocket.send(b'g')
+            self.live = True
+
+    def get_frame(self):
+        if(self.vs == 0):
+            if self.vid.isOpened():
+                ret, frame = self.vid.read()
+                if ret:
+                    # Return a boolean success flag and the current frame converted to BGR
+                    return (ret, cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
+                else:
+                    return (ret, None)
+            else:
+                return (ret, None)
+        else:
+            return(True, self.frame)
+
+
+    # Release the video source when the object is destroyed
+    def __del__(self):
+        if(self.vs == 0):
+            if self.vid.isOpened():
+                self.vid.release()
+        else:
+            self.terminate = True
+            while(True):
+                time.sleep(0.001)
+                if(self.live == False):
+                    break
+            self.cam.close()
+            self.stop_video.set()
+            try:
+                self.out.release()
+            except AttributeError:
+                pass
+            
+
+# Create a window and pass it to the Application object
+App(tkinter.Tk(), "PyroLab",1)
\ No newline at end of file
diff --git a/examples/906.uc480-camera/gui_test4.py b/examples/906.uc480-camera/gui_test4.py
new file mode 100644
index 0000000..07a8b4b
--- /dev/null
+++ b/examples/906.uc480-camera/gui_test4.py
@@ -0,0 +1,52 @@
+from tkinter import Tk, Text, BOTH, W, N, E, S
+from tkinter.ttk import Frame, Button, Label, Style
+
+
+class Example(Frame):
+
+    def __init__(self):
+        super().__init__()
+
+        self.initUI()
+
+
+    def initUI(self):
+
+        self.master.title("Windows")
+        self.pack(fill=BOTH, expand=True)
+
+        self.columnconfigure(1, weight=1)
+        self.columnconfigure(3, pad=7)
+        self.rowconfigure(3, weight=1)
+        self.rowconfigure(5, pad=7)
+
+        lbl = Label(self, text="Windows")
+        lbl.grid(sticky=W, pady=4, padx=5)
+
+        area = Text(self)
+        area.grid(row=1, column=0, columnspan=2, rowspan=4,
+            padx=5, sticky=E+W+S+N)
+
+        abtn = Button(self, text="Activate")
+        abtn.grid(row=1, column=3)
+
+        cbtn = Button(self, text="Close")
+        cbtn.grid(row=2, column=3, pady=4)
+
+        hbtn = Button(self, text="Help")
+        hbtn.grid(row=5, column=0, padx=5)
+
+        obtn = Button(self, text="OK")
+        obtn.grid(row=5, column=3)
+
+
+def main():
+
+    root = Tk()
+    root.geometry("600x300")
+    app = Example()
+    root.mainloop()
+
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file