Support the older GigE cameras (Manta) on rietschen

Rietschen has three Manta G-1236 on one GbE port, whose firmware predates the
names the program uses. A feature missing by its SFNC name is now looked up by
the older one (ExposureTimeAbs, AcquisitionFrameRateAbs, ExposureAutoTarget,
ExposureAutoAdjustTol), so test camera, the telemetry and the keep rule's
reads work. export_camera_profile.py picks a gige recipe for such a camera:
frame trigger off, BayerRG8 or Mono8, 60 MB/s, auto-exposure target 50 %.
At the cameras' old target of 25 every waypoint ran into frame_timeout_ms.

The camera probe now lists every camera. With several, the hardware.ini kit
writes CHOOSE_ONE, install-tower asks which is the colour one, and the profile
export is given that id - the first one the SDK lists was the NIR camera.
install-tower names the unit that holds the motor port instead of assuming
start_gimbal.service.

Tested on rietschen's NIR camera with the motor mocked, beside the running old
program: test camera PASS, a snapshot as a 3008x4112 grey JXL, a scan keeping
a picture every 5 s. Tests 542/542 (mock build). The colour camera is not yet
tested; the old program has it open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Peter Bochmann 2026-10-20 17:13:04 +02:00
parent ed72f97fe5
commit 8850337250
Signed by: peter.bochmann
GPG Key ID: 94E8C523489B8AC1
5 changed files with 206 additions and 27 deletions

View File

@ -714,6 +714,46 @@ feature vocabularies differ, so copying another tower's XML does not work:
| `IntensityAutoPrecedence` | `Minimizeexposuretime` / `Minimizenoise` | **`MinimizeBlur` / `MinimizeNoise`** | | `IntensityAutoPrecedence` | `Minimizeexposuretime` / `Minimizenoise` | **`MinimizeBlur` / `MinimizeNoise`** |
| Sensor | 12 MP | 16 MP, 5328x3040 | | Sensor | 12 MP | 16 MP, 5328x3040 |
### Older GigE cameras (Manta) `[partial]`
Rietschen has three Allied Vision Manta G-1236 on one GbE port (`enp1s0`,
MTU 8000): RGB (G-1236C), NIR and ACR (both G-1236B). Their firmware
(00.01.54) predates the names the program and the Alvium use:
| Program / Alvium | Manta |
|---|---|
| `ExposureTime` | `ExposureTimeAbs` |
| `AcquisitionFrameRate` (+ `…Enable`) | `AcquisitionFrameRateAbs`, free-running at `AcquisitionFrameRateLimit` |
| `IntensityControllerTarget` / `…Tolerance` | `ExposureAutoTarget` / `ExposureAutoAdjustTol` |
| `DeviceLinkThroughputLimit` | `StreamBytesPerSecond` (max 124 MB/s, shared by every camera on the port) |
| `BayerRG12p`, chunk data, `DeviceReset` | none |
The program looks a missing feature up under its older name, so `test camera`
and the telemetry work. `export_camera_profile.py` picks its `gige` recipe by
itself when a camera has `ExposureTimeAbs` and no `ExposureTime`: frame trigger
off (the legacy cameras booted with `TriggerMode = On`, `Line1`), `BayerRG8` on
the colour camera and `Mono8` on a mono one, 60 MB/s each, and an auto-exposure
target of 50 %. That last one matters: the keep rule waits for the frame mean
to reach 50 %, and at the cameras' old 25 every waypoint ran into
`frame_timeout_ms`.
Give `--camera` the id: with several cameras the first one the SDK lists is
not the colour one, and the order changes from run to run. `install.sh
--probe` lists them all, `install.sh --hardware-ini` writes `CHOOSE_ONE`
without `--camera-id`, and `install-tower` asks (`camera_id`).
What is still missing: a Manta has no `DeviceReset`, so the program cannot
reset a stuck camera (it says so and goes on); and without chunk data the keep
rule cannot tell a dark scene at the exposure limit, so at night it keeps the
newest frame after `frame_timeout_ms`.
`[verified]` on rietschen's NIR camera, 2026-10-20, with the motor mocked and
the old program running beside it: `test camera` PASS (5 frames 4112x3008
Mono8 in 1.7 s, dark 100 % <= 20, bright mean 255), `snapshot` writes a
3008x4112 grey JXL (`rotation_deg = 270`, as the old program rotated), and a
scan keeps a picture every 5 s without a timeout. The colour camera is not yet
tested: the old program has it open.
### Binning is not optional, and the frame-rate ceiling is not what it looks like ### Binning is not optional, and the frame-rate ceiling is not what it looks like
Unbinned RGB8 on the 16 MP sensor is a **48.6 MB frame** - over the ~20 MB Unbinned RGB8 on the 16 MP sensor is a **48.6 MB frame** - over the ~20 MB

