212 lines
6.8 KiB
HTML
212 lines
6.8 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>ESP32 BME680 + ADXL345 BLE Dashboard</title>
|
|
<style>
|
|
body { font-family: sans-serif; max-width: 760px; margin: 24px auto; padding: 0 16px; }
|
|
button { padding: 12px 16px; margin: 6px 6px 6px 0; font-size: 16px; }
|
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
.card { border: 1px solid #ddd; border-radius: 10px; padding: 14px; }
|
|
.big { font-size: 34px; font-weight: 700; }
|
|
.label { color: #666; margin-bottom: 6px; }
|
|
.status { white-space: pre-wrap; background:#f7f7f7; padding:10px; border-radius:8px; }
|
|
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
|
|
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>ESP32 BME680 + ADXL345 (BLE)</h2>
|
|
|
|
<div class="card">
|
|
<button id="btnConnect">Connect</button>
|
|
<button id="btnDisconnect" disabled>Disconnect</button>
|
|
<div>Device: <span id="deviceName">—</span></div>
|
|
<div>Connection: <span id="connState">disconnected</span></div>
|
|
</div>
|
|
|
|
<div class="grid" style="margin-top:12px;">
|
|
<div class="card">
|
|
<div class="label">Temperature</div>
|
|
<div class="big"><span id="t">—</span> °C</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">Humidity</div>
|
|
<div class="big"><span id="h">—</span> %</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">Pressure</div>
|
|
<div class="big"><span id="p">—</span> hPa</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">Gas Resistance</div>
|
|
<div class="big"><span id="g">—</span> Ω</div>
|
|
<div>Valid: <span id="gv">—</span></div>
|
|
</div>
|
|
|
|
<div class="card" style="grid-column: 1 / -1;">
|
|
<div class="label">ADXL345</div>
|
|
<div class="big">Shake: <span id="shake">—</span></div>
|
|
<div class="mono">ax=<span id="ax">—</span> g ay=<span id="ay">—</span> g az=<span id="az">—</span> g</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:12px;">
|
|
<div class="label">LED Control</div>
|
|
<button class="cmd" data-cmd="0" disabled>OFF</button>
|
|
<button class="cmd" data-cmd="1" disabled>RED</button>
|
|
<button class="cmd" data-cmd="2" disabled>GREEN</button>
|
|
<button class="cmd" data-cmd="3" disabled>BLUE</button>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:12px;">
|
|
<div class="label">Log</div>
|
|
<div class="status" id="log"></div>
|
|
</div>
|
|
|
|
<script>
|
|
(() => {
|
|
const SERVICE_UUID = '19b10000-e8f2-537e-4f6c-d104768a1214';
|
|
const SENSOR_CHAR_UUID = '19b10001-e8f2-537e-4f6c-d104768a1214';
|
|
const LED_CHAR_UUID = '19b10002-e8f2-537e-4f6c-d104768a1214';
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
const logEl = $('log');
|
|
const td = new TextDecoder('utf-8');
|
|
|
|
let device, server, sensorChar, ledChar;
|
|
|
|
function log(...args) {
|
|
const msg = args.join(' ');
|
|
console.log(...args);
|
|
logEl.textContent = (msg + '\n' + logEl.textContent).slice(0, 8000);
|
|
}
|
|
|
|
function setConnectedUI(on) {
|
|
$('connState').textContent = on ? 'connected' : 'disconnected';
|
|
$('btnDisconnect').disabled = !on;
|
|
document.querySelectorAll('button.cmd').forEach(b => b.disabled = !on);
|
|
}
|
|
|
|
function setText(id, v) { $(id).textContent = (v === null || v === undefined) ? '—' : String(v); }
|
|
function setNum(id, v, d=2) {
|
|
if (v === null || v === undefined) return setText(id, '—');
|
|
if (typeof v === 'number') return setText(id, v.toFixed(d));
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return setText(id, v);
|
|
setText(id, n.toFixed(d));
|
|
}
|
|
|
|
function updateUI(o) {
|
|
// BME
|
|
setNum('t', o.t_c, 2);
|
|
setNum('h', o.h_pct, 2);
|
|
setNum('p', o.p_hpa, 2);
|
|
setText('g', (o.gas_ohms === null || o.gas_ohms === undefined) ? '—' : o.gas_ohms);
|
|
setText('gv', o.gas_valid ? 'true' : 'false');
|
|
|
|
// ADXL
|
|
setNum('ax', o.ax, 3);
|
|
setNum('ay', o.ay, 3);
|
|
setNum('az', o.az, 3);
|
|
setNum('shake', o.shake, 3);
|
|
}
|
|
|
|
function extractJson(text) {
|
|
// sometimes you get extra junk; grab first {...} block
|
|
const s = text.indexOf('{');
|
|
const e = text.lastIndexOf('}');
|
|
if (s >= 0 && e > s) return text.slice(s, e + 1);
|
|
return text;
|
|
}
|
|
|
|
function onNotify(event) {
|
|
try {
|
|
const dv = event.target.value;
|
|
const bytes = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
|
let text = td.decode(bytes).trim();
|
|
|
|
// show raw for debugging
|
|
log('RX raw:', text.slice(0, 120));
|
|
|
|
text = extractJson(text);
|
|
const obj = JSON.parse(text);
|
|
updateUI(obj);
|
|
} catch (e) {
|
|
log('Notify parse error:', e);
|
|
}
|
|
}
|
|
|
|
async function connect() {
|
|
try {
|
|
log('Requesting device...');
|
|
device = await navigator.bluetooth.requestDevice({
|
|
filters: [{ services: [SERVICE_UUID] }]
|
|
});
|
|
|
|
$('deviceName').textContent = device.name || '(no name)';
|
|
device.addEventListener('gattserverdisconnected', () => {
|
|
log('Disconnected.');
|
|
setConnectedUI(false);
|
|
});
|
|
|
|
log('Connecting GATT...');
|
|
server = await device.gatt.connect();
|
|
|
|
log('Getting service...');
|
|
const service = await server.getPrimaryService(SERVICE_UUID);
|
|
|
|
log('Getting characteristics...');
|
|
sensorChar = await service.getCharacteristic(SENSOR_CHAR_UUID);
|
|
ledChar = await service.getCharacteristic(LED_CHAR_UUID);
|
|
|
|
log('Start notifications...');
|
|
await sensorChar.startNotifications();
|
|
sensorChar.addEventListener('characteristicvaluechanged', onNotify);
|
|
|
|
// also do one read immediately
|
|
try {
|
|
const dv = await sensorChar.readValue();
|
|
const bytes = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
|
let text = td.decode(bytes).trim();
|
|
log('Initial read:', text.slice(0, 120));
|
|
text = extractJson(text);
|
|
updateUI(JSON.parse(text));
|
|
} catch (e) {
|
|
log('Initial read failed:', e);
|
|
}
|
|
|
|
setConnectedUI(true);
|
|
log('Connected OK.');
|
|
} catch (e) {
|
|
log('Connect error:', e);
|
|
setConnectedUI(false);
|
|
}
|
|
}
|
|
|
|
async function disconnect() {
|
|
try { if (device?.gatt?.connected) device.gatt.disconnect(); }
|
|
finally { setConnectedUI(false); }
|
|
}
|
|
|
|
async function sendLed(cmd) {
|
|
try {
|
|
if (!ledChar) return;
|
|
await ledChar.writeValue(new Uint8Array([cmd & 0xFF]));
|
|
log('TX LED:', cmd);
|
|
} catch (e) {
|
|
log('LED write error:', e);
|
|
}
|
|
}
|
|
|
|
$('btnConnect').addEventListener('click', connect);
|
|
$('btnDisconnect').addEventListener('click', disconnect);
|
|
document.querySelectorAll('button.cmd').forEach(btn => {
|
|
btn.addEventListener('click', () => sendLed(parseInt(btn.dataset.cmd, 10)));
|
|
});
|
|
|
|
setConnectedUI(false);
|
|
})();
|
|
</script>
|
|
|