import 'dart:async'; import 'dart:convert'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../models/app_event.dart'; enum SocketConnectionStatus { disconnected, connecting, connected } class WebSocketService { WebSocketChannel? _channel; StreamSubscription? _subscription; Timer? _reconnectTimer; String? _deviceIp; bool _manualDisconnect = false; SocketConnectionStatus _connectionStatus = SocketConnectionStatus.disconnected; 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 _connectionController = 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 onConnectionChanged => _connectionController.stream; SocketConnectionStatus get connectionStatus => _connectionStatus; bool get isConnected => _connectionStatus == SocketConnectionStatus.connected; Future connect(String deviceIp) async { _manualDisconnect = false; _deviceIp = _normalizeDeviceIp(deviceIp); _reconnectTimer?.cancel(); await _subscription?.cancel(); await _channel?.sink.close(); _setConnectionStatus(SocketConnectionStatus.connecting); final url = Uri.parse('ws://$_deviceIp:8080/ws'); _channel = WebSocketChannel.connect(url); _subscription = _channel!.stream.listen( _handleMessage, onDone: _handleSocketClosed, onError: (_) => _handleSocketClosed(), cancelOnError: true, ); _setConnectionStatus(SocketConnectionStatus.connected); } 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; } _reconnectTimer?.cancel(); _reconnectTimer = Timer(const Duration(seconds: 2), () { unawaited(connect(deviceIp)); }); } Future disconnect() async { _manualDisconnect = true; _reconnectTimer?.cancel(); await _subscription?.cancel(); _subscription = null; await _channel?.sink.close(); _channel = null; _setConnectionStatus(SocketConnectionStatus.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 _connectionController.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; _setConnectionStatus(SocketConnectionStatus.disconnected); unawaited(reconnect()); } void _setConnectionStatus(SocketConnectionStatus status) { _connectionStatus = status; _connectionController.add(status); } 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; } }