Add rpiConfigServer to tools

This commit is contained in:
Peter Johnson 2018-12-11 20:04:00 -08:00
parent 88f549108f
commit 98842b6728
No known key found for this signature in database
GPG Key ID: D39DD4DA7D41E329
16 changed files with 1996 additions and 2 deletions

2
deps/allwpilib vendored

@ -1 +1 @@
Subproject commit bfe15245a625d8e30351fc1096d2df1ca71210c3 Subproject commit 7d7af287f6718c0fcb2123aca1b2cebc3177ade5

43
deps/tools/Makefile vendored
View File

@ -1,6 +1,16 @@
COMPILER=../02-extract/raspbian9/bin/arm-raspbian9-linux-gnueabihf- 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 setuidgids: setuidgids.c
${COMPILER}gcc -O -Wall -D_GNU_SOURCE -o $@ $< ${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 \ -lcscore \
-lwpiutil \ -lwpiutil \
-lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc -lopencv_core -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
View 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)

View 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_

View 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");
}
}

View 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_

View 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);
}

View 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_

View 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);
}

View 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_

View 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") {
}
}

View 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
View 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();
}

View 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;
}

View 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
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">&times;</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 "&amp;";
if (str == "<") return "&lt;";
if (str == ">") return "&gt;";
});
}
*/
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();

View 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>