Repository: python-microscopy/python-microscopy (language: python)

Issue:
[BUG] dependency for newish fit _factory does not get built?
**Describe the bug**

For a few months now I have seen the following which looks like a (largely inconsequential) bug (?) on all platforms where we have built recent PYMEs. Essentially, registering the fit factory `LatGaussFitlmR` fails due to a missing `PYME.localization.lev_gfit.levmar_gfit` dependency. The suspicion is that it does not get built due to something in a setup file not being quite right or similar, but we are not certain and have not investigated deeply.

The bug (if you can call it that) is not a big deal but it would be good to get rid of the "noise" in the logs/output when starting `PYMEAcquire`. If it can be fixed easily so much the better.

```
ERROR:PYME.localization.FitFactories:Error registering fit factory: LatGaussFitlmR, LatGaussFitlmR, PYME.localization.FitFactories
Traceback (most recent call last):
  File "/Users/csoe002/Documents/src/PYMEsrc/pyme38/PYME/localization/FitFactories/__init__.py", line 56, in _register_fit_factory
    fm = importlib.import_module(pkg + '.' + module_name)
  File "/Users/csoe002/miniconda3/envs/pyme38_2/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 843, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/csoe002/Documents/src/PYMEsrc/pyme38/PYME/localization/FitFactories/LatGaussFitlmR.py", line 33, in <module>
    from PYME.localization.lev_gfit.levmar_gfit import *
ModuleNotFoundError: No module named 'PYME.localization.lev_gfit.levmar_gfit'
```

**To Reproduce**
Steps to reproduce the behavior:
1. start `PymeAcquire`
2. See bug


**Platforms (please complete the following information - or copy-paste from error dialog if shown):**
 - OS: Windows 10, 11, macOS



Discussion / hints:

Fix patch:
diff --git a/PYME/Acquire/ActionManager.py b/PYME/Acquire/ActionManager.py
index 3d94933d4..bac083804 100644
--- a/PYME/Acquire/ActionManager.py
+++ b/PYME/Acquire/ActionManager.py
@@ -46,7 +46,7 @@ def __init__(self, scope):
         Parameters
         ----------
         
-        scope : PYME.Acquire.microscope.microscope object
+        scope : PYME.Acquire.microscope.Microscope object
             The microscope. The function object to call for an action should be 
             accessible within the scope namespace, and will be resolved by
             calling eval('scope.functionName')
diff --git a/PYME/Acquire/ExecTools.py b/PYME/Acquire/ExecTools.py
index 241980ff9..77308ad8a 100755
--- a/PYME/Acquire/ExecTools.py
+++ b/PYME/Acquire/ExecTools.py
@@ -195,7 +195,7 @@ def run(self, parent, scope):
         Parameters
         ----------
         parent : PYME.Acquire.acquiremainframe.PYMEMainFrame, wx.Frame
-        scope : PYME.Acquire.microscope.microscope
+        scope : PYME.Acquire.microscope.Microscope
         
         """
         global defGlobals
diff --git a/PYME/Acquire/Hardware/Piezos/base_piezo.py b/PYME/Acquire/Hardware/Piezos/base_piezo.py
index 9c13a1941..7cfdb7551 100644
--- a/PYME/Acquire/Hardware/Piezos/base_piezo.py
+++ b/PYME/Acquire/Hardware/Piezos/base_piezo.py
@@ -62,6 +62,12 @@ def GetFirmwareVersion(self):
     def OnTarget(self):
         raise NotImplementedError
     
+    @property
+    def effective_pos(self):
+        """ Return the effective position of the piezo. This is exclusively for use in simulation,
+        and allows for derived classes to simulate drifty piezos, etc. """
+        return self.GetPos()
+    
 
 class SingleAxisWrapper(PiezoBase):
     """
diff --git a/PYME/Acquire/Hardware/Simulator/fakeCam.py b/PYME/Acquire/Hardware/Simulator/fakeCam.py
index 269e166e3..9608c9547 100755
--- a/PYME/Acquire/Hardware/Simulator/fakeCam.py
+++ b/PYME/Acquire/Hardware/Simulator/fakeCam.py
@@ -259,15 +259,15 @@ def run(self):
             while ((not self.aqRunning) or (self.numBufferedImages > self.bufferlength/2.)) and (not self.kill) :
                 time.sleep(.01)
 
-            zPos = (self.zPiezo.GetPos() - self.zOffset)*1e3
+            zPos = (self.zPiezo.effective_pos - self.zOffset)*1e3
 
             xp = 0
             yp = 0
             if not self.xPiezo is None:
-                xp = (self.xPiezo.GetPos() - self.xPiezo.max_travel/2)*1e3
+                xp = (self.xPiezo.effective_pos - self.xPiezo.max_travel/2)*1e3
 
             if not self.xPiezo is None:
-                yp = (self.yPiezo.GetPos() - self.yPiezo.max_travel/2)*1e3
+                yp = (self.yPiezo.effective_pos - self.yPiezo.max_travel/2)*1e3
 
             roi_bbox = (xp,yp, xp+self.XVals[-1], yp + self.YVals[-1])
 
diff --git a/PYME/Acquire/Hardware/Simulator/fakePiezo.py b/PYME/Acquire/Hardware/Simulator/fakePiezo.py
index f54fc8d5e..edde5e316 100755
--- a/PYME/Acquire/Hardware/Simulator/fakePiezo.py
+++ b/PYME/Acquire/Hardware/Simulator/fakePiezo.py
@@ -22,6 +22,9 @@
 ##################
 
 from PYME.Acquire.Hardware.Piezos.base_piezo import PiezoBase
+import threading
+import numpy as np
+import time
 
 class FakePiezo(PiezoBase):
     gui_description = 'Fake %s-piezo'
@@ -64,4 +67,29 @@ def GetMax(self, iChan=1):
     def __getattr__(self, name):
         if name == 'lastPos':
             return self.curpos
-        else: raise AttributeError(name)  # <<< DON'T FORGET THIS LINE !!
+        else: 
+            raise AttributeError(name)  # <<< DON'T FORGET THIS LINE !!
+        
+class DriftyFakePiezo(FakePiezo):
+    """Fake piezo that drifts over time using a random-walk model. Drift rate is specified in um/s^2.
+    
+    We use a 100ms time step.
+    """
+    def __init__(self, maxtravel = 100.00, drift_rate = 0.01):
+        FakePiezo.__init__(self, maxtravel)
+        self._100ms_drift_rate = drift_rate*np.sqrt(0.1)
+
+        self.drift = 0.0 # initial drift
+
+        self._drift_thread = threading.Thread(target=self._drift)
+        self._drift_thread.setDaemon(True)
+        self._drift_thread.start()
+
+    def _drift(self):
+        while True:
+            self.drift += self._100ms_drift_rate*np.random.randn()
+            time.sleep(0.1)
+
+    @property
+    def effective_pos(self):
+        return self.curpos + self.drift
diff --git a/PYME/Acquire/Hardware/Simulator/simcontrol.py b/PYME/Acquire/Hardware/Simulator/simcontrol.py
index 5406c7209..e772eff13 100644
--- a/PYME/Acquire/Hardware/Simulator/simcontrol.py
+++ b/PYME/Acquire/Hardware/Simulator/simcontrol.py
@@ -145,7 +145,7 @@ def __init__(self, scope=None, states=['Caged', 'On', 'Blinked', 'Bleached'],
         self.states = states
         self.stateTypes = stateTypes
         self.activeState = activeState
-        self.scope = scope # type: PYME.Acquire.microscope.microscope
+        self.scope = scope # type: PYME.Acquire.microscope.Microscope
 
         if scope:
             scope.StatusCallbacks.append(self.simulation_status)
diff --git a/PYME/Acquire/Hardware/focus_locks/reflection_focus_lock.py b/PYME/Acquire/Hardware/focus_locks/reflection_focus_lock.py
index d94bce2c3..178721a28 100644
--- a/PYME/Acquire/Hardware/focus_locks/reflection_focus_lock.py
+++ b/PYME/Acquire/Hardware/focus_locks/reflection_focus_lock.py
@@ -116,7 +116,7 @@ def __init__(self, scope, piezo, p=1., i=0.1, d=0.05, sample_time=0.01,
 
         Parameters
         ----------
-        scope: PYME.Acquire.Hardware.microscope.microscope
+        scope: PYME.Acquire.Hardware.microscope.Microscope
         piezo: PYME.Acquire.Hardware.Piezos.offsetPiezoREST.OffsetPiezoClient
         p: float
         i: float
diff --git a/PYME/Acquire/PYMEAcquire.py b/PYME/Acquire/PYMEAcquire.py
index 6de12f457..934cc2970 100755
--- a/PYME/Acquire/PYMEAcquire.py
+++ b/PYME/Acquire/PYMEAcquire.py
@@ -94,30 +94,41 @@ def OnInit(self):
 def main():
     import os
     import sys
-    from optparse import OptionParser
+    #from optparse import OptionParser
+    import argparse
     setup_logging()
     
     from PYME import config
     
     logger = logging.getLogger(__name__)
-    parser = OptionParser()
-    parser.add_option("-i", "--init-file", dest="initFile",
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-i", "--init-file", dest="initFile",
                       help="Read initialisation from file [defaults to init.py]",
                       metavar="FILE", default='init.py')
 
-    parser.add_option("-m", "--gui_mode", dest="gui_mode", default='default',
+    parser.add_argument("-m", "--gui_mode", dest="gui_mode", default='default',
                       help="GUI mode for PYMEAcquire - either default or 'compact'")
 
-    parser.add_option("-t", "--title", dest="window_title", default='PYME Acquire',
+    parser.add_argument("-t", "--title", dest="window_title", default='PYME Acquire',
                       help="Set the PYMEAcquire display name (useful when running multiple copies - e.g. for drift tracking)")
     
-    parser.add_option('-p', '--port', dest='port', default=8999, help='port to use for server functions')
-    parser.add_option('-s', '--server', dest='server', default=False, action='store_true', help='run in server mode')
-    parser.add_option('-b', '--browser', dest='browser', default=False, action='store_true', help='launch web browser based ui')
+    parser.add_argument('-p', '--port', dest='port', default=8999, help='port to use for server functions')
+    parser.add_argument('-a', '--bind_addr', dest='bind_addr', default=None, help='address to bind to for server functions (defaults to localhost). Only bind to an external address if you are on a trusted network and *really* know what you are doing. NB - university networks should generally not be trusted.')
+    parser.add_argument('-s', '--server', dest='server', default=False, action='store_true', help='run in server mode')
+    parser.add_argument('-b', '--browser', dest='browser', default=False, action='store_true', help='launch web browser based ui')
+    parser.add_argument('-e', '--threaded_event_loop', dest='threaded_event_loop', default=False, action='store_true', help='Run hardware event loop in separate thread. Required (and implied) for server mode.')
+    parser.add_argument('-I', '--ipy', dest='ipy', default=False, action='store_true', help='launch ipython server for remote control')
+    parser.add_argument('--no-wx', dest='no_wx', default=False, action='store_true', help='run without wx gui')
 
+    #(options, args) = parser.parse_args()
+
+    options = parser.parse_args()
+
+    print(options)
+
+    if options.server:
+        options.threaded_event_loop = True
 
-    (options, args) = parser.parse_args()
-    
     # continue to support loading scripts from the PYMEAcquire/Scripts directory
     legacy_scripts_dir = os.path.join(os.path.dirname(__file__), 'Scripts')
     
@@ -133,8 +144,20 @@ def main():
 
     logger.debug('using initialization script %s, %s' % (init_file, os.path.abspath(init_file)))
 
-    application = BoaApp(options, 0)
-    application.MainLoop()
+    if options.no_wx:
+        #implies server mode (with or without a browser GUI)
+        assert options.server
+
+        from PYME.Acquire.acquire_server import AcquireHTTPServer
+        server = AcquireHTTPServer(options, port=int(options.port), bind_addr=options.bind_addr)
+        if options.browser:
+            import webbrowser
+            webbrowser.open('http://localhost:8999') #FIXME - delay this until server is up
+
+        server.run()
+    else:
+        application = BoaApp(options, 0)
+        application.MainLoop()
 
 if __name__ == '__main__':
     from PYME.util import mProfile, fProfile
diff --git a/PYME/Acquire/Scripts/init.py b/PYME/Acquire/Scripts/init.py
index b25b2371f..e8666dc34 100755
--- a/PYME/Acquire/Scripts/init.py
+++ b/PYME/Acquire/Scripts/init.py
@@ -38,8 +38,14 @@
 @init_hardware('Fake Piezos')
 def pz(scope):
     from PYME.Acquire.Hardware.Simulator import fakePiezo
-    scope.fakePiezo = fakePiezo.FakePiezo(100)
+    from PYME.Acquire.Hardware.Piezos import offsetPiezoREST
+
+    scope._fakePiezo = fakePiezo.FakePiezo(100)
+    #scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True)
+
+    scope.fakePiezo = offsetPiezoREST.server_class()(scope._fakePiezo)
     scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True)
+
     
     scope.fakeXPiezo = fakePiezo.FakePiezo(10000)
     scope.register_piezo(scope.fakeXPiezo, 'x')
@@ -49,6 +55,7 @@ def pz(scope):
 
 pz.join() #piezo must be there before we start camera
 
+
 @init_hardware('Fake Camera')
 def cm(scope):
     import numpy as np
@@ -133,7 +140,7 @@ def fake_dmd(MainFrame, scope):
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 cm.join()
@@ -152,11 +159,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    #MainFrame.time1.WantNotification.append(lcf.update)
+    #MainFrame.time1.register_callback(lcf.update)
     #MainFrame.camPanels.append((lcf, 'Laser Control'))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Laser Control'))
 
 @init_gui('Focus Keys')
diff --git a/PYME/Acquire/Scripts/init_N1_Ti_Exeter_ZIx.py b/PYME/Acquire/Scripts/init_N1_Ti_Exeter_ZIx.py
index 734059c88..cf05cc2ba 100644
--- a/PYME/Acquire/Scripts/init_N1_Ti_Exeter_ZIx.py
+++ b/PYME/Acquire/Scripts/init_N1_Ti_Exeter_ZIx.py
@@ -118,7 +118,7 @@ def GetComputerName():
 InitGUI('''
 scope.camControls['Zyla'] = AndorNeoControlFrame.AndorNeoPanel(MainFrame, scope.cameras['Zyla'], scope)
 camPanels.append((scope.camControls['Zyla'], 'Zyla Properties'))
-time1.WantNotification.append(scope.camControls['Zyla'].refresh)
+time1.register_callback(scope.camControls['Zyla'].refresh)
 
 #scope.camControls['B - Right'] = AndorControlFrame.AndorPanel(MainFrame, scope.cameras['B - Right'], scope)
 #camPanels.append((scope.camControls['B - Right'], 'EMCCD B Properties'))
@@ -185,7 +185,7 @@ class chaninfo:
 pt = positionTracker.PositionTracker(scope, time1)
 pv = positionTracker.TrackerPanel(MainFrame, pt)
 MainFrame.AddPage(page=pv, select=False, caption='Track')
-time1.WantNotification.append(pv.draw)
+time1.register_callback(pv.draw)
 ''')
 
 #splitter
@@ -214,7 +214,7 @@ class chaninfo:
 
 TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath)
 toolPanels.append((TiPanel, 'Nikon Ti'))
-time1.WantNotification.append(TiPanel.SetSelections)
+time1.register_callback(TiPanel.SetSelections)
 
 MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata)
 MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata)
@@ -223,7 +223,7 @@ class chaninfo:
 InitGUI('''
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope)
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 ''')
 
 #from PYME.Acquire.Hardware import frZStage
@@ -313,14 +313,14 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 ''')
 
 InitGUI('''
 from PYME.Acquire import lasersliders
 lsf = lasersliders.LaserSliders(toolPanel, scope.lasers)
-time1.WantNotification.append(lsf.update)
+time1.register_callback(lsf.update)
 #lsf.update()
 camPanels.append((lsf, 'Laser Powers'))
 ''')
@@ -331,7 +331,7 @@ class chaninfo:
     scope.arclampshutter = priorLumen.PriorLumen('Arc lamp shutter', portname=hwconfig['Lumen200S'].portname())
     scope.shuttercontrol = [scope.arclampshutter]
     acf = arclampshutterpanel.Arclampshutterpanel(MainFrame,scope.shuttercontrol)
-    time1.WantNotification.append(acf.refresh)
+    time1.register_callback(acf.refresh)
     camPanels.append((acf, 'Shutter Control'))
 except:
     print('Error starting arc-lamp shutter ...')
diff --git a/PYME/Acquire/Scripts/init_N2_Ti_Zyla_lasers.py b/PYME/Acquire/Scripts/init_N2_Ti_Zyla_lasers.py
index 9c19f0df6..843359467 100644
--- a/PYME/Acquire/Scripts/init_N2_Ti_Zyla_lasers.py
+++ b/PYME/Acquire/Scripts/init_N2_Ti_Zyla_lasers.py
@@ -93,9 +93,9 @@ class chaninfo:
 
 TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath)
 toolPanels.append((TiPanel, 'Nikon Ti'))
-#time1.WantNotification.append(TiPanel.SetSelections)
-time1.WantNotification.append(scope.dichroic.Poll)
-time1.WantNotification.append(scope.lightpath.Poll)
+#time1.register_callback(TiPanel.SetSelections)
+time1.register_callback(scope.dichroic.Poll)
+time1.register_callback(scope.lightpath.Poll)
 
 MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata)
 MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata)
@@ -104,7 +104,7 @@ class chaninfo:
 InitGUI('''
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope)
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 ''')
 
 #from PYME.Acquire.Hardware import frZStage
@@ -178,14 +178,14 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 ''')
 
 InitGUI('''
 from PYME.Acquire import lasersliders
 lsf = lasersliders.LaserSliders(toolPanel, scope.lasers)
-time1.WantNotification.append(lsf.update)
+time1.register_callback(lsf.update)
 #lsf.update()
 camPanels.append((lsf, 'Laser Powers'))
 ''')
diff --git a/PYME/Acquire/Scripts/init_Neo.py b/PYME/Acquire/Scripts/init_Neo.py
index 3b3f84d28..e4539166b 100644
--- a/PYME/Acquire/Scripts/init_Neo.py
+++ b/PYME/Acquire/Scripts/init_Neo.py
@@ -127,7 +127,7 @@ class chaninfo:
 #pt = positionTracker.PositionTracker(scope, time1)
 #pv = positionTracker.TrackerPanel(MainFrame, pt)
 #MainFrame.AddPage(page=pv, select=False, caption='Track')
-#time1.WantNotification.append(pv.draw)
+#time1.register_callback(pv.draw)
 #""")
 
 #splitter
@@ -148,7 +148,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope)
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 #from PYME.Acquire.Hardware import frZStage
@@ -226,7 +226,7 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 """)
 #
diff --git a/PYME/Acquire/Scripts/init_NeoSim.py b/PYME/Acquire/Scripts/init_NeoSim.py
index 90fbbde66..7d891a564 100644
--- a/PYME/Acquire/Scripts/init_NeoSim.py
+++ b/PYME/Acquire/Scripts/init_NeoSim.py
@@ -96,7 +96,7 @@ class chaninfo:
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 cm.join()
@@ -109,7 +109,7 @@ class chaninfo:
 #InitGUI("""
 #from PYME.Acquire.Hardware import LaserControlFrame
 #lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-#time1.WantNotification.append(lcf.refresh)
+#time1.register_callback(lcf.refresh)
 #lcf.Show()
 #toolPanels.append((lcf, 'Laser Control'))
 #""")
@@ -127,7 +127,7 @@ class chaninfo:
 #InitGUI("""
 #from PYME.Acquire.Hardware import focusKeys
 #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0])
-#time1.WantNotification.append(fk.refresh)
+#time1.register_callback(fk.refresh)
 #""")
 
 #InitGUI("""
diff --git a/PYME/Acquire/Scripts/init_TIRF.py b/PYME/Acquire/Scripts/init_TIRF.py
index 5af00fbab..0dd6c6c86 100644
--- a/PYME/Acquire/Scripts/init_TIRF.py
+++ b/PYME/Acquire/Scripts/init_TIRF.py
@@ -94,7 +94,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0])
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 InitGUI("""
@@ -102,7 +102,7 @@ class chaninfo:
 pt = positionTracker.PositionTracker(scope, time1)
 pv = positionTracker.TrackerPanel(MainFrame, pt)
 MainFrame.AddPage(page=pv, select=False, caption='Track')
-time1.WantNotification.append(pv.draw)
+time1.register_callback(pv.draw)
 """)
 
 #splitter
@@ -189,7 +189,7 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 """)
 
diff --git a/PYME/Acquire/Scripts/init_TIRF_Neo.py b/PYME/Acquire/Scripts/init_TIRF_Neo.py
index 15bc09ce2..1f69aea03 100644
--- a/PYME/Acquire/Scripts/init_TIRF_Neo.py
+++ b/PYME/Acquire/Scripts/init_TIRF_Neo.py
@@ -104,7 +104,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0])
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 #InitGUI("""
@@ -112,7 +112,7 @@ class chaninfo:
 #pt = positionTracker.PositionTracker(scope, time1)
 #pv = positionTracker.TrackerPanel(MainFrame, pt)
 #MainFrame.AddPage(page=pv, select=False, caption='Track')
-#time1.WantNotification.append(pv.draw)
+#time1.register_callback(pv.draw)
 #""")
 
 #splitter
@@ -199,7 +199,7 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 """)
 
diff --git a/PYME/Acquire/Scripts/init_TIRF_NeoO.py b/PYME/Acquire/Scripts/init_TIRF_NeoO.py
index 867b7904a..0b146749d 100644
--- a/PYME/Acquire/Scripts/init_TIRF_NeoO.py
+++ b/PYME/Acquire/Scripts/init_TIRF_NeoO.py
@@ -104,7 +104,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0])
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 #InitGUI("""
@@ -112,7 +112,7 @@ class chaninfo:
 #pt = positionTracker.PositionTracker(scope, time1)
 #pv = positionTracker.TrackerPanel(MainFrame, pt)
 #MainFrame.AddPage(page=pv, select=False, caption='Track')
-#time1.WantNotification.append(pv.draw)
+#time1.register_callback(pv.draw)
 #""")
 
 #splitter
@@ -198,7 +198,7 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 """)
 
diff --git a/PYME/Acquire/Scripts/init_TIRF_onecam.py b/PYME/Acquire/Scripts/init_TIRF_onecam.py
index 0525b22cf..23d23e8a9 100644
--- a/PYME/Acquire/Scripts/init_TIRF_onecam.py
+++ b/PYME/Acquire/Scripts/init_TIRF_onecam.py
@@ -95,7 +95,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0])
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 InitGUI("""
@@ -103,7 +103,7 @@ class chaninfo:
 pt = positionTracker.PositionTracker(scope, time1)
 pv = positionTracker.TrackerPanel(MainFrame, pt)
 MainFrame.AddPage(page=pv, select=False, caption='Track')
-time1.WantNotification.append(pv.draw)
+time1.register_callback(pv.draw)
 """)
 
 #splitter
@@ -190,7 +190,7 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 """)
 
diff --git a/PYME/Acquire/Scripts/init_Ti.py b/PYME/Acquire/Scripts/init_Ti.py
index 9230ce380..f1f1533de 100644
--- a/PYME/Acquire/Scripts/init_Ti.py
+++ b/PYME/Acquire/Scripts/init_Ti.py
@@ -108,7 +108,7 @@ class chaninfo:
 #pt = positionTracker.PositionTracker(scope, time1)
 #pv = positionTracker.TrackerPanel(MainFrame, pt)
 #MainFrame.AddPage(page=pv, select=False, caption='Track')
-#time1.WantNotification.append(pv.draw)
+#time1.register_callback(pv.draw)
 #""")
 
 #splitter
@@ -137,7 +137,7 @@ class chaninfo:
 
 TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath)
 toolPanels.append((TiPanel, 'Nikon Ti'))
-time1.WantNotification.append(TiPanel.SetSelections)
+time1.register_callback(TiPanel.SetSelections)
 
 MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata)
 MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata)
@@ -146,7 +146,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope)
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 #from PYME.Acquire.Hardware import frZStage
@@ -223,7 +223,7 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 """)
 #
diff --git a/PYME/Acquire/Scripts/init_UOA_n.py b/PYME/Acquire/Scripts/init_UOA_n.py
index 6ff265341..17d2b6998 100644
--- a/PYME/Acquire/Scripts/init_UOA_n.py
+++ b/PYME/Acquire/Scripts/init_UOA_n.py
@@ -139,7 +139,7 @@ def power_meter(scope):
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 # @init_hardware('Lasers')
@@ -167,11 +167,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     # lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    # MainFrame.time1.WantNotification.append(lcf.update)
+    # MainFrame.time1.register_callback(lcf.update)
     # MainFrame.camPanels.append((lcf, 'Lasers', False, False))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Lasers', False, False))
 
 # @init_hardware('Line scanner')
diff --git a/PYME/Acquire/Scripts/init_UOA_n2.py b/PYME/Acquire/Scripts/init_UOA_n2.py
index 72280dfbc..b5e5ca61e 100644
--- a/PYME/Acquire/Scripts/init_UOA_n2.py
+++ b/PYME/Acquire/Scripts/init_UOA_n2.py
@@ -135,7 +135,7 @@ def samp_db(MainFrame, scope):
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 # @init_hardware('Lasers')
@@ -163,11 +163,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     # lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    # MainFrame.time1.WantNotification.append(lcf.update)
+    # MainFrame.time1.register_callback(lcf.update)
     # MainFrame.camPanels.append((lcf, 'Lasers', False, False))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Lasers', False, False))
 
 # @init_hardware('Line scanner')
diff --git a/PYME/Acquire/Scripts/init_Y1.py b/PYME/Acquire/Scripts/init_Y1.py
index f3f4eb441..c2d485803 100644
--- a/PYME/Acquire/Scripts/init_Y1.py
+++ b/PYME/Acquire/Scripts/init_Y1.py
@@ -142,7 +142,7 @@ def GetComputerName():
 pt = positionTracker.PositionTracker(scope, time1)
 pv = positionTracker.TrackerPanel(MainFrame, pt)
 MainFrame.AddPage(page=pv, select=False, caption='Track')
-time1.WantNotification.append(pv.draw)
+time1.register_callback(pv.draw)
 """, 'Position Tracker')
 
 #splitter
@@ -162,9 +162,9 @@ def GetComputerName():
 
 TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath)
 toolPanels.append((TiPanel, 'Nikon Ti'))
-#time1.WantNotification.append(TiPanel.SetSelections)
-time1.WantNotification.append(scope.dichroic.Poll)
-time1.WantNotification.append(scope.lightpath.Poll)
+#time1.register_callback(TiPanel.SetSelections)
+time1.register_callback(scope.dichroic.Poll)
+time1.register_callback(scope.lightpath.Poll)
 
 MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata)
 MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata)
@@ -216,13 +216,13 @@ def GetComputerName():
 InitGUI("""
 from PYME.Acquire.ui import lasersliders
 lsf = lasersliders.LaserSliders(toolPanel, scope.state)
-time1.WantNotification.append(lsf.update)
+time1.register_callback(lsf.update)
 #lsf.update()
 camPanels.append((lsf, 'Laser Powers'))
 
 if 'lasers' in dir(scope):
     lcf = lasersliders.LaserToggles(toolPanel, scope.state)
-    time1.WantNotification.append(lcf.update)
+    time1.register_callback(lcf.update)
     camPanels.append((lcf, 'Laser Control'))
 """, 'Laser Sliders')
 
diff --git a/PYME/Acquire/Scripts/init_Zyla.py b/PYME/Acquire/Scripts/init_Zyla.py
index 49e333b99..7551e9927 100644
--- a/PYME/Acquire/Scripts/init_Zyla.py
+++ b/PYME/Acquire/Scripts/init_Zyla.py
@@ -127,7 +127,7 @@ class chaninfo:
 #pt = positionTracker.PositionTracker(scope, time1)
 #pv = positionTracker.TrackerPanel(MainFrame, pt)
 #MainFrame.AddPage(page=pv, select=False, caption='Track')
-#time1.WantNotification.append(pv.draw)
+#time1.register_callback(pv.draw)
 #""")
 
 #splitter
@@ -148,7 +148,7 @@ class chaninfo:
 #InitGUI("""
 #from PYME.Acquire.Hardware import focusKeys
 #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope)
-#time1.WantNotification.append(fk.refresh)
+#time1.register_callback(fk.refresh)
 #""")
 
 #from PYME.Acquire.Hardware import frZStage
@@ -226,7 +226,7 @@ class chaninfo:
 #if 'lasers'in dir(scope):
 #    from PYME.Acquire.Hardware import LaserControlFrame
 #    lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-#    time1.WantNotification.append(lcf.refresh)
+#    time1.register_callback(lcf.refresh)
 #    toolPanels.append((lcf, 'Laser Control'))
 #""")
 ##
diff --git a/PYME/Acquire/Scripts/init_drift_tracking.py b/PYME/Acquire/Scripts/init_drift_tracking.py
index a3139e307..2061db2fa 100644
--- a/PYME/Acquire/Scripts/init_drift_tracking.py
+++ b/PYME/Acquire/Scripts/init_drift_tracking.py
@@ -62,7 +62,7 @@ def drift_tracking(MainFrame, scope):
     scope.dt = driftTracking.Correlator(scope, scope.piFoc)
     dtp = driftTrackGUI.DriftTrackingControl(MainFrame, scope.dt)
     MainFrame.camPanels.append((dtp, 'Focus Lock'))
-    MainFrame.time1.WantNotification.append(dtp.refresh)
+    MainFrame.time1.register_callback(dtp.refresh)
 
 @init_gui('Focus Keys')
 def focus_keys(MainFrame, scope):
diff --git a/PYME/Acquire/Scripts/init_emccd_basic.py b/PYME/Acquire/Scripts/init_emccd_basic.py
index ddab2934a..a3cac84cf 100644
--- a/PYME/Acquire/Scripts/init_emccd_basic.py
+++ b/PYME/Acquire/Scripts/init_emccd_basic.py
@@ -128,7 +128,7 @@ class chaninfo:
 #pt = positionTracker.PositionTracker(scope, time1)
 #pv = positionTracker.TrackerPanel(MainFrame, pt)
 #MainFrame.AddPage(page=pv, select=False, caption='Track')
-#time1.WantNotification.append(pv.draw)
+#time1.register_callback(pv.draw)
 #""")
 
 #splitter
@@ -157,7 +157,7 @@ class chaninfo:
 #
 #TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath)
 #toolPanels.append((TiPanel, 'Nikon Ti'))
-#time1.WantNotification.append(TiPanel.SetSelections)
+#time1.register_callback(TiPanel.SetSelections)
 #
 #MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata)
 #MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata)
@@ -166,7 +166,7 @@ class chaninfo:
 #InitGUI("""
 #from PYME.Acquire.Hardware import focusKeys
 #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope)
-#time1.WantNotification.append(fk.refresh)
+#time1.register_callback(fk.refresh)
 #""")
 
 #from PYME.Acquire.Hardware import frZStage
@@ -243,7 +243,7 @@ class chaninfo:
 #if 'lasers'in dir(scope):
 #    from PYME.Acquire.Hardware import LaserControlFrame
 #    lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-#    time1.WantNotification.append(lcf.refresh)
