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/.bumpversion.cfg b/.bumpversion.cfg
index ab25a74..041be43 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 0.1.1
+current_version = 0.2.0
 commit = False
 tag = False
 parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
diff --git a/README.md b/README.md
index b7ed115..41a3121 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 ---
 
 <p align="center">
-<img alt="Development version" src="https://img.shields.io/badge/master-v0.1.1-informational">
+<img alt="Development version" src="https://img.shields.io/badge/master-v0.2.0-informational">
 <a href="https://pypi.python.org/pypi/pyrolab"><img alt="PyPI Version" src="https://img.shields.io/pypi/v/pyrolab.svg"></a>
 <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/pyrolab">
 <!-- <a href="https://github.com/BYUCamachoLab/simphony/actions?query=workflow%3A%22build+%28pip%29%22"><img alt="Build Status" src="https://github.com/BYUCamachoLab/simphony/workflows/build%20(pip)/badge.svg"></a> -->
diff --git a/dependencies/cameras/uc480/README.md b/dependencies/cameras/uc480/README.md
new file mode 100644
index 0000000..a6a3325
--- /dev/null
+++ b/dependencies/cameras/uc480/README.md
@@ -0,0 +1,4 @@
+The ThorLabs UC480 requires certain drivers already be installed on your
+system. Get the drivers from ThorLabs website:
+
+* https://www.thorlabs.com/software_pages/ViewSoftwarePage.cfm?Code=ThorCam
diff --git a/dependencies/firmata/relay/SimpleDigitalFirmata.ino b/dependencies/firmata/relay/SimpleDigitalFirmata.ino
new file mode 100644
index 0000000..cc4a447
--- /dev/null
+++ b/dependencies/firmata/relay/SimpleDigitalFirmata.ino
@@ -0,0 +1,95 @@
+/*
+ * Firmata is a generic protocol for communicating with microcontrollers
+ * from software on a host computer. It is intended to work with
+ * any host computer software package.
+ *
+ * To download a host software package, please click on the following link
+ * to open the list of Firmata client libraries in your default browser.
+ *
+ * https://github.com/firmata/arduino#firmata-client-libraries
+ */
+​
+/* Supports as many digital inputs and outputs as possible.
+ *
+ * This example code is in the public domain.
+ */
+#include <Firmata.h>
+​
+int lamp_pin = 13;
+int button_pin = 7;
+bool count = 0;
+unsigned long timer = 0;
+unsigned long auto_turnoff = 3600000;
+byte previousPIN[TOTAL_PORTS];  // PIN means PORT for input
+byte previousPORT[TOTAL_PORTS];
+​
+void outputPort(byte portNumber, byte portValue)
+{
+  // only send the data when it changes, otherwise you get too many messages!
+  if (previousPIN[portNumber] != portValue) {
+    Firmata.sendDigitalPort(portNumber, portValue);
+    previousPIN[portNumber] = portValue;
+  }
+}
+​
+void setPinModeCallback(byte pin, int mode) {
+  if (IS_PIN_DIGITAL(pin)) {
+    pinMode(PIN_TO_DIGITAL(pin), mode);
+  }
+}
+​
+void digitalWriteCallback(byte port, int value)
+{
+  byte i;
+  byte currentPinValue, previousPinValue;
+​
+  if (port < TOTAL_PORTS && value != previousPORT[port]) {
+    for (i = 0; i < 8; i++) {
+      currentPinValue = (byte) value & (1 << i);
+      previousPinValue = previousPORT[port] & (1 << i);
+      if (currentPinValue != previousPinValue) {
+        digitalWrite(i + (port * 8), currentPinValue);
+      }
+    }
+    previousPORT[port] = value;
+  }
+}
+​
+void setup()
+{
+  Firmata.setFirmwareVersion(FIRMATA_FIRMWARE_MAJOR_VERSION, FIRMATA_FIRMWARE_MINOR_VERSION);
+  Firmata.attach(DIGITAL_MESSAGE, digitalWriteCallback);
+  Firmata.attach(SET_PIN_MODE, setPinModeCallback);
+  Firmata.begin(57600);
+  pinMode(button_pin, INPUT);
+}
+​
+void loop()
+{
+  byte i;
+​
+  for (i = 0; i < TOTAL_PORTS; i++) {
+    outputPort(i, readPort(i, 0xff));
+  }
+​
+  while (Firmata.available()) {
+    Firmata.processInput();
+  }
+  
+  buttonSwitch();
+  if(count && ((millis() - timer) >= auto_turnoff)){
+    count = 0;
+    digitalWrite(lamp_pin, count);
+  }
+}
+​
+void buttonSwitch()
+{
+  if(digitalRead(button_pin)){
+    count = !count;
+    digitalWrite(lamp_pin, count);
+    while(digitalRead(button_pin));
+    delay(1000);
+    timer = millis();
+  }
+}
\ No newline at end of file
diff --git a/dependencies/lasers/sachertec/README.md b/dependencies/lasers/sachertec/README.md
new file mode 100644
index 0000000..76c5e35
--- /dev/null
+++ b/dependencies/lasers/sachertec/README.md
@@ -0,0 +1,5 @@
+The SacherTec laser requires some DLL's in order to run. Get more info at the
+links below:
+
+* https://support.maxongroup.com/hc/en-us/articles/360012695739
+* https://www.maxongroup.com/medias/sys_master/8823917281310.pdf
diff --git a/dependencies/lasers/tsl550/README.md b/dependencies/lasers/tsl550/README.md
new file mode 100644
index 0000000..d6bcdb0
--- /dev/null
+++ b/dependencies/lasers/tsl550/README.md
@@ -0,0 +1,3 @@
+The Santec TSL550 laser requires proprietary USB drivers that are loaded on
+a DVD included with your laser. Install the drivers before attempting to use
+PyroLab.
diff --git a/pyrolab/drivers/lasers/docs/Santec TSL-550-M-E 1370 nm laser.pdf b/dependencies/lasers/tsl550/Santec TSL-550-M-E 1370 nm laser.pdf
similarity index 100%
rename from pyrolab/drivers/lasers/docs/Santec TSL-550-M-E 1370 nm laser.pdf
rename to dependencies/lasers/tsl550/Santec TSL-550-M-E 1370 nm laser.pdf
diff --git a/dependencies/motion/README.md b/dependencies/motion/README.md
new file mode 100644
index 0000000..a8478a2
--- /dev/null
+++ b/dependencies/motion/README.md
@@ -0,0 +1,3 @@
+The ThorLabs motor controllers are controlled using ThorLabs Kinesis DLL's.
+These are available for download from ThorLabs at 
+https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=10285
diff --git a/pyrolab/drivers/scopes/docs/RTO_UserManual_en_11.pdf b/dependencies/scopes/rohdeschwarz/RTO_UserManual_en_11.pdf
similarity index 100%
rename from pyrolab/drivers/scopes/docs/RTO_UserManual_en_11.pdf
rename to dependencies/scopes/rohdeschwarz/RTO_UserManual_en_11.pdf
diff --git a/docs/changelog/0.2.0-changelog.md b/docs/changelog/0.2.0-changelog.md
new file mode 100644
index 0000000..1e5f99b
--- /dev/null
+++ b/docs/changelog/0.2.0-changelog.md
@@ -0,0 +1,25 @@
+## 0.2.0 - 2021-06-IN_PROGRESS
+
+The first major API change to PyroLab, but not yet guaranteeing backwards
+compatability.
+
+### Added
+- New driver: UC480 camera
+- New driver: Arudino board (using Firmata)
+- New driver: Pure Photonics laser (550)
+- New driver: Sacher-tec laser
+- New installation directory for preserving program configuration betwen 
+sessions, used for nameservers and instrument servers.
+- New API calls for starting a nameserver, setting a reusable configuration.
+- New API calls for starting an instrument server, setting a reusable 
+configuration. Remembers previously configured instruments.
+- Instrument server spawns instrument servers in a separate thread and manages
+them, autorestarts them when they fail, etc.
+- Instruments can be designated as "lockable", preventing new connections to 
+an instrument when in use.
+
+### Changed
+- Examples reorganized and expanded.
+
+### Removed
+- None
\ No newline at end of file
diff --git a/examples/lasers/sachertec_sweep.py b/examples/904.sacher-tec/sachertec_sweep.py
similarity index 100%
rename from examples/lasers/sachertec_sweep.py
rename to examples/904.sacher-tec/sachertec_sweep.py
diff --git a/examples/902.laser-scope/ppcl550_example.py b/examples/905.pp-laser/ppcl550_example.py
similarity index 100%
rename from examples/902.laser-scope/ppcl550_example.py
rename to examples/905.pp-laser/ppcl550_example.py
diff --git a/examples/902.laser-scope/ppcl551_example.py b/examples/905.pp-laser/ppcl551_example.py
similarity index 100%
rename from examples/902.laser-scope/ppcl551_example.py
rename to examples/905.pp-laser/ppcl551_example.py
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/__init__.py b/pyrolab/__init__.py
index 53d88f8..ee09337 100644
--- a/pyrolab/__init__.py
+++ b/pyrolab/__init__.py
@@ -38,7 +38,7 @@
 __name__ = "PyroLab"
 __author__ = "CamachoLab"
 __copyright__ = "Copyright 2020, The PyroLab Project"
