แปลงจังหวะหัวใจให้มองเห็นได้! โปรเจกต์เครื่องวัดชีพจรแบบไร้สัมผัส พร้อมส่งข้อความให้กำลังใจด้วย AI

Heartbeat Visualization & Sonification Device
อุปกรณ์ต้นแบบสำหรับวัดและแสดงผลอัตราการเต้นของหัวใจแบบไร้สัมผัส

สวัสดีชาว Maker และเพื่อนๆ ทุกคนครับ! 💡 เวลาที่คุณรู้สึกตื่นเต้น ประหม่า หรือใจเต้นแรง เคยคิดไหมครับว่า ถ้ามีเครื่องมือที่ช่วยเปลี่ยน "ความรู้สึก" เหล่านั้นให้มองเห็นเป็นรูปธรรมได้ มันจะเจ๋งขนาดไหน?

วันนี้เราจะพาไปดูโปรเจกต์ไอเดียสุดล้ำที่ผสานเทคโนโลยี AI และ IoT เข้าด้วยกัน เพื่อสร้างอุปกรณ์ที่สามารถ "วัดอัตราการเต้นของหัวใจแบบไร้สัมผัส (Contactless)" และแปลผลออกมาเป็นจังหวะไฟกะพริบ แรงสั่นสะเทือน แถมยังแอบมีกิมมิคส่งข้อความให้กำลังใจน่ารักๆ ผ่านแอปพลิเคชัน LINE หรือ Email ได้ด้วยครับ!

เบื้องหลังโปรเจกต์: เปลี่ยนความประหม่าเป็นพลังใจ 💪

ผู้พัฒนาเล่าถึงแรงบันดาลใจในการสร้างโปรเจกต์นี้ไว้ 2 ข้อหลักๆ ครับ:

  • สะท้อนความรู้สึกที่ซ่อนอยู่: บางครั้งคนเราอาจดูสงบนิ่งจากภายนอก แต่ภายในใจกลับเต้นรัวอย่างหนัก (อาการ "Dokidoki" ในภาษาญี่ปุ่น) การทำให้ความรู้สึกเหล่านี้ถูกมองเห็นและจับต้องได้ จะช่วยให้เราและคนรอบข้างแชร์ความรู้สึกกันได้ง่ายขึ้น และลดความตึงเครียดลงได้
  • สร้างแรงผลักดันเล็กๆ: ในจังหวะที่เรารู้สึกลังเลหรือประหม่า การได้รับ "ข้อความให้กำลังใจ" เล็กๆ น้อยๆ ก็อาจเป็นแรงผลักดันชั้นดีให้เรากล้าที่จะก้าวต่อไป

ชมวิดีโอสาธิตการทำงาน (Demonstration) 🎬

เมื่อคุณกดปุ่มวัดค่า ระบบจะทำการวิเคราะห์ความเปลี่ยนแปลงของจังหวะการเต้นหัวใจในช่วง 10 วินาทีล่าสุด จากนั้น AI จะประมวลผลแล้วส่งข้อความเพื่อให้กำลังใจหรือเป็นแรงผลักดันเล็กๆ ไปยังแอป LINE หรือ Email ของคุณครับ

LINE Notification Example
ตัวอย่างการแจ้งเตือนผ่านแอปพลิเคชัน LINE
Email Notification Example
ตัวอย่างการแจ้งเตือนผ่าน Email

ส่วนประกอบและการทำงานของระบบ (System Configuration) ⚙️

โปรเจกต์นี้มีหลักการทำงานและการต่อสายที่ไม่ซับซ้อนมากนัก โดยมีหัวใจหลักคือ บอร์ดไมโครคอนโทรลเลอร์ Xiao ESP32 ที่รับข้อมูลจาก เซนเซอร์เรดาร์คลื่นมิลลิเมตร (mmWave Radar) ซึ่งสามารถวัดอัตราการหายใจและการเต้นของหัวใจได้โดยไม่ต้องสัมผัสตัว

  • สัญญาณชีพจรที่วัดได้ จะถูกนำมาซิงค์กับหลอด LED ให้กะพริบเป็นจังหวะ และนำไปขับมอเตอร์สั่นสะเทือน (ที่แฮกมาจากเครื่องตรวจจับโลหะ) ให้ทำงานประสานกัน
  • หน้าจอ OLED ขนาด 0.96 นิ้ว จะคอยแสดงผลค่าอัตราการเต้นของหัวใจแบบเรียลไทม์
  • แหล่งจ่ายไฟใช้แบตเตอรี่ 9V ร่วมกับวงจร Step-down Converter เพื่อแปลงไฟเป็น 5V สำหรับเลี้ยงบอร์ด
  • ข้อมูลทั้งหมดถูกนำไปประมวลผลด้วยโมเดล Generative AI ผ่านแพลตฟอร์ม SORACOM Flux แบบ Low-code