+#    time1.register_callback(lcf.refresh)
 #    toolPanels.append((lcf, 'Laser Control'))
 #""")
 #
diff --git a/PYME/Acquire/Scripts/init_htsms.py b/PYME/Acquire/Scripts/init_htsms.py
index d58e0199a..cad3817af 100644
--- a/PYME/Acquire/Scripts/init_htsms.py
+++ b/PYME/Acquire/Scripts/init_htsms.py
@@ -175,7 +175,7 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
 
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Laser Powers'))
 
 @init_gui('Failsafe')
@@ -197,7 +197,7 @@ def multiview_selection(MainFrame, scope):
     from PYME.Acquire.ui import multiview_select
 
     ms = multiview_select.MultiviewSelect(MainFrame.toolPanel, scope)
-    MainFrame.time1.WantNotification.append(ms.update)
+    MainFrame.time1.register_callback(ms.update)
     MainFrame.camPanels.append((ms, 'Multiview Selection'))
 
 @init_gui('Focus Keys')
@@ -207,7 +207,7 @@ def focus_keys(MainFrame, scope):
     fk = focusKeys.FocusKeys(MainFrame, scope.piFoc)
     panel = FocusLockPanel(MainFrame, scope.focus_lock, offset_piezo=scope.piFoc)
     MainFrame.camPanels.append((panel, 'Focus Lock'))
-    MainFrame.time1.WantNotification.append(panel.refresh)
+    MainFrame.time1.register_callback(panel.refresh)
 
 @init_gui('Action manager')
 def action_manager(MainFrame, scope):
diff --git a/PYME/Acquire/Scripts/init_htsms_focus_lock.py b/PYME/Acquire/Scripts/init_htsms_focus_lock.py
index f0ab6497d..427ca8e03 100644
--- a/PYME/Acquire/Scripts/init_htsms_focus_lock.py
+++ b/PYME/Acquire/Scripts/init_htsms_focus_lock.py
@@ -52,7 +52,7 @@ def pifoc(scope):
 #
 #         fg.SetData(np.arange(scope.frameWrangler.currentFrame.shape[1]), scope.frameWrangler.currentFrame.sum(0))
 #
-#     MainFrame.time1.WantNotification.append(refr_profile)
+#     MainFrame.time1.register_callback(refr_profile)
 
 @init_gui('Focus Lock')
 def focus_lock(MainFrame, scope):
@@ -73,7 +73,7 @@ def focus_lock(MainFrame, scope):
     scope.focus_lock.register()
     panel = FocusLockPanel(MainFrame, scope.focus_lock)
     MainFrame.camPanels.append((panel, 'Focus Lock'))
-    MainFrame.time1.WantNotification.append(panel.refresh)
+    MainFrame.time1.register_callback(panel.refresh)
     # we don't benefit at all from multiple frames piling up in a polling interval, so try and match the camera cycle
     # Can't get frame rate higher than ~297 Hz for the current ROI, so default to just under that
     scope.state['Camera.IntegrationTime'] = 0.0035
@@ -88,7 +88,7 @@ def focus_lock(MainFrame, scope):
     #         profile = profile - scope.focus_lock.subtraction_profile
     #     fg.SetData(np.arange(scope.frameWrangler.currentFrame.shape[1]), profile)
     #
-    # MainFrame.time1.WantNotification.append(refresh_profile)
+    # MainFrame.time1.register_callback(refresh_profile)
 
     # # display setpoint / error over time
     n = 500
@@ -107,7 +107,7 @@ def refresh_position(*args, **kwargs):
         # time[-1] = scope.focus_lock._last_time
         position_plot.SetData(time, position)
 
-    MainFrame.time1.WantNotification.append(refresh_position)
+    MainFrame.time1.register_callback(refresh_position)
 
     # panel to log focus to file at set intervals
     focus_logger = FocusLogger(scope.focus_lock.GetPeakPosition)
diff --git a/PYME/Acquire/Scripts/init_rev.py b/PYME/Acquire/Scripts/init_rev.py
index 4ddbda76d..5f1a504a8 100644
--- a/PYME/Acquire/Scripts/init_rev.py
+++ b/PYME/Acquire/Scripts/init_rev.py
@@ -59,7 +59,7 @@ def GetComputerName():
 scope.dt = driftTracking.correlator(scope, scope.piFoc)
 dtp = driftTrackGUI.DriftTrackingControl(MainFrame, scope.dt)
 camPanels.append((dtp, 'Focus Lock'))
-time1.WantNotification.append(dtp.refresh)
+time1.register_callback(dtp.refresh)
 """, 'Drift Tracking')
 
 
diff --git a/PYME/Acquire/Scripts/init_sim100.py b/PYME/Acquire/Scripts/init_sim100.py
index 17b861314..9b9754339 100755
--- a/PYME/Acquire/Scripts/init_sim100.py
+++ b/PYME/Acquire/Scripts/init_sim100.py
@@ -109,7 +109,7 @@ def fake_dmd(MainFrame, scope):
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 cm.join()
@@ -128,11 +128,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lcf.update)
+    MainFrame.time1.register_callback(lcf.update)
     MainFrame.camPanels.append((lcf, 'Laser Control'))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Laser Powers'))
 
 @init_gui('Focus Keys')
diff --git a/PYME/Acquire/Scripts/init_sim103.py b/PYME/Acquire/Scripts/init_sim103.py
index 7bbe9e80b..db289e95e 100755
--- a/PYME/Acquire/Scripts/init_sim103.py
+++ b/PYME/Acquire/Scripts/init_sim103.py
@@ -109,7 +109,7 @@ def fake_dmd(MainFrame, scope):
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 cm.join()
@@ -128,11 +128,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lcf.update)
+    MainFrame.time1.register_callback(lcf.update)
     MainFrame.camPanels.append((lcf, 'Laser Control'))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Laser Powers'))
 
 @init_gui('Focus Keys')
diff --git a/PYME/Acquire/Scripts/init_sim2.py b/PYME/Acquire/Scripts/init_sim2.py
new file mode 100644
index 000000000..c507d1f32
--- /dev/null
+++ b/PYME/Acquire/Scripts/init_sim2.py
@@ -0,0 +1,211 @@
+#!/usr/bin/python
+
+##################
+# init.py
+#
+# Copyright David Baddeley, 2009
+# d.baddeley@auckland.ac.nz
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##################
+
+#!/usr/bin/python
+from PYME.Acquire.ExecTools import joinBGInit, HWNotPresent, init_gui, init_hardware
+from PYME import config
+import scipy
+import time
+
+# Set a microscope name which describes this hardware configuration (e.g. a room number or similar)
+# Used with the splitting ratio database and in other places where a microscope identifier is required.
+scope.microscope_name = 'PYMESimulator'
+
+# set some defaults for PYMEAcquire
+# uncomment the line below for high-thoughput style directory hashing
+# config.config['acquire-spool_subdirectories'] = True
+
+@init_hardware('Fake Piezos')
+def pz(scope):
+    from PYME.Acquire.Hardware.Simulator import fakePiezo
+    scope.fakePiezo = fakePiezo.FakePiezo(100)
+    scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True)
+    
+    scope.fakeXPiezo = fakePiezo.FakePiezo(10000)
+    scope.register_piezo(scope.fakeXPiezo, 'x')
+    
+    scope.fakeYPiezo = fakePiezo.FakePiezo(10000)
+    scope.register_piezo(scope.fakeYPiezo, 'y')
+
+pz.join() #piezo must be there before we start camera
+
+@init_hardware('Fake Camera')
+def cm(scope):
+    import numpy as np
+    from PYME.Acquire.Hardware.Simulator import fakeCam
+    from PYME.Acquire.Hardware import multiview
+    size = 256
+    cam = fakeCam.FakeCamera(size, #70*np.arange(0.0, 4*256.0),
+                                             size, #70*np.arange(0.0, 256.0),
+                                             fakeCam.NoiseMaker(),
+                                             scope.fakePiezo, xpiezo = scope.fakeXPiezo,
+                                             ypiezo = scope.fakeYPiezo,
+                                             pixel_size_nm=70.,
+                                             illumFcn = 'ROIIllumFunction'
+                                             )
+    cam.SetEMGain(150)
+    
+    # mv_cam = multiview.MultiviewWrapper(cam, multiview_info = {
+    #                                                             'Multiview.NumROIs': 4,
+    #                                                             'Multiview.ChannelColor': [0, 1, 1, 0],
+    #                                                             'Multiview.DefaultROISize': (size, size),
+    #                                                             'Multiview.ROISizeOptions': [128, 240, 256],
+    #                                                             'Multiview.ROI0Origin': (0, 0),
+    #                                                             'Multiview.ROI1Origin': (size, 0),
+    #                                                             'Multiview.ROI2Origin': (2*size, 0),
+    #                                                             'Multiview.ROI3Origin': (3*size, 0),
+    #                                                         }, 
+    #                                         default_roi= {
+    #                                                         'xi' : 0,
+    #                                                         'yi' : 0,
+    #                                                         'xf' : size*4,
+    #                                                         'yf' : size
+    #                                                      })
+    scope.register_camera(cam,'Fake Camera')
+
+#scope.EnableJoystick = 'foo'
+
+#InitBG('Should Fail', """
+#raise Exception, 'test error'
+#time.sleep(1)
+#""")
+#
+#InitBG('Should not be there', """
+#raise HWNotPresent, 'test error'
+#time.sleep(1)
+#""")
+
+
+# @init_gui('Simulation UI')
+# def sim_controls(MainFrame, scope):
+#     from PYME.Acquire.Hardware.Simulator import dSimControl
+#     dsc = dSimControl.dSimControl(MainFrame, scope)
+#     MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings')
+#
+#     scope.dsc = dsc
+
+
+@init_gui('Simulation UI')
+def sim_controls(MainFrame, scope):
+    from PYME.Acquire.Hardware.Simulator import simcontrol, simui_wx
+    #pre-polulate for dSTORM using tweaked values
+    #note, probabilities are [spontaneous/s, switching laser/Ws, readout laser/Ws]
+    transition_tensor =  simcontrol.fluor.createSimpleTransitionMatrix(pPA=[1e9, 0, 0],
+                        pOnDark=[0, 0, 0.1],
+                        pDarkOn=[0.02,0.001, 0],
+                        pOnBleach=[0, 0, 0.00])
+    scope.simcontrol = simcontrol.SimController(scope, 
+                                                transistion_tensor=transition_tensor,
+                                                spectral_signatures=[[1, 0.05], [0.05, 1]],
+                                                splitter_info=([0, 0, 500., 500.], [0, 1, 1, 0]),
+                                                excitation_crossections=(1, 200))
+    #scope.simcontrol.change_num_channels(4)
+    #scope.simcontrol.set_psf_model(simcontrol.PSFSettings(zernike_modes={4:1.5}))
+    dsc = simui_wx.dSimControl(MainFrame, scope.simcontrol, show_status=False)
+    MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings')
+
+    msc = simui_wx.MiniSimPanel(MainFrame, scope.simcontrol)
+    MainFrame.camPanels.append((msc, 'Simulation'))
+
+    from PYME.simulation import pointsets
+    scope.simcontrol.point_gen = simcontrol.RandomDistribution(n_instances=25,region_size=70e3, 
+                                                                generator=simcontrol.Group(generators=[pointsets.WiglyFibreSource(),
+                                                                    #simcontrol.AssignChannel(channel=1, generator=pointsets.SHNucleusSource())
+                                                                    ]))
+    scope.simcontrol.generate_fluorophores()
+    
+    scope.dsc = dsc
+
+@init_gui('Camera controls')
+def cam_controls(MainFrame, scope):
+    from PYME.Acquire.Hardware.AndorIXon import AndorControlFrame
+    scope.camControls['Fake Camera'] = AndorControlFrame.AndorPanel(MainFrame, scope.cam, scope)
+    MainFrame.camPanels.append((scope.camControls['Fake Camera'], 'EMCCD Properties', False))
+
+cm.join()
+
+
+@init_hardware('Lasers')
+def lasers(scope):
+    from PYME.Acquire.Hardware import lasers
+    scope.l642 = lasers.FakeLaser('l642',scope.cam,1, initPower=1)
+    scope.l642.register(scope)
+    #scope.l405 = lasers.FakeLaser('l405',scope.cam,0, initPower=10)
+    #scope.l405.register(scope)
+    
+
+@init_gui('Laser controls')
+def laser_controls(MainFrame, scope):
+    from PYME.Acquire.ui import lasersliders
+    
+    #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
+    #MainFrame.time1.register_callback(lcf.update)
+    #MainFrame.camPanels.append((lcf, 'Laser Control'))
+    
+    lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
+    MainFrame.time1.register_callback(lsf.update)
+    MainFrame.camPanels.append((lsf, 'Laser Control'))
+
+@init_gui('Focus Keys')
+def focus_keys(MainFrame, scope):
+    from PYME.Acquire.Hardware import focusKeys
+    fk = focusKeys.FocusKeys(MainFrame, scope.piezos[0])
+
+# @init_gui('Sample Metadata')
+# def sample_metadata(main_frame, scope):
+#     from PYME.Acquire.sampleInformation import SimpleSampleInfoPanel
+#     sampanel = SimpleSampleInfoPanel(main_frame)
+#     main_frame.camPanels.append((sampanel, 'Sample Metadata'))
+#     # Prefill the data for our simulated structure
+#     sampanel.slide.SetValue('Sim01')
+#     sampanel.notes.SetValue('Chan0: WiglyFibre')
+
+@init_gui('Action manager')
+def action_manager(MainFrame, scope):
+    from PYME.Acquire.ui import actionUI
+    
+    ap = actionUI.ActionPanel(MainFrame, scope.actions, scope)
+    MainFrame.AddPage(ap, caption='Queued Actions')
+
+
+@init_gui('Tiling')
+def action_manager(MainFrame, scope):
+    from PYME.Acquire.ui import tile_panel
+    
+    ap = tile_panel.TilePanel(MainFrame, scope)
+    MainFrame.aqPanels.append((ap, 'Tiling'))
+
+@init_gui('Chained Analysis')
+def chained_analysis(main_frame, scope):
+    from PYME.Acquire.htsms.rule_ui import SMLMChainedAnalysisPanel, get_rule_tile, RuleChain
+    from PYME.cluster.rules import RecipeRuleFactory, SpoolLocalLocalizationRuleFactory
+    from PYME.IO.MetaDataHandler import DictMDHandler
+    
+    SMLMChainedAnalysisPanel.plug(main_frame, scope)
+
+
+#must be here!!!
+joinBGInit() #wait for anything which was being done in a separate thread
+
+
+scope.initDone = True
diff --git a/PYME/Acquire/Scripts/init_sim50.py b/PYME/Acquire/Scripts/init_sim50.py
index b0c7c7d5a..fb704cac4 100755
--- a/PYME/Acquire/Scripts/init_sim50.py
+++ b/PYME/Acquire/Scripts/init_sim50.py
@@ -109,7 +109,7 @@ def fake_dmd(MainFrame, scope):
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 cm.join()
@@ -128,11 +128,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lcf.update)
+    MainFrame.time1.register_callback(lcf.update)
     MainFrame.camPanels.append((lcf, 'Laser Control'))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Laser Powers'))
 
 @init_gui('Focus Keys')
diff --git a/PYME/Acquire/Scripts/init_sim75.py b/PYME/Acquire/Scripts/init_sim75.py
index aa2e5fc59..dcb683875 100755
--- a/PYME/Acquire/Scripts/init_sim75.py
+++ b/PYME/Acquire/Scripts/init_sim75.py
@@ -109,7 +109,7 @@ def fake_dmd(MainFrame, scope):
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 cm.join()
@@ -128,11 +128,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lcf.update)
+    MainFrame.time1.register_callback(lcf.update)
     MainFrame.camPanels.append((lcf, 'Laser Control'))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Laser Powers'))
 
 @init_gui('Focus Keys')
diff --git a/PYME/Acquire/Scripts/init_sim_drift_tracking.py b/PYME/Acquire/Scripts/init_sim_drift_tracking.py
new file mode 100644
index 000000000..36c9c950f
--- /dev/null
+++ b/PYME/Acquire/Scripts/init_sim_drift_tracking.py
@@ -0,0 +1,166 @@
+#!/usr/bin/python
+
+##################
+# init_TIRF.py
+#
+# Copyright David Baddeley, 2009
+# d.baddeley@auckland.ac.nz
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##################
+from PYME.Acquire.ExecTools import joinBGInit, HWNotPresent, init_gui, init_hardware
+
+from PYME.Acquire.Hardware import fakeShutters
+import time
+import os
+import sys
+
+def GetComputerName():
+    if sys.platform == 'win32':
+        return os.environ['COMPUTERNAME']
+    else:
+        return os.uname()[1]
+
+#scope.cameras = {}
+#scope.camControls = {}
+from PYME.IO import MetaDataHandler
+
+
+#PIFoc
+@init_hardware('PIFoc')
+def pifoc(scope):
+    import sys
+    if sys.version_info.major > 2:
+        from PYME.Acquire.Hardware.Piezos import offsetPiezoREST as offsetPiezo
+    else:
+        from PYME.Acquire.Hardware.Piezos import offsetPiezo
+    scope.piFoc = offsetPiezo.getClient()
+    scope.register_piezo(scope.piFoc, 'z')
+
+pifoc.join()
+
+@init_hardware('Fake Piezos')
+def pz(scope):
+    from PYME.Acquire.Hardware.Simulator import fakePiezo
+    scope.fakePiezo = scope.piFoc# fakePiezo.FakePiezo(100)
+    #scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True)
+    
+    scope.fakeXPiezo = fakePiezo.FakePiezo(10000)
+    scope.register_piezo(scope.fakeXPiezo, 'x')
+    
+    scope.fakeYPiezo = fakePiezo.FakePiezo(10000)
+    scope.register_piezo(scope.fakeYPiezo, 'y')
+
+pz.join() #piezo must be there before we start camera
+
+
+
+@init_hardware('Cameras')
+def cam(scope):
+    from PYME.Acquire.Hardware.Simulator import fakeCam
+    cam = fakeCam.FakeCamera(256, #70*np.arange(0.0, 4*256.0),
+                                             256, #70*np.arange(0.0, 256.0),
+                                             fakeCam.NoiseMaker(),
+                                             scope.fakePiezo, xpiezo = scope.fakeXPiezo,
+                                             ypiezo = scope.fakeYPiezo,
+                                             pixel_size_nm=70.,
+                                             )
+    cam.SetEMGain(150)
+    scope.register_camera(cam,'Fake Camera')
+
+@init_gui('Simulation UI')
+def sim_controls(MainFrame, scope):
+    from PYME.Acquire.Hardware.Simulator import simcontrol, simui_wx
+    # simulate some fluorescent fiducials
+    #note, probabilities are [spontaneous/s, switching laser/Ws, readout laser/Ws]
+    transition_tensor =  simcontrol.fluor.createSimpleTransitionMatrix(pPA=[1e9, 0, 0],
+                        pOnDark=[0, 0,0],
+                        pDarkOn=[0.02,0.001, 0],
+                        pOnBleach=[0, 0, 0.00])
+    scope.simcontrol = simcontrol.SimController(scope, 
+                                                transistion_tensor=transition_tensor,
+                                                spectral_signatures=[[1, 0.05], [0.05, 1]],
+                                                splitter_info=([0, 0, 500., 500.], [0, 1, 1, 0]),
+                                                excitation_crossections=(1, 200))
+    #scope.simcontrol.change_num_channels(4)
+    #scope.simcontrol.set_psf_model(simcontrol.PSFSettings(zernike_modes={4:1.5}))
+    dsc = simui_wx.dSimControl(MainFrame, scope.simcontrol, show_status=False)
+    MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings')
+
+    msc = simui_wx.MiniSimPanel(MainFrame, scope.simcontrol)
+    MainFrame.camPanels.append((msc, 'Simulation'))
+
+    from PYME.simulation import pointsets
+    scope.simcontrol.point_gen = simcontrol.Shift(dx=5000, dy=5000, generator=pointsets.RandomSource(numPoints=20))
+    #scope.simcontrol.point_gen = simcontrol.RandomDistribution(n_instances=25,region_size=70e3, 
+    #                                                            generator=simcontrol.Group(generators=[pointsets.WiglyFibreSource(),
+                                                                    #simcontrol.AssignChannel(channel=1, generator=pointsets.SHNucleusSource())
+    #                                                                ]))
+    scope.simcontrol.generate_fluorophores()
+    
+    scope.dsc = dsc
+
+@init_gui('Camera controls')
+def cam_controls(MainFrame, scope):
+    from PYME.Acquire.Hardware.AndorIXon import AndorControlFrame
+    scope.camControls['Fake Camera'] = AndorControlFrame.AndorPanel(MainFrame, scope.cam, scope)
+    MainFrame.camPanels.append((scope.camControls['Fake Camera'], 'EMCCD Properties', False))
+
+cam.join()
+
+
+@init_hardware('Lasers')
+def lasers(scope):
+    from PYME.Acquire.Hardware import lasers
+    scope.l642 = lasers.FakeLaser('l642',scope.cam,1, initPower=1)
+    scope.l642.register(scope)
+    #scope.l405 = lasers.FakeLaser('l405',scope.cam,0, initPower=10)
+    #scope.l405.register(scope)
+    
+
+@init_gui('Laser controls')
+def laser_controls(MainFrame, scope):
+    from PYME.Acquire.ui import lasersliders
+    
+    #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
+    #MainFrame.time1.register_callback(lcf.update)
+    #MainFrame.camPanels.append((lcf, 'Laser Control'))
+    
+    lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
+    MainFrame.time1.register_callback(lsf.update)
+    MainFrame.camPanels.append((lsf, 'Laser Control'))
+
+@init_gui('Drift tracking')
+def drift_tracking(MainFrame, scope):
+    from PYME.Acquire.Hardware import driftTracking, driftTrackGUI
+    scope.dt = driftTracking.Correlator(scope, scope.piFoc)
+    dtp = driftTrackGUI.DriftTrackingControl(MainFrame, scope.dt)
+    MainFrame.camPanels.append((dtp, 'Focus Lock'))
+    MainFrame.time1.register_callback(dtp.refresh)
+
+@init_gui('Focus Keys')
+def focus_keys(MainFrame, scope):
+    from PYME.Acquire.Hardware import focusKeys
+    fk = focusKeys.FocusKeys(MainFrame, scope.piezos[0])
+
+
+
+#must be here!!!
+joinBGInit() #wait for anyhting which was being done in a separate thread
+
+#scope.SetCamera('A')
+
+time.sleep(.5)
+scope.initDone = True
diff --git a/PYME/Acquire/Scripts/init_sim_htsms.py b/PYME/Acquire/Scripts/init_sim_htsms.py
index 6a41a35f1..7d81307ab 100644
--- a/PYME/Acquire/Scripts/init_sim_htsms.py
+++ b/PYME/Acquire/Scripts/init_sim_htsms.py
@@ -155,7 +155,7 @@ def multiview_selection(MainFrame, scope):
     from PYME.Acquire.ui import multiview_select
 
     ms = multiview_select.MultiviewSelect(MainFrame.toolPanel, scope)
-    MainFrame.time1.WantNotification.append(ms.update)
+    MainFrame.time1.register_callback(ms.update)
     MainFrame.camPanels.append((ms, 'Multiview Selection'))
 
 @init_hardware('Lasers')
@@ -172,11 +172,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    #MainFrame.time1.WantNotification.append(lcf.update)
+    #MainFrame.time1.register_callback(lcf.update)
     #MainFrame.camPanels.append((lcf, 'Laser Control'))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Laser Control'))
 
 @init_gui('Focus Keys')
diff --git a/PYME/Acquire/Scripts/init_sim_min.py b/PYME/Acquire/Scripts/init_sim_min.py
index d84f24d94..11b915eef 100755
--- a/PYME/Acquire/Scripts/init_sim_min.py
+++ b/PYME/Acquire/Scripts/init_sim_min.py
@@ -121,7 +121,7 @@ def cm(scope):
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 cm.join()
@@ -148,11 +148,11 @@ def simcontrol(scope):
 #     from PYME.Acquire.ui import lasersliders
 #
 #     #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-#     #MainFrame.time1.WantNotification.append(lcf.update)
+#     #MainFrame.time1.register_callback(lcf.update)
 #     #MainFrame.camPanels.append((lcf, 'Laser Control'))
 #
 #     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-#     MainFrame.time1.WantNotification.append(lsf.update)
+#     MainFrame.time1.register_callback(lsf.update)
 #     MainFrame.camPanels.append((lsf, 'Laser Control'))
 
 # @init_gui('Focus Keys')
diff --git a/PYME/Acquire/Scripts/init_sim_rem.py b/PYME/Acquire/Scripts/init_sim_rem.py
index 0ed0293c2..3b4f9f231 100644
--- a/PYME/Acquire/Scripts/init_sim_rem.py
+++ b/PYME/Acquire/Scripts/init_sim_rem.py
@@ -111,7 +111,7 @@ class chaninfo:
 #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR')
 ##camPanels.append((snrPan, 'SNR etc ...'))
 ##f.Show()
-##time1.WantNotification.append(snrPan.ccdPan.draw)
+##time1.register_callback(snrPan.ccdPan.draw)
 #""")
 
 cm.join()
@@ -124,7 +124,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import LaserControlFrame
 lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-time1.WantNotification.append(lcf.refresh)
+time1.register_callback(lcf.refresh)
 #lcf.Show()
 camPanels.append((lcf, 'Laser Control'))
 """)
@@ -142,7 +142,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0])
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 InitGUI("""
diff --git a/PYME/Acquire/Scripts/init_smi1.py b/PYME/Acquire/Scripts/init_smi1.py
index 4a0e72d18..b09e0e045 100755
--- a/PYME/Acquire/Scripts/init_smi1.py
+++ b/PYME/Acquire/Scripts/init_smi1.py
@@ -60,7 +60,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[-1])
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 
@@ -82,7 +82,7 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 """)
 
@@ -92,7 +92,7 @@ class chaninfo:
 import wx
 
 scope.step = SMI1.CStepOp()
-time1.WantNotification.append(scope.step.ContIO)
+time1.register_callback(scope.step.ContIO)
 
 mb = wx.MessageDialog(sh.GetParent(), 'Continue with Calibration of stage?\\nPLEASE CHECK that the slide holder has been removed\\n(and then press OK)', 'Stage Callibration', wx.YES_NO|wx.NO_DEFAULT)
 ret = mb.ShowModal()
diff --git a/PYME/Acquire/Scripts/init_spectro.py b/PYME/Acquire/Scripts/init_spectro.py
index e2684a9dc..ee9e591bc 100644
--- a/PYME/Acquire/Scripts/init_spectro.py
+++ b/PYME/Acquire/Scripts/init_spectro.py
@@ -65,7 +65,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import LaserControlFrame
 lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-time1.WantNotification.append(lcf.refresh)
+time1.register_callback(lcf.refresh)
 lcf.Show()
 toolPanels.append((lcf, 'Laser Control'))
 """)
diff --git a/PYME/Acquire/Scripts/init_spim.py b/PYME/Acquire/Scripts/init_spim.py
index 3b4ee1ace..6be5f4285 100644
--- a/PYME/Acquire/Scripts/init_spim.py
+++ b/PYME/Acquire/Scripts/init_spim.py
@@ -101,11 +101,11 @@ def laser_controls(MainFrame, scope):
     from PYME.Acquire.ui import lasersliders
     
     # lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state)
-    # MainFrame.time1.WantNotification.append(lcf.update)
+    # MainFrame.time1.register_callback(lcf.update)
     # MainFrame.camPanels.append((lcf, 'Lasers', False, False))
     
     lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state)
-    MainFrame.time1.WantNotification.append(lsf.update)
+    MainFrame.time1.register_callback(lsf.update)
     MainFrame.camPanels.append((lsf, 'Lasers', False, False))
 
 #must be here!!!
diff --git a/PYME/Acquire/Scripts/init_twophoton.py b/PYME/Acquire/Scripts/init_twophoton.py
index ef8ef1869..dd1f36347 100755
--- a/PYME/Acquire/Scripts/init_twophoton.py
+++ b/PYME/Acquire/Scripts/init_twophoton.py
@@ -62,7 +62,7 @@ class chaninfo:
 InitGUI("""
 from PYME.Acquire.Hardware import focusKeys
 fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0])
-time1.WantNotification.append(fk.refresh)
+time1.register_callback(fk.refresh)
 """)
 
 
@@ -72,7 +72,7 @@ class chaninfo:
 import wx
 
 scope.step = SMI1.CStepOp()
-time1.WantNotification.append(scope.step.ContIO)
+time1.register_callback(scope.step.ContIO)
 
 mb = wx.MessageDialog(sh.GetParent(), 'Continue with Calibration of stage?\\nPLEASE CHECK that the slide holder has been removed\\n(and then press OK)', 'Stage Callibration', wx.YES_NO|wx.NO_DEFAULT)
 ret = mb.ShowModal()
@@ -97,7 +97,7 @@ class chaninfo:
 if 'lasers'in dir(scope):
     from PYME.Acquire.Hardware import LaserControlFrame
     lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-    time1.WantNotification.append(lcf.refresh)
+    time1.register_callback(lcf.refresh)
     toolPanels.append((lcf, 'Laser Control'))
 """)
 
diff --git a/PYME/Acquire/Scripts/init_uc480.py b/PYME/Acquire/Scripts/init_uc480.py
index aba0ffb42..18b8419c4 100644
--- a/PYME/Acquire/Scripts/init_uc480.py
+++ b/PYME/Acquire/Scripts/init_uc480.py
@@ -113,7 +113,7 @@ class chaninfo:
 #pt = positionTracker.PositionTracker(scope, time1)
 #pv = positionTracker.TrackerPanel(MainFrame, pt)
 #MainFrame.AddPage(page=pv, select=False, caption='Track')
-#time1.WantNotification.append(pv.draw)
+#time1.register_callback(pv.draw)
 #""")
 
 #splitter
@@ -142,7 +142,7 @@ class chaninfo:
 #
 #TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath)
 #toolPanels.append((TiPanel, 'Nikon Ti'))
-#time1.WantNotification.append(TiPanel.SetSelections)
+#time1.register_callback(TiPanel.SetSelections)
 #
 #MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata)
 #MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata)
@@ -151,7 +151,7 @@ class chaninfo:
 #InitGUI("""
 #from PYME.Acquire.Hardware import focusKeys
 #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope)
-#time1.WantNotification.append(fk.refresh)
+#time1.register_callback(fk.refresh)
 #""")
 
 #from PYME.Acquire.Hardware import frZStage
@@ -228,7 +228,7 @@ class chaninfo:
 #if 'lasers'in dir(scope):
 #    from PYME.Acquire.Hardware import LaserControlFrame
 #    lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-#    time1.WantNotification.append(lcf.refresh)
+#    time1.register_callback(lcf.refresh)
 #    toolPanels.append((lcf, 'Laser Control'))
 #""")
 #
diff --git a/PYME/Acquire/Scripts/init_ueye.py b/PYME/Acquire/Scripts/init_ueye.py
index 40874dfc0..4a57b12ca 100644
--- a/PYME/Acquire/Scripts/init_ueye.py
+++ b/PYME/Acquire/Scripts/init_ueye.py
@@ -112,7 +112,7 @@ class chaninfo:
 #pt = positionTracker.PositionTracker(scope, time1)
 #pv = positionTracker.TrackerPanel(MainFrame, pt)
 #MainFrame.AddPage(page=pv, select=False, caption='Track')
