旅する医用工学者

医用工学に関するトピックを中心に,臨床工学技士国家試験,第1種ME技術実力検定試験,第2種ME技術実力検定試験に関係する内容についても取り扱います.日々の技術開発や受験勉強や学校の授業の予習・復習にお役立てください.内容の正確性には留意していますが,これを保証するものではありません.

Arduino Nano互換ボードでパルスオキシメータ製作

【注意】本記事にて製作したデバイスは薬機法に定める医療機器ではなく、診断を目的としたものでもありません。COVID-19が疑わしき場合は、保健所およびかかりつけの医療機関等にご相談願います。

Notice: This pulse oximetry system is NOT for patient diagnose. In case of suspected COVID-19, you should contact health and medical services.

 

 COVID-19のパンデミックの中、パルスオキシメータに対する注目が高まったのは記憶に新しいと思う。理由は至極単純、肺炎になればSpO2が低下する、すなわちSpO2が重症度の目安として使えそうだというのだ。

 仕事柄、職場に行けばパルスオキシメータなんぞ自由に使える。しかし、問題は自宅で悪化したときだ。今、我が家にはパルスオキシメータなどない。家庭用の安物は精度や確度に難があるとの報告もあった。

 かといって、職場に出入りしている業者さんやメディカルスタッフ向けの通販経由で医療用のパルスオキシメータを買ってしまうと、本当に必要な施設への供給を妨げることになってしまう。メディカルグレードでないにせよ、自宅療養やホテル療養の患者様に貸し出されているとのことなので、既製品を購入するのは躊躇われる。

 さらに、パルスオキシメータというのは家庭用でさえ最低でも1万円程度、そこそこのグレードでは2~3万円程度と結構値が張るのだ。したがって、おいそれとは購入できない。医療現場のため、自分のお財布のため、ちょっと我慢しよう……と思わなくもないが、あったら万一の際に便利だ。

 

 だったら、作ればいいよねっ?!

 

……というわけで、パルスオキシメータを作ることにした。

 ブレッドボードにArduino NanoとMAX30102とSSD1306OLEDディスプレイを載せ、ジャンパワイヤでI2C接続する。たったこれだけのいたってシンプルなシステムだ。

 今回私が使ったのは正確には本家Arduino Nanoではなく、Arduino Nano互換ボードだ。しかし、本家Arduino Nanoと遜色ないレベルでありながら価格も手頃だ。適当にAmazonやら秋月やらRS componentsやらで購入すればよい。参考までに、私が使用したKKHMF製のArduino Nano互換ボードおよび各パーツのAmazonリンクを付しておく。

 

 

 

  今回、私が利用したKKHMF製ボードにおける使用ピンは5V, GND, A4=SDA , A5=SCLだ。 一応、各ピンの用途を説明しておこう。5Vは電源線、GNDはGroundの略で接地(アース)線を、SDAはSerial Dataの略で信号線を、SCLはSerial Clockの略でクロックラインを意味するものだ。

 5Vの電源線は当然のごとく電源供給のためのラインだ。これがないとI2C接続されたデバイスは当然動作しない。

 GNDは0Vの電位基準であるとともに、回路を構成する帰線でもあるため、当然だがこれも接続しないとうまく動作しない。

 シリアル形式のデジタル伝送においては、クロックラインに送られるクロックパルスに同期して信号線の信号を読み書きするのだ。

 

f:id:medicalengineer:20210504221153p:plain

pulseoximeter schema

 さて、接続ができたら、母艦となるPC(今回はRaspberry Pi 4を使用)上のArduino IDE上でソースコードを打ち込む。正確にはArduino言語なのだが、ほとんどC言語と同じような感覚でコーディングできる。どうでもいいけど、Cは直感的なコーディングができるから初学者にとってはわかりやすくていいと思う。参考のため、ソースコードを記しておこう。



// ************************
// Arduino Pulse Oximeter
// ************************


// Include libralies
#include <wire.h>
#include <adafruit_gfx.h>
#include <adafruit_ssd1306.h>
#include <max30105.h>


