import 'dart:async'; import 'dart:convert'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../models/app_event.dart'; enum WsConnectionState { connected, connecting, disconnected } @Deprecated('Use WsConnectionState instead') enum SocketConnectionStatus { disconnected, connecting, connected } class WebSocketService { static const Duration _initialReconnectDelay = Duration(seconds: 2); static const Duration _maxReconnectDelay = Duration(seconds: 60); WebSocketChannel? _channel; StreamSubscription? _subscription; Timer? _reconnectTimer; String? _deviceIp; int _devicePort = 5000; bool _manualDisconnect = false; WsConnectionState _connectionState = WsConnectionState.disconnected; int _retryCount = 0; Duration _nextReconnectDelay = _initialReconnectDelay; final StreamController _eventController = StreamController.broadcast(); final StreamController> _statusController = StreamController>.broadcast(); final StreamController> _stateController = StreamController>.broadcast(); final StreamController> _configController = StreamController>.broadcast(); final StreamController> _wifiController = StreamController>.broadcast(); final StreamController> _bleController = StreamController>.broadcast(); final StreamController _connectionStateController = StreamController.broadcast(); Stream get events => _eventController.stream; Stream> get onStatusUpdate => _statusController.stream; Stream> get onStateUpdate => _stateController.stream; Stream> get onConfigUpdate => _configController.stream; Stream> get onWifiUpdate => _wifiController.stream; Stream> get onBleUpdate => _bleController.stream; Stream get connectionState => _connectionStateController.stream.map(_toLegacyConnectionStatus); Stream get connectionStateStream => _connectionStateController.stream; Stream get onConnectionChanged => _connectionStateController.stream.map(_toLegacyConnectionStatus); WsConnectionState get wsConnectionState => _connectionState; SocketConnectionStatus get connectionStatus => _toLegacyConnectionStatus(_connectionState); bool get isConnected => _connectionState == WsConnectionState.connected; int get retryCount => _retryCount; Future connect(String deviceIp, {int port = 5000}) async { _manualDisconnect = false; _deviceIp = _normalizeDeviceIp(deviceIp); _devicePort = _normalizePort(port); _reconnectTimer?.cancel(); await _establishConnection(resetBackoff: true); } Future manualReconnect() async { final deviceIp = _deviceIp; if (deviceIp == null || deviceIp.isEmpty) { return; } _manualDisconnect = false; _reconnectTimer?.cancel(); await _establishConnection(resetBackoff: true); } Future _establishConnection({bool resetBackoff = false}) async { if (resetBackoff) { _retryCount = 0; _nextReconnectDelay = _initialReconnectDelay; } await _subscription?.cancel(); _subscription = null; await _channel?.sink.close(); _channel = null; _setConnectionState(WsConnectionState.connecting); try { final url = Uri.parse('ws://$_deviceIp:$_devicePort/ws'); _channel = WebSocketChannel.connect(url); await _channel!.ready; _subscription = _channel!.stream.listen( _handleMessage, onDone: _handleSocketClosed, onError: (_) => _handleSocketClosed(), cancelOnError: true, ); _retryCount = 0; _nextReconnectDelay = _initialReconnectDelay; _setConnectionState(WsConnectionState.connected); } catch (_) { _channel = null; _subscription = null; if (_manualDisconnect) { _setConnectionState(WsConnectionState.disconnected); return; } await reconnect(); } } void sendCommand(Map command) { if (!isConnected || _channel == null) { throw StateError('WebSocket 未连接'); } _channel!.sink.add(jsonEncode(command)); } Future reconnect() async { final deviceIp = _deviceIp; if (deviceIp == null || deviceIp.isEmpty || _manualDisconnect) { return; } _setConnectionState(WsConnectionState.connecting); _scheduleReconnect(); } void _scheduleReconnect() { if (_manualDisconnect) { return; } _reconnectTimer?.cancel(); final delay = _nextReconnectDelay; _retryCount += 1; _nextReconnectDelay = _nextReconnectDelay * 2; if (_nextReconnectDelay > _maxReconnectDelay) { _nextReconnectDelay = _maxReconnectDelay; } _reconnectTimer = Timer(delay, () { unawaited(_establishConnection()); }); } Future disconnect() async { _manualDisconnect = true; _reconnectTimer?.cancel(); _retryCount = 0; _nextReconnectDelay = _initialReconnectDelay; await _subscription?.cancel(); _subscription = null; await _channel?.sink.close(); _channel = null; _setConnectionState(WsConnectionState.disconnected); } Future dispose() async { await disconnect(); await _eventController.close(); await _statusController.close(); await _stateController.close(); await _configController.close(); await _wifiController.close(); await _bleController.close(); await _connectionStateController.close(); } void _handleMessage(dynamic data) { final raw = data is String ? data : utf8.decode(data as List); final decoded = jsonDecode(raw); if (decoded is! Map) { return; } final event = AppEvent.fromJson(Map.from(decoded)); _eventController.add(event); switch (event.type) { case 'status_update': _statusController.add(event.payload); break; case 'state_update': _stateController.add(event.payload); break; case 'config_update': _configController.add(event.payload); break; case 'wifi_update': _wifiController.add(event.payload); break; case 'ble_update': _bleController.add(event.payload); break; } } void _handleSocketClosed() { _channel = null; _subscription = null; if (_manualDisconnect) { _retryCount = 0; _nextReconnectDelay = _initialReconnectDelay; _setConnectionState(WsConnectionState.disconnected); return; } unawaited(reconnect()); } void _setConnectionState(WsConnectionState state) { _connectionState = state; _connectionStateController.add(state); } SocketConnectionStatus _toLegacyConnectionStatus(WsConnectionState state) { switch (state) { case WsConnectionState.connected: return SocketConnectionStatus.connected; case WsConnectionState.connecting: return SocketConnectionStatus.connecting; case WsConnectionState.disconnected: return SocketConnectionStatus.disconnected; } } String _normalizeDeviceIp(String raw) { var value = raw.trim(); if (value.startsWith('ws://')) { value = value.substring(5); } else if (value.startsWith('wss://')) { value = value.substring(6); } else if (value.startsWith('http://')) { value = value.substring(7); } else if (value.startsWith('https://')) { value = value.substring(8); } final slashIndex = value.indexOf('/'); if (slashIndex >= 0) { value = value.substring(0, slashIndex); } final colonIndex = value.indexOf(':'); if (colonIndex >= 0) { value = value.substring(0, colonIndex); } return value; } int _normalizePort(int value) { if (value <= 0 || value > 65535) { return 5000; } return value; } }