Refactor hydraulic components: Simplify pump and tank logic, remove unused properties, and enhance flow calculations

- Updated osHydPump.cs to replace NPSH and cavitation calculations with a simpler flow check.
- Modified pump status display to indicate whether the pump is pumping or not.
- Streamlined fluid updates in osHydPump.cs to only occur when there is flow.
- Refactored osHydTank.cs to consolidate flow handling and remove unnecessary three-section logic.
- Adjusted UI elements in ucHydTank.xaml to reflect changes in flow representation and removed deprecated visual indicators.
- Enhanced DebugConsoleServer.cs to improve client connection handling with timeout and error management.
This commit is contained in:
Miguel 2025-09-07 17:06:15 +02:00
parent b39c58e6d6
commit 3c9c0e2479
8 changed files with 403 additions and 594 deletions

View File

@ -1,5 +1,7 @@
# CtrEditor MCP Server - LLM Guide
*MCP 2025-06-18 compliant | Compatible: Claude Desktop + Cursor*
## ⚡ Command Efficiency Tiers
### 🚀 **Ultra-Fast** (Use Liberally)
@ -117,6 +119,11 @@ High-value, low-token patterns:
2. Check CtrEditor is running with `get_ctreditor_status`
3. Verify MCP server is active on port 5006
### MCP Client Issues
- **Red LED in Cursor**: Proxy now handles all standard MCP methods correctly
- **Connection timeout**: CtrEditor must be running for tool calls to work
- **Build errors**: Use `build_project` to see only error output
## 💡 Best Practices
- Always use `get_simulation_status` before expensive operations
@ -124,3 +131,4 @@ High-value, low-token patterns:
- Call `list_objects` only when object data is actually needed
- Stop simulation before major structural changes
- Use appropriate units: meters, Pascal, m³/s
- Proxy works with both Claude Desktop and Cursor (MCP 2025-06-18)

View File

@ -577,3 +577,159 @@ using CtrEditor.HydraulicSimulator; // Para IHydraulicComponent
- ✅ **Código limpio** - Eliminadas duplicaciones y mantenida una sola fuente de verdad por funcionalidad
- ✅ **Compilación exitosa** - El proyecto compila completamente sin errores
## Corrección del Sistema de Debug Logging para .NET Core/.NET 8 (Enero 2025)
### Problema Identificado
El MCP proxy no estaba retornando los logs de la consola de debug de CtrEditor. El análisis reveló que el `DebugConsoleServer` solo escuchaba `Trace.WriteLine`, pero todo el código de CtrEditor usaba `Debug.WriteLine`. En .NET Core/.NET 8, `Debug.Listeners` no existe (solo disponible en .NET Framework), causando errores de compilación.
### Errores de Compilación
```
D:\Proyectos\VisualStudio\CtrEditor\Services\DebugConsoleServer.cs(57,23): error CS0117: 'Debug' no contiene una definición para 'Listeners'
D:\Proyectos\VisualStudio\CtrEditor\Services\DebugConsoleServer.cs(92,27): error CS0117: 'Debug' no contiene una definición para 'Listeners'
```
### Solución Implementada
**1. Corrección del DebugConsoleServer**
- **Antes**: Intentaba registrar `DebugTraceListener` en `Debug.Listeners` (no disponible en .NET Core)
- **Después**: Solo usa `Trace.Listeners` que sí está disponible en .NET Core/.NET 8
- **Comentarios actualizados**: Explicar que `Debug.WriteLine` no está disponible en .NET Core/.NET 8
**2. Migración de Logs Críticos a Trace.WriteLine**
Se cambiaron los logs más importantes del `HydraulicSimulationManager` de `Debug.WriteLine` a `Trace.WriteLine`:
```csharp
// Antes
Debug.WriteLine("HydraulicSimulationManager inicializado");
Debug.WriteLine($"❌ Simulación hidráulica no convergió: {LastSolutionResult.ErrorMessage}");
// Después
Trace.WriteLine("HydraulicSimulationManager inicializado");
Trace.WriteLine($"❌ Simulación hidráulica no convergió: {LastSolutionResult.ErrorMessage}");
```
### Logs Migrados a Trace.WriteLine
- Inicialización del HydraulicSimulationManager
- Errores de convergencia de simulación hidráulica
- Errores generales en HydraulicSimulationManager.Step
- Reinicio de simulación hidráulica
- Creación de bombas y tanques hidráulicos
### Validación del Sistema
**Test Completo Ejecutado:**
1. ✅ **Compilación exitosa**: Sin errores de `Debug.Listeners`
2. ✅ **CtrEditor iniciado**: Proceso PID 36724 funcionando
3. ✅ **Debug listener operativo**: Puerto 5007 conectado exitosamente
4. ✅ **Logs capturados**: Buffer conteniendo logs de `DebugConsoleServer`
**Resultado del Test:**
```
"matches_found": 3,
"matches": [
"[Debug Console Server] Conectado al servidor de debug de CtrEditor en puerto 5007",
"[Debug Console Server] Cliente debug conectado",
"[Debug Console Server] Esperando conexión de cliente..."
]
```
### Arquitectura Corregida
**Flujo de Logging .NET Core/.NET 8:**
1. `Trace.WriteLine()` en código de CtrEditor
2. → `DebugTraceListener` registrado en `Trace.Listeners`
3. → `DebugConsoleServer` TCP puerto 5007
4. → MCP Proxy debug listener
5. → Búsqueda y análisis de logs via MCP
### Beneficios de la Corrección
- **Compatibilidad .NET Core**: Sistema funciona en .NET 8 en lugar de solo .NET Framework
- **Logs visibles**: Los logs críticos del sistema hidráulico ahora son accesibles via MCP
- **Debugging mejorado**: Posibilidad de monitorear simulación hidráulica en tiempo real
- **Arquitectura limpia**: Solo usa APIs disponibles en la plataforma target
## Corrección Crítica del Freeze en I/O Completion Port
### Problema Identificado
La aplicación se congelaba en `System.Private.CoreLib.dll!System.Threading.PortableThreadPool.IOCompletionPoller.Poll()` debido a que `AcceptTcpClientAsync()` sin `CancellationToken` no puede ser cancelado, causando bloqueos indefinidos en el thread del I/O completion port cuando se intentaba detener el `DebugConsoleServer`.
**Stack trace del problema:**
```
System.Private.CoreLib.dll!System.Threading.PortableThreadPool.IOCompletionPoller.Poll()
Interop.Kernel32.GetQueuedCompletionStatusEx(..., Timeout.Infinite, ...)
```
### Causa Raíz
```csharp
// PROBLEMA: Sin cancelación en .NET Core/.NET 8
private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
{
while (_isRunning && !cancellationToken.IsCancellationRequested)
{
var tcpClient = await _tcpListener.AcceptTcpClientAsync(); // ⚠️ BLOQUEO INDEFINIDO
}
}
```
El método `AcceptTcpClientAsync()` sin parámetros no acepta `CancellationToken` en .NET Core, causando que el thread se quede esperando una conexión TCP indefinidamente.
### Solución Implementada
Implementación de timeout responsive con `Task.WhenAny()`:
```csharp
private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
{
try
{
while (_isRunning && !cancellationToken.IsCancellationRequested)
{
try
{
// Implementar timeout y cancelación manual para evitar freeze
var acceptTask = _tcpListener.AcceptTcpClientAsync();
var delayTask = Task.Delay(1000, cancellationToken); // Check every 1s
var completedTask = await Task.WhenAny(acceptTask, delayTask);
if (completedTask == acceptTask && !cancellationToken.IsCancellationRequested)
{
var tcpClient = await acceptTask;
_connectedClients.Add(tcpClient);
// Manejar cliente...
}
// Si delayTask completa, continúa el loop para verificar estado
}
catch (ObjectDisposedException) { break; } // TcpListener cerrado
catch (Exception ex)
{
if (!_isRunning) break;
await Task.Delay(1000, cancellationToken); // Esperar antes de reintentar
}
}
}
catch (OperationCanceledException) { /* Cancelación normal */ }
}
```
### Validación de la Corrección
**Test Completo Ejecutado:**
1. ✅ **Compilación exitosa**: Sin errores de compatibilidad
2. ✅ **CtrEditor iniciado**: Proceso PID 42876 funcionando correctamente
3. ✅ **Debug listener iniciado**: Puerto 5007 conectado sin problemas
4. ✅ **Sin freezes**: Aplicación responde a comandos inmediatamente
5. ✅ **Shutdown limpio**: Debug listener y CtrEditor se cierran correctamente
### Resultados
- ✅ **Eliminado freeze completo** - No más bloqueos en I/O completion port
- ✅ **Cancelación responsive** - Verificación de estado cada 1 segundo
- ✅ **Shutdown limpio** - Debug listener se detiene correctamente
- ✅ **Estabilidad mejorada** - Aplicación responde a comandos de stop inmediatamente
- ✅ **Manejo robusto de errores** - Captura `ObjectDisposedException` y `OperationCanceledException`
Esta corrección resuelve un problema crítico de estabilidad que impedía el uso seguro del sistema de debug logging.