-#time1.WantNotification.append(pv.draw)
+#time1.register_callback(pv.draw)
 #''')
 
 #splitter
@@ -141,7 +141,7 @@ class chaninfo:
 #
 #TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath)
 #toolPanels.append((TiPanel, 'Nikon Ti'))
-#time1.WantNotification.append(TiPanel.SetSelections)
+#time1.register_callback(TiPanel.SetSelections)
 #
 #MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata)
 #MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata)
@@ -150,7 +150,7 @@ class chaninfo:
 #InitGUI('''
 #from PYME.Acquire.Hardware import focusKeys
 #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope)
-#time1.WantNotification.append(fk.refresh)
+#time1.register_callback(fk.refresh)
 #''')
 
 #from PYME.Acquire.Hardware import frZStage
@@ -227,7 +227,7 @@ class chaninfo:
 #if 'lasers'in dir(scope):
 #    from PYME.Acquire.Hardware import LaserControlFrame
 #    lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers)
-#    time1.WantNotification.append(lcf.refresh)
+#    time1.register_callback(lcf.refresh)
 #    toolPanels.append((lcf, 'Laser Control'))
 #''')
 #
diff --git a/PYME/Acquire/SpoolController.py b/PYME/Acquire/SpoolController.py
index 7fe7425b6..14196baaa 100644
--- a/PYME/Acquire/SpoolController.py
+++ b/PYME/Acquire/SpoolController.py
@@ -7,8 +7,6 @@
 
 
 #import datetime
-#from PYME.Acquire import HDFSpooler, QueueSpooler
-#from PYME.IO import HTTPSpooler
 from PYME.IO import acquisition_backends
 # TODO: change to use a metadata handler / provideStartMetadata hook
 # MetaDataHandler.provideStartMetadata from the init file when
@@ -24,6 +22,9 @@
 from PYME.IO.FileUtils.nameUtils import numToAlpha, getRelFilename, genHDFDataFilepath
 from PYME.IO import unifiedIO, MetaDataHandler
 
+from PYME.Acquire.protocol_acquisition import ProtocolAcquisition
+from PYME.Acquire.xyztc import XYZTCAcquisition, ZStackAcquisition
+
 
 #import PYME.Acquire.Protocols
 import PYME.Acquire.protocol as prot
@@ -52,6 +53,93 @@
 
 from PYME.util import webframework
 
+class ProtocolAcquisitionSettings(object):
+    '''
+    Manages settings which are specific to protocol-based acquisitions
+    '''
+    def __init__(self) -> None:
+        self.protocol = prot.NullProtocol
+        self.protocolZ = prot.NullZProtocol
+        self.z_stepped = False  # z-step during acquisition
+        self.z_dwell = 100 # time to spend at each z level (if z_stepped == True)
+
+    def set_protocol(self, protocolName=None, reloadProtocol=True):
+        """
+        Set the current protocol.
+        
+        Parameters
+        ----------
+        protocolName: str
+            path to protocol file including extension
+        reloadProtocol : bool
+            currently ignored; protocol module is reinitialized regardless.
+        
+        See also: PYME.Acquire.Protocols.
+        """
+
+        if (protocolName is None) or (protocolName == '<None>'):
+            self.protocol = prot.NullProtocol
+            self.protocolZ = prot.NullZProtocol
+        else:
+            #pmod = __import__('PYME.Acquire.Protocols.' + protocolName.split('.')[0],fromlist=['PYME', 'Acquire','Protocols'])
+            
+            #if reloadProtocol:
+            #    reload(pmod) #force module to be reloaded so that changes in the protocol will be recognised
+            pmod = prot.get_protocol(protocol_name=protocolName, reloadProtocol=reloadProtocol)
+
+            self.protocol = pmod.PROTOCOL
+            self.protocol.filename = protocolName
+            
+            self.protocolZ = pmod.PROTOCOL_STACK
+            self.protocolZ.filename = protocolName
+            self.z_dwell = self.protocolZ.dwellTime
+
+    def get_protocol_for_acquistion(self, settings={}):
+        stack = settings.get('z_stepped', self.z_stepped)
+        stack_settings = settings.get('stack_settings', None)
+        
+        # try stack settings for z_dwell, then aq settings.
+        # precedence is settings > stack_settings > self.z_dwell
+        # The reasoning for allowing the dwell time to be set in either the spooling or stack settings is to allow
+        # API users to choose which is most coherent for their use case (it would seem logical to put dwell time with
+        # the other stack settings, but this becomes problematic when sharing stack settings across modalities - e.g.
+        # PALM/STORM and widefield stacks which are likely to share most of the stack settings but have greatly different
+        # z dwell times). PYMEAcquire specifies it in the spooling/series settings by default to allow shared usage
+        # between modalities.
+        if stack_settings:
+            if isinstance(stack_settings, dict):
+                z_dwell = stack_settings.get('DwellFrames', self.z_dwell)
+            else:
+                # have a StackSettings object
+                # TODO - fix this to be a bit more sane and not use private attributes etc ...
+                z_dwell = stack_settings._dwell_frames
+                # z_dwell defaults to -1  (with a meaning of ignore) in StackSettings objects if not value is not
+                # explicitly provided. In this case, use our internal value instead. The reason for the 'ignore'
+                # special value is to allow the same StackSettings object to be used for widefield stacks and
+                # localization series (where sharing everything except dwell time makes sense).
+                if z_dwell < 1:
+                    z_dwell = self.z_dwell
+        else:
+            z_dwell = self.z_dwell
+        
+        z_dwell = settings.get('z_dwell', z_dwell)
+        protocol_name = settings.get('protocol_name', None)
+
+        if protocol_name is None:
+            protocol, protocol_z = self.protocol, self.protocolZ
+        else:
+            pmod = prot.get_protocol(protocol_name)
+            protocol, protocol_z = pmod.PROTOCOL, pmod.PROTOCOL_STACK
+
+        if stack:
+            protocol = protocol_z
+            protocol.dwellTime = z_dwell
+            #print(protocol)
+        else:
+            protocol = protocol
+
+        return protocol
+
 class SpoolController(object):
     def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(month)d_series'):
         """Initialise the spooling controller.
@@ -76,10 +164,6 @@ def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(mont
             #else default to file
             self.spoolType = 'File'
         
-        #dtn = datetime.datetime.now()
-        
-        #dateDict = {'username' : win32api.GetUserName(), 'day' : dtn.day, 'month' : dtn.month, 'year':dtn.year}
-        
         self._base_dir = nameUtils.get_local_data_directory()
         self._dirname = os.sep.join([self._base_dir, ] + nameUtils.get_spool_subdir())
         self._cluster_dirname = self.get_cluster_dirname(self._dirname)
@@ -89,12 +173,17 @@ def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(mont
         self.seriesCounter = 0
         self._series_name = None
 
-        self.protocol = prot.NullProtocol
-        self.protocolZ = prot.NullZProtocol
+        self.acquisition_types = {'ZStackAcquisition': ZStackAcquisition,
+                                  'ProtocolAcquisition': ProtocolAcquisition,
+                                 }
+
+        
+        self.acquisition_type='ProtocolAcquisition'
+        self.protocol_settings = ProtocolAcquisitionSettings()
         
         self.onSpoolProgress = dispatch.Signal()
         self.onSpoolStart = dispatch.Signal()
-        self.onSpoolStop = dispatch.Signal()
+        self.on_stop = dispatch.Signal()
 
         self._analysis_launchers = queue.Queue(3)
         
@@ -102,8 +191,7 @@ def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(mont
         
         #settings which were managed by GUI
         self.hdf_compression_level = 2 # zlib compression level that pytables should use (spool to file and queue)
-        self.z_stepped = False  # z-step during acquisition
-        self.z_dwell = 100 # time to spend at each z level (if z_stepped == True)
+
         self.cluster_h5 = False # spool to h5 on cluster (cluster of one)
         self.pzf_compression_settings=acquisition_backends.ClusterBackend.default_compression_settings # only for cluster spooling
 
@@ -118,19 +206,19 @@ def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(mont
             
     @property
     def available_spool_methods(self):
-        if int(sys.version[0]) < 3:
-            return ['File', 'Queue', 'Cluster']
-        else:
+        if False:#self.acquisition_type == 'ProtocolAcquision':
             return ['File', 'Cluster']
+        else:
+            return ['File', 'Cluster', 'Memory']
         
     def get_info(self):
         info =  {'settings' : {'method' : self.spoolType,
                                 'hdf_compression_level': self.hdf_compression_level,
-                                'z_stepped' : self.z_stepped,
-                                'z_dwell' : self.z_dwell,
+                                'z_stepped' : self.protocol_settings.z_stepped,
+                                'z_dwell' : self.protocol_settings.z_dwell,
                                 'cluster_h5' : self.cluster_h5,
                                 'pzf_compression_settings' : self.pzf_compression_settings,
-                                'protocol_name' : self.protocol.filename,
+                                'protocol_name' : self.protocol_settings.protocol.filename,
                                 'series_name' : self.seriesName
                               },
                  'available_spool_methods' : self.available_spool_methods
@@ -143,6 +231,9 @@ def get_info(self):
         
         return info
     
+    def register_acquisition_type(self, name, cls):
+        self.acquisition_types[name] = cls
+    
     def update_settings(self, settings):
         """
         Sets the state of the `SpoolController` by calling set-methods or by 
@@ -184,14 +275,20 @@ def update_settings(self, settings):
         
         protocol_name = settings.pop('protocol_name', None)
         if protocol_name:
-            self.SetProtocol(protocol_name)
+            self.protocol_settings.SetProtocol(protocol_name)
+
             
         pzf_settings = settings.pop('pzf_compression_settings', None)
         if pzf_settings:
             self.pzf_compression_settings = dict(pzf_settings)
         
         for k, v in settings.items():
-            setattr(self, k, v)
+            if k in ['z_stepped', 'z_dwell']:
+                # these settings belong to the protocol
+                # TODO - rename to protocol.xx???
+                setattr(self.protocol_settings, k, v)
+            else:
+                setattr(self, k, v)
 
         with self._status_changed_condition:
             self._status_changed_condition.notify_all()
@@ -272,6 +369,7 @@ def _GenSeriesName(self):
        
     def _checkOutputExists(self, fn):
         if self.spoolType == 'Cluster':
+            #FIXME - remove dependance on HTTPSpooler
             from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
             # special case for HTTP spooling.  Make sure 000\series.pcs -> 000/series.pcs
             pyme_cluster = self.dirname + '/' + fn.replace('\\', '/')
@@ -400,57 +498,20 @@ def start_spooling(self, fn=None, settings={}, preflight_mode='interactive'):
             via the action manager.
 
         """
+        
         # these settings were managed by the GUI, but are now managed by the 
         # controller, still allow them to be passed in, but default to internals
-        
+
+        acquisition_type = settings.get('acquisition_type', self.acquisition_type)
         
         fn = self.seriesName if fn in ['', None] else fn
-        stack = settings.get('z_stepped', self.z_stepped)
-        compLevel = settings.get('hdf_compression_level', self.hdf_compression_level)
-        pzf_compression_settings = settings.get('pzf_compression_settings', self.pzf_compression_settings)
-        cluster_h5 = settings.get('cluster_h5', self.cluster_h5)
-        maxFrames = settings.get('max_frames', sys.maxsize)
-        stack_settings = settings.get('stack_settings', None)
         
+        #compLevel = settings.get('hdf_compression_level', self.hdf_compression_level)
+        #pzf_compression_settings = settings.get('pzf_compression_settings', self.pzf_compression_settings)
+        cluster_h5 = settings.get('cluster_h5', self.cluster_h5)
         
-        # try stack settings for z_dwell, then aq settings.
-        # precedence is settings > stack_settings > self.z_dwell
-        # The reasoning for allowing the dwell time to be set in either the spooling or stack settings is to allow
-        # API users to choose which is most coherent for their use case (it would seem logical to put dwell time with
-        # the other stack settings, but this becomes problematic when sharing stack settings across modalities - e.g.
-        # PALM/STORM and widefield stacks which are likely to share most of the stack settings but have greatly different
-        # z dwell times). PYMEAcquire specifies it in the spooling/series settings by default to allow shared usage
-        # between modalities.
-        if stack_settings:
-            if isinstance(stack_settings, dict):
-                z_dwell = stack_settings.get('DwellFrames', self.z_dwell)
-            else:
-                # have a StackSettings object
-                # TODO - fix this to be a bit more sane and not use private attributes etc ...
-                z_dwell = stack_settings._dwell_frames
-                # z_dwell defaults to -1  (with a meaning of ignore) in StackSettings objects if not value is not
-                # explicitly provided. In this case, use our internal value instead. The reason for the 'ignore'
-                # special value is to allow the same StackSettings object to be used for widefield stacks and
-                # localization series (where sharing everything except dwell time makes sense).
-                if z_dwell < 1:
-                    z_dwell = self.z_dwell
-        else:
-            z_dwell = self.z_dwell
-        
-        z_dwell = settings.get('z_dwell', z_dwell)
-        
-        if (stack_settings is not None) and (not isinstance(stack_settings, stackSettings.StackSettings)):
-            # let us pass stack settings as a dict, constructing a StackSettings instance as needed
-            stack_settings = stackSettings.StackSettings(**dict(stack_settings))
-        
-        protocol_name = settings.get('protocol_name')
-        if protocol_name is None:
-            protocol, protocol_z = self.protocol, self.protocolZ
-        else:
-            pmod = prot.get_protocol(protocol_name)
-            protocol, protocol_z = pmod.PROTOCOL, pmod.PROTOCOL_STACK
-
         subdirectory  = settings.get('subdirectory', None)
+
         # make directories as needed, makedirs(dir, exist_ok=True) once py2 support is dropped
         if (self.spoolType != 'Cluster') and (not os.path.exists(self.get_dirname(subdirectory))):
                 os.makedirs(self.get_dirname(subdirectory))
@@ -460,59 +521,42 @@ def start_spooling(self, fn=None, settings={}, preflight_mode='interactive'):
             self.seriesName = self._GenSeriesName()
             
             raise IOError('A series with the same name already exists')
-
-        if stack:
-            protocol = protocol_z
-            protocol.dwellTime = z_dwell
-            #print(protocol)
-        else:
-            protocol = protocol
-
-        if (preflight_mode != 'skip') and not preflight.ShowPreflightResults(protocol.PreflightCheck(), preflight_mode):
-            return #bail if we failed the pre flight check, and the user didn't choose to continue
             
           
-        #fix timing when using fake camera
-        if self.scope.cam.__class__.__name__ == 'FakeCamera':
-            fakeCycleTime = self.scope.cam.GetIntegTime()
-        else:
-            fakeCycleTime = None
+        
             
-        frameShape = (self.scope.cam.GetPicWidth(), self.scope.cam.GetPicHeight())
-        
-        if self.spoolType == 'Queue':
-            from PYME.IO import QueueSpooler
-            self.queueName = getRelFilename(self._get_queue_name(fn, subdirectory=subdirectory))
-            self.spooler = QueueSpooler.Spooler(self.queueName, self.scope.frameWrangler.onFrame, 
-                                                frameShape = frameShape, protocol=protocol, 
-                                                guiUpdateCallback=self._ProgressUpate, complevel=compLevel, 
-                                                fakeCamCycleTime=fakeCycleTime, maxFrames=maxFrames, stack_settings=stack_settings)
-        elif self.spoolType == 'Cluster':
-            from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
+        
+        if self.spoolType == 'Cluster':
             self.queueName = self._get_queue_name(fn, pcs=(not cluster_h5), 
                                                   subdirectory=subdirectory)
-            self.spooler = HTTPSpooler.Spooler(self.queueName, self.scope.frameWrangler.onFrame,
-                                               frameShape = frameShape, protocol=protocol,
-                                               guiUpdateCallback=self._ProgressUpate,
-                                               fakeCamCycleTime=fakeCycleTime, maxFrames=maxFrames,
-                                               compressionSettings=pzf_compression_settings, aggregate_h5=cluster_h5, stack_settings=stack_settings)
-           
         else:
-            from PYME.IO import HDFSpooler
-            self.spooler = HDFSpooler.Spooler(self._get_queue_name(fn, subdirectory=subdirectory),
-                                              self.scope.frameWrangler.onFrame,
-                                              frameShape = frameShape, protocol=protocol, 
-                                              guiUpdateCallback=self._ProgressUpate, complevel=compLevel, 
-                                              fakeCamCycleTime=fakeCycleTime, maxFrames=maxFrames, stack_settings=stack_settings)
-
-        #TODO - sample info is probably better handled with a metadata hook
-        #if sampInf:
-        #    try:
-        #        sampleInformation.getSampleData(self, self.spooler.md)
-        #    except:
-        #        #the connection to the database will timeout if not present
-        #        #FIXME: catch the right exception (or delegate handling to sampleInformation module)
-        #        pass
+            self.queueName = self._get_queue_name(fn, subdirectory=subdirectory)
+
+
+        from PYME.IO import acquisition_backends
+        backends = {'File': acquisition_backends.HDFBackend,
+                    'Cluster': acquisition_backends.ClusterBackend, 
+                    'Memory': acquisition_backends.MemoryBackend}
+        
+        backend_kwargs = {}
+        if self.spoolType == 'Cluster':
+            backend_kwargs['cluster_h5'] = settings.get('cluster_h5', self.cluster_h5)
+            backend_kwargs['compression_settings'] = settings.get('pzf_compression_settings', self.pzf_compression_settings)
+        elif self.spoolType == 'File':
+            backend_kwargs['complevel'] = settings.get('hdf_compression_level', self.hdf_compression_level)
+        
+        # put preflight mode into settings so we can pass it to the protocol acquisition
+        settings['preflight_mode'] = preflight_mode
+
+        
+        try:
+            self.spooler = self.acquisition_types[acquisition_type].from_spool_settings(self.scope, settings, backend=backends[self.spoolType], backend_kwargs=backend_kwargs, series_name=self.queueName, spool_controller=self)
+        except KeyError:
+            raise RuntimeError('Unknown acquisition type %s' % acquisition_type)
+        
+        self.spooler.on_progress.connect(self._ProgressUpate)
+           
+        
         extra_metadata = settings.get('extra_metadata')
         if extra_metadata is not None:
             self.spooler.md.mergeEntriesFrom(MetaDataHandler.DictMDHandler(extra_metadata))
@@ -524,8 +568,8 @@ def start_spooling(self, fn=None, settings={}, preflight_mode='interactive'):
         self.scope.frameWrangler.stop()
         
         try:
-            self.spooler.onSpoolStop.connect(self.SpoolStopped)
-            self.spooler.StartSpool()
+            self.spooler.on_stop.connect(self.SpoolStopped)
+            self.spooler.start()
         except:
             self.spooler.abort()
             raise
@@ -535,9 +579,44 @@ def start_spooling(self, fn=None, settings={}, preflight_mode='interactive'):
         self.scope.frameWrangler.start()
         
         self.onSpoolStart.send(self)
+
+        if self.spoolType == 'Memory':
+            # open a viewer window for the data that is being acquired
+            self._display_image()
         
         #return a function which can be called to indicate if we are done
         return lambda : self.spooler.spool_complete
+    
+    def _display_image(self):
+        ''' Display the image in a viewer (for memory backend)
+        '''
+        try:
+            assert isinstance(self.spooler.storage, acquisition_backends.MemoryBackend)
+            
+            import wx
+            if not wx.App.IsMainLoopRunning():
+                logger.debug("No wx app, can't show image")
+                return
+            
+            from PYME.DSView import ViewIm3D
+            self._view = ViewIm3D(self.spooler.storage.image)
+            self.scope.frameWrangler.onFrameGroup.connect(self._update_display)
+        except:
+            logger.exception('Error displaying image')
+
+    def _update_display(self, *args, **kwargs):
+        ''' Update the displayed image (for memory backend)
+        '''
+        import wx
+        wx.CallAfter(self._view.view.Redraw)
+
+    def _unlink_display(self, *args, **kwargs):
+        ''' Unlink the display from the frameWrangler (for memory backend)
+        '''
+        try:
+            self.scope.frameWrangler.onFrameGroup.disconnect(self._update_display)
+        except:
+            pass
 
     @property
     def display_dirname(self):
@@ -558,13 +637,23 @@ def display_dirname(self):
 
     def StopSpooling(self, **kwargs):
         """GUI callback to stop spooling."""
-        self.spooler.StopSpool()
+        self.spooler.stop()
         
     def SpoolStopped(self, **kwargs):
         self.seriesCounter +=1
         self.seriesName = self._GenSeriesName()
+
+        logger.info('Spooling stopped')
         
-        self.onSpoolStop.send(self)
+        self.on_stop.send(self)
+
+        try:
+            self.spooler.on_progress.disconnect(self._ProgressUpate)
+            self._ProgressUpate()
+            self._unlink_display()
+        except AttributeError:
+            pass
+
         
     @property
     def autostart_analysis(self):
@@ -577,8 +666,8 @@ def autostart_analysis(self):
     def LaunchAnalysis(self):
         """Launch analysis
         """
-        from PYME.IO import QueueSpooler
-        from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
+        #from PYME.IO import QueueSpooler
+        #from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
         
         dh5view_cmd = 'dh5view'
         if sys.platform == 'win32':
@@ -587,9 +676,7 @@ def LaunchAnalysis(self):
         if self.autostart_analysis:
             dh5view_cmd += ' -g'
         
-        if isinstance(self.spooler, QueueSpooler.Spooler): #queue or not
-            subprocess.Popen('%s -q %s QUEUE://%s' % (dh5view_cmd, self.spooler.tq.URI, self.queueName), shell=True)
-        elif isinstance(self.spooler, HTTPSpooler.Spooler): #queue or not
+        if self.spoolType == 'Cluster': #queue or not
             if self.autostart_analysis:
                 # launch analysis in a separate thread
                 t = threading.Thread(target=self.launch_cluster_analysis)
@@ -611,37 +698,6 @@ def launch_cluster_analysis(self):
         except:
             logger.exception('Error launching analysis for %s' % seriesName)
 
-
-    def SetProtocol(self, protocolName=None, reloadProtocol=True):
-        """
-        Set the current protocol.
-        
-        Parameters
-        ----------
-        protocolName: str
-            path to protocol file including extension
-        reloadProtocol : bool
-            currently ignored; protocol module is reinitialized regardless.
-        
-        See also: PYME.Acquire.Protocols.
-        """
-
-        if (protocolName is None) or (protocolName == '<None>'):
-            self.protocol = prot.NullProtocol
-            self.protocolZ = prot.NullZProtocol
-        else:
-            #pmod = __import__('PYME.Acquire.Protocols.' + protocolName.split('.')[0],fromlist=['PYME', 'Acquire','Protocols'])
-            
-            #if reloadProtocol:
-            #    reload(pmod) #force module to be reloaded so that changes in the protocol will be recognised
-            pmod = prot.get_protocol(protocol_name=protocolName, reloadProtocol=reloadProtocol)
-
-            self.protocol = pmod.PROTOCOL
-            self.protocol.filename = protocolName
-            
-            self.protocolZ = pmod.PROTOCOL_STACK
-            self.protocolZ.filename = protocolName
-            self.z_dwell = self.protocolZ.dwellTime
             
     def SetSpoolMethod(self, method):
         """Set the spooling method
diff --git a/PYME/Acquire/Utils/pointScanner.py b/PYME/Acquire/Utils/pointScanner.py
index c1f5c90c6..9d0907f42 100644
--- a/PYME/Acquire/Utils/pointScanner.py
+++ b/PYME/Acquire/Utils/pointScanner.py
@@ -92,27 +92,6 @@ def genCoords(self):
     def start(self):
         self.running = True
         
-        #pixels = np.array(pixels)
-
-#        if np.isscalar(self.pixels):
-#            #constant - use as number of pixels
-#            #center on current piezo position
-#            self.xp = self.pixelsize*np.arange(-self.pixels/2, self.pixels/2 +1) + self.xpiezo[0].GetPos(self.xpiezo[1])
-#            self.yp = self.pixelsize*np.arange(-self.pixels/2, self.pixels/2 +1) + self.ypiezo[0].GetPos(self.ypiezo[1])
-#        elif np.isscalar(self.pixels[0]):
-#            #a 1D array - numbers in either direction centered on piezo pos
-#            self.xp = self.pixelsize*np.arange(-self.pixels[0]/2, self.pixels[0]/2 +1) + self.xpiezo[0].GetPos(self.xpiezo[1])
-#            self.yp = self.pixelsize*np.arange(-self.pixels[1]/2, self.pixels[1]/2 +1) + self.ypiezo[0].GetPos(self.ypiezo[1])
-#        else:
-#            #actual pixel positions
-#            self.xp = self.pixels[0]
-#            self.yp = self.pixels[1]
-#
-#        self.nx = len(self.xp)
-#        self.ny = len(self.yp)
-#
-#        self.imsize = self.nx*self.ny
-
         self.genCoords()
 
         self.callNum = 0
@@ -128,11 +107,11 @@ def start(self):
         #self.ypiezo[0].MoveTo(self.ypiezo[1], self.yp[0])
 
         #self.scope.SetPos(x=self.xp[0], y = self.yp[0])
-        self.scope.frameWrangler.stop()
-        self.scope.state.setItems({'Positioning.x' : self.xp[0], 'Positioning.y' : self.yp[0]}, stopCamera = True)
-        if self.trigger:
-            self.scope.cam.SetAcquisitionMode(self.scope.cam.MODE_SOFTWARE_TRIGGER)
-        self.scope.frameWrangler.start()
+        with self.scope.frameWrangler.spooling_stopped():
+            self.scope.state.setItems({'Positioning.x' : self.xp[0], 'Positioning.y' : self.yp[0]}, stopCamera = True)
+            if self.trigger:
+                self.scope.cam.SetAcquisitionMode(self.scope.cam.MODE_SOFTWARE_TRIGGER)
+            #self.scope.frameWrangler.start()
 
         #if self.sync:
         #    while not self.xpiezo[0].IsOnTarget(): #wait for stage to move
@@ -144,7 +123,7 @@ def start(self):
 
 
         #self.scope.frameWrangler.WantFrameNotification.append(self.tick)
-        self.scope.frameWrangler.onFrame.connect(self.tick, dispatch_uid=self._uuid)
+        self.scope.frameWrangler.onFrame.connect(self.on_frame, dispatch_uid=self._uuid)
 
         if self.trigger:
             self.scope.cam.FireSoftwareTrigger()
@@ -172,7 +151,7 @@ def _position_for_index(self, callN):
         
         return new_x, new_y
 
-    def tick(self, frameData, **kwargs):
+    def on_frame(self, frameData, **kwargs):
         with self._rlock:
             if not self.running:
                 return
@@ -236,7 +215,7 @@ def _stop(self, send_stop=True):
         #self.scope.SetPos(**self.currPos)
         try:
             #self.scope.frameWrangler.WantFrameNotification.remove(self.tick)
-            self.scope.frameWrangler.onFrame.disconnect(self.tick, dispatch_uid=self._uuid)
+            self.scope.frameWrangler.onFrame.disconnect(self.on_frame, dispatch_uid=self._uuid)
             #if self.sync:
             #    self.scope.frameWrangler.HardwareChecks.remove(self.onTarget)
         except:
diff --git a/PYME/Acquire/Utils/tiler.py b/PYME/Acquire/Utils/tiler.py
index 2c7f3a254..ad674cc4c 100644
--- a/PYME/Acquire/Utils/tiler.py
+++ b/PYME/Acquire/Utils/tiler.py
@@ -8,7 +8,10 @@
 import logging
 logger = logging.getLogger(__name__)
 
