OmegaSim Wiki

OmegaSim Wiki – die Idee dazu stammt von Gnen54. Hier stehen die offenen Artikel von Nutzer Luuke42 unter der Lizenz CC BY-SA 4.0.

OmegaBle

Stand dieses Artikels: OmegaSim 1.2.9 vom 26.10.2026 · Firmware des Autos: nicht bekannt (OmegaSim liest die Firmware-Version nicht aus)
Dieser Text wurde mithilfe von KI erstellt (Claude) und ist nicht in jedem Punkt von Menschen geprüft. Fehler bitte direkt verbessern oder auf der Diskussionsseite melden, siehe Mitmachen. Die öffentliche Fassung auf luuke42.github.io/omegasim kann älter sein als der hier beschriebene Stand.

OmegaBle ist ein natives Bluetooth-Plugin der Android-App von OmegaSim. Es schreibt die Steuerbefehle an die Carrera HYBRID-Autos auf einem eigenen Java-Thread direkt an die Bluetooth-Schnittstelle von Android, ohne Umweg über den Hauptthread, ersetzt dabei wartende Befehle durch neuere („neuester gewinnt“) und räumt Verbindungen hart auf, die das allgemeine Bluetooth-Plugin der App sonst hängen lässt. OmegaBle ist ein Baustein der Latenzarbeit in der App: Es entstand, weil Nutzer in der APK eine spürbare Verzögerung meldeten, die es im Browser nicht gab. Im Browser gibt es OmegaBle nicht; dort spricht Chrome oder Edge über Web Bluetooth direkt mit dem Auto.

Auf einen Blick

Punkt Wert
Name im Code Capacitor-Plugin OmegaBle, Klasse de.lroeseler.omegasim.OmegaBlePlugin
Plattform nur Android-App (APK), Android 7 und neuer (minSdkVersion 24, Ziel 36)
Grundlage Capacitor 8.5.2; die Verbindung selbst gehört dem Community-Plugin @capacitor-community/bluetooth-le 8.3.0
Aufgaben Steuerbefehle schreiben (ohne Antwort, eigener Thread), Direktweg ohne Hauptthread, Verbindungen schließen, Zähler für die Diagnose
Menüpfad der Diagnose Optionen → System → Funk-Rundlauf
Schalter keiner für OmegaBle selbst; es wird automatisch benutzt, wenn die APK es enthält
Status stabil; Teile seit 1.2.3 brauchen eine neue APK
seit 0.8.124 (erste Fassung), neu gebaut in 0.9.2, Direktweg seit 0.10.4
Quelldateien android-app/…/OmegaBlePlugin.java, MainActivity.java, OmegaPadNativ.java, src/05-app-bruecke.js, src/20-protocol.js, src/90-ghosts.js, src/50-drive.js

Grundlagen

Dieser Abschnitt erklärt die Technik, die man braucht, um OmegaBle zu verstehen. Ausführlicher: Bluetooth Low Energy, Aufbau von OmegaSim, Glossar.

Wie Bluetooth Low Energy überträgt

Funk: Bluetooth Low Energy (BLE) funkt im 2,4-GHz-Band, wie WLAN. Damit es stromsparend bleibt, sind Sender und Empfänger nicht dauernd aktiv, sondern treffen sich in festen Abständen zu kurzen Verbindungsereignissen. Nur dann können Pakete fließen.

Verbindung: Ein Auto, das nicht verbunden ist, sendet regelmäßig Werbung (advertising): „Ich bin HYBRID-…“. Das Handy sucht (scan), findet die Werbung und baut eine Verbindung auf. Ein verbundenes Auto hört auf zu werben. Deshalb ist ein Auto, das sich fälschlich für verbunden hält, für jede neue Suche unsichtbar.

GATT, Dienst, Merkmal: Nach dem Verbinden fragt das Handy den Datenbaum des Autos ab (service discovery): Dienste mit Merkmalen. Für das Fahren zählen zwei Merkmale des Nordic UART Service: RX (6e400002-…), auf das die App schreibt, und TX (6e400003-…), über das das Auto meldet.

Schreiben mit und ohne Antwort: Beim Schreiben mit Antwort (write request) bestätigt das Auto jedes Paket im nächsten Verbindungsereignis; erst dann ist der Vorgang fertig. Beim Schreiben ohne Antwort (write command) legt das Handy das Paket in den Sendepuffer seines Funkchips und ist sofort fertig. Für Steuerbefehle, die ohnehin alle 45 ms neu kommen, ist „ohne Antwort“ richtig: Eine Bestätigung kostet Zeit und nützt nichts, weil der nächste Befehl den alten sowieso ersetzt.

Notifications: Das Auto schickt Meldungen von sich aus, wenn die App das Merkmal TX abonniert hat. Jede Meldung (Akku, Streckencode, Teilezähler) kommt beim Handy als Ereignis an.

Verbindungsintervall und Latenz: Der Abstand der Verbindungsereignisse heißt Verbindungsintervall. Er wird beim Verbinden ausgehandelt und kann sich später ändern. Er bestimmt die Latenz (Verzögerung) direkt: Ein Paket, das kurz nach einem Ereignis fertig wird, wartet bis zum nächsten. Der Code von OmegaSim hält fest: „Android wählt sonst 30–50 ms, und der Sendetakt ist 45 ms – dann fällt jedes zweite Paket auf das nächste Intervall.“ Deshalb fordert die App mit requestConnectionPriority(HIGH) ein kürzeres Intervall an. Mehrere Autos teilen sich einen Funkadapter im Handy; wird er voll, lehnt Android Schreibvorgänge ab.

Ein Vorgang zugleich: Android erlaubt je Verbindung nur einen GATT-Vorgang zur selben Zeit (Verbinden, Dienstsuche, Abonnieren, Lesen, Schreiben mit Antwort). Ein zweiter wird abgelehnt, solange der erste läuft.

WebView, Capacitor und natives Plugin

WebView: Die Android-App von OmegaSim ist dieselbe HTML-Seite wie im Browser, angezeigt in einer WebView, einem in die App eingebauten Browserfenster (Android System WebView, technisch Chrome). Der WebView fehlt aber Web Bluetooth, die Schnittstelle, mit der Chrome im Browser Bluetooth-Geräte anspricht.

