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:
parent
ed72f97fe5
commit
8850337250
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue