mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-02 02:51:40 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec66645667 | ||
|
|
39aaa34520 | ||
|
|
4a3200d0c0 | ||
|
|
01dc7ea5ce | ||
|
|
2a9502be3d | ||
|
|
39216db143 | ||
|
|
428f926ac2 | ||
|
|
4efeb3d412 | ||
|
|
6a2d83e19b | ||
|
|
1c0d92641f | ||
|
|
9653c46bdb | ||
|
|
3738e7821b | ||
|
|
0eb0a4e3c5 | ||
|
|
7666f152bb | ||
|
|
45a39f6609 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -290,13 +290,13 @@ jobs:
|
|||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
artifact-name: LinuxArm64
|
artifact-name: LinuxArm64
|
||||||
image_suffix: orangepi5
|
image_suffix: orangepi5
|
||||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.6/photonvision_opi5.img.xz
|
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.9/photonvision_opi5.img.xz
|
||||||
cpu: cortex-a8
|
cpu: cortex-a8
|
||||||
image_additional_mb: 4096
|
image_additional_mb: 4096
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
artifact-name: LinuxArm64
|
artifact-name: LinuxArm64
|
||||||
image_suffix: orangepi5plus
|
image_suffix: orangepi5plus
|
||||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.6/photonvision_opi5plus.img.xz
|
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.9/photonvision_opi5plus.img.xz
|
||||||
cpu: cortex-a8
|
cpu: cortex-a8
|
||||||
image_additional_mb: 4096
|
image_additional_mb: 4096
|
||||||
|
|
||||||
|
|||||||
22
.github/workflows/documentation.yml
vendored
22
.github/workflows/documentation.yml
vendored
@@ -68,10 +68,6 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
needs: [build-client, run_docs]
|
needs: [build-client, run_docs]
|
||||||
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@@ -79,14 +75,12 @@ jobs:
|
|||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
|
|
||||||
- run: find .
|
- run: find .
|
||||||
|
- name: copy file via ssh password
|
||||||
- name: Setup Pages
|
uses: appleboy/scp-action@v0.1.7
|
||||||
uses: actions/configure-pages@v4
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
with:
|
||||||
# Upload entire repository
|
host: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||||
path: '.'
|
username: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||||
- name: Deploy to GitHub Pages
|
password: ${{ secrets.WEBMASTER_SSH_KEY }}
|
||||||
id: deployment
|
port: ${{ secrets.WEBMASTER_SSH_PORT }}
|
||||||
uses: actions/deploy-pages@v4
|
source: "*"
|
||||||
|
target: /var/www/html/photonvision-docs/
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.diffplug.spotless" version "6.24.0"
|
id "com.diffplug.spotless" version "6.24.0"
|
||||||
id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false
|
id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false
|
||||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||||
id 'com.google.protobuf' version '0.9.4' apply false
|
id 'com.google.protobuf' version '0.9.4' apply false
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ allprojects {
|
|||||||
apply from: "versioningHelper.gradle"
|
apply from: "versioningHelper.gradle"
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
wpilibVersion = "2024.2.1"
|
wpilibVersion = "2024.3.1"
|
||||||
wpimathVersion = wpilibVersion
|
wpimathVersion = wpilibVersion
|
||||||
openCVversion = "4.8.0-2"
|
openCVversion = "4.8.0-2"
|
||||||
joglVersion = "2.4.0-rc-20200307"
|
joglVersion = "2.4.0-rc-20200307"
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
|
|||||||
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
|
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
|
||||||
if (calib !== undefined) {
|
if (calib !== undefined) {
|
||||||
// For each error, square it, sum the squares, and divide by total points N
|
// For each error, square it, sum the squares, and divide by total points N
|
||||||
format.mean = calib.meanErrors.reduce((a, b) => a + b) / calib.meanErrors.length;
|
if (calib.meanErrors.length)
|
||||||
|
format.mean = calib.meanErrors.reduce((a, b) => a + b, 0) / calib.meanErrors.length;
|
||||||
|
else format.mean = NaN;
|
||||||
|
|
||||||
format.horizontalFOV =
|
format.horizontalFOV =
|
||||||
2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI);
|
2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI);
|
||||||
@@ -102,7 +104,7 @@ const downloadCalibBoard = () => {
|
|||||||
const yPos = chessboardStartY + squareY * squareSizeIn.value;
|
const yPos = chessboardStartY + squareY * squareSizeIn.value;
|
||||||
|
|
||||||
// Only draw the odd squares to create the chessboard pattern
|
// Only draw the odd squares to create the chessboard pattern
|
||||||
if ((xPos + yPos + 0.25) % 2 === 0) {
|
if (squareY % 2 != squareX % 2) {
|
||||||
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
|
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +258,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
|||||||
>
|
>
|
||||||
<td>{{ getResolutionString(value.resolution) }}</td>
|
<td>{{ getResolutionString(value.resolution) }}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ value.mean !== undefined ? (isNaN(value.mean) ? "NaN" : value.mean.toFixed(2) + "px") : "-" }}
|
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ const settingsHaveChanged = (): boolean => {
|
|||||||
a.shouldPublishProto !== b.shouldPublishProto ||
|
a.shouldPublishProto !== b.shouldPublishProto ||
|
||||||
a.networkManagerIface !== b.networkManagerIface ||
|
a.networkManagerIface !== b.networkManagerIface ||
|
||||||
a.setStaticCommand !== b.setStaticCommand ||
|
a.setStaticCommand !== b.setStaticCommand ||
|
||||||
a.setDHCPcommand !== b.setDHCPcommand
|
a.setDHCPcommand !== b.setDHCPcommand ||
|
||||||
|
a.matchCamerasOnlyByPath !== b.matchCamerasOnlyByPath
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ const saveGeneralSettings = () => {
|
|||||||
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
|
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
|
||||||
shouldManage: tempSettingsStruct.value.shouldManage,
|
shouldManage: tempSettingsStruct.value.shouldManage,
|
||||||
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
|
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
|
||||||
|
matchCamerasOnlyByPath: tempSettingsStruct.value.matchCamerasOnlyByPath,
|
||||||
staticIp: tempSettingsStruct.value.staticIp
|
staticIp: tempSettingsStruct.value.staticIp
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,6 +139,8 @@ watchEffect(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
|
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
|
||||||
|
<v-card-title>Global Settings</v-card-title>
|
||||||
|
<v-divider />
|
||||||
<v-card-title>Networking</v-card-title>
|
<v-card-title>Networking</v-card-title>
|
||||||
<div class="ml-5">
|
<div class="ml-5">
|
||||||
<v-form ref="form" v-model="settingsValid">
|
<v-form ref="form" v-model="settingsValid">
|
||||||
@@ -254,6 +258,9 @@ watchEffect(() => {
|
|||||||
>
|
>
|
||||||
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
|
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
|
||||||
</v-banner>
|
</v-banner>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
<v-card-title>Miscellaneous</v-card-title>
|
||||||
<pv-switch
|
<pv-switch
|
||||||
v-model="tempSettingsStruct.shouldPublishProto"
|
v-model="tempSettingsStruct.shouldPublishProto"
|
||||||
label="Also Publish Protobuf"
|
label="Also Publish Protobuf"
|
||||||
@@ -272,6 +279,32 @@ watchEffect(() => {
|
|||||||
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
|
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
|
||||||
this mode.
|
this mode.
|
||||||
</v-banner>
|
</v-banner>
|
||||||
|
<pv-switch
|
||||||
|
v-model="tempSettingsStruct.matchCamerasOnlyByPath"
|
||||||
|
label="Strictly match ONLY known cameras"
|
||||||
|
tooltip="ONLY match cameras by the USB port they're plugged into + (basename or USB VID/PID), and never only by the device product string. Also disables automatic detection of new cameras."
|
||||||
|
class="mt-3 mb-2"
|
||||||
|
:label-cols="4"
|
||||||
|
/>
|
||||||
|
<v-banner
|
||||||
|
v-show="tempSettingsStruct.matchCamerasOnlyByPath"
|
||||||
|
rounded
|
||||||
|
color="red"
|
||||||
|
class="mb-3"
|
||||||
|
text-color="white"
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
>
|
||||||
|
Physical cameras will be strictly matched to camera configurations using physical USB port they are plugged
|
||||||
|
into, in addition to device name and other USB metadata. Additionally, no new cameras are allowed to be added.
|
||||||
|
This setting is useful for guaranteeing that an already known and configured camera can never be matched as an
|
||||||
|
"unknown"/"new" camera, which resets pipelines and calibration data.
|
||||||
|
<p />
|
||||||
|
Cameras will NOT be matched if they change USB ports, and new cameras plugged into this coprocessor will NOT
|
||||||
|
be automatically recognized or configured for vision processing.
|
||||||
|
<p />
|
||||||
|
To add a new camera to this coprocessor, disable this setting, connect the camera, and re-enable.
|
||||||
|
</v-banner>
|
||||||
|
<v-divider class="mb-3" />
|
||||||
</v-form>
|
</v-form>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ export const useSettingsStore = defineStore("settings", {
|
|||||||
connName: "Example Wired Connection",
|
connName: "Example Wired Connection",
|
||||||
devName: "eth0"
|
devName: "eth0"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
networkingDisabled: false,
|
||||||
|
matchCamerasOnlyByPath: false
|
||||||
},
|
},
|
||||||
lighting: {
|
lighting: {
|
||||||
supported: true,
|
supported: true,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface NetworkSettings {
|
|||||||
setDHCPcommand?: string;
|
setDHCPcommand?: string;
|
||||||
networkInterfaceNames: NetworkInterfaceType[];
|
networkInterfaceNames: NetworkInterfaceType[];
|
||||||
networkingDisabled: boolean;
|
networkingDisabled: boolean;
|
||||||
|
matchCamerasOnlyByPath: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigurableNetworkSettings = Omit<
|
export type ConfigurableNetworkSettings = Omit<
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import org.photonvision.common.logging.LogGroup;
|
import org.photonvision.common.logging.LogGroup;
|
||||||
import org.photonvision.common.logging.Logger;
|
import org.photonvision.common.logging.Logger;
|
||||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||||
@@ -51,6 +52,12 @@ public class CameraConfiguration {
|
|||||||
|
|
||||||
@JsonIgnore public String[] otherPaths = {};
|
@JsonIgnore public String[] otherPaths = {};
|
||||||
|
|
||||||
|
@JsonProperty("usbVID")
|
||||||
|
public int usbVID = -1;
|
||||||
|
|
||||||
|
@JsonProperty("usbPID")
|
||||||
|
public int usbPID = -1;
|
||||||
|
|
||||||
public CameraType cameraType = CameraType.UsbCamera;
|
public CameraType cameraType = CameraType.UsbCamera;
|
||||||
public double FOV = 70;
|
public double FOV = 70;
|
||||||
public final List<CameraCalibrationCoefficients> calibrations;
|
public final List<CameraCalibrationCoefficients> calibrations;
|
||||||
@@ -98,7 +105,9 @@ public class CameraConfiguration {
|
|||||||
@JsonProperty("cameraType") CameraType cameraType,
|
@JsonProperty("cameraType") CameraType cameraType,
|
||||||
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
|
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
|
||||||
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
|
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
|
||||||
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
|
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
|
||||||
|
@JsonProperty("usbVID") int usbVID,
|
||||||
|
@JsonProperty("usbPID") int usbPID) {
|
||||||
this.baseName = baseName;
|
this.baseName = baseName;
|
||||||
this.uniqueName = uniqueName;
|
this.uniqueName = uniqueName;
|
||||||
this.nickname = nickname;
|
this.nickname = nickname;
|
||||||
@@ -108,6 +117,8 @@ public class CameraConfiguration {
|
|||||||
this.cameraQuirks = cameraQuirks;
|
this.cameraQuirks = cameraQuirks;
|
||||||
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
|
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
|
||||||
this.currentPipelineIndex = currentPipelineIndex;
|
this.currentPipelineIndex = currentPipelineIndex;
|
||||||
|
this.usbPID = usbPID;
|
||||||
|
this.usbVID = usbVID;
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Creating camera configuration for "
|
"Creating camera configuration for "
|
||||||
@@ -156,6 +167,17 @@ public class CameraConfiguration {
|
|||||||
calibrations.add(calibration);
|
calibrations.add(calibration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a unique descriptor of the USB port this camera is attached to. EG
|
||||||
|
* "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0"
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
public Optional<String> getUSBPath() {
|
||||||
|
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "CameraConfiguration [baseName="
|
return "CameraConfiguration [baseName="
|
||||||
|
|||||||
@@ -20,6 +20,24 @@ package org.photonvision.common.configuration;
|
|||||||
public class HardwareSettings {
|
public class HardwareSettings {
|
||||||
public int ledBrightnessPercentage = 100;
|
public int ledBrightnessPercentage = 100;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ledBrightnessPercentage;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) return true;
|
||||||
|
if (obj == null) return false;
|
||||||
|
if (getClass() != obj.getClass()) return false;
|
||||||
|
HardwareSettings other = (HardwareSettings) obj;
|
||||||
|
if (ledBrightnessPercentage != other.ledBrightnessPercentage) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]";
|
return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]";
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ public class NetworkConfig {
|
|||||||
public boolean shouldManage;
|
public boolean shouldManage;
|
||||||
public boolean shouldPublishProto = false;
|
public boolean shouldPublishProto = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we should ONLY match cameras by path, and NEVER only by base-name. For now default to false
|
||||||
|
* to preserve old matching logic.
|
||||||
|
*
|
||||||
|
* <p>This also disables creating new CameraConfigurations for detected "new" cameras.
|
||||||
|
*/
|
||||||
|
public boolean matchCamerasOnlyByPath = false;
|
||||||
|
|
||||||
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
|
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
|
||||||
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
|
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
|
||||||
|
|
||||||
@@ -76,7 +84,8 @@ public class NetworkConfig {
|
|||||||
@JsonProperty("shouldPublishProto") boolean shouldPublishProto,
|
@JsonProperty("shouldPublishProto") boolean shouldPublishProto,
|
||||||
@JsonProperty("networkManagerIface") String networkManagerIface,
|
@JsonProperty("networkManagerIface") String networkManagerIface,
|
||||||
@JsonProperty("setStaticCommand") String setStaticCommand,
|
@JsonProperty("setStaticCommand") String setStaticCommand,
|
||||||
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
|
@JsonProperty("setDHCPcommand") String setDHCPcommand,
|
||||||
|
@JsonProperty("matchCamerasOnlyByPath") boolean matchCamerasOnlyByPath) {
|
||||||
this.ntServerAddress = ntServerAddress;
|
this.ntServerAddress = ntServerAddress;
|
||||||
this.connectionType = connectionType;
|
this.connectionType = connectionType;
|
||||||
this.staticIp = staticIp;
|
this.staticIp = staticIp;
|
||||||
@@ -86,6 +95,7 @@ public class NetworkConfig {
|
|||||||
this.networkManagerIface = networkManagerIface;
|
this.networkManagerIface = networkManagerIface;
|
||||||
this.setStaticCommand = setStaticCommand;
|
this.setStaticCommand = setStaticCommand;
|
||||||
this.setDHCPcommand = setDHCPcommand;
|
this.setDHCPcommand = setDHCPcommand;
|
||||||
|
this.matchCamerasOnlyByPath = matchCamerasOnlyByPath;
|
||||||
setShouldManage(shouldManage);
|
setShouldManage(shouldManage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public class MrCalJNILoader extends PhotonJNICommon {
|
|||||||
"libcolamd",
|
"libcolamd",
|
||||||
"libccolamd",
|
"libccolamd",
|
||||||
"openblas",
|
"openblas",
|
||||||
|
"libwinpthread-1",
|
||||||
"libgcc_s_seh-1",
|
"libgcc_s_seh-1",
|
||||||
"libquadmath-0",
|
"libquadmath-0",
|
||||||
"libgfortran-5",
|
"libgfortran-5",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ package org.photonvision.vision.camera;
|
|||||||
|
|
||||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.photonvision.common.hardware.Platform;
|
||||||
|
|
||||||
public class CameraInfo extends UsbCameraInfo {
|
public class CameraInfo extends UsbCameraInfo {
|
||||||
public final CameraType cameraType;
|
public final CameraType cameraType;
|
||||||
@@ -68,15 +70,54 @@ public class CameraInfo extends UsbCameraInfo {
|
|||||||
return getBaseName().replaceAll(" ", "_");
|
return getBaseName().replaceAll(" ", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a unique descriptor of the USB port this camera is attached to. EG
|
||||||
|
* "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0"
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Optional<String> getUSBPath() {
|
||||||
|
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object obj) {
|
||||||
if (o == this) return true;
|
if (this == obj) return true;
|
||||||
if (!(o instanceof UsbCameraInfo || o instanceof CameraInfo)) return false;
|
if (obj == null) return false;
|
||||||
UsbCameraInfo other = (UsbCameraInfo) o;
|
if (getClass() != obj.getClass()) return false;
|
||||||
return path.equals(other.path)
|
CameraInfo other = (CameraInfo) obj;
|
||||||
// && a.dev == b.dev (dev is not constant in Windows)
|
|
||||||
&& name.equals(other.name)
|
// Windows device number is not significant. See
|
||||||
&& productId == other.productId
|
// https://github.com/wpilibsuite/allwpilib/blob/4b94a64b06057c723d6fcafeb1a45f55a70d179a/cscore/src/main/native/windows/UsbCameraImpl.cpp#L1128
|
||||||
&& vendorId == other.vendorId;
|
if (!Platform.isWindows()) {
|
||||||
|
if (dev != other.dev) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.equals(other.path)) return false;
|
||||||
|
if (!name.equals(other.name)) return false;
|
||||||
|
if (!Arrays.asList(this.otherPaths).containsAll(Arrays.asList(other.otherPaths))) return false;
|
||||||
|
if (vendorId != other.vendorId) return false;
|
||||||
|
if (productId != other.productId) return false;
|
||||||
|
|
||||||
|
// Don't trust super.equals, as it compares references. Should PR this to allwpilib at some
|
||||||
|
// point
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "CameraInfo [cameraType="
|
||||||
|
+ cameraType
|
||||||
|
+ "baseName="
|
||||||
|
+ getBaseName()
|
||||||
|
+ ", vid="
|
||||||
|
+ vendorId
|
||||||
|
+ ", pid="
|
||||||
|
+ productId
|
||||||
|
+ ", path="
|
||||||
|
+ path
|
||||||
|
+ ", otherPaths="
|
||||||
|
+ Arrays.toString(otherPaths)
|
||||||
|
+ "]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Photon Vision.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.photonvision.vision.camera;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import org.photonvision.common.configuration.CameraConfiguration;
|
||||||
|
import org.photonvision.vision.frame.Frame;
|
||||||
|
import org.photonvision.vision.frame.FrameProvider;
|
||||||
|
import org.photonvision.vision.frame.FrameThresholdType;
|
||||||
|
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||||
|
import org.photonvision.vision.pipe.impl.HSVPipe.HSVParams;
|
||||||
|
import org.photonvision.vision.processes.VisionSource;
|
||||||
|
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||||
|
|
||||||
|
/** Dummy class for unit testing the vision source manager */
|
||||||
|
public class TestSource extends VisionSource {
|
||||||
|
private FrameProvider usbFrameProvider;
|
||||||
|
|
||||||
|
public TestSource(CameraConfiguration config) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
if (getCameraConfiguration().cameraQuirks == null)
|
||||||
|
getCameraConfiguration().cameraQuirks =
|
||||||
|
QuirkyCamera.getQuirkyCamera(config.usbVID, config.usbVID, config.baseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FrameProvider getFrameProvider() {
|
||||||
|
return new FrameProvider() {
|
||||||
|
@Override
|
||||||
|
public Frame get() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'get'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return cameraConfiguration.uniqueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestFrameThresholdType(FrameThresholdType type) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'requestFrameThresholdType'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestFrameRotation(ImageRotationMode rotationMode) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'requestFrameRotation'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestFrameCopies(boolean copyInput, boolean copyOutput) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'requestFrameCopies'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestHsvSettings(HSVParams params) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'requestHsvSettings'");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VisionSourceSettables getSettables() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'getSettables'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isVendorCamera() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'isVendorCamera'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,9 +49,17 @@ public class USBCameraSource extends VisionSource {
|
|||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
|
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
|
||||||
camera = new UsbCamera(config.nickname, config.path);
|
// cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i
|
||||||
|
// swap cameras around, the same /dev/videoN ID will be assigned to that camera. So instead
|
||||||
|
// default to pinning to a particular USB port, or by "path" (appears to be a global identifier)
|
||||||
|
// on Windows.
|
||||||
|
camera = new UsbCamera(config.nickname, config.getUSBPath().orElse(config.path));
|
||||||
cvSink = CameraServer.getVideo(this.camera);
|
cvSink = CameraServer.getVideo(this.camera);
|
||||||
|
|
||||||
|
// set vid/pid if not done already for future matching
|
||||||
|
if (config.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
|
||||||
|
if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
|
||||||
|
|
||||||
if (getCameraConfiguration().cameraQuirks == null)
|
if (getCameraConfiguration().cameraQuirks == null)
|
||||||
getCameraConfiguration().cameraQuirks =
|
getCameraConfiguration().cameraQuirks =
|
||||||
QuirkyCamera.getQuirkyCamera(
|
QuirkyCamera.getQuirkyCamera(
|
||||||
@@ -395,6 +403,7 @@ public class USBCameraSource extends VisionSource {
|
|||||||
// Sort by resolution
|
// Sort by resolution
|
||||||
var sortedList =
|
var sortedList =
|
||||||
videoModesList.stream()
|
videoModesList.stream()
|
||||||
|
.distinct() // remove redundant video mode entries
|
||||||
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
|
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Collections.reverse(sortedList);
|
Collections.reverse(sortedList);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.photonvision.common.configuration.CameraConfiguration;
|
import org.photonvision.common.configuration.CameraConfiguration;
|
||||||
import org.photonvision.common.configuration.ConfigManager;
|
import org.photonvision.common.configuration.ConfigManager;
|
||||||
@@ -38,6 +39,7 @@ import org.photonvision.vision.camera.CameraInfo;
|
|||||||
import org.photonvision.vision.camera.CameraQuirk;
|
import org.photonvision.vision.camera.CameraQuirk;
|
||||||
import org.photonvision.vision.camera.CameraType;
|
import org.photonvision.vision.camera.CameraType;
|
||||||
import org.photonvision.vision.camera.LibcameraGpuSource;
|
import org.photonvision.vision.camera.LibcameraGpuSource;
|
||||||
|
import org.photonvision.vision.camera.TestSource;
|
||||||
import org.photonvision.vision.camera.USBCameraSource;
|
import org.photonvision.vision.camera.USBCameraSource;
|
||||||
|
|
||||||
public class VisionSourceManager {
|
public class VisionSourceManager {
|
||||||
@@ -145,8 +147,8 @@ public class VisionSourceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return no new sources because there are no new sources
|
// Return no new sources because there are no new sources
|
||||||
if (connectedCameras.isEmpty() && !cameraInfos.isEmpty()) {
|
if (connectedCameras.isEmpty()) {
|
||||||
if (hasWarnedNoCameras) {
|
if (!hasWarnedNoCameras) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"No cameras were detected! Check that all cameras are connected, and that the path is correct.");
|
"No cameras were detected! Check that all cameras are connected, and that the path is correct.");
|
||||||
hasWarnedNoCameras = true;
|
hasWarnedNoCameras = true;
|
||||||
@@ -164,7 +166,7 @@ public class VisionSourceManager {
|
|||||||
|
|
||||||
// Debug prints
|
// Debug prints
|
||||||
for (var info : connectedCameras) {
|
for (var info : connectedCameras) {
|
||||||
logger.info("Adding local video device - \"" + info.name + "\" at \"" + info.path + "\"");
|
logger.info("Detected unmatched physical camera: " + info.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!unmatchedLoadedConfigs.isEmpty())
|
if (!unmatchedLoadedConfigs.isEmpty())
|
||||||
@@ -185,7 +187,7 @@ public class VisionSourceManager {
|
|||||||
"Unloaded configs: "
|
"Unloaded configs: "
|
||||||
+ unmatchedLoadedConfigs.stream()
|
+ unmatchedLoadedConfigs.stream()
|
||||||
.map(it -> it.nickname)
|
.map(it -> it.nickname)
|
||||||
.collect(Collectors.joining()));
|
.collect(Collectors.joining(", ")));
|
||||||
hasWarned = true;
|
hasWarned = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,13 +196,8 @@ public class VisionSourceManager {
|
|||||||
|
|
||||||
if (matchedCameras.isEmpty()) return null;
|
if (matchedCameras.isEmpty()) return null;
|
||||||
|
|
||||||
// for unit tests only!
|
|
||||||
if (!createSources) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn these camera configs into vision sources
|
// Turn these camera configs into vision sources
|
||||||
var sources = loadVisionSourcesFromCamConfigs(matchedCameras);
|
var sources = loadVisionSourcesFromCamConfigs(matchedCameras, createSources);
|
||||||
|
|
||||||
// Print info about each vision source
|
// Print info about each vision source
|
||||||
for (var src : sources) {
|
for (var src : sources) {
|
||||||
@@ -216,6 +213,52 @@ public class VisionSourceManager {
|
|||||||
return sources;
|
return sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a predicate for checking cameras against a saved config.
|
||||||
|
*
|
||||||
|
* @param savedConfig The saved camera configuration to match against
|
||||||
|
* @param checkUSBPath If we should compare the USB port/bus IDs
|
||||||
|
* @param checkVidPid If we should compare USB VID and PID
|
||||||
|
* @param checkBaseName If we should compare {@link CameraInfo#getBaseName}
|
||||||
|
* @param checkPath If we should check {@link CameraInfo::path} (eg /dev/videoN on Linux, or
|
||||||
|
* ?/usb#vid_05c8&pid_03df&mi_00#7&fa76035&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
|
||||||
|
* on Windows)
|
||||||
|
*/
|
||||||
|
private final Predicate<CameraInfo> getCameraMatcher(
|
||||||
|
final CameraConfiguration savedConfig,
|
||||||
|
boolean checkUSBPath,
|
||||||
|
boolean checkVidPid,
|
||||||
|
boolean checkBaseName,
|
||||||
|
boolean checkPath) {
|
||||||
|
if (checkUSBPath && savedConfig.getUSBPath().isEmpty()) {
|
||||||
|
logger.debug(
|
||||||
|
"WARN: Camera has empty USB path, but asked to match by name: "
|
||||||
|
+ camCfgToString(savedConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (CameraInfo physicalCamera) -> {
|
||||||
|
var matches = true;
|
||||||
|
|
||||||
|
if (checkUSBPath) {
|
||||||
|
var savedPath = savedConfig.getUSBPath();
|
||||||
|
matches &= (savedPath.isPresent() && physicalCamera.getUSBPath().equals(savedPath));
|
||||||
|
}
|
||||||
|
if (checkBaseName) {
|
||||||
|
matches &= physicalCamera.getBaseName().equals(savedConfig.baseName);
|
||||||
|
}
|
||||||
|
if (checkVidPid) {
|
||||||
|
matches &=
|
||||||
|
(physicalCamera.vendorId == savedConfig.usbVID
|
||||||
|
&& physicalCamera.productId == savedConfig.usbPID);
|
||||||
|
}
|
||||||
|
if (checkPath) {
|
||||||
|
matches &= (physicalCamera.path.equals(savedConfig.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
|
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
|
||||||
* disk.
|
* disk.
|
||||||
@@ -226,35 +269,133 @@ public class VisionSourceManager {
|
|||||||
*/
|
*/
|
||||||
public List<CameraConfiguration> matchCameras(
|
public List<CameraConfiguration> matchCameras(
|
||||||
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> loadedCamConfigs) {
|
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> loadedCamConfigs) {
|
||||||
|
return matchCameras(
|
||||||
|
detectedCamInfos,
|
||||||
|
loadedCamConfigs,
|
||||||
|
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String camCfgToString(CameraConfiguration c) {
|
||||||
|
return new StringBuilder()
|
||||||
|
.append("[baseName=")
|
||||||
|
.append(c.baseName)
|
||||||
|
.append(", uniqueName=")
|
||||||
|
.append(c.uniqueName)
|
||||||
|
.append(", otherPaths=")
|
||||||
|
.append(Arrays.toString(c.otherPaths))
|
||||||
|
.append(", vid=")
|
||||||
|
.append(c.usbVID)
|
||||||
|
.append(", pid=")
|
||||||
|
.append(c.usbPID)
|
||||||
|
.append("]")
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
|
||||||
|
* disk.
|
||||||
|
*
|
||||||
|
* @param detectedCamInfos Information about currently connected USB cameras.
|
||||||
|
* @param loadedCamConfigs The USB {@link CameraConfiguration}s loaded from disk.
|
||||||
|
* @param matchCamerasOnlyByPath If we should never try to match only by (base name, vid, pid)
|
||||||
|
* @return the matched configurations.
|
||||||
|
*/
|
||||||
|
public List<CameraConfiguration> matchCameras(
|
||||||
|
List<CameraInfo> detectedCamInfos,
|
||||||
|
List<CameraConfiguration> loadedCamConfigs,
|
||||||
|
boolean matchCamerasOnlyByPath) {
|
||||||
var detectedCameraList = new ArrayList<>(detectedCamInfos);
|
var detectedCameraList = new ArrayList<>(detectedCamInfos);
|
||||||
ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>();
|
ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>();
|
||||||
ArrayList<CameraConfiguration> unloadedConfigs =
|
ArrayList<CameraConfiguration> unloadedConfigs =
|
||||||
new ArrayList<CameraConfiguration>(loadedCamConfigs);
|
new ArrayList<CameraConfiguration>(loadedCamConfigs);
|
||||||
|
|
||||||
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
|
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
|
||||||
cameraConfigurations.addAll(matchByPathByID(detectedCameraList, unloadedConfigs));
|
logger.info("Matching by usb port & name & USB VID/PID...");
|
||||||
else logger.debug("Skipping matchByPath no configs or cameras left to match");
|
|
||||||
|
|
||||||
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
|
|
||||||
cameraConfigurations.addAll(matchByPath(detectedCameraList, unloadedConfigs));
|
|
||||||
else logger.debug("Skipping matchByPath no configs or cameras left to match");
|
|
||||||
|
|
||||||
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
|
|
||||||
cameraConfigurations.addAll(matchByName(detectedCameraList, unloadedConfigs));
|
|
||||||
else logger.debug("Skipping matchByName no configs or cameras left to match");
|
|
||||||
|
|
||||||
if (detectedCameraList.size() > 0)
|
|
||||||
cameraConfigurations.addAll(
|
cameraConfigurations.addAll(
|
||||||
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations));
|
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// On windows, the v4l path is actually useful and tells us the port the camera is physically
|
||||||
|
// connected to which is neat
|
||||||
|
if (Platform.isWindows() && !matchCamerasOnlyByPath) {
|
||||||
|
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
|
||||||
|
logger.info("Matching by windows-path & USB VID/PID only...");
|
||||||
|
cameraConfigurations.addAll(
|
||||||
|
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
|
||||||
|
logger.info("Matching by usb port & USB VID/PID...");
|
||||||
|
cameraConfigurations.addAll(
|
||||||
|
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy migration -- VID/PID will be unset, so we have to try with our most relaxed strategy
|
||||||
|
// at least once. We _should_ still have a valid USB path (assuming cameras have not moved), so
|
||||||
|
// try that first, then fallback to base name only beloow
|
||||||
|
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
|
||||||
|
logger.info("Matching by base-name & usb port...");
|
||||||
|
cameraConfigurations.addAll(
|
||||||
|
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, false, true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle disabling only-by-base-name matching
|
||||||
|
if (!matchCamerasOnlyByPath) {
|
||||||
|
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
|
||||||
|
logger.info("Matching by base-name & USB VID/PID only...");
|
||||||
|
cameraConfigurations.addAll(
|
||||||
|
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy migration for if no USB VID/PID set
|
||||||
|
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
|
||||||
|
logger.info("Matching by base-name only...");
|
||||||
|
cameraConfigurations.addAll(
|
||||||
|
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, false, true, false));
|
||||||
|
}
|
||||||
|
} else logger.info("Skipping match by filepath/vid/pid, disabled by user");
|
||||||
|
|
||||||
|
if (detectedCameraList.size() > 0) {
|
||||||
|
// handle disabling only-by-base-name matching
|
||||||
|
if (!matchCamerasOnlyByPath) {
|
||||||
|
cameraConfigurations.addAll(
|
||||||
|
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations));
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"Not creating 'new' Photon CameraConfigurations for ["
|
||||||
|
+ detectedCamInfos.stream()
|
||||||
|
.map(CameraInfo::toString)
|
||||||
|
.collect(Collectors.joining(";"))
|
||||||
|
+ "], disabled by user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug("Matched or created " + cameraConfigurations.size() + " camera configs!");
|
logger.debug("Matched or created " + cameraConfigurations.size() + " camera configs!");
|
||||||
return cameraConfigurations;
|
return cameraConfigurations;
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop over all the configs loaded from disk, attempting to match each camera
|
/**
|
||||||
// to a config by path-by-id on linux
|
* Abstractly match cameras
|
||||||
private List<CameraConfiguration> matchByPathByID(
|
*
|
||||||
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
|
* @param detectedCamInfos Physical cameras unmatched and attached to the device
|
||||||
|
* @param unloadedConfigs {@link CameraConfiguration}
|
||||||
|
* @param checkUSBPath If we should compare the USB port/bus IDs
|
||||||
|
* @param checkVidPid If we should compare USB VID and PID
|
||||||
|
* @param checkBaseName If we should check {@link CameraInfo::getBaseName}
|
||||||
|
* @param checkPath If we should check {@link CameraInfo::path} (eg /dev/videoN on Linux, or
|
||||||
|
* usb#vid_05c8&pid_03df&mi_00#7&fa76035&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
|
||||||
|
* on Windows). Note that path may change based on order cameras are plugged in/unplugged on
|
||||||
|
* Linux, and should not be trusted to remain the same.
|
||||||
|
* @return All matched or created new configs
|
||||||
|
*/
|
||||||
|
private List<CameraConfiguration> matchCamerasByStrategy(
|
||||||
|
List<CameraInfo> detectedCamInfos,
|
||||||
|
List<CameraConfiguration> unloadedConfigs,
|
||||||
|
boolean checkUSBPath,
|
||||||
|
boolean checkVidPid,
|
||||||
|
boolean checkBaseName,
|
||||||
|
boolean checkPath) {
|
||||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
||||||
List<CameraConfiguration> unloadedConfigsCopy =
|
List<CameraConfiguration> unloadedConfigsCopy =
|
||||||
new ArrayList<CameraConfiguration>(unloadedConfigs);
|
new ArrayList<CameraConfiguration>(unloadedConfigs);
|
||||||
@@ -262,111 +403,43 @@ public class VisionSourceManager {
|
|||||||
for (CameraConfiguration config : unloadedConfigsCopy) {
|
for (CameraConfiguration config : unloadedConfigsCopy) {
|
||||||
// Only run match path by id if the camera is not a CSI camera.
|
// Only run match path by id if the camera is not a CSI camera.
|
||||||
if (config.cameraType != CameraType.ZeroCopyPicam) {
|
if (config.cameraType != CameraType.ZeroCopyPicam) {
|
||||||
CameraInfo cameraInfo;
|
logger.debug(
|
||||||
if (config.otherPaths.length == 0) {
|
String.format(
|
||||||
logger.debug("No valid path-by-id found for config with name " + config.baseName);
|
"Trying to find a match for loaded camera %s by strategy (path %s vid/pid %s basename %s path %s) with camera config: %s",
|
||||||
} else {
|
config.baseName,
|
||||||
// attempt matching by path and basename
|
checkUSBPath,
|
||||||
logger.debug(
|
checkVidPid,
|
||||||
"Trying to find a match for loaded camera "
|
checkBaseName,
|
||||||
+ config.baseName
|
checkPath,
|
||||||
+ " with path-by-id "
|
camCfgToString(config)));
|
||||||
+ config.otherPaths[0]);
|
|
||||||
cameraInfo =
|
|
||||||
detectedCamInfos.stream()
|
|
||||||
.filter(
|
|
||||||
usbCameraInfo ->
|
|
||||||
usbCameraInfo.otherPaths.length != 0
|
|
||||||
&& usbCameraInfo.otherPaths[0].equals(config.otherPaths[0])
|
|
||||||
&& usbCameraInfo.getBaseName().equals(config.baseName))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
// If we actually matched a camera to a config, remove that camera from the list
|
// Get matcher and filter against it, picking out the first match
|
||||||
// and add it to the output
|
Predicate<CameraInfo> matches =
|
||||||
if (cameraInfo != null) {
|
getCameraMatcher(config, checkUSBPath, checkVidPid, checkBaseName, checkPath);
|
||||||
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
|
var cameraInfo = detectedCamInfos.stream().filter(matches).findFirst().orElse(null);
|
||||||
ret.add(mergeInfoIntoConfig(config, cameraInfo));
|
|
||||||
detectedCamInfos.remove(cameraInfo);
|
// If we actually matched a camera to a config, remove that camera from the list
|
||||||
unloadedConfigs.remove(config);
|
// and add it to the output
|
||||||
}
|
if (cameraInfo != null) {
|
||||||
|
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
|
||||||
|
ret.add(mergeInfoIntoConfig(config, cameraInfo));
|
||||||
|
detectedCamInfos.remove(cameraInfo);
|
||||||
|
unloadedConfigs.remove(config);
|
||||||
|
} else {
|
||||||
|
logger.debug("No camera found for the config " + config.baseName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<CameraConfiguration> matchByPath(
|
/**
|
||||||
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
|
* Create new {@link CameraConfiguration}s for unmatched cameras, and assign them a unique name
|
||||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
* (unique in the set of (loaded configs, unloaded configs, loaded vision modules) at least)
|
||||||
List<CameraConfiguration> unloadedConfigsCopy =
|
*/
|
||||||
new ArrayList<CameraConfiguration>(unloadedConfigs);
|
|
||||||
// now attempt to match the cameras and configs remaining by normal path
|
|
||||||
for (CameraConfiguration config : unloadedConfigsCopy) {
|
|
||||||
CameraInfo cameraInfo;
|
|
||||||
|
|
||||||
// attempt matching by path and basename
|
|
||||||
logger.debug(
|
|
||||||
"Trying to find a match for loaded camera "
|
|
||||||
+ config.baseName
|
|
||||||
+ " with path "
|
|
||||||
+ config.path);
|
|
||||||
cameraInfo =
|
|
||||||
detectedCamInfos.stream()
|
|
||||||
.filter(
|
|
||||||
usbCameraInfo ->
|
|
||||||
usbCameraInfo.path.equals(config.path)
|
|
||||||
&& usbCameraInfo.getBaseName().equals(config.baseName))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
// If we actually matched a camera to a config, remove that camera from the list
|
|
||||||
// and add it to the output
|
|
||||||
if (cameraInfo != null) {
|
|
||||||
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
|
|
||||||
ret.add(mergeInfoIntoConfig(config, cameraInfo));
|
|
||||||
detectedCamInfos.remove(cameraInfo);
|
|
||||||
unloadedConfigs.remove(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try matching cameras to configs by name.
|
|
||||||
private List<CameraConfiguration> matchByName(
|
|
||||||
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
|
|
||||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
|
||||||
List<CameraConfiguration> unloadedConfigsCopy =
|
|
||||||
new ArrayList<CameraConfiguration>(unloadedConfigs);
|
|
||||||
// if both path and ID based matching fails, attempt basename only match
|
|
||||||
for (CameraConfiguration config : unloadedConfigsCopy) {
|
|
||||||
CameraInfo cameraInfo;
|
|
||||||
|
|
||||||
logger.debug("Trying to find a match for loaded camera with name " + config.baseName);
|
|
||||||
|
|
||||||
cameraInfo =
|
|
||||||
detectedCamInfos.stream()
|
|
||||||
.filter(CameraInfo -> CameraInfo.getBaseName().equals(config.baseName))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
// If we actually matched a camera to a config, remove that camera from the list
|
|
||||||
// and add it to the output
|
|
||||||
if (cameraInfo != null) {
|
|
||||||
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
|
|
||||||
ret.add(mergeInfoIntoConfig(config, cameraInfo));
|
|
||||||
detectedCamInfos.remove(cameraInfo);
|
|
||||||
unloadedConfigs.remove(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have any unmatched cameras left, create a new CameraConfiguration for
|
|
||||||
// them here.
|
|
||||||
private List<CameraConfiguration> createConfigsForCameras(
|
private List<CameraConfiguration> createConfigsForCameras(
|
||||||
List<CameraInfo> detectedCameraList,
|
List<CameraInfo> detectedCameraList,
|
||||||
List<CameraConfiguration> loadedCamConfigs,
|
List<CameraConfiguration> unloadedCamConfigs,
|
||||||
List<CameraConfiguration> loadedConfigs) {
|
List<CameraConfiguration> loadedConfigs) {
|
||||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -377,7 +450,10 @@ public class VisionSourceManager {
|
|||||||
String uniqueName = info.getHumanReadableName();
|
String uniqueName = info.getHumanReadableName();
|
||||||
|
|
||||||
int suffix = 0;
|
int suffix = 0;
|
||||||
while (containsName(loadedConfigs, uniqueName) || containsName(uniqueName)) {
|
while (containsName(loadedConfigs, uniqueName)
|
||||||
|
|| containsName(uniqueName)
|
||||||
|
|| containsName(unloadedCamConfigs, uniqueName)
|
||||||
|
|| containsName(ret, uniqueName)) {
|
||||||
suffix++;
|
suffix++;
|
||||||
uniqueName = String.format("%s (%d)", uniqueName, suffix);
|
uniqueName = String.format("%s (%d)", uniqueName, suffix);
|
||||||
}
|
}
|
||||||
@@ -457,10 +533,16 @@ public class VisionSourceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static List<VisionSource> loadVisionSourcesFromCamConfigs(
|
private static List<VisionSource> loadVisionSourcesFromCamConfigs(
|
||||||
List<CameraConfiguration> camConfigs) {
|
List<CameraConfiguration> camConfigs, boolean createSources) {
|
||||||
var cameraSources = new ArrayList<VisionSource>();
|
var cameraSources = new ArrayList<VisionSource>();
|
||||||
for (var configuration : camConfigs) {
|
for (var configuration : camConfigs) {
|
||||||
logger.debug("Creating VisionSource for " + configuration);
|
logger.debug("Creating VisionSource for " + camCfgToString(configuration));
|
||||||
|
|
||||||
|
// In unit tests, create dummy
|
||||||
|
if (!createSources) {
|
||||||
|
cameraSources.add(new TestSource(configuration));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
boolean is_pi = Platform.isRaspberryPi();
|
boolean is_pi = Platform.isRaspberryPi();
|
||||||
|
|
||||||
|
|||||||
@@ -139,8 +139,31 @@ public class ConfigTest {
|
|||||||
writer.write(str);
|
writer.write(str);
|
||||||
writer.flush();
|
writer.flush();
|
||||||
writer.close();
|
writer.close();
|
||||||
Assertions.assertDoesNotThrow(
|
CameraConfiguration result =
|
||||||
() -> JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class));
|
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
|
||||||
|
|
||||||
|
tempFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testJacksonAddUSBVIDPID() throws IOException {
|
||||||
|
var str =
|
||||||
|
"{\"baseName\":\"aaaaaa\",\"uniqueName\":\"aaaaaa\",\"nickname\":\"aaaaaa\",\"FOV\":70.0,\"path\":\"dev/vid\",\"cameraType\":\"UsbCamera\",\"currentPipelineIndex\":0,\"camPitch\":{\"radians\":0.0},\"calibrations\":[], \"usbVID\":3, \"usbPID\":4, \"cameraLEDs\":[]}";
|
||||||
|
File tempFile = File.createTempFile("test", ".json");
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
var writer = new FileWriter(tempFile);
|
||||||
|
writer.write(str);
|
||||||
|
writer.flush();
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
try {
|
||||||
|
CameraConfiguration result =
|
||||||
|
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
|
||||||
|
String ser = JacksonUtils.serializeToString(result);
|
||||||
|
System.out.println(ser);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
tempFile.delete();
|
tempFile.delete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ public class SQLConfigTest {
|
|||||||
CameraType.UsbCamera,
|
CameraType.UsbCamera,
|
||||||
QuirkyCamera.getQuirkyCamera(-1, -1),
|
QuirkyCamera.getQuirkyCamera(-1, -1),
|
||||||
List.of(),
|
List.of(),
|
||||||
0);
|
0,
|
||||||
|
-1,
|
||||||
|
-1);
|
||||||
testcamcfg.pipelineSettings =
|
testcamcfg.pipelineSettings =
|
||||||
List.of(
|
List.of(
|
||||||
new ReflectivePipelineSettings(),
|
new ReflectivePipelineSettings(),
|
||||||
|
|||||||
@@ -21,17 +21,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.photonvision.common.configuration.CameraConfiguration;
|
import org.photonvision.common.configuration.CameraConfiguration;
|
||||||
import org.photonvision.common.configuration.ConfigManager;
|
import org.photonvision.common.configuration.ConfigManager;
|
||||||
|
import org.photonvision.common.logging.LogGroup;
|
||||||
|
import org.photonvision.common.logging.LogLevel;
|
||||||
|
import org.photonvision.common.logging.Logger;
|
||||||
import org.photonvision.vision.camera.CameraInfo;
|
import org.photonvision.vision.camera.CameraInfo;
|
||||||
import org.photonvision.vision.camera.CameraType;
|
import org.photonvision.vision.camera.CameraType;
|
||||||
|
|
||||||
public class VisionSourceManagerTest {
|
public class VisionSourceManagerTest {
|
||||||
@Test
|
@Test
|
||||||
public void visionSourceTest() {
|
public void visionSourceTest() {
|
||||||
|
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||||
|
|
||||||
var inst = new VisionSourceManager();
|
var inst = new VisionSourceManager();
|
||||||
var cameraInfos = new ArrayList<CameraInfo>();
|
var cameraInfos = new ArrayList<CameraInfo>();
|
||||||
|
ConfigManager.getInstance().clearConfig();
|
||||||
ConfigManager.getInstance().load();
|
ConfigManager.getInstance().load();
|
||||||
|
|
||||||
inst.tryMatchCamImpl(cameraInfos);
|
inst.tryMatchCamImpl(cameraInfos);
|
||||||
@@ -43,6 +50,8 @@ public class VisionSourceManagerTest {
|
|||||||
"thirdTestVideo",
|
"thirdTestVideo",
|
||||||
"dev/video1",
|
"dev/video1",
|
||||||
new String[] {"by-id/123"});
|
new String[] {"by-id/123"});
|
||||||
|
config3.usbVID = 3;
|
||||||
|
config3.usbPID = 4;
|
||||||
var config4 =
|
var config4 =
|
||||||
new CameraConfiguration(
|
new CameraConfiguration(
|
||||||
"fourthTestVideo",
|
"fourthTestVideo",
|
||||||
@@ -50,6 +59,8 @@ public class VisionSourceManagerTest {
|
|||||||
"fourthTestVideo",
|
"fourthTestVideo",
|
||||||
"dev/video2",
|
"dev/video2",
|
||||||
new String[] {"by-id/321"});
|
new String[] {"by-id/321"});
|
||||||
|
config4.usbVID = 5;
|
||||||
|
config4.usbPID = 6;
|
||||||
|
|
||||||
CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2);
|
CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2);
|
||||||
|
|
||||||
@@ -261,4 +272,268 @@ public class VisionSourceManagerTest {
|
|||||||
assertEquals(10, inst.knownCameras.size());
|
assertEquals(10, inst.knownCameras.size());
|
||||||
assertEquals(0, inst.unmatchedLoadedConfigs.size());
|
assertEquals(0, inst.unmatchedLoadedConfigs.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDisableInhibitPathChangeIdenticalCams() {
|
||||||
|
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||||
|
|
||||||
|
var inst = new VisionSourceManager();
|
||||||
|
ConfigManager.getInstance().clearConfig();
|
||||||
|
ConfigManager.getInstance().load();
|
||||||
|
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
|
||||||
|
|
||||||
|
var CAM2_OLD_PATH =
|
||||||
|
new String[] {"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"};
|
||||||
|
var CAM2_NEW_PATH =
|
||||||
|
new String[] {"/dev/v4l/by-path/platform-fc880080.usb-usb-0:1:1.3-video-index0"};
|
||||||
|
|
||||||
|
var CAM1_OLD_PATHS =
|
||||||
|
new String[] {
|
||||||
|
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||||
|
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var camera1_saved_config =
|
||||||
|
new CameraConfiguration(
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
"fromt-left",
|
||||||
|
"/dev/video0",
|
||||||
|
CAM1_OLD_PATHS);
|
||||||
|
camera1_saved_config.usbVID = 3141;
|
||||||
|
camera1_saved_config.usbPID = 25446;
|
||||||
|
var camera2_saved_config =
|
||||||
|
new CameraConfiguration(
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
"Arducam OV2311 USB Camera (1)",
|
||||||
|
"fromt-left",
|
||||||
|
"/dev/video2",
|
||||||
|
CAM2_OLD_PATH);
|
||||||
|
camera2_saved_config.usbVID = 3141;
|
||||||
|
camera2_saved_config.usbPID = 25446;
|
||||||
|
|
||||||
|
// And load our "old" configs
|
||||||
|
inst.registerLoadedConfigs(camera1_saved_config, camera2_saved_config);
|
||||||
|
|
||||||
|
// Camera attached to new port, but strict matching disabled
|
||||||
|
{
|
||||||
|
CameraInfo info1 =
|
||||||
|
new CameraInfo(
|
||||||
|
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
|
||||||
|
CameraInfo info2 =
|
||||||
|
new CameraInfo(
|
||||||
|
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_NEW_PATH, 3141, 25446);
|
||||||
|
|
||||||
|
var cameraInfos = new ArrayList<CameraInfo>();
|
||||||
|
cameraInfos.add(info1);
|
||||||
|
cameraInfos.add(info2);
|
||||||
|
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||||
|
|
||||||
|
// and check the new one got matched got matched
|
||||||
|
assertEquals(2, ret1.size());
|
||||||
|
assertEquals(
|
||||||
|
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
|
||||||
|
assertEquals(
|
||||||
|
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInhibitPathChangeIdenticalCams() {
|
||||||
|
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||||
|
|
||||||
|
var inst = new VisionSourceManager();
|
||||||
|
ConfigManager.getInstance().clearConfig();
|
||||||
|
ConfigManager.getInstance().load();
|
||||||
|
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = true;
|
||||||
|
|
||||||
|
var CAM2_OLD_PATH =
|
||||||
|
new String[] {"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"};
|
||||||
|
var CAM2_NEW_PATH =
|
||||||
|
new String[] {"/dev/v4l/by-path/platform-fc880080.usb-usb-0:1:1.3-video-index0"};
|
||||||
|
|
||||||
|
var CAM1_OLD_PATHS =
|
||||||
|
new String[] {
|
||||||
|
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||||
|
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var camera1_saved_config =
|
||||||
|
new CameraConfiguration(
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
"Arducam OV2311 USB Camera (1)",
|
||||||
|
"fromt-left",
|
||||||
|
"/dev/video0",
|
||||||
|
CAM1_OLD_PATHS);
|
||||||
|
camera1_saved_config.usbVID = 3141;
|
||||||
|
camera1_saved_config.usbPID = 25446;
|
||||||
|
var camera2_saved_config =
|
||||||
|
new CameraConfiguration(
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
"Arducam OV2311 USB Camera (1)",
|
||||||
|
"fromt-left",
|
||||||
|
"/dev/video2",
|
||||||
|
CAM2_OLD_PATH);
|
||||||
|
camera2_saved_config.usbVID = 3141;
|
||||||
|
camera2_saved_config.usbPID = 25446;
|
||||||
|
|
||||||
|
// And load our "old" configs
|
||||||
|
inst.registerLoadedConfigs(camera1_saved_config, camera2_saved_config);
|
||||||
|
|
||||||
|
// initial pass with camera in the wrong spot
|
||||||
|
{
|
||||||
|
// Give our cameras new "paths" to fake the windows logic out. this should not
|
||||||
|
// affect strict matching
|
||||||
|
CameraInfo info1 =
|
||||||
|
new CameraInfo(
|
||||||
|
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
|
||||||
|
CameraInfo info2 =
|
||||||
|
new CameraInfo(
|
||||||
|
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_NEW_PATH, 3141, 25446);
|
||||||
|
|
||||||
|
var cameraInfos = new ArrayList<CameraInfo>();
|
||||||
|
cameraInfos.add(info1);
|
||||||
|
cameraInfos.add(info2);
|
||||||
|
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||||
|
|
||||||
|
// Our cameras should be "known"
|
||||||
|
assertTrue(inst.knownCameras.contains(info1));
|
||||||
|
assertTrue(inst.knownCameras.contains(info2));
|
||||||
|
assertEquals(2, inst.knownCameras.size());
|
||||||
|
|
||||||
|
// And we should have matched one camera
|
||||||
|
assertEquals(1, ret1.size());
|
||||||
|
// and only matched camera1, not 2
|
||||||
|
assertEquals(
|
||||||
|
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
|
||||||
|
assertEquals(
|
||||||
|
0, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now move our camera back
|
||||||
|
{
|
||||||
|
CameraInfo info1 =
|
||||||
|
new CameraInfo(
|
||||||
|
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
|
||||||
|
CameraInfo info2 =
|
||||||
|
new CameraInfo(
|
||||||
|
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_OLD_PATH, 3141, 25446);
|
||||||
|
|
||||||
|
var cameraInfos = new ArrayList<CameraInfo>();
|
||||||
|
cameraInfos.add(info1);
|
||||||
|
cameraInfos.add(info2);
|
||||||
|
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||||
|
|
||||||
|
// and check the new one got matched got matched
|
||||||
|
assertEquals(1, ret1.size());
|
||||||
|
assertEquals(
|
||||||
|
0, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
|
||||||
|
assertEquals(
|
||||||
|
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIdenticalCameras() {
|
||||||
|
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||||
|
|
||||||
|
// List of known cameras
|
||||||
|
var cameraInfos = new ArrayList<CameraInfo>();
|
||||||
|
|
||||||
|
var inst = new VisionSourceManager();
|
||||||
|
ConfigManager.getInstance().clearConfig();
|
||||||
|
ConfigManager.getInstance().load();
|
||||||
|
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
|
||||||
|
|
||||||
|
// Match empty camera infos
|
||||||
|
inst.tryMatchCamImpl(cameraInfos);
|
||||||
|
|
||||||
|
CameraInfo info1 =
|
||||||
|
new CameraInfo(
|
||||||
|
0,
|
||||||
|
"/dev/video0",
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
new String[] {
|
||||||
|
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||||
|
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
|
||||||
|
},
|
||||||
|
3141,
|
||||||
|
25446);
|
||||||
|
CameraInfo info2 =
|
||||||
|
new CameraInfo(
|
||||||
|
0,
|
||||||
|
"/dev/video2",
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
new String[] {
|
||||||
|
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||||
|
"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"
|
||||||
|
},
|
||||||
|
3141,
|
||||||
|
25446);
|
||||||
|
|
||||||
|
cameraInfos.add(info1);
|
||||||
|
cameraInfos.add(info2);
|
||||||
|
|
||||||
|
// Match two "new" cameras
|
||||||
|
var ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||||
|
|
||||||
|
// Our cameras should be "known"
|
||||||
|
assertTrue(inst.knownCameras.contains(info1));
|
||||||
|
assertTrue(inst.knownCameras.contains(info2));
|
||||||
|
assertEquals(2, inst.knownCameras.size());
|
||||||
|
assertEquals(2, ret1.size());
|
||||||
|
|
||||||
|
// Exactly one camera should have the path we put in
|
||||||
|
for (int i = 0; i < cameraInfos.size(); i++) {
|
||||||
|
var testPath = cameraInfos.get(i).getUSBPath().get();
|
||||||
|
assertEquals(
|
||||||
|
1,
|
||||||
|
ret1.stream()
|
||||||
|
.filter(it -> testPath.equals(it.cameraConfiguration.getUSBPath().get()))
|
||||||
|
.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// and the names should be unique
|
||||||
|
for (int i = 0; i < ret1.size(); i++) {
|
||||||
|
var thisName = ret1.get(i).cameraConfiguration.uniqueName;
|
||||||
|
assertEquals(
|
||||||
|
1,
|
||||||
|
ret1.stream().filter(it -> thisName.equals(it.cameraConfiguration.uniqueName)).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplciate cameras, same info, new ref
|
||||||
|
var duplicateCameraInfos = new ArrayList<CameraInfo>();
|
||||||
|
CameraInfo info1_dup =
|
||||||
|
new CameraInfo(
|
||||||
|
0,
|
||||||
|
"/dev/video0",
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
new String[] {
|
||||||
|
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||||
|
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
|
||||||
|
},
|
||||||
|
3141,
|
||||||
|
25446);
|
||||||
|
CameraInfo info2_dup =
|
||||||
|
new CameraInfo(
|
||||||
|
0,
|
||||||
|
"/dev/video2",
|
||||||
|
"Arducam OV2311 USB Camera",
|
||||||
|
new String[] {
|
||||||
|
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||||
|
"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"
|
||||||
|
},
|
||||||
|
3141,
|
||||||
|
25446);
|
||||||
|
|
||||||
|
duplicateCameraInfos.add(info1_dup);
|
||||||
|
duplicateCameraInfos.add(info2_dup);
|
||||||
|
|
||||||
|
inst.tryMatchCamImpl(duplicateCameraInfos);
|
||||||
|
|
||||||
|
// Our cameras should be "known", and we should only "know" two cameras still
|
||||||
|
assertTrue(inst.knownCameras.contains(info1_dup));
|
||||||
|
assertTrue(inst.knownCameras.contains(info2_dup));
|
||||||
|
assertEquals(2, inst.knownCameras.size());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class PhotonCamera:
|
|||||||
retVal.populateFromPacket(pkt)
|
retVal.populateFromPacket(pkt)
|
||||||
# NT4 allows us to correct the timestamp based on when the message was sent
|
# NT4 allows us to correct the timestamp based on when the message was sent
|
||||||
retVal.setTimestampSeconds(
|
retVal.setTimestampSeconds(
|
||||||
timestamp / 1e-6 - retVal.getLatencyMillis() / 1e-3
|
timestamp / 1e6 - retVal.getLatencyMillis() / 1e3
|
||||||
)
|
)
|
||||||
return retVal
|
return retVal
|
||||||
|
|
||||||
|
|||||||
@@ -408,8 +408,8 @@ public class PhotonPoseEstimator {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (estimatedPose.isEmpty()) {
|
if (estimatedPose.isPresent()) {
|
||||||
lastPose = null;
|
lastPose = estimatedPose.get().estimatedPose;
|
||||||
}
|
}
|
||||||
|
|
||||||
return estimatedPose;
|
return estimatedPose;
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ public class PhotonCameraSim implements AutoCloseable {
|
|||||||
|
|
||||||
detectableTgts.add(
|
detectableTgts.add(
|
||||||
new PhotonTrackedTarget(
|
new PhotonTrackedTarget(
|
||||||
Math.toDegrees(centerRot.getZ()),
|
-Math.toDegrees(centerRot.getZ()),
|
||||||
-Math.toDegrees(centerRot.getY()),
|
-Math.toDegrees(centerRot.getY()),
|
||||||
areaPercent,
|
areaPercent,
|
||||||
Math.toDegrees(centerRot.getX()),
|
Math.toDegrees(centerRot.getX()),
|
||||||
|
|||||||
@@ -186,6 +186,9 @@ std::optional<EstimatedRobotPose> PhotonPoseEstimator::Update(
|
|||||||
ret = std::nullopt;
|
ret = std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ret) {
|
||||||
|
lastPose = ret.value().estimatedPose;
|
||||||
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ class PhotonCameraSim {
|
|||||||
std::vector<std::pair<double, double>> cornersDouble{cornersFloat.begin(),
|
std::vector<std::pair<double, double>> cornersDouble{cornersFloat.begin(),
|
||||||
cornersFloat.end()};
|
cornersFloat.end()};
|
||||||
detectableTgts.emplace_back(PhotonTrackedTarget{
|
detectableTgts.emplace_back(PhotonTrackedTarget{
|
||||||
centerRot.Z().convert<units::degrees>().to<double>(),
|
-centerRot.Z().convert<units::degrees>().to<double>(),
|
||||||
-centerRot.Y().convert<units::degrees>().to<double>(), areaPercent,
|
-centerRot.Y().convert<units::degrees>().to<double>(), areaPercent,
|
||||||
centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId,
|
centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId,
|
||||||
pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble});
|
pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble});
|
||||||
|
|||||||
@@ -256,7 +256,8 @@ class VisionSystemSimTest {
|
|||||||
cameraSim.setMinTargetAreaPixels(0.0);
|
cameraSim.setMinTargetAreaPixels(0.0);
|
||||||
visionSysSim.addVisionTargets(new VisionTargetSim(targetPose, new TargetModel(0.5, 0.5), 3));
|
visionSysSim.addVisionTargets(new VisionTargetSim(targetPose, new TargetModel(0.5, 0.5), 3));
|
||||||
|
|
||||||
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(-1.0 * testYaw));
|
// If the robot is rotated x deg (CCW+), the target yaw should be x deg (CW+)
|
||||||
|
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(testYaw));
|
||||||
visionSysSim.update(robotPose);
|
visionSysSim.update(robotPose);
|
||||||
var res = camera.getLatestResult();
|
var res = camera.getLatestResult();
|
||||||
assertTrue(res.hasTargets());
|
assertTrue(res.hasTargets());
|
||||||
|
|||||||
@@ -220,8 +220,9 @@ TEST_P(VisionSystemSimTestWithParamsTest, YawAngles) {
|
|||||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||||
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}});
|
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}});
|
||||||
|
|
||||||
robotPose = frc::Pose2d{frc::Translation2d{10_m, 0_m},
|
// If the robot is rotated x deg (CCW+), the target yaw should be x deg (CW+)
|
||||||
frc::Rotation2d{-1 * GetParam()}};
|
robotPose =
|
||||||
|
frc::Pose2d{frc::Translation2d{10_m, 0_m}, frc::Rotation2d{GetParam()}};
|
||||||
visionSysSim.Update(robotPose);
|
visionSysSim.Update(robotPose);
|
||||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||||
ASSERT_NEAR(GetParam().to<double>(),
|
ASSERT_NEAR(GetParam().to<double>(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "cpp"
|
id "cpp"
|
||||||
id "google-test-test-suite"
|
id "google-test-test-suite"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
|
|
||||||
id "com.dorongold.task-tree" version "2.1.0"
|
id "com.dorongold.task-tree" version "2.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "cpp"
|
id "cpp"
|
||||||
id "google-test-test-suite"
|
id "google-test-test-suite"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
|
|
||||||
id "com.dorongold.task-tree" version "2.1.0"
|
id "com.dorongold.task-tree" version "2.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ class Robot : public frc::TimedRobot {
|
|||||||
void TeleopPeriodic() override;
|
void TeleopPeriodic() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Change this to match the name of your camera
|
// Change this to match the name of your camera as shown in the web UI
|
||||||
photon::PhotonCamera camera{"photonvision"};
|
photon::PhotonCamera camera{"YOUR_CAMERA_NAME_HERE"};
|
||||||
// PID constants should be tuned per robot
|
// PID constants should be tuned per robot
|
||||||
frc::PIDController controller{.1, 0, 0};
|
frc::PIDController controller{.1, 0, 0};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "cpp"
|
id "cpp"
|
||||||
id "google-test-test-suite"
|
id "google-test-test-suite"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
|
|
||||||
id "com.dorongold.task-tree" version "2.1.0"
|
id "com.dorongold.task-tree" version "2.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "cpp"
|
id "cpp"
|
||||||
id "google-test-test-suite"
|
id "google-test-test-suite"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
|
|
||||||
id "com.dorongold.task-tree" version "2.1.0"
|
id "com.dorongold.task-tree" version "2.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "cpp"
|
id "cpp"
|
||||||
id "google-test-test-suite"
|
id "google-test-test-suite"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
|
|
||||||
id "com.dorongold.task-tree" version "2.1.0"
|
id "com.dorongold.task-tree" version "2.1.0"
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,8 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wpi.maven.useDevelopment = true
|
wpi.maven.useDevelopment = true
|
||||||
wpi.versions.wpilibVersion = "2024.2.1"
|
wpi.versions.wpilibVersion = "2024.3.1"
|
||||||
wpi.versions.wpimathVersion = "2024.2.1"
|
wpi.versions.wpimathVersion = "2024.3.1"
|
||||||
|
|
||||||
apply from: "${rootDir}/../shared/examples_common.gradle"
|
apply from: "${rootDir}/../shared/examples_common.gradle"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ public class Robot extends TimedRobot {
|
|||||||
// How far from the target we want to be
|
// How far from the target we want to be
|
||||||
final double GOAL_RANGE_METERS = Units.feetToMeters(3);
|
final double GOAL_RANGE_METERS = Units.feetToMeters(3);
|
||||||
|
|
||||||
// Change this to match the name of your camera
|
// Change this to match the name of your camera as shown in the web UI
|
||||||
PhotonCamera camera = new PhotonCamera("photonvision");
|
PhotonCamera camera = new PhotonCamera("YOUR_CAMERA_NAME_HERE");
|
||||||
|
|
||||||
// PID constants should be tuned per robot
|
// PID constants should be tuned per robot
|
||||||
final double LINEAR_P = 0.1;
|
final double LINEAR_P = 0.1;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "edu.wpi.first.GradleRIO" version "2024.2.1"
|
id "edu.wpi.first.GradleRIO" version "2024.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
@@ -11,8 +11,8 @@ apply from: "${rootDir}/../shared/examples_common.gradle"
|
|||||||
def ROBOT_MAIN_CLASS = "frc.robot.Main"
|
def ROBOT_MAIN_CLASS = "frc.robot.Main"
|
||||||
|
|
||||||
wpi.maven.useDevelopment = true
|
wpi.maven.useDevelopment = true
|
||||||
wpi.versions.wpilibVersion = "2024.2.1"
|
wpi.versions.wpilibVersion = "2024.3.1"
|
||||||
wpi.versions.wpimathVersion = "2024.2.1"
|
wpi.versions.wpimathVersion = "2024.3.1"
|
||||||
|
|
||||||
|
|
||||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ if [[ "$DISTRO" = "Ubuntu" && "$INSTALL_NETWORK_MANAGER" != "true" && -z "$QUIET
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Update package list"
|
||||||
|
apt-get update
|
||||||
|
|
||||||
echo "Installing curl..."
|
echo "Installing curl..."
|
||||||
apt-get install --yes curl
|
apt-get install --yes curl
|
||||||
echo "curl installation complete."
|
echo "curl installation complete."
|
||||||
@@ -136,6 +139,9 @@ echo "Installing v4l-utils..."
|
|||||||
apt-get install --yes v4l-utils
|
apt-get install --yes v4l-utils
|
||||||
echo "v4l-utils installation complete."
|
echo "v4l-utils installation complete."
|
||||||
|
|
||||||
|
echo "Installing sqlite3"
|
||||||
|
apt-get install --yes sqlite3
|
||||||
|
|
||||||
echo "Downloading latest stable release of PhotonVision..."
|
echo "Downloading latest stable release of PhotonVision..."
|
||||||
mkdir -p /opt/photonvision
|
mkdir -p /opt/photonvision
|
||||||
cd /opt/photonvision
|
cd /opt/photonvision
|
||||||
|
|||||||
Reference in New Issue
Block a user