185 lines
5.5 KiB
HTML
185 lines
5.5 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>FLL Umbrella Env BLE Control</title>
|
|
<style>
|
|
body { font-family: system-ui, sans-serif; margin: 16px; }
|
|
.row { margin: 10px 0; }
|
|
label { display:block; margin-bottom:6px; }
|
|
input[type="range"] { width: 100%; }
|
|
button { padding: 10px 12px; margin-right: 8px; }
|
|
code { background:#f3f3f3; padding:2px 6px; border-radius:6px; }
|
|
.pill { display:inline-block; padding:2px 8px; border-radius: 999px; background:#eee; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>FLL Umbrella Env BLE Control</h2>
|
|
<div class="row">
|
|
<button id="btnConnect">Connect</button>
|
|
<button id="btnDisconnect" disabled>Disconnect</button>
|
|
<span class="pill" id="status">Disconnected</span>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<button id="btnLive" disabled>Set LIVE</button>
|
|
<button id="btnSim" disabled>Set SIM</button>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<label>Temp (°C): <span id="tVal"></span></label>
|
|
<input id="temp" type="range" min="-10.00" max="60.00" step="0.01" value="25.00">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<label>RH (%): <span id="rhVal"></span></label>
|
|
<input id="rh" type="range" min="0.00" max="100.00" step="0.01" value="50.00">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<label>Pressure (hPa): <span id="pVal"></span></label>
|
|
<input id="press" type="range" min="900.0" max="1100.0" step="0.1" value="1013.2">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<label>IAQ override (0..500) [optional]: <span id="iaqVal"></span></label>
|
|
<input id="iaq" type="range" min="0" max="500" step="1" value="25">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<button id="btnSend" disabled>Send SIM Values</button>
|
|
</div>
|
|
|
|
<p>
|
|
BLE Service: <code>12345678-1234-5678-1234-56789abcdef0</code><br/>
|
|
Characteristic: <code>12345678-1234-5678-1234-56789abcdef1</code>
|
|
</p>
|
|
|
|
<script>
|
|
const UUID_SVC = "12345678-1234-5678-1234-56789abcdef0";
|
|
const UUID_CHR = "12345678-1234-5678-1234-56789abcdef1";
|
|
|
|
let device = null;
|
|
let server = null;
|
|
let chr = null;
|
|
|
|
const elStatus = document.getElementById("status");
|
|
const btnConnect = document.getElementById("btnConnect");
|
|
const btnDisconnect = document.getElementById("btnDisconnect");
|
|
const btnLive = document.getElementById("btnLive");
|
|
const btnSim = document.getElementById("btnSim");
|
|
const btnSend = document.getElementById("btnSend");
|
|
|
|
const sTemp = document.getElementById("temp");
|
|
const sRh = document.getElementById("rh");
|
|
const sPress = document.getElementById("press");
|
|
const sIaq = document.getElementById("iaq");
|
|
|
|
const tVal = document.getElementById("tVal");
|
|
const rhVal = document.getElementById("rhVal");
|
|
const pVal = document.getElementById("pVal");
|
|
const iaqVal = document.getElementById("iaqVal");
|
|
|
|
function setUiConnected(connected) {
|
|
btnDisconnect.disabled = !connected;
|
|
btnLive.disabled = !connected;
|
|
btnSim.disabled = !connected;
|
|
btnSend.disabled = !connected;
|
|
elStatus.textContent = connected ? "Connected" : "Disconnected";
|
|
}
|
|
|
|
function refreshLabels() {
|
|
tVal.textContent = Number(sTemp.value).toFixed(2);
|
|
rhVal.textContent = Number(sRh.value).toFixed(2);
|
|
pVal.textContent = Number(sPress.value).toFixed(1);
|
|
iaqVal.textContent = Number(sIaq.value).toFixed(0);
|
|
}
|
|
[sTemp, sRh, sPress, sIaq].forEach(x => x.addEventListener("input", refreshLabels));
|
|
refreshLabels();
|
|
|
|
async function writeBytes(u8) {
|
|
if (!chr) throw new Error("Not connected");
|
|
// Use writeValueWithoutResponse if available, otherwise writeValue
|
|
if (chr.writeValueWithoutResponse) return chr.writeValueWithoutResponse(u8);
|
|
return chr.writeValue(u8);
|
|
}
|
|
|
|
function packMode(mode) {
|
|
return new Uint8Array([0x01, mode ? 1 : 0]);
|
|
}
|
|
|
|
function packSim(tempC, rhPct, pressHpa, iaqOverride) {
|
|
const temp_x100 = Math.round(tempC * 100);
|
|
const rh_x100 = Math.round(rhPct * 100);
|
|
const press_x10 = Math.round(pressHpa * 10);
|
|
const iaq = Math.round(iaqOverride);
|
|
|
|
const buf = new ArrayBuffer(9);
|
|
const dv = new DataView(buf);
|
|
dv.setUint8(0, 0x02);
|
|
dv.setInt16(1, temp_x100, true);
|
|
dv.setUint16(3, rh_x100, true);
|
|
dv.setUint16(5, press_x10, true);
|
|
dv.setUint16(7, iaq, true);
|
|
return new Uint8Array(buf);
|
|
}
|
|
|
|
btnConnect.addEventListener("click", async () => {
|
|
try {
|
|
device = await navigator.bluetooth.requestDevice({
|
|
filters: [{ services: [UUID_SVC] }],
|
|
optionalServices: [UUID_SVC]
|
|
});
|
|
|
|
device.addEventListener("gattserverdisconnected", () => {
|
|
server = null; chr = null;
|
|
setUiConnected(false);
|
|
});
|
|
|
|
server = await device.gatt.connect();
|
|
const svc = await server.getPrimaryService(UUID_SVC);
|
|
chr = await svc.getCharacteristic(UUID_CHR);
|
|
|
|
setUiConnected(true);
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert("Connect failed: " + e);
|
|
setUiConnected(false);
|
|
}
|
|
});
|
|
|
|
btnDisconnect.addEventListener("click", async () => {
|
|
try {
|
|
if (device?.gatt?.connected) device.gatt.disconnect();
|
|
} catch {}
|
|
server = null; chr = null;
|
|
setUiConnected(false);
|
|
});
|
|
|
|
btnLive.addEventListener("click", async () => {
|
|
try {
|
|
await writeBytes(packMode(0));
|
|
} catch (e) { alert("Write failed: " + e); }
|
|
});
|
|
|
|
btnSim.addEventListener("click", async () => {
|
|
try {
|
|
await writeBytes(packMode(1));
|
|
} catch (e) { alert("Write failed: " + e); }
|
|
});
|
|
|
|
btnSend.addEventListener("click", async () => {
|
|
try {
|
|
const t = Number(sTemp.value);
|
|
const rh = Number(sRh.value);
|
|
const p = Number(sPress.value);
|
|
const iaq = Number(sIaq.value);
|
|
await writeBytes(packSim(t, rh, p, iaq));
|
|
} catch (e) { alert("Write failed: " + e); }
|
|
});
|
|
|
|
setUiConnected(false);
|
|
</script>
|
|
</body>
|
|
</html>
|