From 98842b67285ea0e216d226d5a8405735a395e58f Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 11 Dec 2018 20:04:00 -0800 Subject: [PATCH] Add rpiConfigServer to tools --- deps/allwpilib | 2 +- deps/tools/Makefile | 43 +- deps/tools/gen_resource.py | 33 ++ deps/tools/rpiConfigServer_src/DataHistory.h | 56 +++ .../rpiConfigServer_src/MyHttpConnection.cpp | 108 +++++ .../rpiConfigServer_src/MyHttpConnection.h | 28 ++ .../rpiConfigServer_src/SystemStatus.cpp | 209 +++++++++ deps/tools/rpiConfigServer_src/SystemStatus.h | 60 +++ .../rpiConfigServer_src/VisionStatus.cpp | 188 ++++++++ deps/tools/rpiConfigServer_src/VisionStatus.h | 57 +++ .../rpiConfigServer_src/WebSocketHandlers.cpp | 203 ++++++++ .../rpiConfigServer_src/WebSocketHandlers.h | 20 + deps/tools/rpiConfigServer_src/main.cpp | 136 ++++++ .../resources/frcvision.css | 69 +++ .../resources/frcvision.js | 346 ++++++++++++++ .../rpiConfigServer_src/resources/index.html | 440 ++++++++++++++++++ 16 files changed, 1996 insertions(+), 2 deletions(-) create mode 100755 deps/tools/gen_resource.py create mode 100644 deps/tools/rpiConfigServer_src/DataHistory.h create mode 100644 deps/tools/rpiConfigServer_src/MyHttpConnection.cpp create mode 100644 deps/tools/rpiConfigServer_src/MyHttpConnection.h create mode 100644 deps/tools/rpiConfigServer_src/SystemStatus.cpp create mode 100644 deps/tools/rpiConfigServer_src/SystemStatus.h create mode 100644 deps/tools/rpiConfigServer_src/VisionStatus.cpp create mode 100644 deps/tools/rpiConfigServer_src/VisionStatus.h create mode 100644 deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp create mode 100644 deps/tools/rpiConfigServer_src/WebSocketHandlers.h create mode 100644 deps/tools/rpiConfigServer_src/main.cpp create mode 100644 deps/tools/rpiConfigServer_src/resources/frcvision.css create mode 100644 deps/tools/rpiConfigServer_src/resources/frcvision.js create mode 100644 deps/tools/rpiConfigServer_src/resources/index.html diff --git a/deps/allwpilib b/deps/allwpilib index bfe1524..7d7af28 160000 --- a/deps/allwpilib +++ b/deps/allwpilib @@ -1 +1 @@ -Subproject commit bfe15245a625d8e30351fc1096d2df1ca71210c3 +Subproject commit 7d7af287f6718c0fcb2123aca1b2cebc3177ade5 diff --git a/deps/tools/Makefile b/deps/tools/Makefile index 233f20e..9fe2696 100644 --- a/deps/tools/Makefile +++ b/deps/tools/Makefile @@ -1,6 +1,16 @@ COMPILER=../02-extract/raspbian9/bin/arm-raspbian9-linux-gnueabihf- -ALL: setuidgids _cscore.so +.PHONY: all +.SUFFIXES: + +all: setuidgids _cscore.so rpiConfigServer + +clean: + rm -f setuidgids + rm -f _cscore.so + rm -f rpiConfigServer + rm -f rpiConfigServer/*.o + rm -f rpiConfigServer/resources/*.o setuidgids: setuidgids.c ${COMPILER}gcc -O -Wall -D_GNU_SOURCE -o $@ $< @@ -19,3 +29,34 @@ _cscore.so: ../robotpy-cscore/src/_cscore.cpp ../robotpy-cscore/src/ndarray_conv -lcscore \ -lwpiutil \ -lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc -lopencv_core + +RPICONFIGSERVER_OBJS= \ + rpiConfigServer_src/main.o \ + rpiConfigServer_src/MyHttpConnection.o \ + rpiConfigServer_src/SystemStatus.o \ + rpiConfigServer_src/VisionStatus.o \ + rpiConfigServer_src/WebSocketHandlers.o \ + rpiConfigServer_src/resources/index.html.o \ + rpiConfigServer_src/resources/frcvision.css.o \ + rpiConfigServer_src/resources/frcvision.js.o + +rpiConfigServer: ${RPICONFIGSERVER_OBJS} + ${COMPILER}g++ -pthread -o $@ \ + ${RPICONFIGSERVER_OBJS} \ + -L../allwpilib/wpiutil/build/libs/wpiutil/static/release \ + -lwpiutil + +%.o: %.cpp + ${COMPILER}g++ -O -Wall -c -o $@ \ + -I../allwpilib/wpiutil/src/main/native/include \ + $< + +%.html.cpp: %.html + ./gen_resource.py $@ $< + +%.css.cpp: %.css + ./gen_resource.py $@ $< + +%.js.cpp: %.js + ./gen_resource.py $@ $< + diff --git a/deps/tools/gen_resource.py b/deps/tools/gen_resource.py new file mode 100755 index 0000000..6c1de17 --- /dev/null +++ b/deps/tools/gen_resource.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re + +parser = argparse.ArgumentParser(description='Generate resource .cpp file.') +parser.add_argument('outputFile', help='output file') +parser.add_argument('inputFile', help='input file') +parser.add_argument('--prefix', dest='prefix', default='', help='C function prefix') +parser.add_argument('--namespace', dest='namespace', default='', help='C++ namespace') + +args = parser.parse_args() + +with open(args.inputFile, "rb") as f: + data = f.read() + +fileSize = len(data) + +inputBase = os.path.basename(args.inputFile) +funcName = "GetResource_" + re.sub(r"[^a-zA-Z0-9]", "_", inputBase) + +with open(args.outputFile, "wt") as f: + print("#include \n#include \nextern \"C\" {\nstatic const unsigned char contents[] = { ", file=f, end='') + print(", ".join("0x%02x" % x for x in data), file=f, end='') + print(" };", file=f) + print("const unsigned char* {}{}(size_t* len) {{\n *len = {};\n return contents;\n}}\n}}".format(args.prefix, funcName, fileSize), file=f) + + if args.namespace: + print("namespace {} {{".format(namespace), file=f) + print("wpi::StringRef {}() {{\n return wpi::StringRef(reinterpret_cast(contents), {});\n}}".format(funcName, fileSize), file=f) + if args.namespace: + print("}", file=f) diff --git a/deps/tools/rpiConfigServer_src/DataHistory.h b/deps/tools/rpiConfigServer_src/DataHistory.h new file mode 100644 index 0000000..f6edc04 --- /dev/null +++ b/deps/tools/rpiConfigServer_src/DataHistory.h @@ -0,0 +1,56 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_DATAHISTORY_H_ +#define WPIUTIL_DATAHISTORY_H_ + +#include + +#include +#include + +template +class DataHistory { + public: + void Add(T val) { + if (m_qty >= Size) { + std::copy(&m_data[1], &m_data[Size], &m_data[0]); + m_data[Size - 1] = val; + } else { + m_data[m_qty++] = val; + } + } + + /* first is "least recent", last is "most recent" */ + bool GetFirstLast(T* first, T* last, size_t* qty) const { + if (m_qty == 0) return false; + if (first) *first = m_data[0]; + if (last) *last = m_data[m_qty - 1]; + if (qty) *qty = m_qty; + return true; + } + + /* only look at most recent "count" samples */ + bool GetFirstLast(T* first, T* last, size_t* qty, size_t count) const { + if (count == 0 || m_qty < count) return false; + if (first) *first = m_data[m_qty - count]; + if (last) *last = m_data[m_qty - 1]; + if (qty) *qty = m_qty; + return true; + } + + T GetTotal(size_t* qty = nullptr) const { + if (qty) *qty = m_qty; + return std::accumulate(&m_data[0], &m_data[m_qty], 0); + } + + private: + T m_data[Size]; + size_t m_qty = 0; +}; + +#endif // WPIUTIL_DATAHISTORY_H_ diff --git a/deps/tools/rpiConfigServer_src/MyHttpConnection.cpp b/deps/tools/rpiConfigServer_src/MyHttpConnection.cpp new file mode 100644 index 0000000..47e1476 --- /dev/null +++ b/deps/tools/rpiConfigServer_src/MyHttpConnection.cpp @@ -0,0 +1,108 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "MyHttpConnection.h" + +#include "WebSocketHandlers.h" +#include "wpi/UrlParser.h" +#include "wpi/raw_ostream.h" + +// static resources +namespace wpi { +StringRef GetResource_bootstrap_4_1_min_js_gz(); +StringRef GetResource_coreui_2_1_min_css_gz(); +StringRef GetResource_coreui_2_1_min_js_gz(); +StringRef GetResource_feather_4_8_min_js_gz(); +StringRef GetResource_jquery_3_3_slim_min_js_gz(); +StringRef GetResource_popper_1_14_min_js_gz(); +StringRef GetResource_wpilib_128_png(); +} // namespace wpi +wpi::StringRef GetResource_frcvision_css(); +wpi::StringRef GetResource_frcvision_js(); +wpi::StringRef GetResource_index_html(); + +MyHttpConnection::MyHttpConnection(std::shared_ptr stream) + : HttpServerConnection(stream), m_websocketHelper(m_request) { + // Handle upgrade event + m_websocketHelper.upgrade.connect([this] { + //wpi::errs() << "got websocket upgrade\n"; + // Disconnect HttpServerConnection header reader + m_dataConn.disconnect(); + m_messageCompleteConn.disconnect(); + + // Accepting the stream may destroy this (as it replaces the stream user + // data), so grab a shared pointer first. + auto self = shared_from_this(); + + // Accept the upgrade + auto ws = m_websocketHelper.Accept(m_stream, "frcvision"); + + // Connect the websocket open event to our connected event. + // Pass self to delay destruction until this callback happens + ws->open.connect_extended([self, s = ws.get()](auto conn, wpi::StringRef) { + wpi::errs() << "websocket connected\n"; + InitWs(*s); + conn.disconnect(); // one-shot + }); + ws->text.connect([s = ws.get()](wpi::StringRef msg, bool) { + ProcessWsText(*s, msg); + }); + }); +} + +void MyHttpConnection::ProcessRequest() { + //wpi::errs() << "HTTP request: '" << m_request.GetUrl() << "'\n"; + wpi::UrlParser url{m_request.GetUrl(), + m_request.GetMethod() == wpi::HTTP_CONNECT}; + if (!url.IsValid()) { + // failed to parse URL + SendError(400); + return; + } + + wpi::StringRef path; + if (url.HasPath()) path = url.GetPath(); + //wpi::errs() << "path: \"" << path << "\"\n"; + + wpi::StringRef query; + if (url.HasQuery()) query = url.GetQuery(); + //wpi::errs() << "query: \"" << query << "\"\n"; + + const bool isGET = m_request.GetMethod() == wpi::HTTP_GET; + if (isGET && (path.equals("/") || path.equals("/index.html"))) { + SendStaticResponse(200, "OK", "text/html", GetResource_index_html(), false); + } else if (isGET && path.equals("/frcvision.css")) { + SendStaticResponse(200, "OK", "text/css", GetResource_frcvision_css(), + false); + } else if (isGET && path.equals("/frcvision.js")) { + SendStaticResponse(200, "OK", "text/javascript", GetResource_frcvision_js(), + false); + } else if (isGET && path.equals("/bootstrap.min.js")) { + SendStaticResponse(200, "OK", "text/javascript", + wpi::GetResource_bootstrap_4_1_min_js_gz(), true); + } else if (isGET && path.equals("/coreui.min.css")) { + SendStaticResponse(200, "OK", "text/css", + wpi::GetResource_coreui_2_1_min_css_gz(), true); + } else if (isGET && path.equals("/coreui.min.js")) { + SendStaticResponse(200, "OK", "text/javascript", + wpi::GetResource_coreui_2_1_min_js_gz(), true); + } else if (isGET && path.equals("/feather.min.js")) { + SendStaticResponse(200, "OK", "text/javascript", + wpi::GetResource_feather_4_8_min_js_gz(), true); + } else if (isGET && path.equals("/jquery-3.3.1.slim.min.js")) { + SendStaticResponse(200, "OK", "text/javascript", + wpi::GetResource_jquery_3_3_slim_min_js_gz(), true); + } else if (isGET && path.equals("/popper.min.js")) { + SendStaticResponse(200, "OK", "text/javascript", + wpi::GetResource_popper_1_14_min_js_gz(), true); + } else if (isGET && path.equals("/wpilib.png")) { + SendStaticResponse(200, "OK", "image/png", + wpi::GetResource_wpilib_128_png(), false); + } else { + SendError(404, "Resource not found"); + } +} diff --git a/deps/tools/rpiConfigServer_src/MyHttpConnection.h b/deps/tools/rpiConfigServer_src/MyHttpConnection.h new file mode 100644 index 0000000..a1c70e6 --- /dev/null +++ b/deps/tools/rpiConfigServer_src/MyHttpConnection.h @@ -0,0 +1,28 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_MYHTTPCONNECTION_H_ +#define WPIUTIL_MYHTTPCONNECTION_H_ + +#include + +#include "wpi/HttpServerConnection.h" +#include "wpi/WebSocketServer.h" +#include "wpi/uv/Stream.h" + +class MyHttpConnection : public wpi::HttpServerConnection, + public std::enable_shared_from_this { + public: + explicit MyHttpConnection(std::shared_ptr stream); + + protected: + void ProcessRequest() override; + + wpi::WebSocketServerHelper m_websocketHelper; +}; + +#endif // WPIUTIL_MYHTTPCONNECTION_H_ diff --git a/deps/tools/rpiConfigServer_src/SystemStatus.cpp b/deps/tools/rpiConfigServer_src/SystemStatus.cpp new file mode 100644 index 0000000..6e50e81 --- /dev/null +++ b/deps/tools/rpiConfigServer_src/SystemStatus.cpp @@ -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::GetInstance() { + static auto sysStatus = std::make_shared(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 strs; + line.split(strs, ' ', -1, false); + if (strs.size() < 4) continue; + + if (strs[1] == "/") return strs[2].contains("rw"); + } + return false; +} + +void SystemStatus::UpdateMemory() { + std::error_code ec; + wpi::raw_fd_istream is("/proc/meminfo", ec); + if (ec) return; + wpi::SmallString<256> lineBuf; + while (!is.has_error()) { + wpi::StringRef line = is.getline(lineBuf, 256).trim(); + if (line.empty()) break; + + wpi::StringRef name, amtStr; + std::tie(name, amtStr) = line.split(':'); + + uint64_t amt; + amtStr = amtStr.trim(); + if (amtStr.consumeInteger(10, amt)) continue; + + if (name == "MemFree") { + m_memoryFree.Add(amt); + } else if (name == "MemAvailable") { + m_memoryAvail.Add(amt); + } + } +} + +void SystemStatus::UpdateCpu() { + std::error_code ec; + wpi::raw_fd_istream is("/proc/stat", ec); + if (ec) return; + wpi::SmallString<256> lineBuf; + while (!is.has_error()) { + wpi::StringRef line = is.getline(lineBuf, 256).trim(); + if (line.empty()) break; + + wpi::StringRef name, amtStr; + std::tie(name, amtStr) = line.split(' '); + if (name == "cpu") { + CpuData data; + + // individual values we care about + amtStr = amtStr.ltrim(); + if (amtStr.consumeInteger(10, data.user)) break; + amtStr = amtStr.ltrim(); + if (amtStr.consumeInteger(10, data.nice)) break; + amtStr = amtStr.ltrim(); + if (amtStr.consumeInteger(10, data.system)) break; + amtStr = amtStr.ltrim(); + if (amtStr.consumeInteger(10, data.idle)) break; + + // compute total + data.total = data.user + data.nice + data.system + data.idle; + for (;;) { + uint64_t amt; + amtStr = amtStr.ltrim(); + if (amtStr.consumeInteger(10, amt)) break; + data.total += amt; + } + + m_cpu.Add(data); + break; + } + } +} + +void SystemStatus::UpdateNetwork() { + std::error_code ec; + wpi::raw_fd_istream is("/proc/net/dev", ec); + if (ec) return; + + NetworkData data; + + wpi::SmallString<256> lineBuf; + while (!is.has_error()) { + wpi::StringRef line = is.getline(lineBuf, 256).trim(); + if (line.empty()) break; + + wpi::StringRef name, amtStr; + std::tie(name, amtStr) = line.split(':'); + name = name.trim(); + if (name.empty() || name == "lo") continue; + + wpi::SmallVector 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); +} diff --git a/deps/tools/rpiConfigServer_src/SystemStatus.h b/deps/tools/rpiConfigServer_src/SystemStatus.h new file mode 100644 index 0000000..7cf23f0 --- /dev/null +++ b/deps/tools/rpiConfigServer_src/SystemStatus.h @@ -0,0 +1,60 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_SYSTEMSTATUS_H_ +#define WPIUTIL_SYSTEMSTATUS_H_ + +#include + +#include "DataHistory.h" +#include "wpi/Signal.h" + +namespace wpi { +class json; +} // namespace wpi + +class SystemStatus { + struct private_init {}; + + public: + explicit SystemStatus(const private_init&) {} + SystemStatus(const SystemStatus&) = delete; + SystemStatus& operator=(const SystemStatus&) = delete; + + void UpdateAll(); + + wpi::json GetStatusJson(); + bool GetWritable(); + + wpi::sig::Signal status; + wpi::sig::Signal writable; + + static std::shared_ptr GetInstance(); + + private: + void UpdateMemory(); + void UpdateCpu(); + void UpdateNetwork(); + + DataHistory m_memoryFree; + DataHistory m_memoryAvail; + struct CpuData { + uint64_t user; + uint64_t nice; + uint64_t system; + uint64_t idle; + uint64_t total; + }; + DataHistory m_cpu; + struct NetworkData { + uint64_t recvBytes = 0; + uint64_t xmitBytes = 0; + }; + DataHistory m_network; +}; + +#endif // WPIUTIL_SYSTEMSTATUS_H_ diff --git a/deps/tools/rpiConfigServer_src/VisionStatus.cpp b/deps/tools/rpiConfigServer_src/VisionStatus.cpp new file mode 100644 index 0000000..de25c8a --- /dev/null +++ b/deps/tools/rpiConfigServer_src/VisionStatus.cpp @@ -0,0 +1,188 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "VisionStatus.h" + +#ifndef _WIN32 +#include +#include +#include +#include +#include +#endif + +#include + +#include "wpi/SmallString.h" +#include "wpi/StringRef.h" +#include "wpi/json.h" +#include "wpi/raw_ostream.h" +#include "wpi/uv/Buffer.h" +#include "wpi/uv/Pipe.h" +#include "wpi/uv/Process.h" +#include "wpi/uv/Work.h" + +namespace uv = wpi::uv; + +#define SERVICE "/service/camera" + +std::shared_ptr VisionStatus::GetInstance() { + static auto visStatus = std::make_shared(private_init{}); + return visStatus; +} + +void VisionStatus::RunSvc(const char* cmd, + std::function onFail) { +#ifndef _WIN32 + struct SvcWorkReq : public uv::WorkReq { + SvcWorkReq(const char* cmd_, std::function onFail_) + : cmd(cmd_), onFail(onFail_) {} + const char* cmd; + std::function onFail; + wpi::SmallString<128> err; + }; + + auto workReq = std::make_shared(cmd, onFail); + workReq->work.connect([r = workReq.get()] { + int fd = open(SERVICE "/supervise/control", O_WRONLY | O_NDELAY); + if (fd == -1) { + wpi::raw_svector_ostream os(r->err); + if (errno == ENXIO) + os << "unable to control service: supervise not running"; + else + os << "unable to control service: " << std::strerror(errno); + } else { + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); + if (write(fd, r->cmd, std::strlen(r->cmd)) == -1) { + wpi::raw_svector_ostream os(r->err); + os << "error writing command: " << std::strerror(errno); + } + close(fd); + } + }); + workReq->afterWork.connect([r = workReq.get()] { + if (r->onFail && !r->err.empty()) r->onFail(r->err.str()); + }); + + uv::QueueWork(m_loop, workReq); +#endif +} + +void VisionStatus::Up(std::function onFail) { + RunSvc("u", onFail); + UpdateStatus(); +} + +void VisionStatus::Down(std::function onFail) { + RunSvc("d", onFail); + UpdateStatus(); +} + +void VisionStatus::Terminate(std::function onFail) { + RunSvc("t", onFail); + UpdateStatus(); +} + +void VisionStatus::Kill(std::function onFail) { + RunSvc("k", onFail); + UpdateStatus(); +} + +void VisionStatus::UpdateStatus() { +#ifndef _WIN32 + struct StatusWorkReq : public uv::WorkReq { + bool enabled = false; + wpi::SmallString<128> status; + }; + + auto workReq = std::make_shared(); + + 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(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(status[15]) << 24) | + (static_cast(status[14]) << 16) | + (static_cast(status[13]) << 8) | + (static_cast(status[12])); + bool paused = status[16]; + auto want = status[17]; + uint64_t when = (static_cast(status[0]) << 56) | + (static_cast(status[1]) << 48) | + (static_cast(status[2]) << 40) | + (static_cast(status[3]) << 32) | + (static_cast(status[4]) << 24) | + (static_cast(status[5]) << 16) | + (static_cast(status[6]) << 8) | + (static_cast(status[7])); + + // constant is from daemontools tai.h + uint64_t now = + 4611686018427387914ULL + static_cast(std::time(nullptr)); + if (now >= when) + when = now - when; + else + when = 0; + + // convert to status string + if (pid) + os << "up (pid " << pid << ") "; + else + os << "down "; + os << when << " seconds"; + if (pid && paused) os << ", paused"; + if (!pid && want == 'u') os << ", want up"; + if (pid && want == 'd') os << ", want down"; + + if (pid) r->enabled = true; + }); + + workReq->afterWork.connect([this, r = workReq.get()] { + wpi::json j = {{"type", "visionStatus"}, + {"visionServiceEnabled", r->enabled}, + {"visionServiceStatus", r->status.str()}}; + update(j); + }); + + uv::QueueWork(m_loop, workReq); +#endif +} + +void VisionStatus::ConsoleLog(uv::Buffer& buf, size_t len) { + wpi::json j = {{"type", "visionLog"}, + {"data", wpi::StringRef(buf.base, len)}}; + log(j); +} diff --git a/deps/tools/rpiConfigServer_src/VisionStatus.h b/deps/tools/rpiConfigServer_src/VisionStatus.h new file mode 100644 index 0000000..aaa2a22 --- /dev/null +++ b/deps/tools/rpiConfigServer_src/VisionStatus.h @@ -0,0 +1,57 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_VISIONSTATUS_H_ +#define WPIUTIL_VISIONSTATUS_H_ + +#include +#include + +#include "wpi/Signal.h" +#include "wpi/StringRef.h" + +namespace wpi { +class json; + +namespace uv { +class Buffer; +class Loop; +} // namespace uv +} // namespace wpi + +class VisionStatus { + struct private_init {}; + + public: + explicit VisionStatus(const private_init&) {} + VisionStatus(const VisionStatus&) = delete; + VisionStatus& operator=(const VisionStatus&) = delete; + + void SetLoop(std::shared_ptr loop) { + m_loop = std::move(loop); + } + + void Up(std::function onFail); + void Down(std::function onFail); + void Terminate(std::function onFail); + void Kill(std::function onFail); + + void UpdateStatus(); + void ConsoleLog(wpi::uv::Buffer& buf, size_t len); + + wpi::sig::Signal update; + wpi::sig::Signal log; + + static std::shared_ptr GetInstance(); + + private: + void RunSvc(const char* cmd, std::function onFail); + + std::shared_ptr m_loop; +}; + +#endif // WPIUTIL_VISIONSTATUS_H_ diff --git a/deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp b/deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp new file mode 100644 index 0000000..5e6817f --- /dev/null +++ b/deps/tools/rpiConfigServer_src/WebSocketHandlers.cpp @@ -0,0 +1,203 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "WebSocketHandlers.h" + +#include + +#include "SystemStatus.h" +#include "VisionStatus.h" +#include "wpi/SmallVector.h" +#include "wpi/WebSocket.h" +#include "wpi/json.h" +#include "wpi/raw_ostream.h" +#include "wpi/raw_uv_ostream.h" +#include "wpi/uv/Loop.h" +#include "wpi/uv/Pipe.h" +#include "wpi/uv/Process.h" + +namespace uv = wpi::uv; + +#define SERVICE "/service/camera" + +struct WebSocketData { + bool visionLogEnabled = false; + + wpi::sig::ScopedConnection sysStatusConn; + wpi::sig::ScopedConnection sysWritableConn; + wpi::sig::ScopedConnection visStatusConn; + wpi::sig::ScopedConnection visLogConn; +}; + +static void SendWsText(wpi::WebSocket& ws, const wpi::json& j) { + wpi::SmallVector toSend; + wpi::raw_uv_ostream os{toSend, 4096}; + os << j; + ws.SendText(toSend, [](wpi::MutableArrayRef bufs, uv::Error) { + for (auto&& buf : bufs) buf.Deallocate(); + }); +} + +template +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(); + 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(); + 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(); + if (d->visionLogEnabled) SendWsText(ws, j); + }); + visStatus->UpdateStatus(); +} + +void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg) { + wpi::errs() << "ws: '" << msg << "'\n"; + + // parse + wpi::json j; + try { + j = wpi::json::parse(msg, nullptr, false); + } catch (const wpi::json::parse_error& e) { + wpi::errs() << "parse error at byte " << e.byte << ": " << e.what() << '\n'; + return; + } + + // top level must be an object + if (!j.is_object()) { + wpi::errs() << "not object\n"; + return; + } + + // type + std::string type; + try { + type = j.at("type").get(); + } 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()->visionLogEnabled = + j.at("value").get(); + } catch (const wpi::json::exception& e) { + wpi::errs() << "could not read visionLogEnabled value: " << e.what() + << '\n'; + return; + } + } + } else if (t == "networkSave") { + } +} diff --git a/deps/tools/rpiConfigServer_src/WebSocketHandlers.h b/deps/tools/rpiConfigServer_src/WebSocketHandlers.h new file mode 100644 index 0000000..567ef52 --- /dev/null +++ b/deps/tools/rpiConfigServer_src/WebSocketHandlers.h @@ -0,0 +1,20 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_WEBSOCKETHANDLERS_H_ +#define WPIUTIL_WEBSOCKETHANDLERS_H_ + +#include "wpi/StringRef.h" + +namespace wpi { +class WebSocket; +} // namespace wpi + +void InitWs(wpi::WebSocket& ws); +void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg); + +#endif // WPIUTIL_WEBSOCKETHANDLERS_H_ diff --git a/deps/tools/rpiConfigServer_src/main.cpp b/deps/tools/rpiConfigServer_src/main.cpp new file mode 100644 index 0000000..7fc96ac --- /dev/null +++ b/deps/tools/rpiConfigServer_src/main.cpp @@ -0,0 +1,136 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include +#include + +#include "MyHttpConnection.h" +#include "SystemStatus.h" +#include "VisionStatus.h" +#include "wpi/ArrayRef.h" +#include "wpi/StringRef.h" +#include "wpi/raw_ostream.h" +#include "wpi/raw_uv_ostream.h" +#include "wpi/timestamp.h" +#include "wpi/uv/Loop.h" +#include "wpi/uv/Process.h" +#include "wpi/uv/Tcp.h" +#include "wpi/uv/Timer.h" +#include "wpi/uv/Udp.h" + +namespace uv = wpi::uv; + +static uint64_t startTime = wpi::Now(); + +int main(int argc, char* argv[]) { + int port = 80; + if (argc == 2) port = std::atoi(argv[1]); + + uv::Process::DisableStdioInheritance(); + + SystemStatus::GetInstance(); + + auto loop = uv::Loop::Create(); + + VisionStatus::GetInstance()->SetLoop(loop); + + loop->error.connect( + [](uv::Error err) { wpi::errs() << "uv ERROR: " << err.str() << '\n'; }); + + auto tcp = uv::Tcp::Create(loop); + + // bind to listen address and port + tcp->Bind("", port); + + // when we get a connection, accept it and start reading + tcp->connection.connect([srv = tcp.get()] { + auto tcp = srv->Accept(); + if (!tcp) return; + // wpi::errs() << "Got a connection\n"; + + // Close on error + tcp->error.connect([s = tcp.get()](wpi::uv::Error err) { + wpi::errs() << "stream error: " << err.str() << '\n'; + s->Close(); + }); + + auto conn = std::make_shared(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(), tcpPtr = tcp.get() ]( + uv::Buffer & buf, size_t len, const sockaddr&, unsigned) { + // build buffers + wpi::SmallVector 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( + {static_cast((pktlen >> 8) & 0xff), + static_cast(pktlen & 0xff), 12, + static_cast((ts >> 24) & 0xff), + static_cast((ts >> 16) & 0xff), + static_cast((ts >> 8) & 0xff), + static_cast(ts & 0xff), + static_cast((*tcpSeq >> 8) & 0xff), + static_cast(*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(); +} diff --git a/deps/tools/rpiConfigServer_src/resources/frcvision.css b/deps/tools/rpiConfigServer_src/resources/frcvision.css new file mode 100644 index 0000000..47d34f7 --- /dev/null +++ b/deps/tools/rpiConfigServer_src/resources/frcvision.css @@ -0,0 +1,69 @@ +.feather { + width: 16px; + height: 16px; + vertical-align: text-bottom; +} + +@-webkit-keyframes spinnow { + 100% { + transform: rotate(360deg); + -webkit-transform: rotate(360deg); + } +} +@-moz-keyframes spinnow { + 100% { + transform: rotate(360deg); + -moz-transform: rotate(360deg); + } +} +@-ms-keyframes spinnow { + 100% { + transform: rotate(360deg); + -ms-transform: rotate(360deg); + } +} + +/* Fix icon on IE */ +_:-ms-lang(x), .brand-icon { + height: 75%; + -ms-interpolation-mode: bicubic; +} + +.spin { + animation: spinnow 5s infinite linear; +} + +/* + * Console Log + */ +.log { + white-space: pre-wrap; + color: black; + font-size: 0.85em; + background: inherit; + border: 0; + padding: 0; + height: 400px; + overflow-y: scroll; +} + +.log .inner-line { + padding: 0 15px; + margin-left: 84pt; + text-indent: -84pt; + margin-bottom: 0; +} + +.log .inner-line:empty::after { + content: '.'; + visibility: hidden; +} + +.log.no-indent .inner-line { + margin-left: 0; + text-indent: 0; +} + +.log .line-selected { + background-color: #ffb2b0; +} diff --git a/deps/tools/rpiConfigServer_src/resources/frcvision.js b/deps/tools/rpiConfigServer_src/resources/frcvision.js new file mode 100644 index 0000000..b5456db --- /dev/null +++ b/deps/tools/rpiConfigServer_src/resources/frcvision.js @@ -0,0 +1,346 @@ +"use strict"; +var connection = null; + +var WebSocket = WebSocket || MozWebSocket; + +/* +// Implement bootstrap 3 style button loading support +(function($) { + $.fn.button = function(action) { + if (action === 'loading' && this.data('loading-text')) { + this.data('original-text', this.html()).html(this.data('loading-text')).prop('disabled', true); + } + if (action === 'reset' && this.data('original-text')) { + this.html(this.data('original-text')).prop('disabled', false); + } + feather.replace(); + }; +}(jQuery)); +*/ + +// HTML escaping +var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' +}; + +function escapeHtml(string) { + return String(string).replace(/[&<>"'`=\/]/g, function (s) { + return entityMap[s]; + }); +} + +function displayStatus(message) { + $('#status-content').html(''); +} + +// Enable and disable buttons based on connection status +var connectedButtonIds = ['systemRestart', 'networkApproach', 'visionUp', 'visionDown', 'visionTerm', 'visionKill', 'systemReadOnly', 'systemWritable']; +var writableButtonIds = ['networkSave']; +var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s', + 'systemMemoryAvail1s', 'systemMemoryAvail5s', + 'systemCpuUser1s', 'systemCpuUser5s', + 'systemCpuSystem1s', 'systemCpuSystem5s', + 'systemCpuIdle1s', 'systemCpuIdle5s', + 'systemNetwork1s', 'systemNetwork5s']; + +function displayDisconnected() { + displayReadOnly(); + $('#connectionBadge').removeClass('badge-primary').addClass('badge-secondary').text('Disconnected'); + $('#visionServiceStatus').removeClass('badge-primary').removeClass('badge-secondary').addClass('badge-dark').text('Unknown Status'); + for (var i = 0; i < connectedButtonIds.length; i++) { + $('#' + connectedButtonIds[i]).prop('disabled', true); + } + for (var i = 0; i < systemStatusIds.length; i++) { + $('#' + systemStatusIds[i]).text(""); + } +} + +function displayConnected() { + $('#connectionBadge').removeClass('badge-secondary').addClass('badge-primary').text('Connected'); + for (var i = 0; i < connectedButtonIds.length; i++) { + $('#' + connectedButtonIds[i]).prop('disabled', false); + } +} + +// Enable and disable buttons based on writable status +function displayReadOnly() { + for (var i = 0; i < writableButtonIds.length; i++) { + $('#' + writableButtonIds[i]).prop('disabled', true); + } + $('#systemReadOnly').addClass('active').prop('aria-pressed', true); + $('#systemWritable').removeClass('active').prop('aria-pressed', false); +} + +function displayWritable() { + for (var i = 0; i < writableButtonIds.length; i++) { + $('#' + writableButtonIds[i]).prop('disabled', false); + } + $('#systemReadOnly').removeClass('active').prop('aria-pressed', false); + $('#systemWritable').addClass('active').prop('aria-pressed', true); +} + +// Handle Read-Only and Writable buttons +$('#systemReadOnly').click(function() { + var $this = $(this); + if ($this.hasClass('active')) return; + var msg = { + type: 'systemReadOnly' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#systemWritable').click(function() { + var $this = $(this); + if ($this.hasClass('active')) return; + var msg = { + type: 'systemWritable' + }; + connection.send(JSON.stringify(msg)); +}); + +// WebSocket automatic reconnection timer +var reconnectTimerId = 0; + +// Establish WebSocket connection +function connect() { + if (connection && connection.readyState !== WebSocket.CLOSED) return; + var serverUrl = "ws://" + window.location.hostname; + if (window.location.port !== '') { + serverUrl += ':' + window.location.port; + } + connection = new WebSocket(serverUrl, 'frcvision'); + connection.onopen = function(evt) { + if (reconnectTimerId) { + window.clearInterval(reconnectTimerId); + reconnectTimerId = 0; + } + displayConnected(); + }; + connection.onclose = function(evt) { + displayDisconnected(); + if (!reconnectTimerId) { + reconnectTimerId = setInterval(function() { connect(); }, 2000); + } + }; + // WebSocket incoming message handling + connection.onmessage = function(evt) { + var msg = JSON.parse(evt.data); + switch (msg.type) { + case 'systemStatus': + for (var i = 0; i < systemStatusIds.length; i++) { + $('#' + systemStatusIds[i]).text(msg[systemStatusIds[i]]); + } + break; + case 'visionStatus': + var elem = $('#visionServiceStatus'); + if (msg.visionServiceStatus) { + elem.text(msg.visionServiceStatus); + } + if (msg.visionServiceEnabled && !elem.hasClass('badge-primary')) { + elem.removeClass('badge-dark').removeClass('badge-secondary').addClass('badge-primary'); + } else if (!msg.visionServiceEnabled && !elem.hasClass('badge-secondary')) { + elem.removeClass('badge-dark').removeClass('badge-primary').addClass('badge-secondary'); + } + break; + case 'visionLog': + visionLog(msg.data); + break; + case 'networkSettings': + $('#networkApproach').value = msg.networkApproach; + $('#networkAddress').value = msg.networkAddress; + $('#networkMask').value = msg.networkMask; + $('#networkGateway').value = msg.networkGateway; + $('#networkDNS').value = msg.networkDNS; + break; + case 'systemReadOnly': + displayReadOnly(); + break; + case 'systemWritable': + displayWritable(); + break; + case 'status': + displayStatus(msg.message); + break; + } + }; +} + +// Button handlers +$('#systemRestart').click(function() { + var msg = { + type: 'systemRestart' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#visionUp').click(function() { + var msg = { + type: 'visionUp' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#visionDown').click(function() { + var msg = { + type: 'visionDown' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#visionTerm').click(function() { + var msg = { + type: 'visionTerm' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#visionKill').click(function() { + var msg = { + type: 'visionKill' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#visionLogEnabled').change(function() { + var msg = { + type: 'visionLogEnabled', + value: this.checked + }; + connection.send(JSON.stringify(msg)); +}); + +// +// Vision console output +// +var visionConsole = document.getElementById('visionConsole'); +var visionLogEnabled = $('#visionLogEnabled'); +var _linesLimit = 100; + +/* +function escape_for_html(txt) { + return txt.replace(/[&<>]/gm, function(str) { + if (str == "&") return "&"; + if (str == "<") return "<"; + if (str == ">") return ">"; + }); +} +*/ + +function visionLog(data) { + if (!visionLogEnabled.prop('checked')) { + return; + } + var wasScrolledBottom = (visionConsole.scrollTop === (visionConsole.scrollHeight - visionConsole.offsetHeight)); + var div = document.createElement('div'); + var p = document.createElement('p'); + p.className = 'inner-line'; + + // escape HTML tags + data = escapeHtml(data); + p.innerHTML = data; + + div.className = 'line'; + div.addEventListener('click', function click() { + if (this.className.indexOf('selected') === -1) { + this.className = 'line-selected'; + } else { + this.className = 'line'; + } + }); + + div.appendChild(p); + visionConsole.appendChild(div); + + if (visionConsole.children.length > _linesLimit) { + visionConsole.removeChild(visionConsole.children[0]); + } + + if (wasScrolledBottom) { + visionConsole.scrollTop = visionConsole.scrollHeight; + } +} + +// Show details when appropriate for network approach +$('#networkApproach').change(function() { + if (this.value == "dhcp") { + $('#networkIpDetails').collapse('hide'); + } else { + $('#networkIpDetails').collapse('show'); + } +}); + +// Network Save button handler +$('#networkSave').click(function() { + var $this = $(this); + $this.button('loading'); + var msg = { + type: 'networkSave', + networkApproach: $('#networkApproach').value, + networkAddress: $('#networkAddress').value, + networkMask: $('#networkMask').value, + networkGateway: $('#networkGateway').value, + networkDNS: $('#networkDNS').value + }; + connection.send(JSON.stringify(msg)); +}); + +// Show details when appropriate for NT client +$('#visionClient').change(function() { + if (this.checked) { + $('#visionClientDetails').collapse('show'); + } else { + $('#visionClientDetails').collapse('hide'); + } +}); + +// Update view from data structure +var visionSettings = { + team: '294', + ntmode: 'client', + cameras: [] +}; + +function updateVisionSettingsCameraView(cardElem, data) { +} + +function updateVisionSettingsView() { + $('#visionClient').prop('checked', visionSettings.ntmode === 'client'); + if (visionSettings.ntmode === 'client') { + $('#visionClientDetails').collapse('show'); + } else { + $('#visionClientDetails').collapse('hide'); + } + $('#visionTeam').val(visionSettings.team); + + var newCamera = $('#cameraNEW').clone(); + newCamera.find('[id]').each(function() { + $(this).attr('id', $(this).attr('id').replace('NEW', '')); + }); + newCamera.find('[for]').each(function() { + $(this).attr('for', $(this).attr('for').replace('NEW', '')); + }); +} + +$('#cameraSettingsFile0').change(function() { + if (this.files.length <= 0) { + return false; + } + var fr = new FileReader(); + fr.onload = function(e) { + var result = JSON.parse(e.target.result); + console.log(result); + }; + fr.readAsText(this.files.item(0)); +}); + +// Start with display disconnected and start initial connection attempt +displayDisconnected(); +updateVisionSettingsView(); +connect(); diff --git a/deps/tools/rpiConfigServer_src/resources/index.html b/deps/tools/rpiConfigServer_src/resources/index.html new file mode 100644 index 0000000..5a894af --- /dev/null +++ b/deps/tools/rpiConfigServer_src/resources/index.html @@ -0,0 +1,440 @@ + + + + + + + + + + + WPILib FRCVision Raspberry PI Configuration + + + + +
+ + + + +
+ + +
+
+ + + + + + + + + +