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.Map; 023import java.util.concurrent.locks.ReentrantLock; 024import org.apache.commons.lang3.tuple.Pair; 025import org.opencv.core.Point; 026import org.photonvision.common.dataflow.DataChangeSubscriber; 027import org.photonvision.common.dataflow.events.DataChangeEvent; 028import org.photonvision.common.dataflow.events.IncomingWebSocketEvent; 029import org.photonvision.common.logging.LogGroup; 030import org.photonvision.common.logging.Logger; 031import org.photonvision.common.util.file.JacksonUtils; 032import org.photonvision.common.util.numbers.DoubleCouple; 033import org.photonvision.common.util.numbers.IntegerCouple; 034import org.photonvision.vision.calibration.CameraCalibrationCoefficients; 035import org.photonvision.vision.pipeline.AdvancedPipelineSettings; 036import org.photonvision.vision.pipeline.PipelineType; 037import org.photonvision.vision.pipeline.UICalibrationData; 038import org.photonvision.vision.target.RobotOffsetPointOperation; 039 040@SuppressWarnings("unchecked") 041public class VisionModuleChangeSubscriber extends DataChangeSubscriber { 042 private final VisionModule parentModule; 043 private final Logger logger; 044 private List<VisionModuleChange<?>> settingChanges = new ArrayList<>(); 045 private final ReentrantLock changeListLock = new ReentrantLock(); 046 047 public VisionModuleChangeSubscriber(VisionModule parentModule) { 048 this.parentModule = parentModule; 049 logger = 050 new Logger( 051 VisionModuleChangeSubscriber.class, 052 parentModule.visionSource.getSettables().getConfiguration().nickname, 053 LogGroup.VisionModule); 054 } 055 056 @Override 057 public void onDataChangeEvent(DataChangeEvent<?> event) { 058 if (event instanceof IncomingWebSocketEvent wsEvent) { 059 // Camera index -1 means a "multicast event" (i.e. the event is received by all cameras) 060 if (wsEvent.cameraUniqueName != null 061 && wsEvent.cameraUniqueName.equals(parentModule.uniqueName())) { 062 logger.trace("Got PSC event - propName: " + wsEvent.propertyName); 063 changeListLock.lock(); 064 try { 065 getSettingChanges() 066 .add( 067 new VisionModuleChange( 068 wsEvent.propertyName, 069 wsEvent.data, 070 parentModule.pipelineManager.getCurrentPipeline().getSettings(), 071 wsEvent.originContext)); 072 } finally { 073 changeListLock.unlock(); 074 } 075 } 076 } 077 } 078 079 public List<VisionModuleChange<?>> getSettingChanges() { 080 return settingChanges; 081 } 082 083 public void processSettingChanges() { 084 // special case for non-PipelineSetting changes 085 changeListLock.lock(); 086 try { 087 for (var change : settingChanges) { 088 var propName = change.getPropName(); 089 var newPropValue = change.getNewPropValue(); 090 var currentSettings = change.getCurrentSettings(); 091 var originContext = change.getOriginContext(); 092 boolean handled = true; 093 switch (propName) { 094 case "pipelineName" -> { 095 newPipelineNickname((String) newPropValue); 096 continue; 097 } 098 case "newPipelineInfo" -> newPipelineInfo((Pair<String, PipelineType>) newPropValue); 099 case "deleteCurrPipeline" -> deleteCurrPipeline(); 100 case "changePipeline" -> changePipeline((Integer) newPropValue); 101 case "startCalibration" -> startCalibration((Map<String, Object>) newPropValue); 102 case "saveInputSnapshot" -> parentModule.saveInputSnapshot(); 103 case "saveOutputSnapshot" -> parentModule.saveOutputSnapshot(); 104 case "takeCalSnapshot" -> parentModule.takeCalibrationSnapshot(); 105 case "duplicatePipeline" -> duplicatePipeline((Integer) newPropValue); 106 case "calibrationUploaded" -> { 107 if (newPropValue instanceof CameraCalibrationCoefficients newCal) { 108 parentModule.addCalibrationToConfig(newCal); 109 } else { 110 logger.warn("Received invalid calibration data"); 111 } 112 } 113 case "robotOffsetPoint" -> { 114 if (currentSettings instanceof AdvancedPipelineSettings curAdvSettings) { 115 robotOffsetPoint(curAdvSettings, (Integer) newPropValue); 116 } 117 } 118 case "changePipelineType" -> { 119 parentModule.changePipelineType((Integer) newPropValue); 120 parentModule.saveAndBroadcastAll(); 121 } 122 case "isDriverMode" -> parentModule.setDriverMode((Boolean) newPropValue); 123 default -> handled = false; 124 } 125 126 // special case for camera settables 127 if (propName.startsWith("camera")) { 128 var propMethodName = "set" + propName.replace("camera", ""); 129 var methods = parentModule.visionSource.getSettables().getClass().getMethods(); 130 for (var method : methods) { 131 if (method.getName().equalsIgnoreCase(propMethodName)) { 132 try { 133 method.invoke(parentModule.visionSource.getSettables(), newPropValue); 134 } catch (Exception e) { 135 logger.error("Failed to invoke camera settable method: " + method.getName(), e); 136 } 137 } 138 } 139 } 140 141 if (!handled) { 142 try { 143 setProperty(currentSettings, propName, newPropValue); 144 logger.trace("Set prop " + propName + " to value " + newPropValue); 145 } catch (NoSuchFieldException | IllegalAccessException e) { 146 logger.error( 147 "Could not set prop " 148 + propName 149 + " with value " 150 + newPropValue 151 + " on " 152 + currentSettings 153 + " | " 154 + e.getClass().getSimpleName(), 155 e); 156 } catch (Exception e) { 157 logger.error("Unknown exception when setting PSC prop!", e); 158 } 159 } 160 161 parentModule.saveAndBroadcastSelective(originContext, propName, newPropValue); 162 } 163 getSettingChanges().clear(); 164 } finally { 165 changeListLock.unlock(); 166 } 167 } 168 169 public void newPipelineNickname(String newNickname) { 170 logger.info("Changing pipeline nickname to " + newNickname); 171 parentModule.pipelineManager.getCurrentPipelineSettings().pipelineNickname = newNickname; 172 parentModule.saveAndBroadcastAll(); 173 } 174 175 public void newPipelineInfo(Pair<String, PipelineType> typeName) { 176 var type = typeName.getRight(); 177 var name = typeName.getLeft(); 178 179 logger.info("Adding a " + type + " pipeline with name " + name); 180 181 var addedSettings = parentModule.pipelineManager.addPipeline(type); 182 addedSettings.pipelineNickname = name; 183 parentModule.saveAndBroadcastAll(); 184 } 185 186 public void deleteCurrPipeline() { 187 var indexToDelete = parentModule.pipelineManager.getRequestedIndex(); 188 logger.info("Deleting current pipe at index " + indexToDelete); 189 int newIndex = parentModule.pipelineManager.removePipeline(indexToDelete); 190 parentModule.setPipeline(newIndex); 191 parentModule.saveAndBroadcastAll(); 192 } 193 194 public void changePipeline(int index) { 195 if (index == parentModule.pipelineManager.getRequestedIndex()) { 196 logger.debug("Skipping pipeline change, index " + index + " already active"); 197 return; 198 } 199 parentModule.setPipeline(index); 200 parentModule.saveAndBroadcastAll(); 201 } 202 203 public void startCalibration(Map<String, Object> data) { 204 try { 205 var deserialized = JacksonUtils.deserialize(data, UICalibrationData.class); 206 parentModule.startCalibration(deserialized); 207 parentModule.saveAndBroadcastAll(); 208 } catch (Exception e) { 209 logger.error("Error deserializing start-calibration request", e); 210 } 211 } 212 213 public void duplicatePipeline(int index) { 214 var newIndex = parentModule.pipelineManager.duplicatePipeline(index); 215 parentModule.setPipeline(newIndex); 216 parentModule.saveAndBroadcastAll(); 217 } 218 219 public void robotOffsetPoint(AdvancedPipelineSettings curAdvSettings, int offsetIndex) { 220 RobotOffsetPointOperation offsetOperation = RobotOffsetPointOperation.fromIndex(offsetIndex); 221 222 var latestTarget = parentModule.lastPipelineResultBestTarget; 223 if (latestTarget == null) { 224 return; 225 } 226 227 var newPoint = latestTarget.getTargetOffsetPoint(); 228 switch (curAdvSettings.offsetRobotOffsetMode) { 229 case Single -> { 230 switch (offsetOperation) { 231 case CLEAR -> curAdvSettings.offsetSinglePoint = new Point(); 232 case TAKE_SINGLE -> curAdvSettings.offsetSinglePoint = newPoint; 233 case TAKE_FIRST_DUAL, TAKE_SECOND_DUAL -> { 234 logger.warn("Dual point operation in single point mode"); 235 } 236 } 237 } 238 case Dual -> { 239 switch (offsetOperation) { 240 case CLEAR -> { 241 curAdvSettings.offsetDualPointA = new Point(); 242 curAdvSettings.offsetDualPointAArea = 0; 243 curAdvSettings.offsetDualPointB = new Point(); 244 curAdvSettings.offsetDualPointBArea = 0; 245 } 246 case TAKE_FIRST_DUAL -> { 247 // update point and area 248 curAdvSettings.offsetDualPointA = newPoint; 249 curAdvSettings.offsetDualPointAArea = latestTarget.getArea(); 250 } 251 case TAKE_SECOND_DUAL -> { 252 // update point and area 253 curAdvSettings.offsetDualPointB = newPoint; 254 curAdvSettings.offsetDualPointBArea = latestTarget.getArea(); 255 } 256 case TAKE_SINGLE -> { 257 logger.warn("Single point operation in dual point mode"); 258 } 259 } 260 } 261 case None -> { 262 logger.warn("Robot offset point operation requested, but no offset mode set"); 263 } 264 } 265 } 266 267 /** 268 * Sets the value of a property in the given object using reflection. This method should not be 269 * used generally and is only known to be correct in the context of `onDataChangeEvent`. 270 * 271 * @param currentSettings The object whose property needs to be set. 272 * @param propName The name of the property to be set. 273 * @param newPropValue The new value to be assigned to the property. 274 * @throws IllegalAccessException If the field cannot be accessed. 275 * @throws NoSuchFieldException If the field does not exist. 276 * @throws Exception If an some other unknown exception occurs while setting the property. 277 */ 278 protected static void setProperty(Object currentSettings, String propName, Object newPropValue) 279 throws IllegalAccessException, NoSuchFieldException, Exception { 280 var propField = currentSettings.getClass().getField(propName); 281 var propType = propField.getType(); 282 283 if (propType.isEnum()) { 284 var actual = propType.getEnumConstants()[(int) newPropValue]; 285 propField.set(currentSettings, actual); 286 } else if (propType.isAssignableFrom(DoubleCouple.class)) { 287 var orig = (ArrayList<Number>) newPropValue; 288 var actual = new DoubleCouple(orig.get(0), orig.get(1)); 289 propField.set(currentSettings, actual); 290 } else if (propType.isAssignableFrom(IntegerCouple.class)) { 291 var orig = (ArrayList<Number>) newPropValue; 292 var actual = new IntegerCouple(orig.get(0).intValue(), orig.get(1).intValue()); 293 propField.set(currentSettings, actual); 294 } else if (propType.equals(Double.TYPE)) { 295 propField.setDouble(currentSettings, ((Number) newPropValue).doubleValue()); 296 } else if (propType.equals(Integer.TYPE)) { 297 propField.setInt(currentSettings, (Integer) newPropValue); 298 } else if (propType.equals(Boolean.TYPE)) { 299 if (newPropValue instanceof Integer intValue) { 300 propField.setBoolean(currentSettings, intValue != 0); 301 } else { 302 propField.setBoolean(currentSettings, (Boolean) newPropValue); 303 } 304 } else { 305 propField.set(currentSettings, newPropValue); 306 } 307 } 308}