Files
templates/python/logic_analyzer/files.py

300 lines
13 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.
"""Digital capture readers. Store edges, never expand captures to sample arrays.
DSView layout: DreamSourceLab/DSView libsigrok4DSL/session_driver.c.
SAL v1 layout is validated against example/digitOSC; v3 RLE layout reference:
https://github.com/nemanjan00/sigrok2sal/blob/master/SAL_FORMAT.md
Unknown binary versions fail explicitly instead of guessing their layout.
"""
from __future__ import annotations
from array import array
from bisect import bisect_right
import configparser
import csv
from dataclasses import dataclass
import json
import math
from pathlib import Path
import re
import struct
import zipfile
class ImportCancelled(Exception):
pass
@dataclass
class DigitalChannel:
name: str
initial: int
edges: array
def level_at(self, time):
return self.initial ^ (bisect_right(self.edges, time) & 1)
@dataclass
class DigitalCapture:
source: str
format: str
channels: list
start: float
end: float
sample_rate: float = 0
def _check(cancel):
if cancel():
raise ImportCancelled()
def read_capture(path, progress=lambda value: None, cancel=lambda: False):
path = Path(path)
try:
if path.suffix.lower() == '.csv':
result = _csv(path, progress, cancel)
elif path.suffix.lower() in ('.sal', '.dsl'):
with zipfile.ZipFile(path) as archive:
result = (_sal if path.suffix.lower() == '.sal' else _dsl)(
path, archive, progress, cancel)
else:
raise ValueError('Выберите цифровую запись CSV, SAL или DSL.')
except (KeyError, struct.error, zipfile.BadZipFile, EOFError,
configparser.Error, UnicodeError) as exc:
raise ValueError('Повреждённый или неподдерживаемый файл: %s' % exc) from exc
_check(cancel)
if not result.channels or not math.isfinite(result.start) or not math.isfinite(result.end):
raise ValueError('Файл не содержит цифровой записи.')
progress(100)
return result
def _csv(path, progress, cancel):
size = max(1, path.stat().st_size)
consumed = 0
channels = None
previous = []
start = end = None
dsview = False
with path.open(encoding='utf-8-sig', newline='') as stream:
for number, line in enumerate(stream, 1):
consumed += len(line)
if number % 4096 == 0:
_check(cancel)
progress(min(99, int(consumed * 100 / size)))
if not line.strip():
continue
if line.lstrip().startswith((';', '#')):
dsview |= 'DSView' in line
continue
if channels is None:
delimiter = max((',', ';', '\t'), key=line.count)
names = [v.strip() for v in next(csv.reader([line], delimiter=delimiter))]
match = re.fullmatch(r'(?:time|timestamp)\s*(?:[\[(](s|ms|us|µs|ns)[\])])?', names[0], re.I)
if not match or not 2 <= len(names) <= 65 or any(not n for n in names):
raise ValueError('Ожидается CSV: Time [s], затем цифровые каналы (0/1).')
if len(set(names)) != len(names):
raise ValueError('Повторяющиеся имена каналов CSV.')
factor = {'s': 1, 'ms': 1e-3, 'us': 1e-6, 'µs': 1e-6, 'ns': 1e-9}[(match[1] or 's').lower()]
channels = [DigitalChannel(name, 0, array('d')) for name in names[1:]]
continue
row = next(csv.reader([line], delimiter=delimiter))
try:
time = float(row[0].strip().replace(',', '.')) * factor
values = [v.strip() for v in row[1:]]
if len(values) != len(channels) or any(v not in ('0', '1') for v in values):
raise ValueError('цифровые уровни должны быть 0 или 1')
if not math.isfinite(time) or (end is not None and time <= end):
raise ValueError('время должно строго возрастать и быть конечным')
except (ValueError, IndexError) as exc:
raise ValueError('CSV, строка %d: %s' % (number, exc)) from exc
if start is None:
start = time
for channel, value in zip(channels, values):
channel.initial = int(value)
else:
for i, channel in enumerate(channels):
if values[i] != previous[i]:
channel.edges.append(time)
previous = values
end = time
if start is None:
raise ValueError('CSV не содержит данных.')
return DigitalCapture(str(path), 'DSLogic CSV' if dsview else 'Logic 2 CSV', channels, start, end)
class _Binary:
def __init__(self, data):
self.data, self.pos = data, 0
def take(self, size):
if size < 0 or self.pos + size > len(self.data):
raise ValueError('Обрезанный двоичный блок.')
value = self.data[self.pos:self.pos + size]
self.pos += size
return value
def unpack(self, fmt):
return struct.unpack('<' + fmt, self.take(struct.calcsize('<' + fmt)))
def _runs(data):
pos = 0
while pos < len(data):
value = data[pos]
pos += 1
if value >= 128:
raise ValueError('Некорректный RLE SAL.')
if value & 64:
value &= 63
for _ in range(9):
if pos == len(data):
raise ValueError('Обрезанный RLE SAL.')
byte = data[pos]
pos += 1
value = (value << 7) | (byte & 127)
if not byte & 128:
break
else:
raise ValueError('Слишком длинный RLE SAL.')
yield value + 1
def _sal_channel(data, name, cancel):
reader = _Binary(data)
if reader.take(8) != b'<SALEAE>':
raise ValueError('Неверная сигнатура SAL.')
version, kind = reader.unpack('II')
if version not in (1, 3, 4) or kind != 100:
raise ValueError('Не поддерживается цифровой SAL версии %d, тип %d.' % (version, kind))
schema, rate, unix_ms, fractional_ms = reader.unpack('BdQd')
if schema != 1 or not math.isfinite(rate) or rate <= 0:
raise ValueError('Некорректный заголовок канала SAL.')
for _ in range(2):
optional, = reader.unpack('B')
if optional not in (0, 1):
raise ValueError('Неизвестный заголовок SAL.')
if optional:
reader.take(8)
count, = reader.unpack('Q')
if not 0 < count <= len(data) // 26:
raise ValueError('Неверное число блоков SAL.')
channel = DigitalChannel(name, 0, array('d'))
previous_end = None
level = 0
for chunk in range(count):
_check(cancel)
if version == 1:
begin, end, samples, chunk_rate, scale, length = reader.unpack('6Q')
if samples != end - begin or chunk_rate != rate or scale != 1:
raise ValueError('Неподдерживаемая шкала блока SAL.')
encoded = reader.take(length)
entries, = reader.unpack('Q')
if not entries:
raise ValueError('В SAL отсутствует индекс начального уровня.')
index = reader.take(entries * 20)
offset, byte_offset, initial = struct.unpack_from('<QQI', index)
if offset or byte_offset:
raise ValueError('Некорректный индекс SAL.')
else:
begin, end, initial, flags, length = reader.unpack('QQBBQ')
if flags:
raise ValueError('Неизвестные флаги SAL.')
encoded = reader.take(length)
if initial not in (0, 1) or end <= begin or (previous_end is not None and begin != previous_end):
raise ValueError('Некорректные границы или уровень блока SAL.')
if chunk == 0:
first = begin
channel.initial = initial
elif initial != level:
channel.edges.append(begin / rate)
level = initial
position = begin
for run_index, run in enumerate(_runs(encoded)):
if run_index % 8192 == 0:
_check(cancel)
position += run
if position > end:
raise ValueError('RLE выходит за границы блока SAL.')
if position < end:
channel.edges.append(position / rate)
level ^= 1
if position != end:
raise ValueError('Неполный RLE блока SAL.')
previous_end = end
if reader.pos != len(data):
raise ValueError('Неизвестные дополнительные данные SAL.')
return channel, first / rate, previous_end / rate, rate
def _sal(path, archive, progress, cancel):
meta = json.loads(archive.read('meta.json'))
names = {}
for row in meta.get('data', {}).get('rowsSettings', []):
channel = row.get('channel', {})
if channel.get('type') == 'Digital':
names[channel.get('deviceChannel')] = row.get('name')
files = sorted((int(m[1]), name) for name in archive.namelist()
for m in [re.fullmatch(r'digital-(\d+)\.bin', name)] if m)
if not files:
raise ValueError('В SAL нет цифровых каналов.')
channels, bounds = [], []
for i, (index, filename) in enumerate(files):
_check(cancel)
channel, start, end, rate = _sal_channel(archive.read(filename), names.get(index) or 'D%d' % index, cancel)
channels.append(channel)
bounds.append((start, end, rate))
progress(int((i + 1) * 100 / len(files)))
if any(bound != bounds[0] for bound in bounds):
raise ValueError('SAL с разными временными границами каналов не поддерживается.')
return DigitalCapture(str(path), 'Logic 2 SAL', channels, *bounds[0])
def _dsl(path, archive, progress, cancel):
config = configparser.ConfigParser(interpolation=None)
config.read_string(archive.read('header').decode('utf-8-sig'))
version = config.getint('version', 'version')
header = config['header']
if version not in (2, 3) or header.getint('device mode', 0) != 0:
raise ValueError('Поддерживаются цифровые сессии DSView DSL версий 2 и 3.')
match = re.fullmatch(r'\s*([\d.]+)\s*([kKmMgG]?)(?:Hz)?\s*', header['samplerate'])
if not match:
raise ValueError('Неверная частота DSL.')
rate = float(match[1]) * {'': 1, 'k': 1e3, 'm': 1e6, 'g': 1e9}[match[2].lower()]
samples = header.getint('total samples')
blocks = header.getint('total blocks')
if not math.isfinite(rate) or rate <= 0 or samples <= 0 or blocks <= 0:
raise ValueError('Некорректный размер или частота DSL.')
probes = sorted((int(m[1]), name) for key, name in header.items()
for m in [re.fullmatch(r'probe(\d+)', key)] if m)
channels = []
for i, (index, name) in enumerate(probes):
channel = DigitalChannel(name, 0, array('d'))
position, level = 0, 0
for block in range(blocks):
_check(cancel)
# DSView v3 stores enabled channels in sequential directories.
directory = i if version == 3 else index
data = archive.read('L-%d/%d' % (directory, block))
for offset, byte in enumerate(data):
if offset % 65536 == 0:
_check(cancel)
if position >= samples:
break
if position == 0:
channel.initial = level = byte & 1
changes = byte ^ ((byte << 1) & 255 | level)
while changes:
bit = (changes & -changes).bit_length() - 1
if position + bit < samples:
channel.edges.append((position + bit) / rate)
changes &= changes - 1
level = byte >> 7
position += 8
if position < samples:
raise ValueError('Обрезанные данные DSL, канал %s.' % name)
channels.append(channel)
progress(int((i + 1) * 100 / max(1, len(probes))))
return DigitalCapture(str(path), 'DSLogic DSL', channels, 0, samples / rate, rate)