A.I.M.S - an Arduino Based Automatic IV Infusion Monitoring System

by AdrHiroshi in Circuits > Arduino

926 Views, 4 Favorites, 0 Comments

A.I.M.S - an Arduino Based Automatic IV Infusion Monitoring System

PXL_20251219_004054126.jpg

This instructable showcases how an Arduino Nano and a few sensors can be used to create a rudimentary IV infusion monitoring system. In addition to that, we can use the data from the sensors to create an actuator that can automatically adjust the IV infusion dosage.

(Note: The automatic dosage adjuster is mostly a prototype and not medically tested, the adjuster is for educational purposes.)

Supplies

61 IRKVsjSL.png
beeper1_ASFjueO9VW.png
images.png
mg996r-servo-front.png
no-brand_arduino-nano-v3-kabel_f.png
S3021.png

The main components needed to create an automatic IV infusion monitoring system includes the following:

  1. LM393 Optocoupler Speed Module
  2. XKC-Y25-NPN non contact fluid sensor
  3. Arduino Nano (any ATmega328p microcontroller can be used)
  4. Piezo Buzzer (With On-board oscillator)
  5. LED (Red.)

The rest of these components were used in the project but are optional, as they are either supporting features such as automatic dosage adjustment or things like 3D printed casings.

  1. MG996R Servo
  2. Rotary Switch (6 Position, Dual Pole.)

In terms of materials, we need:

  1. 3D printing Filament (preferably PETG)
  2. 3mm Acrylic Sheet (optional, can be supplemented by 3D printing instead)
  3. 4x M4*15 Screws (heads can be completely up to you)
  4. Jumper wires
  5. 1x Blank PCB Board (optional, you can supplement with a breadboard instead.)
  6. 1x velcro strip

Designing Main Controller Housing

fefd7986-6a42-4aae-8427-c0ddc50914a3.jpg
Screenshot 2026-03-30 013233.png

Before we start putting everything together, we need to design a housing for the main controller and components like a rotary switch and ports for wires that lead to the sensors and actuators.


In this design made in fusion, the main controller housing is just a simple box with a rear snap-on clip that is used to attach the housing to an IV stand. Other than that, it has various holes for USB ports and sensor wires.


Afterwards, we can export the .stl file from fusion and slice it on any slicer software, use a slicer compatible with your 3d printer. In this case, the model was sliced using Prusa Slicer and printed on a Prusa MK4. In addition to that, the housing was also printed on an Anycubic Kobra 3 V2.


Additionally, when slicing, try to print the model with the layer lines perpendicular to the IV stand, this ensures that the layer lines are facing the snap-on clip to increase it's strength and eliminate weaknesses from layer lines.

Designing Optocoupler & Drip Chamber Housing

da6aa274-0072-46eb-b77e-1d520e0e5e07.jpg
IMG_9138.jpg

The drip chamber mount is relatively straight forward, the real issue is that depending on the manufacturer the drip chambers can vary in size and features, so it's good to try and design your own housing that fulfills the specifications of your drip chamber. The goal of designing the housing is to place the IR optocoupler sensor in such a way that the droplets coming from the IV fluid blocks or refracts the beam from the receiver, which generates a signal that we can then read using a microcontroller.


Before you design the housing, it's also important that you remove the small black plastic piece on the optocoupler (the black U-shaped piece) to expose the IR LED and IR receiver, this way it is much easier to line up the IR beam path with the IV droplets.


It is recommended to test different heights at which the optocoupler sits inside the housing, since different heights means that the droplet inside the drip chamber takes a different shape, which can alter the performance of the optocoupler sensor in reading accurately.


In this case, the optocoupler picked up the droplets more consistently if it was placed on the bottom half as seen in the photo. However, you might find that placing it higher up gives better performance since the droplet is usually much larger just as it falls down from the IV bag.

Designing the Clamp Body for IV Tubing (optional)

IMG_9145.jpg
f440634d-5c21-4123-9ef1-9d5cc7040abc.jpg

This step is optional if you don't need an automatic dosage adjuster and purely want to monitor IV drip rate. For this step, we need to design a body that can house the IV tubing and a servo that can pinch the IV tube to control the dosage / drip rate. We can call this part the Clamp Body.


The design is simple, just a circular tube with a hole in the middle for the servo, along with the servo horn that is used to pinch the IV tubing.


