← All projects

IEIndustrial Automation & Embedded

Benchtop Bioprocess Automation & Sample Monitoring System

A master-slave benchtop system mimicking commercial continuous-culture platforms — a Raspberry Pi orchestrates and logs while an Arduino handles real-time sensor polling and peristaltic-pump actuation, all housed in custom Fusion 360 enclosures.

  • Lab Automation
  • Master-Slave Architecture
  • Continuous Data Logging
  • CAD/3D Printing

Hardware & Architecture

  • Supervisory master: Raspberry Pi 4 running Python for serial comms, timestamped CSV logging, and process scheduling.
  • Real-time slave: Arduino handling pH ADC reads, OneWire DS18B20 temperature probes, and PWM for motor drivers.
  • L298N/L293D motor drivers controlling 12V peristaltic pumps with calibratable flow rates.
  • Fusion 360 watertight enclosure, modular probe mounts, and integrated pump housing for a wet-lab environment.

Key Highlights

  • Isolates high-level software logic from timing-critical hardware tasks for reliable multi-day runs.
  • Arduino transmits comma-separated telemetry at 1 Hz; the Pi parses, logs, and runs a pH feedback loop.
  • Automated dosing: the Pi actuates the pump when pH drops below threshold and stops it once stabilized.

Images

Code

bioprocess_arduino_slave.inoArduino Subordinate Processorcpp
#include <OneWire.h>
#include <DallasTemperature.h>

// --- Hardware Pins ---
const int pHPin = A0;
const int tempPin = 2; // DS18B20 Data pin
const int pumpPWM = 5;
const int pumpDir = 6;

// --- Sensor Setup ---
OneWire oneWire(tempPin);
DallasTemperature sensors(&oneWire);

// --- Variables ---
float calibration_value = 21.34; // pH calibration constant
int pumpSpeed = 0; // 0-255 PWM

void setup() {
  Serial.begin(115200);
  sensors.begin();
  pinMode(pumpPWM, OUTPUT);
  pinMode(pumpDir, OUTPUT);
  digitalWrite(pumpDir, HIGH); // Set default pump direction
}

void loop() {
  // 1. Read Commands from Raspberry Pi
  if (Serial.available() > 0) {
    String command = Serial.readStringUntil('\n');
    command.trim();
    // Command format: "PUMP:150"
    if (command.startsWith("PUMP:")) {
      String speedStr = command.substring(5);
      pumpSpeed = speedStr.toInt();
      analogWrite(pumpPWM, pumpSpeed);
    }
  }

  // 2. Read Sensors
  sensors.requestTemperatures();
  float temperature = sensors.getTempCByIndex(0);
  int sensorValue = analogRead(pHPin);
  float voltage = sensorValue * (5.0 / 1023.0);
  float pHValue = 3.5 * voltage + calibration_value; // Standard linear pH conversion

  // 3. Transmit Data to Raspberry Pi
  // Format: "TEMP:24.5,PH:7.2,PUMP:150"
  Serial.print("TEMP:");
  Serial.print(temperature);
  Serial.print(",PH:");
  Serial.print(pHValue);
  Serial.print(",PUMP:");
  Serial.println(pumpSpeed);
  delay(1000); // 1 Hz polling rate
}
bioprocess_pi_logger.pyRaspberry Pi Logger & Controllerpython
import serial
import time
import csv
from datetime import datetime

# --- Configuration ---
# Update with the correct Arduino USB port (e.g., /dev/ttyACM0 or /dev/ttyUSB0)
SERIAL_PORT = '/dev/ttyACM0'
BAUD_RATE = 115200
LOG_FILE = 'bioprocess_data_log.csv'

# Thresholds for automated actuation
TARGET_PH_MIN = 6.5
PUMP_DOSING_SPEED = "150"  # PWM value


def setup_csv():
    """Initializes the CSV file with headers for statistical tracking."""
    with open(LOG_FILE, mode='w', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(['Timestamp', 'Temperature_C', 'pH_Level', 'Pump_PWM'])


def parse_data(serial_line):
    """Parses the comma-separated string from the Arduino."""
    try:
        parts = serial_line.split(',')
        temp = float(parts[0].split(':')[1])
        ph = float(parts[1].split(':')[1])
        pump = int(parts[2].split(':')[1])
        return temp, ph, pump
    except (IndexError, ValueError) as e:
        print(f"[ERROR] Corrupt data received: {serial_line} -> {e}")
        return None, None, None


def main():
    setup_csv()
    print("[SYSTEM] Starting Continuous Benchtop Monitoring...")
    try:
        # Establish serial connection
        ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=2)
        time.sleep(2)  # Allow Arduino to reset upon serial connection
        while True:
            if ser.in_waiting > 0:
                raw_data = ser.readline().decode('utf-8').strip()
                temp, ph, pump = parse_data(raw_data)
                if temp is not None:
                    # 1. Log Data
                    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                    with open(LOG_FILE, mode='a', newline='') as file:
                        writer = csv.writer(file)
                        writer.writerow([timestamp, temp, ph, pump])
                    print(f"[{timestamp}] Temp: {temp}C | pH: {ph} | Pump PWM: {pump}")

                    # 2. Automated Logic (Feedback Loop)
                    # If pH is too low, actuate pump to dose base solution
                    if ph < TARGET_PH_MIN and pump == 0:
                        print(f"[ALERT] pH below {TARGET_PH_MIN}. Initiating dosing pump.")
                        command = f"PUMP:{PUMP_DOSING_SPEED}\n"
                        ser.write(command.encode('utf-8'))
                    # If pH recovers, stop the pump
                    elif ph >= TARGET_PH_MIN and pump > 0:
                        print("[INFO] pH stabilized. Stopping dosing pump.")
                        ser.write(b"PUMP:0\n")
    except serial.SerialException as e:
        print(f"[FATAL] Serial Connection Error: {e}")
    except KeyboardInterrupt:
        print("\n[SYSTEM] Manual override triggered. Stopping monitoring.")
        if 'ser' in locals() and ser.is_open:
            ser.write(b"PUMP:0\n")  # Ensure pump is off before exiting
            ser.close()


if __name__ == '__main__':
    main()