summaryrefslogtreecommitdiff
path: root/devices/serial_device.py
diff options
context:
space:
mode:
Diffstat (limited to 'devices/serial_device.py')
-rw-r--r--devices/serial_device.py85
1 files changed, 82 insertions, 3 deletions
diff --git a/devices/serial_device.py b/devices/serial_device.py
index 61c3a64..cef34e8 100644
--- a/devices/serial_device.py
+++ b/devices/serial_device.py
@@ -28,7 +28,7 @@ from PyQt6.QtWidgets import (
QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout, QGroupBox,
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QListWidget,
QListWidgetItem, QPushButton, QScrollArea, QSizePolicy, QSpinBox,
- QStackedWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
+ QStackedWidget, QTableWidget, QTableWidgetItem, QTextEdit, QVBoxLayout, QWidget,
)
from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus
@@ -46,11 +46,12 @@ _FORMAT_LABELS: Dict[str, str] = {
"Modbus RTU": "modbus_rtu",
"ASCII Mark-10": "mark10",
"ASCII CML": "cml",
+ "ASCII DSCUSB": "dscusb",
}
_LABEL_FOR: Dict[str, str] = {v: k for k, v in _FORMAT_LABELS.items()}
_GENERIC_FORMATS = {"key:val", "csv", "json"}
-_PROTOCOL_FORMATS = {"scpi", "modbus_rtu", "mark10", "cml"}
+_PROTOCOL_FORMATS = {"scpi", "modbus_rtu", "mark10", "cml", "dscusb"}
# parse_format → auto-name label — fallback only, used when the OS can't
# describe the port (simulation mode, or the device isn't enumerable)
@@ -59,6 +60,7 @@ _AUTO_NAME_LABEL: Dict[str, str] = {
"scpi": "SCPI Serial Device",
"modbus_rtu": "Modbus Serial Device",
"cml": "CML Serial Device",
+ "dscusb": "DSCUSB Extensometer",
}
@@ -82,6 +84,7 @@ _STACK_IDX = {
"modbus_rtu": 2,
"mark10": 3,
"cml": 4,
+ "dscusb": 5,
}
@@ -116,6 +119,7 @@ class SerialDevice(BaseDevice):
self._baud = baud_rate
self._fmt = parse_format
self.simulate = simulate
+ self._last_error = ""
# Generic
self._num_channels = num_channels
@@ -149,7 +153,10 @@ class SerialDevice(BaseDevice):
# ── BaseDevice ────────────────────────────────────────────────────────
def connect(self) -> bool:
+ self._last_error = ""
ok = self._layer.connect()
+ if not ok and hasattr(self._layer, "last_error"):
+ self._last_error = self._layer.last_error
self.status = DeviceStatus.SIMULATED if self.simulate else (
DeviceStatus.CONNECTED if ok else DeviceStatus.ERROR
)
@@ -220,6 +227,8 @@ class SerialDevice(BaseDevice):
})
elif self._fmt == "mark10":
pass # no extra config
+ elif self._fmt == "dscusb":
+ pass # no extra config
elif self._fmt == "cml":
cfg["motors"] = self._motors
return cfg
@@ -320,6 +329,10 @@ class SerialDevice(BaseDevice):
ChannelConfig("zero", "Zero Gauge", "", 0.0, 1.0, color=_COLORS[2], writable=True),
ChannelConfig("cycle_units", "Cycle Units", "", 0.0, 1.0, color=_COLORS[3], writable=True),
]
+ elif fmt == "dscusb":
+ return [
+ ChannelConfig("extension", "Extension", "", -50.0, 50.0, color=_COLORS[0]),
+ ]
elif fmt == "cml":
_CMD_UNITS = {"TP": "counts", "TV": "counts/s", "TC": "%×10", "TS": "flags"}
_WRITE_UNITS = {"VS": "counts/s", "MA": "counts"}
@@ -407,6 +420,9 @@ class SerialDevice(BaseDevice):
elif fmt == "mark10":
from api_layers.protocols.mark10 import Mark10Layer
return Mark10Layer(self._port, self._baud, simulate=self.simulate)
+ elif fmt == "dscusb":
+ from api_layers.protocols.dscusb import DSCUSBLayer
+ return DSCUSBLayer(self._port, self._baud, simulate=self.simulate)
elif fmt == "cml":
from api_layers.protocols.cml import CMLLayer, CMLMotor
cml_motors = [
@@ -496,8 +512,10 @@ class SerialConfigWidget(QWidget):
self._modbus_panel = _ModbusPanel(device=self.device)
self._mark10_panel = _Mark10Panel()
self._cml_panel = _CMLPanel(device=self.device)
+ self._dscusb_panel = _DSCUSBPanel()
for panel in (self._generic_panel, self._scpi_panel,
- self._modbus_panel, self._mark10_panel, self._cml_panel):
+ self._modbus_panel, self._mark10_panel, self._cml_panel,
+ self._dscusb_panel):
self._proto_stack.addWidget(panel)
self._proto_stack.setCurrentIndex(_STACK_IDX.get(self.device._fmt, 0))
sp = self._proto_stack.sizePolicy()
@@ -612,6 +630,42 @@ class _GenericPanel(QWidget):
self._ch_spin.setValue(len(device.info.channels) if device._fmt in _GENERIC_FORMATS else 4)
lay.addRow("Signals:", self._ch_spin)
+ # ── Diagnostics ───────────────────────────────────────────────────
+ # Connected but no channels showing values? This is where to look —
+ # shows exactly what's arriving on the wire, so you can see whether
+ # it matches the expected key:val/csv/json shape at all.
+ diag_grp = QGroupBox("Connection Diagnostics")
+ diag_lay = QVBoxLayout(diag_grp)
+ diag_lay.setContentsMargins(10, 12, 10, 10)
+ self._diag_box = QTextEdit()
+ self._diag_box.setObjectName("codeEditor")
+ self._diag_box.setReadOnly(True)
+ self._diag_box.setMaximumHeight(120)
+ diag_lay.addWidget(self._diag_box)
+ refresh_btn = QPushButton("🔍 Refresh")
+ refresh_btn.setToolTip("Show the last few raw lines received on this port")
+ refresh_btn.clicked.connect(self._refresh_diag)
+ diag_lay.addWidget(refresh_btn)
+ lay.addRow(diag_grp)
+ self._refresh_diag()
+
+ def _refresh_diag(self):
+ lines = [f"Status: {self.device.status.value}"]
+ layer = getattr(self.device, "_layer", None)
+ raw = getattr(layer, "raw_lines", None) if layer is not None else None
+ if raw:
+ lines.append("Last lines received:")
+ for r in raw:
+ lines.append(f" {r!r}")
+ else:
+ lines.append(
+ "(no lines received yet — check the port is receiving data "
+ "at all, and that baud rate matches the device)"
+ )
+ if getattr(self.device, "_last_error", ""):
+ lines.append(f"\nError:\n{self.device._last_error}")
+ self._diag_box.setPlainText("\n".join(lines))
+
def collect(self) -> dict:
return {"num_channels": self._ch_spin.value()}
@@ -846,6 +900,31 @@ class _Mark10Panel(QWidget):
lay.addStretch()
+class _DSCUSBPanel(QWidget):
+ """Info panel for DSCUSB — no extra user config needed."""
+
+ def __init__(self):
+ super().__init__()
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(0, 4, 0, 4)
+ grp = QGroupBox("DSCUSB Protocol")
+ grp_lay = QVBoxLayout(grp)
+ lbl = QLabel(
+ "Sends '!001:SYS?\\r', parses a plain numeric response.\n\n"
+ "Fixed channel:\n"
+ " extension — raw reading, unit as reported by the device\n"
+ " (add a Scale/Offset Channel downstream for\n"
+ " unit conversion, e.g. ×25.4 for in→mm)\n\n"
+ "Response is CR-terminated only (no LF) — this protocol reads\n"
+ "with read_until(CR), not the generic reader's readline()."
+ )
+ lbl.setObjectName("traceSource")
+ lbl.setWordWrap(True)
+ grp_lay.addWidget(lbl)
+ lay.addWidget(grp)
+ lay.addStretch()
+
+
class _CMLPanel(QWidget):
"""Motor list for CoolMuscle CML."""