#nullable enable using System.Net.NetworkInformation; using System.Net.Sockets; namespace Lib.io.udp; [Guid("34E4E63B-2708-4673-B682-1D07D0245E1E")] internal sealed class UdpOutput : Instance, IStatusProvider, ICustomDropdownHolder, IDisposable { [Input(Guid = "7AB8F2A6-4874-4235-85A5-D0E1F30C0446")] public readonly InputSlot Connect = new(); [Output(Guid = "07B57F3A-8993-4B9F-8349-D0A4762E4447")] public readonly Slot IsConnected = new(); [Input(Guid = "9E23335A-D63A-4286-930E-C63E86D0E6F0")] public readonly InputSlot LocalIpAddress = new("127.0.0.1"); // Default updated [Input(Guid = "59074D76-1F4F-406A-B512-5813F4E3420E")] public readonly MultiInputSlot MessageParts = new(); [Input(Guid = "A1B2C3D4-E5F6-4789-89AB-CDEF01234567")] public readonly InputSlot PrintToLog = new(); // New GUID for PrintToLog [Output(Guid = "0E2B808F-63A5-4927-9610-410E5F5227B1")] public readonly Slot Result = new(); [Input(Guid = "216A0356-EF4A-413A-A656-7497127E31D4")] public readonly InputSlot SendOnChange = new(true); [Input(Guid = "C7AC22C0-A31E-41F6-B29D-D40956E6688B")] public readonly InputSlot SendTrigger = new(); [Input(Guid = "82933C40-DA9E-4340-A227-E9BACF6E2063")] public readonly InputSlot Separator = new(" "); [Input(Guid = "24B5D450-4E83-49DB-88B1-7D688E64585D")] public readonly InputSlot TargetIpAddress = new("127.0.0.1"); [Input(Guid = "36C2BF8B-3E0C-4856-AA4A-32943A4B0223")] public readonly InputSlot TargetPort = new(7001); private bool _lastConnectState; private string? _lastErrorMessage = "Not connected."; private string? _lastLocalIp; private string? _lastSentMessage; private IStatusProvider.StatusLevel _lastStatusLevel = IStatusProvider.StatusLevel.Notice; private bool _printToLog; // Added for PrintToLog functionality private UdpClient? _udpClient; public UdpOutput() { Result.UpdateAction = Update; } string ICustomDropdownHolder.GetValueForInput(Guid id) { return id == LocalIpAddress.Id ? LocalIpAddress.Value ?? string.Empty: string.Empty; } IEnumerable ICustomDropdownHolder.GetOptionsForInput(Guid id) { if (id == LocalIpAddress.Id) { _networkInterfaces = GetNetworkInterfaces(); foreach (var adapter in _networkInterfaces) yield return adapter.DisplayName; } Enumerable.Empty(); } void ICustomDropdownHolder.HandleResultForInput(Guid id, string? s, bool i) { if (string.IsNullOrEmpty(s) || !i || id != LocalIpAddress.Id) return; var foundAdapter = _networkInterfaces.FirstOrDefault(adapter => adapter.DisplayName == s); if (foundAdapter != null) LocalIpAddress.SetTypedInputValue(foundAdapter.IpAddress.ToString()); } public void Dispose() { CloseSocket(); } public IStatusProvider.StatusLevel GetStatusLevel() { return _lastStatusLevel; } public string? GetStatusMessage() { return _lastErrorMessage; } private void Update(EvaluationContext context) { _printToLog = PrintToLog.GetValue(context); // Update printToLog flag var localIp = LocalIpAddress.GetValue(context); var shouldConnect = Connect.GetValue(context); var settingsChanged = localIp != _lastLocalIp || shouldConnect != _lastConnectState; if (settingsChanged) { CloseSocket(); if (shouldConnect) OpenSocket(localIp); _lastLocalIp = localIp; _lastConnectState = shouldConnect; } var isConnected = _udpClient != null; IsConnected.Value = isConnected; var separator = Separator.GetValue(context) ?? ""; var messageParts = MessageParts.GetCollectedTypedInputs().Select(p => p.GetValue(context)); var currentMessage = string.Join(separator, messageParts); var hasMessageChanged = currentMessage != _lastSentMessage; var manualTrigger = SendTrigger.GetValue(context); var sendOnChange = SendOnChange.GetValue(context); var shouldSend = manualTrigger || (sendOnChange && hasMessageChanged); if (isConnected && shouldSend) { if (manualTrigger) SendTrigger.SetTypedInputValue(false); var targetIp = TargetIpAddress.GetValue(context); var targetPort = TargetPort.GetValue(context); if (!string.IsNullOrEmpty(currentMessage) && !string.IsNullOrEmpty(targetIp) && targetPort > 0) { var data = Encoding.UTF8.GetBytes(currentMessage); try { _udpClient!.Send(data, data.Length, targetIp, targetPort); if (_printToLog) { Log.Debug($"UDP Output → '{currentMessage}' to {targetIp}:{targetPort}", this); } } catch (Exception e) { SetStatus($"UDP send error: {e.Message}", IStatusProvider.StatusLevel.Warning); if (_printToLog) { Log.Warning($"UDP Output: Send error to {targetIp}:{targetPort}: {e.Message}", this); } } _lastSentMessage = currentMessage; } } } private void OpenSocket(string? localIpAddress) { if (string.IsNullOrEmpty(localIpAddress)) { SetStatus("Local IP not selected.", IStatusProvider.StatusLevel.Warning); return; } if (!IPAddress.TryParse(localIpAddress, out var ip)) { SetStatus("Invalid Local IP.", IStatusProvider.StatusLevel.Warning); return; } try { var localEndPoint = new IPEndPoint(ip, 0); // Bind to a dynamic port for sending _udpClient = new UdpClient(localEndPoint); SetStatus($"Socket ready on {localEndPoint}", IStatusProvider.StatusLevel.Success); if (_printToLog) { Log.Debug($"UDP Output: Socket opened on {localEndPoint}", this); } } catch (Exception e) { SetStatus($"Failed to open socket: {e.Message}", IStatusProvider.StatusLevel.Error); if (_printToLog) { Log.Error($"UDP Output: Failed to open socket on {localIpAddress}: {e.Message}", this); } _udpClient?.Dispose(); _udpClient = null; } } private void CloseSocket() { _udpClient?.Close(); _udpClient = null; if (_lastConnectState) { SetStatus("Disconnected", IStatusProvider.StatusLevel.Notice); if (_printToLog) { Log.Debug("UDP Output: Socket closed.", this); } } } public void SetStatus(string m, IStatusProvider.StatusLevel l) { _lastErrorMessage = m; _lastStatusLevel = l; } #region Network Interface Logic private static List _networkInterfaces = new(); private static List GetNetworkInterfaces() { var list = new List(); list.Add(new NetworkAdapterInfo(IPAddress.Any, IPAddress.Any, "Any")); list.Add(new NetworkAdapterInfo(IPAddress.Loopback, IPAddress.Parse("255.0.0.0"), "Localhost")); try { list.AddRange(from ni in NetworkInterface.GetAllNetworkInterfaces() where ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback from ip in ni.GetIPProperties().UnicastAddresses where ip.Address.AddressFamily == AddressFamily.InterNetwork select new NetworkAdapterInfo(ip.Address, ip.IPv4Mask, ni.Name)); } catch (Exception e) { Log.Warning("Could not enumerate network interfaces: " + e.Message); } return list; } private sealed record NetworkAdapterInfo(IPAddress IpAddress, IPAddress SubnetMask, string Name) { public string DisplayName => $"{Name}: {IpAddress}"; } #endregion }