HighRes Biosolutions MicroSpin#

The MicroSpin is a sealed automated microplate centrifuge from HighRes Biosolutions. It has two swing-out buckets (1 SBS plate per bucket), spins up to 3000 ×g (4729 RPM), and is driven over TCP/IP – there is no separate USB driver or FTDI library needed.

It is controlled by the MicroSpinBackend class and the MicroSpin() factory.

Reference: HighRes Biosolutions, MicroSpin User Manual (document 1058675).

1. Network configuration#

The MicroSpin ships with a static IP address printed on the product label (factory default 192.168.127.60, mask 255.255.255.0). To talk to it you need to put your computer on the same subnet, or change the device’s IP to one that fits your network.

There are two ways to find/change the IP:

  1. Direct-attached (Ethernet point-to-point). Connect the MicroSpin to a wired NIC on your computer (e.g. via a USB-to-Ethernet adapter). Configure that NIC with a static address such as 192.168.127.10 / 255.255.255.0. Then open http://<microspin-ip>/network.html in a browser, and either:

    • switch the device to DHCP (recommended for lab networks with a DHCP server), or

    • enter a static IP in your lab’s subnet plus the appropriate gateway.

    The same page also exposes a Server Port setting (factory default 1000); change it here if 1000 conflicts with something else on your network, and remember to pass the matching port=... when constructing MicroSpin(...) in Python.

    After saving, wait ~30 seconds, then reboot the MicroSpin (back-panel power switch).

  2. Plug into the lab network directly. If the device is already DHCP-enabled, find its lease on your DHCP server using the MAC address printed on the product label (the OUI is 00:d0:69, assigned to Technologic Systems, who make the embedded controller inside the MicroSpin).

Once you know the IP, you can verify connectivity with a simple ping before going any further.

2. Connecting from PLR#

The MicroSpin() factory returns a fully configured Centrifuge. Unlike the VSpin + Access2 combo, no separate Loader is needed: an integration arm (or a human) places plates directly into the presented bucket.

from pylabrobot.centrifuge import MicroSpin

# Default: port 1000 (the MicroSpin's factory default).
cf = MicroSpin(name="microspin", host="192.168.127.60")

# If your device has been reconfigured (Server Port setting on /network.html),
# pass the matching port explicitly:
# cf = MicroSpin(name="microspin", host="192.168.127.60", port=2300)

await cf.setup()  # opens a TCP connection to the configured port

3. Homing#

The MicroSpin needs to be homed once after every power-cycle before bucket or spin commands are accepted. The backend exposes home() and is_homed() for this:

if not await cf.backend.is_homed():
    await cf.backend.home()

4. Reading status & version#

Two read-only helpers are convenient for diagnostics:

await cf.backend.request_version()
# {'Product Name': 'RandomServe', 'Serial Number': 'HRB-...',
#  'Version': 'MS-1.3.3', 'Firmware Build': '...'}
await cf.backend.request_status()
# {'Spindle Position': '1958', 'Door Position': '-457'}

5. Positioning buckets#

The MicroSpin’s open <bucket> firmware command both opens the door and rotates the chosen bucket into the load position; PLR exposes this as go_to_bucket1() / go_to_bucket2(). There is no separate “open the door without a bucket” workflow.

Door closing is handled automatically by the firmware at the start of spin and home, so application code never has to issue a close. The pneumatic door lock and the nest-pin that holds plates during loading are likewise managed by the firmware as part of open <bucket> and spin – none of these primitives (od, cd, lockdoor, unlockdoor, locknest, unlocknest) are exposed as standalone calls on the MicroSpin backend; the manual classifies them all as maintenance commands (§6.7).

# Open the door and present bucket 1 in one step
await cf.go_to_bucket1()

# Or bucket 2
# await cf.go_to_bucket2()

# The next spin or home will close the door automatically; no explicit
# close call is needed (and is in fact disabled on this backend).

6. Spinning#

Warning

Pre-spin checklist (manual §§5.3, 7, 8):

  • The device must be homed (see step 3).

  • The shipping tie-wraps that hold the buckets to the rotor MUST be removed – starting a spin with them in place will damage the buckets.

  • Both buckets must be properly seated on the rotor pins, swing freely, and the bucket pivot pins must not protrude.

  • The payload must be balanced (max 15 g imbalance at full speed, up to 75 g at lower speeds).

  • The chamber must be free of debris.

  • Compressed air must be supplied at 70-135 psi.

(The door is closed automatically by the firmware at the start of spin / home, so it does not need to be closed by the caller.)

The MicroSpin does not have biosafety seals – do not centrifuge hazardous, flammable, or corrosive materials.

PLR’s spin() takes:

  • g: G-force in ×g (valid range 1-3000)

  • duration: time at speed in seconds (≥ 1)

  • acceleration: ramp-up rate as a fraction (0, 1]

  • deceleration: ramp-down rate as a fraction (0, 1]

The backend converts the 0-1 fractions to the integer 0-100 percentages the firmware expects.

