187 lines
9.2 KiB
Python
187 lines
9.2 KiB
Python
from array import array
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
import struct
|
|
import unittest
|
|
|
|
from logic_analyzer.files import DigitalChannel, DigitalCapture, read_capture, ImportCancelled
|
|
from logic_analyzer.analysis import analyze_capture, can_crc, pulse_measurements, interval_edges
|
|
from logic_analyzer.decoders.set_uart import crc16
|
|
|
|
|
|
PING = bytes.fromhex('A5 5A 02 08 01 00 00 00 2A 00 34 12 00 00 33 EC 33 04')
|
|
|
|
|
|
def bits_of(value, count):
|
|
return [(value >> bit) & 1 for bit in range(count - 1, -1, -1)]
|
|
|
|
|
|
def capture_bits(bits, rate, invert=False, origin=0):
|
|
edges = array('d')
|
|
for i in range(1, len(bits)):
|
|
if bits[i] != bits[i - 1]:
|
|
edges.append(origin + i / rate)
|
|
channel = DigitalChannel('bus', bits[0] ^ invert, edges)
|
|
return DigitalCapture('test', 'test', [channel], origin, origin + len(bits) / rate)
|
|
|
|
|
|
def uart_capture(data, baud=115200, parity='none', stops=1, invert=False, bad_parity=False, bad_stop=False):
|
|
bits = [1] * 5
|
|
for byte in data:
|
|
bits += [0] + [(byte >> i) & 1 for i in range(8)]
|
|
if parity != 'none':
|
|
bits += [((sum(bits[-8:]) + (parity == 'odd')) & 1) ^ bad_parity]
|
|
bits += [0 if bad_stop else 1] * stops
|
|
bits += [1] * 5
|
|
return capture_bits(bits, baud, invert)
|
|
|
|
|
|
def can_packet(ident, data, extended=True, remote=False, corrupt_crc=False, ack=True):
|
|
if extended:
|
|
bits = [0] + bits_of(ident >> 18, 11) + [1, 1] + bits_of(ident & 0x3ffff, 18) + [int(remote), 0, 0]
|
|
else:
|
|
bits = [0] + bits_of(ident, 11) + [int(remote), 0, 0]
|
|
bits += bits_of(len(data), 4)
|
|
if not remote:
|
|
for byte in data:
|
|
bits += bits_of(byte, 8)
|
|
bits += bits_of(can_crc(bits) ^ int(corrupt_crc), 15)
|
|
stuffed, last, run = [], None, 0
|
|
for value in bits:
|
|
stuffed.append(value)
|
|
run = run + 1 if value == last else 1
|
|
last = value
|
|
if run == 5:
|
|
stuffed.append(1 - value)
|
|
last, run = 1 - value, 1
|
|
return stuffed + [1, 0 if ack else 1, 1] + [1] * 7
|
|
|
|
|
|
class LogicAnalysisTests(unittest.TestCase):
|
|
def test_uart_byte_values_inverted_and_parity(self):
|
|
for invert in (False, True):
|
|
for parity in ('none', 'even', 'odd'):
|
|
cap = uart_capture(b'\x00\xff\xa5\x5a', parity=parity, stops=2, invert=invert)
|
|
result = analyze_capture(cap, dict(mode='UART', baudrate=115200, parity=parity, stops=2, inverted=invert))
|
|
self.assertEqual([e.text for e in result.events], ['UART 0x00', 'UART 0xFF', 'UART 0xA5', 'UART 0x5A'])
|
|
|
|
def test_uart_errors_are_not_accepted_as_set_frames(self):
|
|
cap = uart_capture(PING, parity='even', bad_parity=True)
|
|
result = analyze_capture(cap, dict(mode='SET UART', parity='even'))
|
|
self.assertTrue(result.events)
|
|
self.assertTrue(all(e.kind == 'error' for e in result.events))
|
|
result = analyze_capture(uart_capture(b'\xa5', bad_stop=True), dict(mode='UART'))
|
|
self.assertTrue(any('стопового' in e.text for e in result.events))
|
|
|
|
def test_set_ping_bridge_crc_and_resynchronization(self):
|
|
damaged = bytearray(PING)
|
|
damaged[-1] ^= 1
|
|
data = b'noise' + damaged + PING + bytes.fromhex('AA 55 08 01 01 67 45 23 01 AA BB FE 14')
|
|
events = analyze_capture(uart_capture(data), dict(mode='SET UART')).events
|
|
self.assertTrue(any('CRC' in e.text and e.kind == 'error' for e in events))
|
|
self.assertEqual(sum('SET v2 PING' in e.text for e in events), 1)
|
|
self.assertTrue(any('ProtoCAN seq=' in e.text for e in events))
|
|
self.assertTrue(any('ProtoCAN GAS' in e.text for e in events))
|
|
|
|
def test_pm35_request_and_special_response(self):
|
|
raw = bytes.fromhex('100300000002')
|
|
raw += struct.pack('<H', crc16(raw, True))
|
|
events = analyze_capture(uart_capture(raw), dict(mode='PM35 UART', role='request')).events
|
|
self.assertEqual(len(events), 1)
|
|
self.assertIn('READ R0 count=2', events[0].text)
|
|
raw = b'\x10\x03\x00' + bytes(256)
|
|
raw += struct.pack('<H', crc16(raw, True))
|
|
events = analyze_capture(uart_capture(raw), dict(mode='PM35 UART', role='response')).events
|
|
self.assertEqual(len(events), 1)
|
|
self.assertIn('128 words', events[0].text)
|
|
|
|
def test_uart_end_flush_and_gap(self):
|
|
result = analyze_capture(uart_capture(PING[:9]), dict(mode='SET UART'))
|
|
self.assertTrue(any('Incomplete' in e.text for e in result.events))
|
|
cap = uart_capture(PING)
|
|
result = analyze_capture(cap, dict(mode='SET UART'), end=cap.end / 2)
|
|
self.assertFalse(any(e.kind == 'frame' for e in result.events))
|
|
|
|
def test_crc15_known_check_value(self):
|
|
bits = [bit for byte in b'123456789' for bit in bits_of(byte, 8)]
|
|
self.assertEqual(can_crc(bits), 0x059e)
|
|
|
|
def test_can_standard_extended_remote_and_nack(self):
|
|
bits = [1] * 10
|
|
bits += can_packet(0x123, b'\x01\x02', extended=False) + [1] * 5
|
|
bits += can_packet(0x1b530010, b'\x34\x12\xfe\xff') + [1] * 5
|
|
bits += can_packet(0x123, b'\x00\x00', extended=False, remote=True, ack=False) + [1] * 5
|
|
result = analyze_capture(capture_bits(bits, 500000), dict(mode='CAN', baudrate=500000))
|
|
self.assertEqual(len(result.events), 3, [e.text for e in result.events])
|
|
self.assertTrue(all(e.kind == 'can' for e in result.events))
|
|
self.assertIn('STD ID=00000123', result.events[0].text)
|
|
self.assertIn('EXT ID=1B530010', result.events[1].text)
|
|
self.assertEqual(result.events[1].details, '34 12 fe ff')
|
|
self.assertIn('RTR NACK', result.events[2].text)
|
|
|
|
def test_can_crc_error_recovery_and_application(self):
|
|
bits = [1] * 10 + can_packet(0x1b530010, b'\x34\x12', corrupt_crc=True) + [1] * 5
|
|
bits += can_packet(0x1b530010, b'\x34\x12') + [1] * 5
|
|
events = analyze_capture(capture_bits(bits, 1000000), dict(mode='SET CAN')).events
|
|
self.assertTrue(any(e.kind == 'error' and 'CRC15' in e.text for e in events))
|
|
self.assertEqual(sum('R0010=1234' in e.text for e in events), 1)
|
|
|
|
def test_can_stuff_error(self):
|
|
bits = [1] * 10 + [0] * 20 + [1] * 10
|
|
events = analyze_capture(capture_bits(bits, 1000000), dict(mode='CAN')).events
|
|
self.assertTrue(any('stuffing' in e.text for e in events))
|
|
|
|
def test_can_set_reassembly_and_balsam(self):
|
|
parts = [bytes([0x10]) + struct.pack('<H', len(PING)) + PING[:5]]
|
|
parts += [bytes([0x20 | i]) + PING[offset:offset+7] for i, offset in enumerate(range(5, len(PING), 7), 1)]
|
|
bits = [1] * 10
|
|
for part in parts:
|
|
bits += can_packet(0x122a0000, part) + [1] * 5
|
|
events = analyze_capture(capture_bits(bits, 1000000), dict(mode='SET CAN', protocol='set-v2')).events
|
|
self.assertTrue(any('SET v2 PING' in e.text for e in events), [e.text for e in events])
|
|
bits = [1] * 10 + can_packet(0xba0010, bytes.fromhex('a01012340000ffff')) + [1] * 5
|
|
events = analyze_capture(capture_bits(bits, 1000000), dict(mode='SET CAN', protocol='balsam')).events
|
|
self.assertTrue(any('R0010=1234 R0012=FFFF' in e.text for e in events))
|
|
|
|
def test_can_incomplete_end(self):
|
|
bits = [1] * 10 + can_packet(0x123, b'\x55', extended=False)[:-8]
|
|
events = analyze_capture(capture_bits(bits, 1000000), dict(mode='CAN')).events
|
|
self.assertTrue(any('Неполный' in e.text for e in events))
|
|
self.assertFalse(any(e.kind == 'can' for e in events))
|
|
|
|
def test_gate_profiles_and_truncated_window(self):
|
|
vin = DigitalChannel('Vin', 0, array('d', [1e-6, 4e-6]))
|
|
status = DigitalChannel('Vstat', 0, array('d', [1.25e-6, 1.95e-6]))
|
|
cap = DigitalCapture('test', 'test', [vin, status], 0, 6e-6)
|
|
events = analyze_capture(cap, dict(mode='1SP0635')).events
|
|
self.assertEqual(Counter(e.kind for e in events), dict(control=2, delay_ok=1, width_ok=1, missing=1))
|
|
self.assertAlmostEqual(next(e for e in events if e.kind == 'delay_ok').end - 1e-6, 250e-9)
|
|
events = analyze_capture(cap, dict(mode='1SP0635'), end=4.2e-6).events
|
|
self.assertTrue(any(e.kind == 'incomplete' for e in events))
|
|
self.assertFalse(any(e.kind == 'missing' for e in events))
|
|
with self.assertRaises(ValueError):
|
|
analyze_capture(cap, dict(mode='1SP0635', status_channel=0))
|
|
|
|
def test_measurements_cycle_and_clipped_pulse(self):
|
|
ch = DigitalChannel('D0', 0, array('d', [1e-6, 2e-6, 5e-6, 6e-6]))
|
|
measured = pulse_measurements(ch, 1.5e-6, 0, 8e-6)
|
|
self.assertAlmostEqual(measured['period'], 4e-6)
|
|
self.assertAlmostEqual(measured['duty'], 25)
|
|
self.assertEqual(interval_edges(ch, 1e-6, 6e-6), (1, 2))
|
|
self.assertNotIn('width', pulse_measurements(ch, 0.5e-6, 0, 8e-6))
|
|
|
|
def test_result_limit_cancellation_and_progress(self):
|
|
cap = uart_capture(bytes(range(100)))
|
|
progress = []
|
|
result = analyze_capture(cap, dict(mode='UART', max_events=3), progress=progress.append)
|
|
self.assertTrue(result.truncated)
|
|
self.assertEqual(len(result.events), 3)
|
|
self.assertLessEqual(len(progress), 101)
|
|
with self.assertRaises(ImportCancelled):
|
|
analyze_capture(cap, dict(mode='UART'), cancel=lambda: True)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|