System Configuration Diagram
แผนภาพแสดงการเชื่อมต่อระบบ (System Configuration)

💡 Maker's Tip: เซนเซอร์ mmWave เป็นเทคโนโลยีที่กำลังมาแรงมากๆ ในวงการ IoT เพราะสามารถตรวจจับความเคลื่อนไหวเล็กๆ น้อยๆ อย่างการหายใจหรือชีพจรได้ทะลุผ่านสิ่งกีดขวางบางประเภทเลยครับ!

หากเพื่อนๆ คนไหนกำลังมองหา บอร์ดพัฒนาตระกูล ESP32, หน้าจอ OLED, โมดูลแปลงไฟ (DC-DC Converter) หรือเซนเซอร์ต่างๆ เพื่อนำไปทดลองสร้างโปรเจกต์ล้ำๆ แบบนี้ แวะมาช้อปของแท้คุณภาพดีได้ที่ Globalbyte เลยครับ!

การประกอบและการปรับปรุงเพื่อการใช้งานจริง 🛠️

ในตอนแรก ผู้พัฒนาทดลองต่อวงจรบน Breadboard แต่พบปัญหาว่าสายไฟหลุดง่ายเวลาขยับเครื่อง จึงได้ทำการอัปเกรดโดยการสร้าง Custom PCB ขึ้นมาเพื่อใช้หัวต่อแบบ Connector แทน ช่วยให้ระบบแข็งแรงและทนทานขึ้นมาก

Custom PCB design
การออกแบบแผ่นวงจรพิมพ์ (Custom PCB) เพื่อให้การเชื่อมต่อแข็งแรงและเป็นระเบียบยิ่งขึ้น

นอกจากนี้ ยังมีการเพิ่ม กลไกยึดด้วยแม่เหล็ก (Magnetic detachable mechanism) ให้กับหน้าจอ OLED ทำให้สามารถปรับมุมหรือถอดจอออกได้ง่าย เพื่อให้สะดวกต่อการดูหน้าจอทั้งเวลาที่คุณวัดชีพจรตัวเอง หรือเวลาที่นำเครื่องไปจ่อวัดชีพจรของคนอื่นครับ

Magnetic attachment mechanism for OLED
กลไกยึดจอด้วยแม่เหล็ก สามารถถอดและปรับเปลี่ยนมุมมองได้อย่างอิสระ
OLED display attached via magnet
หน้าจอ OLED ขณะติดตั้งอยู่บนกลไกแม่เหล็ก

Source Code และการเขียนโปรแกรม 💻

โปรเจกต์นี้เขียนด้วยภาษา C++ บน Arduino IDE และมีการใช้ FreeRTOS เพื่อจัดการ Task ต่างๆ ให้ทำงานพร้อมกันได้อย่างราบรื่น เช่น การอ่านค่าเซนเซอร์แบบเบื้องหลัง, การกะพริบไฟ LED ให้ตรงจังหวะ และการอัปเดตข้อมูลบนหน้าจอ OLED โดยไม่ให้การทำงานหลักสะดุด

C++ (Arduino IDE with FreeRTOS)
#include <Arduino.h>
#include "Seeed_Arduino_mmWave.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>

// --- Wi-Fi configuration ---
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
const char* flux_url = "YOUR_API_ENDPOINT";

// --- OLED configuration ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET     -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// --- LED & Button pin configuration ---
#define LED_PIN 2
#define BUTTON_PIN 1
#define SAMPLE_COUNT 100

// --- For mmWave sensor ---
#ifdef ESP32
  HardwareSerial mmWaveSerial(0);
#else
  #define mmWaveSerial Serial1
#endif

SEEED_MR60BHA2 mmWave;

// --- Sensor data structure ---
typedef struct {
  float total_phase;
  float breath_phase;
  float heart_phase;
  float breath_rate;
  float heart_rate;
  float distance;
} SensorData_t;

// --- FreeRTOS queue ---
static QueueHandle_t sensorQueuePrint   = nullptr;
static QueueHandle_t sensorQueueDisplay = nullptr;
static QueueHandle_t heartRateQueue     = nullptr;
static QueueHandle_t sensorQueueFlux    = nullptr;

