|
|
# File: components/i2c/i2c_polling.py
|
|
|
|
|
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QHBoxLayout
|
|
|
from PyQt6.QtCore import QTimer, Qt
|
|
|
from PyQt6.QtGui import QPainter, QColor, QPen
|
|
|
from components.console.console_registry import log_main_console
|
|
|
from smbus2 import SMBus
|
|
|
import config.config as config
|
|
|
import math
|
|
|
|
|
|
AS5048_ADDR = 0x40
|
|
|
REG_ANGLE_MSB = 0xFE
|
|
|
REG_ANGLE_LSB = 0xFF
|
|
|
|
|
|
|
|
|
class AngleGauge(QWidget):
|
|
|
def __init__(self, parent=None):
|
|
|
super().__init__(parent)
|
|
|
self.setMinimumSize(200, 200)
|
|
|
self.angle = 0 # degrees
|
|
|
|
|
|
def set_angle(self, angle):
|
|
|
self.angle = max(0.0, min(90.0, angle))
|
|
|
self.update()
|
|
|
|
|
|
def paintEvent(self, event):
|
|
|
painter = QPainter(self)
|
|
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
|
|
|
|
rect = self.rect()
|
|
|
width = rect.width()
|
|
|
height = rect.height()
|
|
|
center_x = width // 2
|
|
|
bottom_y = height - 10
|
|
|
length = height - 40
|
|
|
|
|
|
# Draw vertical Y axis
|
|
|
pen = QPen(QColor(180, 180, 180), 2)
|
|
|
painter.setPen(pen)
|
|
|
painter.drawLine(center_x, bottom_y, center_x, bottom_y - length)
|
|
|
|
|
|
# Draw needle
|
|
|
radians = math.radians(self.angle)
|
|
|
end_x = int(center_x + length * math.sin(radians))
|
|
|
end_y = int(bottom_y - length * math.cos(radians))
|
|
|
|
|
|
pen = QPen(QColor(0, 100, 200), 4)
|
|
|
painter.setPen(pen)
|
|
|
painter.drawLine(center_x, bottom_y, end_x, end_y)
|
|
|
|
|
|
# Draw angle value
|
|
|
painter.setPen(Qt.GlobalColor.black)
|
|
|
painter.drawText(
|
|
|
rect,
|
|
|
int(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter),
|
|
|
f"{self.angle:.1f}°",
|
|
|
)
|
|
|
|
|
|
|
|
|
class I2CPollingWidget(QWidget):
|
|
|
def __init__(self, bus_id=1, parent=None):
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.label = QLabel("AS5048A Angle Sensor")
|
|
|
self.gauge = AngleGauge()
|
|
|
self.zero_raw = None
|
|
|
|
|
|
self.start_button = QPushButton("Start")
|
|
|
self.stop_button = QPushButton("Stop")
|
|
|
self.set_zero_button = QPushButton("Set Zero")
|
|
|
|
|
|
self.start_button.clicked.connect(self.start_polling)
|
|
|
self.stop_button.clicked.connect(self.stop_polling)
|
|
|
self.set_zero_button.clicked.connect(self.set_zero)
|
|
|
|
|
|
button_layout = QHBoxLayout()
|
|
|
button_layout.addWidget(self.start_button)
|
|
|
button_layout.addWidget(self.stop_button)
|
|
|
button_layout.addWidget(self.set_zero_button)
|
|
|
|
|
|
layout = QVBoxLayout()
|
|
|
layout.addWidget(self.label)
|
|
|
layout.addWidget(self.gauge)
|
|
|
layout.addLayout(button_layout)
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
self.bus_id = bus_id
|
|
|
self.bus = None
|
|
|
self.timer = QTimer(self)
|
|
|
self.timer.timeout.connect(self.read_angle)
|
|
|
|
|
|
if not config.DEBUG_MODE:
|
|
|
try:
|
|
|
self.bus = SMBus(self.bus_id)
|
|
|
log_main_console("success", f"AS5048A ready on bus {self.bus_id}")
|
|
|
except Exception as e:
|
|
|
log_main_console("error", f"Failed to open I2C bus {self.bus_id}: {e}")
|
|
|
else:
|
|
|
log_main_console("info", "DEBUG_MODE: fake polling setup complete")
|
|
|
|
|
|
def start_polling(self):
|
|
|
self.timer.start(50)
|
|
|
log_main_console("info", "Started I2C angle polling")
|
|
|
|
|
|
def stop_polling(self):
|
|
|
self.timer.stop()
|
|
|
log_main_console("info", "Stopped I2C angle polling")
|
|
|
|
|
|
def set_zero(self):
|
|
|
raw = self._read_raw_angle()
|
|
|
if raw is not None:
|
|
|
self.zero_raw = raw
|
|
|
log_main_console("info", f"Zero set to raw value: {self.zero_raw}")
|
|
|
|
|
|
def _read_raw_angle(self):
|
|
|
if config.DEBUG_MODE:
|
|
|
import random
|
|
|
|
|
|
return random.randint(0, 16383)
|
|
|
try:
|
|
|
msb = self.bus.read_byte_data(AS5048_ADDR, REG_ANGLE_MSB)
|
|
|
lsb = self.bus.read_byte_data(AS5048_ADDR, REG_ANGLE_LSB)
|
|
|
raw = ((msb << 6) | (lsb & 0x3F)) & 0x3FFF
|
|
|
return raw
|
|
|
except Exception as e:
|
|
|
log_main_console("error", f"I2C read failed: {e}")
|
|
|
return None
|
|
|
|
|
|
def read_angle(self):
|
|
|
current_raw = self._read_raw_angle()
|
|
|
if current_raw is None or self.zero_raw is None:
|
|
|
return
|
|
|
|
|
|
# Calculate signed delta
|
|
|
diff = (current_raw - self.zero_raw + 8192) % 16384 - 8192
|
|
|
angle_deg = -((diff * 360.0) / 16384.0)
|
|
|
|
|
|
# Clamp to 0–90° range for display
|
|
|
angle_clamped = max(0.0, min(90.0, angle_deg))
|
|
|
|
|
|
log_main_console(
|
|
|
"info",
|
|
|
f"Read raw: {current_raw}, Δ: {diff}, Degrees: {angle_deg:.2f}° → Gauge: {angle_clamped:.2f}°",
|
|
|
)
|
|
|
self.gauge.set_angle(angle_clamped)
|
|
|
|
|
|
def closeEvent(self, event):
|
|
|
self.timer.stop()
|
|
|
if self.bus:
|
|
|
self.bus.close()
|
|
|
super().closeEvent(event)
|