Add DSLogic capture, signal conversion and DSView decoders
This commit is contained in:
242
python/logic_analyzer/dsview_helper.py
Normal file
242
python/logic_analyzer/dsview_helper.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user