// Definition for OLED
Adafruit_SSD1306 display(-1);
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels


// Definition for MAX30102
MAX30105 particleSensor;
#define MAX_BRIGHTNESS 255


// Constants for threshold and initial value
const uint32_t THRESHOLD_FINGER_DETECT = 7000;
const int32_t THRESHOLD_PULSE_UP_EDGE = 120;
const int32_t MIN_INIT = 9999999;
const int32_t MAX_INIT = 0;


// Limitation of HR and SpO2
const uint32_t LOWER_LIM_HR = 30;
const uint32_t UPPER_LIM_HR = 180;
const uint32_t LOWER_LIM_SPO2 = 70;
const uint32_t UPPER_LIM_SPO2 = 100;


// Valuables to pulse detect
long l_time = millis();
int32_t prev_irValue = 0;
int32_t prev_diff = 0;
int32_t diff;
long pulse_interval = -1;


// Constants and valuables for moving average
const int FRAME_REF = 20;
const int FRAME_SPO2 = 5;
const int FRAME_HR = 1;

long sum_ir;
long sum_red;
long sum_spo2;
long sum_hr;
int cnt_ref = 0;
int cnt_spo2 = 0;
int cnt_hr = 0;

long data_ir[FRAME_REF];
long data_red[FRAME_REF];
long data_spo2[FRAME_SPO2];
long data_hr[FRAME_HR];

long irValue_dc = 0;
long redValue_dc = 0;
double spo2_avg = 0;
double hr_avg = 0;


// Keep minimum and maximum value
int32_t min_irValue = MIN_INIT;
int32_t max_irValue = MAX_INIT;
int32_t min_redValue = MIN_INIT;
int32_t max_redValue = MAX_INIT;


void setup() {
// put your setup code here, to run once:
display.begin(SSD1306_SWITCHCAPVCC,0x3C);
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Starting System");
display.println("Please Wait");
display.display();
delay(2000);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Pulse");
display.println("Oximeter");
display.display();
delay(2000);


// Initialize sensor
particleSensor.begin(Wire, I2C_SPEED_FAST); //Use default I2C port, 400kHz speed
particleSensor.setup(); //Configure sensor with default settings
particleSensor.setPulseAmplitudeRed(0x0A); //Turn Red LED to low to indicate sensor is running


byte ledBrightness = 0x1F; //Options: 0=Off to 255=50mA
byte diffmpleAverage = 8; //Options: 1, 2, 4, 8, 16, 32
byte ledMode = 2; //Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
int diffmpleRate = 400; //Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulse_intervalWidth = 411; //Options: 69, 118, 215, 411
int adcRange = 4096; //Options: 2048, 4096, 8192, 16384

 

// Initialize array for moving average
// For irValue and redValue
int i_ref;
for (i_ref = 0;i_ref < FRAME_REF;i_ref++){
data_ir[i_ref] = 0;
data_red[i_ref] = 0;
}
sum_ir = 0;
sum_red = 0;

// For SpO2
int i_spo2;
for (i_spo2 = 0;i_spo2 < FRAME_SPO2;i_spo2++){
data_spo2[i_spo2] = 0;
}
sum_spo2 = 0;

// For HR
int i_hr;
for (i_hr = 0;i_hr < FRAME_HR;i_hr++){
data_hr[i_hr] = 0;
}
sum_hr = 0;

// Set initial values
prev_irValue = particleSensor.getRed();
l_time = millis();
}

 

