Skip to content
This repository has been archived by the owner on Nov 9, 2023. It is now read-only.

Commit

Permalink
Art-Net Connection UDP
Browse files Browse the repository at this point in the history
New Logix:

LogiX/Network/ART-NET

ArtNetChannelDataExtractor
ArtNetReceiverConnect
ArtNetUniverseDataReceiver

New Component:

Network

ArtNetReceiver
  • Loading branch information
Xlinka authored Jul 26, 2023
2 parents 16322a8 + ce803b3 commit 727817b
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 10 deletions.
16 changes: 8 additions & 8 deletions NEOSPlus.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 104 additions & 0 deletions NEOSPlus/Components/Network/ArtNetReceiver.cs
Original file line number Diff line number Diff line change
@@ -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<Uri> URL;
public readonly UserRef HandlingUser;
public readonly Sync<string> AccessReason;
public readonly Sync<float> ConnectRetryInterval;
public readonly Sync<bool> IsConnected;

private Uri _currentURL;
private UdpClient _udpClient;

public event Action<ArtNetReceiver> Connected;
public event Action<ArtNetReceiver> Closed;
public event Action<ArtNetReceiver, string> Error;
public event Action<ArtNetReceiver, byte[]> 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();
}
}
}
33 changes: 33 additions & 0 deletions NEOSPlus/Logix/Network/ART-NET/ArtNetChannelDataExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using FrooxEngine;
using FrooxEngine.LogiX;

[Category("LogiX/Network/ART-NET")]
public class ArtNetChannelDataExtractor : LogixNode
{
public readonly Input<byte[]> Data;
public readonly Input<int> Channel;
public readonly Input<int> StartIndex;
public readonly Output<byte> 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();
}
}
43 changes: 43 additions & 0 deletions NEOSPlus/Logix/Network/ART-NET/ArtNetReceiverBaseNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using FrooxEngine;
using FrooxEngine.LogiX;

public abstract class ArtNetReceiverBaseNode : LogixNode
{
public readonly Input<ArtNetReceiver> 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();
}
}
26 changes: 26 additions & 0 deletions NEOSPlus/Logix/Network/ART-NET/ArtNetReceiverConnect.cs
Original file line number Diff line number Diff line change
@@ -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<ArtNetReceiver> Receiver;
public readonly Input<Uri> URL;
public readonly Input<User> 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();
}
}
}
104 changes: 104 additions & 0 deletions NEOSPlus/Logix/Network/ART-NET/ArtNetUniverseDataReceiver.cs
Original file line number Diff line number Diff line change
@@ -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<int> UniverseID;
public readonly Impulse Received;
public readonly Output<byte[]> 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;
}
}
5 changes: 3 additions & 2 deletions NEOSPlus/NEOSPlus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
<NeosPath Condition="Exists('E:\SteamLibrary/steamapps/common/NeosVR/')">E:\SteamLibrary/steamapps/common/NeosVR/</NeosPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CopyToPlugin|AnyCPU'">
<PostBuildEvent>copy "$(TargetPath)" "$(NeosPath)\Libraries"</PostBuildEvent>
<PostBuildEvent>copy "$(TargetPath)" "$(NeosPath)\Libraries"</PostBuildEvent>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'AutoPostX|AnyCPU' ">
<PostBuildEvent>cd "$(ProjectDir)"
<PostBuildEvent>
cd "$(ProjectDir)"
powershell -NoProfile -ExecutionPolicy Bypass ./Scripts/PostBuild.ps1 '$(NeosPath)' $(ConfigurationName)
</PostBuildEvent>
</PropertyGroup>
Expand Down

0 comments on commit 727817b

Please sign in to comment.