ספריית Arduino לניטור דופק עם דופק אוקסימטר
אחד הפרמטרים המנוטרים בפרויקט ניהול השינה שלי
זה הדופק. למדוד אותו פיתחתי מכשיר המבוסס על התנהגות של המוגלובין ואוקסיהמוגלובין כנגד אורכי גל שונים של אור. בעיקרון מדובר במדידה של כמה אור מסוג מסוים מסוגל לעבור או להשתקף באזור מושקה היטב בגוף. התדירות שבה מתרחש מחזור שלם של תופעה זו מאפשרת מדידת pulso.בשלב התכנון והבדיקה של ה מכשיר למדידת דופק פיתחתי כמה תוכנות קטנות שיעזרו לי לוודא שההרכבה הייתה נכונה. ראשית כתבתי את הקוד למטה, שלקח את הערכים הנמדדים מעת לעת (לפחות כל ולכל היותר כל אחד ) כאשר הם השתנו מינימום בין אחד לקודם (הערך שמתאים לו ) וה מנוטר ממחשב עם יישום Python כדי להיות מסוגל לנתח אותם מאוחר יותר.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#define PIN_OXIMETRO 0 // Pin analógico 0
#define MEDIDA_MINIMA 10 // Cambio menor monitorizado
#define TIEMPO_MAXIMO_MEDIDA 200 // Cambio menor entre medidas (determina la resolución vertical)
#define TIEMPO_MINIMO_MEDIDA 100 // Milisegundos entre medidas (determina la resolución horizontal)
int lectura_anterior_oximetro=0;
int lectura_oximetro;
unsigned long cronometro_minimo=0;
unsigned long cronometro_maximo=0;
void setup()
{
Serial.begin(9600);
//pinMode(PIN_OXIMETRO,INPUT); // Ya es entrada por defecto
}
void loop()
{
if(millis()>cronometro_minimo)
{
lectura_oximetro=analogRead(PIN_OXIMETRO);
if(abs(lectura_oximetro–lectura_anterior_oximetro)>MEDIDA_MINIMA||millis()>cronometro_maximo)
{
cronometro_minimo=millis()+TIEMPO_MINIMO_MEDIDA;
cronometro_maximo=millis()+TIEMPO_MAXIMO_MEDIDA;
lectura_anterior_oximetro=lectura_oximetro;
Serial.println(String(millis(),DEC)+“,”+String(lectura_oximetro,DEC));
}
}
}
|
ברגע שהערכים הותאמו (החל ממדידות צפופות מאוד) קיבלתי אוסף של ערכים מה- דופק אוקסימטר עם הזמן שיכולתי לצייר גרף באמצעות גיליון אלקטרוני, חישוב LibreOffice de LibreOffice, ספציפי.
עם הנתונים שנאספו, כפי שמוצגים בתמונה למעלה, הפעולה הבאה הייתה לקבוע אם צפיפות הערכים מותרת לחשב בצורה אמינה אך "חסכונית" (לא מדגימה יותר מהנתונים הדרושים) את הערך של pulso; כפי שניתן לראות בגרף להלן, נראה כי האמצעים שננקטו שימשו להשגת התוצאות שסביר לצפות.
.
לאחר מכן, עם המידע מדגימת הנתונים, היה צורך לפתח אלגוריתם שימדד את קצב הדופק. היצמדות לגרף שלמען הפשטות, ההנחה היא שהוא מייצג פריסה דומה ל- מתחם QRS, נראה שהדבר הפשוט ביותר הוא למדוד את הזמנים בין החלקים הבולטים ביותר, עם ערכים גבוהים יותר (המתאימים לאזור qRs של דה-פולריזציה של החדרים) להשליך את האזור השטוח וה"רועש" יותר, ולכן קשה יותר לבצע מידה. הפתרון שאומץ, המתאים לקוד הבדיקה שלהלן, פועל לפי הנוהל הבא:
-
זהה את השטח שנמדד בכל מקרה כדי לטפל רק בפסגות הערך qRs ולזרוק את העמק. לשם כך ניתן למדוד ערכים גבוהים מקבוע מסוים, אך קיים סיכון שאדם ו/או נסיבות עלולים, אם כי באופן יחסי, להעלות או להוריד את הערכים. כדי להימנע מכך, ערך בשטח נחשב גדול מזה העולה על הערך הממוצע במקדם מסוים. בדרך זו, המדידה מכוילת עצמית ברגישות וניתן להתאים אותה עוד יותר על ידי כוונון עדין של המקדם, שבמקרה שלי השגתי בניסוי במהלך הבדיקות.
בחר את הערכים של האזור היורד עבור המדידה (Rs) של השיא qRs, קרוב ככל האפשר למקסימום של העקומה. כדי לדעת שאזור העלייה נטוש, מספיק לוודא שערך חדש קטן מהקודם ולוודא שהערך המבוקש עדיין לא נמצא מאחר ובאופן כללי ישנם מספר ערכים בירידה אזור של qRs תלוי בצפיפות הדגימה. כדי לתזמן את הדופק, הערך של הרגע שבו נמצאה הנקודה מאוחסן (המילישניות המוחזרות על ידי מיליס ()) ומשווה אותו עם הבא.
כדי להבטיח שהערך הנמדד הוא הגדול ביותר באזור היורד של העקומה הגבוהה ביותר, נעשה שימוש במשתנה בוליאני ( בדוגמה זו ו בספרייה) המופעל בעת כניסה לאזור העולה של העקומה הראשית ומושבת ברגע שנמצא הערך היורד הראשון, שהוא הערך המתוזמן.
כפי שנהוג לייצג את משך הדופק כפעימות לדקה (ppm), ערך הזמן בין הפולסים המתקבל מתוקן על ידי חישוב על ידי חלוקת הזמן הכולל של הייצוג (דקה אחת, 60000 מילישניות) במרווח המתקבל על ידי הפחתת האלפיות הנוכחיות (מהערך הנוכחי) בין אלו שנקבעו בזמן קודם.
כדי למנוע מדידות שגויות (כגון המכשיר המודד בוואקום, למשל), יש לוודא שהתוצאה היא בין ערכי מקסימום למינימום לפני קבלתה כמובן מאליו. למרות שנחשב כממוצע שערך תקין למבוגר בריא במנוחה הוא בין 60 ל-100 עמודים לדקה, ישנם ערכים קבילים להלן, קל למצוא 40 עמודים לדקה בספורטאי במנוחה, עד 200 עמודים לדקה במהלך פעילות גופנית אינטנסיבית ועוד של 100 עמודים לדקה אצל מבוגרים בישיבה במצבי התרגשות, בדיוק גורם מעניין לפרויקט ניהול השינה מה שמוביל אותי לפתח את זה מכשיר למדידת דופק. מסיבה זו, מומלץ להרגיע הרבה את הערכים הללו כדי שהקצוות לא יאבדו, שיכולים להראות בדיוק היבטים רלוונטיים.
הערך הממוצע החדש מחושב על ידי הפחתת הרלוונטיות של הממוצע הנוכחי בהתבסס על מספר הערכים שנדגמו והערך האחרון מתווסף, גם הוא משוקלל עם מקדם שמפחית אותו עוד יותר ככל שנמדדו יותר ערכים עד כה .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
#define PIN_OXIMETRO 0 // Pin analógico 0
#define TIEMPO_MINIMO_MEDIDA 20 // Milisegundos entre medidas (determina la resolución horizontal)
#define COEFICIENTE_PULSO 1.25 // Coeficiente que determina la zona de valores en la que medir el pulso
#define PULSO_MENOR 30 // Ignorar valores menores (Es infrecuente un pulso más lento aún en reposo)
#define PULSO_MAYOR 180 // Ignorar valores mayores (Es infrecuente un pulso mayor en reposo)
#define MINUTO 60000.0 // Milisegundos en un minuto
float velocidad_pulso;
float lectura_media_oximetro=511.5;
unsigned long valores_contados=0;
int lectura_anterior_oximetro=0;
int lectura_oximetro;
boolean medir_pulso=false;
unsigned long cronometro_pulso=0;
unsigned long cronometro_oximetro=0;
void setup()
{
Serial.begin(9600);
//pinMode(PIN_OXIMETRO,INPUT); // Ya es entrada por defecto
}
void loop()
{
if(millis()>cronometro_oximetro)
{
cronometro_oximetro=millis()+TIEMPO_MINIMO_MEDIDA;
lectura_oximetro=analogRead(PIN_OXIMETRO);
valores_contados++;
lectura_media_oximetro=lectura_media_oximetro*(float(valores_contados–1)/valores_contados);
lectura_media_oximetro+=lectura_oximetro*(1.0/valores_contados);
if(lectura_oximetro>lectura_media_oximetro*COEFICIENTE_PULSO)
{
if(lectura_anterior_oximetro<lectura_oximetro)
{
medir_pulso=true;
}
else
{
if(medir_pulso)
{
velocidad_pulso=MINUTO/float(millis()–cronometro_pulso);
medir_pulso=false;
if(velocidad_pulso>PULSO_MENOR&&velocidad_pulso<PULSO_MAYOR)
{
Serial.println(“Pulso “+String(velocidad_pulso,DEC));
}
cronometro_pulso=millis();
}
}
}
lectura_anterior_oximetro=lectura_oximetro;
}
}
|
לבסוף, באמצעות האלגוריתם שתואר קודם לכן, פיתחתי את הספרייה כדי לחשב את הדופק על ידי זיהוי נוכחות של הֵמוֹגלוֹבִּין o אוקסיהמוגלובין (בהתאם לאורך הגל של האור בשימוש) מהקוד למטה.
הספרייה מצפה שפונקציית הדגימה תיקרא מעת לעת pulso בפרויקט ניהול השינה שלי
. בכל מקרה, מהבדיקות שעשיתי, נראה שאין צורך; או על ידי המכשיר או על ידי ההתנהגות של כדי לחשב את הדופק, אשר ניתן להתייעץ עם הפונקציה או עם הפונקציה הדופק הממוצע. בנוסף להיותי משאב מוגבל, שללתי שימוש בהפרעות מכיוון שלא הייתי זקוק לערכים מיידיים אלא לערכים מתמשכים לאורך זמן כדי לפקח על pulso, דגימה בתדירות מסוימת מציעה מספיק מידע ולא מתקבל הרבה יותר (רלוונטי) בהגדלתו, וגם לא ניתן להקטין אותו הרבה מבלי לאבד נתונים רלוונטיים לחישוב; בגרסאות מוקדמות של הקוד כדי לפקח על קריאת ה- דופק אוקסימטר גיליתי שאין צורך להקפיד על זמן מדידה מקסימלי מכיוון שאם הווריאציות של ערכים עוקבים נשקלו בצורה נכונה, זה היה קרוב מאוד למינימום.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//pulso.h
#if defined(ARDUINO) && ARDUINO>=100
#include “Arduino.h”
#else
#include “WProgram.h”
#endif
#define PULSO_MINIMO 5 // Pulso mínimo. Un pulso menor se considera un error.
#define PULSO_MAXIMO 200 // Pulso máximo. Un pulso mayor se considera un error.
#define COEFICIENTE_PULSO 1.25 // Coeficiente que multiplica el pulso medio para determinar si el valor medido está en la zona que se cronometra. Puede usarse para calibra el dispositivo
#define OXIMETRIA_MEDIA 511.5 // Medida media inicial del pulso (1023/2) Puede usarse para calibrar el dispositivo
#define PULSO_MEDIO 80.0 // Pulso medio de un adulto en reposo (sólo para referencia)
class Pulso
{
private:
byte pin_oximetro; // Pin analógico al que se conecta el sensor de pulso
int lectura_oximetro; // Último valor medido en el sensor de pulso
int lectura_anterior_oximetro; // Penúltimo valor medido en el sensor de pulso para compararlo con el último y establecer la zona de valores en la que se encuentra
unsigned long numero_lecturas_oximetro; // Número de medidas del oxímetro tomadas. Usado para calcular la media
unsigned long numero_lecturas_pulso; // Número de medidas de pulso tomadas. Usado para calcular la media
float lectura_media_oximetro; // Medida media del sensor de pulso. Usado para saber si un valor se encuentra en la zona de medida (que se cronometra) del pulso
boolean medicion_de_pulso_activa; // Verdadero si se ha entrado de la zona del pulso (para no confundir si aún se encuentra en una zona ya medida)
float velocidad_pulso; // Último valor de pulso calculado
float ultima_velocidad_pulso; // Último valor de pulso correcto
float velocidad_media_pulso; // Media de los pulsos calculados durante la última sesión (creación del objeto Pulso)
unsigned long cronometro_pulso; // Tiempo entre medidas de pulso consecutivas
protected:
public:
Pulso(byte pin_oximetro_solicitado);
~Pulso();
void monitorizar_pulso(); // Se llama periódicamente (entre 10 y 50 ms) para calcular el pulso
byte ultimo_pulso(); // Devuelve el último valor correcto de pulso muestreado
byte pulso_medio(); // Devuelve el pulso medio de la sesión
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
//pulso.cpp
#include “pulso.h”
Pulso::Pulso(byte pin_oximetro_solicitado)
{
pin_oximetro=pin_oximetro_solicitado;
//pinMode(pin_oximetro,INPUT); // Ya es entrada por defecto
numero_lecturas_oximetro=0;
numero_lecturas_pulso=0;
lectura_media_oximetro=OXIMETRIA_MEDIA;
velocidad_media_pulso=PULSO_MEDIO;
medicion_de_pulso_activa=false;
lectura_anterior_oximetro=0;
ultima_velocidad_pulso=0; // Para indicar que es un valor incorrecto por el momento
cronometro_pulso=0;
}
Pulso::~Pulso()
{
}
byte Pulso::ultimo_pulso()
{
return round(ultima_velocidad_pulso);
}
byte Pulso::pulso_medio()
{
return round(velocidad_media_pulso);
}
void Pulso::monitorizar_pulso()
{
lectura_oximetro=analogRead(pin_oximetro);
numero_lecturas_oximetro++;
lectura_media_oximetro=lectura_media_oximetro*(float(numero_lecturas_oximetro–1)/numero_lecturas_oximetro); // Cambiar la representatividad de la parte actual de la media de pulso
lectura_media_oximetro+=lectura_oximetro*(1.0/numero_lecturas_oximetro); // Añadir la nueva lectura a la media
if(lectura_oximetro>lectura_media_oximetro*COEFICIENTE_PULSO)
{
if(lectura_anterior_oximetro<lectura_oximetro) // Medida de valores creciente
{
medicion_de_pulso_activa=true;
}
else
{
if(medicion_de_pulso_activa)
{
velocidad_pulso=60000.0/float(millis()–cronometro_pulso); // Cálculo de las pulsaciones por minuto (representación habitual del pulso)
medicion_de_pulso_activa=false; // Ya se ha medido el pulso, no medir hasta entrar en una nueva zona ascendente
if(velocidad_pulso>PULSO_MINIMO&&velocidad_pulso<PULSO_MAXIMO)
{
numero_lecturas_pulso++;
ultima_velocidad_pulso=velocidad_pulso;
velocidad_media_pulso=velocidad_media_pulso*(float(numero_lecturas_pulso–1)/numero_lecturas_pulso); // Calcular la representatividad de la media actual en la nueva media
velocidad_media_pulso+=velocidad_pulso*(1.0/numero_lecturas_pulso); // Añadir la nueva lectura a la media
}
cronometro_pulso=millis();
}
}
}
lectura_anterior_oximetro=lectura_oximetro;
}
|
התוכנית הבאה לדוגמה מראה כיצד להשתמש בספרייה הקודמת כדי למדוד את pulso עם דופק אוקסימטר. בנוסף לאינסטציה של השיעור ניטור רמת אוקסיהמוגלובין/הֵמוֹגלוֹבִּין ועם מחזוריות קטנה יותר הערך של ה pulso מחושב וממוצע.
כדי להבטיח שהמידות רלוונטיות, מתוכנת המתנה לפני הצגת ערך כלשהו. מכיוון שהערך עשוי להיות שגוי (לדוגמה אם המשתמש מסיר את המכשיר), הערכים מוצגים רק אם הם נמצאים בטווח של אלה שנחשבים תקפים.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
#define PIN_PULSOMETRO 0
#define TIEMPO_MONITORIZACION_PULSO 20
#define TIEMPO_PRESENTACION_PULSO 5000
#include “pulso.h”
Pulso pulso(PIN_PULSOMETRO);
unsigned long cronometro_monitorizacion_pulso;
unsigned long cronometro_presentacion_pulso;
byte velocidad_pulso;
byte velocidad_media_pulso;
void setup()
{
Serial.begin(9600);
cronometro_monitorizacion_pulso=0;
cronometro_presentacion_pulso=TIEMPO_PRESENTACION_PULSO*2;
}
void loop()
{
if(millis()>cronometro_monitorizacion_pulso)
{
cronometro_monitorizacion_pulso=millis()+TIEMPO_MONITORIZACION_PULSO;
pulso.monitorizar_pulso();
}
if(millis()>cronometro_presentacion_pulso)
{
cronometro_presentacion_pulso=millis()+TIEMPO_PRESENTACION_PULSO;
velocidad_pulso=pulso.ultimo_pulso();
velocidad_media_pulso=pulso.pulso_medio();
if(velocidad_pulso)
{
Serial.print(“Pulso “);
Serial.print(velocidad_pulso,DEC);
Serial.print(” | Pulso medio “);
Serial.println(velocidad_media_pulso,DEC);
}
}
}
|
לפרסם תגובה