Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions photon-client/src/stores/settings/GeneralSettingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export const useSettingsStore = defineStore("settings", {
hardwarePlatform: undefined,
mrCalWorking: true,
availableModels: [],
supportedBackends: []
supportedBackends: [],
conflictingHostname: false,
conflictingCameras: ""
},
network: {
ntServerAddress: "",
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions photon-client/src/types/SettingTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface GeneralSettings {
mrCalWorking: boolean;
availableModels: ObjectDetectionModelProperties[];
supportedBackends: string[];
conflictingHostname: boolean;
conflictingCameras: string;
}

export interface ObjectDetectionModelProperties {
Expand Down
30 changes: 21 additions & 9 deletions photon-client/src/views/DashboardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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[] => {
Expand Down Expand Up @@ -50,24 +51,35 @@ 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"
v-model="arducamWarningShown"
rounded
color="error"
dark
class="mb-3"
icon="mdi-alert-circle-outline"
>
<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="conflictingHostnameShown" rounded bg-color="error" color="error" dark class="mb-3" icon="mdi-alert-circle-outline">
<span
>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>
<v-col cols="12" class="pb-3 pr-lg-3" lg="8" align-self="stretch">
<CamerasCard v-model="cameraViewType" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ public String toString() {
+ setDHCPcommand
+ ", shouldManage="
+ shouldManage
+ ", shouldPublishProto="
+ shouldPublishProto
+ "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@

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.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;
Expand All @@ -37,6 +42,7 @@
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;
Expand All @@ -48,9 +54,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);

MultiSubscriber sub =
new MultiSubscriber(ntInstance, new String[] {kRootTableName + "/" + kCoprocTableName + "/"});

// Creating the alert up here since it should be persistent
Alert conflictAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);

public boolean conflictingHostname = false;
public String conflictingCameras = "";

private boolean m_isRetryingConnection = false;

private StringSubscriber m_fieldLayoutSubscriber =
Expand All @@ -70,14 +86,19 @@ private 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;
Expand Down Expand Up @@ -205,6 +226,97 @@ private void broadcastVersion() {
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()) {
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
// Check for camera name conflicts using streams
for (String cameraName : cameraNames) {
if (java.util.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();
Expand Down Expand Up @@ -244,11 +356,10 @@ private void setServerMode() {
broadcastVersion();
}

// 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)
// 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)
private void ntTick() {
if (!ntInstance.isConnected()
&& !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ public 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;
this.availableModels = availableModels;
this.supportedBackends = supportedBackends;
this.hardwareModel = hardwareModel;
this.hardwarePlatform = hardwarePlatform;
this.conflictingHostname = conflictingHostname;
this.conflictingCameras = conflictingCameras;
}

public String version;
Expand All @@ -45,4 +49,6 @@ public UIGeneralSettings(
public List<String> supportedBackends;
public String hardwareModel;
public String hardwarePlatform;
public boolean conflictingHostname;
public String conflictingCameras;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -59,7 +60,9 @@ public static UIPhotonConfiguration programStateToUi(PhotonConfiguration c) {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,36 @@ private void setHostname(String hostname) {
}
}

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();
for (byte b : mac) {
sb.append(String.format("%02X-", b));
}
// remove the last colon
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;

Expand Down
Loading