Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolves #64 Feature suggestion: Connecting instruments #65

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions pyvisa_sim/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __init__(self, device: "Device", ids: List[str], can_select: bool):
self._getters = ChDict(__default__={})
self._dialogues = ChDict(__default__={})

def add_dialogue(self, query: str, response: str) -> None:
def add_dialogue(self, query: str, response: str, sources: dict = {}) -> None:
"""Add dialogue to channel.

Parameters
Expand All @@ -91,7 +91,12 @@ def add_dialogue(self, query: str, response: str) -> None:
Response sent in response to a query.

"""
self._dialogues["__default__"][to_bytes(query)] = to_bytes(response)
if sources:
dialogue = {"func": response, "sources": (sources)}
self._dialogues["__default__"][to_bytes(query)] = dialogue

else:
self._dialogues["__default__"][to_bytes(query)] = to_bytes(response)

def add_property(
self,
Expand Down
42 changes: 39 additions & 3 deletions pyvisa_sim/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,9 @@ def __init__(self) -> None:
self._properties = {}
self._getters = {}
self._setters = []
self.devices = {}

def add_dialogue(self, query: str, response: str) -> None:
def add_dialogue(self, query: str, response: str, sources: dict = None) -> None:
"""Add dialogue to device.

Parameters
Expand All @@ -205,7 +206,12 @@ def add_dialogue(self, query: str, response: str) -> None:
Response to the dialog query.

"""
self._dialogues[to_bytes(query)] = to_bytes(response)
if sources:
dialogue = {"func": response, "sources": (sources)}
self._dialogues[to_bytes(query)] = dialogue

else:
self._dialogues[to_bytes(query)] = to_bytes(response)

def add_property(
self,
Expand Down Expand Up @@ -244,6 +250,13 @@ def add_property(
(name, stringparser.Parser(query), to_bytes(response_), to_bytes(error))
)

def set_devices(self, devices: dict) -> None:
""" "Add all initialized devices

:param devices: storage for devices
"""
self.devices = devices

def match(self, query: bytes) -> Optional[OptionalBytes]:
"""Try to find a match for a query in the instrument commands."""
raise NotImplementedError()
Expand Down Expand Up @@ -288,7 +301,30 @@ def _match_dialog(

# Try to match in the queries
if query in dialogues:
response = dialogues[query]
# if connection
if type(dialogues[query]) == dict:
dialogue = dialogues[query]
function = dialogue["func"]
sources = dialogue["sources"]
prop_dict = {}

for source in sources:
# Find source for connection
if source["name"] in self.devices._internal.keys():
device = self.devices._internal[source["name"]]
# Find correct property
property = device._properties[source["parameter"]]
# Add property and value to prop_dict
value = property._value
prop_dict[property.name] = value

# Populate and run function
func = function.format(**prop_dict)
response = to_bytes(str(eval(func)))

else:
response = dialogues[query]

logger.debug("Found response in queries: %s" % repr(response))

return response
Expand Down
138 changes: 138 additions & 0 deletions pyvisa_sim/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,140 @@ devices:
max: 6
type: float

device 5:
eom:
ASRL INSTR:
q: "\r\n"
r: "\n"
USB INSTR:
q: "\r\n"
r: "\n"
TCPIP INSTR:
q: "\r\n"
r: "\n"
TCPIP SOCKET:
q: "\r\n"
r: "\n"
GPIB INSTR:
q: "\r\n"
r: "\n"
error: ERROR
dialogues:
- q: "*IDN?"
r: "HEWLETT-PACKARD, E3632A,0,1.2-5.0-1.0"
- q: "SYST:ERR?"
r: "NO ERROR"
- q: "SOUR1:POW?"
r: "2"
- q: "READ1"
r: "0.23 * ({amplitude} ** 2) - {frequency}"
sources: [
{name: "GPIB0::2::INSTR", parameter: amplitude},
{name: "GPIB0::22::INSTR", parameter: frequency}
]
properties:
frequency:
default: 1.0
getter:
q: "SENS1:FREQ?"
r: "{:.2f}"
setter:
q: "SENS1:FREQ {:.2f} MHZ"
r: ""
e: "FREQ ERROR"
specs:
min: 0.0
max: 27.0
type: float
current limit:
default: 1.0
getter:
q: "MEAS:CURR?"
r: "{:.2f}"
setter:
q: "CURR:LIM {:}"
r: ""
e: 'CURRENT_ERROR'
specs:
min: 0.0
max: 7.0
type: float
voltage:
default: 1.0
getter:
q: "MEAS:VOLT?"
r: "{:.2f}"
setter:
q: "VOLT {:.2f}"
r: ""
specs:
min: 0
max: 30
type: float
output_enabled:
default: '0'
getter:
q: "OUTPUT:STATE?"
r: "{:s}"
setter:
q: "OUTPUT:STATE {:s}"
r: ""
specs:
valid: ['0', '1', 'ON', 'OFF']
type: str
device 6:
eom:
ASRL INSTR:
q: "\r\n"
r: "\n"
USB INSTR:
q: "\r\n"
r: "\n"
TCPIP INSTR:
q: "\r\n"
r: "\n"
TCPIP SOCKET:
q: "\r\n"
r: "\n"
GPIB INSTR:
q: "\r\n"
r: "\n"
error: ERROR
dialogues:
- q: "*IDN?"
r: "Hewlett-Packard, ESG-D4000B, GB40050924, B.03.86"
- q: "*OPC?"
r: "Hewlett-Packard, ESG-D4000B, GB40050924, B.03.86"
- q: "SYST:ERR?"
r: "NO ERROR"
properties:
frequency:
default: 1.0
getter:
q: "FREQ?"
r: "{:.2f}"
setter:
q: "FREQ {:.2f}MHZ"
r: ""
e: "FREQ ERROR"
specs:
min: 0.0
max: 6.0
type: float
amplitude:
default: 0.0
getter:
q: "POW:LEV?"
r: "{:.2f}"
setter:
q: "POW:LEV {:.2f}"
r: ""
e: "POW ERROR"
specs:
min: -100.0
max: 20.0
type: float

resources:
ASRL1::INSTR:
device: device 1
Expand Down Expand Up @@ -279,3 +413,7 @@ resources:
device: device 4
USB::0x1111::0x2222::0x4445::RAW:
device: device 1
GPIB0::22::INSTR:
device: device 5
GPIB0::2::INSTR:
device: device 6
40 changes: 34 additions & 6 deletions pyvisa_sim/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ def __getitem__(self, key: K) -> V:
raise KeyError(key)


def _get_dialogue(dd: Dict[str, str]) -> Tuple[str, str, Dict[str, str]]:
"""Return a dialogue from a dialogue dictionary.

:param dd: Dialogue dictionary.
:type dd: Dict[str, str] or Dict[str, str, str]
:return: (query, response, sources)
:rtype: (str, str, str)
"""
if "sources" in dd.keys():
sources = dd["sources"]
else:
sources = None

return dd["q"].strip(" "), dd["r"].strip(" "), sources


def _get_pair(dd: Dict[str, str]) -> Tuple[str, str]:
"""Return a pair from a dialogue dictionary."""
return dd["q"].strip(" "), dd["r"].strip(" ")
Expand Down Expand Up @@ -122,12 +138,15 @@ def parse_file(fullpath: Union[str, pathlib.Path]) -> Dict[str, Any]:


def update_component(
name: str, comp: Component, component_dict: Dict[str, Any]
name: str,
comp: Component,
component_dict: Dict[str, Any],
devices: Dict[str, Device],
) -> None:
"""Get a component from a component dict."""
for dia in component_dict.get("dialogues", ()):
try:
comp.add_dialogue(*_get_pair(dia))
comp.add_dialogue(*_get_dialogue(dia))
except Exception as e:
msg = "In device %s, malformed dialogue %s\n%r"
raise Exception(msg % (name, dia, e))
Expand All @@ -149,6 +168,12 @@ def update_component(
msg = "In device %s, malformed property %s\n%r"
raise type(e)(msg % (name, prop_name, format_exc()))

try:
comp.set_devices(devices)
except Exception as e:
msg = "In device %s, malformed devices %s\n%r"
raise Exception(msg % (name, devices, e))


def get_bases(definition_dict: Dict[str, Any], loader: "Loader") -> Dict[str, Any]:
"""Collect inherited behaviors."""
Expand All @@ -171,6 +196,7 @@ def get_channel(
channel_dict: Dict[str, Any],
loader: "Loader",
resource_dict: Dict[str, Any],
devices: Dict[str, Device],
) -> Channels:
"""Get a channels from a channels dictionary.

Expand Down Expand Up @@ -201,7 +227,7 @@ def get_channel(
can_select = False if channel_dict.get("can_select") == "False" else True
channels = Channels(device, ids, can_select)

update_component(ch_name, channels, cd)
update_component(ch_name, channels, cd, devices)

return channels

Expand All @@ -211,6 +237,7 @@ def get_device(
device_dict: Dict[str, Any],
loader: "Loader",
resource_dict: Dict[str, str],
devices: Dict[str, Device],
) -> Device:
"""Get a device from a device dictionary.

Expand Down Expand Up @@ -241,11 +268,12 @@ def get_device(
for itype, eom_dict in device_dict.get("eom", {}).items():
device.add_eom(itype, *_get_pair(eom_dict))

update_component(name, device, device_dict)
update_component(name, device, device_dict, devices)

for ch_name, ch_dict in device_dict.get("channels", {}).items():
device.add_channels(
ch_name, get_channel(device, ch_name, ch_dict, loader, resource_dict)
ch_name,
get_channel(device, ch_name, ch_dict, loader, resource_dict, devices),
)

return device
Expand Down Expand Up @@ -410,7 +438,7 @@ def get_devices(filename: Union[str, pathlib.Path], bundled: bool) -> Devices:
)

devices.add_device(
resource_name, get_device(device_name, dd, loader, resource_dict)
resource_name, get_device(device_name, dd, loader, resource_dict, devices)
)

return devices
13 changes: 13 additions & 0 deletions pyvisa_sim/testsuite/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ def test_list(resource_manager):
"USB0::0x1111::0x2222::0x3692::0::INSTR",
"USB0::0x1111::0x2222::0x4444::0::INSTR",
"USB0::0x1111::0x2222::0x4445::0::RAW",
"GPIB0::2::INSTR",
"GPIB0::4::INSTR",
"GPIB0::8::INSTR",
"GPIB0::9::INSTR",
"GPIB0::10::INSTR",
"GPIB0::22::INSTR",
}


Expand Down Expand Up @@ -179,3 +181,14 @@ def test_instrument_for_error_state(resource, resource_manager):

inst.write(":VOLT:IMM:AMPL 0")
assert_instrument_response(inst, ":SYST:ERR?", "1, Command error")


def test_dialogue_connection(resource_manager):
inst1 = resource_manager.open_resource("GPIB0::22::INSTR", read_termination="\n")
inst2 = resource_manager.open_resource("GPIB0::2::INSTR", read_termination="\n")
inst2.query("POW:LEV 8.5")
response = inst1.query("READ1")
assert response == "15.6175"
inst2.query("POW:LEV 1.0")
response = inst1.query("READ1")
assert response == "-0.77"