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

Flutter Android TCP应用如何检测ESP32控制器断电?

解决Flutter Socket检测ESP32意外断电的问题

首先明确核心原因:TCP连接在一端意外断电(未发送FIN断开包)时,客户端不会立刻收到系统层面的断开通知,因此Dart Socket的onDone/onError不会自动触发。你需要手动基于心跳实现超时检测,同时修正流监听的异常问题。

具体解决方案

1. 手动实现心跳超时判定

不要依赖Socket内置超时,自己维护连接活跃状态:

  • 每次收到ESP32的任何消息(包括心跳响应),更新最后活跃时间戳
  • 启动定时任务,定期检查最后活跃时间是否超出阈值(建议设为心跳间隔的1.5倍,比如你每分钟发心跳,阈值设90秒)
  • 一旦超时,主动判定连接断开,触发离线处理逻辑

2. 修正Socket流监听异常

你使用asBroadcastStream()后若重复监听同一流,会触发“流已被监听”错误:

  • 如果不需要多订阅,直接监听原始Socket流即可,无需转为广播流
  • 若确实需要多订阅,确保只调用一次asBroadcastStream()并复用这个广播流对象,不要重复创建

3. 主动写入验证连接状态

在发送心跳时,尝试向Socket写入数据,若连接已断,写入操作会抛出异常,此时可直接捕获并处理断开逻辑。

完整代码示例

import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';

class LampSocketManager {
  Socket? _socket;
  StreamSubscription? _socketSubscription;
  Timer? _heartbeatTimer;
  Timer? _connectionCheckTimer;
  DateTime _lastActiveTime = DateTime.now();
  static const int heartbeatInterval = 60; // 心跳间隔(秒)
  static const int connectionTimeout = 90; // 超时阈值(秒)

  // 初始化连接
  Future<void> connect(String espHost, int espPort) async {
    try {
      _socket = await Socket.connect(espHost, espPort);
      _lastActiveTime = DateTime.now();
      _setupSocketListener();
      _startHeartbeat();
      _startConnectionCheck();
    } catch (e) {
      if (kDebugMode) print('连接ESP32失败: $e');
      _handleDisconnection();
    }
  }

  // 配置Socket监听
  void _setupSocketListener() {
    // 直接监听原始Socket流,无需转为广播流(除非需要多订阅)
    _socketSubscription = _socket!.listen(
      (event) {
        String receivedMsg = String.fromCharCodes(event);
        _lastActiveTime = DateTime.now(); // 更新活跃时间
        // 处理收到的消息:心跳响应、开关状态反馈等
        if (kDebugMode) print('收到ESP32消息: $receivedMsg');
      },
      onDone: () {
        if (kDebugMode) print('连接正常关闭');
        _handleDisconnection();
      },
      onError: (error) {
        if (kDebugMode) print('Socket错误: $error');
        _handleDisconnection();
      },
    );
  }

  // 启动心跳发送任务
  void _startHeartbeat() {
    _heartbeatTimer?.cancel();
    _heartbeatTimer = Timer.periodic(Duration(seconds: heartbeatInterval), (timer) {
      if (_socket != null && _socket!.isConnected) {
        try {
          _socket!.write('HEARTBEAT\n'); // 发送心跳指令
          if (kDebugMode) print('发送心跳');
        } catch (e) {
          if (kDebugMode) print('心跳发送失败: $e');
          _handleDisconnection();
        }
      }
    });
  }

  // 启动连接状态检查
  void _startConnectionCheck() {
    _connectionCheckTimer?.cancel();
    _connectionCheckTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
      final now = DateTime.now();
      if (now.difference(_lastActiveTime).inSeconds > connectionTimeout) {
        if (kDebugMode) print('连接超时,判定ESP32断电');
        _handleDisconnection();
      }
    });
  }

  // 统一处理断开逻辑
  void _handleDisconnection() {
    _heartbeatTimer?.cancel();
    _connectionCheckTimer?.cancel();
    _socketSubscription?.cancel();
    _socket?.destroy();
    _socket = null;
    // 这里触发UI更新:比如显示设备离线、启动重连逻辑等
    if (kDebugMode) print('已处理连接断开');
  }

  // 主动断开连接
  void disconnect() {
    _handleDisconnection();
  }
}

额外注意事项

  • ESP32端需配合实现心跳响应:每次收到客户端的HEARTBEAT指令,回复确认消息(比如HEARTBEAT_ACK),确保客户端能准确更新活跃时间
  • 超时阈值要略大于心跳间隔,避免网络波动导致误判
  • 若需要多订阅Socket流,仅调用一次asBroadcastStream()并保存该流对象,后续订阅均复用此对象

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 00:15:48