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