K1_250705

神谷&重田により設計・生産された心電図計。ソフトウェアが難点であったので,Arduino側とサーバー側,双方を再構成してみた。ハードウェアは非常に優れており,ECGは大変美しく測定できる。
Arduino側
マルチスレッド化してコア1が波形処理,コア2がHTTP通信を担当することに。
NTPを用いて時刻を取得,IBIのタイムスタンプをms単位で記録することに。
コードをメインモジュールとサブモジュールに分割
HTTP-ECG_N1_03が最新版。
サーバー側(アクセスはここから)
4モジュール同時表示&データ回収に対応。
グラフの再描画は5秒間隔とし,グラフのリフレッシュはAJAXで行うことに。
指定した時間帯のIBIを狙って表示&取得することもできる。
今後の展開
・本体でRMSSDなどのHRVを計算し,フィードバックできるようにする
・本体フルカラーLEDを心拍に合わせて光らせる
・モードを切り替えるとUDPで心拍波形を受け取れるようにする
などの機能を追加したい。

<?php
// showECG.php
// Auto‐Refresh Start/Stop & AJAX 更新版 ECG IBI Plot(Date/EndTime セッションクッキー対応)

/* ───── CONFIGURATION ───── */
$allModules = ['ECG01','ECG02','ECG03','ECG04'];
$dataDir    = __DIR__.'/data/';
$rootDir    = __DIR__.'/';

/* ───── GET / COOKIE / DEFAULTS ───── */
// ★ まず Date/time(EndTime)をクッキー→GET→現在時刻の順で取得
$date = $_COOKIE['date'] ?? ($_GET['date'] ?? date('Ymd'));
$time = $_COOKIE['time'] ?? ($_GET['time'] ?? date('His'));

$length = isset($_GET['length']) ? intval($_GET['length']) : 600;
$min    = isset($_GET['min']) ? intval($_GET['min']) : ($_COOKIE['min'] ?? 500);
$max    = isset($_GET['max']) ? intval($_GET['max']) : ($_COOKIE['max'] ?? 1500);

/* ───── バリデーション & Cookie 保存 ───── */
if (!preg_match('/^\d{8}$/',$date)) $date = date('Ymd');
if (!preg_match('/^\d{6}$/',$time)) $time = date('His');
if ($length < 1)  $length = 600;
if ($min < 0)     $min = 0;
if ($max <= $min) $max = $min + 100;

// ★ Date/time をセッションクッキー(有効期限省略)で保存
setcookie('date', $date);
setcookie('time', $time);
// min/max は従来どおり 1 年有効
if (isset($_GET['min'])) setcookie('min', $min, time()+86400*365,'/');
if (isset($_GET['max'])) setcookie('max', $max, time()+86400*365,'/');

/* ───── Module1–4 入力処理(変更なし) ───── */
if (isset($_GET['mod1'])||isset($_GET['mod2'])||isset($_GET['mod3'])||isset($_GET['mod4'])) {
  $raw=[];
  for($i=1;$i<=4;$i++){
    $k="mod{$i}";
    if(!empty($_GET[$k]) && in_array($_GET[$k],$allModules,true)) $raw[]=$_GET[$k];
  }
  $mods=array_slice(array_unique($raw),0,4);
  setcookie('modules',implode(',',$mods),time()+86400*365,'/');
}elseif(!empty($_COOKIE['modules'])){
  $mods=array_values(array_intersect($allModules,explode(',',$_COOKIE['modules'])));
}else $mods=$allModules;
if(empty($mods)) $mods=$allModules;

