346 lines
16 KiB
HTML
346 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Lasertag Provisioning</title>
|
|
<style>
|
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; align-items: center; padding: 2rem; background: #f0f2f5; color: #333; }
|
|
.card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 100%; max-width: 550px; }
|
|
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #1a73e8; }
|
|
.status-box { padding: 0.75rem; border-radius: 6px; margin-bottom: 1.5rem; font-size: 0.9rem; background: #e8f0fe; color: #1967d2; border: 1px solid #d2e3fc; min-height: 20px; }
|
|
.button-group { display: flex; gap: 10px; width: 100%; flex-wrap: wrap; }
|
|
button { background: #1a73e8; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 1rem; width: 100%; font-weight: 600; transition: background 0.2s; margin-top: 10px; }
|
|
button:hover { background: #1557b0; }
|
|
button:disabled { background: #ccc; cursor: not-allowed; }
|
|
.btn-danger { background: #d93025; }
|
|
.btn-danger:hover { background: #a50e0e; }
|
|
.btn-secondary { background: #5f6368; }
|
|
.btn-secondary:hover { background: #474a4d; }
|
|
.btn-success { background: #188038; }
|
|
.btn-success:hover { background: #13652c; }
|
|
|
|
.device-info { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 2px solid #f0f2f5; display: none; }
|
|
.config-section { margin-top: 1.5rem; }
|
|
.config-group { margin-bottom: 1rem; border-bottom: 1px solid #eee; padding-bottom: 1rem; }
|
|
.config-group:last-child { border-bottom: none; }
|
|
label { display: block; font-size: 0.85rem; color: #666; margin-bottom: 4px; font-weight: bold; }
|
|
.input-row { display: flex; gap: 8px; }
|
|
input { flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 0.9rem; font-family: monospace; }
|
|
.btn-small { width: auto; font-size: 0.85rem; padding: 8px 16px; margin-top: 0; }
|
|
.hint { font-size: 0.8rem; color: #888; margin-top: 1rem; font-style: italic; }
|
|
.action-bar { display: flex; gap: 10px; margin-bottom: 15px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h1>Lasertag Konfiguration</h1>
|
|
<div class="status-box" id="status">Bereit zum Scannen...</div>
|
|
|
|
<div class="button-group">
|
|
<button id="connectBtn">Gerät suchen</button>
|
|
<button id="disconnectBtn" class="btn-danger" style="display: none;">Trennen</button>
|
|
</div>
|
|
|
|
<div class="config-section" id="globalActions" style="margin-top: 20px;">
|
|
<label>Globale Aktionen</label>
|
|
<div class="button-group">
|
|
<button id="genParamsBtn" class="btn-secondary">Zufallsparameter generieren</button>
|
|
<button id="saveToCookieBtn" class="btn-secondary">Netzwerk im Browser merken</button>
|
|
<button id="loadFromCookieBtn" class="btn-success" style="display: none;">Gemerktes Netzwerk auf Node schreiben</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="device-info" id="deviceInfo">
|
|
<div class="config-section">
|
|
<!-- Device Name -->
|
|
<div class="config-group">
|
|
<label>Gerätename (ID)</label>
|
|
<div class="input-row">
|
|
<input type="text" id="nameInput" maxlength="31" placeholder="z.B. Alpha-Wolf">
|
|
<button class="btn-small" id="saveNameBtn">Setzen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Thread Network Name -->
|
|
<div class="config-group">
|
|
<label>Thread Netzwerk Name</label>
|
|
<div class="input-row">
|
|
<input type="text" id="netNameInput" maxlength="16" placeholder="z.B. MyThreadNet">
|
|
<button class="btn-small" id="saveNetNameBtn">Setzen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Thread PAN ID -->
|
|
<div class="config-group">
|
|
<label>Thread PAN ID (Hex, 4 Stellen)</label>
|
|
<div class="input-row">
|
|
<input type="text" id="panInput" maxlength="4" placeholder="abcd">
|
|
<button class="btn-small" id="savePanBtn">Setzen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Thread Channel -->
|
|
<div class="config-group">
|
|
<label>Thread Kanal (11-26)</label>
|
|
<div class="input-row">
|
|
<input type="number" id="chanInput" min="11" max="26">
|
|
<button class="btn-small" id="saveChanBtn">Setzen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Extended PAN ID -->
|
|
<div class="config-group">
|
|
<label>Extended PAN ID (Hex, 16 Stellen)</label>
|
|
<div class="input-row">
|
|
<input type="text" id="extPanInput" maxlength="16" placeholder="deadbeefcafebabe">
|
|
<button class="btn-small" id="saveExtPanBtn">Setzen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Key -->
|
|
<div class="config-group">
|
|
<label>Network Key (Hex, 32 Stellen)</label>
|
|
<div class="input-row">
|
|
<input type="text" id="keyInput" maxlength="32" placeholder="00112233445566778899aabbccddeeff">
|
|
<button class="btn-small" id="saveKeyBtn">Setzen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="hint">Hinweis: Alle Änderungen werden im NVS gespeichert. Ein Reboot ist erforderlich, um Thread neu zu starten.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const connectBtn = document.getElementById('connectBtn');
|
|
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
const status = document.getElementById('status');
|
|
const deviceInfo = document.getElementById('deviceInfo');
|
|
const saveToCookieBtn = document.getElementById('saveToCookieBtn');
|
|
const loadFromCookieBtn = document.getElementById('loadFromCookieBtn');
|
|
const genParamsBtn = document.getElementById('genParamsBtn');
|
|
|
|
const nameInput = document.getElementById('nameInput');
|
|
const netNameInput = document.getElementById('netNameInput');
|
|
const panInput = document.getElementById('panInput');
|
|
const chanInput = document.getElementById('chanInput');
|
|
const extPanInput = document.getElementById('extPanInput');
|
|
const keyInput = document.getElementById('keyInput');
|
|
|
|
let bluetoothDevice = null;
|
|
let provService = null;
|
|
|
|
// UUIDs passend zur Firmware
|
|
const SERVICE_UUID = '03afe2cf-6c64-4a22-9289-c3ae820cbc00';
|
|
const NAME_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc01';
|
|
const PANID_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc02';
|
|
const CHAN_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc03';
|
|
const EXTPAN_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc04';
|
|
const NETKEY_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc05';
|
|
const NETNAME_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc06';
|
|
|
|
// Check if we have saved network settings
|
|
function updateLoadButtonVisibility() {
|
|
const saved = localStorage.getItem('lasertag_net_config');
|
|
loadFromCookieBtn.style.display = (saved && bluetoothDevice && bluetoothDevice.gatt.connected) ? 'inline-block' : 'none';
|
|
}
|
|
|
|
function hexToBytes(hex) {
|
|
let bytes = [];
|
|
for (let c = 0; c < hex.length; c += 2)
|
|
bytes.push(parseInt(hex.substr(c, 2), 16));
|
|
return new Uint8Array(bytes);
|
|
}
|
|
|
|
function bytesToHex(uint8arr) {
|
|
return Array.from(uint8arr).map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
}
|
|
|
|
function generateRandomHex(len) {
|
|
const arr = new Uint8Array(len / 2);
|
|
window.crypto.getRandomValues(arr);
|
|
return bytesToHex(arr);
|
|
}
|
|
|
|
async function readAll() {
|
|
try {
|
|
status.textContent = 'Lese Konfiguration...';
|
|
|
|
// Name
|
|
const nameData = await (await provService.getCharacteristic(NAME_CHAR)).readValue();
|
|
nameInput.value = new TextDecoder().decode(nameData);
|
|
|
|
// Thread Net Name
|
|
const netNameData = await (await provService.getCharacteristic(NETNAME_CHAR)).readValue();
|
|
netNameInput.value = new TextDecoder().decode(netNameData);
|
|
|
|
// PAN ID
|
|
const panData = await (await provService.getCharacteristic(PANID_CHAR)).readValue();
|
|
panInput.value = panData.getUint16(0, true).toString(16).padStart(4, '0').toUpperCase();
|
|
|
|
// Kanal
|
|
const chanData = await (await provService.getCharacteristic(CHAN_CHAR)).readValue();
|
|
chanInput.value = chanData.getUint8(0);
|
|
|
|
// Ext PAN ID
|
|
const extData = await (await provService.getCharacteristic(EXTPAN_CHAR)).readValue();
|
|
extPanInput.value = bytesToHex(new Uint8Array(extData.buffer));
|
|
|
|
// Net Key
|
|
const keyData = await (await provService.getCharacteristic(NETKEY_CHAR)).readValue();
|
|
keyInput.value = bytesToHex(new Uint8Array(keyData.buffer));
|
|
|
|
status.textContent = 'Verbunden mit ' + bluetoothDevice.name;
|
|
updateLoadButtonVisibility();
|
|
} catch (e) {
|
|
status.textContent = 'Fehler beim Lesen: ' + e.message;
|
|
}
|
|
}
|
|
|
|
connectBtn.addEventListener('click', async () => {
|
|
try {
|
|
bluetoothDevice = await navigator.bluetooth.requestDevice({
|
|
filters: [{ services: [SERVICE_UUID] }]
|
|
});
|
|
|
|
status.textContent = 'Verbinde...';
|
|
const server = await bluetoothDevice.gatt.connect();
|
|
provService = await server.getPrimaryService(SERVICE_UUID);
|
|
|
|
deviceInfo.style.display = 'block';
|
|
disconnectBtn.style.display = 'inline-block';
|
|
connectBtn.style.display = 'none';
|
|
|
|
await readAll();
|
|
|
|
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
|
|
} catch (error) {
|
|
status.textContent = 'Suche abgebrochen oder Fehler: ' + error.message;
|
|
}
|
|
});
|
|
|
|
disconnectBtn.addEventListener('click', () => {
|
|
if (bluetoothDevice && bluetoothDevice.gatt.connected) {
|
|
bluetoothDevice.gatt.disconnect();
|
|
}
|
|
});
|
|
|
|
function onDisconnected() {
|
|
status.textContent = 'Verbindung getrennt.';
|
|
deviceInfo.style.display = 'none';
|
|
disconnectBtn.style.display = 'none';
|
|
connectBtn.style.display = 'inline-block';
|
|
provService = null;
|
|
updateLoadButtonVisibility();
|
|
}
|
|
|
|
async function writeValue(uuid, data) {
|
|
if (!provService) return;
|
|
try {
|
|
status.textContent = 'Speichere...';
|
|
const char = await provService.getCharacteristic(uuid);
|
|
await char.writeValue(data);
|
|
status.textContent = 'Erfolgreich gespeichert!';
|
|
setTimeout(() => { if(provService) status.textContent = 'Verbunden.'; }, 2000);
|
|
} catch (e) {
|
|
status.textContent = 'Speichern fehlgeschlagen: ' + e.message;
|
|
}
|
|
}
|
|
|
|
// Global Action: Generate Random Params
|
|
genParamsBtn.addEventListener('click', () => {
|
|
panInput.value = generateRandomHex(4);
|
|
extPanInput.value = generateRandomHex(16);
|
|
keyInput.value = generateRandomHex(32);
|
|
chanInput.value = Math.floor(Math.random() * (26 - 11 + 1)) + 11;
|
|
status.textContent = 'Zufallsparameter generiert.';
|
|
});
|
|
|
|
// Global Action: Save to Local Storage
|
|
saveToCookieBtn.addEventListener('click', () => {
|
|
const config = {
|
|
netName: netNameInput.value,
|
|
panId: panInput.value,
|
|
channel: chanInput.value,
|
|
extPanId: extPanInput.value,
|
|
netKey: keyInput.value
|
|
};
|
|
localStorage.setItem('lasertag_net_config', JSON.stringify(config));
|
|
status.textContent = 'Netzwerk-Einstellungen lokal gemerkt.';
|
|
updateLoadButtonVisibility();
|
|
});
|
|
|
|
// Global Action: Load and Write to Node
|
|
loadFromCookieBtn.addEventListener('click', async () => {
|
|
const saved = localStorage.getItem('lasertag_net_config');
|
|
if (!saved || !provService) return;
|
|
|
|
try {
|
|
const config = JSON.parse(saved);
|
|
status.textContent = 'Schreibe gemerktes Netzwerk...';
|
|
|
|
// Write each setting
|
|
const encoder = new TextEncoder();
|
|
|
|
// Net Name
|
|
await (await provService.getCharacteristic(NETNAME_CHAR)).writeValue(encoder.encode(config.netName));
|
|
|
|
// PAN ID
|
|
const panView = new DataView(new ArrayBuffer(2));
|
|
panView.setUint16(0, parseInt(config.panId, 16), true);
|
|
await (await provService.getCharacteristic(PANID_CHAR)).writeValue(panView.buffer);
|
|
|
|
// Channel
|
|
const chanView = new DataView(new ArrayBuffer(1));
|
|
chanView.setUint8(0, parseInt(config.channel));
|
|
await (await provService.getCharacteristic(CHAN_CHAR)).writeValue(chanView.buffer);
|
|
|
|
// Ext PAN
|
|
await (await provService.getCharacteristic(EXTPAN_CHAR)).writeValue(hexToBytes(config.extPanId));
|
|
|
|
// Net Key
|
|
await (await provService.getCharacteristic(NETKEY_CHAR)).writeValue(hexToBytes(config.netKey));
|
|
|
|
status.textContent = 'Netzwerk vollständig übertragen!';
|
|
|
|
// Update UI fields to show what was written
|
|
netNameInput.value = config.netName;
|
|
panInput.value = config.panId;
|
|
chanInput.value = config.channel;
|
|
extPanInput.value = config.extPanId;
|
|
keyInput.value = config.netKey;
|
|
|
|
} catch (e) {
|
|
status.textContent = 'Fehler beim Batch-Schreiben: ' + e.message;
|
|
}
|
|
});
|
|
|
|
// Manual Saves
|
|
document.getElementById('saveNameBtn').addEventListener('click', () =>
|
|
writeValue(NAME_CHAR, new TextEncoder().encode(nameInput.value)));
|
|
|
|
document.getElementById('saveNetNameBtn').addEventListener('click', () =>
|
|
writeValue(NETNAME_CHAR, new TextEncoder().encode(netNameInput.value)));
|
|
|
|
document.getElementById('savePanBtn').addEventListener('click', () => {
|
|
const view = new DataView(new ArrayBuffer(2));
|
|
view.setUint16(0, parseInt(panInput.value, 16), true);
|
|
writeValue(PANID_CHAR, view.buffer);
|
|
});
|
|
|
|
document.getElementById('saveChanBtn').addEventListener('click', () => {
|
|
const view = new DataView(new ArrayBuffer(1));
|
|
view.setUint8(0, parseInt(chanInput.value));
|
|
writeValue(CHAN_CHAR, view.buffer);
|
|
});
|
|
|
|
document.getElementById('saveExtPanBtn').addEventListener('click', () =>
|
|
writeValue(EXTPAN_CHAR, hexToBytes(extPanInput.value)));
|
|
|
|
document.getElementById('saveKeyBtn').addEventListener('click', () =>
|
|
writeValue(NETKEY_CHAR, hexToBytes(keyInput.value)));
|
|
|
|
</script>
|
|
</body>
|
|
</html> |