Skip to content

Controller Capability

Die Controller Capability ist ein Plugin im Capability-System der App. Sie wird verwendet, wenn ein Thing eine steuerbare Controller-Funktion besitzt, zum Beispiel für einen fahrbaren Roboter, eine Kameraausrichtung oder andere Geräte mit mehreren Bewegungsachsen.

In der Benutzeroberfläche stellt die Capability zwei virtuelle Joysticks bereit. Jeder Joystick besitzt eine X- und eine Y-Achse, deren Werte im Bereich von -1 bis +1 liegen. Die Mittelstellung entspricht dem Wert 0 und bedeutet, dass keine Bewegung oder Auslenkung ausgeführt wird.

Darstellung in der App

Die Controller Capability besitzt eine kompakte Ansicht und eine Vollbildansicht. In der kompakten Ansicht wird der Controller nur als Bedienelement dargestellt. Durch Antippen öffnet sich die Vollbildansicht, in der die Joysticks aktiv verwendet werden können. Dort können beide Joysticks gleichzeitig bedient werden; die aktuellen Achsenwerte werden direkt angezeigt.

graph TD
    Compact["Kompakte Ansicht<br/>(zwei Icons, nicht interaktiv)"] -->|Antippen| Modal["Vollbild-Modal"]
    Modal --> GameMode["Spielmodus-Anzeige<br/>(Geschwister-Capabilities Typ MESSAGE)"]
    Modal --> Row["Joystick-Reihe<br/>(links = Joystick 1, rechts = Joystick 2)"]
    Modal --> Stop["Not-Aus-Toggle<br/>(Geschwister-Capability id = car_stop)"]
    Row --> Close["Schließen -> Mittelstellung senden"]

Verarbeitung der Steuerwerte

Die Positionen der Joysticks werden in normalisierte X- und Y-Werte umgerechnet. Dadurch ist die Steuerung unabhängig von der tatsächlichen Bildschirmgröße oder der Darstellung des Bedienelements. Eine Bewegung nach rechts, links, oben oder unten wird immer als Wert zwischen -1 und +1 dargestellt.

Beim Loslassen eines Joysticks springt dieser wieder in die Mittelstellung zurück. Dieser Zustand wird sofort an das Gerät gesendet, damit keine alte Bewegung aktiv bleibt. Das ist besonders wichtig bei Geräten wie Robotern, da sie sonst weiterfahren könnten, obwohl der Nutzer den Joystick bereits losgelassen hat.

Die Controller Capability sendet ihre Werte als strukturiertes Payload weiter (Positionen beider Joysticks mit X/Y). Die Daten werden anschließend über die zentrale Capability-Logik der App verarbeitet.

Begrenzung der Sendefrequenz

Da Joystick-Bewegungen sehr viele Werte in kurzer Zeit erzeugen können, werden die gesendeten Updates gedrosselt (SEND_THROTTLE_MS). Wichtige Zustände wie das Loslassen eines Joysticks oder das Schließen der Vollbildansicht werden trotzdem sofort gesendet, damit das Gerät zuverlässig in die Mittelstellung zurückgesetzt wird.

Eingehende Werte und Synchronisierung

Die Controller Capability kann nicht nur Werte senden, sondern auch eingehende Zustände verarbeiten. Wenn das Gerät oder der ConnectHub aktuelle Controller-Werte zurückmeldet, werden diese in der App übernommen und in der Oberfläche dargestellt. Die Herkunft der Daten spielt dabei keine Rolle: Sie können über BLE, MQTT oder eine andere angebundene Schnittstelle in die App gelangen.

Multitouch: ein gemeinsamer Gesture-Handler statt zwei Pan-Handler

Zwei getrennte Pan-Handler für die beiden Joysticks funktionieren speziell in einem Modal unter Android nicht zuverlässig gleichzeitig: der zuerst berührte Joystick beansprucht den Touch-Stream, der zweite Finger reagiert nicht. Die App löst das mit einem einzigen Gesture.Manual()-Handler, der jede rohe Touch-Berührung anhand ihrer X-Position selbst dem linken oder rechten Joystick zuordnet:

flowchart TD
    Down["onTouchesDown"] --> Side{"absoluteX < Mitte?"}
    Side -->|ja| J1[Joystick 1 zuordnen]
    Side -->|nein| J2[Joystick 2 zuordnen]
    Move["onTouchesMove"] --> Offset["Auslenkung relativ zum Startpunkt<br/>auf max. Radius begrenzen"]
    Offset --> Normalize["auf -1..+1 normalisieren -> updateJoystick()"]
    Up["onTouchesUp / onTouchesCancelled"] --> Release["Joystick freigeben,<br/>Position zurück auf 0,0"]
const controllerGesture = Gesture.Manual()
  .runOnJS(true)
  .onTouchesDown((e, manager) => {
    for (const t of e.changedTouches) {
      if (pointersRef.current.size >= 2) break;
      const key: JoystickKey = t.absoluteX < rowMidXRef.current ? 'joystick1' : 'joystick2';
      const occupied = [...pointersRef.current.values()].some((v) => v.key === key);
      if (!occupied) pointersRef.current.set(t.id, { key, ox: t.absoluteX, oy: t.absoluteY });
    }
    if (pointersRef.current.size > 0) manager.activate();
  })
  .onTouchesMove((e) => {
    for (const t of e.changedTouches) {
      const p = pointersRef.current.get(t.id);
      if (!p) continue;
      const dx = t.absoluteX - p.ox;
      const dy = t.absoluteY - p.oy;
      const dist = Math.sqrt(dx * dx + dy * dy);
      const maxOffset = maxOffsetRef.current;
      const cDx = dist > maxOffset ? (dx / dist) * maxOffset : dx;
      const cDy = dist > maxOffset ? (dy / dist) * maxOffset : dy;
      updateJoystickRef.current(p.key, {
        x: Number(clamp(cDx / maxOffset, -1, 1).toFixed(3)),
        y: Number(clamp(-cDy / maxOffset, -1, 1).toFixed(3)),
      });
    }
  })
  .onTouchesUp((e, manager) => {
    releaseTouches(e.changedTouches);
    if (pointersRef.current.size === 0) manager.end();
  });

Gedrosseltes Senden

scheduleSend() sendet sofort, wenn seit dem letzten Update mindestens SEND_THROTTLE_MS vergangen sind, plant sonst genau ein verzögertes Update ein. Bei immediate = true (Loslassen, Modal schließen) wird ohne Verzögerung gesendet:

function scheduleSend(nextState: ControllerState, immediate = false) {
  setControllerState(nextState);
  latestStateRef.current = nextState;
  if (!writable) return;

  const now = Date.now();
  if (immediate || now - lastSentAtRef.current >= SEND_THROTTLE_MS) {
    lastSentAtRef.current = now;
    onValueChange?.(toPayload(nextState));
    return;
  }
  if (pendingTimeoutRef.current) return;
  pendingTimeoutRef.current = setTimeout(() => {
    pendingTimeoutRef.current = null;
    lastSentAtRef.current = Date.now();
    onValueChange?.(toPayload(latestStateRef.current));
  }, SEND_THROTTLE_MS - (now - lastSentAtRef.current));
}

Registrierung im Plugin-System

Wie jede Capability wird die Komponente über registerPlugin() registriert und behandelt Things vom Typ CONTROLLER:

registerPlugin({
  component: ControllerCapability,
  canHandle: (cap) => cap.type === 'CONTROLLER',
  priority: 10,
});

Siehe auch