Big scary buttons (#1471)

This commit is contained in:
Matt
2024-10-24 20:48:02 -07:00
committed by GitHub
parent 9b61ed156c
commit 385059c233
14 changed files with 375 additions and 24 deletions

View File

@@ -76,6 +76,24 @@ if (!is_demo) {
}
}
/* Custom scrollbar styles */
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: #232c37;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background-color: #e4c33c;
}
.main-container {
background-color: #232c37;
padding: 0 !important;

View File

@@ -79,10 +79,10 @@ onBeforeUnmount(() => {
<div class="stream-container" :style="containerStyle">
<img :src="loadingImage" class="stream-loading" />
<img
:id="id"
class="stream-video"
ref="mjpgStream"
v-show="streamSrc !== emptyStreamSrc"
:id="id"
ref="mjpgStream"
class="stream-video"
crossorigin="anonymous"
:src="streamSrc"
:alt="streamDesc"

View File

@@ -3,8 +3,9 @@ import PvSelect from "@/components/common/pv-select.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, ref, watchEffect } from "vue";
import { computed, inject, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import axios from "axios";
const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
@@ -108,6 +109,49 @@ watchEffect(() => {
// Reset temp settings on remote camera settings change
resetTempSettingsStruct();
});
const showDeleteCamera = ref(false);
const address = inject<string>("backendHost");
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
};
const yesDeleteMySettingsText = ref("");
const deleteThisCamera = () => {
const payload = {
cameraUniqueName: useCameraSettingsStore().cameraUniqueNames[useStateStore().currentCameraIndex]
};
axios
.post("/utils/nukeOneCamera", payload)
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the delete command. Waiting for backend to start back up",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to delete 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"
});
}
});
showDeleteCamera.value = false;
};
</script>
<template>
@@ -144,17 +188,82 @@ watchEffect(() => {
:select-cols="8"
/>
<br />
<v-btn
class="mt-2 mb-3"
style="width: 100%"
small
color="secondary"
:disabled="!settingsHaveChanged()"
@click="saveCameraSettings"
>
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
<v-row>
<v-col cols="6">
<v-btn
class="mt-2 mb-3"
style="width: 100%"
small
color="secondary"
:disabled="!settingsHaveChanged()"
@click="saveCameraSettings"
>
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
</v-col>
<v-col cols="6">
<v-btn class="mt-2 mb-3" style="width: 100%" small color="red" @click="() => (showDeleteCamera = true)">
<v-icon left> mdi-bomb </v-icon>
Delete Camera
</v-btn>
</v-col>
</v-row>
</div>
<v-dialog v-model="showDeleteCamera" dark width="1500">
<v-card dark class="dialog-container pa-6" color="primary" flat>
<v-card-title
>Delete camera "{{ useCameraSettingsStore().cameraNames[useStateStore().currentCameraIndex] }}"</v-card-title
>
<v-row class="pl-3 align-center pa-6">
<v-col cols="12" md="6">
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
<v-divider class="mt-4 mb-4" />
<v-row class="pl-3 align-center pa-6">
<v-col>
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + useCameraSettingsStore().currentCameraName + '&quot;:'"
:label-cols="12"
:input-cols="12"
/>
</v-col>
<v-btn
color="red"
:disabled="
yesDeleteMySettingsText.toLowerCase() !== useCameraSettingsStore().currentCameraName.toLowerCase()
"
@click="deleteThisCamera"
>
<v-icon left class="open-icon"> mdi-skull </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
</v-btn>
</v-row>
</v-card>
</v-dialog>
</v-card>
</template>
<style scoped>
.v-divider {
border-color: white !important;
}
</style>

View File

@@ -201,6 +201,39 @@ const handleSettingsImport = () => {
importType.value = -1;
importFile.value = null;
};
const showFactoryReset = ref(false);
const expected = "Delete Everything";
const yesDeleteMySettingsText = ref("");
const nukePhotonConfigDirectory = () => {
axios
.post("/utils/nukeConfigDirectory")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the reset command. Waiting for backend to start back up",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to reset the device.",
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"
});
}
});
showFactoryReset.value = false;
};
</script>
<template>
@@ -322,11 +355,85 @@ const handleSettingsImport = () => {
</v-btn>
</v-col>
</v-row>
<v-divider style="margin: 12px 0" />
<v-row>
<v-col cols="12">
<v-btn color="red" @click="() => (showFactoryReset = true)">
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-icon">
{{
$vuetify.breakpoint.mdAndUp
? "Factory Reset PhotonVision and delete EVERYTHING (big scary button)"
: "Factory Reset PhotonVision"
}}
</span>
</v-btn>
</v-col>
</v-row>
</div>
<v-dialog v-model="showFactoryReset" width="1500" dark>
<v-card dark class="dialog-container pa-6" color="primary" flat>
<v-card-title>
<span class="open-label">
<v-icon right color="red" class="open-icon">mdi-nuke</v-icon>
Factory Reset PhotonVision
<v-icon right color="red" class="open-icon">mdi-nuke</v-icon>
</span>
</v-card-title>
<v-row class="pl-3 align-center pa-6">
<v-col cols="12" md="6">
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
<v-divider class="mt-4 mb-4" />
<v-row class="pl-3 align-center pa-6">
<v-col>
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + expected + '&quot;:'"
:label-cols="2"
:input-cols="10"
/>
</v-col>
</v-row>
<v-btn
color="red"
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
@click="nukePhotonConfigDirectory"
>
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-label">
{{ $vuetify.breakpoint.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
</span>
</v-btn>
</v-card>
</v-dialog>
</v-card>
</template>
<style scoped>
.dialog-container {
min-height: 300px !important;
}
.v-divider {
border-color: white !important;
}

View File

@@ -51,6 +51,9 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
cameraNames(): string[] {
return this.cameras.map((c) => c.nickname);
},
cameraUniqueNames(): string[] {
return this.cameras.map((c) => c.nickname);
},
currentCameraName(): string {
return this.cameraNames[useStateStore().currentCameraIndex];
},

View File

@@ -54,6 +54,7 @@ public class ConfigManager {
// special case flag to disable flushing settings to disk at shutdown. Avoids the jvm shutdown
// hook overwriting the settings we just uploaded
private boolean flushOnShutdown = true;
private boolean allowWriteTask = false;
enum ConfigSaveStrategy {
SQL,
@@ -135,6 +136,10 @@ public class ConfigManager {
}
}
public static boolean nukeConfigDirectory() {
return FileUtils.deleteDirectory(getRootFolder());
}
public static boolean saveUploadedSettingsZip(File uploadPath) {
// Unpack to /tmp/something/photonvision
var folderPath = Path.of(System.getProperty("java.io.tmpdir"), "photonvision").toFile();
@@ -142,7 +147,9 @@ public class ConfigManager {
ZipUtil.unpack(uploadPath, folderPath);
// Nuke the current settings directory
FileUtils.deleteDirectory(getRootFolder());
if (!nukeConfigDirectory()) {
return false;
}
// If there's a cameras folder in the upload, we know we need to import from the
// old style
@@ -250,7 +257,14 @@ public class ConfigManager {
return imgFilePath.toPath();
}
public Path getCalibrationImageSavePath(Size frameSize, String uniqueCameraName) {
public Path getCalibrationImageSavePath(String uniqueCameraName) {
var imgFilePath =
Path.of(configDirectoryFile.toString(), "calibration", uniqueCameraName).toFile();
if (!imgFilePath.exists()) imgFilePath.mkdirs();
return imgFilePath.toPath();
}
public Path getCalibrationImageSavePathWithRes(Size frameSize, String uniqueCameraName) {
var imgFilePath =
Path.of(
configDirectoryFile.toString(),
@@ -301,7 +315,9 @@ public class ConfigManager {
private void saveAndWriteTask() {
// Only save if 1 second has past since the request was made
while (!Thread.currentThread().isInterrupted()) {
if (saveRequestTimestamp > 0 && (System.currentTimeMillis() - saveRequestTimestamp) > 1000L) {
if (saveRequestTimestamp > 0
&& (System.currentTimeMillis() - saveRequestTimestamp) > 1000L
&& allowWriteTask) {
saveRequestTimestamp = -1;
logger.debug("Saving to disk...");
saveToDisk();
@@ -330,6 +346,11 @@ public class ConfigManager {
this.flushOnShutdown = false;
}
/** Prevent pending automatic saves */
public void setWriteTaskEnabled(boolean enabled) {
this.allowWriteTask = enabled;
}
public void onJvmExit() {
if (flushOnShutdown) {
logger.info("Force-flushing settings...");

View File

@@ -114,6 +114,16 @@ public class PhotonConfiguration {
cameraConfigurations.put(name, config);
}
/**
* Delete a camera by its unique name
*
* @param name The camera name (usually unique name)
* @return True if the camera configuration was removed
*/
public boolean removeCameraConfig(String name) {
return cameraConfigurations.remove(name) != null;
}
public Map<String, Object> toHashMap() {
Map<String, Object> map = new HashMap<>();
var settingsSubmap = new HashMap<String, Object>();

View File

@@ -349,6 +349,19 @@ public class SqlConfigProvider extends ConfigProvider {
private void saveCameras(Connection conn) {
try {
// Delete all cameras we don't need anymore
String deleteExtraCamsString =
String.format(
"DELETE FROM %s WHERE %s not in (%s)",
Tables.CAMERAS,
Columns.CAM_UNIQUE_NAME,
config.getCameraConfigurations().keySet().stream()
.map(it -> "\"" + it + "\"")
.collect(Collectors.joining(", ")));
var stmt = conn.createStatement();
stmt.executeUpdate(deleteExtraCamsString);
// Replace this camera's row with the new settings
var sqlString =
String.format(
@@ -388,6 +401,7 @@ public class SqlConfigProvider extends ConfigProvider {
statement.executeUpdate();
}
} catch (SQLException | IOException e) {
logger.error("Err saving cameras", e);
try {

View File

@@ -123,6 +123,11 @@ public class Logger {
currentAppenders.add(new FileLogAppender(logFilePath));
}
public static void closeAllLoggers() {
currentAppenders.forEach(LogAppender::shutdown);
currentAppenders.clear();
}
public static void cleanLogs(Path folderToClean) {
File[] logs = folderToClean.toFile().listFiles();
if (logs == null) return;
@@ -284,6 +289,9 @@ public class Logger {
private interface LogAppender {
void log(String message, LogLevel level);
/** Release any file or other resources currently held by the Logger */
default void shutdown() {}
}
private static class ConsoleLogAppender implements LogAppender {
@@ -343,5 +351,16 @@ public class Logger {
// Nothing to do - no stream available for writing
}
}
@Override
public void shutdown() {
try {
out.close();
out = null;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

View File

@@ -40,7 +40,7 @@ public class FileUtils {
private static final Set<PosixFilePermission> allReadWriteExecutePerms =
new HashSet<>(Arrays.asList(PosixFilePermission.values()));
public static void deleteDirectory(Path path) {
public static boolean deleteDirectory(Path path) {
try {
var files = Files.walk(path);
@@ -53,8 +53,11 @@ public class FileUtils {
// close the stream
files.close();
return true;
} catch (IOException e) {
logger.error("Exception deleting files in " + path + "!", e);
return false;
}
}

View File

@@ -390,7 +390,7 @@ public class VisionModule {
var ret =
pipelineManager.calibration3dPipeline.tryCalibration(
ConfigManager.getInstance()
.getCalibrationImageSavePath(
.getCalibrationImageSavePathWithRes(
pipelineManager.calibration3dPipeline.getSettings().resolution,
visionSource.getCameraConfiguration().uniqueName));
pipelineManager.setCalibrationMode(false);

View File

@@ -255,7 +255,8 @@ public class Calibrate3dPipeTest {
var cal =
calibration3dPipeline.tryCalibration(
ConfigManager.getInstance().getCalibrationImageSavePath(imgRes, "Calibration_Test"));
ConfigManager.getInstance()
.getCalibrationImageSavePathWithRes(imgRes, "Calibration_Test"));
calibration3dPipeline.finishCalibration();
// visuallyDebugDistortion(directoryListing, imgRes, cal );

View File

@@ -94,16 +94,18 @@ public class RequestHandler {
return;
}
ConfigManager.getInstance().setWriteTaskEnabled(false);
ConfigManager.getInstance().disableFlushOnShutdown();
// We want to delete the -whole- zip file, so we need to teardown loggers for now
logger.info("Writing new settings zip (logs may be truncated)...");
Logger.closeAllLoggers();
if (ConfigManager.saveUploadedSettingsZip(tempFilePath.get())) {
ctx.status(200);
ctx.result("Successfully saved the uploaded settings zip, rebooting...");
logger.info("Successfully saved the uploaded settings zip, rebooting...");
ConfigManager.getInstance().disableFlushOnShutdown();
restartProgram();
} else {
ctx.status(500);
ctx.result("There was an error while saving the uploaded zip file");
logger.error("There was an error while saving the uploaded zip file");
}
}
@@ -771,4 +773,46 @@ public class RequestHandler {
},
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 {
var payload = kObjectMapper.readTree(ctx.bodyInputStream());
var name = payload.get("cameraUniqueName").asText();
logger.warn("Deleting camera name " + name);
var cameraDir = ConfigManager.getInstance().getCalibrationImageSavePath(name).toFile();
if (cameraDir.exists()) {
FileUtils.deleteDirectory(cameraDir);
}
// prevent -anyone- else from writing camera configs -- but flush first
ConfigManager.getInstance().saveToDisk();
ConfigManager.getInstance().setWriteTaskEnabled(false);
ConfigManager.getInstance().disableFlushOnShutdown();
// remove the config from the global config and force-flush
ConfigManager.getInstance().getConfig().removeCameraConfig(name);
ConfigManager.getInstance().saveToDisk();
ctx.status(200);
restartProgram();
} catch (IOException e) {
// todo
logger.error("asdf", e);
ctx.status(500);
}
}
}

View File

@@ -134,6 +134,8 @@ public class Server {
app.get("/api/utils/getImageSnapshots", RequestHandler::onImageSnapshotsRequest);
app.get("/api/utils/getCalSnapshot", RequestHandler::onCalibrationSnapshotRequest);
app.get("/api/utils/getCalibrationJSON", RequestHandler::onCalibrationExportRequest);
app.post("/api/utils/nukeConfigDirectory", RequestHandler::onNukeConfigDirectory);
app.post("/api/utils/nukeOneCamera", RequestHandler::onNukeOneCamera);
// Calibration
app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest);