mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
Compare commits
81 Commits
v2020.9.1-
...
v2021.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5298de0f64 | ||
|
|
2330b72451 | ||
|
|
69d2499e1a | ||
|
|
0a4dcd17e0 | ||
|
|
15c687655a | ||
|
|
839f959681 | ||
|
|
0b2de7d9f1 | ||
|
|
5d139e0a4e | ||
|
|
b2d939b3b5 | ||
|
|
f7e29a1992 | ||
|
|
08a51fd237 | ||
|
|
b8bc65ec32 | ||
|
|
8e190ce5f7 | ||
|
|
f676023a5d | ||
|
|
d92595f622 | ||
|
|
69142928b3 | ||
|
|
28e3b510c7 | ||
|
|
fc05bcab2c | ||
|
|
abf5226405 | ||
|
|
d327428e1b | ||
|
|
79fc194575 | ||
|
|
7a9d999c15 | ||
|
|
2cf725876f | ||
|
|
2ca879c82d | ||
|
|
be5d8f6518 | ||
|
|
7a032cce6e | ||
|
|
f2f32da2f9 | ||
|
|
5768648cde | ||
|
|
6856427f86 | ||
|
|
2a687a1db8 | ||
|
|
771f7442c9 | ||
|
|
c7d092a775 | ||
|
|
11a66b15ed | ||
|
|
0c89db421c | ||
|
|
36de88f903 | ||
|
|
a49f3ac7f0 | ||
|
|
229570d522 | ||
|
|
d346513ad7 | ||
|
|
a5437f7215 | ||
|
|
0c3aeb409b | ||
|
|
e4b6559b81 | ||
|
|
e608d073bd | ||
|
|
663684fb10 | ||
|
|
c3dbd45716 | ||
|
|
15d21b7841 | ||
|
|
14ed7ce7a4 | ||
|
|
d2b0e465ff | ||
|
|
5e385b5925 | ||
|
|
c11f1f3f28 | ||
|
|
308e108953 | ||
|
|
2ae750f00f | ||
|
|
da2aaba033 | ||
|
|
e74f750fc0 | ||
|
|
3f48346557 | ||
|
|
bf7c9fea44 | ||
|
|
8109a2a437 | ||
|
|
0ce49bd8f2 | ||
|
|
33bbb4c69f | ||
|
|
35a6c0bfa4 | ||
|
|
869e4628ce | ||
|
|
866ce2197e | ||
|
|
3a78e23a55 | ||
|
|
31013346c0 | ||
|
|
e37fcdea98 | ||
|
|
23f3c0e6e1 | ||
|
|
6e0a6b804e | ||
|
|
2d7a4dd1b9 | ||
|
|
28459704c6 | ||
|
|
125cd35557 | ||
|
|
3305c6619d | ||
|
|
24132555b8 | ||
|
|
44bfc3ea6c | ||
|
|
71fc8a7017 | ||
|
|
b73c698e4d | ||
|
|
45686b7c9d | ||
|
|
90f8397688 | ||
|
|
2495d348ea | ||
|
|
a35f775b05 | ||
|
|
ddd15d362b | ||
|
|
7bf92a9db3 | ||
|
|
73fc8e04ca |
21
.github/workflows/main.yml
vendored
21
.github/workflows/main.yml
vendored
@@ -7,6 +7,8 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
@@ -104,9 +106,6 @@ jobs:
|
||||
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Install LaTeX and other system dependencies
|
||||
run: sudo apt install -y texlive-latex-recommended texlive-fonts-recommended texlive-latex-extra latexmk texlive-lang-greek texlive-luatex texlive-xetex texlive-fonts-extra dvipng graphviz
|
||||
|
||||
- name: Check the docs
|
||||
run: |
|
||||
make linkcheck
|
||||
@@ -121,7 +120,6 @@ jobs:
|
||||
with:
|
||||
name: built-docs
|
||||
path: build/html
|
||||
|
||||
|
||||
build-package:
|
||||
needs: [build-client, build-server, build-offline-docs]
|
||||
@@ -208,4 +206,17 @@ jobs:
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew spotlessCheck
|
||||
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [build-package]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: jar
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: '**/*'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -125,3 +125,4 @@ photon-server/build
|
||||
photon-server/photon-vision
|
||||
photon-server/src/main/resources/web
|
||||
photon-server/src/main/java/org/photonvision/PhotonVersion.java
|
||||
photon-server/src/main/generated/native/include/org_photonvision_raspi_PicamJNI.h
|
||||
|
||||
16
README.md
16
README.md
@@ -1,16 +1,12 @@
|
||||
# Photon Vision
|
||||
|
||||
[](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [](https://codecov.io/gh/PhotonVision/photonvision)
|
||||
[](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [](https://codecov.io/gh/PhotonVision/photonvision) [](https://discord.gg/wYxTwym)
|
||||
|
||||
A copy of the latest development release is available [here](https://github.com/PhotonVision/photonvision/releases/tag/Dev).
|
||||
PhotonVision is the free, fast, and easy-to-use computer vision solution for the *FIRST* Robotics Competition. You can read an overview of our features [on our website](https://photonvision.org). You can find our comprehensive documentation [here](https://docs.photonvision.org).
|
||||
|
||||
PhotonVision is a fork of [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/), a free open-source software for FRC teams to use for vision processing on their robots. Thank you to everyone who worked on the original project.
|
||||
A copy of the latest Raspberry Pi image is available [here](https://github.com/PhotonVision/photon-pi-gen/releases). A copy of the latest standalone JAR is available [here](https://github.com/PhotonVision/photonvision/releases). If you are a Gloworm user you can find the latest Gloworm image [here](https://github.com/gloworm-vision/pi-gen/releases).
|
||||
|
||||
For information on contributing or running PhotonVision, please read our documentation on ReadTheDocs.
|
||||
|
||||
# Roadmap
|
||||
|
||||
Our roadmap is publicly available on [Trello](https://trello.com/photonvision).
|
||||
If you are interested in contributing code or documentation to the project, please [read our getting started page for contributors](https://docs.photonvision.org/en/latest/docs/other/contributing/index.html) and **[join the Discord](https://discord.gg/wYxTwym) to introduce yourself!** We hope to provide a welcoming community to anyone who is interested in helping.
|
||||
|
||||
## Authors
|
||||
|
||||
@@ -18,6 +14,8 @@ A list of contributors is available in our documentation on ReadTheDocs.
|
||||
|
||||
|
||||
## Acknowledgments
|
||||
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
|
||||
|
||||
|
||||
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
|
||||
|
||||
@@ -30,4 +28,4 @@ A list of contributors is available in our documentation on ReadTheDocs.
|
||||
* [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
|
||||
|
||||
## License
|
||||
Usage of PhotonVision must fall under all terms of [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
PhotonVision is licensed under the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
ref="camerasTabOpener"
|
||||
link
|
||||
to="cameras"
|
||||
ref="camerasTabOpener"
|
||||
@click="switchToDriverMode()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
@@ -53,6 +53,7 @@
|
||||
<v-list-item
|
||||
link
|
||||
to="settings"
|
||||
@click="switchToSettingsTab()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-settings</v-icon>
|
||||
@@ -118,7 +119,7 @@
|
||||
>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<router-view v-on:switch-to-cameras="switchToDriverMode" />
|
||||
<router-view @switch-to-cameras="switchToDriverMode" />
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
@@ -144,7 +145,7 @@ import Logs from "./views/LogsView"
|
||||
},
|
||||
data: () => ({
|
||||
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
||||
previouslySelectedIndex: undefined,
|
||||
previouslySelectedIndices: [],
|
||||
timer: undefined,
|
||||
}),
|
||||
computed: {
|
||||
@@ -197,6 +198,7 @@ import Logs from "./views/LogsView"
|
||||
};
|
||||
this.$options.sockets.onopen = () => {
|
||||
this.$store.state.backendConnected = true;
|
||||
this.$store.state.connectedCallbacks.forEach(it => it())
|
||||
};
|
||||
|
||||
let closed = () => {
|
||||
@@ -238,14 +240,25 @@ import Logs from "./views/LogsView"
|
||||
})
|
||||
},
|
||||
switchToDriverMode() {
|
||||
this.previouslySelectedIndex = this.$store.getters.currentPipelineIndex;
|
||||
this.handleInputWithIndex('currentPipeline', -1)
|
||||
},
|
||||
rollbackPipelineIndex() {
|
||||
if (this.previouslySelectedIndex !== null) {
|
||||
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndex || 0);
|
||||
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
|
||||
|
||||
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
|
||||
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
|
||||
this.handleInputWithIndex('currentPipeline', -1, i);
|
||||
}
|
||||
this.previouslySelectedIndex = null;
|
||||
},
|
||||
rollbackPipelineIndex()
|
||||
{
|
||||
if (this.previouslySelectedIndices !== null) {
|
||||
for (const [i] of this.$store.state.cameraSettings.entries()) {
|
||||
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
|
||||
}
|
||||
}
|
||||
this.previouslySelectedIndices = null;
|
||||
}
|
||||
,
|
||||
switchToSettingsTab() {
|
||||
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"2020 Hex Goal": {
|
||||
"realWorldCoordinatesArray": [
|
||||
{
|
||||
"x": -0.49847498536109924,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
{
|
||||
"x": -0.24942462146282196,
|
||||
"y": -0.4318000078201294,
|
||||
"z": 0.0
|
||||
},
|
||||
{
|
||||
"x": 0.24942462146282196,
|
||||
"y": -0.4318000078201294,
|
||||
"z": 0.0
|
||||
},
|
||||
{
|
||||
"x": 0.49847498536109924,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
}
|
||||
],
|
||||
"boxHeight": 0.30479999999999996
|
||||
},
|
||||
"2019 Dual Target": {
|
||||
"realWorldCoordinatesArray": [
|
||||
{
|
||||
"x": -0.15077440440654755,
|
||||
"y": 0.06761480122804642,
|
||||
"z": 0.0
|
||||
},
|
||||
{
|
||||
"x": -0.18575020134449005,
|
||||
"y": -0.06761480122804642,
|
||||
"z": 0.0
|
||||
},
|
||||
{
|
||||
"x": 0.18575020134449005,
|
||||
"y": -0.06761480122804642,
|
||||
"z": 0.0
|
||||
},
|
||||
{
|
||||
"x": 0.15077440440654755,
|
||||
"y": 0.06761480122804642,
|
||||
"z": 0.0
|
||||
}
|
||||
],
|
||||
"boxHeight": 0.1
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
name: "CvImage",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
|
||||
data() {
|
||||
return {
|
||||
seed: 1.0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styleObject: {
|
||||
get() {
|
||||
@@ -41,9 +46,17 @@
|
||||
},
|
||||
src: {
|
||||
get() {
|
||||
return this.disconnected ? require("../../assets/noStream.jpg") : this.address;
|
||||
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.reload(); // Force reload image on creation
|
||||
},
|
||||
methods: {
|
||||
reload() {
|
||||
this.seed = new Date().getTime();
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -12,6 +12,7 @@
|
||||
color="#ffd843"
|
||||
:label="name"
|
||||
:value="index"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</div>
|
||||
@@ -21,7 +22,7 @@
|
||||
export default {
|
||||
name: 'Radio',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value', 'list'],
|
||||
props: ['value', 'list', 'disabled'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return Object.values(this.value);
|
||||
return Object.values(this.value || [0, 0]);
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("input", value);
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
:hover="true"
|
||||
text="edit"
|
||||
tooltip="Edit camera name"
|
||||
@click="toCameraNameChange"
|
||||
@click="changeCameraName"
|
||||
/>
|
||||
<div v-else>
|
||||
<CVicon
|
||||
@@ -76,9 +76,9 @@
|
||||
lg="2"
|
||||
>
|
||||
<v-menu
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
offset-y
|
||||
auto
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon
|
||||
@@ -292,13 +292,23 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toCameraNameChange() {
|
||||
changeCameraName() {
|
||||
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
|
||||
this.isCameraNameEdit = true;
|
||||
},
|
||||
saveCameraNameChange() {
|
||||
if (this.checkCameraName === "") {
|
||||
this.handleInputWithIndex("changeCameraName", this.newCameraName);
|
||||
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
|
||||
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
|
||||
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
|
||||
// eslint-disable-next-line
|
||||
.then(r => {
|
||||
this.$emit('camera-name-changed')
|
||||
})
|
||||
.catch(e => {
|
||||
console.log("HTTP error while changing camera name " + e);
|
||||
this.$emit('camera-name-changed')
|
||||
})
|
||||
this.discardCameraNameChange();
|
||||
}
|
||||
},
|
||||
@@ -322,7 +332,7 @@
|
||||
},
|
||||
deleteCurrentPipeline() {
|
||||
if (this.$store.getters.pipelineList.length > 1) {
|
||||
this.handleInputWithIndex('deleteCurrentPipeline');
|
||||
this.handleInputWithIndex('deleteCurrentPipeline', {});
|
||||
} else {
|
||||
this.snackbar = true;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import VueNativeSock from 'vue-native-websocket';
|
||||
|
||||
Vue.use(VueNativeSock, wsURL, {
|
||||
reconnection: true,
|
||||
reconnectionDelay: 100,
|
||||
connectManually: true,
|
||||
format: "arraybuffer",
|
||||
});
|
||||
|
||||
@@ -4,10 +4,10 @@ export const dataHandleMixin = {
|
||||
let msg = this.$msgPack.encode({[key]: value});
|
||||
this.$socket.send(msg);
|
||||
},
|
||||
handleInputWithIndex(key, value) {
|
||||
handleInputWithIndex(key, value, cameraIndex = this.$store.getters.currentCameraIndex) {
|
||||
let msg = this.$msgPack.encode({
|
||||
[key]: value,
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
["cameraIndex"]: cameraIndex,
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
},
|
||||
|
||||
@@ -15,12 +15,12 @@ export default new Vuex.Store({
|
||||
},
|
||||
state: {
|
||||
backendConnected: false,
|
||||
connectedCallbacks: [],
|
||||
colorPicking: false,
|
||||
logsOverlay: false,
|
||||
compactMode: localStorage.getItem("compactMode") === undefined ? undefined : localStorage.getItem("compactMode") === "true", // Compact mode is initially unset on purpose
|
||||
logMessages: [],
|
||||
currentCameraIndex: 0,
|
||||
selectedOutputs: [0, 1], // 0 indicates normal, 1 indicates threshold
|
||||
cameraSettings: [ // This is a list of objects representing the settings of all cameras
|
||||
{
|
||||
tiltDegrees: 0.0,
|
||||
@@ -57,8 +57,6 @@ export default new Vuex.Store({
|
||||
hsvHue: [0, 15],
|
||||
hsvSaturation: [0, 15],
|
||||
hsvValue: [0, 25],
|
||||
erode: false,
|
||||
dilate: false,
|
||||
contourArea: [0, 12],
|
||||
contourRatio: [0, 12],
|
||||
contourFullness: [0, 12],
|
||||
@@ -66,6 +64,8 @@ export default new Vuex.Store({
|
||||
contourGroupingMode: 0,
|
||||
contourIntersection: 0,
|
||||
contourSortMode: 0,
|
||||
inputShouldShow: true,
|
||||
outputShouldShow: true,
|
||||
outputShouldDraw: true,
|
||||
outputShowMultipleTargets: false,
|
||||
offsetRobotOffsetMode: 0,
|
||||
@@ -108,8 +108,8 @@ export default new Vuex.Store({
|
||||
// Below options are only configurable if supported is true
|
||||
connectionType: 0, // 0 = DHCP, 1 = Static
|
||||
staticIp: "",
|
||||
netmask: "",
|
||||
hostname: "photonvision",
|
||||
runNTServer: false,
|
||||
},
|
||||
lighting: {
|
||||
supported: true,
|
||||
@@ -119,13 +119,21 @@ export default new Vuex.Store({
|
||||
calibrationData: {
|
||||
count: 0,
|
||||
videoModeIndex: 0,
|
||||
minCount: 25,
|
||||
minCount: 12, // Gets set by backend anyways, but we need a sane default
|
||||
hasEnough: false,
|
||||
squareSizeIn: 1.0,
|
||||
patternWidth: 7,
|
||||
patternHeight: 7,
|
||||
patternWidth: 8,
|
||||
patternHeight: 8,
|
||||
boardType: 0, // Chessboard, dotboard
|
||||
},
|
||||
metrics: {
|
||||
cpuTemp: "N/A",
|
||||
cpuUtil: "N/A",
|
||||
cpuMem: "N/A",
|
||||
gpuMem: "N/A",
|
||||
ramUtil: "N/A",
|
||||
gpuMemUtil: "N/A",
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
compactMode: set('compactMode'),
|
||||
@@ -134,9 +142,10 @@ export default new Vuex.Store({
|
||||
selectedOutputs: set('selectedOutputs'),
|
||||
settings: set('settings'),
|
||||
calibrationData: set('calibrationData'),
|
||||
metrics: set('metrics'),
|
||||
logString: (state, newStr) => {
|
||||
const str = state.logMessages;
|
||||
str.push(newStr)
|
||||
str.push(newStr);
|
||||
Vue.set(state, 'logString', str)
|
||||
},
|
||||
|
||||
@@ -172,6 +181,17 @@ export default new Vuex.Store({
|
||||
}
|
||||
},
|
||||
|
||||
mutateNetworkSettings: (state, payload) => {
|
||||
for (let key in payload) {
|
||||
if (!payload.hasOwnProperty(key)) continue;
|
||||
const value = payload[key];
|
||||
const settings = state.settings.networkSettings;
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
Vue.set(settings, key, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mutatePipelineResults(state, payload) {
|
||||
// Key: index, value: result
|
||||
for (let key in payload) {
|
||||
@@ -181,8 +201,12 @@ export default new Vuex.Store({
|
||||
Vue.set(state, 'pipelineResults', payload[key])
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
mutateEnabledLEDPercentage(state, payload) {
|
||||
const settings = state.settings;
|
||||
settings.lighting.brightness = payload;
|
||||
Vue.set(state, "settings", settings);
|
||||
},
|
||||
|
||||
mutateCalibrationState: (state, payload) => {
|
||||
@@ -215,6 +239,7 @@ export default new Vuex.Store({
|
||||
currentCameraIndex: state => state.currentCameraIndex,
|
||||
currentPipelineIndex: state => state.cameraSettings[state.currentCameraIndex].currentPipelineIndex,
|
||||
currentPipelineSettings: state => state.cameraSettings[state.currentCameraIndex].currentPipelineSettings,
|
||||
currentVideoFormat: state => state.cameraSettings[state.currentCameraIndex].videoFormatList[state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.cameraVideoModeIndex],
|
||||
videoFormatList: state => {
|
||||
return Object.values(state.cameraSettings[state.currentCameraIndex].videoFormatList); // convert to a list
|
||||
},
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
/>
|
||||
<CVnumberinput
|
||||
v-model="cameraSettings.fov"
|
||||
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame' : 'This setting is managed by a vendor'"
|
||||
name="Diagonal FOV"
|
||||
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.' : 'This setting is managed by a vendor'"
|
||||
name="Maximum diagonal FOV"
|
||||
:disabled="!cameraSettings.isFovConfigurable"
|
||||
/>
|
||||
<br>
|
||||
@@ -93,14 +93,14 @@
|
||||
v-model="boardWidth"
|
||||
name="Board width"
|
||||
label-cols="5"
|
||||
tooltip="Width of the board in dots or corners; with the standard chessboard, this is usually 7"
|
||||
tooltip="Width of the board in dots or chessboard squares; with the standard chessboard, this is usually 8"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<CVnumberinput
|
||||
v-model="boardHeight"
|
||||
name="Board height"
|
||||
label-cols="5"
|
||||
tooltip="Height of the board in dots or corners; with the standard chessboard, this is usually 7"
|
||||
tooltip="Height of the board in dots or chessboard squares; with the standard chessboard, this is usually 8"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
</v-col>
|
||||
@@ -143,7 +143,7 @@
|
||||
v-for="(value, index) in filteredResolutionList"
|
||||
:key="index"
|
||||
>
|
||||
<td> {{ value.width }} X {{ value.height }} </td>
|
||||
<td> {{ value.width }} X {{ value.height }}</td>
|
||||
<td>
|
||||
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
|
||||
</td>
|
||||
@@ -178,7 +178,7 @@
|
||||
@input="e => handlePipelineUpdate('cameraExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="this.$store.getters.currentPipelineSettings.cameraBrightness"
|
||||
v-model="$store.getters.currentPipelineSettings.cameraBrightness"
|
||||
name="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@@ -218,7 +218,7 @@
|
||||
:disabled="checkCancellation"
|
||||
@click="sendCalibrationFinish"
|
||||
>
|
||||
{{ hasEnough ? "End Calibration" : "Cancel Calibration" }}
|
||||
{{ hasEnough ? "Finish Calibration" : "Cancel Calibration" }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
@@ -250,21 +250,72 @@
|
||||
cols="12"
|
||||
md="5"
|
||||
>
|
||||
<CVimage
|
||||
:address="$store.getters.streamAddress[1]"
|
||||
:disconnected="!$store.state.backendConnected"
|
||||
scale="100"
|
||||
style="border-radius: 5px;"
|
||||
/>
|
||||
<template>
|
||||
<CVimage
|
||||
:address="$store.getters.streamAddress[1]"
|
||||
:disconnected="!$store.state.backendConnected"
|
||||
scale="100"
|
||||
style="border-radius: 5px;"
|
||||
/>
|
||||
<v-dialog
|
||||
v-model="snack"
|
||||
width="500px"
|
||||
:persistent="true"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title> Camera Calibration </v-card-title>
|
||||
<div
|
||||
class="ml-3"
|
||||
>
|
||||
<v-col align="center">
|
||||
<template v-if="calibrationInProgress && !calibrationFailed">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
:size="70"
|
||||
:width="8"
|
||||
color="accent"
|
||||
/>
|
||||
<v-card-text>Camera is being calibrated. This process make take several minutes...</v-card-text>
|
||||
</template>
|
||||
<template v-else-if="!calibrationFailed">
|
||||
<v-icon
|
||||
color="green"
|
||||
size="70"
|
||||
>
|
||||
mdi-check-bold
|
||||
</v-icon>
|
||||
<v-card-text>Camera has been successfully calibrated at {{ stringResolutionList[selectedFilteredResIndex] }}!</v-card-text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon
|
||||
color="red"
|
||||
size="70"
|
||||
>
|
||||
mdi-close
|
||||
</v-icon>
|
||||
<v-card-text>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align with the corners of the chessboard, and try again. More information is available in the program logs.</v-card-text>
|
||||
</template>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="!calibrationInProgress || calibrationFailed"
|
||||
color="white"
|
||||
text
|
||||
@click="closeDialog"
|
||||
>
|
||||
OK
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -278,7 +329,7 @@ import TooltippedLabel from "../components/common/cv-tooltipped-label";
|
||||
export default {
|
||||
name: 'Cameras',
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
TooltippedLabel,
|
||||
CVselect,
|
||||
CVnumberinput,
|
||||
CVslider,
|
||||
@@ -286,12 +337,10 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
},
|
||||
snack: false,
|
||||
filteredVideomodeIndex: 0
|
||||
calibrationInProgress: false,
|
||||
calibrationFailed: false,
|
||||
filteredVideomodeIndex: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -325,7 +374,7 @@ export default {
|
||||
if (!filtered.some(e => e.width === it.width && e.height === it.height)) {
|
||||
it['index'] = i;
|
||||
const calib = this.getCalibrationCoeffs(it);
|
||||
if(calib != null) {
|
||||
if (calib != null) {
|
||||
it['standardDeviation'] = calib.standardDeviation;
|
||||
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
|
||||
}
|
||||
@@ -409,7 +458,6 @@ export default {
|
||||
return this.$store.getters.currentPipelineIndex === -2;
|
||||
}
|
||||
},
|
||||
|
||||
selectedFilteredResIndex: {
|
||||
get() {
|
||||
return this.filteredVideomodeIndex
|
||||
@@ -422,12 +470,16 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
closeDialog() {
|
||||
this.snack = false;
|
||||
this.calibrationInProgress = false;
|
||||
this.calibrationFailed = false;
|
||||
},
|
||||
getCalibrationCoeffs(resolution) {
|
||||
const calList = this.$store.getters.calibrationList;
|
||||
let ret = null;
|
||||
calList.forEach(cal => {
|
||||
if(cal.width === resolution.width && cal.height === resolution.height) {
|
||||
if (cal.width === resolution.width && cal.height === resolution.height) {
|
||||
ret = cal
|
||||
}
|
||||
});
|
||||
@@ -476,34 +528,19 @@ export default {
|
||||
sendCalibrationFinish() {
|
||||
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
|
||||
|
||||
this.snackbar.text = "Calibrating...";
|
||||
this.snackbar.color = "secondary";
|
||||
this.snack = true;
|
||||
this.calibrationInProgress = true;
|
||||
|
||||
this.axios.post("http://" + this.$address + "/api/settings/endCalibration", this.$store.getters.currentCameraIndex)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Calibration successful! \n" +
|
||||
"Standard deviation: " + response.data.toFixed(5)
|
||||
};
|
||||
this.snack = true;
|
||||
if (response.status === 200) {
|
||||
this.calibrationInProgress = false;
|
||||
} else {
|
||||
this.calibrationFailed = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Calibration Failed!"
|
||||
};
|
||||
this.snack = true;
|
||||
}
|
||||
}
|
||||
).catch(() => {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Calibration Failed!"
|
||||
};
|
||||
this.snack = true;
|
||||
).catch(() => {
|
||||
this.calibrationFailed = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -511,18 +548,19 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-data-table {
|
||||
text-align: center;
|
||||
background-color: transparent !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.v-data-table th {
|
||||
background-color: #006492 !important;
|
||||
}
|
||||
.v-data-table {
|
||||
text-align: center;
|
||||
background-color: transparent !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-data-table th,td {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
.v-data-table th {
|
||||
background-color: #006492 !important;
|
||||
}
|
||||
|
||||
.v-data-table th, td {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +1,69 @@
|
||||
<template>
|
||||
<v-card
|
||||
dark
|
||||
class="pt-3"
|
||||
color="primary"
|
||||
flat
|
||||
>
|
||||
<v-card-title>
|
||||
View Program Logs
|
||||
<v-card
|
||||
dark
|
||||
class="pt-3"
|
||||
color="primary"
|
||||
flat
|
||||
>
|
||||
<v-card-title>
|
||||
View Program Logs
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
style="margin-left: auto;"
|
||||
depressed
|
||||
@click="download('photonlog.log', rawLogs.map(it => it.message).join('\n'))"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon>
|
||||
Download Log
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<div class="pr-6 pl-6">
|
||||
<v-btn-toggle
|
||||
v-model="logLevel"
|
||||
dark
|
||||
multiple
|
||||
class="fill mb-4"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
style="margin-left: auto;"
|
||||
depressed
|
||||
@click="download('photonlog.log', rawLogs.map(it => it.message).join('\n'))"
|
||||
v-for="(level) in possibleLevelArray"
|
||||
:key="level"
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon>
|
||||
Download Log
|
||||
{{ level }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<div class="pr-6 pl-6">
|
||||
<v-btn-toggle
|
||||
v-model="logLevel"
|
||||
dark
|
||||
multiple
|
||||
class="fill mb-4"
|
||||
>
|
||||
<v-btn
|
||||
v-for="(level) in possibleLevelArray"
|
||||
:key="level"
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
{{ level }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<!-- Logs -->
|
||||
</v-btn-toggle>
|
||||
<!-- Logs -->
|
||||
|
||||
<v-virtual-scroll
|
||||
:items="logMessageArray"
|
||||
item-height="50"
|
||||
height="600"
|
||||
>
|
||||
<template v-slot="{ item }">
|
||||
<div :class="[getColor(item) + '--text', 'log-item']">{{ item.message }}</div>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
<v-virtual-scroll
|
||||
:items="logMessageArray"
|
||||
item-height="50"
|
||||
height="600"
|
||||
>
|
||||
<template v-slot="{ item }">
|
||||
<div :class="[getColor(item) + '--text', 'log-item']">
|
||||
{{ item.message }}
|
||||
</div>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="white"
|
||||
text
|
||||
@click="$store.state.logsOverlay = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="white"
|
||||
text
|
||||
@click="$store.state.logsOverlay = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -26,6 +26,18 @@
|
||||
style="height: 15%; min-height: 50px;"
|
||||
>
|
||||
Cameras
|
||||
<v-chip
|
||||
:class="fpsTooLow ? 'ml-2 mt-1' : 'mt-2'"
|
||||
x-small
|
||||
label
|
||||
:color="fpsTooLow ? 'red' : 'transparent'"
|
||||
:text-color="fpsTooLow ? 'white' : 'grey'"
|
||||
>
|
||||
<span class="pr-1">{{ Math.round($store.state.pipelineResults.fps) }} FPS –</span>
|
||||
<span v-if="!fpsTooLow">{{ Math.round($store.state.pipelineResults.latency) }} ms latency</span>
|
||||
<span v-else-if="!$store.getters.currentPipelineSettings.inputShouldShow">HSV thresholds are too broad; narrow them for better performance</span>
|
||||
<span v-else>stop viewing the color stream for better performance</span>
|
||||
</v-chip>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
label="Driver Mode"
|
||||
@@ -45,8 +57,9 @@
|
||||
style="height: 100%;"
|
||||
>
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<cvImage
|
||||
<cv-image
|
||||
:id="idx === 0 ? 'normal-stream' : ''"
|
||||
ref="streams"
|
||||
:address="$store.getters.streamAddress[idx]"
|
||||
:disconnected="!$store.state.backendConnected"
|
||||
scale="100"
|
||||
@@ -54,7 +67,7 @@
|
||||
:max-height-md="$store.getters.isDriverMode ? '50vh' : '320px'"
|
||||
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
|
||||
:alt="'Stream' + idx"
|
||||
:color-picking="$store.state.colorPicking && idx == 0"
|
||||
:color-picking="$store.state.colorPicking && idx === 0"
|
||||
@click="onImageClick"
|
||||
/>
|
||||
</div>
|
||||
@@ -71,7 +84,10 @@
|
||||
<v-card
|
||||
color="primary"
|
||||
>
|
||||
<camera-and-pipeline-select />
|
||||
<!-- <v-btn @click="onCamNameChange">-->
|
||||
<!-- Reload-->
|
||||
<!-- </v-btn>-->
|
||||
<camera-and-pipeline-select @camera-name-changed="reloadStreams" />
|
||||
</v-card>
|
||||
<v-card
|
||||
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
|
||||
@@ -144,7 +160,7 @@
|
||||
v-for="(tabs, idx) in tabGroups"
|
||||
:key="idx"
|
||||
:cols="Math.floor(12 / tabGroups.length)"
|
||||
:class="idx != tabGroups.length - 1 ? 'pr-3' : ''"
|
||||
:class="idx !== tabGroups.length - 1 ? 'pr-3' : ''"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-card
|
||||
@@ -212,9 +228,7 @@
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
Because the current resolution {{ this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].width }}
|
||||
x {{ this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].height }}
|
||||
is not yet calibrated, 3D mode cannot be enabled. Please
|
||||
Because the current resolution {{ this.$store.getters.currentVideoFormat.width }} x {{ this.$store.getters.currentVideoFormat.height }} is not yet calibrated, 3D mode cannot be enabled. Please
|
||||
<a
|
||||
href="/#/cameras"
|
||||
class="white--text"
|
||||
@@ -240,202 +254,219 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
|
||||
import cvImage from '../components/common/cv-image';
|
||||
import InputTab from './PipelineViews/InputTab';
|
||||
import ThresholdTab from './PipelineViews/ThresholdTab';
|
||||
import ContoursTab from './PipelineViews/ContoursTab';
|
||||
import OutputTab from './PipelineViews/OutputTab';
|
||||
import TargetsTab from "./PipelineViews/TargetsTab";
|
||||
import PnPTab from './PipelineViews/PnPTab';
|
||||
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
|
||||
import cvImage from '../components/common/cv-image';
|
||||
import InputTab from './PipelineViews/InputTab';
|
||||
import ThresholdTab from './PipelineViews/ThresholdTab';
|
||||
import ContoursTab from './PipelineViews/ContoursTab';
|
||||
import OutputTab from './PipelineViews/OutputTab';
|
||||
import TargetsTab from "./PipelineViews/TargetsTab";
|
||||
import PnPTab from './PipelineViews/PnPTab';
|
||||
|
||||
export default {
|
||||
name: 'CameraTab',
|
||||
components: {
|
||||
CameraAndPipelineSelect,
|
||||
cvImage,
|
||||
InputTab,
|
||||
ThresholdTab,
|
||||
ContoursTab,
|
||||
OutputTab,
|
||||
TargetsTab,
|
||||
PnPTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTabsData: [0, 0, 0, 0],
|
||||
snackbar: false,
|
||||
counterData: 0,
|
||||
dialog: false,
|
||||
processingModeOverride: false
|
||||
export default {
|
||||
name: 'Pipeline',
|
||||
components: {
|
||||
CameraAndPipelineSelect,
|
||||
cvImage,
|
||||
InputTab,
|
||||
ThresholdTab,
|
||||
ContoursTab,
|
||||
OutputTab,
|
||||
TargetsTab,
|
||||
PnPTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTabsData: [0, 0, 0, 0],
|
||||
snackbar: false,
|
||||
counterData: 0,
|
||||
dialog: false,
|
||||
processingModeOverride: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedTabs: {
|
||||
get() {
|
||||
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
|
||||
},
|
||||
set(value) {
|
||||
this.selectedTabsData = value;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedTabs: {
|
||||
get() {
|
||||
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
|
||||
},
|
||||
set(value) {
|
||||
this.selectedTabsData = value;
|
||||
}
|
||||
},
|
||||
tabGroups: {
|
||||
get() {
|
||||
let tabs = {
|
||||
input: {
|
||||
name: "Input",
|
||||
component: "InputTab",
|
||||
},
|
||||
threshold: {
|
||||
name: "Threshold",
|
||||
component: "ThresholdTab",
|
||||
},
|
||||
contours: {
|
||||
name: "Contours",
|
||||
component: "ContoursTab",
|
||||
},
|
||||
output: {
|
||||
name: "Output",
|
||||
component: "OutputTab",
|
||||
},
|
||||
targets: {
|
||||
name: "Target Info",
|
||||
component: "TargetsTab",
|
||||
},
|
||||
pnp: {
|
||||
name: "3D",
|
||||
component: "PnPTab",
|
||||
}
|
||||
};
|
||||
|
||||
// 2D array of tab names and component names; each sub-array is a separate tab group
|
||||
let ret = [];
|
||||
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
|
||||
// One big tab group with all the tabs
|
||||
ret[0] = Object.values(tabs);
|
||||
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
|
||||
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
|
||||
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.output];
|
||||
ret[1] = [tabs.targets, tabs.pnp];
|
||||
} else if (this.$vuetify.breakpoint.lgAndDown) {
|
||||
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
|
||||
ret[0] = [tabs.input];
|
||||
ret[1] = [tabs.threshold, tabs.contours, tabs.output];
|
||||
ret[2] = [tabs.targets, tabs.pnp];
|
||||
} else if (this.$vuetify.breakpoint.xl) {
|
||||
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
|
||||
ret[0] = [tabs.input];
|
||||
ret[1] = [tabs.threshold];
|
||||
ret[2] = [tabs.contours, tabs.output]
|
||||
ret[3] = [tabs.targets, tabs.pnp];
|
||||
tabGroups: {
|
||||
get() {
|
||||
let tabs = {
|
||||
input: {
|
||||
name: "Input",
|
||||
component: "InputTab",
|
||||
},
|
||||
threshold: {
|
||||
name: "Threshold",
|
||||
component: "ThresholdTab",
|
||||
},
|
||||
contours: {
|
||||
name: "Contours",
|
||||
component: "ContoursTab",
|
||||
},
|
||||
output: {
|
||||
name: "Output",
|
||||
component: "OutputTab",
|
||||
},
|
||||
targets: {
|
||||
name: "Target Info",
|
||||
component: "TargetsTab",
|
||||
},
|
||||
pnp: {
|
||||
name: "3D",
|
||||
component: "PnPTab",
|
||||
}
|
||||
};
|
||||
|
||||
// 2D array of tab names and component names; each sub-array is a separate tab group
|
||||
let ret = [];
|
||||
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
|
||||
// One big tab group with all the tabs
|
||||
ret[0] = Object.values(tabs);
|
||||
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
|
||||
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
|
||||
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.output];
|
||||
ret[1] = [tabs.targets, tabs.pnp];
|
||||
} else if (this.$vuetify.breakpoint.lgAndDown) {
|
||||
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
|
||||
ret[0] = [tabs.input];
|
||||
ret[1] = [tabs.threshold, tabs.contours, tabs.output];
|
||||
ret[2] = [tabs.targets, tabs.pnp];
|
||||
} else if (this.$vuetify.breakpoint.xl) {
|
||||
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
|
||||
ret[0] = [tabs.input];
|
||||
ret[1] = [tabs.threshold];
|
||||
ret[2] = [tabs.contours, tabs.output];
|
||||
ret[3] = [tabs.targets, tabs.pnp];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
processingMode: {
|
||||
get() {
|
||||
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
|
||||
},
|
||||
set(value) {
|
||||
if (this.$store.getters.isCalibrated) {
|
||||
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
driverMode: {
|
||||
get() {
|
||||
return this.$store.getters.isDriverMode;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
|
||||
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
|
||||
}
|
||||
},
|
||||
selectedOutputs: {
|
||||
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
|
||||
get() {
|
||||
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
|
||||
let ret = [];
|
||||
if (this.$store.state.colorPicking) {
|
||||
ret = [0]; // We want the input stream only while color picking
|
||||
} else if (this.$store.getters.isDriverMode) {
|
||||
ret = [1]; // We want only the output stream in driver mode
|
||||
} else {
|
||||
if (this.$store.getters.currentPipelineSettings.inputShouldShow) ret = ret.concat([0]);
|
||||
if (this.$store.getters.currentPipelineSettings.outputShouldShow) ret = ret.concat([1]);
|
||||
if (!ret.length) ret = [0];
|
||||
}
|
||||
|
||||
if (this.$vuetify.breakpoint.mdAndUp) {
|
||||
return ret;
|
||||
} else {
|
||||
return ret[0] || 0;
|
||||
}
|
||||
},
|
||||
processingMode: {
|
||||
get() {
|
||||
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
|
||||
},
|
||||
set(value) {
|
||||
if (this.$store.getters.isCalibrated) {
|
||||
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
|
||||
}
|
||||
set(value) {
|
||||
let valToCommit = [0];
|
||||
if (value instanceof Array) {
|
||||
// Value is already an array, we don't need to do anything
|
||||
valToCommit = value;
|
||||
} else if (value) {
|
||||
// Value is assumed to be a number, so we wrap it into an array
|
||||
valToCommit = [value];
|
||||
}
|
||||
},
|
||||
driverMode: {
|
||||
get() {
|
||||
return this.$store.getters.isDriverMode;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
|
||||
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
|
||||
}
|
||||
},
|
||||
selectedOutputs: {
|
||||
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
|
||||
get() {
|
||||
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
|
||||
let ret;
|
||||
if (this.$store.state.colorPicking) {
|
||||
ret = [0]; // We want the input stream only while color picking
|
||||
} else if (!this.$store.getters.isDriverMode) {
|
||||
ret = this.$store.state.selectedOutputs || [0];
|
||||
} else {
|
||||
ret = [1]; // We want the output stream in driver mode
|
||||
}
|
||||
|
||||
if (this.$vuetify.breakpoint.mdAndUp) {
|
||||
return ret;
|
||||
} else {
|
||||
return ret[0] || 0;
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
let valToCommit = [0];
|
||||
if (value instanceof Array) {
|
||||
// Value is already an array, we don't need to do anything
|
||||
value.sort(); // Sort for visual consistency
|
||||
valToCommit = value;
|
||||
} else if (value) {
|
||||
// Value is assumed to be a number, so we wrap it into an array
|
||||
valToCommit = [value];
|
||||
}
|
||||
this.$store.commit("selectedOutputs", valToCommit);
|
||||
// TODO: Currently the backend just sends both streams regardless of the selected outputs value, so we don't need to send anything
|
||||
// this.handlePipelineUpdate('selectedOutputs', valToCommit);
|
||||
}
|
||||
},
|
||||
latency: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineResults.latency;
|
||||
}
|
||||
this.$store.commit("mutatePipeline", {"inputShouldShow": valToCommit.includes(0)});
|
||||
this.$store.commit("mutatePipeline", {"outputShouldShow": valToCommit.includes(1)});
|
||||
this.handlePipelineUpdate("inputShouldShow", valToCommit.includes(0));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isCalibrated() {
|
||||
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
|
||||
return this.$store.getters.currentCameraSettings.calibrations
|
||||
.some(e => e.width === resolution.width && e.height === resolution.height)
|
||||
},
|
||||
onImageClick(event) {
|
||||
// Only run on the input stream
|
||||
if (event.target.alt !== "Stream0") return;
|
||||
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
|
||||
let ref = this.$refs["Threshold"];
|
||||
if (ref && ref[0])
|
||||
ref[0].onClick(event)
|
||||
},
|
||||
on3DClick() {
|
||||
if (!this.$store.getters.isCalibrated) {
|
||||
this.dialog = true;
|
||||
this.processingModeOverride = true;
|
||||
}
|
||||
},
|
||||
closeUncalibratedDialog() {
|
||||
this.dialog = false;
|
||||
this.processingModeOverride = false;
|
||||
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", false);
|
||||
fpsTooLow: {
|
||||
get() {
|
||||
// For now we only show the FPS is too low warning when GPU acceleration is enabled, because we don't really trust the presented video modes otherwise
|
||||
return this.$store.state.pipelineResults.fps - this.$store.getters.currentVideoFormat.fps < -5 && this.$store.state.pipelineResults.fps !== 0 && !this.$store.getters.isDriverMode && this.$store.state.settings.general.gpuAcceleration;
|
||||
}
|
||||
},
|
||||
latency: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineResults.latency;
|
||||
}
|
||||
},
|
||||
isCalibrated: {
|
||||
get() {
|
||||
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
|
||||
return this.$store.getters.currentCameraSettings.calibrations
|
||||
.some(e => e.width === resolution.width && e.height === resolution.height)
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.state.connectedCallbacks.push(this.reloadStreams)
|
||||
},
|
||||
methods: {
|
||||
reloadStreams() {
|
||||
// Reload the streams as we technically close and reopen them
|
||||
this.$refs.streams.forEach(it => it.reload())
|
||||
},
|
||||
onImageClick(event) {
|
||||
// Only run on the input stream
|
||||
if (event.target.alt !== "Stream0") return;
|
||||
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
|
||||
let ref = this.$refs["Threshold"];
|
||||
if (ref && ref[0])
|
||||
ref[0].onClick(event)
|
||||
},
|
||||
on3DClick() {
|
||||
if (!this.$store.getters.isCalibrated) {
|
||||
this.dialog = true;
|
||||
this.processingModeOverride = true;
|
||||
}
|
||||
},
|
||||
closeUncalibratedDialog() {
|
||||
this.dialog = false;
|
||||
this.processingModeOverride = false;
|
||||
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@
|
||||
name="Exposure"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraExposure')"
|
||||
@@ -55,7 +56,6 @@
|
||||
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
|
||||
:list="streamResolutionList"
|
||||
:select-cols="largeBox"
|
||||
@input="handlePipelineData('streamingFrameDivisor')"
|
||||
@rollback="e => rollback('streamingFrameDivisor', e)"
|
||||
/>
|
||||
</div>
|
||||
@@ -65,6 +65,8 @@
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
import CVselect from '../../components/common/cv-select'
|
||||
|
||||
const unfilteredStreamDivisors = [1, 2, 4, 6];
|
||||
|
||||
export default {
|
||||
name: 'Input',
|
||||
components: {
|
||||
@@ -74,7 +76,9 @@
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
rawStreamDivisorIndex: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
largeBox: {
|
||||
@@ -87,10 +91,10 @@
|
||||
},
|
||||
cameraExposure: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraExposure);
|
||||
return parseFloat(this.$store.getters.currentPipelineSettings.cameraExposure);
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraExposure": parseInt(val)});
|
||||
this.$store.commit("mutatePipeline", {"cameraExposure": parseFloat(val)});
|
||||
}
|
||||
},
|
||||
cameraBrightness: {
|
||||
@@ -119,18 +123,22 @@
|
||||
},
|
||||
cameraVideoModeIndex: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.cameraVideoModeIndex
|
||||
return this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
|
||||
|
||||
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors());
|
||||
this.rawStreamDivisorIndex = 0;
|
||||
}
|
||||
},
|
||||
streamingFrameDivisor: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor
|
||||
return this.rawStreamDivisorIndex;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
|
||||
this.rawStreamDivisorIndex = val;
|
||||
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -146,21 +154,31 @@
|
||||
|
||||
streamResolutionList: {
|
||||
get() {
|
||||
let cam_res = this.$store.getters.videoFormatList[
|
||||
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex]
|
||||
const 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) {
|
||||
for (const x of this.getRawStreamDivisors()) {
|
||||
tmp_list.push(`${Math.floor(cam_res['width'] / x)} X ${Math.floor(cam_res['height'] / x)}`);
|
||||
}
|
||||
return tmp_list;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
methods: {
|
||||
getRawStreamDivisors() {
|
||||
// Limit stream res when GPU acceleration is enabled because we *know* that we won't be able to get smooth streams above ~640x480
|
||||
// It would probably be cleaner if this checked that we're on the Raspi 3 instead of checking for GPU accel status
|
||||
const width = this.$store.getters.videoFormatList[
|
||||
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex]['width'];
|
||||
return unfilteredStreamDivisors.filter((x) => !this.$store.state.settings.general.gpuAcceleration || width / x < 400);
|
||||
},
|
||||
getNumSkippedStreamDivisors() {
|
||||
return unfilteredStreamDivisors.length - this.getRawStreamDivisors().length;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
label="Select a target model"
|
||||
:items="FRCtargets"
|
||||
:items="targetList"
|
||||
item-text="name"
|
||||
item-value="data"
|
||||
@change="onModelSelect"
|
||||
@input="handlePipelineUpdate('targetModel', targetList.indexOf(selectedModel))"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="cornerDetectionAccuracyPercentage"
|
||||
@@ -51,28 +51,34 @@
|
||||
import Papa from 'papaparse';
|
||||
import miniMap from '../../components/pipeline/3D/MiniMap';
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
import FRCtargetsConfig from '../../assets/FRCtargets'
|
||||
|
||||
export default {
|
||||
name: "SolvePNP",
|
||||
name: "PnP",
|
||||
components: {
|
||||
CVslider,
|
||||
miniMap
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
FRCtargets: null,
|
||||
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', 'Power Cell (7in)', '2016 High Goal'], //Keep in sync with TargetModel.java
|
||||
snackbar: {
|
||||
color: "Success",
|
||||
text: ""
|
||||
},
|
||||
snack: false,
|
||||
selectedModel: {
|
||||
isCustom: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedModel: {
|
||||
get() {
|
||||
let ret = this.$store.getters.currentPipelineSettings.targetModel
|
||||
console.log(ret)
|
||||
return this.targetList[ret];
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"targetModel": this.targetList.indexOf(val)})
|
||||
}
|
||||
},
|
||||
cornerDetectionAccuracyPercentage: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.cornerDetectionAccuracyPercentage
|
||||
@@ -97,20 +103,6 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
let tmp = [];
|
||||
for (let t in FRCtargetsConfig) {
|
||||
if (FRCtargetsConfig.hasOwnProperty(t)) {
|
||||
tmp.push({name: t, data: FRCtargetsConfig[t]});
|
||||
}
|
||||
}
|
||||
|
||||
// Special dropdown item for uploading your own model
|
||||
// data is what gets put in selectedMode, so we add a special field
|
||||
tmp.push({name: "Custom model", data: {isCustom: true}});
|
||||
|
||||
this.FRCtargets = tmp;
|
||||
},
|
||||
methods: {
|
||||
readFile(event) {
|
||||
let file = event.target.files[0];
|
||||
@@ -119,13 +111,6 @@
|
||||
skipEmptyLines: true
|
||||
});
|
||||
},
|
||||
onModelSelect() {
|
||||
if (this.selectedModel.isCustom) {
|
||||
this.$refs.file.click();
|
||||
} else {
|
||||
this.uploadPremade();
|
||||
}
|
||||
},
|
||||
onParse(result) {
|
||||
if (result.data.length > 0) {
|
||||
let data = [];
|
||||
@@ -158,29 +143,6 @@
|
||||
this.selectedModel = null;
|
||||
}
|
||||
},
|
||||
uploadPremade() {
|
||||
this.uploadModel(this.selectedModel, true);
|
||||
},
|
||||
uploadModel(model, premade = false) {
|
||||
this.axios.post("http://" + this.$address + "/api/vision/pnpModel", {
|
||||
['targetModel']: model,
|
||||
['index']: this.$store.getters.currentCameraIndex
|
||||
}).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: premade ? "Target model changed successfully" : "Custom target model uploaded and selected successfully"
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(() => {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "An error occurred selecting a target model"
|
||||
};
|
||||
this.snack = true;
|
||||
|
||||
this.selectedModel = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -84,33 +84,16 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-row>
|
||||
<v-divider class="mb-3" />
|
||||
<CVswitch
|
||||
v-model="erode"
|
||||
name="Erode"
|
||||
tooltip="Removes pixels around the edges of white areas in the thresholded image"
|
||||
@input="handlePipelineData('erode')"
|
||||
@rollback="e => rollback('erode',e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="dilate"
|
||||
name="Dilate"
|
||||
tooltip="Adds pixels around the edges of white areas in the thresholded image"
|
||||
@input="handlePipelineData('dilate')"
|
||||
@rollback="e => rollback('dilate',e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVrangeSlider from '../../components/common/cv-range-slider'
|
||||
import CVswitch from '../../components/common/cv-switch'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'Threshold',
|
||||
components: {
|
||||
CVrangeSlider,
|
||||
CVswitch
|
||||
CVrangeSlider
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
@@ -145,23 +128,7 @@
|
||||
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;
|
||||
@@ -207,6 +174,7 @@
|
||||
case 0:
|
||||
this.currentFunction = undefined;
|
||||
this.$store.state.colorPicking = false;
|
||||
|
||||
this.handlePipelineUpdate("outputShouldDraw", true);
|
||||
return;
|
||||
case 1:
|
||||
@@ -220,7 +188,10 @@
|
||||
break;
|
||||
}
|
||||
this.$store.state.colorPicking = true;
|
||||
|
||||
this.handlePipelineUpdate("outputShouldDraw", false);
|
||||
this.$store.commit("mutatePipeline", {"inputShouldShow": true});
|
||||
this.handlePipelineUpdate("inputShouldShow", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,32 +8,19 @@
|
||||
cols="12"
|
||||
style="max-width: 1400px"
|
||||
>
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
<v-card
|
||||
v-for="item in tabList"
|
||||
:key="item.name"
|
||||
dark
|
||||
class="mb-3 pr-6 pb-3"
|
||||
style="background-color: #006492;"
|
||||
>
|
||||
<v-card
|
||||
v-for="item in tabList"
|
||||
:key="item.name"
|
||||
dark
|
||||
class="mb-3 pr-6 pb-3"
|
||||
style="background-color: #006492;"
|
||||
>
|
||||
<v-card-title>{{ item.name }}</v-card-title>
|
||||
<component
|
||||
:is="item"
|
||||
class="ml-5"
|
||||
/>
|
||||
</v-card>
|
||||
<v-btn
|
||||
color="accent"
|
||||
style="color: black; width: 100%;"
|
||||
:disabled="!valid"
|
||||
@click="sendGeneralSettings()"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-form>
|
||||
<v-card-title>{{ item.name }}</v-card-title>
|
||||
<component
|
||||
:is="item"
|
||||
class="ml-5"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar
|
||||
@@ -61,7 +48,6 @@
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 0,
|
||||
valid: true, // Are all settings valid
|
||||
snack: false,
|
||||
snackbar: {
|
||||
color: "accent",
|
||||
@@ -86,28 +72,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendGeneralSettings() {
|
||||
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
|
||||
function (response) {
|
||||
if (response.status === 200) {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Settings updated successfully"
|
||||
};
|
||||
this.snack = true;
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: (error.response || {data: "Couldn't save settings"}).data
|
||||
};
|
||||
this.snack = true;
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +1,92 @@
|
||||
<template>
|
||||
<div>
|
||||
<span>Version: {{ settings.version }}</span>
|
||||
—
|
||||
<span>Hardware model: {{ settings.hardwareModel }}</span>
|
||||
—
|
||||
<span>Platform: {{ settings.hardwarePlatform }}</span>
|
||||
—
|
||||
<span>GPU Acceleration: {{ settings.gpuAcceleration ? "Enabled" : "Unsupported" }}{{ settings.gpuAcceleration ? " (" + settings.gpuAcceleration + " mode)" : "" }}</span>
|
||||
<v-row class="pa-4">
|
||||
<table class="infoTable">
|
||||
<tr>
|
||||
<th class="infoElem">
|
||||
Version
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
Hardware Model
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
Platform
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
GPU Acceleration
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="infoElem">
|
||||
{{ version.replace(" ", "") }}
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ hwModel.replace(" ", "") }}
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ platform.replace(" ", "") }}
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ gpuAccel.replace(" ", "") }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="infoTable">
|
||||
<tr>
|
||||
<th class="infoElem">
|
||||
CPU Usage
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
CPU Temp
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
CPU Memory Usage
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
GPU Memory Usage
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
Disk Usage
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="metrics.cpuUtil !== 'N/A'">
|
||||
<td class="infoElem">
|
||||
{{ metrics.cpuUtil.replace(" ", "") }}%
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ parseInt(metrics.cpuTemp) }}° C
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.ramUtil.replace(" ", "") }}MB of {{ metrics.cpuMem }}MB
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.gpuMemUtil.replace(" ", "") }}MB of {{ metrics.gpuMem }}MB
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.diskUtilPct.replace(" ", "") }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="metrics.cpuUtil === 'N/A'">
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
@@ -19,7 +99,8 @@
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon> Export Settings
|
||||
</v-icon>
|
||||
Export Settings
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
@@ -33,7 +114,8 @@
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-upload
|
||||
</v-icon> Import Settings
|
||||
</v-icon>
|
||||
Import Settings
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
@@ -42,11 +124,12 @@
|
||||
>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="axios.post('http://' + this.$address + '/api/restartProgram')"
|
||||
@click="restartProgram()"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-restart
|
||||
</v-icon> Restart Photon
|
||||
</v-icon>
|
||||
Restart Photon
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
@@ -55,11 +138,12 @@
|
||||
>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="axios.post('http://' + this.$address + '/api/restartDevice')"
|
||||
@click="restartDevice()"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-restart
|
||||
</v-icon> Restart Device
|
||||
</v-icon>
|
||||
Restart Device
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -76,7 +160,7 @@
|
||||
<input
|
||||
ref="importSettings"
|
||||
type="file"
|
||||
accept=".zip"
|
||||
accept=".zip, .json"
|
||||
style="display: none;"
|
||||
|
||||
@change="readImportedSettings"
|
||||
@@ -106,9 +190,35 @@ export default {
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$store.state.settings.general;
|
||||
},
|
||||
version() {
|
||||
return `${this.settings.version}`;
|
||||
},
|
||||
hwModel() {
|
||||
if (this.settings.hardwareModel !== '') {
|
||||
return `${this.settings.hardwareModel}`;
|
||||
} else {
|
||||
return `Unknown`;
|
||||
}
|
||||
},
|
||||
platform() {
|
||||
return `${this.settings.hardwarePlatform}`;
|
||||
},
|
||||
gpuAccel() {
|
||||
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${this.settings.gpuAcceleration ? "(" + this.settings.gpuAcceleration + ")" : ""}`
|
||||
},
|
||||
metrics() {
|
||||
console.log(this.$store.state.metrics);
|
||||
return this.$store.state.metrics;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
restartProgram() {
|
||||
this.axios.post('http://' + this.$address + '/api/restartProgram', {});
|
||||
},
|
||||
restartDevice() {
|
||||
this.axios.post('http://' + this.$address + '/api/restartDevice', {});
|
||||
},
|
||||
readImportedSettings(event) {
|
||||
let formData = new FormData();
|
||||
formData.append("zipData", event.target.files[0]);
|
||||
@@ -119,11 +229,23 @@ export default {
|
||||
text: "Settings imported successfully! Program will now exit...",
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Settings imported successfully! Program will now exit...",
|
||||
};
|
||||
}).catch(err => {
|
||||
if (err.response) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading settings file! Could not process provided file.",
|
||||
};
|
||||
} else if (err.request) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading settings file! No respond to upload attempt.",
|
||||
};
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading settings file!",
|
||||
};
|
||||
}
|
||||
this.snack = true;
|
||||
});
|
||||
},
|
||||
@@ -135,4 +257,23 @@ export default {
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.infoTable{
|
||||
border: 1px solid;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0px;
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.infoElem {
|
||||
padding-right: 15px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
padding-left: 10px;
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVslider
|
||||
v-model="settings.brightness"
|
||||
v-model="enabledLEDPercentage"
|
||||
class="pt-2"
|
||||
slider-cols="12"
|
||||
name="Brightness"
|
||||
min="0"
|
||||
max="100"
|
||||
@input="handleData('accuracy')"
|
||||
@rollback="e => rollback('accuracy', e)"
|
||||
@input="handleData('enabledLEDPercentage')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,6 +21,14 @@
|
||||
CVslider,
|
||||
},
|
||||
computed: {
|
||||
enabledLEDPercentage: {
|
||||
get() {
|
||||
return this.settings.brightness
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("mutateEnabledLEDPercentage", value)
|
||||
}
|
||||
},
|
||||
isDHCP() {
|
||||
return this.settings.connectionType === 0;
|
||||
},
|
||||
|
||||
@@ -1,99 +1,182 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVnumberinput
|
||||
v-model="settings.teamNumber"
|
||||
name="Team Number"
|
||||
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
|
||||
/>
|
||||
<template v-if="$store.state.settings.networkSettings.supported">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="valid"
|
||||
>
|
||||
<CVSwitch
|
||||
v-model="runNTServer"
|
||||
name="Run NetworkTables Server"
|
||||
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
|
||||
/>
|
||||
<CVnumberinput
|
||||
v-model="teamNumber"
|
||||
:disabled="settings.runNTServer"
|
||||
name="Team Number"
|
||||
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
|
||||
class="mb-4"
|
||||
/>
|
||||
<CVradio
|
||||
v-model="settings.connectionType"
|
||||
v-model="connectionType"
|
||||
:list="['DHCP','Static']"
|
||||
:disabled="!$store.state.settings.networkSettings.supported"
|
||||
/>
|
||||
<template v-if="!isDHCP">
|
||||
<CVinput
|
||||
v-model="settings.ip"
|
||||
v-model="staticIp"
|
||||
:input-cols="inputCols"
|
||||
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
|
||||
name="IP"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<CVinput
|
||||
v-model="settings.hostname"
|
||||
:input-cols="inputCols"
|
||||
:rules="[v => isHostname(v) || 'Invalid hostname']"
|
||||
name="Hostname"
|
||||
/>
|
||||
<CVinput
|
||||
v-model="hostname"
|
||||
:input-cols="inputCols"
|
||||
:rules="[v => isHostname(v) || 'Invalid hostname']"
|
||||
name="Hostname"
|
||||
/>
|
||||
</v-form>
|
||||
<v-btn
|
||||
color="accent"
|
||||
style="color: black; width: 100%;"
|
||||
:disabled="!valid && !runNTServer"
|
||||
@click="sendGeneralSettings()"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVnumberinput from '../../components/common/cv-number-input'
|
||||
import CVradio from '../../components/common/cv-radio'
|
||||
import CVinput from '../../components/common/cv-input'
|
||||
import CVnumberinput from '../../components/common/cv-number-input'
|
||||
import CVradio from '../../components/common/cv-radio'
|
||||
import CVinput from '../../components/common/cv-input'
|
||||
import CVSwitch from "@/components/common/cv-switch";
|
||||
|
||||
// https://stackoverflow.com/a/17871737
|
||||
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
|
||||
// https://stackoverflow.com/a/18494710
|
||||
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
|
||||
// https://stackoverflow.com/a/17871737
|
||||
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
|
||||
// https://stackoverflow.com/a/18494710
|
||||
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
|
||||
|
||||
export default {
|
||||
name: 'Networking',
|
||||
components: {
|
||||
CVnumberinput,
|
||||
CVradio,
|
||||
CVinput
|
||||
export default {
|
||||
name: 'Networking',
|
||||
components: {
|
||||
CVSwitch,
|
||||
CVnumberinput,
|
||||
CVradio,
|
||||
CVinput
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
file: undefined,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
},
|
||||
snack: false,
|
||||
isLoading: false,
|
||||
valid: true, // Are all settings valid
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputCols() {
|
||||
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
file: undefined,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
},
|
||||
snack: false,
|
||||
isLoading: false
|
||||
isDHCP() {
|
||||
return this.settings.connectionType === 0;
|
||||
},
|
||||
settings() {
|
||||
return this.$store.state.settings.networkSettings;
|
||||
},
|
||||
teamNumber: {
|
||||
get() {
|
||||
return this.settings.teamNumber
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('mutateNetworkSettings', {['teamNumber']: value || 0});
|
||||
}
|
||||
},
|
||||
runNTServer: {
|
||||
get() {
|
||||
return this.settings.runNTServer
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('mutateNetworkSettings', {['runNTServer']: value});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputCols() {
|
||||
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
|
||||
connectionType: {
|
||||
get() {
|
||||
return this.settings.connectionType
|
||||
},
|
||||
isDHCP() {
|
||||
return this.settings.connectionType === 0;
|
||||
},
|
||||
settings() {
|
||||
return this.$store.state.settings.networkSettings;
|
||||
set(value) {
|
||||
this.$store.commit('mutateNetworkSettings', {['connectionType']: value});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isIPv4(v) {
|
||||
return ipv4Regex.test(v);
|
||||
staticIp: {
|
||||
get() {
|
||||
return this.settings.staticIp
|
||||
},
|
||||
isHostname(v) {
|
||||
return hostnameRegex.test(v);
|
||||
set(value) {
|
||||
this.$store.commit('mutateNetworkSettings', {['staticIp']: value});
|
||||
}
|
||||
},
|
||||
hostname: {
|
||||
get() {
|
||||
return this.settings.hostname
|
||||
},
|
||||
// https://www.freesoft.org/CIE/Course/Subnet/6.htm
|
||||
// https://stackoverflow.com/a/13957228
|
||||
isSubnetMask(v) {
|
||||
// Has to be valid IPv4 so we'll start here
|
||||
if (!this.isIPv4(v)) return false;
|
||||
set(value) {
|
||||
this.$store.commit('mutateNetworkSettings', {['hostname']: value});
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isIPv4(v) {
|
||||
return ipv4Regex.test(v);
|
||||
},
|
||||
isHostname(v) {
|
||||
return hostnameRegex.test(v);
|
||||
},
|
||||
// https://www.freesoft.org/CIE/Course/Subnet/6.htm
|
||||
// https://stackoverflow.com/a/13957228
|
||||
isSubnetMask(v) {
|
||||
// Has to be valid IPv4 so we'll start here
|
||||
if (!this.isIPv4(v)) return false;
|
||||
|
||||
let octets = v.split(".").map(it => Number(it));
|
||||
let restAreOnes = false;
|
||||
for (let i = 3; i >= 0; i--) {
|
||||
let octets = v.split(".").map(it => Number(it));
|
||||
let restAreOnes = false;
|
||||
for (let i = 3; i >= 0; i--) {
|
||||
for (let j = 0; j < 8; j++) {
|
||||
let bitValue = (octets[i] >>> j & 1) == 1;
|
||||
if (restAreOnes && !bitValue)
|
||||
return false;
|
||||
restAreOnes = bitValue;
|
||||
let bitValue = (octets[i] >>> j & 1) == 1;
|
||||
if (restAreOnes && !bitValue)
|
||||
return false;
|
||||
restAreOnes = bitValue;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
sendGeneralSettings() {
|
||||
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
|
||||
function (response) {
|
||||
if (response.status === 200) {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Settings updated successfully"
|
||||
};
|
||||
this.snack = true;
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: (error.response || {data: "Couldn't save settings"}).data
|
||||
};
|
||||
this.snack = true;
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) $YEAR Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '5.2.0'
|
||||
id "com.diffplug.gradle.spotless" version "3.28.0"
|
||||
id 'org.hidetake.ssh' version '2.10.1'
|
||||
id "jacoco"
|
||||
}
|
||||
|
||||
@@ -18,6 +19,9 @@ sourceCompatibility = 11
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = "https://maven.photonvision.org/repository/internal/"
|
||||
}
|
||||
maven {
|
||||
url = 'https://frcmaven.wpi.edu:443/artifactory/development'
|
||||
}
|
||||
@@ -25,15 +29,12 @@ repositories {
|
||||
|
||||
ext {
|
||||
wpilibVersion = '2020.3.2-99-g9f4de91'
|
||||
joglVersion = '2.4.0-rc-20200307'
|
||||
openCVVersion = '3.4.7-2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "io.javalin:javalin:3.7.0"
|
||||
compile group: 'eu.xeli', name: 'jpigpio_2.12', version: '0.1.0'
|
||||
|
||||
|
||||
|
||||
|
||||
implementation "com.fasterxml.jackson.core:jackson-annotations:2.10.0"
|
||||
implementation "com.fasterxml.jackson.core:jackson-core:2.10.0"
|
||||
@@ -49,7 +50,23 @@ dependencies {
|
||||
implementation "org.apache.commons:commons-collections4:4.4"
|
||||
implementation "org.apache.commons:commons-exec:1.3"
|
||||
|
||||
// wpilib stuff
|
||||
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
|
||||
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
|
||||
implementation "org.jogamp.jogl:jogl-all:$joglVersion"
|
||||
|
||||
// implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion:natives-linux-amd64"
|
||||
// implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion:natives-linux-armv6hf"
|
||||
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion:natives-linux-aarch64"
|
||||
// implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion:natives-macosx-universal"
|
||||
// implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion:natives-windows-amd64"
|
||||
|
||||
// implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-linux-amd64"
|
||||
// implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-linux-armv6hf"
|
||||
implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-linux-aarch64"
|
||||
// implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-macosx-universal"
|
||||
// implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-windows-amd64"
|
||||
|
||||
// WPILib stuff
|
||||
implementation "edu.wpi.first.wpiutil:wpiutil-java:$wpilibVersion"
|
||||
implementation "edu.wpi.first.cameraserver:cameraserver-java:$wpilibVersion"
|
||||
|
||||
@@ -144,6 +161,18 @@ spotless {
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
if (project.hasProperty("profile")) {
|
||||
jvmArgs=[
|
||||
"-Dcom.sun.management.jmxremote=true",
|
||||
"-Dcom.sun.management.jmxremote.ssl=false",
|
||||
"-Dcom.sun.management.jmxremote.authenticate=false",
|
||||
"-Dcom.sun.management.jmxremote.port=5000",
|
||||
"-Djava.rmi.server.hostname=0.0.0.0",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test // Tests are required to run before generating the report
|
||||
|
||||
@@ -159,3 +188,84 @@ jacocoTestReport {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
remotes {
|
||||
pi {
|
||||
host = 'photonvision.local'
|
||||
user = 'pi'
|
||||
password = 'raspberry'
|
||||
knownHosts = allowAnyHosts
|
||||
}
|
||||
gloworm {
|
||||
host = 'gloworm.local'
|
||||
user = 'pi'
|
||||
password = 'raspberry'
|
||||
knownHosts = allowAnyHosts
|
||||
}
|
||||
}
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
||||
task findDeployTarget {
|
||||
doLast {
|
||||
if(project.hasProperty('tgtIP')){
|
||||
//If user specificed IP, default to using the PI profile
|
||||
// but adjust hostname to match the provided IP address
|
||||
findDeployTarget.ext.rmt = remotes.pi
|
||||
findDeployTarget.ext.rmt.host=tgtIP
|
||||
} else {
|
||||
findDeployTarget.ext.rmt = null
|
||||
for(testRmt in remotes){
|
||||
println "Checking for " + testRmt.host
|
||||
boolean canContact = false;
|
||||
try {
|
||||
InetAddress testAddr = InetAddress.getByName(testRmt.host)
|
||||
canContact = testAddr.isReachable(5000)
|
||||
} catch(UnknownHostException e) {
|
||||
canContact = false;
|
||||
}
|
||||
if(canContact){
|
||||
println "Found!"
|
||||
findDeployTarget.ext.rmt = testRmt
|
||||
break
|
||||
} else {
|
||||
println "Not Found."
|
||||
}
|
||||
}
|
||||
if(findDeployTarget.ext.rmt == null ){
|
||||
throw new GradleException("Could not find a supported target for deployment!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
task deploy {
|
||||
dependsOn assemble
|
||||
dependsOn findDeployTarget
|
||||
doLast {
|
||||
println 'Starting deployment to ' + findDeployTarget.rmt.host
|
||||
ssh.run{
|
||||
session(findDeployTarget.rmt) {
|
||||
//Stop photonvision before manipulating its files
|
||||
execute 'sudo systemctl stop photonvision.service'
|
||||
// gerth2 - I was having issues with the .jar being in use still - waiting a tiny bit here seems to get rid of it on a pi4
|
||||
execute 'sleep 3'
|
||||
// Copy into a folder owned by PI. Mostly because, as far as I can tell, the put command doesn't support sudo.
|
||||
put from: "${projectDir}/build/libs/photonvision-${project.version}.jar", into: "/tmp/photonvision.jar"
|
||||
//belt-and-suspenders. Make sure the old jar is gone first.
|
||||
execute 'sudo rm -f /opt/photonvision/photonvision.jar'
|
||||
//Copy in the new .jar and make sure it's executable
|
||||
execute 'sudo mv /tmp/photonvision.jar /opt/photonvision/photonvision.jar'
|
||||
execute 'sudo chmod +x /opt/photonvision/photonvision.jar'
|
||||
//Fire up photonvision again
|
||||
execute 'sudo systemctl start photonvision.service'
|
||||
//Cleanup
|
||||
execute 'sudo rm -f /tmp/photonvision.jar'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,20 +20,24 @@ package org.photonvision;
|
||||
import edu.wpi.cscore.CameraServerCvJNI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.apache.commons.cli.*;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.LogLevel;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.server.Server;
|
||||
import org.photonvision.vision.camera.FileVisionSource;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ContourGroupingMode;
|
||||
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.PipelineProfiler;
|
||||
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
@@ -84,7 +88,7 @@ public class Main {
|
||||
}
|
||||
|
||||
private static void addTestModeSources() {
|
||||
var collectedSources = new HashMap<VisionSource, List<CVPipelineSettings>>();
|
||||
var collectedSources = new HashMap<VisionSource, CameraConfiguration>();
|
||||
|
||||
var camConf2019 =
|
||||
new CameraConfiguration("WPI2019", TestUtils.getTestMode2019ImagePath().toString());
|
||||
@@ -93,7 +97,9 @@ public class Main {
|
||||
|
||||
var pipeline2019 = new ReflectivePipelineSettings();
|
||||
pipeline2019.pipelineNickname = "CargoShip";
|
||||
pipeline2019.targetModel = TargetModel.get2019Target();
|
||||
pipeline2019.targetModel = TargetModel.k2019DualTarget;
|
||||
pipeline2019.outputShowMultipleTargets = true;
|
||||
pipeline2019.contourGroupingMode = ContourGroupingMode.Dual;
|
||||
|
||||
var psList2019 = new ArrayList<CVPipelineSettings>();
|
||||
psList2019.add(pipeline2019);
|
||||
@@ -107,7 +113,7 @@ public class Main {
|
||||
|
||||
var pipeline2020 = new ReflectivePipelineSettings();
|
||||
pipeline2020.pipelineNickname = "OuterPort";
|
||||
pipeline2020.targetModel = TargetModel.get2020Target();
|
||||
pipeline2020.targetModel = TargetModel.k2020HighGoalOuter;
|
||||
camConf2020.calibrations.add(TestUtils.get2020LifeCamCoeffs(true));
|
||||
|
||||
var psList2020 = new ArrayList<CVPipelineSettings>();
|
||||
@@ -115,8 +121,12 @@ public class Main {
|
||||
|
||||
var fvs2020 = new FileVisionSource(camConf2020);
|
||||
|
||||
collectedSources.put(fvs2019, psList2019);
|
||||
collectedSources.put(fvs2020, psList2020);
|
||||
var cfg2019 = new CameraConfiguration("2019", "2019");
|
||||
cfg2019.pipelineSettings = psList2019;
|
||||
var cfg2020 = new CameraConfiguration("2019", "2019");
|
||||
cfg2020.pipelineSettings = psList2020;
|
||||
collectedSources.put(fvs2019, cfg2019);
|
||||
collectedSources.put(fvs2020, cfg2020);
|
||||
|
||||
// logger.info("Adding " + allSources.size() + " configs to VMM.");
|
||||
VisionModuleManager.getInstance().addSources(collectedSources).forEach(VisionModule::start);
|
||||
@@ -130,14 +140,16 @@ public class Main {
|
||||
logger.error("Failed to parse command-line options!", e);
|
||||
}
|
||||
|
||||
logger.info("Running in " + (isRelease ? "release" : "development") + " mode!");
|
||||
var logLevel = (isRelease || printDebugLogs) ? LogLevel.INFO : LogLevel.DEBUG;
|
||||
CVMat.enablePrint(false);
|
||||
PipelineProfiler.enablePrint(false);
|
||||
|
||||
var logLevel = printDebugLogs ? LogLevel.TRACE : LogLevel.DEBUG;
|
||||
Logger.setLevel(LogGroup.Camera, logLevel);
|
||||
Logger.setLevel(LogGroup.WebServer, logLevel);
|
||||
Logger.setLevel(LogGroup.VisionModule, logLevel);
|
||||
Logger.setLevel(LogGroup.Data, logLevel);
|
||||
Logger.setLevel(LogGroup.General, logLevel);
|
||||
logger.info("Logging initialized in " + (isRelease ? "Release" : "Debug") + " mode.");
|
||||
logger.info("Logging initialized in debug mode.");
|
||||
|
||||
logger.info(
|
||||
"Starting PhotonVision version "
|
||||
@@ -147,6 +159,7 @@ public class Main {
|
||||
|
||||
try {
|
||||
CameraServerCvJNI.forceLoad();
|
||||
PicamJNI.forceLoad();
|
||||
TestUtils.loadLibraries();
|
||||
logger.info("Native libraries loaded.");
|
||||
} catch (Exception e) {
|
||||
@@ -156,17 +169,14 @@ public class Main {
|
||||
ConfigManager.getInstance().load(); // init config manager
|
||||
ConfigManager.getInstance().requestSave();
|
||||
|
||||
NetworkManager.getInstance().initialize(false);
|
||||
// Force load the hardware manager
|
||||
HardwareManager.getInstance();
|
||||
|
||||
NetworkManager.getInstance().reinitialize();
|
||||
|
||||
NetworkTablesManager.getInstance()
|
||||
.setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
|
||||
|
||||
// HashMap<VisionSource, List<CVPipelineSettings>> allSources = gatherSources();
|
||||
|
||||
// logger.info("Adding " + allSources.size() + " configs to VMM.");
|
||||
// VisionModuleManager.getInstance().addSources(allSources);
|
||||
// ConfigManager.getInstance().addCameraConfigurations(allSources);
|
||||
|
||||
if (!isTestMode) {
|
||||
VisionSourceManager.getInstance()
|
||||
.registerLoadedConfigs(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -49,10 +49,11 @@ public class CameraConfiguration {
|
||||
public CameraType cameraType = CameraType.UsbCamera;
|
||||
public double FOV = 70;
|
||||
public final List<CameraCalibrationCoefficients> calibrations;
|
||||
public List<Integer> cameraLeds = new ArrayList<>();
|
||||
public int currentPipelineIndex = 0;
|
||||
public Rotation2d camPitch = new Rotation2d();
|
||||
|
||||
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
|
||||
|
||||
@JsonIgnore // this ignores the pipes as we serialize them to their own subfolder
|
||||
public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
|
||||
|
||||
@@ -89,7 +90,6 @@ public class CameraConfiguration {
|
||||
@JsonProperty("path") String path,
|
||||
@JsonProperty("cameraType") CameraType cameraType,
|
||||
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
|
||||
@JsonProperty("cameraLEDs") List<Integer> cameraLeds,
|
||||
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
|
||||
@JsonProperty("camPitch") Rotation2d camPitch) {
|
||||
this.baseName = baseName;
|
||||
@@ -99,7 +99,6 @@ public class CameraConfiguration {
|
||||
this.path = path;
|
||||
this.cameraType = cameraType;
|
||||
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
|
||||
this.cameraLeds = cameraLeds;
|
||||
this.currentPipelineIndex = currentPipelineIndex;
|
||||
this.camPitch = camPitch;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,8 +22,12 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
@@ -40,8 +44,13 @@ public class ConfigManager {
|
||||
private static final Logger logger = new Logger(ConfigManager.class, LogGroup.General);
|
||||
private static ConfigManager INSTANCE;
|
||||
|
||||
public static final String HW_CFG_FNAME = "hardwareConfig.json";
|
||||
public static final String HW_SET_FNAME = "hardwareSettings.json";
|
||||
public static final String NET_SET_FNAME = "networkSettings.json";
|
||||
|
||||
private PhotonConfiguration config;
|
||||
private final File hardwareConfigFile;
|
||||
private final File hardwareSettingsFile;
|
||||
private final File networkConfigFile;
|
||||
private final File camerasFolder;
|
||||
|
||||
@@ -81,9 +90,11 @@ public class ConfigManager {
|
||||
ConfigManager(Path configDirectoryFile) {
|
||||
this.configDirectoryFile = new File(configDirectoryFile.toUri());
|
||||
this.hardwareConfigFile =
|
||||
new File(Path.of(configDirectoryFile.toString(), "hardwareConfig.json").toUri());
|
||||
new File(Path.of(configDirectoryFile.toString(), HW_CFG_FNAME).toUri());
|
||||
this.hardwareSettingsFile =
|
||||
new File(Path.of(configDirectoryFile.toString(), HW_SET_FNAME).toUri());
|
||||
this.networkConfigFile =
|
||||
new File(Path.of(configDirectoryFile.toString(), "networkSettings.json").toUri());
|
||||
new File(Path.of(configDirectoryFile.toString(), NET_SET_FNAME).toUri());
|
||||
this.camerasFolder = new File(Path.of(configDirectoryFile.toString(), "cameras").toUri());
|
||||
|
||||
TimedTaskManager.getInstance().addTask("ConfigManager", this::checkSaveAndWrite, 1000);
|
||||
@@ -110,6 +121,7 @@ public class ConfigManager {
|
||||
}
|
||||
|
||||
HardwareConfig hardwareConfig;
|
||||
HardwareSettings hardwareSettings;
|
||||
NetworkConfig networkConfig;
|
||||
|
||||
if (hardwareConfigFile.exists()) {
|
||||
@@ -129,6 +141,23 @@ public class ConfigManager {
|
||||
hardwareConfig = new HardwareConfig();
|
||||
}
|
||||
|
||||
if (hardwareSettingsFile.exists()) {
|
||||
try {
|
||||
hardwareSettings =
|
||||
JacksonUtils.deserialize(hardwareSettingsFile.toPath(), HardwareSettings.class);
|
||||
if (hardwareSettings == null) {
|
||||
logger.error("Could not deserialize hardware settings! Loading defaults");
|
||||
hardwareSettings = new HardwareSettings();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize hardware settings! Loading defaults");
|
||||
hardwareSettings = new HardwareSettings();
|
||||
}
|
||||
} else {
|
||||
logger.info("Hardware settings does not exist! Loading defaults");
|
||||
hardwareSettings = new HardwareSettings();
|
||||
}
|
||||
|
||||
if (networkConfigFile.exists()) {
|
||||
try {
|
||||
networkConfig = JacksonUtils.deserialize(networkConfigFile.toPath(), NetworkConfig.class);
|
||||
@@ -155,23 +184,25 @@ public class ConfigManager {
|
||||
|
||||
HashMap<String, CameraConfiguration> cameraConfigurations = loadCameraConfigs();
|
||||
|
||||
this.config = new PhotonConfiguration(hardwareConfig, networkConfig, cameraConfigurations);
|
||||
this.config =
|
||||
new PhotonConfiguration(
|
||||
hardwareConfig, hardwareSettings, networkConfig, cameraConfigurations);
|
||||
}
|
||||
|
||||
public void saveToDisk() {
|
||||
// Delete old configs
|
||||
FileUtils.deleteDirectory(camerasFolder.toPath());
|
||||
|
||||
try {
|
||||
JacksonUtils.serialize(hardwareConfigFile.toPath(), config.getHardwareConfig());
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not save hardware config!", e);
|
||||
}
|
||||
try {
|
||||
JacksonUtils.serialize(networkConfigFile.toPath(), config.getNetworkConfig());
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not save network config!", e);
|
||||
}
|
||||
try {
|
||||
JacksonUtils.serialize(hardwareSettingsFile.toPath(), config.getHardwareSettings());
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not save hardware config!", e);
|
||||
}
|
||||
|
||||
// save all of our cameras
|
||||
var cameraConfigMap = config.getCameraConfigurations();
|
||||
@@ -301,12 +332,8 @@ 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);
|
||||
public void addCameraConfigurations(HashMap<VisionSource, CameraConfiguration> sources) {
|
||||
getConfig().addCameraConfigs(sources.values());
|
||||
requestSave();
|
||||
}
|
||||
|
||||
@@ -330,15 +357,69 @@ public class ConfigManager {
|
||||
requestSave();
|
||||
}
|
||||
|
||||
public Path getLogsDir() {
|
||||
return Path.of(configDirectoryFile.toString(), "logs");
|
||||
}
|
||||
|
||||
public Path getCalibDir() {
|
||||
return Path.of(configDirectoryFile.toString(), "calibImgs");
|
||||
}
|
||||
|
||||
public static final String LOG_PREFIX = "photonvision-";
|
||||
public static final String LOG_EXT = ".log";
|
||||
public static final String LOG_DATE_TIME_FORMAT = "yyyy-M-d_hh-mm-ss";
|
||||
|
||||
public String taToLogFname(TemporalAccessor date) {
|
||||
var dateString = DateTimeFormatter.ofPattern(LOG_DATE_TIME_FORMAT).format(date);
|
||||
return LOG_PREFIX + dateString + LOG_EXT;
|
||||
}
|
||||
|
||||
public Date logFnameToDate(String fname) throws ParseException {
|
||||
// Strip away known unneded portions of the log file name
|
||||
fname = fname.replace(LOG_PREFIX, "").replace(LOG_EXT, "");
|
||||
DateFormat format = new SimpleDateFormat(LOG_DATE_TIME_FORMAT);
|
||||
return format.parse(fname);
|
||||
}
|
||||
|
||||
public Path getLogPath() {
|
||||
var dateString = DateTimeFormatter.ofPattern("yyyy-M-d_hh-mm-ss").format(LocalDateTime.now());
|
||||
var logFile =
|
||||
Path.of(configDirectoryFile.toString(), "logs", "photonvision-" + dateString + ".log")
|
||||
.toFile();
|
||||
var logFile = Path.of(this.getLogsDir().toString(), taToLogFname(LocalDateTime.now())).toFile();
|
||||
if (!logFile.getParentFile().exists()) logFile.getParentFile().mkdirs();
|
||||
return logFile.toPath();
|
||||
}
|
||||
|
||||
public Path getImageSavePath() {
|
||||
var imgFilePath = Path.of(configDirectoryFile.toString(), "imgSaves").toFile();
|
||||
if (!imgFilePath.exists()) imgFilePath.mkdirs();
|
||||
return imgFilePath.toPath();
|
||||
}
|
||||
|
||||
public Path getHardwareConfigFile() {
|
||||
return this.hardwareConfigFile.toPath();
|
||||
}
|
||||
|
||||
public Path getHardwareSettingsFile() {
|
||||
return this.hardwareSettingsFile.toPath();
|
||||
}
|
||||
|
||||
public Path getNetworkConfigFile() {
|
||||
return this.networkConfigFile.toPath();
|
||||
}
|
||||
|
||||
public void saveUploadedHardwareConfig(Path uploadPath) {
|
||||
FileUtils.deleteFile(this.getHardwareConfigFile());
|
||||
FileUtils.copyFile(uploadPath, this.getHardwareConfigFile());
|
||||
}
|
||||
|
||||
public void saveUploadedHardwareSettings(Path uploadPath) {
|
||||
FileUtils.deleteFile(this.getHardwareSettingsFile());
|
||||
FileUtils.copyFile(uploadPath, this.getHardwareSettingsFile());
|
||||
}
|
||||
|
||||
public void saveUploadedNetworkConfig(Path uploadPath) {
|
||||
FileUtils.deleteFile(this.getNetworkConfigFile());
|
||||
FileUtils.copyFile(uploadPath, this.getNetworkConfigFile());
|
||||
}
|
||||
|
||||
public void requestSave() {
|
||||
logger.trace("Requesting save...");
|
||||
saveRequestTimestamp = System.currentTimeMillis();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,8 +17,12 @@
|
||||
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class HardwareConfig {
|
||||
|
||||
public final String deviceName;
|
||||
@@ -29,9 +33,7 @@ public class HardwareConfig {
|
||||
public final ArrayList<Integer> ledPins;
|
||||
public final String ledSetCommand;
|
||||
public final boolean ledsCanDim;
|
||||
public final ArrayList<Integer> ledPWMRange;
|
||||
public final String ledPWMSetRange;
|
||||
public final int ledPWMFrequency;
|
||||
public final ArrayList<Integer> ledBrightnessRange;
|
||||
public final String ledDimCommand;
|
||||
public final String ledBlinkCommand;
|
||||
public final ArrayList<Integer> statusRGBPins;
|
||||
@@ -41,12 +43,14 @@ public class HardwareConfig {
|
||||
public final String cpuMemoryCommand;
|
||||
public final String cpuUtilCommand;
|
||||
public final String gpuMemoryCommand;
|
||||
public final String gpuTempCommand;
|
||||
public final String ramUtilCommand;
|
||||
public final String gpuMemUsageCommand;
|
||||
public final String diskUsageCommand;
|
||||
|
||||
// Device stuff
|
||||
public final String restartHardwareCommand;
|
||||
public final double vendorFOV; // -1 for unmanaged
|
||||
public final List<Integer> blacklistedResIndices; // this happens before the defaults are applied
|
||||
|
||||
public HardwareConfig() {
|
||||
deviceName = "";
|
||||
@@ -55,22 +59,22 @@ public class HardwareConfig {
|
||||
ledPins = new ArrayList<>();
|
||||
ledSetCommand = "";
|
||||
ledsCanDim = false;
|
||||
ledPWMRange = new ArrayList<>();
|
||||
ledBrightnessRange = new ArrayList<>();
|
||||
statusRGBPins = new ArrayList<>();
|
||||
ledPWMFrequency = 0;
|
||||
ledPWMSetRange = "";
|
||||
ledDimCommand = "";
|
||||
|
||||
cpuTempCommand = "";
|
||||
cpuMemoryCommand = "";
|
||||
cpuUtilCommand = "";
|
||||
gpuMemoryCommand = "";
|
||||
gpuTempCommand = "";
|
||||
ramUtilCommand = "";
|
||||
ledBlinkCommand = "";
|
||||
gpuMemUsageCommand = "";
|
||||
diskUsageCommand = "";
|
||||
|
||||
restartHardwareCommand = "";
|
||||
vendorFOV = -1;
|
||||
blacklistedResIndices = Collections.emptyList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -81,29 +85,27 @@ public class HardwareConfig {
|
||||
ArrayList<Integer> ledPins,
|
||||
String ledSetCommand,
|
||||
boolean ledsCanDim,
|
||||
ArrayList<Integer> statusRGBPins,
|
||||
String ledPWMSetRange,
|
||||
int ledPWMFrequency,
|
||||
ArrayList<Integer> ledBrightnessRange,
|
||||
String ledDimCommand,
|
||||
String ledBlinkCommand,
|
||||
ArrayList<Integer> ledPWMRange,
|
||||
ArrayList<Integer> statusRGBPins,
|
||||
String cpuTempCommand,
|
||||
String cpuMemoryCommand,
|
||||
String cpuUtilCommand,
|
||||
String gpuMemoryCommand,
|
||||
String gpuTempCommand,
|
||||
String ramUtilCommand,
|
||||
String gpuMemUsageCommand,
|
||||
String diskUsageCommand,
|
||||
String restartHardwareCommand,
|
||||
double vendorFOV) {
|
||||
double vendorFOV,
|
||||
List<Integer> blacklistedResIndices) {
|
||||
this.deviceName = deviceName;
|
||||
this.deviceLogoPath = deviceLogoPath;
|
||||
this.supportURL = supportURL;
|
||||
this.ledPins = ledPins;
|
||||
this.ledSetCommand = ledSetCommand;
|
||||
this.ledsCanDim = ledsCanDim;
|
||||
this.ledPWMRange = ledPWMRange;
|
||||
this.ledPWMSetRange = ledPWMSetRange;
|
||||
this.ledPWMFrequency = ledPWMFrequency;
|
||||
this.ledBrightnessRange = ledBrightnessRange;
|
||||
this.ledDimCommand = ledDimCommand;
|
||||
this.ledBlinkCommand = ledBlinkCommand;
|
||||
this.statusRGBPins = statusRGBPins;
|
||||
@@ -111,10 +113,12 @@ public class HardwareConfig {
|
||||
this.cpuMemoryCommand = cpuMemoryCommand;
|
||||
this.cpuUtilCommand = cpuUtilCommand;
|
||||
this.gpuMemoryCommand = gpuMemoryCommand;
|
||||
this.gpuTempCommand = gpuTempCommand;
|
||||
this.ramUtilCommand = ramUtilCommand;
|
||||
this.gpuMemUsageCommand = gpuMemUsageCommand;
|
||||
this.diskUsageCommand = diskUsageCommand;
|
||||
this.restartHardwareCommand = restartHardwareCommand;
|
||||
this.vendorFOV = vendorFOV;
|
||||
this.blacklistedResIndices = blacklistedResIndices;
|
||||
}
|
||||
|
||||
public final boolean hasPresetFOV() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,15 +17,6 @@
|
||||
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
public enum StreamDivisor {
|
||||
NONE(1),
|
||||
HALF(2),
|
||||
QUARTER(4),
|
||||
SIXTH(6);
|
||||
|
||||
public final Integer value;
|
||||
|
||||
StreamDivisor(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
public class HardwareSettings {
|
||||
public int ledBrightnessPercentage = 100;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,35 +17,42 @@
|
||||
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.networking.NetworkMode;
|
||||
|
||||
public class NetworkConfig {
|
||||
public int teamNumber = -1;
|
||||
public int teamNumber = 0;
|
||||
public NetworkMode connectionType = NetworkMode.DHCP;
|
||||
public String staticIp = "";
|
||||
public String netmask = "";
|
||||
public String hostname = "photonvision";
|
||||
public boolean runNTServer = false;
|
||||
|
||||
// TODO implement networking
|
||||
public boolean shouldManage;
|
||||
private boolean shouldManage;
|
||||
|
||||
public NetworkConfig() {}
|
||||
public NetworkConfig() {
|
||||
setShouldManage(false);
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public NetworkConfig(
|
||||
int teamNumber,
|
||||
NetworkMode connectionType,
|
||||
String staticIp,
|
||||
String netmask,
|
||||
String hostname,
|
||||
boolean shouldManage) {
|
||||
@JsonProperty("teamNumber") int teamNumber,
|
||||
@JsonProperty("connectionType") NetworkMode connectionType,
|
||||
@JsonProperty("staticIp") String staticIp,
|
||||
@JsonProperty("hostname") String hostname,
|
||||
@JsonProperty("runNTServer") boolean runNTServer,
|
||||
@JsonProperty("shouldManage") boolean shouldManage) {
|
||||
this.teamNumber = teamNumber;
|
||||
this.connectionType = connectionType;
|
||||
this.staticIp = staticIp;
|
||||
this.netmask = netmask;
|
||||
this.hostname = hostname;
|
||||
this.shouldManage = shouldManage;
|
||||
this.runNTServer = runNTServer;
|
||||
setShouldManage(shouldManage);
|
||||
}
|
||||
|
||||
public static NetworkConfig fromHashMap(Map<String, Object> map) {
|
||||
@@ -53,22 +60,32 @@ public class NetworkConfig {
|
||||
// staticIp (str), netmask (str), hostname (str)
|
||||
var ret = new NetworkConfig();
|
||||
ret.teamNumber = Integer.parseInt(map.get("teamNumber").toString());
|
||||
ret.shouldManage = (Boolean) map.get("supported");
|
||||
ret.connectionType = NetworkMode.values()[(Integer) map.get("connectionType")];
|
||||
ret.staticIp = (String) map.get("staticIp");
|
||||
ret.netmask = (String) map.get("netmask");
|
||||
ret.hostname = (String) map.get("hostname");
|
||||
ret.runNTServer = (Boolean) map.get("runNTServer");
|
||||
ret.setShouldManage((Boolean) map.get("supported"));
|
||||
return ret;
|
||||
}
|
||||
|
||||
public HashMap<String, Object> toHashMap() {
|
||||
HashMap<String, Object> tmp = new HashMap<>();
|
||||
tmp.put("teamNumber", teamNumber);
|
||||
tmp.put("supported", shouldManage);
|
||||
tmp.put("supported", shouldManage());
|
||||
tmp.put("connectionType", connectionType.ordinal());
|
||||
tmp.put("staticIp", staticIp);
|
||||
tmp.put("netmask", netmask);
|
||||
tmp.put("hostname", hostname);
|
||||
tmp.put("runNTServer", runNTServer);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
@JsonGetter("shouldManage")
|
||||
public boolean shouldManage() {
|
||||
return this.shouldManage || Platform.isRaspberryPi();
|
||||
}
|
||||
|
||||
@JsonSetter("shouldManage")
|
||||
public void setShouldManage(boolean shouldManage) {
|
||||
this.shouldManage = shouldManage || Platform.isRaspberryPi();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,17 +17,44 @@
|
||||
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.raspi.PicamJNI;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
|
||||
// TODO rename this class
|
||||
public class PhotonConfiguration {
|
||||
|
||||
private HardwareConfig hardwareConfig;
|
||||
private HardwareSettings hardwareSettings;
|
||||
private NetworkConfig networkConfig;
|
||||
private HashMap<String, CameraConfiguration> cameraConfigurations;
|
||||
|
||||
public PhotonConfiguration(
|
||||
HardwareConfig hardwareConfig,
|
||||
HardwareSettings hardwareSettings,
|
||||
NetworkConfig networkConfig) {
|
||||
this(hardwareConfig, hardwareSettings, networkConfig, new HashMap<>());
|
||||
}
|
||||
|
||||
public PhotonConfiguration(
|
||||
HardwareConfig hardwareConfig,
|
||||
HardwareSettings hardwareSettings,
|
||||
NetworkConfig networkConfig,
|
||||
HashMap<String, CameraConfiguration> cameraConfigurations) {
|
||||
this.hardwareConfig = hardwareConfig;
|
||||
this.hardwareSettings = hardwareSettings;
|
||||
this.networkConfig = networkConfig;
|
||||
this.cameraConfigurations = cameraConfigurations;
|
||||
}
|
||||
|
||||
public HardwareConfig getHardwareConfig() {
|
||||
return hardwareConfig;
|
||||
}
|
||||
@@ -36,6 +63,10 @@ public class PhotonConfiguration {
|
||||
return networkConfig;
|
||||
}
|
||||
|
||||
public HardwareSettings getHardwareSettings() {
|
||||
return hardwareSettings;
|
||||
}
|
||||
|
||||
public void setNetworkConfig(NetworkConfig networkConfig) {
|
||||
this.networkConfig = networkConfig;
|
||||
}
|
||||
@@ -44,7 +75,7 @@ public class PhotonConfiguration {
|
||||
return cameraConfigurations;
|
||||
}
|
||||
|
||||
public void addCameraConfigs(List<CameraConfiguration> config) {
|
||||
public void addCameraConfigs(Collection<CameraConfiguration> config) {
|
||||
for (var c : config) {
|
||||
addCameraConfig(c);
|
||||
}
|
||||
@@ -58,25 +89,6 @@ public class PhotonConfiguration {
|
||||
cameraConfigurations.put(name, config);
|
||||
}
|
||||
|
||||
private HardwareConfig hardwareConfig;
|
||||
|
||||
private NetworkConfig networkConfig;
|
||||
|
||||
private HashMap<String, CameraConfiguration> cameraConfigurations;
|
||||
|
||||
public PhotonConfiguration(HardwareConfig hardwareConfig, NetworkConfig networkConfig) {
|
||||
this(hardwareConfig, networkConfig, new HashMap<>());
|
||||
}
|
||||
|
||||
public PhotonConfiguration(
|
||||
HardwareConfig hardwareConfig,
|
||||
NetworkConfig networkConfig,
|
||||
HashMap<String, CameraConfiguration> cameraConfigurations) {
|
||||
this.hardwareConfig = hardwareConfig;
|
||||
this.networkConfig = networkConfig;
|
||||
this.cameraConfigurations = cameraConfigurations;
|
||||
}
|
||||
|
||||
public Map<String, Object> toHashMap() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
var settingsSubmap = new HashMap<String, Object>();
|
||||
@@ -89,20 +101,31 @@ public class PhotonConfiguration {
|
||||
.map(SerializationUtils::objectToHashMap)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
settingsSubmap.put("lighting", SerializationUtils.objectToHashMap(hardwareConfig));
|
||||
var lightingConfig = new UILightingConfig();
|
||||
lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage;
|
||||
lightingConfig.supported = (hardwareConfig.ledPins.size() != 0);
|
||||
settingsSubmap.put("lighting", SerializationUtils.objectToHashMap(lightingConfig));
|
||||
|
||||
var generalSubmap = new HashMap<String, Object>();
|
||||
generalSubmap.put("version", PhotonVersion.versionString);
|
||||
generalSubmap.put("gpuAcceleration", false); // TODO gpu accel and accel type
|
||||
generalSubmap.put("gpuAccelerationType", "Unknown");
|
||||
generalSubmap.put("hardwareModel", "Unknown"); // TODO hardware model and platform
|
||||
generalSubmap.put("hardwarePlatform", "Unknown");
|
||||
generalSubmap.put(
|
||||
"gpuAcceleration",
|
||||
PicamJNI.isSupported()
|
||||
? "Zerocopy MMAL on " + PicamJNI.getSensorModel().getFriendlyName()
|
||||
: ""); // TODO add support for other types of GPU accel
|
||||
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
|
||||
generalSubmap.put("hardwarePlatform", Platform.getCurrentPlatform().toString());
|
||||
settingsSubmap.put("general", generalSubmap);
|
||||
|
||||
map.put("settings", settingsSubmap);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static class UILightingConfig {
|
||||
public int brightness = 0;
|
||||
public boolean supported = true;
|
||||
}
|
||||
|
||||
public static class UICameraConfiguration {
|
||||
@SuppressWarnings("unused")
|
||||
public double fov, tiltDegrees;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -49,6 +49,10 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
private NetworkTableEntry targetPoseEntry;
|
||||
private NetworkTableEntry targetSkewEntry;
|
||||
|
||||
// The raw position of the best target, in pixels.
|
||||
private NetworkTableEntry bestTargetPosX;
|
||||
private NetworkTableEntry bestTargetPosY;
|
||||
|
||||
private final Supplier<Integer> pipelineIndexSupplier;
|
||||
private final BooleanSupplier driverModeSupplier;
|
||||
|
||||
@@ -71,6 +75,12 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
var newIndex = (int) entryNotification.value.getDouble();
|
||||
var originalIndex = pipelineIndexSupplier.get();
|
||||
|
||||
// ignore indexes below 0
|
||||
if (newIndex < 0) {
|
||||
pipelineIndexEntry.forceSetNumber(originalIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newIndex == originalIndex) {
|
||||
// TODO: Log
|
||||
return;
|
||||
@@ -98,6 +108,7 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
// TODO: Log
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void removeEntries() {
|
||||
if (rawBytesEntry != null) rawBytesEntry.delete();
|
||||
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||
@@ -111,6 +122,8 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
if (targetYawEntry != null) targetYawEntry.delete();
|
||||
if (targetPoseEntry != null) targetPoseEntry.delete();
|
||||
if (targetSkewEntry != null) targetSkewEntry.delete();
|
||||
if (bestTargetPosX != null) bestTargetPosX.delete();
|
||||
if (bestTargetPosY != null) bestTargetPosY.delete();
|
||||
}
|
||||
|
||||
private void updateEntries() {
|
||||
@@ -137,6 +150,9 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
targetYawEntry = subTable.getEntry("targetYaw");
|
||||
targetPoseEntry = subTable.getEntry("targetPose");
|
||||
targetSkewEntry = subTable.getEntry("targetSkew");
|
||||
|
||||
bestTargetPosX = subTable.getEntry("targetPixelsX");
|
||||
bestTargetPosY = subTable.getEntry("targetPixelsY");
|
||||
}
|
||||
|
||||
public void updateCameraNickname(String newCameraNickname) {
|
||||
@@ -170,12 +186,18 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
var poseY = bestTarget.getCameraToTarget().getTranslation().getY();
|
||||
var poseRot = bestTarget.getCameraToTarget().getRotation().getDegrees();
|
||||
targetPoseEntry.forceSetDoubleArray(new double[] {poseX, poseY, poseRot});
|
||||
|
||||
var targetOffsetPoint = bestTarget.getTargetOffsetPoint();
|
||||
bestTargetPosX.forceSetDouble(targetOffsetPoint.x);
|
||||
bestTargetPosY.forceSetDouble(targetOffsetPoint.y);
|
||||
} else {
|
||||
targetPitchEntry.forceSetDouble(0);
|
||||
targetYawEntry.forceSetDouble(0);
|
||||
targetAreaEntry.forceSetDouble(0);
|
||||
targetSkewEntry.forceSetDouble(0);
|
||||
targetPoseEntry.forceSetDoubleArray(new double[] {0, 0, 0});
|
||||
bestTargetPosX.forceSetDouble(0);
|
||||
bestTargetPosY.forceSetDouble(0);
|
||||
}
|
||||
rootTable.getInstance().flush();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -46,8 +46,6 @@ public class NetworkTablesManager {
|
||||
|
||||
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
|
||||
|
||||
public boolean isServer = false;
|
||||
|
||||
private static class NTLogger implements Consumer<LogMessage> {
|
||||
|
||||
private boolean hasReportedConnectionFailure = false;
|
||||
@@ -69,15 +67,14 @@ public class NetworkTablesManager {
|
||||
}
|
||||
|
||||
public void setConfig(NetworkConfig config) {
|
||||
if (config.teamNumber > 0) {
|
||||
setClientMode(config.teamNumber);
|
||||
} else {
|
||||
if (config.runNTServer) {
|
||||
setServerMode();
|
||||
} else {
|
||||
setClientMode(config.teamNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private void setClientMode(int teamNumber) {
|
||||
isServer = false;
|
||||
logger.info("Starting NT Client");
|
||||
ntInstance.stopServer();
|
||||
|
||||
@@ -91,7 +88,6 @@ public class NetworkTablesManager {
|
||||
}
|
||||
|
||||
private void setServerMode() {
|
||||
isServer = true;
|
||||
logger.info("Starting NT Server");
|
||||
ntInstance.stopClient();
|
||||
ntInstance.startServer();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -47,6 +47,7 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
|
||||
var uiMap = new HashMap<Integer, HashMap<String, Object>>();
|
||||
var dataMap = new HashMap<String, Object>();
|
||||
|
||||
dataMap.put("fps", result.fps);
|
||||
dataMap.put("latency", result.getLatencyMillis());
|
||||
|
||||
var targets = result.targets;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,15 +17,12 @@
|
||||
|
||||
package org.photonvision.common.hardware.GPIO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
|
||||
public class CustomGPIO extends GPIOBase {
|
||||
|
||||
private boolean currentState;
|
||||
private List<Integer> pwmRange = new ArrayList<>();
|
||||
private final int port;
|
||||
|
||||
public CustomGPIO(int port) {
|
||||
@@ -75,22 +72,6 @@ public class CustomGPIO extends GPIOBase {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPwmRangeImpl(List<Integer> range) {
|
||||
execute(
|
||||
commands
|
||||
.get("setRange")
|
||||
.replace("{lower_range}", String.valueOf(range.get(0)))
|
||||
.replace("{upper_range}", String.valueOf(range.get(1)))
|
||||
.replace("{p}", String.valueOf(port)));
|
||||
pwmRange = range;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getPwmRangeImpl() {
|
||||
return pwmRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blinkImpl(int pulseTimeMillis, int blinks) {
|
||||
execute(
|
||||
@@ -103,8 +84,6 @@ public class CustomGPIO extends GPIOBase {
|
||||
|
||||
@Override
|
||||
public void setBrightnessImpl(int brightness) {
|
||||
// Check to see if dimValue is within the range
|
||||
if (brightness < pwmRange.get(0) || brightness > pwmRange.get(1)) return;
|
||||
execute(
|
||||
commands
|
||||
.get("dim")
|
||||
@@ -115,7 +94,6 @@ public class CustomGPIO extends GPIOBase {
|
||||
public static void setConfig(HardwareConfig config) {
|
||||
if (Platform.isRaspberryPi()) return;
|
||||
commands.replace("setState", config.ledSetCommand);
|
||||
commands.replace("setRange", config.ledPWMSetRange);
|
||||
commands.replace("dim", config.ledDimCommand);
|
||||
commands.replace("blink", config.ledBlinkCommand);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,6 @@ package org.photonvision.common.hardware.GPIO;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
@@ -32,7 +31,6 @@ public abstract class GPIOBase {
|
||||
new HashMap<>() {
|
||||
{
|
||||
put("setState", "");
|
||||
put("setRange", "");
|
||||
put("shutdown", "");
|
||||
put("dim", "");
|
||||
put("blink", "");
|
||||
@@ -81,22 +79,6 @@ public abstract class GPIOBase {
|
||||
|
||||
public abstract boolean getStateImpl();
|
||||
|
||||
public final void setPwmRange(List<Integer> range) {
|
||||
if (getPinNumber() != -1) {
|
||||
setPwmRangeImpl(range);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void setPwmRangeImpl(List<Integer> range);
|
||||
|
||||
public final List<Integer> getPwmRange() {
|
||||
if (getPinNumber() != -1) {
|
||||
return getPwmRangeImpl();
|
||||
} else return List.of(0, 255);
|
||||
}
|
||||
|
||||
protected abstract List<Integer> getPwmRangeImpl();
|
||||
|
||||
public final void blink(int pulseTimeMillis, int blinks) {
|
||||
if (getPinNumber() != -1) {
|
||||
blinkImpl(pulseTimeMillis, blinks);
|
||||
@@ -107,6 +89,8 @@ public abstract class GPIOBase {
|
||||
|
||||
public final void setBrightness(int brightness) {
|
||||
if (getPinNumber() != -1) {
|
||||
if (brightness > 100) brightness = 100;
|
||||
if (brightness < 0) brightness = 0;
|
||||
setBrightnessImpl(brightness);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
/*
|
||||
* 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.hardware.GPIO;
|
||||
|
||||
import static eu.xeli.jpigpio.PigpioException.*;
|
||||
|
||||
import eu.xeli.jpigpio.JPigpio;
|
||||
import eu.xeli.jpigpio.PigpioException;
|
||||
import eu.xeli.jpigpio.PigpioSocket;
|
||||
import eu.xeli.jpigpio.Pulse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class PiGPIO extends GPIOBase {
|
||||
private static final Logger logger = new Logger(PiGPIO.class, LogGroup.General);
|
||||
private final ArrayList<Pulse> pulses = new ArrayList<>();
|
||||
private final int port;
|
||||
|
||||
private int activeWaveId = -1;
|
||||
|
||||
public static JPigpio getPigpioDaemon() {
|
||||
return Singleton.INSTANCE;
|
||||
}
|
||||
|
||||
public PiGPIO(int address) {
|
||||
this(address, 8000, 255);
|
||||
}
|
||||
|
||||
public PiGPIO(int address, int frequency, int range) {
|
||||
port = address;
|
||||
if (port != -1) {
|
||||
try {
|
||||
// var pigpioRange = (int) (range / 255.0) * 40000; // TODO: is this conversion
|
||||
// correct/necessary?
|
||||
getPigpioDaemon().setPWMFrequency(port, frequency);
|
||||
getPigpioDaemon().setPWMRange(port, range);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not set PWM settings on port " + port, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelWave() throws PigpioException {
|
||||
if (activeWaveId != -1) {
|
||||
logger.debug("Cancelling wave with id " + activeWaveId);
|
||||
getPigpioDaemon().waveDelete(activeWaveId);
|
||||
getPigpioDaemon().waveTxStop();
|
||||
activeWaveId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPinNumber() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStateImpl(boolean state) {
|
||||
try {
|
||||
cancelWave();
|
||||
getPigpioDaemon().gpioWrite(port, state);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not set pin state on port " + port, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shutdown() {
|
||||
try {
|
||||
getPigpioDaemon().gpioTerminate();
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not terminate GPIO instance", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getStateImpl() {
|
||||
try {
|
||||
return getPigpioDaemon().gpioRead(port);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not read pin on port " + port, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPwmRangeImpl(List<Integer> range) {
|
||||
try {
|
||||
cancelWave();
|
||||
getPigpioDaemon().setPWMRange(port, range.get(0));
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not set PWM range on port " + port, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getPwmRangeImpl() {
|
||||
try {
|
||||
return List.of(0, getPigpioDaemon().getPWMRange(port));
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not get PWM range on port " + port, e);
|
||||
return List.of(0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blinkImpl(int pulseTimeMillis, int blinks) {
|
||||
boolean repeat = blinks == -1;
|
||||
|
||||
if (repeat) {
|
||||
blinks = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
cancelWave();
|
||||
pulses.clear();
|
||||
|
||||
var startPulse = new Pulse(1 << port, 0, pulseTimeMillis * 1000);
|
||||
var endPulse = new Pulse(0, 1 << port, pulseTimeMillis * 1000);
|
||||
|
||||
for (int i = 0; i < blinks; i++) {
|
||||
pulses.add(startPulse);
|
||||
pulses.add(endPulse);
|
||||
}
|
||||
|
||||
getPigpioDaemon().waveAddGeneric(pulses);
|
||||
var waveId = getPigpioDaemon().waveCreate();
|
||||
|
||||
if (waveId >= 0) {
|
||||
if (repeat) getPigpioDaemon().waveSendRepeat(waveId);
|
||||
else getPigpioDaemon().waveSendOnce(waveId);
|
||||
activeWaveId = waveId;
|
||||
} else {
|
||||
String error = "";
|
||||
switch (waveId) {
|
||||
case PI_EMPTY_WAVEFORM:
|
||||
error = "Waveform empty";
|
||||
break;
|
||||
case PI_TOO_MANY_CBS:
|
||||
error = "Too many CBS";
|
||||
break;
|
||||
case PI_TOO_MANY_OOL:
|
||||
error = "Too many OOL";
|
||||
break;
|
||||
case PI_NO_WAVEFORM_ID:
|
||||
error = "No waveform ID";
|
||||
break;
|
||||
}
|
||||
logger.error("Failed to send wave: " + error);
|
||||
}
|
||||
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not set blink on port " + port, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightnessImpl(int brightness) {
|
||||
try {
|
||||
cancelWave();
|
||||
getPigpioDaemon().setPWMDutycycle(port, getPwmRangeImpl().get(1) * (brightness / 100));
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not dim PWM on port " + port);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static class Singleton {
|
||||
public static JPigpio INSTANCE;
|
||||
|
||||
static {
|
||||
try {
|
||||
// Make sure daemon is running before connecting to it
|
||||
execute("pigpiod");
|
||||
INSTANCE = new PigpioSocket("localhost", 8888);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not connect to pigpio daemon.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
public enum PigpioCommand {
|
||||
PCMD_READ(3), // int gpio_read(unsigned gpio)
|
||||
PCMD_WRITE(4), // int gpio_write(unsigned gpio, unsigned level)
|
||||
PCMD_WVCLR(27), // int wave_clear(void)
|
||||
PCMD_WVAG(28), // int wave_add_generic(unsigned numPulses, gpioPulse_t *pulses)
|
||||
PCMD_WVHLT(33), // int wave_tx_stop(void)
|
||||
PCMD_WVCRE(49), // int wave_create(void)
|
||||
PCMD_WVDEL(50), // int wave_delete(unsigned wave_id)
|
||||
PCMD_WVTX(51), // int wave_tx_send(unsigned wave_id) (once)
|
||||
PCMD_WVTXR(52), // int wave_tx_send(unsigned wave_id) (repeat)
|
||||
PCMD_GDC(83), // int get_duty_cyle(unsigned user_gpio)
|
||||
PCMD_HP(86), // int hardware_pwm(unsigned gpio, unsigned PWMfreq, unsigned PWMduty)
|
||||
PCMD_WVTXM(100); // int wave_tx_send(unsigned wave_id, unsigned wave_mode)
|
||||
|
||||
public final int value;
|
||||
|
||||
PigpioCommand(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A class that defines the exceptions that can be thrown by Pigpio.
|
||||
*
|
||||
* <p>Credit to nkolban
|
||||
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/PigpioException.java
|
||||
*/
|
||||
@SuppressWarnings({"SpellCheckingInspection", "unused", "RedundantSuppression"})
|
||||
public class PigpioException extends Exception {
|
||||
|
||||
private int rc = -99999999;
|
||||
private static final long serialVersionUID = 443595760654129068L;
|
||||
|
||||
public PigpioException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PigpioException(int rc) {
|
||||
super();
|
||||
this.rc = rc;
|
||||
}
|
||||
|
||||
public PigpioException(int rc, String msg) {
|
||||
super(msg);
|
||||
this.rc = rc;
|
||||
}
|
||||
|
||||
public PigpioException(String arg0, Throwable arg1, boolean arg2, boolean arg3) {
|
||||
super(arg0, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
public PigpioException(String arg0, Throwable arg1) {
|
||||
super(arg0, arg1);
|
||||
}
|
||||
|
||||
public PigpioException(String arg0) {
|
||||
super(arg0);
|
||||
}
|
||||
|
||||
public PigpioException(Throwable arg0) {
|
||||
super(arg0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "(" + rc + ") " + getMessageForError(rc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the error code that was returned by the underlying Pigpio call.
|
||||
*
|
||||
* @return The error code that was returned by the underlying Pigpio call.
|
||||
*/
|
||||
public int getErrorCode() {
|
||||
return rc;
|
||||
} // End of getErrorCode
|
||||
|
||||
// Public constants for the error codes that can be thrown by Pigpio
|
||||
public static final int PI_INIT_FAILED = -1; // gpioInitialise failed
|
||||
public static final int PI_BAD_USER_GPIO = -2; // gpio not 0-31
|
||||
public static final int PI_BAD_GPIO = -3; // gpio not 0-53
|
||||
public static final int PI_BAD_MODE = -4; // mode not 0-7
|
||||
public static final int PI_BAD_LEVEL = -5; // level not 0-1
|
||||
public static final int PI_BAD_PUD = -6; // pud not 0-2
|
||||
public static final int PI_BAD_PULSEWIDTH = -7; // pulsewidth not 0 or 500-2500
|
||||
public static final int PI_BAD_DUTYCYCLE = -8; // dutycycle outside set range
|
||||
public static final int PI_BAD_TIMER = -9; // timer not 0-9
|
||||
public static final int PI_BAD_MS = -10; // ms not 10-60000
|
||||
public static final int PI_BAD_TIMETYPE = -11; // timetype not 0-1
|
||||
public static final int PI_BAD_SECONDS = -12; // seconds < 0
|
||||
public static final int PI_BAD_MICROS = -13; // micros not 0-999999
|
||||
public static final int PI_TIMER_FAILED = -14; // gpioSetTimerFunc failed
|
||||
public static final int PI_BAD_WDOG_TIMEOUT = -15; // timeout not 0-60000
|
||||
public static final int PI_NO_ALERT_FUNC = -16; // DEPRECATED
|
||||
public static final int PI_BAD_CLK_PERIPH = -17; // clock peripheral not 0-1
|
||||
public static final int PI_BAD_CLK_SOURCE = -18; // DEPRECATED
|
||||
public static final int PI_BAD_CLK_MICROS = -19; // clock micros not 1, 2, 4, 5, 8, or 10
|
||||
public static final int PI_BAD_BUF_MILLIS = -20; // buf millis not 100-10000
|
||||
public static final int PI_BAD_DUTYRANGE = -21; // dutycycle range not 25-40000
|
||||
public static final int PI_BAD_DUTY_RANGE = -21; // DEPRECATED (use PI_BAD_DUTYRANGE)
|
||||
public static final int PI_BAD_SIGNUM = -22; // signum not 0-63
|
||||
public static final int PI_BAD_PATHNAME = -23; // can't open pathname
|
||||
public static final int PI_NO_HANDLE = -24; // no handle available
|
||||
public static final int PI_BAD_HANDLE = -25; // unknown handle
|
||||
public static final int PI_BAD_IF_FLAGS = -26; // ifFlags > 3
|
||||
public static final int PI_BAD_CHANNEL = -27; // DMA channel not 0-14
|
||||
public static final int PI_BAD_PRIM_CHANNEL = -27; // DMA primary channel not 0-14
|
||||
public static final int PI_BAD_SOCKET_PORT = -28; // socket port not 1024-32000
|
||||
public static final int PI_BAD_FIFO_COMMAND = -29; // unrecognized fifo command
|
||||
public static final int PI_BAD_SECO_CHANNEL = -30; // DMA secondary channel not 0-6
|
||||
public static final int PI_NOT_INITIALISED = -31; // function called before gpioInitialise
|
||||
public static final int PI_INITIALISED = -32; // function called after gpioInitialise
|
||||
public static final int PI_BAD_WAVE_MODE = -33; // waveform mode not 0-1
|
||||
public static final int PI_BAD_CFG_INTERNAL = -34; // bad parameter in gpioCfgInternals call
|
||||
public static final int PI_BAD_WAVE_BAUD = -35; // baud rate not 50-250K(RX)/50-1M(TX)
|
||||
public static final int PI_TOO_MANY_PULSES = -36; // waveform has too many pulses
|
||||
public static final int PI_TOO_MANY_CHARS = -37; // waveform has too many chars
|
||||
public static final int PI_NOT_SERIAL_GPIO = -38; // no serial read in progress on gpio
|
||||
public static final int PI_BAD_SERIAL_STRUC = -39; // bad (null) serial structure parameter
|
||||
public static final int PI_BAD_SERIAL_BUF = -40; // bad (null) serial buf parameter
|
||||
public static final int PI_NOT_PERMITTED = -41; // gpio operation not permitted
|
||||
public static final int PI_SOME_PERMITTED = -42; // one or more gpios not permitted
|
||||
public static final int PI_BAD_WVSC_COMMND = -43; // bad WVSC subcommand
|
||||
public static final int PI_BAD_WVSM_COMMND = -44; // bad WVSM subcommand
|
||||
public static final int PI_BAD_WVSP_COMMND = -45; // bad WVSP subcommand
|
||||
public static final int PI_BAD_PULSELEN = -46; // trigger pulse length not 1-100
|
||||
public static final int PI_BAD_SCRIPT = -47; // invalid script
|
||||
public static final int PI_BAD_SCRIPT_ID = -48; // unknown script id
|
||||
public static final int PI_BAD_SER_OFFSET = -49; // add serial data offset > 30 minutes
|
||||
public static final int PI_GPIO_IN_USE = -50; // gpio already in use
|
||||
public static final int PI_BAD_SERIAL_COUNT = -51; // must read at least a byte at a time
|
||||
public static final int PI_BAD_PARAM_NUM = -52; // script parameter id not 0-9
|
||||
public static final int PI_DUP_TAG = -53; // script has duplicate tag
|
||||
public static final int PI_TOO_MANY_TAGS = -54; // script has too many tags
|
||||
public static final int PI_BAD_SCRIPT_CMD = -55; // illegal script command
|
||||
public static final int PI_BAD_VAR_NUM = -56; // script variable id not 0-149
|
||||
public static final int PI_NO_SCRIPT_ROOM = -57; // no more room for scripts
|
||||
public static final int PI_NO_MEMORY = -58; // can't allocate temporary memory
|
||||
public static final int PI_SOCK_READ_FAILED = -59; // socket read failed
|
||||
public static final int PI_SOCK_WRIT_FAILED = -60; // socket write failed
|
||||
public static final int PI_TOO_MANY_PARAM = -61; // too many script parameters (> 10)
|
||||
public static final int PI_NOT_HALTED = -62; // script already running or failed
|
||||
public static final int PI_BAD_TAG = -63; // script has unresolved tag
|
||||
public static final int PI_BAD_MICS_DELAY = -64; // bad MICS delay (too large)
|
||||
public static final int PI_BAD_MILS_DELAY = -65; // bad MILS delay (too large)
|
||||
public static final int PI_BAD_WAVE_ID = -66; // non existent wave id
|
||||
public static final int PI_TOO_MANY_CBS = -67; // No more CBs for waveform
|
||||
public static final int PI_TOO_MANY_OOL = -68; // No more OOL for waveform
|
||||
public static final int PI_EMPTY_WAVEFORM = -69; // attempt to create an empty waveform
|
||||
public static final int PI_NO_WAVEFORM_ID = -70; // no more waveforms
|
||||
public static final int PI_I2C_OPEN_FAILED = -71; // can't open I2C device
|
||||
public static final int PI_SER_OPEN_FAILED = -72; // can't open serial device
|
||||
public static final int PI_SPI_OPEN_FAILED = -73; // can't open SPI device
|
||||
public static final int PI_BAD_I2C_BUS = -74; // bad I2C bus
|
||||
public static final int PI_BAD_I2C_ADDR = -75; // bad I2C address
|
||||
public static final int PI_BAD_SPI_CHANNEL = -76; // bad SPI channel
|
||||
public static final int PI_BAD_FLAGS = -77; // bad i2c/spi/ser open flags
|
||||
public static final int PI_BAD_SPI_SPEED = -78; // bad SPI speed
|
||||
public static final int PI_BAD_SER_DEVICE = -79; // bad serial device name
|
||||
public static final int PI_BAD_SER_SPEED = -80; // bad serial baud rate
|
||||
public static final int PI_BAD_PARAM = -81; // bad i2c/spi/ser parameter
|
||||
public static final int PI_I2C_WRITE_FAILED = -82; // i2c write failed
|
||||
public static final int PI_I2C_READ_FAILED = -83; // i2c read failed
|
||||
public static final int PI_BAD_SPI_COUNT = -84; // bad SPI count
|
||||
public static final int PI_SER_WRITE_FAILED = -85; // ser write failed
|
||||
public static final int PI_SER_READ_FAILED = -86; // ser read failed
|
||||
public static final int PI_SER_READ_NO_DATA = -87; // ser read no data available
|
||||
public static final int PI_UNKNOWN_COMMAND = -88; // unknown command
|
||||
public static final int PI_SPI_XFER_FAILED = -89; // spi xfer/read/write failed
|
||||
public static final int PI_BAD_POINTER = -90; // bad (NULL) pointer
|
||||
public static final int PI_NO_AUX_SPI = -91; // need a A+/B+/Pi2 for auxiliary SPI
|
||||
public static final int PI_NOT_PWM_GPIO = -92; // gpio is not in use for PWM
|
||||
public static final int PI_NOT_SERVO_GPIO = -93; // gpio is not in use for servo pulses
|
||||
public static final int PI_NOT_HCLK_GPIO = -94; // gpio has no hardware clock
|
||||
public static final int PI_NOT_HPWM_GPIO = -95; // gpio has no hardware PWM
|
||||
public static final int PI_BAD_HPWM_FREQ = -96; // hardware PWM frequency not 1-125M
|
||||
public static final int PI_BAD_HPWM_DUTY = -97; // hardware PWM dutycycle not 0-1M
|
||||
public static final int PI_BAD_HCLK_FREQ = -98; // hardware clock frequency not 4689-250M
|
||||
public static final int PI_BAD_HCLK_PASS = -99; // need password to use hardware clock 1
|
||||
public static final int PI_HPWM_ILLEGAL = -100; // illegal, PWM in use for main clock
|
||||
public static final int PI_BAD_DATABITS = -101; // serial data bits not 1-32
|
||||
public static final int PI_BAD_STOPBITS = -102; // serial (half) stop bits not 2-8
|
||||
public static final int PI_MSG_TOOBIG = -103; // socket/pipe message too big
|
||||
public static final int PI_BAD_MALLOC_MODE = -104; // bad memory allocation mode
|
||||
public static final int PI_TOO_MANY_SEGS = -105; // too many I2C transaction parts
|
||||
public static final int PI_BAD_I2C_SEG = -106; // a combined I2C transaction failed
|
||||
public static final int PI_BAD_SMBUS_CMD = -107;
|
||||
public static final int PI_NOT_I2C_GPIO = -108;
|
||||
public static final int PI_BAD_I2C_WLEN = -109;
|
||||
public static final int PI_BAD_I2C_RLEN = -110;
|
||||
public static final int PI_BAD_I2C_CMD = -111;
|
||||
public static final int PI_BAD_I2C_BAUD = -112;
|
||||
public static final int PI_CHAIN_LOOP_CNT = -113;
|
||||
public static final int PI_BAD_CHAIN_LOOP = -114;
|
||||
public static final int PI_CHAIN_COUNTER = -115;
|
||||
public static final int PI_BAD_CHAIN_CMD = -116;
|
||||
public static final int PI_BAD_CHAIN_DELAY = -117;
|
||||
public static final int PI_CHAIN_NESTING = -118;
|
||||
public static final int PI_CHAIN_TOO_BIG = -119;
|
||||
public static final int PI_DEPRECATED = -120;
|
||||
public static final int PI_BAD_SER_INVERT = -121;
|
||||
public static final int PI_BAD_EDGE = -122;
|
||||
public static final int PI_BAD_ISR_INIT = -123;
|
||||
public static final int PI_BAD_FOREVER = -124;
|
||||
public static final int PI_BAD_FILTER = -125;
|
||||
|
||||
public static final int PI_PIGIF_ERR_0 = -2000;
|
||||
public static final int PI_PIGIF_ERR_99 = -2099;
|
||||
|
||||
public static final int PI_CUSTOM_ERR_0 = -3000;
|
||||
public static final int PI_CUSTOM_ERR_999 = -3999;
|
||||
|
||||
private static final HashMap<Integer, String> errorMessages = new HashMap<>();
|
||||
|
||||
static {
|
||||
errorMessages.put(PI_INIT_FAILED, "pigpio initialisation failed");
|
||||
errorMessages.put(PI_BAD_USER_GPIO, "GPIO not 0-31");
|
||||
errorMessages.put(PI_BAD_GPIO, "GPIO not 0-53");
|
||||
errorMessages.put(PI_BAD_MODE, "mode not 0-7");
|
||||
errorMessages.put(PI_BAD_LEVEL, "level not 0-1");
|
||||
errorMessages.put(PI_BAD_PUD, "pud not 0-2");
|
||||
errorMessages.put(PI_BAD_PULSEWIDTH, "pulsewidth not 0 or 500-2500");
|
||||
errorMessages.put(PI_BAD_DUTYCYCLE, "dutycycle not 0-range (default 255)");
|
||||
errorMessages.put(PI_BAD_TIMER, "timer not 0-9");
|
||||
errorMessages.put(PI_BAD_MS, "ms not 10-60000");
|
||||
errorMessages.put(PI_BAD_TIMETYPE, "timetype not 0-1");
|
||||
errorMessages.put(PI_BAD_SECONDS, "seconds < 0");
|
||||
errorMessages.put(PI_BAD_MICROS, "micros not 0-999999");
|
||||
errorMessages.put(PI_TIMER_FAILED, "gpioSetTimerFunc failed");
|
||||
errorMessages.put(PI_BAD_WDOG_TIMEOUT, "timeout not 0-60000");
|
||||
errorMessages.put(PI_NO_ALERT_FUNC, "DEPRECATED");
|
||||
errorMessages.put(PI_BAD_CLK_PERIPH, "clock peripheral not 0-1");
|
||||
errorMessages.put(PI_BAD_CLK_SOURCE, "DEPRECATED");
|
||||
errorMessages.put(PI_BAD_CLK_MICROS, "clock micros not 1, 2, 4, 5, 8, or 10");
|
||||
errorMessages.put(PI_BAD_BUF_MILLIS, "buf millis not 100-10000");
|
||||
errorMessages.put(PI_BAD_DUTYRANGE, "dutycycle range not 25-40000");
|
||||
errorMessages.put(PI_BAD_SIGNUM, "signum not 0-63");
|
||||
errorMessages.put(PI_BAD_PATHNAME, "can't open pathname");
|
||||
errorMessages.put(PI_NO_HANDLE, "no handle available");
|
||||
errorMessages.put(PI_BAD_HANDLE, "unknown handle");
|
||||
errorMessages.put(PI_BAD_IF_FLAGS, "ifFlags > 3");
|
||||
errorMessages.put(PI_BAD_CHANNEL, "DMA channel not 0-14");
|
||||
errorMessages.put(PI_BAD_SOCKET_PORT, "socket port not 1024-30000");
|
||||
errorMessages.put(PI_BAD_FIFO_COMMAND, "unknown fifo command");
|
||||
errorMessages.put(PI_BAD_SECO_CHANNEL, "DMA secondary channel not 0-14");
|
||||
errorMessages.put(PI_NOT_INITIALISED, "function called before gpioInitialise");
|
||||
errorMessages.put(PI_INITIALISED, "function called after gpioInitialise");
|
||||
errorMessages.put(PI_BAD_WAVE_MODE, "waveform mode not 0-1");
|
||||
errorMessages.put(PI_BAD_CFG_INTERNAL, "bad parameter in gpioCfgInternals call");
|
||||
errorMessages.put(PI_BAD_WAVE_BAUD, "baud rate not 50-250000(RX)/1000000(TX)");
|
||||
errorMessages.put(PI_TOO_MANY_PULSES, "waveform has too many pulses");
|
||||
errorMessages.put(PI_TOO_MANY_CHARS, "waveform has too many chars");
|
||||
errorMessages.put(PI_NOT_SERIAL_GPIO, "no bit bang serial read in progress on GPIO");
|
||||
errorMessages.put(PI_NOT_PERMITTED, "no permission to update GPIO");
|
||||
errorMessages.put(PI_SOME_PERMITTED, "no permission to update one or more GPIO");
|
||||
errorMessages.put(PI_BAD_WVSC_COMMND, "bad WVSC subcommand");
|
||||
errorMessages.put(PI_BAD_WVSM_COMMND, "bad WVSM subcommand");
|
||||
errorMessages.put(PI_BAD_WVSP_COMMND, "bad WVSP subcommand");
|
||||
errorMessages.put(PI_BAD_PULSELEN, "trigger pulse length not 1-100");
|
||||
errorMessages.put(PI_BAD_SCRIPT, "invalid script");
|
||||
errorMessages.put(PI_BAD_SCRIPT_ID, "unknown script id");
|
||||
errorMessages.put(PI_BAD_SER_OFFSET, "add serial data offset > 30 minute");
|
||||
errorMessages.put(PI_GPIO_IN_USE, "GPIO already in use");
|
||||
errorMessages.put(PI_BAD_SERIAL_COUNT, "must read at least a byte at a time");
|
||||
errorMessages.put(PI_BAD_PARAM_NUM, "script parameter id not 0-9");
|
||||
errorMessages.put(PI_DUP_TAG, "script has duplicate tag");
|
||||
errorMessages.put(PI_TOO_MANY_TAGS, "script has too many tags");
|
||||
errorMessages.put(PI_BAD_SCRIPT_CMD, "illegal script command");
|
||||
errorMessages.put(PI_BAD_VAR_NUM, "script variable id not 0-149");
|
||||
errorMessages.put(PI_NO_SCRIPT_ROOM, "no more room for scripts");
|
||||
errorMessages.put(PI_NO_MEMORY, "can't allocate temporary memory");
|
||||
errorMessages.put(PI_SOCK_READ_FAILED, "socket read failed");
|
||||
errorMessages.put(PI_SOCK_WRIT_FAILED, "socket write failed");
|
||||
errorMessages.put(PI_TOO_MANY_PARAM, "too many script parameters (> 10)");
|
||||
errorMessages.put(PI_NOT_HALTED, "script already running or failed");
|
||||
errorMessages.put(PI_BAD_TAG, "script has unresolved tag");
|
||||
errorMessages.put(PI_BAD_MICS_DELAY, "bad MICS delay (too large)");
|
||||
errorMessages.put(PI_BAD_MILS_DELAY, "bad MILS delay (too large)");
|
||||
errorMessages.put(PI_BAD_WAVE_ID, "non existent wave id");
|
||||
errorMessages.put(PI_TOO_MANY_CBS, "No more CBs for waveform");
|
||||
errorMessages.put(PI_TOO_MANY_OOL, "No more OOL for waveform");
|
||||
errorMessages.put(PI_EMPTY_WAVEFORM, "attempt to create an empty waveform");
|
||||
errorMessages.put(PI_NO_WAVEFORM_ID, "No more waveform ids");
|
||||
errorMessages.put(PI_I2C_OPEN_FAILED, "can't open I2C device");
|
||||
errorMessages.put(PI_SER_OPEN_FAILED, "can't open serial device");
|
||||
errorMessages.put(PI_SPI_OPEN_FAILED, "can't open SPI device");
|
||||
errorMessages.put(PI_BAD_I2C_BUS, "bad I2C bus");
|
||||
errorMessages.put(PI_BAD_I2C_ADDR, "bad I2C address");
|
||||
errorMessages.put(PI_BAD_SPI_CHANNEL, "bad SPI channel");
|
||||
errorMessages.put(PI_BAD_FLAGS, "bad i2c/spi/ser open flags");
|
||||
errorMessages.put(PI_BAD_SPI_SPEED, "bad SPI speed");
|
||||
errorMessages.put(PI_BAD_SER_DEVICE, "bad serial device name");
|
||||
errorMessages.put(PI_BAD_SER_SPEED, "bad serial baud rate");
|
||||
errorMessages.put(PI_BAD_PARAM, "bad i2c/spi/ser parameter");
|
||||
errorMessages.put(PI_I2C_WRITE_FAILED, "I2C write failed");
|
||||
errorMessages.put(PI_I2C_READ_FAILED, "I2C read failed");
|
||||
errorMessages.put(PI_BAD_SPI_COUNT, "bad SPI count");
|
||||
errorMessages.put(PI_SER_WRITE_FAILED, "ser write failed");
|
||||
errorMessages.put(PI_SER_READ_FAILED, "ser read failed");
|
||||
errorMessages.put(PI_SER_READ_NO_DATA, "ser read no data available");
|
||||
errorMessages.put(PI_UNKNOWN_COMMAND, "unknown command");
|
||||
errorMessages.put(PI_SPI_XFER_FAILED, "SPI xfer/read/write failed");
|
||||
errorMessages.put(PI_BAD_POINTER, "bad (NULL) pointer");
|
||||
errorMessages.put(PI_NO_AUX_SPI, "no auxiliary SPI on Pi A or B");
|
||||
errorMessages.put(PI_NOT_PWM_GPIO, "GPIO is not in use for PWM");
|
||||
errorMessages.put(PI_NOT_SERVO_GPIO, "GPIO is not in use for servo pulses");
|
||||
errorMessages.put(PI_NOT_HCLK_GPIO, "GPIO has no hardware clock");
|
||||
errorMessages.put(PI_NOT_HPWM_GPIO, "GPIO has no hardware PWM");
|
||||
errorMessages.put(PI_BAD_HPWM_FREQ, "hardware PWM frequency not 1-125M");
|
||||
errorMessages.put(PI_BAD_HPWM_DUTY, "hardware PWM dutycycle not 0-1M");
|
||||
errorMessages.put(PI_BAD_HCLK_FREQ, "hardware clock frequency not 4689-250M");
|
||||
errorMessages.put(PI_BAD_HCLK_PASS, "need password to use hardware clock 1");
|
||||
errorMessages.put(PI_HPWM_ILLEGAL, "illegal, PWM in use for main clock");
|
||||
errorMessages.put(PI_BAD_DATABITS, "serial data bits not 1-32");
|
||||
errorMessages.put(PI_BAD_STOPBITS, "serial (half) stop bits not 2-8");
|
||||
errorMessages.put(PI_MSG_TOOBIG, "socket/pipe message too big");
|
||||
errorMessages.put(PI_BAD_MALLOC_MODE, "bad memory allocation mode");
|
||||
errorMessages.put(PI_TOO_MANY_SEGS, "too many I2C transaction segments");
|
||||
errorMessages.put(PI_BAD_I2C_SEG, "an I2C transaction segment failed");
|
||||
errorMessages.put(PI_BAD_SMBUS_CMD, "SMBus command not supported");
|
||||
errorMessages.put(PI_NOT_I2C_GPIO, "no bit bang I2C in progress on GPIO");
|
||||
errorMessages.put(PI_BAD_I2C_WLEN, "bad I2C write length");
|
||||
errorMessages.put(PI_BAD_I2C_RLEN, "bad I2C read length");
|
||||
errorMessages.put(PI_BAD_I2C_CMD, "bad I2C command");
|
||||
errorMessages.put(PI_BAD_I2C_BAUD, "bad I2C baud rate, not 50-500k");
|
||||
errorMessages.put(PI_CHAIN_LOOP_CNT, "bad chain loop count");
|
||||
errorMessages.put(PI_BAD_CHAIN_LOOP, "empty chain loop");
|
||||
errorMessages.put(PI_CHAIN_COUNTER, "too many chain counters");
|
||||
errorMessages.put(PI_BAD_CHAIN_CMD, "bad chain command");
|
||||
errorMessages.put(PI_BAD_CHAIN_DELAY, "bad chain delay micros");
|
||||
errorMessages.put(PI_CHAIN_NESTING, "chain counters nested too deeply");
|
||||
errorMessages.put(PI_CHAIN_TOO_BIG, "chain is too long");
|
||||
errorMessages.put(PI_DEPRECATED, "deprecated function removed");
|
||||
errorMessages.put(PI_BAD_SER_INVERT, "bit bang serial invert not 0 or 1");
|
||||
errorMessages.put(PI_BAD_EDGE, "bad ISR edge value, not 0-2");
|
||||
errorMessages.put(PI_BAD_ISR_INIT, "bad ISR initialisation");
|
||||
errorMessages.put(PI_BAD_FOREVER, "loop forever must be last chain command");
|
||||
errorMessages.put(PI_BAD_FILTER, "bad filter parameter");
|
||||
}
|
||||
|
||||
public static String getMessageForError(int errorCode) {
|
||||
return errorMessages.get(errorCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
|
||||
|
||||
import org.photonvision.common.hardware.GPIO.GPIOBase;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class PigpioPin extends GPIOBase {
|
||||
|
||||
public static final Logger logger = new Logger(PigpioPin.class, LogGroup.General);
|
||||
private static final PigpioSocket piSocket = new PigpioSocket();
|
||||
|
||||
private final boolean isHardwarePWMPin;
|
||||
private final int pinNo;
|
||||
|
||||
private boolean hasFailedHardwarePWM;
|
||||
|
||||
public PigpioPin(int pinNo) {
|
||||
isHardwarePWMPin = pinNo == 12 || pinNo == 13 || pinNo == 17 || pinNo == 18;
|
||||
this.pinNo = pinNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPinNumber() {
|
||||
return pinNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateImpl(boolean state) {
|
||||
try {
|
||||
piSocket.gpioWrite(pinNo, state);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("gpioWrite FAIL - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shutdown() {
|
||||
setState(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getStateImpl() {
|
||||
try {
|
||||
return piSocket.gpioRead(pinNo);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("gpioRead FAIL - " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void blinkImpl(int pulseTimeMillis, int blinks) {
|
||||
try {
|
||||
piSocket.generateAndSendWaveform(pulseTimeMillis, blinks, pinNo);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not set blink - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setBrightnessImpl(int brightness) {
|
||||
if (isHardwarePWMPin) {
|
||||
try {
|
||||
piSocket.hardwarePWM(pinNo, 22000, (int) (1000000 * (brightness / 100.0)));
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Failed to hardPWM - " + e.getMessage());
|
||||
}
|
||||
} else if (!hasFailedHardwarePWM) {
|
||||
logger.warn(
|
||||
"Specified pin ("
|
||||
+ pinNo
|
||||
+ ") is not capable of hardware PWM - no action will be taken.");
|
||||
hasFailedHardwarePWM = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
public class PigpioPulse {
|
||||
int gpioOn;
|
||||
int gpioOff;
|
||||
int delayMicros;
|
||||
|
||||
/**
|
||||
* Initialises a pulse.
|
||||
*
|
||||
* @param gpioOn GPIO number to switch on at the start of the pulse. If zero, then no GPIO will be
|
||||
* switched on.
|
||||
* @param gpioOff GPIO number to switch off at the start of the pulse. If zero, then no GPIO will
|
||||
* be switched off.
|
||||
* @param delayMicros the delay in microseconds before the next pulse.
|
||||
*/
|
||||
public PigpioPulse(int gpioOn, int gpioOff, int delayMicros) {
|
||||
this.gpioOn = gpioOn != 0 ? 1 << gpioOn : 0;
|
||||
this.gpioOff = gpioOff != 0 ? 1 << gpioOff : 0;
|
||||
this.delayMicros = delayMicros;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
|
||||
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.PI_NO_WAVEFORM_ID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
@SuppressWarnings({"SpellCheckingInspection", "unused"})
|
||||
public class PigpioSocket {
|
||||
private static final Logger logger = new Logger(PigpioSocket.class, LogGroup.General);
|
||||
private static final int PIGPIOD_MESSAGE_SIZE = 12;
|
||||
|
||||
private PigpioSocketLock commandSocket;
|
||||
private int activeWaveformID = -1;
|
||||
|
||||
/** Creates and starts a socket connection to a pigpio daemon on localhost */
|
||||
public PigpioSocket() {
|
||||
this("127.0.0.1", 8888);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts a socket connection to a pigpio daemon on a remote host with the specified
|
||||
* address and port
|
||||
*
|
||||
* @param addr Address of remote pigpio daemon
|
||||
* @param port Port of remote pigpio daemon
|
||||
*/
|
||||
public PigpioSocket(String addr, int port) {
|
||||
try {
|
||||
commandSocket = new PigpioSocketLock(addr, port);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create or connect to Pigpio Daemon socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnects to the pigpio daemon
|
||||
*
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void reconnect() throws PigpioException {
|
||||
try {
|
||||
commandSocket.reconnect();
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to reconnect to Pigpio Daemon socket", e);
|
||||
throw new PigpioException("reconnect", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the connection to the pigpio daemon
|
||||
*
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void gpioTerminate() throws PigpioException {
|
||||
try {
|
||||
commandSocket.terminate();
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to terminate connection to Pigpio Daemon socket", e);
|
||||
throw new PigpioException("gpioTerminate", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the GPIO level
|
||||
*
|
||||
* @param pin Pin to read from
|
||||
* @return Value of the pin
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public boolean gpioRead(int pin) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_READ.value, pin);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode != 0;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to read GPIO pin: " + pin, e);
|
||||
throw new PigpioException("gpioRead", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the GPIO level
|
||||
*
|
||||
* @param pin Pin to write to
|
||||
* @param value Value to write
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void gpioWrite(int pin, boolean value) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WRITE.value, pin, value ? 1 : 0);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to write to GPIO pin: " + pin, e);
|
||||
throw new PigpioException("gpioWrite", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all waveforms and any data added by calls to {@link #waveAddGeneric(ArrayList)}
|
||||
*
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void waveClear() throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCLR.value);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to clear waveforms", e);
|
||||
throw new PigpioException("waveClear", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a number of pulses to the current waveform
|
||||
*
|
||||
* @param pulses ArrayList of pulses to add
|
||||
* @return the new total number of pulses in the current waveform
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
private int waveAddGeneric(ArrayList<PigpioPulse> pulses) throws PigpioException {
|
||||
// pigpio wave message format
|
||||
|
||||
// I p1 0
|
||||
// I p2 0
|
||||
// I p3 pulses * 12
|
||||
// ## extension ##
|
||||
// III on/off/delay * pulses
|
||||
|
||||
if (pulses == null || pulses.size() == 0) return 0;
|
||||
|
||||
try {
|
||||
ByteBuffer bb = ByteBuffer.allocate(pulses.size() * 12);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (var pulse : pulses) {
|
||||
bb.putInt(pulse.gpioOn).putInt(pulse.gpioOff).putInt(pulse.delayMicros);
|
||||
}
|
||||
|
||||
int retCode =
|
||||
commandSocket.sendCmd(
|
||||
PigpioCommand.PCMD_WVAG.value,
|
||||
0,
|
||||
0,
|
||||
pulses.size() * PIGPIOD_MESSAGE_SIZE,
|
||||
bb.array());
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
|
||||
return retCode;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to add pulse(s) to waveform", e);
|
||||
throw new PigpioException("waveAddGeneric", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates pulses and adds them to the current waveform
|
||||
*
|
||||
* @param pulseTimeMillis Pulse length in milliseconds
|
||||
* @param blinks Number of times to pulse. -1 for repeat
|
||||
* @param pinNo Pin to pulse
|
||||
*/
|
||||
private void addBlinkPulsesToWaveform(int pulseTimeMillis, int blinks, int pinNo) {
|
||||
boolean repeat = blinks == -1;
|
||||
|
||||
if (blinks == 0) return;
|
||||
|
||||
if (repeat) {
|
||||
blinks = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
ArrayList<PigpioPulse> pulses = new ArrayList<>();
|
||||
var startPulse = new PigpioPulse(pinNo, 0, pulseTimeMillis * 1000);
|
||||
var endPulse = new PigpioPulse(0, pinNo, pulseTimeMillis * 1000);
|
||||
|
||||
for (int i = 0; i < blinks; i++) {
|
||||
pulses.add(startPulse);
|
||||
pulses.add(endPulse);
|
||||
}
|
||||
|
||||
waveAddGeneric(pulses);
|
||||
pulses.clear();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and sends a waveform to the given pins with the specified parameters.
|
||||
*
|
||||
* @param pulseTimeMillis Pulse length in milliseconds
|
||||
* @param blinks Number of times to pulse. -1 for repeat
|
||||
* @param pins Pins to pulse
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void generateAndSendWaveform(int pulseTimeMillis, int blinks, int... pins)
|
||||
throws PigpioException {
|
||||
if (pins.length == 0) return;
|
||||
boolean repeat = blinks == -1;
|
||||
if (blinks == 0) return;
|
||||
|
||||
// stop any active waves
|
||||
waveTxStop();
|
||||
waveClear();
|
||||
|
||||
if (activeWaveformID != -1) {
|
||||
waveDelete(activeWaveformID);
|
||||
activeWaveformID = -1;
|
||||
}
|
||||
|
||||
for (int pin : pins) {
|
||||
addBlinkPulsesToWaveform(pulseTimeMillis, blinks, pin);
|
||||
}
|
||||
|
||||
int waveformId = waveCreate();
|
||||
|
||||
if (waveformId >= 0) {
|
||||
if (repeat) {
|
||||
waveSendRepeat(waveformId);
|
||||
} else {
|
||||
waveSendOnce(waveformId);
|
||||
}
|
||||
} else {
|
||||
String error = "";
|
||||
switch (waveformId) {
|
||||
case PI_EMPTY_WAVEFORM:
|
||||
error = "Waveform empty";
|
||||
break;
|
||||
case PI_TOO_MANY_CBS:
|
||||
error = "Too many CBS";
|
||||
break;
|
||||
case PI_TOO_MANY_OOL:
|
||||
error = "Too many OOL";
|
||||
break;
|
||||
case PI_NO_WAVEFORM_ID:
|
||||
error = "No waveform ID";
|
||||
break;
|
||||
}
|
||||
logger.error("Failed to send wave: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the transmission of the current waveform
|
||||
*
|
||||
* @return success
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public boolean waveTxStop() throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVHLT.value);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode == 0;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to stop waveform", e);
|
||||
throw new PigpioException("waveTxStop", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a waveform from the data provided by the prior calls to {@link
|
||||
* #waveAddGeneric(ArrayList)} Upon success a wave ID greater than or equal to 0 is returned
|
||||
*
|
||||
* @return ID of the created waveform
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public int waveCreate() throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCRE.value);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create new waveform", e);
|
||||
throw new PigpioException("waveCreate", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the waveform with specified wave ID
|
||||
*
|
||||
* @param waveId ID of the waveform to delete
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void waveDelete(int waveId) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVDEL.value, waveId);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to delete wave: " + waveId, e);
|
||||
throw new PigpioException("waveDelete", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits the waveform with specified wave ID. The waveform is sent once
|
||||
*
|
||||
* @param waveId ID of the waveform to transmit
|
||||
* @return The number of DMA control blocks in the waveform
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public int waveSendOnce(int waveId) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTX.value, waveId);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode;
|
||||
} catch (IOException e) {
|
||||
throw new PigpioException("waveSendOnce", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits the waveform with specified wave ID. The waveform cycles until cancelled (either by
|
||||
* the sending of a new waveform or {@link #waveTxStop()}
|
||||
*
|
||||
* @param waveId ID of the waveform to transmit
|
||||
* @return The number of DMA control blocks in the waveform
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public int waveSendRepeat(int waveId) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTXR.value, waveId);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode;
|
||||
} catch (IOException e) {
|
||||
throw new PigpioException("waveSendRepeat", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts hardware PWM on a GPIO at the specified frequency and dutycycle
|
||||
*
|
||||
* @param pin GPIO pin to start PWM on
|
||||
* @param pwmFrequency Frequency to run at (1Hz-125MHz). Frequencies above 30MHz are unlikely to
|
||||
* work
|
||||
* @param pwmDuty Duty cycle to run at (0-1,000,000)
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void hardwarePWM(int pin, int pwmFrequency, int pwmDuty) throws PigpioException {
|
||||
try {
|
||||
ByteBuffer bb = ByteBuffer.allocate(4);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.putInt(pwmDuty);
|
||||
|
||||
int retCode =
|
||||
commandSocket.sendCmd(PigpioCommand.PCMD_HP.value, pin, pwmFrequency, 4, bb.array());
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
} catch (IOException e) {
|
||||
throw new PigpioException("hardwarePWM", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Credit to nkolban
|
||||
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/SocketLock.java
|
||||
*/
|
||||
final class PigpioSocketLock {
|
||||
private static final int replyTimeoutMillis = 1000;
|
||||
|
||||
private final String addr;
|
||||
private final int port;
|
||||
|
||||
private Socket socket;
|
||||
private DataInputStream in;
|
||||
private DataOutputStream out;
|
||||
|
||||
public PigpioSocketLock(String addr, int port) throws IOException {
|
||||
this.addr = addr;
|
||||
this.port = port;
|
||||
reconnect();
|
||||
}
|
||||
|
||||
public void reconnect() throws IOException {
|
||||
socket = new Socket(addr, port);
|
||||
out = new DataOutputStream(socket.getOutputStream());
|
||||
in = new DataInputStream(socket.getInputStream());
|
||||
}
|
||||
|
||||
public void terminate() throws IOException {
|
||||
in.close();
|
||||
in = null;
|
||||
|
||||
out.flush();
|
||||
out.close();
|
||||
out = null;
|
||||
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
public synchronized int sendCmd(int cmd) throws IOException {
|
||||
byte[] b = {};
|
||||
return sendCmd(cmd, 0, 0, 0, b);
|
||||
}
|
||||
|
||||
public synchronized int sendCmd(int cmd, int p1) throws IOException {
|
||||
byte[] b = {};
|
||||
return sendCmd(cmd, p1, 0, 0, b);
|
||||
}
|
||||
|
||||
public synchronized int sendCmd(int cmd, int p1, int p2) throws IOException {
|
||||
byte[] b = {};
|
||||
return sendCmd(cmd, p1, p2, 0, b);
|
||||
}
|
||||
|
||||
public synchronized int sendCmd(int cmd, int p1, int p2, int p3) throws IOException {
|
||||
byte[] b = {};
|
||||
return sendCmd(cmd, p1, p2, p3, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send extended command to pigpiod and return result code
|
||||
*
|
||||
* @param cmd Command to send
|
||||
* @param p1 Command parameter 1
|
||||
* @param p2 Command parameter 2
|
||||
* @param p3 Command parameter 3 (usually length of extended data - see paramater ext)
|
||||
* @param ext Array of bytes containing extended data
|
||||
* @return Command result code
|
||||
* @throws IOException in case of network connection error
|
||||
*/
|
||||
@SuppressWarnings("UnusedAssignment")
|
||||
public synchronized int sendCmd(int cmd, int p1, int p2, int p3, byte[] ext) throws IOException {
|
||||
ByteBuffer bb = ByteBuffer.allocate(16 + ext.length);
|
||||
|
||||
bb.putInt(Integer.reverseBytes(cmd));
|
||||
bb.putInt(Integer.reverseBytes(p1));
|
||||
bb.putInt(Integer.reverseBytes(p2));
|
||||
bb.putInt(Integer.reverseBytes(p3));
|
||||
|
||||
if (ext.length > 0) {
|
||||
bb.put(ext);
|
||||
}
|
||||
|
||||
out.write(bb.array());
|
||||
out.flush();
|
||||
|
||||
int w = replyTimeoutMillis;
|
||||
int a = in.available();
|
||||
|
||||
// if by any chance there is no response from pigpiod, then wait up to
|
||||
// specified timeout
|
||||
while (w > 0 && a < 16) {
|
||||
w -= 10;
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
a = in.available();
|
||||
}
|
||||
|
||||
// throw exception if response from pigpiod has not arrived yet
|
||||
if (in.available() < 16) {
|
||||
throw new IOException(
|
||||
"Timeout: No response from pigpio daemon within " + replyTimeoutMillis + " ms.");
|
||||
}
|
||||
|
||||
int resp = Integer.reverseBytes(in.readInt()); // ignore response
|
||||
resp = Integer.reverseBytes(in.readInt()); // ignore response
|
||||
resp = Integer.reverseBytes(in.readInt()); // ignore response
|
||||
resp = Integer.reverseBytes(in.readInt()); // contains error or response
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all remaining bytes coming from pigpiod
|
||||
*
|
||||
* @param data Array to store read bytes.
|
||||
* @throws IOException if unable to read from network
|
||||
*/
|
||||
public void readBytes(byte[] data) throws IOException {
|
||||
in.readFully(data);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,14 +19,14 @@ package org.photonvision.common.hardware;
|
||||
|
||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.ProgramStatus;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
import org.photonvision.common.configuration.HardwareSettings;
|
||||
import org.photonvision.common.dataflow.networktables.NTDataChangeListener;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.hardware.GPIO.CustomGPIO;
|
||||
import org.photonvision.common.hardware.GPIO.GPIOBase;
|
||||
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
|
||||
import org.photonvision.common.hardware.VisionLED.VisionLEDMode;
|
||||
import org.photonvision.common.hardware.metrics.MetricsBase;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
@@ -36,45 +36,101 @@ import org.photonvision.common.util.ShellExec;
|
||||
public class HardwareManager {
|
||||
private static HardwareManager instance;
|
||||
|
||||
private final HashMap<Integer, GPIOBase> VisionLEDs = new HashMap<>();
|
||||
private final ShellExec shellExec = new ShellExec(true, false);
|
||||
private final Logger logger = new Logger(HardwareManager.class, LogGroup.General);
|
||||
|
||||
private final HardwareConfig hardwareConfig;
|
||||
private final HardwareSettings hardwareSettings;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final StatusLED statusLED;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final NetworkTableEntry ledModeEntry;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final NTDataChangeListener ledModeListener;
|
||||
|
||||
public final VisionLED visionLED;
|
||||
public final VisionLED visionLED; // May be null if no LED is specified
|
||||
|
||||
private final PigpioSocket pigpioSocket; // will be null unless on Raspi
|
||||
|
||||
public static HardwareManager getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new HardwareManager(ConfigManager.getInstance().getConfig().getHardwareConfig());
|
||||
var conf = ConfigManager.getInstance().getConfig();
|
||||
instance = new HardwareManager(conf.getHardwareConfig(), conf.getHardwareSettings());
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private HardwareManager(HardwareConfig hardwareConfig) {
|
||||
private HardwareManager(HardwareConfig hardwareConfig, HardwareSettings hardwareSettings) {
|
||||
this.hardwareConfig = hardwareConfig;
|
||||
this.hardwareSettings = hardwareSettings;
|
||||
CustomGPIO.setConfig(hardwareConfig);
|
||||
MetricsBase.setConfig(hardwareConfig);
|
||||
|
||||
statusLED = new StatusLED(hardwareConfig.statusRGBPins);
|
||||
if (Platform.isRaspberryPi()) {
|
||||
pigpioSocket = new PigpioSocket();
|
||||
} else {
|
||||
pigpioSocket = null;
|
||||
}
|
||||
|
||||
statusLED =
|
||||
hardwareConfig.statusRGBPins.size() == 3
|
||||
? new StatusLED(hardwareConfig.statusRGBPins)
|
||||
: null;
|
||||
|
||||
var hasBrightnessRange = hardwareConfig.ledBrightnessRange.size() == 2;
|
||||
visionLED =
|
||||
new VisionLED(
|
||||
hardwareConfig.ledPins,
|
||||
hardwareConfig.ledPWMFrequency,
|
||||
hardwareConfig.ledPWMRange.get(1));
|
||||
hardwareConfig.ledPins.isEmpty()
|
||||
? null
|
||||
: new VisionLED(
|
||||
hardwareConfig.ledPins,
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(0) : 0,
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
|
||||
pigpioSocket);
|
||||
|
||||
ledModeEntry = NetworkTablesManager.getInstance().kRootTable.getEntry("ledMode");
|
||||
ledModeEntry.setNumber(VisionLEDMode.VLM_DEFAULT.value);
|
||||
ledModeListener = new NTDataChangeListener(ledModeEntry, visionLED::onLedModeChange);
|
||||
ledModeListener =
|
||||
visionLED == null
|
||||
? null
|
||||
: new NTDataChangeListener(ledModeEntry, visionLED::onLedModeChange);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::onJvmExit));
|
||||
|
||||
if (visionLED != null) {
|
||||
visionLED.setBrightness(hardwareSettings.ledBrightnessPercentage);
|
||||
visionLED.blink(85, 4); // bootup blink
|
||||
}
|
||||
|
||||
// Start hardware metrics thread (Disabled until implemented)
|
||||
// if (Platform.isLinux()) MetricsPublisher.getInstance().startTask();
|
||||
}
|
||||
|
||||
public void setBrightnessPercent(int percent) {
|
||||
if (percent != hardwareSettings.ledBrightnessPercentage) {
|
||||
hardwareSettings.ledBrightnessPercentage = percent;
|
||||
if (visionLED != null) visionLED.setBrightness(percent);
|
||||
ConfigManager.getInstance().requestSave();
|
||||
logger.info("Setting led brightness to " + percent + "%");
|
||||
}
|
||||
}
|
||||
|
||||
private void onJvmExit() {
|
||||
logger.info("Shutting down LEDs...");
|
||||
if (visionLED != null) visionLED.setState(false);
|
||||
}
|
||||
|
||||
public boolean restartDevice() {
|
||||
if (Platform.isRaspberryPi()) {
|
||||
try {
|
||||
return shellExec.executeBashCommand("reboot now") == 0;
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not restart device!", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return shellExec.executeBashCommand(hardwareConfig.restartHardwareCommand) == 0;
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -88,7 +88,7 @@ public enum Platform {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Platform getCurrentPlatform() {
|
||||
public static Platform getCurrentPlatform() {
|
||||
if (RuntimeDetector.isWindows()) {
|
||||
if (RuntimeDetector.is32BitIntel()) return WINDOWS_32;
|
||||
if (RuntimeDetector.is64BitIntel()) return WINDOWS_64;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,7 +20,7 @@ package org.photonvision.common.hardware;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.hardware.GPIO.CustomGPIO;
|
||||
import org.photonvision.common.hardware.GPIO.GPIOBase;
|
||||
import org.photonvision.common.hardware.GPIO.PiGPIO;
|
||||
import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
|
||||
|
||||
public class StatusLED {
|
||||
public final GPIOBase redLED;
|
||||
@@ -36,9 +36,9 @@ public class StatusLED {
|
||||
}
|
||||
|
||||
if (Platform.isRaspberryPi()) {
|
||||
redLED = new PiGPIO(statusLedPins.get(0));
|
||||
greenLED = new PiGPIO(statusLedPins.get(1));
|
||||
blueLED = new PiGPIO(statusLedPins.get(2));
|
||||
redLED = new PigpioPin(statusLedPins.get(0));
|
||||
greenLED = new PigpioPin(statusLedPins.get(1));
|
||||
blueLED = new PigpioPin(statusLedPins.get(2));
|
||||
} else {
|
||||
redLED = new CustomGPIO(statusLedPins.get(0));
|
||||
greenLED = new CustomGPIO(statusLedPins.get(1));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,25 +23,40 @@ import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import org.photonvision.common.hardware.GPIO.CustomGPIO;
|
||||
import org.photonvision.common.hardware.GPIO.GPIOBase;
|
||||
import org.photonvision.common.hardware.GPIO.PiGPIO;
|
||||
import org.photonvision.common.hardware.GPIO.pi.PigpioException;
|
||||
import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
|
||||
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
|
||||
public class VisionLED {
|
||||
private static final Logger logger = new Logger(VisionLED.class, LogGroup.VisionModule);
|
||||
|
||||
public final List<GPIOBase> leds = new ArrayList<>();
|
||||
private final int[] ledPins;
|
||||
private final List<GPIOBase> visionLEDs = new ArrayList<>();
|
||||
private final int brightnessMin;
|
||||
private final int brightnessMax;
|
||||
private final PigpioSocket pigpioSocket;
|
||||
|
||||
private VisionLEDMode currentLedMode = VisionLEDMode.VLM_DEFAULT;
|
||||
private BooleanSupplier pipelineModeSupplier;
|
||||
|
||||
public VisionLED(List<Integer> ledPins, int pwmFreq, int pwmRangeMax) {
|
||||
private int mappedBrightnessPercentage;
|
||||
|
||||
public VisionLED(
|
||||
List<Integer> ledPins, int brightnessMin, int brightnessMax, PigpioSocket pigpioSocket) {
|
||||
this.brightnessMin = brightnessMin;
|
||||
this.brightnessMax = brightnessMax;
|
||||
this.pigpioSocket = pigpioSocket;
|
||||
this.ledPins = ledPins.stream().mapToInt(i -> i).toArray();
|
||||
ledPins.forEach(
|
||||
pin -> {
|
||||
if (Platform.isRaspberryPi()) {
|
||||
leds.add(new PiGPIO(pin, pwmFreq, pwmRangeMax));
|
||||
visionLEDs.add(new PigpioPin(pin));
|
||||
} else {
|
||||
leds.add(new CustomGPIO(pin));
|
||||
visionLEDs.add(new CustomGPIO(pin));
|
||||
}
|
||||
});
|
||||
pipelineModeSupplier = () -> false;
|
||||
@@ -52,15 +67,47 @@ public class VisionLED {
|
||||
}
|
||||
|
||||
public void setBrightness(int percentage) {
|
||||
leds.forEach((led) -> led.setBrightness(percentage));
|
||||
mappedBrightnessPercentage = MathUtils.map(percentage, 0, 100, brightnessMin, brightnessMax);
|
||||
setInternal(currentLedMode, false);
|
||||
}
|
||||
|
||||
public void blink(int pulseLengthMillis, int blinkCount) {
|
||||
blinkImpl(pulseLengthMillis, blinkCount);
|
||||
int blinkDuration = pulseLengthMillis * blinkCount * 2;
|
||||
TimedTaskManager.getInstance()
|
||||
.addOneShotTask(() -> setInternal(this.currentLedMode, false), blinkDuration + 150);
|
||||
}
|
||||
|
||||
private void blinkImpl(int pulseLengthMillis, int blinkCount) {
|
||||
leds.forEach((led) -> led.blink(pulseLengthMillis, blinkCount));
|
||||
if (Platform.isRaspberryPi()) {
|
||||
try {
|
||||
setStateImpl(false); // hack to ensure hardware PWM has stopped before trying to blink
|
||||
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Failed to blink!", e);
|
||||
}
|
||||
} else {
|
||||
for (GPIOBase led : visionLEDs) {
|
||||
led.blink(pulseLengthMillis, blinkCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStateImpl(boolean state) {
|
||||
leds.forEach((led) -> led.setState(state));
|
||||
if (Platform.isRaspberryPi()) {
|
||||
try {
|
||||
// stop any active blink
|
||||
pigpioSocket.waveTxStop();
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Failed to stop blink!", e);
|
||||
}
|
||||
}
|
||||
// if the user has set an LED brightness other than 100%, use that instead
|
||||
if (mappedBrightnessPercentage == 100 || !state) {
|
||||
visionLEDs.forEach((led) -> led.setState(state));
|
||||
} else {
|
||||
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
|
||||
}
|
||||
}
|
||||
|
||||
public void setState(boolean on) {
|
||||
@@ -108,7 +155,7 @@ public class VisionLED {
|
||||
setStateImpl(true);
|
||||
break;
|
||||
case VLM_BLINK:
|
||||
blinkImpl(175, -1);
|
||||
blinkImpl(85, -1);
|
||||
break;
|
||||
}
|
||||
currentLedMode = newLedMode;
|
||||
@@ -121,6 +168,9 @@ public class VisionLED {
|
||||
} else {
|
||||
if (currentLedMode == VisionLEDMode.VLM_DEFAULT) {
|
||||
switch (newLedMode) {
|
||||
case VLM_DEFAULT:
|
||||
setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
break;
|
||||
case VLM_OFF:
|
||||
setStateImpl(false);
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,17 +19,23 @@ package org.photonvision.common.hardware.metrics;
|
||||
|
||||
public class CPUMetrics extends MetricsBase {
|
||||
|
||||
public CPUMetrics() {}
|
||||
private String cpuMemSplit = null;
|
||||
|
||||
public String getMemory() {
|
||||
if (cpuMemoryCommand.isEmpty()) return "";
|
||||
return execute(cpuMemoryCommand);
|
||||
if (cpuMemSplit == null) {
|
||||
cpuMemSplit = execute(cpuMemoryCommand);
|
||||
}
|
||||
return cpuMemSplit;
|
||||
}
|
||||
|
||||
// TODO: Command should return in Celsius
|
||||
public String getTemp() {
|
||||
if (cpuTemperatureCommand.isEmpty()) return "";
|
||||
return execute(cpuTemperatureCommand);
|
||||
try {
|
||||
return execute(cpuTemperatureCommand);
|
||||
} catch (Exception e) {
|
||||
return "N/A";
|
||||
}
|
||||
}
|
||||
|
||||
public String getUtilization() {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
public class DiskMetrics extends MetricsBase {
|
||||
public String getUsedDiskPct() {
|
||||
if (diskUsageCommand.isEmpty()) return "";
|
||||
return execute(diskUsageCommand);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -18,11 +18,16 @@
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
public class GPUMetrics extends MetricsBase {
|
||||
public String getMemory() {
|
||||
return execute(gpuMemoryCommand);
|
||||
private String gpuMemSplit = null;
|
||||
|
||||
public String getGPUMemorySplit() {
|
||||
if (gpuMemSplit == null) {
|
||||
gpuMemSplit = execute(gpuMemoryCommand);
|
||||
}
|
||||
return gpuMemSplit;
|
||||
}
|
||||
|
||||
public String getTemp() {
|
||||
return execute(gpuTemperatureCommand);
|
||||
public String getMallocedMemory() {
|
||||
return execute(gpuMemUsageCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
@@ -26,18 +28,21 @@ import org.photonvision.common.util.ShellExec;
|
||||
public abstract class MetricsBase {
|
||||
private static final Logger logger = new Logger(MetricsBase.class, LogGroup.General);
|
||||
// CPU
|
||||
public static String cpuMemoryCommand = "sudo vcgencmd get_mem arm | grep -Eo '[0-9]+'";
|
||||
public static String cpuMemoryCommand = "vcgencmd get_mem arm | grep -Eo '[0-9]+'";
|
||||
public static String cpuTemperatureCommand =
|
||||
"sudo cat /sys/class/thermal/thermal_zone0/temp | grep -x -E '[0-9]+'";
|
||||
"sed 's/.\\{3\\}$/.&/' <<< cat /sys/class/thermal/thermal_zone0/temp";
|
||||
public static String cpuUtilizationCommand =
|
||||
"sudo top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
|
||||
"top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
|
||||
|
||||
// GPU
|
||||
public static String gpuMemoryCommand = "sudo vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
|
||||
public static String gpuTemperatureCommand = "sudo vcgencmd measure_temp | sed 's/[^0-9]*//g'\n";
|
||||
public static String gpuMemoryCommand = "vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
|
||||
public static String gpuMemUsageCommand = "vcgencmd get_mem malloc | grep -Eo '[0-9]+'";
|
||||
|
||||
// RAM
|
||||
public static String ramUsageCommand = "sudo free | awk -v i=2 -v j=3 'FNR == i {print $j}'";
|
||||
public static String ramUsageCommand = "free --mega | awk -v i=2 -v j=3 'FNR == i {print $j}'";
|
||||
|
||||
// Disk
|
||||
public static String diskUsageCommand = "df ./ --output=pcent | tail -n +2";
|
||||
|
||||
private static ShellExec runCommand = new ShellExec(true, true);
|
||||
|
||||
@@ -48,16 +53,22 @@ public abstract class MetricsBase {
|
||||
cpuUtilizationCommand = config.cpuUtilCommand;
|
||||
|
||||
gpuMemoryCommand = config.gpuMemoryCommand;
|
||||
gpuTemperatureCommand = config.gpuTempCommand;
|
||||
gpuMemUsageCommand = config.gpuMemUsageCommand;
|
||||
|
||||
diskUsageCommand = config.diskUsageCommand;
|
||||
|
||||
ramUsageCommand = config.ramUtilCommand;
|
||||
}
|
||||
|
||||
public static String execute(String command) {
|
||||
public static synchronized String execute(String command) {
|
||||
try {
|
||||
runCommand.executeBashCommand(command);
|
||||
return runCommand.getOutput();
|
||||
} catch (Exception e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
|
||||
logger.error(
|
||||
"Command: \""
|
||||
+ command
|
||||
@@ -71,7 +82,10 @@ public abstract class MetricsBase {
|
||||
+ "\nError completed: "
|
||||
+ runCommand.isErrorCompleted()
|
||||
+ "\nExit code: "
|
||||
+ runCommand.getExitCode());
|
||||
+ runCommand.getExitCode()
|
||||
+ "\n Exception: "
|
||||
+ e.toString()
|
||||
+ sw.toString());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,19 +17,20 @@
|
||||
|
||||
package org.photonvision.common.hardware.metrics;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.server.SocketHandler;
|
||||
|
||||
public class MetricsPublisher {
|
||||
private final HashMap<String, String> metrics;
|
||||
private static final Logger logger = new Logger(MetricsPublisher.class, LogGroup.General);
|
||||
private static CPUMetrics cpuMetrics;
|
||||
private static GPUMetrics gpuMetrics;
|
||||
private static RAMMetrics ramMetrics;
|
||||
private static DiskMetrics diskMetrics;
|
||||
|
||||
public static MetricsPublisher getInstance() {
|
||||
return Singleton.INSTANCE;
|
||||
@@ -39,26 +40,7 @@ public class MetricsPublisher {
|
||||
cpuMetrics = new CPUMetrics();
|
||||
gpuMetrics = new GPUMetrics();
|
||||
ramMetrics = new RAMMetrics();
|
||||
|
||||
metrics = new HashMap<>();
|
||||
}
|
||||
|
||||
public void startTask() {
|
||||
TimedTaskManager.getInstance()
|
||||
.addTask(
|
||||
"Metrics",
|
||||
() -> {
|
||||
metrics.put("cpuTemp", cpuMetrics.getTemp());
|
||||
metrics.put("cpuUtil", cpuMetrics.getUtilization());
|
||||
metrics.put("cpuMem", cpuMetrics.getMemory());
|
||||
metrics.put("gpuTemp", gpuMetrics.getTemp());
|
||||
metrics.put("gpuMem", gpuMetrics.getMemory());
|
||||
metrics.put("ramUtil", ramMetrics.getUsedRam());
|
||||
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(new OutgoingUIEvent<>("metrics", metrics));
|
||||
},
|
||||
1000);
|
||||
diskMetrics = new DiskMetrics();
|
||||
}
|
||||
|
||||
public void stopTask() {
|
||||
@@ -66,6 +48,33 @@ public class MetricsPublisher {
|
||||
logger.info("This device does not support running bash commands. Stopped metrics thread.");
|
||||
}
|
||||
|
||||
public void publish() {
|
||||
if (!Platform.isRaspberryPi()) {
|
||||
logger.debug("Ignoring metrics on non-Pi devices");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Publishing Metrics...");
|
||||
final var metrics = new HashMap<String, String>();
|
||||
|
||||
metrics.put("cpuTemp", cpuMetrics.getTemp());
|
||||
metrics.put("cpuUtil", cpuMetrics.getUtilization());
|
||||
metrics.put("cpuMem", cpuMetrics.getMemory());
|
||||
metrics.put("gpuMem", gpuMetrics.getGPUMemorySplit());
|
||||
metrics.put("ramUtil", ramMetrics.getUsedRam());
|
||||
metrics.put("gpuMemUtil", gpuMetrics.getMallocedMemory());
|
||||
metrics.put("diskUtilPct", diskMetrics.getUsedDiskPct());
|
||||
|
||||
var retMap = new HashMap<String, Object>();
|
||||
retMap.put("metrics", metrics);
|
||||
|
||||
try {
|
||||
SocketHandler.getInstance().broadcastMessage(retMap, null);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("Exception while sending metrics!", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Singleton {
|
||||
public static final MetricsPublisher INSTANCE = new MetricsPublisher();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,10 +19,13 @@ package org.photonvision.common.logging;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
@@ -45,6 +48,8 @@ public class Logger {
|
||||
public static final String ANSI_CYAN = "\u001B[36m";
|
||||
public static final String ANSI_WHITE = "\u001B[37m";
|
||||
|
||||
public static final int MAX_LOGS_TO_KEEP = 100;
|
||||
|
||||
private static final SimpleDateFormat simpleDateFormat =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@@ -105,6 +110,7 @@ public class Logger {
|
||||
currentAppenders.add(new ConsoleLogAppender());
|
||||
currentAppenders.add(uiLogAppender);
|
||||
addFileAppender(ConfigManager.getInstance().getLogPath());
|
||||
cleanLogs(ConfigManager.getInstance().getLogsDir());
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
@@ -121,6 +127,48 @@ public class Logger {
|
||||
currentAppenders.add(new FileLogAppender(logFilePath));
|
||||
}
|
||||
|
||||
public static void cleanLogs(Path folderToClean) {
|
||||
|
||||
LinkedList<File> logFileList =
|
||||
new LinkedList<>(Arrays.asList(folderToClean.toFile().listFiles()));
|
||||
HashMap<File, Date> logFileStartDateMap = new HashMap<>();
|
||||
|
||||
// Remove any files from the list for which we can't parse a start date from their name.
|
||||
// Simultaneously populate our HashMap with Date objects repeseting the file-name
|
||||
// indicated log start time.
|
||||
logFileList.removeIf(
|
||||
(File arg0) -> {
|
||||
try {
|
||||
logFileStartDateMap.put(
|
||||
arg0, ConfigManager.getInstance().logFnameToDate(arg0.getName()));
|
||||
return false;
|
||||
} catch (ParseException e) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Execute a sort on the log file list by date in the filename.
|
||||
logFileList.sort(
|
||||
(File arg0, File arg1) -> {
|
||||
Date date0 = logFileStartDateMap.get(arg0);
|
||||
Date date1 = logFileStartDateMap.get(arg1);
|
||||
return date1.compareTo(date0);
|
||||
});
|
||||
|
||||
int logCounter = 0;
|
||||
for (File file : logFileList) {
|
||||
// Due to filtering above, everything in logFileList should be a log file
|
||||
if (logCounter < MAX_LOGS_TO_KEEP) {
|
||||
// Skip over the first MAX_LOGS_TO_KEEP files
|
||||
logCounter++;
|
||||
continue;
|
||||
} else {
|
||||
// Delete this file.
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void setLevel(LogGroup group, LogLevel newLevel) {
|
||||
levelMap.put(group, newLevel);
|
||||
}
|
||||
@@ -132,15 +180,21 @@ public class Logger {
|
||||
var formattedMessage = format(message, level, group, clazz, shouldColor);
|
||||
a.log(formattedMessage, level);
|
||||
}
|
||||
if (!connected) uiBacklog.add(Pair.of(format(message, level, group, clazz, false), level));
|
||||
if (!connected) {
|
||||
synchronized (uiBacklog) {
|
||||
uiBacklog.add(Pair.of(format(message, level, group, clazz, false), level));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendConnectedBacklog() {
|
||||
for (var message : uiBacklog) {
|
||||
uiLogAppender.log(message.getLeft(), message.getRight());
|
||||
}
|
||||
connected = true;
|
||||
uiBacklog.clear();
|
||||
synchronized (uiBacklog) {
|
||||
for (var message : uiBacklog) {
|
||||
uiLogAppender.log(message.getLeft(), message.getRight());
|
||||
}
|
||||
uiBacklog.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldLog(LogLevel logLevel) {
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* 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.networking;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class LinuxNetworking extends SysNetworking {
|
||||
private static final String PATH = "/etc/dhcpcd.conf";
|
||||
|
||||
private Logger logger = new Logger(LinuxNetworking.class, LogGroup.General);
|
||||
|
||||
@Override
|
||||
public boolean setDHCP() {
|
||||
File dhcpConf = new File(PATH);
|
||||
logger.debug("Removing static IP from " + PATH);
|
||||
if (dhcpConf.exists()) {
|
||||
try {
|
||||
List<String> lines = FileUtils.readLines(dhcpConf, StandardCharsets.UTF_8);
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String line = lines.get(i);
|
||||
if (line.startsWith("interface " + networkInterface.name)) {
|
||||
lines.remove(i);
|
||||
for (int j = i; j < lines.size(); j++) {
|
||||
String subInterface = lines.get(j);
|
||||
if (subInterface.contains("static ip_address")
|
||||
|| subInterface.contains("static routers")) {
|
||||
lines.remove(j);
|
||||
j--;
|
||||
}
|
||||
if (subInterface.contains("interface")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
FileUtils.writeLines(dhcpConf, lines);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to set DHCP!", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.error("dhcpcd5 is not installed, unable to set IP.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setHostname(String newHostname) {
|
||||
try {
|
||||
var setHostnameRetCode = shell.execute("hostnamectl", "set-hostname", newHostname);
|
||||
return setHostnameRetCode == 0;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to set hostname!", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setStatic(String ipAddress, String netmask) {
|
||||
setDHCP(); // clean up old static interface
|
||||
File dhcpConf = new File(PATH);
|
||||
try {
|
||||
List<String> lines = FileUtils.readLines(dhcpConf, StandardCharsets.UTF_8);
|
||||
lines.add("interface " + networkInterface.name);
|
||||
InetAddress iNetMask = InetAddress.getByName(netmask);
|
||||
int prefix = convertNetmaskToCIDR(iNetMask);
|
||||
lines.add("static ip_address=" + ipAddress + "/" + prefix);
|
||||
FileUtils.writeLines(dhcpConf, lines);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to set Static IP!", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<java.net.NetworkInterface> getNetworkInterfaces() throws SocketException {
|
||||
List<java.net.NetworkInterface> netInterfaces;
|
||||
try {
|
||||
netInterfaces = Collections.list(java.net.NetworkInterface.getNetworkInterfaces());
|
||||
} catch (SocketException e) {
|
||||
return null;
|
||||
}
|
||||
List<java.net.NetworkInterface> goodInterfaces = new ArrayList<>();
|
||||
|
||||
for (var netInterface : netInterfaces) {
|
||||
if (netInterface.getDisplayName().contains("lo")) continue;
|
||||
if (!netInterface.isUp()) continue;
|
||||
goodInterfaces.add(netInterface);
|
||||
}
|
||||
return goodInterfaces;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -46,20 +46,74 @@ public class NetworkManager {
|
||||
}
|
||||
|
||||
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
|
||||
logger.info("Setting " + config.connectionType + " with team team " + config.teamNumber);
|
||||
if (Platform.isLinux()) {
|
||||
if (!Platform.isRoot) {
|
||||
logger.error("Cannot manage network without root!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.connectionType == NetworkMode.DHCP) {
|
||||
return; // TODO do we need to reconnect or something?
|
||||
} else if (config.connectionType == NetworkMode.STATIC) {
|
||||
// always set hostname
|
||||
if (config.hostname.length() > 0) {
|
||||
try {
|
||||
new ShellExec()
|
||||
.executeBashCommand("ip addr add " + config.staticIp + "/24" + " dev eth0");
|
||||
var shell = new ShellExec(true, false);
|
||||
shell.executeBashCommand("cat /etc/hostname | tr -d \" \\t\\n\\r\"");
|
||||
var oldHostname = shell.getOutput().replace("\n", "");
|
||||
|
||||
var setHostnameRetCode =
|
||||
shell.executeBashCommand(
|
||||
"echo $NEW_HOSTNAME > /etc/hostname".replace("$NEW_HOSTNAME", config.hostname));
|
||||
setHostnameRetCode =
|
||||
shell.executeBashCommand("hostnamectl set-hostname " + config.hostname);
|
||||
|
||||
// Add to /etc/hosts
|
||||
var addHostRetCode =
|
||||
shell.executeBashCommand(
|
||||
String.format(
|
||||
"sed -i \"s/127.0.1.1.*%s/127.0.1.1\\t%s/g\" /etc/hosts",
|
||||
oldHostname, config.hostname));
|
||||
|
||||
shell.executeBashCommand("sudo service avahi-daemon restart");
|
||||
|
||||
var success = setHostnameRetCode == 0 && addHostRetCode == 0;
|
||||
if (!success) {
|
||||
logger.error(
|
||||
"Setting hostname returned non-zero codes (hostname/hosts) "
|
||||
+ setHostnameRetCode
|
||||
+ "|"
|
||||
+ addHostRetCode
|
||||
+ "!");
|
||||
} else {
|
||||
logger.info("Set hostname to " + config.hostname);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Failed to set hostname!", e);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.warn("Got empty hostname?");
|
||||
}
|
||||
|
||||
if (config.connectionType == NetworkMode.DHCP) {
|
||||
var shell = new ShellExec();
|
||||
try {
|
||||
if (!config.staticIp.equals("")) {
|
||||
shell.executeBashCommand("ip addr del " + config.staticIp + "/8 dev eth0");
|
||||
}
|
||||
shell.executeBashCommand("dhclient eth0", false);
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while setting DHCP!");
|
||||
}
|
||||
} else if (config.connectionType == NetworkMode.STATIC) {
|
||||
var shell = new ShellExec();
|
||||
if (config.staticIp.length() > 0) {
|
||||
try {
|
||||
shell.executeBashCommand("ip addr add " + config.staticIp + "/8" + " dev eth0");
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while setting static IP!", e);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Got empty static IP?");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -68,6 +122,6 @@ public class NetworkManager {
|
||||
}
|
||||
|
||||
public void reinitialize() {
|
||||
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage);
|
||||
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* 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.networking;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.SocketException;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
|
||||
public abstract class SysNetworking {
|
||||
NetworkInterface networkInterface;
|
||||
ShellExec shell = new ShellExec(true, true);
|
||||
|
||||
private String hostname = getHostname();
|
||||
|
||||
public String getHostname() {
|
||||
if (hostname == null) {
|
||||
try {
|
||||
var retCode = shell.execute("hostname", null, true);
|
||||
if (retCode == 0) {
|
||||
while (!shell.isOutputCompleted()) {}
|
||||
return shell.getOutput();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
} else return hostname;
|
||||
}
|
||||
|
||||
// code belongs to
|
||||
// https://stackoverflow.com/questions/19531411/calculate-cidr-from-a-given-netmask-java
|
||||
public static int convertNetmaskToCIDR(InetAddress netmask) {
|
||||
|
||||
byte[] netmaskBytes = netmask.getAddress();
|
||||
int cidr = 0;
|
||||
boolean zero = false;
|
||||
for (byte b : netmaskBytes) {
|
||||
int mask = 0x80;
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int result = b & mask;
|
||||
if (result == 0) {
|
||||
zero = true;
|
||||
} else if (zero) {
|
||||
throw new IllegalArgumentException("Invalid netmask.");
|
||||
} else {
|
||||
cidr++;
|
||||
}
|
||||
mask >>>= 1;
|
||||
}
|
||||
}
|
||||
return cidr;
|
||||
}
|
||||
|
||||
public void setNetworkInterface(NetworkInterface networkInterface) {
|
||||
this.networkInterface = networkInterface;
|
||||
}
|
||||
|
||||
public abstract boolean setDHCP();
|
||||
|
||||
public abstract boolean setHostname(String hostname);
|
||||
|
||||
public abstract boolean setStatic(String ipAddress, String netmask);
|
||||
|
||||
public abstract List<java.net.NetworkInterface> getNetworkInterfaces() throws SocketException;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -28,6 +28,7 @@ public final class SerializationUtils {
|
||||
var ret = new HashMap<String, Object>();
|
||||
for (var field : src.getClass().getFields()) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
if (!field
|
||||
.getType()
|
||||
.isEnum()) { // if the field is not an enum, get it based on the current pipeline
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -18,10 +18,14 @@
|
||||
package org.photonvision.common.util;
|
||||
|
||||
import java.io.*;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
/** Execute external process and optionally read output buffer. */
|
||||
@SuppressWarnings({"unused", "ConstantConditions"})
|
||||
public class ShellExec {
|
||||
private static final Logger logger = new Logger(ShellExec.class, LogGroup.General);
|
||||
|
||||
private int exitCode;
|
||||
private boolean readOutput, readError;
|
||||
private StreamGobbler errorGobbler, outputGobbler;
|
||||
@@ -35,6 +39,10 @@ public class ShellExec {
|
||||
this.readError = readError;
|
||||
}
|
||||
|
||||
public int executeBashCommand(String command) throws IOException {
|
||||
return executeBashCommand(command, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a bash command. We can handle complex bash commands including multiple executions (; |
|
||||
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
|
||||
@@ -42,8 +50,9 @@ public class ShellExec {
|
||||
* @param command Bash command to execute
|
||||
* @return true if bash got started, but your command may have failed.
|
||||
*/
|
||||
public int executeBashCommand(String command) throws IOException {
|
||||
boolean wait = true;
|
||||
public int executeBashCommand(String command, boolean wait) throws IOException {
|
||||
logger.debug("Executing \"" + command + "\"");
|
||||
|
||||
boolean success = false;
|
||||
Runtime r = Runtime.getRuntime();
|
||||
// Use bash -c so we can handle things like multi commands separated by ; and
|
||||
@@ -57,7 +66,9 @@ public class ShellExec {
|
||||
|
||||
// Consume streams, older jvm's had a memory leak if streams were not read,
|
||||
// some other jvm+OS combinations may block unless streams are consumed.
|
||||
return doProcess(wait, process);
|
||||
int retcode = doProcess(wait, process);
|
||||
logger.debug("Got exit code " + retcode);
|
||||
return retcode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -182,6 +182,10 @@ public class TestUtils {
|
||||
return getResourcesFolderPath(false).resolve("calibrationBoardImages");
|
||||
}
|
||||
|
||||
public static Path getSquaresBoardImagesPath() {
|
||||
return getResourcesFolderPath(false).resolve("calibrationSquaresImg");
|
||||
}
|
||||
|
||||
public static File getHardwareConfigJson() {
|
||||
return getResourcesFolderPath(false)
|
||||
.resolve("hardware")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -58,6 +58,22 @@ public class FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteFile(Path path) {
|
||||
try {
|
||||
Files.delete(path);
|
||||
} catch (IOException e) {
|
||||
logger.error("Exception deleting file " + path + "!", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyFile(Path src, Path dst) {
|
||||
try {
|
||||
Files.copy(src, dst);
|
||||
} catch (IOException e) {
|
||||
logger.error("Exception copying file " + src + " to " + dst + "!", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setFilePerms(Path path) throws IOException {
|
||||
if (!Platform.CurrentPlatform.isWindows()) {
|
||||
File thisFile = path.toFile();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.photonvision.common.util.file;
|
||||
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
@@ -53,6 +54,7 @@ public class JacksonUtils {
|
||||
ObjectMapper objectMapper =
|
||||
JsonMapper.builder()
|
||||
.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
|
||||
.build();
|
||||
File jsonFile = new File(path.toString());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,8 +15,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
package org.photonvision.common.util.java;
|
||||
|
||||
public enum CameraQuirks {
|
||||
Gain
|
||||
public interface TriConsumer<T, U, V> {
|
||||
void accept(T t, U u, V v);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,23 +22,18 @@ import org.apache.commons.math3.util.FastMath;
|
||||
public class MathUtils {
|
||||
MathUtils() {}
|
||||
|
||||
public static double sigmoid(Number x) {
|
||||
double bias = 0;
|
||||
double a = 5;
|
||||
double b = -0.05;
|
||||
double k = 200;
|
||||
|
||||
if (x.doubleValue() < 50) {
|
||||
bias = -1.338;
|
||||
}
|
||||
|
||||
return ((k / (1 + Math.pow(Math.E, (a + (b * x.doubleValue()))))) + bias);
|
||||
}
|
||||
|
||||
public static double toSlope(Number angle) {
|
||||
return FastMath.atan(FastMath.toRadians(angle.doubleValue() - 90));
|
||||
}
|
||||
|
||||
public static int safeDivide(int quotient, int divisor) {
|
||||
if (divisor == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return quotient / divisor;
|
||||
}
|
||||
}
|
||||
|
||||
public static double roundTo(double value, int to) {
|
||||
double toMult = Math.pow(10, to);
|
||||
return (double) Math.round(value * toMult) / toMult;
|
||||
@@ -48,6 +43,14 @@ public class MathUtils {
|
||||
return nanos / 1000000.0;
|
||||
}
|
||||
|
||||
public static long millisToNanos(long millis) {
|
||||
return millis * 1000000;
|
||||
}
|
||||
|
||||
public static long microsToNanos(long micros) {
|
||||
return micros * 1000;
|
||||
}
|
||||
|
||||
public static double map(
|
||||
double value, double in_min, double in_max, double out_min, double out_max) {
|
||||
return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
141
photon-server/src/main/java/org/photonvision/raspi/PicamJNI.java
Normal file
141
photon-server/src/main/java/org/photonvision/raspi/PicamJNI.java
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.raspi;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class PicamJNI {
|
||||
|
||||
private static boolean libraryLoaded = false;
|
||||
private static Logger logger = new Logger(PicamJNI.class, LogGroup.Camera);
|
||||
|
||||
public enum SensorModel {
|
||||
Disconnected,
|
||||
OV5647, // Picam v1
|
||||
IMX219, // Picam v2
|
||||
IMX477, // Picam HQ
|
||||
Unknown;
|
||||
|
||||
public String getFriendlyName() {
|
||||
switch (this) {
|
||||
case Disconnected:
|
||||
return "Disconnected Camera";
|
||||
case OV5647:
|
||||
return "Camera Module v1";
|
||||
case IMX219:
|
||||
return "Camera Module v2";
|
||||
case IMX477:
|
||||
return "HQ Camera";
|
||||
case Unknown:
|
||||
default:
|
||||
return "Unknown Camera";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void forceLoad() throws IOException {
|
||||
if (libraryLoaded || !Platform.isRaspberryPi()) return;
|
||||
|
||||
try {
|
||||
File libDirectory = Path.of("lib/").toFile();
|
||||
if (!libDirectory.exists()) {
|
||||
Files.createDirectory(libDirectory.toPath()).toFile();
|
||||
}
|
||||
|
||||
// We always extract the shared object (we could hash each so, but that's a lot of work)
|
||||
URL resourceURL = PicamJNI.class.getResource("/nativelibraries/libpicam.so");
|
||||
File libFile = Path.of("lib/libpicam.so").toFile();
|
||||
try (InputStream in = resourceURL.openStream()) {
|
||||
if (libFile.exists()) Files.delete(libFile.toPath());
|
||||
Files.copy(in, libFile.toPath());
|
||||
} catch (Exception e) {
|
||||
logger.error("Could not extract the native library!");
|
||||
}
|
||||
System.load(libFile.getAbsolutePath());
|
||||
|
||||
libraryLoaded = true;
|
||||
logger.info("Successfully loaded libpicam shared object");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
logger.error("Couldn't load libpicam shared object");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return libraryLoaded && getSensorModel() != SensorModel.Disconnected;
|
||||
}
|
||||
|
||||
public static SensorModel getSensorModel() {
|
||||
switch (getSensorModelRaw().toLowerCase()) {
|
||||
case "":
|
||||
return SensorModel.Disconnected;
|
||||
case "ov5647":
|
||||
return SensorModel.OV5647;
|
||||
case "imx219":
|
||||
return SensorModel.IMX219;
|
||||
case "imx477":
|
||||
return SensorModel.IMX477;
|
||||
default:
|
||||
return SensorModel.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
private static native String getSensorModelRaw();
|
||||
|
||||
// Everything here is static because multiple picams are unsupported at the hardware level
|
||||
|
||||
/**
|
||||
* Called once for each video mode change. Starts a native thread running MMAL that stays alive
|
||||
* until destroyCamera is called.
|
||||
*
|
||||
* @return true on error.
|
||||
*/
|
||||
public static native boolean createCamera(int width, int height, int fps);
|
||||
|
||||
/**
|
||||
* Destroys MMAL and EGL contexts. Called once for each video mode change *before* createCamera.
|
||||
*
|
||||
* @return true on error.
|
||||
*/
|
||||
public static native boolean destroyCamera();
|
||||
|
||||
public static native void setThresholds(
|
||||
double hL, double sL, double vL, double hU, double sU, double vU);
|
||||
|
||||
public static native boolean setExposure(int exposure);
|
||||
|
||||
public static native boolean setBrightness(int brightness);
|
||||
|
||||
public static native boolean setGain(int gain);
|
||||
|
||||
public static native boolean setRotation(int rotation);
|
||||
|
||||
public static native void setShouldCopyColor(boolean shouldCopyColor);
|
||||
|
||||
public static native long getFrameLatency();
|
||||
|
||||
public static native long grabFrame(boolean shouldReturnColor);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -33,9 +33,11 @@ import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.hardware.metrics.MetricsPublisher;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.target.TargetModel;
|
||||
|
||||
@@ -47,26 +49,58 @@ public class RequestHandler {
|
||||
public static void onSettingUpload(Context ctx) {
|
||||
var file = ctx.uploadedFile("zipData");
|
||||
if (file != null) {
|
||||
var tempZipPath =
|
||||
|
||||
// Copy the file from the client to a temporary location
|
||||
var tempFilePath =
|
||||
new File(Path.of(System.getProperty("java.io.tmpdir"), file.getFilename()).toString());
|
||||
tempZipPath.getParentFile().mkdirs();
|
||||
tempFilePath.getParentFile().mkdirs();
|
||||
try {
|
||||
FileUtils.copyInputStreamToFile(file.getContent(), tempZipPath);
|
||||
FileUtils.copyInputStreamToFile(file.getContent(), tempFilePath);
|
||||
} catch (IOException e) {
|
||||
logger.error("Exception uploading settings file!");
|
||||
logger.error("Exception while uploading settings file to temp folder!");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
ConfigManager.saveUploadedSettingsZip(tempZipPath);
|
||||
|
||||
// Process the file by its extension
|
||||
if (file.getExtension().contains("zip")) {
|
||||
// .zip files are assumed to be full packages of configuration files
|
||||
logger.debug("Processing uploaded settings zip " + file.getFilename());
|
||||
ConfigManager.saveUploadedSettingsZip(tempFilePath);
|
||||
|
||||
} else if (file.getFilename().equals(ConfigManager.HW_CFG_FNAME)) {
|
||||
// Filenames matching the hardware config .json file are assumed to be
|
||||
// hardware config .json's
|
||||
logger.debug("Processing uploaded hardware config " + file.getFilename());
|
||||
ConfigManager.getInstance().saveUploadedHardwareConfig(tempFilePath.toPath());
|
||||
|
||||
} else if (file.getFilename().equals(ConfigManager.HW_SET_FNAME)) {
|
||||
// Filenames matching the hardware settings .json file are assumed to be
|
||||
// hardware settings.json's
|
||||
logger.debug("Processing uploaded hardware settings" + file.getFilename());
|
||||
ConfigManager.getInstance().saveUploadedHardwareSettings(tempFilePath.toPath());
|
||||
|
||||
} else if (file.getFilename().equals(ConfigManager.NET_SET_FNAME)) {
|
||||
// Filenames matching the network config .json file are assumed to be
|
||||
// network config .json's
|
||||
logger.debug("Processing uploaded network config " + file.getFilename());
|
||||
ConfigManager.getInstance().saveUploadedNetworkConfig(tempFilePath.toPath());
|
||||
|
||||
} else {
|
||||
logger.error(
|
||||
"Couldn't apply provided settings file - did not recognize "
|
||||
+ file.getFilename()
|
||||
+ " as a supported file.");
|
||||
ctx.status(500);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.status(200);
|
||||
logger.info("Settings uploaded, going down for restart.");
|
||||
restartProgram(ctx);
|
||||
|
||||
if (!Platform.isRaspberryPi()) {
|
||||
logger.info("(On non-PI platforms, the program may not restart manually...)");
|
||||
}
|
||||
|
||||
System.exit(0);
|
||||
} else {
|
||||
logger.error("Couldn't read uploaded settings ZIP! Ignoring.");
|
||||
logger.error("Couldn't read uploaded file! Ignoring.");
|
||||
ctx.status(500);
|
||||
}
|
||||
}
|
||||
@@ -75,21 +109,13 @@ public class RequestHandler {
|
||||
public static void onGeneralSettings(Context context) throws JsonProcessingException {
|
||||
Map<String, Object> map =
|
||||
(Map<String, Object>) kObjectMapper.readValue(context.body(), Map.class);
|
||||
var networking =
|
||||
(Map<String, Object>)
|
||||
map.get("networkSettings"); // teamNumber (int), supported (bool), connectionType (int),
|
||||
// staticIp (str), netmask (str), gateway (str), hostname (str)
|
||||
var lighting =
|
||||
(Map<String, Object>) map.get("lighting"); // supported (true/false), brightness (int)
|
||||
// TODO do stuff with lighting
|
||||
|
||||
var networkConfig = NetworkConfig.fromHashMap(networking);
|
||||
var networkConfig = NetworkConfig.fromHashMap(map);
|
||||
ConfigManager.getInstance().setNetworkSettings(networkConfig);
|
||||
ConfigManager.getInstance().requestSave();
|
||||
NetworkManager.getInstance().reinitialize();
|
||||
NetworkTablesManager.getInstance().setConfig(networkConfig);
|
||||
|
||||
logger.info("Responding to general settings with http 200");
|
||||
context.status(200);
|
||||
}
|
||||
|
||||
@@ -136,6 +162,7 @@ public class RequestHandler {
|
||||
}
|
||||
|
||||
public static void onCalibrationEnd(Context ctx) {
|
||||
logger.info("Calibrating camera! This will take a long time...");
|
||||
var index = Integer.parseInt(ctx.body());
|
||||
var calData = VisionModuleManager.getInstance().getModule(index).endCalibration();
|
||||
if (calData == null) {
|
||||
@@ -145,6 +172,7 @@ public class RequestHandler {
|
||||
|
||||
ctx.result(String.valueOf(calData.standardDeviation));
|
||||
ctx.status(200);
|
||||
logger.info("Camera calibrated!");
|
||||
}
|
||||
|
||||
public static void restartDevice(Context ctx) {
|
||||
@@ -157,7 +185,31 @@ public class RequestHandler {
|
||||
*/
|
||||
public static void restartProgram(Context ctx) {
|
||||
ctx.status(200);
|
||||
System.exit(0);
|
||||
|
||||
if (Platform.isRaspberryPi()) {
|
||||
try {
|
||||
new ShellExec().executeBashCommand("systemctl restart photonvision.service");
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not restart device!", e);
|
||||
System.exit(0);
|
||||
}
|
||||
} else {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setCameraNickname(Context ctx) {
|
||||
try {
|
||||
var data = kObjectMapper.readValue(ctx.body(), HashMap.class);
|
||||
String name = String.valueOf(data.get("name"));
|
||||
int idx = Integer.parseInt(String.valueOf(data.get("cameraIndex")));
|
||||
VisionModuleManager.getInstance().getModule(idx).setCameraNickname(name);
|
||||
ctx.status(200);
|
||||
return;
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
ctx.status(500);
|
||||
}
|
||||
|
||||
public static void uploadPnpModel(Context ctx) {
|
||||
@@ -174,6 +226,11 @@ public class RequestHandler {
|
||||
ctx.status(200);
|
||||
}
|
||||
|
||||
public static void sendMetrics(Context ctx) {
|
||||
MetricsPublisher.getInstance().publish();
|
||||
ctx.status(200);
|
||||
}
|
||||
|
||||
public static class UITargetData {
|
||||
public int index;
|
||||
public TargetModel targetModel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -80,6 +80,8 @@ public class Server {
|
||||
app.post("/api/restartDevice", RequestHandler::restartDevice);
|
||||
app.post("api/restartProgram", RequestHandler::restartProgram);
|
||||
app.post("api/vision/pnpModel", RequestHandler::uploadPnpModel);
|
||||
app.post("api/sendMetrics", RequestHandler::sendMetrics);
|
||||
app.post("api/setCameraNickname", RequestHandler::setCameraNickname);
|
||||
|
||||
app.start(port);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -35,6 +35,7 @@ import org.msgpack.jackson.dataformat.MessagePackFactory;
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.pipeline.PipelineType;
|
||||
@@ -83,6 +84,19 @@ public class SocketHandler {
|
||||
var reason = context.reason() != null ? context.reason() : "Connection closed by client";
|
||||
logger.info("Closing websocket connection from " + host + " for reason: " + reason);
|
||||
users.remove(context);
|
||||
|
||||
if (users.size() == 0) {
|
||||
logger.info("All websocket connections are closed. Setting inputShouldShow to false.");
|
||||
|
||||
// cameraIndex -1 means the event is received by all cameras
|
||||
dcService.publishEvent(
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEPIPELINESETTINGS,
|
||||
"inputShouldShow",
|
||||
false,
|
||||
-1,
|
||||
null));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@@ -182,6 +196,12 @@ public class SocketHandler {
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGEBRIGHTNESS:
|
||||
{
|
||||
HardwareManager.getInstance()
|
||||
.setBrightnessPercent(Integer.parseInt(entryValue.toString()));
|
||||
break;
|
||||
}
|
||||
case SMT_DUPLICATEPIPELINE:
|
||||
{
|
||||
var pipeIndex = (Integer) entryValue;
|
||||
@@ -247,7 +267,7 @@ public class SocketHandler {
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"startcalibration",
|
||||
"startCalibration",
|
||||
(Map) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -34,6 +34,7 @@ public enum SocketMessageType {
|
||||
SMT_STARTPNPCALIBRATION("startPnpCalibration"),
|
||||
SMT_TAKECALIBRATIONSNAPSHOT("takeCalibrationSnapshot"),
|
||||
SMT_DUPLICATEPIPELINE("duplicatePipeline"),
|
||||
SMT_CHANGEBRIGHTNESS("enabledLEDPercentage"),
|
||||
SMT_ROBOTOFFSETPOINT("robotOffsetPoint");
|
||||
|
||||
public final String entryKey;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Photon Vision.
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user