diff --git a/applications/gui/CMakeLists.txt b/applications/gui/CMakeLists.txt
index c4ea2272fd92e76ccf0aa6b4b9154d06aaea8485..ac9ae7ec20881ab9c4112d9baf0696ce87693536 100644
--- a/applications/gui/CMakeLists.txt
+++ b/applications/gui/CMakeLists.txt
@@ -5,6 +5,7 @@
 set(GUISRC
 	src/main.cpp
 	src/ctrl_window.cpp
+	src/src_window.cpp
 )
 
 add_executable(ftl-gui ${GUISRC})
diff --git a/applications/gui/src/main.cpp b/applications/gui/src/main.cpp
index a12ff59055e2aca407f3b0032850bce52163cc07..f0cd4c325894c5708be23bee6a02d578c726c2d5 100644
--- a/applications/gui/src/main.cpp
+++ b/applications/gui/src/main.cpp
@@ -17,137 +17,18 @@
 #include <nanogui/label.h>
 
 #include "ctrl_window.hpp"
+#include "src_window.hpp"
 
 using std::string;
 using ftl::rgbd::RGBDSource;
 
-class GLTexture {
-	public:
-	GLTexture() {
-		glGenTextures(1, &glid_);
-        glBindTexture(GL_TEXTURE_2D, glid_);
-		cv::Mat m(cv::Size(100,100), CV_8UC3);
-		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m.cols, m.rows, 0, GL_RGB, GL_UNSIGNED_BYTE, m.data);
-		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-	}
-	~GLTexture() {
-		glDeleteTextures(1, &glid_);
-	}
-
-	void update(cv::Mat &m) {
-		if (m.rows == 0) return;
-		glBindTexture(GL_TEXTURE_2D, glid_);
-		// TODO Allow for other formats
-		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m.cols, m.rows, 0, GL_BGR, GL_UNSIGNED_BYTE, m.data);
-		auto err = glGetError();
-		if (err != 0) LOG(ERROR) << "OpenGL Texture error: " << err;
-	}
-
-	unsigned int texture() const { return glid_; }
-
-	private:
-	unsigned int glid_;
-};
-
 /*struct SourceViews {
 	ftl::rgbd::RGBDSource *source;
 	GLTexture texture;
 	nanogui::ImageView *view;
 };*/
 
