mirror of
https://git.rwth-aachen.de/acs/public/villas/node/
synced 2025-03-09 00:00:00 +01:00
Merge pull request #613 from VILLASframework/node-webrtc-cpp
Port WebRTC node-type from Golang to C++
This commit is contained in:
commit
e18d9c0f99
49 changed files with 1574 additions and 2725 deletions
|
@ -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)
|
||||
|
||||
# For compat between libfmt 8 and 9
|
||||
add_compile_definitions(FMT_DEPRECATED_OSTREAM)
|
||||
|
@ -89,17 +93,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)
|
||||
|
@ -161,7 +158,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)
|
||||
|
@ -180,7 +176,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
|
||||
|
@ -200,6 +195,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)
|
||||
|
||||
|
@ -216,10 +212,6 @@ if(WITH_FPGA)
|
|||
add_subdirectory(fpga)
|
||||
endif()
|
||||
|
||||
if(WITH_GO)
|
||||
add_subdirectory(go)
|
||||
endif()
|
||||
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(etc)
|
||||
add_subdirectory(lib)
|
||||
|
@ -261,7 +253,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")
|
||||
|
@ -280,7 +271,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")
|
||||
|
@ -300,6 +290,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")
|
||||
|
||||
|
|
2
common
2
common
|
@ -1 +1 @@
|
|||
Subproject commit b18d2aca5b9a158aaba3a0f82cffe253fc427b15
|
||||
Subproject commit d9d4ac76a5403e14f7899dae480781e9cdcf0572
|
|
@ -1,15 +0,0 @@
|
|||
nodes = {
|
||||
example_node = {
|
||||
type = "go.example"
|
||||
value = 555
|
||||
|
||||
format = "json"
|
||||
|
||||
in = {
|
||||
# signals ={
|
||||
# count = 5,
|
||||
# type = "float"
|
||||
# }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
)
|
|
@ -1,31 +1,15 @@
|
|||
|
||||
nodes = {
|
||||
webrtc_node = {
|
||||
type = "webrtc",
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
add_subdirectory(lib)
|
|
@ -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)
|
||||
}
|
||||
}
|
32
go/go.mod
32
go/go.mod
|
@ -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
|
||||
)
|
154
go/go.sum
154
go/go.sum
|
@ -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=
|
|
@ -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}
|
||||
)
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
190
go/lib/lib.go
190
go/lib/lib.go
|
@ -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)
|
||||
}
|
113
go/lib/logger.go
113
go/lib/logger.go
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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{})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
120
include/villas/nodes/webrtc.hpp
Normal file
120
include/villas/nodes/webrtc.hpp
Normal file
|
@ -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 */
|
91
include/villas/nodes/webrtc/peer_connection.hpp
Normal file
91
include/villas/nodes/webrtc/peer_connection.hpp
Normal file
|
@ -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 */
|
||||
|
117
include/villas/nodes/webrtc/signaling_client.hpp
Normal file
117
include/villas/nodes/webrtc/signaling_client.hpp
Normal file
|
@ -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 */
|
||||
|
59
include/villas/nodes/webrtc/signaling_message.hpp
Normal file
59
include/villas/nodes/webrtc/signaling_message.hpp
Normal file
|
@ -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 */
|
|
@ -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()
|
||||
|
|
288
lib/nodes/go.cpp
288
lib/nodes/go.cpp
|
@ -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;
|
272
lib/nodes/webrtc.cpp
Normal file
272
lib/nodes/webrtc.cpp
Normal file
|
@ -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;
|
411
lib/nodes/webrtc/peer_connection.cpp
Normal file
411
lib/nodes/webrtc/peer_connection.cpp
Normal file
|
@ -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);
|
||||
}
|
223
lib/nodes/webrtc/signaling_client.cpp
Normal file
223
lib/nodes/webrtc/signaling_client.cpp
Normal file
|
@ -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);
|
||||
}
|
212
lib/nodes/webrtc/signaling_message.cpp
Normal file
212
lib/nodes/webrtc/signaling_message.cpp
Normal file
|
@ -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;
|
||||
}
|
15
lib/web.cpp
15
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 = nullptr /* terminator */
|
||||
.name = "webrtc-signaling",
|
||||
.callback = webrtc::SignalingClient::protocolCallbackStatic,
|
||||
.per_session_data_size = sizeof(webrtc::SignalingClient),
|
||||
.rx_buffer_size = 0
|
||||
},
|
||||
#endif
|
||||
{
|
||||
.name = nullptr,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -316,6 +316,26 @@ if ! pkg-config "libwebsockets >= 4.3.0" && \
|
|||
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}
|
||||
|
||||
|
|
|
@ -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 && \
|
||||
|
|
|
@ -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 && \
|
||||
|
|
|
@ -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 && \
|
||||
|
|
|
@ -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 && \
|
||||
|
|
Loading…
Add table
Reference in a new issue