Compare commits

...

14 Commits

Author SHA1 Message Date
Matt
6444ae884d Restart server on general settings change (#1137) 2024-01-08 13:02:31 -05:00
Matt
02df8aa925 Bound check sliders in web UI (#1134)
* Bound check sliders

* Update pv-range-slider.vue

---------

Co-authored-by: Sriman Achanta <68172138+srimanachanta@users.noreply.github.com>
2024-01-08 09:27:54 -05:00
Sriman Achanta
4d458198c1 Fix bug with saving general settings not using tempSettingsStruct and using store values instead (#1131)
residual from #1075
closes #1129
2024-01-08 08:32:56 -05:00
Craig Schardt
5cbb507c87 Fix OV9281 typo in UI (#1130) 2024-01-07 21:15:35 -05:00
Matt
e71ce899d6 Add mrcal packges to install script (#1128)
Closes #1124

Also bumps opencv to 4.6 to match our libcamera driver
2024-01-07 20:18:48 -05:00
Matt
60220f38e6 Add cameralensmodel enum (#1126)
Preparing for future lens models like splined stereo
2024-01-06 23:17:55 -07:00
Matt
bf5e8dc81b Bump wpilib to 2024.1.1 (#1127)
Does not yet include test mode
2024-01-07 00:44:28 -05:00
Craig Schardt
b8a6a5d56a Install NetworkManager on Ubuntu distributions (fixes #1052) (#1070)
Add the following args to the install script:

Syntax: sudo ./install.sh [-h|m|n|q]
  options:
  -h        Display this help message.
  -m        Install and configure NetworkManager (Ubuntu only).
  -n        Disable networking. This will also prevent installation of NetworkManager.
  -q        Silent install, automatically accepts all defaults. For non-interactive use.
2024-01-06 09:45:56 -05:00
Sriman Achanta
bf156f544e Update HTTP based settings when new fullsettings are emited (#1122)
Previously, if someone were changing network or camera settings while the backend sent an update request, the frontend wouldn't update the UI until the HTTP request was sent, likely leading to an error or confusion, now, values will be reset whenever new settings are sent. It also checks that settings were changed before allowing the user to click the save button.
2024-01-06 09:43:29 -05:00
Chris Gerth
851f2e4e68 Update Python rawBytes parsing (#1119)
*  data updates to capture multiple rawBytes packets associated with serde updates from late this past month

---------

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2024-01-06 06:17:06 -06:00
Matt
4068025572 Check all new prop names not just exposure time (#1080)
Fixes v4l renaming prop names and OV9281 exposure min/max being wrong by introducing new UI control
2024-01-05 23:40:06 -05:00
Craig Schardt
f37a0d0300 Add database migrations (fixes #1046) (#1065)
Add database version pragma to SQL database to automatically migrate between versions
2024-01-05 21:02:47 -05:00
Chris Gerth
276fc6178e Apparently we need to get better about longDescription? (#1117)
Maybe make pypi happy(ier)
2024-01-05 18:03:55 -05:00
Chris Gerth
107a0f3a8b code checkout fixups (#1116) 2024-01-05 15:07:50 -06:00
49 changed files with 1022 additions and 498 deletions

View File

@@ -17,11 +17,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
sparse-checkout-cone-mode: false
fetch-tags: true
fetch-depth: 99999
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5

View File

@@ -2,7 +2,7 @@ plugins {
id "com.diffplug.spotless" version "6.22.0"
id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "edu.wpi.first.GradleRIO" version "2024.1.1-beta-4"
id "edu.wpi.first.GradleRIO" version "2024.1.1"
id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.4' apply false
}
@@ -22,7 +22,7 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2024.1.1-beta-4-35-g141241d"
wpilibVersion = "2024.1.1"
wpimathVersion = wpilibVersion
openCVversion = "4.8.0-2"
joglVersion = "2.4.0-rc-20200307"

View File

@@ -3,22 +3,79 @@ 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 { ref, watchEffect } from "vue";
import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
const currentFov = ref();
const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
quirksToChange: Object.assign({}, useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks)
});
const arducamSelectWrapper = computed<number>({
get: () => {
if (tempSettingsStruct.value.quirksToChange.ArduOV9281) return 1;
else if (tempSettingsStruct.value.quirksToChange.ArduOV2311) return 2;
else return 0;
},
set: (v) => {
switch (v) {
case 1:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = true;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
break;
case 2:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = true;
break;
default:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
break;
}
}
});
const currentCameraIsArducam = computed<boolean>(
() => useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks.ArduCamCamera
);
const settingsHaveChanged = (): boolean => {
const a = tempSettingsStruct.value;
const b = useCameraSettingsStore().currentCameraSettings;
for (const q in ValidQuirks) {
if (a.quirksToChange[q] != b.cameraQuirks.quirks[q]) return true;
}
return a.fov != b.fov.value;
};
const resetTempSettingsStruct = () => {
tempSettingsStruct.value.fov = useCameraSettingsStore().currentCameraSettings.fov.value;
tempSettingsStruct.value.quirksToChange = Object.assign(
{},
useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks
);
};
const saveCameraSettings = () => {
useCameraSettingsStore()
.updateCameraSettings({ fov: currentFov.value }, false)
.updateCameraSettings(tempSettingsStruct.value)
.then((response) => {
useCameraSettingsStore().currentCameraSettings.fov.value = currentFov.value;
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
// Update the local settings cause the backend checked their validity. Assign is to deref value
useCameraSettingsStore().currentCameraSettings.fov.value = tempSettingsStruct.value.fov;
useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks = Object.assign(
{},
tempSettingsStruct.value.quirksToChange
);
})
.catch((error) => {
currentFov.value = useCameraSettingsStore().currentCameraSettings.fov.value;
resetTempSettingsStruct();
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
@@ -39,7 +96,8 @@ const saveCameraSettings = () => {
};
watchEffect(() => {
currentFov.value = useCameraSettingsStore().currentCameraSettings.fov.value;
// Reset temp settings on remote camera settings change
resetTempSettingsStruct();
});
</script>
@@ -52,15 +110,9 @@ watchEffect(() => {
label="Camera"
:items="useCameraSettingsStore().cameraNames"
:select-cols="8"
@input="
(args) => {
currentFov = useCameraSettingsStore().cameras[args].fov.value;
useCameraSettingsStore().setCurrentCameraIndex(args);
}
"
/>
<pv-number-input
v-model="currentFov"
v-model="tempSettingsStruct.fov"
:tooltip="
!useCameraSettingsStore().currentCameraSettings.fov.managedByVendor
? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.'
@@ -70,12 +122,24 @@ watchEffect(() => {
:disabled="useCameraSettingsStore().currentCameraSettings.fov.managedByVendor"
:label-cols="4"
/>
<pv-select
v-show="currentCameraIsArducam"
v-model="arducamSelectWrapper"
label="Arducam Model"
:items="[
{ name: 'None', value: 0, disabled: true },
{ name: 'OV9281', value: 1 },
{ name: 'OV2311', value: 2 }
]"
:select-cols="8"
/>
<br />
<v-btn
style="margin-top: 10px"
class="mt-2 mb-3"
style="width: 100%"
small
color="secondary"
:disabled="currentFov === useCameraSettingsStore().currentCameraSettings.fov.value"
:disabled="!settingsHaveChanged()"
@click="saveCameraSettings"
>
<v-icon left> mdi-content-save </v-icon>

View File

@@ -40,12 +40,21 @@ const localValue = computed<[number, number]>({
}
});
const changeFromSlot = (v: number, i: number) => {
const changeFromSlot = (v: string, i: number) => {
// v comes in as a string, not a number, for some reason
// if v is undefined, take a guess and set it to 0
const val = Math.max(props.min, Math.min(parseFloat(v) || 0, props.max));
// localValue.value must be replaced for a reactive change to take place
const temp = localValue.value;
temp[i] = v;
temp[i] = val;
localValue.value = temp;
};
const checkNumberRange = (v: string): boolean => {
const val: number = parseFloat(v);
return isFinite(val) && val >= props.min && val <= props.max;
};
</script>
<template>
@@ -79,6 +88,7 @@ const changeFromSlot = (v: number, i: number) => {
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 0)"
@@ -95,6 +105,7 @@ const changeFromSlot = (v: number, i: number) => {
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 1)"

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { computed, ref } from "vue";
import { computed, ref, watchEffect } from "vue";
import PvInput from "@/components/common/pv-input.vue";
import PvRadio from "@/components/common/pv-radio.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
@@ -8,9 +8,15 @@ import PvSelect from "@/components/common/pv-select.vue";
import { NetworkConnectionType, type NetworkSettings } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
const settingsValid = ref(true);
// Copy object to remove reference to store
const tempSettingsStruct = ref<NetworkSettings>(Object.assign({}, useSettingsStore().network));
const resetTempSettingsStruct = () => {
tempSettingsStruct.value = Object.assign({}, useSettingsStore().network);
};
const settingsValid = ref(true);
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
// Check if it is a valid team number between 1-9999
const teamNumberRegex = /^[1-9][0-9]{0,3}$/;
@@ -62,18 +68,33 @@ const settingsHaveChanged = (): boolean => {
const saveGeneralSettings = () => {
const changingStaticIp = useSettingsStore().network.connectionType === NetworkConnectionType.Static;
// Update with new values
Object.assign(useSettingsStore().network, tempSettingsStruct.value);
// replace undefined members with empty strings for backend
const payload = {
connectionType: tempSettingsStruct.value.connectionType,
hostname: tempSettingsStruct.value.hostname,
networkManagerIface: tempSettingsStruct.value.networkManagerIface || "",
ntServerAddress: tempSettingsStruct.value.ntServerAddress,
runNTServer: tempSettingsStruct.value.runNTServer,
setDHCPcommand: tempSettingsStruct.value.setDHCPcommand || "",
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
staticIp: tempSettingsStruct.value.staticIp
};
useSettingsStore()
.saveGeneralSettings()
.updateGeneralSettings(payload)
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = Object.assign({}, tempSettingsStruct.value);
})
.catch((error) => {
resetTempSettingsStruct();
if (error.response) {
if (error.status === 504 || changingStaticIp) {
useStateStore().showSnackbarMessage({
@@ -106,6 +127,11 @@ const currentNetworkInterfaceIndex = computed<number>({
get: () => useSettingsStore().networkInterfaceNames.indexOf(useSettingsStore().network.networkManagerIface || ""),
set: (v) => (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
});
watchEffect(() => {
// Reset temp settings on remote network settings change
resetTempSettingsStruct();
});
</script>
<template>

View File

@@ -3,7 +3,7 @@ import type {
CalibrationBoardTypes,
CameraCalibrationResult,
CameraSettings,
ConfigurableCameraSettings,
CameraSettingsChangeRequest,
Resolution,
RobotOffsetType,
VideoFormat
@@ -103,21 +103,17 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
isCSICamera: d.isCSICamera,
pipelineNicknames: d.pipelineNicknames,
currentPipelineIndex: d.currentPipelineIndex,
pipelineSettings: d.currentPipelineSettings
pipelineSettings: d.currentPipelineSettings,
cameraQuirks: d.cameraQuirks
}));
},
/**
* Update the configurable camera settings.
*
* @param data camera settings to save.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera.
*/
updateCameraSettings(
data: ConfigurableCameraSettings,
updateStore = true,
cameraIndex: number = useStateStore().currentCameraIndex
) {
updateCameraSettings(data: CameraSettingsChangeRequest, cameraIndex: number = useStateStore().currentCameraIndex) {
// The camera settings endpoint doesn't actually require all data, instead, it needs key data such as the FOV
const payload = {
settings: {
@@ -125,9 +121,6 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
},
index: cameraIndex
};
if (updateStore) {
this.currentCameraSettings.fov.value = data.fov;
}
return axios.post("/settings/camera", payload);
},
/**

View File

@@ -105,19 +105,7 @@ export const useSettingsStore = defineStore("settings", {
this.network = data.networkSettings;
this.currentFieldLayout = data.atfl;
},
saveGeneralSettings() {
const payload: Required<ConfigurableNetworkSettings> = {
connectionType: this.network.connectionType,
hostname: this.network.hostname,
networkManagerIface: this.network.networkManagerIface || "",
ntServerAddress: this.network.ntServerAddress,
runNTServer: this.network.runNTServer,
setDHCPcommand: this.network.setDHCPcommand || "",
setStaticCommand: this.network.setStaticCommand || "",
shouldManage: this.network.shouldManage,
shouldPublishProto: this.network.shouldPublishProto,
staticIp: this.network.staticIp
};
updateGeneralSettings(payload: Required<ConfigurableNetworkSettings>) {
return axios.post("/settings/general", payload);
},
/**

View File

@@ -135,8 +135,25 @@ export interface CameraCalibrationResult {
calobjectWarp?: number[];
}
export interface ConfigurableCameraSettings {
fov: number;
export enum ValidQuirks {
AWBGain = "AWBGain",
AdjustableFocus = "AdjustableFocus",
ArduOV9281 = "ArduOV9281",
ArduOV2311 = "ArduOV2311",
ArduCamCamera = "ArduCamCamera",
CompletelyBroken = "CompletelyBroken",
FPSCap100 = "FPSCap100",
Gain = "Gain",
PiCam = "PiCam",
StickyFPS = "StickyFPS"
}
export interface QuirkyCamera {
baseName: string;
usbVid: number;
usbPid: number;
displayName: string;
quirks: Record<ValidQuirks, boolean>;
}
export interface CameraSettings {
@@ -159,15 +176,22 @@ export interface CameraSettings {
currentPipelineIndex: number;
pipelineNicknames: string[];
pipelineSettings: ActivePipelineSettings;
cameraQuirks: QuirkyCamera;
isCSICamera: boolean;
}
export interface CameraSettingsChangeRequest {
fov: number;
quirksToChange: Record<ValidQuirks, boolean>;
}
export const PlaceholderCameraSettings: CameraSettings = {
nickname: "Placeholder Camera",
uniqueName: "Placeholder Name",
fov: {
value: 70,
managedByVendor: true
managedByVendor: false
},
stream: {
inputPort: 0,
@@ -233,6 +257,24 @@ export const PlaceholderCameraSettings: CameraSettings = {
lastPipelineIndex: 0,
currentPipelineIndex: 0,
pipelineSettings: DefaultAprilTagPipelineSettings,
cameraQuirks: {
displayName: "Blank 1",
baseName: "Blank 2",
usbVid: -1,
usbPid: -1,
quirks: {
AWBGain: false,
AdjustableFocus: false,
ArduOV9281: false,
ArduOV2311: false,
ArduCamCamera: false,
CompletelyBroken: false,
FPSCap100: false,
Gain: false,
PiCam: false,
StickyFPS: false
}
},
isCSICamera: false
};

View File

@@ -4,7 +4,8 @@ import type {
LightingSettings,
LogLevel,
MetricData,
NetworkSettings
NetworkSettings,
QuirkyCamera
} from "@/types/SettingTypes";
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import type { AprilTagFieldLayout, PipelineResult } from "@/types/PhotonTrackingTypes";
@@ -56,6 +57,7 @@ export interface WebsocketCameraSettingsUpdate {
outputStreamPort: number;
pipelineNicknames: string[];
videoFormatList: WebsocketVideoFormat;
cameraQuirks: QuirkyCamera;
}
export interface WebsocketNTUpdate {
connected: boolean;

View File

@@ -27,6 +27,7 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.CameraType;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.processes.PipelineManager;
@@ -46,6 +47,8 @@ public class CameraConfiguration {
/** Can be either path (ex /dev/videoX) or index (ex 1). */
public String path = "";
public QuirkyCamera cameraQuirks;
@JsonIgnore public String[] otherPaths = {};
public CameraType cameraType = CameraType.UsbCamera;
@@ -93,6 +96,7 @@ public class CameraConfiguration {
@JsonProperty("FOV") double FOV,
@JsonProperty("path") String path,
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
this.baseName = baseName;
@@ -101,6 +105,7 @@ public class CameraConfiguration {
this.FOV = FOV;
this.path = path;
this.cameraType = cameraType;
this.cameraQuirks = cameraQuirks;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex;
@@ -165,6 +170,8 @@ public class CameraConfiguration {
+ Arrays.toString(otherPaths)
+ ", cameraType="
+ cameraType
+ ", cameraQuirks="
+ cameraQuirks
+ ", FOV="
+ FOV
+ ", calibrations="

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.configuration;
/**
* Add migrations by adding the SQL commands for each migration sequentially to this array. DO NOT
* edit or delete existing SQL commands. That will lead to producing an icompatible database.
*
* <p>You can use multiple SQL statements in one migration step as long as you separate them with a
* semicolon (;).
*/
public final class DatabaseSchema {
public static final String[] migrations = {
// #1 - initial schema
"CREATE TABLE IF NOT EXISTS global (\n"
+ " filename TINYTEXT PRIMARY KEY,\n"
+ " contents mediumtext NOT NULL\n"
+ ");"
+ "CREATE TABLE IF NOT EXISTS cameras (\n"
+ " unique_name TINYTEXT PRIMARY KEY,\n"
+ " config_json text NOT NULL,\n"
+ " drivermode_json text NOT NULL,\n"
+ " pipeline_jsons mediumtext NOT NULL\n"
+ ");",
// #2 - add column otherpaths_json
"ALTER TABLE cameras ADD COLUMN otherpaths_json TEXT NOT NULL DEFAULT '[]';",
// add future migrations here
};
// Constants for the tables and column to help prevent typos in SQL queries
// Update these tables to keep them constant with the current schema
public final class Tables {
// These constants should match the current SQL name of each table
public static final String GLOBAL = "global";
public static final String CAMERAS = "cameras";
}
public final class Columns {
// These constants should match the current SQL name of each column
static final String GLB_FILENAME = "filename";
static final String GLB_CONTENTS = "contents";
static final String CAM_UNIQUE_NAME = "unique_name";
static final String CAM_CONFIG_JSON = "config_json";
static final String CAM_DRIVERMODE_JSON = "drivermode_json";
static final String CAM_PIPELINE_JSONS = "pipeline_jsons";
static final String CAM_OTHERPATHS_JSON = "otherpaths_json";
}
}

View File

@@ -196,7 +196,7 @@ class LegacyConfigProvider extends ConfigProvider {
}
}
if (atfl == null) {
logger.info("Loading default apriltags for 2023 field...");
logger.info("Loading default apriltags for 2024 field...");
try {
atfl = AprilTagFields.kDefaultField.loadAprilTagLayoutField();
} catch (UncheckedIOException e) {

View File

@@ -30,6 +30,7 @@ import org.photonvision.common.util.SerializationUtils;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager;
import org.photonvision.vision.processes.VisionSource;
@@ -178,6 +179,7 @@ public class PhotonConfiguration {
public int inputStreamPort;
public List<CameraCalibrationCoefficients> calibrations;
public boolean isFovConfigurable = true;
public QuirkyCamera cameraQuirks;
public boolean isCSICamera;
}
}

View File

@@ -30,6 +30,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.photonvision.common.configuration.DatabaseSchema.Columns;
import org.photonvision.common.configuration.DatabaseSchema.Tables;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.JacksonUtils;
@@ -47,13 +49,7 @@ import org.photonvision.vision.pipeline.DriverModePipelineSettings;
public class SqlConfigProvider extends ConfigProvider {
private static final Logger logger = new Logger(SqlConfigProvider.class, LogGroup.Config);
static class TableKeys {
static final String CAM_UNIQUE_NAME = "unique_name";
static final String CONFIG_JSON = "config_json";
static final String DRIVERMODE_JSON = "drivermode_json";
static final String OTHERPATHS_JSON = "otherpaths_json";
static final String PIPELINE_JSONS = "pipeline_jsons";
static class GlobalKeys {
static final String NETWORK_CONFIG = "networkConfig";
static final String HARDWARE_CONFIG = "hardwareConfig";
static final String HARDWARE_SETTINGS = "hardwareSettings";
@@ -61,14 +57,24 @@ public class SqlConfigProvider extends ConfigProvider {
}
private static final String dbName = "photon.sqlite";
// private final File rootFolder;
private final String dbPath;
private final String url;
private final Object m_mutex = new Object();
private final File rootFolder;
public SqlConfigProvider(Path rootFolder) {
this.rootFolder = rootFolder.toFile();
public SqlConfigProvider(Path rootPath) {
File rootFolder = rootPath.toFile();
// Make sure root dir exists
if (!rootFolder.exists()) {
if (rootFolder.mkdirs()) {
logger.debug("Root config folder did not exist. Created!");
} else {
logger.error("Failed to create root config folder!");
}
}
dbPath = Path.of(rootFolder.toString(), dbName).toAbsolutePath().toString();
url = "jdbc:sqlite:" + dbPath;
logger.debug("Using database " + dbPath);
initDatabase();
}
@@ -80,91 +86,136 @@ public class SqlConfigProvider extends ConfigProvider {
return config;
}
private Connection createConn() {
String url = "jdbc:sqlite:" + dbPath;
private Connection createConn(boolean autoCommit) {
Connection conn = null;
try {
var conn = DriverManager.getConnection(url);
conn.setAutoCommit(false);
return conn;
conn = DriverManager.getConnection(url);
conn.setAutoCommit(autoCommit);
} catch (SQLException e) {
logger.error("Error creating connection", e);
return null;
}
return conn;
}
private Connection createConn() {
return createConn(false);
}
private void tryCommit(Connection conn) {
try {
conn.commit();
} catch (SQLException e) {
logger.error("Err committing changes: ", e);
} catch (SQLException e1) {
logger.error("Err committing changes: ", e1);
try {
conn.rollback();
} catch (SQLException e1) {
logger.error("Err rolling back changes: ", e);
} catch (SQLException e2) {
logger.error("Err rolling back changes: ", e2);
}
}
}
private void initDatabase() {
// Make sure root dir exists
private int getIntPragma(String pragma) {
int retval = 0;
try (Connection conn = createConn(true);
Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("PRAGMA " + pragma + ";");
retval = rs.getInt(1);
} catch (SQLException e) {
logger.error("Error querying " + pragma, e);
}
return retval;
}
if (!rootFolder.exists()) {
if (rootFolder.mkdirs()) {
logger.debug("Root config folder did not exist. Created!");
} else {
logger.error("Failed to create root config folder!");
private int getSchemaVersion() {
return getIntPragma("schema_version");
}
public int getUserVersion() {
return getIntPragma("user_version");
}
private void setUserVersion(Connection conn, int value) {
try (Statement stmt = conn.createStatement()) {
stmt.execute("PRAGMA user_version = " + value + ";");
} catch (SQLException e) {
logger.error("Error setting user_version to ", e);
}
}
private void doMigration(int index) throws SQLException {
logger.debug("Running migration step " + index);
try (Connection conn = createConn();
Statement stmt = conn.createStatement()) {
for (String sql : DatabaseSchema.migrations[index].split(";")) {
stmt.addBatch(sql);
}
stmt.executeBatch();
setUserVersion(conn, index + 1);
tryCommit(conn);
} catch (SQLException e) {
logger.error("Error with migration step " + index, e);
throw e;
}
}
private void initDatabase() {
int userVersion = getUserVersion();
int expectedVersion = DatabaseSchema.migrations.length;
if (userVersion < expectedVersion) {
// older database, run migrations
// first, check to see if this is one of the ones from 2024 beta that need special handling
if (userVersion == 0 && getSchemaVersion() > 0) {
String sql =
"SELECT COUNT(*) AS CNTREC FROM pragma_table_info('cameras') WHERE name='otherpaths_json';";
try (Connection conn = createConn(true);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql); ) {
if (rs.getInt("CNTREC") == 0) {
// need to add otherpaths_json
userVersion = 1;
} else {
// already there, no need to add the column
userVersion = 2;
}
setUserVersion(conn, userVersion);
} catch (SQLException e) {
logger.error(
"Could not determine the version of the database. Try deleting "
+ dbName
+ "and restart photonvision.",
e);
}
}
logger.debug("Older database version. Migrating ... ");
try {
for (int index = userVersion; index < expectedVersion; index++) {
doMigration(index);
}
logger.debug("Database migration complete");
} catch (SQLException e) {
logger.error("Error with database migration", e);
}
}
Connection conn = null;
Statement createGlobalTableStatement = null, createCameraTableStatement = null;
try {
conn = createConn();
if (conn == null) {
logger.error("No connection, cannot init db");
return;
}
// Create global settings table. Just a dumb table with list of jsons and their
// name
try {
createGlobalTableStatement = conn.createStatement();
String sql =
"CREATE TABLE IF NOT EXISTS global (\n"
+ " filename TINYTEXT PRIMARY KEY,\n"
+ " contents mediumtext NOT NULL\n"
+ ");";
createGlobalTableStatement.execute(sql);
} catch (SQLException e) {
logger.error("Err creating global table", e);
}
// Create cameras table, key is the camera unique name
try {
createCameraTableStatement = conn.createStatement();
var sql =
"CREATE TABLE IF NOT EXISTS cameras (\n"
+ " unique_name TINYTEXT PRIMARY KEY,\n"
+ " config_json text NOT NULL,\n"
+ " drivermode_json text NOT NULL,\n"
+ " otherpaths_json text NOT NULL,\n"
+ " pipeline_jsons mediumtext NOT NULL\n"
+ ");";
createCameraTableStatement.execute(sql);
} catch (SQLException e) {
logger.error("Err creating cameras table", e);
}
this.tryCommit(conn);
} finally {
try {
if (createGlobalTableStatement != null) createGlobalTableStatement.close();
if (createCameraTableStatement != null) createCameraTableStatement.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
// Warn if the database still isn't at the correct version
userVersion = getUserVersion();
if (userVersion > expectedVersion) {
// database must be from a newer version, so warn
logger.warn(
"This database is from a newer version of PhotonVision. Check that you are running the right version of PhotonVision.");
} else if (userVersion < expectedVersion) {
// migration didn't work, so warn
logger.warn(
"This database migration failed. Expected version: "
+ expectedVersion
+ ", got version: "
+ userVersion);
} else {
// migration worked
logger.info("Using correct database version: " + userVersion);
}
}
@@ -212,7 +263,7 @@ public class SqlConfigProvider extends ConfigProvider {
try {
hardwareConfig =
JacksonUtils.deserialize(
getOneConfigFile(conn, TableKeys.HARDWARE_CONFIG), HardwareConfig.class);
getOneConfigFile(conn, GlobalKeys.HARDWARE_CONFIG), HardwareConfig.class);
} catch (IOException e) {
logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig();
@@ -221,7 +272,7 @@ public class SqlConfigProvider extends ConfigProvider {
try {
hardwareSettings =
JacksonUtils.deserialize(
getOneConfigFile(conn, TableKeys.HARDWARE_SETTINGS), HardwareSettings.class);
getOneConfigFile(conn, GlobalKeys.HARDWARE_SETTINGS), HardwareSettings.class);
} catch (IOException e) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
@@ -230,7 +281,7 @@ public class SqlConfigProvider extends ConfigProvider {
try {
networkConfig =
JacksonUtils.deserialize(
getOneConfigFile(conn, TableKeys.NETWORK_CONFIG), NetworkConfig.class);
getOneConfigFile(conn, GlobalKeys.NETWORK_CONFIG), NetworkConfig.class);
} catch (IOException e) {
logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig();
@@ -239,7 +290,7 @@ public class SqlConfigProvider extends ConfigProvider {
try {
atfl =
JacksonUtils.deserialize(
getOneConfigFile(conn, TableKeys.ATFL_CONFIG_FILE), AprilTagFieldLayout.class);
getOneConfigFile(conn, GlobalKeys.ATFL_CONFIG_FILE), AprilTagFieldLayout.class);
} catch (IOException e) {
logger.error("Could not deserialize apriltag layout! Loading defaults");
try {
@@ -273,12 +324,15 @@ public class SqlConfigProvider extends ConfigProvider {
PreparedStatement query = null;
try {
query =
conn.prepareStatement("SELECT contents FROM global where filename=\"" + filename + "\"");
conn.prepareStatement(
String.format(
"SELECT %s FROM %s WHERE %s = \"%s\"",
Columns.GLB_CONTENTS, Tables.GLOBAL, Columns.GLB_FILENAME, filename));
var result = query.executeQuery();
while (result.next()) {
return result.getString("contents");
return result.getString(Columns.GLB_CONTENTS);
}
} catch (SQLException e) {
logger.error("SQL Err getting file " + filename, e);
@@ -297,8 +351,14 @@ public class SqlConfigProvider extends ConfigProvider {
try {
// Replace this camera's row with the new settings
var sqlString =
"REPLACE INTO cameras (unique_name, config_json, drivermode_json, otherpaths_json, pipeline_jsons) VALUES "
+ "(?,?,?,?,?);";
String.format(
"REPLACE INTO %s (%s, %s, %s, %s, %s) VALUES (?,?,?,?,?);",
Tables.CAMERAS,
Columns.CAM_UNIQUE_NAME,
Columns.CAM_CONFIG_JSON,
Columns.CAM_DRIVERMODE_JSON,
Columns.CAM_OTHERPATHS_JSON,
Columns.CAM_PIPELINE_JSONS);
for (var c : config.getCameraConfigurations().entrySet()) {
PreparedStatement statement = conn.prepareStatement(sqlString);
@@ -372,13 +432,16 @@ public class SqlConfigProvider extends ConfigProvider {
PreparedStatement statement3 = null;
try {
// Replace this camera's row with the new settings
var sqlString = "REPLACE INTO global (filename, contents) VALUES " + "(?,?);";
var sqlString =
String.format(
"REPLACE INTO %s (%s, %s) VALUES (?,?);",
Tables.GLOBAL, Columns.GLB_FILENAME, Columns.GLB_CONTENTS);
if (!skipSavingHWSet) {
statement1 = conn.prepareStatement(sqlString);
addFile(
statement1,
TableKeys.HARDWARE_SETTINGS,
GlobalKeys.HARDWARE_SETTINGS,
JacksonUtils.serializeToString(config.getHardwareSettings()));
statement1.executeUpdate();
}
@@ -387,7 +450,7 @@ public class SqlConfigProvider extends ConfigProvider {
statement2 = conn.prepareStatement(sqlString);
addFile(
statement2,
TableKeys.NETWORK_CONFIG,
GlobalKeys.NETWORK_CONFIG,
JacksonUtils.serializeToString(config.getNetworkConfig()));
statement2.executeUpdate();
statement2.close();
@@ -397,7 +460,7 @@ public class SqlConfigProvider extends ConfigProvider {
statement3 = conn.prepareStatement(sqlString);
addFile(
statement3,
TableKeys.HARDWARE_CONFIG,
GlobalKeys.HARDWARE_CONFIG,
JacksonUtils.serializeToString(config.getHardwareConfig()));
statement3.executeUpdate();
statement3.close();
@@ -432,7 +495,10 @@ public class SqlConfigProvider extends ConfigProvider {
}
// Replace this camera's row with the new settings
var sqlString = "REPLACE INTO global (filename, contents) VALUES " + "(?,?);";
var sqlString =
String.format(
"REPLACE INTO %s (%s, %s) VALUES (?,?);",
Tables.GLOBAL, Columns.GLB_FILENAME, Columns.GLB_CONTENTS);
statement1 = conn.prepareStatement(sqlString);
addFile(statement1, fname, Files.readString(path));
@@ -461,25 +527,25 @@ public class SqlConfigProvider extends ConfigProvider {
@Override
public boolean saveUploadedHardwareConfig(Path uploadPath) {
skipSavingHWCfg = true;
return saveOneFile(TableKeys.HARDWARE_CONFIG, uploadPath);
return saveOneFile(GlobalKeys.HARDWARE_CONFIG, uploadPath);
}
@Override
public boolean saveUploadedHardwareSettings(Path uploadPath) {
skipSavingHWSet = true;
return saveOneFile(TableKeys.HARDWARE_SETTINGS, uploadPath);
return saveOneFile(GlobalKeys.HARDWARE_SETTINGS, uploadPath);
}
@Override
public boolean saveUploadedNetworkConfig(Path uploadPath) {
skipSavingNWCfg = true;
return saveOneFile(TableKeys.NETWORK_CONFIG, uploadPath);
return saveOneFile(GlobalKeys.NETWORK_CONFIG, uploadPath);
}
@Override
public boolean saveUploadedAprilTagFieldLayout(Path uploadPath) {
skipSavingAPRTG = true;
return saveOneFile(TableKeys.ATFL_CONFIG_FILE, uploadPath);
return saveOneFile(GlobalKeys.ATFL_CONFIG_FILE, uploadPath);
}
private HashMap<String, CameraConfiguration> loadCameraConfigs(Connection conn) {
@@ -491,12 +557,13 @@ public class SqlConfigProvider extends ConfigProvider {
query =
conn.prepareStatement(
String.format(
"SELECT %s, %s, %s, %s, %s FROM cameras",
TableKeys.CAM_UNIQUE_NAME,
TableKeys.CONFIG_JSON,
TableKeys.DRIVERMODE_JSON,
TableKeys.OTHERPATHS_JSON,
TableKeys.PIPELINE_JSONS));
"SELECT %s, %s, %s, %s, %s FROM %s",
Columns.CAM_UNIQUE_NAME,
Columns.CAM_CONFIG_JSON,
Columns.CAM_DRIVERMODE_JSON,
Columns.CAM_OTHERPATHS_JSON,
Columns.CAM_PIPELINE_JSONS,
Tables.CAMERAS));
var result = query.executeQuery();
@@ -504,18 +571,18 @@ public class SqlConfigProvider extends ConfigProvider {
while (result.next()) {
List<String> dummyList = new ArrayList<>();
var uniqueName = result.getString(TableKeys.CAM_UNIQUE_NAME);
var uniqueName = result.getString(Columns.CAM_UNIQUE_NAME);
var config =
JacksonUtils.deserialize(
result.getString(TableKeys.CONFIG_JSON), CameraConfiguration.class);
result.getString(Columns.CAM_CONFIG_JSON), CameraConfiguration.class);
var driverMode =
JacksonUtils.deserialize(
result.getString(TableKeys.DRIVERMODE_JSON), DriverModePipelineSettings.class);
result.getString(Columns.CAM_DRIVERMODE_JSON), DriverModePipelineSettings.class);
var otherPaths =
JacksonUtils.deserialize(result.getString(TableKeys.OTHERPATHS_JSON), String[].class);
JacksonUtils.deserialize(result.getString(Columns.CAM_OTHERPATHS_JSON), String[].class);
List<?> pipelineSettings =
JacksonUtils.deserialize(
result.getString(TableKeys.PIPELINE_JSONS), dummyList.getClass());
result.getString(Columns.CAM_PIPELINE_JSONS), dummyList.getClass());
List<CVPipelineSettings> loadedSettings = new ArrayList<>();
for (var str : pipelineSettings) {

View File

@@ -26,6 +26,7 @@ public enum DataChangeDestination {
DCD_ACTIVEPIPELINESETTINGS,
DCD_GENSETTINGS,
DCD_UI,
DCD_WEBSERVER,
DCD_OTHER;
public static final List<DataChangeDestination> AllDestinations =

View File

@@ -19,6 +19,10 @@ package org.photonvision.common.networking;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeDestination;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.DataChangeSource;
import org.photonvision.common.dataflow.events.DataChangeEvent;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -43,6 +47,7 @@ public class NetworkManager {
public void initialize(boolean shouldManage) {
isManaged = shouldManage && !networkingIsDisabled;
if (!isManaged) {
logger.info("Network management is disabled.");
return;
}
@@ -147,5 +152,13 @@ public class NetworkManager {
public void reinitialize() {
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage());
DataChangeService.getInstance()
.publishEvent(
new DataChangeEvent<Boolean>(
DataChangeSource.DCS_OTHER,
DataChangeDestination.DCD_WEBSERVER,
"restartServer",
true));
}
}

View File

@@ -54,6 +54,9 @@ public class CameraCalibrationCoefficients implements Releasable {
@JsonProperty("calobjectSpacing")
public final double calobjectSpacing;
@JsonProperty("lensmodel")
public final CameraLensModel lensmodel;
@JsonIgnore private final double[] intrinsicsArr = new double[9];
@JsonIgnore private final double[] distCoeffsArr = new double[5];
@@ -83,13 +86,15 @@ public class CameraCalibrationCoefficients implements Releasable {
@JsonProperty("calobjectWarp") double[] calobjectWarp,
@JsonProperty("observations") List<BoardObservation> observations,
@JsonProperty("calobjectSize") Size calobjectSize,
@JsonProperty("calobjectSpacing") double calobjectSpacing) {
@JsonProperty("calobjectSpacing") double calobjectSpacing,
@JsonProperty("lensmodel") CameraLensModel lensmodel) {
this.resolution = resolution;
this.cameraIntrinsics = cameraIntrinsics;
this.distCoeffs = distCoeffs;
this.calobjectWarp = calobjectWarp;
this.calobjectSize = calobjectSize;
this.calobjectSpacing = calobjectSpacing;
this.lensmodel = lensmodel;
// Legacy migration just to make sure that observations is at worst empty and never null
if (observations == null) {
@@ -174,7 +179,8 @@ public class CameraCalibrationCoefficients implements Releasable {
new double[0],
List.of(),
new Size(0, 0),
0);
0,
CameraLensModel.LENSMODEL_OPENCV);
}
@Override

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.calibration;
/**
* What kind of camera lens model our intrinsics are modeling. For more info see:
* https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html
* https://mrcal.secretsauce.net/lensmodels.html#org4e95788
*/
public enum CameraLensModel {
/** OpenCV[4,5,8,12]-based model */
LENSMODEL_OPENCV,
/** Mrcal steriographic lens model. See LENSMODEL_STEREOGRAPHIC in the mrcal docs */
LENSMODEL_STERIOGRAPHIC,
/**
* Mrcal splined-steriographic lens model. See LENSMODEL_SPLINED_STEREOGRAPHIC_ in the mrcal docs
*/
LENSMODEL_SPLINED_STERIOGRAPHIC
}

View File

@@ -32,4 +32,13 @@ public enum CameraQuirk {
AdjustableFocus,
/** Changing FPS repeatedly with small delay does not work correctly */
StickyFPS,
/** Camera is an arducam. This means it shares VID/PID with other arducams (ew) */
ArduCamCamera,
/**
* Camera is an arducam ov9281 which has a funky exposure issue where it is defined in v4l as
* 1-5000 instead of 1-75
*/
ArduOV9281,
/** Dummy quirk to tell OV2311 from OV9281 */
ArduOV2311,
}

View File

@@ -17,6 +17,8 @@
package org.photonvision.vision.camera;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -47,8 +49,32 @@ public class QuirkyCamera {
-1, -1, "mmal service 16.1", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
new QuirkyCamera(-1, -1, "unicam", CameraQuirk.PiCam), // PiCam (via V4L2, not zerocopy)
new QuirkyCamera(0x85B, 0x46D, CameraQuirk.AdjustableFocus), // Logitech C925-e
new QuirkyCamera(0x6366, 0x0c45, CameraQuirk.StickyFPS) // Arducam OV2311
);
// Generic arducam. Since OV2311 can't be differentiated at first boot, apply stickyFPS to
// the generic case, too
new QuirkyCamera(
0x0c45,
0x6366,
"",
"Arducam Generic",
CameraQuirk.ArduCamCamera,
CameraQuirk.StickyFPS),
// Arducam OV2311
new QuirkyCamera(
0x0c45,
0x6366,
"OV2311",
"OV2311",
CameraQuirk.ArduCamCamera,
CameraQuirk.ArduOV2311,
CameraQuirk.StickyFPS),
// Arducam OV9281
new QuirkyCamera(
0x0c45,
0x6366,
"OV9281",
"OV9281",
CameraQuirk.ArduCamCamera,
CameraQuirk.ArduOV9281));
public static final QuirkyCamera DefaultCamera = new QuirkyCamera(0, 0, "");
public static final QuirkyCamera ZeroCopyPiCamera =
@@ -60,9 +86,19 @@ public class QuirkyCamera {
CameraQuirk.Gain,
CameraQuirk.AWBGain); // PiCam (special zerocopy version)
@JsonProperty("baseName")
public final String baseName;
@JsonProperty("usbVid")
public final int usbVid;
@JsonProperty("usbPid")
public final int usbPid;
@JsonProperty("displayName")
public final String displayName;
@JsonProperty("quirks")
public final HashMap<CameraQuirk, Boolean> quirks;
/**
@@ -85,9 +121,24 @@ public class QuirkyCamera {
* @param quirks Camera quirks
*/
private QuirkyCamera(int usbVid, int usbPid, String baseName, CameraQuirk... quirks) {
this(usbVid, usbPid, baseName, "", quirks);
}
/**
* Creates a QuirkyCamera that matches by USB VID/PID and name
*
* @param usbVid USB VID of camera
* @param usbPid USB PID of camera
* @param baseName CSCore name of camera
* @param displayName Human-friendly quicky camera name
* @param quirks Camera quirks
*/
private QuirkyCamera(
int usbVid, int usbPid, String baseName, String displayName, CameraQuirk... quirks) {
this.usbVid = usbVid;
this.usbPid = usbPid;
this.baseName = baseName;
this.displayName = displayName;
this.quirks = new HashMap<>();
for (var q : quirks) {
@@ -98,6 +149,20 @@ public class QuirkyCamera {
}
}
@JsonCreator
public QuirkyCamera(
@JsonProperty("baseName") String baseName,
@JsonProperty("usbVid") int usbVid,
@JsonProperty("usbPid") int usbPid,
@JsonProperty("displayName") String displayName,
@JsonProperty("quirks") HashMap<CameraQuirk, Boolean> quirks) {
this.baseName = baseName;
this.usbPid = usbPid;
this.usbVid = usbVid;
this.quirks = quirks;
this.displayName = displayName;
}
public boolean hasQuirk(CameraQuirk quirk) {
return quirks.get(quirk);
}
@@ -144,8 +209,39 @@ public class QuirkyCamera {
&& Objects.equals(quirks, that.quirks);
}
@Override
public String toString() {
String ret =
"QuirkyCamera [baseName="
+ baseName
+ ", displayName="
+ displayName
+ ", usbVid="
+ usbVid
+ ", usbPid="
+ usbPid
+ ", quirks="
+ quirks.toString()
+ "]";
return ret;
}
@Override
public int hashCode() {
return Objects.hash(usbVid, usbPid, baseName, quirks);
}
/**
* Add/remove quirks from the camera we're controlling
*
* @param quirksToChange map of true/false for quirks we should change
*/
public void updateQuirks(HashMap<CameraQuirk, Boolean> quirksToChange) {
for (var q : quirksToChange.entrySet()) {
var quirk = q.getKey();
var hasQuirk = q.getValue();
this.quirks.put(quirk, hasQuirk);
}
}
}

View File

@@ -45,8 +45,6 @@ public class USBCameraSource extends VisionSource {
private FrameProvider usbFrameProvider;
private final CvSink cvSink;
private QuirkyCamera cameraQuirks;
public USBCameraSource(CameraConfiguration config) {
super(config);
@@ -54,17 +52,21 @@ public class USBCameraSource extends VisionSource {
camera = new UsbCamera(config.nickname, config.path);
cvSink = CameraServer.getVideo(this.camera);
cameraQuirks =
QuirkyCamera.getQuirkyCamera(
camera.getInfo().productId, camera.getInfo().vendorId, config.baseName);
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(
camera.getInfo().vendorId, camera.getInfo().productId, config.baseName);
if (cameraQuirks.hasQuirks()) {
logger.info("Quirky camera detected: " + cameraQuirks.baseName);
if (getCameraConfiguration().cameraQuirks.hasQuirks()) {
logger.info("Quirky camera detected: " + getCameraConfiguration().cameraQuirks.baseName);
}
if (cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken)) {
// set some defaults, as these should never be used.
logger.info("Camera " + cameraQuirks.baseName + " is not supported for PhotonVision");
logger.info(
"Camera "
+ getCameraConfiguration().cameraQuirks.baseName
+ " is not supported for PhotonVision");
usbCameraSettables = null;
usbFrameProvider = null;
} else {
@@ -88,7 +90,9 @@ public class USBCameraSource extends VisionSource {
public USBCameraSource(CameraConfiguration config, int pid, int vid, boolean unitTest) {
this(config);
cameraQuirks = QuirkyCamera.getQuirkyCamera(pid, vid, config.baseName);
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(pid, vid, config.baseName);
if (unitTest)
usbFrameProvider =
@@ -99,7 +103,7 @@ public class USBCameraSource extends VisionSource {
}
void disableAutoFocus() {
if (cameraQuirks.hasQuirk(CameraQuirk.AdjustableFocus)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.AdjustableFocus)) {
try {
camera.getProperty("focus_auto").set(0);
camera.getProperty("focus_absolute").set(0); // Focus into infinity
@@ -110,7 +114,7 @@ public class USBCameraSource extends VisionSource {
}
public QuirkyCamera getCameraQuirks() {
return this.cameraQuirks;
return getCameraConfiguration().cameraQuirks;
}
@Override
@@ -124,17 +128,21 @@ public class USBCameraSource extends VisionSource {
}
public class USBCameraSettables extends VisionSourceSettables {
// We need to remember the last exposure set when exiting auto exposure mode so we can restore
// it
private double last_exposure = -1;
protected USBCameraSettables(CameraConfiguration configuration) {
super(configuration);
getAllVideoModes();
if (!cameraQuirks.hasQuirk(CameraQuirk.StickyFPS))
if (!configuration.cameraQuirks.hasQuirk(CameraQuirk.StickyFPS))
if (!videoModes.isEmpty()) setVideoMode(videoModes.get(0)); // fixes double FPS set
}
public void setAutoExposure(boolean cameraAutoExposure) {
logger.debug("Setting auto exposure to " + cameraAutoExposure);
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
// Case, we know this is a picam. Go through v4l2-ctl interface directly
// Common settings
@@ -166,20 +174,46 @@ public class USBCameraSource extends VisionSource {
} else {
// Case - this is some other USB cam. Default to wpilib's implementation
var canSetWhiteBalance = !cameraQuirks.hasQuirk(CameraQuirk.Gain);
var canSetWhiteBalance = !getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.Gain);
if (!cameraAutoExposure) {
// Pick a bunch of reasonable setting defaults for vision processing retroreflective
if (canSetWhiteBalance) {
camera.setWhiteBalanceManual(4000); // Auto white-balance disabled, 4000K preset
// Linux kernel bump changed names -- now called white_balance_automatic and
// white_balance_temperature
if (camera.getProperty("white_balance_automatic").getKind() != Kind.kNone) {
// 1=auto, 0=manual
camera.getProperty("white_balance_automatic").set(0);
camera.getProperty("white_balance_temperature").set(4000);
} else {
camera.setWhiteBalanceManual(4000); // Auto white-balance disabled, 4000K preset
}
// Most cameras leave exposure time absolute at the last value from their AE algorithm.
// Set it back to the exposure slider value
setExposure(this.last_exposure);
}
} else {
// Pick a bunch of reasonable setting defaults for driver, fiducials, or otherwise
// nice-for-humans
if (canSetWhiteBalance) {
camera.setWhiteBalanceAuto(); // Auto white-balance enabled
// Linux kernel bump changed names -- now called white_balance_automatic
if (camera.getProperty("white_balance_automatic").getKind() != Kind.kNone) {
// 1=auto, 0=manual
camera.getProperty("white_balance_automatic").set(1);
} else {
camera.setWhiteBalanceAuto(); // Auto white-balance enabled
}
}
// Linux kernel bump changed names -- exposure_auto is now called auto_exposure
if (camera.getProperty("auto_exposure").getKind() != Kind.kNone) {
var prop = camera.getProperty("auto_exposure");
// 3=auto-aperature
prop.set((int) 3);
} else {
camera.setExposureAuto(); // auto exposure enabled
}
camera.setExposureAuto(); // auto exposure enabled
}
}
}
@@ -207,17 +241,30 @@ public class USBCameraSource extends VisionSource {
if (exposure >= 0.0) {
try {
int scaledExposure = 1;
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
scaledExposure = Math.round(timeToPiCamRawExposure(pctToExposureTimeUs(exposure)));
logger.debug("Setting camera raw exposure to " + scaledExposure);
camera.getProperty("raw_exposure_time_absolute").set(scaledExposure);
camera.getProperty("raw_exposure_time_absolute").set(scaledExposure);
} else if (camera.getProperty("exposure_time_absolute").getKind() != Kind.kNone) {
// Seems like the name changed at some point in v4l? set it instead
var prop = camera.getProperty("exposure_time_absolute");
var exposure_manual_val =
MathUtils.map(Math.round(exposure), 0, 100, prop.getMin(), prop.getMax());
// Yay thanks v4l for changing names randomly
} else if (camera.getProperty("exposure_time_absolute").getKind() != Kind.kNone
&& camera.getProperty("auto_exposure").getKind() != Kind.kNone) {
// 1=manual-aperature
camera.getProperty("auto_exposure").set(1);
// Seems like the name changed at some point in v4l? set it ouyrselves too
var prop = camera.getProperty("raw_exposure_time_absolute");
var propMin = prop.getMin();
var propMax = prop.getMax();
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.ArduOV9281)) {
propMin = 1;
propMax = 75;
}
var exposure_manual_val = MathUtils.map(Math.round(exposure), 0, 100, propMin, propMax);
prop.set((int) exposure_manual_val);
} else {
scaledExposure = (int) Math.round(exposure);
@@ -228,6 +275,7 @@ public class USBCameraSource extends VisionSource {
} catch (VideoException e) {
logger.error("Failed to set camera exposure!", e);
}
this.last_exposure = exposure;
}
}
@@ -244,7 +292,7 @@ public class USBCameraSource extends VisionSource {
@Override
public void setGain(int gain) {
try {
if (cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
camera.getProperty("gain_automatic").set(0);
camera.getProperty("gain").set(gain);
}
@@ -278,7 +326,7 @@ public class USBCameraSource extends VisionSource {
List<VideoMode> videoModesList = new ArrayList<>();
try {
VideoMode[] modes;
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
modes =
new VideoMode[] {
new VideoMode(PixelFormat.kBGR, 320, 240, 90),
@@ -306,13 +354,13 @@ public class USBCameraSource extends VisionSource {
}
// On picam, filter non-bgr modes for performance
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam)) {
if (videoMode.pixelFormat != PixelFormat.kBGR) {
continue;
}
}
if (cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
if (getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
if (videoMode.fps > 100) {
continue;
}
@@ -370,7 +418,7 @@ public class USBCameraSource extends VisionSource {
@Override
public boolean isVendorCamera() {
return ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV()
&& cameraQuirks.hasQuirk(CameraQuirk.PiCam);
&& getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.PiCam);
}
@Override
@@ -391,15 +439,22 @@ public class USBCameraSource extends VisionSource {
if (cvSink == null) {
if (other.cvSink != null) return false;
} else if (!cvSink.equals(other.cvSink)) return false;
if (cameraQuirks == null) {
if (other.cameraQuirks != null) return false;
} else if (!cameraQuirks.equals(other.cameraQuirks)) return false;
if (getCameraConfiguration().cameraQuirks == null) {
if (other.getCameraConfiguration().cameraQuirks != null) return false;
} else if (!getCameraConfiguration()
.cameraQuirks
.equals(other.getCameraConfiguration().cameraQuirks)) return false;
return true;
}
@Override
public int hashCode() {
return Objects.hash(
camera, usbCameraSettables, usbFrameProvider, cameraConfiguration, cvSink, cameraQuirks);
camera,
usbCameraSettables,
usbFrameProvider,
cameraConfiguration,
cvSink,
getCameraConfiguration().cameraQuirks);
}
}

View File

@@ -35,6 +35,7 @@ import org.photonvision.mrcal.MrCalJNI.MrCalResult;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.vision.calibration.BoardObservation;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.calibration.CameraLensModel;
import org.photonvision.vision.calibration.JsonImageMat;
import org.photonvision.vision.calibration.JsonMatOfDouble;
import org.photonvision.vision.pipe.CVPipe;
@@ -158,7 +159,8 @@ public class Calibrate3dPipe
new double[0],
observations,
new Size(params.boardWidth, params.boardHeight),
params.squareSize);
params.squareSize,
CameraLensModel.LENSMODEL_OPENCV);
}
protected CameraCalibrationCoefficients calibrateMrcal(
@@ -240,7 +242,8 @@ public class Calibrate3dPipe
new double[] {result.warp_x, result.warp_y},
observations,
new Size(params.boardWidth, params.boardHeight),
params.squareSize);
params.squareSize,
CameraLensModel.LENSMODEL_OPENCV);
}
private List<BoardObservation> createObservations(

View File

@@ -512,6 +512,7 @@ public class VisionModule {
SerializationUtils.objectToHashMap(pipelineManager.getCurrentPipelineSettings());
ret.currentPipelineIndex = pipelineManager.getCurrentPipelineIndex();
ret.pipelineNicknames = pipelineManager.getPipelineNicknames();
ret.cameraQuirks = visionSource.getSettables().getConfiguration().cameraQuirks;
// TODO refactor into helper method
var temp = new HashMap<Integer, HashMap<String, Object>>();
@@ -609,4 +610,14 @@ public class VisionModule {
saveAndBroadcastAll();
}
/**
* Add/remove quirks from the camera we're controlling
*
* @param quirksToChange map of true/false for quirks we should change
*/
public void changeCameraQuirks(HashMap<CameraQuirk, Boolean> quirksToChange) {
visionSource.getCameraConfiguration().cameraQuirks.updateQuirks(quirksToChange);
saveAndBroadcastAll();
}
}

View File

@@ -19,25 +19,58 @@ package org.photonvision.common.configuration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.CameraType;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
import org.photonvision.vision.pipeline.ColoredShapePipelineSettings;
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
public class SQLConfigTest {
private static Path tmpDir;
@BeforeAll
public static void init() {
TestUtils.loadLibraries();
try {
tmpDir = Files.createTempDirectory("SQLConfigTest");
} catch (IOException e) {
System.out.println("Couldn't create temporary directory, using current directory");
tmpDir = Path.of("jdbc_test", "temp");
}
}
@AfterAll
public static void cleanUp() throws IOException {
Files.walk(tmpDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
}
@Test
@Order(1)
public void testMigration() {
SqlConfigProvider cfgLoader = new SqlConfigProvider(tmpDir);
cfgLoader.load();
assertEquals(
DatabaseSchema.migrations.length,
cfgLoader.getUserVersion(),
"Database isn't at the correct version");
}
@Test
@Order(2)
public void testLoad() {
var cfgLoader = new SqlConfigProvider(Path.of("jdbc_test"));
var cfgLoader = new SqlConfigProvider(tmpDir);
cfgLoader.load();
@@ -49,6 +82,7 @@ public class SQLConfigTest {
69,
"a/path/idk",
CameraType.UsbCamera,
QuirkyCamera.getQuirkyCamera(-1, -1),
List.of(),
0);
testcamcfg.pipelineSettings =

View File

@@ -17,6 +17,10 @@ class PNPResult:
def createFromPacket(self, packet: Packet) -> Packet:
self.isPresent = packet.decodeBoolean()
if not self.isPresent:
return packet
self.best = packet.decodeTransform()
self.alt = packet.decodeTransform()
self.bestReprojError = packet.decodeDouble()

View File

@@ -14,7 +14,7 @@ class VisionLEDMode(Enum):
kBlink = 2
lastVersionTimeCheck = 0.0
_lastVersionTimeCheck = 0.0
_VERSION_CHECK_ENABLED = True
@@ -26,41 +26,41 @@ def setVersionCheckEnabled(enabled: bool):
class PhotonCamera:
def __init__(self, cameraName: str):
instance = ntcore.NetworkTableInstance.getDefault()
self.name = cameraName
self._name = cameraName
self._tableName = "photonvision"
photonvision_root_table = instance.getTable(self._tableName)
self.cameraTable = photonvision_root_table.getSubTable(cameraName)
self.path = self.cameraTable.getPath()
self.rawBytesEntry = self.cameraTable.getRawTopic("rawBytes").subscribe(
self._cameraTable = photonvision_root_table.getSubTable(cameraName)
self._path = self._cameraTable.getPath()
self._rawBytesEntry = self._cameraTable.getRawTopic("rawBytes").subscribe(
"rawBytes", bytes([]), ntcore.PubSubOptions(periodic=0.01, sendAll=True)
)
self.driverModePublisher = self.cameraTable.getBooleanTopic(
self._driverModePublisher = self._cameraTable.getBooleanTopic(
"driverModeRequest"
).publish()
self.driverModeSubscriber = self.cameraTable.getBooleanTopic(
self._driverModeSubscriber = self._cameraTable.getBooleanTopic(
"driverMode"
).subscribe(False)
self.inputSaveImgEntry = self.cameraTable.getIntegerTopic(
self._inputSaveImgEntry = self._cameraTable.getIntegerTopic(
"inputSaveImgCmd"
).getEntry(0)
self.outputSaveImgEntry = self.cameraTable.getIntegerTopic(
self._outputSaveImgEntry = self._cameraTable.getIntegerTopic(
"outputSaveImgCmd"
).getEntry(0)
self.pipelineIndexRequest = self.cameraTable.getIntegerTopic(
self._pipelineIndexRequest = self._cameraTable.getIntegerTopic(
"pipelineIndexRequest"
).publish()
self.pipelineIndexState = self.cameraTable.getIntegerTopic(
self._pipelineIndexState = self._cameraTable.getIntegerTopic(
"pipelineIndexState"
).subscribe(0)
self.heartbeatEntry = self.cameraTable.getIntegerTopic("heartbeat").subscribe(
self._heartbeatEntry = self._cameraTable.getIntegerTopic("heartbeat").subscribe(
-1
)
self.ledModeRequest = photonvision_root_table.getIntegerTopic(
self._ledModeRequest = photonvision_root_table.getIntegerTopic(
"ledModeRequest"
).publish()
self.ledModeState = photonvision_root_table.getIntegerTopic(
self._ledModeState = photonvision_root_table.getIntegerTopic(
"ledModeState"
).subscribe(-1)
self.versionEntry = photonvision_root_table.getStringTopic("version").subscribe(
@@ -72,14 +72,14 @@ class PhotonCamera:
instance, ["/photonvision/"], ntcore.PubSubOptions(topicsOnly=True)
)
self.prevHeartbeat = 0
self.prevHeartbeatChangeTime = Timer.getFPGATimestamp()
self._prevHeartbeat = 0
self._prevHeartbeatChangeTime = Timer.getFPGATimestamp()
def getLatestResult(self) -> PhotonPipelineResult:
self._versionCheck()
retVal = PhotonPipelineResult()
packetWithTimestamp = self.rawBytesEntry.getAtomic()
packetWithTimestamp = self._rawBytesEntry.getAtomic()
byteList = packetWithTimestamp.value
timestamp = packetWithTimestamp.time
@@ -94,57 +94,57 @@ class PhotonCamera:
return retVal
def getDriverMode(self) -> bool:
return self.driverModeSubscriber.get()
return self._driverModeSubscriber.get()
def setDriverMode(self, driverMode: bool) -> None:
self.driverModePublisher.set(driverMode)
self._driverModePublisher.set(driverMode)
def takeInputSnapshot(self) -> None:
self.inputSaveImgEntry.set(self.inputSaveImgEntry.get() + 1)
self._inputSaveImgEntry.set(self._inputSaveImgEntry.get() + 1)
def takeOutputSnapshot(self) -> None:
self.outputSaveImgEntry.set(self.outputSaveImgEntry.get() + 1)
self._outputSaveImgEntry.set(self._outputSaveImgEntry.get() + 1)
def getPipelineIndex(self) -> int:
return self.pipelineIndexState.get(0)
return self._pipelineIndexState.get(0)
def setPipelineIndex(self, index: int) -> None:
self.pipelineIndexRequest.set(index)
self._pipelineIndexRequest.set(index)
def getLEDMode(self) -> VisionLEDMode:
mode = self.ledModeState.get()
mode = self._ledModeState.get()
return VisionLEDMode(mode)
def setLEDMode(self, led: VisionLEDMode) -> None:
self.ledModeRequest.set(led.value)
self._ledModeRequest.set(led.value)
def getName(self) -> str:
return self.name
return self._name
def isConnected(self) -> bool:
curHeartbeat = self.heartbeatEntry.get()
curHeartbeat = self._heartbeatEntry.get()
now = Timer.getFPGATimestamp()
if curHeartbeat != self.prevHeartbeat:
self.prevHeartbeat = curHeartbeat
self.prevHeartbeatChangeTime = now
if curHeartbeat != self._prevHeartbeat:
self._prevHeartbeat = curHeartbeat
self._prevHeartbeatChangeTime = now
return (now - self.prevHeartbeatChangeTime) < 0.5
return (now - self._prevHeartbeatChangeTime) < 0.5
def _versionCheck(self) -> None:
global lastVersionTimeCheck
global _lastVersionTimeCheck
if not _VERSION_CHECK_ENABLED:
return
if (Timer.getFPGATimestamp() - lastVersionTimeCheck) < 5.0:
if (Timer.getFPGATimestamp() - _lastVersionTimeCheck) < 5.0:
return
lastVersionTimeCheck = Timer.getFPGATimestamp()
_lastVersionTimeCheck = Timer.getFPGATimestamp()
if not self.heartbeatEntry.exists():
if not self._heartbeatEntry.exists():
cameraNames = (
self.cameraTable.getInstance().getTable(self._tableName).getSubTables()
self._cameraTable.getInstance().getTable(self._tableName).getSubTables()
)
if len(cameraNames) == 0:
wpilib.reportError(
@@ -153,13 +153,13 @@ class PhotonCamera:
)
else:
wpilib.reportError(
f"PhotonVision coprocessor at path {self.path} not found in Network Tables. Double check that your camera names match! Only the following camera names were found: { ''.join(cameraNames)}",
f"PhotonVision coprocessor at path {self._path} not found in Network Tables. Double check that your camera names match! Only the following camera names were found: { ''.join(cameraNames)}",
True,
)
elif not self.isConnected():
wpilib.reportWarning(
f"PhotonVision coprocessor at path {self.path} is not sending new data.",
f"PhotonVision coprocessor at path {self._path} is not sending new data.",
True,
)

View File

@@ -15,14 +15,17 @@ class PhotonPipelineResult:
def populateFromPacket(self, packet: Packet) -> Packet:
self.targets = []
self.latencyMillis = packet.decodeDouble()
self.multiTagResult = MultiTargetPNPResult()
self.multiTagResult.createFromPacket(packet)
targetCount = packet.decode8()
print(f"targetCount = {targetCount}")
for _ in range(targetCount):
target = PhotonTrackedTarget()
target.createFromPacket(packet)
self.targets.append(target)
self.multiTagResult = MultiTargetPNPResult()
self.multiTagResult.createFromPacket(packet)
return packet
def setTimestampSeconds(self, timestampSec: float) -> None:

View File

@@ -51,4 +51,6 @@ setup(
description=descriptionStr,
url="https://photonvision.org",
author="Photonvision Development Team",
long_description="A Pure-python implementation of PhotonLib",
long_description_content_type="text/markdown",
)

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,9 @@ from data import rawBytes3
from data import rawBytes4
from data import rawBytes5
from data import rawBytes6
from data import rawBytes7
from data import rawBytes8
from data import rawBytes9
def setupCommon(bytesIn):
@@ -28,7 +31,7 @@ def test_byteParse2():
def test_byteParse3():
res = setupCommon(rawBytes3)
assert len(res.getTargets()) == 0
assert len(res.getTargets()) >= 4
def test_byteParse4():
@@ -38,9 +41,24 @@ def test_byteParse4():
def test_byteParse5():
res = setupCommon(rawBytes5)
assert len(res.getTargets()) == 1
assert len(res.getTargets()) == 2
def test_byteParse6():
res = setupCommon(rawBytes6)
assert len(res.getTargets()) > 6
# assert len(res.getTargets()) >= 0
def test_byteParse7():
res = setupCommon(rawBytes7)
# assert len(res.getTargets()) >= 0
def test_byteParse8():
res = setupCommon(rawBytes8)
# assert len(res.getTargets()) >= 0
def test_byteParse9():
res = setupCommon(rawBytes9)
# assert len(res.getTargets()) >= 0

View File

@@ -385,6 +385,6 @@ public class Main {
logger.info("Starting server...");
HardwareManager.getInstance().setRunning(true);
Server.start(DEFAULT_WEBPORT);
Server.initialize(DEFAULT_WEBPORT);
}
}

View File

@@ -82,9 +82,14 @@ public class DataSocketHandler {
protected void onClose(WsCloseContext context) {
users.remove(context);
var remote = (InetSocketAddress) context.session.getRemoteAddress();
var host = remote.getAddress().toString() + ":" + remote.getPort();
var reason = context.reason() != null ? context.reason() : "Connection closed by client";
logger.info("Closing websocket connection from " + host + " for reason: " + reason);
// Remote can be null if server is being closed for restart
if (remote != null) {
var host = remote.getAddress().toString() + ":" + remote.getPort();
var reason = context.reason() != null ? context.reason() : "Connection closed by client";
logger.info("Closing websocket connection from " + host + " for reason: " + reason);
} else {
logger.info("Closing websockets for user " + context.getSessionId());
}
}
@SuppressWarnings({"unchecked"})

View File

@@ -17,6 +17,7 @@
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;
@@ -43,8 +44,10 @@ 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;
import org.photonvision.vision.processes.VisionModuleManager;
public class RequestHandler {
@@ -364,22 +367,36 @@ public class RequestHandler {
NetworkTablesManager.getInstance().setConfig(config);
}
public static class UICameraSettingsRequest {
@JsonProperty("fov")
double fov;
@JsonProperty("quirksToChange")
HashMap<CameraQuirk, Boolean> quirksToChange;
}
public static void onCameraSettingsRequest(Context ctx) {
try {
var data = kObjectMapper.readTree(ctx.body());
int index = data.get("index").asInt();
double fov = data.get("settings").get("fov").asDouble();
var settings =
JacksonUtils.deserialize(data.get("settings").toString(), UICameraSettingsRequest.class);
var fov = settings.fov;
logger.info("Changing camera FOV to: " + fov);
logger.info("Changing quirks to: " + settings.quirksToChange.toString());
var module = VisionModuleManager.getInstance().getModule(index);
module.setFov(fov);
module.changeCameraQuirks(settings.quirksToChange);
module.saveModule();
ctx.status(200);
ctx.result("Successfully saved camera settings");
logger.info("Successfully saved camera settings");
} catch (JsonProcessingException | NullPointerException e) {
} catch (NullPointerException | IOException e) {
ctx.status(400);
ctx.result("The provided camera settings were malformed");
logger.error("The provided camera settings were malformed", e);

View File

@@ -20,15 +20,42 @@ package org.photonvision.server;
import io.javalin.Javalin;
import io.javalin.plugin.bundled.CorsPluginConfig;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.StringJoiner;
import org.photonvision.common.dataflow.DataChangeDestination;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.DataChangeSource;
import org.photonvision.common.dataflow.DataChangeSubscriber;
import org.photonvision.common.dataflow.events.DataChangeEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class Server {
private static final Logger logger = new Logger(Server.class, LogGroup.WebServer);
public static void start(int port) {
var app =
private static Javalin app = null;
static class RestartSubscriber extends DataChangeSubscriber {
private RestartSubscriber() {
super(DataChangeSource.AllSources, List.of(DataChangeDestination.DCD_WEBSERVER));
}
@Override
public void onDataChangeEvent(DataChangeEvent<?> event) {
if (event.propertyName.equals("restartServer")) {
Server.restart();
}
}
}
public static void initialize(int port) {
DataChangeService.getInstance().addSubscriber(new RestartSubscriber());
start(port);
}
private static void start(int port) {
app =
Javalin.create(
javalinConfig -> {
javalinConfig.showJavalinBanner = false;
@@ -111,5 +138,17 @@ public class Server {
app.post("/api/calibration/importFromData", RequestHandler::onDataCalibrationImportRequest);
app.start(port);
System.out.println("hi");
}
/**
* Seems like if we change the static IP of this device, Javalin refuses to tell us when new
* Websocket clients connect. As a hack, we can restart the server every time we change static IPs
*/
public static void restart() {
logger.info("Web server going down for restart");
int oldPort = app.port();
app.stop();
start(oldPort);
}
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": true,
"currentLanguage": "cpp",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 5
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": true,
"currentLanguage": "cpp",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 5
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": true,
"currentLanguage": "cpp",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 5
}

View File

@@ -1,6 +1,6 @@
plugins {
id "com.diffplug.spotless" version "6.1.2"
id "edu.wpi.first.GradleRIO" version "2024.1.1-beta-4" apply false
id "edu.wpi.first.GradleRIO" version "2024.1.1" apply false
}
allprojects {

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": true,
"currentLanguage": "cpp",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 5
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": true,
"currentLanguage": "cpp",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 5
}

View File

@@ -12,8 +12,8 @@ repositories {
}
wpi.maven.useDevelopment = true
wpi.versions.wpilibVersion = "2024.1.1-beta-4-35-g141241d"
wpi.versions.wpimathVersion = "2024.1.1-beta-4-35-g141241d"
wpi.versions.wpilibVersion = "2024.1.1"
wpi.versions.wpimathVersion = "2024.1.1"
apply from: "${rootDir}/../shared/examples_common.gradle"

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 6995
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 6995
}

View File

@@ -1,6 +1,6 @@
plugins {
id "com.diffplug.spotless" version "6.1.2"
id "edu.wpi.first.GradleRIO" version "2024.1.1-beta-4" apply false
id "edu.wpi.first.GradleRIO" version "2024.1.1" apply false
}
apply from: "examples.gradle"

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 6995
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 6995
}

View File

@@ -1,6 +1,6 @@
{
"enableCppIntellisense": false,
"currentLanguage": "java",
"projectYear": "2023",
"projectYear": "2024",
"teamNumber": 4512
}

View File

@@ -11,8 +11,8 @@ apply from: "${rootDir}/../shared/examples_common.gradle"
def ROBOT_MAIN_CLASS = "frc.robot.Main"
wpi.maven.useDevelopment = true
wpi.versions.wpilibVersion = "2024.1.1-beta-4-35-g141241d"
wpi.versions.wpimathVersion = "2024.1.1-beta-4-35-g141241d"
wpi.versions.wpilibVersion = "2024.1.1"
wpi.versions.wpimathVersion = "2024.1.1"
// Define my targets (RoboRIO) and artifacts (deployable files)

View File

@@ -4,6 +4,42 @@ package_is_installed(){
dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q "ok installed"
}
help() {
echo "This script installs Photonvision."
echo "It must be run as root."
echo
echo "Syntax: sudo ./install.sh [-h|m|n|q]"
echo " options:"
echo " -h Display this help message."
echo " -m Install and configure NetworkManager (Ubuntu only)."
echo " -n Disable networking. This will also prevent installation of NetworkManager."
echo " -q Silent install, automatically accepts all defaults. For non-interactive use."
echo
}
INSTALL_NETWORK_MANAGER="false"
while getopts ":hmnq" name; do
case "$name" in
h)
help
exit 0
;;
m) INSTALL_NETWORK_MANAGER="true"
;;
n) DISABLE_NETWORKING="true"
;;
q) QUIET="true"
;;
\?)
echo "Error: Invalid option -- '$OPTARG'"
echo "Try './install.sh -h' for more information."
exit 1
esac
done
shift $(($OPTIND -1))
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" 1>&2
exit 1
@@ -34,6 +70,16 @@ fi
echo "This is the installation script for PhotonVision."
echo "Installing for platform $ARCH_NAME"
DISTRO=$(lsb_release -is)
if [[ "$DISTRO" = "Ubuntu" && "$INSTALL_NETWORK_MANAGER" != "true" && -z "$QUIET" && -z "$DISABLE_NETWORKING" ]]; then
echo ""
echo "Photonvision uses NetworkManager to control networking on your device."
read -p "Do you want this script to install and configure NetworkManager? [y/N]: " response
if [[ $response == [yY] || $response == [yY][eE][sS] ]]; then
INSTALL_NETWORK_MANAGER="true"
fi
fi
echo "Installing curl..."
apt-get install --yes curl
echo "curl installation complete."
@@ -53,6 +99,16 @@ else
echo 'GOVERNOR=performance' > /etc/default/cpufrequtils
fi
if [[ "$INSTALL_NETWORK_MANAGER" == "true" ]]; then
echo "Installing network-manager..."
apt-get install --yes network-manager
cat > /etc/netplan/00-default-nm-renderer.yaml <<EOF
network:
renderer: NetworkManager
EOF
echo "network-manager installation complete."
fi
echo "Installing the JRE..."
if ! package_is_installed openjdk-17-jre-headless
then
@@ -63,16 +119,19 @@ echo "JRE installation complete."
if [ "$ARCH" == "aarch64" ]
then
if package_is_installed libopencv-core4.5
if package_is_installed libopencv-core4.6
then
echo "libopencv-core4.5 already installed"
echo "libopencv-core4.6 already installed"
else
# libphotonlibcamera.so on raspberry pi has dep on libopencv_core
echo "Installing libopencv-core4.5 on aarch64"
apt-get install --yes libopencv-core4.5
echo "Installing libopencv-core4.6 on aarch64"
apt-get install --yes libopencv-core4.6
fi
fi
echo "Installing additional math packages"
apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5
echo "Downloading latest stable release of PhotonVision..."
mkdir -p /opt/photonvision
cd /opt/photonvision
@@ -85,7 +144,10 @@ echo "Downloaded latest stable release of PhotonVision."
echo "Creating the PhotonVision systemd service..."
# service --status-all doesn't list photonvision on OrangePi use systemctl instead:
#if systemctl --quiet is-active photonvision; then
if service --status-all | grep -Fq 'photonvision'; then
echo "PhotonVision is already running. Stopping service."
systemctl stop photonvision
systemctl disable photonvision
rm /lib/systemd/system/photonvision.service
@@ -116,6 +178,10 @@ RestartSec=1
WantedBy=multi-user.target
EOF
if [ "$DISABLE_NETWORKING" = "true" ]; then
sed -i "s/photonvision.jar/photonvision.jar -n/" /lib/systemd/system/photonvision.service
fi
cp /lib/systemd/system/photonvision.service /etc/systemd/system/photonvision.service
chmod 644 /etc/systemd/system/photonvision.service
systemctl daemon-reload