-template<class T>
-Eigen::Matrix<T,4,4> lookAt
-(
-	Eigen::Matrix<T,3,1> const & eye,
-	Eigen::Matrix<T,3,1> const & center,
-	Eigen::Matrix<T,3,1> const & up
-)
-{
-	typedef Eigen::Matrix<T,4,4> Matrix4;
-	typedef Eigen::Matrix<T,3,1> Vector3;
-
-	Vector3 f = (center - eye).normalized();
-	Vector3 u = up.normalized();
-	Vector3 s = f.cross(u).normalized();
-	u = s.cross(f);
-
-	Matrix4 res;
-	res <<	s.x(),s.y(),s.z(),-s.dot(eye),
-			u.x(),u.y(),u.z(),-u.dot(eye),
-			-f.x(),-f.y(),-f.z(),f.dot(eye),
-			0,0,0,1;
-
-	return res;
-}
-
-class VirtualCameraView : public nanogui::ImageView {
-	public:
-	VirtualCameraView(nanogui::Widget *parent) : nanogui::ImageView(parent, 0) {
-		src_ = nullptr;
-		eye_ = Eigen::Vector3f(0.0f, 0.0f, 0.0f);
-		centre_ = Eigen::Vector3f(0.0f, 0.0f, -4.0f);
-		up_ = Eigen::Vector3f(0,1.0f,0);
-		lookPoint_ = Eigen::Vector3f(0.0f,0.0f,-4.0f);
-		lerpSpeed_ = 0.4f;
-	}
 
-	void setSource(RGBDSource *src) { src_ = src; }
-
-	bool mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) {
-		//LOG(INFO) << "Mouse move: " << p[0];
-		if (src_ && down) {
-			Eigen::Vector4f camPos = src_->point(p[0],p[1]);
-			camPos *= -1.0f;
-			Eigen::Vector4f worldPos =  src_->getPose() * camPos;
-			lookPoint_ = Eigen::Vector3f(worldPos[0],worldPos[1],worldPos[2]);
-			LOG(INFO) << "Depth at click = " << -camPos[2];
-		}
-	}
-
-	bool keyboardEvent(int key, int scancode, int action, int modifiers) {
-		LOG(INFO) << "Key press" << key << " - " << action;
-		if (key == 81 || key == 83) {
-			// TODO Should rotate around lookAt object, but requires correct depth
-			Eigen::Quaternion<float> q;  q = Eigen::AngleAxis<float>((key == 81) ? 0.01f : -0.01f, up_);
-			eye_ = (q * (eye_ - centre_)) + centre_;
-		} else if (key == 84 || key == 82) {
-			float scalar = (key == 84) ? 0.99f : 1.01f;
-			eye_ = ((eye_ - centre_) * scalar) + centre_;
-		}
-	}
-
-	void draw(NVGcontext *ctx) {
-		//net_->broadcast("grab");
-		if (src_) {
-			cv::Mat rgb, depth;
-			centre_ += (lookPoint_ - centre_) * (lerpSpeed_ * 0.1f);
-			Eigen::Matrix4f viewPose = lookAt<float>(eye_,centre_,up_).inverse();
-
-			src_->setPose(viewPose);
-			src_->grab();
-			src_->getRGBD(rgb, depth);
-			if (rgb.rows > 0) {
-				texture_.update(rgb);
-				bindImage(texture_.texture());
-			}
-
-			screen()->performLayout(ctx);
-		}
-		ImageView::draw(ctx);
-	}
-
-	private:
-	RGBDSource *src_;
-	GLTexture texture_;
-	Eigen::Vector3f eye_;
-	Eigen::Vector3f centre_;
-	Eigen::Vector3f up_;
-	Eigen::Vector3f lookPoint_;
-	float lerpSpeed_;
-};
 
 class FTLApplication : public nanogui::Screen {
 	public:
@@ -155,7 +36,7 @@ class FTLApplication : public nanogui::Screen {
 		using namespace nanogui;
 		net_ = net;
 
-		for (auto &src : root->getConfig()["sources"]) {		
+		/*for (auto &src : root->getConfig()["sources"]) {		
 			RGBDSource *in = ftl::rgbd::RGBDSource::create(src, net); //new ftl::rgbd::NetSource(src, &net);
 			if (!in) {
 				LOG(ERROR) << "Unrecognised source: " << src;
@@ -185,9 +66,10 @@ class FTLApplication : public nanogui::Screen {
 				//displays.emplace_back(config["display"], src["uri"]);
 				LOG(INFO) << (string)src["uri"] << " loaded ";
 			}
-		}
+		}*/
 
-		auto window = new ftl::gui::ControlWindow(this, controller);
+		auto cwindow = new ftl::gui::ControlWindow(this, controller);
+		auto swindow = new ftl::gui::SourceWindow(this, controller);
 
 		setVisible(true);
 		performLayout();
diff --git a/applications/gui/src/src_window.cpp b/applications/gui/src/src_window.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5bbb12c83ad9aa7b05af146eadcd077f698e6c2b
--- /dev/null
+++ b/applications/gui/src/src_window.cpp
@@ -0,0 +1,161 @@
+#include "src_window.hpp"
+
+#include <nanogui/imageview.h>
+#include <nanogui/combobox.h>
+#include <nanogui/label.h>
+#include <nanogui/opengl.h>
+#include <nanogui/glutil.h>
+#include <nanogui/screen.h>
+#include <nanogui/layout.h>
+
+using ftl::gui::SourceWindow;
+using ftl::rgbd::RGBDSource;
+using std::string;
+
+class GLTexture {
+	public:
+	GLTexture() {
+		glGenTextures(1, &glid_);
+        glBindTexture(GL_TEXTURE_2D, glid_);
+		cv::Mat m(cv::Size(100,100), CV_8UC3);
+		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m.cols, m.rows, 0, GL_RGB, GL_UNSIGNED_BYTE, m.data);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+	}
+	~GLTexture() {
+		glDeleteTextures(1, &glid_);
+	}
+
+	void update(cv::Mat &m) {
+		if (m.rows == 0) return;
+		glBindTexture(GL_TEXTURE_2D, glid_);
+		// TODO Allow for other formats
+		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, m.cols, m.rows, 0, GL_BGR, GL_UNSIGNED_BYTE, m.data);
+		auto err = glGetError();
+		if (err != 0) LOG(ERROR) << "OpenGL Texture error: " << err;
+	}
+
+	unsigned int texture() const { return glid_; }
+
+	private:
+	unsigned int glid_;
+};
+
+template<class T>
+Eigen::Matrix<T,4,4> lookAt
+(
+	Eigen::Matrix<T,3,1> const & eye,
+	Eigen::Matrix<T,3,1> const & center,
+	Eigen::Matrix<T,3,1> const & up
+)
+{
+	typedef Eigen::Matrix<T,4,4> Matrix4;
+	typedef Eigen::Matrix<T,3,1> Vector3;
+
+	Vector3 f = (center - eye).normalized();
+	Vector3 u = up.normalized();
+	Vector3 s = f.cross(u).normalized();
+	u = s.cross(f);
+
+	Matrix4 res;
+	res <<	s.x(),s.y(),s.z(),-s.dot(eye),
+			u.x(),u.y(),u.z(),-u.dot(eye),
+			-f.x(),-f.y(),-f.z(),f.dot(eye),
+			0,0,0,1;
+
+	return res;
+}
+
+class VirtualCameraView : public nanogui::ImageView {
+	public:
+	VirtualCameraView(nanogui::Widget *parent) : nanogui::ImageView(parent, 0) {
+		src_ = nullptr;
+		eye_ = Eigen::Vector3f(0.0f, 0.0f, 0.0f);
+		centre_ = Eigen::Vector3f(0.0f, 0.0f, -4.0f);
+		up_ = Eigen::Vector3f(0,1.0f,0);
+		lookPoint_ = Eigen::Vector3f(0.0f,0.0f,-4.0f);
+		lerpSpeed_ = 0.4f;
+	}
+
+	void setSource(RGBDSource *src) { src_ = src; }
+
+	bool mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) {
+		//LOG(INFO) << "Mouse move: " << p[0];
+		if (src_ && down) {
+			Eigen::Vector4f camPos = src_->point(p[0],p[1]);
+			camPos *= -1.0f;
+			Eigen::Vector4f worldPos =  src_->getPose() * camPos;
+			lookPoint_ = Eigen::Vector3f(worldPos[0],worldPos[1],worldPos[2]);
+			LOG(INFO) << "Depth at click = " << -camPos[2];
+		}
+	}
+
+	bool keyboardEvent(int key, int scancode, int action, int modifiers) {
+		LOG(INFO) << "Key press" << key << " - " << action;
+		if (key == 81 || key == 83) {
+			// TODO Should rotate around lookAt object, but requires correct depth
+			Eigen::Quaternion<float> q;  q = Eigen::AngleAxis<float>((key == 81) ? 0.01f : -0.01f, up_);
+			eye_ = (q * (eye_ - centre_)) + centre_;
+		} else if (key == 84 || key == 82) {
+			float scalar = (key == 84) ? 0.99f : 1.01f;
+			eye_ = ((eye_ - centre_) * scalar) + centre_;
+		}
+	}
+
+	void draw(NVGcontext *ctx) {
+		//net_->broadcast("grab");
+		if (src_) {
+			cv::Mat rgb, depth;
+			centre_ += (lookPoint_ - centre_) * (lerpSpeed_ * 0.1f);
+			Eigen::Matrix4f viewPose = lookAt<float>(eye_,centre_,up_).inverse();
+
+			src_->setPose(viewPose);
+			src_->grab();
+			src_->getRGBD(rgb, depth);
+			if (rgb.rows > 0) {
+				texture_.update(rgb);
+				bindImage(texture_.texture());
+			}
+
+			screen()->performLayout(ctx);
+		}
+		ImageView::draw(ctx);
+	}
+
+	private:
+	RGBDSource *src_;
+	GLTexture texture_;
+	Eigen::Vector3f eye_;
+	Eigen::Vector3f centre_;
+	Eigen::Vector3f up_;
+	Eigen::Vector3f lookPoint_;
+	float lerpSpeed_;
+};
+
+SourceWindow::SourceWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl)
+		: nanogui::Window(parent, "Source View"), ctrl_(ctrl) {
+	setLayout(new nanogui::GroupLayout());
+
+	using namespace nanogui;
+
+    src_ = ftl::create<ftl::rgbd::NetSource>(ctrl->getRoot(), "source", ctrl->getNet());
+
+    new Label(this, "Select source","sans-bold");
+    auto available = ctrl->getNet()->findAll<string>("list_streams");
+    auto select = new ComboBox(this, available);
+    select->setCallback([this,available](int ix) {
+        LOG(INFO) << "Change source: " << ix;
+        src_->set("uri", available[ix]);
+    });
+
+    auto imageView = new VirtualCameraView(this);
+    //cam.view = imageView;
+    imageView->setGridThreshold(20);
+    imageView->setSource(src_);
+}
+
+SourceWindow::~SourceWindow() {
+
+}
diff --git a/applications/gui/src/src_window.hpp b/applications/gui/src/src_window.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b52ae8034ff6733d2582c33dd689e3a7c937f7c9
--- /dev/null
+++ b/applications/gui/src/src_window.hpp
@@ -0,0 +1,29 @@
+#ifndef _FTL_GUI_SRCWINDOW_HPP_
+#define _FTL_GUI_SRCWINDOW_HPP_
+
+#include <nanogui/window.h>
+#include <ftl/master.hpp>
+#include <ftl/uuid.hpp>
+#include <ftl/net_source.hpp>
+
+namespace ftl {
+namespace gui {
+
+/**
+ * Manage connected nodes and add new connections.
+ */
+class SourceWindow : public nanogui::Window {
+	public:
+	SourceWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl);
+	~SourceWindow();
+
+	private:
+	ftl::ctrl::Master *ctrl_;
+	ftl::rgbd::NetSource *src_;
+
+};
+
+}
+}
+
+#endif  // _FTL_GUI_SRCWINDOW_HPP_
diff --git a/components/common/cpp/include/ftl/configuration.hpp b/components/common/cpp/include/ftl/configuration.hpp
index 046b58a07bf536a691a970b3a9d93bf4b5ab119b..d38472746254587e4f495547ba48e095f750ed12 100644
--- a/components/common/cpp/include/ftl/configuration.hpp
+++ b/components/common/cpp/include/ftl/configuration.hpp
@@ -126,15 +126,23 @@ T *ftl::config::create(ftl::Configurable *parent, const std::string &name, ARGS
                 entity["$id"] = id_str + std::string("#") + name;
             }
         }
