| .. | .. |
|---|
| 516 | 516 | case AppLifecycleState.resumed: |
|---|
| 517 | 517 | if (_intentionalClose) break; |
|---|
| 518 | 518 | _mqttLog('MQTT: app resumed, status=$_status'); |
|---|
| 519 | | - if (_status != ConnectionStatus.connected) { |
|---|
| 520 | | - // Already knows it's disconnected — just reconnect |
|---|
| 521 | | - connect(); |
|---|
| 522 | | - } else { |
|---|
| 523 | | - // Thinks it's connected — verify by sending a ping command. |
|---|
| 524 | | - // If the connection is dead, the publish will fail or we won't |
|---|
| 525 | | - // get a pong back. Set a watchdog timer. |
|---|
| 526 | | - _mqttLog('MQTT: sending health check...'); |
|---|
| 527 | | - _publish('pailot/control/in', { |
|---|
| 528 | | - 'type': 'command', |
|---|
| 529 | | - 'command': 'ping', |
|---|
| 530 | | - 'msgId': DateTime.now().millisecondsSinceEpoch.toString(), |
|---|
| 531 | | - 'ts': DateTime.now().millisecondsSinceEpoch, |
|---|
| 532 | | - }, MqttQos.atLeastOnce); |
|---|
| 533 | | - // If no pong within 3s, force reconnect |
|---|
| 534 | | - Future.delayed(const Duration(seconds: 3), () { |
|---|
| 535 | | - if (_status == ConnectionStatus.connected) { |
|---|
| 536 | | - // Check if client is still actually connected |
|---|
| 537 | | - final client = _client; |
|---|
| 538 | | - if (client == null || client.connectionStatus?.state != MqttConnectionState.connected) { |
|---|
| 539 | | - _mqttLog('MQTT: health check failed, reconnecting...'); |
|---|
| 540 | | - _client = null; |
|---|
| 541 | | - _setStatus(ConnectionStatus.reconnecting); |
|---|
| 542 | | - connect(); |
|---|
| 543 | | - } |
|---|
| 544 | | - } |
|---|
| 545 | | - }); |
|---|
| 519 | + // iOS kills TCP sockets during suspend. Always force a clean |
|---|
| 520 | + // reconnect to avoid the "looks connected but dead" state. |
|---|
| 521 | + final resumeClient = _client; |
|---|
| 522 | + if (resumeClient != null) { |
|---|
| 523 | + _intentionalClose = true; // Prevent _onDisconnected from cascading |
|---|
| 524 | + resumeClient.autoReconnect = false; |
|---|
| 525 | + resumeClient.disconnect(); |
|---|
| 526 | + _client = null; |
|---|
| 527 | + _updatesSub?.cancel(); |
|---|
| 528 | + _updatesSub = null; |
|---|
| 529 | + _intentionalClose = false; |
|---|
| 546 | 530 | } |
|---|
| 531 | + _setStatus(ConnectionStatus.reconnecting); |
|---|
| 532 | + Future.delayed(const Duration(milliseconds: 300), () { |
|---|
| 533 | + if (!_intentionalClose) connect(); |
|---|
| 534 | + }); |
|---|
| 547 | 535 | case AppLifecycleState.paused: |
|---|
| 548 | 536 | break; |
|---|
| 549 | 537 | default: |
|---|