Skip to content

Add event handling for TCPIP instruments, allowing SRQ interrupts. Closes #544#577

Open
SimonMerrett wants to merge 16 commits intopyvisa:mainfrom
SimonMerrett:events
Open

Add event handling for TCPIP instruments, allowing SRQ interrupts. Closes #544#577
SimonMerrett wants to merge 16 commits intopyvisa:mainfrom
SimonMerrett:events

Conversation

@SimonMerrett
Copy link
Copy Markdown

My second attempt at providing the event handling I would like to see for instruments capable of sending SRQ service requests. @MatthieuDartiailh helpfully advised on the approach and reviewed my previous PR but I did not and do not have enough Python skill to take that review into action. I have done my best in this PR to provide the feature but actually was able to test it on hardware which took 5 measurements at 1Hz and returned SRQ. I have attached Wireshark pcap showing the communication of a successful test (measurement results are not retrieved during the test).
pyvisa-py_srq.zip

I used an agentic workflow in opencode to plan, code, test, review. Some of the wider-reaching changes were bound up in reviewer recommendations for robustness, such as compatibility "tweaks" in attributes.py. Everything is up for editing but with this PR I am happy that it should pass some degree of testing (the agents were testing against the built-in tests and added their own for the event aspects). I am also more confident because I was able to take my script for my instrument and test the functionality was there, that measurements were taken, SRQ was sent properly as far as I can tell (not a hidden python thread polling the instrument status registers, like one of the earlier agents tried to sneak in).

Please go easy on the feedback and feel free to edit! If you agree that this is closer to the mark than #547 I will close that PR.

@MatthieuDartiailh
Copy link
Copy Markdown
Member

Thanks for the PR given the size of the changes ot will take me time to do a thorough review. Do not hesitate to ping me if I go silent for too long.

@xyphro
Copy link
Copy Markdown
Contributor

xyphro commented May 5, 2026

BTW: I really like that SRQs support gets added. Was missing it!

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 78.52273% with 189 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.00%. Comparing base (7c458f2) to head (bdc446a).

Files with missing lines Patch % Lines
pyvisa_py/protocols/vxi11.py 17.64% 126 Missing ⚠️
pyvisa_py/tcpip.py 49.33% 32 Missing and 6 partials ⚠️
pyvisa_py/highlevel.py 77.41% 13 Missing and 1 partial ⚠️
pyvisa_py/events.py 95.91% 2 Missing and 4 partials ⚠️
pyvisa_py/sessions.py 81.25% 3 Missing ⚠️
pyvisa_py/protocols/rpc.py 0.00% 1 Missing ⚠️
pyvisa_py/testsuite/test_events.py 99.76% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #577      +/-   ##
==========================================
+ Coverage   30.72%   39.00%   +8.27%     
==========================================
  Files          25       27       +2     
  Lines        4182     5053     +871     
  Branches      459      520      +61     
