Architektur, Web Bluetooth API, Designregeln und Fallbacks – ohne Hype
SerieBLE als Gamechanger
Teil 3 von 3
Die ersten beiden Teile dieser Serie haben gezeigt, wo BLE echte Probleme löst. Jetzt geht es ans Eingemachte: Wie baut man das technisch? Welche Architektur funktioniert? Was sind die Fallstricke?
Dieser Artikel ist für Entwickler, die BLE-Features in Web-Apps umsetzen wollen – pragmatisch, ohne Hype.
Der technische Kern
Was ein BLE-Beacon wirklich ist
Ein Beacon ist ein dummes Signal. Mehr nicht.
Der Beacon weiß nichts. Er sendet nur. Die gesamte Intelligenz liegt in der empfangenden Anwendung.
Beacon-Formate:
- iBeacon: Apple-Standard, weit verbreitet
- Eddystone: Google-Standard, kann URLs senden
- AltBeacon: Open Source Alternative
Für Web-Apps ist das Format meist egal – wichtig ist die UUID.
Die drei Schichten
Datenfluss:
- Beacon sendet UUID
- Browser empfängt via Web Bluetooth API
- App sendet Beacon-ID an Backend
- Backend liefert Kontext zurück
- UI reagiert
Web Bluetooth API: Das Wesentliche
Browser-Support
| Browser | Support |
|---|---|
| Chrome (Desktop) | ✅ Voll |
| Chrome (Android) | ✅ Voll |
| Edge | ✅ Voll |
| Opera | ✅ Voll |
| Firefox | ❌ Nein |
| Safari | ❌ Nein |
Realität: ~65% der Nutzer können Web Bluetooth nutzen. Für den Rest braucht man Fallbacks.
Grundlegende Nutzung
// Permission anfordern und Gerät scannen
async function scanForBeacons() {
try {
// Nutzer muss aktiv zustimmen (Klick erforderlich)
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: ['battery_service']
});
console.log('Gerät gefunden:', device.name);
// Verbindung herstellen
const server = await device.gatt.connect();
// Services lesen
const services = await server.getPrimaryServices();
} catch (error) {
console.error('Bluetooth-Fehler:', error);
}
}
// Muss durch User-Geste getriggert werden
button.addEventListener('click', scanForBeacons);
Monitoring vs. Ranging
Monitoring: Erkennt, ob ein Beacon in Reichweite ist
- Batterieschonend
- Hintergrund-fähig (mit Einschränkungen)
- Für die meisten Use Cases ausreichend
Ranging: Misst Entfernung zum Beacon
- Batterie-intensiv
- Erfordert aktive App
- Nötig für präzise Navigation
Empfehlung: Starte mit Monitoring. Ranging nur, wenn wirklich nötig.
// Monitoring-Ansatz: Einfach prüfen, ob Beacon da ist
class BeaconMonitor {
constructor(targetUUIDs) {
this.targetUUIDs = targetUUIDs;
this.currentZone = null;
}
async startMonitoring() {
// Scan starten
const device = await navigator.bluetooth.requestDevice({
filters: this.targetUUIDs.map(uuid => ({
services: [uuid]
}))
});
if (device) {
this.onBeaconDetected(device);
}
}
onBeaconDetected(device) {
const newZone = this.getZoneFromDevice(device);
if (newZone !== this.currentZone) {
this.currentZone = newZone;
this.notifyZoneChange(newZone);
}
}
notifyZoneChange(zone) {
// Backend informieren
fetch('/api/zone-change', {
method: 'POST',
body: JSON.stringify({ zone, timestamp: Date.now() })
});
}
}
Architektur für Production
Kommunikation: WebSocket oder SSE?
WebSocket: Bidirektional
- Gut für Echtzeit-Interaktion
- Komplexer zu implementieren
- Höherer Ressourcenverbrauch
Server-Sent Events (SSE): Unidirektional (Server → Client)
- Einfacher
- Reicht für die meisten Beacon-Apps
- Automatische Reconnection
// SSE für Zone-Updates
class ZoneEventStream {
constructor(userId) {
this.eventSource = new EventSource(`/api/zones/stream?user=${userId}`);
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleZoneEvent(data);
};
this.eventSource.onerror = () => {
// Automatische Reconnection durch Browser
console.log('Verbindung unterbrochen, reconnecting...');
};
}
handleZoneEvent(data) {
switch (data.type) {
case 'content_update':
this.showContent(data.content);
break;
case 'navigation_hint':
this.showNavigation(data.hint);
break;
case 'alert':
this.showAlert(data.message);
break;
}
}
}
Backend-Struktur
Backend
├── /api/beacon
│ ├── POST /detect → Beacon-Erkennung melden
│ └── GET /zones → Alle Zonen abrufen
│
├── /api/content
│ └── GET /:zoneId → Content für Zone
│
├── /api/stream
│ └── GET /events → SSE-Stream für Updates
│
└── Services
├── ZoneResolver → UUID → Zone-Mapping
├── ContentService → Zone → Content-Mapping
└── EventDispatcher → Echtzeit-Events
Datenmodell
// Zone
{
id: "zone-lobby-north",
name: "Eingangshalle Nord",
beacons: ["uuid-1", "uuid-2"], // Mehrere Beacons pro Zone
parentZone: "zone-lobby",
contentId: "content-welcome"
}
// Beacon
{
uuid: "uuid-1",
major: 1,
minor: 101,
location: "Säule A3",
batteryLevel: 87,
lastSeen: "2025-12-21T10:30:00Z"
}
// Event
{
type: "zone_enter",
userId: "user-123",
zoneId: "zone-lobby-north",
timestamp: "2025-12-21T10:30:00Z",
previousZone: "zone-entrance"
}
Wichtige Designregeln
1. Permission-UX ernst nehmen
// SCHLECHT: Sofort nach Bluetooth fragen
window.onload = () => {
navigator.bluetooth.requestDevice({...}); // Wird blockiert!
};
// GUT: Kontext erklären, dann fragen
const startButton = document.getElementById('start-navigation');
const explainer = document.getElementById('bluetooth-explainer');
// Erst erklären
explainer.innerHTML = `
<h3>Navigation aktivieren</h3>
<p>Um Sie durch das Gebäude zu führen, benötigen wir
Zugriff auf Bluetooth.</p>
<ul>
<li>Wir tracken Sie nicht</li>
<li>Daten bleiben auf Ihrem Gerät</li>
<li>Sie können jederzeit stoppen</li>
</ul>
`;
// Dann Button anbieten
startButton.addEventListener('click', async () => {
const device = await navigator.bluetooth.requestDevice({...});
});
2. Privacy by Design
// Lokale Verarbeitung bevorzugen
class PrivacyFirstBeaconHandler {
constructor() {
this.zoneMapping = new Map(); // Lokal cachen
}
async onBeaconDetected(uuid) {
// 1. Lokal nachschlagen, wenn möglich
if (this.zoneMapping.has(uuid)) {
return this.zoneMapping.get(uuid);
}
// 2. Nur UUID senden, nicht User-ID
const response = await fetch(`/api/zone?beacon=${uuid}`);
const zone = await response.json();
// 3. Lokal cachen
this.zoneMapping.set(uuid, zone);
return zone;
}
}
Privacy-Prinzipien:
- Keine User-IDs, wenn nicht nötig
- Lokale Verarbeitung bevorzugen
- Bewegungsdaten nicht speichern
- Opt-in, nicht Opt-out
3. Fallback ohne BLE
class AdaptiveLocationService {
constructor() {
this.method = this.detectBestMethod();
}
detectBestMethod() {
if ('bluetooth' in navigator) {
return 'ble';
}
if ('geolocation' in navigator) {
return 'gps'; // Outdoor-Fallback
}
return 'manual';
}
async getCurrentZone() {
switch (this.method) {
case 'ble':
return this.getZoneFromBLE();
case 'gps':
return this.getZoneFromGPS();
case 'manual':
return this.showManualSelector();
}
}
showManualSelector() {
// QR-Code-Scanner oder Dropdown
return new Promise((resolve) => {
const modal = new ZoneSelectorModal();
modal.onSelect = (zone) => resolve(zone);
modal.show();
});
}
}
4. Batterie schonen
class BatteryAwareScanner {
constructor() {
this.scanInterval = 1000; // Start: 1 Sekunde
}
async adaptToContext() {
// Bewegung erkennen
if ('DeviceMotionEvent' in window) {
window.addEventListener('devicemotion', (e) => {
const moving = this.isMoving(e);
// Schneller scannen, wenn in Bewegung
this.scanInterval = moving ? 500 : 3000;
});
}
// Batterie berücksichtigen
if ('getBattery' in navigator) {
const battery = await navigator.getBattery();
if (battery.level < 0.2) {
this.scanInterval = 5000; // Batterie sparen
this.showLowBatteryHint();
}
}
}
}
Häufige Fehler und Lösungen
Fehler 1: Zu viele Beacons
Problem: 50 Beacons, die alle gleichzeitig senden, überlasten den Scanner.
Lösung: Zonen-basiert denken. Ein Beacon pro Entscheidungspunkt, nicht pro Quadratmeter.
Fehler 2: Ranging für alles
Problem: Ständige Distanzmessung frisst Batterie.
Lösung: Monitoring für Zone-Erkennung. Ranging nur für die letzten Meter (z.B. Exponat im Museum).
Fehler 3: Keine Offline-Fähigkeit
Problem: Kein Backend-Zugang = keine Navigation.
Lösung: Zone-Content im Service Worker cachen.
// Service Worker
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('zone-content-v1').then((cache) => {
return cache.addAll([
'/api/zones',
'/api/content/zone-1',
'/api/content/zone-2',
// ...
]);
})
);
});
Fehler 4: Keine Beacon-Wartung
Problem: Batterie leer, Beacon verrutscht, niemand merkt es.
Lösung: Beacon-Health-Monitoring.
// Backend: Beacon-Status tracken
class BeaconHealthMonitor {
checkHealth() {
const now = Date.now();
for (const beacon of this.beacons) {
const lastSeen = beacon.lastDetection;
const silent = now - lastSeen;
if (silent > 24 * 60 * 60 * 1000) { // 24 Stunden
this.alertMaintenance(beacon, 'Nicht erkannt seit 24h');
}
if (beacon.batteryLevel < 20) {
this.alertMaintenance(beacon, 'Batterie niedrig');
}
}
}
}
Schnellstart-Setup
Für den schnellen Start:
Hardware
- 5-10 Beacons (ca. 15-30€/Stück)
- Smartphone mit Chrome zum Testen
Software
Erste Schritte
- Beacons konfigurieren (UUIDs vergeben)
- Zone-Mapping erstellen
- Web-App mit Permission-Flow bauen
- Content pro Zone anlegen
- Testen, iterieren
Beacon-Hardware auswählen
Die Wahl der richtigen Beacons ist entscheidend für den Projekterfolg. Nicht jeder Beacon passt zu jedem Einsatzzweck.
Wichtige Kriterien
| Kriterium | Worauf achten |
|---|---|
| Reichweite | 10-30m für Indoor, konfigurierbar |
| Batterie | 2-5 Jahre Laufzeit, austauschbar |
| Gehäuse | Wetterfest für Außenbereiche |
| Konfiguration | App oder Cloud-Management |
| Protokolle | iBeacon + Eddystone für Flexibilität |
Empfehlungen nach Einsatzbereich
Museum / Retail:
- Kleine, unauffällige Beacons
- Mittlere Reichweite (10-15m)
- Lange Batterielaufzeit wichtig
Krankenhaus / Industrie:
- Robustes Gehäuse
- Hohe Sendeleistung für Überwindung von Hindernissen
- Zentrale Verwaltung über Cloud
Outdoor / Events:
- Wetterfestes Gehäuse (IP67)
- Hohe Reichweite (30m+)
- Einfacher Batteriewechsel
Beacon-Konfiguration
Die meisten Beacons lassen sich über Hersteller-Apps konfigurieren:
Faustregel für TX Power:
- Höher = mehr Reichweite, mehr Batterieverbrauch
- Niedriger = präzisere Erkennung, längere Laufzeit
Signalstärke und Distanzberechnung
Die Distanz zu einem Beacon wird über die Signalstärke (RSSI) geschätzt – aber das ist fehleranfällig.
Warum RSSI unzuverlässig ist
Pragmatischer Umgang mit Distanz
Statt exakte Meter zu berechnen, besser in Zonen denken:
class ProximityZone {
// Statt: "Beacon ist 3.7m entfernt"
// Besser: "Beacon ist in Zone NEAR"
static fromRSSI(rssi) {
if (rssi > -50) return 'IMMEDIATE'; // < 0.5m
if (rssi > -70) return 'NEAR'; // 0.5-3m
if (rssi > -85) return 'FAR'; // 3-10m
return 'UNKNOWN'; // > 10m oder gestört
}
static getConfidence(readings) {
// Mehrere Messungen mitteln
const avg = readings.reduce((a, b) => a + b) / readings.length;
const variance = this.calculateVariance(readings, avg);
// Hohe Varianz = geringe Konfidenz
return variance < 5 ? 'HIGH' : variance < 15 ? 'MEDIUM' : 'LOW';
}
}
Glättung für stabilere Werte
class RSSISmoother {
constructor(windowSize = 5) {
this.readings = [];
this.windowSize = windowSize;
}
addReading(rssi) {
this.readings.push(rssi);
if (this.readings.length > this.windowSize) {
this.readings.shift();
}
}
getSmoothedRSSI() {
if (this.readings.length === 0) return null;
// Median statt Durchschnitt (robuster gegen Ausreißer)
const sorted = [...this.readings].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
}
}
Testing und Debugging
BLE-Apps sind schwer zu testen. Hier sind bewährte Strategien.
Entwicklungs-Setup
Mock-Beacons für lokales Testing:
class MockBeaconService {
constructor() {
this.mockBeacons = [
{ uuid: 'test-uuid-1', major: 1, minor: 101, rssi: -65 },
{ uuid: 'test-uuid-2', major: 1, minor: 102, rssi: -78 },
];
}
async scan() {
// Simulierte Verzögerung
await new Promise(r => setTimeout(r, 500));
// Zufällige RSSI-Schwankung simulieren
return this.mockBeacons.map(b => ({
...b,
rssi: b.rssi + (Math.random() * 10 - 5)
}));
}
}
// In der App
const beaconService = process.env.NODE_ENV === 'development'
? new MockBeaconService()
: new RealBeaconService();
Chrome DevTools für Web Bluetooth:
chrome://bluetooth-internalsöffnen- Aktive Verbindungen und Scans einsehen
- GATT-Services und Characteristics inspizieren
Logging-Strategie
class BeaconLogger {
constructor(enabled = true) {
this.enabled = enabled;
this.events = [];
}
log(event, data) {
if (!this.enabled) return;
const entry = {
timestamp: new Date().toISOString(),
event,
data,
battery: navigator.getBattery?.() || null
};
this.events.push(entry);
console.log(`[BLE] ${event}:`, data);
// Optional: An Backend senden für Analyse
if (this.events.length >= 50) {
this.flush();
}
}
flush() {
fetch('/api/logs/beacon', {
method: 'POST',
body: JSON.stringify(this.events)
});
this.events = [];
}
}
// Nutzung
const logger = new BeaconLogger();
logger.log('beacon_detected', { uuid: 'xxx', rssi: -67 });
logger.log('zone_changed', { from: 'lobby', to: 'floor-1' });
Feldtests durchführen
Checkliste für Vor-Ort-Tests:
- Reichweiten-Test: An verschiedenen Positionen RSSI messen
- Interferenz-Test: Mit vielen Menschen im Raum testen
- Batterie-Test: App über längere Zeit laufen lassen
- Edge-Cases: Zwischen Zonen stehen, schnell gehen
- Offline-Test: WLAN deaktivieren, Verhalten prüfen
Skalierung und Performance
Was bei 10 Beacons funktioniert, kann bei 100 Beacons scheitern.
Client-seitige Optimierung
class ScalableBeaconHandler {
constructor() {
this.knownBeacons = new Map();
this.processingQueue = [];
this.isProcessing = false;
}
onBeaconDetected(beacon) {
// Duplikate früh filtern
const key = `${beacon.uuid}-${beacon.major}-${beacon.minor}`;
const lastSeen = this.knownBeacons.get(key);
if (lastSeen && Date.now() - lastSeen < 1000) {
return; // Innerhalb 1s bereits verarbeitet
}
this.knownBeacons.set(key, Date.now());
this.processingQueue.push(beacon);
this.processQueue();
}
async processQueue() {
if (this.isProcessing || this.processingQueue.length === 0) return;
this.isProcessing = true;
// Batch-Verarbeitung
const batch = this.processingQueue.splice(0, 10);
try {
await fetch('/api/beacons/batch', {
method: 'POST',
body: JSON.stringify(batch)
});
} finally {
this.isProcessing = false;
if (this.processingQueue.length > 0) {
requestAnimationFrame(() => this.processQueue());
}
}
}
}
Backend-seitige Optimierung
// Redis für schnelles Zone-Lookup
class ZoneCacheService {
constructor(redis) {
this.redis = redis;
this.localCache = new Map();
}
async getZone(beaconUUID) {
// 1. Lokaler Cache (schnellste Option)
if (this.localCache.has(beaconUUID)) {
return this.localCache.get(beaconUUID);
}
// 2. Redis (schnell, geteilt zwischen Instanzen)
const cached = await this.redis.get(`zone:${beaconUUID}`);
if (cached) {
const zone = JSON.parse(cached);
this.localCache.set(beaconUUID, zone);
return zone;
}
// 3. Datenbank (langsam, aber autoritativ)
const zone = await this.db.zones.findByBeacon(beaconUUID);
// Cachen für nächstes Mal
await this.redis.setex(`zone:${beaconUUID}`, 3600, JSON.stringify(zone));
this.localCache.set(beaconUUID, zone);
return zone;
}
}
Metriken sammeln
// Wichtige Metriken für BLE-Apps
const metrics = {
// Performance
scanDuration: [], // Wie lange dauert ein Scan?
zoneResolutionTime: [], // Wie schnell wird Zone aufgelöst?
// Qualität
beaconsPerScan: [], // Wie viele Beacons pro Scan?
zoneChangesPerMinute: [],// Wie oft wechselt die Zone?
// Fehler
scanFailures: 0, // Wie oft schlägt Scan fehl?
permissionDenied: 0, // Wie oft wird Permission verweigert?
// Business
contentViews: {}, // Welche Inhalte werden angezeigt?
navigationCompleted: 0, // Wie oft wird Navigation abgeschlossen?
};
Komplexität managen
BLE-Web-Apps sind technisch machbar. Die Herausforderung liegt nicht in der API, sondern in den Randfällen:
- Browser-Support (Fallbacks nötig)
- Permission-UX (Nutzer müssen verstehen, warum)
- Batterie (Monitoring vor Ranging)
- Wartung (Beacons sind physische Geräte)
- Signalqualität (RSSI ist unzuverlässig)
- Skalierung (viele Beacons, viele Nutzer)
Der Schlüssel ist Schichtentrennung:
- Beacons sind dumm
- Browser ist der Sensor
- Backend liefert Kontext
- UI reagiert
Wer das versteht, kann robuste BLE-Web-Apps bauen – ohne den Hype, mit echtem Nutzen.
Weiterführende Ressourcen
Spezifikationen:
- Web Bluetooth API – W3C Community Group
- Bluetooth Core Specification – Bluetooth SIG
Tools:
- nRF Connect – Beacon-Scanner für Debugging
- Beacon Simulator – Android-App zum Testen
Beacon-Hersteller:
- Kontakt.io, Estimote, Minew – etablierte Anbieter mit Cloud-Management
- Günstige China-Beacons – für Prototypen, aber weniger zuverlässig