// --- Data buffer ---
float heartRateBuffer[SAMPLE_COUNT];
float breathRateBuffer[SAMPLE_COUNT];
float distanceBuffer[SAMPLE_COUNT];

// --- Display message ---
String displayMessage = "";
unsigned long messageExpireTime = 0;
SemaphoreHandle_t displayMutex;

// --- WiFi connection ---
void connectToWiFi() {
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
}

// --- Measurement task ---
void MeasureTask(void *pvParameters) {
  SensorData_t data;
  for (;;) {
    if (mmWave.update(100)) {
      mmWave.getHeartBreathPhases(data.total_phase, data.breath_phase, data.heart_phase);
      mmWave.getBreathRate(data.breath_rate);
      mmWave.getHeartRate(data.heart_rate);
      mmWave.getDistance(data.distance);
    } else {
      data = {0, 0, 0, 0, 0, 0};
    }

    xQueueOverwrite(sensorQueuePrint, &data);
    xQueueOverwrite(sensorQueueDisplay, &data);
    xQueueOverwrite(sensorQueueFlux, &data);

    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

// --- OLED display task ---
void DisplayTask(void *pvParameters) {
  SensorData_t data;
  for (;;) {
    if (xQueueReceive(sensorQueueDisplay, &data, pdMS_TO_TICKS(100)) == pdPASS) {
      if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
        display.clearDisplay();
        display.setTextSize(1);
        display.setTextColor(SSD1306_WHITE);
        display.setCursor(0, 0);
        display.printf("Breath: %.1f bpm", data.breath_rate);
        display.setCursor(0, 16);
        display.printf("Heart : %.1f bpm", data.heart_rate);
        display.setCursor(0, 32);
        display.printf("Dist. : %.1f cm", data.distance);

        if (millis() < messageExpireTime && displayMessage.length() > 0) {
          display.setCursor(0, 48);
          display.print(displayMessage);
        }

        display.display();
        xSemaphoreGive(displayMutex);
      }
    }
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

// --- Serial output & heartbeat transmission task for LED ---
void PrintTask(void *pvParameters) {
  SensorData_t data;
  for (;;) {
    if (xQueueReceive(sensorQueuePrint, &data, portMAX_DELAY) == pdPASS) {
      Serial.printf("total_phase: %.2f\tbreath_phase: %.2f\theart_phase: %.2f\n",
                    data.total_phase, data.breath_phase, data.heart_phase);
      Serial.printf("breath_rate: %.2f\theart_rate: %.2f\tdistance: %.2f\n\n",
                    data.breath_rate, data.heart_rate, data.distance);

      xQueueOverwrite(heartRateQueue, &data.heart_rate);
    }
  }
}

// --- LED blinking task (synchronized with heartbeat) ---
void HeartLEDTask(void *pvParameters) {
  pinMode(LED_PIN, OUTPUT);
  float rate = 0.0;

  for (;;) {
    xQueueReceive(heartRateQueue, &rate, pdMS_TO_TICKS(100));

    if (rate < 10.0 || isnan(rate) || rate > 200.0) {
      digitalWrite(LED_PIN, LOW);
      vTaskDelay(pdMS_TO_TICKS(100));
      continue;
    }

    float interval = 60.0 / rate;
    int delay_ms = (int)(interval * 1000 / 2);

    digitalWrite(LED_PIN, HIGH);
    vTaskDelay(pdMS_TO_TICKS(delay_ms));
    digitalWrite(LED_PIN, LOW);
    vTaskDelay(pdMS_TO_TICKS(delay_ms));
  }
}

// --- 10-second data transmission task triggered by a button press ---
void ButtonTriggeredFluxTask(void *pvParameters) {
  SensorData_t data;

  for (;;) {
    if (digitalRead(BUTTON_PIN) == LOW) {
      Serial.println("Button pressed: start 10s collection");

      // Display message (button pressed !)
      if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        displayMessage = "button pressed !";
        messageExpireTime = millis() + 10000;
        xSemaphoreGive(displayMutex);
      }

      int count = 0;
      while (count < SAMPLE_COUNT) {
        if (xQueueReceive(sensorQueueFlux, &data, pdMS_TO_TICKS(200)) == pdPASS) {
          heartRateBuffer[count]  = data.heart_rate;
          breathRateBuffer[count] = data.breath_rate;
          distanceBuffer[count]   = data.distance;
          count++;
        }
        vTaskDelay(pdMS_TO_TICKS(100));
      }

      String json = "{\"heart_rate\":[";
      for (int i = 0; i < SAMPLE_COUNT; i++) {
        json += String(heartRateBuffer[i], 1);
        if (i < SAMPLE_COUNT - 1) json += ",";
      }
      json += "],\"breath_rate\":[";
      for (int i = 0; i < SAMPLE_COUNT; i++) {
        json += String(breathRateBuffer[i], 1);
        if (i < SAMPLE_COUNT - 1) json += ",";
      }
      json += "],\"distance\":[";
      for (int i = 0; i < SAMPLE_COUNT; i++) {
        json += String(distanceBuffer[i], 1);
        if (i < SAMPLE_COUNT - 1) json += ",";
      }
      json += "]}";

      if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        http.begin(flux_url);
        http.addHeader("Content-Type", "application/json");
        int res = http.POST(json);
        Serial.print("POST Flux (button): "); Serial.println(json);
        Serial.print("Response: "); Serial.println(res);
        http.end();
      }

      // Display message (data sended !)
      if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
        displayMessage = "data sended !";
        messageExpireTime = millis() + 3000;
        xSemaphoreGive(displayMutex);
      }

      vTaskDelay(pdMS_TO_TICKS(3000)); // Prevent button spamming
    }

    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

// --- Setup ---
void setup() {
  Serial.begin(115200);
  mmWave.begin(&mmWaveSerial);

  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println("SSD1306 init failed");
    while (1);
  }

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Initializing...");
  display.display();

  connectToWiFi();

  pinMode(BUTTON_PIN, INPUT_PULLUP);

  displayMutex = xSemaphoreCreateMutex();

  sensorQueuePrint   = xQueueCreate(1, sizeof(SensorData_t));
  sensorQueueDisplay = xQueueCreate(1, sizeof(SensorData_t));
  heartRateQueue     = xQueueCreate(1, sizeof(float));
  sensorQueueFlux    = xQueueCreate(1, sizeof(SensorData_t));

  xTaskCreate(MeasureTask,            "MeasureTask",     4096, nullptr, 2, nullptr);
  xTaskCreate(PrintTask,              "PrintTask",       4096, nullptr, 1, nullptr);
  xTaskCreate(DisplayTask,            "DisplayTask",     4096, nullptr, 1, nullptr);
  xTaskCreate(HeartLEDTask,           "LEDTask",         2048, nullptr, 1, nullptr);
  xTaskCreate(ButtonTriggeredFluxTask,"BtnFluxTask",     8192, nullptr, 1, nullptr);
}

void loop() {
  // Empty (reserved for FreeRTOS use)
}

⚠️ ข้อควรระวังเพิ่มเติม (Regulatory Compliance)

ผู้พัฒนาได้ระบุว่า โปรเจกต์นี้ใช้งานอยู่ภายใต้ข้อกำหนดพิเศษของประเทศญี่ปุ่น สำหรับการทดลองใช้อุปกรณ์ไร้สายที่ไม่ได้รับการรับรองชั่วคราว (สูงสุด 180 วัน) หากเพื่อนๆ นำโมดูลคลื่นวิทยุ (เช่น mmWave) มาทดลองใช้งาน ควรศึกษาข้อกฎหมายหรือความถี่ที่อนุญาตให้ใช้งานได้ในประเทศไทยเพื่อความปลอดภัยด้วยนะครับ

อ้างอิงและเรียบเรียงข้อมูลจาก: Globalbyteshop Blog

แหล่งที่มาบทความต้นฉบับ: Hackster.io - Heartbeat Visualization & Sonification with AI Messaging

ดาวน์โหลดโค้ด (.ino): Source Code Download

*คำเตือน: เนื้อหานี้เป็นการสรุปและแปลมาจากบทความ DIY โปรเจกต์ต้นฉบับภาษาอังกฤษ ข้อมูลสเปคฮาร์ดแวร์ การกำหนดค่า API และการเชื่อมต่อผ่านระบบ Cloud อาจมีความคลาดเคลื่อน หรือต้องมีการปรับเปลี่ยนให้เข้ากับสภาพแวดล้อมเครือข่ายที่คุณใช้งาน การจัดการระบบไฟ 9V ควรทำด้วยความระมัดระวัง สามารถตรวจสอบรายละเอียดเชิงเทคนิคและการตั้งค่าเพิ่มเติมได้ที่เว็บไซต์ต้นฉบับที่แนบไว้ ก่อนลงมือทำโปรเจกต์

 

แท็ก


Blog posts

เข้าสู่ระบบ

ลืมรหัสผ่านใช่ไหม?

ยังไม่มีบัญชีใช่ไหม?
สร้างบัญชี