View File

@ -100,6 +100,9 @@ probe_mcu() {
# therefore taken only from a block whose transport layer is the USB or GigE # therefore taken only from a block whose transport layer is the USB or GigE
# TL. A tower configured with DEV_Cam1 would "find its camera" and capture # TL. A tower configured with DEV_Cam1 would "find its camera" and capture
# synthetic images forever. # synthetic images forever.
#
# One id per line, like probe_mcu: rietschen has three GigE cameras on one port,
# and the first one listed is not the colour one, so the caller has to choose.
probe_camera_id() { probe_camera_id() {
local tool local tool
for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do
@ -113,7 +116,7 @@ probe_camera_id() {
done done
"$tool" 2>/dev/null | awk ' "$tool" 2>/dev/null | awk '
/Camera ID/ { id = $NF } /Camera ID/ { id = $NF }
/Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) { print id; exit } } /Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) print id; id = "" }
' '
return return
done done
@ -134,7 +137,8 @@ probe_camera_lpm_path() {
probe_report() { probe_report() {
local board mcu cam lpm local board mcu cam lpm
board="$(probe_board)"; mcu="$(probe_mcu)"; cam="$(probe_camera_id)"; lpm="$(probe_camera_lpm_path)" board="$(probe_board)"; mcu="$(probe_mcu)"; lpm="$(probe_camera_lpm_path)"
cam="$(probe_camera_id | paste -sd ' ')"
echo "board : $board" echo "board : $board"
echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))" echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))"
echo "motor MCU : ${mcu:-NOT FOUND}" echo "motor MCU : ${mcu:-NOT FOUND}"
@ -270,7 +274,7 @@ do_vimba() {
local id; id="$(probe_camera_id)" local id; id="$(probe_camera_id)"
if [[ -n $id ]]; then if [[ -n $id ]]; then
say "camera found: $id" say "camera found: ${id//$'\n'/, }"
else else
warn "no camera reported by ListCameras yet" warn "no camera reported by ListCameras yet"
warn "the SDK asks for a reboot before the transport layers are usable" warn "the SDK asks for a reboot before the transport layers are usable"
@ -374,6 +378,11 @@ emit_hardware_ini() {
warn "$mcu" warn "$mcu"
mcu="CHOOSE_ONE" mcu="CHOOSE_ONE"
fi fi
if [[ $(wc -l <<< "$cam") -gt 1 ]]; then
warn "several cameras found; pick one with --camera-id:"
warn "$cam"
cam="CHOOSE_ONE"
fi
cat <<EOF cat <<EOF
; hardware.ini - what this machine IS. Generated by hardware/$KIT_BOARD/install.sh ; hardware.ini - what this machine IS. Generated by hardware/$KIT_BOARD/install.sh

View File

@ -120,6 +120,83 @@ RECIPES = {
], ],
} }
# The older GigE cameras (Manta G-1236 on rietschen, firmware 00.01.54) name
# things differently - ExposureTimeAbs, AcquisitionFrameRateAbs, ExposureAutoTarget
# - and have no IntensityController, no DeviceLinkThroughputLimit and no 12-bit
# packed Bayer the program reads. Measured on rietschen's NIR camera, 2026-10-20.
# The link is shared: the camera free-runs as fast as StreamBytesPerSecond lets
# it (41 MB/s gave 3.2 fps at 12.4 MB a frame), and the cameras of one tower sit
# on one GbE port (~124 MB/s), so each gets half of it - room for RGB and NIR.
# PixelFormat takes the first the camera offers: raw Bayer on the colour camera,
# de-Bayered on the tower and a third of RGB8 on the wire; Mono8 on a mono one.
# The packet size is left alone: it has to fit the port's MTU, which is the
# tower's network setup, not the camera's.
_GIGE_COMMON = [
("TriggerSelector", "FrameStart"),
("TriggerMode", "Off"),
("AcquisitionMode", "Continuous"),
("BinningVertical", 1),
("BinningHorizontal", 1),
("OffsetX", 0),
("OffsetY", 0),
("Width", 100000),
("Height", 100000),
("PixelFormat", ("BayerRG8", "Mono8")),
("StreamBytesPerSecond", 60000000),
("ChunkModeActive", False),
# The keep rule waits for the frame mean to reach the camera's target. The
# host starts on its default band of 50 % - rietschen's cameras were set to
# 25, and every waypoint then ran into frame_timeout_ms (7.5 s, 2026-10-20).
("ExposureAutoTarget", 50),
("ExposureAuto", "Continuous"),
("GainAuto", "Continuous"),
]
GIGE_RECIPES = {
"standard": _GIGE_COMMON + [
("ExposureAutoMax", 20000),
("ExposureAutoMin", 200),
("GainAutoMax", 24.0),
],
"night": _GIGE_COMMON + [
("ExposureAutoMax", 500000),
("ExposureAutoMin", 1000),
("GainAutoMax", 48.0), # clamped to the camera's 40
],
}
def has_feature(cam, name):
try:
cam.get_feature_by_name(name)
return True
except Exception:
return False
def recipe_family(cam):
"""'gige' for a camera that only knows the older names, else 'alvium'."""
if not has_feature(cam, "ExposureTime") and has_feature(cam, "ExposureTimeAbs"):
return "gige"
return "alvium"
def resolve_value(feat, value):
"""The value to write to this feature; None if a choice list has no match.
A tuple is a list of choices for an enum: the first one the camera offers.
An integer feature takes no float, even a whole one (ExposureAutoMin is Int
on the Manta and Float on the Alvium)."""
if isinstance(value, tuple):
try:
offered = {str(e) for e in feat.get_available_entries()}
except Exception:
offered = set()
return next((v for v in value if v in offered), None)
if isinstance(feat, vmbpy.IntFeature) and isinstance(value, float):
return int(round(value))
return value
def apply_feature(cam, name, value, dry_run): def apply_feature(cam, name, value, dry_run):
try: try:
@ -131,6 +208,10 @@ def apply_feature(cam, name, value, dry_run):
except Exception: except Exception:
before = None before = None
value = resolve_value(feat, value)
if value is None:
return ("refused", name, before, "none of the choices is offered")
# Clamp a numeric value the camera cannot reach, rather than letting the # Clamp a numeric value the camera cannot reach, rather than letting the
# write fail and leaving whatever was there before. Without this, exporting # write fail and leaving whatever was there before. Without this, exporting
# 'standard' right after 'night' wrote a standard profile running at the # 'standard' right after 'night' wrote a standard profile running at the
@ -168,6 +249,8 @@ def main():
"fire_gimbal_control", "camera_profiles", "rgb"), "fire_gimbal_control", "camera_profiles", "rgb"),
help="directory to write <profile>.xml into") help="directory to write <profile>.xml into")
ap.add_argument("--camera", default=None, help="camera id (default: the first real one)") ap.add_argument("--camera", default=None, help="camera id (default: the first real one)")
ap.add_argument("--family", default="auto", choices=("auto", "alvium", "gige"),
help="which recipe set (default: from the features the camera has)")
ap.add_argument("--dry-run", action="store_true") ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args() args = ap.parse_args()
@ -191,10 +274,12 @@ def main():
cam = cams[0] cam = cams[0]
with cam: with cam:
family = recipe_family(cam) if args.family == "auto" else args.family
recipe = (GIGE_RECIPES if family == "gige" else RECIPES)[args.profile]
print(f"camera : {cam.get_id()} {cam.get_model()}") print(f"camera : {cam.get_id()} {cam.get_model()}")
print(f"profile : {args.profile}{' (dry run)' if args.dry_run else ''}") print(f"profile : {args.profile} ({family} recipe){' (dry run)' if args.dry_run else ''}")
refused = [] refused = []
for name, value in RECIPES[args.profile]: for name, value in recipe:
state, n, before, after = apply_feature(cam, name, value, args.dry_run) state, n, before, after = apply_feature(cam, name, value, args.dry_run)
mark = {"set": " ", "would-set": " ?", "missing": " !", "refused": " X", mark = {"set": " ", "would-set": " ?", "missing": " !", "refused": " X",
"clamped": " ~", "would-clamp": " ~"}[state] "clamped": " ~", "would-clamp": " ~"}[state]
@ -205,13 +290,15 @@ def main():
refused.append(n) refused.append(n)
# What the settings actually cost per frame - the number that decides # What the settings actually cost per frame - the number that decides
# whether this profile can be delivered over USB at all. # whether this profile can be delivered over the link at all. A
# free-running GigE camera reports its ceiling as the rate limit.
try: try:
w = cam.get_feature_by_name("Width").get() w = cam.get_feature_by_name("Width").get()
h = cam.get_feature_by_name("Height").get() h = cam.get_feature_by_name("Height").get()
fmt = cam.get_feature_by_name("PixelFormat").get() fmt = cam.get_feature_by_name("PixelFormat").get()
bpp = {"RGB8": 3, "BGR8": 3, "BayerRG12p": 1.5, "BayerRG12": 2}.get(str(fmt), 1) bpp = {"RGB8": 3, "BGR8": 3, "BayerRG12p": 1.5, "BayerRG12": 2}.get(str(fmt), 1)
rate = cam.get_feature_by_name("AcquisitionFrameRate").get() rate_name = "AcquisitionFrameRateLimit" if family == "gige" else "AcquisitionFrameRate"
rate = cam.get_feature_by_name(rate_name).get()
mb = w * h * bpp / 1e6 mb = w * h * bpp / 1e6
print(f"\nresult : {w}x{h} {fmt} {mb:.1f} MB/frame " print(f"\nresult : {w}x{h} {fmt} {mb:.1f} MB/frame "
f"{rate:.2f} fps = {mb * rate:.0f} MB/s") f"{rate:.2f} fps = {mb * rate:.0f} MB/s")

