001/*
002 * Copyright (C) Photon Vision.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package org.photonvision.vision.camera.USBCameras;
019
020import edu.wpi.first.cameraserver.CameraServer;
021import edu.wpi.first.cscore.UsbCamera;
022import edu.wpi.first.cscore.VideoException;
023import edu.wpi.first.cscore.VideoProperty;
024import java.util.*;
025import org.photonvision.common.configuration.CameraConfiguration;
026import org.photonvision.common.hardware.Platform;
027import org.photonvision.common.logging.LogGroup;
028import org.photonvision.common.logging.Logger;
029import org.photonvision.vision.camera.CameraQuirk;
030import org.photonvision.vision.camera.PVCameraInfo.PVUsbCameraInfo;
031import org.photonvision.vision.camera.QuirkyCamera;
032import org.photonvision.vision.frame.FrameProvider;
033import org.photonvision.vision.frame.provider.USBFrameProvider;
034import org.photonvision.vision.processes.VisionSource;
035import org.photonvision.vision.processes.VisionSourceSettables;
036
037public class USBCameraSource extends VisionSource {
038    private final Logger logger;
039    private final UsbCamera camera;
040    protected GenericUSBCameraSettables settables;
041    protected FrameProvider usbFrameProvider;
042
043    private void onCameraConnected() {
044        // Aid to the development team - record the properties available for whatever the user plugged
045        // in
046        printCameraProperaties();
047
048        settables.onCameraConnected();
049    }
050
051    public USBCameraSource(CameraConfiguration config) {
052        super(config);
053
054        logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
055
056        if (!(config.matchedCameraInfo instanceof PVUsbCameraInfo)) {
057            logger.error(
058                    "USBCameraSource matched to a non-USB camera info?? "
059                            + config.matchedCameraInfo.toString());
060        }
061
062        camera = new UsbCamera(config.nickname, config.getDevicePath());
063
064        // TODO - I don't need this, do I?
065        // // set vid/pid if not done already for future matching
066        // if (config.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
067        // if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
068
069        // TODO - why do we delegate this to USBCameraSource? Quirks are part of the CameraConfig??
070        // also TODO - is the config's saved usb info a reasonable guess for quirk detection? seems like
071        // yes to me...
072        if (getCameraConfiguration().cameraQuirks == null) {
073            int vid =
074                    (config.matchedCameraInfo instanceof PVUsbCameraInfo)
075                            ? ((PVUsbCameraInfo) config.matchedCameraInfo).vendorId
076                            : -1;
077            int pid =
078                    (config.matchedCameraInfo instanceof PVUsbCameraInfo)
079                            ? ((PVUsbCameraInfo) config.matchedCameraInfo).productId
080                            : -1;
081
082            getCameraConfiguration().cameraQuirks =
083                    QuirkyCamera.getQuirkyCamera(vid, pid, config.matchedCameraInfo.name());
084        }
085
086        if (getCameraConfiguration().cameraQuirks.hasQuirks()) {
087            logger.info("Quirky camera detected: " + getCameraConfiguration().cameraQuirks);
088        }
089
090        var cameraBroken = getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken);
091
092        if (cameraBroken) {
093            // Known issues - Disable this camera
094            logger.info(
095                    "Camera "
096                            + getCameraConfiguration().cameraQuirks.baseName
097                            + " is not supported for PhotonVision");
098            // set some defaults, as these should never be used.
099            settables = null;
100            usbFrameProvider = null;
101
102        } else {
103            // Camera is likely to work, set up the Settables
104            settables = createSettables(config, camera);
105            logger.info("Created settables " + settables);
106
107            usbFrameProvider = new USBFrameProvider(camera, settables, this::onCameraConnected);
108        }
109    }
110
111    /**
112     * Factory for making appropriate settables
113     *
114     * @param config
115     * @param camera
116     * @return
117     */
118    protected GenericUSBCameraSettables createSettables(
119            CameraConfiguration config, UsbCamera camera) {
120        var quirks = getCameraConfiguration().cameraQuirks;
121
122        GenericUSBCameraSettables settables;
123
124        if (quirks.hasQuirk(CameraQuirk.LifeCamControls)) {
125            if (Platform.isWindows()) {
126                logger.debug("Using Microsoft Lifecam 3000 Windows-Specific Settables");
127                settables = new LifeCam3kWindowsCameraSettables(config, camera);
128            } else {
129                logger.debug("Using Microsoft Lifecam 3000 Settables");
130                settables = new LifeCam3kCameraSettables(config, camera);
131            }
132        } else if (quirks.hasQuirk(CameraQuirk.PsEyeControls)) {
133            logger.debug("Using PlayStation Eye Camera Settables");
134            settables = new PsEyeCameraSettables(config, camera);
135        } else if (quirks.hasQuirk(CameraQuirk.ArduOV2311Controls)) {
136            if (Platform.isWindows()) {
137                logger.debug("Using Arducam OV2311 Windows-Specific Settables");
138                settables = new ArduOV2311WindowsCameraSettables(config, camera);
139            } else {
140                logger.debug("Using Arducam OV2311 Settables");
141                settables = new ArduOV2311CameraSettables(config, camera);
142            }
143        } else if (quirks.hasQuirk(CameraQuirk.ArduOV9281Controls)) {
144            logger.debug("Using Arducam OV9281 Settables");
145            settables = new InnoOV9281CameraSettables(config, camera);
146        } else if (quirks.hasQuirk(CameraQuirk.ArduOV9782Controls)) {
147            logger.debug("Using Arducam OV9782 Settables");
148            settables = new ArduOV9782CameraSettables(config, camera);
149        } else if (quirks.hasQuirk(CameraQuirk.InnoOV9281Controls)) {
150            settables = new InnoOV9281CameraSettables(config, camera);
151        } else if (quirks.hasQuirk(CameraQuirk.See3Cam_24CUG)) {
152            settables = new See3Cam24CUGSettables(config, camera);
153        } else {
154            logger.debug("Using Generic USB Cam Settables");
155            settables = new GenericUSBCameraSettables(config, camera);
156        }
157
158        return settables;
159    }
160
161    /**
162     * Must be called after createSettables Using the current config/camera and modified quirks, make
163     * a new settables
164     */
165    public void remakeSettables() {
166        var oldConfig = this.cameraConfiguration;
167        var oldCamera = this.camera;
168
169        // Re-create settables
170        var oldVideoMode = this.settables.getCurrentVideoMode();
171        this.settables = createSettables(oldConfig, oldCamera);
172
173        // Settables only cache videomodes on connect - force this to happen next tick
174        if (settables.camera.isConnected()) {
175            this.settables.onCameraConnected();
176        } else {
177            this.usbFrameProvider.cameraPropertiesCached = false;
178        }
179
180        // And update the settables' FrameStaticProps
181        settables.setVideoMode(oldVideoMode);
182
183        // Propagate our updated settables over to the frame provider
184        ((USBFrameProvider) this.usbFrameProvider).updateSettables(this.settables);
185    }
186
187    private void printCameraProperaties() {
188        VideoProperty[] cameraProperties = null;
189        try {
190            cameraProperties = camera.enumerateProperties();
191        } catch (VideoException e) {
192            logger.error("Failed to list camera properties!", e);
193        }
194
195        if (cameraProperties != null) {
196            String cameraPropertiesStr = "Cam Properties Dump:\n";
197            for (int i = 0; i < cameraProperties.length; i++) {
198                cameraPropertiesStr +=
199                        "Name: "
200                                + cameraProperties[i].getName()
201                                + ", Kind: "
202                                + cameraProperties[i].getKind()
203                                + ", Value: "
204                                + cameraProperties[i].getKind().getValue()
205                                + ", Min: "
206                                + cameraProperties[i].getMin()
207                                + ", Max: "
208                                + cameraProperties[i].getMax()
209                                + ", Dflt: "
210                                + cameraProperties[i].getDefault()
211                                + ", Step: "
212                                + cameraProperties[i].getStep()
213                                + "\n";
214            }
215            logger.debug(cameraPropertiesStr);
216        }
217    }
218
219    public QuirkyCamera getCameraQuirks() {
220        return getCameraConfiguration().cameraQuirks;
221    }
222
223    @Override
224    public FrameProvider getFrameProvider() {
225        return usbFrameProvider;
226    }
227
228    @Override
229    public VisionSourceSettables getSettables() {
230        return this.settables;
231    }
232
233    @Override
234    public boolean isVendorCamera() {
235        return false; // Vendors do not supply USB Cameras
236    }
237
238    @Override
239    public boolean hasLEDs() {
240        return false; // Assume USB cameras do not have photonvision-controlled LEDs
241    }
242
243    @Override
244    public void release() {
245        CameraServer.removeCamera(camera.getName());
246        camera.close();
247        usbFrameProvider.release();
248        usbFrameProvider = null;
249    }
250
251    @Override
252    public boolean equals(Object obj) {
253        if (this == obj) return true;
254        if (obj == null) return false;
255        if (getClass() != obj.getClass()) return false;
256        USBCameraSource other = (USBCameraSource) obj;
257        if (camera == null) {
258            if (other.camera != null) return false;
259        } else if (!camera.equals(other.camera)) return false;
260        if (settables == null) {
261            if (other.settables != null) return false;
262        } else if (!settables.equals(other.settables)) return false;
263        if (usbFrameProvider == null) {
264            if (other.usbFrameProvider != null) return false;
265        } else if (!usbFrameProvider.equals(other.usbFrameProvider)) return false;
266        if (getCameraConfiguration().cameraQuirks == null) {
267            if (other.getCameraConfiguration().cameraQuirks != null) return false;
268        } else if (!getCameraConfiguration()
269                .cameraQuirks
270                .equals(other.getCameraConfiguration().cameraQuirks)) return false;
271        return true;
272    }
273
274    @Override
275    public int hashCode() {
276        return Objects.hash(
277                camera,
278                settables,
279                usbFrameProvider,
280                cameraConfiguration,
281                getCameraConfiguration().cameraQuirks);
282    }
283}