Repository: VILLASframework/node (language: python)

Issue:
Remove Golang node-types
This is implemented by #613 




Discussion / hints:
marked this issue as related to #341

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8bbe1a2d4..f552dbeaa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -76,6 +76,10 @@ find_package(RDMACM)
 find_package(spdlog)
 find_package(Etherlab)
 find_package(Lua)
+find_package(LibDataChannel)
+
+# For compat between libfmt 8 and 9
+add_compile_definitions(FMT_DEPRECATED_OSTREAM)
 
 # Check for tools
 find_program(PASTE NAMES paste)
@@ -86,17 +90,10 @@ endif()
 # Check programs
 find_program(PROTOBUFC_COMPILER NAMES protoc-c)
 find_program(PROTOBUF_COMPILER NAMES protoc)
-find_program(GO NAMES go PATHS /usr/local/go/bin)
 
 # Build without any GPL-code
 option(WITHOUT_GPL "Build VILLASnode without any GPL code" OFF)
 
-# Optionally download Go toolchain
-option(DOWNLOAD_GO "Download Go toolchain" ON)
-if(NOT GO AND DOWNLOAD_GO)
-    include("${CMAKE_CURRENT_LIST_DIR}/cmake/Go.cmake")
-endif()
-
 set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/share/pkgconfig:/usr/lib64/pkgconfig")
 
 pkg_check_modules(JANSSON IMPORTED_TARGET REQUIRED jansson>=2.13)
