Automate liquid and solid powder dispensing into microplates using CNC machines.
A Python package for precise control of CNC machines designed for laboratory automation and high-throughput dispensing workflows.
- Precise G-code control of CNC machines via serial communication
- Automatic serial port detection across all platforms
- Pre-configured for Genmitsu 3018-PROVer V2 and 4040 PRO
- Built-in simulator for testing movements before execution
- Cross-platform: Windows, Linux, Raspberry Pi, and macOS
- Safety boundaries to prevent collisions
- YAML-based configuration for easy machine setup
- Motorized plate loader with collision avoidance for different plate types
- Motorized Plate Loader - Automated well plate loading with synchronized servo lift and lid control
- Solid Powder Doser - Precise solid material dispensing with servo gate and DC motor auger
- Power-Safe Operation - Sequential control designed for 5V 5A single-supply operation
- Waveshare PCA9685 HAT - I2C servo control with relay-based motor switching
git clone https://github.com/yourusername/dose_every_well.git
cd dose_every_well
pip install -e .For Raspberry Pi hardware support (Plate Loader, Solid Doser):
# Install with Raspberry Pi dependencies
pip install -e ".[rpi]"
# For Raspberry Pi 5, also install rpi-lgpio (RPi.GPIO compatibility layer)
pip install rpi-lgpiofrom dose_every_well import CNC_Controller, load_config, find_port
# Load configuration and connect
config = load_config("cnc_settings.yaml", "Genmitsu 4040 PRO")
controller = CNC_Controller(find_port(), config)
# Read position
coords = controller.read_coordinates()
print(f"Position: X={coords['X']}, Y={coords['Y']}, Z={coords['Z']}")
# Move to position
controller.move_to_point(10, 20) # X=10mm, Y=20mm
controller.execute_movement()# CNC control demos
python demo/simple_connect_demo.py
python demo/axis_movement_demo.py
# Raspberry Pi hardware demos (requires hardware)
python demo/plate_loader_demo.py
python demo/solid_doser_demo.py| Platform | Status | Notes |
|---|---|---|
| Windows 7/10/11 | Supported | Works out of box |
| Linux (Ubuntu/Debian) | Supported | Requires dialout group |
| Raspberry Pi 3/4/5 | Supported | Pi 5 requires rpi-lgpio |
| macOS (Intel/M1/M2) | Supported | Full support |
- Installation Guide - Platform-specific setup instructions
- Quick Start Guide - Get running in 5 minutes
- API Reference - Complete API documentation
- Troubleshooting - Solutions to common issues
- Wiring Guide - Complete hardware setup with diagrams
- Plate Loader Guide - Motorized plate loader documentation
- Solid Doser Guide - Solid powder dosing documentation
- Servo Power Guide - Power management and optimization
- Genmitsu 4040 PRO (400×400×75mm work area)
- Genmitsu 3018-PROVer V2 (300×180×45mm work area)
- Any GRBL-compatible CNC machine (requires custom configuration)
Complete system for automated solid/liquid dispensing:
| Component | Purpose | Channels |
|---|---|---|
| Waveshare PCA9685 HAT | I2C servo driver (0x40) | 16 channels |
| 4× Servo Motors | Gate, lifts, lid control | Ch 0, 3, 6, 9 |
| 5V Relay Module | DC motor ON/OFF | GPIO 17 |
| DC Motor | Auger/screw feeder | Via relay |
| 5V 5A Power Supply | Single plug powers all | USB-C + distribution |
See Wiring Guide for complete setup instructions
Motorized plate loader with automatic collision avoidance for safe operation with different plate types:
from dose_every_well import PlateLoader
# Specify plate type for automatic safety settings
loader = PlateLoader(plate_type='shallow_plate') # 96-well plates
loader = PlateLoader(plate_type='deep_well') # Deep-well plates
# Operate safely with collision avoidance
loader.open_lid()
loader.raise_plate()
loader.close_lid() # Auto-blocked if plate position would cause collision
# Switch plate types on the fly
loader.set_plate_type('custom_384_well')
loader.reload_config() # Reload settings from plate_settings.yamlFeatures:
- Collision Avoidance - Prevents lid-plate crashes based on plate type
- YAML Configuration - Customize plate types in
plate_settings.yaml - Hot-Reload - Update settings without restarting
- Multiple Plate Types - Pre-configured for shallow, deep-well, and custom plates
Requires Raspberry Pi with PCA9685 PWM HAT and servos. See plate_settings.yaml for configuration.
Verify your setup works correctly:
python test_platform.pydose_every_well/
├── src/dose_every_well/ # Main package
│ ├── cnc_controller.py # Core CNC controller
│ ├── plate_loader.py # Raspberry Pi plate loader (3 servos)
│ ├── solid_doser.py # Raspberry Pi solid doser (servo + motor)
│ ├── cnc_settings.yaml # CNC machine configs
│ ├── plate_settings.yaml # Plate loader configs
│ └── __init__.py
├── demo/ # Example scripts
│ ├── simple_connect_demo.py
│ ├── axis_movement_demo.py
│ ├── plate_loader_demo.py
│ └── solid_doser_demo.py
├── docs/ # Documentation
│ ├── wiring_guide.md # Hardware setup (START HERE!)
│ ├── solid_doser.md
│ ├── plate_loader.md
│ ├── installation.md
│ └── troubleshooting.md
├── test_platform.py # Platform compatibility test
└── README.md # This file
Important Safety Practices:
- Test in simulation before hardware execution
- Verify movement boundaries match your machine
- Keep emergency stop accessible during operation
- Clear work area before automated runs
- Monitor Z-axis movements to prevent crashes
from dose_every_well import CNC_Controller, load_config, find_port
# Setup
config = load_config("cnc_settings.yaml", "Genmitsu 4040 PRO")
controller = CNC_Controller(find_port(), config)
# 96-well plate parameters
well_spacing = 9.0 # mm
start_x, start_y = 10.0, 10.0
dispense_height = 5.0
# Visit each well
for row in range(8): # A-H
for col in range(12): # 1-12
x = start_x + col * well_spacing
y = start_y + row * well_spacing
controller.move_to_point(x, y)
controller.move_to_height(dispense_height)
controller.execute_movement()
print(f"Dispensing to well {chr(65+row)}{col+1}")from dose_every_well import CNC_Controller, PlateLoader, SolidDoser, load_config, find_port
import time
# Initialize all controllers
cnc = CNC_Controller(find_port(), load_config("cnc_settings.yaml", "Genmitsu 4040 PRO"))
plate_loader = PlateLoader(i2c_address=0x40)
solid_doser = SolidDoser(i2c_address=0x40, motor_gpio_pin=17)
try:
# 1. Load plate onto balance
print("Loading plate...")
plate_loader.load_sequence()
# 2. Dispense solid into each well
well_spacing = 9.0 # mm
start_x, start_y = 10.0, 10.0
for row in range(8):
for col in range(12):
well = f"{chr(65+row)}{col+1}"
print(f"Dispensing to well {well}...")
# Move CNC to well position
x = start_x + col * well_spacing
y = start_y + row * well_spacing
cnc.move_to_point(x, y)
cnc.move_to_height(5.0)
cnc.execute_movement()
# Dispense solid material (motor + gate servo)
solid_doser.dispense(duration=2.0)
# Move up
cnc.move_to_height(20.0)
cnc.execute_movement()
# 3. Unload plate
print("Unloading plate...")
plate_loader.unload_sequence()
finally:
solid_doser.shutdown()
plate_loader.shutdown()
cnc.disconnect()from dose_every_well import SolidDoser
doser = SolidDoser(i2c_address=0x40, motor_gpio_pin=17)
try:
# Simple dispense for 5 seconds
doser.dispense(duration=5.0)
# Precise low-flow dispense (30° gate opening)
doser.dispense(duration=3.0, gate_angle=30)
finally:
doser.shutdown()Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Run
python test_platform.pyto verify - Submit a pull request
For major changes, open an issue first to discuss.
This project is licensed under the MIT License - see the LICENSE file for details.
Yang Cao
- Email: [email protected]
If you use this package in your research, please cite:
@software{dose_every_well,
author = {Cao, Yang},
title = {Dose Every Well: CNC-based Automated Dispensing},
year = {2025},
url = {https://github.com/AccelerationConsortium/dose_every_well}
}Built for laboratory automation workflows with a focus on microplate dispensing applications. Uses GRBL firmware for CNC control.
Need help? Check the documentation or open an issue on GitHub.