mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-27 02:01:40 +00:00
Bootup sprint (#18)
* Did some stuff * Fix gradle, start implementing mjpeg frame consumer * Did some stuff * bade changes * rename camera config to USBCameraConfiguration, add name * unrename cameraconfiguration * Add pub/sub framework * Add setResolution to mjpeg frame consumer * add NTDataConsumer * Add some totally broken hsv hacks * Start refactoring UI data * Update index.js * Commit and push, he says * Fix up some errors * Fix input tab * Fix fps * Update index.js * Add pipeline field setting, update PipelineManager, fix nullpointers and USBCameraSettables * Change v-model to point to data() * update hsv to use mutations * Work on saving, fix hsv * Rename shouldErode/shouldDilate to erode and dilate * Hook all the tabs up to the Store * Change handleData to handlePipelineData * camera quirk redo, add ICCSub to SocketHandler * Fix some property names * Fixed tons of naming in UI, fix backend for multi-val PSCs, fix PSC enums * change pipeline type to an int in store * Fix mutation naming * Attempt threshold fix * Update SocketHandler.java * Add truthy data sending * Start adding logging support * [UI] Add delay to slider input boxes (#1) * [UI] [Backend] potentially fix camera settings, various logging tweaks * Don't release raw input mat * add setVideoModeIndex to vision settables * Implement pipeline index in socket handler, add framework for renaming/changing pipes * (ish) get pipeline change working * Create index.html * Cleanups, fix pipeline index bug, fix stream res for MJPG, add dashboard stream (unused) * Refactor UI to use mutatePipeline, send pipeline results * Update NetworkConfig.java * Change double to number * Run spotless * Fix reversal of large/small comparators * Fix left/right * Fix pitch/yaw calculation bug, fix area bug * Use Vue.set instead of assignment This fixes {{ }} * Update App.vue * run spotless * Actually add pipelines and reassign indecies * Delete old pipeline configs Fixes duplication on renaming pipeline * Start working on deleting pipes * Fix camera nickname change * run spotless * Fix some test stuff * Update VisionModuleManagerTest.java * vision source manager test is still broken * Fix VisionSourceManager test * Apply spotless 2 electric boogaloo Co-authored-by: Banks Troutman <btrout.dhrs@gmail.com> Co-authored-by: Declan Freeman-Gleason <declanfreemangleason@gmail.com> Co-authored-by: Aaryan Agrawal <54345060+13Ducks@users.noreply.github.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -123,3 +123,4 @@ New client/photon-client/*
|
||||
# *.iml
|
||||
photon-server/build
|
||||
photon-server/photon-vision
|
||||
photon-server/src/main/resources/web
|
||||
|
||||
@@ -118,14 +118,19 @@
|
||||
},
|
||||
methods: {
|
||||
handleMessage(key, value) {
|
||||
if (this.$store.state.hasOwnProperty(key)) {
|
||||
if (key === "logMessage") {
|
||||
console.log(value)
|
||||
this.logMessage(value, 0)
|
||||
} else if (key === "updatePipelineResult") {
|
||||
this.$store.commit('mutatePipelineResults', value)
|
||||
} else if (this.$store.state.hasOwnProperty(key)) {
|
||||
this.$store.commit(key, value);
|
||||
} else if (this.$store.state.pipeline.hasOwnProperty(key)) {
|
||||
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutatePipeline', {'key': key, 'value': value});
|
||||
} else {
|
||||
switch (key) {
|
||||
default: {
|
||||
console.log(key + " : " + value);
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +146,7 @@
|
||||
}
|
||||
this.timer = setInterval(this.saveSettings, 4000);
|
||||
},
|
||||
logMessage({message, level}) {
|
||||
logMessage(message, level) {
|
||||
const colors = ["\u001b[31m", "\u001b[32m", "\u001b[33m", "\u001b[34m"]
|
||||
const reset = "\u001b[0m"
|
||||
this.log += `${colors[level]}${message}${reset}\n`
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Icon',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['color', 'tooltip', 'text', 'right', 'hover'],
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: "CvImage",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['address', 'scale'],
|
||||
data: () => {
|
||||
return {}
|
||||
|
||||
@@ -24,6 +24,7 @@ s
|
||||
<script>
|
||||
export default {
|
||||
name: 'Input',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'disabled', 'errorMessage'],
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'NumberInput',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'step'],
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Radio',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value', 'list'],
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="2">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
@@ -62,46 +59,65 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RangeSlider',
|
||||
props: ['name', 'min', 'max', 'value', 'step'],
|
||||
data() {
|
||||
return {
|
||||
prependFocused: false,
|
||||
appendFocused: false
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(val) {
|
||||
let i = 0;
|
||||
if (this.prependFocused === false && this.appendFocused === true) {
|
||||
i = 1;
|
||||
}
|
||||
if (this.prependFocused || this.appendFocused) {
|
||||
this.$set(this.localValue, i, val);
|
||||
this.$emit('rollback', this.localValue)
|
||||
}
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.prependFocused || !this.appendFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: "RangeSlider",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ["name", "min", "max", "value", "step"],
|
||||
data() {
|
||||
return {
|
||||
prependFocused: false,
|
||||
appendFocused: false,
|
||||
currentBoxVals: [null, null]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return Object.values(this.value);
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("input", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// handleChange(val) {
|
||||
// let i = 0;
|
||||
// if (this.prependFocused === false && this.appendFocused === true) {
|
||||
// i = 1;
|
||||
// }
|
||||
// if (this.prependFocused || this.appendFocused) {
|
||||
// this.$set(this.localValue, i, val);
|
||||
// this.$emit('rollback', this.localValue)
|
||||
// }
|
||||
// },
|
||||
handleChange(val) {
|
||||
let i = 0;
|
||||
if (this.prependFocused === false && this.appendFocused === true) {
|
||||
i = 1;
|
||||
}
|
||||
|
||||
this.currentBoxVals[i] = val;
|
||||
setTimeout(() => {
|
||||
if (this.currentBoxVals[i] !== val) return;
|
||||
//if (this.prependFocused || this.appendFocused) {
|
||||
let tmp = this.localValue;
|
||||
let parsed = parseFloat(val);
|
||||
if (isNaN(parsed)) return;
|
||||
tmp[i] = parsed;
|
||||
this.localValue = tmp;
|
||||
this.$emit("rollback", this.localValue);
|
||||
//}
|
||||
}, 200);
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.prependFocused || !this.appendFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
@@ -27,6 +27,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Select',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['list', 'name', 'value', 'disabled'],
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="2">
|
||||
<span>{{ name }}</span>
|
||||
</v-col>
|
||||
@@ -19,7 +16,7 @@
|
||||
:step="step"
|
||||
@start="isClicked = true"
|
||||
@end="isClicked = false"
|
||||
@change="handleclick"
|
||||
@change="handleClick"
|
||||
@input="handleInput"
|
||||
@mousedown="$emit('rollback', localValue)"
|
||||
>
|
||||
@@ -47,46 +44,51 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Slider',
|
||||
props: ['min', 'max', 'name', 'value', 'step'],
|
||||
data() {
|
||||
return {
|
||||
isFocused: false,
|
||||
isClicked: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(val) {
|
||||
if (this.isFocused) {
|
||||
this.localValue = parseFloat(val);
|
||||
this.$emit('rollback', this.localValue)
|
||||
}
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.isFocused && this.isClicked) {
|
||||
this.localValue = val;
|
||||
}
|
||||
},
|
||||
handleclick(val) {
|
||||
if (!this.isFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: "Slider",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ["min", "max", "name", "value", "step"],
|
||||
data() {
|
||||
return {
|
||||
isFocused: false,
|
||||
isClicked: false,
|
||||
currentBoxVal: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("input", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(val) {
|
||||
this.currentBoxVal = val;
|
||||
setTimeout(() => {
|
||||
if (this.currentBoxVal !== val) return;
|
||||
// if (this.isFocused) {
|
||||
this.localValue = parseFloat(val);
|
||||
this.$emit("rollback", this.localValue);
|
||||
// }
|
||||
}, 200);
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.isFocused && this.isClicked) {
|
||||
this.localValue = val;
|
||||
}
|
||||
},
|
||||
handleClick(val) {
|
||||
if (!this.isFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
@@ -23,6 +23,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'CVSwitch',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'disabled'],
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
export default {
|
||||
name: "MiniMap",
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
targets: Array,
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
horizontalFOV: Number
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
v-model="currentPipelineIndex"
|
||||
name="Pipeline"
|
||||
:list="['Driver Mode'].concat($store.getters.pipelineList)"
|
||||
@input="handleInput('currentPipeline',currentPipelineIndex - 1)"
|
||||
@input="handleInputWithIndex('currentPipeline',currentPipelineIndex - 1)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
@@ -265,7 +265,7 @@
|
||||
for (let cam in this.cameraList) {
|
||||
if (this.cameraList.hasOwnProperty(cam)) {
|
||||
if (this.newCameraName === this.cameraList[cam]) {
|
||||
return "Camera by that name already Exists"
|
||||
return "Camera by that name already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +296,7 @@
|
||||
return this.$store.getters.currentCameraIndex;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('currentPipelineIndex', value - 1);
|
||||
this.$store.commit('currentCameraIndex', value);
|
||||
}
|
||||
},
|
||||
currentPipelineIndex: {
|
||||
@@ -315,7 +315,7 @@
|
||||
},
|
||||
saveCameraNameChange() {
|
||||
if (this.checkCameraName === "") {
|
||||
this.handleInput("changeCameraName", this.newCameraName);
|
||||
this.handleInputWithIndex("changeCameraName", this.newCameraName);
|
||||
this.discardCameraNameChange();
|
||||
}
|
||||
},
|
||||
@@ -342,7 +342,7 @@
|
||||
},
|
||||
deleteCurrentPipeline() {
|
||||
if (this.$store.getters.pipelineList.length > 1) {
|
||||
this.handleInput('command', 'deleteCurrentPipeline');
|
||||
this.handleInputWithIndex('command', 'deleteCurrentPipeline');
|
||||
} else {
|
||||
this.snackbar = true;
|
||||
}
|
||||
@@ -350,9 +350,9 @@
|
||||
savePipelineNameChange() {
|
||||
if (this.checkPipelineName === "") {
|
||||
if (this.isPipelineNameEdit) {
|
||||
this.handleInput("changePipelineName", this.newPipelineName);
|
||||
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
|
||||
} else {
|
||||
this.handleInput("addNewPipeline", this.newPipelineName);
|
||||
this.handleInputWithIndex("addNewPipeline", this.newPipelineName);
|
||||
}
|
||||
this.discardPipelineNameChange();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: "DualCalibration",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['rawPoint'],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: "SingleCalibration",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['rawPoint'],
|
||||
methods: {
|
||||
clearPoint() {
|
||||
|
||||
@@ -31,6 +31,7 @@ Vue.prototype.$msgPack = msgPack(true);
|
||||
import {dataHandleMixin} from './mixins/global/dataHandleMixin'
|
||||
|
||||
Vue.mixin(dataHandleMixin);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
|
||||
@@ -4,8 +4,35 @@ export const dataHandleMixin = {
|
||||
let msg = this.$msgPack.encode({[key]: value});
|
||||
this.$socket.send(msg);
|
||||
},
|
||||
handleInputWithIndex(key, value) {
|
||||
let msg = this.$msgPack.encode({
|
||||
[key]: value,
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
},
|
||||
handleData(val) {
|
||||
this.handleInput(val, this.value[val]);
|
||||
this.handleInput(val, this[val]);
|
||||
this.$emit('update')
|
||||
},
|
||||
handlePipelineData(val) {
|
||||
let msg = this.$msgPack.encode({
|
||||
["changePipelineSetting"]: {
|
||||
[val]: this[val],
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
handleTruthyPipelineData(val) {
|
||||
let msg = this.$msgPack.encode({
|
||||
["changePipelineSetting"]: {
|
||||
[val]: !!(this[val]),
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
rollback(val, e) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import pipeline from "./modules/pipeline";
|
||||
import generalSettings from "./modules/generalSettings";
|
||||
import cameraSettings from "./modules/cameraSettings";
|
||||
import networkSettings from "./modules/networkSettings"
|
||||
import undoRedo from "./modules/undoRedo";
|
||||
|
||||
Vue.use(Vuex);
|
||||
@@ -14,53 +12,137 @@ const set = key => (state, val) => {
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
pipeline: pipeline,
|
||||
settings: generalSettings,
|
||||
cameraSettings: cameraSettings,
|
||||
reflectivePipelineSettings: {
|
||||
state: {
|
||||
currentResolutionIndex: 0,
|
||||
},
|
||||
},
|
||||
networkSettings: networkSettings,
|
||||
undoRedo: undoRedo
|
||||
},
|
||||
state: {
|
||||
resolutionList: [],
|
||||
port: 1181,
|
||||
currentCameraIndex: 0,
|
||||
currentPipelineIndex: 0,
|
||||
cameraList: [],
|
||||
pipelineList: [],
|
||||
point: {},
|
||||
saveBar: false
|
||||
saveBar: false,
|
||||
cameraSettings: [ // This is a list of objects representing the settings of all cameras
|
||||
{
|
||||
tiltDegrees: 0.0,
|
||||
currentPipelineIndex: 0,
|
||||
pipelineNicknames: ["Unknown"],
|
||||
streamPort: 1181,
|
||||
nickname: "Unknown",
|
||||
videoFormatList: [
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 30,
|
||||
"pixelFormat": "BGR"
|
||||
}
|
||||
],
|
||||
fov: 70.0,
|
||||
calibrated: false,
|
||||
currentPipelineSettings: {
|
||||
pipelineType: 2, // One of "driver", "reflective", "shape"
|
||||
// 2 is reflective
|
||||
|
||||
// Settings that apply to all pipeline types
|
||||
cameraExposure: 1,
|
||||
cameraBrightness: 2,
|
||||
cameraGain: 3,
|
||||
inputImageRotationMode: 0,
|
||||
cameraVideoModeIndex: 0,
|
||||
outputFrameDivisor: 0,
|
||||
|
||||
// Settings that apply to reflective
|
||||
hsvHue: [0, 15],
|
||||
hsvSaturation: [0, 15],
|
||||
hsvValue: [0, 25],
|
||||
erode: false,
|
||||
dilate: false,
|
||||
contourArea: [0, 12],
|
||||
contourRatio: [0, 12],
|
||||
contourExtent: [0, 12],
|
||||
contourSpecklePercentage: 5,
|
||||
contourGroupingMode: 0,
|
||||
contourIntersection: 0,
|
||||
contourSortMode: 0,
|
||||
outputShowMultipleTargets: false,
|
||||
outputShowThresholded: 0,
|
||||
offsetRobotOffsetMode: 0,
|
||||
solvePNPEnabled: false,
|
||||
targetRegion: 0,
|
||||
contourTargetOrientation: 1
|
||||
|
||||
// Settings that apply to shape
|
||||
}
|
||||
}
|
||||
],
|
||||
pipelineResults: [
|
||||
{
|
||||
fps: 0,
|
||||
targets: [{
|
||||
// Available in both 2D and 3D
|
||||
pitch: 0,
|
||||
yaw: 0,
|
||||
skew: 0,
|
||||
area: 0,
|
||||
// 3D only
|
||||
pose: {x: 0, y: 0, rot: 0},
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
mutations: {
|
||||
settings: set('settings'),
|
||||
pipeline: set('pipeline'),
|
||||
cameraSettings: set('cameraSettings'),
|
||||
resolutionList: set('resolutionList'),
|
||||
port: set('port'),
|
||||
saveBar: set('saveBar'),
|
||||
currentCameraIndex: set('currentCameraIndex'),
|
||||
currentPipelineIndex: set('currentPipelineIndex'),
|
||||
cameraList: set('cameraList'),
|
||||
pipelineList: set('pipelineList'),
|
||||
point: set('point'),
|
||||
driverMode: set('driverMode'),
|
||||
saveBar: set("saveBar")
|
||||
pipelineResults: set('pipelineResults'),
|
||||
networkSettings: set('networkSettings'),
|
||||
|
||||
currentPipelineIndex: (state, val) => {
|
||||
const settings = state.cameraSettings[state.currentCameraIndex];
|
||||
Vue.set(settings, 'currentPipelineIndex', val);
|
||||
},
|
||||
|
||||
// TODO change everything to use this
|
||||
mutatePipeline: (state, payload) => {
|
||||
for (let key in payload) {
|
||||
if (!payload.hasOwnProperty(key)) continue;
|
||||
const value = payload[key];
|
||||
const settings = state.cameraSettings[state.currentCameraIndex].currentPipelineSettings;
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
Vue.set(settings, key, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mutatePipelineResults(state, payload) {
|
||||
// Key: index, value: result
|
||||
let newResultArray = [];
|
||||
for (let key in payload) {
|
||||
if (!payload.hasOwnProperty(key)) continue;
|
||||
const index = parseInt(key);
|
||||
newResultArray[index] = payload[key];
|
||||
}
|
||||
|
||||
Vue.set(state, 'pipelineResults', newResultArray)
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
streamAddress: state => {
|
||||
return "http://" + location.hostname + ":" + state.port + "/stream.mjpg";
|
||||
pipelineSettings: state => state.pipelineSettings,
|
||||
streamAddress: state =>
|
||||
"http://" + location.hostname + ":" + state.cameraSettings[state.currentCameraIndex].streamPort + "/stream.mjpg",
|
||||
targets: state => state.pipelineResults.length,
|
||||
currentPipelineResults: state =>
|
||||
state.pipelineResults[state.cameraSettings[state.currentCameraIndex].currentPipelineIndex],
|
||||
cameraList: state => state.cameraSettings.map(it => it.nickname),
|
||||
currentCameraSettings: state => state.cameraSettings[state.currentCameraIndex],
|
||||
currentCameraIndex: state => state.currentCameraIndex,
|
||||
currentPipelineIndex: state => state.cameraSettings[state.currentCameraIndex].currentPipelineIndex,
|
||||
currentPipelineSettings: state => state.cameraSettings[state.currentCameraIndex].currentPipelineSettings,
|
||||
videoFormatList: state => {
|
||||
return Object.values(state.cameraSettings[state.currentCameraIndex].videoFormatList); // convert to a list
|
||||
},
|
||||
targets: state => {
|
||||
return state.point['targets']
|
||||
},
|
||||
cameraList: state => {
|
||||
return state.cameraList
|
||||
},
|
||||
pipelineList: state => {
|
||||
return state.pipelineList
|
||||
},
|
||||
currentCameraIndex: state => {
|
||||
return state.currentCameraIndex
|
||||
},
|
||||
currentPipelineIndex: state => {
|
||||
return state.currentPipelineIndex
|
||||
}
|
||||
pipelineList: state => state.cameraSettings[state.currentCameraIndex].pipelineNicknames,
|
||||
currentCameraFPS: state => state.pipelineResults[state.currentCameraIndex].fps
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
export default {
|
||||
state: {
|
||||
calibration: [],
|
||||
fov: 0,
|
||||
resolution: 0,
|
||||
streamDivisor: 0,
|
||||
tilt: 0
|
||||
},
|
||||
getters: {
|
||||
cameraSettings: state => {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export default {
|
||||
state:{
|
||||
teamNumber: 0,
|
||||
connectionType: 0,
|
||||
ip: "",
|
||||
gateway: "",
|
||||
netmask: "",
|
||||
hostname: "photon-vision"
|
||||
}
|
||||
}
|
||||
17
photon-client/src/store/modules/networkSettings.js
Normal file
17
photon-client/src/store/modules/networkSettings.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default {
|
||||
state: {
|
||||
netmask: "",
|
||||
ip: "",
|
||||
teamNumber: "",
|
||||
connectionType: "",
|
||||
gateway: ""
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
actions: {},
|
||||
getters: {
|
||||
pipeline: state => {
|
||||
return state
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -52,14 +52,14 @@
|
||||
<!-- camera image tabs -->
|
||||
<v-tabs
|
||||
v-if="($store.getters.currentPipelineIndex + 1) !== 0"
|
||||
v-model="isBinaryNumber"
|
||||
v-model="outputShowThresholded"
|
||||
background-color="#232c37"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="#ffd843"
|
||||
centered
|
||||
style="padding-bottom:10px"
|
||||
@change="handleInput('isBinary',$store.getters.pipeline.isBinary)"
|
||||
@change="handleTruthyPipelineData('outputShowThresholded')"
|
||||
>
|
||||
<v-tab>Normal</v-tab>
|
||||
<v-tab>Threshold</v-tab>
|
||||
@@ -106,7 +106,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, index) in $store.getters.targets"
|
||||
v-for="(value, index) in $store.getters.currentPipelineResults.targets"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ index }}</td>
|
||||
@@ -165,16 +165,15 @@
|
||||
return {
|
||||
selectedTab: 0,
|
||||
snackbar: false,
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isBinaryNumber: {
|
||||
outputShowThresholded: {
|
||||
get() {
|
||||
return this.$store.getters.pipeline.isBinary ? 1 : 0
|
||||
return this.$store.getters.currentPipelineSettings.outputShowThresholded ? 1 : 0;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('isBinary', !!value);
|
||||
this.$store.commit('mutatePipeline', {'outputShowThresholded': !!value});
|
||||
}
|
||||
},
|
||||
selectedComponent: {
|
||||
@@ -184,7 +183,7 @@
|
||||
},
|
||||
fps: {
|
||||
get() {
|
||||
return this.$store.state.point.fps;
|
||||
return this.$store.getters.currentCameraFPS;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
CVslider,
|
||||
miniMap
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
@@ -106,14 +107,14 @@
|
||||
computed: {
|
||||
targets: {
|
||||
get() {
|
||||
return this.$store.state.point.targets;
|
||||
return 330; // TODO fix
|
||||
}
|
||||
},
|
||||
horizontalFOV: {
|
||||
get() {
|
||||
let index = this.$store.state.cameraSettings.resolution;
|
||||
let FOV = this.$store.state.cameraSettings.fov;
|
||||
let resolution = this.$store.state.resolutionList[index];
|
||||
let resolution = this.$store.getters.videoFormatList[index];
|
||||
let diagonalView = FOV * (Math.PI / 180);
|
||||
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
|
||||
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
|
||||
@@ -121,8 +122,9 @@
|
||||
},
|
||||
allow3D: {
|
||||
get() {
|
||||
let currentRes = this.$store.state.resolutionList[this.$store.state.pipeline.videoModeIndex];
|
||||
for (let res of this.$store.state.cameraSettings.calibration) {
|
||||
let index = this.$store.state.cameraSettings.resolution;
|
||||
let currentRes = this.$store.getters.videoFormatList[index];
|
||||
for (let res of this.$store.state.cameraSettings.calibrated) {
|
||||
if (currentRes.width === res.width && currentRes.height === res.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="value.area"
|
||||
name="Area"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
@input="handleData('area')"
|
||||
@rollback="e=> rollback('area',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="value.ratio"
|
||||
name="Ratio (W/H)"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
@input="handleData('ratio')"
|
||||
@rollback="e=> rollback('ratio',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="value.extent"
|
||||
name="Extent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('extent')"
|
||||
@rollback="e=> rollback('extent',e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="value.speckle"
|
||||
name="Speckle Rejection"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('speckle')"
|
||||
@rollback="e=> rollback('speckle',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.targetGroup"
|
||||
name="Target Group"
|
||||
:list="['Single','Dual']"
|
||||
@input="handleData('targetGroup')"
|
||||
@rollback="e=> rollback('targetGroup',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.targetIntersection"
|
||||
name="Target Intersection"
|
||||
:list="['None','Up','Down','Left','Right']"
|
||||
:disabled="isDisabled"
|
||||
@input="handleData('targetIntersection')"
|
||||
@rollback="e=> rollback('targetIntersection',e)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="contourArea"
|
||||
name="Area"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
@input="handlePipelineData('contourArea')"
|
||||
@rollback="e=> rollback('contourArea',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="contourRatio"
|
||||
name="Ratio (W/H)"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
@input="handlePipelineData('contourRatio')"
|
||||
@rollback="e=> rollback('contourRatio',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="contourExtent"
|
||||
name="Extent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handlePipelineData('contourExtent')"
|
||||
@rollback="e=> rollback('contourExtent',e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="contourSpecklePercentage"
|
||||
name="Speckle Rejection"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handlePipelineData('contourSpecklePercentage')"
|
||||
@rollback="e=> rollback('contourSpecklePercentage',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="contourGroupingMode"
|
||||
name="Target Group"
|
||||
:list="['Single','Dual']"
|
||||
@input="handlePipelineData('targetGroup')"
|
||||
@rollback="e=> rollback('targetGroup',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="contourIntersection"
|
||||
name="Target Intersection"
|
||||
:list="['None','Up','Down','Left','Right']"
|
||||
:disabled="contourGroupingMode === 0"
|
||||
@input="handlePipelineData('contourIntersection')"
|
||||
@rollback="e=> rollback('contourIntersection',e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -64,15 +64,60 @@
|
||||
CVselect,
|
||||
CVslider
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
isDisabled() {
|
||||
return this.value.targetGroup === 0;
|
||||
|
||||
contourArea: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourArea
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourArea": val});
|
||||
}
|
||||
},
|
||||
contourRatio: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourRatio
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourRatio": val});
|
||||
}
|
||||
},
|
||||
contourExtent: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourExtent
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourExtent": val});
|
||||
}
|
||||
},
|
||||
contourSpecklePercentage: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourSpecklePercentage
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
|
||||
}
|
||||
},
|
||||
contourGroupingMode: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourGroupingMode
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourGroupingMode": val});
|
||||
}
|
||||
},
|
||||
contourIntersection: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourIntersection
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourIntersection": val});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVslider
|
||||
v-model="value.exposure"
|
||||
v-model="cameraExposure"
|
||||
name="Exposure"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('exposure')"
|
||||
@rollback="e => rollback('exposure', e)"
|
||||
@input="handlePipelineData('cameraExposure')"
|
||||
@rollback="e => rollback('cameraExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="value.brightness"
|
||||
v-model="cameraBrightness"
|
||||
name="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('brightness')"
|
||||
@rollback="e => rollback('brightness', e)"
|
||||
@input="handlePipelineData('cameraBrightness')"
|
||||
@rollback="e => rollback('cameraBrightness', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="value.gain !== -1"
|
||||
v-model="value.gain"
|
||||
v-if="cameraGain !== -1"
|
||||
v-model="cameraGain"
|
||||
name="Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="handleData('gain')"
|
||||
@rollback="e => rollback('gain', e)"
|
||||
@input="handlePipelineData('cameraGain')"
|
||||
@rollback="e => rollback('cameraGain', e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.rotationMode"
|
||||
v-model="inputImageRotationMode"
|
||||
name="Orientation"
|
||||
:list="['Normal','90° CW','180°','90° CCW']"
|
||||
@input="handleData('rotationMode')"
|
||||
@rollback="e => e => rollback('rotationMode',e)"
|
||||
@input="handlePipelineData('inputImageRotationMode')"
|
||||
@rollback="e => rollback('inputImageRotationMode',e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.videoModeIndex"
|
||||
v-model="cameraVideoModeIndex"
|
||||
name="Resolution"
|
||||
:list="resolutionList"
|
||||
@input="handleData('videoModeIndex')"
|
||||
@rollback="e => rollback('videoModeIndex', e)"
|
||||
@input="handlePipelineData('cameraVideoModeIndex')"
|
||||
@rollback="e => rollback('cameraVideoModeIndex', e)"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.streamDivisor"
|
||||
v-model="outputFrameDivisor"
|
||||
name="Stream Resolution"
|
||||
:list="streamResolutionList"
|
||||
@input="handleData('streamDivisor')"
|
||||
@rollback="e => rollback('streamDivisor', e)"
|
||||
@input="handlePipelineData('outputFrameDivisor')"
|
||||
@rollback="e => rollback('outputFrameDivisor', e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,24 +59,75 @@
|
||||
CVslider,
|
||||
CVselect,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
cameraExposure: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraExposure);
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraExposure": parseInt(val)});
|
||||
}
|
||||
},
|
||||
cameraBrightness: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraBrightness)
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraBrightness": parseInt(val)});
|
||||
}
|
||||
},
|
||||
cameraGain: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
|
||||
}
|
||||
},
|
||||
inputImageRotationMode: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.inputImageRotationMode
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"inputImageRotationMode": val});
|
||||
}
|
||||
},
|
||||
cameraVideoModeIndex: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.cameraVideoModeIndex
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
|
||||
}
|
||||
},
|
||||
outputFrameDivisor: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.outputFrameDivisor
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"outputFrameDivisor": val});
|
||||
}
|
||||
},
|
||||
|
||||
resolutionList: {
|
||||
get() {
|
||||
let tmp_list = [];
|
||||
for (let i of this.$store.state.resolutionList) {
|
||||
for (let i of this.$store.getters.videoFormatList) {
|
||||
tmp_list.push(`${i['width']} X ${i['height']} at ${i['fps']} FPS, ${i['pixelFormat']}`)
|
||||
}
|
||||
return tmp_list;
|
||||
}
|
||||
},
|
||||
|
||||
streamResolutionList: {
|
||||
get() {
|
||||
let cam_res = this.$store.state.resolutionList[this.value.videoModeIndex];
|
||||
let cam_res = this.$store.getters.videoFormatList[
|
||||
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex]
|
||||
let tmp_list = [];
|
||||
tmp_list.push(`${Math.floor(cam_res['width'])} X ${Math.floor(cam_res['height'])}`);
|
||||
for (let x = 2; x <= 6; x += 2) {
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVselect
|
||||
v-model="value.sortMode"
|
||||
v-model="contourSortMode"
|
||||
name="Sort Mode"
|
||||
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
|
||||
@input="handleData('sortMode')"
|
||||
@rollback="rollback('sortMode',e)"
|
||||
@input="handlePipelineData('contourSortMode')"
|
||||
@rollback="e => rollback('contourSortMode', e)"
|
||||
/>
|
||||
|
||||
<CVselect
|
||||
v-model="value.targetRegion"
|
||||
name="Target Region"
|
||||
v-model="contourTargetOffsetPointEdge"
|
||||
name="Target Offset Point"
|
||||
:list="['Center','Top','Bottom','Left','Right']"
|
||||
@input="handleData('targetRegion')"
|
||||
@rollback="e=> rollback('targetRegion',e)"
|
||||
@input="handlePipelineData('contourTargetOffsetPointEdge')"
|
||||
@rollback="e=> rollback('contourTargetOffsetPointEdge', e)"
|
||||
/>
|
||||
|
||||
<CVselect
|
||||
v-model="value.targetOrientation"
|
||||
v-model="contourTargetOrientation"
|
||||
name="Target Orientation"
|
||||
:list="['Portrait', 'Landscape']"
|
||||
@input="handleData('targetOrientation')"
|
||||
@rollback="e=> rollback('targetOrientation',e)"
|
||||
@input="handlePipelineData('contourTargetOrientation')"
|
||||
@rollback="e=> rollback('contourTargetOrientation', e)"
|
||||
/>
|
||||
|
||||
<CVswitch
|
||||
v-model="value.multiple"
|
||||
name="Output multiple"
|
||||
@input="handleData('multiple')"
|
||||
@rollback="e=> rollback('multiple',e)"
|
||||
v-model="outputShowMultipleTargets"
|
||||
name="Show Multiple Targets"
|
||||
@input="handlePipelineData('outputShowMultipleTargets')"
|
||||
@rollback="e=> rollback('outputShowMultipleTargets', e)"
|
||||
/>
|
||||
<span>Calibrate:</span>
|
||||
<span>Robot Offset:</span>
|
||||
<v-divider
|
||||
dark
|
||||
color="white"
|
||||
/>
|
||||
<CVselect
|
||||
v-model="value.calibrationMode"
|
||||
name="Calibration Mode"
|
||||
:list="['None','Single point','Dual point']"
|
||||
@input="handleData('calibrationMode')"
|
||||
@rollback="e=> rollback('calibrationMode',e)"
|
||||
v-model="offsetRobotOffsetMode"
|
||||
name="Robot Offset Mode"
|
||||
:list="['None','Single Point','Dual Point']"
|
||||
@input="handlePipelineData('offsetRobotOffsetMode')"
|
||||
@rollback="e=> rollback('offsetRobotOffsetMode',e)"
|
||||
/>
|
||||
<component
|
||||
:is="selectedComponent"
|
||||
@@ -82,6 +82,7 @@
|
||||
DualCalibration,
|
||||
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
|
||||
data() {
|
||||
@@ -91,22 +92,65 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
contourSortMode: {
|
||||
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourSortMode
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourSortMode": val});
|
||||
}
|
||||
},
|
||||
contourTargetOffsetPointEdge: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourTargetOffsetPointEdge
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourTargetOffsetPointEdge": val});
|
||||
}
|
||||
},
|
||||
contourTargetOrientation: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.contourTargetOrientation
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"contourTargetOrientation": val});
|
||||
}
|
||||
},
|
||||
outputShowMultipleTargets: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.outputShowMultipleTargets
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"outputShowMultipleTargets": val});
|
||||
}
|
||||
},
|
||||
offsetRobotOffsetMode: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.offsetRobotOffsetMode
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"offsetRobotOffsetMode": val});
|
||||
}
|
||||
},
|
||||
|
||||
selectedComponent: {
|
||||
get() {
|
||||
switch (this.value.calibrationMode) {
|
||||
case 0:
|
||||
return "";
|
||||
case 1:
|
||||
return "SingleCalibration";
|
||||
return "Single Point";
|
||||
case 2:
|
||||
return "DualCalibration"
|
||||
return "Dual Point"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
},
|
||||
rawPoint: {
|
||||
get() {
|
||||
return this.$store.state.point.rawPoint;
|
||||
return undefined; // TODO fix
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="value.hue"
|
||||
name="Hue"
|
||||
:min="0"
|
||||
:max="180"
|
||||
@input="handleData('hue')"
|
||||
@rollback="e => rollback('hue',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="value.saturation"
|
||||
name="Saturation"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handleData('saturation')"
|
||||
@rollback="e => rollback('saturation',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="value.value"
|
||||
name="Value"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handleData('value')"
|
||||
@rollback="e => rollback('value',e)"
|
||||
/>
|
||||
<v-divider
|
||||
color="black"
|
||||
style="margin-top: 5px"
|
||||
/>
|
||||
<v-row justify="center">
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(1)"
|
||||
>
|
||||
<v-icon>colorize</v-icon>
|
||||
Eye drop
|
||||
</v-btn>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(2)"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Expand Selection
|
||||
</v-btn>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(3)"
|
||||
>
|
||||
<v-icon>remove</v-icon>
|
||||
Shrink Selection
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<v-divider color="black" />
|
||||
<CVswitch
|
||||
v-model="value.erode"
|
||||
name="Erode"
|
||||
@input="handleData('erode')"
|
||||
@rollback="e => rollback('erode',e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="value.dilate"
|
||||
name="Dilate"
|
||||
@input="handleData('dilate')"
|
||||
@rollback="e => rollback('dilate',e)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CVrangeSlider
|
||||
v-model="hsvHue"
|
||||
name="Hue"
|
||||
:min="0"
|
||||
:max="180"
|
||||
@input="handlePipelineData('hsvHue')"
|
||||
@rollback="e => rollback('hue',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="hsvSaturation"
|
||||
name="Saturation"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handlePipelineData('hsvSaturation')"
|
||||
@rollback="e => rollback('saturation',e)"
|
||||
/>
|
||||
<CVrangeSlider
|
||||
v-model="hsvValue"
|
||||
name="Value"
|
||||
:min="0"
|
||||
:max="255"
|
||||
@input="handlePipelineData('hsvValue')"
|
||||
@rollback="e => rollback('value',e)"
|
||||
/>
|
||||
<v-divider
|
||||
color="black"
|
||||
style="margin-top: 5px"
|
||||
/>
|
||||
<v-row justify="center">
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(1)"
|
||||
>
|
||||
<v-icon>colorize</v-icon>
|
||||
Eye drop
|
||||
</v-btn>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(2)"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Expand Selection
|
||||
</v-btn>
|
||||
<v-btn
|
||||
style="margin: 20px;"
|
||||
color="#ffd843"
|
||||
small
|
||||
@click="setFunction(3)"
|
||||
>
|
||||
<v-icon>remove</v-icon>
|
||||
Shrink Selection
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<v-divider color="black"/>
|
||||
<CVswitch
|
||||
v-model="erode"
|
||||
name="Erode"
|
||||
@input="handlePipelineData('erode')"
|
||||
@rollback="e => rollback('erode',e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="dilate"
|
||||
name="Dilate"
|
||||
@input="handlePipelineData('dilate')"
|
||||
@rollback="e => rollback('dilate',e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -83,28 +83,56 @@
|
||||
CVrangeSlider,
|
||||
CVswitch
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
currentFunction: undefined,
|
||||
colorPicker: undefined,
|
||||
currentBinaryState: 0
|
||||
showThresholdState: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pipeline: {
|
||||
hsvHue: {
|
||||
get() {
|
||||
return this.$store.state.pipeline;
|
||||
}
|
||||
},
|
||||
driverState: {
|
||||
get() {
|
||||
return this.$store.state.driverMode;
|
||||
return this.$store.getters.currentPipelineSettings.hsvHue
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("driverMode", val);
|
||||
this.$store.commit("mutatePipeline", {"hsvHue": val})
|
||||
}
|
||||
}
|
||||
},
|
||||
hsvSaturation: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.hsvSaturation
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
|
||||
}
|
||||
},
|
||||
hsvValue: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.hsvValue
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"hsvValue": val})
|
||||
}
|
||||
},
|
||||
erode: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.erode
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"erode": val});
|
||||
}
|
||||
},
|
||||
dilate: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.dilate
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"dilate": val});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
const self = this;
|
||||
@@ -119,25 +147,24 @@
|
||||
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
|
||||
[[this.value.hue[0], this.value.saturation[0], this.value.value[0]], [this.value.hue[1], this.value.saturation[1], this.value.value[1]]]);
|
||||
this.currentFunction = undefined;
|
||||
this.value.hue = [hsvArray[0][0], hsvArray[1][0]];
|
||||
this.value.saturation = [hsvArray[0][1], hsvArray[1][1]];
|
||||
this.value.value = [hsvArray[0][2], hsvArray[1][2]];
|
||||
this.value.isBinary = this.currentBinaryState;
|
||||
let msg = this.$msgPack.encode({
|
||||
'hue': this.value.hue,
|
||||
'saturation': this.value.saturation,
|
||||
'value': this.value.value,
|
||||
'isBinary': this.value.isBinary
|
||||
"changePipelineSetting": {
|
||||
'hsvHue': [hsvArray[0][0], hsvArray[1][0]],
|
||||
'hsvSaturation': [hsvArray[0][1], hsvArray[1][1]],
|
||||
'hsvValue': [hsvArray[0][2], hsvArray[1][2]],
|
||||
'outputShowThresholded': this.showThresholdState,
|
||||
'cameraIndex': this.$store.state.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$emit('update');
|
||||
}
|
||||
},
|
||||
setFunction(index) {
|
||||
this.currentBinaryState = this.value.isBinary;
|
||||
if (this.currentBinaryState === true) {
|
||||
this.value.isBinary = false;
|
||||
this.handleData('isBinary')
|
||||
this.showThresholdState = this.value.outputShowThresholded;
|
||||
if (this.showThresholdState === true) {
|
||||
this.value.outputShowThresholded = false;
|
||||
this.handlePipelineData('outputShowThresholded')
|
||||
}
|
||||
switch (index) {
|
||||
case 0:
|
||||
|
||||
4
photon-server/.gitignore
vendored
4
photon-server/.gitignore
vendored
@@ -3,8 +3,8 @@ bin/*
|
||||
.project
|
||||
.classpath
|
||||
*.prefs
|
||||
photonvision
|
||||
.gradle
|
||||
.gradle/*
|
||||
build
|
||||
build/*
|
||||
build/*
|
||||
photonvision/*
|
||||
@@ -1,16 +1,16 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '5.2.0'
|
||||
id "com.diffplug.gradle.spotless" version "3.28.0"
|
||||
id "application"
|
||||
id "jacoco"
|
||||
}
|
||||
|
||||
mainClassName = 'org.photonvision.Main'
|
||||
|
||||
group 'org.photonvision'
|
||||
version '2020.6.1'
|
||||
|
||||
mainClassName = "org.photonvision.server.Main"
|
||||
|
||||
sourceCompatibility = 11
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Represents a 2d pose containing translational and rotational elements. */
|
||||
@@ -228,4 +229,12 @@ public class Pose2d {
|
||||
public int hashCode() {
|
||||
return Objects.hash(m_translation, m_rotation);
|
||||
}
|
||||
|
||||
public HashMap<String, Object> toHashMap() {
|
||||
var ret = new HashMap<String, Object>();
|
||||
ret.put("x", getTranslation().getX());
|
||||
ret.put("y", getTranslation().getY());
|
||||
ret.put("rot", getRotation().getDegrees());
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
72
photon-server/src/main/java/org/photonvision/Main.java
Normal file
72
photon-server/src/main/java/org/photonvision/Main.java
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.datatransfer.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.logging.Level;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.server.Server;
|
||||
import org.photonvision.vision.camera.USBCameraSource;
|
||||
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
import org.photonvision.vision.processes.VisionSourceManager;
|
||||
|
||||
public class Main {
|
||||
private static final Logger logger = new Logger(Main.class, LogGroup.General);
|
||||
|
||||
public static final int DEFAULT_WEBPORT = 5800;
|
||||
|
||||
public static void main(String[] args) {
|
||||
Logger.setLevel(LogGroup.Camera, Level.DE_PEST);
|
||||
Logger.setLevel(LogGroup.Server, Level.DE_PEST);
|
||||
Logger.setLevel(LogGroup.VisionProcess, Level.DE_PEST);
|
||||
Logger.setLevel(LogGroup.Data, Level.DE_PEST);
|
||||
Logger.setLevel(LogGroup.General, Level.DE_PEST);
|
||||
|
||||
TestUtils.loadLibraries();
|
||||
ConfigManager.getInstance(); // init config manager
|
||||
NetworkManager.getInstance().initialize(false); // basically empty. todo: link to ConfigManager?
|
||||
NetworkTablesManager.setClientMode("127.0.0.1");
|
||||
|
||||
HashMap<String, CameraConfiguration> camConfigs =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations();
|
||||
logger.info("Loaded " + camConfigs.size() + " configs from disk!");
|
||||
List<VisionSource> sources = VisionSourceManager.loadAllSources(camConfigs.values());
|
||||
|
||||
var collectedSources = new HashMap<VisionSource, List<CVPipelineSettings>>();
|
||||
for (var src : sources) {
|
||||
var usbSrc = (USBCameraSource) src;
|
||||
collectedSources.put(usbSrc, usbSrc.configuration.pipelineSettings);
|
||||
}
|
||||
|
||||
logger.info("Adding " + collectedSources.size() + " configs to VMM");
|
||||
VisionModuleManager.getInstance().addSources(collectedSources);
|
||||
ConfigManager.getInstance().addCameraConfigurations(collectedSources);
|
||||
|
||||
VisionModuleManager.getInstance().startModules();
|
||||
Server.main(DEFAULT_WEBPORT);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,14 @@ public class CameraConfiguration {
|
||||
public String path = "";
|
||||
public CameraType cameraType = CameraType.UsbCamera;
|
||||
public CameraCalibrationCoefficients calibration;
|
||||
public List<Integer> CameraLEDs = new ArrayList<>();
|
||||
public List<Integer> cameraLeds = new ArrayList<>();
|
||||
public int currentPipelineIndex = -1;
|
||||
|
||||
@JsonIgnore // this ignores the pipes as we serialize them to their own subfolder
|
||||
public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
|
||||
|
||||
public CameraConfiguration(String baseName, String path) {
|
||||
this(baseName, baseName, baseName, path);
|
||||
@@ -51,6 +58,15 @@ public class CameraConfiguration {
|
||||
this.uniqueName = uniqueName;
|
||||
this.nickname = nickname;
|
||||
this.path = path;
|
||||
|
||||
logger.debug(
|
||||
"Creating USB camera configuration for "
|
||||
+ cameraType
|
||||
+ baseName
|
||||
+ " (AKA "
|
||||
+ nickname
|
||||
+ ") at "
|
||||
+ path);
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
@@ -62,7 +78,8 @@ public class CameraConfiguration {
|
||||
@JsonProperty("path") String path,
|
||||
@JsonProperty("cameraType") CameraType cameraType,
|
||||
@JsonProperty("calibration") CameraCalibrationCoefficients calibration,
|
||||
@JsonProperty("CameraLEDs") List<Integer> cameraLEDs) {
|
||||
@JsonProperty("cameraLEDs") List<Integer> cameraLeds,
|
||||
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
|
||||
this.baseName = baseName;
|
||||
this.uniqueName = uniqueName;
|
||||
this.nickname = nickname;
|
||||
@@ -70,15 +87,19 @@ public class CameraConfiguration {
|
||||
this.path = path;
|
||||
this.cameraType = cameraType;
|
||||
this.calibration = calibration;
|
||||
this.CameraLEDs = cameraLEDs;
|
||||
this.cameraLeds = cameraLeds;
|
||||
this.currentPipelineIndex = currentPipelineIndex;
|
||||
|
||||
logger.debug(
|
||||
"Creating camera configuration for "
|
||||
+ cameraType
|
||||
+ baseName
|
||||
+ " (AKA "
|
||||
+ nickname
|
||||
+ ") at "
|
||||
+ path);
|
||||
}
|
||||
|
||||
@JsonIgnore // this ignores the pipes as we serialize them to their own subfolder
|
||||
public final List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
|
||||
|
||||
public void addPipelineSettings(List<CVPipelineSettings> settings) {
|
||||
for (var setting : settings) {
|
||||
addPipelineSetting(setting);
|
||||
@@ -101,4 +122,8 @@ public class CameraConfiguration {
|
||||
pipelineSettings.add(setting);
|
||||
pipelineSettings.sort(PipelineManager.PipelineSettingsIndexComparator);
|
||||
}
|
||||
|
||||
public void setPipelineSettings(List<CVPipelineSettings> settings) {
|
||||
pipelineSettings = settings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.photonvision.common.logging.Logger;
|
||||
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;
|
||||
|
||||
public class ConfigManager {
|
||||
private static final Logger logger = new Logger(ConfigManager.class, LogGroup.General);
|
||||
@@ -65,10 +66,11 @@ public class ConfigManager {
|
||||
this.networkConfigFile =
|
||||
new File(Path.of(rootFolder.toString(), "networkSettings.json").toUri());
|
||||
this.camerasFolder = new File(Path.of(rootFolder.toString(), "cameras").toUri());
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
public void load() {
|
||||
private void load() {
|
||||
logger.info("Loading settings...");
|
||||
if (!rootFolder.exists()) {
|
||||
if (rootFolder.mkdirs()) {
|
||||
@@ -150,6 +152,7 @@ public class ConfigManager {
|
||||
var subdir = Path.of(camerasFolder.toPath().toString(), subdirName);
|
||||
|
||||
if (!subdir.toFile().exists()) {
|
||||
// TODO: check for error
|
||||
subdir.toFile().mkdirs();
|
||||
}
|
||||
|
||||
@@ -166,10 +169,20 @@ public class ConfigManager {
|
||||
logger.error("Could not save drivermode.json for " + subdir);
|
||||
}
|
||||
|
||||
// Delete old pipe configs so that we don't get any conflicts
|
||||
try {
|
||||
var pipelineFolder = Path.of(subdir.toString(), "pipelines");
|
||||
Files.list(pipelineFolder).forEach(it -> it.toFile().delete());
|
||||
} catch (IOException e) {
|
||||
logger.error("Exception while deleting old configs!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -235,9 +248,7 @@ public class ConfigManager {
|
||||
var relativizedFilePath =
|
||||
getRootFolder().toAbsolutePath().relativize(p).toString();
|
||||
try {
|
||||
var ret = JacksonUtils.deserialize(p, CVPipelineSettings.class);
|
||||
return ret;
|
||||
|
||||
return JacksonUtils.deserialize(p, CVPipelineSettings.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("Exception while deserializing " + relativizedFilePath);
|
||||
e.printStackTrace();
|
||||
@@ -260,4 +271,18 @@ public class ConfigManager {
|
||||
}
|
||||
return loadedConfigurations;
|
||||
}
|
||||
|
||||
public void addCameraConfigurations(HashMap<VisionSource, List<CVPipelineSettings>> sources) {
|
||||
List<CameraConfiguration> list =
|
||||
sources.keySet().stream()
|
||||
.map(it -> it.getSettables().getConfiguration())
|
||||
.collect(Collectors.toList());
|
||||
getConfig().addCameraConfigs(list);
|
||||
save();
|
||||
}
|
||||
|
||||
public void saveModule(CameraConfiguration config, String uniqueName) {
|
||||
getConfig().addCameraConfig(uniqueName, config);
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,24 @@
|
||||
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.networking.NetworkMode;
|
||||
|
||||
public class NetworkConfig {
|
||||
public int teamNumber = 1577;
|
||||
public int teamNumber = 1;
|
||||
public NetworkMode connectionType = NetworkMode.DHCP;
|
||||
public String ip = "";
|
||||
public String gateway = "";
|
||||
public String netmask = "";
|
||||
public String hostname = "photonvision";
|
||||
|
||||
public HashMap<String, Object> toHashMap() {
|
||||
HashMap<String, Object> tmp = new HashMap<>();
|
||||
tmp.put("teamNumber", teamNumber);
|
||||
tmp.put("connectionType", connectionType.ordinal());
|
||||
tmp.put("ip", ip);
|
||||
tmp.put("gateway", gateway);
|
||||
tmp.put("netmask", netmask);
|
||||
return tmp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
|
||||
// TODO rename this class
|
||||
public class PhotonConfiguration {
|
||||
@@ -33,6 +39,12 @@ public class PhotonConfiguration {
|
||||
return cameraConfigurations;
|
||||
}
|
||||
|
||||
public void addCameraConfigs(List<CameraConfiguration> config) {
|
||||
for (var c : config) {
|
||||
addCameraConfig(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void addCameraConfig(CameraConfiguration config) {
|
||||
addCameraConfig(config.uniqueName, config);
|
||||
}
|
||||
@@ -58,4 +70,29 @@ public class PhotonConfiguration {
|
||||
this.networkConfig = networkConfig;
|
||||
this.cameraConfigurations = cameraConfigurations;
|
||||
}
|
||||
|
||||
public Map<String, Object> toHashMap() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
map.put("networkSettings", networkConfig.toHashMap());
|
||||
map.put(
|
||||
"cameraSettings",
|
||||
VisionModuleManager.getInstance().getModules().stream()
|
||||
.map(VisionModule::toUICameraConfig)
|
||||
.map(SerializationUtils::objectToHashMap)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public static class UICameraConfiguration {
|
||||
@SuppressWarnings("unused")
|
||||
public double fov, tiltDegrees;
|
||||
public String nickname;
|
||||
public HashMap<String, Object> currentPipelineSettings;
|
||||
public int currentPipelineIndex;
|
||||
public List<String> pipelineNicknames;
|
||||
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
|
||||
public int streamPort;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public enum DataChangeDestination {
|
||||
DCD_ACTIVEMODULE,
|
||||
DCD_ACTIVEPIPELINESETTINGS,
|
||||
DCD_GENSETTINGS,
|
||||
DCD_UI,
|
||||
DCD_OTHER;
|
||||
|
||||
public static final List<DataChangeDestination> AllDestinations =
|
||||
Arrays.asList(DataChangeDestination.values());
|
||||
|
||||
public static class DataChangeDestinationList extends ArrayList<DataChangeDestination> {}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
import org.photonvision.common.logging.Level;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class DataChangeService {
|
||||
|
||||
private static final Logger logger = new Logger(DataChangeService.class, LogGroup.Server);
|
||||
|
||||
private static class ThreadSafeSingleton {
|
||||
private static final DataChangeService INSTANCE = new DataChangeService();
|
||||
}
|
||||
|
||||
public static DataChangeService getInstance() {
|
||||
return ThreadSafeSingleton.INSTANCE;
|
||||
}
|
||||
|
||||
private final CopyOnWriteArrayList<DataChangeSubscriber> subscribers;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final Thread dispatchThread;
|
||||
|
||||
private final BlockingQueue<DataChangeEvent> eventQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
private DataChangeService() {
|
||||
subscribers = new CopyOnWriteArrayList<>();
|
||||
dispatchThread = new Thread(this::dispatchFromQueue);
|
||||
dispatchThread.setName("EventDispatchThread");
|
||||
dispatchThread.start();
|
||||
}
|
||||
|
||||
public boolean hasEvents() {
|
||||
return !eventQueue.isEmpty();
|
||||
}
|
||||
|
||||
private void dispatchFromQueue() {
|
||||
while (true) {
|
||||
try {
|
||||
var taken = eventQueue.take();
|
||||
for (var sub : subscribers) {
|
||||
if (sub.wantedSources.contains(taken.sourceType)
|
||||
&& sub.wantedDestinations.contains(taken.destType)) {
|
||||
sub.onDataChangeEvent(taken);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception when dispatching event!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addSubscriber(DataChangeSubscriber subscriber) {
|
||||
if (!subscribers.addIfAbsent(subscriber)) {
|
||||
logger.warn("Attempted to add already added subscriber!");
|
||||
} else {
|
||||
if (Logger.shouldLog(Level.TRACE, LogGroup.Data)) {
|
||||
var sources =
|
||||
subscriber.wantedSources.stream().map(Enum::toString).collect(Collectors.joining(", "));
|
||||
var dests =
|
||||
subscriber.wantedDestinations.stream()
|
||||
.map(Enum::toString)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
logger.trace("Added subscriber - " + "Sources: " + sources + ", Destinations: " + dests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addSubscribers(DataChangeSubscriber... subs) {
|
||||
for (var sub : subs) {
|
||||
addSubscriber(sub);
|
||||
}
|
||||
}
|
||||
|
||||
public void publishEvent(DataChangeEvent event) {
|
||||
eventQueue.offer(event);
|
||||
}
|
||||
|
||||
public void publishEvents(DataChangeEvent... events) {
|
||||
for (var event : events) {
|
||||
publishEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public enum DataChangeSource {
|
||||
DCS_WEBSOCKET,
|
||||
DCS_HTTP,
|
||||
DCS_NETWORKTABLES,
|
||||
DCS_VISIONMODULE,
|
||||
DCS_OTHER;
|
||||
|
||||
public static final List<DataChangeSource> AllSources = Arrays.asList(DataChangeSource.values());
|
||||
|
||||
public static class DataChangeSourceList extends ArrayList<DataChangeSource> {}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public abstract class DataChangeSubscriber {
|
||||
public final List<DataChangeSource> wantedSources;
|
||||
public final List<DataChangeDestination> wantedDestinations;
|
||||
|
||||
private final int hash;
|
||||
|
||||
public DataChangeSubscriber(
|
||||
List<DataChangeSource> wantedSources, List<DataChangeDestination> wantedDestinations) {
|
||||
this.wantedSources = wantedSources;
|
||||
this.wantedDestinations = wantedDestinations;
|
||||
hash = Objects.hash(wantedSources, wantedDestinations);
|
||||
}
|
||||
|
||||
public DataChangeSubscriber() {
|
||||
this(DataChangeSource.AllSources, DataChangeDestination.AllDestinations);
|
||||
}
|
||||
|
||||
public DataChangeSubscriber(DataChangeSource.DataChangeSourceList wantedSources) {
|
||||
this(wantedSources, DataChangeDestination.AllDestinations);
|
||||
}
|
||||
|
||||
public DataChangeSubscriber(DataChangeDestination.DataChangeDestinationList wantedDestinations) {
|
||||
this(DataChangeSource.AllSources, wantedDestinations);
|
||||
}
|
||||
|
||||
public abstract void onDataChangeEvent(DataChangeEvent event);
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow;
|
||||
|
||||
public interface DataProvider {}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.camera;
|
||||
|
||||
import org.photonvision.common.dataflow.DataChangeSubscriber;
|
||||
import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
|
||||
public class IncomingCameraCommandSubscriber extends DataChangeSubscriber {
|
||||
private static final Logger logger =
|
||||
new Logger(IncomingCameraCommandSubscriber.class, LogGroup.Camera);
|
||||
|
||||
private final VisionModuleManager vmm;
|
||||
|
||||
public IncomingCameraCommandSubscriber(VisionModuleManager instance) {
|
||||
this.vmm = instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent event) {
|
||||
// logger.de_pest("Got event from [" + event.sourceType + "] and dest [" + event.destType
|
||||
// + "] with property name [" + event.propertyName
|
||||
// + "] and value [" + event.data + "]");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.events;
|
||||
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
import org.photonvision.common.dataflow.DataChangeSource;
|
||||
|
||||
public class DataChangeEvent<T> {
|
||||
public final DataChangeSource sourceType;
|
||||
public final DataChangeDestination destType;
|
||||
public final String propertyName;
|
||||
public final T data;
|
||||
|
||||
public DataChangeEvent(
|
||||
DataChangeSource sourceType,
|
||||
DataChangeDestination destType,
|
||||
String propertyName,
|
||||
T newValue) {
|
||||
this.sourceType = sourceType;
|
||||
this.destType = destType;
|
||||
this.propertyName = propertyName;
|
||||
this.data = newValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DataChangeEvent{"
|
||||
+ "sourceType="
|
||||
+ sourceType
|
||||
+ ", destType="
|
||||
+ destType
|
||||
+ ", propertyName='"
|
||||
+ propertyName
|
||||
+ '\''
|
||||
+ ", data="
|
||||
+ data
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.events;
|
||||
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
import org.photonvision.common.dataflow.DataChangeSource;
|
||||
|
||||
public class HTTPRequestEvent<T> extends DataChangeEvent<T> {
|
||||
public HTTPRequestEvent(
|
||||
DataChangeSource sourceType,
|
||||
DataChangeDestination destType,
|
||||
String propertyName,
|
||||
T newValue) {
|
||||
super(sourceType, destType, propertyName, newValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.events;
|
||||
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
import org.photonvision.common.dataflow.DataChangeSource;
|
||||
|
||||
public class IncomingWebSocketEvent<T> extends DataChangeEvent<T> {
|
||||
|
||||
public final Integer cameraIndex;
|
||||
|
||||
public IncomingWebSocketEvent(DataChangeDestination destType, String propertyName, T newValue) {
|
||||
this(destType, propertyName, newValue, null);
|
||||
}
|
||||
|
||||
public IncomingWebSocketEvent(
|
||||
DataChangeDestination destType, String propertyName, T newValue, Integer cameraIndex) {
|
||||
super(DataChangeSource.DCS_WEBSOCKET, destType, propertyName, newValue);
|
||||
this.cameraIndex = cameraIndex;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public IncomingWebSocketEvent(
|
||||
DataChangeDestination destType, String dataKey, HashMap<String, Object> data) {
|
||||
this(destType, dataKey, (T) data.get(dataKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IncomingWebSocketEvent{"
|
||||
+ "cameraIndex="
|
||||
+ cameraIndex
|
||||
+ ", sourceType="
|
||||
+ sourceType
|
||||
+ ", destType="
|
||||
+ destType
|
||||
+ ", propertyName='"
|
||||
+ propertyName
|
||||
+ '\''
|
||||
+ ", data="
|
||||
+ data
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.events;
|
||||
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
import org.photonvision.common.dataflow.DataChangeSource;
|
||||
import org.photonvision.server.UIUpdateType;
|
||||
|
||||
public class OutgoingUIEvent<T> extends DataChangeEvent<T> {
|
||||
public final UIUpdateType updateType;
|
||||
|
||||
public OutgoingUIEvent(UIUpdateType updateType, String propertyName, T newValue) {
|
||||
super(DataChangeSource.DCS_WEBSOCKET, DataChangeDestination.DCD_UI, propertyName, newValue);
|
||||
this.updateType = updateType;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public OutgoingUIEvent(UIUpdateType updateType, String dataKey, HashMap<String, Object> data) {
|
||||
this(updateType, dataKey, (T) data.get(dataKey));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
||||
import org.photonvision.common.datatransfer.DataConsumer;
|
||||
import org.photonvision.vision.pipeline.result.SimplePipelineResult;
|
||||
import org.photonvision.vision.processes.Data;
|
||||
|
||||
public class NTDataConsumer implements DataConsumer {
|
||||
|
||||
private final NetworkTable rootTable;
|
||||
NetworkTable subTable;
|
||||
NetworkTableEntry rawData;
|
||||
|
||||
public NTDataConsumer(NetworkTable root, String camName) {
|
||||
this.rootTable = root;
|
||||
this.subTable = root.getSubTable(camName);
|
||||
rawData = subTable.getEntry("rawData");
|
||||
}
|
||||
|
||||
public void setCameraName(String camName) {
|
||||
this.subTable = rootTable.getSubTable(camName);
|
||||
rawData = subTable.getEntry("rawData");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Data data) {
|
||||
var simplified = new SimplePipelineResult(data.result);
|
||||
var bytes = simplified.toByteArray();
|
||||
rawData.setRaw(bytes);
|
||||
rootTable.getInstance().flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.networktables.LogMessage;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import java.util.function.Consumer;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.scripting.ScriptEventType;
|
||||
import org.photonvision.common.scripting.ScriptManager;
|
||||
|
||||
public class NetworkTablesManager {
|
||||
|
||||
private NetworkTablesManager() {}
|
||||
|
||||
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
|
||||
|
||||
private static final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
|
||||
|
||||
public static final String kRootTableName = "/chameleon-vision";
|
||||
public static final NetworkTable kRootTable =
|
||||
NetworkTableInstance.getDefault().getTable(kRootTableName);
|
||||
|
||||
public static boolean isServer = false;
|
||||
|
||||
private static int getTeamNumber() {
|
||||
// TODO: FIX
|
||||
return 0;
|
||||
// return ConfigManager.settings.teamNumber;
|
||||
}
|
||||
|
||||
private static class NTLogger implements Consumer<LogMessage> {
|
||||
|
||||
private boolean hasReportedConnectionFailure = false;
|
||||
|
||||
@Override
|
||||
public void accept(LogMessage logMessage) {
|
||||
if (!hasReportedConnectionFailure && logMessage.message.contains("timed out")) {
|
||||
logger.error("NT Connection has failed! Will retry in background.");
|
||||
hasReportedConnectionFailure = true;
|
||||
} else if (logMessage.message.contains("connected")) {
|
||||
logger.info("NT Connected!");
|
||||
hasReportedConnectionFailure = false;
|
||||
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
NetworkTableInstance.getDefault().addLogger(new NTLogger(), 0, 255); // to hide error messages
|
||||
}
|
||||
|
||||
public static void setClientMode(String host) {
|
||||
isServer = false;
|
||||
logger.info("Starting NT Client");
|
||||
ntInstance.stopServer();
|
||||
if (host != null) {
|
||||
ntInstance.startClient(host);
|
||||
} else {
|
||||
ntInstance.startClientTeam(getTeamNumber());
|
||||
if (ntInstance.isConnected()) {
|
||||
logger.info("[NetworkTablesManager] Connected to the robot!");
|
||||
} else {
|
||||
logger.info(
|
||||
"[NetworkTablesManager] Could NOT to the robot! Will retry in the background...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void setTeamClientMode() {
|
||||
setClientMode(null);
|
||||
}
|
||||
|
||||
public static void setServerMode() {
|
||||
isServer = true;
|
||||
logger.info("Starting NT Server");
|
||||
ntInstance.stopClient();
|
||||
ntInstance.startServer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.structures;
|
||||
|
||||
@SuppressWarnings("PointlessBitwiseExpression")
|
||||
public abstract class BytePackable {
|
||||
public abstract byte[] toByteArray();
|
||||
|
||||
public abstract void fromByteArray(byte[] src);
|
||||
|
||||
protected int bufferPosition = 0;
|
||||
|
||||
public int getBufferPosition() {
|
||||
return bufferPosition;
|
||||
}
|
||||
|
||||
public void resetBufferPosition() {
|
||||
bufferPosition = 0;
|
||||
}
|
||||
|
||||
protected void bufferData(byte[] src, byte[] dest) {
|
||||
System.arraycopy(src, 0, dest, bufferPosition, src.length);
|
||||
bufferPosition += src.length;
|
||||
}
|
||||
|
||||
protected void bufferData(byte src, byte[] dest) {
|
||||
System.arraycopy(new byte[] {src}, 0, dest, bufferPosition, 1);
|
||||
bufferPosition++;
|
||||
}
|
||||
|
||||
protected void bufferData(int src, byte[] dest) {
|
||||
System.arraycopy(toBytes(src), 0, dest, bufferPosition, Integer.BYTES);
|
||||
bufferPosition += Integer.BYTES;
|
||||
}
|
||||
|
||||
protected void bufferData(double src, byte[] dest) {
|
||||
System.arraycopy(toBytes(src), 0, dest, bufferPosition, Double.BYTES);
|
||||
bufferPosition += Double.BYTES;
|
||||
}
|
||||
|
||||
protected void bufferData(boolean src, byte[] dest) {
|
||||
System.arraycopy(toBytes(src), 0, dest, bufferPosition, 1);
|
||||
bufferPosition += 1;
|
||||
}
|
||||
|
||||
protected boolean unbufferBoolean(byte[] src) {
|
||||
return toBoolean(src, bufferPosition++);
|
||||
}
|
||||
|
||||
protected byte unbufferByte(byte[] src) {
|
||||
var value = src[bufferPosition];
|
||||
bufferPosition++;
|
||||
return value;
|
||||
}
|
||||
|
||||
protected byte[] unbufferBytes(byte[] src, int len) {
|
||||
var bytes = new byte[len];
|
||||
System.arraycopy(src, bufferPosition, bytes, 0, len);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
protected int unbufferInt(byte[] src) {
|
||||
var value = toInt(src, bufferPosition);
|
||||
bufferPosition += Integer.BYTES;
|
||||
return value;
|
||||
}
|
||||
|
||||
protected double unbufferDouble(byte[] src) {
|
||||
var value = toDouble(src, bufferPosition);
|
||||
bufferPosition += Double.BYTES;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean toBoolean(byte[] src, int pos) {
|
||||
return src[pos] != 0;
|
||||
}
|
||||
|
||||
private static int toInt(byte[] src, int pos) {
|
||||
return (0xff & src[pos]) << 24
|
||||
| (0xff & src[pos + 1]) << 16
|
||||
| (0xff & src[pos + 2]) << 8
|
||||
| (0xff & src[pos + 3]) << 0;
|
||||
}
|
||||
|
||||
private static long toLong(byte[] src, int pos) {
|
||||
return (long) (0xff & src[pos]) << 56
|
||||
| (long) (0xff & src[pos + 1]) << 48
|
||||
| (long) (0xff & src[pos + 2]) << 40
|
||||
| (long) (0xff & src[pos + 3]) << 32
|
||||
| (long) (0xff & src[pos + 4]) << 24
|
||||
| (long) (0xff & src[pos + 5]) << 16
|
||||
| (long) (0xff & src[pos + 6]) << 8
|
||||
| (long) (0xff & src[pos + 7]) << 0;
|
||||
}
|
||||
|
||||
private static double toDouble(byte[] src, int pos) {
|
||||
return Double.longBitsToDouble(toLong(src, pos));
|
||||
}
|
||||
|
||||
protected byte[] toBytes(double value) {
|
||||
long data = Double.doubleToRawLongBits(value);
|
||||
return new byte[] {
|
||||
(byte) ((data >> 56) & 0xff),
|
||||
(byte) ((data >> 48) & 0xff),
|
||||
(byte) ((data >> 40) & 0xff),
|
||||
(byte) ((data >> 32) & 0xff),
|
||||
(byte) ((data >> 24) & 0xff),
|
||||
(byte) ((data >> 16) & 0xff),
|
||||
(byte) ((data >> 8) & 0xff),
|
||||
(byte) ((data >> 0) & 0xff),
|
||||
};
|
||||
}
|
||||
|
||||
protected byte[] toBytes(int value) {
|
||||
return new byte[] {
|
||||
(byte) ((value >> 24) & 0xff),
|
||||
(byte) ((value >> 16) & 0xff),
|
||||
(byte) ((value >> 8) & 0xff),
|
||||
(byte) ((value >> 0) & 0xff)
|
||||
};
|
||||
}
|
||||
|
||||
protected byte[] toBytes(boolean value) {
|
||||
return new byte[] {(byte) (value ? 1 : 0)};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.structures;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
public class SimplePipelineResult extends BytePackable {
|
||||
|
||||
private double latencyMillis;
|
||||
private boolean hasTargets;
|
||||
public final List<SimpleTrackedTarget> targets = new ArrayList<>();
|
||||
|
||||
public SimplePipelineResult() {}
|
||||
|
||||
public SimplePipelineResult(
|
||||
double latencyMillis, boolean hasTargets, List<SimpleTrackedTarget> targets) {
|
||||
this.latencyMillis = latencyMillis;
|
||||
this.hasTargets = hasTargets;
|
||||
this.targets.addAll(targets);
|
||||
}
|
||||
|
||||
public SimplePipelineResult(CVPipelineResult origResult) {
|
||||
update(origResult);
|
||||
}
|
||||
|
||||
public void update(CVPipelineResult origResult) {
|
||||
latencyMillis = origResult.getLatencyMillis();
|
||||
hasTargets = origResult.hasTargets();
|
||||
targets.clear();
|
||||
for (var origTarget : origResult.targets) {
|
||||
targets.add(new SimpleTrackedTarget(origTarget));
|
||||
}
|
||||
}
|
||||
|
||||
public double getLatencyMillis() {
|
||||
return latencyMillis;
|
||||
}
|
||||
|
||||
public boolean hasTargets() {
|
||||
return hasTargets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SimplePipelineResult that = (SimplePipelineResult) o;
|
||||
boolean latencyMatch = Double.compare(that.latencyMillis, latencyMillis) == 0;
|
||||
boolean hasTargetsMatch = that.hasTargets == hasTargets;
|
||||
boolean targetsMatch = that.targets.equals(targets);
|
||||
return latencyMatch && hasTargetsMatch && targetsMatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(latencyMillis, hasTargets, targets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
bufferPosition = 0;
|
||||
int bufferSize =
|
||||
8 + 1 + 1 + (targets.size() * 48); // latencyMillis + hasTargets + targetCount + targets
|
||||
var buff = new byte[bufferSize];
|
||||
|
||||
bufferData(latencyMillis, buff);
|
||||
bufferData(hasTargets, buff);
|
||||
bufferData((byte) targets.size(), buff);
|
||||
for (var target : targets) {
|
||||
bufferData(target.toByteArray(), buff);
|
||||
}
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromByteArray(byte[] src) {
|
||||
bufferPosition = 0;
|
||||
|
||||
latencyMillis = unbufferDouble(src);
|
||||
hasTargets = unbufferBoolean(src);
|
||||
byte targetCount = unbufferByte(src);
|
||||
|
||||
targets.clear();
|
||||
for (int i = 0; i < targetCount; i++) {
|
||||
var target = new SimpleTrackedTarget();
|
||||
target.fromByteArray(unbufferBytes(src, 48));
|
||||
bufferPosition += 48;
|
||||
targets.add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dataflow.structures;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import java.util.Objects;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
public class SimpleTrackedTarget extends BytePackable {
|
||||
public static final int PACK_SIZE_BYTES = Double.BYTES * 6;
|
||||
|
||||
private double yaw;
|
||||
private double pitch;
|
||||
private double area;
|
||||
private Pose2d robotRelativePose = new Pose2d();
|
||||
|
||||
public SimpleTrackedTarget() {}
|
||||
|
||||
public SimpleTrackedTarget(double yaw, double pitch, double area, Pose2d pose) {
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
this.area = area;
|
||||
robotRelativePose = pose;
|
||||
}
|
||||
|
||||
public SimpleTrackedTarget(TrackedTarget origTarget) {
|
||||
yaw = origTarget.getYaw();
|
||||
pitch = origTarget.getPitch();
|
||||
area = origTarget.getArea();
|
||||
robotRelativePose = origTarget.getRobotRelativePose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SimpleTrackedTarget that = (SimpleTrackedTarget) o;
|
||||
return Double.compare(that.yaw, yaw) == 0
|
||||
&& Double.compare(that.pitch, pitch) == 0
|
||||
&& Double.compare(that.area, area) == 0
|
||||
&& Objects.equals(robotRelativePose, that.robotRelativePose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(yaw, pitch, area, robotRelativePose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
resetBufferPosition();
|
||||
byte[] data = new byte[PACK_SIZE_BYTES];
|
||||
|
||||
bufferData(yaw, data);
|
||||
bufferData(pitch, data);
|
||||
bufferData(area, data);
|
||||
bufferData(robotRelativePose.getTranslation().getX(), data);
|
||||
bufferData(robotRelativePose.getTranslation().getY(), data);
|
||||
bufferData(robotRelativePose.getRotation().getDegrees(), data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromByteArray(byte[] src) {
|
||||
resetBufferPosition();
|
||||
if (src.length < PACK_SIZE_BYTES) {
|
||||
// TODO: log error?
|
||||
return;
|
||||
}
|
||||
|
||||
yaw = unbufferDouble(src);
|
||||
pitch = unbufferDouble(src);
|
||||
area = unbufferDouble(src);
|
||||
|
||||
var poseX = unbufferDouble(src);
|
||||
var poseY = unbufferDouble(src);
|
||||
var poseR = unbufferDouble(src);
|
||||
robotRelativePose = new Pose2d(poseX, poseY, Rotation2d.fromDegrees(poseR));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import edu.wpi.first.networktables.LogMessage;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import java.util.function.Consumer;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.scripting.ScriptEventType;
|
||||
@@ -41,9 +42,7 @@ public class NetworkTablesManager {
|
||||
public static boolean isServer = false;
|
||||
|
||||
private static int getTeamNumber() {
|
||||
// TODO: FIX
|
||||
return 0;
|
||||
// return ConfigManager.settings.teamNumber;
|
||||
return ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber;
|
||||
}
|
||||
|
||||
private static class NTLogger implements Consumer<LogMessage> {
|
||||
|
||||
@@ -21,5 +21,6 @@ public enum LogGroup {
|
||||
Camera,
|
||||
Server,
|
||||
VisionProcess,
|
||||
Data,
|
||||
General
|
||||
}
|
||||
|
||||
@@ -27,11 +27,12 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.server.UIUpdateType;
|
||||
|
||||
public class Logger {
|
||||
|
||||
private final String className;
|
||||
|
||||
public static final String ANSI_RESET = "\u001B[0m";
|
||||
public static final String ANSI_BLACK = "\u001B[30m";
|
||||
public static final String ANSI_RED = "\u001B[31m";
|
||||
@@ -42,7 +43,10 @@ public class Logger {
|
||||
public static final String ANSI_CYAN = "\u001B[36m";
|
||||
public static final String ANSI_WHITE = "\u001B[37m";
|
||||
|
||||
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
private static final SimpleDateFormat simpleDateFormat =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final String className;
|
||||
private final LogGroup group;
|
||||
|
||||
public Logger(Class<?> clazz, LogGroup group) {
|
||||
@@ -50,6 +54,11 @@ public class Logger {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public Logger(Class<?> clazz, String suffix, LogGroup group) {
|
||||
this.className = clazz.getSimpleName() + " - " + suffix;
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public static String getDate() {
|
||||
return simpleDateFormat.format(new Date());
|
||||
}
|
||||
@@ -81,11 +90,13 @@ public class Logger {
|
||||
levelMap.put(LogGroup.Camera, Level.INFO);
|
||||
levelMap.put(LogGroup.General, Level.INFO);
|
||||
levelMap.put(LogGroup.Server, Level.INFO);
|
||||
levelMap.put(LogGroup.Data, Level.INFO);
|
||||
levelMap.put(LogGroup.VisionProcess, Level.INFO);
|
||||
}
|
||||
|
||||
static {
|
||||
currentAppenders.add(new ConsoleAppender());
|
||||
currentAppenders.add(new UILogAppender());
|
||||
}
|
||||
|
||||
public static void addFileAppender(Path logFilePath) {
|
||||
@@ -113,7 +124,7 @@ public class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldLog(Level logLevel, LogGroup group) {
|
||||
public static boolean shouldLog(Level logLevel, LogGroup group) {
|
||||
return logLevel.code <= levelMap.get(group).code;
|
||||
}
|
||||
|
||||
@@ -152,8 +163,18 @@ public class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
private static class UILogAppender extends Appender {
|
||||
@Override
|
||||
void log(String message) {
|
||||
var message_ = new HashMap<>();
|
||||
message_.put("logMessage", message);
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(new OutgoingUIEvent<>(UIUpdateType.BROADCAST, "log", message_));
|
||||
}
|
||||
}
|
||||
|
||||
private static class AsyncFileAppender extends Appender {
|
||||
private Path filePath;
|
||||
private final Path filePath;
|
||||
|
||||
public AsyncFileAppender(Path logFilePath) {
|
||||
this.filePath = logFilePath;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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;
|
||||
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public final class SerializationUtils {
|
||||
private static final Logger LOGGER = new Logger(SerializationUtils.class, LogGroup.General);
|
||||
|
||||
public static HashMap<String, Object> objectToHashMap(Object src) {
|
||||
var ret = new HashMap<String, Object>();
|
||||
for (var field : src.getClass().getFields()) {
|
||||
try {
|
||||
if (!field
|
||||
.getType()
|
||||
.isEnum()) { // if the field is not an enum, get it based on the current pipeline
|
||||
ret.put(field.getName(), field.get(src));
|
||||
} else {
|
||||
var ordinal = (Enum) field.get(src);
|
||||
ret.put(field.getName(), ordinal.ordinal());
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
LOGGER.error("Could not serialize " + src.getClass().getSimpleName());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,10 @@ public class DoubleCouple extends NumberCouple<Double> {
|
||||
super(0.0, 0.0);
|
||||
}
|
||||
|
||||
public DoubleCouple(Number first, Number second) {
|
||||
super(first.doubleValue(), second.doubleValue());
|
||||
}
|
||||
|
||||
public DoubleCouple(Double first, Double second) {
|
||||
super(first, second);
|
||||
}
|
||||
|
||||
@@ -18,24 +18,54 @@
|
||||
package org.photonvision.server;
|
||||
|
||||
import io.javalin.Javalin;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class Server {
|
||||
private static final Logger logger = new Logger(Server.class, LogGroup.Server);
|
||||
|
||||
public static void main(int port) {
|
||||
|
||||
Javalin app =
|
||||
Javalin.create(
|
||||
javalinConfig -> {
|
||||
javalinConfig.showJavalinBanner = false;
|
||||
javalinConfig.addStaticFiles("web");
|
||||
javalinConfig.enableCorsForAllOrigins();
|
||||
config -> {
|
||||
config.showJavalinBanner = false;
|
||||
config.addStaticFiles("web");
|
||||
config.enableCorsForAllOrigins();
|
||||
|
||||
config.requestLogger(
|
||||
(ctx, ms) ->
|
||||
logger.debug(
|
||||
"Handled HTTP "
|
||||
+ ctx.req.getMethod()
|
||||
+ " request from "
|
||||
+ ctx.req.getRemoteHost()
|
||||
+ " in "
|
||||
+ ms.toString()
|
||||
+ "ms"));
|
||||
|
||||
config.wsLogger(
|
||||
ws ->
|
||||
ws.onMessage(
|
||||
ctx -> logger.debug("Got WebSockets message: " + ctx.message())));
|
||||
|
||||
config.wsLogger(
|
||||
ws ->
|
||||
ws.onBinaryMessage(
|
||||
ctx ->
|
||||
logger.debug(
|
||||
"Got WebSockets binary message from host " + ctx.host())));
|
||||
});
|
||||
|
||||
var socketHandler = SocketHandler.getInstance();
|
||||
|
||||
/*Web Socket Events */
|
||||
app.ws(
|
||||
"/websocket",
|
||||
ws -> {
|
||||
ws.onConnect(SocketHandler::onConnect);
|
||||
ws.onClose(SocketHandler::onClose);
|
||||
ws.onBinaryMessage(SocketHandler::onBinaryMessage);
|
||||
ws.onConnect(socketHandler::onConnect);
|
||||
ws.onClose(socketHandler::onClose);
|
||||
ws.onBinaryMessage(socketHandler::onBinaryMessage);
|
||||
});
|
||||
/*API Events*/
|
||||
app.post("/api/settings/general", RequestHandler::onGeneralSettings);
|
||||
|
||||
@@ -23,49 +23,273 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.javalin.websocket.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.msgpack.jackson.dataformat.MessagePackFactory;
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.camera.IncomingCameraCommandSubscriber;
|
||||
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.pipeline.PipelineType;
|
||||
import org.photonvision.vision.processes.PipelineManager;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class SocketHandler {
|
||||
static List<WsContext> users = new ArrayList<>();
|
||||
static ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
|
||||
|
||||
public static void onConnect(WsConnectContext context) {
|
||||
users.add(context);
|
||||
private final Logger logger = new Logger(SocketHandler.class, LogGroup.Server);
|
||||
private final List<WsContext> users = new ArrayList<>();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
|
||||
private final DataChangeService dcService = DataChangeService.getInstance();
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final UIOutboundSubscriber uiOutboundSubscriber = new UIOutboundSubscriber(this);
|
||||
|
||||
private final IncomingCameraCommandSubscriber cameraChangeSubscriber =
|
||||
new IncomingCameraCommandSubscriber(VisionModuleManager.getInstance());
|
||||
|
||||
public static class UIMap extends HashMap<String, Object> {}
|
||||
|
||||
abstract static class SelectiveBroadcastPair extends Pair<UIMap, WsContext> {}
|
||||
|
||||
private static class ThreadSafeSingleton {
|
||||
private static final SocketHandler INSTANCE = new SocketHandler();
|
||||
}
|
||||
|
||||
static void onClose(WsCloseContext context) {
|
||||
public static SocketHandler getInstance() {
|
||||
return SocketHandler.ThreadSafeSingleton.INSTANCE;
|
||||
}
|
||||
|
||||
private SocketHandler() {
|
||||
dcService.addSubscribers(
|
||||
uiOutboundSubscriber,
|
||||
cameraChangeSubscriber,
|
||||
new UIInboundSubscriber()); // Subscribe outgoing messages to the data change service
|
||||
}
|
||||
|
||||
public void onConnect(WsConnectContext context) {
|
||||
context.session.setIdleTimeout(Long.MAX_VALUE); // TODO: determine better value
|
||||
var host = context.session.getRemote().getInetSocketAddress().getHostName();
|
||||
logger.info("New websocket connection from " + host);
|
||||
users.add(context);
|
||||
dcService.publishEvent(
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_GENSETTINGS, "userConnected", context));
|
||||
}
|
||||
|
||||
protected void onClose(WsCloseContext context) {
|
||||
var host = context.session.getRemote().getInetSocketAddress().getHostName();
|
||||
var reason = context.reason() != null ? context.reason() : "Connection closed by client";
|
||||
logger.info("Closing websocket connection from " + host + " for reason: " + reason);
|
||||
users.remove(context);
|
||||
}
|
||||
|
||||
public static void onBinaryMessage(WsBinaryMessageContext context) {
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public void onBinaryMessage(WsBinaryMessageContext context) {
|
||||
try {
|
||||
Map<String, Object> data =
|
||||
objectMapper.readValue(context.data(), new TypeReference<Map<String, Object>>() {});
|
||||
// TODO pass data to ui data provider
|
||||
Map<String, Object> deserializedData =
|
||||
objectMapper.readValue(context.data(), new TypeReference<>() {});
|
||||
|
||||
// Special case the current camera index
|
||||
var camIndexValue = deserializedData.get("cameraIndex");
|
||||
Integer cameraIndex = null;
|
||||
if (camIndexValue instanceof Integer) {
|
||||
cameraIndex = (Integer) camIndexValue;
|
||||
deserializedData.remove("cameraIndex");
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Object> entry : deserializedData.entrySet()) {
|
||||
try {
|
||||
var entryKey = entry.getKey();
|
||||
var entryValue = entry.getValue();
|
||||
var socketMessageType = SocketMessageType.fromEntryKey(entryKey);
|
||||
|
||||
logger.debug(
|
||||
"Got WS message: ["
|
||||
+ socketMessageType
|
||||
+ "] ==> ["
|
||||
+ entryKey
|
||||
+ "], ["
|
||||
+ entryValue
|
||||
+ "]");
|
||||
|
||||
if (socketMessageType == null) {
|
||||
logger.warn("Got unknown socket message type: " + entryKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (socketMessageType) {
|
||||
case SMT_DRIVERMODE:
|
||||
{
|
||||
// TODO: what is this event?
|
||||
var data = (HashMap<String, Object>) entryValue;
|
||||
var dmExpEvent =
|
||||
new IncomingWebSocketEvent<Integer>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE, "driverExposure", data);
|
||||
var dmBrightEvent =
|
||||
new IncomingWebSocketEvent<Integer>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE, "driverBrightness", data);
|
||||
var dmIsDriverEvent =
|
||||
new IncomingWebSocketEvent<Boolean>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE, "isDriver", data);
|
||||
|
||||
dcService.publishEvents(dmExpEvent, dmBrightEvent, dmIsDriverEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGECAMERANAME:
|
||||
{
|
||||
var ccnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"cameraNickname",
|
||||
(String) entryValue,
|
||||
cameraIndex);
|
||||
dcService.publishEvent(ccnEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGEPIPELINENAME:
|
||||
{
|
||||
var cpnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"pipelineName",
|
||||
(String) entryValue,
|
||||
cameraIndex);
|
||||
dcService.publishEvent(cpnEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_ADDNEWPIPELINE:
|
||||
{
|
||||
// HashMap<String, Object> data = (HashMap<String,
|
||||
// Object>) entryValue;
|
||||
// var type = (PipelineType)
|
||||
// data.get("pipelineType");
|
||||
// var name = (String) data.get("pipelineName");
|
||||
var type = PipelineType.Reflective;
|
||||
var name = (String) entryValue;
|
||||
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"newPipelineInfo",
|
||||
Pair.of(name, type),
|
||||
cameraIndex);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_COMMAND:
|
||||
{
|
||||
var cmd = SocketMessageCommandType.fromEntryKey((String) entryValue);
|
||||
switch (cmd) {
|
||||
case SMCT_DELETECURRENTPIPELINE:
|
||||
{
|
||||
var deleteCurrentPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"deleteCurrPipeline",
|
||||
0,
|
||||
cameraIndex);
|
||||
dcService.publishEvent(deleteCurrentPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMCT_SAVE:
|
||||
{
|
||||
var saveEvent =
|
||||
new IncomingWebSocketEvent<>(DataChangeDestination.DCD_OTHER, "save", 0);
|
||||
dcService.publishEvent(saveEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SMT_CURRENTCAMERA:
|
||||
{
|
||||
var changeCurrentCameraEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_OTHER, "changeUICamera", (Integer) entryValue);
|
||||
dcService.publishEvent(changeCurrentCameraEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CURRENTPIPELINE:
|
||||
{
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipeline",
|
||||
(Integer) entryValue,
|
||||
cameraIndex);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_ISPNPCALIBRATION:
|
||||
{
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipeline",
|
||||
PipelineManager.CAL_3D_INDEX,
|
||||
cameraIndex);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_TAKECALIBRATIONSNAPSHOT:
|
||||
{
|
||||
var takeCalSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE, "takeCalSnapshot", 0, cameraIndex);
|
||||
dcService.publishEvent(takeCalSnapshotEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_PIPELINESETTINGCHANGE:
|
||||
{
|
||||
HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
|
||||
|
||||
if (data.size() >= 2) {
|
||||
var cameraIndex2 = (int) data.get("cameraIndex");
|
||||
for (var dataEntry : data.entrySet()) {
|
||||
if (dataEntry.getKey().equals("cameraIndex")) {
|
||||
continue;
|
||||
}
|
||||
var pipelineSettingChangeEvent =
|
||||
new IncomingWebSocketEvent(
|
||||
DataChangeDestination.DCD_ACTIVEPIPELINESETTINGS,
|
||||
dataEntry.getKey(),
|
||||
dataEntry.getValue(),
|
||||
cameraIndex2);
|
||||
dcService.publishEvent(pipelineSettingChangeEvent);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unknown message for PSC: " + data.keySet().iterator().next());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("unknown booboo");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// TODO: log
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendMessage(Object message, WsContext user) throws JsonProcessingException {
|
||||
// TODO: change to use the DataFlow system
|
||||
private void sendMessage(Object message, WsContext user) throws JsonProcessingException {
|
||||
ByteBuffer b = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message));
|
||||
user.send(b);
|
||||
}
|
||||
|
||||
public static void broadcastMessage(Object message, WsContext userToSkip)
|
||||
// TODO: change to use the DataFlow system
|
||||
public void broadcastMessage(Object message, WsContext userToSkip)
|
||||
throws JsonProcessingException {
|
||||
for (WsContext user : users) {
|
||||
if (user != userToSkip) {
|
||||
sendMessage(message, user);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public static void broadcastMessage(Object message) throws JsonProcessingException {
|
||||
broadcastMessage(message, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.server;
|
||||
|
||||
public enum SocketMessageCommandType {
|
||||
SMCT_DELETECURRENTPIPELINE("deleteCurrentPipeline"),
|
||||
SMCT_SAVE("save");
|
||||
|
||||
public final String entryValue;
|
||||
|
||||
SocketMessageCommandType(String entryValue) {
|
||||
this.entryValue = entryValue;
|
||||
}
|
||||
|
||||
public static SocketMessageCommandType fromEntryKey(String entryValue) {
|
||||
if (entryValue.equalsIgnoreCase(SMCT_SAVE.entryValue)) return SMCT_SAVE;
|
||||
else return SMCT_DELETECURRENTPIPELINE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.server;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public enum SocketMessageType {
|
||||
SMT_DRIVERMODE("driverMode"),
|
||||
SMT_CHANGECAMERANAME("changeCameraName"),
|
||||
SMT_CHANGEPIPELINENAME("changePipelineName"),
|
||||
SMT_ADDNEWPIPELINE("addNewPipeline"),
|
||||
SMT_COMMAND("command"),
|
||||
SMT_CURRENTCAMERA("currentCamera"),
|
||||
SMT_PIPELINESETTINGCHANGE("changePipelineSetting"),
|
||||
SMT_CURRENTPIPELINE("currentPipeline"),
|
||||
SMT_ISPNPCALIBRATION("isPNPCalibration"),
|
||||
SMT_TAKECALIBRATIONSNAPSHOT("takeCalibrationSnapshot");
|
||||
|
||||
public final String entryKey;
|
||||
|
||||
SocketMessageType(String entryKey) {
|
||||
this.entryKey = entryKey;
|
||||
}
|
||||
|
||||
private static final Map<String, SocketMessageType> entryKeyToValueMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (var value : EnumSet.allOf(SocketMessageType.class)) {
|
||||
entryKeyToValueMap.put(value.entryKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static SocketMessageType fromEntryKey(String entryKey) {
|
||||
return entryKeyToValueMap.get(entryKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.server;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.DataChangeSource;
|
||||
import org.photonvision.common.dataflow.DataChangeSubscriber;
|
||||
import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
|
||||
public class UIInboundSubscriber extends DataChangeSubscriber {
|
||||
|
||||
public UIInboundSubscriber() {
|
||||
super(
|
||||
Collections.singletonList(DataChangeSource.DCS_WEBSOCKET),
|
||||
Collections.singletonList(DataChangeDestination.DCD_GENSETTINGS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent event) {
|
||||
if (event instanceof IncomingWebSocketEvent) {
|
||||
var incomingWSEvent = (IncomingWebSocketEvent) event;
|
||||
if (incomingWSEvent.propertyName.equals("userConnected")
|
||||
|| incomingWSEvent.propertyName.equals("sendFullSettings")) {
|
||||
// Send full settings
|
||||
var settings = ConfigManager.getInstance().getConfig().toHashMap();
|
||||
var message = new OutgoingUIEvent<>(UIUpdateType.BROADCAST, "fullsettings", settings);
|
||||
DataChangeService.getInstance().publishEvent(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.server;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
import org.photonvision.common.dataflow.DataChangeSource;
|
||||
import org.photonvision.common.dataflow.DataChangeSubscriber;
|
||||
import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
/*
|
||||
* DO NOT use logging in this class. If you do, the logs will recuse forever!
|
||||
*/
|
||||
class UIOutboundSubscriber extends DataChangeSubscriber {
|
||||
Logger logger = new Logger(UIOutboundSubscriber.class, LogGroup.Server);
|
||||
|
||||
private final SocketHandler socketHandler;
|
||||
|
||||
public UIOutboundSubscriber(SocketHandler socketHandler) {
|
||||
super(DataChangeSource.AllSources, Collections.singletonList(DataChangeDestination.DCD_UI));
|
||||
this.socketHandler = socketHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent event) {
|
||||
if (event instanceof OutgoingUIEvent) {
|
||||
var thisEvent = (OutgoingUIEvent) event;
|
||||
try {
|
||||
switch (thisEvent.updateType) {
|
||||
case BROADCAST:
|
||||
{
|
||||
// logger.debug("Broadcasting message");
|
||||
if (event.data instanceof HashMap) {
|
||||
var data = (HashMap) event.data;
|
||||
socketHandler.broadcastMessage(data, null);
|
||||
} else {
|
||||
socketHandler.broadcastMessage(event.data, null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SINGLEUSER:
|
||||
{
|
||||
// logger.debug("Sending single user message");
|
||||
if (event.data instanceof Pair) {
|
||||
var pair = (SocketHandler.SelectiveBroadcastPair) event.data;
|
||||
socketHandler.broadcastMessage(pair.getLeft(), pair.getRight());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
// TODO: Log
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.server;
|
||||
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
|
||||
public class UISettings {
|
||||
public NetworkConfig networkConfig;
|
||||
public CameraConfiguration currentCameraConfiguration;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.server;
|
||||
|
||||
public enum UIUpdateType {
|
||||
BROADCAST,
|
||||
SINGLEUSER
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
public enum CameraQuirk {
|
||||
/** Camera settable for controllable image gain */
|
||||
Gain,
|
||||
/** For cameras that need a bit of encouragement for settings to stick */
|
||||
DoubleSet
|
||||
}
|
||||
@@ -17,36 +17,65 @@
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class QuirkyCamera {
|
||||
public static final List<QuirkyCamera> quirkyCameras =
|
||||
private static final List<QuirkyCamera> quirkyCameras =
|
||||
List.of(
|
||||
// ps3 eye
|
||||
new QuirkyCamera(0x1415, 0x2000, "PS3Eye", List.of(CameraQuirks.Gain)));
|
||||
new QuirkyCamera(0x1415, 0x2000, "PS3Eye", CameraQuirk.Gain),
|
||||
new QuirkyCamera(0x72E, 0x45D, "LifeCam VX-5500", CameraQuirk.DoubleSet));
|
||||
|
||||
public final int usbVid;
|
||||
public final int usbPid;
|
||||
public final String name;
|
||||
public List<CameraQuirks> quirks;
|
||||
public final String baseName;
|
||||
public final HashMap<CameraQuirk, Boolean> quirks;
|
||||
|
||||
public QuirkyCamera(int usbVid, int usbPid, String baseName, List<CameraQuirks> quirks) {
|
||||
private QuirkyCamera(int usbVid, int usbPid, String baseName, CameraQuirk quirk) {
|
||||
this(usbVid, usbPid, baseName, List.of(quirk));
|
||||
}
|
||||
|
||||
private QuirkyCamera(int usbVid, int usbPid, String baseName, List<CameraQuirk> quirks) {
|
||||
this.usbVid = usbVid;
|
||||
this.usbPid = usbPid;
|
||||
this.name = baseName;
|
||||
this.quirks = quirks;
|
||||
}
|
||||
this.baseName = baseName;
|
||||
|
||||
public QuirkyCamera(int usbVid, int usbPid, String baseName) {
|
||||
this(usbVid, usbPid, baseName, new ArrayList<>());
|
||||
QuirkyCamera quirky =
|
||||
quirkyCameras.stream()
|
||||
.filter(quirkyCamera -> quirkyCamera.usbPid == usbPid && quirkyCamera.usbVid == usbVid)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (quirky != null) {
|
||||
this.quirks = quirky.quirks;
|
||||
this.quirks = new HashMap<>();
|
||||
for (var q : quirks) {
|
||||
this.quirks.put(q, true);
|
||||
}
|
||||
for (var q : CameraQuirk.values()) {
|
||||
this.quirks.putIfAbsent(q, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static QuirkyCamera getQuirkyCamera(int usbVid, int usbPid, String baseName) {
|
||||
for (var qc : quirkyCameras) {
|
||||
if (qc.usbVid == usbVid && qc.usbPid == usbPid) {
|
||||
return qc;
|
||||
}
|
||||
}
|
||||
return new QuirkyCamera(usbVid, usbPid, baseName, List.of());
|
||||
}
|
||||
|
||||
public boolean hasQuirk(CameraQuirk quirk) {
|
||||
return quirks.get(quirk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
QuirkyCamera that = (QuirkyCamera) o;
|
||||
return usbVid == that.usbVid
|
||||
&& usbPid == that.usbPid
|
||||
&& Objects.equals(baseName, that.baseName)
|
||||
&& Objects.equals(quirks, that.quirks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(usbVid, usbPid, baseName, quirks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,47 +23,34 @@ import edu.wpi.cscore.VideoMode;
|
||||
import edu.wpi.first.cameraserver.CameraServer;
|
||||
import java.util.*;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.frame.provider.USBFrameProvider;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
public class USBCameraSource implements VisionSource {
|
||||
private final Logger logger;
|
||||
private final UsbCamera camera;
|
||||
private final USBCameraSettables usbCameraSettables;
|
||||
private final USBFrameProvider usbFrameProvider;
|
||||
private final CameraConfiguration configuration;
|
||||
public final CameraConfiguration configuration;
|
||||
private final CvSink cvSink;
|
||||
|
||||
private final QuirkyCamera cameraQuirks;
|
||||
|
||||
public USBCameraSource(CameraConfiguration config) {
|
||||
this.configuration = config;
|
||||
this.camera = new UsbCamera(config.nickname, config.path);
|
||||
this.cameraQuirks =
|
||||
new QuirkyCamera(camera.getInfo().productId, camera.getInfo().vendorId, config.baseName);
|
||||
CvSink cvSink = CameraServer.getInstance().getVideo(this.camera);
|
||||
this.usbCameraSettables = new USBCameraSettables(config);
|
||||
this.usbFrameProvider =
|
||||
new USBFrameProvider(cvSink, usbCameraSettables.getFrameStaticProperties());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj.getClass() != USBCameraSource.class) {
|
||||
return false;
|
||||
}
|
||||
USBCameraSource tmp = (USBCameraSource) obj;
|
||||
boolean i = this.cameraQuirks.quirks.equals(tmp.cameraQuirks.quirks);
|
||||
|
||||
boolean r = this.configuration.uniqueName.equals(tmp.configuration.uniqueName);
|
||||
boolean c = this.configuration.baseName.equals(tmp.configuration.baseName);
|
||||
boolean j = this.configuration.nickname.equals(tmp.configuration.nickname);
|
||||
|
||||
boolean k = this.camera.getInfo().name.equals(tmp.camera.getInfo().name);
|
||||
boolean x = this.camera.getInfo().productId == tmp.camera.getInfo().productId;
|
||||
boolean y = this.camera.getInfo().vendorId == tmp.camera.getInfo().vendorId;
|
||||
var t = i && r && c && j && k && x && y;
|
||||
return t;
|
||||
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
|
||||
configuration = config;
|
||||
camera = new UsbCamera(config.nickname, config.path);
|
||||
cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(
|
||||
camera.getInfo().productId, camera.getInfo().vendorId, config.baseName);
|
||||
cvSink = CameraServer.getInstance().getVideo(this.camera);
|
||||
usbCameraSettables = new USBCameraSettables(config);
|
||||
usbFrameProvider = new USBFrameProvider(cvSink, usbCameraSettables.getFrameStaticProperties());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -79,6 +66,9 @@ public class USBCameraSource implements VisionSource {
|
||||
public class USBCameraSettables extends VisionSourceSettables {
|
||||
protected USBCameraSettables(CameraConfiguration configuration) {
|
||||
super(configuration);
|
||||
getAllVideoModes();
|
||||
setCurrentVideoMode(videoModes.get(0));
|
||||
frameStaticProperties = new FrameStaticProperties(getCurrentVideoMode(), getFOV());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,6 +79,7 @@ public class USBCameraSource implements VisionSource {
|
||||
@Override
|
||||
public void setExposure(int exposure) {
|
||||
camera.setExposureManual(exposure);
|
||||
camera.setExposureManual(exposure);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,16 +90,17 @@ public class USBCameraSource implements VisionSource {
|
||||
@Override
|
||||
public void setBrightness(int brightness) {
|
||||
camera.setBrightness(brightness);
|
||||
camera.setBrightness(brightness);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGain() {
|
||||
return camera.getProperty("gain").get();
|
||||
return !cameraQuirks.hasQuirk(CameraQuirk.Gain) ? -1 : camera.getProperty("gain").get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGain(int gain) {
|
||||
if (cameraQuirks.quirks.contains(CameraQuirks.Gain)) {
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
|
||||
camera.getProperty("gain_automatic").set(0);
|
||||
camera.getProperty("gain").set(gain);
|
||||
}
|
||||
@@ -116,19 +108,31 @@ public class USBCameraSource implements VisionSource {
|
||||
|
||||
@Override
|
||||
public VideoMode getCurrentVideoMode() {
|
||||
return camera.getVideoMode();
|
||||
return camera.isConnected() ? camera.getVideoMode() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentVideoMode(VideoMode videoMode) {
|
||||
if (videoMode == null) {
|
||||
logger.error("Got a null video mode! Doing nothing...");
|
||||
return;
|
||||
}
|
||||
camera.setVideoMode(videoMode);
|
||||
this.frameStaticProperties = new FrameStaticProperties(getCurrentVideoMode(), getFOV());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Integer, VideoMode> getAllVideoModes() {
|
||||
if (videoModes == null) {
|
||||
videoModes = new HashMap<>();
|
||||
List<VideoMode> videoModesList = Arrays.asList(camera.enumerateVideoModes());
|
||||
List<VideoMode> videoModesList;
|
||||
try {
|
||||
videoModesList = Arrays.asList(camera.enumerateVideoModes());
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while enumerating video modes!");
|
||||
e.printStackTrace();
|
||||
videoModesList = List.of();
|
||||
}
|
||||
for (VideoMode videoMode : videoModesList) {
|
||||
videoModes.put(videoModesList.indexOf(videoMode), videoMode);
|
||||
}
|
||||
@@ -136,4 +140,18 @@ public class USBCameraSource implements VisionSource {
|
||||
return videoModes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
USBCameraSource that = (USBCameraSource) o;
|
||||
return cameraQuirks.equals(that.cameraQuirks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(
|
||||
camera, usbCameraSettables, usbFrameProvider, configuration, cvSink, cameraQuirks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class FrameStaticProperties {
|
||||
* @param fov The fov of the image.
|
||||
*/
|
||||
public FrameStaticProperties(VideoMode mode, double fov) {
|
||||
this(mode.width, mode.height, fov);
|
||||
this(mode != null ? mode.width : 1, mode != null ? mode.height : 1, fov);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,13 +17,57 @@
|
||||
|
||||
package org.photonvision.vision.frame.consumer;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import edu.wpi.cscore.CvSource;
|
||||
import edu.wpi.cscore.MjpegServer;
|
||||
import edu.wpi.cscore.VideoMode;
|
||||
import edu.wpi.first.cameraserver.CameraServer;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.Size;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameConsumer;
|
||||
import org.photonvision.vision.frame.FrameDivisor;
|
||||
|
||||
public class MJPGFrameConsumer implements FrameConsumer {
|
||||
|
||||
private final CvSource cvSource;
|
||||
private final MjpegServer mjpegServer;
|
||||
private FrameDivisor divisor = FrameDivisor.NONE;
|
||||
|
||||
public MJPGFrameConsumer(String sourceName, int width, int height) {
|
||||
// TODO h264?
|
||||
this.cvSource = new CvSource(sourceName, VideoMode.PixelFormat.kMJPEG, width, height, 30);
|
||||
this.mjpegServer = CameraServer.getInstance().startAutomaticCapture(cvSource);
|
||||
}
|
||||
|
||||
public MJPGFrameConsumer(String name) {
|
||||
this(name, 320, 240);
|
||||
}
|
||||
|
||||
public void setFrameDivisor(FrameDivisor divisor) {
|
||||
this.divisor = divisor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Frame frame) {
|
||||
throw new NotImplementedException("");
|
||||
if (!frame.image.getMat().empty()) {
|
||||
if (divisor != FrameDivisor.NONE) {
|
||||
var tempMat = new Mat();
|
||||
Imgproc.resize(
|
||||
frame.image.getMat(), tempMat, getScaledSize(frame.image.getMat().size(), divisor));
|
||||
cvSource.putFrame(tempMat);
|
||||
tempMat.release();
|
||||
} else {
|
||||
cvSource.putFrame(frame.image.getMat());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Size getScaledSize(Size orig, FrameDivisor divisor) {
|
||||
return new Size(orig.width / divisor.value, orig.height / divisor.value);
|
||||
}
|
||||
|
||||
public int getCurrentStreamPort() {
|
||||
return mjpegServer.getPort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,20 +24,20 @@ import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
|
||||
public class USBFrameProvider implements FrameProvider {
|
||||
private static int count = 0;
|
||||
private CvSink cvSink;
|
||||
private FrameStaticProperties frameStaticProperties;
|
||||
private CVMat mat;
|
||||
private final CvSink cvSink;
|
||||
private final FrameStaticProperties frameStaticProperties;
|
||||
private final CVMat mat;
|
||||
|
||||
public USBFrameProvider(CvSink sink, FrameStaticProperties frameStaticProperties) {
|
||||
cvSink = sink;
|
||||
cvSink.setEnabled(true);
|
||||
this.frameStaticProperties = frameStaticProperties;
|
||||
mat = new CVMat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Frame get() {
|
||||
if (mat != null && mat.getMat() != null) {
|
||||
if (mat.getMat() != null) {
|
||||
mat.release();
|
||||
}
|
||||
long time = cvSink.grabFrame(mat.getMat());
|
||||
@@ -46,6 +46,6 @@ public class USBFrameProvider implements FrameProvider {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "USBFrameProvider" + count++;
|
||||
return "USBFrameProvider - " + cvSink.getName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,13 @@ import org.apache.commons.math3.util.FastMath;
|
||||
import org.photonvision.vision.target.PotentialTarget;
|
||||
|
||||
public enum ContourSortMode {
|
||||
Largest(Comparator.comparingDouble(PotentialTarget::getArea)),
|
||||
Largest(
|
||||
Comparator.comparingDouble(PotentialTarget::getArea)
|
||||
.reversed()), // reversed so that zero index has the largest size
|
||||
Smallest(Largest.getComparator().reversed()),
|
||||
Highest(Comparator.comparingDouble(rect -> rect.getMinAreaRect().center.y)),
|
||||
Lowest(Highest.getComparator().reversed()),
|
||||
Leftmost(Comparator.comparingDouble(target -> target.getMinAreaRect().center.x)),
|
||||
Leftmost(Comparator.comparingDouble(target -> target.getMinAreaRect().center.x * -1)),
|
||||
Rightmost(Leftmost.getComparator().reversed()),
|
||||
Centermost(
|
||||
Comparator.comparingDouble(
|
||||
|
||||
@@ -35,7 +35,7 @@ public class AdvancedPipelineSettings extends CVPipelineSettings {
|
||||
public IntegerCouple hsvSaturation = new IntegerCouple(50, 255);
|
||||
public IntegerCouple hsvValue = new IntegerCouple(50, 255);
|
||||
|
||||
public boolean outputShowThresholded = false;
|
||||
public boolean outputShowThresholded = true;
|
||||
public boolean outputShowMultipleTargets = false;
|
||||
|
||||
public boolean erode = false;
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelineSettings> {
|
||||
protected S settings;
|
||||
@@ -32,6 +34,7 @@ public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelin
|
||||
return settings;
|
||||
}
|
||||
|
||||
// TODO (BANKS) ACTUALLY SET THE CAMERA RESOLUTION
|
||||
public R run(Frame frame) {
|
||||
long pipelineStartNanos = System.nanoTime();
|
||||
|
||||
@@ -40,6 +43,9 @@ public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelin
|
||||
}
|
||||
setPipeParams(frame.frameStaticProperties, settings);
|
||||
|
||||
if (frame.image.getMat().empty()) {
|
||||
return (R) new CVPipelineResult(0, List.of(), frame);
|
||||
}
|
||||
R result = process(frame, settings);
|
||||
|
||||
result.setLatencyMillis(MathUtils.nanosToMillis(System.nanoTime() - pipelineStartNanos));
|
||||
|
||||
@@ -39,9 +39,9 @@ public class CVPipelineSettings {
|
||||
public ImageFlipMode inputImageFlipMode = ImageFlipMode.NONE;
|
||||
public ImageRotationMode inputImageRotationMode = ImageRotationMode.DEG_0;
|
||||
public String pipelineNickname = "New Pipeline";
|
||||
public double cameraExposure = 50.0;
|
||||
public double cameraBrightness = 50.0;
|
||||
public double cameraGain = 50.0;
|
||||
public int cameraExposure = 50;
|
||||
public int cameraBrightness = 50;
|
||||
public int cameraGain = 50;
|
||||
public int cameraVideoModeIndex = 0;
|
||||
public FrameDivisor inputFrameDivisor = FrameDivisor.NONE;
|
||||
public FrameDivisor outputFrameDivisor = FrameDivisor.NONE;
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.photonvision.vision.pipeline;
|
||||
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.processes.PipelineManager;
|
||||
|
||||
public class Calibration3dPipeline extends CVPipeline<CVPipelineResult, CVPipelineSettings> {
|
||||
|
||||
@@ -26,6 +28,7 @@ public class Calibration3dPipeline extends CVPipeline<CVPipelineResult, CVPipeli
|
||||
|
||||
public Calibration3dPipeline() {
|
||||
settings = new CVPipelineSettings();
|
||||
settings.pipelineIndex = PipelineManager.CAL_3D_INDEX;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.opencv.*;
|
||||
import org.photonvision.vision.pipe.CVPipeResult;
|
||||
import org.photonvision.vision.pipe.impl.*;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.PotentialTarget;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
@@ -64,6 +65,10 @@ public class ColoredShapePipeline
|
||||
settings = new ColoredShapePipelineSettings();
|
||||
}
|
||||
|
||||
public ColoredShapePipeline(ColoredShapePipelineSettings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPipeParams(
|
||||
FrameStaticProperties frameStaticProperties, ColoredShapePipelineSettings settings) {
|
||||
|
||||
@@ -52,7 +52,7 @@ public class ColoredShapePipelineSettings extends AdvancedPipelineSettings {
|
||||
public boolean solvePNPEnabled = false;
|
||||
public CameraCalibrationCoefficients cameraCalibration;
|
||||
public TargetModel targetModel;
|
||||
public Rotation2d cameraPitch = Rotation2d.fromDegrees(0.0);
|
||||
public Rotation2d cameraPitch = Rotation2d.fromDegrees(0.0); // TODO where should pitch live?
|
||||
|
||||
// Corner detection settings
|
||||
public CornerDetectionPipe.DetectionStrategy cornerDetectionStrategy =
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.pipe.impl.Draw2dCrosshairPipe;
|
||||
import org.photonvision.vision.pipe.impl.ResizeImagePipe;
|
||||
import org.photonvision.vision.pipe.impl.RotateImagePipe;
|
||||
import org.photonvision.vision.pipeline.result.DriverModePipelineResult;
|
||||
|
||||
public class DriverModePipeline
|
||||
extends CVPipeline<DriverModePipelineResult, DriverModePipelineSettings> {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.photonvision.vision.pipeline;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import org.photonvision.common.util.numbers.DoubleCouple;
|
||||
import org.photonvision.vision.processes.PipelineManager;
|
||||
import org.photonvision.vision.target.RobotOffsetPointMode;
|
||||
|
||||
@JsonTypeName("DriverModePipelineSettings")
|
||||
@@ -28,6 +29,8 @@ public class DriverModePipelineSettings extends CVPipelineSettings {
|
||||
|
||||
public DriverModePipelineSettings() {
|
||||
super();
|
||||
pipelineNickname = "Driver Mode";
|
||||
pipelineIndex = PipelineManager.DRIVERMODE_INDEX;
|
||||
pipelineType = PipelineType.DriverMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.photonvision.vision.pipe.impl.RotateImagePipe;
|
||||
import org.photonvision.vision.pipe.impl.SolvePNPPipe;
|
||||
import org.photonvision.vision.pipe.impl.SortContoursPipe;
|
||||
import org.photonvision.vision.pipe.impl.SpeckleRejectPipe;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.PotentialTarget;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
@@ -71,6 +72,10 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
settings = new ReflectivePipelineSettings();
|
||||
}
|
||||
|
||||
public ReflectivePipeline(ReflectivePipelineSettings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPipeParams(
|
||||
FrameStaticProperties frameStaticProperties, ReflectivePipelineSettings settings) {
|
||||
@@ -162,6 +167,7 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
|
||||
long sumPipeNanosElapsed = 0L;
|
||||
|
||||
rawInputMat.release();
|
||||
frame.image.getMat().copyTo(rawInputMat);
|
||||
|
||||
CVPipeResult<Mat> rotateImageResult = rotateImagePipe.apply(frame.image.getMat());
|
||||
@@ -187,8 +193,12 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
speckleRejectPipe.apply(findContoursResult.result);
|
||||
sumPipeNanosElapsed += speckleRejectResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<Contour>> filterContoursResult =
|
||||
filterContoursPipe.apply(speckleRejectResult.result);
|
||||
sumPipeNanosElapsed += filterContoursResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<PotentialTarget>> groupContoursResult =
|
||||
groupContoursPipe.apply(speckleRejectResult.result);
|
||||
groupContoursPipe.apply(filterContoursResult.result);
|
||||
sumPipeNanosElapsed += groupContoursResult.nanosElapsed;
|
||||
|
||||
CVPipeResult<List<PotentialTarget>> sortContoursResult =
|
||||
@@ -234,11 +244,6 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
|
||||
result = draw2dContoursResult;
|
||||
}
|
||||
|
||||
// TODO: better way?
|
||||
if (settings.outputShowThresholded) {
|
||||
rawInputMat.release();
|
||||
}
|
||||
|
||||
// TODO: Implement all the things
|
||||
return new CVPipelineResult(
|
||||
MathUtils.nanosToMillis(sumPipeNanosElapsed),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipeline.result;
|
||||
|
||||
@SuppressWarnings("PointlessBitwiseExpression")
|
||||
public abstract class BytePackable {
|
||||
public abstract byte[] toByteArray();
|
||||
|
||||
public abstract void fromByteArray(byte[] src);
|
||||
|
||||
protected int bufferPosition = 0;
|
||||
|
||||
public int getBufferPosition() {
|
||||
return bufferPosition;
|
||||
}
|
||||
|
||||
public void resetBufferPosition() {
|
||||
bufferPosition = 0;
|
||||
}
|
||||
|
||||
protected void bufferData(byte[] src, byte[] dest) {
|
||||
System.arraycopy(src, 0, dest, bufferPosition, src.length);
|
||||
bufferPosition += src.length;
|
||||
}
|
||||
|
||||
protected void bufferData(byte src, byte[] dest) {
|
||||
System.arraycopy(new byte[] {src}, 0, dest, bufferPosition, 1);
|
||||
bufferPosition++;
|
||||
}
|
||||
|
||||
protected void bufferData(int src, byte[] dest) {
|
||||
System.arraycopy(toBytes(src), 0, dest, bufferPosition, Integer.BYTES);
|
||||
bufferPosition += Integer.BYTES;
|
||||
}
|
||||
|
||||
protected void bufferData(double src, byte[] dest) {
|
||||
System.arraycopy(toBytes(src), 0, dest, bufferPosition, Double.BYTES);
|
||||
bufferPosition += Double.BYTES;
|
||||
}
|
||||
|
||||
protected void bufferData(boolean src, byte[] dest) {
|
||||
System.arraycopy(toBytes(src), 0, dest, bufferPosition, 1);
|
||||
bufferPosition += 1;
|
||||
}
|
||||
|
||||
protected boolean unbufferBoolean(byte[] src) {
|
||||
return toBoolean(src, bufferPosition++);
|
||||
}
|
||||
|
||||
protected byte unbufferByte(byte[] src) {
|
||||
var value = src[bufferPosition];
|
||||
bufferPosition++;
|
||||
return value;
|
||||
}
|
||||
|
||||
protected byte[] unbufferBytes(byte[] src, int len) {
|
||||
var bytes = new byte[len];
|
||||
System.arraycopy(src, bufferPosition, bytes, 0, len);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
protected int unbufferInt(byte[] src) {
|
||||
var value = toInt(src, bufferPosition);
|
||||
bufferPosition += Integer.BYTES;
|
||||
return value;
|
||||
}
|
||||
|
||||
protected double unbufferDouble(byte[] src) {
|
||||
var value = toDouble(src, bufferPosition);
|
||||
bufferPosition += Double.BYTES;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean toBoolean(byte[] src, int pos) {
|
||||
return src[pos] != 0;
|
||||
}
|
||||
|
||||
private static int toInt(byte[] src, int pos) {
|
||||
return (0xff & src[pos]) << 24
|
||||
| (0xff & src[pos + 1]) << 16
|
||||
| (0xff & src[pos + 2]) << 8
|
||||
| (0xff & src[pos + 3]) << 0;
|
||||
}
|
||||
|
||||
private static long toLong(byte[] src, int pos) {
|
||||
return (long) (0xff & src[pos]) << 56
|
||||
| (long) (0xff & src[pos + 1]) << 48
|
||||
| (long) (0xff & src[pos + 2]) << 40
|
||||
| (long) (0xff & src[pos + 3]) << 32
|
||||
| (long) (0xff & src[pos + 4]) << 24
|
||||
| (long) (0xff & src[pos + 5]) << 16
|
||||
| (long) (0xff & src[pos + 6]) << 8
|
||||
| (long) (0xff & src[pos + 7]) << 0;
|
||||
}
|
||||
|
||||
private static double toDouble(byte[] src, int pos) {
|
||||
return Double.longBitsToDouble(toLong(src, pos));
|
||||
}
|
||||
|
||||
protected byte[] toBytes(double value) {
|
||||
long data = Double.doubleToRawLongBits(value);
|
||||
return new byte[] {
|
||||
(byte) ((data >> 56) & 0xff),
|
||||
(byte) ((data >> 48) & 0xff),
|
||||
(byte) ((data >> 40) & 0xff),
|
||||
(byte) ((data >> 32) & 0xff),
|
||||
(byte) ((data >> 24) & 0xff),
|
||||
(byte) ((data >> 16) & 0xff),
|
||||
(byte) ((data >> 8) & 0xff),
|
||||
(byte) ((data >> 0) & 0xff),
|
||||
};
|
||||
}
|
||||
|
||||
protected byte[] toBytes(int value) {
|
||||
return new byte[] {
|
||||
(byte) ((value >> 24) & 0xff),
|
||||
(byte) ((value >> 16) & 0xff),
|
||||
(byte) ((value >> 8) & 0xff),
|
||||
(byte) ((value >> 0) & 0xff)
|
||||
};
|
||||
}
|
||||
|
||||
protected byte[] toBytes(boolean value) {
|
||||
return new byte[] {(byte) (value ? 1 : 0)};
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
package org.photonvision.vision.pipeline.result;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
@@ -50,7 +50,7 @@ public class CVPipelineResult implements Releasable {
|
||||
return latencyMillis;
|
||||
}
|
||||
|
||||
protected void setLatencyMillis(double latencyMillis) {
|
||||
public void setLatencyMillis(double latencyMillis) {
|
||||
this.latencyMillis = latencyMillis;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
package org.photonvision.vision.pipeline.result;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipeline.result;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
public class SimplePipelineResult extends BytePackable {
|
||||
|
||||
private double latencyMillis;
|
||||
private boolean hasTargets;
|
||||
public final List<SimpleTrackedTarget> targets = new ArrayList<>();
|
||||
|
||||
public SimplePipelineResult() {}
|
||||
|
||||
public SimplePipelineResult(
|
||||
double latencyMillis, boolean hasTargets, List<SimpleTrackedTarget> targets) {
|
||||
this.latencyMillis = latencyMillis;
|
||||
this.hasTargets = hasTargets;
|
||||
this.targets.addAll(targets);
|
||||
}
|
||||
|
||||
public SimplePipelineResult(CVPipelineResult r) {
|
||||
this(r.processingMillis, r.hasTargets(), simpleFromTrackedTargets(r.targets));
|
||||
}
|
||||
|
||||
public double getLatencyMillis() {
|
||||
return latencyMillis;
|
||||
}
|
||||
|
||||
public boolean hasTargets() {
|
||||
return hasTargets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SimplePipelineResult that = (SimplePipelineResult) o;
|
||||
boolean latencyMatch = Double.compare(that.latencyMillis, latencyMillis) == 0;
|
||||
boolean hasTargetsMatch = that.hasTargets == hasTargets;
|
||||
boolean targetsMatch = that.targets.equals(targets);
|
||||
return latencyMatch && hasTargetsMatch && targetsMatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(latencyMillis, hasTargets, targets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
bufferPosition = 0;
|
||||
int bufferSize =
|
||||
8 + 1 + 1 + (targets.size() * 48); // latencyMillis + hasTargets + targetCount + targets
|
||||
var buff = new byte[bufferSize];
|
||||
|
||||
bufferData(latencyMillis, buff);
|
||||
bufferData(hasTargets, buff);
|
||||
bufferData((byte) targets.size(), buff);
|
||||
for (var target : targets) {
|
||||
bufferData(target.toByteArray(), buff);
|
||||
}
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromByteArray(byte[] src) {
|
||||
bufferPosition = 0;
|
||||
|
||||
latencyMillis = unbufferDouble(src);
|
||||
hasTargets = unbufferBoolean(src);
|
||||
byte targetCount = unbufferByte(src);
|
||||
|
||||
targets.clear();
|
||||
for (int i = 0; i < targetCount; i++) {
|
||||
var target = new SimpleTrackedTarget();
|
||||
target.fromByteArray(unbufferBytes(src, 48));
|
||||
bufferPosition += 48;
|
||||
targets.add(target);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SimpleTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
|
||||
var ret = new ArrayList<SimpleTrackedTarget>();
|
||||
for (var t : targets) {
|
||||
ret.add(new SimpleTrackedTarget(t));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipeline.result;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import java.util.Objects;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
public class SimpleTrackedTarget extends BytePackable {
|
||||
public static final int PACK_SIZE_BYTES = Double.BYTES * 6;
|
||||
|
||||
private double yaw;
|
||||
private double pitch;
|
||||
private double area;
|
||||
private Pose2d robotRelativePose = new Pose2d();
|
||||
|
||||
public SimpleTrackedTarget() {}
|
||||
|
||||
public SimpleTrackedTarget(double yaw, double pitch, double area, Pose2d pose) {
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
this.area = area;
|
||||
robotRelativePose = pose;
|
||||
}
|
||||
|
||||
public SimpleTrackedTarget(TrackedTarget t) {
|
||||
this(t.getYaw(), t.getPitch(), t.getArea(), t.getRobotRelativePose());
|
||||
}
|
||||
|
||||
public double getYaw() {
|
||||
return yaw;
|
||||
}
|
||||
|
||||
public double getPitch() {
|
||||
return pitch;
|
||||
}
|
||||
|
||||
public double getArea() {
|
||||
return area;
|
||||
}
|
||||
|
||||
public Pose2d getRobotRelativePose() {
|
||||
return robotRelativePose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SimpleTrackedTarget that = (SimpleTrackedTarget) o;
|
||||
return Double.compare(that.yaw, yaw) == 0
|
||||
&& Double.compare(that.pitch, pitch) == 0
|
||||
&& Double.compare(that.area, area) == 0
|
||||
&& Objects.equals(robotRelativePose, that.robotRelativePose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(yaw, pitch, area, robotRelativePose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
resetBufferPosition();
|
||||
byte[] data = new byte[PACK_SIZE_BYTES];
|
||||
|
||||
bufferData(yaw, data);
|
||||
bufferData(pitch, data);
|
||||
bufferData(area, data);
|
||||
if (robotRelativePose != null) {
|
||||
bufferData(robotRelativePose.getTranslation().getX(), data);
|
||||
bufferData(robotRelativePose.getTranslation().getY(), data);
|
||||
bufferData(robotRelativePose.getRotation().getDegrees(), data);
|
||||
} else {
|
||||
bufferData((double) 0, data);
|
||||
bufferData((double) 0, data);
|
||||
bufferData((double) 0, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromByteArray(byte[] src) {
|
||||
resetBufferPosition();
|
||||
if (src.length < PACK_SIZE_BYTES) {
|
||||
// TODO: log error?
|
||||
return;
|
||||
}
|
||||
|
||||
yaw = unbufferDouble(src);
|
||||
pitch = unbufferDouble(src);
|
||||
area = unbufferDouble(src);
|
||||
|
||||
var poseX = unbufferDouble(src);
|
||||
var poseY = unbufferDouble(src);
|
||||
var poseR = unbufferDouble(src);
|
||||
robotRelativePose = new Pose2d(poseX, poseY, Rotation2d.fromDegrees(poseR));
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,15 @@
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import org.photonvision.vision.pipeline.CVPipelineResult;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
// TODO replace with CTT's data class
|
||||
public class Data {
|
||||
public class Data implements Releasable {
|
||||
public CVPipelineResult result;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
result.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,26 +17,30 @@
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.pipeline.CVPipeline;
|
||||
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.Calibration3dPipeline;
|
||||
import org.photonvision.vision.pipeline.DriverModePipeline;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.pipeline.*;
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unused"})
|
||||
public class PipelineManager {
|
||||
private static final Logger logger = new Logger(PipelineManager.class, LogGroup.VisionProcess);
|
||||
|
||||
private static final int DRIVERMODE_INDEX = -1;
|
||||
private static final int CAL_3D_INDEX = -2;
|
||||
public static final int DRIVERMODE_INDEX = -1;
|
||||
public static final int CAL_3D_INDEX = -2;
|
||||
|
||||
public final List<CVPipeline> userPipelines;
|
||||
private final Calibration3dPipeline calibration3dPipeline = new Calibration3dPipeline();
|
||||
private final DriverModePipeline driverModePipeline = new DriverModePipeline();
|
||||
protected final List<CVPipelineSettings> userPipelineSettings;
|
||||
protected final Calibration3dPipeline calibration3dPipeline = new Calibration3dPipeline();
|
||||
protected final DriverModePipeline driverModePipeline = new DriverModePipeline();
|
||||
|
||||
/** Index of the currently active pipeline. */
|
||||
private int currentPipelineIndex = DRIVERMODE_INDEX;
|
||||
|
||||
/** The currently active pipeline. */
|
||||
private CVPipeline currentPipeline = driverModePipeline;
|
||||
|
||||
/**
|
||||
* Index of the last active user-created pipeline. <br>
|
||||
* <br>
|
||||
@@ -50,32 +54,8 @@ public class PipelineManager {
|
||||
*
|
||||
* @param userPipelines Pipelines to add to the manager.
|
||||
*/
|
||||
public PipelineManager(List<CVPipeline> userPipelines) {
|
||||
this.userPipelines = userPipelines;
|
||||
}
|
||||
|
||||
/** Creates a PipelineManager with a DriverModePipeline, and a Calibration3dPipeline. */
|
||||
public PipelineManager() {
|
||||
this(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pipeline by index.
|
||||
*
|
||||
* @param index Index of desired pipeline.
|
||||
* @return The desired pipeline.
|
||||
*/
|
||||
public CVPipeline getPipeline(int index) {
|
||||
if (index < 0) {
|
||||
switch (index) {
|
||||
case DRIVERMODE_INDEX:
|
||||
return driverModePipeline;
|
||||
case CAL_3D_INDEX:
|
||||
return calibration3dPipeline;
|
||||
}
|
||||
}
|
||||
|
||||
return userPipelines.get(index);
|
||||
public PipelineManager(List<CVPipelineSettings> userPipelines) {
|
||||
this.userPipelineSettings = userPipelines;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +65,41 @@ public class PipelineManager {
|
||||
* @return The gotten settings of the pipeline whose index was provided.
|
||||
*/
|
||||
public CVPipelineSettings getPipelineSettings(int index) {
|
||||
return getPipeline(index).getSettings();
|
||||
if (index < 0) {
|
||||
switch (index) {
|
||||
case DRIVERMODE_INDEX:
|
||||
return driverModePipeline.getSettings();
|
||||
case CAL_3D_INDEX:
|
||||
return calibration3dPipeline.getSettings();
|
||||
}
|
||||
}
|
||||
|
||||
for (var setting : userPipelineSettings) {
|
||||
if (setting.pipelineIndex == index) return setting;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of nicknames for all user pipelines
|
||||
*
|
||||
* @return The list of nicknames for all user pipelines
|
||||
*/
|
||||
public List<String> getPipelineNicknames() {
|
||||
List<String> ret = new ArrayList<>();
|
||||
for (var p : userPipelineSettings) {
|
||||
ret.add(p.pipelineNickname);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index of the currently active pipeline
|
||||
*
|
||||
* @return The index of the currently active pipeline
|
||||
*/
|
||||
public int getCurrentPipelineIndex() {
|
||||
return currentPipelineIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +108,30 @@ public class PipelineManager {
|
||||
* @return The currently active pipeline.
|
||||
*/
|
||||
public CVPipeline getCurrentPipeline() {
|
||||
return getPipeline(currentPipelineIndex);
|
||||
if (currentPipelineIndex < 0) {
|
||||
switch (currentPipelineIndex) {
|
||||
case CAL_3D_INDEX:
|
||||
return calibration3dPipeline;
|
||||
case DRIVERMODE_INDEX:
|
||||
return driverModePipeline;
|
||||
}
|
||||
}
|
||||
|
||||
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
|
||||
if (currentPipeline.getSettings().pipelineIndex != desiredPipelineSettings.pipelineIndex) {
|
||||
switch (desiredPipelineSettings.pipelineType) {
|
||||
case Reflective:
|
||||
currentPipeline =
|
||||
new ReflectivePipeline((ReflectivePipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case ColoredShape:
|
||||
currentPipeline =
|
||||
new ColoredShapePipeline((ColoredShapePipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return currentPipeline;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,20 +170,7 @@ public class PipelineManager {
|
||||
}
|
||||
|
||||
public static final Comparator<CVPipelineSettings> PipelineSettingsIndexComparator =
|
||||
(o1, o2) -> {
|
||||
int o1Index = o1.pipelineIndex;
|
||||
int o2Index = o2.pipelineIndex;
|
||||
|
||||
if (o1Index == o2Index) {
|
||||
return 0;
|
||||
} else if (o1Index < o2Index) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
public static final Comparator<CVPipeline> PipelineIndexComparator =
|
||||
(o1, o2) -> PipelineSettingsIndexComparator.compare(o1.getSettings(), o2.getSettings());
|
||||
Comparator.comparingInt(o -> o.pipelineIndex);
|
||||
|
||||
/**
|
||||
* Sorts the pipeline list by index, and reassigns their indexes to match the new order. <br>
|
||||
@@ -154,11 +178,53 @@ public class PipelineManager {
|
||||
* I don't like this but I have no other ideas, and it works so ¯\_(ツ)_/¯
|
||||
*/
|
||||
private void reassignIndexes() {
|
||||
userPipelines.sort(PipelineIndexComparator);
|
||||
for (int i = 0; i < userPipelines.size(); i++) {
|
||||
userPipelineSettings.sort(PipelineSettingsIndexComparator);
|
||||
for (int i = 0; i < userPipelineSettings.size(); i++) {
|
||||
getPipelineSettings(i).pipelineIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: adding/removing pipelines
|
||||
public CVPipelineSettings addPipeline(PipelineType type) {
|
||||
switch (type) {
|
||||
case Reflective:
|
||||
{
|
||||
var added = new ReflectivePipelineSettings();
|
||||
addPipelineInternal(added);
|
||||
return added;
|
||||
}
|
||||
case ColoredShape:
|
||||
{
|
||||
var added = new ColoredShapePipelineSettings();
|
||||
addPipelineInternal(added);
|
||||
return added;
|
||||
}
|
||||
}
|
||||
reassignIndexes();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addPipelineInternal(CVPipelineSettings settings) {
|
||||
settings.pipelineIndex = userPipelineSettings.size();
|
||||
userPipelineSettings.add(settings);
|
||||
reassignIndexes();
|
||||
}
|
||||
|
||||
private void removePipelineInternal(int index) {
|
||||
userPipelineSettings.remove(index);
|
||||
reassignIndexes();
|
||||
}
|
||||
|
||||
public void setIndex(int index) {
|
||||
this.setPipelineInternal(index);
|
||||
}
|
||||
|
||||
public void removePipeline(int index) {
|
||||
if (index < 0) {
|
||||
logger.debug("Could not remove preset pipes!");
|
||||
return;
|
||||
}
|
||||
// TODO should we block/lock on a mutex?
|
||||
removePipelineInternal(index);
|
||||
currentPipelineIndex = Math.max(userPipelineSettings.size() - 1, currentPipelineIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,32 @@
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import java.util.*;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.PhotonConfiguration;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.DataChangeSubscriber;
|
||||
import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.networktables.NTDataConsumer;
|
||||
import org.photonvision.common.datatransfer.DataConsumer;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.common.util.numbers.DoubleCouple;
|
||||
import org.photonvision.common.util.numbers.IntegerCouple;
|
||||
import org.photonvision.server.SocketHandler;
|
||||
import org.photonvision.server.UIUpdateType;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameConsumer;
|
||||
import org.photonvision.vision.pipeline.CVPipelineResult;
|
||||
import org.photonvision.vision.frame.consumer.MJPGFrameConsumer;
|
||||
import org.photonvision.vision.pipeline.PipelineType;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
/**
|
||||
* This is the God Class
|
||||
@@ -31,13 +52,25 @@ import org.photonvision.vision.pipeline.CVPipelineResult;
|
||||
*/
|
||||
public class VisionModule {
|
||||
|
||||
private final Logger logger;
|
||||
private final PipelineManager pipelineManager;
|
||||
private final VisionSource visionSource;
|
||||
private final VisionRunner visionRunner;
|
||||
private final LinkedList<DataConsumer> dataConsumers = new LinkedList<>();
|
||||
private final LinkedList<FrameConsumer> frameConsumers = new LinkedList<>();
|
||||
private final NTDataConsumer ntConsumer;
|
||||
private final int moduleIndex;
|
||||
private final MJPGFrameConsumer uiStreamer;
|
||||
private long lastUpdateTimestamp = -1;
|
||||
|
||||
public VisionModule(PipelineManager pipelineManager, VisionSource visionSource) {
|
||||
private MJPGFrameConsumer dashboardStreamer;
|
||||
|
||||
public VisionModule(PipelineManager pipelineManager, VisionSource visionSource, int index) {
|
||||
logger =
|
||||
new Logger(
|
||||
VisionModule.class,
|
||||
visionSource.getSettables().getConfiguration().nickname,
|
||||
LogGroup.VisionProcess);
|
||||
this.pipelineManager = pipelineManager;
|
||||
this.visionSource = visionSource;
|
||||
this.visionRunner =
|
||||
@@ -45,12 +78,296 @@ public class VisionModule {
|
||||
this.visionSource.getFrameProvider(),
|
||||
this.pipelineManager::getCurrentPipeline,
|
||||
this::consumeResult);
|
||||
this.moduleIndex = index;
|
||||
|
||||
DataChangeService.getInstance().addSubscriber(new VisionSettingChangeSubscriber());
|
||||
|
||||
dashboardStreamer =
|
||||
new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().uniqueName);
|
||||
uiStreamer = new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().nickname);
|
||||
addFrameConsumer(dashboardStreamer);
|
||||
addFrameConsumer(uiStreamer);
|
||||
|
||||
ntConsumer =
|
||||
new NTDataConsumer(
|
||||
NetworkTableInstance.getDefault().getTable("photonvision"),
|
||||
visionSource.getSettables().getConfiguration().nickname);
|
||||
addDataConsumer(ntConsumer);
|
||||
addDataConsumer(
|
||||
data -> {
|
||||
var now = System.currentTimeMillis();
|
||||
if (lastUpdateTimestamp + 1000.0 / 15.0 > now) return;
|
||||
|
||||
var uiMap = new HashMap<Integer, HashMap<String, Object>>();
|
||||
var dataMap = new HashMap<String, Object>();
|
||||
dataMap.put("fps", 1000.0 / data.result.getLatencyMillis());
|
||||
var targets = data.result.targets;
|
||||
var uiTargets = new ArrayList<HashMap<String, Object>>();
|
||||
for (var t : targets) {
|
||||
uiTargets.add(t.toHashMap());
|
||||
}
|
||||
dataMap.put("targets", uiTargets);
|
||||
|
||||
uiMap.put(index, dataMap);
|
||||
var retMap = new HashMap<String, Object>();
|
||||
retMap.put("updatePipelineResult", uiMap);
|
||||
|
||||
try {
|
||||
SocketHandler.getInstance().broadcastMessage(retMap, null);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error(e.getMessage());
|
||||
logger.error(Arrays.toString(e.getStackTrace()));
|
||||
}
|
||||
|
||||
lastUpdateTimestamp = now;
|
||||
});
|
||||
|
||||
setPipeline(visionSource.getSettables().getConfiguration().currentPipelineIndex);
|
||||
|
||||
dashboardStreamer.setFrameDivisor(
|
||||
pipelineManager.getCurrentPipelineSettings().outputFrameDivisor);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
visionRunner.startProcess();
|
||||
}
|
||||
|
||||
private class VisionSettingChangeSubscriber extends DataChangeSubscriber {
|
||||
|
||||
private VisionSettingChangeSubscriber() {
|
||||
super();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent event) {
|
||||
if (event instanceof IncomingWebSocketEvent) {
|
||||
var wsEvent = (IncomingWebSocketEvent<?>) event;
|
||||
|
||||
// TODO: remove?
|
||||
if (wsEvent.propertyName.equals("save")) {
|
||||
logger.debug("UI-based saving deprecated, ignoring");
|
||||
// saveAndBroadcast();
|
||||
return;
|
||||
}
|
||||
|
||||
if (wsEvent.cameraIndex != null && wsEvent.cameraIndex == moduleIndex) {
|
||||
logger.debug("Got PSC event - propName: " + wsEvent.propertyName);
|
||||
|
||||
var propName = wsEvent.propertyName;
|
||||
var newPropValue = wsEvent.data;
|
||||
var currentSettings = pipelineManager.getCurrentPipeline().getSettings();
|
||||
|
||||
// special case for non-PipelineSetting changes
|
||||
switch (propName) {
|
||||
case "cameraNickname": // rename camera
|
||||
var newNickname = (String) newPropValue;
|
||||
logger.info("Changing nickname to " + newNickname);
|
||||
setCameraNickname(newNickname);
|
||||
saveAndBroadcast();
|
||||
return;
|
||||
case "pipelineName": // rename current pipeline
|
||||
logger.info("Changing nick to " + newPropValue);
|
||||
pipelineManager.getCurrentPipelineSettings().pipelineNickname = (String) newPropValue;
|
||||
// TODO rename config file
|
||||
saveAndBroadcast();
|
||||
return;
|
||||
case "newPipelineInfo": // add new pipeline
|
||||
var typeName = (Pair<String, PipelineType>) newPropValue;
|
||||
var type = typeName.getRight();
|
||||
var name = typeName.getLeft();
|
||||
|
||||
logger.info("Adding a " + type + " pipeline with name " + name);
|
||||
|
||||
var addedSettings = pipelineManager.addPipeline(type);
|
||||
addedSettings.pipelineNickname = name;
|
||||
saveAndBroadcast();
|
||||
return;
|
||||
case "deleteCurrPipeline":
|
||||
var indexToDelete = pipelineManager.getCurrentPipelineIndex();
|
||||
logger.info("Deleting current pipe at index " + indexToDelete);
|
||||
pipelineManager.removePipeline(indexToDelete);
|
||||
saveAndBroadcast();
|
||||
return;
|
||||
case "changePipeline": // change active pipeline
|
||||
var index = (Integer) newPropValue;
|
||||
if (index == pipelineManager.getCurrentPipelineIndex()) {
|
||||
logger.debug("Skipping pipeline change, index " + index + " already active");
|
||||
return;
|
||||
}
|
||||
logger.debug("Setting pipeline index to " + index);
|
||||
setPipeline(index);
|
||||
saveAndBroadcast();
|
||||
return;
|
||||
}
|
||||
|
||||
// special case for camera settables
|
||||
if (propName.startsWith("camera")) {
|
||||
var propMethodName = "set" + propName.replace("camera", "");
|
||||
var methods = visionSource.getSettables().getClass().getMethods();
|
||||
for (var method : methods) {
|
||||
if (method.getName().equalsIgnoreCase(propMethodName)) {
|
||||
try {
|
||||
method.invoke(visionSource.getSettables(), newPropValue);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to invoke camera settable method: " + method.getName());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
var propField = currentSettings.getClass().getField(propName);
|
||||
var propType = propField.getType();
|
||||
|
||||
if (propType.isEnum()) {
|
||||
var actual = propType.getEnumConstants()[(int) newPropValue];
|
||||
propField.set(currentSettings, actual);
|
||||
} else if (propType.isAssignableFrom(DoubleCouple.class)) {
|
||||
var orig = (ArrayList<Number>) newPropValue;
|
||||
var actual = new DoubleCouple(orig.get(0), orig.get(1));
|
||||
propField.set(currentSettings, actual);
|
||||
} else if (propType.isAssignableFrom(IntegerCouple.class)) {
|
||||
var orig = (ArrayList<Integer>) newPropValue;
|
||||
var actual = new IntegerCouple(orig.get(0), orig.get(1));
|
||||
propField.set(currentSettings, actual);
|
||||
} else if (propType.equals(Double.TYPE)) {
|
||||
propField.setDouble(currentSettings, (Double) newPropValue);
|
||||
} else if (propType.equals(Integer.TYPE)) {
|
||||
propField.setInt(currentSettings, (Integer) newPropValue);
|
||||
} else if (propType.equals(Boolean.TYPE)) {
|
||||
if (newPropValue instanceof Integer) {
|
||||
propField.setBoolean(currentSettings, (Integer) newPropValue != 0);
|
||||
} else {
|
||||
propField.setBoolean(currentSettings, (Boolean) newPropValue);
|
||||
}
|
||||
} else {
|
||||
propField.set(newPropValue, newPropValue);
|
||||
}
|
||||
logger.trace("Set prop " + propName + " to value " + newPropValue);
|
||||
|
||||
// special case for extra tasks to perform after setting PipelineSettings
|
||||
if (propName.equals("outputFrameDivisor")) {
|
||||
dashboardStreamer.setFrameDivisor(
|
||||
pipelineManager.getCurrentPipelineSettings().outputFrameDivisor);
|
||||
}
|
||||
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
logger.error(
|
||||
"Could not set prop "
|
||||
+ propName
|
||||
+ " with value "
|
||||
+ newPropValue
|
||||
+ " on "
|
||||
+ currentSettings);
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
logger.error("Unknown exception when setting PSC prop!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
saveAndBroadcast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPipeline(int index) {
|
||||
logger.info("Setting pipeline to " + index);
|
||||
pipelineManager.setIndex(index);
|
||||
var config = pipelineManager.getPipelineSettings(index);
|
||||
|
||||
if (config == null) {
|
||||
logger.error("Config for index " + index + " was null!");
|
||||
return;
|
||||
}
|
||||
|
||||
visionSource.getSettables().setCurrentVideoMode(config.cameraVideoModeIndex);
|
||||
visionSource.getSettables().setBrightness(config.cameraBrightness);
|
||||
visionSource.getSettables().setExposure(config.cameraExposure);
|
||||
visionSource.getSettables().setGain(config.cameraGain);
|
||||
visionSource.getSettables().getConfiguration().currentPipelineIndex =
|
||||
pipelineManager.getCurrentPipelineIndex();
|
||||
}
|
||||
|
||||
private void saveAndBroadcast() {
|
||||
ConfigManager.getInstance()
|
||||
.saveModule(
|
||||
getStateAsCameraConfig(), visionSource.getSettables().getConfiguration().uniqueName);
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
UIUpdateType.BROADCAST,
|
||||
"fullsettings",
|
||||
ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
}
|
||||
|
||||
private void save() {
|
||||
ConfigManager.getInstance()
|
||||
.saveModule(
|
||||
getStateAsCameraConfig(), visionSource.getSettables().getConfiguration().uniqueName);
|
||||
}
|
||||
|
||||
private void setCameraNickname(String newName) {
|
||||
visionSource.getSettables().getConfiguration().nickname = newName;
|
||||
ntConsumer.setCameraName(newName);
|
||||
|
||||
frameConsumers.remove(dashboardStreamer);
|
||||
dashboardStreamer =
|
||||
new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().nickname);
|
||||
frameConsumers.add(dashboardStreamer);
|
||||
saveAndBroadcast();
|
||||
}
|
||||
|
||||
public PhotonConfiguration.UICameraConfiguration toUICameraConfig() {
|
||||
var ret = new PhotonConfiguration.UICameraConfiguration();
|
||||
|
||||
ret.fov = visionSource.getSettables().getFOV();
|
||||
// ret.tiltDegrees = this.visionSource.getSettables() // TODO implement tilt in camera
|
||||
// settings
|
||||
ret.nickname = visionSource.getSettables().getConfiguration().nickname;
|
||||
ret.currentPipelineSettings =
|
||||
SerializationUtils.objectToHashMap(pipelineManager.getCurrentPipelineSettings());
|
||||
ret.currentPipelineIndex = pipelineManager.getCurrentPipelineIndex();
|
||||
ret.pipelineNicknames = pipelineManager.getPipelineNicknames();
|
||||
|
||||
// TODO refactor into helper method
|
||||
var temp = new HashMap<Integer, HashMap<String, Object>>();
|
||||
var videoModes = visionSource.getSettables().getAllVideoModes();
|
||||
for (var k : videoModes.keySet()) {
|
||||
var internalMap = new HashMap<String, Object>();
|
||||
|
||||
internalMap.put("width", videoModes.get(k).width);
|
||||
internalMap.put("height", videoModes.get(k).height);
|
||||
internalMap.put("fps", videoModes.get(k).fps);
|
||||
internalMap.put("pixelFormat", videoModes.get(k).pixelFormat.toString());
|
||||
|
||||
temp.put(k, internalMap);
|
||||
}
|
||||
ret.videoFormatList = temp;
|
||||
ret.streamPort = dashboardStreamer.getCurrentStreamPort();
|
||||
// ret.uiStreamPort = uiStreamer.getCurrentStreamPort();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public CameraConfiguration getStateAsCameraConfig() {
|
||||
var config = visionSource.getSettables().getConfiguration();
|
||||
config.setPipelineSettings(pipelineManager.userPipelineSettings);
|
||||
config.driveModeSettings = pipelineManager.driverModePipeline.getSettings();
|
||||
config.currentPipelineIndex = pipelineManager.getCurrentPipelineIndex();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public void addDataConsumer(DataConsumer dataConsumer) {
|
||||
dataConsumers.add(dataConsumer);
|
||||
}
|
||||
|
||||
void addFrameConsumer(FrameConsumer consumer) {
|
||||
frameConsumers.add(consumer);
|
||||
}
|
||||
|
||||
void consumeResult(CVPipelineResult result) {
|
||||
// TODO: put result in to Data (not this way!)
|
||||
var data = new Data();
|
||||
@@ -59,6 +376,8 @@ public class VisionModule {
|
||||
|
||||
var frame = result.outputFrame;
|
||||
consumeFrame(frame);
|
||||
|
||||
data.release();
|
||||
}
|
||||
|
||||
void consumeData(Data data) {
|
||||
@@ -67,14 +386,6 @@ public class VisionModule {
|
||||
}
|
||||
}
|
||||
|
||||
public void addDataConsumer(DataConsumer dataConsumer) {
|
||||
dataConsumers.add(dataConsumer);
|
||||
}
|
||||
|
||||
public void addFrameConsumer(FrameConsumer frameConsumer) {
|
||||
frameConsumers.add(frameConsumer);
|
||||
}
|
||||
|
||||
void consumeFrame(Frame frame) {
|
||||
for (var frameConsumer : frameConsumers) {
|
||||
frameConsumer.accept(frame);
|
||||
|
||||
@@ -18,25 +18,47 @@
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
||||
|
||||
/** VisionModuleManager has many VisionModules, and provides camera configuration data to them. */
|
||||
public class VisionModuleManager {
|
||||
|
||||
private static class ThreadSafeSingleton {
|
||||
private static final VisionModuleManager INSTANCE = new VisionModuleManager();
|
||||
}
|
||||
|
||||
public static VisionModuleManager getInstance() {
|
||||
return VisionModuleManager.ThreadSafeSingleton.INSTANCE;
|
||||
}
|
||||
|
||||
protected final List<VisionModule> visionModules = new ArrayList<>();
|
||||
|
||||
public VisionModuleManager(List<VisionSource> visionSources) {
|
||||
for (var visionSource : visionSources) {
|
||||
private VisionModuleManager() {}
|
||||
|
||||
// TODO: loading existing pipelines from config
|
||||
var pipelineManager = new PipelineManager();
|
||||
public List<VisionModule> getModules() {
|
||||
return visionModules;
|
||||
}
|
||||
|
||||
visionModules.add(new VisionModule(pipelineManager, visionSource));
|
||||
public VisionModule getModule(int i) {
|
||||
return visionModules.get(i);
|
||||
}
|
||||
|
||||
public void addSources(HashMap<VisionSource, List<CVPipelineSettings>> visionSources) {
|
||||
for (var entry : visionSources.entrySet()) {
|
||||
var visionSource = entry.getKey();
|
||||
var pipelineManager = new PipelineManager(entry.getValue());
|
||||
var module = new VisionModule(pipelineManager, visionSource, visionModules.size());
|
||||
visionModules.add(module);
|
||||
// todo: logging
|
||||
}
|
||||
}
|
||||
|
||||
public void startModules() {
|
||||
for (var visionModule : visionModules) {
|
||||
visionModule.start();
|
||||
// todo: logging
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,18 @@ package org.photonvision.vision.processes;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.pipeline.CVPipeline;
|
||||
import org.photonvision.vision.pipeline.CVPipelineResult;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
/** VisionRunner has a frame supplier, a pipeline supplier, and a result consumer */
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class VisionRunner {
|
||||
|
||||
private final Logger logger;
|
||||
private final Thread visionProcessThread;
|
||||
private final Supplier<Frame> frameSupplier;
|
||||
private final Supplier<CVPipeline> pipelineSupplier;
|
||||
@@ -51,19 +54,17 @@ public class VisionRunner {
|
||||
this.pipelineSupplier = pipelineSupplier;
|
||||
this.pipelineResultConsumer = pipelineResultConsumer;
|
||||
|
||||
this.visionProcessThread = new Thread(this::update);
|
||||
this.visionProcessThread.setName("VisionRunner - " + frameSupplier.getName());
|
||||
visionProcessThread = new Thread(this::update);
|
||||
visionProcessThread.setName("VisionRunner - " + frameSupplier.getName());
|
||||
logger = new Logger(VisionRunner.class, frameSupplier.getName(), LogGroup.VisionProcess);
|
||||
}
|
||||
|
||||
public void startProcess() {
|
||||
visionProcessThread.start();
|
||||
}
|
||||
|
||||
private boolean hasThrown;
|
||||
|
||||
private void update() {
|
||||
while (!Thread.interrupted()) {
|
||||
loopCount++;
|
||||
var pipeline = pipelineSupplier.get();
|
||||
var frame = frameSupplier.get();
|
||||
|
||||
@@ -71,13 +72,10 @@ public class VisionRunner {
|
||||
var pipelineResult = pipeline.run(frame);
|
||||
pipelineResultConsumer.accept(pipelineResult);
|
||||
} catch (Exception ex) {
|
||||
if (hasThrown) {
|
||||
System.err.println(
|
||||
"Exception in thread \"" + visionProcessThread.getName() + "\", loop " + loopCount);
|
||||
ex.printStackTrace();
|
||||
hasThrown = true;
|
||||
}
|
||||
logger.error("Exception on loop " + loopCount);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
loopCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.photonvision.vision.processes;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
|
||||
public interface VisionSource {
|
||||
|
||||
FrameProvider getFrameProvider();
|
||||
|
||||
VisionSourceSettables getSettables();
|
||||
|
||||
@@ -21,6 +21,7 @@ import edu.wpi.cscore.UsbCamera;
|
||||
import edu.wpi.cscore.UsbCameraInfo;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
@@ -31,12 +32,13 @@ import org.photonvision.vision.camera.USBCameraSource;
|
||||
import org.photonvision.vision.frame.provider.NetworkFrameProvider;
|
||||
|
||||
public class VisionSourceManager {
|
||||
public List<VisionSource> LoadAllSources(List<CameraConfiguration> camerasConfiguration) {
|
||||
public static List<VisionSource> loadAllSources(
|
||||
Collection<CameraConfiguration> camerasConfiguration) {
|
||||
return LoadAllSources(camerasConfiguration, Arrays.asList(UsbCamera.enumerateUsbCameras()));
|
||||
}
|
||||
|
||||
public List<VisionSource> LoadAllSources(
|
||||
List<CameraConfiguration> camerasConfiguration, List<UsbCameraInfo> usbCameraInfos) {
|
||||
public static List<VisionSource> LoadAllSources(
|
||||
Collection<CameraConfiguration> camerasConfiguration, List<UsbCameraInfo> usbCameraInfos) {
|
||||
var UsbCamerasConfiguration =
|
||||
camerasConfiguration.stream()
|
||||
.filter(configuration -> configuration.cameraType == CameraType.UsbCamera)
|
||||
@@ -47,11 +49,11 @@ public class VisionSourceManager {
|
||||
return loadUSBCameraSources(matchedCameras);
|
||||
}
|
||||
|
||||
private NetworkFrameProvider loadHTTPCamera(CameraConfiguration config) {
|
||||
private static NetworkFrameProvider loadHTTPCamera(CameraConfiguration config) {
|
||||
throw new NotImplementedException("");
|
||||
}
|
||||
|
||||
private List<CameraConfiguration> matchUSBCameras(
|
||||
private static List<CameraConfiguration> matchUSBCameras(
|
||||
List<UsbCameraInfo> infos, List<CameraConfiguration> cameraConfigurationList) {
|
||||
ArrayList<UsbCameraInfo> loopableInfo = new ArrayList<>(infos);
|
||||
List<CameraConfiguration> cameraConfigurations = new ArrayList<>();
|
||||
@@ -91,21 +93,21 @@ public class VisionSourceManager {
|
||||
}
|
||||
|
||||
CameraConfiguration configuration =
|
||||
new CameraConfiguration(name, uniqueName, uniqueName, ((Integer) info.dev).toString());
|
||||
new CameraConfiguration(name, uniqueName, uniqueName, info.path);
|
||||
cameraConfigurations.add(configuration);
|
||||
}
|
||||
|
||||
return cameraConfigurations;
|
||||
}
|
||||
|
||||
private List<VisionSource> loadUSBCameraSources(List<CameraConfiguration> configurations) {
|
||||
private static List<VisionSource> loadUSBCameraSources(List<CameraConfiguration> configurations) {
|
||||
List<VisionSource> usbCameraSources = new ArrayList<>();
|
||||
configurations.forEach(
|
||||
configuration -> usbCameraSources.add(new USBCameraSource(configuration)));
|
||||
return usbCameraSources;
|
||||
}
|
||||
|
||||
private boolean containsName(final List<CameraConfiguration> list, final String name) {
|
||||
private static boolean containsName(final List<CameraConfiguration> list, final String name) {
|
||||
return list.stream().anyMatch(configuration -> configuration.uniqueName.equals(name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,15 +23,19 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
|
||||
public abstract class VisionSourceSettables {
|
||||
private CameraConfiguration configuration;
|
||||
private final CameraConfiguration configuration;
|
||||
|
||||
protected VisionSourceSettables(CameraConfiguration configuration) {
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
FrameStaticProperties frameStaticProperties;
|
||||
protected FrameStaticProperties frameStaticProperties;
|
||||
protected HashMap<Integer, VideoMode> videoModes;
|
||||
|
||||
public CameraConfiguration getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public abstract int getExposure();
|
||||
|
||||
public abstract void setExposure(int exposure);
|
||||
@@ -46,8 +50,17 @@ public abstract class VisionSourceSettables {
|
||||
|
||||
public abstract VideoMode getCurrentVideoMode();
|
||||
|
||||
public void setCurrentVideoMode(int index) {
|
||||
setCurrentVideoMode(getAllVideoModes().get(index));
|
||||
}
|
||||
|
||||
public abstract void setCurrentVideoMode(VideoMode videoMode);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void setVideoModeIndex(int index) {
|
||||
setCurrentVideoMode(videoModes.get(index));
|
||||
}
|
||||
|
||||
public abstract HashMap<Integer, VideoMode> getAllVideoModes();
|
||||
|
||||
public double getFOV() {
|
||||
|
||||
@@ -25,12 +25,14 @@ import org.photonvision.common.util.numbers.DoubleCouple;
|
||||
public class TargetCalculations {
|
||||
public static double calculateYaw(
|
||||
double offsetCenterX, double targetCenterX, double horizontalFocalLength) {
|
||||
return FastMath.toDegrees(FastMath.atan(offsetCenterX - targetCenterX) / horizontalFocalLength);
|
||||
return FastMath.toDegrees(
|
||||
FastMath.atan((offsetCenterX - targetCenterX) / horizontalFocalLength));
|
||||
}
|
||||
|
||||
public static double calculatePitch(
|
||||
double offsetCenterY, double targetCenterY, double verticalFocalLength) {
|
||||
return -FastMath.toDegrees(FastMath.atan(offsetCenterY - targetCenterY) / verticalFocalLength);
|
||||
return -FastMath.toDegrees(
|
||||
FastMath.atan((offsetCenterY - targetCenterY) / verticalFocalLength));
|
||||
}
|
||||
|
||||
public static Point calculateTargetOffsetPoint(
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.photonvision.vision.target;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.MatOfPoint2f;
|
||||
@@ -78,6 +79,11 @@ public class TrackedTarget implements Releasable {
|
||||
return m_yaw;
|
||||
}
|
||||
|
||||
private double getSkew() {
|
||||
// TODO skew;
|
||||
return 5940;
|
||||
}
|
||||
|
||||
public double getArea() {
|
||||
return m_area;
|
||||
}
|
||||
@@ -161,6 +167,24 @@ public class TrackedTarget implements Releasable {
|
||||
cameraRelativeRvec.copyTo(this.m_cameraRelativeRvec);
|
||||
}
|
||||
|
||||
public HashMap<String, Object> toHashMap() {
|
||||
// pitch: 0,
|
||||
// yaw: 0,
|
||||
// skew: 0,
|
||||
// area: 0,
|
||||
// // 3D only
|
||||
// pose: {x: 0, y: 0, rot: 0},
|
||||
var ret = new HashMap<String, Object>();
|
||||
ret.put("pitch", getPitch());
|
||||
ret.put("yaw", getYaw());
|
||||
ret.put("skew", getSkew());
|
||||
ret.put("area", getArea());
|
||||
if (getRobotRelativePose() != null) {
|
||||
ret.put("pose", getRobotRelativePose().toHashMap());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static class TargetCalculationParameters {
|
||||
// TargetOffset calculation values
|
||||
final boolean isLandscape;
|
||||
|
||||
1
photon-server/src/main/resources/web/index.html
Normal file
1
photon-server/src/main/resources/web/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<p>UI has not been copied!</p>
|
||||
@@ -32,8 +32,8 @@ import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ContourGroupingMode;
|
||||
import org.photonvision.vision.opencv.ContourIntersectionDirection;
|
||||
import org.photonvision.vision.pipeline.CVPipeline;
|
||||
import org.photonvision.vision.pipeline.CVPipelineResult;
|
||||
import org.photonvision.vision.pipeline.ReflectivePipeline;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
/** Various tests that check performance on long-running tasks (i.e. a pipeline) */
|
||||
public class BenchmarkTest {
|
||||
|
||||
@@ -90,7 +90,6 @@ public class ConfigTest {
|
||||
@Test
|
||||
@Order(2)
|
||||
public void deserializeConfig() {
|
||||
configMgr.load();
|
||||
|
||||
var reflectivePipelineSettings =
|
||||
configMgr.getConfig().getCameraConfigurations().get("TestCamera").pipelineSettings.get(0);
|
||||
|
||||
@@ -17,23 +17,33 @@
|
||||
|
||||
package org.photonvision.vision;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.photonvision.vision.camera.CameraQuirks;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
|
||||
public class QuirkyCameraTest {
|
||||
@Test
|
||||
public void ps3EyeTest() {
|
||||
QuirkyCamera psEye = new QuirkyCamera(0x1415, 0x2000, "psEye");
|
||||
Assertions.assertEquals(psEye.quirks, List.of(CameraQuirks.Gain));
|
||||
HashMap<CameraQuirk, Boolean> ps3EyeQuirks = new HashMap<>();
|
||||
ps3EyeQuirks.put(CameraQuirk.Gain, true);
|
||||
for (var q : CameraQuirk.values()) {
|
||||
ps3EyeQuirks.putIfAbsent(q, false);
|
||||
}
|
||||
|
||||
QuirkyCamera psEye = QuirkyCamera.getQuirkyCamera(0x1415, 0x2000, "psEye");
|
||||
Assertions.assertEquals(psEye.quirks, ps3EyeQuirks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void quirklessCameraTest() {
|
||||
QuirkyCamera noQuirk = new QuirkyCamera(1234, 888, "empty");
|
||||
Assertions.assertEquals(noQuirk.quirks, new ArrayList<>());
|
||||
HashMap<CameraQuirk, Boolean> noQuirks = new HashMap<>();
|
||||
for (var q : CameraQuirk.values()) {
|
||||
noQuirks.put(q, false);
|
||||
}
|
||||
|
||||
QuirkyCamera quirkless = QuirkyCamera.getQuirkyCamera(1234, 888, "empty");
|
||||
Assertions.assertEquals(quirkless.quirks, noQuirks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ContourGroupingMode;
|
||||
import org.photonvision.vision.opencv.ContourIntersectionDirection;
|
||||
import org.photonvision.vision.opencv.ContourShape;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.TargetModel;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.photonvision.vision.frame.provider.FileFrameProvider;
|
||||
import org.photonvision.vision.opencv.ContourGroupingMode;
|
||||
import org.photonvision.vision.opencv.ContourIntersectionDirection;
|
||||
import org.photonvision.vision.opencv.ContourShape;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
public class ColoredShapePipelineTest {
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user