Thorlabs Cube Drivers
Python device drivers for quantum optics motor controllers with network accessibility
2024 • python, quantum-computing, hardware-drivers, networking
The Problem
Thorlabs motor controllers are ubiquitous in quantum optics labs. They control beam shutters, mirror mounts, rotation stages, and other precision optical components. However, they come with a significant limitation: Thorlabs' proprietary Kinesis software.
Kinesis works fine for manual control, but it fails in three critical areas:
- No programmatic control — You can't script complex sequences or integrate with experiment control systems
- No network accessibility — Controllers are "network-deaf," requiring direct USB connection to a Windows PC
- No integration with ARTIQ — The de facto standard for quantum experiment control has no native Thorlabs support
For a quantum computing lab running dozens of Thorlabs devices across multiple optical tables, this is a serious operational bottleneck.
The Solution
I developed a Python driver package that:
- Communicates directly with Thorlabs T-Cube and K-Cube controllers via their APT protocol
- Exposes devices as Network Device Support Package (NDSP) servers
- Integrates seamlessly with the ARTIQ experiment control framework
This transforms isolated USB devices into networked instruments that can be controlled from anywhere in the lab.
Technical Architecture
The APT Protocol
Thorlabs controllers use the APT (Advanced Positioning Technology) protocol over USB serial. The protocol is documented but not trivial—it uses a binary message format with headers, destinations, and payload structures.
Message structure:
| Header (6 bytes) | Data (variable) |
| msg_id | param1 | param2 | dest | source | data... |Key challenges:
- Endianness: Mixed little-endian and big-endian fields
- Device identification: Multiple device types share the same USB VID/PID
- State management: Some commands require specific controller states
Example: Opening a beam shutter
# Message breakdown for MGMSG_MOT_MOVE_JOG
msg_id = 0x046A # Jog command
chan_ident = 0x01 # Channel 1
direction = 0x01 # Forward
dest = 0x50 # Generic USB destination
source = 0x01 # Host controllerThe driver abstracts this complexity into a clean Python API:
shutter = SH05R("/dev/ttyUSB0")
shutter.open()
shutter.close()Driver Architecture
I designed the driver package with separation of concerns:
thorlabs_cube/
├── protocol/
│ ├── apt.py # Low-level APT message encoding/decoding
│ └── serial.py # Serial port management
├── devices/
│ ├── base.py # Abstract device class
│ ├── ksc101.py # K-Cube solenoid controller
│ ├── kdc101.py # K-Cube DC servo controller
│ └── tdc001.py # T-Cube DC servo controller
├── ndsp/
│ ├── server.py # NDSP server implementation
│ └── client.py # NDSP client for remote access
└── utils/
└── discovery.py # USB device enumerationDesign decisions:
| Decision | Rationale |
|---|---|
| Abstract base class for devices | Common interface, easy to add new device types |
| Separate protocol layer | Reusable across device types, testable in isolation |
| Async I/O for NDSP | Non-blocking server, handles multiple clients |
| Type hints throughout | Better IDE support, catches errors early |
Network Device Support Package (NDSP)
Why NDSP?
ARTIQ uses a client-server model for device control. The experiment controller (master) communicates with device servers (NDSPs) over the network. Each NDSP exposes a device's functionality via RPC.
Before NDSP:
[Experiment PC] --USB--> [Thorlabs Controller]Problem: Experiment PC must be physically near the controllerAfter NDSP:
[Experiment PC] --Network--> [NDSP Server] --USB--> [Thorlabs Controller]Solution: Controller can be anywhere on the networkImplementation
The NDSP server exposes device methods as RPC endpoints:
class SH05RNDSP:
def __init__(self, device_path):
self.device = SH05R(device_path)def open_shutter(self):
"""RPC method: Opens the beam shutter."""
self.device.open()
return Truedef close_shutter(self):
"""RPC method: Closes the beam shutter."""
self.device.close()
return Truedef get_state(self):
"""RPC method: Returns current shutter state."""
return self.device.stateARTIQ integration:
# In device_db.py
device_db = {
"shutter": {
"type": "controller",
"host": "192.168.1.100",
"port": 3251,
"command": "aqctl_thorlabs_sh05r -p /dev/ttyUSB0"
}
}# In experiment
class MyExperiment(EnvExperiment):
def build(self):
self.setattr_device("shutter")def run(self):
self.shutter.open_shutter()
delay(100*ms)
self.shutter.close_shutter()Challenges and Solutions
Challenge 1: USB Device Discovery
Problem: Multiple Thorlabs controllers have the same USB vendor/product ID. When you plug in three K-Cube controllers, how do you know which is which?
Solution: Query the device serial number via APT protocol after connection. I implemented an auto-discovery function that:
- Enumerates all USB serial devices matching Thorlabs VID/PID
- Opens each device and sends an identification request
- Parses the response to get model and serial number
- Returns a mapping of serial numbers to device paths
devices = discover_thorlabs_devices()
# Returns: {"27123456": "/dev/ttyUSB0", "27123457": "/dev/ttyUSB1"}Challenge 2: Timing-Sensitive Commands
Problem: Some motor movements require precise timing. Network latency can introduce jitter.
Solution: Implemented command queuing on the NDSP server side. The server maintains a command queue with timestamps, executing commands at the correct relative times regardless of when they arrived over the network.
For sub-millisecond timing requirements, I added a "burst mode" that downloads a sequence of commands to the NDSP server and executes them locally with hardware timing.
Challenge 3: Error Handling Across Network Boundary
Problem: If the USB device disconnects, how does the remote client know?
Solution: Implemented heartbeat monitoring and automatic reconnection:
- NDSP server pings device every 5 seconds
- If 3 consecutive pings fail, mark device as disconnected
- RPC calls to disconnected devices raise DeviceOfflineError
- Background thread attempts reconnection every 10 seconds
- Clients can subscribe to connection state changes
Testing Strategy
Unit Tests
I used pytest with mock serial devices to test the protocol layer:
def test_apt_message_encoding():
msg = APTMessage(MGMSG_MOT_MOVE_JOG, chan=1, direction=1)
encoded = msg.encode()
assert encoded == bytes([0x6A, 0x04, 0x01, 0x01, 0x50, 0x01])Integration Tests
Hardware-in-the-loop tests with actual Thorlabs controllers:
@pytest.mark.hardware
def test_shutter_open_close():
shutter = SH05R("/dev/ttyUSB0")
shutter.open()
assert shutter.state == "open"
shutter.close()
assert shutter.state == "closed"Network Tests
End-to-end tests with NDSP server and client:
def test_ndsp_rpc():
# Start server in subprocess
server = start_ndsp_server("SH05R", "/dev/ttyUSB0", port=3251)# Connect client
client = NDSPClient("localhost", 3251)
client.open_shutter()
assert client.get_state() == "open"server.terminate()Results
Packages Released
I released 4 driver packages for the lab:
| Device | Description | Status |
|---|---|---|
| KSC101 | K-Cube solenoid controller (beam shutters) | Production |
| KDC101 | K-Cube DC servo (rotation stages) | Production |
| TDC001 | T-Cube DC servo (translation stages) | Production |
| KBD101 | K-Cube brushless DC (mirror mounts) | Beta |
Impact
- Eliminated Windows dependency — Controllers now accessible from Linux experiment PCs
- Enabled remote operation — Operators can control shutters from the control room
- Reduced cabling — Single Ethernet drop per optical table instead of multiple USB runs
- Integrated with ARTIQ — Beam shutters now synchronized with ion trap pulse sequences
Cost Savings
The lab was considering purchasing network-enabled motor controllers at ~$500 premium per unit. With NDSP:
Existing controllers: 12 units
Premium avoided: 12 × $500 = $6,000
Raspberry Pi hosts: 3 × $50 = $150
Net savings: $5,850Lessons Learned
What Worked
- Starting with the protocol — Understanding APT deeply made device implementation straightforward
- NDSP abstraction — Network transparency is powerful; clients don't care where the device is
- Comprehensive logging — Debugging hardware issues is much easier with detailed logs
What I'd Improve
- Add async support throughout — Currently mixing sync serial with async NDSP
- Better documentation — Would add interactive examples in Jupyter notebooks
- Configuration files — Currently hardcoding device paths; should use YAML/TOML config
Technical Specifications
| Component | Details |
|---|---|
| Language | Python 3.9+ |
| Dependencies | pyserial, sipyco (ARTIQ), asyncio |
| Supported devices | KSC101, KDC101, TDC001, KBD101 |
| Protocol | Thorlabs APT over USB serial |
| Network | TCP/IP, ARTIQ NDSP RPC |
| Latency | <5ms local, <20ms network |
| Platforms | Linux, macOS, Windows |
Conclusion
This project solved a real pain point in the quantum optics lab: making "dumb" USB devices network-accessible and programmable. By understanding the underlying APT protocol and leveraging ARTIQ's NDSP framework, I was able to transform isolated controllers into networked instruments.
The key insight was that the protocol layer is separate from the network layer. Once I had reliable APT communication, exposing it via NDSP was straightforward. This architecture is extensible—adding support for new Thorlabs devices requires only implementing the device-specific APT commands.
More broadly, this project reinforced the value of understanding systems deeply. The Thorlabs protocol documentation is sparse, but by reverse-engineering Kinesis traffic and reading the APT specification carefully, I was able to build something more flexible than the official software.