プレイグラウンド、サンドボックス、使い捨てスクリプト置き場
0

Configure Feed

Select the types of activity you want to include in your feed.

heat-rate (ads1x15 and analog heart rate sensor)

Kohei Watanabe (Sep 27, 2025, 3:24 PM +0900) 0e7d97ed 1334030f

+125
+125
heart-rate/main.js
··· 1 + import ADS1X15 from "@chirimen/ads1x15"; 2 + import { requestI2CAccess } from "node-web-i2c"; 3 + 4 + async function readDataFromSensor(duration = 10000) { 5 + const i2cAccess = await requestI2CAccess(); 6 + const ads1x15 = new ADS1X15(i2cAccess.ports.get(1), 0x48); 7 + await ads1x15.init(true); 8 + 9 + const values = []; 10 + const start = Date.now(); 11 + 12 + while (Date.now() - start <= duration) { 13 + const output = await ads1x15.read(0); 14 + values.push(output); 15 + await new Promise((resolve) => setTimeout(resolve, 100)); 16 + } 17 + 18 + return values; 19 + } 20 + 21 + /** 22 + * 移動平均除去AC成分を計算 23 + * @param values 入力値配列 24 + * @param windowSize 窓サイズ 25 + * @returns AC成分の配列 26 + */ 27 + function calculateAcValues(values, windowSize) { 28 + const acValues = []; 29 + 30 + for (let i = 0; i < values.length; i++) { 31 + const start = Math.max(0, i - Math.floor(windowSize / 2)); 32 + const end = Math.min(values.length, i + Math.floor(windowSize / 2) + 1); 33 + const sum = values.slice(start, end).reduce((acc, val) => acc + val, 0); 34 + const average = sum / (end - start); 35 + 36 + acValues.push(values[i] - average); 37 + } 38 + 39 + return acValues; 40 + } 41 + 42 + /** 43 + * ピーク検出 44 + * @param values AC成分配列 45 + * @param minDistance 最小ピーク間隔 46 + * @returns ピークのインデックス配列 47 + */ 48 + function detectPeaks(values, minDistance) { 49 + const peaks = []; 50 + 51 + for (let i = 1; i < values.length - 1; i++) { 52 + if ( 53 + values[i] > values[i - 1] && 54 + values[i] > values[i + 1] && 55 + values[i] > 0 56 + ) { 57 + if (peaks.length === 0 || i - peaks[peaks.length - 1] >= minDistance) { 58 + peaks.push(i); 59 + } 60 + } 61 + } 62 + 63 + return peaks; 64 + } 65 + 66 + /** 67 + * 脈拍検出 68 + */ 69 + function detectHeartRate(values, samplingRate) { 70 + const minBPM = (2 * samplingRate * 60) / values.length; 71 + const maxBPM = 120; 72 + const windowSize = Math.round(samplingRate * 1); // 1秒の窓サイズ 73 + const minDistance = Math.round(samplingRate * 0.5); // 0.5秒の最小間隔 74 + 75 + const acValues = calculateAcValues(values, windowSize); 76 + const peaks = detectPeaks(acValues, minDistance); 77 + 78 + if (peaks.length < 2) { 79 + return null; 80 + } 81 + 82 + const intervals = []; 83 + for (let i = 1; i < peaks.length; i++) { 84 + intervals.push(peaks[i] - peaks[i - 1]); 85 + } 86 + 87 + const averageInterval = 88 + intervals.reduce((acc, val) => acc + val, 0) / intervals.length; 89 + const heartRate = Math.round((samplingRate * 60) / averageInterval); 90 + 91 + if (minBPM <= heartRate && heartRate <= maxBPM) { 92 + return heartRate; 93 + } else { 94 + console.log( 95 + `Heart rate ${heartRate} BPM is outside valid range (${minBPM.toFixed( 96 + 1, 97 + )}-${maxBPM})`, 98 + ); 99 + return null; 100 + } 101 + } 102 + 103 + console.log("Starting heart rate monitoring..."); 104 + 105 + while (true) { 106 + console.log(`\n--- ${new Date().toLocaleTimeString()} ---`); 107 + 108 + try { 109 + const values = await readDataFromSensor(10000); 110 + const samplingRate = values.length / 10; 111 + console.log(`Data points: ${values.length}`); 112 + console.log(`Sampling rate: ${samplingRate} Hz`); 113 + console.log(`Data range: ${Math.min(...values)} - ${Math.max(...values)}`); 114 + 115 + const heartRate = detectHeartRate(values, samplingRate); 116 + console.log( 117 + `Detected Heart Rate: ${ 118 + heartRate !== null ? heartRate + " BPM" : "Not detected" 119 + }`, 120 + ); 121 + } catch (error) { 122 + console.error("Error reading sensor data:", error); 123 + console.log("Falling back to file data..."); 124 + } 125 + }