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'