forked from M-Labs/artiq
waveform: add WaveformDock
This commit is contained in:
parent
ba04b69aaa
commit
4800c0e775
|
@ -1,14 +1,18 @@
|
||||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from sipyco.sync_struct import Subscriber
|
||||||
from sipyco.pc_rpc import AsyncioClient
|
from sipyco.pc_rpc import AsyncioClient
|
||||||
|
from sipyco import pyon
|
||||||
|
|
||||||
from artiq.tools import exc_to_warning
|
from artiq.tools import exc_to_warning
|
||||||
|
from artiq.gui.tools import LayoutWidget, get_open_file_name, get_save_file_name
|
||||||
from artiq.gui.models import DictSyncTreeSepModel, LocalModelManager
|
from artiq.gui.models import DictSyncTreeSepModel, LocalModelManager
|
||||||
from artiq.gui.dndwidgets import DragDropSplitter, VDragScrollArea
|
from artiq.gui.dndwidgets import DragDropSplitter, VDragScrollArea
|
||||||
from artiq.coredevice import comm_analyzer
|
from artiq.coredevice import comm_analyzer
|
||||||
from artiq.coredevice.comm_analyzer import WaveformType
|
from artiq.coredevice.comm_analyzer import WaveformType
|
||||||
|
|
||||||
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import itertools
|
import itertools
|
||||||
import bisect
|
import bisect
|
||||||
|
@ -16,6 +20,7 @@ import pyqtgraph as pg
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import struct
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -653,3 +658,240 @@ class _CursorTimeControl(QtWidgets.QLineEdit):
|
||||||
self.submit.emit(self._value)
|
self.submit.emit(self._value)
|
||||||
self.display_value(self._value)
|
self.display_value(self._value)
|
||||||
self.clearFocus()
|
self.clearFocus()
|
||||||
|
|
||||||
|
|
||||||
|
class WaveformDock(QtWidgets.QDockWidget):
|
||||||
|
traceDataChanged = QtCore.pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, loop=None):
|
||||||
|
QtWidgets.QDockWidget.__init__(self, "Waveform")
|
||||||
|
self.setObjectName("Waveform")
|
||||||
|
self.setFeatures(
|
||||||
|
QtWidgets.QDockWidget.DockWidgetMovable | QtWidgets.QDockWidget.DockWidgetFloatable)
|
||||||
|
|
||||||
|
self._channels_mgr = LocalModelManager(Model)
|
||||||
|
self._channels_mgr.init({})
|
||||||
|
|
||||||
|
self._devices = None
|
||||||
|
self._dump = None
|
||||||
|
|
||||||
|
self._state = {
|
||||||
|
"timescale": 1,
|
||||||
|
"stopped_x": None,
|
||||||
|
"logs": dict(),
|
||||||
|
"data": dict(),
|
||||||
|
}
|
||||||
|
|
||||||
|
self._current_dir = "c://"
|
||||||
|
|
||||||
|
self.proxy_client = WaveformProxyClient(self._state, loop)
|
||||||
|
devices_sub = Subscriber("devices", self.init_ddb, self.update_ddb)
|
||||||
|
|
||||||
|
proxy_receiver = comm_analyzer.AnalyzerProxyReceiver(
|
||||||
|
self.on_dump_receive)
|
||||||
|
self.proxy_client.devices_sub = devices_sub
|
||||||
|
self.proxy_client.proxy_receiver = proxy_receiver
|
||||||
|
|
||||||
|
grid = LayoutWidget()
|
||||||
|
self.setWidget(grid)
|
||||||
|
|
||||||
|
self._menu_btn = QtWidgets.QPushButton()
|
||||||
|
self._menu_btn.setIcon(
|
||||||
|
QtWidgets.QApplication.style().standardIcon(
|
||||||
|
QtWidgets.QStyle.SP_FileDialogStart))
|
||||||
|
grid.addWidget(self._menu_btn, 0, 0)
|
||||||
|
|
||||||
|
self._request_dump_btn = QtWidgets.QToolButton()
|
||||||
|
self._request_dump_btn.setToolTip("Trigger proxy")
|
||||||
|
self._request_dump_btn.setIcon(
|
||||||
|
QtWidgets.QApplication.style().standardIcon(
|
||||||
|
QtWidgets.QStyle.SP_BrowserReload))
|
||||||
|
grid.addWidget(self._request_dump_btn, 0, 1)
|
||||||
|
self._request_dump_btn.clicked.connect(
|
||||||
|
lambda: asyncio.ensure_future(self.proxy_client.trigger_proxy_task()))
|
||||||
|
|
||||||
|
self._waveform_area = WaveformArea(self, self._state,
|
||||||
|
self._channels_mgr)
|
||||||
|
self.traceDataChanged.connect(self._waveform_area.on_trace_update)
|
||||||
|
self.traceDataChanged.connect(self._update_log_channels)
|
||||||
|
grid.addWidget(self._waveform_area, 2, 0, colspan=12)
|
||||||
|
|
||||||
|
self._add_btn = QtWidgets.QToolButton()
|
||||||
|
self._add_btn.setToolTip("Add channels...")
|
||||||
|
self._add_btn.setIcon(
|
||||||
|
QtWidgets.QApplication.style().standardIcon(
|
||||||
|
QtWidgets.QStyle.SP_FileDialogListView))
|
||||||
|
grid.addWidget(self._add_btn, 0, 2)
|
||||||
|
self._add_btn.clicked.connect(self._waveform_area.on_add_channel_click)
|
||||||
|
|
||||||
|
self._cursor_control = _CursorTimeControl(parent=self, state=self._state)
|
||||||
|
grid.addWidget(self._cursor_control, 0, 3, colspan=3)
|
||||||
|
self._cursor_control.submit.connect(
|
||||||
|
self._waveform_area.on_cursor_move)
|
||||||
|
self._waveform_area.cursorMoved.connect(self._cursor_control.display_value)
|
||||||
|
|
||||||
|
self._file_menu = QtWidgets.QMenu()
|
||||||
|
self._add_async_action("Open trace...", self.load_trace)
|
||||||
|
self._add_async_action("Save trace...", self.save_trace)
|
||||||
|
self._add_async_action("Save VCD...", self.save_vcd)
|
||||||
|
self._add_async_action("Open list of channels...", self.load_channels)
|
||||||
|
self._add_async_action("Save list of channels...", self.save_channels)
|
||||||
|
self._menu_btn.setMenu(self._file_menu)
|
||||||
|
|
||||||
|
def _add_async_action(self, label, coro):
|
||||||
|
action = QtWidgets.QAction(label, self)
|
||||||
|
action.triggered.connect(
|
||||||
|
lambda: asyncio.ensure_future(exc_to_warning(coro())))
|
||||||
|
self._file_menu.addAction(action)
|
||||||
|
|
||||||
|
def _update_log_channels(self):
|
||||||
|
for log in self._state['logs']:
|
||||||
|
self._channels_mgr.update({
|
||||||
|
"action": "setitem",
|
||||||
|
"path": "",
|
||||||
|
"key": log,
|
||||||
|
"value": (0, "log")
|
||||||
|
})
|
||||||
|
|
||||||
|
def on_dump_receive(self, *args):
|
||||||
|
header = comm_analyzer.decode_header_from_receiver(*args)
|
||||||
|
decoded_dump = comm_analyzer.decode_dump_loop(*header)
|
||||||
|
ddb = self._ddb
|
||||||
|
trace = comm_analyzer.decoded_dump_to_dict(ddb, decoded_dump)
|
||||||
|
self._state.update(trace)
|
||||||
|
self._dump = args
|
||||||
|
self.traceDataChanged.emit()
|
||||||
|
|
||||||
|
def on_dump_read(self, dump):
|
||||||
|
endian_byte = dump[0]
|
||||||
|
if endian_byte == ord("E"):
|
||||||
|
endian = '>'
|
||||||
|
elif endian_byte == ord("e"):
|
||||||
|
endian = '<'
|
||||||
|
else:
|
||||||
|
logger.warning("first byte is not endian")
|
||||||
|
raise ValueError
|
||||||
|
payload_length_word = dump[1:5]
|
||||||
|
payload_length = struct.unpack(endian + "I", payload_length_word)[0]
|
||||||
|
data = dump[5:]
|
||||||
|
self.on_dump_receive(endian, payload_length, data)
|
||||||
|
|
||||||
|
def _decode_dump(self):
|
||||||
|
dump = self._dump
|
||||||
|
header = comm_analyzer.decode_header_from_receiver(*dump)
|
||||||
|
return comm_analyzer.decode_dump_loop(*header)
|
||||||
|
|
||||||
|
def _dump_header(self, endian, payload_length):
|
||||||
|
payload_length_word = struct.pack(endian + "I", payload_length)
|
||||||
|
if endian == ">":
|
||||||
|
endian_byte = b"E"
|
||||||
|
else:
|
||||||
|
endian_byte = b"e"
|
||||||
|
return endian_byte + payload_length_word
|
||||||
|
|
||||||
|
async def load_trace(self):
|
||||||
|
try:
|
||||||
|
filename = await get_open_file_name(
|
||||||
|
self,
|
||||||
|
"Load Analyzer Trace",
|
||||||
|
self._current_dir,
|
||||||
|
"All files (*.*)")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
self._current_dir = os.path.dirname(filename)
|
||||||
|
try:
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
dump = f.read()
|
||||||
|
self.on_dump_read(dump)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to open analyzer trace: %s", e)
|
||||||
|
|
||||||
|
async def save_trace(self):
|
||||||
|
dump = self._dump
|
||||||
|
try:
|
||||||
|
filename = await get_save_file_name(
|
||||||
|
self,
|
||||||
|
"Save Analyzer Trace",
|
||||||
|
self._current_dir,
|
||||||
|
"All files (*.*)")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
self._current_dir = os.path.dirname(filename)
|
||||||
|
try:
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(self._dump_header(dump[0], dump[1]))
|
||||||
|
f.write(dump[2])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to save analyzer trace: %s", e)
|
||||||
|
|
||||||
|
async def save_vcd(self):
|
||||||
|
ddb = self._ddb
|
||||||
|
dump = self._dump
|
||||||
|
try:
|
||||||
|
filename = await get_save_file_name(
|
||||||
|
self,
|
||||||
|
"Save VCD",
|
||||||
|
self._current_dir,
|
||||||
|
"All files (*.*)")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
self._current_dir = os.path.dirname(filename)
|
||||||
|
try:
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
decoded_dump = comm_analyzer.decode_dump(dump)
|
||||||
|
comm_analyzer.decoded_dump_to_vcd(f, ddb, decoded_dump)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to save as VCD: %s", e)
|
||||||
|
finally:
|
||||||
|
logger.info("Finished writing to VCD.")
|
||||||
|
|
||||||
|
async def load_channels(self):
|
||||||
|
try:
|
||||||
|
filename = await get_open_file_name(
|
||||||
|
self,
|
||||||
|
"Open List of Channels",
|
||||||
|
self._current_dir,
|
||||||
|
"All files (*.*)")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
self._current_dir = os.path.dirname(filename)
|
||||||
|
try:
|
||||||
|
channel_list = pyon.load_file(filename)
|
||||||
|
self._waveform_area.clear_channels()
|
||||||
|
self._waveform_area.update_channels(channel_list)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to open list of channels: %s", e)
|
||||||
|
|
||||||
|
async def save_channels(self):
|
||||||
|
try:
|
||||||
|
filename = await get_save_file_name(
|
||||||
|
self,
|
||||||
|
"Load Analyzer Trace",
|
||||||
|
self._current_dir,
|
||||||
|
"All files (*.*)")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
self._current_dir = os.path.dirname(filename)
|
||||||
|
try:
|
||||||
|
obj = self._waveform_area.get_channels()
|
||||||
|
pyon.store_file(filename, obj)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to open analyzer trace: %s", e)
|
||||||
|
|
||||||
|
# DeviceDB subscriber callbacks
|
||||||
|
def init_ddb(self, ddb):
|
||||||
|
self._ddb = ddb
|
||||||
|
|
||||||
|
def update_ddb(self, mod):
|
||||||
|
devices = self._ddb
|
||||||
|
addr = None
|
||||||
|
self._channels_mgr.init(comm_analyzer.get_channel_list(devices))
|
||||||
|
for name, desc in devices.items():
|
||||||
|
if isinstance(desc, dict):
|
||||||
|
if desc["type"] == "controller" and name == "core_analyzer":
|
||||||
|
addr = desc["host"]
|
||||||
|
port = desc.get("port_proxy", 1385)
|
||||||
|
port_control = desc.get("port_proxy_control", 1386)
|
||||||
|
if addr is not None:
|
||||||
|
self.proxy_client.update_address(addr, port, port_control)
|
||||||
|
|
Loading…
Reference in New Issue