Files
templates/python/logic_analyzer/dsview_helper.py

243 lines
12 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.
"""Isolated DSView driver host. JSON request/result files, raw CROSS_DATA spool.
Native callbacks and their references live until process exit. Never import
this module's native dependencies into the Qt process.
"""
import json
import os
from pathlib import Path
import re
import sys
import threading
import time
def run(request, directory):
import pydsview
from pydsview import Config, DeviceType, DeviceMode
from pydsview._binding import ffi, lib
from pydsview._constants import PacketType, Event
from pydsview.errors import check_sr
# The upstream library is a process-global singleton with async callbacks.
# This process owns exactly one operation and explicitly closes the device.
context = pydsview.DSContext()
devices = []
selected = None
for info in context.list_devices():
if 'DSLogic' not in info.name:
continue
device = context.activate_device(info.handle)
if device.device_type != DeviceType.USB or device.mode != DeviceMode.LOGIC:
continue
connection = device.get_config(Config.CONN)
ident = 'dsview:' + str(connection)
item = dict(device_id=ident, name=device.name,
channels=[c.index for c in device.channels])
devices.append(item)
if request['action'] == 'capture' and ident == request['settings']['device_id']:
selected = device
break
if request['action'] == 'scan':
lib.ds_close_all_device()
return {'devices': devices}
if selected is None:
raise RuntimeError('Выбранный DSLogic отключён. Повторите поиск устройств.')
options = request['settings']
from logic_analyzer.dslogic import DSLogicSettings
from logic_analyzer.dslogic_trigger import program_trigger
DSLogicSettings(**options).validate()
acquisition = options.get('acquisition_mode', 'stream')
selected.set_config(Config.OPERATION_MODE, {'buffer': 0, 'stream': 1, 'internal': 2}[acquisition])
if acquisition != 'stream':
selected.set_config(Config.BUFFER_OPTIONS, 1) # Upload on manual stop, as in DSView.
trigger = options.get('trigger') or {'kind': 'none'}
trigger_enabled = trigger.get('kind', 'none') != 'none'
# The driver accepts arbitrary samplerate values without validating them.
# Select a supported channel mode and verify against its advertised rates.
ffi.cdef('''
struct setgui_list_item { int id; const char *name; };
void *g_variant_lookup_value(void *, const char *, const void *);
const void *g_variant_get_fixed_array(void *, size_t *, size_t);
void g_variant_unref(void *);
struct setgui_trigger_pos { uint32_t check_id, real_pos, ram_saddr, remain_cnt_l, remain_cnt_h, status; };
''')
glib = ffi.dlopen(str(Path(pydsview.__file__).parent / '_libs/libglib-2.0-0.dll'))
modes_variant = ffi.new('GVariant *[1]')
check_sr(lib.ds_get_actived_device_config_list(ffi.NULL, Config.CHANNEL_MODE, modes_variant), 'Режимы каналов DSView')
try:
modes = ffi.cast('struct setgui_list_item *', lib.pyds_gvariant_get_uint64(modes_variant[0]))
selected_mode = None
for index in range(64):
if modes[index].id == -1:
break
title = ffi.string(modes[index].name).decode()
match = re.fullmatch(r'Use (\d+) Channels \(Max (\d+)(MHz|GHz)\)', title)
buffered = re.fullmatch(r'Use Channels 0~(\d+) \(Max (\d+)(MHz|GHz)\)', title)
capacity = int(match[1]) if match else int(buffered[1])+1 if buffered else 0
detail = match or buffered
maximum = int(detail[2]) * (1000000000 if detail[3] == 'GHz' else 1000000) if detail else 0
fits = (len(options['channels']) <= capacity if match else max(options['channels']) < capacity)
if fits and (acquisition == 'internal' or maximum >= options['sample_rate']):
selected_mode = modes[index].id
break
if selected_mode is None:
raise ValueError('Частота или число каналов не поддерживаются в выбранном режиме DSView.')
selected.set_config(Config.CHANNEL_MODE, selected_mode)
finally:
lib.pyds_gvariant_unref(modes_variant[0])
rates_variant = ffi.new('GVariant *[1]')
check_sr(lib.ds_get_actived_device_config_list(ffi.NULL, Config.SAMPLERATE, rates_variant), 'Частоты DSView')
rate_array = glib.g_variant_lookup_value(rates_variant[0], b'samplerates', ffi.NULL)
try:
if rate_array == ffi.NULL:
raise ValueError('DSView не вернул доступные частоты.')
count_rates = ffi.new('size_t *')
values = ffi.cast('uint64_t *', glib.g_variant_get_fixed_array(rate_array, count_rates, 8))
rates = [int(values[i]) for i in range(count_rates[0])]
if acquisition != 'internal' and options['sample_rate'] not in rates:
raise ValueError('Допустимые частоты, S/s: ' + ', '.join(map(str, rates)))
finally:
if rate_array != ffi.NULL:
glib.g_variant_unref(rate_array)
lib.pyds_gvariant_unref(rates_variant[0])
available = {c.index for c in selected.channels}
channels = sorted(options['channels'])
if not set(channels).issubset(available):
raise ValueError('Выбранные каналы недоступны в этом режиме.')
for channel in selected.channels:
selected.enable_channel(channel.index, channel.index in channels)
if acquisition != 'internal':
selected.samplerate = options['sample_rate']
rate = selected.samplerate
if acquisition != 'internal' and rate != options['sample_rate']:
raise ValueError('Устройство изменило частоту. Выберите поддерживаемую частоту.')
if options['threshold'] is not None:
selected.set_config(Config.VTH, float(options['threshold']))
# Round storage to complete groups: 64 samples per enabled channel.
cap_groups = options['buffer_mb'] * 1024 * 1024 // (8 * len(channels))
requested = max(1, round(options['duration'] * rate)) if options['duration'] else cap_groups * 64
limit = min(requested, cap_groups * 64)
if acquisition != 'stream':
limit = min(limit, int(selected.get_config(Config.HW_DEPTH)))
if limit < 1024:
raise ValueError('Для буферного захвата нужно не менее 1024 выборок. Увеличьте длительность.')
# Buffer hardware transfers whole 1024-sample blocks. Round the request
# up, then trim transport padding back to the requested samples on import.
selected.sample_count = ((limit + 1023) // 1024 * 1024) if acquisition != 'stream' else limit
program_trigger(lib, trigger, acquisition, check_sr)
done = threading.Event()
overflow = threading.Event()
errors = []
count = [0]
trigger_sample = [None]
trigger_seen = threading.Event()
ceiling = ((limit + 63) // 64) * 8 * len(channels)
stream = open(directory / 'capture.bin', 'wb')
@ffi.callback('void(const void*, const struct sr_datafeed_packet*)')
def data_callback(_device, packet):
try:
if packet.status:
errors.append('Ошибка пакета DSView: %d' % packet.status)
overflow.set()
if packet.type == PacketType.TRIGGER:
info = ffi.cast('const struct setgui_trigger_pos *', packet.payload)
if info.status & 1:
trigger_sample[0] = int(info.real_pos)
trigger_seen.set()
(directory / 'triggered').touch()
elif packet.type == PacketType.LOGIC:
logic = ffi.cast('const struct sr_datafeed_logic *', packet.payload)
if logic.format != 0 or logic.data_error:
raise ValueError('Неподдерживаемый формат или ошибка данных DSView.')
size = min(int(logic.length), ceiling - count[0])
if size > 0:
stream.write(ffi.buffer(ffi.cast('const char *', logic.data), size))
count[0] += size
if count[0] >= ceiling:
overflow.set()
elif packet.type == PacketType.OVERFLOW:
errors.append('Переполнение USB-потока DSLogic. Уменьшите частоту.')
overflow.set()
except Exception as exc:
errors.append(str(exc))
overflow.set()
@ffi.callback('void(int)')
def event_callback(event):
if event in (Event.COLLECT_TASK_END_BY_DETACHED, Event.COLLECT_TASK_END_BY_ERROR):
errors.append('DSLogic отключён или захват завершился с ошибкой (%d).' % event)
if event in (Event.COLLECT_TASK_END, Event.COLLECT_TASK_END_BY_DETACHED,
Event.COLLECT_TASK_END_BY_ERROR):
done.set()
lib.ds_set_datafeed_callback(data_callback)
lib.ds_set_event_callback(event_callback)
# Keep callbacks alive even if an exception unwinds this frame.
global _callbacks
_callbacks = (data_callback, event_callback)
started = False
try:
check_sr(lib.ds_start_collect(), 'Не удалось начать захват DSView')
started = True
(directory / 'ready').touch()
waiting = trigger_enabled
timeout = trigger.get('timeout', 30)
deadline = (time.monotonic() + timeout if timeout else None) if waiting else (time.monotonic() + limit / rate + 30 if options['duration'] else None)
stopped = False
while not done.wait(.02):
if waiting and trigger_seen.is_set():
waiting = False
deadline = time.monotonic() + limit / rate + 30 if options['duration'] else None
if (directory / 'stop').exists() or overflow.is_set() or (deadline and time.monotonic() > deadline):
stopped = True
check_sr(lib.ds_stop_collect(), 'Не удалось остановить DSView')
if not done.wait(5):
raise RuntimeError('DSView не подтвердил остановку захвата.')
if deadline and time.monotonic() > deadline:
raise RuntimeError('Истекло время ожидания триггера DSLogic.' if waiting else 'Истекло время ожидания данных DSLogic.')
break
if errors:
raise RuntimeError(errors[0])
stream.flush()
group_bytes = 8 * len(channels)
if count[0] % group_bytes:
raise ValueError('Неполная группа выборок DSLogic.')
samples = min(limit, count[0] // group_bytes * 64)
if not samples:
raise ValueError('DSLogic не вернул выборки.')
return dict(channels=channels, sample_rate=rate, samples=samples,
format='cross64-le', stopped=stopped, name=selected.name + ' · ' + acquisition,
trigger_sample=trigger_sample[0] if trigger_enabled else None,
limited=limit < requested or not options['duration'] and samples >= limit)
finally:
if started and lib.ds_is_collecting():
lib.ds_stop_collect()
done.wait(5)
# Keep stream open for late callbacks until OS process cleanup.
lib.ds_close_all_device()
def main():
directory = Path(sys.argv[1]).resolve()
code = 0
try:
request = json.loads((directory / 'request.json').read_text(encoding='utf-8'))
result = run(request, directory)
except BaseException as exc:
result = {'error': str(exc)}
code = 1
(directory / 'result.json').write_text(json.dumps(result, ensure_ascii=False), encoding='utf-8')
# DSView owns native threads. Interpreter finalization can invalidate cffi
# callbacks before they exit. All device I/O was closed before this point.
os._exit(code)
if __name__ == '__main__':
main()