Add DSLogic capture, signal conversion and DSView decoders

This commit is contained in:
2026-09-27 01:45:08 +03:00
parent cc22c803d1
commit 513e79b127
44 changed files with 5982 additions and 36 deletions

View File

@@ -4,10 +4,12 @@ from __future__ import annotations
from bisect import bisect_left, bisect_right
from dataclasses import dataclass
import heapq
from itertools import groupby
import math
from .files import ImportCancelled
from .decoders.gate_timing import TimingChecker
from .decoders.transistor_pair import PairTimingChecker
from .decoders.set_uart import StreamParser
from .decoders.pm35_uart import PM35Parser
from .decoders.set_can import legacy, Reassembler
@@ -84,7 +86,7 @@ def uart_frames(channel, start, end, baud, parity='none', stops=1, inverted=Fals
continue
finish = time + bits * bit
if finish > end + bit * 1e-6:
yield time, end, None, 'Неполный UART-байт в конце диапазона'
yield time, end, None, 'Неполный UART-байт в конце диапазона'
break
read = lambda pos: channel.level_at(time + pos * bit) ^ inverted
if read(0.5):
@@ -94,10 +96,10 @@ def uart_frames(channel, start, end, baud, parity='none', stops=1, inverted=Fals
value = sum(v << i for i, v in enumerate(values))
error = None
if parity != 'none' and read(9.5) != ((sum(values) + (parity == 'odd')) & 1):
error = 'Ошибка чётности UART'
error = 'Ошибка чётности UART'
stop_start = 9 + (parity != 'none')
if any(read(stop_start + i + 0.5) != 1 for i in range(stops)):
error = 'Ошибка стопового бита UART / BREAK'
error = 'Ошибка стопового бита UART / BREAK'
yield time, finish, value, error
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
@@ -142,7 +144,7 @@ class _CanBits:
break
sample = boundary + self.sample_point * self.bit
if sample >= self.end:
raise EOFError('Неполный CAN-кадр в конце диапазона')
raise EOFError('Неполный CAN-кадр в конце диапазона')
self.last_sample = sample
self.position += 1
return self.channel.level_at(sample) ^ self.inverted
@@ -151,7 +153,7 @@ class _CanBits:
if self.run == 5:
value = self.raw()
if value == self.previous:
raise ValueError('CAN: ошибка bit stuffing / error frame')
raise ValueError('CAN: ошибка bit stuffing / error frame')
self.previous, self.run = value, 1
value = self.raw()
self.run = self.run + 1 if value == self.previous else 1
@@ -181,34 +183,34 @@ def can_frames(channel, start, end, bitrate, inverted=False, sample_point=.7, ca
frame, error = None, None
try:
if reader.take(1):
raise ValueError('CAN: неверный SOF')
raise ValueError('CAN: неверный SOF')
ident = reader.take(11)
rtr, extended = reader.take(1), reader.take(1)
if extended:
if rtr != 1:
raise ValueError('CAN: неверный SRR')
raise ValueError('CAN: неверный SRR')
ident = (ident << 18) | reader.take(18)
rtr = reader.take(1)
if reader.take(2):
raise ValueError('CAN FD / reserved bits не поддерживаются')
raise ValueError('CAN FD / reserved bits не поддерживаются')
elif reader.take(1):
raise ValueError('CAN FD / reserved bit не поддерживается')
raise ValueError('CAN FD / reserved bit не поддерживается')
dlc = reader.take(4)
if dlc > 8:
raise ValueError('CAN: поддерживается classic DLC 0…8')
raise ValueError('CAN: поддерживается classic DLC 0…8')
data = bytes(reader.take(8) for _ in range(0 if rtr else dlc))
expected = can_crc(reader.bits)
received = reader.take(15)
if reader.run == 5:
if reader.raw() == reader.previous:
raise ValueError('CAN: неверный последний stuff bit')
raise ValueError('CAN: неверный последний stuff bit')
if reader.raw() != 1:
raise ValueError('CAN: неверный CRC delimiter')
raise ValueError('CAN: неверный CRC delimiter')
ack = reader.raw() == 0
if reader.raw() != 1 or any(reader.raw() != 1 for _ in range(7)):
raise ValueError('CAN: неверный ACK delimiter / EOF')
raise ValueError('CAN: неверный ACK delimiter / EOF')
if received != expected:
raise ValueError('CAN CRC15: получено %04X, ожидается %04X' % (received, expected))
raise ValueError('CAN CRC15: получено %04X, ожидается %04X' % (received, expected))
frame = dict(ident=ident, extended=bool(extended), remote=bool(rtr),
data=data, dlc=dlc, ack=ack)
except (ValueError, EOFError) as exc:
@@ -222,14 +224,14 @@ def analyze_capture(capture, options, start=None, end=None, progress=lambda n: N
start = capture.start if start is None else max(capture.start, start)
end = capture.end if end is None else min(capture.end, end)
if end <= start:
raise ValueError('Выберите непустой интервал анализа.')
raise ValueError('Выберите непустой интервал анализа.')
mode = options['mode']
channel = capture.channels[options.get('channel', 0)]
events = []
last_progress = -1
limit = options.get('max_events', 50000)
if limit <= 0:
raise ValueError('Лимит результатов должен быть положительным.')
raise ValueError('Лимит результатов должен быть положительным.')
def emit(a, b, kind, text, details=''):
if len(events) >= limit:
@@ -260,8 +262,10 @@ def analyze_capture(capture, options, start=None, end=None, progress=lambda n: N
if mode in ('1SP0635', '1SD536F2'):
other = options.get('status_channel', 1)
if other == options.get('channel', 0):
raise ValueError('Vin и Vstat должны быть разными каналами.')
raise ValueError('Vin1 и Vstat должны быть разными каналами.')
status = capture.channels[other]
if options.get('pair_analysis'):
raise ValueError('Use the Transistor pair analyzer for Vin1/Vin2 timing.')
checker = TimingChecker(1e9, mode, options.get('tolerance_ns', 100),
not options.get('vin_low', False), not options.get('status_low', False))
# The pure checker works in integer ticks. Offset before rounding
@@ -272,7 +276,7 @@ def analyze_capture(capture, options, start=None, end=None, progress=lambda n: N
for item in items:
text = item['text']
if item['kind'] == 'fault':
text += ' · аварийная обратная связь; превышение тока не подтверждено'
text += ' · аварийная обратная связь; превышение тока не подтверждено'
emit(start + item['start'] / 1e9, start + item['end'] / 1e9, item['kind'], text)
def transitions(ch, which):
@@ -281,21 +285,48 @@ def analyze_capture(capture, options, start=None, end=None, progress=lambda n: N
yield ch.edges[i], which, ch.initial ^ ((i + 1) & 1)
if bool(status.level_at(start)) == checker.vstat_active_high:
emit(start, start, 'orphan', 'Vstat активен в начале диапазона: начало импульса не записано')
for time, which, level in heapq.merge(transitions(channel, 0), transitions(status, 1)):
emit(start, start, 'orphan', 'Vstat активен в начале диапазона: начало импульса не записано')
streams = [transitions(channel, 0), transitions(status, 1)]
for time, which, level in heapq.merge(*streams):
report(time)
timing(checker.expire(tick(time)))
timing(checker.on_control_edge(tick(time), level) if which == 0 else checker.on_status_edge(tick(time), level))
timing(checker.on_control_edge(tick(time), level) if which == 0
else checker.on_status_edge(tick(time), level))
timing(checker.expire(tick(end)))
if checker.status_start is not None:
emit(start + checker.status_start / 1e9, end, 'incomplete', 'Vstat: импульс не завершён в диапазоне')
emit(start + checker.status_start / 1e9, end, 'incomplete', 'Vstat: импульс не завершён в диапазоне')
for pending in checker.pending:
emit(start + pending['sample'] / 1e9, end, 'incomplete', 'Vin: диапазон закончился до тайм-аута ACK')
emit(start + pending['sample'] / 1e9, end, 'incomplete', 'Vin1: диапазон закончился до тайм-аута ACK')
elif mode == 'Transistor pair':
second = options.get('vin2_channel', 1)
if second == options.get('channel', 0):
raise ValueError('Vin1 and Vin2 must be different channels.')
vin2 = capture.channels[second]
numeric = {name: options.get(name, 0) for name in (
'vin1_mintime_ns', 'vin2_mintime_ns', 'vin1_minoff_ns', 'vin2_minoff_ns',
'deadtime_12_ns', 'deadtime_21_ns')}
checker = PairTimingChecker(1e9, not options.get('vin_low', False),
not options.get('vin2_low', False), **numeric)
levels = [channel.level_at(start), vin2.level_at(start)]
checker.update(0, *levels)
def edges(ch, which):
for index in range(bisect_right(ch.edges, start), bisect_right(ch.edges, end)):
yield ch.edges[index], which, ch.initial ^ ((index + 1) & 1)
def emit_pair(items):
for item in items:
emit(start + item['start'] / 1e9, start + item['end'] / 1e9,
item['kind'], item['text'])
for time, group in groupby(heapq.merge(edges(channel, 0), edges(vin2, 1)), key=lambda e: e[0]):
report(time)
for _, which, level in group:
levels[which] = level
emit_pair(checker.update(round((time - start) * 1e9), *levels))
emit_pair(checker.finish(round((end - start) * 1e9)))
elif mode in ('UART', 'SET UART', 'PM35 UART'):
baud = options.get('baudrate', 115200)
parity, stops = options.get('parity', 'none'), options.get('stops', 1)
if not math.isfinite(baud) or baud <= 0 or parity not in ('none', 'even', 'odd') or stops not in (1, 2):
raise ValueError('Некорректные настройки UART.')
raise ValueError('Некорректные настройки UART.')
parser = (StreamParser(options.get('protocol', 'auto')) if mode == 'SET UART' else
PM35Parser(options.get('role', 'response')) if mode == 'PM35 UART' else None)
gap = (3.5 * (9 + (parity != 'none') + stops) / baud if mode == 'PM35 UART'
@@ -320,7 +351,7 @@ def analyze_capture(capture, options, start=None, end=None, progress=lambda n: N
bitrate = options.get('baudrate', 1000000)
sample_point = options.get('sample_point', 70) / 100
if not math.isfinite(bitrate) or bitrate <= 0 or not .1 <= sample_point <= .95:
raise ValueError('Некорректный битрейт / точка выборки CAN.')
raise ValueError('Некорректный битрейт / точка выборки CAN.')
reassembler = Reassembler()
for a, b, frame, error in can_frames(channel, start, end, bitrate, options.get('inverted', False), sample_point, cancel):
report(a)
@@ -328,7 +359,7 @@ def analyze_capture(capture, options, start=None, end=None, progress=lambda n: N
emit(a, b, 'error', error)
# Never bridge an invalid/missing physical frame.
for pending in reassembler.pending.values():
emit(pending['start'], b, 'incomplete', 'SET CAN: сборка прервана ошибкой шины')
emit(pending['start'], b, 'incomplete', 'SET CAN: сборка прервана ошибкой шины')
reassembler.pending.clear()
continue
ident, data = frame['ident'], frame['data']
@@ -347,9 +378,9 @@ def analyze_capture(capture, options, start=None, end=None, progress=lambda n: N
except ValueError as exc:
emit(a, b, 'error', str(exc))
for pending in reassembler.pending.values():
emit(pending['start'], end, 'incomplete', 'Неполная сборка SET CAN в конце диапазона')
emit(pending['start'], end, 'incomplete', 'Неполная сборка SET CAN в конце диапазона')
else:
raise ValueError('Неизвестный анализатор: ' + mode)
raise ValueError('Неизвестный анализатор: ' + mode)
except OverflowError:
truncated = True
_cancel(cancel)