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.processes;
019
020import java.util.ArrayList;
021import java.util.List;
022import java.util.concurrent.Callable;
023import java.util.concurrent.CompletableFuture;
024import java.util.concurrent.Future;
025import java.util.function.Consumer;
026import java.util.function.Supplier;
027import org.photonvision.common.configuration.ConfigManager;
028import org.photonvision.common.dataflow.DataChangeService;
029import org.photonvision.common.dataflow.events.OutgoingUIEvent;
030import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
031import org.photonvision.common.logging.LogGroup;
032import org.photonvision.common.logging.Logger;
033import org.photonvision.vision.camera.QuirkyCamera;
034import org.photonvision.vision.frame.Frame;
035import org.photonvision.vision.frame.FrameProvider;
036import org.photonvision.vision.pipe.impl.HSVPipe;
037import org.photonvision.vision.pipeline.AdvancedPipelineSettings;
038import org.photonvision.vision.pipeline.CVPipeline;
039import org.photonvision.vision.pipeline.result.CVPipelineResult;
040
041/** VisionRunner has a frame supplier, a pipeline supplier, and a result consumer */
042@SuppressWarnings("rawtypes")
043public class VisionRunner {
044    private final Logger logger;
045    private final Thread visionProcessThread;
046    private final FrameProvider frameSupplier;
047    private final Supplier<CVPipeline> pipelineSupplier;
048    private final Consumer<CVPipelineResult> pipelineResultConsumer;
049    private final VisionModuleChangeSubscriber changeSubscriber;
050    private final List<Runnable> runnableList = new ArrayList<Runnable>();
051    private final QuirkyCamera cameraQuirks;
052
053    private long loopCount;
054
055    /**
056     * VisionRunner contains a thread to run a pipeline, given a frame, and will give the result to
057     * the consumer.
058     *
059     * @param frameSupplier The supplier of the latest frame.
060     * @param pipelineSupplier The supplier of the current pipeline.
061     * @param pipelineResultConsumer The consumer of the latest result.
062     */
063    public VisionRunner(
064            FrameProvider frameSupplier,
065            Supplier<CVPipeline> pipelineSupplier,
066            Consumer<CVPipelineResult> pipelineResultConsumer,
067            QuirkyCamera cameraQuirks,
068            VisionModuleChangeSubscriber changeSubscriber) {
069        this.frameSupplier = frameSupplier;
070        this.pipelineSupplier = pipelineSupplier;
071        this.pipelineResultConsumer = pipelineResultConsumer;
072        this.cameraQuirks = cameraQuirks;
073        this.changeSubscriber = changeSubscriber;
074
075        visionProcessThread = new Thread(this::update);
076        visionProcessThread.setName("VisionRunner - " + frameSupplier.getName());
077        logger = new Logger(VisionRunner.class, frameSupplier.getName(), LogGroup.VisionModule);
078        changeSubscriber.processSettingChanges();
079    }
080
081    public void startProcess() {
082        visionProcessThread.start();
083    }
084
085    public void stopProcess() {
086        try {
087            System.out.println("Interrupting vision process thread");
088            visionProcessThread.interrupt();
089            visionProcessThread.join();
090        } catch (InterruptedException e) {
091            logger.error("Exception killing process thread", e);
092        }
093    }
094
095    public Future<Void> runSynchronously(Runnable runnable) {
096        CompletableFuture<Void> future = new CompletableFuture<>();
097
098        synchronized (runnableList) {
099            runnableList.add(
100                    () -> {
101                        try {
102                            runnable.run();
103                            future.complete(null);
104                        } catch (Exception ex) {
105                            future.completeExceptionally(ex);
106                        }
107                    });
108        }
109        return future;
110    }
111
112    public <T> Future<T> runSynchronously(Callable<T> callable) {
113        CompletableFuture<T> future = new CompletableFuture<>();
114
115        synchronized (runnableList) {
116            runnableList.add(
117                    () -> {
118                        try {
119                            T result = callable.call();
120                            future.complete(result);
121                        } catch (Exception ex) {
122                            future.completeExceptionally(ex);
123                        }
124                    });
125        }
126
127        return future;
128    }
129
130    private void update() {
131        // wait for the camera to connect
132        while (!frameSupplier.checkCameraConnected() && !Thread.interrupted()) {
133            // yield
134            pipelineResultConsumer.accept(new CVPipelineResult(0l, 0, 0, null, new Frame()));
135            try {
136                Thread.sleep(100);
137            } catch (InterruptedException e) {
138                return;
139            }
140        }
141
142        DataChangeService.getInstance()
143                .publishEvent(
144                        new OutgoingUIEvent<>(
145                                "fullsettings",
146                                UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
147
148        while (!Thread.interrupted()) {
149            changeSubscriber.processSettingChanges();
150            synchronized (runnableList) {
151                for (var runnable : runnableList) {
152                    try {
153                        runnable.run();
154                    } catch (Exception ex) {
155                        logger.error("Exception running runnable", ex);
156                    }
157                }
158                runnableList.clear();
159            }
160
161            var pipeline = pipelineSupplier.get();
162
163            // Tell our camera implementation here what kind of pre-processing we need it to
164            // be doing
165            // (pipeline-dependent). I kinda hate how much leak this has...
166            // TODO would a callback object be a better fit?
167            var wantedProcessType = pipeline.getThresholdType();
168
169            frameSupplier.requestFrameThresholdType(wantedProcessType);
170            var settings = pipeline.getSettings();
171            if (settings instanceof AdvancedPipelineSettings advanced) {
172                var hsvParams =
173                        new HSVPipe.HSVParams(
174                                advanced.hsvHue, advanced.hsvSaturation, advanced.hsvValue, advanced.hueInverted);
175                // TODO who should deal with preventing this from happening _every single loop_?
176                frameSupplier.requestHsvSettings(hsvParams);
177            }
178            frameSupplier.requestFrameRotation(settings.inputImageRotationMode);
179            frameSupplier.requestFrameCopies(settings.inputShouldShow, settings.outputShouldShow);
180
181            // Grab the new camera frame
182            var frame = frameSupplier.get();
183
184            // Frame empty -- no point in trying to do anything more?
185            if (frame.processedImage.getMat().empty() && frame.colorImage.getMat().empty()) {
186                // give up without increasing loop count
187                // Still feed with blank frames just dont run any pipelines
188
189                pipelineResultConsumer.accept(new CVPipelineResult(0l, 0, 0, null, new Frame()));
190                continue;
191            }
192
193            // If the pipeline has changed while we are getting our frame we should scrap
194            // that frame it
195            // may result in incorrect frame settings like hsv values
196            if (pipeline == pipelineSupplier.get()) {
197                // There's no guarantee the processing type change will occur this tick, so
198                // pipelines should
199                // check themselves
200                try {
201                    var pipelineResult = pipeline.run(frame, cameraQuirks);
202                    pipelineResultConsumer.accept(pipelineResult);
203                } catch (Exception ex) {
204                    logger.error("Exception on loop " + loopCount, ex);
205                }
206            }
207
208            loopCount++;
209        }
210    }
211}