diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/App.xaml.cs b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/App.xaml.cs
index 5b56902..4cd8dc0 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/App.xaml.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/App.xaml.cs
@@ -1,8 +1,6 @@
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using WinRT.Interop; // For WindowNative
-using Microsoft.UI; // For WindowId
-using System.Linq; // For FirstOrDefault
namespace TRION_SDK_UI.WinUI
{
@@ -19,18 +17,21 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
{
base.OnLaunched(args);
- // Get the first MAUI window and its native WinUI window
- var mauiWindow = Microsoft.Maui.Controls.Application.Current.Windows.FirstOrDefault();
- var nativeWindow = mauiWindow?.Handler?.PlatformView as Microsoft.UI.Xaml.Window;
-
- if (nativeWindow is not null)
+ var app = Microsoft.Maui.Controls.Application.Current;
+ if (app != null && app.Windows.Count > 0)
{
- var hwnd = WindowNative.GetWindowHandle(nativeWindow);
- var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd);
- var appWindow = AppWindow.GetFromWindowId(windowId);
+ var mauiWindow = app.Windows[0];
+ var nativeWindow = mauiWindow?.Handler?.PlatformView as Microsoft.UI.Xaml.Window;
+
+ if (nativeWindow is not null)
+ {
+ var hwnd = WindowNative.GetWindowHandle(nativeWindow);
+ var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd);
+ var appWindow = AppWindow.GetFromWindowId(windowId);
- // Set your desired window size here
- appWindow.Resize(new Windows.Graphics.SizeInt32 { Width = 1200, Height = 800 });
+ // Set your desired window size here
+ appWindow.Resize(new Windows.Graphics.SizeInt32 { Width = 1200, Height = 800 });
+ }
}
}
}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-100.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-100.png
new file mode 100644
index 0000000..6f82687
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-100.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-125.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-125.png
new file mode 100644
index 0000000..5f47854
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-125.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-150.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-150.png
new file mode 100644
index 0000000..32efb34
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-150.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-200.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-200.png
new file mode 100644
index 0000000..08e7c4e
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-200.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-400.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-400.png
new file mode 100644
index 0000000..4573d03
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/BadgeLogo.scale-400.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/appicon.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/appicon.png
new file mode 100644
index 0000000..077432f
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/appicon.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/appicon.svg b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/appicon.svg
new file mode 100644
index 0000000..046d321
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/appicon.svg
@@ -0,0 +1,63 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/splashgeneric.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/splashgeneric.png
new file mode 100644
index 0000000..5c790ba
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Images/splashgeneric.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-100.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-100.png
new file mode 100644
index 0000000..3fcf5d1
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-100.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-125.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-125.png
new file mode 100644
index 0000000..d4e7839
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-125.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-150.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-150.png
new file mode 100644
index 0000000..5476d36
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-150.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-200.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-200.png
new file mode 100644
index 0000000..4f410fc
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-200.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-400.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-400.png
new file mode 100644
index 0000000..4b12647
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/LargeTile.scale-400.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-100.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-100.png
new file mode 100644
index 0000000..ee26c92
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-100.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-125.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-125.png
new file mode 100644
index 0000000..47df2b1
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-125.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-150.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-150.png
new file mode 100644
index 0000000..c7cb464
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-150.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-200.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-200.png
new file mode 100644
index 0000000..a306223
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-200.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-400.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-400.png
new file mode 100644
index 0000000..4aa687e
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SmallTile.scale-400.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-100.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-100.png
new file mode 100644
index 0000000..4148dcf
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-100.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-125.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-125.png
new file mode 100644
index 0000000..9f698f5
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-125.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-150.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-150.png
new file mode 100644
index 0000000..27a7d82
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-150.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-200.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-200.png
index 32f486a..3920335 100644
Binary files a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-200.png and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-200.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-400.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-400.png
new file mode 100644
index 0000000..125a119
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/SplashScreen.scale-400.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-100.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-100.png
new file mode 100644
index 0000000..eb1d3ef
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-100.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-125.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-125.png
new file mode 100644
index 0000000..f5b0769
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-125.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-150.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-150.png
new file mode 100644
index 0000000..d3eafbf
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-150.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-200.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-200.png
index 53ee377..f503f18 100644
Binary files a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-200.png and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-200.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-400.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-400.png
new file mode 100644
index 0000000..7b6ada7
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square150x150Logo.scale-400.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png
new file mode 100644
index 0000000..014a643
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png
new file mode 100644
index 0000000..40c3ee4
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png
new file mode 100644
index 0000000..da10442
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png
new file mode 100644
index 0000000..7ac7016
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png
new file mode 100644
index 0000000..fbfec4b
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png
new file mode 100644
index 0000000..014a643
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png
new file mode 100644
index 0000000..da10442
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png
new file mode 100644
index 0000000..7ac7016
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png
new file mode 100644
index 0000000..fbfec4b
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-100.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-100.png
new file mode 100644
index 0000000..44b91f1
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-100.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-125.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-125.png
new file mode 100644
index 0000000..22bb989
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-125.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-150.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-150.png
new file mode 100644
index 0000000..b8e6d0e
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-150.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-200.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-200.png
index f713bba..a8c5533 100644
Binary files a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-200.png and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-200.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-400.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-400.png
new file mode 100644
index 0000000..e5c96dd
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.scale-400.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-16.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-16.png
new file mode 100644
index 0000000..5a2cfc5
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-16.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-24.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-24.png
new file mode 100644
index 0000000..6590711
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-24.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
index dc9f5be..40c3ee4 100644
Binary files a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-256.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-256.png
new file mode 100644
index 0000000..9f02fba
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-256.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-32.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-32.png
new file mode 100644
index 0000000..ebb644c
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-32.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-48.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-48.png
new file mode 100644
index 0000000..3651a67
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Square44x44Logo.targetsize-48.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.backup.png
similarity index 100%
rename from app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.png
rename to app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.backup.png
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-100.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-100.png
new file mode 100644
index 0000000..fc0707c
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-100.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-125.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-125.png
new file mode 100644
index 0000000..c1535be
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-125.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-150.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-150.png
new file mode 100644
index 0000000..c98e991
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-150.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-200.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-200.png
new file mode 100644
index 0000000..7b7aa9c
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-200.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-400.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-400.png
new file mode 100644
index 0000000..135cc24
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/StoreLogo.scale-400.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-100.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-100.png
new file mode 100644
index 0000000..8b797d8
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-100.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-125.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-125.png
new file mode 100644
index 0000000..db5a77a
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-125.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-150.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-150.png
new file mode 100644
index 0000000..e98d0ad
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-150.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-200.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-200.png
index 8b4a5d0..361fc63 100644
Binary files a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-200.png and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-400.png b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-400.png
new file mode 100644
index 0000000..f3d94e4
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Assets/Wide310x150Logo.scale-400.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/MauiProgram.cs b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/MauiProgram.cs
index d12dcd2..da398b0 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/MauiProgram.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/MauiProgram.cs
@@ -1,4 +1,5 @@
using LiveChartsCore.SkiaSharpView.Maui;
+using ScottPlot.Maui;
using SkiaSharp.Views.Maui.Controls.Hosting;
namespace TRION_SDK_UI.WinUI
@@ -12,9 +13,10 @@ public static MauiApp CreateMauiApp()
builder
.UseSharedMauiApp()
.UseLiveCharts()
- .UseSkiaSharp();
+ .UseSkiaSharp()
+ .UseScottPlot();
return builder.Build();
}
}
-}
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Package.appxmanifest b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Package.appxmanifest
index 8c03eb2..7bc1a48 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Package.appxmanifest
+++ b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/Package.appxmanifest
@@ -29,12 +29,13 @@
-
+
+
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/TRION-SDK-UI.WinUI.csproj b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/TRION-SDK-UI.WinUI.csproj
index 379f0c6..af86310 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/TRION-SDK-UI.WinUI.csproj
+++ b/app/TRION-SDK-UI/TRION-SDK-UI.WinUI/TRION-SDK-UI.WinUI.csproj
@@ -17,12 +17,27 @@
false
+
+
+ true
+ True
+ False
+ SHA256
+ True
+ False
+ True
+ Never
+ C:\development
+ 0
+
+
+
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI.sln b/app/TRION-SDK-UI/TRION-SDK-UI.sln
index 5fa6182..08d6f1c 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI.sln
+++ b/app/TRION-SDK-UI/TRION-SDK-UI.sln
@@ -47,6 +47,7 @@ Global
{EB895243-61D5-493C-A564-B326347D06B6}.Debug|Any CPU.Deploy.0 = Debug|x64
{EB895243-61D5-493C-A564-B326347D06B6}.Release|Any CPU.ActiveCfg = Release|x64
{EB895243-61D5-493C-A564-B326347D06B6}.Release|Any CPU.Build.0 = Release|x64
+ {EB895243-61D5-493C-A564-B326347D06B6}.Release|Any CPU.Deploy.0 = Release|x64
{5586FAB3-D546-2C14-0B8C-DFE03B46CAE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5586FAB3-D546-2C14-0B8C-DFE03B46CAE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5586FAB3-D546-2C14-0B8C-DFE03B46CAE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/App.xaml.cs b/app/TRION-SDK-UI/TRION-SDK-UI/App.xaml.cs
index 81d3608..c58712c 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/App.xaml.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/App.xaml.cs
@@ -1,6 +1,4 @@
-using Trion;
-
-namespace TRION_SDK_UI
+namespace TRION_SDK_UI
{
public partial class App : Application
{
@@ -9,5 +7,12 @@ public App()
InitializeComponent();
MainPage = new AppShell();
}
+ protected override Window CreateWindow(IActivationState? activationState)
+ {
+ var window = base.CreateWindow(activationState);
+ window.Width = 1600;
+ window.Height = 900;
+ return window;
+ }
}
}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Converters/ChannelToStringConverter.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Converters/ChannelToStringConverter.cs
index 59e3c7a..c24ac64 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Converters/ChannelToStringConverter.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Converters/ChannelToStringConverter.cs
@@ -1,7 +1,5 @@
-using System;
using System.Globalization;
-using Microsoft.Maui.Controls;
-using TRION_SDK_UI.Models; // Adjust namespace if needed
+using TRION_SDK_UI.Models;
namespace TRION_SDK_UI.Converters
{
@@ -9,10 +7,9 @@ public class ChannelToStringConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
- var channel = value as Channel;
- if (channel == null)
+ if (value is not Channel channel)
return string.Empty;
- return $"Board {channel.BoardID} - {channel.Name}";
+ return $"{channel.BoardName} - {channel.Name}";
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/AnalogChannel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/AnalogChannel.cs
new file mode 100644
index 0000000..a092c05
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/AnalogChannel.cs
@@ -0,0 +1,31 @@
+using Trion;
+using TrionApiUtils;
+
+namespace TRION_SDK_UI.Models
+{
+ public class AnalogChannel : Channel
+ {
+ public AnalogChannel()
+ {
+ Type = ChannelType.Analog;
+ }
+
+ public override void Activate()
+ {
+ string target = GetTargetName();
+ TrionError error;
+
+ error = TrionApi.DeWeSetParamStruct(target, "Used", "True");
+ Utils.CheckErrorCode(error, $"Failed to activate channel {Name} on board {BoardID}");
+
+ error = TrionApi.DeWeSetParamStruct(target, "Mode", Mode.Name);
+ Utils.CheckErrorCode(error, $"Failed to set mode {Mode.Name} for channel {Name} on board {BoardID}");
+
+ if (!string.IsNullOrEmpty(Range))
+ {
+ error = TrionApi.DeWeSetParamStruct(target, "Range", $"{Range} {Unit}");
+ Utils.CheckErrorCode(error, $"Failed to set range for channel {Name} on board {BoardID}");
+ }
+ }
+ }
+}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/Board.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/Board.cs
index 5410057..bcd4084 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Models/Board.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/Board.cs
@@ -1,90 +1,120 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Diagnostics;
using Trion;
using TrionApiUtils;
namespace TRION_SDK_UI.Models
{
- public enum BoardType
+ public class Board()
{
- Unknown = 0,
- Analog = 1,
- Digital = 2,
- Counter = 3
- }
- public class Board(BoardPropertyModel BoardProperties)
- {
- public int Id { get; set; }
-
- public string? Name { get; set; }
- public bool IsActive { get; set; }
- public BoardPropertyModel BoardProperties { get; set; } = BoardProperties;
- public List Channels { get; set; } = [];
- public uint ScanSizeBytes { get; set; }
- public ScanDescriptorDecoder? ScanDescriptorDecoder { get; set; }
+ public required int Id { get; set; }
+ public required string Name { get; set; }
+ public required BoardPropertyParser BoardProperties { get; init; }
+ public required List Channels { get; set; } = [];
+ public ScanDescriptorDecoder? ScanDescriptor { get; set; }
public string ScanDescriptorXml { get; set; } = string.Empty;
+ private int BufferBlockSize { get; set; }
+ public required int SamplingRate { get; set; }
+ public required int BufferBlockCount { get; set; }
+ public required string OperationMode { get; set; }
+ public required string ExternalTrigger { get; set; }
+ public required string ExternalClock { get; set; }
+ public bool IsAcquiring { get; set; }
+ public int SampleRateDivider { get; set; }
+ public string? ResolutionAI { get; set; }
+ public void RefreshScanDescriptor()
+ {
+ Debug.WriteLine($"Refreshing scan descriptor for board {Id}");
+ Debug.WriteLine($"Current ScanDescriptorXml: {ScanDescriptorXml}");
+ (var error, ScanDescriptorXml) = TrionApi.DeWeGetParamStruct_String($"BoardID{Id}", "ScanDescriptor_V3");
+ Utils.CheckErrorCode(error, $"Failed to get scan descriptor {Id}");
- public void ReadScanDescriptor(string scanDescriptorXml)
+ ScanDescriptor = new ScanDescriptorDecoder(ScanDescriptorXml);
+ Debug.WriteLine($"Updated ScanDescriptorXml: {ScanDescriptorXml}");
+ }
+
+ public void SetAcqProp(string str, string value)
{
- if (string.IsNullOrWhiteSpace(scanDescriptorXml))
+ if (string.IsNullOrEmpty(value)) return;
+ var error = TrionApi.DeWeSetParamStruct($"BoardID{Id}/AcqProp", str, value);
+ Utils.CheckErrorCode(error, $"Failed to set acquisition property '{str}' for board {Id}");
+ Update();
+ }
+
+ public void ActivateChannels(IEnumerable selectedChannels)
+ {
+ DeactivateAllChannels(Id);
+
+ foreach (var channel in selectedChannels)
{
- System.Diagnostics.Debug.WriteLine($"Return Early");
- return;
+ channel.Activate();
}
- System.Diagnostics.Debug.WriteLine($"BoardID {Id}");
-
- ScanDescriptorDecoder = new ScanDescriptorDecoder(scanDescriptorXml);
- Channels = [.. ScanDescriptorDecoder.Channels
- .Select(c => new Channel
- {
- BoardID = Id,
- Name = c.Name ?? string.Empty,
- ChannelType = c.Type,
- Index = c.Index,
- SampleSize = c.SampleSize,
- SampleOffset = c.SampleOffset
- })];
- ScanSizeBytes = ScanDescriptorDecoder.ScanSizeBytes;
+ Update();
+ }
+
+ private static void DeactivateAllChannels(int boardId)
+ {
+ var error = TrionApi.DeWeSetParamStruct($"BoardID{boardId}/AIAll", "Used", "False");
+ Utils.CheckErrorCode(error, $"Failed to deactivate all analog channels on board {boardId}");
}
- public void SetBoardProperties()
+ public void UpdateBuffer(bool update)
{
- Id = BoardProperties.GetBoardID();
- Name = BoardProperties.GetBoardName();
- IsActive = true;
+ const double PollingInterval = 0.1; // 100ms target
+
+ int BufferBlockSize = (int)(SamplingRate * PollingInterval);
+ var test = Math.Max(1, BufferBlockSize);
+
+ var error = TrionApi.DeWeSetParam_i32(Id, TrionCommand.BUFFER_BLOCK_SIZE, test);
+ Utils.CheckErrorCode(error, $"Failed to set buffer block size for board {Id}");
+
+ error = TrionApi.DeWeSetParam_i32(Id, TrionCommand.BUFFER_BLOCK_COUNT, BufferBlockCount);
+ Utils.CheckErrorCode(error, $"Failed to set buffer block count for board {Id}");
+
+ error = TrionApi.DeWeSetParamStruct($"BoardID{Id}/AcqProp", "SampleRate", SamplingRate.ToString());
+ Utils.CheckErrorCode(error, $"Failed to set sampling rate for board {Id}");
+
+ if (update) Update();
}
- public void SetAcquisitionProperties(string operationMode = "Slave",
- string externalTrigger = "False",
- string externalClock = "False",
- string sampleRate = "2000",
- int buffer_block_size = 200,
- int buffer_block_count = 50)
+ public void SetResolutionAI(bool update)
{
- var error = TrionApi.DeWeSetParamStruct($"BoardID{Id}/AcqProp", "OperationMode", operationMode);
- error |= TrionApi.DeWeSetParamStruct($"BoardID{Id}/AcqProp", "ExtTrigger", externalTrigger);
- error |= TrionApi.DeWeSetParamStruct($"BoardID{Id}/AcqProp", "ExtClk", externalClock);
- error |= TrionApi.DeWeSetParamStruct($"BoardID{Id}/AcqProp", "SampleRate", sampleRate);
+ if (string.IsNullOrEmpty(ResolutionAI)) return;
- error |= TrionApi.DeWeSetParam_i32(Id, Trion.TrionCommand.BUFFER_BLOCK_SIZE, buffer_block_size);
- error |= TrionApi.DeWeSetParam_i32(Id, Trion.TrionCommand.BUFFER_BLOCK_COUNT, buffer_block_count);
+ var error = TrionApi.DeWeSetParamStruct($"BoardID{Id}/AcqProp", "ResolutionAI", ResolutionAI);
+ if (error > 0)
+ {
+ Debug.WriteLine($"Failed to set ResolutionAI to {ResolutionAI} on board {Id}. Error: {error}");
+ }
- Utils.CheckErrorCode(error, $"Failed to set acquisition properties for board {Id}");
+ if (update) Update();
+ }
+
+ public void UpdateAcquisitionProperties()
+ {
+ UpdateBuffer(false);
+ Debug.WriteLine($"Setting sampling rate to {SamplingRate} Hz on board {Id}");
+ SetAcqProp("OperationMode", OperationMode);
+ SetAcqProp("ExtTrigger", ExternalTrigger);
+ SetAcqProp("ExtClk", ExternalClock);
+ SetAcqProp("ResolutionAI", ResolutionAI ?? "");
+ Update();
}
- public void ResetBoard()
+ public void Reset()
{
var error = TrionApi.DeWeSetParam_i32(Id, TrionCommand.RESET_BOARD, 0);
Utils.CheckErrorCode(error, $"Failed to reset board {Id}");
+
+ ScanDescriptor = null;
+ ScanDescriptorXml = string.Empty;
+
+ Update();
}
- public void UpdateBoard()
+ public void Update()
{
var error = TrionApi.DeWeSetParam_i32(Id, TrionCommand.UPDATE_PARAM_ALL, 0);
Utils.CheckErrorCode(error, $"Failed to update board {Id}");
}
}
-}
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/BoardPropertyModel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/BoardPropertyModel.cs
deleted file mode 100644
index 7fd3e79..0000000
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Models/BoardPropertyModel.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System.Reflection.Metadata;
-using System.Xml.XPath;
-
-public class BoardPropertyModel
-{
- private XPathDocument _document;
- private XPathNavigator _navigator;
-
- public BoardPropertyModel(string boardXML)
- {
- using var stringReader = new StringReader(boardXML);
- _document = new XPathDocument(stringReader);
- _navigator = _document.CreateNavigator();
- }
-
- public List GetChannelNames()
- {
- var channelNames = new List();
- var iterator = _navigator.Select("Properties/ChannelProperties/*");
-
- while (iterator.MoveNext())
- {
- var channelNav = iterator.Current;
- if (channelNav != null)
- {
- string channel = channelNav.Name;
- channelNames.Add(channel);
- }
- }
-
- return channelNames;
- }
-
- public string GetBoardName()
- {
- var boardName = _navigator.SelectSingleNode("/Properties/BoardInfo/BoardName");
- return boardName != null ? boardName.Value : string.Empty;
- }
-
- public int GetBoardID()
- {
- var propertiesNode = _navigator.SelectSingleNode("/Properties");
- if (propertiesNode != null)
- {
- var idStr = propertiesNode.GetAttribute("BoardID", "");
- if (int.TryParse(idStr, out int id))
- return id;
- }
- return -1;
- }
-}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/BoardPropertyParser.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/BoardPropertyParser.cs
new file mode 100644
index 0000000..1e6953a
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/BoardPropertyParser.cs
@@ -0,0 +1,298 @@
+using System.Xml.Linq;
+using TRION_SDK_UI.POCO;
+using static TRION_SDK_UI.Models.Channel;
+
+namespace TRION_SDK_UI.Models;
+
+public sealed class BoardPropertyParser
+{
+ private readonly XDocument _xmlDocument;
+ private readonly XElement _rootElement;
+ private readonly XElement _AcquisitionProperties;
+
+ private string GetBoardInfoValue(string elementName) => _rootElement.Element("BoardInfo")?.Element(elementName)?.Value ?? string.Empty;
+
+ public BoardPropertyParser(string boardPropertiesXml)
+ {
+ ArgumentNullException.ThrowIfNull(boardPropertiesXml, nameof(boardPropertiesXml));
+ _xmlDocument = XDocument.Parse(boardPropertiesXml);
+ _rootElement = _xmlDocument.Root ?? throw new InvalidOperationException("Invalid XML: Missing root element.");
+ _AcquisitionProperties = _rootElement.Element("AcquisitionProperties") ?? throw new InvalidOperationException("Invalid XML: Missing AcquisitionProperties element.");
+ }
+
+ private static ChannelType GetChannelTypeFromString(string name)
+ {
+ if (name.StartsWith("AI", StringComparison.OrdinalIgnoreCase)) return ChannelType.Analog;
+ if (name.StartsWith("Discret", StringComparison.OrdinalIgnoreCase)) return ChannelType.Digital;
+ if (name.StartsWith("CNT", StringComparison.OrdinalIgnoreCase)) return ChannelType.Counter;
+ if (name.StartsWith("BoardCNT", StringComparison.OrdinalIgnoreCase)) return ChannelType.BoardCounter;
+ return ChannelType.Unknown;
+ }
+
+ public Board CreateBoard(int ID, string scanDescriptorXML, int bufferBlockCount)
+ {
+ var boardName = GetBoardName();
+
+ return new Board
+ {
+ Id = ID,
+ BoardProperties = this,
+ ScanDescriptorXml = scanDescriptorXML,
+ Name = boardName,
+ Channels = GetChannels(ID, boardName),
+ SamplingRate = GetDefaultIntAcqPropFromString("SampleRate"),
+ ExternalTrigger = GetDefaultStringAcqPropFromString("ExtTrigger"),
+ ExternalClock = GetDefaultStringAcqPropFromString("ExtClk"),
+ OperationMode = GetDefaultStringAcqPropFromString("OperationMode"),
+ BufferBlockCount = bufferBlockCount,
+ SampleRateDivider = GetDefaultIntAcqPropFromString("SampleRateDivider"),
+ ResolutionAI = GetDefaultStringAcqPropFromString("ResolutionAI")
+ };
+ }
+
+ private static int GetDefaultIntValueFromElement(int defaultIndex, XElement element)
+ {
+ var valueStr = element.Elements()
+ .FirstOrDefault(e => e.Name.LocalName.Equals($"ID{defaultIndex}", StringComparison.OrdinalIgnoreCase))
+ ?.Value;
+
+ if (int.TryParse(valueStr, out var rate))
+ {
+ return rate;
+ }
+
+ throw new InvalidOperationException($"Invalid XML: Unable to parse integer value at ID{defaultIndex} in {element.Name}.");
+ }
+
+ private static string GetDefaultStringValueFromElement(int defaultIndex, XElement element)
+ {
+ var value = element.Elements()
+ .FirstOrDefault(e => e.Name.LocalName.Equals($"ID{defaultIndex}", StringComparison.OrdinalIgnoreCase))
+ ?.Value;
+
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new InvalidOperationException($"Invalid XML: Unable to parse string value at ID{defaultIndex} in {element.Name}.");
+ }
+ return value;
+ }
+
+ private string GetDefaultStringAcqPropFromString(string str)
+ {
+ var acqProp = _AcquisitionProperties.Element("AcqProp");
+ var element = acqProp?.Element(str);
+
+ if (element is null)
+ {
+ return string.Empty;
+ }
+
+ var defaultIndex = element.GetAttrInt("Default", -1);
+ if (defaultIndex < 0)
+ {
+ return string.Empty;
+ }
+
+ return GetDefaultStringValueFromElement(defaultIndex, element);
+ }
+
+ private int GetDefaultIntAcqPropFromString(string str)
+ {
+ var acqProp = _AcquisitionProperties.Element("AcqProp");
+ var element = acqProp?.Element(str);
+
+ if (element is null)
+ {
+ return 0;
+ }
+
+ var defaultIndex = element.GetAttrInt("Default", -1);
+ if (defaultIndex < 0)
+ {
+ return 0;
+ }
+
+ return GetDefaultIntValueFromElement(defaultIndex, element);
+
+ }
+
+ private static string GetDefaultRange(ChannelMode mode)
+ {
+ if (int.TryParse(mode.DefaultValue, out var idx))
+ {
+ if (idx >= 0 && idx < mode.Ranges.Count)
+ {
+ return mode.Ranges[idx];
+ }
+ }
+ return mode.Ranges.FirstOrDefault() ?? string.Empty;
+ }
+
+ public string GetBoardName() => GetBoardInfoValue("BoardName");
+
+ public List GetChannels(int boardId = -1, string boardName = "")
+ {
+ var channels = new List(128);
+ var channelProps = _rootElement.Element("ChannelProperties");
+
+ if (channelProps is null)
+ {
+ return channels;
+ }
+
+ foreach (var channelElem in channelProps.Elements())
+ {
+ var type = GetChannelTypeFromString(channelElem.Name.LocalName);
+ if (type == ChannelType.Unknown)
+ {
+ continue;
+ }
+
+ var modes = GetChannelModes(channelElem);
+ if (modes.Count == 0)
+ {
+ continue;
+ }
+
+ var defaultModeName = channelElem.GetAttrString("Default");
+ var currentMode = modes.FirstOrDefault(m => m.Name.Equals(defaultModeName, StringComparison.OrdinalIgnoreCase))
+ ?? modes.First();
+
+ var defaultRange = GetDefaultRange(currentMode);
+
+ if (ChannelType.Analog == GetChannelTypeFromString(channelElem.Name.LocalName))
+ {
+ channels.Add(new AnalogChannel
+ {
+ BoardID = boardId,
+ BoardName = boardName,
+ Name = channelElem.Name.LocalName,
+ ModeList = modes,
+ Mode = currentMode,
+ Unit = currentMode.Unit ?? string.Empty,
+ Range = defaultRange
+ });
+ }
+ else if (ChannelType.Digital == GetChannelTypeFromString(channelElem.Name.LocalName))
+ {
+ channels.Add(new DigitalChannel
+ {
+ BoardID = boardId,
+ BoardName = boardName,
+ Name = channelElem.Name.LocalName,
+ ModeList = modes,
+ Mode = currentMode,
+ Unit = currentMode.Unit ?? string.Empty,
+ Range = defaultRange
+ });
+ }
+ else if (ChannelType.Counter == GetChannelTypeFromString(channelElem.Name.LocalName))
+ {
+ channels.Add(new CounterChannel
+ {
+ BoardID = boardId,
+ BoardName = boardName,
+ Name = channelElem.Name.LocalName,
+ ModeList = modes,
+ Mode = currentMode,
+ Unit = currentMode.Unit ?? string.Empty,
+ Range = defaultRange
+ });
+ }
+ }
+ return channels;
+ }
+
+ private XElement? AcqPropElem => _rootElement.Element("AcquisitionProperties")?.Element("AcqProp");
+
+ public IEnumerable GetAvailableValuesFromString(string str)
+ {
+ return AcqPropElem?.Element(str)?.Elements().Select(e => e.Value) ?? [];
+ }
+
+ public (bool IsProg, int Min, int Max, List Rates) GetSampleRateCapabilities()
+ {
+ var elem = AcqPropElem?.Element("SampleRate");
+ if (elem is null)
+ {
+ return (false, 0, 0, []);
+ }
+
+ var isProg = bool.TryParse(elem.Attribute("Programmable")?.Value, out var p) && p;
+
+ if (int.TryParse(elem.Attribute("ProgMin")?.Value, out var min) &&
+ int.TryParse(elem.Attribute("ProgMax")?.Value, out var max))
+ {
+ var rates = elem.Elements()
+ .Select(e => int.TryParse(e.Value, out var r) ? r : -1)
+ .Where(r => r > 0)
+ .ToList();
+
+ return (isProg, min, max, rates);
+ }
+ throw new InvalidOperationException("Invalid XML: Unable to parse SampleRate capabilities.");
+ }
+
+ public (int Min, int Max, List Proposed) GetDividerCapabilities()
+ {
+ var elem = AcqPropElem?.Element("SampleRateDivider");
+ if (elem is null)
+ {
+ return (0, 0, []);
+ }
+
+ if (int.TryParse(elem.Attribute("ProgMin")?.Value, out var min) &&
+ int.TryParse(elem.Attribute("ProgMax")?.Value, out var max))
+ {
+ var proposed = elem.Elements()
+ .Select(e => int.TryParse(e.Value, out var r) ? r : -1)
+ .Where(r => r > 0)
+ .ToList();
+
+ return (min, max, proposed);
+ }
+ throw new InvalidOperationException("Invalid XML: Unable to parse SampleRateDivider capabilities.");
+ }
+
+ private static List GetChannelModes(XElement channelElem)
+ {
+ var modes = new List();
+ foreach (var modeElem in channelElem.Elements("Mode"))
+ {
+ var name = modeElem.GetAttrString("Mode");
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ name = modeElem.GetAttrString("Name");
+ }
+
+ var rangeElem = modeElem.Element("Range");
+ var unit = rangeElem?.GetAttrString("Unit") ?? modeElem.GetAttrString("Unit");
+ var defaultVal = rangeElem?.GetAttrString("Default");
+
+ var ranges = rangeElem?.Elements()
+ .Where(e => e.Name.LocalName.StartsWith("ID"))
+ .Select(e => e.Value.Trim())
+ .ToList() ?? [];
+
+ modes.Add(new ChannelMode
+ {
+ Name = name,
+ Unit = unit,
+ Ranges = ranges,
+ DefaultValue = defaultVal,
+ Options = []
+ });
+ }
+ return modes;
+ }
+
+}
+
+file static class XmlExt
+{
+ public static string GetAttrString(this XElement? e, string name)
+ => e?.Attribute(name)?.Value ?? string.Empty;
+
+ public static int GetAttrInt(this XElement? e, string name, int def = 0)
+ => int.TryParse(e?.Attribute(name)?.Value, out var i) ? i : def;
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/Channel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/Channel.cs
index c5281b3..5a09d6d 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Models/Channel.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/Channel.cs
@@ -1,18 +1,118 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using TRION_SDK_UI.POCO;
namespace TRION_SDK_UI.Models
{
- public class Channel
+ public abstract class Channel : INotifyPropertyChanged
{
- public int BoardID { get; set; }
- public string? Name { get; set; }
- public string? ChannelType { get; set; }
- public uint Index { get; set; }
- public uint SampleSize { get; set; }
- public uint SampleOffset { get; set; }
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? name = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+
+ public enum ChannelType
+ {
+ Unknown = 0,
+ Analog = 1,
+ Digital = 2,
+ Counter = 3,
+ BoardCounter = 4
+ }
+
+ public required List ModeList { get; set; } = [];
+ public required int BoardID { get; set; }
+ public required string BoardName { get; set; }
+ public required string Name { get; set; }
+
+ public ChannelType Type { get; protected set; }
+
+ private string? _range;
+
+ public string? Range
+ {
+ get => _range;
+ set
+ {
+ if (value == _range)
+ {
+ return;
+ }
+ _range = value;
+ OnPropertyChanged();
+ }
+ }
+
+ private ChannelMode _mode = null!;
+ public required ChannelMode Mode
+ {
+ get => _mode;
+ set
+ {
+ if (ReferenceEquals(_mode, value))
+ {
+ return;
+ }
+ _mode = value;
+ OnModeChanged();
+ OnPropertyChanged();
+ }
+ }
+
+ protected virtual void OnModeChanged()
+ {
+ if (_mode == null)
+ {
+ return;
+ }
+
+ if (_mode.Unit != null)
+ {
+ Unit = _mode.Unit;
+ }
+
+ if (!string.IsNullOrEmpty(_mode.DefaultValue))
+ {
+ Range = _mode.DefaultValue;
+ }
+ else if (_mode.Ranges.Count > 0)
+ {
+ Range = _mode.Ranges[0];
+ }
+ }
+
+ private bool _isSelected;
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set
+ {
+ if (value == _isSelected)
+ {
+ return;
+ }
+
+ _isSelected = value;
+ OnPropertyChanged();
+ }
+ }
+
+ private string _unit = null!;
+ public required string Unit
+ {
+ get => _unit;
+ set
+ {
+ if (value == _unit)
+ {
+ return;
+ }
+ _unit = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public abstract void Activate();
+
+ protected string GetTargetName() => $"BoardID{BoardID}/{Name}";
}
-}
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/ChartRecorder.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/ChartRecorder.cs
deleted file mode 100644
index 225bc5c..0000000
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Models/ChartRecorder.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System.Collections.ObjectModel;
-
-public class ChartRecorder
-{
- public List Data { get; } = [];
- public ObservableCollection Window { get; } = [];
-
- private int _windowSize = 800;
- public int WindowSize
- {
- get => _windowSize;
- set
- {
- if (_windowSize != value)
- {
- _windowSize = value;
- UpdateWindow();
- }
- }
- }
-
- private int _scrollIndex;
- public int ScrollIndex
- {
- get => _scrollIndex;
- set
- {
- if (_scrollIndex != value)
- {
- _scrollIndex = value;
- UpdateWindow();
- }
- }
- }
-
- public int MaxScrollIndex => Math.Max(0, Data.Count - WindowSize);
-
- public void AddSamples(IEnumerable samples)
- {
- Data.AddRange(samples);
- UpdateWindow();
- }
-
- public void UpdateWindow()
- {
- Window.Clear();
- foreach (var v in Data.Skip(ScrollIndex).Take(WindowSize))
- Window.Add(v);
- }
-
- public void AutoScroll()
- {
- ScrollIndex = MaxScrollIndex;
- UpdateWindow();
- }
-}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/CircularBuffer.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/CircularBuffer.cs
deleted file mode 100644
index dfa2043..0000000
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Models/CircularBuffer.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using HarfBuzzSharp;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace TRION_SDK_UI.Models
-{
- internal class CircularBuffer(int board_id)
- {
- public long StartPosition { get; set; } = TrionApi.DeWeGetParam_i64(board_id, Trion.TrionCommand.BUFFER_0_START_POINTER).value;
-
- public long EndPosition { get; set; } = TrionApi.DeWeGetParam_i64(board_id, Trion.TrionCommand.BUFFER_0_END_POINTER).value;
- public int Size { get; set; } = TrionApi.DeWeGetParam_i32(board_id, Trion.TrionCommand.BUFFER_0_TOTAL_MEM_SIZE).value;
- }
-}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/CounterChannel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/CounterChannel.cs
new file mode 100644
index 0000000..540430c
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/CounterChannel.cs
@@ -0,0 +1,28 @@
+using Trion;
+using TrionApiUtils;
+
+namespace TRION_SDK_UI.Models
+{
+ public class CounterChannel : Channel
+ {
+ public CounterChannel()
+ {
+ Type = ChannelType.Counter;
+ }
+
+ public override void Activate()
+ {
+ var target = GetTargetName();
+ TrionError error;
+
+ error = TrionApi.DeWeSetParamStruct(target, "Used", "True");
+ Utils.CheckErrorCode(error, $"Failed to activate channel {Name} on board {BoardID}");
+
+ error = TrionApi.DeWeSetParamStruct(target, "Mode", Mode.Name);
+ Utils.CheckErrorCode(error, $"Failed to set mode {Mode.Name} for channel {Name} on board {BoardID}");
+
+ error = TrionApi.DeWeSetParamStruct(target, "Source_A", "Acq_Clk");
+ Utils.CheckErrorCode(error, $"Failed to set Source_A for channel {Name} on board {BoardID}");
+ }
+ }
+}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/DigitalChannel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/DigitalChannel.cs
new file mode 100644
index 0000000..0ff11a7
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/DigitalChannel.cs
@@ -0,0 +1,25 @@
+using Trion;
+using TrionApiUtils;
+
+namespace TRION_SDK_UI.Models
+{
+ public class DigitalChannel : Channel
+ {
+ public DigitalChannel()
+ {
+ Type = ChannelType.Digital;
+ }
+
+ public override void Activate()
+ {
+ string target = GetTargetName();
+ TrionError error;
+
+ error = TrionApi.DeWeSetParamStruct(target, "Used", "True");
+ Utils.CheckErrorCode(error, $"Failed to activate channel {Name} on board {BoardID}");
+
+ error = TrionApi.DeWeSetParamStruct(target, "Mode", Mode.Name);
+ Utils.CheckErrorCode(error, $"Failed to set mode {Mode.Name} for channel {Name} on board {BoardID}");
+ }
+ }
+}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/DigitalMeter.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/DigitalMeter.cs
new file mode 100644
index 0000000..2f94d2a
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/DigitalMeter.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace TRION_SDK_UI.Models;
+public class DigitalMeter : INotifyPropertyChanged
+{
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ private double _value;
+ public double Value
+ {
+ get => _value;
+ set
+ {
+ if (_value == value) return;
+ _value = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public string? Unit { get; set; }
+ public string? Label { get; set; }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/Enclosure.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/Enclosure.cs
index a2a8030..de94b4c 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Models/Enclosure.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/Enclosure.cs
@@ -1,33 +1,23 @@
-using System.Collections.ObjectModel;
+using System.Diagnostics;
using Trion;
-using TRION_SDK_UI.Models;
+using TrionApiUtils;
-
-public class Enclosure
+namespace TRION_SDK_UI.Models;
+ public class Enclosure
{
public string? Name { get; set; }
- public ObservableCollection Boards { get; set; } = [];
+ public List Boards { get; set; } = [];
public void AddBoard(int boardId)
{
var error = TrionApi.DeWeSetParam_i32(boardId, TrionCommand.OPEN_BOARD, 0);
- if (error != TrionError.NONE)
- {
- System.Diagnostics.Debug.WriteLine($"TRION_API: OpenBoard failed for board {boardId}");
- return;
- }
+ Utils.CheckErrorCode(error, "Failed to open board");
var boardPropertiesXml = TrionApi.DeWeGetParamStruct_String($"BoardID{boardId}", "boardproperties").value;
- var boardPropertiesModel = new BoardPropertyModel(boardPropertiesXml);
-
- var newBoard = new Board(boardPropertiesModel);
-
- string scanDescriptorXml = TrionApi.DeWeGetParamStruct_String($"BoardID{boardId}", "ScanDescriptor").value;
- newBoard.SetBoardProperties();
- newBoard.ReadScanDescriptor(scanDescriptorXml);
+ var boardPropertiesModel = new BoardPropertyParser(boardPropertiesXml);
+ var newBoard = boardPropertiesModel.CreateBoard(boardId, boardPropertiesXml, 50);
Boards.Add(newBoard);
}
-
public void Init(int numberOfBoards)
{
Name = TrionApi.DeWeGetParamXML_String("BoardID0/boardproperties/SystemInfo/EnclosureInfo", "Name").value;
@@ -37,4 +27,4 @@ public void Init(int numberOfBoards)
AddBoard(i);
}
}
-}
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/PlotThemeUtil.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/PlotThemeUtil.cs
new file mode 100644
index 0000000..f687fb1
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/PlotThemeUtil.cs
@@ -0,0 +1,70 @@
+using ScottPlot;
+using ScottPlot.Plottables;
+
+namespace TRION_SDK_UI.Models;
+
+public static class PlotThemeUtil
+{
+ public static void ApplyTheme(Plot plot, AppTheme theme, Crosshair? crosshair = null, VerticalLine? lockLine = null)
+ {
+ ScottPlot.Palettes.Dark palette = new();
+ // Base palette
+ ScottPlot.Color background;
+ ScottPlot.Color fontColor;
+ ScottPlot.Color crosshairColor;
+ ScottPlot.Color gridColor;
+
+ if (theme == AppTheme.Light)
+ {
+ fontColor = ScottPlot.Colors.Black;
+ crosshairColor = ScottPlot.Colors.Magenta;
+ background = ScottPlot.Colors.White;
+ gridColor = ScottPlot.Colors.Gray;
+ }
+ else if (theme == AppTheme.Dark)
+ {
+ fontColor = ScottPlot.Colors.White;
+ crosshairColor = ScottPlot.Colors.Magenta;
+ background = ScottPlot.Colors.Black;
+ gridColor = ScottPlot.Colors.Gray;
+ }
+ else // System or default
+ {
+ fontColor = palette.GetColor(0);
+ background = palette.GetColor(1);
+ crosshairColor = palette.GetColor(2);
+ gridColor = palette.GetColor(3);
+ }
+
+ // Figure & data area
+ plot.FigureBackground.Color = background;
+ plot.DataBackground.Color = background.Darken(0.1);
+
+ // Grid
+ plot.Grid.LineColor = gridColor;
+ plot.Grid.MinorLineColor = gridColor.WithAlpha(.4);
+
+ // Axes styling
+ plot.Axes.Color(fontColor);
+
+ // Legend
+ if (plot.Legend is not null)
+ {
+ plot.Legend.BackgroundColor = fontColor.WithAlpha(.8);
+ plot.Legend.FontColor = background;
+ plot.Legend.OutlineColor = background;
+ }
+
+ if (crosshair is not null)
+ {
+ crosshair.LineWidth = 2;
+ crosshair.LineColor = crosshairColor;
+ }
+
+ if (lockLine is not null)
+ {
+ lockLine.LineWidth = 2;
+ lockLine.LineStyle.Color = crosshairColor;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/RecorderGraph.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/RecorderGraph.cs
new file mode 100644
index 0000000..2273978
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/RecorderGraph.cs
@@ -0,0 +1,533 @@
+using ScottPlot;
+using ScottPlot.Maui;
+using ScottPlot.Plottables;
+using TRION_SDK_UI.POCO;
+
+namespace TRION_SDK_UI.Models
+{
+ public class RecorderGraph
+ {
+ private readonly Plot _plot;
+ private readonly Border _cursorLabel;
+ private readonly MauiPlot _mauiPlot;
+ private readonly Dictionary _lineColors = [];
+ private readonly Dictionary _loggers = [];
+ private readonly Microsoft.Maui.Controls.Label _cursorLabelText;
+ private readonly ScottPlot.Palettes.Category10 _palette = new();
+ private readonly Func _isScrollLocked;
+ private const double _cursorLabelOffsetX = 14;
+ private const double _cursorLabelOffsetY = 14;
+ private const double _viewWidthSeconds = 2.2;
+ private const double _markerGrabTolerancePx = 12;
+ private VerticalLine _lockLine;
+ private Crosshair _crosshair;
+ private Coordinates _lastCursorCoordinates;
+ private bool _hasCursor;
+ private VerticalLine? _markerA;
+ private VerticalLine? _markerB;
+ private double? _markerAx;
+ private double? _markerBx;
+ private HorizontalSpan? _calculationSpan;
+ private enum DragTarget { NONE, MARKER_A, MARKER_B }
+ private DragTarget _dragTarget = DragTarget.NONE;
+ private bool AnyDataPresent() => _loggers.Values.Any(l => l.Data.Coordinates.Count != 0);
+
+ public bool IsScrollLocked => _isScrollLocked();
+ public bool IsDraggingMarker => _dragTarget != DragTarget.NONE;
+ public Pixel LastCursorPixel { get; private set; }
+
+ public RecorderGraph(MauiPlot mauiPlot,
+ Border cursorLabel,
+ Microsoft.Maui.Controls.Label cursorLabelText,
+ VerticalLine lockLine,
+ Crosshair crossHair,
+ Func isScrollLocked)
+ {
+ _mauiPlot = mauiPlot;
+ _plot = mauiPlot.Plot;
+ _cursorLabel = cursorLabel;
+ _cursorLabelText = cursorLabelText;
+ _lockLine = lockLine;
+ _crosshair = crossHair;
+ _isScrollLocked = isScrollLocked;
+
+ SetLockCrossVisibility();
+ }
+
+ public bool TryBeginMarkerDrag(Pixel pixel)
+ {
+ if (!_markerAx.HasValue && !_markerBx.HasValue)
+ {
+ return false;
+ }
+
+ var lastRender = _plot.LastRender;
+ double? distanceA = null;
+ double? distanceB = null;
+
+ if (_markerAx.HasValue)
+ {
+ distanceA = Math.Abs(pixel.X - DoubleXToPixelX(_markerAx.Value, lastRender));
+ }
+
+ if (_markerBx.HasValue)
+ {
+ distanceB = Math.Abs(pixel.X - DoubleXToPixelX(_markerBx.Value, lastRender));
+ }
+
+ DragTarget best = DragTarget.NONE;
+ double bestDistance = _markerGrabTolerancePx;
+
+ if (distanceA.HasValue && distanceA.Value < bestDistance)
+ {
+ bestDistance = distanceA.Value;
+ best = DragTarget.MARKER_A;
+ }
+ if (distanceB.HasValue && distanceB.Value < bestDistance)
+ {
+ best = DragTarget.MARKER_B;
+ }
+
+ _dragTarget = best;
+ return DragTarget.NONE != _dragTarget;
+ }
+
+ public void UpdateMarkerDrag(Pixel pixel)
+ {
+ if (DragTarget.NONE == _dragTarget)
+ {
+ return;
+ }
+
+ var coordinates = _mauiPlot.Plot.GetCoordinates(pixel);
+
+ _lockLine.X = coordinates.X;
+
+ switch (_dragTarget)
+ {
+ case DragTarget.MARKER_A when _markerA is not null:
+ _markerA.X = coordinates.X;
+ _markerAx = coordinates.X;
+ break;
+ case DragTarget.MARKER_B when _markerB is not null:
+ _markerB.X = coordinates.X;
+ _markerBx = coordinates.X;
+ break;
+ }
+
+ UpdateCursorLabel(pixel);
+ RebuildCalculationSpan();
+ _mauiPlot.Refresh();
+ }
+
+ public void EndMarkerDrag()
+ {
+ _dragTarget = DragTarget.NONE;
+ }
+
+ public void PlaceRangeMarker()
+ {
+ if (!IsScrollLocked)
+ {
+ return;
+ }
+
+ if (_calculationSpan is not null)
+ {
+ _plot.Remove(_calculationSpan);
+ _calculationSpan = null;
+ }
+
+ ClearRangeMarkers();
+
+ var limits = _plot.Axes.GetLimits();
+ var xMin = limits.Left;
+ var xRange = _plot.Axes.Bottom.Range;
+ var currentSpan = xRange.Span;
+
+ var markerAStartPosition = xMin + (currentSpan * 1 / 3);
+ var markerBStartPosition = xMin + (currentSpan * 2 / 3);
+
+ _markerA = _plot.Add.VerticalLine(markerAStartPosition);
+ _markerA.LineWidth = 2;
+ _markerA.LineStyle.Color = ScottPlot.Colors.Cyan;
+ _markerA.LineStyle.Pattern = LinePattern.Dashed;
+ _markerAx = markerAStartPosition;
+
+ _markerB = _plot.Add.VerticalLine(markerBStartPosition);
+ _markerB.LineWidth = 2;
+ _markerB.LineStyle.Color = ScottPlot.Colors.Cyan;
+ _markerB.LineStyle.Pattern = LinePattern.Dashed;
+ _markerBx = markerBStartPosition;
+
+ RebuildCalculationSpan();
+ _mauiPlot.Refresh();
+ }
+
+ public void ClearRangeMarkers()
+ {
+ if (_markerA is not null)
+ {
+ _plot.Remove(_markerA);
+ _markerA = null;
+ }
+ if (_markerB is not null)
+ {
+ _plot.Remove(_markerB);
+ _markerB = null;
+ }
+ if (_calculationSpan != null)
+ {
+ _plot.Remove(_calculationSpan);
+ _calculationSpan = null;
+ }
+ _markerAx = null;
+ _markerBx = null;
+ _dragTarget = DragTarget.NONE;
+ _mauiPlot.Refresh();
+ }
+
+ public List ComputeRangeStats()
+ {
+ var results = new List();
+
+ if (!_markerAx.HasValue || !_markerBx.HasValue)
+ {
+ return results;
+ }
+
+ double xMin = Math.Min(_markerAx.Value, _markerBx.Value);
+ double xMax = Math.Max(_markerAx.Value, _markerBx.Value);
+
+ foreach (var (channelKey, logger) in _loggers)
+ {
+ var coordinates = logger.Data.Coordinates;
+ if (0 == coordinates.Count)
+ {
+ continue;
+ }
+
+ double min = double.MaxValue;
+ double max = double.MinValue;
+ double sum = 0;
+ int count = 0;
+
+ for (int i = 0; i < coordinates.Count; ++i)
+ {
+ var c = coordinates[i];
+ if (c.X < xMin) continue;
+ if (c.X > xMax) break;
+
+ if (c.Y < min) min = c.Y;
+ if (c.Y > max) max = c.Y;
+ sum += c.Y;
+ count++;
+ }
+
+ if (count > 0)
+ {
+ results.Add(new ChannelRangeStats(channelKey, min, max, sum / count, count));
+ }
+ }
+
+ return results;
+ }
+
+ public void SetLockLineX()
+ {
+ if (IsScrollLocked)
+ {
+ _lockLine.X = _hasCursor ? _lastCursorCoordinates.X : _crosshair.X;
+ }
+ }
+
+
+ public void SetLockCrossVisibility()
+ {
+ _crosshair ??= _mauiPlot.Plot.Add.Crosshair(0, 0);
+ _lockLine ??= _mauiPlot.Plot.Add.VerticalLine(0);
+ _cursorLabel.IsVisible = true;
+ if (false == AnyDataPresent())
+ {
+ HideLockCross();
+ return;
+ }
+ if (IsScrollLocked)
+ {
+ _lockLine.IsVisible = true;
+ _crosshair.IsVisible = false;
+ }
+ else
+ {
+ _crosshair.IsVisible = true;
+ _lockLine.IsVisible = false;
+ }
+ _mauiPlot.Refresh();
+ }
+
+ public void HideLockCross()
+ {
+ _crosshair.IsVisible = false;
+ _lockLine.IsVisible = false;
+ _cursorLabel.IsVisible = false;
+ _mauiPlot.Refresh();
+ }
+
+ public void ApplyTheme()
+ {
+ if (Application.Current is null)
+ {
+ return;
+ }
+ PlotThemeUtil.ApplyTheme(_plot, Application.Current.RequestedTheme, _crosshair, _lockLine);
+ }
+
+ public void StartAcquisition(HashSet keys)
+ {
+ _loggers.Clear();
+ _plot.Clear();
+
+ foreach (var key in keys)
+ {
+ _ = GetOrCreateDataLogger(key);
+ }
+
+ _crosshair = _plot.Add.Crosshair(0, 0);
+ _lockLine = _plot.Add.VerticalLine(0);
+ SetLockCrossVisibility();
+
+ if (Application.Current is not null)
+ {
+ PlotThemeUtil.ApplyTheme(
+ _plot,
+ Application.Current.RequestedTheme,
+ _crosshair,
+ _lockLine
+ );
+ }
+
+ if (IsScrollLocked)
+ {
+ UpdateValuesAtLockLine();
+ }
+ _mauiPlot.Refresh();
+ }
+
+ public void UpdatePointer(PointerEventArgs e)
+ {
+ var pointerPos = e.GetPosition(_mauiPlot);
+ if (pointerPos is null) return;
+
+ var cursorPixel = new Pixel(pointerPos.Value.X, pointerPos.Value.Y);
+ LastCursorPixel = cursorPixel;
+ var cursorCoordinates = _mauiPlot.Plot.GetCoordinates(cursorPixel);
+ var lastRender = _mauiPlot.Plot.LastRender;
+
+ _lastCursorCoordinates = cursorCoordinates;
+ _hasCursor = true;
+
+ if (DragTarget.NONE != _dragTarget)
+ {
+ UpdateMarkerDrag(cursorPixel);
+ return;
+ }
+
+ if (IsScrollLocked)
+ {
+ RenderLine(cursorPixel, cursorCoordinates);
+ }
+ else
+ {
+ RenderCrosshair(cursorPixel, cursorCoordinates, lastRender);
+ }
+ _mauiPlot.Refresh();
+ }
+
+ public void UpdateValuesAtLockLine()
+ {
+
+ if (!IsScrollLocked && !_lockLine.IsVisible)
+ {
+ return;
+ }
+
+ var lastRender = _plot.LastRender;
+
+ var x = _lockLine.X;
+ var queryCoordinates = new Coordinates(x, 0);
+
+ var lines = new List();
+
+ foreach (var logger in _loggers.Values)
+ {
+ if (0 == logger.Data.Coordinates.Count)
+ {
+ continue;
+ }
+
+ var dp = logger.GetNearestX(queryCoordinates, lastRender.DataRect, maxDistance: 1_000_000);
+ if (!dp.IsReal)
+ {
+ dp = logger.GetNearest(queryCoordinates, lastRender.DataRect, maxDistance: 1_000_000);
+ }
+
+ if (!dp.IsReal)
+ {
+ continue;
+ }
+
+ lines.Add($"{logger.LegendText}: {dp.Y:F3}");
+ }
+
+ if (0 == lines.Count) return;
+
+ _cursorLabelText.Text = string.Join("\n", lines) + $"\nX: {x:F3}";
+ }
+
+ public void RenderCrosshair(Pixel cursorPixel, Coordinates cursorCoordinates, RenderDetails lastRender)
+ {
+ var nearestPoint = DataPoint.None;
+ DataLogger? nearestLogger = null;
+ var bestDistance = double.MaxValue;
+
+ SetLockCrossVisibility();
+
+ foreach (var logger in _loggers.Values)
+ {
+ if (logger.Data.Coordinates.Count == 0)
+ {
+ continue;
+ }
+
+ var candidate = logger.GetNearest(cursorCoordinates, lastRender.DataRect, maxDistance: 128);
+ if (!candidate.IsReal)
+ {
+ continue;
+ }
+
+ double dx = candidate.X - cursorCoordinates.X;
+ double dy = candidate.Y - cursorCoordinates.Y;
+ double d2 = dx * dx + dy * dy;
+ if (d2 >= bestDistance)
+ {
+ continue;
+ }
+
+ bestDistance = d2;
+ nearestPoint = candidate;
+ nearestLogger = logger;
+ }
+
+ if (!nearestPoint.IsReal || nearestLogger is null)
+ {
+ return;
+ }
+
+ _crosshair.X = nearestPoint.X;
+ _crosshair.Y = nearestPoint.Y;
+
+ _cursorLabelText.Text = $"{nearestLogger.LegendText}\nX: {nearestPoint.X:F3}\nY: {nearestPoint.Y:F3}";
+
+ UpdateCursorLabel(cursorPixel);
+ }
+ public void RenderLine(Pixel cursorPixel, Coordinates cursorCoordinates)
+ {
+ _lockLine.X = cursorCoordinates.X;
+ UpdateValuesAtLockLine();
+
+ UpdateCursorLabel(cursorPixel);
+ }
+
+ private void UpdateCursorLabel(Pixel targetPixel)
+ {
+ double labelX = targetPixel.X + _cursorLabelOffsetX;
+ double labelY = targetPixel.Y + _cursorLabelOffsetY;
+
+ double maxLabelX = _mauiPlot.Width - _cursorLabel.Width - 4;
+ double maxLabelY = _mauiPlot.Height - _cursorLabel.Height - 4;
+ if (maxLabelX > 0 && labelX > maxLabelX)
+ {
+ labelX = maxLabelX;
+ }
+ if (maxLabelY > 0 && labelY > maxLabelY)
+ {
+ labelY = maxLabelY;
+ }
+
+ _cursorLabel.TranslationX = labelX;
+ _cursorLabel.TranslationY = labelY;
+ }
+
+ private DataLogger GetOrCreateDataLogger(string channelKey)
+ {
+ if (_loggers.TryGetValue(channelKey, out var existing))
+ {
+ return existing;
+ }
+ var newDataLogger = _plot.Add.DataLogger();
+ newDataLogger.ViewSlide(_viewWidthSeconds);
+ newDataLogger.LineWidth = 2;
+ newDataLogger.LegendText = channelKey;
+ newDataLogger.Color = GetColorForChannel(channelKey);
+ _loggers[channelKey] = newDataLogger;
+ return newDataLogger;
+ }
+
+ private static (double[] ys, double[] xs) ConvertSamplesToXYArrays(ReadOnlySpan samples)
+ {
+ var ys = GC.AllocateUninitializedArray(samples.Length);
+ var xs = GC.AllocateUninitializedArray(samples.Length);
+ for (int i = 0; i < samples.Length; ++i)
+ {
+ ys[i] = samples[i].Value;
+ xs[i] = samples[i].ElapsedSeconds;
+ }
+ return (ys, xs);
+ }
+
+ private ScottPlot.Color GetColorForChannel(string channelKey)
+ {
+ if (_lineColors.TryGetValue(channelKey, out var color))
+ {
+ return color;
+ }
+ var index = Math.Abs(channelKey.GetHashCode()) % 10;
+ var newColor = _palette.GetColor(index);
+ _lineColors[channelKey] = newColor;
+ return newColor;
+ }
+
+ public void AddSamples(Sample[] samples, string channelKey, bool followLatest)
+ {
+ var dataLogger = GetOrCreateDataLogger(channelKey);
+ dataLogger.ManageAxisLimits = followLatest;
+ var (ySamples, xSamples) = ConvertSamplesToXYArrays(samples);
+ dataLogger.Add(xSamples, ySamples);
+ }
+
+ private double DoubleXToPixelX(double dataX, RenderDetails lastRender)
+ {
+ var dataRect = lastRender.DataRect;
+ var xRange = _plot.Axes.Bottom.Range;
+ double fraction = (dataX - xRange.Min) / (xRange.Max - xRange.Min);
+ return dataRect.Left + fraction * dataRect.Width;
+ }
+
+ private void RebuildCalculationSpan()
+ {
+ if (_calculationSpan is not null)
+ {
+ _plot.Remove(_calculationSpan);
+ _calculationSpan = null;
+ }
+
+ if (_markerAx.HasValue && _markerBx.HasValue)
+ {
+ _calculationSpan = _plot.Add.HorizontalSpan(
+ _markerAx.Value,
+ _markerBx.Value,
+ ScottPlot.Colors.Cyan.WithAlpha(0.2));
+ }
+ }
+ }
+}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Models/ScanDescriptorDecoder.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Models/ScanDescriptorDecoder.cs
index 92b8006..b085e02 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Models/ScanDescriptorDecoder.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Models/ScanDescriptorDecoder.cs
@@ -1,8 +1,6 @@
-using System;
-using System.Collections.Generic;
using System.Xml.XPath;
-using TRION_SDK_UI.Models;
+namespace TRION_SDK_UI.Models;
public partial class ScanDescriptorDecoder
{
public class ChannelInfo
@@ -10,11 +8,13 @@ public class ChannelInfo
public string? Name { get; set; }
public string? Type { get; set; }
public uint Index { get; set; }
+ public uint SamplePos { get; set; }
public uint SampleSize { get; set; }
public uint SampleOffset { get; set; }
}
- public List Channels { get; private set; } = new();
+ public List Channels { get; private set; } = [];
+
public uint ScanSizeBytes { get; private set; }
public ScanDescriptorDecoder(string scanDescriptorXML)
@@ -24,14 +24,11 @@ public ScanDescriptorDecoder(string scanDescriptorXML)
private void ParseScanDescriptor(string scanDescriptorXML)
{
- var doc = new XPathDocument(new System.IO.StringReader(scanDescriptorXML));
+ var doc = new XPathDocument(new StringReader(scanDescriptorXML));
var nav = doc.CreateNavigator();
- var scanDescNode = nav.SelectSingleNode("ScanDescriptor/*/ScanDescription");
- if (scanDescNode == null)
- {
- throw new Exception("ScanDescriptor unexpected element");
- }
+ var scanDescNode = nav.SelectSingleNode("ScanDescriptor/*/ScanDescription")
+ ?? throw new Exception("ScanDescriptor unexpected element (ScanDescription not found).");
ScanSizeBytes = uint.Parse(scanDescNode.GetAttribute("scan_size", "")) / 8;
@@ -39,20 +36,23 @@ private void ParseScanDescriptor(string scanDescriptorXML)
while (channelNodes.MoveNext())
{
var channel = channelNodes.Current;
- if (channel == null)
+ if (channel is null)
{
continue;
}
+
var sample = channel.SelectSingleNode("Sample");
- if (sample == null)
+ if (sample is null)
{
continue;
}
+
Channels.Add(new ChannelInfo
{
Name = channel.GetAttribute("name", ""),
Type = channel.GetAttribute("type", ""),
Index = uint.Parse(channel.GetAttribute("index", "")),
+ SamplePos = uint.Parse(sample.GetAttribute("pos", "")),
SampleSize = uint.Parse(sample.GetAttribute("size", "")),
SampleOffset = uint.Parse(sample.GetAttribute("offset", ""))
});
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ChannelMode.cs b/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ChannelMode.cs
new file mode 100644
index 0000000..53ae8a0
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ChannelMode.cs
@@ -0,0 +1,11 @@
+namespace TRION_SDK_UI.POCO;
+public class ChannelMode
+{
+ public required string Name { get; set; }
+
+ public string? Unit { get; set; }
+
+ public required List Ranges { get; set; }
+ public required List Options { get; set; }
+ public string? DefaultValue { get; set; }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ChannelStats.cs b/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ChannelStats.cs
new file mode 100644
index 0000000..c08ba01
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ChannelStats.cs
@@ -0,0 +1,8 @@
+namespace TRION_SDK_UI.POCO;
+
+public readonly record struct ChannelRangeStats(
+ string ChannelKey,
+ double Min,
+ double Max,
+ double Average,
+ int SampleCount);
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ModeOption.cs b/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ModeOption.cs
new file mode 100644
index 0000000..12e6678
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/POCO/ModeOption.cs
@@ -0,0 +1,14 @@
+namespace TRION_SDK_UI.POCO
+{
+ public class ModeOption
+ {
+ public double Default { get; set; }
+ public required string Name { get; set; }
+ public required List Values { get; set; }
+ public string? Unit { get; set; }
+ public string? Programmable { get; set; }
+ public double ProgMax { get; set; }
+ public double ProgMin { get; set; }
+ public double ProgRes { get; set; }
+ }
+}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/POCO/Sample.cs b/app/TRION-SDK-UI/TRION-SDK-UI/POCO/Sample.cs
new file mode 100644
index 0000000..9c95b2c
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/POCO/Sample.cs
@@ -0,0 +1,4 @@
+namespace TRION_SDK_UI.POCO
+{
+ public readonly record struct Sample(double Value, double ElapsedSeconds);
+}
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Resources/Images/appicon.png b/app/TRION-SDK-UI/TRION-SDK-UI/Resources/Images/appicon.png
new file mode 100644
index 0000000..077432f
Binary files /dev/null and b/app/TRION-SDK-UI/TRION-SDK-UI/Resources/Images/appicon.png differ
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Services/AcquisitionManager.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Services/AcquisitionManager.cs
new file mode 100644
index 0000000..30d89a4
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Services/AcquisitionManager.cs
@@ -0,0 +1,440 @@
+using System.Buffers;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Trion;
+using TRION_SDK_UI.Models;
+using TRION_SDK_UI.POCO;
+using TrionApiUtils;
+
+namespace TRION_SDK_UI.Services;
+
+internal sealed class CircularBuffer
+{
+ private long EndPosition { get; }
+ private int Size { get; }
+ private long StartPosition { get; }
+
+ public CircularBuffer(int board_id)
+ {
+ var (err, endPos) = TrionApi.DeWeGetParam_i64(board_id, TrionCommand.BUFFER_0_END_POINTER);
+ Utils.CheckErrorCode(err, "Failed to get buffer end pointer");
+ EndPosition = endPos;
+
+ (err, var size) = TrionApi.DeWeGetParam_i32(board_id, TrionCommand.BUFFER_0_TOTAL_MEM_SIZE);
+ Utils.CheckErrorCode(err, "Failed to get buffer total mem size");
+ Size = size;
+
+ StartPosition = EndPosition - Size;
+ }
+
+ public void CheckWrapAround(ref long readPos)
+ {
+ if (readPos >= EndPosition)
+ {
+ readPos -= Size;
+ }
+ }
+}
+
+public class AcquisitionManager(Enclosure enclosure)
+{
+ private readonly Enclosure _enclosure = enclosure;
+ private List _selectedChannels = [];
+ private readonly List _acquisitionTasks = [];
+ private readonly List _ctsList = [];
+ private readonly ConcurrentDictionary> _sampleQueues = new();
+ private readonly ConcurrentDictionary _runningBoards = new();
+ private bool _isRunning = false;
+
+ private sealed record BoardRunContext(
+ Board Board,
+ List Channels,
+ int[] Offsets,
+ int[] SampleSizes,
+ string[] ChannelKeys
+ );
+
+ private void PrepareBoardRunContext(IGrouping boardGroup)
+ {
+ var board = _enclosure.Boards.FirstOrDefault(b => b.Id == boardGroup.Key);
+ if (board is null)
+ {
+ Debug.WriteLine($"Skipping board {boardGroup.Key}: board not found in enclosure.");
+ return;
+ }
+
+ var scanDescriptor = board.ScanDescriptor;
+ if (scanDescriptor is null)
+ {
+ return;
+ }
+
+ var channels = boardGroup.ToList();
+ var channelInfos = channels
+ .Select(ch => scanDescriptor.Channels.FirstOrDefault(c => c.Name == ch.Name))
+ .ToArray();
+
+ if (channelInfos.Any(ci => ci is null))
+ {
+ var missing = channels
+ .Select((ch, i) => (ch, ci: channelInfos[i]))
+ .Where(x => x.ci is null)
+ .Select(x => x.ch.Name)
+ .ToArray();
+
+ Debug.WriteLine($"Skipping board {board.Id}: ScanDescriptor missing channels [{string.Join(", ", missing)}].");
+ return;
+ }
+
+ var offsets = channelInfos.Select(ci => (int)ci!.SampleOffset / 8).ToArray();
+ var sampleSizes = channelInfos.Select(ci => (int)ci!.SampleSize).ToArray();
+ var channelKeys = channels.Select(ch => $"{ch.BoardID}/{ch.Name}").ToArray();
+
+ for (int i = 0; i < channelKeys.Length; i++)
+ {
+ _sampleQueues.TryAdd(channelKeys[i], new ConcurrentQueue());
+ }
+
+ _runningBoards[board.Id] = new BoardRunContext(board, channels, offsets, sampleSizes, channelKeys);
+ }
+
+ private void StartBoardAcquisition(BoardRunContext ctx)
+ {
+ var cts = new CancellationTokenSource();
+ _ctsList.Add(cts);
+
+ _runningBoards[ctx.Board.Id] = ctx;
+
+ var worker = new AcquisitionWorker(ctx, _sampleQueues);
+ var task = Task.Run(() => worker.RunAsync(cts.Token), cts.Token);
+
+ _acquisitionTasks.Add(task);
+ }
+
+ public async Task StartAcquisitionAsync(IEnumerable channels)
+ {
+ if (_isRunning)
+ {
+ await StopAcquisitionAsync();
+ }
+
+ _selectedChannels = [.. channels];
+ _acquisitionTasks.Clear();
+ _ctsList.Clear();
+ _sampleQueues.Clear();
+ _runningBoards.Clear();
+
+ var selectedBoardIds = _selectedChannels.Select(c => c.BoardID).Distinct();
+ var selectedBoards = _enclosure.Boards.Where(b => selectedBoardIds.Contains(b.Id)).ToList();
+
+ foreach (var board in selectedBoards)
+ {
+ board.Reset();
+ board.UpdateAcquisitionProperties();
+ board.ActivateChannels(_selectedChannels.Where(c => c.BoardID == board.Id));
+ board.Update();
+ board.RefreshScanDescriptor();
+ board.IsAcquiring = true;
+ }
+
+ var channelsByBoard = _selectedChannels.GroupBy(c => c.BoardID);
+
+ foreach (var boardGroup in channelsByBoard)
+ {
+ PrepareBoardRunContext(boardGroup);
+
+ if (_runningBoards.TryGetValue(boardGroup.Key, out var ctx))
+ {
+ StartBoardAcquisition(ctx);
+ }
+ }
+
+ _isRunning = true;
+ }
+ public Dictionary DrainSamples(int maxPerChannel = 100_00000)
+ {
+ var result = new Dictionary(_sampleQueues.Count);
+
+ foreach (var (key, q) in _sampleQueues)
+ {
+ if (q.IsEmpty)
+ {
+ continue;
+ }
+
+ var count = Math.Min(q.Count, maxPerChannel);
+
+ if (0 == count)
+ {
+ continue;
+ }
+
+ var rented = ArrayPool.Shared.Rent(count);
+ var n = 0;
+ try
+ {
+ while (n < count && q.TryDequeue(out var sample))
+ {
+ rented[n++] = sample;
+ }
+
+ if (n <= 0)
+ {
+ continue;
+ }
+
+ var arr = GC.AllocateUninitializedArray(n);
+ Array.Copy(rented, arr, n);
+ result[key] = arr;
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(rented, clearArray: false);
+ }
+ }
+
+ return result;
+ }
+
+ public async Task StopAcquisitionAsync()
+ {
+ foreach (var cts in _ctsList)
+ {
+ cts.Cancel();
+ }
+ try
+ {
+ if (_acquisitionTasks.Count > 0)
+ {
+ await Task.WhenAll(_acquisitionTasks);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Exception during StopAcquisitionAsync: {ex}");
+ }
+ finally
+ {
+ _isRunning = false;
+ }
+
+ foreach (var boardId in _runningBoards.Keys)
+ {
+ var error = TrionApi.DeWeSetParam_i32(boardId, TrionCommand.STOP_ACQUISITION, 0);
+ Utils.CheckErrorCode(error, $"Failed to stop acquisition on board {boardId}");
+
+ if (_runningBoards.TryGetValue(boardId, out var ctx))
+ {
+ ctx.Board.IsAcquiring = false;
+ }
+ }
+
+ _acquisitionTasks.Clear();
+ _ctsList.Clear();
+ _runningBoards.Clear();
+ }
+
+ private sealed class AcquisitionWorker(
+ BoardRunContext context,
+ ConcurrentDictionary> sampleQueues)
+ {
+ private readonly Board _board = context.Board;
+ private readonly List _channels = context.Channels;
+ private readonly int[] _offsets = context.Offsets;
+ private readonly int[] _sampleSizes = context.SampleSizes;
+ private readonly string[] _channelKeys = context.ChannelKeys;
+ private readonly List[] _channelBuffers = [.. context.Channels.Select(_ => new List())];
+
+ public async Task RunAsync(CancellationToken token)
+ {
+ if (_board.ScanDescriptor is null)
+ {
+ Debug.WriteLine($"ScanDescriptor is null for board {_board.Id}. Acquisition loop will exit.");
+ return;
+ }
+ var scanSize = (int)_board.ScanDescriptor.ScanSizeBytes;
+
+ (var error, var adcDelay) = TrionApi.DeWeGetParam_i32(_board.Id, TrionCommand.BOARD_ADC_DELAY);
+ Utils.CheckErrorCode(error, $"Failed to get ADC Delay {_board.Id}");
+
+ error = TrionApi.DeWeSetParam_i32(_board.Id, TrionCommand.START_ACQUISITION, 0);
+ Utils.CheckErrorCode(error, $"Failed start acquisition {_board.Id}");
+
+ CircularBuffer buffer = new(_board.Id);
+ long sampleIndex = 0;
+
+ (error, var hwReadPos) = TrionApi.DeWeGetParam_i64(_board.Id, TrionCommand.BUFFER_0_ACT_SAMPLE_POS);
+ Utils.CheckErrorCode(error, $"Failed to get initial sample position {_board.Id}");
+
+ buffer.CheckWrapAround(ref hwReadPos);
+
+ try
+ {
+ while (!token.IsCancellationRequested)
+ {
+ (error, var rawAvailable) = TrionApi.DeWeGetParam_i32(_board.Id, TrionCommand.BUFFER_0_AVAIL_NO_SAMPLE);
+ Utils.CheckErrorCode(error, $"Failed to get available samples {_board.Id}");
+
+ if (rawAvailable <= adcDelay)
+ {
+ await Task.Delay(1, token);
+ continue;
+ }
+
+ var processableSamples = rawAvailable - adcDelay;
+ var basePtr = hwReadPos;
+ var analogPtr = hwReadPos + ((long)adcDelay * scanSize);
+
+ for (int c = 0; c < _channelBuffers.Length; ++c)
+ {
+ var list = _channelBuffers[c];
+ list.Clear();
+ if (list.Capacity < processableSamples)
+ {
+ list.Capacity = processableSamples;
+ }
+ }
+
+ for (int i = 0; i < processableSamples; ++i)
+ {
+ buffer.CheckWrapAround(ref basePtr);
+ buffer.CheckWrapAround(ref analogPtr);
+
+ ProcessScan(basePtr, analogPtr);
+
+ basePtr += scanSize;
+ analogPtr += scanSize;
+ }
+
+ hwReadPos = basePtr;
+ buffer.CheckWrapAround(ref hwReadPos);
+
+ for (int i = 0; i < processableSamples; ++i)
+ {
+ var elapsedSeconds = (double)(sampleIndex + i) / _board.SamplingRate;
+ for (int c = 0; c < _channelKeys.Length; ++c)
+ {
+ sampleQueues[_channelKeys[c]].Enqueue(new Sample(_channelBuffers[c][i], elapsedSeconds));
+ }
+ }
+ sampleIndex += processableSamples;
+
+ TrionApi.DeWeSetParam_i32(_board.Id, TrionCommand.BUFFER_0_FREE_NO_SAMPLE, processableSamples);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected during shutdown
+ }
+ }
+
+ private void ProcessScan(long readPos, long analogPos)
+ {
+ for (int c = 0; c < _channels.Count; ++c)
+ {
+ var channel = _channels[c];
+ var samplePos = (nint)(readPos + _offsets[c]);
+ var analogSamplePos = (nint)(analogPos + _offsets[c]);
+ var sampleSize = _sampleSizes[c];
+
+ var value = ReadChannelValue(channel, samplePos, analogSamplePos, sampleSize);
+
+ _channelBuffers[c].Add(value);
+ }
+ }
+
+ private static double ReadChannelValue(Channel channel, nint samplePos, nint analogPos, int sampleSize)
+ {
+ if (channel.Type == Channel.ChannelType.Digital)
+ {
+ return ReadDiscreteSample(samplePos);
+ }
+ else if (channel.Type == Channel.ChannelType.Analog)
+ {
+ double range = 1.0;
+ if (double.TryParse(channel.Range, out var parsedRange))
+ {
+ range = parsedRange;
+ }
+ return ReadAnalogSample(analogPos, sampleSize, range);
+ }
+ else if (channel.Type == Channel.ChannelType.Counter)
+ {
+ return ReadCounterSample(samplePos, sampleSize);
+ }
+ else
+ {
+ throw new NotSupportedException($"Unsupported channel type: {channel.Type}");
+ }
+ }
+
+ private static uint ReadDiscreteSample(nint samplePos)
+ {
+ int raw = Marshal.ReadByte(samplePos);
+ return (uint)(raw & 0x1);
+ }
+
+ private unsafe static double ReadCounterSample(nint samplePos, int sampleSize)
+ {
+ if (sampleSize == 32)
+ {
+ return (double)System.Runtime.CompilerServices.Unsafe.ReadUnaligned((byte*)samplePos);
+ }
+ else if (sampleSize == 24)
+ {
+ var ptr = (byte*)samplePos;
+ var val = (uint)(ptr[0] | (ptr[1] << 8) | (ptr[2] << 16));
+ return (double)val;
+ }
+ else
+ {
+ throw new NotSupportedException($"Unsupported counter sample size: {sampleSize}");
+ }
+ }
+
+ private unsafe static double ReadAnalogSample(nint samplePos, int sampleSize, double scale)
+ {
+ int raw;
+ switch (sampleSize)
+ {
+ case 16:
+ {
+ var test = (byte*)samplePos;
+ var b0 = test[0];
+ var b1 = test[1];
+ raw = b0 | (b1 << 8);
+ if ((raw & 0x8000) != 0)
+ {
+ raw |= unchecked((int)0xFFFF0000);
+ }
+ break;
+ }
+ case 24:
+ {
+ var test = (byte*)samplePos;
+ var b0 = test[0];
+ var b1 = test[1];
+ var b2 = test[2];
+ raw = b0 | (b1 << 8) | (b2 << 16);
+ if ((raw & 0x800000) != 0)
+ {
+ raw |= unchecked((int)0xFF000000);
+ }
+ break;
+ }
+ case 32:
+ {
+ raw = System.Runtime.CompilerServices.Unsafe.ReadUnaligned((byte*)samplePos);
+ break;
+ }
+ default:
+ throw new NotSupportedException($"Unsupported sample size: {sampleSize}");
+ }
+
+ int signBit = 1 << (sampleSize - 1);
+ var value = (double)raw / (double)(signBit - 1) * scale;
+ return value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Services/BoardService.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Services/BoardService.cs
new file mode 100644
index 0000000..4e49b22
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Services/BoardService.cs
@@ -0,0 +1,13 @@
+using TRION_SDK_UI.Models;
+
+public class BoardService
+{
+ public void SetupBoard(Board board, IEnumerable channels)
+ {
+ board.Reset();
+ board.SetAcquisitionProperties();
+ board.ActivateChannels(channels);
+ board.Update();
+ board.RefreshScanDescriptor();
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Services/DataAcquisitionService.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Services/DataAcquisitionService.cs
new file mode 100644
index 0000000..de72fc5
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Services/DataAcquisitionService.cs
@@ -0,0 +1,164 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Trion;
+using TRION_SDK_UI.Models;
+using TrionApiUtils;
+
+public class DataAcquisitionService
+{
+ public Task StartAcquisition(Board board, List channels, Action> onSamplesReceived, CancellationToken token)
+ {
+ // Prepare required arguments for AcquireDataLoop
+ int[] offsets = channels.Select(c => (int)c.SampleOffset).ToArray();
+ int[] sampleSizes = channels.Select(c => (int)c.SampleSize).ToArray();
+ string[] channelKeys = channels.Select(c => c.Name ?? $"Channel_{c.Index}").ToArray();
+
+ // Move AcquireDataLoop logic here
+ return Task.Run(() => AcquireDataLoop(board, channels, offsets, sampleSizes, channelKeys, onSamplesReceived, token), token);
+ }
+
+ private static async Task AcquireDataLoop(
+ Board board,
+ List selectedChannels,
+ int[] offsets,
+ int[] sampleSizes,
+ string[] channelKeys,
+ Action> onSamplesReceived,
+ CancellationToken token)
+ {
+ Debug.WriteLine($"TEST: AcquireDataLoop started for Board ID: {board.Id} with channels: {string.Join(", ", selectedChannels.Select(c => c.Name))}");
+ var scanSize = (int)board.ScanSizeBytes;
+ var polling_interval = (int)(board.BufferBlockSize / (double)board.SamplingRate * 1000);
+
+ TrionError error;
+ (error, var adc_delay) = TrionApi.DeWeGetParam_i32(board.Id, TrionCommand.BOARD_ADC_DELAY);
+ Utils.CheckErrorCode(error, $"Failed to get ADC Delay {board.Id}");
+
+ error = TrionApi.DeWeSetParam_i32(board.Id, TrionCommand.START_ACQUISITION, 0);
+ Utils.CheckErrorCode(error, $"Failed start acquisition {board.Id}");
+
+ CircularBuffer buffer = new(board.Id);
+ int available_samples = 0;
+
+ while (!token.IsCancellationRequested)
+ {
+ (error, available_samples) = TrionApi.DeWeGetParam_i32(board.Id, TrionCommand.BUFFER_0_AVAIL_NO_SAMPLE);
+ Utils.CheckErrorCode(error, $"Failed to get available samples {board.Id}, {available_samples}");
+ if (available_samples <= 0)
+ {
+ //await Task.Delay(polling_interval, token);
+ continue;
+ }
+
+ available_samples -= adc_delay;
+ if (available_samples <= 0)
+ {
+ //await Task.Delay(polling_interval, token);
+ continue;
+ }
+
+ (error, var read_pos) = TrionApi.DeWeGetParam_i64(board.Id, TrionCommand.BUFFER_0_ACT_SAMPLE_POS);
+ Utils.CheckErrorCode(error, $"Failed to get actual sample position {board.Id}");
+
+ read_pos += adc_delay * scanSize;
+
+ var sampleLists = new List[selectedChannels.Count];
+ for (int c = 0; c < selectedChannels.Count; ++c)
+ {
+ sampleLists[c] = new List(available_samples);
+ }
+
+ for (int i = 0; i < available_samples; ++i)
+ {
+ if (read_pos >= buffer.EndPosition)
+ {
+ read_pos -= buffer.Size;
+ }
+
+ for (int c = 0; c < selectedChannels.Count; ++c)
+ {
+ var channel = selectedChannels[c];
+ nint samplePos = (nint)(read_pos + offsets[c]);
+ int sampleSize = sampleSizes[c];
+ if (channel.Type == Channel.ChannelType.Digital)
+ {
+ uint value = ReadDiscreteSample(samplePos);
+ sampleLists[c].Add(value);
+ continue;
+ }
+ else if (channel.Type == Channel.ChannelType.Analog)
+ {
+ double value = ReadSample(samplePos, sampleSize);
+ sampleLists[c].Add(value);
+ continue;
+ }
+ else
+ {
+ throw new NotSupportedException($"Unsupported channel type: {channel.Type}");
+ }
+ }
+ read_pos += scanSize;
+ }
+
+ TrionApi.DeWeSetParam_i32(board.Id, TrionCommand.BUFFER_0_FREE_NO_SAMPLE, available_samples);
+
+ for (int c = 0; c < selectedChannels.Count; ++c)
+ {
+ onSamplesReceived(channelKeys[c], sampleLists[c]);
+ }
+ }
+ TrionApi.DeWeSetParam_i32(board.Id, TrionCommand.BUFFER_0_FREE_NO_SAMPLE, available_samples);
+ Utils.CheckErrorCode(TrionApi.DeWeSetParam_i32(board.Id, TrionCommand.STOP_ACQUISITION, 0), $"Failed to stop acquisition {board.Id}");
+ }
+
+ private static uint ReadDiscreteSample(nint samplePos)
+ {
+ int raw = Marshal.ReadByte(samplePos);
+ return (uint)(raw & 0x1); // only the least significant bit
+ }
+
+
+ private static double ReadSample(nint samplePos, int sampleSize)
+ {
+ // little endian (i guess could be different, maybe check xml)
+ int raw;
+ switch (sampleSize)
+ {
+ case 16:
+ {
+ byte b0 = Marshal.ReadByte(samplePos);
+ byte b1 = Marshal.ReadByte(samplePos + 1);
+ raw = b0 | (b1 << 8);
+ if ((raw & 0x8000) != 0)
+ {
+ raw |= unchecked((int)0xFFFF0000);
+ }
+ break;
+ }
+ case 24:
+ {
+ byte b0 = Marshal.ReadByte(samplePos);
+ byte b1 = Marshal.ReadByte(samplePos + 1);
+ byte b2 = Marshal.ReadByte(samplePos + 2);
+ raw = b0 | (b1 << 8) | (b2 << 16);
+ if ((raw & 0x800000) != 0)
+ {
+ raw |= unchecked((int)0xFF000000);
+ }
+ break;
+ }
+ case 32:
+ {
+ raw = Marshal.ReadInt32(samplePos);
+ // no sign extension needed already 32 bits
+ break;
+ }
+ default:
+ throw new NotSupportedException($"Unsupported sample size: {sampleSize}");
+ }
+ int signBit = 1 << (sampleSize - 1);
+ double value = (double)raw / (double)(signBit - 1) * 10.0;
+ return value;
+ }
+
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Services/HardwareService.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Services/HardwareService.cs
new file mode 100644
index 0000000..1b9e75e
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Services/HardwareService.cs
@@ -0,0 +1,68 @@
+using System.Diagnostics;
+using Trion;
+using TRION_SDK_UI.Models;
+using TrionApiUtils;
+
+namespace TRION_SDK_UI.Services;
+
+public sealed class HardwareService : IDisposable
+{
+ private bool _initialized;
+ private static readonly string _ipAddress = "10.0.0.100";
+ private static readonly string _mask = "255.255.0.0";
+
+
+ public Enclosure Enclosure { get; } = new Enclosure { Name = string.Empty, Boards = [] };
+
+ public HardwareInitResult Initialize()
+ {
+ API.DeWeConfigure(API.Backend.TRIONET);
+
+ var error = TrionApi.DeWeSetParamStruct("trionetapi/config", "Network/IPV4/LocalIP", _ipAddress);
+ Utils.CheckErrorCode(error, "Failed to set local IP address");
+
+ error = TrionApi.DeWeSetParamStruct("trionetapi/config", "Network/IPV4/NetMask", _mask);
+ Utils.CheckErrorCode(error, "Failed to set subnet mask");
+
+ var numberOfBoards = TrionApi.Initialize();
+ _initialized = true;
+
+ if (numberOfBoards == 0)
+ {
+ return new HardwareInitResult(0, false);
+ }
+
+ var isSimulated = numberOfBoards < 0;
+ var boardCount = Math.Abs(numberOfBoards);
+
+ Enclosure.Init(boardCount);
+
+ return new HardwareInitResult(boardCount, isSimulated);
+ }
+
+ public IReadOnlyList GetChannels(params Channel.ChannelType[] types)
+ {
+ var result = new List();
+ foreach (var board in Enclosure.Boards)
+ {
+ result.AddRange(board.Channels.Where(c => types.Contains(c.Type)));
+ }
+ return result;
+ }
+
+ public void Dispose()
+ {
+ if (!_initialized)
+ {
+ return;
+ }
+
+ TrionApi.DeWeSetParam_i32(0, TrionCommand.CLOSE_BOARD_ALL, 0);
+ TrionApi.Uninitialize();
+ _initialized = false;
+
+ Debug.WriteLine("HardwareService disposed — driver shut down.");
+ }
+}
+
+public readonly record struct HardwareInitResult(int BoardCount, bool IsSimulated);
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/TRION-SDK-UI.csproj b/app/TRION-SDK-UI/TRION-SDK-UI/TRION-SDK-UI.csproj
index 908a444..df95fd4 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/TRION-SDK-UI.csproj
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/TRION-SDK-UI.csproj
@@ -1,5 +1,9 @@
+
+ true
+
+
net8.0
true
@@ -28,11 +32,8 @@
-
-
-
-
-
+
+
@@ -40,10 +41,20 @@
+
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/BaseViewModel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/BaseViewModel.cs
index fbc9fdb..b413d12 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/BaseViewModel.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/BaseViewModel.cs
@@ -1,12 +1,17 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
+namespace TRION_SDK_UI.ViewModels;
public class BaseViewModel : INotifyPropertyChanged
{
- public event PropertyChangedEventHandler PropertyChanged;
+ public event PropertyChangedEventHandler? PropertyChanged;
- protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
+ protected static Task ShowAlertAsync(string title, string message, string ok = "OK")
+ {
+ return MainThread.InvokeOnMainThreadAsync(() => (Application.Current?.MainPage?.DisplayAlert(title, message, ok)) ?? Task.CompletedTask);
+ }
}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/BoardDetailViewModel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/BoardDetailViewModel.cs
new file mode 100644
index 0000000..1bf145a
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/BoardDetailViewModel.cs
@@ -0,0 +1,336 @@
+using System.Collections.ObjectModel;
+using System.Windows.Input;
+using TRION_SDK_UI.Models;
+using TrionApiUtils;
+
+namespace TRION_SDK_UI.ViewModels;
+
+public sealed class BoardDetailViewModel : BaseViewModel
+{
+ public Board Board { get; }
+ public string Title => $"Board {Board.Id} - {Board.Name}";
+
+ public ObservableCollection ResolutionAIValues { get; } = [];
+ public ObservableCollection OperationModes { get; } = [];
+ public ObservableCollection ExternalTriggerValues { get; } = [];
+ public ObservableCollection ExternalClockValues { get; } = [];
+ public ObservableCollection ProposedSampleRates { get; } = [];
+ public ObservableCollection ProposedDividerValues { get; } = [];
+
+ public bool HasProposedSampleRates => ProposedSampleRates.Count > 0;
+ public bool IsSampleRateProgrammable { get; }
+ public int SampleRateMin { get; }
+ public int SampleRateMax { get; }
+ public string SampleRateRangeHint => IsSampleRateProgrammable ? $"Range: {SampleRateMin} - {SampleRateMax} Hz" : string.Empty;
+
+ public bool HasProposedDividerValues => ProposedDividerValues.Count > 0;
+ public bool HasSampleRateDivider { get; }
+ public int DividerMin { get; }
+ public int DividerMax { get; }
+ public string DividerRangeHint => HasSampleRateDivider ? $"Range: {DividerMin} - {DividerMax}" : string.Empty;
+
+ private bool _suppressSync;
+
+ private string _sampleRateText = string.Empty;
+ public string SampleRateText
+ {
+ get => _sampleRateText;
+ set
+ {
+ if (_sampleRateText == value) return;
+ _sampleRateText = value;
+ OnPropertyChanged();
+ ValidateSampleRate();
+
+ if (!HasSampleRateError)
+ {
+ CommitPropertyChange(() =>
+ {
+ if (int.TryParse(value, out var rate))
+ {
+ Board.SamplingRate = rate;
+ Board.UpdateBuffer(true);
+ }
+ });
+ }
+ }
+ }
+
+ private string? _sampleRateError;
+ public string? SampleRateError
+ {
+ get => _sampleRateError;
+ set { if (_sampleRateError != value) { _sampleRateError = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasSampleRateError)); } }
+ }
+ public bool HasSampleRateError => !string.IsNullOrEmpty(SampleRateError);
+
+ private string _dividerText = string.Empty;
+ public string DividerText
+ {
+ get => _dividerText;
+ set
+ {
+ if (_dividerText == value) return;
+ _dividerText = value;
+ OnPropertyChanged();
+ ValidateDivider();
+
+ if (!HasDividerError)
+ {
+ CommitPropertyChange(() =>
+ {
+ if (int.TryParse(value, out var div))
+ {
+ Board.SetAcqProp("SampleRateDivider", div.ToString());
+ Board.SampleRateDivider = div;
+ }
+ });
+ }
+ }
+ }
+
+ private string? _dividerError;
+ public string? DividerError
+ {
+ get => _dividerError;
+ set { if (_dividerError != value) { _dividerError = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasDividerError)); } }
+ }
+ public bool HasDividerError => !string.IsNullOrEmpty(DividerError);
+
+ private string? _selectedOperationMode;
+ public string? SelectedOperationMode
+ {
+ get => _selectedOperationMode;
+ set
+ {
+ if (_selectedOperationMode == value) return;
+ _selectedOperationMode = value;
+ OnPropertyChanged();
+
+ if (value != null)
+ {
+ CommitPropertyChange(() =>
+ {
+ Board.SetAcqProp("OperationMode", value);
+ Board.OperationMode = value;
+ });
+ }
+ }
+ }
+
+ private string? _externalTrigger;
+ public string? ExternalTrigger
+ {
+ get => _externalTrigger;
+ set
+ {
+ if (_externalTrigger == value) return;
+ _externalTrigger = value;
+ OnPropertyChanged();
+
+ if (value != null)
+ {
+ CommitPropertyChange(() =>
+ {
+ Board.SetAcqProp("ExtTrigger", value);
+ Board.ExternalTrigger = value;
+ });
+ }
+ }
+ }
+
+ private string? _externalClock;
+ public string? ExternalClock
+ {
+ get => _externalClock;
+ set
+ {
+ if (_externalClock == value) return;
+ _externalClock = value;
+ OnPropertyChanged();
+
+ if (value != null)
+ {
+ CommitPropertyChange(() =>
+ {
+ Board.SetAcqProp("ExtClk", value);
+ Board.ExternalClock = value;
+ });
+ }
+ }
+ }
+
+ private string? _resolutionAI;
+ public string? ResolutionAI
+ {
+ get => _resolutionAI;
+ set
+ {
+ if (_resolutionAI == value) return;
+ _resolutionAI = value;
+ OnPropertyChanged();
+
+ if (value != null)
+ {
+ CommitPropertyChange(() =>
+ {
+ Board.SetAcqProp("ResolutionAI", value);
+ Board.ResolutionAI = value;
+
+ var (error, currentValue) = TrionApi.DeWeGetParamStruct_String($"BoardID{Board.Id}/AcqProp", "ResolutionAI");
+ Utils.CheckErrorCode(error, $"Failed to get ResolutionAI for board {Board.Id}");
+ if (currentValue != value)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ _ = ShowAlertAsync("ResolutionAI Error", "ResolutionAI not set correctly."));
+ }
+ });
+ }
+ }
+ }
+
+ public ICommand SelectProposedSampleRateCommand { get; }
+ public ICommand SelectProposedDividerCommand { get; }
+ public ICommand CloseCommand { get; }
+ public event EventHandler? CloseRequested;
+
+ public BoardDetailViewModel(Board board)
+ {
+ Board = board ?? throw new ArgumentNullException(nameof(board));
+
+ var parser = board.BoardProperties;
+
+ foreach (var mode in parser.GetAvailableValuesFromString("OperationMode"))
+ {
+ OperationModes.Add(mode);
+ }
+
+ foreach (var trig in parser.GetAvailableValuesFromString("ExtTrigger"))
+ {
+ ExternalTriggerValues.Add(trig);
+ }
+
+ foreach (var clk in parser.GetAvailableValuesFromString("ExtClk"))
+ {
+ ExternalClockValues.Add(clk);
+ }
+
+ foreach (var res in parser.GetAvailableValuesFromString("ResolutionAI"))
+ {
+ ResolutionAIValues.Add(res);
+ }
+
+ var (srProg, srMin, srMax, srRates) = parser.GetSampleRateCapabilities();
+ IsSampleRateProgrammable = srProg;
+ SampleRateMin = srMin;
+ SampleRateMax = srMax;
+
+ foreach (var rate in srRates)
+ {
+ ProposedSampleRates.Add(rate);
+ }
+
+ var (divMin, divMax, divProposed) = parser.GetDividerCapabilities();
+ HasSampleRateDivider = divMax > 0;
+ DividerMin = divMin;
+ DividerMax = divMax;
+
+ foreach (var val in divProposed)
+ {
+ ProposedDividerValues.Add(val);
+ }
+
+ SelectProposedSampleRateCommand = new Command(rate => SampleRateText = rate.ToString());
+ SelectProposedDividerCommand = new Command(val => DividerText = val.ToString());
+ CloseCommand = new Command(() => CloseRequested?.Invoke(this, EventArgs.Empty));
+
+ RefreshBoardState();
+ }
+
+ private void CommitPropertyChange(Action hardwareUpdateAction)
+ {
+ if (_suppressSync) return;
+
+ _ = CommitPropertyChangeInternal(hardwareUpdateAction);
+ }
+
+ private async Task CommitPropertyChangeInternal(Action hardwareUpdateAction)
+ {
+ if (Board.IsAcquiring)
+ {
+ RefreshBoardState();
+ await ShowAlertAsync("Action Failed", "Cannot change settings while acquisition is running.");
+ return;
+ }
+
+ try
+ {
+ await Task.Run(hardwareUpdateAction);
+
+ RefreshBoardState();
+ }
+ catch (Exception ex)
+ {
+ await ShowAlertAsync("Update Failed", ex.Message);
+ RefreshBoardState();
+ }
+ }
+
+ private void RefreshBoardState()
+ {
+ _suppressSync = true;
+ try
+ {
+ SelectedOperationMode = Board.OperationMode;
+ SampleRateText = Board.SamplingRate.ToString();
+ DividerText = Board.SampleRateDivider.ToString();
+ ExternalTrigger = Board.ExternalTrigger;
+ ExternalClock = Board.ExternalClock;
+ ResolutionAI = Board.ResolutionAI;
+ }
+ finally
+ {
+ _suppressSync = false;
+ }
+ }
+
+ private void ValidateSampleRate()
+ {
+ if (!int.TryParse(SampleRateText, out var value))
+ {
+ SampleRateError = "Must be a valid number";
+ return;
+ }
+
+ if (IsSampleRateProgrammable && (value < SampleRateMin || value > SampleRateMax))
+ {
+ SampleRateError = $"Must be between {SampleRateMin} and {SampleRateMax}";
+ return;
+ }
+
+ SampleRateError = null;
+ }
+
+ private void ValidateDivider()
+ {
+ if (!HasSampleRateDivider)
+ {
+ DividerError = null;
+ return;
+ }
+
+ if (!int.TryParse(DividerText, out var value))
+ {
+ DividerError = "Must be a valid number";
+ return;
+ }
+
+ if (value < DividerMin || value > DividerMax)
+ {
+ DividerError = $"Must be between {DividerMin} and {DividerMax}";
+ return;
+ }
+
+ DividerError = null;
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/ChannelDetailViewModel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/ChannelDetailViewModel.cs
new file mode 100644
index 0000000..b7d10b0
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/ChannelDetailViewModel.cs
@@ -0,0 +1,191 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Windows.Input;
+using TRION_SDK_UI.Models;
+
+namespace TRION_SDK_UI.ViewModels;
+
+public sealed class ChannelDetailViewModel : BaseViewModel
+{
+ public Channel Channel { get; }
+
+ public ObservableCollection Modes { get; } = [];
+ public ObservableCollection Ranges { get; } = [];
+ public string Title { get; set; }
+
+ public event EventHandler? CloseRequested;
+
+ private string? _selectedMode;
+ public string? SelectedMode
+ {
+ get => _selectedMode;
+ set
+ {
+ if (_selectedMode == value) return;
+ _selectedMode = value;
+ OnPropertyChanged();
+ OnSelectedModeChanged();
+ }
+ }
+
+ private string? _selectedRange;
+ public string? SelectedRange
+ {
+ get => _selectedRange;
+ set { if (_selectedRange != value) { _selectedRange = value; OnPropertyChanged(); } }
+ }
+
+ private bool _isSelected;
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set { if (_isSelected != value) { _isSelected = value; OnPropertyChanged(); } }
+ }
+
+ public ICommand ApplyCommand { get; }
+
+ private bool _suppressSync;
+
+ public ChannelDetailViewModel(Channel channel)
+ {
+ ArgumentNullException.ThrowIfNull(channel);
+ ArgumentNullException.ThrowIfNull(channel.Mode);
+ Title = $"{channel.BoardName}/{channel.Name}";
+
+ Channel = channel;
+
+ if (Channel.ModeList is { Count: > 0 })
+ {
+ foreach (var m in Channel.ModeList)
+ {
+ Modes.Add(m.Name);
+ }
+
+ SelectedMode = channel.Mode?.Name;
+
+ if (channel.Mode?.Ranges is { Count: > 0 })
+ {
+ foreach (var range in channel.Mode.Ranges)
+ {
+ if (Ranges.Contains(range)) continue;
+ Ranges.Add(range);
+ }
+ }
+ }
+ IsSelected = channel.IsSelected;
+
+ channel.PropertyChanged += ChannelOnPropertyChanged;
+
+ ApplyCommand = new Command(async () => await ApplyAsync());
+ }
+
+ private void ChannelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (_suppressSync) return;
+
+ switch (e.PropertyName)
+ {
+ case nameof(Channel.IsSelected):
+ IsSelected = Channel.IsSelected;
+ break;
+ case nameof(Channel.Mode):
+ if (!string.Equals(SelectedMode, Channel.Mode?.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ SelectedMode = Channel.Mode?.Name;
+ Ranges.Clear();
+ if (Channel.Mode?.Ranges is { Count: > 0 })
+ {
+ foreach (var r in Channel.Mode.Ranges)
+ {
+ Ranges.Add(r);
+ }
+ }
+ }
+ break;
+ case nameof(Channel.Unit):
+ break;
+ }
+ }
+ private void OnSelectedModeChanged()
+ {
+ var mode = Channel.ModeList.FirstOrDefault(m => string.Equals(m.Name, _selectedMode, StringComparison.OrdinalIgnoreCase));
+
+ Ranges.Clear();
+ if (mode?.Ranges is { Count: > 0 })
+ {
+ foreach (var r in mode.Ranges)
+ {
+ if (string.IsNullOrWhiteSpace(r)) continue;
+ Ranges.Add(r);
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(SelectedRange) && Ranges.Contains(SelectedRange))
+ {
+ return;
+ }
+
+ if (!string.IsNullOrWhiteSpace(Channel.Range) && Ranges.Contains(Channel.Range))
+ {
+ SelectedRange = Channel.Range;
+ return;
+ }
+
+ if (mode != null && int.TryParse(mode.DefaultValue, out var idx) && idx >= 0 && idx < Ranges.Count)
+ {
+ SelectedRange = Ranges[idx];
+ }
+ else
+ {
+ SelectedRange = Ranges.FirstOrDefault();
+ }
+ }
+
+ private async Task ApplyAsync()
+ {
+ try
+ {
+ _suppressSync = true;
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(SelectedMode))
+ {
+ var newMode = Channel.ModeList.FirstOrDefault(m => string.Equals(m.Name, SelectedMode, StringComparison.OrdinalIgnoreCase));
+
+ if (newMode is not null && !ReferenceEquals(newMode, Channel.Mode))
+ {
+ Channel.Mode = newMode;
+ if (!string.IsNullOrWhiteSpace(newMode.Unit))
+ {
+ Channel.Unit = newMode.Unit!;
+ }
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(SelectedRange))
+ {
+ if (Channel.Mode?.Ranges?.Contains(SelectedRange) == true)
+ {
+ Channel.Range = SelectedRange;
+ }
+ else if (Channel.Mode?.Ranges?.Count > 0)
+ {
+ Channel.Range = Channel.Mode.Ranges[0];
+ }
+ }
+
+ Channel.IsSelected = IsSelected;
+ }
+ finally
+ {
+ _suppressSync = false;
+ }
+
+ CloseRequested?.Invoke(this, EventArgs.Empty);
+ }
+ catch (Exception ex)
+ {
+ await ShowAlertAsync("Apply Failed", ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/MainViewModel.cs b/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/MainViewModel.cs
index 7037e50..ce8865c 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/MainViewModel.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/ViewModels/MainViewModel.cs
@@ -1,253 +1,434 @@
-using LiveChartsCore;
-using LiveChartsCore.SkiaSharpView;
-using System.Collections.ObjectModel;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
using System.Windows.Input;
-using Trion;
using TRION_SDK_UI.Models;
-using System.Runtime.InteropServices;
-using System.Diagnostics;
+using System.ComponentModel;
+using TRION_SDK_UI.Services;
+using TRION_SDK_UI.POCO;
+namespace TRION_SDK_UI.ViewModels;
public class MainViewModel : BaseViewModel, IDisposable
{
- public ChartRecorder Recorder { get; } = new();
-
- public ISeries[] MeasurementSeries { get; set; } = [];
-
- public ObservableCollection ChartWindowData => Recorder.Window;
- public int WindowSize
+ public ObservableCollection DigitalMeters { get; } = [];
+ public ObservableCollection Channels { get; } = [];
+ public ObservableCollection LogMessages { get; } = [];
+ public ICommand? StartAcquisitionCommand { get; private set; }
+ public ICommand? StopAcquisitionCommand { get; private set; }
+ public ICommand? LockScrollingCommand { get; private set; }
+ public ICommand? ToggleThemeCommand { get; private set; }
+ public ICommand? CopyChannelPathCommand { get; private set; }
+ public ICommand? SelectOnlyChannelCommand { get; private set; }
+ public ICommand? SelectAllOnBoardCommand { get; private set; }
+ public ICommand? DeselectAllOnBoardCommand { get; private set; }
+ public ICommand? OpenChannelWindowCommand { get; private set; }
+ public ICommand? OpenBoardWindowCommand { get; private set; }
+ public ICommand? MaxCalcCommand { get; private set; }
+ public ICommand? PlaceMarkerCommand { get; private set; }
+ public ICommand? ClearMarkersCommand { get; private set; }
+ public bool IsAcquiring
{
- get => Recorder.WindowSize;
+ get => _isAcquiring;
set
{
- if (Recorder.WindowSize != value)
+ if (_isAcquiring != value)
{
- Recorder.WindowSize = value;
- OnPropertyChanged(nameof(WindowSize));
- OnPropertyChanged(nameof(MaxScrollIndex));
+ _isAcquiring = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(IsNotAcquiring));
+ OnPropertyChanged(nameof(CalcEnabled));
}
}
}
- public int ScrollIndex
+
+ public bool IsNotAcquiring => !IsAcquiring;
+ public bool CalcEnabled => IsAcquiring && IsScrollLocked;
+ public event EventHandler>? AcquisitionStarting;
+ public event EventHandler? SamplesBatchAppended;
+
+ public event EventHandler? PlaceMarkerRequested;
+ public event EventHandler? ClearMarkersRequested;
+ public event EventHandler? RangeStatsRequested;
+
+ public bool IsScrollLocked
{
- get => Recorder.ScrollIndex;
- set
+ get => _isScrollLocked;
+ private set
{
- if (Recorder.ScrollIndex != value)
+ if (_isScrollLocked != value)
{
- Recorder.ScrollIndex = value;
- OnPropertyChanged(nameof(ScrollIndex));
+ _isScrollLocked = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(CalcEnabled));
}
}
}
- public int MaxScrollIndex => Recorder.MaxScrollIndex;
- public ObservableCollection Channels { get; } = [];
- public ObservableCollection LogMessages { get; } = [];
+ public bool FollowLatest => !IsScrollLocked;
+
+ public Enclosure MyEnc => _hardwareService.Enclosure;
- private bool _isScrollingLocked = true;
+ private readonly Dictionary _meterByKey = [];
+ private IDispatcherTimer? _uiDrainTimer;
+ private EventHandler? _drainTickHandler;
+ private readonly TimeSpan _meterUpdatePeriod = TimeSpan.FromMilliseconds(33.3); // 30 Hz
+ private DateTime _lastMeterUpdateUtc = DateTime.MinValue;
+ private bool _isAcquiring;
+ private readonly HardwareService _hardwareService = new();
+ private readonly AcquisitionManager? _acquisitionManager;
+ private bool _isScrollLocked;
+ private const int MaxSelectableChannels = 8;
+ private bool _suppressSelectionGuard = false;
- public Enclosure MyEnc { get; } = new Enclosure
+ public sealed class SamplesBatchAppendedEventArgs(IReadOnlyDictionary batches) : EventArgs
{
- Name = "MyEnc",
- Boards = []
- };
+ public IReadOnlyDictionary Batches { get; } = batches;
+ }
+ public MainViewModel()
+ {
+ IsAcquiring = false;
+ Debug.WriteLine("Started");
+ LogMessages.Add("App started.");
+
+ var initResult = _hardwareService.Initialize();
- private CancellationTokenSource? _cts;
- private Task? _acquisitionTask;
- private double _yAxisMin = -10;
- public double YAxisMin
+ if (initResult.BoardCount == 0)
+ {
+ LogMessages.Add("No Trion Boards found.");
+ _ = ShowAlertAsync("No TRION boards", "No TRION boards were detected. Configure a system and try again.");
+ return;
+ }
+
+ LogMessages.Add(initResult.IsSimulated
+ ? $"Number of simulated Boards found: {initResult.BoardCount}"
+ : $"Number of real Boards found: {initResult.BoardCount}");
+
+ OnPropertyChanged(nameof(MyEnc));
+
+ foreach (var board in MyEnc.Boards)
+ {
+ LogMessages.Add($"Board: {board.Name} (ID: {board.Id})");
+ }
+
+ var channels = _hardwareService.GetChannels(
+ Channel.ChannelType.Analog,
+ Channel.ChannelType.Digital,
+ Channel.ChannelType.Counter);
+
+ foreach (var channel in channels)
+ {
+ Channels.Add(channel);
+ channel.PropertyChanged += OnChannelPropertyChanged;
+ }
+
+ _acquisitionManager = new AcquisitionManager(MyEnc);
+ OnPropertyChanged(nameof(Channels));
+
+ StartAcquisitionCommand = new Command(async () => await StartAcquisition());
+ StopAcquisitionCommand = new Command(async () => await StopAcquisition());
+ LockScrollingCommand = new Command(LockScrolling);
+ ToggleThemeCommand = new Command(ToggleTheme);
+ CopyChannelPathCommand = new Command(async ch => await CopyChannelPathAsync(ch));
+ SelectOnlyChannelCommand = new Command(SelectOnlyChannel);
+ SelectAllOnBoardCommand = new Command(SelectAllOnBoard);
+ DeselectAllOnBoardCommand = new Command(DeselectAllOnBoard);
+ OpenChannelWindowCommand = new Command(OpenChannelWindow);
+ OpenBoardWindowCommand = new Command(OpenBoardWindow);
+ MaxCalcCommand = new Command(MaxCalc);
+ PlaceMarkerCommand = new Command(PlaceMarker);
+ ClearMarkersCommand = new Command(ClearMarkers);
+ }
+ private void OpenChannelWindow(Channel? ch)
{
- get => _yAxisMin;
- set
+ if (ch is null)
{
- if (_yAxisMin != value)
- {
- _yAxisMin = value;
- UpdateYAxes();
- OnPropertyChanged();
- }
+ return;
}
+
+ var window = new ChannelDetailWindow(ch);
+ Application.Current?.OpenWindow(window);
+
+ LogMessages.Add($"Opened window for {ch.BoardID}/{ch.Name} ({window.Width}x{window.Height})");
}
- private void StartAcquisition()
+ private void OpenBoardWindow(Board? board)
{
- LogMessages.Add("Starting acquisition...");
+ if (board is null)
+ {
+ return;
+ }
+
+ var window = new BoardDetailWindow(board);
+ Application.Current?.OpenWindow(window);
+
+ LogMessages.Add($"Opened board window for {board.Name} (ID: {board.Id})");
}
- private void StopAcquisition()
+ private void PlaceMarker()
{
- LogMessages.Add("Stopping acquisition...");
+ if (!CalcEnabled) return;
+ PlaceMarkerRequested?.Invoke(this, EventArgs.Empty);
+ LogMessages.Add("Range marker placed.");
}
- private void LockScrolling()
+
+ private void ClearMarkers()
{
- _isScrollingLocked = !_isScrollingLocked;
- LogMessages.Add(_isScrollingLocked ? "Scrolling locked." : "Scrolling unlocked.");
+ ClearMarkersRequested?.Invoke(this, EventArgs.Empty);
+ LogMessages.Add("Range markers cleared.");
+ }
- if (_isScrollingLocked)
+ private void MaxCalc()
+ {
+ if (!CalcEnabled)
{
- ScrollIndex = MaxScrollIndex;
+ LogMessages.Add("Lock scrolling during acquisition to compute range stats.");
+ return;
}
+
+ RangeStatsRequested?.Invoke(this, EventArgs.Empty);
}
- private double _yAxisMax = 10;
- public double YAxisMax
+ public void ReceiveRangeStats(List stats)
{
- get => _yAxisMax;
- set
+ if (stats.Count == 0)
{
- if (_yAxisMax != value)
- {
- _yAxisMax = value;
- UpdateYAxes();
- OnPropertyChanged();
- }
+ LogMessages.Add("No data in selected range. Place two markers first.");
+ return;
}
+
+ LogMessages.Add("───── Statistics ─────");
+ foreach (var s in stats)
+ {
+ LogMessages.Add($" {s.ChannelKey}: Min={s.Min:F4} Max={s.Max:F4} Avg={s.Average:F4} ({s.SampleCount} samples)");
+ }
+ LogMessages.Add("──────────────────────");
}
- public Axis[]? YAxes { get; set; }
- private void UpdateYAxes()
+ private void OnChannelPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (_suppressSelectionGuard || sender is not Channel ch || e.PropertyName != nameof(Channel.IsSelected) || !ch.IsSelected)
+ {
+ return;
+ }
+
+ var selected = Channels.Count(c => c.IsSelected);
+ if (selected > MaxSelectableChannels)
+ {
+ _suppressSelectionGuard = true;
+ ch.IsSelected = false;
+ _suppressSelectionGuard = false;
+ LogMessages.Add($"You can select up to {MaxSelectableChannels} channels.");
+ }
+ }
+ private async Task StartAcquisition()
{
- YAxes = [
- new Axis
+ var selectedChannels = Channels.Where(c => c.IsSelected).ToList();
+
+ if (selectedChannels.Count > MaxSelectableChannels)
+ {
+ foreach (var extra in selectedChannels.Skip(MaxSelectableChannels))
{
- MinLimit = YAxisMin,
- MaxLimit = YAxisMax,
- Name = "Voltage"
+ extra.IsSelected = false;
}
- ];
- OnPropertyChanged(nameof(YAxes));
+
+ selectedChannels = [.. selectedChannels.Take(MaxSelectableChannels)];
+ LogMessages.Add($"Selection limited to {MaxSelectableChannels} channels.");
+ }
+
+ if (selectedChannels.Count == 0)
+ {
+ LogMessages.Add("No channels selected. Please select at least one channel.");
+ await ShowAlertAsync("No channels selected", "Please select at least one channel and try again.");
+ return;
+ }
+
+ PrepareUIForAcquisition(selectedChannels);
+ AcquisitionStarting?.Invoke(this, selectedChannels);
+
+ IsAcquiring = true;
+ Debug.WriteLine("Starting acquisition...");
+ LogMessages.Add("Starting acquisition...");
+
+ await _acquisitionManager!.StartAcquisitionAsync(selectedChannels);
+ StartUiDrainTimer();
}
+ private void PrepareUIForAcquisition(List selectedChannels)
+ {
+ DigitalMeters.Clear();
+ _meterByKey.Clear();
- public ICommand ChannelSelectedCommand { get; private set; }
- public ICommand StartAcquisitionCommand { get; private set; }
- public ICommand StopAcquisitionCommand { get; private set; }
- public ICommand LockScrollingCommand { get; private set; }
- public MainViewModel()
+ foreach (var channel in selectedChannels)
+ {
+ var key = $"{channel.BoardID}/{channel.Name}";
+
+ var meter = new DigitalMeter
+ {
+ Label = key,
+ Unit = channel.Unit
+ };
+ _meterByKey[key] = meter;
+ DigitalMeters.Add(meter);
+ }
+
+ OnPropertyChanged(nameof(DigitalMeters));
+ }
+ private async Task StopAcquisition()
{
- LogMessages.Add("App started.");
+ if (!IsAcquiring)
+ {
+ return;
+ }
- var numberOfBoards = TrionApi.Initialize();
- if (numberOfBoards < 0)
+ LogMessages.Add("Stopping acquisition...");
+ StopUiDrainTimer();
+ IsAcquiring = false;
+ await _acquisitionManager!.StopAcquisitionAsync();
+ LogMessages.Add("Acquisition stopped.");
+ }
+ private void LockScrolling()
+ {
+ IsScrollLocked = !IsScrollLocked;
+ LogMessages.Add(IsScrollLocked ? "Scrolling locked." : "Scrolling unlocked.");
+ }
+ private void ToggleTheme()
+ {
+ if (Application.Current is not null)
{
- LogMessages.Add($"Number of simulated Boards found: {Math.Abs(numberOfBoards)}");
+ Application.Current.UserAppTheme = Application.Current.UserAppTheme == AppTheme.Light ? AppTheme.Dark : AppTheme.Light;
+ LogMessages.Add($"Theme changed to {Application.Current.UserAppTheme}.");
}
- else if (numberOfBoards > 0)
+ }
+ private void StartUiDrainTimer()
+ {
+ StopUiDrainTimer();
+ var dispatcher = Application.Current?.Dispatcher;
+ if (dispatcher is null)
{
- LogMessages.Add($"Number of real Boards found: {numberOfBoards}");
+ return;
}
- else
+
+ _uiDrainTimer = dispatcher.CreateTimer();
+ _uiDrainTimer.Interval = TimeSpan.FromMilliseconds(33.3); // ~30 Hz
+ _uiDrainTimer.IsRepeating = true;
+
+ _drainTickHandler = (_, _) => DrainAndPublish();
+ _uiDrainTimer.Tick += _drainTickHandler;
+ _uiDrainTimer.Start();
+ }
+ private void StopUiDrainTimer()
+ {
+ if (_uiDrainTimer is null)
{
- LogMessages.Add("No Trion Boards found.");
+ return;
+ }
+ if (_drainTickHandler is not null)
+ {
+ _uiDrainTimer.Tick -= _drainTickHandler;
}
- numberOfBoards = Math.Abs(numberOfBoards);
+ _uiDrainTimer.Stop();
+ _uiDrainTimer = null;
+ _drainTickHandler = null;
+ }
+ private void DrainAndPublish()
+ {
+ var batches = _acquisitionManager!.DrainSamples(maxPerChannel: 10_000);
+ if (0 == batches.Count)
+ {
+ return;
+ }
- MyEnc.Init(numberOfBoards);
- OnPropertyChanged(nameof(MyEnc));
+ var now = DateTime.UtcNow;
+ bool updateMeters = (now - _lastMeterUpdateUtc) >= _meterUpdatePeriod;
+ if (updateMeters) _lastMeterUpdateUtc = now;
- foreach (var board in MyEnc.Boards)
+ foreach (var (channelKey, samples) in batches)
{
- LogMessages.Add($"Board: {board.Name} (ID: {board.Id})");
- foreach (var channel in board.Channels)
+ if (updateMeters && samples.Length > 0)
{
- if (channel.Name != null)
+ var latestValue = samples[^1].Value;
+ if (_meterByKey.TryGetValue(channelKey, out var meter))
{
- Channels.Add(channel);
+ meter.Value = latestValue;
}
}
}
- OnPropertyChanged(nameof(Channels));
- ChannelSelectedCommand = new Command(OnChannelSelected);
- StartAcquisitionCommand = new Command(StartAcquisition);
- StopAcquisitionCommand = new Command(StopAcquisition);
- LockScrollingCommand = new Command(LockScrolling);
- UpdateYAxes();
+ SamplesBatchAppended?.Invoke(this, new SamplesBatchAppendedEventArgs(batches));
}
-
- public void Dispose()
+ private async Task CopyChannelPathAsync(Channel? ch)
{
- TrionApi.DeWeSetParam_i32(0, TrionCommand.CLOSE_BOARD_ALL, 0);
+ if (ch is null)
+ {
+ return;
+ }
- // Call your uninitialize function here
- TrionApi.Uninitialize();
+ string channelPath = $"BoardID{ch.BoardID}/{ch.Name}";
+ await Clipboard.SetTextAsync(channelPath);
+ LogMessages.Add($"Copied: {channelPath}");
}
-
- private void OnChannelSelected(Channel selectedChannel)
+ private void SelectOnlyChannel(Channel? ch)
{
- _cts?.Cancel();
- _acquisitionTask?.Wait();
- Recorder.Data.Clear();
- Recorder.Window.Clear();
+ if (ch is null)
+ {
+ return;
+ }
- MeasurementSeries = [
- new LineSeries
- {
- Values = ChartWindowData,
- Name = $"{selectedChannel.Name}",
- AnimationsSpeed = TimeSpan.Zero,
- GeometrySize = 0
- }];
- OnPropertyChanged(nameof(MeasurementSeries));
+ foreach (var c in Channels)
+ {
+ c.IsSelected = false;
+ }
- _cts = new CancellationTokenSource();
- _acquisitionTask = Task.Run(() => AcquireDataLoop(selectedChannel), _cts.Token);
+ ch.IsSelected = true;
+ OnPropertyChanged(nameof(Channels));
+ LogMessages.Add($"Selected only {ch.BoardID}/{ch.Name}");
}
-
- private void AcquireDataLoop(Channel selectedChannel)
+ private void SelectAllOnBoard(Channel? ch)
{
- var board_id = selectedChannel.BoardID;
- var channel_name = selectedChannel.Name;
-
- TrionApi.DeWeSetParamStruct($"BoardID{board_id}/AIAll", "Used", "False");
- TrionApi.DeWeSetParamStruct($"BoardID{board_id}/{channel_name}", "Used", "True");
- TrionApi.DeWeSetParamStruct($"BoardID{board_id}/{channel_name}", "Range", "10 V");
-
- MyEnc.Boards[board_id].SetAcquisitionProperties(sampleRate: "2000", buffer_block_size: 200, buffer_block_count: 50);
- MyEnc.Boards[board_id].UpdateBoard();
-
- var (adcDelayError, adc_delay) = TrionApi.DeWeGetParam_i32(board_id, Trion.TrionCommand.BOARD_ADC_DELAY);
- TrionApi.DeWeSetParam_i32(board_id, TrionCommand.START_ACQUISITION, 0);
- CircularBuffer buffer = new(board_id);
+ if (ch is null)
+ {
+ return;
+ }
- while (_cts != null && !_cts.IsCancellationRequested)
+ int selected = Channels.Count(x => x.IsSelected);
+ foreach (var c in Channels.Where(x => x.BoardID == ch.BoardID))
{
- var (available_samples_error, available_samples) = TrionApi.DeWeGetParam_i32(board_id, TrionCommand.BUFFER_0_WAIT_AVAIL_NO_SAMPLE);
- available_samples -= adc_delay;
- if (available_samples <= 0)
+ if (c.IsSelected)
{
- Thread.Sleep(10);
continue;
}
- var (read_pos_error, read_pos) = TrionApi.DeWeGetParam_i64(board_id, TrionCommand.BUFFER_0_ACT_SAMPLE_POS);
- read_pos += adc_delay * sizeof(uint);
- List tempValues = [.. new double[available_samples]];
- for (int i = 0; i < available_samples; ++i)
+ if (selected >= MaxSelectableChannels)
{
- if (read_pos >= buffer.EndPosition)
- {
- read_pos -= buffer.Size;
- }
-
- float value = Marshal.ReadInt32((IntPtr)read_pos);
- value = (float)((float)value / 0x7FFFFF00 * 10.0);
- tempValues[i] = value;
-
- read_pos += sizeof(uint);
+ break;
}
- TrionApi.DeWeSetParam_i32(board_id, TrionCommand.BUFFER_0_FREE_NO_SAMPLE, available_samples);
-
- MainThread.BeginInvokeOnMainThread(() =>
- {
- Recorder.AddSamples(tempValues);
+ c.IsSelected = true;
+ selected++;
+ }
- if (_isScrollingLocked)
- {
- Recorder.AutoScroll();
- OnPropertyChanged(nameof(ScrollIndex));
- }
- OnPropertyChanged(nameof(MaxScrollIndex));
- });
+ OnPropertyChanged(nameof(Channels));
+ if (selected >= MaxSelectableChannels)
+ {
+ LogMessages.Add($"Selection limited to {MaxSelectableChannels} channels.");
}
- TrionApi.DeWeSetParam_i32(board_id, TrionCommand.STOP_ACQUISITION, 0);
+ else
+ {
+ LogMessages.Add($"Selected all channels on Board {ch.BoardID}");
+ }
+ }
+ private void DeselectAllOnBoard(Channel? ch)
+ {
+ if (ch is null)
+ {
+ return;
+ }
+ foreach (var c in Channels.Where(x => x.BoardID == ch.BoardID))
+ {
+ c.IsSelected = false;
+ }
+ OnPropertyChanged(nameof(Channels));
+ LogMessages.Add($"Deselected all channels on Board {ch.BoardID}");
+ }
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ _hardwareService.Dispose();
}
}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailPage.xaml b/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailPage.xaml
new file mode 100644
index 0000000..3b58b06
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailPage.xaml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailPage.xaml.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailPage.xaml.cs
new file mode 100644
index 0000000..d8306f9
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailPage.xaml.cs
@@ -0,0 +1,15 @@
+using TRION_SDK_UI.Models;
+using TRION_SDK_UI.ViewModels;
+
+namespace TRION_SDK_UI;
+
+public partial class BoardDetailPage : ContentPage
+{
+ public BoardDetailPage(Board board)
+ {
+ InitializeComponent();
+
+ var vm = new BoardDetailViewModel(board);
+ BindingContext = vm;
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailWindow.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailWindow.cs
new file mode 100644
index 0000000..fc43285
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Views/BoardDetailWindow.cs
@@ -0,0 +1,16 @@
+using TRION_SDK_UI.Models;
+
+namespace TRION_SDK_UI;
+
+public sealed class BoardDetailWindow : Window
+{
+ public BoardDetailWindow(Board board)
+ : base(new BoardDetailPage(board))
+ {
+ Title = $"Board {board.Id}/{board.Name}";
+ Width = 520;
+ Height = 520;
+ X = 140;
+ Y = 140;
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailPage.xaml b/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailPage.xaml
new file mode 100644
index 0000000..56beef6
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailPage.xaml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailPage.xaml.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailPage.xaml.cs
new file mode 100644
index 0000000..6bb2e45
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailPage.xaml.cs
@@ -0,0 +1,21 @@
+using TRION_SDK_UI.Models;
+using TRION_SDK_UI.ViewModels;
+
+namespace TRION_SDK_UI;
+
+public partial class ChannelDetailPage : ContentPage
+{
+ public ChannelDetailPage(Channel ch)
+ {
+ InitializeComponent();
+
+ var vm = new ChannelDetailViewModel(ch);
+ vm.CloseRequested += (_, __) =>
+ {
+ var window = this.Window;
+ if (window is not null)
+ Application.Current?.CloseWindow(window);
+ };
+ BindingContext = vm;
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailWindow.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailWindow.cs
new file mode 100644
index 0000000..90057d4
--- /dev/null
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Views/ChannelDetailWindow.cs
@@ -0,0 +1,15 @@
+namespace TRION_SDK_UI;
+
+public sealed class ChannelDetailWindow : Window
+{
+ public ChannelDetailWindow(TRION_SDK_UI.Models.Channel channel)
+ : base(new ChannelDetailPage(channel))
+ {
+ Title = $"Channel {channel.BoardID}/{channel.Name}";
+ Width = 600;
+ Height = 600;
+
+ X = 120;
+ Y = 120;
+ }
+}
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Views/MainPage.xaml b/app/TRION-SDK-UI/TRION-SDK-UI/Views/MainPage.xaml
index e3fb2fb..c9f3b21 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Views/MainPage.xaml
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Views/MainPage.xaml
@@ -1,88 +1,160 @@
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/TRION-SDK-UI/TRION-SDK-UI/Views/MainPage.xaml.cs b/app/TRION-SDK-UI/TRION-SDK-UI/Views/MainPage.xaml.cs
index 6eaa1d9..d647936 100644
--- a/app/TRION-SDK-UI/TRION-SDK-UI/Views/MainPage.xaml.cs
+++ b/app/TRION-SDK-UI/TRION-SDK-UI/Views/MainPage.xaml.cs
@@ -1,42 +1,233 @@
-using System.Collections.ObjectModel;
+using ScottPlot;
+using ScottPlot.Plottables;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using TRION_SDK_UI.Models;
+using TRION_SDK_UI.POCO;
+using TRION_SDK_UI.ViewModels;
namespace TRION_SDK_UI
{
public partial class MainPage : ContentPage
{
- double _startX;
- double _startWidth;
+ private readonly RecorderGraph _recorder;
+ public double StartWidth { get; set; }
+
+ void OnPointerEntered(object sender, PointerEventArgs e)
+ {
+ _recorder.SetLockCrossVisibility();
+ }
+
+ void OnPointerExited(object sender, PointerEventArgs e)
+ {
+ _recorder.HideLockCross();
+ }
+
+ void OnPointerMoved(object sender, PointerEventArgs e)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _recorder.UpdatePointer(e);
+ });
+ }
+
+ void OnPointerPressed(object? sender, PointerEventArgs e)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ var pos = e.GetPosition(MauiPlot1);
+ if (pos is null)
+ {
+ return;
+ }
+
+ var pixel = new Pixel((float)pos.Value.X, (float)pos.Value.Y);
+ if (_recorder.TryBeginMarkerDrag(pixel))
+ {
+ MauiPlot1.UserInputProcessor.IsEnabled = false;
+ }
+ });
+ }
+
+ void OnPointerReleased(object? sender, PointerEventArgs e)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ if (_recorder.IsDraggingMarker)
+ {
+ _recorder.EndMarkerDrag();
+ MauiPlot1.UserInputProcessor.IsEnabled = true;
+ }
+ });
+ }
public MainPage()
{
InitializeComponent();
- //BindingContext = this;
+
BindingContext = new MainViewModel();
+ var vm = (MainViewModel)BindingContext;
+ vm.AcquisitionStarting += VmOnAcquisitionStarted;
+ vm.SamplesBatchAppended += VmOnSamplesBatchAppended;
+ vm.LogMessages.CollectionChanged += VmLogMessagesCollectionChanged;
+ vm.PropertyChanged += VmOnPropertyChanged;
+ vm.PlaceMarkerRequested += VmOnPlaceMarkerRequested;
+ vm.ClearMarkersRequested += VmOnClearMarkersRequested;
+ vm.RangeStatsRequested += VmOnRangeStatsRequested;
+
+ MauiPlot1.Plot.Title("Live Signals");
+ MauiPlot1.Plot.XLabel("Elapsed Seconds");
+ MauiPlot1.Plot.YLabel("Value");
+ MauiPlot1.Plot.Axes.Hairline(true);
+
+ _recorder = new RecorderGraph(MauiPlot1,
+ CursorLabel,
+ CursorLabelText,
+ MauiPlot1.Plot.Add.VerticalLine(0),
+ MauiPlot1.Plot.Add.Crosshair(0, 0),
+ () => vm.IsScrollLocked);
var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += OnDragHandlePanUpdated;
DragHandle.GestureRecognizers.Add(panGesture);
+
+ if (Application.Current is null)
+ {
+ return;
+ }
+ _recorder.ApplyTheme();
+
+ Application.Current.RequestedThemeChanged += (s, a) =>
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _recorder.ApplyTheme();
+ MauiPlot1.Refresh();
+ });
+ };
+ }
+
+ private void VmOnPlaceMarkerRequested(object? sender, EventArgs e)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _recorder.PlaceRangeMarker();
+ });
+ }
+
+ private void VmOnClearMarkersRequested(object? sender, EventArgs e)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _recorder.ClearRangeMarkers();
+ });
}
+ private void VmOnRangeStatsRequested(object? sender, EventArgs e)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ var stats = _recorder.ComputeRangeStats();
+ var vm = (MainViewModel)BindingContext;
+ vm.ReceiveRangeStats(stats);
+ });
+ }
+
+ private void VmOnSamplesBatchAppended(object? sender, MainViewModel.SamplesBatchAppendedEventArgs e)
+ {
+ if (sender is not MainViewModel vm)
+ {
+ return;
+ }
+
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ foreach (var (channelKey, samples) in e.Batches)
+ {
+ if (samples is { Length: > 0 })
+ {
+ _recorder.AddSamples(samples, channelKey, vm.FollowLatest);
+ }
+ }
+
+ _recorder.UpdateValuesAtLockLine();
+ MauiPlot1.Refresh();
+ });
+ }
+
+ private void VmLogMessagesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems is { Count: > 0 })
+ {
+ MainThread.BeginInvokeOnMainThread(async () =>
+ {
+ await Task.Delay(50);
+ await LogScrollView.ScrollToAsync(0, LogScrollView.ContentSize.Height, animated: true);
+ });
+ }
+ }
+
+ private void VmOnAcquisitionStarted(object? sender, IReadOnlyList channels)
+ {
+ if (Application.Current is null)
+ {
+ return;
+ }
+
+ var keys = channels.Select(ch => $"{ch.BoardID}/{ch.Name}").ToHashSet();
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _recorder.StartAcquisition(keys);
+ });
+ }
+
+ private void VmOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(MainViewModel.IsScrollLocked))
+ {
+ return;
+ }
+ if (Application.Current is null) return;
+
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ _recorder.ApplyTheme();
+ _recorder.SetLockCrossVisibility();
+ _recorder.UpdateValuesAtLockLine();
+ MauiPlot1.Refresh();
+ });
+ }
private void OnDragHandlePanUpdated(object? sender, PanUpdatedEventArgs e)
{
switch (e.StatusType)
{
case GestureStatus.Started:
- _startWidth = SidebarColumn.Width.Value;
+ StartWidth = SidebarColumn.Width.Value;
break;
case GestureStatus.Running:
- double newWidth = _startWidth - e.TotalX;
+ double newWidth = StartWidth - e.TotalX;
if (newWidth < 100) newWidth = 100;
if (newWidth > 400) newWidth = 400;
SidebarColumn.Width = new GridLength(newWidth);
break;
}
}
-
- private void OnCounterClicked(object sender, EventArgs e)
+ protected override void OnDisappearing()
{
- //LogMessages.Add($"Button clicked at {DateTime.Now:T}");
+ base.OnDisappearing();
+
+ if (BindingContext is MainViewModel vm)
+ {
+ vm.AcquisitionStarting -= VmOnAcquisitionStarted;
+ vm.SamplesBatchAppended -= VmOnSamplesBatchAppended;
+ vm.LogMessages.CollectionChanged -= VmLogMessagesCollectionChanged;
+ vm.PropertyChanged -= VmOnPropertyChanged;
+ vm.PlaceMarkerRequested -= VmOnPlaceMarkerRequested;
+ vm.ClearMarkersRequested -= VmOnClearMarkersRequested;
+ vm.RangeStatsRequested -= VmOnRangeStatsRequested;
+
+ vm.Dispose();
+ }
}
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/trion_api/CS/TrionApi/TrionApi.cs b/trion_api/CS/TrionApi/TrionApi.cs
index 8b3583d..582b749 100644
--- a/trion_api/CS/TrionApi/TrionApi.cs
+++ b/trion_api/CS/TrionApi/TrionApi.cs
@@ -1,23 +1,21 @@
-using System;
using Trion;
-
public class TrionApi
{
public static int Initialize()
{
// Configure the TRION API backend (choose TRION or TRIONET as needed)
- Trion.API.DeWeConfigure(Trion.API.Backend.TRION);
+ // should be done separate?
+ // Trion.API.DeWeConfigure(Trion.API.Backend.TRION);
// Call DeWeDriverInit on startup
- int nNoOfBoards;
- TrionError nErrorCode = Trion.API.DeWeDriverInit(out nNoOfBoards);
+ TrionError nErrorCode = Trion.API.DeWeDriverInit(out int nNoOfBoards);
// Optional: handle error or log result
if (nErrorCode != TrionError.NONE)
{
// Handle error (e.g., log, show message, etc.)
- System.Diagnostics.Debug.WriteLine($"TRION API Init failed: {Trion.API.DeWeErrorConstantToString(nErrorCode)}");
+ System.Diagnostics.Debug.WriteLine($"TRION API Init failed: {API.DeWeErrorConstantToString(nErrorCode)}");
}
else
{
@@ -31,7 +29,7 @@ public static void CloseBoards()
var error = TrionApi.DeWeSetParam_i32(0, Trion.TrionCommand.CLOSE_BOARD_ALL, 0);
if (error != TrionError.NONE)
{
- System.Diagnostics.Debug.WriteLine($"TRION API CloseBoards failed: {Trion.API.DeWeErrorConstantToString(error)}");
+ System.Diagnostics.Debug.WriteLine($"TRION API CloseBoards failed");
}
}
@@ -40,7 +38,7 @@ public static void Uninitialize()
TrionError nErrorCode = Trion.API.DeWeDriverDeInit();
if (nErrorCode != TrionError.NONE)
{
- System.Diagnostics.Debug.WriteLine($"TRION API Uninit failed: {Trion.API.DeWeErrorConstantToString(nErrorCode)}");
+ System.Diagnostics.Debug.WriteLine($"TRION API Uninit failed:");
}
else
{
@@ -49,39 +47,39 @@ public static void Uninitialize()
}
- public static (Trion.TrionError error, Int32 value) DeWeGetParam_i32(Int32 nBoardNo, Trion.TrionCommand nCommandId)
+ public static (TrionError error, Int32 value) DeWeGetParam_i32(Int32 nBoardNo, TrionCommand nCommandId)
{
Int32 value = 0;
- Trion.TrionError error = Trion.API.DeWeGetParam_i32(nBoardNo, nCommandId, out value);
+ Trion.TrionError error = API.DeWeGetParam_i32(nBoardNo, nCommandId, out value);
return (error, value);
}
- public static Trion.TrionError DeWeSetParam_i32(Int32 nBoardNo, Trion.TrionCommand nCommandId, Int32 value)
+ public static TrionError DeWeSetParam_i32(Int32 nBoardNo, TrionCommand nCommandId, Int32 value)
{
- Trion.TrionError error = Trion.API.DeWeSetParam_i32(nBoardNo, nCommandId, value);
+ TrionError error = API.DeWeSetParam_i32(nBoardNo, nCommandId, value);
return (error);
}
- public static (Trion.TrionError error, Int64 value) DeWeGetParam_i64(Int32 nBoardNo, Trion.TrionCommand nCommandId)
+ public static (TrionError error, Int64 value) DeWeGetParam_i64(Int32 nBoardNo, TrionCommand nCommandId)
{
Int64 value = 0;
- Trion.TrionError error = Trion.API.DeWeGetParam_i64(nBoardNo, nCommandId, out value);
+ TrionError error = API.DeWeGetParam_i64(nBoardNo, nCommandId, out value);
return (error, value);
}
- public static Trion.TrionError DeWeSetParam_i64(Int32 nBoardNo, Trion.TrionCommand nCommandId, Int64 value)
+ public static TrionError DeWeSetParam_i64(Int32 nBoardNo, TrionCommand nCommandId, Int64 value)
{
- Trion.TrionError error = Trion.API.DeWeSetParam_i64(nBoardNo, nCommandId, value);
- return (error);
+ TrionError error = API.DeWeSetParam_i64(nBoardNo, nCommandId, value);
+ return error;
}
- public static (Trion.TrionError error, string value) DeWeGetParamStruct_String(string target, string item)
+ public static (TrionError error, string value) DeWeGetParamStruct_String(string target, string item)
{
// First, get the required buffer size
- Trion.TrionError error = Trion.API.DeWeGetParamStruct_strLEN(target, item, out uint requiredLength);
+ TrionError error = Trion.API.DeWeGetParamStruct_strLEN(target, item, out uint requiredLength);
- if (error != Trion.TrionError.NONE) // Assuming None represents success
+ if (error != TrionError.NONE) // Assuming None represents success
{
return (error, string.Empty);
}
diff --git a/trion_api/CS/TrionApi/trion_net_api.cs b/trion_api/CS/TrionApi/trion_net_api.cs
index 103210a..33f7d34 100644
--- a/trion_api/CS/TrionApi/trion_net_api.cs
+++ b/trion_api/CS/TrionApi/trion_net_api.cs
@@ -149,15 +149,15 @@ public static Trion.TrionError DeWeConfigure(Backend backend)
_dewe_error_constant_to_string = TrionNET_x86.API.DeWeErrorConstantToString;
- return Trion.TrionError.NONE;
+ return TrionError.NONE;
}
}
- return Trion.TrionError.API_NOT_LOADED;
+ return TrionError.API_NOT_LOADED;
}
- public static Trion.TrionError DeWeDriverInit(out Int32 nNumOfBoard)
+ public static TrionError DeWeDriverInit(out Int32 nNumOfBoard)
{
nNumOfBoard = 0;
if (_dewe_driver_init != null)
diff --git a/trion_api/CS/TrionApi/utils.cs b/trion_api/CS/TrionApi/utils.cs
index 3050825..f9689ea 100644
--- a/trion_api/CS/TrionApi/utils.cs
+++ b/trion_api/CS/TrionApi/utils.cs
@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace TrionApiUtils
+namespace TrionApiUtils
{
public class Utils
{
@@ -17,14 +11,17 @@ public static void CheckErrorCode(Trion.TrionError error_code, String user_messa
Console.ResetColor();
return;
}
- if ((int)error_code == 0)
- {
- return;
- }
+ //if ((int)error_code == 0)
+ //{
+ // Console.ForegroundColor = ConsoleColor.Green;
+ // System.Diagnostics.Debug.WriteLine($"TRION API Success: {user_message} {error_code}");
+ // Console.ResetColor();
+ // return;
+ //}
if ((int)error_code > 0)
{
Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine($"TRION API Error: {user_message} {error_code}");
+ System.Diagnostics.Debug.WriteLine($"TRION API Error: {user_message} {error_code}");
Console.ResetColor();
TrionApi.CloseBoards();
TrionApi.Uninitialize();