Skip to content

v0.29.2

Choose a tag to compare

@github-actions github-actions released this 28 Sep 02:10
· 68 commits to main since this release
3b15ce1

What changed and why

Fixes to how live acquisition starts and stops, most of them in ReconnectingCam, plus clearer diagnostics when a camera's
automatic exposure is on. No public type or member was added or removed. Two ICam documentation remarks now state
what implementations and callers can rely on (see "Contract notes").

Take this release if you wrap cameras in ReconnectingCam (the recommended setup for GigE), or if one thread can call
StopContinuous while another is still in StartContinuous.

Imaging.Gev — a stop that arrives while a live start waits now wins

0.29.1 added a short wait before a live start that follows an aborted single grab, and ran that wait outside the camera's
lock. A StopContinuous from another thread during the wait found nothing running and returned, and the waking start then
began live acquisition: Stop was called, yet live kept running. 0.29.0 held the lock for the whole start, so the stop
queued behind it. A consumer found this by reading the 0.29.1 source; it was then reproduced on the shared Basler
(acA2500-14gm): 30 ms exposure, stop 20 ms after the start call, 40 tries. All 25 tries whose start waited were still live
after the stop; in the same run, none of the 15 tries without a wait were.

  • StopContinuous now counts stop requests at the moment it is called, even when nothing is running.
    StartContinuous records the count when called and stands down if a stop was called after it. It logs
    continuous grab not started: …, throws nothing and raises no GrabbingChanged. A close and reopen in the meantime, or
    a control loss during the wait, also makes the start stand down.
  • The stop is counted before it waits for the camera lock. A first version counted it once it held the lock. Then a stop
    called before a start, but still waiting while another call held the lock (an exposure write, another stop), was
    counted as the later command and cancelled the start. The pre-release review found this; regression 10-G-16.
  • Same probe after the fix, with each try classified by when the start thread actually entered: 0 violations in 40 tries
    directly on GevCam, and 0 in 40 through ReconnectingCam.

Imaging — ReconnectingCam

A consumer porting this class found four defects and reported them by reading our source. Each was first reproduced here
with a fake camera: the new tests failed on the code just before each fix (the defects themselves predate 0.29.1).
Reviewing the fixes turned up more of the same kind. What changes for a subscriber:

  • Close and Dispose now announce the disconnect. Before, closing a connected, live camera raised no
    GrabbingChanged(false) and no ConnectionChanged(false) at all. A screen that mirrors state from events kept showing it
    connected. The order is grabbing first, then connection, as the inner cameras raise them.
  • A wrapped DeadCam is no longer reported connected. Its Open only logs a warning. With default options the first
    Open now throws with the dead slot's reason; with RetryInitialOpen it keeps retrying without ever announcing a
    connection.
  • A session lost right after it opened is retried. A loss that landed between the inner Open and the swap was dropped
    as a stale notification, so the wrapper kept a dead camera as connected and never reconnected.
  • An open that finishes after Close is discarded. Close waits for a reconnect in progress only up to ShutdownWaitMs.
    If the inner Open finished later, its session was attached and the closed camera stayed connected (and kept device
    control until the next Close). It is now closed and disposed, and an Info line says so. The same applies to a Close or
    Dispose that wins over a slow first Open.
  • Connected and grabbing are announced only for the session that is still current. A Close landing in the short gap
    after a reconnect could leave IsConnected == true after Close, with no camera behind it. A session that died while live
    was being started or resumed could be announced as grabbing after its disconnect. Both states are now decided under the
    same lock that records them.
  • Live is announced only when the inner camera is really live. When the inner camera stood down (see above), or when two
    threads called Stop and Start at almost the same moment, the wrapper raised GrabbingChanged(true) from the return alone
    and reported IsGrabbing == true with nothing running. It now checks the inner camera's IsGrabbing after the call. An
    adapter that reports live later, from a callback, is still picked up from its GrabbingChanged(true), unless a Stop has
    come in first.
  • A reconnect no longer tears down a healthy session. If a reconnect attempt failed and, during the backoff, your code
    called Open() to recover (as ICam.Open suggests), the loop's next attempt closed that working session and opened
    another. During that, grabs failed with "Camera is reconnecting." and live dropped. Only a session whose loss was
    reported is replaced now.
  • Open() after a loss restores live, as a reconnect does. Before, only the reconnect loop brought back the live intent.
    A session you opened yourself, or a StartContinuous called while Open was still running, left live off with the
    intent still set, and a later, unrelated reconnect then started live that nobody had asked for at that moment.
  • An exposure set during a reconnect is applied. A SetExposureTimeUs that landed after the new session read the
    exposure, but before it was announced, was recorded but never reached the camera. The camera then ran the old exposure
    with no error and no log line.
  • A start ended by Close returns quietly. Before, the caller got the inner camera's ObjectDisposedException, which
    names the inner camera even though only the wrapper had been closed.
  • Dispose no longer strands a queued Open. An Open waiting behind a slow reconnect could wait forever once Dispose
    ran; it now wakes and throws ObjectDisposedException. A reconnect attempt that finishes after Dispose no longer logs
    reconnect loop failed.
  • A cancelled reconnect loop whose task starts late no longer clears the next session's pending reconnect request. When
    that happened, the dead session was never retried.
  • A start that fails because the connection was just lost no longer drops the live intent. The reconnect then brings live
    back. (GevCam also treats such a start as a loss, not a refusal.)
  • Known limitation: if two threads call StopContinuous and StartContinuous at almost the same moment, within a window of
    microseconds, the inner camera can drop the start. Live then stays off while the recorded intent says on. This is not a
    false "live" report, and the next Open or reconnect restores it.
  • Events are decided under the wrapper's lock but raised outside it, so transitions made on different threads can reach a
    subscriber out of order. As ICam already recommends, IsConnected and IsGrabbing are the values to trust.

