From 8850337250ef552fd24356220d97efcef10a8053 Mon Sep 17 00:00:00 2001 From: Peter Bochmann Date: Tue, 20 Oct 2026 17:13:04 +0200 Subject: [PATCH] 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 --- docs/tower-installation.md | 40 ++++++++++++++ hardware/common/kit.sh | 15 ++++- scripts/export_camera_profile.py | 95 ++++++++++++++++++++++++++++++-- scripts/install-tower | 43 +++++++++++---- src/camera/VimbaCameraSource.cpp | 40 +++++++++++--- 5 files changed, 206 insertions(+), 27 deletions(-) diff --git a/docs/tower-installation.md b/docs/tower-installation.md index 5f48024..8ce50c3 100644 --- a/docs/tower-installation.md +++ b/docs/tower-installation.md @@ -714,6 +714,46 @@ feature vocabularies differ, so copying another tower's XML does not work: | `IntensityAutoPrecedence` | `Minimizeexposuretime` / `Minimizenoise` | **`MinimizeBlur` / `MinimizeNoise`** | | 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 Unbinned RGB8 on the 16 MP sensor is a **48.6 MB frame** - over the ~20 MB diff --git a/hardware/common/kit.sh b/hardware/common/kit.sh index 8974487..09f3dea 100644 --- a/hardware/common/kit.sh +++ b/hardware/common/kit.sh @@ -100,6 +100,9 @@ probe_mcu() { # 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 # 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() { local tool for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do @@ -113,7 +116,7 @@ probe_camera_id() { done "$tool" 2>/dev/null | awk ' /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 done @@ -134,7 +137,8 @@ probe_camera_lpm_path() { probe_report() { 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 "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))" echo "motor MCU : ${mcu:-NOT FOUND}" @@ -270,7 +274,7 @@ do_vimba() { local id; id="$(probe_camera_id)" if [[ -n $id ]]; then - say "camera found: $id" + say "camera found: ${id//$'\n'/, }" else warn "no camera reported by ListCameras yet" warn "the SDK asks for a reboot before the transport layers are usable" @@ -374,6 +378,11 @@ emit_hardware_ini() { warn "$mcu" mcu="CHOOSE_ONE" fi + if [[ $(wc -l <<< "$cam") -gt 1 ]]; then + warn "several cameras found; pick one with --camera-id:" + warn "$cam" + cam="CHOOSE_ONE" + fi cat </dev/null); do args="$(ps -o args= -p "$pid" 2>/dev/null)" [[ $args == */fire_gimbal_control* ]] && continue - die "the motor MCU's port is held by another program: pid $pid ($args). -Stop it for good before installing. On a legacy tower it is started by -start_gimbal.service from ~/scripts/startup_gimbal.sh, which also runs the picture -transfer: comment out the old program's line there, keep the transfer, then + unit="$(ps -o unit= -p "$pid" 2>/dev/null | tr -d ' ')" + if [[ $unit == start_gimbal.service ]]; then + how="It is started by start_gimbal.service from ~/scripts/startup_gimbal.sh, which +also runs the picture transfer: comment out the old program's line there, keep the +transfer, then 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 fi 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_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')" 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 motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE] 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 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) @@ -604,7 +617,11 @@ if [[ ! -r $HW ]]; then ask serial "No motor MCU found - its /dev/serial/by-id path" KIT_ARGS+=(--serial "$REPLY_VAL") 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)" KIT_ARGS+=(--camera-id "$REPLY_VAL") fi @@ -1092,9 +1109,13 @@ if [[ ${DO[profiles]} == change ]]; 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 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 ( 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)" done done_step profiles diff --git a/src/camera/VimbaCameraSource.cpp b/src/camera/VimbaCameraSource.cpp index be51cea..068de83 100644 --- a/src/camera/VimbaCameraSource.cpp +++ b/src/camera/VimbaCameraSource.cpp @@ -188,12 +188,37 @@ private: 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 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 // 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). double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) { FeaturePtr f; - if (SP_ACCESS(cam)->GetFeatureByName(name, f) != VmbErrorSuccess) return fallback; + if (!findFeature(cam, name, f)) return fallback; double v = fallback; if (f->GetValue(v) == VmbErrorSuccess) return v; 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) { FeaturePtr f; 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 {}; } @@ -649,8 +674,7 @@ bool VimbaCameraSource::setFeature(int camera_index, const std::string& name, if (camera_index < 0 || idx >= impl_->cameras.size()) return false; FeaturePtr f; VmbFeatureDataType type = VmbFeatureDataUnknown; - if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || - f->GetDataType(type) != VmbErrorSuccess) + if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess) return false; // 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. @@ -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; FeaturePtr f; VmbFeatureDataType type = VmbFeatureDataUnknown; - if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || - f->GetDataType(type) != VmbErrorSuccess) + if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess) return false; if (type == VmbFeatureDataFloat) return f->GetValue(value) == VmbErrorSuccess; 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; FeaturePtr f; VmbFeatureDataType type = VmbFeatureDataUnknown; - if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || - f->GetDataType(type) != VmbErrorSuccess) + if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess) return false; if (type == VmbFeatureDataFloat) return f->GetRange(min, max) == VmbErrorSuccess; if (type == VmbFeatureDataInt) { @@ -788,7 +810,7 @@ bool VimbaCameraSource::restoreProfile(int camera_index) { for (auto it = undo.rbegin(); ok && it != undo.rend(); ++it) { FeaturePtr f; 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; } VmbErrorType rc = VmbErrorWrongType; try {