"""Wi-Fi Direct und die Anmeldung als Wi-Fi-Display-Quelle. Hier liegt der entscheidende Unterschied zur Android-Fassung: Über wpa_supplicant lässt sich das WFD-Informationselement wirklich setzen. Damit erkennt der Fernseher uns als Miracast-Quelle und baut die Steuerverbindung von sich aus auf. Auf Android ist genau das Systemapps vorbehalten. """ import fcntl import logging import re import shutil import socket import struct import subprocess import time from dataclasses import dataclass from typing import Dict, List, Optional from .wpa import WpaClient, parse_key_values log = logging.getLogger("wfd.p2p") # WFD-Gerätetyp in den unteren zwei Bits des Informationsfeldes. DEVICE_TYPE_SOURCE = 0b00 DEVICE_TYPE_PRIMARY_SINK = 0b01 SESSION_AVAILABLE = 0b01 << 4 @dataclass class Peer: address: str name: str is_sink: bool session_available: bool control_port: int device_type: str = "" def describe(self) -> str: marks = [] if self.is_sink: marks.append("Bildschirm") if self.session_available: marks.append("wartet auf Verbindung") suffix = f" ({', '.join(marks)})" if marks else "" return f"{self.name} [{self.address}]{suffix}" @dataclass class Group: interface: str role: str # "GO" oder "client" go_address: str = "" local_address: str = "" def parse_wfd_dev_info(value: str) -> Dict[str, object]: """Zerlegt wfd_dev_info, z.B. 0x00111c440032. Die ersten zwei Bytes sind das Informationsfeld, dann folgt der Port für die Steuerverbindung und der maximale Durchsatz. """ text = value.strip() if text.startswith("0x"): text = text[2:] if len(text) < 4: return {} info = int(text[0:4], 16) port = int(text[4:8], 16) if len(text) >= 8 else 7236 device_type = info & 0b11 return { "info": info, "device_type": device_type, "is_sink": device_type in (DEVICE_TYPE_PRIMARY_SINK, 0b10, 0b11), "session_available": bool((info >> 4) & 0b11), "control_port": port or 7236, } def interface_ip(interface: str) -> str: """Eigene IPv4-Adresse eines Interfaces, ohne externe Werkzeuge.""" try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) packed = struct.pack("256s", interface[:15].encode()) return socket.inet_ntoa(fcntl.ioctl(sock.fileno(), 0x8915, packed)[20:24]) except OSError: return "" def default_gateway(interface: str) -> str: """Gegenstelle der Gruppe - beim Wi-Fi-Direct-Client also der Fernseher.""" try: with open("/proc/net/route", encoding="ascii") as handle: for line in handle.readlines()[1:]: fields = line.split() if fields[0] != interface or fields[1] != "00000000": continue return socket.inet_ntoa(struct.pack(" bool: """Kündigt uns als Miracast-Quelle an, die eine Sitzung annehmen kann.""" if not self.client.ok("SET wifi_display 1"): log.warning("wifi_display liess sich nicht einschalten") return False info = DEVICE_TYPE_SOURCE | SESSION_AVAILABLE payload = f"{info:04x}{control_port:04x}{max_throughput:04x}" # Subelement 0 ist die Geräteinformation, davor steht ihre Länge (6 Byte). if not self.client.ok(f"WFD_SUBELEM_SET 0 0006{payload}"): log.warning("WFD-Kennzeichen liess sich nicht setzen") return False log.info("Als Wi-Fi-Display-Quelle angemeldet (Port %s)", control_port) return True def disable_wfd(self): self.client.ok("SET wifi_display 0") # ------------------------------------------------------------- Suchen def find(self, seconds: int = 30): self.client.ok(f"P2P_FIND {seconds}") def stop_find(self): self.client.ok("P2P_STOP_FIND") def peers(self) -> List[Peer]: raw = self.client.request("P2P_PEERS") result = [] for address in raw.split(): if not re.fullmatch(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", address): continue details = parse_key_values(self.client.request(f"P2P_PEER {address}")) wfd = parse_wfd_dev_info(details.get("wfd_dev_info", "")) result.append(Peer( address=address, name=details.get("device_name", address), is_sink=bool(wfd.get("is_sink", False)), session_available=bool(wfd.get("session_available", False)), control_port=int(wfd.get("control_port", 7236)), device_type=details.get("pri_dev_type", ""), )) return result # ---------------------------------------------------------- Verbinden def connect(self, peer: Peer, go_intent: int = 0, method: str = "pbc") -> str: """Stellt die Direktverbindung her. go_intent=0 heißt: der Fernseher soll die Gruppe führen - dann bekommen wir von ihm eine Adresse.""" command = f"P2P_CONNECT {peer.address} {method} go_intent={go_intent}" answer = self.client.request(command, timeout=10) log.info("P2P_CONNECT -> %s", answer) return answer def wait_for_group(self, timeout: float = 60.0) -> Optional[Group]: """Wartet auf den Gruppenstart und liefert Interface und Rolle.""" event = self.client.wait_for_event("P2P-GROUP-STARTED", timeout=timeout) if not event: return None # Beispiel: P2P-GROUP-STARTED p2p-wlan0-0 client ssid="DIRECT-xy" ... parts = event.split() if len(parts) < 3: return None interface = parts[1] role = "GO" if parts[2].upper() == "GO" else "client" return Group(interface=interface, role=role) def remove_group(self, interface: str = ""): self.client.ok(f"P2P_GROUP_REMOVE {interface or '*'}") def obtain_address(interface: str, timeout: int = 20) -> str: """Holt per DHCP eine Adresse in der Gruppe. wpa_supplicant kümmert sich nicht um IP-Adressen; als Gruppen-Gast müssen wir selbst fragen. Wir probieren die üblichen Clients der Reihe nach. """ existing = interface_ip(interface) if existing: return existing candidates = [ (["dhclient", "-1", "-v", interface], 25), (["udhcpc", "-i", interface, "-q", "-n", "-t", "6"], 20), (["dhcpcd", "-4", "-t", str(timeout), interface], timeout + 5), ] for command, limit in candidates: if not shutil.which(command[0]): continue log.info("Hole Adresse mit %s", command[0]) try: subprocess.run(command, timeout=limit, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except (subprocess.TimeoutExpired, OSError) as error: log.debug("%s erfolglos: %s", command[0], error) address = interface_ip(interface) if address: return address # Letzter Ausweg: die übliche Aufteilung einer Wi-Fi-Direct-Gruppe. deadline = time.monotonic() + 5 while time.monotonic() < deadline: address = interface_ip(interface) if address: return address time.sleep(0.5) return ""