diff --git a/photon-client/src/components/settings/DeviceCard.vue b/photon-client/src/components/settings/DeviceCard.vue index e1bd5c22af..ac0f925fdc 100644 --- a/photon-client/src/components/settings/DeviceCard.vue +++ b/photon-client/src/components/settings/DeviceCard.vue @@ -30,11 +30,67 @@ const offlineUpdate = ref(); const openOfflineUpdatePrompt = () => { offlineUpdate.value.click(); }; -const handleOfflineUpdate = async () => { + +const offlineUpdateRegex = new RegExp("photonvision-((?:dev-)?v[\\w.-]+)-((?:linux|win|mac)\\w+)\\.jar"); +const majorVersionRegex = new RegExp("(?:dev-)?(\\d+)\\.\\d+\\.\\d+"); + +const offlineUpdateDialog = ref({ show: false, confirmString: "" }); + +const handleOfflineUpdateRequest = async () => { const files = offlineUpdate.value.files; if (files.length === 0) return; + + const match = files[0].name.match(offlineUpdateRegex); + if (!match) { + useStateStore().showSnackbarMessage({ + message: "Selected file does not match expected naming convention.", + color: "error" + }); + return; + } + + const version = match[1] as string; + const arch = match[2] as string; + + const currentVersion = useSettingsStore().general.imageVersion; + const currentArch = useSettingsStore().general.wpilibArch; + + const versionMajor = version.match(majorVersionRegex)?.[1]; + const currentVersionMajor = currentVersion?.match(majorVersionRegex)?.[1]; + + const versionMatch = currentVersion ? versionMajor === currentVersionMajor : false; + const dev = version.includes("dev"); + + if (currentArch && arch !== currentArch) { + useStateStore().showSnackbarMessage({ + message: `Selected file architecture (${arch}) does not match device architecture (${currentArch}).`, + color: "error" + }); + return; + } else if (versionMatch && !dev) { + handleOfflineUpdate(files[0]); + } else if (!versionMatch && !dev) { + offlineUpdateDialog.value = { + show: true, + confirmString: `You are attempting to update from PhotonVision ${currentVersion} on image ${useSettingsStore().general.imageVersion} to ${version} from a different FRC year. These versions may be incompatible. Are you sure you want to proceed?` + }; + } else if (versionMatch && dev) { + offlineUpdateDialog.value = { + show: true, + confirmString: + "You are attempting to update to a dev version. This could result in instability. Are you sure you want to proceed?" + }; + } else if (!versionMatch && dev) { + offlineUpdateDialog.value = { + show: true, + confirmString: `You are attempting to update to a dev version, from PhotonVision ${currentVersion} on image ${useSettingsStore().general.imageVersion} to ${version} from a different FRC year. These versions may be incompatible, and you may experience instability. Are you sure you want to proceed?` + }; + } +}; + +const handleOfflineUpdate = async (file: File) => { const formData = new FormData(); - formData.append("jarData", files[0]); + formData.append("jarData", file); useStateStore().showSnackbarMessage({ message: "New Software Upload in Progress...", color: "secondary", @@ -134,6 +190,7 @@ interface MetricItem { const generalMetrics = computed(() => { const stats = [ { header: "Version", value: useSettingsStore().general.version || "Unknown" }, + { header: "Image Version", value: useSettingsStore().general.imageVersion || "Unknown" }, { header: "Hardware Model", value: useSettingsStore().general.hardwareModel || "Unknown" }, { header: "Platform", value: useSettingsStore().general.hardwarePlatform || "Unknown" }, { header: "GPU Acceleration", value: useSettingsStore().general.gpuAcceleration || "None detected" } @@ -333,7 +390,7 @@ watch(metricsHistorySnapshot, () => { type="file" accept=".jar" style="display: none" - @change="handleOfflineUpdate" + @change="handleOfflineUpdateRequest" /> @@ -483,6 +540,33 @@ watch(metricsHistorySnapshot, () => { + + + Offline Update + + {{ offlineUpdateDialog.confirmString }} + + + + + + mdi-upload + Confirm Update + + + + + + + ({ general: { version: undefined, + imageVersion: undefined, gpuAcceleration: undefined, hardwareModel: undefined, hardwarePlatform: undefined, + wpilibArch: undefined, mrCalWorking: true, availableModels: [], supportedBackends: [], @@ -155,8 +157,10 @@ export const useSettingsStore = defineStore("settings", { updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) { this.general = { version: data.general.version || undefined, + imageVersion: data.general.imageVersion || undefined, hardwareModel: data.general.hardwareModel || undefined, hardwarePlatform: data.general.hardwarePlatform || undefined, + wpilibArch: data.general.wpilibArch || undefined, gpuAcceleration: data.general.gpuAcceleration || undefined, mrCalWorking: data.general.mrCalWorking, availableModels: data.general.availableModels || undefined, diff --git a/photon-client/src/types/SettingTypes.ts b/photon-client/src/types/SettingTypes.ts index bab4547dc3..291f812abb 100644 --- a/photon-client/src/types/SettingTypes.ts +++ b/photon-client/src/types/SettingTypes.ts @@ -5,9 +5,11 @@ import { reactive } from "vue"; export interface GeneralSettings { version?: string; + imageVersion?: string; gpuAcceleration?: string; hardwareModel?: string; hardwarePlatform?: string; + wpilibArch?: string; mrCalWorking: boolean; availableModels: ObjectDetectionModelProperties[]; supportedBackends: string[]; diff --git a/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIGeneralSettings.java b/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIGeneralSettings.java index 069d11aeeb..087ec2f749 100644 --- a/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIGeneralSettings.java +++ b/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIGeneralSettings.java @@ -23,32 +23,38 @@ public class UIGeneralSettings { public UIGeneralSettings( String version, + String imageVersion, String gpuAcceleration, boolean mrCalWorking, NeuralNetworkModelsSettings.ModelProperties[] availableModels, List supportedBackends, String hardwareModel, String hardwarePlatform, + String wpilibArch, boolean conflictingHostname, String conflictingCameras) { this.version = version; + this.imageVersion = imageVersion; this.gpuAcceleration = gpuAcceleration; this.mrCalWorking = mrCalWorking; this.availableModels = availableModels; this.supportedBackends = supportedBackends; this.hardwareModel = hardwareModel; this.hardwarePlatform = hardwarePlatform; + this.wpilibArch = wpilibArch; this.conflictingHostname = conflictingHostname; this.conflictingCameras = conflictingCameras; } public String version; + public String imageVersion; public String gpuAcceleration; public boolean mrCalWorking; public NeuralNetworkModelsSettings.ModelProperties[] availableModels; public List supportedBackends; public String hardwareModel; public String hardwarePlatform; + public String wpilibArch; public boolean conflictingHostname; public String conflictingCameras; } diff --git a/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIPhotonConfiguration.java b/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIPhotonConfiguration.java index 04e9a70db7..6e5a14eb09 100644 --- a/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIPhotonConfiguration.java +++ b/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UIPhotonConfiguration.java @@ -24,6 +24,7 @@ import org.photonvision.common.configuration.NeuralNetworkModelManager; import org.photonvision.common.configuration.PhotonConfiguration; import org.photonvision.common.dataflow.networktables.NetworkTablesManager; +import org.photonvision.common.hardware.OsImageData; import org.photonvision.common.hardware.Platform; import org.photonvision.common.networking.NetworkManager; import org.photonvision.common.networking.NetworkUtils; @@ -52,6 +53,9 @@ public static UIPhotonConfiguration programStateToUi(PhotonConfiguration c) { !c.getHardwareConfig().ledPins.isEmpty()), new UIGeneralSettings( PhotonVersion.versionString, + OsImageData.IMAGE_METADATA.isPresent() + ? OsImageData.IMAGE_METADATA.get().commitTag() + : "", // TODO add support for other types of GPU accel LoadJNI.hasLoaded(JNITypes.LIBCAMERA) ? "Zerocopy Libcamera Working" : "", LoadJNI.hasLoaded(JNITypes.MRCAL), @@ -61,6 +65,7 @@ public static UIPhotonConfiguration programStateToUi(PhotonConfiguration c) { ? Platform.getHardwareModel() : c.getHardwareConfig().deviceName, Platform.getPlatformName(), + Platform.getNativePlatform(), NetworkTablesManager.getInstance().conflictingHostname, NetworkTablesManager.getInstance().conflictingCameras), c.getApriltagFieldLayout()), diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/OsImageData.java b/photon-core/src/main/java/org/photonvision/common/hardware/OsImageData.java index 5dbb5af85a..11c0b76ccf 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/OsImageData.java +++ b/photon-core/src/main/java/org/photonvision/common/hardware/OsImageData.java @@ -38,8 +38,11 @@ public class OsImageData { private static Path imageVersionFile = Path.of("/opt/photonvision/image-version"); private static Path imageMetadataFile = Path.of("/opt/photonvision/image-version.json"); - /** The OS image version string, if available. This is legacy, use {@link ImageMetadata}. */ - public static final Optional IMAGE_VERSION = getImageVersion(); + /** + * The OS image version string, if available. This is legacy, use {@link ImageMetadata}. + * Deprecated for removal in 2027. + */ + @Deprecated public static final Optional IMAGE_VERSION = getImageVersion(); private static Optional getImageVersion() { if (!imageVersionFile.toFile().exists()) { diff --git a/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java b/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java index 65c1970ba9..01f359eb88 100644 --- a/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java +++ b/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java @@ -17,6 +17,7 @@ package org.photonvision.common.hardware; +import edu.wpi.first.util.CombinedRuntimeLoader; import java.io.BufferedReader; import java.io.File; import java.io.IOException; @@ -139,6 +140,27 @@ public static String getPlatformName() { } } + /** + * This function serves to map between formats used in the CombinedRuntimeLoader and the platform + * names used in the wpilib-tools-plugin. This is typically used for native libraries. + * + * @return String representing the platform in the format used by wpilib-tools-plugin, or an empty + * string if the platform is not recognized. + */ + public static String getNativePlatform() { + String platPath = CombinedRuntimeLoader.getPlatformPath(); + + if (platPath == "/linux/x86-64/") { + return "linuxx64"; + } else if (platPath == "/windows/x86-64/") { + return "winx64"; + } else if (platPath == "/linux/arm64/") { + return "linuxarm64"; + } else { + return ""; + } + } + public static String getHardwareModel() { return currentPlatform.hardwareModel; }