Skip to content

Keysight Oscilloscope Implementation with pyVISA #129

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

Open
wants to merge 21 commits into
base: master
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
703 changes: 703 additions & 0 deletions labscript_devices/KeysightScope/KeysightScope.py

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions labscript_devices/KeysightScope/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Keysight oscilloscope implementation

## Supported models
* Currently supported models: EDUX1052A, EDUX1052G, DSOX1202A, DSOX1202G, DSOX1204A, DSOX1204G

## Current possible utilization

### Triggering
* The oscilloscope is to be used in trigger mode (Single mode).
* Triggering must be performed via the external trigger input.
* Data is read from the channels currently displayed on the oscilloscope.

### Oscilloscope configuration
* You can configure the oscilloscope manually and then upload the configuration to labscript using the BLACS GUI interface, as shown in the following example:

![alt text](<Screenshot_BLACS_tab.png>)

* **1** By pressing `activate` on a spot index (in this example, Spot 0), the oscilloscope loads the configuration state saved in that spot, and the tab number lights up red.

* **2** Once a specific spot is activated,it becomes editable. You can either:
* Manually change the oscilloscope settings directly on the device, then click `load and save` in the BLACS tab to import and save the updated configuration for that spot.
* Or, click `reset to default` to load the default oscilloscope configuration.

* **3** This zone gives an overview of the most important setting parameters for the currently selected (green highleted, not necessarly activated) tab.


## Example Script

### In the Python connection table

```python
KeysightScope(
name="osci_keysight",
serial_number="CN61364200",
parent_device=osci_Trigger # parent_device must be a digital output initialized as Trigger(...)
)
```

### In the python experiment file
There are two main functions to use in the experiment scipt:

* `set_config( spot_index : int or string )` : The oscilloscope has ten different spots where it saves its global settings. set_config(spot_index) must be called at the beginning of the experiment script with the desired spot_index to initialize the oscilloscope with the corresponding configuration for the shot.


* `trigger_at( t=t, duration=trigger_duration )` : This function allows the parent device (of type Trigger()) to trigger the oscilloscope for the specified trigger_duration. During this short period, data will be read from the displayed channels.

