Skip to content

Add support for NI GPIB-ENET/100 bridge.#587

Open
bytewarrior wants to merge 79 commits into
pyvisa:mainfrom
bytewarrior:feature/ni-gpib-enet100
Open

Add support for NI GPIB-ENET/100 bridge.#587
bytewarrior wants to merge 79 commits into
pyvisa:mainfrom
bytewarrior:feature/ni-gpib-enet100

Conversation

@bytewarrior

Copy link
Copy Markdown

Summary

Adds a pure-Python session driver for the NI GPIB-ENET/100 Ethernet-to-GPIB bridge, modeled after the existing Prologix support. Once the companion pyvisa changes are merged, one can

rm = pyvisa.ResourceManager("@py")
rm.open_resource("NI-ENET100-TCPIP0::192.0.2.42::INTFC")
inst = rm.open_resource("GPIB0::16::INSTR")  # dispatched through the bridge
print(inst.query("*IDN?"))

with no additional dependencies (stdlib socket / struct / threading only).

Depends on

pyvisa#980 – Add InterfaceType.ni_enet100_tcpip, NIEnet100TCPIPIntfc rname class and Resource subclass.

Until that PR is merged, from pyvisa_py import nienet100 raises ImportError, which highlevel.py swallows at debug level (mirrors the vicp pattern). All NIENET100 tests skip cleanly in that case, so this PR remains importable today; it just stays inert.

What's added

  • Wire protocol (pyvisa_py/protocols/nienet100.py)
  • Discovery (pyvisa_py/protocols/nienet100_discovery.py) — UDP broadcast on the local broadcast domain.
  • Session layer (pyvisa_py/nienet100.py) — NIEnet100TCPIPIntfcSession for NI-ENET100-TCPIP::INTFC, NIEnet100InstrSession for GPIB::INSTR resources that route through a registered bridge.
  • Dispatch hook — chains in front of gpib.py's GPIBSessionDispatch and falls back to it for non-NIENET100 boards. Coexists with Prologix: load order is prologix → gpib → nienet100, dispatch order at open_resource is NIENET100 → Prologix → linux-gpib. Users wire each bridge to a distinct board number.
  • Module wiring in pyvisa_py/highlevel.py (one new try/except block).
  • Tests — offline unit tests for the wire layer, discovery, and SRQ verbs; assisted (hardware-gated) tests for the wire layer and full pyvisa-stack session layer.

Testing

Offline (always runs, no hardware needed):

  • pyvisa_py/testsuite/test_nienet100.py (~25 tests) — wire framing, status header parsing, open/close sequences, SRQ verbs.
  • pyvisa_py/testsuite/test_nienet100_discovery.py — discovery frame parsers + UDP loop with mocked sockets.

Hardware-gated (pyvisa_py/testsuite/nienet100_assisted_tests/) — opt-in via env vars:

PYVISA_TEST_NIENET100_HOST  # IP or hostname of the bridge
PYVISA_TEST_GPIB_PAD        # primary address of an instrument on the bus
PYVISA_TEST_GPIB_SAD        # (optional) secondary address
PYVISA_TEST_IDN_VENDOR      # (optional) substring expected in *IDN?
PYVISA_TEST_GPIB_TERM       # (optional) write/read termination — defaults to \n

Without PYVISA_TEST_NIENET100_HOST set, every assisted test skips cleanly. Verified locally against a real GPIB-ENET/100.

Out of scope / known limitations

These are documented in the code and listed here so reviewers don't have to chase them:

  • Six of the seven gpib_control_ren modes return error_nonsupported_operation — only VI_GPIB_REN_DEASSERT_GTL (which is by far the common case) is wired. The other six need an ibsre verb whose wire frame is not yet reverse-engineered. Prologix exposes none of these modes, so this is already a strict improvement.
  • No SRQ event delivery to pyvisa's event modelibwait is implemented at the wire layer but the bridge to pyvisa's enable_event/wait_on_event machinery is not.
  • ibrsp with cnt != 1 raises NIEnet100ProtocolError. The wire spec lets the bridge cram multi-byte STB blocks into a single response; in practice every device we've seen sends a single STB byte. Will relax when an instrument shows otherwise.
  • Multi-chunk ibrd data fragmentation — the parser handles it (accumulates into payload across data chunks until END or final-status), but has not been validated against very large device responses.
  • *IDN? instrument tests assume \n termination because pyvisa's \r\n default chokes several older GPIB instruments. The PYVISA_TEST_GPIB_TERM env var overrides.

The bridge itself has two intrinsic quirks worth noting for users:

  • Built-in timeout minimum ~3 s. Per-call tmo_ms in ibrd does not lower this floor — short pyvisa timeouts (e.g. inst.timeout = 200) surface as the configured error_timeout but after ~3 s wall-clock, not 200 ms. The socket-level timeout ceiling is sized to accommodate.
  • Limited concurrent sessions. Rapid open/close cycles can put the bridge into a state where it stops accepting new TCP connections for ~60 s. Practical workaround: serialize bridge sessions, or wait/power-cycle.

Reviewer guidance

The commit history is structured for sequential review — each commit is self-contained and the rough phases are:

  1. Wire-protocol primitives (chunks, status headers, command framing).
  2. Open / close sequences and Device-I/O verbs.
  3. Session-layer integration into pyvisa-py (pyvisa_py/nienet100.py, dispatch hook, module loader).
  4. SRQ verbs and lazy wait/control socket lifecycle.
  5. Discovery (frame primitives, UDP loop, list_resources wiring).
  6. Offline tests.
  7. Hardware-gated tests.
  8. Wire-level fixes uncovered by hardware testing (ibrsp combined chunk, ibrd no-END path, IbcTMO rejection, status-header chunk wrapping).
  9. Session-layer fixes uncovered by full-stack hardware testing (resource-string mapping, board-key type, timeout handling, test robustness).

