labels = new LinkedList<>();
String rawLabels = ctx.formParam("labels");
if (rawLabels != null) {
for (String label : rawLabels.split(",")) {
labels.add(label.trim());
}
}
int width = Integer.parseInt(ctx.formParam("width"));
int height = Integer.parseInt(ctx.formParam("height"));
NeuralNetworkModelManager.Version version =
switch (ctx.formParam("version").toString()) {
case "YOLOv5" -> NeuralNetworkModelManager.Version.YOLOV5;
case "YOLOv8" -> NeuralNetworkModelManager.Version.YOLOV8;
case "YOLO11" -> NeuralNetworkModelManager.Version.YOLOV11;
// Add more versions as necessary for new models
default -> {
ctx.status(400);
ctx.result("The provided version was not valid");
logger.error("The provided version was not valid");
yield null;
}
};
if (modelFile == null) {
ctx.status(400);
ctx.result(
"No File was sent with the request. Make sure that the model file is sent at the key 'modelFile'");
logger.error(
"No File was sent with the request. Make sure that the model file is sent at the key 'modelFile'");
return;
}
if (labels == null || labels.isEmpty()) {
ctx.status(400);
ctx.result("The provided labels were malformed");
logger.error("The provided labels were malformed");
return;
}
if (width < 0 || height < 0 || width != Math.floor(width) || height != Math.floor(height)) {
ctx.status(400);
ctx.result(
"The provided width and height were malformed. They must be integers greater than one.");
logger.error(
"The provided width and height were malformed. They must be integers greater than one.");
return;
}
NeuralNetworkModelManager.Family family;
switch (Platform.getCurrentPlatform()) {
case LINUX_QCS6490:
family = NeuralNetworkModelManager.Family.RUBIK;
break;
case LINUX_RK3588_64:
family = NeuralNetworkModelManager.Family.RKNN;
break;
default:
ctx.status(400);
ctx.result("The current platform does not support object detection models");
logger.error("The current platform does not support object detection models");
return;
}
// If adding additional platforms, check platform matches
if (!modelFile.extension().contains(family.extension())) {
ctx.status(400);
ctx.result(
"The uploaded file was not of type '"
+ family.extension()
+ "'. The uploaded file should be a ."
+ family.extension()
+ " file.");
logger.error(
"The uploaded file was not of type '"
+ family.extension()
+ "'. The uploaded file should be a ."
+ family.extension()
+ " file.");
return;
}
Path modelPath =
Paths.get(
ConfigManager.getInstance().getModelsDirectory().toString(), modelFile.filename());
if (modelPath.toFile().exists()) {
ctx.status(400);
ctx.result(
"The model file already exists. Please delete the existing model file before uploading a new one.");
logger.error(
"The model file already exists. Please delete the existing model file before uploading a new one.");
return;
}
try (InputStream modelFileStream = modelFile.content()) {
Files.copy(modelFileStream, modelPath, StandardCopyOption.REPLACE_EXISTING);
}
int idx = modelFile.filename().lastIndexOf('.');
String nickname = modelFile.filename().substring(0, idx);
ModelProperties modelProperties =
new ModelProperties(modelPath, nickname, labels, width, height, family, version);
if (!testMode) {
ObjectDetector objDetector = null;
try {
objDetector =
switch (family) {
case RUBIK -> new RubikModel(modelProperties).load();
case RKNN -> new RknnModel(modelProperties).load();
};
} catch (RuntimeException e) {
ctx.status(400);
ctx.result("Failed to load object detection model: " + e.getMessage());
try {
Files.deleteIfExists(modelPath);
} catch (IOException ex) {
e.addSuppressed(ex);
}
logger.error("Failed to load object detection model", e);
return;
} finally {
// this finally block will run regardless of what happens in try/catch
// please see https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html
// for a summary on how finally works
if (objDetector != null) {
objDetector.release();
}
}
}
ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.addModelProperties(modelProperties);
logger.debug(
ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().toString());
NeuralNetworkModelManager.getInstance().discoverModels();
ctx.status(200).result("Successfully uploaded object detection model");
} catch (Exception e) {
ctx.status(500).result("Error processing files: " + e.getMessage());
logger.error("Error processing new object detection model", e);
}
DataChangeService.getInstance()
.publishEvent(
new OutgoingUIEvent<>(
"fullsettings",
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
}
public static void onExportObjectDetectionModelsRequest(Context ctx) {
logger.info("Exporting Object Detection Models to ZIP Archive");
try {
var zip = ConfigManager.getInstance().getObjectDetectionExportAsZip();
var stream = new FileInputStream(zip);
logger.info("Uploading object detection models with size " + stream.available());
ctx.contentType("application/zip");
ctx.header(
"Content-Disposition",
"attachment; filename=\"photonvision-object-detection-models-export.zip\"");
ctx.result(stream);
ctx.status(200);
} catch (IOException e) {
logger.error("Unable to export object detection models archive, bad recode from zip to byte");
ctx.status(500);
ctx.result("There was an error while exporting the object detection models archive");
}
}
public static void onExportIndividualObjectDetectionModelRequest(Context ctx) {
logger.info("Exporting Individual Object Detection Model");
try {
String modelPath = ctx.queryParam("modelPath");
if (modelPath == null || modelPath.isEmpty()) {
ctx.status(400);
ctx.result("The provided model path was malformed");
logger.error("The provided model path was malformed");
return;
}
File modelFile = NeuralNetworkModelManager.getInstance().exportSingleModel(modelPath);
var stream = new FileInputStream(modelFile);
logger.info("Uploading object detection model with size " + stream.available());
ctx.contentType("application/octet-stream");
ctx.header("Content-Disposition", "attachment; filename=" + modelFile.getName());
ctx.result(stream);
ctx.status(200);
} catch (IOException e) {
logger.error("Unable to export object detection model, " + e);
ctx.status(500);
ctx.result("There was an error while exporting the object detection model");
}
}
public static void onBulkImportObjectDetectionModelRequest(Context ctx) {
var file = ctx.uploadedFile("data");
if (file == null) {
ctx.status(400);
ctx.result(
"No File was sent with the request. Make sure that the object detection zip is sent at the key 'data'");
logger.error(
"No File was sent with the request. Make sure that the object detection zip file is sent at the key 'data'");
return;
}
if (!file.extension().contains("zip")) {
ctx.status(400);
ctx.result(
"The uploaded file was not of type 'zip'. The uploaded file should be a .zip file.");
logger.error(
"The uploaded file was not of type 'zip'. The uploaded file should be a .zip file.");
return;
}
// Create a temp file
var tempFilePath = handleTempFileCreation(file);
if (tempFilePath.isEmpty()) {
ctx.status(500);
ctx.result("There was an error while creating a temporary copy of the file");
logger.error("There was an error while creating a temporary copy of the file");
return;
}
Path tempDir = null;
// Extract .rknn files from zip and move to models directory
try {
tempDir = Files.createTempDirectory("photonvision-od-models");
ZipUtil.unpack(tempFilePath.get(), tempDir.toFile());
Path targetModelsDir = ConfigManager.getInstance().getModelsDirectory().toPath();
// Copy all files from the source models directory to the target models
// directory
try (var stream = Files.list(tempDir)) {
for (Path modelFile : stream.toList()) {
if (Files.isRegularFile(modelFile)
&& !modelFile.getFileName().toString().endsWith(".json")) {
logger.debug("Copying model file: " + modelFile.getFileName());
Files.copy(
modelFile,
Path.of(targetModelsDir.toString(), modelFile.getFileName().toString()),
StandardCopyOption.REPLACE_EXISTING);
}
}
}
logger.info("Successfully copied models from " + tempDir + " to " + targetModelsDir);
} catch (Exception e) {
ctx.status(500);
ctx.result("There was an error while extracting and coyping the object detection models");
logger.error(
"There was an error while extracting and copying the object detection models", e);
return;
}
if (ConfigManager.getInstance()
.saveUploadedNeuralNetworkProperties(
Path.of(tempDir.toString(), "photonvision-object-detection-models.json"))) {
ctx.status(200);
ctx.result("Successfully saved the uploaded object detection models, rebooting...");
logger.info("Successfully saved the uploaded object detection models, rebooting...");
restartProgram();
} else {
ctx.status(500);
ctx.result("There was an error while saving the uploaded object detection models");
logger.error("There was an error while saving the uploaded object detection models");
}
}
private record DeleteObjectDetectionModelRequest(Path modelPath) {}
public static void onDeleteObjectDetectionModelRequest(Context ctx) {
logger.info("Deleting object detection model");
try {
DeleteObjectDetectionModelRequest request =
JacksonUtils.deserialize(ctx.body(), DeleteObjectDetectionModelRequest.class);
if (request.modelPath == null) {
ctx.status(400);
ctx.result("The provided model path was malformed");
logger.error("The provided model path was malformed");
return;
}
if (!request.modelPath.toFile().exists()) {
ctx.status(400);
ctx.result("The provided model path does not exist");
logger.error("The provided model path does not exist");
return;
}
if (!request.modelPath.toFile().delete()) {
ctx.status(500);
ctx.result("Unable to delete the model file");
logger.error("Unable to delete the model file");
return;
}
if (!ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.removeModel(request.modelPath)) {
ctx.status(400);
ctx.result("The model's information was not found in the config");
logger.error("The model's information was not found in the config");
return;
}
NeuralNetworkModelManager.getInstance().discoverModels();
ctx.status(200).result("Successfully deleted object detection model");
} catch (Exception e) {
ctx.status(500);
ctx.result("Error deleting object detection model: " + e.getMessage());
logger.error("Error deleting object detection model", e);
}
DataChangeService.getInstance()
.publishEvent(
new OutgoingUIEvent<>(
"fullsettings",
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
}
private record RenameObjectDetectionModelRequest(Path modelPath, String newName) {}
public static void onRenameObjectDetectionModelRequest(Context ctx) {
try {
RenameObjectDetectionModelRequest request =
JacksonUtils.deserialize(ctx.body(), RenameObjectDetectionModelRequest.class);
if (request.modelPath == null) {
ctx.status(400);
ctx.result("The provided model path was malformed");
logger.error("The provided model path was malformed");
return;
}
if (!request.modelPath.toFile().exists()) {
ctx.status(400);
ctx.result("The provided model path does not exist");
logger.error("The model path: " + request.modelPath + " does not exist");
return;
}
if (request.newName == null || request.newName.isEmpty()) {
ctx.status(400);
ctx.result("The provided new name was malformed");
logger.error("The provided new name was malformed");
return;
}
if (!ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.renameModel(request.modelPath, request.newName)) {
ctx.status(400);
ctx.result("The model's information was not found in the config");
logger.error("The model's information was not found in the config");
return;
}
NeuralNetworkModelManager.getInstance().discoverModels();
ctx.status(200).result("Successfully renamed object detection model");
} catch (Exception e) {
ctx.status(500);
ctx.result("Error renaming object detection model: " + e.getMessage());
logger.error("Error renaming object detection model", e);
return;
}
}
public static void onNukeObjectDetectionModelsRequest(Context ctx) {
logger.info("Attempting to clear object detection models");
try {
NeuralNetworkModelManager.getInstance().clearModels();
NeuralNetworkModelManager.getInstance().extractModels();
ctx.status(200).result("Successfully cleared and reset object detection models");
} catch (Exception e) {
ctx.status(500);
ctx.result("Error clearing object detection models: " + e.getMessage());
logger.error("Error clearing object detection models", e);
}
}
public static void onDeviceRestartRequest(Context ctx) {
ctx.status(HardwareManager.getInstance().restartDevice() ? 204 : 500);
}
private record CameraNicknameChangeRequest(String name, String cameraUniqueName) {}
public static void onCameraNicknameChangeRequest(Context ctx) {
try {
CameraNicknameChangeRequest request =
kObjectMapper.readValue(ctx.body(), CameraNicknameChangeRequest.class);
VisionSourceManager.getInstance()
.vmm
.getModule(request.cameraUniqueName)
.setCameraNickname(request.name);
ctx.status(200);
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).result("Invalid JSON format");
logger.error("Failed to process camera nickname change request", e);
} catch (Exception e) {
ctx.status(500).result("Failed to change camera nickname");
logger.error("Unexpected error while changing camera nickname", e);
}
}
/**
* Get the calibration JSON for a specific observation. Excludes camera image data
*
* This is excluded from UICalibrationCoefficients by default to save bandwidth on large
* calibrations
*/
public static void onCalibrationJsonRequest(Context ctx) {
String cameraUniqueName = ctx.queryParam("cameraUniqueName");
var width = Integer.parseInt(ctx.queryParam("width"));
var height = Integer.parseInt(ctx.queryParam("height"));
var module = VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName);
if (module == null) {
ctx.status(404);
return;
}
CameraCalibrationCoefficients calList =
module.getStateAsCameraConfig().calibrations.stream()
.filter(
it ->
Math.abs(it.unrotatedImageSize.width - width) < 1e-4
&& Math.abs(it.unrotatedImageSize.height - height) < 1e-4)
.findFirst()
.orElse(null);
if (calList == null) {
ctx.status(404);
return;
}
ctx.json(calList);
ctx.status(200);
}
private record CalibrationRemoveRequest(int width, int height, String cameraUniqueName) {}
public static void onCalibrationRemoveRequest(Context ctx) {
try {
CalibrationRemoveRequest request =
kObjectMapper.readValue(ctx.body(), CalibrationRemoveRequest.class);
logger.info(
"Attempting to remove calibration for camera: "
+ request.cameraUniqueName
+ " with a resolution of "
+ request.width
+ "x"
+ request.height);
VisionSourceManager.getInstance()
.vmm
.getModule(request.cameraUniqueName)
.removeCalibrationFromConfig(new Size(request.width, request.height));
ctx.status(200);
ctx.result(
"Successfully removed calibration for resolution: "
+ request.width
+ "x"
+ request.height);
logger.info(
"Successfully removed calibration for resolution: "
+ request.width
+ "x"
+ request.height);
} catch (JsonProcessingException e) {
ctx.status(400).result("Invalid JSON format");
logger.error("Failed to process calibration removed request", e);
} catch (Exception e) {
ctx.status(500).result("Failed to removed calibration");
logger.error("Unexpected error while attempting to remove calibration", e);
}
}
public static void onCalibrationSnapshotRequest(Context ctx) {
String cameraUniqueName = ctx.queryParam("cameraUniqueName");
var width = Integer.parseInt(ctx.queryParam("width"));
var height = Integer.parseInt(ctx.queryParam("height"));
Integer observationIdx = Integer.parseInt(ctx.queryParam("snapshotIdx"));
CameraCalibrationCoefficients calList =
VisionSourceManager.getInstance()
.vmm
.getModule(cameraUniqueName)
.getStateAsCameraConfig()
.calibrations
.stream()
.filter(
it ->
Math.abs(it.unrotatedImageSize.width - width) < 1e-4
&& Math.abs(it.unrotatedImageSize.height - height) < 1e-4)
.findFirst()
.orElse(null);
if (calList == null || calList.observations.size() < observationIdx) {
ctx.status(404);
return;
}
// encode as jpeg to save even more space. reduces size of a 1280p image from 300k to 25k
var mat = calList.observations.get(observationIdx).annotateImage();
if (mat == null) {
ctx.status(404);
return;
}
var jpegBytes = new MatOfByte();
Imgcodecs.imencode(".jpg", mat, jpegBytes, new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 60));
ctx.result(jpegBytes.toArray());
mat.release();
jpegBytes.release();
ctx.status(200);
}
public static void onCalibrationExportRequest(Context ctx) {
String cameraUniqueName = ctx.queryParam("cameraUniqueName");
var width = Integer.parseInt(ctx.queryParam("width"));
var height = Integer.parseInt(ctx.queryParam("height"));
var cc =
VisionSourceManager.getInstance().vmm.getModule(cameraUniqueName).getStateAsCameraConfig();
CameraCalibrationCoefficients calList =
cc.calibrations.stream()
.filter(
it ->
Math.abs(it.unrotatedImageSize.width - width) < 1e-4
&& Math.abs(it.unrotatedImageSize.height - height) < 1e-4)
.findFirst()
.orElse(null);
if (calList == null) {
ctx.status(404);
return;
}
var filename = "photon_calibration_" + cc.uniqueName + "_" + width + "x" + height + ".json";
ctx.contentType("application/zip");
ctx.header("Content-Disposition", "attachment; filename=\"" + filename + "\"");
ctx.json(calList);
ctx.status(200);
}
public static void onImageSnapshotsRequest(Context ctx) {
var snapshots = new ArrayList>();
var cameraDirs = ConfigManager.getInstance().getImageSavePath().toFile().listFiles();
if (cameraDirs != null) {
try {
for (File cameraDir : cameraDirs) {
var cameraSnapshots = cameraDir.listFiles();
if (cameraSnapshots == null) continue;
String cameraUniqueName = cameraDir.getName();
for (File snapshot : cameraSnapshots) {
var snapshotData = new HashMap();
var bufferedImage = ImageIO.read(snapshot);
var buffer = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "jpg", buffer);
byte[] data = buffer.toByteArray();
snapshotData.put("snapshotName", snapshot.getName());
snapshotData.put("cameraUniqueName", cameraUniqueName);
snapshotData.put("snapshotData", data);
snapshots.add(snapshotData);
}
}
} catch (IOException e) {
ctx.status(500);
ctx.result("Unable to read saved images");
}
}
ctx.status(200);
ctx.json(snapshots);
}
public static void onCameraCalibImagesRequest(Context ctx) {
try {
HashMap>>> snapshots =
new HashMap<>();
var cameraDirs = ConfigManager.getInstance().getCalibDir().toFile().listFiles();
if (cameraDirs != null) {
var camData = new HashMap>>();
for (var cameraDir : cameraDirs) {
var resolutionDirs = cameraDir.listFiles();
if (resolutionDirs == null) continue;
for (var resolutionDir : resolutionDirs) {
var calibImages = resolutionDir.listFiles();
if (calibImages == null) continue;
var resolutionImages = new ArrayList>();
for (var calibImg : calibImages) {
var snapshotData = new HashMap();
var bufferedImage = ImageIO.read(calibImg);
var buffer = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", buffer);
byte[] data = buffer.toByteArray();
snapshotData.put("snapshotData", data);
snapshotData.put("snapshotFilename", calibImg.getName());
resolutionImages.add(snapshotData);
}
camData.put(resolutionDir.getName(), resolutionImages);
}
var cameraName = cameraDir.getName();
snapshots.put(cameraName, camData);
}
}
ctx.json(snapshots);
} catch (Exception e) {
ctx.status(500);
ctx.result("An error occurred while getting calib data");
logger.error("An error occurred while getting calib data", e);
}
}
/**
* Create a temporary file using the UploadedFile from Javalin.
*
* @param file the uploaded file.
* @return Temporary file. Empty if the temporary file was unable to be created.
*/
private static Optional handleTempFileCreation(UploadedFile file) {
var tempFilePath =
new File(Path.of(System.getProperty("java.io.tmpdir"), file.filename()).toString());
boolean makeDirsRes = tempFilePath.getParentFile().mkdirs();
if (!makeDirsRes && !(tempFilePath.getParentFile().exists())) {
logger.error(
"There was an error while creating "
+ tempFilePath.getAbsolutePath()
+ "! Exists: "
+ tempFilePath.getParentFile().exists());
return Optional.empty();
}
try {
FileUtils.copyInputStreamToFile(file.content(), tempFilePath);
} catch (IOException e) {
logger.error(
"There was an error while copying "
+ file.filename()
+ " to the temp file "
+ tempFilePath.getAbsolutePath());
return Optional.empty();
}
return Optional.of(tempFilePath);
}
/**
* Restart the running program. Note that this doesn't actually restart the program itself,
* instead, it relies on systemd or an equivalent.
*/
private static void restartProgram() {
TimedTaskManager.getInstance()
.addOneShotTask(
() -> {
if (Platform.isLinux()) {
try {
new ShellExec().executeBashCommand("systemctl restart photonvision.service");
} catch (IOException e) {
logger.error("Could not restart device!", e);
System.exit(0);
}
} else {
System.exit(0);
}
},
0);
}
public static void onNukeConfigDirectory(Context ctx) {
ConfigManager.getInstance().setWriteTaskEnabled(false);
ConfigManager.getInstance().disableFlushOnShutdown();
Logger.closeAllLoggers();
if (ConfigManager.nukeConfigDirectory()) {
ctx.status(200);
ctx.result("Successfully nuked config dir");
restartProgram();
} else {
ctx.status(500);
ctx.result("There was an error while nuking the config directory");
}
}
public static void onNukeOneCamera(Context ctx) {
try {
CommonCameraUniqueName request =
kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class);
logger.warn("Deleting camera name " + request.cameraUniqueName);
var cameraDir =
ConfigManager.getInstance()
.getCalibrationImageSavePath(request.cameraUniqueName)
.toFile();
if (cameraDir.exists()) {
FileUtils.deleteDirectory(cameraDir);
}
VisionSourceManager.getInstance().deleteVisionSource(request.cameraUniqueName);
ctx.status(200);
} catch (IOException 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);
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;
}
}
private record AssignUnmatchedCamera(PVCameraInfo cameraInfo) {}
public static void onAssignUnmatchedCameraRequest(Context ctx) {
logger.info(ctx.queryString());
try {
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;
}
}
public static void onUnassignCameraRequest(Context ctx) {
logger.info(ctx.queryString());
try {
CommonCameraUniqueName request =
kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class);
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;
}
}
}