Skip to content

Vspin driver improvements#1141

Draft
TheRealDarkLuke wants to merge 20 commits into
PyLabRobot:mainfrom
TheRealDarkLuke:vspin-driver-improvements
Draft

Vspin driver improvements#1141
TheRealDarkLuke wants to merge 20 commits into
PyLabRobot:mainfrom
TheRealDarkLuke:vspin-driver-improvements

Conversation

@TheRealDarkLuke

Copy link
Copy Markdown

Add a driver for the old V11 Vspin

TheBirdAttack and others added 16 commits June 27, 2026 08:54
Keep the existing Agilent VSpin backend as the default path for newer Agilent-branded centrifuges.

Add a separate Velocity11 legacy backend for the trace-derived V11 communication sequence and expose create_vspin_backend(..., variant='agilent'|'v11') for explicit selection.

Note: hardware validation for the legacy V11 path is still pending; these changes are untested on real Velocity11 hardware.
Add a pyserial-backed FTDI path for Windows COM-port access, harden the V11 startup and runtime attach flow, and make homing wait for real controller activity before accepting idle status.

Validated with the local VSpin hardware workflow at 300 g for 10 seconds.
Reject all-zero runtime attach status packets and fall back to full 14-byte status polling when short idle status lacks position/home data during homing.

Validated with the local VSpin hardware workflow at 300 g for 10 seconds.
Do not accept a zero home position after homing, refresh bucket targets when the home reference is unknown, and require measured RPM before considering a spin cycle started.

Validated with the local VSpin hardware workflow at 300 g for 10 seconds.
@TheRealDarkLuke TheRealDarkLuke marked this pull request as draft July 1, 2026 15:06
Removed 'pyserial' from the 'ftdi' optional dependency list.
@TheRealDarkLuke TheRealDarkLuke marked this pull request as ready for review July 1, 2026 15:24
@rickwierenga

Copy link
Copy Markdown
Member

thanks for the PR!

interestingly, we have vspins with the Agilent and v11 label and both of them work through the existing backend. I wonder if this is not an agilent/v11 difference but rather a firmware version difference which would correlate with agilent/v11 (because Agilent acquired them of course)

the structure of the code looks very similar between the two. Rather than having a completely new backend, why dont we have one backend with a structure like this?

def something():
  if self._model == "agilent":
    cmd = "A"
  elif self._model == "v11":
    cmd = "V"
  else:
    raise
  self.send(cmd)

this way we have less duplication and the "Agilent" version can benefit from your improvements as well. It might also give us some more clarity on what exactly changed between versions and if there's an even smaller abstraction

@TheRealDarkLuke

Copy link
Copy Markdown
Author

Good catch! I didn't think of that, but it makes perfect sense.

Fair point about the duplicates—I'll fix that right away.

@TheRealDarkLuke TheRealDarkLuke marked this pull request as draft July 1, 2026 22:03

@rickwierenga rickwierenga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the refactor!

looks like we actually did have this "old" firmware at some point, and then I changed it to this new format to make it work on some other vspins and since it worked on others must have assumed this new one was more universal. That was clearly a mistake, we have too many vspins 😅

still the question stands what is the difference between them? why do you think it's Agilent vs v11?

Comment on lines +7 to +19
class _FakeIO:
def __init__(self, read_chunks):
self.read_chunks = list(read_chunks)
self.writes = []

async def read(self, num_bytes: int) -> bytes:
if self.read_chunks:
return self.read_chunks.pop(0)
return b""

async def write(self, data: bytes) -> int:
self.writes.append(data)
return len(data)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use unit test mock for this?

Comment on lines +355 to +357
def _command(self, name: str) -> bytes:
return _VSPIN_COMMANDS[self._command_set][name]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this name is a bit confusing. maybe _get_command_bytes or sth?

Comment on lines +90 to +100
def test_find_status_packet_scans_noise_and_validates_checksum(self):
packet = _status_packet()

parsed = VSpinBackend._find_status_packet(b"\x00\xff" + packet + b"\x00")

assert parsed is not None
self.assertEqual(parsed.status, 0x11)
self.assertEqual(parsed.current_position, 12070)
self.assertEqual(parsed.tachometer, -10)
self.assertEqual(parsed.home_position, 6733)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this depends on the default parameter in _status_packet. ideally we would just test parsing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants