Files
templates/python/logic_analyzer/dslogic.py

206 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""DSLogic ONLINE through an isolated DSView/libsigrok4DSL host."""
from array import array
from dataclasses import dataclass, asdict
import json
import math
import os
from pathlib import Path
import re
import subprocess
import struct
import tempfile
import time
from .files import DigitalCapture, DigitalChannel, ImportCancelled
from .dslogic_trigger import validate_trigger
DRIVER = 'dsview'
@dataclass(frozen=True)
class DSLogicDevice:
device_id: str
name: str
channels: tuple
@dataclass(frozen=True)
class DSLogicSettings:
device_id: str
executable: str = 'setgui-dslogic'
channels: tuple = (0, 1)
sample_rate: int = 1000000
duration: float = 1.0
threshold: float = None
buffer_mb: int = 256
acquisition_mode: str = 'stream'
trigger: dict = None
def validate(self):
if not re.fullmatch(r'dsview:\d+\.\d+', self.device_id):
raise ValueError('Выберите устройство DSLogic; повторите поиск устройств.')
if not self.executable.strip():
raise ValueError('Не найден модуль DSView. Пересоберите или переустановите SETGUI.')
if (not self.channels or len(set(self.channels)) != len(self.channels)
or any(type(c) is not int or not 0 <= c < 32 for c in self.channels)):
raise ValueError('Выберите цифровые каналы без повторений.')
if type(self.sample_rate) is not int or not 1 <= self.sample_rate <= 400000000:
raise ValueError('Частота должна быть от 1 до 400000000 S/s.')
if not math.isfinite(self.duration) or not 0 <= self.duration <= 3600:
raise ValueError('Длительность должна быть от 0 до 3600 с.')
if self.threshold is not None and (not math.isfinite(self.threshold) or not 0 <= self.threshold <= 5):
raise ValueError('Порог должен быть от 0 до 5 V.')
if not 16 <= self.buffer_mb <= 4096:
raise ValueError('Лимит записи должен быть от 16 до 4096 MB.')
if self.acquisition_mode not in ('buffer', 'stream', 'internal'):
raise ValueError('Неизвестный режим прибора.')
if self.duration == 0 and self.acquisition_mode != 'stream':
raise ValueError('Режим «До остановки» доступен только для потокового захвата.')
validate_trigger(self.trigger, self.channels, self.acquisition_mode)
def _popen(arguments, **kwargs):
try:
# A frozen GUI must not pass its extraction directory to the separate
# PyInstaller host (especially PyInstaller 5's _MEIPASS2 protocol).
environment = {k: v for k, v in os.environ.items()
if k != '_MEIPASS2' and not k.startswith('_PYI_')}
kwargs.setdefault('env', environment)
return subprocess.Popen(arguments, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0), **kwargs)
except OSError as exc:
raise RuntimeError('Не удалось запустить модуль DSView. Нужен setgui-dslogic.exe из сборки SETGUI. %s' % exc) from exc
def _terminate(process):
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=3)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=3)
def _request(executable, request, directory, stop, cancel, status):
directory = Path(directory)
(directory / 'request.json').write_text(json.dumps(request), encoding='utf-8')
with tempfile.TemporaryFile() as log:
process = _popen([executable, str(directory)], stdin=subprocess.DEVNULL, stdout=log, stderr=log)
try:
startup_deadline = time.monotonic() + 30
capture_deadline = None
stop_deadline = None
ready = False
waiting_trigger = False
while process.poll() is None:
now = time.monotonic()
if cancel():
raise ImportCancelled()
if request['action'] == 'capture' and not ready and (directory / 'ready').exists():
ready = True
duration = request['settings']['duration']
trigger = request['settings'].get('trigger') or {'kind': 'none'}
triggered = trigger.get('kind', 'none') != 'none'
waiting_trigger = triggered
timeout = trigger.get('timeout', 30)
capture_deadline = (now + duration + timeout + 35 if timeout else None) if triggered else (now + duration + 35 if duration else None)
status('Ожидание триггера DSLogic…' if triggered else 'Захват DSLogic…')
if waiting_trigger and (directory / 'triggered').exists():
waiting_trigger = False
capture_deadline = now + duration + 35 if duration else None
if stop_deadline is None:
status('Триггер сработал. Получение записи DSLogic…')
if stop() and stop_deadline is None:
(directory / 'stop').touch()
stop_deadline = now + 10
status('Остановка DSView и получение записи…')
if ((not ready and now > startup_deadline) or
(capture_deadline is not None and now > capture_deadline) or
(stop_deadline is not None and now > stop_deadline)):
raise RuntimeError('Модуль DSView не отвечает. Проверьте USB и закройте DSView.')
time.sleep(.02)
if cancel():
raise ImportCancelled()
result_file = directory / 'result.json'
if not result_file.exists():
log.seek(0)
detail = log.read(4096).decode('utf-8', 'replace')
raise RuntimeError('Модуль DSView завершился с кодом %s. %s' % (process.returncode, detail))
result = json.loads(result_file.read_text(encoding='utf-8'))
if result.get('error'):
raise RuntimeError(result['error'])
if process.returncode:
raise RuntimeError('Модуль DSView завершился с кодом %s.' % process.returncode)
return result
finally:
_terminate(process)
def list_devices(executable='setgui-dslogic', cancel=lambda: False):
with tempfile.TemporaryDirectory(prefix='setgui-dsview-') as directory:
result = _request(executable, {'action': 'scan'}, directory, lambda: False, cancel, lambda _: None)
return [DSLogicDevice(d['device_id'], d['name'], tuple(d['channels'])) for d in result['devices']]
def read_cross(stream, metadata, settings, cancel=lambda: False):
"""Convert channel-interleaved 64-bit words to edges without expanding samples."""
bits = metadata['channels']
samples, rate = metadata['samples'], metadata['sample_rate']
if (metadata.get('format') != 'cross64-le' or bits != sorted(settings.channels)
or type(samples) is not int or samples <= 0 or not math.isfinite(rate) or rate <= 0
or (settings.acquisition_mode != 'internal' and rate != settings.sample_rate)):
raise ValueError('Некорректные параметры записи DSView.')
channels = [DigitalChannel('D%d' % bit, 0, array('d')) for bit in bits]
group = struct.Struct('<' + 'Q' * len(bits))
offset = edges = 0
previous = [0] * len(bits)
while offset < samples:
if cancel():
raise ImportCancelled()
groups = min(4096, (samples - offset + 63) // 64)
raw = stream.read(groups * group.size)
if len(raw) != groups * group.size:
raise ValueError('Обрезанная запись DSView.')
for words in group.iter_unpack(raw):
length = min(64, samples - offset)
mask = (1 << length) - 1
for index, word in enumerate(words):
channel = channels[index]
if offset == 0:
channel.initial = word & 1
previous[index] = channel.initial
changed = (word ^ ((word << 1) | previous[index])) & mask
while changed:
low_bit = changed & -changed
channel.edges.append((offset + low_bit.bit_length() - 1) / rate)
changed ^= low_bit
edges += 1
if edges * 8 > settings.buffer_mb * 1024 * 1024:
raise ValueError('Превышен лимит памяти фронтов. Уменьшите длительность или число каналов.')
previous[index] = (word >> (length - 1)) & 1
offset += length
source = metadata.get('name', 'DSLogic') + ' · %g MS/s' % (rate / 1e6)
if metadata.get('limited'):
source += ' · достигнут лимит записи'
capture = DigitalCapture(source, 'DSLogic ONLINE · DSView', channels, 0, samples / rate, rate)
trigger_sample = metadata.get('trigger_sample')
if trigger_sample is not None and 0 <= trigger_sample < samples:
capture.trigger_time = trigger_sample / rate
return capture
def acquire(settings, stop, cancel, status=lambda text: None):
settings.validate()
if cancel():
raise ImportCancelled()
status('Подключение к DSLogic через DSView…')
with tempfile.TemporaryDirectory(prefix='setgui-dsview-') as directory:
result = _request(settings.executable, {'action': 'capture', 'settings': asdict(settings)},
directory, stop, cancel, status)
status('Построение цифровых каналов…')
with (Path(directory) / 'capture.bin').open('rb') as stream:
capture = read_cross(stream, result, settings, cancel)
if cancel():
raise ImportCancelled()
return capture