-__version__ = "0.1.1"
+__version__ = "0.2.0"
 __license__ = "GPLv3+"
 __maintainer__ = "Sequoia Ploeg"
 __maintainer_email__ = "sequoia.ploeg@byu.edu"
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/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/motion/nanomax300.py b/pyrolab/drivers/motion/nanomax300.py
index 2590eb5..af62a69 100644
--- a/pyrolab/drivers/motion/nanomax300.py
+++ b/pyrolab/drivers/motion/nanomax300.py
@@ -5,15 +5,14 @@
 # (see pyrolab/__init__.py for details)
 
 """
-Nanomax300
-------
+NanoMax 300
+-----------
 
 Submodule containing drivers for the ThorLabs NanoMax 300 piezo controlled
 motion stage.
 
-Contributors
- * David Hill (https://github.com/hillda3141)  
- * Sequoia Ploeg (https://github.com/sequoiap)
+Supported OS:
+- Windows
 """
 
 from numpy import interp
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
index 1481bd1..3227ad8 100644
--- a/pyrolab/drivers/utils/driverLocker.py
+++ b/pyrolab/drivers/utils/driverLocker.py
@@ -1,3 +1,5 @@
+# TODO: DEPRECATED! REMOVE AFTER TESTING COMPATABILITY OF NEW LOCKER.
+
 import os
 from os import path
 import pyfirmata
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()
diff --git a/setup.py b/setup.py
index 8c406b2..e218e33 100644
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,7 @@
 # ==============================================================================
 setup_args = dict(
     name=NAME,
-    version="0.1.1",
+    version="0.2.0",
     author="Sequoia Ploeg",
     maintainer="Sequoia Ploeg",
     maintainer_email="sequoia.ploeg@byu.edu",


Test patch:
diff --git a/pyrolab/drivers/lasers/st_test.py b/examples/904.sacher-tec/st_test.py
similarity index 100%
rename from pyrolab/drivers/lasers/st_test.py
rename to examples/904.sacher-tec/st_test.py
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