// ============================================================================= // screens/settings/port_settings_screen.dart — COM port configuration // ============================================================================= // // Lets the operator choose which COM port is the concentrador RX-OUT // (the one the display reads from) and which is the TX-IN // (the one the display writes commands to). // // Ports are persisted in SharedPreferences and auto-applied at startup. // // Layout: // • Two dropdowns — RX port, TX port — populated from available COM ports // • [Conectar] button — tries to open both ports; shows error if it fails // • [Desconectar] button — drops to demo mode // • Status row — green ● CONECTADO / grey ● DEMO // ============================================================================= import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../data/autopilot_state.dart'; import '../../theme/autopilot_theme.dart'; import '../../theme/theme_provider.dart'; class PortSettingsScreen extends StatefulWidget { const PortSettingsScreen({super.key}); static const String routeName = '/settings/ports'; @override State createState() => _PortSettingsScreenState(); } class _PortSettingsScreenState extends State { static const _kRxKey = 'port.rx'; static const _kTxKey = 'port.tx'; List _ports = []; String? _rxPort; String? _txPort; bool _connecting = false; String? _errorMsg; @override void initState() { super.initState(); _loadPorts(); } Future _loadPorts() async { final available = AutopilotState.availablePorts(); final prefs = await SharedPreferences.getInstance(); setState(() { _ports = available; _rxPort = prefs.getString(_kRxKey); _txPort = prefs.getString(_kTxKey); // If saved port no longer exists, clear it. if (_rxPort != null && !_ports.contains(_rxPort)) _rxPort = null; if (_txPort != null && !_ports.contains(_txPort)) _txPort = null; }); } Future _connect() async { if (_rxPort == null || _txPort == null) return; setState(() { _connecting = true; _errorMsg = null; }); try { final ap = context.read(); await ap.connectToSerial(rxPort: _rxPort!, txPort: _txPort!); // Persist the working configuration final prefs = await SharedPreferences.getInstance(); await prefs.setString(_kRxKey, _rxPort!); await prefs.setString(_kTxKey, _txPort!); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Conectado — RX: $_rxPort TX: $_txPort'), backgroundColor: Colors.green.shade700, duration: const Duration(seconds: 3), ), ); Navigator.pop(context); } } catch (e) { setState(() => _errorMsg = 'Error al abrir puertos: $e'); } finally { if (mounted) setState(() => _connecting = false); } } Future _disconnect() async { final ap = context.read(); await ap.disconnectSerial(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Desconectado — modo demo activo'), duration: Duration(seconds: 2), ), ); Navigator.pop(context); } } @override Widget build(BuildContext context) { final theme = context.watch().current; final ap = context.watch(); return Scaffold( backgroundColor: theme.background, appBar: AppBar( backgroundColor: theme.backgroundMid, foregroundColor: theme.textMain, elevation: 0, title: Text( 'Conexión al Concentrador', style: TextStyle(color: theme.textMain, fontWeight: FontWeight.w600), ), ), body: AnimatedContainer( duration: const Duration(milliseconds: 400), decoration: theme.backgroundDecoration, child: ListView( padding: const EdgeInsets.all(20), children: [ // ── Status ──────────────────────────────────────────────────────── _StatusRow(theme: theme, connected: ap.isConnected), const SizedBox(height: 24), // ── Port selection ──────────────────────────────────────────────── _SectionLabel(label: 'Puerto RX (leer datos del concentrador)', theme: theme), const SizedBox(height: 8), _PortDropdown( theme: theme, value: _rxPort, ports: _ports, onChanged: (v) => setState(() => _rxPort = v), ), const SizedBox(height: 16), _SectionLabel(label: 'Puerto TX (enviar comandos al concentrador)', theme: theme), const SizedBox(height: 8), _PortDropdown( theme: theme, value: _txPort, ports: _ports, onChanged: (v) => setState(() => _txPort = v), ), const SizedBox(height: 8), // Note Text( 'Conectar el cable USB-OUT del concentrador al puerto RX,\n' 'y el USB-IN al puerto TX. Ambos a 115 200 baud, 8N1.', style: TextStyle(color: theme.textMuted, fontSize: 11), ), const SizedBox(height: 24), // ── Error message ───────────────────────────────────────────────── if (_errorMsg != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.disengageBackground.colors.first.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), border: Border.all(color: theme.disengageBorder.withValues(alpha: 0.4)), ), child: Text( _errorMsg!, style: TextStyle(color: theme.disengageText, fontSize: 12), ), ), const SizedBox(height: 16), ], // ── Buttons ─────────────────────────────────────────────────────── Row( children: [ Expanded( child: _ActionButton( theme: theme, label: _connecting ? 'Conectando…' : 'Conectar', enabled: !_connecting && _rxPort != null && _txPort != null, primary: true, onPressed: _connect, ), ), const SizedBox(width: 12), Expanded( child: _ActionButton( theme: theme, label: 'Desconectar', enabled: ap.isConnected && !_connecting, primary: false, onPressed: _disconnect, ), ), ], ), const SizedBox(height: 16), // ── Refresh ports ───────────────────────────────────────────────── TextButton( onPressed: _loadPorts, child: Text( 'Actualizar lista de puertos', style: TextStyle(color: theme.textMuted, fontSize: 12), ), ), ], ), ), ); } } // ── Private widgets ─────────────────────────────────────────────────────────── class _StatusRow extends StatelessWidget { const _StatusRow({required this.theme, required this.connected}); final AutopilotTheme theme; final bool connected; @override Widget build(BuildContext context) { final color = connected ? theme.okColor : theme.textMuted; final label = connected ? 'CONECTADO AL CONCENTRADOR' : 'MODO DEMO (sin conexión)'; return Row( children: [ Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: color, boxShadow: connected ? [BoxShadow(color: color.withValues(alpha: 0.5), blurRadius: 8)] : null, ), ), const SizedBox(width: 10), Text( label, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.8, ), ), ], ); } } class _SectionLabel extends StatelessWidget { const _SectionLabel({required this.label, required this.theme}); final String label; final AutopilotTheme theme; @override Widget build(BuildContext context) => Text( label.toUpperCase(), style: TextStyle( color: theme.textMuted, fontSize: 10, letterSpacing: 1.2, fontWeight: FontWeight.w600, ), ); } class _PortDropdown extends StatelessWidget { const _PortDropdown({ required this.theme, required this.value, required this.ports, required this.onChanged, }); final AutopilotTheme theme; final String? value; final List ports; final ValueChanged onChanged; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( gradient: theme.panelBackground, borderRadius: BorderRadius.circular(6), border: Border.all(color: theme.panelBorder), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: ports.contains(value) ? value : null, hint: Text( ports.isEmpty ? 'Sin puertos disponibles' : 'Seleccionar puerto…', style: TextStyle(color: theme.textMuted, fontSize: 13), ), dropdownColor: theme.backgroundMid, style: TextStyle(color: theme.textMain, fontSize: 13), icon: Icon(Icons.expand_more, color: theme.textMuted), isExpanded: true, items: ports .map((p) => DropdownMenuItem(value: p, child: Text(p))) .toList(), onChanged: onChanged, ), ), ); } } class _ActionButton extends StatelessWidget { const _ActionButton({ required this.theme, required this.label, required this.enabled, required this.primary, required this.onPressed, }); final AutopilotTheme theme; final String label; final bool enabled; final bool primary; final VoidCallback onPressed; @override Widget build(BuildContext context) { final color = primary ? theme.okColor : theme.accentMid; return GestureDetector( onTap: enabled ? onPressed : null, child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(vertical: 14), alignment: Alignment.center, decoration: BoxDecoration( color: enabled ? color.withValues(alpha: 0.15) : theme.backgroundDeep, borderRadius: BorderRadius.circular(7), border: Border.all( color: enabled ? color : theme.panelBorder, width: 1.5, ), ), child: Text( label, style: TextStyle( color: enabled ? color : theme.textDisabled, fontSize: 13, fontWeight: FontWeight.w700, letterSpacing: 1, ), ), ), ); } }