diff --git a/photon-client/src/stores/settings/CameraSettingsStore.ts b/photon-client/src/stores/settings/CameraSettingsStore.ts index 4e5e1dc6c..e21b56f6d 100644 --- a/photon-client/src/stores/settings/CameraSettingsStore.ts +++ b/photon-client/src/stores/settings/CameraSettingsStore.ts @@ -155,7 +155,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * Update the configurable camera settings. * * @param data camera settings to save. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ updateCameraSettings( data: CameraSettingsChangeRequest, @@ -163,9 +163,8 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { ) { // The camera settings endpoint doesn't actually require all data, instead, it needs key data such as the FOV const payload = { - settings: { - ...data - }, + fov: data.fov, + quirksToChange: data.quirksToChange, cameraUniqueName: cameraUniqueName }; return axios.post("/settings/camera", payload); @@ -175,7 +174,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * * @param newPipelineName the name of the new pipeline. * @param pipelineType the type of the new pipeline. Cannot be {@link WebsocketPipelineType.Calib3d} or {@link WebsocketPipelineType.DriverMode}. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ createNewPipeline( newPipelineName: string, @@ -193,7 +192,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * * @param settings settings to modify. The type of the settings should match the currently selected pipeline type. * @param updateStore whether or not to update the store. This is useful if the input field already models the store reference. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ changeCurrentPipelineSetting( settings: ActiveConfigurablePipelineSettings, @@ -224,7 +223,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * * @param newName the new nickname for the camera. * @param updateStore whether or not to update the store. This is useful if the input field already models the store reference. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ changeCurrentPipelineNickname( newName: string, @@ -244,7 +243,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * Modify the Pipeline type of the currently selected pipeline of the provided camera. This overwrites the current pipeline's settings when the backend resets the current pipeline settings. * * @param type the pipeline type to set. Cannot be {@link WebsocketPipelineType.Calib3d} or {@link WebsocketPipelineType.DriverMode}. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ changeCurrentPipelineType( type: Exclude, @@ -261,7 +260,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * * @param index pipeline index to set. * @param updateStore whether or not to update the store. This is useful if the input field already models the store reference. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ changeCurrentPipelineIndex( index: number, @@ -293,7 +292,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { /** * Change the currently selected pipeline of the provided camera. * - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ deleteCurrentPipeline(cameraUniqueName: string = useStateStore().currentCameraUniqueName) { const payload = { @@ -306,7 +305,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * Duplicate the pipeline at the provided index. * * @param pipelineIndex index of the pipeline to duplicate. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ duplicatePipeline(pipelineIndex: number, cameraUniqueName: string = useStateStore().currentCameraUniqueName) { const payload = { @@ -335,7 +334,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * * @param newName the new nickname of the camera. * @param updateStore whether or not to update the store. This is useful if the input field already models the store reference. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. * @return HTTP request promise to the backend */ changeCameraNickname( @@ -356,7 +355,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { * Start the 3D calibration process for the provided camera. * * @param calibrationInitData initialization calibration data. - * @param cameraUniqueNamendex the unique name of the camera. + * @param cameraUniqueName the unique name of the camera. */ startPnPCalibration( calibrationInitData: { diff --git a/photon-client/src/views/CameraMatchingView.vue b/photon-client/src/views/CameraMatchingView.vue index 082ab1285..999a89c19 100644 --- a/photon-client/src/views/CameraMatchingView.vue +++ b/photon-client/src/views/CameraMatchingView.vue @@ -23,36 +23,106 @@ const activatingModule = ref(false); const activateModule = (moduleUniqueName: string) => { if (activatingModule.value) return; activatingModule.value = true; - const url = new URL(`http://${host}/api/utils/activateMatchedCamera`); - url.searchParams.set("cameraUniqueName", moduleUniqueName); - fetch(url.toString(), { - method: "POST" - }).finally(() => (activatingModule.value = false)); + axios + .post("/utils/activateMatchedCamera", { cameraUniqueName: moduleUniqueName }) + .then(() => { + useStateStore().showSnackbarMessage({ + message: "Camera activated successfully", + color: "success" + }); + }) + .catch((error) => { + if (error.response) { + useStateStore().showSnackbarMessage({ + message: "The backend is unable to fulfil the request to activate this camera.", + color: "error" + }); + } else if (error.request) { + useStateStore().showSnackbarMessage({ + message: "Error while trying to process the request! The backend didn't respond.", + color: "error" + }); + } else { + useStateStore().showSnackbarMessage({ + message: "An error occurred while trying to process the request.", + color: "error" + }); + } + }) + .finally(() => (activatingModule.value = false)); }; const assigningCamera = ref(false); const assignCamera = (cameraInfo: PVCameraInfo) => { if (assigningCamera.value) return; assigningCamera.value = true; - const url = new URL(`http://${host}/api/utils/assignUnmatchedCamera`); - url.searchParams.set("cameraInfo", JSON.stringify(cameraInfo)); - fetch(url.toString(), { - method: "POST" - }).finally(() => (assigningCamera.value = false)); + const payload = { + cameraInfo: cameraInfo + }; + + axios + .post("/utils/assignUnmatchedCamera", payload) + .then(() => { + useStateStore().showSnackbarMessage({ + message: "Unmatched camera assigned successfully", + color: "success" + }); + }) + .catch((error) => { + if (error.response) { + useStateStore().showSnackbarMessage({ + message: "The backend is unable to fulfil the request to assign this unmatched camera.", + color: "error" + }); + } else if (error.request) { + useStateStore().showSnackbarMessage({ + message: "Error while trying to process the request! The backend didn't respond.", + color: "error" + }); + } else { + useStateStore().showSnackbarMessage({ + message: "An error occurred while trying to process the request.", + color: "error" + }); + } + }) + .finally(() => (assigningCamera.value = false)); }; const deactivatingModule = ref(false); const deactivateModule = (cameraUniqueName: string) => { if (deactivatingModule.value) return; deactivatingModule.value = true; - const url = new URL(`http://${host}/api/utils/unassignCamera`); - url.searchParams.set("cameraUniqueName", cameraUniqueName); - fetch(url.toString(), { - method: "POST" - }).finally(() => (deactivatingModule.value = false)); + axios + .post("/utils/unassignCamera", { cameraUniqueName: cameraUniqueName }) + .then(() => { + useStateStore().showSnackbarMessage({ + message: "Camera deactivated successfully", + color: "success" + }); + }) + .catch((error) => { + if (error.response) { + useStateStore().showSnackbarMessage({ + message: "The backend is unable to fulfil the request to deactivate this camera.", + color: "error" + }); + } else if (error.request) { + useStateStore().showSnackbarMessage({ + message: "Error while trying to process the request! The backend didn't respond.", + color: "error" + }); + } else { + useStateStore().showSnackbarMessage({ + message: "An error occurred while trying to process the request.", + color: "error" + }); + } + }) + .finally(() => (deactivatingModule.value = false)); }; const deletingCamera = ref(false); diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java b/photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java index 9728569cb..d67bdfb34 100644 --- a/photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java +++ b/photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java @@ -19,6 +19,7 @@ package org.photonvision.vision.camera; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -27,6 +28,7 @@ import edu.wpi.first.cscore.UsbCameraInfo; import java.util.Arrays; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) +@JsonIgnoreProperties(ignoreUnknown = true) @JsonSubTypes({ @JsonSubTypes.Type(value = PVCameraInfo.PVUsbCameraInfo.class), @JsonSubTypes.Type(value = PVCameraInfo.PVCSICameraInfo.class), diff --git a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java index f734f38ef..6f7bd04c0 100644 --- a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java +++ b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java @@ -17,7 +17,6 @@ package org.photonvision.server; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.javalin.http.Context; @@ -51,7 +50,6 @@ import org.photonvision.common.logging.Logger; import org.photonvision.common.networking.NetworkManager; import org.photonvision.common.util.ShellExec; import org.photonvision.common.util.TimedTaskManager; -import org.photonvision.common.util.file.JacksonUtils; import org.photonvision.common.util.file.ProgramDirectoryUtilities; import org.photonvision.vision.calibration.CameraCalibrationCoefficients; import org.photonvision.vision.camera.CameraQuirk; @@ -68,6 +66,8 @@ public class RequestHandler { private static final ObjectMapper kObjectMapper = new ObjectMapper(); + private record CommonCameraUniqueName(String cameraUniqueName) {} + public static void onSettingsImportRequest(Context ctx) { var file = ctx.uploadedFile("data"); @@ -380,39 +380,38 @@ public class RequestHandler { NetworkTablesManager.getInstance().setConfig(config); } - public static class UICameraSettingsRequest { - @JsonProperty("fov") - double fov; - - @JsonProperty("quirksToChange") - HashMap quirksToChange; - } + private record CameraSettingsRequest( + double fov, HashMap quirksToChange, String cameraUniqueName) {} public static void onCameraSettingsRequest(Context ctx) { try { - var data = kObjectMapper.readTree(ctx.bodyInputStream()); + CameraSettingsRequest request = + kObjectMapper.readValue(ctx.body(), CameraSettingsRequest.class); + // Extract the settings from the request + double fov = request.fov; + HashMap quirksToChange = request.quirksToChange; + String cameraUniqueName = request.cameraUniqueName; - String cameraUniqueName = data.get("cameraUniqueName").asText(); - var settings = - JacksonUtils.deserialize(data.get("settings").toString(), UICameraSettingsRequest.class); - var fov = settings.fov; + if (cameraUniqueName == null || cameraUniqueName.isEmpty()) { + ctx.status(400).result("cameraUniqueName is required"); + logger.error("cameraUniqueName is missing in the request"); + return; + } logger.info("Changing camera FOV to: " + fov); - logger.info("Changing quirks to: " + settings.quirksToChange.toString()); + + logger.info("Changing quirks to: " + quirksToChange.toString()); var module = VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName); - module.setFov(fov); - module.changeCameraQuirks(settings.quirksToChange); + module.setFov(fov); + module.changeCameraQuirks(quirksToChange); module.saveModule(); - ctx.status(200); - ctx.result("Successfully saved camera settings"); - logger.info("Successfully saved camera settings"); - } catch (NullPointerException | IOException e) { - ctx.status(400); - ctx.result("The provided camera settings were malformed"); - logger.error("The provided camera settings were malformed", e); + ctx.status(200).result("Camera settings updated successfully"); + } catch (Exception e) { + logger.error("Failed to process camera settings request", e); + ctx.status(500).result("Failed to process camera settings request"); } } @@ -474,20 +473,21 @@ public class RequestHandler { public static void onCalibrationEndRequest(Context ctx) { logger.info("Calibrating camera! This will take a long time..."); - String cameraUniqueName; - try { - cameraUniqueName = - kObjectMapper.readTree(ctx.bodyInputStream()).get("cameraUniqueName").asText(); + CommonCameraUniqueName request = + kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class); var calData = - VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName).endCalibration(); + VisionSourceManager.getInstance() + .vmm + .getModule(request.cameraUniqueName) + .endCalibration(); if (calData == null) { ctx.result("The calibration process failed"); ctx.status(500); logger.error( "The calibration process failed. Calibration data for module at cameraUniqueName (" - + cameraUniqueName + + request.cameraUniqueName + ") was null"); return; } @@ -509,20 +509,20 @@ public class RequestHandler { } } + private record DataCalibrationImportRequest( + String cameraUniqueName, CameraCalibrationCoefficients calibration) {} + public static void onDataCalibrationImportRequest(Context ctx) { try { - var data = kObjectMapper.readTree(ctx.bodyInputStream()); - - String cameraUniqueName = data.get("cameraUniqueName").asText(); - var coeffs = - kObjectMapper.convertValue(data.get("calibration"), CameraCalibrationCoefficients.class); + DataCalibrationImportRequest request = + kObjectMapper.readValue(ctx.body(), DataCalibrationImportRequest.class); var uploadCalibrationEvent = new IncomingWebSocketEvent<>( DataChangeDestination.DCD_ACTIVEMODULE, "calibrationUploaded", - coeffs, - cameraUniqueName, + request.calibration, + request.cameraUniqueName, null); DataChangeService.getInstance().publishEvent(uploadCalibrationEvent); @@ -612,26 +612,26 @@ public class RequestHandler { ctx.status(HardwareManager.getInstance().restartDevice() ? 204 : 500); } + private record CameraNicknameChangeRequest(String name, String cameraUniqueName) {} + public static void onCameraNicknameChangeRequest(Context ctx) { try { - var data = kObjectMapper.readTree(ctx.bodyInputStream()); + CameraNicknameChangeRequest request = + kObjectMapper.readValue(ctx.body(), CameraNicknameChangeRequest.class); - String name = data.get("name").asText(); - String cameraUniqueName = data.get("cameraUniqueName").asText(); - - VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName).setCameraNickname(name); + VisionSourceManager.getInstance() + .vmm + .getModule(request.cameraUniqueName) + .setCameraNickname(request.name); ctx.status(200); - ctx.result("Successfully changed the camera name to: " + name); - logger.info("Successfully changed the camera name to: " + name); + ctx.result("Successfully changed the camera name to: " + request.name); + logger.info("Successfully changed the camera name to: " + request.name); } catch (JsonProcessingException e) { - ctx.status(400); - ctx.result("The provided nickname data was malformed"); - logger.error("The provided nickname data was malformed", e); - + ctx.status(400).result("Invalid JSON format"); + logger.error("Failed to process camera nickname change request", e); } catch (Exception e) { - ctx.status(500); - ctx.result("An error occurred while changing the camera's nickname"); - logger.error("An error occurred while changing the camera's nickname", e); + ctx.status(500).result("Failed to change camera nickname"); + logger.error("Unexpected error while changing camera nickname", e); } } @@ -641,12 +641,10 @@ public class RequestHandler { } public static void onCalibrationSnapshotRequest(Context ctx) { - logger.info(ctx.queryString()); - String cameraUniqueName = ctx.queryParam("cameraUniqueName"); var width = Integer.parseInt(ctx.queryParam("width")); var height = Integer.parseInt(ctx.queryParam("height")); - var observationIdx = Integer.parseInt(ctx.queryParam("snapshotIdx")); + Integer observationIdx = Integer.parseInt(ctx.queryParam("snapshotIdx")); CameraCalibrationCoefficients calList = VisionSourceManager.getInstance() @@ -695,8 +693,6 @@ public class RequestHandler { } public static void onCalibrationExportRequest(Context ctx) { - logger.info(ctx.queryString()); - String cameraUniqueName = ctx.queryParam("cameraUniqueName"); var width = Integer.parseInt(ctx.queryParam("width")); var height = Integer.parseInt(ctx.queryParam("height")); @@ -880,72 +876,97 @@ public class RequestHandler { public static void onNukeOneCamera(Context ctx) { try { - var payload = kObjectMapper.readTree(ctx.bodyInputStream()); - var name = payload.get("cameraUniqueName").asText(); - logger.warn("Deleting camera name " + name); + CommonCameraUniqueName request = + kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class); - var cameraDir = ConfigManager.getInstance().getCalibrationImageSavePath(name).toFile(); + logger.warn("Deleting camera name " + request.cameraUniqueName); + + var cameraDir = + ConfigManager.getInstance() + .getCalibrationImageSavePath(request.cameraUniqueName) + .toFile(); if (cameraDir.exists()) { FileUtils.deleteDirectory(cameraDir); } - VisionSourceManager.getInstance().deleteVisionSource(name); + VisionSourceManager.getInstance().deleteVisionSource(request.cameraUniqueName); ctx.status(200); } catch (IOException e) { - // todo - logger.error("asdf", e); + logger.error("Failed to delete camera", e); ctx.status(500); + ctx.result("Failed to delete camera"); + return; } } public static void onActivateMatchedCameraRequest(Context ctx) { logger.info(ctx.queryString()); + try { + CommonCameraUniqueName request = + kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class); - String cameraUniqueName = ctx.queryParam("cameraUniqueName"); - - if (VisionSourceManager.getInstance().reactivateDisabledCameraConfig(cameraUniqueName)) { - ctx.status(200); - } else { - ctx.status(403); + if (VisionSourceManager.getInstance() + .reactivateDisabledCameraConfig(request.cameraUniqueName)) { + ctx.status(200); + } else { + ctx.status(403); + } + } catch (IOException e) { + ctx.status(401); + logger.error("Failed to process activate matched camera request", e); + ctx.result("Failed to process activate matched camera request"); + return; } - - ctx.result("Successfully assigned camera with unique name: " + cameraUniqueName); } + private record AssignUnmatchedCamera(PVCameraInfo cameraInfo) {} + public static void onAssignUnmatchedCameraRequest(Context ctx) { logger.info(ctx.queryString()); - PVCameraInfo camera; try { - camera = JacksonUtils.deserialize(ctx.queryParam("cameraInfo"), PVCameraInfo.class); + AssignUnmatchedCamera request = + kObjectMapper.readValue(ctx.body(), AssignUnmatchedCamera.class); + + if (request.cameraInfo == null) { + ctx.status(400); + ctx.result("cameraInfo is required"); + logger.error("cameraInfo is missing in the request"); + return; + } + + if (VisionSourceManager.getInstance().assignUnmatchedCamera(request.cameraInfo)) { + ctx.status(200); + } else { + ctx.status(404); + } + + ctx.result("Successfully assigned camera: " + request.cameraInfo); } catch (IOException e) { ctx.status(401); + logger.error("Failed to process assign unmatched camera request", e); + ctx.result("Failed to process assign unmatched camera request"); return; } - - if (VisionSourceManager.getInstance().assignUnmatchedCamera(camera)) { - ctx.status(200); - } else { - ctx.status(404); - } - - ctx.result("Successfully assigned camera: " + camera); } public static void onUnassignCameraRequest(Context ctx) { logger.info(ctx.queryString()); + try { + CommonCameraUniqueName request = + kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class); - String cameraUniqueName = ctx.queryParam("cameraUniqueName"); - - if (VisionSourceManager.getInstance().deactivateVisionSource(cameraUniqueName)) { - ctx.status(200); - } else { - ctx.status(403); + if (VisionSourceManager.getInstance().deactivateVisionSource(request.cameraUniqueName)) { + ctx.status(200); + } else { + ctx.status(403); + } + } catch (IOException e) { + ctx.status(401); + logger.error("Failed to process unassign camera request", e); + ctx.result("Failed to process unassign camera request"); + return; } - - ctx.status(200); - - ctx.result("Successfully assigned camera with unique name: " + cameraUniqueName); } }