serialPlotter / index.html
OzoneAsai's picture
Update index.html
f2f3129 verified
raw
history blame
22 kB
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>リアルタイムセンサーデータプロット</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
}
/* ボタンのスタイル */
button {
padding: 10px 20px;
font-size: 16px;
margin: 10px;
cursor: pointer;
}
/* モーダルダイアログのスタイル */
.modal {
display: none; /* 初期状態では非表示 */
position: fixed; /* 固定位置 */
z-index: 1; /* 他の要素より前面に表示 */
left: 0;
top: 0;
width: 100%; /* 幅を全画面に */
height: 100%; /* 高さを全画面に */
overflow: auto; /* 必要に応じてスクロール */
background-color: rgba(0, 0, 0, 0.4); /* 背景を半透明に */
}
.modal-content {
background-color: #fefefe;
margin: 5% auto; /* 上下にマージン、中央揃え */
padding: 20px;
border: 1px solid #888;
width: 90%; /* 幅を90%に */
max-width: 600px; /* 最大幅を600pxに */
border-radius: 8px;
position: relative;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
position: absolute;
right: 20px;
top: 10px;
cursor: pointer;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
}
.regex-container textarea,
.options-container input[type="number"] {
width: 100%;
padding: 8px;
margin: 8px 0;
box-sizing: border-box;
font-size: 16px;
}
.message {
color: red;
font-weight: bold;
margin-top: 10px;
}
.modal-buttons {
text-align: right;
margin-top: 20px;
}
.modal-buttons button {
margin-left: 10px;
}
/* グラフコンテナのスタイル */
#charts {
display: block;
width: 100%;
box-sizing: border-box;
}
.chart-container {
width: 100%;
height: 30vh; /* ビューポート高さの30% */
margin: 10px 0;
position: relative;
}
svg {
width: 100%;
height: 100%;
border: 1px solid #ccc;
box-sizing: border-box;
}
/* Rawデータ表示のスタイル */
#rawDataOutput {
width: 90%;
margin: 20px auto;
text-align: left;
}
#rawDataDisplay {
width: 100%;
max-height: 200px;
overflow: auto;
border: 1px solid #ccc;
padding: 10px;
background-color: #f9f9f9;
white-space: pre-wrap;
}
</style>
<!-- D3.jsの読み込み -->
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<h1>リアルタイムセンサーデータプロット</h1>
<button id="connectButton">シリアルポートに接続</button>
<button id="settingsButton">設定</button>
<div id="charts">
<div class="chart-container">
<h3>左センサー</h3>
<svg id="leftChart"></svg>
</div>
<div class="chart-container">
<h3>中央センサー</h3>
<svg id="centerChart"></svg>
</div>
<div class="chart-container">
<h3>右センサー</h3>
<svg id="rightChart"></svg>
</div>
</div>
<div id="rawDataOutput">
<h2>Rawデータ</h2>
<pre id="rawDataDisplay"></pre>
</div>
<!-- モーダルダイアログ -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>設定</h2>
<div class="regex-container">
<h3>正規表現設定</h3>
<p>以下の正規表現を使用してシリアルデータを解析します。3つのキャプチャグループ(左、中央、右センサー)を含むように設定してください。</p>
<textarea id="regexInput">Left Sensor:\s*(\d+)\s*\|\s*Center Sensor:\s*(\d+)\s*\|\s*Right Sensor:\s*(\d+)</textarea><br>
<button id="applyRegexButton">適用</button>
<div id="regexMessage" class="message"></div>
</div>
<div class="options-container">
<h3>表示オプション</h3>
<label>
<input type="checkbox" id="includeZeroCheckbox" checked>
Y軸に0を含める
</label>
<br>
<label>
<input type="checkbox" id="useSplineCheckbox" checked>
スプライン曲線を使用
</label>
</div>
<div class="options-container">
<h3>データ保持数</h3>
<input type="number" id="maxPointsInput" value="75" min="10" max="1000">
<button id="applyMaxPointsButton">適用</button>
<div id="maxPointsMessage" class="message"></div>
</div>
<div class="modal-buttons">
<button id="closeSettingsButton">閉じる</button>
</div>
</div>
</div>
<script>
// -------------------------------
// グローバル変数の設定
// -------------------------------
let MAX_POINTS = 75; // データ保持数(変更可能に)
const leftData = [];
const centerData = [];
const rightData = [];
const timeData = [];
let currentTime = 0;
let sensorPattern = /Left Sensor:\s*(\d+)\s*\|\s*Center Sensor:\s*(\d+)\s*\|\s*Right Sensor:\s*(\d+)/;
// -------------------------------
// オプション関連の要素取得
// -------------------------------
const includeZeroCheckbox = document.getElementById('includeZeroCheckbox');
const useSplineCheckbox = document.getElementById('useSplineCheckbox');
const maxPointsInput = document.getElementById('maxPointsInput');
const applyMaxPointsButton = document.getElementById('applyMaxPointsButton');
const maxPointsMessage = document.getElementById('maxPointsMessage');
// -------------------------------
// モーダルダイアログの設定
// -------------------------------
const settingsModal = document.getElementById('settingsModal');
const settingsButton = document.getElementById('settingsButton');
const closeModalSpan = document.querySelector('.modal .close');
const closeSettingsButton = document.getElementById('closeSettingsButton');
// モーダルを開く
settingsButton.onclick = function() {
settingsModal.style.display = "block";
}
// モーダルを閉じる(×ボタン)
closeModalSpan.onclick = function() {
settingsModal.style.display = "none";
}
// モーダルを閉じる(閉じるボタン)
closeSettingsButton.onclick = function() {
settingsModal.style.display = "none";
}
// モーダル外をクリックすると閉じる
window.onclick = function(event) {
if (event.target == settingsModal) {
settingsModal.style.display = "none";
}
}
// -------------------------------
// オプションのイベントリスナー
// -------------------------------
includeZeroCheckbox.addEventListener('change', () => {
updateCharts();
});
useSplineCheckbox.addEventListener('change', () => {
updateLineCurves();
updateCharts();
});
applyMaxPointsButton.addEventListener('click', () => {
const newMax = parseInt(maxPointsInput.value, 10);
if (isNaN(newMax) || newMax < 10 || newMax > 1000) {
maxPointsMessage.style.color = 'red';
maxPointsMessage.textContent = '有効な数値を入力してください(10〜1000)。';
return;
}
// 新しいMAX_POINTSを設定
MAX_POINTS = newMax;
maxPointsMessage.style.color = 'green';
maxPointsMessage.textContent = `データ保持数が${newMax}に設定されました。`;
// データ配列を更新
[leftData, centerData, rightData, timeData].forEach(dataArray => {
while (dataArray.length > MAX_POINTS) {
dataArray.shift();
}
});
// スケールを再設定
for (let key in charts) {
const chart = charts[key];
chart.xScale.domain([0, MAX_POINTS - 1]);
chart.svg.select(".x-axis")
.transition()
.duration(500)
.call(d3.axisBottom(chart.xScale));
}
updateCharts();
});
// -------------------------------
// スプライン曲線の更新関数
// -------------------------------
function updateLineCurves() {
const curveType = useSplineCheckbox.checked ? d3.curveBasis : d3.curveLinear;
for (let key in charts) {
const chart = charts[key];
chart.line.curve(curveType);
// パスを再設定
chart.path
.datum(chart.data)
.attr("d", chart.line);
}
}
// -------------------------------
// D3.jsによるグラフの初期設定
// -------------------------------
const charts = {
left: {
svg: d3.select("#leftChart"),
data: leftData,
color: "red",
path: null,
xScale: null,
yScale: null,
line: null
},
center: {
svg: d3.select("#centerChart"),
data: centerData,
color: "green",
path: null,
xScale: null,
yScale: null,
line: null
},
right: {
svg: d3.select("#rightChart"),
data: rightData,
color: "blue",
path: null,
xScale: null,
yScale: null,
line: null
}
};
function initCharts() {
for (let key in charts) {
const chart = charts[key];
const svgElement = chart.svg.node();
const boundingRect = svgElement.getBoundingClientRect();
const width = boundingRect.width;
const height = boundingRect.height;
// スケールの設定
chart.xScale = d3.scaleLinear().domain([0, MAX_POINTS - 1]).range([50, width - 50]);
chart.yScale = d3.scaleLinear().domain([0, 1023]).range([height - 30, 20]); // マージンを考慮
// 軸の追加
chart.svg.append("g")
.attr("transform", `translate(0,${height - 30})`)
.attr("class", "x-axis")
.call(d3.axisBottom(chart.xScale));
chart.svg.append("g")
.attr("transform", `translate(50,0)`)
.attr("class", "y-axis")
.call(d3.axisLeft(chart.yScale));
// パスの生成
chart.line = d3.line()
.x((d, i) => chart.xScale(i))
.y(d => chart.yScale(d))
.curve(useSplineCheckbox.checked ? d3.curveBasis : d3.curveLinear); // 初期曲線タイプ
chart.path = chart.svg.append("path")
.datum(chart.data)
.attr("fill", "none")
.attr("stroke", chart.color)
.attr("stroke-width", 2)
.attr("d", chart.line);
}
// リサイズイベントの設定
window.addEventListener('resize', () => {
resizeCharts();
});
}
initCharts();
// -------------------------------
// シリアルポートへの接続とデータ読み取り
// -------------------------------
const connectButton = document.getElementById('connectButton');
connectButton.addEventListener('click', async () => {
try {
// シリアルポートのリクエスト
const port = await navigator.serial.requestPort();
// シリアルポートを開く
await port.open({ baudRate: 9600 });
// テキストストリームの作成
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// データの読み取り
while (true) {
const { value, done } = await reader.read();
if (done) {
// 読み取りストリームが終了した場合
break;
}
if (value) {
// 改行でデータを分割
const lines = value.split('\n');
lines.forEach(line => processData(line.trim()));
}
}
} catch (error) {
console.error('シリアルポート接続エラー:', error);
alert('シリアルポートの接続に失敗しました。コンソールを確認してください。');
}
});
// -------------------------------
// 正規表現の適用
// -------------------------------
const applyRegexButton = document.getElementById('applyRegexButton');
const regexInput = document.getElementById('regexInput');
const regexMessage = document.getElementById('regexMessage');
applyRegexButton.addEventListener('click', () => {
const userRegex = regexInput.value.trim();
if (!userRegex) {
regexMessage.textContent = '正規表現を入力してください。';
regexMessage.style.color = 'red';
return;
}
try {
// 新しい正規表現をコンパイル
const newPattern = new RegExp(userRegex);
// テストデータで正規表現を確認(3つのキャプチャグループがあるか)
const testMatch = newPattern.exec('Left Sensor: 123 | Center Sensor: 456 | Right Sensor: 789');
if (!testMatch || testMatch.length < 4) {
throw new Error('正規表現には3つのキャプチャグループが必要です。');
}
// 正規表現を更新
sensorPattern = newPattern;
regexMessage.style.color = 'green';
regexMessage.textContent = '正規表現が正常に適用されました。';
} catch (e) {
console.error('正規表現エラー:', e);
regexMessage.style.color = 'red';
regexMessage.textContent = `正規表現エラー: ${e.message}`;
}
});
// -------------------------------
// データの処理関数
// -------------------------------
let lastLeft = null;
let lastCenter = null;
let lastRight = null;
function processData(line) {
if (!line) return; // 空行は無視
const match = sensorPattern.exec(line);
if (match) {
if (match.length < 4) {
console.warn('正規表現が期待するキャプチャグループを含んでいません。');
return;
}
const left = parseInt(match[1], 10);
const center = parseInt(match[2], 10);
const right = parseInt(match[3], 10);
// データが一定かどうかをチェック
if (left === lastLeft && center === lastCenter && right === lastRight) {
console.log(`データが一定です。Rawデータ: Left=${left}, Center=${center}, Right=${right}`);
appendRawData(left, center, right);
// 一定データの場合、グラフの更新をスキップ
return;
}
// データの追加
leftData.push(left);
centerData.push(center);
rightData.push(right);
timeData.push(currentTime);
currentTime += 1;
// データの保持数を制限
if (leftData.length > MAX_POINTS) {
leftData.shift();
centerData.shift();
rightData.shift();
timeData.shift();
}
// グラフの更新
updateCharts();
// 最後のデータを更新
lastLeft = left;
lastCenter = center;
lastRight = right;
} else {
console.warn('不正なデータ形式:', line);
}
}
// -------------------------------
// Rawデータの表示関数
// -------------------------------
function appendRawData(left, center, right) {
const rawDataDisplay = document.getElementById('rawDataDisplay');
const rawData = `Left=${left}, Center=${center}, Right=${right}\n`;
rawDataDisplay.textContent += rawData;
// スクロールを最新にする
rawDataDisplay.scrollTop = rawDataDisplay.scrollHeight;
}
// -------------------------------
// グラフの更新関数
// -------------------------------
function updateCharts() {
for (let key in charts) {
const chart = charts[key];
const svgElement = chart.svg.node();
const boundingRect = svgElement.getBoundingClientRect();
const width = boundingRect.width;
const height = boundingRect.height;
// スケールの再設定
chart.xScale.range([50, width - 50]);
chart.yScale.range([height - 30, 20]); // マージンを考慮
// Yスケールの更新
let yMin, yMax;
if (includeZeroCheckbox.checked) {
yMin = d3.min(chart.data.concat([0])) || 0;
yMax = d3.max(chart.data.concat([0])) || 1023;
} else {
yMin = d3.min(chart.data) || 0;
yMax = d3.max(chart.data) || 1023;
}
chart.yScale.domain([Math.min(yMin, 0), Math.max(yMax, 0)]);
// Y軸の再描画
chart.svg.select(".y-axis")
.transition()
.duration(500)
.call(d3.axisLeft(chart.yScale));
// パスの更新
chart.path
.datum(chart.data)
.transition()
.duration(500)
.attr("d", chart.line);
}
}
// -------------------------------
// グラフのリサイズ関数
// -------------------------------
function resizeCharts() {
for (let key in charts) {
const chart = charts[key];
const svgElement = chart.svg.node();
const boundingRect = svgElement.getBoundingClientRect();
const width = boundingRect.width;
const height = boundingRect.height;
// スケールの更新
chart.xScale.range([50, width - 50]);
chart.yScale.range([height - 30, 20]); // マージンを考慮
// Yスケールの更新
let yMin, yMax;
if (includeZeroCheckbox.checked) {
yMin = d3.min(chart.data.concat([0])) || 0;
yMax = d3.max(chart.data.concat([0])) || 1023;
} else {
yMin = d3.min(chart.data) || 0;
yMax = d3.max(chart.data) || 1023;
}
chart.yScale.domain([Math.min(yMin, 0), Math.max(yMax, 0)]);
// Y軸の再描画
chart.svg.select(".y-axis")
.transition()
.duration(500)
.call(d3.axisLeft(chart.yScale));
// パスの更新
chart.path
.datum(chart.data)
.transition()
.duration(500)
.attr("d", chart.line);
}
}
</script>
</body>
</html>