mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-22 01:11:40 +00:00
De-conflict camera names and hostnames by use of a banner (#1982)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface GeneralSettings {
|
||||
mrCalWorking: boolean;
|
||||
availableModels: ObjectDetectionModelProperties[];
|
||||
supportedBackends: string[];
|
||||
conflictingHostname: boolean;
|
||||
conflictingCameras: string;
|
||||
}
|
||||
|
||||
export interface ObjectDetectionModelProperties {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -128,6 +128,8 @@ public class NetworkConfig {
|
||||
+ setDHCPcommand
|
||||
+ ", shouldManage="
|
||||
+ shouldManage
|
||||
+ ", shouldPublishProto="
|
||||
+ shouldPublishProto
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user