The acrylic top piece is used to make sure the IV tube doesn't fall out, but it can also be supplemented by a 3d-printed circular piece that covers the Clamp Body, but using an acrylic piece enables us to gauge if the servo is pinching too hard or not pinching hard enough.

Designing Miscellaneous Mounts & Acrylic Pieces

71b0dca4-7d0c-43c1-9d5c-4b99d9bd23c9.jpg
2692542f-42a0-474c-a344-f3236d72ae1b.jpg
image13.png

The rest of the hardware that we need to design is just mounts or acrylic pieces for the housings that we just made. Most of these are just mounts with snap-on clips in the back to allow them to be attached to the IV stand easily. As mentioned before, when printing these snap-on clips its important to make sure the layer lines run perpendicular to the IV stand to ensure that we eliminate any weaknesses from layer lines.


additionally, if you don't want to use acrylic, all of these parts can just be 3D printed instead. That being said, using acrylic in some places allows us to add some decorative bits like text and images by using a laser engraver.


All Acrylic pieces were cut using an epilog fusion laser cutter/engraver, but you can usually just find an online service for custom acrylic pieces.

Soldering Hardware and Designing Schematic

image8.png
Screenshot 2026-03-30 083203.png
Screenshot 2026-03-30 090143.png

Now we finally get into the electronics, since most of our electronics come in the form of modules or already have some form of connector attached to them, all we need to do is create a PCB to attach all the wires to the arduino nano.


In the picture above, you can see the schematic used for this project as well as their flags on which pin they connect to. There's also a picture of the PCB laid out in EasyEDA, but if you wish to use a breadboard you can do so, as using a PCB is just to save space and organize wiring.


After this we can start soldering our wires and connectors, as well as headers to connect our Arduino Nano to the board.

Assembly & Coding

PXL_20251219_004118783.jpg
Screenshot 2026-03-30 091509.png

This part should be the easiest. With everything wired up, all we need to do is place everything where it needs to be and upload our code to the Arduino Nano. It is recommended to first attach your connectors to the main housing before running wires out to the sensors.


The XKC sensor only needs to attach to the IV bag / bottle by using some velcro. You should tighten the velcro to ensure that the sensor does not come loose otherwise it might trigger a false "IV empty" reading.


Next, we can start coding and upload the code to the microcontroller. Now before we start, we need to know the basics of how this system works, and we will break down how each section works before implementing everything to a main program.


Optocoupler Drip Rate Measurement

This part discusses how we can actually measure the drip rate of our IV infusion. We can use the IR optocoupler to count how many drops per minute comes out of the IV bag, and the way to do so is simple. All we need to do is count the time between 2 drops of IV fluid, and from this interval we can estimate how many drops we can get in 1 minute, since IV infusion droplets are usually consistent unless there is some form of external pressure acting on the fluid. We can obtain the time between 2 drops of fluid by using the Micros() function in Arduino to capture a timestamp of the first drop before then taking the timestamp of the next drop, then simply subtracting the two to find the interval. Then, we can use this data to print out a DPM (Drops Per Minute) reading in the serial monitor or use it for something else.


Empty IV detection using the XKC-Y25

To detect if the IV fluid has gone dry, we can use the XKC's capacitive fluid sensing to check if the fluid level has dropped below the set amount of volume that we can choose by adjusting the sensor's height relative to the IV bottle / bag. This is why it is important to make sure the XKC sensor is secured tightly with velcro, otherwise it might trigger a false signal. This sensor has two modes, depending on what signal the sensor will send if it detects fluid. In the first mode it will send a HIGH signal if fluid is detected, and the second mode is the opposite where it pulls the output line LOW if it detects a fluid. You can use whichever mode you want.


The capacitive fluid sensing simply works by the difference in dielectric constants between air and fluids. The change in dielectric constant changes capacitance, which also changes voltage. This change can be picked up and interpreted as a fluid level change.


Dosage / Drip Rate Adjustment Rate (optional)

This part onwards is completely optional, if you only want to monitor IV infusions then using the optocoupler and XKC sensor is adequate.


The drip rate is adjusted using a PI controller that can automatically adjust for minor deviations or errors in the DPM reading. To put it simply, a PI controller controls damping and corrects errors, and it is capable of correcting small errors over time. To learn more about it, you can visit this link


