Bug Fix Grab Bag (#688)

* Reordered ov video modes to be lowest-to-highest res

* Save off sensor model on init. Guard against low, crashy exposures.

* Pulled in matt's fixups from https://github.com/PhotonVision/photon-libcamera-gl-driver/suites/10144555465/artifacts/495489276

* Further autoexposure tweaks for picam v1

* Allow undercores in camera rename

* Additional guarding against output images being empty

* lock out auto-exposure on ov9281's

* Guarding stream pipelines against empty frames from cameras. Rearranged driver stream to resize first, then draw crosshairs (matchces with other pipelines now).

* NT Priority fixup - if client is sending commands on NT, its nt value should win over anything done from the UI

* Synchronous pipline adjustmet fix, method cleanup

* lint

* circle pipe and data publish bugfixes

* lint

* Pulled in matt's latest .so and re-enabled auto exposure on 9281's
This commit is contained in:
Chris Gerth
2023-01-03 21:53:04 -06:00
committed by GitHub
parent af6f5eb0c4
commit 8c8c32af1a
10 changed files with 139 additions and 114 deletions

View File

@@ -257,7 +257,7 @@ export default {
}, },
data: () => { data: () => {
return { return {
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"), re: RegExp("^[A-Za-z0-9_ \\-)(]*[A-Za-z0-9][A-Za-z0-9_ \\-)(.]*$"),
isCameraNameEdit: false, isCameraNameEdit: false,
newCameraName: "", newCameraName: "",
cameraNameError: "", cameraNameError: "",

View File

@@ -142,8 +142,8 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
ts.rawBytesEntry.set(packet.getData()); ts.rawBytesEntry.set(packet.getData());
ts.pipelineIndexPublisher.set(pipelineIndexSupplier.get()); ts.pipelineIndexPublisher.setDefault(pipelineIndexSupplier.get());
ts.driverModePublisher.set(driverModeSupplier.getAsBoolean()); ts.driverModePublisher.setDefault(driverModeSupplier.getAsBoolean());
ts.latencyMillisEntry.set(result.getLatencyMillis()); ts.latencyMillisEntry.set(result.getLatencyMillis());
ts.hasTargetEntry.set(result.hasTargets()); ts.hasTargetEntry.set(result.hasTargets());

View File

@@ -29,13 +29,15 @@ import org.photonvision.vision.processes.VisionSourceSettables;
public class LibcameraGpuSettables extends VisionSourceSettables { public class LibcameraGpuSettables extends VisionSourceSettables {
private FPSRatedVideoMode currentVideoMode; private FPSRatedVideoMode currentVideoMode;
private double lastExposure = 50; private double lastManualExposure = 50;
private int lastBrightness = 50; private int lastBrightness = 50;
private boolean lastExposureMode; private boolean lastAutoExposureActive;
private int lastGain = 50; private int lastGain = 50;
private Pair<Integer, Integer> lastAwbGains = new Pair<>(18, 18); private Pair<Integer, Integer> lastAwbGains = new Pair<>(18, 18);
private boolean m_initialized = false; private boolean m_initialized = false;
private final LibCameraJNI.SensorModel sensorModel;
private ImageRotationMode m_rotationMode; private ImageRotationMode m_rotationMode;
public void setRotation(ImageRotationMode rotationMode) { public void setRotation(ImageRotationMode rotationMode) {
@@ -51,7 +53,7 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
videoModes = new HashMap<>(); videoModes = new HashMap<>();
LibCameraJNI.SensorModel sensorModel = LibCameraJNI.getSensorModel(); sensorModel = LibCameraJNI.getSensorModel();
if (sensorModel == LibCameraJNI.SensorModel.IMX219) { if (sensorModel == LibCameraJNI.SensorModel.IMX219) {
// Settings for the IMX219 sensor, which is used on the Pi Camera Module v2 // Settings for the IMX219 sensor, which is used on the Pi Camera Module v2
@@ -72,13 +74,14 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
6, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 3280 / 4, 2464 / 4, 15, 20, 1)); 6, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 3280 / 4, 2464 / 4, 15, 20, 1));
} else if (sensorModel == LibCameraJNI.SensorModel.OV9281) { } else if (sensorModel == LibCameraJNI.SensorModel.OV9281) {
videoModes.put( videoModes.put(
0, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280, 800, 60, 60, 1)); 0, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 30, 30, .39));
videoModes.put( videoModes.put(
1, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280 / 2, 800 / 2, 60, 60, 1)); 1, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280 / 2, 800 / 2, 60, 60, 1));
videoModes.put( videoModes.put(
2, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 320, 240, 30, 30, .39)); 2, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 65, 90, .39));
videoModes.put( videoModes.put(
3, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 640, 480, 65, 90, .39)); 3, new FPSRatedVideoMode(VideoMode.PixelFormat.kUnknown, 1280, 800, 60, 60, 1));
} else { } else {
if (sensorModel == LibCameraJNI.SensorModel.IMX477) { if (sensorModel == LibCameraJNI.SensorModel.IMX477) {
LibcameraGpuSource.logger.warn( LibcameraGpuSource.logger.warn(
@@ -114,20 +117,33 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
@Override @Override
public void setAutoExposure(boolean cameraAutoExposure) { public void setAutoExposure(boolean cameraAutoExposure) {
lastExposureMode = cameraAutoExposure; lastAutoExposureActive = cameraAutoExposure;
// TODO (Matt) -- call LibCameraJNI's auto exposure function, when that exists
LibCameraJNI.setAutoExposure(cameraAutoExposure); LibCameraJNI.setAutoExposure(cameraAutoExposure);
} }
@Override @Override
public void setExposure(double exposure) { public void setExposure(double exposure) {
// Todo (Chris) - for now, handle auto exposure by using -1 if (exposure < 0.0 || lastAutoExposureActive) {
if (exposure < 0.0) { // Auto-exposure is active right now, don't set anything.
exposure = -1; return;
} }
// TODO convert to uS // HACKS!
lastExposure = exposure; // If we set exposure too low, libcamera crashes or slows down
// Very weird and smelly
// For now, band-aid this by just not setting it lower than the "it breaks" limit
// Limit is different depending on camera.
if (sensorModel == LibCameraJNI.SensorModel.OV9281) {
if (exposure < 6.0) {
exposure = 6.0;
}
} else if (sensorModel == LibCameraJNI.SensorModel.OV5647) {
if (exposure < 0.7) {
exposure = 0.7;
}
}
lastManualExposure = exposure;
var success = LibCameraJNI.setExposure((int) Math.round(exposure) * 800); var success = LibCameraJNI.setExposure((int) Math.round(exposure) * 800);
if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera exposure"); if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera exposure");
} }
@@ -150,19 +166,25 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
@Override @Override
public void setRedGain(int red) { public void setRedGain(int red) {
lastAwbGains = Pair.of(red, lastAwbGains.getSecond()); if (sensorModel != LibCameraJNI.SensorModel.OV9281) {
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond()); lastAwbGains = Pair.of(red, lastAwbGains.getSecond());
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());
}
} }
@Override @Override
public void setBlueGain(int blue) { public void setBlueGain(int blue) {
lastAwbGains = Pair.of(lastAwbGains.getFirst(), blue); if (sensorModel != LibCameraJNI.SensorModel.OV9281) {
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond()); lastAwbGains = Pair.of(lastAwbGains.getFirst(), blue);
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());
}
} }
public void setAwbGain(int red, int blue) { public void setAwbGain(int red, int blue) {
var success = LibCameraJNI.setAwbGain(red / 10.0, blue / 10.0); if (sensorModel != LibCameraJNI.SensorModel.OV9281) {
if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera AWB gains"); var success = LibCameraJNI.setAwbGain(red / 10.0, blue / 10.0);
if (!success) LibcameraGpuSource.logger.warn("Couldn't set Pi Camera AWB gains");
}
} }
@Override @Override
@@ -202,8 +224,8 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
// We don't store last settings on the native side, and when you change video mode these get // We don't store last settings on the native side, and when you change video mode these get
// reset on MMAL's end // reset on MMAL's end
setExposure(lastExposure); setExposure(lastManualExposure);
setAutoExposure(lastExposureMode); setAutoExposure(lastAutoExposureActive);
setBrightness(lastBrightness); setBrightness(lastBrightness);
setGain(lastGain); setGain(lastGain);
setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond()); setAwbGain(lastAwbGains.getFirst(), lastAwbGains.getSecond());

