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.
343 lines
13 KiB
343 lines
13 KiB
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())
|