Add rpiConfigServer to tools
This commit is contained in:
parent
88f549108f
commit
98842b6728
2
deps/allwpilib
vendored
2
deps/allwpilib
vendored
|
@ -1 +1 @@
|
|||
Subproject commit bfe15245a625d8e30351fc1096d2df1ca71210c3
|
||||
Subproject commit 7d7af287f6718c0fcb2123aca1b2cebc3177ade5
|
43
deps/tools/Makefile
vendored
43
deps/tools/Makefile
vendored
|
@ -1,6 +1,16 @@
|
|||
COMPILER=../02-extract/raspbian9/bin/arm-raspbian9-linux-gnueabihf-
|
||||
|
||||
ALL: setuidgids _cscore.so
|
||||
.PHONY: all
|
||||
.SUFFIXES:
|
||||
|
||||
all: setuidgids _cscore.so rpiConfigServer
|
||||
|
||||
clean:
|
||||
rm -f setuidgids
|
||||
rm -f _cscore.so
|
||||
rm -f rpiConfigServer
|
||||
rm -f rpiConfigServer/*.o
|
||||
rm -f rpiConfigServer/resources/*.o
|
||||
|
||||
setuidgids: setuidgids.c
|
||||
${COMPILER}gcc -O -Wall -D_GNU_SOURCE -o $@ $<
|
||||
|
@ -19,3 +29,34 @@ _cscore.so: ../robotpy-cscore/src/_cscore.cpp ../robotpy-cscore/src/ndarray_conv
|
|||
-lcscore \
|
||||
-lwpiutil \
|
||||
-lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc -lopencv_core
|
||||
|
||||
RPICONFIGSERVER_OBJS= \
|
||||
rpiConfigServer_src/main.o \
|
||||
rpiConfigServer_src/MyHttpConnection.o \
|
||||
rpiConfigServer_src/SystemStatus.o \
|
||||
rpiConfigServer_src/VisionStatus.o \
|
||||
rpiConfigServer_src/WebSocketHandlers.o \
|
||||
rpiConfigServer_src/resources/index.html.o \
|
||||
rpiConfigServer_src/resources/frcvision.css.o \
|
||||
rpiConfigServer_src/resources/frcvision.js.o
|
||||
|
||||
rpiConfigServer: ${RPICONFIGSERVER_OBJS}
|
||||
${COMPILER}g++ -pthread -o $@ \
|
||||
${RPICONFIGSERVER_OBJS} \
|
||||
-L../allwpilib/wpiutil/build/libs/wpiutil/static/release \
|
||||
-lwpiutil
|
||||
|
||||
%.o: %.cpp
|
||||
${COMPILER}g++ -O -Wall -c -o $@ \
|
||||
-I../allwpilib/wpiutil/src/main/native/include \
|
||||
$<
|
||||
|
||||
%.html.cpp: %.html
|
||||
./gen_resource.py $@ $<
|
||||
|
||||
%.css.cpp: %.css
|
||||
./gen_resource.py $@ $<
|
||||
|
||||
%.js.cpp: %.js
|
||||
./gen_resource.py $@ $<
|
||||
|
||||
|
|
33
deps/tools/gen_resource.py
vendored
Executable file
33
deps/tools/gen_resource.py
vendored
Executable file
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
|
||||
parser = argparse.ArgumentParser(description='Generate resource .cpp file.')
|
||||
parser.add_argument('outputFile', help='output file')
|
||||
parser.add_argument('inputFile', help='input file')
|
||||
parser.add_argument('--prefix', dest='prefix', default='', help='C function prefix')
|
||||
parser.add_argument('--namespace', dest='namespace', default='', help='C++ namespace')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.inputFile, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
fileSize = len(data)
|
||||
|
||||
inputBase = os.path.basename(args.inputFile)
|
||||
funcName = "GetResource_" + re.sub(r"[^a-zA-Z0-9]", "_", inputBase)
|
||||
|
||||
with open(args.outputFile, "wt") as f:
|
||||
print("#include <stddef.h>\n#include <wpi/StringRef.h>\nextern \"C\" {\nstatic const unsigned char contents[] = { ", file=f, end='')
|
||||
print(", ".join("0x%02x" % x for x in data), file=f, end='')
|
||||
print(" };", file=f)
|
||||
print("const unsigned char* {}{}(size_t* len) {{\n *len = {};\n return contents;\n}}\n}}".format(args.prefix, funcName, fileSize), file=f)
|
||||
|
||||
if args.namespace:
|
||||
print("namespace {} {{".format(namespace), file=f)
|
||||
print("wpi::StringRef {}() {{\n return wpi::StringRef(reinterpret_cast<const char*>(contents), {});\n}}".format(funcName, fileSize), file=f)
|
||||
if args.namespace:
|
||||
print("}", file=f)
|
56
deps/tools/rpiConfigServer_src/DataHistory.h
vendored
Normal file
56
deps/tools/rpiConfigServer_src/DataHistory.h
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#ifndef WPIUTIL_DATAHISTORY_H_
|
||||
#define WPIUTIL_DATAHISTORY_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
template <typename T, size_t Size>
|
||||
class DataHistory {
|
||||
public:
|
||||
void Add(T val) {
|
||||
if (m_qty >= Size) {
|
||||
std::copy(&m_data[1], &m_data[Size], &m_data[0]);
|
||||
m_data[Size - 1] = val;
|
||||
} else {
|
||||
m_data[m_qty++] = val;
|
||||
}
|
||||
}
|
||||
|
||||
/* first is "least recent", last is "most recent" */
|
||||
bool GetFirstLast(T* first, T* last, size_t* qty) const {
|
||||
if (m_qty == 0) return false;
|
||||
if (first) *first = m_data[0];
|
||||
if (last) *last = m_data[m_qty - 1];
|
||||
if (qty) *qty = m_qty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* only look at most recent "count" samples */
|
||||
bool GetFirstLast(T* first, T* last, size_t* qty, size_t count) const {
|
||||
if (count == 0 || m_qty < count) return false;
|
||||
if (first) *first = m_data[m_qty - count];
|
||||
if (last) *last = m_data[m_qty - 1];
|
||||
if (qty) *qty = m_qty;
|
||||
return true;
|
||||
}
|
||||
|
||||
T GetTotal(size_t* qty = nullptr) const {
|
||||
if (qty) *qty = m_qty;
|
||||
return std::accumulate(&m_data[0], &m_data[m_qty], 0);
|
||||
}
|
||||
|
||||
private:
|
||||
T m_data[Size];
|
||||
size_t m_qty = 0;
|
||||
};
|
||||
|
||||
#endif // WPIUTIL_DATAHISTORY_H_
|
108
deps/tools/rpiConfigServer_src/MyHttpConnection.cpp
vendored
Normal file
108
deps/tools/rpiConfigServer_src/MyHttpConnection.cpp
vendored
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "MyHttpConnection.h"
|
||||
|
||||
#include "WebSocketHandlers.h"
|
||||
#include "wpi/UrlParser.h"
|
||||
#include "wpi/raw_ostream.h"
|
||||
|
||||
// static resources
|
||||
namespace wpi {
|
||||
StringRef GetResource_bootstrap_4_1_min_js_gz();
|
||||
StringRef GetResource_coreui_2_1_min_css_gz();
|
||||
StringRef GetResource_coreui_2_1_min_js_gz();
|
||||
StringRef GetResource_feather_4_8_min_js_gz();
|
||||
StringRef GetResource_jquery_3_3_slim_min_js_gz();
|
||||
StringRef GetResource_popper_1_14_min_js_gz();
|
||||
StringRef GetResource_wpilib_128_png();
|
||||
} // namespace wpi
|
||||
wpi::StringRef GetResource_frcvision_css();
|
||||
wpi::StringRef GetResource_frcvision_js();
|
||||
wpi::StringRef GetResource_index_html();
|
||||
|
||||
MyHttpConnection::MyHttpConnection(std::shared_ptr<wpi::uv::Stream> stream)
|
||||
: HttpServerConnection(stream), m_websocketHelper(m_request) {
|
||||
// Handle upgrade event
|
||||
m_websocketHelper.upgrade.connect([this] {
|
||||
//wpi::errs() << "got websocket upgrade\n";
|
||||
// Disconnect HttpServerConnection header reader
|
||||
m_dataConn.disconnect();
|
||||
m_messageCompleteConn.disconnect();
|
||||
|
||||
// Accepting the stream may destroy this (as it replaces the stream user
|
||||
// data), so grab a shared pointer first.
|
||||
auto self = shared_from_this();
|
||||
|
||||
// Accept the upgrade
|
||||
auto ws = m_websocketHelper.Accept(m_stream, "frcvision");
|
||||
|
||||
// Connect the websocket open event to our connected event.
|
||||
// Pass self to delay destruction until this callback happens
|
||||
ws->open.connect_extended([self, s = ws.get()](auto conn, wpi::StringRef) {
|
||||
wpi::errs() << "websocket connected\n";
|
||||
InitWs(*s);
|
||||
conn.disconnect(); // one-shot
|
||||
});
|
||||
ws->text.connect([s = ws.get()](wpi::StringRef msg, bool) {
|
||||
ProcessWsText(*s, msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void MyHttpConnection::ProcessRequest() {
|
||||
//wpi::errs() << "HTTP request: '" << m_request.GetUrl() << "'\n";
|
||||
wpi::UrlParser url{m_request.GetUrl(),
|
||||
m_request.GetMethod() == wpi::HTTP_CONNECT};
|
||||
if (!url.IsValid()) {
|
||||
// failed to parse URL
|
||||
SendError(400);
|
||||
return;
|
||||
}
|
||||
|
||||
wpi::StringRef path;
|
||||
if (url.HasPath()) path = url.GetPath();
|
||||
//wpi::errs() << "path: \"" << path << "\"\n";
|
||||
|
||||
wpi::StringRef query;
|
||||
if (url.HasQuery()) query = url.GetQuery();
|
||||
//wpi::errs() << "query: \"" << query << "\"\n";
|
||||
|
||||
const bool isGET = m_request.GetMethod() == wpi::HTTP_GET;
|
||||
if (isGET && (path.equals("/") || path.equals("/index.html"))) {
|
||||
SendStaticResponse(200, "OK", "text/html", GetResource_index_html(), false);
|
||||
} else if (isGET && path.equals("/frcvision.css")) {
|
||||
SendStaticResponse(200, "OK", "text/css", GetResource_frcvision_css(),
|
||||
false);
|
||||
} else if (isGET && path.equals("/frcvision.js")) {
|
||||
SendStaticResponse(200, "OK", "text/javascript", GetResource_frcvision_js(),
|
||||
false);
|
||||
} else if (isGET && path.equals("/bootstrap.min.js")) {
|
||||
SendStaticResponse(200, "OK", "text/javascript",
|
||||
wpi::GetResource_bootstrap_4_1_min_js_gz(), true);
|
||||
} else if (isGET && path.equals("/coreui.min.css")) {
|
||||
SendStaticResponse(200, "OK", "text/css",
|
||||
wpi::GetResource_coreui_2_1_min_css_gz(), true);
|
||||
} else if (isGET && path.equals("/coreui.min.js")) {
|
||||
SendStaticResponse(200, "OK", "text/javascript",
|
||||
wpi::GetResource_coreui_2_1_min_js_gz(), true);
|
||||
} else if (isGET && path.equals("/feather.min.js")) {
|
||||
SendStaticResponse(200, "OK", "text/javascript",
|
||||
wpi::GetResource_feather_4_8_min_js_gz(), true);
|
||||
} else if (isGET && path.equals("/jquery-3.3.1.slim.min.js")) {
|
||||
SendStaticResponse(200, "OK", "text/javascript",
|
||||
wpi::GetResource_jquery_3_3_slim_min_js_gz(), true);
|
||||
} else if (isGET && path.equals("/popper.min.js")) {
|
||||
SendStaticResponse(200, "OK", "text/javascript",
|
||||
wpi::GetResource_popper_1_14_min_js_gz(), true);
|
||||
} else if (isGET && path.equals("/wpilib.png")) {
|
||||
SendStaticResponse(200, "OK", "image/png",
|
||||
wpi::GetResource_wpilib_128_png(), false);
|
||||
} else {
|
||||
SendError(404, "Resource not found");
|
||||
}
|
||||
}
|
28
deps/tools/rpiConfigServer_src/MyHttpConnection.h
vendored
Normal file
28
deps/tools/rpiConfigServer_src/MyHttpConnection.h
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#ifndef WPIUTIL_MYHTTPCONNECTION_H_
|
||||
#define WPIUTIL_MYHTTPCONNECTION_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "wpi/HttpServerConnection.h"
|
||||
#include "wpi/WebSocketServer.h"
|
||||
#include "wpi/uv/Stream.h"
|
||||
|
||||
class MyHttpConnection : public wpi::HttpServerConnection,
|
||||
public std::enable_shared_from_this<MyHttpConnection> {
|
||||
public:
|
||||
explicit MyHttpConnection(std::shared_ptr<wpi::uv::Stream> stream);
|
||||
|
||||
protected:
|
||||
void ProcessRequest() override;
|
||||
|
||||
wpi::WebSocketServerHelper m_websocketHelper;
|
||||
};
|
||||
|
||||
#endif // WPIUTIL_MYHTTPCONNECTION_H_
|
209
deps/tools/rpiConfigServer_src/SystemStatus.cpp
vendored
Normal file
209
deps/tools/rpiConfigServer_src/SystemStatus.cpp
vendored
Normal file
|
@ -0,0 +1,209 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "SystemStatus.h"
|
||||
|
||||
#include "wpi/SmallString.h"
|
||||
#include "wpi/SmallVector.h"
|
||||
#include "wpi/StringRef.h"
|
||||
#include "wpi/json.h"
|
||||
#include "wpi/raw_istream.h"
|
||||
#include "wpi/raw_ostream.h"
|
||||
|
||||
std::shared_ptr<SystemStatus> SystemStatus::GetInstance() {
|
||||
static auto sysStatus = std::make_shared<SystemStatus>(private_init{});
|
||||
return sysStatus;
|
||||
}
|
||||
|
||||
void SystemStatus::UpdateAll() {
|
||||
UpdateMemory();
|
||||
UpdateCpu();
|
||||
UpdateNetwork();
|
||||
status(GetStatusJson());
|
||||
writable(GetWritable());
|
||||
}
|
||||
|
||||
wpi::json SystemStatus::GetStatusJson() {
|
||||
wpi::json j = {{"type", "systemStatus"}};
|
||||
|
||||
size_t qty;
|
||||
|
||||
// memory
|
||||
{
|
||||
uint64_t first;
|
||||
if (m_memoryFree.GetFirstLast(&first, nullptr, &qty)) {
|
||||
j["systemMemoryFree1s"] = first / 1000;
|
||||
if (qty >= 5)
|
||||
j["systemMemoryFree5s"] = m_memoryFree.GetTotal() / qty / 1000;
|
||||
}
|
||||
if (m_memoryAvail.GetFirstLast(&first, nullptr, &qty)) {
|
||||
j["systemMemoryAvail1s"] = first / 1000;
|
||||
if (qty >= 5)
|
||||
j["systemMemoryAvail5s"] = m_memoryAvail.GetTotal() / qty / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// cpu
|
||||
{
|
||||
CpuData first, last;
|
||||
if (m_cpu.GetFirstLast(&first, &last, nullptr, 2)) {
|
||||
uint64_t deltaTotal = last.total - first.total;
|
||||
if (deltaTotal != 0) {
|
||||
j["systemCpuUser1s"] =
|
||||
(last.user + last.nice - first.user - first.nice) * 100 /
|
||||
deltaTotal;
|
||||
j["systemCpuSystem1s"] =
|
||||
(last.system - first.system) * 100 / deltaTotal;
|
||||
j["systemCpuIdle1s"] = (last.idle - first.idle) * 100 / deltaTotal;
|
||||
}
|
||||
}
|
||||
if (m_cpu.GetFirstLast(&first, &last, nullptr, 6)) {
|
||||
uint64_t deltaTotal = last.total - first.total;
|
||||
if (deltaTotal != 0) {
|
||||
j["systemCpuUser5s"] =
|
||||
(last.user + last.nice - first.user - first.nice) * 100 /
|
||||
deltaTotal;
|
||||
j["systemCpuSystem5s"] =
|
||||
(last.system - first.system) * 100 / deltaTotal;
|
||||
j["systemCpuIdle5s"] = (last.idle - first.idle) * 100 / deltaTotal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// network
|
||||
{
|
||||
NetworkData first, last;
|
||||
if (m_network.GetFirstLast(&first, &last, nullptr, 2)) {
|
||||
j["systemNetwork1s"] = (last.recvBytes + last.xmitBytes -
|
||||
first.recvBytes - first.xmitBytes) *
|
||||
8 / 1000;
|
||||
}
|
||||
if (m_network.GetFirstLast(&first, &last, nullptr, 6)) {
|
||||
j["systemNetwork5s"] = (last.recvBytes + last.xmitBytes -
|
||||
first.recvBytes - first.xmitBytes) *
|
||||
8 / 5000;
|
||||
}
|
||||
}
|
||||
|
||||
return j;
|
||||
}
|
||||
|
||||
bool SystemStatus::GetWritable() {
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_istream is("/proc/mounts", ec);
|
||||
if (ec) return false;
|
||||
wpi::SmallString<256> lineBuf;
|
||||
while (!is.has_error()) {
|
||||
wpi::StringRef line = is.getline(lineBuf, 256).trim();
|
||||
if (line.empty()) break;
|
||||
|
||||
wpi::SmallVector<wpi::StringRef, 8> strs;
|
||||
line.split(strs, ' ', -1, false);
|
||||
if (strs.size() < 4) continue;
|
||||
|
||||
if (strs[1] == "/") return strs[2].contains("rw");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SystemStatus::UpdateMemory() {
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_istream is("/proc/meminfo", ec);
|
||||
if (ec) return;
|
||||
wpi::SmallString<256> lineBuf;
|
||||
while (!is.has_error()) {
|
||||
wpi::StringRef line = is.getline(lineBuf, 256).trim();
|
||||
if (line.empty()) break;
|
||||
|
||||
wpi::StringRef name, amtStr;
|
||||
std::tie(name, amtStr) = line.split(':');
|
||||
|
||||
uint64_t amt;
|
||||
amtStr = amtStr.trim();
|
||||
if (amtStr.consumeInteger(10, amt)) continue;
|
||||
|
||||
if (name == "MemFree") {
|
||||
m_memoryFree.Add(amt);
|
||||
} else if (name == "MemAvailable") {
|
||||
m_memoryAvail.Add(amt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SystemStatus::UpdateCpu() {
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_istream is("/proc/stat", ec);
|
||||
if (ec) return;
|
||||
wpi::SmallString<256> lineBuf;
|
||||
while (!is.has_error()) {
|
||||
wpi::StringRef line = is.getline(lineBuf, 256).trim();
|
||||
if (line.empty()) break;
|
||||
|
||||
wpi::StringRef name, amtStr;
|
||||
std::tie(name, amtStr) = line.split(' ');
|
||||
if (name == "cpu") {
|
||||
CpuData data;
|
||||
|
||||
// individual values we care about
|
||||
amtStr = amtStr.ltrim();
|
||||
if (amtStr.consumeInteger(10, data.user)) break;
|
||||
amtStr = amtStr.ltrim();
|
||||
if (amtStr.consumeInteger(10, data.nice)) break;
|
||||
amtStr = amtStr.ltrim();
|
||||
if (amtStr.consumeInteger(10, data.system)) break;
|
||||
amtStr = amtStr.ltrim();
|
||||
if (amtStr.consumeInteger(10, data.idle)) break;
|
||||
|
||||
// compute total
|
||||
data.total = data.user + data.nice + data.system + data.idle;
|
||||
for (;;) {
|
||||
uint64_t amt;
|
||||
amtStr = amtStr.ltrim();
|
||||
if (amtStr.consumeInteger(10, amt)) break;
|
||||
data.total += amt;
|
||||
}
|
||||
|
||||
m_cpu.Add(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SystemStatus::UpdateNetwork() {
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_istream is("/proc/net/dev", ec);
|
||||
if (ec) return;
|
||||
|
||||
NetworkData data;
|
||||
|
||||
wpi::SmallString<256> lineBuf;
|
||||
while (!is.has_error()) {
|
||||
wpi::StringRef line = is.getline(lineBuf, 256).trim();
|
||||
if (line.empty()) break;
|
||||
|
||||
wpi::StringRef name, amtStr;
|
||||
std::tie(name, amtStr) = line.split(':');
|
||||
name = name.trim();
|
||||
if (name.empty() || name == "lo") continue;
|
||||
|
||||
wpi::SmallVector<wpi::StringRef, 20> amtStrs;
|
||||
amtStr.split(amtStrs, ' ', -1, false);
|
||||
if (amtStrs.size() < 16) continue;
|
||||
|
||||
uint64_t amt;
|
||||
|
||||
// receive bytes
|
||||
if (amtStrs[0].getAsInteger(10, amt)) continue;
|
||||
data.recvBytes += amt;
|
||||
|
||||
// transmit bytes
|
||||
if (amtStrs[8].getAsInteger(10, amt)) continue;
|
||||
data.xmitBytes += amt;
|
||||
}
|
||||
|
||||
m_network.Add(data);
|
||||
}
|
60
deps/tools/rpiConfigServer_src/SystemStatus.h
vendored
Normal file
60
deps/tools/rpiConfigServer_src/SystemStatus.h
vendored
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#ifndef WPIUTIL_SYSTEMSTATUS_H_
|
||||
#define WPIUTIL_SYSTEMSTATUS_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "DataHistory.h"
|
||||
#include "wpi/Signal.h"
|
||||
|
||||
namespace wpi {
|
||||
class json;
|
||||
} // namespace wpi
|
||||
|
||||
class SystemStatus {
|
||||
struct private_init {};
|
||||
|
||||
public:
|
||||
explicit SystemStatus(const private_init&) {}
|
||||
SystemStatus(const SystemStatus&) = delete;
|
||||
SystemStatus& operator=(const SystemStatus&) = delete;
|
||||
|
||||
void UpdateAll();
|
||||
|
||||
wpi::json GetStatusJson();
|
||||
bool GetWritable();
|
||||
|
||||
wpi::sig::Signal<const wpi::json&> status;
|
||||
wpi::sig::Signal<bool> writable;
|
||||
|
||||
static std::shared_ptr<SystemStatus> GetInstance();
|
||||
|
||||
private:
|
||||
void UpdateMemory();
|
||||
void UpdateCpu();
|
||||
void UpdateNetwork();
|
||||
|
||||
DataHistory<uint64_t, 5> m_memoryFree;
|
||||
DataHistory<uint64_t, 5> m_memoryAvail;
|
||||
struct CpuData {
|
||||
uint64_t user;
|
||||
uint64_t nice;
|
||||
uint64_t system;
|
||||
uint64_t idle;
|
||||
uint64_t total;
|
||||
};
|
||||
DataHistory<CpuData, 6> m_cpu;
|
||||
struct NetworkData {
|
||||
uint64_t recvBytes = 0;
|
||||
uint64_t xmitBytes = 0;
|
||||
};
|
||||
DataHistory<NetworkData, 6> m_network;
|
||||
};
|
||||
|
||||
#endif // WPIUTIL_SYSTEMSTATUS_H_
|
188
deps/tools/rpiConfigServer_src/VisionStatus.cpp
vendored
Normal file
188
deps/tools/rpiConfigServer_src/VisionStatus.cpp
vendored
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "VisionStatus.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "wpi/SmallString.h"
|
||||
#include "wpi/StringRef.h"
|
||||
#include "wpi/json.h"
|
||||
#include "wpi/raw_ostream.h"
|
||||
#include "wpi/uv/Buffer.h"
|
||||
#include "wpi/uv/Pipe.h"
|
||||
#include "wpi/uv/Process.h"
|
||||
#include "wpi/uv/Work.h"
|
||||
|
||||
namespace uv = wpi::uv;
|
||||
|
||||
#define SERVICE "/service/camera"
|
||||
|
||||
std::shared_ptr<VisionStatus> VisionStatus::GetInstance() {
|
||||
static auto visStatus = std::make_shared<VisionStatus>(private_init{});
|
||||
return visStatus;
|
||||
}
|
||||
|
||||
void VisionStatus::RunSvc(const char* cmd,
|
||||
std::function<void(wpi::StringRef)> onFail) {
|
||||
#ifndef _WIN32
|
||||
struct SvcWorkReq : public uv::WorkReq {
|
||||
SvcWorkReq(const char* cmd_, std::function<void(wpi::StringRef)> onFail_)
|
||||
: cmd(cmd_), onFail(onFail_) {}
|
||||
const char* cmd;
|
||||
std::function<void(wpi::StringRef)> onFail;
|
||||
wpi::SmallString<128> err;
|
||||
};
|
||||
|
||||
auto workReq = std::make_shared<SvcWorkReq>(cmd, onFail);
|
||||
workReq->work.connect([r = workReq.get()] {
|
||||
int fd = open(SERVICE "/supervise/control", O_WRONLY | O_NDELAY);
|
||||
if (fd == -1) {
|
||||
wpi::raw_svector_ostream os(r->err);
|
||||
if (errno == ENXIO)
|
||||
os << "unable to control service: supervise not running";
|
||||
else
|
||||
os << "unable to control service: " << std::strerror(errno);
|
||||
} else {
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
|
||||
if (write(fd, r->cmd, std::strlen(r->cmd)) == -1) {
|
||||
wpi::raw_svector_ostream os(r->err);
|
||||
os << "error writing command: " << std::strerror(errno);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
});
|
||||
workReq->afterWork.connect([r = workReq.get()] {
|
||||
if (r->onFail && !r->err.empty()) r->onFail(r->err.str());
|
||||
});
|
||||
|
||||
uv::QueueWork(m_loop, workReq);
|
||||
#endif
|
||||
}
|
||||
|
||||
void VisionStatus::Up(std::function<void(wpi::StringRef)> onFail) {
|
||||
RunSvc("u", onFail);
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void VisionStatus::Down(std::function<void(wpi::StringRef)> onFail) {
|
||||
RunSvc("d", onFail);
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void VisionStatus::Terminate(std::function<void(wpi::StringRef)> onFail) {
|
||||
RunSvc("t", onFail);
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void VisionStatus::Kill(std::function<void(wpi::StringRef)> onFail) {
|
||||
RunSvc("k", onFail);
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void VisionStatus::UpdateStatus() {
|
||||
#ifndef _WIN32
|
||||
struct StatusWorkReq : public uv::WorkReq {
|
||||
bool enabled = false;
|
||||
wpi::SmallString<128> status;
|
||||
};
|
||||
|
||||
auto workReq = std::make_shared<StatusWorkReq>();
|
||||
|
||||
workReq->work.connect([r = workReq.get()] {
|
||||
wpi::raw_svector_ostream os(r->status);
|
||||
|
||||
// check to make sure supervise is running
|
||||
int fd = open(SERVICE "/supervise/ok", O_WRONLY | O_NDELAY);
|
||||
if (fd == -1) {
|
||||
if (errno == ENXIO)
|
||||
os << "supervise not running";
|
||||
else
|
||||
os << "unable to open supervise/ok: " << std::strerror(errno);
|
||||
return;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
// read the status data
|
||||
fd = open(SERVICE "/supervise/status", O_RDONLY | O_NDELAY);
|
||||
if (fd == -1) {
|
||||
os << "unable to open supervise/status: " << std::strerror(errno);
|
||||
return;
|
||||
}
|
||||
uint8_t status[18];
|
||||
int nr = read(fd, status, sizeof status);
|
||||
close(fd);
|
||||
if (nr < static_cast<int>(sizeof status)) {
|
||||
os << "unable to read supervise/status: ";
|
||||
if (nr == -1)
|
||||
os << std::strerror(errno);
|
||||
else
|
||||
os << "bad format";
|
||||
return;
|
||||
}
|
||||
|
||||
// decode the status data (based on daemontools svstat.c)
|
||||
uint32_t pid = (static_cast<uint32_t>(status[15]) << 24) |
|
||||
(static_cast<uint32_t>(status[14]) << 16) |
|
||||
(static_cast<uint32_t>(status[13]) << 8) |
|
||||
(static_cast<uint32_t>(status[12]));
|
||||
bool paused = status[16];
|
||||
auto want = status[17];
|
||||
uint64_t when = (static_cast<uint64_t>(status[0]) << 56) |
|
||||
(static_cast<uint64_t>(status[1]) << 48) |
|
||||
(static_cast<uint64_t>(status[2]) << 40) |
|
||||
(static_cast<uint64_t>(status[3]) << 32) |
|
||||
(static_cast<uint64_t>(status[4]) << 24) |
|
||||
(static_cast<uint64_t>(status[5]) << 16) |
|
||||
(static_cast<uint64_t>(status[6]) << 8) |
|
||||
(static_cast<uint64_t>(status[7]));
|
||||
|
||||
// constant is from daemontools tai.h
|
||||
uint64_t now =
|
||||
4611686018427387914ULL + static_cast<uint64_t>(std::time(nullptr));
|
||||
if (now >= when)
|
||||
when = now - when;
|
||||
else
|
||||
when = 0;
|
||||
|
||||
// convert to status string
|
||||
if (pid)
|
||||
os << "up (pid " << pid << ") ";
|
||||
else
|
||||
os << "down ";
|
||||
os << when << " seconds";
|
||||
if (pid && paused) os << ", paused";
|
||||
if (!pid && want == 'u') os << ", want up";
|
||||
if (pid && want == 'd') os << ", want down";
|
||||
|
||||
if (pid) r->enabled = true;
|
||||
});
|
||||
|
||||
workReq->afterWork.connect([this, r = workReq.get()] {
|
||||
wpi::json j = {{"type", "visionStatus"},
|
||||
{"visionServiceEnabled", r->enabled},
|
||||
{"visionServiceStatus", r->status.str()}};
|
||||
update(j);
|
||||
});
|
||||
|
||||
uv::QueueWork(m_loop, workReq);
|
||||
#endif
|
||||
}
|
||||
|
||||
void VisionStatus::ConsoleLog(uv::Buffer& buf, size_t len) {
|
||||
wpi::json j = {{"type", "visionLog"},
|
||||
{"data", wpi::StringRef(buf.base, len)}};
|
||||
log(j);
|
||||
}
|
57
deps/tools/rpiConfigServer_src/VisionStatus.h
vendored
Normal file
57
deps/tools/rpiConfigServer_src/VisionStatus.h
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#ifndef WPIUTIL_VISIONSTATUS_H_
|
||||
#define WPIUTIL_VISIONSTATUS_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "wpi/Signal.h"
|
||||
#include "wpi/StringRef.h"
|
||||
|
||||
namespace wpi {
|
||||
class json;
|
||||
|
||||
namespace uv {
|
||||
class Buffer;
|
||||
class Loop;
|
||||
} // namespace uv
|
||||
} // namespace wpi
|
||||
|
||||
class VisionStatus {
|
||||
struct private_init {};
|
||||
|
||||
public:
|
||||
explicit VisionStatus(const private_init&) {}
|
||||
VisionStatus(const VisionStatus&) = delete;
|
||||
VisionStatus& operator=(const VisionStatus&) = delete;
|
||||
|
||||
void SetLoop(std::shared_ptr<wpi::uv::Loop> loop) {
|
||||
m_loop = std::move(loop);
|
||||
}
|
||||
|
||||
void Up(std::function<void(wpi::StringRef)> onFail);
|
||||
void Down(std::function<void(wpi::StringRef)> onFail);
|
||||
void Terminate(std::function<void(wpi::StringRef)> onFail);
|
||||
void Kill(std::function<void(wpi::StringRef)> onFail);
|
||||
|
||||
void UpdateStatus();
|
||||
void ConsoleLog(wpi::uv::Buffer& buf, size_t len);
|
||||
|
||||
wpi::sig::Signal<const wpi::json&> update;
|
||||
wpi::sig::Signal<const wpi::json&> log;
|
||||
|
||||
static std::shared_ptr<VisionStatus> GetInstance();
|
||||
|
||||
private:
|
||||
void RunSvc(const char* cmd, std::function<void(wpi::StringRef)> onFail);
|
||||
|
||||
std::shared_ptr<wpi::uv::Loop> m_loop;
|
||||
};
|
||||
|
||||
#endif // WPIUTIL_VISIONSTATUS_H_
|
203
deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp
vendored
Normal file
203
deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp
vendored
Normal file
|
@ -0,0 +1,203 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "WebSocketHandlers.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "SystemStatus.h"
|
||||
#include "VisionStatus.h"
|
||||
#include "wpi/SmallVector.h"
|
||||
#include "wpi/WebSocket.h"
|
||||
#include "wpi/json.h"
|
||||
#include "wpi/raw_ostream.h"
|
||||
#include "wpi/raw_uv_ostream.h"
|
||||
#include "wpi/uv/Loop.h"
|
||||
#include "wpi/uv/Pipe.h"
|
||||
#include "wpi/uv/Process.h"
|
||||
|
||||
namespace uv = wpi::uv;
|
||||
|
||||
#define SERVICE "/service/camera"
|
||||
|
||||
struct WebSocketData {
|
||||
bool visionLogEnabled = false;
|
||||
|
||||
wpi::sig::ScopedConnection sysStatusConn;
|
||||
wpi::sig::ScopedConnection sysWritableConn;
|
||||
wpi::sig::ScopedConnection visStatusConn;
|
||||
wpi::sig::ScopedConnection visLogConn;
|
||||
};
|
||||
|
||||
static void SendWsText(wpi::WebSocket& ws, const wpi::json& j) {
|
||||
wpi::SmallVector<uv::Buffer, 4> toSend;
|
||||
wpi::raw_uv_ostream os{toSend, 4096};
|
||||
os << j;
|
||||
ws.SendText(toSend, [](wpi::MutableArrayRef<uv::Buffer> bufs, uv::Error) {
|
||||
for (auto&& buf : bufs) buf.Deallocate();
|
||||
});
|
||||
}
|
||||
|
||||
template <typename OnSuccessFunc, typename OnFailFunc, typename... Args>
|
||||
static void RunProcess(wpi::WebSocket& ws, OnSuccessFunc success,
|
||||
OnFailFunc fail, const wpi::Twine& file,
|
||||
const Args&... args) {
|
||||
uv::Loop& loop = ws.GetStream().GetLoopRef();
|
||||
|
||||
// create pipe to capture stderr
|
||||
auto pipe = uv::Pipe::Create(loop);
|
||||
if (auto proc = uv::Process::Spawn(
|
||||
loop, file,
|
||||
pipe ? uv::Process::StdioCreatePipe(2, *pipe, UV_WRITABLE_PIPE)
|
||||
: uv::Process::Option(),
|
||||
args...)) {
|
||||
// capture stderr output into string
|
||||
auto output = std::make_shared<std::string>();
|
||||
if (pipe) {
|
||||
pipe->StartRead();
|
||||
pipe->data.connect([output](uv::Buffer& buf, size_t len) {
|
||||
output->append(buf.base, len);
|
||||
});
|
||||
pipe->end.connect([p = pipe.get()] { p->Close(); });
|
||||
}
|
||||
|
||||
// on exit, report
|
||||
proc->exited.connect(
|
||||
[ p = proc.get(), output, s = ws.shared_from_this(), fail, success ](
|
||||
int64_t status, int sig) {
|
||||
if (status != EXIT_SUCCESS) {
|
||||
SendWsText(
|
||||
*s,
|
||||
{{"type", "status"}, {"code", status}, {"message", *output}});
|
||||
fail(*s);
|
||||
} else {
|
||||
success(*s);
|
||||
}
|
||||
p->Close();
|
||||
});
|
||||
} else {
|
||||
SendWsText(ws,
|
||||
{{"type", "status"}, {"message", "could not spawn process"}});
|
||||
fail(ws);
|
||||
}
|
||||
}
|
||||
|
||||
void InitWs(wpi::WebSocket& ws) {
|
||||
// set ws data
|
||||
auto data = std::make_shared<WebSocketData>();
|
||||
ws.SetData(data);
|
||||
|
||||
// send initial system status and hook up system status updates
|
||||
auto sysStatus = SystemStatus::GetInstance();
|
||||
|
||||
auto statusFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); };
|
||||
statusFunc(sysStatus->GetStatusJson());
|
||||
data->sysStatusConn = sysStatus->status.connect_connection(statusFunc);
|
||||
|
||||
auto writableFunc = [&ws](bool writable) {
|
||||
if (writable)
|
||||
SendWsText(ws, {{"type", "systemWritable"}});
|
||||
else
|
||||
SendWsText(ws, {{"type", "systemReadOnly"}});
|
||||
};
|
||||
writableFunc(sysStatus->GetWritable());
|
||||
data->sysWritableConn = sysStatus->writable.connect_connection(writableFunc);
|
||||
|
||||
// hook up vision status updates and logging
|
||||
auto visStatus = VisionStatus::GetInstance();
|
||||
data->visStatusConn = visStatus->update.connect_connection(
|
||||
[&ws](const wpi::json& j) { SendWsText(ws, j); });
|
||||
data->visLogConn =
|
||||
visStatus->log.connect_connection([&ws](const wpi::json& j) {
|
||||
auto d = ws.GetData<WebSocketData>();
|
||||
if (d->visionLogEnabled) SendWsText(ws, j);
|
||||
});
|
||||
visStatus->UpdateStatus();
|
||||
}
|
||||
|
||||
void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg) {
|
||||
wpi::errs() << "ws: '" << msg << "'\n";
|
||||
|
||||
// parse
|
||||
wpi::json j;
|
||||
try {
|
||||
j = wpi::json::parse(msg, nullptr, false);
|
||||
} catch (const wpi::json::parse_error& e) {
|
||||
wpi::errs() << "parse error at byte " << e.byte << ": " << e.what() << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// top level must be an object
|
||||
if (!j.is_object()) {
|
||||
wpi::errs() << "not object\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// type
|
||||
std::string type;
|
||||
try {
|
||||
type = j.at("type").get<std::string>();
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "could not read type: " << e.what() << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
wpi::outs() << "type: " << type << '\n';
|
||||
|
||||
//uv::Loop& loop = ws.GetStream().GetLoopRef();
|
||||
|
||||
wpi::StringRef t(type);
|
||||
if (t.startswith("system")) {
|
||||
wpi::StringRef subType = t.substr(6);
|
||||
|
||||
auto readOnlyFunc = [](wpi::WebSocket& s) {
|
||||
SendWsText(s, {{"type", "systemReadOnly"}});
|
||||
};
|
||||
auto writableFunc = [](wpi::WebSocket& s) {
|
||||
SendWsText(s, {{"type", "systemWritable"}});
|
||||
};
|
||||
|
||||
if (subType == "Restart") {
|
||||
RunProcess(ws, [](wpi::WebSocket&) {}, [](wpi::WebSocket&) {},
|
||||
"/sbin/reboot", "/sbin/reboot");
|
||||
} else if (subType == "ReadOnly") {
|
||||
RunProcess(
|
||||
ws, readOnlyFunc, writableFunc, "/bin/sh", "/bin/sh", "-c",
|
||||
"/bin/mount -o remount,ro / && /bin/mount -o remount,ro /boot");
|
||||
} else if (subType == "Writable") {
|
||||
RunProcess(
|
||||
ws, writableFunc, readOnlyFunc, "/bin/sh", "/bin/sh", "-c",
|
||||
"/bin/mount -o remount,rw / && /bin/mount -o remount,rw /boot");
|
||||
}
|
||||
} else if (t.startswith("vision")) {
|
||||
wpi::StringRef subType = t.substr(6);
|
||||
|
||||
auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) {
|
||||
SendWsText(*s, {{"type", "status"}, {"message", msg}});
|
||||
};
|
||||
|
||||
if (subType == "Up") {
|
||||
VisionStatus::GetInstance()->Up(statusFunc);
|
||||
} else if (subType == "Down") {
|
||||
VisionStatus::GetInstance()->Down(statusFunc);
|
||||
} else if (subType == "Term") {
|
||||
VisionStatus::GetInstance()->Terminate(statusFunc);
|
||||
} else if (subType == "Kill") {
|
||||
VisionStatus::GetInstance()->Kill(statusFunc);
|
||||
} else if (subType == "LogEnabled") {
|
||||
try {
|
||||
ws.GetData<WebSocketData>()->visionLogEnabled =
|
||||
j.at("value").get<bool>();
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "could not read visionLogEnabled value: " << e.what()
|
||||
<< '\n';
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (t == "networkSave") {
|
||||
}
|
||||
}
|
20
deps/tools/rpiConfigServer_src/WebSocketHandlers.h
vendored
Normal file
20
deps/tools/rpiConfigServer_src/WebSocketHandlers.h
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#ifndef WPIUTIL_WEBSOCKETHANDLERS_H_
|
||||
#define WPIUTIL_WEBSOCKETHANDLERS_H_
|
||||
|
||||
#include "wpi/StringRef.h"
|
||||
|
||||
namespace wpi {
|
||||
class WebSocket;
|
||||
} // namespace wpi
|
||||
|
||||
void InitWs(wpi::WebSocket& ws);
|
||||
void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg);
|
||||
|
||||
#endif // WPIUTIL_WEBSOCKETHANDLERS_H_
|
136
deps/tools/rpiConfigServer_src/main.cpp
vendored
Normal file
136
deps/tools/rpiConfigServer_src/main.cpp
vendored
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "MyHttpConnection.h"
|
||||
#include "SystemStatus.h"
|
||||
#include "VisionStatus.h"
|
||||
#include "wpi/ArrayRef.h"
|
||||
#include "wpi/StringRef.h"
|
||||
#include "wpi/raw_ostream.h"
|
||||
#include "wpi/raw_uv_ostream.h"
|
||||
#include "wpi/timestamp.h"
|
||||
#include "wpi/uv/Loop.h"
|
||||
#include "wpi/uv/Process.h"
|
||||
#include "wpi/uv/Tcp.h"
|
||||
#include "wpi/uv/Timer.h"
|
||||
#include "wpi/uv/Udp.h"
|
||||
|
||||
namespace uv = wpi::uv;
|
||||
|
||||
static uint64_t startTime = wpi::Now();
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
int port = 80;
|
||||
if (argc == 2) port = std::atoi(argv[1]);
|
||||
|
||||
uv::Process::DisableStdioInheritance();
|
||||
|
||||
SystemStatus::GetInstance();
|
||||
|
||||
auto loop = uv::Loop::Create();
|
||||
|
||||
VisionStatus::GetInstance()->SetLoop(loop);
|
||||
|
||||
loop->error.connect(
|
||||
[](uv::Error err) { wpi::errs() << "uv ERROR: " << err.str() << '\n'; });
|
||||
|
||||
auto tcp = uv::Tcp::Create(loop);
|
||||
|
||||
// bind to listen address and port
|
||||
tcp->Bind("", port);
|
||||
|
||||
// when we get a connection, accept it and start reading
|
||||
tcp->connection.connect([srv = tcp.get()] {
|
||||
auto tcp = srv->Accept();
|
||||
if (!tcp) return;
|
||||
// wpi::errs() << "Got a connection\n";
|
||||
|
||||
// Close on error
|
||||
tcp->error.connect([s = tcp.get()](wpi::uv::Error err) {
|
||||
wpi::errs() << "stream error: " << err.str() << '\n';
|
||||
s->Close();
|
||||
});
|
||||
|
||||
auto conn = std::make_shared<MyHttpConnection>(tcp);
|
||||
tcp->SetData(conn);
|
||||
});
|
||||
|
||||
// start listening for incoming connections
|
||||
tcp->Listen();
|
||||
|
||||
wpi::errs() << "Listening on port " << port << '\n';
|
||||
|
||||
// start timer to collect system and vision status
|
||||
auto timer = uv::Timer::Create(loop);
|
||||
timer->Start(std::chrono::seconds(1), std::chrono::seconds(1));
|
||||
timer->timeout.connect([&loop] {
|
||||
SystemStatus::GetInstance()->UpdateAll();
|
||||
VisionStatus::GetInstance()->UpdateStatus();
|
||||
});
|
||||
|
||||
// listen on port 6666 for console logging
|
||||
auto udpCon = uv::Udp::Create(loop);
|
||||
udpCon->Bind("127.0.0.1", 6666, UV_UDP_REUSEADDR);
|
||||
udpCon->StartRecv();
|
||||
udpCon->received.connect(
|
||||
[](uv::Buffer& buf, size_t len, const sockaddr&, unsigned) {
|
||||
VisionStatus::GetInstance()->ConsoleLog(buf, len);
|
||||
});
|
||||
|
||||
// create riolog console port
|
||||
auto tcpCon = uv::Tcp::Create(loop);
|
||||
tcpCon->Bind("", 1740);
|
||||
|
||||
// when we get a connection, accept it
|
||||
tcpCon->connection.connect([ srv = tcpCon.get(), udpCon ] {
|
||||
auto tcp = srv->Accept();
|
||||
if (!tcp) return;
|
||||
|
||||
// close on error
|
||||
tcp->error.connect([s = tcp.get()](uv::Error err) { s->Close(); });
|
||||
|
||||
// copy console log to it with headers
|
||||
udpCon->received.connect(
|
||||
[ tcpSeq = std::make_shared<uint16_t>(), tcpPtr = tcp.get() ](
|
||||
uv::Buffer & buf, size_t len, const sockaddr&, unsigned) {
|
||||
// build buffers
|
||||
wpi::SmallVector<uv::Buffer, 4> bufs;
|
||||
wpi::raw_uv_ostream out(bufs, 4096);
|
||||
|
||||
// Header is 2 byte len, 1 byte type, 4 byte timestamp, 2 byte
|
||||
// sequence num
|
||||
uint32_t ts = wpi::FloatToBits((wpi::Now() - startTime) * 1.0e-6);
|
||||
uint16_t pktlen = len + 1 + 4 + 2;
|
||||
out << wpi::ArrayRef<uint8_t>(
|
||||
{static_cast<uint8_t>((pktlen >> 8) & 0xff),
|
||||
static_cast<uint8_t>(pktlen & 0xff), 12,
|
||||
static_cast<uint8_t>((ts >> 24) & 0xff),
|
||||
static_cast<uint8_t>((ts >> 16) & 0xff),
|
||||
static_cast<uint8_t>((ts >> 8) & 0xff),
|
||||
static_cast<uint8_t>(ts & 0xff),
|
||||
static_cast<uint8_t>((*tcpSeq >> 8) & 0xff),
|
||||
static_cast<uint8_t>(*tcpSeq & 0xff)});
|
||||
out << wpi::StringRef(buf.base, len);
|
||||
(*tcpSeq)++;
|
||||
|
||||
// send output
|
||||
tcpPtr->Write(bufs, [](auto bufs2, uv::Error) {
|
||||
for (auto buf : bufs2) buf.Deallocate();
|
||||
});
|
||||
},
|
||||
tcp);
|
||||
});
|
||||
|
||||
// start listening for incoming connections
|
||||
tcpCon->Listen();
|
||||
|
||||
// run loop
|
||||
loop->Run();
|
||||
}
|
69
deps/tools/rpiConfigServer_src/resources/frcvision.css
vendored
Normal file
69
deps/tools/rpiConfigServer_src/resources/frcvision.css
vendored
Normal file
|
@ -0,0 +1,69 @@
|
|||
.feather {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spinnow {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes spinnow {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-ms-keyframes spinnow {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix icon on IE */
|
||||
_:-ms-lang(x), .brand-icon {
|
||||
height: 75%;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spinnow 5s infinite linear;
|
||||
}
|
||||
|
||||
/*
|
||||
* Console Log
|
||||
*/
|
||||
.log {
|
||||
white-space: pre-wrap;
|
||||
color: black;
|
||||
font-size: 0.85em;
|
||||
background: inherit;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
height: 400px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.log .inner-line {
|
||||
padding: 0 15px;
|
||||
margin-left: 84pt;
|
||||
text-indent: -84pt;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.log .inner-line:empty::after {
|
||||
content: '.';
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.log.no-indent .inner-line {
|
||||
margin-left: 0;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.log .line-selected {
|
||||
background-color: #ffb2b0;
|
||||
}
|
346
deps/tools/rpiConfigServer_src/resources/frcvision.js
vendored
Normal file
346
deps/tools/rpiConfigServer_src/resources/frcvision.js
vendored
Normal file
|
@ -0,0 +1,346 @@
|
|||
"use strict";
|
||||
var connection = null;
|
||||
|
||||
var WebSocket = WebSocket || MozWebSocket;
|
||||
|
||||
/*
|
||||
// Implement bootstrap 3 style button loading support
|
||||
(function($) {
|
||||
$.fn.button = function(action) {
|
||||
if (action === 'loading' && this.data('loading-text')) {
|
||||
this.data('original-text', this.html()).html(this.data('loading-text')).prop('disabled', true);
|
||||
}
|
||||
if (action === 'reset' && this.data('original-text')) {
|
||||
this.html(this.data('original-text')).prop('disabled', false);
|
||||
}
|
||||
feather.replace();
|
||||
};
|
||||
}(jQuery));
|
||||
*/
|
||||
|
||||
// HTML escaping
|
||||
var entityMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
function escapeHtml(string) {
|
||||
return String(string).replace(/[&<>"'`=\/]/g, function (s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
function displayStatus(message) {
|
||||
$('#status-content').html('<div id="status" class="alert alert-warning alert-dismissable fade show" role="alert"><span>' + escapeHtml(message) + '</span><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button></div>');
|
||||
}
|
||||
|
||||
// Enable and disable buttons based on connection status
|
||||
var connectedButtonIds = ['systemRestart', 'networkApproach', 'visionUp', 'visionDown', 'visionTerm', 'visionKill', 'systemReadOnly', 'systemWritable'];
|
||||
var writableButtonIds = ['networkSave'];
|
||||
var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s',
|
||||
'systemMemoryAvail1s', 'systemMemoryAvail5s',
|
||||
'systemCpuUser1s', 'systemCpuUser5s',
|
||||
'systemCpuSystem1s', 'systemCpuSystem5s',
|
||||
'systemCpuIdle1s', 'systemCpuIdle5s',
|
||||
'systemNetwork1s', 'systemNetwork5s'];
|
||||
|
||||
function displayDisconnected() {
|
||||
displayReadOnly();
|
||||
$('#connectionBadge').removeClass('badge-primary').addClass('badge-secondary').text('Disconnected');
|
||||
$('#visionServiceStatus').removeClass('badge-primary').removeClass('badge-secondary').addClass('badge-dark').text('Unknown Status');
|
||||
for (var i = 0; i < connectedButtonIds.length; i++) {
|
||||
$('#' + connectedButtonIds[i]).prop('disabled', true);
|
||||
}
|
||||
for (var i = 0; i < systemStatusIds.length; i++) {
|
||||
$('#' + systemStatusIds[i]).text("");
|
||||
}
|
||||
}
|
||||
|
||||
function displayConnected() {
|
||||
$('#connectionBadge').removeClass('badge-secondary').addClass('badge-primary').text('Connected');
|
||||
for (var i = 0; i < connectedButtonIds.length; i++) {
|
||||
$('#' + connectedButtonIds[i]).prop('disabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable and disable buttons based on writable status
|
||||
function displayReadOnly() {
|
||||
for (var i = 0; i < writableButtonIds.length; i++) {
|
||||
$('#' + writableButtonIds[i]).prop('disabled', true);
|
||||
}
|
||||
$('#systemReadOnly').addClass('active').prop('aria-pressed', true);
|
||||
$('#systemWritable').removeClass('active').prop('aria-pressed', false);
|
||||
}
|
||||
|
||||
function displayWritable() {
|
||||
for (var i = 0; i < writableButtonIds.length; i++) {
|
||||
$('#' + writableButtonIds[i]).prop('disabled', false);
|
||||
}
|
||||
$('#systemReadOnly').removeClass('active').prop('aria-pressed', false);
|
||||
$('#systemWritable').addClass('active').prop('aria-pressed', true);
|
||||
}
|
||||
|
||||
// Handle Read-Only and Writable buttons
|
||||
$('#systemReadOnly').click(function() {
|
||||
var $this = $(this);
|
||||
if ($this.hasClass('active')) return;
|
||||
var msg = {
|
||||
type: 'systemReadOnly'
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
$('#systemWritable').click(function() {
|
||||
var $this = $(this);
|
||||
if ($this.hasClass('active')) return;
|
||||
var msg = {
|
||||
type: 'systemWritable'
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
// WebSocket automatic reconnection timer
|
||||
var reconnectTimerId = 0;
|
||||
|
||||
// Establish WebSocket connection
|
||||
function connect() {
|
||||
if (connection && connection.readyState !== WebSocket.CLOSED) return;
|
||||
var serverUrl = "ws://" + window.location.hostname;
|
||||
if (window.location.port !== '') {
|
||||
serverUrl += ':' + window.location.port;
|
||||
}
|
||||
connection = new WebSocket(serverUrl, 'frcvision');
|
||||
connection.onopen = function(evt) {
|
||||
if (reconnectTimerId) {
|
||||
window.clearInterval(reconnectTimerId);
|
||||
reconnectTimerId = 0;
|
||||
}
|
||||
displayConnected();
|
||||
};
|
||||
connection.onclose = function(evt) {
|
||||
displayDisconnected();
|
||||
if (!reconnectTimerId) {
|
||||
reconnectTimerId = setInterval(function() { connect(); }, 2000);
|
||||
}
|
||||
};
|
||||
// WebSocket incoming message handling
|
||||
connection.onmessage = function(evt) {
|
||||
var msg = JSON.parse(evt.data);
|
||||
switch (msg.type) {
|
||||
case 'systemStatus':
|
||||
for (var i = 0; i < systemStatusIds.length; i++) {
|
||||
$('#' + systemStatusIds[i]).text(msg[systemStatusIds[i]]);
|
||||
}
|
||||
break;
|
||||
case 'visionStatus':
|
||||
var elem = $('#visionServiceStatus');
|
||||
if (msg.visionServiceStatus) {
|
||||
elem.text(msg.visionServiceStatus);
|
||||
}
|
||||
if (msg.visionServiceEnabled && !elem.hasClass('badge-primary')) {
|
||||
elem.removeClass('badge-dark').removeClass('badge-secondary').addClass('badge-primary');
|
||||
} else if (!msg.visionServiceEnabled && !elem.hasClass('badge-secondary')) {
|
||||
elem.removeClass('badge-dark').removeClass('badge-primary').addClass('badge-secondary');
|
||||
}
|
||||
break;
|
||||
case 'visionLog':
|
||||
visionLog(msg.data);
|
||||
break;
|
||||
case 'networkSettings':
|
||||
$('#networkApproach').value = msg.networkApproach;
|
||||
$('#networkAddress').value = msg.networkAddress;
|
||||
$('#networkMask').value = msg.networkMask;
|
||||
$('#networkGateway').value = msg.networkGateway;
|
||||
$('#networkDNS').value = msg.networkDNS;
|
||||
break;
|
||||
case 'systemReadOnly':
|
||||
displayReadOnly();
|
||||
break;
|
||||
case 'systemWritable':
|
||||
displayWritable();
|
||||
break;
|
||||
case 'status':
|
||||
displayStatus(msg.message);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Button handlers
|
||||
$('#systemRestart').click(function() {
|
||||
var msg = {
|
||||
type: 'systemRestart'
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
$('#visionUp').click(function() {
|
||||
var msg = {
|
||||
type: 'visionUp'
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
$('#visionDown').click(function() {
|
||||
var msg = {
|
||||
type: 'visionDown'
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
$('#visionTerm').click(function() {
|
||||
var msg = {
|
||||
type: 'visionTerm'
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
$('#visionKill').click(function() {
|
||||
var msg = {
|
||||
type: 'visionKill'
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
$('#visionLogEnabled').change(function() {
|
||||
var msg = {
|
||||
type: 'visionLogEnabled',
|
||||
value: this.checked
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
//
|
||||
// Vision console output
|
||||
//
|
||||
var visionConsole = document.getElementById('visionConsole');
|
||||
var visionLogEnabled = $('#visionLogEnabled');
|
||||
var _linesLimit = 100;
|
||||
|
||||
/*
|
||||
function escape_for_html(txt) {
|
||||
return txt.replace(/[&<>]/gm, function(str) {
|
||||
if (str == "&") return "&";
|
||||
if (str == "<") return "<";
|
||||
if (str == ">") return ">";
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
function visionLog(data) {
|
||||
if (!visionLogEnabled.prop('checked')) {
|
||||
return;
|
||||
}
|
||||
var wasScrolledBottom = (visionConsole.scrollTop === (visionConsole.scrollHeight - visionConsole.offsetHeight));
|
||||
var div = document.createElement('div');
|
||||
var p = document.createElement('p');
|
||||
p.className = 'inner-line';
|
||||
|
||||
// escape HTML tags
|
||||
data = escapeHtml(data);
|
||||
p.innerHTML = data;
|
||||
|
||||
div.className = 'line';
|
||||
div.addEventListener('click', function click() {
|
||||
if (this.className.indexOf('selected') === -1) {
|
||||
this.className = 'line-selected';
|
||||
} else {
|
||||
this.className = 'line';
|
||||
}
|
||||
});
|
||||
|
||||
div.appendChild(p);
|
||||
visionConsole.appendChild(div);
|
||||
|
||||
if (visionConsole.children.length > _linesLimit) {
|
||||
visionConsole.removeChild(visionConsole.children[0]);
|
||||
}
|
||||
|
||||
if (wasScrolledBottom) {
|
||||
visionConsole.scrollTop = visionConsole.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Show details when appropriate for network approach
|
||||
$('#networkApproach').change(function() {
|
||||
if (this.value == "dhcp") {
|
||||
$('#networkIpDetails').collapse('hide');
|
||||
} else {
|
||||
$('#networkIpDetails').collapse('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Network Save button handler
|
||||
$('#networkSave').click(function() {
|
||||
var $this = $(this);
|
||||
$this.button('loading');
|
||||
var msg = {
|
||||
type: 'networkSave',
|
||||
networkApproach: $('#networkApproach').value,
|
||||
networkAddress: $('#networkAddress').value,
|
||||
networkMask: $('#networkMask').value,
|
||||
networkGateway: $('#networkGateway').value,
|
||||
networkDNS: $('#networkDNS').value
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
// Show details when appropriate for NT client
|
||||
$('#visionClient').change(function() {
|
||||
if (this.checked) {
|
||||
$('#visionClientDetails').collapse('show');
|
||||
} else {
|
||||
$('#visionClientDetails').collapse('hide');
|
||||
}
|
||||
});
|
||||
|
||||
// Update view from data structure
|
||||
var visionSettings = {
|
||||
team: '294',
|
||||
ntmode: 'client',
|
||||
cameras: []
|
||||
};
|
||||
|
||||
function updateVisionSettingsCameraView(cardElem, data) {
|
||||
}
|
||||
|
||||
function updateVisionSettingsView() {
|
||||
$('#visionClient').prop('checked', visionSettings.ntmode === 'client');
|
||||
if (visionSettings.ntmode === 'client') {
|
||||
$('#visionClientDetails').collapse('show');
|
||||
} else {
|
||||
$('#visionClientDetails').collapse('hide');
|
||||
}
|
||||
$('#visionTeam').val(visionSettings.team);
|
||||
|
||||
var newCamera = $('#cameraNEW').clone();
|
||||
newCamera.find('[id]').each(function() {
|
||||
$(this).attr('id', $(this).attr('id').replace('NEW', ''));
|
||||
});
|
||||
newCamera.find('[for]').each(function() {
|
||||
$(this).attr('for', $(this).attr('for').replace('NEW', ''));
|
||||
});
|
||||
}
|
||||
|
||||
$('#cameraSettingsFile0').change(function() {
|
||||
if (this.files.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
var fr = new FileReader();
|
||||
fr.onload = function(e) {
|
||||
var result = JSON.parse(e.target.result);
|
||||
console.log(result);
|
||||
};
|
||||
fr.readAsText(this.files.item(0));
|
||||
});
|
||||
|
||||
// Start with display disconnected and start initial connection attempt
|
||||
displayDisconnected();
|
||||
updateVisionSettingsView();
|
||||
connect();
|
440
deps/tools/rpiConfigServer_src/resources/index.html
vendored
Normal file
440
deps/tools/rpiConfigServer_src/resources/index.html
vendored
Normal file
|
@ -0,0 +1,440 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<link rel="stylesheet" href="coreui.min.css">
|
||||
<link rel="stylesheet" href="frcvision.css">
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="wpilib.png">
|
||||
|
||||
<title>WPILib FRCVision Raspberry PI Configuration</title>
|
||||
</head>
|
||||
<body class="app header-fixed sidebar-md-show sidebar-fixed">
|
||||
<header class="app-header navbar">
|
||||
<a class="navbar-brand" href="#">
|
||||
<img src="wpilib.png" class="img-fluid ml-2 mr-2 brand-icon" alt="WPILib">
|
||||
FRCVision-rPi
|
||||
</a>
|
||||
<form class="form-inline">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-secondary active" id="systemReadOnly" aria-pressed="true" data-loading-text="<i class='spin' data-feather='loader'></i> Read-Only">Read-Only</button>
|
||||
<button type="button" class="btn btn-secondary" id="systemWritable" aria-pressed="false" data-loading-text="<i class='spin' data-feather='loader'></i> Writable">Writable</button>
|
||||
</div>
|
||||
</form>
|
||||
<span class="navbar-text mr-2">
|
||||
<span class="badge badge-secondary" id="connectionBadge">Disconnected</span>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="app-body">
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="system-status-tab" data-toggle="tab" href="#system-status">
|
||||
<i class="nav-icon" data-feather="info"></i>
|
||||
System Status
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="vision-status-tab" data-toggle="tab" href="#vision-status">
|
||||
<i class="nav-icon" data-feather="camera"></i>
|
||||
Vision Status
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="network-settings-tab" data-toggle="tab" href="#network-settings">
|
||||
<i class="nav-icon" data-feather="settings"></i>
|
||||
Network Settings
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="vision-settings-tab" data-toggle="tab" href="#vision-settings">
|
||||
<i class="nav-icon" data-feather="sliders"></i>
|
||||
Vision Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Main Pane -->
|
||||
<main class="main">
|
||||
<span id="status-content"></span>
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
<!-- System Status -->
|
||||
<div class="tab-pane show active" id="system-status" role="tabpanel" aria-labelledby="system-status-tab">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 50%;"></th>
|
||||
<th scope="col" style="width: 25%;">1 sec</th>
|
||||
<th scope="col" style="width: 25%;">5 sec</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Memory (MB Free)</th>
|
||||
<td id="systemMemoryFree1s"></td>
|
||||
<td id="systemMemoryFree5s"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Memory (MB Avail)</th>
|
||||
<td id="systemMemoryAvail1s"></td>
|
||||
<td id="systemMemoryAvail5s"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">CPU (% User)</th>
|
||||
<td id="systemCpuUser1s"></td>
|
||||
<td id="systemCpuUser5s"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">CPU (% System)</th>
|
||||
<td id="systemCpuSystem1s"></td>
|
||||
<td id="systemCpuSystem5s"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">CPU (% Idle)</th>
|
||||
<td id="systemCpuIdle1s"></td>
|
||||
<td id="systemCpuIdle5s"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Network (Kbps)</th>
|
||||
<td id="systemNetwork1s"></td>
|
||||
<td id="systemNetwork5s"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-warning" id="systemRestart">
|
||||
<i data-feather="rotate-cw"></i>
|
||||
Restart System
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Vision Status -->
|
||||
<div class="tab-pane" id="vision-status" role="tabpanel" aria-labelledby="vision-status-tab">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto mr-auto">
|
||||
<h6>Background Service (Automatic Restart)</h6>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span class="badge badge-dark align-top" id="visionServiceStatus">Unknown Status</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-sm btn-primary" id="visionUp">
|
||||
<span data-feather="play-circle"></span>
|
||||
Up
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="visionDown">
|
||||
<span data-feather="stop-circle"></span>
|
||||
Down
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-warning" id="visionTerm">
|
||||
<span data-feather="slash"></span>
|
||||
Terminate
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" id="visionKill">
|
||||
<span data-feather="x"></span>
|
||||
Kill
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto mr-auto">
|
||||
<h6>Console Output</h6>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label for="visionLogEnabled">
|
||||
Enable
|
||||
</label>
|
||||
<label class="switch switch-sm switch-pill switch-primary align-bottom">
|
||||
<input type="checkbox" class="switch-input" id="visionLogEnabled">
|
||||
<span class="switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="log" id="visionConsole"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Settings -->
|
||||
<div class="tab-pane" id="network-settings" role="tabpanel" aria-labelledby="network-settings-tab">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="networkApproach">Configure IPv4 Address</label>
|
||||
<select class="form-control" id="networkApproach">
|
||||
<option value="dhcp">DHCP</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dhcp-fallback">DHCP with Static Fallback</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="collapse" id="networkIpDetails">
|
||||
<div class="form-group">
|
||||
<label for="networkAddress">IPv4 Address</label>
|
||||
<input type=text" class="form-control" id="networkAddress">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="networkMask">Subnet Mask</label>
|
||||
<input type=text" class="form-control" id="networkMask">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="networkGateway">Gateway</label>
|
||||
<input type=text" class="form-control" id="networkGateway">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="networkDNS">DNS Server</label>
|
||||
<input type=text" class="form-control" id="networkDNS">
|
||||
</div>
|
||||
</div>
|
||||
<button id="networkSave" type="submit" class="btn btn-primary" data-loading-text="<i class='spin' data-feather='loader'></i> Saving" disabled>
|
||||
<span data-feather="save"></span>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Vision Settings -->
|
||||
<div class="tab-pane" id="vision-settings" role="tabpanel" aria-labelledby="vision-settings-tab">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Network Tables</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="visionClient">
|
||||
Client
|
||||
</label>
|
||||
<label class="switch switch-sm switch-pill switch-primary align-bottom">
|
||||
<input type="checkbox" class="switch-input" id="visionClient" checked>
|
||||
<span class="switch-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="collapse show" id="visionClientDetails">
|
||||
<div class="form-group">
|
||||
<label for="visionTeam">Team Number</label>
|
||||
<input type=text" class="form-control" id="visionTeam">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion" id="cameras">
|
||||
<div class="card" id="camera0">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto mr-auto">
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#cameraBody0">
|
||||
<h5>Camera 1</h5>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-sm btn-success" href="" target="_blank" role="button" id="cameraStream0">
|
||||
<span data-feather="play"></span>
|
||||
Open Stream
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-danger" id="cameraRemove0">
|
||||
<span data-feather="x"></span>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="cameraBody0" data-parent="#cameras">
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-row">
|
||||
<div class="form-group col">
|
||||
<label for="cameraName0">Name</label>
|
||||
<input type="text" class="form-control" id="cameraName0">
|
||||
</div>
|
||||
<div class="form-group col">
|
||||
<label for="cameraPath0">Path</label>
|
||||
<input type="text" class="form-control" id="cameraPath0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" accept=".json,text/json,application/json" id="cameraSettingsFile0">
|
||||
<label class="custom-file-label" for="cameraSettingsFile0">Load camera settings from JSON file</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraPixelFormat0">Pixel Format</label>
|
||||
<select class="form-control" id="cameraPixelFormat0">
|
||||
<option value="mjpeg">MJPEG</option>
|
||||
<option value="yuyv">YUYV</option>
|
||||
<option value="rgb565">RGB565</option>
|
||||
<option value="bgr">BGR</option>
|
||||
<option value="gray">Gray</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraWidth0">Width</label>
|
||||
<input type="text" class="form-control" id="cameraWidth0" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraHeight0">Height</label>
|
||||
<input type="text" class="form-control" id="cameraHeight0" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraFps0">FPS</label>
|
||||
<input type="text" class="form-control" id="cameraFps0" size="4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraBrightness0">Brightness</label>
|
||||
<input type="text" class="form-control" id="cameraBrightness0" size="4">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraBrightness0">White Balance</label>
|
||||
<input type="text" class="form-control" id="cameraWhiteBalance0" size="4">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraBrightness0">Exposure</label>
|
||||
<input type="text" class="form-control" id="cameraExposure0" size="4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cameraProperties0">Custom Properties JSON</label>
|
||||
<textarea class="form-control" id="cameraProperties0" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="display:none" id="cameraNEW">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto mr-auto">
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#cameraBodyNEW">
|
||||
<h5>Camera NEW</h5>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-sm btn-success" href="" target="_blank" role="button" id="cameraStreamNEW">
|
||||
<span data-feather="play"></span>
|
||||
Open Stream
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-danger" id="cameraRemoveNEW">
|
||||
<span data-feather="x"></span>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="cameraBodyNEW" data-parent="#cameras">
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-row">
|
||||
<div class="form-group col">
|
||||
<label for="cameraNameNEW">Name</label>
|
||||
<input type="text" class="form-control" id="cameraNameNEW">
|
||||
</div>
|
||||
<div class="form-group col">
|
||||
<label for="cameraPathNEW">Path</label>
|
||||
<input type="text" class="form-control" id="cameraPathNEW">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" accept=".json,text/json,application/json" id="cameraSettingsFileNEW">
|
||||
<label class="custom-file-label" for="cameraSettingsFileNEW">Load camera settings from JSON file</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraPixelFormatNEW">Pixel Format</label>
|
||||
<select class="form-control" id="cameraPixelFormatNEW">
|
||||
<option value="mjpeg">MJPEG</option>
|
||||
<option value="yuyv">YUYV</option>
|
||||
<option value="rgb565">RGB565</option>
|
||||
<option value="bgr">BGR</option>
|
||||
<option value="gray">Gray</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraWidthNEW">Width</label>
|
||||
<input type="text" class="form-control" id="cameraWidthNEW" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraHeightNEW">Height</label>
|
||||
<input type="text" class="form-control" id="cameraHeightNEW" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraFpsNEW">FPS</label>
|
||||
<input type="text" class="form-control" id="cameraFpsNEW" size="4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraBrightnessNEW">Brightness</label>
|
||||
<input type="text" class="form-control" id="cameraBrightnessNEW" size="4">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraBrightnessNEW">White Balance</label>
|
||||
<input type="text" class="form-control" id="cameraWhiteBalanceNEW" size="4">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraExposureNEW">Exposure</label>
|
||||
<input type="text" class="form-control" id="cameraExposureNEW" size="4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cameraPropertiesNEW">Custom Properties JSON</label>
|
||||
<textarea class="form-control" id="cameraPropertiesNEW" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-auto mr-auto">
|
||||
<button type="submit" class="btn btn-primary" id="visionSave">
|
||||
<span data-feather="save"></span>
|
||||
Save
|
||||
</button>
|
||||
<button type="submit" class="btn btn-secondary" id="visionDiscard">
|
||||
<span data-feather="x"></span>
|
||||
Discard Changes
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-success" id="addCamera">
|
||||
<span data-feather="plus"></span>
|
||||
Add Camera
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="jquery-3.3.1.slim.min.js"></script>
|
||||
<script type="text/javascript" src="popper.min.js"></script>
|
||||
<script type="text/javascript" src="bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="coreui.min.js"></script>
|
||||
<script type="text/javascript" src="feather.min.js"></script>
|
||||
<script>
|
||||
feather.replace()
|
||||
</script>
|
||||
<script type="text/javascript" src="frcvision.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user