Switch from FasterXML Jackson to Avaje Jsonb (#2503)

## Description

WPILib switched from FasterXML Jackson to Avaje Jsonb for speed reasons
in https://github.com/wpilibsuite/allwpilib/pull/8721. This does the
same for PhotonVision. Some temporary Jackson adapters are present to
allow compatibility with alpha-4 ahead of updating Photon's WPILib
version. A few old backwards compatibility migrations were also dropped
if they were difficult to port to Avaje Jsonb or otherwise complicated
the code.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_, including events
that led to this PR
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with all settings going back to the previous seasons's last release
(seasons end after champs ends)
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
- [ ] If this PR adds a dependency, the license has been checked for
compatibility and steps taken to follow it

---------

Co-authored-by: samfreund <samf.236@proton.me>
Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
This commit is contained in:
Alan Everett
2026-05-24 13:05:10 -04:00
committed by GitHub
parent 4db3d7be57
commit 0525e762b4
95 changed files with 1306 additions and 1216 deletions

View File

@@ -98,7 +98,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node.js
@@ -239,7 +239,7 @@ jobs:
include:
# - os: windows-2022
# artifact-name: Win64
- os: macos-26
- os: macos-15 # TODO: Restore to macos-26 with WPILib alpha-6
artifact-name: macOS
- os: ubuntu-24.04
artifact-name: Linux

View File

@@ -2,7 +2,7 @@ cppHeaderFileInclude {
\.h$
}
modifiableFileExclude {
generatedFileExclude {
photon-lib/py/photonlibpy/generated/
photon-targeting/src/generated/
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/

View File

@@ -67,7 +67,7 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis
* [EJML](https://github.com/lessthanoptimal/ejml)
* [Javalin](https://javalin.io/)
* [JSON](https://json.org)
* [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
* [Avaje](https://avaje.io) - Specifically [jsonb](https://avaje.io/jsonb/)
* [MessagePack for Java](https://github.com/msgpack/msgpack-java)
* [OSHI](https://github.com/oshi/oshi)
* [QuickBuffers](https://github.com/HebiRobotics/QuickBuffers)

View File

@@ -4,7 +4,7 @@ plugins {
id "cpp"
id "com.diffplug.spotless" version "8.1.0"
id "org.wpilib.WPILibRepositoriesPlugin" version "2027.0.0"
id 'org.wpilib.NativeUtils' version '2027.4.1' apply false
id 'org.wpilib.NativeUtils' version '2027.5.1' apply false
id 'org.wpilib.DeployUtils' version '2027.1.0' apply false
id 'org.photonvision.tools.WpilibTools' version '3.0.0-photon'
id 'com.google.protobuf' version '0.9.5' apply false
@@ -40,6 +40,8 @@ ext {
openCVversion = "4.10.0-3"
ejmlVersion = "0.43.1";
jacksonVersion = "2.15.2";
avajeJsonbVersion = "3.14-RC4";
msgpackVersion = "0.9.0";
quickbufVersion = "1.3.3";
jacocoVersion = "0.8.14";
@@ -69,7 +71,7 @@ spotless {
java {
target fileTree('.') {
include '**/*.java'
exclude '**/build/**', '**/build-*/**', '**/src/generated/**'
exclude '**/build/**', '**/build-*/**', '**/src/generated/**', "**/bin/generated-sources/**"
}
toggleOffOn()
googleJavaFormat()

View File

@@ -1,57 +1,51 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
import { cameraInfoFor } from "@/lib/PhotonUtils";
import type { PVCameraInfo } from "@/types/SettingTypes";
const { camera } = defineProps({
camera: {
type: PVCameraInfo,
required: true
}
});
const { camera } = defineProps<{ camera: PVCameraInfo }>();
</script>
<template>
<div>
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody>
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
<tr v-if="'dev' in camera && camera.dev !== null">
<td>Device Number:</td>
<td>{{ cameraInfoFor(camera).dev }}</td>
<td>{{ camera.dev }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).name !== undefined && cameraInfoFor(camera).name !== null">
<tr v-if="'name' in camera && camera.name !== null">
<td>Name:</td>
<td>{{ cameraInfoFor(camera).name }}</td>
<td>{{ camera.name }}</td>
</tr>
<tr>
<td>Type:</td>
<td v-if="camera.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="camera.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="camera.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-if="camera.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td>
<td v-else-if="camera.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td>
<td v-else-if="camera.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
</tr>
<tr v-if="cameraInfoFor(camera).baseName !== undefined && cameraInfoFor(camera).baseName !== null">
<tr v-if="'baseName' in camera && camera.baseName !== null">
<td>Base Name:</td>
<td>{{ cameraInfoFor(camera).baseName }}</td>
<td>{{ camera.baseName }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).vendorId !== undefined && cameraInfoFor(camera).vendorId !== null">
<tr v-if="'vendorId' in camera && camera.vendorId !== null">
<td>Vendor ID:</td>
<td>{{ cameraInfoFor(camera).vendorId }}</td>
<td>{{ camera.vendorId }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).productId !== undefined && cameraInfoFor(camera).productId !== null">
<tr v-if="'productId' in camera && camera.productId !== null">
<td>Product ID:</td>
<td>{{ cameraInfoFor(camera).productId }}</td>
<td>{{ camera.productId }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).path !== undefined && cameraInfoFor(camera).path !== null">
<tr v-if="'path' in camera && camera.path !== null">
<td>Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(camera).path }}</td>
<td style="word-break: break-all">{{ camera.path }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).uniquePath !== undefined && cameraInfoFor(camera).uniquePath !== null">
<tr v-if="'uniquePath' in camera && camera.uniquePath !== null">
<td>Unique Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(camera).uniquePath }}</td>
<td style="word-break: break-all">{{ camera.uniquePath }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).otherPaths !== undefined && cameraInfoFor(camera).otherPaths !== null">
<tr v-if="'otherPaths' in camera && camera.otherPaths !== null">
<td>Other Paths:</td>
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
<td>{{ camera.otherPaths }}</td>
</tr>
</tbody>
</v-table>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
import { cameraInfoFor } from "@/lib/PhotonUtils";
import type { PVCameraInfo } from "@/types/SettingTypes";
function isEqual<T>(a: T, b: T): boolean {
if (a === b) {
@@ -16,16 +15,7 @@ function isEqual<T>(a: T, b: T): boolean {
);
}
const { saved, current } = defineProps({
saved: {
type: PVCameraInfo,
required: true
},
current: {
type: PVCameraInfo,
required: true
}
});
const { saved, current } = defineProps<{ saved: PVCameraInfo; current: PVCameraInfo }>();
</script>
<template>
@@ -38,79 +28,70 @@ const { saved, current } = defineProps({
<th>Current</th>
</tr>
<tr
v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null"
:class="cameraInfoFor(saved).dev !== cameraInfoFor(current).dev ? 'mismatch' : ''"
v-if="'dev' in saved && 'dev' in current && saved.dev !== null"
:class="saved.dev !== current.dev ? 'mismatch' : ''"
>
<td>Device Number:</td>
<td>{{ cameraInfoFor(saved).dev }}</td>
<td>{{ cameraInfoFor(current).dev }}</td>
<td>{{ saved.dev }}</td>
<td>{{ current.dev }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null"
:class="cameraInfoFor(saved).name !== cameraInfoFor(current).name ? 'mismatch' : ''"
>
<tr v-if="saved.name !== null" :class="saved.name !== current.name ? 'mismatch' : ''">
<td>Name:</td>
<td>{{ cameraInfoFor(saved).name }}</td>
<td>{{ cameraInfoFor(current).name }}</td>
<td>{{ saved.name }}</td>
<td>{{ current.name }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).baseName !== undefined && cameraInfoFor(saved).baseName !== null"
:class="cameraInfoFor(saved).baseName !== cameraInfoFor(current).baseName ? 'mismatch' : ''"
v-if="'baseName' in saved && 'baseName' in current && saved.baseName !== null"
:class="saved.baseName !== current.baseName ? 'mismatch' : ''"
>
<td>Base Name:</td>
<td>{{ cameraInfoFor(saved).baseName }}</td>
<td>{{ cameraInfoFor(current).baseName }}</td>
<td>{{ saved.baseName }}</td>
<td>{{ current.baseName }}</td>
</tr>
<tr>
<td>Type:</td>
<td v-if="saved.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="saved.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="saved.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-if="saved.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td>
<td v-else-if="saved.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td>
<td v-else-if="saved.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
<td v-if="current.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="current.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="current.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-if="current.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td>
<td v-else-if="current.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td>
<td v-else-if="current.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
</tr>
<tr
v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null"
:class="cameraInfoFor(saved).vendorId !== cameraInfoFor(current).vendorId ? 'mismatch' : ''"
v-if="'vendorId' in saved && 'vendorId' in current && saved.vendorId !== null"
:class="saved.vendorId !== current.vendorId ? 'mismatch' : ''"
>
<td>Vendor ID:</td>
<td>{{ cameraInfoFor(saved).vendorId }}</td>
<td>{{ cameraInfoFor(current).vendorId }}</td>
<td>{{ saved.vendorId }}</td>
<td>{{ current.vendorId }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null"
:class="cameraInfoFor(saved).productId !== cameraInfoFor(current).productId ? 'mismatch' : ''"
v-if="'productId' in saved && 'productId' in current && saved.productId !== null"
:class="saved.productId !== current.productId ? 'mismatch' : ''"
>
<td>Product ID:</td>
<td>{{ cameraInfoFor(saved).productId }}</td>
<td>{{ cameraInfoFor(current).productId }}</td>
<td>{{ saved.productId }}</td>
<td>{{ current.productId }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null"
:class="cameraInfoFor(saved).path !== cameraInfoFor(current).path ? 'mismatch' : ''"
>
<tr v-if="saved.path !== null" :class="saved.path !== current.path ? 'mismatch' : ''">
<td>Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(saved).path }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(current).path }}</td>
<td style="word-break: break-all">{{ saved.path }}</td>
<td style="word-break: break-all">{{ current.path }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).uniquePath !== undefined && cameraInfoFor(saved).uniquePath !== null"
:class="cameraInfoFor(saved).uniquePath !== cameraInfoFor(current).uniquePath ? 'mismatch' : ''"
>
<tr v-if="saved.uniquePath !== null" :class="saved.uniquePath !== current.uniquePath ? 'mismatch' : ''">
<td>Unique Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(saved).uniquePath }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(current).uniquePath }}</td>
<td style="word-break: break-all">{{ saved.uniquePath }}</td>
<td style="word-break: break-all">{{ current.uniquePath }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
:class="isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? '' : 'mismatch'"
v-if="'otherPaths' in saved && 'otherPaths' in current && saved.otherPaths !== null"
:class="isEqual(saved.otherPaths, current.otherPaths) ? '' : 'mismatch'"
>
<td>Other Paths:</td>
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
<td>{{ cameraInfoFor(current).otherPaths }}</td>
<td>{{ saved.otherPaths }}</td>
<td>{{ current.otherPaths }}</td>
</tr>
</tbody>
</v-table>

View File

@@ -163,7 +163,7 @@ const interactiveCols = computed(() =>
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames"
:disabled="!useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.PVUsbCameraInfo"
:disabled="useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.type !== 'PVUsbCameraInfo'"
label="Low Latency Mode"
:switch-cols="interactiveCols"
tooltip="When enabled, USB cameras wait for the next camera frame for lowest latency. When disabled, uses the most recent available frame for higher FPS."

View File

@@ -1,5 +1,5 @@
import { useStateStore } from "@/stores/StateStore";
import type { PVCameraInfo, Resolution } from "@/types/SettingTypes";
import type { Resolution } from "@/types/SettingTypes";
import axios, { type AxiosRequestConfig } from "axios";
export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
@@ -109,23 +109,3 @@ export const axiosPost = async (
return false;
}
};
type CameraInfoDetails = Partial<
NonNullable<PVCameraInfo["PVUsbCameraInfo"]> &
NonNullable<PVCameraInfo["PVCSICameraInfo"]> &
NonNullable<PVCameraInfo["PVFileCameraInfo"]>
>;
export const cameraInfoFor = (camera: PVCameraInfo | null): CameraInfoDetails => {
if (!camera) return {};
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};

View File

@@ -116,23 +116,20 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
inputPort: d.inputStreamPort,
outputPort: d.outputStreamPort
},
validVideoFormats: Object.entries(d.videoFormatList)
.sort(([firstKey], [secondKey]) => parseInt(firstKey) - parseInt(secondKey))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map<VideoFormat>(([k, v], i) => ({
resolution: {
width: v.width,
height: v.height
},
fps: v.fps,
pixelFormat: v.pixelFormat,
index: v.index || i,
diagonalFOV: v.diagonalFOV,
horizontalFOV: v.horizontalFOV,
verticalFOV: v.verticalFOV,
standardDeviation: v.standardDeviation,
mean: v.mean
})),
validVideoFormats: d.videoFormatList.map((v, i) => ({
resolution: {
width: v.width,
height: v.height
},
fps: v.fps,
pixelFormat: v.pixelFormat,
index: v.index || i,
diagonalFOV: v.diagonalFOV,
horizontalFOV: v.horizontalFOV,
verticalFOV: v.verticalFOV,
standardDeviation: v.standardDeviation,
mean: v.mean
})),
completeCalibrations: d.calibrations,
isCSICamera: d.isCSICamera,
minExposureRaw: d.minExposureRaw,

View File

@@ -75,46 +75,29 @@ export type ConfigurableNetworkSettings = Omit<
"canManage" | "networkInterfaceNames" | "networkingDisabled"
>;
export interface PVCameraInfoBase {
/*
Huge hack. In Jackson, this is set based on the underlying type -- this
then maps to one of the 3 subclasses here below. Not sure how to best deal with this.
*/
cameraTypename: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo";
interface PVCameraInfoBase {
type: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo";
path: string;
name: string;
uniquePath: string;
}
export interface PVUsbCameraInfo {
export interface PVUsbCameraInfo extends PVCameraInfoBase {
type: "PVUsbCameraInfo";
dev: number;
name: string;
otherPaths: string[];
path: string;
vendorId: number;
productId: number;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
}
export interface PVCSICameraInfo {
export interface PVCSICameraInfo extends PVCameraInfoBase {
type: "PVCSICameraInfo";
baseName: string;
path: string;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
}
export interface PVFileCameraInfo {
path: string;
name: string;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
export interface PVFileCameraInfo extends PVCameraInfoBase {
type: "PVFileCameraInfo";
}
// This camera info will only ever hold one of its members - the others should be undefined.
export class PVCameraInfo {
PVUsbCameraInfo: PVUsbCameraInfo | undefined;
PVCSICameraInfo: PVCSICameraInfo | undefined;
PVFileCameraInfo: PVFileCameraInfo | undefined;
}
export type PVCameraInfo = PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo;
export interface VsmState {
disabledConfigs: WebsocketCameraSettingsUpdate[];
@@ -439,13 +422,10 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
minWhiteBalanceTemp: 2000,
maxWhiteBalanceTemp: 10000,
matchedCameraInfo: {
PVFileCameraInfo: {
name: "Foobar",
path: "/dev/foobar",
uniquePath: "/dev/foobar2"
},
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
type: "PVFileCameraInfo",
name: "Foobar",
path: "/dev/foobar",
uniquePath: "/dev/foobar2"
},
fpsLimit: -1,
isEnabled: true,

View File

@@ -30,21 +30,18 @@ export interface WebsocketNumberPair {
second: number;
}
export type WebsocketVideoFormat = Record<
number,
{
fps: number;
height: number;
width: number;
pixelFormat: string;
index?: number;
diagonalFOV?: number;
horizontalFOV?: number;
verticalFOV?: number;
standardDeviation?: number;
mean?: number;
}
>;
export type WebsocketVideoFormat = {
fps: number;
height: number;
width: number;
pixelFormat: string;
index?: number;
diagonalFOV?: number;
horizontalFOV?: number;
verticalFOV?: number;
standardDeviation?: number;
mean?: number;
}[];
// Companion to UICameraConfiguration in Java
export interface WebsocketCameraSettingsUpdate {

View File

@@ -2,8 +2,8 @@
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, inject, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { PlaceholderCameraSettings, PVCameraInfo } from "@/types/SettingTypes";
import { axiosPost, getResolutionString, cameraInfoFor } from "@/lib/PhotonUtils";
import { PlaceholderCameraSettings, type PVCameraInfo } from "@/types/SettingTypes";
import { axiosPost, getResolutionString } from "@/lib/PhotonUtils";
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
@@ -59,22 +59,15 @@ const deleteThisCamera = async (cameraUniqueName: string) => {
const cameraConnected = (uniquePath: string | undefined): boolean => {
if (!uniquePath) return false;
return (
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
);
return useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === uniquePath) !== undefined;
};
const unmatchedCameras = computed(() => {
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
);
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
);
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map((it) => it.matchedCameraInfo.uniquePath);
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map((it) => it.matchedCameraInfo.uniquePath);
return useStateStore().vsmState.allConnectedCameras.filter(
(it) =>
!activeVmPaths.includes(cameraInfoFor(it).uniquePath) && !disabledVmPaths.includes(cameraInfoFor(it).uniquePath)
(it) => !activeVmPaths.includes(it.uniquePath) && !disabledVmPaths.includes(it.uniquePath)
);
});
@@ -85,8 +78,8 @@ const activeVisionModules = computed(() =>
// Display connected cameras first
.sort(
(first, second) =>
(cameraConnected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
(cameraConnected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
(cameraConnected(second.matchedCameraInfo.uniquePath) ? 1 : 0) -
(cameraConnected(first.matchedCameraInfo.uniquePath) ? 1 : 0)
)
);
@@ -105,18 +98,18 @@ const setCameraView = (camera: PVCameraInfo | null, isConnected: boolean | null)
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) {
return {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
type: "PVFileCameraInfo",
path: "",
name: "",
uniquePath: ""
};
}
return (
useStateStore().vsmState.allConnectedCameras.find(
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
) || {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === info.uniquePath) || {
type: "PVFileCameraInfo",
path: "",
name: "",
uniquePath: ""
}
);
};
@@ -135,12 +128,11 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="pr-0"
>
<v-card color="surface" class="rounded-12">
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
<v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
<v-card-title>{{ module.matchedCameraInfo.name }}</v-card-title>
<v-card-subtitle v-if="!cameraConnected(module.matchedCameraInfo.uniquePath)"
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
>
<v-card-subtitle
v-else-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) && !module.mismatch"
<v-card-subtitle v-else-if="cameraConnected(module.matchedCameraInfo.uniquePath) && !module.mismatch"
>Status: <span class="active-status">Active</span></v-card-subtitle
>
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
@@ -149,7 +141,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<tbody>
<tr
v-if="
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
cameraConnected(module.matchedCameraInfo.uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
@@ -191,7 +183,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</tbody>
</v-table>
<div
v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
v-if="cameraConnected(module.matchedCameraInfo.uniquePath)"
:id="`stream-container-${index}`"
class="d-flex flex-column justify-center align-center mt-3"
style="height: 250px"
@@ -210,12 +202,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonPassive"
style="width: 100%"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
setCameraView(
module.matchedCameraInfo,
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
>
<span>Details</span>
</v-btn>
@@ -292,7 +279,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
<td>{{ cameraConnected(module.matchedCameraInfo.uniquePath) }}</td>
</tr>
</tbody>
</v-table>
@@ -304,12 +291,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonPassive"
style="width: 100%"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
setCameraView(
module.matchedCameraInfo,
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
>
<span>Details</span>
</v-btn>
@@ -354,15 +336,15 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4" class="pr-0">
<v-card class="pr-0 rounded-12" color="surface">
<v-card-title>
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
<span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
<span v-else-if="camera.PVFileCameraInfo">File Camera:</span>
<span v-if="camera.type === 'PVUsbCameraInfo'">USB Camera:</span>
<span v-else-if="camera.type === 'PVCSICameraInfo'">CSI Camera:</span>
<span v-else-if="camera.type === 'PVFileCameraInfo'">File Camera:</span>
<span v-else>Unknown Camera:</span>
&nbsp;<span>{{ cameraInfoFor(camera)?.name ?? cameraInfoFor(camera)?.baseName }}</span>
&nbsp;<span>{{ camera.name }}</span>
</v-card-title>
<v-card-subtitle>Status: Unassigned</v-card-subtitle>
<v-card-text class="pt-3">
<span style="word-break: break-all">{{ cameraInfoFor(camera)?.path }}</span>
<span style="word-break: break-all">{{ camera?.path }}</span>
</v-card-text>
<v-card-text class="pt-0">
<v-row>
@@ -413,7 +395,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-dialog v-model="viewingDetails" max-width="800">
<v-card v-if="viewingCamera[0] !== null" flat color="surface">
<v-card-title class="d-flex justify-space-between">
<span>{{ cameraInfoFor(viewingCamera[0])?.name ?? cameraInfoFor(viewingCamera[0])?.baseName }}</span>
<span>{{ viewingCamera[0].name }}</span>
<v-btn variant="text" @click="setCameraView(null, null)">
<v-icon size="x-large">mdi-close</v-icon>
</v-btn>
@@ -423,9 +405,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</v-card-text>
<v-card-text
v-else-if="
activeVisionModules.find(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath === cameraInfoFor(viewingCamera[0]).uniquePath
)?.mismatch
activeVisionModules.find((it) => it.matchedCameraInfo.uniquePath === viewingCamera[0]?.uniquePath)?.mismatch
"
>
<v-alert

View File

@@ -17,9 +17,7 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -36,6 +34,7 @@ import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.processes.PipelineManager;
import org.wpilib.vision.camera.UsbCameraInfo;
@Json
public class CameraConfiguration {
private static final Logger logger = new Logger(CameraConfiguration.class, LogGroup.Camera);
@@ -62,11 +61,8 @@ public class CameraConfiguration {
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
// Ignore the pipes, as we serialize them to their own column to hack around
// polymorphic lists
@JsonIgnore public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
@JsonIgnore
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
public CameraConfiguration(PVCameraInfo cameraInfo, String uniqueName, String nickname) {
@@ -78,24 +74,22 @@ public class CameraConfiguration {
logger.debug("Creating USB camera configuration for " + this.toShortString());
}
// Shiny new constructor
@JsonCreator
// JSON Constructor (can't be marked with @Json.Creator due to public fields that aren't part of
// the parameters)
public CameraConfiguration(
@JsonProperty("uniqueName") String uniqueName,
@JsonProperty("matchedCameraInfo") PVCameraInfo matchedCameraInfo,
@JsonProperty("nickname") String nickname,
@JsonProperty("deactivated") boolean deactivated,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("FOV") double FOV,
@JsonProperty("calibrations") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
String uniqueName,
PVCameraInfo matchedCameraInfo,
String nickname,
boolean deactivated,
QuirkyCamera cameraQuirks,
double FOV,
int currentPipelineIndex) {
this.uniqueName = uniqueName;
this.matchedCameraInfo = matchedCameraInfo;
this.nickname = nickname;
this.deactivated = deactivated;
this.cameraQuirks = cameraQuirks;
this.FOV = FOV;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex;
}
@@ -120,14 +114,14 @@ public class CameraConfiguration {
PVCameraInfo matchedCameraInfo;
/** Legacy constructor for compat with 2024.3.1 */
@JsonCreator
@Json.Creator
public LegacyCameraConfigStruct(
@JsonProperty("baseName") String baseName,
@JsonProperty("path") String path,
@JsonProperty("otherPaths") String[] otherPaths,
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("usbVID") int usbVID,
@JsonProperty("usbPID") int usbPID) {
String baseName,
String path,
String[] otherPaths,
CameraType cameraType,
int usbVID,
int usbPID) {
if (cameraType == CameraType.UsbCamera) {
this.matchedCameraInfo =
PVCameraInfo.fromUsbCameraInfo(
@@ -171,16 +165,16 @@ public class CameraConfiguration {
}
/**
* Replace a calibration in our list with the same unrotatedImageSize with a new one, or add it if
* none exists yet. If we are replacing an existing calibration, the old one will be "released"
* and the underlying data matrices will become invalid.
* Replace a calibration in our list with the same resolution with a new one, or add it if none
* exists yet. If we are replacing an existing calibration, the old one will be "released" and the
* underlying data matrices will become invalid.
*
* @param calibration The calibration to add.
*/
public void addCalibration(CameraCalibrationCoefficients calibration) {
logger.info("adding calibration " + calibration.unrotatedImageSize);
logger.info("adding calibration " + calibration.resolution);
calibrations.stream()
.filter(it -> it.unrotatedImageSize.equals(calibration.unrotatedImageSize))
.filter(it -> it.resolution.equals(calibration.resolution))
.findAny()
.ifPresent(
(it) -> {
@@ -194,12 +188,12 @@ public class CameraConfiguration {
* Remove a calibration from our list. If found, the calibration will be "released". If not found,
* no-op.
*
* @param unrotatedImageSize The resolution to remove.
* @param resolution The resolution to remove.
*/
public void removeCalibration(Size unrotatedImageSize) {
logger.info("deleting calibration " + unrotatedImageSize);
public void removeCalibration(Size resolution) {
logger.info("deleting calibration " + resolution);
calibrations.stream()
.filter(it -> it.unrotatedImageSize.equals(unrotatedImageSize))
.filter(it -> it.resolution.equals(resolution))
.findAny()
.ifPresent(
(it) -> {
@@ -215,7 +209,6 @@ public class CameraConfiguration {
*
* <p>This represents our best guess at an immutable path to detect a camera at.
*/
@JsonIgnore
public String getDevicePath() {
return matchedCameraInfo.uniquePath();
}

View File

@@ -17,7 +17,10 @@
package org.photonvision.common.configuration;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -34,7 +37,6 @@ import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.FileUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.processes.VisionSource;
import org.zeroturnaround.zip.ZipUtil;
@@ -233,14 +235,15 @@ public class ConfigManager {
Path.of(getModelsDirectory().toString(), "photonvision-object-detection-models.json")
.toFile();
try {
JacksonUtils.serialize(
tempProperties.toPath(), this.getConfig().neuralNetworkPropertyManager());
Jsonb.instance()
.type(NeuralNetworkModelsSettings.class)
.toJson(this.getConfig().getNeuralNetworkProperties(), new FileWriter(tempProperties));
ZipUtil.pack(getModelsDirectory(), out);
// Now delete the tempProperties
if (tempProperties.exists()) {
Files.delete(tempProperties.toPath());
}
} catch (Exception e) {
} catch (IOException | IllegalStateException | JsonException e) {
e.printStackTrace();
}
return out;

View File

@@ -17,47 +17,49 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.avaje.jsonb.Json;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.hardware.statusLED.StatusLEDType;
@JsonIgnoreProperties(ignoreUnknown = true)
@Json
public class HardwareConfig {
public final String deviceName;
public String deviceName;
// LED control
public final ArrayList<Integer> ledPins;
public final boolean ledsCanDim;
public final ArrayList<Integer> ledBrightnessRange;
public final int ledPWMFrequency;
public final StatusLEDType statusLEDType;
public List<Integer> ledPins;
public boolean ledsCanDim;
public List<Integer> ledBrightnessRange;
public int ledPWMFrequency;
public StatusLEDType statusLEDType;
@JsonAlias("statusRGBPins")
public final ArrayList<Integer> statusLEDPins;
// MIGRATION: 2026
@Json.Alias("statusRGBPins")
public List<Integer> statusLEDPins;
@JsonAlias("statusRGBActiveHigh")
public final boolean statusLEDActiveHigh;
// MIGRATION: 2026
@Json.Alias("statusRGBActiveHigh")
public boolean statusLEDActiveHigh;
// Custom GPIO
public final String getGPIOCommand;
public final String setGPIOCommand;
public final String setPWMCommand;
public final String setPWMFrequencyCommand;
public final String releaseGPIOCommand;
public String getGPIOCommand;
public String setGPIOCommand;
public String setPWMCommand;
public String setPWMFrequencyCommand;
public String releaseGPIOCommand;
// Device stuff
public final String restartHardwareCommand;
public final double vendorFOV; // -1 for unmanaged
public String restartHardwareCommand;
public double vendorFOV; // -1 for unmanaged
public HardwareConfig(
String deviceName,
ArrayList<Integer> ledPins,
List<Integer> ledPins,
boolean ledsCanDim,
ArrayList<Integer> ledBrightnessRange,
List<Integer> ledBrightnessRange,
int ledPwmFrequency,
StatusLEDType statusLEDType,
ArrayList<Integer> statusLEDPins,
List<Integer> statusLEDPins,
boolean statusLEDActiveHigh,
String getGPIOCommand,
String setGPIOCommand,

View File

@@ -17,8 +17,11 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
@@ -34,9 +37,6 @@ import java.util.stream.Stream;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.FileUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.processes.VisionSource;
import org.wpilib.vision.apriltag.AprilTagFieldLayout;
import org.wpilib.vision.apriltag.AprilTagFields;
@@ -126,14 +126,13 @@ class LegacyConfigProvider extends ConfigProvider {
AprilTagFieldLayout atfl = null;
if (hardwareConfigFile.exists()) {
try {
hardwareConfig =
JacksonUtils.deserialize(hardwareConfigFile.toPath(), HardwareConfig.class);
try (var stream = new FileInputStream(hardwareConfigFile)) {
hardwareConfig = Jsonb.instance().type(HardwareConfig.class).fromJson(stream);
if (hardwareConfig == null) {
logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig();
}
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig();
}
@@ -143,14 +142,13 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (hardwareSettingsFile.exists()) {
try {
hardwareSettings =
JacksonUtils.deserialize(hardwareSettingsFile.toPath(), HardwareSettings.class);
try (var stream = new FileInputStream(hardwareSettingsFile)) {
hardwareSettings = Jsonb.instance().type(HardwareSettings.class).fromJson(stream);
if (hardwareSettings == null) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
}
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
}
@@ -160,13 +158,13 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (networkConfigFile.exists()) {
try {
networkConfig = JacksonUtils.deserialize(networkConfigFile.toPath(), NetworkConfig.class);
try (var stream = new FileInputStream(networkConfigFile)) {
networkConfig = Jsonb.instance().type(NetworkConfig.class).fromJson(stream);
if (networkConfig == null) {
logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig();
}
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig();
}
@@ -184,13 +182,12 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (apriltagFieldLayoutFile.exists()) {
try {
atfl =
JacksonUtils.deserialize(apriltagFieldLayoutFile.toPath(), AprilTagFieldLayout.class);
try (var stream = new FileInputStream(apriltagFieldLayoutFile)) {
atfl = Jsonb.instance().type(AprilTagFieldLayout.class).fromJson(stream);
if (atfl == null) {
logger.error("Could not deserialize apriltag field layout! (still null)");
}
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize apriltag field layout!", e);
atfl = null; // not required, nice to be explicit
}
@@ -227,14 +224,14 @@ class LegacyConfigProvider extends ConfigProvider {
// Delete old configs
FileUtils.deleteDirectory(camerasFolder.toPath());
try {
JacksonUtils.serialize(networkConfigFile.toPath(), config.getNetworkConfig());
} catch (IOException e) {
try (var stream = new FileOutputStream(networkConfigFile)) {
Jsonb.instance().type(NetworkConfig.class).toJson(config.getNetworkConfig(), stream);
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save network config!", e);
}
try {
JacksonUtils.serialize(hardwareSettingsFile.toPath(), config.getHardwareSettings());
} catch (IOException e) {
try (var stream = new FileOutputStream(hardwareSettingsFile)) {
Jsonb.instance().type(HardwareSettings.class).toJson(config.getHardwareSettings(), stream);
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save hardware config!", e);
}
@@ -249,33 +246,11 @@ class LegacyConfigProvider extends ConfigProvider {
subdir.toFile().mkdirs();
}
try {
JacksonUtils.serialize(Path.of(subdir.toString(), "config.json"), camConfig);
} catch (IOException e) {
try (var stream = new FileOutputStream(Path.of(subdir.toString(), "config.json").toFile())) {
Jsonb.instance().type(CameraConfiguration.class).toJson(camConfig, stream);
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save config.json for " + subdir, e);
}
try {
JacksonUtils.serialize(
Path.of(subdir.toString(), "drivermode.json"), camConfig.driveModeSettings);
} catch (IOException e) {
logger.error("Could not save drivermode.json for " + subdir, e);
}
for (var pipe : camConfig.pipelineSettings) {
var pipePath = Path.of(subdir.toString(), "pipelines", pipe.pipelineNickname + ".json");
if (!pipePath.getParent().toFile().exists()) {
// TODO: check for error
pipePath.getParent().toFile().mkdirs();
}
try {
JacksonUtils.serialize(pipePath, pipe);
} catch (IOException e) {
logger.error("Could not save " + pipe.pipelineNickname + ".json!", e);
}
}
}
logger.info("Settings saved!");
return false; // TODO, deal with this. Do I need to?
@@ -289,11 +264,9 @@ class LegacyConfigProvider extends ConfigProvider {
for (var subdir : subdirectories) {
var cameraConfigPath = Path.of(subdir.toString(), "config.json");
CameraConfiguration loadedConfig = null;
try {
loadedConfig =
JacksonUtils.deserialize(
cameraConfigPath.toAbsolutePath(), CameraConfiguration.class);
} catch (JsonProcessingException e) {
try (var stream = new FileInputStream(cameraConfigPath.toFile())) {
loadedConfig = Jsonb.instance().type(CameraConfiguration.class).fromJson(stream);
} catch (IllegalStateException | JsonException e) {
logger.error("Camera config deserialization failed!", e);
e.printStackTrace();
}
@@ -302,63 +275,6 @@ class LegacyConfigProvider extends ConfigProvider {
continue; // TODO how do we later try to load this camera if it gets reconnected?
}
// At this point we have only loaded the base stuff
// We still need to deserialize pipelines, as well as
// driver mode settings
var driverModeFile = Path.of(subdir.toString(), "drivermode.json");
DriverModePipelineSettings driverMode;
try {
driverMode =
JacksonUtils.deserialize(
driverModeFile.toAbsolutePath(), DriverModePipelineSettings.class);
} catch (JsonProcessingException e) {
logger.error("Could not deserialize drivermode.json! Loading defaults");
logger.debug(Arrays.toString(e.getStackTrace()));
driverMode = new DriverModePipelineSettings();
}
if (driverMode == null) {
logger.warn(
"Could not load camera " + subdir + "'s drivermode.json! Loading" + " default");
driverMode = new DriverModePipelineSettings();
}
// Load pipelines by mapping the files within the pipelines subdir
// to their deserialized equivalents
var pipelineSubdirectory = Path.of(subdir.toString(), "pipelines");
List<CVPipelineSettings> settings = Collections.emptyList();
if (pipelineSubdirectory.toFile().exists()) {
try (Stream<Path> subdirectoryFiles = Files.list(pipelineSubdirectory)) {
settings =
subdirectoryFiles
.filter(p -> p.toFile().isFile())
.map(
p -> {
var relativizedFilePath =
configDirectoryFile
.toPath()
.toAbsolutePath()
.relativize(p)
.toString();
try {
return JacksonUtils.deserialize(p, CVPipelineSettings.class);
} catch (JsonProcessingException e) {
logger.error("Exception while deserializing " + relativizedFilePath, e);
} catch (IOException e) {
logger.warn(
"Could not load pipeline at "
+ relativizedFilePath
+ "! Skipping...");
}
return null;
})
.filter(Objects::nonNull)
.toList();
}
}
loadedConfig.driveModeSettings = driverMode;
loadedConfig.addPipelineSettings(settings);
loadedConfigurations.put(subdir.toFile().getName(), loadedConfig);
}
} catch (IOException e) {

View File

@@ -17,16 +17,17 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkMode;
@Json
public class NetworkConfig {
// Can be an integer team number, or an IP address
// MIGRATION: 2023
@Json.Alias("teamNumber")
public String ntServerAddress = "0";
public NetworkMode connectionType = NetworkMode.DHCP;
public String staticIp = "";
public String hostname = "photonvision";
@@ -34,8 +35,8 @@ public class NetworkConfig {
public boolean shouldManage;
public boolean shouldPublishProto = false;
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
public static final String NM_IFACE_STRING = "${interface}";
public static final String NM_IP_STRING = "${ipaddr}";
public String networkManagerIface = "";
// TODO: remove these strings if no longer needed
@@ -50,19 +51,17 @@ public class NetworkConfig {
setShouldManage(deviceCanManageNetwork());
}
@JsonCreator
public NetworkConfig(
@JsonProperty("ntServerAddress") @JsonAlias({"ntServerAddress", "teamNumber"})
String ntServerAddress,
@JsonProperty("connectionType") NetworkMode connectionType,
@JsonProperty("staticIp") String staticIp,
@JsonProperty("hostname") String hostname,
@JsonProperty("runNTServer") boolean runNTServer,
@JsonProperty("shouldManage") boolean shouldManage,
@JsonProperty("shouldPublishProto") boolean shouldPublishProto,
@JsonProperty("networkManagerIface") String networkManagerIface,
@JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
String ntServerAddress,
NetworkMode connectionType,
String staticIp,
String hostname,
boolean runNTServer,
boolean shouldManage,
boolean shouldPublishProto,
String networkManagerIface,
String setStaticCommand,
String setDHCPcommand) {
this.ntServerAddress = ntServerAddress;
this.connectionType = connectionType;
this.staticIp = staticIp;
@@ -89,12 +88,10 @@ public class NetworkConfig {
config.setDHCPcommand);
}
@JsonIgnore
public String getPhysicalInterfaceName() {
return this.networkManagerIface;
}
@JsonIgnore
public String getEscapedInterfaceName() {
return "\"" + networkManagerIface + "\"";
}
@@ -103,7 +100,6 @@ public class NetworkConfig {
this.shouldManage = shouldManage && this.deviceCanManageNetwork();
}
@JsonIgnore
protected boolean deviceCanManageNetwork() {
return Platform.isLinux();
}

View File

@@ -318,7 +318,7 @@ public class NeuralNetworkModelManager {
}
ModelProperties properties =
ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().getModel(path);
ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().getModel(path);
if (properties == null) {
logger.warn(
@@ -332,7 +332,7 @@ public class NeuralNetworkModelManager {
// NeuralNetworkModelsSettings
ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.getNeuralNetworkProperties()
.addModelProperties(properties);
} catch (IllegalArgumentException | IOException e) {
logger.error("Failed to translate legacy model filename to properties: " + path, e);
@@ -486,7 +486,7 @@ public class NeuralNetworkModelManager {
.getConfig()
.setNeuralNetworkProperties(
supportedProperties.sum(
ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager()));
ConfigManager.getInstance().getConfig().getNeuralNetworkProperties()));
}
public boolean clearModels() {
@@ -511,7 +511,7 @@ public class NeuralNetworkModelManager {
}
// Delete model info
return ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().clear();
return ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().clear();
}
public File exportSingleModel(String modelPath) {
@@ -525,7 +525,7 @@ public class NeuralNetworkModelManager {
ModelProperties properties =
ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.getNeuralNetworkProperties()
.getModel(Path.of(modelPath));
String fileName = "";

View File

@@ -17,9 +17,10 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -27,27 +28,29 @@ import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
@Json
public class NeuralNetworkModelsSettings {
/*
* The properties of the model. This is used to determine which model to load.
* The only families currently supported are RKNN and Rubik (custom .tflite)
*/
@Json
public record ModelProperties(
@JsonProperty("modelPath") Path modelPath,
@JsonProperty("nickname") String nickname,
@JsonProperty("labels") List<String> labels,
@JsonProperty("resolutionWidth") int resolutionWidth,
@JsonProperty("resolutionHeight") int resolutionHeight,
@JsonProperty("family") Family family,
@JsonProperty("version") Version version) {
@JsonCreator
public ModelProperties {}
Path modelPath,
String nickname,
List<String> labels,
int resolutionWidth,
int resolutionHeight,
Family family,
Version version) {
ModelProperties(ModelProperties other) {
this(
other.modelPath,
@@ -59,13 +62,6 @@ public class NeuralNetworkModelsSettings {
other.version);
}
// In v2025.3.1, this was single string for the model path. but the first argument
// is now nickname
public ModelProperties(@JsonProperty("nickname") String filename)
throws IllegalArgumentException, IOException {
this(createFromFilename(filename));
}
// ============= Migration code from v2025.3.1 ===========
private static Pattern modelPattern =
@@ -160,25 +156,58 @@ public class NeuralNetworkModelsSettings {
// The path to the model is used as the key in the map because it is unique to
// the model, and should not change
@JsonProperty("modelPathToProperties")
@Json.Ignore
private HashMap<Path, ModelProperties> modelPathToProperties =
new HashMap<Path, ModelProperties>();
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a LinkedList of {@link ModelProperties} objects
* <p>This object holds a HashMap of {@link ModelProperties} objects
*/
public NeuralNetworkModelsSettings() {}
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a LinkedList of {@link ModelProperties} objects.
* <p>This object holds a HashMap of {@link ModelProperties} objects.
*
* @param modelPropertiesMap When the class is constructed, it will hold the provided map
*/
public NeuralNetworkModelsSettings(HashMap<Path, ModelProperties> modelPropertiesMap) {
modelPathToProperties = modelPropertiesMap;
}
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a HashMap of {@link ModelProperties} objects.
*
* @param modelPropertiesList When the class is constructed, it will hold the provided list
*/
public NeuralNetworkModelsSettings(HashMap<Path, ModelProperties> modelPropertiesList) {}
@Json.Creator
public NeuralNetworkModelsSettings(
ModelProperties[] models, @Json.Unmapped Map<String, Object> unmapped) {
JsonType<Map<String, ModelProperties>> modelPropsMapJsonb =
Jsonb.instance().type(Types.mapOf(ModelProperties.class));
Stream<ModelProperties> modelPropsStream;
if (models != null) {
modelPropsStream = Arrays.stream(models);
} else if (unmapped.containsKey("modelPathToProperties")) {
// MIGRATION: 2026
modelPropsStream =
modelPropsMapJsonb.fromObject(unmapped.get("modelPathToProperties")).values().stream();
} else {
modelPropsStream = Stream.empty();
}
this(
modelPropsStream.collect(
Collectors.toMap(
(model) -> model.modelPath(),
(model) -> model,
(prev, next) -> next,
HashMap::new)));
}
@Override
public String toString() {
@@ -239,7 +268,7 @@ public class NeuralNetworkModelsSettings {
*
* @return A list of all models
*/
@JsonIgnore
@Json.Property("models")
public ModelProperties[] getModels() {
return modelPathToProperties.values().toArray(new ModelProperties[0]);
}

View File

@@ -17,19 +17,25 @@
package org.photonvision.common.configuration;
import io.avaje.jsonb.Json;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.photonvision.vision.processes.VisionSource;
import org.wpilib.vision.apriltag.AprilTagFieldLayout;
@Json
public class PhotonConfiguration {
private final HardwareConfig hardwareConfig;
private final HardwareSettings hardwareSettings;
private NetworkConfig networkConfig;
private AprilTagFieldLayout atfl;
@Json.Property("atfl")
private AprilTagFieldLayout aprilTagFieldLayout;
private NeuralNetworkModelsSettings neuralNetworkProperties;
private HashMap<String, CameraConfiguration> cameraConfigurations;
private Map<String, CameraConfiguration> cameraConfigurations;
public PhotonConfiguration(
HardwareConfig hardwareConfig,
@@ -46,19 +52,20 @@ public class PhotonConfiguration {
new HashMap<>());
}
@Json.Creator
public PhotonConfiguration(
HardwareConfig hardwareConfig,
HardwareSettings hardwareSettings,
NetworkConfig networkConfig,
AprilTagFieldLayout atfl,
NeuralNetworkModelsSettings neuralNetworkProperties,
HashMap<String, CameraConfiguration> cameraConfigurations) {
Map<String, CameraConfiguration> cameraConfigurations) {
this.hardwareConfig = hardwareConfig;
this.hardwareSettings = hardwareSettings;
this.networkConfig = networkConfig;
this.neuralNetworkProperties = neuralNetworkProperties;
this.cameraConfigurations = cameraConfigurations;
this.atfl = atfl;
this.aprilTagFieldLayout = atfl;
}
public PhotonConfiguration() {
@@ -83,15 +90,15 @@ public class PhotonConfiguration {
}
public AprilTagFieldLayout getApriltagFieldLayout() {
return atfl;
return aprilTagFieldLayout;
}
public NeuralNetworkModelsSettings neuralNetworkPropertyManager() {
public NeuralNetworkModelsSettings getNeuralNetworkProperties() {
return neuralNetworkProperties;
}
public void setApriltagFieldLayout(AprilTagFieldLayout atfl) {
this.atfl = atfl;
this.aprilTagFieldLayout = atfl;
}
public void setNetworkConfig(NetworkConfig networkConfig) {
@@ -102,7 +109,7 @@ public class PhotonConfiguration {
this.neuralNetworkProperties = neuralNetworkProperties;
}
public HashMap<String, CameraConfiguration> getCameraConfigurations() {
public Map<String, CameraConfiguration> getCameraConfigurations() {
return cameraConfigurations;
}
@@ -148,8 +155,8 @@ public class PhotonConfiguration {
+ hardwareSettings
+ "\n networkConfig="
+ networkConfig
+ "\n atfl="
+ atfl
+ "\n aprilTagFieldLayout="
+ aprilTagFieldLayout
+ "\n neuralNetworkProperties="
+ neuralNetworkProperties
+ "\n cameraConfigurations={"

View File

@@ -17,24 +17,27 @@
package org.photonvision.common.configuration;
import io.avaje.json.JsonException;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.photonvision.common.configuration.CameraConfiguration.LegacyCameraConfigStruct;
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;
import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.wpilib.vision.apriltag.AprilTagFieldLayout;
@@ -260,16 +263,16 @@ public class SqlConfigProvider extends ConfigProvider {
T configObj;
if (!configString.isBlank()) {
try {
configObj = JacksonUtils.deserialize(configString, ref);
configObj = Jsonb.instance().type(ref).fromJson(configString);
logger.info("Loaded " + ref.getSimpleName() + " from database");
return configObj;
} catch (IOException e) {
} catch (IllegalStateException | JsonException e) {
logger.error("Could not deserialize " + ref.getSimpleName() + " from database!", e);
}
} else {
logger.debug("No " + ref.getSimpleName() + " in database");
}
// either the config entry is empty or Jackson threw an exception
// either the config entry is empty or Jsonb threw an exception
try {
configObj = factory.get();
logger.info("Loaded default " + ref.getSimpleName());
@@ -390,30 +393,16 @@ public class SqlConfigProvider extends ConfigProvider {
var config = c.getValue();
statement.setString(1, c.getKey());
statement.setString(2, JacksonUtils.serializeToString(config));
statement.setString(3, JacksonUtils.serializeToString(config.driveModeSettings));
statement.setString(2, Jsonb.instance().type(CameraConfiguration.class).toJson(config));
// Serializing a list of abstract classes sucks. Instead, make it into an array
// of strings, which we can later unpack back into individual settings
List<String> settings =
config.pipelineSettings.stream()
.map(
it -> {
try {
return JacksonUtils.serializeToString(it);
} catch (IOException e) {
e.printStackTrace();
return null;
}
})
.filter(Objects::nonNull)
.toList();
statement.setString(4, JacksonUtils.serializeToString(settings));
// MIGRATION: 2026
// We used to serialize pipelines separately, but don't anymore
statement.setString(3, "null");
statement.setString(4, "[]");
statement.executeUpdate();
}
} catch (SQLException | IOException e) {
} catch (SQLException | IllegalStateException | JsonException e) {
logger.error("Err saving cameras", e);
try {
conn.rollback();
@@ -469,7 +458,7 @@ public class SqlConfigProvider extends ConfigProvider {
addFile(
statement1,
GlobalKeys.HARDWARE_SETTINGS,
JacksonUtils.serializeToString(config.getHardwareSettings()));
Jsonb.instance().type(HardwareSettings.class).toJson(config.getHardwareSettings()));
statement1.executeUpdate();
}
@@ -478,7 +467,7 @@ public class SqlConfigProvider extends ConfigProvider {
addFile(
statement2,
GlobalKeys.NETWORK_CONFIG,
JacksonUtils.serializeToString(config.getNetworkConfig()));
Jsonb.instance().type(NetworkConfig.class).toJson(config.getNetworkConfig()));
statement2.executeUpdate();
statement2.close();
}
@@ -488,7 +477,7 @@ public class SqlConfigProvider extends ConfigProvider {
addFile(
statement3,
GlobalKeys.HARDWARE_CONFIG,
JacksonUtils.serializeToString(config.getHardwareConfig()));
Jsonb.instance().type(HardwareConfig.class).toJson(config.getHardwareConfig()));
statement3.executeUpdate();
statement3.close();
}
@@ -498,12 +487,14 @@ public class SqlConfigProvider extends ConfigProvider {
addFile(
statement3,
GlobalKeys.NEURAL_NETWORK_PROPERTIES,
JacksonUtils.serializeToString(config.neuralNetworkPropertyManager()));
Jsonb.instance()
.type(NeuralNetworkModelsSettings.class)
.toJson(config.getNeuralNetworkProperties()));
statement3.executeUpdate();
statement3.close();
}
} catch (SQLException | IOException e) {
} catch (SQLException | IllegalStateException | JsonException e) {
logger.error("Err saving global", e);
try {
conn.rollback();
@@ -594,6 +585,12 @@ public class SqlConfigProvider extends ConfigProvider {
private HashMap<String, CameraConfiguration> loadCameraConfigs(Connection conn) {
HashMap<String, CameraConfiguration> loadedConfigurations = new HashMap<>();
// MIGRATION: 2026
// This is designed to always match for efficiency reasons, so that the whole camera config
// isn't scanned. The second capture group determines if the camera info is in the old or new
// format
final var cameraInfoPattern = Pattern.compile("\"(PV[\\w.]*CameraInfo)\"\\s*(:?)");
// Query every single row of the cameras db
PreparedStatement query = null;
try {
@@ -614,57 +611,82 @@ public class SqlConfigProvider extends ConfigProvider {
while (result.next()) {
String uniqueName = "";
try {
List<String> dummyList = new ArrayList<>();
JsonType<List<String>> strListJsonb = Jsonb.instance().type(Types.listOf(String.class));
uniqueName = result.getString(Columns.CAM_UNIQUE_NAME);
// A horrifying hack to keep backward compat with otherpaths
// We -really- need to delete this -stupid- otherpaths column. I hate it.
var configStr = result.getString(Columns.CAM_CONFIG_JSON);
CameraConfiguration config =
JacksonUtils.deserialize(configStr, CameraConfiguration.class);
// MIGRATION: 2024
var configJson = result.getString(Columns.CAM_CONFIG_JSON);
// MIGRATION: 2026
var cameraInfoMatcher = cameraInfoPattern.matcher(configJson);
if (cameraInfoMatcher.find() && cameraInfoMatcher.group(2).equals(":")) {
logger.info("Legacy type-wrapper PVCameraInfo being migrated");
configJson = PVCameraInfo.remapConfigJson(configJson, cameraInfoMatcher.group(1));
}
CameraConfiguration config =
Jsonb.instance().type(CameraConfiguration.class).fromJson(configJson);
// MIGRATION: 2024
if (config.matchedCameraInfo == null) {
logger.info("Legacy CameraConfiguration detected - upgrading");
// manually create the matchedCameraInfo ourselves. Need to upgrade:
// baseName, path, otherPaths, cameraType, usbvid/pid -> matchedCameraInfo
config.matchedCameraInfo =
JacksonUtils.deserialize(configStr, LegacyCameraConfigStruct.class)
Jsonb.instance()
.type(LegacyCameraConfigStruct.class)
.fromJson(configJson)
.matchedCameraInfo;
// Except that otherPaths used to be its own column. so hack that in here as well
var otherPaths =
JacksonUtils.deserialize(
result.getString(Columns.CAM_OTHERPATHS_JSON), String[].class);
Jsonb.instance()
.type(String[].class)
.fromJson(result.getString(Columns.CAM_OTHERPATHS_JSON));
if (config.matchedCameraInfo instanceof UsbCameraInfo usbInfo) {
usbInfo.otherPaths = otherPaths;
}
}
var driverMode =
JacksonUtils.deserialize(
result.getString(Columns.CAM_DRIVERMODE_JSON), DriverModePipelineSettings.class);
List<?> pipelineSettings =
JacksonUtils.deserialize(
result.getString(Columns.CAM_PIPELINE_JSONS), dummyList.getClass());
// MIGRATION: 2026
List<String> legacyPipelineSettings =
strListJsonb.fromJson(result.getString(Columns.CAM_PIPELINE_JSONS));
List<CVPipelineSettings> loadedSettings = new ArrayList<>();
for (var setting : pipelineSettings) {
if (setting instanceof String str) {
try {
loadedSettings.add(JacksonUtils.deserialize(str, CVPipelineSettings.class));
} catch (IOException e) {
logger.error(
"Could not deserialize pipeline setting for camera " + config.nickname, e);
}
for (var pipelineJson : legacyPipelineSettings) {
logger.info("Importing pipeline JSON into camera settings");
if (pipelineJson.startsWith("[")) {
logger.info("Legacy type-wrapper CVPipelineSettings being migrated");
pipelineJson = CVPipelineSettings.remapSettingsJson(pipelineJson);
}
try {
config.pipelineSettings.add(
Jsonb.instance().type(CVPipelineSettings.class).fromJson(pipelineJson));
} catch (IllegalStateException | JsonException e) {
logger.error(
"Could not deserialize pipeline setting for camera " + config.nickname, e);
}
}
config.pipelineSettings = loadedSettings;
config.driveModeSettings = driverMode;
// MIGRATION: 2026
if (config.driveModeSettings == null) {
logger.info("Importing driver mode JSON into camera settings");
var driverModeJson = result.getString(Columns.CAM_DRIVERMODE_JSON);
if (driverModeJson.startsWith("[")) {
logger.info("Legacy type-wrapper CVPipelineSettings being migrated");
driverModeJson = CVPipelineSettings.remapSettingsJson(driverModeJson);
}
config.driveModeSettings =
Jsonb.instance().type(DriverModePipelineSettings.class).fromJson(driverModeJson);
}
loadedConfigurations.put(uniqueName, config);
} catch (IOException e) {
} catch (IllegalStateException | JsonException e) {
logger.error(
"Could not deserialize camera configuration " + uniqueName + " from database!", e);
}

View File

@@ -46,7 +46,7 @@ public abstract class DataChangeSubscriber {
this(DataChangeSource.AllSources, wantedDestinations);
}
public abstract void onDataChangeEvent(DataChangeEvent<?> event);
public abstract <T> void onDataChangeEvent(DataChangeEvent<T> event);
@Override
public int hashCode() {

View File

@@ -17,10 +17,12 @@
package org.photonvision.common.dataflow.networktables;
import java.io.IOException;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
@@ -34,7 +36,6 @@ import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.networking.NetworkUtils;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.common.util.file.JacksonUtils;
import org.wpilib.driverstation.Alert;
import org.wpilib.driverstation.Alert.Level;
import org.wpilib.networktables.LogMessage;
@@ -199,7 +200,7 @@ public class NetworkTablesManager {
var atfl_json = event.valueData.value.getString();
try {
System.out.println("Got new field layout!");
var atfl = JacksonUtils.deserialize(atfl_json, AprilTagFieldLayout.class);
var atfl = Jsonb.instance().type(AprilTagFieldLayout.class).fromJson(atfl_json);
ConfigManager.getInstance().getConfig().setApriltagFieldLayout(atfl);
ConfigManager.getInstance().requestSave();
DataChangeService.getInstance()
@@ -207,7 +208,7 @@ public class NetworkTablesManager {
new OutgoingUIEvent<>(
"fullsettings",
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
} catch (IOException e) {
} catch (IllegalStateException | JsonException e) {
logger.error("Error deserializing atfl!");
logger.error(atfl_json);
}
@@ -270,7 +271,7 @@ public class NetworkTablesManager {
return;
}
HashMap<String, CameraConfiguration> cameraConfigs =
Map<String, CameraConfiguration> cameraConfigs =
ConfigManager.getInstance().getConfig().getCameraConfigurations();
String[] cameraNames =
cameraConfigs.entrySet().stream()

View File

@@ -17,13 +17,15 @@
package org.photonvision.common.dataflow.websocket;
import java.util.HashMap;
import io.avaje.jsonb.Json;
import java.util.List;
import java.util.Map;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.camera.QuirkyCamera;
@Json
public class UICameraConfiguration {
// Path to the camera device. On Linux, this is a special file in /dev/v4l/by-id
// or /dev/videoN.
@@ -37,10 +39,10 @@ public class UICameraConfiguration {
public String uniqueName;
public double fov;
public HashMap<String, Object> currentPipelineSettings;
public Map<String, Object> currentPipelineSettings;
public int currentPipelineIndex;
public List<String> pipelineNicknames;
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
public List<Map<String, Object>> videoFormatList;
public int outputStreamPort;
public int inputStreamPort;
public List<UICameraCalibrationCoefficients> calibrations;

View File

@@ -17,9 +17,11 @@
package org.photonvision.common.dataflow.websocket;
import io.avaje.jsonb.Json;
import java.util.List;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings;
@Json
public class UIGeneralSettings {
public UIGeneralSettings(
String version,

View File

@@ -17,6 +17,9 @@
package org.photonvision.common.dataflow.websocket;
import io.avaje.jsonb.Json;
@Json
public class UILightingConfig {
public UILightingConfig(int brightness, boolean supported) {
this.brightness = brightness;

View File

@@ -17,11 +17,18 @@
package org.photonvision.common.dataflow.websocket;
import io.avaje.jsonb.Json;
import java.util.List;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.networking.NetworkUtils.NMDeviceInfo;
@Json
public class UINetConfig extends NetworkConfig {
// Constructor for JSON to allow all properties to be set directly
public UINetConfig() {
this.canManage = this.deviceCanManageNetwork();
}
public UINetConfig(
NetworkConfig config, List<NMDeviceInfo> networkInterfaceNames, boolean networkingDisabled) {
super(config);
@@ -32,5 +39,7 @@ public class UINetConfig extends NetworkConfig {
public List<NMDeviceInfo> networkInterfaceNames;
public boolean networkingDisabled;
@Json.Ignore(serialize = true)
public boolean canManage;
}

View File

@@ -17,6 +17,7 @@
package org.photonvision.common.dataflow.websocket;
import io.avaje.jsonb.Json;
import java.util.List;
import org.photonvision.PhotonVersion;
import org.photonvision.common.LoadJNI;
@@ -31,6 +32,7 @@ import org.photonvision.common.networking.NetworkUtils;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionSourceManager;
@Json
public class UIPhotonConfiguration {
public List<UICameraConfiguration> cameraSettings;
public UIProgramSettings settings;
@@ -59,7 +61,7 @@ public class UIPhotonConfiguration {
// TODO add support for other types of GPU accel
LoadJNI.hasLoaded(JNITypes.LIBCAMERA) ? "Zerocopy Libcamera Working" : "",
LoadJNI.hasLoaded(JNITypes.MRCAL),
c.neuralNetworkPropertyManager().getModels(),
c.getNeuralNetworkProperties().getModels(),
NeuralNetworkModelManager.getInstance().getSupportedBackends(),
c.getHardwareConfig().deviceName.isEmpty()
? Platform.getHardwareModel()

View File

@@ -17,8 +17,10 @@
package org.photonvision.common.dataflow.websocket;
import io.avaje.jsonb.Json;
import org.wpilib.vision.apriltag.AprilTagFieldLayout;
@Json
public class UIProgramSettings {
public UIProgramSettings(
UINetConfig networkSettings,

View File

@@ -17,10 +17,12 @@
package org.photonvision.common.hardware;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.Jsonb;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Optional;
import org.photonvision.common.logging.LogGroup;
@@ -35,26 +37,22 @@ import org.photonvision.common.logging.Logger;
public class OsImageData {
private static final Logger logger = new Logger(OsImageData.class, LogGroup.General);
private static Path imageMetadataFile = Path.of("/opt/photonvision/image-version.json");
private static File imageMetadataFile = Path.of("/opt/photonvision/image-version.json").toFile();
public static final Optional<ImageMetadata> IMAGE_METADATA = getImageMetadata();
@Json(naming = Json.Naming.LowerUnderscore)
public static record ImageMetadata(
String buildDate, String commitSha, String commitTag, String imageName, String imageSource) {}
private static Optional<ImageMetadata> getImageMetadata() {
if (!imageMetadataFile.toFile().exists()) {
if (!imageMetadataFile.exists()) {
logger.warn("Photon cannot locate OS image metadata at " + imageMetadataFile.toString());
return Optional.empty();
}
try {
String content = Files.readString(imageMetadataFile).strip();
ObjectMapper mapper =
new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
ImageMetadata md = mapper.readValue(content, ImageMetadata.class);
try (InputStream stream = new FileInputStream(imageMetadataFile)) {
ImageMetadata md = Jsonb.instance().type(ImageMetadata.class).fromJson(stream);
if (md.buildDate() == null
&& md.commitSha() == null

View File

@@ -17,8 +17,10 @@
package org.photonvision.common.hardware.metrics;
import io.avaje.jsonb.Json;
import org.photonvision.common.hardware.metrics.proto.DeviceMetricsProto;
@Json
public record DeviceMetrics(
double cpuTemp,
double cpuUtil,

View File

@@ -17,13 +17,14 @@
package org.photonvision.common.networking;
import com.fasterxml.jackson.annotation.JsonValue;
import io.avaje.jsonb.Json;
@Json
public enum NetworkMode {
DHCP,
STATIC;
@JsonValue
@Json.Value
public int toValue() {
return ordinal();
}

View File

@@ -17,6 +17,7 @@
package org.photonvision.common.networking;
import io.avaje.jsonb.Json;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
@@ -62,9 +63,10 @@ public class NetworkUtils {
* @param devName The underlying device name, used by dhclient
* @param nmType The NetworkManager device type
*/
@Json
public static record NMDeviceInfo(String connName, String devName, NMType nmType) {
public NMDeviceInfo(String c, String d, String type) {
this(c, d, NMType.typeForString(type));
public NMDeviceInfo(String connName, String devName, String nmType) {
this(connName, devName, NMType.typeForString(nmType));
}
}

View File

@@ -17,9 +17,11 @@
package org.photonvision.common.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import java.awt.HeadlessException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import org.opencv.core.Mat;
@@ -342,12 +344,10 @@ public class TestUtils {
public static final String LIMELIGHT_480P_CAL_FILE = "limelight_1280_720.json";
public static CameraCalibrationCoefficients getCoeffs(String filename, boolean testMode) {
try {
return new ObjectMapper()
.readValue(
(Path.of(getCalibrationPath(testMode).toString(), filename).toFile()),
CameraCalibrationCoefficients.class);
} catch (IOException e) {
try (var stream =
new FileInputStream(Path.of(getCalibrationPath(testMode).toString(), filename).toFile())) {
return Jsonb.instance().type(CameraCalibrationCoefficients.class).fromJson(stream);
} catch (IOException | IllegalStateException | JsonException e) {
e.printStackTrace();
return null;
}

View File

@@ -0,0 +1,83 @@
/*
* 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.util.file;
import io.avaje.json.JsonAdapter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.avaje.json.PropertyNames;
import io.avaje.jsonb.CustomAdapter;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
import java.util.List;
import org.wpilib.vision.apriltag.AprilTag;
import org.wpilib.vision.apriltag.AprilTagFieldLayout;
@Json.Import(AprilTag.class)
@CustomAdapter
public class AprilTagFieldLayoutJsonAdapter implements JsonAdapter<AprilTagFieldLayout> {
@Json
record FieldDimensions(double length, double width) {}
JsonAdapter<List<AprilTag>> aprilTagListJsonAdapter;
JsonAdapter<FieldDimensions> fieldDimensionsJsonAdapter;
PropertyNames names;
public AprilTagFieldLayoutJsonAdapter(Jsonb jsonb) {
aprilTagListJsonAdapter = jsonb.adapter(Types.listOf(AprilTag.class));
fieldDimensionsJsonAdapter = jsonb.adapter(FieldDimensions.class);
names = jsonb.properties("tags", "field");
}
@Override
public void toJson(JsonWriter writer, AprilTagFieldLayout value) {
writer.beginObject(names);
writer.name(0);
aprilTagListJsonAdapter.toJson(writer, value.getTags());
writer.name(1);
fieldDimensionsJsonAdapter.toJson(
writer, new FieldDimensions(value.getFieldLength(), value.getFieldWidth()));
writer.endObject();
}
@Override
public AprilTagFieldLayout fromJson(JsonReader reader) {
List<AprilTag> tags = null;
FieldDimensions field = null;
reader.beginObject();
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
case "tags":
tags = aprilTagListJsonAdapter.fromJson(reader);
break;
case "field":
field = fieldDimensionsJsonAdapter.fromJson(reader);
break;
default:
reader.unmappedField(fieldName);
reader.skipValue();
}
}
reader.endObject();
return new AprilTagFieldLayout(tags, field.length, field.width);
}
}

View File

@@ -1,152 +0,0 @@
/*
* 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.util.file;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.StreamReadFeature;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ext.NioPathDeserializer;
import com.fasterxml.jackson.databind.ext.NioPathSerializer;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.io.EofException;
public class JacksonUtils {
public static class UIMap extends HashMap<String, Object> {}
// Custom Path key deserializer for Maps with Path keys
public static class PathKeySerializer
extends com.fasterxml.jackson.databind.JsonSerializer<Path> {
@Override
public void serialize(Path value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (value == null) {
gen.writeNull();
} else {
gen.writeFieldName(value.toUri().toString());
}
}
}
// Custom Path key deserializer for Maps with Path keys
public static class PathKeyDeserializer extends com.fasterxml.jackson.databind.KeyDeserializer {
@Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
if (key == null || key.isEmpty()) {
return null;
}
return Paths.get(URI.create(key));
}
}
// Helper method to create ObjectMapper with Path serialization support
private static ObjectMapper createObjectMapperWithPathSupport(Class<?> baseType) {
PolymorphicTypeValidator ptv =
BasicPolymorphicTypeValidator.builder().allowIfBaseType(baseType).build();
SimpleModule pathModule = new SimpleModule();
pathModule.addSerializer(Path.class, new NioPathSerializer());
pathModule.addKeySerializer(Path.class, new PathKeySerializer());
pathModule.addDeserializer(Path.class, new NioPathDeserializer());
pathModule.addKeyDeserializer(Path.class, new PathKeyDeserializer());
return JsonMapper.builder()
.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
.addModule(pathModule)
.build();
}
public static <T> void serialize(Path path, T object) throws IOException {
serialize(path, object, true);
}
public static <T> String serializeToString(T object) throws IOException {
ObjectMapper objectMapper = createObjectMapperWithPathSupport(object.getClass());
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
}
public static <T> void serialize(Path path, T object, boolean forceSync) throws IOException {
ObjectMapper objectMapper = createObjectMapperWithPathSupport(object.getClass());
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
saveJsonString(json, path, forceSync);
}
public static <T> T deserialize(Map<?, ?> s, Class<T> ref) throws IOException {
ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref);
return objectMapper.convertValue(s, ref);
}
public static <T> T deserialize(String s, Class<T> ref) throws IOException {
if (s.length() == 0) {
throw new EofException("Provided empty string for class " + ref.getName());
}
ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref);
objectMapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
return objectMapper.readValue(s, ref);
}
public static <T> T deserialize(Path path, Class<T> ref) throws IOException {
ObjectMapper objectMapper = createObjectMapperWithPathSupport(ref);
File jsonFile = new File(path.toString());
if (jsonFile.exists() && jsonFile.length() > 0) {
return objectMapper.readValue(jsonFile, ref);
}
return null;
}
private static void saveJsonString(String json, Path path, boolean forceSync) throws IOException {
var file = path.toFile();
if (file.getParentFile() != null && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
if (!file.exists()) {
if (!file.canWrite()) {
file.setWritable(true);
}
file.createNewFile();
}
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(json.getBytes());
fileOutputStream.flush();
if (forceSync) {
FileDescriptor fileDescriptor = fileOutputStream.getFD();
fileDescriptor.sync();
}
fileOutputStream.close();
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.util.file;
import io.avaje.json.JsonAdapter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.avaje.jsonb.CustomAdapter;
import io.avaje.jsonb.Jsonb;
import java.net.URI;
import java.nio.file.Path;
@CustomAdapter
public class PathAdapter implements JsonAdapter<Path> {
public PathAdapter(Jsonb jsonb) {}
@Override
public Path fromJson(JsonReader reader) {
return Path.of(URI.create(reader.readString()));
}
@Override
public void toJson(JsonWriter writer, Path value) {
writer.value(value.toUri().toString());
}
}

View File

@@ -0,0 +1,103 @@
/*
* 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.util.file;
import io.avaje.json.JsonAdapter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.avaje.json.PropertyNames;
import io.avaje.json.view.ViewBuilder;
import io.avaje.json.view.ViewBuilderAware;
import io.avaje.jsonb.CustomAdapter;
import io.avaje.jsonb.Jsonb;
import java.lang.invoke.MethodHandle;
import org.wpilib.math.geometry.Pose3d;
import org.wpilib.math.geometry.Rotation3d;
import org.wpilib.math.geometry.Translation3d;
@CustomAdapter
public class Pose3dJsonAdapter implements JsonAdapter<Pose3d>, ViewBuilderAware {
private final JsonAdapter<Translation3d> translation3dJsonAdapter;
private final JsonAdapter<Rotation3d> rotation3dJsonAdapter;
private final PropertyNames names;
public Pose3dJsonAdapter(Jsonb jsonb) {
this.translation3dJsonAdapter = jsonb.adapter(Translation3d.class);
this.rotation3dJsonAdapter = jsonb.adapter(Rotation3d.class);
this.names = jsonb.properties("translation", "rotation");
}
@Override
public void toJson(JsonWriter writer, Pose3d value) {
writer.beginObject(names);
writer.name(0);
translation3dJsonAdapter.toJson(writer, value.getTranslation());
writer.name(1);
rotation3dJsonAdapter.toJson(writer, value.getRotation());
writer.endObject();
}
@Override
public Pose3d fromJson(JsonReader reader) {
Translation3d translation = null;
Rotation3d rotation = null;
reader.beginObject(names);
while (reader.hasNextField()) {
final String fieldName = reader.nextField();
switch (fieldName) {
case "translation":
translation = translation3dJsonAdapter.fromJson(reader);
break;
case "rotation":
rotation = rotation3dJsonAdapter.fromJson(reader);
break;
default:
reader.unmappedField(fieldName);
reader.skipValue();
}
}
reader.endObject();
return new Pose3d(translation, rotation);
}
@Override
public boolean isViewBuilderAware() {
return true;
}
@Override
public ViewBuilderAware viewBuild() {
return this;
}
@Override
public void build(ViewBuilder builder, String name, MethodHandle handle) {
builder.beginObject(name, handle);
builder.add(
"translation",
translation3dJsonAdapter,
builder.method(Pose3d.class, "getTranslation", Translation3d.class));
builder.add(
"rotation",
rotation3dJsonAdapter,
builder.method(Pose3d.class, "getRotation", Rotation3d.class));
builder.endObject();
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.util.file;
import io.avaje.jsonb.Json;
import org.wpilib.math.geometry.Quaternion;
@Json.MixIn(Quaternion.class)
public class QuaternionMixIn {
@Json.Ignore(deserialize = true)
@Json.Property("W")
double m_w;
@Json.Ignore(deserialize = true)
@Json.Property("X")
double m_x;
@Json.Ignore(deserialize = true)
@Json.Property("Y")
double m_y;
@Json.Ignore(deserialize = true)
@Json.Property("Z")
double m_z;
@Json.Creator
public static Quaternion construct(double m_w, double m_x, double m_y, double m_z) {
return new Quaternion(m_w, m_x, m_y, m_z);
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.util.file;
import io.avaje.jsonb.Json;
import org.wpilib.math.geometry.Quaternion;
import org.wpilib.math.geometry.Rotation3d;
@Json.MixIn(Rotation3d.class)
public class Rotation3dMixIn {
@Json.Ignore(deserialize = true)
@Json.Property("quaternion")
Quaternion m_q;
@Json.Creator
public static Rotation3d construct(Quaternion m_q) {
return new Rotation3d(m_q);
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.util.file;
import io.avaje.jsonb.Json;
import org.wpilib.math.geometry.Translation3d;
@Json.MixIn(Translation3d.class)
public class Translation3dMixIn {
@Json.Ignore(deserialize = true)
@Json.Property("x")
double m_x;
@Json.Ignore(deserialize = true)
@Json.Property("y")
double m_y;
@Json.Ignore(deserialize = true)
@Json.Property("z")
double m_z;
@Json.Creator
public static Translation3d construct(double m_x, double m_y, double m_z) {
return new Translation3d(m_x, m_y, m_z);
}
}

View File

@@ -17,8 +17,10 @@
package org.photonvision.common.util.numbers;
import io.avaje.jsonb.Json;
import org.opencv.core.Point;
@Json
public class DoubleCouple extends NumberCouple<Double> {
public DoubleCouple() {
super(0.0, 0.0);

View File

@@ -17,6 +17,9 @@
package org.photonvision.common.util.numbers;
import io.avaje.jsonb.Json;
@Json
public class IntegerCouple extends NumberCouple<Integer> {
public IntegerCouple() {
super(0, 0);

View File

@@ -17,8 +17,6 @@
package org.photonvision.common.util.numbers;
import com.fasterxml.jackson.annotation.JsonIgnore;
public abstract class NumberCouple<T extends Number> {
protected T first;
protected T second;
@@ -56,7 +54,6 @@ public abstract class NumberCouple<T extends Number> {
&& couple.second.equals(second);
}
@JsonIgnore
public boolean isEmpty() {
return first.intValue() == 0 && second.intValue() == 0;
}

View File

@@ -17,10 +17,7 @@
package org.photonvision.vision.calibration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import java.awt.Color;
import java.nio.file.Path;
import java.util.Arrays;
@@ -36,45 +33,36 @@ import org.opencv.imgproc.Imgproc;
import org.photonvision.common.util.ColorHelper;
import org.wpilib.math.geometry.Pose3d;
@Json
// Ignore the previous calibration data that was stored in the json file.
@JsonIgnoreProperties(ignoreUnknown = true)
public final class BoardObservation implements Cloneable {
// Expected feature 3d location in the camera frame
@JsonProperty("locationInObjectSpace")
public List<Point3> locationInObjectSpace;
// Observed location in pixel space
@JsonProperty("locationInImageSpace")
public List<Point> locationInImageSpace;
// (measured location in pixels) - (expected from FK)
@JsonProperty("reprojectionErrors")
public List<Point> reprojectionErrors;
// Solver optimized board poses
@JsonProperty("optimisedCameraToObject")
public Pose3d optimisedCameraToObject;
// If we should use this observation when re-calculating camera calibration
@JsonProperty("cornersUsed")
public boolean[] cornersUsed;
@JsonProperty("snapshotName")
public String snapshotName;
@JsonProperty("snapshotDataLocation")
@Nullable
public Path snapshotDataLocation;
@Nullable public Path snapshotDataLocation;
@JsonCreator
public BoardObservation(
@JsonProperty("locationInObjectSpace") List<Point3> locationInObjectSpace,
@JsonProperty("locationInImageSpace") List<Point> locationInImageSpace,
@JsonProperty("reprojectionErrors") List<Point> reprojectionErrors,
@JsonProperty("optimisedCameraToObject") Pose3d optimisedCameraToObject,
@JsonProperty("cornersUsed") boolean[] cornersUsed,
@JsonProperty("snapshotName") String snapshotName,
@JsonProperty("snapshotDataLocation") Path snapshotDataLocation) {
List<Point3> locationInObjectSpace,
List<Point> locationInImageSpace,
List<Point> reprojectionErrors,
Pose3d optimisedCameraToObject,
boolean[] cornersUsed,
String snapshotName,
Path snapshotDataLocation) {
this.locationInObjectSpace = locationInObjectSpace;
this.locationInImageSpace = locationInImageSpace;
this.reprojectionErrors = reprojectionErrors;
@@ -119,7 +107,6 @@ public final class BoardObservation implements Cloneable {
}
}
@JsonIgnore
/**
* Load the captured board image from disk. Allocates a new Mat, which the caller is responsible
* for releasing.
@@ -141,7 +128,6 @@ public final class BoardObservation implements Cloneable {
* @return Annotated image, or null if the image could not be loaded. Caller is responsible for
* releasing the Mat.
*/
@JsonIgnore
public Mat annotateImage() {
var image = loadImage();
@@ -179,7 +165,6 @@ public final class BoardObservation implements Cloneable {
*
* @return Mean reprojection error in pixels.
*/
@JsonIgnore
double meanReprojectionError() {
return reprojectionErrors.stream()
.filter(pt -> cornersUsed[reprojectionErrors.indexOf(pt)])

View File

@@ -17,11 +17,7 @@
package org.photonvision.vision.calibration;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import java.util.Arrays;
import java.util.List;
import org.opencv.core.Mat;
@@ -30,31 +26,23 @@ import org.opencv.core.Size;
import org.photonvision.vision.opencv.ImageRotationMode;
import org.photonvision.vision.opencv.Releasable;
@JsonIgnoreProperties(ignoreUnknown = true)
@Json
public class CameraCalibrationCoefficients implements Releasable {
@JsonProperty("resolution")
public final Size unrotatedImageSize;
/** The unrotated resolution of the calibration */
public final Size resolution;
@JsonProperty("cameraIntrinsics")
public final JsonMatOfDouble cameraIntrinsics;
@JsonProperty("distCoeffs")
@JsonAlias({"distCoeffs", "distCoeffs"})
public final JsonMatOfDouble distCoeffs;
@JsonProperty("observations")
public final List<BoardObservation> observations;
@JsonProperty("calobjectWarp")
public final double[] calobjectWarp;
@JsonProperty("calobjectSize")
public final Size calobjectSize;
@JsonProperty("calobjectSpacing")
public final double calobjectSpacing;
@JsonProperty("lensmodel")
public final CameraLensModel lensmodel;
/**
@@ -75,17 +63,16 @@ public class CameraCalibrationCoefficients implements Releasable {
* width/height
* @param calobjectSpacing Spacing between adjacent squares, in meters
*/
@JsonCreator
public CameraCalibrationCoefficients(
@JsonProperty("resolution") Size resolution,
@JsonProperty("cameraIntrinsics") JsonMatOfDouble cameraIntrinsics,
@JsonProperty("distCoeffs") JsonMatOfDouble distCoeffs,
@JsonProperty("calobjectWarp") double[] calobjectWarp,
@JsonProperty("observations") List<BoardObservation> observations,
@JsonProperty("calobjectSize") Size calobjectSize,
@JsonProperty("calobjectSpacing") double calobjectSpacing,
@JsonProperty("lensmodel") CameraLensModel lensmodel) {
this.unrotatedImageSize = resolution;
Size resolution,
JsonMatOfDouble cameraIntrinsics,
JsonMatOfDouble distCoeffs,
double[] calobjectWarp,
List<BoardObservation> observations,
Size calobjectSize,
double calobjectSpacing,
CameraLensModel lensmodel) {
this.resolution = resolution;
this.cameraIntrinsics = cameraIntrinsics;
this.distCoeffs = distCoeffs;
this.calobjectWarp = calobjectWarp;
@@ -129,7 +116,7 @@ public class CameraCalibrationCoefficients implements Releasable {
rotatedIntrinsics.put(1, 1, fx);
// CX
rotatedIntrinsics.put(0, 2, unrotatedImageSize.height - cy);
rotatedIntrinsics.put(0, 2, resolution.height - cy);
// CY
rotatedIntrinsics.put(1, 2, cx);
@@ -139,14 +126,14 @@ public class CameraCalibrationCoefficients implements Releasable {
rotatedDistCoeffs.put(0, 3, -p1);
// The rotated image size is the same as the unrotated image size, but the width and height
// are flipped
rotatedImageSize = new Size(unrotatedImageSize.height, unrotatedImageSize.width);
// are swapped
rotatedImageSize = new Size(resolution.height, resolution.width);
break;
case DEG_180_CCW:
// CX
rotatedIntrinsics.put(0, 2, unrotatedImageSize.width - cx);
rotatedIntrinsics.put(0, 2, resolution.width - cx);
// CY
rotatedIntrinsics.put(1, 2, unrotatedImageSize.height - cy);
rotatedIntrinsics.put(1, 2, resolution.height - cy);
// P1
rotatedDistCoeffs.put(0, 2, -p1);
@@ -154,7 +141,7 @@ public class CameraCalibrationCoefficients implements Releasable {
rotatedDistCoeffs.put(0, 3, -p2);
// The rotated image size is the same as the unrotated image size
rotatedImageSize = unrotatedImageSize;
rotatedImageSize = resolution;
break;
case DEG_90_CCW:
// FX
@@ -165,7 +152,7 @@ public class CameraCalibrationCoefficients implements Releasable {
// CX
rotatedIntrinsics.put(0, 2, cy);
// CY
rotatedIntrinsics.put(1, 2, unrotatedImageSize.width - cx);
rotatedIntrinsics.put(1, 2, resolution.width - cx);
// P1
rotatedDistCoeffs.put(0, 2, -p2);
@@ -173,8 +160,8 @@ public class CameraCalibrationCoefficients implements Releasable {
rotatedDistCoeffs.put(0, 3, p1);
// The rotated image size is the same as the unrotated image size, but the width and height
// are flipped
rotatedImageSize = new Size(unrotatedImageSize.height, unrotatedImageSize.width);
// are swapped
rotatedImageSize = new Size(resolution.height, resolution.width);
break;
}
@@ -196,27 +183,22 @@ public class CameraCalibrationCoefficients implements Releasable {
lensmodel);
}
@JsonIgnore
public Mat getCameraIntrinsicsMat() {
return cameraIntrinsics.getAsMatOfDouble();
}
@JsonIgnore
public MatOfDouble getDistCoeffsMat() {
return distCoeffs.getAsMatOfDouble();
}
@JsonIgnore
public double[] getIntrinsicsArr() {
return cameraIntrinsics.data;
}
@JsonIgnore
public double[] getDistCoeffsArr() {
return distCoeffs.data;
}
@JsonIgnore
public List<BoardObservation> getObservations() {
return observations;
}
@@ -230,7 +212,7 @@ public class CameraCalibrationCoefficients implements Releasable {
@Override
public String toString() {
return "CameraCalibrationCoefficients [resolution="
+ unrotatedImageSize
+ resolution
+ ", cameraIntrinsics="
+ cameraIntrinsics
+ ", distCoeffs="
@@ -244,7 +226,7 @@ public class CameraCalibrationCoefficients implements Releasable {
public UICameraCalibrationCoefficients cloneWithoutObservations() {
return new UICameraCalibrationCoefficients(
unrotatedImageSize,
resolution,
cameraIntrinsics,
distCoeffs,
calobjectWarp,

View File

@@ -17,8 +17,7 @@
package org.photonvision.vision.calibration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import java.util.Arrays;
import org.ejml.simple.SimpleMatrix;
import org.opencv.core.CvType;
@@ -28,6 +27,7 @@ import org.photonvision.vision.opencv.Releasable;
import org.wpilib.math.linalg.Matrix;
import org.wpilib.math.util.Num;
@Json
/** JSON-serializable image. Data is stored as a raw JSON array. */
public class JsonMatOfDouble implements Releasable {
public final int rows;
@@ -36,28 +36,23 @@ public class JsonMatOfDouble implements Releasable {
public final double[] data;
// Cached matrices to avoid object recreation
@JsonIgnore private Mat wrappedMat = null;
@JsonIgnore private Matrix wpilibMat = null;
@Json.Ignore private Mat wrappedMat = null;
@Json.Ignore private Matrix wpilibMat = null;
@JsonIgnore private MatOfDouble wrappedMatOfDouble;
private boolean released = false;
@Json.Ignore private MatOfDouble wrappedMatOfDouble;
@Json.Ignore private boolean released = false;
public JsonMatOfDouble(int rows, int cols, double[] data) {
this(rows, cols, CvType.CV_64FC1, data);
}
public JsonMatOfDouble(
@JsonProperty("rows") int rows,
@JsonProperty("cols") int cols,
@JsonProperty("type") int type,
@JsonProperty("data") double[] data) {
public JsonMatOfDouble(int rows, int cols, int type, double[] data) {
this.rows = rows;
this.cols = cols;
this.type = type;
this.data = data;
}
@JsonIgnore
private static double[] getDataFromMat(Mat mat) {
double[] data = new double[(int) mat.total()];
mat.get(0, 0, data);
@@ -75,7 +70,6 @@ public class JsonMatOfDouble implements Releasable {
return new JsonMatOfDouble(mat.rows(), mat.cols(), getDataFromMat(mat));
}
@JsonIgnore
private Mat getAsMat() {
if (this.type != CvType.CV_64FC1) return null;
@@ -91,7 +85,6 @@ public class JsonMatOfDouble implements Releasable {
return this.wrappedMat;
}
@JsonIgnore
public MatOfDouble getAsMatOfDouble() {
if (this.released) {
throw new RuntimeException("This calibration object was already released");
@@ -105,7 +98,6 @@ public class JsonMatOfDouble implements Releasable {
}
@SuppressWarnings("unchecked")
@JsonIgnore
public <R extends Num, C extends Num> Matrix<R, C> getAsWpilibMat() {
if (wpilibMat == null) {
wpilibMat = new Matrix<R, C>(new SimpleMatrix(rows, cols, true, data));

View File

@@ -17,10 +17,12 @@
package org.photonvision.vision.calibration;
import io.avaje.jsonb.Json;
import java.util.List;
import java.util.stream.IntStream;
import org.opencv.core.Size;
@Json
public class UICameraCalibrationCoefficients extends CameraCalibrationCoefficients {
public int numSnapshots;

View File

@@ -18,7 +18,8 @@
package org.photonvision.vision.camera;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.vision.frame.FrameProvider;
import org.photonvision.vision.frame.FrameStaticProperties;
@@ -100,7 +101,7 @@ public class FileVisionSource extends VisionSource {
public static class FileSourceSettables extends VisionSourceSettables {
private final VideoMode videoMode;
private final HashMap<Integer, VideoMode> videoModes = new HashMap<>();
private final ArrayList<VideoMode> videoModes = new ArrayList<>();
FileSourceSettables(
CameraConfiguration cameraConfiguration, FrameStaticProperties frameStaticProperties) {
@@ -112,7 +113,7 @@ public class FileVisionSource extends VisionSource {
frameStaticProperties.imageWidth,
frameStaticProperties.imageHeight,
30);
videoModes.put(0, videoMode);
videoModes.add(videoMode);
}
@Override
@@ -137,7 +138,7 @@ public class FileVisionSource extends VisionSource {
}
@Override
public HashMap<Integer, VideoMode> getAllVideoModes() {
public List<VideoMode> getAllVideoModes() {
return videoModes;
}

View File

@@ -17,33 +17,31 @@
package org.photonvision.vision.camera;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.wpilib.vision.camera.UsbCameraInfo;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonSubTypes({
@JsonSubTypes.Type(value = PVCameraInfo.PVUsbCameraInfo.class),
@JsonSubTypes.Type(value = PVCameraInfo.PVCSICameraInfo.class),
@JsonSubTypes.Type(value = PVCameraInfo.PVFileCameraInfo.class)
})
@Json(typeProperty = "type")
@Json.SubType(type = PVCameraInfo.PVUsbCameraInfo.class)
@Json.SubType(type = PVCameraInfo.PVCSICameraInfo.class)
@Json.SubType(type = PVCameraInfo.PVFileCameraInfo.class)
public sealed interface PVCameraInfo {
/**
* @return The path of the camera.
*/
@Json.Property("path")
String path();
/**
* @return The base name of the camera aka the name as just ascii.
*/
@Json.Property("name")
String name();
/**
@@ -64,7 +62,7 @@ public sealed interface PVCameraInfo {
*
* @return The unique path of the camera
*/
@JsonGetter(value = "uniquePath")
@Json.Property("uniquePath")
String uniquePath();
String[] otherPaths();
@@ -82,16 +80,9 @@ public sealed interface PVCameraInfo {
return this.equals((Object) other);
}
@JsonTypeName("PVUsbCameraInfo")
public static final class PVUsbCameraInfo extends UsbCameraInfo implements PVCameraInfo {
@JsonCreator
public PVUsbCameraInfo(
@JsonProperty("dev") int dev,
@JsonProperty("path") String path,
@JsonProperty("name") String name,
@JsonProperty("otherPaths") String[] otherPaths,
@JsonProperty("vendorId") int vendorId,
@JsonProperty("productId") int productId) {
int dev, String path, String name, String[] otherPaths, int vendorId, int productId) {
super(dev, path, name, otherPaths, vendorId, productId);
}
@@ -168,14 +159,11 @@ public sealed interface PVCameraInfo {
}
}
@JsonTypeName("PVCSICameraInfo")
public static final class PVCSICameraInfo implements PVCameraInfo {
public final String path;
public final String baseName;
@JsonCreator
public PVCSICameraInfo(
@JsonProperty("path") String path, @JsonProperty("baseName") String baseName) {
public PVCSICameraInfo(String path, String baseName) {
this.path = path;
this.baseName = baseName;
}
@@ -233,13 +221,11 @@ public sealed interface PVCameraInfo {
}
}
@JsonTypeName("PVFileCameraInfo")
public static final class PVFileCameraInfo implements PVCameraInfo {
public final String path;
public final String name;
@JsonCreator
public PVFileCameraInfo(@JsonProperty("path") String path, @JsonProperty("name") String name) {
public PVFileCameraInfo(String path, String name) {
this.path = path;
this.name = name;
}
@@ -300,4 +286,25 @@ public sealed interface PVCameraInfo {
public static PVCameraInfo fromFileInfo(String path, String baseName) {
return new PVFileCameraInfo(path, baseName);
}
// MIGRATION: 2026
public static String remapConfigJson(String configJson, String cameraType) {
final JsonType<Map<String, Object>> objMapJsonb =
Jsonb.instance().type(Types.mapOf(Object.class));
Map<String, Object> cameraMigration = objMapJsonb.fromJson(configJson);
@SuppressWarnings("unchecked")
var cameraMigrationIn = (Map<String, Object>) cameraMigration.get("matchedCameraInfo");
@SuppressWarnings("unchecked")
var cameraData = (Map<String, Object>) cameraMigrationIn.get(cameraType);
Map<String, Object> cameraMigrationOut = new HashMap<>();
cameraMigrationOut.putAll(cameraData);
cameraMigrationOut.put("type", "PVCameraInfo." + cameraType);
cameraMigration.put("matchedCameraInfo", cameraMigrationOut);
return objMapJsonb.toJson(cameraMigration);
}
}

View File

@@ -17,13 +17,13 @@
package org.photonvision.vision.camera;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Json
public class QuirkyCamera {
private static final List<QuirkyCamera> quirkyCameras =
List.of(
@@ -98,19 +98,14 @@ public class QuirkyCamera {
CameraQuirk.Gain,
CameraQuirk.AwbRedBlueGain); // PiCam (using libcamera GPU Driver on raspberry pi)
@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 Map<CameraQuirk, Boolean> quirks;
/**
@@ -165,13 +160,12 @@ public class QuirkyCamera {
}
}
@JsonCreator
public QuirkyCamera(
@JsonProperty("baseName") String baseName,
@JsonProperty("usbVid") int usbVid,
@JsonProperty("usbPid") int usbPid,
@JsonProperty("displayName") String displayName,
@JsonProperty("quirks") Map<CameraQuirk, Boolean> quirks) {
String baseName,
int usbVid,
int usbPid,
String displayName,
Map<CameraQuirk, Boolean> quirks) {
this.baseName = baseName;
this.usbPid = usbPid;
this.usbVid = usbVid;

View File

@@ -20,7 +20,6 @@ package org.photonvision.vision.camera.USBCameras;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -286,7 +285,6 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
}
private void cacheVideoModes() {
videoModes = new HashMap<>();
List<VideoMode> videoModesList = new ArrayList<>();
try {
for (VideoMode videoMode : camera.enumerateVideoModes()) {
@@ -316,10 +314,7 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
// The ordering is usually more logical when done like this. It typically puts higher FPSes
// closer to the bottom.
Collections.reverse(sortedList);
for (int i = 0; i < sortedList.size(); i++) {
videoModes.put(i, sortedList.get(i));
}
videoModes = sortedList;
// If after all that we still have no video modes, not much we can do besides
// throw up our hands
@@ -329,11 +324,11 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
}
@Override
public HashMap<Integer, VideoMode> getAllVideoModes() {
public List<VideoMode> getAllVideoModes() {
if (!cameraPropertiesCached) {
// Device hasn't connected at least once, best I can do is given up
logger.warn("Device hasn't connected, cannot enumerate video modes");
return new HashMap<>();
return new ArrayList<>();
}
return videoModes;

View File

@@ -17,7 +17,8 @@
package org.photonvision.vision.camera.csi;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.raspi.LibCameraJNI;
@@ -61,7 +62,7 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
public LibcameraGpuSettables(CameraConfiguration configuration) {
super(configuration);
videoModes = new HashMap<>();
videoModes = new ArrayList<>();
LibCameraJNI.SensorModel tempSensorModel;
try {
@@ -74,18 +75,18 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
if (sensorModel == LibCameraJNI.SensorModel.IMX219) {
// Settings for the IMX219 sensor, which is used on the Pi Camera Module v2
videoModes.put(0, new FPSRatedVideoMode(PixelFormat.kUnknown, 320, 240, 120, 120, .39));
videoModes.put(1, new FPSRatedVideoMode(PixelFormat.kUnknown, 320, 240, 30, 30, .39));
videoModes.put(2, new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 480, 65, 90, .39));
videoModes.put(3, new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 480, 30, 30, .39));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 320, 240, 120, 120, .39));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 320, 240, 30, 30, .39));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 480, 65, 90, .39));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 480, 30, 30, .39));
// TODO: fix 1280x720 in the native code and re-add it
videoModes.put(4, new FPSRatedVideoMode(PixelFormat.kUnknown, 1920, 1080, 15, 20, .53));
videoModes.put(5, new FPSRatedVideoMode(PixelFormat.kUnknown, 3280 / 2, 2464 / 2, 15, 20, 1));
videoModes.put(6, new FPSRatedVideoMode(PixelFormat.kUnknown, 3280 / 4, 2464 / 4, 15, 20, 1));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 1920, 1080, 15, 20, .53));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 3280 / 2, 2464 / 2, 15, 20, 1));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 3280 / 4, 2464 / 4, 15, 20, 1));
} else if (sensorModel == LibCameraJNI.SensorModel.OV9281) {
// Taken from https://www.ovt.com/wp-content/uploads/2022/01/OV9281-OV9282-PB-v1.3-WEB.pdf
videoModes.put(0, new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 400, 120, 240, 1));
videoModes.put(1, new FPSRatedVideoMode(PixelFormat.kUnknown, 1280, 800, 120, 120, 1));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 400, 120, 240, 1));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 1280, 800, 120, 120, 1));
} else {
if (sensorModel == LibCameraJNI.SensorModel.IMX477) {
@@ -100,13 +101,13 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
}
// Settings for the OV5647 sensor, which is used by the Pi Camera Module v1
videoModes.put(0, new FPSRatedVideoMode(PixelFormat.kUnknown, 320, 240, 90, 90, 1));
videoModes.put(1, new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 480, 85, 90, 1));
videoModes.put(2, new FPSRatedVideoMode(PixelFormat.kUnknown, 960, 720, 45, 49, 0.74));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 320, 240, 90, 90, 1));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 480, 85, 90, 1));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 960, 720, 45, 49, 0.74));
// Half the size of the active areas on the OV5647
videoModes.put(3, new FPSRatedVideoMode(PixelFormat.kUnknown, 2592 / 2, 1944 / 2, 20, 20, 1));
videoModes.put(4, new FPSRatedVideoMode(PixelFormat.kUnknown, 1280, 720, 30, 45, 0.91));
videoModes.put(5, new FPSRatedVideoMode(PixelFormat.kUnknown, 1920, 1080, 15, 20, 0.72));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 2592 / 2, 1944 / 2, 20, 20, 1));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 1280, 720, 30, 45, 0.91));
videoModes.add(new FPSRatedVideoMode(PixelFormat.kUnknown, 1920, 1080, 15, 20, 0.72));
}
// TODO need to add more video modes for new sensors here
@@ -253,7 +254,7 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
}
@Override
public HashMap<Integer, VideoMode> getAllVideoModes() {
public List<VideoMode> getAllVideoModes() {
return videoModes;
}

View File

@@ -17,7 +17,7 @@
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import io.avaje.jsonb.Json;
import java.util.Objects;
import org.opencv.core.Point;
import org.photonvision.common.util.numbers.DoubleCouple;
@@ -91,19 +91,16 @@ public class AdvancedPipelineSettings extends CVPipelineSettings {
public int cornerDetectionSideCount = 4;
public double cornerDetectionAccuracyPercentage = 10;
// MIGRATION: 2025
/**
* Handles backward compatibility for the deprecated outputShowMultipleTargets property. When
* outputShowMultipleTargets is encountered during deserialization, it sets outputMaximumTargets
* appropriately. If outputShowMultipleTargets is false, outputMaximumTargets is set to 1.
*/
@JsonAnySetter
public void handleUnknownProperty(String name, Object value) {
// Handle the old showMultipleTargets property for backward compatibility
if ("outputShowMultipleTargets".equals(name) && value instanceof Boolean showMultipleTargets) {
if (!showMultipleTargets) {
// If showMultipleTargets is false, limit to 1 target
outputMaximumTargets = 1;
}
@Json.Property("outputShowMultipleTargets")
public void importShowMultipleTargets(boolean showMultipleTargets) {
if (!showMultipleTargets) {
outputMaximumTargets = 1;
}
}

View File

@@ -17,11 +17,9 @@
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.photonvision.vision.apriltag.AprilTagFamily;
import org.photonvision.vision.target.TargetModel;
@JsonTypeName("AprilTagPipelineSettings")
public class AprilTagPipelineSettings extends AdvancedPipelineSettings {
public AprilTagFamily tagFamily = AprilTagFamily.kTag36h11;
public int decimate = 1;

View File

@@ -17,12 +17,10 @@
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.photonvision.common.util.numbers.IntegerCouple;
import org.photonvision.vision.apriltag.AprilTagFamily;
import org.photonvision.vision.target.TargetModel;
@JsonTypeName("ArucoPipelineSettings")
public class ArucoPipelineSettings extends AdvancedPipelineSettings {
public AprilTagFamily tagFamily = AprilTagFamily.kTag36h11;

View File

@@ -17,23 +17,25 @@
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.photonvision.vision.frame.FrameDivisor;
import org.photonvision.vision.opencv.ImageRotationMode;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.WRAPPER_ARRAY,
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = ColoredShapePipelineSettings.class),
@JsonSubTypes.Type(value = ReflectivePipelineSettings.class),
@JsonSubTypes.Type(value = DriverModePipelineSettings.class),
@JsonSubTypes.Type(value = AprilTagPipelineSettings.class),
@JsonSubTypes.Type(value = ArucoPipelineSettings.class),
@JsonSubTypes.Type(value = ObjectDetectionPipelineSettings.class)
@Json(typeProperty = "type")
@Json.SubTypes({
@Json.SubType(type = ColoredShapePipelineSettings.class),
@Json.SubType(type = ReflectivePipelineSettings.class),
@Json.SubType(type = DriverModePipelineSettings.class),
@Json.SubType(type = AprilTagPipelineSettings.class),
@Json.SubType(type = ArucoPipelineSettings.class),
@Json.SubType(type = ObjectDetectionPipelineSettings.class)
})
public class CVPipelineSettings implements Cloneable {
public int pipelineIndex = 0;
@@ -153,4 +155,22 @@ public class CVPipelineSettings implements Cloneable {
+ outputShouldShow
+ '}';
}
// MIGRATION: 2026
public static String remapSettingsJson(String pipelineJson) {
final JsonType<List<Object>> objListJsonb = Jsonb.instance().type(Types.listOf(Object.class));
final JsonType<Map<String, Object>> objMapJsonb =
Jsonb.instance().type(Types.mapOf(Object.class));
List<Object> pipelineMigrationIn = objListJsonb.fromJson(pipelineJson);
@SuppressWarnings("unchecked")
var pipelineData = (Map<String, Object>) pipelineMigrationIn.get(1);
Map<String, Object> pipelineMigrationOut = new HashMap<>();
pipelineMigrationOut.putAll(pipelineData);
pipelineMigrationOut.put("type", pipelineMigrationIn.get(0));
return objMapJsonb.toJson(pipelineMigrationOut);
}
}

View File

@@ -17,14 +17,12 @@
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import java.util.Objects;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.common.util.numbers.IntegerCouple;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.opencv.ContourShape;
@JsonTypeName("ColoredShapePipelineSettings")
public class ColoredShapePipelineSettings extends AdvancedPipelineSettings {
public ContourShape contourShape = ContourShape.Triangle;
public DoubleCouple contourPerimeter = new DoubleCouple(0, Double.MAX_VALUE);

View File

@@ -17,11 +17,11 @@
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.avaje.jsonb.Json;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.vision.processes.PipelineManager;
@JsonTypeName("DriverModePipelineSettings")
@Json
public class DriverModePipelineSettings extends CVPipelineSettings {
public DoubleCouple offsetPoint = new DoubleCouple();
public boolean crosshair = true;

View File

@@ -17,10 +17,8 @@
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.photonvision.vision.processes.PipelineManager;
@JsonTypeName("FocusPipelineSettings")
public class FocusPipelineSettings extends CVPipelineSettings {
public FocusPipelineSettings() {
super();

View File

@@ -17,9 +17,6 @@
package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
@JsonTypeName("ReflectivePipelineSettings")
public class ReflectivePipelineSettings extends AdvancedPipelineSettings {
public double contourFilterRangeX = 2;
public double contourFilterRangeY = 2;

View File

@@ -17,8 +17,10 @@
package org.photonvision.vision.pipeline;
import io.avaje.jsonb.Json;
import org.opencv.objdetect.Objdetect;
@Json
public class UICalibrationData {
public int videoModeIndex;
public int count;
@@ -53,11 +55,18 @@ public class UICalibrationData {
this.tagFamily = tagFamily;
}
@Json
public enum BoardType {
CHESSBOARD,
CHARUCOBOARD,
CHARUCOBOARD;
@Json.Value
int toValue() {
return ordinal();
}
}
@Json
public enum TagFamily {
Dict_4X4_1000(Objdetect.DICT_4X4_1000),
Dict_5X5_1000(Objdetect.DICT_5X5_1000),
@@ -75,6 +84,11 @@ public class UICalibrationData {
private TagFamily(int value) {
this.value = value;
}
@Json.Value
int toValue() {
return ordinal();
}
}
@Override

View File

@@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import org.opencv.core.Size;
import org.photonvision.common.configuration.CameraConfiguration;
@@ -591,23 +592,23 @@ public class VisionModule {
ret.fpsLimit = this.fpsLimit;
// TODO refactor into helper method
var temp = new HashMap<Integer, HashMap<String, Object>>();
var temp = new ArrayList<Map<String, Object>>();
var videoModes = visionSource.getSettables().getAllVideoModes();
for (var k : videoModes.entrySet()) {
for (var videoMode : videoModes) {
var internalMap = new HashMap<String, Object>();
internalMap.put("width", k.getValue().width);
internalMap.put("height", k.getValue().height);
internalMap.put("fps", k.getValue().fps);
internalMap.put("width", videoMode.width);
internalMap.put("height", videoMode.height);
internalMap.put("fps", videoMode.fps);
internalMap.put(
"pixelFormat",
((k.getValue() instanceof LibcameraGpuSource.FPSRatedVideoMode)
((videoMode instanceof LibcameraGpuSource.FPSRatedVideoMode)
? "kPicam"
: k.getValue().pixelFormat.toString())
: videoMode.pixelFormat.toString())
.substring(1)); // Remove the k prefix
temp.put(k.getKey(), internalMap);
temp.add(internalMap);
}
if (videoModes.size() == 0) {
@@ -728,7 +729,7 @@ public class VisionModule {
public void addCalibrationToConfig(CameraCalibrationCoefficients newCalibration) {
if (newCalibration != null) {
logger.info("Got new calibration for " + newCalibration.unrotatedImageSize);
logger.info("Got new calibration for " + newCalibration.resolution);
visionSource.getSettables().addCalibration(newCalibration);
} else {
logger.error("Got null calibration?");
@@ -737,9 +738,9 @@ public class VisionModule {
saveAndBroadcastAll();
}
public void removeCalibrationFromConfig(Size unrotatedImageSize) {
if (unrotatedImageSize != null) {
visionSource.getSettables().removeCalibration(unrotatedImageSize);
public void removeCalibrationFromConfig(Size resolution) {
if (resolution != null) {
visionSource.getSettables().removeCalibration(resolution);
} else {
logger.error("Got null size?");
}

View File

@@ -17,7 +17,7 @@
package org.photonvision.vision.processes;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.jsonb.Jsonb;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -30,7 +30,6 @@ import org.photonvision.common.dataflow.events.DataChangeEvent;
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.common.util.numbers.IntegerCouple;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
@@ -57,10 +56,10 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
}
@Override
public void onDataChangeEvent(DataChangeEvent<?> event) {
public <T> void onDataChangeEvent(DataChangeEvent<T> event) {
// Camera index -1 means a "multicast event" (i.e. the event is received by all
// cameras)
if (event instanceof IncomingWebSocketEvent wsEvent
if (event instanceof IncomingWebSocketEvent<T> wsEvent
&& wsEvent.cameraUniqueName != null
&& wsEvent.cameraUniqueName.equals(parentModule.uniqueName())) {
logger.trace("Got PSC event - propName: " + wsEvent.propertyName);
@@ -68,7 +67,7 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
try {
getSettingChanges()
.add(
new VisionModuleChange(
new VisionModuleChange<T>(
wsEvent.propertyName,
wsEvent.data,
parentModule.pipelineManager.getCurrentPipeline().getSettings(),
@@ -92,6 +91,10 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
var newPropValue = change.getNewPropValue();
var currentSettings = change.getCurrentSettings();
var originContext = change.getOriginContext();
if (newPropValue instanceof Long) {
newPropValue = ((Long) newPropValue).intValue();
}
switch (propName) {
case "pipelineName" -> newPipelineNickname((String) newPropValue);
case "newPipelineInfo" -> newPipelineInfo((Pair<String, PipelineType>) newPropValue);
@@ -199,7 +202,7 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
public void startCalibration(Map<String, Object> data) {
try {
var deserialized = JacksonUtils.deserialize(data, UICalibrationData.class);
var deserialized = Jsonb.instance().type(UICalibrationData.class).fromObject(data);
parentModule.startCalibration(deserialized);
parentModule.saveAndBroadcastAll();
} catch (Exception e) {
@@ -295,8 +298,8 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
}
} else if (propField.getType() == ModelProperties.class
&& newPropValue instanceof LinkedHashMap) {
ObjectMapper mapper = new ObjectMapper();
ModelProperties modelProps = mapper.convertValue(newPropValue, ModelProperties.class);
ModelProperties modelProps =
Jsonb.instance().type(ModelProperties.class).fromObject(newPropValue);
propField.set(currentSettings, modelProps);
} else {
propField.set(currentSettings, newPropValue);

View File

@@ -17,6 +17,7 @@
package org.photonvision.vision.processes;
import io.avaje.jsonb.Json;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -72,7 +73,7 @@ public class VisionSourceManager {
return SingletonHolder.INSTANCE;
}
// Jackson does use these members even if your IDE claims otherwise
@Json
public static class VisionSourceManagerState {
public List<UICameraConfiguration> disabledConfigs;
public List<PVCameraInfo> allConnectedCameras;

View File

@@ -17,7 +17,8 @@
package org.photonvision.vision.processes;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import org.opencv.core.Size;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.logging.LogGroup;
@@ -38,7 +39,7 @@ public abstract class VisionSourceSettables {
}
protected FrameStaticProperties frameStaticProperties = null;
protected HashMap<Integer, VideoMode> videoModes = new HashMap<>();
protected List<VideoMode> videoModes = new ArrayList<>();
public CameraConfiguration getConfiguration() {
return configuration;
@@ -99,12 +100,11 @@ public abstract class VisionSourceSettables {
protected abstract void setVideoModeInternal(VideoMode videoMode);
@SuppressWarnings("unused")
public void setVideoModeIndex(int index) {
setVideoMode(videoModes.get(index));
}
public abstract HashMap<Integer, VideoMode> getAllVideoModes();
public abstract List<VideoMode> getAllVideoModes();
public double getFOV() {
return configuration.FOV;
@@ -121,8 +121,8 @@ public abstract class VisionSourceSettables {
calculateFrameStaticProps();
}
public void removeCalibration(Size unrotatedImageSize) {
configuration.removeCalibration(unrotatedImageSize);
public void removeCalibration(Size resolution) {
configuration.removeCalibration(resolution);
calculateFrameStaticProps();
}
@@ -135,8 +135,8 @@ public abstract class VisionSourceSettables {
configuration.calibrations.stream()
.filter(
it ->
it.unrotatedImageSize.width == videoMode.width
&& it.unrotatedImageSize.height == videoMode.height)
it.resolution.width == videoMode.width
&& it.resolution.height == videoMode.height)
.findFirst()
.orElse(null));
}

View File

@@ -17,10 +17,7 @@
package org.photonvision.vision.target;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import java.util.ArrayList;
import java.util.List;
import org.opencv.core.MatOfPoint3f;
@@ -49,6 +46,7 @@ import org.wpilib.math.util.Units;
*
* <p>AprilTag models are currently only used for drawing on the output stream.
*/
@Json
public enum TargetModel implements Releasable {
k2016HighGoal(
List.of(
@@ -129,7 +127,8 @@ public enum TargetModel implements Releasable {
-Units.inchesToMeters(16.25) / 2)),
0),
// 2023 AprilTag, with 6 inch marker width (inner black square).
@JsonAlias({"k6in_16h5"})
// MIGRATION: 2023
@Json.Alias({"k6in_16h5"})
kAprilTag6in_16h5(
// Corners of the tag's inner black square (excluding white border)
List.of(
@@ -139,7 +138,8 @@ public enum TargetModel implements Releasable {
new Point3(Units.inchesToMeters(3), -Units.inchesToMeters(3), 0)),
Units.inchesToMeters(3 * 2)),
// 2024 AprilTag, with 6.5 inch marker width (inner black square).
@JsonAlias({"k6p5in_36h11", "k200mmAprilTag", "kAruco6p5in_36h11"})
// MIGRATION: 2023
@Json.Alias({"k6p5in_36h11", "k200mmAprilTag", "kAruco6p5in_36h11"})
kAprilTag6p5in_36h11(
// Corners of the tag's inner black square (excluding white border)
List.of(
@@ -149,18 +149,13 @@ public enum TargetModel implements Releasable {
new Point3(-Units.inchesToMeters(6.5 / 2.0), -Units.inchesToMeters(6.5 / 2.0), 0)),
Units.inchesToMeters(6.5));
@JsonIgnore private MatOfPoint3f realWorldTargetCoordinates;
@JsonIgnore private final MatOfPoint3f visualizationBoxBottom = new MatOfPoint3f();
@JsonIgnore private final MatOfPoint3f visualizationBoxTop = new MatOfPoint3f();
@Json.Ignore private final MatOfPoint3f realWorldTargetCoordinates;
@Json.Ignore private final MatOfPoint3f visualizationBoxBottom = new MatOfPoint3f();
@Json.Ignore private final MatOfPoint3f visualizationBoxTop = new MatOfPoint3f();
@JsonProperty("realWorldCoordinatesArray")
private List<Point3> realWorldCoordinatesArray;
@JsonProperty("boxHeight")
private double boxHeight;
TargetModel() {}
TargetModel(MatOfPoint3f realWorldTargetCoordinates, double boxHeight) {
this.realWorldTargetCoordinates = realWorldTargetCoordinates;
this.realWorldCoordinatesArray = realWorldTargetCoordinates.toList();
@@ -176,11 +171,8 @@ public enum TargetModel implements Releasable {
this.visualizationBoxTop.fromList(topList);
}
@JsonCreator
TargetModel(
@JsonProperty(value = "realWorldCoordinatesArray") List<Point3> points,
@JsonProperty(value = "boxHeight") double boxHeight) {
this(listToMat(points), boxHeight);
TargetModel(List<Point3> realWorldCoordinatesArray, double boxHeight) {
this(listToMat(realWorldCoordinatesArray), boxHeight);
}
public List<Point3> getRealWorldCoordinatesArray() {
@@ -227,6 +219,12 @@ public enum TargetModel implements Releasable {
// return new TargetModel(corners, 0);
// }
@Json.Value
@Override
public String toString() {
return super.toString();
}
@Override
public void release() {
realWorldTargetCoordinates.release();

View File

@@ -20,8 +20,8 @@ package org.photonvision.common.configuration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.avaje.jsonb.Jsonb;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -32,7 +32,6 @@ import org.photonvision.common.LoadJNI;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
import org.photonvision.vision.pipeline.CVPipelineSettings;
@@ -135,60 +134,31 @@ public class ConfigTest {
}
@Test
public void testJacksonHandlesOldVersions() throws IOException {
var str =
public void testJsonbHandlesOldVersions() throws IOException {
var json =
"{\"baseName\":\"aaaaaa\",\"uniqueName\":\"aaaaaa\",\"nickname\":\"aaaaaa\",\"FOV\":70.0,\"path\":\"dev/vid\",\"cameraType\":\"UsbCamera\",\"currentPipelineIndex\":0,\"camPitch\":{\"radians\":0.0},\"calibrations\":[], \"cameraLEDs\":[]}";
File tempFile = File.createTempFile("test", ".json");
tempFile.deleteOnExit();
var writer = new FileWriter(tempFile);
writer.write(str);
writer.flush();
writer.close();
CameraConfiguration result =
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
tempFile.delete();
CameraConfiguration result = Jsonb.instance().type(CameraConfiguration.class).fromJson(json);
}
@Test
public void testJacksonAddUSBVIDPID() throws IOException {
var str =
public void testJsonbAddUSBVIDPID() throws IOException {
var json =
"{\"baseName\":\"aaaaaa\",\"uniqueName\":\"aaaaaa\",\"nickname\":\"aaaaaa\",\"FOV\":70.0,\"path\":\"dev/vid\",\"cameraType\":\"UsbCamera\",\"currentPipelineIndex\":0,\"camPitch\":{\"radians\":0.0},\"calibrations\":[], \"usbVID\":3, \"usbPID\":4, \"cameraLEDs\":[]}";
File tempFile = File.createTempFile("test", ".json");
tempFile.deleteOnExit();
var writer = new FileWriter(tempFile);
writer.write(str);
writer.flush();
writer.close();
try {
CameraConfiguration result =
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
String ser = JacksonUtils.serializeToString(result);
System.out.println(ser);
} catch (Exception e) {
e.printStackTrace();
}
tempFile.delete();
CameraConfiguration result = Jsonb.instance().type(CameraConfiguration.class).fromJson(json);
String ser = Jsonb.instance().toJson(result);
System.out.println(ser);
}
@Test
public void testJacksonHandlesOldTargetEnum() throws IOException {
var str = "[ \"AprilTagPipelineSettings\", {\n \"targetModel\" : \"k6in_16h5\"\n} ]\n";
public void testJsonbHandlesOldTargetEnum() throws IOException {
var json = "[ \"AprilTagPipelineSettings\", {\n \"targetModel\" : \"k6in_16h5\"\n} ]\n";
File tempFile = File.createTempFile("test", ".json");
tempFile.deleteOnExit();
var writer = new FileWriter(tempFile);
writer.write(str);
writer.flush();
writer.close();
json = CVPipelineSettings.remapSettingsJson(json);
AprilTagPipelineSettings settings =
(AprilTagPipelineSettings)
JacksonUtils.deserialize(tempFile.toPath(), CVPipelineSettings.class);
(AprilTagPipelineSettings) Jsonb.instance().type(CVPipelineSettings.class).fromJson(json);
assertEquals(TargetModel.kAprilTag6in_16h5, settings.targetModel);
tempFile.delete();
}
}

View File

@@ -20,8 +20,11 @@ package org.photonvision.common.configuration;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
@@ -30,26 +33,30 @@ import org.photonvision.common.util.TestUtils;
public class NetworkConfigTest {
@Test
public void testSerialization() throws IOException {
var mapper = new ObjectMapper();
var path = Path.of("netTest.json");
mapper.writeValue(path.toFile(), new NetworkConfig());
assertDoesNotThrow(() -> mapper.readValue(path.toFile(), NetworkConfig.class));
JsonType<NetworkConfig> jsonb = Jsonb.instance().type(NetworkConfig.class);
try (var outputStream = new FileOutputStream(path.toFile())) {
jsonb.toJson(new NetworkConfig(), outputStream);
}
try (var inputStream = new FileInputStream(path.toFile())) {
assertDoesNotThrow(() -> jsonb.fromJson(inputStream));
}
new File("netTest.json").delete();
}
@Test
public void testDeserializeTeamNumberOrNtServerAddress() {
{
var folder = TestUtils.getResourcesFolderPath(true).resolve("network-old-team-number");
var folder = TestUtils.getResourcesFolderPath(true).resolve("network-team-number");
var configMgr = new ConfigManager(folder, new LegacyConfigProvider(folder));
configMgr.load();
assertEquals("9999", configMgr.getConfig().getNetworkConfig().ntServerAddress);
}
{
var folder = TestUtils.getResourcesFolderPath(true).resolve("network-new-team-number");
var folder = TestUtils.getResourcesFolderPath(true).resolve("network-ip-addr");
var configMgr = new ConfigManager(folder, new LegacyConfigProvider(folder));
configMgr.load();
assertEquals("9999", configMgr.getConfig().getNetworkConfig().ntServerAddress);
assertEquals("127.0.0.1", configMgr.getConfig().getNetworkConfig().ntServerAddress);
}
}
}

View File

@@ -20,13 +20,14 @@ package org.photonvision.common.configuration;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import java.nio.file.Path;
import java.util.LinkedList;
import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
import org.photonvision.common.util.file.JacksonUtils;
public class NeuralNetworkPropertyManagerTest {
@Test
@@ -42,10 +43,12 @@ public class NeuralNetworkPropertyManagerTest {
640,
Family.RKNN,
Version.YOLOV8));
String result = assertDoesNotThrow(() -> JacksonUtils.serializeToString(nnpm));
var deserializedNnpm =
assertDoesNotThrow(
() -> JacksonUtils.deserialize(result, NeuralNetworkModelsSettings.class));
JsonType<NeuralNetworkModelsSettings> jsonb =
Jsonb.instance().type(NeuralNetworkModelsSettings.class);
String result = assertDoesNotThrow(() -> jsonb.toJson(nnpm));
System.out.println(result);
var deserializedNnpm = assertDoesNotThrow(() -> jsonb.fromJson(result));
assertEquals(nnpm.getModels().length, deserializedNnpm.getModels().length);
assertEquals(nnpm.getModels()[0], deserializedNnpm.getModels()[0]);
}
}

View File

@@ -19,8 +19,9 @@ package org.photonvision.common.configuration;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.avaje.json.JsonDataException;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
@@ -32,8 +33,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.CameraQuirk;
import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.pipeline.AdvancedPipelineSettings;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
@@ -92,32 +93,6 @@ public class SQLConfigTest {
assertEquals(cfgLoader.getConfig().getNetworkConfig().ntServerAddress, "5940");
}
@Test
public void testLoad2024_3_1() throws IOException {
// Copy the 2024.3.1 config to a temp dir
FileUtils.copyDirectory(
TestUtils.getConfigDirectoriesPath(false)
.resolve("photonvision_config_from_v2024.3.1")
.toFile(),
tmpDir.resolve("photonvision_config_from_v2024.3.1").toFile());
var cfgLoader = new SqlConfigProvider(tmpDir.resolve("photonvision_config_from_v2024.3.1"));
assertDoesNotThrow(cfgLoader::load);
System.out.println(cfgLoader.getConfig());
for (var c : CameraQuirk.values()) {
assertDoesNotThrow(
() ->
cfgLoader
.config
.getCameraConfigurations()
.get("Microsoft_LifeCam_HD-3000")
.cameraQuirks
.hasQuirk(c));
}
}
void common2025p3p1Assertions(PhotonConfiguration config) {
// Make sure we got 8 cameras
assertEquals(8, config.getCameraConfigurations().size());
@@ -134,7 +109,7 @@ public class SQLConfigTest {
}
@Test
public void testLoadNewNNMM() throws JsonProcessingException, IOException {
public void testLoadNewNNMM() throws JsonDataException, IOException {
var folder = tmpDir.resolve("2025.3.1-old-nnmm");
FileUtils.copyDirectory(
TestUtils.getConfigDirectoriesPath(false).resolve("2025.3.1-old-nnmm").toFile(),
@@ -165,7 +140,7 @@ public class SQLConfigTest {
common2025p3p1Assertions(reloadedProvider.getConfig());
// And make sure NNPM has all 5 models
assertEquals(5, reloadedProvider.getConfig().neuralNetworkPropertyManager().getModels().length);
assertEquals(5, reloadedProvider.getConfig().getNeuralNetworkProperties().getModels().length);
ConfigManager.INSTANCE = null;
}
@@ -210,4 +185,31 @@ public class SQLConfigTest {
ConfigManager.INSTANCE = null;
}
@Test
public void testV2026p3p4WindowsPaths() throws JsonDataException, IOException {
assumeTrue(
Platform.isWindows(), "This test is only relevant on Windows, skipping on other platforms");
var configName = "2026.3.4-windows";
var folder = tmpDir.resolve(configName);
FileUtils.copyDirectory(
TestUtils.getConfigDirectoriesPath(false).resolve(configName).toFile(), folder.toFile());
var cfgManager = new ConfigManager(folder, new SqlConfigProvider(folder));
cfgManager.load();
// Make sure we have calibrated 1280x720, and the board observation paths matches
var camCfg =
cfgManager
.getConfig()
.getCameraConfigurations()
.get("1414304b-6812-487a-ab5c-89ee70704fae");
assertEquals(1280, camCfg.calibrations.get(0).resolution.width);
assertEquals(720, camCfg.calibrations.get(0).resolution.height);
assertEquals(
"C:\\Users\\matth\\Documents\\GitHub\\photonvision\\test\\photonvision_config\\calibration\\1414304b-6812-487a-ab5c-89ee70704fae\\imgs\\1280x720\\img0.png",
camCfg.calibrations.get(0).observations.get(0).snapshotDataLocation.toString());
}
}

View File

@@ -22,7 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.diozero.internal.spi.NativeDeviceFactoryInterface;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.jsonb.Jsonb;
import java.io.FileInputStream;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.HardwareConfig;
@@ -33,10 +34,9 @@ import org.photonvision.common.util.TestUtils;
public class HardwareConfigTest {
@Test
public void loadJson() {
try {
System.out.println("Loading Hardware configs...");
var config =
new ObjectMapper().readValue(TestUtils.getHardwareConfigJson(), HardwareConfig.class);
System.out.println("Loading Hardware configs...");
try (var stream = new FileInputStream(TestUtils.getHardwareConfigJson())) {
var config = Jsonb.instance().type(HardwareConfig.class).fromJson(stream);
assertEquals(config.deviceName, "PhotonVision");
// Ensure defaults are not null
assertArrayEquals(config.ledPins.stream().mapToInt(i -> i).toArray(), new int[] {2, 13});

View File

@@ -22,7 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.diozero.internal.provider.builtin.DefaultDeviceFactory;
import com.diozero.internal.spi.NativeDeviceFactoryInterface;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.jsonb.Jsonb;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
@@ -68,8 +69,9 @@ public class HardwareTest {
@BeforeEach
void setup() throws IOException {
System.out.println("Loading Hardware configs...");
hardwareConfig =
new ObjectMapper().readValue(TestUtils.getHardwareConfigJson(), HardwareConfig.class);
try (var stream = new FileInputStream(TestUtils.getHardwareConfigJson())) {
hardwareConfig = Jsonb.instance().type(HardwareConfig.class).fromJson(stream);
}
deviceFactory = HardwareManager.configureCustomGPIO(hardwareConfig);
}

View File

@@ -128,10 +128,7 @@ public class CalibrationRotationPipeTest {
FrameStaticProperties frameProps =
new FrameStaticProperties(
(int) coeffs.unrotatedImageSize.width,
(int) coeffs.unrotatedImageSize.height,
66,
coeffs);
(int) coeffs.resolution.width, (int) coeffs.resolution.height, 66, coeffs);
FrameStaticProperties rotatedFrameProps = frameProps.rotate(rot);
@@ -210,7 +207,7 @@ public class CalibrationRotationPipeTest {
double[] rotatedCamMat = {fx, 0, res.width - cx, 0, fy, res.height - cy, 0, 0, 1};
assertArrayEquals(rotatedCamMat, coeffs2.cameraIntrinsics.data);
// AND the image size should be the same
assertEquals(res, coeffs2.unrotatedImageSize);
assertEquals(res, coeffs2.resolution);
// WHEN the camera calibration is rotated 180 degrees
var coeffs3 = coeffs2.rotateCoefficients(rot);
@@ -218,7 +215,7 @@ public class CalibrationRotationPipeTest {
// THEN the camera matrix will be the same as the original
assertArrayEquals(intrinsics, coeffs3.cameraIntrinsics.data);
// AND the image size should be the same
assertEquals(res, coeffs2.unrotatedImageSize);
assertEquals(res, coeffs2.resolution);
}
@CartesianTest

View File

@@ -21,8 +21,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -115,9 +115,9 @@ public class VisionModuleManagerTest {
}
@Override
public HashMap<Integer, VideoMode> getAllVideoModes() {
var ret = new HashMap<Integer, VideoMode>();
ret.put(0, getCurrentVideoMode());
public List<VideoMode> getAllVideoModes() {
var ret = new ArrayList<VideoMode>();
ret.add(getCurrentVideoMode());
return ret;
}

View File

@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.avaje.jsonb.Jsonb;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -31,8 +32,8 @@ import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.PhotonConfiguration;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.camera.PVCameraInfo;
import org.wpilib.vision.camera.UsbCameraInfo;
@@ -94,17 +95,18 @@ public class VisionSourceManagerTest {
7,
8));
var str = JacksonUtils.serializeToString(usb);
var str = Jsonb.instance().type(PVCameraInfo.class).toJson(usb);
System.out.println(str);
System.out.println(JacksonUtils.deserialize(str, PVCameraInfo.class));
System.out.println(Jsonb.instance().type(PVCameraInfo.class).fromJson(str));
}
{
var csi =
PVCameraInfo.fromCSICameraInfo(
"/dev/v4l/by-path/platform-1f00110000.csi-video-index0", "rp1-cfe");
var str = JacksonUtils.serializeToString(csi);
var str = Jsonb.instance().type(PVCameraInfo.class).toJson(csi);
System.out.println(str);
System.out.println(JacksonUtils.deserialize(str, PVCameraInfo.class));
System.out.println(Jsonb.instance().type(PVCameraInfo.class).fromJson(str));
}
}
@@ -137,7 +139,10 @@ public class VisionSourceManagerTest {
vsm.assignUnmatchedCamera(fileCamera1);
System.out.println(JacksonUtils.serializeToString(ConfigManager.getInstance().getConfig()));
System.out.println(
Jsonb.instance()
.type(PhotonConfiguration.class)
.toJson(ConfigManager.getInstance().getConfig()));
// And make assertions about the current matching state
assertEquals(1, vsm.getVsmState().allConnectedCameras.size());
@@ -268,7 +273,10 @@ public class VisionSourceManagerTest {
vsm.assignUnmatchedCamera(fileCamera3);
System.out.println(JacksonUtils.serializeToString(ConfigManager.getInstance().getConfig()));
System.out.println(
Jsonb.instance()
.type(PhotonConfiguration.class)
.toJson(ConfigManager.getInstance().getConfig()));
// And make assertions about the current matching state
assertEquals(3, vsm.getVsmState().allConnectedCameras.size());

View File

@@ -24,7 +24,10 @@
package org.photonvision.simulation;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonIoException;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.Jsonb;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -66,6 +69,26 @@ import org.wpilib.math.util.Pair;
* network latency and the latency reported will always be perfect.
*/
public class SimCameraProperties {
@Json
record SimCameraData(SimCameraCalibrationData[] calibrations) {
@Json
record SimCameraCalibrationData(
ResolutionData resolution,
CameraIntrinsicsData cameraIntrinsics,
DistortionCoefficients distCoeffs,
double[] perViewErrors,
double standardDeviation) {
@Json
record ResolutionData(int width, int height) {}
@Json
record CameraIntrinsicsData(double[] data) {}
@Json
record DistortionCoefficients(double[] data) {}
}
}
private final Random rand = new Random();
// calibration
private int resWidth;
@@ -114,47 +137,26 @@ public class SimCameraProperties {
* calibrated resolution.
*/
public SimCameraProperties(Path path, int width, int height) throws IOException {
var mapper = new ObjectMapper();
var json = mapper.readTree(path.toFile());
json = json.get("calibrations");
SimCameraData data;
try (var stream = new FileInputStream(path.toFile())) {
data = Jsonb.instance().type(SimCameraData.class).fromJson(stream);
} catch (JsonIoException e) {
throw new IOException("Invalid calibration JSON", e);
}
boolean success = false;
try {
for (int i = 0; i < json.size() && !success; i++) {
// check if this calibration entry is our desired resolution
var calib = json.get(i);
int jsonWidth = calib.get("resolution").get("width").asInt();
int jsonHeight = calib.get("resolution").get("height").asInt();
if (jsonWidth != width || jsonHeight != height) continue;
// get the relevant calibration values
var jsonIntrinsicsNode = calib.get("cameraIntrinsics").get("data");
double[] jsonIntrinsics = new double[jsonIntrinsicsNode.size()];
for (int j = 0; j < jsonIntrinsicsNode.size(); j++) {
jsonIntrinsics[j] = jsonIntrinsicsNode.get(j).asDouble();
}
var jsonDistortNode = calib.get("distCoeffs").get("data");
double[] jsonDistortion = new double[8];
Arrays.fill(jsonDistortion, 0);
for (int j = 0; j < jsonDistortNode.size(); j++) {
jsonDistortion[j] = jsonDistortNode.get(j).asDouble();
}
var jsonViewErrors = calib.get("perViewErrors");
double jsonAvgError = 0;
for (int j = 0; j < jsonViewErrors.size(); j++) {
jsonAvgError += jsonViewErrors.get(j).asDouble();
}
jsonAvgError /= jsonViewErrors.size();
double jsonErrorStdDev = calib.get("standardDeviation").asDouble();
// assign the read JSON values to this CameraProperties
setCalibration(
jsonWidth,
jsonHeight,
MatBuilder.fill(Nat.N3(), Nat.N3(), jsonIntrinsics),
MatBuilder.fill(Nat.N8(), Nat.N1(), jsonDistortion));
setCalibError(jsonAvgError, jsonErrorStdDev);
success = true;
}
} catch (Exception e) {
throw new IOException("Invalid calibration JSON");
for (var calib : data.calibrations) {
// check if this calibration entry is our desired resolution
if (calib.resolution.width != width || calib.resolution.height != height) continue;
// get the relevant calibration values
double avgViewError = Arrays.stream(calib.perViewErrors).average().orElse(0);
// assign the read JSON values to this CameraProperties
setCalibration(
calib.resolution.width,
calib.resolution.height,
MatBuilder.fill(Nat.N3(), Nat.N3(), calib.cameraIntrinsics.data),
MatBuilder.fill(Nat.N8(), Nat.N1(), calib.distCoeffs.data));
setCalibError(avgViewError, calib.standardDeviation);
success = true;
}
if (!success) throw new IOException("Requested resolution not found in calibration");
}

View File

@@ -39,6 +39,7 @@ shadowJar {
configurations = [
project.configurations.runtimeClasspath
]
mergeServiceFiles()
}
node {

View File

@@ -235,6 +235,8 @@ public class Main {
Logger.setLevel(LogGroup.General, logLevel);
logger.info("Logging initialized in debug mode.");
System.setProperty("jsonb.disableAdapterSpi", "true");
logger.info(
"Starting PhotonVision version "
+ PhotonVersion.versionString

View File

@@ -17,22 +17,21 @@
package org.photonvision.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.jackson.JacksonAdapter;
import io.javalin.websocket.WsBinaryMessageContext;
import io.javalin.websocket.WsCloseContext;
import io.javalin.websocket.WsConnectContext;
import io.javalin.websocket.WsContext;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.jetbrains.annotations.Nullable;
import org.msgpack.jackson.dataformat.MessagePackFactory;
import org.photonvision.common.dataflow.DataChangeDestination;
import org.photonvision.common.dataflow.DataChangeService;
@@ -47,7 +46,9 @@ import org.wpilib.math.util.Pair;
public class DataSocketHandler {
private final Logger logger = new Logger(DataSocketHandler.class, LogGroup.WebServer);
private final List<WsContext> users = new CopyOnWriteArrayList<>();
private final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
private final JacksonAdapter adapter =
JacksonAdapter.builder().jsonFactory(new MessagePackFactory()).serializeEmpty(true).build();
private final Jsonb msgpackJsonb = Jsonb.builder().adapter(adapter).build();
private final DataChangeService dcService = DataChangeService.getInstance();
@SuppressWarnings("FieldCanBeLocal")
@@ -91,20 +92,16 @@ public class DataSocketHandler {
}
}
@Json
static record WSMessage(
@Nullable String cameraUniqueName, @Json.Unmapped Map<String, Object> properties) {}
@SuppressWarnings({"unchecked"})
public void onBinaryMessage(WsBinaryMessageContext context) {
try {
Map<String, Object> deserializedData =
objectMapper.readValue(context.data(), new TypeReference<>() {});
var message = msgpackJsonb.type(WSMessage.class).fromJson(context.data());
// Special case the current camera index
String cameraUniqueName = "";
if (deserializedData.get("cameraUniqueName") instanceof String camUniqueNameValue) {
cameraUniqueName = camUniqueNameValue;
deserializedData.remove("cameraUniqueName");
}
for (Map.Entry<String, Object> entry : deserializedData.entrySet()) {
for (Map.Entry<String, Object> entry : message.properties.entrySet()) {
try {
var entryKey = entry.getKey();
var entryValue = entry.getValue();
@@ -131,7 +128,7 @@ public class DataSocketHandler {
DataChangeDestination.DCD_ACTIVEMODULE,
"isDriverMode",
(Boolean) entryValue,
cameraUniqueName,
message.cameraUniqueName,
context));
case SMT_CHANGECAMERANAME ->
dcService.publishEvent(
@@ -139,7 +136,7 @@ public class DataSocketHandler {
DataChangeDestination.DCD_ACTIVEMODULE,
"cameraNickname",
(String) entryValue,
cameraUniqueName,
message.cameraUniqueName,
context));
case SMT_CHANGEPIPELINENAME ->
dcService.publishEvent(
@@ -147,38 +144,39 @@ public class DataSocketHandler {
DataChangeDestination.DCD_ACTIVEMODULE,
"pipelineName",
(String) entryValue,
cameraUniqueName,
message.cameraUniqueName,
context));
case SMT_ADDNEWPIPELINE -> {
// HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
// var type = (PipelineType) data.get("pipelineType");
// var name = (String) data.get("pipelineName");
var arr = (ArrayList<Object>) entryValue;
var arr = (List<Object>) entryValue;
var name = (String) arr.get(0);
var type = PipelineType.values()[(Integer) arr.get(1) + 3];
var type = PipelineType.values()[((Long) arr.get(1)).intValue() + 3];
dcService.publishEvent(
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEMODULE,
"newPipelineInfo",
Pair.of(name, type),
cameraUniqueName,
message.cameraUniqueName,
context));
}
case SMT_CHANGEBRIGHTNESS ->
HardwareManager.getInstance()
.setBrightnessPercent(Integer.parseInt(entryValue.toString()));
case SMT_DUPLICATEPIPELINE -> {
var pipeIndex = (Integer) entryValue;
var pipeIndex = ((Long) entryValue).intValue();
logger.info("Duplicating pipe@index" + pipeIndex + " for camera " + cameraUniqueName);
logger.info(
"Duplicating pipe@index" + pipeIndex + " for camera " + message.cameraUniqueName);
dcService.publishEvent(
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEMODULE,
"duplicatePipeline",
pipeIndex,
cameraUniqueName,
message.cameraUniqueName,
context));
}
case SMT_DELETECURRENTPIPELINE ->
@@ -187,27 +185,29 @@ public class DataSocketHandler {
DataChangeDestination.DCD_ACTIVEMODULE,
"deleteCurrPipeline",
0,
cameraUniqueName,
message.cameraUniqueName,
context));
case SMT_ROBOTOFFSETPOINT ->
dcService.publishEvent(
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEMODULE,
"robotOffsetPoint",
(Integer) entryValue,
cameraUniqueName,
((Long) entryValue).intValue(),
message.cameraUniqueName,
null));
case SMT_CURRENTCAMERA ->
dcService.publishEvent(
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_OTHER, "changeUICamera", (Integer) entryValue));
DataChangeDestination.DCD_OTHER,
"changeUICamera",
((Long) entryValue).intValue()));
case SMT_CURRENTPIPELINE ->
dcService.publishEvent(
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEMODULE,
"changePipeline",
(Integer) entryValue,
cameraUniqueName,
((Long) entryValue).intValue(),
message.cameraUniqueName,
context));
case SMT_STARTPNPCALIBRATION ->
dcService.publishEvent(
@@ -215,7 +215,7 @@ public class DataSocketHandler {
DataChangeDestination.DCD_ACTIVEMODULE,
"startCalibration",
(Map) entryValue,
cameraUniqueName,
message.cameraUniqueName,
context));
case SMT_SAVEINPUTSNAPSHOT ->
dcService.publishEvent(
@@ -223,7 +223,7 @@ public class DataSocketHandler {
DataChangeDestination.DCD_ACTIVEMODULE,
"saveInputSnapshot",
0,
cameraUniqueName,
message.cameraUniqueName,
context));
case SMT_SAVEOUTPUTSNAPSHOT ->
dcService.publishEvent(
@@ -231,7 +231,7 @@ public class DataSocketHandler {
DataChangeDestination.DCD_ACTIVEMODULE,
"saveOutputSnapshot",
0,
cameraUniqueName,
message.cameraUniqueName,
context));
case SMT_TAKECALIBRATIONSNAPSHOT ->
dcService.publishEvent(
@@ -239,10 +239,10 @@ public class DataSocketHandler {
DataChangeDestination.DCD_ACTIVEMODULE,
"takeCalSnapshot",
0,
cameraUniqueName,
message.cameraUniqueName,
context));
case SMT_PIPELINESETTINGCHANGE -> {
HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
Map<String, Object> data = (Map) entryValue;
if (data.size() >= 2) {
var cameraIndex2 = (String) data.get("cameraUniqueName");
@@ -267,28 +267,27 @@ public class DataSocketHandler {
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEMODULE,
"changePipelineType",
(Integer) entryValue,
cameraUniqueName,
((Long) entryValue).intValue(),
message.cameraUniqueName,
context));
}
} catch (Exception e) {
logger.error("Failed to parse message!", e);
}
}
} catch (IOException e) {
} catch (IllegalStateException | JsonException e) {
logger.error("Failed to deserialize message!", e);
}
}
private void sendMessage(ByteBuffer b, WsContext user) throws JsonProcessingException {
private void sendMessage(ByteBuffer b, WsContext user) {
if (user.session.isOpen()) {
user.send(b);
}
}
public void broadcastMessage(Object message, WsContext userToSkip)
throws JsonProcessingException {
ByteBuffer b = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message));
public void broadcastMessage(Object message, WsContext userToSkip) throws JsonException {
ByteBuffer b = ByteBuffer.wrap(msgpackJsonb.toJsonBytes(message));
if (userToSkip == null) {
for (WsContext user : users) {

View File

@@ -21,7 +21,6 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unused")
public enum DataSocketMessageType {
SMT_DRIVERMODE("driverMode"),
SMT_CHANGECAMERANAME("changeCameraName"),

View File

@@ -17,8 +17,9 @@
package org.photonvision.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.Jsonb;
import io.javalin.http.Context;
import io.javalin.http.UploadedFile;
import java.io.*;
@@ -53,7 +54,6 @@ 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;
@@ -71,8 +71,6 @@ public class RequestHandler {
private static final Logger logger = new Logger(RequestHandler.class, LogGroup.WebServer);
private static final ObjectMapper kObjectMapper = new ObjectMapper();
private static boolean testMode = false;
public static void onStatusRequest(Context ctx) {
@@ -84,7 +82,8 @@ public class RequestHandler {
testMode = isTestMode;
}
private record CommonCameraUniqueName(String cameraUniqueName) {}
@Json
record CommonCameraUniqueName(String cameraUniqueName) {}
public static void onSettingsImportRequest(Context ctx) {
var file = ctx.uploadedFile("data");
@@ -372,12 +371,12 @@ public class RequestHandler {
public static void onGeneralSettingsRequest(Context ctx) {
NetworkConfig config;
try {
config = kObjectMapper.readValue(ctx.bodyInputStream(), NetworkConfig.class);
config = Jsonb.instance().type(NetworkConfig.class).fromJson(ctx.bodyInputStream());
ctx.status(200);
ctx.result("Successfully saved general settings");
logger.info("Successfully saved general settings");
} catch (IOException e) {
} catch (IllegalStateException | JsonException e) {
// If the settings can't be parsed, use the default network settings
config = new NetworkConfig();
@@ -394,13 +393,14 @@ public class RequestHandler {
NetworkTablesManager.getInstance().setConfig(config);
}
private record CameraSettingsRequest(
@Json
record CameraSettingsRequest(
double fov, HashMap<CameraQuirk, Boolean> quirksToChange, String cameraUniqueName) {}
public static void onCameraSettingsRequest(Context ctx) {
try {
CameraSettingsRequest request =
kObjectMapper.readValue(ctx.body(), CameraSettingsRequest.class);
Jsonb.instance().type(CameraSettingsRequest.class).fromJson(ctx.body());
// Extract the settings from the request
double fov = request.fov;
HashMap<CameraQuirk, Boolean> quirksToChange = request.quirksToChange;
@@ -489,7 +489,7 @@ public class RequestHandler {
try {
CommonCameraUniqueName request =
kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class);
Jsonb.instance().type(CommonCameraUniqueName.class).fromJson(ctx.body());
var calData =
VisionSourceManager.getInstance()
@@ -509,7 +509,7 @@ public class RequestHandler {
ctx.result("Camera calibration successfully completed!");
ctx.status(200);
logger.info("Camera calibration successfully completed!");
} catch (JsonProcessingException e) {
} catch (IllegalStateException | JsonException e) {
ctx.status(400);
ctx.result(
"The 'cameraUniqueName' field was not found in the request. Please make sure the cameraUniqueName of the vision module is specified with the 'cameraUniqueName' key.");
@@ -523,13 +523,14 @@ public class RequestHandler {
}
}
private record DataCalibrationImportRequest(
@Json
record DataCalibrationImportRequest(
String cameraUniqueName, CameraCalibrationCoefficients calibration) {}
public static void onDataCalibrationImportRequest(Context ctx) {
try {
try (var stream = ctx.req().getInputStream()) {
DataCalibrationImportRequest request =
kObjectMapper.readValue(ctx.req().getInputStream(), DataCalibrationImportRequest.class);
Jsonb.instance().type(DataCalibrationImportRequest.class).fromJson(stream);
var uploadCalibrationEvent =
new IncomingWebSocketEvent<>(
@@ -543,7 +544,7 @@ public class RequestHandler {
ctx.status(200);
ctx.result("Calibration imported successfully from imported data!");
logger.info("Calibration imported successfully from imported data!");
} catch (JsonProcessingException e) {
} catch (IllegalStateException | JsonException e) {
ctx.status(400);
ctx.result("The provided calibration data was malformed");
logger.error("The provided calibration data was malformed", e);
@@ -704,11 +705,10 @@ public class RequestHandler {
}
ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.getNeuralNetworkProperties()
.addModelProperties(modelProperties);
logger.debug(
ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().toString());
logger.debug(ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().toString());
NeuralNetworkModelManager.getInstance().discoverModels();
@@ -860,14 +860,15 @@ public class RequestHandler {
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
}
private record DeleteObjectDetectionModelRequest(Path modelPath) {}
@Json
record DeleteObjectDetectionModelRequest(Path modelPath) {}
public static void onDeleteObjectDetectionModelRequest(Context ctx) {
logger.info("Deleting object detection model");
try {
DeleteObjectDetectionModelRequest request =
JacksonUtils.deserialize(ctx.body(), DeleteObjectDetectionModelRequest.class);
Jsonb.instance().type(DeleteObjectDetectionModelRequest.class).fromJson(ctx.body());
if (request.modelPath == null) {
ctx.status(400);
@@ -892,7 +893,7 @@ public class RequestHandler {
if (!ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.getNeuralNetworkProperties()
.removeModel(request.modelPath)) {
ctx.status(400);
ctx.result("The model's information was not found in the config");
@@ -917,12 +918,13 @@ public class RequestHandler {
}
}
private record RenameObjectDetectionModelRequest(Path modelPath, String newName) {}
@Json
record RenameObjectDetectionModelRequest(Path modelPath, String newName) {}
public static void onRenameObjectDetectionModelRequest(Context ctx) {
try {
RenameObjectDetectionModelRequest request =
JacksonUtils.deserialize(ctx.body(), RenameObjectDetectionModelRequest.class);
Jsonb.instance().type(RenameObjectDetectionModelRequest.class).fromJson(ctx.body());
if (request.modelPath == null) {
ctx.status(400);
@@ -947,7 +949,7 @@ public class RequestHandler {
if (!ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.getNeuralNetworkProperties()
.renameModel(request.modelPath, request.newName)) {
ctx.status(400);
ctx.result("The model's information was not found in the config");
@@ -994,12 +996,13 @@ public class RequestHandler {
ctx.status(HardwareManager.getInstance().restartDevice() ? 204 : 500);
}
private record CameraNicknameChangeRequest(String name, String cameraUniqueName) {}
@Json
record CameraNicknameChangeRequest(String name, String cameraUniqueName) {}
public static void onCameraNicknameChangeRequest(Context ctx) {
try {
CameraNicknameChangeRequest request =
kObjectMapper.readValue(ctx.body(), CameraNicknameChangeRequest.class);
Jsonb.instance().type(CameraNicknameChangeRequest.class).fromJson(ctx.body());
VisionSourceManager.getInstance()
.vmm
@@ -1008,7 +1011,7 @@ public class RequestHandler {
ctx.status(200);
ctx.result("Successfully changed the camera name to: " + request.name);
logger.info("Successfully changed the camera name to: " + request.name);
} catch (JsonProcessingException e) {
} catch (IllegalStateException | JsonException e) {
ctx.status(400).result("Invalid JSON format");
logger.error("Failed to process camera nickname change request", e);
} catch (Exception e) {
@@ -1038,8 +1041,8 @@ public class RequestHandler {
module.getStateAsCameraConfig().calibrations.stream()
.filter(
it ->
Math.abs(it.unrotatedImageSize.width - width) < 1e-4
&& Math.abs(it.unrotatedImageSize.height - height) < 1e-4)
Math.abs(it.resolution.width - width) < 1e-4
&& Math.abs(it.resolution.height - height) < 1e-4)
.findFirst()
.orElse(null);
@@ -1052,12 +1055,13 @@ public class RequestHandler {
ctx.status(200);
}
private record CalibrationRemoveRequest(int width, int height, String cameraUniqueName) {}
@Json
record CalibrationRemoveRequest(int width, int height, String cameraUniqueName) {}
public static void onCalibrationRemoveRequest(Context ctx) {
try {
CalibrationRemoveRequest request =
kObjectMapper.readValue(ctx.body(), CalibrationRemoveRequest.class);
Jsonb.instance().type(CalibrationRemoveRequest.class).fromJson(ctx.body());
logger.info(
"Attempting to remove calibration for camera: "
@@ -1083,7 +1087,7 @@ public class RequestHandler {
+ request.width
+ "x"
+ request.height);
} catch (JsonProcessingException e) {
} catch (IllegalStateException | JsonException e) {
ctx.status(400).result("Invalid JSON format");
logger.error("Failed to process calibration removed request", e);
} catch (Exception e) {
@@ -1107,8 +1111,8 @@ public class RequestHandler {
.stream()
.filter(
it ->
Math.abs(it.unrotatedImageSize.width - width) < 1e-4
&& Math.abs(it.unrotatedImageSize.height - height) < 1e-4)
Math.abs(it.resolution.width - width) < 1e-4
&& Math.abs(it.resolution.height - height) < 1e-4)
.findFirst()
.orElse(null);
@@ -1146,8 +1150,8 @@ public class RequestHandler {
cc.calibrations.stream()
.filter(
it ->
Math.abs(it.unrotatedImageSize.width - width) < 1e-4
&& Math.abs(it.unrotatedImageSize.height - height) < 1e-4)
Math.abs(it.resolution.width - width) < 1e-4
&& Math.abs(it.resolution.height - height) < 1e-4)
.findFirst()
.orElse(null);
@@ -1319,7 +1323,7 @@ public class RequestHandler {
public static void onNukeOneCamera(Context ctx) {
try {
CommonCameraUniqueName request =
kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class);
Jsonb.instance().type(CommonCameraUniqueName.class).fromJson(ctx.body());
logger.warn("Deleting camera name " + request.cameraUniqueName);
@@ -1334,7 +1338,7 @@ public class RequestHandler {
VisionSourceManager.getInstance().deleteVisionSource(request.cameraUniqueName);
ctx.status(200);
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Failed to delete camera", e);
ctx.status(500);
ctx.result("Failed to delete camera");
@@ -1346,7 +1350,7 @@ public class RequestHandler {
logger.info(ctx.queryString());
try {
CommonCameraUniqueName request =
kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class);
Jsonb.instance().type(CommonCameraUniqueName.class).fromJson(ctx.body());
if (VisionSourceManager.getInstance()
.reactivateDisabledCameraConfig(request.cameraUniqueName)) {
@@ -1354,7 +1358,7 @@ public class RequestHandler {
} else {
ctx.status(403);
}
} catch (IOException e) {
} catch (IllegalStateException | JsonException e) {
ctx.status(401);
logger.error("Failed to process activate matched camera request", e);
ctx.result("Failed to process activate matched camera request");
@@ -1362,14 +1366,15 @@ public class RequestHandler {
}
}
private record AssignUnmatchedCamera(PVCameraInfo cameraInfo) {}
@Json
record AssignUnmatchedCamera(PVCameraInfo cameraInfo) {}
public static void onAssignUnmatchedCameraRequest(Context ctx) {
logger.info(ctx.queryString());
try {
AssignUnmatchedCamera request =
kObjectMapper.readValue(ctx.body(), AssignUnmatchedCamera.class);
Jsonb.instance().type(AssignUnmatchedCamera.class).fromJson(ctx.body());
if (request.cameraInfo == null) {
ctx.status(400);
@@ -1385,7 +1390,7 @@ public class RequestHandler {
}
ctx.result("Successfully assigned camera: " + request.cameraInfo);
} catch (IOException e) {
} catch (IllegalStateException | JsonException e) {
ctx.status(401);
logger.error("Failed to process assign unmatched camera request", e);
ctx.result("Failed to process assign unmatched camera request");
@@ -1397,14 +1402,14 @@ public class RequestHandler {
logger.info(ctx.queryString());
try {
CommonCameraUniqueName request =
kObjectMapper.readValue(ctx.body(), CommonCameraUniqueName.class);
Jsonb.instance().type(CommonCameraUniqueName.class).fromJson(ctx.body());
if (VisionSourceManager.getInstance().deactivateVisionSource(request.cameraUniqueName)) {
ctx.status(200);
} else {
ctx.status(403);
}
} catch (IOException e) {
} catch (IllegalStateException | JsonException e) {
ctx.status(401);
logger.error("Failed to process unassign camera request", e);
ctx.result("Failed to process unassign camera request");

View File

@@ -41,7 +41,7 @@ public class Server {
}
@Override
public void onDataChangeEvent(DataChangeEvent<?> event) {
public <T> void onDataChangeEvent(DataChangeEvent<T> event) {
if (event.propertyName.equals("restartServer")) {
Server.restart();
}

View File

@@ -17,13 +17,14 @@
package org.photonvision.server;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.Jsonb;
import io.javalin.http.Context;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.JacksonUtils;
public class TestRequestHandler {
// Treat all 2XX calls as "INFO"
@@ -39,12 +40,13 @@ public class TestRequestHandler {
ConfigManager.getInstance().load();
}
private record PlatformOverrideRequest(Platform platform) {}
@Json
record PlatformOverrideRequest(Platform platform) {}
public static void handlePlatformOverrideRequest(Context ctx) {
try {
PlatformOverrideRequest request =
JacksonUtils.deserialize(ctx.body(), PlatformOverrideRequest.class);
Jsonb.instance().type(PlatformOverrideRequest.class).fromJson(ctx.body());
Platform platform = request.platform();
logger.info("Overriding platform to: " + platform);

View File

@@ -38,8 +38,8 @@ public class UIInboundSubscriber extends DataChangeSubscriber {
}
@Override
public void onDataChangeEvent(DataChangeEvent<?> event) {
if (event instanceof IncomingWebSocketEvent incomingWSEvent) {
public <T> void onDataChangeEvent(DataChangeEvent<T> event) {
if (event instanceof IncomingWebSocketEvent<T> incomingWSEvent) {
if (incomingWSEvent.propertyName.equals("userConnected")
|| incomingWSEvent.propertyName.equals("sendFullSettings")) {
// Send full settings

View File

@@ -17,7 +17,7 @@
package org.photonvision.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.avaje.json.JsonDataException;
import java.util.Collections;
import java.util.HashMap;
import org.photonvision.common.dataflow.DataChangeDestination;
@@ -43,15 +43,15 @@ class UIOutboundSubscriber extends DataChangeSubscriber {
}
@Override
public void onDataChangeEvent(DataChangeEvent event) {
if (event instanceof OutgoingUIEvent thisEvent) {
public <T> void onDataChangeEvent(DataChangeEvent<T> event) {
if (event instanceof OutgoingUIEvent<T> thisEvent) {
try {
if (event.data instanceof HashMap data) {
socketHandler.broadcastMessage(data, thisEvent.originContext);
} else {
socketHandler.broadcastMessage(event.data, thisEvent.originContext);
}
} catch (JsonProcessingException e) {
} catch (JsonDataException e) {
logger.error("Failed to process outgoing message!", e);
}
}

View File

@@ -17,8 +17,8 @@
package org.photonvision.common.networktables;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import java.util.HashSet;
import java.util.Set;
import org.photonvision.common.dataflow.structures.Packet;
@@ -34,12 +34,11 @@ public class PacketPublisher<T> implements AutoCloseable {
this.publisher = publisher;
this.photonStruct = photonStruct;
var mapper = new ObjectMapper();
try {
this.publisher
.getTopic()
.setProperty("message_uuid", mapper.writeValueAsString(photonStruct.getInterfaceUUID()));
} catch (JsonProcessingException e) {
.setProperty("message_uuid", Jsonb.instance().toJson(photonStruct.getInterfaceUUID()));
} catch (IllegalStateException | JsonException e) {
// TODO Auto-generated catch block
e.printStackTrace();
throw new RuntimeException(e);

View File

@@ -18,7 +18,9 @@
package org.photonvision.jni;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -139,15 +141,18 @@ public final class CombinedRuntimeLoader {
* Architecture-specific information containing file hashes for a specific CPU architecture (e.g.,
* x86-64, arm64).
*/
@Json
public record ArchInfo(Map<String, String> fileHashes) {}
/**
* Platform-specific information containing architectures for a specific OS platform (e.g., linux,
* windows).
*/
@Json
public record PlatformInfo(Map<String, ArchInfo> architectures) {}
/** Overall resource information to be serialized */
@Json
public record ResourceInformation(
// Combined MD5 hash of all native resource files
String hash,
@@ -167,10 +172,10 @@ public final class CombinedRuntimeLoader {
*/
public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName)
throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonType<ResourceInformation> jsonb = Jsonb.instance().type(ResourceInformation.class);
ResourceInformation resourceInfo;
try (var stream = clazz.getResourceAsStream(resourceName)) {
resourceInfo = mapper.readValue(stream, ResourceInformation.class);
resourceInfo = jsonb.fromJson(stream);
}
var platformPath = Paths.get(getPlatformPath());

View File

@@ -18,8 +18,8 @@ dependencies {
implementation "io.javalin:javalin:$javalinVersion"
implementation 'org.msgpack:msgpack-core:0.9.0'
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.0'
implementation "org.msgpack:msgpack-core:$msgpackVersion"
implementation "org.msgpack:jackson-dataformat-msgpack:$msgpackVersion"
implementation "org.wpilib.wpiutil:wpiutil-java:$wpilibVersion"
implementation "org.wpilib.datalog:datalog-java:$wpilibVersion"
@@ -34,9 +34,11 @@ dependencies {
implementation "org.wpilib.wpiunits:wpiunits-java:$wpilibVersion"
implementation wpilibTools.deps.wpilibOpenCvJava("frc" + openCVYear, openCVversion)
implementation group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: jacksonVersion
implementation group: "com.fasterxml.jackson.core", name: "jackson-core", version: jacksonVersion
implementation group: "com.fasterxml.jackson.core", name: "jackson-databind", version: jacksonVersion
implementation group: "io.avaje", name: "avaje-jsonb", version: avajeJsonbVersion
annotationProcessor group: "io.avaje", name: "avaje-jsonb-generator", version: avajeJsonbVersion
implementation group: "io.avaje", name: "avaje-jsonb-jackson", version: avajeJsonbVersion
implementation group: "org.ejml", name: "ejml-simple", version: ejmlVersion
implementation group: "us.hebi.quickbuf", name: "quickbuf-runtime", version: quickbufVersion;
@@ -53,6 +55,7 @@ dependencies {
test {
useJUnitPlatform()
systemProperty("java.awt.headless", !project.hasProperty("enableTestUi"))
systemProperty 'jsonb.disableAdapterSpi', 'true'
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
exceptionFormat = "full"

View File

@@ -120,6 +120,7 @@ publishing {
test {
useJUnitPlatform()
systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true'
systemProperty 'jsonb.disableAdapterSpi', 'true'
testLogging {
events "failed"
exceptionFormat = "full"
@@ -149,9 +150,10 @@ dependencies {
implementation "org.wpilib.wpiunits:wpiunits-java:$wpilibVersion"
implementation wpilibTools.deps.wpilibOpenCvJava("frc" + openCVYear, openCVversion)
implementation group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: jacksonVersion
implementation group: "com.fasterxml.jackson.core", name: "jackson-core", version: jacksonVersion
implementation group: "com.fasterxml.jackson.core", name: "jackson-databind", version: jacksonVersion
implementation group: "io.avaje", name: "avaje-jsonb", version: avajeJsonbVersion
annotationProcessor group: "io.avaje", name: "avaje-jsonb-generator", version: avajeJsonbVersion
implementation group: "org.ejml", name: "ejml-simple", version: ejmlVersion
implementation group: "us.hebi.quickbuf", name: "quickbuf-runtime", version: quickbufVersion;

View File

@@ -0,0 +1,3 @@
{
"ntServerAddress" : "127.0.0.1"
}

View File

@@ -1,3 +0,0 @@
{
"teamNumber" : 9999
}