Capacitor: Ein Rahmenwerk, das eine Web-App in eine Android-App verpackt und eine Brücke zwischen JavaScript (in der WebView) und Java (in Android) baut. JavaScript ruft eine Methode eines Plugins auf; Capacitor verpackt den Aufruf als JSON-Text, schickt ihn in die Java-Welt, führt dort die Methode aus und schickt die Antwort wieder zurück.

Natives Plugin: Ein Plugin ist ein Stück Java- (oder Kotlin-)Code mit Methoden, die die Web-Seite aufrufen kann. „Nativ“ heißt: Es läuft direkt in Android und darf alles, was Android-Apps dürfen, etwa Bluetooth. OmegaSim nutzt zwei Bluetooth-Plugins: das Community-Plugin @capacitor-community/bluetooth-le für Suchen, Verbinden, Abonnieren und Lesen, und das eigene OmegaBle für das schnelle Schreiben und Aufräumen.

Threads: Ein Thread (Faden) ist ein eigenständiger Ablauf in einem Programm. Android hat einen Hauptthread (UI-Thread), der Bildschirm, Eingaben und viele Rückmeldungen erledigt. Muss ein Befehl dort anstehen, wartet er hinter allem anderen. Ein eigener Thread für das Schreiben umgeht diese Schlange.

JavascriptInterface: Ein Java-Objekt, das Android direkt als JavaScript-Objekt in die Seite hängt (hier window.OmegaDirekt). Ein Aufruf darauf läuft synchron auf dem „JavaBridge“-Thread der WebView, nicht auf dem Hauptthread und ohne JSON-Umweg über Capacitor.

Reflexion: Java kann zur Laufzeit in fremde Objekte hineinsehen und auch private Felder lesen. OmegaBle liest so aus dem Community-Plugin dessen Liste offener Verbindungen (deviceMap) und darin je Auto das Android-Objekt BluetoothGatt.

Atomare Variablen: Wenn mehrere Threads dieselbe Variable benutzen, können sie sich gegenseitig überschreiben. AtomicReference, AtomicBoolean und AtomicLong bieten Operationen, die unteilbar sind (z. B. „lies den alten Wert und setze zugleich einen neuen“: getAndSet; „setze nur, wenn der Wert noch X ist“: compareAndSet). volatile sorgt dafür, dass ein Thread immer den aktuellen Wert sieht, den ein anderer geschrieben hat.

Bedienung

OmegaBle arbeitet von selbst. Was man als Nutzer tun kann:

  1. Die aktuelle APK installieren. Teile von OmegaBle (Direktweg seit 0.10.4, Schließen seit 1.2.3) stecken in der Android-Schicht und kommen nur mit einer neuen APK; die Brücke in der Seite kommt mit jedem Web-Update.
  2. Autos in der Garage verbinden. Mit Optionen → System → Bekannte Autos beim Start verbinden (Vorgabe an) verbindet die App gemerkte Autos selbst.
  3. Unter Optionen → System → Funk-Rundlauf prüfen, welcher Weg aktiv ist. Gut: „Weg OmegaBle direkt“ und „Pad direkt an“.
  4. Steht dort „Direktweg fehlt“: App einmal schließen und neu öffnen; auf älteren Handys „Android System WebView“ im Play Store aktualisieren.
  5. Reagiert ein Auto nicht oder leuchtet hell ohne Verbindung: Garage → Alle trennen. Das schließt auch verwaiste Verbindungen. Danach neu verbinden.
  6. Probleme nur mit mehreren NPCs: Zähler „verworfen“ im Funk-Rundlauf beobachten (siehe Fehlerbilder).

Einstellungen

OmegaBle hat keine eigenen Schalter. Wirksam sind diese Optionen und festen Werte:

Name Bereich/Werte Standard Wirkung (aus dem Code) Interner Schlüssel/ID
Bekannte Autos beim Start verbinden (Optionen → System) an/aus an verbindet nacheinander gemerkte Autos über ihre MAC-Adresse, ohne Auswahlfenster; vorher alleSchliessen, wenn die Garage leer ist setting-autoverbinden, autoVerbindenStart()
Funk-Rundlauf (Optionen → System) Anzeige – Messzeile, jede Sekunde neu, nur wenn sichtbar #funk-rundlauf
Entwicklertools zeigen (Optionen → System) an/aus aus BLE-Werkbank mit eigenem Verbindungsweg setting-dev
Schreibversuche je Befehl fest 3 danach „verworfen“ VERSUCHE
Pause zwischen Versuchen fest 3 ms SystemClock.sleep auf dem Schreib-Thread VERSUCH_PAUSE_MS
Höchstdauer einer Schreibpause fest 1500 ms hebt pause() selbst auf, falls die Seite das Ende vergisst PAUSE_HOECHSTENS_MS
Wachhund je Merkmal (Seite) fest 300 ms ein Schreibvorgang, der länger hängt, gilt als verloren FUNK_HAENGT_MS
Plugin-Zeitgrenze Schreiben fest 300 ms (statt 5 s des Plugins) für den Rückfallweg über das Community-Plugin timeout: 300
Sperre auf das Plugin fest 30 s, verdoppelnd bis 600 s siehe omegaLage-Sperre OMEGA_SPERRE_MS, OMEGA_SPERRE_MAX_MS
Probezeit nach Sperre fest 1500 ms, Blick alle 500 ms OmegaBle wird genau beobachtet OMEGA_PROBE_MS
Verbindungsaufbau gesamt fest 20 s danach trennen und schließen VERBINDE_FRIST_MS
Plugin-connect fest 15 s Zeitgrenze des eigentlichen Verbindens timeout: 15000
Neu verbinden nach Abriss fest Wartezeit 1 s × Versuch, höchstens 5 Versuche danach Auto entfernen VERBINDE_NEU_MS, VERBINDE_NEU_MAX
Meldetakt-Schwelle fest 75 ms, höchstens alle 20 s fordert das schnelle Verbindungsintervall neu an MELDE_LANGSAM_MS, MELDE_PRIO_PAUSE_MS
NPC-Sendebudget fest 30 Pakete/s für alle NPCs zusammen, unveränderte Befehle höchstens alle 200 ms entlastet den Funkadapter NPC_BUDGET_PRO_S, NPC_GLEICH_MS

Funktionsweise