@@ -158,7 +155,6 @@ cmake_dependent_option(WITH_CLIENTS         "Build client applications"
 cmake_dependent_option(WITH_CONFIG          "Build with support for libconfig configuration syntax" ON "LIBCONFIG_FOUND" OFF)
 cmake_dependent_option(WITH_DOC             "Build documentation"                                   ON "TOPLEVEL_PROJECT" OFF)
 cmake_dependent_option(WITH_FPGA            "Build with support for VILLASfpga"                     ON "FOUND_SUBMODULE_FPGA" OFF)
-cmake_dependent_option(WITH_GO              "Build with Go"                                         ON "GO" OFF)
 cmake_dependent_option(WITH_GRAPHVIZ        "Build with Graphviz"                                   ON "CGRAPH_FOUND; GVC_FOUND" OFF)
 cmake_dependent_option(WITH_HOOKS           "Build with support for processing hook plugins"        ON "" OFF)
 cmake_dependent_option(WITH_LUA             "Build with Lua"                                        ON "LUA_FOUND" OFF)
@@ -177,7 +173,6 @@ cmake_dependent_option(WITH_NODE_EXAMPLE    "Build with example node-type"
 cmake_dependent_option(WITH_NODE_EXEC       "Build with exec node-type"                             ON "" OFF)
 cmake_dependent_option(WITH_NODE_FILE       "Build with file node-type"                             ON "" OFF)
 cmake_dependent_option(WITH_NODE_FPGA       "Build with fpga node-type"                             ON "WITH_FPGA" OFF)
-cmake_dependent_option(WITH_NODE_GO         "Build with Go nodes-types"                             ON "WITH_GO" OFF)
 cmake_dependent_option(WITH_NODE_IEC61850   "Build with iec61850 node-types"                        ON "LIBIEC61850_FOUND;NOT WITHOUT_GPL" OFF)
 cmake_dependent_option(WITH_NODE_IEC60870   "Build with iec60870 node-types"                        ON "LIB60870_FOUND;NOT WITHOUT_GPL" OFF)
 cmake_dependent_option(WITH_NODE_INFINIBAND "Build with infiniband node-type"                       ON "IBVerbs_FOUND; RDMACM_FOUND" OFF) # Infiniband node-type is currenly broken
@@ -197,6 +192,7 @@ cmake_dependent_option(WITH_NODE_STATS      "Build with stats node-type"
 cmake_dependent_option(WITH_NODE_TEMPER     "Build with temper node-type"                           ON "LIBUSB_FOUND" OFF)
 cmake_dependent_option(WITH_NODE_TEST_RTT   "Build with test_rtt node-type"                         ON "" OFF)
 cmake_dependent_option(WITH_NODE_ULDAQ      "Build with uldaq node-type"                            ON "LIBULDAQ_FOUND" OFF)
+cmake_dependent_option(WITH_NODE_WEBRTC     "Build with webrtc node-type"                           ON "LibDataChannel_FOUND" OFF)
 cmake_dependent_option(WITH_NODE_WEBSOCKET  "Build with websocket node-type"                        ON "WITH_WEB; LIBWEBSOCKETS_FOUND" OFF)
 cmake_dependent_option(WITH_NODE_ZEROMQ     "Build with zeromq node-type"                           ON "LIBZMQ_FOUND;NOT WITHOUT_GPL" OFF)
 
@@ -213,10 +209,6 @@ if(WITH_FPGA)
     add_subdirectory(fpga)
 endif()
 
-if(WITH_GO)
-    add_subdirectory(go)
-endif()
-
 add_subdirectory(common)
 add_subdirectory(etc)
 add_subdirectory(lib)
@@ -258,7 +250,6 @@ add_feature_info(CLIENTS                WITH_CLIENTS                "Build clien
 add_feature_info(CONFIG                 WITH_CONFIG                 "Build with support for libconfig configuration syntax")
 add_feature_info(DOC                    WITH_DOC                    "Build documentation")
 add_feature_info(FPGA                   WITH_FPGA                   "Build with FPGA support")
-add_feature_info(GO                     WITH_GO                     "Build with Go code")
 add_feature_info(GRAPHVIZ               WITH_GRAPHVIZ               "Build with Graphviz support")
 add_feature_info(HOOKS                  WITH_HOOKS                  "Build with support for processing hook plugins")
 add_feature_info(LUA                    WITH_LUA                    "Build with Lua support")
@@ -277,7 +268,6 @@ add_feature_info(NODE_EXAMPLE           WITH_NODE_EXAMPLE           "Build with
 add_feature_info(NODE_EXEC              WITH_NODE_EXEC              "Build with exec node-type")
 add_feature_info(NODE_FILE              WITH_NODE_FILE              "Build with file node-type")
 add_feature_info(NODE_FPGA              WITH_NODE_FPGA              "Build with fpga node-type")
-add_feature_info(NODE_GO                WITH_NODE_GO                "Build with Go node-types")
 add_feature_info(NODE_IEC61850          WITH_NODE_IEC61850          "Build with iec61850 node-types")
 add_feature_info(NODE_IEC60870          WITH_NODE_IEC60870          "Build with iec60870 node-types")
 add_feature_info(NODE_INFINIBAND        WITH_NODE_INFINIBAND        "Build with infiniband node-type")
@@ -297,6 +287,7 @@ add_feature_info(NODE_STATS             WITH_NODE_STATS             "Build with
 add_feature_info(NODE_TEMPER            WITH_NODE_TEMPER            "Build with temper node-type")
 add_feature_info(NODE_TEST_RTT          WITH_NODE_TEST_RTT          "Build with test_rtt node-type")
 add_feature_info(NODE_ULDAQ             WITH_NODE_ULDAQ             "Build with uldaq node-type")
+add_feature_info(NODE_WEBRTC            WITH_NODE_WEBRTC            "Build with webrtc node-type")
 add_feature_info(NODE_WEBSOCKET         WITH_NODE_WEBSOCKET         "Build with websocket node-type")
 add_feature_info(NODE_ZEROMQ            WITH_NODE_ZEROMQ            "Build with zeromq node-type")
 
diff --git a/common b/common
index b18d2aca5..d9d4ac76a 160000
--- a/common
+++ b/common
@@ -1,1 +1,1 @@
-Subproject commit b18d2aca5b9a158aaba3a0f82cffe253fc427b15
+Subproject commit d9d4ac76a5403e14f7899dae480781e9cdcf0572
diff --git a/etc/examples/nodes/go-example.conf b/etc/examples/nodes/go-example.conf
deleted file mode 100644
index 412c84586..000000000
--- a/etc/examples/nodes/go-example.conf
+++ /dev/null
@@ -1,15 +0,0 @@
-nodes = {
-	example_node = {
-		type = "go.example"
-		value = 555
-
-		format = "json"
-
-		in = {
-			# signals ={
-			# 	count = 5,
-			# 	type = "float"
-			# }
-		}
-	}
-}
diff --git a/etc/examples/nodes/go-loopback.conf b/etc/examples/nodes/go-loopback.conf
deleted file mode 100644
index 3be4ab541..000000000
--- a/etc/examples/nodes/go-loopback.conf
+++ /dev/null
@@ -1,38 +0,0 @@
-nodes = {
-	loopback_node = {
-		type = "go.loopback"
-		value = 555
-
-		format = "opal.asyncip"
-
-		in = {
-			signals ={
-				count = 5,
-				type = "float"
-			}
-		}
-	}
-
-	signal = {
-		type = "signal"
-		signal = "counter"
-		rate = 1
-		values = 5
-	}
-}
-
-paths = (
-	{
-		in = "loopback_node"
-
-		hooks = (
-			{
-				type = "dump"
-			}
-		)
-	},
-	{
-		in = "signal",
-		out = "loopback_node"
-	}
-)
diff --git a/etc/examples/nodes/webrtc.conf b/etc/examples/nodes/webrtc.conf
index 69b130eae..f587093ed 100644
--- a/etc/examples/nodes/webrtc.conf
+++ b/etc/examples/nodes/webrtc.conf
@@ -1,31 +1,15 @@
-nodes = {
-	webrtc_node = {
-		type = "webrtc",
 
+nodes = {
+	webrtc = {
+		type = "webrtc"
+		# required session key.
+		session = "<YOUR SESSION>"
+		# optional signaling server.
+		server = "<ADDRESS OF YOUR SIGNALING SERVER>"
+		# optional format.
 		format = "json"
-
-		# A unique session identifier which must be shared between two nodes
-		session = "my-session-name"
-
-		# Address to the websocket signaling server
-		server = "wss://villas.k8s.eonerc.rwth-aachen.de/ws/signaling"
-
-		# Setting for Interactive Connectivity Establishment
-		ice = {
-			# List of STUN/TURN servers
-			servers = (
-				{
-					urls = [
-						"stun:stun.0l.de:3478",
-						"turn:turn.0l.de:3478?transport=udp",
-						"turn:turn.0l.de:3478?transport=tcp"
-					],
-
-					# Credentials for TURN servers
-					username = "villas"
-					password = "villas"
-				}
-			)
-		}
+		# optional initial connect timeout.
+		wait_seconds = 120
 	}
 }
+
diff --git a/go/CMakeLists.txt b/go/CMakeLists.txt
deleted file mode 100644
index 3ea7a4199..000000000
--- a/go/CMakeLists.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-add_subdirectory(lib)
diff --git a/go/cmd/example.go b/go/cmd/example.go
deleted file mode 100644
index 716fcd11a..000000000
--- a/go/cmd/example.go
+++ /dev/null
@@ -1,108 +0,0 @@
-/** Example of using libvillas node-types in Go code
- *
- * This example demonstrate how you can use VILLASnode's
- * node-types from a Go application.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package main
-
-import (
-	"log"
-	"os"
-	"time"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg"
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/config"
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/node"
-
-	"github.com/google/uuid"
-)
-
-func main() {
-	cfg := &config.LoopbackNode{
-		Node: config.Node{
-			Name: "lo1",
-			Type: "loopback",
-		},
-		In: config.NodeLoopbackIn{
-			Hooks: []interface{}{
-				&config.PrintHook{
-					Hook: config.Hook{
-						Type: "print",
-					},
-				},
-			},
-			Signals: []config.Signal{
-				{
-					Name: "sig1",
-				},
-				{
-					Name: "sig2",
-				},
-				{
-					Name: "sig3",
-				},
-			},
-		},
-	}
-
-	n, err := node.NewNode(cfg, uuid.New())
-	if err != nil {
-		log.Fatalf("Failed to create node: %s", err)
-	}
-	defer n.Close()
-
-	if err := n.Check(); err != nil {
-		log.Fatalf("Failed to check node: %s", err)
-	}
-
-	if err := n.Prepare(); err != nil {
-		log.Fatalf("Failed to prepare node: %s", err)
-	}
-
-	if err := n.Start(); err != nil {
-		log.Fatalf("Failed to start node: %s", err)
-	}
-	defer n.Stop()
-
-	log.Printf("%s\n", n.NameFull())
-
-	now := time.Now()
-
-	smps_send := []node.Sample{
-		{
-			Sequence: 1234,
-			Timestamps: pkg.Timestamps{
-				Origin: pkg.Timestamp{now.Unix(), now.UnixNano()},
-			},
-			Data: []float64{1.1, 2.2, 3.3},
-		},
-		{
-			Sequence: 1235,
-			Timestamps: pkg.Timestamps{
-				Origin: pkg.Timestamp{now.Unix(), now.UnixNano()},
-			}, Data: []float64{4.4, 5.5, 6.6},
-		},
-	}
-
-	log.Printf("Sent: %+#v\n", smps_send)
-
-	cnt_written := n.Write(smps_send)
-	smps_received := n.Read(cnt_written)
-
-	log.Printf("Received: %+#v\n", smps_received)
-
-	if len(smps_send) != len(smps_received) {
-		log.Printf("Different length")
-		os.Exit(-1)
-	}
-
-	if smps_send[0].Data[0] != smps_received[0].Data[0] {
-		log.Printf("Different data")
-		os.Exit(-1)
-	}
-}
diff --git a/go/go.mod b/go/go.mod
deleted file mode 100644
index ef2e67aea..000000000
--- a/go/go.mod
+++ /dev/null
@@ -1,32 +0,0 @@
-module git.rwth-aachen.de/acs/public/villas/node/go
-
-go 1.17
-
-require (
-	github.com/google/uuid v1.3.0
-	github.com/gorilla/websocket v1.5.0
-	github.com/pion/webrtc/v3 v3.1.24
-)
-
-require (
-	github.com/pion/datachannel v1.5.2 // indirect
-	github.com/pion/dtls/v2 v2.1.3 // indirect
-	github.com/pion/ice/v2 v2.2.1 // indirect
-	github.com/pion/interceptor v0.1.7 // indirect
-	github.com/pion/logging v0.2.2 // indirect
-	github.com/pion/mdns v0.0.5 // indirect
-	github.com/pion/randutil v0.1.0 // indirect
-	github.com/pion/rtcp v1.2.9 // indirect
-	github.com/pion/rtp v1.7.4 // indirect
-	github.com/pion/sctp v1.8.2 // indirect
-	github.com/pion/sdp/v3 v3.0.4 // indirect
-	github.com/pion/srtp/v2 v2.0.5 // indirect
-	github.com/pion/stun v0.3.5 // indirect
-	github.com/pion/transport v0.13.0 // indirect
-	github.com/pion/turn/v2 v2.0.8 // indirect
-	github.com/pion/udp v0.1.1 // indirect
-	golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
-	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
-	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
-	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
-)
diff --git a/go/go.sum b/go/go.sum
deleted file mode 100644
index 5aa07fa97..000000000
--- a/go/go.sum
+++ /dev/null
@@ -1,154 +0,0 @@
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
-github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
-github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
-github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
-github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
-github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio=
-github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
-github.com/pion/ice/v2 v2.2.1 h1:R3MeuJZpU1ty3diPqpD5OxaxcZ15eprAc+EtUiSoFxg=
-github.com/pion/ice/v2 v2.2.1/go.mod h1:Op8jlPtjeiycsXh93Cs4jK82C9j/kh7vef6ztIOvtIQ=
-github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo=
-github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
-github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
-github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
-github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
-github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
-github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
-github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
-github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
-github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
-github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/rtp v1.7.4 h1:4dMbjb1SuynU5OpA3kz1zHK+u+eOCQjW3MAeVHf1ODA=
-github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
-github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
-github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
-github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
-github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
-github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
-github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
-github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
-github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
-github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
-github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
-github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
-github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
-github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
-github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
-github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
-github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
-github.com/pion/webrtc/v3 v3.1.24 h1:s9PuwisrgHe1FTqfwK4p3T7rXtAHaUNhycbdMjADT28=
-github.com/pion/webrtc/v3 v3.1.24/go.mod h1:mO/yv7fBN3Lp7YNlnYcTj1jtpvNvssJG+7eh6itZ4xM=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
-golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go/lib/CMakeLists.txt b/go/lib/CMakeLists.txt
deleted file mode 100644
index b866a7c70..000000000
--- a/go/lib/CMakeLists.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-set(LIB libvillas-go.a)
-set(HEADER libvillas-go.h)
-
-file(GLOB_RECURSE SRCS *.go)
-file(GLOB_RECURSE NODE_SRCS ../pkg/nodes/*)
-
-list(FILTER SRCS EXCLUDE REGEX /_obj/)
-list(FILTER NODE_SRCS EXCLUDE REGEX /_obj/)
-
-add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${HEADER}
-  DEPENDS ${SRCS}
-  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
-  COMMAND env "GOPATH=${GOPATH}" "CGO_ENABLED=1" "GOARM=${GOARM}" "GOARCH=${GOARCH}" ${GO} tool cgo  -exportheader "${CMAKE_CURRENT_BINARY_DIR}/${HEADER}" -- -I "${CMAKE_CURRENT_SOURCE_DIR}/../../include" ${SRCS}
-  COMMENT "Generating CGo header ${HEADER}"
-  COMMAND_EXPAND_LISTS
-)
-
-add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${LIB}
-  DEPENDS ${SRCS} ${NODE_SRCS}
-  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
-  COMMAND env "PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig" "GOPATH=${GOPATH}" "CGO_ENABLED=1" "GOARM=${GOARM}" "GOARCH=${GOARCH}" "CGO_CFLAGS=-I${CMAKE_CURRENT_SOURCE_DIR}/../../include" ${GO} build -buildmode=c-archive -o "${CMAKE_CURRENT_BINARY_DIR}/${LIB}" ${CMAKE_GO_FLAGS} .
-  COMMENT "Building CGo library ${LIB}"
-  COMMAND_EXPAND_LISTS)
-
-add_custom_target(villas-go-lib DEPENDS ${LIB})
-add_custom_target(villas-go-header DEPENDS ${HEADER})
-
-add_library(villas-go STATIC IMPORTED GLOBAL)
-add_dependencies(villas-go villas-go-lib)
-set_target_properties(villas-go 
-  PROPERTIES
-  IMPORTED_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/${LIB}"
-  INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}
-)
diff --git a/go/lib/README.md b/go/lib/README.md
deleted file mode 100644
index f9e857ef5..000000000
--- a/go/lib/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Writing custom node-type in Go
-
-This directory contains the required glue code for implementing custom node-types in the Go programming language.
-
-Contents of this directory will be compiled into a static library and linked into 'libvillas.so'.
-
-Have a look at the 'go.example' node-type implemented in '<villas-node-git>/go/pkg/nodes/example' for an example on how to implement your own node-type in Go.
diff --git a/go/lib/bridge.c b/go/lib/bridge.c
deleted file mode 100644
index 719c09582..000000000
--- a/go/lib/bridge.c
+++ /dev/null
@@ -1,21 +0,0 @@
-/** Bridge code for call C-function pointers from Go code
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-#include <stdlib.h>
-
-#include <villas/nodes/go.h>
-
-void bridge_go_register_node_factory(_go_register_node_factory_cb cb, _go_plugin_list pl, char *name, char *desc, int flags)
-{
-    cb(pl, name, desc, flags);
-}
-
-void bridge_go_logger_log(_go_logger_log_cb cb, _go_logger l, int level, char *msg)
-{
-    cb(l, level, msg);
-    free(msg);
-}
diff --git a/go/lib/lib.go b/go/lib/lib.go
deleted file mode 100644
index 95cc39a6d..000000000
--- a/go/lib/lib.go
+++ /dev/null
@@ -1,190 +0,0 @@
-/** CGo interface for writing node-types in Go
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package main
-
-// #include <stdint.h>
-// #include <villas/nodes/go.h>
-// void bridge_go_register_node_factory(_go_register_node_factory_cb cb, _go_plugin_list pl, char *name, char *desc, int flags);
-import "C"
-
-import (
-	"runtime/cgo"
-	"unsafe"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes"
-
-	_ "git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes/example"
-	_ "git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes/loopback"
-	_ "git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes/webrtc"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/errors"
-)
-
-func main() {}
-
-type NodeType struct {
-	nodes.NodeType
-}
-
-//export RegisterGoNodeTypes
-func RegisterGoNodeTypes(cb C._go_register_node_factory_cb, pl C._go_plugin_list) {
-	ntm := nodes.NodeTypes()
-
-	for n, t := range ntm {
-		C.bridge_go_register_node_factory(cb, pl, C.CString(n), C.CString(t.Description), C.int(t.Flags))
-	}
-}
-
-//export NewGoNode
-func NewGoNode(t *C.char) C.uintptr_t {
-	nodeTypes := nodes.NodeTypes()
-
-	typ, ok := nodeTypes[C.GoString(t)]
-	if !ok {
-		return 0
-	}
-
-	node := typ.Constructor()
-	return C.uintptr_t(cgo.NewHandle(node))
-}
-
-//export GoNodeSetLogger
-func GoNodeSetLogger(p C.uintptr_t, cb C._go_logger_log_cb, l C._go_logger) {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	n.SetLogger(NewVillasLogger(cb, l))
-}
-
-//export GoNodeClose
-func GoNodeClose(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Close()))
-}
-
-//export GoNodePrepare
-func GoNodePrepare(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Prepare()))
-}
-
-//export GoNodeParse
-func GoNodeParse(p C.uintptr_t, c *C.char) C.int {
-	scfg := C.GoString(c)
-	bcfg := []byte(scfg)
-
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Parse(bcfg)))
-}
-
-//export GoNodeCheck
-func GoNodeCheck(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Check()))
-}
-
-//export GoNodeStart
-func GoNodeStart(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Start()))
-}
-
-//export GoNodeStop
-func GoNodeStop(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Stop()))
-}
-
-//export GoNodePause
-func GoNodePause(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Pause()))
-}
-
-//export GoNodeResume
-func GoNodeResume(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Resume()))
-}
-
-//export GoNodeRestart
-func GoNodeRestart(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Restart()))
-}
-
-//export GoNodeRead
-func GoNodeRead(p C.uintptr_t, buf *C.char, sz C.int) (C.int, C.int) {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-
-	src, err := n.Read()
-	if err != nil {
-		return -1, C.int(errors.ErrorToInt(err))
-	}
-
-	// Create slice which is backed by buf
-	dst := (*[1 << 30]byte)(unsafe.Pointer(buf))[:sz]
-	lsz := copy(dst, src)
-
-	return C.int(lsz), 0
-}
-
-//export GoNodeWrite
-func GoNodeWrite(p C.uintptr_t, data []byte) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Write(data)))
-}
-
-//export GoNodeReverse
-func GoNodeReverse(p C.uintptr_t) C.int {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	return C.int(errors.ErrorToInt(n.Reverse()))
-}
-
-//export GoNodeGetPollFDs
-func GoNodeGetPollFDs(p C.uintptr_t) ([]int, C.int) {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	f, err := n.PollFDs()
-	if err != nil {
-		return nil, C.int(errors.ErrorToInt(err))
-	}
-
-	return f, 0
-}
-
-//export GoNodeGetNetemFDs
-func GoNodeGetNetemFDs(p C.uintptr_t) ([]int, C.int) {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	f, err := n.NetemFDs()
-	if err != nil {
-		return nil, C.int(errors.ErrorToInt(err))
-	}
-
-	return f, 0
-}
-
-//export GoNodeDetails
-func GoNodeDetails(p C.uintptr_t) *C.char {
-	h := cgo.Handle(p)
-	n := h.Value().(nodes.Node)
-	d := n.Details()
-	return C.CString(d)
-}
diff --git a/go/lib/logger.go b/go/lib/logger.go
deleted file mode 100644
index fd613fc9c..000000000
--- a/go/lib/logger.go
+++ /dev/null
@@ -1,113 +0,0 @@
-/** CGo interface for VILLASnode logger
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package main
-
-// #include <stdlib.h>
-// #include <villas/nodes/go.h>
-// void bridge_go_logger_log(_go_logger_log_cb cb, _go_logger l, int level, char *msg);
-import "C"
-
-import (
-	"fmt"
-	"os"
-)
-
-type LogLevel int
-
-const (
-	// https://github.com/gabime/spdlog/blob/a51b4856377a71f81b6d74b9af459305c4c644f8/include/spdlog/common.h#L76
-	LogLevelTrace    LogLevel = iota
-	LogLevelDebug    LogLevel = iota
-	LogLevelInfo     LogLevel = iota
-	LogLevelWarn     LogLevel = iota
-	LogLevelError    LogLevel = iota
-	LogLevelCritical LogLevel = iota
-	LogLevelOff      LogLevel = iota
-)
-
-type VillasLogger struct {
-	inst C._go_logger
-	cb   C._go_logger_log_cb
-}
-
-func NewVillasLogger(cb C._go_logger_log_cb, l C._go_logger) *VillasLogger {
-	return &VillasLogger{
-		cb:   cb,
-		inst: l,
-	}
-}
-
-func (l *VillasLogger) log(lvl LogLevel, msg string) {
-	C.bridge_go_logger_log(l.cb, l.inst, C.int(lvl), C.CString(msg))
-}
-
-func (l *VillasLogger) Trace(msg string) {
-	l.log(LogLevelTrace, msg)
-}
-
-func (l *VillasLogger) Debug(msg string) {
-	l.log(LogLevelDebug, msg)
-}
-
-func (l *VillasLogger) Info(msg string) {
-	l.log(LogLevelInfo, msg)
-}
-
-func (l *VillasLogger) Warn(msg string) {
-	l.log(LogLevelWarn, msg)
-}
-
-func (l *VillasLogger) Error(msg string) {
-	l.log(LogLevelError, msg)
-}
-
-func (l *VillasLogger) Critical(msg string) {
-	l.log(LogLevelCritical, msg)
-}
-
-func (l *VillasLogger) Tracef(format string, args ...interface{}) {
-	msg := fmt.Sprintf(format, args...)
-	l.log(LogLevelTrace, msg)
-}
-
-func (l *VillasLogger) Debugf(format string, args ...interface{}) {
-	msg := fmt.Sprintf(format, args...)
-	l.log(LogLevelDebug, msg)
-}
-
-func (l *VillasLogger) Infof(format string, args ...interface{}) {
-	msg := fmt.Sprintf(format, args...)
-	l.log(LogLevelInfo, msg)
-}
-
-func (l *VillasLogger) Warnf(format string, args ...interface{}) {
-	msg := fmt.Sprintf(format, args...)
-	l.log(LogLevelWarn, msg)
-}
-
-func (l *VillasLogger) Errorf(format string, args ...interface{}) {
-	msg := fmt.Sprintf(format, args...)
-	l.log(LogLevelError, msg)
-}
-
-func (l *VillasLogger) Criticalf(format string, args ...interface{}) {
-	msg := fmt.Sprintf(format, args...)
-	l.log(LogLevelCritical, msg)
-}
-
-func (l *VillasLogger) Panic(msg string) {
-	l.Critical(msg)
-	fmt.Println("Paniced")
-	os.Exit(-1)
-}
-
-func (l *VillasLogger) Panicf(format string, args ...interface{}) {
-	l.Criticalf(format, args...)
-	fmt.Println("Paniced")
-	os.Exit(-1)
-}
diff --git a/go/pkg/config/hook.go b/go/pkg/config/hook.go
deleted file mode 100644
index 243b08e79..000000000
--- a/go/pkg/config/hook.go
+++ /dev/null
@@ -1,17 +0,0 @@
-/** Go types for hook configuration.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package config
-
-type Hook struct {
-	Type     string `json:"type"`
-	Priority int    `json:"priority,omitempty"`
-}
-
-type PrintHook struct {
-	Hook
-}
diff --git a/go/pkg/config/node.go b/go/pkg/config/node.go
deleted file mode 100644
index d1cd3b774..000000000
--- a/go/pkg/config/node.go
+++ /dev/null
@@ -1,28 +0,0 @@
-/** Go types for node configuration.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package config
-
-type Node struct {
-	Name string `json:"name"`
-	Type string `json:"type"`
-}
-
-type NodeDir struct{}
-
-type NodeLoopbackIn struct {
-	NodeDir
-
-	Signals []Signal      `json:"signals"`
-	Hooks   []interface{} `json:"hooks"`
-}
-
-type LoopbackNode struct {
-	Node
-
-	In NodeLoopbackIn `json:"in"`
-}
diff --git a/go/pkg/config/signal.go b/go/pkg/config/signal.go
deleted file mode 100644
index 2935bfd3b..000000000
--- a/go/pkg/config/signal.go
+++ /dev/null
@@ -1,22 +0,0 @@
-/** Go types for signal configuration.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package config
-
-const (
-	SignalTypeFloat   = "float"
-	SignalTypeInteger = "integer"
-	SignalTypeBoolean = "boolean"
-	SignalTypeComplex = "complex"
-)
-
-type Signal struct {
-	Name string  `json:"name,omitempty"`
-	Type string  `json:"type,omitempty"`
-	Unit string  `json:"unit,omitempty"`
-	Init float64 `json:"init,omitempty"`
-}
diff --git a/go/pkg/errors/errors.go b/go/pkg/errors/errors.go
deleted file mode 100644
index 75a120e00..000000000
--- a/go/pkg/errors/errors.go
+++ /dev/null
@@ -1,31 +0,0 @@
-/** Common error types
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package errors
-
-import (
-	"C"
-)
-import "fmt"
-
-var ErrEndOfFile = fmt.Errorf("end-of-file")
-
-func ErrorToInt(e error) int {
-	if e == nil {
-		return 0
-	} else {
-		return -1
-	}
-}
-
-func IntToError(ret int) error {
-	if ret == 0 {
-		return nil
-	} else {
-		return fmt.Errorf("ret=%d", ret)
-	}
-}
diff --git a/go/pkg/node/node.go b/go/pkg/node/node.go
deleted file mode 100644
index e15823f88..000000000
--- a/go/pkg/node/node.go
+++ /dev/null
@@ -1,212 +0,0 @@
-/** Wrapper for using libvillas in Go applications.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package node
-
-// #cgo LDFLAGS: -lvillas
-// #include <villas/node.h>
-import "C"
-
-import (
-	"encoding/json"
-	"fmt"
-	"log"
-	"unsafe"
-
-	"github.com/google/uuid"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg"
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/errors"
-)
-
-const (
-	MAX_SIGNALS   = 64
-	NUM_HUGEPAGES = 100
-)
-
-type Node struct {
-	inst *C.vnode
-}
-
-func IsValidNodeNname(name string) bool {
-	return bool(C.node_is_valid_name(C.CString(name)))
-}
-
-func NewNode(cfg interface{}, sn_uuid uuid.UUID) (*Node, error) {
-	if js, err := json.Marshal(cfg); err != nil {
-		return nil, fmt.Errorf("failed to serialize config: %w", err)
-	} else {
-		log.Printf("Config = %s\n", js)
-		n := C.node_new(C.CString(string(js)), C.CString(sn_uuid.String()))
-		return &Node{n}, nil
-	}
-}
-
-func (n *Node) Prepare() error {
-	return errors.IntToError(int(C.node_prepare(n.inst)))
-}
-
-func (n *Node) Check() error {
-	return errors.IntToError(int(C.node_check(n.inst)))
-}
-
-func (n *Node) Start() error {
-	return errors.IntToError(int(C.node_start(n.inst)))
-}
-
-func (n *Node) Stop() error {
-	return errors.IntToError(int(C.node_stop(n.inst)))
-}
-
-func (n *Node) Pause() error {
-	return errors.IntToError(int(C.node_pause(n.inst)))
-}
-
-func (n *Node) Resume() error {
-	return errors.IntToError(int(C.node_resume(n.inst)))
-}
-
-func (n *Node) Restart() error {
-	return errors.IntToError(int(C.node_restart(n.inst)))
-}
-
-func (n *Node) Close() error {
-	return errors.IntToError(int(C.node_destroy(n.inst)))
-}
-
-func (n *Node) Reverse() error {
-	return errors.IntToError(int(C.node_reverse(n.inst)))
-}
-
-func (n *Node) Read(cnt int) []Sample {
-	csmps := make([]*C.vsample, cnt)
-
-	for i := 0; i < cnt; i++ {
-		csmps[i] = C.sample_alloc(MAX_SIGNALS)
-	}
-
-	read := int(C.node_read(n.inst, (**C.vsample)(unsafe.Pointer(&csmps[0])), C.uint(cnt)))
-
-	smps := make([]Sample, read)
-	for i := 0; i < read; i++ {
-		smps[i].FromC(csmps[i])
-		C.sample_decref(csmps[i])
-	}
-
-	return smps
-}
-
-func (n *Node) Write(smps []Sample) int {
-	cnt := len(smps)
-	csmps := make([]*C.vsample, cnt)
-
-	for i := 0; i < cnt; i++ {
-		csmps[i] = smps[i].ToC()
-	}
-
-	return int(C.node_write(n.inst, (**C.vsample)(unsafe.Pointer(&csmps[0])), C.uint(cnt)))
-}
-
-func (n *Node) PollFDs() []int {
-	var cfds [16]C.int
-	cnt := int(C.node_poll_fds(n.inst, (*C.int)(unsafe.Pointer(&cfds))))
-
-	fds := make([]int, cnt)
-	for i := 0; i < cnt; i++ {
-		fds[i] = int(cfds[i])
-	}
-
-	return fds
-}
-
-func (n *Node) NetemFDs() []int {
-	var cfds [16]C.int
-	cnt := int(C.node_netem_fds(n.inst, (*C.int)(unsafe.Pointer(&cfds))))
-
-	fds := make([]int, cnt)
-	for i := 0; i < cnt; i++ {
-		fds[i] = int(cfds[i])
-	}
-
-	return fds
-}
-
-func (n *Node) IsEnabled() bool {
-	return bool(C.node_is_enabled(n.inst))
-}
-
-func (n *Node) Name() string {
-	return C.GoString(C.node_name(n.inst))
-}
-
-func (n *Node) NameShort() string {
-	return C.GoString(C.node_name_short(n.inst))
-}
-
-func (n *Node) NameFull() string {
-	return C.GoString(C.node_name_full(n.inst))
-}
-
-func (n *Node) Details() string {
-	return C.GoString(C.node_details(n.inst))
-}
-
-func (n *Node) InputSignalsMaxCount() uint {
-	return uint(C.node_input_signals_max_cnt(n.inst))
-}
-
-func (n *Node) OutputSignalsMaxCount() uint {
-	return uint(C.node_output_signals_max_cnt(n.inst))
-}
-
-func (n *Node) ToJSON() string {
-	json_str := C.node_to_json_str(n.inst)
-	return C.GoString(json_str)
-}
-
-func init() {
-	C.memory_init(NUM_HUGEPAGES)
-}
-
-type Sample pkg.Sample
-
-func (s *Sample) ToC() *C.vsample {
-	return C.sample_pack(
-		C.uint(s.Sequence),
-		&C.struct_timespec{
-			tv_sec:  C.long(s.Timestamps.Origin[0]),
-			tv_nsec: C.long(s.Timestamps.Origin[1]),
-		},
-		&C.struct_timespec{
-			tv_sec:  C.long(s.Timestamps.Received[0]),
-			tv_nsec: C.long(s.Timestamps.Received[1]),
-		},
-		C.uint(len(s.Data)),
-		(*C.double)(unsafe.Pointer(&s.Data[0])),
-	)
-}
-
-func (s *Sample) FromC(c *C.vsample) {
-	var tsOrigin C.struct_timespec
-	var tsReceived C.struct_timespec
-
-	len := C.sample_length(c)
-
-	s.Data = make([]float64, uint(len))
-
-	C.sample_unpack(c,
-		(*C.uint)(unsafe.Pointer(&s.Sequence)),
-		&tsOrigin,
-		&tsReceived,
-		(*C.int)(unsafe.Pointer(&s.Flags)),
-		&len,
-		(*C.double)(unsafe.Pointer(&s.Data[0])),
-	)
-
-	s.Timestamps.Origin = pkg.Timestamp{int64(tsOrigin.tv_sec), int64(tsOrigin.tv_nsec)}
-	s.Timestamps.Received = pkg.Timestamp{int64(tsReceived.tv_sec), int64(tsReceived.tv_nsec)}
-}
diff --git a/go/pkg/nodes/base.go b/go/pkg/nodes/base.go
deleted file mode 100644
index 16fe3de75..000000000
--- a/go/pkg/nodes/base.go
+++ /dev/null
@@ -1,63 +0,0 @@
-/** Common code for implementing node-types in Go code.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package nodes
-
-type BaseNode struct {
-	Node
-
-	Logger Logger
-
-	Stopped chan struct{}
-}
-
-func NewBaseNode() BaseNode {
-	return BaseNode{}
-}
-
-func (n *BaseNode) Start() error {
-	n.Stopped = make(chan struct{})
-	return nil
-}
-
-func (n *BaseNode) Stop() error {
-	close(n.Stopped)
-
-	return nil
-}
-
-func (n *BaseNode) Parse(c []byte) error {
-	return nil
-}
-
-func (n *BaseNode) Check() error {
-	return nil
-}
-
-func (n *BaseNode) Restart() error {
-	return nil
-}
-
-func (n *BaseNode) Pause() error {
-	return nil
-}
-
-func (n *BaseNode) Resume() error {
-	return nil
-}
-
-func (n *BaseNode) Reverse() error {
-	return nil
-}
-
-func (n *BaseNode) Close() error {
-	return nil
-}
-
-func (n *BaseNode) SetLogger(l Logger) {
-	n.Logger = l
-}
diff --git a/go/pkg/nodes/example/node.go b/go/pkg/nodes/example/node.go
deleted file mode 100644
index 6e14fc296..000000000
--- a/go/pkg/nodes/example/node.go
+++ /dev/null
@@ -1,102 +0,0 @@
-/** Little example node-type written in Go code.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package nodes
-
-import (
-	"encoding/json"
-	"fmt"
-	"time"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg"
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/errors"
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes"
-)
-
-type Node struct {
-	nodes.BaseNode
-
-	ticker time.Ticker
-
-	lastSequence uint64
-
-	Config Config
-}
-
-type Config struct {
-	nodes.NodeConfig
-
-	Value int `json:"value"`
-}
-
-func NewNode() nodes.Node {
-	return &Node{
-		BaseNode:     nodes.NewBaseNode(),
-		ticker:       *time.NewTicker(1 * time.Second),
-		lastSequence: 0,
-	}
-}
-
-func (n *Node) Parse(c []byte) error {
-	return json.Unmarshal(c, &n.Config)
-}
-
-func (n *Node) Check() error {
-	return nil
-}
-
-func (n *Node) Prepare() error {
-	return nil
-}
-
-func (n *Node) Start() error {
-	n.Logger.Infof("hello from node")
-	n.Logger.Warnf("hello from node")
-	n.Logger.Errorf("hello from node")
-	n.Logger.Tracef("hello from node")
-	n.Logger.Criticalf("hello from node")
-
-	return n.BaseNode.Start()
-}
-
-func (n *Node) Read() ([]byte, error) {
-	select {
-	case <-n.Stopped:
-		return nil, errors.ErrEndOfFile
-
-	case <-n.ticker.C:
-		n.lastSequence++
-		smp := pkg.GenerateRandomSample()
-		smp.Sequence = n.lastSequence
-		smps := []pkg.Sample{smp}
-
-		return json.Marshal(smps)
-	}
-}
-
-func (n *Node) Write(data []byte) error {
-	n.Logger.Infof("Data: %s", string(data))
-
-	return nil
-}
-
-func (n *Node) PollFDs() ([]int, error) {
-	return []int{}, nil
-}
-
-func (n *Node) NetemFDs() ([]int, error) {
-	return []int{}, nil
-}
-
-func (n *Node) Details() string {
-	return fmt.Sprintf("value=%d", n.Config.Value)
-}
-
-func init() {
-	// Do not forget to import the package in go/lib/main.go!
-	nodes.RegisterNodeType("go.example", "A example node implemented in Go", NewNode, nodes.NodeSupportsRead|nodes.NodeSupportsWrite|nodes.NodeHidden)
-}
diff --git a/go/pkg/nodes/logger.go b/go/pkg/nodes/logger.go
deleted file mode 100644
index 293628616..000000000
--- a/go/pkg/nodes/logger.go
+++ /dev/null
@@ -1,25 +0,0 @@
-/** Logger interface.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package nodes
-
-type Logger interface {
-	Trace(msg string)
-	Debug(msg string)
-	Info(msg string)
-	Warn(msg string)
-	Error(msg string)
-	Critical(msg string)
-	Tracef(format string, args ...interface{})
-	Debugf(format string, args ...interface{})
-	Infof(format string, args ...interface{})
-	Warnf(format string, args ...interface{})
-	Errorf(format string, args ...interface{})
-	Criticalf(format string, args ...interface{})
-	Panic(msg string)
-	Panicf(format string, args ...interface{})
-}
diff --git a/go/pkg/nodes/loopback/node.go b/go/pkg/nodes/loopback/node.go
deleted file mode 100644
index 6c196fe12..000000000
--- a/go/pkg/nodes/loopback/node.go
+++ /dev/null
@@ -1,85 +0,0 @@
-/** Simple loopback node-type written in Go code.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package nodes
-
-import (
-	"encoding/json"
-	"fmt"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/errors"
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes"
-)
-
-type Node struct {
-	nodes.BaseNode
-
-	channel chan []byte
-
-	Config LoopbackConfig
-}
-
-type LoopbackConfig struct {
-	nodes.NodeConfig
-
-	Value int `json:"value"`
-}
-
-func NewNode() nodes.Node {
-	return &Node{
-		BaseNode: nodes.NewBaseNode(),
-		channel:  make(chan []byte, 1024),
-	}
-}
-
-func (n *Node) Parse(c []byte) error {
-	return json.Unmarshal(c, &n.Config)
-}
-
-func (n *Node) Check() error {
-	return nil
-}
-
-func (n *Node) Prepare() error {
-	return nil
-}
-
-func (n *Node) Start() error {
-	return n.BaseNode.Start()
-}
-
-func (n *Node) Read() ([]byte, error) {
-	select {
-	case <-n.Stopped:
-		return nil, errors.ErrEndOfFile
-
-	case buf := <-n.channel:
-		return buf, nil
-	}
-}
-
-func (n *Node) Write(data []byte) error {
-	n.channel <- data
-
-	return nil
-}
-
-func (n *Node) PollFDs() ([]int, error) {
-	return []int{}, nil
-}
-
-func (n *Node) NetemFDs() ([]int, error) {
-	return []int{}, nil
-}
-
-func (n *Node) Details() string {
-	return fmt.Sprintf("value=%d", n.Config.Value)
-}
-
-func init() {
-	nodes.RegisterNodeType("go.loopback", "A loopback node implmented in Go", NewNode, nodes.NodeSupportsRead|nodes.NodeSupportsWrite|nodes.NodeHidden)
-}
diff --git a/go/pkg/nodes/node.go b/go/pkg/nodes/node.go
deleted file mode 100644
index 8ca3279ff..000000000
--- a/go/pkg/nodes/node.go
+++ /dev/null
@@ -1,57 +0,0 @@
-/** Node interface.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package nodes
-
-const (
-	NodeSupportsPoll    = (1 << iota)
-	NodeSupportsRead    = (1 << iota)
-	NodeSupportsWrite   = (1 << iota)
-	NodeRequiresWeb     = (1 << iota)
-	NodeProvidesSignals = (1 << iota)
-	NodeInternal        = (1 << iota)
-	NodeHidden          = (1 << iota)
-)
-
-type NodeConstructor func() Node
-
-type Node interface {
-	Close() error
-
-	Prepare() error
-
-	Parse(cfg []byte) error
-
-	Check() error
-
-	Start() error
-	Stop() error
-
-	Pause() error
-	Resume() error
-	Restart() error
-
-	Read() ([]byte, error)
-	Write(data []byte) error
-
-	Reverse() error
-
-	PollFDs() ([]int, error)
-	NetemFDs() ([]int, error)
-
-	Details() string
-
-	SetLogger(l Logger)
-}
-
-type NodeConfig struct {
-	Type string `json:"type"`
-
-	In struct{} `json:"in"`
-
-	Out struct{} `json:"out"`
-}
diff --git a/go/pkg/nodes/registry.go b/go/pkg/nodes/registry.go
deleted file mode 100644
index f3afcba9d..000000000
--- a/go/pkg/nodes/registry.go
+++ /dev/null
@@ -1,28 +0,0 @@
-/** Node-type registry.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package nodes
-
-var goNodeTypes = map[string]NodeType{}
-
-type NodeType struct {
-	Constructor NodeConstructor
-	Flags       int
-	Description string
-}
-
-func RegisterNodeType(name string, desc string, ctor NodeConstructor, flags int) {
-	goNodeTypes[name] = NodeType{
-		Constructor: ctor,
-		Flags:       flags,
-		Description: desc,
-	}
-}
-
-func NodeTypes() map[string]NodeType {
-	return goNodeTypes
-}
diff --git a/go/pkg/nodes/webrtc/backoff.go b/go/pkg/nodes/webrtc/backoff.go
deleted file mode 100644
index c8b4b8f2f..000000000
--- a/go/pkg/nodes/webrtc/backoff.go
+++ /dev/null
@@ -1,38 +0,0 @@
-/** Exponential backoffs for reconnect timing.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package webrtc
-
-import "time"
-
-var DefaultExponentialBackoff = ExponentialBackoff{
-	Factor:   1.5,
-	Maximum:  1 * time.Minute,
-	Initial:  1 * time.Second,
-	Duration: 1 * time.Second,
-}
-
-type ExponentialBackoff struct {
-	Factor  float32
-	Maximum time.Duration
-	Initial time.Duration
-
-	Duration time.Duration
-}
-
-func (e *ExponentialBackoff) Next() time.Duration {
-	e.Duration = time.Duration(1.5 * float32(e.Duration)).Round(time.Second)
-	if e.Duration > e.Maximum {
-		e.Duration = e.Maximum
-	}
-
-	return e.Duration
-}
-
-func (e *ExponentialBackoff) Reset() {
-	e.Duration = e.Initial
-}
diff --git a/go/pkg/nodes/webrtc/client.go b/go/pkg/nodes/webrtc/client.go
deleted file mode 100644
index 4f25f84f7..000000000
--- a/go/pkg/nodes/webrtc/client.go
+++ /dev/null
@@ -1,173 +0,0 @@
-/** Websocket signaling channel for WebRTC.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package webrtc
-
-import (
-	"fmt"
-	"net/url"
-	"time"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes"
-	"github.com/gorilla/websocket"
-)
-
-type SignalingClient struct {
-	*websocket.Conn
-
-	logger nodes.Logger
-
-	URL *url.URL
-
-	done chan struct{}
-
-	isClosing bool
-	backoff   ExponentialBackoff
-
-	messageCallbacks    []func(msg *SignalingMessage)
-	connectCallbacks    []func()
-	disconnectCallbacks []func()
-}
-
-func NewSignalingClient(u *url.URL, logger nodes.Logger) (*SignalingClient, error) {
-	c := &SignalingClient{
-		messageCallbacks:    []func(msg *SignalingMessage){},
-		connectCallbacks:    []func(){},
-		disconnectCallbacks: []func(){},
-		isClosing:           false,
-		backoff:             DefaultExponentialBackoff,
-		URL:                 u,
-		logger:              logger,
-	}
-
-	return c, nil
-}
-
-func (c *SignalingClient) OnConnect(cb func()) {
-	c.connectCallbacks = append(c.connectCallbacks, cb)
-}
-
-func (c *SignalingClient) OnDisconnect(cb func()) {
-	c.disconnectCallbacks = append(c.connectCallbacks, cb)
-}
-
-func (c *SignalingClient) OnMessage(cb func(msg *SignalingMessage)) {
-	c.messageCallbacks = append(c.messageCallbacks, cb)
-}
-
-func (c *SignalingClient) SendSignalingMessage(msg *SignalingMessage) error {
-	c.logger.Infof("Sending signaling message: %s", msg)
-	return c.Conn.WriteJSON(msg)
-}
-
-func (c *SignalingClient) Close() error {
-	// Return immediatly if there is no open connection
-	if c.Conn == nil {
-		return nil
-	}
-
-	c.isClosing = true
-
-	// Cleanly close the connection by sending a close message and then
-	// waiting (with timeout) for the server to close the connection.
-	err := c.Conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(5*time.Second))
-	if err != nil {
-		return fmt.Errorf("failed to send close message: %s", err)
-	}
-
-	select {
-	case <-c.done:
-		c.logger.Infof("Connection closed")
-	case <-time.After(3 * time.Second):
-		c.logger.Warn("Timed-out waiting for connection close")
-	}
-
-	return nil
-}
-
-func (c *SignalingClient) Connect() error {
-	var err error
-
-	dialer := websocket.Dialer{
-		HandshakeTimeout: 1 * time.Second,
-	}
-
-	c.Conn, _, err = dialer.Dial(c.URL.String(), nil)
-	if err != nil {
-		return fmt.Errorf("failed to dial %s: %w", c.URL, err)
-	}
-
-	for _, cb := range c.connectCallbacks {
-		cb()
-	}
-
-	go c.read()
-
-	// Reset reconnect timer
-	c.backoff.Reset()
-
-	c.done = make(chan struct{})
-
-	return nil
-}
-
-func (c *SignalingClient) ConnectWithBackoff() error {
-	t := time.NewTimer(c.backoff.Duration)
-	for range t.C {
-		if err := c.Connect(); err != nil {
-			t.Reset(c.backoff.Next())
-
-			c.logger.Errorf("Failed to connect: %s. Reconnecting in %s", err, c.backoff.Duration)
-		} else {
-			break
-		}
-	}
-
-	return nil
-}
-
-func (c *SignalingClient) read() {
-	for {
-		msg := &SignalingMessage{}
-		if err := c.Conn.ReadJSON(msg); err != nil {
-			if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
-			} else {
-				c.logger.Errorf("Failed to read: %s", err)
-			}
-			break
-		}
-
-		c.logger.Infof("Received signaling message: %s", msg)
-
-		for _, cb := range c.messageCallbacks {
-			cb(msg)
-		}
-	}
-
-	c.closed()
-}
-
-func (c *SignalingClient) closed() {
-	if err := c.Conn.Close(); err != nil {
-		c.logger.Errorf("Failed to close connection: %s", err)
-	}
-
-	c.Conn = nil
-
-	for _, cb := range c.disconnectCallbacks {
-		cb()
-	}
-
-	close(c.done)
-
-	if c.isClosing {
-		c.logger.Infof("Connection closed")
-	} else {
-		c.logger.Warnf("Connection lost. Reconnecting in %s", c.backoff.Duration)
-		go c.ConnectWithBackoff()
-	}
-}
diff --git a/go/pkg/nodes/webrtc/node.go b/go/pkg/nodes/webrtc/node.go
deleted file mode 100644
index 02d7ae761..000000000
--- a/go/pkg/nodes/webrtc/node.go
+++ /dev/null
@@ -1,220 +0,0 @@
-/** WebRTC node-type.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package webrtc
-
-import (
-	"encoding/json"
-	"errors"
-	"fmt"
-	"net/url"
-	"strings"
-
-	verrors "git.rwth-aachen.de/acs/public/villas/node/go/pkg/errors"
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes"
-	"github.com/pion/webrtc/v3"
-)
-
-var DefaultConfig = Config{
-	Server: &url.URL{
-		Scheme: "wss",
-		Host:   "villas.k8s.eonerc.rwth-aachen.de",
-		Path:   "/ws/signaling",
-	},
-	Wait:           true,
-	MaxRetransmits: 0,
-	Ordered:        false,
-	WebRTC: webrtc.Configuration{
-		ICEServers: []webrtc.ICEServer{
-			{
-				URLs: []string{"stun:stun.l.google.com:19302"},
-			},
-			{
-				URLs: []string{
-					"stun:stun.0l.de",
-				},
-				CredentialType: webrtc.ICECredentialTypePassword,
-				Username:       "villas",
-				Credential:     "villas",
-			},
-			{
-				URLs: []string{
-					"turn:turn.0l.de?transport=udp",
-					"turn:turn.0l.de?transport=tcp",
-				},
-				CredentialType: webrtc.ICECredentialTypePassword,
-				Username:       "villas",
-				Credential:     "villas",
-			},
-		},
-	},
-}
-
-type Node struct {
-	nodes.BaseNode
-	*PeerConnection
-
-	Config Config
-}
-
-type Config struct {
-	Server  *url.URL
-	Session string
-
-	Wait           bool
-	MaxRetransmits uint16
-	Ordered        bool
-
-	WebRTC webrtc.Configuration
-}
-
-func NewNode() nodes.Node {
-	return &Node{
-		Config: DefaultConfig,
-	}
-}
-
-func (n *Node) Parse(c []byte) error {
-	var err error
-	var cfg struct {
-		Session        *string `json:"session"`
-		Server         *string `json:"server,omitempty"`
-		Wait           *bool   `json:"wait,omitemty"`
-		MaxRetransmits *uint16 `json:"max_retransmits,omitempty"`
-		Ordered        *bool   `json:"ordered,omitempty"`
-		Ice            *struct {
-			Servers []struct {
-				URLs     []string `json:"urls,omitempty"`
-				Username *string  `json:"username,omitempty"`
-				Password *string  `json:"password,omitempty"`
-			} `json:"servers,omitempty"`
-		} `json:"ice,omitempty"`
-	}
-
-	if err := json.Unmarshal(c, &cfg); err != nil {
-		return fmt.Errorf("failed to unmarshal config: %w", err)
-	}
-
-	if cfg.Wait != nil {
-		n.Config.Wait = *cfg.Wait
-	}
-
-	if cfg.Ordered != nil {
-		n.Config.Ordered = *cfg.Ordered
-	}
-
-	if cfg.MaxRetransmits != nil {
-		n.Config.MaxRetransmits = *cfg.MaxRetransmits
-	}
-
-	if cfg.Session == nil || *cfg.Session == "" {
-		return errors.New("missing or invalid session name")
-	} else {
-		n.Config.Session = *cfg.Session
-	}
-
-	if cfg.Server != nil {
-		n.Config.Server, err = url.Parse(*cfg.Server)
-		if err != nil {
-			return fmt.Errorf("failed to parse server address: %w", err)
-		}
-	}
-
-	if cfg.Ice != nil {
-		for _, server := range cfg.Ice.Servers {
-			iceServer := webrtc.ICEServer{
-				URLs: server.URLs,
-			}
-
-			if server.Username != nil && server.Password != nil {
-				iceServer.Username = *server.Username
-				iceServer.CredentialType = webrtc.ICECredentialTypePassword
-				iceServer.Credential = *server.Password
-			}
-
-			n.Config.WebRTC.ICEServers = append(n.Config.WebRTC.ICEServers, iceServer)
-		}
-	}
-
-	return nil
-}
-
-func (n *Node) Prepare() error {
-	var err error
-
-	n.PeerConnection, err = NewPeerConnection(&n.Config, n.Logger)
-	if err != nil {
-		return fmt.Errorf("failed to create peer connection: %w", err)
-	}
-
-	return nil
-}
-
-func (n *Node) Start() error {
-	n.DataChannelLock.Lock()
-	defer n.DataChannelLock.Unlock()
-
-	if n.Config.Wait {
-		n.Logger.Info("Waiting until datachannel is connected...")
-		for n.DataChannel == nil {
-			n.DataChannelConnected.Wait()
-		}
-	}
-
-	return n.BaseNode.Start()
-}
-
-func (n *Node) Read() ([]byte, error) {
-	select {
-	case <-n.Stopped:
-		return nil, verrors.ErrEndOfFile
-	case b := <-n.PeerConnection.ReceivedMessages:
-		return b, nil
-	}
-}
-
-func (n *Node) Write(data []byte) error {
-	n.DataChannelLock.Lock()
-	defer n.DataChannelLock.Unlock()
-
-	if n.DataChannel == nil {
-		n.logger.Infof("No datachannel open. Skipping sample...")
-		return nil
-	}
-
-	return n.DataChannel.Send(data)
-}
-
-func (n *Node) PollFDs() ([]int, error) {
-	return []int{}, nil
-}
-
-func (n *Node) NetemFDs() ([]int, error) {
-	return []int{}, nil
-}
-
-func (n *Node) Details() string {
-	details := map[string]string{
-		"server":  n.Config.Server.String(),
-		"session": n.Config.Session,
-	}
-
-	kv := []string{}
-	for k, v := range details {
-		kv = append(kv, fmt.Sprintf("%s=%s", k, v))
-	}
-
-	return strings.Join(kv, ", ")
-}
-
-func (n *Node) Close() error {
-	return nil
-}
-
-func init() {
-	nodes.RegisterNodeType("webrtc", "Web Real-time Communication", NewNode, nodes.NodeSupportsRead|nodes.NodeSupportsWrite)
-}
diff --git a/go/pkg/nodes/webrtc/types.go b/go/pkg/nodes/webrtc/types.go
deleted file mode 100644
index d67cdc289..000000000
--- a/go/pkg/nodes/webrtc/types.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/** Types for WebRTC node-type.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package webrtc
-
-import (
-	"encoding/json"
-	"time"
-
-	"github.com/pion/webrtc/v3"
-)
-
-type Connection struct {
-	ID int `json:"id"`
-
-	Remote    string    `json:"remote"`
-	UserAgent string    `json:"user_agent"`
-	Created   time.Time `json:"created"`
-}
-
-type ControlMessage struct {
-	ConnectionID int          `json:"connection_id"`
-	Connections  []Connection `json:"connections"`
-}
-
-type Role struct {
-	Polite bool `json:"polite"`
-	First  bool `json:"first"`
-}
-
-type SignalingMessage struct {
-	Description *webrtc.SessionDescription `json:"description,omitempty"`
-	Candidate   *webrtc.ICECandidateInit   `json:"candidate,omitempty"`
-	Control     *ControlMessage            `json:"control,omitempty"`
-}
-
-func (msg SignalingMessage) String() string {
-	b, _ := json.Marshal(msg)
-	return string(b)
-}
diff --git a/go/pkg/nodes/webrtc/webrtc.go b/go/pkg/nodes/webrtc/webrtc.go
deleted file mode 100644
index bd613abe2..000000000
--- a/go/pkg/nodes/webrtc/webrtc.go
+++ /dev/null
@@ -1,341 +0,0 @@
-/** WebRTC peer connection handling.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package webrtc
-
-import (
-	"fmt"
-	"path"
-	"sync"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/nodes"
-	"github.com/pion/webrtc/v3"
-)
-
-type PeerConnection struct {
-	*webrtc.PeerConnection
-	*SignalingClient
-
-	config *Config
-
-	logger nodes.Logger
-
-	makingOffer bool
-	ignoreOffer bool
-
-	first  bool
-	polite bool
-
-	rollback bool
-
-	DataChannel          *webrtc.DataChannel
-	DataChannelLock      sync.Mutex
-	DataChannelConnected *sync.Cond
-
-	ReceivedMessages chan []byte
-}
-
-func NewPeerConnection(config *Config, logger nodes.Logger) (*PeerConnection, error) {
-	u := *config.Server
-	u.Path = path.Join(u.Path, config.Session)
-	sc, err := NewSignalingClient(&u, logger)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create signaling client: %w", err)
-	}
-
-	ppc := &PeerConnection{
-		SignalingClient: sc,
-		config:          config,
-		logger:          logger,
-
-		DataChannel:      nil,
-		ReceivedMessages: make(chan []byte, 1024),
-	}
-
-	ppc.DataChannelConnected = sync.NewCond(&ppc.DataChannelLock)
-
-	ppc.SignalingClient.OnMessage(ppc.OnSignalingMessageHandler)
-	ppc.SignalingClient.OnConnect(ppc.OnSignalingConnectedHandler)
-
-	if err := ppc.SignalingClient.ConnectWithBackoff(); err != nil {
-		return nil, fmt.Errorf("failed to connect signaling client: %w", err)
-	}
-
-	return ppc, nil
-}
-
-func (pc *PeerConnection) createPeerConnection() (*webrtc.PeerConnection, error) {
-	pc.logger.Info("Created new peer connection")
-
-	// Create a new RTCPeerConnection
-	ppc, err := webrtc.NewPeerConnection(pc.config.WebRTC)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create peer connection: %w", err)
-	}
-
-	// Set the handler for ICE connection state
-	// This will notify you when the peer has connected/disconnected
-	ppc.OnConnectionStateChange(pc.OnConnectionStateChangeHandler)
-	ppc.OnSignalingStateChange(pc.OnSignalingStateChangeHandler)
-	ppc.OnICECandidate(pc.OnICECandidateHandler)
-	ppc.OnNegotiationNeeded(pc.OnNegotiationNeededHandler)
-	ppc.OnDataChannel(pc.OnDataChannelHandler)
-
-	return ppc, nil
-}
-
-func (pc *PeerConnection) createDataChannel() (*webrtc.DataChannel, error) {
-	dc, err := pc.CreateDataChannel("villas", &webrtc.DataChannelInit{
-		Ordered:        &pc.config.Ordered,
-		MaxRetransmits: &pc.config.MaxRetransmits,
-	})
-	if err != nil {
-		return nil, fmt.Errorf("failed to create datachannel: %w", err)
-	}
-
-	close := make(chan struct{})
-
-	dc.OnClose(func() { pc.OnDataChannelCloseHandler(dc, close) })
-	dc.OnOpen(func() { pc.OnDataChannelOpenHandler(dc, close) })
-	dc.OnMessage(func(msg webrtc.DataChannelMessage) { pc.OnDataChannelMessageHandler(dc, &msg, close) })
-
-	return dc, nil
-}
-
-func (pc *PeerConnection) rollbackPeerConnection() (*webrtc.PeerConnection, error) {
-	pc.rollback = true
-	defer func() { pc.rollback = false }()
-
-	// Close previous peer connection in before creating a new one
-	// We need to do this as pion/webrtc currently does not support rollbacks
-	if err := pc.PeerConnection.Close(); err != nil {
-		return nil, fmt.Errorf("failed to close peer connection: %w", err)
-	}
-
-	if ppc, err := pc.createPeerConnection(); err != nil {
-		return nil, err
-	} else {
-		return ppc, nil
-	}
-}
-
-func (pc *PeerConnection) OnConnectionCreated() error {
-	if !pc.first {
-		if _, err := pc.createDataChannel(); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (pc *PeerConnection) OnDataChannelOpenHandler(dc *webrtc.DataChannel, close chan struct{}) {
-	pc.logger.Infof("DataChannel opened: %s", dc.Label())
-
-	pc.DataChannelLock.Lock()
-	defer pc.DataChannelLock.Unlock()
-
-	pc.DataChannel = dc
-
-	pc.DataChannelConnected.Broadcast()
-}
-
-func (pc *PeerConnection) OnDataChannelCloseHandler(dc *webrtc.DataChannel, cl chan struct{}) {
-	pc.logger.Infof("DataChannel closed: %s", dc.Label())
-
-	pc.DataChannel = nil
-
-	close(cl)
-
-	// We close the connection here to avoid waiting for the disconnected event
-	if err := pc.PeerConnection.Close(); err != nil {
-		pc.logger.Errorf("Failed to close peer connection: %s", err)
-	}
-}
-
-func (pc *PeerConnection) OnDataChannelMessageHandler(dc *webrtc.DataChannel, msg *webrtc.DataChannelMessage, close chan struct{}) {
-	pc.logger.Debugf("Received: %s", string(msg.Data))
-
-	pc.ReceivedMessages <- msg.Data
-}
-
-func (pc *PeerConnection) OnDataChannelHandler(dc *webrtc.DataChannel) {
-	pc.logger.Infof("New DataChannel opened: %s", dc.Label())
-
-	close := make(chan struct{})
-
-	dc.OnOpen(func() { pc.OnDataChannelOpenHandler(dc, close) })
-	dc.OnClose(func() { pc.OnDataChannelCloseHandler(dc, close) })
-	dc.OnMessage(func(msg webrtc.DataChannelMessage) { pc.OnDataChannelMessageHandler(dc, &msg, close) })
-}
-
-func (pc *PeerConnection) OnICECandidateHandler(c *webrtc.ICECandidate) {
-	if c == nil {
-		pc.logger.Info("Candidate gathering concluded")
-		return
-	}
-
-	pc.logger.Infof("Found new candidate: %s", c)
-
-	ci := c.ToJSON()
-	if err := pc.SendSignalingMessage(&SignalingMessage{
-		Candidate: &ci,
-	}); err != nil {
-		pc.logger.Errorf("Failed to send candidate: %s", err)
-	}
-}
-
-func (pc *PeerConnection) OnNegotiationNeededHandler() {
-	pc.logger.Info("Negotation needed!")
-
-	pc.makingOffer = true
-
-	offer, err := pc.CreateOffer(nil)
-	if err != nil {
-		pc.logger.Panicf("Failed to create offer: %s", err)
-	}
-
-	if err := pc.SetLocalDescription(offer); err != nil {
-		pc.logger.Panicf("Failed to set local description: %s", err)
-	}
-
-	if err := pc.SendSignalingMessage(&SignalingMessage{
-		Description: &offer,
-	}); err != nil {
-		pc.logger.Panicf("Failed to send offer: %s", err)
-	}
-
-	pc.makingOffer = false
-}
-
-func (pc *PeerConnection) OnSignalingStateChangeHandler(ss webrtc.SignalingState) {
-	pc.logger.Infof("Signaling State has changed: %s", ss.String())
-}
-
-func (pc *PeerConnection) OnConnectionStateChangeHandler(pcs webrtc.PeerConnectionState) {
-	pc.logger.Infof("Connection State has changed: %s", pcs.String())
-
-	switch pcs {
-	case webrtc.PeerConnectionStateFailed:
-		fallthrough
-	case webrtc.PeerConnectionStateDisconnected:
-		pc.logger.Info("Closing peer connection")
-
-		if err := pc.PeerConnection.Close(); err != nil {
-			pc.logger.Panicf("Failed to close peer connection: %s", err)
-		}
-
-	case webrtc.PeerConnectionStateClosed:
-		if pc.rollback {
-			return
-		}
-
-		pc.logger.Info("Closed peer connection")
-
-		var err error
-		pc.PeerConnection, err = pc.createPeerConnection()
-		if err != nil {
-			pc.logger.Panicf("Failed to set re-create peer connection: %s", err)
-		}
-
-		if err := pc.OnConnectionCreated(); err != nil {
-			pc.logger.Panicf("Failed to create connection: %w", err)
-		}
-	}
-}
-
-func (pc *PeerConnection) OnSignalingConnectedHandler() {
-	var err error
-
-	pc.logger.Info("Signaling connected")
-
-	// Create initial peer connection
-	if pc.PeerConnection == nil {
-		if pc.PeerConnection, err = pc.createPeerConnection(); err != nil {
-			pc.logger.Panicf("Failed to create peer connection: %s", err)
-		}
-
-		if err := pc.OnConnectionCreated(); err != nil {
-			pc.logger.Panicf("Failed to create connection: %s", err)
-		}
-	}
-}
-
-func (pc *PeerConnection) OnSignalingMessageHandler(msg *SignalingMessage) {
-	var err error
-
-	if msg.Control != nil {
-		if len(msg.Control.Connections) > 2 {
-			pc.logger.Panicf("There are already two peers connected to this session.")
-		}
-
-		// The peer with the smallest connection ID connected first
-		pc.first = true
-		for _, c := range msg.Control.Connections {
-			if c.ID < msg.Control.ConnectionID {
-				pc.first = false
-			}
-		}
-
-		pc.polite = pc.first
-
-		pc.logger.Infof("New role: polite=%v, first=%v", pc.polite, pc.first)
-	} else if msg.Description != nil {
-		readyForOffer := !pc.makingOffer && pc.SignalingState() == webrtc.SignalingStateStable
-		offerCollision := msg.Description.Type == webrtc.SDPTypeOffer && !readyForOffer
-
-		pc.ignoreOffer = !pc.polite && offerCollision
-		if pc.ignoreOffer {
-			return
-		}
-
-		if msg.Description.Type == webrtc.SDPTypeOffer && pc.PeerConnection.SignalingState() != webrtc.SignalingStateStable {
-			if pc.PeerConnection, err = pc.rollbackPeerConnection(); err != nil {
-				pc.logger.Panicf("Failed to rollback peer connection: %s", err)
-			}
-
-			if err := pc.OnConnectionCreated(); err != nil {
-				pc.logger.Panicf("Failed to create connection: %s", err)
-			}
-		}
-
-		if err := pc.PeerConnection.SetRemoteDescription(*msg.Description); err != nil {
-			pc.logger.Panicf("Failed to set remote description: %s", err)
-		}
-
-		if msg.Description.Type == webrtc.SDPTypeOffer {
-			answer, err := pc.PeerConnection.CreateAnswer(nil)
-			if err != nil {
-				pc.logger.Panicf("Failed to create answer: %s", err)
-			}
-
-			if err := pc.SetLocalDescription(answer); err != nil {
-				pc.logger.Panicf("Failed to rollback signaling state: %s", err)
-			}
-
-			pc.SendSignalingMessage(&SignalingMessage{
-				Description: pc.LocalDescription(),
-			})
-		}
-	} else if msg.Candidate != nil {
-		if err := pc.AddICECandidate(*msg.Candidate); err != nil && !pc.ignoreOffer {
-			pc.logger.Panicf("Failed to add new ICE candidate: %s", err)
-		}
-	}
-}
-
-func (pc *PeerConnection) Close() error {
-	if err := pc.SignalingClient.Close(); err != nil {
-		return fmt.Errorf("failed to close signaling client: %w", err)
-	}
-
-	if err := pc.PeerConnection.Close(); err != nil {
-		return fmt.Errorf("failed to close peer connection: %w", err)
-	}
-
-	return nil
-}
diff --git a/go/pkg/sample.go b/go/pkg/sample.go
deleted file mode 100644
index a6efcb811..000000000
--- a/go/pkg/sample.go
+++ /dev/null
@@ -1,79 +0,0 @@
-/** Sample datastructure.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package pkg
-
-import (
-	"encoding/json"
-	"math/rand"
-	"time"
-)
-
-type Timestamp [2]int64
-
-func TimestampFromTime(t time.Time) Timestamp {
-	return Timestamp{t.Unix(), int64(t.Nanosecond())}
-}
-
-type Timestamps struct {
-	Origin   Timestamp `json:"origin"`
-	Received Timestamp `json:"received"`
-}
-
-type Sample struct {
-	Flags      int        `json:"-"`
-	Timestamps Timestamps `json:"ts"`
-	Sequence   uint64     `json:"sequence"`
-	Data       []float64  `json:"data"`
-}
-
-func GenerateRandomSample() Sample {
-	now := time.Now()
-
-	return Sample{
-		Timestamps: Timestamps{
-			Origin: TimestampFromTime(now),
-		},
-		Sequence: 1,
-		Data: []float64{
-			1 * rand.Float64(),
-			10 * rand.Float64(),
-			100 * rand.Float64(),
-		},
-	}
-}
-
-func (s Sample) Bytes() []byte {
-	b, _ := json.Marshal(s)
-	return b
-}
-
-func (s Sample) Equal(t Sample) bool {
-	if s.Flags != t.Flags {
-		return false
-	}
-
-	if s.Sequence != t.Sequence {
-		return false
-	}
-
-	if s.Timestamps != t.Timestamps {
-		return false
-	}
-
-	if len(s.Data) != len(t.Data) {
-		return false
-	}
-
-	for i, va := range s.Data {
-		if vb := t.Data[i]; va != vb {
-			return false
-		}
-	}
-
-	return true
-}
diff --git a/include/villas/node/config.hpp.in b/include/villas/node/config.hpp.in
index f1d3d30d3..68e9a245a 100644
--- a/include/villas/node/config.hpp.in
+++ b/include/villas/node/config.hpp.in
@@ -37,6 +37,7 @@
 /* Available Features */
 #cmakedefine WITH_WEB
 #cmakedefine WITH_NODE_WEBSOCKET
+#cmakedefine WITH_NODE_WEBRTC
 #cmakedefine WITH_NODE_OPAL
 #cmakedefine WITH_API
 #cmakedefine WITH_HOOKS
diff --git a/include/villas/nodes/webrtc.hpp b/include/villas/nodes/webrtc.hpp
new file mode 100644
index 000000000..c9bede10e
--- /dev/null
+++ b/include/villas/nodes/webrtc.hpp
@@ -0,0 +1,120 @@
+/** Node-type webrtc.
+ *
+ * @file
+ * @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
+ * @author Philipp Jungkamp <Philipp.Jungkamp@opal-rt.com>
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @copyright 2023, OPAL-RT Germany GmbH
+ * @license Apache 2.0
+ *********************************************************************************/
+
+#pragma once
+
+#include <rtc/rtc.hpp>
+
+#include <villas/nodes/webrtc/peer_connection.hpp>
+#include <villas/node/config.hpp>
+#include <villas/node.hpp>
+#include <villas/timing.hpp>
+#include <villas/format.hpp>
+#include <villas/queue_signalled.h>
+
+namespace villas {
+namespace node {
+
+/* Forward declarations */
+struct Sample;
+
+class WebRTCNode : public Node {
+
+protected:
+	std::string server;
+	std::string session;
+
+	int wait_seconds;
+	Format *format;
+	struct CQueueSignalled queue;
+	struct Pool pool;
+
+	std::shared_ptr<webrtc::PeerConnection> conn;
+	rtc::Configuration rtcConf;
+	rtc::DataChannelInit dci;
+
+	virtual
+	int _read(struct Sample *smps[], unsigned cnt);
+
+	virtual
+	int _write(struct Sample *smps[], unsigned cnt);
+
+public:
+	WebRTCNode(const std::string &name = "");
+
+	virtual
+	~WebRTCNode();
+
+	virtual
+	int prepare();
+
+	virtual
+	int parse(json_t *json, const uuid_t sn_uuid);
+
+	virtual
+	int check();
+
+	virtual
+	int start();
+
+	virtual
+	int stop();
+
+	virtual
+	std::vector<int> getPollFDs();
+
+	virtual
+	const std::string & getDetails();
+};
+
+
+class WebRTCNodeFactory : public NodeFactory {
+
+public:
+	using NodeFactory::NodeFactory;
+
+	virtual
+	Node * make()
+	{
+		auto *n = new WebRTCNode();
+
+		init(n);
+
+		return n;
+	}
+
+	virtual
+	int getFlags() const
+	{
+		return (int) NodeFactory::Flags::SUPPORTS_READ |
+		       (int) NodeFactory::Flags::SUPPORTS_WRITE |
+		       (int) NodeFactory::Flags::SUPPORTS_POLL |
+		       (int) NodeFactory::Flags::REQUIRES_WEB;
+	}
+
+	virtual
+	std::string getName() const
+	{
+		return "webrtc";
+	}
+
+	virtual
+	std::string getDescription() const
+	{
+		return "Web Real-time Communication";
+	}
+
+	virtual
+	int start(SuperNode *sn);
+};
+
+
+} /* namespace node */
+} /* namespace villas */
diff --git a/include/villas/nodes/webrtc/peer_connection.hpp b/include/villas/nodes/webrtc/peer_connection.hpp
new file mode 100644
index 000000000..6dd8f94cb
--- /dev/null
+++ b/include/villas/nodes/webrtc/peer_connection.hpp
@@ -0,0 +1,91 @@
+/** WebRTC peer connection
+ *
+ * @file
+ * @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
+ * @author Philipp Jungkamp <Philipp.Jungkamp@opal-rt.com>
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @copyright 2023, OPAL-RT Germany GmbH
+ * @license Apache 2.0
+ *********************************************************************************/
+
+#pragma once
+
+#include <rtc/rtc.hpp>
+
+#include <villas/log.hpp>
+#include <villas/web.hpp>
+#include <villas/nodes/webrtc/signaling_client.hpp>
+
+namespace villas {
+namespace node {
+namespace webrtc {
+
+class PeerConnection {
+
+public:
+	PeerConnection(const std::string &server, const std::string &session, rtc::Configuration config, Web *w, rtc::DataChannelInit d);
+	~PeerConnection();
+
+	bool waitForDataChannel(std::chrono::seconds timeout);
+	void onMessage(std::function<void(rtc::binary)> callback);
+	void sendMessage(rtc::binary msg);
+
+	void connect();
+	void disconnect();
+
+protected:
+	Web *web;
+	std::vector<rtc::IceServer> extraServers;
+	rtc::DataChannelInit dataChannelInit;
+	rtc::Configuration defaultConfig;
+
+	std::shared_ptr<rtc::PeerConnection> conn;
+	std::shared_ptr<rtc::DataChannel> chan;
+	std::shared_ptr<SignalingClient> client;
+
+	Logger logger;
+
+	std::mutex mutex;
+
+	std::condition_variable_any startupCondition;
+	bool stopStartup;
+
+	bool warnNotConnected;
+	bool standby;
+	bool first;
+	int firstID;
+	int secondID;
+
+	std::function<void(rtc::binary)> onMessageCallback;
+
+	void resetConnection(std::unique_lock<decltype(PeerConnection::mutex)> &lock);
+	void resetConnectionAndStandby(std::unique_lock<decltype(PeerConnection::mutex)> &lock);
+	void notifyStartup();
+
+	void setupPeerConnection(std::shared_ptr<rtc::PeerConnection> = nullptr);
+	void setupDataChannel(std::shared_ptr<rtc::DataChannel> = nullptr);
+
+	void onLocalDescription(rtc::Description sdp);
+	void onLocalCandidate(rtc::Candidate cand);
+
+	void onConnectionStateChange(rtc::PeerConnection::State state);
+	void onSignalingStateChange(rtc::PeerConnection::SignalingState state);
+	void onGatheringStateChange(rtc::PeerConnection::GatheringState state);
+
+	void onSignalingConnected();
+	void onSignalingDisconnected();
+	void onSignalingError(std::string err);
+	void onSignalingMessage(SignalingMessage msg);
+
+	void onDataChannel(std::shared_ptr<rtc::DataChannel> dc);
+	void onDataChannelOpen();
+	void onDataChannelClosed();
+	void onDataChannelError(std::string err);
+	void onDataChannelMessage(rtc::string msg);
+	void onDataChannelMessage(rtc::binary msg);
+};
+
+} /* namespace webrtc */
+} /* namespace node */
+} /* namespace villas */
+
diff --git a/include/villas/nodes/webrtc/signaling_client.hpp b/include/villas/nodes/webrtc/signaling_client.hpp
new file mode 100644
index 000000000..6e54df87e
--- /dev/null
+++ b/include/villas/nodes/webrtc/signaling_client.hpp
@@ -0,0 +1,117 @@
+/** WebRTC signaling client
+ *
+ * @file
+ * @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
+ * @author Philipp Jungkamp <Philipp.Jungkamp@opal-rt.com>
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @copyright 2023, OPAL-RT Germany GmbH
+ * @license Apache 2.0
+ *********************************************************************************/
+
+#pragma once
+
+#include <functional>
+#include <chrono>
+
+#include <libwebsockets.h>
+
+#include <villas/queue.hpp>
+#include <villas/web.hpp>
+#include <villas/log.hpp>
+#include <villas/nodes/webrtc/signaling_message.hpp>
+
+namespace villas {
+namespace node {
+
+/* Forward declarations */
+class Web;
+
+namespace webrtc {
+
+class SignalingClient {
+
+protected:
+	struct sul_offsetof_helper {
+		lws_sorted_usec_list_t sul;	/**> Schedule connection retry */
+		SignalingClient *self;
+	} sul_helper;
+
+	uint16_t retry_count;			/**> Count of consecutive retries */
+
+	struct lws *wsi;
+	struct lws_client_connect_info info;
+
+	/* The retry and backoff policy we want to use for our client connections */
+	static constexpr uint32_t backoff_ms[] = { 1<<4, 1<<6, 1<<8, 1<<10, 1<<12, 1<<14, 1<<16 };
+	static constexpr lws_retry_bo_t retry = {
+		.retry_ms_table			= backoff_ms,
+		.retry_ms_table_count		= LWS_ARRAY_SIZE(backoff_ms),
+		.conceal_count			= LWS_ARRAY_SIZE(backoff_ms) + 1,
+
+		.secs_since_valid_ping		= 3,  /* force PINGs after secs idle */
+		.secs_since_valid_hangup	= 10, /* hangup after secs idle */
+
+		.jitter_percent			= 20,
+	};
+
+	std::function<void(SignalingMessage)> cbMessage;
+	std::function<void()> cbConnected;
+	std::function<void()> cbDisconnected;
+	std::function<void(std::string)> cbError;
+
+	Queue<SignalingMessage> outgoingMessages;
+
+	Web *web;
+
+	char *uri;
+	char *path;
+
+	std::atomic<bool> running;
+
+	Logger logger;
+
+	int protocolCallback(struct lws *wsi, enum lws_callback_reasons reason, void *in, size_t len);
+
+	static
+	void connectStatic(struct lws_sorted_usec_list *sul);
+
+	int receive(void *in, size_t len);
+	int writable();
+
+public:
+	SignalingClient(const std::string &server, const std::string &session, Web *w);
+	~SignalingClient();
+
+	static
+	int protocolCallbackStatic(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
+
+	void connect();
+	void disconnect();
+
+	void sendMessage(SignalingMessage);
+
+	void onMessage(std::function<void(SignalingMessage)> callback)
+	{
+		cbMessage = callback;
+	}
+
+	void onConnected(std::function<void()> callback)
+	{
+		cbConnected = callback;
+	}
+
+	void onDisconnected(std::function<void()> callback)
+	{
+		cbDisconnected = callback;
+	}
+
+	void onError(std::function<void(std::string)> callback)
+	{
+		cbError = callback;
+	}
+};
+
+} /* namespace webrtc */
+} /* namespace node */
+} /* namespace villas */
+
diff --git a/include/villas/nodes/webrtc/signaling_message.hpp b/include/villas/nodes/webrtc/signaling_message.hpp
new file mode 100644
index 000000000..711bdac56
--- /dev/null
+++ b/include/villas/nodes/webrtc/signaling_message.hpp
@@ -0,0 +1,59 @@
+/** WebRTC signaling messages.
+ *
+ * @file
+ * @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
+ * @author Philipp Jungkamp <Philipp.Jungkamp@opal-rt.com>
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @copyright 2023, OPAL-RT Germany GmbH
+ * @license Apache 2.0
+ *********************************************************************************/
+
+#pragma once
+
+#include <string>
+#include <chrono>
+#include <vector>
+#include <variant>
+
+#include <rtc/rtc.hpp>
+#include <jansson.h>
+
+namespace villas {
+namespace node {
+namespace webrtc {
+
+struct Connection {
+	int id;
+	std::string remote;
+	std::string userAgent;
+	std::chrono::time_point<std::chrono::system_clock> created;
+
+	Connection(json_t *j);
+	json_t * toJSON() const;
+};
+
+struct RelayMessage {
+	std::vector<rtc::IceServer> servers;
+
+	RelayMessage(json_t *j);
+};
+
+struct ControlMessage {
+	int connectionID;
+	std::vector<Connection> connections;
+
+	ControlMessage(json_t *j);
+	json_t * toJSON() const;
+};
+
+struct SignalingMessage {
+	std::variant<std::monostate, RelayMessage, ControlMessage, rtc::Description, rtc::Candidate> message;
+
+	static SignalingMessage fromJSON(json_t *j);
+	json_t * toJSON() const;
+	std::string toString() const;
+};
+
+} /* namespace webrtc */
+} /* namespace node */
+} /* namespace villas */
diff --git a/lib/nodes/CMakeLists.txt b/lib/nodes/CMakeLists.txt
index c0c526853..a0b38ecba 100644
--- a/lib/nodes/CMakeLists.txt
+++ b/lib/nodes/CMakeLists.txt
@@ -13,12 +13,6 @@ if(WITH_WEB)
     list(APPEND NODE_SRC api.cpp)
 endif()
 
-# Enable Golang support
-if(WITH_NODE_GO)
-    list(APPEND NODE_SRC go.cpp)
-    list(APPEND LIBRARIES villas-go)
-endif()
-
 if(LIBNL3_ROUTE_FOUND)
     list(APPEND LIBRARIES PkgConfig::LIBNL3_ROUTE)
 endif()
@@ -186,12 +180,12 @@ if(WITH_NODE_REDIS)
 	list(APPEND LIBRARIES PkgConfig::HIREDIS PkgConfig::REDISPP)
 endif()
 
+# Enable WebRTC support
+if(WITH_NODE_WEBRTC)
+	list(APPEND NODE_SRC webrtc.cpp webrtc/signaling_client.cpp webrtc/signaling_message.cpp webrtc/peer_connection.cpp)
+	list(APPEND LIBRARIES LibDataChannel::LibDataChannel)
+endif()
+
 add_library(nodes STATIC ${NODE_SRC})
 target_include_directories(nodes PUBLIC ${INCLUDE_DIRS})
 target_link_libraries(nodes PUBLIC ${LIBRARIES})
-
-if(WITH_NODE_GO)
-    add_dependencies(nodes villas-go-header)
-    target_include_directories(nodes PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/../../go/lib)
-    target_link_libraries(nodes PUBLIC villas-go)
-endif()
diff --git a/lib/nodes/go.cpp b/lib/nodes/go.cpp
deleted file mode 100644
index 2790dc2eb..000000000
--- a/lib/nodes/go.cpp
+++ /dev/null
@@ -1,288 +0,0 @@
-/** Node-type implemeted in Go language
- *
- * @file
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-#include <villas/nodes/go.hpp>
-#include <villas/plugin.hpp>
-#include <villas/format.hpp>
-
-extern "C" {
-	#include <libvillas-go.h>
-	#include <villas/nodes/go.h>
-}
-
-using namespace villas;
-using namespace villas::node;
-
-void _go_register_node_factory(_go_plugin_list pl, char *name, char *desc, int flags)
-{
-	auto *plugins = (villas::plugin::List<> *) pl;
-	plugins->push_back(new villas::node::GoNodeFactory(name, desc, flags));
-}
-
-_go_logger * _go_logger_get(char *name)
-{
-	return (_go_logger *) villas::logging.get(name).get();
-}
-
-void _go_logger_log(_go_logger l, int level, char *msg)
-{
-	auto *log = (spdlog::logger *) l;
-	log->log((spdlog::level::level_enum) level, "{}", msg);
-}
-
-GoNode::GoNode(uintptr_t n) :
-	Node(),
-	node(n),
-	formatter(nullptr)
-{ }
-
-GoNode::~GoNode()
-{
-	GoNodeClose(node);
-}
-
-int GoNode::parse(json_t *json, const uuid_t sn_uuid)
-{
-	int ret = Node::parse(json, sn_uuid);
-	if (ret)
-		return ret;
-
-	GoNodeSetLogger(node, _go_logger_log, logger.get());
-
-	json_t *json_format = nullptr;
-	json_error_t err;
-
-	ret = json_unpack_ex(json, &err, 0, "{ s?: o }",
-		"format", &json_format
-	);
-	if (ret)
-		throw ConfigError(json, err, "node-config-node-format", "Failed to parse node configuration");
-
-	formatter = json_format
-			? FormatFactory::make(json_format)
-			: FormatFactory::make("json");
-	if (!formatter)
-		throw ConfigError(json_format, "node-config-node-format", "Invalid format configuration");
-
-	auto *cfg = json_dumps(json, JSON_COMPACT);
-	ret = GoNodeParse(node, cfg);
-	free(cfg);
-	return ret;
-}
-
-int GoNode::check()
-{
-	int ret = Node::check();
-	if (ret)
-		return ret;
-
-	return GoNodeCheck(node);
-}
-
-int GoNode::prepare()
-{
-	int ret = GoNodePrepare(node);
-	if (ret)
-		return ret;
-
-	return Node::prepare();
-}
-
-int GoNode::start()
-{
-	int ret;
-
-	assert(state == State::PREPARED ||
-	       state == State::PAUSED);
-
-	/* Initialize IO */
-	formatter->start(getInputSignals(false));
-
-	ret = GoNodeStart(node);
-	if (ret)
-		return ret;
-
-	ret = Node::start();
-	if (!ret)
-		state = State::STARTED;
-
-	return ret;
-}
-
-int GoNode::stop() {
-	int ret;
-
-	assert(state == State::STARTED ||
-	       state == State::PAUSED ||
-	       state == State::STOPPING);
-
-	ret = Node::stop();
-	if (ret)
-		return ret;
-
-	ret = GoNodeStop(node);
-	if (ret)
-		return ret;
-
-	return 0;
-}
-
-int GoNode::pause()
-{
-	int ret;
-
-	ret = Node::pause();
-	if (ret)
-		return ret;
-
-	ret = GoNodePause(node);
-	if (!ret)
-		state = State::PAUSED;
-
-	return ret;
-}
-
-int GoNode::resume()
-{
-	int ret;
-
-	ret = Node::resume();
-	if (ret)
-		return ret;
-
-	ret = GoNodeResume(node);
-	if (!ret)
-		state = State::STARTED;
-
-	return ret;
-}
-
-const std::string & GoNode::getDetails()
-{
-	if (_details.empty()) {
-		auto *d = GoNodeDetails(node);
-		_details = std::string(d);
-		free(d);
-	}
-
-	return _details;
-}
-
-std::vector<int> GoNode::getPollFDs()
-{
-	auto ret = GoNodeGetPollFDs(node);
-	if (ret.r1)
-		return {};
-
-	auto begin = (int *) ret.r0.data;
-	auto end = begin + ret.r0.len;
-
-	return std::vector<int>(begin, end);
-}
-
-std::vector<int> GoNode::getNetemFDs()
-{
-	auto ret = GoNodeGetNetemFDs(node);
-	if (ret.r1)
-		return {};
-
-	auto begin = (int *) ret.r0.data;
-	auto end = begin + ret.r0.len;
-
-	return std::vector<int>(begin, end);
-}
-
-int GoNode::_read(struct Sample * smps[], unsigned cnt)
-{
-	int ret;
-	char data[DEFAULT_FORMAT_BUFFER_LENGTH];
-	size_t rbytes;
-
-	auto d = GoNodeRead(node, data, sizeof(data));
-	if (d.r1)
-		return d.r1;
-
-	ret = formatter->sscan(data, d.r0, &rbytes, smps, cnt);
-	if (ret < 0 || (size_t) d.r0 != rbytes) {
-		logger->warn("Received invalid packet: ret={}, bytes={}, rbytes={}", ret, d.r0, rbytes);
-		return ret;
-	}
-
-	return ret;
-}
-
-int GoNode::_write(struct Sample * smps[], unsigned cnt)
-{
-	int ret;
-	char buf[DEFAULT_FORMAT_BUFFER_LENGTH];
-	size_t wbytes;
-
-	ret = formatter->sprint(buf, DEFAULT_FORMAT_BUFFER_LENGTH, &wbytes, smps, cnt);
-	if (ret < 0)
-		return ret;
-
-	GoSlice slice = {
-		data: buf,
-		len: GoInt(wbytes),
-		cap: DEFAULT_FORMAT_BUFFER_LENGTH
-	};
-
-	ret = GoNodeWrite(node, slice);
-	if (ret)
-		return ret;
-
-	return cnt;
-}
-
-int GoNode::restart()
-{
-	assert(state == State::STARTED);
-
-	logger->info("Restarting node");
-
-	return GoNodeRestart(node);
-}
-
-int GoNode::reverse()
-{
-	return GoNodeReverse(node);
-}
-
-
-Node * GoNodeFactory::make()
-{
-	auto nt = NewGoNode((char *) name.c_str());
-	if (!nt)
-		return nullptr;
-
-	auto *n = new GoNode(nt);
-
-	init(n);
-
-	GoNodeSetLogger(n->node, _go_logger_log, n->logger.get());
-
-	return n;
-}
-
-GoPluginRegistry::GoPluginRegistry() {
-	if (plugin::registry == nullptr)
-		plugin::registry = new plugin::Registry();
-
-	plugin::registry->addSubRegistry(this);
-}
-
-villas::plugin::List<> GoPluginRegistry::lookup()
-{
-	plugin::List<> plugins;
-
-	RegisterGoNodeTypes(_go_register_node_factory, &plugins);
-
-	return plugins;
-}
-
-static GoPluginRegistry pr;
diff --git a/lib/nodes/webrtc.cpp b/lib/nodes/webrtc.cpp
new file mode 100644
index 000000000..28ced7c30
--- /dev/null
+++ b/lib/nodes/webrtc.cpp
@@ -0,0 +1,272 @@
+/** Node-type: webrtc
+ *
+ * @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
+ * @author Philipp Jungkamp <Philipp.Jungkamp@opal-rt.com>
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @copyright 2023, OPAL-RT Germany GmbH
+ * @license Apache 2.0
+ *********************************************************************************/
+
+#include <vector>
+
+#include <villas/node_compat.hpp>
+#include <villas/nodes/webrtc.hpp>
+#include <villas/utils.hpp>
+#include <villas/sample.hpp>
+#include <villas/exceptions.hpp>
+#include <villas/super_node.hpp>
+#include <villas/exceptions.hpp>
+
+using namespace villas;
+using namespace villas::node;
+using namespace villas::utils;
+
+static villas::node::Web *web;
+
+WebRTCNode::WebRTCNode(const std::string &name) :
+	Node(name),
+	server("wss://villas.k8s.eonerc.rwth-aachen.de/ws/signaling"),
+	wait_seconds(0),
+	format(nullptr),
+	queue({}),
+	pool({}),
+	dci({})
+{
+	dci.reliability.type = rtc::Reliability::Type::Rexmit;
+}
+
+WebRTCNode::~WebRTCNode()
+{
+	int ret = pool_destroy(&pool);
+	if (ret) // TODO log
+		;
+}
+
+int WebRTCNode::parse(json_t *json, const uuid_t sn_uuid)
+{
+	int ret = Node::parse(json, sn_uuid);
+	if (ret)
+		return ret;
+
+	const char *sess;
+	const char *svr = nullptr;
+	int ord = -1;
+	int &rexmit = dci.reliability.rexmit.emplace<int>(0);
+	json_t *ice_json = nullptr;
+	json_t *fmt_json = nullptr;
+
+	json_error_t err;
+	ret = json_unpack_ex(json, &err, 0, "{ s:s, s?s, s?i, s?i, s?b, s?o }",
+		"session", &sess,
+		"server", &svr,
+		"wait_seconds", &wait_seconds,
+		"max_retransmits", &rexmit,
+		"ordered", &ord,
+		"ice", &ice_json,
+		"format", &fmt_json
+	);
+	if (ret)
+		throw ConfigError(json, err, "node-config-node-webrtc");
+
+	session = sess;
+
+	if (svr)
+		server = svr;
+
+	if (ord)
+		dci.reliability.unordered = !ord;
+
+	if (ice_json) {
+		json_t *json_servers = nullptr;
+
+		ret = json_unpack_ex(ice_json, &err, 0, "{ s?: o }",
+			"servers", &json_servers
+		);
+		if (ret)
+			throw ConfigError(json, err, "node-config-node-webrtc-ice");
+
+		if (json_servers) {
+			rtcConf.iceServers.clear();
+
+			if (!json_is_array(json_servers))
+				throw ConfigError(json_servers, "node-config-node-webrtc-ice-servers", "ICE Servers must be a an array of server configurations.");
+
+			size_t i;
+			json_t *json_server;
+			json_array_foreach(json_servers, i, json_server) {
+				if (!json_is_string(json_server))
+					throw ConfigError(json_server, "node-config-node-webrtc-ice-server", "ICE servers must be provided as STUN/TURN url.");
+
+				std::string uri = json_string_value(json_server);
+
+				rtcConf.iceServers.emplace_back(uri);
+			}
+		}
+	}
+
+	format = fmt_json
+		? FormatFactory::make(fmt_json)
+		: FormatFactory::make("villas.binary");
+
+	assert(format);
+
+	return 0;
+}
+
+int WebRTCNode::check()
+{
+	return Node::check();
+}
+
+int WebRTCNode::prepare()
+{
+	int ret = Node::prepare();
+	if (ret)
+		return ret;
+
+	format->start(getInputSignals(false), ~(int) SampleFlags::HAS_OFFSET);
+
+	conn = std::make_shared<webrtc::PeerConnection>(server, session, rtcConf, web, dci);
+
+	ret = pool_init(&pool, 1024, SAMPLE_LENGTH(getInputSignals(false)->size()));
+	if (ret) // TODO log
+		return ret;
+
+	ret = queue_signalled_init(&queue, 1024);
+	if (ret) // TODO log
+		return ret;
+
+	conn->onMessage([this](rtc::binary msg){
+		int ret;
+		std::vector<Sample *> smps;
+		smps.resize(this->in.vectorize);
+
+		ret = sample_alloc_many(&this->pool, smps.data(), smps.size());
+		if (ret < 0) // TODO log
+			return;
+
+		ret = format->sscan((const char *)msg.data(), msg.size(), nullptr, smps.data(), ret);
+		if (ret < 0) // TODO log
+			return;
+
+		ret = queue_signalled_push_many(&this->queue, (void **) smps.data(), ret);
+		if (ret < 0) // TODO log
+			return;
+
+		this->logger->debug("onMessage(rtc::binary) callback finished pushing {} samples", ret);
+	});
+
+	return 0;
+}
+
+int WebRTCNode::start()
+{
+	int ret = Node::start();
+	if (!ret)
+		state = State::STARTED;
+
+	conn->connect();
+
+	if (wait_seconds > 0) {
+		logger->info("Waiting for datachannel...");
+
+		if (!conn->waitForDataChannel(std::chrono::seconds { wait_seconds })) {
+			throw RuntimeError { "Waiting for datachannel timed out after {} seconds", wait_seconds };
+		}
+	}
+
+	return 0;
+}
+
+int WebRTCNode::stop()
+{
+	conn->disconnect();
+	return Node::stop();
+}
+
+// int WebRTCNode::pause()
+// {
+// 	// TODO add implementation here
+// 	return 0;
+// }
+
+// int WebRTCNode::resume()
+// {
+// 	// TODO add implementation here
+// 	return 0;
+// }
+
+// int WebRTCNode::restart()
+// {
+// 	// TODO add implementation here
+// 	return 0;
+// }
+
+// int WebRTCNode::reverse()
+// {
+// 	// TODO add implementation here
+// 	return 0;
+// }
+
+std::vector<int> WebRTCNode::getPollFDs()
+{
+	return { queue_signalled_fd(&queue) };
+}
+
+// std::vector<int> WebRTCNode::getNetemFDs()
+// {
+// 	// TODO add implementation here
+// 	return {};
+// }
+
+// struct villas::node::memory::Type * WebRTCNode::getMemoryType()
+// {
+//	// TODO add implementation here
+// }
+
+const std::string & WebRTCNode::getDetails()
+{
+	details = fmt::format("");
+	return details;
+}
+
+int WebRTCNode::_read(struct Sample *smps[], unsigned cnt)
+{
+	std::vector<Sample *> smpt;
+	smpt.resize(cnt);
+
+	int pulled = queue_signalled_pull_many(&queue, (void **) smpt.data(), smpt.size());
+
+	sample_copy_many(smps, smpt.data(), pulled);
+	sample_decref_many(smpt.data(), pulled);
+
+	return pulled;
+}
+
+int WebRTCNode::_write(struct Sample *smps[], unsigned cnt)
+{
+	rtc::binary buf;
+	size_t wbytes;
+
+	buf.resize(4 * 1024);
+	int ret = format->sprint((char *) buf.data(), buf.size(), &wbytes, smps, cnt);
+	if (ret < 0) // TODO log
+		return ret;
+
+	buf.resize(wbytes);
+	conn->sendMessage(buf);
+
+	return ret;
+}
+
+int WebRTCNodeFactory::start(SuperNode *sn)
+{
+	web = sn->getWeb();
+	if (!web->isEnabled())
+		return -1;
+
+	return 0;
+}
+
+
+static WebRTCNodeFactory p;
diff --git a/lib/nodes/webrtc/peer_connection.cpp b/lib/nodes/webrtc/peer_connection.cpp
new file mode 100644
index 000000000..1c91ce955
--- /dev/null
+++ b/lib/nodes/webrtc/peer_connection.cpp
@@ -0,0 +1,411 @@
+/** WebRTC peer connection
+ *
+ * @file
+ * @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
+ * @author Philipp Jungkamp <Philipp.Jungkamp@opal-rt.com>
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @copyright 2023, OPAL-RT Germany GmbH
+ * @license Apache 2.0
+ *********************************************************************************/
+
+#include <chrono>
+#include <thread>
+#include <fmt/core.h>
+#include <fmt/ostream.h>
+#include <fmt/chrono.h>
+#include <villas/utils.hpp>
+#include <villas/exceptions.hpp>
+#include <villas/nodes/webrtc/peer_connection.hpp>
+
+using namespace std::placeholders;
+
+using namespace villas;
+using namespace villas::node;
+using namespace villas::node::webrtc;
+
+/*
+ * libdatachannel defines the operator<< overloads required to format
+ * rtc::PeerConnection::State and similar in the global namespace.
+ * But C++ ADL based overload set construction does not find these operators,
+ * if these are invoked in the spdlog/fmt libraries.
+ *
+ * See this issue for a short explaination of ADL errors:
+ * https://github.com/gabime/spdlog/issues/1227#issuecomment-532009129
+ *
+ * Adding the global ::operator<< overload set to the namespace rtc where
+ * the data structures are defined, allows ADL to pick these up in spdlog/fmt.
+ */
+namespace rtc {
+	using ::operator<<;
+}
+
+PeerConnection::PeerConnection(const std::string &server, const std::string &session, rtc::Configuration cfg, Web *w, rtc::DataChannelInit d) :
+	web(w),
+	dataChannelInit(d),
+	defaultConfig(cfg),
+	logger(logging.get("webrtc:pc"))
+{
+	client = std::make_shared<SignalingClient>(server, session, web);
+	client->onConnected([this](){ this->onSignalingConnected(); });
+	client->onDisconnected([this](){ this->onSignalingDisconnected(); });
+	client->onError([this](auto err){ this->onSignalingError(std::move(err)); });
+	client->onMessage([this](auto msg){ this->onSignalingMessage(std::move(msg)); });
+
+	auto lock = std::unique_lock { mutex };
+	resetConnectionAndStandby(lock);
+}
+
+PeerConnection::~PeerConnection()
+{
+}
+
+bool PeerConnection::waitForDataChannel(std::chrono::seconds timeout)
+{
+	auto lock = std::unique_lock { mutex };
+
+	auto deadline = std::chrono::steady_clock::now() + timeout;
+
+	return startupCondition.wait_until(lock, deadline, [this](){ return this->stopStartup; });
+}
+
+void PeerConnection::notifyStartup()
+{
+	stopStartup = true;
+	startupCondition.notify_all();
+}
+
+void PeerConnection::onMessage(std::function<void(rtc::binary)> callback)
+{
+	auto lock = std::unique_lock { mutex };
+
+	onMessageCallback = callback;
+}
+
+void PeerConnection::sendMessage(rtc::binary msg)
+{
+	auto lock = std::unique_lock { mutex };
+
+	if (chan && chan->isOpen()) {
+		chan->send(msg);
+		warnNotConnected = true;
+	} else if (warnNotConnected) {
+		logger->warn("Dropping messages. No peer connected.");
+		warnNotConnected = false;
+	}
+}
+
+void PeerConnection::connect()
+{
+	client->connect();
+}
+
+void PeerConnection::disconnect()
+{
+	client->disconnect();
+}
+
+void PeerConnection::resetConnection(std::unique_lock<decltype(PeerConnection::mutex)> &lock)
+{
+	lock.unlock();
+	chan.reset();
+	conn.reset();
+	lock.lock();
+}
+
+void PeerConnection::resetConnectionAndStandby(std::unique_lock<decltype(PeerConnection::mutex)> &lock)
+{
+	if (!standby)
+		logger->info("Going to standby");
+
+	standby = true;
+	first = false;
+	firstID = INT_MAX;
+	secondID = INT_MAX;
+	warnNotConnected = false;
+
+	resetConnection(lock);
+}
+
+void PeerConnection::setupPeerConnection(std::shared_ptr<rtc::PeerConnection> pc)
+{
+	logger->debug("Setup {} peer connection", pc ? "existing" : "new");
+
+	auto config = defaultConfig;
+	config.iceServers.insert(std::end(config.iceServers), std::begin(extraServers), std::end(extraServers));
+
+	conn = pc ? std::move(pc) : std::make_shared<rtc::PeerConnection>(config);
+	conn->onLocalDescription([this](auto desc){ this->onLocalDescription(std::move(desc)); });
+	conn->onLocalCandidate([this](auto cand){ this->onLocalCandidate(std::move(cand)); });
+	conn->onDataChannel([this](auto channel){ this->onDataChannel(std::move(channel)); });
+	conn->onGatheringStateChange([this](auto state){ this->onGatheringStateChange(std::move(state)); });
+	conn->onSignalingStateChange([this](auto state){ this->onSignalingStateChange(std::move(state)); });
+	conn->onStateChange([this](auto state){ this->onConnectionStateChange(std::move(state)); });
+}
+
+void PeerConnection::setupDataChannel(std::shared_ptr<rtc::DataChannel> dc)
+{
+	logger->debug("Setup {} data channel", dc ? "existing" : "new");
+
+	assert(conn);
+	chan = dc ? std::move(dc) : conn->createDataChannel("villas", dataChannelInit);
+	chan->onMessage(
+		[this](rtc::binary msg){ this->onDataChannelMessage(std::move(msg)); },
+		[this](rtc::string msg){ this->onDataChannelMessage(std::move(msg)); }
+	);
+	chan->onOpen([this](){ this->onDataChannelOpen(); });
+	chan->onClosed([this](){ this->onDataChannelClosed(); });
+	chan->onError([this](auto err){ this->onDataChannelError(std::move(err)); });
+
+	// If this node has it's data channel set up, don't accept any new ones
+	conn->onDataChannel(nullptr);
+}
+
+void PeerConnection::onLocalDescription(rtc::Description desc)
+{
+	logger->debug("New local description: type={} sdp=\n{}", desc.typeString(), desc.generateSdp());
+
+	auto lock = std::unique_lock { mutex };
+
+	client->sendMessage({ desc });
+}
+
+void PeerConnection::onLocalCandidate(rtc::Candidate cand)
+{
+	logger->debug("New local candidate: {}", std::string { cand });
+
+	auto lock = std::unique_lock { mutex };
+
+	client->sendMessage({ cand });
+}
+
+void PeerConnection::onConnectionStateChange(rtc::PeerConnection::State state)
+{
+	logger->debug("Connection State changed: {}", state);
+
+	auto lock = std::unique_lock { mutex };
+
+	switch (state) {
+		case rtc::PeerConnection::State::New: {
+			logger->debug("New peer connection");
+			break;
+		}
+
+		case rtc::PeerConnection::State::Connecting: {
+			logger->debug("Peer connection connecting.");
+			break;
+		}
+
+		case rtc::PeerConnection::State::Connected: {
+			rtc::Candidate local, remote;
+			std::optional<std::chrono::milliseconds> rtt = conn->rtt();
+			if (conn->getSelectedCandidatePair(&local, &remote)) {
+				std::stringstream l, r;
+				l << local, r << remote;
+				logger->debug(
+					"Peer connection connected:\n"
+					"local: {}\n"
+					"remote: {}\n"
+					"bytes sent: {} / bytes received: {} / rtt: {}\n",
+					l.str(),
+					r.str(),
+					conn->bytesSent(),
+					conn->bytesReceived(),
+					rtt.value_or(decltype(rtt)::value_type { 0 })
+				);
+			} else {
+				logger->debug(
+					"Peer connection connected.\n"
+					"Could not get candidate pair info.\n"
+				);
+			}
+			break;
+		}
+
+		case rtc::PeerConnection::State::Disconnected:
+		case rtc::PeerConnection::State::Failed: {
+			logger->debug("Closing peer connection");
+			break;
+		}
+
+		case rtc::PeerConnection::State::Closed: {
+			logger->debug("Closed peer connection");
+			resetConnectionAndStandby(lock);
+			break;
+		}
+	}
+}
+
+void PeerConnection::onSignalingStateChange(rtc::PeerConnection::SignalingState state)
+{
+	std::stringstream s;
+	s << state;
+	logger->debug("Signaling state changed: {}", s.str());
+}
+
+void PeerConnection::onGatheringStateChange(rtc::PeerConnection::GatheringState state)
+{
+	std::stringstream s;
+	s << state;
+	logger->debug("Gathering state changed: {}", s.str());
+}
+
+void PeerConnection::onSignalingConnected()
+{
+	logger->debug("Signaling connection established");
+}
+
+void PeerConnection::onSignalingDisconnected()
+{
+	logger->debug("Signaling connection closed");
+
+	auto lock = std::unique_lock { mutex };
+
+	resetConnectionAndStandby(lock);
+}
+
+void PeerConnection::onSignalingError(std::string err)
+{
+	logger->debug("Signaling connection error: {}", err);
+
+	auto lock = std::unique_lock { mutex };
+
+	resetConnectionAndStandby(lock);
+}
+
+void PeerConnection::onSignalingMessage(SignalingMessage msg)
+{
+	logger->debug("Signaling message received: {}", msg.toString());
+
+	auto lock = std::unique_lock { mutex };
+
+	std::visit(villas::utils::overloaded {
+		[&](RelayMessage &c){
+			extraServers = std::move(c.servers);
+		},
+
+		[&](ControlMessage &c){
+			auto const &id = c.connectionID;
+
+			if (c.connections.size() < 2) {
+				resetConnectionAndStandby(lock);
+				return;
+			}
+
+			auto fst = INT_MAX, snd = INT_MAX;
+			for (auto &c : c.connections) {
+				if (c.id < fst) {
+					snd = fst;
+					fst = c.id;
+				} else if (c.id < snd) {
+					snd = c.id;
+				}
+			}
+
+			standby = (id != fst && id != snd);
+
+			if (standby) {
+				logger->error("There are already two peers connected to this session. Waiting in standby.");
+				return;
+			}
+
+			if (fst == firstID && snd == secondID) {
+				logger->debug("Ignoring control message. This connection is already being established.");
+				return;
+			}
+
+			resetConnection(lock);
+
+			first = (id == fst);
+			firstID = fst;
+			secondID = snd;
+
+			setupPeerConnection();
+
+			if (!first) {
+				setupDataChannel();
+				conn->setLocalDescription(rtc::Description::Type::Offer);
+			}
+
+			logger->trace("New connection pair: first={}, second={}, I am {}", firstID, secondID, first ? "first" : "second");
+		},
+
+		[&](rtc::Description d){
+			if (standby || !conn || (!first && d.type() == rtc::Description::Type::Offer))
+				return;
+
+			conn->setRemoteDescription(d);
+		},
+
+		[&](rtc::Candidate c){
+			if (standby || !conn)
+				return;
+
+			conn->addRemoteCandidate(c);
+		},
+
+		[&](auto other){
+			logger->warn("unknown signaling message");
+		}
+	}, msg.message);
+}
+
+void PeerConnection::onDataChannel(std::shared_ptr<rtc::DataChannel> dc)
+{
+	logger->debug("New data channel: {}protocol={}, max_msg_size={}, label={}",
+		dc->id() && dc->stream() ? fmt::format("id={}, stream={}, ",
+			*(dc->id()),
+			*(dc->stream())
+ 		) : "",
+		dc->protocol(),
+		dc->maxMessageSize(),
+		dc->label()
+	);
+
+	auto lock = std::unique_lock { mutex };
+
+	setupDataChannel(std::move(dc));
+}
+
+void PeerConnection::onDataChannelOpen()
+{
+	logger->debug("Datachannel opened");
+
+	auto lock = std::unique_lock { mutex };
+
+	chan->send("Hello from VILLASnode");
+
+	notifyStartup();
+}
+
+void PeerConnection::onDataChannelClosed()
+{
+	logger->debug("Datachannel closed");
+
+	auto lock = std::unique_lock { mutex };
+
+	resetConnectionAndStandby(lock);
+}
+
+void PeerConnection::onDataChannelError(std::string err)
+{
+	logger->error("Datachannel error: {}", err);
+
+	auto lock = std::unique_lock { mutex };
+
+	resetConnectionAndStandby(lock);
+}
+
+void PeerConnection::onDataChannelMessage(rtc::string msg)
+{
+	logger->info("Received: {}", msg);
+}
+
+void PeerConnection::onDataChannelMessage(rtc::binary msg)
+{
+	logger->trace("Received binary data");
+
+	auto lock = std::unique_lock { mutex };
+
+	if (onMessageCallback)
+		onMessageCallback(msg);
+}
diff --git a/lib/nodes/webrtc/signaling_client.cpp b/lib/nodes/webrtc/signaling_client.cpp
new file mode 100644
index 000000000..4bb8ba49e
--- /dev/null
+++ b/lib/nodes/webrtc/signaling_client.cpp
@@ -0,0 +1,223 @@
+/** WebRTC signaling client
+ *
+ * @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
+ * @author Philipp Jungkamp <Philipp.Jungkamp@opal-rt.com>
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @copyright 2023, OPAL-RT Germany GmbH
+ * @license Apache 2.0
+ *********************************************************************************/
+
+#include <villas/web.hpp>
+#include <villas/exceptions.hpp>
+#include <villas/nodes/webrtc/signaling_client.hpp>
+#include <villas/nodes/webrtc/signaling_message.hpp>
+
+using namespace villas;
+using namespace villas::node;
+using namespace villas::node::webrtc;
+
+SignalingClient::SignalingClient(const std::string &srv, const std::string &sess, Web *w) :
+	retry_count(0),
+	web(w),
+	running(false),
+	logger(logging.get("webrtc:signal"))
+{
+	int ret;
+	const char *prot, *a, *p;
+
+	memset(&info, 0, sizeof(info));
+
+	ret = asprintf(&uri, "%s/%s", srv.c_str(), sess.c_str());
+	if (ret < 0)
+		throw RuntimeError { "Could not format signaling server uri" };
+
+	ret = lws_parse_uri(uri, &prot, &a, &info.port, &p);
+	if (ret)
+		throw RuntimeError("Failed to parse WebSocket URI: '{}'", uri);
+
+	ret = asprintf(&path, "/%s", p);
+	if (ret < 0)
+		throw RuntimeError { "Could not format signaling client path" };
+
+	info.ssl_connection = !strcmp(prot, "https");
+	info.address = a;
+	info.path = path;
+	info.host = a;
+	info.origin = a;
+	info.protocol = "webrtc-signaling";
+	info.local_protocol_name = "webrtc-signaling";
+	info.pwsi = &wsi;
+	info.retry_and_idle_policy = &retry;
+	info.ietf_version_or_minus_one = -1;
+	info.userdata = this;
+
+	sul_helper.self = this;
+	sul_helper.sul = {};
+}
+
+SignalingClient::~SignalingClient()
+{
+	disconnect();
+
+	free(path);
+	free(uri);
+}
+
+void SignalingClient::connect()
+{
+	running = true;
+
+	info.context = web->getContext();
+
+	lws_sul_schedule(info.context, 0, &sul_helper.sul, connectStatic, 1 * LWS_US_PER_SEC);
+}
+
+void SignalingClient::disconnect()
+{
+	running = false;
+	// TODO:
+	// - wait for connectStatic to exit
+	// - close LWS connection
+	if (wsi)
+		lws_callback_on_writable(wsi);
+}
+
+void SignalingClient::connectStatic(struct lws_sorted_usec_list *sul)
+{
+	auto *sh = lws_container_of(sul, struct sul_offsetof_helper, sul);
+	auto *c = sh->self;
+
+	if (!lws_client_connect_via_info(&c->info)) {
+		/* Failed... schedule a retry... we can't use the _retry_wsi()
+		 * convenience wrapper api here because no valid wsi at this
+		 * point.
+		 */
+		if (lws_retry_sul_schedule(c->info.context, 0, sul, nullptr, connectStatic, &c->retry_count))
+			c->logger->error("Signaling connection attempts exhausted");
+	}
+}
+
+int SignalingClient::protocolCallbackStatic(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
+{
+	auto *c = reinterpret_cast<SignalingClient *>(user);
+
+	return c->protocolCallback(wsi, reason, in, len);
+}
+
+int SignalingClient::protocolCallback(struct lws *wsi, enum lws_callback_reasons reason, void *in, size_t len)
+{
+	int ret;
+
+	switch (reason) {
+	case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
+		cbError(in ? (char *) in : "unknown error");
+		goto do_retry;
+
+	case LWS_CALLBACK_CLIENT_RECEIVE:
+		ret = receive(in, len);
+		if (ret)
+			goto do_retry;
+
+		break;
+
+	case LWS_CALLBACK_CLIENT_ESTABLISHED:
+		retry_count = 0;
+		cbConnected();
+		break;
+
+	case LWS_CALLBACK_CLIENT_CLOSED:
+		cbDisconnected();
+		goto do_retry;
+
+	case LWS_CALLBACK_CLIENT_WRITEABLE: {
+		ret = writable();
+		if (ret)
+			goto do_retry;
+
+		break;
+	}
+
+	default:
+		break;
+	}
+
+	return lws_callback_http_dummy(wsi, reason, this, in, len);
+
+do_retry:
+	logger->info("Attempting to reconnect...");
+
+	/* Retry the connection to keep it nailed up
+	 *
+	 * For this example, we try to conceal any problem for one set of
+	 * backoff retries and then exit the app.
+	 *
+	 * If you set retry.conceal_count to be larger than the number of
+	 * elements in the backoff table, it will never give up and keep
+	 * retrying at the last backoff delay plus the random jitter amount.
+	 */
+	if (lws_retry_sul_schedule_retry_wsi(wsi, &sul_helper.sul, connectStatic, &retry_count))
+		logger->error("Signaling connection attempts exhaused");
+
+	return 0;
+}
+
+int SignalingClient::writable()
+{
+	if (!running) {
+		auto reason = "Signaling Client Closing";
+		lws_close_reason(wsi, LWS_CLOSE_STATUS_GOINGAWAY, (unsigned char *) reason, strlen(reason));
+		return 0;
+	}
+
+	// Skip if we have nothing to send
+	if (outgoingMessages.empty()) {
+		return 0;
+	}
+
+	auto msg = outgoingMessages.pop();
+	auto *jsonMsg = msg.toJSON();
+
+	if (!jsonMsg) {
+		return 0;
+	}
+
+	char buf[LWS_PRE + 1024];
+	auto len = json_dumpb(jsonMsg, buf + LWS_PRE, 1024, JSON_INDENT(2));
+
+	auto ret = lws_write(wsi, (unsigned char *) buf + LWS_PRE, len, LWS_WRITE_TEXT);
+	if (ret < 0)
+		return ret;
+
+	logger->debug("Signaling message sent: {:.{}}", buf + LWS_PRE, len);
+
+	// Reschedule callback if there are more messages to be send
+	if (!outgoingMessages.empty())
+		lws_callback_on_writable(wsi);
+
+	return 0;
+}
+
+int SignalingClient::receive(void *in, size_t len)
+{
+	json_error_t err;
+	json_t *json = json_loadb((char *) in, len, 0, &err);
+	if (!json) {
+		logger->error("Failed to decode json: {} at ({}:{})", err.text, err.line, err.column);
+		return -1;
+	}
+
+	logger->debug("Signaling message received: {:.{}}", (char *)in, len);
+
+	cbMessage(SignalingMessage::fromJSON(json));
+
+	json_decref(json);
+
+	return 0;
+}
+
+void SignalingClient::sendMessage(SignalingMessage msg)
+{
+	outgoingMessages.push(msg);
+
+	web->callbackOnWritable(wsi);
+}
diff --git a/lib/nodes/webrtc/signaling_message.cpp b/lib/nodes/webrtc/signaling_message.cpp
new file mode 100644
index 000000000..ddbee7e67
--- /dev/null
+++ b/lib/nodes/webrtc/signaling_message.cpp
@@ -0,0 +1,212 @@
+/** WebRTC signaling messages.
+ *
+ * @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
+ * @author Philipp Jungkamp <Philipp.Jungkamp@opal-rt.com>
+ * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
+ * @copyright 2023, OPAL-RT Germany GmbH
+ * @license Apache 2.0
+ *********************************************************************************/
+
+#include <fmt/format.h>
+#include <villas/utils.hpp>
+#include <villas/exceptions.hpp>
+#include <villas/nodes/webrtc/signaling_message.hpp>
+#include <algorithm>
+
+using namespace villas;
+using namespace villas::node;
+using namespace villas::node::webrtc;
+
+json_t * Connection::toJSON() const
+{
+	return json_pack("{ s:i, s:s, s:s, s:s }",
+		"id", id,
+		"remote", remote.c_str(),
+		"user_agent", userAgent.c_str(),
+		"created", "" // TODO: create json timestamp
+	);
+}
+
+Connection::Connection(json_t *json)
+{
+	const char *rem, *ua, *ts;
+
+	int ret = json_unpack(json, "{ s:i, s:s, s:s, s:s }",
+		"id", &id,
+		"remote", &rem,
+		"user_agent", &ua,
+		"created", &ts
+	);
+	if (ret)
+		throw RuntimeError("Failed to decode signaling message");
+
+	remote = rem;
+	userAgent = ua;
+
+	// TODO: created
+}
+
+RelayMessage::RelayMessage(json_t *json)
+{
+
+	if (!json_is_array(json))
+		throw RuntimeError("Failed to decode signaling message");
+
+	int ret;
+	char *url;
+	char *user;
+	char *pass;
+	char *realm;
+	char *expires;
+	json_t *server_json;
+	size_t i;
+	json_array_foreach(json, i, server_json) {
+		ret = json_unpack(server_json, "{ s:s, s:s, s:s, s:s, s:s }",
+			"url", &url,
+			"user", &user,
+			"pass", &pass,
+			"realm", &realm,
+			"expires", &expires
+		);
+		if (ret)
+			throw RuntimeError("Failed to decode signaling message");
+
+		auto &server = servers.emplace_back(url);
+		server.username = user;
+		server.password = pass;
+
+		// TODO: warn about unsupported realm
+		// TODO: log info about expires time
+	}
+}
+
+json_t * ControlMessage::toJSON() const
+{
+	json_t *json_connections = json_array();
+
+	for (auto &c : connections) {
+		json_t *json_connection = c.toJSON();
+
+		json_array_append_new(json_connections, json_connection);
+	}
+
+	return json_pack("{ s:i, s:o }",
+		"connection_id", connectionID,
+		"connections", json_connections
+	);
+}
+
+ControlMessage::ControlMessage(json_t *j)
+{
+	int ret;
+
+	json_t *json_connections;
+
+	ret = json_unpack(j, "{ s:i, s:o }",
+		"connection_id", &connectionID,
+		"connections", &json_connections
+	);
+	if (ret)
+		throw RuntimeError("Failed to decode signaling message");
+
+	if (!json_is_array(json_connections))
+		throw RuntimeError("Failed to decode signaling message");
+
+	json_t *json_connection;
+	size_t i;
+	// cppcheck-suppress unknownMacro
+	json_array_foreach(json_connections, i, json_connection)
+		connections.emplace_back(json_connection);
+}
+
+json_t * SignalingMessage::toJSON() const
+{
+	return std::visit(villas::utils::overloaded {
+		[](ControlMessage const &c){
+			return json_pack("{ s:o }", "control", c.toJSON());
+		},
+		[](rtc::Description const &d){
+			return json_pack("{ s:{ s:s, s:s } }", "description",
+				"spd", d.generateSdp().c_str(),
+				"type", d.typeString().c_str()
+			);
+		},
+		[](rtc::Candidate const &c){
+			return json_pack("{ s:{ s:s, s:s } }", "candidate",
+				"spd", c.candidate().c_str(),
+				"mid", c.mid().c_str()
+			);
+		},
+		[](auto &other){
+			return (json_t *) { nullptr };
+		}
+	}, message);
+}
+
+std::string SignalingMessage::toString() const
+{
+	return std::visit(villas::utils::overloaded {
+		[](RelayMessage const &r){
+			return fmt::format("type=relay");
+		},
+		[](ControlMessage const &c){
+			return fmt::format("type=control, control={}", json_dumps(c.toJSON(), 0));
+		},
+		[](rtc::Description const &d){
+			return fmt::format("type=description, type={}, spd=\n{}", d.typeString(), d.generateSdp());
+		},
+		[](rtc::Candidate const &c){
+			return fmt::format("type=candidate, mid={}, spd=\n{}", c.candidate(), c.mid());
+		},
+		[](auto other){
+			return fmt::format("invalid signaling message");
+		}
+	}, message);
+}
+
+SignalingMessage SignalingMessage::fromJSON(json_t *json)
+{
+	auto self = SignalingMessage { std::monostate() };
+
+	// Relay message
+	json_t *rlys = nullptr;
+	// Control message
+	json_t *ctrl = nullptr;
+	// Candidate message
+	const char *cand = nullptr;
+	const char *mid = nullptr;
+	// Description message
+	const char *desc = nullptr;
+	const char *typ = nullptr;
+
+	int ret = json_unpack(json, "{ s?o, s?o, s?{ s:s, s:s }, s?{ s:s, s:s } }",
+		"servers", &rlys,
+		"control", &ctrl,
+		"candidate",
+			"spd", &cand,
+			"mid", &mid,
+		"description",
+			"spd", &desc,
+			"type", &typ
+	);
+
+	// Exactly 1 field may be specified
+	const void *fields[] = { ctrl, cand, desc };
+	if (ret || std::count(std::begin(fields), std::end(fields), nullptr) < std::make_signed_t<size_t>(std::size(fields)) - 1)
+		throw RuntimeError("Failed to decode signaling message");
+
+	if (rlys) {
+		self.message.emplace<RelayMessage>(rlys);
+	}
+	else if (ctrl) {
+		self.message.emplace<ControlMessage>(ctrl);
+	}
+	else if (cand) {
+		self.message.emplace<rtc::Candidate>(cand, mid);
+	}
+	else if (desc) {
+		self.message.emplace<rtc::Description>(desc, typ);
+	}
+
+	return self;
+}
diff --git a/lib/web.cpp b/lib/web.cpp
index 85ef147c1..c4bc4db67 100644
--- a/lib/web.cpp
+++ b/lib/web.cpp
@@ -7,6 +7,7 @@
 
 #include <cstring>
 
+#include <villas/config.hpp>
 #include <villas/node/config.hpp>
 #include <villas/utils.hpp>
 #include <villas/web.hpp>
@@ -15,6 +16,10 @@
 #include <villas/node/exceptions.hpp>
 #include <villas/nodes/websocket.hpp>
 
+#ifdef WITH_NODE_WEBRTC
+  #include <villas/nodes/webrtc/signaling_client.hpp>
+#endif
+
 using namespace villas;
 using namespace villas::node;
 
@@ -45,8 +50,16 @@ lws_protocols protocols[] = {
 		.rx_buffer_size = 0
 	},
 #endif /* WITH_NODE_WEBSOCKET */
+#ifdef WITH_NODE_WEBRTC
+	{
+		.name = "webrtc-signaling",
+		.callback = webrtc::SignalingClient::protocolCallbackStatic,
+		.per_session_data_size = sizeof(webrtc::SignalingClient),
+		.rx_buffer_size = 0
+	},
+#endif
 	{
-		.name = nullptr /* terminator */
+		.name = nullptr,
 	}
 };
 
diff --git a/packaging/deps.sh b/packaging/deps.sh
index 3b1d28b22..b8a792e0b 100644
--- a/packaging/deps.sh
+++ b/packaging/deps.sh
@@ -245,7 +245,7 @@ if ! pkg-config "libxil >= 1.0.0" && \
 fi
 
 # Build & Install hiredis
-if ! pkg-config "hiredis>1.0.0" && \
+if ! pkg-config "hiredis >= 1.0.0" && \
     [ -z "${SKIP_HIREDIS}" -a -z "${SKIP_REDIS}" ]; then
     git clone ${GIT_OPTS} --branch v1.0.0 https://github.com/redis/hiredis.git
     mkdir -p hiredis/build
@@ -303,20 +303,39 @@ if ! pkg-config "spdlog >= 1.8.2" && \
 fi
 
 # Build & Install libwebsockets
-if ! pkg-config "libwebsockets >= 3.1.0" && \
+if ! pkg-config "libwebsockets >= 4.3.0" && \
     [ -z "${SKIP_WEBSOCKETS}" ]; then
-    git clone ${GIT_OPTS} --branch v4.0-stable https://libwebsockets.org/repo/libwebsockets
+    git clone ${GIT_OPTS} --branch v4.3-stable https://github.com/warmcat/libwebsockets
     mkdir -p libwebsockets/build
     pushd libwebsockets/build
     cmake -DLWS_WITH_IPV6=ON \
           -DLWS_WITHOUT_TESTAPPS=ON \
           -DLWS_WITHOUT_EXTENSIONS=OFF \
-          -DLWS_WITH_SERVER_STATUS=ON \
           ${CMAKE_OPTS} ..
     make ${MAKE_OPTS} install
     popd
 fi
 
+# Build & Install libdatachannel
+if ! cmake --find-package -DNAME=LibDataChannel -DCOMPILER_ID=GNU -DLANGUAGE=CXX -DMODE=EXIST && \
+    [ -z "${SKIP_LIBDATACHANNEL}" ]; then
+    git clone ${GIT_OPTS} --branch v0.18.4 https://github.com/paullouisageneau/libdatachannel && pushd libdatachannel
+    git submodule update --init --recursive --depth 1
+    mkdir build && pushd build
+
+    if pkg-config "nice >= 0.1.16"; then
+        CMAKE_DATACHANNEL_USE_NICE=-DUSE_NICE=ON
+    fi
+
+    cmake -DNO_MEDIA=ON \
+          -DNO_WEBSOCKET=ON \
+          ${CMAKE_DATACHANNEL_USE_NICE} \
+          ${CMAKE_OPTS} ..
+
+    make ${MAKE_OPTS} install
+    popd; popd
+fi
+
 popd
 rm -rf ${DIR}
 
diff --git a/packaging/docker/Dockerfile.debian b/packaging/docker/Dockerfile.debian
index 12a5b2c05..f6ad41769 100644
--- a/packaging/docker/Dockerfile.debian
+++ b/packaging/docker/Dockerfile.debian
@@ -7,7 +7,7 @@
 
 # You can choose between Debian and Ubuntu here
 ARG DISTRO=debian
-ARG DEBIAN_VERSION=bullseye
+ARG DEBIAN_VERSION=bookworm
 
 FROM ${DISTRO}:${DEBIAN_VERSION} AS dev
 
@@ -49,7 +49,8 @@ RUN apt-get update && \
 		libfmt-dev \
 		libspdlog-dev \
 		liblua5.3-dev \
-		libhiredis-dev
+		libhiredis-dev \
+		libnice-dev
 
 # Add local and 64-bit locations to linker paths
 ENV echo /usr/local/lib >> /etc/ld.so.conf && \
diff --git a/packaging/docker/Dockerfile.fedora b/packaging/docker/Dockerfile.fedora
index 03c15f742..e48548e91 100644
--- a/packaging/docker/Dockerfile.fedora
+++ b/packaging/docker/Dockerfile.fedora
@@ -24,13 +24,11 @@ RUN dnf -y install \
 
 # Several tools only needed for developement and testing
 RUN dnf -y install \
-	doxygen dia graphviz \
 	openssh-clients \
-	rpmdevtools rpm-build \
 	jq nmap-ncat \
 	iproute iproute-tc \
 	python-pip \
-	valgrind gdb gdb-gdbserver \
+	gdb gdb-gdbserver \
 	cppcheck \
 	xmlto dblatex rubygem-asciidoctor \
 	psmisc procps-ng \
@@ -65,7 +63,8 @@ RUN dnf -y install \
 	librdmacm-devel \
 	libusb-devel \
 	lua-devel \
-	hiredis-devel
+	hiredis-devel \
+	libnice-devel
 
 # Add local and 64-bit locations to linker paths
 RUN echo /usr/local/lib   >> /etc/ld.so.conf && \
diff --git a/packaging/docker/Dockerfile.fedora-minimal b/packaging/docker/Dockerfile.fedora-minimal
index f095a5603..5124baca2 100644
--- a/packaging/docker/Dockerfile.fedora-minimal
+++ b/packaging/docker/Dockerfile.fedora-minimal
@@ -6,7 +6,7 @@
 ###################################################################################
 
 ARG DISTRO=fedora
-ARG FEDORA_VERSION=34
+ARG FEDORA_VERSION=36
 
 FROM ${DISTRO}:${FEDORA_VERSION} AS dev
 
diff --git a/packaging/docker/Dockerfile.rocky b/packaging/docker/Dockerfile.rocky
index c9aac273c..ea0fd1212 100644
--- a/packaging/docker/Dockerfile.rocky
+++ b/packaging/docker/Dockerfile.rocky
@@ -6,7 +6,7 @@
 ###################################################################################
 
 ARG DISTRO=rockylinux
-ARG ROCKY_VERSION=8
+ARG ROCKY_VERSION=9
 
 FROM ${DISTRO}:${ROCKY_VERSION} AS dev
 
@@ -16,13 +16,13 @@ USER root
 
 # Enable Extra Packages for Enterprise Linux (EPEL) repo and PowerTools
 RUN dnf -y install epel-release dnf-plugins-core
-RUN dnf config-manager --set-enabled powertools
+RUN dnf config-manager --set-enabled crb
 
 # Toolchain
-RUN dnf -y install \
+RUN dnf --allowerasing -y install \
 	gcc gcc-c++ \
 	pkgconfig cmake make \
-	autoconf automake autogen libtool \
+	autoconf automake libtool \
 	flex bison \
 	texinfo git curl tar \
 	protobuf-compiler protobuf-c-compiler
@@ -46,7 +46,8 @@ RUN dnf -y install \
 	librdmacm-devel \
 	libusb1-devel \
 	lua-devel \
-	hiredis-devel
+	hiredis-devel \
+	libnice-devel
 
 # Add local and 64-bit locations to linker paths
 ENV echo /usr/local/lib >> /etc/ld.so.conf && \
diff --git a/packaging/docker/Dockerfile.ubuntu b/packaging/docker/Dockerfile.ubuntu
index 7fe4c9a48..870e76a0c 100644
--- a/packaging/docker/Dockerfile.ubuntu
+++ b/packaging/docker/Dockerfile.ubuntu
@@ -7,7 +7,7 @@
 
 # You can choose between Debian and Ubuntu here
 ARG DISTRO=ubuntu
-ARG UBUNTU_VERSION=20.04
+ARG UBUNTU_VERSION=22.04
 ARG DISTRO=${DISTRO}
 
 FROM ${DISTRO}:${UBUNTU_VERSION} AS dev
@@ -51,7 +51,8 @@ RUN apt-get update && \
 		libfmt-dev \
 		libspdlog-dev \
 		liblua5.3-dev \
-		libhiredis-dev
+		libhiredis-dev \
+		libnice-dev
 
 # Add local and 64-bit locations to linker paths
 ENV echo /usr/local/lib >> /etc/ld.so.conf && \
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 65dcaa21e..4993367ff 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -44,6 +44,8 @@ endif()
 if(WITH_GRAPHVIZ)
     add_executable(villas-graph villas-graph.cpp)
     target_link_libraries(villas-graph PUBLIC villas)
+
+    list(APPEND SRCS villas-graph)
 endif()
 
 if(WITH_WEB)
@@ -64,8 +66,8 @@ endif()
 if(LIBZMQ_FOUND)
     add_executable(villas-zmq-keygen villas-zmq-keygen.cpp)
     target_link_libraries(villas-zmq-keygen PUBLIC villas-common PkgConfig::LIBZMQ)
-    
-    list(APPEND SRC villas-zmq-keygen)
+
+    list(APPEND SRCS villas-zmq-keygen)
 endif()
 
 if(WITH_HOOKS)
diff --git a/tools/villas b/tools/villas
index 2079dcd8c..1e90d581e 100755
--- a/tools/villas
+++ b/tools/villas
@@ -13,7 +13,7 @@
 ###################################################################################
 
 # Get a list of all available tools
-SUBTOOLS="node compare pipe hook conf2json convert graph relay signal test-config test-rtt zmq-keygen"
+SUBTOOLS="api node compare pipe hook conf2json convert graph relay signal test-config test-rtt zmq-keygen"
 
 # First argument to wrapper is the tool which should be started
 SUBTOOL=$1


Test patch:
diff --git a/go/pkg/node/node_test.go b/go/pkg/node/node_test.go
deleted file mode 100644
index c5e1e4057..000000000
--- a/go/pkg/node/node_test.go
+++ /dev/null
@@ -1,95 +0,0 @@
-/** Unit tests for using libvillas in Go code.
- *
- * @author Steffen Vogel <post@steffenvogel.de>
- * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
- * @license Apache 2.0
- *********************************************************************************/
-
-package node_test
-
-import (
-	"testing"
-	"time"
-
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/config"
-	"git.rwth-aachen.de/acs/public/villas/node/go/pkg/node"
-
-	"github.com/google/uuid"
-)
-
-func TestNode(t *testing.T) {
-	cfg := &config.LoopbackNode{
-		Node: config.Node{
-			Name: "lo1",
-			Type: "loopback",
-		},
-		In: config.NodeLoopbackIn{
-			Hooks: []interface{}{
-				&config.PrintHook{
-					Hook: config.Hook{
-						Type: "print",
-					},
-				},
-			},
-			Signals: []config.Signal{
-				{
-					Name: "sig1",
-				},
-				{
-					Name: "sig2",
-				},
-				{
-					Name: "sig3",
-				},
-			},
-		},
-	}
-
-	n, err := node.NewNode(cfg, uuid.New())
-	if err != nil {
-		t.Fatalf("Failed to create node: %s", err)
-	}
-	defer n.Close()
-
-	if err := n.Check(); err != nil {
-		t.Fatalf("Failed to check node: %s", err)
-	}
-
-	if err := n.Prepare(); err != nil {
-		t.Fatalf("Failed to prepare node: %s", err)
-	}
-
-	if err := n.Start(); err != nil {
-		t.Fatalf("Failed to start node: %s", err)
-	}
-	defer n.Stop()
-
-	t.Logf("%s", n.NameFull())
-
-	smps_send := []node.Sample{
-		{
-			Sequence:        1234,
-			TimestampOrigin: time.Now(),
-			Data:            []float64{1.1, 2.2, 3.3},
-		},
-		{
-			Sequence:        1235,
-			TimestampOrigin: time.Now(),
-			Data:            []float64{4.4, 5.5, 6.6},
-		},
-	}
-
-	t.Logf("Sent: %+#v", smps_send)
-
-	cnt_written := n.Write(smps_send)
-	if cnt_written != len(smps_send) {
-		t.Fatalf("Failed to send all samples")
-	}
-
-	smps_received := n.Read(cnt_written)
-	if len(smps_received) != cnt_written {
-		t.Fatalf("Failed to receive samples back")
-	}
-
-	t.Logf("Received: %+#v", smps_received)
-}