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

297 lines
10 KiB
C#

#nullable enable
using System.Net.NetworkInformation;
using System.Net.Sockets;
using T3.Core.Utils;
namespace Lib.io.freed;
[Guid("a9b8c7d6-e5f4-4210-9876-54321fedcba0")]
internal sealed class FreeDOutput : Instance<FreeDOutput>, IStatusProvider, ICustomDropdownHolder, IDisposable
{
public FreeDOutput()
{
Command.UpdateAction = Update;
_writer = new BinaryWriter(_packetStream, Encoding.ASCII, true);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Update(EvaluationContext context)
{
_printToLog = PrintToLog.GetValue(context);
var shouldConnect = Connect.GetValue(context);
var localIp = LocalIpAddress.GetValue(context) ?? string.Empty;
var targetIp = TargetIpAddress.GetValue(context) ?? string.Empty;
var targetPort = TargetPort.GetValue(context);
var settingsChanged = shouldConnect != _lastConnectState || localIp != _lastLocalIp || targetIp != _lastTargetIp || targetPort != _lastTargetPort;
if (settingsChanged)
{
CloseSocket();
if (shouldConnect) OpenSocket(localIp, targetIp, targetPort);
_lastConnectState = shouldConnect;
_lastLocalIp = localIp;
_lastTargetIp = targetIp;
_lastTargetPort = targetPort;
}
IsConnected.Value = _udpClient != null;
if (_udpClient == null) return;
if (MathUtils.WasTriggered(SendTrigger.GetValue(context), ref _sendTrigger) || SendOnChange.GetValue(context))
{
var cameraId = (byte)CameraId.GetValue(context).Clamp(0, 255);
var rotation = Rotation.GetValue(context);
var position = Position.GetValue(context);
// Clamp values to their valid FreeD ranges
var focus = Math.Clamp(Focus.GetValue(context), 0, 0xFFFFFF);
var zoom = Math.Clamp(Zoom.GetValue(context), 0, 0xFFFFFF);
var user = Math.Clamp(User.GetValue(context), 0, 0xFFFF);
var packetBytes = BuildFreeDPacket(cameraId, rotation, position, zoom, focus, user);
SendPacket(packetBytes, cameraId);
SendTrigger.SetTypedInputValue(false);
}
}
private void SendPacket(byte[] packetBytes, byte cameraId)
{
if (_udpClient == null || _targetEndPoint == null) return;
// Fire and forget
Task.Run(() =>
{
try
{
_udpClient.Send(packetBytes, packetBytes.Length, _targetEndPoint);
if (_printToLog) Log.Debug($"Sent FreeD packet for Camera ID {cameraId}.", this);
}
catch (Exception e)
{
SetStatus($"UDP send error: {e.Message}", IStatusProvider.StatusLevel.Warning);
}
});
SetStatus($"Sent FreeD packet for Camera ID {cameraId}.", IStatusProvider.StatusLevel.Success);
}
private byte[] BuildFreeDPacket(byte cameraId, Vector3 rotation, Vector3 position, int zoom, int focus, int user)
{
_packetStream.SetLength(0);
_writer.Write(FreeDIdentifier);
_writer.Write(cameraId);
WriteInt24BigEndian(_writer, (int)MathF.Round(rotation.X * AngleScale)); // Pan
WriteInt24BigEndian(_writer, (int)MathF.Round(rotation.Y * AngleScale)); // Tilt
WriteInt24BigEndian(_writer, (int)MathF.Round(rotation.Z * AngleScale)); // Roll
WriteInt24BigEndian(_writer, (int)MathF.Round(position.X * PositionToMeterScale)); // PosX
WriteInt24BigEndian(_writer, (int)MathF.Round(position.Y * PositionToMeterScale)); // PosY
WriteInt24BigEndian(_writer, (int)MathF.Round(position.Z * PositionToMeterScale)); // PosZ
WriteInt24BigEndian(_writer, zoom); // Zoom
WriteInt24BigEndian(_writer, focus); // Focus
_writer.Write((byte)((user >> 8) & 0xFF)); // Reserved / User (16-bit)
_writer.Write((byte)(user & 0xFF));
var packet = _packetStream.ToArray();
packet[packet.Length - 1] = CalculateFreeDChecksum(packet, packet.Length - 1);
return packet;
}
private static void WriteInt24BigEndian(BinaryWriter writer, int value)
{
writer.Write((byte)((value >> 16) & 0xFF));
writer.Write((byte)((value >> 8) & 0xFF));
writer.Write((byte)(value & 0xFF));
}
private static byte CalculateFreeDChecksum(byte[] data, int length)
{
var sum = data.Take(length).Aggregate(0u, (current, val) => current + val);
return (byte)(0x40u - sum);
}
private void OpenSocket(string localIpStr, string targetIpStr, int targetPort)
{
if (!IPAddress.TryParse(localIpStr, out var localIp))
{
SetStatus($"Invalid Local IP '{localIpStr}'.", IStatusProvider.StatusLevel.Error);
return;
}
if (string.IsNullOrWhiteSpace(targetIpStr))
targetIpStr = "255.255.255.255";
if (!IPAddress.TryParse(targetIpStr, out var targetIp))
{
SetStatus($"Invalid Target IP '{targetIpStr}'.", IStatusProvider.StatusLevel.Error);
return;
}
try
{
_udpClient = new UdpClient(new IPEndPoint(localIp, 0));
_targetEndPoint = new IPEndPoint(targetIp, targetPort);
if (_printToLog) Log.Debug($"FreeD Output: Socket bound to {localIp}, targeting {targetIp}:{targetPort}", this);
SetStatus($"Socket ready, sending to {targetIp}:{targetPort}", IStatusProvider.StatusLevel.Success);
}
catch (Exception e)
{
SetStatus($"Failed to open socket: {e.Message}", IStatusProvider.StatusLevel.Error);
}
}
private void CloseSocket()
{
_udpClient?.Dispose();
_udpClient = null;
if (_lastConnectState) SetStatus("Disconnected", IStatusProvider.StatusLevel.Notice);
}
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
CloseSocket();
_writer.Dispose();
_packetStream.Dispose();
}
_disposed = true;
}
#region Status and Dropdown
private string _statusMessage = "Not connected.";
private IStatusProvider.StatusLevel _statusLevel = IStatusProvider.StatusLevel.Notice;
private readonly object _statusLock = new();
public void SetStatus(string m, IStatusProvider.StatusLevel l)
{
lock (_statusLock)
{
_statusMessage = m;
_statusLevel = l;
}
}
public IStatusProvider.StatusLevel GetStatusLevel()
{
lock (_statusLock)
{
return _statusLevel;
}
}
public string GetStatusMessage()
{
lock (_statusLock)
{
return _statusMessage;
}
}
string ICustomDropdownHolder.GetValueForInput(Guid id)
{
return id == LocalIpAddress.Id ? LocalIpAddress.Value : string.Empty;
}
IEnumerable<string> ICustomDropdownHolder.GetOptionsForInput(Guid id)
{
if (id != LocalIpAddress.Id)
return Empty<string>();
if (_cachedIpAddresses == null)
_cachedIpAddresses = GetLocalIPv4Addresses().ToList();
return _cachedIpAddresses;
}
void ICustomDropdownHolder.HandleResultForInput(Guid id, string? s, bool i)
{
if (!string.IsNullOrEmpty(s) && i && id == LocalIpAddress.Id) LocalIpAddress.SetTypedInputValue(s.Split(' ')[0]);
}
private static IEnumerable<string> GetLocalIPv4Addresses()
{
if (!NetworkInterface.GetIsNetworkAvailable()) yield break;
foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
{
if (ni.OperationalStatus != OperationalStatus.Up) continue;
foreach (var ipInfo in ni.GetIPProperties().UnicastAddresses)
if (ipInfo.Address.AddressFamily == AddressFamily.InterNetwork)
yield return ipInfo.Address.ToString();
}
}
#endregion
#region Fields
private UdpClient? _udpClient;
private IPEndPoint? _targetEndPoint;
private bool _lastConnectState, _sendTrigger, _printToLog, _disposed;
private string _lastLocalIp = string.Empty, _lastTargetIp = string.Empty;
private int _lastTargetPort;
private readonly MemoryStream _packetStream = new(FreeDPacketLength);
private List<string>? _cachedIpAddresses;
private readonly BinaryWriter _writer;
private const int FreeDPacketLength = 29;
private const byte FreeDIdentifier = 0xD1;
private const float AngleScale = 32768.0f;
private const float PositionToMeterScale = 64000.0f;
#endregion
#region Inputs & Outputs
[Output(Guid = "b0a1c2d3-e4f5-4789-aabb-ccddffeeff00")]
public readonly Slot<Command> Command = new();
[Output(Guid = "b0a1c2d3-e4f5-4789-aabb-ccddffeeff01")]
public readonly Slot<bool> IsConnected = new();
[Input(Guid = "8f9a0b1c-2d3e-4f5a-a6b7-c8d9e0f1a2b3")]
public readonly InputSlot<bool> Connect = new();
[Input(Guid = "9a0b1c2d-3e4f-4a6b-b7c8-d9e0f1a2b3c4")]
public readonly InputSlot<string> LocalIpAddress = new("0.0.0.0");
[Input(Guid = "a0b1c2d3-e4f5-4a7b-c8d9-e0f1a2b3c4d5")]
public readonly InputSlot<string> TargetIpAddress = new("127.0.0.1");
[Input(Guid = "b1c2d3e4-f5a6-4b8c-d9e0-f1a2b3c4d5e6")]
public readonly InputSlot<int> TargetPort = new(6000);
[Input(Guid = "d3e4f5a6-b7c8-4d0e-f1a2-b3c4d5e6f7a8")]
public readonly InputSlot<Vector3> Rotation = new();
[Input(Guid = "a6b7c8d9-e0f1-4a3b-b4c5-d6e7f8a9b0c1")]
public readonly InputSlot<Vector3> Position = new();
[Input(Guid = "c2d3e4f5-a6b7-4c9d-e0f1-a2b3c4d5e6f7")]
public readonly InputSlot<int> CameraId = new();
[Input(Guid = "d9e0f1a2-b3c4-4d6e-8f90-123456789012")]
public readonly InputSlot<int> Focus = new();
[Input(Guid = "e0f1a2b3-c4d5-4e7f-9012-345678901234")]
public readonly InputSlot<int> Zoom = new();
[Input(Guid = "f1a2b3c4-d5e6-4f8a-9123-456789012345")]
public readonly InputSlot<int> User = new();
[Input(Guid = "f1a2b3c4-d5e6-4f8a-8f90-123456789014")]
public readonly InputSlot<bool> SendOnChange = new();
[Input(Guid = "a2b3c4d5-e6f7-4a9b-8f90-123456789015")]
public readonly InputSlot<bool> SendTrigger = new();
[Input(Guid = "b3c4d5e6-f7a8-4b0c-8f90-123456789016")]
public readonly InputSlot<bool> PrintToLog = new();
#endregion
}