Schichten

 Seite (JavaScript, WebView)
   controlHeartbeat() alle 45 ms → sendControlValue() → buildCommandPacket() → funkSchreiben()
        │
   Brücke src/05-app-bruecke.js: baut navigator.bluetooth nach
        │                                   │
   OmegaBle (eigenes Plugin)          Community-Plugin BluetoothLe
   writeControl / OmegaDirekt         connect, getServices, startNotifications,
   status, pause, schliessen …        read, write mit Antwort, disconnect
        │                                   │
        └──────── dasselbe BluetoothGatt-Objekt (per Reflexion geliehen) ───┘
                              │
                     Android-Bluetooth-Stapel → Funkchip → Auto

Der übrige Code von OmegaSim merkt nicht, dass er in einer App läuft: Die Brücke stellt genau die Teile von Web Bluetooth bereit, die er benutzt (requestDevice, gatt.connect, getPrimaryService, getCharacteristic, writeValueWithoutResponse, writeValueWithResponse, startNotifications, die Ereignisse characteristicvaluechanged und gattserverdisconnected). Sie steht in der Seite und nicht in der APK, damit jede Berichtigung mit dem nächsten Web-Update aufs Telefon kommt[1].

Schnittstelle: alle Methoden

Methode Art Parameter Rückgabe Thread Zweck
writeControl @PluginMethod(returnType = RETURN_NONE) deviceId, service, characteristic, value (Hex-Text), t (Date.now() der Seite, optional) keine (es wird nie geantwortet) Capacitor-Plugin-Thread, dann Schreib-Thread Befehl einreihen; Rückfall, wenn der Direktweg fehlt
status @PluginMethod deviceId, service, characteristic bereit, geschrieben, wiederholt, verworfen, ersetzt (je Gerät), brueckeMittel (ms, −1 ohne Messung), brueckeMax (ms, wird beim Lesen auf 0 gesetzt), direkt, direktFehler, version (= 4) Plugin-Thread ehrliche Auskunft: findet OmegaBle die Verbindung?
pause @PluginMethod an (boolean) leer Plugin-Thread Schreiben anhalten, solange das Community-Plugin einen GATT-Vorgang macht; höchstens 1500 ms
schliessen @PluginMethod deviceId geschlossen (boolean) UI-Thread eine Verbindung hart schließen (seit 1.2.3)
alleSchliessen @PluginMethod – anzahl (Zahl geschlossener Verbindungen) UI-Thread alle Verbindungen des Community-Plugins schließen (seit 1.2.3)
direktNeu @PluginMethod – direkt, direktFehler; danach lädt die Seite nach 150 ms neu UI-Thread Direktwege neu anmelden (seit 1.0.40)
OmegaDirekt.schreibe @JavascriptInterface (kein Capacitor) deviceId, service, characteristic, value, tJs keine, synchron JavaBridge-Thread der Direktweg für Steuerbefehle (seit 0.10.4)
OmegaPad.lesen @JavascriptInterface (Klasse OmegaPadNativ.Js) – Text mit 10 Zahlen: lx, ly, rx, ry, lt, rt, Alter des letzten Ereignisses (ms), Geräte (0/1/2), Ereigniszahl, Zustellzeit (ms) JavaBridge-Thread Gamepad direkt (seit 0.10.4)

Dazu kommen zwei Lebenszyklus-Methoden: load() startet den Schreib-Thread „OmegaBle-Write“ und meldet die Direktwege auf dem UI-Thread an, bevor die Seite lädt; handleOnDestroy() leert die Warteschlange des Threads und beendet ihn.

Threading-Modell

Thread Wer läuft dort Was er mit OmegaBle zu tun hat
JavaScript der Seite (WebView-Renderer) Herzschlag, Physik, Brücke baut Pakete, ruft OmegaDirekt.schreibe oder writeControl
JavaBridge (WebView) alle @JavascriptInterface-Aufrufe Direkt.schreibe() → einreihen(); OmegaPad.lesen()
Android-Hauptthread (UI) Capacitor nimmt dort Nachrichten der Seite an und schickt Antworten und Plugin-Ereignisse (auch jede Bluetooth-Meldung) zurück schliessen, alleSchliessen, direktNeu; ohne Direktweg läuft jeder Steuerbefehl hier durch
„CapacitorPlugins“ (HandlerThread von Capacitor) Ausführung der @PluginMethods writeControl, status, pause
„OmegaBle-Write“ (eigener HandlerThread) nur OmegaBle abarbeiten(), schreibenMitVersuchen(), schreibeGatt()
Binder-Threads von Android Rückmeldungen des Bluetooth-Stapels an das Community-Plugin Notifications und Verbindungsereignisse

Gemeinsame Daten und wie sie geschützt sind[2]:

Feld Typ Geschrieben von Bedeutung
plaetze ConcurrentHashMap<String, Platz> alle (über putIfAbsent) ein Platz je Gerätekennung
Platz.neu AtomicReference<byte[]> JavaBridge/Plugin-Thread setzen, Schreib-Thread holt der eine Warteplatz: der neueste ungeschriebene Befehl
Platz.laeuft AtomicBoolean beide ob für dieses Gerät gerade ein Abarbeiten eingeplant ist
Platz.dienst, merkmal, gatt, ziel volatile alle Ziel des Schreibens und geliehenes GATT-Objekt
Platz.fehlInFolge int nur Schreib-Thread Fehlschläge hintereinander
geschrieben, wiederholt, verworfen, ersetzt AtomicLong Schreib-Thread bzw. einreihen() Zähler für die Diagnose
pauseBis volatile long pause() Uhrzeit (uptimeMillis), bis zu der nicht geschrieben wird
brueckeSumme, brueckeN, brueckeMax AtomicLong JavaBridge/Plugin-Thread Brückenzeit; das Maximum per compareAndSet-Schleife

Generationen. Im Java-Teil gibt es keinen Generationszähler, weil dort der Platz atomar geleert wird. Auf der Seite dagegen schützt ein Zähler gen je Merkmal den Wachhund in funkSchreiben(): Hängt ein Schreibvorgang länger als 300 ms, beginnt der nächste mit ++z.gen; kommt das Ende des alten doch noch an, sieht es gen !== z.gen und gibt den neuen Zustand nicht frei[3]. Für NPCs gilt dasselbe mit car.writeGen in writeToCar().

Der Schreibweg eines Pakets

