Add DSLogic capture, signal conversion and DSView decoders

This commit is contained in:
2026-09-27 01:45:08 +03:00
parent cc22c803d1
commit 513e79b127
44 changed files with 5982 additions and 36 deletions

173
tools/dsview/README.md Normal file
View File

@@ -0,0 +1,173 @@
# DSView as an embedded Windows instrument panel
Upstream: https://github.com/DreamSourceLab/DSView, revision `2e9e2c8e`.
The GPL-3.0-or-later application and its private Qt 5/Python runtime stay in
a separate process. `setgui-host.patch` adds the Windows host protocol and
fixes the missing Windows CMake inputs. No DSView DLL is loaded into Qt 6.
`python/logic_analyzer/dsview_host.py` contains the reusable environment,
process/window ownership and close-message helpers. The consuming GUI owns
the stylesheet, window surface and lifecycle.
## Build
MSYS2 UCRT64 packages (install with `pacman -S --needed`):
```text
mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja mingw-w64-ucrt-x86_64-pkgconf
mingw-w64-ucrt-x86_64-qt5-base mingw-w64-ucrt-x86_64-qt5-svg
mingw-w64-ucrt-x86_64-qt5-winextras mingw-w64-ucrt-x86_64-glib2
mingw-w64-ucrt-x86_64-libusb mingw-w64-ucrt-x86_64-fftw
mingw-w64-ucrt-x86_64-boost mingw-w64-ucrt-x86_64-python
```
```powershell
python tools/dsview/build.py --source C:/path/to/DSView --output C:/path/to/runtime --msys-root C:/msys64
```
The builder checks the revision, applies the patch only after `git apply
--check`, and accepts an already applied patch. It never resets the source
checkout. Close the built DSView before rebuilding.
Successful builds write `host-build.json`, recording the recipe digest (patch,
packager, adapters and shared cores) and the native EXE digest. Consumers can
call `runtime_is_current(output)` before packaging to detect an old component
even if the host-protocol version is unchanged. SETGUI's normal EXE builder
automatically rebuilds an installed stale runtime and refuses to package it if
the update fails. A runtime built before this stamp was added is rebuilt once.
## Host protocol 1
- Create a native child surface owned by the host process.
- Start `DSView.exe --embed-parent <decimal HWND>` with its private runtime
as working directory and a sanitized environment.
- `DSVIEW_HOST_STYLE` points to the UTF-8 Qt stylesheet; `DSVIEW_CAPTURE_DIR`
supplies the initial open/save/export directory.
- Wait for `DSVIEW_READY <HWND>` on stdout, then validate both the window PID
and its parent before accepting it.
- The child fits the surface, inherits visibility, and exits if the parent
window disappears. It uses separate settings under `SET/SETGUI-DSView`.
- Write `CLOSE\n` to the child's stdin to close normally. Keep the host
alive while DSView's save dialog is open. Forced termination is only a
fallback for host shutdown or a failed start.
- Write `ADD_DECODER <id>\n` to open the normal decoder configuration dialog.
The host must be ready, in digital mode and not capturing. Stacked decoders
automatically include the matching UART/CAN base decoder.
- New hosts announce `DSVIEW_CAPABILITIES OPEN_FILE` after the ready message.
`OPEN_FILE <base64 UTF-8 path>\n` opens a DSL recording without restarting
the child. It returns `DSVIEW_FILE_RESULT ok|busy|save|error`. A capture,
save operation or modal dialog prevents replacement. Unsaved hardware data
retains the normal save prompt.
- In embedded mode File в†’ Open accepts CSV and emits
`DSVIEW_IMPORT_CSV <base64 UTF-8 path>` for conversion by SETGUI. The shared
`python/logic_analyzer/csv_import.py` streams CSV into temporary DSL v3
blocks in two passes, with progress/cancellation and bounded memory.
Times are read as decimal picoseconds; front-to-front intervals are exact.
The initial state may extend by less than one sample to align the grid;
the final state pads to 64 samples. Original offset metadata is retained.
- Embedded mode disables Qt's quit-on-last-window policy: the child is a
`Qt::Tool`, so accepting/rejecting a modal dialog must not quit the process.
- `--host-version` returns `DSView host protocol 1`. An optional
`DSVIEW_HOST_SCREENSHOT` environment variable saves one render artifact
after startup for native integration tests.
Tested SDK: GCC 16.2, Qt 5.15.19, Python 3.14.7, Windows x64. This runtime
requires Windows 10/11; it does not add Windows 7 support.
## SET decoders
`decoders/` contains the sigrok front ends for `gate_driver_timing` (1SP0635 /
1SD536F2), `set_uart` (SET v1/v2, ProtoCAN bridge), `set_can` (ProtoCAN, SET v2,
Balsam), and `pm35_uart` (TMS320F28335 MODBUS 03/06).
Their parsing cores are maintained only in `python/logic_analyzer/decoders`.
The builder copies those files verbatim to `decoders/common/setgui_decoders`
inside the runtime. No GUI imports or external Python installation are needed.
The adapters originate from `DSLogic_Logic_2/dslogic_script/dsview_decoders`.
IGBT offers `ORPHAN minimum pulse width (ns, 0 = off)` for all profiles.
Only uncorrelated pulses strictly shorter than this duration are omitted from
annotations; zero preserves the original behavior. ACK measurements/errors,
FAULT and lone edges with unknown pulse duration remain visible. This does
not modify the captured signal or debounce Vin1/Vstat.
The independent **Transistor pair** decoder measures mintime, OFF time,
deadtime in both directions, overlap, frequency and duty. It requires only
Vin1/Vin2. **IGBT 1SP/1SD** retains Vin1/Vstat ACK and full-cycle checks.
See [pair settings](decoders/transistor_pair/README.md) and
[driver settings](decoders/gate_driver_timing/README.md).
Run `python -m unittest discover -s tools/dsview/tests` for packet, CRC,
segmentation, timing-profile and sigrok API boundary tests. SETGUI's
`--check-dsview` sets `DSVIEW_HOST_CHECK` to a report file and exercises native
decoder dialogs, single/all removal with Yes/No, stacking and child lifetime.
The check saves decoder dialog screenshots alongside the report.
## Annotation search
The patch searches every enabled result column of the selected decoder and
every annotation label, including short waveform labels such as `ORPHAN`.
Matching is case insensitive; warning columns are enabled by default. Explicitly
hidden columns remain excluded until enabled in Decoding Results settings.
Enter (including the Windows popup editor), the magnifier and the next arrow
navigate forward; the previous arrow navigates backward. Matches are counted
per annotation, ordered by sample time, and wrap around. Tooltips show all
label variants. A nonempty query displays a compact match-only table: one
annotation per row, with its row title and matching label. Clearing the query
restores the source table; a query with no matches displays an empty list.
Click/arrow navigation maps each result back to the original annotation.
Numeric `AA-BB-CC` sequences are still supported; other hyphens
are literal. Search runs in short GUI-thread timer steps, invalidates indices
on model resets, and refreshes when decoding reaches 100 percent.
After rebuilding, verify with an IGBT recording containing an uncorrelated
Vstat pulse: search `ORPHAN`, `orphan`, and `Unexpected Vstat`; each should
select the same warning and show its waveform region. Also check Enter from
the popup editor, magnifier/arrow wraparound, a missing query, clearing/changing
the query during a long search, changing columns/decoders, and removing the
decoder while searching. Confirm numeric sequences and literal `CAN-FD` labels
still work. Source syntax checks do not replace this native GUI check.
## Install analyzers into standalone DSView (Windows)
The adapters in `tools/dsview/decoders` and portable cores in
`python/logic_analyzer/decoders` are shared with the embedded build.
No analyzer implementation is maintained in the consuming GUI.
```powershell
.\tools\dsview\install_decoders.ps1 -DsViewPath 'C:\Program Files\DSView'
```
Close DSView first. An elevated PowerShell is needed if the installation
folder is protected. Add `-CheckOnly` to validate source files and the target
layout without writing. Python/MSYS2 and a DSView rebuild are not required.
The target must contain DSView.exe and decoders/common.
Installs gate_driver_timing, set_uart, set_can and pm35_uart, with unchanged
cores under decoders/common/setgui_decoders. Existing unrelated files are
preserved. Replaced files are backed up under set-decoder-backups next to
DSView.exe; failed copies trigger rollback. Keep backups for manual recovery.
To restore, close DSView and copy the backup contents over decoders.
Restart DSView, add the UART or CAN base decoder and stack SET/PM35 on top.
Gate timing consumes logic channels directly. This installs decoders only;
it does not install the SETGUI host patch, menus or CSV import.
Installer tests run under Windows PowerShell against temporary targets and
check isolated imports, updates, backups, copy-failure rollback and validation:
```powershell
python -m unittest discover -s tools/dsview/tests -p test_install_decoders.py -v
```
## Decoder panel and selector
Embedded DSView does not restore standalone toolbar/dock layout. The Decoders
panel can extend to the top row of the DSView controls; its upper splitter
still allows reducing its height. SETGUI's own folder and tab rows remain
outside the native component.
The decoder selector starts with **Пользовательские**: IGBT 1SP/1SD,
Transistor pair, SET UART, SET CAN and PM35 UART. **Стандартные** contains the remaining
decoders. Search matches names and IDs in both sections and hides empty
section headings. The changes are packaged in `decoder-panel.patch`.

137
tools/dsview/build.py Normal file
View File

