System designTroubleshooting

Software trigger invalid access error after sleep: check feature state

Why a software trigger throws an invalid access error near time.sleep(), and how polling GenICam feature writability and pacing on frame arrival fixes it.

LumaSpec Vision10 min read Share on LinkedIn

On this page 8 sections
  1. 01What the error text tells you
  2. 02Three causes, in order of likelihood
    1. The camera left the state your command needs
    2. The command is not writable mid-cycle
    3. The cleanup stop fired into the same trap
  3. 03Poll access first, then run the command
  4. 04Pace on frame arrival, not on a fixed sleep
  5. 05A grab loop that survives both failure points
  6. 06Checks before you open a ticket
  7. 07When the usual answer is wrong
  8. 08Questions

You armed a GenICam camera for software-triggered capture, put time.sleep() calls around the trigger command to set the frame rate, and the call threw an invalid-access error - first on the trigger command, then again on the stop command while the SDK cleaned up. Delete the sleeps and the same script returns a complete frame, which makes the sleep look guilty. It is not: the sleeps only moved your commands into a different acquisition state, one where the feature is not writable. This guide decodes the error text, ranks the three causes by likelihood, and gives you a loop that polls feature access and paces frames on arrival. The fix is identical for GigE Vision, USB3 Vision and CoaXPress cameras.

What the error text tells you

A command feature - TriggerSoftware, AcquisitionStop - executes through run(). The SDK checks the feature's access state before running it, and your traceback reported read access not allowed with write access allowed. The feature existed and accepted commands of its own kind, but the camera was not in a state where that command could execute. An invalid-access result (error value -6 in that SDK) means wrong state, not broken cable.

Two facts hide in the cascade. The last exception is the cleanup path, not the cause: the SDK tried to run AcquisitionStop on exit, hit the same wrong state, and buried your first error - read the first traceback, not the last. And the feature's access state is queryable, so you never have to guess with a delay. Run the same check the SDK ran, on purpose, before you call run(). A plain "node not writable" refusal on a setting rather than a command is a cousin problem, covered in GenICam node not writable: check camera access state; here we deal with command features.

Three causes, in order of likelihood

The camera left the state your command needs

The tell is the direction of your experiment. With no delay, the trigger landed while the engine was still armed from start_streaming and a complete frame came back; one second later the engine had moved on, because armed-and-waiting is not a guaranteed resting state on every camera. The sleep broke nothing - it exposed a state change.

Test: after start_streaming, poll the acquisition status feature every 10 ms, print it with a timestamp, and fire the trigger. On a camera with this behaviour you watch the status change during that second.

Fix: fire the trigger only when the status reports acquiring and the trigger feature is writable. That poll replaces the sleep before the first trigger entirely. Arming order matters here too - software trigger ownership in image acquisition lays out who holds the trigger when.

The command is not writable mid-cycle

Some sensors make the trigger command read-only while an exposure or readout is in flight, and cooled infrared sensors can hold features during a housekeeping phase such as temperature stabilisation. A pacing sleep that lands inside that window gets the same refusal.

Test: log the trigger feature's access every 10 ms for five seconds while frames flow. A gap that opens and closes marks the window; a level stretch that never opens means the camera is holding the feature.

Fix: trigger from the frame-arrival event. When a frame lands, the previous exposure has finished by definition, so the next command leaves the closed window. If the feature is held for seconds during cool-down, set the poll timeout above that phase instead of shortening it.

The cleanup stop fired into the same trap

The SDK's stop path runs AcquisitionStop on exit. If the camera already left the acquiring state - cause 1 - that command is refused, the exception replaces your real error, and the script dies in teardown instead of stopping cleanly.

Test: catch the stop and print the acquisition status at that moment. A status of not acquiring, with the stop refused, confirms it.

Fix: gate the stop on the same status poll. If the camera is not acquiring there is nothing to stop; let the SDK finish its teardown and tolerate the refusal rather than letting it propagate. The same state machine bites single-shot loops, as single frame acquisition still busy: why repeated starts need a stop and hardware trigger stops after one frame: check acquisition mode show.

Poll access first, then run the command

Commit to these numbers: poll every 10 ms, give up at 2 s. A feature still unwritable 2 s after start is the camera telling you it is not acquiring - stop and read the status rather than retrying forever. The wrapper names below are ours; the names in quotes are the camera's GenICam feature names, and run(), set() and get_feature_by_name() are the calls your SDK already used in the failing script.