Next, we need to actually select the drip rate of our IV infusion, so this is where the rotary switch comes in. It works by setting six pins to HIGH, and whichever pin is currently selected by the rotary switch completes the circuit back to GND. This makes the reading in the selected pin LOW, since there is no potential difference between GND and the selected pin. Using different pins, we can map them to different drip rates.

(Note: If you don't want to use a rotary switch, using an array of regular switches is fine too, but using a rotary switch makes it easier to select drip rates since only one pin is selected at any given moment.)


Full Code

Compiling everything into a single code gives us this:

#include <Servo.h>

// Pin Definitions
const int OPTO_PIN = 4;
const int SENSOR_PIN = 5;
const int BUZZER_PIN = 2;
const int LED_PIN = 3;
const int SERVO_PIN = 9; // Servo signal

// Rotary switch pins
const int DPM1_PIN = A0;
const int DPM2_PIN = A1;
const int DPM3_PIN = A2;
const int DPM4_PIN = A3;
const int DPM5_PIN = A4;
const int DPM6_PIN = A5;

// Drip Rate Settings
const uint16_t DPM_VALUES[6] = {7, 14, 20, 28, 40, 60};

// Servo Settings (you may need to adjust these based on your own servo)
const int SERVO_MIN = 1099;
const int SERVO_MAX = 1800;
const int SERVO_CENTER = 1250;

// PI Settings
const float KP = 1.5;
const float KI = 0.01;

const unsigned long CONTROL_INTERVAL_MS = 10;
const int DPM_DEADBAND = 2;
const int SERVO_STEP_LIMIT = 50;
const float INTEGRAL_LIMIT = 80.0;

// Global Variales
Servo clampServo;

uint16_t selected_dpm = 0;
uint16_t prev_selected_dpm = 0;
uint16_t current_dpm = 0;
uint16_t last_dpm = 0;

int servo_pos = SERVO_CENTER;
float integral = 0.0;

// Optocoupler Settings
bool last_opto_state = HIGH;


unsigned long last_capture_us = 0;

// Timers
unsigned long last_control_time = 0;
unsigned long last_debug_time = 0;

// Read selected DPM from rotary switch (using built-in pull up resistors)
uint16_t readSelectedDPM() {
if (digitalRead(DPM1_PIN) == LOW) return DPM_VALUES[5];
if (digitalRead(DPM2_PIN) == LOW) return DPM_VALUES[4];
if (digitalRead(DPM3_PIN) == LOW) return DPM_VALUES[3];
if (digitalRead(DPM4_PIN) == LOW) return DPM_VALUES[2];
if (digitalRead(DPM5_PIN) == LOW) return DPM_VALUES[1];
if (digitalRead(DPM6_PIN) == LOW) return DPM_VALUES[0];
return 0;
}

// Convert interval between drops to DPM
uint16_t computeDPM(unsigned long interval_us) {
if (interval_us == 0) return 0;

// DPM = 60,000,000 us / interval_us
unsigned long dpm = 60000000UL / interval_us;

if (dpm > 65535UL) dpm = 65535UL; //overflow cap
return (uint16_t)dpm;
}

// Approximate feed-forward servo position based on target DPM
int dpmToServo(uint16_t dpm) {
if (dpm == 0) return SERVO_MIN;

long range = SERVO_MAX - SERVO_MIN;
int pos = SERVO_MIN + (range * (long)(dpm - 7)) / (60 - 7);

if (pos < SERVO_MIN) pos = SERVO_MIN;
if (pos > SERVO_MAX) pos = SERVO_MAX;

return pos;
}

// Setting Servo Safe Limits
void servoSetUs(int us) {
if (us < SERVO_MIN) us = SERVO_MIN;
if (us > SERVO_MAX) us = SERVO_MAX;

clampServo.writeMicroseconds(us);
}

// PI controller
void piUpdate(uint16_t setpoint, uint16_t measured) {
int error = (int)setpoint - (int)measured;

// Discard Spikes / Noise
if (measured > 120) {
integral = 0;
return;
}

// Deadband to ignore very small errors
if (abs(error) < DPM_DEADBAND) {
return;
}

float dt = CONTROL_INTERVAL_MS / 1000.0;
float new_integral = integral + error * dt;

// Integral clamp
if (new_integral > INTEGRAL_LIMIT) new_integral = INTEGRAL_LIMIT;
if (new_integral < -INTEGRAL_LIMIT) new_integral = -INTEGRAL_LIMIT;

float output = KP * error + KI * new_integral;

int delta = (int)output;

// Limit how much servo can move each update
if (delta > SERVO_STEP_LIMIT) delta = SERVO_STEP_LIMIT;
if (delta < -SERVO_STEP_LIMIT) delta = -SERVO_STEP_LIMIT;

int new_servo = servo_pos + delta;

// Clamp servo range and prevent windup
if (new_servo > SERVO_MAX) {
new_servo = SERVO_MAX;
integral = 0;
} else if (new_servo < SERVO_MIN) {
new_servo = SERVO_MIN;
integral = 0;
} else {
integral = new_integral;
}

servo_pos = new_servo;
servoSetUs(servo_pos);
}

// Setup
void setup() {
Serial.begin(9600);

pinMode(OPTO_PIN, INPUT_PULLUP);
pinMode(SENSOR_PIN, INPUT_PULLUP);

pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);

pinMode(DPM1_PIN, INPUT_PULLUP);
pinMode(DPM2_PIN, INPUT_PULLUP);
pinMode(DPM3_PIN, INPUT_PULLUP);
pinMode(DPM4_PIN, INPUT_PULLUP);
pinMode(DPM5_PIN, INPUT_PULLUP);
pinMode(DPM6_PIN, INPUT_PULLUP);

clampServo.attach(SERVO_PIN);
servoSetUs(SERVO_CENTER);

digitalWrite(LED_PIN, HIGH); // Device active
digitalWrite(BUZZER_PIN, HIGH); // Buzzer off if active LOW

last_opto_state = digitalRead(OPTO_PIN);
}

