Skip to content
Open
28 changes: 24 additions & 4 deletions software/control/_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -1244,11 +1244,31 @@ def get_wellplate_settings(wellplate_format):
OBJECTIVE_TURRET_BAUDRATE = 115200
# Objective name -> turret slot index (1..4). Override per machine in .ini.
OBJECTIVE_TURRET_POSITIONS = {"4x": 1, "10x": 2, "20x": 3, "40x": 4}
# Global pulse offset added to every turret slot target, shifting the whole slot
# frame relative to the homing switch (pulse 0). Corrects units whose limit switch
# does not sit exactly at slot 1. 0 on all normal units; set per machine (may be
# negative).
# Pulse offset of slot 1 from the homing zero (the origin sensor's trigger edge);
# the other slots follow at exactly 90-degree spacing. 0 on all normal units; set
# per machine (may be negative). Software homing (2026-07) moved the zero slightly
# vs the old driver homing — re-measure after upgrading a machine with an old value.
OBJECTIVE_TURRET_OFFSET_PULSES = 0
# Gear backlash compensation in turret degrees (0..1, 0 disables). When > 0 every
# slot change first overshoots below the target by this angle and then approaches
# it from below, so the final approach direction is always the same and gear
# backlash cancels out.
OBJECTIVE_TURRET_BACKLASH_DEG = 0.0
# Set True for turret motor models wired with the opposite phase order (same
# commands spin the other way). The controller then negates move targets, jog
# signs and the homing-sweep direction bit, and flips position readbacks, so
# slot mapping, offset and backlash logic keep working in the same logical
# coordinate system — OBJECTIVE_TURRET_OFFSET_PULSES is always logical-coordinate
# pulses. After toggling on an existing machine, re-home and re-measure the
# offset: the physical zero moves with the sweep direction.
OBJECTIVE_TURRET_DIRECTION_INVERTED = False
# Set True for objective changers whose origin-switch sensor triggers on the
# opposite logic level (port of SingleMotor's "原点开关极性取反" option, 2026-08-12).
# Software homing / distance search then invert the DI1 trigger verdict, so the
# homing direction and the sweep-backoff-fine-search state machine stay unchanged.
# Toggling on an existing machine requires re-homing: the sensor edge found by
# fine search (and thus the physical zero) sits on the other side of the window.
OBJECTIVE_TURRET_DI_INVERT = False


def _validate_objective_changer_flags(use_xeryon: bool, use_turret: bool) -> None:
Expand Down
3 changes: 3 additions & 0 deletions software/control/microscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ def build_from_global_config(
baudrate=control._def.OBJECTIVE_TURRET_BAUDRATE,
positions=control._def.OBJECTIVE_TURRET_POSITIONS,
offset_pulses=control._def.OBJECTIVE_TURRET_OFFSET_PULSES,
backlash_deg=control._def.OBJECTIVE_TURRET_BACKLASH_DEG,
direction_inverted=control._def.OBJECTIVE_TURRET_DIRECTION_INVERTED,
di_invert=control._def.OBJECTIVE_TURRET_DI_INVERT,
stage=stage,
)
objective_changer = (
Expand Down
13 changes: 13 additions & 0 deletions software/control/modbus_rtu.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,19 @@ def read_input_register(self, slave_id: int, address: int) -> int:
response = self._send_receive(frame, expected_response_len=7)
return (response[3] << 8) | response[4]

def read_input_registers(self, slave_id: int, address: int, count: int) -> list[int]:
"""Read `count` consecutive 16-bit input registers in one FC 0x04 transaction.

One frame instead of `count` round-trips — needed by pollers that must see a
consistent snapshot of several registers (e.g. DI level + position + alarm)
within a tight polling period.
"""
self._require_connected()
frame = build_read_input_registers_frame(slave_id, address, count)
# Response: slave(1) + fc(1) + byte_count(1) + data(2*count) + crc(2)
response = self._send_receive(frame, expected_response_len=5 + 2 * count)
return [(response[3 + 2 * i] << 8) | response[4 + 2 * i] for i in range(count)]

def read_input_register_32bit(self, slave_id: int, address: int, signed: bool = False) -> int:
"""Read a 32-bit input register pair via FC 0x04 (see read_input_register)."""
self._require_connected()
Expand Down
Loading
Loading