import time

POLL_S: float = 0.01      # poll interval in seconds
TIMEOUT_S: float = 2.0    # give-up time for one feature

def wait_writable(cam, name: str, probe) -> bool:
    """probe(feature) -> bool: bind to the SDK's feature-access query."""
    feature = cam.get_feature_by_name(name)
    deadline: float = time.monotonic() + TIMEOUT_S
    while time.monotonic() < deadline:
        if probe(feature):
            return True
        time.sleep(POLL_S)
    return False

writable: bool = wait_writable(cam, 'TriggerSoftware', probe)

The probe is one line in any GenICam SDK: it returns the feature's current read and write access, the same pair of flags your error printed. If your SDK exposes the acquisition status feature, poll it the same way and require both answers before run().

Pace on frame arrival, not on a fixed sleep

The sleep you wanted - take an image every second - defines a rate, and rates belong after the frame, never before the trigger. Compute the frame cycle, wait for the frame, then sleep only the remainder.

cycle = exposure + readout + handler_overhead max_trigger_rate = 1000 ms / cycle pacing_sleep = trigger_interval - cycle

Worked example, in milliseconds, with exposure at 8 ms, readout at 12 ms and handler overhead at 3 ms:

  • cycle = 8 + 12 + 3 = 23 ms, so this camera tops out at 1000 / 23 = 43 frames per second.
  • Trigger interval 1000 ms (one image per second): pacing_sleep = 1000 - 23 = 977 ms, applied after the frame lands.
  • Exposure later raised to 100 ms for a dark short-wave infrared scene: cycle = 100 + 12 + 3 = 115 ms, pacing_sleep = 885 ms - still positive, no overtrigger.
  • Rule: keep the trigger interval at or above the longest exposure you will ever set plus readout, with 20% headroom. Here 1000 ms against a 138 ms requirement, seven times the margin.

Rate the frames from the camera's per-frame timestamp rather than the host clock: camera buffer timestamp latency: interpret frame timing shows where the two disagree. What happens when the interval drops below the exposure is its own failure, with its own error - trigger call versus exposure time: understand capture timing and reduce exposure time mid-acquisition without an overtrigger error cover it.

A grab loop that survives both failure points

Putting it together: poll before the first trigger, wait on the frame event, sleep the remainder, gate the stop. frame_done is a threading.Event; frame_count is a one-element list used as a mutable counter.

import threading

TRIGGER_INTERVAL_S: float = 1.0
CYCLE_S: float = 0.023       # 23 ms from the worked example
FRAMES_WANTED: int = 10

frame_done = threading.Event()
frame_count = [0]

def handler(cam, stream, frame) -> None:
    frame_count[0] += 1
    cam.queue_frame(frame)
    frame_done.set()

# arm first: TriggerSource 'Software', TriggerSelector 'FrameStart',
# TriggerMode 'On', AcquisitionMode 'Continuous', then:
cam.start_streaming(handler)
try:
    assert wait_writable(cam, 'TriggerSoftware', probe), 'trigger never writable'
    while frame_count[0] < FRAMES_WANTED:
        cam.TriggerSoftware.run()
        if not frame_done.wait(timeout=5.0):
            raise TimeoutError('no frame within 5 s of a software trigger')
        frame_done.clear()
        time.sleep(max(0.0, TRIGGER_INTERVAL_S - CYCLE_S))
finally:
    if is_acquiring(cam):                    # your status probe
        cam.get_feature_by_name('AcquisitionStop').run()
    cam.stop_streaming()
print('frames:', frame_count[0])             # test: expect 10

Prove it three ways. Run it: ten frames, each logged with a complete status, no invalid access. Pull the cable mid-run: the 5 s wait raises, the finally block runs, and the process exits instead of hanging - hardware trigger grab timeout: interrupt the wait cleanly covers the interruption patterns. Set the interval below the cycle, say 20 ms: pacing_sleep clamps to zero, the loop self-limits to the camera's true rate, and no error appears.

Checks before you open a ticket

  • Set AcquisitionMode to Continuous and TriggerSelector to FrameStart before start_streaming; the order is part of the contract, as hardware trigger configuration per camera: stop the free run default shows.
  • No blind sleep between start and the first trigger - poll access and status, 10 ms poll, 2 s timeout.
  • Trigger interval at or above longest exposure plus readout, plus 20% headroom.
  • Stop gated on the acquiring status; tolerate the SDK's stop refusal when the camera already stopped.
  • Log the first exception; the cleanup cascade is noise.
  • Export the camera's current settings (most viewer tools save them) and send that file with the serial and firmware version through the contact page.