-class Tiler(pointScanner.PointScanner):
+from PYME.Acquire.acquisition_base import AcquisitionBase
+from PYME.IO import acquisition_backends
+
+class Tiler(pointScanner.PointScanner, AcquisitionBase):
     def __init__(self, scope, tile_dir, n_tiles = 10, tile_spacing=None, dwelltime = 1, background=0, evtLog=False,
                  trigger=False, base_tile_size=256, return_to_start=True, backend='file'):
         """
@@ -33,10 +36,34 @@ def __init__(self, scope, tile_dir, n_tiles = 10, tile_spacing=None, dwelltime =
         
         self._backend = backend
         
-        self.on_stop = dispatch.Signal()
-        self.progress = dispatch.Signal()
+        #self.on_stop = dispatch.Signal()
+        #self.on_progress = dispatch.Signal()
+        AcquisitionBase.__init__(self)
         
-    
+    @classmethod
+    def from_spool_settings(cls, scope, settings, backend, backend_kwargs={}, series_name=None, spool_controller=None):
+        '''Create an Acquisition object from settings and a backend.'''
+
+        if backend is acquisition_backends.ClusterBackend:
+            backend='cluster'
+        elif backend is acquisition_backends.HDFBackend:
+            backend='file'
+        else:
+            raise ValueError('Unknown backend')
+        
+        tile_dir = settings.get('tile_dir', series_name)
+        
+        return cls(scope=scope, 
+                    tile_dir=tile_dir, 
+                    n_tiles=settings.get('n_tiles', 10), 
+                    tile_spacing=settings.get('tile_spacing', None),
+                    dwelltime=settings.get('dwelltime', 1),
+                    background=settings.get('background', 0),
+                    evtLog=settings.get('evtLog', False),
+                    trigger=settings.get('software_trigger', False),
+                    base_tile_size=settings.get('base_tile_size', 256),
+                    return_to_start=settings.get('return_to_start', True),
+                    backend=backend)
         
     def start(self):
         #self._weights =tile_pyramid.ImagePyramid.frame_weights(self.scope.frameWrangler.currentFrame.shape[:2]).squeeze()
@@ -75,9 +102,9 @@ def start(self):
         
         pointScanner.PointScanner.start(self)
         
-    def tick(self, frameData, **kwargs):
+    def on_frame(self, frameData, **kwargs):
         pos = self.scope.GetPos()
-        pointScanner.PointScanner.tick(self, frameData, **kwargs)
+        pointScanner.PointScanner.on_frame(self, frameData, **kwargs)
         
         d = frameData.astype('f').squeeze()
         if not self.background is None:
@@ -95,11 +122,11 @@ def tick(self, frameData, **kwargs):
         t = time.time()
         if t > (self._last_update_time + 1):
             self._last_update_time = t
-            self.progress.send(self)
+            self.on_progress.send(self)
         
     def _stop(self):
         pointScanner.PointScanner._stop(self, send_stop=False)
-        self.progress.send(self)
+        self.on_progress.send(self)
         t_ = time.time()
         
         logger.info('Finished tile acquisition')
@@ -123,7 +150,10 @@ def _stop(self):
         logger.info('Pyramid complete (dt = %3.2f)' % (time.time()-t_))
             
         self.on_stop.send(self)
-        self.progress.send(self)
+        self.on_progress.send(self)
+
+    def md(self):
+        return self.mdh
 
 class CircularTiler(Tiler, pointScanner.CircularPointScanner):
     def __init__(self, scope, tile_dir, max_radius_um=100, tile_spacing=None, dwelltime=1, background=0, evtLog=False,
@@ -153,7 +183,7 @@ def __init__(self, scope, tile_dir, max_radius_um=100, tile_spacing=None, dwellt
         self._last_update_time = 0
         
         self.on_stop = dispatch.Signal()
-        self.progress = dispatch.Signal()
+        self.on_progress = dispatch.Signal()
 
 
 class MultiwellCircularTiler(object):
diff --git a/PYME/Acquire/acquire_client.py b/PYME/Acquire/acquire_client.py
index 7038b7229..78b031ac0 100644
--- a/PYME/Acquire/acquire_client.py
+++ b/PYME/Acquire/acquire_client.py
@@ -61,4 +61,20 @@ def start_spooling(self, filename='', preflight_mode='abort', settings={}):
         
         requests.post(self.base_url + f'/spool_controller/start_spooling?filename={filename}&preflight_mode={preflight_mode}', json=settings)
 
+        return self.spooling_finished
+
+    def spooling_info(self):
+        """
+        Returns information about the current spooling.
+
+        TODO - Use the long-polling endpoint???
+        """
+        return requests.get(self.base_url + '/spool_controller/info').json()
+    
+    def spooling_finished(self):
+        """
+        Returns True if the spooling is finished, False otherwise.
+        """
+        return self.spooling_info()['status']['spool_complete']
+
     
\ No newline at end of file
diff --git a/PYME/Acquire/acquire_server.py b/PYME/Acquire/acquire_server.py
index 2b3b9837f..f4b3f8fff 100755
--- a/PYME/Acquire/acquire_server.py
+++ b/PYME/Acquire/acquire_server.py
@@ -38,9 +38,9 @@
 
 from PYME.Acquire.acquirebase import PYMEAcquireBase
 
-class PYMEAcquireServer(PYMEAcquireBase):
-    def __init__(self, options = None, evt_loop = None):
-       PYMEAcquireBase.__init__(self, options, evt_loop=evt_loop)
+class PYMEAcquireServerMixin(object):
+    def __init__(self, *args, **kwargs):
+       pass
         
     @webframework.register_endpoint('/get_frame_pzf', mimetype='image/pzf')
     def get_frame_pzf(self):
@@ -185,9 +185,9 @@ def update_scope_state(self, body=''):
 
 from PYME.Acquire import webui
 from PYME.Acquire import SpoolController
-class AcquireHTTPServer(webframework.APIHTTPServer, PYMEAcquireServer):
-    def __init__(self, options, port, bind_addr=None, evt_loop=None):
-        PYMEAcquireServer.__init__(self, options, evt_loop=evt_loop)
+class AcquireHTTPServerMixin(webframework.APIHTTPServer, PYMEAcquireServerMixin):
+    def __init__(self, port, bind_addr=None):
+        PYMEAcquireServerMixin.__init__(self)
 
         if bind_addr is None:
             bind_addr = 'localhost' # bind to localhost by default in an attempt to make this safer
@@ -251,6 +251,12 @@ def run(self):
             logger.info('Closing server ...')
             self.server_close()
 
+class AcquireHTTPServer(PYMEAcquireBase, AcquireHTTPServerMixin):
+    """Server without wx GUI"""
+    def __init__(self, options, port, bind_addr=None, evt_loop=None):
+        PYMEAcquireBase.__init__(self, options, evt_loop=evt_loop)
+        AcquireHTTPServerMixin.__init__(self, port, bind_addr)
+
 
 def main():
     import os
diff --git a/PYME/Acquire/acquirebase.py b/PYME/Acquire/acquirebase.py
index c4f5f3483..37a1dbd16 100644
--- a/PYME/Acquire/acquirebase.py
+++ b/PYME/Acquire/acquirebase.py
@@ -26,14 +26,14 @@ def __init__(self, options=None, evt_loop=None) -> None:
         self.initDone = False
         self.postInit = [] #for protocol compat
         
-        self.scope = microscope.microscope()
+        self.scope = microscope.Microscope()
 
         self.roi_on = False
         self.bin_on = False
-        
-        #functions to call in each polling iteration
-        # Replaces time1 in GUI version
-        self._want_loop_notification = []
+
+        # non-GUI timer (replaces time1 for non-GUI sceduled events)
+        self._timer0 = self.evt_loop.MultiTargetTimer()
+        self._timer0.start(50)
         
         self._is_running = False
         
@@ -64,11 +64,7 @@ def main_loop(self):
         
         logger.debug('Starting post-init')
 
-        if self.scope.cam.CamReady():# and ('chaninfo' in self.scope.__dict__)):
-            self._start_polling_camera()
-
-        self._want_loop_notification.append(self.scope.actions.Tick)
-        self.initDone = True
+        self.non_gui_post_init()
 
         logger.debug('Finished post-init')
         
@@ -81,6 +77,13 @@ def main_loop(self):
             logger.debug('Shutting down')
             self._shutdown()
 
+    def non_gui_post_init(self):
+        if self.scope.cam.CamReady():# and ('chaninfo' in self.scope.__dict__)):
+            self._start_polling_camera()
+
+        self._timer0.register_callback(self.scope.actions.Tick)
+        self.initDone = True
+
 
     def _initialize_hardware(self):
         """
diff --git a/PYME/Acquire/acquiremainframe.py b/PYME/Acquire/acquiremainframe.py
index 8fa895aad..a205ed3cf 100755
--- a/PYME/Acquire/acquiremainframe.py
+++ b/PYME/Acquire/acquiremainframe.py
@@ -47,7 +47,7 @@
 from PYME.Acquire.ui import seqdialog
 from PYME.Acquire.ui import selectCameraPanel
 from PYME.Acquire.ui import splashScreen
-from PYME.Acquire.ui import HDFSpoolFrame
+from PYME.Acquire.ui import spool_panel
 
 from PYME.Acquire import microscope
 from PYME.Acquire import protocol
@@ -58,11 +58,13 @@
 
 
 from PYME.ui.AUIFrame import AUIFrame
+from PYME.Acquire import acquirebase
+
 
 def create(parent, options = None):
     return PYMEMainFrame(parent, options)
 
-class PYMEMainFrame(AUIFrame):
+class PYMEMainFrame(acquirebase.PYMEAcquireBase, AUIFrame):
     def _create_menu(self):     
         #self._menus = {}
         #self.menubar = wx.MenuBar()
@@ -101,25 +103,20 @@ def __init__(self, parent, options = None):
               parent=parent, pos=wx.Point(20, 20), size=wx.Size(600, 800),
               style=wx.DEFAULT_FRAME_STYLE, title= getattr(options, 'window_title', 'PYME Acquire'))
         
-        self._create_menu()
-        self.options = options
+        acquirebase.PYMEAcquireBase.__init__(self, options, evt_loop=mytimer)
 
-        self.snapNum = 0
-
-        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)        
+        self._create_menu()
         
-        self.MainFrame = self #reference to this window for use in scripts etc...
-        protocol.MainFrame = self
+        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
 
+        # create list of UI panels to be added after init
+        # Will be populated buy init script
         self.toolPanels = []
         self.camPanels = []
         self.aqPanels = []
         self.anPanels = []
-        self.postInit = []
 
-        self.initDone = False
-
-        self.scope = microscope.microscope()
+        self._aq_uis = {}
 
         self.splash = splashScreen.SplashScreen(self, self.scope)
         self.splash.Show()
@@ -133,34 +130,24 @@ def __init__(self, parent, options = None):
         self.CreateToolPanel(getattr(options, 'gui_mode', 'default'))
 
         self.SetSize((1030, 895))
-
-        self.roi_on = False
-        self.bin_on = False
         
         self.time1 = mytimer.mytimer()
-        
         self.time1.Start(50)
-        
-        wx.CallAfter(self._initialize_hardware)
-
-        #self.time1.WantNotification.append(self.runInitScript)
-        self.time1.WantNotification.append(self.splash.Tick)
 
+        if self.options.threaded_event_loop:
+            wx.CallAfter(self.run)
+        else:
+            wx.CallAfter(self._initialize_hardware)
 
-    def _initialize_hardware(self):
-        """
-        Launch microscope hardware initialization and start polling for completion
-
-        """
-        #this spawns a new thread to run the initialization script
-        self.scope.initialize(self.options.initFile, self.__dict__)
-
-        logger.debug('Init run, waiting on background threads')
-        
         #poll to see if the init script has run
-        self.time1.WantNotification.append(self._check_init_done)
+        self.time1.register_callback(self._check_init_done)
+        self.time1.register_callback(self.splash.Tick)
 
-        
+    def run(self):
+        import threading
+        self._poll_thread = threading.Thread(target=self.main_loop)
+        self._poll_thread.start()
+     
 
     def _check_init_done(self):
         if self.scope.initialized == True and self._check_init_done in self.time1.WantNotification:
@@ -179,9 +166,7 @@ def _refreshDataStack(self):
         except AttributeError:
 
             pass
-        
-    def _start_polling_camera(self):
-        self.scope.startFrameWrangler()
+
     
     def _add_live_view(self):
         """Gets called once during post-init to start pulling data from the
@@ -203,8 +188,8 @@ def _add_live_view(self):
                 self.vsp = disppanel.dispSettingsPanel2(self, self.view, self.scope)
 
 
-                self.time1.WantNotification.append(self.vsp.RefrData)
-                self.time1.WantNotification.append(self._refreshDataStack)
+                self.time1.register_callback(self.vsp.RefrData)
+                self.time1.register_callback(self._refreshDataStack)
 
                 self.AddPage(page=self.view, select=True,caption='Preview')
 
@@ -231,8 +216,8 @@ def _add_live_view(self):
     def doPostInit(self):
         logger.debug('Starting post-init')
         
-        if self.scope.cam.CamReady():# and ('chaninfo' in self.scope.__dict__)):
-            self._start_polling_camera()
+        if not self.options.threaded_event_loop:
+            self.non_gui_post_init()
         
         for cm in self.postInit:
             logger.debug('Loading GUI component for %s' %cm.name)
@@ -241,10 +226,6 @@ def doPostInit(self):
             except Exception as e:
                 logger.exception('Error whilst initializing %s GUI' % cm.name)
 
-        #if len(self.scope.piezos) > 0.5:
-        #    self.piezo_sl = psliders.PiezoSliders(self.scope.piezos, self, self.scope.joystick)
-        #    self.time1.WantNotification.append(self.piezo_sl.update)
-
         logger.debug('Run all custom GUI init tasks')
             
         
@@ -266,7 +247,7 @@ def doPostInit(self):
 
         
         
-        self.time1.WantNotification.append(self.StatusBarUpdate)
+        self.time1.register_callback(self.StatusBarUpdate)
 
         for t in self.camPanels:
             #print(t)
@@ -274,12 +255,13 @@ def doPostInit(self):
         
         if len(self.scope.positioning.keys()) > 0.5:
             self.pos_sl = positionUI.PositionPanel(self.scope, self, self.scope.joystick)
-            self.time1.WantNotification.append(self.pos_sl.update)
+            self.time1.register_callback(self.pos_sl.update)
 
             self.AddTool(self.pos_sl, 'Positioning')
 
-            self.seq_d = seqdialog.seqPanel(self, self.scope)
-            self.AddAqTool(self.seq_d, 'Z-Stack', pinned=False)
+            self.seq_d = seqdialog.seqPanel(self, self.scope, mode='compact')
+            self.register_acquisition_ui('ZStackAcquisition', (self.seq_d, 'Z-Stack'))
+            #self.AddAqTool(self.seq_d, 'Z-Stack', pinned=False)
             #self.seq_d.Show()
         
         for t in self.toolPanels:
@@ -287,9 +269,21 @@ def doPostInit(self):
             self.AddTool(*t)
             
         if self.scope.cam.CamReady():
-            self.pan_spool = HDFSpoolFrame.PanSpool(self, self.scope)
-            self.AddAqTool(self.pan_spool, 'Time/Blinking series', pinned=False, folded=False)
+            self.pan_protocol = spool_panel.ProtocolAcquisitionPane(self, self.scope)
+            self.register_acquisition_ui('ProtocolAcquisition', (self.pan_protocol, 'Time/Blinking series'))
+            #self.AddAqTool(self.pan_protocol, 'Time/Blinking series', pinned=False, folded=False)
+
+            # for t in self.scope.spoolController.acquisition_types.keys():
+            #     if t in self._aq_uis:
+            #         self.AddAqTool(*self._aq_uis[t], folded=(t != self.scope.spoolController.acquisition_type))
+            #     else:
+            #         logger.warning('No UI registered for acquisition type %s' % t)
+
             
+
+            self.pan_spool = spool_panel.SpoolingPane(self, self.scope, acquisition_uis=self._aq_uis)
+            self.AddAqTool(self.pan_spool, 'Acquisition', pinned=True, folded=False)
+
         for t in self.aqPanels:
             self.AddAqTool(*t)
             
@@ -300,7 +294,7 @@ def doPostInit(self):
 
         #self.splash.Destroy()
 
-        self.time1.WantNotification.append(self.scope.actions.Tick)
+        #self.time1.register_callback(self.scope.actions.Tick)
         
         self.initDone = True
         self._mgr.Update()
@@ -438,7 +432,7 @@ def AddCamTool(self, pane, title, pinned=True, folded=True):
         self.AddTool(pane, title, pinned=pinned, panel=self.camPanel, folded=folded)
         
 
-    def AddAqTool(self, pane, title, pinned=True, folded=True):
+    def AddAqTool(self, pane, title, pinned=False, folded=True):
         """Adds a pane to the Acquisition section of the GUI
         
         Parameters
@@ -450,6 +444,12 @@ def AddAqTool(self, pane, title, pinned=True, folded=True):
         """
         self.AddTool(pane, title, pinned=pinned, panel=self.aqPanel, folded=folded)
         
+    def register_acquisition_ui(self, acquisition_type, panel):
+        """Registers a panel as a UI for a particular acquisition type"""
+        pane, title = panel
+        #self.aqPanels.append((pane, title))
+        pane._aq_type = acquisition_type
+        self._aq_uis[acquisition_type] = (pane, title)
 
     def OnFileOpenStack(self, event):
         #self.dv = dsviewer.DSViewFrame(self)
@@ -537,8 +537,6 @@ def OnMCamRoi(self, event):
         logging.debug('Stopping frame wrangler to set ROI')
         self.scope.frameWrangler.stop()
         
-        #print (self.scope.vp.selection.start.x, self.scope.vp.selection.start.y, self.scope.vp.selection.finish.x, self.scope.vp.selection.finish.y)
-
         if 'validROIS' in dir(self.scope.cam) and self.scope.cam.ROIsAreFixed():
             #special case for cameras with restricted ROIs - eg Neo
             #print('setting ROI')
@@ -610,8 +608,6 @@ def OnMCamRoi(self, event):
     def SetCentredRoi(self, event=None, halfwidth=5):
         self.scope.frameWrangler.stop()
 
-        #print (self.scope.vp.selection.start.x, self.scope.vp.selection.start.y, self.scope.vp.selection.finish.x, self.scope.vp.selection.finish.y)
-
         w = self.scope.cam.GetCCDWidth()
         h = self.scope.cam.GetCCDHeight()
 
diff --git a/PYME/Acquire/acquirewx.py b/PYME/Acquire/acquirewx.py
index bebc4ccf9..dcb5870e3 100644
--- a/PYME/Acquire/acquirewx.py
+++ b/PYME/Acquire/acquirewx.py
@@ -24,124 +24,27 @@
 This contains the bulk of the GUI code for the main window of PYMEAcquire.
 """
 import logging
-import os
-import time
-
-#import PYME.ui.autoFoldPanel as afp
-import PYME.ui.manualFoldPanel as afp
-import wx
-import wx.lib.agw.aui as aui
-import wx.py.shell
 
 logging.basicConfig(level=logging.DEBUG)
 logger = logging.getLogger(__name__)
 
-import PYME.DSView.displaySettingsPanel as disppanel
-from PYME.DSView import arrayViewPanel
-from PYME.DSView import dsviewer as dsviewer
-
-from PYME.ui import mytimer
-
-from PYME.Acquire.ui import positionUI
-from PYME.Acquire.ui import intsliders
-from PYME.Acquire.ui import seqdialog
-from PYME.Acquire.ui import selectCameraPanel
-from PYME.Acquire.ui import splashScreen
-from PYME.Acquire.ui import HDFSpoolFrame
-
-from PYME.Acquire import microscope
-from PYME.Acquire import protocol
-
-from PYME.Acquire import acquire_server
-
-from PYME.IO import MetaDataHandler
-#from PYME.IO.FileUtils import nameUtils
-import six
-
-
-from PYME.ui.AUIFrame import AUIFrame
+from PYME.Acquire.acquiremainframe import PYMEMainFrame   
+from PYME.Acquire.acquire_server import AcquireHTTPServerMixin
 
 def create(parent, options = None):
-    return PYMEMainFrame(parent, options)
-
-class PYMEMainFrame(acquire_server.AcquireHTTPServer, AUIFrame):
-    def _create_menu(self):     
-        #self._menus = {}
-        #self.menubar = wx.MenuBar()
-        
-        ##############
-        #Create the menus
-        
-        #File menu
-        self.AddMenuItem('File', '&Open Stack', self.OnFileOpenStack)
-        self.AddMenuItem('File', itemType='separator')        
-        self.AddMenuItem('File', 'Exit', self.OnFileExit, id = wx.ID_EXIT)
-        
-        #Camera menu
-        self.AddMenuItem('Camera', 'Toggle ROI\tF8', self.OnMCamRoi)
-        self.miCamBin = self.AddMenuItem('Camera', 'Turn binning on', self.OnMCamBin, itemType='check')
-        self.AddMenuItem('Camera', 'Set pixel size', self.OnMCamSetPixelSize)
-        
-        #Acquire menu
-        self.AddMenuItem('Acquire', 'Snapshot\tF5', self.OnMAquireOnePic)
-              
-        #self._init_utils()
-        #self.SetMenuBar(self.menubar)
-        #######################
-        
-        self.SetClientSize(wx.Size(1020, 800))
-        
+    return AcquireWxHTTPServer(parent, options, port=int(options.port), bind_addr=options.bind_addr)
 
-        self.statusBar1 = wx.StatusBar(id=wx.ID_ANY,
-              name='statusBar1', parent=self, style=wx.STB_SIZEGRIP)
-        self.SetStatusBar(self.statusBar1)
-
-
-
-    def __init__(self, parent, options = None):
-        AUIFrame.__init__(self, id=wx.ID_ANY, name='smiMainFrame',
-              parent=parent, pos=wx.Point(20, 20), size=wx.Size(600, 800),
-              style=wx.DEFAULT_FRAME_STYLE, title= getattr(options, 'window_title', 'PYME Acquire'))
-        
-        acquire_server.AcquireHTTPServer.__init__(self, options, port=options.port, evt_loop=mytimer)
-        
-        self._create_menu()
-    
-        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)        
+class AcquireWxHTTPServer(PYMEMainFrame, AcquireHTTPServerMixin):
+    """Server with wx GUI"""
+    def __init__(self, parent, options, port, bind_addr=None):
+        assert(options.threaded_event_loop)
         
-
-        # create list of UI panels to be added after init
-        # Will be populated buy init script
-        self.toolPanels = []
-        self.camPanels = []
-        self.aqPanels = []
-        self.anPanels = []
-
-        self.splash = splashScreen.SplashScreen(self, self.scope)
-        self.splash.Show()
-        
-
-        self.sh = wx.py.shell.Shell(id=-1,
-              parent=self, size=wx.Size(-1, -1), style=0, locals=self.__dict__,
-              introText='PYMEAcquire - note that help, license, etc. below is for Python, not PYME\n\n')
-        self.AddPage(self.sh, caption='Shell')
-
-        self.CreateToolPanel(getattr(options, 'gui_mode', 'default'))
-
-        self.SetSize((1030, 895))
-        
-        self.time1 = mytimer.mytimer()   
-        self.time1.Start(50)
-        
-        wx.CallAfter(self.run)
-        self.time1.WantNotification.append(self._check_init_done)
-
-        self.time1.WantNotification.append(self.splash.Tick) 
+        PYMEMainFrame.__init__(self, parent, options)
+        AcquireHTTPServerMixin.__init__(self, port, bind_addr)
 
     def run(self):
         import threading
-        self._poll_thread = threading.Thread(target=self.main_loop)
-        self._poll_thread.start()
+        PYMEMainFrame.run(self)
 
         if self.options.server:
             # only start the server if requested
@@ -156,540 +59,3 @@ def run(self):
                 ns = dict(scope=self.scope, server=self)
                 print('namespace:', ns)
                 ipy.launch_ipy_server_thread(user_ns=ns)
