Skip to content

Commit

Permalink
Moved world redirection visualization data to JsonLogging
Browse files Browse the repository at this point in the history
  • Loading branch information
Kiremai974 committed Jul 5, 2024
1 parent 4919238 commit 47ae5e6
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2357,6 +2357,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
logDirectoryPath: LoggedData\
optionalFilenamePrefix:
filename:
pythonPath:
--- !u!114 &844723486
MonoBehaviour:
m_ObjectHideFlags: 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
using VHToolkit.Redirection.BodyRedirection;
using VHToolkit.Redirection.WorldRedirection;
using Newtonsoft.Json;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Linq;
using UnityEditor;

namespace VHToolkit.Logging {

Expand Down Expand Up @@ -36,9 +42,31 @@ public record PhysicalLimbData {
public PhysicalLimbData(Limb l) => limb = l;
}

/// <summary>
/// Record type <c>WorldRedirectionData</c> is a wrapper around <c>WRData</c>.
/// It allows for serializing the redirection values : rotation over time, rotational and curvature
/// given by the WorldRedirection script
/// </summary>
public record WorldRedirectionData
{
public readonly float RotationOverTime;
public readonly float Rotational;
public readonly float Curvature;
public readonly float Time;

public WorldRedirectionData(float overTime, float rotational, float curvature, float time)
{
this.RotationOverTime = overTime;
this.Rotational = rotational;
this.Curvature = curvature;
this.Time = time;
}
}

public record JsonRedirectionData {
public readonly DateTime TimeStamp = DateTime.Now;
private readonly Interaction script;
private readonly DateTime startTime;

/// <value>
/// Property <c>Strategy</c> is a string corresponding to the name of the current World Redirection
Expand All @@ -62,43 +90,87 @@ public record JsonRedirectionData {

public bool Redirecting => script.redirect;

public WorldRedirectionData RedirectionData =>
new WorldRedirectionData((script.redirect && script.scene.enableHybridOverTime) ? Razzaque2001OverTimeRotation.GetRedirection(script.scene) : 0f,
(script.redirect && script.scene.enableHybridRotational) ? Razzaque2001Rotational.GetRedirection(script.scene) : 0f,
(script.redirect && script.scene.enableHybridCurvature) ? Razzaque2001Curvature.GetRedirection(script.scene) : 0f,
(float)(TimeStamp - startTime).TotalSeconds);

public List<PhysicalLimbData> Limbs => script.scene.limbs.ConvertAll(l => new PhysicalLimbData(l));
public TransformData PhysicalHead => script.scene.physicalHead ? new(script.scene.physicalHead) : null;
public List<TransformData> Targets => script.scene.targets.ConvertAll(t => new TransformData(t));
public TransformData PhysicalTarget => script.scene.physicalTarget ? new(script.scene.physicalTarget) : null;
public TransformData VirtualTarget => script.scene.virtualTarget ? new(script.scene.virtualTarget) : null;
public string StrategyDirection => script.scene.forwardTarget != null ? Convert.ToString(script.scene.forwardTarget.ToString()) : null;

public JsonRedirectionData(Interaction script) => this.script = script;
public JsonRedirectionData(Interaction script, DateTime startTime)
{
this.script = script;
this.startTime = startTime;
}
}

/// <summary>
/// Logs structured data in the JSON Lines format.
/// </summary>
public class JsonLogging : Logger<JsonRedirectionData> {
public class JsonLogging : Logger<JsonRedirectionData>
{
private Scene scene;
private DateTime startTime;

[SerializeField] private string filename;

[SerializeField] private string pythonPath;

private void Start() {
CreateNewFile(logDirectoryPath, optionalFilenamePrefix ?? "");
script = GetComponent<Interaction>();
}
scene = script.scene;

private void Update() {
records.Enqueue(new JsonRedirectionData(script));
WriteRecords(records);
}
/// <summary>
/// Create a new log file.
/// </summary>
/// <param name="logDirectoryPath">The path to the directory where the file should be placed.</param>
/// <param name="optionalFilenamePrefix">An optional prefix string to appear in the filename before its timestamp.</param>
public void CreateNewFile(string logDirectoryPath, string optionalFilenamePrefix = "") {
if (script is WorldRedirection)
CreateTcpClient(filename, pythonPath);

startTime = DateTime.Now;
}

private void Update()
{
records.Enqueue(new JsonRedirectionData(script, startTime));
WriteRecords(records);
}

private void OnDestroy()
{
while (observers.Count > 0)
{
var o = observers.FirstOrDefault();
o.OnCompleted();
}
}

/// <summary>
/// Create a new log file.
/// </summary>
/// <param name="logDirectoryPath">The path to the directory where the file should be placed.</param>
/// <param name="optionalFilenamePrefix">An optional prefix string to appear in the filename before its timestamp.</param>
public void CreateNewFile(string logDirectoryPath, string optionalFilenamePrefix = "") {
Directory.CreateDirectory(logDirectoryPath);
var fileName = $"{logDirectoryPath}{optionalFilenamePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.jsonl";
var observer = new JsonFileObserver(fileName);
observer.Subscribe(this);
observers.Add(observer);
}

/// <summary>
/// Create a new TCP client
/// </summary>
/// <param name="filename">The path to the python script that starts the server socket locally</param>
/// <param name="pythonPath">The path to the python executable file</param>
public void CreateTcpClient(string filename, string pythonPath)
{
var wrapper = new TcpSocketObserver(filename, pythonPath);
wrapper.Subscribe(this);
}

/// <summary>
/// The class <c>JsonFileObserver</c> implements the Observer pattern for <c>JsonRedirectionData</c> instances,
/// serializing the information which it receives and writing it to a file.
Expand All @@ -108,5 +180,146 @@ public JsonFileObserver(string filename) : base(filename) { }

override public void OnNext(JsonRedirectionData value) => writer.WriteLine(JsonConvert.SerializeObject(value));
}
}

/// <summary>
/// The class <c>TcpSocketObserver</c> implements the Observer pattern for <c>JsonRedirectionData</c> instances,
/// serializing the World Redirection information which it receives and sending it over a TCP connection to a local socket.
/// </summary>
private sealed class TcpSocketObserver : IObserver<JsonRedirectionData>
{
private IDisposable unsubscriber;

private TcpClient client;
private CancellationTokenSource periodicConnectTokenSrc, periodicSendMsgTokenSrc;

private string filename;
private string pythonPath;

private WRData currentRedirectionData;

public TcpSocketObserver(string filename, string pythonPath)
{
// TODO move the python process in an entire different wrapper than the Socket observer
this.filename = filename;
this.pythonPath = pythonPath;
periodicConnectTokenSrc = new();
periodicSendMsgTokenSrc = new();

currentRedirectionData = new();

if (filename is null || !filename.EndsWith(".py") || !File.Exists(filename))
{
UnityEngine.Debug.LogWarning($"Invalid Python script {filename}.");
}
else if (pythonPath is null || !pythonPath.EndsWith(".exe") || !File.Exists(pythonPath))
{
UnityEngine.Debug.LogWarning($"Invalid Python executable path {filename}.");
}
else
{
UnityEngine.Debug.Log($"Launch visualizer with Python {pythonPath}.");
// TODO not great for non-windows
System.Diagnostics.Process p = new()
{
StartInfo = new System.Diagnostics.ProcessStartInfo(pythonPath, filename)
{
ErrorDialog = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
}
};

// allow for redirecting python process standard output to Unity's console
p.ErrorDataReceived += (sendingProcess, errorLine) => UnityEngine.Debug.LogError(errorLine.Data);
p.OutputDataReceived += (sendingProcess, dataLine) => UnityEngine.Debug.Log(dataLine.Data);

p.Start();
p.BeginErrorReadLine();
p.BeginOutputReadLine();
}

PeriodicAsync(GetClient, TimeSpan.FromSeconds(5), periodicConnectTokenSrc.Token);
PeriodicAsync(StartSendingMessages, TimeSpan.FromSeconds(1), periodicSendMsgTokenSrc.Token);
}
public void Subscribe(IObservable<JsonRedirectionData> observable) => unsubscriber = observable?.Subscribe(this);

public void OnCompleted()
{
periodicConnectTokenSrc.Cancel();
periodicSendMsgTokenSrc.Cancel();
client.Close();
unsubscriber.Dispose();
}

public void OnError(Exception error)
{
throw new NotImplementedException();
}

void IObserver<JsonRedirectionData>.OnNext(JsonRedirectionData value)
{
var redirectionData = value.RedirectionData;
currentRedirectionData.AddTo(redirectionData.RotationOverTime, redirectionData.Rotational,
redirectionData.Curvature, redirectionData.Time);
}

private void GetClient()
{
client ??= new();
if (!client.Connected)
{
try
{
client.ConnectAsync("localhost", 13000);
}
catch (SocketException) { }
}
}
private void StartSendingMessages()
{
if (client != null && client.Connected)
{
string json = JsonConvert.SerializeObject(currentRedirectionData);
Thread thread = new(() => SendMessage(client, json));
thread.Start();
currentRedirectionData.Reset();
}
}

private void SendMessage(TcpClient client, string json)
{

// Translate the passed message into ASCII and store it as a Byte array.
byte[] messageBytes = System.Text.Encoding.ASCII.GetBytes(json + '\n');

try
{
// Get a client stream for reading and writing.
NetworkStream stream = client.GetStream();
// Send the message to the connected TcpServer.
stream.Write(messageBytes, 0, messageBytes.Length);
stream.Flush();
}
catch (IOException) { UnityEngine.Debug.LogWarning("Socket closed."); }
}

private static async void PeriodicAsync(Action action, TimeSpan interval,
CancellationToken cancellationToken = default)
{
while (true)
{
await Task.Run(action);

try
{
await Task.Delay(interval, cancellationToken);
}
catch (TaskCanceledException) { break; }
}
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using UnityEngine;
using UnityEditor;

namespace VHToolkit.Logging
{
[CustomEditor(typeof(JsonLogging)), CanEditMultipleObjects]
public class JsonLoggingEditor : Editor
{
SerializedProperty logDirectoryPath, optionalFilenamePrefix,
filename, pythonPath;
private void OnEnable()
{
logDirectoryPath = serializedObject.FindProperty("logDirectoryPath");
optionalFilenamePrefix = serializedObject.FindProperty("optionalFilenamePrefix");
filename = serializedObject.FindProperty("filename");
pythonPath = serializedObject.FindProperty("pythonPath");
}
private void MakePropertyField(SerializedProperty property, string text, string tooltip = null)
{
EditorGUILayout.PropertyField(property, new GUIContent(text, tooltip));
}

public override void OnInspectorGUI()
{
GUI.enabled = false;
EditorGUILayout.ObjectField("Script:", MonoScript.FromMonoBehaviour((JsonLogging) target), typeof(JsonLogging), false);
GUI.enabled = true;

serializedObject.Update();

EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Json logging Parameters", EditorStyles.largeLabel);

MakePropertyField(logDirectoryPath, "Log directory Path", "The directory path inside which json file(s) will be created");
MakePropertyField(optionalFilenamePrefix, "Optional filename prefix", "Optional prefix for the json file");

EditorGUILayout.Space(5);
EditorGUILayout.LabelField("TCP Socket Parameters", EditorStyles.largeLabel);

MakePropertyField(filename, "Python visualizer script", "File name for the Python visualization script.");
MakePropertyField(pythonPath, "Python Path", "File name for the Python executable path.");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 47ae5e6

Please sign in to comment.