aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs209
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/Mappers/EventMapper.cs25
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/Mappers/LogMapper.cs42
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsHistorySource.cs79
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsStreamingSource.cs64
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs19
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj6
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryEvent.cs16
-rw-r--r--Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs4
9 files changed, 448 insertions, 16 deletions
diff --git a/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs b/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs
new file mode 100644
index 000000000..2a0c3cc85
--- /dev/null
+++ b/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace Tango.Telemetry
+{
+ internal static class DateTimeUtcFixer
+ {
+ /// <summary>
+ /// Recursively scans the object graph starting at <paramref name="obj"/> and
+ /// sets Kind=Utc on any DateTime/DateTime? properties or collection elements that are Kind=Unspecified.
+ /// Uses DateTime.SpecifyKind(value, DateTimeKind.Utc) (no clock adjustment).
+ /// Notes:
+ /// - Only properties are updated (not fields).
+ /// - Collections: updates elements for IList/arrays and dictionary values; traverses other IEnumerable to reach nested objects.
+ /// - Read-only properties (no setter) are skipped.
+ /// - Root objects that are a DateTime value cannot be changed in place (struct); wrap them in a container if needed.
+ /// </summary>
+ public static void EnsureDateTimeUTC(object obj)
+ {
+ var visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
+ EnsureUtcInternal(obj, visited);
+ }
+
+ private static void EnsureUtcInternal(object obj, HashSet<object> visited)
+ {
+ if (obj == null) return;
+
+ var type = obj.GetType();
+
+ // Avoid revisiting reference objects (handle cycles, EF proxies, etc.)
+ if (!type.IsValueType) // only track reference types
+ {
+ if (!visited.Add(obj)) return;
+ }
+
+ // Handle dictionaries: update values, recurse into objects
+ if (obj is IDictionary dict)
+ {
+ var keys = new List<object>();
+ foreach (var k in dict.Keys) keys.Add(k);
+
+ foreach (var key in keys)
+ {
+ var value = dict[key];
+ if (TrySpecifyUtcOnBoxedDateTime(ref value))
+ {
+ dict[key] = value!;
+ }
+ else
+ {
+ EnsureUtcInternal(value, visited);
+ }
+ }
+ return;
+ }
+
+ // Handle lists/arrays: update elements, recurse into objects
+ if (obj is IList list)
+ {
+ for (int i = 0; i < list.Count; i++)
+ {
+ var element = list[i];
+ if (TrySpecifyUtcOnBoxedDateTime(ref element))
+ {
+ list[i] = element!;
+ }
+ else
+ {
+ EnsureUtcInternal(element, visited);
+ }
+ }
+ return;
+ }
+
+ // Traverse other enumerables just to reach nested objects (cannot replace DateTime elements)
+ if (obj is IEnumerable enumerable && !(obj is string))
+ {
+ foreach (var element in enumerable)
+ {
+ EnsureUtcInternal(element, visited);
+ }
+ }
+
+ // Inspect properties
+ const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
+ foreach (var prop in type.GetProperties(flags))
+ {
+ // indexers not supported
+ if (prop.GetIndexParameters().Length != 0) continue;
+
+ // must be readable
+ if (!prop.CanRead) continue;
+
+ var pType = prop.PropertyType;
+
+ // DateTime
+ if (pType == typeof(DateTime))
+ {
+ var val = (DateTime)prop.GetValue(obj);
+ if (val.Kind == DateTimeKind.Unspecified)
+ {
+ var newVal = DateTime.SpecifyKind(val, DateTimeKind.Utc);
+ TrySetProperty(obj, prop, newVal);
+ }
+ continue;
+ }
+
+ // Nullable<DateTime>
+ var underlying = Nullable.GetUnderlyingType(pType);
+ if (underlying == typeof(DateTime))
+ {
+ var val = (DateTime?)prop.GetValue(obj);
+ if (val.HasValue && val.Value.Kind == DateTimeKind.Unspecified)
+ {
+ var newVal = (DateTime?)DateTime.SpecifyKind(val.Value, DateTimeKind.Utc);
+ TrySetProperty(obj, prop, newVal);
+ }
+ continue;
+ }
+
+ // Skip obvious simple types
+ if (IsSimple(pType)) continue;
+
+ // Recurse into complex objects
+ var child = prop.GetValue(obj);
+ EnsureUtcInternal(child, visited);
+ }
+ }
+
+ private static bool TrySpecifyUtcOnBoxedDateTime(ref object obj)
+ {
+ if (obj == null) return false;
+
+ var t = obj.GetType();
+
+ // Exact DateTime
+ if (t == typeof(DateTime))
+ {
+ var dt = (DateTime)obj;
+ if (dt.Kind == DateTimeKind.Unspecified)
+ {
+ obj = DateTime.SpecifyKind(dt, DateTimeKind.Utc);
+ }
+ return true;
+ }
+
+ // Nullable<DateTime> boxed
+ var underlying = Nullable.GetUnderlyingType(t);
+ if (underlying == typeof(DateTime))
+ {
+ // boxed Nullable<DateTime> can be unboxed to DateTime? safely
+ var ndt = (DateTime?)obj;
+ if (ndt.HasValue && ndt.Value.Kind == DateTimeKind.Unspecified)
+ {
+ obj = (DateTime?)DateTime.SpecifyKind(ndt.Value, DateTimeKind.Utc);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TrySetProperty(object target, PropertyInfo prop, object value)
+ {
+ try
+ {
+ // Prefer invoking the setter (even if non-public)
+ var setter = prop.GetSetMethod(nonPublic: true);
+ if (setter == null) return false;
+ setter.Invoke(target, new[] { value });
+ return true;
+ }
+ catch
+ {
+ // Fall back to PropertyInfo.SetValue (may still work in some runtimes)
+ try
+ {
+ prop.SetValue(target, value);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ private static bool IsSimple(Type t)
+ {
+ if (t.IsPrimitive || t.IsEnum) return true;
+ if (t == typeof(string) || t == typeof(decimal) || t == typeof(Guid) ||
+ t == typeof(Uri) || t == typeof(TimeSpan) || t == typeof(DateTimeOffset))
+ return true;
+ // Value types other than DateTime/Nullable<DateTime> are considered simple
+ return t.IsValueType;
+ }
+
+ /// <summary>Reference equality comparer for the visited set.</summary>
+ private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
+ {
+ public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer();
+ public new bool Equals(object x, object y) => ReferenceEquals(x, y);
+ public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Tango.Telemetry/Mappers/EventMapper.cs b/Software/Visual_Studio/Tango.Telemetry/Mappers/EventMapper.cs
new file mode 100644
index 000000000..02969d5e3
--- /dev/null
+++ b/Software/Visual_Studio/Tango.Telemetry/Mappers/EventMapper.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tango.BL.Entities;
+using Tango.Telemetry.Telemetries;
+
+namespace Tango.Telemetry.Mappers
+{
+ public class EventMapper
+ {
+ public static TelemetryEvent MapEvent(MachinesEvent ev)
+ {
+ TelemetryEvent t = new TelemetryEvent();
+ t.ID = ev.Guid;
+ t.Time = ev.DateTime;
+ t.HostName = ev.HostName;
+ t.EventTypeGuid = ev.EventTypeGuid;
+ t.Description = ev.Description;
+
+ return t;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Tango.Telemetry/Mappers/LogMapper.cs b/Software/Visual_Studio/Tango.Telemetry/Mappers/LogMapper.cs
new file mode 100644
index 000000000..6f5abbf1b
--- /dev/null
+++ b/Software/Visual_Studio/Tango.Telemetry/Mappers/LogMapper.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tango.Logging;
+using Tango.PMR.Debugging;
+using Tango.Telemetry.Telemetries;
+
+namespace Tango.Telemetry.Mappers
+{
+ public class LogMapper
+ {
+ public static TelemetryLog MapLog(LogItemBase log)
+ {
+ TelemetryLog tLog = new TelemetryLog();
+ tLog.Source = "Application";
+ tLog.Time = DateTime.UtcNow;
+ tLog.Category = log.Category.ToString();
+ tLog.Class = log.ClassName;
+ tLog.Method = log.CallerMethodName;
+ tLog.Line = log.CallerLineNumber;
+ tLog.Message = log.Message;
+
+ return tLog;
+ }
+
+ public static TelemetryLog MapLog(StartDebugLogResponse log)
+ {
+ TelemetryLog tLog = new TelemetryLog();
+ tLog.Source = "Firmware";
+ tLog.Time = DateTime.UtcNow;
+ tLog.Category = log.Category.ToString();
+ tLog.Class = log.FileName;
+ tLog.Method = $"[{log.ModuleId}] [{log.Filter}] [{log.Parameter}]";
+ tLog.Line = log.LineNumber;
+ tLog.Message = log.Message;
+
+ return tLog;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsHistorySource.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsHistorySource.cs
new file mode 100644
index 000000000..57abee66b
--- /dev/null
+++ b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsHistorySource.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Entity;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tango.BL;
+using Tango.Telemetry.Mappers;
+using Tango.Telemetry.Telemetries;
+
+namespace Tango.Telemetry.Sources
+{
+ public class TelemetryEventsHistorySource : ITelemetryHistorySource
+ {
+ private bool _isBusy;
+
+ public TelemetryHistorySourceDirection Direction { get; } = TelemetryHistorySourceDirection.Descending;
+ public string Name { get; } = "Events History";
+ public bool RequiresTelemetryDuplicationTracking { get; } = true;
+
+ public async Task<bool> CanRequestHistory(DateTime from)
+ {
+ if (_isBusy)
+ {
+ return false;
+ }
+ else
+ {
+ try
+ {
+ using (ObservablesContext db = ObservablesContext.CreateDefault())
+ {
+ return await db.MachinesEvents.CountAsync(x => x.LastUpdated < from) > 0;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ public async Task<IEnumerable<ITelemetry>> RequestHistory(DateTime from)
+ {
+ try
+ {
+ _isBusy = true;
+
+ using (ObservablesContext db = ObservablesContext.CreateDefault())
+ {
+ var events = await db.MachinesEvents
+ .OrderByDescending(x => x.LastUpdated)
+ .Where(x => x.LastUpdated < from)
+ .Take(10)
+ .ToListAsync();
+
+ List<TelemetryEvent> tRuns = new List<TelemetryEvent>();
+
+ foreach (var ev in events)
+ {
+ var tEvent = EventMapper.MapEvent(ev);
+ tRuns.Add(tEvent);
+ }
+
+ return tRuns;
+ }
+ }
+ finally
+ {
+ _isBusy = false;
+ }
+ }
+
+ public void Dispose()
+ {
+
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsStreamingSource.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsStreamingSource.cs
new file mode 100644
index 000000000..4b00c49ce
--- /dev/null
+++ b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsStreamingSource.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tango.BL.Entities;
+using Tango.Integration.Operation;
+using Tango.Telemetry.Mappers;
+using Tango.Telemetry.Telemetries;
+
+namespace Tango.Telemetry.Sources
+{
+ public class TelemetryEventsStreamingSource : ITelemetryStreamingSource
+ {
+ private IMachineOperator _machineOperator;
+
+ public bool IsStarted { get; private set; }
+ public string Name { get; } = "Events Streaming";
+ public bool RequiresTelemetryDuplicationTracking { get; } = true;
+
+ public event EventHandler<TelemetryAvailableEventArgs> TelemetryAvailable;
+
+ public TelemetryEventsStreamingSource(IMachineOperator machineOperator)
+ {
+ _machineOperator = machineOperator;
+ }
+
+ private void MachineEventsStateProvider_NewEvents(object sender, IEnumerable<MachinesEvent> events)
+ {
+ if (IsStarted)
+ {
+ foreach (var ev in events)
+ {
+ TelemetryEvent t = EventMapper.MapEvent(ev);
+
+ TelemetryAvailable?.Invoke(this, new TelemetryAvailableEventArgs() { TelemetryObject = t });
+ }
+ }
+ }
+
+ public void Start()
+ {
+ if (!IsStarted)
+ {
+ IsStarted = true;
+ _machineOperator.MachineEventsStateProvider.NewEvents += MachineEventsStateProvider_NewEvents;
+ }
+ }
+
+ public void Stop()
+ {
+ if (IsStarted)
+ {
+ IsStarted = false;
+ _machineOperator.MachineEventsStateProvider.NewEvents -= MachineEventsStateProvider_NewEvents;
+ }
+ }
+
+ public void Dispose()
+ {
+ Stop();
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs
index 9deb93132..2c8020f7c 100644
--- a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs
+++ b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs
@@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using Tango.Integration.Operation;
using Tango.Logging;
+using Tango.Telemetry.Mappers;
using Tango.Telemetry.Telemetries;
namespace Tango.Telemetry.Sources
@@ -53,14 +54,7 @@ namespace Tango.Telemetry.Sources
_lastFirmwareMessage = new KeyValuePair<DateTime, string>(DateTime.UtcNow, log.Message);
- TelemetryLog tLog = new TelemetryLog();
- tLog.Source = "Firmware";
- tLog.Time = DateTime.UtcNow;
- tLog.Category = log.Category.ToString();
- tLog.Class = log.FileName;
- tLog.Method = $"[{log.ModuleId}] [{log.Filter}] [{log.Parameter}]";
- tLog.Line = log.LineNumber;
- tLog.Message = log.Message;
+ TelemetryLog tLog = LogMapper.MapLog(log);
TelemetryAvailable?.Invoke(this, new TelemetryAvailableEventArgs() { TelemetryObject = tLog });
}
}
@@ -71,14 +65,7 @@ namespace Tango.Telemetry.Sources
{
if (!Config.Categories.Contains(log.Category)) return;
- TelemetryLog tLog = new TelemetryLog();
- tLog.Source = "Application";
- tLog.Time = log.TimeStamp.ToUniversalTime();
- tLog.Category = log.Category.ToString();
- tLog.Class = log.ClassName;
- tLog.Method = log.CallerMethodName;
- tLog.Line = log.CallerLineNumber;
- tLog.Message = log.Message;
+ TelemetryLog tLog = LogMapper.MapLog(log);
TelemetryAvailable?.Invoke(this, new TelemetryAvailableEventArgs() { TelemetryObject = tLog });
}
}
diff --git a/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj b/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj
index 35ddf9839..14cd6e904 100644
--- a/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj
+++ b/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj
@@ -239,6 +239,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
+ <Compile Include="DateTimeUTCFixer.cs" />
<Compile Include="Destinations\TelemetryAzureHubDestination.cs" />
<Compile Include="ExtensionMethods\ITelemetryExtensions.cs" />
<Compile Include="ITelemetryCheckpointsRecoveryClient.cs" />
@@ -250,7 +251,9 @@
<Compile Include="ITelemetryStorageManager.cs" />
<Compile Include="ITelemetryPublisher.cs" />
<Compile Include="ITelemetryStreamingSource.cs" />
+ <Compile Include="Mappers\EventMapper.cs" />
<Compile Include="Mappers\JobRunMapper.cs" />
+ <Compile Include="Mappers\LogMapper.cs" />
<Compile Include="Mappers\MachineUpdateMapper.cs" />
<Compile Include="Reporting\DestinationStatusSummary.cs" />
<Compile Include="Reporting\SourceSummary.cs" />
@@ -258,6 +261,8 @@
<Compile Include="Reporting\TelemetryReport.cs" />
<Compile Include="Sources\TelemetryDiagnosticsStreamingSource.cs" />
<Compile Include="Sources\TelemetryDiagnosticsStreamingSourceConfig.cs" />
+ <Compile Include="Sources\TelemetryEventsHistorySource.cs" />
+ <Compile Include="Sources\TelemetryEventsStreamingSource.cs" />
<Compile Include="Sources\TelemetryJobRunsHistorySource.cs" />
<Compile Include="Sources\TelemetryJobRunsHistorySourceConfig.cs" />
<Compile Include="PendingTelemetry.cs" />
@@ -266,6 +271,7 @@
<Compile Include="Sources\TelemetryLogsStreamingSourceConfig.cs" />
<Compile Include="Sources\TelemetryMachineUpdatesStreamingSource.cs" />
<Compile Include="Sources\TelemetryMachineUpdatesHistorySource.cs" />
+ <Compile Include="Telemetries\TelemetryEvent.cs" />
<Compile Include="Telemetries\TelemetryLog.cs" />
<Compile Include="Telemetries\TelemetryMachineUpdate.cs" />
<Compile Include="TelemetryConfigurableSource.cs" />
diff --git a/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryEvent.cs b/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryEvent.cs
new file mode 100644
index 000000000..2b11b7922
--- /dev/null
+++ b/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryEvent.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Tango.Telemetry.Telemetries
+{
+ [TelemetryName("Event", 1)]
+ public class TelemetryEvent : TelemetryBase
+ {
+ public String HostName { get; set; }
+ public String EventTypeGuid { get; set; }
+ public String Description { get; set; }
+ }
+}
diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs
index 70b1f6f18..f92d508e7 100644
--- a/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs
+++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs
@@ -147,6 +147,10 @@ namespace Tango.Telemetry
lock (_lock)
{
var collection = GetPendingTelemetriesCollection();
+
+ //Ensure all datetimes "Kind" is UTC so LiteDB won't change them on query.
+ DateTimeUtcFixer.EnsureDateTimeUTC(pendingTelemetry.TelemetryObject);
+
collection.Upsert(pendingTelemetry);
}
}