diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f621ec..083492a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,47 @@ 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] + +### 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 + 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 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. 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. 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. 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. + +### Changed + +- `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. + ## [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] 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_helpers.jl b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl index 0a9bba2..1a786a1 100644 --- a/src/hardware_implementations/dcam4_camera/dcam_helpers.jl +++ b/src/hardware_implementations/dcam4_camera/dcam_helpers.jl @@ -203,5 +203,122 @@ 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!`. 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" 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 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`. 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 + # 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) + 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; 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. 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; + current::Function = () -> true) + deadline = time() + timeout_ms / 1000 + logged = false + while current() + err, status = dcamcap_status(camera.camera_handle) + if is_failed(err) + 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 + if time() >= deadline + 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/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 7d8df9e..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 @@ -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 @@ -163,24 +163,27 @@ function dcambuf_getframe(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" - 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 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 2124162..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 @@ -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..0a36e12 100644 --- a/src/hardware_implementations/dcam4_camera/interface_methods.jl +++ b/src/hardware_implementations/dcam4_camera/interface_methods.jl @@ -2,67 +2,106 @@ """ CameraInterface.getlastframe(camera::DCAM4Camera) + +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`. A frame that cannot be copied returns `nothing` with `last_error` set. """ 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, cached_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) + err, frame = dcambuf_getframe_err(hdcam, Int32(-1)) + frame === nothing && (camera.last_error = err) + return frame + finally + dcamwait_close(hwait) end - - return framedata end """ CameraInterface.capture(camera::DCAM4Camera) + +`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. A frame that cannot be copied returns `nothing` with `last_error` set. """ function CameraInterface.capture(camera::DCAM4Camera) - # Start a capture + # 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 + # 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) 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 + 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. + 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 """ @@ -78,6 +117,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)) @@ -98,6 +138,11 @@ 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). 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 @@ -108,6 +153,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)) @@ -124,24 +170,22 @@ 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") + gen = camera.capture_generation # this sequence's generation (abort, above, already moved it on) @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) + 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 = 0 - println("Sequence done") end - - - return end @@ -155,53 +199,71 @@ 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) + +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 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) - # 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) - - camera.is_running = false - display(timeout_milisec) - if is_timeout(err) - display(event) - camera.last_error = err - err, hwait = dcamwait_close(hwait) - return + 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 - 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)) + try + if camera.capture_mode == SEQUENCE + # 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 + # 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 + 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 + # 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 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 - 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 end end diff --git a/src/hardware_implementations/dcam4_camera/types.jl b/src/hardware_implementations/dcam4_camera/types.jl index cb64a4d..6a270b6 100644 --- a/src/hardware_implementations/dcam4_camera/types.jl +++ b/src/hardware_implementations/dcam4_camera/types.jl @@ -27,6 +27,8 @@ mutable struct DCAM4Camera <: Camera is_running::Bool camerastate data::Array{UInt16} + readout_s::Float64 + capture_generation::Int end function DCAM4Camera(dev_id::Int = 0; @@ -64,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) + 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 new file mode 100644 index 0000000..1624c6c --- /dev/null +++ b/test/dcam4_pure.jl @@ -0,0 +1,98 @@ +# 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, 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, 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 + @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 "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 + + @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 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")