refactor: standardize codebase formatting, optimize import orders, and update GUI resolution to 1080p

This commit is contained in:
jack
2026-06-18 13:13:47 -07:00
parent 8ad2a9b972
commit 66942d250d
32 changed files with 453 additions and 120 deletions
+2 -2
View File
@@ -4,8 +4,8 @@ import ISC
// ===== App Window Shell =====
Window {
// ===== Window Dimensions =====
width: 1400
height: 820
width: 1920
height: 1080
minimumWidth: 1100
minimumHeight: 700
visible: true
+5 -4
View File
@@ -1,18 +1,19 @@
import logging
import sys
from pathlib import Path
import logging
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle
from PySide6.QtWidgets import QApplication
from pygui.backend.controllers.device_controller import DeviceController
from pygui.backend.controllers.session_controller import SessionController
from pygui.backend.data.file_browser import FileBrowser
from pygui.backend.data.local_settings import LocalSettings
from pygui.backend.data.local_settings_model import LocalSettingsModel
from pygui.backend.data.file_browser import FileBrowser
from pygui.backend.controllers.session_controller import SessionController
# Importing wafer_map_item registers the @QmlElement WaferMapItem (QML: import ISC.Wafer).
from pygui.backend.visualization.wafer_map_item import WaferMapItem
# ===== Application Entry Point =====
def main() -> int:
+13 -17
View File
@@ -4,29 +4,25 @@
from pygui.backend.controllers.device_controller import DeviceController # noqa: F401
from pygui.backend.controllers.session_controller import SessionController # noqa: F401
from pygui.backend.models.frame import Frame # noqa: F401
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data # noqa: F401
from pygui.backend.models.frame_stats import Stats, compute_stats # noqa: F401
from pygui.backend.models.session_model import SessionModel # noqa: F401
from pygui.backend.models.data_model import TemperatureTableModel # noqa: F401
from pygui.backend.models.sensor_editor import SensorEditor # noqa: F401
from pygui.backend.models.threshold_classifier import ThresholdConfig # noqa: F401
from pygui.backend.models.stability_detector import StabilityDetector # noqa: F401
from pygui.backend.data.csv_recorder import CsvRecorder # noqa: F401
from pygui.backend.data.csv_file_metadata import CSVFileMetadata # noqa: F401
from pygui.backend.data.csv_recorder import CsvRecorder # noqa: F401
from pygui.backend.data.data_records import read_data_records # noqa: F401
from pygui.backend.data.data_segment import DataSegment # noqa: F401
from pygui.backend.data.file_browser import FileBrowser # noqa: F401
from pygui.backend.data.local_settings import LocalSettings # noqa: F401
from pygui.backend.data.local_settings_model import LocalSettingsModel # noqa: F401
from pygui.backend.visualization.wafer_map_item import WaferMapItem # noqa: F401
from pygui.backend.models.data_model import TemperatureTableModel # noqa: F401
from pygui.backend.models.frame import Frame # noqa: F401
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data # noqa: F401
from pygui.backend.models.frame_stats import Stats, compute_stats # noqa: F401
from pygui.backend.models.sensor_editor import SensorEditor # noqa: F401
from pygui.backend.models.session_model import SessionModel # noqa: F401
from pygui.backend.models.stability_detector import StabilityDetector # noqa: F401
from pygui.backend.models.threshold_classifier import ThresholdConfig # noqa: F401
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment # noqa: F401
from pygui.backend.visualization.graph_view import GraphView # noqa: F401
from pygui.backend.visualization.rbf_heatmap import interpolate_field # noqa: F401
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment # noqa: F401
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor, DataRecord # noqa: F401
from pygui.backend.visualization.wafer_map_item import WaferMapItem # noqa: F401
from pygui.backend.wafer.wafer_layouts import available_families, load_layout # noqa: F401
from pygui.backend.wafer.zwafer_models import DataRecord, Sensor, ZWaferData # noqa: F401
from pygui.backend.wafer.zwafer_parser import ZWaferParser # noqa: F401
from pygui.backend.wafer.wafer_layouts import load_layout, available_families # noqa: F401
+3 -1
View File
@@ -2,7 +2,9 @@
from __future__ import annotations
import math
from pygui.backend.wafer.zwafer_models import Sensor
@@ -20,7 +22,7 @@ def average_clusters(values: list[float], clusters: list[list[int]]) -> list[flo
val = values[idx]
if not math.isnan(val):
valid_vals.append(val)
if valid_vals:
mean_val = sum(valid_vals) / len(valid_vals)
for idx in cluster:
@@ -3,4 +3,3 @@ from pygui.backend.controllers.device_controller import DeviceController
from pygui.backend.controllers.session_controller import SessionController
__all__ = ["DeviceController", "SessionController"]
@@ -7,11 +7,12 @@ import threading
from datetime import datetime
from typing import Any, Optional
from PySide6.QtCore import QObject, Property, Qt, Signal, Slot
from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
from pygui.backend.controllers.session_controller import SessionController
from pygui.backend.data.local_settings import LocalSettings
from pygui.backend.models.data_model import TemperatureTableModel
from pygui.backend.visualization.graph_view import GraphView
from pygui.backend.data.local_settings import LocalSettings
from pygui.serialcomm.data_parser import (
convert_to_temperatures,
parse_binary_data,
@@ -20,7 +21,7 @@ from pygui.serialcomm.data_parser import (
)
from pygui.serialcomm.device_service import DeviceService
from pygui.serialcomm.serial_port import WaferInfo
from pygui.backend.controllers.session_controller import SessionController
# import pygui.backend.wafer_map_item
stream_controller = SessionController()
@@ -1,21 +1,23 @@
"""QML-facing controller for the live/review wafer dashboard."""
from __future__ import annotations
import logging
import time
from typing import Any, Optional
from typing import Optional
from PySide6.QtCore import QObject, Property, QTimer, Qt, Signal, Slot
from PySide6.QtCore import Property, QObject, Qt, QTimer, Signal, Slot
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
from pygui.backend.data.csv_recorder import CsvRecorder
from pygui.backend.models.frame import Frame
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data
from pygui.backend.models.sensor_editor import SensorEditor
from pygui.backend.models.session_model import SessionModel
from pygui.backend.models.threshold_classifier import ThresholdConfig
from pygui.backend.wafer.zwafer_models import Sensor
from pygui.backend.wafer.zwafer_parser import ZWaferParser
from pygui.serialcomm.stream_reader import StreamReader
from pygui.backend.models.sensor_editor import SensorEditor
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
log = logging.getLogger(__name__)
MODE_LIVE = "live"
@@ -24,7 +26,7 @@ MODE_REVIEW = "review"
class SessionController(QObject):
# public signals
frameUpdated = Signal()
frameUpdated = Signal()
modeChanged = Signal()
stateChanged = Signal()
recordingChanged = Signal()
@@ -56,7 +58,7 @@ class SessionController(QObject):
self._dirty = False
self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection)
self._sensor_editor = SensorEditor()
self._last_raw_frame: Frame | None = None
self._loaded_file: str = ""
@@ -74,13 +76,13 @@ class SessionController(QObject):
def frameTotal(self) -> int: return self._player.total
@Property(str, notify=stateChanged)
def state(self) -> str:
def state(self) -> str:
if self._last:
return self._last.state
# IF we dont have data yet, but the reader is running -> Streaming
if self._mode == MODE_LIVE and self._reader is not None:
return "streaming"
return "idle"
@Property(bool, notify=recordingChanged)
@@ -198,7 +200,12 @@ class SessionController(QObject):
@Slot(str)
def loadFile(self, file_path: str) -> None:
from pathlib import Path
from pygui.backend.data.data_records import is_official_csv, read_data_records, read_official_csv
from pygui.backend.data.data_records import (
is_official_csv,
read_data_records,
read_official_csv,
)
from pygui.backend.wafer.wafer_layouts import WaferLayout, resolve_shape_and_size
frames = []
@@ -319,10 +326,9 @@ class SessionController(QObject):
# ---- live: stream start/stop ----
@Slot(str, str)
def startStream(self, port: str, family_code: str = "") -> None:
from pygui.serialcomm.serial_port import SerialPort # transport open
import serial as pyserial
from pygui.backend.wafer.wafer_layouts import load_layout_for_wafer_id
import time
from pygui.serialcomm.serial_port import SerialPort # transport open
if family_code:
try:
@@ -346,7 +352,7 @@ class SessionController(QObject):
high_byte = payload[i * 2]
low_byte = payload[i * 2 + 1]
val16 = (high_byte << 8) | low_byte
is_negative = (val16 & 0x8000) != 0
integer_part = (val16 >> 7) & 0xFF
fractional_bits = val16 & 0x7F
@@ -355,7 +361,7 @@ class SessionController(QObject):
if is_negative:
result = -result
values.append(result)
return Frame(seq=seq, time=time.monotonic(), values=values)
# Clear out any old data from the prev sessions
@@ -378,7 +384,7 @@ class SessionController(QObject):
family_code=family_code or "A")
self._reader.start()
self._repaint_timer.start()
self.setMode("live")
self.stateChanged.emit()
@@ -389,7 +395,7 @@ class SessionController(QObject):
transport = self._reader._transport
self._reader.stop()
self._reader = None
# Send 'D2S' command padded to 512 bytes to stop the stream
if transport and transport.is_open:
try:
@@ -398,7 +404,7 @@ class SessionController(QObject):
transport.flush()
except Exception as exc:
log.error("Error sending stop command: %s", exc)
self.stateChanged.emit()
self.stopRecording()
@@ -455,6 +461,5 @@ class SessionController(QObject):
"""Remove all sensor overrides."""
self._sensor_editor.clear()
self._reprocess_current()
+1 -1
View File
@@ -2,8 +2,8 @@ import secrets
from pathlib import Path
from typing import Union
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# ===== Crypto Utilities =====
+2 -2
View File
@@ -1,7 +1,7 @@
# ===== Data Sub-package =====
from pygui.backend.data.csv_recorder import CsvRecorder
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
from pygui.backend.data.data_records import read_data_records, is_official_csv, read_official_csv
from pygui.backend.data.csv_recorder import CsvRecorder
from pygui.backend.data.data_records import is_official_csv, read_data_records, read_official_csv
from pygui.backend.data.data_segment import DataSegment
from pygui.backend.data.file_browser import FileBrowser
from pygui.backend.data.local_settings import LocalSettings
+8 -7
View File
@@ -1,24 +1,25 @@
"""Append live frames"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from typing import Optional, TextIO
from pygui.backend.models.frame import Frame
from pygui.backend.wafer.zwafer_models import Sensor
class CsvRecorder:
def __init__(self) -> None:
self._file_handle: Optional[TextIO] = None
self._oath: Optional[str] = None
@property
def is_recording(self) -> bool:
return self._file_handle is not None
def start(self, path: str, sensors:list[Sensor], serial: str = "") -> None:
Path(path).parent.mkdir(parents=True, exist_ok=True)
file_handle = open(path, "w", encoding="utf-8", newline ="")
@@ -30,14 +31,14 @@ class CsvRecorder:
file_handle.write("data\n")
file_handle.flush()
self._file_handle, self._path = file_handle, path
def write(self, frame: Frame) -> None:
if self._file_handle is None:
return
row = [f"{frame.time:.3f}"] + [f"{v:.3f}" for v in frame.values]
self._file_handle.write(",".join(row) + "\n")
self._file_handle.flush()
def stop(self) -> Optional[str]:
if self._file_handle is None:
return None
+2
View File
@@ -1,9 +1,11 @@
"""Read the data section of a Z-wafer CSV into DataRecords, or the official headerless CSV."""
from __future__ import annotations
from pathlib import Path
from pygui.backend.wafer.zwafer_models import DataRecord, Sensor
def read_data_records(file_path: str) -> list[DataRecord]:
records: list[DataRecord] = []
in_data = False
+2 -2
View File
@@ -5,12 +5,12 @@ import re
from datetime import datetime
from pathlib import Path
from PySide6.QtCore import QObject, Property, QStandardPaths, Signal, Slot
from PySide6.QtCore import Property, QObject, QStandardPaths, Signal, Slot
from PySide6.QtWidgets import QFileDialog, QMessageBox
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
from pygui.backend.wafer.zwafer_parser import ZWaferParser
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
# ===== File Browser Model =====
@@ -4,11 +4,10 @@ from copy import deepcopy
from pathlib import Path
from typing import Any
from PySide6.QtCore import QObject, Property, QDateTime, QStandardPaths, Signal, Slot
from PySide6.QtCore import Property, QDateTime, QObject, QStandardPaths, Signal, Slot
from pygui.backend.data.local_settings import LocalSettings
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
from pygui.backend.data.local_settings import LocalSettings
MASTER_FAMILIES = ("A", "B", "C", "D", "E", "F", "P", "X", "Z")
+3 -3
View File
@@ -1,12 +1,12 @@
# ===== Models Sub-package =====
from pygui.backend.models.data_model import TemperatureTableModel
from pygui.backend.models.frame import Frame
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data
from pygui.backend.models.frame_stats import Stats, compute_stats
from pygui.backend.models.session_model import SessionModel, SessionUpdate
from pygui.backend.models.data_model import TemperatureTableModel
from pygui.backend.models.sensor_editor import SensorEditor
from pygui.backend.models.threshold_classifier import ThresholdConfig, classify, resolve_bounds
from pygui.backend.models.session_model import SessionModel, SessionUpdate
from pygui.backend.models.stability_detector import StabilityDetector
from pygui.backend.models.threshold_classifier import ThresholdConfig, classify, resolve_bounds
__all__ = [
"Frame", "FramePlayer", "frames_from_wafer_data",
+1
View File
@@ -1,4 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass
+13 -11
View File
@@ -1,7 +1,9 @@
from __future__ import annotations
from __future__ import annotations
from pygui.backend.models.frame import Frame
from pygui.backend.wafer.zwafer_models import ZWaferData
def frames_from_wafer_data(data: ZWaferData, records) -> list[Frame]:
"""Build Frames from parsed DataRecords (records = list[DataRecord].)"""
return [Frame(seq=i, time=r.time, values=list(r.values)) for i, r in enumerate(records)]
@@ -10,36 +12,36 @@ class FramePlayer:
def __init__(self) -> None:
self._frames: list[Frame] =[]
self._i =0
def load(self, frames: list[Frame]) -> "FramePlayer":
self._frames = frames
self._i = 0
return self
@property
def total(self) -> int:
return len(self._frames)
@property
def index(self) -> int:
return self._i
return self._i
@property
def at_end(self) -> bool:
return self._i >= len(self._frames) -1
def current(self) -> Frame | None:
if not self._frames:
return None
return self._frames[self._i]
def _clamp(self, i:int):
return max(0, min(i, len(self._frames) - 1)) if self._frames else 0
def step(self, delta: int) -> "FramePlayer":
self._i = self._clamp(self._i + delta)
return self
def seek(self, i: int) -> "FramePlayer":
self._i = self._clamp(i)
return self
@@ -48,4 +50,4 @@ class FramePlayer:
"""Wall-clock ms until the next frame based on recorded timestamps; 0 if at end."""
if self._i + 1 >= len(self._frames):
return 0.0
return (self._frames[self._i + 1].time - self._frames[self._i].time) * 1000.0
return (self._frames[self._i + 1].time - self._frames[self._i].time) * 1000.0
+1
View File
@@ -1,6 +1,7 @@
"""Per-frame descriptive statistics"""
from __future__ import annotations
import math
from dataclasses import dataclass
+9 -9
View File
@@ -7,16 +7,16 @@ class SensorEditor:
def __init__(self) -> None:
self._replacements: dict[int, float] = {}
self._offsets: dict[int, float] = {}
def set_replacement(self, index: int, value: float) -> None:
"""Force sensor `index` to read `value` for every frame this session."""
self._replacements[index] = value
def set_offset(self, index: int, value: float) -> None:
"""Shift sensor `index` by `delta` (applied after any replacement)."""
self._offsets[index] = value
def clear(self, index: int | None = None) -> None:
"""Remove overrides. Pass an index to clear one sensor; omit to clear all."""
if index is None:
@@ -27,16 +27,16 @@ class SensorEditor:
self._offsets.pop(index, None)
def has_overrides(self) -> bool:
return bool(self._replacements or self._offsets)
def active_indices(self) -> list[int]:
"""Return sorted sensor indices that have any override."""
return sorted(set(self._replacements) | set(self._offsets))
def apply(self, values: list[float]) -> list[float]:
"""Return a new list wit all overrides applied (original unchanged)"""
if not self.has_overrides():
return list(values)
return list(values)
result = list(values)
for i in range(len(result)):
v = result[i]
@@ -45,4 +45,4 @@ class SensorEditor:
if i in self._offsets:
v += self._offsets[i]
result[i] = v
return result
return result
+8 -6
View File
@@ -1,4 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass
from pygui.backend.models.frame import Frame
@@ -6,6 +7,7 @@ from pygui.backend.models.frame_stats import Stats, compute_stats
from pygui.backend.models.stability_detector import StabilityDetector
from pygui.backend.models.threshold_classifier import ThresholdConfig, classify, resolve_bounds
@dataclass(frozen=True)
class SessionUpdate:
seq: int
@@ -15,18 +17,18 @@ class SessionUpdate:
state: str
target: float
margin: float
class SessionModel:
def __init__(self, thresholds: ThresholdConfig | None = None) -> None:
self._config = thresholds or ThresholdConfig()
self._stability = StabilityDetector()
def set_thresholds(self, config: ThresholdConfig) -> None:
self._config = config
def reset(self) -> None:
self._stability.reset()
def process(self, frame: Frame) -> SessionUpdate:
stats = compute_stats(frame.values)
target, margin = resolve_bounds(frame.values, self._config)
@@ -34,6 +36,6 @@ class SessionModel:
# State always track the user's process set_point, even in auto-color mode.
state = self._stability.update(stats.avg, frame.time, self._config.set_point)
return SessionUpdate(
seq=frame.seq, values=frame.values, bands=bands,
seq=frame.seq, values=frame.values, bands=bands,
stats=stats, state=state, target=target, margin=margin,
)
)
@@ -1,5 +1,6 @@
"""Detect process state (Idle, Ramp, Set) from the running average temp"""
from __future__ import annotations
from typing import Optional
STATE_IDLE = "idle"
@@ -7,28 +8,27 @@ STATE_RAMP = "ramp"
STATE_SET = "set"
class StabilityDetector:
def __init__(self, idle_below: float = 50.0, tolerance: float = 1.0,
class StabilityDetector:
def __init__(self, idle_below: float = 50.0, tolerance: float = 1.0,
settle_seconds: float = 10.0) -> None:
self._idle_below = idle_below
self._tolerance = tolerance
self._settle_seconds = settle_seconds
self._near_since: Optional[float] = None # When avg entered the +- tolerance band
def reset(self) -> None:
self._near_since = None
def update(self, avg: float, time: float, set_point: float) ->str:
if avg < self._idle_below:
self._near_since = None
return STATE_IDLE
if abs(avg - set_point) <= self._tolerance:
if self._near_since is None:
self._near_since = time
self._near_since = time
if time - self._near_since >= self._settle_seconds:
return STATE_SET
return STATE_RAMP
self._near_since = None
return STATE_RAMP
@@ -4,11 +4,12 @@ Auto mode derives target=mean, margin=1
"""
from __future__ import annotations
import math
from dataclasses import dataclass
BAND_IN = "in_range"
BAND_HIGH = "high"
BAND_HIGH = "high"
BAND_LOW = "low"
@@ -39,4 +40,4 @@ def classify(value: float, target: float, margin: float) -> str:
def classify_all(values: list[float], cfg: ThresholdConfig) -> list[str]:
target, margin = resolve_bounds(values, cfg)
return [classify(v, target, margin)for v in values ]
return [classify(v, target, margin)for v in values ]
+2 -2
View File
@@ -1,8 +1,8 @@
# ===== Visualization Sub-package =====
from pygui.backend.visualization.wafer_map_item import WaferMapItem
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment
from pygui.backend.visualization.graph_view import GraphView
from pygui.backend.visualization.rbf_heatmap import interpolate_field
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment
from pygui.backend.visualization.wafer_map_item import WaferMapItem
__all__ = [
"WaferMapItem", "GraphView",
@@ -9,7 +9,7 @@ from __future__ import annotations
import logging
from typing import Any, Optional
from PySide6.QtCore import QObject, Property, Signal, Slot
from PySide6.QtCore import Property, QObject, Signal, Slot
from PySide6.QtWidgets import QWidget
log = logging.getLogger(__name__)
@@ -1,4 +1,4 @@
from typing import List, Tuple, Optional
from typing import List, Optional, Tuple
import numpy as np
@@ -13,9 +13,15 @@ from __future__ import annotations
import math
import numpy as np
from PySide6.QtCore import Property, QPoint, Signal, Slot, Qt
from PySide6.QtCore import Property, QPoint, Qt, Signal, Slot
from PySide6.QtGui import (
QBrush, QColor, QFont, QImage, QPainter, QPen, QPolygon,
QBrush,
QColor,
QFont,
QImage,
QPainter,
QPen,
QPolygon,
)
from PySide6.QtQml import QmlElement
from PySide6.QtQuick import QQuickPaintedItem
+2 -2
View File
@@ -1,7 +1,7 @@
# ===== Wafer Sub-package =====
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor, DataRecord
from pygui.backend.wafer.wafer_layouts import available_families, load_layout
from pygui.backend.wafer.zwafer_models import DataRecord, Sensor, ZWaferData
from pygui.backend.wafer.zwafer_parser import ZWaferParser
from pygui.backend.wafer.wafer_layouts import load_layout, available_families
__all__ = [
"ZWaferData", "Sensor", "DataRecord",
+3 -3
View File
@@ -1,8 +1,8 @@
from datetime import datetime
from pathlib import Path
from typing import Tuple, Optional
from datetime import datetime, timedelta
from typing import Optional, Tuple
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor
from pygui.backend.wafer.zwafer_models import Sensor, ZWaferData
# ===== Z-Wafer CSV Parser =====
+1 -2
View File
@@ -6,7 +6,6 @@ QML via @Slot methods on a QObject controller.
"""
import logging
from pathlib import Path
from typing import Optional
import serial.tools.list_ports
@@ -42,8 +41,8 @@ class DeviceService:
# On macOS/Linux, also detect virtual loopback serial ports (PTYs) opened by socat
import platform
import subprocess
import re
import subprocess
if platform.system() in ("Darwin", "Linux"):
try:
+3 -2
View File
@@ -1,5 +1,6 @@
"""Continuous serial frame reader (Live-mode source)"""
from __future__ import annotations
import logging
import threading
import time
@@ -13,7 +14,7 @@ log = logging.getLogger(__name__)
ParseFrame = Callable[[bytes, int], Frame]
class StreamReader:
def __init__(self, transport, parse_frame: ParseFrame,
on_frame: Callable[[Frame], None],
on_error: Callable[[Exception], None] | None = None,
@@ -26,7 +27,7 @@ class StreamReader:
self._thread: threading.Thread | None = None
self._stop = threading.Event()
self.error_count = 0
def start(self) -> None:
self._stop.clear()
self._thread = threading.Thread(target=self._run, daemon = True)