"""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()