Repository navigation
v0.30.1
What changed and why
Fixes only — no public API was added, so this is a patch. Most of them make the cameras keep promises ICam already states.
Five sections change something a caller can observe; the three marked behavior change can break a caller that relied on the
old behavior, and each says what to do. 43 of the 50 new tests fail on 0.30.0; the other 7 are controls and guards that pass
before and after.
Take this release if you start live on a VirtualCam or VideoCaptureCam while a single grab may still be running, bind
commands with a CanExecute to CvDispCtrl's camera buttons, subscribe to GevCam or VideoCaptureCam notifications directly,
or call StartContinuous / StopContinuous on a ReconnectingCam from more than one thread.
Imaging — VirtualCam and VideoCaptureCam refuse a start or a second grab while a single grab is waiting (behavior change)
ICam says that while GrabOne waits for its frame, a second GrabOne and StartContinuous on the same camera throw
InvalidOperationException. GevCam did; VirtualCam and VideoCaptureCam did not track a pending grab, so both went ahead —
live frames or another grab's frame could reach the subscription waiting for this grab's answer, with no error.
- Now both throw, with a message that says what to wait for. A handler that calls
GrabOneorStartContinuouson the same camera
from inside the grab's ownFrameAcquiredthrows too (the camera logs it and the outer grab returns normally). - Who notices: a caller that starts live from another thread before a grab has returned.
GrabFrameAsyncwith a short timeout
returnsnullwhileGrabOnekeeps running in the background — onVirtualCamup to about 1 s more right after a stop — and a
start in that window now throws. On the same instance,ClosethenOpendoes not clear the mark while the previous session's
grab is still inside itsFrameAcquiredhandler. - What to do: start live after the grab has returned, or catch
InvalidOperationExceptionaroundStartContinuouswhere a
grab may be in flight (asGevCamcallers already had to). - Wrapped in
ReconnectingCam, the refusal is rethrown to the caller and the live intent is rolled back, as it already was forGevCam. - The Demo sample's Live button reports the refusal in the status bar instead of closing the window.
- Measured with gates that hold the grab in each window (image load, publish, the wait for a stopped live's last tick): 12 of the
13 new tests failed before the fix, in five runs.VideoCaptureCamwas measured with a generated MJPG clip. Not verified on Linux
CI: if OpenCV there cannot write MJPG, those cases return early and show as passed.
Imaging — VideoCaptureCam reports a live loop that ended on its own
When the live loop died on an unexpected error, it logged one Error and left IsGrabbing true with no notification. The next
StartContinuous returned silently as "already running", and a wrapping ReconnectingCam never rebuilt the session. An
ObjectDisposedException that no stop caused was swallowed without a trace.
- Now
IsGrabbingdrops to false andGrabbingChanged(false)is raised once, from the live thread ({Name}.Live), with a
Warning; such anObjectDisposedExceptionis logged as an Error.StartContinuousrestarts;ReconnectingCamrebuilds the
session and resumes live. - Live resumes only if the failure was transient. A source whose every frame fails conversion (for example a pixel format
CamFramedoes not support) now cycles underReconnectingCam— reopen, fail on the first frame, rebuild at the first backoff
step — and logs each round. Before, it stopped after one Error line and kept reporting live. Per-frame isolation is a candidate
for a later release. - Measured by making the loop throw through reflection (an unexpected exception and a disposed capture): red before, green after,
including theReconnectingCamrebuild.
Imaging.Gev — GevCam notifications follow the transition-only rule
Close/DisposeraiseGrabbingChanged(false)only if acquisition was running. Closing an idle camera no longer reports a
stop, and stop-then-close reports one stop instead of two.- After a control loss,
Close/Disposeno longer raiseConnectionChanged(false)a second time. - On a control loss, the disconnect is now raised at the loss even if a
GrabbingChangedsubscriber throws (0.30.0 skipped it and
the followingCloseraised it). Both subscribers' exceptions are logged as Errors ("a GrabbingChanged subscriber threw." /
"a ConnectionChanged subscriber threw."). - An
ObjectDisposedExceptionfrom the acquisition library — which documents it for a disposed device and for a request that
reaches a closing control channel — is now taken like a control loss at the device-command sites:StopContinuousno longer throws it after the receive pump has stopped, and the stop is still reported.Closeno longer aborts halfway through its teardown on it.StartContinuousno longer throws it either: it returns and reports live, and the control loss that follows reports the stop and
the disconnect.- In
StopContinuousonly, if a later device step throws anything else, the stop is still reported and the exception still
propagates.Closestill stops at an exception outside the caught set.
- Reopening after a control loss reports
GrabbingChanged(false)when the dead session it folds had a receive pump started after
the loss. That pump can exist when the mode write never reached the device: a camera without anAcquisitionModenode, or one
whose mode lock is judged from a cached register value while live was running at the loss — "cable pulled during live, then the
live button" (reproduced with a fake node map modeled on that lock, not on a camera). - Through
ReconnectingCam: it unsubscribes before it closes, disposes or replaces an inner camera, and opens a new instance for
every session, so the Close / Dispose / reopen changes are not visible through it. TheObjectDisposedExceptionchange is
visible: a wrappedStopContinuousno longer rethrows it, and a wrappedStartContinuousthat hit it no longer rolls back the
live intent (read from the code). - How it was measured: a fake session set up through reflection and a node map whose port throws a chosen exception. The device
teardown itself needs hardware and was not exercised. That the acquisition library raises these exceptions in the field is taken
from its documented error contract and the decompiled 0.5.0 package, not observed on site. - Not changed, documented at the site (read only, unconfirmed): a frame from a timed-out single grab may become the first frame of
the next live, because starting live does not drain or filter by start time. In free-running preview that is one stale frame; in
triggered acquisition it could be the first target's answer and, for a consumer that pairs frames with targets in order, shift
every pairing after it.
Imaging — ReconnectingCam with overlapping starts and stops (behavior change)
IsGrabbingno longer stays true over a stopped inner camera when an overlappingStopContinuousis still finishing and a later
StartContinuouswas stood down by the inner camera.- A late
GrabbingChanged(true)from the inner camera is checked against the inner camera's current state. - If the inner camera's
StopContinuousthrows, the display is still settled: lowered if the inner camera had stopped, left on if
it threw before stopping. The same exception is rethrown. - What a caller sees:
IsGrabbingfollows what the inner camera is actually doing. In an overlap window it can read false where it
used to read a stale true. One extra false→true edge may appear when a deferred relight lands. A toggle that readsIsGrabbing
in that window now routes to Start instead of Stop. - Notifications from different threads can reach a subscriber out of order. Under forced preemption a subscriber can see
[true, true, false]whileIsGrabbingis true. What to do: treat each notification as a cue and re-readIsGrabbing; do
not mirror the notification's value. - Known limits, documented on the class and reproduced only with a fake camera (not observed on site):
- In six places a read of the inner camera's state, taken outside the wrapper's lock, can set
IsGrabbingtrue. A stop, a
crossing start and a refused relight that all complete inside that gap can leaveIsGrabbingtrue over a stopped inner camera
until the next command or reconnect. A "live could not be restarted" warning preceded it in every reproduced ordering. - One opposite place: after a throwing stop, the same gap can leave
IsGrabbingfalse over a running inner camera. This needs a
camera whoseStopContinuouscan throw before turning live off, plus a second throwing stop; none of the cameras in this
package do that. The same false-off state was already reachable before this release when such a stop overlaps a normal stop. - Closing these needs a structural change — a candidate for a later release.
- In six places a read of the inner camera's state, taken outside the wrapper's lock, can set
- Measured:
- Deterministic fake cameras with gates.
- An interleaving explorer (preemption-bounded and exhaustive, plus random schedules) on the first round of these changes found
no new end-state category. In its three-thread scenario for the original defect (about 7,000 schedules each), the schedules
whereIsGrabbingwas true over a stopped inner camera whenStopContinuousreturned dropped from 597 to 33. A second pass
that added throwing stops found the opposite place above, a new path into a state that was already reachable.
Wpf — CvDispCtrl's 📸 / ⏯ buttons honour CanExecute (behavior change)
The buttons called Execute directly and never asked CanExecute, so a host that disabled the command during a grab still got a
live, clickable button.
- They now follow
Button.Commandsemantics:- Greyed out and not executed while
CanExecute(null)is false. - Re-queried on
CanExecuteChanged, which may be raised from any thread. - The subscription is weak.
- Greyed out and not executed while
- ⏯ follows
StopCommandwhileIsRunningis true andContinuousCommandotherwise, so a blocked start never blocks stopping. - A click refused by
CanExecutere-queries the buttons.- A command that turned false without raising
CanExecuteChangedgreys out on that click.
- A command that turned false without raising
- What to do: raise
CanExecuteChangedon both edges — when the command becomes blocked and when it becomes runnable again.
Otherwise the button stays grey until something else re-queries. - A
CanExecutethat throws is treated as runnable, with one warning per command instance. LoadFrameCommandstill does not askCanExecute: its parameter exists only after the file dialog closes. This is documented.- Read from the consumer code we could inspect (20 bindings):
- Two places change behavior, both in the intended direction: a screen whose 📸 / ⏯ commands report false while a grab runs.
- No binding was found that would leave a button stuck.
Documentation
ICam.GrabbingChangednow states which thread each implementation raises on, that notifications can cross, and how to consume
them asynchronously (re-readIsGrabbingwhere you decide).ICam.GrabOnelists "another single grab is waiting" among itsInvalidOperationExceptioncases, and how a close interrupts a
waiting grab is stated per implementation.- READMEs: the
CvDispCtrlcommand table, and a subscriber note onGrabbingChangedfor the Imaging package.
Tests
The core suite grows from 78 to 118 and the WPF suite from 44 to 54. The race tests force each ordering with gates or hooks and
assert that the forced order actually happened before they assert the result; the GevCam and CvDispCtrl tests set up state
directly.
Version
0.30.1 — patch. No public API was added. The behavior changes above correct behavior that broke documented contracts or the
standard ICommand contract.
Checks
-
dotnet build CvInspect.sln -c Release --no-incrementalreports 0 warnings (an incremental build reports none when it recompiles nothing) -
mainis an ancestor of this branch (the release-pr-guard job verifies it) - the tag will be created on the merge commit,
mainmerged back intodevafterwards, anddevbumped to the next-devversion