Regressions 10-11, 10-R1…10-R14 and 10-G-16. The new ones were confirmed to fail on the code before their fix.
Each guard was also removed on its own to see whether its test fails; the result is in "Checks" below.

Imaging.Gev — automatic exposure

With ExposureAuto on, GevCam wrote the configured exposure anyway. On the Basler measured here the exposure node is
locked while automatic exposure runs, so the write was refused with a warning that said only "read-only". The camera then
set its own exposure: 29995 → 175910 µs, and mean brightness went from 22 to 127. Neither Open nor SetExposureTimeUs
threw. On Crevis MG-A500M-22 (firmware 3.7.3.0) the XML declares no such lock, so the write reaches the device. What that
camera then does with it was not measured.

  • The camera-state line at open now includes exposure= (the value after the write) and exposureAuto=.
  • When automatic exposure is on and the node is locked, the warning names ExposureAuto and gives both ways out. For a fixed
    exposure, turn ExposureAuto off on the camera or in its user set. For automatic exposure, leave CamOpt.ExposureTimeUs
    at 0 and do not call SetExposureTimeUs. If the write is accepted but automatic exposure stays on, a warning says the
    value will not hold. If the write turned automatic exposure off, an Info line says so.
  • GevCam does not turn automatic exposure off by itself. CamOpt.ExposureTimeUs defaults to 10000, so every open
    writes an exposure. Switching auto exposure off would silently change sites that rely on it.
  • Diagnostic reads on the open path no longer throw on transport errors.

Contract notes (documentation, no API change)

  • ICam.StartContinuous: a normal return does not mean live started. A StopContinuous called between the call and the
    start wins, and the call returns without starting. Read IsGrabbing or GrabbingChanged to know. A wrapper must not
    announce "started" from the return alone. If you implement it: when live did start, IsGrabbing must already be true
    on return.
  • ICam.Open: an implementation that will be wrapped in ReconnectingCam must return with IsConnected already true. The
    wrapper checks it after Open and treats false as a failed open, so an adapter that sets IsConnected later from a
    callback never attaches. Every camera in this repository already does this, and DeadCam deliberately does not. The
    Imaging README's "Vendor adapters" section now states both requirements.

What can change for you when you upgrade from 0.29.1

  • A ReconnectingCam whose factory returns a DeadCam (for example CamFactory.Create with an unregistered ComType)
    used to report itself connected. Now, with default options, its first Open() throws InvalidOperationException
    with the dead slot's reason. With RetryInitialOpen, it keeps retrying without announcing a connection; set
    MaxAttempts if the slot will never heal.
  • ReconnectingCam.Close() and Dispose() now raise GrabbingChanged(false) and ConnectionChanged(false) when live or
    connected. 0.29.1 raised nothing.
  • ReconnectingCam.Open() after a loss (with no Close in between) now restores live if it was on. A StartContinuous
    called while the wrapper was closed is still ignored by the next Open, as in 0.29.1.
  • New GevCam log lines appear only on the paths they describe, not on a normal configuration:
    • continuous grab not started: … (Info)
    • exposure=… exposureAuto=… on the camera-state line; a declared node that could not be read shows (not read)
      rather than (absent)
    • the ExposureAuto warnings described above

Version

0.29.2 — patch. No public type or member was added or removed. The behaviour changes are in the start/stop ordering of
GevCam and ReconnectingCam, the events ReconnectingCam raises on Close/Dispose/reconnect, the open-time state line,
and the automatic-exposure warnings. The four packages are released in lockstep.

Checks

  • dotnet build CvInspect.sln -c Release --no-incremental reports 0 warnings
  • Tests: 56 (core) + 30 (Wpf) pass
  • Four adversarial reviews of this range, each finding checked by three verifiers before being acted on. Everything that
    survived is fixed above, except the known limitation.
  • Guards removed one at a time (14): 13 are caught by their test. The one that is not, the "closed" term in the connected
    announcement, is almost always redundant because Close has already removed the inner camera. Without it the worst case is
    an extra true/false pair, not a wrong final state.
  • Hardware (shared Basler acA2500-14gm): the start/stop race was reproduced before the fix. On the final code it is clean:
    0 violations in 40 tries directly and 0 in 40 through ReconnectingCam, and live runs at 14–15 frames/s with no warning or
    error lines. The ExposureAuto behaviour was measured before and after. The reconnect-path fixes (loss, reopen, Close/Dispose
    races) were verified with the fake camera only.
  • main is an ancestor of this branch (the release-pr-guard job verifies it)
  • the tag will be created on the merge commit, main merged back into dev afterwards, and dev bumped to the next -dev version