|
|
# components/i2c/i2c_logger_ui.py
|
|
|
#
|
|
|
from PyQt6.QtWidgets import (
|
|
|
QWidget,
|
|
|
QVBoxLayout,
|
|
|
QHBoxLayout,
|
|
|
QPushButton,
|
|
|
QComboBox,
|
|
|
QLineEdit,
|
|
|
QSplitter,
|
|
|
)
|
|
|
from PyQt6.QtCore import QObject, pyqtSignal
|
|
|
from PyQt6.QtCore import Qt
|
|
|
|
|
|
|
|
|
from components.console.console_ui import console_widget
|
|
|
from components.console.console_registry import log_main_console
|
|
|
import components.items.elements as elements
|
|
|
import config.config as config
|
|
|
|
|
|
from components.i2c.i2c_logic import I2CLogic, _coerce_int
|
|
|
|
|
|
|
|
|
class _SafeConsoleProxy(QObject):
|
|
|
log_signal = pyqtSignal(str, str) # level, message
|
|
|
|
|
|
def __init__(self, console):
|
|
|
super().__init__()
|
|
|
self.log_signal.connect(console.log)
|
|
|
|
|
|
def __call__(self, level, msg):
|
|
|
self.log_signal.emit(level, msg)
|
|
|
|
|
|
|
|
|
class I2CLoggerWidget(QWidget):
|
|
|
"""Drop-in single-page I2C widget with table | console splitter."""
|
|
|
|
|
|
def __init__(self, parent=None):
|
|
|
super().__init__(parent)
|
|
|
self.i2c_logic = I2CLogic()
|
|
|
self.commands = self.i2c_logic.get_predefined_commands()
|
|
|
self.comboboxes = {}
|
|
|
self.connection_status = False
|
|
|
self.init_ui()
|
|
|
|
|
|
def init_ui(self):
|
|
|
# Top controls
|
|
|
top_controls = QWidget()
|
|
|
top_controls_layout = QHBoxLayout(top_controls)
|
|
|
top_controls_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
top_controls_layout.setSpacing(12)
|
|
|
top_controls_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
|
# Port
|
|
|
self.comboboxes["port"] = QComboBox()
|
|
|
self.comboboxes["port"].addItems(self.i2c_logic.get_channels())
|
|
|
top_controls_layout.addWidget(
|
|
|
elements.label_and_widget("Port", self.comboboxes["port"])
|
|
|
)
|
|
|
|
|
|
self.button_refresh_channels = elements.create_icon_button(
|
|
|
config.REFRESH_BUTTON_ICON_LINK, icon_size=30, border_size=4
|
|
|
)
|
|
|
top_controls_layout.addWidget(self.button_refresh_channels)
|
|
|
|
|
|
# Address (empty until connected → scan)
|
|
|
self.comboboxes["address"] = QComboBox()
|
|
|
top_controls_layout.addWidget(
|
|
|
elements.label_and_widget("Address", self.comboboxes["address"])
|
|
|
)
|
|
|
|
|
|
self.button_refresh_address = elements.create_icon_button(
|
|
|
config.REFRESH_BUTTON_ICON_LINK, icon_size=30, border_size=4
|
|
|
)
|
|
|
top_controls_layout.addWidget(self.button_refresh_address)
|
|
|
|
|
|
self.input_register = QLineEdit()
|
|
|
self.input_register.setPlaceholderText("0x04")
|
|
|
top_controls_layout.addWidget(
|
|
|
elements.label_and_widget("Register", self.input_register)
|
|
|
)
|
|
|
|
|
|
self.input_length = QLineEdit()
|
|
|
self.input_length.setPlaceholderText("1")
|
|
|
top_controls_layout.addWidget(
|
|
|
elements.label_and_widget("Length", self.input_length)
|
|
|
)
|
|
|
|
|
|
# Set Zero / Connect / start / Disconnect
|
|
|
self.button_set_zero = QPushButton("Set Zero")
|
|
|
top_controls_layout.addWidget(
|
|
|
elements.label_and_widget("", self.button_set_zero)
|
|
|
)
|
|
|
|
|
|
self.button_connect = QPushButton("Connect")
|
|
|
top_controls_layout.addWidget(
|
|
|
elements.label_and_widget("", self.button_connect)
|
|
|
)
|
|
|
|
|
|
self.button_start = QPushButton("Start")
|
|
|
top_controls_layout.addWidget(elements.label_and_widget("", self.button_start))
|
|
|
|
|
|
self.button_disconnect = QPushButton("Disconnect")
|
|
|
top_controls_layout.addWidget(
|
|
|
elements.label_and_widget("", self.button_disconnect)
|
|
|
)
|
|
|
|
|
|
col1_widget = QWidget()
|
|
|
col1_layout = QVBoxLayout(col1_widget)
|
|
|
# col1_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
col1_layout.setSpacing(4)
|
|
|
|
|
|
# Console
|
|
|
self.console = console_widget()
|
|
|
|
|
|
# IMPORTANT: keep a strong reference so it isn’t GC’d
|
|
|
self._i2c_console_proxy = _SafeConsoleProxy(self.console)
|
|
|
self.i2c_logic.set_logger(self._i2c_console_proxy)
|
|
|
|
|
|
# === Console only ===
|
|
|
console_stack_widget = QWidget()
|
|
|
console_stack_layout = QVBoxLayout(console_stack_widget)
|
|
|
console_stack_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
console_stack_layout.setSpacing(4)
|
|
|
console_stack_layout.addWidget(self.console)
|
|
|
|
|
|
# === Splitter (kept for consistency, only console pane) ===
|
|
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
splitter.addWidget(console_stack_widget)
|
|
|
splitter.setStretchFactor(0, 1)
|
|
|
|
|
|
# === Main Layout ===
|
|
|
main_layout = QVBoxLayout()
|
|
|
main_layout.setContentsMargins(4, 4, 4, 4)
|
|
|
main_layout.setSpacing(6)
|
|
|
main_layout.addWidget(top_controls)
|
|
|
main_layout.addWidget(splitter, stretch=1)
|
|
|
self.setLayout(main_layout)
|
|
|
|
|
|
# Signals
|
|
|
self.button_refresh_channels.clicked.connect(self.refresh_channels)
|
|
|
self.button_refresh_address.clicked.connect(self.refresh_address)
|
|
|
self.button_set_zero.clicked.connect(self.set_zero)
|
|
|
self.button_connect.clicked.connect(self.connect)
|
|
|
self.button_start.clicked.connect(self.start_logging)
|
|
|
self.button_disconnect.clicked.connect(self.disconnect)
|
|
|
|
|
|
self.disconnected_enable_status()
|
|
|
|
|
|
# --- UI state toggles ---
|
|
|
def disconnected_enable_status(self):
|
|
|
elements.set_enabled_state(True, self.button_refresh_channels, grayOut=False)
|
|
|
elements.set_enabled_state(True, self.comboboxes["port"], grayOut=False)
|
|
|
elements.set_enabled_state(False, self.button_disconnect, grayOut=True)
|
|
|
elements.set_enabled_state(True, self.button_connect, grayOut=False)
|
|
|
elements.set_enabled_state(False, self.button_refresh_address, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.comboboxes["address"], grayOut=True)
|
|
|
elements.set_enabled_state(False, self.input_register, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.button_start, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.input_length, grayOut=True)
|
|
|
|
|
|
def connected_enable_status(self):
|
|
|
elements.set_enabled_state(False, self.button_refresh_channels, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.comboboxes["port"], grayOut=True)
|
|
|
elements.set_enabled_state(True, self.button_disconnect, grayOut=False)
|
|
|
elements.set_enabled_state(False, self.button_connect, grayOut=True)
|
|
|
elements.set_enabled_state(True, self.button_refresh_address, grayOut=False)
|
|
|
elements.set_enabled_state(True, self.comboboxes["address"], grayOut=False)
|
|
|
elements.set_enabled_state(True, self.input_register, grayOut=False)
|
|
|
elements.set_enabled_state(True, self.button_start, grayOut=False)
|
|
|
elements.set_enabled_state(True, self.input_length, grayOut=False)
|
|
|
|
|
|
def started_enable_status(self):
|
|
|
elements.set_enabled_state(False, self.button_refresh_channels, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.comboboxes["port"], grayOut=True)
|
|
|
elements.set_enabled_state(True, self.button_disconnect, grayOut=False)
|
|
|
elements.set_enabled_state(False, self.button_connect, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.button_refresh_address, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.comboboxes["address"], grayOut=True)
|
|
|
elements.set_enabled_state(False, self.input_register, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.button_start, grayOut=True)
|
|
|
elements.set_enabled_state(False, self.input_length, grayOut=True)
|
|
|
|
|
|
# --- Buttons ---
|
|
|
def set_zero(self):
|
|
|
log_main_console("info", "Stting Zero")
|
|
|
self.i2c_logic.measure_zero_raw14()
|
|
|
|
|
|
def connect(self):
|
|
|
log_main_console("info", "🔗 Connecting...")
|
|
|
success = self.i2c_logic.connect(self.comboboxes["port"].currentText())
|
|
|
if success:
|
|
|
self.connected_enable_status()
|
|
|
self.refresh_address(silent=True)
|
|
|
self.connection_status = True
|
|
|
else:
|
|
|
elements.flash_button(
|
|
|
self.button_connect, flash_style="background-color: red;"
|
|
|
)
|
|
|
|
|
|
def start_logging(self):
|
|
|
# use the combobox address and user fields (reg, length)
|
|
|
addr = self._current_address_int()
|
|
|
reg_txt = (self.input_register.text() or "").strip()
|
|
|
len_txt = (self.input_length.text() or "").strip() or "1"
|
|
|
|
|
|
def _to_int_any(s, default=None):
|
|
|
try:
|
|
|
return int(str(s), 0)
|
|
|
except Exception:
|
|
|
return default
|
|
|
|
|
|
reg = _to_int_any(reg_txt)
|
|
|
length = _to_int_any(len_txt, default=1)
|
|
|
if reg is None or length is None or length <= 0:
|
|
|
self.console.log("warning", "⚠️ Invalid register or length")
|
|
|
return
|
|
|
|
|
|
# start the logic-side logger (read-only continuous)
|
|
|
if self.i2c_logic.start_logger(
|
|
|
device_address=addr, reg=reg, length=length, interval_ms=100
|
|
|
):
|
|
|
self.started_enable_status()
|
|
|
else:
|
|
|
elements.flash_button(
|
|
|
self.button_start, flash_style="background-color: red;"
|
|
|
)
|
|
|
|
|
|
def disconnect(self):
|
|
|
log_main_console("info", "🔌 Disconnecting...")
|
|
|
if self.i2c_logic.disconnect():
|
|
|
self.disconnected_enable_status()
|
|
|
self.connection_status = False
|
|
|
self.refresh_channels(silent=True)
|
|
|
else:
|
|
|
elements.flash_button(
|
|
|
self.button_disconnect, flash_style="background-color: red;"
|
|
|
)
|
|
|
|
|
|
def refresh_channels(self, silent: bool = False):
|
|
|
log_main_console("info", "🔄 Refreshing buses...")
|
|
|
self.comboboxes["port"].clear()
|
|
|
ports = self.i2c_logic.get_channels()
|
|
|
if ports:
|
|
|
self.comboboxes["port"].addItems(ports)
|
|
|
if not silent:
|
|
|
elements.flash_button(self.button_refresh_channels)
|
|
|
log_main_console("success", "🔄 Bus list refreshed")
|
|
|
else:
|
|
|
elements.flash_button(
|
|
|
self.button_refresh_channels, flash_style="background-color: red;"
|
|
|
)
|
|
|
log_main_console("warn", "No I2C buses found")
|
|
|
|
|
|
def refresh_address(self, silent: bool = False):
|
|
|
log_main_console("info", "🔄 Scanning bus for devices...")
|
|
|
self.comboboxes["address"].clear()
|
|
|
addresses = self.i2c_logic.scan_bus()
|
|
|
if addresses:
|
|
|
self.comboboxes["address"].addItems(addresses)
|
|
|
if not silent:
|
|
|
elements.flash_button(self.button_refresh_address)
|
|
|
log_main_console("success", "🔄 Bus scan complete")
|
|
|
else:
|
|
|
elements.flash_button(
|
|
|
self.button_refresh_address, flash_style="background-color: red;"
|
|
|
)
|
|
|
log_main_console("info", "No devices detected")
|
|
|
|
|
|
def get_current_config(self):
|
|
|
return {key: cb.currentText() for key, cb in self.comboboxes.items()}
|
|
|
|
|
|
def _current_address_int(self) -> int:
|
|
|
txt = self.comboboxes["address"].currentText().strip() or "0x40"
|
|
|
return _coerce_int(txt)
|
|
|
|
|
|
def send_command(self, command: dict):
|
|
|
self.i2c_logic.send_command(command, device_address=self._current_address_int())
|
|
|
|
|
|
def send_command_raw(self):
|
|
|
action = self.comboboxes["action"].currentText().strip().upper()
|
|
|
reg_txt = (self.input_register.text() or "").strip()
|
|
|
data_txt = (self.input_hex.text() or "").strip()
|
|
|
length_txt = (self.input_length.text() or "").strip()
|
|
|
|
|
|
# --- helpers ---
|
|
|
def _to_int_any(s, default=None):
|
|
|
try:
|
|
|
return int(str(s), 0) # accepts "0xFE", "254"
|
|
|
except Exception:
|
|
|
return default
|
|
|
|
|
|
def _split_tokens(s: str):
|
|
|
return [t for t in s.replace(",", " ").split() if t]
|
|
|
|
|
|
# --- validate register ---
|
|
|
reg = _to_int_any(reg_txt)
|
|
|
if reg is None:
|
|
|
self.console.log("warning", "⚠️ Invalid or missing register (e.g. 0xFE).")
|
|
|
return
|
|
|
|
|
|
cmd = {
|
|
|
"action": action.lower(),
|
|
|
"reg": reg,
|
|
|
}
|
|
|
|
|
|
if action == "READ":
|
|
|
ln = _to_int_any(length_txt, default=1)
|
|
|
if ln is None or ln <= 0:
|
|
|
self.console.log("warning", "⚠️ Invalid length (must be >0).")
|
|
|
return
|
|
|
cmd["length"] = ln
|
|
|
|
|
|
elif action == "WRITE":
|
|
|
toks = _split_tokens(data_txt)
|
|
|
if not toks:
|
|
|
self.console.log(
|
|
|
"warning", "⚠️ WRITE requires data (e.g. 00 or 01,02,03)."
|
|
|
)
|
|
|
return
|
|
|
if len(toks) == 1:
|
|
|
v = _to_int_any(toks[0])
|
|
|
if v is None:
|
|
|
self.console.log(
|
|
|
"warning", "⚠️ Invalid byte. Use hex/dec like 0x00 or 0."
|
|
|
)
|
|
|
return
|
|
|
cmd["value"] = v
|
|
|
else:
|
|
|
cmd["bytes"] = data_txt # leave as string; logic will parse
|
|
|
|
|
|
self.i2c_logic.send_command(cmd, device_address=self._current_address_int())
|