The protocol was reverse-engineered from wire captures and the gpibhlpr.dll decompilate; key wire-layer details (status-header chunking, ibrsp combined-chunk format, ibrd two-chunk model, tmo_ms semantics) are documented inline at their use sites.

  • Executed ruff with no errors
  • The change is fully covered by automated unit tests

still open, will continue after initial feedback:

  • Documented in docs/ as appropriate
  • Added an entry to the CHANGES file

@MatthieuDartiailh

Copy link
Copy Markdown
Member

Thanks for this. It will take me time to go through all of it. Do not hesitate to ping me if you do hear back within a couple of weeks.

@bytewarrior

Copy link
Copy Markdown
Author

Thanks, Matthieu. Your efforts are very much appreciated.

Comment thread pyvisa_py/nienet100.py Outdated
Comment thread pyvisa_py/nienet100.py
Comment thread pyvisa_py/nienet100.py Outdated
Comment on lines +324 to +329
# The wire-level ibrd always reads the full message (until EOI/EOS);
# truncate here if the caller's max-count is smaller. Extra bytes
# are dropped — ibrd has no resume semantics, so a caller that
# supplied too small a count loses the tail.
if len(data) > count:
return bytes(data[:count]), StatusCode.success_max_count_read

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 is not acceptable. The session should maintain an internal buffer to allow reading responses one byte at a time (useful in debugging).

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.

Understood. I have added an internal read buffer to the INSTR session. Reading responses byte-wise is now possible.

Comment thread pyvisa_py/nienet100.py
Comment thread pyvisa_py/nienet100.py Outdated
return StatusCode.error_system_error


# --- (gpib, INSTR) dispatch hook --------------------------------------------

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 following feels quite brittle since it relies heavily on the order of the imports. It may be better to have a central location registering GPIB::INSTR if any of the possible interfaces is functional and managing the dispatching in a single place.

@bytewarrior bytewarrior Jun 4, 2026

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.

Yes, this is exactly the point where I was unsure how to solve it.

This single place you're referring to is probably supposed to be in pyvisa, not pyvisa-py?

Thinking further, and as an alternative, one could perhaps add a dispatcher here in pyvisa-py, something like gpib_dispatch.py. This one would populate the (gpib, INSTR) slot once and for all without the cumbersome pop/re-register stuff.

It could also create an ordered candidate list of backends, to which each backend would contribute if imported successfully.

What do you think?

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.

My idea was indeed to have that single point of registration living in pyvisa-py.

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.

Ok, and I suppose it's okay to implement this inside this PR and not create a separate one?

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.

Yes

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.

Ok, so I went ahead and gave it a try. Instead of the earlier approach of popping and re-wrapping the registry at import time, the backends are now registered as priority-ordered resolvers:

  • NI-ENET/100 bridge (priority 10): board bound to a NI GPIB/ENET-100 INTFC
  • Prologix (priority 20): board bound to a Prologix controller
  • native linux-gpib / gpib-ctypes (priority 100): catch-all fallback

register_builtin_backends() imports each backend defensively, so a backend whose import fails (e.g. gpib.py raising when no GPIB library is installed) is simply absent from the list. As a consequence, the remaining backends keep working. The module itself has no hard dependency on any GPIB library, so it can own the slot even when gpib.py fails to import.

Dispatch no longer depends on the order in which backends are imported, and adding another GPIB backend in the future is a one-line register_backend().

I've yet come across another question: list_resources() still enumerates only native GPIB listeners (it delegates to gpib._find_listeners() exactly as the old GPIBSessionDispatch.list_resources did). Prologix has never been listed (the old # FIXME is still accurate), and GPIB-ENET/100s are discovered at the INTFC level via NI-ENET100-TCPIP…::INTFC. For now, I kept that behaviour identical rather than change discovery semantics.
Is this okay?

@MatthieuDartiailh

Copy link
Copy Markdown
Member

First quick round of review for high level stuff and interfaces.

@MatthieuDartiailh MatthieuDartiailh left a comment

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 general comment, I did not go through everything yet. Since the supported Python version is 3.10 for new/edited code I would like to avoid the typing imports List, Tuple, etc

@bytewarrior

Copy link
Copy Markdown
Author

I've made two new commits-- one addressing the Python 3.10 compatibility/ syntax issue.
One more on the way as I noticed a few offline tests would not run on Linux.

@MatthieuDartiailh MatthieuDartiailh left a comment

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 comments but we are close.

Thanks a mot for tour work.

Comment on lines +33 to +37
#: A resolver inspects a parsed resource name and returns the session class
#: that should handle it, or ``None`` if the resource is not served by this
#: backend (e.g. the board number is not registered to it). Resources reach
#: this dispatcher only via the ``(gpib, "INSTR")`` slot, so the parsed name
#: is always a :class:`~pyvisa.rname.GPIBInstr`.

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.

I have been convinced that even though this format is supported by sphinx using standard docstring after the object is better in particular because it is better supported by IDE.

So at least in new contributions it would make sense to do it that way.

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'll format it as you wish, but I'm a bit uncertain now what to do. Should I change it to improve the IDE experience, or leave it as it is?

Comment thread pyvisa_py/nienet100.py Outdated
Comment on lines +31 to +42
# Resolve the required pyvisa names early so a missing upstream PR produces
# an ImportError that highlevel.py logs at debug level (mirrors how vicp
# falls back when pyvicp is not installed). Users opening NI-ENET100-TCPIP
# resources then see a clean "No class registered" error instead of a
# cryptic AttributeError during session creation.
try:
_IFACE_NIENET100_TCPIP = constants.InterfaceType.ni_enet100_tcpip
except AttributeError as e:
raise ImportError(
"pyvisa-py NI GPIB-ENET/100 support requires pyvisa with "
"some definitions specific to nienet100; please update pyvisa."
) from e

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.

