001/* 002 * Copyright (C) Photon Vision. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program. If not, see <https://www.gnu.org/licenses/>. 016 */ 017 018package org.photonvision.server; 019 020import com.fasterxml.jackson.annotation.JsonProperty; 021import com.fasterxml.jackson.core.JsonProcessingException; 022import com.fasterxml.jackson.databind.ObjectMapper; 023import io.javalin.http.Context; 024import io.javalin.http.UploadedFile; 025import java.io.*; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.nio.file.Paths; 029import java.util.ArrayList; 030import java.util.HashMap; 031import java.util.Optional; 032import javax.imageio.ImageIO; 033import org.apache.commons.io.FileUtils; 034import org.opencv.core.Mat; 035import org.opencv.core.MatOfByte; 036import org.opencv.core.MatOfInt; 037import org.opencv.imgcodecs.Imgcodecs; 038import org.photonvision.common.configuration.ConfigManager; 039import org.photonvision.common.configuration.NetworkConfig; 040import org.photonvision.common.configuration.NeuralNetworkModelManager; 041import org.photonvision.common.dataflow.DataChangeDestination; 042import org.photonvision.common.dataflow.DataChangeService; 043import org.photonvision.common.dataflow.events.IncomingWebSocketEvent; 044import org.photonvision.common.dataflow.events.OutgoingUIEvent; 045import org.photonvision.common.dataflow.networktables.NetworkTablesManager; 046import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration; 047import org.photonvision.common.hardware.HardwareManager; 048import org.photonvision.common.hardware.Platform; 049import org.photonvision.common.logging.LogGroup; 050import org.photonvision.common.logging.Logger; 051import org.photonvision.common.networking.NetworkManager; 052import org.photonvision.common.util.ShellExec; 053import org.photonvision.common.util.TimedTaskManager; 054import org.photonvision.common.util.file.JacksonUtils; 055import org.photonvision.common.util.file.ProgramDirectoryUtilities; 056import org.photonvision.vision.calibration.CameraCalibrationCoefficients; 057import org.photonvision.vision.camera.CameraQuirk; 058import org.photonvision.vision.camera.PVCameraInfo; 059import org.photonvision.vision.processes.VisionSourceManager; 060import org.zeroturnaround.zip.ZipUtil; 061 062public class RequestHandler { 063 // Treat all 2XX calls as "INFO" 064 // Treat all 4XX calls as "ERROR" 065 // Treat all 5XX calls as "ERROR" 066 067 private static final Logger logger = new Logger(RequestHandler.class, LogGroup.WebServer); 068 069 private static final ObjectMapper kObjectMapper = new ObjectMapper(); 070 071 public static void onSettingsImportRequest(Context ctx) { 072 var file = ctx.uploadedFile("data"); 073 074 if (file == null) { 075 ctx.status(400); 076 ctx.result( 077 "No File was sent with the request. Make sure that the settings zip is sent at the key 'data'"); 078 logger.error( 079 "No File was sent with the request. Make sure that the settings zip is sent at the key 'data'"); 080 return; 081 } 082 083 if (!file.extension().contains("zip")) { 084 ctx.status(400); 085 ctx.result( 086 "The uploaded file was not of type 'zip'. The uploaded file should be a .zip file."); 087 logger.error( 088 "The uploaded file was not of type 'zip'. The uploaded file should be a .zip file."); 089 return; 090 } 091 092 // Create a temp file 093 var tempFilePath = handleTempFileCreation(file); 094 095 if (tempFilePath.isEmpty()) { 096 ctx.status(500); 097 ctx.result("There was an error while creating a temporary copy of the file"); 098 logger.error("There was an error while creating a temporary copy of the file"); 099 return; 100 } 101 102 ConfigManager.getInstance().setWriteTaskEnabled(false); 103 ConfigManager.getInstance().disableFlushOnShutdown(); 104 // We want to delete the -whole- zip file, so we need to teardown loggers for 105 // now 106 logger.info("Writing new settings zip (logs may be truncated)..."); 107 Logger.closeAllLoggers(); 108 if (ConfigManager.saveUploadedSettingsZip(tempFilePath.get())) { 109 ctx.status(200); 110 ctx.result("Successfully saved the uploaded settings zip, rebooting..."); 111 restartProgram(); 112 } else { 113 ctx.status(500); 114 ctx.result("There was an error while saving the uploaded zip file"); 115 } 116 } 117 118 public static void onSettingsExportRequest(Context ctx) { 119 logger.info("Exporting Settings to ZIP Archive"); 120 121 try { 122 var zip = ConfigManager.getInstance().getSettingsFolderAsZip(); 123 var stream = new FileInputStream(zip); 124 logger.info("Uploading settings with size " + stream.available()); 125 126 ctx.contentType("application/zip"); 127 ctx.header( 128 "Content-Disposition", "attachment; filename=\"photonvision-settings-export.zip\""); 129 130 ctx.result(stream); 131 ctx.status(200); 132 } catch (IOException e) { 133 logger.error("Unable to export settings archive, bad recode from zip to byte"); 134 ctx.status(500); 135 ctx.result("There was an error while exporting the settings archive"); 136 } 137 } 138 139 public static void onHardwareConfigRequest(Context ctx) { 140 var file = ctx.uploadedFile("data"); 141 142 if (file == null) { 143 ctx.status(400); 144 ctx.result( 145 "No File was sent with the request. Make sure that the hardware config json is sent at the key 'data'"); 146 logger.error( 147 "No File was sent with the request. Make sure that the hardware config json is sent at the key 'data'"); 148 return; 149 } 150 151 if (!file.extension().contains("json")) { 152 ctx.status(400); 153 ctx.result( 154 "The uploaded file was not of type 'json'. The uploaded file should be a .json file."); 155 logger.error( 156 "The uploaded file was not of type 'json'. The uploaded file should be a .json file."); 157 return; 158 } 159 160 // Create a temp file 161 var tempFilePath = handleTempFileCreation(file); 162 163 if (tempFilePath.isEmpty()) { 164 ctx.status(500); 165 ctx.result("There was an error while creating a temporary copy of the file"); 166 logger.error("There was an error while creating a temporary copy of the file"); 167 return; 168 } 169 170 if (ConfigManager.getInstance().saveUploadedHardwareConfig(tempFilePath.get().toPath())) { 171 ctx.status(200); 172 ctx.result("Successfully saved the uploaded hardware config, rebooting..."); 173 logger.info("Successfully saved the uploaded hardware config, rebooting..."); 174 restartProgram(); 175 } else { 176 ctx.status(500); 177 ctx.result("There was an error while saving the uploaded hardware config"); 178 logger.error("There was an error while saving the uploaded hardware config"); 179 } 180 } 181 182 public static void onHardwareSettingsRequest(Context ctx) { 183 var file = ctx.uploadedFile("data"); 184 185 if (file == null) { 186 ctx.status(400); 187 ctx.result( 188 "No File was sent with the request. Make sure that the hardware settings json is sent at the key 'data'"); 189 logger.error( 190 "No File was sent with the request. Make sure that the hardware settings json is sent at the key 'data'"); 191 return; 192 } 193 194 if (!file.extension().contains("json")) { 195 ctx.status(400); 196 ctx.result( 197 "The uploaded file was not of type 'json'. The uploaded file should be a .json file."); 198 logger.error( 199 "The uploaded file was not of type 'json'. The uploaded file should be a .json file."); 200 return; 201 } 202 203 // Create a temp file 204 var tempFilePath = handleTempFileCreation(file); 205 206 if (tempFilePath.isEmpty()) { 207 ctx.status(500); 208 ctx.result("There was an error while creating a temporary copy of the file"); 209 logger.error("There was an error while creating a temporary copy of the file"); 210 return; 211 } 212 213 if (ConfigManager.getInstance().saveUploadedHardwareSettings(tempFilePath.get().toPath())) { 214 ctx.status(200); 215 ctx.result("Successfully saved the uploaded hardware settings, rebooting..."); 216 logger.info("Successfully saved the uploaded hardware settings, rebooting..."); 217 restartProgram(); 218 } else { 219 ctx.status(500); 220 ctx.result("There was an error while saving the uploaded hardware settings"); 221 logger.error("There was an error while saving the uploaded hardware settings"); 222 } 223 } 224 225 public static void onNetworkConfigRequest(Context ctx) { 226 var file = ctx.uploadedFile("data"); 227 228 if (file == null) { 229 ctx.status(400); 230 ctx.result( 231 "No File was sent with the request. Make sure that the network config json is sent at the key 'data'"); 232 logger.error( 233 "No File was sent with the request. Make sure that the network config json is sent at the key 'data'"); 234 return; 235 } 236 237 if (!file.extension().contains("json")) { 238 ctx.status(400); 239 ctx.result( 240 "The uploaded file was not of type 'json'. The uploaded file should be a .json file."); 241 logger.error( 242 "The uploaded file was not of type 'json'. The uploaded file should be a .json file."); 243 return; 244 } 245 246 // Create a temp file 247 var tempFilePath = handleTempFileCreation(file); 248 249 if (tempFilePath.isEmpty()) { 250 ctx.status(500); 251 ctx.result("There was an error while creating a temporary copy of the file"); 252 logger.error("There was an error while creating a temporary copy of the file"); 253 return; 254 } 255 256 if (ConfigManager.getInstance().saveUploadedNetworkConfig(tempFilePath.get().toPath())) { 257 ctx.status(200); 258 ctx.result("Successfully saved the uploaded network config, rebooting..."); 259 logger.info("Successfully saved the uploaded network config, rebooting..."); 260 restartProgram(); 261 } else { 262 ctx.status(500); 263 ctx.result("There was an error while saving the uploaded network config"); 264 logger.error("There was an error while saving the uploaded network config"); 265 } 266 } 267 268 public static void onAprilTagFieldLayoutRequest(Context ctx) { 269 var file = ctx.uploadedFile("data"); 270 271 if (file == null) { 272 ctx.status(400); 273 ctx.result( 274 "No File was sent with the request. Make sure that the field layout json is sent at the key 'data'"); 275 logger.error( 276 "No File was sent with the request. Make sure that the field layout json is sent at the key 'data'"); 277 return; 278 } 279 280 if (!file.extension().contains("json")) { 281 ctx.status(400); 282 ctx.result( 283 "The uploaded file was not of type 'json'. The uploaded file should be a .json file."); 284 logger.error( 285 "The uploaded file was not of type 'json'. The uploaded file should be a .json file."); 286 return; 287 } 288 289 // Create a temp file 290 var tempFilePath = handleTempFileCreation(file); 291 292 if (tempFilePath.isEmpty()) { 293 ctx.status(500); 294 ctx.result("There was an error while creating a temporary copy of the file"); 295 logger.error("There was an error while creating a temporary copy of the file"); 296 return; 297 } 298 299 if (ConfigManager.getInstance().saveUploadedAprilTagFieldLayout(tempFilePath.get().toPath())) { 300 ctx.status(200); 301 ctx.result("Successfully saved the uploaded AprilTagFieldLayout, rebooting..."); 302 logger.info("Successfully saved the uploaded AprilTagFieldLayout, rebooting..."); 303 restartProgram(); 304 } else { 305 ctx.status(500); 306 ctx.result("There was an error while saving the uploaded AprilTagFieldLayout"); 307 logger.error("There was an error while saving the uploaded AprilTagFieldLayout"); 308 } 309 } 310 311 public static void onOfflineUpdateRequest(Context ctx) { 312 var file = ctx.uploadedFile("jarData"); 313 314 if (file == null) { 315 ctx.status(400); 316 ctx.result( 317 "No File was sent with the request. Make sure that the new jar is sent at the key 'jarData'"); 318 logger.error( 319 "No File was sent with the request. Make sure that the new jar is sent at the key 'jarData'"); 320 return; 321 } 322 323 if (!file.extension().contains("jar")) { 324 ctx.status(400); 325 ctx.result( 326 "The uploaded file was not of type 'jar'. The uploaded file should be a .jar file."); 327 logger.error( 328 "The uploaded file was not of type 'jar'. The uploaded file should be a .jar file."); 329 return; 330 } 331 332 try { 333 Path filePath = 334 Paths.get(ProgramDirectoryUtilities.getProgramDirectory(), "photonvision.jar"); 335 File targetFile = new File(filePath.toString()); 336 var stream = new FileOutputStream(targetFile); 337 338 file.content().transferTo(stream); 339 stream.close(); 340 341 ctx.status(200); 342 ctx.result( 343 "Offline update successfully complete. PhotonVision will restart in the background."); 344 logger.info( 345 "Offline update successfully complete. PhotonVision will restart in the background."); 346 restartProgram(); 347 } catch (FileNotFoundException e) { 348 ctx.result("The current program jar file couldn't be found."); 349 ctx.status(500); 350 logger.error("The current program jar file couldn't be found.", e); 351 } catch (IOException e) { 352 ctx.result("Unable to overwrite the existing program with the new program."); 353 ctx.status(500); 354 logger.error("Unable to overwrite the existing program with the new program.", e); 355 } 356 } 357 358 public static void onGeneralSettingsRequest(Context ctx) { 359 NetworkConfig config; 360 try { 361 config = kObjectMapper.readValue(ctx.bodyInputStream(), NetworkConfig.class); 362 363 ctx.status(200); 364 ctx.result("Successfully saved general settings"); 365 logger.info("Successfully saved general settings"); 366 } catch (IOException e) { 367 // If the settings can't be parsed, use the default network settings 368 config = new NetworkConfig(); 369 370 ctx.status(400); 371 ctx.result("The provided general settings were malformed"); 372 logger.error("The provided general settings were malformed", e); 373 } 374 375 ConfigManager.getInstance().setNetworkSettings(config); 376 ConfigManager.getInstance().requestSave(); 377 378 NetworkManager.getInstance().reinitialize(); 379 380 NetworkTablesManager.getInstance().setConfig(config); 381 } 382 383 public static class UICameraSettingsRequest { 384 @JsonProperty("fov") 385 double fov; 386 387 @JsonProperty("quirksToChange") 388 HashMap<CameraQuirk, Boolean> quirksToChange; 389 } 390 391 public static void onCameraSettingsRequest(Context ctx) { 392 try { 393 var data = kObjectMapper.readTree(ctx.bodyInputStream()); 394 395 String cameraUniqueName = data.get("cameraUniqueName").asText(); 396 var settings = 397 JacksonUtils.deserialize(data.get("settings").toString(), UICameraSettingsRequest.class); 398 var fov = settings.fov; 399 400 logger.info("Changing camera FOV to: " + fov); 401 logger.info("Changing quirks to: " + settings.quirksToChange.toString()); 402 403 var module = VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName); 404 module.setFov(fov); 405 module.changeCameraQuirks(settings.quirksToChange); 406 407 module.saveModule(); 408 409 ctx.status(200); 410 ctx.result("Successfully saved camera settings"); 411 logger.info("Successfully saved camera settings"); 412 } catch (NullPointerException | IOException e) { 413 ctx.status(400); 414 ctx.result("The provided camera settings were malformed"); 415 logger.error("The provided camera settings were malformed", e); 416 } 417 } 418 419 public static void onLogExportRequest(Context ctx) { 420 if (!Platform.isLinux()) { 421 ctx.status(405); 422 ctx.result("Logs can only be exported on a Linux platform"); 423 // INFO only log because this isn't ERROR worthy 424 logger.info("Logs can only be exported on a Linux platform"); 425 return; 426 } 427 428 try { 429 ShellExec shell = new ShellExec(); 430 var tempPath = Files.createTempFile("photonvision-journalctl", ".txt"); 431 var tempPath2 = Files.createTempFile("photonvision-kernelogs", ".txt"); 432 // In the command below: 433 // dmesg = output all kernel logs since current boot 434 // cat /var/log/kern.log = output all kernel logs since first boot 435 shell.executeBashCommand( 436 "journalctl -u photonvision.service > " 437 + tempPath.toAbsolutePath() 438 + " && dmesg > " 439 + tempPath2.toAbsolutePath()); 440 441 while (!shell.isOutputCompleted()) { 442 // TODO: add timeout 443 } 444 445 if (shell.getExitCode() == 0) { 446 // Wrote to the temp file! Zip and yeet it to the client 447 448 var out = Files.createTempFile("photonvision-logs", "zip").toFile(); 449 450 try { 451 ZipUtil.packEntries(new File[] {tempPath.toFile(), tempPath2.toFile()}, out); 452 } catch (Exception e) { 453 e.printStackTrace(); 454 } 455 456 var stream = new FileInputStream(out); 457 ctx.contentType("application/zip"); 458 ctx.header("Content-Disposition", "attachment; filename=\"photonvision-logs.zip\""); 459 ctx.result(stream); 460 ctx.status(200); 461 logger.info("Outputting log ZIP with size " + stream.available()); 462 } else { 463 ctx.status(500); 464 ctx.result("The journalctl service was unable to export logs"); 465 logger.error("The journalctl service was unable to export logs"); 466 } 467 } catch (IOException e) { 468 ctx.status(500); 469 ctx.result("There was an error while exporting journalctl logs"); 470 logger.error("There was an error while exporting journalctl logs", e); 471 } 472 } 473 474 public static void onCalibrationEndRequest(Context ctx) { 475 logger.info("Calibrating camera! This will take a long time..."); 476 477 String cameraUniqueName; 478 479 try { 480 cameraUniqueName = 481 kObjectMapper.readTree(ctx.bodyInputStream()).get("cameraUniqueName").asText(); 482 483 var calData = 484 VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName).endCalibration(); 485 if (calData == null) { 486 ctx.result("The calibration process failed"); 487 ctx.status(500); 488 logger.error( 489 "The calibration process failed. Calibration data for module at cameraUniqueName (" 490 + cameraUniqueName 491 + ") was null"); 492 return; 493 } 494 495 ctx.result("Camera calibration successfully completed!"); 496 ctx.status(200); 497 logger.info("Camera calibration successfully completed!"); 498 } catch (JsonProcessingException e) { 499 ctx.status(400); 500 ctx.result( 501 "The 'cameraUniqueName' field was not found in the request. Please make sure the cameraUniqueName of the vision module is specified with the 'cameraUniqueName' key."); 502 logger.error( 503 "The 'cameraUniqueName' field was not found in the request. Please make sure the cameraUniqueName of the vision module is specified with the 'cameraUniqueName' key.", 504 e); 505 } catch (Exception e) { 506 ctx.status(500); 507 ctx.result("There was an error while ending calibration"); 508 logger.error("There was an error while ending calibration", e); 509 } 510 } 511 512 public static void onDataCalibrationImportRequest(Context ctx) { 513 try { 514 var data = kObjectMapper.readTree(ctx.bodyInputStream()); 515 516 String cameraUniqueName = data.get("cameraUniqueName").asText(); 517 var coeffs = 518 kObjectMapper.convertValue(data.get("calibration"), CameraCalibrationCoefficients.class); 519 520 var uploadCalibrationEvent = 521 new IncomingWebSocketEvent<>( 522 DataChangeDestination.DCD_ACTIVEMODULE, 523 "calibrationUploaded", 524 coeffs, 525 cameraUniqueName, 526 null); 527 DataChangeService.getInstance().publishEvent(uploadCalibrationEvent); 528 529 ctx.status(200); 530 ctx.result("Calibration imported successfully from imported data!"); 531 logger.info("Calibration imported successfully from imported data!"); 532 } catch (JsonProcessingException e) { 533 ctx.status(400); 534 ctx.result("The provided calibration data was malformed"); 535 logger.error("The provided calibration data was malformed", e); 536 537 } catch (Exception e) { 538 ctx.status(500); 539 ctx.result("An error occurred while uploading calibration data"); 540 logger.error("An error occurred while uploading calibration data", e); 541 } 542 } 543 544 public static void onProgramRestartRequest(Context ctx) { 545 // TODO, check if this was successful or not 546 ctx.status(204); 547 restartProgram(); 548 } 549 550 public static void onImportObjectDetectionModelRequest(Context ctx) { 551 try { 552 // Retrieve the uploaded files 553 var modelFile = ctx.uploadedFile("rknn"); 554 var labelsFile = ctx.uploadedFile("labels"); 555 556 if (modelFile == null || labelsFile == null) { 557 ctx.status(400); 558 ctx.result( 559 "No File was sent with the request. Make sure that the model and labels files are sent at the keys 'rknn' and 'labels'"); 560 logger.error( 561 "No File was sent with the request. Make sure that the model and labels files are sent at the keys 'rknn' and 'labels'"); 562 return; 563 } 564 565 if (!modelFile.extension().contains("rknn") || !labelsFile.extension().contains("txt")) { 566 ctx.status(400); 567 ctx.result( 568 "The uploaded files were not of type 'rknn' and 'txt'. The uploaded files should be a .rknn and .txt file."); 569 logger.error( 570 "The uploaded files were not of type 'rknn' and 'txt'. The uploaded files should be a .rknn and .txt file."); 571 return; 572 } 573 574 // verify naming convention 575 576 // throws IllegalArgumentException if the model name is invalid 577 NeuralNetworkModelManager.verifyRKNNNames(modelFile.filename(), labelsFile.filename()); 578 579 // TODO move into neural network manager 580 581 var modelPath = 582 Paths.get( 583 ConfigManager.getInstance().getModelsDirectory().toString(), modelFile.filename()); 584 var labelsPath = 585 Paths.get( 586 ConfigManager.getInstance().getModelsDirectory().toString(), labelsFile.filename()); 587 588 try (FileOutputStream out = new FileOutputStream(modelPath.toFile())) { 589 modelFile.content().transferTo(out); 590 } 591 592 try (FileOutputStream out = new FileOutputStream(labelsPath.toFile())) { 593 labelsFile.content().transferTo(out); 594 } 595 596 NeuralNetworkModelManager.getInstance() 597 .discoverModels(ConfigManager.getInstance().getModelsDirectory()); 598 599 ctx.status(200).result("Successfully uploaded object detection model"); 600 } catch (Exception e) { 601 ctx.status(500).result("Error processing files: " + e.getMessage()); 602 } 603 604 DataChangeService.getInstance() 605 .publishEvent( 606 new OutgoingUIEvent<>( 607 "fullsettings", 608 UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig()))); 609 } 610 611 public static void onDeviceRestartRequest(Context ctx) { 612 ctx.status(HardwareManager.getInstance().restartDevice() ? 204 : 500); 613 } 614 615 public static void onCameraNicknameChangeRequest(Context ctx) { 616 try { 617 var data = kObjectMapper.readTree(ctx.bodyInputStream()); 618 619 String name = data.get("name").asText(); 620 String cameraUniqueName = data.get("cameraUniqueName").asText(); 621 622 VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName).setCameraNickname(name); 623 ctx.status(200); 624 ctx.result("Successfully changed the camera name to: " + name); 625 logger.info("Successfully changed the camera name to: " + name); 626 } catch (JsonProcessingException e) { 627 ctx.status(400); 628 ctx.result("The provided nickname data was malformed"); 629 logger.error("The provided nickname data was malformed", e); 630 631 } catch (Exception e) { 632 ctx.status(500); 633 ctx.result("An error occurred while changing the camera's nickname"); 634 logger.error("An error occurred while changing the camera's nickname", e); 635 } 636 } 637 638 public static void onMetricsPublishRequest(Context ctx) { 639 HardwareManager.getInstance().publishMetrics(); 640 ctx.status(204); 641 } 642 643 public static void onCalibrationSnapshotRequest(Context ctx) { 644 logger.info(ctx.queryString().toString()); 645 646 String cameraUniqueName = ctx.queryParam("cameraUniqueName"); 647 var width = Integer.parseInt(ctx.queryParam("width")); 648 var height = Integer.parseInt(ctx.queryParam("height")); 649 var observationIdx = Integer.parseInt(ctx.queryParam("snapshotIdx")); 650 651 CameraCalibrationCoefficients calList = 652 VisionSourceManager.getInstance() 653 .vmm 654 .getModule(cameraUniqueName) 655 .getStateAsCameraConfig() 656 .calibrations 657 .stream() 658 .filter( 659 it -> 660 Math.abs(it.unrotatedImageSize.width - width) < 1e-4 661 && Math.abs(it.unrotatedImageSize.height - height) < 1e-4) 662 .findFirst() 663 .orElse(null); 664 665 if (calList == null || calList.observations.size() < observationIdx) { 666 ctx.status(404); 667 return; 668 } 669 670 // encode as jpeg to save even more space. reduces size of a 1280p image from 671 // 300k to 25k 672 var jpegBytes = new MatOfByte(); 673 Mat img = null; 674 try { 675 img = 676 Imgcodecs.imread( 677 calList.observations.get(observationIdx).snapshotDataLocation.toString()); 678 } catch (Exception e) { 679 ctx.status(500); 680 ctx.result("Unable to read calibration image"); 681 return; 682 } 683 if (img == null || img.empty()) { 684 ctx.status(500); 685 ctx.result("Unable to read calibration image"); 686 return; 687 } 688 689 Imgcodecs.imencode(".jpg", img, jpegBytes, new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 60)); 690 691 ctx.result(jpegBytes.toArray()); 692 jpegBytes.release(); 693 694 ctx.status(200); 695 } 696 697 public static void onCalibrationExportRequest(Context ctx) { 698 logger.info(ctx.queryString().toString()); 699 700 String cameraUniqueName = ctx.queryParam("cameraUniqueName"); 701 var width = Integer.parseInt(ctx.queryParam("width")); 702 var height = Integer.parseInt(ctx.queryParam("height")); 703 704 var cc = 705 VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName).getStateAsCameraConfig(); 706 707 CameraCalibrationCoefficients calList = 708 cc.calibrations.stream() 709 .filter( 710 it -> 711 Math.abs(it.unrotatedImageSize.width - width) < 1e-4 712 && Math.abs(it.unrotatedImageSize.height - height) < 1e-4) 713 .findFirst() 714 .orElse(null); 715 716 if (calList == null) { 717 ctx.status(404); 718 return; 719 } 720 721 var filename = "photon_calibration_" + cc.uniqueName + "_" + width + "x" + height + ".json"; 722 ctx.contentType("application/zip"); 723 ctx.header("Content-Disposition", "attachment; filename=\"" + filename + "\""); 724 ctx.json(calList); 725 726 ctx.status(200); 727 } 728 729 public static void onImageSnapshotsRequest(Context ctx) { 730 var snapshots = new ArrayList<HashMap<String, Object>>(); 731 var cameraDirs = ConfigManager.getInstance().getImageSavePath().toFile().listFiles(); 732 733 if (cameraDirs != null) { 734 try { 735 for (File cameraDir : cameraDirs) { 736 var cameraSnapshots = cameraDir.listFiles(); 737 if (cameraSnapshots == null) continue; 738 739 String cameraUniqueName = cameraDir.getName(); 740 741 for (File snapshot : cameraSnapshots) { 742 var snapshotData = new HashMap<String, Object>(); 743 744 var bufferedImage = ImageIO.read(snapshot); 745 var buffer = new ByteArrayOutputStream(); 746 ImageIO.write(bufferedImage, "jpg", buffer); 747 byte[] data = buffer.toByteArray(); 748 749 snapshotData.put("snapshotName", snapshot.getName()); 750 snapshotData.put("cameraUniqueName", cameraUniqueName); 751 snapshotData.put("snapshotData", data); 752 753 snapshots.add(snapshotData); 754 } 755 } 756 } catch (IOException e) { 757 ctx.status(500); 758 ctx.result("Unable to read saved images"); 759 } 760 } 761 762 ctx.status(200); 763 ctx.json(snapshots); 764 } 765 766 public static void onCameraCalibImagesRequest(Context ctx) { 767 try { 768 HashMap<String, HashMap<String, ArrayList<HashMap<String, Object>>>> snapshots = 769 new HashMap<>(); 770 771 var cameraDirs = ConfigManager.getInstance().getCalibDir().toFile().listFiles(); 772 if (cameraDirs != null) { 773 var camData = new HashMap<String, ArrayList<HashMap<String, Object>>>(); 774 for (var cameraDir : cameraDirs) { 775 var resolutionDirs = cameraDir.listFiles(); 776 if (resolutionDirs == null) continue; 777 for (var resolutionDir : resolutionDirs) { 778 var calibImages = resolutionDir.listFiles(); 779 if (calibImages == null) continue; 780 var resolutionImages = new ArrayList<HashMap<String, Object>>(); 781 for (var calibImg : calibImages) { 782 var snapshotData = new HashMap<String, Object>(); 783 784 var bufferedImage = ImageIO.read(calibImg); 785 var buffer = new ByteArrayOutputStream(); 786 ImageIO.write(bufferedImage, "png", buffer); 787 byte[] data = buffer.toByteArray(); 788 789 snapshotData.put("snapshotData", data); 790 snapshotData.put("snapshotFilename", calibImg.getName()); 791 792 resolutionImages.add(snapshotData); 793 } 794 camData.put(resolutionDir.getName(), resolutionImages); 795 } 796 797 var cameraName = cameraDir.getName(); 798 snapshots.put(cameraName, camData); 799 } 800 } 801 802 ctx.json(snapshots); 803 } catch (Exception e) { 804 ctx.status(500); 805 ctx.result("An error occurred while getting calib data"); 806 logger.error("An error occurred while getting calib data", e); 807 } 808 } 809 810 /** 811 * Create a temporary file using the UploadedFile from Javalin. 812 * 813 * @param file the uploaded file. 814 * @return Temporary file. Empty if the temporary file was unable to be created. 815 */ 816 private static Optional<File> handleTempFileCreation(UploadedFile file) { 817 var tempFilePath = 818 new File(Path.of(System.getProperty("java.io.tmpdir"), file.filename()).toString()); 819 boolean makeDirsRes = tempFilePath.getParentFile().mkdirs(); 820 821 if (!makeDirsRes && !(tempFilePath.getParentFile().exists())) { 822 logger.error( 823 "There was an error while creating " 824 + tempFilePath.getAbsolutePath() 825 + "! Exists: " 826 + tempFilePath.getParentFile().exists()); 827 return Optional.empty(); 828 } 829 830 try { 831 FileUtils.copyInputStreamToFile(file.content(), tempFilePath); 832 } catch (IOException e) { 833 logger.error( 834 "There was an error while copying " 835 + file.filename() 836 + " to the temp file " 837 + tempFilePath.getAbsolutePath()); 838 return Optional.empty(); 839 } 840 841 return Optional.of(tempFilePath); 842 } 843 844 /** 845 * Restart the running program. Note that this doesn't actually restart the program itself, 846 * instead, it relies on systemd or an equivalent. 847 */ 848 private static void restartProgram() { 849 TimedTaskManager.getInstance() 850 .addOneShotTask( 851 () -> { 852 if (Platform.isLinux()) { 853 try { 854 new ShellExec().executeBashCommand("systemctl restart photonvision.service"); 855 } catch (IOException e) { 856 logger.error("Could not restart device!", e); 857 System.exit(0); 858 } 859 } else { 860 System.exit(0); 861 } 862 }, 863 0); 864 } 865 866 public static void onNukeConfigDirectory(Context ctx) { 867 ConfigManager.getInstance().setWriteTaskEnabled(false); 868 ConfigManager.getInstance().disableFlushOnShutdown(); 869 870 Logger.closeAllLoggers(); 871 if (ConfigManager.nukeConfigDirectory()) { 872 ctx.status(200); 873 ctx.result("Successfully nuked config dir"); 874 restartProgram(); 875 } else { 876 ctx.status(500); 877 ctx.result("There was an error while nuking the config directory"); 878 } 879 } 880 881 public static void onNukeOneCamera(Context ctx) { 882 try { 883 var payload = kObjectMapper.readTree(ctx.bodyInputStream()); 884 var name = payload.get("cameraUniqueName").asText(); 885 logger.warn("Deleting camera name " + name); 886 887 var cameraDir = ConfigManager.getInstance().getCalibrationImageSavePath(name).toFile(); 888 if (cameraDir.exists()) { 889 FileUtils.deleteDirectory(cameraDir); 890 } 891 892 VisionSourceManager.getInstance().deleteVisionSource(name); 893 894 ctx.status(200); 895 } catch (IOException e) { 896 // todo 897 logger.error("asdf", e); 898 ctx.status(500); 899 } 900 } 901 902 public static void onActivateMatchedCameraRequest(Context ctx) { 903 logger.info(ctx.queryString().toString()); 904 905 String cameraUniqueName = ctx.queryParam("cameraUniqueName"); 906 907 if (VisionSourceManager.getInstance().reactivateDisabledCameraConfig(cameraUniqueName)) { 908 ctx.status(200); 909 } else { 910 ctx.status(403); 911 } 912 913 ctx.result("Successfully assigned camera with unique name: " + cameraUniqueName); 914 } 915 916 public static void onAssignUnmatchedCameraRequest(Context ctx) { 917 logger.info(ctx.queryString().toString()); 918 919 PVCameraInfo camera; 920 try { 921 camera = JacksonUtils.deserialize(ctx.queryParam("cameraInfo"), PVCameraInfo.class); 922 } catch (IOException e) { 923 ctx.status(401); 924 return; 925 } 926 927 if (VisionSourceManager.getInstance().assignUnmatchedCamera(camera)) { 928 ctx.status(200); 929 } else { 930 ctx.status(404); 931 } 932 933 ctx.result("Successfully assigned camera: " + camera); 934 } 935 936 public static void onUnassignCameraRequest(Context ctx) { 937 logger.info(ctx.queryString().toString()); 938 939 String cameraUniqueName = ctx.queryParam("cameraUniqueName"); 940 941 if (VisionSourceManager.getInstance().deactivateVisionSource(cameraUniqueName)) { 942 ctx.status(200); 943 } else { 944 ctx.status(403); 945 } 946 947 ctx.status(200); 948 949 ctx.result("Successfully assigned camera with unique name: " + cameraUniqueName); 950 } 951}