Revamp image to build dependencies as part of stages (#83)
Fixes #17. Stage 2 is fairly minimal, stage 3 builds/installs OpenCV and WPILib et al, and stage 4 builds/installs the FRCVision webdash and adds the vision examples. Other changes: - OpenCV compiled with ffmpeg, OpenBLAS, and libgtk (fixes #79, fixes #80) - OpenBLAS added to image (fixes #65) - C++ Makefile is more easily extensible (fixes #71) - Sources for everything are bundled into image into /usr/src - README updated (fixes #16) - pkg-config files for wpilibc et al are now installed and C++ Makefile uses them (if compiled local to Pi) - Both dynamic and static libs are included in image The only downside of all these changes (particularly the ffmpeg, OpenBLAS, and libgtk inclusion) is the image size is now over 3GB (800MB compressed). The previous image didn't quite fit on a 2GB card however.
This commit is contained in:
+202
@@ -0,0 +1,202 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 "Application.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <wpi/FileSystem.h>
|
||||
#include <wpi/json.h>
|
||||
#include <wpi/raw_istream.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
#include "VisionStatus.h"
|
||||
|
||||
#define TYPE_TAG "### TYPE:"
|
||||
|
||||
std::shared_ptr<Application> Application::GetInstance() {
|
||||
static auto inst = std::make_shared<Application>(private_init{});
|
||||
return inst;
|
||||
}
|
||||
|
||||
void Application::Set(wpi::StringRef appType,
|
||||
std::function<void(wpi::StringRef)> onFail) {
|
||||
wpi::StringRef appDir;
|
||||
wpi::StringRef appEnv;
|
||||
wpi::StringRef appCommand;
|
||||
|
||||
if (appType == "builtin") {
|
||||
appCommand = "/usr/local/frc/bin/multiCameraServer";
|
||||
} else if (appType == "example-java") {
|
||||
appDir = "examples/java-multiCameraServer";
|
||||
appCommand =
|
||||
"env LD_LIBRARY_PATH=/usr/local/frc/lib java -jar "
|
||||
"build/libs/java-multiCameraServer-all.jar";
|
||||
} else if (appType == "example-cpp") {
|
||||
appDir = "examples/cpp-multiCameraServer";
|
||||
appCommand = "./multiCameraServerExample";
|
||||
} else if (appType == "example-python") {
|
||||
appDir = "examples/python-multiCameraServer";
|
||||
appEnv = "export PYTHONUNBUFFERED=1";
|
||||
appCommand = "./multiCameraServer.py";
|
||||
} else if (appType == "upload-java") {
|
||||
appCommand =
|
||||
"env LD_LIBRARY_PATH=/usr/local/frc/lib java -jar uploaded.jar";
|
||||
} else if (appType == "upload-cpp") {
|
||||
appCommand = "./uploaded";
|
||||
} else if (appType == "upload-python") {
|
||||
appEnv = "export PYTHONUNBUFFERED=1";
|
||||
appCommand = "./uploaded.py";
|
||||
} else if (appType == "custom") {
|
||||
return;
|
||||
} else {
|
||||
wpi::SmallString<64> msg;
|
||||
msg = "unrecognized application type '";
|
||||
msg += appType;
|
||||
msg += "'";
|
||||
onFail(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
// write file
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_ostream os(EXEC_HOME "/runCamera", ec, wpi::sys::fs::F_Text);
|
||||
if (ec) {
|
||||
onFail("could not write " EXEC_HOME "/runCamera");
|
||||
return;
|
||||
}
|
||||
os << "#!/bin/sh\n";
|
||||
os << TYPE_TAG << ' ' << appType << '\n';
|
||||
os << "echo \"Waiting 5 seconds...\"\n";
|
||||
os << "sleep 5\n";
|
||||
if (!appDir.empty()) os << "cd " << appDir << '\n';
|
||||
if (!appEnv.empty()) os << appEnv << '\n';
|
||||
os << "exec " << appCommand << '\n';
|
||||
}
|
||||
|
||||
// terminate vision process so it reloads
|
||||
VisionStatus::GetInstance()->Terminate(onFail);
|
||||
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
int Application::StartUpload(wpi::StringRef appType, char* filename,
|
||||
std::function<void(wpi::StringRef)> onFail) {
|
||||
int fd = mkstemp(filename);
|
||||
if (fd < 0) {
|
||||
wpi::SmallString<64> msg;
|
||||
msg = "could not open temporary file: ";
|
||||
msg += std::strerror(errno);
|
||||
onFail(msg);
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
void Application::Upload(int fd, bool text, wpi::ArrayRef<uint8_t> contents) {
|
||||
// write contents
|
||||
wpi::raw_fd_ostream out(fd, false);
|
||||
if (text) {
|
||||
wpi::StringRef str(reinterpret_cast<const char*>(contents.data()),
|
||||
contents.size());
|
||||
// convert any Windows EOL to Unix
|
||||
for (;;) {
|
||||
size_t idx = str.find("\r\n");
|
||||
if (idx == wpi::StringRef::npos) break;
|
||||
out << str.slice(0, idx) << '\n';
|
||||
str = str.slice(idx + 2, wpi::StringRef::npos);
|
||||
}
|
||||
out << str;
|
||||
// ensure file ends with EOL
|
||||
if (!str.empty() && str.back() != '\n') out << '\n';
|
||||
} else {
|
||||
out << contents;
|
||||
}
|
||||
}
|
||||
|
||||
void Application::FinishUpload(wpi::StringRef appType, int fd,
|
||||
const char* tmpFilename,
|
||||
std::function<void(wpi::StringRef)> onFail) {
|
||||
wpi::StringRef filename;
|
||||
if (appType == "upload-java") {
|
||||
filename = "/uploaded.jar";
|
||||
} else if (appType == "upload-cpp") {
|
||||
filename = "/uploaded";
|
||||
} else if (appType == "upload-python") {
|
||||
filename = "/uploaded.py";
|
||||
} else {
|
||||
wpi::SmallString<64> msg;
|
||||
msg = "cannot upload application type '";
|
||||
msg += appType;
|
||||
msg += "'";
|
||||
onFail(msg);
|
||||
::close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
wpi::SmallString<64> pathname;
|
||||
pathname = EXEC_HOME;
|
||||
pathname += filename;
|
||||
|
||||
// change ownership
|
||||
if (fchown(fd, APP_UID, APP_GID) == -1) {
|
||||
wpi::errs() << "could not change app ownership: " << std::strerror(errno)
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
// set file to be executable
|
||||
if (fchmod(fd, 0775) == -1) {
|
||||
wpi::errs() << "could not change app permissions: " << std::strerror(errno)
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
// close temporary file
|
||||
::close(fd);
|
||||
|
||||
// remove old file (need to do this as we can't overwrite a running exe)
|
||||
if (unlink(pathname.c_str()) == -1) {
|
||||
wpi::errs() << "could not remove app executable: " << std::strerror(errno)
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
// rename temporary file to new file
|
||||
if (rename(tmpFilename, pathname.c_str()) == -1) {
|
||||
wpi::errs() << "could not rename to app executable: "
|
||||
<< std::strerror(errno) << '\n';
|
||||
}
|
||||
|
||||
// terminate vision process so it reloads
|
||||
VisionStatus::GetInstance()->Terminate(onFail);
|
||||
}
|
||||
|
||||
void Application::UpdateStatus() { status(GetStatusJson()); }
|
||||
|
||||
wpi::json Application::GetStatusJson() {
|
||||
wpi::json j = {{"type", "applicationSettings"},
|
||||
{"applicationType", "custom"}};
|
||||
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_istream is(EXEC_HOME "/runCamera", ec);
|
||||
if (ec) {
|
||||
wpi::errs() << "could not read " EXEC_HOME "/runCamera\n";
|
||||
return j;
|
||||
}
|
||||
|
||||
// scan file
|
||||
wpi::SmallString<256> lineBuf;
|
||||
while (!is.has_error()) {
|
||||
wpi::StringRef line = is.getline(lineBuf, 256).trim();
|
||||
if (line.startswith(TYPE_TAG)) {
|
||||
j["applicationType"] = line.substr(strlen(TYPE_TAG)).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return j;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 RPICONFIGSERVER_APPLICATION_H_
|
||||
#define RPICONFIGSERVER_APPLICATION_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <wpi/ArrayRef.h>
|
||||
#include <wpi/Signal.h>
|
||||
#include <wpi/StringRef.h>
|
||||
|
||||
namespace wpi {
|
||||
class json;
|
||||
} // namespace wpi
|
||||
|
||||
class Application {
|
||||
struct private_init {};
|
||||
|
||||
public:
|
||||
explicit Application(const private_init&) {}
|
||||
Application(const Application&) = delete;
|
||||
Application& operator=(const Application&) = delete;
|
||||
|
||||
void Set(wpi::StringRef appType, std::function<void(wpi::StringRef)> onFail);
|
||||
|
||||
int StartUpload(wpi::StringRef appType, char* filename,
|
||||
std::function<void(wpi::StringRef)> onFail);
|
||||
void Upload(int fd, bool text, wpi::ArrayRef<uint8_t> contents);
|
||||
void FinishUpload(wpi::StringRef appType, int fd, const char* tmpFilename,
|
||||
std::function<void(wpi::StringRef)> onFail);
|
||||
|
||||
void UpdateStatus();
|
||||
|
||||
wpi::json GetStatusJson();
|
||||
|
||||
wpi::sig::Signal<const wpi::json&> status;
|
||||
|
||||
static std::shared_ptr<Application> GetInstance();
|
||||
};
|
||||
|
||||
#endif // RPICONFIGSERVER_APPLICATION_H_
|
||||
+56
@@ -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 RPICONFIGSERVER_DATAHISTORY_H_
|
||||
#define RPICONFIGSERVER_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 // RPICONFIGSERVER_DATAHISTORY_H_
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 <unistd.h>
|
||||
#include <uv.h>
|
||||
|
||||
#include <wpi/FileSystem.h>
|
||||
#include <wpi/SmallVector.h>
|
||||
#include <wpi/UrlParser.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
#include <wpi/raw_uv_ostream.h>
|
||||
#include <wpi/uv/Request.h>
|
||||
|
||||
#include "WebSocketHandlers.h"
|
||||
|
||||
#define ZIPS_DIR "/home/pi/zips"
|
||||
|
||||
namespace uv = wpi::uv;
|
||||
|
||||
// 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);
|
||||
});
|
||||
ws->binary.connect([s = ws.get()](wpi::ArrayRef<uint8_t> msg, bool) {
|
||||
ProcessWsBinary(*s, msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class SendfileReq : public uv::RequestImpl<SendfileReq, uv_fs_t> {
|
||||
public:
|
||||
SendfileReq(uv_file out, uv_file in, int64_t inOffset, size_t len)
|
||||
: m_out(out), m_in(in), m_inOffset(inOffset), m_len(len) {
|
||||
error = [this](uv::Error err) { GetLoop().error(err); };
|
||||
}
|
||||
|
||||
uv::Loop& GetLoop() const {
|
||||
return *static_cast<uv::Loop*>(GetRaw()->loop->data);
|
||||
}
|
||||
|
||||
int Send(uv::Loop& loop) {
|
||||
int err = uv_fs_sendfile(loop.GetRaw(), GetRaw(), m_out, m_in, m_inOffset,
|
||||
m_len, [](uv_fs_t* req) {
|
||||
auto& h = *static_cast<SendfileReq*>(req->data);
|
||||
if (req->result < 0) {
|
||||
h.ReportError(req->result);
|
||||
h.complete();
|
||||
h.Release();
|
||||
return;
|
||||
}
|
||||
|
||||
h.m_inOffset += req->result;
|
||||
h.m_len -= req->result;
|
||||
if (h.m_len == 0) {
|
||||
// done
|
||||
h.complete();
|
||||
h.Release(); // this is always a one-shot
|
||||
return;
|
||||
}
|
||||
|
||||
// need to send more
|
||||
h.Send(h.GetLoop());
|
||||
});
|
||||
if (err < 0) {
|
||||
ReportError(err);
|
||||
complete();
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
wpi::sig::Signal<> complete;
|
||||
|
||||
private:
|
||||
uv_file m_out;
|
||||
uv_file m_in;
|
||||
int64_t m_inOffset;
|
||||
size_t m_len;
|
||||
};
|
||||
|
||||
void Sendfile(uv::Loop& loop, uv_file out, uv_file in, int64_t inOffset,
|
||||
size_t len, std::function<void()> complete) {
|
||||
auto req = std::make_shared<SendfileReq>(out, in, inOffset, len);
|
||||
if (complete) req->complete.connect(complete);
|
||||
int err = req->Send(loop);
|
||||
if (err >= 0) req->Keep();
|
||||
}
|
||||
|
||||
void MyHttpConnection::SendFileResponse(int code, const wpi::Twine& codeText,
|
||||
const wpi::Twine& contentType,
|
||||
const wpi::Twine& filename,
|
||||
const wpi::Twine& extraHeader) {
|
||||
// open file
|
||||
int infd;
|
||||
if (wpi::sys::fs::openFileForRead(filename, infd)) {
|
||||
SendError(404);
|
||||
return;
|
||||
}
|
||||
|
||||
// get status (to get file size)
|
||||
wpi::sys::fs::file_status status;
|
||||
if (wpi::sys::fs::status(infd, status)) {
|
||||
SendError(404);
|
||||
::close(infd);
|
||||
return;
|
||||
}
|
||||
|
||||
uv_os_fd_t outfd;
|
||||
int err = uv_fileno(m_stream.GetRawHandle(), &outfd);
|
||||
if (err < 0) {
|
||||
m_stream.GetLoopRef().ReportError(err);
|
||||
SendError(404);
|
||||
::close(infd);
|
||||
return;
|
||||
}
|
||||
|
||||
wpi::SmallVector<uv::Buffer, 4> toSend;
|
||||
wpi::raw_uv_ostream os{toSend, 4096};
|
||||
BuildHeader(os, code, codeText, contentType, status.getSize(), extraHeader);
|
||||
SendData(os.bufs(), false);
|
||||
|
||||
// close after write completes if we aren't keeping alive
|
||||
// since we're using sendfile, set socket to blocking
|
||||
m_stream.SetBlocking(true);
|
||||
Sendfile(m_stream.GetLoopRef(), outfd, infd, 0, status.getSize(),
|
||||
[ infd, closeAfter = !m_keepAlive, stream = &m_stream ] {
|
||||
::close(infd);
|
||||
if (closeAfter)
|
||||
stream->Close();
|
||||
else
|
||||
stream->SetBlocking(false);
|
||||
});
|
||||
}
|
||||
|
||||
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 if (isGET && path.startswith("/") && path.endswith(".zip") &&
|
||||
!path.contains("..")) {
|
||||
SendFileResponse(200, "OK", "application/zip", wpi::Twine(ZIPS_DIR) + path);
|
||||
} else {
|
||||
SendError(404, "Resource not found");
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 RPICONFIGSERVER_MYHTTPCONNECTION_H_
|
||||
#define RPICONFIGSERVER_MYHTTPCONNECTION_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <wpi/HttpServerConnection.h>
|
||||
#include <wpi/Twine.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;
|
||||
void SendFileResponse(int code, const wpi::Twine& codeText,
|
||||
const wpi::Twine& contentType,
|
||||
const wpi::Twine& filename,
|
||||
const wpi::Twine& extraHeader = wpi::Twine{});
|
||||
|
||||
wpi::WebSocketServerHelper m_websocketHelper;
|
||||
};
|
||||
|
||||
#endif // RPICONFIGSERVER_MYHTTPCONNECTION_H_
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 "NetworkSettings.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <wpi/FileSystem.h>
|
||||
#include <wpi/MathExtras.h>
|
||||
#include <wpi/SmallString.h>
|
||||
#include <wpi/json.h>
|
||||
#include <wpi/raw_istream.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
#include <wpi/uv/Process.h>
|
||||
#include <wpi/uv/util.h>
|
||||
|
||||
namespace uv = wpi::uv;
|
||||
|
||||
#define GEN_MARKER "###### BELOW THIS LINE EDITED BY RPICONFIGSERVER ######"
|
||||
|
||||
/*
|
||||
Format of generated portion for static:
|
||||
|
||||
interface eth0
|
||||
static ip_address=<networkAddress>/<networkMask as CIDR>
|
||||
static routers=<networkGateway>
|
||||
static domain_name_servers=<networkDNS>
|
||||
|
||||
For static fallback:
|
||||
|
||||
profile static_eth0
|
||||
static ip_address=<networkAddress>/<networkMask as CIDR>
|
||||
static routers=<networkGateway>
|
||||
static domain_name_servers=<networkDNS>
|
||||
interface eth0
|
||||
fallback static_eth0
|
||||
*/
|
||||
|
||||
wpi::StringRef CidrToNetmask(unsigned int cidr,
|
||||
wpi::SmallVectorImpl<char>& buf) {
|
||||
in_addr addr = { htonl(wpi::maskLeadingOnes<uint32_t>(cidr)) };
|
||||
wpi::uv::AddrToName(addr, &buf);
|
||||
return wpi::StringRef(buf.data(), buf.size());
|
||||
}
|
||||
|
||||
bool NetmaskToCidr(wpi::StringRef netmask, unsigned int* cidr) {
|
||||
in_addr addr;
|
||||
if (wpi::uv::NameToAddr(netmask, &addr) != 0) return false;
|
||||
uint32_t hostAddr = ntohl(addr.s_addr);
|
||||
auto leadingOnes = wpi::countLeadingOnes(hostAddr);
|
||||
auto trailingZeros = wpi::countTrailingZeros(hostAddr);
|
||||
if (leadingOnes + trailingZeros != 32) return false;
|
||||
*cidr = leadingOnes;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<NetworkSettings> NetworkSettings::GetInstance() {
|
||||
static auto inst = std::make_shared<NetworkSettings>(private_init{});
|
||||
return inst;
|
||||
}
|
||||
|
||||
void NetworkSettings::Set(Mode mode, wpi::StringRef address,
|
||||
wpi::StringRef mask, wpi::StringRef gateway,
|
||||
wpi::StringRef dns,
|
||||
std::function<void(wpi::StringRef)> onFail) {
|
||||
// validate and sanitize inputs
|
||||
wpi::SmallString<32> addressOut;
|
||||
unsigned int cidr;
|
||||
wpi::SmallString<32> gatewayOut;
|
||||
wpi::SmallString<128> dnsOut;
|
||||
|
||||
// address
|
||||
in_addr addressAddr;
|
||||
if (wpi::uv::NameToAddr(address, &addressAddr) != 0) {
|
||||
wpi::SmallString<128> err;
|
||||
err += "invalid address '";
|
||||
err += address;
|
||||
err += "'";
|
||||
onFail(err);
|
||||
return;
|
||||
}
|
||||
wpi::uv::AddrToName(addressAddr, &addressOut);
|
||||
|
||||
// mask
|
||||
if (!NetmaskToCidr(mask, &cidr)) {
|
||||
wpi::SmallString<128> err;
|
||||
err += "invalid netmask '";
|
||||
err += mask;
|
||||
err += "'";
|
||||
onFail(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// gateway (may be blank)
|
||||
in_addr gatewayAddr;
|
||||
if (wpi::uv::NameToAddr(gateway, &gatewayAddr) == 0)
|
||||
wpi::uv::AddrToName(gatewayAddr, &gatewayOut);
|
||||
|
||||
// dns
|
||||
wpi::SmallVector<wpi::StringRef, 4> dnsStrs;
|
||||
wpi::SmallString<32> oneDnsOut;
|
||||
bool first = true;
|
||||
dns.split(dnsStrs, ' ', -1, false);
|
||||
for (auto dnsStr : dnsStrs) {
|
||||
in_addr dnsAddr;
|
||||
if (wpi::uv::NameToAddr(dnsStr, &dnsAddr) != 0) {
|
||||
wpi::SmallString<128> err;
|
||||
err += "invalid DNS address '";
|
||||
err += dnsStr;
|
||||
err += "'";
|
||||
onFail(err);
|
||||
return;
|
||||
}
|
||||
wpi::uv::AddrToName(dnsAddr, &oneDnsOut);
|
||||
if (!first) dnsOut += ' ';
|
||||
first = false;
|
||||
dnsOut += oneDnsOut;
|
||||
}
|
||||
|
||||
// read file (up to but not including the marker)
|
||||
std::vector<std::string> lines;
|
||||
std::error_code ec;
|
||||
{
|
||||
wpi::raw_fd_istream is(DHCPCD_CONF, ec);
|
||||
if (ec) {
|
||||
onFail("could not read " DHCPCD_CONF);
|
||||
return;
|
||||
}
|
||||
|
||||
wpi::SmallString<256> lineBuf;
|
||||
while (!is.has_error()) {
|
||||
wpi::StringRef line = is.getline(lineBuf, 256).trim();
|
||||
if (line == GEN_MARKER) break;
|
||||
lines.emplace_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
// write file
|
||||
{
|
||||
// write original lines
|
||||
wpi::raw_fd_ostream os(DHCPCD_CONF, ec, wpi::sys::fs::F_Text);
|
||||
if (ec) {
|
||||
onFail("could not write " DHCPCD_CONF);
|
||||
return;
|
||||
}
|
||||
for (auto&& line : lines)
|
||||
os << line << '\n';
|
||||
|
||||
// write marker
|
||||
os << GEN_MARKER << '\n';
|
||||
|
||||
// write generated config
|
||||
switch (mode) {
|
||||
case kDhcp:
|
||||
break; // nothing required
|
||||
case kStatic:
|
||||
os << "interface eth0\n";
|
||||
os << "static ip_address=" << addressOut << '/' << cidr << '\n';
|
||||
if (!gatewayOut.empty()) os << "static routers=" << gatewayOut << '\n';
|
||||
if (!dnsOut.empty())
|
||||
os << "static domain_name_servers=" << dnsOut << '\n';
|
||||
break;
|
||||
case kDhcpStatic:
|
||||
os << "profile static_eth0\n";
|
||||
os << "static ip_address=" << addressOut << '/' << cidr << '\n';
|
||||
if (!gatewayOut.empty()) os << "static routers=" << gatewayOut << '\n';
|
||||
if (!dnsOut.empty())
|
||||
os << "static domain_name_servers=" << dnsOut << '\n';
|
||||
os << "interface eth0\n";
|
||||
os << "fallback static_eth0\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// tell dhcpcd to reload config
|
||||
if (auto proc =
|
||||
uv::Process::Spawn(m_loop, "/sbin/dhcpcd", "/sbin/dhcpcd", "-n")) {
|
||||
proc->exited.connect([p = proc.get()](int64_t, int) { p->Close(); });
|
||||
}
|
||||
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void NetworkSettings::UpdateStatus() { status(GetStatusJson()); }
|
||||
|
||||
wpi::json NetworkSettings::GetStatusJson() {
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_istream is(DHCPCD_CONF, ec);
|
||||
if (ec) {
|
||||
wpi::errs() << "could not read " DHCPCD_CONF "\n";
|
||||
return wpi::json();
|
||||
}
|
||||
|
||||
wpi::json j = {{"type", "networkSettings"}, {"networkApproach", "dhcp"}};
|
||||
|
||||
wpi::SmallString<256> lineBuf;
|
||||
bool foundMarker = false;
|
||||
while (!is.has_error()) {
|
||||
wpi::StringRef line = is.getline(lineBuf, 256).trim();
|
||||
if (line == GEN_MARKER) foundMarker = true;
|
||||
if (!foundMarker) continue;
|
||||
if (line.empty()) continue;
|
||||
if (line.startswith("static ip_address")) {
|
||||
j["networkApproach"] = "static";
|
||||
|
||||
wpi::StringRef value = line.split('=').second.trim();
|
||||
wpi::StringRef cidrStr;
|
||||
std::tie(j["networkAddress"], cidrStr) = value.split('/');
|
||||
|
||||
unsigned int cidrInt;
|
||||
if (!cidrStr.getAsInteger(10, cidrInt)) {
|
||||
wpi::SmallString<64> netmaskBuf;
|
||||
j["networkMask"] = CidrToNetmask(cidrInt, netmaskBuf);
|
||||
}
|
||||
} else if (line.startswith("static routers")) {
|
||||
j["networkGateway"] = line.split('=').second.trim();
|
||||
} else if (line.startswith("static domain_name_servers")) {
|
||||
j["networkDNS"] = line.split('=').second.trim();
|
||||
} else if (line.startswith("fallback")) {
|
||||
j["networkApproach"] = "dhcp-fallback";
|
||||
}
|
||||
}
|
||||
|
||||
return j;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 RPICONFIGSERVER_NETWORKSETTINGS_H_
|
||||
#define RPICONFIGSERVER_NETWORKSETTINGS_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <wpi/Signal.h>
|
||||
#include <wpi/StringRef.h>
|
||||
#include <wpi/uv/Loop.h>
|
||||
|
||||
namespace wpi {
|
||||
class json;
|
||||
} // namespace wpi
|
||||
|
||||
class NetworkSettings {
|
||||
struct private_init {};
|
||||
|
||||
public:
|
||||
explicit NetworkSettings(const private_init&) {}
|
||||
NetworkSettings(const NetworkSettings&) = delete;
|
||||
NetworkSettings& operator=(const NetworkSettings&) = delete;
|
||||
|
||||
void SetLoop(std::shared_ptr<wpi::uv::Loop> loop) {
|
||||
m_loop = std::move(loop);
|
||||
}
|
||||
|
||||
enum Mode { kDhcp, kStatic, kDhcpStatic };
|
||||
|
||||
void Set(Mode mode, wpi::StringRef address, wpi::StringRef mask,
|
||||
wpi::StringRef gateway, wpi::StringRef dns,
|
||||
std::function<void(wpi::StringRef)> onFail);
|
||||
|
||||
void UpdateStatus();
|
||||
|
||||
wpi::json GetStatusJson();
|
||||
|
||||
wpi::sig::Signal<const wpi::json&> status;
|
||||
|
||||
static std::shared_ptr<NetworkSettings> GetInstance();
|
||||
|
||||
private:
|
||||
std::shared_ptr<wpi::uv::Loop> m_loop;
|
||||
};
|
||||
|
||||
#endif // RPICONFIGSERVER_NETWORKSETTINGS_H_
|
||||
+209
@@ -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[3].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);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 RPICONFIGSERVER_SYSTEMSTATUS_H_
|
||||
#define RPICONFIGSERVER_SYSTEMSTATUS_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <wpi/Signal.h>
|
||||
|
||||
#include "DataHistory.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 // RPICONFIGSERVER_SYSTEMSTATUS_H_
|
||||
+60
@@ -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. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "VisionSettings.h"
|
||||
|
||||
#include <wpi/FileSystem.h>
|
||||
#include <wpi/json.h>
|
||||
#include <wpi/raw_istream.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
#include "VisionStatus.h"
|
||||
|
||||
std::shared_ptr<VisionSettings> VisionSettings::GetInstance() {
|
||||
static auto inst = std::make_shared<VisionSettings>(private_init{});
|
||||
return inst;
|
||||
}
|
||||
|
||||
void VisionSettings::Set(const wpi::json& data,
|
||||
std::function<void(wpi::StringRef)> onFail) {
|
||||
{
|
||||
// write file
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_ostream os(FRC_JSON, ec, wpi::sys::fs::F_Text);
|
||||
if (ec) {
|
||||
onFail("could not write " FRC_JSON);
|
||||
return;
|
||||
}
|
||||
data.dump(os, 4);
|
||||
os << '\n';
|
||||
}
|
||||
|
||||
// terminate vision process so it reloads the file
|
||||
VisionStatus::GetInstance()->Terminate(onFail);
|
||||
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void VisionSettings::UpdateStatus() { status(GetStatusJson()); }
|
||||
|
||||
wpi::json VisionSettings::GetStatusJson() {
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_istream is(FRC_JSON, ec);
|
||||
if (ec) {
|
||||
wpi::errs() << "could not read " FRC_JSON "\n";
|
||||
return wpi::json();
|
||||
}
|
||||
|
||||
try {
|
||||
wpi::json j = {{"type", "visionSettings"},
|
||||
{"settings", wpi::json::parse(is)}};
|
||||
return j;
|
||||
} catch (wpi::json::exception& e) {
|
||||
wpi::errs() << "could not parse " FRC_JSON "\n";
|
||||
return wpi::json();
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 RPICONFIGSERVER_VISIONSETTINGS_H_
|
||||
#define RPICONFIGSERVER_VISIONSETTINGS_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <wpi/Signal.h>
|
||||
#include <wpi/StringRef.h>
|
||||
|
||||
namespace wpi {
|
||||
class json;
|
||||
} // namespace wpi
|
||||
|
||||
class VisionSettings {
|
||||
struct private_init {};
|
||||
|
||||
public:
|
||||
explicit VisionSettings(const private_init&) {}
|
||||
VisionSettings(const VisionSettings&) = delete;
|
||||
VisionSettings& operator=(const VisionSettings&) = delete;
|
||||
|
||||
void Set(const wpi::json& data, std::function<void(wpi::StringRef)> onFail);
|
||||
|
||||
void UpdateStatus();
|
||||
|
||||
wpi::json GetStatusJson();
|
||||
|
||||
wpi::sig::Signal<const wpi::json&> status;
|
||||
|
||||
static std::shared_ptr<VisionSettings> GetInstance();
|
||||
};
|
||||
|
||||
#endif // RPICONFIGSERVER_VISIONSETTINGS_H_
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <cscore.h>
|
||||
#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/FsEvent.h>
|
||||
#include <wpi/uv/Pipe.h>
|
||||
#include <wpi/uv/Process.h>
|
||||
#include <wpi/uv/Timer.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::SetLoop(std::shared_ptr<wpi::uv::Loop> loop) {
|
||||
m_loop = std::move(loop);
|
||||
|
||||
auto refreshTimer = wpi::uv::Timer::Create(m_loop);
|
||||
refreshTimer->timeout.connect([this] { RefreshCameraList(); });
|
||||
refreshTimer->Unreference();
|
||||
|
||||
auto devEvents = wpi::uv::FsEvent::Create(m_loop);
|
||||
devEvents->fsEvent.connect([refreshTimer](const char* fn, int flags) {
|
||||
if (wpi::StringRef(fn).startswith("video"))
|
||||
refreshTimer->Start(wpi::uv::Timer::Time(200));
|
||||
});
|
||||
devEvents->Start("/dev");
|
||||
devEvents->Unreference();
|
||||
|
||||
refreshTimer->Start(wpi::uv::Timer::Time(200));
|
||||
}
|
||||
|
||||
void VisionStatus::UpdateCameraList() {
|
||||
wpi::json j = {{"type", "cameraList"}, {"cameras", wpi::json::array()}};
|
||||
auto& cams = j["cameras"];
|
||||
for (const auto& caminfo : m_cameraInfo) {
|
||||
wpi::json cam = {{"dev", caminfo.info.dev},
|
||||
{"path", caminfo.info.path},
|
||||
{"name", caminfo.info.name},
|
||||
{"otherPaths", wpi::json::array()},
|
||||
{"modes", wpi::json::array()}};
|
||||
|
||||
auto& otherPaths = cam["otherPaths"];
|
||||
for (const auto& path : caminfo.info.otherPaths)
|
||||
otherPaths.emplace_back(path);
|
||||
|
||||
auto& modes = cam["modes"];
|
||||
for (const auto& mode : caminfo.modes) {
|
||||
wpi::json jmode;
|
||||
|
||||
wpi::StringRef pixelFormatStr;
|
||||
switch (mode.pixelFormat) {
|
||||
case cs::VideoMode::kMJPEG:
|
||||
pixelFormatStr = "mjpeg";
|
||||
break;
|
||||
case cs::VideoMode::kYUYV:
|
||||
pixelFormatStr = "yuyv";
|
||||
break;
|
||||
case cs::VideoMode::kRGB565:
|
||||
pixelFormatStr = "rgb565";
|
||||
break;
|
||||
case cs::VideoMode::kBGR:
|
||||
pixelFormatStr = "bgr";
|
||||
break;
|
||||
case cs::VideoMode::kGray:
|
||||
pixelFormatStr = "gray";
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
jmode.emplace("pixelFormat", pixelFormatStr);
|
||||
|
||||
jmode.emplace("width", mode.width);
|
||||
jmode.emplace("height", mode.height);
|
||||
jmode.emplace("fps", mode.fps);
|
||||
|
||||
modes.emplace_back(jmode);
|
||||
}
|
||||
cams.emplace_back(cam);
|
||||
}
|
||||
cameraList(j);
|
||||
}
|
||||
|
||||
void VisionStatus::RefreshCameraList() {
|
||||
struct RefreshCameraWorkReq : public uv::WorkReq {
|
||||
std::vector<CameraInfo> cameraInfo;
|
||||
};
|
||||
auto workReq = std::make_shared<RefreshCameraWorkReq>();
|
||||
workReq->work.connect([r = workReq.get()] {
|
||||
CS_Status status = 0;
|
||||
for (auto&& caminfo : cs::EnumerateUsbCameras(&status)) {
|
||||
cs::UsbCamera camera{"usbcam", caminfo.dev};
|
||||
r->cameraInfo.emplace_back();
|
||||
r->cameraInfo.back().info = std::move(caminfo);
|
||||
r->cameraInfo.back().modes = camera.EnumerateVideoModes();
|
||||
}
|
||||
});
|
||||
workReq->afterWork.connect([ this, r = workReq.get() ] {
|
||||
m_cameraInfo = std::move(r->cameraInfo);
|
||||
UpdateCameraList();
|
||||
});
|
||||
uv::QueueWork(m_loop, workReq);
|
||||
}
|
||||
|
||||
void VisionStatus::RunSvc(const char* cmd,
|
||||
std::function<void(wpi::StringRef)> onFail) {
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
|
||||
void VisionStatus::ConsoleLog(uv::Buffer& buf, size_t len) {
|
||||
wpi::json j = {{"type", "visionLog"},
|
||||
{"data", wpi::StringRef(buf.base, len)}};
|
||||
log(j);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 RPICONFIGSERVER_VISIONSTATUS_H_
|
||||
#define RPICONFIGSERVER_VISIONSTATUS_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <cscore.h>
|
||||
#include <wpi/Signal.h>
|
||||
#include <wpi/StringRef.h>
|
||||
#include <wpi/uv/Loop.h>
|
||||
|
||||
namespace wpi {
|
||||
class json;
|
||||
|
||||
namespace uv {
|
||||
class Buffer;
|
||||
} // 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);
|
||||
|
||||
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);
|
||||
void UpdateCameraList();
|
||||
|
||||
wpi::sig::Signal<const wpi::json&> update;
|
||||
wpi::sig::Signal<const wpi::json&> log;
|
||||
wpi::sig::Signal<const wpi::json&> cameraList;
|
||||
|
||||
static std::shared_ptr<VisionStatus> GetInstance();
|
||||
|
||||
private:
|
||||
void RunSvc(const char* cmd, std::function<void(wpi::StringRef)> onFail);
|
||||
void RefreshCameraList();
|
||||
|
||||
std::shared_ptr<wpi::uv::Loop> m_loop;
|
||||
|
||||
struct CameraInfo {
|
||||
cs::UsbCameraInfo info;
|
||||
std::vector<cs::VideoMode> modes;
|
||||
};
|
||||
std::vector<CameraInfo> m_cameraInfo;
|
||||
};
|
||||
|
||||
#endif // RPICONFIGSERVER_VISIONSTATUS_H_
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 <unistd.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#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>
|
||||
|
||||
#include "Application.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "SystemStatus.h"
|
||||
#include "VisionSettings.h"
|
||||
#include "VisionStatus.h"
|
||||
|
||||
namespace uv = wpi::uv;
|
||||
|
||||
#define SERVICE "/service/camera"
|
||||
|
||||
struct WebSocketData {
|
||||
~WebSocketData() {
|
||||
if (uploadFd != -1) ::close(uploadFd);
|
||||
}
|
||||
|
||||
bool visionLogEnabled = false;
|
||||
int uploadFd = -1;
|
||||
bool uploadText = false;
|
||||
char uploadFilename[128];
|
||||
|
||||
wpi::sig::ScopedConnection sysStatusConn;
|
||||
wpi::sig::ScopedConnection sysWritableConn;
|
||||
wpi::sig::ScopedConnection visStatusConn;
|
||||
wpi::sig::ScopedConnection visLogConn;
|
||||
wpi::sig::ScopedConnection cameraListConn;
|
||||
wpi::sig::ScopedConnection netSettingsConn;
|
||||
wpi::sig::ScopedConnection visSettingsConn;
|
||||
wpi::sig::ScopedConnection appSettingsConn;
|
||||
};
|
||||
|
||||
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();
|
||||
data->cameraListConn = visStatus->cameraList.connect_connection(
|
||||
[&ws](const wpi::json& j) { SendWsText(ws, j); });
|
||||
visStatus->UpdateCameraList();
|
||||
|
||||
// send initial network settings
|
||||
auto netSettings = NetworkSettings::GetInstance();
|
||||
auto netSettingsFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); };
|
||||
netSettingsFunc(netSettings->GetStatusJson());
|
||||
data->netSettingsConn =
|
||||
netSettings->status.connect_connection(netSettingsFunc);
|
||||
|
||||
// send initial vision settings
|
||||
auto visSettings = VisionSettings::GetInstance();
|
||||
auto visSettingsFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); };
|
||||
visSettingsFunc(visSettings->GetStatusJson());
|
||||
data->visSettingsConn =
|
||||
visSettings->status.connect_connection(visSettingsFunc);
|
||||
|
||||
// send initial application settings
|
||||
auto appSettings = Application::GetInstance();
|
||||
auto appSettingsFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); };
|
||||
appSettingsFunc(appSettings->GetStatusJson());
|
||||
data->appSettingsConn =
|
||||
appSettings->status.connect_connection(appSettingsFunc);
|
||||
}
|
||||
|
||||
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 (subType == "Save") {
|
||||
try {
|
||||
VisionSettings::GetInstance()->Set(j.at("settings"), statusFunc);
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "could not read visionSave value: " << e.what() << '\n';
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (t == "networkSave") {
|
||||
auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) {
|
||||
SendWsText(*s, {{"type", "status"}, {"message", msg}});
|
||||
};
|
||||
try {
|
||||
NetworkSettings::Mode mode;
|
||||
auto& approach = j.at("networkApproach").get_ref<const std::string&>();
|
||||
if (approach == "dhcp")
|
||||
mode = NetworkSettings::kDhcp;
|
||||
else if (approach == "static")
|
||||
mode = NetworkSettings::kStatic;
|
||||
else if (approach == "dhcp-fallback")
|
||||
mode = NetworkSettings::kDhcpStatic;
|
||||
else {
|
||||
wpi::errs() << "could not understand networkApproach value: "
|
||||
<< approach << '\n';
|
||||
return;
|
||||
}
|
||||
NetworkSettings::GetInstance()->Set(
|
||||
mode, j.at("networkAddress").get_ref<const std::string&>(),
|
||||
j.at("networkMask").get_ref<const std::string&>(),
|
||||
j.at("networkGateway").get_ref<const std::string&>(),
|
||||
j.at("networkDNS").get_ref<const std::string&>(), statusFunc);
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "could not read networkSave value: " << e.what() << '\n';
|
||||
return;
|
||||
}
|
||||
} else if (t.startswith("application")) {
|
||||
wpi::StringRef subType = t.substr(11);
|
||||
|
||||
auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) {
|
||||
SendWsText(*s, {{"type", "status"}, {"message", msg}});
|
||||
};
|
||||
|
||||
std::string appType;
|
||||
try {
|
||||
appType = j.at("applicationType").get<std::string>();
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "could not read applicationSave value: " << e.what()
|
||||
<< '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
if (subType == "Save") {
|
||||
Application::GetInstance()->Set(appType, statusFunc);
|
||||
} else if (subType == "StartUpload") {
|
||||
auto d = ws.GetData<WebSocketData>();
|
||||
std::strcpy(d->uploadFilename, EXEC_HOME "/appUploadXXXXXX");
|
||||
d->uploadFd = Application::GetInstance()->StartUpload(
|
||||
appType, d->uploadFilename, statusFunc);
|
||||
d->uploadText = wpi::StringRef(appType).endswith("python");
|
||||
} else if (subType == "FinishUpload") {
|
||||
auto d = ws.GetData<WebSocketData>();
|
||||
if (d->uploadFd != -1)
|
||||
Application::GetInstance()->FinishUpload(appType, d->uploadFd,
|
||||
d->uploadFilename, statusFunc);
|
||||
d->uploadFd = -1;
|
||||
SendWsText(ws, {{"type", "applicationSaveComplete"}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessWsBinary(wpi::WebSocket& ws, wpi::ArrayRef<uint8_t> msg) {
|
||||
auto d = ws.GetData<WebSocketData>();
|
||||
if (d->uploadFd != -1)
|
||||
Application::GetInstance()->Upload(d->uploadFd, d->uploadText, msg);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 RPICONFIGSERVER_WEBSOCKETHANDLERS_H_
|
||||
#define RPICONFIGSERVER_WEBSOCKETHANDLERS_H_
|
||||
|
||||
#include <wpi/ArrayRef.h>
|
||||
#include <wpi/StringRef.h>
|
||||
|
||||
namespace wpi {
|
||||
class WebSocket;
|
||||
} // namespace wpi
|
||||
|
||||
void InitWs(wpi::WebSocket& ws);
|
||||
void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg);
|
||||
void ProcessWsBinary(wpi::WebSocket& ws, wpi::ArrayRef<uint8_t> msg);
|
||||
|
||||
#endif // RPICONFIGSERVER_WEBSOCKETHANDLERS_H_
|
||||
Vendored
+139
@@ -0,0 +1,139 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 <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>
|
||||
|
||||
#include "MyHttpConnection.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "SystemStatus.h"
|
||||
#include "VisionStatus.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();
|
||||
|
||||
NetworkSettings::GetInstance()->SetLoop(loop);
|
||||
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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spinnow 5s infinite linear;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
+764
@@ -0,0 +1,764 @@
|
||||
"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);
|
||||
feather.replace();
|
||||
}
|
||||
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', 'networkAddress', 'networkMask', 'networkGateway', 'networkDNS', 'visionUp', 'visionDown', 'visionTerm', 'visionKill', 'systemReadOnly', 'systemWritable', 'visionClient', 'visionTeam', 'visionDiscard', 'addConnectedCamera', 'addCamera', 'applicationType'];
|
||||
var connectedButtonClasses = ['cameraName', 'cameraPath', 'cameraAlternatePaths', 'cameraPixelFormat', 'cameraWidth', 'cameraHeight', 'cameraFps', 'cameraBrightness', 'cameraWhiteBalance', 'cameraExposure', 'cameraProperties', 'streamWidth', 'streamHeight', 'streamFps', 'streamCompression', 'streamDefaultCompression', 'cameraRemove', 'cameraCopyConfig']
|
||||
var writableButtonIds = ['networkSave', 'visionSave', 'applicationSave'];
|
||||
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');
|
||||
$('.cameraConnectionBadge').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 < connectedButtonClasses.length; i++) {
|
||||
$('.' + connectedButtonClasses[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);
|
||||
}
|
||||
for (var i = 0; i < connectedButtonClasses.length; i++) {
|
||||
$('.' + connectedButtonClasses[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));
|
||||
});
|
||||
|
||||
// Vision settings
|
||||
var visionSettingsServer = {};
|
||||
var visionSettingsDisplay = {'cameras': []};
|
||||
var cameraList = [];
|
||||
|
||||
function pushVisionLogEnabled() {
|
||||
var msg = {
|
||||
type: 'visionLogEnabled',
|
||||
value: visionLogEnabled.prop('checked')
|
||||
};
|
||||
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();
|
||||
pushVisionLogEnabled();
|
||||
};
|
||||
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);
|
||||
if (msg === null) {
|
||||
return;
|
||||
}
|
||||
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').val(msg.networkApproach);
|
||||
$('#networkAddress').val(msg.networkAddress);
|
||||
$('#networkMask').val(msg.networkMask);
|
||||
$('#networkGateway').val(msg.networkGateway);
|
||||
$('#networkDNS').val(msg.networkDNS);
|
||||
updateNetworkSettingsView();
|
||||
break;
|
||||
case 'visionSettings':
|
||||
visionSettingsServer = msg.settings;
|
||||
visionSettingsDisplay = $.extend(true, {}, visionSettingsServer);
|
||||
updateVisionSettingsView();
|
||||
break;
|
||||
case 'applicationSettings':
|
||||
$('#applicationType').val(msg.applicationType);
|
||||
updateApplicationView();
|
||||
break;
|
||||
case 'applicationSaveComplete':
|
||||
$('#applicationSave').button('reset');
|
||||
updateApplicationView();
|
||||
break;
|
||||
case 'systemReadOnly':
|
||||
displayReadOnly();
|
||||
break;
|
||||
case 'systemWritable':
|
||||
displayWritable();
|
||||
break;
|
||||
case 'status':
|
||||
displayStatus(msg.message);
|
||||
break;
|
||||
case 'cameraList':
|
||||
cameraList = msg.cameras;
|
||||
updateCameraListView();
|
||||
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() {
|
||||
pushVisionLogEnabled();
|
||||
});
|
||||
|
||||
//
|
||||
// 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
|
||||
function updateNetworkSettingsView() {
|
||||
if ($('#networkApproach').val() === "dhcp") {
|
||||
$('#networkIpDetails').collapse('hide');
|
||||
} else {
|
||||
$('#networkIpDetails').collapse('show');
|
||||
}
|
||||
}
|
||||
|
||||
$('#networkApproach').change(function() {
|
||||
updateNetworkSettingsView();
|
||||
});
|
||||
|
||||
// Network Save button handler
|
||||
$('#networkSave').click(function() {
|
||||
var msg = {
|
||||
type: 'networkSave',
|
||||
networkApproach: $('#networkApproach').val(),
|
||||
networkAddress: $('#networkAddress').val(),
|
||||
networkMask: $('#networkMask').val(),
|
||||
networkGateway: $('#networkGateway').val(),
|
||||
networkDNS: $('#networkDNS').val()
|
||||
};
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
function getCameraPropertyValue(data, name) {
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
if (data[i].name === name) {
|
||||
return data[i].value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateVisionCameraView(camera, value) {
|
||||
if ('name' in value) {
|
||||
camera.find('.cameraTitle').text('Camera ' + value.name);
|
||||
camera.find('.cameraName').val(value.name);
|
||||
}
|
||||
if ('path' in value) {
|
||||
camera.find('.cameraPath').val(value.path);
|
||||
}
|
||||
camera.find('.cameraPixelFormat').val(value['pixel format']);
|
||||
camera.find('.cameraWidth').val(value.width);
|
||||
camera.find('.cameraHeight').val(value.height);
|
||||
camera.find('.cameraFps').val(value.fps);
|
||||
camera.find('.cameraBrightness').val(value.brightness);
|
||||
camera.find('.cameraWhiteBalance').val(value['white balance']);
|
||||
camera.find('.cameraExposure').val(value.exposure);
|
||||
camera.find('.cameraProperties').val(JSON.stringify(value.properties));
|
||||
if ('stream' in value && 'properties' in value.stream) {
|
||||
camera.find('.streamWidth').val(getCameraPropertyValue(value.stream.properties, 'width'));
|
||||
camera.find('.streamHeight').val(getCameraPropertyValue(value.stream.properties, 'height'));
|
||||
camera.find('.streamFps').val(getCameraPropertyValue(value.stream.properties, 'fps'));
|
||||
camera.find('.streamCompression').val(getCameraPropertyValue(value.stream.properties, 'compression'));
|
||||
camera.find('.streamDefaultCompression').val(getCameraPropertyValue(value.stream.properties, 'default_compression'));
|
||||
} else {
|
||||
camera.find('.streamWidth').val('');
|
||||
camera.find('.streamHeight').val('');
|
||||
camera.find('.streamFps').val('');
|
||||
camera.find('.streamCompression').val('');
|
||||
camera.find('.streamDefaultCompression').val('');
|
||||
}
|
||||
}
|
||||
|
||||
function updateVisionCameraDataFromJson(i, data) {
|
||||
if (!('name' in data)) {
|
||||
data.name = visionSettingsDisplay.cameras[i].name;
|
||||
}
|
||||
if (!('path' in data)) {
|
||||
data.path = visionSettingsDisplay.cameras[i].path;
|
||||
}
|
||||
if ('properties' in data) {
|
||||
var newProps = [];
|
||||
var wbAuto = false;
|
||||
var exAuto = false;
|
||||
|
||||
for (var j = 0; j < data.properties.length; j++) {
|
||||
var name = data.properties[j].name;
|
||||
|
||||
// remove all raw properties
|
||||
if (name.startsWith('raw_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// brightness
|
||||
if (name === 'brightness') {
|
||||
data.brightness = data.properties[j].value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// white balance
|
||||
if (name === 'white_balance_temperature_auto') {
|
||||
if (data.properties[j].value === true) {
|
||||
data['white balance'] = 'auto';
|
||||
wbAuto = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (name === 'white_balance_temperature') {
|
||||
if (wbAuto === false) {
|
||||
data['white balance'] = data.properties.white_balance_temperature;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// exposure
|
||||
if (name === 'exposure_auto') {
|
||||
if (data.properties[j].value === 3) {
|
||||
data.exposure = 'auto';
|
||||
exAuto = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (name === 'exposure_absolute') {
|
||||
if (exAuto === false) {
|
||||
data.exposure = data.properties.exposure_absolute;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
newProps.push(data.properties[j]);
|
||||
}
|
||||
data.properties = newProps;
|
||||
}
|
||||
visionSettingsDisplay.cameras[i] = data;
|
||||
}
|
||||
|
||||
function appendNewVisionCameraView(value, i) {
|
||||
var camera = $('#cameraNEW').clone();
|
||||
camera.attr('id', 'camera' + i);
|
||||
camera.addClass('cameraSetting');
|
||||
camera.removeAttr('style');
|
||||
|
||||
updateVisionCameraView(camera, value);
|
||||
camera.find('.cameraStream').attr('href', 'http://' + window.location.hostname + ':' + (1181 + i) + '/');
|
||||
camera.find('.cameraRemove').click(function() {
|
||||
visionSettingsDisplay.cameras.splice(i, 1);
|
||||
camera.remove();
|
||||
updateCameraListView();
|
||||
});
|
||||
camera.find('.cameraSettingsFile').change(function() {
|
||||
if (this.files.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
var fr = new FileReader();
|
||||
fr.onload = function(e) {
|
||||
var result = JSON.parse(e.target.result);
|
||||
updateVisionCameraDataFromJson(i, result);
|
||||
updateVisionCameraView(camera, result);
|
||||
};
|
||||
fr.readAsText(this.files.item(0));
|
||||
});
|
||||
camera.find('.cameraCopyConfig').click(function() {
|
||||
fetch('http://' + window.location.hostname + ':' + (1181 + i) + '/config.json')
|
||||
.then(response => response.json())
|
||||
.then(function(result) {
|
||||
updateVisionCameraDataFromJson(i, result);
|
||||
updateVisionCameraView(camera, result);
|
||||
})
|
||||
.catch(function(error) {
|
||||
displayStatus('error reading camera config: ' + error);
|
||||
});
|
||||
});
|
||||
|
||||
camera.find('[id]').each(function() {
|
||||
$(this).attr('id', $(this).attr('id').replace('NEW', i));
|
||||
});
|
||||
camera.find('[for]').each(function() {
|
||||
$(this).attr('for', $(this).attr('for').replace('NEW', i));
|
||||
});
|
||||
camera.find('[data-target]').each(function() {
|
||||
$(this).attr('data-target', $(this).attr('data-target').replace('NEW', i));
|
||||
});
|
||||
camera.find('[aria-labelledby]').each(function() {
|
||||
$(this).attr('aria-labelledby', $(this).attr('aria-labelledby').replace('NEW', i));
|
||||
});
|
||||
|
||||
$('#cameras').append(camera);
|
||||
}
|
||||
|
||||
function updateVisionSettingsView() {
|
||||
var isClient = !visionSettingsDisplay.ntmode || visionSettingsDisplay.ntmode === 'client';
|
||||
$('#visionClient').prop('checked', isClient);
|
||||
if (isClient) {
|
||||
$('#visionClientDetails').collapse('show');
|
||||
} else {
|
||||
$('#visionClientDetails').collapse('hide');
|
||||
}
|
||||
$('#visionTeam').val(visionSettingsDisplay.team);
|
||||
|
||||
$('.cameraSetting').remove();
|
||||
visionSettingsDisplay.cameras.forEach(function (value, i) {
|
||||
appendNewVisionCameraView(value, i);
|
||||
});
|
||||
updateCameraListView();
|
||||
feather.replace();
|
||||
}
|
||||
|
||||
$('#visionSave').click(function() {
|
||||
// update json from view
|
||||
visionSettingsDisplay.ntmode = $('#visionClient').prop('checked') ? 'client' : 'server';
|
||||
visionSettingsDisplay.team = parseInt($('#visionTeam').val(), 10);
|
||||
visionSettingsDisplay.cameras.forEach(function (value, i) {
|
||||
var camera = $('#camera' + i);
|
||||
value.name = camera.find('.cameraName').val();
|
||||
value.path = camera.find('.cameraPath').val();
|
||||
value['pixel format'] = camera.find('.cameraPixelFormat').val();
|
||||
value.width = parseInt(camera.find('.cameraWidth').val(), 10);
|
||||
if (isNaN(value.width)) {
|
||||
delete value["width"];
|
||||
}
|
||||
value.height = parseInt(camera.find('.cameraHeight').val(), 10);
|
||||
if (isNaN(value.height)) {
|
||||
delete value["height"];
|
||||
}
|
||||
value.fps = parseInt(camera.find('.cameraFps').val(), 10);
|
||||
if (isNaN(value.fps)) {
|
||||
delete value["fps"];
|
||||
}
|
||||
|
||||
var brightness = camera.find('.cameraBrightness').val();
|
||||
if (brightness !== '') {
|
||||
value.brightness = parseInt(brightness);
|
||||
if (isNaN(value.brightness)) {
|
||||
value.brightness = brightness;
|
||||
}
|
||||
} else {
|
||||
delete value['brightness'];
|
||||
}
|
||||
|
||||
var whiteBalance = camera.find('.cameraWhiteBalance').val();
|
||||
if (whiteBalance !== '') {
|
||||
value['white balance'] = parseInt(whiteBalance);
|
||||
if (isNaN(value['white balance'])) {
|
||||
value['white balance'] = whiteBalance;
|
||||
}
|
||||
} else {
|
||||
delete value['white balance'];
|
||||
}
|
||||
|
||||
var exposure = camera.find('.cameraExposure').val();
|
||||
if (exposure !== '') {
|
||||
value.exposure = parseInt(exposure);
|
||||
if (isNaN(value.exposure)) {
|
||||
value.exposure = exposure;
|
||||
}
|
||||
} else {
|
||||
delete value['exposure'];
|
||||
}
|
||||
|
||||
try {
|
||||
value.properties = JSON.parse(camera.find('.cameraProperties').val());
|
||||
} catch (err) {
|
||||
delete value['properties'];
|
||||
}
|
||||
|
||||
value.stream = {'properties': []};
|
||||
|
||||
var streamWidth = parseInt(camera.find('.streamWidth').val(), 10);
|
||||
if (!isNaN(streamWidth)) {
|
||||
value.stream.properties.push({'name': 'width', 'value': streamWidth});
|
||||
}
|
||||
|
||||
var streamHeight = parseInt(camera.find('.streamHeight').val(), 10);
|
||||
if (!isNaN(streamHeight)) {
|
||||
value.stream.properties.push({'name': 'height', 'value': streamHeight});
|
||||
}
|
||||
|
||||
var streamFps = parseInt(camera.find('.streamFps').val(), 10);
|
||||
if (!isNaN(streamFps)) {
|
||||
value.stream.properties.push({'name': 'fps', 'value': streamFps});
|
||||
}
|
||||
|
||||
var streamCompression = parseInt(camera.find('.streamCompression').val(), 10);
|
||||
if (!isNaN(streamCompression)) {
|
||||
value.stream.properties.push({'name': 'compression', 'value': streamCompression});
|
||||
}
|
||||
|
||||
var streamDefaultCompression = parseInt(camera.find('.streamDefaultCompression').val(), 10);
|
||||
if (!isNaN(streamDefaultCompression)) {
|
||||
value.stream.properties.push({'name': 'default_compression', 'value': streamDefaultCompression});
|
||||
}
|
||||
});
|
||||
var msg = {
|
||||
type: 'visionSave',
|
||||
settings: visionSettingsDisplay
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
$('#visionDiscard').click(function() {
|
||||
visionSettingsDisplay = $.extend(true, {}, visionSettingsServer);
|
||||
updateVisionSettingsView();
|
||||
});
|
||||
|
||||
$('#addCamera').click(function() {
|
||||
var i = visionSettingsDisplay.cameras.length;
|
||||
visionSettingsDisplay.cameras.push({});
|
||||
appendNewVisionCameraView({}, i);
|
||||
updateCameraListView();
|
||||
});
|
||||
|
||||
function updateCameraListView() {
|
||||
var addConnectedDropdown = $('#addConnectedCameraList');
|
||||
addConnectedDropdown.html('');
|
||||
|
||||
// disable all the alternate paths by default
|
||||
visionSettingsDisplay.cameras.forEach(function (value, k) {
|
||||
var cameraElem = $('#camera' + k);
|
||||
cameraElem.find('.cameraConnectionBadge').removeClass('badge-dark').removeClass('badge-primary').addClass('badge-secondary').text('Disconnected');
|
||||
cameraElem.find('.cameraAlternatePathsList').html('');
|
||||
cameraElem.find('.cameraAlternatePaths').prop('disabled', true);
|
||||
});
|
||||
|
||||
cameraList.forEach(function (camera, i) {
|
||||
// See if one of the paths is an already existing camera
|
||||
// Include the "main path" as the first path
|
||||
var matchedCamera = false;
|
||||
var paths = [camera.path];
|
||||
camera.otherPaths.forEach(function (path, j) {
|
||||
paths.push(path);
|
||||
});
|
||||
paths.forEach(function (path, j) {
|
||||
visionSettingsDisplay.cameras.forEach(function (value, k) {
|
||||
var cameraElem = $('#camera' + k);
|
||||
var pathElem = cameraElem.find('.cameraPath');
|
||||
if (path === pathElem.val()) {
|
||||
matchedCamera = true;
|
||||
|
||||
// show camera as connected
|
||||
cameraElem.find('.cameraConnectionBadge').removeClass('badge-dark').removeClass('badge-secondary').addClass('badge-primary').text('Connected');
|
||||
|
||||
// build alternate path list
|
||||
var setAlternateDropdown = cameraElem.find('.cameraAlternatePathsList');
|
||||
setAlternateDropdown.html('');
|
||||
paths.forEach(function (altPath, j) {
|
||||
setAlternateDropdown.append('<button class="dropdown-item cameraSetAlternatePath" type="button">' + altPath + '</button>');
|
||||
});
|
||||
|
||||
cameraElem.find('.cameraAlternatePaths').prop('disabled', setAlternateDropdown.html() === '');
|
||||
|
||||
// hook up dropdown items to set alternate path
|
||||
setAlternateDropdown.find('.cameraSetAlternatePath').click(function() {
|
||||
pathElem.val($(this).text());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!matchedCamera) {
|
||||
// add it to add connected camera list
|
||||
addConnectedDropdown.append('<h5 class="dropdown-header">' + camera.name + '</h5>');
|
||||
paths.forEach(function (path, j) {
|
||||
addConnectedDropdown.append('<button class="dropdown-item addConnectedCameraItem" type="button">' + path + '</button>');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#addConnectedCamera').prop('disabled', addConnectedDropdown.html() === '');
|
||||
|
||||
// hook up dropdown items to create cameras
|
||||
addConnectedDropdown.find('.addConnectedCameraItem').click(function() {
|
||||
var i = visionSettingsDisplay.cameras.length;
|
||||
var camera = {"path": $(this).text()};
|
||||
visionSettingsDisplay.cameras.push(camera);
|
||||
appendNewVisionCameraView(camera, i);
|
||||
updateCameraListView();
|
||||
});
|
||||
}
|
||||
|
||||
var applicationFiles = [];
|
||||
|
||||
// Show details when appropriate for application type
|
||||
function updateApplicationView() {
|
||||
if ($('#applicationType').val().startsWith("upload")) {
|
||||
$('#applicationUpload').collapse('show');
|
||||
} else {
|
||||
$('#applicationUpload').collapse('hide');
|
||||
}
|
||||
$('#applicationFile').val(null);
|
||||
applicationFiles = [];
|
||||
}
|
||||
|
||||
$('#applicationType').change(function() {
|
||||
updateApplicationView();
|
||||
});
|
||||
|
||||
$('#applicationFile').change(function() {
|
||||
applicationFiles = this.files;
|
||||
});
|
||||
|
||||
$('#applicationSave').click(function() {
|
||||
var msg = {
|
||||
type: 'applicationSave',
|
||||
applicationType: $('#applicationType').val()
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
|
||||
// upload the file if requested
|
||||
if (applicationFiles.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('#applicationSave').button('loading');
|
||||
|
||||
var msg = {
|
||||
type: 'applicationStartUpload',
|
||||
applicationType: $('#applicationType').val()
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
|
||||
var reader = new FileReader();
|
||||
var file = applicationFiles.item(0);
|
||||
|
||||
function uploadFile(start) {
|
||||
var nextSlice = start + (64 * 1024) + 1;
|
||||
reader.onloadend = function(e) {
|
||||
if (e.target.readyState !== FileReader.DONE) {
|
||||
return;
|
||||
}
|
||||
connection.send(e.target.result);
|
||||
if (nextSlice < file.size) {
|
||||
// more to go
|
||||
uploadFile(nextSlice);
|
||||
} else {
|
||||
// done
|
||||
var msg = {
|
||||
type: 'applicationFinishUpload',
|
||||
applicationType: $('#applicationType').val()
|
||||
};
|
||||
connection.send(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
reader.readAsArrayBuffer(file.slice(start, nextSlice));
|
||||
}
|
||||
uploadFile(0);
|
||||
});
|
||||
|
||||
// Start with display disconnected and start initial connection attempt
|
||||
displayDisconnected();
|
||||
updateNetworkSettingsView();
|
||||
updateVisionSettingsView();
|
||||
updateApplicationView();
|
||||
connect();
|
||||
+488
@@ -0,0 +1,488 @@
|
||||
<!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>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="application-tab" data-toggle="tab" href="#application">
|
||||
<i class="nav-icon" data-feather="cpu"></i>
|
||||
Application
|
||||
</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="button" 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">
|
||||
<h6>Network Tables</h6>
|
||||
</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" 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">
|
||||
<h6 class="cameraTitle">Camera NEW</h6>
|
||||
</button>
|
||||
<span class="badge badge-secondary align-text-top cameraConnectionBadge">Disconnected</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-sm btn-success cameraStream" href="" target="_blank" role="button">
|
||||
<span data-feather="play"></span>
|
||||
Open Stream
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-danger cameraRemove">
|
||||
<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 col-4">
|
||||
<label for="cameraNameNEW">Name</label>
|
||||
<input type="text" class="form-control cameraName" id="cameraNameNEW">
|
||||
</div>
|
||||
<div class="form-group col">
|
||||
<label for="cameraPathNEW">Path</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control cameraPath" id="cameraPathNEW">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn dropdown-toggle cameraAlternatePaths" id="cameraAlternatePathsNEW" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" disabled="true">
|
||||
Alternate Path
|
||||
</button>
|
||||
<div class="dropdown-menu cameraAlternatePathsList" aria-labelledby="cameraAlternatePathsNEW">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Camera Settings
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input cameraSettingsFile" accept=".json,text/json,application/json" id="cameraSettingsFileNEW">
|
||||
<label class="custom-file-label" for="cameraSettingsFileNEW">Load Source Config From JSON File</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-sm cameraCopyConfig" type="button">
|
||||
<span data-feather="copy"></span>
|
||||
Copy Source Config From Camera
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraPixelFormatNEW">Pixel Format</label>
|
||||
<select class="form-control cameraPixelFormat" 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="number" class="form-control cameraWidth" id="cameraWidthNEW" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraHeightNEW">Height</label>
|
||||
<input type="number" class="form-control cameraHeight" id="cameraHeightNEW" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraFpsNEW">FPS</label>
|
||||
<input type="number" class="form-control cameraFps" 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 cameraBrightness" id="cameraBrightnessNEW" size="4">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraWhiteBalanceNEW">White Balance</label>
|
||||
<input type="text" class="form-control cameraWhiteBalance" id="cameraWhiteBalanceNEW" size="4">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="cameraExposureNEW">Exposure</label>
|
||||
<input type="text" class="form-control cameraExposure" id="cameraExposureNEW" size="4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cameraPropertiesNEW">Custom Properties JSON</label>
|
||||
<textarea class="form-control cameraProperties" id="cameraPropertiesNEW" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Video Stream Defaults
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-auto">
|
||||
<label for="streamWidthNEW">Width</label>
|
||||
<input type="number" class="form-control streamWidth" id="streamWidthNEW" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="streamHeightNEW">Height</label>
|
||||
<input type="number" class="form-control streamHeight" id="streamHeightNEW" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="streamFpsNEW">FPS</label>
|
||||
<input type="number" class="form-control streamFps" id="streamFpsNEW" size="4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-auto">
|
||||
<label for="streamCompressionNEW">Compression</label>
|
||||
<input type="number" class="form-control streamCompression" id="streamWidthNEW" size="5">
|
||||
</div>
|
||||
<div class="form-group col-auto">
|
||||
<label for="streamDefaultCompressionNEW">Default Compression</label>
|
||||
<input type="number" class="form-control streamDefaultCompression" id="streamDefaultCompressionNEW" size="4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-auto mr-auto">
|
||||
<button type="button" class="btn btn-primary" id="visionSave">
|
||||
<span data-feather="save"></span>
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" id="visionDiscard">
|
||||
<span data-feather="x"></span>
|
||||
Discard Changes
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-info dropdown-toggle" id="addConnectedCamera" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" disabled="true">
|
||||
<span data-feather="plus"></span>
|
||||
Add Connected Camera
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="addConnectedCamera" id="addConnectedCameraList">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-success" id="addCamera">
|
||||
<span data-feather="plus"></span>
|
||||
Add Other Camera
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application -->
|
||||
<div class="tab-pane" id="application" role="tabpanel" aria-labelledby="application-tab">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6>Vision Application Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="applicationType">Application</label>
|
||||
<select class="form-control" id="applicationType">
|
||||
<option value="builtin">Built-in multi-camera streaming</option>
|
||||
<option value="custom">Custom</option>
|
||||
<option value="example-java">Java example (must be built to work)</option>
|
||||
<option value="example-cpp">C++ example (must be built to work)</option>
|
||||
<option value="example-python">Python example</option>
|
||||
<option value="upload-java">Uploaded Java jar</option>
|
||||
<option value="upload-cpp">Uploaded C++ executable</option>
|
||||
<option value="upload-python">Uploaded Python file</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="collapse" id="applicationUpload">
|
||||
<div class="form-group">
|
||||
<label for="applicationFile">Upload executable file</label>
|
||||
<input type="file" class="form-control-file" id="applicationFile">
|
||||
</div>
|
||||
</div>
|
||||
<button id="applicationSave" type="button" 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>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6>Example Programs</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-sm btn-success" href="java-multiCameraServer.zip" target="_blank" role="button">
|
||||
<span data-feather="download"></span>
|
||||
Download Java Example
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-sm btn-success" href="cpp-multiCameraServer.zip" target="_blank" role="button">
|
||||
<span data-feather="download"></span>
|
||||
Download C++ Example
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-sm btn-success" href="python-multiCameraServer.zip" target="_blank" role="button">
|
||||
<span data-feather="download"></span>
|
||||
Download Python Example
|
||||
</a>
|
||||
</div>
|
||||
</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>
|
||||
Reference in New Issue
Block a user