Week 1: Project Description

The machine is built to automate the central dogma of molecular biology, the idea that DNA encodes the instructions to assemble proteins and other higher order structures. To accomplish this, I am building a iquid handling robot.

Week 3: System Preliminary Design

System Preliminary Design

To automate the central dogma of biology, we will need to translate our DNA into proteins, there are many ways of accomplishing this but to keep things simple we will use a fairly standard approach of cell based protein expression.

The steps are as follows:
a) insert vector into plasmid (outside the scope of this work)
b) introduce plasmid into cells using a technique like electroporation
c) incubate cells for an extended duration, often overnight
d) lyse the cells to yield functional proteins

gantry

The machine will be responsible for automating some of these steps, what's unique about this machine is that it interacts with micro-arrays of proteins or DNA. Each micro-array can accomodate in excess of 384 reactions in a 25mmx25mm area while typical well plates would require 127mmx87mm. As a consequence, I could parallelize up to 10k simultaneous reactions in my bench-top tool, the same number of reactions that would be capable of a well equipped biology lab.

Week 5: Systems Engineering Design

redacted

Week 7: Gantry Accesories, 1

Big week here, I switched out the previous H2W STS-0620 6lb Single axis linear stepper motor for its air bearing sibling, the H2W STS-1220, with 10lb of force! This took a TON of work as I had to run air lines through the shop, secure a valve manifold, and take care of all plumbing.


A little waterjet here, a little waterjet there...




Spent some time waterjetting a new faceplate for the larger air bearing motor (look at countersinks!!)


Then I got started on the code, what's appealing about the linear stepper motor is that it can be driven essentially like a normal stepper motor. When I won this auction on eBay it included a linear encoder, precise to 1µm so I decided to try out the Duet 1HCL's built in closed loop control method. To implement this I had to turn on the follow instructions in the configuration:.

                
    ; Duet 3 MB6XD
    M569.1 P1.0 T2 C12728 R20 I0 D0.2  ;; physical drive 1.0 goes forwards, X 12728 counts per step
    M569 P1.0 D5 S1                    ;; configure the motor on the 1HCL as being in closed-loop drive mode (D4) and not reversed (S1)
    M917 X50                           ;; set the closed loop axis holding current         
            

Subsequently, I was able to use the tuning interface to loop through P, then D, then I values in that order. Eventually converging on PID values of 20, 0, 0.2. The performance difference was imperceptible however, I wonder if this would make a bigger difference if I had a heavier load. Overall, I'm not very impressed, as this forcer should be precise to less than 5µm, especially with a closed loop controller.

Week 9: Gantry Accesories, 2

The aluminum parts arrived, the uprights were installed and worked great, but the supplier that provided the ball screw nut holders provided parts that came in WAY out of specification.


In the meantime, I built a cheeky cable guide, and a base plate for testing (more waterjetting!)



Looks nice, is useless. At least the cable guide is lovely (pictured on the left).


Week 10: Solenoid Driver + Comms

