Добавить протокол Altera Logic и общие клиенты прошивки
This commit is contained in:
1
python/altera_logic/.gitignore
vendored
Normal file
1
python/altera_logic/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
native/
|
||||
4
python/altera_logic/__init__.py
Normal file
4
python/altera_logic/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Portable Altera analyzer client. Wire protocol and sequencing live in C99."""
|
||||
from .native import Capture, NativeAnalyzer
|
||||
|
||||
__all__ = ["Capture", "NativeAnalyzer"]
|
||||
89
python/altera_logic/markers.py
Normal file
89
python/altera_logic/markers.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Presentation state for waveform rulers; no transport or wire processing."""
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
def drag_zoom_factor(delta_pixels):
|
||||
"""Visual gesture mapping: 200 pixels doubles the selected axis."""
|
||||
return 2 ** (max(-1000., min(1000., delta_pixels)) / 200.)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Marker:
|
||||
name: str
|
||||
axis: str
|
||||
value: float
|
||||
color: str
|
||||
visible: bool = False
|
||||
|
||||
|
||||
class WaveformMarkers:
|
||||
def __init__(self):
|
||||
self.items = [Marker('X1', 'x', .2, '#ffce63'),
|
||||
Marker('X2', 'x', .4, '#ffce63'),
|
||||
Marker('X3', 'x', .6, '#e794ff'),
|
||||
Marker('X4', 'x', .8, '#e794ff'),
|
||||
Marker('Y1', 'y', .2, '#72e7ff'),
|
||||
Marker('Y2', 'y', .4, '#72e7ff'),
|
||||
Marker('Y3', 'y', .6, '#a9df79'),
|
||||
Marker('Y4', 'y', .8, '#a9df79')]
|
||||
self._math = None
|
||||
self.session = None
|
||||
self.bounds = None
|
||||
|
||||
def window(self, session, start, end):
|
||||
if session != self.session or self.bounds is None:
|
||||
for marker, fraction in zip(self.items[:4], (.2, .4, .6, .8)):
|
||||
marker.value = start + (end-start)*fraction
|
||||
self.session, self.bounds = session, (start, end)
|
||||
|
||||
def reset(self):
|
||||
if self.bounds:
|
||||
start, end = self.bounds
|
||||
for marker, fraction in zip(self.items[:4], (.2, .4, .6, .8)):
|
||||
marker.value = start + (end-start)*fraction
|
||||
for marker, fraction in zip(self.items[4:], (.2, .4, .6, .8)):
|
||||
marker.value = fraction
|
||||
|
||||
def measurements(self, period_ns):
|
||||
"""Use the same native delta operation as Android's plotDelta."""
|
||||
if self._math is None:
|
||||
from protocan.plot import PlotMath
|
||||
from .native import NativeAnalyzer
|
||||
self._math = PlotMath(NativeAnalyzer().lib)
|
||||
result = []
|
||||
for index in range(0, 8, 2):
|
||||
a, b = self.items[index:index+2]
|
||||
if not (a.visible and b.visible):
|
||||
continue
|
||||
if a.axis == 'x':
|
||||
if self.bounds is None:
|
||||
continue
|
||||
factor = period_ns / 1e9
|
||||
av, bv = a.value*factor, b.value*factor
|
||||
delta = self._math.delta(a.value, b.value, factor)
|
||||
frequency = 1/abs(delta) if delta else None
|
||||
else:
|
||||
av, bv = 100*(1-a.value), 100*(1-b.value)
|
||||
delta = self._math.delta(av, bv)
|
||||
frequency = None
|
||||
result.append((a, b, av, bv, delta, frequency))
|
||||
return result
|
||||
|
||||
def place(self, index, fraction):
|
||||
marker = self.items[index]
|
||||
fraction = min(1., max(0., fraction))
|
||||
if marker.axis == 'x':
|
||||
if self.bounds is None:
|
||||
return
|
||||
start, end = self.bounds
|
||||
marker.value = start + (end-start)*fraction
|
||||
else:
|
||||
marker.value = fraction
|
||||
|
||||
def fraction(self, marker):
|
||||
if marker.axis == 'y':
|
||||
return marker.value
|
||||
if self.bounds is None:
|
||||
return 0.
|
||||
start, end = self.bounds
|
||||
return (marker.value-start)/max(1, end-start)
|
||||
131
python/altera_logic/native.py
Normal file
131
python/altera_logic/native.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""ctypes binding only: no Python packet codec or protocol fallback."""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import ctypes as C
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Capture:
|
||||
samples: tuple[int, ...]
|
||||
trigger_index: int
|
||||
sample_rate: float
|
||||
demo: bool = False
|
||||
|
||||
def save_csv(self, path):
|
||||
with open(path, "w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(["index", "time_s", "sample_hex", "demo"] +
|
||||
["D%d" % n for n in range(16)])
|
||||
for i, sample in enumerate(self.samples):
|
||||
writer.writerow([i, (i-self.trigger_index)/self.sample_rate,
|
||||
"%04X" % sample, int(self.demo)] +
|
||||
[(sample >> n) & 1 for n in range(16)])
|
||||
|
||||
|
||||
class NativeAnalyzer:
|
||||
CONNECTING, READY, CAPTURING, DONE, ERROR = range(5)
|
||||
ERROR_TEXT = {
|
||||
1: "Неверный формат ответа FPGA",
|
||||
2: "Ошибка контрольной суммы ответа",
|
||||
3: "FPGA отклонила команду",
|
||||
4: "Тайм-аут UART. При потере синхронизации нужен RESET_N и повторная настройка",
|
||||
5: "Неподдерживаемая конфигурация FPGA: ожидается 16 каналов, 4096 выборок, версия 1",
|
||||
6: "Недопустимые или противоречивые настройки триггера",
|
||||
7: "Ошибка последовательного порта",
|
||||
}
|
||||
|
||||
def __init__(self, library=None):
|
||||
if library is None:
|
||||
explicit = os.environ.get("ALTERA_LOGIC_LIBRARY")
|
||||
name = "setprotocol.dll" if sys.platform == "win32" else "libsetprotocol.so"
|
||||
bundled = Path(getattr(sys, "_MEIPASS", "")) / "gui_desktop/native" / name
|
||||
local = Path(__file__).resolve().parent / "native" / name
|
||||
path = Path(explicit) if explicit else (bundled if bundled.is_file() else local)
|
||||
try:
|
||||
library = C.CDLL(str(path))
|
||||
except OSError as exc:
|
||||
raise RuntimeError("Не загружено C-ядро Altera Logic. Соберите templates/"
|
||||
"c/set-protocol/tools/build_host.py; " + str(exc)) from exc
|
||||
self.lib = library
|
||||
signatures = {
|
||||
"la_context_size": ([], C.c_size_t),
|
||||
"la_init": ([C.c_void_p], None),
|
||||
"la_start": ([C.c_void_p] + [C.c_uint32]*5, C.c_int),
|
||||
"la_next": ([C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"la_feed": ([C.c_void_p, C.c_void_p, C.c_size_t], None),
|
||||
"la_tick": ([C.c_void_p, C.c_uint32], None),
|
||||
"la_fail": ([C.c_void_p, C.c_uint32], None),
|
||||
"la_get": ([C.c_void_p, C.c_uint32], C.c_uint32),
|
||||
"la_samples": ([C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"la_demo_init": ([C.c_void_p], None),
|
||||
"la_demo_capture": ([C.c_void_p, C.c_uint32], C.c_int),
|
||||
}
|
||||
try:
|
||||
for name, (args, result) in signatures.items():
|
||||
fn = getattr(self.lib, name)
|
||||
fn.argtypes, fn.restype = args, result
|
||||
except AttributeError as exc:
|
||||
raise RuntimeError("C-ядро устарело: пересоберите templates с Altera Logic") from exc
|
||||
# Explicitly aligned caller-owned storage; C never allocates memory.
|
||||
self.ctx = (C.c_uint64 * ((self.lib.la_context_size()+7)//8))()
|
||||
self.demo = False
|
||||
self.reset()
|
||||
|
||||
def reset(self, demo=False):
|
||||
self.demo = demo
|
||||
(self.lib.la_demo_init if demo else self.lib.la_init)(self.ctx)
|
||||
|
||||
def get(self, field):
|
||||
return int(self.lib.la_get(self.ctx, field))
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self.get(0)
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self.ERROR_TEXT.get(self.get(1), "Ошибка обмена")
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
return self.get(7)
|
||||
|
||||
@property
|
||||
def flags(self):
|
||||
return self.get(5)
|
||||
|
||||
def start(self, divider, mask=0, value=0, edge_mask=0, edge_value=0):
|
||||
values = (divider, mask, value, edge_mask, edge_value)
|
||||
if any(not isinstance(v, int) or not 0 <= v <= 65535 for v in values):
|
||||
raise ValueError(self.ERROR_TEXT[6])
|
||||
result = (self.lib.la_demo_capture(self.ctx, divider) if self.demo else
|
||||
self.lib.la_start(self.ctx, *values))
|
||||
if result:
|
||||
raise ValueError(self.ERROR_TEXT.get(result, "Захват уже выполняется"))
|
||||
|
||||
def next_request(self):
|
||||
out = (C.c_ubyte*6)()
|
||||
size = self.lib.la_next(self.ctx, out, len(out))
|
||||
return bytes(out[:size])
|
||||
|
||||
def feed(self, data):
|
||||
self.lib.la_feed(self.ctx, data, len(data))
|
||||
|
||||
def tick(self, elapsed_ms):
|
||||
self.lib.la_tick(self.ctx, max(0, min(int(elapsed_ms), 0xffffffff)))
|
||||
|
||||
def fail_io(self):
|
||||
self.lib.la_fail(self.ctx, 7)
|
||||
|
||||
def capture(self):
|
||||
out = (C.c_uint16*4096)()
|
||||
count = self.lib.la_samples(self.ctx, out, len(out))
|
||||
if not count:
|
||||
raise RuntimeError("Запись ещё не завершена")
|
||||
return Capture(tuple(out[:count]), self.get(6),
|
||||
self.get(4)/(self.get(8)+1), self.demo)
|
||||
27
python/altera_logic/plot_scroll.py
Normal file
27
python/altera_logic/plot_scroll.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Qt presentation port: wheel pans X; Ctrl+wheel pans Y, never zooms."""
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QScrollArea
|
||||
|
||||
|
||||
class PlotScrollArea(QScrollArea):
|
||||
user_scrolled = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._remainder = {'x': 0., 'y': 0.}
|
||||
|
||||
def wheelEvent(self, event):
|
||||
axis = 'y' if event.modifiers() & Qt.KeyboardModifier.ControlModifier else 'x'
|
||||
bar = self.verticalScrollBar() if axis == 'y' else self.horizontalScrollBar()
|
||||
pixels, angle = event.pixelDelta(), event.angleDelta()
|
||||
if not pixels.isNull():
|
||||
delta = pixels.y() if pixels.y() else pixels.x()
|
||||
else:
|
||||
delta = (angle.y() if angle.y() else angle.x()) / 120. * 60.
|
||||
self._remainder[axis] -= delta
|
||||
movement = int(self._remainder[axis])
|
||||
self._remainder[axis] -= movement
|
||||
if delta:
|
||||
self.user_scrolled.emit(axis)
|
||||
bar.setValue(bar.value()+movement)
|
||||
event.accept()
|
||||
114
python/altera_logic/qt_port.py
Normal file
114
python/altera_logic/qt_port.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Qt serial/lifecycle port. Protocol decisions are exclusively in C99."""
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QObject, QTimer, QElapsedTimer, Signal
|
||||
from PySide6.QtSerialPort import QSerialPort, QSerialPortInfo
|
||||
from .native import NativeAnalyzer
|
||||
|
||||
|
||||
class AnalyzerPort(QObject):
|
||||
changed = Signal()
|
||||
completed = Signal(object)
|
||||
error = Signal(str)
|
||||
log = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.core = None
|
||||
self.active = False
|
||||
self._last_done = False
|
||||
self.serial = QSerialPort(self)
|
||||
self.serial.readyRead.connect(self._read)
|
||||
self.serial.errorOccurred.connect(self._serial_error)
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(10)
|
||||
self.timer.timeout.connect(self._tick)
|
||||
self.clock = QElapsedTimer()
|
||||
|
||||
@staticmethod
|
||||
def ports():
|
||||
return [(p.portName(), p.description()) for p in QSerialPortInfo.availablePorts()]
|
||||
|
||||
def open(self, name, demo=False):
|
||||
self.close()
|
||||
try:
|
||||
self.core = NativeAnalyzer()
|
||||
self.core.reset(demo)
|
||||
except RuntimeError as exc:
|
||||
self.error.emit(str(exc))
|
||||
return
|
||||
if not demo:
|
||||
self.serial.setPortName(name)
|
||||
self.serial.setBaudRate(921600)
|
||||
self.serial.setDataBits(QSerialPort.DataBits.Data8)
|
||||
self.serial.setParity(QSerialPort.Parity.NoParity)
|
||||
self.serial.setStopBits(QSerialPort.StopBits.OneStop)
|
||||
self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
|
||||
if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite):
|
||||
self.error.emit(self.serial.errorString())
|
||||
return
|
||||
self.active = True
|
||||
self._last_done = False
|
||||
self.clock.start()
|
||||
self.timer.start()
|
||||
self.log.emit("ДЕМО: синтетические данные, триггер не моделируется" if demo
|
||||
else "%s · 921600 8N1" % name)
|
||||
self._pump()
|
||||
|
||||
def close(self):
|
||||
self.active = False
|
||||
self.timer.stop()
|
||||
self.serial.close()
|
||||
self.changed.emit()
|
||||
|
||||
def start(self, *settings):
|
||||
if not self.active or not self.core:
|
||||
return
|
||||
try:
|
||||
self.core.start(*settings)
|
||||
except ValueError as exc:
|
||||
self.error.emit(str(exc))
|
||||
return
|
||||
self._last_done = False
|
||||
self._pump()
|
||||
|
||||
def _tick(self):
|
||||
if not self.active:
|
||||
return
|
||||
self.core.tick(self.clock.restart())
|
||||
self._pump()
|
||||
|
||||
def _read(self):
|
||||
data = bytes(self.serial.readAll())
|
||||
if self.active and data:
|
||||
self.log.emit("RX " + data.hex(" ").upper())
|
||||
self.core.feed(data)
|
||||
self._pump()
|
||||
|
||||
def _pump(self):
|
||||
if not self.active:
|
||||
return
|
||||
if self.core.state == NativeAnalyzer.ERROR:
|
||||
message = self.core.error
|
||||
self.close()
|
||||
self.error.emit(message)
|
||||
return
|
||||
packet = self.core.next_request()
|
||||
if packet:
|
||||
self.log.emit("TX " + packet.hex(" ").upper())
|
||||
if self.serial.write(packet) != len(packet):
|
||||
self.core.fail_io()
|
||||
self._pump()
|
||||
return
|
||||
self.clock.restart()
|
||||
if self.core.state == NativeAnalyzer.DONE and not self._last_done:
|
||||
self._last_done = True
|
||||
self.completed.emit(self.core.capture())
|
||||
self.changed.emit()
|
||||
|
||||
def _serial_error(self, code):
|
||||
if self.active and code != QSerialPort.SerialPortError.NoError:
|
||||
message = self.serial.errorString()
|
||||
self.core.fail_io()
|
||||
self.close()
|
||||
self.error.emit(message)
|
||||
108
python/altera_logic/stream.py
Normal file
108
python/altera_logic/stream.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""SETCAN streaming FFI and immutable display/export models. No wire codec."""
|
||||
from __future__ import annotations
|
||||
import csv
|
||||
import ctypes as C
|
||||
from dataclasses import dataclass
|
||||
from .native import NativeAnalyzer
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StreamSnapshot:
|
||||
indices: tuple[int, ...]
|
||||
samples: tuple[int, ...]
|
||||
breaks: tuple[int, ...]
|
||||
period_ns: int
|
||||
session: int
|
||||
demo: bool = False
|
||||
traces: tuple = ()
|
||||
|
||||
def save_csv(self, path):
|
||||
with open(path, "w", newline="", encoding="utf-8") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(["session", "sample_index", "time_s", "gap_before", "sample_hex", "demo"] +
|
||||
["D%d" % n for n in range(16)])
|
||||
for index, sample, gap in zip(self.indices, self.samples, self.breaks):
|
||||
writer.writerow([self.session, index, index*self.period_ns/1e9, gap,
|
||||
"%04X" % sample, int(self.demo)] +
|
||||
[(sample >> bit)&1 for bit in range(16)])
|
||||
|
||||
|
||||
class NativeStream:
|
||||
def __init__(self, device_id=None):
|
||||
self._owner = NativeAnalyzer()
|
||||
self.lib = self._owner.lib
|
||||
signatures = {
|
||||
"las_context_size": ([], C.c_size_t),
|
||||
"las_init": ([C.c_void_p, C.c_uint32], C.c_int),
|
||||
"las_can": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint32, C.c_uint32], None),
|
||||
"las_uart": ([C.c_void_p, C.c_void_p, C.c_size_t], None),
|
||||
"las_get": ([C.c_void_p, C.c_uint32], C.c_uint32),
|
||||
"las_snapshot": ([C.c_void_p, C.c_void_p, C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"las_trace": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"las_device_type": ([], C.c_uint32),
|
||||
"las_device_id": ([], C.c_uint32),
|
||||
"las_device_name": ([], C.c_char_p),
|
||||
"las_demo_step": ([C.c_void_p, C.c_uint32], None),
|
||||
"las_metadata": ([C.c_uint32]*4+[C.POINTER(C.c_uint32), C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"las_data": ([C.c_uint32]*5+[C.POINTER(C.c_uint32), C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
}
|
||||
try:
|
||||
for name, (args, result) in signatures.items():
|
||||
fn = getattr(self.lib, name)
|
||||
fn.argtypes, fn.restype = args, result
|
||||
except AttributeError as exc:
|
||||
raise RuntimeError("Обновите DLL templates: отсутствует SETCAN Altera stream") from exc
|
||||
self.device_type = int(self.lib.las_device_type())
|
||||
self.device_id = int(self.lib.las_device_id()) if device_id is None else device_id
|
||||
self.device_name = self.lib.las_device_name().decode("utf-8")
|
||||
self.ctx = (C.c_uint64*((self.lib.las_context_size()+7)//8))()
|
||||
if not 0 <= self.device_id <= 15 or not self.lib.las_init(self.ctx, self.device_id):
|
||||
raise ValueError("DeviceID должен быть 0…15")
|
||||
|
||||
@property
|
||||
def stats(self):
|
||||
fields = ("count", "period_ns", "session", "received", "missing", "duplicates",
|
||||
"invalid", "ignored", "crc_errors", "has_meta")
|
||||
return {name: int(self.lib.las_get(self.ctx, i)) for i, name in enumerate(fields)}
|
||||
|
||||
def feed_can(self, identifier, data, extended=True, remote=False):
|
||||
if not 0 <= identifier <= 0xffffffff:
|
||||
return
|
||||
self.lib.las_can(self.ctx, identifier, data, len(data), int(extended), int(remote))
|
||||
|
||||
def feed_uart(self, data):
|
||||
self.lib.las_uart(self.ctx, data, len(data))
|
||||
|
||||
def snapshot(self, demo=False):
|
||||
stats = self.stats
|
||||
count = stats["count"]
|
||||
indices, samples, breaks = (C.c_uint64*count)(), (C.c_uint16*count)(), (C.c_ubyte*count)()
|
||||
n = self.lib.las_snapshot(self.ctx, indices, samples, breaks, count)
|
||||
traces = []
|
||||
xs, ys, moves = (C.c_uint64*(2*count))(), (C.c_ubyte*(2*count))(), (C.c_ubyte*(2*count))()
|
||||
for channel in range(16):
|
||||
size = self.lib.las_trace(self.ctx, channel, xs, ys, moves, 2*count)
|
||||
traces.append(tuple(zip(xs[:size], ys[:size], moves[:size])))
|
||||
return StreamSnapshot(tuple(indices[:n]), tuple(samples[:n]), tuple(breaks[:n]),
|
||||
stats["period_ns"], stats["session"], demo, tuple(traces))
|
||||
|
||||
def demo_step(self, count=50):
|
||||
self.lib.las_demo_step(self.ctx, count)
|
||||
|
||||
def metadata_packet(self, session, period_ns, uart=False):
|
||||
if not 0 <= session <= 65535 or not 1 <= period_ns <= 0xffffffff:
|
||||
raise ValueError("Invalid stream metadata")
|
||||
return self._packet(self.lib.las_metadata, session, period_ns, int(uart))
|
||||
|
||||
def data_packet(self, session, index, sample, uart=False):
|
||||
if not 0 <= index <= 0xffffffff or any(not 0 <= v <= 65535 for v in (session, sample)):
|
||||
raise ValueError("Invalid stream samples")
|
||||
return self._packet(self.lib.las_data, session, index, sample, int(uart))
|
||||
|
||||
def _packet(self, fn, *args):
|
||||
identifier = C.c_uint32()
|
||||
out = (C.c_ubyte*32)()
|
||||
n = fn(self.device_id, *args, C.byref(identifier), out, len(out))
|
||||
if not n:
|
||||
raise ValueError("Invalid SETCAN packet arguments")
|
||||
return identifier.value, bytes(out[:n])
|
||||
103
python/altera_logic/stream_port.py
Normal file
103
python/altera_logic/stream_port.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Qt transport/lifecycle port for the C SETCAN stream receiver.
|
||||
|
||||
CAN ingress accepts canonical RX events from an existing bus connection.
|
||||
UART uses the same SETCAN frames inside the shared AA55/CRC16 transport.
|
||||
Reception and rendering clocks are separate; no packet-rate repainting.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from PySide6.QtCore import QObject, QTimer, QElapsedTimer, Signal
|
||||
from PySide6.QtSerialPort import QSerialPort, QSerialPortInfo
|
||||
from .stream import NativeStream
|
||||
|
||||
|
||||
class StreamPort(QObject):
|
||||
updated = Signal(object, object)
|
||||
changed = Signal(bool)
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.core = None
|
||||
self.active = False
|
||||
self.mode = "demo"
|
||||
self.paused = False
|
||||
self.serial = QSerialPort(self)
|
||||
self.serial.readyRead.connect(self._read)
|
||||
self.serial.errorOccurred.connect(self._error)
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(50)
|
||||
self.timer.timeout.connect(self._tick)
|
||||
self._previous = None
|
||||
self._last_data = QElapsedTimer()
|
||||
self._last_received = None
|
||||
|
||||
@staticmethod
|
||||
def ports():
|
||||
return [(p.portName(), p.description()) for p in QSerialPortInfo.availablePorts()]
|
||||
|
||||
def open(self, mode, port_name="", device_id=None):
|
||||
self.close()
|
||||
if mode not in ("demo", "can", "uart", "both"):
|
||||
self.error.emit("Неизвестный транспорт")
|
||||
return
|
||||
try:
|
||||
self.core = NativeStream(device_id)
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
self.error.emit(str(exc))
|
||||
return
|
||||
self.mode = mode
|
||||
if mode in ("uart", "both"):
|
||||
self.serial.setPortName(port_name)
|
||||
self.serial.setBaudRate(921600)
|
||||
self.serial.setDataBits(QSerialPort.DataBits.Data8)
|
||||
self.serial.setParity(QSerialPort.Parity.NoParity)
|
||||
self.serial.setStopBits(QSerialPort.StopBits.OneStop)
|
||||
self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
|
||||
if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite):
|
||||
self.error.emit(self.serial.errorString())
|
||||
return
|
||||
self.active = True
|
||||
self.paused = False
|
||||
self._previous = None
|
||||
self._last_received = None
|
||||
self._last_data.start()
|
||||
self.timer.start()
|
||||
self.changed.emit(True)
|
||||
|
||||
def close(self):
|
||||
self.active = False
|
||||
self.timer.stop()
|
||||
self.serial.close()
|
||||
self.changed.emit(False)
|
||||
|
||||
def receive_event(self, event):
|
||||
if (self.active and self.mode in ("can", "both") and event.get("kind") == "can"
|
||||
and event.get("direction") == "RX"):
|
||||
self.core.feed_can(event["identifier"], event["data"],
|
||||
event.get("extended", False), event.get("remote", False))
|
||||
|
||||
def _read(self):
|
||||
data = bytes(self.serial.readAll())
|
||||
if self.active and self.mode in ("uart", "both"):
|
||||
self.core.feed_uart(data)
|
||||
|
||||
def _tick(self):
|
||||
if not self.active:
|
||||
return
|
||||
if self.mode == "demo":
|
||||
self.core.demo_step()
|
||||
stats = self.core.stats
|
||||
signature = (stats["received"], stats["session"])
|
||||
if signature != self._last_received:
|
||||
self._last_received = signature
|
||||
self._last_data.restart()
|
||||
stats["stale"] = self._last_data.elapsed() > max(2000, stats["period_ns"]*3/1e6)
|
||||
if not self.paused and stats != self._previous:
|
||||
self._previous = stats
|
||||
self.updated.emit(self.core.snapshot(self.mode == "demo"), stats)
|
||||
|
||||
def _error(self, code):
|
||||
if self.active and code != QSerialPort.SerialPortError.NoError:
|
||||
message = self.serial.errorString()
|
||||
self.close()
|
||||
self.error.emit(message)
|
||||
Reference in New Issue
Block a user