-    } /*else {
-		nlohmann::json &res = resolve(entity);
-		if (!res["uri"].is_string()) {
-            res["uri"] = *parent->get<std::string>("uri") + std::string("/") + name;
-			LOG(WARNING) << "Creating false URI!!! - " << res["uri"].get<std::string>();
+
+        return create<T>(entity, args...);
+    } else if (entity.is_null()) {
+        // Must create the object from scratch...
+        std::string id_str = *parent->get<std::string>("$id");
+        if (id_str.find('#') != std::string::npos) {
+            id_str = id_str + std::string("/") + name;
+        } else {
+            id_str = id_str + std::string("#") + name;
         }
-	}*/
+        parent->getConfig()[name] = {
+            {"$id", id_str}
+        };
 
-    return create<T>(entity, args...);
+        nlohmann::json &entity2 = parent->getConfig()[name];
+        return create<T>(entity2, args...);
+    }
 }
 
 #endif  // _FTL_COMMON_CONFIGURATION_HPP_
diff --git a/components/control/cpp/include/ftl/master.hpp b/components/control/cpp/include/ftl/master.hpp
index 2a961af8de19a97ace119a70dd2870e993733dc9..82b68cf525de5e425d2bc705e599028995798dea 100644
--- a/components/control/cpp/include/ftl/master.hpp
+++ b/components/control/cpp/include/ftl/master.hpp
@@ -55,6 +55,9 @@ class Master {
 	//void onStatus();
 	// void onChange();
 
+	ftl::net::Universe *getNet() { return net_; }
+	ftl::Configurable *getRoot() { return root_; }
+
 	private:
 	std::vector<std::function<void(const LogEvent&)>> log_handlers_;
 	ftl::Configurable *root_;
diff --git a/components/rgbd-sources/include/ftl/net_source.hpp b/components/rgbd-sources/include/ftl/net_source.hpp
index c95437da81a75a11084a853221218cbced87e05d..2693128d275e3e4f7ad317e8d93b68dd5f096aa1 100644
--- a/components/rgbd-sources/include/ftl/net_source.hpp
+++ b/components/rgbd-sources/include/ftl/net_source.hpp
@@ -29,15 +29,17 @@ class NetSource : public RGBDSource {
 	}
 
 	void setPose(const Eigen::Matrix4f &pose);
-	void setURI(const std::string &uri);
 
 	private:
 	bool has_calibration_;
 	ftl::UUID peer_;
 	int N_;
+	bool active_;
+	std::string uri_;
 
 	bool _getCalibration(ftl::net::Universe &net, const ftl::UUID &peer, const std::string &src, ftl::rgbd::CameraParameters &p);
 	void _recv(const std::vector<unsigned char> &jpg, const std::vector<unsigned char> &d);
+	void _updateURI();
 };
 
 }
