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;
019
020import edu.wpi.first.hal.HAL;
021import java.io.IOException;
022import java.nio.file.Path;
023import java.util.ArrayList;
024import java.util.List;
025import org.apache.commons.cli.*;
026import org.photonvision.common.configuration.CameraConfiguration;
027import org.photonvision.common.configuration.ConfigManager;
028import org.photonvision.common.configuration.NeuralNetworkModelManager;
029import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
030import org.photonvision.common.hardware.HardwareManager;
031import org.photonvision.common.hardware.OsImageVersion;
032import org.photonvision.common.hardware.PiVersion;
033import org.photonvision.common.hardware.Platform;
034import org.photonvision.common.logging.KernelLogLogger;
035import org.photonvision.common.logging.LogGroup;
036import org.photonvision.common.logging.LogLevel;
037import org.photonvision.common.logging.Logger;
038import org.photonvision.common.logging.PvCSCoreLogger;
039import org.photonvision.common.networking.NetworkManager;
040import org.photonvision.common.util.TestUtils;
041import org.photonvision.jni.PhotonTargetingJniLoader;
042import org.photonvision.jni.RknnDetectorJNI;
043import org.photonvision.mrcal.MrCalJNILoader;
044import org.photonvision.raspi.LibCameraJNILoader;
045import org.photonvision.server.Server;
046import org.photonvision.vision.apriltag.AprilTagFamily;
047import org.photonvision.vision.camera.PVCameraInfo;
048import org.photonvision.vision.opencv.CVMat;
049import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
050import org.photonvision.vision.pipeline.CVPipelineSettings;
051import org.photonvision.vision.pipeline.PipelineProfiler;
052import org.photonvision.vision.processes.VisionSourceManager;
053import org.photonvision.vision.target.TargetModel;
054
055public class Main {
056    public static final int DEFAULT_WEBPORT = 5800;
057
058    private static final Logger logger = new Logger(Main.class, LogGroup.General);
059    private static final boolean isRelease = PhotonVersion.isRelease;
060
061    private static boolean isTestMode = false;
062    private static boolean isSmoketest = false;
063    private static Path testModeFolder = null;
064    private static boolean printDebugLogs;
065
066    private static boolean handleArgs(String[] args) throws ParseException {
067        final var options = new Options();
068        options.addOption("d", "debug", false, "Enable debug logging prints");
069        options.addOption("h", "help", false, "Show this help text and exit");
070        options.addOption(
071                "t",
072                "test-mode",
073                false,
074                "Run in test mode with 2019 and 2020 WPI field images in place of cameras");
075
076        options.addOption("p", "path", true, "Point test mode to a specific folder");
077        options.addOption("n", "disable-networking", false, "Disables control device network settings");
078        options.addOption(
079                "c",
080                "clear-config",
081                false,
082                "Clears PhotonVision pipeline and networking settings. Preserves log files");
083        options.addOption(
084                "s",
085                "smoketest",
086                false,
087                "Exit Photon after loading native libraries and camera configs, but before starting up camera runners");
088
089        CommandLineParser parser = new DefaultParser();
090        CommandLine cmd = parser.parse(options, args);
091
092        if (cmd.hasOption("help")) {
093            HelpFormatter formatter = new HelpFormatter();
094            formatter.printHelp("java -jar photonvision.jar [options]", options);
095            return false; // exit program
096        } else {
097            if (cmd.hasOption("debug")) {
098                printDebugLogs = true;
099                logger.info("Enabled debug logging");
100            }
101
102            if (cmd.hasOption("test-mode")) {
103                isTestMode = true;
104                logger.info("Running in test mode - Cameras will not be used");
105
106                if (cmd.hasOption("path")) {
107                    Path p = Path.of(System.getProperty("PATH_PREFIX", "") + cmd.getOptionValue("path"));
108                    logger.info("Loading from Path " + p.toAbsolutePath().toString());
109                    testModeFolder = p;
110                }
111            }
112
113            if (cmd.hasOption("disable-networking")) {
114                NetworkManager.getInstance().networkingIsDisabled = true;
115            }
116
117            if (cmd.hasOption("clear-config")) {
118                ConfigManager.getInstance().clearConfig();
119            }
120
121            if (cmd.hasOption("smoketest")) {
122                isSmoketest = true;
123            }
124        }
125        return true;
126    }
127
128    private static void addTestModeSources() {
129        ConfigManager.getInstance().load();
130
131        CameraConfiguration camConf2024 =
132                ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2024");
133        if (camConf2024 == null || true) {
134            camConf2024 =
135                    new CameraConfiguration(
136                            PVCameraInfo.fromFileInfo(
137                                    TestUtils.getResourcesFolderPath(true)
138                                            .resolve("testimages")
139                                            .resolve(TestUtils.WPI2024Images.kSpeakerCenter_143in.path)
140                                            .toString(),
141                                    "WPI2024"));
142
143            camConf2024.FOV = TestUtils.WPI2024Images.FOV;
144            // same camera as 2023
145            camConf2024.calibrations.add(TestUtils.get2023LifeCamCoeffs(true));
146
147            var pipeline2024 = new AprilTagPipelineSettings();
148            var path_split = Path.of(camConf2024.matchedCameraInfo.path()).getFileName().toString();
149            pipeline2024.pipelineNickname = path_split.replace(".jpg", "");
150            pipeline2024.targetModel = TargetModel.kAprilTag6p5in_36h11;
151            pipeline2024.tagFamily = AprilTagFamily.kTag36h11;
152            pipeline2024.inputShouldShow = true;
153            pipeline2024.solvePNPEnabled = true;
154
155            var psList2024 = new ArrayList<CVPipelineSettings>();
156            psList2024.add(pipeline2024);
157            camConf2024.pipelineSettings = psList2024;
158        }
159
160        var cameraConfigs = List.of(camConf2024);
161
162        ConfigManager.getInstance().unloadCameraConfigs();
163        cameraConfigs.stream().forEach(ConfigManager.getInstance()::addCameraConfiguration);
164        VisionSourceManager.getInstance().registerLoadedConfigs(cameraConfigs);
165    }
166
167    public static void main(String[] args) {
168        logger.info(
169                "Starting PhotonVision version "
170                        + PhotonVersion.versionString
171                        + " on platform "
172                        + Platform.getPlatformName()
173                        + (Platform.isRaspberryPi() ? (" (Pi " + PiVersion.getPiVersion() + ")") : ""));
174
175        if (OsImageVersion.IMAGE_VERSION.isPresent()) {
176            logger.info("PhotonVision image version: " + OsImageVersion.IMAGE_VERSION.get());
177        }
178
179        try {
180            if (!handleArgs(args)) {
181                System.exit(1);
182            }
183        } catch (ParseException e) {
184            logger.error("Failed to parse command-line options!", e);
185        }
186
187        // We don't want to trigger an exit in test mode or smoke test. This is specifically for MacOS.
188        if (!(Platform.isSupported() || isSmoketest || isTestMode)) {
189            logger.error("This platform is unsupported!");
190            System.exit(1);
191        }
192
193        try {
194            boolean success = TestUtils.loadLibraries();
195
196            if (!success) {
197                logger.error("Failed to load native libraries! Giving up :(");
198                System.exit(1);
199            }
200        } catch (Exception e) {
201            logger.error("Failed to load native libraries!", e);
202            System.exit(1);
203        }
204        logger.info("WPI JNI libraries loaded.");
205
206        try {
207            boolean success = PhotonTargetingJniLoader.load();
208
209            if (!success) {
210                logger.error("Failed to load native libraries! Giving up :(");
211                System.exit(1);
212            }
213        } catch (Exception e) {
214            logger.error("Failed to load photon-targeting JNI!", e);
215            System.exit(1);
216        }
217        logger.info("photon-targeting JNI libraries loaded.");
218
219        if (!HAL.initialize(500, 0)) {
220            logger.error("Failed to initialize the HAL! Giving up :(");
221            System.exit(1);
222        }
223
224        try {
225            if (Platform.isRaspberryPi()) {
226                LibCameraJNILoader.forceLoad();
227            }
228        } catch (IOException e) {
229            logger.error("Failed to load libcamera-JNI!", e);
230        }
231        try {
232            if (Platform.isRK3588()) {
233                RknnDetectorJNI.forceLoad();
234            } else {
235                logger.error("Platform does not support RKNN based machine learning!");
236            }
237        } catch (IOException e) {
238            logger.error("Failed to load rknn-JNI!", e);
239        }
240        try {
241            MrCalJNILoader.forceLoad();
242        } catch (IOException e) {
243            logger.warn(
244                    "Failed to load mrcal-JNI! Camera calibration will fall back to opencv\n"
245                            + e.getMessage());
246        }
247
248        CVMat.enablePrint(false);
249        PipelineProfiler.enablePrint(false);
250
251        var logLevel = printDebugLogs ? LogLevel.TRACE : LogLevel.DEBUG;
252        Logger.setLevel(LogGroup.Camera, logLevel);
253        Logger.setLevel(LogGroup.WebServer, logLevel);
254        Logger.setLevel(LogGroup.VisionModule, logLevel);
255        Logger.setLevel(LogGroup.Data, logLevel);
256        Logger.setLevel(LogGroup.Config, logLevel);
257        Logger.setLevel(LogGroup.General, logLevel);
258        logger.info("Logging initialized in debug mode.");
259
260        // Add Linux kernel log->Photon logger
261        KernelLogLogger.getInstance();
262
263        // Add CSCore->Photon logger
264        PvCSCoreLogger.getInstance();
265
266        logger.debug("Loading ConfigManager...");
267        ConfigManager.getInstance().load(); // init config manager
268        ConfigManager.getInstance().requestSave();
269
270        logger.info("Loading ML models...");
271        var modelManager = NeuralNetworkModelManager.getInstance();
272        modelManager.extractModels(ConfigManager.getInstance().getModelsDirectory());
273        modelManager.discoverModels(ConfigManager.getInstance().getModelsDirectory());
274
275        logger.debug("Loading HardwareManager...");
276        // Force load the hardware manager
277        HardwareManager.getInstance();
278
279        logger.debug("Loading NetworkManager...");
280        NetworkManager.getInstance().reinitialize();
281
282        logger.debug("Loading NetworkTablesManager...");
283        NetworkTablesManager.getInstance()
284                .setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
285        NetworkTablesManager.getInstance().registerTimedTasks();
286
287        if (isSmoketest) {
288            logger.info("PhotonVision base functionality loaded -- smoketest complete");
289            System.exit(0);
290        }
291
292        // todo - should test mode just add test mode sources, but still allow local usb cameras to be
293        // added?
294        if (!isTestMode) {
295            logger.debug("Loading VisionSourceManager...");
296            VisionSourceManager.getInstance()
297                    .registerLoadedConfigs(
298                            ConfigManager.getInstance().getConfig().getCameraConfigurations().values());
299        } else {
300            if (testModeFolder == null) {
301                addTestModeSources();
302            }
303        }
304
305        VisionSourceManager.getInstance().registerTimedTasks();
306
307        logger.info("Starting server...");
308        HardwareManager.getInstance().setRunning(true);
309        Server.initialize(DEFAULT_WEBPORT);
310    }
311}