From eff48d09257c29c9b3e545ade734b82d5ed4f59e Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Mon, 9 Nov 2020 00:32:11 -0800 Subject: [PATCH] Add Romi image --- deps/tools/configServer/Makefile | 14 +- .../configServer/src/NetworkSettings.cpp | 425 ++++++++++++++---- deps/tools/configServer/src/NetworkSettings.h | 6 +- deps/tools/configServer/src/RomiStatus.cpp | 228 ++++++++++ deps/tools/configServer/src/RomiStatus.h | 59 +++ .../configServer/src/WebSocketHandlers.cpp | 83 +++- deps/tools/configServer/src/main.cpp | 22 + .../configServer/src/resources/frcvision.js | 256 ++++++++++- .../configServer/src/resources/index.html | 155 ++++++- export-image/02-network/01-run.sh | 12 +- stage2/01-sys-tweaks/01-run.sh | 3 +- .../02-net-tweaks/files/wpa_supplicant.conf | 1 + stage5/01-sys-tweaks/00-packages | 5 + stage5/01-sys-tweaks/01-run.sh | 48 ++ stage5/01-sys-tweaks/files/configServer_run | 2 + stage5/01-sys-tweaks/files/i2c.conf | 1 + stage5/01-sys-tweaks/files/uploadRomi.py | 60 +++ .../01-sys-tweaks/files/wpilibws-romi_log_run | 2 + stage5/01-sys-tweaks/files/wpilibws-romi_run | 6 + stage5/02-net-tweaks/01-run.sh | 32 ++ stage5/02-net-tweaks/files/dnsmasq.conf | 2 + stage5/02-net-tweaks/files/hostapd.conf | 13 + stage5/EXPORT_IMAGE | 5 + stage5/prerun.sh | 5 + 24 files changed, 1346 insertions(+), 99 deletions(-) create mode 100644 deps/tools/configServer/src/RomiStatus.cpp create mode 100644 deps/tools/configServer/src/RomiStatus.h create mode 100644 stage5/01-sys-tweaks/00-packages create mode 100755 stage5/01-sys-tweaks/01-run.sh create mode 100755 stage5/01-sys-tweaks/files/configServer_run create mode 100644 stage5/01-sys-tweaks/files/i2c.conf create mode 100755 stage5/01-sys-tweaks/files/uploadRomi.py create mode 100755 stage5/01-sys-tweaks/files/wpilibws-romi_log_run create mode 100755 stage5/01-sys-tweaks/files/wpilibws-romi_run create mode 100755 stage5/02-net-tweaks/01-run.sh create mode 100644 stage5/02-net-tweaks/files/dnsmasq.conf create mode 100644 stage5/02-net-tweaks/files/hostapd.conf create mode 100644 stage5/EXPORT_IMAGE create mode 100755 stage5/prerun.sh diff --git a/deps/tools/configServer/Makefile b/deps/tools/configServer/Makefile index 4735301128..678df5f61d 100644 --- a/deps/tools/configServer/Makefile +++ b/deps/tools/configServer/Makefile @@ -4,10 +4,13 @@ DEPS_LIBS?=$(shell pkg-config --libs --static cscore wpiutil) EXEC_HOME?=/home/${FIRST_USER_NAME} FRC_JSON?=/boot/frc.json DHCPCD_CONF?=/boot/dhcpcd.conf +WPA_SUPPLICANT_CONF?=/etc/wpa_supplicant/wpa_supplicant.conf +DNSMASQ_CONF?=/etc/dnsmasq.d/wpilib.conf +HOSTAPD_CONF?=/etc/hostapd/hostapd.conf APP_UID?=1000 APP_GID?=1000 -.PHONY: all clean +.PHONY: all clean install .SUFFIXES: all: configServer @@ -17,11 +20,17 @@ clean: rm -f src/*.o rm -f src/resources/*.o +install: + svc -d /service/configServer + install -m 755 configServer /usr/local/sbin/ + svc -u /service/configServer + OBJS= \ src/main.o \ src/Application.o \ src/MyHttpConnection.o \ src/NetworkSettings.o \ + src/RomiStatus.o \ src/SystemStatus.o \ src/UploadHelper.o \ src/VisionSettings.o \ @@ -41,6 +50,9 @@ configServer: ${OBJS} '-DEXEC_HOME="${EXEC_HOME}"' \ '-DFRC_JSON="${FRC_JSON}"' \ '-DDHCPCD_CONF="${DHCPCD_CONF}"' \ + '-DWPA_SUPPLICANT_CONF="${WPA_SUPPLICANT_CONF}"' \ + '-DDNSMASQ_CONF="${DNSMASQ_CONF}"' \ + '-DHOSTAPD_CONF="${HOSTAPD_CONF}"' \ '-DAPP_UID=${APP_UID}' \ '-DAPP_GID=${APP_GID}' \ $< diff --git a/deps/tools/configServer/src/NetworkSettings.cpp b/deps/tools/configServer/src/NetworkSettings.cpp index 658cbde55b..d03f90d89e 100644 --- a/deps/tools/configServer/src/NetworkSettings.cpp +++ b/deps/tools/configServer/src/NetworkSettings.cpp @@ -7,6 +7,8 @@ #include "NetworkSettings.h" +#include + #include #include @@ -21,6 +23,8 @@ namespace uv = wpi::uv; +extern bool romi; + #define GEN_MARKER "###### BELOW THIS LINE EDITED BY RPICONFIGSERVER ######" /* @@ -64,10 +68,10 @@ std::shared_ptr NetworkSettings::GetInstance() { return inst; } -void NetworkSettings::Set(Mode mode, wpi::StringRef address, - wpi::StringRef mask, wpi::StringRef gateway, - wpi::StringRef dns, - std::function onFail) { +static std::string BuildDhcpcdSetting( + wpi::StringRef iface, NetworkSettings::Mode mode, wpi::StringRef address, + wpi::StringRef mask, wpi::StringRef gateway, wpi::StringRef dns, + std::function onFail) { // validate and sanitize inputs wpi::SmallString<32> addressOut; unsigned int cidr; @@ -82,7 +86,7 @@ void NetworkSettings::Set(Mode mode, wpi::StringRef address, err += address; err += "'"; onFail(err); - return; + return {}; } wpi::uv::AddrToName(addressAddr, &addressOut); @@ -93,7 +97,7 @@ void NetworkSettings::Set(Mode mode, wpi::StringRef address, err += mask; err += "'"; onFail(err); - return; + return {}; } // gateway (may be blank) @@ -114,7 +118,7 @@ void NetworkSettings::Set(Mode mode, wpi::StringRef address, err += dnsStr; err += "'"; onFail(err); - return; + return {}; } wpi::uv::AddrToName(dnsAddr, &oneDnsOut); if (!first) dnsOut += ' '; @@ -122,59 +126,248 @@ void NetworkSettings::Set(Mode mode, wpi::StringRef address, dnsOut += oneDnsOut; } - // read file (up to but not including the marker) - std::vector lines; - std::error_code ec; + std::string rv; + wpi::raw_string_ostream os{rv}; + // write generated config + switch (mode) { + case NetworkSettings::kDhcp: + os << '\n'; // nothing required + break; + case NetworkSettings::kStatic: + os << "interface " << iface << '\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 NetworkSettings::kDhcpStatic: + os << "profile static_" << iface << '\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 " << iface << '\n'; + os << "fallback static_" << iface << '\n'; + break; + } + return os.str(); +} + +void NetworkSettings::Set(Mode mode, wpi::StringRef address, + wpi::StringRef mask, wpi::StringRef gateway, + wpi::StringRef dns, WifiMode wifiAPMode, + int wifiChannel, wpi::StringRef wifiSsid, + wpi::StringRef wifiWpa2, Mode wifiMode, + wpi::StringRef wifiAddress, wpi::StringRef wifiMask, + wpi::StringRef wifiGateway, wpi::StringRef wifiDns, + std::function onFail) { + // sanity check access point settings + if (wifiAPMode == kAccessPoint) { + if (wifiSsid.empty()) { + onFail("must set SSID for access point"); + return; + } + if (wifiMode != kStatic || wifiAddress.empty() || wifiMask.empty()) { + onFail("must set static address and netmask for access point"); + return; + } + if (!wifiWpa2.empty() && (wifiWpa2.size() < 8 || wifiWpa2.size() > 63)) { + onFail("passphrase must be between 8 and 63 characters"); + return; + } + } + + // + // dhcpcd configuration + // { - wpi::raw_fd_istream is(DHCPCD_CONF, ec); - if (ec) { - onFail("could not read " DHCPCD_CONF); + std::string eth0 = + BuildDhcpcdSetting("eth0", mode, address, mask, gateway, dns, onFail); + if (eth0.empty()) return; + std::string wlan0; + if (romi) { + wlan0 = BuildDhcpcdSetting("wlan0", wifiMode, wifiAddress, wifiMask, + wifiGateway, wifiDns, onFail); + if (wlan0.empty()) return; + } + + // read file (up to but not including the marker) + std::vector 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 + os << eth0; + os << '\n'; + os << wlan0; + if (wifiAPMode == kAccessPoint) { + os << "nohook wpa_supplicant\n"; + } + } + } + + // + // dnsmasq configuration + // + if (wifiAPMode == kAccessPoint) { + // take the 3 parts of IP range + in_addr addressAddr; + if (wpi::uv::NameToAddr(wifiAddress, &addressAddr) != 0) { + wpi::SmallString<128> err; + err += "invalid address '"; + err += wifiAddress; + err += "'"; + onFail(err); return; } + wpi::SmallString<32> addressOut; + wpi::uv::AddrToName(addressAddr, &addressOut); + wpi::StringRef addr3part = wpi::StringRef{addressOut}.rsplit('.').first; - 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); + std::error_code ec; + wpi::raw_fd_ostream os(DNSMASQ_CONF, ec, wpi::sys::fs::F_Text); + if (ec) { + onFail("could not write " DNSMASQ_CONF); + return; } + os << "interface=wlan0\n"; + os << "dhcp-range=" << addr3part << ".100," << addr3part << ".200," + << wifiMask << ",5m\n"; + } else { + // remove dnsmasq config file + unlink(DNSMASQ_CONF); } - // write file - { - // write original lines - wpi::raw_fd_ostream os(DHCPCD_CONF, ec, wpi::sys::fs::F_Text); + // + // hostapd configuration + // + if (wifiAPMode == kAccessPoint) { + std::error_code ec; + wpi::raw_fd_ostream os(HOSTAPD_CONF, ec, wpi::sys::fs::F_Text); if (ec) { - onFail("could not write " DHCPCD_CONF); + onFail("could not write " HOSTAPD_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; + os << "interface=wlan0\n"; + os << "hw_mode=g\n"; + os << "channel=" << wifiChannel << '\n'; + os << "wmm_enabled=0\n"; + os << "macaddr_acl=0\n"; + os << "auth_algs=1\n"; + os << "ignore_broadcast_ssid=0\n"; + os << "ssid=" << wifiSsid << '\n'; + if (!wifiWpa2.empty()) { + os << "wpa=2\n"; + os << "wpa_key_mgmt=WPA-PSK\n"; + os << "wpa_pairwise=TKIP\n"; + os << "rsn_pairwise=CCMP\n"; + os << "wpa_passphrase=" << wifiWpa2 << '\n'; } + } else { + // remove hostapd config file + unlink(HOSTAPD_CONF); + } + + // + // wpa supplicant configuration + // + { + // read file (up to but not including the marker) + std::vector lines; + std::error_code ec; + { + wpi::raw_fd_istream is(WPA_SUPPLICANT_CONF, ec); + if (ec) { + onFail("could not read " WPA_SUPPLICANT_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(WPA_SUPPLICANT_CONF, ec, wpi::sys::fs::F_Text); + if (ec) { + onFail("could not write " WPA_SUPPLICANT_CONF); + return; + } + for (auto&& line : lines) os << line << '\n'; + + // write marker + os << GEN_MARKER << '\n'; + + // write generated config + os << "network={\n"; + os << " ssid=\"" << wifiSsid << "\"\n"; + if (!wifiWpa2.empty()) { + os << " psk=\"" << wifiWpa2 << "\"\n"; + } else { + os << " key_mgmt=NONE\n"; + } + os << "}\n"; + } + } + + // start or stop hostapd + if (wifiAPMode == kAccessPoint) { + if (auto proc = + uv::Process::Spawn(m_loop, "/usr/bin/systemctl", + "/usr/bin/systemctl", "unmask", "hostapd")) { + proc->exited.connect([p = proc.get(), loop = m_loop](int64_t, int) { + p->Close(); + if (auto proc = uv::Process::Spawn(loop, "/usr/bin/systemctl", + "/usr/bin/systemctl", "restart", + "hostapd")) { + proc->exited.connect([p = proc.get()](int64_t, int) { p->Close(); }); + } + }); + } + } else { + if (auto proc = uv::Process::Spawn(m_loop, "/usr/bin/systemctl", + "/usr/bin/systemctl", "--now", "mask", + "hostapd")) { + proc->exited.connect([p = proc.get()](int64_t, int) { p->Close(); }); + } + } + + // tell dnsmasq to restart + if (auto proc = + uv::Process::Spawn(m_loop, "/usr/sbin/service", "/usr/sbin/service", + "dnsmasq", "restart")) { + proc->exited.connect([p = proc.get()](int64_t, int) { p->Close(); }); } // tell dhcpcd to reload config @@ -183,46 +376,114 @@ void NetworkSettings::Set(Mode mode, wpi::StringRef address, proc->exited.connect([p = proc.get()](int64_t, int) { p->Close(); }); } + // tell wpa to reconfigure + if (auto proc = uv::Process::Spawn(m_loop, "/sbin/wpa_cli", "/sbin/wpa_cli", + "-i", "wlan0", "reconfigure")) { + 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); + wpi::json j = {{"type", "networkSettings"}, + {"networkApproach", "dhcp"}, + {"wifiNetworkApproach", "dhcp"}}; + + // + // dhcpcd configuration + // + { + 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::SmallString<256> lineBuf; + bool foundMarker = false; + bool wlan = 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.contains("wlan0")) wlan = true; + if (line.startswith("static ip_address")) { + j[wlan ? "wifiNetworkApproach" : "networkApproach"] = "static"; + + wpi::StringRef value = line.split('=').second.trim(); + wpi::StringRef cidrStr; + std::tie(j[wlan ? "wifiNetworkAddress" : "networkAddress"], cidrStr) = + value.split('/'); + + unsigned int cidrInt; + if (!cidrStr.getAsInteger(10, cidrInt)) { + wpi::SmallString<64> netmaskBuf; + j[wlan ? "wifiNetworkMask" : "networkMask"] = + CidrToNetmask(cidrInt, netmaskBuf); + } + } else if (line.startswith("static routers")) { + j[wlan ? "wifiNetworkGateway" : "networkGateway"] = + line.split('=').second.trim(); + } else if (line.startswith("static domain_name_servers")) { + j[wlan ? "wifiNetworkDNS" : "networkDNS"] = + line.split('=').second.trim(); + } else if (line.startswith("fallback")) { + j[wlan ? "wifiNetworkApproach" : "networkApproach"] = "dhcp-fallback"; + } + } + } + + // + // hostapd configuration + // + { + std::error_code ec; + wpi::raw_fd_istream is(HOSTAPD_CONF, ec); + if (ec) { + j["wifiMode"] = "bridge"; + j["wifiChannel"] = ""; + } else { + j["wifiMode"] = "access-point"; + wpi::SmallString<256> lineBuf; + while (!is.has_error()) { + wpi::StringRef line = is.getline(lineBuf, 256).trim(); + if (line.startswith("channel=")) { + j["wifiChannel"] = line.drop_front(8); + } + } + } + } + + // + // wpa supplicant configuration + // + { + std::error_code ec; + wpi::raw_fd_istream is(WPA_SUPPLICANT_CONF, ec); + if (ec) { + wpi::errs() << "could not read " WPA_SUPPLICANT_CONF "\n"; + return j; + } + + 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.contains("ssid")) { + j["wifiSsid"] = line.split('=').second.trim().drop_front().drop_back(); + } else if (line.contains("psk")) { + j["wifiWpa2"] = line.split('=').second.trim().drop_front().drop_back(); + } else if (line.contains("key_mgmt")) { + j["wifiWpa2"] = ""; } - } 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"; } } diff --git a/deps/tools/configServer/src/NetworkSettings.h b/deps/tools/configServer/src/NetworkSettings.h index d8f13a8b1b..e106d827ac 100644 --- a/deps/tools/configServer/src/NetworkSettings.h +++ b/deps/tools/configServer/src/NetworkSettings.h @@ -31,10 +31,14 @@ class NetworkSettings { m_loop = std::move(loop); } + enum WifiMode { kBridge, kAccessPoint }; enum Mode { kDhcp, kStatic, kDhcpStatic }; void Set(Mode mode, wpi::StringRef address, wpi::StringRef mask, - wpi::StringRef gateway, wpi::StringRef dns, + wpi::StringRef gateway, wpi::StringRef dns, WifiMode wifiAPMode, + int wifiChannel, wpi::StringRef wifiSsid, wpi::StringRef wifiWpa2, + Mode wifiMode, wpi::StringRef wifiAddress, wpi::StringRef wifiMask, + wpi::StringRef wifiGateway, wpi::StringRef wifiDns, std::function onFail); void UpdateStatus(); diff --git a/deps/tools/configServer/src/RomiStatus.cpp b/deps/tools/configServer/src/RomiStatus.cpp new file mode 100644 index 0000000000..320cb4d84f --- /dev/null +++ b/deps/tools/configServer/src/RomiStatus.cpp @@ -0,0 +1,228 @@ +/*----------------------------------------------------------------------------*/ +/* 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 "RomiStatus.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace uv = wpi::uv; + +#define SERVICE "/service/wpilibws-romi" + +std::shared_ptr RomiStatus::GetInstance() { + static auto status = std::make_shared(private_init{}); + return status; +} + +void RomiStatus::SetLoop(std::shared_ptr loop) { + m_loop = std::move(loop); +} + +void RomiStatus::RunSvc(const char* cmd, + std::function onFail) { + 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); +} + +void RomiStatus::Up(std::function onFail) { + RunSvc("u", onFail); + UpdateStatus(); +} + +void RomiStatus::Down(std::function onFail) { + RunSvc("d", onFail); + UpdateStatus(); +} + +void RomiStatus::Terminate(std::function onFail) { + RunSvc("t", onFail); + UpdateStatus(); +} + +void RomiStatus::Kill(std::function onFail) { + RunSvc("k", onFail); + UpdateStatus(); +} + +void RomiStatus::UpdateStatus() { + 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", "romiStatus"}, + {"romiServiceEnabled", r->enabled}, + {"romiServiceStatus", r->status.str()}}; + update(j); + }); + + uv::QueueWork(m_loop, workReq); + + { + wpi::json j = {{"type", "romiFirmwareInterface"}, + {"exists", access("/dev/ttyACM0", 0) == 0}}; + update(j); + } +} + +void RomiStatus::ConsoleLog(uv::Buffer& buf, size_t len) { + wpi::json j = {{"type", "romiLog"}, + {"data", wpi::StringRef(buf.base, len)}}; + log(j); +} + +void RomiStatus::FirmwareUpdate(std::function onFail) { + // create pipe to capture stdout + auto pipe = uv::Pipe::Create(m_loop); + if (auto proc = uv::Process::Spawn( + m_loop, "/usr/bin/python", + pipe ? uv::Process::StdioCreatePipe(1, *pipe, UV_WRITABLE_PIPE) + : uv::Process::Option(), + "/usr/bin/python", "/usr/src/wpilib-ws-romi/scripts/uploadRomi.py")) { + // send stdout output to firmware log + if (pipe) { + pipe->StartRead(); + pipe->data.connect([=](uv::Buffer& buf, size_t len) { + wpi::json j = {{"type", "romiFirmwareLog"}, + {"data", wpi::StringRef(buf.base, len)}}; + update(j); + }); + pipe->end.connect([p = pipe.get()] { p->Close(); }); + } + + // on exit, report + proc->exited.connect( + [this, p = proc.get(), onFail](int64_t status, int sig) { + wpi::json j = {{"type", "romiFirmwareComplete"}}; + update(j); + if (status != EXIT_SUCCESS) { + onFail("firmware update failed"); + } + p->Close(); + }); + } else { + onFail("could not spawn process"); + } +} diff --git a/deps/tools/configServer/src/RomiStatus.h b/deps/tools/configServer/src/RomiStatus.h new file mode 100644 index 0000000000..38aa07b0e5 --- /dev/null +++ b/deps/tools/configServer/src/RomiStatus.h @@ -0,0 +1,59 @@ +/*----------------------------------------------------------------------------*/ +/* 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_ROMISTATUS_H_ +#define RPICONFIGSERVER_ROMISTATUS_H_ + +#include +#include +#include + +#include +#include +#include +#include + +namespace wpi { +class json; + +namespace uv { +class Buffer; +} // namespace uv +} // namespace wpi + +class RomiStatus { + struct private_init {}; + + public: + explicit RomiStatus(const private_init&) {} + RomiStatus(const RomiStatus&) = delete; + RomiStatus& operator=(const RomiStatus&) = delete; + + void SetLoop(std::shared_ptr 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); + + void FirmwareUpdate(std::function onFail); + + 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 // RPICONFIGSERVER_VISIONSTATUS_H_ diff --git a/deps/tools/configServer/src/WebSocketHandlers.cpp b/deps/tools/configServer/src/WebSocketHandlers.cpp index 164eb9130c..7198eadd0e 100644 --- a/deps/tools/configServer/src/WebSocketHandlers.cpp +++ b/deps/tools/configServer/src/WebSocketHandlers.cpp @@ -23,6 +23,7 @@ #include "Application.h" #include "NetworkSettings.h" +#include "RomiStatus.h" #include "SystemStatus.h" #include "UploadHelper.h" #include "VisionSettings.h" @@ -30,10 +31,11 @@ namespace uv = wpi::uv; -#define SERVICE "/service/camera" +extern bool romi; struct WebSocketData { bool visionLogEnabled = false; + bool romiLogEnabled = false; UploadHelper upload; @@ -41,6 +43,8 @@ struct WebSocketData { wpi::sig::ScopedConnection sysWritableConn; wpi::sig::ScopedConnection visStatusConn; wpi::sig::ScopedConnection visLogConn; + wpi::sig::ScopedConnection romiStatusConn; + wpi::sig::ScopedConnection romiLogConn; wpi::sig::ScopedConnection cameraListConn; wpi::sig::ScopedConnection netSettingsConn; wpi::sig::ScopedConnection visSettingsConn; @@ -135,6 +139,20 @@ void InitWs(wpi::WebSocket& ws) { [&ws](const wpi::json& j) { SendWsText(ws, j); }); visStatus->UpdateCameraList(); + // hook up romi status updates and logging + if (romi) { + SendWsText(ws, {{"type", "romiEnable"}}); + auto romiStatus = RomiStatus::GetInstance(); + data->romiStatusConn = romiStatus->update.connect_connection( + [&ws](const wpi::json& j) { SendWsText(ws, j); }); + data->romiLogConn = + romiStatus->log.connect_connection([&ws](const wpi::json& j) { + auto d = ws.GetData(); + if (d->romiLogEnabled) SendWsText(ws, j); + }); + romiStatus->UpdateStatus(); + } + // send initial network settings auto netSettings = NetworkSettings::GetInstance(); auto netSettingsFunc = [&ws](const wpi::json& j) { SendWsText(ws, j); }; @@ -243,6 +261,32 @@ void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg) { return; } } + } else if (t.startswith("romi") && romi) { + wpi::StringRef subType = t.substr(4); + + auto statusFunc = [s = ws.shared_from_this()](wpi::StringRef msg) { + SendWsText(*s, {{"type", "status"}, {"message", msg}}); + }; + + if (subType == "Up") { + RomiStatus::GetInstance()->Up(statusFunc); + } else if (subType == "Down") { + RomiStatus::GetInstance()->Down(statusFunc); + } else if (subType == "Term") { + RomiStatus::GetInstance()->Terminate(statusFunc); + } else if (subType == "Kill") { + RomiStatus::GetInstance()->Kill(statusFunc); + } else if (subType == "FirmwareUpdate") { + RomiStatus::GetInstance()->FirmwareUpdate(statusFunc); + } else if (subType == "LogEnabled") { + try { + ws.GetData()->romiLogEnabled = j.at("value").get(); + } catch (const wpi::json::exception& e) { + wpi::errs() << "could not read romiLogEnabled 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}}); @@ -261,11 +305,46 @@ void ProcessWsText(wpi::WebSocket& ws, wpi::StringRef msg) { << approach << '\n'; return; } + + NetworkSettings::Mode wifiMode; + auto& wifiApproach = + j.at("wifiNetworkApproach").get_ref(); + if (wifiApproach == "dhcp") + wifiMode = NetworkSettings::kDhcp; + else if (wifiApproach == "static") + wifiMode = NetworkSettings::kStatic; + else if (wifiApproach == "dhcp-fallback") + wifiMode = NetworkSettings::kDhcpStatic; + else { + wpi::errs() << "could not understand wifiNetworkApproach value: " + << wifiApproach << '\n'; + return; + } + + NetworkSettings::WifiMode wifiAPMode; + auto& wifiAPModeStr = j.at("wifiMode").get_ref(); + if (wifiAPModeStr == "bridge") + wifiAPMode = NetworkSettings::kBridge; + else if (wifiAPModeStr == "access-point") + wifiAPMode = NetworkSettings::kAccessPoint; + else { + wpi::errs() << "could not understand wifiMode value: " + << wifiAPModeStr << '\n'; + return; + } + NetworkSettings::GetInstance()->Set( mode, j.at("networkAddress").get_ref(), j.at("networkMask").get_ref(), j.at("networkGateway").get_ref(), - j.at("networkDNS").get_ref(), statusFunc); + j.at("networkDNS").get_ref(), wifiAPMode, + j.at("wifiChannel").get(), + j.at("wifiSsid").get_ref(), + j.at("wifiWpa2").get_ref(), wifiMode, + j.at("wifiNetworkAddress").get_ref(), + j.at("wifiNetworkMask").get_ref(), + j.at("wifiNetworkGateway").get_ref(), + j.at("wifiNetworkDNS").get_ref(), statusFunc); } catch (const wpi::json::exception& e) { wpi::errs() << "could not read networkSave value: " << e.what() << '\n'; return; diff --git a/deps/tools/configServer/src/main.cpp b/deps/tools/configServer/src/main.cpp index 2f8b995c99..abc83f8c9e 100644 --- a/deps/tools/configServer/src/main.cpp +++ b/deps/tools/configServer/src/main.cpp @@ -21,15 +21,22 @@ #include "MyHttpConnection.h" #include "NetworkSettings.h" +#include "RomiStatus.h" #include "SystemStatus.h" #include "VisionStatus.h" namespace uv = wpi::uv; +bool romi = false; static uint64_t startTime = wpi::Now(); int main(int argc, char* argv[]) { int port = 80; + if (argc >= 2 && wpi::StringRef(argv[1]) == "--romi") { + --argc; + ++argv; + romi = true; + } if (argc == 2) port = std::atoi(argv[1]); uv::Process::DisableStdioInheritance(); @@ -39,6 +46,7 @@ int main(int argc, char* argv[]) { auto loop = uv::Loop::Create(); NetworkSettings::GetInstance()->SetLoop(loop); + if (romi) RomiStatus::GetInstance()->SetLoop(loop); VisionStatus::GetInstance()->SetLoop(loop); loop->error.connect( @@ -76,6 +84,9 @@ int main(int argc, char* argv[]) { timer->timeout.connect([&loop] { SystemStatus::GetInstance()->UpdateAll(); VisionStatus::GetInstance()->UpdateStatus(); + if (romi) { + RomiStatus::GetInstance()->UpdateStatus(); + } }); // listen on port 6666 for console logging @@ -87,6 +98,17 @@ int main(int argc, char* argv[]) { VisionStatus::GetInstance()->ConsoleLog(buf, len); }); + // listen on port 7777 for romi console logging + if (romi) { + auto udpCon = uv::Udp::Create(loop); + udpCon->Bind("127.0.0.1", 7777, UV_UDP_REUSEADDR); + udpCon->StartRecv(); + udpCon->received.connect( + [](uv::Buffer& buf, size_t len, const sockaddr&, unsigned) { + RomiStatus::GetInstance()->ConsoleLog(buf, len); + }); + } + // create riolog console port auto tcpCon = uv::Tcp::Create(loop); tcpCon->Bind("", 1740); diff --git a/deps/tools/configServer/src/resources/frcvision.js b/deps/tools/configServer/src/resources/frcvision.js index 1a580433cc..2a94831c15 100644 --- a/deps/tools/configServer/src/resources/frcvision.js +++ b/deps/tools/configServer/src/resources/frcvision.js @@ -48,8 +48,60 @@ function dismissStatus() { } // 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', 'cameraKey'] +var connectedButtonIds = [ + 'systemRestart', + 'networkApproach', + 'networkAddress', + 'networkMask', + 'networkGateway', + 'networkDNS', + 'wifiMode', + 'wifiChannel', + 'wifiSsid', + 'wifiWpa2', + 'wifiNetworkApproach', + 'wifiNetworkAddress', + 'wifiNetworkMask', + 'wifiNetworkGateway', + 'wifiNetworkDNS', + 'romiUp', + 'romiDown', + 'romiTerm', + 'romiKill', + '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', + 'cameraKey' +]; var writableButtonIds = ['networkSave', 'visionSave', 'applicationSave', 'fileUploadButton']; var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s', 'systemMemoryAvail1s', 'systemMemoryAvail5s', @@ -134,6 +186,14 @@ function pushVisionLogEnabled() { connection.send(JSON.stringify(msg)); } +function pushRomiLogEnabled() { + var msg = { + type: 'romiLogEnabled', + value: romiLogEnabled.prop('checked') + }; + connection.send(JSON.stringify(msg)); +} + // WebSocket automatic reconnection timer var reconnectTimerId = 0; @@ -152,6 +212,7 @@ function connect() { } displayConnected(); pushVisionLogEnabled(); + pushRomiLogEnabled(); }; connection.onclose = function(evt) { displayDisconnected(); @@ -166,6 +227,10 @@ function connect() { return; } switch (msg.type) { + case 'romiEnable': + $('#romi-nav-item').removeAttr('style'); + $('#wifiSettings').removeAttr('style'); + break; case 'systemStatus': for (var i = 0; i < systemStatusIds.length; i++) { $('#' + systemStatusIds[i]).text(msg[systemStatusIds[i]]); @@ -185,13 +250,47 @@ function connect() { case 'visionLog': visionLog(msg.data); break; + case 'romiStatus': + var elem = $('#romiServiceStatus'); + if (msg.romiServiceStatus) { + elem.text(msg.romiServiceStatus); + } + if (msg.romiServiceEnabled && !elem.hasClass('badge-primary')) { + elem.removeClass('badge-dark').removeClass('badge-secondary').addClass('badge-primary'); + } else if (!msg.romiServiceEnabled && !elem.hasClass('badge-secondary')) { + elem.removeClass('badge-dark').removeClass('badge-primary').addClass('badge-secondary'); + } + break; + case 'romiFirmwareInterface': + $('#romiFirmwareUpdate').prop('disabled', !msg.exists); + break; + case 'romiFirmwareComplete': + $('#romiFirmwareUpdate').button('reset'); + break; + case 'romiLog': + romiLog(msg.data); + break; + case 'romiFirmwareLog': + romiFirmwareLog(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); + $('#wifiMode').val(msg.wifiMode); + $('#wifiChannel').val(msg.wifiChannel); + $('#wifiSsid').val(msg.wifiSsid); + $('#wifiWpa2').val(msg.wifiWpa2); + $('#wifiNetworkApproach').val(msg.wifiNetworkApproach); + $('#wifiNetworkAddress').val(msg.wifiNetworkAddress); + $('#wifiNetworkMask').val(msg.wifiNetworkMask); + $('#wifiNetworkGateway').val(msg.wifiNetworkGateway); + $('#wifiNetworkDNS').val(msg.wifiNetworkDNS); updateNetworkSettingsView(); + updateWifiNetworkSettingsView(); + updateWifiModeView(); break; case 'visionSettings': visionSettingsServer = msg.settings; @@ -271,6 +370,48 @@ $('#visionLogEnabled').change(function() { pushVisionLogEnabled(); }); +$('#romiUp').click(function() { + var msg = { + type: 'romiUp' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#romiDown').click(function() { + var msg = { + type: 'romiDown' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#romiTerm').click(function() { + var msg = { + type: 'romiTerm' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#romiKill').click(function() { + var msg = { + type: 'romiKill' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#romiFirmwareUpdate').click(function() { + $('#romiFirmwareUpdate').button('loading'); + $('#romiFirmwareConsole').removeAttr('style'); + + var msg = { + type: 'romiFirmwareUpdate' + }; + connection.send(JSON.stringify(msg)); +}); + +$('#romiLogEnabled').change(function() { + pushRomiLogEnabled(); +}); + // // Vision console output // @@ -322,6 +463,76 @@ function visionLog(data) { } } +// +// Romi console output +// +var romiConsole = document.getElementById('romiConsole'); +var romiLogEnabled = $('#romiLogEnabled'); +var _linesLimit = 100; + +function romiLog(data) { + if (!romiLogEnabled.prop('checked')) { + return; + } + var wasScrolledBottom = (romiConsole.scrollTop === (romiConsole.scrollHeight - romiConsole.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); + romiConsole.appendChild(div); + + if (romiConsole.children.length > _linesLimit) { + romiConsole.removeChild(romiConsole.children[0]); + } + + if (wasScrolledBottom) { + romiConsole.scrollTop = romiConsole.scrollHeight; + } +} + +var romiFirmwareConsole = document.getElementById('romiFirmwareConsole'); + +function romiFirmwareLog(data) { + var wasScrolledBottom = (romiFirmwareConsole.scrollTop === (romiFirmwareConsole.scrollHeight - romiFirmwareConsole.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); + romiFirmwareConsole.appendChild(div); + + if (wasScrolledBottom) { + romiFirmwareConsole.scrollTop = romiFirmwareConsole.scrollHeight; + } +} + // Show details when appropriate for network approach function updateNetworkSettingsView() { if ($('#networkApproach').val() === "dhcp") { @@ -335,6 +546,34 @@ $('#networkApproach').change(function() { updateNetworkSettingsView(); }); +// Show details when appropriate for wifi network approach +function updateWifiNetworkSettingsView() { + if ($('#wifiNetworkApproach').val() === "dhcp") { + $('#wifiNetworkIpDetails').collapse('hide'); + } else { + $('#wifiNetworkIpDetails').collapse('show'); + } +} + +$('#wifiNetworkApproach').change(function() { + updateWifiNetworkSettingsView(); +}); + +// Show details when appropriate for wifi mode +function updateWifiModeView() { + if ($('#wifiMode').val() === "bridge") { + $('#wifiAccessPointDetails').collapse('hide'); + } else { + $('#wifiAccessPointDetails').collapse('show'); + $('#wifiNetworkApproach').val('static'); + updateWifiNetworkSettingsView(); + } +} + +$('#wifiMode').change(function() { + updateWifiModeView(); +}); + // Network Save button handler $('#networkSave').click(function() { var msg = { @@ -343,7 +582,16 @@ $('#networkSave').click(function() { networkAddress: $('#networkAddress').val(), networkMask: $('#networkMask').val(), networkGateway: $('#networkGateway').val(), - networkDNS: $('#networkDNS').val() + networkDNS: $('#networkDNS').val(), + wifiMode: $('#wifiMode').val(), + wifiChannel: parseInt($('#wifiChannel').val()), + wifiSsid: $('#wifiSsid').val(), + wifiWpa2: $('#wifiWpa2').val(), + wifiNetworkApproach: $('#wifiNetworkApproach').val(), + wifiNetworkAddress: $('#wifiNetworkAddress').val(), + wifiNetworkMask: $('#wifiNetworkMask').val(), + wifiNetworkGateway: $('#wifiNetworkGateway').val(), + wifiNetworkDNS: $('#wifiNetworkDNS').val() }; connection.send(JSON.stringify(msg)); }); @@ -894,6 +1142,8 @@ $('#fileUploadButton').click(function() { // Start with display disconnected and start initial connection attempt displayDisconnected(); updateNetworkSettingsView(); +updateWifiNetworkSettingsView(); +updateWifiModeView(); updateVisionSettingsView(); updateApplicationView(); updateFileUploadView(); diff --git a/deps/tools/configServer/src/resources/index.html b/deps/tools/configServer/src/resources/index.html index 91e1c1a59a..51beb58de8 100644 --- a/deps/tools/configServer/src/resources/index.html +++ b/deps/tools/configServer/src/resources/index.html @@ -8,13 +8,13 @@ - WPILib FRCVision Raspberry PI Configuration + WPILib Raspberry PI Configuration