// Main Loop
void loop() {
// 1. Read rotary switch
selected_dpm = readSelectedDPM();

// 2. If setpoint changed, jump servo near estimated position
if (selected_dpm != prev_selected_dpm) {
servo_pos = dpmToServo(selected_dpm);
servoSetUs(servo_pos);
integral = 0;
prev_selected_dpm = selected_dpm;
}

// 3. Read optocoupler and detect falling edge
bool opto_state = digitalRead(OPTO_PIN);

if (last_opto_state == HIGH && opto_state == LOW) {
unsigned long now_us = micros();

if (last_capture_us != 0) {
unsigned long interval_us = now_us - last_capture_us;
last_dpm = computeDPM(interval_us);
current_dpm = last_dpm;
}

last_capture_us = now_us;
}

last_opto_state = opto_state;

// 4. XKC sensor controls buzzer
if (digitalRead(SENSOR_PIN) == LOW) {
digitalWrite(BUZZER_PIN, LOW); // buzzer ON
} else {
digitalWrite(BUZZER_PIN, HIGH); // buzzer OFF
}

// 5. Run PI controller at fixed interval
unsigned long now_ms = millis();
if (now_ms - last_control_time >= CONTROL_INTERVAL_MS) {
last_control_time = now_ms;
piUpdate(selected_dpm, current_dpm);
}

// 6. Debug print every 200 ms
if (now_ms - last_debug_time >= 200) {
last_debug_time = now_ms;

Serial.print("SP=");
Serial.print(selected_dpm);
Serial.print(" DPM=");
Serial.print(current_dpm);
Serial.print(" Servo=");
Serial.println(servo_pos);
}
}

this code is also available in the main.cpp file listed below.


Uploading to Arduino Nano

Now, with the code written, all we need to do is uploaded to the Microcontroller by using any IDE software. You can use Arduino IDE, but here we used PlatformIO.


And with that, we're basically done. All that's left to do is to attach the IV bag to the spike and start monitoring.


Downloads

Future Improvements and Suggestions

This part is just a few recommendations and some things that I think can be improved. First off, I think using some sort of dosing pump or a peristaltic pump would be a much better and medically viable application for adjusting dosages, since that's what professional infusion pumps use as dosing pumps are much less likely to kink, bend, or deform IV tubing lines. In addition to that, migrating to an ESP32 or the ESP family of microcontrollers would be a good step forward to integrating this device in an IoT environment.


In addition to all this, some minor quality of life improvements can be made, like making a self-adjusting strap for the XKC adaptor, as well as adjusting optocoupler resistor values to make it more or less sensitive to changes in the beam path, or even including multiple optocouplers to improve sensor accuracy.



This concludes our project on A.I.M.S - the Arduino Based Automatic IV Infusion Monitoring System. Thank you for reading.