diff --git a/NEOSPlus.sln b/NEOSPlus.sln index c2c5d00..8e8a3ed 100644 --- a/NEOSPlus.sln +++ b/NEOSPlus.sln @@ -3,34 +3,34 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32414.318 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NEOSPlus", "NEOSPlus\NEOSPlus.csproj", "{08A94620-ECB7-41FD-8C9C-C11F2EBFC776}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NEOSPlus", "NEOSPlus\NEOSPlus.csproj", "{08A94620-ECB7-41FD-8C9C-C11F2EBFC776}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGenerators", "SourceGenerators\SourceGenerators.csproj", "{88053493-5CC5-41EA-B598-816373CF48FE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + AutoPostX|Any CPU = AutoPostX|Any CPU CopyToPlugin|Any CPU = CopyToPlugin|Any CPU Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU - AutoPostX|Any CPU = AutoPostX|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.CopyToPlugin|Any CPU.ActiveCfg = CopyToPlugin|Any CPU - {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.CopyToPlugin|Any CPU.Build.0 = CopyToPlugin|Any CPU + {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.AutoPostX|Any CPU.ActiveCfg = AutoPostX|Any CPU + {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.AutoPostX|Any CPU.Build.0 = AutoPostX|Any CPU + {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.CopyToPlugin|Any CPU.ActiveCfg = Debug|Any CPU + {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.CopyToPlugin|Any CPU.Build.0 = Debug|Any CPU {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.Debug|Any CPU.Build.0 = Debug|Any CPU {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.Release|Any CPU.ActiveCfg = Release|Any CPU {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.Release|Any CPU.Build.0 = Release|Any CPU - {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.AutoPostX|Any CPU.ActiveCfg = AutoPostX|Any CPU - {08A94620-ECB7-41FD-8C9C-C11F2EBFC776}.AutoPostX|Any CPU.Build.0 = AutoPostX|Any CPU + {88053493-5CC5-41EA-B598-816373CF48FE}.AutoPostX|Any CPU.ActiveCfg = AutoPostX|Any CPU + {88053493-5CC5-41EA-B598-816373CF48FE}.AutoPostX|Any CPU.Build.0 = AutoPostX|Any CPU {88053493-5CC5-41EA-B598-816373CF48FE}.CopyToPlugin|Any CPU.ActiveCfg = CopyToPlugin|Any CPU {88053493-5CC5-41EA-B598-816373CF48FE}.CopyToPlugin|Any CPU.Build.0 = CopyToPlugin|Any CPU {88053493-5CC5-41EA-B598-816373CF48FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {88053493-5CC5-41EA-B598-816373CF48FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {88053493-5CC5-41EA-B598-816373CF48FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {88053493-5CC5-41EA-B598-816373CF48FE}.Release|Any CPU.Build.0 = Release|Any CPU - {88053493-5CC5-41EA-B598-816373CF48FE}.AutoPostX|Any CPU.ActiveCfg = AutoPostX|Any CPU - {88053493-5CC5-41EA-B598-816373CF48FE}.AutoPostX|Any CPU.Build.0 = AutoPostX|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NEOSPlus/Components/Network/ArtNetReceiver.cs b/NEOSPlus/Components/Network/ArtNetReceiver.cs new file mode 100644 index 0000000..57e2051 --- /dev/null +++ b/NEOSPlus/Components/Network/ArtNetReceiver.cs @@ -0,0 +1,104 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using BaseX; +using FrooxEngine; + +[Category("Network")] +public class ArtNetReceiver : Component +{ + public readonly Sync URL; + public readonly UserRef HandlingUser; + public readonly Sync AccessReason; + public readonly Sync ConnectRetryInterval; + public readonly Sync IsConnected; + + private Uri _currentURL; + private UdpClient _udpClient; + + public event Action Connected; + public event Action Closed; + public event Action Error; + public event Action PacketReceived; + + protected override void OnAwake() + { + base.OnAwake(); + ConnectRetryInterval.Value = 10f; + } + + protected override void OnChanges() + { + Uri uri = (Enabled ? URL.Value : null); + if (HandlingUser.Target != LocalUser) + { + uri = null; + } + if (uri != _currentURL) + { + _currentURL = uri; + CloseCurrent(); + IsConnected.Value = false; + if (_currentURL != null) + { + StartTask(async () => + { + await ConnectTo(_currentURL); + }); + } + } + } + + private async Task ConnectTo(Uri target) + { + if (target.Scheme != "artnet") + { + throw new ArgumentException("Invalid URL scheme. Expected 'artnet://'."); + } + + if (await Engine.Security.RequestAccessPermission(target.Host, target.Port, AccessReason.Value ?? "Art-Net Receiver") == HostAccessPermission.Allowed && target == _currentURL && !IsRemoved) + { + _udpClient = new UdpClient(target.Port); + IsConnected.Value = true; + Connected?.Invoke(this); + StartTask(ReceiveLoop); + } + } + + private async Task ReceiveLoop() + { + IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + while (IsConnected.Value) + { + UdpReceiveResult result = await _udpClient.ReceiveAsync(); + byte[] receivedData = result.Buffer; + + PacketReceived?.Invoke(this, receivedData); + } + } + + protected override void OnDispose() + { + CloseCurrent(); + base.OnDispose(); + } + + private void CloseCurrent() + { + if (_udpClient != null) + { + UdpClient udpClient = _udpClient; + _udpClient = null; + try + { + Closed?.Invoke(this); + } + catch (Exception ex) + { + UniLog.Error($"Exception in running Closed event on ArtNetReceiver:\n{ex}"); + } + udpClient.Close(); + } + } +} \ No newline at end of file diff --git a/NEOSPlus/Logix/Network/ART-NET/ArtNetChannelDataExtractor.cs b/NEOSPlus/Logix/Network/ART-NET/ArtNetChannelDataExtractor.cs new file mode 100644 index 0000000..bba5012 --- /dev/null +++ b/NEOSPlus/Logix/Network/ART-NET/ArtNetChannelDataExtractor.cs @@ -0,0 +1,33 @@ +using FrooxEngine; +using FrooxEngine.LogiX; + +[Category("LogiX/Network/ART-NET")] +public class ArtNetChannelDataExtractor : LogixNode +{ + public readonly Input Data; + public readonly Input Channel; + public readonly Input StartIndex; + public readonly Output Output; + + public readonly Impulse OnEvaluationComplete; + + [ImpulseTarget] + public void EvaluateData() + { + byte[] data = Data.Evaluate(); + int channel = Channel.Evaluate(); + int startIndex = StartIndex.Evaluate(); + + if (data != null && data.Length > startIndex + channel - 1) + { + Output.Value = data[startIndex + channel - 1]; + } + else + { + Output.Value = 0; + } + + // Triggering output impulse when data evaluation is complete + OnEvaluationComplete.Trigger(); + } +} diff --git a/NEOSPlus/Logix/Network/ART-NET/ArtNetReceiverBaseNode.cs b/NEOSPlus/Logix/Network/ART-NET/ArtNetReceiverBaseNode.cs new file mode 100644 index 0000000..72b46fd --- /dev/null +++ b/NEOSPlus/Logix/Network/ART-NET/ArtNetReceiverBaseNode.cs @@ -0,0 +1,43 @@ +using FrooxEngine; +using FrooxEngine.LogiX; + +public abstract class ArtNetReceiverBaseNode : LogixNode +{ + public readonly Input Receiver; + + private ArtNetReceiver _registered; + + protected override void OnChanges() + { + base.OnChanges(); + ArtNetReceiver artNetReceiver = Receiver.Evaluate(); + if (artNetReceiver != _registered) + { + Unregister(); + if (artNetReceiver != null) + { + Register(artNetReceiver); + } + _registered = artNetReceiver; + } + } + + protected abstract void Register(ArtNetReceiver receiver); + + protected abstract void Unregister(ArtNetReceiver receiver); + + private void Unregister() + { + if (_registered != null) + { + Unregister(_registered); + } + _registered = null; + } + + protected override void OnDispose() + { + Unregister(); + base.OnDispose(); + } +} \ No newline at end of file diff --git a/NEOSPlus/Logix/Network/ART-NET/ArtNetReceiverConnect.cs b/NEOSPlus/Logix/Network/ART-NET/ArtNetReceiverConnect.cs new file mode 100644 index 0000000..2c59381 --- /dev/null +++ b/NEOSPlus/Logix/Network/ART-NET/ArtNetReceiverConnect.cs @@ -0,0 +1,26 @@ +using System; +using FrooxEngine; +using FrooxEngine.LogiX; + +[Category(new string[] { "LogiX/Network/ART-NET" })] +public class ArtNetReceiverConnect : LogixNode +{ + public readonly Input Receiver; + public readonly Input URL; + public readonly Input HandlingUser; + public readonly Impulse OnConnectStart; + + [ImpulseTarget] + public void Connect() + { + ArtNetReceiver artNetReceiver = Receiver.Evaluate(); + if (artNetReceiver != null) + { + Uri value = URL.Evaluate(artNetReceiver.URL.Value); + User target = HandlingUser.Evaluate(base.LocalUser); + artNetReceiver.URL.Value = value; + artNetReceiver.HandlingUser.Target = target; + OnConnectStart.Trigger(); + } + } +} \ No newline at end of file diff --git a/NEOSPlus/Logix/Network/ART-NET/ArtNetUniverseDataReceiver.cs b/NEOSPlus/Logix/Network/ART-NET/ArtNetUniverseDataReceiver.cs new file mode 100644 index 0000000..0ed43eb --- /dev/null +++ b/NEOSPlus/Logix/Network/ART-NET/ArtNetUniverseDataReceiver.cs @@ -0,0 +1,104 @@ +using FrooxEngine; +using FrooxEngine.LogiX; +using System; +using BaseX; + +[Category(new string[] { "LogiX/Network/ART-NET" })] + +public class ArtNetUniverseDataReceiver : ArtNetReceiverBaseNode +{ + public readonly Input UniverseID; + public readonly Impulse Received; + public readonly Output Data; + + protected override void Register(ArtNetReceiver receiver) + { + receiver.PacketReceived += OnArtNetPacketReceived; + } + + protected override void Unregister(ArtNetReceiver receiver) + { + receiver.PacketReceived -= OnArtNetPacketReceived; + } + + private void OnArtNetPacketReceived(ArtNetReceiver receiver, byte[] data) + { + // UniLog.Log("Packet received. Data length: " + data.Length); + + if (IsValidArtNetPacket(data)) + { + // UniLog.Log("Data is a valid Art-Net packet"); + + int receivedUniverseID = ParseUniverseID(data); + // UniLog.Log("Parsed Universe ID: " + receivedUniverseID); + + if (receivedUniverseID == UniverseID.Evaluate()) + { + RunSynchronously(delegate + { + byte[] dmxData = ExtractDMXData(data); + Data.Value = dmxData; + Received.Trigger(); + }); + } + } + else if (IsValidDMXPacket(data)) + { + UniLog.Log("Data is a valid DMX packet"); + + RunSynchronously(delegate + { + byte[] dmxData = ExtractDMXData(data); + Data.Value = dmxData; + Received.Trigger(); + }); + } + else + { + UniLog.Log("Received data is not a valid Art-Net or DMX packet."); + } + } + + private bool IsValidArtNetPacket(byte[] data) + { + return data.Length >= 8 && System.Text.Encoding.ASCII.GetString(data, 0, 7) == "Art-Net"; + } + + private bool IsValidDMXPacket(byte[] data) + { + return data.Length >= 1 && data[0] == 0; + } + + private int ParseUniverseID(byte[] data) + { + // According to the Art-Net protocol, the Universe ID is stored as a 16-bit integer (2 bytes) with the Low Byte at offset 14 and High Byte at offset 15. + int universeIDOffsetLowByte = 14; + int universeIDOffsetHighByte = 15; + + int universeID = (data[universeIDOffsetHighByte] << 8) | data[universeIDOffsetLowByte]; + + // Additional logging details + byte subuni = data[14]; + string subnet = Convert.ToString(subuni >> 4); + string universe = Convert.ToString(subuni & 0x0F); + + //UniLog.Log("Subnet: " + subnet); + //UniLog.Log("Universe: " + universe); + + return universeID; + } + + private byte[] ExtractDMXData(byte[] data) + { + int dmxDataOffset = 18; + int dmxDataLength = 512; // fixed size of DMX data + + byte[] dmxData = new byte[dmxDataLength]; + Array.Copy(data, dmxDataOffset, dmxData, 0, dmxDataLength); + + string dmx = BitConverter.ToString(dmxData); + UniLog.Log("DMX Data: " + dmx); + + return dmxData; + } +} diff --git a/NEOSPlus/NEOSPlus.csproj b/NEOSPlus/NEOSPlus.csproj index b96e26b..90728a1 100644 --- a/NEOSPlus/NEOSPlus.csproj +++ b/NEOSPlus/NEOSPlus.csproj @@ -18,10 +18,11 @@ E:\SteamLibrary/steamapps/common/NeosVR/ - copy "$(TargetPath)" "$(NeosPath)\Libraries" + copy "$(TargetPath)" "$(NeosPath)\Libraries" - cd "$(ProjectDir)" + + cd "$(ProjectDir)" powershell -NoProfile -ExecutionPolicy Bypass ./Scripts/PostBuild.ps1 '$(NeosPath)' $(ConfigurationName)