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

View File

@@ -2,7 +2,7 @@ cppHeaderFileInclude {
\.h$ \.h$
} }
modifiableFileExclude { generatedFileExclude {
photon-lib/py/photonlibpy/generated/ photon-lib/py/photonlibpy/generated/
photon-targeting/src/generated/ photon-targeting/src/generated/
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/ 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) * [EJML](https://github.com/lessthanoptimal/ejml)
* [Javalin](https://javalin.io/) * [Javalin](https://javalin.io/)
* [JSON](https://json.org) * [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) * [MessagePack for Java](https://github.com/msgpack/msgpack-java)
* [OSHI](https://github.com/oshi/oshi) * [OSHI](https://github.com/oshi/oshi)
* [QuickBuffers](https://github.com/HebiRobotics/QuickBuffers) * [QuickBuffers](https://github.com/HebiRobotics/QuickBuffers)

View File

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

View File

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

View File

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

View File

@@ -163,7 +163,7 @@ const interactiveCols = computed(() =>
/> />
<pv-switch <pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames" v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames"
:disabled="!useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.PVUsbCameraInfo" :disabled="useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.type !== 'PVUsbCameraInfo'"
label="Low Latency Mode" label="Low Latency Mode"
:switch-cols="interactiveCols" :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." 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 { useStateStore } from "@/stores/StateStore";
import type { PVCameraInfo, Resolution } from "@/types/SettingTypes"; import type { Resolution } from "@/types/SettingTypes";
import axios, { type AxiosRequestConfig } from "axios"; import axios, { type AxiosRequestConfig } from "axios";
export const resolutionsAreEqual = (a: Resolution, b: Resolution) => { export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
@@ -109,23 +109,3 @@ export const axiosPost = async (
return false; 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, inputPort: d.inputStreamPort,
outputPort: d.outputStreamPort outputPort: d.outputStreamPort
}, },
validVideoFormats: Object.entries(d.videoFormatList) validVideoFormats: d.videoFormatList.map((v, i) => ({
.sort(([firstKey], [secondKey]) => parseInt(firstKey) - parseInt(secondKey)) resolution: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars width: v.width,
.map<VideoFormat>(([k, v], i) => ({ height: v.height
resolution: { },
width: v.width, fps: v.fps,
height: v.height pixelFormat: v.pixelFormat,
}, index: v.index || i,
fps: v.fps, diagonalFOV: v.diagonalFOV,
pixelFormat: v.pixelFormat, horizontalFOV: v.horizontalFOV,
index: v.index || i, verticalFOV: v.verticalFOV,
diagonalFOV: v.diagonalFOV, standardDeviation: v.standardDeviation,
horizontalFOV: v.horizontalFOV, mean: v.mean
verticalFOV: v.verticalFOV, })),
standardDeviation: v.standardDeviation,
mean: v.mean
})),
completeCalibrations: d.calibrations, completeCalibrations: d.calibrations,
isCSICamera: d.isCSICamera, isCSICamera: d.isCSICamera,
minExposureRaw: d.minExposureRaw, minExposureRaw: d.minExposureRaw,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,8 +17,11 @@
package org.photonvision.common.configuration; 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.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.nio.file.Files; 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.LogGroup;
import org.photonvision.common.logging.Logger; import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.FileUtils; 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.photonvision.vision.processes.VisionSource;
import org.wpilib.vision.apriltag.AprilTagFieldLayout; import org.wpilib.vision.apriltag.AprilTagFieldLayout;
import org.wpilib.vision.apriltag.AprilTagFields; import org.wpilib.vision.apriltag.AprilTagFields;
@@ -126,14 +126,13 @@ class LegacyConfigProvider extends ConfigProvider {
AprilTagFieldLayout atfl = null; AprilTagFieldLayout atfl = null;
if (hardwareConfigFile.exists()) { if (hardwareConfigFile.exists()) {
try { try (var stream = new FileInputStream(hardwareConfigFile)) {
hardwareConfig = hardwareConfig = Jsonb.instance().type(HardwareConfig.class).fromJson(stream);
JacksonUtils.deserialize(hardwareConfigFile.toPath(), HardwareConfig.class);
if (hardwareConfig == null) { if (hardwareConfig == null) {
logger.error("Could not deserialize hardware config! Loading defaults"); logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig(); hardwareConfig = new HardwareConfig();
} }
} catch (IOException e) { } catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize hardware config! Loading defaults"); logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig(); hardwareConfig = new HardwareConfig();
} }
@@ -143,14 +142,13 @@ class LegacyConfigProvider extends ConfigProvider {
} }
if (hardwareSettingsFile.exists()) { if (hardwareSettingsFile.exists()) {
try { try (var stream = new FileInputStream(hardwareSettingsFile)) {
hardwareSettings = hardwareSettings = Jsonb.instance().type(HardwareSettings.class).fromJson(stream);
JacksonUtils.deserialize(hardwareSettingsFile.toPath(), HardwareSettings.class);
if (hardwareSettings == null) { if (hardwareSettings == null) {
logger.error("Could not deserialize hardware settings! Loading defaults"); logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings(); hardwareSettings = new HardwareSettings();
} }
} catch (IOException e) { } catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize hardware settings! Loading defaults"); logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings(); hardwareSettings = new HardwareSettings();
} }
@@ -160,13 +158,13 @@ class LegacyConfigProvider extends ConfigProvider {
} }
if (networkConfigFile.exists()) { if (networkConfigFile.exists()) {
try { try (var stream = new FileInputStream(networkConfigFile)) {
networkConfig = JacksonUtils.deserialize(networkConfigFile.toPath(), NetworkConfig.class); networkConfig = Jsonb.instance().type(NetworkConfig.class).fromJson(stream);
if (networkConfig == null) { if (networkConfig == null) {
logger.error("Could not deserialize network config! Loading defaults"); logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig(); networkConfig = new NetworkConfig();
} }
} catch (IOException e) { } catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize network config! Loading defaults"); logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig(); networkConfig = new NetworkConfig();
} }
@@ -184,13 +182,12 @@ class LegacyConfigProvider extends ConfigProvider {
} }
if (apriltagFieldLayoutFile.exists()) { if (apriltagFieldLayoutFile.exists()) {
try { try (var stream = new FileInputStream(apriltagFieldLayoutFile)) {
atfl = atfl = Jsonb.instance().type(AprilTagFieldLayout.class).fromJson(stream);
JacksonUtils.deserialize(apriltagFieldLayoutFile.toPath(), AprilTagFieldLayout.class);
if (atfl == null) { if (atfl == null) {
logger.error("Could not deserialize apriltag field layout! (still 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); logger.error("Could not deserialize apriltag field layout!", e);
atfl = null; // not required, nice to be explicit atfl = null; // not required, nice to be explicit
} }
@@ -227,14 +224,14 @@ class LegacyConfigProvider extends ConfigProvider {
// Delete old configs // Delete old configs
FileUtils.deleteDirectory(camerasFolder.toPath()); FileUtils.deleteDirectory(camerasFolder.toPath());
try { try (var stream = new FileOutputStream(networkConfigFile)) {
JacksonUtils.serialize(networkConfigFile.toPath(), config.getNetworkConfig()); Jsonb.instance().type(NetworkConfig.class).toJson(config.getNetworkConfig(), stream);
} catch (IOException e) { } catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save network config!", e); logger.error("Could not save network config!", e);
} }
try { try (var stream = new FileOutputStream(hardwareSettingsFile)) {
JacksonUtils.serialize(hardwareSettingsFile.toPath(), config.getHardwareSettings()); Jsonb.instance().type(HardwareSettings.class).toJson(config.getHardwareSettings(), stream);
} catch (IOException e) { } catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save hardware config!", e); logger.error("Could not save hardware config!", e);
} }
@@ -249,33 +246,11 @@ class LegacyConfigProvider extends ConfigProvider {
subdir.toFile().mkdirs(); subdir.toFile().mkdirs();
} }
try { try (var stream = new FileOutputStream(Path.of(subdir.toString(), "config.json").toFile())) {
JacksonUtils.serialize(Path.of(subdir.toString(), "config.json"), camConfig); Jsonb.instance().type(CameraConfiguration.class).toJson(camConfig, stream);
} catch (IOException e) { } catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save config.json for " + subdir, 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!"); logger.info("Settings saved!");
return false; // TODO, deal with this. Do I need to? return false; // TODO, deal with this. Do I need to?
@@ -289,11 +264,9 @@ class LegacyConfigProvider extends ConfigProvider {
for (var subdir : subdirectories) { for (var subdir : subdirectories) {
var cameraConfigPath = Path.of(subdir.toString(), "config.json"); var cameraConfigPath = Path.of(subdir.toString(), "config.json");
CameraConfiguration loadedConfig = null; CameraConfiguration loadedConfig = null;
try { try (var stream = new FileInputStream(cameraConfigPath.toFile())) {
loadedConfig = loadedConfig = Jsonb.instance().type(CameraConfiguration.class).fromJson(stream);
JacksonUtils.deserialize( } catch (IllegalStateException | JsonException e) {
cameraConfigPath.toAbsolutePath(), CameraConfiguration.class);
} catch (JsonProcessingException e) {
logger.error("Camera config deserialization failed!", e); logger.error("Camera config deserialization failed!", e);
e.printStackTrace(); 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? 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); loadedConfigurations.put(subdir.toFile().getName(), loadedConfig);
} }
} catch (IOException e) { } catch (IOException e) {

View File

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

View File

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

View File

@@ -17,9 +17,10 @@
package org.photonvision.common.configuration; package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator; import io.avaje.jsonb.Json;
import com.fasterxml.jackson.annotation.JsonIgnore; import io.avaje.jsonb.JsonType;
import com.fasterxml.jackson.annotation.JsonProperty; import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -27,27 +28,29 @@ import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; 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.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version; import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
@Json
public class NeuralNetworkModelsSettings { public class NeuralNetworkModelsSettings {
/* /*
* The properties of the model. This is used to determine which model to load. * 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) * The only families currently supported are RKNN and Rubik (custom .tflite)
*/ */
@Json
public record ModelProperties( public record ModelProperties(
@JsonProperty("modelPath") Path modelPath, Path modelPath,
@JsonProperty("nickname") String nickname, String nickname,
@JsonProperty("labels") List<String> labels, List<String> labels,
@JsonProperty("resolutionWidth") int resolutionWidth, int resolutionWidth,
@JsonProperty("resolutionHeight") int resolutionHeight, int resolutionHeight,
@JsonProperty("family") Family family, Family family,
@JsonProperty("version") Version version) { Version version) {
@JsonCreator
public ModelProperties {}
ModelProperties(ModelProperties other) { ModelProperties(ModelProperties other) {
this( this(
other.modelPath, other.modelPath,
@@ -59,13 +62,6 @@ public class NeuralNetworkModelsSettings {
other.version); 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 =========== // ============= Migration code from v2025.3.1 ===========
private static Pattern modelPattern = 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 path to the model is used as the key in the map because it is unique to
// the model, and should not change // the model, and should not change
@JsonProperty("modelPathToProperties") @Json.Ignore
private HashMap<Path, ModelProperties> modelPathToProperties = private HashMap<Path, ModelProperties> modelPathToProperties =
new HashMap<Path, ModelProperties>(); new HashMap<Path, ModelProperties>();
/** /**
* Constructor for the NeuralNetworkProperties class. * 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() {} public NeuralNetworkModelsSettings() {}
/** /**
* Constructor for the NeuralNetworkProperties class. * 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 * @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 @Override
public String toString() { public String toString() {
@@ -239,7 +268,7 @@ public class NeuralNetworkModelsSettings {
* *
* @return A list of all models * @return A list of all models
*/ */
@JsonIgnore @Json.Property("models")
public ModelProperties[] getModels() { public ModelProperties[] getModels() {
return modelPathToProperties.values().toArray(new ModelProperties[0]); return modelPathToProperties.values().toArray(new ModelProperties[0]);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,9 +17,11 @@
package org.photonvision.common.util; 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.awt.HeadlessException;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import org.opencv.core.Mat; 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 final String LIMELIGHT_480P_CAL_FILE = "limelight_1280_720.json";
public static CameraCalibrationCoefficients getCoeffs(String filename, boolean testMode) { public static CameraCalibrationCoefficients getCoeffs(String filename, boolean testMode) {
try { try (var stream =
return new ObjectMapper() new FileInputStream(Path.of(getCalibrationPath(testMode).toString(), filename).toFile())) {
.readValue( return Jsonb.instance().type(CameraCalibrationCoefficients.class).fromJson(stream);
(Path.of(getCalibrationPath(testMode).toString(), filename).toFile()), } catch (IOException | IllegalStateException | JsonException e) {
CameraCalibrationCoefficients.class);
} catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return null; 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; package org.photonvision.common.util.numbers;
import io.avaje.jsonb.Json;
import org.opencv.core.Point; import org.opencv.core.Point;
@Json
public class DoubleCouple extends NumberCouple<Double> { public class DoubleCouple extends NumberCouple<Double> {
public DoubleCouple() { public DoubleCouple() {
super(0.0, 0.0); super(0.0, 0.0);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,33 +17,31 @@
package org.photonvision.vision.camera; package org.photonvision.vision.camera;
import com.fasterxml.jackson.annotation.JsonCreator; import io.avaje.jsonb.Json;
import com.fasterxml.jackson.annotation.JsonGetter; import io.avaje.jsonb.JsonType;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.avaje.jsonb.Jsonb;
import com.fasterxml.jackson.annotation.JsonProperty; import io.avaje.jsonb.Types;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import org.wpilib.vision.camera.UsbCameraInfo; import org.wpilib.vision.camera.UsbCameraInfo;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) @Json(typeProperty = "type")
@JsonIgnoreProperties(ignoreUnknown = true) @Json.SubType(type = PVCameraInfo.PVUsbCameraInfo.class)
@JsonSubTypes({ @Json.SubType(type = PVCameraInfo.PVCSICameraInfo.class)
@JsonSubTypes.Type(value = PVCameraInfo.PVUsbCameraInfo.class), @Json.SubType(type = PVCameraInfo.PVFileCameraInfo.class)
@JsonSubTypes.Type(value = PVCameraInfo.PVCSICameraInfo.class),
@JsonSubTypes.Type(value = PVCameraInfo.PVFileCameraInfo.class)
})
public sealed interface PVCameraInfo { public sealed interface PVCameraInfo {
/** /**
* @return The path of the camera. * @return The path of the camera.
*/ */
@Json.Property("path")
String path(); String path();
/** /**
* @return The base name of the camera aka the name as just ascii. * @return The base name of the camera aka the name as just ascii.
*/ */
@Json.Property("name")
String name(); String name();
/** /**
@@ -64,7 +62,7 @@ public sealed interface PVCameraInfo {
* *
* @return The unique path of the camera * @return The unique path of the camera
*/ */
@JsonGetter(value = "uniquePath") @Json.Property("uniquePath")
String uniquePath(); String uniquePath();
String[] otherPaths(); String[] otherPaths();
@@ -82,16 +80,9 @@ public sealed interface PVCameraInfo {
return this.equals((Object) other); return this.equals((Object) other);
} }
@JsonTypeName("PVUsbCameraInfo")
public static final class PVUsbCameraInfo extends UsbCameraInfo implements PVCameraInfo { public static final class PVUsbCameraInfo extends UsbCameraInfo implements PVCameraInfo {
@JsonCreator
public PVUsbCameraInfo( public PVUsbCameraInfo(
@JsonProperty("dev") int dev, int dev, String path, String name, String[] otherPaths, int vendorId, int productId) {
@JsonProperty("path") String path,
@JsonProperty("name") String name,
@JsonProperty("otherPaths") String[] otherPaths,
@JsonProperty("vendorId") int vendorId,
@JsonProperty("productId") int productId) {
super(dev, path, name, otherPaths, vendorId, 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 static final class PVCSICameraInfo implements PVCameraInfo {
public final String path; public final String path;
public final String baseName; public final String baseName;
@JsonCreator public PVCSICameraInfo(String path, String baseName) {
public PVCSICameraInfo(
@JsonProperty("path") String path, @JsonProperty("baseName") String baseName) {
this.path = path; this.path = path;
this.baseName = baseName; this.baseName = baseName;
} }
@@ -233,13 +221,11 @@ public sealed interface PVCameraInfo {
} }
} }
@JsonTypeName("PVFileCameraInfo")
public static final class PVFileCameraInfo implements PVCameraInfo { public static final class PVFileCameraInfo implements PVCameraInfo {
public final String path; public final String path;
public final String name; public final String name;
@JsonCreator public PVFileCameraInfo(String path, String name) {
public PVFileCameraInfo(@JsonProperty("path") String path, @JsonProperty("name") String name) {
this.path = path; this.path = path;
this.name = name; this.name = name;
} }
@@ -300,4 +286,25 @@ public sealed interface PVCameraInfo {
public static PVCameraInfo fromFileInfo(String path, String baseName) { public static PVCameraInfo fromFileInfo(String path, String baseName) {
return new PVFileCameraInfo(path, 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; package org.photonvision.vision.camera;
import com.fasterxml.jackson.annotation.JsonCreator; import io.avaje.jsonb.Json;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@Json
public class QuirkyCamera { public class QuirkyCamera {
private static final List<QuirkyCamera> quirkyCameras = private static final List<QuirkyCamera> quirkyCameras =
List.of( List.of(
@@ -98,19 +98,14 @@ public class QuirkyCamera {
CameraQuirk.Gain, CameraQuirk.Gain,
CameraQuirk.AwbRedBlueGain); // PiCam (using libcamera GPU Driver on raspberry pi) CameraQuirk.AwbRedBlueGain); // PiCam (using libcamera GPU Driver on raspberry pi)
@JsonProperty("baseName")
public final String baseName; public final String baseName;
@JsonProperty("usbVid")
public final int usbVid; public final int usbVid;
@JsonProperty("usbPid")
public final int usbPid; public final int usbPid;
@JsonProperty("displayName")
public final String displayName; public final String displayName;
@JsonProperty("quirks")
public final Map<CameraQuirk, Boolean> quirks; public final Map<CameraQuirk, Boolean> quirks;
/** /**
@@ -165,13 +160,12 @@ public class QuirkyCamera {
} }
} }
@JsonCreator
public QuirkyCamera( public QuirkyCamera(
@JsonProperty("baseName") String baseName, String baseName,
@JsonProperty("usbVid") int usbVid, int usbVid,
@JsonProperty("usbPid") int usbPid, int usbPid,
@JsonProperty("displayName") String displayName, String displayName,
@JsonProperty("quirks") Map<CameraQuirk, Boolean> quirks) { Map<CameraQuirk, Boolean> quirks) {
this.baseName = baseName; this.baseName = baseName;
this.usbPid = usbPid; this.usbPid = usbPid;
this.usbVid = usbVid; this.usbVid = usbVid;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,23 +17,25 @@
package org.photonvision.vision.pipeline; package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonSubTypes; import io.avaje.jsonb.Json;
import com.fasterxml.jackson.annotation.JsonTypeInfo; 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 java.util.Objects;
import org.photonvision.vision.frame.FrameDivisor; import org.photonvision.vision.frame.FrameDivisor;
import org.photonvision.vision.opencv.ImageRotationMode; import org.photonvision.vision.opencv.ImageRotationMode;
@JsonTypeInfo( @Json(typeProperty = "type")
use = JsonTypeInfo.Id.NAME, @Json.SubTypes({
include = JsonTypeInfo.As.WRAPPER_ARRAY, @Json.SubType(type = ColoredShapePipelineSettings.class),
property = "type") @Json.SubType(type = ReflectivePipelineSettings.class),
@JsonSubTypes({ @Json.SubType(type = DriverModePipelineSettings.class),
@JsonSubTypes.Type(value = ColoredShapePipelineSettings.class), @Json.SubType(type = AprilTagPipelineSettings.class),
@JsonSubTypes.Type(value = ReflectivePipelineSettings.class), @Json.SubType(type = ArucoPipelineSettings.class),
@JsonSubTypes.Type(value = DriverModePipelineSettings.class), @Json.SubType(type = ObjectDetectionPipelineSettings.class)
@JsonSubTypes.Type(value = AprilTagPipelineSettings.class),
@JsonSubTypes.Type(value = ArucoPipelineSettings.class),
@JsonSubTypes.Type(value = ObjectDetectionPipelineSettings.class)
}) })
public class CVPipelineSettings implements Cloneable { public class CVPipelineSettings implements Cloneable {
public int pipelineIndex = 0; public int pipelineIndex = 0;
@@ -153,4 +155,22 @@ public class CVPipelineSettings implements Cloneable {
+ outputShouldShow + 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; package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import java.util.Objects; import java.util.Objects;
import org.photonvision.common.util.numbers.DoubleCouple; import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.common.util.numbers.IntegerCouple; import org.photonvision.common.util.numbers.IntegerCouple;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients; import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.opencv.ContourShape; import org.photonvision.vision.opencv.ContourShape;
@JsonTypeName("ColoredShapePipelineSettings")
public class ColoredShapePipelineSettings extends AdvancedPipelineSettings { public class ColoredShapePipelineSettings extends AdvancedPipelineSettings {
public ContourShape contourShape = ContourShape.Triangle; public ContourShape contourShape = ContourShape.Triangle;
public DoubleCouple contourPerimeter = new DoubleCouple(0, Double.MAX_VALUE); public DoubleCouple contourPerimeter = new DoubleCouple(0, Double.MAX_VALUE);

View File

@@ -17,11 +17,11 @@
package org.photonvision.vision.pipeline; 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.common.util.numbers.DoubleCouple;
import org.photonvision.vision.processes.PipelineManager; import org.photonvision.vision.processes.PipelineManager;
@JsonTypeName("DriverModePipelineSettings") @Json
public class DriverModePipelineSettings extends CVPipelineSettings { public class DriverModePipelineSettings extends CVPipelineSettings {
public DoubleCouple offsetPoint = new DoubleCouple(); public DoubleCouple offsetPoint = new DoubleCouple();
public boolean crosshair = true; public boolean crosshair = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,10 +17,7 @@
package org.photonvision.vision.target; package org.photonvision.vision.target;
import com.fasterxml.jackson.annotation.JsonAlias; import io.avaje.jsonb.Json;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.opencv.core.MatOfPoint3f; 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. * <p>AprilTag models are currently only used for drawing on the output stream.
*/ */
@Json
public enum TargetModel implements Releasable { public enum TargetModel implements Releasable {
k2016HighGoal( k2016HighGoal(
List.of( List.of(
@@ -129,7 +127,8 @@ public enum TargetModel implements Releasable {
-Units.inchesToMeters(16.25) / 2)), -Units.inchesToMeters(16.25) / 2)),
0), 0),
// 2023 AprilTag, with 6 inch marker width (inner black square). // 2023 AprilTag, with 6 inch marker width (inner black square).
@JsonAlias({"k6in_16h5"}) // MIGRATION: 2023
@Json.Alias({"k6in_16h5"})
kAprilTag6in_16h5( kAprilTag6in_16h5(
// Corners of the tag's inner black square (excluding white border) // Corners of the tag's inner black square (excluding white border)
List.of( List.of(
@@ -139,7 +138,8 @@ public enum TargetModel implements Releasable {
new Point3(Units.inchesToMeters(3), -Units.inchesToMeters(3), 0)), new Point3(Units.inchesToMeters(3), -Units.inchesToMeters(3), 0)),
Units.inchesToMeters(3 * 2)), Units.inchesToMeters(3 * 2)),
// 2024 AprilTag, with 6.5 inch marker width (inner black square). // 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( kAprilTag6p5in_36h11(
// Corners of the tag's inner black square (excluding white border) // Corners of the tag's inner black square (excluding white border)
List.of( 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)), new Point3(-Units.inchesToMeters(6.5 / 2.0), -Units.inchesToMeters(6.5 / 2.0), 0)),
Units.inchesToMeters(6.5)); Units.inchesToMeters(6.5));
@JsonIgnore private MatOfPoint3f realWorldTargetCoordinates; @Json.Ignore private final MatOfPoint3f realWorldTargetCoordinates;
@JsonIgnore private final MatOfPoint3f visualizationBoxBottom = new MatOfPoint3f(); @Json.Ignore private final MatOfPoint3f visualizationBoxBottom = new MatOfPoint3f();
@JsonIgnore private final MatOfPoint3f visualizationBoxTop = new MatOfPoint3f(); @Json.Ignore private final MatOfPoint3f visualizationBoxTop = new MatOfPoint3f();
@JsonProperty("realWorldCoordinatesArray")
private List<Point3> realWorldCoordinatesArray; private List<Point3> realWorldCoordinatesArray;
@JsonProperty("boxHeight")
private double boxHeight; private double boxHeight;
TargetModel() {}
TargetModel(MatOfPoint3f realWorldTargetCoordinates, double boxHeight) { TargetModel(MatOfPoint3f realWorldTargetCoordinates, double boxHeight) {
this.realWorldTargetCoordinates = realWorldTargetCoordinates; this.realWorldTargetCoordinates = realWorldTargetCoordinates;
this.realWorldCoordinatesArray = realWorldTargetCoordinates.toList(); this.realWorldCoordinatesArray = realWorldTargetCoordinates.toList();
@@ -176,11 +171,8 @@ public enum TargetModel implements Releasable {
this.visualizationBoxTop.fromList(topList); this.visualizationBoxTop.fromList(topList);
} }
@JsonCreator TargetModel(List<Point3> realWorldCoordinatesArray, double boxHeight) {
TargetModel( this(listToMat(realWorldCoordinatesArray), boxHeight);
@JsonProperty(value = "realWorldCoordinatesArray") List<Point3> points,
@JsonProperty(value = "boxHeight") double boxHeight) {
this(listToMat(points), boxHeight);
} }
public List<Point3> getRealWorldCoordinatesArray() { public List<Point3> getRealWorldCoordinatesArray() {
@@ -227,6 +219,12 @@ public enum TargetModel implements Releasable {
// return new TargetModel(corners, 0); // return new TargetModel(corners, 0);
// } // }
@Json.Value
@Override
public String toString() {
return super.toString();
}
@Override @Override
public void release() { public void release() {
realWorldTargetCoordinates.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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import io.avaje.jsonb.Jsonb;
import java.io.File; import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; 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.LogGroup;
import org.photonvision.common.logging.LogLevel; import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger; import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.camera.PVCameraInfo; import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings; import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
import org.photonvision.vision.pipeline.CVPipelineSettings; import org.photonvision.vision.pipeline.CVPipelineSettings;
@@ -135,60 +134,31 @@ public class ConfigTest {
} }
@Test @Test
public void testJacksonHandlesOldVersions() throws IOException { public void testJsonbHandlesOldVersions() throws IOException {
var str = var json =
"{\"baseName\":\"aaaaaa\",\"uniqueName\":\"aaaaaa\",\"nickname\":\"aaaaaa\",\"FOV\":70.0,\"path\":\"dev/vid\",\"cameraType\":\"UsbCamera\",\"currentPipelineIndex\":0,\"camPitch\":{\"radians\":0.0},\"calibrations\":[], \"cameraLEDs\":[]}"; "{\"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 @Test
public void testJacksonAddUSBVIDPID() throws IOException { public void testJsonbAddUSBVIDPID() throws IOException {
var str = 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\":[]}"; "{\"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 = Jsonb.instance().type(CameraConfiguration.class).fromJson(json);
CameraConfiguration result = String ser = Jsonb.instance().toJson(result);
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class); System.out.println(ser);
String ser = JacksonUtils.serializeToString(result);
System.out.println(ser);
} catch (Exception e) {
e.printStackTrace();
}
tempFile.delete();
} }
@Test @Test
public void testJacksonHandlesOldTargetEnum() throws IOException { public void testJsonbHandlesOldTargetEnum() throws IOException {
var str = "[ \"AprilTagPipelineSettings\", {\n \"targetModel\" : \"k6in_16h5\"\n} ]\n"; var json = "[ \"AprilTagPipelineSettings\", {\n \"targetModel\" : \"k6in_16h5\"\n} ]\n";
File tempFile = File.createTempFile("test", ".json"); json = CVPipelineSettings.remapSettingsJson(json);
tempFile.deleteOnExit();
var writer = new FileWriter(tempFile);
writer.write(str);
writer.flush();
writer.close();
AprilTagPipelineSettings settings = AprilTagPipelineSettings settings =
(AprilTagPipelineSettings) (AprilTagPipelineSettings) Jsonb.instance().type(CVPipelineSettings.class).fromJson(json);
JacksonUtils.deserialize(tempFile.toPath(), CVPipelineSettings.class);
assertEquals(TargetModel.kAprilTag6in_16h5, settings.targetModel); 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.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -30,26 +33,30 @@ import org.photonvision.common.util.TestUtils;
public class NetworkConfigTest { public class NetworkConfigTest {
@Test @Test
public void testSerialization() throws IOException { public void testSerialization() throws IOException {
var mapper = new ObjectMapper();
var path = Path.of("netTest.json"); var path = Path.of("netTest.json");
mapper.writeValue(path.toFile(), new NetworkConfig()); JsonType<NetworkConfig> jsonb = Jsonb.instance().type(NetworkConfig.class);
assertDoesNotThrow(() -> mapper.readValue(path.toFile(), 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(); new File("netTest.json").delete();
} }
@Test @Test
public void testDeserializeTeamNumberOrNtServerAddress() { 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)); var configMgr = new ConfigManager(folder, new LegacyConfigProvider(folder));
configMgr.load(); configMgr.load();
assertEquals("9999", configMgr.getConfig().getNetworkConfig().ntServerAddress); 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)); var configMgr = new ConfigManager(folder, new LegacyConfigProvider(folder));
configMgr.load(); 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.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.nio.file.Path;
import java.util.LinkedList; import java.util.LinkedList;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family; import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version; import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties; import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
import org.photonvision.common.util.file.JacksonUtils;
public class NeuralNetworkPropertyManagerTest { public class NeuralNetworkPropertyManagerTest {
@Test @Test
@@ -42,10 +43,12 @@ public class NeuralNetworkPropertyManagerTest {
640, 640,
Family.RKNN, Family.RKNN,
Version.YOLOV8)); Version.YOLOV8));
String result = assertDoesNotThrow(() -> JacksonUtils.serializeToString(nnpm)); JsonType<NeuralNetworkModelsSettings> jsonb =
var deserializedNnpm = Jsonb.instance().type(NeuralNetworkModelsSettings.class);
assertDoesNotThrow( String result = assertDoesNotThrow(() -> jsonb.toJson(nnpm));
() -> JacksonUtils.deserialize(result, NeuralNetworkModelsSettings.class)); System.out.println(result);
var deserializedNnpm = assertDoesNotThrow(() -> jsonb.fromJson(result));
assertEquals(nnpm.getModels().length, deserializedNnpm.getModels().length);
assertEquals(nnpm.getModels()[0], deserializedNnpm.getModels()[0]); 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.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
@@ -32,8 +33,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import org.photonvision.common.LoadJNI; import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family; import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.util.TestUtils; import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.CameraQuirk;
import org.photonvision.vision.camera.PVCameraInfo; import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.pipeline.AdvancedPipelineSettings; import org.photonvision.vision.pipeline.AdvancedPipelineSettings;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings; import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
@@ -92,32 +93,6 @@ public class SQLConfigTest {
assertEquals(cfgLoader.getConfig().getNetworkConfig().ntServerAddress, "5940"); 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) { void common2025p3p1Assertions(PhotonConfiguration config) {
// Make sure we got 8 cameras // Make sure we got 8 cameras
assertEquals(8, config.getCameraConfigurations().size()); assertEquals(8, config.getCameraConfigurations().size());
@@ -134,7 +109,7 @@ public class SQLConfigTest {
} }
@Test @Test
public void testLoadNewNNMM() throws JsonProcessingException, IOException { public void testLoadNewNNMM() throws JsonDataException, IOException {
var folder = tmpDir.resolve("2025.3.1-old-nnmm"); var folder = tmpDir.resolve("2025.3.1-old-nnmm");
FileUtils.copyDirectory( FileUtils.copyDirectory(
TestUtils.getConfigDirectoriesPath(false).resolve("2025.3.1-old-nnmm").toFile(), TestUtils.getConfigDirectoriesPath(false).resolve("2025.3.1-old-nnmm").toFile(),
@@ -165,7 +140,7 @@ public class SQLConfigTest {
common2025p3p1Assertions(reloadedProvider.getConfig()); common2025p3p1Assertions(reloadedProvider.getConfig());
// And make sure NNPM has all 5 models // 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; ConfigManager.INSTANCE = null;
} }
@@ -210,4 +185,31 @@ public class SQLConfigTest {
ConfigManager.INSTANCE = null; 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 static org.junit.jupiter.api.Assertions.assertTrue;
import com.diozero.internal.spi.NativeDeviceFactoryInterface; 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.io.IOException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.HardwareConfig; import org.photonvision.common.configuration.HardwareConfig;
@@ -33,10 +34,9 @@ import org.photonvision.common.util.TestUtils;
public class HardwareConfigTest { public class HardwareConfigTest {
@Test @Test
public void loadJson() { public void loadJson() {
try { System.out.println("Loading Hardware configs...");
System.out.println("Loading Hardware configs..."); try (var stream = new FileInputStream(TestUtils.getHardwareConfigJson())) {
var config = var config = Jsonb.instance().type(HardwareConfig.class).fromJson(stream);
new ObjectMapper().readValue(TestUtils.getHardwareConfigJson(), HardwareConfig.class);
assertEquals(config.deviceName, "PhotonVision"); assertEquals(config.deviceName, "PhotonVision");
// Ensure defaults are not null // Ensure defaults are not null
assertArrayEquals(config.ledPins.stream().mapToInt(i -> i).toArray(), new int[] {2, 13}); 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.provider.builtin.DefaultDeviceFactory;
import com.diozero.internal.spi.NativeDeviceFactoryInterface; 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.io.IOException;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -68,8 +69,9 @@ public class HardwareTest {
@BeforeEach @BeforeEach
void setup() throws IOException { void setup() throws IOException {
System.out.println("Loading Hardware configs..."); System.out.println("Loading Hardware configs...");
hardwareConfig = try (var stream = new FileInputStream(TestUtils.getHardwareConfigJson())) {
new ObjectMapper().readValue(TestUtils.getHardwareConfigJson(), HardwareConfig.class); hardwareConfig = Jsonb.instance().type(HardwareConfig.class).fromJson(stream);
}
deviceFactory = HardwareManager.configureCustomGPIO(hardwareConfig); deviceFactory = HardwareManager.configureCustomGPIO(hardwareConfig);
} }

View File

@@ -128,10 +128,7 @@ public class CalibrationRotationPipeTest {
FrameStaticProperties frameProps = FrameStaticProperties frameProps =
new FrameStaticProperties( new FrameStaticProperties(
(int) coeffs.unrotatedImageSize.width, (int) coeffs.resolution.width, (int) coeffs.resolution.height, 66, coeffs);
(int) coeffs.unrotatedImageSize.height,
66,
coeffs);
FrameStaticProperties rotatedFrameProps = frameProps.rotate(rot); 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}; double[] rotatedCamMat = {fx, 0, res.width - cx, 0, fy, res.height - cy, 0, 0, 1};
assertArrayEquals(rotatedCamMat, coeffs2.cameraIntrinsics.data); assertArrayEquals(rotatedCamMat, coeffs2.cameraIntrinsics.data);
// AND the image size should be the same // AND the image size should be the same
assertEquals(res, coeffs2.unrotatedImageSize); assertEquals(res, coeffs2.resolution);
// WHEN the camera calibration is rotated 180 degrees // WHEN the camera calibration is rotated 180 degrees
var coeffs3 = coeffs2.rotateCoefficients(rot); var coeffs3 = coeffs2.rotateCoefficients(rot);
@@ -218,7 +215,7 @@ public class CalibrationRotationPipeTest {
// THEN the camera matrix will be the same as the original // THEN the camera matrix will be the same as the original
assertArrayEquals(intrinsics, coeffs3.cameraIntrinsics.data); assertArrayEquals(intrinsics, coeffs3.cameraIntrinsics.data);
// AND the image size should be the same // AND the image size should be the same
assertEquals(res, coeffs2.unrotatedImageSize); assertEquals(res, coeffs2.resolution);
} }
@CartesianTest @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.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -115,9 +115,9 @@ public class VisionModuleManagerTest {
} }
@Override @Override
public HashMap<Integer, VideoMode> getAllVideoModes() { public List<VideoMode> getAllVideoModes() {
var ret = new HashMap<Integer, VideoMode>(); var ret = new ArrayList<VideoMode>();
ret.put(0, getCurrentVideoMode()); ret.add(getCurrentVideoMode());
return ret; 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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import io.avaje.jsonb.Jsonb;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -31,8 +32,8 @@ import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI; import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.CameraConfiguration; import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager; import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.PhotonConfiguration;
import org.photonvision.common.util.TestUtils; import org.photonvision.common.util.TestUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.camera.PVCameraInfo; import org.photonvision.vision.camera.PVCameraInfo;
import org.wpilib.vision.camera.UsbCameraInfo; import org.wpilib.vision.camera.UsbCameraInfo;
@@ -94,17 +95,18 @@ public class VisionSourceManagerTest {
7, 7,
8)); 8));
var str = JacksonUtils.serializeToString(usb); var str = Jsonb.instance().type(PVCameraInfo.class).toJson(usb);
System.out.println(str); System.out.println(str);
System.out.println(JacksonUtils.deserialize(str, PVCameraInfo.class)); System.out.println(Jsonb.instance().type(PVCameraInfo.class).fromJson(str));
} }
{ {
var csi = var csi =
PVCameraInfo.fromCSICameraInfo( PVCameraInfo.fromCSICameraInfo(
"/dev/v4l/by-path/platform-1f00110000.csi-video-index0", "rp1-cfe"); "/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(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); 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 // And make assertions about the current matching state
assertEquals(1, vsm.getVsmState().allConnectedCameras.size()); assertEquals(1, vsm.getVsmState().allConnectedCameras.size());
@@ -268,7 +273,10 @@ public class VisionSourceManagerTest {
vsm.assignUnmatchedCamera(fileCamera3); 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 // And make assertions about the current matching state
assertEquals(3, vsm.getVsmState().allConnectedCameras.size()); assertEquals(3, vsm.getVsmState().allConnectedCameras.size());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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