@@ -0,0 +1,137 @@
"""Build and package DSView host protocol 1 using an existing MSYS2 UCRT64 SDK."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
REVISION = '2e9e2c8e'
def recipe_digest():
"""Identify the patch, packaging recipe, adapters and their shared cores."""
recipe = Path(__file__).resolve().parent
core = recipe.parents[1] / 'python/logic_analyzer/decoders'
inputs = [recipe / 'build.py', recipe / 'setgui-host.patch', recipe / 'gate-pair-checkbox.patch', recipe / 'decoder-panel.patch', recipe / 'driver-summary.patch', recipe / 'windows-usb-events.patch', recipe / 'native/gate_summary.h']
inputs += sorted((recipe / 'decoders').rglob('*.py'))
inputs += [core / (name + '.py') for name in ('gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart')]
digest = hashlib.sha256()
for item in inputs:
digest.update(item.relative_to(recipe.parents[1]).as_posix().encode('utf-8') + b'\0')
digest.update(hashlib.sha256(item.read_bytes()).digest())
return digest.hexdigest()
def runtime_is_current(output):
output = Path(output)
try:
stamp = json.loads((output / 'host-build.json').read_text(encoding='utf-8'))
return (stamp.get('recipe') == recipe_digest()
and stamp.get('executable') == hashlib.sha256((output / 'DSView.exe').read_bytes()).hexdigest()
and (output / 'host-protocol-1').read_text(encoding='utf-8').strip() == 'DSView host protocol 1')
except (OSError, ValueError, AttributeError):
return False
def package_decoders(output):
"""Stage sigrok adapters and the canonical parsing cores without forks."""
recipe = Path(__file__).resolve().parent
decoders = Path(output) / 'decoders'
shutil.copytree(recipe / 'decoders', decoders, dirs_exist_ok=True,
ignore=shutil.ignore_patterns('__pycache__'))
common = decoders / 'common/setgui_decoders'
common.mkdir(parents=True, exist_ok=True)
(common / '__init__.py').write_text('', encoding='utf-8')
core = recipe.parents[1] / 'python/logic_analyzer/decoders'
for name in ('gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart'):
shutil.copy2(core / (name + '.py'), common / (name + '.py'))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--msys-root', type=Path, default=Path(r'C:\setcorp\tools\msys64'))
args = parser.parse_args()
source, output = args.source.resolve(), args.output.resolve()
prefix = args.msys_root.resolve() / 'ucrt64'
if not (prefix / 'bin/cmake.exe').is_file():
raise SystemExit('Install the MSYS2 UCRT64 dependencies listed in README.md first.')
revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=source, text=True).strip()
if not revision.startswith(REVISION):
raise SystemExit('Expected DSView revision ' + REVISION + ', got ' + revision)
for retired_name in ('driver-summary.patch', 'gate-pair-checkbox.patch'):
retired = Path(__file__).with_name(retired_name)
if subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', '--check', str(retired)],
cwd=source, capture_output=True).returncode == 0:
subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', str(retired)], cwd=source, check=True)
for patch_name in ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch'):
patch = Path(__file__).with_name(patch_name)
applied = subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', '--check', str(patch)], cwd=source, capture_output=True)
if applied.returncode:
subprocess.run(['git', 'apply', '--ignore-space-change', '--check', str(patch)], cwd=source, check=True)
subprocess.run(['git', 'apply', '--ignore-space-change', str(patch)], cwd=source, check=True)
env = dict(os.environ)
env['PATH'] = os.pathsep.join([str(prefix/'bin'), str(args.msys_root/'usr/bin'), env.get('PATH', '')])
env['PKG_CONFIG_PATH'] = str(prefix/'lib/pkgconfig')
build = source/'build-win64'
subprocess.run([str(prefix/'bin/cmake.exe'), '-S', str(source), '-B', str(build), '-G', 'Ninja',
'-DCMAKE_POLICY_VERSION_MINIMUM=3.5', '-DCMAKE_CXX_STANDARD=11',
'-DCMAKE_POLICY_DEFAULT_CMP0167=OLD', '-DCMAKE_PREFIX_PATH='+prefix.as_posix()],
env=env, check=True)
subprocess.run([str(prefix/'bin/cmake.exe'), '--build', str(build), '--parallel', '6'], env=env, check=True)
output.mkdir(parents=True, exist_ok=True)
shutil.copy2(source/'build.dir/DSView.exe', output/'DSView.exe')
for relative in ('DSView/res', 'DSView/demo', 'libsigrokdecode4DSL/decoders', 'lang'):
item = source/relative
shutil.copytree(item, output/item.name, dirs_exist_ok=True, ignore=shutil.ignore_patterns('__pycache__'))
package_decoders(output)
for name in ('COPYING', 'NEWS25', 'NEWS31', 'ug25.pdf', 'ug31.pdf'):
shutil.copy2(source/name, output/name)
stdlib = sorted((prefix/'lib').glob('python3.*'))
stdlib = [item for item in stdlib if (item/'encodings').is_dir()]
if len(stdlib) != 1:
raise RuntimeError('Expected exactly one Python standard library in UCRT64')
shutil.copytree(stdlib[0], output/'lib'/stdlib[0].name, dirs_exist_ok=True,
ignore=shutil.ignore_patterns('__pycache__', 'site-packages', 'test', 'tests', 'idlelib', 'tkinter', 'ensurepip'))
for name in ('platforms', 'imageformats', 'iconengines', 'styles'):
shutil.copytree(prefix/'share/qt5/plugins'/name, output/name, dirs_exist_ok=True)
shutil.copytree(prefix/'share/licenses', output/'licenses', dirs_exist_ok=True)
(output/'qt.conf').write_text('[Paths]\nPrefix=.\nPlugins=.\n', encoding='utf-8')
queue = [p for p in output.rglob('*') if p.suffix.lower() in ('.exe', '.dll', '.pyd')]
seen = set()
while queue:
binary = queue.pop()
if binary in seen:
continue
seen.add(binary)
info = subprocess.check_output([str(prefix/'bin/objdump.exe'), '-p', str(binary)], env=env, text=True, errors='replace')
for dll in re.findall(r'DLL Name:\s*(\S+)', info):
dependency, target = prefix/'bin'/dll, output/dll
if dependency.is_file():
if target not in seen:
shutil.copy2(dependency, target)
queue.append(target)
elif not (Path(os.environ['SystemRoot'])/'System32'/dll).is_file() and not dll.lower().startswith(('api-ms-', 'ext-ms-')):
raise RuntimeError('Missing dependency: ' + dll)
check_env = dict(os.environ)
check_env['PATH'] = str(Path(os.environ['SystemRoot'])/'System32')
version = subprocess.check_output([str(output/'DSView.exe'), '--host-version'], env=check_env, text=True).strip()
if version != 'DSView host protocol 1':
raise RuntimeError('Host protocol self-check failed: ' + version)
(output/'host-protocol-1').write_text(version+'\n', encoding='utf-8')
for patch_name in ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch'):
shutil.copy2(Path(__file__).with_name(patch_name), output/patch_name)
(output/'SOURCE.txt').write_text('https://github.com/DreamSourceLab/DSView\nCommit: '+revision+'\nPatches: setgui-host.patch, decoder-panel.patch, windows-usb-events.patch\nLicense: GPL-3.0-or-later (COPYING)\n', encoding='utf-8')
(output/'host-build.json').write_text(json.dumps({
'recipe': recipe_digest(),
'executable': hashlib.sha256((output/'DSView.exe').read_bytes()).hexdigest(),
}, indent=2) + '\n', encoding='utf-8')
print('DSView runtime:', output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,119 @@
--- a/DSView/pv/dock/searchcombobox.h
+++ b/DSView/pv/dock/searchcombobox.h
@@ -58,6 +58,7 @@
public:
QString _id;
QString _name;
+ QString _section;
void *_data_handle;
QWidget *_control;
};
@@ -74,7 +75,7 @@
void ShowDlg(QWidget *editline);
- void AddDataItem(QString id, QString name, void *data_handle);
+ void AddDataItem(QString id, QString name, void *data_handle, QString section = QString());
inline void SetItemClickHandle(ISearchItemClick *click){
_item_click = click;
@@ -95,6 +96,7 @@
std::vector<SearchDataItem*> _items;
ISearchItemClick *_item_click;
QScrollArea *_scroll;
+ std::vector<std::pair<QString, QWidget*> > _sections;
};
#endif // SEARCHCOMBOBOX_H
--- a/DSView/pv/dock/searchcombobox.cpp
+++ b/DSView/pv/dock/searchcombobox.cpp
@@ -25,6 +25,7 @@
#include <QPoint>
#include <QLineEdit>
#include <QScrollBar>
+#include <QLabel>
#include "../config/appconfig.h"
#include "../appcontrol.h"
#include "../ui/fn.h"
@@ -107,8 +108,17 @@
QFont font = this->font();
font.setPointSizeF(AppConfig::Instance().appOptions.fontSize);
+ QString section;
for(auto o : _items)
- {
+ {
+ if (!o->_section.isEmpty() && o->_section != section) {
+ section = o->_section;
+ auto heading = new QLabel(section, listPanel);
+ heading->setFont(font);
+ heading->setStyleSheet("font-weight: bold; padding: 8px 4px; border-top: 1px solid #344762;");
+ listLay->addWidget(heading);
+ _sections.push_back(std::make_pair(section, heading));
+ }
ComboButtonItem *bt = new ComboButtonItem(panel, this, o);
bt->setText(o->_name);
bt->setObjectName("flat");
@@ -143,11 +153,12 @@
this->show();
}
-void SearchComboBox::AddDataItem(QString id, QString name, void *data_handle)
+void SearchComboBox::AddDataItem(QString id, QString name, void *data_handle, QString section)
{
SearchDataItem *item = new SearchDataItem();
item->_id = id;
item->_name = name;
+ item->_section = section;
item->_data_handle = data_handle;
this->_items.push_back(item);
}
@@ -197,5 +208,14 @@
}
}
+ for (auto section : _sections) {
+ bool visible = false;
+ for (auto item : _items)
+ if (item->_section == section.first && !item->_control->isHidden()) {
+ visible = true;
+ break;
+ }
+ section.second->setVisible(visible);
+ }
_scroll->verticalScrollBar()->setValue(0);
}
--- a/DSView/pv/dock/protocoldock.cpp
+++ b/DSView/pv/dock/protocoldock.cpp
@@ -1060,10 +1060,15 @@
{
SearchComboBox *panel = new SearchComboBox(this);
- for (auto info : _decoderInfoList)
- {
- srd_decoder *dec = (srd_decoder *)(info->_data_handle);
- panel->AddDataItem(QString(dec->id), QString(dec->name), info);
+ const QStringList customIds = {"gate_driver_timing", "transistor_pair", "set_uart", "set_can", "pm35_uart"};
+ for (bool custom : {true, false}) {
+ for (auto info : _decoderInfoList) {
+ srd_decoder *dec = (srd_decoder *)(info->_data_handle);
+ if (customIds.contains(QString(dec->id)) != custom)
+ continue;
+ panel->AddDataItem(QString(dec->id), QString(dec->name), info,
+ custom ? QString::fromUtf8("Пользовательские") : QString::fromUtf8("Стандартные"));
+ }
}
QFont font = this->font();
--- a/DSView/pv/mainwindow.cpp
+++ b/DSView/pv/mainwindow.cpp
@@ -1329,7 +1329,8 @@
// default dockwidget size
AppConfig &app = AppConfig::Instance();
QByteArray st = app.frameOptions.windowState;
- if (!st.isEmpty())
+ // Standalone toolbar rows must not reserve space above embedded docks.
+ if (!qApp->property("embeddedHost").toBool() && !st.isEmpty())
{
try
{

View File

@@ -0,0 +1,60 @@
# 1SP0635 / 1SD536F2 timing decoder for DSView
This decoder checks driver control and status responses:
- `Vin1`: optional control sent to the first gate driver (legacy channel ID `vin`);
- `Vstat`: edge acknowledgement and fault feedback returned by the driver.
Built-in data-sheet profiles:
| Profile | ACK delay (typ.) | ACK width (specified) | Long pulse |
|---|---:|---:|---:|
| 1SP0635 | 250 ns | 400...1050 ns | fault above 1.5 us |
| 1SD536F2 | 380 ns | 600...1800 ns | fault above 1.8 us* |
`*` The 1.8 us fault threshold is a project classification choice based on the
data-sheet ACK maximum, not a verified universal manufacturer fault threshold.
The previously stated 1.5 us manual threshold could not be verified.
Correlated pulses within the ACK width limits pass. Uncorrelated pulses up to
the fault threshold are warnings; pulses above it are faults. Width and fault
annotations are emitted only when the pulse ends.
The data sheets specify only a *typical* ACK delay, not minimum and maximum
limits. DSView therefore emits a warning outside the configurable tolerance
(default +/-100 ns). The ACK-width check uses the published min/max values.
## Use in SETGUI
The DSView build recipe packages this adapter together with the canonical
`python/logic_analyzer/decoders/gate_timing.py` core. In the DSView tab,
choose **Анализаторы SET → IGBT · 1SP0635 / 1SD536F2** (or search Decode for
`IGBT 1SP/1SD`). Assign Vstat and, if available, Vin1, choose the driver profile, and set `Vstat acknowledge/fault level`
to match the receiver circuit. The diagrams in the application manual show
the logical event; some optical receiver circuits invert it.
Use at least 50 MS/s; 100 MS/s or more is recommended. Never connect a DSLogic
digital input directly to the IGBT gate (+15/-10 V) or to a power-stage node.
Use the isolated optical receiver logic signals and a common logic ground.
## Separate transistor pair analyzer
This decoder analyzes only Vin1/Vstat driver responses. Vin1 is optional;
Vstat alone retains pulse/fault classification, while Vin1 enables ACK timing
and full-cycle results. Vstat is required. The channel ID `vin` remains stable.
Pair analysis was moved to **Transistor pair** in the **Пользовательские**
section. Add that decoder separately and assign Vin1/Vin2. Old pair settings
on IGBT instances must be recreated in the new decoder. The pair checkbox,
Vin2 assignment and pulse limits are no longer part of the driver decoder.
## Full pulse cycle result
The **Vin1 / ACK full cycle** waveform row displays OK, WARNING or FAULT
across each complete sequence **Vin1 ON -> ACK -> Vin1 OFF -> ACK**.
The annotation starts at Vin1 ON and ends at the end of the OFF acknowledgment.
It is emitted only after both ACKs have been classified. A delay warning makes
that cycle WARNING; missing ACK, invalid width or fault feedback makes it FAULT.
A later normal cycle can be OK independently of previous cycles.
Unfinished cycles are never marked OK. Active-low Vin1 is supported.
The results panel retains its original layout without a large summary banner.
Deadtime and mintime are provided by the separate Transistor pair analyzer.

View File

@@ -0,0 +1,3 @@
"""DSView protocol decoder for SCALE gate-driver timing checks."""
from .pd import Decoder

View File

@@ -0,0 +1,168 @@
"""DSView/libsigrokdecode front end for SCALE gate-driver timing checks."""
import sigrokdecode as srd
from common.setgui_decoders.gate_timing import TimingChecker
class SamplerateError(Exception):
pass
class Ann(object):
CONTROL = 0
DELAY_OK = 1
WIDTH_OK = 2
WARNING = 3
ERROR = 4
FAULT = 5
CYCLE_OK = 6
CYCLE_WARNING = 7
CYCLE_FAULT = 8
ANN_FOR_KIND = {
'control': Ann.CONTROL,
'delay_ok': Ann.DELAY_OK,
'width_ok': Ann.WIDTH_OK,
'delay_warn': Ann.WARNING,
'width_fail': Ann.ERROR,
'missing': Ann.ERROR,
'orphan': Ann.WARNING,
'fault': Ann.FAULT,
'cycle_ok': Ann.CYCLE_OK,
'cycle_warning': Ann.CYCLE_WARNING,
'cycle_fault': Ann.CYCLE_FAULT,
}
class Decoder(srd.Decoder):
api_version = 3
id = 'gate_driver_timing'
name = 'IGBT 1SP/1SD'
longname = 'Power Integrations 1SP0635 / 1SD536F2 timing checker'
desc = 'Checks driver Vin1/Vstat ACK timing, faults and full pulse cycles.'
license = 'gplv2+'
inputs = ['logic']
outputs = []
tags = ['Clock/timing', 'Util']
channels = ()
optional_channels = (
{'id': 'vin', 'name': 'Vin1', 'desc': 'First driver control input'},
{'id': 'vstat', 'name': 'Vstat', 'desc': 'Driver status/acknowledge output'},
)
options = (
{'id': 'profile', 'desc': 'Driver profile', 'default': '1SP0635',
'values': ('1SP0635', '1SD536F2', 'custom')},
{'id': 'vin_active', 'desc': 'Vin1 active level', 'default': 'high',
'values': ('high', 'low')},
{'id': 'vstat_active', 'desc': 'Vstat acknowledge/fault level', 'default': 'high',
'values': ('high', 'low')},
{'id': 'delay_tolerance_ns',
'desc': 'Warning tolerance around typical ACK delay (ns)', 'default': 100},
{'id': 'orphan_min_width_ns',
'desc': 'ORPHAN minimum pulse width (ns, 0 = off)', 'default': 0},
{'id': 'custom_delay_ns', 'desc': 'Custom typical ACK delay (ns)', 'default': 250},
{'id': 'custom_width_min_ns', 'desc': 'Custom ACK width minimum (ns)', 'default': 400},
{'id': 'custom_width_typ_ns', 'desc': 'Custom ACK width typical (ns)', 'default': 700},
{'id': 'custom_width_max_ns', 'desc': 'Custom ACK width maximum (ns)', 'default': 1050},
{'id': 'custom_fault_ns', 'desc': 'Custom fault pulse threshold (ns)', 'default': 1500},
)
annotations = (
('control', 'Vin1 edge'),
('delay-ok', 'ACK delay pass'),
('width-ok', 'ACK width pass'),
('warning', 'Warning'),
('error', 'Timing failure'),
('fault', 'Driver fault'),
('cycle-ok', 'Cycle OK'),
('cycle-warning', 'Cycle WARNING'),
('cycle-fault', 'Cycle FAULT'),
)
annotation_rows = (
('cycle', 'Vin1 / ACK full cycle', (Ann.CYCLE_OK, Ann.CYCLE_WARNING, Ann.CYCLE_FAULT)),
('vin', 'Vin1', (Ann.CONTROL,)),
('measurements', 'Measurements', (Ann.DELAY_OK, Ann.WIDTH_OK)),
('checks', 'Warnings / failures', (Ann.WARNING, Ann.ERROR, Ann.FAULT)),
)
def __init__(self):
self.reset()
# Состояние принадлежит одному запуску декодера. Сброс не должен
# переносить незавершённый кадр или частоту из предыдущей записи.
def reset(self):
self.samplerate = None
self.checker = None
# DSView передаёт время в номерах отсчётов. Частота нужна общему ядру
# для перевода тайм-аутов и измерений в физические единицы.
def metadata(self, key, value):
if key == srd.SRD_CONF_SAMPLERATE:
self.samplerate = value
# Выходы регистрируются в жизненном цикле sigrok, а не в конструкторе.
# Здесь регистрируются аннотации для дорожек измерений и предупреждений.
def start(self):
self.out_ann = self.register(srd.OUTPUT_ANN)
# Адаптер переводит настройки DSView в параметры общего TimingChecker.
# Пороговые алгоритмы остаются в templates/python/logic_analyzer/decoders.
def _make_checker(self):
custom = {
'ack_delay_typ_ns': float(self.options['custom_delay_ns']),
'ack_width_min_ns': float(self.options['custom_width_min_ns']),
'ack_width_typ_ns': float(self.options['custom_width_typ_ns']),
'ack_width_max_ns': float(self.options['custom_width_max_ns']),
'fault_threshold_ns': float(self.options['custom_fault_ns']),
}
return TimingChecker(
self.samplerate,
profile=self.options['profile'],
delay_tolerance_ns=float(self.options['delay_tolerance_ns']),
vin_active_high=self.options['vin_active'] == 'high',
vstat_active_high=self.options['vstat_active'] == 'high',
orphan_min_width_ns=self.options.get('orphan_min_width_ns', 0),
custom=custom, cycle_results=True)
def _put_events(self, events):
for event in events:
ann = ANN_FOR_KIND[event['kind']]
self.put(event['start'], max(event['start'], event['end']),
self.out_ann, [ann, [event['text'], event['short']]])
def decode(self):
if not self.samplerate:
raise SamplerateError('Cannot decode without samplerate.')
self.checker = self._make_checker()
has_vin1 = self.has_channel(0)
if not self.has_channel(1):
raise ValueError('Connect Vstat for driver response analysis.')
while True:
conditions = [{0: 'e'}, {1: 'e'}] if has_vin1 else [{1: 'e'}]
timer_bit = 1 << len(conditions)
deadline = self.checker.next_deadline()
if deadline is not None:
conditions.append({'skip': max(1, deadline - self.samplenum)})
# DSView mutates and returns the same native tuple on every wait.
# Release that tuple before waiting again (PyTuple_SetItem requires
# exclusive ownership); tuple(...) would not make a copy.
pins = list(self.wait(conditions))
vin, vstat = pins[:2]
now = self.samplenum
# Both channels may transition at the same sample; preserve both.
if has_vin1 and self.matched & 0b001:
self._put_events(self.checker.on_control_edge(now, vin))
if self.matched & (0b010 if has_vin1 else 0b001):
self._put_events(self.checker.on_status_edge(now, vstat))
if deadline is not None and self.matched & timer_bit:
self._put_events(self.checker.expire(now))
def end(self):
if self.checker is not None:
self._put_events(self.checker.finish_cycles(self.samplenum))

View File

@@ -0,0 +1 @@
from .pd import Decoder

View File

@@ -0,0 +1,82 @@
import sigrokdecode as srd
from common.setgui_decoders.pm35_uart import PM35Parser
class Decoder(srd.Decoder):
api_version = 3
id = 'pm35_uart'
name = 'PM35 UART'
longname = 'PM35 / TMS320F28335 terminal'
desc = 'MODBUS 03/06 register traffic and CRC16.'
license = 'gplv2+'
inputs = ['uart']
outputs = ['pm35']
tags = ['Embedded/industrial']
options = (
{'id': 'rx_role', 'desc': 'RX carries', 'default': 'response', 'values': ('request', 'response', 'auto')},
{'id': 'tx_role', 'desc': 'TX carries', 'default': 'request', 'values': ('request', 'response', 'auto')},
{'id': 'baudrate', 'desc': 'UART baudrate (RTU gap detection)', 'default': 115200},
{'id': 'bits_per_char', 'desc': 'Bits per UART character including start/stop/parity', 'default': 10},
)
annotations = (('frame', 'Frame'), ('error', 'Error'))
annotation_rows = (('frames', 'Frames', (0,)), ('errors', 'Errors', (1,)))
def __init__(self):
self.reset()
# Состояние принадлежит одному запуску декодера. Сброс не должен
# переносить незавершённый кадр или частоту из предыдущей записи.
def reset(self):
self.parsers = {}
self.last = {}
self.samplerate = None
# DSView передаёт время в номерах отсчётов. Частота нужна общему ядру
# для перевода тайм-аутов и измерений в физические единицы.
def metadata(self, key, value):
if key == srd.SRD_CONF_SAMPLERATE:
self.samplerate = value
# Выходы регистрируются в жизненном цикле sigrok, а не в конструкторе.
# Аннотации предназначены для дорожек, структурированные события — для стека.
def start(self):
if self.options['baudrate'] <= 0 or self.options['bits_per_char'] <= 0:
raise ValueError('Baudrate and bits per character must be positive')
self.out_ann = self.register(srd.OUTPUT_ANN)
self.out_python = self.register(srd.OUTPUT_PYTHON)
def emit(self, events, direction):
for ss, es, result, error in events:
self.put(ss, es, self.out_ann, [1 if error else 0,
[('RX', 'TX')[direction] + ' ' + (error or result['summary'])]])
if result:
result['direction'] = direction
self.put(ss, es, self.out_python, ['FRAME', result])
def decode(self, ss, es, data):
kind, direction, payload = data
if direction not in (0, 1):
return
parser = self.parsers.setdefault(direction, PM35Parser(self.options[('rx_role', 'tx_role')[direction]]))
if kind in ('INVALID STOPBIT', 'INVALID STARTBIT', 'PARITY ERROR', 'BREAK'):
self.emit(parser.flush(), direction)
self.put(ss, es, self.out_ann, [1, [kind]])
return
if kind != 'FRAME':
return
if not payload[1]:
self.emit(parser.flush(), direction)
self.put(ss, es, self.out_ann, [1, ['Invalid UART frame']])
return
# Граница MODBUS определяется паузой 3,5 символа. bits_per_char включает
# все биты символа; эта настройка должна соответствовать базовому UART.
if self.samplerate and direction in self.last:
gap = self.samplerate * 3.5 * self.options['bits_per_char'] / self.options['baudrate']
if ss - self.last[direction] >= gap:
self.emit(parser.flush(), direction)
self.last[direction] = es
if not 0 <= payload[0] <= 255:
self.emit(parser.flush(), direction)
self.put(ss, es, self.out_ann, [1, ['Configure UART for 8 data bits']])
return
self.emit(parser.feed(payload[0], ss, es), direction)

View File

@@ -0,0 +1 @@
from .pd import Decoder

View File

@@ -0,0 +1,66 @@
import sigrokdecode as srd
from common.setgui_decoders.set_can import Reassembler, legacy
class Decoder(srd.Decoder):
api_version = 3
id = 'set_can'
name = 'SET CAN'
longname = 'SET v2 CAN / ProtoCAN / Balsam'
desc = 'Application fields and segmented SET packets over classic CAN.'
license = 'gplv2+'
inputs = ['can']
outputs = ['set']
tags = ['Embedded/industrial']
options = ({'id': 'protocol', 'desc': 'CAN protocol (select explicitly)',
'default': 'protocan', 'values': ('protocan', 'set-v2', 'balsam')},)
annotations = (('frame', 'Frame'), ('payload', 'Payload'), ('error', 'Error'))
annotation_rows = (('frames', 'Frames', (0,)), ('payload', 'Payload', (1,)), ('errors', 'Errors', (2,)))
def __init__(self):
self.reset()
# Состояние принадлежит одному запуску декодера. Сброс не должен
# переносить незавершённый кадр или частоту из предыдущей записи.
def reset(self):
self.reassembler = Reassembler()
self.samplerate = None
# DSView передаёт время в номерах отсчётов. Частота нужна общему ядру
# для перевода тайм-аутов и измерений в физические единицы.
def metadata(self, key, value):
if key == srd.SRD_CONF_SAMPLERATE:
self.samplerate = value
# Выходы регистрируются в жизненном цикле sigrok, а не в конструкторе.
# Аннотации предназначены для дорожек, структурированные события — для стека.
def start(self):
self.out_ann = self.register(srd.OUTPUT_ANN)
self.out_python = self.register(srd.OUTPUT_PYTHON)
def decode(self, ss, es, data):
frame_type, ident, rtr, dlc, payload = data
if frame_type != 'extended' or rtr == 'remote':
return
mode = self.options['protocol']
if not 0 <= dlc <= 8 or len(payload) != dlc:
events = [(ss, es, None, 'Invalid classic CAN DLC')]
# Сегментированный SET v2 требует времени для тайм-аута сборки.
# Идентификатор, порядок сегментов и CRC проверяет общее ядро.
elif mode == 'set-v2':
if not self.samplerate:
raise ValueError('SET CAN requires samplerate for reassembly timeout')
events = self.reassembler.feed(ident, payload, ss, es, es * 1000.0 / self.samplerate)
else:
try:
result = legacy(ident, payload, mode)
events = [(ss, es, result, None)] if result else []
except ValueError as exc:
events = [(ss, es, None, str(exc))]
for start, end, result, error in events:
self.put(start, end, self.out_ann, [2 if error else 0, [error or result['summary']]])
if result:
result['can_id'] = ident
self.put(start, end, self.out_python, ['FRAME', result])
if events and payload:
self.put(ss, es, self.out_ann, [1, [' '.join('%02X' % b for b in payload)]])

View File

@@ -0,0 +1 @@
from .pd import Decoder

View File

@@ -0,0 +1,89 @@
import sigrokdecode as srd
from common.setgui_decoders.set_uart import StreamParser
class Decoder(srd.Decoder):
api_version = 3
id = 'set_uart'
name = 'SET UART'
longname = 'SET v2 / SETGUI v1 / ProtoCAN bridge'
desc = 'SET frames, fields and CRC over UART.'
license = 'gplv2+'
inputs = ['uart']
outputs = ['set']
tags = ['Embedded/industrial']
options = (
{'id': 'protocol', 'desc': 'Protocol', 'default': 'auto',
'values': ('auto', 'v2', 'v1', 'bridge')},
{'id': 'gap_ms', 'desc': 'Reset incomplete frame after gap (ms, 0 disables)', 'default': 100},
)
annotations = (('frame', 'Frame'), ('payload', 'Payload'), ('error', 'Error'))
annotation_rows = (('frames', 'Frames', (0,)), ('payload', 'Payload', (1,)), ('errors', 'Errors', (2,)))
def __init__(self):
self.reset()
# Состояние принадлежит одному запуску декодера. Сброс не должен
# переносить незавершённый кадр или частоту из предыдущей записи.
def reset(self):
self.parsers = {}
self.last = {}
self.samplerate = None
# DSView передаёт время в номерах отсчётов. Частота нужна общему ядру
# для перевода тайм-аутов и измерений в физические единицы.
def metadata(self, key, value):
if key == srd.SRD_CONF_SAMPLERATE:
self.samplerate = value
# Выходы регистрируются в жизненном цикле sigrok, а не в конструкторе.
# Аннотации предназначены для дорожек, структурированные события — для стека.
def start(self):
self.out_ann = self.register(srd.OUTPUT_ANN)
self.out_python = self.register(srd.OUTPUT_PYTHON)
def emit(self, events, direction):
for ss, es, frame, error in events:
prefix = ('RX', 'TX')[direction] + ' '
self.put(ss, es, self.out_ann, [2 if error else 0, [prefix + (error or frame['summary'])]])
if frame:
if frame['protocol'] == 'ProtoCAN bridge' and frame['flags'] & 1 and not frame['flags'] & 10:
# Imported only at decode time: both packages are installed together.
from common.setgui_decoders.set_can import legacy
try:
application = legacy(frame['can_id'], frame['payload'])
frame['application'] = application
self.put(ss, es, self.out_ann, [0, [application['summary']]])
except ValueError as exc:
self.put(ss, es, self.out_ann, [2, [str(exc)]])
frame['direction'] = direction
self.put(ss, es, self.out_python, ['FRAME', frame])
if frame['payload']:
self.put(ss, es, self.out_ann, [1, [prefix + ' '.join('%02X' % b for b in frame['payload'])]])
def decode(self, ss, es, data):
kind, direction, payload = data
if direction not in (0, 1):
return
# RX и TX имеют независимые парсеры: перемежающиеся байты двух направлений
# нельзя объединять в один прикладной кадр.
parser = self.parsers.setdefault(direction, StreamParser(self.options['protocol']))
if kind in ('INVALID STOPBIT', 'INVALID STARTBIT', 'PARITY ERROR', 'BREAK'):
self.emit(parser.flush(), direction)
self.put(ss, es, self.out_ann, [2, [kind]])
return
if kind != 'FRAME':
return
if not payload[1]:
self.emit(parser.flush(), direction)
self.put(ss, es, self.out_ann, [2, ['Invalid UART frame']])
return
gap = float(self.options['gap_ms'])
if self.samplerate and gap > 0 and direction in self.last and ss - self.last[direction] > self.samplerate * gap / 1000:
self.emit(parser.flush(), direction)
self.last[direction] = es
if not 0 <= payload[0] <= 255:
self.emit(parser.flush(), direction)
self.put(ss, es, self.out_ann, [2, ['Configure UART for 8 data bits']])
return
self.emit(parser.feed(payload[0], ss, es), direction)

View File

@@ -0,0 +1,26 @@
# Transistor pair
Independent two-channel analyzer. Assign Vin1 and Vin2; Vstat is not used.
Available in DSView's **Пользовательские** section as **Transistor pair**.
- Minimum active ON pulse duration (mintime), independently per input.
- Minimum inactive OFF duration, independently per input.
- Deadtime from Vin1 OFF to Vin2 ON and vice versa, with independent minimums.
- Active overlap is a fault, regardless of the deadtime settings.
- Period, frequency and active duty cycle for complete cycles on each input.
- Independent active-high/active-low polarity for both inputs.
All limits are nanoseconds. Zero disables the corresponding lower limit;
equality passes. Simultaneous handovers measure zero deadtime. These settings
check measured pulses and do not debounce or filter the source waveform.
Measurements occupy separate waveform rows. Timing violations appear in the
FAULT row. Unknown edges outside the capture are not fabricated; partial
pulses do not produce full pulse widths or full-cycle frequency/duty values.
Python: `PairTimingChecker` in `logic_analyzer.decoders.transistor_pair`.
Offline API: `analyze_capture(..., dict(mode='Transistor pair', channel=0,
vin2_channel=1, vin1_mintime_ns=100, deadtime_12_ns=50))`.
For Vin/Vstat ACK checks and full driver-cycle OK/WARNING/FAULT results use
the separate **IGBT 1SP/1SD** decoder.

View File

@@ -0,0 +1 @@
from .pd import Decoder

View File

@@ -0,0 +1,88 @@
"""Two-input transistor pair analyzer, independent of driver feedback."""
import sigrokdecode as srd
from common.setgui_decoders.transistor_pair import PairTimingChecker
class Decoder(srd.Decoder):
api_version = 3
id = 'transistor_pair'
name = 'Transistor pair'
longname = 'Transistor pair: mintime / deadtime / overlap'
desc = 'Checks ON/OFF pulse limits, deadtime, overlap, frequency and duty.'
license = 'gplv2+'
inputs = ['logic']
outputs = []
tags = ['Clock/timing', 'Util']
channels = (
{'id': 'vin1', 'name': 'Vin1', 'desc': 'First transistor command'},
{'id': 'vin2', 'name': 'Vin2', 'desc': 'Second transistor command'},
)
options = (
{'id': 'vin1_active', 'desc': 'Vin1 active level', 'default': 'high', 'values': ('high', 'low')},
{'id': 'vin2_active', 'desc': 'Vin2 active level', 'default': 'high', 'values': ('high', 'low')},
{'id': 'vin1_mintime_ns', 'desc': 'Vin1 minimum ON time (ns)', 'default': 0},
{'id': 'vin2_mintime_ns', 'desc': 'Vin2 minimum ON time (ns)', 'default': 0},
{'id': 'vin1_minoff_ns', 'desc': 'Vin1 minimum OFF time (ns)', 'default': 0},
{'id': 'vin2_minoff_ns', 'desc': 'Vin2 minimum OFF time (ns)', 'default': 0},
{'id': 'deadtime_12_ns', 'desc': 'Minimum deadtime Vin1 -> Vin2 (ns)', 'default': 0},
{'id': 'deadtime_21_ns', 'desc': 'Minimum deadtime Vin2 -> Vin1 (ns)', 'default': 0},
)
annotations = (
('vin1-on', 'Vin1 ON duration'), ('vin2-on', 'Vin2 ON duration'),
('deadtime', 'Deadtime'), ('failure', 'FAULT'),
('vin1-off', 'Vin1 OFF duration'), ('vin2-off', 'Vin2 OFF duration'),
('vin1-period', 'Vin1 period / frequency / duty'),
('vin2-period', 'Vin2 period / frequency / duty'),
)
annotation_rows = (
('deadtime', 'Deadtime', (2,)),
('faults', 'FAULT: overlap / timing', (3,)),
('vin1', 'Vin1 mintime / OFF', (0, 4)),
('vin2', 'Vin2 mintime / OFF', (1, 5)),
('pwm1', 'Vin1 period / frequency / duty', (6,)),
('pwm2', 'Vin2 period / frequency / duty', (7,)),
)
def __init__(self):
self.reset()
def reset(self):
self.samplerate = None
self.checker = None
def metadata(self, key, value):
if key == srd.SRD_CONF_SAMPLERATE:
self.samplerate = value
def start(self):
self.out_ann = self.register(srd.OUTPUT_ANN)
def _put_events(self, events):
for event in events:
kind = event['kind']
channel = event.get('channel', 1) - 1
ann = (3 if kind in ('mintime_fail', 'deadtime_fail', 'off_fail', 'overlap') else
2 if kind == 'deadtime' else channel if kind == 'mintime_ok' else
4 + channel if kind == 'off_time' else 6 + channel)
self.put(event['start'], event['end'], self.out_ann, [ann, [event['text'], event['short']]])
def decode(self):
if not self.samplerate:
raise ValueError('Cannot decode without samplerate.')
if not self.has_channel(0) or not self.has_channel(1):
raise ValueError('Connect both Vin1 and Vin2.')
numeric = {name: self.options[name] for name in (
'vin1_mintime_ns', 'vin2_mintime_ns', 'vin1_minoff_ns', 'vin2_minoff_ns',
'deadtime_12_ns', 'deadtime_21_ns')}
self.checker = PairTimingChecker(self.samplerate,
self.options['vin1_active'] == 'high', self.options['vin2_active'] == 'high', **numeric)
# Do not retain DSView's mutable native wait tuple.
pins = list(self.wait({}))
self._put_events(self.checker.update(self.samplenum, *pins))
while True:
pins = list(self.wait([{0: 'e'}, {1: 'e'}]))
self._put_events(self.checker.update(self.samplenum, *pins))
def end(self):
if self.checker is not None:
self._put_events(self.checker.finish(self.samplenum))

View File

@@ -0,0 +1,83 @@
--- a/DSView/pv/data/decoderstack.h
+++ b/DSView/pv/data/decoderstack.h
@@ -30,6 +30,7 @@
#include <QString>
#include <mutex>
+#include "decode/gate_summary.h"
#include "decode/row.h"
#include "../data/signaldata.h"
#include "decode/decoderstatus.h"
@@ -96,6 +97,7 @@
}
const char* get_root_decoder_id();
+ GateSummary gate_summary;
void add_sub_decoder(decode::Decoder *decoder);
void remove_sub_decoder(decode::Decoder *decoder);
--- a/DSView/pv/data/decoderstack.cpp
+++ b/DSView/pv/data/decoderstack.cpp
@@ -369,6 +369,7 @@
void DecoderStack::init()
{
+ gate_summary.reset();
_sample_count = 0;
_samples_decoded = 0;
_error_message = QString();
@@ -814,6 +815,9 @@
const srd_decoder *const decc = pdata->pdo->di->decoder;
assert(decc);
+ if (strcmp(decc->id, "gate_driver_timing") == 0)
+ d->gate_summary.observe(a->format());
+
auto row_iter = d->_rows.end();
// Try looking up the sub-row of this class
--- a/DSView/pv/dock/protocoldock.cpp
+++ b/DSView/pv/dock/protocoldock.cpp
@@ -190,6 +190,42 @@
match_layout->addStretch(1);
QVBoxLayout *bot_layout = new QVBoxLayout();
+ auto gate_status = new QLabel(bot_panel);
+ gate_status->setObjectName("gateDriverSummary");
+ gate_status->setAlignment(Qt::AlignCenter);
+ gate_status->setMinimumHeight(64);
+ gate_status->hide();
+ bot_layout->addWidget(gate_status);
+ auto gate_timer = new QTimer(this);
+ connect(gate_timer, &QTimer::timeout, this, [this, gate_status]() {
+ int severity = 0;
+ int drivers = 0;
+ bool complete = true;
+ for (auto trace : _session->get_decode_signals()) {
+ auto stack = trace->decoder();
+ if (QString(stack->get_root_decoder_id()) != "gate_driver_timing") continue;
+ ++drivers;
+ const bool done = !stack->IsRunning() && stack->get_progress() == 100;
+ complete = complete && done;
+ int result = stack->gate_summary.severity(done);
+ if (stack->out_of_memory() || !stack->error_message().isEmpty())
+ result = qMax(result, 1);
+ severity = qMax(severity, result);
+ }
+ gate_status->setVisible(drivers > 0);
+ if (!drivers) return;
+ const QString text = severity == 2 ? "FAULT" : severity == 1 ? "WARNING" : "OK";
+ const QString color = severity == 2 ? "#ff6262" : severity == 1 ? "#ffc857" : "#41d687";
+ if (gate_status->text() != text) {
+ gate_status->setText(text);
+ gate_status->setStyleSheet("QLabel#gateDriverSummary { font-size: 32px; font-weight: bold; "
+ "padding: 8px; border: 2px solid " + color + "; border-radius: 6px; color: " + color + "; }");
+ }
+ gate_status->setToolTip(complete
+ ? QString::fromUtf8("Итог всех анализаторов драйвера за всю запись. FAULT: ошибки ACK, Vstat или входов; WARNING: предупреждения, неполные события или нет подтверждений; OK: проверки пройдены.")
+ : QString::fromUtf8("Анализ ещё не завершён. Итог OK пока недоступен."));
+ });
+ gate_timer->start(250);
bot_layout->addLayout(bot_title_layout);
bot_layout->addLayout(ann_search_layout);
bot_layout->addLayout(match_layout);

View File

@@ -0,0 +1,35 @@
--- a/DSView/pv/prop/binding/decoderoptions.cpp
+++ b/DSView/pv/prop/binding/decoderoptions.cpp
@@ -27,6 +27,7 @@
#include "../../data/decoderstack.h"
#include "../../data/decode/decoder.h"
+#include "../bool.h"
#include "../double.h"
#include "../enum.h"
#include "../int.h"
@@ -85,7 +86,23 @@
Property *prop = NULL;
- if (opt->values)
+ if (strcmp(dec->id, "gate_driver_timing") == 0 &&
+ strcmp(opt->id, "pair_analysis") == 0) {
+ prop = new Bool(name, name,
+ [getter]() -> GVariant* {
+ GVariant *value = getter();
+ const bool enabled = value && g_variant_get_int64(value) != 0;
+ if (value) g_variant_unref(value);
+ return g_variant_ref_sink(g_variant_new_boolean(enabled));
+ },
+ [setter](GVariant *value) {
+ GVariant *number = g_variant_ref_sink(g_variant_new_int64(
+ g_variant_get_boolean(value) ? 1 : 0));
+ setter(number);
+ g_variant_unref(number);
+ });
+ }
+ else if (opt->values)
prop = bind_enum(name, opt, getter, setter);
else if (g_variant_is_of_type(opt->def, G_VARIANT_TYPE("d")))
prop = new Double(name, name, 2, "",none, none, getter, setter);

View File

@@ -0,0 +1,151 @@
[CmdletBinding()]
param(
[string]$DsViewPath = (Join-Path $env:ProgramFiles 'DSView'),
[switch]$CheckOnly
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2
# Resolve source paths relative to templates, never the caller working directory.
# Standalone DSView and SETGUI use the same adapters and parsing cores.
$templateRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
$targetRoot = [IO.Path]::GetFullPath($DsViewPath)
$decoderRoot = Join-Path $targetRoot 'decoders'
# Use .NET for compatibility with PowerShell lacking Get-FileHash.
# Release both stream and hashing provider even if reading fails.
function Get-ContentHash([string]$Path) {
$algorithm = [Security.Cryptography.SHA256]::Create()
$stream = [IO.File]::OpenRead($Path)
try {
return [BitConverter]::ToString($algorithm.ComputeHash($stream))
} finally {
$stream.Dispose()
$algorithm.Dispose()
}
}
# Check all existing ancestors: a junction under decoders/common could
# redirect a local-looking update outside the chosen installation.
function Assert-PlainPath([string]$Path) {
$cursor = [IO.Path]::GetFullPath($Path)
$first = $true
while ($cursor) {
if (Test-Path -LiteralPath $cursor) {
$item = Get-Item -LiteralPath $cursor -Force
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
throw "Symbolic links and junctions are not supported: $cursor"
}
if (-not $first -and -not $item.PSIsContainer) {
throw "Expected a parent directory: $cursor"
}
}
$parent = [IO.Path]::GetDirectoryName($cursor)
if ($parent -eq $cursor) { break }
$cursor = $parent
$first = $false
}
}
Assert-PlainPath $targetRoot
$exe = Join-Path $targetRoot 'DSView.exe'
if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) {
throw "DSView.exe not found in '$targetRoot'. Pass -DsViewPath with the installed DSView directory."
}
if (-not (Test-Path -LiteralPath (Join-Path $decoderRoot 'common') -PathType Container)) {
throw "DSView decoders/common is missing. Select a complete DSView installation."
}
# An explicit allowlist excludes tests, bytecode and unrelated modules.
# The private common/setgui_decoders layout matches the embedded build.
$files = @()
foreach ($name in @('gate_driver_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart')) {
foreach ($leaf in @('__init__.py', 'pd.py')) {
$files += [pscustomobject]@{
Source = Join-Path $PSScriptRoot "decoders/$name/$leaf"
Relative = "$name/$leaf"
}
}
}
foreach ($name in @('__init__', 'gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart')) {
$files += [pscustomobject]@{
Source = Join-Path $templateRoot "python/logic_analyzer/decoders/$name.py"
Relative = "common/setgui_decoders/$name.py"
}
}
# Validate the entire payload and destination before changing any decoder.
foreach ($file in $files) {
if (-not (Test-Path -LiteralPath $file.Source -PathType Leaf)) {
throw "Missing templates source: $($file.Source). Initialize the templates submodule."
}
$destination = Join-Path $decoderRoot $file.Relative
Assert-PlainPath $destination
if (Test-Path -LiteralPath $destination -PathType Container) {
throw "Expected a file, found a directory: $destination"
}
if ([IO.Path]::GetFullPath($file.Source) -eq [IO.Path]::GetFullPath($destination)) {
throw 'The installation destination must differ from the templates source.'
}
}
# Read-only validation does not create backups or check elevated write access.
# Real installation performs its own write-access check before copying.
if ($CheckOnly) {
Write-Output "Validated $($files.Count) files for $targetRoot. No files changed."
return
}
# A mapped/running Windows EXE cannot be opened for writing. Never terminate DSView.
try {
$handle = [IO.File]::Open($exe, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
$handle.Dispose()
} catch {
throw "Close DSView and check write permissions for '$targetRoot'. For Program Files, run PowerShell as administrator. $($_.Exception.Message)"
}
# Keep each previous version outside decoder discovery directories.
# Retain backups after success for manual recovery, not only after errors.
$backup = Join-Path $targetRoot ('set-decoder-backups/' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N'))
Assert-PlainPath $backup
# Back up all existing files before writing the first replacement.
foreach ($file in $files) {
$destination = Join-Path $decoderRoot $file.Relative
if (Test-Path -LiteralPath $destination -PathType Leaf) {
$saved = Join-Path $backup $file.Relative
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($saved)) | Out-Null
Copy-Item -LiteralPath $destination -Destination $saved
}
}
# Track files before copying: a failed copy may leave a partial destination.
# Rollback must include that file as well as completed replacements.
$attempted = @()
try {
foreach ($file in $files) {
$destination = Join-Path $decoderRoot $file.Relative
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
$attempted += $file
Copy-Item -LiteralPath $file.Source -Destination $destination -Force
if ((Get-ContentHash $file.Source) -ne (Get-ContentHash $destination)) {
throw "Copy verification failed: $destination"
}
}
} catch {
$failure = $_
# Restore overwritten files and remove only new files from our allowlist.
# Never recursively delete decoder folders or remove unrelated analyzers.
foreach ($file in $attempted) {
$destination = Join-Path $decoderRoot $file.Relative
$saved = Join-Path $backup $file.Relative
try {
if (Test-Path -LiteralPath $saved -PathType Leaf) {
Copy-Item -LiteralPath $saved -Destination $destination -Force
} elseif (Test-Path -LiteralPath $destination -PathType Leaf) {
Remove-Item -LiteralPath $destination -Force
}
} catch {
Write-Warning "Could not restore $destination. Retained backup: $backup"
}
}
throw $failure
}
Write-Output "Installed: IGBT 1SP/1SD, SET UART, SET CAN, PM35 UART in $decoderRoot"
if (Test-Path -LiteralPath $backup) { Write-Output "Previous files retained in: $backup" }
Write-Output 'Restart DSView. Add UART/CAN first, then stack the corresponding SET decoder. No external Python is required.'

View File

@@ -0,0 +1,29 @@
#pragma once
#include <atomic>
#include <cstdint>
// Annotation IDs are the public gate_driver_timing decoder contract.
// Independent of row visibility and text search; one summary per decode run.
class GateSummary {
public:
void reset() {
controls = 0; acknowledgments = 0; warnings = 0; faults = 0;
}
void observe(int annotation) {
switch (annotation) {
case 0: ++controls; break;
case 2: ++acknowledgments; break;
case 3: ++warnings; break;
case 4: case 5: ++faults; break;
}
}
// 0 = OK, 1 = WARNING / incomplete, 2 = FAULT.
int severity(bool complete) const {
if (faults.load()) return 2;
if (!complete || warnings.load() || !acknowledgments.load() ||
controls.load() != acknowledgments.load()) return 1;
return 0;
}
private:
std::atomic<uint64_t> controls{0}, acknowledgments{0}, warnings{0}, faults{0};
};

View File

@@ -0,0 +1,1686 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cc82669f..575eb851 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -190,6 +190,10 @@ if(Qt5Core_FOUND)
#set(QT_LIBRARIES Qt5::Gui Qt5::Widgets Qt5::WinExtras)
set(QT_INCLUDE_DIRS ${Qt5Gui_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS})
set(QT_LIBRARIES Qt5::Gui Qt5::Widgets)
+ if(WIN32)
+ find_package(Qt5WinExtras REQUIRED)
+ list(APPEND QT_LIBRARIES Qt5::WinExtras)
+ endif()
add_definitions(${Qt5Gui_DEFINITIONS} ${Qt5Widgets_DEFINITIONS})
else()
find_package(Qt6Core QUIET)
@@ -551,10 +555,13 @@ set(DSView_RESOURCES
if(WIN32)
# Use the DSView icon for the DSView.exe executable.
+ add_definitions(-DUNICODE -D_UNICODE)
set(CMAKE_RC_COMPILE_OBJECT "${CMAKE_RC_COMPILER} -O coff -I${CMAKE_CURRENT_SOURCE_DIR} <SOURCE> <OBJECT>")
enable_language(RC)
# app icon
list(APPEND DSView_SOURCES applogo.rc)
+ list(APPEND DSView_SOURCES DSView/pv/winnativewidget.cpp DSView/pv/winshadow.cpp)
+ list(APPEND DSView_HEADERS DSView/pv/winshadow.h)
endif()
if(Qt5Core_FOUND)
diff --git a/DSView/main.cpp b/DSView/main.cpp
index 748a59d9..58d2d155 100644
--- a/DSView/main.cpp
+++ b/DSView/main.cpp
@@ -64,6 +64,7 @@ int main(int argc, char *argv[])
const char *open_file = NULL;
int logLevel = -1;
bool bStoreLog = false;
+ quintptr embedParent = 0;
//----------------------rebuild command param
#ifdef _WIN32
@@ -103,6 +104,8 @@ int main(int argc, char *argv[])
{"version", no_argument, 0, 'v'},
{"storelog", no_argument, 0, 's'},
{"help", no_argument, 0, 'h'},
+ {"embed-parent", required_argument, 0, 1000},
+ {"host-version", no_argument, 0, 1001},
{0, 0, 0, 0}
};
@@ -113,6 +116,20 @@ int main(int argc, char *argv[])
switch (c)
{
+ case 1000: {
+ bool ok = false;
+ embedParent = QString::fromUtf8(optarg).toULongLong(&ok, 0);
+#ifdef _WIN32
+ if (!ok || !embedParent || !IsWindow(reinterpret_cast<HWND>(embedParent)))
+#else
+ if (true)
+#endif
+ return 2;
+ break;
+ }
+ case 1001:
+ printf("DSView host protocol 1\n");
+ return 0;
case 'l': // log level
logLevel = atoi(optarg);
break;
@@ -174,6 +191,14 @@ bool bHighScale = true;
QApplication::setApplicationName("DSView");
QApplication::setOrganizationName("DreamSourceLab");
QApplication::setOrganizationDomain("www.DreamSourceLab.com");
+ if (embedParent) {
+ QApplication::setOrganizationName("SET");
+ QApplication::setApplicationName("SETGUI-DSView");
+ a.setProperty("embeddedHost", true);
+ // The native child uses Qt::Tool and is not a primary Qt window.
+ // Closing a modal dialog must not end the hosted application's loop.
+ a.setQuitOnLastWindowClosed(false);
+ }
//----------------------init log
dsv_log_init(); // Don't call before QApplication be inited
@@ -199,6 +224,17 @@ bool bHighScale = true;
AppControl *control = AppControl::Instance();
AppConfig &app = AppConfig::Instance();
app.LoadAll(); //load app config
+ if (embedParent) {
+ app.frameOptions.style = "dark";
+ app.appOptions.fontSize = 10;
+ QString captureDir = QString::fromUtf8(qgetenv("DSVIEW_CAPTURE_DIR"));
+ if (QDir(captureDir).exists() && !captureDir.isEmpty()) {
+ app.userHistory.saveDir = captureDir;
+ app.userHistory.openDir = captureDir;
+ app.userHistory.exportDir = captureDir;
+ app.userHistory.screenShotPath = captureDir;
+ }
+ }
LangResource::Instance()->Load(app.frameOptions.language);
if (app.appOptions.ableSaveLog){
@@ -237,8 +273,23 @@ bool bHighScale = true;
{
pv::MainFrame w;
control->Start();
- w.ShowFormInit();
- w.ShowHelpDocAsync(); //to show the dailog for open help document
+#ifdef _WIN32
+ if (embedParent) {
+ if (!w.ShowEmbedded(embedParent)) return 2;
+ printf("DSVIEW_READY %llu\n", static_cast<unsigned long long>(w.winId()));
+ printf("DSVIEW_CAPABILITIES OPEN_FILE\n");
+ fflush(stdout);
+ // Optional render artifact for integration smoke tests.
+ const QString screenshot = QString::fromUtf8(qgetenv("DSVIEW_HOST_SCREENSHOT"));
+ if (!screenshot.isEmpty()) QTimer::singleShot(2500, &w, [&w, screenshot]() {
+ w.grab().save(screenshot);
+ });
+ } else
+#endif
+ {
+ w.ShowFormInit();
+ w.ShowHelpDocAsync();
+ }
ret = a.exec(); //Run the application
control->Stop();
diff --git a/DSView/pv/config/appconfig.cpp b/DSView/pv/config/appconfig.cpp
index 1e318306..b4a7f5ea 100644
--- a/DSView/pv/config/appconfig.cpp
+++ b/DSView/pv/config/appconfig.cpp
@@ -438,8 +438,9 @@ bool AppConfig::IsDarkStyle()
return false;
}
-QColor AppConfig::GetStyleColor()
-{
+QColor AppConfig::GetStyleColor()
+{
+ if (qApp->property("embeddedHost").toBool()) return QColor("#10151d");
if (IsDarkStyle()){
return QColor(38, 38, 38);
}
@@ -551,4 +552,4 @@ QString GetProfileDir()
#else
return QStandardPaths::writableLocation(QStandardPaths::DataLocation);
#endif
-}
\ No newline at end of file
+}
diff --git a/DSView/pv/data/decode/annotation.cpp b/DSView/pv/data/decode/annotation.cpp
index f285247c..7da1849c 100644
--- a/DSView/pv/data/decode/annotation.cpp
+++ b/DSView/pv/data/decode/annotation.cpp
@@ -179,6 +179,16 @@ const std::vector<QString>& Annotation::annotations() const
return resItem.cvt_lines;
}
+bool Annotation::contains_text(const QString &text) const
+{
+ // The waveform can display any of these labels, not just the longest one.
+ for (const auto &label : annotations()) {
+ if (label.contains(text, Qt::CaseInsensitive))
+ return true;
+ }
+ return false;
+}
+
bool Annotation::is_numberic()
{
AnnotationSourceItem *resItem = _status->m_resTable.GetItem(_resIndex);
diff --git a/DSView/pv/data/decode/annotation.h b/DSView/pv/data/decode/annotation.h
index a5b0e440..06067395 100644
--- a/DSView/pv/data/decode/annotation.h
+++ b/DSView/pv/data/decode/annotation.h
@@ -66,6 +66,8 @@ public:
const std::vector<QString>& annotations() const;
+ bool contains_text(const QString &text) const;
+
private:
uint64_t _start_sample;
uint64_t _end_sample;
diff --git a/DSView/pv/data/decodermodel.cpp b/DSView/pv/data/decodermodel.cpp
index 61710e54..ee814a76 100644
--- a/DSView/pv/data/decodermodel.cpp
+++ b/DSView/pv/data/decodermodel.cpp
@@ -32,6 +32,58 @@ using namespace std;
namespace pv {
namespace data {
+AnnotationMatchModel::AnnotationMatchModel(QAbstractItemModel *source, QObject *parent)
+ : QAbstractTableModel(parent), _source(source)
+{
+ connect(source, &QAbstractItemModel::modelAboutToBeReset, this, [this]() {
+ setMatches({}, QString());
+ });
+}
+
+int AnnotationMatchModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : int(_matches.size());
+}
+
+int AnnotationMatchModel::columnCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : 2;
+}
+
+QModelIndex AnnotationMatchModel::sourceIndex(int row) const
+{
+ return _source && row >= 0 && row < int(_matches.size()) ? _matches[row] : QModelIndex();
+}
+
+void AnnotationMatchModel::setMatches(const std::vector<QModelIndex> &matches, const QString &term)
+{
+ beginResetModel();
+ _matches = matches;
+ _term = term;
+ endResetModel();
+}
+
+QVariant AnnotationMatchModel::data(const QModelIndex &index, int role) const
+{
+ const QModelIndex source = sourceIndex(index.row());
+ if (!index.isValid() || !source.isValid()) return QVariant();
+ if (role == Qt::DisplayRole && index.column() == 0)
+ return _source->headerData(source.column(), Qt::Horizontal, role);
+ if (role == Qt::DisplayRole) {
+ const auto labels = source.data(Qt::ToolTipRole).toString().split('\n');
+ for (const auto &label : labels)
+ if (label.contains(_term, Qt::CaseInsensitive)) return label;
+ }
+ return source.data(role);
+}
+
+QVariant AnnotationMatchModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (role != Qt::DisplayRole) return QVariant();
+ if (orientation == Qt::Vertical) return section + 1;
+ return section == 0 ? tr("Annotation") : tr("Match");
+}
+
DecoderModel::DecoderModel(QObject *parent)
: QAbstractTableModel(parent),
_decoder_stack(NULL)
@@ -68,17 +120,51 @@ QVariant DecoderModel::data(const QModelIndex &index, int role) const
if (role == Qt::TextAlignmentRole) {
return int(Qt::AlignLeft | Qt::AlignVCenter);
}
- else if (role == Qt::DisplayRole) {
+ else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
if (_decoder_stack) {
pv::data::decode::Annotation ann;
if (_decoder_stack->list_annotation(&ann, index.column(), index.row())) {
- return ann.annotations().at(0);
+ const auto &labels = ann.annotations();
+ if (role == Qt::DisplayRole)
+ return labels.empty() ? QVariant() : QVariant(labels.front());
+ QStringList text;
+ for (const auto &label : labels)
+ if (!text.contains(label)) text.append(label);
+ return text.join("\n");
}
}
}
return QVariant();
}
+bool DecoderModel::annotation_matches(int column, int row, const QStringList &terms) const
+{
+ if (!_decoder_stack || terms.isEmpty() || terms.first().isEmpty())
+ return false;
+ decode::Annotation ann;
+ if (!_decoder_stack->list_annotation(&ann, column, row) ||
+ !ann.contains_text(terms.first()))
+ return false;
+
+ if (terms.size() == 1)
+ return true;
+
+ // Retain the numeric AA-BB-CC sequence search in each individual column.
+ // Text containing a hyphen is handled as one literal term by the caller.
+ if (!ann.is_numberic())
+ return false;
+ uint64_t next_row = uint64_t(row) + 1;
+ for (int term = 1; term < terms.size(); ++term) {
+ do {
+ if (!_decoder_stack->list_annotation(&ann, column, next_row++))
+ return false;
+ } while (!ann.is_numberic());
+ if (!ann.contains_text(terms.at(term)))
+ return false;
+ }
+ return true;
+}
+
QVariant DecoderModel::headerData(int section,
Qt::Orientation orientation,
int role) const
diff --git a/DSView/pv/data/decodermodel.h b/DSView/pv/data/decodermodel.h
index d35849f4..f2f15887 100644
--- a/DSView/pv/data/decodermodel.h
+++ b/DSView/pv/data/decodermodel.h
@@ -23,6 +23,8 @@
#define DSVIEW_PV_DATA_DECODERMODEL_H
#include <QAbstractTableModel>
+#include <QStringList>
+#include <QPointer>
#include "decode/rowdata.h"
@@ -31,6 +33,24 @@ namespace data {
class DecoderStack;
+// One matched annotation per row; columns in DecoderModel have independent
+// timelines, so filtering its row numbers would also expose unrelated events.
+class AnnotationMatchModel : public QAbstractTableModel
+{
+public:
+ AnnotationMatchModel(QAbstractItemModel *source, QObject *parent);
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ void setMatches(const std::vector<QModelIndex> &matches, const QString &term);
+ QModelIndex sourceIndex(int row) const;
+private:
+ QPointer<QAbstractItemModel> _source;
+ std::vector<QModelIndex> _matches;
+ QString _term;
+};
+
namespace decode {
class Annotation;
class Decoder;
@@ -47,6 +67,8 @@ public:
QVariant data(const QModelIndex &index, int role) const;
QVariant headerData(int section, Qt::Orientation orientation,int role) const;
+ bool annotation_matches(int column, int row, const QStringList &terms) const;
+
void setDecoderStack(DecoderStack *decoder_stack);
inline DecoderStack* getDecoderStack(){
diff --git a/DSView/pv/data/decoderstack.cpp b/DSView/pv/data/decoderstack.cpp
index cb6b0b9a..ca46182f 100644
--- a/DSView/pv/data/decoderstack.cpp
+++ b/DSView/pv/data/decoderstack.cpp
@@ -168,8 +168,7 @@ void DecoderStack::build_row()
std::map<const decode::Row, bool>::const_iterator iter = _rows_gshow.find(row);
if (iter == _rows_gshow.end()) {
_rows_gshow[row] = true;
- if (row.title().contains("bit", Qt::CaseInsensitive) ||
- row.title().contains("warning", Qt::CaseInsensitive)) {
+ if (row.title().contains("bit", Qt::CaseInsensitive)) {
_rows_lshow[row] = false;
} else {
_rows_lshow[row] = true;
@@ -192,8 +191,7 @@ void DecoderStack::build_row()
std::map<const decode::Row, bool>::const_iterator iter = _rows_gshow.find(row);
if (iter == _rows_gshow.end()) {
_rows_gshow[row] = true;
- if (row.title().contains("bit", Qt::CaseInsensitive) ||
- row.title().contains("warning", Qt::CaseInsensitive)) {
+ if (row.title().contains("bit", Qt::CaseInsensitive)) {
_rows_lshow[row] = false;
} else {
_rows_lshow[row] = true;
diff --git a/DSView/pv/dialogs/deviceoptions.cpp b/DSView/pv/dialogs/deviceoptions.cpp
index 7d4ae1c4..efe19693 100644
--- a/DSView/pv/dialogs/deviceoptions.cpp
+++ b/DSView/pv/dialogs/deviceoptions.cpp
@@ -392,18 +392,15 @@ void DeviceOptions::logic_probes(QVBoxLayout &layout)
QPushButton *enable_all_probes = new QPushButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_ENABLE_ALL), "Enable All"));
QPushButton *disable_all_probes = new QPushButton(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_DISABLE_ALL), "Disable All"));
- enable_all_probes->setMaximumHeight(33);
- disable_all_probes->setMaximumHeight(33);
+ // Let Qt include the full label and themed padding in the preferred size.
+ enable_all_probes->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ disable_all_probes->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
enable_all_probes->setFont(font);
- disable_all_probes->setFont(font);
-
- int bt_width = enable_all_probes->fontMetrics().horizontalAdvance(enable_all_probes->text()) + 20;
- enable_all_probes->setMaximumWidth(bt_width);
- disable_all_probes->setMaximumWidth(bt_width);
-
- this->update_font();
-
- contentHeight += enable_all_probes->sizeHint().height();
+ disable_all_probes->setFont(font);
+
+ this->update_font();
+
+ contentHeight += qMax(enable_all_probes->sizeHint().height(), disable_all_probes->sizeHint().height());
contentHeight += channel_line_height * row2 + 50;
connect(enable_all_probes, SIGNAL(clicked()),
diff --git a/DSView/pv/dock/keywordlineedit.cpp b/DSView/pv/dock/keywordlineedit.cpp
index 2c2e4722..9a33e11a 100644
--- a/DSView/pv/dock/keywordlineedit.cpp
+++ b/DSView/pv/dock/keywordlineedit.cpp
@@ -353,6 +353,10 @@ void PopupLineEdit::showPupopInput()
_popup_input = input;
connect(input, SIGNAL(sig_inputEnd(QString)), this, SLOT(onPopupInputEditEnd(QString)));
+ // The popup owns keyboard input on Windows. Forward Enter after it commits
+ // the text and closes, so actions connected to the outer editor still run.
+ connect(line, &QLineEdit::returnPressed, this, &QLineEdit::returnPressed,
+ Qt::QueuedConnection);
if (_is_number_mode){
connect(line, SIGNAL(valueChanged(int)), this, SLOT(onPopupInputValueChanged(int)));
diff --git a/DSView/pv/dock/protocoldock.cpp b/DSView/pv/dock/protocoldock.cpp
index 648eb5b8..1a023707 100644
--- a/DSView/pv/dock/protocoldock.cpp
+++ b/DSView/pv/dock/protocoldock.cpp
@@ -37,14 +37,13 @@
#include <QHeaderView>
#include <QScrollBar>
#include <QRegularExpression>
-#include <QFuture>
-#include <QProgressDialog>
-#include <QtConcurrent/QtConcurrent>
+#include <QElapsedTimer>
#include <QSizePolicy>
#include <assert.h>
#include <map>
#include <string>
#include <algorithm>
+#include <limits>
#include <QTableWidgetItem>
#include <QHeaderView>
#include "../ui/msgbox.h"
@@ -56,6 +55,11 @@
#include "../ui/langresource.h"
#include "../appcontrol.h"
#include "../ui/fn.h"
+#include "../dialogs/decoderoptionsdlg.h"
+#include "../dialogs/dsmessagebox.h"
+#include <QApplication>
+#include <QFile>
+#include <QTimer>
using namespace std;
@@ -69,9 +73,10 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio
_view(view)
{
_session = session;
- _cur_search_index = -1;
- _search_edited = false;
_pro_add_button = NULL;
+ _search_timer = new QTimer(this);
+ _search_timer->setSingleShot(true);
+ connect(_search_timer, &QTimer::timeout, this, &ProtocolDock::search_step);
//-----------------------------get protocol list
GSList *l = const_cast<GSList*>(srd_decoder_list());
@@ -160,7 +165,6 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio
_ann_search_edit = new PopupLineEdit(bot_panel);
_ann_search_button->setFixedWidth(_ann_search_button->height());
- _ann_search_button->setDisabled(true);
QHBoxLayout *ann_search_layout = new QHBoxLayout();
ann_search_layout->setSpacing(2);
@@ -170,6 +174,7 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio
ann_search_layout->addWidget(_nxt_button);
_table_view = new QTableView(bot_panel);
+ _match_model = new data::AnnotationMatchModel(_session->get_decoder_model(), this);
_table_view->setModel(_session->get_decoder_model());
_table_view->setAlternatingRowColors(true);
_table_view->setShowGrid(false);
@@ -191,12 +196,32 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio
bot_layout->addWidget(_table_view);
bot_panel->setLayout(bot_layout);
+ // Keep decoder rows scrollable so their combined minimum height does not
+ // prevent the user from dragging the divider upwards.
+ auto top_scroll = new QScrollArea(this);
+ top_scroll->setFrameShape(QFrame::NoFrame);
+ top_scroll->setWidgetResizable(true);
+ top_scroll->setMinimumHeight(70);
+ top_scroll->setWidget(top_panel);
+
QSplitter *split_widget = new QSplitter(this);
- split_widget->insertWidget(0, top_panel);
+ split_widget->insertWidget(0, top_scroll);
split_widget->insertWidget(1, bot_panel);
split_widget->setOrientation(Qt::Vertical);
split_widget->setCollapsible(0, false);
split_widget->setCollapsible(1, false);
+ split_widget->setHandleWidth(10);
+ split_widget->setStretchFactor(0, 0);
+ split_widget->setStretchFactor(1, 1);
+ split_widget->setSizes({180, 420});
+ split_widget->handle(1)->setCursor(Qt::SizeVerCursor);
+ split_widget->handle(1)->setToolTip(tr("Drag up or down to resize Decoders and Decoding Results"));
+ if (qApp->property("embeddedHost").toBool()) {
+ split_widget->setStyleSheet(
+ "QSplitter#protocolWidget::handle:vertical { background: #243247; "
+ "border-top: 1px solid #344762; border-bottom: 1px solid #344762; }"
+ "QSplitter#protocolWidget::handle:vertical:hover { background: #1570d8; }");
+ }
this->setWidgetResizable(true);
this->setWidget(split_widget);
@@ -207,6 +232,7 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio
connect(_bot_set_button, SIGNAL(clicked()),this, SLOT(set_model()));
connect(_pre_button, SIGNAL(clicked()),this, SLOT(search_pre()));
connect(_nxt_button, SIGNAL(clicked()),this, SLOT(search_nxt()));
+ connect(_ann_search_button, SIGNAL(clicked()),this, SLOT(search_nxt()));
connect(_pro_add_button, SIGNAL(clicked()),this, SLOT(on_add_protocol()));
connect(_del_all_button, SIGNAL(clicked()),this, SLOT(on_del_all_protocol()));
@@ -218,7 +244,12 @@ ProtocolDock::ProtocolDock(QWidget *parent, view::View &view, SigSession *sessio
connect(_pro_search_button, SIGNAL(clicked()), this, SLOT(show_protocol_select()));
- connect(_ann_search_edit, SIGNAL(editingFinished()), this, SLOT(search_changed()));
+ connect(_ann_search_edit, &QLineEdit::textChanged, this, &ProtocolDock::search_changed);
+ connect(_ann_search_edit, &QLineEdit::returnPressed, this, &ProtocolDock::search_nxt);
+ connect(_session->get_decoder_model(), &QAbstractItemModel::modelAboutToBeReset,
+ this, &ProtocolDock::invalidate_search);
+ connect(_session->get_decoder_model(), &QAbstractItemModel::modelReset,
+ this, &ProtocolDock::search_done);
ADD_UI(this);
@@ -241,6 +272,12 @@ ProtocolDock::~ProtocolDock()
void ProtocolDock::retranslateUi()
{
_ann_search_edit->setPlaceholderText(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SEARCH), "search"));
+ _ann_search_edit->setToolTip(tr("Search all enabled result columns, including short waveform labels.\n"
+ "Case insensitive. Enter or the magnifier: next match.\n"
+ "Numeric sequence: AA-BB-CC."));
+ _ann_search_button->setToolTip(tr("Find next (Enter)"));
+ _pre_button->setToolTip(tr("Previous match"));
+ _nxt_button->setToolTip(tr("Next match"));
_matchs_title_label->setText(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_MATCHING_ITEMS), "Matching Items:"));
_bot_title_label->setText(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_PROTOCOL_LIST_VIEWER), "Protocol List Viewer"));
_pro_keyword_edit->ResetText();
@@ -290,6 +327,89 @@ int ProtocolDock::get_protocol_index_by_id(QString id)
return -1;
}
+bool ProtocolDock::add_host_decoder(const QString &id)
+{
+ if (_session->is_working() || _session->get_device()->get_work_mode() != LOGIC ||
+ QApplication::activeModalWidget() || get_protocol_index_by_id(id) < 0)
+ return false;
+ _selected_protocol_id = id;
+ const size_t count = _protocol_lay_items.size();
+ on_add_protocol();
+ return _protocol_lay_items.size() > count;
+}
+
+// Opt-in integration check used by SETGUI --check-dsview. Exercise the same
+// modal dialogs and removal paths as the right-hand decoder panel.
+void ProtocolDock::check_host_decoders()
+{
+ const QString report = QString::fromUtf8(qgetenv("DSVIEW_HOST_CHECK"));
+ if (report.isEmpty()) return;
+ auto log = [report](const QString &message) {
+ QFile file(report);
+ if (file.open(QIODevice::WriteOnly | QIODevice::Append))
+ file.write((message + "\n").toUtf8());
+ };
+ auto timer = new QTimer(this);
+ int phase = 0;
+ connect(timer, &QTimer::timeout, this, [this, timer, phase, log, report]() mutable {
+ bool ok = true;
+ auto answer = [this](QMessageBox::StandardButton button) {
+ QTimer::singleShot(80, this, [button]() {
+ auto dialog = qobject_cast<dialogs::DSMessageBox*>(QApplication::activeModalWidget());
+ if (dialog && dialog->mBox()->button(button))
+ dialog->mBox()->button(button)->click();
+ });
+ };
+ if (phase < 4) {
+ const auto before = _protocol_lay_items.size();
+ if (before == 0) ok = false;
+ else {
+ answer(phase % 2 ? QMessageBox::Yes : QMessageBox::No);
+ if (phase < 2) OnProtocolDelete(_protocol_lay_items.front());
+ else on_del_all_protocol();
+ const auto after = _protocol_lay_items.size();
+ ok = phase % 2 == 0 ? after == before : after == (phase == 1 ? before - 1 : 0);
+ }
+ } else if (phase < 8) {
+ const QString id = QStringList({"gate_driver_timing", "set_uart", "set_can", "pm35_uart"})[phase - 4];
+ if (get_protocol_index_by_id(id) < 0) ok = false;
+ else {
+ QTimer::singleShot(80, this, [report, id]() {
+ auto dialog = qobject_cast<dialogs::DecoderOptionsDlg*>(QApplication::activeModalWidget());
+ if (!dialog) return;
+ int channel = 1;
+ for (auto combo : dialog->findChildren<QComboBox*>()) {
+ if (combo->count() > 1 && combo->itemText(0) == "-" && combo->itemData(0).toInt() == -1)
+ combo->setCurrentIndex(qMin(channel++, combo->count() - 1));
+ }
+ dialog->grab().save(report + "." + id + ".png");
+ QMetaObject::invokeMethod(dialog, "on_accept", Qt::DirectConnection);
+ });
+ ok = add_host_decoder(id);
+ if (ok) {
+ auto trace = _session->get_decode_signals().back();
+ ok = QString(trace->decoder()->stack().back()->decoder()->id) == id;
+ }
+ }
+ } else if (phase == 8) {
+ answer(QMessageBox::Yes);
+ on_del_all_protocol();
+ ok = _protocol_lay_items.empty() && _session->get_decode_signals().empty();
+ } else {
+ log("PASS");
+ timer->stop();
+ timer->deleteLater();
+ return;
+ }
+ log(QString("phase %1: %2").arg(phase++).arg(ok ? "ok" : "FAIL"));
+ if (!ok) {
+ timer->stop();
+ timer->deleteLater();
+ }
+ });
+ timer->start(350);
+}
+
void ProtocolDock::on_add_protocol()
{
if (_decoderInfoList.size() == 0){
@@ -471,7 +591,7 @@ void ProtocolDock::decoded_progress(int progress)
index++;
}
- if (progress == 0 || progress % 10 == 1){
+ if (progress == 0 || progress == 100 || progress % 10 == 1){
update_model();
}
}
@@ -481,8 +601,6 @@ void ProtocolDock::set_model()
pv::dialogs::ProtocolList *protocollist_dlg = new pv::dialogs::ProtocolList(this, _session);
protocollist_dlg->exec();
resize_table_view(_session->get_decoder_model());
- _model_proxy.setSourceModel(_session->get_decoder_model());
- search_done();
// clear mark_index of all DecoderStacks
const auto &decode_sigs = _session->get_decode_signals();
@@ -513,15 +631,13 @@ void ProtocolDock::update_model()
if (index >= decode_sigs.size())
decoder_model->setDecoderStack(decode_sigs.at(0)->decoder());
}
- _model_proxy.setSourceModel(decoder_model);
- search_done();
resize_table_view(decoder_model);
}
void ProtocolDock::resize_table_view(data::DecoderModel* decoder_model)
{
if (decoder_model->getDecoderStack()) {
- for (int i = 0; i < decoder_model->columnCount(QModelIndex()) - 1; i++) {
+ for (int i = 0; i < _table_view->model()->columnCount(QModelIndex()) - 1; i++) {
_table_view->resizeColumnToContents(i);
if (_table_view->columnWidth(i) > 200)
_table_view->setColumnWidth(i, 200);
@@ -535,8 +651,11 @@ void ProtocolDock::resize_table_view(data::DecoderModel* decoder_model)
}
}
-void ProtocolDock::item_clicked(const QModelIndex &index)
+void ProtocolDock::item_clicked(const QModelIndex &table_index)
{
+ const QModelIndex index = table_index.model() == _match_model
+ ? _match_model->sourceIndex(table_index.row()) : table_index;
+ if (!index.isValid()) return;
pv::data::DecoderModel *decoder_model = _session->get_decoder_model();
auto decoder_stack = decoder_model->getDecoderStack();
@@ -555,41 +674,16 @@ void ProtocolDock::item_clicked(const QModelIndex &index)
}
}
- _table_view->resizeRowToContents(index.row());
- if (index.column() != _model_proxy.filterKeyColumn()) {
- _model_proxy.setFilterKeyColumn(index.column());
- _model_proxy.setSourceModel(decoder_model);
- search_done();
- }
- QModelIndex filterIndex = _model_proxy.mapFromSource(index);
- if (filterIndex.isValid()) {
- _cur_search_index = filterIndex.row();
- } else {
- if (_model_proxy.rowCount() == 0) {
- _cur_search_index = -1;
- } else {
- uint64_t up = 0;
- uint64_t dn = _model_proxy.rowCount() - 1;
- do {
- uint64_t md = (up + dn)/2;
- QModelIndex curIndex = _model_proxy.mapToSource(_model_proxy.index(md,_model_proxy.filterKeyColumn()));
- if (index.row() == curIndex.row()) {
- _cur_search_index = md;
- break;
- } else if (md == up) {
- if (curIndex.row() < index.row() && up < dn) {
- QModelIndex nxtIndex = _model_proxy.mapToSource(_model_proxy.index(md+1,_model_proxy.filterKeyColumn()));
- if (nxtIndex.row() < index.row())
- md++;
- }
- _cur_search_index = md + ((curIndex.row() < index.row()) ? 0.5 : -0.5);
- break;
- } else if (curIndex.row() < index.row()) {
- up = md;
- } else if (curIndex.row() > index.row()) {
- dn = md;
- }
- }while(1);
+ _table_view->resizeRowToContents(table_index.row());
+ // A table click must not silently restrict subsequent searches to this column.
+ if (_search_index >= 0 && _search_index < int(_search_matches.size()) &&
+ _search_matches[_search_index].index == index)
+ return;
+ _search_index = -1;
+ for (size_t i = 0; i < _search_matches.size(); ++i) {
+ if (_search_matches[i].index == index) {
+ _search_index = int(i);
+ break;
}
}
}
@@ -618,6 +712,21 @@ void ProtocolDock::export_table_view()
void ProtocolDock::nav_table_view()
{
+ if (_table_view->model() == _match_model) {
+ if (_search_running || _search_matches.empty()) return;
+ auto stack = _session->get_decoder_model()->getDecoderStack();
+ if (!stack) return;
+ const uint64_t offset = _view.offset() * (stack->samplerate() * _view.scale());
+ auto match = std::lower_bound(_search_matches.begin(), _search_matches.end(), offset,
+ [](const SearchMatch &item, uint64_t sample) { return item.sample < sample; });
+ _search_index = match == _search_matches.end() ? int(_search_matches.size()) - 1
+ : int(match - _search_matches.begin());
+ const QModelIndex index = _match_model->index(_search_index, 1);
+ _table_view->setCurrentIndex(index);
+ _table_view->scrollTo(index);
+ item_clicked(index);
+ return;
+ }
uint64_t row_index = 0;
pv::data::DecoderModel *decoder_model = _session->get_decoder_model();
@@ -625,7 +734,8 @@ void ProtocolDock::nav_table_view()
if (decoder_stack) {
uint64_t offset = _view.offset() * (decoder_stack->samplerate() * _view.scale());
std::map<const pv::data::decode::Row, bool> rows = decoder_stack->get_rows_lshow();
- int column = _model_proxy.filterKeyColumn();
+ const int selected_column = qMax(0, _table_view->currentIndex().column());
+ int column = selected_column;
for (std::map<const pv::data::decode::Row, bool>::const_iterator i = rows.begin();
i != rows.end(); i++) {
if ((*i).second && column-- == 0) {
@@ -633,7 +743,7 @@ void ProtocolDock::nav_table_view()
break;
}
}
- QModelIndex index = _model_proxy.mapToSource(_model_proxy.index(row_index, _model_proxy.filterKeyColumn()));
+ QModelIndex index = decoder_model->index(row_index, selected_column);
if(index.isValid()){
@@ -660,196 +770,141 @@ void ProtocolDock::nav_table_view()
void ProtocolDock::search_pre()
{
- search_update();
- // now the proxy only contains rows that match the name
- // let's take the pre one and map it to the original model
- if (_model_proxy.rowCount() == 0) {
- _table_view->scrollToTop();
- _table_view->clearSelection();
- _matchs_label->setText(QString::number(0));
- _cur_search_index = -1;
- return;
- }
- int i = 0;
- uint64_t rowCount = _model_proxy.rowCount();
- QModelIndex matchingIndex;
- pv::data::DecoderModel *decoder_model = _session->get_decoder_model();
-
- auto decoder_stack = decoder_model->getDecoderStack();
- do {
- _cur_search_index--;
- if (_cur_search_index <= -1 || _cur_search_index >= _model_proxy.rowCount())
- _cur_search_index = _model_proxy.rowCount() - 1;
-
- matchingIndex = _model_proxy.mapToSource(_model_proxy.index(ceil(_cur_search_index),_model_proxy.filterKeyColumn()));
- if (!decoder_stack || !matchingIndex.isValid())
- break;
- i = 1;
- uint64_t row = matchingIndex.row() + 1;
- uint64_t col = matchingIndex.column();
- pv::data::decode::Annotation ann;
- bool ann_valid = false;
-
- while(i < _str_list.size()) {
- QString nxt = _str_list.at(i);
-
- do {
- ann_valid = decoder_stack->list_annotation(&ann, col, row);
- row++;
- }
- while(ann_valid && !ann.is_numberic());
-
- if (ann_valid){
- QString source = ann.annotations().at(0);
- if (source.contains(nxt))
- i++;
- else
- break;
- }
- else{
- break;
- }
- }
- }
- while(i < _str_list.size() && --rowCount);
-
- if(i >= _str_list.size() && matchingIndex.isValid()){
- _table_view->scrollTo(matchingIndex);
- _table_view->setCurrentIndex(matchingIndex);
- _table_view->clicked(matchingIndex);
- } else {
- _table_view->scrollToTop();
- _table_view->clearSelection();
- _matchs_label->setText(QString::number(0));
- _cur_search_index = -1;
- }
+ navigate_search(-1);
}
void ProtocolDock::search_nxt()
{
- search_update();
- // now the proxy only contains rows that match the name
- // let's take the pre one and map it to the original model
- if (_model_proxy.rowCount() == 0) {
- _table_view->scrollToTop();
- _table_view->clearSelection();
- _matchs_label->setText(QString::number(0));
- _cur_search_index = -1;
+ navigate_search(1);
+}
+
+void ProtocolDock::navigate_search(int direction)
+{
+ if (_search_running) {
+ // Finish the current query first; repeated presses do not restart it.
+ _search_direction = direction;
+ _search_timer->start(0);
return;
}
-
- int i = 0;
- uint64_t rowCount = _model_proxy.rowCount();
- QModelIndex matchingIndex;
- pv::data::DecoderModel *decoder_model = _session->get_decoder_model();
- auto decoder_stack = decoder_model->getDecoderStack();
-
- if (decoder_stack == NULL){
- dsv_err("decoder_stack is null");
+ if (_search_matches.empty())
return;
- }
-
- do {
- _cur_search_index++;
- if (_cur_search_index < 0 || _cur_search_index >= _model_proxy.rowCount())
- _cur_search_index = 0;
- matchingIndex = _model_proxy.mapToSource(_model_proxy.index(floor(_cur_search_index),_model_proxy.filterKeyColumn()));
-
- if (!matchingIndex.isValid())
- break;
-
- i = 1;
- uint64_t row = matchingIndex.row() + 1;
- uint64_t col = matchingIndex.column();
- pv::data::decode::Annotation ann;
- bool ann_valid = false;
-
- while(i < _str_list.size()) {
- QString nxt = _str_list.at(i);
+ const int count = int(_search_matches.size());
+ if (_search_index < 0)
+ _search_index = direction > 0 ? 0 : count - 1;
+ else
+ _search_index = (_search_index + direction + count) % count;
+ const QModelIndex index = _match_model->index(_search_index, 1);
+ _table_view->setCurrentIndex(index);
+ _table_view->scrollTo(index);
+ item_clicked(index);
+}
- do {
- ann_valid = decoder_stack->list_annotation(&ann, col, row);
- row++;
- }
- while(ann_valid && !ann.is_numberic());
-
- if (ann_valid){
- QString source = ann.annotations().at(0);
- if (source.contains(nxt))
- i++;
- else
- break;
- }
- else{
- break;
- }
- }
- }while(i < _str_list.size() && --rowCount);
-
- if(i >= _str_list.size() && matchingIndex.isValid()){
- _table_view->scrollTo(matchingIndex);
- _table_view->setCurrentIndex(matchingIndex);
- _table_view->clicked(matchingIndex);
- } else {
- _table_view->scrollToTop();
- _table_view->clearSelection();
- _matchs_label->setText(QString::number(0));
- _cur_search_index = -1;
- }
+void ProtocolDock::invalidate_search()
+{
+ _search_timer->stop();
+ _search_running = false;
+ _search_direction = 0;
+ _search_index = -1;
+ _search_matches.clear();
+ _match_model->setMatches({}, QString());
+ _search_column_sizes.clear();
+ _search_row = 0;
+ _search_column = 0;
+ _matchs_label->setText(QString::number(0));
}
void ProtocolDock::search_done()
{
- QString str = _ann_search_edit->text().trimmed();
- QRegularExpression rx("(-)");
- _str_list = str.split(rx);
- _model_proxy.setFilterFixedString(_str_list.first());
- if (_str_list.size() > 1)
- _matchs_label->setText("...");
- else
- _matchs_label->setText(QString::number(_model_proxy.rowCount()));
+ const int pending_direction = _search_direction;
+ invalidate_search();
+ const QString text = _ann_search_edit->text().trimmed();
+ auto decoder_model = _session->get_decoder_model();
+ QAbstractItemModel *table_model = text.isEmpty()
+ ? static_cast<QAbstractItemModel*>(decoder_model) : _match_model;
+ if (_table_view->model() != table_model)
+ _table_view->setModel(table_model);
+ auto decoder_stack = decoder_model->getDecoderStack();
+ if (text.isEmpty() || !decoder_stack)
+ return;
+
+ // Only numeric sequences use '-'. Labels like CAN-FD and negative numbers
+ // must remain literal text. Always match every waveform label variant.
+ static const QRegularExpression sequence(
+ "^[0-9A-Fa-f]+(?:\\s*-\\s*[0-9A-Fa-f]+)+$");
+ _search_terms = sequence.match(text).hasMatch()
+ ? text.split(QRegularExpression("\\s*-\\s*")) : QStringList{text};
+ for (int col = 0; col < decoder_model->columnCount(QModelIndex()); ++col)
+ _search_column_sizes.push_back(int(qMin<uint64_t>(
+ decoder_stack->list_annotation_size(col), std::numeric_limits<int>::max())));
+ _search_running = true;
+ _search_direction = pending_direction;
+ _matchs_label->setText("...");
+ _search_timer->start(0);
}
void ProtocolDock::search_changed()
{
- _search_edited = true;
- _matchs_label->setText("...");
+ // A new query cancels any pending navigation for the old text.
+ _search_direction = 0;
+ search_done();
+ if (_search_running)
+ _search_timer->start(180);
}
-void ProtocolDock::search_update()
+void ProtocolDock::search_step()
{
- if (!_search_edited)
+ if (!_search_running)
return;
-
- pv::data::DecoderModel *decoder_model = _session->get_decoder_model();
-
+ auto decoder_model = _session->get_decoder_model();
auto decoder_stack = decoder_model->getDecoderStack();
- if (!decoder_stack)
+ if (!decoder_stack) {
+ invalidate_search();
return;
+ }
- if (decoder_stack->list_annotation_size(_model_proxy.filterKeyColumn()) > ProgressRows) {
- QFuture<void> future;
- future = QtConcurrent::run([&]{
- search_done();
- });
- Qt::WindowFlags flags = Qt::CustomizeWindowHint;
- QProgressDialog dlg(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_SEARCHING), "Searching..."),
- L_S(STR_PAGE_DLG, S_ID(IDS_DLG_CANCEL), "Cancel"),0,0,this,flags);
- dlg.setWindowModality(Qt::WindowModal);
- dlg.setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint |
- Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint);
- dlg.setCancelButton(NULL);
-
- QFutureWatcher<void> watcher;
- connect(&watcher,SIGNAL(finished()),&dlg,SLOT(cancel()));
- watcher.setFuture(future);
-
- dlg.exec();
- } else {
- search_done();
+ // Keep Qt models and widgets on the GUI thread, yielding between chunks.
+ // Model resets stop this timer and discard indices before a decoder dies.
+ QElapsedTimer elapsed;
+ elapsed.start();
+ while (_search_column < int(_search_column_sizes.size())) {
+ if (_search_row >= _search_column_sizes[_search_column]) {
+ ++_search_column;
+ _search_row = 0;
+ continue;
+ }
+ const int row = _search_row++;
+ if (decoder_model->annotation_matches(_search_column, row, _search_terms)) {
+ pv::data::decode::Annotation ann;
+ if (decoder_stack->list_annotation(&ann, _search_column, row))
+ _search_matches.push_back({decoder_model->index(row, _search_column),
+ ann.start_sample()});
+ }
+ if (elapsed.elapsed() >= 8) {
+ _matchs_label->setText(QString("%1...").arg(qulonglong(_search_matches.size())));
+ _search_timer->start(1);
+ return;
+ }
}
- _search_edited = false;
+
+ // Result columns have independent row numbers. Navigate in time order.
+ std::sort(_search_matches.begin(), _search_matches.end(),
+ [](const SearchMatch &a, const SearchMatch &b) {
+ if (a.sample != b.sample) return a.sample < b.sample;
+ if (a.index.column() != b.index.column()) return a.index.column() < b.index.column();
+ return a.index.row() < b.index.row();
+ });
+ _search_running = false;
+ std::vector<QModelIndex> matches;
+ matches.reserve(_search_matches.size());
+ for (const auto &match : _search_matches) matches.push_back(match.index);
+ _match_model->setMatches(matches, _search_terms.first());
+ resize_table_view(decoder_model);
+ _matchs_label->setText(QString::number(qulonglong(_search_matches.size())));
+ const int direction = _search_direction;
+ _search_direction = 0;
+ if (direction)
+ navigate_search(direction);
}
//-------------------IProtocolItemLayerCallback
@@ -1100,7 +1155,8 @@ void ProtocolDock::UpdateFont()
QRect rc = fm.boundingRect(str);
int lineHeight = rc.height() + 15;
- _pro_keyword_edit->setFixedHeight(rc.height() + 5);
+ // Include the line edit's styled padding and border so glyphs are not clipped.
+ _pro_keyword_edit->setFixedHeight(qMax(rc.height() + 5, _pro_keyword_edit->sizeHint().height()));
int pannelHeight = lineHeight * _protocol_lay_items.size() + _pro_keyword_edit->height();
if (pannelHeight < 100){
diff --git a/DSView/pv/dock/protocoldock.h b/DSView/pv/dock/protocoldock.h
index 0da4c4cc..2bcceeb8 100644
--- a/DSView/pv/dock/protocoldock.h
+++ b/DSView/pv/dock/protocoldock.h
@@ -33,9 +33,8 @@
#include <QScrollArea>
#include <QSplitter>
#include <QTableView>
-#include <QSortFilterProxyModel>
+#include <QTimer>
#include <vector>
-#include <mutex>
#include <list>
#include "../data/decodermodel.h"
#include "protocolitemlayer.h"
@@ -75,14 +74,13 @@ public IUiWindow
{
Q_OBJECT
-public:
- static const uint64_t ProgressRows = 100000;
-
public:
ProtocolDock(QWidget *parent, view::View &view, SigSession *session);
~ProtocolDock();
void del_all_protocol();
+ bool add_host_decoder(const QString &id);
+ void check_host_decoders();
bool add_protocol_by_id(QString id, bool silent, std::list<pv::data::decode::Decoder*> &sub_decoders);
void reset_view();
@@ -119,6 +117,8 @@ private:
void UpdateFont() override;
void adjustPannelSize();
+ void invalidate_search();
+ void navigate_search(int direction);
signals:
void protocol_updated();
@@ -139,18 +139,29 @@ private slots:
void search_nxt();
void search_done();
void search_changed();
- void search_update();
+ void search_step();
void show_protocol_select();
private:
SigSession *_session;
view::View &_view;
- QSortFilterProxyModel _model_proxy;
- int _cur_search_index;
- QStringList _str_list;
+ struct SearchMatch {
+ QModelIndex index;
+ uint64_t sample;
+ };
+ std::vector<SearchMatch> _search_matches;
+ std::vector<int> _search_column_sizes;
+ QTimer *_search_timer;
+ QStringList _search_terms;
+ int _search_row = 0;
+ int _search_column = 0;
+ int _search_index = -1;
+ int _search_direction = 0;
+ bool _search_running = false;
QWidget *_top_panel;
QTableView *_table_view;
+ data::AnnotationMatchModel *_match_model;
QPushButton *_pre_button;
QPushButton *_nxt_button;
PopupLineEdit *_ann_search_edit;
@@ -172,8 +183,6 @@ private:
QString _selected_protocol_id;
XToolButton *_pro_search_button;
- mutable std::mutex _search_mutex;
- bool _search_edited;
};
} // namespace dock
diff --git a/DSView/pv/mainframe.cpp b/DSView/pv/mainframe.cpp
index 9f92d42b..f493c90e 100644
--- a/DSView/pv/mainframe.cpp
+++ b/DSView/pv/mainframe.cpp
@@ -44,7 +44,8 @@
#include <QFile>
#include <QGuiApplication>
#include <QFont>
-#include <algorithm>
+#include <algorithm>
+#include <cstdio>
#include <QWindow>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
@@ -97,7 +98,8 @@ MainFrame::MainFrame()
#ifdef _WIN32
setWindowFlags(Qt::FramelessWindowHint);
_is_win32_parent_window = true;
- _taskBtn = NULL;
+ _taskBtn = NULL;
+ _taskPrg = NULL;
isWin32 = true;
#else
setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint);
@@ -297,7 +299,7 @@ void MainFrame::resizeEvent(QResizeEvent *event)
void MainFrame::closeEvent(QCloseEvent *event)
{
- writeSettings();
+ if (!qApp->property("embeddedHost").toBool()) writeSettings();
if (_mainWindow->able_to_close()){
@@ -309,7 +311,8 @@ void MainFrame::closeEvent(QCloseEvent *event)
}
#endif
- event->accept();
+ event->accept();
+ if (qApp->property("embeddedHost").toBool()) qApp->quit();
}
else{
event->ignore();
@@ -645,7 +648,7 @@ void MainFrame::writeSettings()
dsv_info("Save form, x:%d, y:%d, w:%d, h:%d", x, y, w, h);
}
-void MainFrame::ShowFormInit()
+void MainFrame::ShowFormInit()
{
ReadSettings();
@@ -697,9 +700,80 @@ void MainFrame::ShowFormInit()
AttachNativeWindow();
}
#endif
-}
-
-void MainFrame::AttachNativeWindow()
+}
+
+#ifdef _WIN32
+bool MainFrame::ShowEmbedded(quintptr parentId)
+{
+ const HWND parent = reinterpret_cast<HWND>(parentId);
+ DWORD parentPid = 0;
+ GetWindowThreadProcessId(parent, &parentPid);
+ if (!parentPid || !IsWindow(parent)) return false;
+ _titleBar->hide();
+ _titleBar->EnableAbleDrag(false);
+ setMinimumSize(0, 0);
+ _mainWindow->setMinimumSize(0, 0);
+ setWindowFlags(Qt::FramelessWindowHint | Qt::Tool);
+ const HWND child = reinterpret_cast<HWND>(winId());
+ SetWindowLongPtr(child, GWL_STYLE, WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
+ SetWindowLongPtr(child, GWL_EXSTYLE, GetWindowLongPtr(child, GWL_EXSTYLE) & ~WS_EX_APPWINDOW);
+ SetLastError(0);
+ SetParent(child, parent);
+ if (GetLastError() != 0 || GetParent(child) != parent) return false;
+ _mainWindow->restore_dock();
+ PopupDlgList::SetCurrentScreen(QGuiApplication::primaryScreen());
+ QByteArray commandBuffer;
+ auto fit = [this, parent, child, parentPid, commandBuffer]() mutable {
+ DWORD currentPid = 0;
+ GetWindowThreadProcessId(parent, &currentPid);
+ if (!IsWindow(parent) || currentPid != parentPid) {
+ qApp->quit();
+ return;
+ }
+ DWORD available = 0;
+ const HANDLE input = GetStdHandle(STD_INPUT_HANDLE);
+ if (PeekNamedPipe(input, NULL, 0, NULL, &available, NULL) && available) {
+ char data[256];
+ DWORD count = 0;
+ if (ReadFile(input, data, qMin<DWORD>(available, sizeof(data)), &count, NULL)) {
+ commandBuffer.append(data, count);
+ int newline;
+ while ((newline = commandBuffer.indexOf('\n')) >= 0) {
+ const QByteArray command = commandBuffer.left(newline).trimmed();
+ commandBuffer.remove(0, newline + 1);
+ if (command == "CLOSE") close();
+ else if (command.startsWith("ADD_DECODER "))
+ _mainWindow->addHostDecoder(QString::fromUtf8(command.mid(12)));
+ else if (command.startsWith("OPEN_FILE ")) {
+ const QString path = QString::fromUtf8(QByteArray::fromBase64(command.mid(10)));
+ printf("DSVIEW_FILE_RESULT %s\n", _mainWindow->openHostFile(path));
+ fflush(stdout);
+ }
+ }
+ if (commandBuffer.size() > 262144) commandBuffer.clear();
+ }
+ }
+ RECT rect, childRect;
+ GetClientRect(parent, &rect);
+ GetWindowRect(child, &childRect);
+ const int width = rect.right - rect.left;
+ const int height = rect.bottom - rect.top;
+ if (width > 0 && height > 0 &&
+ (childRect.right - childRect.left != width || childRect.bottom - childRect.top != height))
+ ::MoveWindow(child, 0, 0, width, height, TRUE);
+ };
+ QTimer *hostTimer = new QTimer(this);
+ connect(hostTimer, &QTimer::timeout, this, fit);
+ hostTimer->start(50);
+ fit();
+ QFrame::show();
+ if (!qgetenv("DSVIEW_HOST_CHECK").isEmpty())
+ QTimer::singleShot(3500, _mainWindow, &MainWindow::checkHostDecoders);
+ return true;
+}
+#endif
+
+void MainFrame::AttachNativeWindow()
{
#ifdef _WIN32
@@ -1022,7 +1096,7 @@ void MainFrame::ReadSettings()
void MainFrame::showEvent(QShowEvent *event)
{
// Taskbar Progress Effert for Win7 and Above
- if (_taskBtn && _taskBtn->window() == NULL) {
+ if (!qApp->property("embeddedHost").toBool() && _taskBtn && _taskBtn->window() == NULL) {
_taskBtn->setWindow(windowHandle());
_taskPrg = _taskBtn->progress();
}
@@ -1032,8 +1106,9 @@ void MainFrame::showEvent(QShowEvent *event)
void MainFrame::setTaskbarProgress(int progress)
{
-#ifdef _WIN32
- if (progress > 0) {
+#ifdef _WIN32
+ if (!_taskPrg) return;
+ if (progress > 0) {
_taskPrg->setVisible(true);
_taskPrg->setValue(progress);
} else {
@@ -1107,8 +1182,14 @@ QWidget* MainFrame::GetBodyView()
}
#ifdef _WIN32
-bool MainFrame::nativeEvent(const QByteArray &eventType, void *message, MESSAGE_RESULT_PTR result)
-{
+bool MainFrame::nativeEvent(const QByteArray &eventType, void *message, MESSAGE_RESULT_PTR result)
+{
+ if (qApp->property("embeddedHost").toBool() &&
+ static_cast<MSG*>(message)->message == WM_CLOSE) {
+ QTimer::singleShot(0, this, [this]() { close(); });
+ *result = 0;
+ return true;
+ }
if (_parentNativeWidget != NULL)
{
MSG *msg = static_cast<MSG*>(message);
diff --git a/DSView/pv/mainframe.h b/DSView/pv/mainframe.h
index 0c2ec3ee..76f91faa 100644
--- a/DSView/pv/mainframe.h
+++ b/DSView/pv/mainframe.h
@@ -99,7 +99,10 @@ public:
public:
MainFrame();
- void ShowFormInit();
+ void ShowFormInit();
+#ifdef _WIN32
+ bool ShowEmbedded(quintptr parentId);
+#endif
void ShowHelpDocAsync();
bool IsMaxsized();
diff --git a/DSView/pv/mainwindow.cpp b/DSView/pv/mainwindow.cpp
index 74dc348b..c6800415 100644
--- a/DSView/pv/mainwindow.cpp
+++ b/DSView/pv/mainwindow.cpp
@@ -22,12 +22,16 @@
#include <QAction>
#include <QButtonGroup>
-#include <QFileDialog>
+#include <QFileDialog>
+#include <QFileInfo>
#include <QMessageBox>
#include <QMenu>
#include <QMenuBar>
#include <QStatusBar>
-#include <QVBoxLayout>
+#include <QVBoxLayout>
+#include <QScrollArea>
+#include <QScrollBar>
+#include <QSplitter>
#include <QWidget>
#include <QDesktopServices>
#include <QKeyEvent>
@@ -108,8 +112,60 @@
namespace pv
{
- namespace{
- QString tmp_file;
+ namespace{
+ QString tmp_file;
+
+ // Keep the instrument controls above the waveform, rather than above
+ // the right-hand docks. Narrow hosts scroll instead of wrapping Help.
+ class EmbeddedToolbars : public QScrollArea {
+ public:
+ EmbeddedToolbars(const QList<QToolBar*> &bars, QWidget *parent)
+ : QScrollArea(parent), _bars(bars)
+ {
+ setFrameShape(QFrame::NoFrame);
+ setWidgetResizable(true);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ auto panel = new QWidget(this);
+ auto layout = new QHBoxLayout(panel);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(0);
+ layout->setSizeConstraint(QLayout::SetMinAndMaxSize);
+ for (auto bar : _bars) {
+ layout->addWidget(bar);
+ bar->installEventFilter(this);
+ }
+ layout->addStretch(1);
+ setWidget(panel);
+ panel->installEventFilter(this);
+ updateSize();
+ }
+ protected:
+ bool eventFilter(QObject *object, QEvent *event) override {
+ if (event->type() == QEvent::LayoutRequest)
+ updateSize();
+ return QScrollArea::eventFilter(object, event);
+ }
+ void resizeEvent(QResizeEvent *event) override {
+ QScrollArea::resizeEvent(event);
+ updateSize();
+ }
+ private:
+ void updateSize() {
+ int height = 0;
+ int width = 0;
+ for (auto bar : _bars) {
+ const QSize hint = bar->sizeHint();
+ bar->setMinimumSize(hint);
+ width += hint.width();
+ height = qMax(height, hint.height());
+ }
+ if (width > viewport()->width())
+ height += horizontalScrollBar()->sizeHint().height();
+ setFixedHeight(height);
+ }
+ QList<QToolBar*> _bars;
+ };
}
MainWindow::MainWindow(toolbars::TitleBar *title_bar, QWidget *parent)
@@ -189,10 +245,15 @@ namespace pv
setIconSize(QSize(40, 40));
- addToolBar(_sampling_bar);
- addToolBar(_trig_bar);
- addToolBar(_file_bar);
- addToolBar(_logo_bar);
+ if (qApp->property("embeddedHost").toBool()) {
+ _vertical_layout->insertWidget(0, new EmbeddedToolbars(
+ {_sampling_bar, _trig_bar, _file_bar, _logo_bar}, _central_widget));
+ } else {
+ addToolBar(_sampling_bar);
+ addToolBar(_trig_bar);
+ addToolBar(_file_bar);
+ addToolBar(_logo_bar);
+ }
// Setup the dockWidget
_protocol_dock = new QDockWidget(L_S(STR_PAGE_DLG, S_ID(IDS_DLG_PROTOCOL_DOCK_TITLE), "Decode Protocol"), this);
@@ -200,8 +261,43 @@ namespace pv
_protocol_dock->setFeatures(QDockWidget::DockWidgetMovable);
_protocol_dock->setAllowedAreas(Qt::RightDockWidgetArea);
_protocol_dock->setVisible(false);
- _protocol_widget = new dock::ProtocolDock(_protocol_dock, *_view, _session);
- _protocol_dock->setWidget(_protocol_widget);
+ _protocol_widget = new dock::ProtocolDock(_protocol_dock, *_view, _session);
+ if (qApp->property("embeddedHost").toBool()) {
+ // The dock occupies the right column, but its visible panel can be
+ // shortened from the top. Move the title with the whole panel.
+ _protocol_dock->setTitleBarWidget(new QWidget(_protocol_dock));
+ _protocol_dock->setFeatures(QDockWidget::NoDockWidgetFeatures);
+ auto outer = new QSplitter(Qt::Vertical, _protocol_dock);
+ outer->setObjectName("decoderOuterSplit");
+ auto space = new QWidget(outer);
+ space->setMinimumHeight(0);
+ auto panel = new QWidget(outer);
+ auto panel_layout = new QVBoxLayout(panel);
+ panel_layout->setContentsMargins(0, 0, 0, 0);
+ panel_layout->setSpacing(0);
+ auto title = new QLabel(_protocol_dock->windowTitle(), panel);
+ title->setStyleSheet("background: #151d28; padding: 5px 10px;");
+ connect(_protocol_dock, &QWidget::windowTitleChanged, title, &QLabel::setText);
+ panel_layout->addWidget(title);
+ panel_layout->addWidget(_protocol_widget, 1);
+ outer->addWidget(space);
+ outer->addWidget(panel);
+ outer->setCollapsible(0, true);
+ outer->setCollapsible(1, false);
+ outer->setHandleWidth(10);
+ outer->setStretchFactor(0, 0);
+ outer->setStretchFactor(1, 1);
+ outer->setSizes({0, 600});
+ outer->handle(1)->setCursor(Qt::SizeVerCursor);
+ outer->handle(1)->setToolTip(tr("Drag the top edge to resize the entire Decoders panel"));
+ outer->setStyleSheet(
+ "QSplitter#decoderOuterSplit::handle:vertical { background: #243247; "
+ "border-top: 1px solid #344762; border-bottom: 1px solid #344762; }"
+ "QSplitter#decoderOuterSplit::handle:vertical:hover { background: #1570d8; }");
+ _protocol_dock->setWidget(outer);
+ } else {
+ _protocol_dock->setWidget(_protocol_widget);
+ }
_session->set_decoder_pannel(_protocol_widget);
@@ -479,7 +575,38 @@ namespace pv
return true;
}
- void MainWindow::on_protocol(bool visible)
+ const char *MainWindow::openHostFile(const QString &path)
+ {
+ if (_session->is_working() || _session->is_saving() || QApplication::activeModalWidget())
+ return "busy";
+ if (!QFileInfo(path).isFile() || QFileInfo(path).suffix().compare("dsl", Qt::CaseInsensitive))
+ return "error";
+ if (confirm_to_store_data()) {
+ on_save();
+ return "save";
+ }
+ if (_session->is_working() || _session->is_saving()) return "busy";
+ if (_device_agent->is_hardware()) save_config();
+ try {
+ return _session->set_file(path) ? "ok" : "error";
+ } catch (...) {
+ return "error";
+ }
+ }
+
+ void MainWindow::addHostDecoder(const QString &id)
+ {
+ on_protocol(true);
+ _protocol_widget->add_host_decoder(id);
+ }
+
+ void MainWindow::checkHostDecoders()
+ {
+ on_protocol(true);
+ _protocol_widget->check_host_decoders();
+ }
+
+ void MainWindow::on_protocol(bool visible)
{
_protocol_dock->setVisible(visible);
@@ -1414,8 +1541,9 @@ namespace pv
_session->update_lang_text();
}
- void MainWindow::switchTheme(QString style)
- {
+ void MainWindow::switchTheme(QString style)
+ {
+ if (qApp->property("embeddedHost").toBool()) style = "dark";
AppConfig &app = AppConfig::Instance();
if (app.frameOptions.style != style)
@@ -1427,7 +1555,25 @@ namespace pv
QString qssRes = ":/" + style + ".qss";
QFile qss(qssRes);
qss.open(QFile::ReadOnly | QFile::Text);
- qApp->setStyleSheet(qss.readAll());
+ QString stylesheet = QString::fromUtf8(qss.readAll());
+ if (qApp->property("embeddedHost").toBool()) {
+ QFile hostStyle(QString::fromUtf8(qgetenv("DSVIEW_HOST_STYLE")));
+ if (hostStyle.open(QFile::ReadOnly | QFile::Text))
+ stylesheet += QString::fromUtf8(hostStyle.readAll());
+ QPalette palette;
+ palette.setColor(QPalette::Window, QColor("#10151d"));
+ palette.setColor(QPalette::Base, QColor("#111c29"));
+ palette.setColor(QPalette::AlternateBase, QColor("#151d28"));
+ palette.setColor(QPalette::WindowText, QColor("#dce6f2"));
+ palette.setColor(QPalette::Text, QColor("#dce6f2"));
+ palette.setColor(QPalette::Button, QColor("#243247"));
+ palette.setColor(QPalette::ButtonText, QColor("#dce6f2"));
+ palette.setColor(QPalette::Highlight, QColor("#1570d8"));
+ palette.setColor(QPalette::HighlightedText, Qt::white);
+ qApp->setPalette(palette);
+ qApp->setFont(QFont("Segoe UI", 10));
+ }
+ qApp->setStyleSheet(stylesheet);
qss.close();
UiManager::Instance()->Update(UI_UPDATE_ACTION_THEME);
diff --git a/DSView/pv/mainwindow.h b/DSView/pv/mainwindow.h
index a43e0427..8212fe14 100644
--- a/DSView/pv/mainwindow.h
+++ b/DSView/pv/mainwindow.h
@@ -93,7 +93,10 @@ public:
public:
explicit MainWindow(toolbars::TitleBar *title_bar, QWidget *parent = 0);
- void openDoc();
+ void openDoc();
+ void addHostDecoder(const QString &id);
+ const char *openHostFile(const QString &path);
+ void checkHostDecoders();
public slots:
void switchTheme(QString style);
diff --git a/DSView/pv/toolbars/filebar.cpp b/DSView/pv/toolbars/filebar.cpp
index f562431c..792c6971 100644
--- a/DSView/pv/toolbars/filebar.cpp
+++ b/DSView/pv/toolbars/filebar.cpp
@@ -24,6 +24,7 @@
#include <QFileDialog>
#include <deque>
#include <QApplication>
+#include <cstdio>
#include "filebar.h"
#include "../ui/msgbox.h"
@@ -146,7 +147,9 @@ void FileBar::on_actionOpen_triggered()
this,
L_S(STR_PAGE_DLG, S_ID(IDS_DLG_OPEN_FILE), "Open File"),
app.userHistory.openDir,
- "DSView Data (*.dsl)");
+ qApp->property("embeddedHost").toBool()
+ ? "Digital captures (*.dsl *.csv);;DSView Data (*.dsl);;CSV (*.csv)"
+ : "DSView Data (*.dsl)");
if (!file_name.isEmpty()) {
QString fname = path::GetDirectoryName(file_name);
@@ -155,7 +158,12 @@ void FileBar::on_actionOpen_triggered()
app.SaveHistory();
}
- sig_load_file(file_name);
+ if (qApp->property("embeddedHost").toBool() && file_name.endsWith(".csv", Qt::CaseInsensitive)) {
+ printf("DSVIEW_IMPORT_CSV %s\n", file_name.toUtf8().toBase64().constData());
+ fflush(stdout);
+ } else {
+ sig_load_file(file_name);
+ }
}
}
diff --git a/DSView/pv/toolbars/logobar.cpp b/DSView/pv/toolbars/logobar.cpp
index 4b3b492c..95c28b49 100644
--- a/DSView/pv/toolbars/logobar.cpp
+++ b/DSView/pv/toolbars/logobar.cpp
@@ -106,13 +106,17 @@ LogoBar::LogoBar(SigSession *session, QWidget *parent) :
_logo_button.setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
_logo_button.setPopupMode(QToolButton::InstantPopup);
- QWidget *spacer = new QWidget(this);
- spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- addWidget(spacer);
+ if (!qApp->property("embeddedHost").toBool()) {
+ QWidget *spacer = new QWidget(this);
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ addWidget(spacer);
+ }
addWidget(&_logo_button);
- QWidget *margin = new QWidget(this);
- margin->setMinimumWidth(20);
- addWidget(margin);
+ if (!qApp->property("embeddedHost").toBool()) {
+ QWidget *margin = new QWidget(this);
+ margin->setMinimumWidth(20);
+ addWidget(margin);
+ }
connect(_action_en, SIGNAL(triggered()), this, SLOT(on_actionEn_triggered()));
connect(_action_cn, SIGNAL(triggered()), this, SLOT(on_actionCn_triggered()));

View File

@@ -0,0 +1,24 @@
#include "gate_summary.h"
#include <cassert>
int main() {
GateSummary summary;
assert(summary.severity(true) == 1); // Empty captures are not OK.
summary.observe(0); summary.observe(1); summary.observe(2);
assert(summary.severity(false) == 1);
assert(summary.severity(true) == 0);
summary.observe(0); // A command without a complete ACK at EOF.
assert(summary.severity(true) == 1);
summary.observe(2);
assert(summary.severity(true) == 0);
summary.observe(3);
assert(summary.severity(true) == 1);
summary.observe(4);
assert(summary.severity(false) == 2);
assert(summary.severity(true) == 2);
summary.observe(2); // Later good events never hide earlier faults.
assert(summary.severity(true) == 2);
summary.reset();
assert(summary.severity(true) == 1);
summary.observe(5);
assert(summary.severity(true) == 2);
}

View File

@@ -0,0 +1,438 @@
"""Wire vectors and DSView API boundary tests; no sigrok installation needed."""
import binascii
import importlib
from pathlib import Path
import struct
import sys
import types
import tempfile
import importlib.util
import unittest
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location('dsview_build', ROOT / 'build.py')
BUILD = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(BUILD)
RUNTIME = tempfile.TemporaryDirectory()
BUILD.package_decoders(Path(RUNTIME.name))
(Path(RUNTIME.name) / 'decoders/common/__init__.py').write_text('')
class FakeDecoder:
def has_channel(self, channel):
return channel < 2
def register(self, kind):
return kind
def put(self, ss, es, output, data):
self.emitted.append((ss, es, output, data))
stub = types.ModuleType('sigrokdecode')
stub.Decoder = FakeDecoder
stub.OUTPUT_ANN, stub.OUTPUT_PYTHON, stub.SRD_CONF_SAMPLERATE = 0, 1, 2
previous = sys.modules.get('sigrokdecode')
sys.modules['sigrokdecode'] = stub
sys.path.insert(0, str(Path(RUNTIME.name) / 'decoders'))
try:
uart = importlib.import_module('set_uart.pd')
wire = importlib.import_module('common.setgui_decoders.set_uart')
can = importlib.import_module('set_can.pd')
cancore = importlib.import_module('common.setgui_decoders.set_can')
pm = importlib.import_module('pm35_uart.pd')
gate = importlib.import_module('gate_driver_timing.pd')
pair_decoder = importlib.import_module('transistor_pair.pd')
pmcore = importlib.import_module('common.setgui_decoders.pm35_uart')
finally:
sys.path.pop(0)
if previous is None:
del sys.modules['sigrokdecode']
else:
sys.modules['sigrokdecode'] = previous
PING = bytes.fromhex('A5 5A 02 08 01 00 00 00 2A 00 34 12 00 00 33 EC 33 04')
BRIDGE = bytes.fromhex('AA 55 08 01 01 67 45 23 01 AA BB FE 14')
def crc32_frame(header, payload=b''):
data = header + payload
return data + struct.pack('<I', binascii.crc32(data[2:]) & 0xffffffff)
def modbus(raw):
return raw + struct.pack('<H', wire.crc16(raw, True))
def feed(parser, raw):
events = []
for i, byte in enumerate(raw):
events.extend(parser.feed(byte, i * 10, i * 10 + 9))
return events
def instance(module, **options):
decoder = module.Decoder()
decoder.options = {item['id']: item['default'] for item in decoder.options}
decoder.options.update(options)
decoder.emitted = []
decoder.metadata(2, 1000000)
decoder.start()
return decoder
def uart_bytes(decoder, raw, direction=0, base=0):
for i, byte in enumerate(raw):
ss = base + i * 100
decoder.decode(ss, ss+80, ('DATA', direction, (byte, [])))
decoder.decode(ss, ss+90, ('FRAME', direction, (byte, True)))
def frames(decoder):
return [item[3][1] for item in decoder.emitted if item[2] == 1]
class WireTests(unittest.TestCase):
def test_reference_crc(self):
self.assertEqual(wire.crc16(b'123456789'), 0x29b1)
self.assertEqual(wire.crc16(b'123456789', True), 0x4b37)
def test_documented_ping(self):
result = wire.parse_frame(PING)
self.assertEqual((result['destination'], result['sequence']), (42, 0x1234))
def test_documented_bridge(self):
result = wire.parse_frame(BRIDGE)
self.assertEqual((result['can_id'], result['payload']), (0x1234567, b'\xaa\xbb'))
def test_v1_big_endian_header(self):
frame = crc32_frame(bytes.fromhex('a55a010c12340003'), b'abc')
result = wire.parse_frame(frame)
self.assertEqual(result['sequence'], 0x1234)
self.assertIn('FIRMWARE_DATA', result['summary'])
def test_noise_crc_and_resync(self):
damaged = bytearray(PING)
damaged[-1] ^= 1
events = feed(wire.StreamParser(), b'noise\xa5' + damaged + PING + BRIDGE)
self.assertEqual(sum(e[2] is not None for e in events), 2)
self.assertTrue(any(e[3] and 'CRC' in e[3] for e in events))
def test_bad_version_and_length(self):
events = feed(wire.StreamParser(), bytes.fromhex('a55aff a55a010100000201') + PING)
self.assertEqual(sum(e[2] is not None for e in events), 1)
self.assertGreaterEqual(sum(bool(e[3]) for e in events), 2)
def test_max_payload(self):
raw = crc32_frame(bytes.fromhex('a55a020009000000000001000002'), bytes(range(256))*2)
events = feed(wire.StreamParser(), raw)
self.assertEqual(len(events), 1)
self.assertEqual(len(events[0][2]['payload']), 512)
def test_unknown_message_preserved(self):
raw = crc32_frame(bytes.fromhex('a55a020034120000000001000000'))
self.assertEqual(wire.parse_frame(raw)['message_type'], 0x1234)
def test_uart_directions_are_independent(self):
decoder = instance(uart)
for i, byte in enumerate(PING):
decoder.decode(i*10, i*10+9, ('FRAME', 0, (byte, True)))
decoder.decode(i*10, i*10+9, ('FRAME', 1, (byte, True)))
self.assertEqual([f['direction'] for f in frames(decoder)], [0, 1])
def test_uart_invalid_last_byte_not_emitted(self):
decoder = instance(uart)
uart_bytes(decoder, PING[:-1])
decoder.decode(2000, 2090, ('FRAME', 0, (PING[-1], False)))
self.assertFalse(frames(decoder))
uart_bytes(decoder, PING, base=3000)
self.assertEqual(len(frames(decoder)), 1)
def test_uart_gap_discards_partial(self):
decoder = instance(uart)
uart_bytes(decoder, PING[:10])
uart_bytes(decoder, PING, base=200000)
self.assertEqual(len(frames(decoder)), 1)
class CanTests(unittest.TestCase):
def segments(self, packet=PING):
yield bytes([0x10]) + struct.pack('<H', len(packet)) + packet[:5]
for i, offset in enumerate(range(5, len(packet), 7), 1):
yield bytes([0x20 | (i & 15)]) + packet[offset:offset+7]
def test_set_reassembly(self):
decoder = instance(can, protocol='set-v2')
for i, data in enumerate(self.segments()):
decoder.decode(i*1000, i*1000+900, ('extended', 0x122a0000, 'data', len(data), list(data)))
self.assertEqual(frames(decoder)[-1]['raw'], PING)
def test_missing_segment(self):
reader = cancore.Reassembler()
parts = list(self.segments())
reader.feed(0x122a0000, parts[0], 0, 9, 0)
events = reader.feed(0x122a0000, parts[2], 10, 19, 1)
self.assertIn('sequence', events[-1][3])
self.assertFalse(reader.pending)
def test_timeout_and_new_first(self):
reader = cancore.Reassembler()
first = next(self.segments())
reader.feed(0x122a0000, first, 0, 9, 0)
events = reader.feed(0x122a0000, first, 20, 29, 500)
self.assertIn('timeout', events[0][3])
self.assertEqual(reader.pending[0x122a0000]['start'], 20)
def test_id_mismatch(self):
reader = cancore.Reassembler()
for i, part in enumerate(self.segments()):
events = reader.feed(0x122b0000, part, i*10, i*10+9, i)
self.assertIn('disagree', events[-1][3])
def test_sequence_wrap(self):
packet = crc32_frame(bytes.fromhex('a55a0200010000002a0001008000'), bytes(128))
reader = cancore.Reassembler()
for i, part in enumerate(self.segments(packet)):
events = reader.feed(0x122a0000, part, i*10, i*10+9, i)
self.assertEqual(events[-1][2]['raw'], packet)
def test_interleaved_channels(self):
reader = cancore.Reassembler()
complete = []
for i, part in enumerate(self.segments()):
for channel in (0, 1):
complete += reader.feed(0x122a0000 | channel, part, i*10, i*10+9, i)
self.assertEqual(sum(bool(e[2] and 'raw' in e[2]) for e in complete), 2)
def test_gas(self):
result = cancore.legacy(0x1b530010, b'\x34\x12\xfe\xff')
self.assertEqual(result['registers'], [(16, 0x1234), (17, 0xfffe)])
with self.assertRaises(ValueError):
cancore.legacy(0x1b530010, b'\x00')
def test_balsam_mask_and_endianness(self):
result = cancore.legacy(0xba0010, bytes.fromhex('a01012340000ffff'), 'balsam')
self.assertEqual(result['registers'], [(16, 0x1234), (18, 0xffff)])
self.assertIsNone(cancore.legacy(0xba000f, bytes(8), 'balsam'))
def test_ignore_standard_and_remote(self):
decoder = instance(can)
decoder.decode(0, 9, ('standard', 0x123, 'data', 0, []))
decoder.decode(10, 19, ('extended', 0x1b530010, 'remote', 0, []))
self.assertFalse(decoder.emitted)
def test_boot_vectors(self):
self.assertIn('ENTER_BOOT', cancore.legacy(0x13590702, b'')['summary'])
self.assertIn('offset=0x8', cancore.legacy(0x135b0001, bytes.fromhex('1011121314151617'))['summary'])
self.assertIn('status=0', cancore.legacy(0x1b5c0702, bytes.fromhex('00ff000000000000'))['summary'])
class PM35Tests(unittest.TestCase):
def test_single_wire_auto(self):
reader = pmcore.PM35Parser('auto')
raw = modbus(bytes.fromhex('100300000002')) + modbus(bytes.fromhex('1003041234ffff'))
events = feed(reader, raw)
self.assertEqual(len(events), 2)
self.assertTrue(all(event[2] for event in events))
def test_request_and_response(self):
request = modbus(bytes.fromhex('100300000002'))
response = modbus(bytes.fromhex('1003041234ffff'))
decoder = instance(pm)
uart_bytes(decoder, request, 1)
uart_bytes(decoder, response, 0, 10000)
self.assertEqual(len(frames(decoder)), 2)
self.assertEqual(frames(decoder)[1]['values'], [0x1234, 65535])
def test_128_word_legacy_reply(self):
events = feed(pmcore.PM35Parser('response'), modbus(b'\x10\x03\x00' + bytes(256)))
self.assertEqual(len(events[0][2]['values']), 128)
def test_write_command(self):
result = pmcore.parse_pm35(modbus(bytes.fromhex('1006007f8000')), 'request')
self.assertIn('command_bits=8000', result['summary'])
def test_exception(self):
result = pmcore.parse_pm35(modbus(bytes.fromhex('108302')), 'response')
self.assertIn('exception', result['summary'])
def test_crc_and_range_errors(self):
with self.assertRaisesRegex(ValueError, 'CRC'):
pmcore.parse_pm35(bytes.fromhex('1003000000010000'), 'request')
with self.assertRaisesRegex(ValueError, 'range'):
pmcore.parse_pm35(modbus(bytes.fromhex('1003007f0002')), 'request')
class GateAdapterTests(unittest.TestCase):
def test_wait_releases_dsview_shared_pin_tuple(self):
for channels, pair in (((1,), 0), ((0, 1), 0), ((0, 1), 1)):
with self.subTest(channels=channels, pair=pair):
decoder = instance(pair_decoder if pair else gate)
decoder.has_channel = lambda channel: channel in channels
shared_pins = tuple([0, 0])
calls = []
def wait(conditions):
# Native DSView owns one reference and PyTuple_SetItem
# rejects any reference retained by the previous caller.
self.assertEqual(sys.getrefcount(shared_pins), 2)
if len(calls) == 3:
raise StopIteration
calls.append(conditions)
decoder.samplenum = len(calls) * 10
decoder.matched = 0
return shared_pins
decoder.wait = wait
with self.assertRaises(StopIteration):
decoder.decode()
self.assertEqual(len(calls), 3)
def test_orphan_filter_boundaries_profiles_and_polarities(self):
for profile in ('1SP0635', '1SD536F2'):
for active_high in (True, False):
with self.subTest(profile=profile, active_high=active_high):
checker = gate.TimingChecker(1_000_000_000, profile=profile,
vstat_active_high=active_high, orphan_min_width_ns=50)
for start, width, expected in ((100, 20, []), (200, 49, []),
(300, 50, ['orphan']), (400, 51, ['orphan'])):
self.assertEqual(checker.on_status_edge(start, int(active_high)), [])
events = checker.on_status_edge(start + width, int(not active_high))
self.assertEqual([event['kind'] for event in events], expected)
self.assertIsNone(checker.status_start)
def test_orphan_filter_preserves_ack_errors_faults_and_missing_ack(self):
checker = gate.TimingChecker(1_000_000_000, orphan_min_width_ns=10_000)
checker.on_control_edge(0, 1)
self.assertEqual(checker.on_status_edge(250, 1)[0]['kind'], 'delay_ok')
self.assertEqual(checker.on_status_edge(950, 0)[0]['kind'], 'width_ok')
checker.on_control_edge(1000, 0)
checker.on_status_edge(1250, 1)
self.assertEqual(checker.on_status_edge(1270, 0)[0]['kind'], 'width_fail')
checker.on_status_edge(2000, 1)
self.assertEqual(checker.on_status_edge(4000, 0)[0]['kind'], 'fault')
checker.on_control_edge(5000, 1)
self.assertEqual(checker.expire(checker.next_deadline())[0]['kind'], 'missing')
def test_orphan_filter_default_disabled_and_unknown_duration_retained(self):
checker = gate.TimingChecker(1_000_000_000)
checker.on_status_edge(10, 1)
self.assertEqual(checker.on_status_edge(30, 0)[0]['short'], 'ORPHAN 20.0 ns')
checker = gate.TimingChecker(1_000_000_000, orphan_min_width_ns=50)
# A recording starting within a pulse gives no measurable pulse width.
self.assertEqual(checker.on_status_edge(10, 0)[0]['short'], 'Vstat?')
def test_orphan_filter_rejects_invalid_thresholds(self):
for value in (-1, float('nan'), float('inf')):
with self.subTest(value=value), self.assertRaisesRegex(ValueError, 'ORPHAN'):
gate.TimingChecker(100_000_000, orphan_min_width_ns=value)
def test_orphan_filter_option_through_native_edge_api(self):
decoder = instance(gate, orphan_min_width_ns=50)
decoder.metadata(2, 100_000_000)
decoder.samplenum = 0
samples = iter([(10, 1), (12, 0), (20, 1), (25, 0)])
def wait(conditions):
decoder.samplenum, level = next(samples)
decoder.matched = 2
return 0, level
decoder.wait = wait
with self.assertRaises(StopIteration):
decoder.decode()
self.assertEqual(len(decoder.emitted), 1)
self.assertEqual(decoder.emitted[0][:2], (20, 25))
self.assertEqual(decoder.emitted[0][3][1][1], 'ORPHAN 50.0 ns')
def test_profiles_through_native_edge_api(self):
for profile, active, edges, expected in [
('1SP0635', 'high', [(100, 1, 0, 1), (125, 1, 1, 2), (195, 1, 0, 2)], 700),
('1SD536F2', 'low', [(20, 1, 1, 1), (58, 1, 0, 2), (148, 1, 1, 2)], 900),
]:
with self.subTest(profile=profile):
decoder = instance(gate, profile=profile, vstat_active=active)
decoder.metadata(2, 100_000_000)
decoder.samplenum = 0
samples = iter(edges)
def wait(conditions):
decoder.samplenum, vin, vstat, decoder.matched = next(samples)
return vin, vstat
decoder.wait = wait
with self.assertRaises(StopIteration):
decoder.decode()
self.assertEqual([item[3][0] for item in decoder.emitted], [0, 1, 2])
self.assertIn(str(expected), decoder.emitted[-1][3][1][0])
def test_driver_has_no_pair_controls(self):
decoder = instance(gate)
self.assertNotIn('pair_analysis', decoder.options)
self.assertNotIn('vin2_mintime_ns', decoder.options)
self.assertEqual([c['id'] for c in gate.Decoder.optional_channels], ['vin', 'vstat'])
def test_pair_decoder_without_vstat(self):
decoder = instance(pair_decoder, vin2_mintime_ns=200, deadtime_12_ns=50)
decoder.metadata(2, 1_000_000_000)
samples = iter([(0, (0, 0)), (10, (1, 0)), (110, (0, 0)),
(140, (0, 1)), (240, (0, 0)), (280, (1, 0))])
def wait(conditions):
decoder.samplenum, pins = next(samples)
return pins
decoder.wait = wait
with self.assertRaises(StopIteration):
decoder.decode()
text = ' '.join(item[3][1][0] for item in decoder.emitted)
self.assertIn('Deadtime Vin1 -> Vin2: 30.0 ns', text)
self.assertIn('FAIL: Vin2 active', text)
self.assertIn('duty', text)
self.assertNotIn('ACK', text)
self.assertTrue(any(e[3][0] == 3 for e in decoder.emitted))
decoder.reset()
self.assertIsNone(decoder.checker)
def test_status_only_and_pair_requires_two_inputs(self):
decoder = instance(gate)
decoder.has_channel = lambda channel: channel == 1
samples = iter([(10, 1), (1010, 0)])
def wait(conditions):
self.assertEqual(conditions, [{1: 'e'}])
decoder.samplenum, status = next(samples)
decoder.matched = 1
return (255, status, 255)
decoder.wait = wait
with self.assertRaises(StopIteration):
decoder.decode()
self.assertEqual(len(decoder.emitted), 1)
decoder = instance(pair_decoder)
decoder.has_channel = lambda channel: channel == 0
with self.assertRaisesRegex(ValueError, 'Vin1 and Vin2'):
decoder.decode()
def test_missing_ack_timer_emits_error(self):
decoder = instance(gate)
decoder.metadata(2, 100_000_000)
decoder.samplenum = 0
calls = []
def wait(conditions):
calls.append(conditions)
if len(calls) == 1:
decoder.samplenum, decoder.matched = 100, 1
elif len(calls) == 2:
decoder.samplenum += conditions[2]['skip']
decoder.matched = 4
else:
raise StopIteration
return 1, 0
decoder.wait = wait
with self.assertRaises(StopIteration):
decoder.decode()
self.assertEqual([item[3][0] for item in decoder.emitted], [0, 4])
def test_packaged_core_matches_canonical_templates(self):
canonical = ROOT.parents[1] / 'python/logic_analyzer/decoders'
packaged = Path(RUNTIME.name) / 'decoders/common/setgui_decoders'
for name in ('gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart'):
self.assertEqual((canonical / (name + '.py')).read_bytes(),
(packaged / (name + '.py')).read_bytes())
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,21 @@
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
COMPILER = shutil.which('g++') or (r'C:/setcorp/tools/msys64/ucrt64/bin/g++.exe' if os.name == 'nt' else None)
@unittest.skipUnless(COMPILER and Path(COMPILER).is_file(), 'C++ compiler required')
class GateSummaryTests(unittest.TestCase):
def test_verdict_priority_completion_and_reset(self):
with tempfile.TemporaryDirectory() as temporary:
binary = Path(temporary) / ('summary.exe' if os.name == 'nt' else 'summary')
env = dict(os.environ)
env['PATH'] = str(Path(COMPILER).parent) + os.pathsep + env.get('PATH', '')
subprocess.run([COMPILER, '-std=c++11', '-static', '-I', str(ROOT / 'native'),
str(ROOT / 'tests/gate_summary_test.cpp'), '-o', str(binary)],
env=env, check=True, capture_output=True)
subprocess.run([str(binary)], env=env, check=True, capture_output=True)

View File

@@ -0,0 +1,156 @@
import importlib.util
from pathlib import Path
import unittest
MODULE_PATH = (Path(__file__).resolve().parents[3] / 'python/logic_analyzer/decoders/gate_timing.py')
SPEC = importlib.util.spec_from_file_location('gate_driver_timing_core', MODULE_PATH)
TIMING = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(TIMING)
def kinds(events):
return [item['kind'] for item in events]
class TimingCheckerTests(unittest.TestCase):
def test_1sp0635_nominal_ack_at_100_mhz(self):
checker = TIMING.TimingChecker(100_000_000, profile='1SP0635')
checker.on_control_edge(100, 1)
self.assertEqual(kinds(checker.on_status_edge(125, 1)), ['delay_ok'])
result = checker.on_status_edge(195, 0)
self.assertEqual(kinds(result), ['width_ok'])
self.assertEqual(result[0]['width_ns'], 700.0)
def test_1sd536f2_nominal_ack_at_100_mhz_active_low(self):
checker = TIMING.TimingChecker(
100_000_000, profile='1SD536F2', vstat_active_high=False)
checker.on_control_edge(20, 1)
self.assertEqual(kinds(checker.on_status_edge(58, 0)), ['delay_ok'])
result = checker.on_status_edge(148, 1)
self.assertEqual(kinds(result), ['width_ok'])
self.assertEqual(result[0]['width_ns'], 900.0)
def test_late_delay_is_warning_but_valid_width_passes(self):
checker = TIMING.TimingChecker(100_000_000, profile='1SP0635')
checker.on_control_edge(0, 1)
self.assertEqual(kinds(checker.on_status_edge(50, 1)), ['delay_warn'])
self.assertEqual(kinds(checker.on_status_edge(120, 0)), ['width_ok'])
def test_too_short_ack_fails(self):
checker = TIMING.TimingChecker(100_000_000, profile='1SP0635')
checker.on_control_edge(0, 1)
checker.on_status_edge(25, 1)
self.assertEqual(kinds(checker.on_status_edge(55, 0)), ['width_fail'])
def test_long_status_is_fault(self):
checker = TIMING.TimingChecker(100_000_000, profile='1SP0635')
checker.on_control_edge(0, 1)
checker.on_status_edge(25, 1)
self.assertEqual(kinds(checker.on_status_edge(225, 0)), ['fault'])
def test_missing_ack_expires(self):
checker = TIMING.TimingChecker(100_000_000, profile='1SP0635')
checker.on_control_edge(10, 0)
deadline = checker.next_deadline()
self.assertIsNotNone(deadline)
self.assertEqual(kinds(checker.expire(deadline)), ['missing'])
if __name__ == '__main__':
unittest.main()
class InputTimingTests(unittest.TestCase):
def test_both_directions_and_independent_minimums(self):
c = TIMING.InputTimingChecker(1e9, vin1_mintime_ns=100, vin2_mintime_ns=200)
c.update(0, 0, 0)
c.update(10, 1, 0)
self.assertEqual(kinds(c.update(110, 0, 0)), ['mintime_ok'])
event = c.update(140, 0, 1)[0]
self.assertEqual(event['duration_ns'], 30)
self.assertIn('Vin1 -> Vin2', event['text'])
self.assertEqual(kinds(c.update(240, 0, 0)), ['mintime_fail'])
event = c.update(280, 1, 0)[0]
self.assertEqual(event['duration_ns'], 40)
self.assertIn('Vin2 -> Vin1', event['text'])
def test_atomic_handover_and_active_low(self):
c = TIMING.InputTimingChecker(1e9, False, False)
c.update(0, 1, 1)
c.update(10, 0, 1)
events = c.update(20, 1, 0)
self.assertEqual(kinds(events), ['mintime_ok', 'deadtime'])
self.assertEqual(events[1]['duration_ns'], 0)
self.assertEqual(kinds(c.update(30, 0, 1)), ['mintime_ok', 'deadtime'])
def test_overlap_and_capture_boundaries(self):
c = TIMING.InputTimingChecker(1e9)
c.update(0, 1, 0)
c.update(10, 1, 1)
events = c.update(25, 0, 1)
self.assertEqual(kinds(events), ['overlap'])
self.assertEqual(events[0]['duration_ns'], 15)
c.update(30, 1, 1)
self.assertEqual(kinds(c.finish(50)), ['overlap'])
c = TIMING.InputTimingChecker(1e9)
c.update(0, 0, 0)
self.assertEqual(c.update(10, 1, 0), [])
self.assertEqual(c.finish(20), [])
def test_no_stale_deadtime_after_same_input_restarts(self):
c = TIMING.InputTimingChecker(1e9)
for t, a, b in [(0, 0, 0), (10, 1, 0), (20, 0, 0), (30, 1, 0), (40, 0, 0)]:
c.update(t, a, b)
self.assertEqual(c.update(50, 0, 1)[0]['duration_ns'], 10)
def test_invalid_minimums(self):
for value in (-1, float('nan'), float('inf')):
with self.assertRaises(ValueError):
TIMING.InputTimingChecker(1e9, vin2_mintime_ns=value)
class CycleResultTests(unittest.TestCase):
def cycle(self, checker, origin=0, delay=250, width=700, inverted=False):
events = checker.on_control_edge(origin + 100, not inverted)
events += checker.on_status_edge(origin + 100 + delay, 1)
events += checker.on_status_edge(origin + 100 + delay + width, 0)
events += checker.on_control_edge(origin + 3000, inverted)
events += checker.on_status_edge(origin + 3250, 1)
events += checker.on_status_edge(origin + 3950, 0)
return [e for e in events if e['kind'].startswith('cycle_')]
def test_complete_cycle_range_and_active_low(self):
for inverted in (False, True):
checker = TIMING.TimingChecker(1e9, cycle_results=True, vin_active_high=not inverted)
events = self.cycle(checker, inverted=inverted)
self.assertEqual(len(events), 1)
self.assertEqual((events[0]['kind'], events[0]['start'], events[0]['end']), ('cycle_ok', 100, 3950))
def test_warning_fault_and_next_cycle_recovery(self):
for delay, width, verdict in [(500, 700, 'warning'), (250, 100, 'fault'), (250, 2000, 'fault')]:
checker = TIMING.TimingChecker(1e9, cycle_results=True)
self.assertEqual(self.cycle(checker, delay=delay, width=width)[0]['kind'], 'cycle_' + verdict)
self.assertEqual(self.cycle(checker, origin=10000)[0]['kind'], 'cycle_ok')
def test_missing_ack_and_clipped_capture(self):
checker = TIMING.TimingChecker(1e9, cycle_results=True)
checker.on_control_edge(100, 1)
checker.expire(1500)
checker.on_control_edge(3000, 0)
checker.on_status_edge(3250, 1)
events = checker.on_status_edge(3950, 0)
self.assertEqual(events[-1]['kind'], 'cycle_fault')
checker.on_control_edge(10000, 1)
self.assertEqual(checker.finish_cycles(10100)[0]['kind'], 'cycle_warning')
self.assertEqual(checker.finish_cycles(10100), [])
def test_no_ok_before_second_ack_finishes(self):
checker = TIMING.TimingChecker(1e9, cycle_results=True)
events = checker.on_control_edge(100, 1)
events += checker.on_status_edge(350, 1)
events += checker.on_status_edge(1050, 0)
events += checker.on_control_edge(3000, 0)
events += checker.on_status_edge(3250, 1)
self.assertFalse(any(e['kind'].startswith('cycle_') for e in events))
self.assertEqual(checker.on_status_edge(3950, 0)[-1]['kind'], 'cycle_ok')

View File

@@ -0,0 +1,116 @@
"""Exercise the standalone PowerShell installer without modifying installed DSView."""
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
SHELL = shutil.which('powershell.exe') or shutil.which('pwsh')
@unittest.skipUnless(os.name == 'nt' and SHELL, 'Windows PowerShell required')
class InstallerTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='dsview installer ')
self.addCleanup(self.temp.cleanup)
self.app = Path(self.temp.name)/'DSView test'
self.app.mkdir()
(self.app/'DSView.exe').write_bytes(b'fixture, not executable')
(self.app/'decoders/common').mkdir(parents=True)
(self.app/'decoders/common/__init__.py').write_text('# existing common package\n')
(self.app/'decoders/common/keep.txt').write_text('unrelated')
def run_installer(self, *extra, script=None):
return subprocess.run([SHELL, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
str(script or ROOT/'install_decoders.ps1'),
'-DsViewPath', str(self.app), *extra], capture_output=True, text=True)
def test_install_import_and_update_backup(self):
result = self.run_installer()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
code = '''
import importlib, pathlib, sys, types
root = pathlib.Path(sys.argv[1])
sys.path.insert(0, str(root))
stub = types.ModuleType('sigrokdecode')
stub.Decoder = type('Decoder', (), {})
sys.modules['sigrokdecode'] = stub
for name in ('gate_driver_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart'):
module = importlib.import_module(name + '.pd')
assert module.Decoder.api_version == 3
core = importlib.import_module('common.setgui_decoders.gate_timing')
pathlib.Path(core.__file__).resolve().relative_to(root)
assert '1SP0635' in core.PROFILES
'''
check = subprocess.run([sys.executable, '-I', '-c', code, str(self.app/'decoders')],
cwd=self.app, capture_output=True, text=True)
self.assertEqual(check.returncode, 0, check.stderr)
target = self.app/'decoders/set_uart/pd.py'
target.write_text('# old decoder\n')
result = self.run_installer()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
backups = list((self.app/'set-decoder-backups').glob('*/set_uart/pd.py'))
self.assertEqual(len(backups), 1)
self.assertEqual(backups[0].read_text(), '# old decoder\n')
self.assertEqual(target.read_bytes(), (ROOT/'decoders/set_uart/pd.py').read_bytes())
self.assertEqual((self.app/'decoders/common/keep.txt').read_text(), 'unrelated')
self.assertEqual((self.app/'decoders/common/__init__.py').read_text(), '# existing common package\n')
for name in ('gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart'):
self.assertEqual((self.app/('decoders/common/setgui_decoders/'+name+'.py')).read_bytes(),
(ROOT.parents[1]/('python/logic_analyzer/decoders/'+name+'.py')).read_bytes())
def test_check_only_does_not_install(self):
result = self.run_installer('-CheckOnly')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse((self.app/'decoders/set_uart').exists())
self.assertFalse((self.app/'set-decoder-backups').exists())
def test_copy_failure_restores_previous_decoders(self):
old = self.app/'decoders/gate_driver_timing/pd.py'
old.parent.mkdir()
old.write_text('# original\n')
runner = Path(self.temp.name)/'inject-failure.ps1'
runner.write_text('''
param([string]$Installer, [string]$Target)
$ErrorActionPreference = 'Stop'
$global:installerTestFailed = $false
function Copy-Item {
param([string]$LiteralPath, [string]$Destination, [switch]$Force)
if (-not $global:installerTestFailed -and $Destination.Replace('\\', '/').EndsWith('/decoders/set_uart/pd.py')) {
$global:installerTestFailed = $true
throw 'Injected copy failure'
}
Microsoft.PowerShell.Management\\Copy-Item -LiteralPath $LiteralPath -Destination $Destination -Force:$Force
}
& $Installer -DsViewPath $Target
''', encoding='utf-8')
result = subprocess.run([SHELL, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
str(runner), '-Installer', str(ROOT/'install_decoders.ps1'),
'-Target', str(self.app)], capture_output=True)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(old.read_text(), '# original\n')
self.assertFalse((old.parent/'__init__.py').exists())
self.assertFalse((self.app/'decoders/set_uart/__init__.py').exists())
self.assertEqual(len(list((self.app/'set-decoder-backups').glob('*/gate_driver_timing/pd.py'))), 1, result.stderr)
def test_incomplete_target_is_untouched(self):
(self.app/'DSView.exe').unlink()
result = self.run_installer()
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.app/'decoders/set_uart').exists())
def test_missing_source_is_detected_before_changes(self):
source = Path(self.temp.name)/'empty-template/tools/dsview'
source.mkdir(parents=True)
shutil.copy2(ROOT/'install_decoders.ps1', source/'install_decoders.ps1')
result = self.run_installer(script=source/'install_decoders.ps1')
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.app/'decoders/gate_driver_timing').exists())
self.assertFalse((self.app/'set-decoder-backups').exists())
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,55 @@
"""Prevent packaging a new GUI with a stale native DSView component."""
import hashlib
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
RECIPE = Path(__file__).resolve().parents[1] / 'build.py'
SPEC = importlib.util.spec_from_file_location('dsview_runtime_build', RECIPE)
BUILD = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(BUILD)
class RuntimeTests(unittest.TestCase):
def test_legacy_runtime_and_mismatched_binary_or_recipe_require_rebuild(self):
with tempfile.TemporaryDirectory() as temporary:
output = Path(temporary)
(output / 'DSView.exe').write_bytes(b'native fixture')
(output / 'host-protocol-1').write_text('DSView host protocol 1\n')
self.assertFalse(BUILD.runtime_is_current(output))
stamp = {'recipe': 'new', 'executable': hashlib.sha256(b'native fixture').hexdigest()}
(output / 'host-build.json').write_text(json.dumps(stamp))
with patch.object(BUILD, 'recipe_digest', return_value='new'):
self.assertTrue(BUILD.runtime_is_current(output))
(output / 'DSView.exe').write_bytes(b'old native fixture')
self.assertFalse(BUILD.runtime_is_current(output))
(output / 'DSView.exe').write_bytes(b'native fixture')
with patch.object(BUILD, 'recipe_digest', return_value='changed'):
self.assertFalse(BUILD.runtime_is_current(output))
(output / 'host-build.json').write_text('partial JSON')
self.assertFalse(BUILD.runtime_is_current(output))
def test_patch_adapters_and_shared_cores_all_invalidate_runtime(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
recipe = root / 'tools/dsview'
core = root / 'python/logic_analyzer/decoders'
core.mkdir(parents=True)
(recipe / 'decoders/gate_driver_timing').mkdir(parents=True)
(recipe / 'native').mkdir()
files = [recipe / 'build.py', recipe / 'setgui-host.patch', recipe / 'gate-pair-checkbox.patch', recipe / 'decoder-panel.patch', recipe / 'driver-summary.patch', recipe / 'windows-usb-events.patch', recipe / 'native/gate_summary.h',
recipe / 'decoders/gate_driver_timing/pd.py']
files += [core / (name + '.py') for name in ('gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart')]
for item in files:
item.write_text('initial')
with patch.object(BUILD, '__file__', str(recipe / 'build.py')):
initial = BUILD.recipe_digest()
for item in files:
with self.subTest(file=item.name):
item.write_text('changed')
self.assertNotEqual(initial, BUILD.recipe_digest())
item.write_text('initial')
self.assertEqual(initial, BUILD.recipe_digest())

View File

@@ -0,0 +1,58 @@
import sys
from pathlib import Path
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / 'python'))
from logic_analyzer.decoders.transistor_pair import PairTimingChecker
class TransistorPairTests(unittest.TestCase):
def test_directional_deadtime_limits_and_equality(self):
c = PairTimingChecker(1e9, deadtime_12_ns=30, deadtime_21_ns=50)
c.update(0, 0, 0)
c.update(10, 1, 0)
c.update(110, 0, 0)
events = c.update(140, 0, 1)
self.assertEqual(events[0]['kind'], 'deadtime')
self.assertEqual(events[0]['duration_ns'], 30)
c.update(240, 0, 0)
self.assertEqual(c.update(280, 1, 0)[0]['kind'], 'deadtime_fail')
def test_independent_on_off_times_frequency_and_duty(self):
c = PairTimingChecker(1e9, vin1_mintime_ns=100, vin2_mintime_ns=101,
vin1_minoff_ns=201, vin2_minoff_ns=50)
c.update(0, 0, 0)
c.update(10, 1, 0)
self.assertEqual(c.update(110, 0, 0)[0]['kind'], 'mintime_ok')
c.update(140, 0, 1)
self.assertEqual(c.update(240, 0, 0)[0]['kind'], 'mintime_fail')
events = c.update(310, 1, 0)
self.assertTrue(any(e['kind'] == 'off_fail' for e in events))
period = next(e for e in events if e['kind'] == 'period')
self.assertAlmostEqual(period['frequency_hz'], 1e9/300)
self.assertAlmostEqual(period['duty_percent'], 100/3)
self.assertEqual((period['start'], period['end']), (10, 310))
def test_atomic_handover_active_low_and_overlap(self):
c = PairTimingChecker(1e9, False, False, deadtime_12_ns=1)
c.update(0, 1, 1)
c.update(10, 0, 1)
events = c.update(20, 1, 0)
self.assertTrue(any(e['kind'] == 'deadtime_fail' and e['duration_ns'] == 0 for e in events))
c.update(30, 0, 0)
self.assertEqual(c.finish(50)[0]['kind'], 'overlap')
self.assertTrue(any(e['kind'] == 'overlap' for e in c.update(60, 1, 0)))
def test_capture_boundary_does_not_invent_complete_pulses(self):
c = PairTimingChecker(1e9)
self.assertEqual(c.update(0, 1, 0), [])
self.assertEqual(c.update(10, 0, 0), [])
events = c.update(20, 1, 0)
self.assertEqual([e['kind'] for e in events], ['off_time'])
self.assertEqual(c.finish(30), [])
def test_invalid_limits(self):
for name in ('deadtime_12_ns', 'deadtime_21_ns', 'vin1_minoff_ns', 'vin2_minoff_ns'):
for value in (-1, float('nan'), float('inf')):
with self.subTest(name=name, value=value), self.assertRaises(ValueError):
PairTimingChecker(1e9, **{name: value})

View File

@@ -0,0 +1,96 @@
--- a/libsigrok4DSL/hardware/DSL/dslogic.c
+++ b/libsigrok4DSL/hardware/DSL/dslogic.c
@@ -1316,10 +1316,15 @@
{
int i;
sr_info("%s: remove fds from polling", __func__);
+#ifdef _WIN32
+ /* Windows libusb has no poll descriptors; remove the timer source. */
+ sr_source_remove(-1);
+#else
/* Remove fds from polling. */
for (i = 0; devc->usbfd[i] != -1; i++)
sr_source_remove(devc->usbfd[i]);
g_free(devc->usbfd);
+#endif
}
static void report_overflow(struct DSL_context *devc)
@@ -1486,6 +1491,13 @@
}
/* setup callback function for data transfer */
+#ifdef _WIN32
+ /* libusb_get_pollfds() always returns NULL on Windows. The acquisition
+ * thread pumps libusb via receive_data on a bounded timer instead. */
+ ret = sr_source_add(-1, 0, 10, receive_data, sdi);
+ if (ret != SR_OK)
+ return ret;
+#else
lupfd = libusb_get_pollfds(drvc->sr_ctx->libusb_ctx);
for (i = 0; lupfd[i]; i++);
@@ -1500,7 +1512,8 @@
devc->usbfd[i] = lupfd[i]->fd;
}
devc->usbfd[i] = -1;
- g_free(lupfd);
+ libusb_free_pollfds(lupfd);
+#endif
wr_cmd.header.dest = DSL_CTL_START;
wr_cmd.header.size = 0;
--- a/libsigrok4DSL/hardware/DSL/dscope.c
+++ b/libsigrok4DSL/hardware/DSL/dscope.c
@@ -1901,10 +1901,15 @@
{
int i;
sr_info("%s: remove fds from polling", __func__);
+#ifdef _WIN32
+ /* Windows libusb has no poll descriptors; remove the timer source. */
+ sr_source_remove(-1);
+#else
/* Remove fds from polling. */
for (i = 0; devc->usbfd[i] != -1; i++)
sr_source_remove(devc->usbfd[i]);
g_free(devc->usbfd);
+#endif
}
static int receive_data(int fd, int revents, const struct sr_dev_inst *sdi)
@@ -2088,6 +2093,13 @@
}
/* setup callback function for data transfer */
+#ifdef _WIN32
+ /* libusb_get_pollfds() always returns NULL on Windows. The acquisition
+ * thread pumps libusb via receive_data on a bounded timer instead. */
+ ret = sr_source_add(-1, 0, 10, receive_data, sdi);
+ if (ret != SR_OK)
+ return ret;
+#else
lupfd = libusb_get_pollfds(drvc->sr_ctx->libusb_ctx);
for (i = 0; lupfd[i]; i++);
@@ -2103,7 +2115,8 @@
}
devc->usbfd[i] = -1;
- g_free(lupfd);
+ libusb_free_pollfds(lupfd);
+#endif
wr_cmd.header.dest = DSL_CTL_START;
wr_cmd.header.size = 0;
--- a/libsigrok4DSL/session.c
+++ b/libsigrok4DSL/session.c
@@ -207,7 +207,8 @@
sr_dbg("Running...");
/* Do we have real sources? */
- if (session->num_sources == 1 && session->pollfds[0].fd == -1) {
+ if (session->num_sources == 1 && session->pollfds[0].fd == -1
+ && session->sources[0].timeout == 0) {
/* Dummy source, freewheel over it. */
while (session->num_sources) {
if (session->abort_session) {