You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter技术问题:等待变量变为指定值(UDP设备发现)

问题解决:UDP广播获取ESP8266 IP时的StreamController报错与自身IP过滤问题

问题根源

  1. 原代码找到ESP IP后关闭了StreamController,但RawDatagramSocket的监听并未停止,后续收到的数据包(包括自身广播的延迟包)会尝试往已关闭的StreamSink中添加事件,触发Bad state: Cannot add event after closing异常。
  2. 未正确释放RawDatagramSocket资源,多次调用时存在遗留的socket实例持续发送事件。
  3. 使用StreamController的实现方式冗余,完全可以用更轻量的Completer完成异步等待逻辑。

修复方案

用Completer替代StreamController,找到目标IP后立即关闭socket停止接收事件,同时添加超时处理避免无限等待:

import 'dart:async';
import 'dart:io';

class EspDeviceUdp {
  /// Port to send UDP messages to.
  int port = 2390;

  /// IP address of the smartphone.
  final InternetAddress ownIP;

  /// IP address of the target device.
  InternetAddress tarIP = InternetAddress('0.0.0.0');

  EspDeviceUdp({required this.ownIP});

  /// Tries to find the esp in network while using udp broadcast
  Future<InternetAddress?> getEspIp({Duration timeout = const Duration(seconds: 5)}) async {
    final Uint8List tXData = Uint8List(12);
    const String ipAdr = "255.255.255.255";
    const int id = udpIdHandshakeRequest;
    final completer = Completer<InternetAddress?>();
    RawDatagramSocket? udpSocket;

    // Build tx data stream
    buildUdpStream(tXData, id, 0, 0, 0);

    try {
      udpSocket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port);
      udpSocket.broadcastEnabled = true;

      // Listen for incoming datagrams
      udpSocket.listen((RawSocketEvent event) {
        if (event == RawSocketEvent.read) {
          final datagram = udpSocket!.receive();
          if (datagram != null) {
            final address = datagram.address;
            // Filter out own IP and avoid duplicate completion
            if (address != ownIP && !completer.isCompleted) {
              completer.complete(address);
              udpSocket.close();
            }
          }
        }
      }, onError: (error) {
        if (!completer.isCompleted) {
          completer.completeError(error);
          udpSocket?.close();
        }
      }, onDone: () {
        if (!completer.isCompleted) {
          completer.complete(null);
        }
      });

      // Send broadcast request
      udpSocket.send(tXData, InternetAddress(ipAdr), port);

      // Add timeout to avoid infinite waiting
      return await completer.future.timeout(timeout, onTimeout: () {
        udpSocket?.close();
        return null;
      });
    } catch (e) {
      udpSocket?.close();
      rethrow;
    }
  }

  /// Building the tx stream to config the esp device via udp.
  void buildUdpStream(
      Uint8List txDataList, int id, int data1, int data2, int data3) {
    // id
    txDataList[0] = id & 0xFF;
    txDataList[1] = (id >> 8) & 0xFF;
    // data1
    txDataList[2] = data1 & 0xFF;
    txDataList[3] = (data1 >> 8) & 0xFF;
    // data2
    txDataList[4] = data2 & 0xFF;
    txDataList[5] = (data2 >> 8) & 0xFF;
    // data3
    txDataList[6] = data3 & 0xFF;
    txDataList[7] = (data3 >> 8) & 0xFF;
    // reserved
    txDataList[8] = 0;
    txDataList[9] = 0;
    txDataList[10] = 0;
    txDataList[11] = 0;
  }

  // 补充定义缺失的常量(已有则删除)
  static const int udpIdHandshakeRequest = 0x0100;
}

关键修改点说明

  1. Completer替代StreamController:
    Completer是专门处理一次性异步结果的工具,比StreamController更适配"等待第一个符合条件结果"的场景,找到目标IP后立即完成异步任务并关闭socket。

  2. 及时释放Socket资源:
    无论找到IP、超时还是发生错误,都会关闭RawDatagramSocket,避免资源泄漏和后续无效事件触发。

  3. 超时处理:
    添加5秒超时逻辑,防止因网络问题或ESP设备未响应导致的无限等待,超时后自动关闭socket并返回null。

  4. 过滤逻辑优化:
    在读取事件中直接过滤自身IP,同时检查completer.isCompleted,避免重复触发完成操作。

测试验证

第二次调用getEspIp()时会创建全新的socket实例,用完即关闭,不会出现旧socket往已关闭的StreamSink发送事件的情况,彻底解决Bad state异常。

内容的提问来源于stack exchange,提问作者Kerendel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 15:24:15