diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3d0318ec6922368e283d32e94f6473e7633f73f4..3d371af7b18bc5ad7d0b994465696c2bb1204987 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -65,7 +65,7 @@ windows-vision:
     - master
   stage: all
   variables:
-    CMAKE_ARGS: '-DWITH_OPTFLOW=TRUE -DBUILD_VISION=TRUE -DBUILD_CALIBRATION=FALSE -DBUILDRECONSTRUCT=FALSE -DBUILDRENDERER=FALSE -DBUILD_TESTING=FALSE'
+    CMAKE_ARGS: '-DWITH_OPTFLOW=TRUE -DBUILD_VISION=TRUE -DBUILD_CALIBRATION=FALSE -DBUILDRECONSTRUCT=FALSE -DBUILDRENDERER=FALSE -DBUILD_TESTING=FALSE -DBUILD_TESTS=FALSE'
     DEPLOY_DIR: 'D:/Shared/AutoDeploy'
   tags:
     - win
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5b08d4d0cd9c034e7fe40ac701053a9b1d72097e..2f6d5b6d5e4db2bd15396934089e88847a0b61ac 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,6 +21,7 @@ option(BUILD_RENDERER "Enable the renderer component" ON)
 option(BUILD_GUI "Enable the GUI" ON)
 option(BUILD_CALIBRATION "Enable the calibration component" OFF)
 option(BUILD_TOOLS "Compile developer and research tools" ON)
+option(BUILD_TESTS "Compile all unit and integration tests" ON)
 
 set(THREADS_PREFER_PTHREAD_FLAG ON)
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
@@ -33,6 +34,12 @@ find_package( URIParser REQUIRED )
 find_package( MsgPack REQUIRED )
 find_package( Eigen3 REQUIRED )
 
+find_program(CCACHE_PROGRAM ccache)
+if(CCACHE_PROGRAM)
+	set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
+	message(STATUS "Found ccache: ${CCACHE_PROGRAM}")
+endif()
+
 # find_package( ffmpeg )
 
 if (WITH_OPTFLOW)
diff --git a/applications/gui/src/camera.cpp b/applications/gui/src/camera.cpp
index 4a30668cc5afdf116869ff589ef635b765384d81..4488fbeafad03b7ee5802de88905850585acd1d2 100644
--- a/applications/gui/src/camera.cpp
+++ b/applications/gui/src/camera.cpp
@@ -5,6 +5,9 @@
 
 #include <ftl/operators/antialiasing.hpp>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 #include <fstream>
 
 #ifdef HAVE_OPENVR