In this case this should be simply handled by requesting a recent enough version of pyvisa in pyvisa-py.
For the development period it may be worth keeping but we should cleanup before the release.

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.

Ok, I'll add a TODO here, and leave that until the end, when it is known which minimum version is required.

Comment thread pyvisa_py/protocols/nienet100.py Outdated
older GPIB-ENET (10 MBit/s, libnienet target), which uses a similar frame
layout but different verb opcodes and a single-step open.

Wire reference: ``work/GPIB-ENET-100_Protocol.md``.

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 reference does not exist.

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 is an overlooked reference to my protocol reverse engineering docs I did not catch. I'll reformulate and update the comment and remove the stale reference.



@require_instrument
def test_idn_query_via_pyvisa(inst):

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.

Could you extend this test to test reading the response using multiple read bytes to stress the use of the intermediate buffer.

@bytewarrior bytewarrior Jun 21, 2026

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. Also, I've noticed the code would not clear the buffer in case of a scenario like:

  • write and start to read the response
  • leave tail bytes in the buffer
  • write again, expect to read response from the second write

Hence, I've modified the write to clear the intermediate buffer as well, and added a test to capture this case.

@bytewarrior

Copy link
Copy Markdown
Author

I have found a problem with ibwait while running the tests many times in succession with real hardware. I probably have to change a few pieces regarding the ibwait/SRQ machinery. Working on it.

@bytewarrior

bytewarrior commented Jun 22, 2026

Copy link
Copy Markdown
Author

Ok, this was a bit more than I had expected. I've run into some issues while running the tests multiple times. Turns out my conclusions drawn from the disassembly alone were not entirely correct regarding ibwait/SRQ, in fact this led to removing significant portions of code dealing with 5003/5005 ports (as of now unused, never opened).

I decided to give the code this kind of cleanup. I hope you don't mind. I have addressed your comments as well of course.

@MatthieuDartiailh MatthieuDartiailh left a comment

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.

Sorry but this PR is huge. I did a review on the bulk of the code. I will need more time to go through the added tests.

Comment thread pyvisa_py/nienet100.py
Comment on lines +173 to +176
if not self.open_timeout:
connect_timeout_s = 10.0
else:
connect_timeout_s = max(self.open_timeout / 1000.0, 0.001)

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 is not exactly wrong but open_timeout is specified as a mean to control the timeout on acquiring a lock when open the resource. It confuses all users. Here we deviate from the standard but match expectations.
We can keep it I just wanted to point out this VISA quirk.

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.

Understood. We'll leave it as it is for now, but it can be changed when needed. Also, I think the comment above it summarizes the situation well enough.

