บทความนี้จะพาเพื่อนๆ มาดูไอเดียการใช้บอร์ดไมโครคอนโทรลเลอร์ยอดฮิตอย่าง Arduino Nano คู่กับเซนเซอร์อีกนิดหน่อย เพื่อสร้าง ระบบมอนิเตอร์การให้เสาน้ำเกลือ (IV Infusion Monitoring System) แบบพื้นฐานกันครับ แถมเรายังเอาข้อมูลจากเซนเซอร์มาต่อยอดสร้างกลไก (Actuator) สำหรับ "ปรับอัตราการหยดของน้ำเกลือ" แบบอัตโนมัติได้อีกด้วย!
(หมายเหตุ: ระบบปรับปริมาณน้ำเกลืออัตโนมัตินี้เป็นเพียงเครื่องต้นแบบ (Prototype) ที่ทำขึ้นเพื่อการศึกษาเท่านั้น ยังไม่ผ่านการทดสอบทางการแพทย์เพื่อใช้งานจริงกับผู้ป่วยนะครับ)
อุปกรณ์ที่ต้องใช้ (Supplies)
ชิ้นส่วนหลักๆ สำหรับโปรเจกต์นี้ไม่ได้หายากเลยครับ (แอบกระซิบว่าถ้าเพื่อนๆ กำลังหาบอร์ด Arduino, เซนเซอร์แปลกๆ หรือเส้นพลาสติก 3D Print คุณภาพดีสำหรับทำโปรเจกต์ สามารถเข้าไปช้อปได้ที่ Globalbyte เลยครับ ครบจบในที่เดียว!)
-
LM393 Optocoupler Speed Module: โมดูลนับความเร็ว (ใช้นับหยดน้ำเกลือ)
-
XKC-Y25-NPN: เซนเซอร์ตรวจจับของเหลวแบบไร้การสัมผัส (Non-contact fluid sensor)
-
Arduino Nano: (หรือไมโครคอนโทรลเลอร์ตัวไหนก็ได้ที่ใช้ชิป ATmega328p)
-
Piezo Buzzer: ลำโพงบัซเซอร์เตือนเสียง
-
LED (สีแดง): ไฟแสดงสถานะ
ส่วนอุปกรณ์ด้านล่างนี้เป็น "ออปชันเสริม" หากคุณต้องการทำระบบบีบสายน้ำเกลือเพื่อปรับอัตราการหยดอัตโนมัติ และทำเคส 3D ครับ:
- เซอร์โวมอเตอร์ MG996R Servo
- สวิตช์แบบหมุน (Rotary Switch) 6 ตำแหน่ง
- เส้นพลาสติก 3D Print (แนะนำเป็นวัสดุ PETG เพื่อความเหนียวและทนทาน)
- แผ่นอะคริลิกหนา 3mm (ออปชันเสริม สามารถปริ้นท์ 3D แทนได้)
- น็อต M4*15 จำนวน 4 ตัว, สายแพ Jumper, แผ่นวงจร PCB อเนกประสงค์ และสายรัดตีนตุ๊กแก (Velcro)
Step 1: ออกแบบกล่องควบคุมหลัก (Main Controller Housing)
ก่อนจะจับทุกอย่างยัดรวมกัน เราต้องมีกล่อง (Housing) สำหรับใส่ตัวควบคุมหลัก, สวิตช์หมุน และช่องร้อยสายไฟไปยังเซนเซอร์ต่างๆ ก่อนครับ
ในดีไซน์นี้วาดด้วย Fusion 360 โดยทำเป็นกล่องง่ายๆ มีคลิปล็อคด้านหลังแบบ Snap-on เอาไว้หนีบเกาะกับเสาน้ำเกลือได้เลย เมื่อวาดเสร็จก็นำไฟล์ .stl ไปตั้งค่าในโปรแกรม Slicer (เช่น Prusa Slicer) ได้เลยครับ
💡 ทริคสำคัญตอนปริ้นท์ 3D: พยายามจัดวางชิ้นงานให้ ลายเส้นเลเยอร์ (Layer lines) ขนานตั้งฉากกับเสาน้ำเกลือ ครับ การทำแบบนี้จะทำให้ตัวหนีบ (Snap-on clip) หันหน้ารับแรงดึงได้ดีขึ้น ช่วยลบจุดอ่อนที่มักจะหักตามรอยต่อเลเยอร์ของการปริ้นท์ 3D นั่นเอง
Step 2: ออกแบบตัวยึดเซนเซอร์และกระเปาะน้ำเกลือ (Optocoupler Housing)
กระเปาะน้ำเกลือ (Drip chamber) ของแต่ละโรงพยาบาลมักจะมีรูปทรงและขนาดไม่เท่ากันครับ ดังนั้นทางที่ดีเราควรออกแบบตัวยึดให้พอดีกับสเปกกระเปาะที่เรามี เป้าหมายคือการล็อคตำแหน่งของ IR Optocoupler ไว้ตรงกลาง เพื่อให้ทุกครั้งที่หยดน้ำเกลือตกลงมา มันจะไปบังหรือหักเหแสงอินฟราเรด ทำให้เซนเซอร์จับสัญญาณไปให้บอร์ดคำนวณได้ครับ
💡 สิ่งที่ต้องทำก่อนประกอบ: แกะพลาสติกสีดำรูปตัว U ที่ครอบโมดูล Optocoupler อยู่ออกก่อนนะครับ เพื่อให้หลอด LED อินฟราเรดกับตัวรับแสงเปิดโล่ง จะได้จัดตำแหน่งให้ลำแสงยิงผ่านหยดน้ำเกลือได้ง่ายขึ้น
แนะนำให้ลองทดสอบระดับความสูงของเซนเซอร์ดูครับ สำหรับโปรเจกต์นี้ ผมพบว่าวางเซนเซอร์ไว้ "ครึ่งล่าง" ของกระเปาะจะจับสัญญาณได้เสถียรกว่า แต่บางครั้งถ้ากระเปาะแบบอื่น การวางไว้สูงๆ ใกล้ปากทางออกที่หยดน้ำยังมีขนาดใหญ่อยู่ อาจจะจับสัญญาณได้ดีกว่าครับ
Step 3: ออกแบบตัวหนีบสายน้ำเกลือ (Clamp Body) - ออปชันเสริม
ข้ามขั้นตอนนี้ได้เลยถ้าคุณต้องการแค่ "มอนิเตอร์" อย่างเดียว แต่ถ้าอยากให้ระบบมันปรับอัตราการหยดได้ด้วย เราต้องออกแบบตัวหนีบสายน้ำเกลือครับ โดยสร้างบล็อกทรงกลมเจาะรูตรงกลางสำหรับใส่ เซอร์โวมอเตอร์ (Servo) แล้วใช้แขนของเซอร์โว (Servo horn) บีบสายน้ำเกลือเพื่อคุมอัตราการไหลครับ
ส่วนแผ่นใสๆ ด้านบนคืออะคริลิก มีไว้กันสายน้ำเกลือหลุดออกมา (สามารถปริ้นท์ 3D ปิดทับไปเลยก็ได้) แต่การใช้อะคริลิกใสจะช่วยให้เรามองเห็นว่าเซอร์โวมันหนีบสายแรงไปหรือเบาไปไหมครับ
Step 4: ชิ้นส่วนอะคริลิกและตัวยึดอื่นๆ
ฮาร์ดแวร์ส่วนที่เหลือก็จะเป็นพวกขายึดธรรมดาๆ ที่มีตัวหนีบ (Snap-on) ไว้ติดกับเสาน้ำเกลือครับ ซึ่งอย่างที่บอกไป ว่าต้องสั่งให้เลเยอร์การปริ้นท์ตั้งฉากกับเสาเสมอเพื่อความแข็งแรง
ถ้าไม่อยากใช้อะคริลิกเลย คุณสามารถปริ้นท์ 3D แทนได้ทุกชิ้น 100% ครับ แต่ที่ผมใช้อะคริลิกในบางจุด ก็เพราะเราสามารถเอาไปเข้าเครื่องเลเซอร์เพื่อยิงสลักชื่อหรือโลโก้เท่ๆ ลงไปได้นั่นเอง
Step 5: การบัดกรีฮาร์ดแวร์และต่อวงจร (Soldering & Schematic)
มาถึงฝั่งอิเล็กทรอนิกส์กันบ้าง! เนื่องจากอุปกรณ์ส่วนใหญ่ของเรามาเป็นแบบโมดูลพร้อมเสียบอยู่แล้ว เราก็แค่หาแผ่น PCB อเนกประสงค์มาเป็นฐานรวมสายไฟทั้งหมดเพื่อต่อเข้ากับ Arduino Nano ครับ
คุณสามารถดูแผนผังวงจร (Schematic) และการจัดเลย์เอาต์บอร์ดจาก EasyEDA ได้ในรูปเลย หรือถ้าใครอยากลองทำใน Breadboard ไปก่อนเพื่อประหยัดเวลาก็ทำได้เช่นกันครับ จากนั้นก็จับหัวแร้งบัดกรีสายไฟ คอนเนคเตอร์ และพิน Header สำหรับเสียบ Arduino Nano ให้เรียบร้อย
Step 6: การประกอบและการเขียนโค้ด (Assembly & Coding)
ประกอบทุกอย่างเข้าด้วยกันได้เลยครับ! แนะนำให้เสียบคอนเนคเตอร์ต่างๆ เข้ากับกล่องหลักให้เสร็จก่อน แล้วค่อยลากสายยาวๆ ไปที่เซนเซอร์ สำหรับเซนเซอร์ XKC (วัดระดับน้ำเกลือหมด) ให้ใช้สายตีนตุ๊กแก (Velcro) รัดติดกับถุงน้ำเกลือให้แน่นๆ เพราะถ้ามันหลุดขยับ มันอาจจะส่งสัญญาณมั่วว่า "น้ำเกลือหมดแล้ว" ได้ครับ
ก่อนที่เราจะก๊อปโค้ดไปวาง มาทำความเข้าใจลอจิกเบื้องหลังกันสักนิดครับ:
-
การนับอัตราการหยด (Optocoupler): เราใช้เซนเซอร์ IR จับเวลาที่หยดน้ำเกลือแต่ละหยดตกลงมาตัดลำแสง โดยใช้ฟังก์ชัน
Micros() ของ Arduino จับเวลาหยดแรกและหยดที่สอง นำมาลบกันเพื่อหาความห่าง (Interval) จากนั้นนำไปคำนวณประเมินว่า 1 นาทีจะหยดกี่หยด กลายเป็นค่า DPM (Drops Per Minute) นั่นเอง
-
การตรวจจับน้ำเกลือหมด (XKC-Y25): เซนเซอร์ตัวนี้วัดระดับของเหลวแบบไร้การสัมผัส โดยอาศัยหลักการ ค่าความนำไฟฟ้า (Dielectric constants) ที่ต่างกันระหว่างอากาศกับน้ำเกลือ ทำให้ค่าความจุไฟฟ้า (Capacitance) และแรงดันเปลี่ยนไป เราจึงใช้เซนเซอร์ตัวนี้เตือนเมื่อระดับน้ำเกลือต่ำกว่าเส้นที่กำหนดได้ครับ
-
ระบบปรับอัตราการหยดอัตโนมัติด้วย PI Controller (ออปชันเสริม): หากค่า DPM ที่ตั้งไว้เพี้ยนไป ระบบจะใช้สมการ PI Controller คอยส่งสัญญาณไปขยับเซอร์โวมอเตอร์เพื่อเพิ่มหรือลดแรงบีบสายน้ำเกลือทีละนิด เพื่อดึงให้กลับมาตรงกับค่าที่เราตั้งไว้ครับ (ใครอยากเข้าใจสมการ PI ลึกๆ อ่านเพิ่มเติมได้ที่นี่)
เมื่อเข้าใจแล้ว ก๊อปปี้โค้ดฉบับเต็มด้านล่างนี้ไปอัปโหลดลงบอร์ด Arduino Nano (ผ่าน Arduino IDE หรือ PlatformIO) ได้เลย! โค้ดนี้มีอยู่ในไฟล์ main.cpp ด้วยครับ
#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 Variables
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);
}
}
Step 7: แนวทางการพัฒนาในอนาคต (Future Improvements)
เพื่อทำให้โปรเจกต์นี้ใช้งานในระดับจริงจังขึ้น (แบบไม่ต้องใช้ไม้หนีบเซอร์โวมาบีบสาย) ผมแนะนำให้เปลี่ยนไปใช้ Dosing pump หรือ Peristaltic pump (ปั๊มรีดท่อ) แทนครับ เพราะปั๊มพวกนี้ไม่ทำให้สายน้ำเกลือพับหรือผิดรูป ซึ่งเป็นวิธีเดียวกับที่เครื่องให้สารละลายทางการแพทย์ระดับโปรเค้าใช้กัน
นอกจากนี้ การอัปเกรดจากบอร์ด Arduino Nano ไปใช้ซีรีส์ ESP32 ก็เป็นเรื่องที่น่าสนใจมาก เพราะเราจะสามารถโยนข้อมูลเข้าสู่ระบบ IoT แจ้งเตือนเข้ามือถือ หรือเก็บ Data ดูย้อนหลังได้สบายๆ เลยครับ รวมถึงการเพิ่มเซนเซอร์ Optocoupler ซ้าย-ขวา เพื่อให้เช็คหยดน้ำแม่นยำขึ้นก็เป็นไอเดียที่เจ๋งไม่แพ้กัน
และนี่ก็คือโปรเจกต์ A.I.M.S ระบบมอนิเตอร์และปรับหยดน้ำเกลืออัตโนมัติด้วย Arduino ขอบคุณที่ติดตามอ่านครับ หวังว่าจะได้ไอเดียไปพัฒนาต่อยอดกันนะ!
*คำเตือน: เนื้อหานี้เป็นการสรุปและเรียบเรียงจากบทความต้นฉบับภาษาอังกฤษ ข้อมูลฉบับภาษาไทยอาจมีความคลาดเคลื่อนบางประการจากการตีความหรือย่อเนื้อหา สามารถตรวจสอบเนื้อหาโดยละเอียดได้ที่
ต้นฉบับภาษาอังกฤษ และ
โปรเจกต์นี้จัดทำขึ้นเพื่อการศึกษาเท่านั้น ไม่ได้ผ่านการรับรองมาตรฐานทางการแพทย์ ห้ามนำไปใช้งานจริงกับผู้ป่วยโดยเด็ดขาด