Any camera with a GenICam software trigger can show this pattern, including the everyday GigE workhorses:

Area scan camerasHikrobot MV-CT0MG0060/R/NN/NN 6 MP Area Scan Camera, 3rd Gen., GigE, Mono, BASEMV-CT0MG0060/R/NN/NN
Sensor format
1/1.8"
Lens mount
C-mount
Interface
GigE
Resolution
6-10 MP
$909.00View partBuild a system with it

The same loop drives compact USB3 sensors and cooled short-wave infrared modules unchanged:

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
$560.00View part
Infrared camerasShort-wave infrared area scan camera, 0.3 MP, GigE, Mono, 15μm x 15μm pixels (LS-IRC-0029)LS-IRC-0029
Sensor format
1/1.2"
Lens mount
C-mount
$5,999.00 · quotedView and quote

When the usual answer is wrong

Polling says writable, the status says acquiring, and still no frame arrives: the interval has dropped below exposure plus readout and the camera is discarding triggers. Raise the interval or shorten the exposure - one change at a time, so you know which one moved the result.

The camera never offers a writable trigger command at all: some models trigger from an input line only, so move the trigger to hardware and configure the line as hardware trigger source not detected: check GenICam line settings describes. And a feature read-only for minutes on a cooled sensor is a stabilisation phase, not a fault - widen the timeout past it once measured.

The decision, by symptom:

  • Error on the first TriggerSoftware.run() - the engine was not in the accepting state. Poll access and status, and drop the pre-trigger sleep.
  • Error only during teardown - the camera had already stopped. Gate the stop on status and let the refusal pass.
  • Never writable within 2 s - the camera is holding the feature. Trigger from frame arrival, or move to a hardware line.
  • Frames missing with no error - your interval is under the cycle. Pace on arrival and subtract the cycle from the interval.

From here, size the camera and interface in the camera finder, step through the configurator, or browse the systems topic, the area scan cameras and the infrared cameras ranges.

Questions

Why does deleting time.sleep() make the error go away?

Because the calls land in a different acquisition state, not because the sleep is forbidden. With no delay the trigger executes while the camera is still armed from start_streaming, so a complete frame comes back. After a delay the engine has moved to a state where the command feature is not writable, and run() refuses. It fails the other way too: a trigger fired too early, before the engine finishes entering acquisition, gives the same error. Poll, do not time.

What does read access not allowed with write access allowed mean?

It is the feature's access state at that instant: write-only. Command features are executed, never read, so the SDK's access check is what fails when run() is called. The report also tells you the feature exists and the name is spelled right - a missing feature raises a node-not-existing error long before access is checked. That distinction points you at the camera's state machine instead of your own code.

How do I set the frame rate without time.sleep()?

Convert your rate into an interval, subtract the frame cycle, and sleep only the remainder after each frame arrives. For one frame per second with a 23 ms cycle, wait on the frame event, then sleep 977 ms. When you need accuracy, rate from the camera's per-frame timestamp: host sleeps drift by the handler's execution time, which is why fixed delays gradually lose sync with the sensor.

Should I run AcquisitionStop before every restart?

Only when the acquisition status says the camera is acquiring. The GenICam standard features naming convention documents the stop command as optional in SingleFrame acquisition, and cameras that leave the acquiring state on their own will refuse it. That refusal is harmless if you catch it, fatal if it replaces your real error. Gate the stop on the status, catch the refusal, and restart from a known idle state.

What poll interval and timeout should I use?

10 ms poll, 2 s timeout before the first trigger, 5 s wait for a frame after each trigger. A 10 ms poll resolves state changes that a 1 s sleep smears over, and it costs almost nothing on the host. The 2 s figure assumes a warmed-up camera: a cooled sensor stabilising its temperature can hold features longer, so measure that phase once and size the timeout to cover it.

Does this apply to hardware triggers too?

The state machine does; the failure mode does not. A hardware line is sensed by the camera itself, so nothing in your code can fire it in a wrong state - but the arming order of TriggerSelector, TriggerSource and TriggerMode before acquisition starts, and the gated stop, are identical. Overtriggering is easier with hardware: keep the line's pulse rate under exposure plus readout.