View File

@ -55,7 +55,7 @@ namespace CtrEditor.HydraulicSimulator
public int MaxIterations { get; set; } = 300;
public double Tolerance { get; set; } = 1e-3; // Tolerancia más relajada para convergencia inicial
public double RelaxationFactor { get; set; } = 0.8;
public bool VerboseOutput { get; set; } = false; // Activado para debugging
public bool VerboseOutput { get; set; } = true; // Activado para debugging
/// <summary>
/// Contador de pasos de simulación para optimizaciones
@ -101,7 +101,7 @@ namespace CtrEditor.HydraulicSimulator
GlobalTime = 0.0f;
Debug.WriteLine("HydraulicSimulationManager inicializado");
Trace.WriteLine("HydraulicSimulationManager inicializado");
}
#endregion
@ -418,9 +418,9 @@ namespace CtrEditor.HydraulicSimulator
}
else
{
Debug.WriteLine($"❌ Simulación hidráulica no convergió: {LastSolutionResult.ErrorMessage}");
Debug.WriteLine($" Iteraciones: {LastSolutionResult.Iterations}, Residual: {LastSolutionResult.Residual:E6}");
Debug.WriteLine($" Tolerancia requerida: {Tolerance:E6}");
Trace.WriteLine($"❌ Simulación hidráulica no convergió: {LastSolutionResult.ErrorMessage}");
Trace.WriteLine($" Iteraciones: {LastSolutionResult.Iterations}, Residual: {LastSolutionResult.Residual:E6}");
Trace.WriteLine($" Tolerancia requerida: {Tolerance:E6}");
// Diagnóstico detallado deshabilitado para mejorar rendimiento
/*
@ -452,7 +452,7 @@ namespace CtrEditor.HydraulicSimulator
ErrorMessage = $"Error en simulación hidráulica: {ex.Message}"
};
Debug.WriteLine($"Error en HydraulicSimulationManager.Step: {ex}");
Trace.WriteLine($"Error en HydraulicSimulationManager.Step: {ex}");
}
finally
{
@ -660,9 +660,12 @@ namespace CtrEditor.HydraulicSimulator
/// </summary>
private void ApplyResultsToObject(osBase obj)
{
Debug.WriteLine($"[HYDRAULIC] ApplyResultsToObject: {obj.Nombre} ({obj.GetType().Name})");
// Aplicar resultados usando las interfaces hidráulicas
if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents)
{
Debug.WriteLine($"[HYDRAULIC] {obj.Nombre} is IHydraulicComponent with HasHydraulicComponents=true");
// Llamar al método de aplicación de resultados del objeto
hydraulicComponent.ApplyHydraulicResults(LastSolutionResult.Flows, LastSolutionResult.Pressures);
}
@ -670,13 +673,21 @@ namespace CtrEditor.HydraulicSimulator
// Aplicación específica por tipo de interfaz
if (obj is IHydraulicFlowReceiver flowReceiver)
{
Debug.WriteLine($"[HYDRAULIC] {obj.Nombre} is IHydraulicFlowReceiver");
// Buscar caudal asociado al objeto
string branchName = FindBranchNameForObject(obj);
Debug.WriteLine($"[HYDRAULIC] Branch name for {obj.Nombre}: '{branchName}'");
if (!string.IsNullOrEmpty(branchName) && LastSolutionResult.Flows.ContainsKey(branchName))
{
double flow = LastSolutionResult.Flows[branchName];
Debug.WriteLine($"[HYDRAULIC] Setting flow {flow:F6} m³/s to {obj.Nombre}");
flowReceiver.SetFlow(flow);
}
else
{
Debug.WriteLine($"[HYDRAULIC] No flow found for {obj.Nombre} with branch '{branchName}'");
Debug.WriteLine($"[HYDRAULIC] Available flows: {string.Join(", ", LastSolutionResult.Flows.Keys)}");
}
}
if (obj is IHydraulicPressureReceiver pressureReceiver)
@ -791,7 +802,7 @@ namespace CtrEditor.HydraulicSimulator
ErrorMessage = "Simulación reiniciada"
};
Debug.WriteLine("Simulación hidráulica reiniciada");
Trace.WriteLine("Simulación hidráulica reiniciada");
}
/// <summary>
@ -874,7 +885,7 @@ namespace CtrEditor.HydraulicSimulator
HydraulicSimObjects.Add(pump);
_networkNeedsRebuild = true;
Debug.WriteLine($"Bomba hidráulica creada: H={pumpHead}m, Q={maxFlow}m³/s");
Trace.WriteLine($"Bomba hidráulica creada: H={pumpHead}m, Q={maxFlow}m³/s");
return pump;
}
@ -896,7 +907,7 @@ namespace CtrEditor.HydraulicSimulator
HydraulicSimObjects.Add(tank);
_networkNeedsRebuild = true;
Debug.WriteLine($"Tanque hidráulico creado: P={pressure}Pa, Nivel={currentLevel}m");
Trace.WriteLine($"Tanque hidráulico creado: P={pressure}Pa, Nivel={currentLevel}m");
return tank;
}

View File

@ -565,22 +565,38 @@ namespace CtrEditor.ObjetosSim
public override void UpdateGeometryStart()
{
// Se llama cuando inicia la simulación - crear objeto hidráulico si no existe
Debug.WriteLine($"[DEBUG] {Nombre}: UpdateGeometryStart() - ComponenteA: '{Id_ComponenteA}', ComponenteB: '{Id_ComponenteB}'");
if (SimHydraulicPipe == null && !string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
Debug.WriteLine($"[DEBUG] {Nombre}: Creating SimHydraulicPipe...");
SimHydraulicPipe = hydraulicSimulationManager.AddPipe(Length, Diameter, Roughness, Id_ComponenteA, Id_ComponenteB);
if (SimHydraulicPipe != null)
{
SimHydraulicPipe.SimObjectType = "HydraulicPipe";
SimHydraulicPipe.WpfObject = this;
SimHydraulicPipe.Nombre = Nombre;
Debug.WriteLine($"[DEBUG] {Nombre}: SimHydraulicPipe created successfully");
}
else
{
Debug.WriteLine($"[DEBUG] {Nombre}: FAILED to create SimHydraulicPipe!");
}
}
else if (SimHydraulicPipe != null)
{
// Actualizar propiedades si el objeto ya existe
Debug.WriteLine($"[DEBUG] {Nombre}: Updating existing SimHydraulicPipe");
SimHydraulicPipe.Length = Length;
SimHydraulicPipe.Diameter = Diameter;
SimHydraulicPipe.Roughness = Roughness;
SimHydraulicPipe.ComponenteAId = Id_ComponenteA;
SimHydraulicPipe.ComponenteBId = Id_ComponenteB;
}
else
{
Debug.WriteLine($"[DEBUG] {Nombre}: Cannot create SimHydraulicPipe - missing connections or already exists");
}
ActualizarGeometrias();
}
@ -631,18 +647,19 @@ namespace CtrEditor.ObjetosSim
// Actualizar propiedades desde la simulación hidráulica
if (SimHydraulicPipe != null)
{
double previousFlow = CurrentFlow;
CurrentFlow = SimHydraulicPipe.CurrentFlow;
PressureDrop = SimHydraulicPipe.PressureDrop;
// DEBUG: Log flujo para diagnosis
if (CurrentFlow != 0.0)
if (CurrentFlow != 0.0 || CurrentFlow != previousFlow)
{
Debug.WriteLine($"[DEBUG] {Nombre}: Flow={CurrentFlow:F6} m³/s ({CurrentFlowLMin:F2} L/min)");
Debug.WriteLine($"[DEBUG] {Nombre}: Flow={CurrentFlow:F6} m³/s ({CurrentFlowLMin:F2} L/min) - Changed from {previousFlow:F6}");
}
}
else
{
Debug.WriteLine($"[DEBUG] {Nombre}: SimHydraulicPipe is NULL!");
Debug.WriteLine($"[DEBUG] {Nombre}: SimHydraulicPipe is NULL! ComponenteA='{Id_ComponenteA}', ComponenteB='{Id_ComponenteB}'");
}
// Actualizar propiedades del fluido cada ciclo

View File

@ -180,85 +180,22 @@ namespace CtrEditor.ObjetosSim
public double CurrentFlowLMin => CurrentFlow * 60000.0; // m³/s a L/min
[Category("📊 Estado Actual")]
[DisplayName("NPSH Disponible")]
[Description("NPSH disponible calculado (m)")]
[DisplayName("Tiene Flujo")]
[Description("Indica si hay flujo en la bomba")]
[JsonIgnore]
public double NPSHAvailable
{
get
{
var (inletNodeName, _) = GetConnectedNodeNames();
if (!string.IsNullOrEmpty(inletNodeName) &&
hydraulicSimulationManager?.LastSolutionResult?.Pressures?.ContainsKey(inletNodeName) == true)
{
var suctionPressure = hydraulicSimulationManager.LastSolutionResult.Pressures[inletNodeName];
var pump = new PumpHQ(0, 0); // Solo para usar el método de cálculo
return pump.CalculateNPSHAvailable(suctionPressure, hydraulicSimulationManager.SimulationFluid);
}
return 0.0;
}
}
public bool HasFlow => Math.Abs(CurrentFlow) > 1e-6;
[Category("📊 Estado Actual")]
[DisplayName("Factor Cavitación")]
[Description("Factor de cavitación (1=sin cavitación, 0=cavitación total)")]
[DisplayName("Estado")]
[Description("Estado actual de la bomba")]
[JsonIgnore]
public double CavitationFactor
{
get
{
var (inletNodeName, _) = GetConnectedNodeNames();
if (!string.IsNullOrEmpty(inletNodeName) &&
hydraulicSimulationManager?.LastSolutionResult?.Pressures?.ContainsKey(inletNodeName) == true)
{
var suctionPressure = hydraulicSimulationManager.LastSolutionResult.Pressures[inletNodeName];
var pump = new PumpHQ(0, 0); // Solo para usar el método de cálculo
return pump.GetCavitationFactor(suctionPressure, hydraulicSimulationManager.SimulationFluid);
}
return 1.0;
}
}
[Category("📊 Estado Actual")]
[DisplayName("Puede Operar")]
[Description("Indica si la bomba puede operar sin cavitación")]
[JsonIgnore]
public bool CanOperateWithoutCavitation
{
get
{
var (inletNodeName, _) = GetConnectedNodeNames();
if (!string.IsNullOrEmpty(inletNodeName) &&
hydraulicSimulationManager?.LastSolutionResult?.Pressures?.ContainsKey(inletNodeName) == true)
{
var suctionPressure = hydraulicSimulationManager.LastSolutionResult.Pressures[inletNodeName];
var pump = new PumpHQ(0, 0); // Solo para usar el método de cálculo
return pump.CanOperateWithoutCavitation(suctionPressure, hydraulicSimulationManager.SimulationFluid);
}
return false;
}
}
[Category("📊 Estado Actual")]
[DisplayName("Estado Cavitación")]
[Description("Estado actual de cavitación de la bomba")]
[JsonIgnore]
public string CavitationStatus
public string PumpStatus
{
get
{
if (!IsRunning) return "Bomba detenida";
var factor = CavitationFactor;
var canOperate = CanOperateWithoutCavitation;
var npshAvailable = NPSHAvailable;
if (canOperate && factor >= 0.9)
return "✅ Operación normal";
else if (factor >= 0.5)
return $"⚠️ Riesgo cavitación (Factor: {factor:F2})";
else
return $"❌ NPSH insuficiente ({npshAvailable:F2}m)";
if (HasFlow) return "✅ Bombeando";
return "⚠️ Sin flujo";
}
}
@ -270,13 +207,9 @@ namespace CtrEditor.ObjetosSim
{
get
{
if (!IsRunning) return "Bomba detenida";
var factor = CavitationFactor;
var npshAvailable = NPSHAvailable;
var flow = CurrentFlowLMin;
return $"NPSH: {npshAvailable:F2}m | Factor: {factor:F2} | Flujo: {flow:F1} L/min";
var pressure = CurrentPressureBar;
return $"Flujo: {flow:F1} L/min | Presión: {pressure:F1} bar | {PumpStatus}";
}
}
@ -414,7 +347,10 @@ namespace CtrEditor.ObjetosSim
try
{
// Buscar el componente conectado en la succión (entrada de la bomba)
// Solo actualizar fluido si hay flujo
if (HasFlow)
{
// Buscar el componente conectado en la succión
var suctionComponent = FindSuctionComponent();
if (suctionComponent is osHydTank tank)
@ -435,6 +371,18 @@ namespace CtrEditor.ObjetosSim
}
}
}
else
{
// Si no hay flujo, mantener aire como fluido
if (_currentFluid.Type != FluidType.Air)
{
_currentFluid = new FluidProperties(FluidType.Air);
OnPropertyChanged(nameof(CurrentFluidType));
OnPropertyChanged(nameof(CurrentFluidDescription));
UpdatePumpColorFromFluid();
}
}
}
catch (Exception ex)
{
// Mantener fluido por defecto en caso de error
@ -450,27 +398,10 @@ namespace CtrEditor.ObjetosSim
return;
}
// Si no hay flujo, verificar cavitación primero
if (Math.Abs(CurrentFlow) < 1e-6)
// Color basado solo en si hay flujo o no
if (HasFlow)
{
if (hydraulicSimulationManager?.EnableNPSHVerification == true)
{
var cavitationFactor = CavitationFactor;
if (cavitationFactor < 0.1)
ColorButton_oculto = Brushes.Red; // Cavitación severa
else if (cavitationFactor < 0.5)
ColorButton_oculto = Brushes.Orange; // Riesgo de cavitación
else
ColorButton_oculto = Brushes.Yellow; // Standby
}
else
{
ColorButton_oculto = Brushes.Yellow; // Standby
}
return;
}
// Colorear según tipo de fluido bombeado
// Si hay flujo, usar color del fluido o verde por defecto
try
{
var fluidColorHex = _currentFluid.Color;
@ -482,6 +413,11 @@ namespace CtrEditor.ObjetosSim
ColorButton_oculto = Brushes.Green; // Color por defecto si hay flujo
}
}
else
{
ColorButton_oculto = Brushes.Yellow; // Sin flujo (standby o sin succión)
}
}
private IHydraulicComponent FindSuctionComponent()
{
@ -527,14 +463,17 @@ namespace CtrEditor.ObjetosSim
CurrentPressure = SimHydraulicPump.CurrentPressure;
}
// Actualizar propiedades del fluido cada ciclo
// Actualizar propiedades del fluido basado en si hay flujo
UpdateFluidFromSuction();
// Actualizar propiedades de UI
OnPropertyChanged(nameof(CurrentFlowLMin));
OnPropertyChanged(nameof(EffectiveFlowLMin));
OnPropertyChanged(nameof(HasFlow));
OnPropertyChanged(nameof(PumpStatus));
OnPropertyChanged(nameof(DetailedStatus));
// Actualizar el color según el estado (ya se hace en UpdatePumpColorFromFluid)
// Actualizar el color según el flujo
if (!IsRunning)
{
ColorButton_oculto = Brushes.Gray; // Bomba apagada
@ -611,36 +550,17 @@ namespace CtrEditor.ObjetosSim
return elements;
}
// Crear bomba con parámetros actuales
Element pump;
if (hydraulicSimulationManager.EnableNPSHVerification)
{
// Usar bomba con verificación de NPSH
pump = new PumpHQWithSuctionCheck(
h0: PumpHead,
q0: MaxFlow,
speedRel: effectiveSpeed,
direction: PumpDirection,
enableNpshCheck: true
);
}
else
{
// Usar bomba estándar
pump = new PumpHQ(
// Crear bomba estándar - el solver manejará las limitaciones de flujo
var pump = new PumpHQ(
h0: PumpHead,
q0: MaxFlow,
speedRel: effectiveSpeed,
direction: PumpDirection
);
}
// Asignar nombres de nodos para verificación de NPSH
if (pump is PumpHQ pumpHQ)
{
pumpHQ.InletNodeName = inletNode;
pumpHQ.OutletNodeName = outletNode;
}
// Asignar nombres de nodos
pump.InletNodeName = inletNode;
pump.OutletNodeName = outletNode;
var pumpElement = new HydraulicElementDefinition(
$"{Nombre}_Pump",
@ -661,7 +581,7 @@ namespace CtrEditor.ObjetosSim
public void UpdateHydraulicProperties()
{
// Aquí se pueden hacer validaciones o ajustes antes de la simulación
// Validaciones simples antes de la simulación
if (!IsRunning)
{
SpeedRatio = 0.0;
@ -670,10 +590,7 @@ namespace CtrEditor.ObjetosSim
if (SpeedRatio < 0.0) SpeedRatio = 0.0;
if (SpeedRatio > 1.0) SpeedRatio = 1.0;
// Las verificaciones de NPSH ahora se muestran a través de propiedades
// Se eliminaron los Debug.WriteLine para evitar saturación del sistema
//Debug.WriteLine($"Bomba {Nombre}: Velocidad={SpeedRatio:F2}, Funcionando={IsRunning}");
// El solver se encarga del resto - NPSH, limitaciones de flujo, etc.
}
public void ApplyHydraulicResults(Dictionary<string, double> flows, Dictionary<string, double> pressures)

View File

@ -30,16 +30,12 @@ namespace CtrEditor.ObjetosSim
private double _tankPressure = 1.013; // bar (1 atm por defecto)
private double _currentLevelM = 1.0; // m
private double _maxLevelM = 2.0; // m
private double _minLevelM = 0.1; // m
private double _crossSectionalArea = 1.0; // m²
private double _currentVolumeL = 1000.0; // L
private double _maxVolumeL = 2000.0; // L
private double _inletFlow = 0.0; // L/min
private double _outletFlow = 0.0; // L/min
private double _netFlow = 0.0; // L/min (positivo = entrada, negativo = salida)
private double _currentPressure = 1.013; // bar
private bool _isFixedPressure = true;
private HydraulicTankType _tankType = HydraulicTankType.Intermediate;
private double _lastUpdateTime = 0.0;
// Propiedades de fluido
@ -51,22 +47,15 @@ namespace CtrEditor.ObjetosSim
private MixingState _mixingState = MixingState.PrimaryOnly;
private double _currentMixRatio = 0.0; // 0 = solo primario, 1 = solo secundario
// Tank configuration fields
private double _maxLevelM = 2.0; // m - máximo nivel del tanque
private double _minLevelM = 0.0; // m - mínimo nivel del tanque
private string _tankType = "Standard"; // Tipo de tanque
[JsonIgnore]
private object _levelLock = new object();
// Enums
public enum HydraulicTankType
{
[Description("Tanque de succión (inicio del sistema)")]
Suction,
[Description("Tanque intermedio (buffer)")]
Intermediate,
[Description("Tanque de almacenamiento")]
Storage,
[Description("Tanque de proceso")]
Process
}
// Properties
@ -84,42 +73,8 @@ namespace CtrEditor.ObjetosSim
[property: Name("Color")]
private Brush colorButton_oculto = Brushes.LightBlue;
// Three-section tank properties
[ObservableProperty]
[property: Category("🏭 Configuración Tanque")]
[property: Description("Habilitar sistema de tres secciones")]
[property: Name("Tres Secciones")]
private bool enableThreeSection = false;
[ObservableProperty]
[property: Category("🏭 Configuración Tanque")]
[property: Description("Altura de la sección de mezcla (m)")]
[property: Name("Altura Mezcla")]
private double mixingSectionHeight = 0.20;
[ObservableProperty]
[property: Category("📊 Niveles")]
[property: Description("Nivel secundario en metros")]
[property: Name("Nivel Secundario")]
private double secondaryLevelM = 0.0;
[ObservableProperty]
[property: Category("📊 Niveles")]
[property: Description("Nivel primario en metros")]
[property: Name("Nivel Primario")]
private double primaryLevelM = 0.0;
[ObservableProperty]
[property: Category("📊 Porcentajes")]
[property: Description("Porcentaje de llenado sección secundaria")]
[property: Name("% Secundario")]
private double secondaryPercentage = 0.0;
[ObservableProperty]
[property: Category("📊 Porcentajes")]
[property: Description("Porcentaje de llenado sección primaria")]
[property: Name("% Primario")]
private double primaryPercentage = 0.0;
@ -151,11 +106,6 @@ namespace CtrEditor.ObjetosSim
// Inicializar estado de mezcla
UpdateMixingState();
// Inicializar sistema de tres secciones
if (EnableThreeSection)
{
ConfigureThreeSectionLevels();
}
// Debug: Confirmar que el constructor se ejecuta
Debug.WriteLine($"osHydTank Constructor: Nombre='{Nombre}', Tamaño={Tamano}, ZIndex={zIndex_fromFrames}, IsVisFilter={IsVisFilter}, Lock_movement={Lock_movement}");
@ -201,7 +151,9 @@ namespace CtrEditor.ObjetosSim
{
// Convertir bar a Pa para el sistema hidráulico interno
var pressurePa = TankPressure * 100000.0;
SimHydraulicTank = hydraulicSimulationManager.AddTank(pressurePa, CurrentLevelM, MaxLevelM, MinLevelM, CrossSectionalArea, IsFixedPressure);
// Usar nivel basado solo en volumen primario para cálculos hidráulicos
var effectiveLevel = PrimaryVolumeL / (CrossSectionalArea * 1000.0);
SimHydraulicTank = hydraulicSimulationManager.AddTank(pressurePa, effectiveLevel, MaxLevelM, MinLevelM, CrossSectionalArea, IsFixedPressure);
SimHydraulicTank.SimObjectType = "HydraulicTank";
SimHydraulicTank.WpfObject = this;
SimHydraulicTank.Nombre = Nombre;
@ -210,7 +162,9 @@ namespace CtrEditor.ObjetosSim
{
// Actualizar propiedades si el objeto ya existe (convertir bar a Pa)
SimHydraulicTank.TankPressure = TankPressure * 100000.0;
SimHydraulicTank.CurrentLevel = CurrentLevelM;
// Usar nivel basado solo en volumen primario para cálculos hidráulicos
var effectiveLevel = PrimaryVolumeL / (CrossSectionalArea * 1000.0);
SimHydraulicTank.CurrentLevel = effectiveLevel;
SimHydraulicTank.MaxLevel = MaxLevelM;
SimHydraulicTank.MinLevel = MinLevelM;
SimHydraulicTank.CrossSectionalArea = CrossSectionalArea;
@ -224,8 +178,7 @@ namespace CtrEditor.ObjetosSim
if (SimHydraulicTank != null)
{
// Convertir de m³/s a L/min
InletFlow = SimHydraulicTank.InletFlow * 60000.0;
OutletFlow = SimHydraulicTank.OutletFlow * 60000.0;
_netFlow = (SimHydraulicTank.InletFlow - SimHydraulicTank.OutletFlow) * 60000.0;
// Convertir de Pa a bar
CurrentPressure = SimHydraulicTank.CurrentPressure / 100000.0;
@ -246,21 +199,6 @@ namespace CtrEditor.ObjetosSim
// Properties - Configuration
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Tipo de tanque")]
[Description("Función del tanque en el sistema hidráulico")]
public HydraulicTankType TankType
{
get => _tankType;
set
{
if (SetProperty(ref _tankType, value))
{
UpdateTankConfiguration();
InvalidateHydraulicNetwork();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Área sección transversal")]
@ -303,11 +241,14 @@ namespace CtrEditor.ObjetosSim
get => _maxLevelM;
set
{
if (SetProperty(ref _maxLevelM, Math.Max(0.2, value)))
if (SetProperty(ref _maxLevelM, Math.Max(0.1, value)))
{
if (_currentLevelM > _maxLevelM)
CurrentLevelM = _maxLevelM;
// Asegurar que el nivel mínimo no sea mayor que el máximo
if (_minLevelM >= _maxLevelM)
MinLevelM = _maxLevelM * 0.1; // 10% del máximo como mínimo
SafeUpdateVolumeCalculations();
InvalidateHydraulicNetwork();
}
}
}
@ -320,15 +261,29 @@ namespace CtrEditor.ObjetosSim
get => _minLevelM;
set
{
if (SetProperty(ref _minLevelM, Math.Max(0.0, Math.Min(value, _maxLevelM - 0.1))))
if (SetProperty(ref _minLevelM, Math.Max(0.0, value)))
{
if (_currentLevelM < _minLevelM)
CurrentLevelM = _minLevelM;
// Asegurar que el nivel mínimo no sea mayor que el máximo
if (_minLevelM >= _maxLevelM)
MaxLevelM = _minLevelM + 0.1; // Al menos 10cm más que el mínimo
SafeUpdateVolumeCalculations();
InvalidateHydraulicNetwork();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Tipo de tanque")]
[Description("Tipo de tanque (Standard, Storage, Process, etc.)")]
public string TankType
{
get => _tankType;
set => SetProperty(ref _tankType, value ?? "Standard");
}
// Properties - Pressure
@ -380,7 +335,7 @@ namespace CtrEditor.ObjetosSim
EnsureLockInitialized();
lock (_levelLock)
{
var clampedLevel = Math.Max(_minLevelM, Math.Min(_maxLevelM, value));
var clampedLevel = Math.Max(0.0, value);
if (SetProperty(ref _currentLevelM, clampedLevel))
{
SafeUpdateVolumeCalculations();
@ -406,8 +361,8 @@ namespace CtrEditor.ObjetosSim
if (SetProperty(ref _currentVolumeL, clampedVolume))
{
// Actualizar nivel basado en volumen
_currentLevelM = _minLevelM + (clampedVolume / 1000.0) / _crossSectionalArea;
_currentLevelM = Math.Max(_minLevelM, Math.Min(_maxLevelM, _currentLevelM));
_currentLevelM = (clampedVolume / 1000.0) / _crossSectionalArea;
_currentLevelM = Math.Max(0.0, _currentLevelM);
OnPropertyChanged(nameof(CurrentLevelM));
OnPropertyChanged(nameof(FillPercentage));
UpdateMixingState();
@ -420,33 +375,23 @@ namespace CtrEditor.ObjetosSim
[DisplayName("Porcentaje llenado")]
[Description("Porcentaje de llenado del tanque")]
[JsonIgnore]
public double FillPercentage => (_currentLevelM - _minLevelM) / (_maxLevelM - _minLevelM) * 100.0;
public double FillPercentage => MaxLevelM > 0 ? (_currentLevelM / MaxLevelM) * 100.0 : 0.0;
[Category("📊 Estado Actual")]
[DisplayName("Flujo entrada")]
[Description("Flujo de entrada actual (L/min)")]
[DisplayName("Flujo neto")]
[Description("Flujo neto del tanque (L/min) - positivo=entrada, negativo=salida")]
[JsonIgnore]
public double InletFlow
public double NetFlow
{
get => _inletFlow;
private set => SetProperty(ref _inletFlow, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Flujo salida")]
[Description("Flujo de salida actual (L/min)")]
[JsonIgnore]
public double OutletFlow
{
get => _outletFlow;
private set => SetProperty(ref _outletFlow, value);
get => _netFlow;
private set => SetProperty(ref _netFlow, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Balance flujo")]
[Description("Balance de flujo (entrada - salida) (L/min)")]
[Description("Balance de flujo neto (L/min)")]
[JsonIgnore]
public double FlowBalance => InletFlow - OutletFlow;
public double FlowBalance => NetFlow;
[Category("📊 Estado Actual")]
[DisplayName("Presión actual")]
@ -712,7 +657,7 @@ namespace CtrEditor.ObjetosSim
if (VerboseLogging())
{
Trace.WriteLine($"Tanque {Nombre}: Nivel={CurrentLevelM:F2}m ({FillPercentage:F1}%), " +
$"Flujo_entrada={InletFlow:F2}L/min, Flujo_salida={OutletFlow:F2}L/min, " +
$"Flujo_neto={NetFlow:F2}L/min, " +
$"Balance={FlowBalance:F2}L/min, Presión={CurrentPressure:F3}bar ({CurrentPressure * 100000.0:F0}Pa), Fluido={CurrentFluidDescription}");
}
}
@ -801,24 +746,6 @@ namespace CtrEditor.ObjetosSim
}
}
/// <summary>
/// Indicador del tipo de tanque
/// </summary>
[JsonIgnore]
public string TankTypeIndicator
{
get
{
return TankType switch
{
HydraulicTankType.Suction => "S",
HydraulicTankType.Intermediate => "I",
HydraulicTankType.Storage => "A",
HydraulicTankType.Process => "P",
_ => "T"
};
}
}
/// <summary>
/// Color del balance de flujo
@ -834,32 +761,7 @@ namespace CtrEditor.ObjetosSim
}
}
/// <summary>
/// Posición Y de la línea de nivel medio (50%)
/// </summary>
[JsonIgnore]
public double MidLevelY
{
get
{
var tankHeight = Tamano * 100; // Convertir a píxeles aproximados
return tankHeight * 0.5; // 50% de altura
}
}
/// <summary>
/// Posición Y de la línea de nivel mínimo
/// </summary>
[JsonIgnore]
public double MinLevelY
{
get
{
var tankHeight = Tamano * 100; // Convertir a píxeles aproximados
var minPercentage = (MinLevelM - MinLevelM) / (MaxLevelM - MinLevelM) * 100;
return tankHeight - (tankHeight * (minPercentage + 10) / 100); // 10% del nivel mínimo
}
}
/// <summary>
/// Indica si el tanque está vacío (contiene aire)
@ -985,6 +887,16 @@ namespace CtrEditor.ObjetosSim
if (Alto <= 0)
Alto = 0.40f;
// Inicializar configuraciones del tanque
if (_maxLevelM <= 0)
_maxLevelM = 2.0;
if (_minLevelM < 0 || _minLevelM >= _maxLevelM)
_minLevelM = 0.0;
if (string.IsNullOrEmpty(_tankType))
_tankType = "Standard";
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
@ -1001,8 +913,8 @@ namespace CtrEditor.ObjetosSim
try
{
// Calcular volumen total actual basado en nivel
_currentVolumeL = (_currentLevelM - _minLevelM) * _crossSectionalArea * 1000.0; // convertir m³ a L
_maxVolumeL = (_maxLevelM - _minLevelM) * _crossSectionalArea * 1000.0;
_currentVolumeL = _currentLevelM * _crossSectionalArea * 1000.0; // convertir m³ a L
_maxVolumeL = 2.0 * _crossSectionalArea * 1000.0; // Assuming 2m as max height
// Asegurar que los volúmenes de fluidos no excedan el volumen total
var totalFluidVolume = _primaryVolumeL + _secondaryVolumeL;
@ -1030,6 +942,13 @@ namespace CtrEditor.ObjetosSim
{
CurrentPressure = TankPressure;
}
else
{
// Calcular presión basada solo en volumen primario (para cálculos hidráulicos)
var primaryLevel = PrimaryVolumeL / (CrossSectionalArea * 1000.0); // Nivel equivalente del volumen primario
var hydrostaticPressure = primaryLevel * 9.81 * CurrentFluidDensity / 100000.0; // Convertir a bar
CurrentPressure = 1.013 + hydrostaticPressure; // Presión atmosférica + hidráulica
}
}
catch (Exception ex)
{
@ -1039,25 +958,6 @@ namespace CtrEditor.ObjetosSim
}
}
private void UpdateTankConfiguration()
{
// Configuración automática según el tipo de tanque
switch (TankType)
{
case HydraulicTankType.Suction:
// Tanques de succión típicamente a presión atmosférica
if (TankPressure < 0.5) // Si no se ha configurado manualmente
TankPressure = 1.013; // 1 atm
IsFixedPressure = true;
break;
case HydraulicTankType.Intermediate:
case HydraulicTankType.Storage:
case HydraulicTankType.Process:
// Estos pueden tener presión variable o fija según el proceso
break;
}
}
private void UpdateMixingState()
{
@ -1140,11 +1040,6 @@ namespace CtrEditor.ObjetosSim
// Actualizar volúmenes de fluidos según el estado de mezcla
UpdateFluidVolumesFromFlow(volumeChangeL);
// Actualizar sistema de tres secciones si está habilitado
if (EnableThreeSection)
{
UpdateThreeSectionLevels();
}
// Debug para verificar cambios
if (VerboseLogging() && Math.Abs(volumeChangeL) > 0.1)
@ -1200,8 +1095,7 @@ namespace CtrEditor.ObjetosSim
private void UpdateFlowsFromConnectedPipes(Dictionary<string, double> flows)
{
InletFlow = 0.0;
OutletFlow = 0.0;
NetFlow = 0.0;
// Buscar flujos en las ramas que involucren este tanque
foreach (var flow in flows)
@ -1210,65 +1104,22 @@ namespace CtrEditor.ObjetosSim
var flowValueM3s = flow.Value; // El flujo viene en m³/s
var flowValueLmin = flowValueM3s * 60000.0; // Convertir a L/min
// Buscar si esta rama conecta este tanque
if (branchName.Contains("Pump") && HasPumpConnection(branchName, flowValueLmin))
{
continue; // Ya manejado en HasPumpConnection
}
// Buscar otras conexiones directas al tanque por nombre
// Buscar conexiones que involucren este tanque
if (branchName.Contains(Nombre))
{
if (flowValueLmin > 0)
{
// Flujo positivo hacia o desde el tanque
// Determinar dirección del flujo basado en el nombre de la rama
if (IsFlowTowardsTank(branchName))
{
InletFlow += flowValueLmin;
NetFlow += flowValueLmin; // Flujo hacia el tanque (positivo)
}
else
{
OutletFlow += flowValueLmin;
}
NetFlow -= flowValueLmin; // Flujo desde el tanque (negativo)
}
}
}
}
private bool HasPumpConnection(string branchName, double flowValueLmin)
{
if (_mainViewModel == null) return false;
// Buscar si hay una bomba conectada que esté generando este flujo
var connectedPipes = _mainViewModel.ObjetosSimulables
.OfType<osHydPipe>()
.Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre)
.ToList();
foreach (var pipe in connectedPipes)
{
if (pipe.Id_ComponenteA == Nombre)
{
// Esta conexión sale del tanque
if (branchName.Contains(pipe.Id_ComponenteB) && branchName.Contains("Pump"))
{
OutletFlow += Math.Abs(flowValueLmin);
return true;
}
}
else if (pipe.Id_ComponenteB == Nombre)
{
// Esta conexión llega al tanque
if (branchName.Contains(pipe.Id_ComponenteA) && branchName.Contains("Pump"))
{
InletFlow += Math.Abs(flowValueLmin);
return true;
}
}
}
return false;
}
private double GetDeltaTime()
{
@ -1292,14 +1143,7 @@ namespace CtrEditor.ObjetosSim
private string GetTankDescription()
{
return TankType switch
{
HydraulicTankType.Suction => "Tanque de succión",
HydraulicTankType.Intermediate => "Tanque intermedio",
HydraulicTankType.Storage => "Tanque de almacenamiento",
HydraulicTankType.Process => "Tanque de proceso",
_ => "Tanque hidráulico"
};
return "Tanque hidráulico";
}
private bool IsFlowTowardsTank(string branchName)
@ -1377,124 +1221,6 @@ namespace CtrEditor.ObjetosSim
// Three-Section Tank Methods
/// <summary>
/// Obtiene la altura total del tanque
/// </summary>
private double GetHeightFromVolume()
{
return Tamano * 100; // Convertir tamaño a píxeles/altura
}
/// <summary>
/// Configura los niveles para el sistema de tres secciones
/// </summary>
public void ConfigureThreeSectionLevels()
{
if (!EnableThreeSection) return;
try
{
lock (_levelLock)
{
var totalHeight = GetHeightFromVolume();
var mixingHeight = MixingSectionHeight;
// Validar que la altura de mezcla no sea mayor que la altura total
if (mixingHeight > totalHeight)
{
mixingHeight = totalHeight * 0.3; // Máximo 30% de la altura total
MixingSectionHeight = mixingHeight;
}
// Calcular niveles basados en la altura actual del líquido
var currentLevel = CurrentLevelM;
if (currentLevel <= mixingHeight)
{
// Solo sección de mezcla
SecondaryLevelM = 0.0;
PrimaryLevelM = currentLevel;
}
else
{
// Mezcla completa + nivel adicional
SecondaryLevelM = currentLevel - mixingHeight;
PrimaryLevelM = mixingHeight;
}
UpdateThreeSectionPercentages();
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error configurando niveles de tres secciones: {ex.Message}");
}
}
/// <summary>
/// Actualiza los porcentajes de llenado para cada sección
/// </summary>
public void UpdateThreeSectionPercentages()
{
if (!EnableThreeSection) return;
try
{
lock (_levelLock)
{
var totalHeight = GetHeightFromVolume();
var mixingHeight = MixingSectionHeight;
var secondaryHeight = totalHeight - mixingHeight;
// Calcular porcentajes
if (mixingHeight > 0)
{
PrimaryPercentage = (PrimaryLevelM / mixingHeight) * 100.0;
PrimaryPercentage = Math.Min(100.0, Math.Max(0.0, PrimaryPercentage));
}
else
{
PrimaryPercentage = 0.0;
}
if (secondaryHeight > 0)
{
SecondaryPercentage = (SecondaryLevelM / secondaryHeight) * 100.0;
SecondaryPercentage = Math.Min(100.0, Math.Max(0.0, SecondaryPercentage));
}
else
{
SecondaryPercentage = 0.0;
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error actualizando porcentajes de tres secciones: {ex.Message}");
}
}
/// <summary>
/// Actualiza los niveles basados en el nivel total actual
/// </summary>
public void UpdateThreeSectionLevels()
{
if (!EnableThreeSection) return;
try
{
lock (_levelLock)
{
ConfigureThreeSectionLevels();
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error actualizando niveles de tres secciones: {ex.Message}");
}
}

View File

@ -143,24 +143,6 @@
</Rectangle>
</Grid>
<!-- Líneas de nivel mejoradas -->
<Canvas Margin="8">
<!-- Línea de nivel máximo -->
<Line X1="0" Y1="4" X2="{Binding ActualWidth, ElementName=rectTankContainer, Converter={StaticResource SubtractConverter}, ConverterParameter=16}" Y2="4"
Stroke="#FFB22222" StrokeThickness="2" StrokeDashArray="4,2" Opacity="0.8"/>
<TextBlock Canvas.Right="2" Canvas.Top="-2" Text="MAX" Foreground="#FFB22222" FontSize="6" FontWeight="Bold"/>
<!-- Línea de nivel medio -->
<Line X1="0" Y1="{Binding MidLevelY}" X2="{Binding ActualWidth, ElementName=rectTankContainer, Converter={StaticResource SubtractConverter}, ConverterParameter=16}" Y2="{Binding MidLevelY}"
Stroke="#FFFF8C00" StrokeThickness="1" StrokeDashArray="2,2" Opacity="0.6"/>
<TextBlock Canvas.Right="2" Canvas.Top="{Binding MidLevelY, Converter={StaticResource SubtractConverter}, ConverterParameter=8}"
Text="50%" Foreground="#FFFF8C00" FontSize="5"/>
<!-- Línea de nivel mínimo -->
<Line X1="0" Y1="{Binding MinLevelY}" X2="{Binding ActualWidth, ElementName=rectTankContainer, Converter={StaticResource SubtractConverter}, ConverterParameter=16}" Y2="{Binding MinLevelY}"
Stroke="#FFB22222" StrokeThickness="2" StrokeDashArray="4,2" Opacity="0.8"/>
<TextBlock Canvas.Right="2" Canvas.Bottom="2" Text="MIN" Foreground="#FFB22222" FontSize="6" FontWeight="Bold"/>
</Canvas>
<!-- Información central del tanque -->
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
@ -199,13 +181,6 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Tipo de tanque -->
<Border Grid.Row="0" Background="#FF000080" CornerRadius="3" Padding="4,2">
<TextBlock Text="{Binding TankTypeIndicator}"
Foreground="White"
FontWeight="Bold"
FontSize="10" />
</Border>
<!-- Tipo de fluido actual -->
<Border Grid.Row="1" Background="#FF006400" CornerRadius="3" Padding="3,2" Margin="0,2,0,0">
@ -262,70 +237,36 @@
</Border>
</StackPanel>
<!-- Fila 2: Flujos de entrada y salida -->
<!-- Fila 2: Flujo neto -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,2">
<!-- Flujo de entrada -->
<Border Background="#FF228B22" CornerRadius="3" Padding="4,3" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Text="▼" Foreground="White" FontSize="9" FontWeight="Bold"/>
<TextBlock Text="{Binding InletFlow, StringFormat='{}{0:F1}'}"
Foreground="White" FontSize="8" FontWeight="SemiBold" Margin="2,0,0,0"/>
<TextBlock Text="L/min" Foreground="LightGreen" FontSize="6" Opacity="0.9" Margin="1,0,0,0"/>
</StackPanel>
</Border>
<!-- Flujo de salida -->
<Border Background="#FFDC143C" CornerRadius="3" Padding="4,3" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Text="▲" Foreground="White" FontSize="9" FontWeight="Bold"/>
<TextBlock Text="{Binding OutletFlow, StringFormat='{}{0:F1}'}"
Foreground="White" FontSize="8" FontWeight="SemiBold" Margin="2,0,0,0"/>
<TextBlock Text="L/min" Foreground="LightPink" FontSize="6" Opacity="0.9" Margin="1,0,0,0"/>
</StackPanel>
</Border>
<!-- Balance de flujo -->
<!-- Flujo neto -->
<Border Background="{Binding FlowBalanceColor}" CornerRadius="3" Padding="4,3" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Δ" Foreground="White" FontSize="9" FontWeight="Bold"/>
<TextBlock Text="{Binding FlowBalance, StringFormat='{}{0:F1}'}"
<TextBlock Text="⇄" Foreground="White" FontSize="9" FontWeight="Bold"/>
<TextBlock Text="{Binding NetFlow, StringFormat='{}{0:F1}'}"
Foreground="White" FontSize="8" FontWeight="SemiBold" Margin="2,0,0,0"/>
<TextBlock Text="L/min" Foreground="White" FontSize="6" Opacity="0.9" Margin="1,0,0,0"/>
</StackPanel>
</Border>
</StackPanel>
<!-- Fila 3: Información de tres secciones (solo si está habilitado) -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,2"
Visibility="{Binding EnableThreeSection, Converter={StaticResource BooleanToVisibilityConverter}}">
<!-- Fila 3: Volumen secundario y mix (simplificado) -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,2">
<!-- Sección secundaria -->
<!-- Volumen secundario -->
<Border Background="#AA1E90FF" CornerRadius="3" Padding="3,2" Margin="1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="S:" Foreground="White" FontSize="7" FontWeight="Bold"/>
<TextBlock Text="{Binding SecondaryLevelM, StringFormat='{}{0:F2}m'}"
<TextBlock Text="Sec:" Foreground="White" FontSize="7" FontWeight="Bold"/>
<TextBlock Text="{Binding SecondaryVolumeL, StringFormat='{}{0:F0}L'}"
Foreground="White" FontSize="7" Margin="1,0,0,0"/>
<TextBlock Text="{Binding SecondaryVolumeL, StringFormat=' {0:F0}L'}"
Foreground="LightCyan" FontSize="6" Opacity="0.9"/>
</StackPanel>
</Border>
<!-- Sección primaria -->
<Border Background="#AAFF6347" CornerRadius="3" Padding="3,2" Margin="1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="P:" Foreground="White" FontSize="7" FontWeight="Bold"/>
<TextBlock Text="{Binding PrimaryLevelM, StringFormat='{}{0:F2}m'}"
Foreground="White" FontSize="7" Margin="1,0,0,0"/>
<TextBlock Text="{Binding PrimaryVolumeL, StringFormat=' {0:F0}L'}"
Foreground="LightYellow" FontSize="6" Opacity="0.9"/>
</StackPanel>
</Border>
<!-- Ratio de mezcla -->
<!-- Volumen de mezcla -->
<Border Background="#AA8B4513" CornerRadius="3" Padding="3,2" Margin="1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Mix:" Foreground="White" FontSize="7" FontWeight="Bold"/>
<TextBlock Text="{Binding CurrentMixRatio, StringFormat='{}{0:P0}'}"
<TextBlock Text="{Binding MixingVolumeL, StringFormat='{}{0:F0}L'}"
Foreground="White" FontSize="7" Margin="1,0,0,0"/>
</StackPanel>
</Border>

View File

@ -52,6 +52,7 @@ namespace CtrEditor.Services
_isRunning = true;
// Instalar el trace listener personalizado solo para Trace
// Nota: Debug.Listeners no existe en .NET Core/.NET 8
_traceListener = new DebugTraceListener(this);
Trace.Listeners.Add(_traceListener);
@ -84,7 +85,7 @@ namespace CtrEditor.Services
_isRunning = false;
_cancellationTokenSource?.Cancel();
// Remover el trace listener
// Remover el trace listener de Trace
if (_traceListener != null)
{
Trace.Listeners.Remove(_traceListener);
@ -116,8 +117,17 @@ namespace CtrEditor.Services
{
while (_isRunning && !cancellationToken.IsCancellationRequested)
{
Debug.WriteLine("[Debug Console Server] Esperando conexión de cliente...");
var tcpClient = await _tcpListener.AcceptTcpClientAsync();
try
{
// Implementar timeout y cancelación manual para evitar freeze
var acceptTask = _tcpListener.AcceptTcpClientAsync();
var delayTask = Task.Delay(1000, cancellationToken); // Check every 1s
var completedTask = await Task.WhenAny(acceptTask, delayTask);
if (completedTask == acceptTask && !cancellationToken.IsCancellationRequested)
{
var tcpClient = await acceptTask;
_connectedClients.Add(tcpClient);
Debug.WriteLine("[Debug Console Server] Cliente debug conectado");
@ -125,12 +135,34 @@ namespace CtrEditor.Services
// Manejar cliente en background
_ = Task.Run(async () => await HandleClientAsync(tcpClient, cancellationToken));
}
else if (cancellationToken.IsCancellationRequested)
{
break;
}
// Si completedTask == delayTask, simplemente continúa el loop para verificar estado
}
catch (ObjectDisposedException)
{
// TcpListener fue cerrado, salir silenciosamente
break;
}
catch (Exception ex)
{
Debug.WriteLine($"[Debug Console Server] Error aceptando conexión: {ex.Message}");
if (!_isRunning) break;
await Task.Delay(1000, cancellationToken); // Esperar antes de reintentar
}
}
}
catch (OperationCanceledException)
{
// Cancelación normal, no hacer nada
}
catch (Exception ex)
{
if (_isRunning)
{
Debug.WriteLine($"[Debug Console Server] Error aceptando conexión: {ex.Message}");
Debug.WriteLine($"[Debug Console Server] Error en AcceptConnectionsAsync: {ex.Message}");
}
}
}
@ -318,8 +350,9 @@ namespace CtrEditor.Services
}
/// <summary>
/// TraceListener personalizado que captura mensajes de Debug.WriteLine
/// TraceListener personalizado que captura mensajes de Trace.WriteLine
/// y los envía al servidor de debug console
/// Nota: Debug.WriteLine no está disponible en .NET Core/.NET 8
/// </summary>
internal class DebugTraceListener : TraceListener
{