diff --git a/roborioteamnumbersetter/.styleguide b/roborioteamnumbersetter/.styleguide new file mode 100644 index 0000000000..1d448980c2 --- /dev/null +++ b/roborioteamnumbersetter/.styleguide @@ -0,0 +1,28 @@ +cppHeaderFileInclude { + \.h$ + \.inc$ + \.inl$ +} + +cppSrcFileInclude { + \.cpp$ +} + +generatedFileExclude { + src/main/native/resources/ + src/main/native/win/roborioteamnumbersetter.ico + src/main/native/mac/rtns.icns +} + +repoRootNameOverride { + roborioteamnumbersetter +} + +includeOtherLibs { + ^GLFW + ^fmt/ + ^imgui + ^ntcore + ^wpi/ + ^wpigui +} diff --git a/roborioteamnumbersetter/Info.plist b/roborioteamnumbersetter/Info.plist new file mode 100644 index 0000000000..d5fd7c2358 --- /dev/null +++ b/roborioteamnumbersetter/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleName + roboRIOTeamNumberSetter + CFBundleExecutable + roborioteamnumbersetter + CFBundleDisplayName + roboRIOTeamNumberSetter + CFBundleIdentifier + edu.wpi.first.tools.roboRIOTeamNumberSetter + CFBundleIconFile + ov.icns + CFBundlePackageType + APPL + CFBundleSupportedPlatforms + + MacOSX + + CFBundleInfoDictionaryVersion + 6.0 + CFBundleShortVersionString + 2021 + CFBundleVersion + 2021 + LSMinimumSystemVersion + 10.11 + NSHighResolutionCapable + + + diff --git a/roborioteamnumbersetter/build.gradle b/roborioteamnumbersetter/build.gradle new file mode 100644 index 0000000000..2b1a22982a --- /dev/null +++ b/roborioteamnumbersetter/build.gradle @@ -0,0 +1,134 @@ +import org.gradle.internal.os.OperatingSystem + +if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxraspbian') && !project.hasProperty('onlylinuxaarch64bionic')) { + + description = "roboRIO Team Number Setter" + + apply plugin: 'cpp' + apply plugin: 'c' + apply plugin: 'google-test-test-suite' + apply plugin: 'visual-studio' + apply plugin: 'edu.wpi.first.NativeUtils' + + if (OperatingSystem.current().isWindows()) { + apply plugin: 'windows-resources' + } + + ext { + nativeName = 'roborioteamnumbersetter' + } + + apply from: "${rootDir}/shared/resources.gradle" + apply from: "${rootDir}/shared/config.gradle" + + def wpilibVersionFileInput = file("src/main/generate/WPILibVersion.cpp.in") + def wpilibVersionFileOutput = file("$buildDir/generated/main/cpp/WPILibVersion.cpp") + + nativeUtils { + nativeDependencyContainer { + libssh(getNativeDependencyTypeClass('WPIStaticMavenDependency')) { + groupId = "edu.wpi.first.thirdparty.frc2022" + artifactId = "libssh" + headerClassifier = "headers" + sourceClassifier = "sources" + ext = "zip" + version = '0.95-1' + targetPlatforms.addAll(nativeUtils.wpi.platforms.desktopPlatforms) + } + } + } + + task generateCppVersion() { + description = 'Generates the wpilib version class' + group = 'WPILib' + + outputs.file wpilibVersionFileOutput + inputs.file wpilibVersionFileInput + + if (wpilibVersioning.releaseMode) { + outputs.upToDateWhen { false } + } + + // We follow a simple set of checks to determine whether we should generate a new version file: + // 1. If the release type is not development, we generate a new version file + // 2. If there is no generated version number, we generate a new version file + // 3. If there is a generated build number, and the release type is development, then we will + // only generate if the publish task is run. + doLast { + def version = wpilibVersioning.version.get() + println "Writing version ${version} to $wpilibVersionFileOutput" + + if (wpilibVersionFileOutput.exists()) { + wpilibVersionFileOutput.delete() + } + def read = wpilibVersionFileInput.text.replace('${wpilib_version}', version) + wpilibVersionFileOutput.write(read) + } + } + + gradle.taskGraph.addTaskExecutionGraphListener { graph -> + def willPublish = graph.hasTask(publish) + if (willPublish) { + generateCppVersion.outputs.upToDateWhen { false } + } + } + + def generateTask = createGenerateResourcesTask('main', 'RTNS', 'rtns', project) + + project(':').libraryBuild.dependsOn build + tasks.withType(CppCompile) { + dependsOn generateTask + dependsOn generateCppVersion + } + + model { + components { + // By default, a development executable will be generated. This is to help the case of + // testing specific functionality of the library. + "${nativeName}"(NativeExecutableSpec) { + baseName = 'roborioteamnumbersetter' + sources { + cpp { + source { + srcDirs 'src/main/native/cpp', "$buildDir/generated/main/cpp" + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/main/native/include' + } + } + if (OperatingSystem.current().isWindows()) { + rc { + source { + srcDirs 'src/main/native/win' + include '*.rc' + } + } + } + } + binaries.all { + if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) { + it.buildable = false + return + } + it.cppCompiler.define("LIBSSH_STATIC") + lib project: ':glass', library: 'glass', linkage: 'static' + lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' + lib project: ':wpigui', library: 'wpigui', linkage: 'static' + nativeUtils.useRequiredLibrary(it, 'imgui_static', 'libssh') + if (it.targetPlatform.operatingSystem.isWindows()) { + it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib' + it.linker.args << 'ws2_32.lib' << 'advapi32.lib' << 'crypt32.lib' << 'user32.lib' + } else if (it.targetPlatform.operatingSystem.isMacOsX()) { + it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore' + it.linker.args << '-framework' << 'Kerberos' + } else { + it.linker.args << '-lX11' + } + } + } + } + } + + apply from: 'publish.gradle' +} diff --git a/roborioteamnumbersetter/publish.gradle b/roborioteamnumbersetter/publish.gradle new file mode 100644 index 0000000000..103be9f5e6 --- /dev/null +++ b/roborioteamnumbersetter/publish.gradle @@ -0,0 +1,107 @@ +apply plugin: 'maven-publish' + +def baseArtifactId = 'roboRIOTeamNumberSetter' +def artifactGroupId = 'edu.wpi.first.tools' +def zipBaseName = '_GROUP_edu_wpi_first_tools_ID_roboRIOTeamNumberSetter_CLS' + +def outputsFolder = file("$project.buildDir/outputs") + +model { + tasks { + // Create the run task. + $.components.roborioteamnumbersetter.binaries.each { bin -> + if (bin.buildable && bin.name.toLowerCase().contains("debug")) { + Task run = project.tasks.create("run", Exec) { + commandLine bin.tasks.install.runScriptFile.get().asFile.toString() + } + run.dependsOn bin.tasks.install + } + } + } + publishing { + def roboRIOTeamNumberSetterTaskList = [] + $.components.each { component -> + component.binaries.each { binary -> + if (binary in NativeExecutableBinarySpec && binary.component.name.contains("roborioteamnumbersetter")) { + if (binary.buildable && binary.name.contains("Release")) { + // We are now in the binary that we want. + // This is the default application path for the ZIP task. + def applicationPath = binary.executable.file + def icon = file("$project.projectDir/src/main/native/mac/rtns.icns") + + // Create the macOS bundle. + def bundleTask = project.tasks.create("bundleroboRIOTeamNumberSetterOsxApp", Copy) { + description("Creates a macOS application bundle for roboRIO Team Number Setter") + from(file("$project.projectDir/Info.plist")) + into(file("$project.buildDir/outputs/bundles/roboRIOTeamNumberSetter.app/Contents")) + into("MacOS") { with copySpec { from binary.executable.file } } + into("Resources") { with copySpec { from icon } } + + doLast { + if (project.hasProperty("developerID")) { + // Get path to binary. + exec { + workingDir rootDir + def args = [ + "sh", + "-c", + "codesign --force --strict --deep " + + "--timestamp --options=runtime " + + "--verbose -s ${project.findProperty("developerID")} " + + "$project.buildDir/outputs/bundles/roboRIOTeamNumberSetter.app/" + ] + commandLine args + } + } + } + } + + // Reset the application path if we are creating a bundle. + if (binary.targetPlatform.operatingSystem.isMacOsX()) { + applicationPath = file("$project.buildDir/outputs/bundles") + project.build.dependsOn bundleTask + } + + // Create the ZIP. + def task = project.tasks.create("copyroboRIOTeamNumberSetterExecutable", Zip) { + description("Copies the roboRIOTeamNumberSetter executable to the outputs directory.") + destinationDirectory = outputsFolder + + archiveBaseName = '_M_' + zipBaseName + duplicatesStrategy = 'exclude' + classifier = nativeUtils.getPublishClassifier(binary) + + from(licenseFile) { + into '/' + } + + from(applicationPath) + into(nativeUtils.getPlatformPath(binary)) + } + + if (binary.targetPlatform.operatingSystem.isMacOsX()) { + bundleTask.dependsOn binary.tasks.link + task.dependsOn(bundleTask) + } + + task.dependsOn binary.tasks.link + roboRIOTeamNumberSetterTaskList.add(task) + project.build.dependsOn task + project.artifacts { task } + addTaskToCopyAllOutputs(task) + } + } + } + } + + publications { + roborioteamnumbersetter(MavenPublication) { + roboRIOTeamNumberSetterTaskList.each { artifact it } + + artifactId = baseArtifactId + groupId = artifactGroupId + version wpilibVersioning.version.get() + } + } + } +} diff --git a/roborioteamnumbersetter/src/main/generate/WPILibVersion.cpp.in b/roborioteamnumbersetter/src/main/generate/WPILibVersion.cpp.in new file mode 100644 index 0000000000..b0a4490520 --- /dev/null +++ b/roborioteamnumbersetter/src/main/generate/WPILibVersion.cpp.in @@ -0,0 +1,7 @@ +/* + * Autogenerated file! Do not manually edit this file. This version is regenerated + * any time the publish task is run, or when this file is deleted. + */ +const char* GetWPILibVersion() { + return "${wpilib_version}"; +} diff --git a/roborioteamnumbersetter/src/main/native/cpp/App.cpp b/roborioteamnumbersetter/src/main/native/cpp/App.cpp new file mode 100644 index 0000000000..97081460ba --- /dev/null +++ b/roborioteamnumbersetter/src/main/native/cpp/App.cpp @@ -0,0 +1,265 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include +#include + +#ifndef _WIN32 +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "wpi/SmallString.h" +#include "DeploySession.h" +#include "wpi/MulticastServiceResolver.h" + +namespace gui = wpi::gui; + +const char* GetWPILibVersion(); + +#define GLFWAPI extern "C" +GLFWAPI void glfwGetWindowSize(GLFWwindow* window, int* width, int* height); +#define GLFW_DONT_CARE -1 +GLFWAPI void glfwSetWindowSizeLimits(GLFWwindow* window, int minwidth, + int minheight, int maxwidth, + int maxheight); +GLFWAPI void glfwSetWindowSize(GLFWwindow* window, int width, int height); + +struct TeamNumberRefHolder { + explicit TeamNumberRefHolder(glass::Storage& storage) + : teamNumber{storage.GetInt("TeamNumber", 0)} {} + int& teamNumber; +}; + +static std::unique_ptr teamNumberRef; +static std::unordered_map> + foundDevices; +static wpi::Logger logger; +static sysid::DeploySession deploySession{logger}; +static std::unique_ptr multicastResolver; +static glass::MainMenuBar gMainMenu; + +static void FindDevices() { + WPI_EventHandle resolveEvent = multicastResolver->GetEventHandle(); + + bool timedOut = 0; + if (wpi::WaitForObject(resolveEvent, 0, &timedOut)) { + auto allData = multicastResolver->GetData(); + + for (auto&& data : allData) { + // search for MAC + auto macKey = + std::find_if(data.txt.begin(), data.txt.end(), + [](const auto& a) { return a.first == "MAC"; }); + if (macKey != data.txt.end()) { + auto& mac = macKey->second; + foundDevices[mac] = std::make_pair(data.ipv4Address, data.hostName); + } + } + } +} + +static int minWidth = 400; + +static void DisplayGui() { + int& teamNumber = teamNumberRef->teamNumber; + FindDevices(); + + ImGui::GetStyle().WindowRounding = 0; + + // fill entire OS window with this window + ImGui::SetNextWindowPos(ImVec2(0, 0)); + int width, height; + glfwGetWindowSize(gui::GetSystemWindow(), &width, &height); + + ImGui::SetNextWindowSize(ImVec2(width, height)); + + ImGui::Begin("Entries", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_MenuBar | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse); + + ImGui::BeginMenuBar(); + gMainMenu.WorkspaceMenu(); + gui::EmitViewMenu(); + + bool about = false; + if (ImGui::BeginMenu("Info")) { + if (ImGui::MenuItem("About")) { + about = true; + } + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + + if (about) { + ImGui::OpenPopup("About"); + } + if (ImGui::BeginPopupModal("About")) { + ImGui::Text("roboRIO Team Number Setter"); + ImGui::Separator(); + ImGui::Text("v%s", GetWPILibVersion()); + ImGui::Separator(); + ImGui::Text("Has mDNS Implementation: %d", + static_cast(multicastResolver->HasImplementation())); + ImGui::Separator(); + ImGui::Text("Save location: %s", glass::GetStorageDir().c_str()); + if (ImGui::Button("Close")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (multicastResolver->HasImplementation()) { + ImGui::InputInt("Team Number", &teamNumber); + + if (teamNumber < 0) { + teamNumber = 0; + } + + int nameWidth = ImGui::CalcTextSize("roboRIO2-0000-FRC.local. ").x; + int macWidth = ImGui::CalcTextSize("88:88:88:88:88:88").x; + int ipAddressWidth = ImGui::CalcTextSize("255.255.255.255").x; + int setWidth = ImGui::CalcTextSize(" Set Team To 99999 ").x; + int blinkWidth = ImGui::CalcTextSize(" Blink ").x; + int rebootWidth = ImGui::CalcTextSize(" Reboot ").x; + + minWidth = nameWidth + macWidth + ipAddressWidth + setWidth + blinkWidth + + rebootWidth + 100; + + std::string setString = fmt::format("Set team to {}", teamNumber); + + if (ImGui::BeginTable("Table", 6)) { + ImGui::TableSetupColumn( + "Name", + ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed, + nameWidth); + ImGui::TableSetupColumn( + "MAC Address", + ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed, + macWidth); + ImGui::TableSetupColumn( + "IP Address", + ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed, + ipAddressWidth); + ImGui::TableSetupColumn( + "Set", + ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed, + setWidth); + ImGui::TableSetupColumn( + "Blink", + ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed, + blinkWidth); + ImGui::TableSetupColumn( + "Reboot", + ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed, + rebootWidth); + ImGui::TableHeadersRow(); + + for (auto&& i : foundDevices) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", i.second.second.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%s", i.first.c_str()); + ImGui::TableNextColumn(); + struct in_addr in; + in.s_addr = i.second.first; + ImGui::Text("%s", inet_ntoa(in)); + ImGui::TableNextColumn(); + std::future* future = deploySession.GetFuture(i.first); + ImGui::PushID(i.first.c_str()); + if (future) { + ImGui::Button("Deploying"); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + const auto fs = future->wait_for(std::chrono::seconds(0)); + if (fs == std::future_status::ready) { + deploySession.DestroyFuture(i.first); + } + } else { + if (ImGui::Button(setString.c_str())) { + deploySession.ChangeTeamNumber(i.first, teamNumber, i.second.first); + } + ImGui::TableNextColumn(); + if (ImGui::Button("Blink")) { + deploySession.Blink(i.first, i.second.first); + } + ImGui::TableNextColumn(); + if (ImGui::Button("Reboot")) { + deploySession.Reboot(i.first, i.second.first); + } + } + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + ImGui::Columns(6, "Devices"); + + // TODO make columns better + } else { + // Missing MDNS Implementation + ImGui::Text("mDNS Implementation is missing."); +#ifdef _WIN32 + ImGui::Text("Windows 10 1809 or newer is required for this tool"); +#else + ImGui::Text("avahi-client 3 and avahi-core 3 are required for this tool"); + ImGui::Text( + "Install libavahi-client3 and libavahi-core3 from your package " + "manager"); +#endif + } + ImGui::Columns(); + ImGui::End(); + + glfwSetWindowSizeLimits(gui::GetSystemWindow(), minWidth, 200, GLFW_DONT_CARE, + GLFW_DONT_CARE); + if (width < minWidth) { + width = minWidth; + glfwSetWindowSize(gui::GetSystemWindow(), width, height); + } +} + +void Application(std::string_view saveDir) { + gui::CreateContext(); + glass::CreateContext(); + + glass::SetStorageName("roborioteamnumbersetter"); + glass::SetStorageDir(saveDir.empty() ? gui::GetPlatformSaveFileDir() + : saveDir); + + ssh_init(); + + teamNumberRef = + std::make_unique(glass::GetStorageRoot()); + + multicastResolver = + std::make_unique("_ni._tcp"); + multicastResolver->Start(); + + gui::AddLateExecute(DisplayGui); + gui::Initialize("roboRIO Team Number Setter", 600, 400); + + gui::Main(); + multicastResolver->Stop(); + multicastResolver = nullptr; + + glass::DestroyContext(); + gui::DestroyContext(); +} diff --git a/roborioteamnumbersetter/src/main/native/cpp/DeploySession.cpp b/roborioteamnumbersetter/src/main/native/cpp/DeploySession.cpp new file mode 100644 index 0000000000..81c69c4b4d --- /dev/null +++ b/roborioteamnumbersetter/src/main/native/cpp/DeploySession.cpp @@ -0,0 +1,186 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "DeploySession.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "SshSession.h" + +using namespace sysid; + +#ifdef ERROR +#undef ERROR +#endif + +// Macros to make logging easier. +#define INFO(fmt, ...) WPI_INFO(m_logger, fmt, __VA_ARGS__) +#define DEBUG(fmt, ...) WPI_DEBUG(m_logger, fmt, __VA_ARGS__) +#define ERROR(fmt, ...) WPI_DEBUG(m_logger, fmt, __VA_ARGS__) +#define SUCCESS(fmt, ...) WPI_LOG(m_logger, kLogSuccess, fmt, __VA_ARGS__) + +// roboRIO SSH constants. +static constexpr int kPort = 22; +static constexpr std::string_view kUsername = "admin"; +static constexpr std::string_view kPassword = ""; + +std::unordered_map> s_outstanding; + +DeploySession::DeploySession(wpi::Logger& logger) : m_logger{logger} {} + +template +struct SafeDeleter { + explicit SafeDeleter(T d) : deleter(d) {} + ~SafeDeleter() noexcept { deleter(); } + T deleter; +}; + +std::future* DeploySession::GetFuture(const std::string& macAddress) { + auto itr = s_outstanding.find(macAddress); + if (itr == s_outstanding.end()) { + return nullptr; + } + return &itr->second; +} + +void DeploySession::DestroyFuture(const std::string& macAddress) { + s_outstanding.erase(macAddress); +} + +bool DeploySession::ChangeTeamNumber(const std::string& macAddress, + int teamNumber, unsigned int ipAddress) { + auto itr = s_outstanding.find(macAddress); + if (itr != s_outstanding.end()) { + return false; + } + + std::future future = std::async( + std::launch::async, [this, ipAddress, teamNumber, mac = macAddress]() { + // Convert to IP address. + wpi::SmallString<16> ip; + in_addr addr; + addr.s_addr = ipAddress; + wpi::uv::AddrToName(addr, &ip); + DEBUG("Trying to establish SSH connection to {}.", ip); + try { + SshSession session{ip.str(), kPort, kUsername, kPassword, m_logger}; + session.Open(); + DEBUG("SSH connection to {} was successful.", ip); + + SUCCESS("{}", "roboRIO Connected!"); + + try { + session.Execute(fmt::format( + "/usr/local/natinst/bin/nirtcfg " + "--file=/etc/natinst/share/ni-rt.ini --set " + "section=systemsettings,token=host_name,value=roborio-{" + "}-FRC ; sync", + teamNumber)); + } catch (const SshSession::SshException& e) { + ERROR("An exception occurred: {}", e.what()); + throw e; + } + } catch (const SshSession::SshException& e) { + DEBUG("SSH connection to {} failed with {}.", ip, e.what()); + throw e; + } + return 0; + }); + + s_outstanding[macAddress] = std::move(future); + return true; +} + +bool DeploySession::Reboot(const std::string& macAddress, + unsigned int ipAddress) { + auto itr = s_outstanding.find(macAddress); + if (itr != s_outstanding.end()) { + return false; + } + + std::future future = + std::async(std::launch::async, [this, ipAddress, mac = macAddress]() { + // Convert to IP address. + wpi::SmallString<16> ip; + in_addr addr; + addr.s_addr = ipAddress; + wpi::uv::AddrToName(addr, &ip); + DEBUG("Trying to establish SSH connection to {}.", ip); + try { + SshSession session{ip.str(), kPort, kUsername, kPassword, m_logger}; + session.Open(); + DEBUG("SSH connection to {} was successful.", ip); + + SUCCESS("{}", "roboRIO Connected!"); + + try { + session.Execute(fmt::format("sync ; shutdown -r now")); + } catch (const SshSession::SshException& e) { + ERROR("An exception occurred: {}", e.what()); + throw e; + } + } catch (const SshSession::SshException& e) { + DEBUG("SSH connection to {} failed with {}.", ip, e.what()); + throw e; + } + return 0; + }); + + s_outstanding[macAddress] = std::move(future); + return true; +} + +bool DeploySession::Blink(const std::string& macAddress, + unsigned int ipAddress) { + auto itr = s_outstanding.find(macAddress); + if (itr != s_outstanding.end()) { + return false; + } + + std::future future = + std::async(std::launch::async, [this, ipAddress, mac = macAddress]() { + // Convert to IP address. + wpi::SmallString<16> ip; + in_addr addr; + addr.s_addr = ipAddress; + wpi::uv::AddrToName(addr, &ip); + DEBUG("Trying to establish SSH connection to {}.", ip); + try { + SshSession session{ip.str(), kPort, kUsername, kPassword, m_logger}; + session.Open(); + DEBUG("SSH connection to {} was successful.", ip); + + SUCCESS("{}", "roboRIO Connected!"); + + try { + session.Execute(fmt::format( + "for i in 1 2 3 4 5 ; do ` echo 255 > " + "/sys/class/leds/nilrt:wifi:primary/brightness; sleep 0.5; " + "echo 0 > /sys/class/leds/nilrt:wifi:primary/brightness; sleep " + "0.5 ` ; done")); + } catch (const SshSession::SshException& e) { + ERROR("An exception occurred: {}", e.what()); + throw e; + } + } catch (const SshSession::SshException& e) { + DEBUG("SSH connection to {} failed with {}.", ip, e.what()); + throw e; + } + return 0; + }); + + s_outstanding[macAddress] = std::move(future); + return true; +} diff --git a/roborioteamnumbersetter/src/main/native/cpp/DeploySession.h b/roborioteamnumbersetter/src/main/native/cpp/DeploySession.h new file mode 100644 index 0000000000..7cc5bc7853 --- /dev/null +++ b/roborioteamnumbersetter/src/main/native/cpp/DeploySession.h @@ -0,0 +1,70 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace sysid { +// Define an integer for a successful message in the log (shown in green on the +// GUI). +static constexpr unsigned int kLogSuccess = 31; + +/** + * Represents a single deploy session. + * + * An instance of this class must be kept alive in memory until GetStatus() + * returns kDiscoveryFailure or kDone. Otherwise, the deploy will fail! + */ +class DeploySession { + public: + /** Represents the status of the deploy session. */ + enum class Status { kInProgress, kDiscoveryFailure, kDone }; + + /** + * Constructs an instance of the deploy session. + * + * @param team The team number (or an IP address/hostname). + * @param drive Whether the drive program should be deployed to the roboRIO. + * If this is set to false, the mechanism project will be + * deployed. + * @param config The generation configuration file to be sent to the roboRIO. + * @param logger A reference to a logger where log messages should be sent. + */ + explicit DeploySession(wpi::Logger& logger); + + /** + * Executes the deploy. This can be called from any thread. + */ + bool ChangeTeamNumber(const std::string& macAddress, int team, + unsigned int ipAddress); + + bool Blink(const std::string& macAddress, unsigned int ipAddress); + + bool Reboot(const std::string& macAddress, unsigned int ipAddress); + + std::future* GetFuture(const std::string& macAddress); + void DestroyFuture(const std::string& macAddress); + + /** + * Returns the state of the deploy session. + */ + Status GetStatus() const; + + private: + // Logger reference where log messages will be sent. + wpi::Logger& m_logger; + + // The number of hostnames that have completed their resolution/connection + // attempts. + std::atomic_int m_visited = 0; +}; +} // namespace sysid diff --git a/roborioteamnumbersetter/src/main/native/cpp/SshSession.cpp b/roborioteamnumbersetter/src/main/native/cpp/SshSession.cpp new file mode 100644 index 0000000000..9c8920182b --- /dev/null +++ b/roborioteamnumbersetter/src/main/native/cpp/SshSession.cpp @@ -0,0 +1,143 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "SshSession.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +using namespace sysid; + +#define INFO(fmt, ...) WPI_INFO(m_logger, fmt, __VA_ARGS__) + +SshSession::SshSession(std::string_view host, int port, std::string_view user, + std::string_view pass, wpi::Logger& logger) + : m_host{host}, + m_port{port}, + m_username{user}, + m_password{pass}, + m_logger{logger} { + // Create a new SSH session. + m_session = ssh_new(); + if (!m_session) { + throw SshException("The SSH session could not be allocated."); + } + + // Set the host, user, and port. + ssh_options_set(m_session, SSH_OPTIONS_HOST, m_host.c_str()); + ssh_options_set(m_session, SSH_OPTIONS_USER, m_username.c_str()); + ssh_options_set(m_session, SSH_OPTIONS_PORT, &m_port); + + // Set timeout to 3 seconds. + int64_t timeout = 3L; + ssh_options_set(m_session, SSH_OPTIONS_TIMEOUT, &timeout); + + // Set other miscellaneous options. + ssh_options_set(m_session, SSH_OPTIONS_STRICTHOSTKEYCHECK, "no"); +} + +SshSession::~SshSession() { + ssh_disconnect(m_session); + ssh_free(m_session); +} + +void SshSession::Open() { + // Connect to the server. + int rc = ssh_connect(m_session); + if (rc != SSH_OK) { + throw SshException(ssh_get_error(m_session)); + } + + // Authenticate with password. + rc = ssh_userauth_password(m_session, nullptr, m_password.c_str()); + if (rc != SSH_AUTH_SUCCESS) { + throw SshException(ssh_get_error(m_session)); + } +} + +void SshSession::Execute(std::string_view cmd) { + // Allocate a new channel. + ssh_channel channel = ssh_channel_new(m_session); + if (!channel) { + throw SshException(ssh_get_error(m_session)); + } + + // Open the channel. + int rc = ssh_channel_open_session(channel); + if (rc != SSH_OK) { + throw SshException(ssh_get_error(m_session)); + } + + // Execute the command. + std::string command{cmd}; + rc = ssh_channel_request_exec(channel, command.c_str()); + if (rc != SSH_OK) { + ssh_channel_close(channel); + ssh_channel_free(channel); + throw SshException(ssh_get_error(m_session)); + } + INFO("{}", cmd); + + // Log output. + char buf[512]; + int read = ssh_channel_read(channel, buf, sizeof(buf), 0); + if (read != 0) { + INFO("{}", cmd); + } + + // Close and free channel. + ssh_channel_close(channel); + ssh_channel_free(channel); +} + +void SshSession::Put(std::string_view path, std::string_view contents) { + // Allocate the SFTP session. + sftp_session sftp = sftp_new(m_session); + if (!sftp) { + throw SshException(ssh_get_error(m_session)); + } + + // Initialize. + int rc = sftp_init(sftp); + if (rc != SSH_OK) { + sftp_free(sftp); + throw SshException(ssh_get_error(m_session)); + } + + // Copy. + sftp_file file = + sftp_open(sftp, path.data(), O_WRONLY | O_CREAT | O_TRUNC, S_IFMT); + if (!file) { + sftp_free(sftp); + throw SshException(ssh_get_error(m_session)); + } + + // Send 150K at a time. + static constexpr size_t kChunkSize = 150000; + for (size_t i = 0; i < contents.size(); i += kChunkSize) { + size_t len = (std::min)(kChunkSize, contents.size() - i); + size_t written = sftp_write(file, contents.data() + i, len); + if (written != len) { + sftp_close(file); + sftp_free(sftp); + throw SshException(ssh_get_error(m_session)); + } + } + + INFO("[SFTP] Deployed {}!", path); + + // Close file, free memory. + sftp_close(file); + sftp_free(sftp); +} diff --git a/roborioteamnumbersetter/src/main/native/cpp/SshSession.h b/roborioteamnumbersetter/src/main/native/cpp/SshSession.h new file mode 100644 index 0000000000..2e97120f2a --- /dev/null +++ b/roborioteamnumbersetter/src/main/native/cpp/SshSession.h @@ -0,0 +1,81 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include +#include +#include + +#include + +namespace sysid { +/** + * This class is a C++ implementation of the SshSessionController in + * wpilibsuite/deploy-utils. It handles connecting to an SSH server, running + * commands, and transferring files. + */ +class SshSession { + public: + /** + * This is the exception that will be thrown by any of the methods in this + * class if something goes wrong. + */ + class SshException : public std::runtime_error { + public: + explicit SshException(const char* msg) : runtime_error(msg) {} + }; + + /** + * Constructs a new session controller. + * + * @param host The hostname of the server to connect to. + * @param port The port that the sshd server is operating on. + * @param user The username to login as. + * @param pass The password for the given username. + * @param logger A reference to a logger to log messages to. + */ + SshSession(std::string_view host, int port, std::string_view user, + std::string_view pass, wpi::Logger& logger); + + /** + * Destroys the controller object. This also disconnects the session from the + * server. + */ + ~SshSession(); + + /** + * Opens the SSH connection to the given host. + */ + void Open(); + + /** + * Executes a command and logs the output (if there is any). + * + * @param cmd The command to execute on the server. + */ + void Execute(std::string_view cmd); + + /** + * Puts a file on the server using SFTP. + * + * @param path The path to the file to put (on the server). + * @param contents The contents of the file. + */ + void Put(std::string_view path, std::string_view contents); + + private: + ssh_session m_session; + std::string m_host; + + int m_port; + + std::string m_username; + std::string m_password; + + wpi::Logger& m_logger; +}; +} // namespace sysid diff --git a/roborioteamnumbersetter/src/main/native/cpp/main.cpp b/roborioteamnumbersetter/src/main/native/cpp/main.cpp new file mode 100644 index 0000000000..5f1261b00f --- /dev/null +++ b/roborioteamnumbersetter/src/main/native/cpp/main.cpp @@ -0,0 +1,25 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include + +void Application(std::string_view saveDir); + +#ifdef _WIN32 +int __stdcall WinMain(void* hInstance, void* hPrevInstance, char* pCmdLine, + int nCmdShow) { + int argc = __argc; + char** argv = __argv; +#else +int main(int argc, char** argv) { +#endif + std::string_view saveDir; + if (argc == 2) { + saveDir = argv[1]; + } + + Application(saveDir); + + return 0; +} diff --git a/roborioteamnumbersetter/src/main/native/mac/rtns.icns b/roborioteamnumbersetter/src/main/native/mac/rtns.icns new file mode 100644 index 0000000000..4a1d0131b3 Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/mac/rtns.icns differ diff --git a/roborioteamnumbersetter/src/main/native/resources/rtns-128.png b/roborioteamnumbersetter/src/main/native/resources/rtns-128.png new file mode 100644 index 0000000000..5c6ebdd61d Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/resources/rtns-128.png differ diff --git a/roborioteamnumbersetter/src/main/native/resources/rtns-16.png b/roborioteamnumbersetter/src/main/native/resources/rtns-16.png new file mode 100644 index 0000000000..b95cd6a645 Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/resources/rtns-16.png differ diff --git a/roborioteamnumbersetter/src/main/native/resources/rtns-256.png b/roborioteamnumbersetter/src/main/native/resources/rtns-256.png new file mode 100644 index 0000000000..7d07f39143 Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/resources/rtns-256.png differ diff --git a/roborioteamnumbersetter/src/main/native/resources/rtns-32.png b/roborioteamnumbersetter/src/main/native/resources/rtns-32.png new file mode 100644 index 0000000000..10add53108 Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/resources/rtns-32.png differ diff --git a/roborioteamnumbersetter/src/main/native/resources/rtns-48.png b/roborioteamnumbersetter/src/main/native/resources/rtns-48.png new file mode 100644 index 0000000000..bf31571b29 Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/resources/rtns-48.png differ diff --git a/roborioteamnumbersetter/src/main/native/resources/rtns-512.png b/roborioteamnumbersetter/src/main/native/resources/rtns-512.png new file mode 100644 index 0000000000..7a86e77e3e Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/resources/rtns-512.png differ diff --git a/roborioteamnumbersetter/src/main/native/resources/rtns-64.png b/roborioteamnumbersetter/src/main/native/resources/rtns-64.png new file mode 100644 index 0000000000..30819a5aee Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/resources/rtns-64.png differ diff --git a/roborioteamnumbersetter/src/main/native/win/roborioteamnumbersetter.ico b/roborioteamnumbersetter/src/main/native/win/roborioteamnumbersetter.ico new file mode 100644 index 0000000000..c488e68ea0 Binary files /dev/null and b/roborioteamnumbersetter/src/main/native/win/roborioteamnumbersetter.ico differ diff --git a/roborioteamnumbersetter/src/main/native/win/roborioteamnumbersetter.rc b/roborioteamnumbersetter/src/main/native/win/roborioteamnumbersetter.rc new file mode 100644 index 0000000000..5d5d358de5 --- /dev/null +++ b/roborioteamnumbersetter/src/main/native/win/roborioteamnumbersetter.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "roborioteamnumbersetter.ico" diff --git a/settings.gradle b/settings.gradle index dc3be28b6c..2d62f1000f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,7 @@ include 'crossConnIntegrationTests' include 'fieldImages' include 'glass' include 'outlineviewer' +include 'roborioteamnumbersetter' include 'simulation:gz_msgs' include 'simulation:frc_gazebo_plugins' include 'simulation:halsim_gazebo'