Skip to content

v0.26.0

Choose a tag to compare

@github-actions github-actions released this 17 Sep 05:13
· 83 commits to main since this release
5dde6d1

What changed and why

Taking a single frame no longer means writing the plumbing yourself.

ICam.GrabOne() publishes through FrameAcquired and nothing else, so a caller who wants one frame
has to subscribe, call, wait and unsubscribe. Every consumer had written that by hand — two of them
independently, both taking a TimeSpan. That is the signal that the library was missing something,
not that the consumers were doing it wrong.

using CvInspect.Imaging;

var frame = await cam.GrabFrameAsync(TimeSpan.FromSeconds(2));
if (frame is null) { /* nothing arrived in time */ }
else using (var mat = frame.AsMat()) { /* inspect */ }

It is an extension method on ICam, so no implementation has to change — not the six in this
repository, nor anyone else's. Cameras that can do better than the generic path advertise it by
implementing ICamGrabAsync, and the extension calls them instead.

Why not a default interface member. That was the first shape proposed, and it would have been
simpler. It is also unusable here: these packages target netstandard2.1 so they can be consumed
from Unity, and Unity's IL2CPP cannot execute default interface implementations — the build fails
outright. An extension plus an opt-in interface gives the same "no implementer churn" without asking
anything of the runtime.

null versus an exception is the contract. null means no frame, and no reason to give: the
timeout expired, or the implementation can never answer and has already said why. Anything that
should be able to answer and cannot — closed, disposed, control lost, continuous acquisition
running, another grab already waiting — throws, exactly as GrabOne does. This is not a new
boundary; it is the one ICam already drew between DeadCam (warns, never throws) and
ReconnectingCam during a reconnect gap (throws, because "silently ignoring it leaves the caller
waiting forever for a frame").

The generic path waits out the timeout rather than guessing. An earlier draft closed with null
as soon as GrabOne returned without a frame, on the assumption that publication always completes
before the call returns. That holds for the backends here and it does not hold in general — a
backend whose frames arrive on a vendor callback can publish just after returning, and the shortcut
would have turned a good grab into a null. So the deadline governs, and it runs from the call, not
from when the grab returned. The cost is that a camera which will never answer now costs a full
timeout; a camera that knows it will never answer should implement ICamGrabAsync and say so at
once, which DeadCam does.

Two things make a camera worth implementing ICamGrabAsync for, and they are different:

  • It can tell which frame is this grab's — the generic path takes whatever arrives while it is
    subscribed. Only the implementation, holding the device's own pairing evidence, can do better.
    GevCam does it by frame id.
  • It knows no frame is coming — DeadCam answers immediately instead of stalling every grab.

GevCam, ReconnectingCam (which forwards to its inner camera so the inner one's guarantee
survives) and DeadCam implement it.

Alongside, GevCam's single grab now honours the caller's cancellation token — previously an
async caller could cancel and still wait out the full timeout — and its per-call timeout overrides
GevCamOpt.GrabTimeoutMs, because the call site knows more than the configuration did.

Fewer warnings on the way in

Applying an exposure that is not on the camera's grid is no longer reported as a warning. It is
unavoidable quantization: the device refuses everything off-grid, so any configured value that is
not a multiple of the step goes through this path — measured on a Basler acA2500-14gm with a 35 µs
grid, requests of 50, 200, 1000, 5000 and 12000 µs all land on 35, 210, 1015, 5005 and 12005. Those
are ordinary field values, and warning about them on every open and every reconnect is exactly what
this library's own notes say not to do: a warning that fires all the time stops anyone reading the
warning that counts. The applied value is still reported, and genuine failures — a snap that is
refused, a rejection whose grid cannot be read, a missing node — remain warnings.

The readback that follows the write now compares against what was written rather than what was
requested, so a grid snap is not reported twice; and the level splits there. A camera holding a
different value than the one we wrote, without having refused the write, is not quantization — that
is a warning. No threshold was invented: the reference is the value we wrote.

Version

0.26.0 — minor. ICamGrabAsync and CamGrabExt are new public API; nothing was removed or changed
in a breaking way.

Checks

  • dotnet build in the default configuration reports 0 warnings
  • 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