Compare commits
20 Commits
v2024.1.1-
...
v2024.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6917ec8401 | ||
|
|
a8aa32fab5 | ||
|
|
e40761aaba | ||
|
|
354dd15620 | ||
|
|
07b299a076 | ||
|
|
0cec1eef9f | ||
|
|
68d8a943f7 | ||
|
|
9f0aebe4ce | ||
|
|
6444ae884d | ||
|
|
02df8aa925 | ||
|
|
4d458198c1 | ||
|
|
5cbb507c87 | ||
|
|
e71ce899d6 | ||
|
|
60220f38e6 | ||
|
|
bf5e8dc81b | ||
|
|
b8a6a5d56a | ||
|
|
bf156f544e | ||
|
|
851f2e4e68 | ||
|
|
4068025572 | ||
|
|
f37a0d0300 |
30
.github/workflows/build.yml
vendored
@@ -184,7 +184,7 @@ jobs:
|
||||
- name: Publish
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:publish
|
||||
./gradlew photon-lib:publish photon-targeting:publish
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push'
|
||||
@@ -273,14 +273,20 @@ jobs:
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.4/photonvision_raspi.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight2
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.4/photonvision_limelight.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.4/photonvision_opi5.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 4096
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build image - ${{ matrix.image_url }}"
|
||||
@@ -293,11 +299,23 @@ jobs:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
# TODO- replace with the arm-runner action and run this inside of the chroot. but this works for now.
|
||||
- name: Generate image
|
||||
- uses: pguyot/arm-runner-action@v2
|
||||
name: Generate image
|
||||
id: generate_image
|
||||
with:
|
||||
base_image: ${{ matrix.image_url }}
|
||||
image_additional_mb: ${{ matrix.image_additional_mb }}
|
||||
optimize_image: yes
|
||||
cpu: ${{ matrix.cpu }}
|
||||
commands: |
|
||||
chmod +x scripts/armrunner.sh
|
||||
./scripts/armrunner.sh
|
||||
- name: Compress image
|
||||
run: |
|
||||
chmod +x scripts/generatePiImage.sh
|
||||
./scripts/generatePiImage.sh ${{ matrix.image_url }} ${{ matrix.image_suffix }}
|
||||
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
|
||||
new_image_name=$(basename "${new_jar/.jar/_${{ matrix.image_suffix }}.img}")
|
||||
mv ${{ steps.generate_image.outputs.image }} $new_image_name
|
||||
sudo xz -T 0 -v $new_image_name
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload image
|
||||
with:
|
||||
@@ -320,6 +338,7 @@ jobs:
|
||||
files: |
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
**/photonlib*.json
|
||||
if: github.event_name == 'push'
|
||||
# Upload all jars and xz archives
|
||||
- uses: softprops/action-gh-release@v1
|
||||
@@ -327,6 +346,7 @@ jobs:
|
||||
files: |
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
**/photonlib*.json
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
10
build.gradle
@@ -1,8 +1,10 @@
|
||||
import edu.wpi.first.toolchain.*
|
||||
|
||||
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 +24,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"
|
||||
@@ -96,3 +98,7 @@ spotless {
|
||||
wrapper {
|
||||
gradleVersion '8.4'
|
||||
}
|
||||
|
||||
ext.getCurrentArch = {
|
||||
return NativePlatforms.desktop
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
plugins {
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
}
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
apply from: "${rootDir}/shared/common.gradle"
|
||||
|
||||
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
|
||||
|
||||
def nativeConfigName = 'wpilibNatives'
|
||||
def nativeConfig = configurations.create(nativeConfigName)
|
||||
|
||||
def nativeTasks = wpilibTools.createExtractionTasks {
|
||||
configurationName = nativeConfigName
|
||||
}
|
||||
|
||||
nativeTasks.addToSourceSetResources(sourceSets.main)
|
||||
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + wpi.frcYear.get(), wpi.versions.opencvVersion.get())
|
||||
|
||||
dependencies {
|
||||
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
|
||||
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
|
||||
|
||||
@@ -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="
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -26,6 +26,7 @@ public enum DataChangeDestination {
|
||||
DCD_ACTIVEPIPELINESETTINGS,
|
||||
DCD_GENSETTINGS,
|
||||
DCD_UI,
|
||||
DCD_WEBSERVER,
|
||||
DCD_OTHER;
|
||||
|
||||
public static final List<DataChangeDestination> AllDestinations =
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,24 @@ public class TestUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public enum WPI2024Images {
|
||||
kBackAmpZone_117in,
|
||||
kSpeakerCenter_143in;
|
||||
|
||||
public static double FOV = 68.5;
|
||||
|
||||
public final Path path;
|
||||
|
||||
Path getPath() {
|
||||
var filename = this.toString().substring(1);
|
||||
return Path.of("2024", filename + ".jpg");
|
||||
}
|
||||
|
||||
WPI2024Images() {
|
||||
this.path = getPath();
|
||||
}
|
||||
}
|
||||
|
||||
public enum WPI2023Apriltags {
|
||||
k162_36_Angle,
|
||||
k162_36_Straight,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
plugins {
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
}
|
||||
|
||||
apply plugin: "edu.wpi.first.NativeUtils"
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
ext {
|
||||
@@ -13,9 +7,125 @@ ext {
|
||||
generatedHeaders = "src/generate/native/include"
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/javacpp/setupBuild.gradle"
|
||||
apply plugin: 'cpp'
|
||||
apply plugin: 'google-test-test-suite'
|
||||
apply plugin: 'edu.wpi.first.NativeUtils'
|
||||
|
||||
apply from: "${rootDir}/shared/config.gradle"
|
||||
apply from: "${rootDir}/shared/javacommon.gradle"
|
||||
|
||||
apply from: "${rootDir}/versioningHelper.gradle"
|
||||
|
||||
nativeUtils {
|
||||
exportsConfigs {
|
||||
"${nativeName}" {}
|
||||
}
|
||||
}
|
||||
|
||||
model {
|
||||
components {
|
||||
"${nativeName}"(NativeLibrarySpec) {
|
||||
sources {
|
||||
cpp {
|
||||
source {
|
||||
srcDirs 'src/main/native/cpp', "$buildDir/generated/source/proto/main/cpp"
|
||||
include '**/*.cpp', '**/*.cc'
|
||||
}
|
||||
exportedHeaders {
|
||||
srcDirs 'src/main/native/include', "$buildDir/generated/source/proto/main/cpp"
|
||||
if (project.hasProperty('generatedHeaders')) {
|
||||
srcDir generatedHeaders
|
||||
}
|
||||
include "**/*.h"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binaries.all {
|
||||
it.tasks.withType(CppCompile) {
|
||||
it.dependsOn generateProto
|
||||
}
|
||||
if(project.hasProperty('includePhotonTargeting')) {
|
||||
lib project: ':photon-targeting', library: 'photontargeting', linkage: 'shared'
|
||||
}
|
||||
}
|
||||
|
||||
nativeUtils.useRequiredLibrary(it, "wpilib_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "opencv_shared")
|
||||
}
|
||||
}
|
||||
testSuites {
|
||||
"${nativeName}Test"(GoogleTestTestSuiteSpec) {
|
||||
for(NativeComponentSpec c : $.components) {
|
||||
if (c.name == nativeName) {
|
||||
testing c
|
||||
break
|
||||
}
|
||||
}
|
||||
sources {
|
||||
cpp {
|
||||
source {
|
||||
srcDirs 'src/test/native/cpp'
|
||||
include '**/*.cpp'
|
||||
}
|
||||
exportedHeaders {
|
||||
srcDirs 'src/test/native/include', "$buildDir/generated/source/proto/main/cpp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binaries.all {
|
||||
it.tasks.withType(CppCompile) {
|
||||
it.dependsOn generateProto
|
||||
}
|
||||
if(project.hasProperty('includePhotonTargeting')) {
|
||||
lib project: ':photon-targeting', library: 'photontargeting', linkage: 'shared'
|
||||
}
|
||||
}
|
||||
|
||||
nativeUtils.useRequiredLibrary(it, "cscore_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "cameraserver_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "wpilib_executable_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "googletest_static")
|
||||
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "opencv_shared")
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
def c = $.testSuites
|
||||
project.tasks.create('runCpp', Exec) {
|
||||
description = "Run the photon-lib executable"
|
||||
def found = false
|
||||
def systemArch = getCurrentArch()
|
||||
c.each {
|
||||
if (it in GoogleTestTestSuiteSpec && it.name == "${nativeName}Test") {
|
||||
it.binaries.each {
|
||||
if (!found) {
|
||||
def arch = it.targetPlatform.name
|
||||
if (arch == systemArch) {
|
||||
dependsOn it.tasks.install
|
||||
commandLine it.tasks.install.runScriptFile.get().asFile.toString()
|
||||
def filePath = it.tasks.install.installDirectory.get().toString() + File.separatorChar + 'lib'
|
||||
test.dependsOn it.tasks.install
|
||||
test.systemProperty 'java.library.path', filePath
|
||||
test.environment 'LD_LIBRARY_PATH', filePath
|
||||
test.environment 'DYLD_LIBRARY_PATH', filePath
|
||||
test.workingDir filePath
|
||||
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/javacpp/publish.gradle"
|
||||
|
||||
// Include the version file in the distributed sources
|
||||
cppHeadersZip {
|
||||
from('src/generate/native/include') {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -16,10 +16,24 @@ m = re.search(
|
||||
# which should be PEP440 compliant
|
||||
if m:
|
||||
versionString = m.group(0)
|
||||
prefix = m.group(1)
|
||||
maturity = m.group(2)
|
||||
suffix = m.group(3).replace(".", "")
|
||||
versionString = f"{prefix}.{maturity}.{suffix}"
|
||||
# Hack -- for strings like v2024.1.1, do NOT add matruity/suffix
|
||||
if len(m.group(2)) > 0:
|
||||
print("using beta group matcher")
|
||||
prefix = m.group(1)
|
||||
maturity = m.group(2)
|
||||
suffix = m.group(3).replace(".", "")
|
||||
versionString = f"{prefix}.{maturity}.{suffix}"
|
||||
else:
|
||||
split = gitDescribeResult.split("-")
|
||||
if len(split) == 3:
|
||||
year, commits, sha = split
|
||||
# Chop off leading v from "v2024.1.2", and use "post" for commits to master since
|
||||
versionString = f"{year[1:]}post{commits}"
|
||||
print("using dev release " + versionString)
|
||||
else:
|
||||
year = gitDescribeResult
|
||||
versionString = year[1:]
|
||||
print("using full release " + versionString)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"https://maven.photonvision.org/repository/internal",
|
||||
"https://maven.photonvision.org/repository/snapshots"
|
||||
],
|
||||
"jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/PhotonLib-json/1.0/PhotonLib-json-1.0.json",
|
||||
"jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/photonlib-json/1.0/photonlib-json-1.0.json",
|
||||
"jniDependencies": [],
|
||||
"cppDependencies": [
|
||||
{
|
||||
|
||||
@@ -26,31 +26,23 @@ package org.photonvision;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.cscore.CameraServerCvJNI;
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.MathUtil;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opencv.core.Core;
|
||||
import org.photonvision.estimation.CameraTargetRelation;
|
||||
import org.photonvision.estimation.OpenCVHelp;
|
||||
import org.photonvision.estimation.RotTrlTransform3d;
|
||||
import org.photonvision.estimation.TargetModel;
|
||||
import org.photonvision.simulation.SimCameraProperties;
|
||||
import org.photonvision.simulation.VisionSystemSim;
|
||||
import org.photonvision.simulation.VisionTargetSim;
|
||||
|
||||
public class OpenCVTest {
|
||||
@@ -84,28 +76,8 @@ public class OpenCVTest {
|
||||
private static final SimCameraProperties prop = new SimCameraProperties();
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerCvJNI.Helper.setExtractOnStaticLoad(false);
|
||||
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
VisionSystemSim.class,
|
||||
"wpiutiljni",
|
||||
"ntcorejni",
|
||||
"wpinetjni",
|
||||
"wpiHaljni",
|
||||
Core.NATIVE_LIBRARY_NAME,
|
||||
"cscorejni",
|
||||
"apriltagjni");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
public static void setUp() throws IOException {
|
||||
CameraServerCvJNI.forceLoad();
|
||||
|
||||
// NT live for debug purposes
|
||||
NetworkTableInstance.getDefault().startServer();
|
||||
|
||||
@@ -30,17 +30,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTag;
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -56,19 +50,6 @@ class PhotonPoseEstimatorTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void init() {
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
PhotonPoseEstimatorTest.class, "wpiutiljni", "ntcorejni", "wpinetjni", "wpiHaljni");
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
List<AprilTag> tagList = new ArrayList<>(2);
|
||||
tagList.add(new AprilTag(0, new Pose3d(3, 3, 3, new Rotation3d())));
|
||||
tagList.add(new AprilTag(1, new Pose3d(5, 5, 5, new Rotation3d())));
|
||||
|
||||
@@ -30,10 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTag;
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.cscore.CameraServerCvJNI;
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.geometry.Pose2d;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
@@ -42,11 +38,7 @@ import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation2d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
@@ -58,7 +50,6 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.opencv.core.Core;
|
||||
import org.photonvision.estimation.TargetModel;
|
||||
import org.photonvision.estimation.VisionEstimation;
|
||||
import org.photonvision.simulation.PhotonCameraSim;
|
||||
@@ -85,28 +76,6 @@ class VisionSystemSimTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerCvJNI.Helper.setExtractOnStaticLoad(false);
|
||||
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
VisionSystemSim.class,
|
||||
"wpiutiljni",
|
||||
"ntcorejni",
|
||||
"wpinetjni",
|
||||
"wpiHaljni",
|
||||
Core.NATIVE_LIBRARY_NAME,
|
||||
"cscorejni",
|
||||
"apriltagjni");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// NT live for debug purposes
|
||||
NetworkTableInstance.getDefault().startServer();
|
||||
|
||||
|
||||
@@ -26,14 +26,8 @@ package org.photonvision.estimation;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.apriltag.AprilTagFields;
|
||||
import edu.wpi.first.cscore.CameraServerCvJNI;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import edu.wpi.first.wpilibj.smartdashboard.Field2d;
|
||||
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
|
||||
import java.io.IOException;
|
||||
@@ -44,25 +38,6 @@ import org.photonvision.PhotonPoseEstimator;
|
||||
public class ApriltagWorkbenchTest {
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerCvJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
ApriltagWorkbenchTest.class,
|
||||
"wpiutiljni",
|
||||
"ntcorejni",
|
||||
"wpinetjni",
|
||||
"wpiHaljni",
|
||||
"cscorejnicvstatic");
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// No version check for testing
|
||||
PhotonCamera.setVersionCheckEnabled(false);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.photonvision.common.util.numbers.IntegerCouple;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.server.Server;
|
||||
import org.photonvision.vision.apriltag.AprilTagFamily;
|
||||
import org.photonvision.vision.camera.FileVisionSource;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ContourGroupingMode;
|
||||
@@ -261,6 +262,34 @@ public class Main {
|
||||
camConf2023.pipelineSettings = psList2023;
|
||||
}
|
||||
|
||||
CameraConfiguration camConf2024 =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2024");
|
||||
if (camConf2024 == null || true) {
|
||||
camConf2024 =
|
||||
new CameraConfiguration(
|
||||
"WPI2024",
|
||||
TestUtils.getResourcesFolderPath(true)
|
||||
.resolve("testimages")
|
||||
.resolve(TestUtils.WPI2024Images.kSpeakerCenter_143in.path)
|
||||
.toString());
|
||||
|
||||
camConf2024.FOV = TestUtils.WPI2024Images.FOV;
|
||||
// same camera as 2023
|
||||
camConf2024.calibrations.add(TestUtils.get2023LifeCamCoeffs(true));
|
||||
|
||||
var pipeline2024 = new AprilTagPipelineSettings();
|
||||
var path_split = Path.of(camConf2024.path).getFileName().toString();
|
||||
pipeline2024.pipelineNickname = path_split.replace(".jpg", "");
|
||||
pipeline2024.targetModel = TargetModel.kAprilTag6p5in_36h11;
|
||||
pipeline2024.tagFamily = AprilTagFamily.kTag36h11;
|
||||
pipeline2024.inputShouldShow = true;
|
||||
pipeline2024.solvePNPEnabled = true;
|
||||
|
||||
var psList2024 = new ArrayList<CVPipelineSettings>();
|
||||
psList2024.add(pipeline2024);
|
||||
camConf2024.pipelineSettings = psList2024;
|
||||
}
|
||||
|
||||
// Colored shape testing
|
||||
var camConfShape =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("Shape");
|
||||
@@ -290,12 +319,14 @@ public class Main {
|
||||
var fvs2020 = new FileVisionSource(camConf2020);
|
||||
var fvs2022 = new FileVisionSource(camConf2022);
|
||||
var fvs2023 = new FileVisionSource(camConf2023);
|
||||
var fvs2024 = new FileVisionSource(camConf2024);
|
||||
|
||||
collectedSources.add(fvs2023);
|
||||
collectedSources.add(fvs2022);
|
||||
collectedSources.add(fvsShape);
|
||||
collectedSources.add(fvs2020);
|
||||
collectedSources.add(fvs2019);
|
||||
collectedSources.add(fvs2024);
|
||||
// collectedSources.add(fvs2023);
|
||||
// collectedSources.add(fvs2022);
|
||||
// collectedSources.add(fvsShape);
|
||||
// collectedSources.add(fvs2020);
|
||||
// collectedSources.add(fvs2019);
|
||||
|
||||
ConfigManager.getInstance().unloadCameraConfigs();
|
||||
VisionModuleManager.getInstance().addSources(collectedSources).forEach(VisionModule::start);
|
||||
@@ -385,6 +416,6 @@ public class Main {
|
||||
|
||||
logger.info("Starting server...");
|
||||
HardwareManager.getInstance().setRunning(true);
|
||||
Server.start(DEFAULT_WEBPORT);
|
||||
Server.initialize(DEFAULT_WEBPORT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,122 @@
|
||||
plugins {
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
nativeName = "photontargeting"
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/javacpp/setupBuild.gradle"
|
||||
apply plugin: 'cpp'
|
||||
apply plugin: 'google-test-test-suite'
|
||||
apply plugin: 'edu.wpi.first.NativeUtils'
|
||||
|
||||
apply from: "${rootDir}/shared/config.gradle"
|
||||
apply from: "${rootDir}/shared/javacommon.gradle"
|
||||
|
||||
apply from: "${rootDir}/versioningHelper.gradle"
|
||||
|
||||
nativeUtils {
|
||||
exportsConfigs {
|
||||
"${nativeName}" {}
|
||||
}
|
||||
}
|
||||
|
||||
model {
|
||||
components {
|
||||
"${nativeName}"(NativeLibrarySpec) {
|
||||
sources {
|
||||
cpp {
|
||||
source {
|
||||
srcDirs 'src/main/native/cpp', "$buildDir/generated/source/proto/main/cpp"
|
||||
include '**/*.cpp', '**/*.cc'
|
||||
}
|
||||
exportedHeaders {
|
||||
srcDirs 'src/main/native/include', "$buildDir/generated/source/proto/main/cpp"
|
||||
if (project.hasProperty('generatedHeaders')) {
|
||||
srcDir generatedHeaders
|
||||
}
|
||||
include "**/*.h"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binaries.all {
|
||||
it.tasks.withType(CppCompile) {
|
||||
it.dependsOn generateProto
|
||||
}
|
||||
if(project.hasProperty('includePhotonTargeting')) {
|
||||
lib project: ':photon-targeting', library: 'photontargeting', linkage: 'shared'
|
||||
}
|
||||
}
|
||||
|
||||
nativeUtils.useRequiredLibrary(it, "wpilib_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "opencv_shared")
|
||||
}
|
||||
}
|
||||
testSuites {
|
||||
"${nativeName}Test"(GoogleTestTestSuiteSpec) {
|
||||
for(NativeComponentSpec c : $.components) {
|
||||
if (c.name == nativeName) {
|
||||
testing c
|
||||
break
|
||||
}
|
||||
}
|
||||
sources {
|
||||
cpp {
|
||||
source {
|
||||
srcDirs 'src/test/native/cpp'
|
||||
include '**/*.cpp'
|
||||
}
|
||||
exportedHeaders {
|
||||
srcDirs 'src/test/native/include', "$buildDir/generated/source/proto/main/cpp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binaries.all {
|
||||
it.tasks.withType(CppCompile) {
|
||||
it.dependsOn generateProto
|
||||
}
|
||||
if(project.hasProperty('includePhotonTargeting')) {
|
||||
lib project: ':photon-targeting', library: 'photontargeting', linkage: 'shared'
|
||||
}
|
||||
}
|
||||
|
||||
nativeUtils.useRequiredLibrary(it, "cscore_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "cameraserver_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "wpilib_executable_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "googletest_static")
|
||||
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "opencv_shared")
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
def c = $.testSuites
|
||||
project.tasks.create('runCpp', Exec) {
|
||||
description = "Run the photon-lib executable"
|
||||
def found = false
|
||||
def systemArch = getCurrentArch()
|
||||
c.each {
|
||||
if (it in GoogleTestTestSuiteSpec && it.name == "${nativeName}Test") {
|
||||
it.binaries.each {
|
||||
if (!found) {
|
||||
def arch = it.targetPlatform.name
|
||||
if (arch == systemArch) {
|
||||
dependsOn it.tasks.install
|
||||
commandLine it.tasks.install.runScriptFile.get().asFile.toString()
|
||||
def filePath = it.tasks.install.installDirectory.get().toString() + File.separatorChar + 'lib'
|
||||
test.dependsOn it.tasks.install
|
||||
test.systemProperty 'java.library.path', filePath
|
||||
test.environment 'LD_LIBRARY_PATH', filePath
|
||||
test.environment 'DYLD_LIBRARY_PATH', filePath
|
||||
test.workingDir filePath
|
||||
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/javacpp/publish.gradle"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": true,
|
||||
"currentLanguage": "cpp",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 5
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": true,
|
||||
"currentLanguage": "cpp",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 5
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": true,
|
||||
"currentLanguage": "cpp",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 5
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": true,
|
||||
"currentLanguage": "cpp",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 5
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": true,
|
||||
"currentLanguage": "cpp",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 5
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": false,
|
||||
"currentLanguage": "java",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 6995
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": false,
|
||||
"currentLanguage": "java",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 6995
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": false,
|
||||
"currentLanguage": "java",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 6995
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": false,
|
||||
"currentLanguage": "java",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 6995
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": false,
|
||||
"currentLanguage": "java",
|
||||
"projectYear": "2023",
|
||||
"projectYear": "2024",
|
||||
"teamNumber": 4512
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
12
scripts/armrunner.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
###
|
||||
# Alternative ARM Runner installer to setup PhotonVision JAR
|
||||
# for ARM based builds such as Raspberry Pi, Orange Pi, etc.
|
||||
# This assumes that the image provided to arm-runner-action contains
|
||||
# the servicefile needed to auto-launch PhotonVision.
|
||||
###
|
||||
NEW_JAR=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
|
||||
echo "Using jar: " $(basename $NEW_JAR)
|
||||
|
||||
DEST_PV_LOCATION=/opt/photonvision
|
||||
sudo mkdir -p $DEST_PV_LOCATION
|
||||
sudo cp $NEW_JAR ${DEST_PV_LOCATION}/photonvision.jar
|
||||
@@ -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
|
||||
|
||||
BIN
test-resources/testimages/2024/Amp_42in.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
test-resources/testimages/2024/Amp_85in.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
test-resources/testimages/2024/BackAmpZone_117in.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
test-resources/testimages/2024/GeneralField1.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
test-resources/testimages/2024/GeneralField2.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
test-resources/testimages/2024/GeneralField3.jpg
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
test-resources/testimages/2024/GeneralField4.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
test-resources/testimages/2024/GeneralField5.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
test-resources/testimages/2024/Loading_83in.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
test-resources/testimages/2024/Podium_103in.jpg
Normal file
|
After Width: | Height: | Size: 143 KiB |
13
test-resources/testimages/2024/README.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
This folder contains images of the AprilTags on the 2024 playing field. All measurements included
|
||||
are approximate and are intended to give a rough idea of views from various distances from field
|
||||
elements. All images were taken with a Microsoft Lifecam at 1280x720 resolution.
|
||||
Camera height: ~29.75 in
|
||||
Camera ~20 degrees pitch above horizontal
|
||||
The folder includes 2 types of images:
|
||||
1. "General Field" images which provide a
|
||||
general idea of the view from various spots on the field with a camera at this
|
||||
height and angle.
|
||||
2. Specific field element images which include a measurement away from a specific field element.
|
||||
The Amp, Loading, and Stage photos are measured to the face of the respective elements,
|
||||
approximately perpendicular from the face. The remaining photos are all measured to the center of
|
||||
the subwoofer face, perpendicular or diagonal as necessary.
|
||||
BIN
test-resources/testimages/2024/SpeakerCenter_143in.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
test-resources/testimages/2024/SpeakerCenter_19in.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
test-resources/testimages/2024/SpeakerCenter_63in.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
test-resources/testimages/2024/StageLeft_51in.jpg
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
test-resources/testimages/2024/StageRight_51in.jpg
Normal file
|
After Width: | Height: | Size: 194 KiB |