diff --git a/applications/gui/src/config_window.cpp b/applications/gui/src/config_window.cpp
index b3c5ef874961d773f12981c0eee93fd8fd67fa3c..1f8e3a31aebb53350c15e65d3894186a5fb01803 100644
--- a/applications/gui/src/config_window.cpp
+++ b/applications/gui/src/config_window.cpp
@@ -132,7 +132,7 @@ ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 		itembutton->setTooltip(c);
 		itembutton->setBackgroundColor(nanogui::Color(0.9f,0.9f,0.9f,0.9f));
 		itembutton->setCallback([this,c]() {
-			LOG(INFO) << "Change configurable: " << c;
+			//LOG(INFO) << "Change configurable: " << c;
 			_buildForm(c);
 			setVisible(false);
 			//this->parent()->removeChild(this);
@@ -160,7 +160,7 @@ void ConfigWindow::_addElements(nanogui::FormHelper *form, const std::string &su
 		if (i.key() == "$id") continue;
 
 		if (i.key() == "$ref" && i.value().is_string()) {
-			LOG(INFO) << "Follow $ref: " << i.value();
+			//LOG(INFO) << "Follow $ref: " << i.value();
 			const std::string suri = std::string(i.value().get<string>());
 			_addElements(form, suri);
 			continue;
diff --git a/applications/gui/src/ctrl_window.cpp b/applications/gui/src/ctrl_window.cpp
index 3681022bd10c832ea2d722b1eb0bd02877b1af4a..37d311964e22ba800dddeada0cca1512c709c1f9 100644
--- a/applications/gui/src/ctrl_window.cpp
+++ b/applications/gui/src/ctrl_window.cpp
@@ -51,7 +51,7 @@ ControlWindow::ControlWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
 	new Label(this, "Select Node","sans-bold");
 	auto select = new ComboBox(this, node_titles_);
 	select->setCallback([this](int ix) {
-		LOG(INFO) << "Change node: " << ix;
+		//LOG(INFO) << "Change node: " << ix;
 		_changeActive(ix);
 	});
 
diff --git a/applications/gui/src/src_window.cpp b/applications/gui/src/src_window.cpp
index fe63a858b4cb6f105dffa82a6637bd1c5f608d82..fbb38e1d9e33fb513610475f1869bdaf1224159a 100644
--- a/applications/gui/src/src_window.cpp
+++ b/applications/gui/src/src_window.cpp
@@ -15,6 +15,9 @@
 #include <nanogui/layout.h>
 #include <nanogui/vscrollpanel.h>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 #include <ftl/streams/netstream.hpp>
 
 #include "ftl/operators/colours.hpp"
diff --git a/applications/reconstruct/src/reconstruction.cpp b/applications/reconstruct/src/reconstruction.cpp
index 5fab0496bb89bd059700f6da411b5ba3c11a42ef..e4e448793677b44c116fb308ded8a510c551a3e6 100644
--- a/applications/reconstruct/src/reconstruction.cpp
+++ b/applications/reconstruct/src/reconstruction.cpp
@@ -66,7 +66,7 @@ ftl::rgbd::FrameState &Reconstruction::state(size_t ix) {
 	if (ix < fs_align_.frames.size()) {
 		return *fs_align_.frames[ix].origin();
 	}
-	throw ftl::exception("State index out-of-bounds");
+	throw FTL_Error("State index out-of-bounds");
 }
 
 void Reconstruction::onFrameSet(const ftl::rgbd::VideoCallback &cb) {
@@ -78,7 +78,7 @@ bool Reconstruction::post(ftl::rgbd::FrameSet &fs) {
 		
 	{
 		UNIQUE_LOCK(exchange_mtx_, lk);
-		if (new_frame_ == true) LOG(WARNING) << "Frame lost";
+		//if (new_frame_ == true) LOG(WARNING) << "Frame lost";
 		fs.swapTo(fs_align_);
 		new_frame_ = true;
 	}
@@ -102,7 +102,7 @@ bool Reconstruction::post(ftl::rgbd::FrameSet &fs) {
 
 void Reconstruction::setGenerator(ftl::rgbd::Generator *gen) {
 	if (gen_) {
-		throw ftl::exception("Reconstruction already has generator");
+		throw FTL_Error("Reconstruction already has generator");
 	}
 
 	gen_ = gen;
diff --git a/components/audio/include/ftl/audio/frame.hpp b/components/audio/include/ftl/audio/frame.hpp
index efd3338494c264048264af881161baa4edee496c..845123a8ffbab470998bfc657164c6c451cfdb1e 100644
--- a/components/audio/include/ftl/audio/frame.hpp
+++ b/components/audio/include/ftl/audio/frame.hpp
@@ -17,17 +17,17 @@ struct AudioSettings {
 struct AudioData {
 	template <typename T>
 	const T &as() const {
-		throw ftl::exception("Type not valid for audio channel");
+		throw FTL_Error("Type not valid for audio channel");
 	}
 
 	template <typename T>
 	T &as() {
-		throw ftl::exception("Type not valid for audio channel");
+		throw FTL_Error("Type not valid for audio channel");
 	}
 
 	template <typename T>
 	T &make() {
-		throw ftl::exception("Type not valid for audio channel");
+		throw FTL_Error("Type not valid for audio channel");
 	}
 
 	inline void reset() {}
diff --git a/components/audio/src/source.cpp b/components/audio/src/source.cpp
index f43caeba2afb49c0e47de5575ca6f892b6d7bb2a..cec0c2d982987ea62bb6dca0acfe6406bed294b9 100644
--- a/components/audio/src/source.cpp
+++ b/components/audio/src/source.cpp
@@ -2,6 +2,9 @@
 #include <ftl/audio/audio.hpp>
 #include <ftl/audio/portaudio.hpp>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 using ftl::audio::Source;
 using ftl::audio::Frame;
 using ftl::audio::FrameSet;
@@ -176,7 +179,7 @@ size_t Source::size() {
 }
 
 ftl::audio::FrameState &Source::state(size_t ix) {
-    if (ix >= 1) throw ftl::exception("State index out-of-bounds");
+    if (ix >= 1) throw FTL_Error("State index out-of-bounds");
     return state_;
 }
 
diff --git a/components/audio/src/speaker.cpp b/components/audio/src/speaker.cpp
index 4670ca3852fa3aa237ba16ff102f6e93e2953ba3..41836b83796718f12351603fdf75faff741e2f6f 100644
--- a/components/audio/src/speaker.cpp
+++ b/components/audio/src/speaker.cpp
@@ -2,6 +2,9 @@
 #include <ftl/audio/audio.hpp>
 #include <ftl/audio/portaudio.hpp>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 using ftl::audio::Speaker;
 using ftl::audio::Frame;
 using ftl::audio::FrameSet;
diff --git a/components/codecs/CMakeLists.txt b/components/codecs/CMakeLists.txt
index 31496df7de4f0febad42dff9a03e382d1a3b8b3b..4334e6dbea04ef9d244b0bc91c21a930ee415ec5 100644
--- a/components/codecs/CMakeLists.txt
+++ b/components/codecs/CMakeLists.txt
@@ -1,19 +1,44 @@
-set(CODECSRC
+add_library(BaseCodec OBJECT
 	src/bitrates.cpp
 	src/encoder.cpp
 	src/decoder.cpp
-	src/opencv_encoder.cpp
-	src/opencv_decoder.cpp
 	src/generate.cpp
 	src/writer.cpp
 	src/reader.cpp
 	src/channels.cpp
 	src/depth_convert.cu
 )
+target_include_directories(BaseCodec PUBLIC
+	${CMAKE_CURRENT_SOURCE_DIR}/include
+	$<TARGET_PROPERTY:ftlcommon,INTERFACE_INCLUDE_DIRECTORIES>
+	$<TARGET_PROPERTY:nvpipe,INTERFACE_INCLUDE_DIRECTORIES>
+)
+
+add_library(OpenCVCodec OBJECT	
+	src/opencv_encoder.cpp
+	src/opencv_decoder.cpp
+)
+target_include_directories(OpenCVCodec PUBLIC
+	${CMAKE_CURRENT_SOURCE_DIR}/include
+	$<TARGET_PROPERTY:ftlcommon,INTERFACE_INCLUDE_DIRECTORIES>
+)
+
+set(CODECSRC
+$<TARGET_OBJECTS:BaseCodec>
+$<TARGET_OBJECTS:OpenCVCodec>
+)
 
 if (HAVE_NVPIPE)
-	list(APPEND CODECSRC src/nvpipe_encoder.cpp)
-	list(APPEND CODECSRC src/nvpipe_decoder.cpp)
+	add_library(NvPipeCodec OBJECT	
+		src/nvpipe_encoder.cpp
+		src/nvpipe_decoder.cpp
+	)
+	target_include_directories(NvPipeCodec PUBLIC
+		${CMAKE_CURRENT_SOURCE_DIR}/include
+		$<TARGET_PROPERTY:ftlcommon,INTERFACE_INCLUDE_DIRECTORIES>
+		$<TARGET_PROPERTY:nvpipe,INTERFACE_INCLUDE_DIRECTORIES>
+	)
+	list(APPEND CODECSRC $<TARGET_OBJECTS:NvPipeCodec>)
 endif()
 
 add_library(ftlcodecs ${CODECSRC})
@@ -26,5 +51,7 @@ target_include_directories(ftlcodecs PUBLIC
 #target_include_directories(cv-node PUBLIC ${PROJECT_SOURCE_DIR}/include)
 target_link_libraries(ftlcodecs ftlcommon ${OpenCV_LIBS} ${CUDA_LIBRARIES} Eigen3::Eigen nvpipe)
 
+if (BUILD_TESTS)
 add_subdirectory(test)
+endif()
 
diff --git a/components/codecs/test/CMakeLists.txt b/components/codecs/test/CMakeLists.txt
index 63d40a0ea26b09429f98343bbb99de2cf5fcb44e..ea703c7a66ab90b88418e2ad70a48c060d405239 100644
--- a/components/codecs/test/CMakeLists.txt
+++ b/components/codecs/test/CMakeLists.txt
@@ -1,6 +1,6 @@
 ### OpenCV Codec Unit ################################################################
 add_executable(opencv_codec_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	../src/bitrates.cpp
 	../src/encoder.cpp
 	../src/opencv_encoder.cpp
@@ -17,7 +17,7 @@ add_test(OpenCVCodecUnitTest opencv_codec_unit)
 
 ### NvPipe Codec Unit ################################################################
 add_executable(nvpipe_codec_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	../src/bitrates.cpp
 	../src/encoder.cpp
 	../src/nvpipe_encoder.cpp
@@ -48,7 +48,7 @@ add_test(NvPipeCodecUnitTest nvpipe_codec_unit)
 
 ### Channel Unit ###############################################################
 add_executable(channel_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	./channel_unit.cpp
 )
 target_include_directories(channel_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
diff --git a/components/common/cpp/CMakeLists.txt b/components/common/cpp/CMakeLists.txt
index e2f1e70fa304a62b1946972604ac86ffb6565d3d..81816912e9758bb57ffbdae16da3ee8a1d5d0af0 100644
--- a/components/common/cpp/CMakeLists.txt
+++ b/components/common/cpp/CMakeLists.txt
@@ -1,9 +1,12 @@
+add_library(Loguru OBJECT src/loguru.cpp)
+target_include_directories(Loguru PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
+
 set(COMMONSRC
 	src/config.cpp
 	src/uri.cpp
 	src/configuration.cpp
 	src/configurable.cpp
-	src/loguru.cpp
+	$<TARGET_OBJECTS:Loguru>
 	src/cuda_common.cpp
 	src/ctpl_stl.cpp
 	src/timer.cpp
@@ -21,5 +24,7 @@ target_include_directories(ftlcommon PUBLIC
 	PRIVATE src)
 target_link_libraries(ftlcommon Threads::Threads ${OS_LIBS} ${OpenCV_LIBS} ${URIPARSER_LIBRARIES} ${CUDA_LIBRARIES})
 
+if (BUILD_TESTS)
 add_subdirectory(test)
+endif()
 
diff --git a/components/common/cpp/include/ftl/configurable.hpp b/components/common/cpp/include/ftl/configurable.hpp
index 61dca8ea3b883e97f2e26c2f1a5126c92f8aa520..75f3cdc82fdc994506636b554b479409696531ea 100644
--- a/components/common/cpp/include/ftl/configurable.hpp
+++ b/components/common/cpp/include/ftl/configurable.hpp
@@ -2,8 +2,9 @@
 #ifndef _FTL_CONFIGURABLE_HPP_
 #define _FTL_CONFIGURABLE_HPP_
 
-#define LOGURU_REPLACE_GLOG 1
-#include <loguru.hpp>
+//#define LOGURU_REPLACE_GLOG 1
+//#include <loguru.hpp>
+#include <ftl/exception.hpp>
 #include <nlohmann/json.hpp>
 #include <string>
 #include <tuple>
@@ -131,7 +132,7 @@ void Configurable::set<const std::string&>(const std::string &name, const std::s
 
 template <typename T>
 std::optional<T> ftl::Configurable::get(const std::string &name) {
-	if (!config_->is_object() && !config_->is_null()) LOG(FATAL) << "Config is not an object";
+	if (!config_->is_object() && !config_->is_null()) throw FTL_Error("Config is not an object");
 	if (!(*config_)[name].is_null()) {
 		try {
 			return (*config_)[name].get<T>();
@@ -144,13 +145,12 @@ std::optional<T> ftl::Configurable::get(const std::string &name) {
 		std::string res_uri = (*config_)["$ref"].get<std::string>()+"/"+name;
 		auto &r = ftl::config::resolve(res_uri);
 
-		DLOG(2) << "GET: " << res_uri << " = " << r;
+		//DLOG(2) << "GET: " << res_uri << " = " << r;
 
 		try {
 			return r.get<T>();
 		} catch (...) {
-			LOG(ERROR) << "Missing: " << (*config_)["$id"].get<std::string>()+"/"+name;
-			return {};
+			throw FTL_Error("Missing: " << (*config_)["$id"].get<std::string>()+"/"+name);
 		}
 	} else {
 		return {};
diff --git a/components/common/cpp/include/ftl/configuration.hpp b/components/common/cpp/include/ftl/configuration.hpp
index ed7eee95d0e0692242ca5dafd22ef142890561f5..d4a4e1a47d374fb8bd1194755cbc756b80036ebd 100644
--- a/components/common/cpp/include/ftl/configuration.hpp
+++ b/components/common/cpp/include/ftl/configuration.hpp
@@ -2,8 +2,8 @@
 #ifndef _FTL_COMMON_CONFIGURATION_HPP_
 #define _FTL_COMMON_CONFIGURATION_HPP_
 
-#define LOGURU_REPLACE_GLOG 1
-#include <loguru.hpp>
+//#define LOGURU_REPLACE_GLOG 1
+//#include <loguru.hpp>
 #include <nlohmann/json.hpp>
 //#include <ftl/configurable.hpp>
 #include <string>
@@ -139,21 +139,20 @@ T *ftl::config::create(json_t &link, ARGS ...args) {
 	//auto &r = link; // = ftl::config::resolve(link);
 
 	if (!link["$id"].is_string()) {
-		LOG(FATAL) << "Entity does not have $id or parent: " << link;
-		return nullptr;
+		throw FTL_Error("Entity does not have $id or parent: " << link);
 	}
 
 	ftl::Configurable *cfg = ftl::config::find(link["$id"].get<std::string>());
 	if (!cfg) {
-		try {
+		//try {
 			cfg = new T(link, args...);
-		} catch (std::exception &ex) {	
-			LOG(ERROR) << ex.what();
-			LOG(FATAL) << "Could not construct " << link;
-		} catch(...) {
-			LOG(ERROR) << "Unknown exception";
-			LOG(FATAL) << "Could not construct " << link;
-		}
+		//} catch (std::exception &ex) {	
+		//	LOG(ERROR) << ex.what();
+		//	LOG(FATAL) << "Could not construct " << link;
+		//} catch(...) {
+		//	LOG(ERROR) << "Unknown exception";
+		//	LOG(FATAL) << "Could not construct " << link;
+		//}
 	} else {
 		// Make sure configurable has newest object pointer
 		cfg->patchPtr(link);
@@ -162,8 +161,8 @@ T *ftl::config::create(json_t &link, ARGS ...args) {
 	try {
 		return dynamic_cast<T*>(cfg);
 	} catch(...) {
-		LOG(FATAL) << "Configuration URI object is of wrong type: " << link;
-		return nullptr;
+		throw FTL_Error("Configuration URI object is of wrong type: " << link.dump());
+		//return nullptr;
 	}
 }
 
@@ -201,8 +200,8 @@ T *ftl::config::create(ftl::Configurable *parent, const std::string &name, ARGS
 		return create<T>(entity2, args...);
 	}
 
-	LOG(ERROR) << "Unable to create Configurable entity '" << name << "'";
-	return nullptr;
+	throw FTL_Error("Unable to create Configurable entity '" << name << "'");
+	//return nullptr;
 }
 
 template <typename T, typename... ARGS>
@@ -246,7 +245,7 @@ std::vector<T*> ftl::config::createArray(ftl::Configurable *parent, const std::s
 			i++;
 		}
 	} else {
-		LOG(WARNING) << "Expected an array for '" << name << "' in " << parent->getID();
+		//LOG(WARNING) << "Expected an array for '" << name << "' in " << parent->getID();
 	}
 
 	return result;
diff --git a/components/common/cpp/include/ftl/cuda_common.hpp b/components/common/cpp/include/ftl/cuda_common.hpp
index 576b383a97f9a63b00cdbfbabedd32bec0270912..a7041360925d36bd94355554edd5c1b64fa9b30a 100644
--- a/components/common/cpp/include/ftl/cuda_common.hpp
+++ b/components/common/cpp/include/ftl/cuda_common.hpp
@@ -11,7 +11,6 @@
 #include <opencv2/core/cuda/common.hpp>
 
 #ifndef __CUDACC__
-#include <loguru.hpp>
 #include <exception>
 #endif
 
@@ -150,7 +149,7 @@ class TextureObject : public TextureObjectBase {
 template <typename T>
 TextureObject<T> &TextureObject<T>::cast(TextureObjectBase &b) {
 	if (b.cvType() != ftl::traits::OpenCVType<T>::value) {
-		LOG(ERROR) << "Bad cast of texture object";
+		//LOG(ERROR) << "Bad cast of texture object";
 		throw std::bad_cast();
 	}
 	return reinterpret_cast<TextureObject<T>&>(b);
@@ -162,7 +161,7 @@ TextureObject<T> &TextureObject<T>::cast(TextureObjectBase &b) {
 template <typename T>
 TextureObject<T>::TextureObject(const cv::cuda::GpuMat &d, bool interpolated) {
 	// GpuMat must have correct data type
-	CHECK(d.type() == ftl::traits::OpenCVType<T>::value);
+	//CHECK(d.type() == ftl::traits::OpenCVType<T>::value);
 
 	cudaResourceDesc resDesc;
 	memset(&resDesc, 0, sizeof(resDesc));
diff --git a/components/common/cpp/include/ftl/exception.hpp b/components/common/cpp/include/ftl/exception.hpp
index 921e53493eda023a35af6f4c53b43ca2e26125d9..de21aea545c08dbb74f85add523a292db4d036c7 100644
--- a/components/common/cpp/include/ftl/exception.hpp
+++ b/components/common/cpp/include/ftl/exception.hpp
@@ -47,4 +47,6 @@ class exception : public std::exception
 };
 }
 
+#define FTL_Error(A) (ftl::exception(ftl::Formatter() << __FILE__ << ":" << __LINE__ << ": " << A))
+
 #endif  // _FTL_EXCEPTION_HPP_
diff --git a/components/common/cpp/src/configurable.cpp b/components/common/cpp/src/configurable.cpp
index 8186713b11025f1799afbf26ca8f56532f1deb3f..fdbd9470329b01990180c5d854f6d2f0521e0a8d 100644
--- a/components/common/cpp/src/configurable.cpp
+++ b/components/common/cpp/src/configurable.cpp
@@ -1,3 +1,5 @@
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
 #include <ftl/configurable.hpp>
 
 using ftl::Configurable;
diff --git a/components/common/cpp/src/cuda_common.cpp b/components/common/cpp/src/cuda_common.cpp
index 01b0bb346e113d3d3b3be52dec21f5a8ffdfd81a..1fb7b68e5e0a9c3d07250d40dc65c42e4d2a2de5 100644
--- a/components/common/cpp/src/cuda_common.cpp
+++ b/components/common/cpp/src/cuda_common.cpp
@@ -1,3 +1,5 @@
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
 #include <ftl/cuda_common.hpp>
 
 using ftl::cuda::TextureObjectBase;
diff --git a/components/common/cpp/test/CMakeLists.txt b/components/common/cpp/test/CMakeLists.txt
index 1b8dfc7ba3b3c3c8c5fbd915ee87cf84024f37c4..3ef38991a6f9599783bb036f0a570f8bf0348b26 100644
--- a/components/common/cpp/test/CMakeLists.txt
+++ b/components/common/cpp/test/CMakeLists.txt
@@ -1,48 +1,37 @@
+add_library(CatchTest OBJECT ./tests.cpp)
+
 ### Configurable Unit ################################################################
 add_executable(configurable_unit
-	./tests.cpp
-	../src/configurable.cpp
-	../src/uri.cpp
-	../src/config.cpp
-	../src/configuration.cpp
-	../src/loguru.cpp
-	../src/ctpl_stl.cpp
-	../src/cuda_common.cpp
+	$<TARGET_OBJECTS:CatchTest>
 	./configurable_unit.cpp
 )
 target_include_directories(configurable_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
-target_link_libraries(configurable_unit
+target_link_libraries(configurable_unit ftlcommon
 	${URIPARSER_LIBRARIES}
 	Threads::Threads ${OS_LIBS} ${OpenCV_LIBS} ${CUDA_LIBRARIES})
 
 ### URI ########################################################################
 add_executable(uri_unit
-	./tests.cpp
-	../src/uri.cpp
-	../src/loguru.cpp
+	$<TARGET_OBJECTS:CatchTest>
 	./uri_unit.cpp)
 target_include_directories(uri_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
-target_link_libraries(uri_unit
+target_link_libraries(uri_unit ftlcommon
 	Threads::Threads ${OS_LIBS}
 	${URIPARSER_LIBRARIES})
 
 ### Timer Unit ################################################################
 add_executable(timer_unit
-	./tests.cpp
-	../src/timer.cpp
-	../src/config.cpp
-	../src/loguru.cpp
-	../src/ctpl_stl.cpp
+	$<TARGET_OBJECTS:CatchTest>
 	./timer_unit.cpp
 )
 target_include_directories(timer_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
-target_link_libraries(timer_unit
+target_link_libraries(timer_unit ftlcommon
 	Threads::Threads ${OS_LIBS})
 
 ### URI ########################################################################
 add_executable(msgpack_unit
-	./tests.cpp
-	../src/loguru.cpp
+	$<TARGET_OBJECTS:CatchTest>
+	$<TARGET_OBJECTS:Loguru>
 	./msgpack_unit.cpp)
 target_include_directories(msgpack_unit PUBLIC ${OpenCV_INCLUDE_DIRS} "${CMAKE_CURRENT_SOURCE_DIR}/../include")
 target_link_libraries(msgpack_unit Threads::Threads ${OS_LIBS} ${OpenCV_LIBS})
diff --git a/components/common/cpp/test/configurable_unit.cpp b/components/common/cpp/test/configurable_unit.cpp
index af44e026a552279da53986dde38079e7695a889b..a52548bec9c8e529ca0a66c81d1e5590dc2e46ff 100644
--- a/components/common/cpp/test/configurable_unit.cpp
+++ b/components/common/cpp/test/configurable_unit.cpp
@@ -6,11 +6,6 @@
 using ftl::Configurable;
 using std::string;
 
-namespace ftl {
-namespace timer {
-void setInterval(int i) {}
-}
-}
 
 SCENARIO( "Configurable::get()" ) {
 	GIVEN( "a non-existent property" ) {
diff --git a/components/common/cpp/test/timer_unit.cpp b/components/common/cpp/test/timer_unit.cpp
index 6cdea157e9228b920ce2220c0122c8dcad9cf76e..2fdc700345a2700c8cc63a992d4ba7ad6c284a0b 100644
--- a/components/common/cpp/test/timer_unit.cpp
+++ b/components/common/cpp/test/timer_unit.cpp
@@ -4,11 +4,11 @@
 #include <ftl/timer.hpp>
 #include <ftl/threads.hpp>
 
-ctpl::thread_pool ftl::pool(4);
+//ctpl::thread_pool ftl::pool(4);
 
-namespace ftl {
+/*namespace ftl {
 	bool running = true;
-}
+}*/
 
 TEST_CASE( "Timer::add() High Precision Accuracy" ) {
 	SECTION( "An instantly returning callback" ) {
diff --git a/components/control/cpp/include/ftl/master.hpp b/components/control/cpp/include/ftl/master.hpp
index c4782d16ea09928df9d8d3ca5273d84f5a86cf1c..fd173cdc08ecb5ebf1b8005dc5830d543e6640f3 100644
--- a/components/control/cpp/include/ftl/master.hpp
+++ b/components/control/cpp/include/ftl/master.hpp
@@ -74,7 +74,7 @@ class Master {
 	/**
 	 * Do not call! Automatically called from logging subsystem.
 	 */
-	void sendLog(const loguru::Message& message);
+	//void sendLog(const loguru::Message& message);
 
 	bool isPaused() const { return state_.paused; }
 
diff --git a/components/control/cpp/src/master.cpp b/components/control/cpp/src/master.cpp
index 4c1354bee685758eb40eeed9b76730dfe905f03f..9a3ee7f54d34d79964a1e073b2befbbedd259edf 100644
--- a/components/control/cpp/src/master.cpp
+++ b/components/control/cpp/src/master.cpp
@@ -1,5 +1,7 @@
 #include <ftl/master.hpp>
 #include <ftl/net_configurable.hpp>
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
 
 using ftl::ctrl::Master;
 using ftl::net::Universe;
@@ -217,7 +219,7 @@ void Master::stop() {
 	net_->unbind("log");
 }
 
-void Master::sendLog(const loguru::Message& message) {
+/*void Master::sendLog(const loguru::Message& message) {
 	UNIQUE_LOCK(mutex_, lk);
 	if (in_log_) return;
 	in_log_ = true;
@@ -233,4 +235,4 @@ void Master::sendLog(const loguru::Message& message) {
 	}
 
 	in_log_ = false;
-}
\ No newline at end of file
+}*/
\ No newline at end of file
diff --git a/components/net/cpp/CMakeLists.txt b/components/net/cpp/CMakeLists.txt
index 9df16c0b27dda27cc00b75c33c340f1474afd63d..c765fa53877c3e29b2bcde904cccda75487f35ea 100644
--- a/components/net/cpp/CMakeLists.txt
+++ b/components/net/cpp/CMakeLists.txt
@@ -24,8 +24,7 @@ install(TARGETS ftlnet EXPORT ftlnet-config
 	RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
 install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
 
-add_executable(net-cli src/main.cpp)
-target_link_libraries(net-cli ftlnet ftlcommon glog::glog Threads::Threads ${READLINE_LIBRARY} ${UUID_LIBRARIES})
-add_dependencies(net-cli ftlnet)
 
-ADD_SUBDIRECTORY(test)
+if (BUILD_TESTS)
+add_subdirectory(test)
+endif()
diff --git a/components/net/cpp/include/ftl/net/peer.hpp b/components/net/cpp/include/ftl/net/peer.hpp
index 9978da92202d5b457109a195d688421ac350634c..95eccc01428335d3058b4f9e1ea4cda6cb038691 100644
--- a/components/net/cpp/include/ftl/net/peer.hpp
+++ b/components/net/cpp/include/ftl/net/peer.hpp
@@ -11,7 +11,6 @@
 #include <ftl/exception.hpp>
 
 //#define GLOG_NO_ABBREVIATED_SEVERITIES
-#include <loguru.hpp>
 #include <ftl/net/protocol.hpp>
 #include <ftl/net/dispatcher.hpp>
 #include <ftl/uri.hpp>
@@ -347,8 +346,7 @@ R Peer::call(const std::string &name, ARGS... args) {
 	
 	if (!hasreturned) {
 		cancelCall(id);
-		LOG(ERROR) << "RPC Timeout: " << name;
-		throw ftl::exception("RPC failed with timeout");
+		throw FTL_Error("RPC failed with timeout: " << name);
 	}
 	
 	return result;
@@ -371,7 +369,7 @@ int Peer::asyncCall(
 		callbacks_[rpcid] = std::make_unique<caller<T>>(cb);
 	}
 
-	DLOG(INFO) << "RPC " << name << "(" << rpcid << ") -> " << uri_;
+	//DLOG(INFO) << "RPC " << name << "(" << rpcid << ") -> " << uri_;
 
 	auto call_obj = std::make_tuple(0,rpcid,name,args_obj);
 	
diff --git a/components/net/cpp/include/ftl/net/universe.hpp b/components/net/cpp/include/ftl/net/universe.hpp
index 9bd17cc4edce1542bc3b14b424949b0a0a8e8e5a..737955b537da34bdf41f060edefa5d08a8f114e1 100644
--- a/components/net/cpp/include/ftl/net/universe.hpp
+++ b/components/net/cpp/include/ftl/net/universe.hpp
@@ -383,9 +383,8 @@ template <typename R, typename... ARGS>
 R Universe::call(const ftl::UUID &pid, const std::string &name, ARGS... args) {
 	Peer *p = getPeer(pid);
 	if (p == nullptr || !p->isConnected()) {
-		if (p == nullptr) DLOG(WARNING) << "Attempting to call an unknown peer : " << pid.to_string();
-		else DLOG(WARNING) << "Attempting to call an disconnected peer : " << pid.to_string();
-		throw ftl::exception("Calling disconnected peer");
+		if (p == nullptr) throw FTL_Error("Attempting to call an unknown peer : " << pid.to_string());
+		else throw FTL_Error("Attempting to call an disconnected peer : " << pid.to_string());
 	}
 	return p->call<R>(name, args...);
 }
@@ -394,7 +393,7 @@ template <typename... ARGS>
 bool Universe::send(const ftl::UUID &pid, const std::string &name, ARGS... args) {
 	Peer *p = getPeer(pid);
 	if (p == nullptr) {
-		DLOG(WARNING) << "Attempting to call an unknown peer : " << pid.to_string();
+		//DLOG(WARNING) << "Attempting to call an unknown peer : " << pid.to_string();
 		return false;
 	}
 #ifdef WIN32
@@ -409,7 +408,7 @@ template <typename... ARGS>
 int Universe::try_send(const ftl::UUID &pid, const std::string &name, ARGS... args) {
 	Peer *p = getPeer(pid);
 	if (p == nullptr) {
-		DLOG(WARNING) << "Attempting to call an unknown peer : " << pid.to_string();
+		//DLOG(WARNING) << "Attempting to call an unknown peer : " << pid.to_string();
 		return false;
 	}
 
diff --git a/components/net/cpp/src/dispatcher.cpp b/components/net/cpp/src/dispatcher.cpp
index eac33f5e134c0ab38194b3726e2036840867eab2..b1a5b265e8a00b3defabb929426c70366f5d191f 100644
--- a/components/net/cpp/src/dispatcher.cpp
+++ b/components/net/cpp/src/dispatcher.cpp
@@ -147,16 +147,14 @@ void ftl::net::Dispatcher::dispatch_notification(Peer &s, msgpack::object const
 void ftl::net::Dispatcher::enforce_arg_count(std::string const &func, std::size_t found,
                                    std::size_t expected) {
     if (found != expected) {
-    	LOG(FATAL) << "RPC argument missmatch for '" << func << "' - " << found << " != " << expected;
-        throw ftl::exception("RPC argument missmatch");
+    	throw FTL_Error("RPC argument missmatch for '" << func << "' - " << found << " != " << expected);
     }
 }
 
 void ftl::net::Dispatcher::enforce_unique_name(std::string const &func) {
     auto pos = funcs_.find(func);
     if (pos != end(funcs_)) {
-    	LOG(FATAL) << "RPC non unique binding for '" << func << "'";
-        throw ftl::exception("RPC binding not unique");
+    	throw FTL_Error("RPC non unique binding for '" << func << "'");
     }
 }
 
diff --git a/components/net/cpp/src/net_internal.hpp b/components/net/cpp/src/net_internal.hpp
index f8586ea774a9c5b79e9c9a7513e992554c793611..77aab1ecab35c3dd9e38c634e96c273c020e27c4 100644
--- a/components/net/cpp/src/net_internal.hpp
+++ b/components/net/cpp/src/net_internal.hpp
@@ -2,8 +2,8 @@
 #define _FTL_NET_INTERNAL_HPP_
 
 #if defined _DEBUG && DEBUG_NET
-#include <loguru.hpp>
-#include <chrono>
+//#include <loguru.hpp>
+//#include <chrono>
 #endif
 
 namespace ftl { namespace net { namespace internal {
@@ -27,9 +27,9 @@ namespace ftl { namespace net { namespace internal {
 	inline ssize_t writev(int sd, const struct iovec *v, int cnt) {
 		auto start = std::chrono::high_resolution_clock::now();
 		return ::writev(sd,v,cnt);
-		std::chrono::duration<double> elapsed =
+		/*std::chrono::duration<double> elapsed =
 				std::chrono::high_resolution_clock::now() - start;
-		LOG(INFO) << "WRITEV in " << elapsed.count() << "s";
+		LOG(INFO) << "WRITEV in " << elapsed.count() << "s";*/
 	}
 #else
 	inline ssize_t recv(int sd, void *buf, size_t n, int f) { return ::recv(sd,buf,n,f); }
diff --git a/components/net/cpp/src/universe.cpp b/components/net/cpp/src/universe.cpp
index 29cf1254f9a4926190d1753eb4ecc0836dfcbb99..c279cad20458b051640d89dd5c11304128f3c06a 100644
--- a/components/net/cpp/src/universe.cpp
+++ b/components/net/cpp/src/universe.cpp
@@ -1,6 +1,8 @@
 #include <ftl/net/universe.hpp>
 #include <ftl/timer.hpp>
 #include <chrono>
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
 
 #ifdef WIN32
 #include <Ws2tcpip.h>
diff --git a/components/net/cpp/test/CMakeLists.txt b/components/net/cpp/test/CMakeLists.txt
index c786f73d8f8d73a534cae849a3ff9d80331f3267..30d0f17879568e2d16554a79c4fc63f790715e15 100644
--- a/components/net/cpp/test/CMakeLists.txt
+++ b/components/net/cpp/test/CMakeLists.txt
@@ -1,6 +1,6 @@
 ### Socket Unit ################################################################
 add_executable(peer_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	../src/ws_internal.cpp
 	../src/dispatcher.cpp
 	./peer_unit.cpp
@@ -15,7 +15,7 @@ target_link_libraries(peer_unit
 
 ### Net Integration ############################################################
 add_executable(net_integration
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	../../../common/cpp/src/config.cpp
 	./net_integration.cpp)
 add_dependencies(net_integration ftlnet)
@@ -29,7 +29,7 @@ target_link_libraries(net_integration
 
 ### NetConfigurable Unit #######################################################
 add_executable(net_configurable_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	./net_configurable_unit.cpp)
 target_link_libraries(net_configurable_unit
 	ftlnet)
diff --git a/components/operators/src/depth.cpp b/components/operators/src/depth.cpp
index c8a48df6399e45370737320eb49ddd49f6bff177..0a93061ed650800ea9fc6cd69917a689ff27d15a 100644
--- a/components/operators/src/depth.cpp
+++ b/components/operators/src/depth.cpp
@@ -97,11 +97,9 @@ bool DepthBilateralFilter::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out,
 									 cudaStream_t stream) {
 
 	if (!in.hasChannel(Channel::Colour)) {
-		LOG(ERROR) << "Joint Bilateral Filter is missing Colour";
-		return false;
+		throw FTL_Error("Joint Bilateral Filter is missing Colour");
 	} else if (!in.hasChannel(Channel::Depth)) {
-		LOG(ERROR) << "Joint Bilateral Filter is missing Depth";
-		return false;
+		throw FTL_Error("Joint Bilateral Filter is missing Depth");
 	}
 
 	auto cvstream = cv::cuda::StreamAccessor::wrapStream(stream);
diff --git a/components/operators/src/disparity/bilateral_filter.cpp b/components/operators/src/disparity/bilateral_filter.cpp
index 0c766596e1307417c23c51602edc72ec954adda3..cc0285ecef06d3ea534aba5e6990c191cb04286d 100644
--- a/components/operators/src/disparity/bilateral_filter.cpp
+++ b/components/operators/src/disparity/bilateral_filter.cpp
@@ -23,7 +23,7 @@ bool DisparityBilateralFilter::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out
 									 cudaStream_t stream) {
 
 	if (!in.hasChannel(Channel::Colour)) {
-		LOG(ERROR) << "Joint Bilateral Filter is missing Colour";
+		throw FTL_Error("Joint Bilateral Filter is missing Colour");
 		return false;
 	} else if (!in.hasChannel(Channel::Disparity)) {
 		// Have depth, so calculate disparity...
@@ -35,11 +35,11 @@ bool DisparityBilateralFilter::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out
 			GpuMat &disp = out.create<GpuMat>(Channel::Disparity);
 			disp.create(depth.size(), CV_32FC1);
 
-			LOG(ERROR) << "Calculated disparity from depth";
+			//LOG(ERROR) << "Calculated disparity from depth";
 
 			ftl::cuda::depth_to_disparity(disp, depth, params, stream);
 		} else {
-			LOG(ERROR) << "Joint Bilateral Filter is missing Disparity and Depth";
+			throw FTL_Error("Joint Bilateral Filter is missing Disparity and Depth");
 			return false;
 		}
 	}
diff --git a/components/operators/src/disparity/disparity_to_depth.cpp b/components/operators/src/disparity/disparity_to_depth.cpp
index 18a284915f449c34b5b2d9547ed483ef57c9e4dd..f9ae8b12774ca528cef322b1446a66c75aaef7c1 100644
--- a/components/operators/src/disparity/disparity_to_depth.cpp
+++ b/components/operators/src/disparity/disparity_to_depth.cpp
@@ -10,8 +10,7 @@ bool DisparityToDepth::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out,
 							cudaStream_t stream) {
 	
 	if (!in.hasChannel(Channel::Disparity)) {
-		LOG(ERROR) << "Missing disparity before convert to depth";
-		return false;
+		throw FTL_Error("Missing disparity before convert to depth");
 	}
 
 	const auto params = in.getLeftCamera();
diff --git a/components/operators/src/mvmls.cpp b/components/operators/src/mvmls.cpp
index 52bc22a02693ee4cb549be6617c9b5a4f0f02bf0..305bdd98f539cacb39ce063b6f7d3ff1e65af99e 100644
--- a/components/operators/src/mvmls.cpp
+++ b/components/operators/src/mvmls.cpp
@@ -59,12 +59,10 @@ bool MultiViewMLS::apply(ftl::rgbd::FrameSet &in, ftl::rgbd::FrameSet &out, cuda
         contributions_[i].create(size.width, size.height);
 
         if (!f.hasChannel(Channel::Normals)) {
-            LOG(ERROR) << "Required normals channel missing for MLS";
-            return false;
+            throw FTL_Error("Required normals channel missing for MLS");
         }
         if (!f.hasChannel(Channel::Support2)) {
-            LOG(ERROR) << "Required cross support channel missing for MLS";
-            return false;
+            throw FTL_Error("Required cross support channel missing for MLS");
         }
 
         // Create required channels
diff --git a/components/operators/src/normals.cpp b/components/operators/src/normals.cpp
index 8e2ab7d3f52b0b3a69d895c211f7bd90780c76fc..0551a1e4ecb686d6b47305e9c081718680e374a7 100644
--- a/components/operators/src/normals.cpp
+++ b/components/operators/src/normals.cpp
@@ -17,8 +17,7 @@ Normals::~Normals() {
 
 bool Normals::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out, cudaStream_t stream) {
 	if (!in.hasChannel(Channel::Depth)) {
-		LOG(ERROR) << "Missing depth channel in Normals operator";
-		return false;
+		throw FTL_Error("Missing depth channel in Normals operator");
 	}
 
 	if (out.hasChannel(Channel::Normals)) {
@@ -49,8 +48,7 @@ bool SmoothNormals::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out, cudaStrea
     int radius = max(0, min(config()->value("radius",1), 5));
 
 	if (!in.hasChannel(Channel::Depth)) {
-		LOG(ERROR) << "Missing depth channel in SmoothNormals operator";
-		return false;
+		throw FTL_Error("Missing depth channel in SmoothNormals operator");
 	}
 
 	if (out.hasChannel(Channel::Normals)) {
diff --git a/components/operators/src/operator.cpp b/components/operators/src/operator.cpp
index 8dbbf168c0bcce749dbcbd119dbec9282fb3d51e..0ca639e1e9d1f58ca644b01252bf0205414a7d57 100644
--- a/components/operators/src/operator.cpp
+++ b/components/operators/src/operator.cpp
@@ -1,5 +1,8 @@
 #include <ftl/operators/operator.hpp>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 using ftl::operators::Operator;
 using ftl::operators::Graph;
 using ftl::rgbd::Frame;
@@ -17,18 +20,15 @@ Operator::Operator(ftl::Configurable *config) : config_(config) {
 Operator::~Operator() {}
 
 bool Operator::apply(Frame &in, Frame &out, cudaStream_t stream) {
-	LOG(ERROR) << "Operation application to frame not supported";
-	return false;
+	throw FTL_Error("Operation application to frame not supported");
 }
 
 bool Operator::apply(FrameSet &in, FrameSet &out, cudaStream_t stream) {
-	LOG(ERROR) << "Operation application to frameset not supported";
-	return false;
+	throw FTL_Error("Operation application to frameset not supported");
 }
 
 bool Operator::apply(FrameSet &in, Frame &out, cudaStream_t stream) {
-	LOG(ERROR) << "Operation application as a reduction not supported";
-	return false;
+	throw FTL_Error("Operation application as a reduction not supported");
 }
 
 
diff --git a/components/operators/src/smoothing.cpp b/components/operators/src/smoothing.cpp
index ef9cb58cb31f55fdf1f06843e681a3ed1bb42677..3f68620727550ef00b637a49963d8c3a51043a47 100644
--- a/components/operators/src/smoothing.cpp
+++ b/components/operators/src/smoothing.cpp
@@ -1,6 +1,9 @@
 #include <ftl/operators/smoothing.hpp>
 #include "smoothing_cuda.hpp"
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 #include <ftl/cuda/normals.hpp>
 
 using ftl::operators::HFSmoother;
diff --git a/components/renderers/cpp/src/tri_render.cpp b/components/renderers/cpp/src/tri_render.cpp
index c9001bd2dd8ec604db9080bf8a45635db1c16970..f221f16ec3e4d505b6b4ba7ab96fd8ca7557237b 100644
--- a/components/renderers/cpp/src/tri_render.cpp
+++ b/components/renderers/cpp/src/tri_render.cpp
@@ -5,6 +5,9 @@
 #include <ftl/cuda/normals.hpp>
 #include <ftl/cuda/mask.hpp>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 #include <opencv2/core/cuda_stream_accessor.hpp>
 
 //#include <ftl/filters/smoothing.hpp>
diff --git a/components/rgbd-sources/CMakeLists.txt b/components/rgbd-sources/CMakeLists.txt
index 92bab62acd2b5a8ce213f49b83251e2f4297304b..4001d5c1cb27c581e90cd2539ea7f5f77cfe7a01 100644
--- a/components/rgbd-sources/CMakeLists.txt
+++ b/components/rgbd-sources/CMakeLists.txt
@@ -43,5 +43,7 @@ endif()
 
 target_link_libraries(ftlrgbd ftlcommon ${OpenCV_LIBS} ${LIBSGM_LIBRARIES} ${CUDA_LIBRARIES} Eigen3::Eigen ${REALSENSE_LIBRARY} ftlnet ${LibArchive_LIBRARIES} ftlcodecs ftloperators ftldata)
 
+if (BUILD_TESTS)
 add_subdirectory(test)
+endif()
 
diff --git a/components/rgbd-sources/include/ftl/rgbd/format.hpp b/components/rgbd-sources/include/ftl/rgbd/format.hpp
index e52a6627fc58d523a1dff7872e64fab03e45c28a..032e2948d9b3456fe641b3fb6e3ee90100cdad06 100644
--- a/components/rgbd-sources/include/ftl/rgbd/format.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/format.hpp
@@ -42,7 +42,7 @@ struct Format : public ftl::rgbd::FormatBase {
 			a.cols,
 			a.rows,
 			ftl::traits::OpenCVType<T>::value) {
-		CHECK(cvType == a.type());
+		if (cvType != a.type()) throw FTL_Error("Type mismatch");
 	}
 };
 
diff --git a/components/rgbd-sources/include/ftl/rgbd/frame.hpp b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
index 65837020fd2a95187623ebb4128b6125812d0d6d..a2e7813bc4b7671ed71723f5a44a086901574484 100644
--- a/components/rgbd-sources/include/ftl/rgbd/frame.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/frame.hpp
@@ -39,17 +39,17 @@ struct VideoData {
 
 	template <typename T>
 	T &as() {
-		throw ftl::exception("Unsupported type for Video data channel");
+		throw FTL_Error("Unsupported type for Video data channel");
 	};
 
 	template <typename T>
 	const T &as() const {
-		throw ftl::exception("Unsupported type for Video data channel");
+		throw FTL_Error("Unsupported type for Video data channel");
 	};
 
 	template <typename T>
 	T &make() {
-		throw ftl::exception("Unsupported type for Video data channel");
+		throw FTL_Error("Unsupported type for Video data channel");
 	};
 
 	inline void reset() {
@@ -194,14 +194,13 @@ template <> cv::cuda::GpuMat &Frame::create(ftl::codecs::Channel c, const ftl::r
 
 template <typename T>
 ftl::cuda::TextureObject<T> &Frame::getTexture(ftl::codecs::Channel c) {
-	if (!hasChannel(c)) throw ftl::exception(ftl::Formatter() << "Texture channel does not exist: " << (int)c);
+	if (!hasChannel(c)) throw FTL_Error("Texture channel does not exist: " << (int)c);
 
 	auto &m = getData(c);
-	if (!m.isgpu) throw ftl::exception("Texture channel is not on GPU");
+	if (!m.isgpu) throw FTL_Error("Texture channel is not on GPU");
 
 	if (m.tex.cvType() != ftl::traits::OpenCVType<T>::value || m.tex.width() != m.gpu.cols || m.tex.height() != m.gpu.rows || m.gpu.type() != m.tex.cvType()) {
-		LOG(ERROR) << "Texture has not been created for channel = " << (int)c;
-		throw ftl::exception("Texture has not been created properly for this channel");
+		throw FTL_Error("Texture has not been created properly for this channel: " << (int)c);
 	}
 
 	return ftl::cuda::TextureObject<T>::cast(m.tex);
@@ -216,14 +215,13 @@ ftl::cuda::TextureObject<T> &Frame::createTexture(ftl::codecs::Channel c, const
 	auto &m = getData(c);
 
 	if (f.empty()) {
-		throw ftl::exception("createTexture needs a non-empty format");
+		throw FTL_Error("createTexture needs a non-empty format");
 	} else {
 		m.gpu.create(f.size(), f.cvType);
 	}
 
 	if (m.gpu.type() != ftl::traits::OpenCVType<T>::value) {
-		LOG(ERROR) << "Texture type mismatch: " << (int)c << " " << m.gpu.type() << " != " << ftl::traits::OpenCVType<T>::value;
-		throw ftl::exception("Texture type does not match underlying data type");
+		throw FTL_Error("Texture type mismatch: " << (int)c << " " << m.gpu.type() << " != " << ftl::traits::OpenCVType<T>::value);
 	}
 
 	// TODO: Check tex cvType
@@ -242,7 +240,7 @@ ftl::cuda::TextureObject<T> &Frame::createTexture(ftl::codecs::Channel c, const
 
 template <typename T>
 ftl::cuda::TextureObject<T> &Frame::createTexture(ftl::codecs::Channel c, bool interpolated) {
-	if (!hasChannel(c)) throw ftl::exception(ftl::Formatter() << "createTexture needs a format if the channel does not exist: " << (int)c);
+	if (!hasChannel(c)) throw FTL_Error("createTexture needs a format if the channel does not exist: " << (int)c);
 
 	auto &m = getData(c);
 
@@ -251,12 +249,11 @@ ftl::cuda::TextureObject<T> &Frame::createTexture(ftl::codecs::Channel c, bool i
 		// TODO: Should this upload to GPU or not?
 		//gpu_ += c;
 	} else if (!m.isgpu || (m.isgpu && m.gpu.empty())) {
-		throw ftl::exception("createTexture needs a format if no memory is allocated");
+		throw FTL_Error("createTexture needs a format if no memory is allocated");
 	}
 
 	if (m.gpu.type() != ftl::traits::OpenCVType<T>::value) {
-		LOG(ERROR) << "Texture type mismatch: " << (int)c << " " << m.gpu.type() << " != " << ftl::traits::OpenCVType<T>::value;
-		throw ftl::exception("Texture type does not match underlying data type");
+		throw FTL_Error("Texture type mismatch: " << (int)c << " " << m.gpu.type() << " != " << ftl::traits::OpenCVType<T>::value);
 	}
 
 	// TODO: Check tex cvType
diff --git a/components/rgbd-sources/include/ftl/rgbd/source.hpp b/components/rgbd-sources/include/ftl/rgbd/source.hpp
index 23fff94401e9f2bb27420cd3373c96781b68592b..d26d8942a36ad2340305918a133a47be0c5b856a 100644
--- a/components/rgbd-sources/include/ftl/rgbd/source.hpp
+++ b/components/rgbd-sources/include/ftl/rgbd/source.hpp
@@ -134,7 +134,7 @@ class Source : public ftl::Configurable {
 	 */
 	const Camera &parameters() const {
 		if (impl_) return impl_->params_;
-		else throw ftl::exception("Cannot get parameters for bad source");
+		else throw FTL_Error("Cannot get parameters for bad source");
 	}
 
 	/**
diff --git a/components/rgbd-sources/src/frame.cpp b/components/rgbd-sources/src/frame.cpp
index 2a9765ceeb81a662924250c8fb65c351cc076fdd..faaf78cf32993431a55b55abfd14416badbdcf91 100644
--- a/components/rgbd-sources/src/frame.cpp
+++ b/components/rgbd-sources/src/frame.cpp
@@ -12,25 +12,25 @@ static cv::cuda::GpuMat noneGPU;
 
 template <>
 cv::Mat &VideoData::as<cv::Mat>() {
-	if (isgpu) throw ftl::exception("Host request for GPU data without download");
+	if (isgpu) throw FTL_Error("Host request for GPU data without download");
 	return host;
 }
 
 template <>
 const cv::Mat &VideoData::as<cv::Mat>() const {
-	if (isgpu) throw ftl::exception("Host request for GPU data without download");
+	if (isgpu) throw FTL_Error("Host request for GPU data without download");
 	return host;
 }
 
 template <>
 cv::cuda::GpuMat &VideoData::as<cv::cuda::GpuMat>() {
-	if (!isgpu) throw ftl::exception("GPU request for Host data without upload");
+	if (!isgpu) throw FTL_Error("GPU request for Host data without upload");
 	return gpu;
 }
 
 template <>
 const cv::cuda::GpuMat &VideoData::as<cv::cuda::GpuMat>() const {
-	if (!isgpu) throw ftl::exception("GPU request for Host data without upload");
+	if (!isgpu) throw FTL_Error("GPU request for Host data without upload");
 	return gpu;
 }
 
@@ -102,13 +102,13 @@ void Frame::pushPacket(ftl::codecs::Channel c, ftl::codecs::Packet &pkt) {
 		auto &m1 = getData(c);
 		m1.encoded.emplace_back() = std::move(pkt);
 	} else {
-		LOG(ERROR) << "Channel " << (int)c << " doesn't exist for packet push";
+		throw FTL_Error("Channel " << (int)c << " doesn't exist for packet push");
 	}
 }
 
 const std::list<ftl::codecs::Packet> &Frame::getPackets(ftl::codecs::Channel c) const {
 	if (!hasChannel(c)) {
-		throw ftl::exception(ftl::Formatter() << "Frame channel does not exist: " << (int)c);
+		throw FTL_Error("Frame channel does not exist: " << (int)c);
 	}
 
 	auto &m1 = getData(c);
@@ -135,184 +135,9 @@ bool Frame::empty(ftl::codecs::Channels<0> channels) {
 	return false;
 }
 
-/*void Frame::swapTo(ftl::codecs::Channels<0> channels, Frame &f) {
-	f.reset();
-	f.origin_ = origin_;
-	f.state_ = state_;
-
-	// For all channels in this frame object
-	for (auto c : channels_) {
-		// Should we swap this channel?
-		if (channels.has(c)) {
-			// Does 'f' have this channel?
-			//if (!f.hasChannel(c)) {
-				// No, so create it first
-				// FIXME: Allocate the memory as well?
-				if (isCPU(c)) f.create<cv::Mat>(c);
-				else f.create<cv::cuda::GpuMat>(c);
-			//}
-
-			auto &m1 = _get(c);
-			auto &m2 = f._get(c);
-
-			cv::swap(m1.host, m2.host);
-			cv::cuda::swap(m1.gpu, m2.gpu);
-
-			auto temptex = std::move(m2.tex);
-			m2.tex = std::move(m1.tex);
-			m1.tex = std::move(temptex);
-
-			if (m2.encoded.size() > 0 || m1.encoded.size() > 0) {
-				auto tempenc = std::move(m2.encoded);
-				m2.encoded = std::move(m1.encoded);
-				m1.encoded = std::move(tempenc);
-			}
-		}
-	}
-
-	f.data_data_ = std::move(data_data_);
-	f.data_channels_ = data_channels_;
-	data_channels_.clear();
-}
-
-void Frame::swapChannels(ftl::codecs::Channel a, ftl::codecs::Channel b) {
-	auto &m1 = _get(a);
-	auto &m2 = _get(b);
-	cv::swap(m1.host, m2.host);
-	cv::cuda::swap(m1.gpu, m2.gpu);
-
-	auto temptex = std::move(m2.tex);
-	m2.tex = std::move(m1.tex);
-	m1.tex = std::move(temptex);
-
-	if (m2.encoded.size() > 0 || m1.encoded.size() > 0) {
-		auto tempenc = std::move(m2.encoded);
-		m2.encoded = std::move(m1.encoded);
-		m1.encoded = std::move(tempenc);
-	}
-}
-
-void Frame::copyTo(ftl::codecs::Channels<0> channels, Frame &f) {
-	f.reset();
-	f.origin_ = origin_;
-	f.state_ = state_;
-
-	// For all channels in this frame object
-	for (auto c : channels_) {
-		// Should we copy this channel?
-		if (channels.has(c)) {
-			if (isCPU(c)) get<cv::Mat>(c).copyTo(f.create<cv::Mat>(c));
-			else get<cv::cuda::GpuMat>(c).copyTo(f.create<cv::cuda::GpuMat>(c));
-			auto &m1 = _get(c);
-			auto &m2 = f._get(c);
-			m2.encoded = m1.encoded; //std::move(m1.encoded);  // TODO: Copy?
-		}
-	}
-
-	f.data_data_ = data_data_;
-	f.data_channels_ = data_channels_;
-}
-
-template<> cv::Mat& Frame::get(ftl::codecs::Channel channel) {
-	if (channel == Channel::None) {
-		DLOG(WARNING) << "Cannot get the None channel from a Frame";
-		none.release();
-		return none;
-	}
-
-	if (isGPU(channel)) {
-		download(Channels(channel));
-		LOG(WARNING) << "Getting GPU channel on CPU without explicit 'download'";
-	}
-
-	// Add channel if not already there
-	if (!channels_.has(channel)) {
-		throw ftl::exception(ftl::Formatter() << "Frame channel does not exist: " << (int)channel);
-	}
-
-	return _get(channel).host;
-}
-
-template<> cv::cuda::GpuMat& Frame::get(ftl::codecs::Channel channel) {
-	if (channel == Channel::None) {
-		DLOG(WARNING) << "Cannot get the None channel from a Frame";
-		noneGPU.release();
-		return noneGPU;
-	}
-
-	if (isCPU(channel)) {
-		upload(Channels(channel));
-		LOG(WARNING) << "Getting CPU channel on GPU without explicit 'upload'";
-	}
-
-	// Add channel if not already there
-	if (!channels_.has(channel)) {
-		throw ftl::exception(ftl::Formatter() << "Frame channel does not exist: " << (int)channel);
-	}
-
-	return _get(channel).gpu;
-}
-
-template<> const cv::Mat& Frame::get(ftl::codecs::Channel channel) const {
-	if (channel == Channel::None) {
-		LOG(FATAL) << "Cannot get the None channel from a Frame";
-	}
-
-	if (isGPU(channel)) {
-		LOG(FATAL) << "Getting GPU channel on CPU without explicit 'download'";
-	}
-
-	if (!channels_.has(channel)) throw ftl::exception(ftl::Formatter() << "Frame channel does not exist: " << (int)channel);
-
-	return _get(channel).host;
-}
-
-template<> const cv::cuda::GpuMat& Frame::get(ftl::codecs::Channel channel) const {
-	if (channel == Channel::None) {
-		LOG(FATAL) << "Cannot get the None channel from a Frame";
-	}
-
-	if (isCPU(channel)) {
-		LOG(FATAL) << "Getting CPU channel on GPU without explicit 'upload'";
-	}
-
-	// Add channel if not already there
-	if (!channels_.has(channel)) {
-		throw ftl::exception(ftl::Formatter() << "Frame channel does not exist: " << (int)channel);
-	}
-
-	return _get(channel).gpu;
-}
-
-template<> const Eigen::Matrix4d& Frame::get(ftl::codecs::Channel channel) const {
-	if (channel == Channel::Pose) {
-		return state_.getPose();
-	}
-
-	throw ftl::exception(ftl::Formatter() << "Invalid pose channel: " << (int)channel);
-}
-
-template<> const ftl::rgbd::Camera& Frame::get(ftl::codecs::Channel channel) const {
-	if (channel == Channel::Calibration) {
-		return state_.getLeft();
-	} else if (channel == Channel::Calibration2) {
-		return state_.getRight();
-	}
-
-	throw ftl::exception(ftl::Formatter() << "Invalid calibration channel: " << (int)channel);
-}
-
-template<> const nlohmann::json& Frame::get(ftl::codecs::Channel channel) const {
-	if (channel == Channel::Configuration) {
-		return state_.getConfig();
-	}
-
-	throw ftl::exception(ftl::Formatter() << "Invalid configuration channel: " << (int)channel);
-}*/
-
 template <> cv::Mat &Frame::create(ftl::codecs::Channel c, const ftl::rgbd::FormatBase &f) {
 	if (c == Channel::None) {
-		throw ftl::exception("Cannot create a None channel");
+		throw FTL_Error("Cannot create a None channel");
 	}
 	
 	create<cv::Mat>(c);
@@ -329,7 +154,7 @@ template <> cv::Mat &Frame::create(ftl::codecs::Channel c, const ftl::rgbd::Form
 
 template <> cv::cuda::GpuMat &Frame::create(ftl::codecs::Channel c, const ftl::rgbd::FormatBase &f) {
 	if (c == Channel::None) {
-		throw ftl::exception("Cannot create a None channel");
+		throw FTL_Error("Cannot create a None channel");
 	}
 
 	create<cv::cuda::GpuMat>(c);
@@ -349,85 +174,7 @@ void Frame::clearPackets(ftl::codecs::Channel c) {
 	m.encoded.clear();
 }
 
-/*template <> cv::Mat &Frame::create(ftl::codecs::Channel c) {
-	if (c == Channel::None) {
-		throw ftl::exception("Cannot create a None channel");
-	}
-	channels_ += c;
-	gpu_ -= c;
-
-	auto &m = _get(c);
-
-	m.encoded.clear();  // Remove all old encoded data
-
-	return m.host;
-}
-
-template <> cv::cuda::GpuMat &Frame::create(ftl::codecs::Channel c) {
-	if (c == Channel::None) {
-		throw ftl::exception("Cannot create a None channel");
-	}
-	channels_ += c;
-	gpu_ += c;
-
-	auto &m = _get(c);
-
-	m.encoded.clear();  // Remove all old encoded data
-
-	return m.gpu;
-}*/
-
 void Frame::resetTexture(ftl::codecs::Channel c) {
 	auto &m = getData(c);
 	m.tex.free();
 }
-
-/*void Frame::setOrigin(ftl::rgbd::FrameState *state) {
-	if (origin_ != nullptr) {
-		throw ftl::exception("Can only set origin once after reset");
-	}
-
-	origin_ = state;
-	state_ = *state;
-}
-
-const Eigen::Matrix4d &Frame::getPose() const {
-	return get<Eigen::Matrix4d>(ftl::codecs::Channel::Pose);
-}
-
-const ftl::rgbd::Camera &Frame::getLeftCamera() const {
-	return get<ftl::rgbd::Camera>(ftl::codecs::Channel::Calibration);
-}
-
-const ftl::rgbd::Camera &Frame::getRightCamera() const {
-	return get<ftl::rgbd::Camera>(ftl::codecs::Channel::Calibration2);
-}
-
-void ftl::rgbd::Frame::setPose(const Eigen::Matrix4d &pose) {
-	if (origin_) origin_->setPose(pose);
-}
-
-void ftl::rgbd::Frame::setLeftCamera(const ftl::rgbd::Camera &c) {
-	if (origin_) origin_->setLeft(c);
-}
-
-void ftl::rgbd::Frame::setRightCamera(const ftl::rgbd::Camera &c) {
-	if (origin_) origin_->setRight(c);
-}
-
-std::string ftl::rgbd::Frame::getConfigString() const {
-	return get<nlohmann::json>(ftl::codecs::Channel::Configuration).dump();
-}
-
-const std::vector<unsigned char> &ftl::rgbd::Frame::getRawData(ftl::codecs::Channel channel) const {
-	if (static_cast<int>(channel) < static_cast<int>(ftl::codecs::Channel::Data)) throw ftl::exception("Non data channel");
-	if (!hasChannel(channel)) throw ftl::exception("Data channel does not exist");
-
-	return data_data_.at(static_cast<int>(channel));
-}
-
-void ftl::rgbd::Frame::createRawData(ftl::codecs::Channel c, const std::vector<unsigned char> &v) {
-	data_data_.insert({static_cast<int>(c), v});
-	data_channels_ += c;
-}
-*/
diff --git a/components/rgbd-sources/src/frameset.cpp b/components/rgbd-sources/src/frameset.cpp
index c632d0585816bbed7a720ffa43c1cd58a6deab6d..70e6c0c47439d59ebc513a7f3850f0313c70d683 100644
--- a/components/rgbd-sources/src/frameset.cpp
+++ b/components/rgbd-sources/src/frameset.cpp
@@ -1,6 +1,9 @@
 #include <ftl/rgbd/frameset.hpp>
 #include <ftl/timer.hpp>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 #include <chrono>
 
 using ftl::rgbd::Builder;
@@ -188,9 +191,9 @@ void Builder::onFrameSet(const std::function<bool(ftl::rgbd::FrameSet &)> &cb) {
 ftl::rgbd::FrameState &Builder::state(size_t ix) {
 	UNIQUE_LOCK(mutex_, lk);
 	if (ix >= states_.size()) {
-		throw ftl::exception("Frame state out-of-bounds");
+		throw FTL_Error("Frame state out-of-bounds: " << ix);
 	}
-	if (!states_[ix]) throw ftl::exception("Missing framestate");
+	if (!states_[ix]) throw FTL_Error("Missing framestate");
 	return *states_[ix];
 }
 
diff --git a/components/rgbd-sources/src/group.cpp b/components/rgbd-sources/src/group.cpp
index 02f3b6f70415ad943563b0dc67e830053578061c..5739d0396fb2f554dc2c58ed35c0e90e0ca88688 100644
--- a/components/rgbd-sources/src/group.cpp
+++ b/components/rgbd-sources/src/group.cpp
@@ -5,6 +5,9 @@
 
 #include <chrono>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 using ftl::rgbd::Group;
 using ftl::rgbd::Source;
 using ftl::rgbd::kMaxFramesets;
diff --git a/components/rgbd-sources/test/CMakeLists.txt b/components/rgbd-sources/test/CMakeLists.txt
index 9065105d61d56ef02061f580b92d9f9409e1be5a..b0d54ac91c82d8f798c7510ab4d86ac6ebaf97b9 100644
--- a/components/rgbd-sources/test/CMakeLists.txt
+++ b/components/rgbd-sources/test/CMakeLists.txt
@@ -1,6 +1,6 @@
 ### Source Unit ################################################################
 add_executable(source_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	../src/frame.cpp
 	./source_unit.cpp
 )
@@ -12,7 +12,7 @@ add_test(SourceUnitTest source_unit)
 
 ### Frame Unit #################################################################
 add_executable(frame_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	./frame_unit.cpp
 	../src/frame.cpp
 )
diff --git a/components/streams/CMakeLists.txt b/components/streams/CMakeLists.txt
index d971532fb5cd5a8b81130fa9b52a16f2a9623ed7..43722b88881e64e3517a63b225811437bcce7f05 100644
--- a/components/streams/CMakeLists.txt
+++ b/components/streams/CMakeLists.txt
@@ -1,3 +1,10 @@
+#add_library(FtlStream OBJECT src/stream.cpp) 
+#target_include_directories(FtlStream PUBLIC
+#	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+#	$<INSTALL_INTERFACE:include>
+#	PRIVATE src)
+#add_dependencies(FtlStream ftlcommon)
+
 set(STREAMSRC
 	src/stream.cpp
 	src/filestream.cpp
@@ -21,4 +28,6 @@ target_include_directories(ftlstreams PUBLIC
 #target_include_directories(cv-node PUBLIC ${PROJECT_SOURCE_DIR}/include)
 target_link_libraries(ftlstreams ftlrgbd ftlcommon ${OpenCV_LIBS} Eigen3::Eigen ftlnet ftlcodecs ftlaudio)
 
-add_subdirectory(test)
\ No newline at end of file
+if (BUILD_TESTS)
+add_subdirectory(test)
+endif()
diff --git a/components/streams/include/ftl/streams/stream.hpp b/components/streams/include/ftl/streams/stream.hpp
index 397d2f11771a76303f9b199529ddd157969b8e01..04adcf4e88f5718c4fc8f35938432820da3330ed 100644
--- a/components/streams/include/ftl/streams/stream.hpp
+++ b/components/streams/include/ftl/streams/stream.hpp
@@ -1,7 +1,6 @@
 #ifndef _FTL_STREAM_STREAM_HPP_
 #define _FTL_STREAM_STREAM_HPP_
 
-#include <loguru.hpp>
 #include <ftl/configuration.hpp>
 #include <ftl/configurable.hpp>
 #include <ftl/rgbd/source.hpp>
diff --git a/components/streams/src/filestream.cpp b/components/streams/src/filestream.cpp
index 7d7f5d5f4b5f142abf395b8fd182ee4bf0be44e2..62256438b3e8c9951c08a97de3defe67d58bd374 100644
--- a/components/streams/src/filestream.cpp
+++ b/components/streams/src/filestream.cpp
@@ -1,6 +1,9 @@
 #include <fstream>
 #include <ftl/streams/filestream.hpp>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 using ftl::stream::File;
 using ftl::codecs::StreamPacket;
 using ftl::codecs::Packet;
diff --git a/components/streams/src/netstream.cpp b/components/streams/src/netstream.cpp
index f25983ccd093cb52e01f91c54dc5a4869b1df0d8..95a4ca9414b1e5e924963fd64594103d116780fe 100644
--- a/components/streams/src/netstream.cpp
+++ b/components/streams/src/netstream.cpp
@@ -1,5 +1,8 @@
 #include <ftl/streams/netstream.hpp>
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 using ftl::stream::Net;
 using ftl::codecs::StreamPacket;
 using ftl::codecs::Packet;
diff --git a/components/streams/src/receiver.cpp b/components/streams/src/receiver.cpp
index 36d321c4346ca4586cfedc2293512f9bc0491458..93ead346533981ce430d44fecb209566dce6887d 100644
--- a/components/streams/src/receiver.cpp
+++ b/components/streams/src/receiver.cpp
@@ -4,6 +4,9 @@
 #include "parsers.hpp"
 #include "injectors.hpp"
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 using ftl::stream::Receiver;
 using ftl::stream::Stream;
 using ftl::codecs::StreamPacket;
diff --git a/components/streams/src/sender.cpp b/components/streams/src/sender.cpp
index 239597c7119056b85f1dea6e973c624c2668ac72..e1aa69b58f9ec0d2d2187e76f2107848e757281e 100644
--- a/components/streams/src/sender.cpp
+++ b/components/streams/src/sender.cpp
@@ -3,6 +3,9 @@
 
 #include "injectors.hpp"
 
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
 using ftl::stream::Sender;
 using ftl::codecs::StreamPacket;
 using ftl::codecs::Packet;
@@ -28,11 +31,6 @@ Sender::~Sender() {
 	}
 }
 
-/*void Sender::onStateChange(const std::function<void(ftl::codecs::Channel,const ftl::rgbd::FrameState&)> &cb) {
-	if (cb && state_cb_) throw ftl::exception("State change callback already set");
-	state_cb_ = cb;
-}*/
-
 void Sender::setStream(ftl::stream::Stream*s) {
 	if (stream_) stream_->onPacket(nullptr);
     stream_ = s;
diff --git a/components/streams/src/stream.cpp b/components/streams/src/stream.cpp
index 2c409d3b42630ce25b915c3c9e7f04e92fb11c9a..332c9215f381b161d332fb213adeadaf76d1cb1b 100644
--- a/components/streams/src/stream.cpp
+++ b/components/streams/src/stream.cpp
@@ -7,26 +7,26 @@ using ftl::stream::Stream;
 
 const ftl::codecs::Channels<0> &Stream::available(int fs) const {
 	SHARED_LOCK(mtx_, lk);
-	if (fs < 0 || static_cast<uint32_t>(fs) >= state_.size()) throw ftl::exception("Frameset index out-of-bounds");
+	if (fs < 0 || static_cast<uint32_t>(fs) >= state_.size()) throw FTL_Error("Frameset index out-of-bounds: " << fs);
 	return state_[fs].available;
 }
 
 const ftl::codecs::Channels<0> &Stream::selected(int fs) const {
 	SHARED_LOCK(mtx_, lk);
-	if (fs < 0 || static_cast<uint32_t>(fs) >= state_.size()) throw ftl::exception("Frameset index out-of-bounds");
+	if (fs < 0 || static_cast<uint32_t>(fs) >= state_.size()) throw FTL_Error("Frameset index out-of-bounds: " << fs);
 	return state_[fs].selected;
 }
 
 void Stream::select(int fs, const ftl::codecs::Channels<0> &s, bool make) {
 	UNIQUE_LOCK(mtx_, lk);
-	if (fs < 0 || (!make && static_cast<uint32_t>(fs) >= state_.size())) throw ftl::exception("Frameset index out-of-bounds");
+	if (fs < 0 || (!make && static_cast<uint32_t>(fs) >= state_.size())) throw FTL_Error("Frameset index out-of-bounds: " << fs);
 	if (static_cast<uint32_t>(fs) >= state_.size()) state_.resize(fs+1);
 	state_[fs].selected = s;
 }
 
 ftl::codecs::Channels<0> &Stream::available(int fs) {
 	UNIQUE_LOCK(mtx_, lk);
-	if (fs < 0) throw ftl::exception("Frameset index out-of-bounds");
+	if (fs < 0) throw FTL_Error("Frameset index out-of-bounds: " << fs);
 	if (static_cast<uint32_t>(fs) >= state_.size()) state_.resize(fs+1);
 	return state_[fs].available;
 }
diff --git a/components/streams/test/CMakeLists.txt b/components/streams/test/CMakeLists.txt
index cb2b1e5a194cb056841bddffad02ae9ffe64a483..288f2ff07d511a6965707ef278e0f6922aa9501d 100644
--- a/components/streams/test/CMakeLists.txt
+++ b/components/streams/test/CMakeLists.txt
@@ -1,6 +1,6 @@
 ### Stream Unit ################################################################
 add_executable(stream_unit
-	./tests.cpp
+	$<TARGET_OBJECTS:CatchTest>
 	./stream_unit.cpp
 	../src/stream.cpp
 )
@@ -12,7 +12,7 @@ add_test(StreamUnitTest stream_unit)
 
 ### File Stream Unit ###########################################################
 add_executable(filestream_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	./filestream_unit.cpp
 	../src/filestream.cpp
 	../src/stream.cpp
@@ -37,7 +37,7 @@ add_test(FileStreamUnitTest filestream_unit)
 
 ### Sender Unit ################################################################
 add_executable(sender_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	./sender_unit.cpp
 	../src/sender.cpp
 	../src/stream.cpp
@@ -52,7 +52,7 @@ add_test(SenderUnitTest sender_unit)
 
 ### Receiver Unit ##############################################################
 add_executable(receiver_unit
-	./tests.cpp
+$<TARGET_OBJECTS:CatchTest>
 	./receiver_unit.cpp
 	../src/receiver.cpp
 	../src/stream.cpp
diff --git a/components/structures/include/ftl/data/frame.hpp b/components/structures/include/ftl/data/frame.hpp
index b0fa4192289aba9276cc8668cff45a0bbe0d4705..14f1c1e25ac2d5085ff6fb81368515221dada391 100644
--- a/components/structures/include/ftl/data/frame.hpp
+++ b/components/structures/include/ftl/data/frame.hpp
@@ -335,12 +335,12 @@ template <int BASE, int N, typename STATE, typename DATA>
 template <typename T>
 T& ftl::data::Frame<BASE,N,STATE,DATA>::get(ftl::codecs::Channel channel) {
 	if (channel == ftl::codecs::Channel::None) {
-		throw ftl::exception("Attempting to get channel 'None'");
+		throw FTL_Error("Attempting to get channel 'None'");
 	}
 
 	// Add channel if not already there
 	if (!channels_.has(channel)) {
-		throw ftl::exception(ftl::Formatter() << "Frame channel does not exist: " << (int)channel);
+		throw FTL_Error("Frame channel does not exist: " << (int)channel);
 	}
 
 	return getData(channel).template as<T>();
@@ -351,7 +351,7 @@ template <int BASE, int N, typename STATE, typename DATA>
 template <typename T>
 const T& ftl::data::Frame<BASE,N,STATE,DATA>::get(ftl::codecs::Channel channel) const {
 	if (channel == ftl::codecs::Channel::None) {
-		throw ftl::exception("Attempting to get channel 'None'");
+		throw FTL_Error("Attempting to get channel 'None'");
 	} else if (channel == ftl::codecs::Channel::Pose) {
 		return state_.template as<T,ftl::codecs::Channel::Pose>();
 	} else if (channel == ftl::codecs::Channel::Calibration) {
@@ -364,7 +364,7 @@ const T& ftl::data::Frame<BASE,N,STATE,DATA>::get(ftl::codecs::Channel channel)
 
 	// Add channel if not already there
 	if (!channels_.has(channel)) {
-		throw ftl::exception(ftl::Formatter() << "Frame channel does not exist: " << (int)channel);
+		throw FTL_Error("Frame channel does not exist: " << (int)channel);
 	}
 
 	return getData(channel).template as<T>();
@@ -375,11 +375,11 @@ template <int BASE, int N, typename STATE, typename DATA>
 // cppcheck-suppress *
 template <typename T>
 void ftl::data::Frame<BASE,N,STATE,DATA>::get(ftl::codecs::Channel channel, T &params) const {
-	if (static_cast<int>(channel) < static_cast<int>(ftl::codecs::Channel::Data)) throw ftl::exception("Cannot use generic type with non data channel");
-	if (!hasChannel(channel)) throw ftl::exception("Data channel does not exist");
+	if (static_cast<int>(channel) < static_cast<int>(ftl::codecs::Channel::Data)) throw FTL_Error("Cannot use generic type with non data channel");
+	if (!hasChannel(channel)) throw FTL_Error("Data channel does not exist");
 
 	const auto &i = data_data_.find(static_cast<int>(channel));
-	if (i == data_data_.end()) throw ftl::exception("Data channel does not exist");
+	if (i == data_data_.end()) throw FTL_Error("Data channel does not exist");
 
 	auto unpacked = msgpack::unpack((const char*)(*i).second.data(), (*i).second.size());
 	unpacked.get().convert(params);
@@ -390,7 +390,7 @@ template <int BASE, int N, typename STATE, typename DATA>
 template <typename T>
 T &ftl::data::Frame<BASE,N,STATE,DATA>::create(ftl::codecs::Channel c) {
 	if (c == ftl::codecs::Channel::None) {
-		throw ftl::exception("Cannot create a None channel");
+		throw FTL_Error("Cannot create a None channel");
 	}
 	channels_ += c;
 
@@ -402,7 +402,7 @@ template <int BASE, int N, typename STATE, typename DATA>
 // cppcheck-suppress *
 template <typename T>
 void ftl::data::Frame<BASE,N,STATE,DATA>::create(ftl::codecs::Channel channel, const T &value) {
-	if (static_cast<int>(channel) < static_cast<int>(ftl::codecs::Channel::Data)) throw ftl::exception("Cannot use generic type with non data channel");
+	if (static_cast<int>(channel) < static_cast<int>(ftl::codecs::Channel::Data)) throw FTL_Error("Cannot use generic type with non data channel");
 
 	data_channels_ += channel;
 
@@ -415,7 +415,7 @@ void ftl::data::Frame<BASE,N,STATE,DATA>::create(ftl::codecs::Channel channel, c
 template <int BASE, int N, typename STATE, typename DATA>
 void ftl::data::Frame<BASE,N,STATE,DATA>::setOrigin(STATE *state) {
 	if (origin_ != nullptr) {
-		throw ftl::exception("Can only set origin once after reset");
+		throw FTL_Error("Can only set origin once after reset");
 	}
 
 	origin_ = state;
@@ -459,8 +459,8 @@ std::string ftl::data::Frame<BASE,N,STATE,DATA>::getConfigString() const {
 
 template <int BASE, int N, typename STATE, typename DATA>
 const std::vector<unsigned char> &ftl::data::Frame<BASE,N,STATE,DATA>::getRawData(ftl::codecs::Channel channel) const {
-	if (static_cast<int>(channel) < static_cast<int>(ftl::codecs::Channel::Data)) throw ftl::exception("Non data channel");
-	if (!hasChannel(channel)) throw ftl::exception("Data channel does not exist");
+	if (static_cast<int>(channel) < static_cast<int>(ftl::codecs::Channel::Data)) throw FTL_Error("Non data channel");
+	if (!hasChannel(channel)) throw FTL_Error("Data channel does not exist");
 
 	return data_data_.at(static_cast<int>(channel));
 }
diff --git a/components/structures/include/ftl/data/framestate.hpp b/components/structures/include/ftl/data/framestate.hpp
index 7d7255f60b68492b440634539a08fe6c0ac3afe0..3c06bd9a871c16b791973bc31b9215e8860a4efe 100644
--- a/components/structures/include/ftl/data/framestate.hpp
+++ b/components/structures/include/ftl/data/framestate.hpp
@@ -128,11 +128,11 @@ class FrameState {
 	 */
 	template <typename T, ftl::codecs::Channel C, typename S, int N> struct As {
 		static const T &func(const ftl::data::FrameState<S,N> &t) {
-			throw ftl::exception("Type not supported for state channel");
+			throw FTL_Error("Type not supported for state channel");
 		}
 
 		static T &func(ftl::data::FrameState<S,N> &t) {
-			throw ftl::exception("Type not supported for state channel");
+			throw FTL_Error("Type not supported for state channel");
 		}
 	};