from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QLineEdit, QSplitter, ) from PyQt6.QtCore import Qt from PyQt6.QtCore import QObject, pyqtSignal from components.console.console_ui import console_widget from components.console.console_registry import log_main_console import components.items.elements as elements from components.commands.command_table_ui import command_table_widget import config.config as config from components.i2c.i2c_logic import I2CLogic, _coerce_int from components.i2c.i2c_command_editor import I2CCommandEditorDialog from components.data import db class _SafeConsoleProxy(QObject): log_signal = pyqtSignal(str, str) def __init__(self, console): super().__init__() self.log_signal.connect(console.log) def __call__(self, level, msg): # called from worker thread → emits safely into UI thread self.log_signal.emit(level, msg) class I2CHandler: """Adapter used by command_table_widget → forwards to the I2CWidget.""" def __init__(self, widget: "I2CWidget"): self.w = widget def get_command_list(self): return self.w.i2c_logic.get_predefined_commands() def send_command(self, command: dict): self.w.send_command(command) def add_command(self): dialog = I2CCommandEditorDialog() if dialog.exec(): command = dialog.get_data() db.add_i2c_command(command) def modify_command(self, command): dialog = I2CCommandEditorDialog(command=command) if dialog.exec(): updated = dialog.get_data() updated["id"] = command["id"] db.modify_i2c_command(updated) def delete_command(self, command): db.delete_i2c_command(command) class I2CWidget(QWidget): """Drop-in single-page I2C widget with table | console splitter.""" def __init__(self, parent=None): super().__init__(parent) self.i2c_logic = I2CLogic() self.handler = I2CHandler(self) 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) # Connect / Disconnect self.button_connect = QPushButton("Connect") top_controls_layout.addWidget( elements.label_and_widget("", self.button_connect) ) self.button_disconnect = QPushButton("Disconnect") top_controls_layout.addWidget( elements.label_and_widget("", self.button_disconnect) ) # Command table self.command_table = command_table_widget( commands=self.commands, handler=self.handler ) col1_widget = QWidget() col1_layout = QVBoxLayout(col1_widget) col1_layout.setContentsMargins(0, 0, 0, 0) col1_layout.setSpacing(4) col1_layout.addWidget(self.command_table) # Input + Send input_line_layout = QHBoxLayout() input_line_layout.setContentsMargins(0, 0, 0, 0) input_line_layout.setSpacing(4) self.comboboxes["action"] = QComboBox() self.comboboxes["action"].addItems(["Read", "Write"]) input_line_layout.addWidget( elements.label_and_widget("Action", self.comboboxes["action"]) ) self.input_register = QLineEdit() self.input_register.setPlaceholderText("0x04") input_line_layout.addWidget( elements.label_and_widget("Register", self.input_register) ) self.input_hex = QLineEdit() self.input_hex.setPlaceholderText( "Hex bytes (e.g. 0x01,0x02,0x03) → block write to reg 0x00" ) input_line_layout.addWidget(elements.label_and_widget("Data", self.input_hex)) self.input_length = QLineEdit() self.input_length.setPlaceholderText("1") input_line_layout.addWidget( elements.label_and_widget("Length", self.input_length) ) self.button_send_raw = QPushButton("Send Raw") input_line_layout.addWidget(elements.label_and_widget("", self.button_send_raw)) # Console self.console = console_widget() self.i2c_logic.set_logger(self.console.log) 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.addLayout(input_line_layout) console_stack_layout.addWidget(self.console) # Splitter: table | console splitter = QSplitter(Qt.Orientation.Horizontal) splitter.addWidget(col1_widget) splitter.addWidget(console_stack_widget) splitter.setSizes([740, 1200]) splitter.setStretchFactor(0, 0) splitter.setStretchFactor(1, 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_connect.clicked.connect(self.connect) self.button_disconnect.clicked.connect(self.disconnect) self.button_send_raw.clicked.connect(self.send_command_raw) 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(True, self.command_table, grayOut=False) elements.set_enabled_state(False, self.input_hex, grayOut=True) elements.set_enabled_state(False, self.button_send_raw, grayOut=True) 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.comboboxes["action"], grayOut=True) elements.set_enabled_state(False, self.input_register, 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.command_table, grayOut=False) elements.set_enabled_state(True, self.input_hex, grayOut=False) elements.set_enabled_state(True, self.button_send_raw, grayOut=False) 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.comboboxes["action"], grayOut=False) elements.set_enabled_state(True, self.input_register, grayOut=False) # --- Buttons --- def connect(self): log_main_console("info", "🔗 Connecting...") success = self.i2c_logic.connect(self.comboboxes["port"].currentText()) if success: self.connected_enable_status() self.command_table.set_connected_state() self.refresh_address(silent=True) self.connection_status = True else: elements.flash_button( self.button_connect, flash_style="background-color: red;" ) def disconnect(self): log_main_console("info", "🔌 Disconnecting...") if self.i2c_logic.stop_logger(): self.disconnected_enable_status() self.command_table.set_disconnected_state() 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())