discoverHpsTerminals function

Future<HpsDiscoveryResult> discoverHpsTerminals({
  1. int port = hpsDefaultPort,
  2. Duration budget = hpsScanBudget,
  3. Duration probeTimeout = hpsProbeTimeout,
  4. Duration verifyTimeout = hpsVerifyTimeout,
  5. int concurrency = hpsScanConcurrency,
  6. bool stopAtFirst = false,
  7. Future<List<LocalIpv4>> interfaces()?,
  8. HpsTcpProbe probe = hpsTcpReachable,
  9. Client? httpClient,
})

Sucht hobex-HPS-Terminals im lokalen Netz.

Zwei Stufen, und die zweite ist die wesentliche: erst zaehlt ein TCP-Scan die Adressen mit offenem port auf, dann fragt jede davon GET /api/terminals. Nur wer darauf mit einer Terminal-Liste antwortet, ist ein Treffer -- ein offener Port allein sagt nichts, auf 8080 lauscht in fremden Netzen zu viel anderes.

Das Ergebnis traegt die TIDs gleich mit: mit DiscoveredHpsTerminal.baseUrl und einer TID daraus laesst sich unmittelbar ein HpsClient bauen, ohne eine zweite Runde ans Geraet.

iOS ab 14 verlangt dafuer die Freigabe fuer das lokale Netz. Ohne den Eintrag NSLocalNetworkUsageDescription in der Info.plist laufen die Verbindungen ins Leere, und die Suche meldet schlicht nichts gefunden -- ohne Fehler, der darauf hinweist. Wer diese Funktion einbaut, muss den Eintrag setzen.

budget deckelt den ganzen Durchlauf. Laeuft es ab, hoeren die Arbeiter auf; was bis dahin gefunden wurde, bleibt im Ergebnis -- ein Terminal, das nach zwoelf Sekunden auftaucht, ist mehr wert als ein Abbruch mit leerer Liste.

stopAtFirst bricht ab, sobald ein Terminal gefunden ist. In einer Kasse mit genau einem Terminal ist das der Normalfall und spart die Haelfte bis zwei Drittel der Wartezeit: gemessen 18,7 s ueber drei Netze gegenueber rund 8 s, wenn nach dem Treffer im ersten Netz Schluss ist. Der Bericht HpsDiscoveryResult.scanned fuehrt dann nur die tatsaechlich abgesuchten Netze -- was er auch soll.

interfaces, probe und httpClient sind Naehte fuer Tests; die echte Suche braucht sie nicht.

Implementation

Future<HpsDiscoveryResult> discoverHpsTerminals({
  int port = hpsDefaultPort,
  Duration budget = hpsScanBudget,
  Duration probeTimeout = hpsProbeTimeout,
  Duration verifyTimeout = hpsVerifyTimeout,
  int concurrency = hpsScanConcurrency,
  bool stopAtFirst = false,
  Future<List<LocalIpv4>> Function()? interfaces,
  HpsTcpProbe probe = hpsTcpReachable,
  http.Client? httpClient,
}) async {
  final clock = Stopwatch()..start();
  final netze = selectScanInterfaces(
    await (interfaces ?? listLocalIpv4)(),
  );
  final scanned = <ScannedHpsSubnet>[];
  final found = <DiscoveredHpsTerminal>[];

  final client = httpClient ?? http.Client();
  final ownsClient = httpClient == null;
  try {
    for (final netz in netze) {
      if (stopAtFirst && found.isNotEmpty) break;
      final hosts = subnetHosts(netz.address);
      scanned.add(ScannedHpsSubnet(
        interface: netz.name,
        subnet: subnetOf(netz.address),
        hosts: hosts.length,
      ));
      if (clock.elapsed >= budget) continue;

      final offen = await _scanSubnet(
        hosts: hosts,
        port: port,
        timeout: probeTimeout,
        concurrency: concurrency,
        budget: budget - clock.elapsed,
        probe: probe,
      );

      for (final host in offen) {
        // Kein Budget-Abbruch hier: die Kandidatenliste ist kurz (ein offener
        // Port 8080 ist selten), und einen gefundenen Kandidaten ungeprueft
        // liegen zu lassen waere die teuerste Sparsamkeit -- er faellt dann
        // ganz aus dem Ergebnis.
        final infos = await _verify(
          client: client,
          host: host,
          port: port,
          timeout: verifyTimeout,
        );
        if (infos == null) continue;
        found.add(DiscoveredHpsTerminal(
          host: host,
          port: port,
          terminals: List<TerminalInfo>.unmodifiable(infos),
        ));
        if (stopAtFirst) break;
      }
    }
  } finally {
    if (ownsClient) client.close();
  }

  return HpsDiscoveryResult(
    found: List<DiscoveredHpsTerminal>.unmodifiable(found),
    scanned: List<ScannedHpsSubnet>.unmodifiable(scanned),
  );
}