mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-25 08:25:07 +03:00
Merge branch 'develop' into nas_ui_migration
This commit is contained in:
commit
7e3e788842
7 changed files with 261 additions and 44 deletions
|
@ -201,7 +201,7 @@ func run(args yggArgs, ctx context.Context) {
|
||||||
// that neither -autoconf, -useconf or -useconffile were set above. Stop
|
// that neither -autoconf, -useconf or -useconffile were set above. Stop
|
||||||
// if we don't.
|
// if we don't.
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return
|
panic("broken configuration")
|
||||||
}
|
}
|
||||||
n := &node{}
|
n := &node{}
|
||||||
// Have we been asked for the node address yet? If so, print it and then stop.
|
// Have we been asked for the node address yet? If so, print it and then stop.
|
||||||
|
@ -230,13 +230,6 @@ func run(args yggArgs, ctx context.Context) {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//override httpaddress and wwwroot parameters in cfg
|
|
||||||
if len(cfg.HttpAddress) == 0 {
|
|
||||||
cfg.HttpAddress = args.httpaddress
|
|
||||||
}
|
|
||||||
if len(cfg.WwwRoot) == 0 {
|
|
||||||
cfg.WwwRoot = args.wwwroot
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the RiV-mesh node itself.
|
// Setup the RiV-mesh node itself.
|
||||||
{
|
{
|
||||||
|
@ -274,6 +267,14 @@ func run(args yggArgs, ctx context.Context) {
|
||||||
|
|
||||||
// Setup the REST socket.
|
// Setup the REST socket.
|
||||||
{
|
{
|
||||||
|
//override httpaddress and wwwroot parameters in cfg
|
||||||
|
if len(cfg.HttpAddress) == 0 {
|
||||||
|
cfg.HttpAddress = args.httpaddress
|
||||||
|
}
|
||||||
|
if len(cfg.WwwRoot) == 0 {
|
||||||
|
cfg.WwwRoot = args.wwwroot
|
||||||
|
}
|
||||||
|
|
||||||
if n.rest_server, err = restapi.NewRestServer(restapi.RestServerCfg{
|
if n.rest_server, err = restapi.NewRestServer(restapi.RestServerCfg{
|
||||||
Core: n.core,
|
Core: n.core,
|
||||||
Log: logger,
|
Log: logger,
|
||||||
|
|
|
@ -299,8 +299,6 @@ ui.getConnectedPeers = function () {
|
||||||
|
|
||||||
ui.updateConnectedPeersHandler = function (peers) {
|
ui.updateConnectedPeersHandler = function (peers) {
|
||||||
ui.updateStatus(peers);
|
ui.updateStatus(peers);
|
||||||
ui.updateSpeed(peers);
|
|
||||||
ui.updateCoordsInfo();
|
|
||||||
$("peers").innerText = "";
|
$("peers").innerText = "";
|
||||||
if (peers) {
|
if (peers) {
|
||||||
var regexStrip = /%[^\]]*/gm;
|
var regexStrip = /%[^\]]*/gm;
|
||||||
|
@ -381,7 +379,7 @@ ui.updateSelfInfo = function () {
|
||||||
return ui.getSelfInfo().then(function (info) {
|
return ui.getSelfInfo().then(function (info) {
|
||||||
$("ipv6").innerText = info.address;
|
$("ipv6").innerText = info.address;
|
||||||
$("subnet").innerText = info.subnet;
|
$("subnet").innerText = info.subnet;
|
||||||
$("coordinates").innerText = ''.concat('[',info.coords.join(' '),']');
|
$("coordinates").innerText = ''.concat('[', info.coords.join(' '), ']');
|
||||||
$("pub_key").innerText = info.key;
|
$("pub_key").innerText = info.key;
|
||||||
$("priv_key").innerText = info.private_key;
|
$("priv_key").innerText = info.private_key;
|
||||||
$("ipv6").innerText = info.address;
|
$("ipv6").innerText = info.address;
|
||||||
|
@ -391,16 +389,6 @@ ui.updateSelfInfo = function () {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.updateCoordsInfo = function () {
|
|
||||||
return ui.getSelfInfo().then(function (info) {
|
|
||||||
$("coordinates").innerText = ''.concat('[',info.coords.join(' '),']');
|
|
||||||
}).catch(function (error) {
|
|
||||||
$("ipv6").innerText = error.message;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.sse = new EventSource('/api/sse');
|
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
|
||||||
window.addEventListener("load", function () {
|
window.addEventListener("load", function () {
|
||||||
|
@ -412,6 +400,8 @@ function main() {
|
||||||
|
|
||||||
ui.updateSelfInfo();
|
ui.updateSelfInfo();
|
||||||
|
|
||||||
|
ui.sse = new EventSource('/api/sse');
|
||||||
|
|
||||||
ui.sse.addEventListener("ping", function (e) {
|
ui.sse.addEventListener("ping", function (e) {
|
||||||
var data = JSON.parse(e.data);
|
var data = JSON.parse(e.data);
|
||||||
setPingValue(data.peer, data.value);
|
setPingValue(data.peer, data.value);
|
||||||
|
@ -420,6 +410,15 @@ function main() {
|
||||||
ui.sse.addEventListener("peers", function (e) {
|
ui.sse.addEventListener("peers", function (e) {
|
||||||
ui.updateConnectedPeersHandler(JSON.parse(e.data));
|
ui.updateConnectedPeersHandler(JSON.parse(e.data));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.sse.addEventListener("rxtx", function (e) {
|
||||||
|
ui.updateSpeed(JSON.parse(e.data));
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.sse.addEventListener("coord", function (e) {
|
||||||
|
var coords = JSON.parse(e.data);
|
||||||
|
$("coordinates").innerText = ''.concat('[', coords.join(' '), ']');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ img {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body, #notification_windowx {
|
||||||
max-width: 690px;
|
max-width: 690px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
@ -94,11 +94,12 @@ footer {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#version {
|
#footer-row #version {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#status {
|
#footer-row #status {
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -286,8 +286,6 @@ ui.getConnectedPeers = () =>
|
||||||
|
|
||||||
ui.updateConnectedPeersHandler = (peers) => {
|
ui.updateConnectedPeersHandler = (peers) => {
|
||||||
ui.updateStatus(peers);
|
ui.updateStatus(peers);
|
||||||
ui.updateSpeed(peers);
|
|
||||||
ui.updateCoordsInfo();
|
|
||||||
$("peers").innerText = "";
|
$("peers").innerText = "";
|
||||||
if(peers) {
|
if(peers) {
|
||||||
const regexStrip = /%[^\]]*/gm;
|
const regexStrip = /%[^\]]*/gm;
|
||||||
|
@ -368,16 +366,6 @@ ui.updateSelfInfo = () =>
|
||||||
$("ipv6").innerText = error.message;
|
$("ipv6").innerText = error.message;
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.updateCoordsInfo = function () {
|
|
||||||
return ui.getSelfInfo().then(function (info) {
|
|
||||||
$("coordinates").innerText = ''.concat('[',info.coords.join(' '),']');
|
|
||||||
}).catch(function (error) {
|
|
||||||
$("ipv6").innerText = error.message;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.sse = new EventSource('/api/sse');
|
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
|
@ -386,7 +374,8 @@ function main() {
|
||||||
ui.getAllPeers().then(() => ui.updateConnectedPeers());
|
ui.getAllPeers().then(() => ui.updateConnectedPeers());
|
||||||
|
|
||||||
ui.updateSelfInfo();
|
ui.updateSelfInfo();
|
||||||
//setInterval(ui.updateSelfInfo, 5000);
|
|
||||||
|
ui.sse = new EventSource('/api/sse');
|
||||||
|
|
||||||
ui.sse.addEventListener("ping", (e) => {
|
ui.sse.addEventListener("ping", (e) => {
|
||||||
let data = JSON.parse(e.data);
|
let data = JSON.parse(e.data);
|
||||||
|
@ -397,6 +386,15 @@ function main() {
|
||||||
ui.updateConnectedPeersHandler(JSON.parse(e.data));
|
ui.updateConnectedPeersHandler(JSON.parse(e.data));
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ui.sse.addEventListener("rxtx", (e) => {
|
||||||
|
ui.updateSpeed(JSON.parse(e.data));
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.sse.addEventListener("coord", (e) => {
|
||||||
|
let coords = JSON.parse(e.data);
|
||||||
|
$("coordinates").innerText = ''.concat('[',coords.join(' '),']');
|
||||||
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<body class="hero is-fullheight">
|
<body class="hero is-fullheight">
|
||||||
<div>
|
<div>
|
||||||
<div class="box is-hidden" id="notification_window">
|
<div class="box is-hidden" id="notification_window">
|
||||||
<div style="z-index: 9;" class="notification is-primary">
|
<div id="notification_windowx" style="z-index: 9;" class="notification is-primary">
|
||||||
<button class="delete" id="info_win_close"></button>
|
<button class="delete" id="info_win_close"></button>
|
||||||
<p style="padding:3px; max-height: 250px; overflow-y: auto;" id="info_window"></p>
|
<p style="padding:3px; max-height: 250px; overflow-y: auto;" id="info_window"></p>
|
||||||
<div style="padding-left:100px; padding-top:15px;" class="field is-grouped">
|
<div style="padding-left:100px; padding-top:15px;" class="field is-grouped">
|
||||||
|
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/RiV-chain/RiV-mesh/src/defaults"
|
"github.com/RiV-chain/RiV-mesh/src/defaults"
|
||||||
|
|
||||||
|
@ -46,6 +47,191 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Opening: %v", confui.IndexHtml)
|
log.Printf("Opening: %v", confui.IndexHtml)
|
||||||
w.Navigate(confui.IndexHtml)
|
w.SetHtml(strings.Replace(splash, "http://localhost:19019", confui.IndexHtml, 1))
|
||||||
w.Run()
|
w.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var splash = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Riv mesh</title>
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
let ep = "http://localhost:19019";
|
||||||
|
|
||||||
|
function redirect() {
|
||||||
|
fetch(ep + '/api')
|
||||||
|
.then(() => {
|
||||||
|
window.location.replace(ep);
|
||||||
|
}).catch((error) => {
|
||||||
|
document.getElementById("error").innerHTML = "Mesh service connection error<br>Waiting for connection....";
|
||||||
|
setTimeout(redirect, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
redirect();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
position: absolute;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.spinner .blob {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: 2px solid #fff;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.spinner .blob.top {
|
||||||
|
top: 0;
|
||||||
|
-webkit-animation: blob-top 1s infinite ease-in;
|
||||||
|
animation: blob-top 1s infinite ease-in;
|
||||||
|
}
|
||||||
|
.spinner .blob.bottom {
|
||||||
|
top: 100%;
|
||||||
|
-webkit-animation: blob-bottom 1s infinite ease-in;
|
||||||
|
animation: blob-bottom 1s infinite ease-in;
|
||||||
|
}
|
||||||
|
.spinner .blob.left {
|
||||||
|
left: 0;
|
||||||
|
-webkit-animation: blob-left 1s infinite ease-in;
|
||||||
|
animation: blob-left 1s infinite ease-in;
|
||||||
|
}
|
||||||
|
.spinner .move-blob {
|
||||||
|
background: #fff;
|
||||||
|
top: 0;
|
||||||
|
-webkit-animation: blob-spinner-mover 1s infinite ease-in;
|
||||||
|
animation: blob-spinner-mover 1s infinite ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes blob-bottom {
|
||||||
|
25%, 50%, 75% {
|
||||||
|
top: 50%;
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blob-bottom {
|
||||||
|
25%, 50%, 75% {
|
||||||
|
top: 50%;
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes blob-left {
|
||||||
|
25% {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
50%, 100% {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes blob-left {
|
||||||
|
25% {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
50%, 100% {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes blob-top {
|
||||||
|
50% {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
75%, 100% {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes blob-top {
|
||||||
|
50% {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
75%, 100% {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes blob-spinner-mover {
|
||||||
|
0%, 100% {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
top: 50%;
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes blob-spinner-mover {
|
||||||
|
0%, 100% {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
top: 50%;
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#error {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<div id="error"></div>
|
||||||
|
<div class="spinner">
|
||||||
|
<div class="blob top"></div>
|
||||||
|
<div class="blob bottom"></div>
|
||||||
|
<div class="blob left"></div>
|
||||||
|
|
||||||
|
<div class="blob move-blob"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
|
|
||||||
type ServerEvent struct {
|
type ServerEvent struct {
|
||||||
Event string
|
Event string
|
||||||
Data string
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestServerCfg struct {
|
type RestServerCfg struct {
|
||||||
|
@ -40,6 +40,7 @@ type RestServer struct {
|
||||||
listenUrl *url.URL
|
listenUrl *url.URL
|
||||||
serverEvents chan ServerEvent
|
serverEvents chan ServerEvent
|
||||||
serverEventNextId int
|
serverEventNextId int
|
||||||
|
updateTimer *time.Timer
|
||||||
docFsType string
|
docFsType string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +94,7 @@ func NewRestServer(cfg RestServerCfg) (*RestServer, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case a.serverEvents <- ServerEvent{Event: "peers", Data: string(b)}:
|
case a.serverEvents <- ServerEvent{Event: "peers", Data: b}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -124,6 +125,8 @@ func addNoCacheHeaders(w http.ResponseWriter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *RestServer) apiHandler(w http.ResponseWriter, r *http.Request) {
|
func (a *RestServer) apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||||
fmt.Fprintf(w, "Following methods are allowed: GET /api/self, getpeers. litening")
|
fmt.Fprintf(w, "Following methods are allowed: GET /api/self, getpeers. litening")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,23 +284,40 @@ func (a *RestServer) apiSseHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
case v := <-a.serverEvents:
|
case v := <-a.serverEvents:
|
||||||
fmt.Fprintln(w, "id:", a.serverEventNextId)
|
fmt.Fprintln(w, "id:", a.serverEventNextId)
|
||||||
fmt.Fprintln(w, "event:", v.Event)
|
fmt.Fprintln(w, "event:", v.Event)
|
||||||
fmt.Fprintln(w, "data:", v.Data)
|
fmt.Fprintln(w, "data:", string(v.Data))
|
||||||
fmt.Fprintln(w) //end of event
|
fmt.Fprintln(w) //end of event
|
||||||
a.serverEventNextId += 1
|
a.serverEventNextId += 1
|
||||||
default:
|
default:
|
||||||
break Loop
|
break Loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if a.updateTimer != nil {
|
||||||
|
select {
|
||||||
|
case <-a.updateTimer.C:
|
||||||
|
go a.sendSseUpdate()
|
||||||
|
a.updateTimer.Reset(time.Second * 5)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a.updateTimer = time.NewTimer(time.Second * 5)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *RestServer) sendSseUpdate() {
|
||||||
|
rx, tx := a.getPeersRxTxBytes()
|
||||||
|
a.serverEvents <- ServerEvent{Event: "rxtx", Data: []byte(fmt.Sprintf(`[{"bytes_recvd":%d,"bytes_sent":%d}]`, rx, tx))}
|
||||||
|
data, _ := json.Marshal(a.Core.GetSelf().Coords)
|
||||||
|
a.serverEvents <- ServerEvent{Event: "coord", Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *RestServer) ping(peers []string) {
|
func (a *RestServer) ping(peers []string) {
|
||||||
for _, u := range peers {
|
for _, u := range peers {
|
||||||
go func(u string) {
|
go func(u string) {
|
||||||
data, _ := json.Marshal(map[string]string{"peer": u, "value": strconv.FormatInt(check(u), 10)})
|
data, _ := json.Marshal(map[string]string{"peer": u, "value": strconv.FormatInt(check(u), 10)})
|
||||||
a.serverEvents <- ServerEvent{Event: "ping", Data: string(data)}
|
a.serverEvents <- ServerEvent{Event: "ping", Data: data}
|
||||||
}(u)
|
}(u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,3 +335,15 @@ func check(peer string) int64 {
|
||||||
d := time.Since(t)
|
d := time.Since(t)
|
||||||
return d.Milliseconds()
|
return d.Milliseconds()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *RestServer) getPeersRxTxBytes() (uint64, uint64) {
|
||||||
|
var rx uint64
|
||||||
|
var tx uint64
|
||||||
|
|
||||||
|
peers := a.Core.GetPeers()
|
||||||
|
for _, p := range peers {
|
||||||
|
rx += p.RXBytes
|
||||||
|
tx += p.TXBytes
|
||||||
|
}
|
||||||
|
return rx, tx
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue