Skip to content

Commit 703f9ae

Browse files
authored
Merge pull request #158 from ni/users/semoore/get-switch-sessions
2 parents bfa3e7e + fcd8409 commit 703f9ae

File tree

10 files changed

+289
-14
lines changed

10 files changed

+289
-14
lines changed

STATUS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@
7070
| Custom | SetCustomSession | set\_custom\_session | test\_custom\_instruments |
7171
| Custom | GetCustomSession(s) | pins\_to\_custom\_session(s) | test\_custom\_instruments |
7272
| Custom | GetAllCustomSessions | get\_all\_custom\_sessions | test\_custom\_instruments |
73-
| Multiplexer | GetSwitchNames | | |
74-
| Multiplexer | SetSwitchSession | | |
75-
| Multiplexer | GetSwitchSession(s) | | |
76-
| Multiplexer | GetAllSwitchSessions | | |
73+
| Multiplexer | GetSwitchNames | get\_all\_switch\_names | test\_switch |
74+
| Multiplexer | SetSwitchSession | set\_switch\_session | test\_switch |
75+
| Multiplexer | GetSwitchSession(s) | pin\_to\_switch\_sessions | test\_switch |
76+
| Multiplexer | GetAllSwitchSessions | get\_all\_switch\_sessions | test\_switch |
7777

7878
## Pin Query API
7979
| Class | .NET Method | Python | Python System Tests |

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ tox
1010
pytest
1111
pytest-cov
1212
black
13+
ni-python-styleguide

src/nitsm/tsmcontext.py

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""TSM Context Wrapper"""
2-
2+
import ctypes.wintypes
33
import time
44
import typing
55

66
import nitsm._pinmapinterfaces
77
import nitsm.enums
88
import nitsm.pinquerycontexts
99
import pythoncom
10+
import win32com.client
1011

1112
__all__ = ["SemiconductorModuleContext"]
1213

@@ -18,7 +19,6 @@
1819
import nifgen
1920
import niscope
2021
import niswitch
21-
import win32com.client.dynamic
2222

2323
_Any = typing.Any
2424
_Tuple = typing.Tuple
@@ -32,6 +32,7 @@
3232
_CapabilityArg = _Union[nitsm.enums.Capability, str]
3333
_PinsArg = _Union[str, _Sequence[str]] # argument that accepts 1 or more pins
3434
_StringTuple = _Tuple[str, ...]
35+
_AnyTuple = _Tuple[_Any, ...]
3536

3637
_NIDigitalSingleSessionPpmuQuery = _Tuple[_PinQueryContext, nidigital.Session, str]
3738
_NIDigitalMultipleSessionPpmuQuery = _Tuple[
@@ -69,6 +70,8 @@
6970
_PinQueryContext, _Tuple[niscope.Session, ...], _StringTuple
7071
]
7172

73+
_SwitchQuery = _Tuple[_Tuple["SemiconductorModuleContext", ...], _AnyTuple, _StringTuple]
74+
7275
_RelayDriverSingleSessionQuery = _Tuple[niswitch.Session, str]
7376
_RelayDriverMultipleSessionQuery = _Tuple[_Tuple[niswitch.Session, ...], _StringTuple]
7477

@@ -93,16 +96,15 @@ class SemiconductorModuleContext:
9396

9497
_sessions = {}
9598

96-
def __init__(self, tsm_com_obj: "_ISemiconductorModuleContext"):
99+
def __init__(self, tsm_dispatch: "_ISemiconductorModuleContext"):
97100
"""Wraps an instance of ISemiconductorModuleContext.
98101
99102
Args:
100-
tsm_com_obj: The win32com.client.dynamic.CDispatch object provided by TestStand.
103+
tsm_dispatch: The win32com.client.dynamic.CDispatch object provided by TestStand.
101104
"""
102-
self._context = nitsm._pinmapinterfaces.ISemiconductorModuleContext(tsm_com_obj)
103-
self._context._oleobj_ = tsm_com_obj._oleobj_.QueryInterface(
104-
self._context.CLSID, pythoncom.IID_IDispatch
105-
)
105+
clsid = nitsm._pinmapinterfaces.ISemiconductorModuleContext.CLSID
106+
interface = tsm_dispatch._oleobj_.QueryInterface(clsid, pythoncom.IID_IDispatch)
107+
self._context = nitsm._pinmapinterfaces.ISemiconductorModuleContext(interface)
106108

107109
# General and Advanced
108110

@@ -910,6 +912,116 @@ def pins_to_niscope_sessions(self, pins: "_PinsArg") -> "_NIScopeMultipleSession
910912
)
911913
return pin_query_context, sessions, channel_lists
912914

915+
# Switching
916+
917+
def get_all_switch_names(self, multiplexer_type_id: "_InstrTypeIdArg") -> "_StringTuple":
918+
"""Returns the names of all switches of the type specified by the multiplexer_type_id in the
919+
Semiconductor Module context. You can use switch names to open driver sessions.
920+
921+
Args:
922+
multiplexer_type_id: Specifies the type ID for the multiplexer in the pin map file. When
923+
you add a multiplexer to the pin map file, you can define a type ID for the
924+
multiplexer, such as the driver name. Multiplexers in the pin map that do not
925+
specify a type ID have a default ID of
926+
nitsm.enums.InstrumentTypeIdConstants.NI_GENERIC_MULTIPLEXER.
927+
"""
928+
if isinstance(multiplexer_type_id, nitsm.enums.InstrumentTypeIdConstants):
929+
multiplexer_type_id = multiplexer_type_id.value
930+
return self._context.GetSwitchNames(multiplexer_type_id)
931+
932+
def set_switch_session(
933+
self, switch_name: str, session_data: "_Any", multiplexer_type_id: "_InstrTypeIdArg"
934+
) -> None:
935+
"""Associates an open switch session with the switch_name for a multiplexer of type
936+
multiplexer_type_id.
937+
938+
Args:
939+
switch_name: The instrument name in the pin map file for the corresponding session_data.
940+
session_data: The instrument session for the corresponding switch_name and
941+
multiplexer_type_id.
942+
multiplexer_type_id: Specifies the type ID for the multiplexer in the pin map file. When
943+
you add a multiplexer to the pin map file, you can define a type ID for the
944+
multiplexer, such as the driver name. Multiplexers in the pin map that do not
945+
specify a type ID have a default ID of
946+
nitsm.enums.InstrumentTypeIdConstants.NI_GENERIC_MULTIPLEXER.
947+
"""
948+
if isinstance(multiplexer_type_id, nitsm.enums.InstrumentTypeIdConstants):
949+
multiplexer_type_id = multiplexer_type_id.value
950+
session_id = id(session_data)
951+
self._sessions[session_id] = session_data
952+
return self._context.SetSwitchSession(multiplexer_type_id, switch_name, session_id)
953+
954+
def get_all_switch_sessions(self, multiplexer_type_id: "_InstrTypeIdArg") -> "_AnyTuple":
955+
"""Returns a tuple of all switch session data of the type specified by the
956+
multiplexer_type_id in the Semiconductor Module context.
957+
958+
Args:
959+
multiplexer_type_id: Specifies the type ID for the multiplexer in the pin map file. When
960+
you add a multiplexer to the pin map file, you can define a type ID for the
961+
multiplexer, such as the driver name. Multiplexers in the pin map that do not
962+
specify a type ID have a default ID of
963+
nitsm.enums.InstrumentTypeIdConstants.NI_GENERIC_MULTIPLEXER.
964+
"""
965+
if isinstance(multiplexer_type_id, nitsm.enums.InstrumentTypeIdConstants):
966+
multiplexer_type_id = multiplexer_type_id.value
967+
session_ids = self._context.GetSwitchSessions(multiplexer_type_id)
968+
return tuple(map(SemiconductorModuleContext._sessions.get, session_ids))
969+
970+
def pin_to_switch_sessions(
971+
self, pin: str, multiplexer_type_id: "_InstrTypeIdArg"
972+
) -> "_SwitchQuery":
973+
"""Returns the switch sessions, switch routes, and new Semiconductor Module context objects
974+
required to access the specified switched pin.
975+
976+
Args:
977+
pin: The name of the pin to translate to session data and switch routes.
978+
multiplexer_type_id: Specifies the type ID for the multiplexer in the pin map file. When
979+
you add a multiplexer to the pin map file, you can define a type ID for the
980+
multiplexer, such as the driver name. Multiplexers in the pin map that do not
981+
specify a type ID have a default ID of
982+
nitsm.enums.InstrumentTypeIdConstants.NI_GENERIC_MULTIPLEXER.
983+
984+
Returns:
985+
semiconductor_module_contexts: A tuple of Semiconductor Module context objects. Each
986+
element in the tuple represents a site that must be executed serially. Use each
987+
Semiconductor Module context object to query the pin map and publish data.
988+
session_data: A tuple of the session data required to access the switch that connects an
989+
instrument channel to the pin.
990+
switch_routes: The routes required to connect an instrument channel to the pin.
991+
"""
992+
if isinstance(multiplexer_type_id, nitsm.enums.InstrumentTypeIdConstants):
993+
multiplexer_type_id = multiplexer_type_id.value
994+
# We have to use DumbDispatch here because pywin32 fails to recognize
995+
# ISemiconductorModuleContext as deriving from IDispatch; most likely because it isn't
996+
# natively supported. So, we fetch it as IUnknown instead.
997+
vt_by_ref_array = pythoncom.VT_BYREF | pythoncom.VT_ARRAY
998+
site_contexts = win32com.client.VARIANT(vt_by_ref_array | pythoncom.VT_UNKNOWN, [])
999+
sessions = win32com.client.VARIANT(vt_by_ref_array | pythoncom.VT_VARIANT, [])
1000+
switch_routes = win32com.client.VARIANT(vt_by_ref_array | pythoncom.VT_BSTR, [])
1001+
dumb_context = win32com.client.dynamic.DumbDispatch(self._context)
1002+
dumb_context.GetSwitchSessions_2(
1003+
multiplexer_type_id, pin, site_contexts, sessions, switch_routes
1004+
)
1005+
# As of pywin32 303, there is a bug where SAFEARRAYs of IUnknown pointers leak references.
1006+
# See here: https://github.com/mhammond/pywin32/issues/1864
1007+
# As a work-around, we decrement the reference count until the only one left is held by
1008+
# Python. It will be released when the object is garbage collected.
1009+
add_ref = ctypes.WINFUNCTYPE(ctypes.wintypes.ULONG)(1, "AddRef")
1010+
release = ctypes.WINFUNCTYPE(ctypes.wintypes.ULONG)(2, "Release")
1011+
for site_context in site_contexts.value:
1012+
# address of IUnknown has to be parsed from the repr
1013+
# https://github.com/mhammond/pywin32/blob/main/com/win32com/src/PyIUnknown.cpp
1014+
address = ctypes.c_void_p(int(repr(site_context).split()[-1][:-1], 16))
1015+
# first add a reference in case the bug has been fixed; prevents count from reaching 0
1016+
add_ref(address)
1017+
# then release the reference until only one remains
1018+
while release(address) > 1:
1019+
pass
1020+
site_contexts = map(win32com.client.dynamic.DumbDispatch, site_contexts.value)
1021+
site_contexts = tuple(map(SemiconductorModuleContext, site_contexts))
1022+
sessions = tuple(map(SemiconductorModuleContext._sessions.get, sessions.value))
1023+
return site_contexts, sessions, switch_routes.value
1024+
9131025
# Relay Driver
9141026

9151027
def get_relay_driver_module_names(self) -> "_StringTuple":

systemtests/switch.pinmap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<PinMap schemaVersion="1.5" xmlns="http://www.ni.com/TestStand/SemiconductorModule/PinMap.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3+
<Instruments>
4+
<NIDigitalPatternInstrument name="DigitalPattern1" numberOfChannels="32" group="Digital" />
5+
<Multiplexer name="Multiplexer1" multiplexerTypeId="SimulatedMultiplexer" />
6+
</Instruments>
7+
<Pins>
8+
<DUTPin name="DUTPin1" />
9+
</Pins>
10+
<PinGroups></PinGroups>
11+
<Sites>
12+
<Site siteNumber="0" />
13+
<Site siteNumber="1" />
14+
</Sites>
15+
<Connections>
16+
<MultiplexedConnection instrument="DigitalPattern1" channel="0">
17+
<MultiplexedDUTPinRoute pin="DUTPin1" siteNumber="0" multiplexer="Multiplexer1" routeName="DUTPin1Site0" />
18+
<MultiplexedDUTPinRoute pin="DUTPin1" siteNumber="1" multiplexer="Multiplexer1" routeName="DUTPin1Site1" />
19+
</MultiplexedConnection>
20+
</Connections>
21+
</PinMap>

systemtests/switch.seq

12.4 KB
Binary file not shown.

systemtests/switch_codemodules.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import nitsm.codemoduleapi as tsm
2+
3+
MULTIPLEXER_TYPE_ID = "SimulatedMultiplexer"
4+
5+
6+
@tsm.code_module
7+
def open_sessions(tsm_context: tsm.SemiconductorModuleContext):
8+
switch_names = tsm_context.get_all_switch_names(MULTIPLEXER_TYPE_ID)
9+
for switch_name in switch_names:
10+
tsm_context.set_switch_session(switch_name, switch_name, MULTIPLEXER_TYPE_ID)
11+
# nidigital sessions are required to satisfy the pin map but won't be used
12+
instrument_names = tsm_context.get_all_nidigital_instrument_names()
13+
for instrument_name in instrument_names:
14+
tsm_context.set_nidigital_session(instrument_name, ...)
15+
16+
17+
@tsm.code_module
18+
def measure(
19+
tsm_context: tsm.SemiconductorModuleContext, pin, expected_switch_names, expected_switch_routes
20+
):
21+
site_contexts, switch_names, switch_routes = tsm_context.pin_to_switch_sessions(
22+
pin, MULTIPLEXER_TYPE_ID
23+
)
24+
expected_names_and_routes = set(zip(expected_switch_names, expected_switch_routes))
25+
valid_names_and_routes = []
26+
27+
for site_context, switch_name, switch_route in zip(site_contexts, switch_names, switch_routes):
28+
# check switch route we received is in the set of switch routes we expected
29+
actual_name_and_route = (switch_name, switch_route)
30+
valid_name_and_route = actual_name_and_route in expected_names_and_routes
31+
valid_names_and_routes.append(valid_name_and_route)
32+
expected_names_and_routes.discard(actual_name_and_route)
33+
# get a pin query context and publish result
34+
pin_query = site_context.pins_to_nidigital_session_for_ppmu(pin)[0]
35+
pin_query.publish(valid_name_and_route)
36+
37+
# publish missing switch routes for all sites
38+
pin_query = tsm_context.pins_to_nidigital_session_for_ppmu(pin)[0]
39+
num_missing_routes = [len(expected_names_and_routes)] * len(site_contexts)
40+
pin_query.publish(num_missing_routes, "NumMissing")
41+
42+
43+
@tsm.code_module
44+
def close_sessions(tsm_context: tsm.SemiconductorModuleContext):
45+
tsm_context.get_all_switch_sessions(MULTIPLEXER_TYPE_ID)

systemtests/test_system.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,8 @@ def test_site_and_global_data(system_test_runner):
5050
@pytest.mark.sequence_file("specifications.seq")
5151
def test_specifications(system_test_runner):
5252
assert system_test_runner.run()
53+
54+
55+
@pytest.mark.sequence_file("switch.seq")
56+
def test_switch(system_test_runner):
57+
assert system_test_runner.run()

tests/switch.pinmap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<PinMap schemaVersion="1.5" xmlns="http://www.ni.com/TestStand/SemiconductorModule/PinMap.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3+
<Instruments>
4+
<NIDigitalPatternInstrument name="DigitalPattern1" numberOfChannels="32" group="Digital" />
5+
<Multiplexer name="Multiplexer1" multiplexerTypeId="NIGenericMultiplexer" />
6+
</Instruments>
7+
<Pins>
8+
<DUTPin name="DUTPin1" />
9+
</Pins>
10+
<PinGroups></PinGroups>
11+
<Sites>
12+
<Site siteNumber="0" />
13+
<Site siteNumber="1" />
14+
</Sites>
15+
<Connections>
16+
<MultiplexedConnection instrument="DigitalPattern1" channel="0">
17+
<MultiplexedDUTPinRoute pin="DUTPin1" siteNumber="0" multiplexer="Multiplexer1" routeName="DUTPin1Site0" />
18+
<MultiplexedDUTPinRoute pin="DUTPin1" siteNumber="1" multiplexer="Multiplexer1" routeName="DUTPin1Site1" />
19+
</MultiplexedConnection>
20+
</Connections>
21+
</PinMap>

tests/test_switch.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import pytest
2+
import nitsm.codemoduleapi as tsm
3+
4+
5+
@pytest.fixture
6+
def simulated_switch_sessions(standalone_tsm_context):
7+
switch_names = standalone_tsm_context.get_all_switch_names(
8+
tsm.InstrumentTypeIdConstants.NI_GENERIC_MULTIPLEXER
9+
)
10+
for switch_name in switch_names:
11+
standalone_tsm_context.set_switch_session(
12+
switch_name, switch_name, tsm.InstrumentTypeIdConstants.NI_GENERIC_MULTIPLEXER
13+
)
14+
yield switch_names
15+
16+
17+
@pytest.mark.pin_map("switch.pinmap")
18+
@pytest.mark.parametrize(
19+
"multiplexer_type_id",
20+
["NIGenericMultiplexer", tsm.InstrumentTypeIdConstants.NI_GENERIC_MULTIPLEXER],
21+
)
22+
class TestSwitch:
23+
switches = ["Multiplexer1"]
24+
25+
def test_get_all_switch_names(self, standalone_tsm_context, multiplexer_type_id):
26+
switch_names = standalone_tsm_context.get_all_switch_names(multiplexer_type_id)
27+
assert isinstance(switch_names, tuple)
28+
assert len(switch_names) == len(self.switches)
29+
for switch_name in switch_names:
30+
assert isinstance(switch_name, str)
31+
assert switch_name in self.switches
32+
33+
def test_set_switch_session(self, standalone_tsm_context, multiplexer_type_id):
34+
switch_names = standalone_tsm_context.get_all_nidmm_instrument_names()
35+
for switch_name in switch_names:
36+
standalone_tsm_context.set_switch_session(switch_name, switch_name, multiplexer_type_id)
37+
assert tsm.SemiconductorModuleContext._sessions[id(switch_name)] is switch_name
38+
39+
def test_get_all_switch_sessions(
40+
self, standalone_tsm_context, simulated_switch_sessions, multiplexer_type_id
41+
):
42+
queried_sessions = standalone_tsm_context.get_all_switch_sessions(multiplexer_type_id)
43+
assert isinstance(queried_sessions, tuple)
44+
assert len(queried_sessions) == len(simulated_switch_sessions)
45+
for queried_session in queried_sessions:
46+
assert isinstance(queried_session, str)
47+
assert queried_session in simulated_switch_sessions
48+
49+
def test_pin_to_switch_sessions(
50+
self, standalone_tsm_context, simulated_switch_sessions, multiplexer_type_id
51+
):
52+
tsm_contexts, sessions, switch_routes = standalone_tsm_context.pin_to_switch_sessions(
53+
"DUTPin1", multiplexer_type_id
54+
)
55+
assert isinstance(tsm_contexts, tuple)
56+
assert isinstance(sessions, tuple)
57+
assert isinstance(switch_routes, tuple)
58+
assert len(tsm_contexts) == len(sessions)
59+
assert len(sessions) == len(switch_routes)
60+
for tsm_context, session, switch_route in zip(tsm_contexts, sessions, switch_routes):
61+
assert isinstance(tsm_context, tsm.SemiconductorModuleContext)
62+
assert len(tsm_context.site_numbers) == 1
63+
assert isinstance(session, str)
64+
assert session in simulated_switch_sessions
65+
assert isinstance(switch_route, str)
66+
assert switch_route == f"DUTPin1Site{tsm_context.site_numbers[0]}"

tools/makepy_pinmapinterfaces.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
"""Generates _pinmapinterfaces.py
2+
3+
You must register the correct version of TSM with TestStand Version Selector prior to running this
4+
script.
5+
"""
6+
17
import sys
28
import os.path
3-
import winreg
49
from win32com.client import makepy
510

6-
711
output_file = os.path.join(os.path.dirname(__file__), "_pinmapinterfaces.py")
812
teststand_public_path = os.environ["TestStandPublic64"]
913
pmi_type_library = os.path.join(

0 commit comments

Comments
 (0)