diff --git a/components/rgbd-sources/src/net_source.cpp b/components/rgbd-sources/src/net_source.cpp
index a658be388e09d603f18d9062290982cf39185796..8c0187f039e2a60d2a0b5eb041d3efd1464a99f5 100644
--- a/components/rgbd-sources/src/net_source.cpp
+++ b/components/rgbd-sources/src/net_source.cpp
@@ -45,43 +45,18 @@ NetSource::NetSource(nlohmann::json &config) : RGBDSource(config) {
 }
 
 NetSource::NetSource(nlohmann::json &config, ftl::net::Universe *net)
-		: RGBDSource(config, net) {
+		: RGBDSource(config, net), active_(false) {
 
-	auto uri = get<string>("uri");
-	if (!uri) {
-		LOG(ERROR) << "NetSource does not have a URI";
-		return;
-	}
-	auto p = net->findOne<ftl::UUID>("find_stream", *uri);
-	if (!p) {
-		LOG(ERROR) << "Could not find stream: " << *uri;
-		return;
-	}
-	peer_ = *p;
-
-	has_calibration_ = _getCalibration(*net, peer_, *uri, params_);
-	
-	net->bind(*uri, [this](const vector<unsigned char> &jpg, const vector<unsigned char> &d) {
-		unique_lock<mutex> lk(mutex_);
-		_recv(jpg, d);
+	on("uri", [this](const config::Event &e) {
+		_updateURI();
 	});
 
-	N_ = 10;
-
-	// Initiate stream with request for first 10 frames
-	try {
-		net->send(peer_, "get_stream", *uri, 10, 0, net->id(), *uri);
-	} catch(...) {
-		LOG(ERROR) << "Could not connect to stream " << *uri;
-	}
+	_updateURI();
 }
 
 NetSource::~NetSource() {
-	auto uri = get<string>("uri");
-
-	// TODO(Nick) If URI changes then must unbind + rebind.
-	if (uri) {
-		net_->unbind(*uri);
+	if (uri_.size() > 0) {
+		net_->unbind(uri_);
 	}
 }
 
@@ -99,6 +74,8 @@ void NetSource::_recv(const vector<unsigned char> &jpg, const vector<unsigned ch
 }
 
 void NetSource::setPose(const Eigen::Matrix4f &pose) {
+	if (!active_) return;
+
 	vector<unsigned char> vec((unsigned char*)pose.data(), (unsigned char*)(pose.data()+(pose.size())));
 	try {
 		net_->send(peer_, "set_pose", *get<string>("uri"), vec);
@@ -108,10 +85,45 @@ void NetSource::setPose(const Eigen::Matrix4f &pose) {
 	RGBDSource::setPose(pose);
 }
 
-void NetSource::setURI(const std::string &uri) {
-	//RGBDSource::setURI(uri);
-	set("uri", uri);
-	has_calibration_ = _getCalibration(*net_, peer_, *get<string>("uri"), params_);
+void NetSource::_updateURI() {
+	active_ = false;
+	auto uri = get<string>("uri");
+
+	// TODO(Nick) If URI changes then must unbind + rebind.
+	if (uri_.size() > 0) {
+		net_->unbind(uri_);
+	}
+
+	if (uri) {
+		auto p = net_->findOne<ftl::UUID>("find_stream", *uri);
+		if (!p) {
+			LOG(ERROR) << "Could not find stream: " << *uri;
+			return;
+		}
+		peer_ = *p;
+
+		has_calibration_ = _getCalibration(*net_, peer_, *uri, params_);
+
+		net_->bind(*uri, [this](const vector<unsigned char> &jpg, const vector<unsigned char> &d) {
+			unique_lock<mutex> lk(mutex_);
+			_recv(jpg, d);
+		});
+
+		N_ = 10;
+
+		// Initiate stream with request for first 10 frames
+		try {
+			net_->send(peer_, "get_stream", *uri, 10, 0, net_->id(), *uri);
+		} catch(...) {
+			LOG(ERROR) << "Could not connect to stream " << *uri;
+		}
+
+		uri_ = *uri;
+		active_ = true;
+	} else {
+		uri_ = "";
+		LOG(WARNING) << "NetSource URI is missing";
+	}
 }
 
 void NetSource::grab() {