View File

@@ -66,7 +66,7 @@ public class FindCirclesPipe
1.0, 1.0,
params.minDist, params.minDist,
params.maxCannyThresh, params.maxCannyThresh,
params.accuracy, Math.max(1.0, params.accuracy),
minRadius, minRadius,
maxRadius); maxRadius);
// Great, we now found the center point of the circle and it's radius, but we have no idea what // Great, we now found the center point of the circle and it's radius, but we have no idea what

View File

@@ -77,18 +77,22 @@ public class DriverModePipeline
// apply pipes // apply pipes
var inputMat = frame.colorImage.getMat(); var inputMat = frame.colorImage.getMat();
totalNanos += resizeImagePipe.run(inputMat).nanosElapsed; boolean emptyIn = inputMat.empty();
if (!accelerated) { if (!emptyIn) {
var rotateImageResult = rotateImagePipe.run(inputMat); if (!accelerated) {
totalNanos += rotateImageResult.nanosElapsed; var rotateImageResult = rotateImagePipe.run(inputMat);
totalNanos += rotateImageResult.nanosElapsed;
}
totalNanos += resizeImagePipe.run(inputMat).nanosElapsed;
var draw2dCrosshairResult = draw2dCrosshairPipe.run(Pair.of(inputMat, List.of()));
// calculate elapsed nanoseconds
totalNanos += draw2dCrosshairResult.nanosElapsed;
} }
var draw2dCrosshairResult = draw2dCrosshairPipe.run(Pair.of(inputMat, List.of()));
// calculate elapsed nanoseconds
totalNanos += draw2dCrosshairResult.nanosElapsed;
var fpsResult = calculateFPSPipe.run(null); var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output; var fps = fpsResult.output;