View File

@ -334,22 +334,35 @@ set -euo pipefail
S_MCU="$(probe_mcu)" S_MCU="$(probe_mcu)"
S_MCU_N=$(grep -c . <<< "$S_MCU" || true) S_MCU_N=$(grep -c . <<< "$S_MCU" || true)
# Another program on the motor MCU's port - a legacy tower's old program, started # Another program on the motor MCU's port - a legacy tower's old program - would
# by start_gimbal.service - would fight this one for it, and the flash would # fight this one for it, and the flash would fail half-way. Our own program is
# fail half-way. Our own program is fine: it is stopped before anything needs # fine: it is stopped before anything needs the port. The old program is started
# the port. # differently from tower to tower: start_gimbal.service from
# ~/scripts/startup_gimbal.sh, which also runs the picture transfer, on
# staeffelsberg; gimbal_control.service on its own on rietschen.
if (( S_MCU_N == 1 )) && [[ -e $S_MCU ]]; then if (( S_MCU_N == 1 )) && [[ -e $S_MCU ]]; then
for pid in $(fuser "$(readlink -f "$S_MCU")" 2>/dev/null); do for pid in $(fuser "$(readlink -f "$S_MCU")" 2>/dev/null); do
args="$(ps -o args= -p "$pid" 2>/dev/null)" args="$(ps -o args= -p "$pid" 2>/dev/null)"
[[ $args == */fire_gimbal_control* ]] && continue [[ $args == */fire_gimbal_control* ]] && continue
die "the motor MCU's port is held by another program: pid $pid ($args). unit="$(ps -o unit= -p "$pid" 2>/dev/null | tr -d ' ')"
Stop it for good before installing. On a legacy tower it is started by if [[ $unit == start_gimbal.service ]]; then
start_gimbal.service from ~/scripts/startup_gimbal.sh, which also runs the picture how="It is started by start_gimbal.service from ~/scripts/startup_gimbal.sh, which
transfer: comment out the old program's line there, keep the transfer, then also runs the picture transfer: comment out the old program's line there, keep the
transfer, then
sudo systemctl restart start_gimbal" sudo systemctl restart start_gimbal"
elif [[ $unit == *.service ]]; then
how="It is started by $unit:
sudo systemctl disable --now $unit"
else
how="Find what starts it (systemctl status $pid) and stop that."
fi
die "the motor MCU's port is held by another program: pid $pid ($args).
Stop it for good before installing. $how"
done done
fi fi
S_CAM="$(probe_camera_id)" S_CAM="$(probe_camera_id)"
S_CAM_N=$(grep -c . <<< "$S_CAM" || true)
S_CAM_SHOWN="$(paste -sd ' ' <<< "$S_CAM")"
S_LPM="$(probe_camera_lpm_path)" S_LPM="$(probe_camera_lpm_path)"
S_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')" S_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')"
S_PIO="$(pio_path || true)" S_PIO="$(pio_path || true)"
@ -377,7 +390,7 @@ board : $S_BOARD (kit hardware/$S_BOARD, firmware env $KIT_FW_ENV)
software / fw : $S_SW_REV / $S_FW_REV software / fw : $S_SW_REV / $S_FW_REV
motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE] motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE]
running firmware : ${S_FW_NOW:-unknown} running firmware : ${S_FW_NOW:-unknown}
camera : ${S_CAM:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)} camera : ${S_CAM_SHOWN:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)}
usbfs_memory_mb : $S_USBFS usbfs_memory_mb : $S_USBFS
hardware.ini : $([[ -r $HW ]] && echo "present, $(hw_todos | grep -c . || true) TODO(s)" || echo absent) hardware.ini : $([[ -r $HW ]] && echo "present, $(hw_todos | grep -c . || true) TODO(s)" || echo absent)
config.ini : $([[ -r $CFG ]] && echo "$CFG, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?'), [Startup] home $(ini_get "$CFG" Startup home 2>/dev/null || echo unset), mode $(ini_get "$CFG" Startup mode 2>/dev/null || echo unset)" || echo absent) config.ini : $([[ -r $CFG ]] && echo "$CFG, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?'), [Startup] home $(ini_get "$CFG" Startup home 2>/dev/null || echo unset), mode $(ini_get "$CFG" Startup mode 2>/dev/null || echo unset)" || echo absent)
@ -604,7 +617,11 @@ if [[ ! -r $HW ]]; then
ask serial "No motor MCU found - its /dev/serial/by-id path" ask serial "No motor MCU found - its /dev/serial/by-id path"
KIT_ARGS+=(--serial "$REPLY_VAL") KIT_ARGS+=(--serial "$REPLY_VAL")
fi fi
if [[ $CAMS_A != none && -z $S_CAM ]]; then if [[ $CAMS_A != none ]] && (( S_CAM_N > 1 )); then
echo "Several cameras:"; nl -w2 -s') ' <<< "$S_CAM"
ask camera_id "Which is the colour camera (its DEV_ id)"
KIT_ARGS+=(--camera-id "$REPLY_VAL")
elif [[ $CAMS_A != none && -z $S_CAM ]]; then
ask camera_id "Camera DEV_ id (none found; the SDK may need a reboot first)" ask camera_id "Camera DEV_ id (none found; the SDK may need a reboot first)"
KIT_ARGS+=(--camera-id "$REPLY_VAL") KIT_ARGS+=(--camera-id "$REPLY_VAL")
fi fi
@ -1092,9 +1109,13 @@ if [[ ${DO[profiles]} == change ]]; then
"$HOME/.venvs/vmbpy/bin/pip" install -q /opt/VimbaX/api/python/vmbpy-*.whl; }; }; then "$HOME/.venvs/vmbpy/bin/pip" install -q /opt/VimbaX/api/python/vmbpy-*.whl; }; }; then
PROFDIR="$(prof_dir)" # the config may have moved since the plan was made PROFDIR="$(prof_dir)" # the config may have moved since the plan was made
mkdir -p "$PROFDIR" mkdir -p "$PROFDIR"
# The camera hardware.ini names, not the first one the SDK lists: on a
# tower with several (rietschen: three GigE) that is another camera.
cam_id="$(ini_get "$HW" Camera.rgb id 2>/dev/null || true)"
cam_arg=(); [[ $cam_id == DEV_* ]] && cam_arg=(--camera "$cam_id")
for prof in "${PROF_MISSING[@]}"; do for prof in "${PROF_MISSING[@]}"; do
( cd "$REPO" && GENICAM_GENTL64_PATH=/opt/VimbaX/cti "$HOME/.venvs/vmbpy/bin/python" \ ( cd "$REPO" && GENICAM_GENTL64_PATH=/opt/VimbaX/cti "$HOME/.venvs/vmbpy/bin/python" \
scripts/export_camera_profile.py --profile "$prof" --out "$PROFDIR" ) || scripts/export_camera_profile.py --profile "$prof" --out "$PROFDIR" "${cam_arg[@]}" ) ||
warn "could not export the $prof profile; export it later (docs/tower-installation.md section 8)" warn "could not export the $prof profile; export it later (docs/tower-installation.md section 8)"
done done
done_step profiles done_step profiles

View File

@ -188,12 +188,37 @@ private:
bool has_ = false; bool has_ = false;
}; };
// The older GigE cameras (Manta G-1236 on rietschen, firmware 00.01.54) predate
// the SFNC names the program uses and the Alvium answers to. Where a camera has
// no feature by the program's name, it is looked up by the older one. The
// IntensityController pair is the Alvium's auto-exposure target and tolerance;
// the Manta's ExposureAuto* pair means the same, in percent too.
const char* legacyFeatureName(const std::string& name) {
static const std::pair<const char*, const char*> kAliases[] = {
{"ExposureTime", "ExposureTimeAbs"},
{"AcquisitionFrameRate", "AcquisitionFrameRateAbs"},
{"IntensityControllerTarget", "ExposureAutoTarget"},
{"IntensityControllerTolerance", "ExposureAutoAdjustTol"},
};
for (const auto& [sfnc, legacy] : kAliases)
if (name == sfnc) return legacy;
return nullptr;
}
// A feature by the program's name, or by its older name on a camera that has only
// that one.
bool findFeature(const CameraPtr& cam, const std::string& name, FeaturePtr& f) {
if (SP_ACCESS(cam)->GetFeatureByName(name.c_str(), f) == VmbErrorSuccess) return true;
const char* legacy = legacyFeatureName(name);
return legacy && SP_ACCESS(cam)->GetFeatureByName(legacy, f) == VmbErrorSuccess;
}
// Read a numeric/string feature, returning `fallback`/"" if absent (tolerant, for // Read a numeric/string feature, returning `fallback`/"" if absent (tolerant, for
// telemetry). Numeric takes an integer feature too: some are Int on one model and // telemetry). Numeric takes an integer feature too: some are Int on one model and
// Float on another (IntensityControllerTolerance is Int on the Alvium 1800 U-1240c). // Float on another (IntensityControllerTolerance is Int on the Alvium 1800 U-1240c).
double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) { double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
FeaturePtr f; FeaturePtr f;
if (SP_ACCESS(cam)->GetFeatureByName(name, f) != VmbErrorSuccess) return fallback; if (!findFeature(cam, name, f)) return fallback;
double v = fallback; double v = fallback;
if (f->GetValue(v) == VmbErrorSuccess) return v; if (f->GetValue(v) == VmbErrorSuccess) return v;
VmbInt64_t i = 0; VmbInt64_t i = 0;
@ -203,7 +228,7 @@ double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
std::string getString(const CameraPtr& cam, const char* name) { std::string getString(const CameraPtr& cam, const char* name) {
FeaturePtr f; FeaturePtr f;
std::string v; std::string v;
if (SP_ACCESS(cam)->GetFeatureByName(name, f) == VmbErrorSuccess && f->GetValue(v) == VmbErrorSuccess) if (findFeature(cam, name, f) && f->GetValue(v) == VmbErrorSuccess)
return v; return v;
return {}; return {};
} }
@ -649,8 +674,7 @@ bool VimbaCameraSource::setFeature(int camera_index, const std::string& name,
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
// The value before the first change, so restoreProfile() can put it back // The value before the first change, so restoreProfile() can put it back
// live. Only the first: a second change must not record the test's own value. // live. Only the first: a second change must not record the test's own value.
@ -741,8 +765,7 @@ bool VimbaCameraSource::readFeature(int camera_index, const std::string& name, d
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
if (type == VmbFeatureDataFloat) return f->GetValue(value) == VmbErrorSuccess; if (type == VmbFeatureDataFloat) return f->GetValue(value) == VmbErrorSuccess;
if (type == VmbFeatureDataInt) { if (type == VmbFeatureDataInt) {
@ -760,8 +783,7 @@ bool VimbaCameraSource::featureRange(int camera_index, const std::string& name,
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
if (type == VmbFeatureDataFloat) return f->GetRange(min, max) == VmbErrorSuccess; if (type == VmbFeatureDataFloat) return f->GetRange(min, max) == VmbErrorSuccess;
if (type == VmbFeatureDataInt) { if (type == VmbFeatureDataInt) {
@ -788,7 +810,7 @@ bool VimbaCameraSource::restoreProfile(int camera_index) {
for (auto it = undo.rbegin(); ok && it != undo.rend(); ++it) { for (auto it = undo.rbegin(); ok && it != undo.rend(); ++it) {
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(it->first.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], it->first, f) ||
f->GetDataType(type) != VmbErrorSuccess) { ok = false; break; } f->GetDataType(type) != VmbErrorSuccess) { ok = false; break; }
VmbErrorType rc = VmbErrorWrongType; VmbErrorType rc = VmbErrorWrongType;
try { try {