ek15072809's picture
Upload pv17-3.html
119b9a0 verified
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arduino シリアル通信</title>
<style>
:root {
--primary-color: #1e88e5;
--primary-hover: #1565c0;
--background-color: #ffffff; /* 背景を白に */
--text-color: #212121;
--border-color: #e0e0e0;
--placeholder-color: #757575;
--error-color: #d32f2f;
--success-color: #388e3c;
--input-background: #fafafa; /* 入力欄とテキストエリアの統一背景 */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* paddingに関連するスタイル */
body {
padding: 16px;
}
.container {
padding: 32px;
}
button {
padding: 12px;
}
input[type="text"] {
padding: 12px 16px;
}
textarea {
padding: 16px;
}
@media (max-width: 600px) {
.container {
padding: 24px;
}
button {
padding: 12px;
}
}
/* その他のスタイル */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--background-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
line-height: 1.5;
}
.container {
background: var(--background-color); /* 背景と統一(白) */
width: 100%;
max-width: 900px;
display: grid;
grid-template-columns: 1fr 2fr;
gap: 24px;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 16px;
}
.main {
display: flex;
flex-direction: column;
gap: 16px;
position: relative; /* コピーボタンの位置指定用 */
}
h2 {
text-align: center;
font-size: 1.8rem;
margin-bottom: 24px;
color: var(--text-color);
font-weight: 600;
grid-column: 1 / -1;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
button {
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease;
flex: 1;
min-width: 100px;
}
button:hover {
background: var(--primary-hover);
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #b0bec5;
cursor: not-allowed;
}
select, input[type="text"] {
width: 100%;
font-size: 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 16px;
background: var(--input-background);
padding: 12px 16px;
transition: border-color 0.2s ease;
}
select:focus, input[type="text"]:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
input[type="text"]::placeholder {
color: var(--placeholder-color);
}
textarea {
width: 100%;
height: 300px; /* 受信データの高さ */
font-size: 0.95rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--input-background);
resize: none;
line-height: 1.5;
transition: border-color 0.2s ease;
}
textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
.copy-button {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
padding: 4px;
}
.copy-button img {
width: 16px;
height: 16px;
/* 変色(不透明度変化)なし */
}
.status {
text-align: center;
margin-bottom: 16px;
font-size: 0.9rem;
color: var(--placeholder-color);
}
.status.connected {
color: var(--success-color);
}
.error {
color: var(--error-color);
}
.history {
height: 123px; /* テキスト3行分 */
border: 1px solid var(--border-color);
border-radius: 1px;
padding: 1px;
background: var(--input-background);
/* スクロール無効化 */
}
.history-item {
padding: 8px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.history-item:hover {
background: rgba(0, 0, 0, 0.05);
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
h2 {
font-size: 1.5rem;
}
.button-group {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<div class="sidebar">
<div class="status" id="status">未接続</div>
<select id="baudRate">
<option value="9600" selected>9600</option>
<option value="19200">19200</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
<option value="115200">115200</option>
</select>
<div class="button-group">
<button id="connect">接続</button>
<button id="disconnect" disabled>切断</button>
<button id="clear">クリア</button>
<button id="export">エクスポート</button>
</div>
<div class="history" id="history">
<div class="history-item" style="color: var(--placeholder-color);">送信履歴なし</div>
</div>
</div>
<div class="main">
<input type="text" id="input" placeholder="Enterで送信" disabled aria-label="コマンド入力">
<div style="position: relative;">
<textarea id="output" readonly placeholder="受信データが表示されます..." aria-label="受信データ"></textarea>
<div class="copy-button" id="copyButton" title="受信データをコピー">
<img src="copy2.png" alt="コピーアイコン">
</div>
</div>
</div>
</div>
<script>
// デバッグ用ログ
console.log('スクリプトの読み込み開始');
let port, writer, reader;
let keepReading = false;
let isConnected = false;
let commandHistory = []; // セッション内でのみ保持
let receiveBuffer = ''; // 受信データバッファ
// DOM要素の取得
const connectButton = document.getElementById('connect');
const disconnectButton = document.getElementById('disconnect');
const clearButton = document.getElementById('clear');
const exportButton = document.getElementById('export');
const copyButton = document.getElementById('copyButton');
const inputField = document.getElementById('input');
const outputArea = document.getElementById('output');
const statusDisplay = document.getElementById('status');
const baudRateSelect = document.getElementById('baudRate');
const historyContainer = document.getElementById('history');
// DOM要素の存在確認
console.log('DOM要素取得:', {
connectButton, disconnectButton, clearButton, exportButton,
copyButton, inputField, outputArea, statusDisplay, baudRateSelect, historyContainer
});
// 初期ロード時に履歴を反映
updateHistoryUI();
connectButton.addEventListener('click', async () => {
console.log('接続ボタンクリック');
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: parseInt(baudRateSelect.value) });
writer = port.writable.getWriter();
reader = port.readable.getReader();
keepReading = true;
isConnected = true;
updateUI(true);
statusDisplay.textContent = `接続済み (ボーレート: ${baudRateSelect.value})`;
statusDisplay.classList.add('connected');
statusDisplay.classList.remove('error');
const decoder = new TextDecoder();
while (keepReading) {
const { value, done } = await reader.read();
if (done) break;
if (value) {
const text = decoder.decode(value, { stream: true });
console.log('受信データ(生):', text);
displayFilteredOutput(text);
}
}
} catch (err) {
console.error('接続エラー:', err);
statusDisplay.textContent = `接続エラー: ${err.message}`;
statusDisplay.classList.add('error');
statusDisplay.classList.remove('connected');
updateUI(false);
}
});
disconnectButton.addEventListener('click', async () => {
console.log('切断ボタンクリック');
try {
keepReading = false;
if (reader) await reader.releaseLock();
if (writer) await writer.releaseLock();
if (port) await port.close();
isConnected = false;
updateUI(false);
statusDisplay.textContent = '未接続';
statusDisplay.classList.remove('connected', 'error');
receiveBuffer = ''; // バッファをクリア
} catch (err) {
console.error('切断エラー:', err);
statusDisplay.textContent = `切断エラー: ${err.message}`;
statusDisplay.classList.add('error');
}
});
clearButton.addEventListener('click', () => {
console.log('クリアボタンクリック');
outputArea.value = '';
receiveBuffer = ''; // バッファをクリア
console.log('受信データクリア成功');
});
exportButton.addEventListener('click', () => {
console.log('エクスポートボタンクリック');
try {
if (outputArea.value.trim().length === 0) {
statusDisplay.textContent = 'エクスポートするデータがありません';
statusDisplay.classList.add('error');
setTimeout(() => {
if (isConnected) {
statusDisplay.textContent = `接続済み (ボーレート: ${baudRateSelect.value})`;
statusDisplay.classList.add('connected');
statusDisplay.classList.remove('error');
} else {
statusDisplay.textContent = '未接続';
statusDisplay.classList.remove('error');
}
}, 2000);
return;
}
const blob = new Blob([outputArea.value], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `serial_data_${new Date().toISOString()}.txt`;
a.click();
URL.revokeObjectURL(url);
console.log('エクスポート成功');
} catch (e) {
console.error('エクスポートエラー:', e);
statusDisplay.textContent = `エクスポートエラー: ${e.message}`;
statusDisplay.classList.add('error');
}
});
copyButton.addEventListener('click', async () => {
console.log('コピーボタンクリック');
try {
await navigator.clipboard.writeText(outputArea.value);
statusDisplay.textContent = '受信データをコピーしました';
statusDisplay.classList.add('connected');
statusDisplay.classList.remove('error');
console.log('コピー成功');
setTimeout(() => {
if (isConnected) {
statusDisplay.textContent = `接続済み (ボーレート: ${baudRateSelect.value})`;
} else {
statusDisplay.textContent = '未接続';
statusDisplay.classList.remove('connected');
}
}, 2000);
} catch (err) {
console.error('コピーエラー:', err);
statusDisplay.textContent = `コピーエラー: ${err.message}`;
statusDisplay.classList.add('error');
}
});
inputField.addEventListener('keydown', async (e) => {
if (e.key === 'Enter') {
console.log('Enterキー押下:', inputField.value);
e.preventDefault();
const text = inputField.value.trim();
if (text && writer) {
try {
const encoder = new TextEncoder();
await writer.write(encoder.encode(text + '\n'));
updateCommandHistory(text);
inputField.value = '';
} catch (e) {
console.error('送信エラー:', e);
statusDisplay.textContent = `送信エラー: ${e.message}`;
statusDisplay.classList.add('error');
}
}
}
});
historyContainer.addEventListener('click', async (e) => {
if (e.target.classList.contains('history-item') && isConnected) {
console.log('履歴アイテムクリック:', e.target.textContent);
const text = e.target.textContent;
if (text && writer) {
try {
const encoder = new TextEncoder();
await writer.write(encoder.encode(text + '\n'));
console.log('履歴から送信成功:', text);
updateCommandHistory(text); // 履歴を再追加(最新を先頭に)
} catch (e) {
console.error('履歴送信エラー:', e);
statusDisplay.textContent = `送信エラー: ${e.message}`;
statusDisplay.classList.add('error');
}
}
}
});
function updateCommandHistory(text) {
if (!commandHistory.includes(text)) {
commandHistory.unshift(text); // 最新を先頭に追加
} else {
// 既存のコマンドを先頭に移動
commandHistory = commandHistory.filter(cmd => cmd !== text);
commandHistory.unshift(text);
}
// 直近3件に制限
if (commandHistory.length > 3) {
commandHistory = commandHistory.slice(0, 3);
}
console.log('履歴更新:', commandHistory);
updateHistoryUI();
}
function displayFilteredOutput(rawText) {
// 受信データをバッファに追加
receiveBuffer += rawText;
console.log('バッファ更新:', receiveBuffer);
// バッファを改行で分割
const lines = receiveBuffer.split(/\r?\n/);
// 最後の要素が不完全な行(改行で終わっていない)の場合、バッファに残す
if (!rawText.endsWith('\n')) {
receiveBuffer = lines.pop() || '';
} else {
receiveBuffer = '';
}
// 空行(空白のみ)を除外し、表示
const filteredLines = lines.filter(line => line.trim().length > 0);
if (filteredLines.length > 0) {
if (outputArea.value) {
outputArea.value += '\n' + filteredLines.join('\n');
} else {
outputArea.value += filteredLines.join('\n');
}
outputArea.scrollTop = outputArea.scrollHeight;
}
console.log('表示データ:', filteredLines);
}
function updateHistoryUI() {
console.log('履歴UI更新');
historyContainer.innerHTML = '';
if (commandHistory.length === 0) {
historyContainer.innerHTML = '<div class="history-item" style="color: var(--placeholder-color);">送信履歴なし</div>';
} else {
commandHistory.forEach(cmd => {
const div = document.createElement('div');
div.className = 'history-item';
div.textContent = cmd;
historyContainer.appendChild(div);
});
}
}
function updateUI(connected) {
console.log('UI更新:', connected);
connectButton.disabled = connected;
disconnectButton.disabled = !connected;
// clearButtonとexportButtonは常に有効
inputField.disabled = !connected;
baudRateSelect.disabled = connected;
// copyButtonは常に有効
}
window.addEventListener('beforeunload', async () => {
console.log('ページ終了処理');
keepReading = false;
if (reader) await reader.releaseLock();
if (writer) await writer.releaseLock();
if (port) await port.close();
receiveBuffer = ''; // バッファをクリア
});
console.log('スクリプトの読み込み完了');
</script>
</body>
</html>