View File

@@ -110,65 +110,71 @@ public class OutputStreamPipeline {
boolean inEmpty = inMat.empty(); boolean inEmpty = inMat.empty();
if (!inEmpty) if (!inEmpty)
sumPipeNanosElapsed += pipeProfileNanos[0] = resizeImagePipe.run(inMat).nanosElapsed; sumPipeNanosElapsed += pipeProfileNanos[0] = resizeImagePipe.run(inMat).nanosElapsed;
sumPipeNanosElapsed += pipeProfileNanos[1] = resizeImagePipe.run(outMat).nanosElapsed;
// Convert single-channel HSV output mat to 3-channel BGR in preparation for streaming boolean outEmpty = outMat.empty();
if (outMat.channels() == 1) { if (!outEmpty)
var outputMatPipeResult = outputMatPipe.run(outMat); sumPipeNanosElapsed += pipeProfileNanos[1] = resizeImagePipe.run(outMat).nanosElapsed;
sumPipeNanosElapsed += pipeProfileNanos[2] = outputMatPipeResult.nanosElapsed;
} else {
pipeProfileNanos[2] = 0;
}
// Draw 2D Crosshair on output // Only attempt drawing on a non-empty frame
var draw2dCrosshairResultOnInput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw)); if (!outEmpty) {
sumPipeNanosElapsed += pipeProfileNanos[3] = draw2dCrosshairResultOnInput.nanosElapsed; // Convert single-channel HSV output mat to 3-channel BGR in preparation for streaming
if (outMat.channels() == 1) {
if (!(settings instanceof AprilTagPipelineSettings)) { var outputMatPipeResult = outputMatPipe.run(outMat);
// If we're processing anything other than Apriltags... sumPipeNanosElapsed += pipeProfileNanos[2] = outputMatPipeResult.nanosElapsed;
var draw2dCrosshairResultOnOutput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[4] = draw2dCrosshairResultOnOutput.nanosElapsed;
if (settings.solvePNPEnabled) {
// Draw 3D Targets on input and output if possible
pipeProfileNanos[5] = 0;
pipeProfileNanos[6] = 0;
pipeProfileNanos[7] = 0;
var drawOnOutputResult = draw3dTargetsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[8] = drawOnOutputResult.nanosElapsed;
} else { } else {
// Only draw 2d targets pipeProfileNanos[2] = 0;
pipeProfileNanos[5] = 0;
var draw2dTargetsOnOutput = draw2dTargetsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[6] = draw2dTargetsOnOutput.nanosElapsed;
pipeProfileNanos[7] = 0;
pipeProfileNanos[8] = 0;
} }
} else { // Draw 2D Crosshair on output
// If we are doing apriltags... var draw2dCrosshairResultOnInput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
if (settings.solvePNPEnabled) { sumPipeNanosElapsed += pipeProfileNanos[3] = draw2dCrosshairResultOnInput.nanosElapsed;
// Draw 3d Apriltag markers (camera is calibrated and running in 3d mode)
pipeProfileNanos[5] = 0;
pipeProfileNanos[6] = 0;
var drawOnInputResult = draw3dAprilTagsPipe.run(Pair.of(outMat, targetsToDraw)); if (!(settings instanceof AprilTagPipelineSettings)) {
sumPipeNanosElapsed += pipeProfileNanos[7] = drawOnInputResult.nanosElapsed; // If we're processing anything other than Apriltags...
pipeProfileNanos[8] = 0; var draw2dCrosshairResultOnOutput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[4] = draw2dCrosshairResultOnOutput.nanosElapsed;
if (settings.solvePNPEnabled) {
// Draw 3D Targets on input and output if possible
pipeProfileNanos[5] = 0;
pipeProfileNanos[6] = 0;
pipeProfileNanos[7] = 0;
var drawOnOutputResult = draw3dTargetsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[8] = drawOnOutputResult.nanosElapsed;
} else {
// Only draw 2d targets
pipeProfileNanos[5] = 0;
var draw2dTargetsOnOutput = draw2dTargetsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[6] = draw2dTargetsOnOutput.nanosElapsed;
pipeProfileNanos[7] = 0;
pipeProfileNanos[8] = 0;
}
} else { } else {
// Draw 2d apriltag markers // If we are doing apriltags...
var draw2dTargetsOnInput = draw2dAprilTagsPipe.run(Pair.of(outMat, targetsToDraw)); if (settings.solvePNPEnabled) {
sumPipeNanosElapsed += pipeProfileNanos[5] = draw2dTargetsOnInput.nanosElapsed; // Draw 3d Apriltag markers (camera is calibrated and running in 3d mode)
pipeProfileNanos[5] = 0;
pipeProfileNanos[6] = 0;
pipeProfileNanos[6] = 0; var drawOnInputResult = draw3dAprilTagsPipe.run(Pair.of(outMat, targetsToDraw));
pipeProfileNanos[7] = 0; sumPipeNanosElapsed += pipeProfileNanos[7] = drawOnInputResult.nanosElapsed;
pipeProfileNanos[8] = 0;
pipeProfileNanos[8] = 0;
} else {
// Draw 2d apriltag markers
var draw2dTargetsOnInput = draw2dAprilTagsPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[5] = draw2dTargetsOnInput.nanosElapsed;
pipeProfileNanos[6] = 0;
pipeProfileNanos[7] = 0;
pipeProfileNanos[8] = 0;
}
} }
} }