==========================================
+ Hits         1285     1971     +686     
- Misses       2882     3055     +173     
- Partials       15       27      +12     
Flag Coverage Δ
unittests 39.00% <78.52%> (+8.27%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Member

@MatthieuDartiailh MatthieuDartiailh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a first pass. I think one of the main thing that could be improved is the tracking of the type of events for which we handle events. Using an int flag internally could simplify many things I think.

Comment thread pyvisa_py/attributes.py Outdated
Comment on lines +13 to +15
# vicp may not exist in older pyvisa versions
_vicp = getattr(constants.InterfaceType, "vicp", None)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We enforce a recent enough version of pyvisa (or we should I will have to double check). So this is not needed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed now.

Comment thread pyvisa_py/attributes.py Outdated
Comment on lines +30 to +33
(constants.InterfaceType.vicp, "INSTR"),
*([(_vicp, "INSTR")] if _vicp is not None else []),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted

Comment thread pyvisa_py/events.py Outdated
from .common import LOGGER


@dataclass
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be marked as frozen since the intent is for instances to be immutable. Additionally we could have slot=True

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Both frozen and slots.

Comment thread pyvisa_py/events.py
self._deque.extend(kept)


HandlerCallback = Callable[[Any, constants.EventType, int, Any], None]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A docstring explaining the meaning of each argument would be welcomed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added docstring. Please confirm if formatting is acceptable or I need to look at this.

Comment thread pyvisa_py/events.py Outdated
def __init__(self) -> None:
self._lock = threading.RLock()
# event_type -> list of (handler, user_handle)
self._handlers: dict[
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a defaultdict here would simplify the code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultdict implemented

Comment thread pyvisa_py/events.py Outdated
def __init__(self) -> None:
# {event_type: {mechanism_int}}
self._lock = threading.RLock()
self.enabled: dict[constants.EventType, set[int]] = {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a Flag here rather than unnamed integer values in a set would be much more readable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flags implemented. Following pyvisa flag values.

Comment thread pyvisa_py/highlevel.py Outdated

"""

from __future__ import annotations
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this does not bring anything I would rather not add it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment thread pyvisa_py/highlevel.py
sess = self.sessions[session]
except KeyError:
return self.handle_return_value(session, StatusCode.error_invalid_object)
if event_type not in sess._supported_event_types:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some blank lines between logic block would improve the readability.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank lines added.

Comment thread pyvisa_py/sessions.py Outdated
if queue_enabled:
self._event_state.queue.put(ctx)
if handler_enabled:
session_handle = getattr(self, "_session_handle", self)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a bit suspicious. What is the rational.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not know but I asked for a review and this was the output

1. Design intent: why the getattr fallback exists
_session_handle is assigned only once in the entire codebase — in highlevel.py:201 immediately after an instrument session is created:
visa_session = self._register(sess)
sess._session_handle = visa_session
The getattr(self, "_session_handle", self) on sessions.py:387 is defensive coding: the author knew _session_handle is not part of the Session base class __init__ and added a fallback in case it was missing. However, the fallback self (the Python Session object) is never the correct value to pass to a VISA event handler.
2. The Resource Manager session
The RM session is created in highlevel.py:524 by registering the PyVisaLibrary object itself. _session_handle is never set on it.
Relevance: None. The RM session lacks _event_state, _supported_event_types, and any event monitor. If event methods were called on it they would AttributeError long before reaching _fire_event. _fire_event can only be invoked on instrument sessions, and those always receive _session_handle.
3. The test fixture
lib_and_session creates a MagicMock, manually attaches _event_state and _supported_event_types, registers it with the library, but forgets to set _session_handle. The existing test test_vxi11_fire_event_handler_mechanism passes only because its handler ignores the first argument, masking the fact that the mock object (not the integer handle) is being passed.
Verdict: This is a test bug, not a code bug. The fallback accommodates an incomplete test fixture.
4. VISA spec compliance
The handler contract (per the skill context and upstream pyvisa conventions) is:
handler(session: VISASession, event_type: EventType, event_ctx: int, user_handle: Any) -> None
VISASession is an integer handle. Passing self (a Python object) violates this contract.
---
Recommendation
✅ Recommended: Option A
Replace getattr(self, "_session_handle", self) with a direct attribute access:
session_handle = self._session_handle
Rationale:
- Every session that can legitimately fire events is an instrument session created through highlevel.open(), which unconditionally sets _session_handle.
- The fallback self is spec-violating and serves only to hide the incomplete test fixture.
- Direct attribute access makes the invariant obviously correct: "a session firing events must have a VISA handle".
- Option B documents incorrect behavior; Option C introduces unnecessary indirection for a problem that is really a missing test fixture line.
---
Required Test Changes
1. Fix lib_and_session fixture (test_events.py:~345)  
   Add one line so the mock session mirrors what real highlevel.open() does:
      @pytest.fixture
   def lib_and_session():
       lib = PyVisaLibrary()
       sess = MagicMock()
       sess._event_state = EventState()
       sess._supported_event_types = {constants.EventType.service_request}
       sess._start_event_monitor.return_value = StatusCode.success
       session_id = lib._register(sess)
       sess._session_handle = session_id          # <-- ADD THIS
       return lib, sess, session_id
   
2. Strengthen test_vxi11_fire_event_handler_mechanism (test_events.py:~695)  
   Update the handler to capture the session argument and assert it is the integer handle (sid), not the mock object:
      def test_vxi11_fire_event_handler_mechanism(self, lib_and_session):
       lib, sess, sid = lib_and_session
       calls = []
       def my_handler(session, event_type, context_id, user_handle):
           calls.append((session, event_type, context_id, user_handle))
       # ... install and enable as before ...
       Session._fire_event(sess, constants.EventType.service_request, ctx)
       assert calls == [(sid, constants.EventType.service_request, 5555, "uh")]
   
No other tests require modification because they either use the queue mechanism (which does not touch _session_handle) or test EventState/highlevel directly without calling Session._fire_event.

So I implemented the recommendation. I will need more guidance if this is leading in the wrong direction.

Comment thread pyvisa_py/sessions.py Outdated
session_handle = getattr(self, "_session_handle", self)
self._event_state.registry.fire(event_type, session_handle, ctx.context_id)

def _start_srq_monitor(self) -> StatusCode:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name seems a bit narrow since other types of event can be monitored

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generalised the names to _start_event_monitor / _stop_event_monitor

SimonMerrett and others added 12 commits May 6, 2026 21:14
…e access:

session_handle = self._session_handle
Rationale:
- Every session that can legitimately fire events is an instrument session created through highlevel.open(), which unconditionally sets _session_handle.
- The fallback self is spec-violating and serves only to hide the incomplete test fixture.
- Direct attribute access makes the invariant obviously correct: 'a session firing events must have a VISA handle'.
@SimonMerrett
Copy link
Copy Markdown
Author

@MatthieuDartiailh thanks for your review comments. I think I have addressed everything, although I cannot be confident I haven't introduced other mistakes. All pytest tests are passing but I haven't confirmed function on my test instrument yet. Please continue your review and I will confirm functionality asap.

@SimonMerrett
Copy link
Copy Markdown
Author

pyvisa-py_srq-test_script_log.txt
test_pcap_after_commit-bdc446a.zip
Just tested with my test instrument and had successful script output. pcap attached to assist with verification of the protocol.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Event handlers - is it possible for TCPIP INSTR resource?

3 participants