void loop() {
// put your main code here, to run repeatedly:
long irValue = particleSensor.getRed(); //Reading the IR value
long redValue = particleSensor.getIR(); //Reading the Red value


// Finger detect
if(irValue < THRESHOLD_FINGER_DETECT || redValue < THRESHOLD_FINGER_DETECT) {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Put your");
display.println("finger");
display.display();
return;
}


// Moving average
if(cnt_ref == FRAME_REF){
cnt_ref = 0;
}

// For irValue
if(data_ir[cnt_ref] != 0){
sum_ir -= data_ir[cnt_ref];
}
data_ir[cnt_ref] = irValue;
sum_ir += data_ir[cnt_ref];
irValue_dc = sum_ir / FRAME_REF;

// For redValue
if(data_red[cnt_ref] != 0){
sum_red -= data_red[cnt_ref];
}
data_red[cnt_ref] = redValue;
sum_red += data_red[cnt_ref];
redValue_dc = sum_red / FRAME_REF;

// Increment
cnt_ref++;


// Keep minimum and maximum values to calculate irValue_ac and redValue_ac
if(irValue < min_irValue){
min_irValue = irValue;
}
if(irValue > max_irValue){
max_irValue = irValue;
}
if(redValue < min_redValue){
min_redValue = redValue;
}
if(redValue > max_redValue){
max_redValue = redValue;
}

 

// Calculate HR and SpO2 if pulse was detected
diff = prev_irValue-irValue;
if(prev_diff < THRESHOLD_PULSE_UP_EDGE && diff > THRESHOLD_PULSE_UP_EDGE){
// Calculate pulse interval
pulse_interval = millis() - l_time;
l_time = millis();


// Calculate HR
double hr = 60000 / pulse_interval;


// Calculate AC components
int32_t irValue_ac = max_irValue - min_irValue;
int32_t redValue_ac = max_redValue - min_redValue;


// Calculate R
double r = (double(redValue_ac) / redValue_dc) / (double(irValue_ac) / irValue_dc);


// Calculate SpO2 from r
double spo2 = (-45.060 * r * r + 30.354 * r + 94.845);


// Initialize minimum and maximum values
min_irValue = MIN_INIT;
max_irValue = MAX_INIT;
min_redValue = MIN_INIT;
max_redValue = MAX_INIT;


// Moving average for SpO2
if(cnt_spo2 == FRAME_SPO2){
cnt_spo2 = 0;
}
if(data_spo2[cnt_spo2] != 0){
sum_spo2 -= data_spo2[cnt_spo2];
}
data_spo2[cnt_spo2] = spo2;
sum_spo2 += data_spo2[cnt_spo2];
spo2_avg = sum_spo2 / FRAME_SPO2;
cnt_spo2++;


// Moving average for HR
if(cnt_hr == FRAME_HR){
cnt_hr = 0;
}
if(data_hr[cnt_hr] != 0){
sum_hr -= data_hr[cnt_hr];
}
data_hr[cnt_hr] = hr;
sum_hr += data_hr[cnt_hr];
hr_avg = sum_hr / FRAME_HR;
cnt_hr++;


// Show SpO2 and HR
if(hr_avg >= LOWER_LIM_HR && hr_avg <= UPPER_LIM_HR && spo2_avg >= LOWER_LIM_SPO2 && spo2_avg <= UPPER_LIM_SPO2){
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print(" HR ");
display.println(hr_avg,0);
display.print("SpO2 ");
display.println(spo2_avg,0);
display.display();
}
}
prev_irValue = irValue;
prev_diff = diff;
}

 

 コーディングが終わったら、早速Arduinoボードに書き込もう。書き込んだら
早速性能評価だ。おもむろに電源を入れ(単純にUSBケーブルを接続するだけで良い)、立ち上がりを待つ。計測がうまくいけば、程なく心拍数とSpO2の値がモニタに表示される筈だ。

 今回私が組み上げたものは、心拍数の計測値が不安定で、ごくわずかな体動でも150を超える値を吐いてくる。移動平均のフレーム数や立ち上がり検出の閾値といったパラメータを調整するなり、微分を実装するなり、改善の余地がありそうだ。しかし、SpO2は結構良かった。職場のメディカルグレードのパルスオキシメータで正確さ(確度)を確認してみたところ、この自作パルスオキシメータは、メディカルグレードのパルスオキシメータとほぼ同じ値を示した。なかなか良いじゃないか、これ。

f:id:medicalengineer:20210504221156j:plain

 医用工学に足を突っ込んだばかりの学生でも、これならきっと簡単だ。パルスオキシメータはCOVID-19パンデミックが収束したとしても、様々な用途がある便利なアイテムだ。ぜひともトライしてみてほしい。