De-conflict camera names and hostnames by use of a banner (#1982)

This commit is contained in:
Sam Freund
2025-07-04 16:43:17 -05:00
committed by GitHub
parent 46ac1baa69
commit d88ea4a75d
8 changed files with 202 additions and 12 deletions

View File

@@ -28,7 +28,9 @@ export const useSettingsStore = defineStore("settings", {
hardwarePlatform: undefined,
mrCalWorking: true,
availableModels: [],
supportedBackends: []
supportedBackends: [],
conflictingHostname: false,
conflictingCameras: ""
},
network: {
ntServerAddress: "",
@@ -107,7 +109,9 @@ export const useSettingsStore = defineStore("settings", {
gpuAcceleration: data.general.gpuAcceleration || undefined,
mrCalWorking: data.general.mrCalWorking,
availableModels: data.general.availableModels || undefined,
supportedBackends: data.general.supportedBackends || []
supportedBackends: data.general.supportedBackends || [],
conflictingHostname: data.general.conflictingHostname || false,
conflictingCameras: data.general.conflictingCameras || ""
};
this.lighting = data.lighting;
this.network = data.networkSettings;

View File

@@ -10,6 +10,8 @@ export interface GeneralSettings {
mrCalWorking: boolean;
availableModels: ObjectDetectionModelProperties[];
supportedBackends: string[];
conflictingHostname: boolean;
conflictingCameras: string;
}
export interface ObjectDetectionModelProperties {

View File

@@ -6,6 +6,7 @@ import StreamConfigCard from "@/components/dashboard/StreamConfigCard.vue";
import PipelineConfigCard from "@/components/dashboard/ConfigOptions.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const cameraViewType = computed<number[]>({
get: (): number[] => {
@@ -50,22 +51,49 @@ const arducamWarningShown = computed<boolean>(() => {
);
});
const conflictingHostnameShown = computed<boolean>(() => {
return useSettingsStore().general.conflictingHostname;
});
const conflictingCameraShown = computed<boolean>(() => {
return useSettingsStore().general.conflictingCameras.length > 0;
});
const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration);
</script>
<template>
<v-container class="pa-3" fluid>
<v-banner v-if="arducamWarningShown" rounded color="error" dark class="mb-3" icon="mdi-alert-circle-outline">
<span
>Arducam Camera Detected! Please configure the camera model in the <a href="#/cameras">Cameras tab</a>!
</span>
</v-banner>
<v-banner
v-if="arducamWarningShown"
v-model="arducamWarningShown"
v-if="conflictingHostnameShown"
rounded
bg-color="error"
color="error"
dark
class="mb-3"
icon="mdi-alert-circle-outline"
>
<span
>Arducam Camera Detected! Please configure the camera model in the <a href="#/cameras">Cameras tab</a>!
>Conflicting Hostname Detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
</span>
</v-banner>
<v-banner
v-if="conflictingCameraShown"
rounded
bg-color="error"
color="error"
dark
class="mb-3"
icon="mdi-alert-circle-outline"
>
<span
>Conflicting Camera Name(s) Detected! Please change the name(s) of
{{ useSettingsStore().general.conflictingCameras }}!
</span>
</v-banner>
<v-row no-gutters>

View File

@@ -128,6 +128,8 @@ public class NetworkConfig {
+ setDHCPcommand
+ ", shouldManage="
+ shouldManage
+ ", shouldPublishProto="
+ shouldPublishProto
+ "]";
}
}

View File

@@ -19,15 +19,21 @@ package org.photonvision.common.dataflow.networktables;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.networktables.LogMessage;
import edu.wpi.first.networktables.MultiSubscriber;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableEvent;
import edu.wpi.first.networktables.NetworkTableEvent.Kind;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.StringSubscriber;
import edu.wpi.first.wpilibj.Alert;
import edu.wpi.first.wpilibj.Alert.AlertType;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
import java.io.IOException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeService;
@@ -37,6 +43,7 @@ import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.scripting.ScriptEventType;
import org.photonvision.common.scripting.ScriptManager;
import org.photonvision.common.util.TimedTaskManager;
@@ -48,9 +55,19 @@ public class NetworkTablesManager {
private final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
private final String kRootTableName = "/photonvision";
private final String kCoprocTableName = "coprocessors";
private final String kFieldLayoutName = "apriltag_field_layout";
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
private final MultiSubscriber sub =
new MultiSubscriber(ntInstance, new String[] {kRootTableName + "/" + kCoprocTableName + "/"});
// Creating the alert up here since it should be persistent
private final Alert conflictAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
public boolean conflictingHostname = false;
public String conflictingCameras = "";
private boolean m_isRetryingConnection = false;
private StringSubscriber m_fieldLayoutSubscriber =
@@ -70,14 +87,19 @@ public class NetworkTablesManager {
ntDriverStation = new NTDriverStation(this.getNTInst());
// Get the UI state in sync with the backend. NT should fire a callback when it first connects
// to the robot
// This should start as false, since we don't know if there's a conflict yet
conflictAlert.set(false);
// Get the UI state in sync with the backend. NT should fire a callback when it
// first connects to the robot
broadcastConnectedStatus();
}
public void registerTimedTasks() {
m_timeSync.start();
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
TimedTaskManager.getInstance()
.addTask("CheckHostnameAndCameraNames", this::checkHostnameAndCameraNames, 10000);
}
private static NetworkTablesManager INSTANCE;
@@ -205,6 +227,101 @@ public class NetworkTablesManager {
kRootTable.getEntry("buildDate").setString(PhotonVersion.buildDate);
}
/**
* Publishes the hostname and camera names to a table using the MAC address as a key. Then checks
* for conflicts of hostname or camera names across other coprocessors that are also publishing to
* this table.
*/
private void checkHostnameAndCameraNames() {
String MAC = NetworkManager.getInstance().getMACAddress();
if (MAC == null || MAC.isEmpty()) {
logger.error("Cannot check hostname and camera names, MAC address is not set!");
return;
}
String hostname = ConfigManager.getInstance().getConfig().getNetworkConfig().hostname;
if (hostname == null || hostname.isEmpty()) {
logger.error("Cannot check hostname and camera names, hostname is not set!");
return;
}
HashMap<String, CameraConfiguration> cameraConfigs =
ConfigManager.getInstance().getConfig().getCameraConfigurations();
String[] cameraNames =
cameraConfigs.entrySet().stream()
.map(entry -> entry.getValue().nickname)
.toArray(String[]::new);
// Create a subtable under the photonvision root table
NetworkTable coprocTable = kRootTable.getSubTable(kCoprocTableName);
// Create a subtable for this coprocessor using its MAC address
NetworkTable macTable = coprocTable.getSubTable(MAC);
// Publish the hostname and camera names
macTable.getEntry("hostname").setString(hostname);
macTable.getEntry("cameraNames").setStringArray(cameraNames);
logger.debug("Published hostname and camera names to NT under MAC: " + MAC);
boolean conflictingHostname = false;
StringBuilder conflictingCameras = new StringBuilder();
// Check for conflicts with other coprocessors
for (String key : coprocTable.getSubTables()) {
// Check that key is formatted like a MAC address
if (!key.matches("([0-9A-F]{2}-){5}[0-9A-F]{2}")) {
logger.warn("Skipping non-MAC key in conflict detection: " + key);
continue;
}
if (!key.equals(MAC)) { // Skip our own entry
NetworkTable otherCoprocTable = coprocTable.getSubTable(key);
String otherHostname = otherCoprocTable.getEntry("hostname").getString("");
String[] otherCameraNames =
otherCoprocTable.getEntry("cameraNames").getStringArray(new String[0]);
// Check for hostname conflicts
if (otherHostname.equals(hostname)) {
logger.warn("Hostname conflict detected with coprocessor " + key + ": " + hostname);
conflictingHostname = true;
}
// Check for camera name conflicts
for (String cameraName : cameraNames) {
if (Arrays.stream(otherCameraNames).anyMatch(otherName -> otherName.equals(cameraName))) {
logger.warn("Camera name conflict detected: " + cameraName);
conflictingCameras.append(
conflictingCameras.isEmpty() ? cameraName : ", " + cameraName);
}
}
}
}
boolean hasChanged =
this.conflictingHostname != conflictingHostname
|| !this.conflictingCameras.equals(conflictingCameras.toString());
// Publish the conflict status
if (hasChanged) {
DataChangeService.getInstance()
.publishEvent(
new OutgoingUIEvent<>(
"fullsettings",
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
}
conflictAlert.setText(
conflictingHostname
? "Hostname conflict detected for " + hostname + "!"
: ""
+ (conflictingCameras.isEmpty()
? ""
: " Camera name conflict detected: " + conflictingCameras.toString() + "!"));
conflictAlert.set(conflictingHostname || !conflictingCameras.isEmpty());
SmartDashboard.updateValues();
this.conflictingHostname = conflictingHostname;
this.conflictingCameras = conflictingCameras.toString();
}
public void setConfig(NetworkConfig config) {
if (config.runNTServer) {
setServerMode();
@@ -246,9 +363,8 @@ public class NetworkTablesManager {
// So it seems like if Photon starts before the robot NT server does, and both aren't static IP,
// it'll never connect. This hack works around it by restarting the client/server while the nt
// instance
// isn't connected, same as clicking the save button in the settings menu (or restarting the
// service)
// instance isn't connected, same as clicking the save button in the settings menu (or restarting
// the service)
private void ntTick() {
if (!ntInstance.isConnected()
&& !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) {

View File

@@ -28,7 +28,9 @@ public class UIGeneralSettings {
NeuralNetworkPropertyManager.ModelProperties[] availableModels,
List<String> supportedBackends,
String hardwareModel,
String hardwarePlatform) {
String hardwarePlatform,
boolean conflictingHostname,
String conflictingCameras) {
this.version = version;
this.gpuAcceleration = gpuAcceleration;
this.mrCalWorking = mrCalWorking;
@@ -36,6 +38,8 @@ public class UIGeneralSettings {
this.supportedBackends = supportedBackends;
this.hardwareModel = hardwareModel;
this.hardwarePlatform = hardwarePlatform;
this.conflictingHostname = conflictingHostname;
this.conflictingCameras = conflictingCameras;
}
public String version;
@@ -45,4 +49,6 @@ public class UIGeneralSettings {
public List<String> supportedBackends;
public String hardwareModel;
public String hardwarePlatform;
public boolean conflictingHostname;
public String conflictingCameras;
}

View File

@@ -21,6 +21,7 @@ import java.util.List;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.configuration.PhotonConfiguration;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.networking.NetworkUtils;
@@ -59,7 +60,9 @@ public class UIPhotonConfiguration {
c.getHardwareConfig().deviceName().isEmpty()
? Platform.getHardwareModel()
: c.getHardwareConfig().deviceName(),
Platform.getPlatformName()),
Platform.getPlatformName(),
NetworkTablesManager.getInstance().conflictingHostname,
NetworkTablesManager.getInstance().conflictingCameras),
c.getApriltagFieldLayout()),
VisionSourceManager.getInstance().getVisionModules().stream()
.map(VisionModule::toUICameraConfig)

View File

@@ -179,6 +179,35 @@ public class NetworkManager {
}
}
public String getMACAddress() {
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
if (config.networkManagerIface == null || config.networkManagerIface.isBlank()) {
logger.error("No network interface configured, cannot get MAC address!");
return "";
}
try {
NetworkInterface iFace = NetworkInterface.getByName(config.networkManagerIface);
if (iFace == null) {
logger.error("Network interface " + config.networkManagerIface + " not found!");
return "";
}
byte[] mac = iFace.getHardwareAddress();
if (mac == null) {
logger.error("No MAC address found for " + config.networkManagerIface);
return "";
}
StringBuilder sb = new StringBuilder(17);
for (byte b : mac) {
sb.append(String.format("%02X-", b));
}
sb.setLength(sb.length() - 1);
return sb.toString();
} catch (Exception e) {
logger.error("Error getting MAC address for " + config.networkManagerIface, e);
return "";
}
}
private void setConnectionDHCP(NetworkConfig config) {
String connName = "dhcp-" + config.networkManagerIface;