SpectraMax Gemini EM#
The Molecular Devices SpectraMax Gemini EM is a fluorescence plate reader controlled over a
serial RS-232 interface. PyLabRobot supports this reader with the
MolecularDevicesSpectraMaxGeminiEMBackend.
Installation#
pip install pylabrobot[serial]
On Linux, the USB-to-RS-232 adapter will typically appear as /dev/ttyUSB0 or /dev/ttyUSB1.
On Windows, it appears as a COM port.
from pylabrobot.plate_reading import PlateReader, MolecularDevicesSpectraMaxGeminiEMBackend
backend = MolecularDevicesSpectraMaxGeminiEMBackend(port="/dev/ttyUSB0")
pr = PlateReader(name="gemini", backend=backend, size_x=0, size_y=0, size_z=0)
await pr.setup()
2026-05-17 13:10:52,216 - pylabrobot.io.serial - INFO - Using explicitly provided port: /dev/ttyUSB0 (for VID=None, PID=None)
Open Tray#
await pr.open()
Close Tray#
await pr.close()
Validated behavior#
The Gemini EM backend was validated against a Molecular Devices SpectraMax Gemini EM using a USB-to-RS-232 adapter. The instrument identified itself as:
GEMINI EM
2.00b78 01Mar04
The following operations were validated:
opening and closing the drawer
querying reader status
querying and setting temperature
endpoint fluorescence reads on a 96-well plate
endpoint fluorescence reads on a 384-well plate
endpoint luminescence reads on a 96-well plate
endpoint time-resolved fluorescence reads on a 96-well plate
kinetic time-resolved fluorescence reads on a 96-well plate
emission and excitation fluorescence spectrum reads on a 96-well plate
fluorescence fill wellscan reads on a 96-well plate
top and bottom fluorescence read-stage selection
configurable excitation and emission wavelengths
The Windows/SoftMax Pro setup used during development was for protocol discovery only. The intended
deployment path is direct serial control from Linux using the USB-to-RS-232 adapter, typically as
/dev/ttyUSB0 or /dev/ttyUSB1.
Absorbance and fluorescence polarization are not implemented for this backend.
Support status#
Mode |
Backend status |
Hardware status |
|---|---|---|
Drawer open/close |
implemented |
validated |
Status and temperature query |
implemented |
validated |
Temperature setpoint |
implemented |
validated |
Fluorescence endpoint |
implemented |
validated, 96-well and 384-well |
Fluorescence top/bottom read |
implemented |
validated |
Luminescence endpoint |
implemented |
validated, top read |
Time-resolved fluorescence endpoint |
implemented |
validated |
Time-resolved fluorescence kinetic |
implemented |
validated with a short run |
Fluorescence emission spectrum |
implemented |
validated with a short sweep |
Fluorescence excitation spectrum |
implemented |
validated with a short sweep |
Fluorescence wellscan |
implemented |
validated with fill pattern |
Rectangular partial-region reads |
implemented |
unit tested |
Arbitrary non-rectangular partial reads |
not implemented |
not validated |
Absorbance |
not supported by Gemini EM |
not applicable |
Fluorescence polarization |
not implemented |
not validated |
Fluorescence reads#
Assign a plate to the reader before reading. The backend uses the plate resource geometry to configure the reader’s plate position.
from pylabrobot.resources import Cor_96_wellplate_360ul_Fb
plate = Cor_96_wellplate_360ul_Fb(name="plate")
pr.assign_child_resource(plate)
data = await backend.read_fluorescence(
plate=plate,
wells=plate.get_all_items(),
excitation_wavelength=485,
emission_wavelength=520,
)
For a TRITC-like fluorescence read:
data = await backend.read_fluorescence(
plate=plate,
wells=plate.get_all_items(),
excitation_wavelength=557,
emission_wavelength=576,
)
For a bottom read, pass read_from_bottom=True:
data = await backend.read_fluorescence(
plate=plate,
wells=plate.get_all_items(),
excitation_wavelength=485,
emission_wavelength=520,
read_from_bottom=True,
)
Full-plate reads and rectangular contiguous well regions are supported. Arbitrary non-rectangular
well selections raise NotImplementedError.
Luminescence reads#
Endpoint luminescence is available through the Gemini EM backend. The implementation follows the
SoftMax Pro command sequence captured during development, including !READTYPE LUM,
!EMWAVELENGTH 0, !TOPREADCLEAR OFF, and !READSTAGE TOP.
data = await backend.read_luminescence(
plate=plate,
wells=plate.get_all_items(),
)
Optional pre-read shaking is supported:
from pylabrobot.plate_reading.molecular_devices import ShakeSettings
data = await backend.read_luminescence(
plate=plate,
wells=plate.get_all_items(),
shake_settings=ShakeSettings(before_read=True, before_read_duration=10),
)
Only endpoint, top-read luminescence is currently implemented.
Time-Resolved Fluorescence#
Endpoint time-resolved fluorescence is available through the Gemini EM backend. The implementation uses the Gemini EM command form observed from SoftMax Pro:
!READTYPE TIME <delay_time> <integration_time>
For example:
data = await backend.experimental_read_time_resolved_fluorescence(
plate=plate,
excitation_wavelengths=[485],
emission_wavelengths=[525],
cutoff_filters=[7],
delay_time=50,
integration_time=850,
)
For a bottom TRF read:
data = await backend.experimental_read_time_resolved_fluorescence(
plate=plate,
excitation_wavelengths=[485],
emission_wavelengths=[525],
cutoff_filters=[7],
delay_time=50,
integration_time=850,
read_from_bottom=True,
)
Kinetic TRF is also supported with ReadType.KINETIC and KineticSettings:
from pylabrobot.plate_reading.molecular_devices import KineticSettings, ReadType
data = await backend.experimental_read_time_resolved_fluorescence(
plate=plate,
excitation_wavelengths=[485],
emission_wavelengths=[525],
cutoff_filters=[7],
delay_time=50,
integration_time=850,
read_type=ReadType.KINETIC,
kinetic_settings=KineticSettings(interval=30, num_readings=21),
)
Endpoint and kinetic TRF are currently implemented for full plates and rectangular contiguous well regions. Arbitrary non-rectangular well selections are not supported.
Fluorescence Spectra#
Gemini-specific helpers are available for fluorescence spectra. They use SpectrumSettings
internally but expose fixed-excitation and fixed-emission sweeps directly.
For an emission spectrum with fixed excitation:
data = await backend.experimental_read_fluorescence_emission_spectrum(
plate=plate,
wells=plate.get_all_items(),
excitation_wavelength=350,
start_emission_wavelength=400,
step=10,
num_steps=36,
read_from_bottom=True,
)
---------------------------------------------------------------------------
CancelledError Traceback (most recent call last)
Cell In[10], line 1
----> 1 data = await backend.experimental_read_fluorescence_emission_spectrum(
2 plate=plate,
3 wells=plate.get_all_items(),
4 excitation_wavelength=350,
File ~/git/pylabrobot/pylabrobot/plate_reading/molecular_devices/spectramax_gemini_em_backend.py:406, in MolecularDevicesSpectraMaxGeminiEMBackend.experimental_read_fluorescence_emission_spectrum(self, plate, wells, excitation_wavelength, start_emission_wavelength, step, num_steps, cutoff_filters, read_order, calibrate, shake_settings, carriage_speed, read_from_bottom, pmt_gain, flashes_per_well, timeout)
403 await self._set_order(settings)
405 await self._read_now()
--> 406 await self._wait_for_idle(timeout=timeout)
407 return await self._transfer_data(settings)
File ~/git/pylabrobot/pylabrobot/plate_reading/molecular_devices/backend.py:689, in MolecularDevicesBackend._wait_for_idle(self, timeout)
687 if time.time() - start_time > timeout:
688 raise TimeoutError("Timeout waiting for plate reader to become idle.")
--> 689 status = await self.get_status()
690 if status and status[1] == "IDLE":
691 break
File ~/git/pylabrobot/pylabrobot/plate_reading/molecular_devices/backend.py:333, in MolecularDevicesBackend.get_status(self)
332 async def get_status(self) -> List[str]:
--> 333 res = await self.send_command("!STATUS")
334 if len(res) > 1:
335 return res[1].split()
File ~/git/pylabrobot/pylabrobot/plate_reading/molecular_devices/backend.py:287, in MolecularDevicesBackend.send_command(self, command, timeout, num_res_fields)
285 timeout_time = time.time() + timeout
286 while True:
--> 287 raw_response += await self.io.readline()
288 await asyncio.sleep(0.001)
289 if time.time() > timeout_time:
File ~/git/pylabrobot/pylabrobot/io/serial.py:244, in Serial.readline(self)
241 if self._executor is None or self._ser is None:
242 raise RuntimeError(f"Call setup() first for device '{self._human_readable_device_name}'.")
--> 244 data = await loop.run_in_executor(self._executor, self._ser.readline)
246 if len(data) != 0:
247 logger.log(LOG_LEVEL_IO, "[%s] readline %s", self._port, data)
CancelledError:
This sends the Gemini EM spectrum mode:
!EXWAVELENGTH 350
!MODE EMSPECTRUM 400 10 36
!ORDER WAVELENGTH
For an excitation spectrum with fixed emission:
data = await backend.experimental_read_fluorescence_excitation_spectrum(
plate=plate,
wells=plate.get_all_items(),
emission_wavelength=600,
start_excitation_wavelength=350,
step=20,
num_steps=4,
read_from_bottom=True,
)
This sends:
!EMWAVELENGTH 600
!AUTOFILTER EX OFF
!MODE EXSPECTRUM 350 20 4
!ORDER WAVELENGTH
The spectrum helpers default to cutoff filter 1, matching the captured SoftMax Pro spectrum
protocols. Pass cutoff_filters=[...] to override this.
Wellscan fluorescence#
The Gemini EM backend includes a Gemini-specific experimental_read_fluorescence_wellscan method. SoftMax
Pro implements wellscan by enabling wellscan mode and performing separate reads at shifted plate
origins. The backend mirrors that behavior instead of averaging scan points silently.
data = await backend.experimental_read_fluorescence_wellscan(
plate=plate,
wells=plate.get_all_items(),
excitation_wavelength=485,
emission_wavelength=525,
pattern="fill",
)
Supported patterns are:
horizontal
vertical
cross
fill
Each returned read includes wellscan_point, wellscan_x, and wellscan_y fields.
384-well plates#
384-well fluorescence reads were validated with Greiner_384_wellplate_28ul_Fb:
from pylabrobot.resources import Greiner_384_wellplate_28ul_Fb
plate = Greiner_384_wellplate_28ul_Fb(name="plate")
pr.assign_child_resource(plate)
data = await backend.read_fluorescence(
plate=plate,
wells=plate.get_all_items(),
excitation_wavelength=557,
emission_wavelength=576,
)
OEM command observations#
SoftMax Pro was used during development to observe the OEM serial behavior. The observed bottom-read setup sequence was:
!OPTION
!TEMP
!CLEAR DATA
!TAG OFF
!WELLSCANMODE
!XPOS 14.380 9 12
!YPOS 11.235 9 8
!SHAKE OFF
!SHAKE 0 0 0 0 0
!STRIP 1 12
!READTYPE FLU
!EMWAVELENGTH 525
!AUTOFILTER OFF
!EMFILTER 7
!EXWAVELENGTH 490
!FPW 6
!TOPREADCLEAR ON
!AUTOPMT ON
!CSPEED 8
!PMTCAL ON
!MODE ENDPOINT
!ORDER COLUMN
!READSTAGE BOT
!READ
The Gemini EM responds to !READSTAGE BOT and !READSTAGE TOP with a single OK response
field. This differs from some other Molecular Devices readers and is handled by the Gemini EM
backend.
SoftMax Pro sent !TOPREADCLEAR ON before selecting either read stage. The Gemini EM backend
does the same before sending !READSTAGE TOP or !READSTAGE BOT.
Luminescence#
SoftMax Pro used this sequence for a luminescence endpoint read with a 10 second pre-read shake:
!CLEAR DATA
!TAG OFF
!WELLSCANMODE
!XPOS 14.380 9 12
!YPOS 11.235 9 8
!SHAKE ON
!SHAKE 10 0 0 0 0
!STRIP 1 12
!READTYPE LUM
!EMWAVELENGTH 0
!FPW 6
!TOPREADCLEAR OFF
!AUTOPMT ON
!CSPEED 8
!PMTCAL ON
!MODE ENDPOINT
!ORDER COLUMN
!READSTAGE TOP
!READ
Time-resolved fluorescence#
SoftMax Pro used this sequence for a TRF endpoint read:
!TAG OFF
!WELLSCANMODE
!XPOS 14.380 9 12
!YPOS 11.235 9 8
!SHAKE ON
!SHAKE 10 0 0 0 0
!STRIP 1 12
!READTYPE TIME 50 850
!EMWAVELENGTH 525
!AUTOFILTER OFF
!EMFILTER 7
!EXWAVELENGTH 485
!FPW 6
!TOPREADCLEAR ON
!AUTOPMT ON
!CSPEED 8
!PMTCAL ON
!MODE ENDPOINT
!ORDER COLUMN
!READSTAGE BOT
!READ
For a TRF kinetic assay, SoftMax Pro used:
!SHAKE ON
!SHAKE 5 30 27 3 0
!READTYPE TIME 50 850
!AUTOPMT OFF
!PMT MED
!MODE KINETIC 30 21
!READSTAGE TOP
!READ
During the kinetic run, SoftMax Pro repeatedly polled !QUEUE and called !TRANSFER.
Fluorescence spectra#
For an emission spectrum with fixed excitation, SoftMax Pro sent:
!EXWAVELENGTH 350
!AUTOFILTER OFF
!EMFILTER 1
!MODE EMSPECTRUM 400 10 36
!ORDER WAVELENGTH
!READSTAGE BOT
!READ
For an excitation spectrum with fixed emission, SoftMax Pro sent:
!EMWAVELENGTH 600
!AUTOFILTER OFF
!EMFILTER 1
!AUTOFILTER EX OFF
!MODE EXSPECTRUM 350 20 4
!ORDER WAVELENGTH
!READSTAGE BOT
!READ
Partial-region selection#
SoftMax Pro changed the selected wells by changing plate geometry and strip selection. One captured partial-region read used:
!XPOS 14.380 9 12
!YPOS 20.235 9 6
!STRIP 2 6
The Gemini EM backend maps rectangular contiguous well subsets to this command model. The selected
rows change the !YPOS origin and row count, while the selected columns change !STRIP. For
example, wells B2:G7 on a 96-well plate map to row offset 1, six rows, strip 2, and six columns.
Arbitrary non-rectangular well selections are not supported. Wellscan uses explicit scan geometry
and does not yet infer scan geometry from wells.
Wellscan patterns#
SoftMax Pro enabled wellscan with:
!WELLSCANMODE
!WELLSCANMODE ON
It then performed one read per scan point, changing !XPOS and !YPOS between reads. The
captured 3-point and 9-point scans used this coordinate model:
center X = 14.380
center Y = 20.235
offset = 1.133 mm
X positions = 13.247, 14.380, 15.513
Y positions = 19.102, 20.235, 21.368
The horizontal pattern order was:
13.247, 20.235
14.380, 20.235
15.513, 20.235
The vertical pattern order was:
14.380, 19.102
14.380, 20.235
14.380, 21.368
The cross pattern order was:
14.380, 19.102
13.247, 20.235
14.380, 20.235
15.513, 20.235
14.380, 21.368
The fill pattern was a 3 by 3 row-major raster:
13.247, 19.102
14.380, 19.102
15.513, 19.102
13.247, 20.235
14.380, 20.235
15.513, 20.235
13.247, 21.368
14.380, 21.368
15.513, 21.368
After the first scan point, SoftMax Pro did not resend the full optical setup. It resent position,
shake state, !PMTCAL OFF, !STRIP, and then !READ.
Development notes#
The backend intentionally lives in its own module,
pylabrobot/plate_reading/molecular_devices/spectramax_gemini_em_backend.py. It uses the shared
Molecular Devices serial protocol implementation where possible, while keeping Gemini-specific
behavior isolated from the SpectraMax M5 and SpectraMax 384 Plus backends.
During protocol discovery, a serial bridge/logger can be placed between SoftMax Pro and the reader:
SoftMax Pro -> virtual COM port -> Python bridge/logger -> physical serial port -> Gemini EM
This allows OEM traffic to be captured without changing the production backend.