Comment thread pyvisa_py/nienet100.py Outdated
self.interface = nienet100.EnetConnection(
host,
open_timeout=connect_timeout_s,
timeout=connect_timeout_s,

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 however is wrong I think, you should pass the value of the timeout attribute if set or a default value.

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 have reworked the timeout machinery, this should be fixed now as well.

Comment thread pyvisa_py/nienet100.py Outdated
Comment on lines +341 to +349
# A new write starts a fresh exchange: drop any buffered-but-unread
# bytes left over from a partial read of a previous response so the
# stale tail cannot be prepended to the reply for this command.
self._read_buffer.clear()
try:
written = self.interface.ibwrt(data)
except nienet100.NIEnet100IOError as e:
return 0, _map_iberr_to_status(e.err)
return written, StatusCode.success

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.

I never checked what happen with other VISA implementation. Not reading the response usually get a query interrupted error from the device but here the issue is different. I am not sure it is our responsibility to deal with poorly managed message recovery in this way.

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 was fixed in commits 7f0a9ed and b15748b. The read timeout is now driven from the session timeout attribute rather than a fixed value: _sync_read_timeout() converts self.timeout into the bracket tmo_code and falls back to T10s (10 s) when the attribute is unset, and after_parsing seeds the operational socket timeout from self.timeout (else the connect default). It's applied both at open and on mid-session timeout changes via _set_timeout -> _sync_read_timeout.

Comment thread pyvisa_py/protocols/nienet100.py Outdated
Comment on lines +45 to +123
# --- NI-488.2 ibsta bits (subset relevant to this protocol) -----------------

STA_ERR = 0x8000 # operation error, ``err`` field carries the code
STA_TIMO = 0x4000 # timeout during operation
STA_END = 0x2000 # EOI or EOS match (talker signaled end-of-message)
STA_SRQI = 0x1000 # SRQ detected while controller-in-charge
STA_RQS = 0x0800 # device RQS asserted (set in ibrsp/ibwait responses)
STA_CMPL = 0x0100 # operation complete
STA_LOK = 0x0080 # lockout state
STA_REM = 0x0040 # remote state
STA_CIC = 0x0020 # controller-in-charge
STA_ATN = 0x0010 # ATN line asserted
STA_TACS = 0x0008 # talker active
STA_LACS = 0x0004 # listener active
STA_DTAS = 0x0002 # device trigger state
STA_DCAS = 0x0001 # device clear state


# --- NI-488.2 iberr codes (subset relevant to this protocol) ----------------

ERR_EDVR = 0 # OS error (rare)
ERR_ECIC = 1 # function requires controller-in-charge
ERR_ENOL = 2 # no listener on the bus
ERR_EADR = 3 # address error
ERR_EARG = 4 # invalid argument to API
ERR_ESAC = 5 # function requires system controller
ERR_EABO = 6 # I/O aborted / timeout
ERR_ENEB = 7 # non-existent board
ERR_EBUS = 0xA # bus error
ERR_ECAP = 0xB # capability disabled
ERR_EFSO = 0xC # file-system error
ERR_EBNP = 0xD # board not present
ERR_ESTB = 0xE # serial-poll status byte lost
ERR_ESRQ = 0xF # SRQ stuck on


# --- NI-488.2 timeout codes (TMO index, not milliseconds) -------------------
# Used in SetConfig Frame A byte[8] and in the ``'P 03'`` property setter.

TMO_NONE = 0
TMO_10us = 1
TMO_30us = 2
TMO_100us = 3
TMO_300us = 4
TMO_1ms = 5
TMO_3ms = 6
TMO_10ms = 7
TMO_30ms = 8
TMO_100ms = 9
TMO_300ms = 10
TMO_1s = 11
TMO_3s = 12
TMO_10s = 13
TMO_30s = 14
TMO_100s = 15
TMO_300s = 16
TMO_1000s = 17

#: Discrete timeout values in seconds, indexed by TMO code. ``None`` = disabled.
TIMETABLE: tuple = (
None, # TMO_NONE
10e-6,
30e-6,
100e-6,
300e-6,
1e-3,
3e-3,
10e-3,
30e-3,
100e-3,
300e-3,
1.0,
3.0,
10.0,
30.0,
100.0,
300.0,
1000.0,
)

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.

PR #601 is standardizing those so that we stop having bare integer all over the place. Could you use those once the PR has been merged ?

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.

Yes, of course.

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.

PR #601 has been merged so you can use the constants it defines.

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.

Comment thread pyvisa_py/protocols/nienet100.py Outdated
Comment on lines +551 to +552
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("← main: %s", data.hex())

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 Python logging module will take care of filtering logging for disabled levels, no need to do it manually.

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.

Basically yes. However, without the if, data.hex() will be called in a rather hot code path (recv/send), allocating memory for each conversion, even when debug logging is not enabled.

This is why the if was added.

Should I remove the two if statements anyway? It's only used in send/recv this way.

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.

You can directly request the formatting in the string using %x and this way you avoid the need to manually call hex.

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'm probably doing something wrong here. I cannot directly use "%x", because data is a bytes-like object. "%x" on the other hand expects a single integer. What I wanted to do here is to have a "lazy" hex conversion.

Any more thoughts/ suggestions?

Should I just drop the if altogether and accept the performance penalty at this point? After all, I'm wondering if it makes a large difference anyway.

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.

The if line was removed with commit db0ca25.

bytewarrior and others added 19 commits July 5, 2026 12:02
New module pyvisa_py/protocols/nienet100.py with constants and the
12-byte command/status frame primitives. Pure encoding/decoding, no
sockets yet — those follow in later commits.

Covers:
- NI-488.2 ibsta/iberr bits, TMO code table, seconds_to_tmo_code helper
- pack_command / parse_status_header / parse_chunk_header
- Exception hierarchy (NIEnet100Error / IOError / ProtocolError)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
read_chunks_until_end consumes the data-chunk stream that follows the
preliminary status header of a read response, returning concatenated
payload at the END marker. Out-of-band signal chunks (flags=2) are
logged and skipped per the defensive-handling note in the wire spec.

read_one_data_chunk covers verbs whose response is a single fixed-size
chunk and may omit the END marker (ibrsp returns 1 STB byte).

Both helpers take a read_exactly callable so the layer stays socket-free
and is straightforward to unit-test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EnetConnection opens the main socket (port 5000) and the mandatory
companion socket (port 5015) and sends the 'U 02' companion hello as
required by every GPIB-ENET/100 firmware shipped in the last ~20 years.
Wait (5003) and control (5005) sockets are deliberately not opened
here; they are only needed for ibwait and async notify-off paths and
will be added in a later step.

The class exposes minimal recv/send helpers (recv_main_exactly,
send_main, read_status_main, transact_main) so subsequent commits can
build verb-level operations on top without touching socket internals.
close() is idempotent and safe to call before open().

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
open_gpib_session sends the seven-frame open sequence (Frames A through
G of the wire spec) on the main socket: SetConfig with the SC bit,
PPC mode, board-flags SetConfig, online, event-queue depth, bracket
open, and the defensive notify-off. After it returns the box is ready
for Device-I/O against the requested primary/secondary address.

close_gpib_session sends the matching bracket-close frame and swallows
errors so socket cleanup always runs.

Each frame includes a comment showing the exact wire bytes per the
spec so the layout is reviewable against the reference at a glance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the minimal pyvisa-Resource API surface on top of the main
socket and the chunk reader:
- ibwrt sends the 0x62 header + raw payload in a single sendall;
  odd-length payloads are zero-padded on the wire as required.
- ibrd reads the preliminary status, the chunk stream until END, and
  the final status (the box does not take a max-count argument so the
  message is read whole; callers that need to truncate do so after).
- ibclr / ibtrg / ibloc are simple 12-byte command + status round-trips.
- ibrsp reads one data chunk; per the spec the END marker may be
  omitted so we deliberately do not try to consume it.
- set_io_timeout maps to the IbcTMO property (idx 0x03); takes a
  discrete NI-488.2 TMO code, not milliseconds.

Async verbs (ibwait, ibnotify) and board-level verbs (ibsic, ibcmd)
are deferred until the wait/control sockets land.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New module pyvisa_py/nienet100.py with the INTFC half of the session
layer. The INTFC opens an EnetConnection (main + companion sockets,
companion hello) as a connectivity sentinel and registers itself in
_NIEnet100IntfcSession.boards under the resource's board number. The
GPIB dispatch hook (added in a later commit) consults that registry to
route GPIB<n>::*::INSTR resources through the appropriate bridge.

Requires pyvisa to expose InterfaceType.ni_enet100_tcpip and
rname.NIEnet100TCPIPIntfc — both will land in a separate pyvisa PR.
Until then this module raises ImportError on load, which highlevel.py
swallows the same way it does for missing optional backends (e.g.
pyvicp). Users opening NIENET100 resources before the pyvisa change
ships get a clean "No class registered" error from the dispatcher.

Reformatting of the existing protocols/nienet100.py to ruff-format
output rides along since both files were touched together.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NIEnet100InstrSession is instantiated by the GPIB dispatch hook (next
commit) for GPIB<n>::<pad>[::<sad>]::INSTR resources when board n was
previously bound by a NIENET100-TCPIP::INTFC session.

Each INSTR owns its own EnetConnection (main + companion sockets) and
its own bracket (Frames A-G). The wire spec recommends per-resource
TCP sessions over multi-PAD bracket switching on a shared connection
and that is what we do: no cross-resource locks, no shared interface
state. Multiple instruments on the same bridge each pay one extra TCP
session to the box but gain simplicity and concurrency.

Implements write/read/clear/assert_trigger/read_stb/gpib_control_ren
(go-to-local only, the other REN ops need board-level verbs that have
not landed yet). The timeout setter maps pyvisa milliseconds to a
discrete NI-488.2 TMO code for the wire layer and a small-margin
socket timeout so the box always reports its own timeout first.

_map_iberr_to_status translates the bridge's iberr field into the
closest pyvisa StatusCode so the dispatcher returns meaningful errors
to user code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hook the bridge into the (gpib, INSTR) dispatch table by registering a
wrapping dispatcher in nienet100.py. Boards bound to a NIENET100-TCPIP
INTFC route to NIEnet100InstrSession; everything else delegates to the
previously registered class (typically GPIBSessionDispatch from
gpib.py) so Prologix and linux-gpib paths keep working unchanged.

Putting the hook in nienet100.py instead of gpib.py lets it work on
systems where gpib.py fails to import because neither linux-gpib nor
gpib-ctypes is installed — exactly the configuration most GPIB-ENET/100
users run.

The prior registration is popped before our @Session.register call so
the "already registered, overwriting" warning does not fire on every
import; the overwrite is deliberate.

highlevel.py picks up the new module via the same try/except pattern
used for the other backends so an outdated pyvisa (missing the
upstream InterfaceType.ni_enet100_tcpip change) silently disables the
NIENET100 path instead of breaking pyvisa-py load.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
34 tests covering pyvisa_py.protocols.nienet100 without network:
- Frame pack/unpack, status header and chunk header parsing.
- Chunk reader: concatenation across data chunks, END marker,
  defensive tolerance of signal chunks (flags=2), and the protocol
  errors raised for unknown flags or END-with-payload.
- read_one_data_chunk for ibrsp-style single-chunk responses.
- IP-to-u32 helper and seconds-to-TMO-code rounding.
- Device verbs (ibwrt, ibrd, ibrsp, ibclr, ibtrg, ibloc,
  set_io_timeout) driven against an in-memory scripted peer over
  socket.socketpair, asserting exact wire bytes on the send side and
  scripted box responses on the recv side. Includes the iberr->raise
  error path.

Hardware-gated integration tests against a real bridge are not
included here and will land separately once a test fixture exists.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EnetConnection grows two new optional sockets and the lazy helpers
that own their setup:

- ensure_wait_socket() opens port 5003 and sends the device-mode
  async-register frame ('U 01' with flags=2 carrying the main socket's
  getsockname) followed by the 'P 10 01' online re-confirm. After this
  the box accepts ibwait polls from the wait socket and matches SRQs
  against the main session.
- ensure_control_socket() opens port 5005 with no setup frames; the
  first 'O' verb sent there carries whatever the box needs.

Both helpers are idempotent. close() and set_socket_timeout() are
extended to walk all four sockets so existing call sites keep working
unchanged. The class is documented as not thread-safe — concurrent
ibwait polling and synchronous Device-I/O on the same instance would
interleave bytes on the sockets.

ibwait, ibsic, and notify-off-async land in the next commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ibwait(mask) issues one polling round-trip on the wait socket (lazily
opened on first call) and returns the box's sta. The caller decides
how to react: STA_RQS means an SRQ is pending (acknowledge with
ibrsp); STA_TIMO means the box's IbcTMO fired without an event. The
polling loop itself is left to the caller — single-threaded adapters
do fine with 0.2-0.5 s sleep intervals per the wire spec.

Wire layout: 54 00 [htons(mask):2] 00*8.

A higher-level wait-for-srq helper on the pyvisa-py session layer can
be added when pyvisa-py grows a real event mechanism; until then user
code can call session.interface.ibwait(...) directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two new verbs on the control socket (port 5005), both using the 'O'
family layout (IP-before-port, unlike 'U' verbs which put port first):

- ibsic pulses the GPIB IFC line and is useful as a board-level reset
  between tests or to recover from a hung instrument.
- notify_off_async_device deregisters the async event channel that
  ensure_wait_socket previously registered.

close() is extended to call notify_off_async_device automatically when
the wait socket was opened, so the box does not keep the registration
around between sessions. The cleanup is best-effort: errors during it
are logged and swallowed so socket teardown always runs.

A shared _pack_o_verb helper captures the wire layout so future
'O' verbs (ibwait re-arm, notify-off-board) can reuse it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 new tests covering the wait/control socket lifecycle, ibwait, ibsic,
notify_off_async_device, and the close-time notify-off cleanup. All
drive scripted peers over socket.socketpair so the suite still does no
real network I/O. _make_bound_connection grows wait/control = None
defaults so existing tests keep working; _make_empty_connection is the
new helper for lifecycle tests that monkey-patch _connect.

Total test count goes from 34 to 44, all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New module pyvisa_py/protocols/nienet100_discovery.py with the pack
and parse halves of the bridge's UDP discovery protocol. The 184-byte
frame format is captured here once with named offsets so the layout
matches the wire spec by inspection (every byte position is referenced
via a named constant rather than a magic number).

BoxInfo is a frozen dataclass carrying the parsed IP/MAC/serial/model/
hostname/subnet/gateway/comment plus the echoed nonce and the response
op-code. The is_busy property covers the OP_BUSY (0x09) reply variant
so callers can distinguish a found-but-busy box from a found-and-idle
one without poking at the op-code directly.

parse_discovery_response returns None (rather than raising) for any
frame that fails magic/length/op-code validation. This matters because
the discovery socket is a broadcast listener that will see arbitrary
foreign UDP datagrams from other devices on the LAN — silently
discarding them is the correct behavior.

The UDP socket loop (discover()) and the integration with
NIEnet100TCPIPIntfcSession.list_resources() land in the next commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
discover() opens a single UDP socket bound to ('', port) with
SO_REUSEADDR and SO_BROADCAST set, fires one 184-byte probe at the
configured broadcast (or unicast) destination, and reads replies until
the caller-supplied timeout elapses. Replies are parsed via
parse_discovery_response so foreign UDP traffic and the bridge's own
echo of our outgoing probe are silently filtered out. Results are
deduplicated by MAC (boxes commonly answer once per host network
interface) and returned sorted by IP for stable output.

The recv loop tolerates Windows' WSAECONNRESET behavior: when a prior
sendto generates an ICMP port-unreachable response, Windows surfaces
it on the next recvfrom as ConnectionResetError. Treating that as a
single skipped packet (rather than scan-aborting) keeps unicast scans
of partially-reachable subnets useful.

Port parameter doubles as the destination port (where the probe is
sent) and the bind port (where replies arrive). In production both
are 44515 (broadcast) or 44516 (cross-subnet unicast); separate-port
testing happens via in-test monkey-patching in a later commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NIEnet100TCPIPIntfcSession.list_resources now calls into the discovery
helper and emits one resource string per reachable bridge on the local
broadcast domain. The board number is enumerated by discovery sort
order so that multiple bridges on the same LAN come back as
NIENET100-TCPIP0, NIENET100-TCPIP1, ... and can all be opened without
manual disambiguation.

Discovery errors (bind conflict, missing broadcast route) are caught
and surfaced as an empty list rather than propagated — list_resources
is best-effort across all pyvisa-py backends and a noisy failure here
would clutter the resource manager.

The discovery import is local to the method to keep top-level imports
focused on the TCP code paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
22 tests in a new test module mirroring the discovery module split:

- pack_discovery_request: layout, big-endian nonce, zeroing of unset
  fields (paranoid all-bytes-except-known-set must be zero check).
- parse_discovery_response happy paths: full-field round-trip, OP_BUSY
  flag, empty strings, NUL-terminator truncation past garbage bytes.
- parse_discovery_response validation: parametric coverage of wrong
  length / bad magic / wrong op-code / truly foreign datagrams.
- discover(): sorted-by-IP output, MAC dedup (default and opt-out),
  foreign-frame skip, ConnectionResetError tolerance (Windows path),
  timeout-empty path, probe destination correctness, bind-failure path.

discover() tests use unittest.mock to patch socket.socket so the suite
needs no broadcast-capable interface, no port 44515, and no hardware.

Total suite goes from 44 to 66 tests, all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New nienet100_assisted_tests/ subpackage in the testsuite, mirroring
the existing keysight_assisted_tests/ convention. Tests are gated by
environment variables and skip cleanly when no hardware is configured.

Configuration:
- PYVISA_TEST_NIENET100_HOST: bridge IP (required)
- PYVISA_TEST_GPIB_PAD: instrument primary address (required for the
  instrument-level subset)
- PYVISA_TEST_GPIB_SAD: optional secondary address
- PYVISA_TEST_IDN_VENDOR: optional substring asserted in *IDN? reply

test_wire.py drives EnetConnection directly, bypassing the pyvisa-py
session layer. That keeps it independent of the pending pyvisa upstream
change for InterfaceType.ni_enet100_tcpip — useful for first-light
validation against new hardware. Coverage:

- Discovery (broadcast and unicast/cross-subnet variants) must find the
  configured bridge.
- Main + companion socket open/close round-trip.
- Instrument fixture runs Frames A-G open + bracket close on teardown,
  even when the test body raises, so failed tests do not leave state on
  the bridge.
- *IDN? round-trip with optional vendor-substring assertion.
- ibclr / ibtrg / ibrsp accept and complete.
- Timeout configured via IbcTMO=T100ms surfaces as iberr=EABO and
  fires within the configured window (under 5 s sanity bound).
- ibwait exercises the lazy wait-socket setup + async-register +
  online-reconfirm sequence with a real bridge.

Session-layer tests (via ResourceManager('@py')) land next.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
test_session.py exercises the complete pyvisa-py path: ResourceManager
opens the NIENET100-TCPIP::INTFC (binding board 0 in the dispatch
table), then GPIB0::<pad>::INSTR resolves to NIEnet100InstrSession via
the wrap-dispatcher. This catches integration issues that wire-level
tests miss: attribute plumbing, error-code mapping, timeout setter
behavior, fixture-style lifecycle, and the dispatch hook itself.

Module-level pytestmark skips the whole file when pyvisa_py.nienet100
fails to import — that's the dev-machine state until the upstream
pyvisa change for InterfaceType.ni_enet100_tcpip ships. The
ImportError is caught at module load so test collection still
succeeds; the tests just report as skipped with the reason.

Fixtures are scoped so the ResourceManager and INTFC are reused across
the whole module while each test gets its own per-test INSTR session,
keeping bridge state turnover low without sharing instrument state
between tests.

Coverage: rm.list_resources discovery, INTFC board registration,
*IDN? via Resource.query, clear/read_stb/assert_trigger, timeout
mapped to StatusCode.error_timeout, and a back-to-back-query
regression guard for state leakage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bytewarrior and others added 21 commits July 5, 2026 12:05
This was only a remnant of earlier edits.
read() caches a whole wire message and hands it out in count-byte
slices, refilling from the wire only when the buffer is empty. A new
write must therefore drop any tail left over from a partial read of the
previous response; otherwise the next read returns the stale tail (which
ends in the termination char) and the real reply desyncs onto the
following read. clear() and close() already clear the buffer; write()
now does too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover the intermediate read buffer in the INSTR session:

- read an *IDN? response one byte per backend read (chunk_size=1) to
  exercise the slice/refill path,
- a partial read followed by a new write, to ensure the leftover tail is
  discarded rather than prepended to the next response.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pyright flagged HOST (str | None) being passed where str is required in
test_unicast_discovery_against_configured_bridge and
test_open_and_close_main_companion. require_bridge guarantees HOST is
set, so assert it — same pattern already used in _resolve_host_ip and
the opened_session fixture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_ibwrt padded odd-length payloads with a trailing NUL while still sending
the unpadded byte_count. The bridge rejects that mismatch and replies with
a malformed (22-byte) chunk instead of a 12-byte status, so any odd-length
device write failed. The genuine NI software sends odd-length payloads with
no padding (e.g. *SRE 16 = 7 bytes, count=7); match that and send exactly
byte_count bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 'U 02' companion hello carries a port/ip the bridge uses to link the
companion socket (the async/SRQ event channel) to the main session. It must
be the main socket's address, not the companion's own. Reporting the
companion's port left the event channel unlinked: an ibwait poll on the
companion was TCP-acked but never answered, so SRQ waits hung (and abandoning
the poll wedged the bridge). The genuine NI software reports the main
socket's port here; match it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shipped ibwait used a fabricated 0x54 poll on a dedicated wait socket
(port 5003) that the bridge does not actually implement — it never answered
and wedged the box. The real protocol polls with verb 0x22 on the companion
socket (the event channel linked to the session by the companion hello); the
box blocks and replies with a status whose sta is the ibwait result. The
async arm is already sent as Frame G of open_gpib_session, so no extra setup
is needed. Drops the dependency on the wait socket entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The serial-poll response packs the status header and the STB into one
13-byte chunk with the STB as the trailing byte. The header's cnt field is
not reliably 1: a serial poll taken right after an SRQ wait reports cnt=0
with a perfectly valid STB. Drop the cnt==1 check and read the STB by
position so post-SRQ serial polls succeed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wait socket (port 5003) async-register/online-reconfirm path and the
'O 4e' notify-off were reverse-engineered from static analysis and do not
match the bridge: the box never implemented them, and sending those frames
hangs it. ibwait now polls the companion socket directly and the async
channel is armed by Frame G of open_gpib_session, so none of this is needed.

Removes ensure_wait_socket, _send_async_register_device,
_send_online_reconfirm, notify_off_async_device, the notify-off call in
close(), the wait socket attribute, PORT_WAIT and ASYNC_REGISTER_FLAGS_DEVICE,
plus their tests. The control socket and ibsic ('O 49' on 5005) are left
untouched. close() now just drops the sockets after the bracket-close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ibwait round-trip was a smoke test that accepted any sta. Arm a
deterministic Service Request instead (*SRE 16 enables SRQ-on-MAV, *IDN?
queues a response so MAV/RQS is set), assert ibwait reports STA_RQS, and
serial-poll a status byte carrying the RQS bit. Drains the queued response
afterwards. Verified across repeated full assisted-suite runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- session module docstring no longer lists the removed port 5003 (the
  wait socket); it now reads 5000/5005/5015.
- EnetConnection class docstring: sockets are connected by open(), not "on
  instantiation".
- drop the "first-light" phrasing from the wire-test module docstring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous _ibsic emitted an O 49 (4f 49) verb with an IP/port payload on a separate control socket (5005). A live capture of the genuine NI software issuing ibsic shows IFC is a bare 0x1c command on the main socket with no payload; the box replies CMPL|CIC|ATN. Rewrite _ibsic to transact_main(pack_command(0x1c)) and update its unit test to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ibsic was the only user of the control socket (port 5005) and the _pack_o_verb 'O'-verb builder. Now that the verified ibsic runs inline on the main socket, the capture confirms the bridge never opens 5005 at all, so drop the dead machinery: PORT_CONTROL, ensure_control_socket, the EnetConnection.control attribute (and its close()/set_socket_timeout handling), _pack_o_verb, and the matching tests. The protocol now uses only the main (5000) and companion (5015) sockets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the board (INTFC) counterpart to open()+open_gpib_session (device): identify, board config (mode/setconfig/online), a board-mode companion hello, and the two board verbs the genuine NI board-open emits. This leaves the box online and CIC-capable, which hardware shows is the state it requires before it accepts a board-level ibsic (a bare open() never gets an ibsic reply and wedges the box). Parametrizes _send_companion_hello with the companion-hello flag word (0 for board, 2 for device).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both session types converted open_timeout=0 (the pyvisa default, VI_TMO_IMMEDIATE) into a ~1 ms socket timeout via max(0/1000, 0.001). That is far too short: the INTFC open's multi-frame handshake times out outright and the INSTR's TCP connect becomes flaky. Treat 0 like None and fall back to the 10 s default the other backends use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements gpib_send_ifc on the NI-ENET/100 INTFC session, mapping viGpibSendIFC to interface.ibsic() (IFC is a board operation, placed on INTFC as in the linux-gpib backend). The INTFC now opens a board session (open_board_session) so the box is online and accepts IFC. Reachable via library.gpib_send_ifc(session); pyvisa's NIEnet100TCPIPIntfc is a bare Resource with no send_ifc convenience yet. Adds offline delegation unit tests and a hardware-assisted send_ifc test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reference gpib_constants.status, gpib_constants.timeout and
gpib_constants.error directly instead of the driver's own local copies
of the ibsta bits, NI-488.2 timeout indices and iberr codes. The unused
local iberr entries are dropped; the codes actually decoded all share
the same values as the shared module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bytewarrior bytewarrior force-pushed the feature/ni-gpib-enet100 branch from 7587c9c to fb1eee9 Compare July 5, 2026 10:46
bytewarrior and others added 3 commits July 5, 2026 18:18
The device read timeout is the discrete tmo_code carried in the open
Frame A. The box rejects the 'P 03' IbcTMO property while a bracket is
open (EARG), so a change is applied by closing the bracket, re-sending
Frame A with the new code for the same target address, and reopening the
bracket. The connection now remembers the target address and current
code so the reopen can rebuild Frame A.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_set_timeout now pushes the session timeout onto the box via the bracket
tmo_code (reopening the bracket when it changes) and sizes the socket
recv-timeout above the code's real duration (TIMETABLE). This replaces a
fixed +5s / min-8s ceiling whose reasoning no longer holds and which
could trip before the box's own timeout when the requested value rounds
up to a larger code (e.g. 5 s -> T10s). The INTFC session now honours the
session timeout attribute for its operational socket rather than the
connect timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
viWrite is not a buffer-management operation: the intermediate read
buffer that lets a response be read one byte at a time must persist
across writes, matching the other message-based backends such as the
TCPIP socket session. Only viClear/viFlush discard it. Leaving a
response unread and issuing a new write is a device-side message-
recovery concern (the instrument raises a query error); the session
should not paper over it here.

Update the assisted test to verify the buffered tail is dropped via
clear() (the supported discard path) rather than at write time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@MatthieuDartiailh MatthieuDartiailh left a comment

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 small comments but we are getting there. Regarding logging I think we can just drop the conditional. We can revisit later if it proves to be a problem.

Comment on lines +18 to +27
# The session module raises ImportError on load when the upstream pyvisa
# additions it depends on (InterfaceType.ni_enet100_tcpip) are missing; skip
# the whole module cleanly in that case, mirroring the assisted suite.
try:
from pyvisa_py import gpib_constants, nienet100 as ni
from pyvisa_py.protocols import nienet100 as proto
except ImportError as _import_err: # pragma: no cover - depends on pyvisa version
pytestmark = pytest.mark.skip(
reason="pyvisa-py NI GPIB-ENET/100 session layer unavailable: %s" % _import_err
)

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.

Same as in other places this will have to be cleaned up.

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. pyvisa 1.17.0 (not yet confirmed, but assumed in the pyvisa repo) is now a dependency. Guard code has been removed. See commit 1aaf956.

Comment thread pyvisa_py/testsuite/test_nienet100.py Outdated
Comment on lines +30 to +31
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
assert len(frame) == nienet100.COMMAND_FRAME_SIZE

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.

Suggested change
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
assert len(frame) == nienet100.COMMAND_FRAME_SIZE
assert len(frame) == nienet100.COMMAND_FRAME_SIZE
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

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.

Applied.

Comment thread pyvisa_py/testsuite/test_nienet100.py Outdated
Comment on lines +30 to +31
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
assert len(frame) == nienet100.COMMAND_FRAME_SIZE

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.

Suggested change
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
assert len(frame) == nienet100.COMMAND_FRAME_SIZE
assert len(frame) == nienet100.COMMAND_FRAME_SIZE
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

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.

See above (duplicate?)

Comment thread pyvisa_py/testsuite/test_nienet100.py Outdated
Comment on lines +30 to +31
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
assert len(frame) == nienet100.COMMAND_FRAME_SIZE

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.

Suggested change
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
assert len(frame) == nienet100.COMMAND_FRAME_SIZE
assert len(frame) == nienet100.COMMAND_FRAME_SIZE
assert frame == b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

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.

See above (duplicate?)

Comment on lines +31 to +40
# Skip the entire module if the upstream pyvisa changes that NIENET100
# depends on (InterfaceType.ni_enet100_tcpip + rname.NIEnet100TCPIPIntfc)
# are not in place. The pyvisa_py.nienet100 module raises ImportError on
# load when they are missing.
try:
from pyvisa_py import nienet100 as _ni
except ImportError as _import_err:
pytestmark = pytest.mark.skip(
reason="pyvisa-py NI GPIB-ENET/100 session layer unavailable: %s" % _import_err
)

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.

Same as in other places

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, see above. Commit: 1aaf956

bytewarrior and others added 4 commits July 6, 2026 19:25
pyvisa 1.17.0 ships InterfaceType.ni_enet100_tcpip and rname.NIEnet100TCPIPIntfc, so the development-period guards are no longer needed. Bump the dependency and remove the runtime AttributeError guard in nienet100.py (the session now registers directly against constants.InterfaceType.ni_enet100_tcpip), plus the ImportError skip blocks in the offline and assisted session tests. The imports are now unconditional.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ruff isort groups pyvisa and pyvisa_py together as first-party; the removed skip blocks had left a stray blank line splitting that group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants