diff --git a/.gitmodules b/.gitmodules
index b03f7351c6e6a4eaaa5521f6c7a44deded20a9a2..eca2363e385ce7bd01c2e6f4d4b4c32af9b3fe94 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,6 @@
 [submodule "ext/nanogui"]
 	path = ext/nanogui
 	url = https://github.com/wjakob/nanogui.git
-[submodule "SDK/C++/public/ext/pybind11"]
-	path = SDK/C++/public/ext/pybind11
+[submodule "SDK/CPP/public/ext/pybind11"]
+	path = SDK/CPP/public/ext/pybind11
 	url = https://github.com/pybind/pybind11.git
diff --git a/SDK/CPP/CMakeLists.txt b/SDK/CPP/CMakeLists.txt
index b64eae3d9a119df248378b8b1e41f2b271187ba1..123671c2b6581beba4ab4bd5df167ab1e033a531 100644
--- a/SDK/CPP/CMakeLists.txt
+++ b/SDK/CPP/CMakeLists.txt
@@ -11,17 +11,41 @@ add_library(voltu SHARED
 	private/property_impl.cpp
 )
 
+file(READ "public/include/voltu/voltu.hpp" VOLVER)
+
+string(REGEX MATCH "VOLTU_VERSION_MAJOR ([0-9]*)" _ ${VOLVER})
+set(VOLTU_MAJOR ${CMAKE_MATCH_1})
+
+string(REGEX MATCH "VOLTU_VERSION_MINOR ([0-9]*)" _ ${VOLVER})
+set(VOLTU_MINOR ${CMAKE_MATCH_1})
+
+string(REGEX MATCH "VOLTU_VERSION_PATCH ([0-9]*)" _ ${VOLVER})
+set(VOLTU_PATCH ${CMAKE_MATCH_1})
+
+message("VolTu SDK version: ${VOLTU_MAJOR}.${VOLTU_MINOR}.${VOLTU_PATCH}")
+
+set_target_properties( voltu  PROPERTIES
+	VERSION "${VOLTU_MAJOR}.${VOLTU_MINOR}"
+	SOVERSION "${VOLTU_MAJOR}.${VOLTU_MINOR}"
+)
+
 target_include_directories(voltu
 	PUBLIC public/include
 	PRIVATE src)
 
 target_link_libraries(voltu ftlcommon ftldata ftlctrl ftlrgbd ftlstreams ftlrender Threads::Threads ${OpenCV_LIBS} openvr ftlnet nanogui ${NANOGUI_EXTRA_LIBS} ceres nvidia-ml)
 
+set(SDK_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/sdk")
+
 ExternalProject_Add(
 	voltu_sdk
 	SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/public"
-	BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/sdk"
+	BINARY_DIR ${SDK_BINARY_DIR}
 	INSTALL_COMMAND ""
 	BUILD_ALWAYS true
-	CMAKE_ARGS -DOpenCV_DIR=${OpenCV_DIR}
+	CMAKE_ARGS -DOpenCV_DIR=${OpenCV_DIR} -DWITH_PYTHON=True
 )
+
+if (BUILD_TESTS)
+	add_subdirectory(tests)
+endif()
diff --git a/SDK/CPP/private/room_impl.cpp b/SDK/CPP/private/room_impl.cpp
index 764b5740341202140bfaade2bbb0207ea46ef8b8..9fd0d2d437ca39ceeef3355dfb486a2dd270367d 100644
--- a/SDK/CPP/private/room_impl.cpp
+++ b/SDK/CPP/private/room_impl.cpp
@@ -16,7 +16,7 @@ RoomImpl::~RoomImpl()
 	if (filter_) filter_->remove();
 }
 
-bool RoomImpl::waitNextFrame(int64_t timeout)
+bool RoomImpl::waitNextFrame(int64_t timeout, bool except)
 {
 	if (!filter_)
 	{
@@ -40,10 +40,15 @@ bool RoomImpl::waitNextFrame(int64_t timeout)
 				return last_read_ < last_seen_;
 			});
 
+			if (except && last_read_ >= last_seen_)
+			{
+				throw voltu::exceptions::Timeout();
+			}
 			return last_read_ < last_seen_;
 		}
 		else if (timeout == 0)
 		{
+			if (except) throw voltu::exceptions::Timeout();
 			return false;
 		}
 		else
diff --git a/SDK/CPP/private/room_impl.hpp b/SDK/CPP/private/room_impl.hpp
index be8516a637b316c9a3964b14952f11fcadd23459..67d44474de421f97ba178ce509d1e72e2b76be46 100644
--- a/SDK/CPP/private/room_impl.hpp
+++ b/SDK/CPP/private/room_impl.hpp
@@ -16,7 +16,7 @@ public:
 	
 	~RoomImpl() override;
 
-	bool waitNextFrame(int64_t) override;
+	bool waitNextFrame(int64_t, bool except) override;
 
 	voltu::FramePtr getFrame() override;
 
diff --git a/SDK/CPP/private/system.cpp b/SDK/CPP/private/system.cpp
index 84bdbcd406793b0a1b51300b2f7ef0e860e67287..f711a3783c3edc0751d90931b2806e3b5f67e10b 100644
--- a/SDK/CPP/private/system.cpp
+++ b/SDK/CPP/private/system.cpp
@@ -87,6 +87,11 @@ std::list<voltu::RoomId> SystemImpl::listRooms()
 
 voltu::RoomPtr SystemImpl::getRoom(voltu::RoomId id)
 {
+	if (feed_->getURI(id).size() == 0)
+	{
+		throw voltu::exceptions::InvalidRoomId();
+	}
+
 	auto s = std::make_shared<voltu::internal::RoomImpl>(feed_);
 	s->addFrameSet(id);
 	return s;
diff --git a/SDK/CPP/public/CMakeLists.txt b/SDK/CPP/public/CMakeLists.txt
index b3a4d7dc20ec1df5fc529d4a65b805855ec09c11..f1ea62a4e8a428581f6a4a7f73915d7413ac5c66 100644
--- a/SDK/CPP/public/CMakeLists.txt
+++ b/SDK/CPP/public/CMakeLists.txt
@@ -5,13 +5,13 @@ project (voltu_sdk VERSION 0.0.1)
 include(GNUInstallDirs)
 
 option(WITH_OPENCV "Build with OpenCV wrapper" ON)
-option(WITH_PYTHON "Build Python module" OFF)
+option(WITH_PYTHON "Build Python module" ON)
 
-find_package( Eigen3 REQUIRED NO_MODULE )
-find_package( Threads REQUIRED )
+find_package(Eigen3 REQUIRED NO_MODULE)
+find_package(Threads REQUIRED)
 
 if (WITH_OPENCV)
-	find_package( OpenCV REQUIRED )
+	find_package(OpenCV REQUIRED)
 endif()
 
 if(WIN32)
@@ -68,7 +68,30 @@ add_executable(voltu_fusion_evaluator
 )
 target_link_libraries(voltu_fusion_evaluator voltu_sdk)
 
+find_package (Python COMPONENTS Development.Module Interpreter)
+
+function(find_python_module module)
+	string(TOUPPER ${module} module_upper)
+	if(NOT PY_${module_upper})
+		execute_process(COMMAND "${Python_EXECUTABLE}" "-c"
+			"import ${module}; print(${module}.__file__.rstrip('__init__.py'))"
+			RESULT_VARIABLE _${module}_status
+			OUTPUT_VARIABLE _${module}_location
+			ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
+		if(NOT _${module}_status)
+			set(PY_${module_upper} ${_${module}_location} CACHE STRING
+				"Location of Python module ${module}")
+		endif(NOT _${module}_status)
+	endif(NOT PY_${module_upper})
+	find_package_handle_standard_args(PY_${module} DEFAULT_MSG PY_${module_upper})
+endfunction(find_python_module)
+
 if (WITH_PYTHON)
-	add_subdirectory(ext/pybind11)
-	add_subdirectory(python)
+	find_python_module(ply)
+	if (Python_FOUND AND PY_PLY)
+		add_subdirectory(ext/pybind11)
+		add_subdirectory(python)
+	else()
+		message(WARNING "Python dependencies not found, Python module is not built")
+	endif()
 endif()
diff --git a/SDK/CPP/public/ext/pybind11 b/SDK/CPP/public/ext/pybind11
new file mode 160000
index 0000000000000000000000000000000000000000..06b673a0daef1db4f921a19676a51abec6fb13e8
--- /dev/null
+++ b/SDK/CPP/public/ext/pybind11
@@ -0,0 +1 @@
+Subproject commit 06b673a0daef1db4f921a19676a51abec6fb13e8
diff --git a/SDK/CPP/public/include/voltu/defines.hpp b/SDK/CPP/public/include/voltu/defines.hpp
index 19f2416f2812cc41b69b6c2f73a7a7f3bd4f7588..513190c63f792199e063c857f48def876794660a 100644
--- a/SDK/CPP/public/include/voltu/defines.hpp
+++ b/SDK/CPP/public/include/voltu/defines.hpp
@@ -1,5 +1,5 @@
 /**
- * @file system.hpp
+ * @file defines.hpp
  * @copyright Copyright (c) 2020 Sebastian Hahta, MIT License
  * @author Sebastian Hahta
  */
@@ -20,3 +20,15 @@
 /// Lifetime of the return value is tied to the lifetime of a parent object
 #define PY_RV_LIFETIME_PARENT
 #endif
+
+#ifndef PY_SINGLETON
+/// Singleton instance, members exported to module. Requires creating the
+/// instance in PyModule constructor.
+#define PY_SINGLETON
+#endif
+
+#ifndef PY_SINGLETON_OBJECT
+/// Export as singleton instance instead of exporting members to module
+#define PY_SINGLETON_OBJECT
+#endif
+
diff --git a/SDK/CPP/public/include/voltu/initialise.hpp b/SDK/CPP/public/include/voltu/initialise.hpp
index 4a6e5b3ffc2c2326b5934d2577d0458605ddc51c..5d34a1e691797b8c4922aa274775d7f84ef401ac 100644
--- a/SDK/CPP/public/include/voltu/initialise.hpp
+++ b/SDK/CPP/public/include/voltu/initialise.hpp
@@ -13,34 +13,34 @@ namespace voltu
 {
 	/**
 	 * @brief Get core VolTu instance.
-	 * 
+	 *
 	 * This method returns a smart pointer to a singleton VolTu runtime
 	 * instance and must be the first VolTu call. On any given machine it is
 	 * only sensible and possible to have one runtime instance of VolTu due to
 	 * its use of hardware devices. Multiple real instances are not possible.
-	 * 
+	 *
 	 * @code
 	 * int main(int argc, char** argv) {
 	 *     auto vtu = voltu::instance();
-	 * 
+	 *
 	 *     vtu->open("device:camera");
 	 *     ...
 	 * }
 	 * @endcode
-	 * 
+	 *
 	 * @note
 	 * This method must only be called once.
-	 * 
+	 *
 	 * @throw voltu::exceptions::LibraryLoadFailed
 	 * If runtime not found or is invalid.
 	 *
 	 * @throw voltu::exceptions::RuntimeVersionMismatch
 	 * If major or minor version does not match the SDK headers.
-	 * 
+	 *
 	 * @throw voltu::exceptions::RuntimeAlreadyInUse
 	 * If a runtime instance is in use by another application.
-	 * 
+	 *
 	 * @return Singleton VolTu runtime instance.
 	 */
-	PY_API std::shared_ptr<voltu::System> instance();
+	std::shared_ptr<voltu::System> instance();
 }
diff --git a/SDK/CPP/public/include/voltu/room.hpp b/SDK/CPP/public/include/voltu/room.hpp
index 87f0f070cc3fae52bd52a6046c6cd4ae299680ea..f2578236c85c42bc64eb4413c8ba6c616be9bfb8 100644
--- a/SDK/CPP/public/include/voltu/room.hpp
+++ b/SDK/CPP/public/include/voltu/room.hpp
@@ -70,7 +70,7 @@ public:
 	 * @param timeout Millisecond timeout, or 0 or -1.
 	 * @return True if a new unseen frame is available.
 	 */
-	PY_API virtual bool waitNextFrame(int64_t timeout) = 0;
+	PY_API virtual bool waitNextFrame(int64_t timeout, bool except=false) = 0;
 
 	/**
 	 * @brief Check if a new frame is available.
diff --git a/SDK/CPP/public/include/voltu/system.hpp b/SDK/CPP/public/include/voltu/system.hpp
index 6a7e4cd96387a6e50b4933dad6892eb52104755d..8fb9c744cdf3c7646531803f349aeb7455962c12 100644
--- a/SDK/CPP/public/include/voltu/system.hpp
+++ b/SDK/CPP/public/include/voltu/system.hpp
@@ -20,7 +20,7 @@ namespace voltu
 /**
  * @brief Voltu semantic versioning information.
  */
-struct Version
+PY_NO_SHARED_PTR struct Version
 {
 	int major;  ///< API Incompatible change
 	int minor;	///< Possible binary incompatible, extensions
@@ -29,50 +29,51 @@ struct Version
 
 /**
  * @brief Singleton Voltu system instance.
- * 
+ *
  * Provides access to the key components such as opening streams or files and
  * creating virtual cameras. Use `voltu::instance()` to obtain the object. All
- * object instances in VolTu are managed by shared smart pointers.
+ * object instances in VolTu are managed by shared smart pointers. Python API
+ * automatically creates instance available as voltu.System.
  */
-class System
+PY_SINGLETON_OBJECT class System
 {
 public:
 	virtual ~System() = default;
-	
+
 	/**
 	 * @brief Get the runtime version information.
-	 * 
+	 *
 	 * This method gets the VolTu version of the runtime shared library, which
 	 * may not be the same as the version of the SDK here.
-	 * 
+	 *
 	 * @see voltu.hpp
-	 * 
+	 *
 	 * @return Always returns semantic versioning structure.
 	 */
 	virtual voltu::Version getVersion() const = 0;
 
 	/**
 	 * @brief Make a virtual room or composite room.
-	 * 
+	 *
 	 * @return A new virtual room instance.
 	 */
 	PY_API virtual voltu::RoomPtr createRoom() = 0;
 
 	/**
 	 * @brief Create a virtual observer.
-	 * 
+	 *
 	 * An observer renderers virtual camera views, audio and other data from
 	 * submitted framesets. It is possible and recommended that a single
 	 * observer instance be used to renderer multiple different views, rather
 	 * than creating lots of observers. This saves memory resources.
-	 * 
+	 *
 	 * @return A new observer instance.
 	 */
 	PY_API virtual voltu::ObserverPtr createObserver() = 0;
 
 	/**
 	 * @brief Open a file, device or network stream using a URI.
-	 * 
+	 *
 	 * All data sources in VolTu are represented by Universal Resource
 	 * Identifiers (URIs), with some non-standard additions. A few examples
 	 * are:
@@ -83,11 +84,11 @@ public:
 	 * * `./file.ftl`
 	 * * `device:camera`
 	 * * `device:screen`
-	 * 
+	 *
 	 * Note that returning from this call does not guarantee that the source
 	 * is fully open and operational, this depends on network handshakes or
 	 * file processing that occurs asynchronously.
-	 * 
+	 *
 	 * @throw voltu::exceptions::BadSourceURI If an unrecognised URI is given.
 	 * @return A feed management object for the data source.
 	 */
@@ -95,14 +96,14 @@ public:
 
 	/**
 	 * @brief Get a list of all available rooms.
-	 * 
+	 *
 	 * A room is a 3D captured physical space, or a combination of such spaces,
 	 * and is represented by a unique identifier within the local system. This
 	 * method obtains a list of all available rooms from all sources. To obtain
 	 * rooms, either use `open` or `createRoom`.
-	 * 
+	 *
 	 * @return A list of room ids, which can be empty.
-	 * 
+	 *
 	 * @see getRoom
 	 * @see open
 	 */
@@ -110,13 +111,13 @@ public:
 
 	/**
 	 * @brief Get a room instance from identifier.
-	 * 
+	 *
 	 * A room instance enables access to all data frames associated with that
 	 * room, including image data. Calling `getRoom` with the same ID
 	 * multiple times will return different smart pointers to room instances
 	 * but provides access to the same data regardless and is valid. An invalid
 	 * room ID will throw an exception.
-	 * 
+	 *
 	 * @throw voltu::exceptions::InvalidRoomId If the ID does not exist.
 	 * @return Room instance or accessing room data.
 	 */
@@ -127,14 +128,14 @@ public:
 
 	/**
 	 * @brief Make an empty operator pipeline for frame processing.
-	 * 
+	 *
 	 * A pipeline allows a sequence of processing operations to be applied to
 	 * a data frame. These operations include stereo correspondence, fusion,
 	 * data evaluation and various image processing operations. Only some
 	 * of these operators are exposed in the SDK. Once a pipeline instance
 	 * is obtained, you can add specific operators to it, configure them and
 	 * then submit frames from processing.
-	 * 
+	 *
 	 * @return A unique pipeline instance.
 	 */
 	PY_API virtual voltu::PipelinePtr createPipeline() = 0;
diff --git a/SDK/CPP/public/include/voltu/types/errors.hpp b/SDK/CPP/public/include/voltu/types/errors.hpp
index b1e77accc50f9975698db4f61be11a5dd7b1c330..b42964ef8002437e90b2352c9b46d35610faedec 100644
--- a/SDK/CPP/public/include/voltu/types/errors.hpp
+++ b/SDK/CPP/public/include/voltu/types/errors.hpp
@@ -47,6 +47,7 @@ VOLTU_EXCEPTION(NotImplemented, Exception, "Functionality not implemented");
 VOLTU_EXCEPTION(ReadOnly, Exception, "Read only, write not allowed");
 VOLTU_EXCEPTION(WriteOnly, Exception, "Write only, read not allowed");
 VOLTU_EXCEPTION(IncompatibleOperation, Exception, "The input data and operator are incompatible");
+VOLTU_EXCEPTION(Timeout, Exception, "Request timed out");
 
 }
 }
diff --git a/SDK/CPP/public/python/CMakeLists.txt b/SDK/CPP/public/python/CMakeLists.txt
index 40bd39c8b388c667af935563da800a62910f0deb..b00857e3722b06fe467f35fdd8d90dbcffe460a3 100644
--- a/SDK/CPP/public/python/CMakeLists.txt
+++ b/SDK/CPP/public/python/CMakeLists.txt
@@ -1,29 +1,29 @@
 set(SDK_AUTO_HEADERS
-    voltu/types/channel.hpp
-    voltu/types/frame.hpp
-    voltu/types/image.hpp
-    voltu/types/intrinsics.hpp
-    voltu/observer.hpp
-    voltu/feed.hpp
-    voltu/initialise.hpp
-    voltu/room.hpp
-    voltu/source.hpp
-    voltu/system.hpp
+	voltu/types/channel.hpp
+	voltu/types/frame.hpp
+	voltu/types/image.hpp
+	voltu/types/intrinsics.hpp
+	voltu/observer.hpp
+	voltu/feed.hpp
+	voltu/initialise.hpp
+	voltu/room.hpp
+	voltu/source.hpp
+	voltu/system.hpp
 )
 
 add_custom_command(
-    OUTPUT automatic_bindings.cpp
-    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen.py
-            automatic_bindings.cpp
-            ${CMAKE_CURRENT_SOURCE_DIR}/../include
-            ${SDK_AUTO_HEADERS}
+	OUTPUT automatic_bindings.cpp
+	COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen.py
+			automatic_bindings.cpp
+			${CMAKE_CURRENT_SOURCE_DIR}/../include
+			${SDK_AUTO_HEADERS}
 
-    DEPENDS voltu_sdk
+	DEPENDS voltu_sdk gen.py
 )
 
 pybind11_add_module(voltu_sdk_py MODULE
-    automatic_bindings.cpp
-    module.cpp
+	automatic_bindings.cpp
+	module.cpp
 )
 
 target_include_directories(voltu_sdk_py PUBLIC include)
@@ -31,14 +31,3 @@ target_include_directories(voltu_sdk_py PRIVATE .)
 
 target_link_libraries(voltu_sdk_py PUBLIC voltu_sdk)
 set_target_properties(voltu_sdk_py PROPERTIES OUTPUT_NAME voltu)
-
-enable_testing()
-find_package(Python3 COMPONENTS Interpreter)
-
-function(add_python_test TEST_NAME TEST_SCRIPT)
-    add_test(NAME ${TEST_NAME}
-        COMMAND Python3::Interpreter -m unittest ${TEST_SCRIPT}
-        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
-endfunction()
-
-add_python_test(test_load tests/test_load.py)
diff --git a/SDK/CPP/public/python/README.md b/SDK/CPP/public/python/README.md
index a5cd30d6d96bab1733841acf5bca73947d6655f0..076ec86e3fca27d3778ba90522dc39cead275fba 100644
--- a/SDK/CPP/public/python/README.md
+++ b/SDK/CPP/public/python/README.md
@@ -1,7 +1,8 @@
 # Python API generator
 
 Dependencies
- * python3-ply (build only)
+ * python3-dev
+ * python3-ply
 
 Build system uses Pybind11 to generate a python module. Most of the bindings are
 automatically generated by gen.py script which is automatically called by CMake
@@ -14,11 +15,13 @@ build. Several (empty) macros are used in headers to annoate Python API details.
 
  * PY_API function/method is to be included in Python API
  * PY_NO_SHARED_PTR shared_ptr<> is not used with instances of this class.
-   https://pybind11.readthedocs.io/en/latest/advanced/smart_ptrs.html?#std-shared-ptr
+   See [pybind11 documentation](https://pybind11.readthedocs.io/en/latest/advanced/smart_ptrs.html?#std-shared-ptr)
+   for techncal details. Shared pointers are not used for structs.
  * PY_RV_LIFETIME_PARENT lifetime of method's return valued is tied to
-   lifetime of parent objects (this). (return_value_policy::reference_internal
+   lifetime of parent objects (this). ([return_value_policy::reference_internal](https://pybind11.readthedocs.io/en/latest/advanced/functions.html#return-value-policies)
    is set for this method)
-   https://pybind11.readthedocs.io/en/latest/advanced/functions.html#return-value-policies
+ * PY_SINGLETON Singleton class, methods are exported to to module scope.
+ * PY_SINGLETON_OBJECT Singleton instance is accessible as module attribute.
 
 ## Notes:
  * Binding to default constructor is generated for structs. Class constructors
@@ -28,6 +31,10 @@ build. Several (empty) macros are used in headers to annoate Python API details.
  * Default arguments are supported (extracted from header).
  * Public class properties are available in python, read-only if const,
    otherwise read write.
+ * Singletons have to be created in PyModule constructor and be available
+   as class members.
+ * Exceptions have to be included manually in module.cpp
+ * C++ preprocessor is not used
 
 ## Not supported (yet) in automatic binding generation:
  * Nested classes
@@ -35,3 +42,4 @@ build. Several (empty) macros are used in headers to annoate Python API details.
  * Constructors
  * Automatic documentation (Doxygen)
  * Generator does not verify that shared_ptr<> is used consistently/correctly
+ * Member variables of singleton classes
diff --git a/SDK/CPP/public/python/automatic_bindings.cpp.in b/SDK/CPP/public/python/automatic_bindings.cpp.in
deleted file mode 100644
index 291de8913a266dd029a6e7ccbc4e7f3796ed82d3..0000000000000000000000000000000000000000
--- a/SDK/CPP/public/python/automatic_bindings.cpp.in
+++ /dev/null
@@ -1,15 +0,0 @@
-{includes}
-
-#include <pybind11/pybind11.h>
-#include <pybind11/stl.h>
-#include <pybind11/eigen.h>
-
-#include "types/image.hpp"
-
-namespace py = pybind11;
-
-using namespace voltu;
-
-void py_automatic_bindings(py::module& m) {{
-    {code}
-}}
diff --git a/SDK/CPP/public/python/examples/example1.py b/SDK/CPP/public/python/examples/example1.py
index 1cc1a26681470b9212662da2959224daf35bf89c..e124328892a0164790e029c0f447f8df58d0ba60 100644
--- a/SDK/CPP/public/python/examples/example1.py
+++ b/SDK/CPP/public/python/examples/example1.py
@@ -3,7 +3,6 @@
 
 import cv2
 import voltu
-import time
 import sys
 import os
 
@@ -11,23 +10,15 @@ if len(sys.argv) != 2:
 	print("%s filename" % os.path.basename(__file__))
 	exit(1)
 
-#if not os.path.exists(sys.argv[1]):
-#	print("can't find %s" % sys.argv[1])
-#	exit(1)
-
-api = voltu.instance()
+api = voltu.System
 api.open(sys.argv[1])
 room = api.getRoom(0)
 
 while True:
-	try:
-		room.waitNextFrame(1000)
+	if room.waitNextFrame(1000):
 		frames = room.getFrame().getImageSet(voltu.Channel.kColour)
 		im = frames[0].getHost()
 		cv2.imshow("im", im)
 
-		if cv2.waitKey(10) == 27:
-			break
-
-	except Exception as e:
-		print(e)
+	if cv2.waitKey(10) == 27:
+		break
diff --git a/SDK/CPP/public/python/examples/example2.py b/SDK/CPP/public/python/examples/example2.py
index 629fec948de0f93c9d94184e8a79033afb211896..8b8bfcf777d558ef8e267ce4a6685af564d15b22 100644
--- a/SDK/CPP/public/python/examples/example2.py
+++ b/SDK/CPP/public/python/examples/example2.py
@@ -3,7 +3,6 @@
 
 import cv2
 import voltu
-import time
 import sys
 import os
 
@@ -11,25 +10,17 @@ if len(sys.argv) != 2:
 	print("%s filename" % os.path.basename(__file__))
 	exit(1)
 
-#if not os.path.exists(sys.argv[1]):
-#	print("can't find %s" % sys.argv[1])
-#	exit(1)
-
-api = voltu.instance()
+api = voltu.System
 api.open(sys.argv[1])
 room = api.getRoom(0)
 cam = api.createCamera()
 
 while True:
-	try:
-		room.waitNextFrame(1000)
+	if room.waitNextFrame(1000):
 		cam.submit(room.getFrame())
 		frames = cam.getFrame().getImageSet(voltu.Channel.kColour)
 		im = frames[0].getHost()
 		cv2.imshow("im", im)
 
-		if cv2.waitKey(10) == 27:
-			break
-
-	except Exception as e:
-		print(e)
+	if cv2.waitKey(10) == 27:
+		break
diff --git a/SDK/CPP/public/python/gen.py b/SDK/CPP/public/python/gen.py
index 00e8252c7efea6975acc331c5076453aa5c3695b..c284c737e5077419463bb498cf7a82bc25cb303f 100755
--- a/SDK/CPP/public/python/gen.py
+++ b/SDK/CPP/public/python/gen.py
@@ -1,5 +1,30 @@
 #!/usr/bin/env python3
 
+"""
+Python binding generator. Creates pybind11 bindings for given headers.
+See README.md for details.
+"""
+
+template = """ /* This file was automatically generated by gen.py. */
+
+{includes}
+
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include <pybind11/eigen.h>
+
+#include "types/image.hpp"
+#include "module.hpp"
+
+namespace py = pybind11;
+
+using namespace voltu;
+
+void PyModule::py_automatic_bindings(py::module& m) {{
+    {code}
+}}
+"""
+
 import sys
 import os
 
@@ -12,23 +37,25 @@ def read_line(file, lineno):
             if i == lineno:
                 return line
 
-
 def get_loc_msg(data):
     return "({0}:{1})".format(data["filename"], data["line_number"])
 
 def print_warn(msg, loc=None):
     if loc is not None:
         msg += " " + get_loc_msg(loc)
-    print("WARNING: %s" % msg, file=sys.stderr)
+    print("gen.py warning: %s" % msg, file=sys.stderr)
 
 def print_err(msg, loc=None):
     if loc is not None:
         msg += " " + get_loc_msg(loc)
-    print("ERROR: %s" % msg, file=sys.stderr)
+    print("gen.py error: %s" % msg, file=sys.stderr)
 
 def include_in_api(data):
     return "PY_API" in data["debug"]
 
+def no_shared_ptr_cls(clsdata):
+    return (clsdata["declaration_method"] == "struct") or ("PY_NO_SHARED_PTR" in clsdata["debug"])
+
 def create_enum_bindigs(enum, parent=[], pybind_handle="", export_values=False):
     name_full = parent + [enum["name"]]
     name_py = enum["name"]
@@ -49,23 +76,14 @@ def create_enum_bindigs(enum, parent=[], pybind_handle="", export_values=False):
 
     return "\n\t".join(cpp)
 
-def create_function_bindings(func, parent=[], pybind_handle=None):
-    func_name = func["name"]
-    full_name = parent + [func_name]
-    full_name_cpp = "::".join(full_name)
-
-    if "PY_API" not in func["debug"]:
-        print_err("%s not included in Python API" % full_name_cpp, func)
-        raise ValueError("No PY_API")
-
-    args = ["\"{0}\"".format(func_name), "&{0}".format(full_name_cpp)]
+def process_func_args(func):
+    args = []
 
     for param in func["parameters"]:
         param_name = param["name"]
 
-        if  param_name == "&":
-            print_warn("Argument name missing for %s" % full_name_cpp, func)
-
+        if  param_name in ["&", "", "*"]:
+            print_warn("Argument name missing for %s" % func["name"])
             continue
 
         if "default" in param:
@@ -74,9 +92,50 @@ def create_function_bindings(func, parent=[], pybind_handle=None):
         else:
             args.append("py::arg(\"{0}\")".format(param_name))
 
+    return args
+
+def wrap_lambda(func, instance_ptr, capture=True):
+    """ Wrap func to instance (singleton) with C++ lambda """
+    args_lambda = []
+    args_lambda_sig = []
+    for i, param in enumerate(func["parameters"]):
+        argname = "p" + str(i)
+        args_lambda_sig.append("%s %s" % (param["type"], argname))
+        args_lambda.append(argname)
+
+    return "[%s](%s){ return %s->%s(%s); }" % (
+        ("&" if capture else ""),
+        ", ".join(args_lambda_sig),
+        instance_ptr,
+        func["name"],
+        ", ".join(args_lambda))
+
+def create_function_bindings(func, parent=[], pybind_handle=None, bind_to_singleton=None):
+    """ Create function bindings, if bind_to_singleton is set, C++ lamda is
+    generated to wrap the call to instance (named by bind_by_singleton).
+    """
+
+    func_name = func["name"]
+    full_name = parent + [func_name]
+    full_name_cpp = "::".join(full_name)
+
+    if not include_in_api(func):
+        print_err("%s not included in Python API" % full_name_cpp, func)
+        raise ValueError("No PY_API")
+
+    args = ["\"{0}\"".format(func_name)]
+
+    if bind_to_singleton is None:
+        args.append("&{0}".format(full_name_cpp))
+
+    else:
+        args.append(wrap_lambda(func, bind_to_singleton))
+
+    args += process_func_args(func)
+
     if "PY_RV_LIFETIME_PARENT" in func["debug"]:
-        if func["parent"] is None:
-            print_err("PY_RV_LIFETIME_PARENT used for function", func)
+        if func["parent"] is None or bind_to_singleton is not None:
+            print_err("PY_RV_LIFETIME_PARENT used for function or singleton", func)
             raise ValueError()
 
         args.append("py::return_value_policy::reference_internal")
@@ -87,25 +146,28 @@ def create_function_bindings(func, parent=[], pybind_handle=None):
 
     return cpp
 
-def create_class_bindings(cls, parent=[], pybind_handle=""):
-    cls_name = cls["name"]
-    full_name = parent + [cls_name]
+def create_class_bindings(clsdata, parent=[], pybind_handle=""):
+    """ Create bindings for a class """
+
+    clsdata_name = clsdata["name"]
+    full_name = parent + [clsdata_name]
     cpp = []
 
-    if "PY_NO_SHARED_PTR" not in cls["debug"]:
-        cls_cpp = "py::class_<{name}, std::shared_ptr<{name}>>({handle}, \"{name}\")"
-    else:
+    if no_shared_ptr_cls(clsdata):
         cls_cpp = "py::class_<{name}>({handle}, \"{name}\")"
-    cpp.append(cls_cpp.format(handle=pybind_handle, name=cls_name))
+    else:
+        cls_cpp = "py::class_<{name}, std::shared_ptr<{name}>>({handle}, \"{name}\")"
+
+    cpp.append(cls_cpp.format(handle=pybind_handle, name=clsdata_name))
 
-    if cls["declaration_method"] == "struct":
+    if clsdata["declaration_method"] == "struct":
         cpp.append(".def(py::init<>())")
 
-    for method in cls["methods"]["public"]:
+    for method in clsdata["methods"]["public"]:
         if include_in_api(method):
             cpp.append("." + create_function_bindings(method, full_name))
 
-    for field in cls["properties"]["public"]:
+    for field in clsdata["properties"]["public"]:
         if field["constant"]:
             field_cpp = ".def_property_readonly(\"{name}\", &{cpp_name})"
         else:
@@ -116,8 +178,30 @@ def create_class_bindings(cls, parent=[], pybind_handle=""):
 
     return "\n\t\t".join(cpp)
 
-if __name__ == "__main__":
+def create_singleton_bindings(instance_ptr, clsdata, parent=[], pybind_handle="", export=False):
+    """ Singleton class bindings, either creates a singleton instance or
+    exports functions directly to module. Singleton pointer available with
+    instance_ptr, which should be class member of PyModule.
+    """
+    if not export:
+        # use usual bindings and export as attribute to pybind handle
+        return "\n\t".join([
+            create_class_bindings(clsdata, parent, pybind_handle) + ";",
+            "{0}.attr(\"{1}\") = {2};".format(pybind_handle, clsdata["name"], instance_ptr)
+        ])
+
+    else:
+        # use C++ lambdas to wrap all methods
+        cpp = []
+        for func in clsdata["methods"]["public"]:
+            if not include_in_api(func):
+                continue
+
+            cpp.append(create_function_bindings(func, parent, pybind_handle, instance_ptr) + ";")
 
+        return "\n\t".join(cpp)
+
+if __name__ == "__main__":
     from pprint import pprint
 
     if (len(sys.argv) < 4):
@@ -131,6 +215,7 @@ if __name__ == "__main__":
 
     out = []
     includes = []
+
     for fname in fsin:
         includes.append(fname)
 
@@ -145,7 +230,13 @@ if __name__ == "__main__":
             ns = data["namespace"]
             # workaround: parser does not save debug in same way as for funcs
             data["debug"] = read_line(data["filename"], data["line_number"])
-            out.append(create_class_bindings(data, [ns], handle) + ";")
+
+            if "PY_SINGLETON_OBJECT" in data["debug"]:
+                out.append(create_singleton_bindings("instance_" + data["name"], data, [ns], handle, False))
+            elif "PY_SINGLETON" in data["debug"]:
+                out.append(create_singleton_bindings("instance_" + data["name"], data, [ns], handle, True))
+            else:
+                out.append(create_class_bindings(data, [ns], handle) + ";")
 
         for data in hdr.functions:
             ns = data["namespace"].strip("::") # bug? in parser
@@ -153,9 +244,7 @@ if __name__ == "__main__":
                 out.append(create_function_bindings(data, [ns], handle) + ";")
 
     includes = "\n".join("#include <{0}>".format(i) for i in includes)
-    template_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "automatic_bindings.cpp.in")
-    with open(template_file, "r") as f:
-        template = f.read()
+
     out = template.format(includes=includes, code="\n\t".join(out))
     with open(fout, "w",) as f:
         f.write(out)
diff --git a/SDK/CPP/public/python/module.cpp b/SDK/CPP/public/python/module.cpp
index 5ba4e9e2a48f24cbe34353796bf23728f2f93656..636e7b809fcb6757776ef711dd837eb330130c9f 100644
--- a/SDK/CPP/public/python/module.cpp
+++ b/SDK/CPP/public/python/module.cpp
@@ -6,7 +6,11 @@
 
 namespace py = pybind11;
 
-void py_exceptions(pybind11::module& m) {
+PyModule::PyModule() {
+    instance_System = voltu::instance();
+}
+
+void PyModule::py_exceptions(pybind11::module& m) {
 	py::register_exception<voltu::exceptions::Exception>(m, "Error");
 	py::register_exception<voltu::exceptions::BadImageChannel>(m, "ErrorBadImageChannel");
 	py::register_exception<voltu::exceptions::NoFrame>(m, "ErrorNoFrame");
@@ -14,7 +18,11 @@ void py_exceptions(pybind11::module& m) {
 	py::register_exception<voltu::exceptions::BadSourceURI>(m, "ErrorBadSourceURI");
 }
 
+static auto module = PyModule();
+
 PYBIND11_MODULE(voltu, m) {
-	py_exceptions(m);
-	py_automatic_bindings(m);
+	m.attr("version") = py::make_tuple(VOLTU_VERSION_MAJOR, VOLTU_VERSION_MINOR, VOLTU_VERSION_PATCH);
+
+	module.py_exceptions(m);
+	module.py_automatic_bindings(m);
 }
diff --git a/SDK/CPP/public/python/module.hpp b/SDK/CPP/public/python/module.hpp
index b605cf7f6b7030701858cf8d4cf2ffbba95636bf..f59e4d6280356357b56d4d2db928653aadd4a973 100644
--- a/SDK/CPP/public/python/module.hpp
+++ b/SDK/CPP/public/python/module.hpp
@@ -1,5 +1,15 @@
 #pragma once
 
 #include <pybind11/pybind11.h>
+#include <voltu/voltu.hpp>
+#include <voltu/system.hpp>
 
-void py_automatic_bindings(pybind11::module& m);
+class PyModule {
+public:
+    PyModule();
+    void py_automatic_bindings(pybind11::module& m);
+    void py_exceptions(pybind11::module& m);
+
+private:
+    std::shared_ptr<voltu::System> instance_System;
+};
diff --git a/SDK/CPP/public/python/tests/test_load.py b/SDK/CPP/public/python/tests/test_load.py
deleted file mode 100644
index 8e5131d4d4299af3ac116f3585de9403b6ee2f9d..0000000000000000000000000000000000000000
--- a/SDK/CPP/public/python/tests/test_load.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import unittest
-
-class LoadLibrary(unittest.TestCase):
-
-    def test_get_instance(self):
-        import voltu
-        self.assertIsNotNone(voltu.instance())
-        # second call to instance() returns None
-        #self.assertIsNotNone(voltu.instance())
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/SDK/CPP/public/voltu.cpp b/SDK/CPP/public/voltu.cpp
index bf63ed6b4c1f145f1a36547030e97c898359f78f..2c4d19712428be34af7d8fd04c93a7c615fdec7b 100644
--- a/SDK/CPP/public/voltu.cpp
+++ b/SDK/CPP/public/voltu.cpp
@@ -10,10 +10,17 @@
 
 #if defined(WIN32)
 #include <windows.h>
+#pragma comment(lib, "User32.lib")
 #else
 #include <dlfcn.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
 #endif
 
+#include <cstdlib>
+#include <iostream>
+
 static bool g_init = false;
 
 typedef void* Library;
@@ -48,13 +55,50 @@ static void unloadLibrary(Library lib)
 #endif
 }
 
+static bool is_file(const std::string &path)
+{
+#ifdef WIN32
+	WIN32_FIND_DATA ffd;
+	HANDLE hFind = FindFirstFile(path.c_str(), &ffd);
+
+	if (hFind == INVALID_HANDLE_VALUE) return false;
+	FindClose(hFind);
+	return true;
+#else
+	struct stat s;
+	if (::stat(path.c_str(), &s) == 0)
+	{
+		return true;
+	}
+	else
+	{
+		return false;
+	}
+#endif
+}
+
 static std::string locateLibrary()
 {
 	// TODO: Use full paths and find correct versions
 #if defined(WIN32)
 	return "voltu.dll";
 #else
-	return "libvoltu.so";
+	std::string name = "libvoltu.so";
+	std::string vname = name + std::string(".") + std::to_string(VOLTU_VERSION_MAJOR) + std::string(".") + std::to_string(VOLTU_VERSION_MINOR);
+
+	if (const char* env_p = std::getenv("VOLTU_RUNTIME"))
+	{
+		if (is_file(env_p)) return env_p;
+	}
+
+	// FIXME: Should eventually not have these...
+	if (is_file(std::string("./") + vname)) return std::string("./") + vname;
+	//else if (is_file(std::string("./") + name)) return std::string("./") + name;
+	else if (is_file(std::string("../") + vname)) return std::string("../") + vname;
+	/*else if (is_file(std::string("../") + name)) return std::string("../") + name;
+	else if (is_file(std::string("/usr/local/lib/") + vname)) return std::string("/usr/local/lib/") + vname;
+	else if (is_file(std::string("/usr/local/lib/") + name)) return std::string("/usr/local/lib/") + name;*/
+	return vname;
 #endif
 }
 
@@ -63,6 +107,7 @@ std::shared_ptr<voltu::System> voltu::instance()
 	if (g_init) return nullptr;
 	
 	std::string name = locateLibrary();
+	std::cout << "Loading VolTu Runtime: " << name << std::endl;
 	Library handle = loadLibrary(name.c_str());
 
 	if (handle)
diff --git a/SDK/CPP/tests/CMakeLists.txt b/SDK/CPP/tests/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..489f6e3380a20d296427308c61f667d5f5c35936
--- /dev/null
+++ b/SDK/CPP/tests/CMakeLists.txt
@@ -0,0 +1,15 @@
+find_package(Python3 COMPONENTS Interpreter)
+
+function(add_python_test TEST_NAME TEST_SCRIPT)
+	add_test(NAME ${TEST_NAME}
+		COMMAND Python3::Interpreter -B -m unittest ${TEST_SCRIPT}
+		WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+	# binary module directory to PYTHONPATH, additional variables have to be
+	# separated by semicolon
+	set_tests_properties(${TEST_NAME} PROPERTIES ENVIRONMENT
+		"PYTHONPATH=${SDK_BINARY_DIR}/python;LD_LIBRARY_PATH=${CMAKE_CURRENT_BINARY_DIR}/..")
+	set_property(TEST ${TEST_NAME} APPEND PROPERTY DEPENDS voltu_sdk)
+endfunction()
+
+add_python_test(Py_TestLoad test_load.py)
diff --git a/SDK/CPP/public/python/tests/__init__.py b/SDK/CPP/tests/__init__.py
similarity index 100%
rename from SDK/CPP/public/python/tests/__init__.py
rename to SDK/CPP/tests/__init__.py
diff --git a/SDK/CPP/tests/test_load.py b/SDK/CPP/tests/test_load.py
new file mode 100644
index 0000000000000000000000000000000000000000..df4292cc6af08fcced3017415ed8fe097ee3e3ab
--- /dev/null
+++ b/SDK/CPP/tests/test_load.py
@@ -0,0 +1,21 @@
+import unittest
+import os
+
+class LoadLibrary(unittest.TestCase):
+
+    def test_import(self):
+        import voltu
+
+    def test_import_twice(self):
+        # verify that System instance is created just once, even if module
+        # imported multiple times
+        import voltu
+        import voltu
+        self.assertIsNotNone(voltu.System)
+
+    def test_version(self):
+        import voltu
+        major, minor, patch = voltu.version
+
+if __name__ == '__main__':
+    unittest.main()