From 7acee6cdc99b110f4cf0babe1124f4def82899ed Mon Sep 17 00:00:00 2001 From: kalidke Date: Tue, 29 Sep 2026 14:53:09 -0600 Subject: [PATCH 1/5] main: 0.2.6-DEV after the 0.2.5 release; an empty [Unreleased] section Decision 0033: after X.Y.Z, main carries X.Y.(Z+1)-DEV. TagOnMerge skips a -DEV version. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ Project.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f621ec..0bb5c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ the README's Installation section: in `0.x.y`, `x` is the breaking component and `y` is the non-breaking one (releases are tagged; between them `main` carries the next version with `-DEV`). +## [Unreleased] + ## [0.2.5] - 2026-09-29 A non-breaking release. It brings the TCube laser's closed-loop (power) mode and diff --git a/Project.toml b/Project.toml index bd83563..f1f971d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "MicroscopeControl" uuid = "aa70d9ae-4a1e-49fd-870a-8ccfd99f4c3e" -version = "0.2.5" +version = "0.2.6-DEV" authors = ["klidke@unm.edu"] [deps] From a81bfb9e228264ef1858a8650d7c436deda5fb65 Mon Sep 17 00:00:00 2001 From: kalidke Date: Tue, 29 Sep 2026 19:13:11 -0600 Subject: [PATCH 2/5] DCAM4 capture: bound every wait, arm it before the start, clean up on every exit The quickbeam rig saw capture hang at full frame, 12.5 ms (v0.2.4). Fixes: - DCAMWAIT_START is built with its size (it was sent as 0), and a negative timeout (DCAM's INFINITE) is refused before any library call. - Every frame wait is bounded by capture_timeout_ms: 2 x (exposure + readout) + 1 s, with the readout read from the camera (TIMING_READOUTTIME); getdata scales it by N. - capture stops and releases any earlier capture first, arms the wait before dcamcap_start, and stops, releases and closes on every exit. A timeout or failed wait logs, sets last_error and throws a clear error. - getlastframe and getdata no longer destructure dcamwait_close's single DCAMERR (a MethodError on every failure path). getdata reads an ended cycle at once, never passes DCAM a null wait handle, and cleans up on every exit (in LIVE mode it now ends the live view). - test/dcam4_pure.jl covers what runs without the DCAM library. Co-Authored-By: Claude Sonnet 5.5 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 21 ++ .../dcam4_camera/dcam_helpers.jl | 61 ++++++ .../dcam4_camera/dcamwait.jl | 3 +- .../dcam4_camera/interface_methods.jl | 195 +++++++++++------- test/dcam4_pure.jl | 35 ++++ test/runtests.jl | 2 + 6 files changed, 241 insertions(+), 76 deletions(-) create mode 100644 test/dcam4_pure.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb5c07..b3a0ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,27 @@ the next version with `-DEV`). ## [Unreleased] +### Fixed + +- `DCAM4Camera` `capture` could hang or leave the camera unusable after a missed frame. Every frame wait is now + bounded by 2 x (exposure + readout) + 1 s, with the readout read from the camera + (`DCAM_IDPROP_TIMING_READOUTTIME`); the wait is armed before the capture starts; the wait's parameter struct + carries its size (it was sent as 0); and every exit stops the capture, releases the buffer and closes the wait. + A timeout or failed wait in `capture` now logs, sets `last_error` and throws a clear error (it threw a MethodError + before). `capture` also stops a running live view or sequence before it starts. Reported from the quickbeam rig: + full frame at 12.5 ms. +- `DCAM4Camera` `getlastframe` and `getdata`: a timeout or failed wait no longer throws a MethodError. It logs, + sets `last_error` and returns `nothing`, as the code intended. `getdata`'s wait for the end of a sequence now + includes the readout (2 x N x (exposure + readout) + 1 s; before, a full-frame sequence of short exposures timed + out). A sequence that has already ended is read without waiting. Every exit stops the capture, releases the + buffer and closes the wait. + +### Changed + +- `DCAM4Camera` `getdata` in LIVE mode now ends the live view, because every exit stops the capture and releases + the buffer. It already marked the camera not running, but DCAM refused the buffer release while capturing, so the + live view went on. No known rig code calls `getdata` during a live view. + ## [0.2.5] - 2026-09-29 A non-breaking release. It brings the TCube laser's closed-loop (power) mode and diff --git a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl index 0a9bba2..a540c1f 100644 --- a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl +++ b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl @@ -203,5 +203,66 @@ function settriggermode!(camera::DCAM4Camera, trigger_mode::TriggerMode) settriggermode!(camera) end +""" + capture_timeout_ms(exposure_s, readout_s) -> Int32 + +The timeout for one frame wait, in milliseconds: `2 * (exposure_s + readout_s)` seconds plus 1 s. The readout term +matters: a full ORCA frame reads out in tens of milliseconds, far longer than a short exposure. Throws for a +negative or non-finite input, and for a result beyond `typemax(Int32)` ms, so a wait is never unbounded +(DCAM reads the negative value `0x80000000` as INFINITE). +""" +function capture_timeout_ms(exposure_s::Real, readout_s::Real) + (isfinite(exposure_s) && exposure_s >= 0) || + error("capture_timeout_ms: exposure $(exposure_s) s must be finite and non-negative") + (isfinite(readout_s) && readout_s >= 0) || + error("capture_timeout_ms: readout $(readout_s) s must be finite and non-negative") + t = 2000 * (Float64(exposure_s) + Float64(readout_s)) + 1000 + t <= typemax(Int32) || error("capture_timeout_ms: $(t) ms does not fit a DCAM timeout (Int32 ms)") + return Int32(round(t)) +end + +""" + READOUT_FALLBACK_S + +The readout time, in seconds, that `readout_time` assumes when the camera does not report +`DCAM_IDPROP_TIMING_READOUTTIME`. It is deliberately generous, since it only lengthens a timeout. +""" +const READOUT_FALLBACK_S = 1.0 + +""" + readout_time(camera::DCAM4Camera) -> Float64 +The sensor readout time in seconds, read from the camera's `DCAM_IDPROP_TIMING_READOUTTIME`, which depends on the +current ROI and readout speed, so read it after `setroi!`. If the read fails or the value is not a finite, +non-negative number, it warns and returns `READOUT_FALLBACK_S`. +""" +function readout_time(camera::DCAM4Camera) + err, t = dcamprop_getvalue(camera.camera_handle, DCAM_IDPROP_TIMING_READOUTTIME) + if is_failed(err) || !isfinite(t) || t < 0 + @warn "DCAM4Camera $(camera.unique_id): the camera did not report its readout time ($(err), $(t)); assuming $(READOUT_FALLBACK_S) s for the frame-wait timeout" + return READOUT_FALLBACK_S + end + return Float64(t) +end + +""" + stop_and_release!(camera::DCAM4Camera) +Leave the camera with no capture running and no buffer attached, whatever an earlier call left behind (a live view, +a sequence, or a capture that failed). It reads the status first, so a camera with nothing to stop logs no error: +BUSY is stopped, then BUSY or READY (a buffer attached) is released, and STABLE needs nothing. If the status read +fails, it tries both. Sets `camera.is_running = false`. +""" +function stop_and_release!(camera::DCAM4Camera) + hdcam = camera.camera_handle + err, status = dcamcap_status(hdcam) + if is_failed(err) + dcamcap_stop(hdcam) + dcambuf_release(hdcam) + else + status == DCAMCAP_STATUS_BUSY && dcamcap_stop(hdcam) + (status == DCAMCAP_STATUS_BUSY || status == DCAMCAP_STATUS_READY) && dcambuf_release(hdcam) + end + camera.is_running = false + return nothing +end diff --git a/src/hardware_implementations/dcam4_camera/dcamwait.jl b/src/hardware_implementations/dcam4_camera/dcamwait.jl index 2124162..474bb9e 100644 --- a/src/hardware_implementations/dcam4_camera/dcamwait.jl +++ b/src/hardware_implementations/dcam4_camera/dcamwait.jl @@ -82,7 +82,8 @@ function dcamwait_abort(hwait::Ptr{Cvoid}) end function dcamwait_event(hwait::Ptr{Cvoid}, eventmask::Int32, timeout_millisec::Int32) - dws = DCAMWAIT_START(0, 0, eventmask, timeout_millisec) + timeout_millisec >= 0 || error("dcamwait_event: timeout $(timeout_millisec) ms is negative (DCAM reads 0x80000000 as INFINITE); every DCAM wait must be bounded") + dws = DCAMWAIT_START(eventmask, timeout_millisec) err = dcamwait_start(hwait, Ref(dws)) if is_failed(err) @error "DCAM Failed to Wait for Event: $(err)))" diff --git a/src/hardware_implementations/dcam4_camera/interface_methods.jl b/src/hardware_implementations/dcam4_camera/interface_methods.jl index d5c16dc..3f8436a 100644 --- a/src/hardware_implementations/dcam4_camera/interface_methods.jl +++ b/src/hardware_implementations/dcam4_camera/interface_methods.jl @@ -2,67 +2,95 @@ """ CameraInterface.getlastframe(camera::DCAM4Camera) + +Wait for the next frame for at most `capture_timeout_ms(exposure, readout)`. On a timeout or a failed wait it logs, +sets `last_error` and returns `nothing`. """ function CameraInterface.getlastframe(camera::DCAM4Camera) - # Get the last image frame from the camera hdcam = camera.camera_handle - - # check if camera is busy, if so setup a wait - err, status = DCAM4.dcamcap_status(hdcam) - - if true #status == DCAM_STATUS_BUSY - err, hwait = dcamwait_open(hdcam) - timeout_milisec = Int32(max(1000, round(1000 * camera.exposure_time * 1.5))) - err, event = dcamwait_event(hwait, Int32(DCAMWAIT_CAPEVENT_FRAMEREADY), timeout_milisec) + timeout_ms = capture_timeout_ms(camera.exposure_time, readout_time(camera)) + err, hwait = dcamwait_open(hdcam) + if is_failed(err) + @error "Failed to open a frame wait: $err" + camera.last_error = err + return nothing + end + try + err, event = dcamwait_event(hwait, Int32(DCAMWAIT_CAPEVENT_FRAMEREADY), timeout_ms) if is_failed(err) - @error "Failed to get last frame on event $event: $err" - camera.last_error = err - err, hwait = dcamwait_close(hwait) - return - end - if is_timeout(err) - @error "Timeout to get last frame on event $event: $err" + if is_timeout(err) + @error "Timeout to get last frame on event $event after $timeout_ms ms: $err" + else + @error "Failed to get last frame on event $event: $err" + end camera.last_error = err - err, hwait = dcamwait_close(hwait) - return + return nothing end - - framedata = dcambuf_getlastframe(hdcam) - err = dcamwait_close(hwait) - - else - framedata = dcambuf_getlastframe(hdcam) + return dcambuf_getlastframe(hdcam) + finally + dcamwait_close(hwait) end - - return framedata end """ CameraInterface.capture(camera::DCAM4Camera) + +`capture` first stops any earlier capture and releases its buffer. It waits at most +`capture_timeout_ms(exposure, readout)` (2 x (exposure + readout) + 1 s, with the readout read from the camera). +A timeout or a failed wait logs, sets `last_error` and throws, after the capture is stopped and the buffer +released. A failed buffer allocation or start returns `nothing` with `last_error` set, as before. """ function CameraInterface.capture(camera::DCAM4Camera) - # Start a capture + hdcam = camera.camera_handle camera.capture_mode = SINGLE_FRAME + # Clear whatever an earlier call left (a live view, a sequence, a failed capture): + # properties cannot change while a buffer is attached. + stop_and_release!(camera) setexposuretime!(camera) settriggermode!(camera) setroi!(camera) + readout_s = readout_time(camera) + timeout_ms = capture_timeout_ms(camera.exposure_time, readout_s) - err = dcambuf_alloc(camera.camera_handle, Int32(1)) - if is_failed(err) - camera.last_error = err - return - end - - err = dcamcap_start(camera.camera_handle, Int32(DCAMCAP_START_SNAP)) - if is_failed(err) - camera.last_error = err - return + hwait = C_NULL + try + err = dcambuf_alloc(hdcam, Int32(1)) + if is_failed(err) + camera.last_error = err + return nothing + end + # Arm the wait BEFORE the start, so a frame that is ready early cannot be missed. + err, hwait = dcamwait_open(hdcam) + if is_failed(err) + camera.last_error = err + error("DCAM4Camera $(camera.unique_id): capture could not open a frame wait ($(err)).") + end + err = dcamcap_start(hdcam, Int32(DCAMCAP_START_SNAP)) + if is_failed(err) + camera.last_error = err + return nothing + end + err, _ = dcamwait_event(hwait, Int32(DCAMWAIT_CAPEVENT_FRAMEREADY), timeout_ms) + if is_failed(err) + camera.last_error = err + what = is_timeout(err) ? + "timed out after $(timeout_ms) ms (exposure $(camera.exposure_time) s, readout $(readout_s) s)" : + "frame wait failed ($(err))" + @error "DCAM4Camera $(camera.unique_id): capture $(what)" + error("DCAM4Camera $(camera.unique_id): capture $(what). The capture was stopped and its buffer " * + "released, so the camera can be used again.") + end + return dcambuf_getlastframe(hdcam) + finally + # On every exit, so the camera is usable afterwards. A failure here is logged, never + # thrown, so it cannot replace the error that got us here. + try + stop_and_release!(camera) + catch e + @error "DCAM4Camera $(camera.unique_id): cleanup after capture failed" exception = e + end + hwait == C_NULL || dcamwait_close(hwait) end - - data = getlastframe(camera) - err = dcambuf_release(camera.camera_handle) - - return data end """ @@ -165,43 +193,60 @@ end """ CameraInterface.getdata(camera::DCAM4Camera) + +Wait for the end of the cycle for at most `capture_timeout_ms(N * exposure, N * readout)`. A cycle that has +already ended is read at once. A timeout logs, sets `last_error` and returns `nothing`. Every exit stops the +capture, releases the buffer and closes the wait, so in LIVE mode `getdata` ends the live view. """ function CameraInterface.getdata(camera::DCAM4Camera) - # Get the data from the camera - - err, hwait = dcamwait_open(camera.camera_handle) - timeout_milisec = Int32(max(1000, round(1000 * camera.exposure_time * camera.sequence_length * 1.5))) - err, event = dcamwait_event(hwait, Int32(DCAMWAIT_CAPEVENT_CYCLEEND), timeout_milisec) + hdcam = camera.camera_handle + n = camera.sequence_length + # 2 x N x (exposure + readout) + 1 s: a full-frame readout can be longer than the exposure. + timeout_milisec = capture_timeout_ms(camera.exposure_time * n, readout_time(camera) * n) + hwait = C_NULL + try + # A cycle that has already ended (READY: stopped, with the buffer attached) is read at once. + # Waiting for its CYCLEEND could miss it, and the cleanup below would then drop the data. + serr, status = dcamcap_status(hdcam) + if is_failed(serr) || status != DCAMCAP_STATUS_READY + err, hwait = dcamwait_open(hdcam) + if is_failed(err) + # No wait to arm (dcamwait_open logged it): read what is there, as a failed wait + # always did, rather than hand DCAM a null wait handle. + camera.last_error = err + hwait = C_NULL + else + display(timeout_milisec) + err, event = dcamwait_event(hwait, Int32(DCAMWAIT_CAPEVENT_CYCLEEND), timeout_milisec) + if is_timeout(err) + display(event) + @error "DCAM4Camera $(camera.unique_id): getdata timed out after $(timeout_milisec) ms" + camera.last_error = err + return nothing + end + end + end - camera.is_running = false - display(timeout_milisec) - if is_timeout(err) - display(event) - camera.last_error = err - err, hwait = dcamwait_close(hwait) - return - end - err = dcamwait_close(hwait) - - if camera.capture_mode == SEQUENCE - display("Getting sequence data") - im_width, im_height = dcamprop_getsize(camera.camera_handle) - data = zeros(UInt16, im_height, im_width, camera.sequence_length) # (H, W, N) convention - for i in 1:camera.sequence_length - data[:, :, i] = dcambuf_getframe(camera.camera_handle, Int32(i - 1)) + if camera.capture_mode == SEQUENCE + display("Getting sequence data") + im_width, im_height = dcamprop_getsize(hdcam) + data = zeros(UInt16, im_height, im_width, n) # (H, W, N) convention + for i in 1:n + data[:, :, i] = dcambuf_getframe(hdcam, Int32(i - 1)) + end + return data + elseif camera.capture_mode == SINGLE_FRAME || camera.capture_mode == LIVE + return dcambuf_getlastframe(hdcam) + end + finally + # On every exit: stop, release the buffer, close the wait. A failure here is logged, never + # thrown, so it cannot replace the error or the data that got us here. + try + stop_and_release!(camera) + catch e + @error "DCAM4Camera $(camera.unique_id): cleanup after getdata failed" exception = e end - dcambuf_release(camera.camera_handle) - return data - - elseif camera.capture_mode == SINGLE_FRAME - data = dcambuf_getlastframe(camera.camera_handle) - dcambuf_release(camera.camera_handle) - return data - - elseif camera.capture_mode == LIVE - data = dcambuf_getlastframe(camera.camera_handle) - dcambuf_release(camera.camera_handle) - return data + hwait == C_NULL || dcamwait_close(hwait) end end diff --git a/test/dcam4_pure.jl b/test/dcam4_pure.jl new file mode 100644 index 0000000..1b17376 --- /dev/null +++ b/test/dcam4_pure.jl @@ -0,0 +1,35 @@ +# The DCAM library is absent on the machines that run this suite, so this file tests only what +# runs without it. The capture error paths call the DCAM library first and cannot be exercised +# without a swappable-library fake, which is a follow-up. +@testset "DCAM4 (no library)" begin + DC = MicroscopeControl.HardwareImplementations.DCAM4 + + @testset "capture_timeout_ms" begin + @test DC.capture_timeout_ms(0.0125, 0.043) == 1111 + @test DC.capture_timeout_ms(0.25, 0.25) == 2000 + @test DC.capture_timeout_ms(0, 0) == 1000 + @test DC.capture_timeout_ms(0.25, 0.25) isa Int32 + @test_throws ErrorException DC.capture_timeout_ms(NaN, 0) + @test_throws ErrorException DC.capture_timeout_ms(Inf, 0) + @test_throws ErrorException DC.capture_timeout_ms(-0.001, 0) + @test_throws ErrorException DC.capture_timeout_ms(0, -1) + @test_throws ErrorException DC.capture_timeout_ms(0, NaN) + @test_throws ErrorException DC.capture_timeout_ms(1e7, 0) + end + + @testset "wait parameter struct" begin + @test DC.DCAMWAIT_START(Int32(2), Int32(1000)).size == 16 + @test DC.DCAMWAIT_START(Int32(2), Int32(1000)).size == sizeof(DC.DCAMWAIT_START) + end + + @testset "an unbounded wait is refused before any library call" begin + @test_throws r"bounded" DC.dcamwait_event(C_NULL, Int32(2), Int32(-1)) + @test_throws r"bounded" DC.dcamwait_event(C_NULL, Int32(2), reinterpret(Int32, 0x80000000)) + end + + @testset "dcamwait_close is never destructured" begin + src = read(pkgdir(MicroscopeControl, "src", "hardware_implementations", "dcam4_camera", + "interface_methods.jl"), String) + @test !occursin(r",\s*\w+\s*=\s*dcamwait_close\(", src) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 893616a..442fd1a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1525,6 +1525,8 @@ lab_summary("Core") do include("pi_n472.jl") + include("dcam4_pure.jl") + include("contract.jl") include("skills.jl") include("gui.jl") From 485e68a9bc6ffedadf6bbf305514cd440ff6042b Mon Sep 17 00:00:00 2001 From: kalidke Date: Tue, 29 Sep 2026 19:37:03 -0600 Subject: [PATCH 3/5] Review fixes on #75 (D1-D8): capture refuses while running, getdata polls against a deadline, sequence's poller has one Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 35 +++--- .../dcam4_camera/dcam_helpers.jl | 69 ++++++++++-- .../dcam4_camera/dcambuf.jl | 13 ++- .../dcam4_camera/interface_methods.jl | 102 ++++++++---------- .../dcam4_camera/types.jl | 3 +- test/dcam4_pure.jl | 28 +++-- 6 files changed, 159 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a0ace..bf86422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,24 +12,35 @@ the next version with `-DEV`). ### Fixed -- `DCAM4Camera` `capture` could hang or leave the camera unusable after a missed frame. Every frame wait is now +- `DCAM4Camera` `capture` could hang, or leave the camera unusable after a missed frame. Its frame wait is now bounded by 2 x (exposure + readout) + 1 s, with the readout read from the camera (`DCAM_IDPROP_TIMING_READOUTTIME`); the wait is armed before the capture starts; the wait's parameter struct carries its size (it was sent as 0); and every exit stops the capture, releases the buffer and closes the wait. - A timeout or failed wait in `capture` now logs, sets `last_error` and throws a clear error (it threw a MethodError - before). `capture` also stops a running live view or sequence before it starts. Reported from the quickbeam rig: - full frame at 12.5 ms. -- `DCAM4Camera` `getlastframe` and `getdata`: a timeout or failed wait no longer throws a MethodError. It logs, - sets `last_error` and returns `nothing`, as the code intended. `getdata`'s wait for the end of a sequence now - includes the readout (2 x N x (exposure + readout) + 1 s; before, a full-frame sequence of short exposures timed - out). A sequence that has already ended is read without waiting. Every exit stops the capture, releases the - buffer and closes the wait. + A timeout or failed wait logs, sets `last_error` and throws a clear error (it threw a MethodError before). + Reported from the quickbeam rig: full frame at 12.5 ms and at 100 ms, including the first capture in a fresh + session. +- `DCAM4Camera` `getlastframe`: a timeout or failed wait no longer throws a MethodError. It logs, sets `last_error` + and returns `nothing`, as the code intended. Its wait is bounded the same way, with the readout time read once per + `live`, `sequence` or `capture` and cached. +- `DCAM4Camera` `getdata` in SEQUENCE mode polls the capture status against a deadline of + 2 x N x (exposure + readout) + 1 s instead of waiting for the end-of-cycle event. A sequence that has already + ended cannot be missed, a full-frame sequence of short exposures no longer times out, and an interrupt can land + while it waits. A frame that cannot be read returns `nothing` with `last_error` set (it threw a MethodError). + Every exit stops the capture and releases the buffer. +- `DCAM4Camera` `sequence`: the task that marks the sequence finished gives up at the same deadline and stops a + capture stuck running, so `is_running` can no longer stay true forever. +- `DCAM4Camera`: clearing a leftover capture handles every state (a capture in the ERROR state was neither stopped + nor released), and `abort` now does exactly that. ### Changed -- `DCAM4Camera` `getdata` in LIVE mode now ends the live view, because every exit stops the capture and releases - the buffer. It already marked the camera not running, but DCAM refused the buffer release while capturing, so the - live view went on. No known rig code calls `getdata` during a live view. +- `DCAM4Camera` `capture` refuses (throws "Stop the live view or sequence first") while a live view or sequence is + running (`is_running`), instead of failing at the buffer allocation and returning `nothing`. It never releases a + buffer another task may be waiting on. A leftover from an earlier call (a failed capture, or a sequence that + ended and was never read) is stopped and released first. +- `DCAM4Camera` `getdata` in LIVE mode returns the newest frame at once and ends the live view, because every exit + stops the capture and releases the buffer. It already marked the camera not running, but DCAM refused the buffer + release while capturing. No known rig code calls `getdata` during a live view. ## [0.2.5] - 2026-09-29 diff --git a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl index a540c1f..cf9fe8a 100644 --- a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl +++ b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl @@ -233,36 +233,83 @@ const READOUT_FALLBACK_S = 1.0 readout_time(camera::DCAM4Camera) -> Float64 The sensor readout time in seconds, read from the camera's `DCAM_IDPROP_TIMING_READOUTTIME`, which depends on the -current ROI and readout speed, so read it after `setroi!`. If the read fails or the value is not a finite, -non-negative number, it warns and returns `READOUT_FALLBACK_S`. +current ROI and readout speed, so read it after `setroi!`. It reads the camera and caches the value in +`camera.readout_s`. If the read fails or the value is not a finite, non-negative number, it warns and returns (and +caches) `READOUT_FALLBACK_S`. """ function readout_time(camera::DCAM4Camera) err, t = dcamprop_getvalue(camera.camera_handle, DCAM_IDPROP_TIMING_READOUTTIME) if is_failed(err) || !isfinite(t) || t < 0 - @warn "DCAM4Camera $(camera.unique_id): the camera did not report its readout time ($(err), $(t)); assuming $(READOUT_FALLBACK_S) s for the frame-wait timeout" + @warn "DCAM4Camera $(camera.unique_id): the camera did not report its readout time ($(err), $(t)); assuming $(READOUT_FALLBACK_S) s for the frame-wait timeout" maxlog = 1 + camera.readout_s = READOUT_FALLBACK_S return READOUT_FALLBACK_S end + camera.readout_s = Float64(t) return Float64(t) end +""" + cached_readout_time(camera::DCAM4Camera) -> Float64 + +The readout time cached by the last `readout_time` call (`live`, `sequence` and `capture` refresh it after +`setroi!`), or a fresh read if none is cached. Used on per-frame paths, so a live view does not read a +property per frame. +""" +cached_readout_time(camera::DCAM4Camera) = + isfinite(camera.readout_s) ? camera.readout_s : readout_time(camera) + """ stop_and_release!(camera::DCAM4Camera) Leave the camera with no capture running and no buffer attached, whatever an earlier call left behind (a live view, -a sequence, or a capture that failed). It reads the status first, so a camera with nothing to stop logs no error: -BUSY is stopped, then BUSY or READY (a buffer attached) is released, and STABLE needs nothing. If the status read -fails, it tries both. Sets `camera.is_running = false`. +a sequence, or a capture that failed). It reads the status first and does nothing only when the status is known to be +STABLE (no buffer) or UNSTABLE. A failed status read, BUSY or ERROR is stopped (unless READY, which is already +stopped); then anything with a buffer is released. The helpers log their own failures and never throw. Sets +`camera.is_running = false`. """ function stop_and_release!(camera::DCAM4Camera) hdcam = camera.camera_handle err, status = dcamcap_status(hdcam) - if is_failed(err) - dcamcap_stop(hdcam) + # Nothing to do only when the status is known to be STABLE (no buffer) or UNSTABLE. A failed + # status read, BUSY or ERROR is stopped; then anything with a buffer is released. The helpers + # log their own failures and never throw. + if is_failed(err) || !(status == DCAMCAP_STATUS_STABLE || status == DCAMCAP_STATUS_UNSTABLE) + status == DCAMCAP_STATUS_READY || dcamcap_stop(hdcam) dcambuf_release(hdcam) - else - status == DCAMCAP_STATUS_BUSY && dcamcap_stop(hdcam) - (status == DCAMCAP_STATUS_BUSY || status == DCAMCAP_STATUS_READY) && dcambuf_release(hdcam) end camera.is_running = false return nothing end + +""" + STATUS_POLL_S + +The interval, in seconds, at which `wait_not_busy` polls the capture status. +""" +const STATUS_POLL_S = 0.01 + +""" + wait_not_busy(camera::DCAM4Camera, timeout_ms, what) -> Bool + +Poll the capture status every `STATUS_POLL_S` until it is no longer BUSY, for at most `timeout_ms`. Returns `true` +when the capture has ended. On a timeout or a failed status read it logs (naming `what`), sets `last_error` and +returns `false`. It sleeps between polls, so an interrupt can land, unlike a blocking DCAM wait. +""" +function wait_not_busy(camera::DCAM4Camera, timeout_ms::Integer, what::AbstractString) + deadline = time() + timeout_ms / 1000 + while true + err, status = dcamcap_status(camera.camera_handle) + if is_failed(err) + camera.last_error = err + @error "DCAM4Camera $(camera.unique_id): $(what) could not read the capture status ($(err))" + return false + end + status == DCAMCAP_STATUS_BUSY || return true + if time() >= deadline + camera.last_error = DCAMERR_TIMEOUT + @error "DCAM4Camera $(camera.unique_id): $(what) timed out after $(timeout_ms) ms with the capture still running" + return false + end + sleep(STATUS_POLL_S) + end +end diff --git a/src/hardware_implementations/dcam4_camera/dcambuf.jl b/src/hardware_implementations/dcam4_camera/dcambuf.jl index 7d8df9e..f2f5f28 100644 --- a/src/hardware_implementations/dcam4_camera/dcambuf.jl +++ b/src/hardware_implementations/dcam4_camera/dcambuf.jl @@ -132,7 +132,7 @@ end # end -function dcambuf_getframe(hdcam::Ptr{Cvoid}, iFrame::Int32) +function dcambuf_getframe_err(hdcam::Ptr{Cvoid}, iFrame::Int32) err, width = dcamprop_getvalue(hdcam, Int32(DCAM_IDPROP_IMAGE_WIDTH)) err, height = dcamprop_getvalue(hdcam, Int32(DCAM_IDPROP_IMAGE_HEIGHT)) err, rowbytes = dcamprop_getvalue(hdcam, Int32(DCAM_IDPROP_IMAGE_ROWBYTES)) @@ -146,7 +146,7 @@ function dcambuf_getframe(hdcam::Ptr{Cvoid}, iFrame::Int32) bytes_per_pixel = 1 else @error "Unsupported pixel type" - return nothing + return DCAMERR_INVALIDPIXELTYPE, nothing end stride_elements = Int(rowbytes) รท bytes_per_pixel @@ -165,16 +165,19 @@ function dcambuf_getframe(hdcam::Ptr{Cvoid}, iFrame::Int32) err = @ccall "dcamapi.dll".dcambuf_copyframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR if is_failed(err) - @error "DCAM Failed to Copy Frame" - return nothing + @error "DCAM Failed to Copy Frame $(iFrame): $(err)" + return err, nothing end # Reshape from row-major C buffer and permute to column-major Julia (H, W) data = permutedims(reshape(temp_buffer, (Int(width), Int(height))), (2, 1)) - return data + return DCAMERR_SUCCESS, data end +# The data only (`nothing` on a failure, which is logged); `dcambuf_getframe_err` also returns the DCAMERR. +dcambuf_getframe(hdcam::Ptr{Cvoid}, iFrame::Int32) = dcambuf_getframe_err(hdcam, iFrame)[2] + function dcambuf_getlastframe(hdcam::Ptr{Cvoid}) return dcambuf_getframe(hdcam::Ptr{Cvoid}, Int32(-1)) end diff --git a/src/hardware_implementations/dcam4_camera/interface_methods.jl b/src/hardware_implementations/dcam4_camera/interface_methods.jl index 3f8436a..78e0b20 100644 --- a/src/hardware_implementations/dcam4_camera/interface_methods.jl +++ b/src/hardware_implementations/dcam4_camera/interface_methods.jl @@ -3,12 +3,13 @@ """ CameraInterface.getlastframe(camera::DCAM4Camera) -Wait for the next frame for at most `capture_timeout_ms(exposure, readout)`. On a timeout or a failed wait it logs, +Wait for the next frame for at most `capture_timeout_ms(exposure, readout)`, with the readout cached by +`readout_time` (refreshed by `live`, `sequence` and `capture`). On a timeout or a failed wait it logs, sets `last_error` and returns `nothing`. """ function CameraInterface.getlastframe(camera::DCAM4Camera) hdcam = camera.camera_handle - timeout_ms = capture_timeout_ms(camera.exposure_time, readout_time(camera)) + timeout_ms = capture_timeout_ms(camera.exposure_time, cached_readout_time(camera)) err, hwait = dcamwait_open(hdcam) if is_failed(err) @error "Failed to open a frame wait: $err" @@ -35,15 +36,21 @@ end """ CameraInterface.capture(camera::DCAM4Camera) -`capture` first stops any earlier capture and releases its buffer. It waits at most +`capture` refuses (throws) while a live view or sequence is running (`is_running`); otherwise it first stops and +releases any leftover from an earlier call. It waits at most `capture_timeout_ms(exposure, readout)` (2 x (exposure + readout) + 1 s, with the readout read from the camera). A timeout or a failed wait logs, sets `last_error` and throws, after the capture is stopped and the buffer released. A failed buffer allocation or start returns `nothing` with `last_error` set, as before. """ function CameraInterface.capture(camera::DCAM4Camera) + # Never stop or release a capture another task may be waiting on: a live view's + # getlastframe loop crashes the process if its buffer is released under the wait. + camera.is_running && error("DCAM4Camera $(camera.unique_id): capture refused while a live view or " * + "sequence is running (is_running). Stop the live view or sequence first (abort(camera), or " * + "getdata after a sequence).") hdcam = camera.camera_handle camera.capture_mode = SINGLE_FRAME - # Clear whatever an earlier call left (a live view, a sequence, a failed capture): + # Only leftovers get here (a failed capture, or a sequence that ended and was never read): # properties cannot change while a buffer is attached. stop_and_release!(camera) setexposuretime!(camera) @@ -106,6 +113,7 @@ function CameraInterface.live(camera::DCAM4Camera; nframes=10) setexposuretime!(camera) settriggermode!(camera) setroi!(camera) + readout_time(camera) # refresh the cached readout for getlastframe # Allocate memory for the image buffer err = dcambuf_alloc(camera.camera_handle, Int32(nframes)) @@ -126,6 +134,10 @@ end """ CameraInterface.sequence(camera::DCAM4Camera, nframes::Real) + +Start a sequence of `nframes` and return. A task marks `is_running` false when the sequence ends, or after +`capture_timeout_ms(N * exposure, N * readout)`, when it stops a capture still running (logged, with `last_error` +set). """ function CameraInterface.sequence(camera::DCAM4Camera, nframes::Real) # Start collection of a sequence @@ -136,6 +148,7 @@ function CameraInterface.sequence(camera::DCAM4Camera, nframes::Real) settriggermode!(camera) setroi!(camera) + timeout_ms = capture_timeout_ms(camera.exposure_time * camera.sequence_length, readout_time(camera) * camera.sequence_length) # Allocate memory for the image buffer err = dcambuf_alloc(camera.camera_handle, Int32(camera.sequence_length)) @@ -152,24 +165,15 @@ function CameraInterface.sequence(camera::DCAM4Camera, nframes::Real) return end camera.is_running = 1 - err, frameinterval = DCAM4.dcamprop_getvalue(camera.camera_handle, DCAM4.DCAM_IDPROP_INTERNAL_FRAMEINTERVAL) - - err, status = DCAM4.dcamcap_status(camera.camera_handle) - #println("Starting sequence") @async begin println("Starting sequence") - while status == DCAM4.DCAMCAP_STATUS_BUSY - #println("Sequence running") - sleep(frameinterval) - err, status = DCAM4.dcamcap_status(camera.camera_handle) - #println(status) + # Bounded like getdata: a capture stuck BUSY is stopped, so is_running cannot stay true forever. + if !wait_not_busy(camera, timeout_ms, "sequence") + dcamcap_stop(camera.camera_handle) end - camera.is_running = 0 + camera.is_running = false println("Sequence done") end - - - return end @@ -183,70 +187,56 @@ end """ CameraInterface.abort(camera::DCAM4Camera) + +Stop any capture and release its buffer (`stop_and_release!`). Returns `nothing`. """ -function CameraInterface.abort(camera::DCAM4Camera) - # Abort a capture - dcamcap_stop(camera.camera_handle::Ptr{Cvoid}) - err = dcambuf_release(camera.camera_handle) - camera.is_running = 0 -end +CameraInterface.abort(camera::DCAM4Camera) = stop_and_release!(camera) """ CameraInterface.getdata(camera::DCAM4Camera) -Wait for the end of the cycle for at most `capture_timeout_ms(N * exposure, N * readout)`. A cycle that has -already ended is read at once. A timeout logs, sets `last_error` and returns `nothing`. Every exit stops the -capture, releases the buffer and closes the wait, so in LIVE mode `getdata` ends the live view. +In SEQUENCE mode it polls the capture status until the sequence ends, for at most +`capture_timeout_ms(N * exposure, N * readout)`, and reads the N frames. In SINGLE_FRAME or LIVE mode it reads the +newest frame at once. A timeout, a failed status read or a frame that cannot be read logs, sets `last_error` and +returns `nothing`. Every exit stops the capture and releases the buffer, so in LIVE mode `getdata` ends the live +view. """ function CameraInterface.getdata(camera::DCAM4Camera) hdcam = camera.camera_handle n = camera.sequence_length - # 2 x N x (exposure + readout) + 1 s: a full-frame readout can be longer than the exposure. - timeout_milisec = capture_timeout_ms(camera.exposure_time * n, readout_time(camera) * n) - hwait = C_NULL try - # A cycle that has already ended (READY: stopped, with the buffer attached) is read at once. - # Waiting for its CYCLEEND could miss it, and the cleanup below would then drop the data. - serr, status = dcamcap_status(hdcam) - if is_failed(serr) || status != DCAMCAP_STATUS_READY - err, hwait = dcamwait_open(hdcam) - if is_failed(err) - # No wait to arm (dcamwait_open logged it): read what is there, as a failed wait - # always did, rather than hand DCAM a null wait handle. - camera.last_error = err - hwait = C_NULL - else - display(timeout_milisec) - err, event = dcamwait_event(hwait, Int32(DCAMWAIT_CAPEVENT_CYCLEEND), timeout_milisec) - if is_timeout(err) - display(event) - @error "DCAM4Camera $(camera.unique_id): getdata timed out after $(timeout_milisec) ms" - camera.last_error = err - return nothing - end - end - end - if camera.capture_mode == SEQUENCE - display("Getting sequence data") + # 2 x N x (exposure + readout) + 1 s, inside the try so a throw here still cleans up. + timeout_ms = capture_timeout_ms(camera.exposure_time * n, cached_readout_time(camera) * n) + # Poll the status against that deadline rather than wait for the end-of-cycle event: + # a sequence that has already ended cannot be missed, and an interrupt can land. + wait_not_busy(camera, timeout_ms, "getdata") || return nothing im_width, im_height = dcamprop_getsize(hdcam) data = zeros(UInt16, im_height, im_width, n) # (H, W, N) convention for i in 1:n - data[:, :, i] = dcambuf_getframe(hdcam, Int32(i - 1)) + err, frame = dcambuf_getframe_err(hdcam, Int32(i - 1)) + if frame === nothing + camera.last_error = err + @error "DCAM4Camera $(camera.unique_id): getdata could not read frame $(i) of $(n) ($(err))" + return nothing + end + data[:, :, i] = frame end return data elseif camera.capture_mode == SINGLE_FRAME || camera.capture_mode == LIVE - return dcambuf_getlastframe(hdcam) + # No cycle to wait for: read the newest frame now. + err, frame = dcambuf_getframe_err(hdcam, Int32(-1)) + frame === nothing && (camera.last_error = err) + return frame end finally - # On every exit: stop, release the buffer, close the wait. A failure here is logged, never + # On every exit: stop the capture and release the buffer. A failure here is logged, never # thrown, so it cannot replace the error or the data that got us here. try stop_and_release!(camera) catch e @error "DCAM4Camera $(camera.unique_id): cleanup after getdata failed" exception = e end - hwait == C_NULL || dcamwait_close(hwait) end end diff --git a/src/hardware_implementations/dcam4_camera/types.jl b/src/hardware_implementations/dcam4_camera/types.jl index cb64a4d..47e99c8 100644 --- a/src/hardware_implementations/dcam4_camera/types.jl +++ b/src/hardware_implementations/dcam4_camera/types.jl @@ -27,6 +27,7 @@ mutable struct DCAM4Camera <: Camera is_running::Bool camerastate data::Array{UInt16} + readout_s::Float64 end function DCAM4Camera(dev_id::Int = 0; @@ -64,5 +65,5 @@ function DCAM4Camera(dev_id::Int = 0; err, exposure_time = dcamprop_getvalue(dco.hdcam, DCAM_IDPROP_EXPOSURETIME) data = zeros(UInt16, im_width, im_height) - DCAM4Camera(unique_id, camera_format, dco.hdcam, exposure_time, frame_rate, roi, capture_mode, trigger_mode, sequence_length, last_error, is_running, camerastate,data) + DCAM4Camera(unique_id, camera_format, dco.hdcam, exposure_time, frame_rate, roi, capture_mode, trigger_mode, sequence_length, last_error, is_running, camerastate, data, NaN) end \ No newline at end of file diff --git a/test/dcam4_pure.jl b/test/dcam4_pure.jl index 1b17376..46427a7 100644 --- a/test/dcam4_pure.jl +++ b/test/dcam4_pure.jl @@ -1,8 +1,15 @@ # The DCAM library is absent on the machines that run this suite, so this file tests only what -# runs without it. The capture error paths call the DCAM library first and cannot be exercised -# without a swappable-library fake, which is a follow-up. +# runs without it. The capture error paths, getdata's poll and stop_and_release! call the DCAM +# library first and cannot run without a swappable-library fake (a follow-up). The refusal and the +# cache are tested because they come before any library call. @testset "DCAM4 (no library)" begin DC = MicroscopeControl.HardwareImplementations.DCAM4 + CameraInterface = MicroscopeControl.HardwareInterfaces.CameraInterface + + # A camera built with no library call, through the positional constructor. + fake_camera() = DC.DCAM4Camera("fake", DC.CameraFormat(1, 1, 1, 1, "SCMOS"), C_NULL, 0.01, 10.0, + DC.CameraROI(0, 0, 1, 1), DC.LIVE, DC.DCAMPROP_TRIGGER_MODE__NORMAL, 10, DC.DCAMERR_SUCCESS, + false, 0, zeros(UInt16, 1, 1), NaN) @testset "capture_timeout_ms" begin @test DC.capture_timeout_ms(0.0125, 0.043) == 1111 @@ -27,9 +34,18 @@ @test_throws r"bounded" DC.dcamwait_event(C_NULL, Int32(2), reinterpret(Int32, 0x80000000)) end - @testset "dcamwait_close is never destructured" begin - src = read(pkgdir(MicroscopeControl, "src", "hardware_implementations", "dcam4_camera", - "interface_methods.jl"), String) - @test !occursin(r",\s*\w+\s*=\s*dcamwait_close\(", src) + @testset "capture refuses while running, before any library call" begin + cam = fake_camera() + cam.is_running = true + cam.capture_mode = DC.LIVE + @test_throws r"Stop the live view or sequence first" CameraInterface.capture(cam) + @test cam.capture_mode == DC.LIVE + @test cam.is_running + end + + @testset "cached readout time" begin + cam = fake_camera() + cam.readout_s = 0.043 + @test DC.cached_readout_time(cam) == 0.043 end end From bcfa0be282ed2ae6cf8bad8ac2b516025ae427f3 Mon Sep 17 00:00:00 2001 From: kalidke Date: Tue, 29 Sep 2026 19:54:15 -0600 Subject: [PATCH 4/5] Fix-check items on #75 (E1-E3): a stale sequence poller never touches a newer capture, getdata leaves a live view running Also: getdata reads a READY buffer that never ran as LOSTFRAME, and capture and getlastframe set last_error on a failed frame copy. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 13 +++-- .../dcam4_camera/dcam_helpers.jl | 33 +++++++---- .../dcam4_camera/interface_methods.jl | 57 ++++++++++++++----- .../dcam4_camera/types.jl | 3 +- test/dcam4_pure.jl | 9 ++- 5 files changed, 80 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf86422..b03ed9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,14 +21,18 @@ the next version with `-DEV`). session. - `DCAM4Camera` `getlastframe`: a timeout or failed wait no longer throws a MethodError. It logs, sets `last_error` and returns `nothing`, as the code intended. Its wait is bounded the same way, with the readout time read once per - `live`, `sequence` or `capture` and cached. + `live`, `sequence` or `capture` and cached. A frame that cannot be copied, here or in `capture`, sets `last_error`. - `DCAM4Camera` `getdata` in SEQUENCE mode polls the capture status against a deadline of 2 x N x (exposure + readout) + 1 s instead of waiting for the end-of-cycle event. A sequence that has already ended cannot be missed, a full-frame sequence of short exposures no longer times out, and an interrupt can land while it waits. A frame that cannot be read returns `nothing` with `last_error` set (it threw a MethodError). - Every exit stops the capture and releases the buffer. + Every exit stops the capture and releases the buffer. A sequence that transferred fewer frames than requested (or + never ran) returns `nothing` with `last_error` set to `DCAMERR_LOSTFRAME`. In LIVE mode `getdata` returns the + newest frame at once and leaves the live view running; it used to clear `is_running` while the view ran on. - `DCAM4Camera` `sequence`: the task that marks the sequence finished gives up at the same deadline and stops a - capture stuck running, so `is_running` can no longer stay true forever. + capture stuck running, so `is_running` can no longer stay true forever. It acts only while its sequence is + current, so it never stops or marks finished a newer live view or sequence, and a failed status read is retried + until the deadline. - `DCAM4Camera`: clearing a leftover capture handles every state (a capture in the ERROR state was neither stopped nor released), and `abort` now does exactly that. @@ -38,9 +42,6 @@ the next version with `-DEV`). running (`is_running`), instead of failing at the buffer allocation and returning `nothing`. It never releases a buffer another task may be waiting on. A leftover from an earlier call (a failed capture, or a sequence that ended and was never read) is stopped and released first. -- `DCAM4Camera` `getdata` in LIVE mode returns the newest frame at once and ends the live view, because every exit - stops the capture and releases the buffer. It already marked the camera not running, but DCAM refused the buffer - release while capturing. No known rig code calls `getdata` during a live view. ## [0.2.5] - 2026-09-29 diff --git a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl index cf9fe8a..1a786a1 100644 --- a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl +++ b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl @@ -265,9 +265,11 @@ Leave the camera with no capture running and no buffer attached, whatever an ear a sequence, or a capture that failed). It reads the status first and does nothing only when the status is known to be STABLE (no buffer) or UNSTABLE. A failed status read, BUSY or ERROR is stopped (unless READY, which is already stopped); then anything with a buffer is released. The helpers log their own failures and never throw. Sets -`camera.is_running = false`. +`camera.is_running = false`. It also increments `capture_generation`, so a `sequence` poller started before it no +longer acts. Every start goes through it first: `live` and `sequence` through `abort`, and `capture` directly. """ function stop_and_release!(camera::DCAM4Camera) + camera.capture_generation += 1 # any stop makes every older sequence poller stale hdcam = camera.camera_handle err, status = dcamcap_status(hdcam) # Nothing to do only when the status is known to be STABLE (no buffer) or UNSTABLE. A failed @@ -289,27 +291,34 @@ The interval, in seconds, at which `wait_not_busy` polls the capture status. const STATUS_POLL_S = 0.01 """ - wait_not_busy(camera::DCAM4Camera, timeout_ms, what) -> Bool + wait_not_busy(camera::DCAM4Camera, timeout_ms, what; current = () -> true) -> Bool Poll the capture status every `STATUS_POLL_S` until it is no longer BUSY, for at most `timeout_ms`. Returns `true` -when the capture has ended. On a timeout or a failed status read it logs (naming `what`), sets `last_error` and -returns `false`. It sleeps between polls, so an interrupt can land, unlike a blocking DCAM wait. +when the capture has ended. A failed status read is logged once and retried until the deadline. At the deadline +it logs (naming `what`), sets `last_error` (the status read's error if the last read failed, else +`DCAMERR_TIMEOUT`) and returns `false`. It sleeps between polls, so an interrupt can land, unlike a blocking DCAM +wait. It also returns `false` as soon as `current()` is false (a newer capture replaced the one it watches), +touching nothing. """ -function wait_not_busy(camera::DCAM4Camera, timeout_ms::Integer, what::AbstractString) +function wait_not_busy(camera::DCAM4Camera, timeout_ms::Integer, what::AbstractString; + current::Function = () -> true) deadline = time() + timeout_ms / 1000 - while true + logged = false + while current() err, status = dcamcap_status(camera.camera_handle) if is_failed(err) - camera.last_error = err - @error "DCAM4Camera $(camera.unique_id): $(what) could not read the capture status ($(err))" - return false + logged || @error "DCAM4Camera $(camera.unique_id): $(what) could not read the capture status ($(err)); retrying until the deadline" + logged = true + elseif status != DCAMCAP_STATUS_BUSY + return true end - status == DCAMCAP_STATUS_BUSY || return true if time() >= deadline - camera.last_error = DCAMERR_TIMEOUT - @error "DCAM4Camera $(camera.unique_id): $(what) timed out after $(timeout_ms) ms with the capture still running" + camera.last_error = is_failed(err) ? err : DCAMERR_TIMEOUT + @error "DCAM4Camera $(camera.unique_id): $(what) timed out after $(timeout_ms) ms " * + (is_failed(err) ? "(the status read fails: $(err))" : "with the capture still running") return false end sleep(STATUS_POLL_S) end + return false end diff --git a/src/hardware_implementations/dcam4_camera/interface_methods.jl b/src/hardware_implementations/dcam4_camera/interface_methods.jl index 78e0b20..0a36e12 100644 --- a/src/hardware_implementations/dcam4_camera/interface_methods.jl +++ b/src/hardware_implementations/dcam4_camera/interface_methods.jl @@ -5,7 +5,7 @@ Wait for the next frame for at most `capture_timeout_ms(exposure, readout)`, with the readout cached by `readout_time` (refreshed by `live`, `sequence` and `capture`). On a timeout or a failed wait it logs, -sets `last_error` and returns `nothing`. +sets `last_error` and returns `nothing`. A frame that cannot be copied returns `nothing` with `last_error` set. """ function CameraInterface.getlastframe(camera::DCAM4Camera) hdcam = camera.camera_handle @@ -27,7 +27,9 @@ function CameraInterface.getlastframe(camera::DCAM4Camera) camera.last_error = err return nothing end - return dcambuf_getlastframe(hdcam) + err, frame = dcambuf_getframe_err(hdcam, Int32(-1)) + frame === nothing && (camera.last_error = err) + return frame finally dcamwait_close(hwait) end @@ -40,7 +42,7 @@ end releases any leftover from an earlier call. It waits at most `capture_timeout_ms(exposure, readout)` (2 x (exposure + readout) + 1 s, with the readout read from the camera). A timeout or a failed wait logs, sets `last_error` and throws, after the capture is stopped and the buffer -released. A failed buffer allocation or start returns `nothing` with `last_error` set, as before. +released. A failed buffer allocation or start returns `nothing` with `last_error` set, as before. A frame that cannot be copied returns `nothing` with `last_error` set. """ function CameraInterface.capture(camera::DCAM4Camera) # Never stop or release a capture another task may be waiting on: a live view's @@ -87,7 +89,9 @@ function CameraInterface.capture(camera::DCAM4Camera) error("DCAM4Camera $(camera.unique_id): capture $(what). The capture was stopped and its buffer " * "released, so the camera can be used again.") end - return dcambuf_getlastframe(hdcam) + err, frame = dcambuf_getframe_err(hdcam, Int32(-1)) + frame === nothing && (camera.last_error = err) + return frame finally # On every exit, so the camera is usable afterwards. A failure here is logged, never # thrown, so it cannot replace the error that got us here. @@ -137,7 +141,8 @@ end Start a sequence of `nframes` and return. A task marks `is_running` false when the sequence ends, or after `capture_timeout_ms(N * exposure, N * readout)`, when it stops a capture still running (logged, with `last_error` -set). +set). The task acts only while the sequence is current (no `abort`, `live`, `sequence`, `capture` or `getdata` +since); it always ends, and a throw inside it still clears `is_running` for its own sequence. """ function CameraInterface.sequence(camera::DCAM4Camera, nframes::Real) # Start collection of a sequence @@ -165,14 +170,21 @@ function CameraInterface.sequence(camera::DCAM4Camera, nframes::Real) return end camera.is_running = 1 + gen = camera.capture_generation # this sequence's generation (abort, above, already moved it on) @async begin println("Starting sequence") - # Bounded like getdata: a capture stuck BUSY is stopped, so is_running cannot stay true forever. - if !wait_not_busy(camera, timeout_ms, "sequence") - dcamcap_stop(camera.camera_handle) + current() = camera.capture_generation == gen + try + # Bounded like getdata. A capture stuck BUSY is stopped, but only while it is still this + # sequence: a newer live view or sequence must never be stopped by a stale poller. + if !wait_not_busy(camera, timeout_ms, "sequence"; current = current) && current() + dcamcap_stop(camera.camera_handle) + end + finally + # Only this sequence's poller may clear is_running; a newer capture owns it now. + current() && (camera.is_running = false) + println("Sequence done") end - camera.is_running = false - println("Sequence done") end return end @@ -196,14 +208,22 @@ CameraInterface.abort(camera::DCAM4Camera) = stop_and_release!(camera) CameraInterface.getdata(camera::DCAM4Camera) In SEQUENCE mode it polls the capture status until the sequence ends, for at most -`capture_timeout_ms(N * exposure, N * readout)`, and reads the N frames. In SINGLE_FRAME or LIVE mode it reads the -newest frame at once. A timeout, a failed status read or a frame that cannot be read logs, sets `last_error` and -returns `nothing`. Every exit stops the capture and releases the buffer, so in LIVE mode `getdata` ends the live -view. +`capture_timeout_ms(N * exposure, N * readout)`, and reads the N frames. In SINGLE_FRAME mode it reads the +newest frame at once. In LIVE mode it returns the newest frame at once and leaves the live view running (no stop, +no release, `is_running` unchanged). A timeout, a failed status read or a frame that cannot be read logs, sets +`last_error` and returns `nothing`; so does a sequence that transferred fewer frames than requested, with +`last_error` set to `DCAMERR_LOSTFRAME`. Every other exit stops the capture and releases the buffer. """ function CameraInterface.getdata(camera::DCAM4Camera) hdcam = camera.camera_handle n = camera.sequence_length + if camera.capture_mode == LIVE + # Leave the live view running, with no stop and no release: a threaded live reader + # crashes the process if its buffer is released under it. + err, frame = dcambuf_getframe_err(hdcam, Int32(-1)) + frame === nothing && (camera.last_error = err) + return frame + end try if camera.capture_mode == SEQUENCE # 2 x N x (exposure + readout) + 1 s, inside the try so a throw here still cleans up. @@ -211,6 +231,13 @@ function CameraInterface.getdata(camera::DCAM4Camera) # Poll the status against that deadline rather than wait for the end-of-cycle event: # a sequence that has already ended cannot be missed, and an interrupt can land. wait_not_busy(camera, timeout_ms, "getdata") || return nothing + # READY also describes a buffer that was allocated but never ran: check the frame count. + terr, info = dcamcap_transferinfo(hdcam) + if !is_failed(terr) && info.nFrameCount < n + camera.last_error = DCAMERR_LOSTFRAME + @error "DCAM4Camera $(camera.unique_id): getdata found $(info.nFrameCount) of $(n) frames transferred" + return nothing + end im_width, im_height = dcamprop_getsize(hdcam) data = zeros(UInt16, im_height, im_width, n) # (H, W, N) convention for i in 1:n @@ -223,7 +250,7 @@ function CameraInterface.getdata(camera::DCAM4Camera) data[:, :, i] = frame end return data - elseif camera.capture_mode == SINGLE_FRAME || camera.capture_mode == LIVE + elseif camera.capture_mode == SINGLE_FRAME # No cycle to wait for: read the newest frame now. err, frame = dcambuf_getframe_err(hdcam, Int32(-1)) frame === nothing && (camera.last_error = err) diff --git a/src/hardware_implementations/dcam4_camera/types.jl b/src/hardware_implementations/dcam4_camera/types.jl index 47e99c8..6a270b6 100644 --- a/src/hardware_implementations/dcam4_camera/types.jl +++ b/src/hardware_implementations/dcam4_camera/types.jl @@ -28,6 +28,7 @@ mutable struct DCAM4Camera <: Camera camerastate data::Array{UInt16} readout_s::Float64 + capture_generation::Int end function DCAM4Camera(dev_id::Int = 0; @@ -65,5 +66,5 @@ function DCAM4Camera(dev_id::Int = 0; err, exposure_time = dcamprop_getvalue(dco.hdcam, DCAM_IDPROP_EXPOSURETIME) data = zeros(UInt16, im_width, im_height) - DCAM4Camera(unique_id, camera_format, dco.hdcam, exposure_time, frame_rate, roi, capture_mode, trigger_mode, sequence_length, last_error, is_running, camerastate, data, NaN) + DCAM4Camera(unique_id, camera_format, dco.hdcam, exposure_time, frame_rate, roi, capture_mode, trigger_mode, sequence_length, last_error, is_running, camerastate, data, NaN, 0) end \ No newline at end of file diff --git a/test/dcam4_pure.jl b/test/dcam4_pure.jl index 46427a7..bf61e38 100644 --- a/test/dcam4_pure.jl +++ b/test/dcam4_pure.jl @@ -9,7 +9,14 @@ # A camera built with no library call, through the positional constructor. fake_camera() = DC.DCAM4Camera("fake", DC.CameraFormat(1, 1, 1, 1, "SCMOS"), C_NULL, 0.01, 10.0, DC.CameraROI(0, 0, 1, 1), DC.LIVE, DC.DCAMPROP_TRIGGER_MODE__NORMAL, 10, DC.DCAMERR_SUCCESS, - false, 0, zeros(UInt16, 1, 1), NaN) + false, 0, zeros(UInt16, 1, 1), NaN, 0) + + @testset "a superseded wait touches nothing, before any library call" begin + cam = fake_camera() + cam.last_error = DC.DCAMERR_SUCCESS + @test DC.wait_not_busy(cam, 1000, "test"; current = () -> false) == false + @test cam.last_error == DC.DCAMERR_SUCCESS + end @testset "capture_timeout_ms" begin @test DC.capture_timeout_ms(0.0125, 0.043) == 1111 From 44140907b1fbc92ffe67806cbefeb1bd02f0d47f Mon Sep 17 00:00:00 2001 From: kalidke Date: Tue, 29 Sep 2026 23:27:46 -0600 Subject: [PATCH 5/5] Add an opt-in per-call trace of the DCAM library calls @dcamcall replaces @ccall at all 30 DCAM call sites. With tracing on (DCAM4.dcam_trace!(path) or ENV MC_DCAM4_TRACE) it writes a flushed BEGIN and END line per call with arguments, elapsed time, return value and cumulative GC time, so a hang inside a ccall leaves its last BEGIN on disk. Off, it costs one Ref{Bool} check. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 + .../dcam4_camera/DCAM4.jl | 1 + .../dcam4_camera/dcam_trace.jl | 130 ++++++++++++++++++ .../dcam4_camera/dcamapi.jl | 4 +- .../dcam4_camera/dcambuf.jl | 16 +-- .../dcam4_camera/dcamcap.jl | 10 +- .../dcam4_camera/dcamdev.jl | 6 +- .../dcam4_camera/dcamprop.jl | 16 +-- .../dcam4_camera/dcamwait.jl | 8 +- test/dcam4_pure.jl | 40 ++++++ 10 files changed, 207 insertions(+), 30 deletions(-) create mode 100644 src/hardware_implementations/dcam4_camera/dcam_trace.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index b03ed9e..083492a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ the next version with `-DEV`). ## [Unreleased] +### Added + +- Opt-in per-call trace of the DCAM library calls (`DCAM4.dcam_trace!(path)`, or ENV `MC_DCAM4_TRACE=` + at load): a flushed BEGIN and END line per call with arguments, elapsed time, return value and cumulative GC + time, to name a call that hangs. Off by default; one `Ref{Bool}` check per call. + ### Fixed - `DCAM4Camera` `capture` could hang, or leave the camera unusable after a missed frame. Its frame wait is now diff --git a/src/hardware_implementations/dcam4_camera/DCAM4.jl b/src/hardware_implementations/dcam4_camera/DCAM4.jl index 12b39be..46b7acd 100644 --- a/src/hardware_implementations/dcam4_camera/DCAM4.jl +++ b/src/hardware_implementations/dcam4_camera/DCAM4.jl @@ -25,6 +25,7 @@ export dcamprop_getvalue, DCAM_IDPROP_INTERNALFRAMERATE, CameraROI, dcamapi_unin include("dcamerr.jl") include("dcam_idprop.jl") include("types.jl") +include("dcam_trace.jl") include("dcamapi.jl") include("dcamdev.jl") include("dcamprop.jl") diff --git a/src/hardware_implementations/dcam4_camera/dcam_trace.jl b/src/hardware_implementations/dcam4_camera/dcam_trace.jl new file mode 100644 index 0000000..b40929f --- /dev/null +++ b/src/hardware_implementations/dcam4_camera/dcam_trace.jl @@ -0,0 +1,130 @@ +# Opt-in per-call trace of the DCAM library calls. A hang inside a DCAM ccall leaves no Julia frame +# to inspect, so `@dcamcall` (used at every DCAM ccall site in place of `@ccall`) can write a BEGIN +# line before and an END line after each call, flushed at once so a killed process leaves its last +# BEGIN on disk. Cumulative GC time is on every BEGIN and END line: a GC requested by another +# thread while this thread sits in a long ccall (which is not a GC safe point) makes that thread +# spin until the call returns, and shows as a jump between a call's BEGIN and END. +# +# Off by default; the cost when off is one `Ref{Bool}` check per call. Turn on with +# `dcam_trace!(path)` (`dcam_trace!(nothing)` turns it off) or ENV `MC_DCAM4_TRACE=` at load. + +using Dates: Dates + +const TRACE_ON = Ref(false) +const TRACE_IO = Ref{Union{IOStream, Nothing}}(nothing) +const TRACE_LOCK = ReentrantLock() + +""" + dcam_trace!(path) + dcam_trace!(nothing) + +Append a BEGIN/END line for every DCAM library call to `path`, or stop tracing and close the file. +""" +function dcam_trace!(path::Union{AbstractString, Nothing}) + lock(TRACE_LOCK) do + TRACE_ON[] = false + TRACE_IO[] === nothing || close(TRACE_IO[]) + TRACE_IO[] = nothing + if path !== nothing + TRACE_IO[] = open(path, "a") + TRACE_ON[] = true + end + end + return nothing +end + +function trace_line(kind, name, rest::AbstractString) + lock(TRACE_LOCK) do + io = TRACE_IO[] + io === nothing && return + t = Dates.format(Dates.now(), "yyyy-mm-dd HH:MM:SS.sss") + println(io, t, " tid=", Threads.threadid(), " ", kind, " ", name, rest) + flush(io) + end + return nothing +end + +gc_ms() = Base.gc_num().total_time / 1e6 + +""" + dcam_trace_note(msg) + +Write a marker line to the trace, if tracing is on. +""" +function dcam_trace_note(msg) + TRACE_ON[] && trace_line("NOTE", "", " " * string(msg)) + return nothing +end + +function trace_arg(v) + v isa Base.RefValue && (v = v[]) + if v isa Integer || v isa AbstractFloat + return string(v) + elseif v isa Ptr + return string("0x", string(UInt(v), base = 16)) + elseif isstructtype(typeof(v)) && !(v isa Union{AbstractArray, AbstractString}) && fieldcount(typeof(v)) > 0 + parts = String[] + for f in fieldnames(typeof(v)) + fv = getfield(v, f) + fv isa Int32 && push!(parts, string(f, "=", fv)) + end + return string(typeof(v).name.name, "{", join(parts, ","), "}") + else + return string(typeof(v)) + end +end + +function trace_begin(name, vals) + trace_line("BEGIN", name, string(" args=(", join(map(trace_arg, vals), ", "), ") gc_ms=", round(gc_ms(), digits = 3))) + return time_ns() +end + +function trace_end(name, t0, ret) + ms = (time_ns() - t0) / 1e6 + trace_line("END", name, string(" elapsed_ms=", round(ms, digits = 3), " ret=", ret, " gc_ms=", round(gc_ms(), digits = 3))) + return nothing +end + +""" + @dcamcall [lib.]fn(arg::T, ...)::Ret + +`@ccall`, plus a BEGIN and an END trace line when tracing is on (see `dcam_trace!`). Each argument +expression is evaluated once. +""" +macro dcamcall(expr) + Meta.isexpr(expr, :(::), 2) && Meta.isexpr(expr.args[1], :call) || error("@dcamcall: expected fn(args...)::Ret") + call, ret = expr.args + target = call.args[1] + fname = string(target isa Expr ? target.args[end] : target) + fname = startswith(fname, ":") ? fname[2:end] : fname + binds = Expr[] + tmps = Symbol[] + newargs = Any[target] + for a in call.args[2:end] + Meta.isexpr(a, :(::), 2) || error("@dcamcall: every argument needs a type annotation, got $a") + t = gensym("arg") + push!(binds, :($t = $(a.args[1]))) + push!(tmps, t) + push!(newargs, Expr(:(::), t, a.args[2])) + end + plain = :(Base.@ccall $(Expr(:(::), Expr(:call, newargs...), ret))) + r = gensym("ret"); t0 = gensym("t0") + on = GlobalRef(@__MODULE__, :TRACE_ON) + return esc(quote + let $(binds...) + if $on[] + $t0 = $(GlobalRef(@__MODULE__, :trace_begin))($fname, ($(tmps...),)) + $r = $plain + $(GlobalRef(@__MODULE__, :trace_end))($fname, $t0, $r) + $r + else + $plain + end + end + end) +end + +function __init__() + path = get(ENV, "MC_DCAM4_TRACE", "") + isempty(path) || dcam_trace!(path) +end diff --git a/src/hardware_implementations/dcam4_camera/dcamapi.jl b/src/hardware_implementations/dcam4_camera/dcamapi.jl index 7e2d0b3..088a002 100644 --- a/src/hardware_implementations/dcam4_camera/dcamapi.jl +++ b/src/hardware_implementations/dcam4_camera/dcamapi.jl @@ -17,7 +17,7 @@ end function dcamapi_init() dci = DCAMAPI_INIT() - err = @ccall "dcamapi.dll".dcamapi_init(dci::Ref{DCAMAPI_INIT})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamapi_init(dci::Ref{DCAMAPI_INIT})::DCAMERR if is_failed(err) @error "DCAM Failed to Initialize" end @@ -25,7 +25,7 @@ function dcamapi_init() end function dcamapi_uninit() - err = @ccall "dcamapi.dll".dcamapi_uninit()::DCAMERR + err = @dcamcall "dcamapi.dll".dcamapi_uninit()::DCAMERR if is_failed(err) @error "DCAM Failed to Un-Initialize" end diff --git a/src/hardware_implementations/dcam4_camera/dcambuf.jl b/src/hardware_implementations/dcam4_camera/dcambuf.jl index f2f5f28..d803168 100644 --- a/src/hardware_implementations/dcam4_camera/dcambuf.jl +++ b/src/hardware_implementations/dcam4_camera/dcambuf.jl @@ -60,7 +60,7 @@ end ## Functions function dcambuf_alloc(hdcam::Ptr{Cvoid}, framecount::Int32) - err = @ccall "dcamapi.dll".dcambuf_alloc(hdcam::Ptr{Cvoid}, framecount::Int32)::DCAMERR + err = @dcamcall "dcamapi.dll".dcambuf_alloc(hdcam::Ptr{Cvoid}, framecount::Int32)::DCAMERR if is_failed(err) @error "DCAM Failed to Allocate Buffer" end @@ -68,7 +68,7 @@ function dcambuf_alloc(hdcam::Ptr{Cvoid}, framecount::Int32) end function dcambuf_attach(hdcam::Ptr{Cvoid}, param::Ptr{DCAMBUF_ATTACH}) - err = @ccall "dcamapi.dll".dcambuf_attach(hdcam::Ptr{Cvoid}, param::Ptr{DCAMBUF_ATTACH})::DCAMERR + err = @dcamcall "dcamapi.dll".dcambuf_attach(hdcam::Ptr{Cvoid}, param::Ptr{DCAMBUF_ATTACH})::DCAMERR if is_failed(err) @error "DCAM Failed to Attach Buffer" end @@ -77,7 +77,7 @@ end function dcambuf_release(hdcam::Ptr{Cvoid}) iKind = Int32(0) - err = @ccall "dcamapi.dll".dcambuf_release(hdcam::Ptr{Cvoid}, iKind::Int32)::DCAMERR + err = @dcamcall "dcamapi.dll".dcambuf_release(hdcam::Ptr{Cvoid}, iKind::Int32)::DCAMERR if is_failed(err) @error "DCAM Failed to Release Buffer" end @@ -85,7 +85,7 @@ function dcambuf_release(hdcam::Ptr{Cvoid}) end function dcambuf_lockframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME}) - err = @ccall "dcamapi.dll".dcambuf_lockframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR + err = @dcamcall "dcamapi.dll".dcambuf_lockframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR if is_failed(err) @error "DCAM Failed to Lock Frame" end @@ -93,7 +93,7 @@ function dcambuf_lockframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME}) end function dcambuf_copyframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME}) - err = @ccall "dcamapi.dll".dcambuf_copyframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR + err = @dcamcall "dcamapi.dll".dcambuf_copyframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR if is_failed(err) @error "DCAM Failed to Copy Frame" end @@ -124,7 +124,7 @@ end # pFrame=Ref(dcf) -# err = @ccall "dcamapi.dll".dcambuf_copyframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR +# err = @dcamcall "dcamapi.dll".dcambuf_copyframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR # if is_failed(err) # @error "DCAM Failed to Copy Frame" # end @@ -163,7 +163,7 @@ function dcambuf_getframe_err(hdcam::Ptr{Cvoid}, iFrame::Int32) pFrame = Ref(dcf) - err = @ccall "dcamapi.dll".dcambuf_copyframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR + err = @dcamcall "dcamapi.dll".dcambuf_copyframe(hdcam::Ptr{Cvoid}, pFrame::Ptr{DCAMBUF_FRAME})::DCAMERR if is_failed(err) @error "DCAM Failed to Copy Frame $(iFrame): $(err)" return err, nothing @@ -183,7 +183,7 @@ function dcambuf_getlastframe(hdcam::Ptr{Cvoid}) end function dcambuf_copymetadata(hdcam::Ptr{Cvoid}, hdr::Ptr{DCAM_METADATAHDR}) - err = @ccall "dcamapi.dll".dcambuf_copymetadata(hdcam::Ptr{Cvoid}, hdr::Ptr{DCAM_METADATAHDR})::DCAMERR + err = @dcamcall "dcamapi.dll".dcambuf_copymetadata(hdcam::Ptr{Cvoid}, hdr::Ptr{DCAM_METADATAHDR})::DCAMERR if is_failed(err) @error "DCAM Failed to Copy Metadata" end diff --git a/src/hardware_implementations/dcam4_camera/dcamcap.jl b/src/hardware_implementations/dcam4_camera/dcamcap.jl index 0db9f71..d236bf7 100644 --- a/src/hardware_implementations/dcam4_camera/dcamcap.jl +++ b/src/hardware_implementations/dcam4_camera/dcamcap.jl @@ -32,7 +32,7 @@ end ## Functions function dcamcap_start(hdcam::Ptr{Cvoid}, mode::Int32) - err = @ccall "dcamapi.dll".dcamcap_start(hdcam::Ptr{Cvoid}, mode::Int32)::DCAMERR + err = @dcamcall "dcamapi.dll".dcamcap_start(hdcam::Ptr{Cvoid}, mode::Int32)::DCAMERR if is_failed(err) @error "DCAM Failed to Start" end @@ -40,7 +40,7 @@ function dcamcap_start(hdcam::Ptr{Cvoid}, mode::Int32) end function dcamcap_stop(hdcam::Ptr{Cvoid}) - err = @ccall "dcamapi.dll".dcamcap_stop(hdcam::Ptr{Cvoid})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamcap_stop(hdcam::Ptr{Cvoid})::DCAMERR if is_failed(err) @error "DCAM Failed to Stop" end @@ -49,7 +49,7 @@ end function dcamcap_status(hdcam::Ptr{Cvoid}) pStatus = Ref{Int32}(0) - err = @ccall "dcamapi.dll".dcamcap_status(hdcam::Ptr{Cvoid}, pStatus::Ref{Int32})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamcap_status(hdcam::Ptr{Cvoid}, pStatus::Ref{Int32})::DCAMERR if is_failed(err) @error "DCAM Failed to Get Status" end @@ -58,7 +58,7 @@ end function dcamcap_transferinfo(hdcam::Ptr{Cvoid}) param = DCAMCAP_TRANSFERINFO() # Create the struct - err = @ccall "dcamapi.dll".dcamcap_transferinfo(hdcam::Ptr{Cvoid}, param::Ref{DCAMCAP_TRANSFERINFO})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamcap_transferinfo(hdcam::Ptr{Cvoid}, param::Ref{DCAMCAP_TRANSFERINFO})::DCAMERR if is_failed(err) @error "DCAM Failed to Get Transfer Info" end @@ -67,7 +67,7 @@ end function dcamcap_firetrigger(hdcam::Ptr{Cvoid}) iKind = 0 - err = @ccall "dcamapi.dll".dcamcap_firetrigger(hdcam::Ptr{Cvoid}, iKind::Int32)::DCAMERR + err = @dcamcall "dcamapi.dll".dcamcap_firetrigger(hdcam::Ptr{Cvoid}, iKind::Int32)::DCAMERR if is_failed(err) @error "DCAM Failed to Fire Trigger" end diff --git a/src/hardware_implementations/dcam4_camera/dcamdev.jl b/src/hardware_implementations/dcam4_camera/dcamdev.jl index a49d6ee..1081981 100644 --- a/src/hardware_implementations/dcam4_camera/dcamdev.jl +++ b/src/hardware_implementations/dcam4_camera/dcamdev.jl @@ -46,7 +46,7 @@ end function dcamdev_open(i::Int) dco = DCAMDEV_OPEN() dco.index = Int32(i) - err = @ccall "dcamapi.dll".dcamdev_open(dco::Ref{DCAMDEV_OPEN})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamdev_open(dco::Ref{DCAMDEV_OPEN})::DCAMERR if is_failed(err) display(err) @error "DCAM Failed to Open Camera" @@ -55,7 +55,7 @@ function dcamdev_open(i::Int) end function dcamdev_close(hdcam::Ptr{Cvoid}) - err = @ccall "dcamapi.dll".dcamdev_close(hdcam::Ptr{Cvoid})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamdev_close(hdcam::Ptr{Cvoid})::DCAMERR if is_failed(err) display(err) @error "DCAM Failed to Close" @@ -71,7 +71,7 @@ function dcamdev_getstring(hdcam::Ptr{Cvoid}, strid::DCAM_IDSTR) dcs.text = pointer(textbuf) dcs.textbytes = sizeof(textbuf) - err = @ccall "dcamapi.dll".dcamdev_getstring(hdcam::Ptr{Cvoid},dcs::Ref{DCAMDEV_STRING})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamdev_getstring(hdcam::Ptr{Cvoid},dcs::Ref{DCAMDEV_STRING})::DCAMERR if is_failed(err) display(err) @error "DCAM Failed to Get String" diff --git a/src/hardware_implementations/dcam4_camera/dcamprop.jl b/src/hardware_implementations/dcam4_camera/dcamprop.jl index 4044fe2..69a7d20 100644 --- a/src/hardware_implementations/dcam4_camera/dcamprop.jl +++ b/src/hardware_implementations/dcam4_camera/dcamprop.jl @@ -146,7 +146,7 @@ function dcamprop_getattr(hdcam::Ptr{Cvoid}, iProp::Int32) dca = DCAMPROP_ATTR() dca.iProp = iProp ptr_dca = Ref(dca) - err = @ccall "dcamapi.dll".dcamprop_getattr(hdcam::Ptr{Cvoid},ptr_dca::Ref{DCAMPROP_ATTR})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamprop_getattr(hdcam::Ptr{Cvoid},ptr_dca::Ref{DCAMPROP_ATTR})::DCAMERR return err, dca end @@ -156,7 +156,7 @@ end function dcamprop_getvalue(hdcam::Ptr{Cvoid},iProp::Int32) pValue = Ref{Float64}(0.0) - err = @ccall "dcamapi.dll".dcamprop_getvalue(hdcam::Ptr{Cvoid}, iProp::Int32, pValue::Ref{Float64})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamprop_getvalue(hdcam::Ptr{Cvoid}, iProp::Int32, pValue::Ref{Float64})::DCAMERR if is_failed(err) @error "DCAM Failed to get Value" end @@ -169,7 +169,7 @@ end function dcamprop_setvalue(hdcam::Ptr{Cvoid}, iProp::Int32, fValue::Float64) - err = @ccall "dcamapi.dll".dcamprop_setvalue(hdcam::Ptr{Cvoid}, iProp::Int32, fValue::Float64)::DCAMERR + err = @dcamcall "dcamapi.dll".dcamprop_setvalue(hdcam::Ptr{Cvoid}, iProp::Int32, fValue::Float64)::DCAMERR if is_failed(err) @error "DCAM Failed to set value of $(fValue) for: $(DCAM_IDPROP(iProp))" display(err) @@ -184,7 +184,7 @@ end function dcamprop_setgetvalue(hdcam::Ptr{Cvoid}, iProp::Int32) pValue = Ref{Float64}(0.0) option - 0 - err = @ccall "dcamapi.dll".dcamprop_setgetvalue(hdcam::Ptr{Cvoid}, iProp::Int32, pValue::Ref{Float64}, option::Int32)::DCAMERR + err = @dcamcall "dcamapi.dll".dcamprop_setgetvalue(hdcam::Ptr{Cvoid}, iProp::Int32, pValue::Ref{Float64}, option::Int32)::DCAMERR if is_failed(err) @error "DCAM Failed to set/get value" end @@ -199,7 +199,7 @@ DCAMPROP_OPTION_NEXT return value is the next value """ function dcamprop_queryvalue(hdcam::Ptr{Cvoid}, iProp::Int32, option::Int32) pValue = Ref{Float64}(0.0) - err = @ccall "dcamapi.dll".dcamprop_queryvalue(hdcam::Ptr{Cvoid}, iProp::Int32, pValue::Ref{Float64}, option::Int32)::DCAMERR + err = @dcamcall "dcamapi.dll".dcamprop_queryvalue(hdcam::Ptr{Cvoid}, iProp::Int32, pValue::Ref{Float64}, option::Int32)::DCAMERR if is_failed(err) @error "DCAM Failed to query value" end @@ -208,7 +208,7 @@ end function dcamprop_getnextid(hdcam::Ptr{Cvoid}, idprop::Int32, option::DCAMPROP_OPTION) pProp = Ref(idprop) - err = @ccall "dcamapi.dll".dcamprop_getnextid(hdcam::Ptr{Cvoid}, pProp::Ref{Int32}, option::Int32)::DCAMERR + err = @dcamcall "dcamapi.dll".dcamprop_getnextid(hdcam::Ptr{Cvoid}, pProp::Ref{Int32}, option::Int32)::DCAMERR if is_failed(err) @error "DCAM Failed to get next ID" pProp[] = 0 @@ -219,7 +219,7 @@ end function dcamprop_getname(hdcam::Ptr{Cvoid}, iProp::Int32) textbytes = 256 text = Vector{Cchar}(undef, textbytes) - err = @ccall "dcamapi.dll".dcamprop_getname(hdcam::Ptr{Cvoid}, iProp::Int32, text::Ptr{Cchar}, textbytes::Int32)::DCAMERR + err = @dcamcall "dcamapi.dll".dcamprop_getname(hdcam::Ptr{Cvoid}, iProp::Int32, text::Ptr{Cchar}, textbytes::Int32)::DCAMERR if is_failed(err) @error "DCAM Failed to get name" end @@ -243,7 +243,7 @@ end function dcamprop_getvaluetext(hdcam::Ptr{Cvoid}, param::DCAMPROP_VALUETEXT) ptr_param = Ref(param) - err = @ccall "dcamapi.dll".dcamprop_getvaluetext(hdcam::Ptr{Cvoid}, ptr_param::Ref{DCAMPROP_VALUETEXT})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamprop_getvaluetext(hdcam::Ptr{Cvoid}, ptr_param::Ref{DCAMPROP_VALUETEXT})::DCAMERR if is_failed(err) @error "DCAM Failed to get value text" return err, "" diff --git a/src/hardware_implementations/dcam4_camera/dcamwait.jl b/src/hardware_implementations/dcam4_camera/dcamwait.jl index 474bb9e..fc7d1ee 100644 --- a/src/hardware_implementations/dcam4_camera/dcamwait.jl +++ b/src/hardware_implementations/dcam4_camera/dcamwait.jl @@ -50,7 +50,7 @@ end function dcamwait_open(hdcam::Ptr{Cvoid}) dwo = DCAMWAIT_OPEN(hdcam) param = Ref(dwo) - err = @ccall "dcamapi.dll".dcamwait_open(param::Ref{DCAMWAIT_OPEN})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamwait_open(param::Ref{DCAMWAIT_OPEN})::DCAMERR if is_failed(err) @error "DCAM Failed to Open Wait: $(err)))" end @@ -58,7 +58,7 @@ function dcamwait_open(hdcam::Ptr{Cvoid}) end function dcamwait_close(hwait::Ptr{Cvoid}) - err = @ccall "dcamapi.dll".dcamwait_close(hwait::Ptr{Cvoid})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamwait_close(hwait::Ptr{Cvoid})::DCAMERR if is_failed(err) @error "DCAM Failed to Close Wait: $(err)))" end @@ -66,7 +66,7 @@ function dcamwait_close(hwait::Ptr{Cvoid}) end function dcamwait_start(hwait::Ptr{Cvoid}, param::Ref{DCAMWAIT_START}) - err = @ccall "dcamapi.dll".dcamwait_start(hwait::Ptr{Cvoid}, param::Ref{DCAMWAIT_START})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamwait_start(hwait::Ptr{Cvoid}, param::Ref{DCAMWAIT_START})::DCAMERR if is_failed(err) @error "DCAM Failed to Start Wait: $(err))" end @@ -74,7 +74,7 @@ function dcamwait_start(hwait::Ptr{Cvoid}, param::Ref{DCAMWAIT_START}) end function dcamwait_abort(hwait::Ptr{Cvoid}) - err = @ccall "dcamapi.dll".dcamwait_abort(hwait::Ptr{Cvoid})::DCAMERR + err = @dcamcall "dcamapi.dll".dcamwait_abort(hwait::Ptr{Cvoid})::DCAMERR if is_failed(err) @error "DCAM Failed to Abort Wait: $(err)))" end diff --git a/test/dcam4_pure.jl b/test/dcam4_pure.jl index bf61e38..1624c6c 100644 --- a/test/dcam4_pure.jl +++ b/test/dcam4_pure.jl @@ -55,4 +55,44 @@ cam.readout_s = 0.043 @test DC.cached_readout_time(cam) == 0.043 end + + @testset "@dcamcall trace" begin + # The macro is named by its full path: it must resolve while the file is parsed, before DC exists. + # libc stands in for the DCAM library: the macro takes the same syntax as @ccall. + f(s) = MicroscopeControl.HardwareImplementations.DCAM4.@dcamcall strlen(s::Cstring)::Csize_t + g(x) = MicroscopeControl.HardwareImplementations.DCAM4.@dcamcall abs(x::Cint)::Cint + DC.dcam_trace!(nothing) + @test f("hello") == 5 + @test g(Cint(-3)) == 3 + + n = Ref(0) + h() = MicroscopeControl.HardwareImplementations.DCAM4.@dcamcall abs((n[] += 1; Cint(-7))::Cint)::Cint + @test h() == 7 + @test n[] == 1 + + path = tempname() + DC.dcam_trace!(path) + @test f("hello") == 5 + @test h() == 7 + @test n[] == 2 + DC.dcam_trace_note("marker") + lines = readlines(path) # read while the file is still open: every line is flushed + DC.dcam_trace!(nothing) + @test length(lines) == 5 + @test occursin(r"tid=\d+ BEGIN strlen args=\(String\)", lines[1]) + @test occursin(r"tid=\d+ END strlen elapsed_ms=[\d.]+ ret=5 gc_ms=[\d.]+", lines[2]) + @test occursin("BEGIN abs args=(-7)", lines[3]) + @test occursin("NOTE marker", lines[5]) + + # A Ref to a struct logs its Int32 fields, so a wait's timeout shows. + @test occursin("DCAMWAIT_START{size=16,eventhappened=0,eventmask=2,timeout=1000}", DC.trace_arg(Ref(DC.DCAMWAIT_START(Int32(2), Int32(1000))))) + + @test DC.trace_arg(Ptr{Cvoid}(UInt(0x10))) == "0x10" + + # Off again: nothing is written. + size0 = filesize(path) + @test f("hello") == 5 + @test filesize(path) == size0 + rm(path) + end end