Skip to content

Commit d88ea4a

Browse files
authored
De-conflict camera names and hostnames by use of a banner (#1982)
1 parent 46ac1ba commit d88ea4a

File tree

8 files changed

+202
-12
lines changed

8 files changed

+202
-12
lines changed

photon-client/src/stores/settings/GeneralSettingsStore.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export const useSettingsStore = defineStore("settings", {
2828
hardwarePlatform: undefined,
2929
mrCalWorking: true,
3030
availableModels: [],
31-
supportedBackends: []
31+
supportedBackends: [],
32+
conflictingHostname: false,
33+
conflictingCameras: ""
3234
},
3335
network: {
3436
ntServerAddress: "",
@@ -107,7 +109,9 @@ export const useSettingsStore = defineStore("settings", {
107109
gpuAcceleration: data.general.gpuAcceleration || undefined,
108110
mrCalWorking: data.general.mrCalWorking,
109111
availableModels: data.general.availableModels || undefined,
110-
supportedBackends: data.general.supportedBackends || []
112+
supportedBackends: data.general.supportedBackends || [],
113+
conflictingHostname: data.general.conflictingHostname || false,
114+
conflictingCameras: data.general.conflictingCameras || ""
111115
};
112116
this.lighting = data.lighting;
113117
this.network = data.networkSettings;

photon-client/src/types/SettingTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export interface GeneralSettings {
1010
mrCalWorking: boolean;
1111
availableModels: ObjectDetectionModelProperties[];
1212
supportedBackends: string[];
13+
conflictingHostname: boolean;
14+
conflictingCameras: string;
1315
}
1416

1517
export interface ObjectDetectionModelProperties {

photon-client/src/views/DashboardView.vue

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import StreamConfigCard from "@/components/dashboard/StreamConfigCard.vue";
66
import PipelineConfigCard from "@/components/dashboard/ConfigOptions.vue";
77
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
88
import { useStateStore } from "@/stores/StateStore";
9+
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
910
1011
const cameraViewType = computed<number[]>({
1112
get: (): number[] => {
@@ -50,22 +51,49 @@ const arducamWarningShown = computed<boolean>(() => {
5051
);
5152
});
5253
54+
const conflictingHostnameShown = computed<boolean>(() => {
55+
return useSettingsStore().general.conflictingHostname;
56+
});
57+
58+
const conflictingCameraShown = computed<boolean>(() => {
59+
return useSettingsStore().general.conflictingCameras.length > 0;
60+
});
61+
5362
const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration);
5463
</script>
5564

5665
<template>
5766
<v-container class="pa-3" fluid>
67+
<v-banner v-if="arducamWarningShown" rounded color="error" dark class="mb-3" icon="mdi-alert-circle-outline">
68+
<span
69+
>Arducam Camera Detected! Please configure the camera model in the <a href="#/cameras">Cameras tab</a>!
70+
</span>
71+
</v-banner>
5872
<v-banner
59-
v-if="arducamWarningShown"
60-
v-model="arducamWarningShown"
73+
v-if="conflictingHostnameShown"
6174
rounded
75+
bg-color="error"
6276
color="error"
6377
dark
6478
class="mb-3"
6579
icon="mdi-alert-circle-outline"
6680
>
6781
<span
68-
>Arducam Camera Detected! Please configure the camera model in the <a href="#/cameras">Cameras tab</a>!
82+
>Conflicting Hostname Detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
83+
</span>
84+
</v-banner>
85+
<v-banner
86+
v-if="conflictingCameraShown"
87+
rounded
88+
bg-color="error"
89+
color="error"
90+
dark
91+
class="mb-3"
92+
icon="mdi-alert-circle-outline"
93+
>
94+
<span
95+
>Conflicting Camera Name(s) Detected! Please change the name(s) of
96+
{{ useSettingsStore().general.conflictingCameras }}!
6997
</span>
7098
</v-banner>
7199
<v-row no-gutters>

photon-core/src/main/java/org/photonvision/common/configuration/NetworkConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ public String toString() {
128128
+ setDHCPcommand
129129
+ ", shouldManage="
130130
+ shouldManage
131+
+ ", shouldPublishProto="
132+
+ shouldPublishProto
131133
+ "]";
132134
}
133135
}

photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NetworkTablesManager.java

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,21 @@
1919

2020
import edu.wpi.first.apriltag.AprilTagFieldLayout;
2121
import edu.wpi.first.networktables.LogMessage;
22+
import edu.wpi.first.networktables.MultiSubscriber;
2223
import edu.wpi.first.networktables.NetworkTable;
2324
import edu.wpi.first.networktables.NetworkTableEvent;
2425
import edu.wpi.first.networktables.NetworkTableEvent.Kind;
2526
import edu.wpi.first.networktables.NetworkTableInstance;
2627
import edu.wpi.first.networktables.StringSubscriber;
28+
import edu.wpi.first.wpilibj.Alert;
29+
import edu.wpi.first.wpilibj.Alert.AlertType;
30+
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
2731
import java.io.IOException;
32+
import java.util.Arrays;
2833
import java.util.EnumSet;
2934
import java.util.HashMap;
3035
import org.photonvision.PhotonVersion;
36+
import org.photonvision.common.configuration.CameraConfiguration;
3137
import org.photonvision.common.configuration.ConfigManager;
3238
import org.photonvision.common.configuration.NetworkConfig;
3339
import org.photonvision.common.dataflow.DataChangeService;
@@ -37,6 +43,7 @@
3743
import org.photonvision.common.logging.LogGroup;
3844
import org.photonvision.common.logging.LogLevel;
3945
import org.photonvision.common.logging.Logger;
46+
import org.photonvision.common.networking.NetworkManager;
4047
import org.photonvision.common.scripting.ScriptEventType;
4148
import org.photonvision.common.scripting.ScriptManager;
4249
import org.photonvision.common.util.TimedTaskManager;
@@ -48,9 +55,19 @@ public class NetworkTablesManager {
4855

4956
private final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
5057
private final String kRootTableName = "/photonvision";
58+
private final String kCoprocTableName = "coprocessors";
5159
private final String kFieldLayoutName = "apriltag_field_layout";
5260
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
5361

62+
private final MultiSubscriber sub =
63+
new MultiSubscriber(ntInstance, new String[] {kRootTableName + "/" + kCoprocTableName + "/"});
64+
65+
// Creating the alert up here since it should be persistent
66+
private final Alert conflictAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
67+
68+
public boolean conflictingHostname = false;
69+
public String conflictingCameras = "";
70+
5471
private boolean m_isRetryingConnection = false;
5572

5673
private StringSubscriber m_fieldLayoutSubscriber =
@@ -70,14 +87,19 @@ private NetworkTablesManager() {
7087

7188
ntDriverStation = new NTDriverStation(this.getNTInst());
7289

73-
// Get the UI state in sync with the backend. NT should fire a callback when it first connects
74-
// to the robot
90+
// This should start as false, since we don't know if there's a conflict yet
91+
conflictAlert.set(false);
92+
93+
// Get the UI state in sync with the backend. NT should fire a callback when it
94+
// first connects to the robot
7595
broadcastConnectedStatus();
7696
}
7797

7898
public void registerTimedTasks() {
7999
m_timeSync.start();
80100
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
101+
TimedTaskManager.getInstance()
102+
.addTask("CheckHostnameAndCameraNames", this::checkHostnameAndCameraNames, 10000);
81103
}
82104

83105
private static NetworkTablesManager INSTANCE;
@@ -205,6 +227,101 @@ private void broadcastVersion() {
205227
kRootTable.getEntry("buildDate").setString(PhotonVersion.buildDate);
206228
}
207229

230+
/**
231+
* Publishes the hostname and camera names to a table using the MAC address as a key. Then checks
232+
* for conflicts of hostname or camera names across other coprocessors that are also publishing to
233+
* this table.
234+
*/
235+
private void checkHostnameAndCameraNames() {
236+
String MAC = NetworkManager.getInstance().getMACAddress();
237+
if (MAC == null || MAC.isEmpty()) {
238+
logger.error("Cannot check hostname and camera names, MAC address is not set!");
239+
return;
240+
}
241+
242+
String hostname = ConfigManager.getInstance().getConfig().getNetworkConfig().hostname;
243+
if (hostname == null || hostname.isEmpty()) {
244+
logger.error("Cannot check hostname and camera names, hostname is not set!");
245+
return;
246+
}
247+
248+
HashMap<String, CameraConfiguration> cameraConfigs =
249+
ConfigManager.getInstance().getConfig().getCameraConfigurations();
250+
String[] cameraNames =
251+
cameraConfigs.entrySet().stream()
252+
.map(entry -> entry.getValue().nickname)
253+
.toArray(String[]::new);
254+
255+
// Create a subtable under the photonvision root table
256+
NetworkTable coprocTable = kRootTable.getSubTable(kCoprocTableName);
257+
258+
// Create a subtable for this coprocessor using its MAC address
259+
NetworkTable macTable = coprocTable.getSubTable(MAC);
260+
261+
// Publish the hostname and camera names
262+
macTable.getEntry("hostname").setString(hostname);
263+
macTable.getEntry("cameraNames").setStringArray(cameraNames);
264+
logger.debug("Published hostname and camera names to NT under MAC: " + MAC);
265+
266+
boolean conflictingHostname = false;
267+
StringBuilder conflictingCameras = new StringBuilder();
268+
269+
// Check for conflicts with other coprocessors
270+
for (String key : coprocTable.getSubTables()) {
271+
// Check that key is formatted like a MAC address
272+
if (!key.matches("([0-9A-F]{2}-){5}[0-9A-F]{2}")) {
273+
logger.warn("Skipping non-MAC key in conflict detection: " + key);
274+
continue;
275+
}
276+
277+
if (!key.equals(MAC)) { // Skip our own entry
278+
NetworkTable otherCoprocTable = coprocTable.getSubTable(key);
279+
String otherHostname = otherCoprocTable.getEntry("hostname").getString("");
280+
String[] otherCameraNames =
281+
otherCoprocTable.getEntry("cameraNames").getStringArray(new String[0]);
282+
// Check for hostname conflicts
283+
if (otherHostname.equals(hostname)) {
284+
logger.warn("Hostname conflict detected with coprocessor " + key + ": " + hostname);
285+
conflictingHostname = true;
286+
}
287+
288+
// Check for camera name conflicts
289+
for (String cameraName : cameraNames) {
290+
if (Arrays.stream(otherCameraNames).anyMatch(otherName -> otherName.equals(cameraName))) {
291+
logger.warn("Camera name conflict detected: " + cameraName);
292+
conflictingCameras.append(
293+
conflictingCameras.isEmpty() ? cameraName : ", " + cameraName);
294+
}
295+
}
296+
}
297+
}
298+
299+
boolean hasChanged =
300+
this.conflictingHostname != conflictingHostname
301+
|| !this.conflictingCameras.equals(conflictingCameras.toString());
302+
303+
// Publish the conflict status
304+
if (hasChanged) {
305+
DataChangeService.getInstance()
306+
.publishEvent(
307+
new OutgoingUIEvent<>(
308+
"fullsettings",
309+
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
310+
}
311+
312+
conflictAlert.setText(
313+
conflictingHostname
314+
? "Hostname conflict detected for " + hostname + "!"
315+
: ""
316+
+ (conflictingCameras.isEmpty()
317+
? ""
318+
: " Camera name conflict detected: " + conflictingCameras.toString() + "!"));
319+
conflictAlert.set(conflictingHostname || !conflictingCameras.isEmpty());
320+
SmartDashboard.updateValues();
321+
this.conflictingHostname = conflictingHostname;
322+
this.conflictingCameras = conflictingCameras.toString();
323+
}
324+
208325
public void setConfig(NetworkConfig config) {
209326
if (config.runNTServer) {
210327
setServerMode();
@@ -246,9 +363,8 @@ private void setServerMode() {
246363

247364
// So it seems like if Photon starts before the robot NT server does, and both aren't static IP,
248365
// it'll never connect. This hack works around it by restarting the client/server while the nt
249-
// instance
250-
// isn't connected, same as clicking the save button in the settings menu (or restarting the
251-
// service)
366+
// instance isn't connected, same as clicking the save button in the settings menu (or restarting
367+
// the service)
252368
private void ntTick() {
253369
if (!ntInstance.isConnected()
254370
&& !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) {

photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIGeneralSettings.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,18 @@ public UIGeneralSettings(
2828
NeuralNetworkPropertyManager.ModelProperties[] availableModels,
2929
List<String> supportedBackends,
3030
String hardwareModel,
31-
String hardwarePlatform) {
31+
String hardwarePlatform,
32+
boolean conflictingHostname,
33+
String conflictingCameras) {
3234
this.version = version;
3335
this.gpuAcceleration = gpuAcceleration;
3436
this.mrCalWorking = mrCalWorking;
3537
this.availableModels = availableModels;
3638
this.supportedBackends = supportedBackends;
3739
this.hardwareModel = hardwareModel;
3840
this.hardwarePlatform = hardwarePlatform;
41+
this.conflictingHostname = conflictingHostname;
42+
this.conflictingCameras = conflictingCameras;
3943
}
4044

4145
public String version;
@@ -45,4 +49,6 @@ public UIGeneralSettings(
4549
public List<String> supportedBackends;
4650
public String hardwareModel;
4751
public String hardwarePlatform;
52+
public boolean conflictingHostname;
53+
public String conflictingCameras;
4854
}

photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIPhotonConfiguration.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.photonvision.PhotonVersion;
2222
import org.photonvision.common.configuration.NeuralNetworkModelManager;
2323
import org.photonvision.common.configuration.PhotonConfiguration;
24+
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
2425
import org.photonvision.common.hardware.Platform;
2526
import org.photonvision.common.networking.NetworkManager;
2627
import org.photonvision.common.networking.NetworkUtils;
@@ -59,7 +60,9 @@ public static UIPhotonConfiguration programStateToUi(PhotonConfiguration c) {
5960
c.getHardwareConfig().deviceName().isEmpty()
6061
? Platform.getHardwareModel()
6162
: c.getHardwareConfig().deviceName(),
62-
Platform.getPlatformName()),
63+
Platform.getPlatformName(),
64+
NetworkTablesManager.getInstance().conflictingHostname,
65+
NetworkTablesManager.getInstance().conflictingCameras),
6366
c.getApriltagFieldLayout()),
6467
VisionSourceManager.getInstance().getVisionModules().stream()
6568
.map(VisionModule::toUICameraConfig)

photon-core/src/main/java/org/photonvision/common/networking/NetworkManager.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,35 @@ private void setHostname(String hostname) {
179179
}
180180
}
181181

182+
public String getMACAddress() {
183+
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
184+
if (config.networkManagerIface == null || config.networkManagerIface.isBlank()) {
185+
logger.error("No network interface configured, cannot get MAC address!");
186+
return "";
187+
}
188+
try {
189+
NetworkInterface iFace = NetworkInterface.getByName(config.networkManagerIface);
190+
if (iFace == null) {
191+
logger.error("Network interface " + config.networkManagerIface + " not found!");
192+
return "";
193+
}
194+
byte[] mac = iFace.getHardwareAddress();
195+
if (mac == null) {
196+
logger.error("No MAC address found for " + config.networkManagerIface);
197+
return "";
198+
}
199+
StringBuilder sb = new StringBuilder(17);
200+
for (byte b : mac) {
201+
sb.append(String.format("%02X-", b));
202+
}
203+
sb.setLength(sb.length() - 1);
204+
return sb.toString();
205+
} catch (Exception e) {
206+
logger.error("Error getting MAC address for " + config.networkManagerIface, e);
207+
return "";
208+
}
209+
}
210+
182211
private void setConnectionDHCP(NetworkConfig config) {
183212
String connName = "dhcp-" + config.networkManagerIface;
184213

0 commit comments

Comments
 (0)