1223 lines
46 KiB
C#
1223 lines
46 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Threading;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using CtrEditor.ObjetosSim;
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.IO;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Windows.Media;
|
|
using System.Windows.Shapes;
|
|
using System.Windows.Controls;
|
|
using CtrEditor.FuncionesBase;
|
|
|
|
namespace CtrEditor.Services
|
|
{
|
|
/// <summary>
|
|
/// Servidor MCP TCP para CtrEditor que permite control remoto de la aplicación
|
|
/// para debugging y testing usando el protocolo Model Context Protocol
|
|
/// </summary>
|
|
public class MCPServer : IDisposable
|
|
{
|
|
private readonly MainViewModel _mainViewModel;
|
|
private readonly int _port;
|
|
private TcpListener _tcpListener;
|
|
private CancellationTokenSource _cancellationTokenSource;
|
|
private bool _isRunning;
|
|
private readonly object _lockObject = new object();
|
|
|
|
// Simulation timing tracking
|
|
private readonly Stopwatch _simulationStopwatch;
|
|
private long _totalSimulationMilliseconds;
|
|
private bool _lastSimulationStatus;
|
|
|
|
public MCPServer(MainViewModel mainViewModel, int port = 5006)
|
|
{
|
|
_mainViewModel = mainViewModel ?? throw new ArgumentNullException(nameof(mainViewModel));
|
|
_port = port;
|
|
_cancellationTokenSource = new CancellationTokenSource();
|
|
|
|
// Initialize simulation timing
|
|
_simulationStopwatch = new Stopwatch();
|
|
_totalSimulationMilliseconds = 0;
|
|
_lastSimulationStatus = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inicia el servidor MCP TCP
|
|
/// </summary>
|
|
public async Task StartAsync()
|
|
{
|
|
if (_isRunning) return;
|
|
|
|
try
|
|
{
|
|
_tcpListener = new TcpListener(IPAddress.Loopback, _port);
|
|
_tcpListener.Start();
|
|
_isRunning = true;
|
|
|
|
Debug.WriteLine($"[MCP Server] Servidor iniciado en puerto {_port}");
|
|
|
|
// Procesar conexiones en background
|
|
_ = Task.Run(async () => await AcceptConnectionsAsync(_cancellationTokenSource.Token));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Error al iniciar servidor: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detiene el servidor MCP TCP
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
if (!_isRunning) return;
|
|
|
|
lock (_lockObject)
|
|
{
|
|
if (!_isRunning) return;
|
|
|
|
_isRunning = false;
|
|
_cancellationTokenSource?.Cancel();
|
|
_tcpListener?.Stop();
|
|
|
|
Debug.WriteLine("[MCP Server] Servidor detenido");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Acepta conexiones TCP entrantes
|
|
/// </summary>
|
|
private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (_isRunning && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var tcpClient = await _tcpListener.AcceptTcpClientAsync();
|
|
Debug.WriteLine("[MCP Server] Nueva conexión aceptada");
|
|
|
|
// Manejar cliente en tarea separada
|
|
_ = Task.Run(async () => await HandleClientAsync(tcpClient, cancellationToken));
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
// Expected when stopping the server
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_isRunning)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Error aceptando conexión: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maneja un cliente TCP individual
|
|
/// </summary>
|
|
private async Task HandleClientAsync(TcpClient tcpClient, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
using (tcpClient)
|
|
using (var stream = tcpClient.GetStream())
|
|
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
|
using (var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true })
|
|
{
|
|
Debug.WriteLine("[MCP Server] Cliente conectado");
|
|
|
|
// No enviar capabilities automáticamente - esperar initialize
|
|
// await SendInitialCapabilitiesAsync(writer);
|
|
|
|
string line;
|
|
while ((line = await reader.ReadLineAsync()) != null && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(line))
|
|
{
|
|
var response = await ProcessRequestAsync(line);
|
|
await writer.WriteLineAsync(response);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Error procesando solicitud: {ex.Message}");
|
|
var errorResponse = CreateErrorResponse(null, -32603, "Internal error", ex.Message);
|
|
await writer.WriteLineAsync(errorResponse);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Error en cliente: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
Debug.WriteLine("[MCP Server] Cliente desconectado");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Envía las capabilities iniciales del servidor MCP
|
|
/// </summary>
|
|
private async Task SendInitialCapabilitiesAsync(StreamWriter writer)
|
|
{
|
|
var capabilities = new
|
|
{
|
|
jsonrpc = "2.0",
|
|
id = "init",
|
|
result = new
|
|
{
|
|
protocolVersion = "2024-11-05",
|
|
capabilities = new
|
|
{
|
|
tools = new object[]
|
|
{
|
|
new { name = "list_objects", description = "List all objects in the simulation with metadata" },
|
|
new { name = "create_object", description = "Create a new object by type at specified position" },
|
|
new { name = "update_object", description = "Update object properties by ID using JSON" },
|
|
new { name = "delete_objects", description = "Delete objects by ID (single or batch)" },
|
|
new { name = "list_object_types", description = "List all available object types that can be created" },
|
|
new { name = "start_simulation", description = "Start the physics simulation" },
|
|
new { name = "stop_simulation", description = "Stop the physics simulation" },
|
|
new { name = "get_simulation_status", description = "Get current simulation status" },
|
|
new { name = "get_plc_status", description = "Get PLC connection status" },
|
|
new { name = "take_screenshot", description = "Take a screenshot of the canvas" },
|
|
new { name = "save_project", description = "Save the current project" },
|
|
new { name = "reset_simulation_timing", description = "Reset simulation timing counters" }
|
|
}
|
|
},
|
|
serverInfo = new
|
|
{
|
|
name = "CtrEditor MCP Server",
|
|
version = "1.0.0",
|
|
description = "CtrEditor WPF Application MCP Server for debugging and testing"
|
|
}
|
|
}
|
|
};
|
|
|
|
var json = JsonConvert.SerializeObject(capabilities);
|
|
await writer.WriteLineAsync(json);
|
|
Debug.WriteLine("[MCP Server] Capabilities enviadas");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Procesa una solicitud JSON-RPC
|
|
/// </summary>
|
|
private async Task<string> ProcessRequestAsync(string requestJson)
|
|
{
|
|
try
|
|
{
|
|
var request = JsonConvert.DeserializeObject<JObject>(requestJson);
|
|
var method = request["method"]?.ToString();
|
|
var id = request["id"];
|
|
var parameters = request["params"] as JObject;
|
|
|
|
Debug.WriteLine($"[MCP Server] Procesando método: {method}");
|
|
|
|
return method switch
|
|
{
|
|
"initialize" => HandleInitializeAsync(id, parameters),
|
|
"tools/list" => HandleToolsListAsync(id),
|
|
"tools/call" => await HandleToolCallAsync(id, parameters),
|
|
_ => CreateErrorResponse(id, -32601, "Method not found", $"Unknown method: {method}")
|
|
};
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
return CreateErrorResponse(null, -32700, "Parse error", ex.Message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return CreateErrorResponse(null, -32603, "Internal error", ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maneja la solicitud initialize
|
|
/// </summary>
|
|
private string HandleInitializeAsync(JToken id, JObject parameters)
|
|
{
|
|
var result = new
|
|
{
|
|
protocolVersion = "2024-11-05",
|
|
capabilities = new
|
|
{
|
|
tools = new { },
|
|
resources = new { },
|
|
prompts = new { }
|
|
},
|
|
serverInfo = new
|
|
{
|
|
name = "CtrEditor MCP Server",
|
|
version = "1.0.0"
|
|
}
|
|
};
|
|
|
|
return CreateSuccessResponse(id, result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maneja la solicitud tools/list
|
|
/// </summary>
|
|
private string HandleToolsListAsync(JToken id)
|
|
{
|
|
var tools = new object[]
|
|
{
|
|
new {
|
|
name = "list_objects",
|
|
description = "List all objects in the simulation",
|
|
inputSchema = new {
|
|
type = "object",
|
|
properties = new { }
|
|
}
|
|
},
|
|
new {
|
|
name = "create_object",
|
|
description = "Create a new object by type at specified position",
|
|
inputSchema = new {
|
|
type = "object",
|
|
properties = new {
|
|
type = new { type = "string", description = "Object type to create" },
|
|
x = new { type = "number", description = "X position" },
|
|
y = new { type = "number", description = "Y position" }
|
|
},
|
|
required = new[] { "type", "x", "y" }
|
|
}
|
|
},
|
|
new {
|
|
name = "take_screenshot",
|
|
description = "Take a screenshot of the canvas. By default captures full canvas without background image and saves to screenshots subdirectory.",
|
|
inputSchema = new {
|
|
type = "object",
|
|
properties = new {
|
|
filename = new {
|
|
type = "string",
|
|
description = "Optional filename for the screenshot (will be saved in screenshots subdirectory). Defaults to timestamp-based name."
|
|
},
|
|
include_background = new {
|
|
type = "boolean",
|
|
description = "Whether to include canvas background image. Defaults to false (white background)."
|
|
},
|
|
x = new {
|
|
type = "number",
|
|
description = "X coordinate in meters for partial capture (optional)"
|
|
},
|
|
y = new {
|
|
type = "number",
|
|
description = "Y coordinate in meters for partial capture (optional)"
|
|
},
|
|
width = new {
|
|
type = "number",
|
|
description = "Width in meters for partial capture (optional)"
|
|
},
|
|
height = new {
|
|
type = "number",
|
|
description = "Height in meters for partial capture (optional)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
new {
|
|
name = "start_simulation",
|
|
description = "Start the physics simulation",
|
|
inputSchema = new {
|
|
type = "object",
|
|
properties = new { }
|
|
}
|
|
},
|
|
new {
|
|
name = "stop_simulation",
|
|
description = "Stop the physics simulation",
|
|
inputSchema = new {
|
|
type = "object",
|
|
properties = new { }
|
|
}
|
|
},
|
|
new {
|
|
name = "reset_simulation_timing",
|
|
description = "Reset simulation timing counters to zero",
|
|
inputSchema = new {
|
|
type = "object",
|
|
properties = new { }
|
|
}
|
|
}
|
|
};
|
|
|
|
return CreateSuccessResponse(id, new { tools });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Crea una respuesta de éxito
|
|
/// </summary>
|
|
private string CreateSuccessResponse(JToken id, object result)
|
|
{
|
|
var response = new
|
|
{
|
|
jsonrpc = "2.0",
|
|
id = id,
|
|
result = result
|
|
};
|
|
return JsonConvert.SerializeObject(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maneja llamadas a herramientas (tools/call)
|
|
/// </summary>
|
|
private async Task<string> HandleToolCallAsync(JToken id, JObject parameters)
|
|
{
|
|
try
|
|
{
|
|
var toolName = parameters?["name"]?.ToString();
|
|
var arguments = parameters?["arguments"] as JObject ?? new JObject();
|
|
|
|
Debug.WriteLine($"[MCP Server] Ejecutando herramienta: {toolName}");
|
|
|
|
var result = await Application.Current.Dispatcher.InvokeAsync(() => ExecuteTool(toolName, arguments));
|
|
|
|
// Envolver el resultado en el formato MCP correcto
|
|
var mcpResult = new
|
|
{
|
|
content = new[]
|
|
{
|
|
new
|
|
{
|
|
type = "text",
|
|
text = JsonConvert.SerializeObject(result, Formatting.Indented)
|
|
}
|
|
}
|
|
};
|
|
|
|
return CreateSuccessResponse(id, mcpResult);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Error ejecutando herramienta: {ex.Message}");
|
|
return CreateErrorResponse(id, -32603, "Tool execution error", ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ejecuta una herramienta específica (debe ejecutarse en UI thread)
|
|
/// </summary>
|
|
private object ExecuteTool(string toolName, JObject arguments)
|
|
{
|
|
return toolName switch
|
|
{
|
|
"list_objects" => ListObjects(),
|
|
"create_object" => CreateObject(arguments),
|
|
"update_object" => UpdateObject(arguments),
|
|
"delete_objects" => DeleteObjects(arguments),
|
|
"list_object_types" => ListObjectTypes(),
|
|
"start_simulation" => StartSimulation(arguments),
|
|
"stop_simulation" => StopSimulation(),
|
|
"get_simulation_status" => GetSimulationStatus(),
|
|
"get_plc_status" => GetPlcStatus(),
|
|
"take_screenshot" => TakeScreenshot(arguments),
|
|
"save_project" => SaveProject(),
|
|
"reset_simulation_timing" => ResetSimulationTiming(),
|
|
_ => throw new ArgumentException($"Unknown tool: {toolName}")
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates simulation timing based on current status
|
|
/// </summary>
|
|
private void UpdateSimulationTiming()
|
|
{
|
|
var currentSimulationStatus = _mainViewModel.IsSimulationRunning;
|
|
|
|
if (_lastSimulationStatus != currentSimulationStatus)
|
|
{
|
|
if (currentSimulationStatus)
|
|
{
|
|
// Simulation just started
|
|
Debug.WriteLine("[MCP Server] Simulation started - starting timer");
|
|
_simulationStopwatch.Start();
|
|
}
|
|
else
|
|
{
|
|
// Simulation just stopped
|
|
if (_simulationStopwatch.IsRunning)
|
|
{
|
|
_simulationStopwatch.Stop();
|
|
_totalSimulationMilliseconds += _simulationStopwatch.ElapsedMilliseconds;
|
|
Debug.WriteLine($"[MCP Server] Simulation stopped - total time: {_totalSimulationMilliseconds}ms");
|
|
_simulationStopwatch.Reset();
|
|
}
|
|
}
|
|
_lastSimulationStatus = currentSimulationStatus;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets current simulation elapsed time in milliseconds
|
|
/// </summary>
|
|
private long GetCurrentSimulationMilliseconds()
|
|
{
|
|
UpdateSimulationTiming();
|
|
|
|
var currentElapsed = _simulationStopwatch.IsRunning ? _simulationStopwatch.ElapsedMilliseconds : 0;
|
|
return _totalSimulationMilliseconds + currentElapsed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets simulation timing counters
|
|
/// </summary>
|
|
private object ResetSimulationTiming()
|
|
{
|
|
try
|
|
{
|
|
var wasRunning = _simulationStopwatch.IsRunning;
|
|
var previousTotal = _totalSimulationMilliseconds;
|
|
var previousCurrent = _simulationStopwatch.ElapsedMilliseconds;
|
|
|
|
_simulationStopwatch.Reset();
|
|
_totalSimulationMilliseconds = 0;
|
|
|
|
if (wasRunning && _mainViewModel.IsSimulationRunning)
|
|
{
|
|
_simulationStopwatch.Start();
|
|
}
|
|
|
|
Debug.WriteLine("[MCP Server] Simulation timing reset");
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = "Simulation timing reset successfully",
|
|
previous_total_ms = previousTotal,
|
|
previous_current_ms = previousCurrent,
|
|
was_running = wasRunning
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
#region Tool Implementations
|
|
|
|
/// <summary>
|
|
/// Lista todos los objetos en la simulación con metadata
|
|
/// </summary>
|
|
private object ListObjects()
|
|
{
|
|
try
|
|
{
|
|
var objects = _mainViewModel.ObjetosSimulables
|
|
.Where(obj => obj.Show_On_This_Page)
|
|
.Select(obj => new
|
|
{
|
|
id = obj.Id,
|
|
name = obj.Nombre,
|
|
type = obj.GetType().Name,
|
|
position = new { x = obj.Left, y = obj.Top },
|
|
dimensions = new { width = obj.Ancho, height = obj.Alto },
|
|
angle = obj.Angulo,
|
|
visible = obj.IsVisFilter,
|
|
locked = obj.Lock_movement,
|
|
tags = obj.ListaEtiquetas?.ToArray() ?? new string[0],
|
|
properties = GetObjectProperties(obj)
|
|
})
|
|
.ToArray();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
count = objects.Length,
|
|
simulation_elapsed_ms = GetCurrentSimulationMilliseconds(),
|
|
simulation_running = _mainViewModel.IsSimulationRunning,
|
|
objects = objects
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Obtiene las propiedades serializables de un objeto
|
|
/// </summary>
|
|
private object GetObjectProperties(osBase obj)
|
|
{
|
|
try
|
|
{
|
|
var settings = new JsonSerializerSettings
|
|
{
|
|
TypeNameHandling = TypeNameHandling.Auto,
|
|
NullValueHandling = NullValueHandling.Ignore,
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
|
};
|
|
|
|
var json = JsonConvert.SerializeObject(obj, settings);
|
|
return JsonConvert.DeserializeObject(json);
|
|
}
|
|
catch
|
|
{
|
|
return new { error = "Could not serialize object properties" };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Crea un nuevo objeto del tipo especificado
|
|
/// </summary>
|
|
private object CreateObject(JObject arguments)
|
|
{
|
|
try
|
|
{
|
|
var typeName = arguments["type"]?.ToString();
|
|
var x = arguments["x"]?.ToObject<float>() ?? 0f;
|
|
var y = arguments["y"]?.ToObject<float>() ?? 0f;
|
|
var properties = arguments["properties"] as JObject;
|
|
|
|
if (string.IsNullOrEmpty(typeName))
|
|
throw new ArgumentException("Type is required");
|
|
|
|
// Buscar el tipo en la lista de tipos disponibles
|
|
var tipoSimulable = _mainViewModel.ListaOsBase.FirstOrDefault(t =>
|
|
t.Tipo.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase) ||
|
|
t.Nombre.Equals(typeName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (tipoSimulable == null)
|
|
throw new ArgumentException($"Object type '{typeName}' not found");
|
|
|
|
// Crear el objeto
|
|
var newObject = _mainViewModel.CrearObjetoSimulable(tipoSimulable.Tipo, x, y);
|
|
|
|
if (newObject == null)
|
|
throw new Exception("Failed to create object");
|
|
|
|
// Aplicar propiedades adicionales si se proporcionaron
|
|
if (properties != null)
|
|
{
|
|
ApplyPropertiesToObject(newObject, properties);
|
|
}
|
|
|
|
// Crear el UserControl asociado
|
|
_mainViewModel.CrearUserControlDesdeObjetoSimulable(newObject);
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
object_id = newObject.Id,
|
|
message = $"Object of type '{typeName}' created successfully",
|
|
object_data = new
|
|
{
|
|
id = newObject.Id,
|
|
name = newObject.Nombre,
|
|
type = newObject.GetType().Name,
|
|
position = new { x = newObject.Left, y = newObject.Top },
|
|
dimensions = new { width = newObject.Ancho, height = newObject.Alto }
|
|
}
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Actualiza las propiedades de un objeto existente
|
|
/// </summary>
|
|
private object UpdateObject(JObject arguments)
|
|
{
|
|
try
|
|
{
|
|
var objectId = arguments["id"]?.ToObject<int>();
|
|
var properties = arguments["properties"] as JObject;
|
|
|
|
if (!objectId.HasValue)
|
|
throw new ArgumentException("Object ID is required");
|
|
|
|
if (properties == null)
|
|
throw new ArgumentException("Properties are required");
|
|
|
|
var obj = _mainViewModel.ObjetosSimulables.FirstOrDefault(o => o.Id.Value == objectId.Value);
|
|
if (obj == null)
|
|
throw new ArgumentException($"Object with ID {objectId} not found");
|
|
|
|
ApplyPropertiesToObject(obj, properties);
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
object_id = objectId.Value,
|
|
message = "Object updated successfully"
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aplica propiedades JSON a un objeto
|
|
/// </summary>
|
|
private void ApplyPropertiesToObject(osBase obj, JObject properties)
|
|
{
|
|
var objType = obj.GetType();
|
|
|
|
foreach (var prop in properties)
|
|
{
|
|
try
|
|
{
|
|
var property = objType.GetProperty(prop.Key);
|
|
if (property != null && property.CanWrite)
|
|
{
|
|
var value = prop.Value.ToObject(property.PropertyType);
|
|
property.SetValue(obj, value);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Error setting property {prop.Key}: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Elimina objetos por ID
|
|
/// </summary>
|
|
private object DeleteObjects(JObject arguments)
|
|
{
|
|
try
|
|
{
|
|
var ids = new List<int>();
|
|
|
|
// Soportar tanto ID único como array de IDs
|
|
if (arguments["id"] != null)
|
|
{
|
|
ids.Add(arguments["id"].ToObject<int>());
|
|
}
|
|
else if (arguments["ids"] != null)
|
|
{
|
|
ids.AddRange(arguments["ids"].ToObject<int[]>());
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException("Either 'id' or 'ids' is required");
|
|
}
|
|
|
|
var deletedCount = 0;
|
|
var errors = new List<string>();
|
|
|
|
foreach (var id in ids)
|
|
{
|
|
var obj = _mainViewModel.ObjetosSimulables.FirstOrDefault(o => o.Id.Value == id);
|
|
if (obj != null)
|
|
{
|
|
_mainViewModel.RemoverObjetoSimulable(obj);
|
|
deletedCount++;
|
|
}
|
|
else
|
|
{
|
|
errors.Add($"Object with ID {id} not found");
|
|
}
|
|
}
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
deleted_count = deletedCount,
|
|
errors = errors.ToArray(),
|
|
message = $"Deleted {deletedCount} object(s)"
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lista todos los tipos de objetos disponibles para crear
|
|
/// </summary>
|
|
private object ListObjectTypes()
|
|
{
|
|
try
|
|
{
|
|
var types = _mainViewModel.ListaOsBase
|
|
.Select(t => new
|
|
{
|
|
name = t.Nombre,
|
|
type_name = t.Tipo.Name,
|
|
category = t.Categoria,
|
|
full_type_name = t.Tipo.FullName
|
|
})
|
|
.OrderBy(t => t.category)
|
|
.ThenBy(t => t.name)
|
|
.ToArray();
|
|
|
|
var categories = types.GroupBy(t => t.category)
|
|
.Select(g => new
|
|
{
|
|
category = g.Key,
|
|
types = g.ToArray()
|
|
})
|
|
.ToArray();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
total_types = types.Length,
|
|
categories = categories,
|
|
all_types = types
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inicia la simulación
|
|
/// </summary>
|
|
private object StartSimulation(JObject arguments)
|
|
{
|
|
try
|
|
{
|
|
var duration = arguments["duration"]?.ToObject<double?>(); // Duration in seconds
|
|
|
|
if (_mainViewModel.IsSimulationRunning)
|
|
{
|
|
return new
|
|
{
|
|
success = false,
|
|
error = "Simulation is already running"
|
|
};
|
|
}
|
|
|
|
// Iniciar simulación usando el método privado
|
|
var startMethod = typeof(MainViewModel).GetMethod("StartSimulation", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
startMethod?.Invoke(_mainViewModel, null);
|
|
|
|
// Si se especifica duración, programar parada automática
|
|
if (duration.HasValue && duration.Value > 0)
|
|
{
|
|
var timer = new System.Timers.Timer(duration.Value * 1000);
|
|
timer.Elapsed += (s, e) =>
|
|
{
|
|
timer.Dispose();
|
|
Application.Current.Dispatcher.Invoke(() => _mainViewModel.StopSimulation());
|
|
};
|
|
timer.Start();
|
|
}
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = "Simulation started successfully",
|
|
duration_seconds = duration,
|
|
auto_stop = duration.HasValue,
|
|
simulation_elapsed_ms = GetCurrentSimulationMilliseconds()
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detiene la simulación
|
|
/// </summary>
|
|
private object StopSimulation()
|
|
{
|
|
try
|
|
{
|
|
if (!_mainViewModel.IsSimulationRunning)
|
|
{
|
|
return new
|
|
{
|
|
success = false,
|
|
error = "Simulation is not running"
|
|
};
|
|
}
|
|
|
|
_mainViewModel.StopSimulation();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = "Simulation stopped successfully",
|
|
simulation_elapsed_ms = GetCurrentSimulationMilliseconds(),
|
|
simulation_elapsed_seconds = Math.Round(GetCurrentSimulationMilliseconds() / 1000.0, 3)
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Obtiene el estado actual de la simulación
|
|
/// </summary>
|
|
private object GetSimulationStatus()
|
|
{
|
|
try
|
|
{
|
|
var elapsedMs = GetCurrentSimulationMilliseconds();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
is_running = _mainViewModel.IsSimulationRunning,
|
|
simulation_elapsed_ms = elapsedMs,
|
|
simulation_elapsed_seconds = Math.Round(elapsedMs / 1000.0, 3),
|
|
object_count = _mainViewModel.ObjetosSimulables.Count,
|
|
visible_objects = _mainViewModel.ObjetosSimulables.Count(o => o.Show_On_This_Page)
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Obtiene el estado de conexión del PLC
|
|
/// </summary>
|
|
private object GetPlcStatus()
|
|
{
|
|
try
|
|
{
|
|
return new
|
|
{
|
|
success = true,
|
|
is_connected = _mainViewModel.IsConnected,
|
|
plc_enabled = _mainViewModel.PLCViewModel != null
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toma una captura de pantalla del canvas
|
|
/// </summary>
|
|
private object TakeScreenshot(JObject arguments)
|
|
{
|
|
try
|
|
{
|
|
// Parámetros de screenshot
|
|
var filename = arguments["filename"]?.ToString() ?? $"screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.png";
|
|
var includeBackground = arguments["include_background"]?.ToObject<bool>() ?? false; // Por defecto false
|
|
var x = arguments["x"]?.ToObject<float?>();
|
|
var y = arguments["y"]?.ToObject<float?>();
|
|
var width = arguments["width"]?.ToObject<float?>();
|
|
var height = arguments["height"]?.ToObject<float?>();
|
|
|
|
// Asegurar extensión .png
|
|
if (!filename.ToLower().EndsWith(".png"))
|
|
filename += ".png";
|
|
|
|
// Crear subdirectorio screenshots
|
|
var screenshotsDir = System.IO.Path.Combine(EstadoPersistente.Instance.directorio, "screenshots");
|
|
Directory.CreateDirectory(screenshotsDir);
|
|
|
|
// Obtener ruta completa - siempre en subdirectorio screenshots a menos que sea ruta absoluta
|
|
var fullPath = System.IO.Path.IsPathRooted(filename) ? filename : System.IO.Path.Combine(screenshotsDir, filename);
|
|
|
|
// Obtener información del canvas para detalles
|
|
var canvas = _mainViewModel.MainCanvas;
|
|
var canvasWidth = canvas?.ActualWidth ?? 0;
|
|
var canvasHeight = canvas?.ActualHeight ?? 0;
|
|
var canvasWidthMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)canvasWidth);
|
|
var canvasHeightMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)canvasHeight);
|
|
|
|
// Tomar screenshot
|
|
var success = TakeCanvasScreenshot(fullPath, includeBackground, x, y, width, height);
|
|
|
|
if (success)
|
|
{
|
|
var fileInfo = new FileInfo(fullPath);
|
|
return new
|
|
{
|
|
success = true,
|
|
filename = System.IO.Path.GetFileName(fullPath),
|
|
full_path = fullPath,
|
|
directory = System.IO.Path.GetDirectoryName(fullPath),
|
|
file_size_bytes = fileInfo.Length,
|
|
timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
message = "Screenshot saved successfully",
|
|
canvas_info = new
|
|
{
|
|
canvas_width_pixels = canvasWidth,
|
|
canvas_height_pixels = canvasHeight,
|
|
canvas_width_meters = Math.Round(canvasWidthMeters, 3),
|
|
canvas_height_meters = Math.Round(canvasHeightMeters, 3)
|
|
},
|
|
capture_info = new
|
|
{
|
|
include_background = includeBackground,
|
|
area_specified = x.HasValue && y.HasValue && width.HasValue && height.HasValue,
|
|
area = x.HasValue ? new {
|
|
x = Math.Round(x.Value, 3),
|
|
y = Math.Round(y.Value, 3),
|
|
width = Math.Round(width.Value, 3),
|
|
height = Math.Round(height.Value, 3),
|
|
units = "meters"
|
|
} : null,
|
|
capture_type = x.HasValue ? "partial" : "full_canvas"
|
|
}
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return new
|
|
{
|
|
success = false,
|
|
error = "Failed to capture screenshot",
|
|
attempted_path = fullPath
|
|
};
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new {
|
|
success = false,
|
|
error = ex.Message,
|
|
error_type = ex.GetType().Name
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Guarda el proyecto actual
|
|
/// </summary>
|
|
private object SaveProject()
|
|
{
|
|
try
|
|
{
|
|
_mainViewModel.Save();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = "Project saved successfully"
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new { success = false, error = ex.Message };
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Screenshot Implementation
|
|
|
|
/// <summary>
|
|
/// Toma una captura de pantalla del canvas
|
|
/// </summary>
|
|
private bool TakeCanvasScreenshot(string filePath, bool includeBackground = false,
|
|
float? x = null, float? y = null, float? width = null, float? height = null)
|
|
{
|
|
try
|
|
{
|
|
var canvas = _mainViewModel.MainCanvas;
|
|
if (canvas == null)
|
|
{
|
|
Debug.WriteLine("[MCP Server] Canvas is null");
|
|
return false;
|
|
}
|
|
|
|
// Asegurar que el canvas esté renderizado
|
|
canvas.UpdateLayout();
|
|
|
|
// Determinar el área a capturar
|
|
Rect captureRect;
|
|
if (x.HasValue && y.HasValue && width.HasValue && height.HasValue)
|
|
{
|
|
// Convertir coordenadas de metros a píxeles
|
|
var pixelX = PixelToMeter.Instance.calc.MetersToPixels(x.Value);
|
|
var pixelY = PixelToMeter.Instance.calc.MetersToPixels(y.Value);
|
|
var pixelWidth = PixelToMeter.Instance.calc.MetersToPixels(width.Value);
|
|
var pixelHeight = PixelToMeter.Instance.calc.MetersToPixels(height.Value);
|
|
|
|
captureRect = new Rect(pixelX, pixelY, pixelWidth, pixelHeight);
|
|
}
|
|
else
|
|
{
|
|
// Capturar todo el canvas
|
|
captureRect = new Rect(0, 0, canvas.ActualWidth, canvas.ActualHeight);
|
|
}
|
|
|
|
// Validar dimensiones
|
|
if (captureRect.Width <= 0 || captureRect.Height <= 0)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Invalid capture dimensions: {captureRect}");
|
|
return false;
|
|
}
|
|
|
|
Debug.WriteLine($"[MCP Server] Capturing area: {captureRect}, Canvas size: {canvas.ActualWidth}x{canvas.ActualHeight}");
|
|
|
|
// Crear RenderTargetBitmap con alta resolución
|
|
// Usar factor de escala para mejorar calidad en capturas parciales
|
|
var scaleFactor = (x.HasValue && y.HasValue) ? 3.0 : 2.0; // Mayor escala para áreas parciales
|
|
var renderWidth = Math.Max(1, (int)(captureRect.Width * scaleFactor));
|
|
var renderHeight = Math.Max(1, (int)(captureRect.Height * scaleFactor));
|
|
var dpi = 96 * scaleFactor; // Aumentar DPI proporcionalmente
|
|
|
|
var renderBitmap = new RenderTargetBitmap(
|
|
renderWidth,
|
|
renderHeight,
|
|
dpi, // DPI X
|
|
dpi, // DPI Y
|
|
PixelFormats.Pbgra32);
|
|
|
|
// Crear un Canvas temporal para renderizado con escala mejorada
|
|
var tempCanvas = new Canvas()
|
|
{
|
|
Width = captureRect.Width * scaleFactor,
|
|
Height = captureRect.Height * scaleFactor,
|
|
Background = includeBackground ? canvas.Background : Brushes.White
|
|
};
|
|
|
|
// Aplicar escala al canvas
|
|
tempCanvas.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor);
|
|
|
|
// Clonar elementos visibles del canvas principal
|
|
foreach (UIElement child in canvas.Children)
|
|
{
|
|
if (child.Visibility == Visibility.Visible)
|
|
{
|
|
try
|
|
{
|
|
// Obtener posición del elemento
|
|
var left = Canvas.GetLeft(child);
|
|
var top = Canvas.GetTop(child);
|
|
|
|
// Verificar si está en el área de captura
|
|
var elementRect = new Rect(
|
|
double.IsNaN(left) ? 0 : left,
|
|
double.IsNaN(top) ? 0 : top,
|
|
child.RenderSize.Width,
|
|
child.RenderSize.Height);
|
|
|
|
if (captureRect.IntersectsWith(elementRect) || (!x.HasValue && !y.HasValue))
|
|
{
|
|
// Crear una representación visual del elemento
|
|
var visualBrush = new VisualBrush(child)
|
|
{
|
|
Stretch = Stretch.None,
|
|
AlignmentX = AlignmentX.Left,
|
|
AlignmentY = AlignmentY.Top
|
|
};
|
|
|
|
var rect = new Rectangle()
|
|
{
|
|
Width = child.RenderSize.Width * scaleFactor,
|
|
Height = child.RenderSize.Height * scaleFactor,
|
|
Fill = visualBrush
|
|
};
|
|
|
|
// Posicionar relativo al área de captura con escala
|
|
Canvas.SetLeft(rect, (elementRect.X - captureRect.X) * scaleFactor);
|
|
Canvas.SetTop(rect, (elementRect.Y - captureRect.Y) * scaleFactor);
|
|
|
|
tempCanvas.Children.Add(rect);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Error processing child element: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Forzar layout del canvas temporal con las nuevas dimensiones escaladas
|
|
var scaledSize = new Size(captureRect.Width * scaleFactor, captureRect.Height * scaleFactor);
|
|
tempCanvas.Measure(scaledSize);
|
|
tempCanvas.Arrange(new Rect(0, 0, scaledSize.Width, scaledSize.Height));
|
|
tempCanvas.UpdateLayout();
|
|
|
|
// Renderizar
|
|
renderBitmap.Render(tempCanvas);
|
|
|
|
// Guardar imagen
|
|
var encoder = new PngBitmapEncoder();
|
|
encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
|
|
|
|
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(filePath));
|
|
using (var fileStream = new FileStream(filePath, FileMode.Create))
|
|
{
|
|
encoder.Save(fileStream);
|
|
}
|
|
|
|
Debug.WriteLine($"[MCP Server] Screenshot saved successfully: {filePath}");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[MCP Server] Error taking screenshot: {ex.Message}");
|
|
Debug.WriteLine($"[MCP Server] Stack trace: {ex.StackTrace}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
/// <summary>
|
|
/// Crea una respuesta de error JSON-RPC
|
|
/// </summary>
|
|
private string CreateErrorResponse(JToken id, int code, string message, string data = null)
|
|
{
|
|
var error = new
|
|
{
|
|
code = code,
|
|
message = message,
|
|
data = data
|
|
};
|
|
|
|
var response = new
|
|
{
|
|
jsonrpc = "2.0",
|
|
id = id,
|
|
error = error
|
|
};
|
|
|
|
return JsonConvert.SerializeObject(response);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable Implementation
|
|
|
|
public void Dispose()
|
|
{
|
|
Stop();
|
|
_cancellationTokenSource?.Dispose();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|