await cf.spin(
    g=500,
    duration=60,        # seconds
    acceleration=1.0,   # 0-1 fraction of max
    deceleration=1.0,   # 0-1 fraction of max
)

Warning

Slow / stuck deceleration: Low deceleration values cause noticeably long spin-downs, and very low ones can hang the firmware entirely. The backend emits a UserWarning at two tiers:

  • deceleration < 0.40 (40 %) – “long spin-down expected”. A tested spin 1000 100 20 10 (decel = 0.20) took ~7 minutes from full speed to stopped. Make sure your wait_for_spindle_stopped budget covers this.

  • deceleration < 0.20 (20 %) – “possible firmware hang”. A tested spin 1000 100 10 10 (decel = 0.10) ran for >30 minutes without ever reporting spin-down. Recovery is the same as for the low-G hang below.

Warning

Low-G hang: Spinning at less than ~30 ×g is known to occasionally hang the firmware. The spindle “stopped” sensor sometimes fails to latch when the rotor decelerates from a very low speed, so no OK! completion line is emitted and every subsequent command times out. The backend will emit a UserWarning if you call spin() with g < 30. If you hit the hang, the recovery path is:

await cf.backend.abort()
await cf.backend.clear_button_abort()

If the device still won’t respond, power-cycle it from the back-panel switch.


7. Recovering from a stuck or aborted state#

abort() requests a decel + stop. After an abort the firmware enters a latched abort state that must be cleared before any further motion commands will run.

A subtlety: abort and clearbuttonabort both return OK! as soon as the firmware accepts the request – they do not wait for the rotor to actually spin down. The real “we are stopped” gate is the status command: while motion is in progress, the firmware queues status behind it and only answers once motion has completed. The MicroSpin backend uses this property in reset(), which is therefore the canonical “really, fully back to idle” call:

# Sends abort, then clearbuttonabort, then status (which blocks until
# the spindle has actually stopped). Returns the final status report.
final_status = await cf.backend.reset()
print(final_status)
# {'Spindle Position': '...', 'Door Position': '...'}

reset() swallows errors from the abort step by default (it’s common for the firmware to reject abort if there’s nothing to abort), so it’s safe as a generic “get me back to a known state” routine. The arguments let you tune each phase:

  • swallow_abort_errors=False – propagate any error from the abort step instead of ignoring it

  • wait_for_settle=False – skip the final status gate (just clear the abort latch, don’t wait for the rotor)

  • abort_timeout=... / settle_timeout=... – override the per-step timeouts

If you only want the “wait for actual stop” gate without changing any state, use wait_for_spindle_stopped() – it sends a single status with a generous timeout and returns the parsed result:

# Block until the firmware confirms the rotor is fully stopped.
status = await cf.backend.wait_for_spindle_stopped()

You can of course still call the two primitives directly if you need finer control. Remember that without the status step, you do NOT have confirmation that the rotor has actually spun down – just that the abort latch was set/cleared:

await cf.backend.abort()              # request OK; rotor may still be spinning
await cf.backend.clear_button_abort() # latch cleared; rotor may still be spinning
status = await cf.backend.request_status() # NOW we know we're really stopped

8. Error stack#

The MicroSpin maintains an internal error stack. request_errors(n) returns the top n lines (default 10):

await cf.backend.request_errors(10)

9. Sending raw commands#

For advanced use, send_command() exposes the underlying ASCII protocol. It returns the data lines emitted between ACK! and OK!, and raises MicroSpinError on ERROR!. See manual §6.6 for the protocol; use list and info to enumerate commands the device supports:

await cf.backend.send_command("list")     # public commands
# await cf.backend.send_command("list all") # includes maintenance commands

10. Developing without a real device#

Bringing a 43 kg centrifuge into your apartment for testing is suboptimal. PLR ships an in-process mock TCP server that speaks the MicroSpin command protocol faithfully enough for the MicroSpinBackend to drive it end-to-end. Use it from Python:

import asyncio
from pylabrobot.centrifuge import MicroSpin
from pylabrobot.centrifuge.highres.mock_server import MicroSpinMockServer

async def demo():
    async with MicroSpinMockServer() as srv:
        cf = MicroSpin(name="mock-microspin", host=srv.host, port=srv.port)
        await cf.setup()
        await cf.backend.home()
        print(await cf.backend.request_version())
        # The mock implements `status`-blocks-during-motion, so reset(), abort
        # recovery, and the low-G hang are all reproducible in tests.
        await cf.stop()

# await demo()  # uncomment to run from a notebook

Or hand-drive it with nc / telnet after starting it as a script – handy when you want to poke at the wire protocol directly:

python -m pylabrobot.centrifuge.highres.mock_server --port 1000
# then, in another terminal:
nc 127.0.0.1 1000

A few capabilities worth knowing about for tests:

  • srv.motion_dwell["home"] = 0.5 – slow down motions to race against them.

  • srv.state.simulate_low_g_hang = True – reproduce the firmware bug where status never returns after a spin (see §6 Spinning).

  • Inspect srv.state.homed, srv.state.at_bucket, etc. directly from your test code.

11. Cleaning up#

await cf.stop()  # closes the TCP connection