243 lines
12 KiB
Python
243 lines
12 KiB
Python
"""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()
|