I went back to the plastic parts (just for now, don't worry) and reassembled the machine.

Additionally, I got started on the solenoid driver. To save a bit of time, and to further understand the machine control requirements, I elected to move forward with the modular things low mosfet board.


I found that the Duet3D mainboard was able to generate waveforms with a minimum resolution of 1ms, but the solenoid needed a 350µs pulse, so I needed to add a bit of logic to the receiving board.

To do this a pulse was generated by the mainboard, with a custom Macro. When I call M711, the following code is run, where S is the parameter defining volume.

; Duet 3 MB6XD

My initial implementation relied on signal interupts on the RP2040 to detect the rising and falling edge of the waveform, but this was rife with issues as there was a significant amount of noise propagating through the control wiring. After beating my head against the wall, redoing the wiring with a shielded ethernet cable (thanks Cedric!), and generally going to extreme yet fruitless measures like sorting pulse widths in 10µs incremements, I rewrote the code to read the signal pin as fast as possible, and to assign all read values to a buffer. Once at least half the values of the buffer had changed from the previous state, a flag would be set to indicate that the state was either HIGH or LOW. This state change signaled a measurement to take place using the ticks_us() library.

 
                    
from machine import Pin, PWM, Timer
from time import sleep, sleep_us, ticks_ms, ticks_us, ticks_diff

# constants
spike_duration = 0.23  # spike duration, ms
duty_percent = 12.5  # percentage of duty cycle

# calculations for timing
sd_math = int(spike_duration * 1000)

# establish pins
led_pin = Pin(27, Pin.OUT)
sol_pin = Pin(29, Pin.OUT)
trg_pin = Pin(28, mode=Pin.IN, pull=Pin.PULL_DOWN)

# duty cycle calculation
hold_duty = round(duty_percent / 100 * (2**16 - 1))

# PWM setup
pwm_led = PWM(led_pin, freq=1_000_000, duty_u16=0)
pwm_sol = PWM(sol_pin, freq=100_000, duty_u16=0)

# buffer for pin states
state = 0
old_state = 0
state_buffer = []
buffer_size = 20  # length of buffer for averaging, REMINDER: this is a time delay
start = 0
count = 0
testDuration = 0
old_testDuration = 0

def update_buffer():
    global state_buffer
    state_buffer.append(trg_pin.value())
    if len(state_buffer) > buffer_size:
        state_buffer.pop(0)

def check_pin_state(old_state):
    global state_buffer
    if len(state_buffer) == buffer_size:
        if state_buffer.count(1) > buffer_size // 2:
            if old_state == 0:
                print("rising")
            return 1  # Majority is high
        elif state_buffer.count(0) > buffer_size // 2:
            if old_state == 1:
                print("falling")
            return 0  # Majority is low
    return old_state #  this is annoying, and necessary.

print("Initiate runtime")

try:
    while True:
        
        update_buffer()
        old_state = state
        state = check_pin_state(old_state)
        if state == 1 and old_state == 0:
            print("start counting")
            start = ticks_us()
        elif state == 1 and old_state == 1:
            pass
        elif state == 0 and old_state == 1:
            print("stop counting")
            count = ticks_diff(ticks_us(), start)
            print(count)
            
            print("spike")
            # spike, 100% duty cycle
            pwm_led.duty_u16(65535)  # set the duty cycle of the led
            pwm_sol.duty_u16(65535)  # set the duty cycle of the solenoid
            sleep_us(sd_math)
            
            #print("hold")
            ## hold, x% duty cycle
            #pwm_led.duty_u16(hold_duty)  # set the duty cycle of the led
            #pwm_sol.duty_u16(hold_duty)  # set the duty cycle of the solenoid
            #sleep_us(int(count/10)-sd_math)

            # low, 0% duty cycle
            pwm_led.duty_u16(0)  # set the duty cycle of the led
            pwm_sol.duty_u16(0)  # set the duty cycle of the solenoid
        else:
            # low, 0% duty cycle
            pwm_led.duty_u16(0)  # set the duty cycle of the led
            pwm_sol.duty_u16(0)  # set the duty cycle of the solenoid


            

except KeyboardInterrupt:
    print("Interrupted by keyboard")

finally:
    print("Purging resources...")
    pwm_led.deinit()
    pwm_sol.deinit()
    led_pin.init(Pin.OUT)
    sol_pin.init(Pin.OUT)
    led_pin.value(0)
    sol_pin.value(0)
    print("Cleanup complete")  
    

Week 11: System Evaluation

In the meantime, I got started on the syringe pump. The syringe pump is used to aspirate reagents, dispense reagents into the valve, and to serve as a quick-change interface to rapidly switch reagents.

After wrapping up the syringe pump, I focused on the solenoid driver. The solenoid driver is used to control the solenoid that is opens and closes the valve. This is somewhat of a departure from the originally intended design of a piezo dispense capillary, but this was intentional as these kinds of valves are an order of magnitude more affordable and substantially easier to control in an array.

To get this to work, I needed to demonstrate that the valve can be opened with a 350µs pulse and subsequently held open without burning the valve out. This is documented further in the solenoid driver page.

Getting the solenoid driver to work was a major win! And I was able to demonstrate the machine as having been able to dispense droplets in a deterministic pattern.

Week 12: Droplets-on-the-fly

This week I focused on increasing dispense speed, notably getting the machine controller, Duet3D, to play nice with my external controller from Modular Things - thank you Jake and Quentin. The key here was to use the Duet3D in laser mode. This was a bit of a pain to get working, but I eventually got it there. The major roadblock was finding a PWM compatible pin that was also capable of being controlled by the M452 establishing command. For whatever reason, the actual laser/vfd pin Duet suggested using was totally non-functional. So I had to use other pins to debug. The pin I settled on was controlled by the opto couplers on the Duet3d 6xd mainboard as the optocooupler allowed me to use a pull-up on the "Therm" pin as built into the Low-MOSFET board. This convenient hack not only allowed me to control the solenoid driver safely, as there was no risk of providing higher voltages that could fry a microcontroller pin from the machine board, but also cut down on noise which was causing significant issues in interpreting PWM.

                        
    ; Duet 3 MB6XD
    M452 C"io8.out" R255 F1000 ; Enable Laser mode, on io8, with max intensity being 255, and a PWM frequency of 1000hz
                        
                    

This command allowed me to dispense droplets on the fly instead of having to pause after each movement to dispense, cutting down dispense time by 2 orders of magnitude for the system. To further reduce this time, I began implementing raster clustering mode.

Pulsed signal with clustering

Pulsed signal with varied clustering

Pulsed signal corrected

This had some limitations, notably that the pause between pulses in a clustered command was different than the pause between individual commands, creating periodicity in the dispense pattern. To solve this, I included extra 0PWM pulses between the 255PWM pulses, I also found that just running commands serially was fast enough for my purposes. Although, if I tried to loop commands, i.e. inside of a "while True" the pulses were significantly slower by 10s of ms! Peculiar, but not a problem, as it is trivial to generate G-code without loops or other cleverness in the age of 3D printing.
Putting all this together, I was able to generate this early dispense pattern. Droplet on the fly dispense will be crucial to making a practical and useful machine. Unfortunately, during this process the tip of the dispense head collided with the edge of the petri dish, despite not being secured down, the petri dish somehow bound to the baseplate and caused the tip to snap off. Although I was able to glue the tip back on, it continued to leak, causing the large droplets seen in the video.

Epilogue

This section serves as a reflection and overview of the entire project, discussing the successes, challenges, and future prospects of the molecular synthesizer machine. It encapsulates the journey from conception to realization and the lessons learned throughout the process.

Congratulations! The test is now over.