View File

@@ -22,6 +22,9 @@ import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import org.photonvision.common.configuration.CameraConfiguration; import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
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.pipeline.*; import org.photonvision.vision.pipeline.*;
@@ -48,7 +51,7 @@ public class PipelineManager {
* <br> * <br>
* Used only when switching from any of the built-in pipelines back to a user-created pipeline. * Used only when switching from any of the built-in pipelines back to a user-created pipeline.
*/ */
private int lastPipelineIndex; private int lastUserPipelineIdx;
/** /**
* Creates a PipelineManager with a DriverModePipeline, a Calibration3dPipeline, and all provided * Creates a PipelineManager with a DriverModePipeline, a Calibration3dPipeline, and all provided
@@ -141,7 +144,7 @@ public class PipelineManager {
* *
* @return The currently active pipeline. * @return The currently active pipeline.
*/ */
public CVPipeline getCurrentUserPipeline() { public CVPipeline getCurrentPipeline() {
if (currentPipelineIndex < 0) { if (currentPipelineIndex < 0) {
switch (currentPipelineIndex) { switch (currentPipelineIndex) {
case CAL_3D_INDEX: case CAL_3D_INDEX:
@@ -151,23 +154,7 @@ public class PipelineManager {
} }
} }
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex); // Just return the current user pipeline, we're not on aa built-in one
// if (currentPipeline.getSettings().pipelineIndex !=
// desiredPipelineSettings.pipelineIndex) {
// switch (desiredPipelineSettings.pipelineType) {
// case Reflective:
// currentPipeline =
// new ReflectivePipeline((ReflectivePipelineSettings)
// desiredPipelineSettings);
// break;
// case ColoredShape:
// currentPipeline =
// new ColoredShapePipeline((ColoredShapePipelineSettings)
// desiredPipelineSettings);
// break;
// }
// }
return currentUserPipeline; return currentUserPipeline;
} }
@@ -186,20 +173,21 @@ public class PipelineManager {
* All externally accessible methods that intend to change the active pipeline MUST go through * All externally accessible methods that intend to change the active pipeline MUST go through
* here to ensure all proper steps are taken. * here to ensure all proper steps are taken.
* *
* @param index Index of pipeline to be active * @param newIndex Index of pipeline to be active
*/ */
private void setPipelineInternal(int index) { private void setPipelineInternal(int newIndex) {
if (index < 0) { if (newIndex < 0 && currentPipelineIndex >= 0) {
lastPipelineIndex = currentPipelineIndex; // Transitioning to a built-in pipe, save off the current user one
lastUserPipelineIdx = currentPipelineIndex;
} }
if (userPipelineSettings.size() - 1 < index) { if (userPipelineSettings.size() - 1 < newIndex) {
logger.warn("User attempted to set index to non-existent pipeline!"); logger.warn("User attempted to set index to non-existent pipeline!");
return; return;
} }
currentPipelineIndex = index; currentPipelineIndex = newIndex;
if (index >= 0) { if (newIndex >= 0) {
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex); var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
switch (desiredPipelineSettings.pipelineType) { switch (desiredPipelineSettings.pipelineType) {
case Reflective: case Reflective:
@@ -222,6 +210,11 @@ public class PipelineManager {
break; break;
} }
} }
DataChangeService.getInstance()
.publishEvent(
new OutgoingUIEvent<>(
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
} }
/** /**
@@ -233,7 +226,7 @@ public class PipelineManager {
*/ */
public void setCalibrationMode(boolean wantsCalibration) { public void setCalibrationMode(boolean wantsCalibration) {
if (!wantsCalibration) calibration3dPipeline.finishCalibration(); if (!wantsCalibration) calibration3dPipeline.finishCalibration();
setPipelineInternal(wantsCalibration ? CAL_3D_INDEX : lastPipelineIndex); setPipelineInternal(wantsCalibration ? CAL_3D_INDEX : lastUserPipelineIdx);
} }
/** /**
@@ -244,7 +237,7 @@ public class PipelineManager {
* @param state True to enter driver mode, false to exit driver mode. * @param state True to enter driver mode, false to exit driver mode.
*/ */
public void setDriverMode(boolean state) { public void setDriverMode(boolean state) {
setPipelineInternal(state ? DRIVERMODE_INDEX : lastPipelineIndex); setPipelineInternal(state ? DRIVERMODE_INDEX : lastUserPipelineIdx);
} }
/** /**

View File

@@ -120,7 +120,7 @@ public class VisionModule {
this.visionRunner = this.visionRunner =
new VisionRunner( new VisionRunner(
this.visionSource.getFrameProvider(), this.visionSource.getFrameProvider(),
this.pipelineManager::getCurrentUserPipeline, this.pipelineManager::getCurrentPipeline,
this::consumeResult, this::consumeResult,
this.cameraQuirks); this.cameraQuirks);
this.streamRunnable = new StreamRunnable(new OutputStreamPipeline()); this.streamRunnable = new StreamRunnable(new OutputStreamPipeline());
@@ -578,7 +578,7 @@ public class VisionModule {
} }
public void setTargetModel(TargetModel targetModel) { public void setTargetModel(TargetModel targetModel) {
var settings = pipelineManager.getCurrentUserPipeline().getSettings(); var settings = pipelineManager.getCurrentPipeline().getSettings();
if (settings instanceof ReflectivePipelineSettings) { if (settings instanceof ReflectivePipelineSettings) {
((ReflectivePipelineSettings) settings).targetModel = targetModel; ((ReflectivePipelineSettings) settings).targetModel = targetModel;
saveAndBroadcastAll(); saveAndBroadcastAll();

View File

@@ -59,7 +59,7 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
var propName = wsEvent.propertyName; var propName = wsEvent.propertyName;
var newPropValue = wsEvent.data; var newPropValue = wsEvent.data;
var currentSettings = parentModule.pipelineManager.getCurrentUserPipeline().getSettings(); var currentSettings = parentModule.pipelineManager.getCurrentPipeline().getSettings();
// special case for non-PipelineSetting changes // special case for non-PipelineSetting changes
switch (propName) { switch (propName) {