-        
-
-    def _check_init_done(self):
-        if self.scope.initialized == True and self._check_init_done in self.time1.WantNotification:
-            logger.debug('Backround initialization done')
-            self.time1.WantNotification.remove(self._check_init_done)
-            
-            self.doPostInit()
-            
-            self.time1.Stop()
-            self.time1.Start(500)
-    
-    def _refreshDataStack(self):
-        try:
-            if not (self.view.do.ds.data is self.scope.frameWrangler.currentFrame):
-                self.view.SetDataStack(self.scope.frameWrangler.currentFrame)
-        except ArithmeticError:
-            pass
-        
-    
-    def _add_live_view(self):
-        """Gets called once during post-init to start pulling data from the
-        camera
-        
-        """
-
-        if self.scope.cam.GetPicHeight() > 1:
-            try:
-                self.view.SetDataStack(self.scope.frameWrangler.currentFrame)
-            except AttributeError:
-                self._view = arrayViewPanel.ArrayViewPanel(self, self.scope.frameWrangler.currentFrame, initial_overlays=[])
-                self.view.crosshairs = False
-                self.view.showScaleBar = False
-                self.view.do.leftButtonAction = self.view.do.ACTION_SELECTION
-                self.view.do.showSelection = True
-                self.view.CenteringHandlers.append(self.scope.PanCamera)
-
-                self.vsp = disppanel.dispSettingsPanel2(self, self.view, self.scope)
-
-
-                self.time1.WantNotification.append(self.vsp.RefrData)
-                self.time1.WantNotification.append(self._refreshDataStack)
-
-                self.AddPage(page=self.view, select=True,caption='Preview')
-
-                self.AddCamTool(self.vsp, 'Display')
-
-            self.scope.frameWrangler.onFrameGroup.connect(self.view.Redraw)
-
-        else:
-            #1d data - use graph instead
-            from PYME.ui import fastGraph
-            if 'sp' in dir(self):
-                    pass
-            else:
-                self.sp = fastGraph.SpecGraphPanel(self, self)
-
-                self.AddPage(page=self.sp, select=True,caption='Preview')
-
-            #self.scope.frameWrangler.WantFrameGroupNotification.append(self.sp.refr)
-            self.scope.frameWrangler.onFrameGroup.connect(self.sp.refr)
-            
-        #self.scope.PACallbacks.append(self._refreshDataStack)
-
-
-    def doPostInit(self):
-        logger.debug('Starting post-init')
-        
-        for cm in self.postInit:
-            logger.debug('Loading GUI component for %s' %cm.name)
-            try:
-                cm.run(self, self.scope)
-            except Exception as e:
-                logger.exception('Error whilst initializing %s GUI' % cm.name)
-
-        logger.debug('Run all custom GUI init tasks')
-            
-        
-
-        if self.scope.cam.CamReady():# and ('chaninfo' in self.scope.__dict__)):
-            #self._start_polling_camera()
-            self._add_live_view()
-            
-
-            self.int_sl = intsliders.IntegrationSliders(self, self.scope)
-            self.AddCamTool(self.int_sl, 'Integration Time')
-
-            if len(self.scope.cameras) > 1:
-                self.pCamChoose = selectCameraPanel.CameraChooserPanel(self, self.scope)
-                self.AddCamTool(self.pCamChoose, 'Camera Selection')
-
-
-            
-
-        
-        
-        self.time1.WantNotification.append(self.StatusBarUpdate)
-
-        for t in self.camPanels:
-            #print(t)
-            self.AddCamTool(*t)
-        
-        if len(self.scope.positioning.keys()) > 0.5:
-            self.pos_sl = positionUI.PositionPanel(self.scope, self, self.scope.joystick)
-            self.time1.WantNotification.append(self.pos_sl.update)
-
-            self.AddTool(self.pos_sl, 'Positioning')
-
-            self.seq_d = seqdialog.seqPanel(self, self.scope)
-            self.AddAqTool(self.seq_d, 'Z-Stack', pinned=False)
-            #self.seq_d.Show()
-        
-        for t in self.toolPanels:
-            #print(t)
-            self.AddTool(*t)
-            
-        if self.scope.cam.CamReady():
-            self.pan_spool = HDFSpoolFrame.PanSpool(self, self.scope)
-            self.AddAqTool(self.pan_spool, 'Time/Blinking series', pinned=False, folded=False)
-            
-        for t in self.aqPanels:
-            self.AddAqTool(*t)
-            
-        
-        
-        for t in self.anPanels:
-            self.AddTool(*t, panel=self.aqPanel)
-
-        #self.splash.Destroy()
-
-        #self.time1.WantNotification.append(self.scope.actions.Tick)
-        
-        self.initDone = True
-        self._mgr.Update()
-
-        if 'pCamChoose' in dir(self):
-            self.pCamChoose.OnCCamera(None)
-            
-        logger.debug('Finished post-init')
-        
-        self.Show()
-
-        #fudge to get layout right
-#        panes = self.notebook1.GetAuiManager().AllPanes
-#
-#        self.paneNames = []
-#
-#        for p in panes:
-#            self.paneNames.append(p.name)
-
-        #self.LoadPerspective()
-
-    def _getPerspectiveFilename(self):
-        #print __file__
-        fname = os.path.join(os.path.split(__file__)[0], 'GUILayout.txt')
-        return fname
-
-    def SavePerspective(self):
-        #mgr = self.notebook1.GetAuiManager()
-        persp = self._mgr.SavePerspective()
-
-        for i, p in enumerate(self.paneNames):
-            persp = persp.replace(p, 'pane_%d' % i)
-            
-        f = open(self._getPerspectiveFilename(), 'w')
-        f.write(persp)
-        f.close()
-
-    def LoadPerspective(self):
-        pfname = self._getPerspectiveFilename()
-        if os.path.exists(pfname):
-            f = open(pfname)
-            persp = f.read()
-            f.close()
-
-            for i, p in enumerate(self.paneNames):
-                persp = persp.replace('pane_%d' % i, p)
-
-            self._mgr.LoadPerspective(persp)
-            self._mgr.Update()
-
-
-    def CreateToolPanel(self, mode='default'):
-        self.camPanel = afp.foldPanel(self, -1, wx.DefaultPosition,
-                                     wx.Size(240,1000))
-
-        cpinfo = aui.AuiPaneInfo().Name("camControls").Caption("Hardware").Layer(1).Right().CloseButton(False)
-        #cpinfo.dock_proportion  = int(cpinfo.dock_proportion*1.6)
-        
-        self._mgr.AddPane(self.camPanel, cpinfo)
-        
-        self.toolPanel = self.camPanel
-        
-        # self.toolPanel = afp.foldPanel(self, -1, wx.DefaultPosition,
-        #                              wx.Size(240,1000))
-        #
-        # if mode == 'compact':
-        #     self._mgr.AddPane(self.toolPanel, aui.AuiPaneInfo().
-        #                       Name("hardwareControls").Caption("Hardware").CloseButton(False).BestSize(240, 250), target=cpinfo)
-        # else:
-        #     self._mgr.AddPane(self.toolPanel, aui.AuiPaneInfo().
-        #                   Name("hardwareControls").Caption("Hardware").Layer(2).Position(1).Right().CloseButton(False).BestSize(240, 250))
-
-
-        self.aqPanel = afp.foldPanel(self, -1, wx.DefaultPosition,
-                                     wx.Size(240,1000), single_active_pane=True)
-
-        if mode == 'compact':
-            self._mgr.AddPane(self.toolPanel, aui.AuiPaneInfo().
-                              Name("aqControls").Caption("Acquisition").CloseButton(False), target=cpinfo)
-        else:
-            aqinfo = aui.AuiPaneInfo().Name("aqControls").Caption("Acquisition Tasks").Layer(2).Position(0).Right().CloseButton(False)
-            self._mgr.AddPane(self.aqPanel, aqinfo)
-            aqinfo.dock_proportion  = int(aqinfo.dock_proportion*1.3)
-
-        
-            # self.anPanel = afp.foldPanel(self, -1, wx.DefaultPosition,
-            #                         wx.Size(240, 1000))
-            #
-            # self._mgr.AddPane(self.anPanel, aui.AuiPaneInfo().
-            #               Name("anControls").Caption("Analysis").CloseButton(False), target=aqinfo)
-
-
-
-    def AddTool(self, pane, title, pinned=True, folded=True, panel=None):
-        """Adds a pane to the tools section of the GUI
-        
-        Parameters
-        ----------
-        pane : an instance of a wx.Window derived class
-            The pane to add. Optionally, a list of panes
-        title : string
-            The caption for the panel.
-        """
-        
-        if panel is None:
-            panel = self.toolPanel
-        
-        if isinstance(pane, afp.foldingPane):
-            pane.SetCaption(title).Pin(pinned).Fold(folded)
-            pane.Reparent(panel)
-            panel.AddPane(pane)
-        else:
-            # a normal wx.Panel / wx.Window
-            #print(panel, title, pinned, folded)
-            item = afp.foldingPane(panel, -1, caption=title, pinned=pinned, folded=folded)
-            pane.Reparent(item)
-            item.AddNewElement(pane, priority=1)
-            panel.AddPane(item)
-#        item = self.toolPanel.AddFoldPanel(title, collapsed=False, foldIcons=self.Images)
-#        panel.Reparent(item)
-#        self.toolPanel.AddFoldPanelWindow(item, panel, fpb.FPB_ALIGN_WIDTH, fpb.FPB_DEFAULT_SPACING, 10)
-        #wx.LayoutAlgorithm().LayoutWindow(self, self._leftWindow1)
-
-    def AddCamTool(self, pane, title, pinned=True, folded=True):
-        """Adds a pane to the Camera section of the GUI
-        
-        Parameters
-        ----------
-        pane : an instance of a wx.Window derived class
-            The pane to add
-        title : string
-            The caption for the panel.
-        """
-        
-        self.AddTool(pane, title, pinned=pinned, panel=self.camPanel, folded=folded)
-        
-
-    def AddAqTool(self, pane, title, pinned=True, folded=True):
-        """Adds a pane to the Acquisition section of the GUI
-        
-        Parameters
-        ----------
-        pane : an instance of a wx.Window derived class
-            The pane to add
-        title : string
-            The caption for the panel.
-        """
-        self.AddTool(pane, title, pinned=pinned, panel=self.aqPanel, folded=folded)
-        
-
-    def OnFileOpenStack(self, event):
-        #self.dv = dsviewer.DSViewFrame(self)
-        #self.dv.Show()
-        im = dsviewer.ImageStack(haveGUI=True)
-        dvf = dsviewer.DSViewFrame(im, parent=self, size=(500, 500))
-        dvf.SetSize((500,500))
-        dvf.Show()
-        event.Skip()
-
-    def OnFileExit(self, event):
-        self.Close()
-        #event.Skip()
-        
-    def StatusBarUpdate(self):
-        self.statusBar1.SetStatusText(self.scope.genStatus())
-
-    def OnMAquireStack(self, event):
-        self.seq_d.Show()
-        self.seq_d.Raise()
-        #event.Skip()
-
-    def OnMAquireOnePic(self, event):
-        import numpy as np
-        self.scope.frameWrangler.stop()
-        ds2 = np.atleast_3d(self.scope.frameWrangler.currentFrame.reshape(self.scope.cam.GetPicWidth(),self.scope.cam.GetPicHeight()).copy())
-
-
-        #metadata handling
-        mdh = MetaDataHandler.NestedClassMDHandler()
-        mdh.setEntry('StartTime', time.time())
-        mdh.setEntry('AcquisitionType', 'SingleImage')
-
-        #loop over all providers of metadata
-        for mdgen in MetaDataHandler.provideStartMetadata:
-            mdgen(mdh)
-
-        im = dsviewer.ImageStack(data = ds2, mdh = mdh, titleStub='Unsaved Image')
-        if not im.mode == 'graph':
-            im.mode = 'lite'
-
-        #print im.mode
-        dvf = dsviewer.DSViewFrame(im, mode= im.mode, size=(500, 500))
-        dvf.SetSize((500,500))
-        dvf.Show()
-
-        self.snapNum += 1
-
-        self.scope.frameWrangler.Prepare(True)
-        self.scope.frameWrangler.start()
-
-        #event.Skip()
-
-   
-
-    def OnMCamBin(self, event):
-        logging.debug('Stopping frame wrangler to set ROI')
-        self.scope.frameWrangler.stop()
-        
-        binx, biny = self.scope.state['Camera.Binning']
-        if binx > 1:
-            #turn binning off
-            self.scope.state['Camera.Binning'] = (1,1)
-        else:
-            self.scope.state['Camera.Binning'] = (2, 2)
-
-        logging.debug('Preparing frame wrangler for new ROI size')
-
-        self.scope.frameWrangler.Prepare()
-        self.view.SetDataStack(self.scope.frameWrangler.currentFrame)
-
-        logging.debug('Restarting frame wrangler with new ROI')
-        self.scope.frameWrangler.start()
-        self.view.Refresh()
-        self.view.GetParent().Refresh()
-
-    def OnMCamSetPixelSize(self, event):
-        from PYME.Acquire.ui import voxelSizeDialog
-
-        dlg = voxelSizeDialog.VoxelSizeDialog(self, self.scope)
-        dlg.ShowModal()
-
-
-    def OnMCamRoi(self, event):
-        logging.debug('Stopping frame wrangler to set ROI')
-        self.scope.frameWrangler.stop()
-        
-        if 'validROIS' in dir(self.scope.cam) and self.scope.cam.ROIsAreFixed():
-            #special case for cameras with restricted ROIs - eg Neo
-            #print('setting ROI')
-            logging.debug('setting ROI')
-            dlg = wx.SingleChoiceDialog(self, 'Please select the ROI size', 'Camera ROI', ['%dx%d at (%d, %d)' % roi for roi in self.scope.cam.validROIS])
-            dlg.ShowModal()
-            logging.debug('Dlg Shown')
-            self.scope.cam.SetROIIndex(dlg.GetSelection())
-            dlg.Destroy()
-            
-            x1 = 0
-            y1 = 0
-            x2 = self.scope.cam.GetPicWidth()
-            y2 = self.scope.cam.GetPicHeight()
-            logging.debug('ROI Set')
-        
-        else:
-            if (self.roi_on):
-                x1, y1, x2, y2 = self.scope.state['Camera.ROI']
-
-                self.scope.state['Camera.ROI'] = (0,0, self.scope.cam.GetCCDWidth(), self.scope.cam.GetCCDHeight())
-                self.roi_on = False
-            else:
-                x1, y1, x2, y2 = self.view.do.GetSliceSelection()
-    
-                #if we're splitting colours/focal planes across the ccd, then only allow symetric ROIs
-                if 'splitting' in dir(self.scope.cam):
-                    if self.scope.cam.splitting.lower() == 'left_right':
-                        x1 = min(x1, self.scope.cam.GetCCDWidth() - x2)
-                        x2 = max(x2, self.scope.cam.GetCCDWidth() - x1)
-
-                        if not self.scope.cam.splitterFlip:
-                            x1 = 0
-                            x2 = self.scope.cam.GetCCDWidth()
-
-                    if self.scope.cam.splitting.lower() == 'up_down':
-                        y1 = min(y1, self.scope.cam.GetCCDHeight() - y2)
-                        y2 = max(y2, self.scope.cam.GetCCDHeight() - y1)
-    
-                        if not self.scope.cam.splitterFlip:
-                            y1 = 0
-                            y2 = self.scope.cam.GetCCDHeight()
-                        
-                #self.scope.cam.SetROI(x1,y1,x2,y2)
-                self.scope.state['Camera.ROI'] = (x1,y1,x2,y2)
-
-                self.roi_on = True
-    
-                x1 = 0
-                y1 = 0
-                x2 = self.scope.cam.GetPicWidth()
-                y2 = self.scope.cam.GetPicHeight()
-
-            
-        logging.debug('Preparing frame wrangler for new ROI size')
-        #self.scope.cam.SetCOC()
-        #self.scope.cam.GetStatus()
-        self.scope.frameWrangler.Prepare()
-        self.view.SetDataStack(self.scope.frameWrangler.currentFrame)
-        
-        self.view.do.SetSelection((x1,y1,0), (x2,y2,0))
-
-        logging.debug('Restarting frame wrangler with new ROI')
-        self.scope.frameWrangler.start()
-        self.view.Refresh()
-        self.view.GetParent().Refresh()
-        #event.Skip()
-
-    def SetCentredRoi(self, event=None, halfwidth=5):
-        self.scope.frameWrangler.stop()
-
-        w = self.scope.cam.GetCCDWidth()
-        h = self.scope.cam.GetCCDHeight()
-
-        x1 = w/2 - halfwidth
-        y1 = h/2 - halfwidth
-        x2 = w/2 + halfwidth
-        y2 = h/2 + halfwidth
-
-        #self.scope.cam.SetROI(x1,y1,x2,y2)
-        self.scope.state['Camera.ROI'] = (x1,y1,x2,y2)
-        #self.mCam.SetLabel(wxID_SMIMAINFRAMEMCAMROI, 'Clear ROI\tF8')
-        self.roi_on = True
-
-        x1 = 0
-        y1 = 0
-        x2 = self.scope.cam.GetPicWidth()
-        y2 = self.scope.cam.GetPicHeight()
-
-
-        #self.scope.cam.SetCOC()
-        #self.scope.cam.GetStatus()
-        self.scope.frameWrangler.Prepare()
-        self.view.SetDataStack(self.scope.frameWrangler.currentFrame)
-        
-        self.view.do.SetSelection((x1,y1,0), (x2,y2,0))
-
-        self.scope.frameWrangler.start()
-        self.view.Refresh()
-        self.view.GetParent().Refresh()
-        #event.Skip()
-
-    def OnMDisplayClearSel(self, event):
-        self.view.ResetSelection()
-        #event.Skip()
-
-    @property
-    def view(self):
-        '''Return the current view panel. This is the panel that is used to display the camera data.
-        
-        deprecates .vp
-
-        '''
-        try:
-            return self._view
-        except AttributeError:
-            raise AttributeError('View panel not yet created. This usually happens if .view is accessed too early in the initialisation process.') from None
-
-    @property
-    def vp(self):
-        ''' Backwards compatibility for access to the view panel
-        
-        Generates a deprecation warning - use .view instead
-        '''
-        import warnings
-        warnings.warn('The .vp attribute is deprecated. Use .view instead', DeprecationWarning)
-        return self.view
-
-    def OnCloseWindow(self, event):   
-        self.scope.frameWrangler.stop()
-        self.time1.Stop()
-        if 'cameras' in dir(self.scope):
-            for c in self.scope.cameras.values():
-                c.Shutdown()
-        else:
-            self.scope.cam.Shutdown()
-            
-        for f in self.scope.CleanupFunctions:
-            f()
-            
-        logging.info('All cleanup functions called')
-        
-        time.sleep(1)
-        
-        import threading
-        msg = 'Remaining Threads:\n'
-        for t in threading.enumerate():
-            if six.PY3:
-                cd = None
-                if hasattr(t._target, '__code__'):
-                    cd = t._target.__code__
-                elif hasattr(t._target, '__func__'):
-                    cd = t._target.__func__.__code__
-                elif hasattr(t, '__code__'):
-                    cd = t.__code__
-                else:
-                    # Thread sub-class
-                    try:
-                        cd = t.__class__.run.__code__
-                    except AttributeError:
-                        pass
-                msg += '%s, %s, daemon=%s, %s\n' % (t.name, t._target, t.daemon, cd)
-            else:
-                msg += '%s, %s, daemon=%s\n' % (t, t._Thread__target, t.daemon)
-            
-        logging.info(msg)
-
-        self.Destroy()
-        wx.Exit()
-        
diff --git a/PYME/Acquire/acquisition_base.py b/PYME/Acquire/acquisition_base.py
new file mode 100644
index 000000000..230de7a46
--- /dev/null
+++ b/PYME/Acquire/acquisition_base.py
@@ -0,0 +1,119 @@
+"""
+Base class for acquisition modules 
+
+This exists to document the interface which acquisition types should implement in order 
+to be able to be used with spooling etc ...
+"""
+
+import abc
+from PYME.contrib import dispatch
+
+
+class AcquisitionBase(abc.ABC):
+    @abc.abstractmethod
+    def __init__(self, *args, **kwargs):
+        '''Create an Acquisition object'''
+        
+        # A signal to be emitted when progress is made in the acquisition
+        # this triggers a GUI update, and implementations can choose whether to emit this signal
+        # on every frame, or to throttle it. 
+        self.on_progress = dispatch.Signal()
+
+        # A signal to be emitted when the acquisition is stopped
+        # this should always be emitted when the acquisition is stopped, regardless of the reason
+        self.on_stop = dispatch.Signal()
+
+        # A flag to indicate whether the spool is complete. Ths should be set to True once spooling is finished 
+        # (it is watched by the ActionManager to determine if the acquisition is complete and the next action should be executed)
+        self.spool_complete = False
+
+    @classmethod
+    @abc.abstractmethod
+    def from_spool_settings(cls, scope, settings, backend, backend_kwargs={}, series_name=None, spool_controller=None):
+        '''Create an Acquisition object from settings and a backend.
+        
+        
+        Parameters
+        ----------
+
+        scope : microscope.Microscope
+            The microscope object to use for acquisition. Supplied in order to allow hardware control and access to the frameWrangler.
+
+        settings : dict
+            A dictionary of settings for the acquisition
+
+        backed : class
+            The class of the backend to use for the acquisition. Generally one of PYME.IO.acquisition_backends
+
+        backend_kwargs : dict
+            A dictionary of keyword arguments to pass to the backend constructor
+
+        series_name : str
+            The name of the series to create. series_name should be defined unless the MemoryBackend is used, in which case it is ignored.
+
+        spool_controller : object
+            The spool controller object to use for the acquisition. Generally scope.spoolController. Currently used by ProtocolAcquisition 
+            to access default protocol settings held by the spoolController, but a bit of a legacy hack. May be removed in the future.
+        
+        '''
+        pass
+
+    @abc.abstractmethod
+    def start(self):
+        '''Start the acquisition
+        
+        This will usually record any metadata, connect self.on_frame to a frame source (i.e. scope.frameWrangler.onFrame), and initialise
+        the backend. It should not block, with the bulk of the acquisiton logic taking place in frame source handler.
+        
+        '''
+        pass
+
+    @abc.abstractmethod
+    def stop(self):
+        '''Stop the acquisition
+        
+        This should disconnect from the frame souce, flush any remaining buffers, return hardware to starting state (if modified),
+        do any cleanup, and finalise the backend.
+
+        It is desirable to send a final on_progress signal so that the GUI reflects the actual number of frames spooled etc... .
+
+        Once everything is complete, the on_stop signal should be emitted and the spool_complete flag set.
+        
+        '''
+        pass
+
+    
+    def abort(self):
+        '''Abort the acquisition
+        
+        This should stop the acquisition as quickly as practical. In many cases this can just be a call to stop(),
+        but there is no expectation that buffers will be flushed, remaining protocol steps executed or hardware returned 
+        to a starting state. Especially for cases which involve moving translation stages or other hardware it is desirable
+        that the abort method not result in further movement of hardware unless to a known safe state (e.g. lasers off).
+        
+        Abort should, however, finalise the backend and emit the on_stop signal. 
+        TODO - should abort set spool_complete? 
+        '''
+        self.stop()
+
+    @abc.abstractmethod
+    def md(self):
+        ''' Return acqusition metadata (a PYME.IO.MetaDataHandler object)
+        
+        generally just a short wrapper around the backend metadata
+
+        #TODO - rename this to .mdh, or remove entirely and standardise backend access?
+        '''
+        pass
+        
+    @abc.abstractmethod
+    def on_frame(self, sender, frameData, **kwargs):
+        '''Frame source handler
+        
+        This method should be connected to the frame source signal (i.e. scope.frameWrangler.onFrame) and be responsible for 
+        handling incoming frames, passing them off to storage and performing anything that needs to be done (e.g. hardware movements
+        or protocol task handling) before the next frame. NOTE - this method is called when the frame is retrieved by the frame wrangler
+        - if the camera is running in continuous mode, this may be several frames after the camera has actually acquired the frame. 
+        
+        '''
+        pass
diff --git a/PYME/Acquire/eventLog.py b/PYME/Acquire/eventLog.py
index fb975d3d0..0af750024 100755
--- a/PYME/Acquire/eventLog.py
+++ b/PYME/Acquire/eventLog.py
@@ -51,6 +51,28 @@
 
 WantEventNotification = []
 
+def register_event_handler(EventReceiver):
+    """
+    Register an event handler to receive events.
+    
+    Parameters
+    ----------
+    EventReceiver : object (PYME.IO.events.EventLogger or subclass thereof)
+        an object with a `logEvent(eventName, eventDescr = '', timestamp=None)` method
+    """
+    WantEventNotification.append(EventReceiver)
+
+def remove_event_handler(EventReceiver):
+    """
+    Remove an event handler from the list of event receivers.
+    
+    Parameters
+    ----------
+    EventReceiver : object (PYME.IO.events.EventLogger or subclass thereof)
+        an object with a `logEvent(eventName, eventDescr = '', timestamp=None)` method
+    """
+    WantEventNotification.remove(EventReceiver)
+
 def logEvent(eventName, eventDescr = '', timestamp=None):
     """
     Log an event to all event receivers.
diff --git a/PYME/Acquire/event_loop.py b/PYME/Acquire/event_loop.py
index 8f8b70c29..73561fa1f 100644
--- a/PYME/Acquire/event_loop.py
+++ b/PYME/Acquire/event_loop.py
@@ -17,7 +17,7 @@
 class _Timer(object):
     def __init__(self):
         self._next_trigger = sys.float_info.max
-        self._single_shot = True
+        self._single_shot = False
         self._delay_s = -1
         
         logger.debug('Created timer')
@@ -40,7 +40,7 @@ def check(self, t):
         else:
             return False
     
-    def start(self, delay_ms, single_shot=True):
+    def start(self, delay_ms, single_shot=False):
         self._single_shot = single_shot
         self._delay_s = 0.001*delay_ms
         self._next_trigger = time.time() + self._delay_s
diff --git a/PYME/Acquire/frameWrangler.py b/PYME/Acquire/frameWrangler.py
index f8a23de8a..c40022d03 100644
--- a/PYME/Acquire/frameWrangler.py
+++ b/PYME/Acquire/frameWrangler.py
@@ -53,6 +53,8 @@
 import threading
 #sfrom PYME.ui import mytimer
 
+from contextlib import contextmanager
+
 class FrameWrangler(object):
     """
     Grabs frames from the camera buffers
@@ -398,7 +400,7 @@ def Notify(self, event=None):
                 self.inNotify = False
                 
                 #restart the time so we get called again
-                self.timer.start(self.tiint)
+                self.timer.start(self.tiint, single_shot=True)
             
             
     @property
@@ -461,8 +463,23 @@ def start(self, tiint = 100):
         self.n_Frames = 0
 
         #start our timer, this will call Notify
-        self.timer.start(self.tiint)
+        self.timer.start(self.tiint, single_shot=True)
         return True
+    
+
+    @contextmanager
+    def spooling_stopped(self):
+        """ Context manager to ensure that spooling is stopped while performing a task,
+        returning to the original state when the task is complete."""
+
+        was_running = self.aqOn
+        if was_running:
+            self.stop()
+        try:
+            yield
+        finally:
+            if was_running:
+                self.start()
 
 
     def isRunning(self):
diff --git a/PYME/Acquire/htsms/rule_ui.py b/PYME/Acquire/htsms/rule_ui.py
index 509787af8..27635ee1b 100644
--- a/PYME/Acquire/htsms/rule_ui.py
+++ b/PYME/Acquire/htsms/rule_ui.py
@@ -95,7 +95,7 @@ def __init__(self, spool_controller, posting_thread_queue_size=5):
         self['default'] = RuleChain()
 
         self._spool_controller.onSpoolStart.connect(self.on_spool_start)
-        self._spool_controller.onSpoolStop.connect(self.on_spool_stop)
+        self._spool_controller.on_stop.connect(self.on_spool_stop)
     
     def on_spool_start(self, **kwargs):
         self.on_spool_event('spool start')
@@ -811,7 +811,7 @@ def plug(main_frame, scope, default_pairings=None):
         ----------
         main_frame : PYME.Acquire.acquiremainframe.PYMEMainFrame
             microscope gui application
-        scope : PYME.Acquire.microscope.microscope
+        scope : PYME.Acquire.microscope.Microscope
             the microscope itself
         default_pairings : dict
             [optional] protocol keys with lists of RuleFactorys as values to
@@ -931,7 +931,7 @@ def plug(main_frame, scope, default_pairings=None):
         ----------
         main_frame : PYME.Acquire.acquiremainframe.PYMEMainFrame
             microscope gui application
-        scope : PYME.Acquire.microscope.microscope
+        scope : PYME.Acquire.microscope.Microscope
             the microscope itself
         default_pairings : dict
             [optional] protocol keys with RuleChains as values to
diff --git a/PYME/Acquire/microscope.py b/PYME/Acquire/microscope.py
index f8798e15f..ca593f47d 100755
--- a/PYME/Acquire/microscope.py
+++ b/PYME/Acquire/microscope.py
@@ -312,7 +312,7 @@ def registerChangeListener(self, key, callback):
         
     
 
-class microscope(object):
+class Microscope(object):
     """
 
     Attributes
diff --git a/PYME/Acquire/positionTracker.py b/PYME/Acquire/positionTracker.py
index 171f0b1f8..738f8070d 100644
--- a/PYME/Acquire/positionTracker.py
+++ b/PYME/Acquire/positionTracker.py
@@ -107,7 +107,7 @@ def __init__(self, scope, time1, viewsize=25., nPixels=500):
         self.im = np.zeros([nPixels, nPixels])
         self.ps = viewsize/nPixels
 
-        time1.WantNotification.append(self.Tick)
+        time1.register_callback(self.Tick)
 
     def Tick(self):
         positions  = np.zeros(len(self.scope.piezos) + 2)
diff --git a/PYME/Acquire/protocol_acquisition.py b/PYME/Acquire/protocol_acquisition.py
new file mode 100644
index 000000000..699500b74
--- /dev/null
+++ b/PYME/Acquire/protocol_acquisition.py
@@ -0,0 +1,386 @@
+# protocol_acquisition.py 
+
+# replaces Spooler.py and the various backend-specific spoolers, factoring out everything which is specific to 
+# asynchronous protocol based acquisition.
+#
+# Protocol based asynchronous acquisition should be used where it is important that the camera runs as fast as possible.
+# To allow this, we do not wait for various hardware events to complete, but rather just record timestamps ("Events") and work things
+# out in post-processing.
+
+
+import datetime
+import time
+import os
+import uuid
+import sys
+
+from PYME.contrib import dispatch
+
+from PYME import config
+from PYME.IO import MetaDataHandler
+from PYME.IO import acquisition_backends
+from PYME.IO.events import HDFEventLogger, MemoryEventLogger
+
+from PYME.Acquire import eventLog
+from PYME.Acquire import protocol as p
+from PYME.Acquire.acquisition_base import AcquisitionBase
+
+
+try:
+    from PYME.Acquire import sampleInformation
+except:
+    sampleInformation= None
+
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+
+def getReducedFilename(filename):
+    #rname = filename[len(nameUtils.datadir):]
+        
+    sname = '/'.join(filename.split(os.path.sep))
+    if sname.startswith('/'):
+        sname = sname[1:]
+    
+    return sname
+
+
+class ProtocolAcquisition(AcquisitionBase):
+    """Spooler base class"""
+    def __init__(self, filename, frameSource, protocol = p.NullProtocol, 
+                 fakeCamCycleTime=None, maxFrames = p.maxint, backend='hdf', backend_kwargs={}, **kwargs):
+        """Create a new spooler.
+        
+        Parameters
+        ----------
+        scope : PYME.Acquire.microscope.Microscope object
+            The microscope providing the data
+        filename : string
+            The file into which to spool
+        frameSource : dispatch.Signal object
+            A source of frames we can subscribe to. It should implement a "connect"
+            method allowing us to register a callback and then call the callback with
+            the frame data in a "frameData" kwarg.
+        protocol : PYME.Acquire.protocol.TaskListProtocol object
+            The acquisition protocol
+        guiUpdateCallback : function
+            a function to call when the spooling GUI needs updating
+            
+        """
+        AcquisitionBase.__init__(self)
+
+        self.filename=filename
+        self.frameSource = frameSource
+        self.seriesName = getReducedFilename(filename)
+        
+        self.protocol = protocol
+        
+        self.maxFrames = maxFrames
+        
+        stack_settings = kwargs.get('stack_settings', None)
+        if stack_settings:
+            # only record stack settings if provided (letting protocol fall through to global stack settings,
+            # if not provided / None)
+            self.stack_settings = stack_settings
+
+        
+        self.spoolOn = False
+        self.frame_num = 0
+        
+        self.spool_complete = False
+        
+        self._spooler_uuid = uuid.uuid4()
+            
+        self._fakeCamCycleTime = fakeCamCycleTime
+
+        self._last_gui_update = 0
+
+        self._create_backend(backend_type=backend, **backend_kwargs)
+
+    @classmethod
+    def from_spool_settings(cls, scope, settings, backend, backend_kwargs={}, series_name=None, spool_controller=None):
+        '''Create an XYZTCAcquisition object from a spool_controller settings object'''
+        from PYME.IO import acquisition_backends
+
+        if isinstance(backend, acquisition_backends.MemoryBackend):
+            # TODO - make this softer and allow memory backend for fixed length protocol acquisitions???
+            raise RuntimeError('Memory spooling not supported for protocol-based acquisitions')
+        
+        protocol = spool_controller.protocol_settings.get_protocol_for_acquistion(settings=settings)
+        
+        preflight_mode = settings.get('preflight_mode', 'interactive')
+        if (preflight_mode != 'skip'):
+            from PYME.Acquire.ui import preflight
+            if not preflight.ShowPreflightResults(protocol.PreflightCheck(), preflight_mode):
+                logger.debug('Bailing from preflight check')
+                return None #bail if we failed the pre flight check, and the user didn't choose to continue
+        
+        #fix timing when using fake camera
+        if scope.cam.__class__.__name__ == 'FakeCamera':
+            fakeCycleTime = scope.cam.GetIntegTime()
+        else:
+            fakeCycleTime = None
+        
+        #logger.info('Creating spooler for %s' % series_name)
+        return cls(filename=series_name,
+                    frameSource=scope.frameWrangler.onFrame,
+                    protocol=protocol,
+                    fakeCamCycleTime=fakeCycleTime, 
+                    maxFrames=settings.get('max_frames', sys.maxsize),
+                    stack_settings=settings.get('stack_settings', None),
+                    backend=backend, backend_kwargs=backend_kwargs,) 
+                   
+
+
+    def _create_backend(self, backend_type=acquisition_backends.HDFBackend, **kwargs):
+        logger.debug('Creating backend of type %s' % backend_type)
+        
+        if backend_type in  ['cluster', 'Cluster', acquisition_backends.ClusterBackend]:
+            self._aggregate_h5 = kwargs.get('cluster_h5', False)
+            
+            self.clusterFilter = kwargs.get('serverfilter', config.get('dataserver-filter', ''))
+            
+            chunk_size = config.get('httpspooler-chunksize', 50)
+            
+            def dist_fcn(n_servers, i=None):
+                if i is None:
+                    # distribute at random
+                    import random
+                    return random.randrange(n_servers)
+                
+                return int(i/chunk_size) % n_servers
+            
+            
+            self._backend = acquisition_backends.ClusterBackend(self.seriesName, 
+                                                                distribution_fcn=dist_fcn, 
+                                                                compression_settings=kwargs.get('compression_settings', {}),
+                                                                cluster_h5=self._aggregate_h5,
+                                                                serverfilter=self.clusterFilter,
+                                                                shape=[-1,-1,1,-1,1], #spooled aquisitions are time series (for now)
+                                                                evt_time_fcn=self._time_fcn)
+            
+        else: # assume hdf
+            self._backend = acquisition_backends.HDFBackend(self.filename, complevel=kwargs.get('complevel', 6), complib=kwargs.get('complib','zlib'),
+                            shape=[-1,-1,1,-1,1], # spooled series are time-series (for now)
+                            evt_time_fcn=self._time_fcn)
+        
+        
+        self._stopping = False
+
+    @property
+    def md(self):
+        return self._backend.mdh
+
+    def StartSpool(self):
+        from PYME import warnings
+        warnings.warn('StartSpool is deprecated. Use start instead', DeprecationWarning)
+        self.start()
+
+    def StopSpool(self):
+        from PYME import warnings
+        warnings.warn('StopSpool is deprecated. Use stop instead', DeprecationWarning)
+        self.stop()
+    
+    def start(self):
+        """ Perform protocol 'frame -1' tasks, log start metadata, then connect
+        to the frame source.
+        """
+        self.watchingFrames = True
+        eventLog.register_event_handler(self._backend.event_logger)
+
+        self.frame_num = 0
+        
+        # set tStart here for simulator so that events in init phase get time stamps. Real start time is set below
+        # **after** protocol.Init() call
+        self.tStart = time.time()
+
+        self.protocol.Init(self)
+        
+        # record start time when we start receiving frames.
+        self.tStart = time.time()
+        self._collect_start_metadata()
+        self.frameSource.connect(self.on_frame, dispatch_uid=self._spooler_uuid)
+        
+        self.spoolOn = True
+
+        logger.debug('Starting spooling: %s' %self.seriesName)
+
+        self._backend.initialise()
+       
+    def stop(self):
+        #try:
+        logger.debug('Disconnecting from frame source')
+        self.frameSource.disconnect(self.on_frame, dispatch_uid=self._spooler_uuid)
+        logger.debug('Frame source should be disconnected')
+        
+        #there is a race condition on disconnect - ignore any additional frames
+        self.watchingFrames = False 
+        
+        #except:
+        #    pass
+
+        try:
+            self.protocol.OnFinish()#this may still cause events
+            self.FlushBuffer()
+            self._collect_stop_metadata()
+        except:
+            import traceback
+            traceback.print_exc()
+            
+        try:
+            eventLog.remove_event_handler(self._backend.event_logger)
+        except ValueError:
+            pass
+        
+        self.spoolOn = False
+
+        self.on_progress.send(self)
+        
+        self._stopping=True
+        self.finalise()
+        self.on_stop.send(self)
+        self.spool_complete = True
+        
+    def finalise(self):
+        """
+        Over-ride in derived classes to do any spooler specific tidy up - e.g. sending events to server
+
+        """
+        self._backend.finalise()
+
+        
+    def abort(self):
+        """
+        Tidy up if something goes horribly wrong. Disconnects frame source and event logger  and then calls cleanup()
+
+        """
+        #there is a race condition on disconnect - ignore any additional frames
+        self.watchingFrames = False
+        
+        try:
+            logger.debug('Disconnecting from frame source')
+            self.frameSource.disconnect(self.OnFrame, dispatch_uid=self._spooler_uuid)
+            logger.debug('Frame source should be disconnected')
+        except:
+            logger.exception('Error disconnecting frame source')
+
+
+        try:
+            eventLog.remove_event_handler(self._backend.event_logger)
+        except ValueError:
+            pass
+
+        self.spoolOn = False
+        self.on_stop.send(self)
+        
+
+    def on_frame(self, sender, frameData, **kwargs):
+        """Callback which should be called on every frame"""
+        if not self.watchingFrames:
+            #we have allready disconnected - ignore any new frames
+            return
+        
+        self._backend.store_frame(self.frame_num, frameData)
+
+        t = time.time()
+            
+        self.frame_num += 1
+        
+        if (t > (self._last_gui_update +.1)):
+            self._last_gui_update = t
+            self.on_progress.send(self)
+            
+        try:
+            import wx #FIXME - shouldn't do this here
+            wx.CallAfter(self.protocol.OnFrame, self.frame_num)
+            #FIXME - The GUI logic shouldn't be here (really needs to change at the level of the protocol and/or general structure of PYMEAcquire
+        except (ImportError, AssertionError):  # handle if spooler doesn't have a GUI
+            self.protocol.OnFrame(self.frame_num) #FIXME - This will most likely fail for anything but a NullProtocol
+
+        if self.frame_num == 2 and sampleInformation and sampleInformation.currentSlide[0]: #have first frame and should thus have an imageID
+            sampleInformation.createImage(self.md, sampleInformation.currentSlide[0])
+            
+        if self.frame_num >= self.maxFrames:
+            self.stop()
+            
+
+    def _collect_start_metadata(self):
+        """Record pertinant information to metadata at start of acquisition.
+        
+        Loops through all registered sources of start metadata and adds their entries.
+        
+        See Also
+        --------
+        PYME.IO.MetaDataHandler
+        """
+        dt = datetime.datetime.now()
+        
+        self.dtStart = dt
+        
+        #self.tStart = time.time()
+        
+        # create an in-memory metadata handler and populate this prior to copying data over to the spooler
+        # metadata handler. This significantly improves performance if the spooler metadata handler has high latency
+        # (as is the case for both the HDFMetaDataHandler and, especially, the QueueMetaDataHandler).
+        mdt = MetaDataHandler.NestedClassMDHandler()
+        mdt.setEntry('StartTime', self.tStart)
+
+        #loop over all providers of metadata
+        for mdgen in MetaDataHandler.provideStartMetadata:
+            mdgen(mdt)
+            
+        self.md.copyEntriesFrom(mdt)
+       
+
+    def _collect_stop_metadata(self):
+        """Record information to metadata at end of acquisition"""
+        self.md.setEntry('EndTime', time.time())
+        
+        #loop over all providers of metadata
+        for mdgen in MetaDataHandler.provideStopMetadata:
+           mdgen(self.md)
+
+    def _fake_time(self):
+        """Generate a fake timestamp for use with the simulator where the camera
+        cycle time does not match the actual time elapsed to generate the frame"""
+        #return self.tStart + self.frame_num*self.scope.cam.GetIntegTime()
+        return self.tStart + self.frame_num*self._fakeCamCycleTime
+    
+    @property
+    def _time_fcn(self):
+        if self._fakeCamCycleTime:
+            return self._fake_time
+        else:
+            return time.time
+
+    def FlushBuffer(self):
+        pass
+    
+    def status(self):
+        return {'spooling' : self.spoolOn,
+                'frames_spooled' : self.frame_num,
+                'spool_complete' : self.spool_complete,}
+    
+    def cleanup(self):
+        """ over-ride to do any cleanup"""
+        del self._backend
+    
+    def finished(self):
+        """ over-ride in derived classes to indicate when buffers flushed"""
+        
+        # FIXME - this probably needs a bit more work.
+        # FIXME - delegate to backend?
+        return self._stopping
+    
+    def get_n_frames(self):
+        return self.frame_num
+        
+    def __del__(self):
+        if self.spoolOn:
+            self.StopSpool()
+
+
+
+
diff --git a/PYME/Acquire/stage_leveling.py b/PYME/Acquire/stage_leveling.py
index 1b60f452a..732f6f042 100644
--- a/PYME/Acquire/stage_leveling.py
+++ b/PYME/Acquire/stage_leveling.py
@@ -20,7 +20,7 @@ def __init__(self, scope, offset_piezo, pause_on_relocate=0.25,
 
         Parameters
         ----------
-        scope: PYME.Acquire.microscope.microscope
+        scope: PYME.Acquire.microscope.Microscope
         offset_piezo: PYME.Acquire.Hardware.offsetPiezoREST.OffsetPiezo
         pause_on_relocate: float
             [optional] time to pause during measure loop after moving to a new location and before measuring offset.
@@ -28,7 +28,7 @@ def __init__(self, scope, offset_piezo, pause_on_relocate=0.25,
 
         Notes
         -----
-        Units are derived from PYME.Acquire.microscope.microscope.GetPos and SetPos and should be in micrometers.
+        Units are derived from PYME.Acquire.microscope.Microscope.GetPos and SetPos and should be in micrometers.
 
         Attributes
         ----------
diff --git a/PYME/Acquire/ui/HDFSpoolFrame.py b/PYME/Acquire/ui/HDFSpoolFrame.py
index 351029c5a..aaa973e34 100755
--- a/PYME/Acquire/ui/HDFSpoolFrame.py
+++ b/PYME/Acquire/ui/HDFSpoolFrame.py
@@ -343,7 +343,7 @@ def __init__(self, parent, scope, **kwargs):
         
         self.spoolController.onSpoolProgress.connect(self._tick)
         self.spoolController.onSpoolStart.connect(self.OnSpoolingStarted)
-        self.spoolController.onSpoolStop.connect(self.OnSpoolingStopped)
+        self.spoolController.on_stop.connect(self.OnSpoolingStopped)
 
         self.stSpoolDirName.SetLabel(self.spoolController.display_dirname)
         self.tcSpoolFile.SetValue(self.spoolController.seriesName)
diff --git a/PYME/Acquire/ui/intensity_trace.py b/PYME/Acquire/ui/intensity_trace.py
index 27abffdb9..191b110fc 100644
--- a/PYME/Acquire/ui/intensity_trace.py
+++ b/PYME/Acquire/ui/intensity_trace.py
@@ -137,7 +137,7 @@ def OnClear(self, wx_event=None):
 #     scope.frameWrangler.onFrame.connect(intensity_trace.update_trace) # to update data on every frame
 #     #scope.frameWrangler.onFrameGroup.connect(intensity_trace.update_trace) # to update data at ~5Hz, or every frame, whichever is slower
 
-#     MainFrame.time1.WantNotification.append(intensity_trace.update_display) # update the display at 2Hz
+#     MainFrame.time1.register_callback(intensity_trace.update_display) # update the display at 2Hz
 
 #     panel = TraceROISelectPanel(MainFrame, intensity_trace)
 #     MainFrame.camPanels.append((panel, 'Trace ROI Select'))
diff --git a/PYME/Acquire/ui/mpd_picosecond_delay_panel.py b/PYME/Acquire/ui/mpd_picosecond_delay_panel.py
index 043f1d9e5..8dd8966c3 100644
--- a/PYME/Acquire/ui/mpd_picosecond_delay_panel.py
+++ b/PYME/Acquire/ui/mpd_picosecond_delay_panel.py
@@ -11,7 +11,7 @@ def pulse_phase_controls(MainFrame, scope):
         from PYME.Acquire.ui import mpd_picosecond_delay_panel
         delay_panel = mpd_picosecond_delay_panel.DelayPanel(MainFrame, scope.mpd, scope)
         MainFrame.camPanels.append((delay_panel, 'Phase Delay', False, False))
-        MainFrame.time1.WantNotification.append(delay_panel.update)
+        MainFrame.time1.register_callback(delay_panel.update)
     """
     def __init__(self, parent, delayer, scope):
         self.delayer = delayer