/* ───── JSON エンドポイント (AJAX) ───── */
if(isset($_GET['ajax'])&&$_GET['ajax']==='1'){
  header('Content-Type: application/json');

  // EndTime 決定
  if(isset($_GET['now']) && $_GET['now']==='1'){
    $time=date('His');                // 現在時刻
  }elseif(isset($_GET['time']) && preg_match('/^\d{6}$/',$_GET['time'])){
    $time=$_GET['time'];              // 手入力時刻
  } // else: 上部で決定した $time をそのまま使用

  // ★ ajax でも最新 time をクッキー更新(セッション)
  setcookie('time', $time);

  $targetSec=intval(substr($time,0,2))*3600
            +intval(substr($time,2,2))*60
            +intval(substr($time,4,2));
  $fromSec=$targetSec-$length;

  $series=[];
  foreach($mods as $mod){
    $csv="{$mod}_{$date}.csv";
    $file=is_readable($dataDir.$csv)?$dataDir.$csv:(is_readable($rootDir.$csv)?$rootDir.$csv:null);
    $pts=[];
    if($file && ($fh=fopen($file,'r'))){
      while(($L=fgetcsv($fh))!==false){
        if(count($L)<2) continue;
        [$ts,$ibi]=$L;
        if(preg_match('/^(\d{8})_(\d{2})(\d{2})(\d{2})_(\d{3})$/',$ts,$m)){
          [$h,$i,$s,$ms]=$m;
          $h=$m[2];$i=$m[3];$s=$m[4];$ms=$m[5];
          $sec=intval($h)*3600+intval($i)*60+intval($s);
          if($sec>=$fromSec && $sec<=$targetSec){
            $dt=DateTime::createFromFormat('YmdHisv',$m[1].$h.$i.$s.$ms);
            $epoch=((int)$dt->format('U'))*1000+intval($ms);
            $pts[]=['ts_ms'=>$epoch,'y'=>floatval($ibi)];
          }
        }
      }
      fclose($fh);
    }
    $series[$mod]=$pts;
  }

  echo json_encode([
    'date'=>$date,'time'=>$time,'length'=>$length,
    'min'=>$min,'max'=>$max,'mods'=>$mods,'series'=>$series
  ]);
  exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>K1 IBI Plot</title>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<style>
 body{font-family:sans-serif;margin:2em;}
 form{margin-bottom:1em;}
 .row{display:flex;align-items:center;margin-bottom:0.5em;}
 .row input{width:6em;margin-right:1em;}
 button{margin-right:1em;}
 .textarea-container{display:flex;gap:1em;margin-top:1em;}
 .textarea-container textarea{flex:1;height:150px;}
</style>
</head>
<body>
<h2 id="pageTitle">ECG IBI Plot (refresh)</h2>

<form id="cfgForm">
  <div class="row">
    Date:     <input name="date" value="<?php echo $date ?>">
    EndTime:  <input name="time" value="<?php echo $time ?>" id="endTimeField">
    Length:   <input name="length" value="<?php echo $length ?>">
  </div>
  <div class="row">
    Min: <input name="min" value="<?php echo $min ?>">
    Max: <input name="max" value="<?php echo $max ?>">
  </div>
  <div class="row">
    1: <input name="mod1" value="<?php echo $mods[0]??'' ?>">
    2: <input name="mod2" value="<?php echo $mods[1]??'' ?>">
    3: <input name="mod3" value="<?php echo $mods[2]??'' ?>">
    4: <input name="mod4" value="<?php echo $mods[3]??'' ?>">
  </div>
  <button type="button" id="startBtn">Start Auto-Refresh</button>
  <button type="button" id="stopBtn">Stop Auto-Refresh</button>
  <button type="button" id="applyBtn">Apply</button>
</form>

<div id="chart_div" style="width:100%;height:450px;"></div>

<div class="textarea-container">
  <textarea name="text1" placeholder="Module1"></textarea>
  <textarea name="text2" placeholder="Module2"></textarea>
  <textarea name="text3" placeholder="Module3"></textarea>
  <textarea name="text4" placeholder="Module4"></textarea>
</div>

<script>
google.charts.load('current',{packages:['corechart']});
let chart,dataTable,options;
let statusLabel='(refresh)',intervalId=null;

/* === fetch === */
async function fetchData(auto){
  const p=new URLSearchParams(new FormData(document.getElementById('cfgForm')));
  p.set('ajax','1');
  if(auto){ p.set('now','1'); p.delete('time'); }
  const r=await fetch('showECG.php?'+p);
  return r.json();
}

/* === init === */
function initChart(json){
  dataTable=new google.visualization.DataTable();
  dataTable.addColumn('datetime','Time');
  json.mods.forEach(m=>dataTable.addColumn('number',m));
  new google.visualization.DateFormat({pattern:'HH:mm:ss'}).format(dataTable,0);

  options={
    title:'',
    hAxis:{title:'Time',format:'HH:mm:ss'},
    vAxis:{title:'IBI (ms)'},
    legend:{position:'right'},
    pointSize:3,
    explorer:{actions:['dragToZoom','rightClickToReset'],axis:'horizontal'}
  };
  chart=new google.visualization.LineChart(document.getElementById('chart_div'));
  updateChart(json);
}

/* === update === */
function updateChart(json){
  document.getElementById('endTimeField').value=json.time;

  const n=dataTable.getNumberOfRows();
  if(n) dataTable.removeRows(0,n);

  const map={};
  json.mods.forEach(m=>{
    json.series[m].forEach(pt=>{
      if(!map[pt.ts_ms]) map[pt.ts_ms]={};
      map[pt.ts_ms][m]=pt.y;
    });
  });

  Object.keys(map).map(Number).sort((a,b)=>a-b).forEach(ts=>{
    const row=[new Date(ts)];
    json.mods.forEach(m=>row.push(map[ts][m]??null));
    dataTable.addRow(row);
  });

  options.title=`${json.mods.join(', ')} – ${json.date} ${json.time} ±${json.length}s ${statusLabel}`;
  options.vAxis.viewWindow={min:json.min,max:json.max};
  document.getElementById('pageTitle').textContent='ECG IBI Plot '+statusLabel;
  chart.draw(dataTable,options);

  const areas=['text1','text2','text3','text4'];
  for(let i=0;i<4;i++){
    const ta=document.getElementsByName(areas[i])[0];
    if(i>=json.mods.length){ ta.value='データがありません'; continue; }
    const mod=json.mods[i];
    const lines=Object.keys(map).map(Number).sort((a,b)=>a-b).flatMap(ts=>{
      const ibi=map[ts][mod];
      if(ibi==null) return [];
      const d=new Date(ts);
      const hh=`${d.getHours()}`.padStart(2,'0');
      const mm=`${d.getMinutes()}`.padStart(2,'0');
      const ss=`${d.getSeconds()}`.padStart(2,'0');
      const ms=`${d.getMilliseconds()}`.padStart(3,'0');
      return `${hh}:${mm}:${ss}.${ms},${ibi}`;
    });
    ta.value=lines.length?lines.join('\n'):'データがありません';
  }
}

/* === control === */
function startAutoRefresh(){
  if(intervalId) return;
  statusLabel='(refresh)';
  fetchData(true).then(process);
  intervalId=setInterval(()=>fetchData(true).then(process),5000);
}
function stopAutoRefresh(){
  if(!intervalId) return;
  clearInterval(intervalId);
  intervalId=null;
  statusLabel='(stop)';
  fetchData(false).then(process);
}
function process(j){ chart?updateChart(j):initChart(j); }

document.getElementById('startBtn').onclick=startAutoRefresh;
document.getElementById('stopBtn').onclick=stopAutoRefresh;
document.getElementById('applyBtn').onclick=()=>{
  stopAutoRefresh();
  fetchData(false).then(process);
};

/* === onload === */
google.charts.setOnLoadCallback(async ()=>{
  const j=await fetchData(true);
  initChart(j);
  startAutoRefresh();
});
</script>
</body>
</html>

コメントを残す