Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bcdff9756 |
-37
@@ -1,37 +0,0 @@
|
||||
# ADC YOFF Sign Bug — sras_viewer.py
|
||||
|
||||
## Status
|
||||
Fix applied, awaiting user testing.
|
||||
|
||||
## What was wrong
|
||||
|
||||
`DC_YOFF_ADC` in `sras_viewer.py` was `+87.04` instead of `-87.04`.
|
||||
|
||||
The Tektronix scope stores CH3/CH4 waveform data as **signed int8** (−128 to +127), where ADC 0 = screen center. The scope's vertical position for CH3/CH4 is set to `−2.72 div` in `sc3_aui_app.py`, which places 0 V **below** center at ADC count `−2.72 × 32 = −87.04`. The comment in the code had the formula as `-position × (256/8)` (sign flipped), producing `+87.04` instead of the correct `−87.04`.
|
||||
|
||||
## Effect of the bug
|
||||
|
||||
- `adc_to_mv` was off by 272 mV in the negative direction
|
||||
- ADC −87 (true 0 V signal) → −272 mV (should be ≈ 0 mV)
|
||||
- ADC 0 (screen center, above ground) → −136 mV (should be +136 mV)
|
||||
- DC images for CH3/CH4 (Bias A/B) showed large negative voltages, physically impossible for DC bias signals
|
||||
- RF mask threshold (`mv_to_adc`) was also broken: threshold ADC value ~+87 was being compared against pixel means clustered around −87, so nearly every pixel would have been incorrectly masked
|
||||
|
||||
## The fix
|
||||
|
||||
`sras_viewer.py` line 48:
|
||||
```python
|
||||
# Before
|
||||
DC_YOFF_ADC = 87.04 # ADC count that represents 0 V
|
||||
|
||||
# After
|
||||
DC_YOFF_ADC = -87.04 # ADC count that represents 0 V
|
||||
```
|
||||
Comment on line 47 also corrected from `-position × (256/8)` to `position × (256/8)`.
|
||||
|
||||
## What to verify during testing
|
||||
|
||||
1. CH3 and CH4 DC images show positive (or near-zero) voltages consistent with the bias signal levels
|
||||
2. RF (CH1) image is not excessively masked — pixels with a genuine bias signal above the threshold should appear
|
||||
3. `mv_to_adc(0.0)` should now return −87.04 (not +87.04)
|
||||
4. The default threshold of 0.125 mV should correspond to ADC ≈ −87.0, not +87.1
|
||||
Binary file not shown.
-56298
File diff suppressed because one or more lines are too long
@@ -1,407 +0,0 @@
|
||||
"""Focusing Control Panel — Qt UI module for ScanEngine-3.
|
||||
|
||||
This module provides a dialog-based control panel for the T3R stepper
|
||||
controller, allowing users to home, step, jog, and monitor the focus
|
||||
axis in real time. It integrates with scanengine via the hardware
|
||||
abstraction layer (hardware.t3r_driver).
|
||||
|
||||
Usage:
|
||||
from sc3_aui_focusing import FocusingControlPanel
|
||||
panel = FocusingControlPanel(parent_window)
|
||||
panel.show()
|
||||
|
||||
Signals:
|
||||
focus_position_changed(ch, position) — emitted after each move/jog completes
|
||||
focus_error(ch, message) — emitted on controller errors
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QDoubleSpinBox, QPushButton, QLabel, QGroupBox, QVBoxLayout,
|
||||
QHBoxLayout, QSpacerItem, QSizePolicy, QErrorMessage, QMessageBox,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
except ImportError:
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QDoubleSpinBox, QPushButton, QLabel, QGroupBox, QVBoxLayout,
|
||||
QHBoxLayout, QSpacerItem, QSizePolicy, QErrorMessage, QMessageBox,
|
||||
)
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
|
||||
from hardware import T3RStepperDriver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FocusingControlPanel(QDialog):
|
||||
"""Qt dialog for controlling the T3R focus stepper."""
|
||||
|
||||
focus_position_changed = pyqtSignal(int, float)
|
||||
focus_error = pyqtSignal(int, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Focusing Control Panel")
|
||||
self.setWindowFlags(Qt.Window | Qt.WindowCloseButtonHint)
|
||||
self.setMinimumWidth(780)
|
||||
self.setMinimumHeight(560)
|
||||
self._driver = None
|
||||
self._connected = False
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self):
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||
main_layout.setSpacing(10)
|
||||
|
||||
title_label = QLabel("Focusing Control Panel")
|
||||
title_label.setFont(title_label.font().copy(size=Qt.FontSize.Fixed))
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
main_layout.addWidget(title_label)
|
||||
|
||||
status_groupbox = QGroupBox("Status")
|
||||
status_layout = QHBoxLayout(status_groupbox)
|
||||
self.status_label = QLabel("Not connected")
|
||||
self.status_label.setMinimumWidth(200)
|
||||
status_layout.addWidget(self.status_label)
|
||||
main_layout.addWidget(status_groupbox)
|
||||
|
||||
axis_groupbox = QGroupBox("T-axis (Focus)")
|
||||
axis_layout = QHBoxLayout(axis_groupbox)
|
||||
self.position_label = QLabel("Position: 0")
|
||||
self.position_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
axis_layout.addWidget(self.position_label)
|
||||
main_layout.addWidget(axis_groupbox)
|
||||
|
||||
controls_groupbox = QGroupBox("Controls")
|
||||
controls_layout = QVBoxLayout(controls_groupbox)
|
||||
controls_layout.setSpacing(6)
|
||||
|
||||
home_row = QHBoxLayout()
|
||||
self.home_button = QPushButton("Home")
|
||||
home_row.addWidget(self.home_button)
|
||||
spacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||
home_row.addSpacerItem(spacer)
|
||||
controls_layout.addLayout(home_row)
|
||||
|
||||
step_row = QHBoxLayout()
|
||||
self.step_minus_button = QPushButton("< Step (-)")
|
||||
self.steps_spinbox = QDoubleSpinBox()
|
||||
self.steps_spinbox.setMinimum(-10000.0)
|
||||
self.steps_spinbox.setMaximum(10000.0)
|
||||
self.steps_spinbox.setSingleStep(100.0)
|
||||
self.step_plus_button = QPushButton("Step (+) >")
|
||||
step_row.addWidget(self.step_minus_button)
|
||||
step_row.addWidget(self.steps_spinbox)
|
||||
step_row.addWidget(self.step_plus_button)
|
||||
controls_layout.addLayout(step_row)
|
||||
|
||||
jog_row = QHBoxLayout()
|
||||
self.jog_minus_button = QPushButton("< Jog (-)")
|
||||
self.velocity_spinbox = QDoubleSpinBox()
|
||||
self.velocity_spinbox.setMinimum(-10000.0)
|
||||
self.velocity_spinbox.setMaximum(10000.0)
|
||||
self.velocity_spinbox.setSingleStep(500.0)
|
||||
self.jog_plus_button = QPushButton("Jog (+) >")
|
||||
jog_row.addWidget(self.jog_minus_button)
|
||||
jog_row.addWidget(self.velocity_spinbox)
|
||||
jog_row.addWidget(self.jog_plus_button)
|
||||
controls_layout.addLayout(jog_row)
|
||||
|
||||
accel_row = QHBoxLayout()
|
||||
self.accel_minus_button = QPushButton("< Accel (-)")
|
||||
self.accel_spinbox = QDoubleSpinBox()
|
||||
def connect(self, port="/dev/ttyUSB0", spd=115200):
|
||||
"""Connect to the T3R controller and start polling."""
|
||||
try:
|
||||
self._driver = T3RStepperDriver()
|
||||
self._driver.connect(port=port, spd=spd)
|
||||
self._connected = True
|
||||
self.status_label.setText("Connected")
|
||||
self.position_label.setText(f"Position: {self._driver.get_position(0):,.0f}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception("Failed to connect to T3R controller")
|
||||
self.status_label.setText(f"Connection error: {e!s}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the controller."""
|
||||
if self._driver is not None:
|
||||
try:
|
||||
self._driver.disconnect()
|
||||
except Exception as e:
|
||||
logger.warning("Error disconnecting: %s", e)
|
||||
self._driver = None
|
||||
self._connected = False
|
||||
self.status_label.setText("Not connected")
|
||||
|
||||
def home(self):
|
||||
"""Home the T-axis."""
|
||||
if not self._connected or self._driver is None:
|
||||
return
|
||||
try:
|
||||
self._driver.move(0, steps=0, velocity=8000, accel=4000)
|
||||
self.position_label.setText("Position: 0")
|
||||
except Exception as e:
|
||||
logger.exception("Home failed")
|
||||
self.focus_error.emit(0, f"Home error: {e!s}")
|
||||
|
||||
def step(self, direction=1):
|
||||
"""Step by the amount in steps_spinbox."""
|
||||
if not self._connected or self._driver is None:
|
||||
return
|
||||
try:
|
||||
steps = int(round(self.steps_spinbox.value()))
|
||||
if steps == 0:
|
||||
return
|
||||
sign = 1 if direction > 0 else -1
|
||||
self._driver.move(0, steps=sign * steps, velocity=8000, accel=4000)
|
||||
pos = self._driver.get_position(0)
|
||||
self.position_label.setText(f"Position: {pos:,}")
|
||||
except Exception as e:
|
||||
logger.exception("Step failed")
|
||||
self.focus_error.emit(0, f"Step error: {e!s}")
|
||||
|
||||
def jog(self, direction=1):
|
||||
"""Jog at the velocity in velocity_spinbox."""
|
||||
if not self._connected or self._driver is None:
|
||||
return
|
||||
try:
|
||||
vel = int(round(self.velocity_spinbox.value()))
|
||||
if vel == 0:
|
||||
return
|
||||
sign = 1 if direction > 0 else -1
|
||||
accel = int(round(self.accel_spinbox.value()))
|
||||
self._driver.jog(0, velocity=sign * vel, accel=accel)
|
||||
except Exception as e:
|
||||
logger.exception("Jog failed")
|
||||
self.focus_error.emit(0, f"Jog error: {e!s}")
|
||||
|
||||
def stop(self):
|
||||
"""Stop any motion."""
|
||||
if not self._connected or self._driver is None:
|
||||
return
|
||||
try:
|
||||
self._driver.stop(0)
|
||||
except Exception as e:
|
||||
logger.exception("Stop failed")
|
||||
self.focus_error.emit(0, f"Stop error: {e!s}")
|
||||
|
||||
def refresh_position(self):
|
||||
"""Update the position label (called from polling thread)."""
|
||||
if not self._connected or self._driver is None:
|
||||
return
|
||||
try:
|
||||
pos = self._driver.get_position(0)
|
||||
self.position_label.setText(f"Position: {pos:,}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Polling integration ──────────────────────────────────────────────────
|
||||
|
||||
def start_polling(self, interval=0.3):
|
||||
"""Start periodic position polling (called after connect)."""
|
||||
if self._driver is None:
|
||||
return
|
||||
self._driver.start_polling(interval)
|
||||
|
||||
def stop_polling(self):
|
||||
"""Stop polling."""
|
||||
if self._driver is not None:
|
||||
self._driver.stop_polling()
|
||||
|
||||
def poll_loop(self):
|
||||
"""Run the polling loop (typically in a separate thread)."""
|
||||
self.refresh_position()
|
||||
import time
|
||||
while self._connected and self._driver is not None:
|
||||
try:
|
||||
self._driver.start_polling(0.3)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
|
||||
# ── Signal handlers ──────────────────────────────────────────────────────
|
||||
|
||||
def _on_home_clicked(self):
|
||||
self.home()
|
||||
|
||||
def _on_step_minus_clicked(self):
|
||||
self.step(-1)
|
||||
|
||||
def _on_step_plus_clicked(self):
|
||||
self.step(+1)
|
||||
|
||||
def _on_jog_minus_clicked(self):
|
||||
self.jog(-1)
|
||||
|
||||
def _on_jog_plus_clicked(self):
|
||||
self.jog(+1)
|
||||
|
||||
def _on_accel_minus_clicked(self):
|
||||
pass # accel is just a jog parameter, handled by jog()
|
||||
|
||||
def _on_accel_plus_clicked(self):
|
||||
pass
|
||||
|
||||
def _on_stop_clicked(self):
|
||||
self.stop()
|
||||
|
||||
def _on_close_clicked(self):
|
||||
self.close()
|
||||
|
||||
# Wire up all buttons to their handlers
|
||||
self.home_button.clicked.connect(_on_home_clicked)
|
||||
self.step_minus_button.clicked.connect(_on_step_minus_clicked)
|
||||
self.step_plus_button.clicked.connect(_on_step_plus_clicked)
|
||||
self.jog_minus_button.clicked.connect(_on_jog_minus_clicked)
|
||||
self.jog_plus_button.clicked.connect(_on_jog_plus_clicked)
|
||||
self.accel_minus_button.clicked.connect(_on_accel_minus_clicked)
|
||||
self.accel_plus_button.clicked.connect(_on_accel_plus_clicked)
|
||||
self.stop_button.clicked.connect(_on_stop_clicked)
|
||||
self.close_button.clicked.connect(_on_close_clicked)
|
||||
|
||||
|
||||
# ── Signal handlers ──────────────────────────────────────────────────────
|
||||
|
||||
def _on_home_clicked(self):
|
||||
self.home()
|
||||
|
||||
def _on_step_minus_clicked(self):
|
||||
self.step(-1)
|
||||
|
||||
def _on_step_plus_clicked(self):
|
||||
self.step(+1)
|
||||
|
||||
def _on_jog_minus_clicked(self):
|
||||
self.jog(-1)
|
||||
|
||||
def _on_jog_plus_clicked(self):
|
||||
self.jog(+1)
|
||||
|
||||
def _on_accel_minus_clicked(self):
|
||||
pass # accel is just a jog parameter, handled by jog()
|
||||
|
||||
def _on_accel_plus_clicked(self):
|
||||
pass
|
||||
|
||||
def _on_stop_clicked(self):
|
||||
self.stop()
|
||||
|
||||
def _on_close_clicked(self):
|
||||
self.close()
|
||||
|
||||
# Wire up all buttons to their handlers
|
||||
self.home_button.clicked.connect(_on_home_clicked)
|
||||
self.step_minus_button.clicked.connect(_on_step_minus_clicked)
|
||||
self.step_plus_button.clicked.connect(_on_step_plus_clicked)
|
||||
self.jog_minus_button.clicked.connect(_on_jog_minus_clicked)
|
||||
self.jog_plus_button.clicked.connect(_on_jog_plus_clicked)
|
||||
self.accel_minus_button.clicked.connect(_on_accel_minus_clicked)
|
||||
self.accel_plus_button.clicked.connect(_on_accel_plus_clicked)
|
||||
self.stop_button.clicked.connect(_on_stop_clicked)
|
||||
self.close_button.clicked.connect(_on_close_clicked)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Save state and cleanup."""
|
||||
self.disconnect()
|
||||
# ── Polling integration ──────────────────────────────────────────────────
|
||||
|
||||
def start_polling(self, interval=0.3):
|
||||
"""Start periodic position polling (called after connect)."""
|
||||
if self._driver is None:
|
||||
return
|
||||
self._driver.start_polling(interval)
|
||||
|
||||
def stop_polling(self):
|
||||
"""Stop polling."""
|
||||
if self._driver is not None:
|
||||
self._driver.stop_polling()
|
||||
|
||||
def poll_loop(self):
|
||||
"""Run the polling loop (typically in a separate thread)."""
|
||||
self.refresh_position()
|
||||
import time
|
||||
while self._connected and self._driver is not None:
|
||||
try:
|
||||
self._driver.start_polling(0.3)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
|
||||
# ── Module documentation ───────────────────────────────────────────────────
|
||||
|
||||
__all__ = ["FocusingControlPanel"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
panel = FocusingControlPanel()
|
||||
panel.show()
|
||||
sys.exit(app.exec())
|
||||
# ── Polling integration ──────────────────────────────────────────────────
|
||||
|
||||
def start_polling(self, interval=0.3):
|
||||
"""Start periodic position polling (called after connect)."""
|
||||
if self._driver is None:
|
||||
return
|
||||
self._driver.start_polling(interval)
|
||||
|
||||
def stop_polling(self):
|
||||
"""Stop polling."""
|
||||
if self._driver is not None:
|
||||
self._driver.stop_polling()
|
||||
|
||||
def poll_loop(self):
|
||||
"""Run the polling loop (typically in a separate thread)."""
|
||||
self.refresh_position()
|
||||
import time
|
||||
while self._connected and self._driver is not None:
|
||||
try:
|
||||
self._driver.start_polling(0.3)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
|
||||
event.accept()
|
||||
self.accel_spinbox.setMinimum(-10000.0)
|
||||
self.accel_spinbox.setMaximum(10000.0)
|
||||
self.accel_spinbox.setSingleStep(500.0)
|
||||
self.accel_plus_button = QPushButton("Accel (+) >")
|
||||
accel_row.addWidget(self.accel_minus_button)
|
||||
accel_row.addWidget(self.accel_spinbox)
|
||||
accel_row.addWidget(self.accel_plus_button)
|
||||
controls_layout.addLayout(accel_row)
|
||||
|
||||
stop_row = QHBoxLayout()
|
||||
self.stop_button = QPushButton("Stop")
|
||||
stop_row.addWidget(self.stop_button)
|
||||
controls_layout.addLayout(stop_row)
|
||||
|
||||
main_layout.addWidget(controls_groupbox)
|
||||
|
||||
self.close_button = QPushButton("Close")
|
||||
main_layout.addWidget(self.close_button)
|
||||
# ── Module documentation ───────────────────────────────────────────────────
|
||||
|
||||
__all__ = ["FocusingControlPanel"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
panel = FocusingControlPanel()
|
||||
panel.show()
|
||||
sys.exit(app.exec())
|
||||
Reference in New Issue
Block a user