parent
70eb58531f
commit
de53882ead
Binary file not shown.
Binary file not shown.
@ -0,0 +1,76 @@
|
||||
# File: components/i2c/i2c_command_editor.py
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QFormLayout,
|
||||
QComboBox,
|
||||
QSpinBox,
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
|
||||
class I2CCommandEditorDialog(QDialog):
|
||||
def __init__(self, command=None):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Modify I2C Command" if command else "Add I2C Command")
|
||||
|
||||
form_layout = QFormLayout()
|
||||
form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
form_layout.setFormAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
form_layout.setSpacing(10)
|
||||
|
||||
# === Input Fields ===
|
||||
self.name_input = QLineEdit()
|
||||
self.description_input = QLineEdit()
|
||||
self.category_input = QLineEdit()
|
||||
self.operation_input = QComboBox()
|
||||
self.operation_input.addItems(["read", "write"])
|
||||
self.register_input = QSpinBox()
|
||||
self.register_input.setRange(0, 255)
|
||||
self.hex_input = QLineEdit()
|
||||
self.hex_input.setPlaceholderText("0x01,0x02")
|
||||
|
||||
if command:
|
||||
self.name_input.setText(command.get("name", ""))
|
||||
self.description_input.setText(command.get("description", ""))
|
||||
self.category_input.setText(command.get("category", ""))
|
||||
self.operation_input.setCurrentText(command.get("operation", "read"))
|
||||
self.register_input.setValue(command.get("register", 0))
|
||||
self.hex_input.setText(command.get("hex_string", ""))
|
||||
|
||||
form_layout.addRow("Name:", self.name_input)
|
||||
form_layout.addRow("Description:", self.description_input)
|
||||
form_layout.addRow("Category:", self.category_input)
|
||||
form_layout.addRow("Operation:", self.operation_input)
|
||||
form_layout.addRow("Register:", self.register_input)
|
||||
form_layout.addRow("Hex:", self.hex_input)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.ok_button = QPushButton("OK")
|
||||
self.cancel_button = QPushButton("Cancel")
|
||||
self.ok_button.clicked.connect(self.accept)
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.ok_button)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addLayout(form_layout)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
self.setMinimumWidth(400)
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
"name": self.name_input.text().strip(),
|
||||
"description": self.description_input.text().strip(),
|
||||
"category": self.category_input.text().strip(),
|
||||
"operation": self.operation_input.currentText(),
|
||||
"register": self.register_input.value(),
|
||||
"hex_string": self.hex_input.text().strip(),
|
||||
}
|
||||
@ -0,0 +1,342 @@
|
||||
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())
|
||||
Loading…
Reference in new issue