You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

332 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 isnt GCd
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())