diff --git a/PYME/Acquire/ui/seqdialog.py b/PYME/Acquire/ui/seqdialog.py
index b3049fb9c..3fb030671 100755
--- a/PYME/Acquire/ui/seqdialog.py
+++ b/PYME/Acquire/ui/seqdialog.py
@@ -28,6 +28,7 @@
 
 #from PYME.Acquire import simplesequenceaquisator
 from PYME.Acquire import stackSettings
+from PYME.ui import cascading_layout
 #from PYME.IO import MetaDataHandler
 
 #redefine wxFrame with a version that hides when someone tries to close it
@@ -73,7 +74,7 @@ def create(parent):
 
 
 
-class seqPanel(wx.Panel):
+class seqPanel(cascading_layout.CascadingLayoutPanel):
     def _init_ctrls(self, prnt):
         # generated method, don't edit
 
@@ -173,25 +174,26 @@ def _init_ctrls(self, prnt):
         #hsizer.Add(sNSlices, 1, 0, 0)
         vsizer.Add(hsizer, 0, wx.EXPAND|wx.BOTTOM, 5)
 
-        if not (self.mode == 'sequence'):
+        if not (self.mode in ['sequence']):
             hsizer = wx.BoxSizer(wx.HORIZONTAL)
             self.stMemory = wx.StaticText(self, -1, '')
             hsizer.Add(self.stMemory, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
     
-            self.bStart = wx.Button(self, -1, 'Single Stack', style=wx.BU_EXACTFIT)
-            self.bStart.Bind(wx.EVT_BUTTON, self.OnBSingle)
-            hsizer.Add(self.bStart, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5, 0)
-            
-            self.bLive = wx.Button(self, -1, 'Live', style=wx.BU_EXACTFIT)
-            self.bLive.Bind(wx.EVT_BUTTON, self.OnBLive)
-            hsizer.Add(self.bLive, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5, 0)
+            if not (self.mode in ['compact']):
+                self.bStart = wx.Button(self, -1, 'Single Stack', style=wx.BU_EXACTFIT)
+                self.bStart.Bind(wx.EVT_BUTTON, self.OnBSingle)
+                hsizer.Add(self.bStart, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5, 0)
+                
+                self.bLive = wx.Button(self, -1, 'Live', style=wx.BU_EXACTFIT)
+                self.bLive.Bind(wx.EVT_BUTTON, self.OnBLive)
+                hsizer.Add(self.bLive, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5, 0)
     
             vsizer.Add(hsizer, 0, wx.EXPAND, 0)
-        else:
+        elif self.mode == 'sequence':
             # sequence mode doesn't have dwell functionality yet
             hsizer = wx.BoxSizer(wx.HORIZONTAL)
             hsizer.Add(wx.StaticText(self, -1, 'Dwell [frames]:'), 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 0)
-            self.t_zdwell = wx.TextCtrl(self, -1, value=str(self.scope.spoolController.z_dwell), size=(75,-1))
+            self.t_zdwell = wx.TextCtrl(self, -1, value=str(self.scope.spoolController.protocol_settings.z_dwell), size=(75,-1))
             self.t_zdwell.Bind(wx.EVT_KILL_FOCUS, self.OnDwellKillFocus)
             hsizer.Add(self.t_zdwell, 1, wx.ALIGN_CENTER_VERTICAL, 0)
             vsizer.Add(hsizer, 0, wx.EXPAND|wx.BOTTOM, 5)
@@ -416,7 +418,7 @@ def OnTStepSizeKillFocus(self, event):
         event.Skip()
     
     def OnDwellKillFocus(self, wx_event):
-        self.scope.spoolController.z_dwell = int(self.t_zdwell.GetValue())
+        self.scope.spoolController.protocol_settings.z_dwell = int(self.t_zdwell.GetValue())
         wx_event.Skip()
         
 
@@ -456,7 +458,7 @@ def UpdateDisp(self):
         if not self.mode == 'sequence':
             self.stMemory.SetLabel('Mem: %2.1f MB' % (self.scope.cam.GetPicWidth()*self.scope.cam.GetPicHeight()*self.stackSettings.GetSeqLength()*2*1/(1024.0*1024.0)))
         else:
-            self.t_zdwell.SetValue(str(self.scope.spoolController.z_dwell))
+            self.t_zdwell.SetValue(str(self.scope.spoolController.protocol_settings.z_dwell))
 
 
 class SeqProgressPanel(wx.Panel):
diff --git a/PYME/Acquire/ui/spool_panel.py b/PYME/Acquire/ui/spool_panel.py
new file mode 100644
index 000000000..e416d99de
--- /dev/null
+++ b/PYME/Acquire/ui/spool_panel.py
@@ -0,0 +1,697 @@
+#!/usr/bin/python
+
+##################
+# HDFSpoolFrame.py
+#
+# Copyright David Baddeley, 2009
+# d.baddeley@auckland.ac.nz
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##################
+"""The GUI controls for streaming acquisiton.
+
+"""
+
+import wx
+import datetime
+
+import PYME.Acquire.Protocols
+from PYME.Acquire.SpoolController import SpoolController
+#import PYME.Acquire.protocol_acquisition
+
+import os
+import glob
+
+from PYME.IO import PZFFormat
+import sys
+
+from PYME import config
+
+[wxID_FRSPOOL, wxID_FRSPOOLBSETSPOOLDIR, wxID_FRSPOOLBSTARTSPOOL, 
+ wxID_FRSPOOLBSTOPSPOOLING, wxID_FRSPOOLCBCOMPRESS, wxID_FRSPOOLCBQUEUE, 
+ wxID_FRSPOOLPANEL1, wxID_FRSPOOLSTATICBOX1, wxID_FRSPOOLSTATICBOX2, 
+ wxID_FRSPOOLSTATICTEXT1, wxID_FRSPOOLSTNIMAGES, wxID_FRSPOOLSTSPOOLDIRNAME, 
+ wxID_FRSPOOLSTSPOOLINGTO, wxID_FRSPOOLTCSPOOLFILE, 
+] = [wx.NewIdRef() for _init_ctrls in range(14)]
+    
+
+import  PYME.ui.manualFoldPanel as afp
+from PYME.ui import cascading_layout
+from . import seqdialog
+from . import AnalysisSettingsUI
+
+import logging
+logger = logging.getLogger(__name__)
+
+class ProtocolAcquisitionPane(cascading_layout.CascadingLayoutPanel):
+    def _protocol_pan(self):
+        pan = wx.Panel(parent=self, style=wx.TAB_TRAVERSAL)
+    
+        vsizer = wx.BoxSizer(wx.VERTICAL)
+    
+        ### Aquisition Protocol
+        sbAP = wx.StaticBox(pan, -1, 'Aquisition Protocol')
+        APSizer = wx.StaticBoxSizer(sbAP, wx.VERTICAL)
+    
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+    
+        self.stAqProtocol = wx.StaticText(pan, -1, '<None>', size=wx.Size(136, -1))
+        hsizer.Add(self.stAqProtocol, 5, wx.ALL | wx.EXPAND, 2)
+    
+        self.bSetAP = wx.Button(pan, -1, 'Set', style=wx.BU_EXACTFIT)
+        self.bSetAP.Bind(wx.EVT_BUTTON, self.OnBSetAqProtocolButton)
+    
+        hsizer.Add(self.bSetAP, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
+    
+        APSizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 0)
+    
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.rbNoSteps = wx.RadioButton(pan, -1, 'Standard', style=wx.RB_GROUP)
+        self.rbNoSteps.Bind(wx.EVT_RADIOBUTTON, self.OnToggleZStepping)
+        hsizer.Add(self.rbNoSteps, 1, wx.ALL | wx.EXPAND, 2)
+        self.rbZStepped = wx.RadioButton(pan, -1, 'Z stepped')
+        self.rbZStepped.Bind(wx.EVT_RADIOBUTTON, self.OnToggleZStepping)
+        hsizer.Add(self.rbZStepped, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
+        
+        if not hasattr(self.scope, 'stackSettings'):
+            self.rbZStepped.Disable()
+    
+        if self.protocol_settings.z_stepped:
+            self.rbZStepped.SetValue(True)
+        else:
+            self.rbNoSteps.SetValue(True)
+    
+        APSizer.Add(hsizer, 0, wx.TOP | wx.EXPAND, 4)
+             
+    
+        vsizer.Add(APSizer, 0, wx.TOP | wx.EXPAND, 4)
+    
+        pan.SetSizerAndFit(vsizer)
+        return pan
+    
+    def _init_ctrls(self):
+        vsizer = wx.BoxSizer(wx.VERTICAL)
+        vsizer.Add(self._protocol_pan(), 0, wx.ALL | wx.EXPAND, 0)
+
+        if hasattr(self.scope, 'stackSettings'):
+            clp = afp.collapsingPane(self, caption='Z stepping ...')
+            self._seq_panel = seqdialog.seqPanel(clp, self.scope, mode='sequence')
+            clp.AddNewElement(self._seq_panel, priority=1)
+            vsizer.Add(clp, 0, wx.ALL | wx.EXPAND, 0)
+            #self.AddNewElement(clp, priority=1)
+            self.seq_pan = clp
+
+        #analysis settings
+        clp = afp.collapsingPane(self, caption='Real time analysis ...')
+
+        self.scope.analysisSettings = AnalysisSettingsUI.AnalysisSettings() #Move me???
+
+        clp.AddNewElement(AnalysisSettingsUI.AnalysisSettingsPanel(clp, self.scope.analysisSettings,
+                                                                   self.scope.analysisSettings.onMetadataChanged))
+        clp.AddNewElement(AnalysisSettingsUI.AnalysisDetailsPanel(clp, self.scope.analysisSettings,
+                                                                  self.scope.analysisSettings.onMetadataChanged))
+        #self.AddNewElement(clp)
+        vsizer.Add(clp, 0, wx.ALL | wx.EXPAND, 0)
+        self.SetSizerAndFit(vsizer)
+        #end analysis settings
+        
+
+    def __init__(self, parent, scope, **kwargs):
+        """Initialise the spooling panel.
+        
+        Parameters
+        ----------
+        parent : wx.Window derived class
+            The parent window
+        scope : microscope instance
+            The currently active microscope class (see microscope.py)
+        defDir : string pattern
+            The default directory to save data to. Any keys of the form `%(<key>)` 
+            will be substituted using the values defined in `PYME.fileUtils.nameUtils.dateDict` 
+        defSeries : string pattern
+            This specifies a pattern for file naming. Keys will be substituted as for `defDir`
+            
+        """
+        #afp.foldingPane.__init__(self, parent, caption='Protocol Acquisition', **kwargs)
+        cascading_layout.CascadingLayoutPanel.__init__(self, parent, **kwargs)
+        self.scope = scope
+        self.spoolController = scope.spoolController
+        
+        self._init_ctrls()
+
+    @property
+    def protocol_settings(self) -> PYME.Acquire.SpoolController.ProtocolAcquisitionSettings:
+        return self.scope.spoolController.protocol_settings
+
+    def OnToggleZStepping(self, event):
+        self.protocol_settings.z_stepped = self.rbZStepped.GetValue()
+        pan = event.GetEventObject().GetParent()
+        if self.rbZStepped.GetValue():
+            if self.seq_pan.folded:
+                self.seq_pan.OnFold()
+            
+        else:
+            if not self.seq_pan.folded:
+                self.seq_pan.OnFold()
+
+    def OnBSetAqProtocolButton(self, event):
+        """Set the current protocol (GUI callback).
+        
+        See also: PYME.Acquire.Protocols."""
+        from PYME.Acquire import protocol
+        pDlg = wx.SingleChoiceDialog(self, '', 'Select Protocol', protocol.get_protocol_list())
+
+        ret = pDlg.ShowModal()
+        #print 'Protocol choice: ', ret, wx.ID_OK
+        if ret == wx.ID_OK:
+            pname = pDlg.GetStringSelection()
+            self.spoolController.protocol_settings.SetProtocol(pname)
+            # do this after setProtocol so that an error in SetProtocol avoids setting the new name
+            self.stAqProtocol.SetLabel(pname)
+            self._seq_panel.UpdateDisp()  # update display of e.g. z_dwell
+
+        pDlg.Destroy()
+
+
+
+
+class SpoolingPane(afp.foldingPane):
+    """A Panel containing the GUI controls for spooling"""
+    
+    def _spool_to_pan(self):
+        pan = cascading_layout.CascadingLayoutPanel(parent=self, style=wx.TAB_TRAVERSAL)
+        vsizer = wx.BoxSizer(wx.VERTICAL)
+    
+        ###Spool directory
+        sbSpoolDir = wx.StaticBox(pan, -1, 'Spool to:')
+        spoolDirSizer = wx.StaticBoxSizer(sbSpoolDir, wx.VERTICAL)
+    
+        #queues etcc
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        self._spool_method_rbs = []
+        for i, sm in enumerate(self.spoolController.available_spool_methods):
+            rb = wx.RadioButton(pan, -1, sm, style=(wx.RB_GROUP if i == 0 else 0))
+            rb.Bind(wx.EVT_RADIOBUTTON, self.OnSpoolMethodChanged)
+            hsizer.Add(rb, 1, wx.ALL | wx.EXPAND, 2)
+            self._spool_method_rbs.append(rb)
+
+        self._spool_method_rbs[self.spoolController.available_spool_methods.index(self.spoolController.spoolType)].SetValue(True)
+    
+        spoolDirSizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0)
+    
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.stSpoolDirName = wx.StaticText(pan, -1, 'Save images in: Blah Blah', size=wx.Size(136, -1))
+        hsizer.Add(self.stSpoolDirName, 5, wx.ALL | wx.EXPAND, 5)
+    
+        self.bSetSpoolDir = wx.Button(pan, -1, 'Set', style=wx.BU_EXACTFIT)
+        self.bSetSpoolDir.Bind(wx.EVT_BUTTON, self.OnBSetSpoolDirButton)
+    
+        hsizer.Add(self.bSetSpoolDir, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        spoolDirSizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 0)
+
+        ### Series Name & start button
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+    
+        #hsizer.Add(wx.StaticText(self, -1, 'Series: '), 0,wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        self.tcSpoolFile = wx.TextCtrl(pan, -1, 'dd_mm_series_a', size=wx.Size(100, -1))
+        self.tcSpoolFile.Bind(wx.EVT_TEXT, self.OnTcSpoolFileText)
+    
+        hsizer.Add(self.tcSpoolFile, 5, wx.ALL | wx.EXPAND, 5)
+    
+       
+    
+        # self.bStartStack = wx.Button(pan,-1,'Z-Series',style=wx.BU_EXACTFIT)
+        # self.bStartStack.Bind(wx.EVT_BUTTON, self.OnBStartStackButton)
+        # hsizer.Add(self.bStartStack, 0,wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2)
+    
+        spoolDirSizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0)
+    
+        self.stDiskSpace = wx.StaticText(pan, -1, 'Free space:')
+        spoolDirSizer.Add(self.stDiskSpace, 0, wx.ALL | wx.EXPAND, 2)
+
+        ### Compression etc
+        clp = afp.collapsingPane(pan, caption='Compression and quantization ...', padding=2)
+        clp.AddNewElement(self._comp_pan(clp))
+        spoolDirSizer.Add(clp, 0, wx.ALL | wx.EXPAND, 2)
+
+        self._comp_p = clp
+    
+        vsizer.Add(spoolDirSizer, 0, wx.ALL | wx.EXPAND, 0)
+    
+        pan.SetSizerAndFit(vsizer)
+        return pan
+    
+    def _comp_pan(self, clp):
+        pan = cascading_layout.CascadingLayoutPanel(clp, -1)
+    
+        vsizer = wx.BoxSizer(wx.VERTICAL)
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+    
+        self.cbCompress = wx.CheckBox(pan, -1, 'Compression')
+        self.cbCompress.SetValue(self.spoolController.hdf_compression_level > 0)
+    
+        hsizer.Add(self.cbCompress, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        self.cbQuantize = wx.CheckBox(pan, -1, 'Quantization')
+        self.cbQuantize.SetValue(config.get('spooler-quantize_by_default', False))
+    
+        hsizer.Add(self.cbQuantize, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0)
+    
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer.Add(wx.StaticText(pan, -1, 'Quantization offset:'), 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        self.tQuantizeOffset = wx.TextCtrl(pan, -1, 'auto')
+        hsizer.Add(self.tQuantizeOffset, 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0)
+    
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+        hsizer.Add(wx.StaticText(pan, -1, 'Quantization scale:'), 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        self.tQuantizeScale = wx.TextCtrl(pan, -1, '0.5')
+        self.tQuantizeScale.SetToolTip(wx.ToolTip(
+            'Quantization scale in units of sigma\n. The default of 0.5 will give a quantization interval that is half the std dev. of the expected Poisson noise in a pixel.'))
+        hsizer.Add(self.tQuantizeScale, 1, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0)
+    
+        # spool to h5 on cluster
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+    
+        self.cbClusterh5 = wx.CheckBox(pan, -1, 'Spool to h5 on cluster (cluster of 1)')
+        self.cbClusterh5.SetValue(self.spoolController.cluster_h5)
+        self.cbClusterh5.Bind(wx.EVT_CHECKBOX, lambda e: setattr(self.spoolController,'cluster_h5', self.cbClusterh5.GetValue()))
+        #self.cbClusterh5.SetValue(self._N_data_servers == 1) #set to true if we have a single node cluster
+    
+        hsizer.Add(self.cbClusterh5, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
+        vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0)
+        pan.SetSizerAndFit(vsizer)
+        
+        #setup callbacks to update compression settings on UI change
+        self.cbCompress.Bind(wx.EVT_CHECKBOX, lambda e: self.update_spooler_compression_settings())
+        self.cbQuantize.Bind(wx.EVT_CHECKBOX, lambda e: self.update_spooler_compression_settings())
+        self.tQuantizeOffset.Bind(wx.EVT_KILL_FOCUS, lambda e: self.update_spooler_compression_settings())
+        self.tQuantizeScale.Bind(wx.EVT_KILL_FOCUS, lambda e: self.update_spooler_compression_settings())
+        
+        return pan
+    
+        
+    def _spool_pan(self):
+        pan = wx.Panel(parent=self, style=wx.TAB_TRAVERSAL)
+        vsizer = wx.BoxSizer(wx.VERTICAL)
+    
+        ### Spooling Progress
+    
+        self.spoolProgPan = wx.Panel(pan, -1)
+        vsizer_sp = wx.BoxSizer(wx.VERTICAL)
+    
+        self.sbSpoolProgress = wx.StaticBox(self.spoolProgPan, -1, 'Spooling Progress')
+        self.sbSpoolProgress.Enable(False)
+    
+        spoolProgSizer = wx.StaticBoxSizer(self.sbSpoolProgress, wx.VERTICAL)
+    
+        self.stSpoolingTo = wx.StaticText(self.spoolProgPan, -1, 'Spooling to .....')
+        spoolProgSizer.Add(self.stSpoolingTo, 0, wx.ALL, 0)
+    
+        self.stNImages = wx.StaticText(self.spoolProgPan, -1, 'NNNNN images spooled in MM minutes')
+        self.stSpoolingTo.SetForegroundColour(wx.TheColourDatabase.Find('GREY'))
+        self.stNImages.SetForegroundColour(wx.TheColourDatabase.Find('GREY'))
+    
+        spoolProgSizer.Add(self.stNImages, 0, wx.ALL, 0)
+    
+        hsizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        self.bStartSpool = wx.Button(self.spoolProgPan, -1, 'Start', style=wx.BU_EXACTFIT)
+        self.bStartSpool.Bind(wx.EVT_BUTTON, self.OnBStartSpoolButton)
+        #self.bStartSpool.SetDefault()
+        hsizer.Add(self.bStartSpool, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
+    
+        self.bStopSpooling = wx.Button(self.spoolProgPan, -1, 'Stop', style=wx.BU_EXACTFIT)
+        self.bStopSpooling.Enable(False)
+        self.bStopSpooling.Bind(wx.EVT_BUTTON, self.OnBStopSpoolingButton)
+    
+        hsizer.Add(self.bStopSpooling, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        self.bAnalyse = wx.Button(self.spoolProgPan, -1, 'Analyse', style=wx.BU_EXACTFIT)
+        self.bAnalyse.Enable(False)
+        self.bAnalyse.Bind(wx.EVT_BUTTON, self.OnBAnalyse)
+    
+        hsizer.Add(self.bAnalyse, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
+    
+        spoolProgSizer.Add(hsizer, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 0)
+    
+        vsizer_sp.Add(spoolProgSizer, 0, wx.ALL | wx.EXPAND, 0)
+    
+        self.spoolProgPan.SetSizerAndFit(vsizer_sp)
+    
+        vsizer.Add(self.spoolProgPan, 0, wx.ALL | wx.EXPAND, 0)
+    
+        pan.SetSizerAndFit(vsizer)
+        return pan
+        
+    def _aq_settings_pan(self):
+        pan = cascading_layout.CascadingLayoutPanel(parent=self, style=wx.TAB_TRAVERSAL)
+        v1 = wx.BoxSizer(wx.VERTICAL)
+        vsizer = wx.StaticBoxSizer(wx.StaticBox(pan, label='Acquisition Type'), wx.VERTICAL)
+    
+        self._aq_type_btns = []
+
+        _settings_box_name = ''
+
+        for i, aq_type in enumerate(self.spoolController.acquisition_types):
+            pane, name = self.acquisition_uis[aq_type]
+            btn = wx.RadioButton(pan, -1, name, style=(wx.RB_GROUP if i == 0 else 0))
+            btn.aq_type = aq_type
+            btn.SetValue(aq_type == self.spoolController.acquisition_type)
+            if aq_type == self.spoolController.acquisition_type:
+                _settings_box_name = f'{name} Settings'
+            btn.Bind(wx.EVT_RADIOBUTTON, self.OnAqTypeChanged)
+            vsizer.Add(btn, 0, wx.ALL | wx.EXPAND, 2)
+            self._aq_type_btns.append(btn)
+
+    
+        v1.Add(vsizer, 0, wx.ALL | wx.EXPAND, 0)
+        self.sbAqSettings = wx.StaticBox(pan, -1, _settings_box_name)
+        vsizer = wx.StaticBoxSizer(self.sbAqSettings, wx.VERTICAL)
+
+        for i, aq_type in enumerate(self.spoolController.acquisition_types):
+            pane, name = self.acquisition_uis[aq_type]
+
+            pane.Reparent(pan)
+            vsizer.Add(pane, 1, wx.ALL | wx.EXPAND, 2)
+            pane.Show(aq_type == self.spoolController.acquisition_type)
+        
+        v1.Add(vsizer, 0, wx.ALL | wx.EXPAND, 0)
+        pan.SetSizerAndFit(v1)
+
+        self.aq_settings_pan = pan
+        return pan
+    
+    def OnAqTypeChanged(self, event):
+        #btn = event.GetEventObject()
+        #if btn.GetValue():
+        self.spoolController.acquisition_type = event.GetEventObject().aq_type
+        self.on_aq_type_changed()
+
+    def on_aq_type_changed(self):
+        for btn in self._aq_type_btns:
+            btn.SetValue(btn.aq_type == self.spoolController.acquisition_type)
+
+        for aq_type in self.spoolController.acquisition_types:
+            self.acquisition_uis[aq_type][0].Show(aq_type == self.spoolController.acquisition_type)
+            if (aq_type == self.spoolController.acquisition_type):
+                self.sbAqSettings.SetLabel(f'{self.acquisition_uis[aq_type][1]} Settings')
+
+        self.aq_settings_pan.cascading_layout()
+        #self.cascading_layout()
+    
+    def _init_ctrls(self):
+        
+        self.AddNewElement(self._aq_settings_pan(), priority=1, foldable=False)
+        self._pan_spool_to = self._spool_to_pan()
+        self.AddNewElement(self._pan_spool_to)
+
+        self.AddNewElement(self._spool_pan())
+
+        
+        
+
+    def __init__(self, parent, scope, acquisition_uis = {}, **kwargs):
+        """Initialise the spooling panel.
+        
+        Parameters
+        ----------
+        parent : wx.Window derived class
+            The parent window
+        scope : microscope instance
+            The currently active microscope class (see microscope.py)
+        defDir : string pattern
+            The default directory to save data to. Any keys of the form `%(<key>)` 
+            will be substituted using the values defined in `PYME.fileUtils.nameUtils.dateDict` 
+        defSeries : string pattern
+            This specifies a pattern for file naming. Keys will be substituted as for `defDir`
+            
+        """
+        afp.foldingPane.__init__(self, parent, caption='Spooling', **kwargs)
+        self.scope = scope
+        self.spoolController = scope.spoolController
+        self.acquisition_uis = acquisition_uis
+
+        #check to see if we have a cluster
+        #self._N_data_servers = len(hybrid_ns.getNS('_pyme-http').get_advertised_services())
+        
+        self._init_ctrls()
+        
+        #self.spoolController = SpoolController(scope, defDir, **kwargs)
+        
+        self.spoolController.onSpoolProgress.connect(self._tick)
+        self.spoolController.onSpoolStart.connect(self.OnSpoolingStarted)
+        self.spoolController.on_stop.connect(self.OnSpoolingStopped)
+
+        self.stSpoolDirName.SetLabel(self.spoolController.display_dirname)
+        self.tcSpoolFile.SetValue(self.spoolController.seriesName)
+        self.UpdateFreeSpace()
+
+        #update the spool method (specifically so that the default in the GUI and spool controller match)
+        #self.OnSpoolMethodChanged(None)
+        
+        #make default compression settings in spooler match the display.
+        self.update_spooler_compression_settings(False)
+        
+
+    def update_spooler_compression_settings(self, ui_on_error=True):
+        try:
+            self.spoolController.pzf_compression_settings = self.get_compression_settings(ui_on_error)
+            self.spoolController.hdf_compression_level = 2 if self.cbCompress.GetValue() else 0
+        except:
+            logger.warn('Compression settings invalid, disabling quantization')
+            if ui_on_error:
+                ans = wx.MessageBox(
+                    "Compression settings invalid, disabling quantization",
+                    'Error', wx.OK)
+            self.cbQuantize.SetValue(False)
+            self.spoolController.pzf_compression_settings = self.get_compression_settings()
+    
+    def _get_spool_method(self):
+        for i, rb in enumerate(self._spool_method_rbs):
+            if rb.GetValue():
+                return self.spoolController.available_spool_methods[i]
+        
+        return None
+
+    def UpdateFreeSpace(self, event=None):
+        """Updates the free space display.
+        
+        Designed to be used as a callback with one of the system timers, but 
+        can be called separately
+        """
+        freeGB = self.spoolController.get_free_space()
+        self.stDiskSpace.SetLabel('Free Space: %3.2f GB' % freeGB)
+        if freeGB < 5:
+            self.stDiskSpace.SetForegroundColour(wx.Colour(200, 0,0))
+        else:
+            self.stDiskSpace.SetForegroundColour(wx.BLACK)
+       
+    
+                
+    def get_compression_settings(self, ui_message_on_error=True):
+        if not self.cbQuantize.GetValue():
+            compSettings = {
+                'compression': PZFFormat.DATA_COMP_HUFFCODE if self.cbCompress.GetValue() else PZFFormat.DATA_COMP_RAW,
+                'quantization': PZFFormat.DATA_QUANT_NONE,
+                'quantizationOffset': 0.0,
+                'quantizationScale': 1.0
+            }
+    
+            return compSettings
+        
+        else:
+            #try and set our quantization offset automatically as the AD offset of the camera
+            q_offset = self.tQuantizeOffset.GetValue()
+            if q_offset == 'auto':
+                #FIXME - add getter to camera???
+                try:
+                    q_offset = self.scope.cam.noise_properties['ADOffset']
+                except AttributeError:
+                    if ui_message_on_error:
+                        ans = wx.MessageBox(
+                                "Camera doesn't define noise properties, manually set the desired quantization offset",
+                                'Error', wx.OK)
+                    raise
+            else:
+                q_offset = float(q_offset)
+        
+            #quantization scale in GUI is in units of sigma, convert to ADU
+            try:
+                q_scale = float(self.tQuantizeScale.GetValue()) / self.scope.cam.noise_properties['ElectronsPerCount']
+            except (AttributeError, NotImplementedError):
+                logger.warning("Camera doesn't provide electrons per count, using qscale in units of ADUs instead")
+                q_scale = float(self.tQuantizeScale.GetValue())
+        
+            compSettings = {
+                'compression': PZFFormat.DATA_COMP_HUFFCODE if self.cbCompress.GetValue() else PZFFormat.DATA_COMP_RAW,
+                'quantization': PZFFormat.DATA_QUANT_SQRT,
+                'quantizationOffset': q_offset,
+                'quantizationScale': q_scale
+            }
+            
+            return compSettings
+            
+
+    def OnBStartSpoolButton(self, event=None, stack=False):
+        """GUI callback to start spooling.
+        
+        NB: this is also called programatically by the start stack button."""
+        
+        #if self.rbZStepped.GetValue():
+        #    stack = True
+        
+        fn = self.tcSpoolFile.GetValue()
+
+        if fn == '': #sanity checking
+            wx.MessageBox('Please enter a series name', 'No series name given', wx.OK)
+            return #bail
+            
+        # if self.cbCompress.GetValue():
+        #     compLevel = 2
+        # else:
+        #     compLevel = 0
+        
+
+        try:
+            self.spoolController.start_spooling(fn)
+        except IOError as e:
+            logger.exception('IO error whilst spooling')
+            ans = wx.MessageBox(str(e.strerror), 'Error', wx.OK)
+            self.tcSpoolFile.SetValue(self.spoolController.seriesName)
+            
+    def update_ui(self):
+        self.cbCompress.SetValue(self.spoolController.hdf_compression_level > 0)
+            
+            
+            
+    def OnSpoolingStarted(self, **kwargs):
+        if self.spoolController.spoolType in ['Queue', 'Cluster']:
+            self.bAnalyse.Enable()
+
+        self.bStartSpool.Enable(False)
+        #self.bStartStack.Enable(False)
+        self.bStopSpooling.Enable(True)
+        #self.stSpoolingTo.Enable(True)
+        #self.stNImages.Enable(True)
+        self.stSpoolingTo.SetForegroundColour(None)
+        self.stNImages.SetForegroundColour(None)
+        self.stSpoolingTo.SetLabel('Spooling to ' + self.spoolController.seriesName)
+        self.stNImages.SetLabel('0 images spooled in 0 minutes')
+        
+        
+
+    def OnBStartStackButton(self, event=None):
+        """GUI callback to start spooling with z-stepping."""
+        self.OnBStartSpoolButton(stack=True)
+        
+
+    def OnBStopSpoolingButton(self, event):
+        """GUI callback to stop spooling."""
+        self.spoolController.StopSpooling()
+        #self.OnSpoolingStopped()
+        
+    def OnSpoolingStopped(self, **kwargs):
+        self.bStartSpool.Enable(True)
+        #self.bStartStack.Enable(True)
+        self.bStopSpooling.Enable(False)
+        #self.stSpoolingTo.Enable(False)
+        #self.stNImages.Enable(False)
+        self.stSpoolingTo.SetForegroundColour(wx.TheColourDatabase.Find('GREY'))
+        self.stNImages.SetForegroundColour(wx.TheColourDatabase.Find('GREY'))
+
+        self.stSpoolDirName.SetLabel(self.spoolController.display_dirname)
+        self.tcSpoolFile.SetValue(self.spoolController.seriesName)
+        self.UpdateFreeSpace()
+        
+        
+        wx.CallAfter(self.Refresh)
+        #self.Refresh()
+        #self.Update()
+        #self.cascading_layout()
+
+    def OnBAnalyse(self, event):
+        self.spoolController.LaunchAnalysis()
+        
+    
+    def _tick(self, **kwargs):
+        wx.CallAfter(self.Tick)
+
+    def Tick(self, **kwargs):
+        """Called with each new frame. Updates the number of frames spooled
+        and disk space remaining"""
+        dtn = datetime.datetime.now()
+        
+        dtt = dtn - self.spoolController.spooler.dtStart
+        
+        self.stNImages.SetLabel('%d images spooled in %d seconds' % (self.spoolController.spooler.frame_num, dtt.seconds))
+        self.UpdateFreeSpace()
+
+    def OnBSetSpoolDirButton(self, event):
+        """Set the directory we're spooling into (GUI callback)."""
+        ndir = wx.DirSelector()
+        if not ndir == '':
+            logger.debug('series name %s' % self.spoolController.seriesName)
+            self.spoolController.SetSpoolDir(ndir)
+            self.stSpoolDirName.SetLabel(self.spoolController.display_dirname)
+            self.tcSpoolFile.SetValue(self.spoolController.seriesName)
+            logger.debug('series name %s' % self.spoolController.seriesName)
+
+            self.UpdateFreeSpace()
+
+    def OnTcSpoolFileText(self, event):
+        fn = self.tcSpoolFile.GetValue()
+        if not fn == '':
+            self.spoolController.seriesName = fn
+        event.Skip()
+        
+    def OnSpoolMethodChanged(self, event):
+        self.spoolController.SetSpoolMethod(self._get_spool_method())
+        if self.spoolController.spoolType == 'Memory':
+            self.stSpoolDirName.Hide()
+            self.tcSpoolFile.Hide()
+            self.stDiskSpace.Hide()
+            self.bSetSpoolDir.Hide()
+        else:
+            self.stSpoolDirName.Show()
+            self.tcSpoolFile.Show()
+            self.stDiskSpace.Show()
+            self.bSetSpoolDir.Show()
+
+        if self.spoolController.spoolType in ['Cluster', 'File']:
+            self._comp_p.Show()
+        else:
+            self._comp_p.Hide()
+
+        self._pan_spool_to.cascading_layout()
+
+        self.stSpoolDirName.SetLabel(self.spoolController.display_dirname)
+        self.tcSpoolFile.SetValue(self.spoolController.seriesName)
+
+        self.UpdateFreeSpace()
+
+
+
+        
diff --git a/PYME/Acquire/ui/tile_panel.py b/PYME/Acquire/ui/tile_panel.py
index 5f28440af..2b9e6238e 100644
--- a/PYME/Acquire/ui/tile_panel.py
+++ b/PYME/Acquire/ui/tile_panel.py
@@ -107,7 +107,7 @@ def OnGo(self, event=None):
         self.bGo.Disable()
         
         self.scope.tiler.on_stop.connect(self._on_stop)
-        self.scope.tiler.progress.connect(self._update)
+        self.scope.tiler.on_progress.connect(self._update)
         self.scope.tiler.start()
         
         
@@ -122,7 +122,7 @@ def _on_stop(self, *args, **kwargs):
         self.bGo.Enable()
         
         self.scope.tiler.on_stop.disconnect(self._on_stop)
-        self.scope.tiler.progress.disconnect(self._update)
+        self.scope.tiler.on_progress.disconnect(self._update)
         
         # FIXME - previous delay was 1e3, which seems more reasonable. Do we need a config option (or heuristic) here ?
         # assume this change was due to the time it takes to build a pyramid after tiling ends. Might ultimately be fixed when we revisit live tiling. 
@@ -221,7 +221,7 @@ def OnGo(self, event=None):
         self.bGo.Disable()
 
         self.scope.tiler.on_stop.connect(self._on_stop)
-        self.scope.tiler.progress.connect(self._update)
+        self.scope.tiler.on_progress.connect(self._update)
         self.scope.tiler.start()
 
 
@@ -301,7 +301,7 @@ def OnGo(self, event=None):
         self.bGo.Disable()
 
         # self.scope.tiler.on_stop.connect(self._on_stop)
-        # self.scope.tiler.progress.connect(self._update)
+        # self.scope.tiler.on_progress.connect(self._update)
         self.scope.tiler.start()
 
     def OnStop(self, event=None):
diff --git a/PYME/Acquire/xyztc.py b/PYME/Acquire/xyztc.py
index c845a84fb..508fed654 100644
--- a/PYME/Acquire/xyztc.py
+++ b/PYME/Acquire/xyztc.py
@@ -1,9 +1,11 @@
 import numpy as np
 import time
+import datetime
 
 from PYME.contrib import dispatch
 from PYME.IO import MetaDataHandler
 from PYME.IO.acquisition_backends import MemoryBackend
+from PYME.Acquire.acquisition_base import AcquisitionBase
 
 
 class TimeSettings(object):
@@ -28,8 +30,8 @@ def __init__(self, num_timepoints=1, time_interval=None):
         self.num_timepoints = num_timepoints
         self.time_interval = time_interval
 
-class XYZTCAcquisition(object):
-    def __init__(self, scope, dim_order='XYCZT', stack_settings=None, time_settings=None, channel_settings=None, backend=MemoryBackend):
+class XYZTCAcquisition(AcquisitionBase):
+    def __init__(self, scope, dim_order='XYCZT', stack_settings=None, time_settings=None, channel_settings=None, backend=MemoryBackend, backend_kwargs={}):
         """
         Class to handle an XYZTC acquisition. This should serve as a base class for more specific acquisition classes, whilst also allowing 
         for simple 3D and time-series acquisitions.
@@ -37,7 +39,7 @@ def __init__(self, scope, dim_order='XYCZT', stack_settings=None, time_settings=
         Parameters
         ----------
 
-        scope : PYME.Acquire.microscope.microscope instance
+        scope : PYME.Acquire.microscope.Microscope instance
             The microscope instance to use for acquisition.
 
         dim_order : str
@@ -57,8 +59,9 @@ def __init__(self, scope, dim_order='XYCZT', stack_settings=None, time_settings=
             Used for storing the acquired data and metadata. If None, a MemoryBackend will be used.     
 
         """
-        if stack_settings is None:
-            stack_settings = scope.stackSettings
+        #if stack_settings is None:
+        #    stack_settings = scope.stackSettings
+        AcquisitionBase.__init__(self)
 
         assert(dim_order[:2] == 'XY') #first two dimensions must be XY (camera acquisition)
         # TODO more sanity checks on dim_order
@@ -67,27 +70,53 @@ def __init__(self, scope, dim_order='XYCZT', stack_settings=None, time_settings=
         self.scope = scope
         
         self.shape_x, self.shape_y = scope.frameWrangler.currentFrame.shape[:2]
-        self.shape_z = stack_settings.GetSeqLength()
+        
+        if stack_settings:
+            self.shape_z = stack_settings.GetSeqLength()
+        else:
+            self.shape_z = 1
+
         self.shape_t = getattr(time_settings, 'num_timepoints', 1)
         self.shape_c = getattr(channel_settings, 'num_channels', 1)
         
         # note shape_t can be negative if we want to run until explicitly stopped
         self.n_frames = self.shape_z*self.shape_c*self.shape_t
         self.frame_num = 0
+
+        self._running = False
         
-        self.storage = backend(self.shape_x, self.shape_y, self.n_frames, dim_order=dim_order, shape=self.shape)
+        self.storage = backend(size_x = self.shape_x, size_y=self.shape_y, n_frames=self.n_frames, dim_order=dim_order, shape=self.shape, **backend_kwargs)
         
         #do any precomputation
         self._init_z(stack_settings)
         self._init_t(time_settings)
         self._init_c(channel_settings)
+    
+    
+    @classmethod
+    def from_spool_settings(cls, scope, settings, backend, backend_kwargs={}, series_name=None, spool_controller=None):
+        '''Create an XYZTCAcquisition object from a spool_controller settings object'''
+
+        backend_kwargs['series_name'] = series_name
 
-        self.on_single_frame = dispatch.Signal()  #dispatched when a frame is ready
-        self.on_series_end = dispatch.Signal()  #dispatched when a sequence is complete
+        return cls(scope=scope, 
+                   #dim_order=settings.dim_order, 
+                   stack_settings=settings.get('stack_settings', None), 
+                   time_settings=settings.get('time_settings', None), 
+                   channel_settings=settings.get('channel_settings', None), 
+                   backend=backend, backend_kwargs=backend_kwargs)
+        
+    
     
     @property
     def shape(self):
         return self.shape_x, self.shape_y, self.shape_z, self.shape_t, self.shape_c
+    
+    @property
+    def md(self):
+        ''' for compatibility with spoolers'''
+        return self.storage.mdh
+    
         
     def _zct_indices(self, frame_no):
         if self.dim_order == 'XYCZT':
@@ -108,7 +137,7 @@ def on_frame(self, sender, frameData, **kwargs):
         
         if (self.frame_num >= self.n_frames) and (self.n_frames > 0):
             # if shape_t  == -1 (infinte loop), then self.n_frames is negative, don't stop.
-            self.finish()
+            self.stop()
             return
         
         z_idx, c_idx, t_idx = self._zct_indices(self.frame_num)
@@ -119,7 +148,7 @@ def on_frame(self, sender, frameData, **kwargs):
         #probably don't need to set anything along the t axis, but provide anyway
         self.set_t(t_idx)
         
-        self.on_single_frame.send(self)
+        self.on_progress.send(self)
         
     def _collect_metadata(self):
         self.storage.mdh['StartTime'] = time.time()
@@ -132,36 +161,57 @@ def _collect_metadata(self):
         
     def start(self):
         self.scope.stackSettings.SetPrevPos(self.scope.stackSettings._CurPos())
-        self.scope.frameWrangler.stop()
-        self.frame_num = 0
-        
-        z_idx, c_idx, t_idx = self._zct_indices(self.frame_num)
 
-        self.set_z(z_idx)
-        self.set_c(c_idx)
-        #probably don't need to set anything along the t axis, but provide anyway
-        self.set_t(t_idx)
-        
-        self._collect_metadata()
-        
-        self.scope.frameWrangler.onFrame.connect(self.on_frame)
-        self.scope.frameWrangler.start()
-        
+        with self.scope.frameWrangler.spooling_stopped():
+            # avoid stopping both here and in the SpoolController
+            #self.scope.frameWrangler.stop()
+            self.frame_num = 0
+            
+            z_idx, c_idx, t_idx = self._zct_indices(self.frame_num)
+
+            self.set_z(z_idx)
+            self.set_c(c_idx)
+            #probably don't need to set anything along the t axis, but provide anyway
+            self.set_t(t_idx)
+            
+            self._collect_metadata()
+            
+            self.scope.frameWrangler.onFrame.connect(self.on_frame)
+
+            self.storage.initialise()
+
+            self._running = True
+
+            self.dtStart = datetime.datetime.now() #for spooler compatibility - FIXME
+            #self.scope.frameWrangler.start()
+
         
-    def finish(self):
+    def stop(self):
         self.scope.frameWrangler.stop()
         self.scope.frameWrangler.onFrame.disconnect(self.on_frame)
         self.scope.stackSettings.piezoGoHome()
         self.scope.frameWrangler.start()
+
+        self.storage.finalise()
         
-        self.on_series_end.send(self)
+        self.on_stop.send(self)
+        self._running = False
+        self.spool_complete = True
+
+    def abort(self):
+        self.stop()
         
     def _init_z(self, stack_settings):
-        self._z_poss = np.arange(stack_settings.GetStartPos(),
+        if stack_settings:
+            self._z_poss = np.arange(stack_settings.GetStartPos(),
                                stack_settings.GetEndPos() + .95 * stack_settings.GetStepSize(),
                                stack_settings.GetStepSize() * stack_settings.GetDirection())
 
-        self._z_chan = stack_settings.GetScanChannel()
+            self._z_chan = stack_settings.GetScanChannel()
+        else:
+            self._z_chan = 'z'
+            self._z_poss = [self.scope.GetPos()[self._z_chan]]
+
         self._z_initial_pos = self.scope.GetPos()[self._z_chan]
         
     
@@ -179,9 +229,29 @@ def _init_t(self, time_settings):
 
     def set_t(self, t_idx):
         pass
+
+    def status(self):
+        return {'spooling' : self._running,
+                'frames_spooled' : self.frame_num,
+                'spool_complete' : self.spool_complete,}
         
         
             
             
         
-        
\ No newline at end of file
+class ZStackAcquisition(XYZTCAcquisition):
+    """
+    Class for a simple Z-Stack acquisition.
+    """
+    @classmethod
+    def from_spool_settings(cls, scope, settings, backend, backend_kwargs={}, series_name=None, spool_controller=None):
+         '''Create an XYZTCAcquisition object from a spool_controller settings object'''
+    
+         backend_kwargs['series_name'] = series_name
+    
+         return cls(scope=scope, 
+                    #dim_order=settings.dim_order, 
+                    stack_settings=settings.get('stack_settings', scope.stackSettings), 
+                    time_settings=settings.get('time_settings', None), 
+                    channel_settings=settings.get('channel_settings', None), 
+                    backend=backend, backend_kwargs=backend_kwargs)
\ No newline at end of file
diff --git a/PYME/IO/HDFSpooler.py b/PYME/IO/HDFSpooler.py
deleted file mode 100755
index 40ec73da7..000000000
--- a/PYME/IO/HDFSpooler.py
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/usr/bin/python
-
-##################
-# HDFSpooler.py
-#
-# Copyright David Baddeley, 2009
-# d.baddeley@auckland.ac.nz
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##################
-
-import datetime
-import tables
-from PYME.IO import MetaDataHandler
-from PYME.IO.events import HDFEventLogger
-
-import time
-
-import PYME.IO.Spooler as sp
-
-from PYME.IO.FileUtils import fileID
-
-class _Spooler(sp.Spooler):
-    """Responsible for the mechanics of spooling to a pytables/hdf file.
-    """
-    def __init__(self, filename, frameSource, frameShape, complevel=6, complib='zlib', **kwargs):
-        sp.Spooler.__init__(self, filename, frameSource, **kwargs)
-        self.h5File = tables.open_file(filename, 'w')
-           
-        filt = tables.Filters(complevel, complib, shuffle=True)
-        
-        self.imageData = self.h5File.create_earray(self.h5File.root, 'ImageData', tables.UInt16Atom(), (0,frameShape[0],frameShape[1]), filters=filt)
-        self.md = MetaDataHandler.HDFMDHandler(self.h5File)
-        self.evtLogger = HDFEventLogger(self, self.h5File, time_fcn=self._time_fcn)
-        
-
-    def finalise(self):
-        """ close files"""
-           
-        self.h5File.flush()
-        self.h5File.close()
-        
-    def OnFrame(self, sender, frameData, **kwargs):
-        """Called on each frame"""
-
-        if not self.watchingFrames:
-            # drop frames if we've already stopped spooling. TODO - do we also need to check if disconnect works?
-            return
-        
-        #print 'f'
-        if frameData.shape[0] == 1:
-            self.imageData.append(frameData)
-        else:
-            self.imageData.append(frameData.reshape(1,frameData.shape[0],frameData.shape[1]))
-        self.h5File.flush()
-        if self.imNum == 0: #first frame
-            self.md.setEntry('imageID', fileID.genFrameID(self.imageData[0,:,:]))
-            
-        sp.Spooler.OnFrame(self)
-        
-    def __del__(self):
-        if self.spoolOn:
-            self.StopSpool()
-
-class Spooler(sp.Spooler):
-    """Responsible for the mechanics of spooling to a pytables/hdf file.
-    """
-    def __init__(self, filename, frameSource, frameShape, complevel=6, complib='zlib', **kwargs):
-        sp.Spooler.__init__(self, filename, frameSource, **kwargs)
-        from PYME.IO.acquisition_backends import HDFBackend
-
-        self._backend = HDFBackend(filename, complevel=complevel, complib=complib,
-                        shape=[-1,-1,1,-1,1], # spooled series are time-series (for now)
-                        )
-        self.evtLogger = HDFEventLogger(self, self._backend.h5File, time_fcn=self._time_fcn)
-
-        self.md = self._backend.mdh        
-
-    def finalise(self):
-        """ close files"""
-           
-        self._backend.finalise()
-        
-    def OnFrame(self, sender, frameData, **kwargs):
-        """Called on each frame"""
-
-        if not self.watchingFrames:
-            # drop frames if we've already stopped spooling. TODO - do we also need to check if disconnect works?
-            return
-        
-        self._backend.store_frame(self.imNum, frameData)
-            
-        sp.Spooler.OnFrame(self)
-        
-    def __del__(self):
-        if self.spoolOn:
-            self.StopSpool()
\ No newline at end of file
diff --git a/PYME/IO/HTTPSpooler.py b/PYME/IO/HTTPSpooler.py
deleted file mode 100644
index c66c974b5..000000000
--- a/PYME/IO/HTTPSpooler.py
+++ /dev/null
@@ -1,268 +0,0 @@
-#!/usr/bin/python
-
-##################
-# QueueSpooler.py
-#
-# Copyright David Baddeley, 2009
-# d.baddeley@auckland.ac.nz
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##################
-
-from PYME.IO import MetaDataHandler
-from PYME.IO.events import MemoryEventLogger
-import PYME.IO.Spooler as sp
-
-import os
-import time
-import threading
-
-try:
-    # noinspection PyCompatibility
-    import Queue # type: ignore
-except ImportError:
-    #py3
-    import queue as Queue
-
-from PYME.IO import clusterIO
-from PYME.IO import PZFFormat
-from PYME import config
-
-import random
-import logging
-logger = logging.getLogger(__name__)
-
-def genSequenceID(filename=''):
-    return  int(time.time()) & random.randint(0, 2**31) << 31
-
-def getReducedFilename(filename):
-    #rname = filename[len(nameUtils.datadir):]
-        
-    sname = '/'.join(filename.split(os.path.sep))
-    if sname.startswith('/'):
-        sname = sname[1:]
-    
-    return sname
-    
-def exists(seriesName):
-    return clusterIO.exists(getReducedFilename(seriesName))
-
-#Push data to cluster from multiple threads simultaeneously to hide IO latency
-#of each individual node. Not sure what the best number is here - currenty set
-#a "safe" maximum number of nodes that could access data
-NUM_POLL_THREADS = 10
-QUEUE_MAX_SIZE = 200 # ~10k frames
-
-defaultCompSettings = {
-    'compression' : PZFFormat.DATA_COMP_HUFFCODE,
-    'quantization' : PZFFormat.DATA_QUANT_NONE,
-    'quantizationOffset' : -1e6, # set to an unreasonable value so that we raise an error if default offset is used
-    'quantizationScale' : 1.0
-}
-
-class Spooler(sp.Spooler):
-    def __init__(self, filename, frameSource, frameShape, **kwargs):
-        sp.Spooler.__init__(self, filename, frameSource, **kwargs)
-        #filename = filename[len(nameUtils.datadir):]
-        
-        #filename, 
-        self.seriesName = getReducedFilename(filename)
-        
-        #self.seriesName = '/'.join(filename.split(os.path.sep))
-        #if self.seriesName.startswith('/'):
-        #    self.seriesName = self.seriesName[1:]
-        #print filename, self.seriesName
-        self._aggregate_h5 = kwargs.get('aggregate_h5', False)
-        
-        self.clusterFilter = kwargs.get('serverfilter', 
-                                        config.get('dataserver-filter', ''))
-        self._buffer = []
-        
-        self.buflen = config.get('httpspooler-chunksize', 50)
-        
-        self._postQueue = Queue.Queue(QUEUE_MAX_SIZE)
-        self._dPoll = True
-        self._stopping = False
-        self._lock = threading.Lock()
-        
-        self._last_thread_exception = None
-
-        self._numThreadsProcessing = 0
-        
-        self._pollThreads = []
-        for i in range(NUM_POLL_THREADS):
-            pt = threading.Thread(target=self._queuePoll)
-            pt.daemon = False
-            pt.start()
-            self._pollThreads.append(pt)
-        
-        self.md = MetaDataHandler.NestedClassMDHandler()
-        self.evtLogger = MemoryEventLogger(self, time_fcn=self._time_fcn)
-        
-        self.sequenceID = genSequenceID()
-        self.md['imageID'] = self.sequenceID
-
-        self._lastFrameTime = 1e12
-
-        self.compSettings = {}
-        self.compSettings.update(defaultCompSettings)
-        try:
-            self.compSettings.update(kwargs['compressionSettings'])
-        except KeyError:
-            pass
-        
-        if not self.compSettings['quantization'] == PZFFormat.DATA_QUANT_NONE:
-            # do some sanity checks on our quantization parameters
-            # note that these conversions will throw a ValueError if the settings are not numeric
-            offset = float(self.compSettings['quantizationOffset'])
-            scale = float(self.compSettings['quantizationScale'])
-            
-            # these are potentially a bit too permissive, but should catch an offset which has been left at the
-            # default value
-            assert(offset >= 0)
-            assert(scale >=.001)
-            assert(scale <= 100)
-            
-    def _queuePoll(self):
-        while self._dPoll:
-            try:
-                data = self._postQueue.get_nowait()
-
-                with self._lock:
-                    self._numThreadsProcessing += 1
-
-                try:
-                    files = []
-                    for imNum, frame in data:
-                        if self._aggregate_h5:
-                            fn = '/'.join(['__aggregate_h5', self.seriesName, 'frame%05d.pzf' % imNum])
-                        else:
-                            fn = '/'.join([self.seriesName, 'frame%05d.pzf' % imNum])
-                            
-                        pzf = PZFFormat.dumps(frame, sequenceID=self.sequenceID, frameNum = imNum, **self.compSettings)
-
-                        files.append((fn, pzf))
-
-                    if len(files) > 0:
-                        clusterIO.put_files(files, serverfilter=self.clusterFilter)
-                        
-                except Exception as e:
-                    self._last_thread_exception = e
-                    logging.exception('Exception whilst putting files')
-                    raise
-                finally:
-                    with self._lock:
-                        self._numThreadsProcessing -= 1
-
-                time.sleep(.01)
-                #print 't', len(data)
-            except Queue.Empty:
-                if self._stopping:
-                    self._dPoll = False
-                else:
-                    time.sleep(.01)
-                
-    def finished(self):
-        if not self._last_thread_exception is None:
-            #raise an exception here, in the calling thread
-            logging.error('An exception occurred in one of the spooling threads')
-            raise RuntimeError('An exception occurred in one of the spooling threads')
-        else:
-            return (not self._dPoll) and self._postQueue.empty() and (self._numThreadsProcessing == 0)
-
-        
-    def getURL(self):
-        #print CLUSTERID, self.seriesName
-        return 'PYME-CLUSTER://%s/%s' % (self.clusterFilter, self.seriesName)
-        
-    def StartSpool(self):
-        sp.Spooler.StartSpool(self)
-        
-        logger.debug('Starting spooling: %s' %self.seriesName)
-        
-        if self._aggregate_h5:
-            #NOTE: allow a longer timeout than normal here as __aggregate with metadata waits for a lock on the server side before
-            # actually adding (and is therefore susceptible to longer latencies than most operations). FIXME - remove server side lock.
-            clusterIO.put_file('__aggregate_h5/' + self.seriesName + '/metadata.json', self.md.to_JSON().encode(), serverfilter=self.clusterFilter, timeout=3)
-        else:
-            clusterIO.put_file(self.seriesName + '/metadata.json', self.md.to_JSON().encode(), serverfilter=self.clusterFilter)
-    
-    def finalise(self):
-        # wait until our input queue is empty rather than immediately stopping saving.
-        self._stopping=True
-        logger.debug('Stopping spooling %s' % self.seriesName)
-        
-        
-        #join our polling threads
-        if config.get('httpspooler-jointhreads', True):
-            # Allow this to be switched off in a config option for maximum performance on High Throughput system.
-            # Joining threads is the recommended and safest behaviour, but forces spooling of current series to complete
-            # before next series starts, so could have negative performance implications.
-            # The alternative - letting spooling continue during the acquisition of the next series - has the potential
-            # to result in runaway memory and thread usage when things go pear shaped (i.e. spooling is not fast enough)
-            # TODO - is there actually a performance impact that justifies this config option, or is it purely theoretical
-            for pt in self._pollThreads:
-                pt.join()
-
-        # remove our reference to the threads which hold back-references preventing garbage collection
-        del(self._pollThreads)
-        
-        # save events and final metadata
-        # TODO - use a binary format for saving events - they can be quite
-        # numerous, and can trip the standard 1 s clusterIO.put_file timeout.
-        # Use long timeouts as a temporary hack because failing these can ruin
-        # a dataset
-        if self._aggregate_h5:
-            clusterIO.put_file('__aggregate_h5/' + self.seriesName + '/final_metadata.json', 
-                               self.md.to_JSON().encode(), self.clusterFilter)
-            clusterIO.put_file('__aggregate_h5/' + self.seriesName + '/events.json', 
-                               self.evtLogger.to_JSON().encode(),
-                               self.clusterFilter, timeout=10)
-        else:
-            clusterIO.put_file(self.seriesName + '/final_metadata.json', 
-                               self.md.to_JSON().encode(), self.clusterFilter)
-            clusterIO.put_file(self.seriesName + '/events.json', 
-                               self.evtLogger.to_JSON().encode(), 
-                               self.clusterFilter, timeout=10)
-        
-        
-    def OnFrame(self, sender, frameData, **kwargs):
-        # NOTE: copy is now performed in frameWrangler, so we don't need to worry about it here
-        if frameData.shape[0] == 1:
-            self._buffer.append((self.imNum, frameData))
-        else:
-            self._buffer.append((self.imNum, frameData.reshape(1, frameData.shape[0], frameData.shape[1])))
-
-        #print len(self.buffer)
-        t = time.time()
-
-        #purge buffer if more than  self.buflen frames have been added, or more than 1 second elapsed
-        if (len(self._buffer) >= self.buflen) or ((t - self._lastFrameTime) > 1):
-            self.FlushBuffer()
-            self._lastFrameTime = t
-        
-        sp.Spooler.OnFrame(self)
-        
-    def cleanup(self):
-        self._dPoll = False
-      
-    def FlushBuffer(self):
-      self._postQueue.put(self._buffer)
-      self._buffer = []
-     
-
-
-
-   
diff --git a/PYME/IO/QueueSpooler.py b/PYME/IO/QueueSpooler.py
deleted file mode 100644
index 39fb97bfb..000000000
--- a/PYME/IO/QueueSpooler.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/python
-
-##################
-# QueueSpooler.py
-#
-# Copyright David Baddeley, 2009
-# d.baddeley@auckland.ac.nz
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##################
-
-import tables
-from PYME.IO import MetaDataHandler
-import os
-import time
-
-import PYME.IO.Spooler as sp
-from PYME.IO.FileUtils import fileID
-
-try:
-    from PYME.misc import hybrid_ns
-    
-    ns = hybrid_ns.getNS()
-except ImportError:
-    ns = None
-      
-
-class Spooler(sp.Spooler):
-    def __init__(self, filename, frameSource, frameShape, complevel=6, complib='zlib', **kwargs):
-#       if 'PYME_TASKQUEUENAME' in os.environ.keys():
-#            taskQueueName = os.environ['PYME_TASKQUEUENAME']
-#       else:
-#            taskQueueName = 'taskQueue'
-        import Pyro.core
-        from PYME.IO.events import QueueEventLogger
-        sp.Spooler.__init__(self, filename, frameSource, **kwargs)
-
-        from PYME.misc.computerName import GetComputerName
-        compName = GetComputerName()
-
-        taskQueueName = 'TaskQueues.%s' % compName
-
-        if ns:
-            URI = ns.resolve(taskQueueName)
-        else:
-            URI = 'PYRONAME://' + taskQueueName
-
-        self.tq = Pyro.core.getProxyForURI(URI)
-        self.tq._setOneway(['postTask', 'postTasks', 'addQueueEvents', 'setQueueMetaData', 'logQueueEvent'])
-
-        self.seriesName = filename
-        self.buffer = []
-        self.buflen = 30
-
-        self.tq.createQueue('HDFTaskQueue',self.seriesName, filename, frameSize = frameShape, complevel=complevel, complib=complib)
-
-        self.md = MetaDataHandler.QueueMDHandler(self.tq, self.seriesName)
-        self.evtLogger = QueueEventLogger(self, self.tq, self.seriesName, time_fcn=self._time_fcn)
-   
-    def OnFrame(self, sender, frameData, **kwargs):
-        if not self.watchingFrames:
-            #we have already disconnected
-            return
-          
-        #self.tq.postTask(cSMI.CDataStack_AsArray(caller.ds, 0).reshape(1,self.scope.cam.GetPicWidth(),self.scope.cam.GetPicHeight()), self.seriesName)
-
-        # NOTE: copy is now performed in frameWrangler, so we don't need to worry about it here
-        if frameData.shape[0] == 1:
-            self.buffer.append(frameData)
-        else:
-            self.buffer.append(frameData.reshape(1,frameData.shape[0],frameData.shape[1]))
-
-        if self.imNum == 0: #first frame
-            self.md.setEntry('imageID', fileID.genFrameID(self.buffer[-1].squeeze()))
-
-        if (len(self.buffer) >= self.buflen):
-            self.FlushBuffer()
-
-        sp.Spooler.OnFrame(self)
-      
-    def FlushBuffer(self):
-        t1 = time.time()
-        self.tq.postTasks(self.buffer, self.seriesName)
-        #print time.time() -t1
-        self.buffer = []
-
-
-
-   
diff --git a/PYME/IO/Spooler.py b/PYME/IO/Spooler.py
index 427579456..c18428047 100755
--- a/PYME/IO/Spooler.py
+++ b/PYME/IO/Spooler.py
@@ -52,7 +52,7 @@ def __init__(self, filename, frameSource, protocol = p.NullProtocol,
         
         Parameters
         ----------
-        scope : PYME.Acquire.microscope.microscope object
+        scope : PYME.Acquire.microscope.Microscope object
             The microscope providing the data
         filename : string
             The file into which to spool
@@ -80,7 +80,7 @@ def __init__(self, filename, frameSource, protocol = p.NullProtocol,
             # if not provided / None)
             self.stack_settings = stack_settings
         
-        self.onSpoolStop = dispatch.Signal()
+        self.on_stop = dispatch.Signal()
 
         self._last_gui_update = 0
         self.spoolOn = False
@@ -144,7 +144,7 @@ def StopSpool(self):
             self.guiUpdateCallback()
         
         self.finalise()
-        self.onSpoolStop.send(self)
+        self.on_stop.send(self)
         self.spool_complete = True
         
     def finalise(self):
@@ -176,7 +176,7 @@ def abort(self):
             pass
 
         self.spoolOn = False
-        self.onSpoolStop.send(self)
+        self.on_stop.send(self)
         
 
     def OnFrame(self, **kwargs):
diff --git a/PYME/IO/acquisition_backends.py b/PYME/IO/acquisition_backends.py
index d7af97db5..03753b11c 100644
--- a/PYME/IO/acquisition_backends.py
+++ b/PYME/IO/acquisition_backends.py
@@ -8,13 +8,14 @@
 from PYME.IO.DataSources.BaseDataSource import XYZTCWrapper
 from PYME.IO.DataSources.ArrayDataSource import ArrayDataSource
 from PYME.IO import PZFFormat
+from PYME.IO.events import HDFEventLogger, MemoryEventLogger
 
 class Backend(abc.ABC):
     """
     Base class for acquisition backends.
 
     """
-    def __init__(self, dim_order='XYCZT', shape=[-1, -1,-1,1,1]):
+    def __init__(self, dim_order='XYCZT', shape=[-1, -1,-1,1,1], evt_time_fcn=time.time):
         if not hasattr(self, 'mdh'):
             self.mdh = MetaDataHandler.DictMDHandler()
         self.mdh['imageID'] = self.sequence_id
@@ -27,6 +28,13 @@ def __init__(self, dim_order='XYCZT', shape=[-1, -1,-1,1,1]):
         self.mdh['SizeT'] =  shape[3]
         self.mdh['SizeZ'] =  shape[2]
 
+        self.imNum = 0 # for event logger compatibility
+
+        if not hasattr(self, 'event_logger'):
+            # if we haven't already defined an event logger in a derived class, create a memory logger
+            self.event_logger = MemoryEventLogger(spooler=self, time_fcn=evt_time_fcn)
+
+
     @abc.abstractmethod
     def store_frame(self, n, frame_data):
         """ Store frame data (over-ride in derived classes)
@@ -69,8 +77,8 @@ def sequence_id(self):
 
 
 class MemoryBackend(Backend):
-    def __init__(self, size_x, size_y, n_frames, dtype='uint16', dim_order='XYCZT', shape=[-1, -1,1,1,1]):
-        Backend.__init__(self)
+    def __init__(self, size_x, size_y, n_frames, dtype='uint16', series_name=None, dim_order='XYCZT', shape=[-1, -1,1,1,1], evt_time_fcn=time.time, **kwargs):
+        Backend.__init__(self, dim_order, shape, evt_time_fcn=evt_time_fcn)
         self.data = np.empty([size_x, size_y, n_frames], dtype=dtype)
         
         # once we have proper xyztc support in the image viewer
@@ -79,8 +87,10 @@ def __init__(self, size_x, size_y, n_frames, dtype='uint16', dim_order='XYCZT',
         
     def store_frame(self, n, frame_data):
         self.data[:,:,n] = frame_data
+        self.imNum = n+1
 
-
+    def finalise(self):
+        self.image.events = self.event_logger.events
 
 
 def distfcn_oidic(n_servers, i=None):
@@ -104,11 +114,11 @@ class ClusterBackend(Backend):
     }
 
     def __init__(self, series_name, dim_order='XYCZT', shape=[-1, -1,-1,1,1], distribution_fcn=None, compression_settings={}, 
-                cluster_h5=False, serverfilter=clusterIO.local_serverfilter):
+                cluster_h5=False, serverfilter=clusterIO.local_serverfilter, evt_time_fcn = time.time, **kwargs):
         from PYME.IO import cluster_streaming
         from PYME.IO import PZFFormat
 
-        Backend.__init__(self, dim_order, shape)
+        Backend.__init__(self, dim_order, shape, evt_time_fcn=evt_time_fcn)
 
         self.series_name = series_name
         self._cluster_h5 = cluster_h5
@@ -165,11 +175,12 @@ def store_frame(self, n, frame_data):
         fn = '/'.join([self._series_location, 'frame%05d.pzf' % n])
 
         self._streamer.put(fn, (frame_data, n), i=n)
+        self.imNum = n+1
 
     def initialise(self):
         self._streamer.put(self._series_location + '/metadata.json', self.mdh.to_JSON().encode())
     
-    def finalise(self, events='[]'):
+    def finalise(self):
         #TODO - is this needed
         self._streamer.put(self._series_location + '/final_metadata.json', self.mdh.to_JSON().encode())
 
@@ -177,18 +188,17 @@ def finalise(self, events='[]'):
         # TODO - better events support - current assumption is that they are passed already formatted as json
         # TODO - use a binary format for saving events - they can be quite
         # numerous
-        if events is not None:
-            self._streamer.put(self._series_location + '/events.json', events) 
-        
+        self._streamer.put(self._series_location + '/events.json', self.event_logger.to_JSON().encode()) 
         self._streamer.close()
 
 
 class HDFBackend(Backend):
-    def __init__(self, series_name, dim_order='XYCZT', shape=[-1, -1,-1,1,1], complevel=6, complib='zlib'):
+    def __init__(self, series_name, dim_order='XYCZT', shape=[-1, -1,-1,1,1], complevel=6, complib='zlib', evt_time_fcn=time.time, **kwargs):
         import tables
 
         self.h5File = tables.open_file(series_name, 'w')
         self.mdh = MetaDataHandler.HDFMDHandler(self.h5File)
+        self.event_logger = HDFEventLogger(self, self.h5File, time_fcn=evt_time_fcn)
         Backend.__init__(self, dim_order, shape)
            
         self._complevel = complevel
@@ -208,6 +218,8 @@ def store_frame(self, n, frame_data):
         else:
             self.imageData.append(frame_data.reshape(1,frame_data.shape[0],frame_data.shape[1]))
 
+        self.imNum = n+1
+
 
     def finalise(self, events=None):
         self.imageData.attrs.DimOrder = self._dim_order
diff --git a/PYME/IO/clusterExport.py b/PYME/IO/clusterExport.py
index 9afd1ca27..1248cf6f2 100644
--- a/PYME/IO/clusterExport.py
+++ b/PYME/IO/clusterExport.py
@@ -5,7 +5,8 @@
 @author: david
 """
 
-from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
+#from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
+from PYME.IO import acquisition_backends
 from PYME.IO import MetaDataHandler
 
 from PYME.contrib import dispatch
@@ -65,26 +66,42 @@ def ExportImageToCluster(image, filename, progCallback=None):
     """
     
     #create virtual frame and metadata sources
-    imgSource = ImageFrameSource()
-    mds = MDSource(image.mdh)
-    MetaDataHandler.provideStartMetadata.append(mds)
+    # imgSource = ImageFrameSource()
+    # mds = MDSource(image.mdh)
+    # MetaDataHandler.provideStartMetadata.append(mds)
     
-    if not progCallback is None:
-        imgSource.spoolProgress.connect(progCallback)
+    # if not progCallback is None:
+    #     imgSource.spoolProgress.connect(progCallback)
     
     #queueName = getRelFilename(self.dirname + filename + '.h5')
     
     #generate the spooler
-    spooler = HTTPSpooler.Spooler(filename, imgSource.onFrame, frameShape = image.data.shape[:2])
+    #spooler = HTTPSpooler.Spooler(filename, imgSource.onFrame, frameShape = image.data.shape[:2])
+
+    backend = acquisition_backends.ClusterBackend(filename, shape=image.data.shape)
+    backend.mdh.copyEntriesFrom(image.mdh)
+
+    backend.initialise()
+
+    nFrames = image.data.getNumSlices()
+    for i in range(nFrames):
+        # self.onFrame.send(self, frameData=data.getSlice(i))
+        backend.store_frame(i, image.data.getSlice(i))
+        if (i % 3000) == 0:
+            progCallback(percent=float(i)/nFrames)
+            print('Spooling %d of %d frames' % (i, nFrames))
+
+    backend.finalise() #TODO - copy events?
+    
     
-    #spool our data    
-    spooler.StartSpool()
-    imgSource.spoolData(image.data)
-    spooler.FlushBuffer()
-    spooler.StopSpool()
+    # #spool our data    
+    # spooler.StartSpool()
+    # imgSource.spoolData(image.data)
+    # spooler.FlushBuffer()
+    # spooler.StopSpool()
     
     #remove the metadata generator
-    MetaDataHandler.provideStartMetadata.remove(mds)
+    #MetaDataHandler.provideStartMetadata.remove(mds)
 
 
 SERIES_PATTERN = '%(day)d_%(month)d_series_%(counter)'
diff --git a/PYME/IO/events.py b/PYME/IO/events.py
index 293a20978..1fa998db1 100644
--- a/PYME/IO/events.py
+++ b/PYME/IO/events.py
@@ -71,13 +71,13 @@ class HDFEventLogger(EventLogger):
         The open HDF5 file to write to
     """
     
-    def __init__(self, spool, hdf5File, time_fcn=time.time):
+    def __init__(self, spooler, hdf5File, time_fcn=time.time):
         """Create a new Events table.
   
   
         """
         EventLogger.__init__(self, time_fcn=time_fcn)
-        self.spooler = spool
+        self.spooler = spooler
         #self.scope = scope
         self.hdf5File = hdf5File
         self._event_lock = threading.Lock()
@@ -139,6 +139,10 @@ def logEvent(self, eventName, eventDescr='', timestamp=None):
         
         with self._event_lock:
             self._events.append((eventName, eventDescr, timestamp))
+
+    @property
+    def events(self):
+        return self._events
     
     def to_JSON(self):
         import json
@@ -146,24 +150,4 @@ def to_JSON(self):
     
     def to_recarray(self):
         return self.list_to_array(self._events)
-    
-    
-class QueueEventLogger(EventLogger):
-    """ Event backend for TaskQueue (Pyro) based spooling
-    
-    Exists for Py27 backwards compatibility and does not work on py3. Don't use in new code.
-    """
-    def __init__(self, spool, tq, queueName, time_fcn=time.time):
-      EventLogger.__init__(self, time_fcn=time_fcn)
-      self.spooler = spool
-      #self.scope = scope
-      self.tq = tq
-      self.queueName = queueName
-    
-    def logEvent(self, eventName, eventDescr = '', timestamp=None):
-      if eventName == 'StartAq':
-          eventDescr = '%d' % self.spooler.imNum
-    
-      if timestamp is None:
-          timestamp = self._time_fcn()
-      self.tq.logQueueEvent(self.queueName, (eventName, eventDescr, timestamp))
\ No newline at end of file
+    
\ No newline at end of file
diff --git a/PYME/experimental/dcimgSpoolShim.py b/PYME/experimental/dcimgSpoolShim.py
index d06459e45..90d19de01 100644
--- a/PYME/experimental/dcimgSpoolShim.py
+++ b/PYME/experimental/dcimgSpoolShim.py
@@ -2,7 +2,7 @@
 import os
 import time
 
-from PYME.IO import HTTPSpooler
+from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
 from PYME.Analysis import MetaData
 from PYME.IO import MetaDataHandler, clusterIO, unifiedIO
 from PYME.IO.DataSources import DcimgDataSource, MultiviewDataSource
diff --git a/PYME/localization/FitFactories/LatGaussFitlmR.py b/PYME/experimental/levmar_gfit/LatGaussFitlmR.py
similarity index 100%
rename from PYME/localization/FitFactories/LatGaussFitlmR.py
rename to PYME/experimental/levmar_gfit/LatGaussFitlmR.py
diff --git a/PYME/experimental/levmar_gfit/README.txt b/PYME/experimental/levmar_gfit/README.txt
new file mode 100644
index 000000000..1132977ea
--- /dev/null
+++ b/PYME/experimental/levmar_gfit/README.txt
@@ -0,0 +1,12 @@
+This is an early attempt at using an alternative solver for the weighted-least squares fitting,
+moved here to get it out of the way and tidy up the displayed fit factories.
+
+The hope was to improve speed by keeping all computations in complied code, rather than the status 
+quo where the solver is (compiled) fortran and the model function is compiled c, but everything goes 
+through python on each iteration because we are using the scipy minpack wrappings. In practice there
+was a small improvement (around 10-20% if I recall correctly), although not enough to justify the 
+extra maintenance hassle - (the code needs to be compiled with gcc, and so doesn't work with a standard
+windows build pipeline).
+
+It is retained on the off-chance it might be useful if trying to make a SIMD (ie. GPU) implementation 
+of the least-squares fitting but is not really suitable for use in it's current state.
\ No newline at end of file
diff --git a/PYME/localization/lev_gfit/.cvsignore b/PYME/experimental/levmar_gfit/lev_gfit/.cvsignore
similarity index 100%
rename from PYME/localization/lev_gfit/.cvsignore
rename to PYME/experimental/levmar_gfit/lev_gfit/.cvsignore
diff --git a/PYME/localization/lev_gfit/__init__.py b/PYME/experimental/levmar_gfit/lev_gfit/__init__.py
similarity index 100%
rename from PYME/localization/lev_gfit/__init__.py
rename to PYME/experimental/levmar_gfit/lev_gfit/__init__.py
diff --git a/PYME/localization/lev_gfit/levmar-2.2/Axb.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Axb.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/Axb.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Axb.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/Axb_core.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Axb_core.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/Axb_core.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Axb_core.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/LICENSE b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/LICENSE
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/LICENSE
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/LICENSE
diff --git a/PYME/localization/lev_gfit/levmar-2.2/Makefile b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Makefile
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/Makefile
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Makefile
diff --git a/PYME/localization/lev_gfit/levmar-2.2/Makefile.icc b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Makefile.icc
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/Makefile.icc
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Makefile.icc
diff --git a/PYME/localization/lev_gfit/levmar-2.2/Makefile.vc b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Makefile.vc
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/Makefile.vc
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/Makefile.vc
diff --git a/PYME/localization/lev_gfit/levmar-2.2/README.txt b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/README.txt
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/README.txt
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/README.txt
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lm.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lm.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lm.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lm.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lm.h b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lm.h
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lm.h
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lm.h
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lm_core.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lm_core.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lm_core.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lm_core.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lmbc.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmbc.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lmbc.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmbc.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lmbc_core.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmbc_core.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lmbc_core.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmbc_core.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lmblec.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmblec.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lmblec.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmblec.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lmblec_core.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmblec_core.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lmblec_core.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmblec_core.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lmdemo.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmdemo.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lmdemo.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmdemo.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lmlec.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmlec.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lmlec.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmlec.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/lmlec_core.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmlec_core.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/lmlec_core.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/lmlec_core.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/Makefile b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/Makefile
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/Makefile
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/Makefile
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/Makefile.w32 b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/Makefile.w32
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/Makefile.w32
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/Makefile.w32
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/README.txt b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/README.txt
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/README.txt
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/README.txt
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/bt3.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/bt3.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/bt3.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/bt3.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/hs01.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/hs01.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/hs01.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/hs01.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/jacbt3.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/jacbt3.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/jacbt3.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/jacbt3.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/jachs01.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/jachs01.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/jachs01.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/jachs01.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/jacmeyer.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/jacmeyer.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/jacmeyer.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/jacmeyer.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/jacmodhs52.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/jacmodhs52.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/jacmodhs52.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/jacmodhs52.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/levmar.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/levmar.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/levmar.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/levmar.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/levmar.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/levmar.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/levmar.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/levmar.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/lmdemo.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/lmdemo.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/lmdemo.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/lmdemo.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/meyer.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/meyer.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/meyer.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/meyer.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/modhs52.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/modhs52.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/modhs52.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/modhs52.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/matlab/mods235.m b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/mods235.m
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/matlab/mods235.m
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/matlab/mods235.m
diff --git a/PYME/localization/lev_gfit/levmar-2.2/misc.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/misc.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/misc.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/misc.c
diff --git a/PYME/localization/lev_gfit/levmar-2.2/misc.h b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/misc.h
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/misc.h
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/misc.h
diff --git a/PYME/localization/lev_gfit/levmar-2.2/misc_core.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/misc_core.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar-2.2/misc_core.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar-2.2/misc_core.c
diff --git a/PYME/localization/lev_gfit/levmar_gfit.c b/PYME/experimental/levmar_gfit/lev_gfit/levmar_gfit.c
similarity index 100%
rename from PYME/localization/lev_gfit/levmar_gfit.c
rename to PYME/experimental/levmar_gfit/lev_gfit/levmar_gfit.c
diff --git a/PYME/localization/lev_gfit/setup.py b/PYME/experimental/levmar_gfit/lev_gfit/setup.py
similarity index 100%
rename from PYME/localization/lev_gfit/setup.py
rename to PYME/experimental/levmar_gfit/lev_gfit/setup.py
diff --git a/PYME/recipes/acquisition.py b/PYME/recipes/acquisition.py
index c4cdfb1f1..e14b34058 100644
--- a/PYME/recipes/acquisition.py
+++ b/PYME/recipes/acquisition.py
@@ -46,7 +46,7 @@ class QueueAcquisitions(OutputModule):
                 spool to single h5 file on cluster (True) or pzf files (False).
                 Only relevant for `Cluster` method.
             pzf_compression_settings : dict
-                see PYME.IO.HTTPSpooler
+                see PYME.IO.acquisition_backends
             protocol_name : str
                 filename of the acquisition protocol to follow while spooling
             subdirectory : str
diff --git a/PYME/simulation/pointsets.py b/PYME/simulation/pointsets.py
index db6f3c8e4..f0016bcb8 100644
--- a/PYME/simulation/pointsets.py
+++ b/PYME/simulation/pointsets.py
@@ -13,6 +13,24 @@ def points(self):
         c = np.zeros_like(x)
         yield np.array([x,y,z,c], 'f').T
 
+class RandomSource(PointSource):
+    numPoints = Int(100)
+    xRange = Float(10000)
+    yRange = Float(10000)
+    zRange = Float(100)
+    
+    #name = Str('Random')
+    
+    def getPoints(self):
+        return np.random.uniform(-self.xRange, self.xRange, self.numPoints), np.random.uniform(-self.yRange, self.yRange, self.numPoints), np.random.uniform(-self.zRange, self.zRange, self.numPoints)
+
+    def genMetaData(self, mdh):
+        mdh['GeneratedPoints.Source.Type'] = 'Random'
+        mdh['GeneratedPoints.Source.NumPoints'] = self.numPoints
+        mdh['GeneratedPoints.Source.XRange'] = self.xRange
+        mdh['GeneratedPoints.Source.YRange'] = self.yRange
+        mdh['GeneratedPoints.Source.ZRange'] = self.zRange
+
 
 class WormlikeSource(PointSource):
     kbp = Float(200)
diff --git a/PYME/ui/cascading_layout.py b/PYME/ui/cascading_layout.py
new file mode 100644
index 000000000..2d8300c68
--- /dev/null
+++ b/PYME/ui/cascading_layout.py
@@ -0,0 +1,39 @@
+"""A mixin for wxPython windows that allows for cascading layout of parent windows.
+This allows easier hiding and showing of nested panels.
+
+This is a cleaner and more easily readable replacement for the previous `fold1()` method in the fold panels.
+"""
+
+import logging
+logger = logging.getLogger(__name__)
+
+import wx
+
+class CascadingLayoutMixin(object):
+    def cascading_layout(self, depth=0):
+        """Lays out the parent window in a cascading fashion.
+        """
+        #print('    '*depth + 'Cascading layout - %s, size = %s' % (self, tuple(self.GetSize()))) 
+        self.GetSizer().Fit(self)
+        self.SetMinSize(self.GetSize())
+        
+
+        #print('    '*depth + 'Cascading layout - %s, new size = %s' % (self,  tuple(self.GetSize())))
+
+        try:
+            self.GetParent().cascading_layout(depth + 1)
+        except AttributeError:
+            try:
+                self.GetParent().Layout()
+            except AttributeError:
+                pass
+
+        self.Layout()
+        #print('    '*depth + 'Cascading layout - %s, final size = %s' % (self,  tuple(self.GetSize())))
+
+class CascadingLayoutPanel(wx.Panel, CascadingLayoutMixin):
+    """
+    A panel that supports cascading layout.
+    """
+    def __init__(self, *args, **kwargs):
+        wx.Panel.__init__(self, *args, **kwargs)
\ No newline at end of file
diff --git a/PYME/ui/manualFoldPanel.py b/PYME/ui/manualFoldPanel.py
index e59ebb585..4b60114ef 100755
--- a/PYME/ui/manualFoldPanel.py
+++ b/PYME/ui/manualFoldPanel.py
@@ -29,6 +29,10 @@
 import numpy as np
 
 import wx.lib.newevent
+from PYME.ui import cascading_layout
+
+import logging
+logger = logging.getLogger(__name__)
 
 PanelFoldCommandEvent, EVT_CMD_PANEL_FOLD = wx.lib.newevent.NewCommandEvent()
 
@@ -329,7 +333,7 @@ def OnSize(self, event):
 
 
 
-class foldElement:
+class foldElement(object):
     def __init__(self, window, foldable=True, foldedWindow=None):
         self.window = window
         self.foldable = foldable
@@ -339,7 +343,7 @@ def __init__(self, window, foldable=True, foldedWindow=None):
 #            self.foldedWindow = wx.StaticText(self.window.GetParent(), -1, '...')
 #            self.foldedWindow.Hide()
 
-class foldingPane(wx.Panel):
+class foldingPane(wx.Panel, cascading_layout.CascadingLayoutMixin):
     def __init__(self, *args, **kwargs):
         # NOTE: If you are creating a foldingPane to encapsulate other 
         # foldingPanes, the top-level foldingPane must have the flag
@@ -401,7 +405,7 @@ def __init__(self, *args, **kwargs):
 
         self.SetSizer(self.sizer)
         
-        self.Bind(wx.EVT_SIZE, self.OnSize)
+        #self.Bind(wx.EVT_SIZE, self.OnSize)
         
     def _create_caption_bar(self):
         """ This is over-rideable in derived classes so that they can implement their own caption bars"""
@@ -498,7 +502,7 @@ def Fold(self, fold=True):
                 pass
             self.Layout()
             wx.PostEvent(self, PanelFoldCommandEvent(self.GetId()))
-            self.fold1()
+            self.cascading_layout()
             return True
         else:
             return False
@@ -521,8 +525,8 @@ def Unfold(self):
                 self.stCaption.Refresh()
             except AttributeError:
                 pass
-            self.Layout()
-            self.fold1()
+
+            self.cascading_layout()
             wx.PostEvent(self, PanelFoldCommandEvent(self.GetId()))
             return True
         else:
@@ -531,7 +535,9 @@ def Unfold(self):
             #self.Layout()
     
     def OnSize(self, event):
-        self.Layout()
+        print('foldPane size event - %s, %s - %s' % (self.__class__.__name__, hex(id(self)), event.GetSize()))
+        #self.Layout()
+        event.Skip()
 
 
 r_arrow = b'\xff\xff\xdf\xff\x9f\xff\x1f\xff\x5f\xfe\xdf\xfc\xdf\xf9\xdf\xf3\xdf' \
@@ -608,7 +614,7 @@ def OnFold(self, event=None):
 
         #self.Layout()
         #self.Fit()
-        self.fold1()
+        self.cascading_layout()
 
 
 
@@ -676,6 +682,10 @@ def RegenSizer(self):
 
         self.sizer.Layout()
 
+    def cascading_layout(self, depth=0):
+        #logger.info('cascade layout - %s' % self)
+        self.fold1()
+
     def Clear(self):
         self.priorities = []
         for p in self.panes:
@@ -762,6 +772,8 @@ def OnResize(self, event):
         if (not self._in_fold1) and self.IsShownOnScreen():
             self.fold1()
 
+        event.Skip()
+
     def _layout(self, *args, **kwargs):
         self.Layout()
 
diff --git a/PYME/ui/mytimer.py b/PYME/ui/mytimer.py
index 0574c9a9b..8f374801b 100755
--- a/PYME/ui/mytimer.py
+++ b/PYME/ui/mytimer.py
@@ -24,9 +24,11 @@
 import wx
 import time
 
+from wx.core import TIMER_CONTINUOUS
 
 
-class MultiTimer(wx.Timer):
+
+class MultiTargetTimer(wx.Timer):
     """
     Timer which calls multiple handlers
     """
@@ -52,7 +54,13 @@ def Notify(self):
     def register_callback(self, callback):
         self.WantNotification.append(callback)
 
-mytimer=MultiTimer #alias for backwards compatibility
+    def start(self, delay_ms, single_shot=False):
+        if single_shot:
+            wx.CallAfter(self.Start, delay_ms, wx.TIMER_ONE_SHOT)
+        else:
+            wx.CallAfter(self.Start, delay_ms, wx.TIMER_CONTINUOUS)
+
+mytimer=MultiTargetTimer #alias for backwards compatibility
 
 class SingleTargetTimer(wx.Timer):
     """
@@ -68,7 +76,7 @@ def __init__(self, target):
     def Notify(self):
         self._target()
         
-    def start(self, delay_ms, single_shot=True):
+    def start(self, delay_ms, single_shot=False):
         if single_shot:
             wx.CallAfter(self.Start, delay_ms, wx.TIMER_ONE_SHOT)
         else:


Test patch:
diff --git a/PYME/IO/testClusterSpooling.py b/PYME/IO/testClusterSpooling.py
index 75e636816..057c69c12 100644
--- a/PYME/IO/testClusterSpooling.py
+++ b/PYME/IO/testClusterSpooling.py
@@ -54,8 +54,9 @@
 from PYME.IO import MetaDataHandler
 from PYME.IO.DataSources import DcimgDataSource, MultiviewDataSource
 from PYME.Analysis import MetaData
-from PYME.IO import HDFSpooler
-from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
+#from PYME.IO import HDFSpooler
+#from PYME.IO import HTTPSpooler_v2 as HTTPSpooler
+from PYME.Acquire import protocol_acquisition
 from PYME.IO import PZFFormat
 
 import time
@@ -93,17 +94,22 @@ def run(self, filename=None, nFrames = 2000, interval=0, compression=False, hdf_
         #generate the spooler
         if hdf_spooler:
             if compression:
-                self.spooler = HDFSpooler.Spooler(filename, self.onFrame, frameShape = frameShape, complevel=0)
+                #self.spooler = HDFSpooler.Spooler(filename, self.onFrame, frameShape = frameShape, complevel=0)
+                self.spooler = protocol_acquisition.ProtocolAcquisition(filename, self.onFrame, frameShape = frameShape, complevel=0,backend='hdf')
             else:
-                self.spooler = HDFSpooler.Spooler(filename, self.onFrame,frameShape = frameShape, complevel=3)
+                #self.spooler = HDFSpooler.Spooler(filename, self.onFrame,frameShape = frameShape, complevel=3)
+                self.spooler = protocol_acquisition.ProtocolAcquisition(filename, self.onFrame, frameShape = frameShape, complevel=3,backend='hdf')
         else:
             if compression:
-                self.spooler = HTTPSpooler.Spooler(filename, self.onFrame, frameShape = None, serverfilter=self.serverfilter)
+                self.spooler = protocol_acquisition.ProtocolAcquisition(filename, self.onFrame, frameShape = None, serverfilter=self.serverfilter, backend='cluster')
+
             else:
-                self.spooler = HTTPSpooler.Spooler(filename, self.onFrame,
+                self.spooler = protocol_acquisition.ProtocolAcquisition(filename, self.onFrame,
                                                    frameShape = None, serverfilter=self.serverfilter,
                                                    compressionSettings={'compression': PZFFormat.DATA_COMP_RAW,
-                                                                        'quantization':PZFFormat.DATA_QUANT_NONE})
+                                                                        'quantization':PZFFormat.DATA_QUANT_NONE}, backend='cluster')
+                
+        
         
         try:
             #spool our data