Ein Steuerbefehl des Spielerautos, Schritt für Schritt:

  1. Herzschlag (Seite). controlHeartbeat() läuft alle 45 ms (setInterval). Es liest das Gamepad frisch (in der App direkt über OmegaPad.lesen()), rechnet physicsStep() und ruft sendControlValue().
  2. Paket bauen. buildCommandPacket() erzeugt die 20 Byte samt CRC-8 (siehe Carrera HYBRID#Das Befehlspaket (App → Auto)).
  3. Schloss je Merkmal (Seite). funkSchreiben(): Ist für dieses Merkmal noch ein Schreibvorgang unterwegs (jünger als 300 ms), wird das Paket als wartendes gemerkt und ein älteres wartendes überschrieben. Tiefe eins. Sonst beginnt ein neuer Vorgang mit gen = ++z.gen.
  4. Brücke. Weil das Merkmal „ohne Antwort“ anbietet, ruft funkSchreiben() writeValueWithoutResponse() der Brücke. Die prüft: Ist OmegaBle registriert (isPluginAvailable('OmegaBle'), einmal gemerkt)? Ist es für dieses Auto bereit (omegaBleBereit(), siehe Sperre)?
  5. Direktweg. Gibt es window.OmegaDirekt, ruft die Brücke synchron schreibe(deviceId, service, characteristic, hex, Date.now()) und gibt sofort Promise.resolve() zurück. Sonst schickt sie writeControl mit C.toNative ohne Rückruf („Feuer und weg“).
  6. Brückenzeit. In Java misst brueckeMessen() den Abstand zwischen Date.now() der Seite und der Ankunft; Werte über 5000 ms gelten als Uhrsprung und werden verworfen.
  7. Einreihen. einreihen() wandelt den Hex-Text in Bytes, setzt sie mit neu.getAndSet() auf den Platz und zählt ersetzt, wenn dort noch ein ungeschriebener Befehl lag. Hat sich Dienst oder Merkmal geändert, wird das gemerkte Ziel vergessen.
  8. Abarbeiten anstoßen. Nur wenn laeuft.compareAndSet(false, true) gelingt, wird abarbeiten() auf den Schreib-Thread gelegt. So läuft je Gerät höchstens ein Abarbeiten.
  9. Pause prüfen (Schreib-Thread). Liegt pauseBis in der Zukunft, legt sich abarbeiten() nach höchstens 5 ms erneut an und bleibt „laufend“.
  10. Neuesten holen. neu.getAndSet(null) nimmt den neuesten Befehl und leert den Platz.
  11. Schreiben mit Versuchen. schreibenMitVersuchen(): bis zu 3 Versuche. Vor dem 2. und 3. Versuch: wartet inzwischen ein neuerer Befehl, wird der alte als verworfen gezählt und aufgegeben; sonst wiederholt zählen und 3 ms schlafen.
  12. Ziel holen. zielHolen() nimmt das gemerkte BluetoothGatt und Merkmal oder sucht beides per Reflexion im Community-Plugin neu.
  13. GATT-Schreibbefehl. schreibeGatt(): Ab Android 13 gatt.writeCharacteristic(c, bytes, WRITE_TYPE_NO_RESPONSE), Erfolg bei BluetoothStatusCodes.SUCCESS. Davor über das geteilte Merkmal-Objekt: synchronized (c) { setWriteType(NO_RESPONSE); setValue(bytes); writeCharacteristic(c) }. Ergebnis 0 = angenommen, 1 = abgelehnt (belegt, getrennt), −1 = keine Berechtigung.
  14. Auswerten. 0: geschrieben +1, fehlInFolge = 0, fertig. −1: abbrechen (zwecklos). 1: fehlInFolge +1; ab 3 in Folge wird das gemerkte GATT-Objekt vergessen (vermutlich wurde neu verbunden). Nach drei erfolglosen Versuchen: verworfen +1.
  15. Weiter oder aufhören. Wartet schon wieder ein Befehl, legt sich abarbeiten() sofort neu an. Sonst laeuft = false – und dann wird noch einmal nachgesehen: Kam genau zwischen Prüfen und Freigeben ein Befehl, konnte einreihen() den Lauf nicht starten, also startet ihn abarbeiten() selbst.
  16. Seite. Die Promise ist schon erfüllt; funkSchreiben() misst die Zeit (nur JavaScript, in der Anzeige „nur JS“), holt ein inzwischen wartendes Paket und schickt es auf demselben Weg.
// OmegaBlePlugin.java, abarbeiten()
byte[] wert = p.neu.getAndSet(null);
if (wert != null) schreibenMitVersuchen(deviceId, p, wert);
if (p.neu.get() != null) { h.post(() -> abarbeiten(deviceId, p)); return; }
p.laeuft.set(false);
// Kam genau jetzt ein Befehl, hat writeControl den Lauf nicht neu starten koennen.
if (p.neu.get() != null && p.laeuft.compareAndSet(false, true)) h.post(() -> abarbeiten(deviceId, p));

Was bei vollem Puffer passiert. Ist der Funkadapter belegt (anderer GATT-Vorgang, Sendepuffer voll, viele Autos), lehnt Android den Schreibbefehl ab: writeCharacteristic liefert false bzw. einen Status ungleich SUCCESS. OmegaBle versucht es nach 3 ms erneut, höchstens dreimal, und gibt sofort auf, sobald ein neuerer Befehl wartet. Ein verworfener Befehl ist also entweder einer, der dreimal abgelehnt wurde, oder einer, der von einem neueren überholt wurde, während er wiederholt werden sollte. Eine Warteschlange gibt es absichtlich nicht: Sie würde alte Stickstellungen nachliefern, und „ein verspäteter Lenkbefehl ist schlimmer als gar keiner“.

Warum nicht „ersetzt“ und „verworfen“ verwechseln. ersetzt zählt Befehle, die schon beim Einreihen überschrieben wurden, weil der vorige noch nicht abgeholt war (normal bei kurzzeitiger Last). verworfen zählt Befehle, die der Schreib-Thread schon in der Hand hatte und nicht losbekam.

Schreiben mit und ohne Antwort in der App

Weg Wer schreibt Wartet auf Zeitgrenze Pause für OmegaBle
writeValueWithoutResponse, OmegaBle bereit OmegaBle nichts – –
writeValueWithoutResponse, Rückfall Community-Plugin writeWithoutResponse onCharacteristicWrite von Android 300 ms nein
writeValueWithResponse Community-Plugin write Bestätigung des Autos 300 ms ja (mitOmegaPause)
readValue, startNotifications, stopNotifications Community-Plugin Antwort Plugin-Vorgabe ja

Das Community-Plugin hält je Merkmal eine offene Antwort (callbackMap["write|svc|char"]). Überlappten früher zwei Schreibvorgänge, wurde die erste Antwort überschrieben, ihr Promise endete nie und eine globale Sperre blieb für immer gesetzt („Auto reagiert nach Mehrspieler-Beitritt nicht“). Seit 0.8.41 gibt es deshalb das Schloss je Merkmal mit Wachhund[4].

Pause während anderer GATT-Vorgänge

mitOmegaPause(fn) zählt laufende Vorgänge des Community-Plugins (omegaPausen). Beim ersten ruft es pause({an: true}), beim letzten pause({an: false}). In Java setzt pause pauseBis = jetzt + 1500 ms bzw. 0. Die Seite merkt sich den Zeitpunkt (omegaPauseZuletzt), damit Verworfenes in dieser Zeit nicht als Ausfall zählt.

Die omegaLage-Sperre

OmegaBle sagt über status() ehrlich, ob es die Verbindung findet. Die Brücke fragt je Auto im Hintergrund nach und entscheidet, ob dieses Auto über OmegaBle oder über das langsamere Community-Plugin fährt[5].

Zustand Bedingung nächster Zustand
unbekannt erster Befehl; Statusabfrage läuft bereit oder nicht bereit (bis dahin schreibt das Plugin)
bereit Blick alle 2 s: tot = seit dem letzten Blick mehr als 0 verworfen und 0 geschrieben und keine Pause im Fenster (2,5 s) bleibt bereit; bei tot zweimal in Folge → gesperrt
gesperrt sperreMs = 30 s beim ersten Mal, sonst verdoppelt, höchstens 600 s nach Ablauf → Probe
Probe 1500 ms lang Blick alle 500 ms ein Blick mit tot → wieder gesperrt (doppelte Zeit); etwas geschrieben → bereit
alte APK status() wirft einen Fehler (APK 0.8.124–0.8.126) bereit = true, „(alte APK)“; OmegaBle schreibt wie früher
beliebig neuer Verbindungsaufbau oder Aufräumen des Autos Lage gelöscht, alles neu
tot(Blick)   = (verworfen_jetzt − verworfen_vorher > 0)
             ∧ (geschrieben_jetzt − geschrieben_vorher = 0)
             ∧ ¬(Pause läuft ∨ letzte Pause vor < 2500 ms)
kaputt       = tot ∧ (tot im vorigen Blick ∨ in der Probe)
sperreMs_neu = sperreMs_alt = 0 ? 30 000 : min(600 000, 2 × sperreMs_alt)

Die Regel „gar nichts geschrieben“ ist bewusst streng: Bei vielen Autos verwirft OmegaBle auch im Normalbetrieb, weil der Adapter belegt ist. Das ist Funklast, kein toter Weg, und das Plugin wäre dort nur langsamer. Bis 1.1.22 kippte der Weg im Kreis (nach ~4 s aufs Plugin, 2 s später zurück), und in rund zwei Dritteln der Zeit gingen alle Befehle an dieses Auto verloren, auch Haltebefehle.

Verbindungsaufbau

  1. Auswahl. garageConnect() ruft navigator.bluetooth.requestDevice mit zwei Filtern (Name beginnt mit „HYBRID“ oder Nordic UART Service). Die Brücke prüft zuerst mit getConnectedDevices, ob das System schon ein passendes Auto verbunden hat, das die Seite nicht kennt, und nimmt es dann direkt; sonst öffnet das Community-Plugin seine Suche (nur mit dem Namensfilter, weil es nur einen Filter kennt).
  2. Nur ein Aufbau je Auto. garVerbindetGerade (Seite) und verbindetJe (Brücke) verhindern, dass automatisches Verbinden und ein Klick gleichzeitig dasselbe Auto aufbauen.
  3. Zeitgrenze. verbindeGattSicher() lässt verbindeGatt() gegen eine 20-s-Uhr laufen. Bei jedem Fehler oder Ablauf: gatt.disconnect().
  4. Verbinden (Brücke). Gatt._verbinden(): Plugin initialisieren, Trennmeldung abonnieren, alte Weg-Sperre löschen, connect mit 15 s. Scheitert das, _aufraeumen(): Plugin-disconnect, OmegaBle.schliessen, Sperre löschen.
  5. Priorität. Sofort requestConnectionPriority(1) (1 = HIGH), nach 5 s noch einmal.
  6. Dienste. getServices; eine leere Liste (aus einer noch laufenden Dienstsuche) wird nicht gemerkt, sonst scheiterte jeder weitere Versuch mit „Dienst nicht gefunden“.
  7. Abonnieren. startNotifications mit OmegaBle-Pause. Das Ereignis heißt beim Plugin notification|Gerät|Dienst|Merkmal; ein altes Abo unter demselben Namen wird vorher entfernt (sonst lief nach einem Neuverbinden jedes Paket doppelt).
  8. Aufnahme. Auto in die Garage, Rolle zuweisen, Anlassklang.

In der App ist die Gerätekennung die MAC-Adresse und bleibt über Neustarts gleich; deshalb funktioniert das automatische Verbinden über bekanntesGeraet(id) ohne Auswahlfenster.

Abriss im Betrieb. Meldet das Plugin disconnected|Gerät, setzt die Seite rx.verbindungWeg = true: Ab sofort schreibt weder der Spieler (funkSchreiben) noch ein NPC (writeToCar) an dieses Auto. Dann reconnectCar(): Wartezeit 1 s × Versuchsnummer, höchstens 5 Versuche, danach trennen und aus der Garage entfernen. Dass während des Neuverbindens nicht mehr geschrieben wird, verhindert seit 1.2.3 auch, dass die Fehlschläge die Weg-Sperre bis zu 10 Minuten auslösen.

Verbindungsabbau und Aufräumen

Anlass war die Meldung, ein Auto leuchte hell wie nach erfolgreichem Verbinden, die App sei aber nicht verbunden, und es helfe nur das Ladegerät. Ursachen im Community-Plugin: zwei überlappende connect-Aufrufe (der zweite überschreibt den Rückruf des ersten), eine gescheiterte Dienstsuche ohne Trennen und ein disconnect, das nur schließt, wenn die Trennmeldung kommt.

Auslöser Was passiert Code
Auto trennen (Garage) stopGhost, trennend = true, gatt.disconnect() – immer nativ, auch wenn die Seite sich für getrennt hält; Plugin-disconnect (2 s), dann OmegaBle.schliessen disconnectCar(), Gatt.disconnect()
Garage → Alle trennen alle Autos trennen, dann verwaisteTrennen([]) gar-disconnect-all
App-Start 1,5 s nach dem Laden: ist die Garage leer, alleSchliessen(); wurde etwas geschlossen, 1,5 s warten (das Auto wirbt erst wieder, wenn es die Trennung bemerkt), dann automatisch verbinden src/90-ghosts.js
Seite verlassen (Neuladen, Update) alle Autos trennen pagehide
App beenden MainActivity.onDestroy() → bluetoothTrennen(): per Reflexion jedes BluetoothGatt trennen und schließen, die Karte leeren MainActivity.java

schliesseGeraet(id) in OmegaBle, auf dem UI-Thread, auf dem auch das Community-Plugin seine Karte anfasst:

  1. den eigenen Platz vergessen (gatt = null, ziel = null);
  2. aus dem Community-Plugin per Reflexion deviceMap holen, darin das Gerät;
  3. dessen Feld bluetoothGatt lesen, disconnect() und close() rufen (jedes für sich abgesichert);
  4. das Feld auf null setzen und den Eintrag aus deviceMap entfernen.

alleSchliessen tut das für jeden Schlüssel der Karte und gibt die Zahl zurück.

verwaisteTrennen(behalten) in der Brücke findet Verbindungen, die zu keinem Auto der Garage gehören:

  1. getConnectedDevices: alle Geräte, deren Name mit „HYBRID“ beginnt und die nicht behalten werden sollen.
  2. Für jedes: Plugin-disconnect (2 s) und OmegaBle.schliessen.
  3. Noch einmal fragen. Wer immer noch verbunden ist (eine Verbindung ohne eigenen Griff), wird kurz verbunden (5 s) und sauber getrennt (2 s), dann geschlossen.
  4. Zurück kommt die Zahl aus Schritt 1; die App meldet „n hängende Verbindung(en) getrennt“.

Verbindungspriorität

beim Verbinden:          requestConnectionPriority(HIGH)
nach 5 s:                requestConnectionPriority(HIGH)      (einmal)
bei jeder Meldung:       d = min(1000, jetzt − letzteMeldung)
                         meldeAbstand ← meldeAbstand + (d − meldeAbstand) × 0,05
wenn meldeAbstand > 75 ms und letzte Anforderung > 20 s her:
                         requestConnectionPriority(HIGH)

Hintergrund: Android geht nach einiger Zeit oft vom schnellen Intervall zurück, und angefordert wurde vor 1.2.3 nur beim Verbinden. Jede Anforderung kann eine Neuaushandlung auslösen, während der der Funk kurz stockt; deshalb wird sie nicht dauernd wiederholt (bis 0.10.4 alle 10 s, was regelmäßige kleine Hänger erzeugte). Mit gekoppeltem Gamepad wurde die 5-s-Anforderung in 1.0.73 ausgelassen und in 1.1.3 wieder eingeführt, weil alte Handys ohne sie stockten[6].

Funklast mit mehreren Autos

Jedes fahrende Auto schrieb bis 1.1.23 alle 45 ms: mit vier NPCs und dem Fahrer rund 110 Pakete je Sekunde über einen Adapter. Gemessen in 1.1.16: 4000 geschrieben, 2400 wiederholt, 1200 verworfen, auch Pakete des Fahrers. Seitdem teilen sich die NPCs ein Budget[7]:

n = Zahl fahrender NPCs (alle 500 ms neu gezählt)
Abstand(n) = 0                                   für n ≤ 1
Abstand(n) = max(45, round(1000 × n / 30)) − 5   ms, sonst
unveränderter Befehl:  höchstens alle max(Abstand, 200) ms

Ein Halt (Gas wechselt auf 0), Einpark- und Haltefolgen und der Zieleinlauf gehen immer sofort im vollen Takt.

Direktweg und Gamepad direkt

load() meldet OmegaDirekt und OmegaPad auf dem UI-Thread an, bevor die Seite lädt; ein JavascriptInterface gilt erst ab dem nächsten Laden der Seite. Fehlt window.OmegaDirekt beim Start, ruft die Brücke einmal je Sitzung direktNeu (gemerkt in sessionStorage unter omegaDirektNeu), und die Seite lädt einmal neu. OmegaPadNativ nimmt jedes Achsereignis eines Controllers in MainActivity.dispatchGenericMotionEvent() auf und legt sechs Werte in volatile-Felder; der Herzschlag liest sie synchron. Ab Android 11 bittet MainActivity die WebView, Joystick-Ereignisse nicht im Bildtakt zu bündeln (requestUnbufferedDispatch), was bis zu 16 ms spart. Bei zwei Controllern bleibt die Seite beim Web-Gamepad, weil sich die Geräte nicht sicher zuordnen lassen.

Browser gegen App: woher die Latenz kommt

Schritt Browser (Chrome/Edge, Web Bluetooth) App ohne OmegaBle App mit OmegaBle direkt
Gamepad lesen eigener Prozess des Browsers Android-Hauptthread, gebündelt im Bildtakt OmegaPad.lesen(), ungebündelt
Befehl an Java – JSON über Hauptthread und Plugin-Thread synchron über JavaBridge
Schreiben Browser-Prozess, ohne Antwort Plugin wartet auf onCharacteristicWrite, Antwort zurück über Hauptthread eigener Schreib-Thread, keine Antwort
Meldungen der Autos Browser-Prozess je Meldung JSON über den Hauptthread (rund 22 je Sekunde und Auto) unverändert über das Community-Plugin
Folge kein Engpass jeder Befehl steht hinter allen Meldungen aller Autos in der Schlange Befehle umgehen die Schlange

Gemessen ist, dass die Rechnung selbst nicht der Engpass ist: Der ganze Takt kostet im Median 0,3 ms ohne und 0,4 ms mit zwei NPCs, unter 2 % des 45-ms-Budgets. Wie lange ein Schreibvorgang auf echter Hardware dauert, ist nicht gemessen. Gemessen ist, was passiert, wenn er länger als ein Takt dauert: Bei 46 ms Schreibdauer halbierte sich die Befehlsrate vor 0.5.8 auf 11,2 Hz; seit dem Prinzip „neuestes Paket merken“ bleibt sie bei 22,0 Hz[8].

Diagnose: der Funk-Rundlauf

Die Zeile unter Optionen → System → Funk-Rundlauf wird jede Sekunde neu geschrieben, solange sie sichtbar ist[9]. Ihre Teile in der Reihenfolge der Anzeige (die Zahlen in der ersten Spalte sind Beispiele, keine Messwerte):

Teil der Anzeige Bedeutung Quelle
12 / 20 ms Mittelwert und 95. Perzentil der Dauer eines Steuerschreibens auf der Seite, über die letzten 200 Vorgänge. In der App mit „(nur JS)“ markiert: Dort kehrt der Aufruf sofort zurück, die Zahl sagt also nichts über den Funk funkStatistik()
n× hing wie oft der Wachhund einen Vorgang nach 300 ms freigeben musste funkMess.haenger
noch keine Befehle seit dem Laden wurde noch nichts geschrieben –
Weg OmegaBle direkt / OmegaBle / Plugin welcher Schreibweg für das erste Auto der Liste gilt: Direktweg, OmegaBle über Capacitor, oder Rückfall auf das Community-Plugin omegaInfo().weg
(alte APK) die APK kennt status() nicht l.alt
(n Auto(s) fest auf Plugin) wie viele Autos gerade gesperrt sind omegaInfo().gesperrt
x geschrieben, y wiederholt, z verworfen Zähler von OmegaBle für das erste Auto seit dem Verbinden: angenommene Schreibbefehle, zweite und dritte Versuche, aufgegebene Befehle status()
Direktweg fehlt (nach Neuladen) window.OmegaDirekt fehlt; „nach Neuladen“, wenn direktNeu schon versucht wurde omegaInfo().direkt
Pad direkt fehlt window.OmegaPad fehlt omegaInfo().pad
Fehlertext warum die Anmeldung der Direktwege scheiterte (Java-Ausnahme) direktFehler
, n ersetzt Befehle, die beim Einreihen von einem neueren überschrieben wurden status().ersetzt
Brücke 1,2/4 ms Zeit von Date.now() der Seite bis zur Ankunft in OmegaBle: Mittel seit Start / Maximum seit der letzten Abfrage brueckeMittel, brueckeMax
Takt 45/48/60 ms (x % > 60), Arbeit 0,6 ms Abstand zweier Herzschläge (Soll 45 ms): Mittel, 95. Perzentil, Maximum über die letzten 400; Anteil über 60 ms; 95. Perzentil der Rechenzeit eines Herzschlags. Ausreißer zeigen einen überlasteten Hauptthread herzStatistik()
Pad direkt an (3 ms) ob das Gamepad direkt gelesen wird, und die mittlere Zustellzeit eines Achsereignisses im Android-Eingabeweg padNativInfo()
Ton +40 ms Ausgabeverzögerung des Tons (baseLatency + outputLatency) Web Audio
WebView 130 Hauptversion der WebView (Chrome) User-Agent

Der ⓘ-Text nennt die ersten Zahlen „bis zur Bestätigung durch das Bluetooth“; das gilt nur im Browser. In der App messen sie, wie gesagt, nur JavaScript.

Fehlerbilder und ihre Ursachen

Fehlerbild Ursache Abhilfe / Behebung
Auto leuchtet hell wie verbunden, App ist es nicht; nur das Ladegerät hilft Community-Plugin ließ eine Verbindung stehen (überlappende Aufbauten, gescheiterte Dienstsuche, disconnect ohne Schließen) seit 1.2.3: ein Aufbau je Auto, 20-s-Grenze, Schließen beim Start, „Alle trennen“ räumt verwaiste Verbindungen
Nach Schließen und Öffnen der App ist das Auto unsichtbar alter Prozess hielt die Verbindung; Auto wirbt nicht MainActivity.bluetoothTrennen(); die Brücke übernimmt ein noch verbundenes Auto über getConnectedDevices
Spürbare Eingabeverzögerung nur in der App Befehle über den Hauptthread, Bluetooth-Meldungen aller Autos davor in der Schlange OmegaBle direkt, Pad direkt (0.10.4); Anzeige prüfen
„Weg OmegaBle“ statt „OmegaBle direkt“, kein „Pad direkt“ Direktwege nicht in der Seite angekommen direktNeu und einmal neu laden (1.0.40); WebView aktualisieren
Weg wechselt ständig zwischen OmegaBle und Plugin Rückfallregel kippte (bis 1.1.22); Pausen des Plugins zählten als Ausfall (bis 1.1.17) Sperre mit Verdopplung (1.1.23), Pausen ausgenommen (1.1.18), nur „gar nichts geschrieben“ (1.1.24)
Mit einem NPC gut, mit mehreren halten Autos nicht, fahren chaotisch Funklast: rund 110 Pakete/s auf einem Adapter, viel „wiederholt“ und „verworfen“ NPC-Sendebudget 30 Pakete/s (1.1.24); bei weiteren Meldungen erst messen
Nach Neuverbinden eine Runde lang Verzögerung Schreibversuche ins Leere lösten die Sperre aus; Sperre lief nach dem Neuverbinden weiter 1.2.3: kein Schreiben während des Neuverbindens, Sperre wird beim Verbinden gelöscht
Mit 3 NPCs und Safety Car wieder Verzögerung, nach Neuverbinden gut Android fiel auf ein langsames Verbindungsintervall zurück Meldetakt-Überwachung, Priorität neu anfordern (1.2.3)
Altes Handy: Auto stockt, reagiert kaum (mit gekoppeltem Pad) 5-s-Prioritätsanforderung war mit Pad ausgelassen wieder immer nachgelegt (1.1.3)
Jedes Paket läuft doppelt durch die Auswertung altes Notification-Abo blieb nach Neuverbinden Abo je Ereignisname, altes wird entfernt (1.1.3)
Auto reagiert nach Mehrspieler-Beitritt nicht mehr eine verlorene Plugin-Antwort hielt eine globale Sperre für immer Schloss je Merkmal mit 300-ms-Wachhund (0.8.41)

Beispiele

Ersetzt, geschrieben, verworfen

Ein Auto, der Adapter ist kurz belegt. Zeit in ms, Befehle A, B, C kommen im 45-ms-Takt:

  1. t = 0: A kommt, Platz leer → A liegt auf dem Platz; laeuft war false → Abarbeiten startet. Schreib-Thread holt A, Versuch 1 wird abgelehnt (belegt).
  2. t = 3: kein neuerer Befehl → wiederholt = 1, Versuch 2 abgelehnt. t = 6: wiederholt = 2, Versuch 3 abgelehnt. → verworfen = 1.
  3. t = 45: B kommt, Platz leer, Abarbeiten startet, Versuch 1 gelingt → geschrieben = 1.
  4. Angenommen, der Schreib-Thread hängt beim Versuch für B länger (z. B. Android-interne Wartezeit), und C kommt bei t = 90 und D bei t = 135, bevor B fertig ist: C liegt auf dem Platz, D ersetzt C → ersetzt = 1. Danach holt der Thread D.

Ein gesundes Bild bei einem Auto: „verworfen“ und „ersetzt“ bleiben nahe null. Mit vielen Autos sind einige Prozent „verworfen“ normal.

Verdopplung der Sperre

Ein Auto, dessen OmegaBle-Weg dauerhaft nicht schreibt:

Sperre Nr. Dauer Plugin-Zeit bis dahin (Summe)
1 30 s 30 s
2 60 s 1,5 min
3 120 s 3,5 min
4 240 s 7,5 min
5 480 s 15,5 min
6 und folgende 600 s (Deckel) je weitere 10 min

Zwischen zwei Sperren liegt jeweils eine Probe von 1,5 s mit Blicken im 500-ms-Takt. Ein neuer Verbindungsaufbau setzt alles zurück.

NPC-Sendebudget

fahrende NPCs n Abstand je NPC Pakete je NPC und Sekunde NPC-Pakete gesamt
1 0 (voller Takt 45 ms) 22 22
2 max(45, 67) − 5 = 62 ms 16 32
3 max(45, 100) − 5 = 95 ms 10,5 32
4 max(45, 133) − 5 = 128 ms 7,8 31

Ein unveränderter Befehl geht bei n ≥ 2 höchstens alle 200 ms hinaus. Dazu kommt der Spieler mit 22 Paketen je Sekunde.

Wann die Priorität neu angefordert wird

Ein Auto meldete bisher stabil alle 45 ms, plötzlich alle 90 ms. Nach k Meldungen gilt für den gleitenden Mittelwert: 90 − 45 × 0,95^k. Er überschreitet 75 ms, sobald 0,95^k < 1/3, also ab k = 22 Meldungen. Bei 90 ms Abstand sind das rund 2 s. Danach wird frühestens nach 20 s erneut angefordert.

Brückenzeit lesen

Anzeige „Brücke 0,8/3 ms“: Ein Befehl braucht von der Seite bis OmegaBle im Mittel 0,8 ms, der langsamste seit dem letzten Blick 3 ms. Liegen hier Werte von 20 ms und mehr, wartet der Befehl vor OmegaBle; das deutet auf den Weg über Capacitor statt den Direktweg.

Grenzen und bekannte Probleme

Versionsgeschichte

Version Änderung
0.5.8 ein Sender, „neuestes Paket merken“ statt verwerfen (Grundlage, noch ohne OmegaBle)
0.8 Android-App (experimentell) mit Bluetooth-Brücke auf dem Community-Plugin
0.8.41 Schloss je Merkmal mit 300-ms-Wachhund, kurzes Plugin-Timeout, erst senden dann malen, Anzeige Funk-Rundlauf
0.8.124 erste Fassung von OmegaBle
0.9.2 OmegaBle neu gebaut: höchstens 3 Versuche mit 3 ms Pause statt Dauerschleife, GATT-Griff erst nach 3 Fehlschlägen vergessen, atomares Freigeben, ehrliches status(), pause()
0.9.6 Sicherungsdatei; in der App MAC-Adresse als stabile Kennung
0.9.28 Spieler 2 und 3 schreiben über denselben Weg wie Spieler 1 (Ersetzen statt Verwerfen)
0.10.4 Direktweg OmegaDirekt, Gamepad direkt, ungebündelte Joystick-Ereignisse, Priorität nur noch einmal nach 5 s statt alle 10 s, echte Werte im Funk-Rundlauf
1.0.40 fehlender Direktweg wird neu angemeldet, Anzeige nennt den Grund
1.0.73 automatisches Neuverbinden mit Wartezeit; 5-s-Priorität mit Pad ausgelassen
1.1.3 bekannte Autos beim Start verbinden; Priorität wieder immer; kein doppeltes Abo; kein Senden während des Neuverbindens
1.1.18 Verworfenes während einer Plugin-Pause zählt nicht als Ausfall
1.1.23 Sperre mit Verdopplung (30 s bis 10 min) statt Hin- und Herkippen
1.1.24 Rückfall nur, wenn gar nichts mehr geschrieben wird; NPC-Sendebudget
1.2.3 hartes Schließen (schliessen, alleSchliessen, verwaisteTrennen), 20-s-Grenze, ein Aufbau je Auto, Meldetakt-Überwachung, NPCs schreiben nicht während des Neuverbindens (neue APK nötig)

Einzelnachweise

  1. ↑ Kopfkommentar, src/05-app-bruecke.js.
  2. ↑ Klasse Platz und Felder, OmegaBlePlugin.java.
  3. ↑ funkSchreiben(), FUNK_HAENGT_MS, src/20-protocol.js.
  4. ↑ Kommentar „EIN SCHLOSS JE MERKMAL, MIT WACHHUND“, src/20-protocol.js.
  5. ↑ omegaBleBereit(), OMEGA_SPERRE_MS, src/05-app-bruecke.js.
  6. ↑ meldetaktBeobachten(), src/90-ghosts.js; Gatt._verbinden(), src/05-app-bruecke.js.
  7. ↑ npcSendeAbstand(), writeToCar(), src/90-ghosts.js.
  8. ↑ CARRERA_HYBRID.md, Abschnitt „Der Steuerweg“.
  9. ↑ Anzeige-Intervall in src/50-drive.js; funkStatistik(), herzStatistik(), src/20-protocol.js; omegaInfo(), src/05-app-bruecke.js.

Siehe auch

Quellen im Code