Files
2026-07-13 13:13:17 +08:00

638 lines
28 KiB
C#

#nullable enable
using System.Diagnostics;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using System.Threading;
using T3.Core.Utils;
// ReSharper disable MemberCanBePrivate.Global
namespace Lib.io.dmx;
[Guid("e5a8d9e6-3c5a-4bbb-9da3-737b6330b9c3")]
internal sealed class SacnOutput : Instance<SacnOutput>, IStatusProvider, ICustomDropdownHolder, IDisposable
{
private const int SacnPort = 5568;
private const string SacnDiscoveryIp = "239.255.250.214";
[Output(Guid = "a3c4a2e8-bc1b-453a-9773-1952a6ea10a3")]
public readonly Slot<Command> Result = new();
// --- State and Configuration ---
private readonly ConnectionSettings _connectionSettings = new();
private readonly object _dataLock = new();
private readonly object _connectionLock = new();
private bool _wasSendingLastFrame;
private string? _lastErrorMessage;
private IStatusProvider.StatusLevel _lastStatusLevel = IStatusProvider.StatusLevel.Notice;
private readonly byte[] _cid = Guid.NewGuid().ToByteArray();
private double _lastNetworkRefreshTime;
private double _lastRetryTime;
// --- High-Performance Sending Resources ---
private Thread? _senderThread;
private CancellationTokenSource? _senderCts;
private SacnPacketOptions _packetOptions;
private readonly byte[] _packetBuffer = new byte[126 + 512];
private readonly byte[] _universeSequenceNumbers = new byte[65536];
// --- MULTI-THREADING OPTIMIZATION: Zero-Allocation Snapshot Architecture ---
private readonly ConcurrentQueue<FrameData> _dataQueue = new();
private readonly ConcurrentBag<FrameData> _frameDataPool = new();
private struct InputMapping
{
public int Offset;
public int Count;
public int StartUniverse;
}
private sealed class FrameData
{
public int[] Buffer = Array.Empty<int>();
public InputMapping[] Mappings = Array.Empty<InputMapping>();
public int MappingCount;
}
private FrameData RentFrameData(int requiredChannels, int requiredMappings)
{
if (_frameDataPool.TryTake(out var fd))
{
if (fd.Buffer.Length < requiredChannels) fd.Buffer = new int[requiredChannels + 1024];
if (fd.Mappings.Length < requiredMappings) fd.Mappings = new InputMapping[requiredMappings + 4];
return fd;
}
return new FrameData
{
Buffer = new int[requiredChannels + 1024],
Mappings = new InputMapping[requiredMappings + 4]
};
}
// --- Discovery & Network ---
private Thread? _discoveryListenerThread;
private volatile bool _isDiscovering;
private UdpClient? _discoveryUdpClient;
private readonly ConcurrentDictionary<string, string> _discoveredSources = new();
private Socket? _socket;
private volatile bool _connected;
public SacnOutput()
{
Result.UpdateAction = Update;
}
private void Update(EvaluationContext context)
{
var localIpString = LocalIpAddress.GetValue(context);
if (string.IsNullOrEmpty(localIpString) && context.LocalTime - _lastNetworkRefreshTime > 5.0)
{
_lastNetworkRefreshTime = context.LocalTime;
_networkInterfaces = GetNetworkInterfaces();
}
var settingsChanged = _connectionSettings.Update(
localIpString,
TargetIpAddress.GetValue(context),
SendUnicast.GetValue(context)
);
bool needsReconnect = Reconnect.GetValue(context) || settingsChanged;
bool shouldAutoRevive = !_connected && _connectionSettings.LocalIp != null && (_lastRetryTime == 0 || context.LocalTime - _lastRetryTime > 2.0);
if (needsReconnect || shouldAutoRevive)
{
Reconnect.SetTypedInputValue(false);
bool needsThreadRestart = _wasSendingLastFrame && _senderThread != null;
if (needsThreadRestart) StopSenderThread();
lock (_connectionLock)
{
CloseSocket();
_connected = TryConnectSacn(_connectionSettings.LocalIp);
if (shouldAutoRevive) _lastRetryTime = context.LocalTime;
}
if (needsThreadRestart) StartSenderThread();
}
var discoverSources = DiscoverSources.GetValue(context);
if (discoverSources && !_isDiscovering) StartSacnDiscovery();
else if (!discoverSources && _isDiscovering) StopSacnDiscovery();
var enableSending = SendTrigger.GetValue(context);
if (enableSending != _wasSendingLastFrame)
{
if (enableSending) StartSenderThread();
else StopSenderThread();
_wasSendingLastFrame = enableSending;
}
if (!enableSending)
{
SetStatus("Sending is disabled. Enable 'Send Trigger'.", IStatusProvider.StatusLevel.Notice);
return;
}
lock (_connectionLock)
{
if (!_connected)
{
SetStatus($"Not connected. {(_lastErrorMessage ?? "Check settings.")}", IStatusProvider.StatusLevel.Warning);
return;
}
}
SetStatus("Connected and sending.", IStatusProvider.StatusLevel.Success);
// --- Calculate Size and Prepare Offload ---
var inputValueLists = InputsValues.GetCollectedTypedInputs();
int totalChannels = 0;
// Fast pre-pass to determine memory size
for (int i = 0; i < inputValueLists.Count; i++)
{
var buf = inputValueLists[i].GetValue(context);
if (buf != null) totalChannels += buf.Count;
}
if (totalChannels == 0) return;
var universeChannels = UniverseChannels.GetValue(context);
if (universeChannels == null || universeChannels.Count < inputValueLists.Count)
{
if (universeChannels == null) universeChannels = new List<int>();
int nextUniverse = universeChannels.Count > 0 ? universeChannels[^1] + 1 : 1;
while (universeChannels.Count < inputValueLists.Count)
{
universeChannels.Add(nextUniverse);
nextUniverse++;
}
UniverseChannels.SetTypedInputValue(universeChannels);
}
// Rent pooled object for background thread (No allocation!)
var frameData = RentFrameData(totalChannels, inputValueLists.Count);
frameData.MappingCount = 0;
int currentOffset = 0;
// Perform hyper-fast native memory copy.
// We do ZERO math on the main thread now.
for (int i = 0; i < inputValueLists.Count; i++)
{
var buf = inputValueLists[i].GetValue(context);
if (buf == null || buf.Count == 0) continue;
buf.CopyTo(frameData.Buffer, currentOffset);
frameData.Mappings[frameData.MappingCount++] = new InputMapping
{
Offset = currentOffset,
Count = buf.Count,
StartUniverse = universeChannels[i]
};
currentOffset += buf.Count;
}
_packetOptions.MaxFps = MaxFps.GetValue(context);
_packetOptions.Priority = (byte)Priority.GetValue(context).Clamp(0, 200);
_packetOptions.SourceName = SourceName.GetValue(context) ?? string.Empty;
_packetOptions.EnableSync = EnableSync.GetValue(context);
_packetOptions.SyncUniverse = (ushort)SyncUniverse.GetValue(context).Clamp(1, 63999);
_dataQueue.Enqueue(frameData);
// Keep queue extremely tight. 1 active, 1 waiting max.
while (_dataQueue.Count > 2)
{
if (_dataQueue.TryDequeue(out var oldData))
{
_frameDataPool.Add(oldData);
}
}
}
#region Sender Thread Management and Loop
private void StartSenderThread()
{
if (_senderThread != null) return;
_senderCts = new CancellationTokenSource();
_senderThread = new Thread(() => SenderLoop(_senderCts.Token))
{
IsBackground = true, Name = "sACNSender", Priority = ThreadPriority.AboveNormal
};
_senderThread.Start();
}
private void StopSenderThread()
{
if (_senderThread == null) return;
_senderCts?.Cancel();
if (_senderThread.Join(500)) _senderCts?.Dispose();
_senderCts = null;
_senderThread = null;
}
private void SenderLoop(CancellationToken token)
{
var stopwatch = new Stopwatch();
long nextFrameTimeTicks = 0;
byte syncSequenceNumber = 0;
int consecutiveErrors = 0;
const int MAX_CONSECUTIVE_ERRORS = 10;
while (!token.IsCancellationRequested)
{
try
{
if (!_dataQueue.TryDequeue(out var frameData) || frameData == null)
{
Thread.Sleep(1);
continue;
}
SacnPacketOptions optionsCopy;
bool isConnected;
lock (_dataLock) optionsCopy = _packetOptions;
lock (_connectionLock) isConnected = _connected;
if (optionsCopy.MaxFps > 0)
{
if (!stopwatch.IsRunning) stopwatch.Start();
while (true)
{
long now = stopwatch.ElapsedTicks;
if (now >= nextFrameTimeTicks)
{
if (now > nextFrameTimeTicks + Stopwatch.Frequency) nextFrameTimeTicks = now;
nextFrameTimeTicks += (long)(Stopwatch.Frequency / (double)optionsCopy.MaxFps);
break;
}
if (nextFrameTimeTicks - now > Stopwatch.Frequency / 1000) Thread.Sleep(1);
else Thread.SpinWait(10);
}
}
Socket? currentSocket;
bool useUnicast;
IPEndPoint? cachedTargetEndPoint;
lock (_connectionSettings)
{
currentSocket = _socket;
if (currentSocket == null || !isConnected)
{
_frameDataPool.Add(frameData);
Thread.Sleep(10);
continue;
}
useUnicast = (_connectionSettings.SendUnicast && _connectionSettings.TargetIp != null)
|| _connectionSettings.TargetIp != null;
cachedTargetEndPoint = useUnicast ? new IPEndPoint(_connectionSettings.TargetIp!, SacnPort) : null;
}
// Background thread now does the math and packet building
for (int m = 0; m < frameData.MappingCount; m++)
{
if (token.IsCancellationRequested) break;
var mapping = frameData.Mappings[m];
int remainingChannels = mapping.Count;
int currentOffset = mapping.Offset;
int currentUniverse = mapping.StartUniverse;
while (remainingChannels > 0)
{
if (token.IsCancellationRequested) break;
int chunkCount = Math.Min(remainingChannels, 512);
byte seq = _universeSequenceNumbers[currentUniverse]++;
var packetLength = BuildSacnDataPacket(currentUniverse, optionsCopy, frameData.Buffer, currentOffset, chunkCount, seq);
var targetEndPoint = cachedTargetEndPoint ?? new IPEndPoint(GetSacnMulticastAddress(currentUniverse), SacnPort);
bool success = SendSacnPacket(currentSocket, targetEndPoint, _packetBuffer, packetLength, currentUniverse);
if (success) consecutiveErrors = 0;
else
{
consecutiveErrors++;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS)
{
lock (_connectionLock) _connected = false;
consecutiveErrors = 0;
}
}
currentOffset += chunkCount;
remainingChannels -= chunkCount;
currentUniverse++;
}
}
// Return native block memory to pool
_frameDataPool.Add(frameData);
if (optionsCopy.EnableSync) SendSacnSync(currentSocket, optionsCopy.SyncUniverse, syncSequenceNumber++);
}
catch (ThreadAbortException) { break; }
catch (Exception)
{
consecutiveErrors++;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { lock (_connectionLock) _connected = false; consecutiveErrors = 0; }
Thread.Sleep(10);
}
}
}
#endregion
#region Packet Sending
private bool SendSacnPacket(Socket socket, IPEndPoint target, byte[] packetBuffer, int packetLength, int universe)
{
try { socket.SendTo(packetBuffer, packetLength, SocketFlags.None, target); return true; }
catch (SocketException e) { return e.SocketErrorCode is SocketError.WouldBlock or SocketError.NoBufferSpaceAvailable or SocketError.ConnectionReset; }
catch (ObjectDisposedException) { return false; }
}
private void SendSacnSync(Socket socket, ushort syncAddress, byte sequenceNumber)
{
try
{
var packetLength = BuildSacnSyncPacket(syncAddress, sequenceNumber);
var useUnicast = _connectionSettings.TargetIp != null;
var syncEndPoint = useUnicast ? new IPEndPoint(_connectionSettings.TargetIp!, SacnPort) : new IPEndPoint(GetSacnMulticastAddress(syncAddress), SacnPort);
socket.SendTo(_packetBuffer, packetLength, SocketFlags.None, syncEndPoint);
}
catch (SocketException e) { if (e.SocketErrorCode is not (SocketError.WouldBlock or SocketError.NoBufferSpaceAvailable or SocketError.ConnectionReset)) lock (_connectionLock) _connected = false; }
catch (ObjectDisposedException) { lock (_connectionLock) _connected = false; }
}
private int BuildSacnSyncPacket(ushort syncUniverse, byte sequenceNumber)
{
_packetBuffer[0] = 0x00; _packetBuffer[1] = 0x10; _packetBuffer[2] = 0x00; _packetBuffer[3] = 0x00;
Encoding.ASCII.GetBytes("ASC-E1.17", 0, 9, _packetBuffer, 4);
_packetBuffer[13] = 0x00; _packetBuffer[14] = 0x00; _packetBuffer[15] = 0x00;
short rootFlagsAndLength = IPAddress.HostToNetworkOrder((short)(0x7000 | 31));
Array.Copy(BitConverter.GetBytes(rootFlagsAndLength), 0, _packetBuffer, 16, 2);
int vector = IPAddress.HostToNetworkOrder(0x00000004);
Array.Copy(BitConverter.GetBytes(vector), 0, _packetBuffer, 18, 4);
Array.Copy(_cid, 0, _packetBuffer, 22, 16);
short frameFlagsAndLength = IPAddress.HostToNetworkOrder((short)(0x7000 | 9));
Array.Copy(BitConverter.GetBytes(frameFlagsAndLength), 0, _packetBuffer, 38, 2);
int frameVector = IPAddress.HostToNetworkOrder(0x00000001);
Array.Copy(BitConverter.GetBytes(frameVector), 0, _packetBuffer, 40, 4);
_packetBuffer[44] = sequenceNumber;
short syncUni = IPAddress.HostToNetworkOrder((short)syncUniverse);
Array.Copy(BitConverter.GetBytes(syncUni), 0, _packetBuffer, 45, 2);
_packetBuffer[47] = 0x00; _packetBuffer[48] = 0x00;
return 49;
}
private int BuildSacnDataPacket(int universe, SacnPacketOptions options, int[] rawData, int offset, int chunkCount, byte sequenceNumber)
{
var dmxLength = (short)chunkCount;
_packetBuffer[0] = 0x00; _packetBuffer[1] = 0x10; _packetBuffer[2] = 0x00; _packetBuffer[3] = 0x00;
Encoding.ASCII.GetBytes("ASC-E1.17", 0, 9, _packetBuffer, 4);
_packetBuffer[13] = 0x00; _packetBuffer[14] = 0x00; _packetBuffer[15] = 0x00;
short rootFlagsAndLength = IPAddress.HostToNetworkOrder((short)(0x7000 | (108 + dmxLength)));
Array.Copy(BitConverter.GetBytes(rootFlagsAndLength), 0, _packetBuffer, 16, 2);
int vector = IPAddress.HostToNetworkOrder(0x00000004);
Array.Copy(BitConverter.GetBytes(vector), 0, _packetBuffer, 18, 4);
Array.Copy(_cid, 0, _packetBuffer, 22, 16);
short frameFlagsAndLength = IPAddress.HostToNetworkOrder((short)(0x7000 | (86 + dmxLength)));
Array.Copy(BitConverter.GetBytes(frameFlagsAndLength), 0, _packetBuffer, 38, 2);
int frameVector = IPAddress.HostToNetworkOrder(0x00000002);
Array.Copy(BitConverter.GetBytes(frameVector), 0, _packetBuffer, 40, 4);
Array.Clear(_packetBuffer, 44, 64);
if (!string.IsNullOrEmpty(options.SourceName))
{
var sourceBytes = Encoding.UTF8.GetBytes(options.SourceName);
int copyCount = Math.Min(sourceBytes.Length, 63);
Array.Copy(sourceBytes, 0, _packetBuffer, 44, copyCount);
}
_packetBuffer[108] = options.Priority;
short syncUni = IPAddress.HostToNetworkOrder((short)(options.EnableSync ? options.SyncUniverse : 0));
Array.Copy(BitConverter.GetBytes(syncUni), 0, _packetBuffer, 109, 2);
_packetBuffer[111] = sequenceNumber;
_packetBuffer[112] = 0x00;
short netUniverse = IPAddress.HostToNetworkOrder((short)universe);
Array.Copy(BitConverter.GetBytes(netUniverse), 0, _packetBuffer, 113, 2);
short dmpFlagsAndLength = IPAddress.HostToNetworkOrder((short)(0x7000 | (9 + dmxLength)));
Array.Copy(BitConverter.GetBytes(dmpFlagsAndLength), 0, _packetBuffer, 115, 2);
_packetBuffer[117] = 0x02; _packetBuffer[118] = (byte)0xa1;
_packetBuffer[119] = 0x00; _packetBuffer[120] = 0x00;
_packetBuffer[121] = 0x00; _packetBuffer[122] = 0x01;
short propValueCount = IPAddress.HostToNetworkOrder((short)(dmxLength + 1));
Array.Copy(BitConverter.GetBytes(propValueCount), 0, _packetBuffer, 123, 2);
_packetBuffer[125] = 0x00;
// CRITICAL PERFORMANCE LOOP: Execute Math directly into the sending buffer
for (int i = 0; i < chunkCount; i++)
{
int val = rawData[offset + i];
_packetBuffer[126 + i] = (byte)(val < 0 ? 0 : (val > 255 ? 255 : val));
}
return 126 + dmxLength;
}
#endregion
#region Discovery & Boilerplate Helpers
private void StartSacnDiscovery()
{
_isDiscovering = true;
_discoveredSources.Clear();
_discoveryListenerThread = new Thread(ListenForSacnDiscovery) { IsBackground = true, Name = "sACNDiscoveryListener" };
_discoveryListenerThread.Start();
}
private void StopSacnDiscovery()
{
if (!_isDiscovering) return;
_isDiscovering = false;
_discoveryUdpClient?.Close();
_discoveryListenerThread?.Join(200);
_discoveryListenerThread = null;
}
private void ListenForSacnDiscovery()
{
try
{
_discoveryUdpClient = new UdpClient();
var localEp = new IPEndPoint(IPAddress.Any, SacnPort);
_discoveryUdpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_discoveryUdpClient.Client.Bind(localEp);
_discoveryUdpClient.JoinMulticastGroup(IPAddress.Parse(SacnDiscoveryIp));
while (_isDiscovering)
{
try
{
var remoteEp = new IPEndPoint(IPAddress.Any, 0);
var data = _discoveryUdpClient.Receive(ref remoteEp);
if (data.Length <= 125) continue;
var sourceName = Encoding.UTF8.GetString(data, 44, 64).TrimEnd('\0');
var ipString = remoteEp.Address.ToString();
var displayName = string.IsNullOrWhiteSpace(sourceName) ? ipString : sourceName;
_discoveredSources[ipString] = $"{displayName} ({ipString})";
}
catch (SocketException) { if (_isDiscovering) break; }
catch (Exception e) { if (_isDiscovering) Log.Error($"sACN discovery listener error: {e.Message}", this); }
}
}
catch (Exception e) { if (_isDiscovering) Log.Error($"sACN discovery listener failed to bind: {e.Message}", this); }
finally { _discoveryUdpClient?.Close(); _discoveryUdpClient = null; }
}
public void Dispose() { StopSenderThread(); StopSacnDiscovery(); CloseSocket(); }
private void CloseSocket()
{
lock (_connectionLock)
{
lock (_connectionSettings)
{
if (_socket == null) return;
try { _socket.Close(); }
catch { /* Ignore */ }
finally { _socket = null; _connected = false; _lastErrorMessage = "Socket closed."; }
}
}
}
private bool TryConnectSacn(IPAddress? localIp)
{
lock (_connectionLock)
{
lock (_connectionSettings)
{
if (localIp == null) { _lastErrorMessage = "Local IP Address is not valid."; return false; }
try
{
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
_socket.SendBufferSize = 10 * 1024 * 1024;
_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastLoopback, true);
try { _socket.IOControl(-1744830452, new byte[] { 0 }, null); } catch { /* Ignore SIO_UDP_CONNRESET */ }
_socket.Bind(new IPEndPoint(localIp, 0));
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 1);
_lastErrorMessage = null;
return _connected = true;
}
catch (Exception e) { _lastErrorMessage = $"Failed to bind: {e.Message}"; CloseSocket(); return false; }
}
}
}
private static IPAddress GetSacnMulticastAddress(int universe)
{
var u = (ushort)universe.Clamp(1, 63999);
return new IPAddress(new byte[] { 239, 255, (byte)(u >> 8), (byte)(u & 0xFF) });
}
private static List<NetworkAdapterInfo> _networkInterfaces = GetNetworkInterfaces();
private static List<NetworkAdapterInfo> GetNetworkInterfaces()
{
var list = new List<NetworkAdapterInfo> { new(IPAddress.Loopback, IPAddress.Parse("255.0.0.0"), "Localhost (127.0.0.1)") };
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 { /* Ignore */ }
return list;
}
private sealed record NetworkAdapterInfo(IPAddress IpAddress, IPAddress SubnetMask, string Name) { public string DisplayName => $"{Name}: {IpAddress}"; }
private struct SacnPacketOptions { public int MaxFps; public byte Priority; public string SourceName; public bool EnableSync; public ushort SyncUniverse; }
private sealed class ConnectionSettings
{
public IPAddress? LocalIp { get; private set; }
public IPAddress? TargetIp { get; private set; }
public bool SendUnicast { get; private set; }
private string? _lastLocalIpStr, _lastTargetIpStr;
private bool _lastSendUnicast;
public bool Update(string? localIpStr, string? targetIpStr, bool sendUnicast)
{
if (_lastLocalIpStr == localIpStr && _lastTargetIpStr == targetIpStr && _lastSendUnicast == sendUnicast) return false;
_lastLocalIpStr = localIpStr; _lastTargetIpStr = targetIpStr; _lastSendUnicast = sendUnicast; SendUnicast = sendUnicast;
IPAddress.TryParse(localIpStr, out var parsedLocalIp); LocalIp = parsedLocalIp;
IPAddress? targetIp = null;
if (LocalIp != null && LocalIp.Equals(IPAddress.Loopback)) targetIp = IPAddress.Loopback;
else if (sendUnicast) IPAddress.TryParse(targetIpStr, out targetIp);
TargetIp = targetIp;
return true;
}
}
public IStatusProvider.StatusLevel GetStatusLevel() => _lastStatusLevel;
public string? GetStatusMessage() => _lastErrorMessage;
public void SetStatus(string m, IStatusProvider.StatusLevel l) { _lastErrorMessage = m; _lastStatusLevel = l; }
string ICustomDropdownHolder.GetValueForInput(Guid inputId)
{
if (inputId == LocalIpAddress.Id) return LocalIpAddress.Value ?? string.Empty;
if (inputId == TargetIpAddress.Id) return TargetIpAddress.Value ?? string.Empty;
return string.Empty;
}
IEnumerable<string> ICustomDropdownHolder.GetOptionsForInput(Guid inputId)
{
if (inputId == LocalIpAddress.Id)
{
_networkInterfaces = GetNetworkInterfaces();
foreach (var adapter in _networkInterfaces) yield return adapter.DisplayName;
}
else if (inputId == TargetIpAddress.Id)
{
if (!_isDiscovering && _discoveredSources.IsEmpty) yield return "Enable 'Discover Sources' to search...";
else if (_isDiscovering && _discoveredSources.IsEmpty) yield return "Searching for sources...";
else foreach (var sourceName in _discoveredSources.Values.OrderBy(name => name)) yield return sourceName;
}
}
void ICustomDropdownHolder.HandleResultForInput(Guid inputId, string? selected, bool isAListItem)
{
if (string.IsNullOrEmpty(selected) || !isAListItem) return;
if (inputId == LocalIpAddress.Id)
{
var foundAdapter = _networkInterfaces.FirstOrDefault(i => i.DisplayName == selected);
if (foundAdapter == null) return;
LocalIpAddress.SetTypedInputValue(foundAdapter.IpAddress.ToString());
}
else if (inputId == TargetIpAddress.Id)
{
var match = Regex.Match(selected, @"\(([^)]*)\)");
TargetIpAddress.SetTypedInputValue(match.Success ? match.Groups[1].Value : selected);
}
}
#endregion
#region Inputs
[Input(Guid = "2a8d39a3-5a41-477d-815a-8b8b9d8b1e4a")] public readonly MultiInputSlot<List<int>> InputsValues = new();
[Input(Guid = "B2C3D4E5-F6A7-8901-BCDE-F234567890AB")] public readonly InputSlot<List<int>> UniverseChannels = new();
[Input(Guid = "f8a7e0c8-c6c7-4b53-9a3a-3e5f2a4f4e1c")] public readonly InputSlot<string> LocalIpAddress = new();
[Input(Guid = "9c233633-959f-4447-b248-4d431c1b18e7")] public readonly InputSlot<bool> SendTrigger = new();
[Input(Guid = "c2a9e3e3-a4e9-430b-9c6a-4e1a1e0b8e2e")] public readonly InputSlot<bool> Reconnect = new();
[Input(Guid = "8c6c9a8d-29c5-489e-8c6b-9e4a3c1e2b6a")] public readonly InputSlot<bool> SendUnicast = new();
[Input(Guid = "d9e8d7c6-b5a4-434a-9e3a-4e2b1d0c9a7b")] public readonly InputSlot<string> TargetIpAddress = new();
[Input(Guid = "3f25c04c-0a88-42fb-93d3-05992b861e61")] public readonly InputSlot<bool> DiscoverSources = new();
[Input(Guid = "4a9e2d3b-8c6f-4b1d-8d7e-9f3a5b2c1d0e")] public readonly InputSlot<int> Priority = new(100);
[Input(Guid = "5b1d9c8a-7e3f-4a2b-9c8d-1e0f3a5b2c1d")] public readonly InputSlot<string> SourceName = new("T3 sACN Output");
[Input(Guid = "6f5c4b3a-2e1d-4f9c-8a7b-3d2e1f0c9b8a")] public readonly InputSlot<int> MaxFps = new(60);
[Input(Guid = "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d")] public readonly InputSlot<bool> EnableSync = new();
[Input(Guid = "8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e")] public readonly InputSlot<int> SyncUniverse = new(1);
#endregion
}