```python
start()
t = 0

osci_keysight.set_config(3) # Must be called once at the start of each experiment shot

trigger_duration = 1e-4 # Example trigger duration
osci_keysight.trigger_at(t=t, duration=trigger_duration)

t += trigger_duration
stop(t)
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions labscript_devices/KeysightScope/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

from labscript import Device, LabscriptError, set_passed_properties ,LabscriptError, AnalogIn

class ScopeChannel(AnalogIn):
"""Subclass of labscript.AnalogIn that marks an acquiring scope channel.
"""
description = 'Scope Acquisition Channel Class'

def __init__(self, name, parent_device, connection):
"""This instantiates a scope channel to acquire during a buffered shot.

Args:
name (str): Name to assign channel
parent_device (obj): Handle to parent device
connection (str): Which physical scope channel is acquiring.
Generally of the form \'Channel n\' where n is
the channel label.
"""
Device.__init__(self,name,parent_device,connection)
self.acquisitions = []

def acquire(self):
"""Inform BLACS to save data from this channel.
Note that the parent_device controls when the acquisition trigger is sent.
"""
if self.acquisitions:
raise LabscriptError('Scope Channel {0:s}:{1:s} can only have one acquisition!'.format(self.parent_device.name,self.name))
else:
self.acquisitions.append({'label': self.name})
236 changes: 236 additions & 0 deletions labscript_devices/KeysightScope/blacs_tabs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
from blacs.device_base_class import DeviceTab
from labscript import LabscriptError
from blacs.tab_base_classes import define_state,Worker
from blacs.tab_base_classes import MODE_MANUAL, MODE_TRANSITION_TO_BUFFERED, MODE_TRANSITION_TO_MANUAL, MODE_BUFFERED
from blacs.device_base_class import DeviceTab

import os
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QSize
from PyQt5 import uic



class KeysightScopeTab(DeviceTab):
'''The device class handles the creation + interaction with the GUI ~ QueueManager'''

def initialise_workers(self):
# Here we can change the initialization properties in the connection table
worker_initialisation_kwargs = self.connection_table.find_by_name(self.device_name).properties

# Adding porperties as follows allows the blacs worker to access them
# This comes in handy for the device initialization
worker_initialisation_kwargs['address'] = self.BLACS_connection

# Create the device worker
self.create_worker(
'main_worker',
'labscript_devices.KeysightScope.blacs_workers.KeysightScopeWorker',
worker_initialisation_kwargs,
)
self.primary_worker = 'main_worker'


def initialise_GUI(self):

# The osci widget
self.osci_widget = OsciTab()
self.get_tab_layout().addWidget(self.osci_widget)

# Connect radio buttons (activate slot)
for i in range(10):
self.radio_button = self.osci_widget.findChild(QRadioButton, f"activeRadioButton_{i}")
self.radio_button.clicked.connect(lambda checked, i=i: self.activate_radio_button(i))

# Connect load buttons (load current)
self.load_button = self.osci_widget.findChild(QPushButton, f"loadButton_{i}")
self.load_button.clicked.connect(lambda clicked, i=i: self.load_current_config(i))

# Connect reset buttons (default buttons)
self.default_button = self.osci_widget.findChild(QPushButton, f"defaultButton_{i}")
self.default_button.clicked.connect(lambda clicked, i=i: self.default_config(i))

# Loads the Osci Configurations
self.init_osci()

return

@define_state(MODE_MANUAL|MODE_BUFFERED|MODE_TRANSITION_TO_BUFFERED|MODE_TRANSITION_TO_MANUAL,True,True)
def init_osci(self, widget=None ):
list_dict_config = yield(self.queue_work(self._primary_worker,'init_osci'))

for key,value in list_dict_config.items():
if value:
self.osci_widget.load_parameters(current_dict=value , table_index= key)
else:
self.default_config( button_id=key)

@define_state(MODE_MANUAL|MODE_BUFFERED|MODE_TRANSITION_TO_BUFFERED|MODE_TRANSITION_TO_MANUAL,True,True)
def activate_radio_button(self,buttton_id, widget=None ):
yield(self.queue_work(self._primary_worker,'activate_radio_button',buttton_id))

@define_state(MODE_MANUAL|MODE_BUFFERED|MODE_TRANSITION_TO_BUFFERED|MODE_TRANSITION_TO_MANUAL,True,True)
def load_current_config(self,button_id, widget=None ):
dict_config = yield(self.queue_work(self._primary_worker,'load_current_config',button_id))
self.osci_widget.load_parameters(current_dict=dict_config , table_index= button_id)

@define_state(MODE_MANUAL|MODE_BUFFERED|MODE_TRANSITION_TO_BUFFERED|MODE_TRANSITION_TO_MANUAL,True,True)
def default_config(self,button_id, widget=None ):
dict_config = yield(self.queue_work(self._primary_worker,'default_config',button_id))
self.osci_widget.load_parameters(current_dict=dict_config , table_index= button_id)


class TabTemplate(QWidget):
""" A Tab template class that defines the oscilloscope configuration in a table format,
designed to describe the most important settings for the ten available storage slots of the oscilloscope."""
def __init__(self,parent=None):
super().__init__(parent)
tab_template_name = 'tab_template.ui'
tab_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),tab_template_name)
uic.loadUi(tab_template_path,self)


class OsciTab(QWidget):
""" The oscilloscope Widget """
def __init__(self, parent=None):
super().__init__(parent)

tabs_name = 'tabs.ui'
tabs_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),tabs_name)
uic.loadUi(tabs_path,self)

# --- Children
self.tabWidget = self.findChild(QTabWidget,"tabWidget")
self.previous_checked_index = None

self.label_active_setup = self.findChild(QLabel,"label_active_setup")
self.button_group = QButtonGroup(self)

reset_icon = self.style().standardIcon(QStyle.SP_BrowserReload)
load_icon = self.style().standardIcon(QStyle.SP_DialogOpenButton)

for i in range(self.tabWidget.count()):

# --- Promote Tabs
self.tabWidget.removeTab(i)
self.tabWidget.insertTab(i, TabTemplate(), f"s{i}") # Add the new widget to the layout
tab = self.tabWidget.widget(i)

# --- LoadButtons
self.load_button = tab.findChild(QPushButton , "loadButton")
self.load_button.setObjectName(f"loadButton_{i}")
self.load_button.setIcon(load_icon)
self.load_button.setIconSize(QSize(16,16))
if i !=0:
self.load_button.setEnabled(False) # init condition

# --- resetButtons
self.default_button = tab.findChild(QPushButton , "defaultButton")
self.default_button.setObjectName(f"defaultButton_{i}")
self.default_button.setIcon(reset_icon)
self.default_button.setIconSize(QSize(16,16))
if i !=0:
self.default_button.setEnabled(False) # init condition


for i in range(self.tabWidget.count()):

tab = self.tabWidget.widget(i)

# --- RadioButtons
radio_button = tab.findChild(QRadioButton, "activeRadioButton")
radio_button.setObjectName(f"activeRadioButton_{i}")
self.button_group.addButton(radio_button)
self.button_group.setId(radio_button, i )
radio_button.toggled.connect(self.radio_toggled )

# --- TableWidgets
self.tableWidget = tab.findChild(QTableWidget, "tableWidget")
self.tableWidget.setObjectName(f"table_{i}")
self.tableWidget.setRowCount(30)
self.tableWidget.setColumnCount(2)
self.tableWidget.setHorizontalHeaderLabels(["Parameter", "Value"])
self.tableWidget.setEditTriggers(QTableWidget.NoEditTriggers)
header = self.tableWidget.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch) # Stretch all columns to fill the space

# --- Style Sheet
self.sh_tab = """
QTabBar::tab {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #ffffff, stop:1 #dddddd);
border: 1px solid #aaa;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
padding: 10px 20px;
color: #333;
font-weight: bold;
}
"""
self.sh_selected_tab = """
QTabBar::tab:selected {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #7BD77B, stop:1 #68C068);
color: white;
border-bottom: 2px solid #339933;
}
"""
self.tabWidget.setStyleSheet(self.sh_tab + self.sh_selected_tab)

# --- init
self.button_group.button(0).setChecked(True)
self.tabWidget.setCurrentIndex(0)

# --- Connecting the radio buttons
def radio_toggled (self):
selected_button = self.sender()

if self.previous_checked_index is not None:
self.tabWidget.setTabText(self.previous_checked_index, f"s{self.previous_checked_index}" )

tab = self.tabWidget.widget(self.previous_checked_index)
self.load_button = tab.findChild(QPushButton, f"loadButton_{self.previous_checked_index}")
self.default_button = tab.findChild(QPushButton , f"defaultButton_{self.previous_checked_index}")

if self.load_button:
self.load_button.setEnabled(False)

if self.default_button:
self.default_button.setEnabled(False)


if selected_button.isChecked():
index = self.button_group.id(selected_button)
self.label_active_setup.setText("Active setup : " + self.tabWidget.tabText(index) )
self.tabWidget.setTabText(index,f"🔴")

tab = self.tabWidget.widget(index)
self.load_button = tab.findChild(QPushButton,f"loadButton_{index}")
self.default_button = tab.findChild(QPushButton , f"defaultButton_{index}")

if self.load_button:
self.load_button.setEnabled(True)

if self.default_button:
self.default_button.setEnabled(True)

self.previous_checked_index = index


# --- Fill TableWidget
def load_parameters(self, current_dict , table_index):
for i, (key, value) in enumerate(current_dict.items()):
self.tableWidget= self.findChild(QTableWidget, f"table_{table_index}")
self.tableWidget.setItem(i, 0, QTableWidgetItem(str(key)))
self.tableWidget.setItem(i, 1, QTableWidgetItem(str(value)))


# ------------------------------------------------- Tests
if __name__ == "__main__":
app = QApplication(sys.argv)
window = OsciTab()
window.show()
sys.exit(app.exec())


Loading