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 @@ + + + +image/svg+xml \ 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 @@ + + + + +