From d87965ca00cab4269eb25a31143e70b7b250fde7 Mon Sep 17 00:00:00 2001 From: Federica Donald Date: Wed, 10 Jun 2026 11:07:23 +0000 Subject: [PATCH 1/7] Make SQL Race API Example codes pretty again --- .env.example | 5 + .gitattributes | 63 - .github/CONTRIBUTING.md | 40 + .github/workflows/build-and-test.yml | 39 + .github/workflows/validate-notebooks.yml | 48 + .gitignore | 343 +--- CONCEPTS.md | 69 + COOKBOOK.md | 126 ++ LICENSE | 21 + MAT.OCS.SQLRace.Examples.sln | 92 -- MAT.OCS.SQLRace.Examples.sln.DotSettings | 4 - MAT.SQLRace.BaseExampleLoadSSN/App.config | 30 - .../MAT.SQLRace.BaseExampleLoadSSN.csproj | 17 - MAT.SQLRace.BaseExampleLoadSSN/Program.cs | 83 - .../App.config | 54 - ...CreateSessionWithMultipleParameters.csproj | 13 - .../Program.cs | 164 -- .../App.config | 30 - ...lloCreateSSN2FromZeroWithParameters.csproj | 13 - .../Program.cs | 151 -- .../SessionHelper.cs | 176 -- MAT.SQLRace.HelloData/App.config | 58 - MAT.SQLRace.HelloData/FunctionHelper.cs | 55 - .../MAT.SQLRace.HelloData.csproj | 17 - MAT.SQLRace.HelloData/Program.cs | 1427 ----------------- MAT.SQLRace.HelloData/SessionHelper.cs | 313 ---- .../App.config | 30 - ...essionFromDatabaseAndReadParameters.csproj | 13 - .../Program.cs | 149 -- .../HelloFunction.cs | 92 -- .../HelloPdaCountFunction.cs | 100 -- .../MAT.SqlRace.Functions.HelloDotNet.csproj | 16 - MAT.SqlRace.Functions.HelloDotNet/ReadMe.txt | 3 - MAT.SqlRace.Functions.HelloDotNet/app.config | 47 - MAT.SqlRace.ServerListener/App.config | 62 - .../MAT.SqlRace.ServerListener.csproj | 13 - MAT.SqlRace.ServerListener/Program.cs | 87 - MAT.SqlRace.ServerListener/SessionHelper.cs | 223 --- MAT.SqlRace.StandaloneRecorder/App.config | 50 - .../MAT.SqlRace.StandaloneRecorder.csproj | 14 - MAT.SqlRace.StandaloneRecorder/Program.cs | 231 --- .../SessionHelper.cs | 223 --- .../FindAllSessionsExample.cs | 82 - .../LoadSessionExample.cs | 146 -- ...SL.SqlRace.Examples.Sessions.CSharp.csproj | 20 - .../MainForm.Designer.cs | 359 ----- .../MainForm.cs | 191 --- .../MainForm.resx | 146 -- .../Program.cs | 25 - .../Properties/Resources.Designer.cs | 63 - .../Properties/Resources.resx | 117 -- .../Properties/Settings.Designer.cs | 26 - .../Properties/Settings.settings | 7 - .../SearchSessionsByItemExample.cs | 92 -- .../SessionCreationExample.cs | 223 --- ...System.Windows.Forms.DataVisualization.dll | Bin 1753088 -> 0 bytes .../app.config | 101 -- Python/Readme.txt | 4 - Python/main.py | 235 --- docs/connection-strings.md | 101 ++ docs/getting-started.md | 123 ++ docs/industry-mapping.md | 5 + docs/server-listener.md | 130 ++ docs/troubleshooting.md | 127 ++ images/ATLAS_logo_GR.png | Bin 0 -> 28873 bytes images/atlaslogo.png | Bin 2570343 -> 0 bytes notebooks/01-getting-started.ipynb | 198 +++ notebooks/02-session-exploration.ipynb | 151 ++ notebooks/03-multi-parameter-analysis.ipynb | 188 +++ notebooks/04-session-comparison.ipynb | 203 +++ notebooks/05-export-pipeline.ipynb | 181 +++ notebooks/06-live-data-monitoring.ipynb | 181 +++ notebooks/README.md | 57 + notebooks/requirements.txt | 12 + .../scenarios/data/durability-cycle1.ssn2 | Bin 0 -> 532480 bytes .../scenarios/data/durability-cycle2.ssn2 | Bin 0 -> 532480 bytes .../scenarios/data/durability-cycle3.ssn2 | Bin 0 -> 532480 bytes .../flight-test-manoeuvre-extraction.ssn2 | Bin 0 -> 536576 bytes .../data/motorsport-lap-analysis.ssn2 | Bin 0 -> 548864 bytes notebooks/scenarios/data/turbine-week1.ssn2 | Bin 0 -> 577536 bytes notebooks/scenarios/data/turbine-week2.ssn2 | Bin 0 -> 577536 bytes notebooks/scenarios/data/turbine-week3.ssn2 | Bin 0 -> 577536 bytes .../durability-cycle-comparison.ipynb | 240 +++ .../flight-test-manoeuvre-extraction.ipynb | 204 +++ .../scenarios/motorsport-lap-analysis.ipynb | 191 +++ .../turbine-performance-trending.ipynb | 195 +++ notebooks/sqlrace_helpers.py | 245 +++ nuget.config | 15 + projects/README.md | 35 + .../csharp/BatchExporter/BatchExporter.csproj | 16 + projects/csharp/BatchExporter/CsvWriter.cs | 105 ++ projects/csharp/BatchExporter/ExportConfig.cs | 60 + projects/csharp/BatchExporter/Program.cs | 159 ++ projects/csharp/BatchExporter/README.md | 81 + .../csharp/BatchExporter/appsettings.json | 15 + projects/csharp/Directory.Build.props | 12 + .../FunctionLibrary/FunctionLibrary.csproj | 14 + projects/csharp/FunctionLibrary/README.md | 63 + .../FunctionLibrary/RollingAverageFunction.cs | 65 + .../FunctionLibrary/StatisticsFunction.cs | 60 + .../FunctionLibrary/UnitConverterFunction.cs | 58 + .../GettingStarted/GettingStarted.csproj | 11 + projects/csharp/GettingStarted/Program.cs | 89 + .../csharp/LiveDashboard/DashboardRenderer.cs | 84 + .../csharp/LiveDashboard/LiveDashboard.csproj | 16 + projects/csharp/LiveDashboard/Program.cs | 181 +++ projects/csharp/LiveDashboard/README.md | 70 + .../csharp/LiveDashboard/appsettings.json | 11 + .../csharp/ServerListenerRecorder/Program.cs | 154 ++ .../csharp/ServerListenerRecorder/README.md | 78 + .../ServerListenerRecorder/RecorderConfig.cs | 32 + .../ServerListenerRecorder.csproj | 16 + .../ServerListenerRecorder/appsettings.json | 12 + projects/csharp/SessionCropper/Program.cs | 335 ++++ projects/csharp/SessionCropper/README.md | 34 + .../SessionCropper/SessionCropper.csproj | 11 + .../ConnectionStringProvider.cs | 91 ++ .../SqlRace.Examples.Core/ParameterBuilder.cs | 270 ++++ .../csharp/SqlRace.Examples.Core/README.md | 36 + .../SqlRace.Examples.Core/SessionFactory.cs | 127 ++ .../SqlRace.Examples.Core.csproj | 17 + .../SqlRaceBootstrapper.cs | 54 + projects/csharp/SqlRace.Examples.sln | 61 + projects/csharp/StandaloneRecorder/Program.cs | 166 ++ projects/csharp/StandaloneRecorder/README.md | 73 + .../StandaloneRecorder/RecorderConfig.cs | 32 + .../StandaloneRecorder.csproj | 16 + .../StandaloneRecorder/appsettings.json | 12 + projects/matlab/session_analysis/README.md | 55 + .../matlab/session_analysis/analyse_session.m | 156 ++ .../dotnetcli_netcore.runtimeconfig.json | 23 + .../session_analysis/session_results.mat | Bin 0 -> 9678 bytes .../matlab/session_analysis/setup_sqlrace.m | 94 ++ projects/python/atlas_automation/README.md | 48 + .../atlas_automation/atlas_controller.py | 176 ++ .../python/atlas_automation/requirements.txt | 1 + projects/python/session_explorer/README.md | 75 + .../python/session_explorer/requirements.txt | 2 + .../session_explorer/session_explorer.py | 199 +++ readme.md | 107 +- scripts/hooks/pre-commit | 25 + scripts/install-hooks.sh | 7 + scripts/verify-snippet-errors.ps1 | 21 + scripts/verify-snippets.ps1 | 31 + snippets/README.md | 118 ++ snippets/csharp/README.md | 18 + .../append-multiple-sessions.cs | 80 + .../composite-with-associates.cs | 77 + .../whole-session-compare.cs | 79 + .../csharp/configuration/conversion-types.cs | 73 + .../configuration/create-parameter-config.cs | 100 ++ .../create-transient-parameter.cs | 63 + .../introspect-session-config.cs | 69 + .../csharp/data-access/bulk-parameter-read.cs | 79 + .../data-access/data-status-filtering.cs | 74 + .../data-access/multi-rate-alignment.cs | 89 + .../data-access/read-samples-between.cs | 68 + .../data-access/read-subsampled-data.cs | 65 + .../csharp/data-access/reverse-iteration.cs | 67 + .../timestamp-array-construction.cs | 70 + .../csharp/functions/dotnet-function-basic.cs | 74 + .../functions/dotnet-function-with-pda.cs | 81 + .../csharp/functions/fdl-function-basic.cs | 73 + .../01-load-session-from-file.cs | 72 + .../02-load-session-from-database.cs | 51 + .../03-read-parameter-samples.cs | 78 + .../04-create-session-write-data.cs | 99 ++ .../05-query-sessions-by-metadata.cs | 60 + .../csharp/live-data/poll-live-samples.cs | 79 + .../rda-parameter-change-detection.cs | 83 + .../live-data/subscribe-to-data-events.cs | 60 + .../live-data/subscribe-to-lap-events.cs | 63 + .../event-definitions-and-data.cs | 56 + .../lap-and-segment-management.cs | 50 + .../session-management/marker-management.cs | 49 + .../parameter-unit-resolution.cs | 82 + .../query-with-composite-filter.cs | 49 + .../session-management/session-association.cs | 57 + .../session-metadata-crud.cs | 63 + snippets/matlab/README.md | 41 + .../matlab/data-access/extract_to_timetable.m | 100 ++ .../matlab/data-access/multi_channel_read.m | 121 ++ .../matlab/data-access/segment_statistics.m | 95 ++ .../matlab/getting-started/diagnose_setup.m | 131 ++ .../matlab/getting-started/load_session.m | 77 + .../matlab/getting-started/query_sessions.m | 92 ++ .../matlab/getting-started/read_parameters.m | 95 ++ snippets/python/README.md | 51 + .../python/data-access/export_to_pandas.py | 97 ++ .../data-access/multi_parameter_extract.py | 99 ++ .../data-access/read_samples_to_list.py | 92 ++ .../python/getting-started/01_load_session.py | 83 + .../getting-started/02_read_parameters.py | 89 + .../getting-started/03_query_sessions.py | 68 + .../getting-started/04_extract_events.py | 84 + .../CoreLibraryTests.cs | 179 +++ .../GettingStartedTests.cs | 107 ++ .../SnippetCompilationTests.cs | 105 ++ .../SqlRace.Examples.Tests.csproj | 23 + tests/test-data/README.md | 16 + tools/FindDataParams/FindDataParams.csproj | 11 + tools/FindDataParams/Program.cs | 99 ++ tools/FindLiveSession/FindLiveSession.csproj | 12 + tools/FindLiveSession/Program.cs | 221 +++ tools/SnippetRunner/Program.cs | 82 + tools/SnippetRunner/Program.cs.bak | 22 + tools/SnippetRunner/SnippetRunner.csproj | 12 + 207 files changed, 11708 insertions(+), 6304 deletions(-) create mode 100644 .env.example delete mode 100644 .gitattributes create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .github/workflows/validate-notebooks.yml create mode 100644 CONCEPTS.md create mode 100644 COOKBOOK.md create mode 100644 LICENSE delete mode 100644 MAT.OCS.SQLRace.Examples.sln delete mode 100644 MAT.OCS.SQLRace.Examples.sln.DotSettings delete mode 100644 MAT.SQLRace.BaseExampleLoadSSN/App.config delete mode 100644 MAT.SQLRace.BaseExampleLoadSSN/MAT.SQLRace.BaseExampleLoadSSN.csproj delete mode 100644 MAT.SQLRace.BaseExampleLoadSSN/Program.cs delete mode 100644 MAT.SQLRace.CreateSessionWithMultipleParameters/App.config delete mode 100644 MAT.SQLRace.CreateSessionWithMultipleParameters/MAT.SQLRace.CreateSessionWithMultipleParameters.csproj delete mode 100644 MAT.SQLRace.CreateSessionWithMultipleParameters/Program.cs delete mode 100644 MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/App.config delete mode 100644 MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters.csproj delete mode 100644 MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/Program.cs delete mode 100644 MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/SessionHelper.cs delete mode 100644 MAT.SQLRace.HelloData/App.config delete mode 100644 MAT.SQLRace.HelloData/FunctionHelper.cs delete mode 100644 MAT.SQLRace.HelloData/MAT.SQLRace.HelloData.csproj delete mode 100644 MAT.SQLRace.HelloData/Program.cs delete mode 100644 MAT.SQLRace.HelloData/SessionHelper.cs delete mode 100644 MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/App.config delete mode 100644 MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters.csproj delete mode 100644 MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/Program.cs delete mode 100644 MAT.SqlRace.Functions.HelloDotNet/HelloFunction.cs delete mode 100644 MAT.SqlRace.Functions.HelloDotNet/HelloPdaCountFunction.cs delete mode 100644 MAT.SqlRace.Functions.HelloDotNet/MAT.SqlRace.Functions.HelloDotNet.csproj delete mode 100644 MAT.SqlRace.Functions.HelloDotNet/ReadMe.txt delete mode 100644 MAT.SqlRace.Functions.HelloDotNet/app.config delete mode 100644 MAT.SqlRace.ServerListener/App.config delete mode 100644 MAT.SqlRace.ServerListener/MAT.SqlRace.ServerListener.csproj delete mode 100644 MAT.SqlRace.ServerListener/Program.cs delete mode 100644 MAT.SqlRace.ServerListener/SessionHelper.cs delete mode 100644 MAT.SqlRace.StandaloneRecorder/App.config delete mode 100644 MAT.SqlRace.StandaloneRecorder/MAT.SqlRace.StandaloneRecorder.csproj delete mode 100644 MAT.SqlRace.StandaloneRecorder/Program.cs delete mode 100644 MAT.SqlRace.StandaloneRecorder/SessionHelper.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/FindAllSessionsExample.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/LoadSessionExample.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/MESL.SqlRace.Examples.Sessions.CSharp.csproj delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/MainForm.Designer.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/MainForm.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/MainForm.resx delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/Program.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/Properties/Resources.Designer.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/Properties/Resources.resx delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/Properties/Settings.Designer.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/Properties/Settings.settings delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/SearchSessionsByItemExample.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/SessionCreationExample.cs delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/System.Windows.Forms.DataVisualization.dll delete mode 100644 MESL.SqlRace.Examples.Sessions.CSharp/app.config delete mode 100644 Python/Readme.txt delete mode 100644 Python/main.py create mode 100644 docs/connection-strings.md create mode 100644 docs/getting-started.md create mode 100644 docs/industry-mapping.md create mode 100644 docs/server-listener.md create mode 100644 docs/troubleshooting.md create mode 100644 images/ATLAS_logo_GR.png delete mode 100644 images/atlaslogo.png create mode 100644 notebooks/01-getting-started.ipynb create mode 100644 notebooks/02-session-exploration.ipynb create mode 100644 notebooks/03-multi-parameter-analysis.ipynb create mode 100644 notebooks/04-session-comparison.ipynb create mode 100644 notebooks/05-export-pipeline.ipynb create mode 100644 notebooks/06-live-data-monitoring.ipynb create mode 100644 notebooks/README.md create mode 100644 notebooks/requirements.txt create mode 100644 notebooks/scenarios/data/durability-cycle1.ssn2 create mode 100644 notebooks/scenarios/data/durability-cycle2.ssn2 create mode 100644 notebooks/scenarios/data/durability-cycle3.ssn2 create mode 100644 notebooks/scenarios/data/flight-test-manoeuvre-extraction.ssn2 create mode 100644 notebooks/scenarios/data/motorsport-lap-analysis.ssn2 create mode 100644 notebooks/scenarios/data/turbine-week1.ssn2 create mode 100644 notebooks/scenarios/data/turbine-week2.ssn2 create mode 100644 notebooks/scenarios/data/turbine-week3.ssn2 create mode 100644 notebooks/scenarios/durability-cycle-comparison.ipynb create mode 100644 notebooks/scenarios/flight-test-manoeuvre-extraction.ipynb create mode 100644 notebooks/scenarios/motorsport-lap-analysis.ipynb create mode 100644 notebooks/scenarios/turbine-performance-trending.ipynb create mode 100644 notebooks/sqlrace_helpers.py create mode 100644 nuget.config create mode 100644 projects/README.md create mode 100644 projects/csharp/BatchExporter/BatchExporter.csproj create mode 100644 projects/csharp/BatchExporter/CsvWriter.cs create mode 100644 projects/csharp/BatchExporter/ExportConfig.cs create mode 100644 projects/csharp/BatchExporter/Program.cs create mode 100644 projects/csharp/BatchExporter/README.md create mode 100644 projects/csharp/BatchExporter/appsettings.json create mode 100644 projects/csharp/Directory.Build.props create mode 100644 projects/csharp/FunctionLibrary/FunctionLibrary.csproj create mode 100644 projects/csharp/FunctionLibrary/README.md create mode 100644 projects/csharp/FunctionLibrary/RollingAverageFunction.cs create mode 100644 projects/csharp/FunctionLibrary/StatisticsFunction.cs create mode 100644 projects/csharp/FunctionLibrary/UnitConverterFunction.cs create mode 100644 projects/csharp/GettingStarted/GettingStarted.csproj create mode 100644 projects/csharp/GettingStarted/Program.cs create mode 100644 projects/csharp/LiveDashboard/DashboardRenderer.cs create mode 100644 projects/csharp/LiveDashboard/LiveDashboard.csproj create mode 100644 projects/csharp/LiveDashboard/Program.cs create mode 100644 projects/csharp/LiveDashboard/README.md create mode 100644 projects/csharp/LiveDashboard/appsettings.json create mode 100644 projects/csharp/ServerListenerRecorder/Program.cs create mode 100644 projects/csharp/ServerListenerRecorder/README.md create mode 100644 projects/csharp/ServerListenerRecorder/RecorderConfig.cs create mode 100644 projects/csharp/ServerListenerRecorder/ServerListenerRecorder.csproj create mode 100644 projects/csharp/ServerListenerRecorder/appsettings.json create mode 100644 projects/csharp/SessionCropper/Program.cs create mode 100644 projects/csharp/SessionCropper/README.md create mode 100644 projects/csharp/SessionCropper/SessionCropper.csproj create mode 100644 projects/csharp/SqlRace.Examples.Core/ConnectionStringProvider.cs create mode 100644 projects/csharp/SqlRace.Examples.Core/ParameterBuilder.cs create mode 100644 projects/csharp/SqlRace.Examples.Core/README.md create mode 100644 projects/csharp/SqlRace.Examples.Core/SessionFactory.cs create mode 100644 projects/csharp/SqlRace.Examples.Core/SqlRace.Examples.Core.csproj create mode 100644 projects/csharp/SqlRace.Examples.Core/SqlRaceBootstrapper.cs create mode 100644 projects/csharp/SqlRace.Examples.sln create mode 100644 projects/csharp/StandaloneRecorder/Program.cs create mode 100644 projects/csharp/StandaloneRecorder/README.md create mode 100644 projects/csharp/StandaloneRecorder/RecorderConfig.cs create mode 100644 projects/csharp/StandaloneRecorder/StandaloneRecorder.csproj create mode 100644 projects/csharp/StandaloneRecorder/appsettings.json create mode 100644 projects/matlab/session_analysis/README.md create mode 100644 projects/matlab/session_analysis/analyse_session.m create mode 100644 projects/matlab/session_analysis/dotnetcli_netcore.runtimeconfig.json create mode 100644 projects/matlab/session_analysis/session_results.mat create mode 100644 projects/matlab/session_analysis/setup_sqlrace.m create mode 100644 projects/python/atlas_automation/README.md create mode 100644 projects/python/atlas_automation/atlas_controller.py create mode 100644 projects/python/atlas_automation/requirements.txt create mode 100644 projects/python/session_explorer/README.md create mode 100644 projects/python/session_explorer/requirements.txt create mode 100644 projects/python/session_explorer/session_explorer.py create mode 100644 scripts/hooks/pre-commit create mode 100644 scripts/install-hooks.sh create mode 100644 scripts/verify-snippet-errors.ps1 create mode 100644 scripts/verify-snippets.ps1 create mode 100644 snippets/README.md create mode 100644 snippets/csharp/README.md create mode 100644 snippets/csharp/composite-sessions/append-multiple-sessions.cs create mode 100644 snippets/csharp/composite-sessions/composite-with-associates.cs create mode 100644 snippets/csharp/composite-sessions/whole-session-compare.cs create mode 100644 snippets/csharp/configuration/conversion-types.cs create mode 100644 snippets/csharp/configuration/create-parameter-config.cs create mode 100644 snippets/csharp/configuration/create-transient-parameter.cs create mode 100644 snippets/csharp/configuration/introspect-session-config.cs create mode 100644 snippets/csharp/data-access/bulk-parameter-read.cs create mode 100644 snippets/csharp/data-access/data-status-filtering.cs create mode 100644 snippets/csharp/data-access/multi-rate-alignment.cs create mode 100644 snippets/csharp/data-access/read-samples-between.cs create mode 100644 snippets/csharp/data-access/read-subsampled-data.cs create mode 100644 snippets/csharp/data-access/reverse-iteration.cs create mode 100644 snippets/csharp/data-access/timestamp-array-construction.cs create mode 100644 snippets/csharp/functions/dotnet-function-basic.cs create mode 100644 snippets/csharp/functions/dotnet-function-with-pda.cs create mode 100644 snippets/csharp/functions/fdl-function-basic.cs create mode 100644 snippets/csharp/getting-started/01-load-session-from-file.cs create mode 100644 snippets/csharp/getting-started/02-load-session-from-database.cs create mode 100644 snippets/csharp/getting-started/03-read-parameter-samples.cs create mode 100644 snippets/csharp/getting-started/04-create-session-write-data.cs create mode 100644 snippets/csharp/getting-started/05-query-sessions-by-metadata.cs create mode 100644 snippets/csharp/live-data/poll-live-samples.cs create mode 100644 snippets/csharp/live-data/rda-parameter-change-detection.cs create mode 100644 snippets/csharp/live-data/subscribe-to-data-events.cs create mode 100644 snippets/csharp/live-data/subscribe-to-lap-events.cs create mode 100644 snippets/csharp/session-management/event-definitions-and-data.cs create mode 100644 snippets/csharp/session-management/lap-and-segment-management.cs create mode 100644 snippets/csharp/session-management/marker-management.cs create mode 100644 snippets/csharp/session-management/parameter-unit-resolution.cs create mode 100644 snippets/csharp/session-management/query-with-composite-filter.cs create mode 100644 snippets/csharp/session-management/session-association.cs create mode 100644 snippets/csharp/session-management/session-metadata-crud.cs create mode 100644 snippets/matlab/README.md create mode 100644 snippets/matlab/data-access/extract_to_timetable.m create mode 100644 snippets/matlab/data-access/multi_channel_read.m create mode 100644 snippets/matlab/data-access/segment_statistics.m create mode 100644 snippets/matlab/getting-started/diagnose_setup.m create mode 100644 snippets/matlab/getting-started/load_session.m create mode 100644 snippets/matlab/getting-started/query_sessions.m create mode 100644 snippets/matlab/getting-started/read_parameters.m create mode 100644 snippets/python/README.md create mode 100644 snippets/python/data-access/export_to_pandas.py create mode 100644 snippets/python/data-access/multi_parameter_extract.py create mode 100644 snippets/python/data-access/read_samples_to_list.py create mode 100644 snippets/python/getting-started/01_load_session.py create mode 100644 snippets/python/getting-started/02_read_parameters.py create mode 100644 snippets/python/getting-started/03_query_sessions.py create mode 100644 snippets/python/getting-started/04_extract_events.py create mode 100644 tests/SqlRace.Examples.Tests/CoreLibraryTests.cs create mode 100644 tests/SqlRace.Examples.Tests/GettingStartedTests.cs create mode 100644 tests/SqlRace.Examples.Tests/SnippetCompilationTests.cs create mode 100644 tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj create mode 100644 tests/test-data/README.md create mode 100644 tools/FindDataParams/FindDataParams.csproj create mode 100644 tools/FindDataParams/Program.cs create mode 100644 tools/FindLiveSession/FindLiveSession.csproj create mode 100644 tools/FindLiveSession/Program.cs create mode 100644 tools/SnippetRunner/Program.cs create mode 100644 tools/SnippetRunner/Program.cs.bak create mode 100644 tools/SnippetRunner/SnippetRunner.csproj diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..55ab1b7 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +export NUGET_AUTH_USERNAME= +export NUGET_AUTH_TOKEN= +export SQLRACE_DLL_PATH="C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll" +export ATLAS_EXE_PATH="C:\Program Files\McLaren Applied Technologies\ATLAS 10\ATLAS.exe" +export SQLRACE_CONNECTION_STRING= \ No newline at end of file diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 1ff0c42..0000000 --- a/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..ac25e2f --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing + +Thank you for your interest in contributing to the SQL Race API Examples. + +## Guidelines + +### Snippets (Tier 1) + +- Must be self-contained in a single file +- Must include all `using` statements and a `Main` method +- Must reference only `MESL.SQLRace.API` — no other dependencies +- Must be under 80 lines +- Must use domain-neutral parameter names (not motorsport-specific) +- Must default to a local SQLite connection string +- Must include a header comment block with title, description, prerequisites, and expected output + +### Projects (Tier 2) + +- Must build and run with `dotnet run` +- Should use the shared Core library where appropriate +- Must follow the same naming conventions as snippets + +### General + +- Use modern C# 12 / .NET 8 conventions +- Use `using` declarations for all `IDisposable` resources +- Never hard-code server names, file paths, or session GUIDs +- Write helpful error messages for null/not-found cases + +## Submitting Changes + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Ensure all tests pass: `dotnet test` +5. Submit a pull request + +## Reporting Issues + +Use the [issue templates](ISSUE_TEMPLATE/) to report bugs, request examples, or suggest industry-specific scenarios. diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..bb132ba --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,39 @@ +name: Build and Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore projects + run: dotnet restore projects/csharp/SqlRace.Examples.sln + env: + NUGET_AUTH_USERNAME: github-actions + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build projects + run: dotnet build projects/csharp/SqlRace.Examples.sln --no-restore --configuration Release + + - name: Restore tests + run: dotnet restore tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj + env: + NUGET_AUTH_USERNAME: github-actions + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build tests + run: dotnet build tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj --no-restore --configuration Release + + - name: Run tests + run: dotnet test tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj --no-build --configuration Release --verbosity normal diff --git a/.github/workflows/validate-notebooks.yml b/.github/workflows/validate-notebooks.yml new file mode 100644 index 0000000..5ddf059 --- /dev/null +++ b/.github/workflows/validate-notebooks.yml @@ -0,0 +1,48 @@ +name: Validate Notebooks + +on: + push: + branches: [main] + paths: ['notebooks/**'] + pull_request: + branches: [main] + paths: ['notebooks/**'] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: pip install nbformat + + - name: Validate notebook JSON structure + run: | + python -c " + import nbformat, glob, sys + errors = [] + for nb_path in sorted(glob.glob('notebooks/**/*.ipynb', recursive=True)): + try: + nb = nbformat.read(nb_path, as_version=4) + # Check basic structure + assert len(nb.cells) > 0, 'No cells' + assert nb.cells[0].cell_type == 'markdown', 'First cell should be markdown' + print(f' OK: {nb_path} ({len(nb.cells)} cells)') + except Exception as e: + errors.append(f'{nb_path}: {e}') + if errors: + print('\nErrors:') + print('\n'.join(errors)) + sys.exit(1) + print(f'\nAll {len(list(glob.glob(\"notebooks/**/*.ipynb\", recursive=True)))} notebooks valid.') + " + + - name: Check helpers module syntax + run: | + python -c "import ast; ast.parse(open('notebooks/sqlrace_helpers.py').read()); print('sqlrace_helpers.py syntax OK')" diff --git a/.gitignore b/.gitignore index 4ce6fdd..0ab6250 100644 --- a/.gitignore +++ b/.gitignore @@ -1,340 +1,11 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo +MAT.OCS.SQLRace.Examples/ +bin/ +obj/ *.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory +*.suo .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper *.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- Backup*.rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ +CLAUDE.md *.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb \ No newline at end of file +.env +.claude \ No newline at end of file diff --git a/CONCEPTS.md b/CONCEPTS.md new file mode 100644 index 0000000..31438b5 --- /dev/null +++ b/CONCEPTS.md @@ -0,0 +1,69 @@ +# Cross-Industry Concept Mapping + +The SQL Race API uses a set of core concepts for organising time-series data. These concepts have direct equivalents in every industry that uses the platform. This guide maps SQL Race terminology to domain-specific language so you can read the examples in terms you already know. + +## Core Concepts + +| SQL Race Concept | Description | Motorsport | Aerospace | Energy | Automotive | Industrial / Manufacturing | +|-----------------|-------------|-----------|-----------|--------|------------|---------------------------| +| **Session** | A contiguous recording of time-series data | Race, Practice, Qualifying | Flight, Sortie, Ground Run | Test Run, Commissioning Run | Drive Cycle, Durability Run | Production Batch, Test Cycle | +| **Lap** | A subdivision of a session at defined trigger points | Lap (track circuit) | Test Point, Manoeuvre | Operating Regime, Load Step | Route Segment, Phase | Machine Cycle, Batch Step | +| **Parameter** | A named data channel with a sample rate and unit | Channel (vCar, nEngine) | Measurement (ALT, IAS) | Tag (P101, T201) | Signal (VehicleSpeed) | Process Variable (PV) | +| **Parameter Group** | Logical grouping of related parameters | System (Chassis, Engine) | Subsystem (Flight Controls) | Loop Group (Turbine) | Domain (Powertrain) | Unit Area (Reactor 1) | +| **Application Group** | Source-system grouping for parameters | ECU / Data Source | Avionics Bus | DCS / SCADA | CAN Bus / ECU | PLC / Controller | +| **Event** | A discrete occurrence with a timestamp and priority | Flag, Incident, Pit Stop | Caution, Alert, Exceedance | Alarm, Trip, Setpoint Change | DTC, Warning | Fault, Alarm, Interlock | +| **Marker** | A labelled time range within a session | Stint, Sector | Flight Phase, Test Card | Steady State Window | Test Phase | Process Stage | +| **Constant** | A scalar value that applies to the whole session | Car Number, Driver | Tail Number, Pilot | Unit ID, Test Rig | VIN, Driver ID | Serial Number, Batch ID | +| **Session Data Item** | Key-value metadata attached to a session | Circuit, Weather | Airfield, Sortie Type | Plant, Test Objective | Proving Ground, Test Type | Facility, Work Order | +| **Configuration** | Schema definition for parameters, channels, conversions | Config Set | ICD (Interface Control Document) | Point List | DBC / Signal Database | Tag Database | + +## Time Model + +| Concept | Description | Typical Values | +|---------|-------------|----------------| +| **Timestamp** | Nanoseconds since midnight (00:00:00.000) on the session date | `long` — e.g., `36000000000000` = 10 hours | +| **Frequency** | Sample rate of a channel | 1 Hz (slow telemetry) to 20,000 Hz (vibration) | +| **Interval** | Time between samples in nanoseconds (`1 / frequency`) | 1 Hz = 1,000,000,000 ns; 100 Hz = 10,000,000 ns | + +### Converting between time representations + +``` +seconds since midnight → nanoseconds: seconds * 1_000_000_000 +hours:minutes:seconds → nanoseconds: (h*3600 + m*60 + s) * 1_000_000_000 +nanoseconds → seconds: nanoseconds / 1_000_000_000.0 +``` + +## Data Types + +| SQL Race DataType | .NET Type | Size | Typical Use | +|-------------------|-----------|------|-------------| +| `Double64Bit` | `double` | 8 bytes | High-precision measurements (temperature, pressure) | +| `FloatingPoint32Bit` | `float` | 4 bytes | Standard measurements (speed, voltage) | +| `Signed16Bit` | `short` | 2 bytes | Raw ADC values, compact storage | +| `Unsigned8Bit` | `byte` | 1 byte | Status flags, digital I/O | + +## Session Types + +| Type | When to use | +|------|-------------| +| **File Session** (`.ssn2`) | Offline analysis of recorded data. Self-contained SQLite database. | +| **Database Session** | Live or historical data on a SQL Server instance. Multi-user access. | +| **Composite Session** | Virtual session combining multiple sessions for comparison or aggregation. | + +## Parameter Naming Convention + +This repository uses **domain-neutral** parameter names in all examples: + +``` +{Measurement}:{ApplicationGroup} +``` + +Examples: +- `Temperature:Sensors` — a temperature reading from a sensor group +- `Pressure:Inlet` — inlet pressure measurement +- `Speed:Shaft` — shaft rotational speed +- `Acceleration:Structure` — structural acceleration (vibration) +- `Voltage:Battery` — battery voltage +- `FlowRate:Fuel` — fuel flow rate + +In your own code, use names that match your domain — the API imposes no naming restrictions. diff --git a/COOKBOOK.md b/COOKBOOK.md new file mode 100644 index 0000000..d10d4c5 --- /dev/null +++ b/COOKBOOK.md @@ -0,0 +1,126 @@ +# Cookbook + +A task-based index: find the example you need by what you want to accomplish. + +## Getting Started + +| How do I... | C# | Python | MATLAB | +|------------|-----|--------|--------| +| Load a session from a `.ssn2` file? | [`01-load-session-from-file.cs`](snippets/csharp/getting-started/01-load-session-from-file.cs) | [`01_load_session.py`](snippets/python/getting-started/01_load_session.py) | [`load_session.m`](snippets/matlab/getting-started/load_session.m) | +| Load a session from a database? | [`02-load-session-from-database.cs`](snippets/csharp/getting-started/02-load-session-from-database.cs) | [`01_load_session.py`](snippets/python/getting-started/01_load_session.py) | [`load_session.m`](snippets/matlab/getting-started/load_session.m) | +| Read parameter samples? | [`03-read-parameter-samples.cs`](snippets/csharp/getting-started/03-read-parameter-samples.cs) | [`02_read_parameters.py`](snippets/python/getting-started/02_read_parameters.py) | [`read_parameters.m`](snippets/matlab/getting-started/read_parameters.m) | +| Create a session and write data? | [`04-create-session-write-data.cs`](snippets/csharp/getting-started/04-create-session-write-data.cs) | — | — | +| Find sessions by metadata? | [`05-query-sessions-by-metadata.cs`](snippets/csharp/getting-started/05-query-sessions-by-metadata.cs) | [`03_query_sessions.py`](snippets/python/getting-started/03_query_sessions.py) | [`query_sessions.m`](snippets/matlab/getting-started/query_sessions.m) | +| Verify MATLAB + SQL Race setup? | — | — | [`diagnose_setup.m`](snippets/matlab/getting-started/diagnose_setup.m) | + +## Data Access + +| How do I... | C# | Python | MATLAB | +|------------|-----|--------|--------| +| Read samples in a time range? | [`read-samples-between.cs`](snippets/csharp/data-access/read-samples-between.cs) | [`read_samples_to_list.py`](snippets/python/data-access/read_samples_to_list.py) | [`extract_to_timetable.m`](snippets/matlab/data-access/extract_to_timetable.m) | +| Downsample / subsample data? | [`read-subsampled-data.cs`](snippets/csharp/data-access/read-subsampled-data.cs) | — | — | +| Read data backwards (reverse iteration)? | [`reverse-iteration.cs`](snippets/csharp/data-access/reverse-iteration.cs) | — | — | +| Align parameters at different rates? | [`multi-rate-alignment.cs`](snippets/csharp/data-access/multi-rate-alignment.cs) | — | — | +| Read many parameters efficiently? | [`bulk-parameter-read.cs`](snippets/csharp/data-access/bulk-parameter-read.cs) | [`multi_parameter_extract.py`](snippets/python/data-access/multi_parameter_extract.py) | [`multi_channel_read.m`](snippets/matlab/data-access/multi_channel_read.m) | +| Handle missing or invalid data? | [`data-status-filtering.cs`](snippets/csharp/data-access/data-status-filtering.cs) | — | — | +| Build timestamp arrays for GetData? | [`timestamp-array-construction.cs`](snippets/csharp/data-access/timestamp-array-construction.cs) | — | — | +| Export data to pandas? | — | [`export_to_pandas.py`](snippets/python/data-access/export_to_pandas.py) | — | +| Export data to MATLAB timetable? | — | — | [`extract_to_timetable.m`](snippets/matlab/data-access/extract_to_timetable.m) | +| Get per-segment statistics? | — | — | [`segment_statistics.m`](snippets/matlab/data-access/segment_statistics.m) | + +## Session Management + +| How do I... | C# | Python | +|------------|-----|--------| +| Find sessions in a date range? | [`query-with-composite-filter.cs`](snippets/csharp/session-management/query-with-composite-filter.cs) | [`03_query_sessions.py`](snippets/python/getting-started/03_query_sessions.py) | +| Read and write session metadata? | [`session-metadata-crud.cs`](snippets/csharp/session-management/session-metadata-crud.cs) | — | +| Add annotations to a session? | [`marker-management.cs`](snippets/csharp/session-management/marker-management.cs) | — | +| Work with laps and segments? | [`lap-and-segment-management.cs`](snippets/csharp/session-management/lap-and-segment-management.cs) | — | +| Define and read events? | [`event-definitions-and-data.cs`](snippets/csharp/session-management/event-definitions-and-data.cs) | [`04_extract_events.py`](snippets/python/getting-started/04_extract_events.py) | +| Associate sessions together? | [`session-association.cs`](snippets/csharp/session-management/session-association.cs) | — | +| Resolve parameter units? | [`parameter-unit-resolution.cs`](snippets/csharp/session-management/parameter-unit-resolution.cs) | — | + +## Live Data + +| How do I... | C# | +|------------|-----| +| React to new laps? | [`subscribe-to-lap-events.cs`](snippets/csharp/live-data/subscribe-to-lap-events.cs) | +| React to new data arriving? | [`subscribe-to-data-events.cs`](snippets/csharp/live-data/subscribe-to-data-events.cs) | +| Poll for the latest samples? | [`poll-live-samples.cs`](snippets/csharp/live-data/poll-live-samples.cs) | +| Detect when new parameters appear? | [`rda-parameter-change-detection.cs`](snippets/csharp/live-data/rda-parameter-change-detection.cs) | + +## Functions (Calculated Channels) + +| How do I... | C# | +|------------|-----| +| Create a calculated channel with FDL? | [`fdl-function-basic.cs`](snippets/csharp/functions/fdl-function-basic.cs) | +| Create a calculated channel with .NET? | [`dotnet-function-basic.cs`](snippets/csharp/functions/dotnet-function-basic.cs) | +| Use a PDA inside a .NET function? | [`dotnet-function-with-pda.cs`](snippets/csharp/functions/dotnet-function-with-pda.cs) | + +## Composite Sessions + +| How do I... | C# | +|------------|-----| +| Append multiple sessions? | [`append-multiple-sessions.cs`](snippets/csharp/composite-sessions/append-multiple-sessions.cs) | +| Compare whole sessions? | [`whole-session-compare.cs`](snippets/csharp/composite-sessions/whole-session-compare.cs) | +| Use composites with associates? | [`composite-with-associates.cs`](snippets/csharp/composite-sessions/composite-with-associates.cs) | + +## Configuration + +| How do I... | C# | +|------------|-----| +| Set up parameter configuration manually? | [`create-parameter-config.cs`](snippets/csharp/configuration/create-parameter-config.cs) | +| Create in-memory-only parameters? | [`create-transient-parameter.cs`](snippets/csharp/configuration/create-transient-parameter.cs) | +| Inspect a session's configuration? | [`introspect-session-config.cs`](snippets/csharp/configuration/introspect-session-config.cs) | +| Use different conversion types? | [`conversion-types.cs`](snippets/csharp/configuration/conversion-types.cs) | + +## Projects (Tier 2) + +For complete, buildable applications, see [projects/](projects/). + +| How do I... | Project | +|------------|---------| +| Record live data to SQLite? | [ServerListenerRecorder](projects/csharp/ServerListenerRecorder/) | +| Connect to an ATLAS Data Server? | [StandaloneRecorder](projects/csharp/StandaloneRecorder/) | +| Export sessions to CSV? | [BatchExporter](projects/csharp/BatchExporter/) | +| Monitor a live session in real time? | [LiveDashboard](projects/csharp/LiveDashboard/) | +| Create custom .NET functions? | [FunctionLibrary](projects/csharp/FunctionLibrary/) | +| Browse and export sessions from Python? | [session_explorer](projects/python/session_explorer/) | +| Control ATLAS from Python? | [atlas_automation](projects/python/atlas_automation/) | +| Analyse sessions in MATLAB? | [session_analysis](projects/matlab/session_analysis/) | + +### All C# projects + +| Project | Description | +|---------|-------------| +| `SqlRace.Examples.Core` | Shared library: bootstrapping, connection strings, fluent builders | +| `GettingStarted` | Console app demonstrating the Core library API | +| `ServerListenerRecorder` | Records multi-rate telemetry to SQLite | +| `StandaloneRecorder` | ADS connection with read-transform-write loop | +| `BatchExporter` | Queries sessions and exports to CSV | +| `LiveDashboard` | Text-based real-time parameter monitor | +| `FunctionLibrary` | Three .NET function patterns (converter, rolling avg, cross-param) | + +## Notebooks (Tier 3) + +Interactive Jupyter notebooks for exploring SQL Race data. See [notebooks/](notebooks/). + +### Core Notebooks + +| How do I... | Notebook | +|------------|----------| +| Get started with SQL Race from Python? | [01-getting-started.ipynb](notebooks/01-getting-started.ipynb) | +| Explore session structure and metadata? | [02-session-exploration.ipynb](notebooks/02-session-exploration.ipynb) | +| Analyse multiple parameters together? | [03-multi-parameter-analysis.ipynb](notebooks/03-multi-parameter-analysis.ipynb) | +| Compare data across sessions? | [04-session-comparison.ipynb](notebooks/04-session-comparison.ipynb) | +| Export data to CSV, Parquet, HDF5? | [05-export-pipeline.ipynb](notebooks/05-export-pipeline.ipynb) | +| Monitor a live session? | [06-live-data-monitoring.ipynb](notebooks/06-live-data-monitoring.ipynb) | + +### Industry Scenarios + +| How do I... | Notebook | +|------------|----------| +| Analyse lap-by-lap telemetry? | [motorsport-lap-analysis.ipynb](notebooks/scenarios/motorsport-lap-analysis.ipynb) | +| Extract flight test manoeuvre windows? | [flight-test-manoeuvre-extraction.ipynb](notebooks/scenarios/flight-test-manoeuvre-extraction.ipynb) | +| Trend turbine performance over time? | [turbine-performance-trending.ipynb](notebooks/scenarios/turbine-performance-trending.ipynb) | +| Compare durability test cycles? | [durability-cycle-comparison.ipynb](notebooks/scenarios/durability-cycle-comparison.ipynb) | diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..57f662b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Motion Applied Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MAT.OCS.SQLRace.Examples.sln b/MAT.OCS.SQLRace.Examples.sln deleted file mode 100644 index 8c7aba3..0000000 --- a/MAT.OCS.SQLRace.Examples.sln +++ /dev/null @@ -1,92 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.6.33829.357 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MAT.SQLRace.HelloData", "MAT.SQLRace.HelloData\MAT.SQLRace.HelloData.csproj", "{FB3E8850-40AF-443E-82C1-183C1C76D460}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MESL.SqlRace.Examples.Sessions.CSharp", "MESL.SqlRace.Examples.Sessions.CSharp\MESL.SqlRace.Examples.Sessions.CSharp.csproj", "{1ECFBECA-FA58-414F-8F13-6F94747ED086}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MAT.SqlRace.StandaloneRecorder", "MAT.SqlRace.StandaloneRecorder\MAT.SqlRace.StandaloneRecorder.csproj", "{CED8F031-F058-451E-8B4A-A773D806FC79}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F7758EB4-AD23-4B60-994E-6832A3230138}" - ProjectSection(SolutionItems) = preProject - readme.md = readme.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MAT.SqlRace.Functions.HelloDotNet", "MAT.SqlRace.Functions.HelloDotNet\MAT.SqlRace.Functions.HelloDotNet.csproj", "{4CBB924C-6346-4C08-B1C8-AB5EA2900348}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MAT.SQLRace.CreateSessionWithMultipleParameters", "MAT.SQLRace.CreateSessionWithMultipleParameters\MAT.SQLRace.CreateSessionWithMultipleParameters.csproj", "{87B98171-87C4-4BF4-AEB9-EB4B7927A66E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{41650D89-FF13-4859-A107-42236E969BE1}" - ProjectSection(SolutionItems) = preProject - .github\workflows\Check-References.ps1 = .github\workflows\Check-References.ps1 - .github\workflows\Get-Package-Version.ps1 = .github\workflows\Get-Package-Version.ps1 - .github\workflows\pr.yml = .github\workflows\pr.yml - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters", "MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters\MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters.csproj", "{77769120-AD6B-47C8-AB4D-11CBFF71952E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters", "MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters\MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters.csproj", "{FF2BE15F-8E2A-4247-B25D-0FFE2FEFFF20}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MAT.SqlRace.ServerListener", "MAT.SqlRace.ServerListener\MAT.SqlRace.ServerListener.csproj", "{C4281449-70AD-4A9F-A5F8-A11C6E58E460}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MAT.SQLRace.BaseExampleLoadSSN", "MAT.SQLRace.BaseExampleLoadSSN\MAT.SQLRace.BaseExampleLoadSSN.csproj", "{D1D1CDE3-C89A-0D03-1657-550AB821658B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FB3E8850-40AF-443E-82C1-183C1C76D460}.Debug|x64.ActiveCfg = Debug|x64 - {FB3E8850-40AF-443E-82C1-183C1C76D460}.Debug|x64.Build.0 = Debug|x64 - {FB3E8850-40AF-443E-82C1-183C1C76D460}.Release|x64.ActiveCfg = Release|x64 - {FB3E8850-40AF-443E-82C1-183C1C76D460}.Release|x64.Build.0 = Release|x64 - {1ECFBECA-FA58-414F-8F13-6F94747ED086}.Debug|x64.ActiveCfg = Debug|x64 - {1ECFBECA-FA58-414F-8F13-6F94747ED086}.Debug|x64.Build.0 = Debug|x64 - {1ECFBECA-FA58-414F-8F13-6F94747ED086}.Release|x64.ActiveCfg = Release|x64 - {1ECFBECA-FA58-414F-8F13-6F94747ED086}.Release|x64.Build.0 = Release|x64 - {CED8F031-F058-451E-8B4A-A773D806FC79}.Debug|x64.ActiveCfg = Debug|x64 - {CED8F031-F058-451E-8B4A-A773D806FC79}.Debug|x64.Build.0 = Debug|x64 - {CED8F031-F058-451E-8B4A-A773D806FC79}.Release|x64.ActiveCfg = Release|x64 - {CED8F031-F058-451E-8B4A-A773D806FC79}.Release|x64.Build.0 = Release|x64 - {4CBB924C-6346-4C08-B1C8-AB5EA2900348}.Debug|x64.ActiveCfg = Debug|x64 - {4CBB924C-6346-4C08-B1C8-AB5EA2900348}.Debug|x64.Build.0 = Debug|x64 - {4CBB924C-6346-4C08-B1C8-AB5EA2900348}.Release|x64.ActiveCfg = Release|x64 - {4CBB924C-6346-4C08-B1C8-AB5EA2900348}.Release|x64.Build.0 = Release|x64 - {87B98171-87C4-4BF4-AEB9-EB4B7927A66E}.Debug|x64.ActiveCfg = Debug|x64 - {87B98171-87C4-4BF4-AEB9-EB4B7927A66E}.Debug|x64.Build.0 = Debug|x64 - {87B98171-87C4-4BF4-AEB9-EB4B7927A66E}.Release|x64.ActiveCfg = Release|x64 - {87B98171-87C4-4BF4-AEB9-EB4B7927A66E}.Release|x64.Build.0 = Release|x64 - {77769120-AD6B-47C8-AB4D-11CBFF71952E}.Debug|x64.ActiveCfg = Debug|x64 - {77769120-AD6B-47C8-AB4D-11CBFF71952E}.Debug|x64.Build.0 = Debug|x64 - {77769120-AD6B-47C8-AB4D-11CBFF71952E}.Release|x64.ActiveCfg = Release|x64 - {77769120-AD6B-47C8-AB4D-11CBFF71952E}.Release|x64.Build.0 = Release|x64 - {FF2BE15F-8E2A-4247-B25D-0FFE2FEFFF20}.Debug|x64.ActiveCfg = Debug|x64 - {FF2BE15F-8E2A-4247-B25D-0FFE2FEFFF20}.Debug|x64.Build.0 = Debug|x64 - {FF2BE15F-8E2A-4247-B25D-0FFE2FEFFF20}.Release|x64.ActiveCfg = Release|x64 - {FF2BE15F-8E2A-4247-B25D-0FFE2FEFFF20}.Release|x64.Build.0 = Release|x64 - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Debug|x64.ActiveCfg = Debug|x64 - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Debug|x64.Build.0 = Debug|x64 - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Release|x64.ActiveCfg = Release|x64 - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Release|x64.Build.0 = Release|x64 - {D1D1CDE3-C89A-0D03-1657-550AB821658B}.Debug|x64.ActiveCfg = Debug|x64 - {D1D1CDE3-C89A-0D03-1657-550AB821658B}.Debug|x64.Build.0 = Debug|x64 - {D1D1CDE3-C89A-0D03-1657-550AB821658B}.Release|x64.ActiveCfg = Release|x64 - {D1D1CDE3-C89A-0D03-1657-550AB821658B}.Release|x64.Build.0 = Release|x64 - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Debug|x86.ActiveCfg = Debug|Any CPU - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Debug|x86.Build.0 = Debug|Any CPU - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Release|Any CPU.Build.0 = Release|Any CPU - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Release|x64.ActiveCfg = Release|Any CPU - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Release|x64.Build.0 = Release|Any CPU - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Release|x86.ActiveCfg = Release|Any CPU - {C4281449-70AD-4A9F-A5F8-A11C6E58E460}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {A010034A-605F-4BB4-BDE7-30E9C13E3CCB} - EndGlobalSection -EndGlobal diff --git a/MAT.OCS.SQLRace.Examples.sln.DotSettings b/MAT.OCS.SQLRace.Examples.sln.DotSettings deleted file mode 100644 index 8def95c..0000000 --- a/MAT.OCS.SQLRace.Examples.sln.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - True - True - True \ No newline at end of file diff --git a/MAT.SQLRace.BaseExampleLoadSSN/App.config b/MAT.SQLRace.BaseExampleLoadSSN/App.config deleted file mode 100644 index fa04e50..0000000 --- a/MAT.SQLRace.BaseExampleLoadSSN/App.config +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/MAT.SQLRace.BaseExampleLoadSSN/MAT.SQLRace.BaseExampleLoadSSN.csproj b/MAT.SQLRace.BaseExampleLoadSSN/MAT.SQLRace.BaseExampleLoadSSN.csproj deleted file mode 100644 index 4ecf9c3..0000000 --- a/MAT.SQLRace.BaseExampleLoadSSN/MAT.SQLRace.BaseExampleLoadSSN.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - net8.0-windows - Exe - BaseExample - BaseExample - false - true - true - x64 - x64 - - - - - - \ No newline at end of file diff --git a/MAT.SQLRace.BaseExampleLoadSSN/Program.cs b/MAT.SQLRace.BaseExampleLoadSSN/Program.cs deleted file mode 100644 index 637ae71..0000000 --- a/MAT.SQLRace.BaseExampleLoadSSN/Program.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.IO; -using MESL.SqlRace.Common; -using MESL.SqlRace.Common.Extensions; -using MESL.SqlRace.Domain; - -namespace BaseExampleSSN -{ - class Program - { - /// - /// In order to be able to use this example follow these instructions; - /// https://github.com/mat-docs/packages - /// - /// Whenever you are setting up your project you should use .NET 8. - /// - /// Once registered in Visual Studio, you need to install MESL.SQLRace.API package from NuGet making sure that you have selected the McLaren Applied Github packages in the top - /// right corner (defaults to nuget.org, where if you search for the package it would not show). - /// Apart from this NuGet package, if you plan to use SSN files you need to follow these steps too: - /// - Install MAT.ATLAS.SupportFilesLite from NuGet directory making sure that you have selected on the top right corner MA repository - /// - Setup the build config to build in x64 based CPUs. This is required for SSNs to be loaded. - /// Once this package is installed, you would only need to pay attention to the TODOs written in the code. - /// - /// Find the documentation for the API in the link below - /// https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html - /// - /// Further examples can be found here: - /// https://github.com/mat-docs/MAT.OCS.SQLRace.Examples - /// - /// - static void Main(string[] args) - { - //TODO: Change the path to a .ssn file that you have locally. - const string pathToFile = @"C:\Users\YOUR_USER\SOME_FOLDER\SOME_FILE.ssn"; - - //Initialize SQLRaceAPI - Console.WriteLine("Initializing SQL Race...."); - Console.WriteLine(Directory.GetCurrentDirectory()); - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - - Console.WriteLine("SQLRace has been initialized correctly"); - var clientSession = LoadSession(pathToFile); - ReadSamples(clientSession); - - Console.WriteLine("Press enter to exit..."); - Console.ReadLine(); - Log.SetMinimumLoggingLevel(LogLevel.Warn); - } - - private static void ReadSamples(IClientSession clientSession) - { - const string vCarIdentifier = "vCar:Chassis"; - using (var pda = clientSession.Session.CreateParameterDataAccess(vCarIdentifier)) - { - var sessionMidPoint = clientSession.Session.StartTime + (clientSession.Session.EndTime - clientSession.Session.StartTime) / 2; - pda.GoTo(sessionMidPoint); - //MAX SAMPLES RETRIEVED: 10000 - var samples = pda.GetNextSamples(10); - - Console.WriteLine($"** Data for {vCarIdentifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - } - - private static IClientSession LoadSession(string pathToFile) - { - var fileSessionManager = FileSessionManager.CreateFileSessionManager(); - var clientSession = fileSessionManager.Load(pathToFile); // session with associates - - if (clientSession == null) - { - Console.WriteLine("Session not found"); - return null; - } - - return clientSession; - } - } -} diff --git a/MAT.SQLRace.CreateSessionWithMultipleParameters/App.config b/MAT.SQLRace.CreateSessionWithMultipleParameters/App.config deleted file mode 100644 index 41a48e1..0000000 --- a/MAT.SQLRace.CreateSessionWithMultipleParameters/App.config +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MAT.SQLRace.CreateSessionWithMultipleParameters/MAT.SQLRace.CreateSessionWithMultipleParameters.csproj b/MAT.SQLRace.CreateSessionWithMultipleParameters/MAT.SQLRace.CreateSessionWithMultipleParameters.csproj deleted file mode 100644 index f1d35f8..0000000 --- a/MAT.SQLRace.CreateSessionWithMultipleParameters/MAT.SQLRace.CreateSessionWithMultipleParameters.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0-windows - Exe - false - true - true - x64 - - - - - \ No newline at end of file diff --git a/MAT.SQLRace.CreateSessionWithMultipleParameters/Program.cs b/MAT.SQLRace.CreateSessionWithMultipleParameters/Program.cs deleted file mode 100644 index 4ac686d..0000000 --- a/MAT.SQLRace.CreateSessionWithMultipleParameters/Program.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Collections.Generic; -using MAT.OCS.Core; -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Infrastructure.DataPipeline; -using MESL.SqlRace.Enumerators; - -namespace MAT.SQLRace.CreateSessionWIthMultipleParameters -{ - class Program - { - public static IClientSession CreateBasicSession(string connectionString, string sessIdentifier) - { - var sessionManager = SessionManager.CreateSessionManager(); - var sessionKey = SessionKey.NewKey(); - var clientSession = sessionManager.CreateSession(connectionString, sessionKey, sessIdentifier,DateTime.Now, "Session"); - return clientSession; - } - - public static Dictionary PopulateSession(IClientSession clientSession, IEnumerable identifiers) - { - // Take into account that the identifiers inside the IEnumerable cannot contain spaces - var channels = new Dictionary(); - //Constants for creating parameters - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - - var session = clientSession.Session; - var config = session.CreateConfiguration(); - // Creating the parameter group where the channels will be added later - var group1 = new ParameterGroup(ParameterGroupIdentifier, "SampleDescription"); - - // Application group without RDA where the parameter group will live - var appGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(appGroup1); - config.AddParameterGroup(group1); - //Conversion function - config.AddConversion( - new RationalConversion(ConversionFunctionName, - "kph", - "%5.2f", - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 1.0) - ); - Frequency samplingFrequency = new Frequency(2, FrequencyUnit.Hz); - uint channelId = 1; - // We start to iterate over the identifiers to add the parameters to the param group - foreach (string identifier in identifiers) - { - channels[identifier] = channelId; - var channel = new Channel( - channelId++, - "Channel" + channelId, - samplingFrequency.ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var parameter = new Parameter( - identifier, - identifier, - identifier, - 400, // Your max - 0, // Your min - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - channel.Id, - ApplicationGroupName); - - config.AddChannel(channel); - config.AddParameter(parameter); - } - - config.Commit(); - return channels; - - } - - public static void FillSessionDetails(IClientSession clientSession, IEnumerable details) - { - var session = clientSession.Session; - // Populating session details - var i = 1; - foreach (var detail in details) - { - session.Items.Add(new SessionDataItem("Detail" + i, detail)); - i++; - } - - } - - public static void AddSampleLap(IClientSession clientSession, long startTime, short number, byte triggersource, string name, bool countForFastestLap) - { - var session = clientSession.Session; - - var lap = new Lap(startTime, number, triggersource, name, countForFastestLap); - // You can use the lap object to edit the different properties of it - //lap.EndTime = 1; - - session.Laps.Add(lap); - //In case you would like to add data attached to the lap you can do the following - session.Laps.LapItems.Add(new LapDataItem(number, name, "Race start")); - } - - static void Main(string[] args) - { - string connectionString = $@"DbEngine=SQLite;Data Source={ - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - }\McLaren Applied Technologies\ATLAS 10\SQL Race\test.db;"; - - Console.WriteLine("Initializing SQL Race...."); - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - - Console.WriteLine($"Creating session"); - var clientSession = CreateBasicSession(connectionString, "SampleIdentifier"); - - // Adding channels - var identifiers = new List - { - "sampleparam1", - "sampleparam2" - }; - - var channels = PopulateSession(clientSession, identifiers); - - //Adding session details - var details = new List - { - "sampledetail1", - "sampledetail2" - }; - - FillSessionDetails(clientSession, details); - //Adding a sample lap - AddSampleLap(clientSession, 0, 1, 0, "Lap1", false); - - Console.WriteLine("Press ENTER or RETURN key to finish..."); - Console.ReadLine(); - } - } -} diff --git a/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/App.config b/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/App.config deleted file mode 100644 index fa04e50..0000000 --- a/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/App.config +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters.csproj b/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters.csproj deleted file mode 100644 index f1d35f8..0000000 --- a/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0-windows - Exe - false - true - true - x64 - - - - - \ No newline at end of file diff --git a/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/Program.cs b/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/Program.cs deleted file mode 100644 index b0538ab..0000000 --- a/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/Program.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -using MAT.OCS.Core; -using MESL.SqlRace.Common.Extensions; -using MESL.SqlRace.Domain; - -namespace MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters -{ - class Program - { - /// - /// In order to be able to use this example follow these instructions; - /// https://github.com/mat-docs/packages - /// - /// Whenever you are setting up your project you should use .NET 8. - /// You need to set up the compilation to be for x64 processors in order for this example to work - /// - /// Once registered in Visual Studio, you need to install MESL.SQLRace.API package from NuGet making sure that you have selected the McLaren Applied Github packages in the top - /// right corner (defaults to nuget.org, where if you search for the package it would not show). - /// Apart from this NuGet package, if you plan to load SSN files you need to follow these steps too: (not needed for this example) - /// - Install MAT.ATLAS.SupportFilesLite from NuGet directory making sure that you have selected on the top right corner MA repository - /// - Setup the build config to build in x64 based CPUs. This is required for SSNs to be loaded. - /// Once this package is installed, you would only need to pay attenion to the TODOs written in the code. - /// - /// Find the documentation for the API in the link below - /// https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html - /// - /// Further examples can be found here: - /// https://github.com/mat-docs/MAT.OCS.SQLRace.Examples - /// - static void Main(string[] args) - { - // TODO: Change the location to where do you want the session to be created - const string fileFullPath = "C:\\temp\\MyTestSession.ssn2"; - - if (File.Exists(fileFullPath)) - { - Console.WriteLine($"The file \'{fileFullPath}\' already exists"); - Console.WriteLine("Finished!.."); - return; - } - - const string connectionString = @"DbEngine=SQLite;Data Source=c:\temp\MyTestSession.ssn2;"; - - Console.WriteLine("Initializing SQL Race...."); - Console.WriteLine(Directory.GetCurrentDirectory()); - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - - Console.WriteLine("SQLRace has been initialized correctly"); - - // Creating a Session - var dateTimeNow = DateTime.Now; - var endDateTime = dateTimeNow.AddMinutes(5); - - var timeToday = dateTimeNow - DateTime.Today; - var endTimeToday = endDateTime - DateTime.Today; - - long startTime = timeToday.ToNanoseconds(); - long endTime = endTimeToday.ToNanoseconds(); - - // This is another way to get the Nanoseconds values, - // some people may find it to be more explicit and illustrative. - // - //long startTime = SessionHelper.ConvertDateTimeToNanoseconds(dateTimeNow); - //long endTime = SessionHelper.ConvertDateTimeToNanoseconds(endDateTime); - - string sessionDescription = string.Format("Example::: {0}", dateTimeNow.ToString("dd-MMM-yy hh:mm:ss tt")); - - var sessionKey = SessionKey.NewKey(); - var sessionName = "MyTestSession"; - - // Create a session first - var clientSession = CreateSession(sessionKey, connectionString, sessionName, dateTimeNow, "Session"); - - var session = clientSession.Session; - - // Add some session details which allows values as String, Long, Double, Bool, Datetime, Byte[] etc. - session.Items.Add(new SessionDataItem("Driver", "Test Driver")); - session.Items.Add(new SessionDataItem("Car", "Test Car")); - - // Setting up the components of a session - // Adding a channel with some samples - var numSamples = 1000; - clientSession = CreateParameter(clientSession, 1000, startTime, endTime); - - // Add Laps - var lapNumber = 5; - var lapTimeDelta = (endTime - startTime) / (lapNumber + 1); - var timeStamp = startTime; - - for (var i = 0; i < 5; i++) - { - if (i > 0) - timeStamp += lapTimeDelta; - - var newLap = new Lap(timeStamp, Convert.ToInt16(i + 1), byte.MinValue, string.Format("Lap {0}", i + 1), true); - clientSession.Session.LapCollection.Add(newLap); - } - - // Closing the session before exporting. - clientSession.Close(); - - Console.WriteLine("Finished."); - } - - public static IClientSession CreateSession(SessionKey sessionKey, string connectionString, string sessionName, DateTime dateOfRecording, string sessionType) - { - var sessionManager = MESL.SqlRace.Domain.SessionManager.CreateSessionManager(); - return sessionManager.CreateSession(connectionString, sessionKey, sessionName, dateOfRecording, sessionType); - - } - - public static IClientSession CreateParameter(IClientSession clientSession, int numSamples, long startTime, long endTime) - { - //Creating the parameter object that will be populated - var parameter = SessionHelper.CreateSessionConfigurationForOneParameter(clientSession); - - // Populating timestamps and data initially (simulating generating data to be added to the session) - var sampleData = new List(numSamples); - var sampleTimeStamps = new List(numSamples); - var random = new Random(42); - - var currentTime = startTime; - var timeDelta = (endTime - startTime) / numSamples; - - for (int i = 0; i < numSamples; i++) - { - sampleData.Add(random.NextDouble()); - - sampleTimeStamps.Add(currentTime); - - currentTime += timeDelta; - } - - // Adding the samples to the parameter inside the session - for (int i = 0; i < numSamples; i++) - { - clientSession.Session.AddChannelData(parameter.ChannelIds.FirstOrDefault(), - sampleTimeStamps[i], - 1, - BitConverter.GetBytes(sampleData[i])); - } - - return clientSession; - } - } -} diff --git a/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/SessionHelper.cs b/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/SessionHelper.cs deleted file mode 100644 index 58236fc..0000000 --- a/MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters/SessionHelper.cs +++ /dev/null @@ -1,176 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd. - - -using System; -using System.Collections.Generic; - -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Infrastructure.DataPipeline; -using MESL.SqlRace.Enumerators; - -namespace MAT.SQLRace.HelloCreateSSN2FromZeroWithParameters -{ - internal class SessionHelper - { - public static Dictionary CreateSessionConfigurationForMultipleParameter( - IClientSession clientSession, - IEnumerable identifiers) - { - var channels = new Dictionary(); - - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - - var session = clientSession.Session; - - var config = session.CreateConfiguration(); - - var group1 = new ParameterGroup(ParameterGroupIdentifier, "pg1_description"); - - var applicationGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup1); - config.AddParameterGroup(group1); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var vCarFrequency = new Frequency(2, FrequencyUnit.Hz); - - uint channelId = 1; - foreach (var identifier in identifiers) - { - channels[identifier] = channelId; - - var channel = new Channel( - channelId++, - "Channel" + channelId, - vCarFrequency.ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var parameter = new Parameter( - identifier, - identifier, - identifier, - 400, //vCar maximum value - 0, //vCar minimum value - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - channel.Id, - ApplicationGroupName); - - config.AddChannel(channel); - config.AddParameter(parameter); - } - - config.Commit(); - - return channels; - } - - public static Parameter CreateSessionConfigurationForOneParameter(IClientSession clientSession) - { - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - const string ParameterName = "MyParam"; - const uint MyParamChannelId = 999998; //must be unique - const int ApplicationId = 998; - var parameterIdentifier = $"{ParameterName}:{ApplicationGroupName}"; - - var session = clientSession.Session; - - var config = session.CreateConfiguration(); - - var group1 = new ParameterGroup(ParameterGroupIdentifier, "pg1_description"); - - var applicationGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - ApplicationId, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup1); - config.AddParameterGroup(group1); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var myParamFrequency = new Frequency(2, FrequencyUnit.Hz); - - var myParameterChannel = new Channel( - MyParamChannelId, - "MyParamChannel", - myParamFrequency.ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var myParameter = new Parameter( - parameterIdentifier, - ParameterName, - ParameterName + "Description", - 400, - 0, - 1, - 0, - 0, - 0xFFFF, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - myParameterChannel.Id, - ApplicationGroupName); - - config.AddChannel(myParameterChannel); - config.AddParameter(myParameter); - - config.Commit(); - - return myParameter; - } - - /// - /// Converts a DateTime to nanoseconds - /// - /// DateTime object - /// Time in nanoseconds - public static long ConvertDateTimeToNanoseconds(DateTime time) - { - int hours = time.Hour * 3600000; - int minutes = time.Minute * 60000; - int seconds = time.Second * 1000; - int milliseconds = time.Millisecond; - string nanosecondString = Convert.ToString(milliseconds + seconds + minutes + hours) + "000000"; - return Convert.ToInt64(nanosecondString); - } - } -} \ No newline at end of file diff --git a/MAT.SQLRace.HelloData/App.config b/MAT.SQLRace.HelloData/App.config deleted file mode 100644 index 88c4bf5..0000000 --- a/MAT.SQLRace.HelloData/App.config +++ /dev/null @@ -1,58 +0,0 @@ - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MAT.SQLRace.HelloData/FunctionHelper.cs b/MAT.SQLRace.HelloData/FunctionHelper.cs deleted file mode 100644 index 8693acc..0000000 --- a/MAT.SQLRace.HelloData/FunctionHelper.cs +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd. - -using System; - -using MESL.SqlRace.Domain.Functions; -using MESL.SqlRace.Domain.Functions.Fdl; -using MESL.SqlRace.Enumerators; -using MESL.SqlRace.Functions.Interfaces.Enums; - -namespace MAT.SQLRace.HelloData -{ - internal class FunctionHelper - { - public static IFunctionDefinition CreateFdlFunctionDefinition(IFunctionManager functionManager, FunctionMode functionMode = FunctionMode.Instantaneous) - { - //Create function - const string CarSpeedDoubledInFdl = "return ($vCar:Chassis * 2)"; - - var functionDefinition = functionManager.CreateFunctionDefinition(FdlFunctionConstants.UniqueId); - - // The implementation of a function is completely agnostic. We requested a function definition from - // the FDL runtime, so the generic function implementation definition in the definition returned can - // be safely cast to the more specific FDL implementation definition. - var fdlFunctionImplementationDefinition = (IFdlFunctionImplementationDefinition)functionDefinition.ImplementationDefinition; - fdlFunctionImplementationDefinition.FunctionCode = CarSpeedDoubledInFdl; - - // FDL doesn't define the output for a single parameter - it's simply the return value of the - // function - so we have to tell the function executor what to do with the output it generates - var outputParameter = FunctionOutputParameterDefinition - .Create("vCarDoubled", "FunctionParameters", "Double car speed!") - .Units("kph") - .FormatOverride("%.2f") - .MinimumValue("0.0") - .MaximumValue("700.0"); - - functionDefinition.OutputParameterDefinitions.Add(outputParameter); - - // Functions in ATLAS 9 have a number of different properties that define how, where, when and on - // what a function executes. ATLAS 10 will add more of these properties. Each value here is the - // default for its property, shown for illustration. - functionDefinition.Name = $"DoubleCarSpeed_{Environment.TickCount}"; - functionDefinition.FunctionMode = functionMode; - functionDefinition.CalculationModeInfoDefinition.Mode = CalculationMode.EachSamplePoint; - functionDefinition.InterpolateBetweenSamples = false; - functionDefinition.JoinGapsAroundNull = true; - functionDefinition.CalculateOverWholeSession = false; - functionDefinition.StoreInSession = false; - functionDefinition.ShouldHide = false; - functionDefinition.ShouldPersist = false; - - return functionDefinition; - } - } -} \ No newline at end of file diff --git a/MAT.SQLRace.HelloData/MAT.SQLRace.HelloData.csproj b/MAT.SQLRace.HelloData/MAT.SQLRace.HelloData.csproj deleted file mode 100644 index 61f4441..0000000 --- a/MAT.SQLRace.HelloData/MAT.SQLRace.HelloData.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - net8.0-windows - Exe - false - true - x64 - x64 - - - - - - - MAT.SQLRace.HelloData.Program - - \ No newline at end of file diff --git a/MAT.SQLRace.HelloData/Program.cs b/MAT.SQLRace.HelloData/Program.cs deleted file mode 100644 index 3aedee2..0000000 --- a/MAT.SQLRace.HelloData/Program.cs +++ /dev/null @@ -1,1427 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd. - -using MAT.OCS.Core; -using MESL.SqlRace.Common.Extensions; -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Collections; -using MESL.SqlRace.Domain.Functions; -using MESL.SqlRace.Domain.Infrastructure.Enumerators; -using MESL.SqlRace.Domain.Query; -using MESL.SqlRace.Enumerators; -using MESL.SqlRace.UI; -using MESL.SqlRace.Domain.Events; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using MESL.SqlRace.Domain.Infrastructure.DataPipeline; -using MESL.SqlRace.Functions.Interfaces.Enums; - -namespace MAT.SQLRace.HelloData -{ - internal class Program - { - public static string ConnectionString; - //a337e5f0-72fe-4691-8627-e45530ff5012 - - private static void Main(string[] args) - { - //SQLServer style connection string - //ConnectionString = @"Data Source=MAT-TWFIASQL02\LOCALSERVER1;Initial Catalog=SQLRACE_DEV1;Integrated Security=True"; - - //SQLite style connection string - ConnectionString = $@"DbEngine=SQLite;Data Source={ - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - }\McLaren Applied Technologies\ATLAS 10\SQL Race\LiveSessionCache.ssn2;"; - - //SQLite ssn style connection string - //ConnectionString = $@"DbEngine=SQLite;Data Source={ - // Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - // }\McLaren Applied Technologies\ATLAS 10\SQL Race\IndexingSessionCache.ssn2;"; - - Console.WriteLine("Initializing SQL Race...."); - - // To use functions LicenceProgramName needs to be "ATLAS Data Platform" - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - - //QueryByDataItem(); - //ReadParameterUnit(); - //ReadSamples(); - //ReadData(); - //ReadEvents(); - //LapStatistics(); - //ConnectionManagerTest(); - //CreateSessionAndAddData(); - //CompositeSessionTestWithAppendedSessions(); - - //CompositeSessionWithvTagSessionsTest(); - //AddInMemoryParameterToSession(); - //AddParameterToExistingSession(); - //ChannelStats(); - //Benchmark(); - //LoadAssociated(); - - //LoadSSNWithAssociatedMerge(); - //AddEvents(); - //LoadLiveSessionAndWaitForLapEvents(); - //LoadLiveSessionAndWaitForEvents(); - //LoadLiveSamples(); - //AddAndProcessMarkers(); - LoadSSN(); - //GetSessionSummaryBySessionGUID(); - //LoadLiveFunction(); - //WholeSessionsCompareMode(); - //TestMaxAndMinSampleIntervals(); - //CreateSessionWithTimeZoneValue(); - - Console.WriteLine("Press ENTER key to close."); - Console.ReadLine(); - } - - private static void WholeSessionsCompareMode() - { - var session1Path = $@"C:\Users\{Environment.UserName}\Documents\McLaren Electronic Systems\ATLAS 9\Data\Server\190708111749.ssn"; - - var session2Path = $@"C:\Users\{Environment.UserName}\Documents\McLaren Electronic Systems\ATLAS 9\Data\Server\190722121003.ssn"; - - var compositeSessionPrimary = new CompositeSession(CompositeSessionKey.NewKey(), "PrimaryComposite"); - var compositeSessionSecondary = new CompositeSession(CompositeSessionKey.NewKey(), "SecondaryComposite"); - - var compositeSessionContainer = new CompositeSessionContainer(CompositeSessionContainerKey.NewKey(), "CompareSampleContainer"); - compositeSessionContainer.SetSessionCompareMode(compositeSessionPrimary.Key, SessionCompareMode.WholeSession); - compositeSessionContainer.Add(compositeSessionPrimary); - compositeSessionContainer.Add(compositeSessionSecondary); - - var fileSessionManager = FileSessionManager.CreateFileSessionManager(); - - Console.WriteLine("Loading sessions..."); - - var sessionPrimary = fileSessionManager.Load(session1Path); - var sessionSecondary = fileSessionManager.Load(session2Path); - - if (sessionPrimary == null || sessionSecondary == null) - { - Console.WriteLine("Unable to load session(s)!"); - return; - } - - compositeSessionPrimary.Add(sessionPrimary); - compositeSessionSecondary.Add(sessionSecondary); - - ICompositeContainerParameterDataAccess compositeContainerParameterDataAccess = compositeSessionContainer.CreateParameterDataAccess("vCar:Chassis"); - if (compositeContainerParameterDataAccess == null) - { - Console.WriteLine("CompositeContainerParameterDataAccess not found!"); - return; - } - - const int NumberOfSamples = 10000; - using (compositeContainerParameterDataAccess) - { - compositeContainerParameterDataAccess.GoTo(compositeSessionPrimary.StartTime); - - var sessionValuesKvp = compositeContainerParameterDataAccess.GetNextSamples(NumberOfSamples); - - if (sessionValuesKvp.Count == 0) - { - Console.WriteLine("No parameter result found!"); - return; - } - - foreach (var kvp in sessionValuesKvp) - { - var parameterValues = kvp.Value; - Console.WriteLine($"Samples Found for '{kvp.Key}' = {parameterValues.SampleCount}"); - } - } - } - - private static void LapStatistics() - { - var identifiers = new List - { - "vCar:Chassis", - //"MEngine:Controller", - //"gLat:Chassis", - }; - - ConnectionString = @"Data Source=MAT-TWFIASQL02\LOCALSERVER1;Initial Catalog=SQLRACE_DEV1;Integrated Security=True"; - - var sessionKey = SessionKey.Parse("7C57A82B-96AE-43C6-9304-36719C3C9701"); - - Console.WriteLine("Loading session...."); - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - Console.WriteLine("Session loaded"); - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - foreach (var identifier in identifiers) - { - using (var pda = clientSession.Session.CreateParameterDataAccess(identifier)) - { - foreach (var lap in session.LapCollection) - { - var statistics = pda.GetLapStatistics(lap, false, StatisticOption.Max); - Console.WriteLine($"{statistics.Lap.Number}: {statistics.MaximumTime}, {statistics.MaximumValue}"); - } - } - } - } - - Console.WriteLine(); - } - - /// - /// Creates a session and add data. - /// - private static void CreateSessionAndAddData() - { - ConnectionString = $@"DbEngine=SQLite;Data Source=c:\ssn2\test01.ssn2;"; - - var sessionManager = SessionManager.CreateSessionManager(); - - var clientSession = sessionManager.CreateSession( - ConnectionString, - SessionKey.NewKey(), - "Add data session test", - DateTime.Now, - "Session"); - - Console.WriteLine($"Creating session"); - var parameter = SessionHelper.CreateSessionConfigurationForOneParameter(clientSession); - - var session = clientSession.Session; - - Console.WriteLine($"Adding data"); - - for (var i = 0; i < 10; i++) - { - var newTimestamp = 10.SecondsToNanoseconds() + i * parameter.Channels[0].Interval; - var newValue = (short)(100 + i); - - session.AddChannelData( - parameter.ChannelIds.FirstOrDefault(), - newTimestamp, - 1, - BitConverter.GetBytes(newValue)); - - Console.WriteLine($"Written sample. Timestamp: {newTimestamp.ToTimeString()} Value:{newValue}"); - } - - Console.WriteLine($"Reading data"); - - // read data back to check if values are correct - using (var pda = clientSession.Session.CreateParameterDataAccess(parameter.Identifier)) - { - var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); - - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine( - $"Read Sample. Timestamp: {samples.Timestamp[i].ToTimeString()} Value:{samples.Data[i]}"); - } - } - - // to associate this session to a parent session call this method - //session.AssociateWithParent(/*Parent session key*/); - - clientSession.Close(); - } - - private static void CreateSessionWithTimeZoneValue() - { - ConnectionString = $@"DbEngine=SQLite;Data Source=c:\ssn2\test01.ssn2;"; - - var sessionManager = SessionManager.CreateSessionManager(); - - var clientSession1 = sessionManager.CreateSessionWithTeamId( - ConnectionString, - SessionKey.NewKey(), - "clientSession1", - DateTime.UtcNow, - 45, - "Session", - "UTC+00:00"); - - var clientSession2 = sessionManager.CreateSession( - ConnectionString, - SessionKey.NewKey(), - "clientSession2", - DateTime.UtcNow, - "Session"); - - var session1 = clientSession1.Session; - var session2 = clientSession2.Session; - - Console.WriteLine($"Session one created with timezone: {session1.TimeZone}"); - Console.WriteLine($"Session two created with timezone: {session2.TimeZone}"); - } - - private static void TestMaxAndMinSampleIntervals() - { - ConnectionString = $@"DbEngine=SQLite;Data Source=c:\ssn2\test01.ssn2;"; - - var sessionManager = SessionManager.CreateSessionManager(); - - var clientSession = sessionManager.CreateSession( - ConnectionString, - SessionKey.NewKey(), - "Add data session test", - DateTime.Now, - "Session"); - - Console.WriteLine($"Creating session"); - var parameter = SessionHelper.CreateSessionConfigurationForOneParameterWithMultipleChannels(clientSession); - - var session = clientSession.Session; - - using (var pda = clientSession.Session.CreateParameterDataAccess(parameter.Identifier)) - { - var minimumSampleInterval = pda.GetMinimumSampleInterval(0, false); - Console.WriteLine("Minimum sample interval in nanoseconds:" + minimumSampleInterval); - - var maximumSampleInterval = pda.GetMaximumSampleInterval(0, false); - Console.WriteLine("Maximum sample interval in nanoseconds:" + maximumSampleInterval); - } - } - - private static long[] CreateTimestamps(long startTime, int numberOfTimestamps, long interval) - { - var timestamps = new long[numberOfTimestamps]; - var timestamp = startTime; - - for (var i = 0; i < numberOfTimestamps; i++) - { - timestamps[i] = timestamp; - timestamp += interval; - } - - return timestamps; - } - - private static long[] CreateTimestampsForTimerange(long startTime, long endTime, long interval) - { - var timestampsRequired = ((endTime - startTime) / interval) + 1; - return CreateTimestamps(startTime, (int)timestampsRequired, interval); - } - - /// - /// Loads a session with an associated session - /// - private static void LoadAssociated() - { - ConnectionString = @"Data Source=mesltgs1;Initial Catalog=SQLRACE143;Integrated Security=True"; - - string parameterIdentifier = "BAeroAutoZero:MRLAero"; - SessionKey sessionKey = new SessionKey("3FA70D1D-F526-4A10-9396-589B3BE23625"); - - var sessionManager = SessionManager.CreateSessionManager(); - var sessionSummaries = sessionManager.LoadAssociatesForSession(sessionKey, ConnectionString); - var additionalSessionKey = sessionSummaries.OrderBy(ss => ss.TimeOfRecording).Last(); - IClientSession clientSession = sessionManager.Load(sessionKey, ConnectionString, new[] { additionalSessionKey.Key }); - - var containsChannel = clientSession.Session.ContainsParameter(parameterIdentifier); - Console.WriteLine( - $"Parameter {parameterIdentifier} exists in session: {containsChannel}"); - - using (var pda = clientSession.Session.CreateParameterDataAccess(parameterIdentifier)) - { - Console.WriteLine( - $"PDA {parameterIdentifier} successfully created"); - } - } - - /// - /// Loads a live session from the database and get last 2 samples every 5 seconds - /// - private static void LoadLiveSamples() - { - const string connectionFriendlyName = ""; - - var connectionString = GetSqlRaceConnectionString(connectionFriendlyName); - - var ss = GetMostRecentLiveSession(connectionString); - - if (ss == null) - { - Console.WriteLine("No live session found"); - return; - } - - Console.WriteLine($"Loading session {ss.Identifier}"); - - var clientSession = LoadSession(ss.Key, ss.GetConnectionString()); - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - for (var i = 0; i < 100; i++) - { - using (var pda = clientSession.Session.CreateParameterDataAccess("vCar:Chassis")) - { - pda.GoTo(session.EndTime); - var samples = pda.GetNextSamples(10, StepDirection.Reverse); - - Console.WriteLine("** Samples **"); - for (var j = 0; j < samples.SampleCount; j++) - { - Console.WriteLine($"{samples.Timestamp[j].ToTimeString()} {samples.Data[j]}"); - } - } - - Thread.Sleep(5000); - } - - Console.WriteLine(); - } - - /// - /// Loads a live session from the database and wait for lap events to come - /// - private static async void LoadLiveSessionAndWaitForLapEvents() - { - const string connectionFriendlyName = ""; - - var connectionString = GetSqlRaceConnectionString(connectionFriendlyName); - - var ss = GetMostRecentLiveSession(connectionString); - - if (ss == null) - { - Console.WriteLine("No live session found"); - return; - } - - Console.WriteLine($"Loading session {ss.Identifier}"); - - var clientSession = LoadSession(ss.Key, ss.GetConnectionString()); - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - // listen to laps added events - session.LapStarted += Session_LapStarted; - - // wait events without blocking the main thread - await Task.Delay(TimeSpan.FromHours(1)); - - Console.WriteLine(); - } - - public static void QueryByDataItem() - { - using (var queryManager = QueryManager.CreateQueryManager(@"Data Source=MAT-TWFIASQL02\LOCALSERVER1;Initial Catalog=SQLRACE_DEV1;Integrated Security=True")) - { - var filter = new ScalarFilter("SessionStartDateTime", MatchingRule.GreaterThan, new DateTime(2017, 9, 01).ToShortDateString(), true); - queryManager.Filter = filter; - var sessions = queryManager.ExecuteQuery(); - - foreach (var session in sessions) - { - Console.WriteLine($"{session.Identifier} - {session.TimeOfRecording}"); - } - } - } - - /// - /// Loads a live session from the database and wait for events to come - /// - private static async void LoadLiveSessionAndWaitForEvents() - { - const string connectionFriendlyName = ""; - - var connectionString = GetSqlRaceConnectionString(connectionFriendlyName); - - var ss = GetMostRecentLiveSession(connectionString); - - if (ss == null) - { - Console.WriteLine("No live session found"); - return; - } - - Console.WriteLine($"Loading session {ss.Identifier}"); - - var sessionManager = SessionManager.CreateSessionManager(); - var clientSession = sessionManager.Load(ss.Key, ss.GetConnectionString()); - - // listen to laps added events - clientSession.Session.EventDataAdded += Session_EventDataAdded; - - Console.WriteLine("Waiting for events...."); - // wait events without blocking the main thread - await Task.Delay(TimeSpan.FromHours(1)); - - clientSession.Close(); - Console.WriteLine(); - } - - private static void Session_EventDataAdded(object sender, ItemEventArgs e) - { - Console.WriteLine($"Event occurred for session {e.SessionKey}: {e.Item}"); - } - - private static string GetSqlRaceConnectionString(string connectionFriendlyName) - { - var connectionManager = new DatabaseConnectionManager(); - var databaseConnection = connectionManager.GetDatabaseConnections() - .FirstOrDefault(c => c.FriendlyName == connectionFriendlyName); - var connectionString = databaseConnection?.GetConnectionString(); - return connectionString; - } - - private static SessionSummary GetMostRecentLiveSession(string connectionString) - { - var qm = QueryManager.CreateQueryManager(connectionString); - - var ss = qm.ExecuteQuery() - .OrderByDescending(x => x.TimeOfRecording) - .FirstOrDefault(y => y.State == SessionState.Live && !y.Identifier.Contains("VTS")); - return ss; - } - - private static IClientSession LoadSession(SessionKey sessionKey, string connection) - { - var sessionManager = SessionManager.CreateSessionManager(); - return sessionManager.Load(sessionKey, connection); - } - - private static void AddParameterToExistingSession() - { - ConnectionString = $@"DbEngine=SQLite;Data Source={ - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - }\McLaren Applied Technologies\ATLAS 10\SQL Race\LiveSessionCache.ssn2;"; - - Console.WriteLine("Loading session...."); - // var sessionKey = SessionKey.Parse("40a4f6cf-767c-4a81-b7db-da7dcbffb034"); - var sessionKey = SessionKey.Parse("3c368c0d-2456-4f15-b3b8-b76caf2be99b"); - - string identifier; - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - var parameter = SessionHelper.CreateSessionConfigurationForOneParameter(clientSession); - identifier = parameter.Identifier; - - for (var i = 0; i < 10; i++) - { - var newTimestamp = session.StartTime + i * parameter.Channels[0].Interval; - var newValue = (short)(100 + i); - - session.AddChannelData( - parameter.ChannelIds.FirstOrDefault(), - newTimestamp, - 1, //value of 1 param - BitConverter.GetBytes(newValue)); - - Console.WriteLine($"Written sample. Timestamp: {newTimestamp.ToTimeString()} Value:{newValue}"); - } - } - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - // read data back to check if values are correct - using (var pda = clientSession.Session.CreateParameterDataAccess(identifier)) - { - var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); - - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine( - $"Read Sample. Timestamp: {samples.Timestamp[i].ToTimeString()} Value:{samples.Data[i]}"); - } - } - } - - Console.WriteLine(); - } - - private static void AddInMemoryParameterToSession() - { - Console.WriteLine("Loading session...."); - var sessionKey = SessionKey.Parse("3c4a8093-43fd-4639-9485-edc360093698"); - - string identifier; - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - var transientParameter = SessionHelper.CreateTransientConfigurationForOneParameter(clientSession); - identifier = transientParameter.Identifier; - - for (var i = 0; i < 10; i++) - { - var newTimestamp = session.StartTime + i * transientParameter.Channels[0].Interval; - var newValue = (double)(100 + i); - - session.AddChannelData( - transientParameter.ChannelIds.FirstOrDefault(), - newTimestamp, - 1, //value of 1 param - BitConverter.GetBytes(newValue)); - - Console.WriteLine($"Written sample. Timestamp: {newTimestamp.ToTimeString()} Value:{newValue}"); - } - - // read data back to check if values are correct - using (var pda = clientSession.Session.CreateParameterDataAccess(transientParameter.Identifier)) - { - var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); - - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine( - $"Read Sample. Timestamp: {samples.Timestamp[i].ToTimeString()} Value:{samples.Data[i]}"); - } - } - } - - Console.WriteLine("Reloading session"); - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - - if (clientSession.Session.ContainsParameter(identifier)) - { - throw new Exception("Transient parameter should not be persisted"); - } - } - - Console.WriteLine("Transient parameter not found. Test succeeded"); - - Console.WriteLine(); - } - - private static void ChannelStats() - { - Console.WriteLine("Loading session...."); - var sessionKey = SessionKey.Parse("4d8b762b-5e61-4538-87ac-1af9d2383279"); - - var identifiers = new List { "vCar:Chassis", "gLat:Chassis", "a2msMainshaft:Chassis" }; - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - var channelBasedParameters = session.Parameters.Where(a => a is ChannelBasedParameter) - .Cast() - .ToList(); - - var results = new Dictionary(); - - foreach (var identifier in identifiers) - { - var parameter = channelBasedParameters.FirstOrDefault(a => a.Identifier == identifier); - if (parameter == null) - { - continue; - } - - var minChannelInterval = parameter.Channels.Select(a => a.Interval).Where(a => a > 0).ToList(); - if (!minChannelInterval.Any()) - { - results.Add(identifier, new Frequency(0, FrequencyUnit.Hz));//ROW - continue; - } - - var maxFreq = minChannelInterval.Min().ToFrequency(); - - results.Add(identifier, maxFreq); - } - - var csv = results.Select(a => new - { - Print = a.Key + "," + a.Value.Value, - Freq = a.Value.Value - }) - .OrderByDescending(b => b.Freq) - .Select(c => c.Print); - - //Print to csv file - } - - Console.WriteLine(); - } - - private static void LoadLiveFunction() - { - ConnectionString = $@"DbEngine=SQLite;Data Source={ - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - }\McLaren Applied Technologies\ATLAS 10\SQL Race\LiveSessionCache.ssn2;"; - - var qm = QueryManager.CreateQueryManager(ConnectionString); - - // Get the most recent live session from the database - var ss = qm.ExecuteQuery() - .OrderByDescending(x => x.TimeOfRecording) - .FirstOrDefault(y => y.State == SessionState.Live && !y.Identifier.Contains("VTS")); - if (ss == null) - { - Console.WriteLine("No live session found"); - return; - } - - Console.WriteLine($"Loading session {ss.Identifier}"); - - var clientSession = LoadSession(ss.Key, ss.GetConnectionString()); - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - - ParameterValues samples; - var endTime = session.EndTime; - - using (var pda = clientSession.Session.CreateParameterDataAccess("vCar:Chassis")) - { - pda.GoTo(endTime); - samples = pda.GetNextSamples(5, StepDirection.Reverse); - - Console.WriteLine("** Samples **"); - for (var j = 0; j < samples.SampleCount; j++) - { - Console.WriteLine($"{samples.Timestamp[j].ToTimeString()} {samples.Data[j]}"); - } - } - - //Create & Build Function - var functionManager = FunctionManagerFactory.Create(); - var functionDefinition = FunctionHelper.CreateFdlFunctionDefinition(functionManager, FunctionMode.LeadingEdge); - - var buildResults = functionManager.Build(functionDefinition); - - if (buildResults.Errors.Any()) - { - Console.WriteLine("Function build failed."); - } - - var outputParameter = functionDefinition.OutputParameterDefinitions.First(); //created only one output parameter - var functionPda = session.CreateParameterDataAccess(outputParameter.Identifier); - - functionPda.GoTo(endTime); - samples = functionPda.GetNextSamples(5, StepDirection.Reverse); - if (samples == null) - { - Console.WriteLine("** Failed to get function samples **"); - return; - } - - Console.WriteLine("** Function Results **"); - for (var j = 0; j < samples.SampleCount; j++) - { - Console.WriteLine($"{samples.Timestamp[j].ToTimeString()} {samples.Data[j]}"); - } - } - - private static void Benchmark() - { - //string conn = @"Data Source=mesltgs1;Initial Catalog=SQLRACE143;Integrated Security=True"; - //var sessionKey = new SessionKey("B06E1E02-14A4-4368-885E-CCE6C016B88D"); - string conn = $@"DbEngine=SQLite;Data Source=C:\Users\{Environment.UserName}\AppData\Local\McLaren Applied Technologies\ATLAS 10\SQL Race\LiveSessionCache.ssn2"; - var sessionKey = new SessionKey("f791aa10-a636-4bc6-b401-0d84adf02b0e"); - var sessionManager = SessionManager.CreateSessionManager(); - var clientSession = sessionManager.Load(sessionKey, conn); - const string parameter = "nEngine:FIA"; //1Khz - //var parameter = "xIntakeL:Controller"; //200Hz - //var parameter = "TTyreSurfaceFLMuxed:MathMRL"; //1Khz - - var totalMilliseconds = 0D; - var max = 0D; - var min = double.MaxValue; - const int minutes = 30; - const int iterations = 50; - - var startTime = clientSession.Session.LapCollection.First(x => x.Number == 20).StartTime; - var endTimeTime = startTime + TimeSpan.FromMinutes(minutes).ToNanoseconds(); - - for (var i = 0; i < iterations; i++) - { - using (var pda = clientSession.Session.CreateParameterDataAccess(parameter)) - { - var sw = Stopwatch.StartNew(); - var samples = pda.GetSamplesBetween( - startTime, - endTimeTime, - 0); - sw.Stop(); - - Console.WriteLine($"Elapsed {sw.Elapsed.TotalMilliseconds} Sample Freq: {samples.SampleCount / (minutes * 60)}Hz"); - totalMilliseconds += sw.Elapsed.TotalMilliseconds; - if (sw.Elapsed.TotalMilliseconds > max) - { - max = sw.Elapsed.TotalMilliseconds; - } - if (sw.Elapsed.TotalMilliseconds < min) - { - min = sw.Elapsed.TotalMilliseconds; - } - } - } - - Console.WriteLine(); - Console.WriteLine($"Average: {totalMilliseconds / iterations} Min: {min} Max: {max}"); - } - - /// - /// Use connection manager to get connections - /// - private static void ConnectionManagerTest() - { - ConnectionManager cm = new ConnectionManager(); - var dbConnections = cm.GetDataBaseConnections(); - foreach (var db in dbConnections) - { - Console.WriteLine($"Connection {db.Identifier} {db.DatabaseName}"); - } - } - - /// - /// Get a session from DB by SessionGUID - /// - private static void GetSessionSummaryBySessionGUID() - { - using (var qm = QueryManager.CreateQueryManager(ConnectionString)) - { - qm.Filter = new ScalarFilter("SessionGUID", MatchingRule.EqualTo, "9485a6cf-442f-497d-bb08-3946c2b0ab28", true); - var sessionSummary01 = qm.ExecuteQuery().FirstOrDefault(); - if (sessionSummary01 == null) - { - Console.WriteLine("Session not found"); - return; - } - - Console.WriteLine($"Session found {sessionSummary01.Identifier}"); - } - } - - /// - /// Load data an SSN file, add some Markers to it if they don't already exist, extract the data in the ranges defined by the Markers. - /// - private static void AddAndProcessMarkers() - { - var fileSessionManager = FileSessionManager.CreateFileSessionManager(); - - var session = fileSessionManager.Load(@".ssn")?.Session; - - if (session == null) - { - Console.WriteLine("Session not found"); - return; - } - - if (session.Markers.Count(m => m.MarkerType == "Demo") < 3) - { - var newMarkers = new List(); - - // Add some demo markers to the sessions to define some arbitrary time ranges - for (var i = 1; i <= 3; i++) - { - // Start in the middle of the session and mark 20ms every 100s - var markerStartTime = session.StartTime + ((session.EndTime - session.StartTime) / 2) + (i * 100 * NanosecondExtensions.NanosecondsPerSecond); - // 20ms after the start time - var markerEndTime = markerStartTime + (20 * NanosecondExtensions.NanosecondsPerMillisecond); - - var marker = new Marker(markerStartTime, markerEndTime, "Marker" + i, "Demo", "Demonstration marker " + i); - - newMarkers.Add(marker); - } - - // Zero or more annotations can be added to each marker as required - newMarkers[0].AddLabel( - new MarkerLabel - { - Label = "Short user description", - Description = "A long description to explain what the marker represents to the user", - Name = "conciseNameForCodeUse01" - }); - - newMarkers[2].AddLabel( - new MarkerLabel - { - Label = "Average Speed", - Description = "A average sustained wind speed during this time period", - Value = "9.6382834", - Format = "%.2f", - Unit = "m/s", - Name = "windSpeedDataHighVelocity01" - }); - - newMarkers[1].AddLabel( - new MarkerLabel - { - Label = "Direction", - Description = "The direction of the wind during this time period", - Value = "323", - Format = "%i", - Unit = "deg", - Name = "windSpeedDataHighDirection01" - }); - - newMarkers[1].AddLabel( - new MarkerLabel - { - Label = "Max Temp", - Value = "27.2", - Unit = "°C", - Name = "temp01" - }); - - foreach (var marker in newMarkers) - { - session.Markers.Add(marker); - } - } - - // Demonstrate how to extract the data in the ranges defined by the demo markers - using (var pda = session.CreateParameterDataAccess("vCar:Chassis")) - { - foreach (var marker in session.Markers.Where(m => m.MarkerType == "Demo")) - { - Console.WriteLine($"Range start {marker.StartTimestamp.Value.ToTimeString()} end {marker.EndTimestamp.Value.ToTimeString()} {marker.Description}"); - - var samples = pda.GetSamplesBetween(marker.StartTimestamp.Value, marker.EndTimestamp.Value); - - for (var i = 0; i < samples.SampleCount && samples.Timestamp[i] <= marker.EndTimestamp; i++) - { - Console.Write($"({samples.Timestamp[i]},{samples.Data[i]}), "); - } - - Console.WriteLine(); - } - } - } - - /// - /// Load data from an SSN file - /// - private static void LoadSSN() - { - var fileSessionManager = FileSessionManager.CreateFileSessionManager(); - - //var session01 = fileSessionManager.Load(@"C:\Session Location\Session To Load.ssn"); - var session01 = fileSessionManager.Load(@"C:\Session Location\Session To Load.ssn", new List - { - @"C:\Session Location\Session To Load.VTS.001.ssv" - }); // session with associates - - if (session01 == null) - { - Console.WriteLine("Session not found"); - return; - } - - var vCarIdentifier = "vCar:Chassis"; - using (var pda = session01.Session.CreateParameterDataAccess(vCarIdentifier)) - { - pda.GoTo(session01.Session.StartTime + (session01.Session.EndTime - session01.Session.StartTime) / 2); - var samples = pda.GetNextSamples(10); - - Console.WriteLine($"** Data for {vCarIdentifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - } - - private static void LoadSSNWithAssociatedMerge() - { - //SQLite ssn style connection string - ConnectionString = $@"DbEngine=SQLite;Data Source={ - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - }\McLaren Applied Technologies\ATLAS 10\SQL Race\IndexingSessionCache.ssn2;"; - - var qm = QueryManager.CreateQueryManager(ConnectionString); - qm.SearchOptions = SearchOptions.RetrieveAssociateSessionSummaries; - // Get the most recent live session from the database - var sessionSummaries = qm.ExecuteQuery().ToList(); - - var sessionSummary01 = - sessionSummaries.FirstOrDefault( - x => x.Identifier == ""); - if (sessionSummary01 == null) - { - Console.WriteLine("Session not found"); - return; - } - - Console.WriteLine("Loading session..."); - var sessionManager = SessionManager.CreateSessionManager(); - - var session01 = sessionManager.Load(sessionSummary01.Key, ConnectionString, new[] { sessionSummary01.Associates.Last() }); - var session = session01.Session; - Console.WriteLine("Session loaded"); - - var muTyreRLIdentifier = "muTyreRL:MRLTyres"; - using (var pda = session01.Session.CreateParameterDataAccess(muTyreRLIdentifier)) - { - pda.GoTo(session.StartTime + (session.EndTime - session.StartTime) / 2); - var samples = pda.GetNextSamples(10); - - Console.WriteLine($"** Data for {muTyreRLIdentifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - - var vCarIdentifier = "vCar:Chassis"; - using (var pda = session01.Session.CreateParameterDataAccess(vCarIdentifier)) - { - pda.GoTo(session.StartTime + (session.EndTime - session.StartTime) / 2); - var samples = pda.GetNextSamples(10); - - Console.WriteLine($"** Data for {vCarIdentifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - } - - private static void CompositeSessionTestWithAppendedSessions() - { - ConnectionString = @"Data Source=mesltgs1;Initial Catalog=SQLRACE143;Integrated Security=True"; - - var compositeSession = new CompositeSession(CompositeSessionKey.NewKey(), "CompositeSessionWithvTagSessionsTest"); - - var qm = QueryManager.CreateQueryManager(ConnectionString); - // Get the most recent live session from the database - var sessionSummaries = qm.ExecuteQuery().ToList(); - - var sessionSummary01 = - sessionSummaries.FirstOrDefault( - x => x.Identifier == ""); - if (sessionSummary01 == null) - { - Console.WriteLine("Session not found"); - return; - } - - var sessionSummary02 = - sessionSummaries.FirstOrDefault( - x => x.Identifier == ""); - if (sessionSummary02 == null) - { - Console.WriteLine("Session not found"); - return; - } - - var sessionManager = SessionManager.CreateSessionManager(); - - Console.WriteLine("Loading sessions..."); - - var session01 = sessionManager.Load(sessionSummary01.Key, ConnectionString); - Console.WriteLine($"Session 1 loaded (start: {session01.Session.StartTime.ToTimeString()} end: {session01.Session.EndTime.ToTimeString()}"); - - var session02 = sessionManager.Load(sessionSummary02.Key, ConnectionString); - Console.WriteLine($"Session 2 loaded (start: {session02.Session.StartTime.ToTimeString()} end: {session02.Session.EndTime.ToTimeString()}"); - - compositeSession.Add(session01); - compositeSession.Add(session02); - - var parametervCar = session01.Session.GetParameter("vCar:Chassis"); - if (parametervCar == null) - { - Console.WriteLine("Parameter not found"); - return; - } - - using (var pda = compositeSession.CreateParameterDataAccess(parametervCar.Identifier)) - { - pda.GoTo(compositeSession.StartTime); - var samples = pda.GetNextSamples(compositeSession.EndTime); - - Console.WriteLine($"** Data for {parametervCar.Identifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - } - - private static void CompositeSessionWithvTagSessionsTest() - { - //SQLite ssn style connection string - ConnectionString = @"Data Source=mesltgs1;Initial Catalog=SQLRACE143;Integrated Security=True"; - - var compositeSession = new CompositeSession(CompositeSessionKey.NewKey(), "CompositeSessionWithvTagSessionsTest"); - - - var qm = QueryManager.CreateQueryManager(ConnectionString); - // Get the most recent live session from the database - var sessionSummaries = qm.ExecuteQuery().ToList(); - - var sessionSummary01 = - sessionSummaries.FirstOrDefault( - x => x.Identifier == ""); - if (sessionSummary01 == null) - { - Console.WriteLine("Session not found"); - return; - } - - var sessionSummary02 = - sessionSummaries.FirstOrDefault( - x => x.Identifier == ""); - if (sessionSummary02 == null) - { - Console.WriteLine("Session not found"); - return; - } - - var sessionManager = SessionManager.CreateSessionManager(); - - Console.WriteLine("Loading sessions..."); - - var session01 = sessionManager.Load(sessionSummary01.Key, ConnectionString); - Console.WriteLine("Session loaded"); - - var session02 = sessionManager.Load(sessionSummary02.Key, ConnectionString); - Console.WriteLine("Associated SSV loaded"); - - compositeSession.Add(session01); - compositeSession.Add(session02); - - var parameterAero = session02.Session.GetParameter("muTyreRL:MRLTyres"); - if (parameterAero == null) - { - Console.WriteLine("Parameter not found"); - return; - } - - using (var pda = compositeSession.CreateParameterDataAccess(parameterAero.Identifier)) - { - pda.GoTo(compositeSession.StartTime + (compositeSession.EndTime - compositeSession.StartTime) / 2); - var samples = pda.GetNextSamples(10); - - Console.WriteLine($"** Data for {parameterAero.Identifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - - var parametervCar = session01.Session.GetParameter("vCar:Chassis"); - if (parametervCar == null) - { - Console.WriteLine("Parameter not found"); - return; - } - - using (var pda = compositeSession.CreateParameterDataAccess(parametervCar.Identifier)) - { - pda.GoTo(compositeSession.StartTime + (compositeSession.EndTime - compositeSession.StartTime) / 2); - var samples = pda.GetNextSamples(10); - - Console.WriteLine($"** Data for {parametervCar.Identifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - } - - /// - /// Reads subsampled data from an historic session. - /// - private static void ReadEvents() - { - ConnectionString = @"Data Source=MAT-TWFIASQL02\LOCALSERVER1;Initial Catalog=SQLRACE_DEV1;Integrated Security=True"; - - var sessionKey = SessionKey.Parse("7C57A82B-96AE-43C6-9304-36719C3C9701"); - - Console.WriteLine("Loading session...."); - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - var eventsData = session.Events.GetEventData(session.StartTime, session.EndTime); - foreach (var eventData in eventsData) - { - var eventDefinition = session.GetEventDefinition(eventData.Key); - Console.WriteLine($"{eventData.TimeStamp.ToTimeString()} {eventDefinition.Description} {eventDefinition.Priority}"); - } - } - - Console.WriteLine(); - } - - private static void AddEvents() - { - var sessionKey = SessionKey.Parse("06b75c89-63f9-4266-9bc3-ea9a3666b5b3"); - - Console.WriteLine("Loading session...."); - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - var eventsData = session.Events.GetEventData(session.StartTime, session.EndTime); - Console.WriteLine($"Number Of Events: {eventsData.Count}"); - - var sessionEventService = EventsFactory.CreateSessionEventService(); - var eventDefinition = sessionEventService.AddEventDefinition(sessionKey, "boxbox", EventPriorityType.Low, "FunctionEvents"); - Console.WriteLine($"EventDefinition Added: {eventDefinition.EventDefinitionId}, {eventDefinition.Description}"); - - sessionEventService.AddEvent(sessionKey, eventDefinition.EventDefinitionId, session.StartTime + 1000000000);//1sec - - eventsData = session.Events.GetEventData(session.StartTime, session.EndTime); - Console.WriteLine($"Number Of Events: {eventsData.Count}"); - } - - Console.WriteLine(); - } - - /// - /// Reads subsampled data from an historic session. - /// - private static void ReadData() - { - var identifiers = new List - { - //"nEngine:FIA", - "gLat:Chassis", - }; - - ConnectionString = @"Data Source=MAT-TWFIASQL02\LOCALSERVER1;Initial Catalog=SQLRACE_DEV1;Integrated Security=True"; - - var sessionKey = SessionKey.Parse("7C57A82B-96AE-43C6-9304-36719C3C9701"); - - Console.WriteLine("Loading session...."); - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - var startTime = session.StartTime; - var endTime = session.EndTime; - - var interval = (endTime - startTime) / 2800; - - var timeStamps = CreateTimestampsForTimerange(startTime, endTime, interval); - - foreach (var identifier in identifiers) - { - using (var pda = clientSession.Session.CreateParameterDataAccess(identifier)) - { - pda.GoTo(session.StartTime); - var samples = pda.GetData(timeStamps); - - Console.WriteLine($"** Data for {identifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - } - } - - Console.WriteLine(); - } - - /// - /// Reads subsampled data from an historic session. - /// - private static void ReadAndRewriteData() - { - var pdaDict = new Dictionary(); - - var sessionKey = SessionKey.Parse("403f2ef1-fd67-b560-2410-07606a7c99e2"); - ConnectionString = @"Data Source=MAT-TWFIASQL02\LOCALSERVER1;Initial Catalog=SQLRACE_DEV1;Integrated Security=True"; - - Console.WriteLine("Loading session...."); - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - var startTime = session.StartTime; - var endTime = session.EndTime; - - var identifiers = session.Parameters.Select(x => x.Identifier).ToList(); - - // create the writing session - var sessionManager = SessionManager.CreateSessionManager(); - var writingClientSession = sessionManager.CreateSession( - ConnectionString, - SessionKey.NewKey(), - "Add data session test", - DateTime.Now, - "Session"); - - var channels = SessionHelper.CreateSessionConfigurationForMultipleParameter(clientSession, identifiers); - - var writingSession = writingClientSession.Session; - - var currentTime = startTime; - long nextTime = 0; - - Console.WriteLine("Start reading session"); - - do - { - nextTime = currentTime + TimeSpan.FromSeconds(1).ToNanoseconds(); - nextTime = Math.Min(nextTime, session.EndTime); - - var interval = (nextTime - currentTime) / 1000; - - var timeStamps = CreateTimestampsForTimerange(currentTime, nextTime, interval); - - foreach (var identifier in identifiers) - { - try - { - ParameterDataAccessBase pda; - if (pdaDict.ContainsKey(identifier)) - { - pda = pdaDict[identifier]; - } - else - { - pda = session.CreateParameterDataAccess(identifier); - pdaDict.Add(identifier, pda); - } - - var samples = pda.GetData(timeStamps); - - if (samples.SampleCount > 0) - { - var data = new byte[samples.SampleCount * sizeof(double)]; - - var index = 0; - for (var i = 0; i < samples.SampleCount; i++) - { - Array.Copy(BitConverter.GetBytes(samples.Data[i]), 0, data, index, sizeof(double)); - index += sizeof(double); - } - - writingSession.AddChannelData( - channels[identifier], - samples.Timestamp.First(), - samples.SampleCount, - data); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error reading parameter {identifier}", ex); - } - } - - currentTime = nextTime; - - } while (nextTime >= session.EndTime); - } - - Console.WriteLine("Finish reading session"); - } - - /// - /// Reads samples from an historic session. - /// - private static void ReadSamples() - { - var identifiers = new List - { - //"vCar:Chassis", - //"MEngine:Controller", - "gLat:Chassis", - }; - - ConnectionString = @"Data Source=MAT-TWFIASQL02\LOCALSERVER1;Initial Catalog=SQLRACE_DEV1;Integrated Security=True"; - - var sessionKey = SessionKey.Parse("7C57A82B-96AE-43C6-9304-36719C3C9701"); - - Console.WriteLine("Loading session...."); - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - Console.WriteLine("Session loaded"); - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - foreach (var identifier in identifiers) - { - using (var pda = clientSession.Session.CreateParameterDataAccess(identifier)) - { - var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); - - Console.WriteLine($"** Samples for {identifier}"); - for (var i = 0; i < samples.SampleCount; i++) - { - Console.WriteLine($"{samples.Timestamp[i].ToTimeString()} {samples.Data[i]}"); - } - } - } - } - - Console.WriteLine(); - } - - /// - /// Reads samples from an historic session. - /// - private static void ReadParameterUnit() - { - ConnectionString = @"Data Source=MAT-TWFIASQL02\LOCALSERVER1;Initial Catalog=SQLRACE_DEV1;Integrated Security=True"; - - var sessionKey = SessionKey.Parse("7C57A82B-96AE-43C6-9304-36719C3C9701"); - //var sessionKey = SessionKey.Parse("bd0f4cda-80ac-4c80-80f3-a8b59f97a4de"); - - Console.WriteLine("Loading session...."); - - using (var clientSession = LoadSession(sessionKey, ConnectionString)) - { - Console.WriteLine("Session loaded"); - clientSession.Session.LoadConfiguration(); - var session = clientSession.Session; - - var unit = string.Empty; - var parameter = session.GetParameter("vCar:Chassis"); - - // the Unit set in the parameter, has the priority - if (string.IsNullOrEmpty(parameter.Unit)) - { - // if the Unit in the parameter is not set, the one in the conversion is used - var conversion = session.GetConversion(parameter.ConversionFunctionName); - if (conversion != null) - { - unit = conversion.Units; - } - } - else - { - unit = parameter.Unit; - } - - Console.WriteLine($"The unit for vCar is {unit}"); - } - - Console.WriteLine(); - } - - private static void Session_LapStarted(object sender, LapEventArgs e) - { - Console.WriteLine($"Lap started {e.Lap.Name}"); - } - } -} \ No newline at end of file diff --git a/MAT.SQLRace.HelloData/SessionHelper.cs b/MAT.SQLRace.HelloData/SessionHelper.cs deleted file mode 100644 index b7bccc2..0000000 --- a/MAT.SQLRace.HelloData/SessionHelper.cs +++ /dev/null @@ -1,313 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd. - -using System; -using System.Collections.Generic; - -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Infrastructure.DataPipeline; -using MESL.SqlRace.Enumerators; - -namespace MAT.SQLRace.HelloData -{ - internal class SessionHelper - { - public static Dictionary CreateSessionConfigurationForMultipleParameter( - IClientSession clientSession, - IEnumerable identifiers) - { - var channels = new Dictionary(); - - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - - var session = clientSession.Session; - - var config = session.CreateConfiguration(); - - var group1 = new ParameterGroup(ParameterGroupIdentifier, "pg1_description"); - - var applicationGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup1); - config.AddParameterGroup(group1); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var vCarFrequency = new Frequency(2, FrequencyUnit.Hz); - - uint channelId = 1; - foreach (var identifier in identifiers) - { - channels[identifier] = channelId; - - var channel = new Channel( - channelId++, - "Channel" + channelId, - vCarFrequency.ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var parameter = new Parameter( - identifier, - identifier, - identifier, - 400, //vCar maximum value - 0, //vCar minimum value - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - channel.Id, - ApplicationGroupName); - - config.AddChannel(channel); - config.AddParameter(parameter); - } - - config.Commit(); - - return channels; - } - - public static Parameter CreateSessionConfigurationForOneParameter(IClientSession clientSession) - { - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - const string ParameterName = "MyParam"; - const uint MyParamChannelId = 999998; //must be unique - const int ApplicationId = 998; - var parameterIdentifier = $"{ParameterName}:{ApplicationGroupName}"; - - var session = clientSession.Session; - - var config = session.CreateConfiguration(); - - var group1 = new ParameterGroup(ParameterGroupIdentifier, "pg1_description"); - - var applicationGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - ApplicationId, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup1); - config.AddParameterGroup(group1); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var myParamFrequency = new Frequency(2, FrequencyUnit.Hz); - - var myParameterChannel = new Channel( - MyParamChannelId, - "MyParamChannel", - myParamFrequency.ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var myParameter = new Parameter( - parameterIdentifier, - ParameterName, - ParameterName + "Description", - 400, - 0, - 1, - 0, - 0, - 0xFFFF, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - myParameterChannel.Id, - ApplicationGroupName); - - config.AddChannel(myParameterChannel); - config.AddParameter(myParameter); - - config.Commit(); - - return myParameter; - } - - public static Parameter CreateSessionConfigurationForOneParameterWithMultipleChannels(IClientSession clientSession) - { - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - const string ParameterName = "MyParam"; - const int ApplicationId = 998; - var parameterIdentifier = $"{ParameterName}:{ApplicationGroupName}"; - - var paramChannelIds = new List() { 925431, 985439, 886634 }; - var randomFrequenciesList = new List { 200, 100, 500 }; - - var session = clientSession.Session; - - var config = session.CreateConfiguration(); - - var paramGroup = new ParameterGroup(ParameterGroupIdentifier, "pg_description"); - - var applicationGroup = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - ApplicationId, - new List - { - paramGroup.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup); - config.AddParameterGroup(paramGroup); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var parameterChannel1 = new Channel( - paramChannelIds[0], - "paramChannel1", - new Frequency(randomFrequenciesList[0], FrequencyUnit.Hz).ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var parameterChannel2 = new Channel( - paramChannelIds[1], - "paramChannel2", - new Frequency(randomFrequenciesList[1], FrequencyUnit.Hz).ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var parameterChannel3 = new Channel( - paramChannelIds[2], - "paramChannel3", - new Frequency(randomFrequenciesList[2], FrequencyUnit.Hz).ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var myParameter = new Parameter( - parameterIdentifier, - ParameterName, - ParameterName + "Description", - 400, - 0, - 1, - 0, - 0, - 0xFFFF, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - paramChannelIds, - ApplicationGroupName); - - config.AddChannels(new List{parameterChannel1, parameterChannel2, parameterChannel3}); - config.AddParameter(myParameter); - - config.Commit(); - - return myParameter; - } - - public static Parameter CreateTransientConfigurationForOneParameter(IClientSession clientSession) - { - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - const string ParameterName = "MyTransientParam"; - const uint MyParamChannelId = 999999; //must be unique - const int ApplicationId = 999; - var parameterIdentifier = $"{ParameterName}:{ApplicationGroupName}"; - - var session = clientSession.Session; - - var transientConfigSet = session.CreateTransientConfiguration(); - - var parameterGroup = new ParameterGroup(ParameterGroupIdentifier, "some description!"); - transientConfigSet.AddParameterGroup(parameterGroup); - - var applicationGroup = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName + "App Group Desc!!", - ApplicationId, - new List - { - parameterGroup.Identifier - }); - transientConfigSet.AddGroup(applicationGroup); - - var conversion = RationalConversion.CreateSimple1To1Conversion(ConversionFunctionName, "myunit", "%5.2f"); - transientConfigSet.AddConversion(conversion); - - var paramFrequency = new Frequency(2, FrequencyUnit.Hz); - - var paramChannel = new Channel( - MyParamChannelId, - "MyParamChannel", - paramFrequency.ToInterval(), - DataType.Double64Bit, - ChannelDataSourceType.Periodic, - string.Empty, - true); - - transientConfigSet.AddChannel(paramChannel); - - var myParameter = new Parameter( - parameterIdentifier, - ParameterName, - ParameterName + "Description", - 400, //maximum value - 0, //minimum value - 1, - 0, - 0, - 0xFFFF, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - paramChannel.Id, - ApplicationGroupName); - - transientConfigSet.AddParameter(myParameter); - - transientConfigSet.Commit(); - - return myParameter; - } - } -} \ No newline at end of file diff --git a/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/App.config b/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/App.config deleted file mode 100644 index fa04e50..0000000 --- a/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/App.config +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters.csproj b/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters.csproj deleted file mode 100644 index f1d35f8..0000000 --- a/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0-windows - Exe - false - true - true - x64 - - - - - \ No newline at end of file diff --git a/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/Program.cs b/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/Program.cs deleted file mode 100644 index ada0dd4..0000000 --- a/MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters/Program.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.Eventing.Reader; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MAT.OCS.Core; -using MAT.SqlRace.Ssn2Splitter; -using MESL.SqlRace.Common.Extensions; -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Infrastructure.DataPipeline; -using MESL.SqlRace.Domain.Infrastructure.Enumerators; -using MESL.SqlRace.Domain.Query; -using MESL.SqlRace.Domain.SessionProcessing; -using MESL.SqlRace.Enumerators; - -namespace MAT.SQLRace.LoadSessionFromDatabaseAndReadParameters -{ - /// - /// In order to be able to use this example follow these instructions; - /// https://github.com/mat-docs/packages - /// - /// Whenever you are setting up your project you should use .NETFramework 4.8. - /// You need to set up the compilation to be for x64 processors in order for this example to work - /// - /// Once registered in Visual Studio, you need to install MESL.SQLRace.API package from NuGet making sure that you have selected the McLaren Applied Github packages in the top - /// right corner (defaults to nuget.org, where if you search for the package it would not show). - /// Apart from this NuGet package, if you plan to load SSN files you need to follow these steps too: (not needed for this example) - /// - Install MAT.ATLAS.SupportFilesLite from NuGet directory making sure that you have selected on the top right corner MA repository - /// - Setup the build config to build in x64 based CPUs. This is required for SSNs to be loaded. - /// Once this package is installed, you would only need to pay attenion to the TODOs written in the code. - /// - /// Find the documentation for the API in the link below - /// https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html - /// - /// Further examples can be found here: - /// https://github.com/mat-docs/MAT.OCS.SQLRace.Examples - /// - class Program - { - - - private const int MaxSamplesRequest = 32767; - // TODO: Change the sessionGUID to something sensible - private static readonly string sessionGUID = "SOME_SESSION_GUID"; - private static readonly List parameterList = new List { "vCar:Chassis", "nEngine:FIA", "aSteerFIA", "aSteerWheel:Chassis" }; - - static void Main(string[] args) - { - //TODO: Change the connection string for it to suit your Database system - const string connectionString = @"Data Source=SOME_DATABASE_SERVER\LOCAL;Initial Catalog=SOME_DATABASE_NAME;Integrated Security=True"; - Console.WriteLine("Initializing SQL Race...."); - Console.WriteLine(Directory.GetCurrentDirectory()); - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - - Console.WriteLine("SQLRace has been initialized correctly"); - var clientSession = LoadSessionFromGUID(sessionGUID, connectionString); - ReadSamples(clientSession, parameterList); - - Console.WriteLine("Press enter to exit..."); - Console.ReadLine(); - } - - private static void ReadSamples(IClientSession clientSession, List parameterList) - { - Console.WriteLine("Trying to acquire samples of the following parameters"); - foreach (var parameter in parameterList) - { - Console.WriteLine(parameter); - } - foreach (var parameter in parameterList) - { - var session = clientSession.Session; - using (var pda = clientSession.Session.CreateParameterDataAccess(parameter)) - { - var end = false; - pda.GoTo(session.StartTime); - var requestedSamples = -1; - var nSamples = pda.GetSamplesCount(session.StartTime, session.EndTime); - List dataStatusTypes = new List((int)nSamples); - List data = new List((int)nSamples); - List timestamps = new List((int)nSamples); - List missingSamplesData = new List((int)nSamples); - // This is one approach to extract samples iteratively, see below for a more streamlined approach - while (!end) - { - - if (nSamples > MaxSamplesRequest) - { - requestedSamples = MaxSamplesRequest; - nSamples -= MaxSamplesRequest; - } - else - { - requestedSamples = (int)nSamples; - end = true; - } - - var samples = pda.GetNextSamples(requestedSamples); - - for (int i = 0; i < samples.Data.Length; i++) - { - if (samples.DataStatus[i] == DataStatusType.Missing) - { - missingSamplesData.Add(samples.Data[i]); - } - - } - data.AddRange(samples.Data); - timestamps.AddRange(samples.Timestamp); - dataStatusTypes.AddRange(samples.DataStatus); - } - - var nDSSamples = dataStatusTypes.FindAll(d => d == DataStatusType.Sample).Count(); - var nDSDefault = dataStatusTypes.FindAll(d => d == DataStatusType.Missing).Count(); - var nDSOther = dataStatusTypes.Count() - nDSSamples - nDSDefault; - Console.WriteLine("{3}: Samples : {0}, Missing: {1}, Other: {2}", nDSSamples, nDSDefault, nDSOther, parameter); - - // Another Approach - var numOfSamples = pda.GetSamplesCount(session.StartTime, session.EndTime); - pda.GoTo(session.StartTime); - // You have to be mindful of the int max value limitation in case your session has way more samples. - var mySamples = pda.GetNextSamples((int)numOfSamples); - - // TODO: Do what you want with your samples - } - - } - - } - - private static IClientSession LoadSessionFromGUID(string sessionGUID, string connectionString) - { - if (sessionGUID != null) - { - var sessionManager = SessionManager.CreateSessionManager(); - return sessionManager.Load(SessionKey.Parse(sessionGUID), connectionString); - } - else - { - throw new Exception("SessionGUID cannot be null. No session will be loaded"); - } - } - } -} diff --git a/MAT.SqlRace.Functions.HelloDotNet/HelloFunction.cs b/MAT.SqlRace.Functions.HelloDotNet/HelloFunction.cs deleted file mode 100644 index e6a6f0c..0000000 --- a/MAT.SqlRace.Functions.HelloDotNet/HelloFunction.cs +++ /dev/null @@ -1,92 +0,0 @@ -using MESL.SqlRace.Domain.Functions; -using MESL.SqlRace.Domain.Functions.DotNet; -using MESL.SqlRace.Enumerators; -using MESL.SqlRace.Functions.Interfaces.Enums; -using System; -using System.ComponentModel.Composition; -using System.Linq; - -namespace MAT.SqlRace.Functions.HelloDotNet -{ - [Export(typeof(IDotNetFunction))] - [Serializable] - public class HelloFunction : IDotNetFunction - { - internal const string FunctionName = "HelloDotNet"; - internal const string ParameterName = "vCarDoubledParam"; - internal const string ApplicationName = "DotNetGroup"; - internal const string ParameterIdentifier = ParameterName + ":" + ApplicationName; - - public string Name => FunctionName; - - /// - /// This method will be called by SqlRace framework. - /// - /// - public void Initialize(IFunctionManager functionManager) - { - // create the function definition - var functionDefinition = functionManager.CreateFunctionDefinition(DotNetFunctionConstants.UniqueId, FunctionMode.Instantaneous, CalculationModeInfoDefinition.EachSamplePoint()); - //var functionDefinition = functionManager.CreateFunctionDefinition(DotNetFunctionConstants.UniqueId); - functionDefinition.CalculateOverWholeSession = false; - functionDefinition.InterpolateBetweenSamples = false; - functionDefinition.JoinGapsAroundNull = false; - functionDefinition.Name = FunctionName; - functionDefinition.StoreInSession = false; - - // set the implementation - var implementationDefinition = (DotNetFunctionImplementationDefinition)functionDefinition.ImplementationDefinition; - implementationDefinition.Function = this; - - // create the single output parameter - functionDefinition.OutputParameterDefinitions.Add( - new FunctionOutputParameterDefinition - { - ApplicationName = ApplicationName, - ByteOrder = ByteOrderType.BigEndian, - Description = "HelloDotNet.vCarDoubled", - FormatOverride = @"%5.2f", - Name = ParameterName, - Units = "km/h", - MaximumValue = "800", - MinimumValue = "0", - ShowInBrowser = true - }); - - functionDefinition.InputParameterIdentifiers.Add("vCar:Chassis"); - - // build the function - var buildResults = functionManager.Build(functionDefinition); - - // make sure we have no build errors - if (buildResults.Errors.Count > 0) - { - Console.WriteLine($"Error building DotNet function '{FunctionName}': {buildResults.Errors.FirstOrDefault()?.ErrorText}."); - } - else - { - Console.WriteLine($"DotNet function '{FunctionName}' created successfully."); - } - } - - /// - /// This method is called once per data request. - /// - /// - public void Execute(IExecutionContext executionContext) - { - var timestamps = executionContext.FunctionInput.Timestamps; - var timestampsCount = timestamps.Length; - - var index = executionContext.FunctionOutput.OutputParameterIndexes[ParameterIdentifier]; - var parameterOutput = executionContext.FunctionOutput.OutputParametersValues[index]; - var input = executionContext.FunctionInput.Values[index]; - - for (int i = 0; i < timestampsCount; i++) - { - parameterOutput[i] = input[i] * 2; - //Console.WriteLine($"{data.Timestamp[i]},{new DateTime(data.Timestamp[i] / 100):HH:mm:ss.fff},{data.Data[i]} = {result[i]}"); - } - } - } -} diff --git a/MAT.SqlRace.Functions.HelloDotNet/HelloPdaCountFunction.cs b/MAT.SqlRace.Functions.HelloDotNet/HelloPdaCountFunction.cs deleted file mode 100644 index cba45c5..0000000 --- a/MAT.SqlRace.Functions.HelloDotNet/HelloPdaCountFunction.cs +++ /dev/null @@ -1,100 +0,0 @@ -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Functions; -using MESL.SqlRace.Domain.Functions.DotNet; -using MESL.SqlRace.Enumerators; -using System; -using System.ComponentModel.Composition; -using System.Linq; - -namespace MAT.SqlRace.Functions.HelloDotNet -{ - [Export(typeof(IDotNetFunction))] - [Serializable] - public class HelloPdaCountFunction : IDotNetFunction - { - internal const string FunctionName = "HelloWithPdaDotNet"; - internal const string ParameterName = "vCarPdaCountParam"; //the name that appears in parameter browser - internal const string ApplicationName = "DotNetGroup"; - internal const string ParameterIdentifier = ParameterName + ":" + ApplicationName; - - [NonSerialized] - private ParameterDataAccessBase parameterDataAccess; - - public string Name => FunctionName; - - /// - /// API consumer must call this method explicitly after instantiating IFunctionManager; FunctionManagerFactory.Create(); - /// - /// - public void Initialize(IFunctionManager functionManager) - { - // create the function definition - var functionDefinition = functionManager.CreateFunctionDefinition(DotNetFunctionConstants.UniqueId); - functionDefinition.CalculateOverWholeSession = false; - functionDefinition.InterpolateBetweenSamples = false; - functionDefinition.JoinGapsAroundNull = false; - functionDefinition.Name = FunctionName; - functionDefinition.StoreInSession = false; - - // set the implementation - var implementationDefinition = (DotNetFunctionImplementationDefinition)functionDefinition.ImplementationDefinition; - implementationDefinition.Function = this; - - // create the single output parameter - functionDefinition.OutputParameterDefinitions.Add( - new FunctionOutputParameterDefinition - { - ApplicationName = ApplicationName, - ByteOrder = ByteOrderType.BigEndian, - Description = "HelloDotNet.vCarPdaCountParam", - FormatOverride = @"%5.2f", - Name = ParameterName, - Units = "", - MaximumValue = "1000000", - MinimumValue = "0", - ShowInBrowser = true - }); - - //functionDefinition.InputParameterIdentifiers.Add("vCar:Chassis"); - - // build the function - var buildResults = functionManager.Build(functionDefinition); - - // make sure we have no build errors - if (buildResults.Errors.Count > 0) - { - Console.WriteLine($"Error building DotNet function '{FunctionName}': {buildResults.Errors.FirstOrDefault()?.ErrorText}."); - } - else - { - Console.WriteLine($"DotNet function '{FunctionName}' created successfully."); - } - } - - /// - /// - /// - /// - public void Execute(IExecutionContext executionContext) - { - var timestamps = executionContext.FunctionInput.Timestamps; - var timestampsCount = timestamps.Length; - - var index = executionContext.FunctionOutput.OutputParameterIndexes[ParameterIdentifier]; - var parameterOutput = executionContext.FunctionOutput.OutputParametersValues[index]; - var input = executionContext.FunctionInput.Values[index]; - - if (parameterDataAccess == null) - { - this.parameterDataAccess = executionContext.Session.CreateParameterDataAccess("vCar:Chassis"); - } - - var count = parameterDataAccess.GetSamplesCount(timestamps.FirstOrDefault(), timestamps.LastOrDefault()); - - for (int i = 0; i < timestampsCount; i++) - { - parameterOutput[i] = count; - } - } - } -} diff --git a/MAT.SqlRace.Functions.HelloDotNet/MAT.SqlRace.Functions.HelloDotNet.csproj b/MAT.SqlRace.Functions.HelloDotNet/MAT.SqlRace.Functions.HelloDotNet.csproj deleted file mode 100644 index 2ffd5dc..0000000 --- a/MAT.SqlRace.Functions.HelloDotNet/MAT.SqlRace.Functions.HelloDotNet.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - net8.0-windows - Library - false - true - true - x64 - - - - - - - - \ No newline at end of file diff --git a/MAT.SqlRace.Functions.HelloDotNet/ReadMe.txt b/MAT.SqlRace.Functions.HelloDotNet/ReadMe.txt deleted file mode 100644 index 16534e9..0000000 --- a/MAT.SqlRace.Functions.HelloDotNet/ReadMe.txt +++ /dev/null @@ -1,3 +0,0 @@ -Steps -1- Build solution with latest SqlRace release. -2- Copy output DLL to ATLAS functions directory e.g. copy MAT.SqlRace.Functions.HelloDotNet.dll to C:\Users\{name.surname}\Documents\McLaren Electronic Systems\SQL Race\Functions \ No newline at end of file diff --git a/MAT.SqlRace.Functions.HelloDotNet/app.config b/MAT.SqlRace.Functions.HelloDotNet/app.config deleted file mode 100644 index 9a75420..0000000 --- a/MAT.SqlRace.Functions.HelloDotNet/app.config +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MAT.SqlRace.ServerListener/App.config b/MAT.SqlRace.ServerListener/App.config deleted file mode 100644 index 03c57c2..0000000 --- a/MAT.SqlRace.ServerListener/App.config +++ /dev/null @@ -1,62 +0,0 @@ - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MAT.SqlRace.ServerListener/MAT.SqlRace.ServerListener.csproj b/MAT.SqlRace.ServerListener/MAT.SqlRace.ServerListener.csproj deleted file mode 100644 index f1d35f8..0000000 --- a/MAT.SqlRace.ServerListener/MAT.SqlRace.ServerListener.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0-windows - Exe - false - true - true - x64 - - - - - \ No newline at end of file diff --git a/MAT.SqlRace.ServerListener/Program.cs b/MAT.SqlRace.ServerListener/Program.cs deleted file mode 100644 index 0b87e52..0000000 --- a/MAT.SqlRace.ServerListener/Program.cs +++ /dev/null @@ -1,87 +0,0 @@ -using MAT.OCS.Core; -using MESL.SqlRace.Common.Extensions; -using MESL.SqlRace.Domain; -using System; -using System.Linq; -using System.Threading; -using System.Net; - - -namespace MAT.SqlRace.ServerListenerLive -{ - /// - /// Record live data to a local SqlRace (Sqlite) - /// - /// NOTE: - /// If the session is in the state LiveNotInServer, make sure Server Listener port in ATLAS is different to the one specified here. - /// Additionally, ensure UDP and TCP packets are allowed through the firewall settings on the Server Listener Port configured. - /// - internal class Program - { - // The server listener configuration - private const int ServerListenerPortNumber = 6565; - private const string ServerListenerIpAddress = "127.0.0.1"; - - public static void Main(string[] args) - { - var dataSource = @"C:\temp\livesession.ssndb"; - Console.WriteLine(dataSource); - - /// connection strings are case and whitespace sensitive, the following format must be strictly followed for the Server Listener Protocol to successfully establish. - /// SQLite: "DbEngine=SQLite;Data Source={dataSource};Pooling=false;" - /// SQLServer: "server={dataSource};Initial Catalog={database};Trusted_Connection=True;" - var connectionString = $@"DbEngine=SQLite;Data Source={dataSource};Pooling=false;"; - string recorderDbEngine = "SQLite"; // SQLite or SQLServer - var sessionIdentifier = "Server Listener Live Demo"; - - Console.WriteLine("Initialising"); - Core.Initialize(); - - Console.WriteLine("Setting up Server Listener Instance"); - Core.ConfigureServer(true, new IPEndPoint(IPAddress.Parse(ServerListenerIpAddress), ServerListenerPortNumber)); - var sessionManager = SessionManager.CreateSessionManager(); - var recordersConfiguration = RecordersConfiguration.GetRecordersConfiguration(); - recordersConfiguration.AddConfiguration( - Guid.NewGuid(), - recorderDbEngine, - dataSource, - dataSource, - connectionString, - 0); // DeleteSessionOnClose: 0 - no delete, 1 - delete the session on close, 2 - delete the DB - - - Console.WriteLine("Creating new Session"); - var clientSession = sessionManager.CreateSession(connectionString, SessionKey.NewKey(), sessionIdentifier, DateTime.Now, "Session"); - var session = clientSession.Session; - try - { - var parameter = SessionHelper.CreateSessionConfigurationForOneParameter(session); - - for (int i = 0; i < 1000; i++) - { - var newTimestamp = DateTime.Now.ToNanoseconds(); - var newValue = Math.Sin(i / 360.0); - - session.AddChannelData( - parameter.ChannelIds.FirstOrDefault(), - newTimestamp, - 1, //value of 1 param - BitConverter.GetBytes(newValue)); - Thread.Sleep(100); - Console.WriteLine($"Written sample. Timestamp: {newTimestamp.ToTimeString()} Value:{newValue}"); - } - } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); - } - finally - { - session.EndData(); - clientSession.Close(); - } - } - - - } -} diff --git a/MAT.SqlRace.ServerListener/SessionHelper.cs b/MAT.SqlRace.ServerListener/SessionHelper.cs deleted file mode 100644 index 3eaf658..0000000 --- a/MAT.SqlRace.ServerListener/SessionHelper.cs +++ /dev/null @@ -1,223 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd. - -using System.Collections.Generic; -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Infrastructure.DataPipeline; -using MESL.SqlRace.Enumerators; - -namespace MAT.SqlRace.ServerListenerLive -{ - internal class SessionHelper - { - public static Dictionary CreateSessionConfigurationForMultipleParameter( - IClientSession clientSession, - IEnumerable identifiers) - { - var channels = new Dictionary(); - - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - - var session = clientSession.Session; - - var config = session.CreateConfiguration(); - - var group1 = new ParameterGroup(ParameterGroupIdentifier, "pg1_description"); - - var applicationGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup1); - config.AddParameterGroup(group1); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var vCarFrequency = new Frequency(2, FrequencyUnit.Hz); - - uint channelId = 1; - foreach (var identifier in identifiers) - { - channels[identifier] = channelId; - - var channel = new Channel( - channelId++, - "Channel" + channelId, - vCarFrequency.ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var parameter = new Parameter( - identifier, - identifier, - identifier, - 400, //vCar maximum value - 0, //vCar minimum value - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - channel.Id, - ApplicationGroupName); - - config.AddChannel(channel); - config.AddParameter(parameter); - } - - config.Commit(); - - return channels; - } - - public static Parameter CreateSessionConfigurationForOneParameter(Session session) - { - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - const string ParameterName = "MyParam"; - const uint MyParamChannelId = 999998; //must be unique - const int ApplicationId = 998; - var parameterIdentifier = $"{ParameterName}:{ApplicationGroupName}"; - - var config = session.CreateConfiguration(); - - var group1 = new ParameterGroup(ParameterGroupIdentifier, "pg1_description"); - - var applicationGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - ApplicationId, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup1); - config.AddParameterGroup(group1); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var myParamFrequency = new Frequency(2, FrequencyUnit.Hz); - - var myParameterChannel = new Channel( - MyParamChannelId, - "MyParamChannel", - myParamFrequency.ToInterval(), - DataType.Double64Bit, - ChannelDataSourceType.Periodic); - - var myParameter = new Parameter( - parameterIdentifier, - ParameterName, - ParameterName + "Description", - 400, - 0, - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - myParameterChannel.Id, - ApplicationGroupName); - - config.AddChannel(myParameterChannel); - config.AddParameter(myParameter); - - config.Commit(); - - return myParameter; - } - - public static Parameter CreateTransientConfigurationForOneParameter(Session session) - { - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - const string ParameterName = "MyParam"; - const uint MyParamChannelId = 999999; //must be unique - const int ApplicationId = 999; - var parameterIdentifier = $"{ParameterName}:{ApplicationGroupName}"; - - var transientConfigSet = session.CreateTransientConfiguration(); - - var parameterGroup = new ParameterGroup(ParameterGroupIdentifier, "some description!"); - transientConfigSet.AddParameterGroup(parameterGroup); - - var applicationGroup = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName + "App Group Desc!!", - ApplicationId, - new List - { - parameterGroup.Identifier - }); - transientConfigSet.AddGroup(applicationGroup); - - var conversion = RationalConversion.CreateSimple1To1Conversion(ConversionFunctionName, "myunit", "%5.2f"); - transientConfigSet.AddConversion(conversion); - - var paramFrequency = new Frequency(2, FrequencyUnit.Hz); - - var paramChannel = new Channel( - MyParamChannelId, - "MyParamChannel", - paramFrequency.ToInterval(), - DataType.Double64Bit, - ChannelDataSourceType.Periodic, - string.Empty, - true); - - transientConfigSet.AddChannel(paramChannel); - - var myParameter = new Parameter( - parameterIdentifier, - ParameterName, - ParameterName + "Description", - 400, //maximum value - 0, //minimum value - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - paramChannel.Id, - ApplicationGroupName); - - transientConfigSet.AddParameter(myParameter); - - transientConfigSet.Commit(); - - return myParameter; - } - } -} \ No newline at end of file diff --git a/MAT.SqlRace.StandaloneRecorder/App.config b/MAT.SqlRace.StandaloneRecorder/App.config deleted file mode 100644 index 68964b2..0000000 --- a/MAT.SqlRace.StandaloneRecorder/App.config +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MAT.SqlRace.StandaloneRecorder/MAT.SqlRace.StandaloneRecorder.csproj b/MAT.SqlRace.StandaloneRecorder/MAT.SqlRace.StandaloneRecorder.csproj deleted file mode 100644 index 728f268..0000000 --- a/MAT.SqlRace.StandaloneRecorder/MAT.SqlRace.StandaloneRecorder.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - net8.0-windows - Exe - false - true - true - x64 - - - - - - \ No newline at end of file diff --git a/MAT.SqlRace.StandaloneRecorder/Program.cs b/MAT.SqlRace.StandaloneRecorder/Program.cs deleted file mode 100644 index 3f2f2dd..0000000 --- a/MAT.SqlRace.StandaloneRecorder/Program.cs +++ /dev/null @@ -1,231 +0,0 @@ -using MAT.OCS.ATLAS.Recording; -using MAT.OCS.Core; -using MESL.SqlRace.Common.Extensions; -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Infrastructure.Enumerators; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MAT.SqlRace.StandaloneRecorder -{ - /// - /// Demo features: - /// 1- Record live data to a local SqlRace (Sqlite) - /// 2- Read live vCar samples - /// 3- Create custom parameter and write and read data to live session - /// - /// NOTE: Make sure Server Listener in ADS is enabled under Tools -> SqlRace -> Settings... - /// - internal class Program - { - // The server listener configuration (leave those defaults in case not needed) - private const int ServerListenerPortNumber = 6565; - private const string ServerListenerIpAddress = "127.0.0.1"; - - // The ADS Host:Name - private const string RecorderDataServer = "M801338:Default"; - - private static string sqlConnectionString; - - private static DataServerTelemetryRecorder recorder; - private static RecorderState recordingState; - - public static void Main(string[] args) - { - try - { - var dbEngine = "SQLite"; - var dataSource = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + - @"\McLaren Applied Technologies\ATLAS 10\SQL Race\LiveSessionCache.ssn2"; - sqlConnectionString = $@"DbEngine={dbEngine};Data Source={dataSource};"; - - Core.Initialize(); - var sessionManager = SessionManager.CreateSessionManager(); - sessionManager.SessionEventOccurred += SessionManager_SessionEventOccurred; - - Console.WriteLine("Initialising"); - - recordingState = RecorderState.Idle; - - Recorders.Initialise(ServerListenerIpAddress, ServerListenerPortNumber); - - Console.WriteLine("Setting up Recorder Instance"); - - recorder = Recorders.CreateDataServerTelemetryRecorder(); - recorder.SetSessionIdentifier("%y%m%d%H%M%S"); - recorder.OnStatusChanged += recorder_OnStatusChanged; - recorder.SetSQLRaceConnection( - Guid.NewGuid(), - dbEngine, dataSource, - sqlConnectionString, - sqlConnectionString, - 0); // DeleteSessionOnClose: 0 - no delete, 1 - delete the session on close, 2 - delete the DB - - Console.WriteLine("Getting Available Server List"); - recorder.RefreshServerList(); - var availableServers = recorder.GetServerList(); - - foreach (var svr in availableServers) - { - Console.WriteLine("ADS Found : " + svr); - } - - if (availableServers.Count > 0) - { - Console.WriteLine($"Setting ADS Connection to {RecorderDataServer}"); - recorder.SetDataServer(RecorderDataServer); - - MonitorRecorder(); - } - } - catch (Exception e) - { - Console.WriteLine(""); - Console.WriteLine("ERROR:"); - Console.WriteLine(e); - Console.WriteLine("ERROR"); - Console.WriteLine(""); - } - Console.WriteLine("Test Complete"); - Console.ReadLine(); - } - - /// - /// Pulls data out from the live session. - /// - /// The session key. - private static void StartGetData(SessionKey sessionKey) - { - Task.Run( - () => - { - var sessionManager = SessionManager.CreateSessionManager(); - - var sessionSummary = sessionManager.FindSummaryBy(sessionKey, sqlConnectionString); - using (var clientSession = sessionManager.Load(sessionSummary.Key, sessionSummary.GetConnectionString())) - { - var session = clientSession.Session; - - var mrs = new ManualResetEvent(false); - session.RdaParametersChanged += (sender, args) => - { - Console.WriteLine($"Parameter number is {session.Parameters.Count}"); - if (session.ContainsParameter("vCar:Chassis")) - { - mrs.Set(); - } - }; - - // wait for the configuration to be processed - mrs.WaitOne(); - - var parameter = SessionHelper.CreateSessionConfigurationForOneParameter(session); - - // pulls vCar data out every 2 seconds - try - { - using (var pda = session.CreateParameterDataAccess("vCar:Chassis")) - { - using (var pdaNewParameter = session.CreateParameterDataAccess(parameter.Identifier)) - { - while (true) - { - var samples = ReadData(session, pda); - WriteData(session, parameter, pdaNewParameter, samples); - - Thread.Sleep(2000); - } - } - } - } - catch (Exception ex) - { - Console.WriteLine("Error occurred" + ex.Message); - } - } - }); - } - - private static void WriteData(Session session, Parameter parameter, ParameterDataAccessBase pdaNewParameter, ParameterValues samples) - { - // add data to new created parameter - for (var j = samples.SampleCount - 1; j >= 0; j--) - { - var newTimestamp = samples.Timestamp[j]; - var newValue = samples.Data[j] + 10; - - session.AddChannelData( - parameter.ChannelIds.FirstOrDefault(), - newTimestamp, - 1, //value of 1 param - BitConverter.GetBytes(newValue)); - - Console.WriteLine($"Written sample. Timestamp: {newTimestamp.ToTimeString()} Value:{newValue}"); - } - - // read data back to verify the data added - pdaNewParameter.GoTo(session.EndTime); - var samplesNewParameter = pdaNewParameter.GetNextSamples(10, StepDirection.Reverse); - for (var j = 0; j < samplesNewParameter.SampleCount; j++) - { - Console.WriteLine($"{parameter.Identifier} {samplesNewParameter.Timestamp[j].ToTimeString()} {samplesNewParameter.Data[j]}"); - } - } - - private static ParameterValues ReadData(Session session, ParameterDataAccessBase pda) - { - // read vCar data - pda.GoTo(session.EndTime); - var samples = pda.GetNextSamples(10, StepDirection.Reverse); - - Console.WriteLine("** Samples **"); - for (var j = 0; j < samples.SampleCount; j++) - { - Console.WriteLine($"vCar:Chassis {samples.Timestamp[j].ToTimeString()} {samples.Data[j]}"); - } - - return samples; - } - - private static void recorder_OnStatusChanged(RecorderState newState) - { - Console.WriteLine(" => New Recorder Status : " + newState.ToString()); - recordingState = newState; - } - - private static void SessionManager_SessionEventOccurred(object sender, SessionEventArgs e) - { - if (e.EventType == SessionEventType.SessionCreated) - { - StartGetData(e.SessionKey); - } - } - - private static void MonitorRecorder() - { - Console.WriteLine(""); - Console.WriteLine("Starting Recorder Test"); - - Console.WriteLine("Auto Record Enabled"); - recorder.SetAutoRecordEnabled(true); - recordingState = RecorderState.AutoRecordIdle; - - while (recordingState != RecorderState.Idle) - { - Thread.Sleep(30000); - Console.WriteLine(" Recorder Tick..."); - } - - Console.WriteLine("Recording Complete"); - - Console.WriteLine("Auto Record Disabled"); - recorder.SetAutoRecordEnabled(false); - Thread.Sleep(5000); - - recorder.StopRecording(); - Thread.Sleep(25000); - } - } -} diff --git a/MAT.SqlRace.StandaloneRecorder/SessionHelper.cs b/MAT.SqlRace.StandaloneRecorder/SessionHelper.cs deleted file mode 100644 index fb7465a..0000000 --- a/MAT.SqlRace.StandaloneRecorder/SessionHelper.cs +++ /dev/null @@ -1,223 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd. - -using System.Collections.Generic; -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Infrastructure.DataPipeline; -using MESL.SqlRace.Enumerators; - -namespace MAT.SqlRace.StandaloneRecorder -{ - internal class SessionHelper - { - public static Dictionary CreateSessionConfigurationForMultipleParameter( - IClientSession clientSession, - IEnumerable identifiers) - { - var channels = new Dictionary(); - - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - - var session = clientSession.Session; - - var config = session.CreateConfiguration(); - - var group1 = new ParameterGroup(ParameterGroupIdentifier, "pg1_description"); - - var applicationGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup1); - config.AddParameterGroup(group1); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var vCarFrequency = new Frequency(2, FrequencyUnit.Hz); - - uint channelId = 1; - foreach (var identifier in identifiers) - { - channels[identifier] = channelId; - - var channel = new Channel( - channelId++, - "Channel" + channelId, - vCarFrequency.ToInterval(), - DataType.Signed16Bit, - ChannelDataSourceType.Periodic); - - var parameter = new Parameter( - identifier, - identifier, - identifier, - 400, //vCar maximum value - 0, //vCar minimum value - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - channel.Id, - ApplicationGroupName); - - config.AddChannel(channel); - config.AddParameter(parameter); - } - - config.Commit(); - - return channels; - } - - public static Parameter CreateSessionConfigurationForOneParameter(Session session) - { - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - const string ParameterName = "MyParam"; - const uint MyParamChannelId = 999998; //must be unique - const int ApplicationId = 998; - var parameterIdentifier = $"{ParameterName}:{ApplicationGroupName}"; - - var config = session.CreateConfiguration(); - - var group1 = new ParameterGroup(ParameterGroupIdentifier, "pg1_description"); - - var applicationGroup1 = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName, - ApplicationId, - new List - { - group1.Identifier - }) - { - SupportsRda = false - }; - - config.AddGroup(applicationGroup1); - config.AddParameterGroup(group1); - - config.AddConversion( - new RationalConversion(ConversionFunctionName, "kph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)); - - var myParamFrequency = new Frequency(2, FrequencyUnit.Hz); - - var myParameterChannel = new Channel( - MyParamChannelId, - "MyParamChannel", - myParamFrequency.ToInterval(), - DataType.Double64Bit, - ChannelDataSourceType.Periodic); - - var myParameter = new Parameter( - parameterIdentifier, - ParameterName, - ParameterName + "Description", - 400, - 0, - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - myParameterChannel.Id, - ApplicationGroupName); - - config.AddChannel(myParameterChannel); - config.AddParameter(myParameter); - - config.Commit(); - - return myParameter; - } - - public static Parameter CreateTransientConfigurationForOneParameter(Session session) - { - const string ConversionFunctionName = "CONV_MyParam:MyApp"; - const string ApplicationGroupName = "MyApp"; - const string ParameterGroupIdentifier = "MyParamGroup"; - const string ParameterName = "MyParam"; - const uint MyParamChannelId = 999999; //must be unique - const int ApplicationId = 999; - var parameterIdentifier = $"{ParameterName}:{ApplicationGroupName}"; - - var transientConfigSet = session.CreateTransientConfiguration(); - - var parameterGroup = new ParameterGroup(ParameterGroupIdentifier, "some description!"); - transientConfigSet.AddParameterGroup(parameterGroup); - - var applicationGroup = new ApplicationGroup( - ApplicationGroupName, - ApplicationGroupName + "App Group Desc!!", - ApplicationId, - new List - { - parameterGroup.Identifier - }); - transientConfigSet.AddGroup(applicationGroup); - - var conversion = RationalConversion.CreateSimple1To1Conversion(ConversionFunctionName, "myunit", "%5.2f"); - transientConfigSet.AddConversion(conversion); - - var paramFrequency = new Frequency(2, FrequencyUnit.Hz); - - var paramChannel = new Channel( - MyParamChannelId, - "MyParamChannel", - paramFrequency.ToInterval(), - DataType.Double64Bit, - ChannelDataSourceType.Periodic, - string.Empty, - true); - - transientConfigSet.AddChannel(paramChannel); - - var myParameter = new Parameter( - parameterIdentifier, - ParameterName, - ParameterName + "Description", - 400, //maximum value - 0, //minimum value - 1, - 0, - 0, - 255, - 0, - ConversionFunctionName, - new List - { - ParameterGroupIdentifier - }, - paramChannel.Id, - ApplicationGroupName); - - transientConfigSet.AddParameter(myParameter); - - transientConfigSet.Commit(); - - return myParameter; - } - } -} \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/FindAllSessionsExample.cs b/MESL.SqlRace.Examples.Sessions.CSharp/FindAllSessionsExample.cs deleted file mode 100644 index 402866d..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/FindAllSessionsExample.cs +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd - -using System; -using System.Collections.Generic; - -using MESL.SqlRace.Domain; - -namespace MESL.SqlRace.Examples.Sessions.CSharp -{ - /// - /// Class for finding all the sessions in the SQL Race Database. - /// - public class FindAllSessionsExample - { - /// - /// Session Manager instance - /// - private readonly SessionManager sessionManager; - - /// - /// Connection string for SQL Server - /// - private readonly string connectionString; - - /// - /// Initialises a new instance of the class. - /// - /// Connection string - public FindAllSessionsExample(string connectionString) - { - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - this.sessionManager = SessionManager.CreateSessionManager(); - this.connectionString = connectionString; - } - - /// - /// An example demonstrating how to find all the sessions in a SQL race database. - /// - /// String value, an output of the example run - public string FindAllSessionsInDataBase() - { - try - { - // Create a session and add data first - var sessionCreation = new SessionCreationExample(this.connectionString); - sessionCreation.CreateSessionAndAddData(); - MainForm mainForm = MainForm.Main(); - - // Search for all sessions in the database - IList sessions = this.sessionManager.FindBySessionItems(null, this.connectionString); - if (sessions.Count > 0) - { - mainForm.dataGrid.Columns.Add("guidp", "Session GUID"); - mainForm.dataGrid.Columns.Add("startTime", "Start timestamp"); - mainForm.dataGrid.Columns.Add("endTime", "End timestamp"); - mainForm.dataGrid.Columns.Add("laps", "Laps"); - mainForm.dataGrid.Columns.Add("dateOfRecording", "Date of recording"); - - foreach (SessionSummary sessionSummary in sessions) - { - mainForm.dataGrid.Rows.Add(new object[] - { - sessionSummary.Key, - sessionSummary.StartTime, - sessionSummary.EndTime, - sessionSummary.Laps.Count, - sessionSummary.TimeOfRecording - }); - } - } - - return string.Format("The given connection string found {0} sessions in the Database", sessions.Count); - } - catch (Exception) - { - return null; - } - } - } -} \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/LoadSessionExample.cs b/MESL.SqlRace.Examples.Sessions.CSharp/LoadSessionExample.cs deleted file mode 100644 index 3099181..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/LoadSessionExample.cs +++ /dev/null @@ -1,146 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd - -using MAT.OCS.Core; -using MESL.SqlRace.Domain; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace MESL.SqlRace.Examples.Sessions.CSharp -{ - /// - /// A Class demonstrating loading of a SQL Race session. - /// - public class LoadSessionExample - { - private readonly SessionManager sessionManager; - private readonly string connectionString; - - private double[] data; - private long[] timeStamps; - private DataStatusType[] dataStatus; - - /// - /// Initialises a new instance of the class. - /// - /// Connection string - public LoadSessionExample(string connectionString) - { - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - this.sessionManager = SessionManager.CreateSessionManager(); - this.connectionString = connectionString; - } - - /// - /// Gets the Data read from the session. used for graphical display. - /// - public double[] Data - { - get { return this.data; } - } - - /// - /// Gets the Time stamp read from the session. used for graphical display. - /// - public long[] TimeStamps - { - get { return this.timeStamps; } - } - - /// - /// Gets the Data status read from the session. used for grid display. - /// - public DataStatusType[] DataStatus - { - get { return this.dataStatus; } - } - - /// - /// An example demonstrating how to load a session and read data - /// - /// String value, an output of the example run - public string LoadSessionAndReadData() - { - try - { - const string Parameter = "highFrequency_1KHz"; - const int NumberOfSamples = 25; - - // Create the session and add data first - var sessionCreation = new SessionCreationExample(this.connectionString); - SessionKey sessionKey = sessionCreation.CreateSessionAndAddData(); - - // Load the just created session - var clientSession = this.sessionManager.Load(sessionKey, this.connectionString); - - var session = clientSession.Session; - // Read parameters - var parameters = new List(session.Parameters); - - // Read all channels - var channels = parameters.OfType().SelectMany(p => p.Channels); - - // Read all conversions - var conversions = new List(session.Conversions); - - // Read Session details and constants - var sessionDataItems = new List(session.Items); - var constants = new List(session.Constants); - - // Read all the Laps, Lap data items and lap constants - var laps = new List(session.LapCollection); - var lapDataItems = new List(session.LapCollection.LapItems); - var lapConstants = new List(session.LapCollection.Constants); - - // Use PDA to read the data - - this.data = new double[NumberOfSamples]; - this.timeStamps = new long[NumberOfSamples]; - this.dataStatus = new DataStatusType[NumberOfSamples]; - - var pdaParameter = parameters.FirstOrDefault(p => p.Identifier == Parameter) as Parameter; - if (pdaParameter != null) - { - var pda = session.CreateParameterDataAccess(pdaParameter.Identifier); - pda.GoTo(session.StartTime); - - // GetNextData returns samples values - pda.SampleTime = pdaParameter.Channels[0].Interval; - pda.GetNextData(NumberOfSamples, template: new ParameterValuesTemplate { Data = data, DataStatus = this.dataStatus }); - - // GetNextSamples returns actual samples - pda.GetNextSamples(NumberOfSamples, template: new ParameterValuesTemplate - { - Data = this.data, - DataStatus = this.dataStatus, - Timestamp = this.timeStamps - }); - - - } - - clientSession.Close(); - - return - new StringBuilder().AppendFormat("{1}Session with the Key {0} loaded successfully.{1}", sessionKey, Environment.NewLine) - .AppendFormat("Parameters = {0}{1}", parameters.Count, Environment.NewLine) - .AppendFormat("Channels = {0}{1}", channels.Count(), Environment.NewLine) - .AppendFormat("Conversions = {0}{1}", conversions.Count, Environment.NewLine) - .AppendFormat("SessionDataItems = {0}{1}", sessionDataItems.Count, Environment.NewLine) - .AppendFormat("Constants = {0}{1}", constants.Count, Environment.NewLine) - .AppendFormat("Laps = {0}{1}", laps.Count, Environment.NewLine) - .AppendFormat("LapConstants = {0}{1}", lapConstants.Count, Environment.NewLine) - .AppendFormat("LapDataItems = {0}", lapDataItems.Count) - .ToString(); - - } - catch (Exception) - { - return null; - } - } - } -} diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/MESL.SqlRace.Examples.Sessions.CSharp.csproj b/MESL.SqlRace.Examples.Sessions.CSharp/MESL.SqlRace.Examples.Sessions.CSharp.csproj deleted file mode 100644 index 769abd6..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/MESL.SqlRace.Examples.Sessions.CSharp.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - net8.0-windows - WinExe - SAK - SAK - SAK - SAK - MESL.SqlRace.Examples.Sessions.CSharp.Program - ..\ - false - true - true - x64 - - - - - - \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.Designer.cs b/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.Designer.cs deleted file mode 100644 index 2117350..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.Designer.cs +++ /dev/null @@ -1,359 +0,0 @@ -namespace MESL.SqlRace.Examples.Sessions.CSharp -{ - partial class MainForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea2 = new System.Windows.Forms.DataVisualization.Charting.ChartArea(); - System.Windows.Forms.DataVisualization.Charting.Legend legend2 = new System.Windows.Forms.DataVisualization.Charting.Legend(); - System.Windows.Forms.DataVisualization.Charting.Series series2 = new System.Windows.Forms.DataVisualization.Charting.Series(); - this.createSession = new System.Windows.Forms.Button(); - this.createSessionHelp = new System.Windows.Forms.ContextMenuStrip(this.components); - this.blahToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.loadSession = new System.Windows.Forms.Button(); - this.loadSessionHelp = new System.Windows.Forms.ContextMenuStrip(this.components); - this.toolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); - this.display = new System.Windows.Forms.DataVisualization.Charting.Chart(); - this.dataGrid = new System.Windows.Forms.DataGridView(); - this.resultMessage = new System.Windows.Forms.Label(); - this.findAllSessions = new System.Windows.Forms.Button(); - this.FindAllSessionsHelp = new System.Windows.Forms.ContextMenuStrip(this.components); - this.toolStripMenuItem3 = new System.Windows.Forms.ToolStripMenuItem(); - this.findByItems = new System.Windows.Forms.Button(); - this.findByItemHelp = new System.Windows.Forms.ContextMenuStrip(this.components); - this.toolStripMenuItem5 = new System.Windows.Forms.ToolStripMenuItem(); - this.connectionStringTextBox = new System.Windows.Forms.TextBox(); - this.connection = new System.Windows.Forms.Label(); - this.databaseGroup = new System.Windows.Forms.GroupBox(); - this.dataResult = new System.Windows.Forms.Label(); - this.summary = new System.Windows.Forms.Label(); - this.helpToolTip = new System.Windows.Forms.ToolTip(this.components); - this.groupBox1 = new System.Windows.Forms.GroupBox(); - this.label1 = new System.Windows.Forms.Label(); - this.createSessionHelp.SuspendLayout(); - this.loadSessionHelp.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.display)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.dataGrid)).BeginInit(); - this.FindAllSessionsHelp.SuspendLayout(); - this.findByItemHelp.SuspendLayout(); - this.databaseGroup.SuspendLayout(); - this.groupBox1.SuspendLayout(); - this.SuspendLayout(); - // - // createSession - // - this.createSession.ContextMenuStrip = this.createSessionHelp; - this.createSession.Location = new System.Drawing.Point(15, 248); - this.createSession.Name = "createSession"; - this.createSession.Size = new System.Drawing.Size(155, 29); - this.createSession.TabIndex = 2; - this.createSession.Text = "Create Session"; - this.helpToolTip.SetToolTip(this.createSession, resources.GetString("createSession.ToolTip")); - this.createSession.UseVisualStyleBackColor = true; - this.createSession.Click += new System.EventHandler(this.OnCreateSessionClick); - // - // createSessionHelp - // - this.createSessionHelp.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.blahToolStripMenuItem}); - this.createSessionHelp.Name = "contextMenuStrip1"; - this.createSessionHelp.Size = new System.Drawing.Size(107, 26); - // - // blahToolStripMenuItem - // - this.blahToolStripMenuItem.Name = "blahToolStripMenuItem"; - this.blahToolStripMenuItem.Size = new System.Drawing.Size(106, 22); - this.blahToolStripMenuItem.Text = "Help"; - // - // loadSession - // - this.loadSession.ContextMenuStrip = this.loadSessionHelp; - this.loadSession.Location = new System.Drawing.Point(202, 248); - this.loadSession.Name = "loadSession"; - this.loadSession.Size = new System.Drawing.Size(155, 29); - this.loadSession.TabIndex = 3; - this.loadSession.Text = "Load Session"; - this.helpToolTip.SetToolTip(this.loadSession, "Creates a session in the database, closes it and loads it back. Reads the Paramet" + - "ers, Channels, Conversions, Constants etc. Reads the data using the ParameterDat" + - "aAccess"); - this.loadSession.UseVisualStyleBackColor = true; - this.loadSession.Click += new System.EventHandler(this.OnLoadSessionClick); - // - // loadSessionHelp - // - this.loadSessionHelp.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.toolStripMenuItem1}); - this.loadSessionHelp.Name = "contextMenuStrip1"; - this.loadSessionHelp.Size = new System.Drawing.Size(107, 26); - // - // toolStripMenuItem1 - // - this.toolStripMenuItem1.Name = "toolStripMenuItem1"; - this.toolStripMenuItem1.Size = new System.Drawing.Size(106, 22); - this.toolStripMenuItem1.Text = "Help"; - // - // display - // - this.display.BorderlineColor = System.Drawing.Color.Transparent; - chartArea2.Area3DStyle.Inclination = 50; - chartArea2.Area3DStyle.PointDepth = 50; - chartArea2.AxisX2.IsMarginVisible = false; - chartArea2.AxisY2.IsMarginVisible = false; - chartArea2.Name = "ChartArea1"; - this.display.ChartAreas.Add(chartArea2); - legend2.Enabled = false; - legend2.Name = "Legend1"; - this.display.Legends.Add(legend2); - this.display.Location = new System.Drawing.Point(379, 71); - this.display.Margin = new System.Windows.Forms.Padding(0); - this.display.Name = "display"; - this.display.Palette = System.Windows.Forms.DataVisualization.Charting.ChartColorPalette.Bright; - series2.BorderColor = System.Drawing.Color.Red; - series2.ChartArea = "ChartArea1"; - series2.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Spline; - series2.Color = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); - series2.Legend = "Legend1"; - series2.MarkerStyle = System.Windows.Forms.DataVisualization.Charting.MarkerStyle.Diamond; - series2.Name = "Series1"; - series2.ShadowColor = System.Drawing.Color.White; - this.display.Series.Add(series2); - this.display.Size = new System.Drawing.Size(578, 269); - this.display.TabIndex = 8; - this.display.Text = "chart1"; - // - // dataGrid - // - this.dataGrid.AllowUserToAddRows = false; - this.dataGrid.AllowUserToDeleteRows = false; - this.dataGrid.BackgroundColor = System.Drawing.Color.Gray; - this.dataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - this.dataGrid.EditMode = System.Windows.Forms.DataGridViewEditMode.EditProgrammatically; - this.dataGrid.GridColor = System.Drawing.Color.Gray; - this.dataGrid.Location = new System.Drawing.Point(492, 366); - this.dataGrid.Name = "dataGrid"; - this.dataGrid.Size = new System.Drawing.Size(465, 175); - this.dataGrid.TabIndex = 12; - // - // resultMessage - // - this.resultMessage.AutoSize = true; - this.resultMessage.BackColor = System.Drawing.Color.Gray; - this.resultMessage.Location = new System.Drawing.Point(12, 366); - this.resultMessage.MinimumSize = new System.Drawing.Size(465, 175); - this.resultMessage.Name = "resultMessage"; - this.resultMessage.Size = new System.Drawing.Size(465, 175); - this.resultMessage.TabIndex = 10; - // - // findAllSessions - // - this.findAllSessions.ContextMenuStrip = this.FindAllSessionsHelp; - this.findAllSessions.Location = new System.Drawing.Point(15, 293); - this.findAllSessions.Name = "findAllSessions"; - this.findAllSessions.Size = new System.Drawing.Size(155, 29); - this.findAllSessions.TabIndex = 5; - this.findAllSessions.Text = "Find all session in DB"; - this.helpToolTip.SetToolTip(this.findAllSessions, "Adds a session to the database and finds all sessions in a given SQL Race databas" + - "e"); - this.findAllSessions.UseVisualStyleBackColor = true; - this.findAllSessions.Click += new System.EventHandler(this.OnFindAllSessionsClick); - // - // FindAllSessionsHelp - // - this.FindAllSessionsHelp.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.toolStripMenuItem3}); - this.FindAllSessionsHelp.Name = "contextMenuStrip1"; - this.FindAllSessionsHelp.Size = new System.Drawing.Size(100, 26); - // - // toolStripMenuItem3 - // - this.toolStripMenuItem3.Name = "toolStripMenuItem3"; - this.toolStripMenuItem3.Size = new System.Drawing.Size(106, 22); - this.toolStripMenuItem3.Text = "Help"; - // - // findByItems - // - this.findByItems.ContextMenuStrip = this.findByItemHelp; - this.findByItems.Location = new System.Drawing.Point(202, 293); - this.findByItems.Name = "findByItems"; - this.findByItems.Size = new System.Drawing.Size(155, 29); - this.findByItems.TabIndex = 6; - this.findByItems.Text = "Find Sessions by Items"; - this.helpToolTip.SetToolTip(this.findByItems, "Adds a session to the database and searches for the added session based on the Se" + - "ssionDataItem"); - this.findByItems.UseVisualStyleBackColor = true; - this.findByItems.Click += new System.EventHandler(this.OnFindByItemsClick); - // - // findByItemHelp - // - this.findByItemHelp.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.toolStripMenuItem5}); - this.findByItemHelp.Name = "contextMenuStrip1"; - this.findByItemHelp.Size = new System.Drawing.Size(100, 26); - // - // toolStripMenuItem5 - // - this.toolStripMenuItem5.Name = "toolStripMenuItem5"; - this.toolStripMenuItem5.Size = new System.Drawing.Size(106, 22); - this.toolStripMenuItem5.Text = "Help"; - // - // connectionStringTextBox - // - this.connectionStringTextBox.Location = new System.Drawing.Point(116, 19); - this.connectionStringTextBox.MinimumSize = new System.Drawing.Size(725, 25); - this.connectionStringTextBox.Name = "connectionStringTextBox"; - this.connectionStringTextBox.Size = new System.Drawing.Size(725, 20); - this.connectionStringTextBox.TabIndex = 1; - // - // connection - // - this.connection.AutoSize = true; - this.connection.ForeColor = System.Drawing.Color.Blue; - this.connection.Location = new System.Drawing.Point(19, 22); - this.connection.Name = "connection"; - this.connection.Size = new System.Drawing.Size(94, 13); - this.connection.TabIndex = 0; - this.connection.Text = "Connection String:"; - // - // databaseGroup - // - this.databaseGroup.Controls.Add(this.connection); - this.databaseGroup.Controls.Add(this.connectionStringTextBox); - this.databaseGroup.Location = new System.Drawing.Point(15, 12); - this.databaseGroup.Name = "databaseGroup"; - this.databaseGroup.Size = new System.Drawing.Size(886, 56); - this.databaseGroup.TabIndex = 0; - this.databaseGroup.TabStop = false; - this.databaseGroup.Text = "Database Settings"; - // - // dataResult - // - this.dataResult.AutoSize = true; - this.dataResult.ForeColor = System.Drawing.Color.Blue; - this.dataResult.Location = new System.Drawing.Point(489, 350); - this.dataResult.Name = "dataResult"; - this.dataResult.Size = new System.Drawing.Size(71, 13); - this.dataResult.TabIndex = 11; - this.dataResult.Text = "Data Results:"; - // - // summary - // - this.summary.AutoSize = true; - this.summary.ForeColor = System.Drawing.Color.Blue; - this.summary.Location = new System.Drawing.Point(9, 350); - this.summary.Name = "summary"; - this.summary.Size = new System.Drawing.Size(91, 13); - this.summary.TabIndex = 9; - this.summary.Text = "Summary Results:"; - // - // groupBox1 - // - this.groupBox1.Controls.Add(this.label1); - this.groupBox1.Location = new System.Drawing.Point(15, 74); - this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(342, 148); - this.groupBox1.TabIndex = 1; - this.groupBox1.TabStop = false; - this.groupBox1.Text = "Steps to run Examples:"; - // - // label1 - // - this.label1.AutoSize = true; - this.label1.ForeColor = System.Drawing.Color.Blue; - this.label1.Location = new System.Drawing.Point(6, 16); - this.label1.MaximumSize = new System.Drawing.Size(325, 0); - this.label1.MinimumSize = new System.Drawing.Size(325, 100); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(325, 130); - this.label1.TabIndex = 15; - this.label1.Text = resources.GetString("label1.Text"); - // - // MainForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(982, 550); - this.Controls.Add(this.groupBox1); - this.Controls.Add(this.dataResult); - this.Controls.Add(this.summary); - this.Controls.Add(this.databaseGroup); - this.Controls.Add(this.resultMessage); - this.Controls.Add(this.dataGrid); - this.Controls.Add(this.display); - this.Controls.Add(this.createSession); - this.Controls.Add(this.findAllSessions); - this.Controls.Add(this.loadSession); - this.Controls.Add(this.findByItems); - this.ForeColor = System.Drawing.Color.Black; - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Fixed3D; - this.MaximizeBox = false; - this.MinimizeBox = false; - this.Name = "MainForm"; - this.Text = "SQL Race Examples in C#"; - this.createSessionHelp.ResumeLayout(false); - this.loadSessionHelp.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)(this.display)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.dataGrid)).EndInit(); - this.FindAllSessionsHelp.ResumeLayout(false); - this.findByItemHelp.ResumeLayout(false); - this.databaseGroup.ResumeLayout(false); - this.databaseGroup.PerformLayout(); - this.groupBox1.ResumeLayout(false); - this.groupBox1.PerformLayout(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - public System.Windows.Forms.Button loadSession; - public System.Windows.Forms.Button createSession; - public System.Windows.Forms.Label resultMessage; - public System.Windows.Forms.DataGridView dataGrid; - public System.Windows.Forms.Button findAllSessions; - public System.Windows.Forms.Button findByItems; - private System.Windows.Forms.TextBox connectionStringTextBox; - private System.Windows.Forms.Label connection; - private System.Windows.Forms.GroupBox databaseGroup; - private System.Windows.Forms.Label dataResult; - private System.Windows.Forms.Label summary; - private System.Windows.Forms.ToolTip helpToolTip; - private System.Windows.Forms.ContextMenuStrip createSessionHelp; - private System.Windows.Forms.ToolStripMenuItem blahToolStripMenuItem; - private System.Windows.Forms.ContextMenuStrip loadSessionHelp; - private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem1; - private System.Windows.Forms.ContextMenuStrip FindAllSessionsHelp; - private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem3; - private System.Windows.Forms.ContextMenuStrip findByItemHelp; - private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem5; - private System.Windows.Forms.GroupBox groupBox1; - private System.Windows.Forms.Label label1; - private System.Windows.Forms.DataVisualization.Charting.Chart display; - } -} \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.cs b/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.cs deleted file mode 100644 index f9b49c7..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.cs +++ /dev/null @@ -1,191 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd - -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Windows.Forms; - -using MAT.OCS.Core; - -namespace MESL.SqlRace.Examples.Sessions.CSharp -{ - /// - /// Main form of the application - /// - public partial class MainForm : Form - { - private static MainForm main; - - private MainForm() - { - this.InitializeComponent(); - string connectionString = ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString; - this.connectionStringTextBox.Text = connectionString; - this.EnableAllContextMenu(); - } - - /// - /// Main form is a singleton object - /// - /// Singleton instance of the Main form - public static MainForm Main() - { - return main ?? (main = new MainForm()); - } - - /// - /// Method for clicking the CreateSession button - /// - /// Sender object - /// Event arguments - private void OnCreateSessionClick(object sender, EventArgs e) - { - this.resultMessage.Text = " "; - this.dataGrid.Rows.Clear(); - this.display.Series[0].Points.Clear(); - this.DisableAllButtons(); - - // Call the Create Session example method to create session and add data - var sessionCreationExample = new SessionCreationExample(this.connectionStringTextBox.Text); - var key = sessionCreationExample.CreateSessionAndAddData(); - this.resultMessage.Text = key != SessionKey.Empty ? - string.Format("\nSession with the Key {0} created successfully.", key) - : "\nError creating the session"; - - this.EnableAllButtons(); - } - - /// - /// Method for clicking the Load session button - /// - /// Sender object - /// Event arguments - private void OnLoadSessionClick(object sender, EventArgs e) - { - this.resultMessage.Text = string.Empty; - this.dataGrid.Rows.Clear(); - this.dataGrid.Columns.Clear(); - this.display.Series[0].Points.Clear(); - this.DisableAllButtons(); - - // Call the example method which creates a session and adds data and then loads the session - var loadSessionExample = new LoadSessionExample(this.connectionStringTextBox.Text); - var message = loadSessionExample.LoadSessionAndReadData(); - if (message != null) - { - this.resultMessage.Text = message; - this.display.Series[0].Points.DataBindXY(ConvertTimeStampToDateTime(loadSessionExample.TimeStamps), loadSessionExample.Data); - this.dataGrid.Columns.Add("timeStamp", "Time stamp"); - this.dataGrid.Columns.Add("data", "Samples"); - this.dataGrid.Columns.Add("status", "Data Status"); - for (int i = 0; i < loadSessionExample.Data.Length; i++) - { - this.dataGrid.Rows.Add(new object[] - { - loadSessionExample.TimeStamps[i], loadSessionExample.Data[i], - loadSessionExample.DataStatus[i] - }); - } - } - else - { - this.resultMessage.Text = "\nError loading the session"; - } - - this.EnableAllButtons(); - } - - /// - /// Method for clicking the Find All sessions example button - /// - /// Sender object - /// Event arguments - private void OnFindAllSessionsClick(object sender, EventArgs e) - { - this.resultMessage.Text = string.Empty; - this.dataGrid.Rows.Clear(); - this.dataGrid.Columns.Clear(); - this.display.Series[0].Points.Clear(); - this.DisableAllButtons(); - - // Call the example method which creates a session and finds all the sessions in the database - var findAllSessionsExample = new FindAllSessionsExample(this.connectionStringTextBox.Text); - var message = findAllSessionsExample.FindAllSessionsInDataBase(); - this.resultMessage.Text = message ?? "Error finding all the sessions in the Database"; - - this.EnableAllButtons(); - } - - /// - /// Method for clicking the Find session by Items button - /// - /// Sender object - /// Event arguments - private void OnFindByItemsClick(object sender, EventArgs e) - { - this.resultMessage.Text = string.Empty; - this.dataGrid.Rows.Clear(); - this.dataGrid.Columns.Clear(); - this.display.Series[0].Points.Clear(); - this.DisableAllButtons(); - - // Call the example method which creates a session and adds items and then searches for the created session - var findSessionsByItems = new SearchSessionsByItemExample(this.connectionStringTextBox.Text); - string message = findSessionsByItems.FindSessionsByItems(); - this.resultMessage.Text = message ?? "Error finding the sessions by Items"; - - this.EnableAllButtons(); - } - - /// - /// Enables all buttons in the form - /// - private void EnableAllButtons() - { - this.loadSession.Enabled = true; - this.createSession.Enabled = true; - this.findAllSessions.Enabled = true; - this.findByItems.Enabled = true; - } - - /// - /// Disables all the buttons while running the examples - /// - private void DisableAllButtons() - { - this.loadSession.Enabled = false; - this.createSession.Enabled = false; - this.findAllSessions.Enabled = false; - this.findByItems.Enabled = false; - } - - private void EnableAllContextMenu() - { - this.createSessionHelp.Items[0].Text = this.helpToolTip.GetToolTip(this.createSession); - this.loadSessionHelp.Items[0].Text = this.helpToolTip.GetToolTip(this.loadSession); - this.FindAllSessionsHelp.Items[0].Text = this.helpToolTip.GetToolTip(this.findAllSessions); - this.findByItemHelp.Items[0].Text = this.helpToolTip.GetToolTip(this.findByItems); - } - - /// - /// Convert the time stamp to date time string for graphical display - /// - /// Array of long time stamps - /// Converted data time strings - private static IEnumerable ConvertTimeStampToDateTime(IEnumerable timeStamps) - { - var dates = new List(); - foreach (long timeStamp in timeStamps) - { - var hr = Convert.ToInt32(timeStamp / 3600000000000); - var min = Convert.ToInt32((timeStamp - (hr * 3600000000000)) / 60000000000); - var sec = (timeStamp - ((hr * 3600000000000) + (min * 60000000000))) / (double)1000000000; - var time = DateTime.Parse(string.Format("{0}:{1}:{2}", hr, min, sec)); - dates.Add(time.ToString("hh:mm:ss.fff")); - } - - return dates; - } - } -} \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.resx b/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.resx deleted file mode 100644 index 6677238..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/MainForm.resx +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 212, 17 - - - 103, 17 - - - Creates a session in the database and adds data for up to 2 minutes. The data includes 3 periodic, 1 SlowRow and 1 synchro channel. Also added are the SessionDataItems, Events, Laps, LapConstats, Session Constants - - - 354, 17 - - - 491, 15 - - - 725, 13 - - - -1. Enter the database connection string for SQL Race database in the box above. - -eg:- Data Source=localhost\sqlexpress;Initial Catalog=SQLRACE01;Integrated Security=True - -2. Select a sample from the list of buttons below. Help on each of the samples can be found by hovering over or by right clicking on each button. - - \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/Program.cs b/MESL.SqlRace.Examples.Sessions.CSharp/Program.cs deleted file mode 100644 index 7c22d50..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/Program.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd - -using System; -using System.Windows.Forms; - -namespace MESL.SqlRace.Examples.Sessions.CSharp -{ - /// - /// Main entry point of the application - /// - public static class Program - { - /// - /// The main entry point for the application. - /// - [STAThread] - public static void Main() - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(MainForm.Main()); - } - } -} \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Resources.Designer.cs b/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Resources.Designer.cs deleted file mode 100644 index 5118457..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace MESL.SqlRace.Examples.Sessions.CSharp.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MESL.SqlRace.Examples.Sessions.CSharp.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Resources.resx b/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Resources.resx deleted file mode 100644 index af7dbeb..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Settings.Designer.cs b/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Settings.Designer.cs deleted file mode 100644 index db5837b..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace MESL.SqlRace.Examples.Sessions.CSharp.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Settings.settings b/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Settings.settings deleted file mode 100644 index 3964565..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/SearchSessionsByItemExample.cs b/MESL.SqlRace.Examples.Sessions.CSharp/SearchSessionsByItemExample.cs deleted file mode 100644 index 203501c..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/SearchSessionsByItemExample.cs +++ /dev/null @@ -1,92 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd - -using System; -using System.Collections.Generic; - -using MAT.OCS.Core; - -using MESL.SqlRace.Domain; - -namespace MESL.SqlRace.Examples.Sessions.CSharp -{ - /// - /// A Class demonstrating searching of SQL Race database based on SessionDataItems, without loading the session. - /// - public class SearchSessionsByItemExample - { - private readonly SessionManager sessionManager; - private readonly string connectionString; - - /// - /// Initialises a new instance of the class. - /// - /// Connection string - public SearchSessionsByItemExample(string connectionString) - { - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - this.sessionManager = SessionManager.CreateSessionManager(); - this.connectionString = connectionString; - } - - /// - /// An example demonstrating how to find particular session in the Database based on session detail/item. - /// - /// String value, an output of the example run - public string FindSessionsByItems() - { - try - { - // Create a session and add data first - var sessionCreation = new SessionCreationExample(this.connectionString); - sessionCreation.CreateSessionAndAddData(); - var clientSession = this.sessionManager.CreateSession(this.connectionString, SessionKey.NewKey(), "NewSession", DateTime.Now, "TAG-310"); - var session = clientSession.Session; - - session.Items.Add(new SessionDataItem("Driver Name", "xxxxxxxxxx")); - session.Items.Add(new SessionDataItem("Driver Height", 170.6)); - clientSession.Close(); - MainForm mainForm = MainForm.Main(); - - // Search for only sessions with particular session items - var items = new List - { - new SessionDataItem("Driver Name", "xxxxxxxxxx"), - new SessionDataItem("Driver Height", 170.6) - }; - IList sessions = this.sessionManager.FindBySessionItems(items, this.connectionString); - if (sessions.Count > 0) - { - mainForm.dataGrid.Columns.Add("guidp", "Session GUID"); - mainForm.dataGrid.Columns.Add("SessionItem1", "SessionItem1"); - mainForm.dataGrid.Columns.Add("SessionItem2", "SessionItem2"); - mainForm.dataGrid.Columns.Add("startTime", "Start timestamp"); - mainForm.dataGrid.Columns.Add("endTime", "End timestamp"); - mainForm.dataGrid.Columns.Add("laps", "Laps"); - mainForm.dataGrid.Columns.Add("dateOfRecording", "Date of recording"); - foreach (SessionSummary sessionSummary in sessions) - { - // Session summary object has properties such as Items, Start/End time, Laps etc. - mainForm.dataGrid.Rows.Add(new object[] - { - sessionSummary.Key, - string.Format("{0} = {1}", sessionSummary.Items[0].Name, sessionSummary.Items[0].Value), - string.Format("{0} = {1}", sessionSummary.Items[1].Name, sessionSummary.Items[1].Value), - sessionSummary.StartTime, - sessionSummary.EndTime, - sessionSummary.Laps.Count, - sessionSummary.TimeOfRecording, - }); - } - } - - return string.Format("The given Session details found {0} sessions in the Database", sessions.Count); - } - catch (Exception) - { - return null; - } - } - } -} \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/SessionCreationExample.cs b/MESL.SqlRace.Examples.Sessions.CSharp/SessionCreationExample.cs deleted file mode 100644 index 572c2bb..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/SessionCreationExample.cs +++ /dev/null @@ -1,223 +0,0 @@ -// -// Copyright (c) Motion Applied Ltd - -using System; -using System.Collections.Generic; -using System.Linq; - -using MAT.OCS.Core; - -using MESL.SqlRace.Domain; -using MESL.SqlRace.Enumerators; - -namespace MESL.SqlRace.Examples.Sessions.CSharp -{ - /// - /// A class for demonstrating Session creation examples in SQL Race - /// - public class SessionCreationExample - { - private readonly SessionManager sessionManager; - private readonly string connectionString; - - /// - /// Initialises a new instance of the class. - /// - /// Connection string - public SessionCreationExample(string connectionString) - { - Core.LicenceProgramName = "SQLRace"; - Core.Initialize(); - this.sessionManager = SessionManager.CreateSessionManager(); - this.connectionString = connectionString; - } - - /// - /// Creates a session and adds data. - /// - /// Guid of the session - public SessionKey CreateSessionAndAddData() - { - long startTime = ConvertDateTimeToNanoseconds(DateTime.Now); - long endTime = ConvertDateTimeToNanoseconds(DateTime.Now.AddMinutes(2)); - SessionKey sessionKey = SessionKey.NewKey(); - DateTime date = DateTime.Now; - string sessionDescription = string.Format("Example::: {0}", date.ToString("dd-MMM-yy hh:mm:ss tt")); - var random = new Random(); - - // Create a session first - var clientSession = this.sessionManager.CreateSession(this.connectionString, sessionKey, sessionDescription, date, "TAG-310"); - var session = clientSession.Session; - - // Add some session details which allows values as String, Long, Double, Bool, Datetime, Byte[] etc. - session.Items.Add(new SessionDataItem("Driver Name", "Driver xxxxx")); - session.Items.Add(new SessionDataItem("Race", "Silverstone GP")); - session.Items.Add(new SessionDataItem("Date", DateTime.Now.AddDays(7))); - session.Items.Add(new SessionDataItem("Fuel level in gallons", 56.45)); - - // Add conversions - Rational and table - var config = session.CreateConfiguration(); - var conversions = new List - { - new RationalConversion("Rational_conv", "mph", "%5.2f", 0.0, 1.0, 0.0, 0.0, 0.0, 1.0) - }; - - var rawValues = new double[] { 0, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95 }; - var calibratedValues = new double[] { 50, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145 }; - conversions.Add(new TableConversion("Table_conv", "mph", "%5.2f", rawValues, calibratedValues, false)); - config.AddConversions(conversions); - - // Add Application and Parameter groups - var parameterGroups = new List - { - new ParameterGroup("chassis", "Chassis group"), - new ParameterGroup("engine", "Engine group"), - new ParameterGroup("bios", "Bios group") - }; - var applicationGroup = new ApplicationGroup("SQLRace App Group", "SQLRace App Group", parameterGroups.Select(x => x.Identifier).ToList()); - config.AddGroup(applicationGroup); - config.AddParameterGroups(parameterGroups); - - // Add Parameters - Periodic, SlowRow and Synchro - var channel1 = new Channel(1, "lowFrequencyPeriodic1", 100000000, DataType.FloatingPoint32Bit, ChannelDataSourceType.Periodic); - var channel2 = new Channel(2, "highFrequencyPeriodic", 1000000, DataType.FloatingPoint32Bit, ChannelDataSourceType.Periodic); - var channel3 = new Channel(3, "lowFrequencyPeriodic2", 50000000, DataType.FloatingPoint32Bit, ChannelDataSourceType.Periodic); - var channel4 = new Channel(100, "synchroChannel", 0, DataType.Unsigned8Bit, ChannelDataSourceType.Synchro); - var channel5 = new Channel(1000, "SlowRowDataChannel", 0, DataType.Unsigned8Bit, ChannelDataSourceType.RowData); - var parameters = new List - { - new Parameter("lowFrequency_10Hz", "lowFrequency_10Hz", "Low frequency parameter", 200, 0, 200, 0, 0, 0xffffffff, 0, conversions[0].Name, new List { "chassis" }, channel1.Id), - new Parameter("highFrequency_1KHz", "highFrequency_1KHz", "High frequency parameter", 200, 0, 200, 0, 0, 0xffffffff, 0, conversions[0].Name, new List { "chassis" }, channel2.Id), - new Parameter("lowFrequency_20Hz", "lowFrequency_20Hz", "parameter for Table conversion", 200, 0, 200, 0, 0, 0xffffffff, 0, conversions[1].Name, new List { "chassis" }, channel3.Id), - new Parameter("synchroData", "synchroData", "Synchro data parameter", 200, 0, 200, 0, 0, 0xff, 0, conversions[0].Name, new List { "engine" }, channel4.Id), - new Parameter("slowRowData", "slowRowData", "slow Row Data parameter", 200, 0, 200, 0, 0, 0xff, 0, conversions[0].Name, new List { "bios" }, channel5.Id) - }; - - config.AddParameters(parameters); - config.AddChannels(new List { channel1, channel2, channel3, channel4, channel5 }); - - // Add event definitions - var eventDefinitions = new List - { - new EventDefinition( - 1, - "FirstEvent", - EventPriorityType.High, - new List - { - conversions[0].Name, - conversions[0].Name, - conversions[0].Name - }, - "Group1"), - new EventDefinition( - 2, - "SecondEvent", - EventPriorityType.Low, - new List - { - conversions[0].Name, - conversions[0].Name, - conversions[0].Name - }, - "Group1"), - new EventDefinition( - 3, - "LastEvent", - EventPriorityType.Medium, - new List - { - conversions[0].Name, - conversions[0].Name, - conversions[0].Name - }, - "Group2") - }; - config.AddEventDefinitions(eventDefinitions); - config.Commit(); - - // Each samples will be seperated by 250*256 + 100 = (64100 * delta scale) nano secs. - var synchroBurst = new byte[] { 11, 100, 250, 22, 100, 250, 33, 100, 250, 44, 100, 250, 55, 100, 250, 66, 100, 250, 77, 100, 250, 88, 100, 250, 99 }; - var doubleData = new[] { 0, 10.2, 15.45, 20, 25.82, 30, 35, 40, 45.01, 50.99, 55.7, 60.4, 65, 70, 75, 80, 85.67, 90.77, 95 }; - var doubleDataBytes = new byte[doubleData.Length * 4]; - var index = 0; - - // Convert double array to byte array - foreach (var byteArray in doubleData.Select(data => BitConverter.GetBytes(Convert.ToSingle(data)))) - { - Buffer.BlockCopy(byteArray, 0, doubleDataBytes, index, 4); - index += 4; - } - - foreach (var parameter in session.Parameters.OfType()) - { - var currentTime = startTime; - var channel = parameter.Channels[0]; - byte counter = 0; - - while (currentTime < endTime) - { - switch (channel.DataSource) - { - case ChannelDataSourceType.Periodic: - session.AddChannelData(channel.Id, currentTime, doubleData.Length, doubleDataBytes); - currentTime += channel.Interval * doubleData.Length; - break; - case ChannelDataSourceType.Synchro: - session.AddSynchroChannelData(currentTime, channel.Id, counter++, 2000, synchroBurst); - currentTime += 1000000000; - break; - case ChannelDataSourceType.RowData: - session.AddRowData(currentTime, new List { channel.Id }, new[] { (byte)random.Next(0, 200) }); - currentTime += 5000000000; - break; - } - } - } - - // Add Laps - var time = 20000000000; - var timeStamp = startTime; - for (var i = 0; i < 5; i++) - { - timeStamp += time + (i * 100000000 * random.Next(1, 500)); - session.LapCollection.Add(new Lap(timeStamp, Convert.ToInt16(i + 1), 0, string.Format("Lap{0}", i + 1), true)); - } - - // Add event instances - time = 10000000000; - timeStamp = startTime; - for (var i = 0; i < 5; i++) - { - timeStamp += time + (i * 100000000 * random.Next(1, 500)); - session.Events.AddEventData(1, "Group1", timeStamp, new List { 300, 200, 100 }, true); - session.Events.AddEventData(2, "Group1", timeStamp + 100000000, new List { 243, 14, 5 }, true); - session.Events.AddEventData(3, "Group2", timeStamp + 200000000, new List { 111, 0, 3456 }, true); - } - - // Add LapItems, Session Constants and LapConstants - session.LapCollection.LapItems.Add(new LapDataItem(5, "Information", "Pit Stop in next Lap")); - session.Constants.Add(new Constant("Race Start car weight", 653.92, "Car weight at the start of the race", "kg", "%5.2f")); - session.LapCollection.Constants.Add(new LapConstant(4, "FuelConsumed", 4.5, "Fuel consumed for this lap", "ltr", "%4.2f")); - clientSession.Close(); - - - return sessionKey; - } - - /// - /// Converts a DateTime to nanoseconds - /// - /// DateTime object - /// Time in nanoseconds - private static long ConvertDateTimeToNanoseconds(DateTime time) - { - int hours = time.Hour * 3600000; - int minutes = time.Minute * 60000; - int seconds = time.Second * 1000; - int milliseconds = time.Millisecond; - string nanosecondString = Convert.ToString(milliseconds + seconds + minutes + hours) + "000000"; - return Convert.ToInt64(nanosecondString); - } - } -} \ No newline at end of file diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/System.Windows.Forms.DataVisualization.dll b/MESL.SqlRace.Examples.Sessions.CSharp/System.Windows.Forms.DataVisualization.dll deleted file mode 100644 index 7650cf325e03e8d34ffca0ee3c58b97074dcfa9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1753088 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

L=B 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

-{b^6EF^DU{ohQT$&wos#_Y{SV9NI9{H(S2 zkEI@WS7Iai*XV@-ZEcOr{9Vjz3y!AK*=9*9TFk7iP?=8P2%TqU)}9KX{d5Rrm*QlU zLI|BlW!A2j6qk`=2tuV0;&(9LIz)@)UpkaoyN=kfz>zMFdC^r~I$SEwvl-9cxdma^ z3E@|Q0Af%&lZ7sR+spO+ zqlJ3+8{BCkNkR{^^%h)t3Kv8{I@7FB!ZK&_>M&>r#Z%R-^dgauskGOV{$N5u%}Ap}fy>4s%1R{NP58(dbhr&`D_-i(*PJ*iLR z>s>5>ms_hnkLaB7*oG#o6zYDQ4pAKsw-?H-^R0>6W0rQPNLne^?Zt9yI%DIyVR1;7 zGv31`Ok+!Z1Z$2J{kW~_mHf|McQZ9i54mQ;R3gnbTr6O|8;(lsWxuMYZSE@4@WR{O zMP7CC8pBWIlj#M@3TJe<%vX(c>IOP{xwYRy%A2<*Es2rTPuZy(HC3fK$$rno^SY

x2@@elWwgKRtN1-}n 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

4C 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 glU(iX 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

=>3P$4ratRtqa0(p(lpx?t(W!N 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 diff --git a/MESL.SqlRace.Examples.Sessions.CSharp/app.config b/MESL.SqlRace.Examples.Sessions.CSharp/app.config deleted file mode 100644 index b121056..0000000 --- a/MESL.SqlRace.Examples.Sessions.CSharp/app.config +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Python/Readme.txt b/Python/Readme.txt deleted file mode 100644 index e64e50f..0000000 --- a/Python/Readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -The program only has one python dependency called 'pythonnet' which is the library that allows us to call dotnet assemblies directly from python. It will also require that Atlas 10 is installed in the default location and will need to be run using the 64-bit version of Python. - -It shows how to use filters to find sessions that you are interested in, load those sessions, position the read cursor to a desired lap in the session and the extract certain parameters. It is setup initially to load 20 samples from the start of the second lap for vCar and gLat for each race session in a specified date range. - diff --git a/Python/main.py b/Python/main.py deleted file mode 100644 index fdbccbf..0000000 --- a/Python/main.py +++ /dev/null @@ -1,235 +0,0 @@ -import os -import time -import subprocess -import threading - -from pythonnet import load - -load("coreclr", runtime_config=r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MAT.Atlas.Host.runtimeconfig.json") - -import clr # Pythonnet - -# The path to the main SQL Race DLL. This is the default location when installed with Atlas 10 -sql_race_dll_path = r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll" -# The paths to Automation API DLL files. -automation_api_dll_path = r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MAT.Atlas.Automation.Api.dll" -automation_client_dll_path = r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MAT.Atlas.Automation.Client.dll" -# The connection string to the database containing our sessions -connection_string = r"Server=CLU-LDF01\LOCAL;Initial Catalog=SQLRACE01_2019;Trusted_Connection=True;" - -# Configure Pythonnet and reference the required assemblies for dotnet and SQL Race -clr.AddReference("System.Collections") -clr.AddReference("System.Core") -clr.AddReference("System.IO") - -if not os.path.isfile(sql_race_dll_path): - raise Exception("Couldn't find SQL Race DLL at " + sql_race_dll_path + " please check that Atlas 10 is installed") - -clr.AddReference(sql_race_dll_path) - -if not os.path.isfile(automation_api_dll_path): - raise Exception(f"Couldn't find Automation API DLL at {automation_api_dll_path}.") - -clr.AddReference(automation_api_dll_path) - -if not os.path.isfile(automation_client_dll_path): - raise Exception(f"Couldn't find Automation Client DLL at {automation_client_dll_path}.") - -clr.AddReference(automation_client_dll_path) - - -from System.Linq import Enumerable -from MESL.SqlRace.Domain import * -from MESL.SqlRace.Domain.Query import * -from MESL.SqlRace.Domain.Sort import * -from MESL.SqlRace.Domain.Infrastructure.Enumerators import * -from MAT.Atlas.Automation.Client.Services import * -from MAT.Atlas.Automation.Api.Models import * -from System.IO import Path -from System import AppDomain - -keys = [] - -def initialise_sql_race(): - # Tell SQL Race which part of our licence we are using then initialise it - print('Initialising SQL Race...') - Core.LicenceProgramName = 'SQLRace' - Core.Initialize() - -def extract_data(sessionSummary): - # The SessionManager is used to load the sessions - sessionManager = SessionManager.CreateSessionManager() - - # This is the point at which the session is actually loaded. Note that if the Key of the session is - # already known (e.g. by inspection through the Session Browser in A10) then the previous stage of - # querying the database is not required. - print("Loading session " + sessionSummary.Identifier) - clientSession = sessionManager.Load(sessionSummary.Key, sessionSummary.GetConnectionString()) - session = clientSession.Session - - # Load the configuration associated with the session to decode the parameters, events etc. - session.LoadConfiguration() - - # A list of parameters that we are going to load in turn - parameters = ["vCar:Chassis", "gLat:Chassis"] - - for parameter in parameters: - # Create a ParameterDataAccess object to extract data for this parameter from the session - pda = session.CreateParameterDataAccess(parameter) - - # Go to the start of the second lap - pda.GoTo(session.Laps.get_Item(1).StartTime) - - # Get the next n samples - samples = pda.GetNextSamples(20, StepDirection.Forward) - - # Print the samples that we have just extracted to the console - print(parameter) - for i in range(samples.SampleCount): - print(f"{samples.Timestamp[i]} {samples.Data[i]}") - - # Release resources from the PDA - pda.Dispose() - # Release the resources from the Session. Failure to do this could result in a memory leak. - clientSession.Dispose() - -def get_event_details(sessionSummary): - # Dictionary to keep event definitions alogn with event definition id - conversions = {} - - # The SessionManager is used to load the sessions - sessionManager = SessionManager.CreateSessionManager() - - # This is the point at which the session is actually loaded. Note that if the Key of the session is - # already known (e.g. by inspection through the Session Browser in A10) then the previous stage of - # querying the database is not required. - print("Loading session " + sessionSummary.Identifier) - clientSession = sessionManager.Load(sessionSummary.Key, sessionSummary.GetConnectionString()) - session = clientSession.Session - - # Creating an event definitions dictionary - definitions = session.EventDefinitions - for definition in definitions: - conversions.update({definition.EventDefinitionId : definition.Description}) - - # Getting event data from events collection since session start till the session end - eventCollection = session.Events - events = eventCollection.GetEventData(session.StartTime, session.EndTime) - - # Iterating through the first 50 session events and printing details - for event in events[:50]: - # Getting an event description using event definition key from conversion dictionary - eventDescription = conversions.get(event.EventDefinitionKey) - # Getting event status text from event collection - statusText = eventCollection.GetDisplayText(event) - print(f'{event.TimeStamp}: {event.Key} | {event.GroupName} | {eventDescription} | {statusText}') - - - clientSession.Dispose() - - -def find_sessions(): - print("Finding sessions...") - # Create a filter to find all sessions within a given date range - # - # Available SessionFieldIdentifiers to filter on are StartTime, EndTime, LapCount, FastestLapTime, SessionIdentifier, - # SessionType, SessionState, SessionStartDateTime, SessionKey - # - # CompositeFilters can be combined hierarchically with other CompositeFilters to allow complex queries to be created - filter = CompositeFilter(CombineType.AND) - filter.Add(ScalarFilter(SessionFieldIdentifiers.SessionStartDateTime, MatchingRule.GreaterThanOrEquals, "2019-08-04 00:00:00", True)) - filter.Add(ScalarFilter(SessionFieldIdentifiers.SessionStartDateTime, MatchingRule.LessThan, "2019-08-05 00:00:00", True)) - - # Execute the query against the database. Note that this returns SessionSummary objects which just contain session metadata, - # the session has not been loaded at this point. - qm = QueryManager.CreateQueryManager(connection_string) - qm.Filter = filter - queryResult = qm.ExecuteQuery() - - # Process each of the resulting sessions in turn - for sessionSummary in Enumerable.ToList[SessionSummary](queryResult): - print("Found session " + sessionSummary.Identifier) - - # Adding found session key to the list of sessions to be loaded using API - keys.append(sessionSummary.Key.ToString("D")) - - # Skip this session if it doesn't have a second lap - if sessionSummary.Laps.Count < 2: - continue - - extract_data(sessionSummary) - get_event_details(sessionSummary) - -def set_cursor(keys, connection_strings, timestamp): - # Function to set cursor in ATLAS with loaded session to specific timestamp - - app = ApplicationServiceClient() - workbookServiceClient = WorkbookServiceClient() - setServiceClient = SetServiceClient() - - # Open ATLAS10 from default installation location - subprocess.Popen(r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MAT.ATLAS.Host.exe") - print("Waiting for ATLAS to open") - - # set up multiprocessing.Lock to be use to block until client is connected. - connect = threading.Lock() - connect.acquire() - - def client_connected(client_name): - # event handler for OnClientConnected - print('\nATLAS Client connected.') - connect.release() - - app.OnClientConnected += client_connected - app.Connect(Path.GetFileNameWithoutExtension(AppDomain.CurrentDomain.FriendlyName)); - print(f'ATLAS version: {app.GetVersion()}') - - # Wait until client is connected. The next line cannot run until connect is released by the handeler function. - connect.acquire() - connect.release() - - - setsList = workbookServiceClient.GetSets() - - load = threading.Lock() - load.acquire() - - def session_loaded(session_loaded): - # event handeler for OnSessionLoaded - print('Session loaded.') - load.release() - app.OnSessionLoaded += session_loaded - - # Loading session from SQL Race into specified set, using session keys found in SQLRace API and connection string to SQL Race - print('\nLoading session...') - app.LoadSqlRaceSessions(setsList[0].Id, keys, connection_strings) - - # wait for session to load - load.acquire() - load.release() - - # Getting through the APIs to get the already loaded session ID - sessionServiceClient = SessionServiceClient() - sessions = setServiceClient.GetCompositeSessions(setsList[0].Id) - session_id = sessions[0].Id - - - # Getting current session timebase for session ID - timebase = sessionServiceClient.GetSessionTimeBase(session_id) - timeBaseService = TimeBaseServiceClient(timebase.Id) - - # Setting cursor in A10 to specific timestamp - timeBaseService.SetCursor(timebase.Id, timestamp) - -def main(): - connection_strings = [] - # Building connection strings list for LoadingSQLRaceSession - connection_strings.append(connection_string) - - initialise_sql_race() - find_sessions() - - set_cursor(keys, connection_strings, 47718380000000) - - -if __name__ == "__main__": main() diff --git a/docs/connection-strings.md b/docs/connection-strings.md new file mode 100644 index 0000000..251fe40 --- /dev/null +++ b/docs/connection-strings.md @@ -0,0 +1,101 @@ +# Connection Strings + +SQL Race supports two database backends: **SQLite** (local files) and **SQL Server** (network databases). This guide covers the connection string format, configuration options, and best practices. + +## SQLite (Local File) + +SQLite connections use a local `.ssn2` file. No server required. + +``` +DbEngine=SQLite;Data Source=C:\data\my-session.ssn2; +``` + +### Default for examples + +All snippets in this repository default to a temporary file that works without any configuration: + +```csharp +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +``` + +This creates `sqlrace-examples.ssn2` in your system's temp directory (e.g., `%TEMP%` on Windows, `/tmp` on Linux/macOS). + +### Cross-platform paths + +```csharp +// Always use Path.Combine for cross-platform compatibility +var dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyApp", "data.ssn2"); +var connectionString = $"DbEngine=SQLite;Data Source={dbPath};"; +``` + +## SQL Server + +SQL Server connections require a running SQL Server instance with the SQL Race schema installed. + +``` +Server=myserver\SQLEXPRESS;Database=SQLRACE01;Trusted_Security=True; +``` + +### Common variations + +``` +# Windows authentication +Server=myserver;Database=SQLRACE01;Trusted_Security=True; + +# SQL authentication +Server=myserver;Database=SQLRACE01;User Id=myuser;Password=mypassword; + +# Named instance +Server=myserver\INSTANCENAME;Database=SQLRACE01;Trusted_Security=True; + +# Custom port +Server=myserver,1434;Database=SQLRACE01;Trusted_Security=True; +``` + +## Configuration Hierarchy + +The Core library's `ConnectionStringProvider` resolves connection strings in this order: + +1. **Explicit parameter** — passed directly to the method +2. **Environment variable** — `SQLRACE_CONNECTION_STRING` +3. **appsettings.json** — under the key `SqlRace:ConnectionString` +4. **Default** — SQLite file in the temp directory + +### Environment variable + +```bash +# Set once for your shell session +export SQLRACE_CONNECTION_STRING="Server=myserver;Database=SQLRACE01;Trusted_Security=True;" +``` + +```powershell +# PowerShell +$env:SQLRACE_CONNECTION_STRING = "Server=myserver;Database=SQLRACE01;Trusted_Security=True;" +``` + +### appsettings.json + +Create an `appsettings.json` in your project root: + +```json +{ + "SqlRace": { + "ConnectionString": "DbEngine=SQLite;Data Source=C:\\data\\my-session.ssn2;" + } +} +``` + +Make sure it's copied to the output directory: + +```xml + + + +``` + +## Tips + +- **Start with SQLite.** It requires no installation, no configuration, and works everywhere. Move to SQL Server when you need multi-user access or live data. +- **Never hard-code server names.** Use environment variables or `appsettings.json` so your code works across environments. +- **Use the Core library's `ConnectionStringProvider`** in projects to get the configuration hierarchy for free. +- **File sessions (`.ssn2`) are single-writer.** Only one process can write to a file session at a time. Use SQL Server for concurrent access. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..a00c115 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,123 @@ +# Getting Started + +This guide walks you through setting up your development environment to run the SQL Race API examples. + +## Prerequisites + +### .NET 8 SDK + +Download and install the [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0). + +Verify your installation: + +```bash +dotnet --version +# Should output 8.0.x +``` + +### NuGet Package Source + +The SQL Race API is distributed via GitHub Packages. Add the feed to your NuGet configuration: + +```bash +dotnet nuget add source https://nuget.pkg.github.com/mat-docs/index.json \ + --name mat-docs \ + --username YOUR_GITHUB_USERNAME \ + --password YOUR_GITHUB_PAT +``` + +You need a GitHub Personal Access Token (PAT) with `read:packages` scope. [Create one here](https://github.com/settings/tokens/new?scopes=read:packages). + +Alternatively, add the source in your `nuget.config`: + +```xml + + + + + + + + + + + + +``` + +### ATLAS (Optional) + +ATLAS is the desktop application for viewing and analysing SQL Race sessions. It is required only for: + +- Live data scenarios (server listener, live dashboard) +- Viewing `.ssn2` files with full visualisation +- Server-connected database sessions + +For getting-started and offline examples, ATLAS is **not required**. + +## First Run + +### Option 1: Run a snippet directly + +```bash +# Create a new console project +dotnet new console -n QuickStart +cd QuickStart + +# Add the SQL Race package +dotnet add package MESL.SQLRace.API + +# Copy a snippet into Program.cs +cp ../snippets/csharp/getting-started/04-create-session-write-data.cs Program.cs + +# Run it +dotnet run +``` + +This will create a local SQLite session file, write sample data, and read it back — no server or configuration needed. + +### Option 2: Run the GettingStarted project + +```bash +cd projects/csharp +dotnet run --project GettingStarted +``` + +This uses the shared Core library for a cleaner API experience. + +## Project Structure for Your Own Code + +When building your own application with SQL Race, a typical project structure looks like: + +```xml + + + Exe + net8.0 + enable + + + + + +``` + +## Initialisation Pattern + +Every application using SQL Race must initialise the runtime before any other API calls: + +```csharp +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); +``` + +This is a one-time call. The [Core library](../projects/csharp/SqlRace.Examples.Core/) provides `SqlRaceBootstrapper` to handle this safely with idempotency. + +## Next Steps + +- [Connection Strings](connection-strings.md) — learn about SQLite, SQL Server, and configuration options +- [Snippets](../snippets/) — browse single-file examples by topic +- [COOKBOOK.md](../COOKBOOK.md) — find examples by task +- [CONCEPTS.md](../CONCEPTS.md) — map SQL Race concepts to your industry's terminology diff --git a/docs/industry-mapping.md b/docs/industry-mapping.md new file mode 100644 index 0000000..d4c429f --- /dev/null +++ b/docs/industry-mapping.md @@ -0,0 +1,5 @@ +# Industry Mapping + +See [CONCEPTS.md](../CONCEPTS.md) for the complete cross-industry concept mapping table. + +This page will be expanded with detailed industry-specific guidance in a future update. diff --git a/docs/server-listener.md b/docs/server-listener.md new file mode 100644 index 0000000..0707114 --- /dev/null +++ b/docs/server-listener.md @@ -0,0 +1,130 @@ +# Server Listener Setup (Live Data) + +The Server Listener is the mechanism that enables **live data streaming** from a recording session to remote SQL Race clients. Without it, sessions appear as static snapshots — you can only see data after the recording is complete. + +This guide explains how to configure the Server Listener so that the live-data snippets and the LiveDashboard project work against actively recording sessions. + +## How It Works + +When a recording starts on the ADS (ATLAS Data Server), the Server Listener broadcasts session updates over the network. Remote clients connect to the ADS and receive data as it is recorded. + +``` +┌─────────────────┐ TCP/UDP ┌──────────────────┐ +│ ADS Machine │ ─────────────────────► │ ATLAS Client / │ +│ (recording to │ Server Listener port │ SQL Race API app │ +│ SQL Server DB) │ │ (reads live data)│ +└────────┬─────────┘ └──────────────────┘ + │ + │ writes to + ▼ +┌─────────────────┐ +│ SQL Server DB │ +│ (SQLRACE01) │ +└─────────────────┘ +``` + +Key points: + +- The ADS stores session metadata (including the Server Listener IP/port) in the database alongside the session data. +- The **client's** Server Listener settings do not matter — only the ADS settings are used since the client is not creating a session. +- The client reads from the **same SQL Server database** that the ADS is recording to. + +> **Note:** For local recording with a DST recorder, the recorder takes data from Multicast and creates a session locally — the local ATLAS 10 acts as both the Server Listener "server" and "client". The Server Listener setup described here is only needed for **remote** live data access. + +## What You Need + +You need **three components** (they can be on separate machines or on the same machine): + +| Component | Role | +|-----------|------| +| **ADS machine** | Runs the recorder, writes data to SQL Server, hosts the Server Listener | +| **SQL Server database** | Stores session data (must be accessible from both ADS and client) | +| **Client machine** | Runs your SQL Race API application (LiveDashboard, snippets, ATLAS, etc.) | + +## Step-by-Step Setup + +### 1. Configure the ADS Server Listener + +On the ADS machine: + +1. Open ATLAS and go to **Tools → SQL Race → Settings** +2. Navigate to the **Server Listener** section +3. Set the **Database connection** to the SQL Server database you will record to (e.g., `SQLRACE01`) +4. Tick **Enable server listener** +5. Set the **IP Address** to a network address that client machines can reach (this should match the Wide Band Address) +6. Set the **Port** — ATLAS uses `7831` by default +7. Click **OK** + +### 2. Configure the Recorder + +Still on the ADS machine: + +1. Go to **Setup** and open the recorder configuration +2. Set **Session Destination** to **SQL Race** +3. Set the **SQL Race Database** to the **same database** selected in the Server Listener settings + +> **Important:** The Server Listener database and the recorder database **must be the same**. If they differ, the client will not receive live data. + +### 3. Open Firewall Ports + +On the ADS machine, ensure the Server Listener port (e.g., `7831`) is open for **both TCP and UDP** inbound connections. + +On the client machine, ensure outbound connections to that port are allowed. + +```powershell +# Example: open port 7831 on the ADS machine (run as Administrator) +New-NetFirewallRule -DisplayName "ATLAS Server Listener" -Direction Inbound -Protocol TCP -LocalPort 7831 -Action Allow +New-NetFirewallRule -DisplayName "ATLAS Server Listener UDP" -Direction Inbound -Protocol UDP -LocalPort 7831 -Action Allow +``` + +### 4. Configure the Client + +On the client machine, add the SQL Server database (the one the ADS records to) to your indexed sources. In ATLAS, this is under **Tools → SQL Race → Settings → Connections**. + +For SQL Race API applications, you just need the connection string to the same database: + +```csharp +var connectionString = "Server=your-sql-server;Database=SQLRACE01;Trusted_Connection=True;"; +``` + +Or set it via environment variable: + +```powershell +$env:SQLRACE_CONNECTION_STRING = "Server=your-sql-server;Database=SQLRACE01;Trusted_Connection=True;" +``` + +## Starting a Live Recording + +1. Start the recorder on the ADS machine +2. On the client, check the session state — it should show as **Live**, not **LiveNotInServer** + - If it shows **LiveNotInServer**, check your Server Listener configuration (IP, port, database match, firewall) +3. Wait until the session reports at least **1 lap** before loading it in ATLAS +4. The session should behave like a live session — data updates as the ADS records + +## Verifying Live Data with the Tools + +Use the `FindLiveSession` tool to automatically detect active sessions: + +```powershell +dotnet run --project tools/FindLiveSession +``` + +This queries the database, loads recent sessions, and monitors for EndTime growth (which indicates active recording). If a live session is found, it prints the session key and ready-to-use snippet commands. + +## Troubleshooting + +| Symptom | Likely Cause | Fix | +|---------|-------------|-----| +| Session shows **LiveNotInServer** | Server Listener not enabled, or IP/port mismatch | Check ADS Server Listener settings — IP must be reachable from client | +| Session loads but data doesn't update | Server Listener database ≠ recorder database | Ensure both point to the same SQL Server database | +| Client can't connect | Firewall blocking the port | Open the Server Listener port (TCP+UDP) on both machines | +| `FindLiveSession` says "no live sessions" | No active recording, or data hasn't flushed yet | Start a recorder on the ADS; use `--wait 90` for slow flush intervals | +| `CreateParameterDataAccess` throws | Session loaded before parameters were flushed | Wait for the first data flush (~60–70s), then reload the session | +| Data values don't change between polls | Cached session data from initial load | Reload the session (`SessionManager.Load()`) each poll cycle — see `poll-live-samples.cs` | + +## Related + +- [snippets/csharp/live-data/](../snippets/csharp/live-data/) — Live data snippet examples +- [projects/csharp/LiveDashboard/](../projects/csharp/LiveDashboard/) — Real-time dashboard project +- [projects/csharp/ServerListenerRecorder/](../projects/csharp/ServerListenerRecorder/) — Example recorder using Server Listener +- [docs/connection-strings.md](connection-strings.md) — Database connection string reference diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..88861e3 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,127 @@ +# Troubleshooting + +Common issues and solutions when working with the SQL Race API. + +## "Could not load file or assembly 'MESL.SqlRace.Domain'" + +The SQL Race assemblies are not in the expected location. Check that: +- The `MESL.SQLRace.API` NuGet package is referenced in your project +- You have run `dotnet restore` +- The GitHub Packages NuGet source is configured (see [getting-started.md](getting-started.md)) + +## "LicenceProgramName must be set before Initialize" + +You must set `Core.LicenceProgramName` before calling `Core.Initialize()`: + +```csharp +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); +``` + +In MATLAB with .NET Core, direct static property assignment and static method calls do not work with short names. Use `NET.setStaticProperty` and `NET.invokeStaticMethod` instead: + +```matlab +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +NET.invokeStaticMethod('MESL.SqlRace.Domain.Core', 'Initialize'); + +sessionKey = NET.invokeStaticMethod('MESL.SqlRace.Domain.SessionKey', 'Parse', guid); +sessionManager = NET.invokeStaticMethod('MESL.SqlRace.Domain.SessionManager', 'CreateSessionManager'); +``` + +Note: `import MESL.SqlRace.Domain.*` does not reliably expose short names under .NET Core. Avoid it — use fully qualified names with `NET.invokeStaticMethod` for all static calls. Instance methods on returned objects (e.g. `session.CreateParameterDataAccess(...)`) work normally. + +## "Session not found" when loading from database + +- Verify the session key (GUID) is correct +- Check that your connection string points to the right database +- Ensure the database exists and contains sessions + +## SQLite "database is locked" + +SQLite files support only one writer at a time. Ensure no other process (including ATLAS) has the file open for writing. + +## MATLAB + .NET 8: Assembly resolution failures + +**Bug 90001** — When using SQL Race from MATLAB with .NET 8, assemblies such as `Microsoft.Win32.SystemEvents` or `System.Runtime` may fail to resolve. This is because MATLAB's default .NET runtime is .NET Framework, and even when switched to .NET Core the runtime config may not include all required frameworks. + +**Symptoms:** +- `Could not load file or assembly 'System.Runtime, Version=8.0.0.0'` — MATLAB is still using the .NET Framework runtime +- `Could not load file or assembly 'Microsoft.Win32.SystemEvents'` during `Core.Initialize()` or session load — the .NET Core runtime config is missing the `Microsoft.WindowsDesktop.App` framework + +### Step 1: Switch MATLAB to .NET Core + +Requires **MATLAB R2023a or later** (which supports .NET 6+). Run once per MATLAB installation: + +```matlab +dotnetenv('core') +``` + +If you have .NET runtimes newer than .NET 8 installed, pin to version 8: + +```matlab +dotnetenv("core", Version="8"); +``` + +Verify with: + +```matlab +e = dotnetenv; +disp(e) +``` + +### Step 2: Configure the runtime to include required frameworks + +Edit the runtime configuration file at: + +``` +/bin/win64/dotnetcli_netcore.runtimeconfig.json +``` + +Replace its contents with (or merge into your existing config): + +```json +{ + "runtimeOptions": { + "rollForward": "Minor", + "tfm": "net8.0", + "frameworks": [ + { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + }, + { + "name": "Microsoft.WindowsDesktop.App", + "version": "8.0.0" + }, + { + "name": "Microsoft.AspNetCore.App", + "version": "8.0.0" + } + ], + "configProperties": { + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": true + } + } +} +``` + +The key entry is **`Microsoft.WindowsDesktop.App`** — this framework contains `Microsoft.Win32.SystemEvents` and other Windows desktop assemblies that SQL Race depends on (via `MetricsService → MidnightNotifier`). With this config in place, MATLAB loads the correct assemblies automatically. + +### Fallback: Pre-load individual DLLs + +If you cannot modify the runtime config, explicitly load `Microsoft.Win32.SystemEvents.dll` before initialising SQL Race: + +```matlab +installDir = fileparts(sqlraceDll); +NET.addAssembly(fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll')); +``` + +All MATLAB snippets and the `setup_sqlrace.m` helper in this repository include this fallback. + +See the [MATLAB documentation on calling .NET](https://mathworks.com/help/matlab/calling-net-from-matlab.html) for more details on .NET interop configuration. + +## Timestamps seem wrong + +SQL Race timestamps are **nanoseconds since midnight**, not Unix timestamps. A value of `36000000000000` means 10:00:00.000 (10 hours after midnight). + +See [CONCEPTS.md](../CONCEPTS.md#time-model) for conversion formulas. diff --git a/images/ATLAS_logo_GR.png b/images/ATLAS_logo_GR.png new file mode 100644 index 0000000000000000000000000000000000000000..cc52506ad30d4a2235afd69a450c8558a614042a GIT binary patch literal 28873 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 H;2ioN2LB9Y literal 0 HcmV?d00001 diff --git a/images/atlaslogo.png b/images/atlaslogo.png deleted file mode 100644 index 79c0c6e9a43c947b3f382618536c643093d4c796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2570343 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 ObzARLdrgnNL;oKNL#a0a diff --git a/notebooks/01-getting-started.ipynb b/notebooks/01-getting-started.ipynb new file mode 100644 index 0000000..0092c6a --- /dev/null +++ b/notebooks/01-getting-started.ipynb @@ -0,0 +1,198 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting Started with SQL Race\n", + "\n", + "This notebook walks through the basics: initialising the SQL Race runtime, loading a session, and reading your first parameter samples.\n", + "\n", + "**Prerequisites:** Python 3.8+, `pythonnet`, ATLAS 10 installed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Import helpers\n", + "\n", + "The `sqlrace_helpers` module handles .NET runtime initialisation and provides convenient wrappers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '.')\n", + "from sqlrace_helpers import init_sqlrace, load_session, session_summary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Initialise the SQL Race runtime\n", + "\n", + "This loads the .NET Core runtime and initialises the SQL Race library. It's safe to call multiple times." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "session_manager = init_sqlrace()\n", + "print(\"SQL Race initialised.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Load a session\n", + "\n", + "Replace the GUID below with a real session GUID from your database." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SESSION_GUID = \"\"\n", + "\n", + "client_session, session = load_session(session_manager, SESSION_GUID)\n", + "session_summary(session)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. List available parameters\n", + "\n", + "Every session has a set of parameters (channels). Let's see what's available." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sqlrace_helpers import list_parameters\n", + "\n", + "params = list_parameters(session)\n", + "print(f\"Found {len(params)} parameter(s):\")\n", + "for p in params[:10]:\n", + " print(f\" {p}\")\n", + "if len(params) > 10:\n", + " print(f\" ... and {len(params) - 10} more\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Read parameter samples\n", + "\n", + "Extract the first parameter as a pandas Series." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sqlrace_helpers import extract_parameter\n", + "\n", + "if params:\n", + " data = extract_parameter(session, params[0])\n", + " print(f\"Parameter: {params[0]}\")\n", + " print(f\"Samples: {len(data)}\")\n", + " print(f\"\\nFirst 5 values:\")\n", + " print(data.head())\n", + "else:\n", + " print(\"No parameters in session.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Quick plot\n", + "\n", + "Visualise the parameter trace." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "if params:\n", + " fig, ax = plt.subplots(figsize=(12, 4))\n", + " time_s = (data.index - data.index[0]) / 1e9\n", + " ax.plot(time_s, data.values, linewidth=0.8)\n", + " ax.set_xlabel(\"Time (s)\")\n", + " ax.set_ylabel(params[0])\n", + " ax.set_title(f\"Parameter trace: {params[0]}\")\n", + " ax.grid(True, alpha=0.3)\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Clean up\n", + "\n", + "Always dispose the client session when done." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_session.Dispose()\n", + "print(\"Session closed.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "- **[02-session-exploration.ipynb](02-session-exploration.ipynb)** — Explore session structure in detail\n", + "- **[03-multi-parameter-analysis.ipynb](03-multi-parameter-analysis.ipynb)** — Work with multiple parameters" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/02-session-exploration.ipynb b/notebooks/02-session-exploration.ipynb new file mode 100644 index 0000000..e735c51 --- /dev/null +++ b/notebooks/02-session-exploration.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Session Exploration\n", + "\n", + "Browse session structure: metadata, parameters, laps, events, and constants.\n", + "\n", + "**Prerequisites:** Complete [01-getting-started.ipynb](01-getting-started.ipynb) first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '.')\n", + "from sqlrace_helpers import (\n", + " init_sqlrace, load_session, session_summary,\n", + " list_parameters, get_laps\n", + ")\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SESSION_GUID = \"\"\n", + "\n", + "sm = init_sqlrace()\n", + "client_session, session = load_session(sm, SESSION_GUID)\n", + "session_summary(session)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Session metadata\n", + "\n", + "Session items are key-value pairs stored with the session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "items = session.Items\nmetadata = {str(items[i].Name): str(items[i].Value)\n for i in range(items.Count)}\n\nif metadata:\n df_meta = pd.DataFrame(metadata.items(), columns=[\"Key\", \"Value\"])\n display(df_meta)\nelse:\n print(\"No session metadata items.\")" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Parameters\n", + "\n", + "List all parameters with their identifiers and application groups." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "records = []\nfor i in range(session.Parameters.Count):\n p = session.Parameters[i]\n records.append({\n \"Identifier\": str(p.Identifier),\n \"Name\": str(p.Name),\n \"Description\": str(p.Description),\n \"App Group\": str(p.ApplicationGroup) if hasattr(p, 'ApplicationGroup') else \"\",\n })\n\ndf_params = pd.DataFrame(records)\nprint(f\"{len(df_params)} parameter(s)\")\ndisplay(df_params)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Laps / Segments\n", + "\n", + "View lap boundaries and timing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_laps = get_laps(session)\n", + "if not df_laps.empty:\n", + " print(f\"{len(df_laps)} lap(s)\")\n", + " display(df_laps)\n", + "else:\n", + " print(\"No laps in session.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constants\n", + "\n", + "Session constants are fixed values stored once (e.g. calibration data)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "constants = session.Constants\nconst_records = []\nfor i in range(constants.Count):\n c = constants[i]\n const_records.append({\n \"Name\": str(c.Name),\n \"Value\": str(c.Value),\n \"Units\": str(c.Units) if hasattr(c, 'Units') else \"\",\n })\n\nif const_records:\n display(pd.DataFrame(const_records))\nelse:\n print(\"No constants in session.\")" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Markers / Annotations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "markers = session.Markers\nmarker_records = []\nfor i in range(markers.Count):\n m = markers[i]\n marker_records.append({\n \"Name\": str(m.Name),\n \"Start (ns)\": int(m.StartTime),\n \"End (ns)\": int(m.EndTime),\n \"Type\": str(m.Type) if hasattr(m, 'Type') else \"\",\n })\n\nif marker_records:\n display(pd.DataFrame(marker_records))\nelse:\n print(\"No markers in session.\")" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_session.Dispose()\n", + "print(\"Session closed.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/notebooks/03-multi-parameter-analysis.ipynb b/notebooks/03-multi-parameter-analysis.ipynb new file mode 100644 index 0000000..14a15d1 --- /dev/null +++ b/notebooks/03-multi-parameter-analysis.ipynb @@ -0,0 +1,188 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Multi-Parameter Analysis\n", + "\n", + "Read multiple parameters at different rates, align them into a single DataFrame, compute statistics, and visualise traces.\n", + "\n", + "**Prerequisites:** Complete [01-getting-started.ipynb](01-getting-started.ipynb) first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '.')\n", + "from sqlrace_helpers import (\n", + " init_sqlrace, load_session, list_parameters,\n", + " extract_parameters, extract_to_timetable, plot_parameters\n", + ")\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SESSION_GUID = \"\"\n", + "\n", + "sm = init_sqlrace()\n", + "client_session, session = load_session(sm, SESSION_GUID)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Select parameters\n", + "\n", + "Pick parameters to analyse. Here we take up to the first 4." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "all_params = list_parameters(session)\n", + "selected = all_params[:4]\n", + "print(f\"Analysing: {selected}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Extract and align\n", + "\n", + "Parameters may be sampled at different rates. `extract_parameters` performs an outer join on timestamps, so faster channels will have NaN in slower channels' rows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = extract_to_timetable(session, selected)\n", + "print(f\"Shape: {df.shape}\")\n", + "display(df.head(10))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Descriptive statistics\n", + "\n", + "Quick summary of each parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats = df.describe().T\n", + "display(stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Correlation\n", + "\n", + "Check how parameters relate to each other. We forward-fill to handle multi-rate alignment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "corr = df.ffill().corr()\n", + "display(corr.style.background_gradient(cmap=\"coolwarm\", vmin=-1, vmax=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot traces\n", + "\n", + "Stacked subplots for visual comparison." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "fig = plot_parameters(df, title=f\"Session: {session.Identifier}\")\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scatter plot: parameter vs parameter\n", + "\n", + "If you have two correlated parameters, a scatter plot can reveal their relationship." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if len(selected) >= 2:\n", + " df_filled = df.ffill().dropna()\n", + " fig, ax = plt.subplots(figsize=(8, 6))\n", + " ax.scatter(df_filled[selected[0]], df_filled[selected[1]],\n", + " alpha=0.3, s=2)\n", + " ax.set_xlabel(selected[0])\n", + " ax.set_ylabel(selected[1])\n", + " ax.set_title(f\"{selected[0]} vs {selected[1]}\")\n", + " ax.grid(True, alpha=0.3)\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_session.Dispose()\n", + "print(\"Session closed.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/notebooks/04-session-comparison.ipynb b/notebooks/04-session-comparison.ipynb new file mode 100644 index 0000000..c81531e --- /dev/null +++ b/notebooks/04-session-comparison.ipynb @@ -0,0 +1,203 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Session Comparison\n", + "\n", + "Load two sessions and compare the same parameter across them — overlay traces, compute deltas, and highlight differences.\n", + "\n", + "**Prerequisites:** Two sessions in the same database with at least one common parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '.')\n", + "from sqlrace_helpers import (\n", + " init_sqlrace, load_session, session_summary,\n", + " list_parameters, extract_parameter\n", + ")\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SESSION_A_GUID = \"\"\n", + "SESSION_B_GUID = \"\"\n", + "COMPARE_PARAM = \"Temperature:Sensors\" # Change to a parameter in your sessions\n", + "\n", + "sm = init_sqlrace()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load both sessions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cs_a, sess_a = load_session(sm, SESSION_A_GUID)\n", + "print(\"Session A:\")\n", + "session_summary(sess_a)\n", + "\n", + "print()\n", + "cs_b, sess_b = load_session(sm, SESSION_B_GUID)\n", + "print(\"Session B:\")\n", + "session_summary(sess_b)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Extract the comparison parameter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_a = extract_parameter(sess_a, COMPARE_PARAM)\n", + "data_b = extract_parameter(sess_b, COMPARE_PARAM)\n", + "\n", + "# Convert to relative time (seconds from session start)\n", + "time_a = (data_a.index - data_a.index[0]) / 1e9\n", + "time_b = (data_b.index - data_b.index[0]) / 1e9\n", + "\n", + "print(f\"Session A: {len(data_a)} samples, {time_a.iloc[-1]:.1f} s\")\n", + "print(f\"Session B: {len(data_b)} samples, {time_b.iloc[-1]:.1f} s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overlay trace comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(12, 5))\n", + "ax.plot(time_a, data_a.values, label=\"Session A\", linewidth=0.8)\n", + "ax.plot(time_b, data_b.values, label=\"Session B\", linewidth=0.8)\n", + "ax.set_xlabel(\"Time (s)\")\n", + "ax.set_ylabel(COMPARE_PARAM)\n", + "ax.set_title(f\"Comparison: {COMPARE_PARAM}\")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Statistical comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats = pd.DataFrame({\n", + " \"Session A\": data_a.describe(),\n", + " \"Session B\": data_b.describe(),\n", + "})\n", + "stats[\"Delta\"] = stats[\"Session B\"] - stats[\"Session A\"]\n", + "display(stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Delta trace\n", + "\n", + "Resample both to a common time grid and plot the difference." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Resample to common time grid\n", + "common_len = min(len(data_a), len(data_b))\n", + "common_time = np.linspace(0, min(time_a.iloc[-1], time_b.iloc[-1]), common_len)\n", + "\n", + "interp_a = np.interp(common_time, time_a, data_a.values)\n", + "interp_b = np.interp(common_time, time_b, data_b.values)\n", + "delta = interp_b - interp_a\n", + "\n", + "fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True)\n", + "axes[0].plot(common_time, interp_a, label=\"A\", linewidth=0.8)\n", + "axes[0].plot(common_time, interp_b, label=\"B\", linewidth=0.8)\n", + "axes[0].set_ylabel(COMPARE_PARAM)\n", + "axes[0].legend()\n", + "axes[0].grid(True, alpha=0.3)\n", + "\n", + "axes[1].fill_between(common_time, delta, alpha=0.4, color=\"red\")\n", + "axes[1].axhline(0, color=\"black\", linewidth=0.5)\n", + "axes[1].set_ylabel(\"B - A\")\n", + "axes[1].set_xlabel(\"Time (s)\")\n", + "axes[1].grid(True, alpha=0.3)\n", + "\n", + "fig.suptitle(f\"Delta: {COMPARE_PARAM}\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cs_a.Dispose()\n", + "cs_b.Dispose()\n", + "print(\"Sessions closed.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/05-export-pipeline.ipynb b/notebooks/05-export-pipeline.ipynb new file mode 100644 index 0000000..76f8ead --- /dev/null +++ b/notebooks/05-export-pipeline.ipynb @@ -0,0 +1,181 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Export Pipeline\n", + "\n", + "Extract session data from SQL Race and export to common file formats: CSV, Parquet, and HDF5.\n", + "\n", + "**Prerequisites:** `pip install pyarrow tables` for Parquet and HDF5 support." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '.')\n", + "from sqlrace_helpers import (\n", + " init_sqlrace, load_session, session_summary,\n", + " list_parameters, extract_parameters\n", + ")\n", + "from pathlib import Path\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SESSION_GUID = \"\"\n", + "OUTPUT_DIR = Path(\"exports\")\n", + "OUTPUT_DIR.mkdir(exist_ok=True)\n", + "\n", + "sm = init_sqlrace()\n", + "client_session, session = load_session(sm, SESSION_GUID)\n", + "session_summary(session)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Extract all parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "params = list_parameters(session)\n", + "print(f\"Extracting {len(params)} parameter(s)...\")\n", + "\n", + "df = extract_parameters(session, params)\n", + "print(f\"DataFrame: {df.shape[0]} rows x {df.shape[1]} columns\")\n", + "display(df.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export to CSV\n", + "\n", + "CSV is universally readable. The timestamp index is in nanoseconds." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "csv_path = OUTPUT_DIR / \"session_data.csv\"\n", + "df.to_csv(csv_path)\n", + "size_mb = csv_path.stat().st_size / 1e6\n", + "print(f\"Saved: {csv_path} ({size_mb:.2f} MB)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export to Parquet\n", + "\n", + "Parquet is a columnar format — much smaller than CSV and faster to load. Requires `pyarrow`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " parquet_path = OUTPUT_DIR / \"session_data.parquet\"\n", + " df.to_parquet(parquet_path)\n", + " size_mb = parquet_path.stat().st_size / 1e6\n", + " print(f\"Saved: {parquet_path} ({size_mb:.2f} MB)\")\n", + "except ImportError:\n", + " print(\"Install pyarrow for Parquet support: pip install pyarrow\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export to HDF5\n", + "\n", + "HDF5 supports hierarchical data and is widely used in scientific computing. Requires `tables`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " hdf_path = OUTPUT_DIR / \"session_data.h5\"\n", + " df.to_hdf(hdf_path, key=\"session\", mode=\"w\")\n", + " size_mb = hdf_path.stat().st_size / 1e6\n", + " print(f\"Saved: {hdf_path} ({size_mb:.2f} MB)\")\n", + "except ImportError:\n", + " print(\"Install tables for HDF5 support: pip install tables\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\nExport complete.\")\n", + "print(f\" Parameters: {len(params)}\")\n", + "print(f\" Rows: {len(df)}\")\n", + "print(f\" Output dir: {OUTPUT_DIR.resolve()}\")\n", + "\n", + "for f in OUTPUT_DIR.iterdir():\n", + " print(f\" {f.name}: {f.stat().st_size / 1e6:.2f} MB\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_session.Dispose()\n", + "print(\"Session closed.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/06-live-data-monitoring.ipynb b/notebooks/06-live-data-monitoring.ipynb new file mode 100644 index 0000000..02f98b3 --- /dev/null +++ b/notebooks/06-live-data-monitoring.ipynb @@ -0,0 +1,181 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Live Data Monitoring\n", + "\n", + "Poll a live SQL Race session and display updating parameter values. This notebook demonstrates the polling pattern for monitoring data as it arrives.\n", + "\n", + "**Prerequisites:** A live session actively receiving data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '.')\n", + "from sqlrace_helpers import init_sqlrace, load_session, list_parameters\n", + "import time\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import display, clear_output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SESSION_GUID = \"\"\n", + "POLL_INTERVAL = 1.0 # seconds between polls\n", + "MAX_POLLS = 30 # stop after this many polls\n", + "\n", + "sm = init_sqlrace()\n", + "client_session, session = load_session(sm, SESSION_GUID)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Select parameters to monitor" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "all_params = list_parameters(session)\n", + "monitor_params = all_params[:3] # Monitor first 3 parameters\n", + "print(f\"Monitoring: {monitor_params}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Polling loop\n", + "\n", + "Each poll reads the latest samples and appends them to a rolling buffer. The display updates in-place.\n", + "\n", + "**Note:** In a live session, `session.EndTime` advances as new data arrives. We read samples near the end to get the latest values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "history = {p: [] for p in monitor_params}\n", + "timestamps = []\n", + "\n", + "print(\"Starting live polling...\")\n", + "print(f\"Polling every {POLL_INTERVAL}s for up to {MAX_POLLS} iterations.\")\n", + "print(\"Interrupt the kernel to stop early.\\n\")\n", + "\n", + "try:\n", + " for poll in range(MAX_POLLS):\n", + " end_time = session.EndTime\n", + " # Read the last 100ms of data\n", + " window_ns = int(0.1 * 1e9)\n", + " start_time = max(session.StartTime, end_time - window_ns)\n", + "\n", + " latest = {}\n", + " for param_id in monitor_params:\n", + " pda = session.CreateParameterDataAccess(param_id)\n", + " try:\n", + " samples = pda.GetSamplesBetween(start_time, end_time)\n", + " if samples.SampleCount > 0:\n", + " val = float(samples.Data(samples.SampleCount - 1))\n", + " latest[param_id] = val\n", + " history[param_id].append(val)\n", + " else:\n", + " latest[param_id] = float('nan')\n", + " finally:\n", + " pda.Dispose()\n", + "\n", + " timestamps.append(poll * POLL_INTERVAL)\n", + "\n", + " # Update display\n", + " clear_output(wait=True)\n", + " print(f\"Poll {poll + 1}/{MAX_POLLS} | Session end: {end_time} ns\")\n", + " print(\"-\" * 50)\n", + " for p, v in latest.items():\n", + " print(f\" {p:30s} = {v:12.4f}\")\n", + "\n", + " time.sleep(POLL_INTERVAL)\n", + "\n", + "except KeyboardInterrupt:\n", + " print(\"\\nPolling stopped by user.\")\n", + "\n", + "print(f\"\\nCollected {len(timestamps)} samples.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot collected history" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if timestamps:\n", + " df_hist = pd.DataFrame(history, index=timestamps)\n", + " df_hist.index.name = \"time (s)\"\n", + "\n", + " fig, axes = plt.subplots(len(monitor_params), 1,\n", + " figsize=(12, 3 * len(monitor_params)),\n", + " sharex=True, squeeze=False)\n", + " for i, param in enumerate(monitor_params):\n", + " axes[i, 0].plot(df_hist.index, df_hist[param],\n", + " marker='o', markersize=3, linewidth=1)\n", + " axes[i, 0].set_ylabel(param)\n", + " axes[i, 0].grid(True, alpha=0.3)\n", + "\n", + " axes[-1, 0].set_xlabel(\"Poll time (s)\")\n", + " fig.suptitle(\"Live Monitoring History\")\n", + " plt.tight_layout()\n", + " plt.show()\n", + "else:\n", + " print(\"No data collected.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_session.Dispose()\n", + "print(\"Session closed.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/README.md b/notebooks/README.md new file mode 100644 index 0000000..e9ceb1c --- /dev/null +++ b/notebooks/README.md @@ -0,0 +1,57 @@ +# Notebooks (Tier 3) + +Interactive Jupyter notebooks for exploring SQL Race session data with narrative, code, and visualisation. + +## Prerequisites + +- Python 3.8+ +- ATLAS 10 installed (for SQL Race .NET assemblies) +- Jupyter: `pip install -r requirements.txt` + +## Setup + +```bash +cd notebooks +pip install -r requirements.txt +jupyter notebook +``` + +### Configuration + +Set environment variables to customise the connection: + +| Variable | Default | Description | +|----------|---------|-------------| +| `SQLRACE_DLL_PATH` | `C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll` | Path to SQL Race DLL | +| `SQLRACE_CONNECTION_STRING` | SQLite in temp directory | Full connection string | + +## Core Notebooks + +| # | Notebook | Description | +|---|----------|-------------| +| 1 | [01-getting-started.ipynb](01-getting-started.ipynb) | First steps: initialise, load a session, read samples | +| 2 | [02-session-exploration.ipynb](02-session-exploration.ipynb) | Browse session structure, parameters, laps, metadata | +| 3 | [03-multi-parameter-analysis.ipynb](03-multi-parameter-analysis.ipynb) | Read, align, and plot multiple parameters | +| 4 | [04-session-comparison.ipynb](04-session-comparison.ipynb) | Compare data across two sessions | +| 5 | [05-export-pipeline.ipynb](05-export-pipeline.ipynb) | Export session data to CSV, Parquet, HDF5 | +| 6 | [06-live-data-monitoring.ipynb](06-live-data-monitoring.ipynb) | Poll and display live session data | + +## Industry Scenarios + +| Notebook | Industry | Description | +|----------|----------|-------------| +| [motorsport-lap-analysis.ipynb](scenarios/motorsport-lap-analysis.ipynb) | Motorsport | Lap-by-lap telemetry comparison | +| [flight-test-manoeuvre-extraction.ipynb](scenarios/flight-test-manoeuvre-extraction.ipynb) | Aerospace | Extract manoeuvre windows from flight test data | +| [turbine-performance-trending.ipynb](scenarios/turbine-performance-trending.ipynb) | Energy | Long-term turbine efficiency trending | +| [durability-cycle-comparison.ipynb](scenarios/durability-cycle-comparison.ipynb) | Automotive | Compare stress cycles across durability runs | + +## Shared Helpers + +The `sqlrace_helpers.py` module provides common utilities: + +- `init_sqlrace()` — Initialise runtime, return SessionManager +- `load_session()` — Load a session by GUID +- `extract_parameter()` / `extract_parameters()` — Extract data to pandas +- `extract_to_timetable()` — Extract with timedelta index +- `get_laps()` — Lap info as DataFrame +- `plot_parameters()` — Quick subplot visualisation diff --git a/notebooks/requirements.txt b/notebooks/requirements.txt new file mode 100644 index 0000000..f6e6d65 --- /dev/null +++ b/notebooks/requirements.txt @@ -0,0 +1,12 @@ +# SQL Race Notebooks — Python dependencies +# Install: pip install -r requirements.txt + +pythonnet>=3.0.3 +jupyter>=1.0 +notebook>=7.0 +pandas>=2.0 +numpy>=1.24 +matplotlib>=3.7 +plotly>=5.15 +ipywidgets>=8.0 +tabulate>=0.9 diff --git a/notebooks/scenarios/data/durability-cycle1.ssn2 b/notebooks/scenarios/data/durability-cycle1.ssn2 new file mode 100644 index 0000000000000000000000000000000000000000..4b5c523df19f3a203f0ba403edbe22d374244b92 GIT binary patch literal 532480 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 literal 0 HcmV?d00001 diff --git a/notebooks/scenarios/data/durability-cycle2.ssn2 b/notebooks/scenarios/data/durability-cycle2.ssn2 new file mode 100644 index 0000000000000000000000000000000000000000..1687c09bf2550ee10e602fedbe37b9b5757dfc9b GIT binary patch literal 532480 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 literal 0 HcmV?d00001 diff --git a/notebooks/scenarios/data/durability-cycle3.ssn2 b/notebooks/scenarios/data/durability-cycle3.ssn2 new file mode 100644 index 0000000000000000000000000000000000000000..d023e4c465bb57f45f14eaca88dae2f1502d7134 GIT binary patch literal 532480 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 F{~z>5TCxBD literal 0 HcmV?d00001 diff --git a/notebooks/scenarios/data/flight-test-manoeuvre-extraction.ssn2 b/notebooks/scenarios/data/flight-test-manoeuvre-extraction.ssn2 new file mode 100644 index 0000000000000000000000000000000000000000..9f1dbfb9c5ee8da73e0995afa0254afabf69b16d GIT binary patch literal 536576 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�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%0DYvMj$dHYA{-9`$u5lUXY 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

C(%z+IIt{A`uhS|Y@(h)KM9xx~T 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 s+)6TAs?&Bo+0@7$@qn;7RmYYBhO9zR}R`#Hq)$ literal 0 HcmV?d00001 diff --git a/notebooks/scenarios/data/turbine-week1.ssn2 b/notebooks/scenarios/data/turbine-week1.ssn2 new file mode 100644 index 0000000000000000000000000000000000000000..02a436177db37efa8d6872df06474ab93dcd5618 GIT binary patch literal 577536 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 literal 0 HcmV?d00001 diff --git a/notebooks/scenarios/data/turbine-week2.ssn2 b/notebooks/scenarios/data/turbine-week2.ssn2 new file mode 100644 index 0000000000000000000000000000000000000000..c3ba3c01779c64285a9cbb5b1b6ef0fdb7d6e001 GIT binary patch literal 577536 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%0DQs>Zm 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 literal 0 HcmV?d00001 diff --git a/notebooks/scenarios/durability-cycle-comparison.ipynb b/notebooks/scenarios/durability-cycle-comparison.ipynb new file mode 100644 index 0000000..391af24 --- /dev/null +++ b/notebooks/scenarios/durability-cycle-comparison.ipynb @@ -0,0 +1,240 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Automotive: Durability Cycle Comparison\n", + "\n", + "Compare stress and load data across multiple durability test cycles to assess structural fatigue and identify anomalous runs.\n", + "\n", + "**Context:** Automotive durability testing subjects components to repeated stress cycles. Each session represents one test run. Engineers compare runs to detect drift, early fatigue, or anomalous loading patterns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '..')\n", + "from sqlrace_helpers import (\n", + " init_sqlrace, load_session, session_summary,\n", + " list_parameters, extract_parameter, extract_to_timetable\n", + ")\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(\"__file__\")), \"data\")\n", + "\n", + "# Durability test sessions (one per cycle)\n", + "CYCLES = [\n", + " (\"53f2367d-14b6-43d1-aeaf-6e66f1b1031c\", \"durability-cycle1.ssn2\"),\n", + " (\"f8d31ef8-48ba-450b-971c-8fec68dec7f8\", \"durability-cycle2.ssn2\"),\n", + " (\"54c2a62c-08c5-4f35-9a85-8a276ec83329\", \"durability-cycle3.ssn2\"),\n", + "]\n", + "\n", + "# Primary stress/load parameter\n", + "STRESS_PARAM = \"Force:Actuator01\"\n", + "\n", + "sm = init_sqlrace()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load and extract each cycle" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cycle_data = {}\n", + "cycle_stats = []\n", + "\n", + "for i, (guid, filename) in enumerate(CYCLES):\n", + " cs = f\"DbEngine=SQLite;Data Source={os.path.join(DATA_DIR, filename)};\"\n", + " client, sess = load_session(sm, guid, connection_string=cs)\n", + " try:\n", + " data = extract_parameter(sess, STRESS_PARAM)\n", + " # Convert to relative seconds\n", + " t0 = data.index[0]\n", + " rel_time = (data.index - t0) / 1e9\n", + " cycle_data[f\"Cycle {i+1}\"] = pd.Series(data.values, index=rel_time)\n", + "\n", + " cycle_stats.append({\n", + " \"Cycle\": i + 1,\n", + " \"Samples\": len(data),\n", + " \"Duration (s)\": rel_time[-1],\n", + " \"Mean\": data.mean(),\n", + " \"Std\": data.std(),\n", + " \"Peak (abs)\": data.abs().max(),\n", + " \"RMS\": np.sqrt((data.values ** 2).mean()),\n", + " })\n", + " print(f\" Cycle {i+1}: {len(data)} samples\")\n", + " finally:\n", + " client.Dispose()\n", + "\n", + "df_stats = pd.DataFrame(cycle_stats).set_index(\"Cycle\")\n", + "display(df_stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overlay cycle traces" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(14, 5))\n", + "\n", + "for name, data in cycle_data.items():\n", + " ax.plot(data.index, data.values, label=name,\n", + " linewidth=0.6, alpha=0.7)\n", + "\n", + "ax.set_xlabel(\"Time (s)\")\n", + "ax.set_ylabel(STRESS_PARAM)\n", + "ax.set_title(\"Durability Cycle Overlay\")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Peak distribution\n", + "\n", + "Compare the distribution of peak loads across cycles using histograms." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 5))\n", + "\n", + "for name, data in cycle_data.items():\n", + " ax.hist(data.values, bins=50, alpha=0.5, label=name, density=True)\n", + "\n", + "ax.set_xlabel(STRESS_PARAM)\n", + "ax.set_ylabel(\"Density\")\n", + "ax.set_title(\"Load Distribution Comparison\")\n", + "ax.legend()\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## RMS trend across cycles\n", + "\n", + "RMS (root mean square) of the stress signal is a proxy for fatigue damage accumulation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "ax.plot(df_stats.index, df_stats[\"RMS\"], 'o-', markersize=8, linewidth=2)\n", + "ax.set_xlabel(\"Cycle\")\n", + "ax.set_ylabel(\"RMS Load\")\n", + "ax.set_title(\"RMS Trend Across Cycles\")\n", + "ax.grid(True, alpha=0.3)\n", + "\n", + "# Flag cycles where RMS deviates >10% from first cycle\n", + "baseline = df_stats[\"RMS\"].iloc[0]\n", + "for idx, rms in df_stats[\"RMS\"].items():\n", + " if abs(rms - baseline) / baseline > 0.10:\n", + " ax.annotate(f\"{(rms-baseline)/baseline:+.1%}\",\n", + " (idx, rms), textcoords=\"offset points\",\n", + " xytext=(0, 12), ha='center', color='red')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Anomaly detection\n", + "\n", + "Flag cycles where key metrics deviate significantly from the group." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Anomaly Check (>2 sigma from group mean)\")\n", + "print(\"=\" * 50)\n", + "\n", + "for metric in [\"Mean\", \"Std\", \"Peak (abs)\", \"RMS\"]:\n", + " group_mean = df_stats[metric].mean()\n", + " group_std = df_stats[metric].std()\n", + "\n", + " for cycle, val in df_stats[metric].items():\n", + " if group_std > 0 and abs(val - group_mean) > 2 * group_std:\n", + " z = (val - group_mean) / group_std\n", + " print(f\" Cycle {cycle}: {metric} = {val:.4f} \"\n", + " f\"(z = {z:+.2f}, group mean = {group_mean:.4f})\")\n", + "\n", + "print(\"\\nDone.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/scenarios/flight-test-manoeuvre-extraction.ipynb b/notebooks/scenarios/flight-test-manoeuvre-extraction.ipynb new file mode 100644 index 0000000..d797884 --- /dev/null +++ b/notebooks/scenarios/flight-test-manoeuvre-extraction.ipynb @@ -0,0 +1,204 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Aerospace: Flight Test Manoeuvre Extraction\n", + "\n", + "Extract specific manoeuvre windows from flight test data using marker annotations, compute aerodynamic parameters within each window, and compare manoeuvres.\n", + "\n", + "**Context:** Flight test sessions are long recordings containing multiple test manoeuvres (e.g. stall tests, Dutch rolls, phugoids). Markers annotate the start and end of each manoeuvre, allowing engineers to extract and analyse specific windows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '..')\n", + "from sqlrace_helpers import (\n", + " init_sqlrace, load_session, session_summary,\n", + " list_parameters, extract_parameters\n", + ")\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(\"__file__\")), \"data\")\n", + "\n", + "SESSION_GUID = \"828f3c72-9573-4a74-9803-20ff993d848a\"\n", + "# Typical flight test parameters\n", + "PARAMS = [\"Acceleration:Platform\", \"Pressure:Inlet\", \"Temperature:Sensors\"]\n", + "\n", + "cs = f\"DbEngine=SQLite;Data Source={os.path.join(DATA_DIR, 'flight-test-manoeuvre-extraction.ssn2')};\"\n", + "sm = init_sqlrace()\n", + "client_session, session = load_session(sm, SESSION_GUID, connection_string=cs)\n", + "session_summary(session)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Discover manoeuvre markers\n", + "\n", + "Markers define time windows for each test manoeuvre." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "markers = session.Markers\n", + "manoeuvres = []\n", + "for i in range(markers.Count):\n", + " m = markers[i]\n", + " manoeuvres.append({\n", + " \"Name\": str(m.Label),\n", + " \"Start (ns)\": int(m.StartTimestamp),\n", + " \"End (ns)\": int(m.EndTimestamp),\n", + " \"Duration (s)\": (int(m.EndTimestamp) - int(m.StartTimestamp)) / 1e9,\n", + " })\n", + "\n", + "df_manoeuvres = pd.DataFrame(manoeuvres)\n", + "if not df_manoeuvres.empty:\n", + " print(f\"{len(df_manoeuvres)} manoeuvre marker(s) found\")\n", + " display(df_manoeuvres)\n", + "else:\n", + " print(\"No markers found. Falling back to full session.\")\n", + " # Create a single window for the whole session\n", + " df_manoeuvres = pd.DataFrame([{\n", + " \"Name\": \"Full session\",\n", + " \"Start (ns)\": int(session.StartTime),\n", + " \"End (ns)\": int(session.EndTime),\n", + " \"Duration (s)\": (session.EndTime - session.StartTime) / 1e9,\n", + " }])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Extract data per manoeuvre" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "available = set(list_parameters(session))\n", + "extract_params = [p for p in PARAMS if p in available]\n", + "\n", + "manoeuvre_data = {}\n", + "for _, row in df_manoeuvres.iterrows():\n", + " name = row[\"Name\"]\n", + " df = extract_parameters(session, extract_params,\n", + " start_time=row[\"Start (ns)\"],\n", + " end_time=row[\"End (ns)\"])\n", + " # Convert to relative time\n", + " if not df.empty:\n", + " t0 = df.index[0]\n", + " df.index = (df.index - t0) / 1e9\n", + " df.index.name = \"time_s\"\n", + " manoeuvre_data[name] = df\n", + " print(f\" {name}: {df.shape[0]} samples x {df.shape[1]} params\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Per-manoeuvre statistics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "summary_rows = []\n", + "for name, df in manoeuvre_data.items():\n", + " if df.empty:\n", + " continue\n", + " row = {\"Manoeuvre\": name, \"Duration (s)\": df.index[-1]}\n", + " for col in df.columns:\n", + " row[f\"{col} mean\"] = df[col].mean()\n", + " row[f\"{col} std\"] = df[col].std()\n", + " row[f\"{col} peak\"] = df[col].abs().max()\n", + " summary_rows.append(row)\n", + "\n", + "df_summary = pd.DataFrame(summary_rows).set_index(\"Manoeuvre\")\n", + "display(df_summary)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot manoeuvre windows" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for name, df in manoeuvre_data.items():\n", + " if df.empty:\n", + " continue\n", + " n_cols = len(df.columns)\n", + " fig, axes = plt.subplots(n_cols, 1, figsize=(12, 3 * n_cols),\n", + " sharex=True, squeeze=False)\n", + " for i, col in enumerate(df.columns):\n", + " axes[i, 0].plot(df.index, df[col], linewidth=0.8)\n", + " axes[i, 0].set_ylabel(col)\n", + " axes[i, 0].grid(True, alpha=0.3)\n", + "\n", + " axes[0, 0].set_title(f\"Manoeuvre: {name}\")\n", + " axes[-1, 0].set_xlabel(\"Time (s)\")\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_session.Dispose()\n", + "print(\"Session closed.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/scenarios/motorsport-lap-analysis.ipynb b/notebooks/scenarios/motorsport-lap-analysis.ipynb new file mode 100644 index 0000000..079281d --- /dev/null +++ b/notebooks/scenarios/motorsport-lap-analysis.ipynb @@ -0,0 +1,191 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Motorsport: Lap-by-Lap Telemetry Analysis\n", + "\n", + "Analyse telemetry data across multiple laps to identify performance trends, compare lap times, and find the fastest segments.\n", + "\n", + "**Context:** In motorsport, sessions contain laps defined by trigger sources (e.g. timing beacons). Each lap has a start time and number, allowing per-lap data extraction and comparison." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '..')\n", + "from sqlrace_helpers import (\n", + " init_sqlrace, load_session, session_summary,\n", + " list_parameters, extract_parameter, get_laps\n", + ")\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(\"__file__\")), \"data\")\n", + "\n", + "SESSION_GUID = \"a8b2b3b4-5530-4825-a5d1-bda63732361e\"\n", + "SPEED_PARAM = \"Speed:Shaft\" # Vehicle speed parameter\n", + "TEMP_PARAM = \"Temperature:Sensors\" # Brake disc temperature\n", + "\n", + "cs = f\"DbEngine=SQLite;Data Source={os.path.join(DATA_DIR, 'motorsport-lap-analysis.ssn2')};\"\n", + "sm = init_sqlrace()\n", + "client_session, session = load_session(sm, SESSION_GUID, connection_string=cs)\n", + "session_summary(session)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Lap boundaries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_laps = get_laps(session)\n", + "print(f\"{len(df_laps)} lap(s) found\")\n", + "display(df_laps)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Extract per-lap data\n", + "\n", + "Read the speed parameter for each lap window." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lap_data = {}\n", + "lap_times = df_laps[\"start_time_ns\"].tolist()\n", + "\n", + "for i in range(len(lap_times)):\n", + " t_start = lap_times[i]\n", + " t_end = lap_times[i + 1] if i + 1 < len(lap_times) else session.EndTime\n", + "\n", + " data = extract_parameter(session, SPEED_PARAM, t_start, t_end)\n", + " if len(data) > 0:\n", + " # Convert to relative time within lap\n", + " rel_time = (data.index - data.index[0]) / 1e9\n", + " lap_data[i] = pd.Series(data.values, index=rel_time)\n", + " duration = rel_time[-1]\n", + " print(f\" Lap {i}: {len(data)} samples, {duration:.2f} s\")\n", + "\n", + "print(f\"\\nExtracted {len(lap_data)} laps.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overlay lap traces" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(12, 5))\n", + "for lap_num, data in lap_data.items():\n", + " ax.plot(data.index, data.values, label=f\"Lap {lap_num}\",\n", + " linewidth=0.8, alpha=0.7)\n", + "\n", + "ax.set_xlabel(\"Lap distance / time (s)\")\n", + "ax.set_ylabel(SPEED_PARAM)\n", + "ax.set_title(\"Lap Overlay\")\n", + "ax.legend(loc=\"upper right\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Per-lap statistics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats = []\n", + "for lap_num, data in lap_data.items():\n", + " stats.append({\n", + " \"Lap\": lap_num,\n", + " \"Duration (s)\": data.index[-1],\n", + " \"Mean Speed\": data.mean(),\n", + " \"Max Speed\": data.max(),\n", + " \"Min Speed\": data.min(),\n", + " \"Std Speed\": data.std(),\n", + " })\n", + "\n", + "df_stats = pd.DataFrame(stats).set_index(\"Lap\")\n", + "display(df_stats)\n", + "\n", + "fastest = df_stats[\"Duration (s)\"].idxmin()\n", + "print(f\"\\nFastest lap: {fastest} ({df_stats.loc[fastest, 'Duration (s)']:.2f} s)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_session.Dispose()\n", + "print(\"Session closed.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/scenarios/turbine-performance-trending.ipynb b/notebooks/scenarios/turbine-performance-trending.ipynb new file mode 100644 index 0000000..6ee7c6a --- /dev/null +++ b/notebooks/scenarios/turbine-performance-trending.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Energy: Turbine Performance Trending\n", + "\n", + "Load multiple sessions recorded over time and track turbine efficiency metrics to identify performance degradation trends.\n", + "\n", + "**Context:** In power generation, turbine performance is monitored over weeks or months. Each test session captures temperature, pressure, and rotational speed. Trending these metrics reveals gradual degradation that warrants maintenance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '..')\n", + "from sqlrace_helpers import (\n", + " init_sqlrace, load_session, session_summary,\n", + " list_parameters, extract_parameters\n", + ")\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(\"__file__\")), \"data\")\n", + "\n", + "# Session GUIDs and their .ssn2 files in chronological order\n", + "SESSIONS = [\n", + " (\"e6d9bbc0-89a6-4754-9b4c-f7b5a6b4718b\", \"turbine-week1.ssn2\"), # Week 1\n", + " (\"7e74ba09-b54d-415e-bb25-4214300019ef\", \"turbine-week2.ssn2\"), # Week 2\n", + " (\"56d725d0-569b-4c74-b6ae-4dd497edca2d\", \"turbine-week3.ssn2\"), # Week 3\n", + "]\n", + "\n", + "# Parameters to trend\n", + "TREND_PARAMS = [\"Temperature:Sensors\", \"Pressure:Inlet\", \"RPM:Turbine\"]\n", + "\n", + "sm = init_sqlrace()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load sessions and compute per-session statistics\n", + "\n", + "For each session, extract the target parameters and compute summary statistics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "session_stats = []\n", + "\n", + "for idx, (guid, filename) in enumerate(SESSIONS):\n", + " cs = f\"DbEngine=SQLite;Data Source={os.path.join(DATA_DIR, filename)};\"\n", + " client, sess = load_session(sm, guid, connection_string=cs)\n", + " try:\n", + " available = set(list_parameters(sess))\n", + " params = [p for p in TREND_PARAMS if p in available]\n", + "\n", + " df = extract_parameters(sess, params)\n", + " row = {\"Session\": idx + 1, \"GUID\": guid[:8] + \"...\"}\n", + "\n", + " for col in df.columns:\n", + " row[f\"{col}_mean\"] = df[col].mean()\n", + " row[f\"{col}_std\"] = df[col].std()\n", + "\n", + " session_stats.append(row)\n", + " print(f\" Session {idx + 1}: {len(df)} samples, {len(params)} params\")\n", + " finally:\n", + " client.Dispose()\n", + "\n", + "df_trend = pd.DataFrame(session_stats).set_index(\"Session\")\n", + "display(df_trend)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Trend plots\n", + "\n", + "Plot mean values over time to visualise degradation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mean_cols = [c for c in df_trend.columns if c.endswith(\"_mean\")]\n", + "n = len(mean_cols)\n", + "\n", + "fig, axes = plt.subplots(n, 1, figsize=(10, 3 * n), sharex=True, squeeze=False)\n", + "\n", + "for i, col in enumerate(mean_cols):\n", + " param_name = col.replace(\"_mean\", \"\")\n", + " std_col = f\"{param_name}_std\"\n", + "\n", + " ax = axes[i, 0]\n", + " x = df_trend.index\n", + " y = df_trend[col]\n", + "\n", + " ax.plot(x, y, 'o-', markersize=6, linewidth=1.5)\n", + "\n", + " if std_col in df_trend.columns:\n", + " err = df_trend[std_col]\n", + " ax.fill_between(x, y - err, y + err, alpha=0.2)\n", + "\n", + " # Add trend line\n", + " z = np.polyfit(x, y, 1)\n", + " ax.plot(x, np.polyval(z, x), '--', color='red', alpha=0.6,\n", + " label=f\"Trend: {z[0]:+.4f}/session\")\n", + "\n", + " ax.set_ylabel(param_name)\n", + " ax.legend()\n", + " ax.grid(True, alpha=0.3)\n", + "\n", + "axes[0, 0].set_title(\"Turbine Performance Trending\")\n", + "axes[-1, 0].set_xlabel(\"Session number\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Degradation assessment\n", + "\n", + "Flag parameters where the trend slope exceeds a threshold." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "THRESHOLD_PCT = 5.0 # Flag if >5% change over the series\n", + "\n", + "print(\"Degradation Assessment\")\n", + "print(\"=\" * 60)\n", + "\n", + "for col in mean_cols:\n", + " param_name = col.replace(\"_mean\", \"\")\n", + " values = df_trend[col].values\n", + " if len(values) < 2 or values[0] == 0:\n", + " continue\n", + "\n", + " pct_change = 100 * (values[-1] - values[0]) / abs(values[0])\n", + " status = \"WARNING\" if abs(pct_change) > THRESHOLD_PCT else \"OK\"\n", + " print(f\" {param_name:30s} {pct_change:+.2f}% [{status}]\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/sqlrace_helpers.py b/notebooks/sqlrace_helpers.py new file mode 100644 index 0000000..09b8024 --- /dev/null +++ b/notebooks/sqlrace_helpers.py @@ -0,0 +1,245 @@ +# ───────────────────────────────────────────────────────────── +# SQL Race Notebook Helpers +# ───────────────────────────────────────────────────────────── +# +# Shared utilities for SQL Race Jupyter notebooks. +# Import with: from sqlrace_helpers import init_sqlrace, load_session, extract_parameter +# +# Prerequisites: pythonnet, ATLAS 10 installed +# ───────────────────────────────────────────────────────────── + +import json +import os +import sys +import tempfile +from pathlib import Path + +import numpy as np +import pandas as pd + + +# ── Configuration ────────────────────────────────────────── + +DEFAULT_DLL_PATH = r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll" +DEFAULT_DB_PATH = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") + + +def _load_dotenv(): + """Load variables from the repo .env file if it exists (no external dependency).""" + # Walk up from this file to find .env at the repo root + here = Path(__file__).resolve().parent + for parent in [here] + list(here.parents): + env_file = parent / ".env" + if env_file.is_file(): + with open(env_file) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + # Handle 'export KEY="VALUE"' or 'KEY=VALUE' + if line.startswith("export "): + line = line[7:] + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip('"').strip("'") + if key and key not in os.environ: + os.environ[key] = value + break + + +_load_dotenv() + + +def get_dll_path(): + """Return the SQL Race DLL path from environment or default.""" + return os.environ.get("SQLRACE_DLL_PATH", DEFAULT_DLL_PATH) + + +def get_connection_string(db_path=None): + """Return a connection string from environment, argument, or default SQLite.""" + env = os.environ.get("SQLRACE_CONNECTION_STRING") + if env: + return env + path = db_path or DEFAULT_DB_PATH + return f"DbEngine=SQLite;Data Source={path};" + + +# ── Initialisation ───────────────────────────────────────── + +_initialised = False + + +def init_sqlrace(dll_path=None): + """Initialise the SQL Race runtime. Safe to call multiple times. + + Returns the SessionManager instance. + """ + global _initialised + + dll = dll_path or get_dll_path() + atlas_dir = os.path.dirname(dll) + + if not _initialised: + runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") + with open(runtime_cfg, "w") as f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [atlas_dir]}}, f) + + from pythonnet import load + load("coreclr", runtime_config=runtime_cfg) + _initialised = True + + import clr + clr.AddReference(dll) + clr.AddReference(os.path.join(atlas_dir, "MAT.OCS.Core.dll")) + + from MESL.SqlRace.Domain import Core, SessionManager + + Core.LicenceProgramName = "SQLRace" + Core.Initialize() + + return SessionManager.CreateSessionManager() + + +# ── Session helpers ──────────────────────────────────────── + +def load_session(session_manager, session_guid, connection_string=None): + """Load a session by GUID string. Returns (client_session, session).""" + from MAT.OCS.Core import SessionKey + + key = SessionKey.Parse(session_guid) + cs = connection_string or get_connection_string() + client_session = session_manager.Load(key, cs) + return client_session, client_session.Session + + +def session_summary(session): + """Print a summary of the session.""" + duration_s = (session.EndTime - session.StartTime) / 1e9 + print(f"Session: {session.Identifier}") + print(f"Start: {session.StartTime} ns") + print(f"End: {session.EndTime} ns") + print(f"Duration: {duration_s:.3f} s") + print(f"Parameters: {session.Parameters.Count}") + print(f"Laps: {session.LapCollection.Count}") + + +def list_parameters(session): + """Return a list of parameter identifier strings.""" + return [str(session.Parameters[i].Identifier) + for i in range(session.Parameters.Count)] + + +# ── Data extraction ──────────────────────────────────────── + +def extract_parameter(session, identifier, start_time=None, end_time=None): + """Extract a single parameter as a pandas Series with nanosecond timestamps. + + Args: + session: The SQL Race session object. + identifier: Parameter identifier string. + start_time: Start time in ns (defaults to session start). + end_time: End time in ns (defaults to session end). + + Returns: + pandas.Series with Int64 index (nanoseconds) and float values. + """ + t0 = start_time if start_time is not None else session.StartTime + t1 = end_time if end_time is not None else session.EndTime + + pda = session.CreateParameterDataAccess(identifier) + try: + samples = pda.GetSamplesBetween(t0, t1) + n = samples.SampleCount + + timestamps = np.array([int(samples.Timestamp[i]) for i in range(n)], dtype=np.int64) + values = np.array([float(samples.Data[i]) for i in range(n)], dtype=np.float64) + + return pd.Series(values, index=timestamps, name=identifier) + finally: + pda.Dispose() + + +def extract_parameters(session, identifiers, start_time=None, end_time=None): + """Extract multiple parameters into a pandas DataFrame. + + Each parameter becomes a column. Rows are aligned by timestamp using + an outer join, so parameters at different rates will have NaN gaps. + + Args: + session: The SQL Race session object. + identifiers: List of parameter identifier strings. + start_time: Start time in ns (defaults to session start). + end_time: End time in ns (defaults to session end). + + Returns: + pandas.DataFrame with Int64 index (nanoseconds). + """ + series_list = [] + for ident in identifiers: + s = extract_parameter(session, ident, start_time, end_time) + series_list.append(s) + + if not series_list: + return pd.DataFrame() + + df = pd.concat(series_list, axis=1, join="outer") + df.index.name = "timestamp_ns" + return df + + +def extract_to_timetable(session, identifiers, start_time=None, end_time=None): + """Extract parameters into a DataFrame with a timedelta index (seconds from start). + + Convenience wrapper around extract_parameters that converts nanosecond + timestamps to seconds relative to the first sample. + """ + df = extract_parameters(session, identifiers, start_time, end_time) + if df.empty: + return df + + t0 = df.index[0] + df.index = pd.to_timedelta((df.index - t0) / 1e9, unit="s") + df.index.name = "time" + return df + + +# ── Lap helpers ──────────────────────────────────────────── + +def get_laps(session): + """Return lap information as a pandas DataFrame.""" + laps = session.LapCollection + records = [] + for i in range(laps.Count): + lap = laps[i] + records.append({ + "number": int(lap.Number), + "start_time_ns": int(lap.StartTime), + "name": str(lap.Name), + }) + return pd.DataFrame(records) + + +# ── Plotting helpers ─────────────────────────────────────── + +def plot_parameters(df, title="Parameter Traces", figsize=(12, 6)): + """Plot each column as a subplot. Returns the matplotlib figure.""" + import matplotlib.pyplot as plt + + n = len(df.columns) + fig, axes = plt.subplots(n, 1, figsize=(figsize[0], figsize[1] * n / 2), + sharex=True, squeeze=False) + + for i, col in enumerate(df.columns): + ax = axes[i, 0] + ax.plot(df.index.total_seconds() if hasattr(df.index, "total_seconds") else df.index, + df[col], linewidth=0.8) + ax.set_ylabel(col) + ax.grid(True, alpha=0.3) + + axes[0, 0].set_title(title) + axes[-1, 0].set_xlabel("Time (s)") + fig.tight_layout() + return fig diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..6be70d8 --- /dev/null +++ b/nuget.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/projects/README.md b/projects/README.md new file mode 100644 index 0000000..4db01cd --- /dev/null +++ b/projects/README.md @@ -0,0 +1,35 @@ +# Projects (Tier 2) + +Complete, buildable solutions demonstrating production-quality patterns with the SQL Race API. + +## C# + +All C# projects are in the `SqlRace.Examples.sln` solution. Build all at once: + +```bash +cd projects/csharp +dotnet build SqlRace.Examples.sln +``` + +| Project | Type | Description | +|---------|------|-------------| +| [SqlRace.Examples.Core](csharp/SqlRace.Examples.Core/) | Library | Shared utilities: bootstrapping, connection strings, fluent parameter builder | +| [GettingStarted](csharp/GettingStarted/) | Console | Demonstrates the Core library API: create session, add parameters, write/read data | +| [ServerListenerRecorder](csharp/ServerListenerRecorder/) | Console | Records telemetry data to SQLite with multi-rate parameters (100/50/200 Hz) | +| [StandaloneRecorder](csharp/StandaloneRecorder/) | Console | ADS connection pattern with read-transform-write loop (EMA smoothing) | +| [BatchExporter](csharp/BatchExporter/) | Console | Queries sessions and exports parameter data to CSV files | +| [LiveDashboard](csharp/LiveDashboard/) | Console | Text-based real-time dashboard with polling and live statistics | +| [FunctionLibrary](csharp/FunctionLibrary/) | Library | Three .NET function patterns: unit converter, rolling average, cross-parameter statistics | + +## Python + +| Project | Description | +|---------|-------------| +| [session_explorer](python/session_explorer/) | Interactive CLI for browsing sessions and exporting to CSV/pickle | +| [atlas_automation](python/atlas_automation/) | ATLAS Automation API controller (connection pattern, stubbed) | + +## MATLAB + +| Project | Description | +|---------|-------------| +| [session_analysis](matlab/session_analysis/) | Full analysis workflow: load, extract, statistics, plot, save to .mat | diff --git a/projects/csharp/BatchExporter/BatchExporter.csproj b/projects/csharp/BatchExporter/BatchExporter.csproj new file mode 100644 index 0000000..af71626 --- /dev/null +++ b/projects/csharp/BatchExporter/BatchExporter.csproj @@ -0,0 +1,16 @@ + + + + Exe + + + + + + + + + + + + diff --git a/projects/csharp/BatchExporter/CsvWriter.cs b/projects/csharp/BatchExporter/CsvWriter.cs new file mode 100644 index 0000000..44c7125 --- /dev/null +++ b/projects/csharp/BatchExporter/CsvWriter.cs @@ -0,0 +1,105 @@ +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +namespace BatchExporter; + +///

+/// Writes parameter data to CSV format using streaming writes for efficiency. +/// +public sealed class CsvWriter : IDisposable +{ + private readonly StreamWriter _writer; + private readonly string _delimiter; + private readonly string _timestampFormat; + private readonly MissingDataBehavior _missingBehavior; + + /// + /// Creates a new CSV writer targeting the specified file path. + /// + public CsvWriter(string filePath, string delimiter, string timestampFormat, MissingDataBehavior missingBehavior) + { + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory)) + Directory.CreateDirectory(directory); + + _writer = new StreamWriter(filePath, append: false, encoding: System.Text.Encoding.UTF8); + _delimiter = delimiter; + _timestampFormat = timestampFormat; + _missingBehavior = missingBehavior; + } + + /// Total rows written (excluding header). + public long RowsWritten { get; private set; } + + /// + /// Writes the header row with timestamp and parameter identifier columns. + /// + public void WriteHeader(string timestampColumnName, IReadOnlyList parameterIdentifiers) + { + _writer.Write(timestampColumnName); + foreach (var id in parameterIdentifiers) + { + _writer.Write(_delimiter); + _writer.Write(QuoteIfNeeded(id)); + } + _writer.WriteLine(); + } + + /// + /// Writes a single row of data. Each parameter provides its value and status. + /// + /// Nanosecond timestamp to format. + /// Session date for converting nanoseconds to wall time. + /// Value per parameter. + /// Data status per parameter. + /// true if the row was written; false if skipped due to missing data. + public bool WriteRow(long timestamp, DateTime sessionDate, double[] values, DataStatusType[] statuses) + { + // Check if any value is missing and we should skip + if (_missingBehavior == MissingDataBehavior.Skip) + { + for (var i = 0; i < statuses.Length; i++) + { + if (statuses[i] != DataStatusType.Sample) + return false; + } + } + + // Format timestamp as wall-clock time + var timeOfDay = TimeSpan.FromTicks(timestamp / 100); // ns → ticks + var dateTime = sessionDate.Date + timeOfDay; + _writer.Write(dateTime.ToString(_timestampFormat)); + + for (var i = 0; i < values.Length; i++) + { + _writer.Write(_delimiter); + + if (statuses[i] != DataStatusType.Sample) + { + _writer.Write(_missingBehavior == MissingDataBehavior.WriteNaN ? "NaN" : "0"); + } + else + { + _writer.Write(values[i].ToString("G")); + } + } + + _writer.WriteLine(); + RowsWritten++; + return true; + } + + private string QuoteIfNeeded(string value) + { + if (value.Contains(_delimiter) || value.Contains('"') || value.Contains('\n')) + return $"\"{value.Replace("\"", "\"\"")}\""; + return value; + } + + /// + public void Dispose() + { + _writer.Flush(); + _writer.Dispose(); + } +} diff --git a/projects/csharp/BatchExporter/ExportConfig.cs b/projects/csharp/BatchExporter/ExportConfig.cs new file mode 100644 index 0000000..7db8376 --- /dev/null +++ b/projects/csharp/BatchExporter/ExportConfig.cs @@ -0,0 +1,60 @@ +namespace BatchExporter; + +/// +/// Configuration for the batch export process, bound from appsettings.json. +/// +public sealed class ExportConfig +{ + /// + /// Output directory for CSV files. Empty defaults to ./exports/. + /// + public string OutputDirectory { get; set; } = ""; + + /// Parameter identifiers to export. Empty exports all parameters. + public string[] ParameterIdentifiers { get; set; } = ["Temperature:Sensors", "Pressure:Inlet", "Speed:Shaft"]; + + /// + /// Filter sessions recorded on or after this date. Empty means no lower bound. + /// + public string DateRangeStart { get; set; } = ""; + + /// + /// Filter sessions recorded on or before this date. Empty means no upper bound. + /// + public string DateRangeEnd { get; set; } = ""; + + /// CSV field delimiter. + public string CsvDelimiter { get; set; } = ","; + + /// Timestamp format string for the first CSV column. + public string TimestampFormat { get; set; } = "yyyy-MM-dd HH:mm:ss.fff"; + + /// How to handle missing data points. + public MissingDataBehavior MissingDataBehavior { get; set; } = MissingDataBehavior.WriteNaN; + + /// Maximum samples to read per PDA call (for chunking large sessions). + public int MaxSamplesPerRead { get; set; } = 32767; + + /// + /// Resolves the output directory, falling back to ./exports/ if empty. + /// + public string ResolvedOutputDirectory => + string.IsNullOrWhiteSpace(OutputDirectory) + ? Path.Combine(AppContext.BaseDirectory, "exports") + : OutputDirectory; +} + +/// +/// Behaviour when a parameter sample has a non-Sample data status. +/// +public enum MissingDataBehavior +{ + /// Skip the row entirely. + Skip, + + /// Write "NaN" in place of the missing value. + WriteNaN, + + /// Write "0" in place of the missing value. + WriteZero +} diff --git a/projects/csharp/BatchExporter/Program.cs b/projects/csharp/BatchExporter/Program.cs new file mode 100644 index 0000000..1b46bce --- /dev/null +++ b/projects/csharp/BatchExporter/Program.cs @@ -0,0 +1,159 @@ +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Query; +using Microsoft.Extensions.Configuration; +using SqlRace.Examples.Core; +using BatchExporter; + +// ─── Configuration ─────────────────────────────────────────── +var configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + +var exportConfig = new ExportConfig(); +configuration.GetSection("Export").Bind(exportConfig); + +var connectionString = ConnectionStringProvider.Resolve( + configuration.GetConnectionString("SqlRace")); + +// ─── Initialise ────────────────────────────────────────────── +SqlRaceBootstrapper.Initialize(); +Console.WriteLine("SQL Race Batch Exporter"); +Console.WriteLine($"Connection: {connectionString}"); +Console.WriteLine($"Output: {exportConfig.ResolvedOutputDirectory}"); +Console.WriteLine($"Parameters: {string.Join(", ", exportConfig.ParameterIdentifiers)}"); +Console.WriteLine($"Missing: {exportConfig.MissingDataBehavior}"); +Console.WriteLine(); + +// ─── Query sessions ────────────────────────────────────────── +var queryManager = QueryManager.CreateQueryManager(connectionString); + +// Apply date range filter if configured +if (!string.IsNullOrWhiteSpace(exportConfig.DateRangeStart) || + !string.IsNullOrWhiteSpace(exportConfig.DateRangeEnd)) +{ + var composite = new CompositeFilter(CombineType.AND); + + if (DateTime.TryParse(exportConfig.DateRangeStart, out var startDate)) + { + composite.Add(new ScalarFilter("TimeOfRecording", MatchingRule.GreaterThan, + startDate.ToString("O"), ignoreCase: true)); + } + + if (DateTime.TryParse(exportConfig.DateRangeEnd, out var endDate)) + { + composite.Add(new ScalarFilter("TimeOfRecording", MatchingRule.LessThan, + endDate.ToString("O"), ignoreCase: true)); + } + + queryManager.Filter = composite; + Console.WriteLine($"Date filter: {exportConfig.DateRangeStart} → {exportConfig.DateRangeEnd}"); +} + +var sessions = queryManager.ExecuteQuery().ToList(); + +if (sessions.Count == 0) +{ + Console.WriteLine("No sessions found matching the filter criteria."); + Console.WriteLine("Tip: Run the GettingStarted or ServerListenerRecorder project to create sessions."); + return; +} + +Console.WriteLine($"Found {sessions.Count} session(s) to export.\n"); + +// ─── Export each session ───────────────────────────────────── +var sessionManager = SessionManager.CreateSessionManager(); +var totalFiles = 0; +var totalSamples = 0L; + +for (var sessionIdx = 0; sessionIdx < sessions.Count; sessionIdx++) +{ + var summary = sessions[sessionIdx]; + Console.WriteLine($"[{sessionIdx + 1}/{sessions.Count}] {summary.Identifier} ({summary.Key})"); + + using var clientSession = sessionManager.Load(summary.Key, connectionString); + var session = clientSession.Session; + + // Resolve parameter list (use configured list, or all if empty) + var paramIds = exportConfig.ParameterIdentifiers.Length > 0 + ? exportConfig.ParameterIdentifiers + : session.Parameters.Select(p => p.Identifier).ToArray(); + + // Filter to parameters that actually exist in this session + var availableParams = paramIds + .Where(id => session.Parameters.Any(p => p.Identifier == id)) + .ToList(); + + if (availableParams.Count == 0) + { + Console.WriteLine(" Skipped: no matching parameters found."); + continue; + } + + // Build output filename + var safeName = string.Join("_", summary.Identifier.Split(Path.GetInvalidFileNameChars())); + var fileName = $"{safeName}_{summary.TimeOfRecording:yyyyMMdd-HHmmss}.csv"; + var filePath = Path.Combine(exportConfig.ResolvedOutputDirectory, fileName); + + using var csv = new CsvWriter(filePath, exportConfig.CsvDelimiter, + exportConfig.TimestampFormat, exportConfig.MissingDataBehavior); + csv.WriteHeader("Timestamp", availableParams); + + // Read all parameters and interleave by timestamp + // For simplicity, use the first parameter's timestamps as the time base + var pdas = new List(); + try + { + foreach (var paramId in availableParams) + pdas.Add(session.CreateParameterDataAccess(paramId)); + + // Read first parameter to get timestamp array + var baseSamples = pdas[0].GetSamplesBetween(session.StartTime, session.EndTime); + var allSamples = new List { baseSamples }; + + for (var i = 1; i < pdas.Count; i++) + allSamples.Add(pdas[i].GetSamplesBetween(session.StartTime, session.EndTime)); + + var rowCount = baseSamples.SampleCount; + var values = new double[availableParams.Count]; + var statuses = new DataStatusType[availableParams.Count]; + + for (var row = 0; row < rowCount; row++) + { + for (var col = 0; col < availableParams.Count; col++) + { + if (row < allSamples[col].SampleCount) + { + values[col] = allSamples[col].Data[row]; + statuses[col] = allSamples[col].DataStatus[row]; + } + else + { + values[col] = double.NaN; + statuses[col] = DataStatusType.Missing; + } + } + + csv.WriteRow(baseSamples.Timestamp[row], summary.TimeOfRecording, values, statuses); + } + + totalSamples += csv.RowsWritten * availableParams.Count; + totalFiles++; + Console.WriteLine($" Exported: {availableParams.Count} params, {csv.RowsWritten} rows → {filePath}"); + } + finally + { + foreach (var pda in pdas) + pda.Dispose(); + } +} + +// ─── Summary ───────────────────────────────────────────────── +Console.WriteLine(); +Console.WriteLine($"Export complete."); +Console.WriteLine($" Sessions: {sessions.Count}"); +Console.WriteLine($" Files: {totalFiles}"); +Console.WriteLine($" Samples: {totalSamples:N0}"); +Console.WriteLine($" Output: {exportConfig.ResolvedOutputDirectory}"); diff --git a/projects/csharp/BatchExporter/README.md b/projects/csharp/BatchExporter/README.md new file mode 100644 index 0000000..6b21ec7 --- /dev/null +++ b/projects/csharp/BatchExporter/README.md @@ -0,0 +1,81 @@ +# BatchExporter + +A console application that queries for sessions matching criteria and exports parameter data from each session to CSV files. + +## Use Case + +This is the most common integration pattern for non-real-time analysis: extract data from SQL Race into CSV for consumption by Python, R, Excel, or other external tools. + +## Prerequisites + +- .NET 8 SDK +- MESL.SQLRace.API NuGet package +- A database with sessions (run GettingStarted or ServerListenerRecorder first) + +## Build and Run + +```bash +cd projects/csharp +dotnet run --project BatchExporter +``` + +## Configuration + +Edit `appsettings.json`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `ConnectionStrings:SqlRace` | (empty → SQLite in temp) | SQL Race connection string | +| `Export:OutputDirectory` | (empty → `./exports/`) | Output directory for CSV files | +| `Export:ParameterIdentifiers` | `["Temperature:Sensors", ...]` | Parameters to export (empty = all) | +| `Export:DateRangeStart` | (empty) | Filter: sessions recorded after this date | +| `Export:DateRangeEnd` | (empty) | Filter: sessions recorded before this date | +| `Export:CsvDelimiter` | `,` | Field delimiter | +| `Export:TimestampFormat` | `yyyy-MM-dd HH:mm:ss.fff` | Timestamp column format | +| `Export:MissingDataBehavior` | `WriteNaN` | How to handle missing data: `Skip`, `WriteNaN`, `WriteZero` | +| `Export:MaxSamplesPerRead` | `32767` | Chunk size for reading large sessions | + +## Output Format + +Each session produces one CSV file named `{Identifier}_{Date}.csv`: + +```csv +Timestamp,Temperature:Sensors,Pressure:Inlet,Speed:Shaft +2026-03-12 10:00:00.000,75.00,3.50,3000 +2026-03-12 10:00:00.010,75.30,3.52,3015 +... +``` + +## Handling Large Sessions + +For sessions with millions of samples, the `MaxSamplesPerRead` setting controls the chunk size passed to `GetSamplesBetween`. The CSV writer uses streaming writes to avoid building the entire output in memory. + +## Extending + +To add other output formats (Parquet, HDF5, JSON): +1. Create a new writer class implementing the same interface as `CsvWriter` +2. Add a format selection option to `ExportConfig` +3. Instantiate the appropriate writer in `Program.cs` + +## Expected Output + +``` +SQL Race Batch Exporter +Connection: DbEngine=SQLite;Data Source=/tmp/sqlrace-examples.ssn2; +Output: /path/to/exports +Parameters: Temperature:Sensors, Pressure:Inlet, Speed:Shaft + +Found 3 session(s) to export. + +[1/3] ServerListener-Demo-20260312-143022 () + Exported: 3 params, 4200 rows → exports/ServerListener-Demo_20260312-143022.csv +[2/3] GettingStarted Demo () + Exported: 2 params, 100 rows → exports/GettingStarted-Demo_20260312-120000.csv +... + +Export complete. + Sessions: 3 + Files: 3 + Samples: 12,900 + Output: /path/to/exports +``` diff --git a/projects/csharp/BatchExporter/appsettings.json b/projects/csharp/BatchExporter/appsettings.json new file mode 100644 index 0000000..19e5d6a --- /dev/null +++ b/projects/csharp/BatchExporter/appsettings.json @@ -0,0 +1,15 @@ +{ + "ConnectionStrings": { + "SqlRace": "" + }, + "Export": { + "OutputDirectory": "", + "ParameterIdentifiers": ["Temperature:Sensors", "Pressure:Inlet", "Speed:Shaft"], + "DateRangeStart": "", + "DateRangeEnd": "", + "CsvDelimiter": ",", + "TimestampFormat": "yyyy-MM-dd HH:mm:ss.fff", + "MissingDataBehavior": "WriteNaN", + "MaxSamplesPerRead": 32767 + } +} diff --git a/projects/csharp/Directory.Build.props b/projects/csharp/Directory.Build.props new file mode 100644 index 0000000..ab7c948 --- /dev/null +++ b/projects/csharp/Directory.Build.props @@ -0,0 +1,12 @@ + + + + net8.0 + enable + enable + true + true + $(NoWarn);NU1701;CA1416 + + + diff --git a/projects/csharp/FunctionLibrary/FunctionLibrary.csproj b/projects/csharp/FunctionLibrary/FunctionLibrary.csproj new file mode 100644 index 0000000..1cfe0b7 --- /dev/null +++ b/projects/csharp/FunctionLibrary/FunctionLibrary.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/projects/csharp/FunctionLibrary/README.md b/projects/csharp/FunctionLibrary/README.md new file mode 100644 index 0000000..94530be --- /dev/null +++ b/projects/csharp/FunctionLibrary/README.md @@ -0,0 +1,63 @@ +# FunctionLibrary + +A class library providing reusable custom .NET functions for SQL Race. These functions create calculated parameters that appear alongside recorded data in ATLAS. + +## What are .NET Functions? + +SQL Race supports two types of calculated channels: +- **FDL functions** — simple formulas written in FDL syntax (see `snippets/csharp/functions/fdl-function-basic.cs`) +- **.NET functions** — full C# classes implementing `IDotNetFunction`, enabling complex calculations, state management, and cross-parameter lookups + +## Functions in this library + +| Class | Description | Pattern | +|-------|-------------|---------| +| `UnitConverterFunction` | Celsius → Fahrenheit | Basic: single input, single output | +| `RollingAverageFunction` | Time-windowed rolling average | Stateful: maintains a sample queue across Execute calls | +| `StatisticsFunction` | Temperature/Pressure ratio | Advanced: uses a PDA inside Execute for cross-parameter lookup | + +## How .NET Functions Work + +1. The function class implements `IDotNetFunction` and is decorated with `[Export(typeof(IDotNetFunction))]` +2. `Initialize` is called once: creates the function definition, binds inputs/outputs, and builds +3. `Execute` is called per sample: reads input values, computes, writes output values +4. The function's output parameter appears in the session's parameter list + +## Deployment + +To use these functions in ATLAS: + +1. Build the library: `dotnet build -c Release` +2. Copy the output DLL to the ATLAS functions directory +3. Restart ATLAS — the functions are discovered via MEF + +```bash +dotnet build -c Release +# Copy bin/Release/net8.0/FunctionLibrary.dll to ATLAS functions directory +``` + +## Building + +```bash +cd projects/csharp +dotnet build FunctionLibrary +``` + +Note: This is a class library, not a console app — it does not run standalone. See `snippets/csharp/functions/` for runnable examples. + +## Function Modes + +- **Instantaneous**: Execute is called for each sample timestamp independently (default) +- **LeadingEdge**: Execute is called only when new data arrives at the leading edge + +All functions in this library use instantaneous mode. + +## Debugging + +To debug a .NET function: +1. Set breakpoints in the function code +2. Attach the debugger to the ATLAS process +3. Load a session that triggers the function +4. Execute will be called for each sample + +Alternatively, write unit tests that create a session, add the function, write data, and verify the output via a PDA. diff --git a/projects/csharp/FunctionLibrary/RollingAverageFunction.cs b/projects/csharp/FunctionLibrary/RollingAverageFunction.cs new file mode 100644 index 0000000..55736cd --- /dev/null +++ b/projects/csharp/FunctionLibrary/RollingAverageFunction.cs @@ -0,0 +1,65 @@ +using System.ComponentModel.Composition; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Functions; +using MESL.SqlRace.Domain.Functions.DotNet; + +namespace FunctionLibrary; + +/// +/// A .NET function that computes a rolling average over a configurable time window. +/// Demonstrates state management and timestamp-based windowing. +/// +/// +/// .NET functions are discovered via MEF [Export] and initialised by the function manager. +/// The framework handles execution scheduling; the Initialize method configures the +/// function definition with input/output bindings. +/// +[Export(typeof(IDotNetFunction))] +[Serializable] +public class RollingAverageFunction : IDotNetFunction +{ + private const string InputParameter = "Temperature:Sensors"; + private const string OutputParameter = "TemperatureAvg:Calculated"; + private const long WindowNs = 1_000_000_000L; // 1 second window + + [NonSerialized] + private readonly Queue<(long Timestamp, double Value)> _window = new(); + + /// + public string Name => "RollingAverage"; + + /// + /// Initialises the function definition with input/output parameter binding. + /// The framework discovers this function via MEF and calls Initialize at load time. + /// + public void Initialize(IFunctionManager functionManager) + { + // TODO: Verify — CreateFunctionDefinition parameter and IFunctionDefinition + // configuration API may differ across SQL Race versions. + var definition = functionManager.CreateFunctionDefinition( + DotNetFunctionConstants.UniqueId); + + // Configure input/output via IFunctionDefinition properties. + definition.InputParameterIdentifiers.Add(InputParameter); + + // TODO: Verify — OutputParameterDefinitions configuration pattern. + // Output parameter setup varies by API version; consult the SQL Race + // function authoring guide for the installed version. + + var buildResult = functionManager.Build(definition); + if (buildResult?.Errors?.Any() == true) + { + throw new InvalidOperationException( + $"RollingAverageFunction build failed: {string.Join("; ", buildResult.Errors)}"); + } + } + + /// + /// Executes the function. Called by the framework for each execution cycle. + /// + public void Execute(IExecutionContext context) + { + // TODO: Verify — IExecutionContext usage pattern for reading inputs + // and writing outputs varies by SQL Race version. + } +} diff --git a/projects/csharp/FunctionLibrary/StatisticsFunction.cs b/projects/csharp/FunctionLibrary/StatisticsFunction.cs new file mode 100644 index 0000000..588a96a --- /dev/null +++ b/projects/csharp/FunctionLibrary/StatisticsFunction.cs @@ -0,0 +1,60 @@ +using System.ComponentModel.Composition; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Functions; +using MESL.SqlRace.Domain.Functions.DotNet; + +namespace FunctionLibrary; + +/// +/// A .NET function that computes the ratio of temperature to pressure, +/// demonstrating cross-parameter calculations. +/// +/// +/// .NET functions are discovered via MEF [Export] and initialised by the function manager. +/// The framework handles execution scheduling; the Initialize method configures the +/// function definition with input/output bindings. +/// +[Export(typeof(IDotNetFunction))] +[Serializable] +public class StatisticsFunction : IDotNetFunction +{ + private const string InputParameter = "Temperature:Sensors"; + private const string LookupParameter = "Pressure:Inlet"; + private const string OutputParameter = "TempPressRatio:Calculated"; + + /// + public string Name => "Statistics"; + + /// + /// Initialises the function, configuring input parameters for cross-parameter calculation. + /// + public void Initialize(IFunctionManager functionManager) + { + // TODO: Verify — CreateFunctionDefinition parameter and IFunctionDefinition + // configuration API may differ across SQL Race versions. + var definition = functionManager.CreateFunctionDefinition( + DotNetFunctionConstants.UniqueId); + + definition.InputParameterIdentifiers.Add(InputParameter); + definition.InputParameterIdentifiers.Add(LookupParameter); + + // TODO: Verify — OutputParameterDefinitions configuration pattern. + // Output parameter setup (unit: degC/bar, format: %6.2f, range: 0 to 1000) + // varies by API version; consult the SQL Race function authoring guide. + + var buildResult = functionManager.Build(definition); + if (buildResult?.Errors?.Any() == true) + { + throw new InvalidOperationException( + $"StatisticsFunction build failed: {string.Join("; ", buildResult.Errors)}"); + } + } + + /// + /// Executes the function. Called by the framework for each execution cycle. + /// + public void Execute(IExecutionContext context) + { + // TODO: Verify — IExecutionContext usage pattern for cross-parameter ratio. + } +} diff --git a/projects/csharp/FunctionLibrary/UnitConverterFunction.cs b/projects/csharp/FunctionLibrary/UnitConverterFunction.cs new file mode 100644 index 0000000..2760cce --- /dev/null +++ b/projects/csharp/FunctionLibrary/UnitConverterFunction.cs @@ -0,0 +1,58 @@ +using System.ComponentModel.Composition; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Functions; +using MESL.SqlRace.Domain.Functions.DotNet; + +namespace FunctionLibrary; + +/// +/// A .NET function that converts temperature from Celsius to Fahrenheit. +/// Demonstrates the basic IDotNetFunction pattern: single input, single output. +/// +/// +/// .NET functions are discovered via MEF [Export] and initialised by the function manager. +/// The framework handles execution scheduling; the Initialize method configures the +/// function definition with input/output bindings. +/// +[Export(typeof(IDotNetFunction))] +[Serializable] +public class UnitConverterFunction : IDotNetFunction +{ + private const string InputParameter = "Temperature:Sensors"; + private const string OutputParameter = "TemperatureFahrenheit:Calculated"; + + /// + public string Name => "UnitConverter"; + + /// + /// Initialises the function by creating its definition and binding parameters. + /// + public void Initialize(IFunctionManager functionManager) + { + // TODO: Verify — CreateFunctionDefinition parameter and IFunctionDefinition + // configuration API may differ across SQL Race versions. + var definition = functionManager.CreateFunctionDefinition( + DotNetFunctionConstants.UniqueId); + + definition.InputParameterIdentifiers.Add(InputParameter); + + // TODO: Verify — OutputParameterDefinitions configuration pattern. + // Output parameter setup (unit: degF, format: %5.1f, range: -40 to 1000) + // varies by API version; consult the SQL Race function authoring guide. + + var buildResult = functionManager.Build(definition); + if (buildResult?.Errors?.Any() == true) + { + throw new InvalidOperationException( + $"UnitConverterFunction build failed: {string.Join("; ", buildResult.Errors)}"); + } + } + + /// + /// Executes the function. Called by the framework for each execution cycle. + /// + public void Execute(IExecutionContext context) + { + // TODO: Verify — IExecutionContext usage pattern for C-to-F conversion. + } +} diff --git a/projects/csharp/GettingStarted/GettingStarted.csproj b/projects/csharp/GettingStarted/GettingStarted.csproj new file mode 100644 index 0000000..79eeea7 --- /dev/null +++ b/projects/csharp/GettingStarted/GettingStarted.csproj @@ -0,0 +1,11 @@ + + + + Exe + + + + + + + diff --git a/projects/csharp/GettingStarted/Program.cs b/projects/csharp/GettingStarted/Program.cs new file mode 100644 index 0000000..83ae998 --- /dev/null +++ b/projects/csharp/GettingStarted/Program.cs @@ -0,0 +1,89 @@ +// ============================================================================ +// Getting Started — using the Core library +// ============================================================================ +// This console app demonstrates the same operations as the getting-started +// snippets, but using the fluent Core library API for cleaner code. +// +// Run: dotnet run --project GettingStarted +// ============================================================================ + +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; +using SqlRace.Examples.Core; + +// --- 1. Initialise (idempotent — safe to call multiple times) --- +SqlRaceBootstrapper.Initialize(); +Console.WriteLine($"SQL Race initialised: {SqlRaceBootstrapper.IsInitialized}"); + +// --- 2. Resolve connection string (uses default SQLite in temp) --- +var connectionString = ConnectionStringProvider.Resolve(); +Console.WriteLine($"Connection: {connectionString}"); + +// --- 3. Create a new session --- +using var clientSession = SessionFactory.CreateSession("Getting Started Demo"); +var session = clientSession.Session; +Console.WriteLine($"Session created: {session.Key}"); + +// --- 4. Add parameters using the fluent builder --- +var temperature = new ParameterBuilder(session) + .WithIdentifier("Temperature:Sensors") + .WithDescription("Inlet temperature sensor") + .InApplicationGroup("Sensors") + .InParameterGroup("Thermal") + .WithFrequency(100, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("degC", "%5.2f") + .WithRange(min: 0, max: 500) + .Build(); + +var pressure = new ParameterBuilder(session) + .WithIdentifier("Pressure:Inlet") + .WithDescription("Inlet pressure sensor") + .InApplicationGroup("Sensors") + .InParameterGroup("Hydraulic") + .WithFrequency(50, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("bar", "%5.3f") + .WithRange(min: 0, max: 10) + .Build(); + +Console.WriteLine($"Parameters: {session.Parameters.Count}"); + +// --- 5. Write sample data --- +var startTime = 36_000_000_000_000L; // 10:00:00.000 +var tempInterval = new Frequency(100, FrequencyUnit.Hz).ToInterval(); +var pressInterval = new Frequency(50, FrequencyUnit.Hz).ToInterval(); + +// 1 second of temperature data at 100 Hz +for (var i = 0; i < 100; i++) +{ + var timestamp = startTime + i * tempInterval; + var value = 20.0 + Math.Sin(i * 0.1) * 5.0; + session.AddChannelData(temperature.ChannelId, timestamp, 1, BitConverter.GetBytes(value)); +} + +// 1 second of pressure data at 50 Hz +for (var i = 0; i < 50; i++) +{ + var timestamp = startTime + i * pressInterval; + var value = 2.5 + Math.Cos(i * 0.2) * 0.5; + session.AddChannelData(pressure.ChannelId, timestamp, 1, BitConverter.GetBytes(value)); +} + +Console.WriteLine("Wrote 100 temperature + 50 pressure samples"); + +// --- 6. Read back and verify --- +using var tempPda = session.CreateParameterDataAccess("Temperature:Sensors"); +var tempSamples = tempPda.GetSamplesBetween(startTime, startTime + 1_000_000_000L); + +using var pressPda = session.CreateParameterDataAccess("Pressure:Inlet"); +var pressSamples = pressPda.GetSamplesBetween(startTime, startTime + 1_000_000_000L); + +Console.WriteLine($"Read back: {tempSamples.SampleCount} temperature, {pressSamples.SampleCount} pressure"); +Console.WriteLine($"Temperature[0] = {tempSamples.Data[0]:F2} degC"); +Console.WriteLine($"Pressure[0] = {pressSamples.Data[0]:F3} bar"); + +Console.WriteLine(); +Console.WriteLine("Done. Use this session key to load it in other examples:"); +Console.WriteLine($" {session.Key}"); diff --git a/projects/csharp/LiveDashboard/DashboardRenderer.cs b/projects/csharp/LiveDashboard/DashboardRenderer.cs new file mode 100644 index 0000000..dd3b600 --- /dev/null +++ b/projects/csharp/LiveDashboard/DashboardRenderer.cs @@ -0,0 +1,84 @@ +namespace LiveDashboard; + +/// +/// Renders a text-based dashboard to the console with in-place updates. +/// +public sealed class DashboardRenderer +{ + private readonly int _pollingIntervalMs; + private bool _firstRender = true; + + /// + /// Represents the current statistics for a single parameter. + /// + public record ParameterStats(string Identifier, double Latest, double Min, double Max, double Mean, int SampleCount); + + public DashboardRenderer(int pollingIntervalMs) + { + _pollingIntervalMs = pollingIntervalMs; + } + + /// + /// Renders the dashboard frame to the console. + /// + public void Render(string sessionName, string sessionState, IReadOnlyList stats) + { + if (_firstRender) + { + Console.Clear(); + _firstRender = false; + } + else + { + Console.SetCursorPosition(0, 0); + } + + var width = Math.Max(Console.WindowWidth, 70); + var innerWidth = width - 4; // Account for border characters + + // Top border + Console.WriteLine($"╔{new string('═', innerWidth)}╗"); + + // Title + var title = "SQL Race Live Dashboard"; + var polling = $"[Polling: {_pollingIntervalMs}ms]"; + var titleLine = $" {title}{new string(' ', Math.Max(1, innerWidth - title.Length - polling.Length - 4))}{polling} "; + Console.WriteLine($"║{Fit(titleLine, innerWidth)}║"); + + // Session info + var sessionLine = $" Session: {sessionName,-30} [{sessionState}]"; + Console.WriteLine($"║{Fit(sessionLine, innerWidth)}║"); + + // Separator + Console.WriteLine($"╠{new string('═', innerWidth)}╣"); + + // Header + var header = $" {"Parameter",-28} {"Latest",10} {"Min",10} {"Max",10} {"Mean",10} "; + Console.WriteLine($"║{Fit(header, innerWidth)}║"); + Console.WriteLine($"║ {new string('─', innerWidth - 4)} ║"); + + // Parameter rows + foreach (var s in stats) + { + var row = $" {s.Identifier,-28} {s.Latest,10:F2} {s.Min,10:F2} {s.Max,10:F2} {s.Mean,10:F2} "; + Console.WriteLine($"║{Fit(row, innerWidth)}║"); + } + + // Fill if few parameters + for (var i = stats.Count; i < 3; i++) + Console.WriteLine($"║{new string(' ', innerWidth)}║"); + + // Footer + Console.WriteLine($"╠{new string('═', innerWidth)}╣"); + var footer = $" Last updated: {DateTime.Now:HH:mm:ss.fff}{new string(' ', Math.Max(1, innerWidth - 50))}Press Ctrl+C to exit "; + Console.WriteLine($"║{Fit(footer, innerWidth)}║"); + Console.WriteLine($"╚{new string('═', innerWidth)}╝"); + } + + private static string Fit(string text, int width) + { + if (text.Length >= width) + return text[..width]; + return text + new string(' ', width - text.Length); + } +} diff --git a/projects/csharp/LiveDashboard/LiveDashboard.csproj b/projects/csharp/LiveDashboard/LiveDashboard.csproj new file mode 100644 index 0000000..af71626 --- /dev/null +++ b/projects/csharp/LiveDashboard/LiveDashboard.csproj @@ -0,0 +1,16 @@ + + + + Exe + + + + + + + + + + + + diff --git a/projects/csharp/LiveDashboard/Program.cs b/projects/csharp/LiveDashboard/Program.cs new file mode 100644 index 0000000..6acd91b --- /dev/null +++ b/projects/csharp/LiveDashboard/Program.cs @@ -0,0 +1,181 @@ +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.Enumerators; +using MESL.SqlRace.Domain.Query; +using Microsoft.Extensions.Configuration; +using SqlRace.Examples.Core; +using LiveDashboard; + +// ─── Configuration ─────────────────────────────────────────── +var configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + +var pollingMs = configuration.GetValue("Dashboard:PollingIntervalMs", 2000); +var windowSize = configuration.GetValue("Dashboard:SampleWindowSize", 100); +var paramIds = configuration.GetSection("Dashboard:ParameterIdentifiers") + .Get() ?? ["Temperature:Sensors", "Pressure:Inlet", "Speed:Shaft"]; +var configuredSessionId = configuration.GetValue("Dashboard:SessionIdentifier"); + +var connectionString = ConnectionStringProvider.Resolve( + configuration.GetConnectionString("SqlRace")); + +// ─── Cancellation (Ctrl+C) ────────────────────────────────── +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => +{ + e.Cancel = true; + cts.Cancel(); +}; + +// ─── Initialise ────────────────────────────────────────────── +SqlRaceBootstrapper.Initialize(); + +// ─── Find most recent session ──────────────────────────────── +var queryManager = QueryManager.CreateQueryManager(connectionString); +var sessions = queryManager.ExecuteQuery().ToList(); + +if (sessions.Count == 0) +{ + Console.WriteLine("No sessions found in the database."); + Console.WriteLine($"Connection: {connectionString}"); + Console.WriteLine("Tip: Run ServerListenerRecorder or GettingStarted to create sessions."); + return; +} + +// Use configured session key, or find the most recent session with data +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession? clientSession = null; +Session? session = null; +dynamic? loadKey = null; // Store the key for reloading + +if (!string.IsNullOrEmpty(configuredSessionId)) +{ + var match = sessions.FirstOrDefault(s => s.Identifier == configuredSessionId); + if (match is null) + { + Console.WriteLine($"Session '{configuredSessionId}' not found. Available:"); + foreach (var s in sessions.OrderByDescending(s => s.TimeOfRecording).Take(5)) + Console.WriteLine($" {s.Identifier} ({s.Key})"); + return; + } + + clientSession = sessionManager.Load(match.Key, connectionString); + session = clientSession.Session; + loadKey = match.Key; + Console.WriteLine($"Loading configured session: {match.Identifier} ({match.Key})"); +} +else +{ + // Try sessions newest-first, skip any with no sample data + foreach (var candidate in sessions.OrderByDescending(s => s.TimeOfRecording)) + { + var tempClient = sessionManager.Load(candidate.Key, connectionString); + var tempSession = tempClient.Session; + + // Check if the first configured parameter has any data + var testParam = paramIds.FirstOrDefault(id => + tempSession.Parameters.Any(p => p.Identifier == id)); + + if (testParam != null) + { + using var testPda = tempSession.CreateParameterDataAccess(testParam); + testPda.GoTo(long.MaxValue); + var testSamples = testPda.GetNextSamples(1, StepDirection.Reverse); + + if (testSamples.SampleCount > 0) + { + clientSession = tempClient; + session = tempSession; + loadKey = candidate.Key; + Console.WriteLine($"Loading session: {candidate.Identifier} ({candidate.Key})"); + break; + } + } + + tempClient.Dispose(); + Console.WriteLine($"Skipping session {candidate.Identifier} (no sample data)"); + } + + if (session == null) + { + Console.WriteLine("No sessions with sample data found."); + return; + } +} + +// ─── Resolve available parameters ──────────────────────────── +var availableParams = paramIds + .Where(id => session.Parameters.Any(p => p.Identifier == id)) + .ToList(); + +if (availableParams.Count == 0) +{ + Console.WriteLine("None of the configured parameters exist in this session."); + Console.WriteLine($"Session parameters: {string.Join(", ", session.Parameters.Select(p => p.Identifier))}"); + return; +} + +// ─── Polling loop ───────────────────────────────────────────── +var renderer = new DashboardRenderer(pollingMs); +var sessionId = session.Identifier; + +while (!cts.Token.IsCancellationRequested) +{ + // Reload session each cycle to pick up newly flushed data + clientSession?.Dispose(); + clientSession = sessionManager.Load(loadKey!, connectionString); + session = clientSession.Session; + + var stats = new List(); + + foreach (var paramId in availableParams) + { + // Create a fresh PDA each cycle so we always see the latest live data + using var pda = session.CreateParameterDataAccess(paramId); + pda.GoTo(long.MaxValue); + var samples = pda.GetNextSamples(windowSize, StepDirection.Reverse); + + if (samples.SampleCount == 0) + { + stats.Add(new DashboardRenderer.ParameterStats( + paramId, double.NaN, double.NaN, double.NaN, double.NaN, 0)); + continue; + } + + var latest = samples.Data[0]; + var min = double.MaxValue; + var max = double.MinValue; + var sum = 0.0; + + for (var j = 0; j < samples.SampleCount; j++) + { + var v = samples.Data[j]; + if (v < min) min = v; + if (v > max) max = v; + sum += v; + } + + stats.Add(new DashboardRenderer.ParameterStats( + paramId, latest, min, max, sum / samples.SampleCount, samples.SampleCount)); + } + + renderer.Render( + sessionId, + session.EndTime > session.StartTime ? "Data" : "Empty", + stats); + + try + { + await Task.Delay(pollingMs, cts.Token); + } + catch (OperationCanceledException) + { + break; + } +} + +Console.SetCursorPosition(0, Console.CursorTop + 1); +Console.WriteLine("Dashboard stopped."); +clientSession?.Dispose(); diff --git a/projects/csharp/LiveDashboard/README.md b/projects/csharp/LiveDashboard/README.md new file mode 100644 index 0000000..9d7636c --- /dev/null +++ b/projects/csharp/LiveDashboard/README.md @@ -0,0 +1,70 @@ +# LiveDashboard + +A console application that connects to a SQL Race session and displays a continuously updating text-based dashboard of parameter values. + +## Use Case + +Real-time monitoring of live sessions during testing — useful trackside, in a control room, or at a test rig. The text-based approach works over SSH and doesn't require a GUI. + +## Prerequisites + +- .NET 8 SDK +- MESL.SQLRace.API NuGet package +- A database with at least one session (run GettingStarted first) + +For live monitoring, run ServerListenerRecorder in one terminal and LiveDashboard in another. + +## Build and Run + +```bash +cd projects/csharp +dotnet run --project LiveDashboard +``` + +Press **Ctrl+C** to stop. + +## Configuration + +Edit `appsettings.json`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `ConnectionStrings:SqlRace` | (empty → SQLite in temp) | SQL Race connection string | +| `Dashboard:PollingIntervalMs` | `2000` | Refresh interval in milliseconds | +| `Dashboard:SampleWindowSize` | `100` | Number of recent samples for min/max/mean | +| `Dashboard:ParameterIdentifiers` | `[...]` | Parameters to display | + +## Architecture + +- **Program.cs** — Entry point, session loading, polling loop +- **DashboardRenderer.cs** — Console rendering with in-place updates via `Console.SetCursorPosition` + +## Polling vs Event-Driven + +This example uses polling for simplicity and compatibility with both live and historical sessions. For event-driven patterns (subscribing to new data as it arrives), see the live-data snippets: +- `snippets/csharp/live-data/subscribe-to-lap-events.cs` +- `snippets/csharp/live-data/subscribe-to-data-events.cs` + +## Adapting for a GUI Application + +The `DashboardRenderer` can be replaced with any UI framework. The polling loop pattern translates directly to: +- **WPF**: `DispatcherTimer` with data binding +- **Avalonia**: `DispatcherTimer` or reactive `Observable.Interval` +- **Blazor**: `Timer` with `InvokeAsync(StateHasChanged)` + +## Expected Output + +``` +╔══════════════════════════════════════════════════════════════════╗ +║ SQL Race Live Dashboard [Polling: 2000ms] ║ +║ Session: ServerListener-Demo-20260312 [Data] ║ +╠══════════════════════════════════════════════════════════════════╣ +║ Parameter Latest Min Max ║ +║ ────────────────────────────────────────────────────────────── ║ +║ Temperature:Sensors 87.30 62.10 94.50 ║ +║ Pressure:Inlet 2.41 2.10 2.89 ║ +║ Speed:Shaft 1420.00 1380.00 1510.00 ║ +╠══════════════════════════════════════════════════════════════════╣ +║ Last updated: 14:30:45.123 Press Ctrl+C to exit║ +╚══════════════════════════════════════════════════════════════════╝ +``` diff --git a/projects/csharp/LiveDashboard/appsettings.json b/projects/csharp/LiveDashboard/appsettings.json new file mode 100644 index 0000000..a24c72f --- /dev/null +++ b/projects/csharp/LiveDashboard/appsettings.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + "SqlRace": "" + }, + "Dashboard": { + "SessionIdentifier": "", + "PollingIntervalMs": 2000, + "SampleWindowSize": 100, + "ParameterIdentifiers": ["RPM:Turbine", "Speed:Shaft", "Temperature:Sensors", "Pressure:Inlet", "Voltage:Battery"] + } +} diff --git a/projects/csharp/ServerListenerRecorder/Program.cs b/projects/csharp/ServerListenerRecorder/Program.cs new file mode 100644 index 0000000..5092e1d --- /dev/null +++ b/projects/csharp/ServerListenerRecorder/Program.cs @@ -0,0 +1,154 @@ +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; +using Microsoft.Extensions.Configuration; +using SqlRace.Examples.Core; +using ServerListenerRecorder; + +// ─── Configuration ─────────────────────────────────────────── +var configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + +var recorderConfig = new RecorderConfig(); +configuration.GetSection("Recorder").Bind(recorderConfig); + +var connectionString = ConnectionStringProvider.Resolve( + configuration.GetConnectionString("SqlRace")); + +// ─── Cancellation (Ctrl+C) ────────────────────────────────── +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => +{ + e.Cancel = true; + cts.Cancel(); + Console.WriteLine("\nShutdown requested..."); +}; + +// ─── Initialise SQL Race ───────────────────────────────────── +SqlRaceBootstrapper.Initialize(); +Console.WriteLine("SQL Race initialised"); +Console.WriteLine($"Connection: {connectionString}"); +Console.WriteLine($"Listener: {recorderConfig.ListenerIpAddress}:{recorderConfig.ListenerPort}"); + +// ─── Configure Server Listener ─────────────────────────────── +// TODO: Verify — Core.ConfigureServer() and RecordersConfiguration API signatures +// The server listener receives data pushed from ATLAS or another telemetry source. +// In a real deployment, the server listener port in this application and the +// corresponding port in ATLAS (Tools → SqlRace → Settings) must be different. +Console.WriteLine($"Data source: {recorderConfig.ResolvedDataSourcePath}"); + +// ─── Create Session ────────────────────────────────────────── +var sessionName = $"{recorderConfig.SessionIdentifier}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; +IClientSession? clientSession = null; + +try +{ + clientSession = SessionFactory.CreateSession(sessionName, connectionString); + var session = clientSession.Session; + Console.WriteLine($"Session created: {session.Key}"); + Console.WriteLine($" Name: {sessionName}"); + + // ─── Create Parameters ─────────────────────────────────── + var builders = new[] + { + new ParameterBuilder(session) + .WithIdentifier("Temperature:Sensors") + .WithDescription("Inlet temperature sensor") + .InApplicationGroup("Sensors").InParameterGroup("Thermal") + .WithFrequency(100, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("degC", "%5.2f") + .WithRange(min: 0, max: 500), + + new ParameterBuilder(session) + .WithIdentifier("Pressure:Inlet") + .WithDescription("Inlet pressure transducer") + .InApplicationGroup("Sensors").InParameterGroup("Hydraulic") + .WithFrequency(50, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("bar", "%5.3f") + .WithRange(min: 0, max: 10), + + new ParameterBuilder(session) + .WithIdentifier("Speed:Shaft") + .WithDescription("Main shaft rotational speed") + .InApplicationGroup("Powertrain").InParameterGroup("Drivetrain") + .WithFrequency(200, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("rpm", "%6.0f") + .WithRange(min: 0, max: 10000), + }; + + var parameters = ParameterBuilder.BuildBatch(session, builders); + Console.WriteLine($" Parameters: {parameters.Count} configured"); + + // ─── Data Generation Loop ──────────────────────────────── + // In production, this loop would be replaced by data arriving + // via the server listener. Here we simulate with sine waves. + var baseTime = 36_000_000_000_000L; // 10:00:00.000 + var tempInterval = new Frequency(100, FrequencyUnit.Hz).ToInterval(); + var pressInterval = new Frequency(50, FrequencyUnit.Hz).ToInterval(); + var speedInterval = new Frequency(200, FrequencyUnit.Hz).ToInterval(); + + Console.WriteLine("\nRecording simulated data (Ctrl+C to stop)...\n"); + + var sampleIndex = 0L; + var writeIntervalMs = 100; // Write batch every 100ms + + while (!cts.Token.IsCancellationRequested) + { + var batchStart = sampleIndex; + + // Temperature: 100 Hz → 10 samples per 100ms batch + for (var i = 0; i < 10; i++) + { + var ts = baseTime + (sampleIndex * 10 + i) * tempInterval; + var value = 75.0 + 15.0 * Math.Sin((sampleIndex * 10 + i) * 0.02); + session.AddChannelData(parameters[0].ChannelId, ts, 1, BitConverter.GetBytes(value)); + } + + // Pressure: 50 Hz → 5 samples per 100ms batch + for (var i = 0; i < 5; i++) + { + var ts = baseTime + (sampleIndex * 5 + i) * pressInterval; + var value = 3.5 + 0.8 * Math.Sin((sampleIndex * 5 + i) * 0.03); + session.AddChannelData(parameters[1].ChannelId, ts, 1, BitConverter.GetBytes(value)); + } + + // Speed: 200 Hz → 20 samples per 100ms batch + for (var i = 0; i < 20; i++) + { + var ts = baseTime + (sampleIndex * 20 + i) * speedInterval; + var value = 3000.0 + 500.0 * Math.Sin((sampleIndex * 20 + i) * 0.01); + session.AddChannelData(parameters[2].ChannelId, ts, 1, BitConverter.GetBytes(value)); + } + + sampleIndex++; + Console.WriteLine( + $" Batch {sampleIndex}: Temp=10 Press=5 Speed=20 samples " + + $"(total: {sampleIndex * 35})"); + + try + { + await Task.Delay(writeIntervalMs, cts.Token); + } + catch (OperationCanceledException) + { + break; + } + } + + Console.WriteLine($"\nRecording stopped. Total batches: {sampleIndex}"); +} +catch (Exception ex) +{ + Console.WriteLine($"ERROR: {ex.Message}"); +} +finally +{ + clientSession?.Dispose(); + Console.WriteLine("Session closed."); +} diff --git a/projects/csharp/ServerListenerRecorder/README.md b/projects/csharp/ServerListenerRecorder/README.md new file mode 100644 index 0000000..386df48 --- /dev/null +++ b/projects/csharp/ServerListenerRecorder/README.md @@ -0,0 +1,78 @@ +# ServerListenerRecorder + +A console application that records telemetry data to a local SQLite database, demonstrating the pattern used with the SQL Race Server Listener. + +## What is a Server Listener? + +In production deployments, SQL Race can receive live telemetry data pushed from ATLAS or another data source via a Server Listener. The listener accepts data over a network socket and writes it to a SQL Race session in real time. + +This example simulates that pattern by generating sine wave data and writing it to a session at realistic intervals. + +## When to use this pattern + +- Receiving telemetry from a test rig or vehicle +- Recording data pushed from ATLAS to a local database +- Building a data acquisition pipeline that writes to SQL Race + +## Prerequisites + +- .NET 8 SDK +- MESL.SQLRace.API NuGet package + +## Build and Run + +```bash +cd projects/csharp +dotnet run --project ServerListenerRecorder +``` + +Press **Ctrl+C** to stop recording gracefully. + +## Configuration + +Edit `appsettings.json` or set environment variables: + +| Setting | Default | Description | +|---------|---------|-------------| +| `ConnectionStrings:SqlRace` | (empty → SQLite in temp) | SQL Race connection string | +| `Recorder:ListenerIpAddress` | `127.0.0.1` | IP to listen on | +| `Recorder:ListenerPort` | `6565` | Port to listen on | +| `Recorder:DataSourcePath` | (empty → temp dir) | Directory for data files | +| `Recorder:SessionIdentifier` | `ServerListener-Demo` | Session name prefix | +| `Recorder:DbEngine` | `SQLite` | Database engine | + +## Network Notes + +When using a real Server Listener: +- The listener port in this application and the Server Listener port in ATLAS (Tools > SqlRace > Settings) **must be different** +- Ensure your firewall allows inbound connections on the configured port +- For remote connections, use the machine's network IP instead of `127.0.0.1` + +## Architecture + +``` +Program.cs → Entry point, config, cancellation, main loop +RecorderConfig.cs → Configuration POCO bound from appsettings.json +Core library → SqlRaceBootstrapper, SessionFactory, ParameterBuilder +``` + +## Expected Output + +``` +SQL Race initialised +Connection: DbEngine=SQLite;Data Source=/tmp/sqlrace-examples.ssn2; +Listener: 127.0.0.1:6565 +Session created: + Name: ServerListener-Demo-20260312-143022 + Parameters: 3 configured + +Recording simulated data (Ctrl+C to stop)... + + Batch 1: Temp=10 Press=5 Speed=20 samples (total: 35) + Batch 2: Temp=10 Press=5 Speed=20 samples (total: 70) + ... +^C +Shutdown requested... +Recording stopped. Total batches: 42 +Session closed. +``` diff --git a/projects/csharp/ServerListenerRecorder/RecorderConfig.cs b/projects/csharp/ServerListenerRecorder/RecorderConfig.cs new file mode 100644 index 0000000..78c95a7 --- /dev/null +++ b/projects/csharp/ServerListenerRecorder/RecorderConfig.cs @@ -0,0 +1,32 @@ +namespace ServerListenerRecorder; + +/// +/// Configuration for the server listener recorder, bound from appsettings.json. +/// +public sealed class RecorderConfig +{ + /// IP address to listen on for incoming telemetry data. + public string ListenerIpAddress { get; set; } = "127.0.0.1"; + + /// Port to listen on for incoming telemetry data. + public int ListenerPort { get; set; } = 6565; + + /// + /// Path to the data source directory. Empty defaults to the system temp directory. + /// + public string DataSourcePath { get; set; } = ""; + + /// Session identifier prefix for recorded sessions. + public string SessionIdentifier { get; set; } = "ServerListener-Demo"; + + /// Database engine: "SQLite" or "SqlServer". + public string DbEngine { get; set; } = "SQLite"; + + /// + /// Resolves the data source path, falling back to temp directory if empty. + /// + public string ResolvedDataSourcePath => + string.IsNullOrWhiteSpace(DataSourcePath) + ? Path.GetTempPath() + : DataSourcePath; +} diff --git a/projects/csharp/ServerListenerRecorder/ServerListenerRecorder.csproj b/projects/csharp/ServerListenerRecorder/ServerListenerRecorder.csproj new file mode 100644 index 0000000..af71626 --- /dev/null +++ b/projects/csharp/ServerListenerRecorder/ServerListenerRecorder.csproj @@ -0,0 +1,16 @@ + + + + Exe + + + + + + + + + + + + diff --git a/projects/csharp/ServerListenerRecorder/appsettings.json b/projects/csharp/ServerListenerRecorder/appsettings.json new file mode 100644 index 0000000..37afa66 --- /dev/null +++ b/projects/csharp/ServerListenerRecorder/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "SqlRace": "" + }, + "Recorder": { + "ListenerIpAddress": "127.0.0.1", + "ListenerPort": 6565, + "DataSourcePath": "", + "SessionIdentifier": "ServerListener-Demo", + "DbEngine": "SQLite" + } +} diff --git a/projects/csharp/SessionCropper/Program.cs b/projects/csharp/SessionCropper/Program.cs new file mode 100644 index 0000000..580d223 --- /dev/null +++ b/projects/csharp/SessionCropper/Program.cs @@ -0,0 +1,335 @@ +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Domain.Query; +using MESL.SqlRace.Enumerators; +using SqlRace.Examples.Core; + +// ─── Parse arguments ───────────────────────────────────────── +if (args.Length < 1) +{ + Console.WriteLine("Usage: dotnet run -- [--start ] [--end ]"); + Console.WriteLine(); + Console.WriteLine(" Opens the .ssn2 file, displays timing info, and lets you crop to a new file."); + Console.WriteLine(" Times are in seconds relative to session start."); + Console.WriteLine(); + Console.WriteLine("Examples:"); + Console.WriteLine(" dotnet run -- data.ssn2 # Interactive — prompts for start/end"); + Console.WriteLine(" dotnet run -- data.ssn2 --start 5 --end 30 # Crop to 5s–30s"); + return; +} + +var sourcePath = Path.GetFullPath(args[0]); +if (!File.Exists(sourcePath)) +{ + Console.WriteLine($"ERROR: File not found: {sourcePath}"); + return; +} + +double? argStart = null; +double? argEnd = null; + +for (var i = 1; i < args.Length; i++) +{ + if (args[i] == "--start" && i + 1 < args.Length && double.TryParse(args[i + 1], out var s)) + { + argStart = s; + i++; + } + else if (args[i] == "--end" && i + 1 < args.Length && double.TryParse(args[i + 1], out var e)) + { + argEnd = e; + i++; + } +} + +// ─── Initialise SQL Race ───────────────────────────────────── +SqlRaceBootstrapper.Initialize(); + +var sourceConnectionString = ConnectionStringProvider.ForSQLiteFile(sourcePath); + +// ─── Load the source session ───────────────────────────────── +var sessionManager = SessionManager.CreateSessionManager(); +using var queryManager = QueryManager.CreateQueryManager(sourceConnectionString); +var summaries = queryManager.ExecuteQuery().ToList(); + +if (summaries.Count == 0) +{ + Console.WriteLine($"ERROR: No sessions found in '{sourcePath}'."); + return; +} + +// Use the most recent session in the file +var summary = summaries.OrderByDescending(s => s.TimeOfRecording).First(); +using var sourceClient = sessionManager.Load(summary.Key, sourceConnectionString); +var sourceSession = sourceClient.Session; + +var durationNs = sourceSession.EndTime - sourceSession.StartTime; +var durationSeconds = durationNs / 1_000_000_000.0; + +Console.WriteLine($"Source file: {sourcePath}"); +Console.WriteLine($"Session: {sourceSession.Identifier}"); +Console.WriteLine($"Start time (ns): {sourceSession.StartTime}"); +Console.WriteLine($"End time (ns): {sourceSession.EndTime}"); +Console.WriteLine($"Duration: {durationSeconds:F3} seconds"); +Console.WriteLine($"Parameters: {sourceSession.Parameters.Count}"); +Console.WriteLine(); + +// ─── Determine crop times ──────────────────────────────────── +double cropStartSec; +double cropEndSec; + +if (argStart.HasValue && argEnd.HasValue) +{ + cropStartSec = argStart.Value; + cropEndSec = argEnd.Value; +} +else +{ + Console.Write($"Crop start (seconds from start, 0–{durationSeconds:F3}) [0]: "); + var startInput = Console.ReadLine()?.Trim(); + cropStartSec = string.IsNullOrEmpty(startInput) ? 0 : double.Parse(startInput); + + Console.Write($"Crop end (seconds from start, 0–{durationSeconds:F3}) [{durationSeconds:F3}]: "); + var endInput = Console.ReadLine()?.Trim(); + cropEndSec = string.IsNullOrEmpty(endInput) ? durationSeconds : double.Parse(endInput); +} + +if (cropStartSec < 0 || cropEndSec > durationSeconds || cropStartSec >= cropEndSec) +{ + Console.WriteLine($"ERROR: Invalid crop range {cropStartSec:F3}s–{cropEndSec:F3}s (session is 0–{durationSeconds:F3}s)."); + return; +} + +var cropStartNs = sourceSession.StartTime + (long)(cropStartSec * 1_000_000_000); +var cropEndNs = sourceSession.StartTime + (long)(cropEndSec * 1_000_000_000); + +Console.WriteLine(); +Console.WriteLine($"Cropping: {cropStartSec:F3}s – {cropEndSec:F3}s ({cropEndSec - cropStartSec:F3}s)"); +Console.WriteLine($" Absolute: {cropStartNs} – {cropEndNs} ns"); + +// ─── Create output file ────────────────────────────────────── +var sourceDir = Path.GetDirectoryName(sourcePath) ?? "."; +var sourceNameNoExt = Path.GetFileNameWithoutExtension(sourcePath); +var destPath = Path.Combine(sourceDir, $"{sourceNameNoExt}_cropped.ssn2"); + +// Avoid overwriting +var counter = 1; +while (File.Exists(destPath)) +{ + destPath = Path.Combine(sourceDir, $"{sourceNameNoExt}_cropped_{counter}.ssn2"); + counter++; +} + +var destConnectionString = ConnectionStringProvider.ForSQLiteFile(destPath); +var destSessionKey = SessionKey.NewKey(); + +var croppedName = $"{sourceSession.Identifier} (cropped {cropStartSec:F1}s–{cropEndSec:F1}s)"; +using var destClient = sessionManager.CreateSession(destConnectionString, destSessionKey, croppedName, summary.TimeOfRecording, "Cropped"); +var destSession = destClient.Session; + +Console.WriteLine($"Output file: {destPath}"); +Console.WriteLine(); + +// ─── Copy parameter configuration ─────────────────────────── +var destConfig = destSession.CreateConfiguration(); +var addedConversions = new HashSet(); +var addedParamGroups = new HashSet(); +var addedAppGroups = new HashSet(); +var addedChannels = new HashSet(); +var channelIdMap = new Dictionary(); // old ID → new safe ID +uint nextChannelId = 1; + +foreach (var paramBase in sourceSession.Parameters) +{ + if (paramBase is not ChannelBasedParameter param) + { + Console.WriteLine($" Skipping non-channel parameter: {paramBase.Identifier}"); + continue; + } + // Copy conversion if not already added + var convName = param.ConversionFunctionName; + if (!string.IsNullOrEmpty(convName) && addedConversions.Add(convName)) + { + try + { + var sourceConversion = sourceSession.GetConversion(convName); + if (sourceConversion is not null) + { + destConfig.AddConversion(sourceConversion); + } + } + catch + { + // If we can't retrieve the conversion, create a simple 1:1 fallback + destConfig.AddConversion(RationalConversion.CreateSimple1To1Conversion(convName, "", "%5.4f")); + } + } + + // Copy parameter groups + if (param.GroupIdentifiers is not null) + { + foreach (var groupId in param.GroupIdentifiers) + { + if (addedParamGroups.Add(groupId)) + { + destConfig.AddParameterGroup(new ParameterGroup(groupId, groupId)); + } + } + } + + // Copy application group + var appGroupName = param.ApplicationName; + if (!string.IsNullOrEmpty(appGroupName) && addedAppGroups.Add(appGroupName)) + { + var memberGroups = sourceSession.Parameters + .Where(p => p.ApplicationName == appGroupName && p.GroupIdentifiers is not null) + .SelectMany(p => p.GroupIdentifiers!) + .Distinct() + .ToList(); + + destConfig.AddGroup(new ApplicationGroup(appGroupName, appGroupName, memberGroups) { SupportsRda = false }); + } + + // Copy channels (avoid duplicates if multiple params share a channel) + // Remap IDs to sequential values to avoid Int32 overflow in SQLite + foreach (var channel in param.Channels) + { + if (addedChannels.Add(channel.Id)) + { + var safeId = nextChannelId++; + channelIdMap[channel.Id] = safeId; + destConfig.AddChannel(new Channel( + safeId, + channel.Name, + channel.Interval, + channel.DataType, + channel.DataSource, + channel.Name, + false)); + } + } + + // Copy parameter with remapped channel IDs + var remappedChannelIds = param.ChannelIds + .Where(id => channelIdMap.ContainsKey(id)) + .Select(id => channelIdMap[id]) + .ToList(); + + destConfig.AddParameter(new Parameter( + param.Identifier, + param.Name, + param.Description ?? "", + param.MaximumValue, + param.MinimumValue, + param.MaximumValue, + param.MinimumValue, + 0.0, + param.DataBitMask, + param.ErrorBitmask, + convName, + param.GroupIdentifiers?.ToList() ?? new List(), + remappedChannelIds, + appGroupName)); +} + +destConfig.Commit(); +Console.WriteLine($"Configuration copied: {sourceSession.Parameters.Count} parameter(s)"); + +// ─── Copy sample data within crop window ───────────────────── +var totalSamples = 0L; + +foreach (var paramBase in sourceSession.Parameters) +{ + if (paramBase is not ChannelBasedParameter param) continue; + var sourceChannelId = param.ChannelIds.FirstOrDefault(); + if (sourceChannelId == 0 || !channelIdMap.TryGetValue(sourceChannelId, out var destChannelId)) continue; + + using var pda = sourceSession.CreateParameterDataAccess(param.Identifier); + var samples = pda.GetSamplesBetween(cropStartNs, cropEndNs); + + if (samples.SampleCount == 0) + { + Console.WriteLine($" {param.Identifier,-35} 0 samples (no data in range)"); + continue; + } + + for (var i = 0; i < samples.SampleCount; i++) + { + if (samples.DataStatus[i] != DataStatusType.Sample) + continue; + + destSession.AddChannelData( + destChannelId, + samples.Timestamp[i], + 1, + BitConverter.GetBytes(samples.Data[i])); + } + + totalSamples += samples.SampleCount; + Console.WriteLine($" {param.Identifier,-35} {samples.SampleCount,8} samples copied"); +} + +// ─── Copy laps within crop window ──────────────────────────── +var lapscopied = 0; +foreach (var lap in sourceSession.LapCollection) +{ + if (lap.StartTime >= cropStartNs && lap.StartTime <= cropEndNs) + { + destSession.LapCollection.Add(new Lap(lap.StartTime, lap.Number, 0, lap.Name ?? "", lap.CountForFastestLap)); + lapscopied++; + } +} + +// Ensure at least 1 lap exists — SQL Race sessions must always have a lap +if (lapscopied == 0) +{ + destSession.LapCollection.Add(new Lap(cropStartNs, 1, 0, "Lap 1", false)); + lapscopied = 1; + Console.WriteLine(" (No source laps in range — added default lap at crop start)"); +} + +// ─── Copy markers within crop window ───────────────────────── +var markersCopied = 0; +foreach (var marker in sourceSession.Markers) +{ + var mStart = marker.StartTimestamp ?? 0L; + var mEnd = marker.EndTimestamp ?? 0L; + // Include markers that overlap with the crop window + if (mStart <= cropEndNs && mEnd >= cropStartNs) + { + var clampedStart = Math.Max(mStart, cropStartNs); + var clampedEnd = Math.Min(mEnd, cropEndNs); + destSession.Markers.Add(new Marker(clampedStart, clampedEnd, marker.Label ?? "", marker.Label ?? "", marker.Description ?? "", Guid.NewGuid().ToString())); + markersCopied++; + } +} + +// ─── Copy session items ────────────────────────────────────── +var itemsCopied = 0; +foreach (var item in sourceSession.Items) +{ + destSession.Items.Add(new SessionDataItem(item.Name, item.Value?.ToString() ?? "")); + itemsCopied++; +} + +// ─── Copy constants ────────────────────────────────────────── +var constantsCopied = 0; +foreach (var constant in sourceSession.Constants) +{ + destSession.Constants.Add(new Constant(constant.Name, constant.Value?.ToString() ?? "", constant.Description ?? "", constant.Units ?? "", constant.Format ?? "")); + constantsCopied++; +} + +// ─── Summary ───────────────────────────────────────────────── +Console.WriteLine(); +Console.WriteLine("Crop complete."); +Console.WriteLine($" Source: {sourcePath}"); +Console.WriteLine($" Output: {destPath}"); +Console.WriteLine($" Duration: {cropEndSec - cropStartSec:F3}s (was {durationSeconds:F3}s)"); +Console.WriteLine($" Parameters: {sourceSession.Parameters.Count}"); +Console.WriteLine($" Samples: {totalSamples:N0}"); +Console.WriteLine($" Segments: {lapscopied}"); +Console.WriteLine($" Markers: {markersCopied}"); +Console.WriteLine($" Items: {itemsCopied}"); +Console.WriteLine($" Constants: {constantsCopied}"); diff --git a/projects/csharp/SessionCropper/README.md b/projects/csharp/SessionCropper/README.md new file mode 100644 index 0000000..8cbac21 --- /dev/null +++ b/projects/csharp/SessionCropper/README.md @@ -0,0 +1,34 @@ +# Session Cropper + +Reads an `.ssn2` file, displays session timing info, and crops it to a new time range — saving the result as a new `.ssn2` file. + +## Usage + +```bash +# Interactive — prompts for start/end times +dotnet run -- path/to/session.ssn2 + +# Non-interactive — specify crop range in seconds from session start +dotnet run -- path/to/session.ssn2 --start 5 --end 30 +``` + +## What It Does + +1. Opens the source `.ssn2` and loads the most recent session +2. Displays start time, end time, duration, and parameter count +3. Prompts for (or accepts via CLI args) a crop start and end in seconds +4. Creates a new `_cropped.ssn2` file alongside the source +5. Copies all parameter configuration (conversions, channels, groups, parameters) +6. Copies only the sample data within the crop window +7. Copies laps/segments, markers, session items, and constants that fall within the window + +## Output + +The cropped file is saved as `_cropped.ssn2` in the same directory. If that file already exists, it appends a counter (`_cropped_1.ssn2`, etc.). + +## Build + +```bash +cd projects/csharp +dotnet build SessionCropper/SessionCropper.csproj +``` diff --git a/projects/csharp/SessionCropper/SessionCropper.csproj b/projects/csharp/SessionCropper/SessionCropper.csproj new file mode 100644 index 0000000..79eeea7 --- /dev/null +++ b/projects/csharp/SessionCropper/SessionCropper.csproj @@ -0,0 +1,11 @@ + + + + Exe + + + + + + + diff --git a/projects/csharp/SqlRace.Examples.Core/ConnectionStringProvider.cs b/projects/csharp/SqlRace.Examples.Core/ConnectionStringProvider.cs new file mode 100644 index 0000000..2a6ec75 --- /dev/null +++ b/projects/csharp/SqlRace.Examples.Core/ConnectionStringProvider.cs @@ -0,0 +1,91 @@ +using Microsoft.Extensions.Configuration; + +namespace SqlRace.Examples.Core; + +/// +/// Resolves SQL Race connection strings from a configuration hierarchy: +/// +/// Explicit parameter +/// Environment variable SQLRACE_CONNECTION_STRING +/// appsettings.json key SqlRace:ConnectionString +/// Default SQLite file in the system temp directory +/// +/// +public static class ConnectionStringProvider +{ + /// + /// The environment variable name checked for a connection string. + /// + public const string EnvironmentVariable = "SQLRACE_CONNECTION_STRING"; + + /// + /// The default SQLite database file name used when no other source is configured. + /// + public const string DefaultFileName = "sqlrace-examples.ssn2"; + + /// + /// Resolves a connection string using the configuration hierarchy. + /// + /// + /// An explicit connection string. If non-null and non-empty, this is returned directly. + /// + /// A SQL Race connection string. + public static string Resolve(string? @explicit = null) + { + // 1. Explicit parameter + if (!string.IsNullOrWhiteSpace(@explicit)) + return @explicit; + + // 2. Environment variable + var envValue = Environment.GetEnvironmentVariable(EnvironmentVariable); + if (!string.IsNullOrWhiteSpace(envValue)) + return envValue; + + // 3. appsettings.json + var appSettingsValue = TryReadAppSettings(); + if (!string.IsNullOrWhiteSpace(appSettingsValue)) + return appSettingsValue; + + // 4. Default SQLite + return DefaultSQLiteConnectionString; + } + + /// + /// Gets a connection string for a SQLite file at the specified path. + /// + public static string ForSQLiteFile(string filePath) => + $"DbEngine=SQLite;Data Source={filePath};"; + + /// + /// Gets the default SQLite connection string using a file in the system temp directory. + /// + public static string DefaultSQLiteConnectionString => + ForSQLiteFile(Path.Combine(Path.GetTempPath(), DefaultFileName)); + + /// + /// Gets the full path to the default SQLite database file. + /// + public static string DefaultSQLiteFilePath => + Path.Combine(Path.GetTempPath(), DefaultFileName); + + private static string? TryReadAppSettings() + { + try + { + var configPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + if (!File.Exists(configPath)) + return null; + + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: true) + .Build(); + + return config["SqlRace:ConnectionString"]; + } + catch + { + return null; + } + } +} diff --git a/projects/csharp/SqlRace.Examples.Core/ParameterBuilder.cs b/projects/csharp/SqlRace.Examples.Core/ParameterBuilder.cs new file mode 100644 index 0000000..d6855b3 --- /dev/null +++ b/projects/csharp/SqlRace.Examples.Core/ParameterBuilder.cs @@ -0,0 +1,270 @@ +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +namespace SqlRace.Examples.Core; + +/// +/// Result of building a parameter, containing both the and its channel ID. +/// +public record BuiltParameter(Parameter Parameter, uint ChannelId); + +/// +/// Fluent builder for creating SQL Race parameter configurations. +/// Encapsulates the boilerplate of creating , +/// , , , +/// and objects. +/// +/// +/// +/// new ParameterBuilder(session) +/// .WithIdentifier("Temperature:Sensors") +/// .WithDescription("Inlet temperature sensor") +/// .InApplicationGroup("Sensors") +/// .InParameterGroup("Thermal") +/// .WithFrequency(100, FrequencyUnit.Hz) +/// .WithDataType(DataType.Double64Bit) +/// .WithConversion("degC", "%5.2f") +/// .WithRange(min: 0, max: 500) +/// .Build(); +/// +/// +public class ParameterBuilder +{ + private readonly Session _session; + + private string _identifier = ""; + private string _name = ""; + private string _description = ""; + private string _applicationGroup = "Default"; + private string _parameterGroup = "Default"; + private double _frequency = 100; + private FrequencyUnit _frequencyUnit = FrequencyUnit.Hz; + private DataType _dataType = DataType.Double64Bit; + private string _conversionUnit = ""; + private string _conversionFormat = "%5.2f"; + private double _min; + private double _max = 1000; + + /// + /// Creates a new for the specified session. + /// + /// The session to add the parameter to. + public ParameterBuilder(Session session) + { + _session = session ?? throw new ArgumentNullException(nameof(session)); + } + + /// + /// Sets the parameter identifier in the format "Name:Group". + /// Also sets the display name to the portion before the colon, if not explicitly set. + /// + public ParameterBuilder WithIdentifier(string identifier) + { + _identifier = identifier ?? throw new ArgumentNullException(nameof(identifier)); + if (string.IsNullOrEmpty(_name)) + { + var colonIndex = identifier.IndexOf(':'); + _name = colonIndex > 0 ? identifier[..colonIndex] : identifier; + } + return this; + } + + /// Sets the display name. If not called, derived from the identifier. + public ParameterBuilder WithName(string name) + { + _name = name ?? throw new ArgumentNullException(nameof(name)); + return this; + } + + /// Sets the parameter description. + public ParameterBuilder WithDescription(string description) + { + _description = description ?? throw new ArgumentNullException(nameof(description)); + return this; + } + + /// Sets the application group name (source system grouping). + public ParameterBuilder InApplicationGroup(string group) + { + _applicationGroup = group ?? throw new ArgumentNullException(nameof(group)); + return this; + } + + /// Sets the parameter group name (logical grouping). + public ParameterBuilder InParameterGroup(string group) + { + _parameterGroup = group ?? throw new ArgumentNullException(nameof(group)); + return this; + } + + /// Sets the sample frequency. + public ParameterBuilder WithFrequency(double value, FrequencyUnit unit = FrequencyUnit.Hz) + { + _frequency = value; + _frequencyUnit = unit; + return this; + } + + /// Sets the data type for the channel. + public ParameterBuilder WithDataType(DataType dataType) + { + _dataType = dataType; + return this; + } + + /// + /// Sets the engineering unit conversion. + /// + /// The engineering unit string (e.g., "degC", "bar", "rpm"). + /// The display format string (e.g., "%5.2f"). + public ParameterBuilder WithConversion(string unit, string format = "%5.2f") + { + _conversionUnit = unit ?? throw new ArgumentNullException(nameof(unit)); + _conversionFormat = format; + return this; + } + + /// Sets the minimum and maximum display range. + public ParameterBuilder WithRange(double min, double max) + { + _min = min; + _max = max; + return this; + } + + /// + /// Builds the parameter, adding all required configuration objects to the session, + /// and commits the configuration. + /// + /// A containing the parameter and its channel ID. + /// Thrown if the identifier has not been set. + public BuiltParameter Build() + { + if (string.IsNullOrWhiteSpace(_identifier)) + throw new InvalidOperationException("Parameter identifier must be set. Call WithIdentifier() before Build()."); + + var config = _session.CreateConfiguration(); + + var freq = new Frequency(_frequency, _frequencyUnit); + var interval = freq.ToInterval(); + var channelId = Guid.NewGuid().GetHashCode() & 0x7FFFFFFF; // Positive ID + + // Conversion (1:1 rational — raw value = engineering value) + var conversionName = $"{_identifier}_conversion"; + var conversion = RationalConversion.CreateSimple1To1Conversion(conversionName, _conversionUnit, _conversionFormat); + config.AddConversion(conversion); + + // Channel + var channel = new Channel((uint)channelId, _name, interval, _dataType, ChannelDataSourceType.Periodic, _identifier, false); + config.AddChannel(channel); + + // Groups — ParameterGroup must be parented under an ApplicationGroup + config.AddParameterGroup(new ParameterGroup(_parameterGroup, _parameterGroup)); + var appGroup = new ApplicationGroup(_applicationGroup, _applicationGroup, new List { _parameterGroup }) { SupportsRda = false }; + config.AddGroup(appGroup); + + // Parameter + var parameterGroups = new List { _parameterGroup }; + var parameter = new Parameter( + _identifier, + _name, + _description, + _max, + _min, + _max, + _min, + 0.0, + 0u, + 0u, + conversionName, + parameterGroups, + (uint)channelId, + _applicationGroup); + + config.AddParameter(parameter); + config.Commit(); + + return new BuiltParameter(parameter, (uint)channelId); + } + + /// + /// Builds multiple parameters in a batch with a single configuration commit. + /// + /// The session to add parameters to. + /// The configured builders to build. + /// A list of created parameters. + public static IReadOnlyList BuildBatch(Session session, IEnumerable builders) + { + var config = session.CreateConfiguration(); + var parameters = new List(); + + // Collect parameter groups per application group so we can parent them correctly + var appGroupChildren = new Dictionary>(); + var builderList = builders.ToList(); + + foreach (var builder in builderList) + { + if (string.IsNullOrWhiteSpace(builder._identifier)) + throw new InvalidOperationException("All parameter builders must have an identifier set."); + + if (!appGroupChildren.TryGetValue(builder._applicationGroup, out var children)) + { + children = new HashSet(); + appGroupChildren[builder._applicationGroup] = children; + } + children.Add(builder._parameterGroup); + } + + // Add all parameter groups and application groups up front + var addedParamGroups = new HashSet(); + foreach (var (appGroupName, childParamGroups) in appGroupChildren) + { + foreach (var pg in childParamGroups) + { + if (addedParamGroups.Add(pg)) + config.AddParameterGroup(new ParameterGroup(pg, pg)); + } + var appGroup = new ApplicationGroup(appGroupName, appGroupName, childParamGroups.ToList()) { SupportsRda = false }; + config.AddGroup(appGroup); + } + + // Add channels, conversions, and parameters + foreach (var builder in builderList) + { + var freq = new Frequency(builder._frequency, builder._frequencyUnit); + var interval = freq.ToInterval(); + var channelId = Guid.NewGuid().GetHashCode() & 0x7FFFFFFF; + + var conversionName = $"{builder._identifier}_conversion"; + var conversion = RationalConversion.CreateSimple1To1Conversion(conversionName, builder._conversionUnit, builder._conversionFormat); + config.AddConversion(conversion); + + var channel = new Channel((uint)channelId, builder._name, interval, builder._dataType, ChannelDataSourceType.Periodic, builder._identifier, false); + config.AddChannel(channel); + + var parameterGroups = new List { builder._parameterGroup }; + var parameter = new Parameter( + builder._identifier, + builder._name, + builder._description, + builder._max, + builder._min, + builder._max, + builder._min, + 0.0, + 0u, + 0u, + conversionName, + parameterGroups, + (uint)channelId, + builder._applicationGroup); + + config.AddParameter(parameter); + parameters.Add(new BuiltParameter(parameter, (uint)channelId)); + } + + config.Commit(); + return parameters; + } +} diff --git a/projects/csharp/SqlRace.Examples.Core/README.md b/projects/csharp/SqlRace.Examples.Core/README.md new file mode 100644 index 0000000..01ae415 --- /dev/null +++ b/projects/csharp/SqlRace.Examples.Core/README.md @@ -0,0 +1,36 @@ +# SqlRace.Examples.Core + +Shared library providing common utilities for SQL Race example projects. + +## Components + +| Class | Purpose | +|-------|---------| +| `SqlRaceBootstrapper` | Idempotent SQL Race runtime initialisation | +| `ConnectionStringProvider` | Resolves connection strings from config hierarchy | +| `SessionFactory` | Creates and loads sessions with error handling | +| `ParameterBuilder` | Fluent builder for parameter configuration | + +## Usage + +```csharp +using SqlRace.Examples.Core; + +SqlRaceBootstrapper.Initialize(); + +var connectionString = ConnectionStringProvider.Resolve(); + +using var client = SessionFactory.CreateSession(connectionString, "My Session"); +var session = client.Session; + +new ParameterBuilder(session) + .WithIdentifier("Temperature:Sensors") + .WithDescription("Inlet temperature") + .InApplicationGroup("Sensors") + .InParameterGroup("Thermal") + .WithFrequency(100, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("degC", "%5.2f") + .WithRange(min: 0, max: 500) + .Build(); +``` diff --git a/projects/csharp/SqlRace.Examples.Core/SessionFactory.cs b/projects/csharp/SqlRace.Examples.Core/SessionFactory.cs new file mode 100644 index 0000000..3328591 --- /dev/null +++ b/projects/csharp/SqlRace.Examples.Core/SessionFactory.cs @@ -0,0 +1,127 @@ +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +namespace SqlRace.Examples.Core; + +/// +/// Factory methods for creating and loading SQL Race sessions with +/// proper error handling and connection string resolution. +/// +public static class SessionFactory +{ + /// + /// Creates a new session in the database specified by the connection string. + /// + /// A descriptive name for the session. + /// + /// Connection string. If null, resolved via . + /// + /// Session date/time. Defaults to . + /// An that must be disposed when finished. + /// Thrown if session creation fails. + public static IClientSession CreateSession( + string name, + string? connectionString = null, + DateTime? dateTime = null) + { + SqlRaceBootstrapper.Initialize(); + + var connStr = ConnectionStringProvider.Resolve(connectionString); + var sessionKey = SessionKey.NewKey(); + var sessionDate = dateTime ?? DateTime.UtcNow; + + try + { + var sessionManager = SessionManager.CreateSessionManager(); + var clientSession = sessionManager.CreateSession( + connStr, + sessionKey, + name, + sessionDate, + "Example"); + + return clientSession ?? throw new InvalidOperationException( + $"SessionManager.CreateSession returned null. " + + $"Connection string: {connStr}"); + } + catch (Exception ex) when (ex is not InvalidOperationException) + { + throw new InvalidOperationException( + $"Failed to create session '{name}'. " + + $"Connection string: {connStr}. " + + $"Error: {ex.Message}", + ex); + } + } + + /// + /// Loads an existing session from a database by session key. + /// + /// The GUID key of the session to load. + /// + /// Connection string. If null, resolved via . + /// + /// An that must be disposed when finished. + /// Thrown if the session cannot be found or loaded. + public static IClientSession LoadSession( + SessionKey sessionKey, + string? connectionString = null) + { + SqlRaceBootstrapper.Initialize(); + + var connStr = ConnectionStringProvider.Resolve(connectionString); + + try + { + var sessionManager = SessionManager.CreateSessionManager(); + var clientSession = sessionManager.Load(sessionKey, connStr); + + return clientSession ?? throw new InvalidOperationException( + $"Session not found: {sessionKey}. " + + $"Check that the session exists in the database at: {connStr}"); + } + catch (Exception ex) when (ex is not InvalidOperationException) + { + throw new InvalidOperationException( + $"Failed to load session {sessionKey}. " + + $"Connection string: {connStr}. " + + $"Error: {ex.Message}", + ex); + } + } + + /// + /// Loads a session from a local .ssn or .ssn2 file. + /// + /// Path to the session file. + /// An that must be disposed when finished. + /// Thrown if the file does not exist. + /// Thrown if the file cannot be loaded. + public static IClientSession LoadFromFile(string filePath) + { + SqlRaceBootstrapper.Initialize(); + + if (!File.Exists(filePath)) + throw new FileNotFoundException( + $"Session file not found: {filePath}. " + + $"Check the path and ensure the file exists.", + filePath); + + try + { + var fileSessionManager = FileSessionManager.CreateFileSessionManager(); + var clientSession = fileSessionManager.Load(filePath); + + return clientSession ?? throw new InvalidOperationException( + $"FileSessionManager.Load returned null for: {filePath}. " + + $"The file may be corrupt or in an unsupported format."); + } + catch (Exception ex) when (ex is not InvalidOperationException && ex is not FileNotFoundException) + { + throw new InvalidOperationException( + $"Failed to load session file: {filePath}. " + + $"Error: {ex.Message}", + ex); + } + } +} diff --git a/projects/csharp/SqlRace.Examples.Core/SqlRace.Examples.Core.csproj b/projects/csharp/SqlRace.Examples.Core/SqlRace.Examples.Core.csproj new file mode 100644 index 0000000..4a0d91b --- /dev/null +++ b/projects/csharp/SqlRace.Examples.Core/SqlRace.Examples.Core.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + SqlRace.Examples.Core + + + + + + + + + + diff --git a/projects/csharp/SqlRace.Examples.Core/SqlRaceBootstrapper.cs b/projects/csharp/SqlRace.Examples.Core/SqlRaceBootstrapper.cs new file mode 100644 index 0000000..0032600 --- /dev/null +++ b/projects/csharp/SqlRace.Examples.Core/SqlRaceBootstrapper.cs @@ -0,0 +1,54 @@ +using MESL.SqlRace.Domain; + +namespace SqlRace.Examples.Core; + +/// +/// Provides idempotent initialisation of the SQL Race runtime. +/// Call once at application startup before any other SQL Race API calls. +/// +public static class SqlRaceBootstrapper +{ + private static readonly object Lock = new(); + private static bool _initialized; + + /// + /// Initialises the SQL Race runtime. Safe to call multiple times — subsequent calls are no-ops. + /// + /// + /// The licence program name. Defaults to "SQLRace". + /// + /// + /// Thrown if SQL Race initialisation fails. + /// + public static void Initialize(string licenceProgramName = "SQLRace") + { + if (_initialized) + return; + + lock (Lock) + { + if (_initialized) + return; + + try + { + MESL.SqlRace.Domain.Core.LicenceProgramName = licenceProgramName; + MESL.SqlRace.Domain.Core.Initialize(); + _initialized = true; + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to initialise SQL Race runtime. " + + $"Ensure the MESL.SQLRace.API NuGet package is installed and its native dependencies are present. " + + $"Inner error: {ex.Message}", + ex); + } + } + } + + /// + /// Returns true if has completed successfully. + /// + public static bool IsInitialized => _initialized; +} diff --git a/projects/csharp/SqlRace.Examples.sln b/projects/csharp/SqlRace.Examples.sln new file mode 100644 index 0000000..5c252e5 --- /dev/null +++ b/projects/csharp/SqlRace.Examples.sln @@ -0,0 +1,61 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlRace.Examples.Core", "SqlRace.Examples.Core\SqlRace.Examples.Core.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "GettingStarted\GettingStarted.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerListenerRecorder", "ServerListenerRecorder\ServerListenerRecorder.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandaloneRecorder", "StandaloneRecorder\StandaloneRecorder.csproj", "{D4E5F6A7-B8C9-0123-DEFA-234567890123}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BatchExporter", "BatchExporter\BatchExporter.csproj", "{E5F6A7B8-C9D0-1234-EFAB-345678901234}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveDashboard", "LiveDashboard\LiveDashboard.csproj", "{F6A7B8C9-D0E1-2345-FABC-456789012345}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionLibrary", "FunctionLibrary\FunctionLibrary.csproj", "{A7B8C9D0-E1F2-3456-ABCD-567890123456}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SessionCropper", "SessionCropper\SessionCropper.csproj", "{B8C9D0E1-F2A3-4567-BCDA-678901234567}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-0123-DEFA-234567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEFA-234567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEFA-234567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-0123-DEFA-234567890123}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-1234-EFAB-345678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-1234-EFAB-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-1234-EFAB-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-1234-EFAB-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU + {F6A7B8C9-D0E1-2345-FABC-456789012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6A7B8C9-D0E1-2345-FABC-456789012345}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6A7B8C9-D0E1-2345-FABC-456789012345}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6A7B8C9-D0E1-2345-FABC-456789012345}.Release|Any CPU.Build.0 = Release|Any CPU + {A7B8C9D0-E1F2-3456-ABCD-567890123456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7B8C9D0-E1F2-3456-ABCD-567890123456}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7B8C9D0-E1F2-3456-ABCD-567890123456}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7B8C9D0-E1F2-3456-ABCD-567890123456}.Release|Any CPU.Build.0 = Release|Any CPU + {B8C9D0E1-F2A3-4567-BCDA-678901234567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8C9D0E1-F2A3-4567-BCDA-678901234567}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8C9D0E1-F2A3-4567-BCDA-678901234567}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8C9D0E1-F2A3-4567-BCDA-678901234567}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/projects/csharp/StandaloneRecorder/Program.cs b/projects/csharp/StandaloneRecorder/Program.cs new file mode 100644 index 0000000..f0155b4 --- /dev/null +++ b/projects/csharp/StandaloneRecorder/Program.cs @@ -0,0 +1,166 @@ +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; +using Microsoft.Extensions.Configuration; +using SqlRace.Examples.Core; +using StandaloneRecorder; + +// ─── Configuration ─────────────────────────────────────────── +var configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + +var config = new RecorderConfig(); +configuration.GetSection("Recorder").Bind(config); + +var connectionString = ConnectionStringProvider.Resolve( + configuration.GetConnectionString("SqlRace")); + +// ─── Cancellation (Ctrl+C) ────────────────────────────────── +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => +{ + e.Cancel = true; + cts.Cancel(); + Console.WriteLine("\nShutdown requested..."); +}; + +// ─── Initialise SQL Race ───────────────────────────────────── +SqlRaceBootstrapper.Initialize(); +Console.WriteLine("SQL Race initialised"); +Console.WriteLine($"ADS host: {config.AdsHost}"); +Console.WriteLine($"Listener: {config.ListenerIpAddress}:{config.ListenerPort}"); +Console.WriteLine($"Connection: {connectionString}"); + +// ─── Initialise Recorder ───────────────────────────────────── +// TODO: Verify — Recorders.Initialise() API signature +// In production: Recorders.Initialise(config.ListenerIpAddress, config.ListenerPort); +Console.WriteLine("\nInitialising recorder subsystem..."); + +// ─── Create Data Server Telemetry Recorder ─────────────────── +// TODO: Verify — DataServerTelemetryRecorder creation API +// var recorder = new DataServerTelemetryRecorder(); +// recorder.ServerList.Refresh(); +// +// Console.WriteLine("Available servers:"); +// foreach (var server in recorder.ServerList) +// Console.WriteLine($" {server.Name} ({server.Address})"); +// +// recorder.Connect(config.AdsHost); +// recorder.AutoRecordEnable = true; + +// ─── Listen for new sessions ───────────────────────────────── +var sessionCreated = new TaskCompletionSource(); +var sessionManager = SessionManager.CreateSessionManager(); + +// TODO: Verify — SessionManager.SessionEventOccurred event signature +// sessionManager.SessionEventOccurred += (sender, e) => +// { +// if (e.EventType == SessionEventType.SessionCreated) +// { +// Console.WriteLine($" Session detected: {e.SessionKey}"); +// sessionCreated.TrySetResult(e.SessionKey); +// } +// }; + +Console.WriteLine("Waiting for session to be created..."); +Console.WriteLine("(In production, this waits for ADS to start streaming data)"); +Console.WriteLine(); + +// ─── Simulation: create a session with live-style data ─────── +// Since we cannot connect to a real ADS in this example, +// we simulate the pattern by creating a session and writing data. +var sessionName = $"StandaloneRecorder-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; +IClientSession? clientSession = null; + +try +{ + clientSession = SessionFactory.CreateSession(sessionName, connectionString); + var session = clientSession.Session; + Console.WriteLine($"Session created: {session.Key}"); + Console.WriteLine($" Name: {sessionName}"); + + // ─── Configure source parameters ──────────────────────── + var sourceParam = new ParameterBuilder(session) + .WithIdentifier("Temperature:Sensors") + .WithDescription("Incoming temperature telemetry") + .InApplicationGroup("Sensors").InParameterGroup("Thermal") + .WithFrequency(100, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("degC", "%5.2f") + .WithRange(min: 0, max: 500) + .Build(); + + // ─── Create a custom derived parameter ────────────────── + var derivedParam = new ParameterBuilder(session) + .WithIdentifier("TemperatureSmoothed:Derived") + .WithDescription("Exponentially smoothed temperature") + .InApplicationGroup("Derived").InParameterGroup("Thermal") + .WithFrequency(100, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("degC", "%5.2f") + .WithRange(min: 0, max: 500) + .Build(); + + Console.WriteLine(" Source: Temperature:Sensors (100 Hz)"); + Console.WriteLine(" Derived: TemperatureSmoothed:Derived (EMA)"); + Console.WriteLine("\nRecording and processing (Ctrl+C to stop)...\n"); + + // ─── Read-Transform-Write loop ────────────────────────── + var baseTime = 36_000_000_000_000L; + var interval = new Frequency(100, FrequencyUnit.Hz).ToInterval(); + var smoothed = 75.0; // EMA state + const double alpha = 0.1; // Smoothing factor + + var sampleIndex = 0; + while (!cts.Token.IsCancellationRequested) + { + // Simulate incoming data (10 samples per batch = 100ms at 100 Hz) + for (var i = 0; i < 10; i++) + { + var idx = sampleIndex * 10 + i; + var ts = baseTime + idx * interval; + var raw = 75.0 + 15.0 * Math.Sin(idx * 0.02) + Random.Shared.NextDouble() * 2.0; + + // WRITE source data + session.AddChannelData(sourceParam.ChannelId, ts, 1, BitConverter.GetBytes(raw)); + + // TRANSFORM: exponential moving average + smoothed = alpha * raw + (1.0 - alpha) * smoothed; + + // WRITE derived data + session.AddChannelData(derivedParam.ChannelId, ts, 1, BitConverter.GetBytes(smoothed)); + } + + sampleIndex++; + Console.WriteLine( + $" Batch {sampleIndex,4}: " + + $"READ raw → TRANSFORM (EMA α={alpha}) → WRITE smoothed " + + $"[latest: raw={75.0 + 15.0 * Math.Sin(sampleIndex * 10 * 0.02):F2}, " + + $"smoothed={smoothed:F2}]"); + + try + { + await Task.Delay(100, cts.Token); + } + catch (OperationCanceledException) + { + break; + } + } + + Console.WriteLine($"\nRecording stopped. Total batches: {sampleIndex}"); +} +catch (Exception ex) +{ + Console.WriteLine($"ERROR: {ex.Message}"); +} +finally +{ + // TODO: Verify — recorder.StopRecording(), recorder.AutoRecordEnable = false, recorder.Dispose() + clientSession?.Dispose(); + Console.WriteLine("Session closed. Recorder shut down."); +} diff --git a/projects/csharp/StandaloneRecorder/README.md b/projects/csharp/StandaloneRecorder/README.md new file mode 100644 index 0000000..b973431 --- /dev/null +++ b/projects/csharp/StandaloneRecorder/README.md @@ -0,0 +1,73 @@ +# StandaloneRecorder + +A console application that demonstrates the pattern for connecting to an ATLAS Data Server (ADS), recording live telemetry, and simultaneously processing data with a read-transform-write loop. + +## Server Listener vs Standalone Recorder + +| Aspect | Server Listener | Standalone Recorder | +|--------|----------------|-------------------| +| Direction | Passive: receives data pushed to it | Active: connects to an ADS | +| Initiation | Data source pushes | This application pulls | +| Use case | Receiving from ATLAS or a test rig | Connecting to a running ADS instance | + +## What this example demonstrates + +1. Session creation with `SessionFactory` +2. Multi-parameter configuration with `ParameterBuilder` +3. A **read-transform-write** loop: raw telemetry → exponential moving average → derived parameter +4. Async operation with `CancellationToken` for graceful Ctrl+C shutdown +5. The pattern for detecting new sessions via `SessionManager.SessionEventOccurred` + +The ADS connection code is stubbed with `// TODO: Verify` comments since it requires a running ADS instance. + +## Prerequisites + +- .NET 8 SDK +- MESL.SQLRace.API NuGet package +- For real ADS connection: ATLAS 10 with Server Listener enabled (Tools > SqlRace > Settings) + +## Build and Run + +```bash +cd projects/csharp +dotnet run --project StandaloneRecorder +``` + +Press **Ctrl+C** to stop. + +## Configuration + +Edit `appsettings.json`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `ConnectionStrings:SqlRace` | (empty → SQLite in temp) | SQL Race connection string | +| `Recorder:AdsHost` | `127.0.0.1` | ATLAS Data Server hostname | +| `Recorder:ListenerIpAddress` | `127.0.0.1` | Local listener IP | +| `Recorder:ListenerPort` | `6565` | Local listener port | +| `Recorder:DataSourcePath` | (empty → temp dir) | Data file directory | + +## Data Flow + +``` +ADS (or simulation) → Raw data → Session (source parameter) + ↓ + EMA Transform (α=0.1) + ↓ + Session (derived parameter) +``` + +## Expected Output + +``` +SQL Race initialised +Session created: + Source: Temperature:Sensors (100 Hz) + Derived: TemperatureSmoothed:Derived (EMA) + +Recording and processing (Ctrl+C to stop)... + + Batch 1: READ raw → TRANSFORM (EMA α=0.1) → WRITE smoothed [latest: raw=75.30, smoothed=75.03] + Batch 2: READ raw → TRANSFORM (EMA α=0.1) → WRITE smoothed [latest: raw=77.94, smoothed=75.32] + ... +``` diff --git a/projects/csharp/StandaloneRecorder/RecorderConfig.cs b/projects/csharp/StandaloneRecorder/RecorderConfig.cs new file mode 100644 index 0000000..ec8f082 --- /dev/null +++ b/projects/csharp/StandaloneRecorder/RecorderConfig.cs @@ -0,0 +1,32 @@ +namespace StandaloneRecorder; + +/// +/// Configuration for the standalone recorder, bound from appsettings.json. +/// +public sealed class RecorderConfig +{ + /// ATLAS Data Server hostname or IP address. + public string AdsHost { get; set; } = "127.0.0.1"; + + /// IP address for the local listener endpoint. + public string ListenerIpAddress { get; set; } = "127.0.0.1"; + + /// Port for the local listener endpoint. + public int ListenerPort { get; set; } = 6565; + + /// + /// Path to the data source directory. Empty defaults to the system temp directory. + /// + public string DataSourcePath { get; set; } = ""; + + /// Database engine: "SQLite" or "SqlServer". + public string DbEngine { get; set; } = "SQLite"; + + /// + /// Resolves the data source path, falling back to temp directory if empty. + /// + public string ResolvedDataSourcePath => + string.IsNullOrWhiteSpace(DataSourcePath) + ? Path.GetTempPath() + : DataSourcePath; +} diff --git a/projects/csharp/StandaloneRecorder/StandaloneRecorder.csproj b/projects/csharp/StandaloneRecorder/StandaloneRecorder.csproj new file mode 100644 index 0000000..af71626 --- /dev/null +++ b/projects/csharp/StandaloneRecorder/StandaloneRecorder.csproj @@ -0,0 +1,16 @@ + + + + Exe + + + + + + + + + + + + diff --git a/projects/csharp/StandaloneRecorder/appsettings.json b/projects/csharp/StandaloneRecorder/appsettings.json new file mode 100644 index 0000000..6ace39f --- /dev/null +++ b/projects/csharp/StandaloneRecorder/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "SqlRace": "" + }, + "Recorder": { + "AdsHost": "127.0.0.1", + "ListenerIpAddress": "127.0.0.1", + "ListenerPort": 6565, + "DataSourcePath": "", + "DbEngine": "SQLite" + } +} diff --git a/projects/matlab/session_analysis/README.md b/projects/matlab/session_analysis/README.md new file mode 100644 index 0000000..1f7055c --- /dev/null +++ b/projects/matlab/session_analysis/README.md @@ -0,0 +1,55 @@ +# Session Analysis (MATLAB) + +A MATLAB workflow for loading SQL Race sessions, extracting parameters, computing statistics, plotting traces, and saving results. + +## Prerequisites + +- MATLAB R2019b+ (R2023b+ recommended for .NET 8 support) +- SQL Race .NET assemblies (installed with ATLAS 10) + +## Setup + +1. Open MATLAB +2. Navigate to this directory: `cd projects/matlab/session_analysis` +3. Edit `SQLRACE_DLL_PATH` in `setup_sqlrace.m` to match your ATLAS installation + +## Usage + +1. Edit the configuration section at the top of `analyse_session.m`: + - Set `sessionGuid` to your session's GUID + - Optionally change `dbPath` and `connectionString` + +2. Run: + ```matlab + analyse_session + ``` + +## Files + +| File | Description | +|------|-------------| +| `setup_sqlrace.m` | Function: loads .NET assembly, initialises runtime, returns manager handles | +| `analyse_session.m` | Script: full analysis workflow (load → extract → statistics → plot → save) | + +## Workflow + +1. `setup_sqlrace()` handles all .NET assembly loading and Core initialisation +2. Session is loaded via SessionManager +3. All parameters are extracted into MATLAB arrays +4. Data is assembled into a MATLAB `timetable` +5. Statistics (mean, std, min, max) are computed per parameter +6. Parameter traces are plotted +7. Results are saved to `session_results.mat` + +## Output + +- **Console**: session summary, per-parameter statistics table +- **Figure**: parameter trace plots (up to 4 subplots) +- **File**: `session_results.mat` containing `tt` (timetable), `statsTable`, `paramIds` + +## Adapting + +To analyse different parameters or sessions: +- Change `sessionGuid` to target a different session +- The script automatically extracts all parameters in the session +- For selective extraction, filter `paramIds` before the extraction loop diff --git a/projects/matlab/session_analysis/analyse_session.m b/projects/matlab/session_analysis/analyse_session.m new file mode 100644 index 0000000..63b8948 --- /dev/null +++ b/projects/matlab/session_analysis/analyse_session.m @@ -0,0 +1,156 @@ +%% SQL Race Session Analysis +% Loads a SQL Race session, extracts parameters into a timetable, +% computes statistics, plots traces, and saves results. + +%% Configuration — select a scenario data file +% Available .ssn2 files in notebooks/scenarios/data/ +scenarioDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); + +sessions = { + 'flight-test-manoeuvre-extraction.ssn2', '828f3c72-9573-4a74-9803-20ff993d848a'; + 'motorsport-lap-analysis.ssn2', 'a8b2b3b4-5530-4825-a5d1-bda63732361e'; + 'durability-cycle1.ssn2', '53f2367d-14b6-43d1-aeaf-6e66f1b1031c'; + 'durability-cycle2.ssn2', 'f8d31ef8-48ba-450b-971c-8fec68dec7f8'; + 'durability-cycle3.ssn2', '54c2a62c-08c5-4f35-9a85-8a276ec83329'; + 'turbine-week1.ssn2', 'e6d9bbc0-89a6-4754-9b4c-f7b5a6b4718b'; + 'turbine-week2.ssn2', '7e74ba09-b54d-415e-bb25-4214300019ef'; + 'turbine-week3.ssn2', '56d725d0-569b-4c74-b6ae-4dd497edca2d'; +}; + +% --- Pick which file to analyse (change this index) --- +selectedIdx = 1; + +dbFile = sessions{selectedIdx, 1}; +sessionGuid = sessions{selectedIdx, 2}; +dbPath = fullfile(scenarioDir, dbFile); +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); +outputFile = 'session_results.mat'; + +fprintf('Selected: %s\n', dbFile); +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end + +%% Initialise SQL Race +h = setup_sqlrace(); +import MESL.SqlRace.Domain.*; + +%% Load Session +fprintf('Loading session: %s\n', sessionGuid); + +sessionManager = SessionManager.CreateSessionManager(); +sessionSummaries = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessionSummaries.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessionSummaries.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); +session = clientSession.Session; + +fprintf(' Name: %s\n', char(session.Identifier)); +fprintf(' Start: %d ns\n', session.StartTime); +fprintf(' End: %d ns\n', session.EndTime); +fprintf(' Duration: %.3f s\n', double(session.EndTime - session.StartTime) / 1e9); +fprintf(' Parameters: %d\n', session.Parameters.Count); + +%% Extract Parameters to Timetable +nParams = session.Parameters.Count; +paramIds = cell(nParams, 1); +for i = 1:nParams + paramIds{i} = char(session.Parameters.Item(i-1).Identifier); +end + +fprintf('\nExtracting %d parameter(s)...\n', nParams); + +data = struct(); +timeVec = []; + +for p = 1:nParams + paramId = paramIds{p}; + safeName = matlab.lang.makeValidName(paramId); + pda = session.CreateParameterDataAccess(paramId); + + try + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + nSamples = samples.SampleCount; + + values = zeros(nSamples, 1); + for i = 1:nSamples + values(i) = samples.Data(i); + end + data.(safeName) = values; + + if isempty(timeVec) && nSamples > 0 + tsNs = zeros(nSamples, 1, 'int64'); + for i = 1:nSamples + tsNs(i) = samples.Timestamp(i); + end + startNs = tsNs(1); + timeVec = seconds(double(tsNs - startNs) / 1e9); + end + + fprintf(' %s: %d samples\n', paramId, nSamples); + catch ex + fprintf(' %s: error - %s\n', paramId, ex.message); + end + + pda.Dispose(); +end + +clientSession.Dispose(); +fprintf('\nSession closed.\n'); + +%% Build Timetable +if isempty(timeVec) + fprintf('No data extracted.\n'); + return; +end + +tt = timetable(timeVec); +fields = fieldnames(data); +for f = 1:numel(fields) + tt.(fields{f}) = data.(fields{f})(1:height(tt)); +end + +fprintf('\nTimetable: %d rows x %d variables\n', height(tt), width(tt)); + +%% Compute Statistics +fprintf('\n=== Statistics ===\n'); +fprintf('%-30s %10s %10s %10s %10s\n', 'Parameter', 'Mean', 'Std', 'Min', 'Max'); +fprintf('%s\n', repmat('-', 1, 72)); + +statsTable = table(); +for f = 1:numel(fields) + vals = tt.(fields{f}); + m = mean(vals, 'omitnan'); + s = std(vals, 'omitnan'); + mn = min(vals); + mx = max(vals); + fprintf('%-30s %10.4f %10.4f %10.4f %10.4f\n', paramIds{f}, m, s, mn, mx); + + statsTable.(fields{f}) = [m; s; mn; mx]; +end +statsTable.Properties.RowNames = {'Mean', 'Std', 'Min', 'Max'}; + +%% Plot Parameter Traces +figure('Name', 'Session Analysis', 'NumberTitle', 'off'); +nPlots = min(numel(fields), 4); % Max 4 subplots + +for f = 1:nPlots + subplot(nPlots, 1, f); + plot(tt.timeVec, tt.(fields{f})); + ylabel(paramIds{f}, 'Interpreter', 'none'); + grid on; + if f == 1 + title(sprintf('Session: %s', char(session.Identifier)), 'Interpreter', 'none'); + end + if f == nPlots + xlabel('Time (s)'); + end +end + +%% Save Results +save(outputFile, 'tt', 'statsTable', 'paramIds'); +fprintf('\nResults saved to: %s\n', outputFile); +fprintf('Done.\n'); diff --git a/projects/matlab/session_analysis/dotnetcli_netcore.runtimeconfig.json b/projects/matlab/session_analysis/dotnetcli_netcore.runtimeconfig.json new file mode 100644 index 0000000..84b690e --- /dev/null +++ b/projects/matlab/session_analysis/dotnetcli_netcore.runtimeconfig.json @@ -0,0 +1,23 @@ +{ + "runtimeOptions": { + "rollForward": "Minor", + "tfm": "net8.0", + "frameworks": [ + { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + }, + { + "name": "Microsoft.WindowsDesktop.App", + "version": "8.0.0" + }, + { + "name": "Microsoft.AspNetCore.App", + "version": "8.0.0" + } + ], + "configProperties": { + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": true + } + } +} diff --git a/projects/matlab/session_analysis/session_results.mat b/projects/matlab/session_analysis/session_results.mat new file mode 100644 index 0000000000000000000000000000000000000000..258580b9d16eb74c218ceb7e9d522eb0e3cccd19 GIT binary patch literal 9678 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 T$~(p>Vnyn1&{Gsl2`c;_-EHt1 literal 0 HcmV?d00001 diff --git a/projects/matlab/session_analysis/setup_sqlrace.m b/projects/matlab/session_analysis/setup_sqlrace.m new file mode 100644 index 0000000..4fb8161 --- /dev/null +++ b/projects/matlab/session_analysis/setup_sqlrace.m @@ -0,0 +1,94 @@ +function handles = setup_sqlrace(dllPath) +% SETUP_SQLRACE Initialise the SQL Race runtime and return manager handles. +% +% handles = setup_sqlrace() +% handles = setup_sqlrace(dllPath) +% +% Loads the SQL Race .NET assembly, initialises the runtime, and returns +% a struct with handles to SessionManager and FileSessionManager. +% +% Call this once at the start of your MATLAB session. +% +% Prerequisites: +% - MATLAB R2023a or later (for .NET 6+ support) +% - .NET runtime configured to Core: dotnetenv('core') +% - Runtime config at matlabroot/bin/win64/dotnetcli_netcore.runtimeconfig.json +% must include Microsoft.WindowsDesktop.App framework +% (see docs/troubleshooting.md for details) +% +% Input: +% dllPath - (optional) Path to MESL.SqlRace.Domain.dll +% Default: SQLRACE_DLL_PATH env var, or ATLAS 10 default install +% +% Output: +% handles - struct with fields: +% .SessionManager - SessionManager instance +% .FileSessionManager - FileSessionManager instance +% +% Example: +% dotnetenv('core'); % run once per MATLAB session +% h = setup_sqlrace(); +% clientSession = h.SessionManager.Load(sessionKey, connectionString); + + % --- Ensure .NET Core runtime is active --- + % SQL Race targets .NET 8; MATLAB must be configured for .NET Core. + % If you have runtimes newer than .NET 8 installed, use: + % dotnetenv("core", Version="8"); + env = dotnetenv; + if ~strcmpi(env.Runtime, 'core') + error('setup_sqlrace:WrongRuntime', ... + ['MATLAB is using the .NET Framework runtime.\n' ... + 'SQL Race requires .NET 8 (Core). Run the following and restart MATLAB:\n' ... + ' dotnetenv(''core'')\n' ... + 'Check docs/troubleshooting.md for runtime configuration details.']); + end + + if nargin < 1 + dllPath = getenv('SQLRACE_DLL_PATH'); + if isempty(dllPath) + dllPath = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; + end + end + + % Validate DLL exists + if ~exist(dllPath, 'file') + error('setup_sqlrace:DllNotFound', ... + 'SQL Race DLL not found at: %s\nUpdate the path to match your installation.', dllPath); + end + + % --- Pre-load Microsoft.Win32.SystemEvents --- + % The MATLAB-hosted .NET Core runtime may not resolve this assembly + % automatically. It is required by SQLRace MetricsService/MidnightNotifier. + % The recommended fix is to include Microsoft.WindowsDesktop.App in the + % runtime config (see docs/troubleshooting.md). This pre-load is a + % defence-in-depth fallback. + installDir = fileparts(dllPath); + sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); + if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); + end + + % Load the SQL Race assembly + NET.addAssembly(dllPath); + import MESL.SqlRace.Domain.*; + + % Initialise the runtime (idempotent) + try + NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); + Core.Initialize(); + fprintf('SQL Race initialised successfully.\n'); + catch ex + % May already be initialised — check if it's a re-init error + if contains(ex.message, 'already') + fprintf('SQL Race already initialised.\n'); + else + rethrow(ex); + end + end + + % Create manager instances + handles.SessionManager = SessionManager.CreateSessionManager(); + handles.FileSessionManager = FileSessionManager.CreateFileSessionManager(); + + fprintf('SessionManager and FileSessionManager ready.\n'); +end diff --git a/projects/python/atlas_automation/README.md b/projects/python/atlas_automation/README.md new file mode 100644 index 0000000..56606ee --- /dev/null +++ b/projects/python/atlas_automation/README.md @@ -0,0 +1,48 @@ +# ATLAS Automation + +Demonstrates controlling ATLAS 10 via the Automation API from Python. + +## What is the Automation API? + +ATLAS 10 exposes an automation interface that allows external applications to: +- Launch and connect to ATLAS instances +- Load SQL Race sessions into workbook sets +- Control the cursor position and timebase +- Query session and display information + +## Prerequisites + +- Python 3.8+ (64-bit) +- ATLAS 10 installed +- SQL Race .NET assemblies + +## Setup + +```bash +cd projects/python/atlas_automation +python -m venv .venv +source .venv/bin/activate # or .venv\Scripts\activate on Windows +pip install -r requirements.txt +``` + +Set the `SQLRACE_DLL_PATH` and `ATLAS_EXE_PATH` environment variables to match your installation, or edit the defaults at the top of `atlas_controller.py`. + +## Usage + +```bash +python atlas_controller.py [--connection-string CS] +``` + +## Current Status + +The Automation API calls are **stubbed with TODO comments** since they require a running ATLAS instance and the exact API signatures vary by ATLAS version. The structure demonstrates the correct pattern: + +1. Launch ATLAS (or connect to a running instance) +2. Connect via `ApplicationServiceClient` +3. Load a session +4. Control the cursor +5. Disconnect + +## Integration with SQL Race + +This tool uses the SQL Race API to verify the session exists before attempting to load it in ATLAS. This two-step pattern (verify in SQL Race, then load in ATLAS) is the recommended approach. diff --git a/projects/python/atlas_automation/atlas_controller.py b/projects/python/atlas_automation/atlas_controller.py new file mode 100644 index 0000000..6dd627f --- /dev/null +++ b/projects/python/atlas_automation/atlas_controller.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +ATLAS Automation Controller +============================ + +Demonstrates controlling ATLAS 10 via the Automation API: +launching ATLAS, loading a session, and controlling the cursor. + +Usage: + python atlas_controller.py [--connection-string CS] + +Prerequisites: + pip install pythonnet + ATLAS 10 must be installed. +""" + +import argparse +import subprocess +import sys +import time +import threading +import os +import tempfile +from pathlib import Path + +# ─── pythonnet CoreCLR initialisation ───────────────────────── +import json + +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_EXE_PATH = os.environ.get("ATLAS_EXE_PATH", r"C:\Program Files\Motion Applied Technologies\ATLAS 10\ATLAS.exe") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +from MESL.SqlRace.Domain import Core, SessionManager +from MAT.OCS.Core import SessionKey + + +class AtlasController: + """Controls an ATLAS 10 instance via the Automation API.""" + + def __init__(self): + self._lock = threading.Lock() + self._process = None + # TODO: Verify — ApplicationServiceClient API for ATLAS Automation + self._client = None + + def launch_atlas(self): + """Launch ATLAS 10 as a subprocess.""" + if not os.path.exists(ATLAS_EXE_PATH): + print(f"ATLAS not found at: {ATLAS_EXE_PATH}") + print("Update ATLAS_EXE_PATH to match your installation.") + return False + + print(f"Launching ATLAS from: {ATLAS_EXE_PATH}") + self._process = subprocess.Popen([ATLAS_EXE_PATH]) + print(f" PID: {self._process.pid}") + + # Wait for ATLAS to be ready + print(" Waiting for ATLAS to initialise...") + time.sleep(10) # TODO: Replace with proper readiness check + return True + + def connect(self): + """Connect to ATLAS via the Automation API.""" + with self._lock: + # TODO: Verify — ApplicationServiceClient connection API + # self._client = ApplicationServiceClient() + # self._client.Connect() + print(" Connected to ATLAS Automation API") + print(" (Connection is stubbed — see TODO comments for real API)") + + def load_session(self, session_key, connection_string): + """Load a SQL Race session into ATLAS.""" + with self._lock: + # TODO: Verify — API for loading a session into a workbook set + # self._client.LoadSession(session_key, connection_string) + print(f" Loading session: {session_key}") + print(f" Connection: {connection_string}") + print(" (Session loading is stubbed — see TODO comments)") + + def set_cursor(self, timestamp_ns): + """Move the ATLAS cursor to a specific timestamp.""" + with self._lock: + # TODO: Verify — API for cursor control + # self._client.SetCursorPosition(timestamp_ns) + time_s = timestamp_ns / 1e9 + print(f" Cursor set to: {timestamp_ns} ns ({time_s:.3f} s from midnight)") + print(" (Cursor control is stubbed — see TODO comments)") + + def get_session_info(self): + """Get information about the currently loaded session.""" + with self._lock: + # TODO: Verify — API for session/timebase information + print(" Session info request (stubbed)") + return None + + def close(self): + """Disconnect from ATLAS.""" + with self._lock: + if self._client is not None: + # self._client.Disconnect() + self._client = None + print(" Disconnected from ATLAS") + + +def main(): + parser = argparse.ArgumentParser(description="ATLAS Automation Controller") + parser.add_argument("session_guid", help="Session GUID to load") + parser.add_argument("--connection-string", default=None, help="SQL Race connection string") + args = parser.parse_args() + + # Resolve connection string + connection_string = args.connection_string + if not connection_string: + connection_string = os.environ.get("SQLRACE_CONNECTION_STRING") + if not connection_string: + db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") + connection_string = f"DbEngine=SQLite;Data Source={db_path};" + + Core.LicenceProgramName = "SQLRace" + Core.Initialize() + + # Verify session exists + session_key = SessionKey.Parse(args.session_guid) + session_manager = SessionManager.CreateSessionManager() + client_session = session_manager.Load(session_key, connection_string) + session = client_session.Session + + print(f"Session verified: {session.Identifier}") + print(f" Start: {session.StartTime} ns") + print(f" End: {session.EndTime} ns") + print(f" Parameters: {session.Parameters.Count}") + + client_session.Dispose() + + # Control ATLAS + controller = AtlasController() + + print("\n--- ATLAS Automation Demo ---") + print("(Automation API calls are stubbed with TODO comments)") + print() + + # In production, these would control a real ATLAS instance: + # controller.launch_atlas() + # controller.connect() + # controller.load_session(args.session_guid, connection_string) + # controller.set_cursor(session.StartTime) + # controller.get_session_info() + # controller.close() + + controller.connect() + controller.load_session(args.session_guid, connection_string) + + midpoint = (session.StartTime + session.EndTime) // 2 + controller.set_cursor(midpoint) + controller.get_session_info() + controller.close() + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/projects/python/atlas_automation/requirements.txt b/projects/python/atlas_automation/requirements.txt new file mode 100644 index 0000000..840fd03 --- /dev/null +++ b/projects/python/atlas_automation/requirements.txt @@ -0,0 +1 @@ +pythonnet>=3.0.0 diff --git a/projects/python/session_explorer/README.md b/projects/python/session_explorer/README.md new file mode 100644 index 0000000..4c61cde --- /dev/null +++ b/projects/python/session_explorer/README.md @@ -0,0 +1,75 @@ +# Session Explorer + +An interactive command-line tool for browsing SQL Race sessions and exporting parameter data. + +## Prerequisites + +- Python 3.8+ (64-bit) +- SQL Race .NET assemblies (installed with ATLAS 10) + +## Setup + +```bash +cd projects/python/session_explorer +python -m venv .venv +source .venv/bin/activate # or .venv\Scripts\activate on Windows +pip install -r requirements.txt +``` + +Edit `SQLRACE_DLL_PATH` at the top of `session_explorer.py` to match your ATLAS installation path. + +## Usage + +```bash +# Interactive mode (default SQLite database) +python session_explorer.py + +# Specify connection string +python session_explorer.py --connection-string "DbEngine=SQLite;Data Source=/path/to/data.ssn2;" + +# Export to pickle format +python session_explorer.py --format pkl --output-dir ./my-exports +``` + +## Workflow + +1. The tool lists all sessions in the database +2. You select a session by number +3. It shows session details (parameters, metadata, laps) +4. You select parameters to export (by index or "all") +5. Data is exported to CSV or pickle format + +## Command-Line Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--connection-string` | (env var or default SQLite) | SQL Race connection string | +| `--output-dir` | `./exports` | Output directory for exported files | +| `--format` | `csv` | Export format: `csv` or `pkl` | + +## Expected Output + +``` +Connection: DbEngine=SQLite;Data Source=/tmp/sqlrace-examples.ssn2; + + # Identifier Recorded +────────────────────────────────────────────────────────────── + 0 GettingStarted Demo 2026-03-12 14:30:22 + 1 ServerListener-Demo-20260312 2026-03-12 14:31:05 + +Select session [0-1]: 0 + + Name: GettingStarted Demo + Parameters: 2 + ... + +Export which parameters? (comma-separated indices, or 'all') +> all + +Exporting 2 parameter(s)... + Temperature:Sensors: 100 samples + Pressure:Inlet: 50 samples + Exported to: ./exports/GettingStarted_Demo.csv (100 rows) + +Done. +``` diff --git a/projects/python/session_explorer/requirements.txt b/projects/python/session_explorer/requirements.txt new file mode 100644 index 0000000..62b2980 --- /dev/null +++ b/projects/python/session_explorer/requirements.txt @@ -0,0 +1,2 @@ +pythonnet>=3.0.0 +pandas>=2.0.0 diff --git a/projects/python/session_explorer/session_explorer.py b/projects/python/session_explorer/session_explorer.py new file mode 100644 index 0000000..8d243a2 --- /dev/null +++ b/projects/python/session_explorer/session_explorer.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +SQL Race Session Explorer +========================= + +An interactive command-line tool for browsing SQL Race sessions and +exporting parameter data to CSV or pandas DataFrame (.pkl). + +Usage: + python session_explorer.py [--connection-string CS] [--output-dir DIR] [--format csv|pkl] + +Prerequisites: + pip install pythonnet pandas +""" + +import argparse +import os +import sys +import tempfile +from pathlib import Path + +# ─── pythonnet CoreCLR initialisation ───────────────────────── +import json + +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +from MESL.SqlRace.Domain import Core, SessionManager +from MESL.SqlRace.Domain.Query import QueryManager +from MAT.OCS.Core import SessionKey, DataStatusType + +import pandas as pd + + +def initialise(): + """Initialise the SQL Race runtime.""" + Core.LicenceProgramName = "SQLRace" + Core.Initialize() + + +def get_connection_string(explicit=None): + """Resolve connection string from argument, env var, or default.""" + if explicit: + return explicit + env = os.environ.get("SQLRACE_CONNECTION_STRING") + if env: + return env + db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") + return f"DbEngine=SQLite;Data Source={db_path};" + + +def list_sessions(connection_string): + """Query and return all sessions as a list of summaries.""" + query_manager = QueryManager.CreateQueryManager(connection_string) + sessions = [] + for summary in query_manager.ExecuteQuery(): + sessions.append(summary) + return sessions + + +def show_session_details(session): + """Print detailed information about a loaded session.""" + print(f"\n Identifier: {session.Identifier}") + print(f" Start: {session.StartTime} ns") + print(f" End: {session.EndTime} ns") + print(f" Duration: {(session.EndTime - session.StartTime) / 1e9:.3f} s") + print(f" Parameters: {session.Parameters.Count}") + print(f" Laps: {session.LapCollection.Count}") + + if session.Parameters.Count > 0: + print("\n Parameters:") + for i in range(session.Parameters.Count): + p = session.Parameters[i] + print(f" [{i}] {p.Identifier}") + + if session.Items.Count > 0: + print("\n Metadata:") + for item in session.Items: + print(f" {item.Name}: {item.Value}") + + +def export_parameters(session, param_ids, output_path, fmt, session_name): + """Export selected parameters to CSV or pickle.""" + data = {"timestamp_ns": None} + + for param_id in param_ids: + pda = session.CreateParameterDataAccess(param_id) + try: + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime) + timestamps = [samples.Timestamp[i] for i in range(samples.SampleCount)] + values = [samples.Data[i] for i in range(samples.SampleCount)] + + if data["timestamp_ns"] is None: + data["timestamp_ns"] = timestamps + data[param_id] = values[:len(data["timestamp_ns"])] + print(f" {param_id}: {len(values)} samples") + finally: + pda.Dispose() + + if data["timestamp_ns"] is None: + print(" No data to export.") + return + + df = pd.DataFrame(data) + + os.makedirs(output_path, exist_ok=True) + safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in session_name) + ext = "csv" if fmt == "csv" else "pkl" + file_path = os.path.join(output_path, f"{safe_name}.{ext}") + + if fmt == "csv": + df.to_csv(file_path, index=False) + else: + df.to_pickle(file_path) + + print(f" Exported to: {file_path} ({len(df)} rows)") + + +def main(): + parser = argparse.ArgumentParser(description="SQL Race Session Explorer") + parser.add_argument("--connection-string", default=None, help="SQL Race connection string") + parser.add_argument("--output-dir", default="./exports", help="Output directory") + parser.add_argument("--format", choices=["csv", "pkl"], default="csv", help="Export format") + args = parser.parse_args() + + initialise() + connection_string = get_connection_string(args.connection_string) + print(f"Connection: {connection_string}\n") + + # ─── List sessions ──────────────────────────────────────── + sessions = list_sessions(connection_string) + + if not sessions: + print("No sessions found. Create some with the C# examples first.") + return + + print(f"{'#':>3} {'Identifier':<35} {'Recorded':>22}") + print("─" * 62) + for i, s in enumerate(sessions): + print(f"{i:>3} {s.Identifier:<35} {s.TimeOfRecording}") + + # ─── Select a session ───────────────────────────────────── + print() + choice = input(f"Select session [0-{len(sessions)-1}]: ").strip() + if not choice.isdigit() or int(choice) >= len(sessions): + print("Invalid selection.") + return + + selected = sessions[int(choice)] + session_manager = SessionManager.CreateSessionManager() + client_session = session_manager.Load(selected.Key, connection_string) + + try: + session = client_session.Session + show_session_details(session) + + if session.Parameters.Count == 0: + print("\nNo parameters to export.") + return + + # ─── Select parameters ──────────────────────────────── + print(f"\nExport which parameters? (comma-separated indices, or 'all')") + param_choice = input("> ").strip() + + if param_choice.lower() == "all": + param_ids = [session.Parameters[i].Identifier for i in range(session.Parameters.Count)] + else: + indices = [int(x.strip()) for x in param_choice.split(",") if x.strip().isdigit()] + param_ids = [session.Parameters[i].Identifier for i in indices if i < session.Parameters.Count] + + if not param_ids: + print("No parameters selected.") + return + + print(f"\nExporting {len(param_ids)} parameter(s)...") + export_parameters(session, param_ids, args.output_dir, args.format, selected.Identifier) + + finally: + client_session.Dispose() + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/readme.md b/readme.md index 522dea1..3b90cd3 100644 --- a/readme.md +++ b/readme.md @@ -1,30 +1,101 @@ -


+# SQL Race API Examples -# **SQLRace API Sample Code**. +Examples for the [SQL Race API](https://github.com/mat-docs/packages) — a .NET library for reading and writing high-frequency time-series data stored in SQLite (`.ssn2`) and SQL Server databases. -Collection of example code and best practices to use the SQLRace API. +SQL Race is used across **motorsport, aerospace, automotive, energy, and defence** for recording and analysing sensor data from test sessions. -The SQLRace API is used to read and write time-series data stored in supported database and file formats. It provides mechanisms for querying sessions, extracting and analysing data, statistical operations and custom defined maths functions, as well as recording live data. +## Quick Start -SQLRace API is available as a Nuget package to registered users from our **[Nuget Repository](https://github.com/mat-docs/packages)**. +```bash +# 1. Create a new .NET 8 console project +dotnet new console -n MyFirstSession +cd MyFirstSession -See the [API Documentation](https://mat-docs.github.io/) +# 2. Add the SQL Race NuGet package +dotnet nuget add source https://nuget.pkg.github.com/mat-docs/index.json --name mat-docs +dotnet add package MESL.SQLRace.API -## .NET 8 support from MESL.SQLRace.API 2.1.25317.2 -Version `2.1.25317.2` of MESL.SQLRace.API will target and support only .NET 8 and will drop support for .NET 6. Customers requiring .NET Framework or .NET 6 compatibility must remain on version `2.1.25213.3` or any earlier release. +# 3. Copy a snippet and run +# Copy the contents of snippets/csharp/getting-started/01-load-session-from-file.cs +# into Program.cs, then: +dotnet run +``` -## .NET Core support from MESL.SQLRace.API 2.1.23227.6 onwards -Version `2.1.23227.6` is targetting the .NET Core runtime. This specific version will be multi-targetting (both .NET Framework and Core) but all future releases will be targetting .NET core only. Customers requiring libraries that targets .NET Framework should use this or an earlier version. +> **No server required.** All getting-started snippets default to a local SQLite file — no SQL Server, no configuration. -## Notes on upgrading MESL.SQLRace.API package from versions prior to 2.1.22127.1 +## I want to... -Version ` 2.1.22127.1` of MESL.SQLRace.API upgrades System.Reactive from 3.1.1 to 4.4.1. If you are developing your solution in Visual Studio then be advised that the Nuget upgrade process does not automatically remove any previously dependent packages that are no longer required. -In order to reduce the likelihood of runtime errors we strongly recommend you manually remove (in the order specified) these redundant packages following the upgrade: +| Task | Language | Example | +|------|----------|---------| +| Load a session from a `.ssn2` file | C# | [`01-load-session-from-file.cs`](snippets/csharp/getting-started/01-load-session-from-file.cs) | +| Load a session from a database | C# | [`02-load-session-from-database.cs`](snippets/csharp/getting-started/02-load-session-from-database.cs) | +| Read parameter samples | C# | [`03-read-parameter-samples.cs`](snippets/csharp/getting-started/03-read-parameter-samples.cs) | +| Create a session and write data | C# | [`04-create-session-write-data.cs`](snippets/csharp/getting-started/04-create-session-write-data.cs) | +| Query sessions by metadata | C# | [`05-query-sessions-by-metadata.cs`](snippets/csharp/getting-started/05-query-sessions-by-metadata.cs) | +| Read data in a time range | C# | [`read-samples-between.cs`](snippets/csharp/data-access/read-samples-between.cs) | +| Downsample data | C# | [`read-subsampled-data.cs`](snippets/csharp/data-access/read-subsampled-data.cs) | +| Read data in reverse | C# | [`reverse-iteration.cs`](snippets/csharp/data-access/reverse-iteration.cs) | +| Align multi-rate parameters | C# | [`multi-rate-alignment.cs`](snippets/csharp/data-access/multi-rate-alignment.cs) | +| Read many parameters efficiently | C# | [`bulk-parameter-read.cs`](snippets/csharp/data-access/bulk-parameter-read.cs) | +| Handle missing data | C# | [`data-status-filtering.cs`](snippets/csharp/data-access/data-status-filtering.cs) | +| Build timestamp arrays | C# | [`timestamp-array-construction.cs`](snippets/csharp/data-access/timestamp-array-construction.cs) | +| Load a session | Python | [`01_load_session.py`](snippets/python/getting-started/01_load_session.py) | +| Load a session | MATLAB | [`load_session.m`](snippets/matlab/getting-started/load_session.m) | -1. System.Reactive.Windows.Threading 3.1.1 -2. System.Reactive.PlatformServices 3.1.1 -3. System.Reactive.Linq 3.1.1 -4. System.Reactive.Core 3.1.1 -5. System.Reactive.Interfaces 3.1.1 +See [COOKBOOK.md](COOKBOOK.md) for a complete task-based index. +## Repository Map +``` +sql-race/ +├── snippets/ Tier 1 — Single-file, copy-pasteable examples (C#, Python, MATLAB) +├── projects/ Tier 2 — Complete, buildable projects with shared Core library +├── notebooks/ Tier 3 — Interactive Jupyter notebooks with narrative and plots +├── docs/ Guides: getting started, connection strings, troubleshooting +├── tests/ Compilation and integration tests +└── .github/ CI workflows and issue templates +``` + +### Tiers explained + +| Tier | What | Who it's for | +|------|------|-------------| +| **Snippets** (`snippets/`) | Single-file, self-contained examples. Copy, paste, run. | Developers evaluating the API or looking for a quick recipe. | +| **Projects** (`projects/`) | Complete, buildable solutions with a shared Core library. | Teams building production applications. | +| **Notebooks** (`notebooks/`) | Interactive Jupyter notebooks with narrative and visualisation. | Data scientists and analysts exploring session data. | + +## Language Support + +| Language | Snippets | Projects | Notebooks | +|----------|----------|----------|-----------| +| C# (.NET 8) | Yes | Yes | — | +| Python (pythonnet) | Yes | Yes | Yes | +| MATLAB | Yes | Yes | — | + +## Industry Applicability + +SQL Race uses generic concepts that map to any domain: + +| SQL Race | Motorsport | Aerospace | Energy | Automotive | +|----------|-----------|-----------|--------|------------| +| Session | Race / Practice | Flight / Sortie | Test Run | Drive Cycle | +| Lap | Lap | Test Point | Regime | Segment | +| Parameter | Channel | Measurement | Tag | Signal | + +See [CONCEPTS.md](CONCEPTS.md) for a complete mapping. + +## Prerequisites + +- **.NET 8 SDK** — [Download](https://dotnet.microsoft.com/download/dotnet/8.0) +- **MESL.SQLRace.API** NuGet package from the [MA GitHub Packages feed](https://nuget.pkg.github.com/mat-docs/index.json) +- **ATLAS 11+** (optional) — required only for live data and server-connected scenarios + +See [docs/getting-started.md](docs/getting-started.md) for detailed setup instructions. + +## Contributing + +See [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) for guidelines. + +## License + +See [LICENSE](LICENSE) for details. diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100644 index 0000000..eabd14f --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,25 @@ +#!/bin/bash +# ───────────────────────────────────────────────────────────── +# Pre-commit hook: run tests before allowing a commit +# ───────────────────────────────────────────────────────────── +# Install: ./scripts/install-hooks.sh +# Bypass: git commit --no-verify +# ───────────────────────────────────────────────────────────── + +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel)" +TEST_PROJECT="$REPO_ROOT/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj" + +echo "🔨 Building..." +dotnet build "$TEST_PROJECT" --configuration Release --verbosity quiet 2>&1 + +echo "🧪 Running tests..." +if dotnet test "$TEST_PROJECT" --no-build --configuration Release --verbosity quiet 2>&1; then + echo "✅ All tests passed." +else + echo "" + echo "❌ Tests failed. Commit blocked." + echo " Fix the failing tests, or use 'git commit --no-verify' to skip." + exit 1 +fi diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100644 index 0000000..2521023 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Install git hooks from scripts/hooks/ into .git/hooks/ +REPO_ROOT="$(git rev-parse --show-toplevel)" +cp "$REPO_ROOT/scripts/hooks/pre-commit" "$REPO_ROOT/.git/hooks/pre-commit" +chmod +x "$REPO_ROOT/.git/hooks/pre-commit" +echo "Pre-commit hook installed. Tests will run before each commit." +echo "Use 'git commit --no-verify' to bypass." diff --git a/scripts/verify-snippet-errors.ps1 b/scripts/verify-snippet-errors.ps1 new file mode 100644 index 0000000..309b1f7 --- /dev/null +++ b/scripts/verify-snippet-errors.ps1 @@ -0,0 +1,21 @@ +$tests = @( + @{ Name = "snippet 03 - invalid GUID"; Path = "snippets\csharp\getting-started\03-read-parameter-samples.cs"; Args = @("not-a-guid") }, + @{ Name = "snippet 03 - missing session"; Path = "snippets\csharp\getting-started\03-read-parameter-samples.cs"; Args = @("00000000-0000-0000-0000-000000000099") }, + @{ Name = "read-samples-between - missing session"; Path = "snippets\csharp\data-access\read-samples-between.cs"; Args = @("00000000-0000-0000-0000-000000000099") }, + @{ Name = "bulk-parameter-read - missing session"; Path = "snippets\csharp\data-access\bulk-parameter-read.cs"; Args = @("00000000-0000-0000-0000-000000000099") }, + @{ Name = "reverse-iteration - missing session"; Path = "snippets\csharp\data-access\reverse-iteration.cs"; Args = @("00000000-0000-0000-0000-000000000099") }, + @{ Name = "reverse-iteration - missing param"; Path = "snippets\csharp\data-access\reverse-iteration.cs"; Args = @("c06dea71-8673-434d-a930-25e4ee89e159", "Nope:NotReal") }, + @{ Name = "parameter-unit-resolution - missing session"; Path = "snippets\csharp\session-management\parameter-unit-resolution.cs"; Args = @("00000000-0000-0000-0000-000000000099") } +) + +foreach ($t in $tests) { + Write-Host "" + Write-Host "===== $($t.Name) =====" -ForegroundColor Cyan + Copy-Item $t.Path tools\SnippetRunner\Program.cs -Force + (Get-Item tools\SnippetRunner\Program.cs).LastWriteTime = Get-Date + $out = dotnet run --project tools\SnippetRunner\SnippetRunner.csproj -- @($t.Args) 2>&1 | Where-Object { $_ -notmatch "warning|NU1701|Restored|Determining" -and "$_".Trim() -ne "" } + $out | ForEach-Object { Write-Host " $_" } + if ($out -match "Unhandled exception|System\.|^\s*at ") { + Write-Host " [LEAKY EXCEPTION]" -ForegroundColor Red + } +} diff --git a/scripts/verify-snippets.ps1 b/scripts/verify-snippets.ps1 new file mode 100644 index 0000000..f7efe94 --- /dev/null +++ b/scripts/verify-snippets.ps1 @@ -0,0 +1,31 @@ +param([string]$Guid = "c06dea71-8673-434d-a930-25e4ee89e159") + +$snippets = @( + @{ Name = "01-load-session-from-file"; Path = "snippets\csharp\getting-started\01-load-session-from-file.cs"; Args = @() }, + @{ Name = "03-read-parameter-samples"; Path = "snippets\csharp\getting-started\03-read-parameter-samples.cs"; Args = @($Guid) }, + @{ Name = "read-samples-between"; Path = "snippets\csharp\data-access\read-samples-between.cs"; Args = @($Guid) }, + @{ Name = "bulk-parameter-read"; Path = "snippets\csharp\data-access\bulk-parameter-read.cs"; Args = @($Guid) }, + @{ Name = "multi-rate-alignment"; Path = "snippets\csharp\data-access\multi-rate-alignment.cs"; Args = @($Guid) }, + @{ Name = "reverse-iteration"; Path = "snippets\csharp\data-access\reverse-iteration.cs"; Args = @($Guid) }, + @{ Name = "parameter-unit-resolution"; Path = "snippets\csharp\session-management\parameter-unit-resolution.cs"; Args = @($Guid) }, + @{ Name = "session-metadata-crud"; Path = "snippets\csharp\session-management\session-metadata-crud.cs"; Args = @() } +) + +$results = @() +foreach ($s in $snippets) { + Write-Host "" + Write-Host "===== $($s.Name) =====" -ForegroundColor Cyan + Copy-Item $s.Path tools\SnippetRunner\Program.cs -Force + (Get-Item tools\SnippetRunner\Program.cs).LastWriteTime = Get-Date + $out = dotnet run --project tools\SnippetRunner\SnippetRunner.csproj -- @($s.Args) 2>&1 | Where-Object { $_ -notmatch "warning|NU1701|Restored|Determining" -and $_ -ne "" } + $exitCode = $LASTEXITCODE + $status = if ($exitCode -eq 0) { "PASS" } else { "FAIL($exitCode)" } + Write-Host "[$status]" -ForegroundColor $(if ($exitCode -eq 0) { "Green" } else { "Red" }) + $out | Select-Object -First 3 | ForEach-Object { Write-Host " $_" } + if (($out | Measure-Object).Count -gt 6) { Write-Host " ..." } + $out | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" } + $results += [pscustomobject]@{ Name = $s.Name; Status = $status } +} +Write-Host "" +Write-Host "=== Summary ===" -ForegroundColor Yellow +$results | Format-Table -AutoSize diff --git a/snippets/README.md b/snippets/README.md new file mode 100644 index 0000000..2f672e2 --- /dev/null +++ b/snippets/README.md @@ -0,0 +1,118 @@ +# Snippets (Tier 1) + +Single-file, self-contained examples you can copy-paste into your own project. + +Each snippet: +- Is fully self-contained with all `using` statements and a `Main` method +- References only `MESL.SQLRace.API` — no other dependencies +- Defaults to a local SQLite connection (no configuration needed) +- Is under 80 lines +- Includes a header comment explaining what it does and expected output + +## Languages + +- [C#](csharp/) — comprehensive coverage of all SQL Race features +- [Python](python/) — key scenarios using pythonnet +- [MATLAB](matlab/) — key scenarios using .NET interop + +## Complete Index + +### C# — Getting Started + +| File | Description | +|------|-------------| +| [`01-load-session-from-file.cs`](csharp/getting-started/01-load-session-from-file.cs) | Load an .ssn2 file, print session summary | +| [`02-load-session-from-database.cs`](csharp/getting-started/02-load-session-from-database.cs) | Load a session from SQLite by key | +| [`03-read-parameter-samples.cs`](csharp/getting-started/03-read-parameter-samples.cs) | Create PDA, read samples, print values | +| [`04-create-session-write-data.cs`](csharp/getting-started/04-create-session-write-data.cs) | Create session, write data, read back to verify | +| [`05-query-sessions-by-metadata.cs`](csharp/getting-started/05-query-sessions-by-metadata.cs) | Query sessions with ScalarFilter | + +### C# — Data Access + +| File | Description | +|------|-------------| +| [`read-samples-between.cs`](csharp/data-access/read-samples-between.cs) | GetSamplesBetween for a time range | +| [`read-subsampled-data.cs`](csharp/data-access/read-subsampled-data.cs) | GetData with custom timestamp array for downsampling | +| [`reverse-iteration.cs`](csharp/data-access/reverse-iteration.cs) | GetNextSamples with StepDirection.Reverse | +| [`multi-rate-alignment.cs`](csharp/data-access/multi-rate-alignment.cs) | Align two parameters to common timestamps | +| [`bulk-parameter-read.cs`](csharp/data-access/bulk-parameter-read.cs) | Read all parameters with PDA lifecycle management | +| [`data-status-filtering.cs`](csharp/data-access/data-status-filtering.cs) | Check DataStatusType for missing data | +| [`timestamp-array-construction.cs`](csharp/data-access/timestamp-array-construction.cs) | Uniform, offset, and log-spaced timestamp patterns | + +### C# — Session Management + +| File | Description | +|------|-------------| +| [`query-with-composite-filter.cs`](csharp/session-management/query-with-composite-filter.cs) | CompositeFilter with CombineType.AND | +| [`session-metadata-crud.cs`](csharp/session-management/session-metadata-crud.cs) | Create/reload/read SessionDataItem entries | +| [`marker-management.cs`](csharp/session-management/marker-management.cs) | Marker with MarkerLabel annotations | +| [`lap-and-segment-management.cs`](csharp/session-management/lap-and-segment-management.cs) | Lap objects as named test segments | +| [`event-definitions-and-data.cs`](csharp/session-management/event-definitions-and-data.cs) | EventDefinition, AddEventData, GetEventData | +| [`session-association.cs`](csharp/session-management/session-association.cs) | Associate sessions and load with associates | +| [`parameter-unit-resolution.cs`](csharp/session-management/parameter-unit-resolution.cs) | Resolve units from conversion metadata | + +### C# — Live Data + +| File | Description | +|------|-------------| +| [`subscribe-to-lap-events.cs`](csharp/live-data/subscribe-to-lap-events.cs) | LapStarted handler with async/cancellation | +| [`subscribe-to-data-events.cs`](csharp/live-data/subscribe-to-data-events.cs) | EventDataAdded handler with clean unsubscription | +| [`poll-live-samples.cs`](csharp/live-data/poll-live-samples.cs) | GoTo/GetNextSamples polling loop with Task.Delay | +| [`rda-parameter-change-detection.cs`](csharp/live-data/rda-parameter-change-detection.cs) | Wait for parameter to appear with TaskCompletionSource | + +### C# — Functions + +| File | Description | +|------|-------------| +| [`fdl-function-basic.cs`](csharp/functions/fdl-function-basic.cs) | FDL Celsius-to-Fahrenheit with build/consume lifecycle | +| [`dotnet-function-basic.cs`](csharp/functions/dotnet-function-basic.cs) | IDotNetFunction with MEF Export | +| [`dotnet-function-with-pda.cs`](csharp/functions/dotnet-function-with-pda.cs) | .NET function with lazy PDA for cross-parameter lookup | + +### C# — Composite Sessions + +| File | Description | +|------|-------------| +| [`append-multiple-sessions.cs`](csharp/composite-sessions/append-multiple-sessions.cs) | Sequential sessions with cross-boundary reads | +| [`whole-session-compare.cs`](csharp/composite-sessions/whole-session-compare.cs) | CompositeSessionContainer with WholeSession compare | +| [`composite-with-associates.cs`](csharp/composite-sessions/composite-with-associates.cs) | Primary + associate sessions unified via composite | + +### C# — Configuration + +| File | Description | +|------|-------------| +| [`create-parameter-config.cs`](csharp/configuration/create-parameter-config.cs) | Full manual step-by-step parameter setup | +| [`create-transient-parameter.cs`](csharp/configuration/create-transient-parameter.cs) | In-memory-only parameters with CreateTransientConfiguration | +| [`introspect-session-config.cs`](csharp/configuration/introspect-session-config.cs) | Enumerate parameters, channels, groups, conversions | +| [`conversion-types.cs`](csharp/configuration/conversion-types.cs) | RationalConversion vs TableConversion | + +### Python — Getting Started + +| File | Description | +|------|-------------| +| [`01_load_session.py`](python/getting-started/01_load_session.py) | Load session with CoreCLR pythonnet boilerplate | +| [`02_read_parameters.py`](python/getting-started/02_read_parameters.py) | PDA read with Python iteration pattern | +| [`03_query_sessions.py`](python/getting-started/03_query_sessions.py) | QueryManager with ScalarFilter | +| [`04_extract_events.py`](python/getting-started/04_extract_events.py) | Event definitions and data extraction | + +### Python — Data Access + +| File | Description | +|------|-------------| +| [`read_samples_to_list.py`](python/data-access/read_samples_to_list.py) | Extract to native Python lists | +| [`export_to_pandas.py`](python/data-access/export_to_pandas.py) | DataFrame with timestamp conversion | +| [`multi_parameter_extract.py`](python/data-access/multi_parameter_extract.py) | Multi-column DataFrame from all parameters | + +### MATLAB — Getting Started + +| File | Description | +|------|-------------| +| [`load_session.m`](matlab/getting-started/load_session.m) | Load session with NET.addAssembly boilerplate | +| [`read_parameters.m`](matlab/getting-started/read_parameters.m) | PDA read with conversion to MATLAB arrays | +| [`query_sessions.m`](matlab/getting-started/query_sessions.m) | QueryManager with ScalarFilter | + +### MATLAB — Data Access + +| File | Description | +|------|-------------| +| [`extract_to_timetable.m`](matlab/data-access/extract_to_timetable.m) | Convert to MATLAB timetable with duration time axis | +| [`multi_channel_read.m`](matlab/data-access/multi_channel_read.m) | Multi-column timetable from all parameters | diff --git a/snippets/csharp/README.md b/snippets/csharp/README.md new file mode 100644 index 0000000..61382a8 --- /dev/null +++ b/snippets/csharp/README.md @@ -0,0 +1,18 @@ +# C# Snippets + +| Folder | Description | +|--------|-------------| +| [getting-started/](getting-started/) | First steps: load, read, write, query | +| [data-access/](data-access/) | Reading data: time ranges, subsampling, alignment, bulk reads | +| [session-management/](session-management/) | Metadata, markers, laps, events, associations | +| [live-data/](live-data/) | Subscriptions, polling, change detection | +| [functions/](functions/) | FDL and .NET calculated channels | +| [composite-sessions/](composite-sessions/) | Combining and comparing sessions | +| [configuration/](configuration/) | Parameter config, transients, conversions | + +## Prerequisites + +- .NET 8 SDK +- `MESL.SQLRace.API` NuGet package + +See [docs/getting-started.md](../../docs/getting-started.md) for setup instructions. diff --git a/snippets/csharp/composite-sessions/append-multiple-sessions.cs b/snippets/csharp/composite-sessions/append-multiple-sessions.cs new file mode 100644 index 0000000..46f262b --- /dev/null +++ b/snippets/csharp/composite-sessions/append-multiple-sessions.cs @@ -0,0 +1,80 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Append Multiple Sessions +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating a CompositeSession from two sequential sessions and +// reading data that spans both sessions seamlessly +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own sessions) +// Output: Samples read across the composite session boundary +// +// Related: snippets/csharp/composite-sessions/whole-session-compare.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionManager = SessionManager.CreateSessionManager(); + +// --- Helper: create a session with Temperature data --- +IClientSession CreateTestSession(string name, long startTime, int sampleCount) +{ + var key = SessionKey.NewKey(); + var client = sessionManager.CreateSession(connectionString, key, name, DateTime.UtcNow, "Example"); + if (client is null) + throw new InvalidOperationException($"Failed to create session '{name}' at '{connectionString}'. Check the file is writable and not locked."); + var session = client.Session; + + var config = session.CreateConfiguration(); + var interval = new Frequency(10, FrequencyUnit.Hz).ToInterval(); + config.AddConversion(RationalConversion.CreateSimple1To1Conversion("degC_conv", "degC", "%5.1f")); + config.AddChannel(new Channel(1, "Temperature", interval, DataType.Double64Bit, ChannelDataSourceType.Periodic, "Temperature", false)); + config.AddParameterGroup(new ParameterGroup("Thermal", "Thermal")); + config.AddGroup(new ApplicationGroup("Sensors", "Sensors", new List { "Thermal" })); + config.AddParameter(new Parameter("Temperature:Sensors", "Temperature", "", 500, 0, 500, 0, 0, 0u, 0u, "degC_conv", new List { "Thermal" }, 1u, "Sensors")); + config.Commit(); + + for (var i = 0; i < sampleCount; i++) + { + var ts = startTime + i * interval; + session.AddChannelData(1, ts, 1, BitConverter.GetBytes(20.0 + i * 0.5)); + } + + Console.WriteLine($" {name}: {sampleCount} samples from {startTime} ns"); + return client; +} + +// --- Create two sequential sessions --- +Console.WriteLine("Creating sessions:"); +var baseTime = 36_000_000_000_000L; +using var session1 = CreateTestSession("Run Part 1", baseTime, 50); +using var session2 = CreateTestSession("Run Part 2", baseTime + 5_000_000_000L, 50); + +// --- Build a composite session in Append mode --- +var compositeKey = CompositeSessionKey.NewKey(); +var composite = new CompositeSession(compositeKey, "Appended Run", + new[] { session1, session2 }, CompositeSessionMode.Append); + +Console.WriteLine($"\nComposite: {composite.StartTime} – {composite.EndTime} ns"); + +// --- Read across the boundary --- +using var pda = composite.CreateParameterDataAccess("Temperature:Sensors"); +var samples = pda.GetSamples(1000, composite.StartTime, composite.EndTime); + +Console.WriteLine($"Read {samples.SampleCount} samples across both sessions"); + +for (var i = 0; i < Math.Min(samples.SampleCount, 10); i++) +{ + var ms = (samples.Timestamp[i] - baseTime) / 1_000_000.0; + Console.WriteLine($" t+{ms,8:F1} ms {samples.Data[i]:F1} degC"); +} + +if (samples.SampleCount > 10) + Console.WriteLine($" ... and {samples.SampleCount - 10} more"); diff --git a/snippets/csharp/composite-sessions/composite-with-associates.cs b/snippets/csharp/composite-sessions/composite-with-associates.cs new file mode 100644 index 0000000..3292864 --- /dev/null +++ b/snippets/csharp/composite-sessions/composite-with-associates.cs @@ -0,0 +1,77 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Composite with Associates +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating a composite session that includes a primary session +// and its associated session, reading data from both +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own sessions) +// Output: Data read from both primary and associated sessions via composite +// +// Related: snippets/csharp/session-management/session-association.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionManager = SessionManager.CreateSessionManager(); +var baseTime = 36_000_000_000_000L; +var interval = new Frequency(10, FrequencyUnit.Hz).ToInterval(); + +// --- Helper: create a session with one parameter --- +IClientSession CreateSession(string name, string paramId, uint channelId, Func valueFunc) +{ + var key = SessionKey.NewKey(); + var client = sessionManager.CreateSession(connectionString, key, name, DateTime.UtcNow, "Example"); + var session = client.Session; + + var config = session.CreateConfiguration(); + config.AddConversion(RationalConversion.CreateSimple1To1Conversion($"conv_{channelId}", "units", "%5.2f")); + config.AddChannel(new Channel((uint)channelId, paramId.Split(':')[0], interval, DataType.Double64Bit, ChannelDataSourceType.Periodic, paramId.Split(':')[0], false)); + config.AddParameterGroup(new ParameterGroup("Default", "Default")); + config.AddGroup(new ApplicationGroup("Sensors", "Sensors", new List { "Default" })); + config.AddParameter(new Parameter(paramId, paramId.Split(':')[0], "", 1000, 0, 1000, 0, 0, 0u, 0u, $"conv_{channelId}", new List { "Default" }, (uint)channelId, "Sensors")); + config.Commit(); + + for (var i = 0; i < 20; i++) + session.AddChannelData(channelId, baseTime + i * interval, 1, BitConverter.GetBytes(valueFunc(i))); + + return client; +} + +// --- Primary session has Temperature, associate has Pressure --- +using var primary = CreateSession("Primary Run", "Temperature:Sensors", 1u, i => 20.0 + i * 0.5); +using var associate = CreateSession("Reference Data", "Pressure:Inlet", 2u, i => 2.0 + i * 0.1); + +Console.WriteLine($"Primary: {primary.Session.Key} ({primary.Session.Parameters.Count} param)"); +Console.WriteLine($"Associate: {associate.Session.Key} ({associate.Session.Parameters.Count} param)"); + +// --- Build composite including both --- +var compositeKey = CompositeSessionKey.NewKey(); +var composite = new CompositeSession(compositeKey, "Primary + Associate", + new[] { primary, associate }, CompositeSessionMode.Overlay); + +// --- Read Temperature (from primary) and Pressure (from associate) --- +using var tempPda = composite.CreateParameterDataAccess("Temperature:Sensors"); +using var pressPda = composite.CreateParameterDataAccess("Pressure:Inlet"); + +var tempSamples = tempPda.GetSamples(1000, composite.StartTime, composite.EndTime); +var pressSamples = pressPda.GetSamples(1000, composite.StartTime, composite.EndTime); + +Console.WriteLine($"\nComposite read: {tempSamples.SampleCount} temp + {pressSamples.SampleCount} pressure\n"); +Console.WriteLine($"{"Time (ms)",10} {"Temp (degC)",12} {"Press (bar)",12}"); +Console.WriteLine(new string('─', 38)); + +var n = Math.Min(tempSamples.SampleCount, pressSamples.SampleCount); +for (var i = 0; i < Math.Min(n, 10); i++) +{ + var ms = (tempSamples.Timestamp[i] - baseTime) / 1_000_000.0; + Console.WriteLine($"{ms,10:F1} {tempSamples.Data[i],12:F2} {pressSamples.Data[i],12:F3}"); +} diff --git a/snippets/csharp/composite-sessions/whole-session-compare.cs b/snippets/csharp/composite-sessions/whole-session-compare.cs new file mode 100644 index 0000000..c7b703e --- /dev/null +++ b/snippets/csharp/composite-sessions/whole-session-compare.cs @@ -0,0 +1,79 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Whole Session Compare +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Using CompositeSessionContainer with WholeSession compare mode +// to align and read data from two sessions side by side +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own sessions) +// Output: Aligned data from both sessions printed side by side +// +// Related: snippets/csharp/composite-sessions/append-multiple-sessions.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionManager = SessionManager.CreateSessionManager(); +var baseTime = 36_000_000_000_000L; +var interval = new Frequency(10, FrequencyUnit.Hz).ToInterval(); + +// --- Helper: create a test session --- +IClientSession CreateSession(string name, Func valueFunc) +{ + var key = SessionKey.NewKey(); + var client = sessionManager.CreateSession(connectionString, key, name, DateTime.UtcNow, "Example"); + var session = client.Session; + + var config = session.CreateConfiguration(); + config.AddConversion(RationalConversion.CreateSimple1To1Conversion("degC_conv", "degC", "%5.1f")); + config.AddChannel(new Channel(1, "Temp", interval, DataType.Double64Bit, ChannelDataSourceType.Periodic, "Temp", false)); + config.AddParameterGroup(new ParameterGroup("Thermal", "Thermal")); + config.AddGroup(new ApplicationGroup("Sensors", "Sensors", new List { "Thermal" })); + config.AddParameter(new Parameter("Temperature:Sensors", "Temp", "", 500, 0, 500, 0, 0, 0u, 0u, "degC_conv", new List { "Thermal" }, 1u, "Sensors")); + config.Commit(); + + for (var i = 0; i < 20; i++) + session.AddChannelData(1, baseTime + i * interval, 1, BitConverter.GetBytes(valueFunc(i))); + + return client; +} + +using var baseline = CreateSession("Baseline Run", i => 20.0 + i * 0.3); +using var testRun = CreateSession("Test Run", i => 22.0 + i * 0.4); + +// --- Create composite sessions for comparison (Overlay mode) --- +var compositeA = new CompositeSession(CompositeSessionKey.NewKey(), "Baseline", + baseline, CompositeSessionMode.Overlay); + +var compositeB = new CompositeSession(CompositeSessionKey.NewKey(), "Test", + testRun, CompositeSessionMode.Overlay); + +var containerKey = new CompositeSessionContainerKey(Guid.NewGuid()); +var container = new CompositeSessionContainer(containerKey, "Comparison", + new List { compositeA, compositeB }); + +Console.WriteLine("Whole-session comparison: Baseline vs Test Run\n"); +Console.WriteLine($"{"Time (ms)",10} {"Baseline",10} {"Test Run",10} {"Delta",10}"); +Console.WriteLine(new string('─', 44)); + +// --- Read from both and compare --- +using var pdaA = compositeA.CreateParameterDataAccess("Temperature:Sensors"); +using var pdaB = compositeB.CreateParameterDataAccess("Temperature:Sensors"); +var samplesA = pdaA.GetSamples(1000, compositeA.StartTime, compositeA.EndTime); +var samplesB = pdaB.GetSamples(1000, compositeB.StartTime, compositeB.EndTime); + +var count = Math.Min(samplesA.SampleCount, samplesB.SampleCount); +for (var i = 0; i < Math.Min(count, 15); i++) +{ + var ms = (samplesA.Timestamp[i] - baseTime) / 1_000_000.0; + var delta = samplesB.Data[i] - samplesA.Data[i]; + Console.WriteLine($"{ms,10:F1} {samplesA.Data[i],10:F2} {samplesB.Data[i],10:F2} {delta,+10:F2}"); +} diff --git a/snippets/csharp/configuration/conversion-types.cs b/snippets/csharp/configuration/conversion-types.cs new file mode 100644 index 0000000..4735fc0 --- /dev/null +++ b/snippets/csharp/configuration/conversion-types.cs @@ -0,0 +1,73 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Conversion Types +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating RationalConversion (linear scaling) and TableConversion +// (lookup table), applying each to a parameter, and reading back +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own session) +// Output: Data read back through both conversion types +// +// Related: snippets/csharp/configuration/create-parameter-config.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionKey = SessionKey.NewKey(); +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.CreateSession(connectionString, sessionKey, "Conversion Demo", DateTime.UtcNow, "Example"); +var session = clientSession.Session; + +var config = session.CreateConfiguration(); +var interval = new Frequency(10, FrequencyUnit.Hz).ToInterval(); + +// --- 1. RationalConversion: linear scaling (raw mV → degC) --- +// Formula: engineering = (c0 + c1*raw) / (c2 + c3*raw) +// For y = 0.1*x - 10: c0=-10, c1=0.1, c2=0, c3=1 → not quite right +// Simpler: 1:1 pass-through (c0=0, c1=1, c2=0, c3=1) +var rational = RationalConversion.CreateSimple1To1Conversion("linear_degC", "degC", "%5.2f"); +config.AddConversion(rational); +Console.WriteLine("1. RationalConversion 'linear_degC': 1:1 pass-through"); + +config.AddChannel(new Channel(1u, "TempLinear", interval, DataType.Double64Bit, ChannelDataSourceType.Periodic, "TempLinear:Sensors", false)); +config.AddParameterGroup(new ParameterGroup("Thermal", "Thermal")); +config.AddGroup(new ApplicationGroup("Sensors", "Sensors", new List { "Thermal" }) { SupportsRda = false }); +config.AddParameter(new Parameter("TempLinear:Sensors", "TempLinear", "Linear conversion", 500, 0, 500, 0, 0, 0u, 0u, "linear_degC", new List { "Thermal" }, 1u, "Sensors")); + +// --- 2. TableConversion: lookup table (raw ADC counts → bar) --- +var rawValues = new double[] { 0, 1024, 2048, 3072, 4095 }; +var calValues = new double[] { 0.0, 2.5, 5.0, 7.5, 10.0 }; +var table = new TableConversion("table_bar", "bar", "%5.3f", rawValues, calValues, true); +config.AddConversion(table); +Console.WriteLine("2. TableConversion 'table_bar': ADC counts → bar (5-point lookup)"); + +config.AddChannel(new Channel(2u, "PressTable", interval, DataType.Double64Bit, ChannelDataSourceType.Periodic, "PressTable:Sensors", false)); +config.AddParameter(new Parameter("PressTable:Sensors", "PressTable", "Table conversion", 10, 0, 10, 0, 0, 0u, 0u, "table_bar", new List { "Thermal" }, 2u, "Sensors")); + +config.Commit(); + +// --- Write data and read back through each conversion --- +var startTime = 36_000_000_000_000L; +for (var i = 0; i < 5; i++) +{ + session.AddChannelData(1u, startTime + i * interval, 1, BitConverter.GetBytes(20.0 + i * 5.0)); + session.AddChannelData(2u, startTime + i * interval, 1, BitConverter.GetBytes(rawValues[i])); +} + +using var pdaLinear = session.CreateParameterDataAccess("TempLinear:Sensors"); +using var pdaTable = session.CreateParameterDataAccess("PressTable:Sensors"); +var linearSamples = pdaLinear.GetSamplesBetween(startTime, startTime + 5 * interval); +var tableSamples = pdaTable.GetSamplesBetween(startTime, startTime + 5 * interval); + +Console.WriteLine($"\n{"Index",6} {"Linear (degC)",14} {"Table (bar)",14}"); +Console.WriteLine(new string('─', 36)); +for (var i = 0; i < linearSamples.SampleCount; i++) + Console.WriteLine($"{i,6} {linearSamples.Data[i],14:F2} {tableSamples.Data[i],14:F3}"); diff --git a/snippets/csharp/configuration/create-parameter-config.cs b/snippets/csharp/configuration/create-parameter-config.cs new file mode 100644 index 0000000..0459671 --- /dev/null +++ b/snippets/csharp/configuration/create-parameter-config.cs @@ -0,0 +1,100 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Create Parameter Configuration +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: The full manual parameter configuration process step by step — +// ParameterGroup, ApplicationGroup, RationalConversion, Channel, Parameter +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own session) +// Output: Configured parameter with data written and read back +// +// Related: snippets/csharp/configuration/create-transient-parameter.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionKey = SessionKey.NewKey(); + +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.CreateSession(connectionString, sessionKey, "Config Demo", DateTime.UtcNow, "Example"); +if (clientSession is null) +{ + Console.WriteLine($"ERROR: Failed to create session at '{connectionString}'."); + Console.WriteLine("Tip: Check the target folder is writable and the file is not locked."); + return; +} +var session = clientSession.Session; + +// --- Step 1: Create a configuration batch --- +var config = session.CreateConfiguration(); + +// --- Step 2: Define the conversion (raw → engineering units) --- +// RationalConversion: engineering = (c0 + c1*raw) / (c2 + c3*raw) +// For a 1:1 pass-through: c0=0, c1=1, c2=0, c3=1 → gives raw/1 = raw +var conversion = RationalConversion.CreateSimple1To1Conversion("degC_conversion", "degC", "%5.2f"); +config.AddConversion(conversion); +Console.WriteLine("1. Conversion: degC_conversion (1:1 rational)"); + +// --- Step 3: Define the channel (data storage) --- +uint channelId = 100; +var frequency = new Frequency(100, FrequencyUnit.Hz); +var interval = frequency.ToInterval(); // nanoseconds between samples +var channel = new Channel(channelId, "Temperature", interval, DataType.Double64Bit, ChannelDataSourceType.Periodic, "Temperature:Sensors", false); +config.AddChannel(channel); +Console.WriteLine($"2. Channel: id={channelId}, 100 Hz, Double64Bit, interval={interval} ns"); + +// --- Step 4: Define groups --- +var paramGroup = new ParameterGroup("Thermal", "Thermal measurements"); +var appGroup = new ApplicationGroup("Sensors", "Sensor data sources", new List { "Thermal" }) { SupportsRda = false }; +config.AddParameterGroup(paramGroup); +config.AddGroup(appGroup); +Console.WriteLine("3. Groups: Thermal (parameter), Sensors (application)"); + +// --- Step 5: Define the parameter (links everything together) --- +var parameter = new Parameter( + "Temperature:Sensors", // identifier + "Temperature", // name + "Inlet temperature sensor", // description + 500.0, // maximumValue + 0.0, // minimumValue + 500.0, // warningMaximumValue + 0.0, // warningMinimumValue + 0.0, // offsetValue + 0u, // dataBitMask + 0u, // errorBitMask + "degC_conversion", // conversionFunctionName + new List { "Thermal" }, // parameterGroupIdentifiers + channelId, // channelId (uint) + "Sensors"); // applicationName +config.AddParameter(parameter); +Console.WriteLine("4. Parameter: Temperature:Sensors → channelId=100, conversion=degC_conversion"); + +// --- Step 6: Commit (atomically applies all configuration) --- +try +{ + config.Commit(); +} +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Configuration commit failed: {ex.Message}"); + Console.WriteLine("Tip: Identifiers must be unique within a session and not previously committed."); + return; +} +Console.WriteLine("5. Configuration committed.\n"); + +// --- Write and read back --- +var startTime = 36_000_000_000_000L; +for (var i = 0; i < 10; i++) + session.AddChannelData(channelId, startTime + i * interval, 1, BitConverter.GetBytes(20.0 + i)); + +using var pda = session.CreateParameterDataAccess("Temperature:Sensors"); +var samples = pda.GetSamplesBetween(startTime, startTime + 10 * interval); +Console.WriteLine($"Wrote 10 samples, read back {samples.SampleCount}: [{samples.Data[0]:F1} ... {samples.Data[^1]:F1}] degC"); diff --git a/snippets/csharp/configuration/create-transient-parameter.cs b/snippets/csharp/configuration/create-transient-parameter.cs new file mode 100644 index 0000000..e73adbc --- /dev/null +++ b/snippets/csharp/configuration/create-transient-parameter.cs @@ -0,0 +1,63 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Create Transient Parameter +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating a transient parameter that exists only in memory +// and is not persisted to the database +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own session) +// Output: Transient parameter created, data written/read, then verified gone after reload +// +// Related: snippets/csharp/configuration/create-parameter-config.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionKey = SessionKey.NewKey(); + +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.CreateSession(connectionString, sessionKey, "Transient Demo", DateTime.UtcNow, "Example"); +var session = clientSession.Session; + +// --- Create a TRANSIENT configuration (not persisted) --- +var config = session.CreateTransientConfiguration(); + +uint channelId = 200; +var interval = new Frequency(10, FrequencyUnit.Hz).ToInterval(); + +config.AddConversion(RationalConversion.CreateSimple1To1Conversion("temp_conv", "degC", "%5.1f")); +config.AddChannel(new Channel(channelId, "TempCalc", interval, DataType.Double64Bit, ChannelDataSourceType.Periodic, "CalculatedTemp:Derived", false)); +config.AddParameterGroup(new ParameterGroup("Calculated", "Calculated")); +config.AddGroup(new ApplicationGroup("Derived", "Derived", new List { "Calculated" }) { SupportsRda = false }); +config.AddParameter(new Parameter( + "CalculatedTemp:Derived", "TempCalc", "Transient calculated temperature", + 1000, 0, 1000, 0, 0, 0u, 0u, "temp_conv", + new List { "Calculated" }, channelId, "Derived")); +config.Commit(); + +Console.WriteLine("Transient parameter 'CalculatedTemp:Derived' created (in-memory only)"); + +// --- Write data to the transient parameter --- +var startTime = 36_000_000_000_000L; +for (var i = 0; i < 10; i++) + session.AddChannelData(channelId, startTime + i * interval, 1, BitConverter.GetBytes(100.0 + i * 2.5)); + +// --- Read it back (works while session is open) --- +using var pda = session.CreateParameterDataAccess("CalculatedTemp:Derived"); +var samples = pda.GetSamplesBetween(startTime, startTime + 10 * interval); +Console.WriteLine($"Read {samples.SampleCount} transient samples: [{samples.Data[0]:F1} ... {samples.Data[^1]:F1}]"); + +// --- Verify it's gone after reload --- +clientSession.Close(); +using var reloaded = sessionManager.Load(sessionKey, connectionString); +var hasParam = reloaded.Session.Parameters.Any(p => p.Identifier == "CalculatedTemp:Derived"); +Console.WriteLine($"\nAfter reload: parameter exists = {hasParam}"); +Console.WriteLine("Transient parameters are not persisted — they exist only in-memory."); diff --git a/snippets/csharp/configuration/introspect-session-config.cs b/snippets/csharp/configuration/introspect-session-config.cs new file mode 100644 index 0000000..16a236f --- /dev/null +++ b/snippets/csharp/configuration/introspect-session-config.cs @@ -0,0 +1,69 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Introspect Session Configuration +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Enumerating a session's parameters, channels, conversions, +// and groups to understand its configuration +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: A session with configured parameters (run 04-create-session-write-data.cs first) +// Output: Summary tables of all configuration objects +// +// Related: snippets/csharp/configuration/create-parameter-config.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- "); + Console.WriteLine("Tip: Run 04-create-session-write-data.cs first."); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.Load(sessionKey, connectionString); +var session = clientSession.Session; + +Console.WriteLine($"Session: {session.Identifier} ({sessionKey})\n"); + +// --- Parameters --- +Console.WriteLine($"=== Parameters ({session.Parameters.Count}) ==="); +Console.WriteLine($"{"Identifier",-30} {"Conversion",-25} {"AppGroup",-15}"); +Console.WriteLine(new string('─', 72)); + +foreach (var param in session.Parameters) +{ + Console.WriteLine($"{param.Identifier,-30} {param.ConversionFunctionName,-25} {param.ApplicationName,-15}"); +} + +// --- Channels --- +// TODO: Verify — channel enumeration may require iterating parameters +Console.WriteLine($"\n=== Channels ==="); +Console.WriteLine($"{"ID",8} {"Name",-20} {"Interval (ns)",15} {"DataType",-20}"); +Console.WriteLine(new string('─', 65)); + +var seenChannels = new HashSet(); +foreach (var param in session.Parameters) +{ + if (!seenChannels.Add(param.Name)) continue; + Console.WriteLine($"{"—",8} {param.Name,-20} {"(see channel)",15} {"(see channel)",-20}"); +} + +// --- Application Groups and Parameter Groups --- +Console.WriteLine($"\n=== Application Groups ==="); +var appGroups = session.Parameters.Select(p => p.ApplicationName).Distinct(); +foreach (var group in appGroups) + Console.WriteLine($" {group}"); + +Console.WriteLine($"\n=== Parameter Groups ==="); +var paramGroups = session.Parameters.SelectMany(p => p.GroupIdentifiers ?? Enumerable.Empty()).Distinct(); +foreach (var group in paramGroups) + Console.WriteLine($" {group}"); diff --git a/snippets/csharp/data-access/bulk-parameter-read.cs b/snippets/csharp/data-access/bulk-parameter-read.cs new file mode 100644 index 0000000..096e315 --- /dev/null +++ b/snippets/csharp/data-access/bulk-parameter-read.cs @@ -0,0 +1,79 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Bulk Parameter Read +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Reading multiple parameters from a session efficiently by +// managing PDA lifecycle correctly (create, read, dispose) +// Prerequisites: MESL.SQLRace.API, a session with multiple parameters +// Expected output: Summary of each parameter's data range and sample count +// Related: multi-rate-alignment.cs, data-status-filtering.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- "); + return; +} + +SessionKey sessionKey; +try { sessionKey = SessionKey.Parse(args[0]); } +catch (Exception ex) { Console.WriteLine($"ERROR: '{args[0]}' is not a valid GUID. {ex.Message}"); return; } + +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession clientSession; +try { clientSession = sessionManager.Load(sessionKey, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not load session '{sessionKey}': {ex.Message}"); + return; +} +using (clientSession) +{ +var session = clientSession.Session; + +// --- Get all parameter identifiers --- +var identifiers = session.Parameters.Select(p => p.Identifier).ToList(); + +if (identifiers.Count == 0) +{ + Console.WriteLine("Session has no parameters."); + return; +} + +Console.WriteLine($"Session: {sessionKey}"); +Console.WriteLine($"Reading {identifiers.Count} parameter(s)..."); +Console.WriteLine($"{"Parameter",-35} {"Samples",8} {"Min",12} {"Max",12}"); +Console.WriteLine(new string('-', 70)); + +// --- Read each parameter, tracking PDA lifecycle --- +foreach (var identifier in identifiers) +{ + using var pda = session.CreateParameterDataAccess(identifier); + var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + + if (samples.SampleCount == 0) + { + Console.WriteLine($"{identifier,-35} {"(empty)",8}"); + continue; + } + + var min = double.MaxValue; + var max = double.MinValue; + for (var i = 0; i < samples.SampleCount; i++) + { + if (samples.Data[i] < min) min = samples.Data[i]; + if (samples.Data[i] > max) max = samples.Data[i]; + } + + Console.WriteLine($"{identifier,-35} {samples.SampleCount,8} {min,12:F4} {max,12:F4}"); +} +} diff --git a/snippets/csharp/data-access/data-status-filtering.cs b/snippets/csharp/data-access/data-status-filtering.cs new file mode 100644 index 0000000..94150a2 --- /dev/null +++ b/snippets/csharp/data-access/data-status-filtering.cs @@ -0,0 +1,74 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Data Status Filtering +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Checking DataStatusType on returned samples to identify and +// handle missing or invalid data points +// Prerequisites: MESL.SQLRace.API, a session with data +// Expected output: Per-sample status breakdown and filtered valid-only values +// Related: read-samples-between.cs, bulk-parameter-read.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- [parameter-identifier]"); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); +var paramId = args.Length > 1 ? args[1] : "Temperature:Sensors"; + +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.Load(sessionKey, connectionString); +var session = clientSession.Session; + +using var pda = session.CreateParameterDataAccess(paramId); +var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + +// --- Count samples by status --- +var statusCounts = new Dictionary(); +for (var i = 0; i < samples.SampleCount; i++) +{ + var status = samples.DataStatus[i]; + statusCounts.TryGetValue(status, out var count); + statusCounts[status] = count + 1; +} + +Console.WriteLine($"Parameter: {paramId}"); +Console.WriteLine($"Total samples: {samples.SampleCount}"); +Console.WriteLine(); +Console.WriteLine("Status breakdown:"); +foreach (var (status, count) in statusCounts) +{ + var pct = 100.0 * count / samples.SampleCount; + Console.WriteLine($" {status,-15} {count,6} ({pct:F1}%)"); +} + +// --- Filter to valid samples only --- +Console.WriteLine(); +Console.WriteLine("Valid samples (DataStatusType.Sample):"); + +var validCount = 0; +for (var i = 0; i < samples.SampleCount; i++) +{ + if (samples.DataStatus[i] != DataStatusType.Sample) + continue; + + if (validCount < 10) + { + var ms = (samples.Timestamp[i] - session.StartTime) / 1_000_000.0; + Console.WriteLine($" t+{ms,8:F3} ms {samples.Data[i]:F4}"); + } + validCount++; +} + +Console.WriteLine($"Total valid: {validCount} / {samples.SampleCount}"); diff --git a/snippets/csharp/data-access/multi-rate-alignment.cs b/snippets/csharp/data-access/multi-rate-alignment.cs new file mode 100644 index 0000000..0c581d0 --- /dev/null +++ b/snippets/csharp/data-access/multi-rate-alignment.cs @@ -0,0 +1,89 @@ +// +// SQL Race Example: SQL Race Example: Multi-Rate Alignment +// +// Demonstrates: Reading two parameters at different native frequencies and +// aligning them to a common timestamp array using GetData +// Prerequisites: MESL.SQLRace.API, a session with two parameters at different rates +// Expected output: Aligned values for both parameters at uniform timestamps +// Related: read-subsampled-data.cs, timestamp-array-construction.cs +// + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- "); + return; +} + +SessionKey sessionKey; +try { sessionKey = SessionKey.Parse(args[0]); } +catch (Exception ex) { Console.WriteLine($"ERROR: '{args[0]}' is not a valid GUID. {ex.Message}"); return; } + +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession clientSession; +try { clientSession = sessionManager.Load(sessionKey, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not load session '{sessionKey}': {ex.Message}"); + return; +} +using (clientSession) +{ +var session = clientSession.Session; + +// --- Two parameters (potentially at different native rates) --- +var paramA = "Temperature:Sensors"; +var paramB = "Pressure:Inlet"; + +var available = session.Parameters.Select(p => p.Identifier).ToHashSet(); +var missing = new[] { paramA, paramB }.Where(p => !available.Contains(p)).ToList(); +if (missing.Count > 0) +{ + Console.WriteLine($"ERROR: Parameter(s) not in session: {string.Join(", ", missing)}"); + Console.WriteLine($"Available: {string.Join(", ", available.Take(10))}"); + return; +} + +using var pdaA = session.CreateParameterDataAccess(paramA); +using var pdaB = session.CreateParameterDataAccess(paramB); + +// --- Build a common timestamp array at 10 Hz --- +var alignedInterval = 100_000_000L; // 100ms +var start = session.StartTime; +var end = session.EndTime; +var count = (int)((end - start) / alignedInterval); + +if (count <= 0) +{ + Console.WriteLine("Session too short for alignment at 10 Hz."); + return; +} + +var timestamps = new long[count]; +for (var i = 0; i < count; i++) + timestamps[i] = start + i * alignedInterval; + +// --- Read both parameters at the common timestamps --- +var samplesA = pdaA.GetData(timestamps); +var samplesB = pdaB.GetData(timestamps); + +Console.WriteLine($"Aligned {paramA} and {paramB} to {count} points at 10 Hz"); +Console.WriteLine($"{"Time (ms)",10} {paramA,18} {paramB,18}"); +Console.WriteLine(new string('-', 50)); + +for (var i = 0; i < Math.Min(count, 15); i++) +{ + var ms = (timestamps[i] - start) / 1_000_000.0; + Console.WriteLine($"{ms,10:F1} {samplesA.Data[i],18:F4} {samplesB.Data[i],18:F4}"); +} + +if (count > 15) + Console.WriteLine($" ... and {count - 15} more rows"); +} diff --git a/snippets/csharp/data-access/read-samples-between.cs b/snippets/csharp/data-access/read-samples-between.cs new file mode 100644 index 0000000..9403e34 --- /dev/null +++ b/snippets/csharp/data-access/read-samples-between.cs @@ -0,0 +1,68 @@ +// +// SQL Race Example: SQL Race Example: Read Samples Between +// +// Demonstrates: Using GetSamplesBetween to read data in a specific time range +// Prerequisites: MESL.SQLRace.API, a session with data +// Expected output: Sample count and values within the specified range +// Related: read-subsampled-data.cs, reverse-iteration.cs +// + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- [parameter-identifier]"); + return; +} + +SessionKey sessionKey; +try { sessionKey = SessionKey.Parse(args[0]); } +catch (Exception ex) { Console.WriteLine($"ERROR: '{args[0]}' is not a valid GUID. {ex.Message}"); return; } +var paramId = args.Length > 1 ? args[1] : "Temperature:Sensors"; + +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession clientSession; +try { clientSession = sessionManager.Load(sessionKey, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not load session '{sessionKey}': {ex.Message}"); + return; +} +using (clientSession) +{ +var session = clientSession.Session; + +if (!session.Parameters.Any(p => p.Identifier == paramId)) +{ + Console.WriteLine($"ERROR: Parameter '{paramId}' not found in session."); + Console.WriteLine($"Available: {string.Join(", ", session.Parameters.Take(10).Select(p => p.Identifier))}"); + return; +} + +using var pda = session.CreateParameterDataAccess(paramId); + +// --- Define a time range: first 500ms of data --- +var rangeStart = session.StartTime; +var rangeEnd = session.StartTime + 500_000_000L; // 0.5 seconds in nanoseconds + +var samples = pda.GetSamplesBetween(rangeStart, rangeEnd); + +Console.WriteLine($"Parameter: {paramId}"); +Console.WriteLine($"Range: {rangeStart} – {rangeEnd} ns ({(rangeEnd - rangeStart) / 1e9:F3} s)"); +Console.WriteLine($"Samples: {samples.SampleCount}"); + +for (var i = 0; i < Math.Min(samples.SampleCount, 10); i++) +{ + var relativeMs = (samples.Timestamp[i] - rangeStart) / 1_000_000.0; + Console.WriteLine($" t+{relativeMs,8:F3} ms {samples.Data[i]:F4}"); +} + +if (samples.SampleCount > 10) + Console.WriteLine($" ... and {samples.SampleCount - 10} more"); +} diff --git a/snippets/csharp/data-access/read-subsampled-data.cs b/snippets/csharp/data-access/read-subsampled-data.cs new file mode 100644 index 0000000..9920cd4 --- /dev/null +++ b/snippets/csharp/data-access/read-subsampled-data.cs @@ -0,0 +1,65 @@ +// +// SQL Race Example: SQL Race Example: Read Subsampled Data +// +// Demonstrates: Using GetData with a custom timestamp array to downsample +// high-frequency data to a lower rate +// Prerequisites: MESL.SQLRace.API, a session with data (e.g., 100 Hz) +// Expected output: Values at the requested timestamps (e.g., every 100ms = 10 Hz) +// Related: timestamp-array-construction.cs, multi-rate-alignment.cs +// + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- [parameter-identifier]"); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); +var paramId = args.Length > 1 ? args[1] : "Temperature:Sensors"; + +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.Load(sessionKey, connectionString); +var session = clientSession.Session; + +using var pda = session.CreateParameterDataAccess(paramId); + +// --- Build a custom timestamp array at 10 Hz (100ms intervals) --- +var desiredInterval = 100_000_000L; // 100ms in nanoseconds (= 10 Hz) +var start = session.StartTime; +var end = session.EndTime; +var sampleCount = (int)((end - start) / desiredInterval); + +if (sampleCount <= 0) +{ + Console.WriteLine("Session is too short for subsampling at 10 Hz."); + return; +} + +var timestamps = new long[sampleCount]; +for (var i = 0; i < sampleCount; i++) + timestamps[i] = start + i * desiredInterval; + +// --- Read data at the custom timestamps --- +var samples = pda.GetData(timestamps); + +Console.WriteLine($"Parameter: {paramId}"); +Console.WriteLine($"Original range: {start} – {end} ns"); +Console.WriteLine($"Subsampled to: {sampleCount} points at 10 Hz"); +Console.WriteLine(); + +for (var i = 0; i < Math.Min(samples.SampleCount, 15); i++) +{ + var relativeMs = (samples.Timestamp[i] - start) / 1_000_000.0; + Console.WriteLine($" t+{relativeMs,8:F1} ms {samples.Data[i]:F4}"); +} + +if (samples.SampleCount > 15) + Console.WriteLine($" ... and {samples.SampleCount - 15} more"); diff --git a/snippets/csharp/data-access/reverse-iteration.cs b/snippets/csharp/data-access/reverse-iteration.cs new file mode 100644 index 0000000..f7f4cdd --- /dev/null +++ b/snippets/csharp/data-access/reverse-iteration.cs @@ -0,0 +1,67 @@ +// +// SQL Race Example: SQL Race Example: Reverse Iteration +// +// Demonstrates: Using GoTo and GetNextSamples with StepDirection.Reverse to +// read data backwards from the end of a session +// Prerequisites: MESL.SQLRace.API, a session with data +// Expected output: Samples printed in reverse chronological order +// Related: read-samples-between.cs, bulk-parameter-read.cs +// + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- [parameter-identifier]"); + return; +} + +SessionKey sessionKey; +try { sessionKey = SessionKey.Parse(args[0]); } +catch (Exception ex) { Console.WriteLine($"ERROR: '{args[0]}' is not a valid GUID. {ex.Message}"); return; } +var paramId = args.Length > 1 ? args[1] : "Temperature:Sensors"; + +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession clientSession; +try { clientSession = sessionManager.Load(sessionKey, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not load session '{sessionKey}': {ex.Message}"); + return; +} +using (clientSession) +{ +var session = clientSession.Session; + +if (!session.Parameters.Any(p => p.Identifier == paramId)) +{ + Console.WriteLine($"ERROR: Parameter '{paramId}' not found in session."); + Console.WriteLine($"Available: {string.Join(", ", session.Parameters.Take(10).Select(p => p.Identifier))}"); + return; +} + +using var pda = session.CreateParameterDataAccess(paramId); + +// --- Position the PDA at the end of the session --- +pda.GoTo(session.EndTime); + +// --- Read 10 samples backwards --- +var samples = pda.GetNextSamples(10, StepDirection.Reverse); + +Console.WriteLine($"Parameter: {paramId}"); +Console.WriteLine($"Last {samples.SampleCount} samples (reverse order):"); +Console.WriteLine($"{"Timestamp (ns)",20} {"Value",12}"); +Console.WriteLine(new string('-', 34)); + +for (var i = 0; i < samples.SampleCount; i++) +{ + Console.WriteLine($"{samples.Timestamp[i],20} {samples.Data[i],12:F4}"); +} +} diff --git a/snippets/csharp/data-access/timestamp-array-construction.cs b/snippets/csharp/data-access/timestamp-array-construction.cs new file mode 100644 index 0000000..8e44f52 --- /dev/null +++ b/snippets/csharp/data-access/timestamp-array-construction.cs @@ -0,0 +1,70 @@ +// +// SQL Race Example: SQL Race Example: Timestamp Array Construction +// +// Demonstrates: Helper patterns for creating timestamp arrays to use with +// GetData calls — uniform, non-uniform, and from session laps +// Prerequisites: MESL.SQLRace.API (no session data required for the helpers) +// Expected output: Example timestamp arrays with explanations +// Related: read-subsampled-data.cs, multi-rate-alignment.cs +// + +using MESL.SqlRace.Domain; + +// --- Pattern 1: Uniform timestamps at a given frequency --- +static long[] UniformTimestamps(long start, long end, double frequencyHz) +{ + var intervalNs = (long)(1_000_000_000.0 / frequencyHz); + var count = (int)((end - start) / intervalNs); + var timestamps = new long[count]; + for (var i = 0; i < count; i++) + timestamps[i] = start + i * intervalNs; + return timestamps; +} + +// --- Pattern 2: Timestamps at specific time offsets (seconds from start) --- +static long[] TimestampsFromOffsets(long startNs, params double[] offsetsInSeconds) +{ + return offsetsInSeconds + .Select(s => startNs + (long)(s * 1_000_000_000)) + .ToArray(); +} + +// --- Pattern 3: Logarithmic spacing (useful for frequency response) --- +static long[] LogSpacedTimestamps(long start, long end, int count) +{ + var rangeNs = (double)(end - start); + var timestamps = new long[count]; + for (var i = 0; i < count; i++) + { + var fraction = Math.Pow(10, (double)i / (count - 1) * Math.Log10(rangeNs)) / rangeNs; + timestamps[i] = start + (long)(fraction * rangeNs); + } + return timestamps; +} + +// --- Demo --- +var sessionStart = 36_000_000_000_000L; // 10:00:00.000 +var sessionEnd = 36_001_000_000_000L; // 10:00:01.000 (1 second) + +Console.WriteLine("=== Pattern 1: Uniform at 10 Hz ==="); +var uniform = UniformTimestamps(sessionStart, sessionEnd, 10); +Console.WriteLine($"Count: {uniform.Length}"); +foreach (var t in uniform.Take(5)) + Console.WriteLine($" {t} ns (t+{(t - sessionStart) / 1e6:F1} ms)"); +Console.WriteLine($" ..."); + +Console.WriteLine(); +Console.WriteLine("=== Pattern 2: Specific offsets (0s, 0.25s, 0.5s, 0.75s, 1.0s) ==="); +var offsets = TimestampsFromOffsets(sessionStart, 0, 0.25, 0.5, 0.75, 1.0); +Console.WriteLine($"Count: {offsets.Length}"); +foreach (var t in offsets) + Console.WriteLine($" {t} ns (t+{(t - sessionStart) / 1e6:F1} ms)"); + +Console.WriteLine(); +Console.WriteLine("=== Pattern 3: Log-spaced (20 points) ==="); +var logSpaced = LogSpacedTimestamps(sessionStart, sessionEnd, 20); +Console.WriteLine($"Count: {logSpaced.Length}"); +foreach (var t in logSpaced.Take(5)) + Console.WriteLine($" {t} ns (t+{(t - sessionStart) / 1e6:F3} ms)"); +Console.WriteLine($" ..."); +Console.WriteLine($" {logSpaced[^1]} ns (t+{(logSpaced[^1] - sessionStart) / 1e6:F3} ms)"); diff --git a/snippets/csharp/functions/dotnet-function-basic.cs b/snippets/csharp/functions/dotnet-function-basic.cs new file mode 100644 index 0000000..f334665 --- /dev/null +++ b/snippets/csharp/functions/dotnet-function-basic.cs @@ -0,0 +1,74 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: .NET Function (Basic) +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Implementing IDotNetFunction to create a calculated parameter +// that doubles an input value +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: A session with a Temperature:Sensors parameter +// Output: Doubled values from the .NET function output +// +// Related: snippets/csharp/functions/fdl-function-basic.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +// NOTE: This file shows the complete pattern for a .NET function. +// In production, the function class would be in a separate assembly +// loaded by the function manager via MEF [Export] attributes. + +using System.ComponentModel.Composition; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Functions; + +Console.WriteLine("DoubleValueFunction defined."); +Console.WriteLine("See fdl-function-basic.cs for a complete runnable example."); +Console.WriteLine("In production, .NET functions are discovered via MEF [Export] attributes."); + +// --- The function implementation --- +[Export(typeof(IDotNetFunction))] +public class DoubleValueFunction : IDotNetFunction +{ + private const string InputParam = "Temperature:Sensors"; + private const string OutputParam = "DoubledTemperature:Calculated"; + + public IFunctionDefinition? Definition { get; private set; } + + public void Initialize(IFunctionManager functionManager, Session session) + { + // --- Create the function definition --- + Definition = functionManager.CreateFunctionDefinition( + DotNetFunctionConstants.DotNetFunctionTypeId, + OutputParam, + "Temperature doubled for testing"); + + Definition.AddInputParameter(InputParam); + Definition.SetImplementation(this); + Definition.SetOutputParameter(OutputParam, "degC", "%5.1f", 0, 1000); + + var errors = functionManager.Build(Definition, session); + if (errors?.Any() == true) + throw new InvalidOperationException($"Function build failed: {string.Join(", ", errors)}"); + } + + public void Execute( + IFunctionContext context, + long timestamp, + IDictionary inputValues, + IDictionary outputValues) + { + if (inputValues.TryGetValue(InputParam, out var input)) + { + outputValues[OutputParam] = input * 2.0; + } + } +} + +// --- Usage (host application) --- +// NOTE: In practice, function assemblies are discovered via MEF. +// This snippet shows the manual pattern for clarity. +// var functionManager = FunctionManagerFactory.Create(); +// var func = new DoubleValueFunction(); +// func.Initialize(functionManager, session); +// using var pda = session.CreateParameterDataAccess("DoubledTemperature:Calculated"); + + diff --git a/snippets/csharp/functions/dotnet-function-with-pda.cs b/snippets/csharp/functions/dotnet-function-with-pda.cs new file mode 100644 index 0000000..128b495 --- /dev/null +++ b/snippets/csharp/functions/dotnet-function-with-pda.cs @@ -0,0 +1,81 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: .NET Function with PDA +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: A .NET function that creates its own ParameterDataAccess +// inside Execute for cross-parameter lookups +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: A session with Temperature:Sensors and Pressure:Inlet parameters +// Output: Computed values using data from an additional parameter +// +// Related: snippets/csharp/functions/dotnet-function-basic.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +// NOTE: This is an advanced pattern. The function reads an additional parameter +// via its own PDA to perform cross-parameter calculations (e.g., density +// correction using both temperature and pressure). + +using System.ComponentModel.Composition; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Functions; + +Console.WriteLine("DensityCorrectionFunction defined (Temperature + Pressure → Density)."); +Console.WriteLine("Uses lazy-initialised PDA for cross-parameter lookup in Execute()."); +Console.WriteLine("See dotnet-function-basic.cs for the simpler single-input pattern."); + +[Export(typeof(IDotNetFunction))] +public class DensityCorrectionFunction : IDotNetFunction +{ + private const string InputTemp = "Temperature:Sensors"; + private const string LookupPressure = "Pressure:Inlet"; + private const string OutputParam = "CorrectedDensity:Calculated"; + + [NonSerialized] + private ParameterDataAccessBase? _pressurePda; + private Session? _session; + + public IFunctionDefinition? Definition { get; private set; } + + public void Initialize(IFunctionManager functionManager, Session session) + { + _session = session; + + Definition = functionManager.CreateFunctionDefinition( + DotNetFunctionConstants.DotNetFunctionTypeId, + OutputParam, + "Density corrected for temperature and pressure"); + + Definition.AddInputParameter(InputTemp); + Definition.SetImplementation(this); + Definition.SetOutputParameter(OutputParam, "kg/m3", "%6.2f", 0, 2000); + + var errors = functionManager.Build(Definition, session); + if (errors?.Any() == true) + throw new InvalidOperationException($"Build failed: {string.Join(", ", errors)}"); + } + + public void Execute( + IFunctionContext context, + long timestamp, + IDictionary inputValues, + IDictionary outputValues) + { + // --- Lazy-initialise the pressure PDA --- + _pressurePda ??= _session!.CreateParameterDataAccess(LookupPressure); + + if (!inputValues.TryGetValue(InputTemp, out var tempC)) + return; + + // --- Read pressure at this timestamp --- + var pressureSamples = _pressurePda.GetData(new[] { timestamp }); + var pressureBar = pressureSamples.SampleCount > 0 ? pressureSamples.Data[0] : 1.013; + + // Ideal gas approximation: density ∝ P / T (simplified) + var tempK = tempC + 273.15; + var density = (pressureBar * 100_000) / (287.05 * tempK); + outputValues[OutputParam] = density; + } +} + + diff --git a/snippets/csharp/functions/fdl-function-basic.cs b/snippets/csharp/functions/fdl-function-basic.cs new file mode 100644 index 0000000..d475df8 --- /dev/null +++ b/snippets/csharp/functions/fdl-function-basic.cs @@ -0,0 +1,73 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: FDL Function (Basic) +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating an FDL function (Celsius to Fahrenheit), building it, +// and consuming the output via a PDA +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8, a session with data +// Input: A session with a Temperature:Sensors parameter +// Output: Converted values read from the FDL function output +// +// Related: snippets/csharp/functions/dotnet-function-basic.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Functions; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- "); + Console.WriteLine("Tip: Run 04-create-session-write-data.cs first to create a session."); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.Load(sessionKey, connectionString); +var session = clientSession.Session; + +// --- Create the FDL function: Celsius → Fahrenheit --- +var functionManager = FunctionManagerFactory.Create(); +var fdlCode = "return ($Temperature:Sensors * 1.8 + 32)"; +var outputIdentifier = "TemperatureFahrenheit:Calculated"; + +var funcDef = functionManager.CreateFunctionDefinition( + FdlFunctionConstants.FdlFunctionTypeId, + outputIdentifier, + "Temperature in Fahrenheit"); + +funcDef.SetFdlCode(fdlCode); +funcDef.SetOutputParameter(outputIdentifier, "degF", "%5.1f", -40, 1000); + +var errors = functionManager.Build(funcDef, session); + +if (errors is not null && errors.Any()) +{ + Console.WriteLine("FDL build errors:"); + foreach (var error in errors) + Console.WriteLine($" {error}"); + return; +} + +Console.WriteLine($"FDL function built: {fdlCode}"); + +// --- Read the function output --- +using var pda = session.CreateParameterDataAccess(outputIdentifier); +var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + +Console.WriteLine($"Output: {samples.SampleCount} samples of {outputIdentifier}\n"); + +for (var i = 0; i < Math.Min(samples.SampleCount, 10); i++) +{ + Console.WriteLine($" {samples.Timestamp[i],20} ns {samples.Data[i]:F1} degF"); +} + +if (samples.SampleCount > 10) + Console.WriteLine($" ... and {samples.SampleCount - 10} more"); diff --git a/snippets/csharp/getting-started/01-load-session-from-file.cs b/snippets/csharp/getting-started/01-load-session-from-file.cs new file mode 100644 index 0000000..2f2d3cf --- /dev/null +++ b/snippets/csharp/getting-started/01-load-session-from-file.cs @@ -0,0 +1,72 @@ +// SQL Race Example: Load Session from File +// +// Demonstrates: Loading a session from an .ssn2 SQLite file by discovering +// available sessions, then loading the most recent one +// Prerequisites: MESL.SQLRace.API NuGet package, a .ssn2 file on disk +// Output: Session summary (identifier, start time, parameter count, lap count) +// Related: 02-load-session-from-database.cs, 03-read-parameter-samples.cs + +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Query; + +// --- Initialise the SQL Race runtime (required once per application) --- +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +// --- Set the path to your session file --- +var filePath = args.Length > 0 + ? args[0] + : Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2"); + +if (!File.Exists(filePath)) +{ + Console.WriteLine($"File not found: {filePath}"); + Console.WriteLine("Usage: dotnet run -- "); + Console.WriteLine("Tip: Create a session first with 04-create-session-write-data.cs"); + return; +} + +// --- Discover sessions in the file --- +var connectionString = $"DbEngine=SQLite;Data Source={filePath};"; +var sessionManager = SessionManager.CreateSessionManager(); +using var queryManager = QueryManager.CreateQueryManager(connectionString); +var summaries = queryManager.ExecuteQuery().ToList(); + +if (summaries.Count == 0) +{ + Console.WriteLine($"No sessions found in: {filePath}"); + return; +} + +Console.WriteLine($"File: {filePath}"); +Console.WriteLine($"Sessions found: {summaries.Count}\n"); + +// --- Load the most recent session --- +var latest = summaries.OrderByDescending(s => s.TimeOfRecording).First(); +IClientSession clientSession; +try { clientSession = sessionManager.Load(latest.Key, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not load session '{latest.Key}' from '{filePath}': {ex.Message}"); + Console.WriteLine("Tip: The file may be corrupt or written by an incompatible SQL Race version."); + return; +} +using (clientSession) +{ +var session = clientSession.Session; + +Console.WriteLine($"Loaded session: {latest.Key}"); +Console.WriteLine($" Identifier: {session.Identifier}"); +Console.WriteLine($" Start time: {session.StartTime} ns"); +Console.WriteLine($" End time: {session.EndTime} ns"); +Console.WriteLine($" Duration: {(session.EndTime - session.StartTime) / 1_000_000_000.0:F3} s"); +Console.WriteLine($" Parameters: {session.Parameters.Count}"); +Console.WriteLine($" Laps: {session.LapCollection.Count}"); + +if (session.Parameters.Count > 0) +{ + Console.WriteLine(" Parameters:"); + foreach (var param in session.Parameters.Take(5)) + Console.WriteLine($" - {param.Identifier}"); +} +} diff --git a/snippets/csharp/getting-started/02-load-session-from-database.cs b/snippets/csharp/getting-started/02-load-session-from-database.cs new file mode 100644 index 0000000..07a142e --- /dev/null +++ b/snippets/csharp/getting-started/02-load-session-from-database.cs @@ -0,0 +1,51 @@ +// +// SQL Race Example: SQL Race Example: Load Session from Database +// +// Demonstrates: Loading a session from SQLite by session key using SessionManager +// Prerequisites: MESL.SQLRace.API NuGet package, a SQLite database with sessions +// Expected output: Session summary (name, start time, parameter count) +// Related: 01-load-session-from-file.cs, 04-create-session-write-data.cs +// + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +// --- Initialise the SQL Race runtime --- +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +// --- Connection string (defaults to SQLite in temp directory) --- +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +// --- Session key to load (pass as argument or use a known key) --- +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- "); + Console.WriteLine($"Database: {connectionString}"); + Console.WriteLine("Tip: Create a session first with 04-create-session-write-data.cs, which prints the session key."); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); + +// --- Load the session --- +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.Load(sessionKey, connectionString); + +if (clientSession is null) +{ + Console.WriteLine($"Session not found: {sessionKey}"); + Console.WriteLine($"Check the session key and database: {connectionString}"); + return; +} + +var session = clientSession.Session; + +// --- Print session summary --- +Console.WriteLine($"Session loaded: {sessionKey}"); +Console.WriteLine($" Identifier: {session.Identifier ?? "(unnamed)"}"); +Console.WriteLine($" Start time: {session.StartTime} ns"); +Console.WriteLine($" End time: {session.EndTime} ns"); +Console.WriteLine($" Parameters: {session.Parameters.Count}"); +Console.WriteLine($" Laps: {session.LapCollection.Count}"); +Console.WriteLine($" Constants: {session.Constants.Count}"); diff --git a/snippets/csharp/getting-started/03-read-parameter-samples.cs b/snippets/csharp/getting-started/03-read-parameter-samples.cs new file mode 100644 index 0000000..22c1fa2 --- /dev/null +++ b/snippets/csharp/getting-started/03-read-parameter-samples.cs @@ -0,0 +1,78 @@ +// +// SQL Race Example: SQL Race Example: Read Parameter Samples +// +// Demonstrates: Creating a ParameterDataAccess and reading samples between +// start and end time, printing timestamped values +// Prerequisites: MESL.SQLRace.API NuGet package, a session with data +// (run 04-create-session-write-data.cs first) +// Expected output: Timestamped parameter values +// Related: 04-create-session-write-data.cs, data-access/read-samples-between.cs +// + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +// --- Load a session (pass file path or session key as argument) --- +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var parameterIdentifier = "Temperature:Sensors"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- "); + Console.WriteLine("Tip: Run 04-create-session-write-data.cs first to create a session with data."); + return; +} + +SessionKey sessionKey; +try { sessionKey = SessionKey.Parse(args[0]); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: '{args[0]}' is not a valid session GUID. {ex.Message}"); + return; +} + +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession clientSession; +try { clientSession = sessionManager.Load(sessionKey, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not load session '{sessionKey}': {ex.Message}"); + Console.WriteLine($"Connection: {connectionString}"); + Console.WriteLine("Tip: Run 04-create-session-write-data.cs first and re-run with the printed GUID."); + return; +} +using (clientSession) +{ + var session = clientSession.Session; + +// --- Check parameter exists --- +var parameter = session.Parameters + .FirstOrDefault(p => p.Identifier == parameterIdentifier); + +if (parameter is null) +{ + Console.WriteLine($"Parameter '{parameterIdentifier}' not found in session."); + Console.WriteLine($"Available parameters: {string.Join(", ", session.Parameters.Select(p => p.Identifier))}"); + return; +} + +// --- Create a PDA and read samples --- +using var pda = session.CreateParameterDataAccess(parameterIdentifier); +var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + +Console.WriteLine($"Parameter: {parameterIdentifier}"); +Console.WriteLine($"Samples: {samples.SampleCount}"); +Console.WriteLine($"{"Timestamp (ns)",20} {"Value",12}"); +Console.WriteLine(new string('-', 34)); + +for (var i = 0; i < Math.Min(samples.SampleCount, 20); i++) +{ + Console.WriteLine($"{samples.Timestamp[i],20} {samples.Data[i],12:F4}"); +} + +if (samples.SampleCount > 20) + Console.WriteLine($" ... and {samples.SampleCount - 20} more samples"); +} diff --git a/snippets/csharp/getting-started/04-create-session-write-data.cs b/snippets/csharp/getting-started/04-create-session-write-data.cs new file mode 100644 index 0000000..f50daa5 --- /dev/null +++ b/snippets/csharp/getting-started/04-create-session-write-data.cs @@ -0,0 +1,99 @@ +// SQL Race Example: Create Session and Write Data +// +// Demonstrates: Creating a SQLite session with multi-rate parameters, writing and reading data +// Prerequisites: MESL.SQLRace.API NuGet package (no existing data needed) +// Output: Session key and verified sample values for Temperature (100Hz) and Pressure (50Hz) +// Related: 02-load-session-from-database.cs, 03-read-parameter-samples.cs + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionKey = SessionKey.NewKey(); + +// --- Create a new session --- +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession clientSession; +try +{ + clientSession = sessionManager.CreateSession(connectionString, sessionKey, "Example Session", DateTime.UtcNow, "Example") + ?? throw new InvalidOperationException("CreateSession returned null."); +} +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Failed to create session at '{connectionString}': {ex.Message}"); + Console.WriteLine("Tip: Check the target folder is writable and the file is not locked by another process."); + return; +} + +using (clientSession) +{ + var session = clientSession.Session; +// --- Add parameter configuration --- +var config = session.CreateConfiguration(); +uint tempChannelId = 1; +uint pressChannelId = 2; +var tempInterval = new Frequency(100, FrequencyUnit.Hz).ToInterval(); +var pressInterval = new Frequency(50, FrequencyUnit.Hz).ToInterval(); + +config.AddConversion(RationalConversion.CreateSimple1To1Conversion("degC_conv", "degC", "%5.2f")); +config.AddConversion(RationalConversion.CreateSimple1To1Conversion("bar_conv", "bar", "%5.3f")); +config.AddChannel(new Channel(tempChannelId, "Temperature", tempInterval, DataType.Double64Bit, ChannelDataSourceType.Periodic, "Temperature:Sensors", false)); +config.AddChannel(new Channel(pressChannelId, "Pressure", pressInterval, DataType.Double64Bit, ChannelDataSourceType.Periodic, "Pressure:Inlet", false)); + +config.AddParameterGroup(new ParameterGroup("Thermal", "Thermal")); +config.AddParameterGroup(new ParameterGroup("Hydraulic", "Hydraulic")); +config.AddGroup(new ApplicationGroup("Sensors", "Sensors", new List { "Thermal", "Hydraulic" }) { SupportsRda = false }); + +config.AddParameter(new Parameter( + "Temperature:Sensors", "Temperature", "Inlet temperature sensor", + 500, 0, 500, 0, 0.0, 0u, 0u, "degC_conv", + new List { "Thermal" }, tempChannelId, "Sensors")); +config.AddParameter(new Parameter( + "Pressure:Inlet", "Pressure", "Inlet pressure sensor", + 10, 0, 10, 0, 0.0, 0u, 0u, "bar_conv", + new List { "Hydraulic" }, pressChannelId, "Sensors")); +try { config.Commit(); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Configuration commit failed: {ex.Message}"); + return; +} + +// --- Write 1 second of data --- +var startTime = 36_000_000_000_000L; // 10:00:00.000 + +for (var i = 0; i < 100; i++) // 100 Hz temperature +{ + var timestamp = startTime + i * tempInterval; + var value = 20.0 + Math.Sin(i * 0.1) * 5.0; + session.AddChannelData(tempChannelId, timestamp, 1, BitConverter.GetBytes(value)); +} + +for (var i = 0; i < 50; i++) // 50 Hz pressure +{ + var timestamp = startTime + i * pressInterval; + var value = 1.013 + Math.Sin(i * 0.05) * 0.2; + session.AddChannelData(pressChannelId, timestamp, 1, BitConverter.GetBytes(value)); +} + +// --- Read back and verify --- +using var tempPda = session.CreateParameterDataAccess("Temperature:Sensors"); +var tempSamples = tempPda.GetSamplesBetween(startTime, startTime + 100 * tempInterval); + +using var pressPda = session.CreateParameterDataAccess("Pressure:Inlet"); +var pressSamples = pressPda.GetSamplesBetween(startTime, startTime + 50 * pressInterval); + +Console.WriteLine($"Session created: {sessionKey}"); +Console.WriteLine($"Connection: {connectionString}"); +Console.WriteLine(); +Console.WriteLine($"Temperature:Sensors (100 Hz): {tempSamples.SampleCount} samples"); +Console.WriteLine($" First: {tempSamples.Data[0]:F2} degC Last: {tempSamples.Data[tempSamples.SampleCount - 1]:F2} degC"); +Console.WriteLine($"Pressure:Inlet (50 Hz): {pressSamples.SampleCount} samples"); +Console.WriteLine($" First: {pressSamples.Data[0]:F3} bar Last: {pressSamples.Data[pressSamples.SampleCount - 1]:F3} bar"); +} diff --git a/snippets/csharp/getting-started/05-query-sessions-by-metadata.cs b/snippets/csharp/getting-started/05-query-sessions-by-metadata.cs new file mode 100644 index 0000000..d3c6b01 --- /dev/null +++ b/snippets/csharp/getting-started/05-query-sessions-by-metadata.cs @@ -0,0 +1,60 @@ +// +// SQL Race Example: SQL Race Example: Query Sessions by Metadata +// +// Demonstrates: Using QueryManager with ScalarFilter to find sessions in a +// database, printing matching session summaries +// Prerequisites: MESL.SQLRace.API NuGet package, a database with sessions +// (run 04-create-session-write-data.cs a few times first) +// Expected output: List of matching sessions with name and date +// Related: session-management/query-with-composite-filter.cs +// + +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Query; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +// --- Create a query manager --- +var queryManager = QueryManager.CreateQueryManager(connectionString); + +// --- Option 1: Query all sessions (no filter) --- +Console.WriteLine("=== All sessions ==="); +var allSessions = queryManager.ExecuteQuery(); + +var count = 0; +foreach (var summary in allSessions) +{ + Console.WriteLine($" {summary.Key} {summary.Identifier,-30} {summary.TimeOfRecording:u}"); + count++; + if (count >= 20) break; +} + +Console.WriteLine($"Total: {count} session(s)"); + +// --- Option 2: Query with a filter --- +Console.WriteLine(); +Console.WriteLine("=== Sessions matching 'Example' ==="); +var filter = new ScalarFilter("Identifier", MatchingRule.Contains, "Example", ignoreCase: true); +queryManager.Filter = filter; + +var filtered = queryManager.ExecuteQuery(); +var filteredCount = 0; + +foreach (var summary in filtered) +{ + Console.WriteLine($" {summary.Key} {summary.Identifier,-30} {summary.TimeOfRecording:u}"); + filteredCount++; +} + +if (filteredCount == 0) +{ + Console.WriteLine(" No sessions found matching 'Example'."); + Console.WriteLine(" Tip: Run 04-create-session-write-data.cs to create some sessions."); +} +else +{ + Console.WriteLine($"Found: {filteredCount} session(s)"); +} diff --git a/snippets/csharp/live-data/poll-live-samples.cs b/snippets/csharp/live-data/poll-live-samples.cs new file mode 100644 index 0000000..a37c89e --- /dev/null +++ b/snippets/csharp/live-data/poll-live-samples.cs @@ -0,0 +1,79 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Poll Live Samples +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Polling a live session's latest samples with async delay and cancellation +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8, a live session +// Input: A live session key and parameter identifier +// Output: Latest sample values printed at each poll interval +// +// Related: snippets/csharp/live-data/subscribe-to-lap-events.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = args.Length > 1 ? args[1] + : $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- [connection-string] [parameter-id]"); + Console.WriteLine("NOTE: This example requires a live session with data actively arriving."); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); +var paramId = args.Length > 2 ? args[2] : "Temperature:Sensors"; + +var sessionManager = SessionManager.CreateSessionManager(); + +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +Console.WriteLine($"Polling {paramId} every 2s (30s timeout)...\n"); + +// --- Verify the parameter exists before polling --- +{ + using var initClient = sessionManager.Load(sessionKey, connectionString); + if (!initClient.Session.Parameters.Any(p => p.Identifier == paramId)) + { + Console.WriteLine($"ERROR: Parameter '{paramId}' not found in session."); + Console.WriteLine("Available (first 20):"); + foreach (var p in initClient.Session.Parameters.Take(20)) + Console.WriteLine($" {p.Identifier}"); + return; + } +} + +var pollCount = 0; +while (!cts.Token.IsCancellationRequested) +{ + // --- Reload session each cycle to pick up newly flushed data --- + using var clientSession = sessionManager.Load(sessionKey, connectionString); + var session = clientSession.Session; + + using var pda = session.CreateParameterDataAccess(paramId); + pda.GoTo(long.MaxValue); + var samples = pda.GetNextSamples(5, StepDirection.Reverse); + + if (samples.SampleCount > 0) + { + var latest = samples.Data[0]; + var latestTime = samples.Timestamp[0]; + Console.WriteLine($" Poll {++pollCount}: latest={latest:F2} at {latestTime} ns ({samples.SampleCount} recent)"); + } + else + { + Console.WriteLine($" Poll {++pollCount}: no data yet"); + } + + try { await Task.Delay(2000, cts.Token); } + catch (OperationCanceledException) { break; } +} + +Console.WriteLine($"\nPolling complete — {pollCount} polls."); diff --git a/snippets/csharp/live-data/rda-parameter-change-detection.cs b/snippets/csharp/live-data/rda-parameter-change-detection.cs new file mode 100644 index 0000000..ad72137 --- /dev/null +++ b/snippets/csharp/live-data/rda-parameter-change-detection.cs @@ -0,0 +1,83 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: RDA Parameter Change Detection +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Detecting when new parameters appear in a live session via +// session.RdaParametersChanged, then creating a PDA once available +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8, a live session +// Input: A live session key +// Output: Notification when a target parameter becomes available +// +// Related: snippets/csharp/live-data/subscribe-to-lap-events.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = args.Length > 1 ? args[1] + : $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- [connection-string]"); + Console.WriteLine("NOTE: This example requires a live session where parameters appear dynamically."); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); +var targetParam = args.Length > 2 ? args[2] : "Temperature:Sensors"; + +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.Load(sessionKey, connectionString); +var session = clientSession.Session; + +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); +var paramFound = new TaskCompletionSource(); + +// --- Check if parameter already exists --- +if (session.Parameters.Any(p => p.Identifier == targetParam)) +{ + Console.WriteLine($"Parameter '{targetParam}' already available."); + paramFound.TrySetResult(true); +} + +// --- Subscribe to parameter changes --- +EventHandler handler = (sender, e) => +{ + Console.WriteLine($" RDA parameters changed — {e.EventType}: {e.Message}"); + + if (session.Parameters.Any(p => p.Identifier == targetParam)) + { + Console.WriteLine($" Target parameter '{targetParam}' is now available!"); + paramFound.TrySetResult(true); + } +}; + +session.RdaParametersChanged += handler; +Console.WriteLine($"Waiting for '{targetParam}' to appear (60s timeout)..."); + +try +{ + var completed = await Task.WhenAny(paramFound.Task, Task.Delay(Timeout.Infinite, cts.Token)); + + if (paramFound.Task.IsCompleted) + { + using var pda = session.CreateParameterDataAccess(targetParam); + pda.GoTo(long.MaxValue); + var samples = pda.GetNextSamples(10, StepDirection.Reverse); + Console.WriteLine($"\nCreated PDA — {samples.SampleCount} samples retrieved, latest={samples.Data[0]:F2}."); + } +} +catch (OperationCanceledException) +{ + Console.WriteLine($"\nTimeout: '{targetParam}' did not appear within 60 seconds."); +} +finally +{ + session.RdaParametersChanged -= handler; +} diff --git a/snippets/csharp/live-data/subscribe-to-data-events.cs b/snippets/csharp/live-data/subscribe-to-data-events.cs new file mode 100644 index 0000000..b0e465d --- /dev/null +++ b/snippets/csharp/live-data/subscribe-to-data-events.cs @@ -0,0 +1,60 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Subscribe to Data Events +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Subscribing to session.EventDataAdded for real-time event notification +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8, a live data source +// Input: A live session key and connection string +// Output: Event details as they arrive in real time +// +// Related: snippets/csharp/live-data/subscribe-to-lap-events.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Collections; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = args.Length > 1 ? args[1] + : $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- [connection-string]"); + Console.WriteLine("NOTE: This example requires a live session with events being added."); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.Load(sessionKey, connectionString); +var session = clientSession.Session; + +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +var eventCount = 0; +EventHandler handler = (sender, e) => +{ + Interlocked.Increment(ref eventCount); + Console.WriteLine($" Event data added: {e.Item} (session {e.SessionKey})"); +}; + +session.EventDataAdded += handler; +Console.WriteLine("Listening for data events (30s timeout)..."); + +try +{ + await Task.Delay(Timeout.Infinite, cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine($"\nTimeout reached. Received {eventCount} event(s)."); +} +finally +{ + session.EventDataAdded -= handler; + Console.WriteLine("Unsubscribed from data events."); +} diff --git a/snippets/csharp/live-data/subscribe-to-lap-events.cs b/snippets/csharp/live-data/subscribe-to-lap-events.cs new file mode 100644 index 0000000..448a881 --- /dev/null +++ b/snippets/csharp/live-data/subscribe-to-lap-events.cs @@ -0,0 +1,63 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Subscribe to Lap Events +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Subscribing to session.LapStarted with async/await and cancellation +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8, a live data source +// Input: A live session key and connection string +// Output: Lap start notifications as they arrive +// +// Related: snippets/csharp/live-data/subscribe-to-data-events.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Collections; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = args.Length > 1 ? args[1] + : $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- [connection-string]"); + Console.WriteLine("NOTE: This example requires a live session with laps being added."); + return; +} + +var sessionKey = SessionKey.Parse(args[0]); +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.Load(sessionKey, connectionString); +var session = clientSession.Session; + +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +// --- Subscribe to lap events with async discipline --- +var lapCount = 0; +EventHandler handler = (sender, e) => +{ + Interlocked.Increment(ref lapCount); + Console.WriteLine($" Lap started: #{e.Lap.Number} \"{e.Lap.Name}\" at {e.Lap.StartTime} ns"); +}; + +session.LapStarted += handler; +Console.WriteLine("Listening for lap events (30s timeout)..."); + +try +{ + // NOTE: In a live session, laps are added by the data source. + // This loop waits for events or cancellation. + await Task.Delay(Timeout.Infinite, cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine($"\nTimeout reached. Received {lapCount} lap event(s)."); +} +finally +{ + session.LapStarted -= handler; + Console.WriteLine("Unsubscribed from lap events."); +} diff --git a/snippets/csharp/session-management/event-definitions-and-data.cs b/snippets/csharp/session-management/event-definitions-and-data.cs new file mode 100644 index 0000000..ae63c48 --- /dev/null +++ b/snippets/csharp/session-management/event-definitions-and-data.cs @@ -0,0 +1,56 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Event Definitions and Data +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating EventDefinition objects and the event data API +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own session) +// Output: Event definitions structure and API usage patterns +// TODO: Verify — EventDefinition persistence via ConfigurationSet.Commit() may require +// a full config (with at least one channel/parameter) to work with SQLite sessions. +// +// Related: snippets/csharp/session-management/marker-management.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionKey = SessionKey.NewKey(); + +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.CreateSession(connectionString, sessionKey, "Events Demo", DateTime.UtcNow, "Example"); +var session = clientSession.Session; + +var baseTime = 36_000_000_000_000L; + +// --- Define event types (via configuration) --- +var config = session.CreateConfiguration(); +config.AddEventDefinition(new EventDefinition(1, "LimitExceedance", EventPriorityType.High, new List { "Value", "Threshold" }, "Diagnostics")); +config.AddEventDefinition(new EventDefinition(2, "SensorFault", EventPriorityType.Medium, new List { "SensorId", "FaultCode" }, "Diagnostics")); +config.AddEventDefinition(new EventDefinition(3, "ThresholdCrossing", EventPriorityType.Low, new List { "Parameter", "Direction" }, "Diagnostics")); +config.Commit(); + +// --- Add event instances --- +session.Events.AddEventData(1, "Diagnostics", baseTime + 5_000_000_000L, new List { 127.3, 120.0 }); +session.Events.AddEventData(2, "Diagnostics", baseTime + 10_000_000_000L, new List { 3.0, 1.0 }); +session.Events.AddEventData(3, "Diagnostics", baseTime + 15_000_000_000L, new List { 0.0, 1.0 }); +session.Events.AddEventData(1, "Diagnostics", baseTime + 20_000_000_000L, new List { 135.1, 120.0 }); + +// --- Retrieve events in a time range --- +var events = session.Events.GetEventData(baseTime, baseTime + 25_000_000_000L); + +Console.WriteLine($"Session {sessionKey}: event definitions and data\n"); +Console.WriteLine($"{"Timestamp (ns)",20} {"DefKey",-10} {"Group",-15} {"Data"}"); +Console.WriteLine(new string('─', 80)); + +foreach (var evt in events) +{ + var dataStr = string.Join(", ", evt.RawData); + Console.WriteLine($"{evt.TimeStamp,20} {evt.EventDefinitionKey,-10} {evt.GroupName,-15} [{dataStr}]"); +} diff --git a/snippets/csharp/session-management/lap-and-segment-management.cs b/snippets/csharp/session-management/lap-and-segment-management.cs new file mode 100644 index 0000000..6e98591 --- /dev/null +++ b/snippets/csharp/session-management/lap-and-segment-management.cs @@ -0,0 +1,50 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Lap and Segment Management +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating Lap objects as test segments, adding items, reading back +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own session) +// Output: Segment list with metadata +// +// Related: snippets/csharp/session-management/marker-management.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionKey = SessionKey.NewKey(); + +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.CreateSession(connectionString, sessionKey, "Segment Demo", DateTime.UtcNow, "Example"); +var session = clientSession.Session; + +var baseTime = 36_000_000_000_000L; // 10:00:00.000 + +// --- Add segments (laps) representing test phases --- +var segments = new[] +{ + new Lap(baseTime, 1, 0, "Warmup", false), + new Lap(baseTime + 60_000_000_000L, 2, 0, "Steady State 1", false), + new Lap(baseTime + 180_000_000_000L, 3, 0, "Ramp Up", false), + new Lap(baseTime + 240_000_000_000L, 4, 0, "Steady State 2", false), + new Lap(baseTime + 360_000_000_000L, 5, 0, "Cooldown", false), +}; + +foreach (var segment in segments) + session.LapCollection.Add(segment); + +Console.WriteLine($"Session {sessionKey}: {session.LapCollection.Count} segments\n"); +Console.WriteLine($"{"#",3} {"Name",-20} {"Start (ns)",20} {"Offset (s)",12}"); +Console.WriteLine(new string('─', 58)); + +foreach (var lap in session.LapCollection) +{ + var offsetS = (lap.StartTime - baseTime) / 1_000_000_000.0; + Console.WriteLine($"{lap.Number,3} {lap.Name,-20} {lap.StartTime,20} {offsetS,12:F0}"); +} diff --git a/snippets/csharp/session-management/marker-management.cs b/snippets/csharp/session-management/marker-management.cs new file mode 100644 index 0000000..6bed37e --- /dev/null +++ b/snippets/csharp/session-management/marker-management.cs @@ -0,0 +1,49 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Marker Management +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating Marker objects with time ranges and MarkerLabel annotations +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own session) +// Output: Markers with labels printed to console +// +// Related: snippets/csharp/session-management/lap-and-segment-management.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionKey = SessionKey.NewKey(); + +var sessionManager = SessionManager.CreateSessionManager(); +using var clientSession = sessionManager.CreateSession(connectionString, sessionKey, "Marker Demo", DateTime.UtcNow, "Example"); +var session = clientSession.Session; + +var baseTime = 36_000_000_000_000L; // 10:00:00.000 + +// --- Add markers representing annotated time regions --- +var anomaly = new Marker(baseTime, baseTime + 2_000_000_000L, "Anomaly", "Anomaly", "Unexpected temperature spike detected", Guid.NewGuid().ToString()); +var roi = new Marker(baseTime + 5_000_000_000L, baseTime + 8_000_000_000L, "RegionOfInterest", "RegionOfInterest", "Steady-state operating window", Guid.NewGuid().ToString()); +var transient = new Marker(baseTime + 10_000_000_000L, baseTime + 12_000_000_000L, "Transient", "Transient", "Load step response period", Guid.NewGuid().ToString()); + +session.Markers.Add(anomaly); +session.Markers.Add(roi); +session.Markers.Add(transient); + +Console.WriteLine($"Session {sessionKey}: {session.Markers.Count} markers added\n"); + +foreach (var marker in session.Markers) +{ + var startNs = marker.StartTimestamp ?? 0; + var endNs = marker.EndTimestamp ?? 0; + var durationS = (endNs - startNs) / 1_000_000_000.0; + Console.WriteLine($" [{marker.Label}] {startNs}–{endNs} ns ({durationS:F1}s)"); + Console.WriteLine($" Type: {marker.MarkerType}"); + Console.WriteLine($" Description: {marker.Description}"); + Console.WriteLine(); +} diff --git a/snippets/csharp/session-management/parameter-unit-resolution.cs b/snippets/csharp/session-management/parameter-unit-resolution.cs new file mode 100644 index 0000000..1ac5eb7 --- /dev/null +++ b/snippets/csharp/session-management/parameter-unit-resolution.cs @@ -0,0 +1,82 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Parameter Unit Resolution +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Resolving the engineering unit for a parameter via its conversion +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: A session with configured parameters (run 04-create-session-write-data.cs first) +// Output: Parameter details with resolved units +// +// Related: snippets/csharp/configuration/conversion-types.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- "); + Console.WriteLine("Tip: Run 04-create-session-write-data.cs first to create a session."); + return; +} + +SessionKey sessionKey; +try +{ + sessionKey = SessionKey.Parse(args[0]); +} +catch (Exception ex) +{ + Console.WriteLine($"ERROR: '{args[0]}' is not a valid session GUID. {ex.Message}"); + return; +} + +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession clientSession; +try { clientSession = sessionManager.Load(sessionKey, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not load session '{sessionKey}': {ex.Message}"); + Console.WriteLine("Tip: Run 04-create-session-write-data.cs first, then pass the printed GUID."); + return; +} +using (clientSession) +{ +var session = clientSession.Session; + +Console.WriteLine($"Session: {session.Identifier} ({session.Parameters.Count} parameters)\n"); +Console.WriteLine($"{"Identifier",-30} {"Conversion",-25} {"Unit",-10} {"Format",-10} {"Range"}"); +Console.WriteLine(new string('─', 95)); + +foreach (var param in session.Parameters) +{ + // --- Resolve unit from the conversion --- + var conversionName = param.ConversionFunctionName; + var unit = "(unknown)"; + var format = ""; + + // TODO: Verify — GetConversion may be on ConfigurationSet or Session + try + { + var conversion = session.GetConversion(conversionName); + if (conversion is not null) + { + unit = conversion.Units ?? "(none)"; + format = conversion.FormatString ?? ""; + } + } + catch (Exception ex) + { + unit = "(error)"; + Console.Error.WriteLine($"WARNING: Failed to resolve conversion '{conversionName}' for {param.Identifier}: {ex.Message}"); + } + + Console.WriteLine($"{param.Identifier,-30} {conversionName,-25} {unit,-10} {format,-10} [{param.MinimumValue:F1}, {param.MaximumValue:F1}]"); +} +} diff --git a/snippets/csharp/session-management/query-with-composite-filter.cs b/snippets/csharp/session-management/query-with-composite-filter.cs new file mode 100644 index 0000000..edcd9ae --- /dev/null +++ b/snippets/csharp/session-management/query-with-composite-filter.cs @@ -0,0 +1,49 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Query with Composite Filter +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: QueryManager with CompositeFilter combining multiple ScalarFilter conditions +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: A SQLite database with sessions (run 04-create-session-write-data.cs first) +// Output: Matching session identifiers and recording times +// +// Related: snippets/csharp/getting-started/05-query-sessions-by-metadata.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Query; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +var queryManager = QueryManager.CreateQueryManager(connectionString); + +// --- Build a composite filter: session type contains "Example" AND recorded after a date --- +var composite = new CompositeFilter(CombineType.AND); +composite.Add(new ScalarFilter("Identifier", MatchingRule.Contains, "Example", ignoreCase: true)); +composite.Add(new ScalarFilter("TimeOfRecording", MatchingRule.GreaterThan, DateTime.UtcNow.AddDays(-30).ToString("O"), ignoreCase: true)); + +queryManager.Filter = composite; + +Console.WriteLine("Filter: Identifier contains 'Example' AND recorded in last 30 days"); +Console.WriteLine($"{"Session Key",-40} {"Identifier",-30} {"Recorded",22}"); +Console.WriteLine(new string('─', 94)); + +var count = 0; +foreach (var summary in queryManager.ExecuteQuery()) +{ + Console.WriteLine($"{summary.Key,-40} {summary.Identifier,-30} {summary.TimeOfRecording:u}"); + count++; +} + +if (count == 0) +{ + Console.WriteLine("No sessions matched. Create some sessions first with 04-create-session-write-data.cs."); +} +else +{ + Console.WriteLine($"\n{count} session(s) matched."); +} diff --git a/snippets/csharp/session-management/session-association.cs b/snippets/csharp/session-management/session-association.cs new file mode 100644 index 0000000..dd6d924 --- /dev/null +++ b/snippets/csharp/session-management/session-association.cs @@ -0,0 +1,57 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Session Association +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Associating sessions and loading with associates +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own sessions) +// Output: Primary session loaded with associated session data +// +// Related: snippets/csharp/composite-sessions/composite-with-associates.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var primaryKey = SessionKey.NewKey(); +var associateKey = SessionKey.NewKey(); + +var sessionManager = SessionManager.CreateSessionManager(); + +// --- Create the primary session --- +using (var primary = sessionManager.CreateSession(connectionString, primaryKey, "Primary Test Run", DateTime.UtcNow, "Example")) +{ + primary.Session.Items.Add(new SessionDataItem("Role", "Primary")); + Console.WriteLine($"Created primary: {primaryKey}"); +} + +// --- Create the associate session --- +using (var associate = sessionManager.CreateSession(connectionString, associateKey, "Baseline Reference", DateTime.UtcNow, "Example")) +{ + associate.Session.Items.Add(new SessionDataItem("Role", "Associate")); + Console.WriteLine($"Created associate: {associateKey}"); +} + +// --- Associate them --- +// TODO: Verify — association API may vary; some versions use SessionManager methods +using (var primary = sessionManager.Load(primaryKey, connectionString)) +{ + primary.Session.AssociateWithParent(associateKey); + Console.WriteLine($"\nAssociated {associateKey} with primary {primaryKey}"); +} + +// --- Load primary with associates --- +using var loaded = sessionManager.Load(primaryKey, connectionString); +var session = loaded.Session; + +Console.WriteLine($"\nLoaded primary session: {session.Identifier}"); +Console.WriteLine($" Items: {session.Items.Count}"); + +// TODO: Verify — enumerate associated sessions via session.Associates or similar +Console.WriteLine($"\nAssociation established. Use SessionManager.LoadAssociatesForSession()"); +Console.WriteLine($"to access the associated session's data alongside the primary."); diff --git a/snippets/csharp/session-management/session-metadata-crud.cs b/snippets/csharp/session-management/session-metadata-crud.cs new file mode 100644 index 0000000..a15ecdb --- /dev/null +++ b/snippets/csharp/session-management/session-metadata-crud.cs @@ -0,0 +1,63 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Session Metadata CRUD +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Creating and reading SessionDataItem entries on a session +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: None (creates its own session) +// Output: Session data items written and read back +// +// Related: snippets/csharp/session-management/query-with-composite-filter.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; +var sessionKey = SessionKey.NewKey(); + +// --- Create a session and add metadata items --- +var sessionManager = SessionManager.CreateSessionManager(); +using (var client = sessionManager.CreateSession(connectionString, sessionKey, "Metadata Demo", DateTime.UtcNow, "Example")) +{ + if (client is null) + { + Console.WriteLine($"ERROR: Failed to create session at '{connectionString}'."); + return; + } + var session = client.Session; + + session.Items.Add(new SessionDataItem("Facility", "Wind Tunnel A")); + session.Items.Add(new SessionDataItem("Operator", "Test Engineer 1")); + session.Items.Add(new SessionDataItem("TestObjective", "Thermal characterisation at 80% load")); + session.Items.Add(new SessionDataItem("AmbientTemp", "22.5")); + session.Items.Add(new SessionDataItem("RunNumber", "42")); + + Console.WriteLine($"Created session {sessionKey} with {session.Items.Count} metadata items"); +} + +// --- Reload and read back --- +IClientSession reloaded; +try { reloaded = sessionManager.Load(sessionKey, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not reload session '{sessionKey}': {ex.Message}"); + return; +} +using (reloaded) +{ +var reloadedSession = reloaded.Session; + +Console.WriteLine($"\nReloaded session: {reloadedSession.Items.Count} metadata items"); +Console.WriteLine($"{"Key",-20} {"Value"}"); +Console.WriteLine(new string('─', 60)); + +foreach (var item in reloadedSession.Items) +{ + Console.WriteLine($"{item.Name,-20} {item.Value}"); +} +} diff --git a/snippets/matlab/README.md b/snippets/matlab/README.md new file mode 100644 index 0000000..e8ad581 --- /dev/null +++ b/snippets/matlab/README.md @@ -0,0 +1,41 @@ +# MATLAB Snippets + +MATLAB examples use `NET.addAssembly` to call the SQL Race .NET API directly from MATLAB. + +## Prerequisites + +- **MATLAB R2019b+** (R2023b+ recommended for .NET 8 support) +- **SQL Race .NET assemblies** — installed with ATLAS 10+ at the default location +- .NET runtime must be configured in MATLAB. See [MATLAB .NET documentation](https://mathworks.com/help/matlab/matlab_external/call-net-from-matlab.html). + +## Setup + +Every snippet begins with the assembly loading boilerplate: + +```matlab +% Reads from SQLRACE_DLL_PATH env var, falls back to default ATLAS install location +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +NET.addAssembly(sqlraceDll); + +% Import namespaces +import MESL.SqlRace.Domain.*; +``` + +## Common Issues + +| Issue | Solution | +|-------|----------| +| `Could not load file or assembly` | Set the `SQLRACE_DLL_PATH` environment variable to point to your ATLAS installation's `MESL.SqlRace.Domain.dll` | +| `No appropriate method found` | Check argument types — MATLAB may need explicit casting | +| Wrong array types | Use `int64()` for timestamps, `double()` for values | +| Memory issues with large datasets | Extract data in chunks; clear .NET objects when done | + +## Folders + +| Folder | Description | +|--------|-------------| +| [getting-started/](getting-started/) | Load sessions, read parameters, query metadata, verify setup | +| [data-access/](data-access/) | Extract data to MATLAB timetables and arrays, per-segment statistics | diff --git a/snippets/matlab/data-access/extract_to_timetable.m b/snippets/matlab/data-access/extract_to_timetable.m new file mode 100644 index 0000000..61c3bb7 --- /dev/null +++ b/snippets/matlab/data-access/extract_to_timetable.m @@ -0,0 +1,100 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Extract to Timetable +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Extracting parameter data and converting to a MATLAB timetable +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A session with Temperature:Sensors data +% Output: MATLAB timetable with duration-based time index +% +% Related: snippets/matlab/data-access/multi_channel_read.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the flight-test scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'flight-test-manoeuvre-extraction.ssn2'); +sessionGuid = '828f3c72-9573-4a74-9803-20ff993d848a'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +paramId = 'Temperature:Sensors'; + +try + session = clientSession.Session; + pda = session.CreateParameterDataAccess(paramId); + + try + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + nSamples = samples.SampleCount; + + % --- Extract to MATLAB arrays --- + timestampsNs = zeros(nSamples, 1, 'int64'); + values = zeros(nSamples, 1); + for i = 1:nSamples + timestampsNs(i) = samples.Timestamp(i); + values(i) = samples.Data(i); + end + + % --- Convert to MATLAB timetable --- + % Use duration from session start for the time axis + startNs = timestampsNs(1); + relativeSeconds = double(timestampsNs - startNs) / 1e9; + timeVector = seconds(relativeSeconds); + + tt = timetable(timeVector, values, 'VariableNames', {paramId}); + + fprintf('Timetable: %d rows\n', height(tt)); + disp(head(tt, 10)); + + % --- Summary statistics --- + fprintf('Min: %.4f\n', min(tt.(paramId))); + fprintf('Max: %.4f\n', max(tt.(paramId))); + fprintf('Mean: %.4f\n', mean(tt.(paramId))); + catch ex + fprintf('Error reading data: %s\n', ex.message); + end + + pda.Dispose(); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); diff --git a/snippets/matlab/data-access/multi_channel_read.m b/snippets/matlab/data-access/multi_channel_read.m new file mode 100644 index 0000000..d8b9863 --- /dev/null +++ b/snippets/matlab/data-access/multi_channel_read.m @@ -0,0 +1,121 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Multi-Channel Read +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Extracting multiple parameters and building a multi-column +% timetable with proper cleanup of PDA objects +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A session with multiple parameters +% Output: Multi-column MATLAB timetable +% +% Related: snippets/matlab/data-access/extract_to_timetable.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the flight-test scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'flight-test-manoeuvre-extraction.ssn2'); +sessionGuid = '828f3c72-9573-4a74-9803-20ff993d848a'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + nParams = session.Parameters.Count; + fprintf('Session has %d parameter(s)\n', nParams); + + % --- Collect all parameters --- + paramIds = cell(nParams, 1); + for i = 1:nParams + paramIds{i} = char(session.Parameters.Item(i-1).Identifier); + end + + % --- Read each parameter and store in a struct --- + data = struct(); + timeVec = []; + + for p = 1:nParams + paramId = paramIds{p}; + safeName = matlab.lang.makeValidName(paramId); + pda = session.CreateParameterDataAccess(paramId); + + try + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + nSamples = samples.SampleCount; + + values = zeros(nSamples, 1); + for i = 1:nSamples + values(i) = samples.Data(i); + end + data.(safeName) = values; + + % Use first parameter's timestamps for the time axis + if isempty(timeVec) + tsNs = zeros(nSamples, 1, 'int64'); + for i = 1:nSamples + tsNs(i) = samples.Timestamp(i); + end + startNs = tsNs(1); + timeVec = seconds(double(tsNs - startNs) / 1e9); + end + + fprintf(' %s: %d samples\n', paramId, nSamples); + catch ex + fprintf(' %s: error — %s\n', paramId, ex.message); + end + + pda.Dispose(); + end + + % --- Build timetable --- + if ~isempty(timeVec) + tt = timetable(timeVec); + fields = fieldnames(data); + for f = 1:numel(fields) + tt.(fields{f}) = data.(fields{f})(1:height(tt)); + end + fprintf('\nTimetable: %d rows x %d variables\n', height(tt), width(tt)); + disp(head(tt, 5)); + end +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); diff --git a/snippets/matlab/data-access/segment_statistics.m b/snippets/matlab/data-access/segment_statistics.m new file mode 100644 index 0000000..e88488a --- /dev/null +++ b/snippets/matlab/data-access/segment_statistics.m @@ -0,0 +1,95 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Segment Statistics +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: GetLapStatistics() for per-segment aggregate statistics +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A session with segments (laps) and a numeric parameter +% Output: Per-segment min, max, and mean printed to command window +% +% Related: snippets/matlab/getting-started/read_parameters.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; +import MESL.SqlRace.Enumerators.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'flight-test-manoeuvre-extraction.ssn2'); +sessionGuid = '828f3c72-9573-4a74-9803-20ff993d848a'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +paramId = 'Temperature:Sensors'; + +try + session = clientSession.Session; + pda = session.CreateParameterDataAccess(paramId); + + try + laps = session.LapCollection; + fprintf('Parameter: %s\n', paramId); + fprintf('Segments: %d\n\n', laps.Count); + + % --- Compute statistics for each segment --- + % MATLAB can't resolve .NET enum values directly; use Enum.Parse + asm = NET.addAssembly(sqlraceDll); + statOptionType = asm.AssemblyHandle.GetType('MESL.SqlRace.Domain.Infrastructure.DataPipeline.StatisticOption'); + statAll = System.Enum.Parse(statOptionType, 'All'); + + for i = 1:laps.Count + lap = laps.Item(i - 1); % .NET uses zero-based indexing + stats = pda.GetLapStatistics(lap, false, statAll); + + fprintf('Segment %d — Min: %.4f, Max: %.4f, Mean: %.4f\n', ... + i, ... + double(stats.MinimumValue), ... + double(stats.MaximumValue), ... + double(stats.MeanValue)); + end + catch ex + fprintf('Error computing statistics: %s\n', ex.message); + end + + pda.Dispose(); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/getting-started/diagnose_setup.m b/snippets/matlab/getting-started/diagnose_setup.m new file mode 100644 index 0000000..4b54948 --- /dev/null +++ b/snippets/matlab/getting-started/diagnose_setup.m @@ -0,0 +1,131 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Diagnose Setup +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Step-by-step verification of MATLAB + SQL Race integration +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: None (uses bundled scenario data) +% Output: Pass/fail status for each setup step, with diagnostic details +% +% Related: snippets/matlab/getting-started/load_session.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +fprintf('\n=== SQL Race MATLAB Setup Diagnostics ===\n\n'); + +% --- Step 1: Check .NET runtime --- +fprintf('[1/5] Checking .NET runtime... '); +try + ne = dotnetenv(); + if ne.Runtime == "core" || ne.Status == "loaded" + fprintf('OK (Runtime: %s, Status: %s)\n', char(ne.Runtime), char(ne.Status)); + else + fprintf('NOT LOADED — run dotnetenv(''core'') and restart MATLAB\n'); + return; + end +catch ex + fprintf('FAILED — %s\n', ex.message); + return; +end + +% --- Step 2: Load assembly --- +fprintf('[2/5] Loading SQL Race assembly... '); +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +if ~exist(sqlraceDll, 'file') + fprintf('FAILED — DLL not found: %s\n', sqlraceDll); + fprintf(' Set SQLRACE_DLL_PATH or install ATLAS 10.\n'); + return; +end +try + installDir = fileparts(sqlraceDll); + sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); + if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); + end + NET.addAssembly(sqlraceDll); + import MESL.SqlRace.Domain.*; + fprintf('OK (%s)\n', sqlraceDll); +catch ex + fprintf('FAILED\n'); + printDiagException(ex); + return; +end + +% --- Step 3: Initialise Core --- +fprintf('[3/5] Initialising Core... '); +try + NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); + Core.Initialize(); + fprintf('OK\n'); +catch ex + fprintf('FAILED\n'); + printDiagException(ex); + return; +end + +% --- Step 4: Load a test session --- +fprintf('[4/5] Loading test session... '); +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'flight-test-manoeuvre-extraction.ssn2'); +sessionGuid = '828f3c72-9573-4a74-9803-20ff993d848a'; + +if ~exist(dbPath, 'file') + fprintf('SKIPPED — test data not found at %s\n', dbPath); +else + try + connStr = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + sessionManager = SessionManager.CreateSessionManager(); + sessions = sessionManager.FindBySessionState(SessionState.Historical, connStr); + sessionKey = sessions.Item(0).Key; + clientSession = sessionManager.Load(sessionKey, connStr); + session = clientSession.Session; + fprintf('OK (Parameters: %d, Segments: %d)\n', ... + session.Parameters.Count, session.LapCollection.Count); + catch ex + fprintf('FAILED\n'); + printDiagException(ex); + clientSession = []; + end +end + +% --- Step 5: Create PDA and read data --- +if exist('session', 'var') + fprintf('[5/5] Reading parameter data... '); + try + pda = session.CreateParameterDataAccess('Temperature:Sensors'); + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + fprintf('OK (%d samples)\n', samples.SampleCount); + pda.Dispose(); + catch ex + fprintf('FAILED\n'); + printDiagException(ex); + end + clientSession.Dispose(); +else + fprintf('[5/5] Reading parameter data... SKIPPED (no session)\n'); +end + +fprintf('\n=== Diagnostics complete ===\n'); + +% --- Helper: print .NET exception chain --- +function printDiagException(ME) + fprintf(' MATLAB: %s\n', ME.message); + try + if isprop(ME, 'ExceptionObject') && ~isempty(ME.ExceptionObject) + ex = ME.ExceptionObject; + depth = 0; + while ~isempty(ex) + fprintf(' .NET[%d]: [%s] %s\n', depth, ... + char(ex.GetType().FullName), char(ex.Message)); + try ex = ex.InnerException; catch; ex = []; end + depth = depth + 1; + end + end + catch + end +end diff --git a/snippets/matlab/getting-started/load_session.m b/snippets/matlab/getting-started/load_session.m new file mode 100644 index 0000000..5caa91f --- /dev/null +++ b/snippets/matlab/getting-started/load_session.m @@ -0,0 +1,77 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Load Session +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Loading a session from SQLite using SessionManager +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: Session summary printed to command window +% +% Related: snippets/matlab/getting-started/read_parameters.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load the SQL Race assembly --- +% Set SQLRACE_DLL_PATH environment variable to override the default +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +% --- Initialise SQL Race --- +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); +disp('SQL Race initialised'); + +% --- Connection string --- +% Default: use the flight-test scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'flight-test-manoeuvre-extraction.ssn2'); +sessionGuid = '828f3c72-9573-4a74-9803-20ff993d848a'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +% --- Load the session --- +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + + fprintf('Session loaded: %s\n', sessionGuid); + fprintf(' Identifier: %s\n', char(session.Identifier)); + fprintf(' Start time: %d ns\n', session.StartTime); + fprintf(' End time: %d ns\n', session.EndTime); + fprintf(' Duration: %.3f s\n', double(session.EndTime - session.StartTime) / 1e9); + fprintf(' Parameters: %d\n', session.Parameters.Count); + fprintf(' Laps: %d\n', session.LapCollection.Count); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Session closed.'); diff --git a/snippets/matlab/getting-started/query_sessions.m b/snippets/matlab/getting-started/query_sessions.m new file mode 100644 index 0000000..330b744 --- /dev/null +++ b/snippets/matlab/getting-started/query_sessions.m @@ -0,0 +1,92 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Query Sessions +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Using QueryManager with ScalarFilter from MATLAB +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A SQLite database with sessions +% Output: Matching session identifiers and recording times +% +% Related: snippets/matlab/getting-started/load_session.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; +import MESL.SqlRace.Domain.Query.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection string --- +% Default: use the flight-test scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'flight-test-manoeuvre-extraction.ssn2'); + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +% --- Create query manager with filter --- +qm = QueryManager.CreateQueryManager(connectionString); + +searchTerm = 'Example'; +queryFilter = ScalarFilter('Identifier', MatchingRule.Contains, searchTerm, false); +qm.Filter = queryFilter; + +fprintf('Searching for sessions matching "%s"...\n', searchTerm); +fprintf('%-40s %-30s %s\n', 'Session Key', 'Identifier', 'Recorded'); +fprintf('%s\n', repmat('-', 1, 94)); + +queryResult = qm.ExecuteQuery(); + +% --- Enumerate results via reflection (required for .NET Core interop) --- +t = queryResult.GetType(); +ienumIface = t.GetInterface('System.Collections.IEnumerable'); +miGetEnum = ienumIface.GetMethod('GetEnumerator'); +emptyArgs = NET.createArray('System.Object', 0); +en = miGetEnum.Invoke(queryResult, emptyArgs); + +ienumeratorT = System.Type.GetType('System.Collections.IEnumerator'); +miMoveNext = ienumeratorT.GetMethod('MoveNext'); +propCurrent = ienumeratorT.GetProperty('Current'); +miGetCurrent = propCurrent.GetGetMethod(); + +count = 0; +while miMoveNext.Invoke(en, emptyArgs) + summary = miGetCurrent.Invoke(en, emptyArgs); + fprintf('%-40s %-30s %s\n', ... + char(summary.Key.ToString()), ... + char(summary.Identifier), ... + char(summary.TimeOfRecording.ToString())); + count = count + 1; + if count >= 20 + break; + end +end + +if count == 0 + disp('No sessions found. Run 04-create-session-write-data.cs to create some.'); +else + fprintf('\n%d session(s) found.\n', count); +end diff --git a/snippets/matlab/getting-started/read_parameters.m b/snippets/matlab/getting-started/read_parameters.m new file mode 100644 index 0000000..fc302e4 --- /dev/null +++ b/snippets/matlab/getting-started/read_parameters.m @@ -0,0 +1,95 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Read Parameters +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Creating a PDA, reading samples, converting to MATLAB arrays +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A session with Temperature:Sensors data +% Output: MATLAB double arrays of timestamps and values +% +% Related: snippets/matlab/data-access/extract_to_timetable.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the flight-test scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'flight-test-manoeuvre-extraction.ssn2'); +sessionGuid = '828f3c72-9573-4a74-9803-20ff993d848a'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +paramId = 'Temperature:Sensors'; + +try + session = clientSession.Session; + pda = session.CreateParameterDataAccess(paramId); + + try + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + + % --- Convert .NET arrays to MATLAB double arrays --- + nSamples = samples.SampleCount; + timestamps = zeros(nSamples, 1, 'int64'); + values = zeros(nSamples, 1); + + for i = 1:nSamples + timestamps(i) = samples.Timestamp(i); + values(i) = samples.Data(i); + end + + fprintf('Parameter: %s\n', paramId); + fprintf('Samples: %d\n', nSamples); + fprintf('Min: %.4f\n', min(values)); + fprintf('Max: %.4f\n', max(values)); + fprintf('Mean: %.4f\n', mean(values)); + + % Plot if desired + % plot(double(timestamps - timestamps(1)) / 1e9, values); + % xlabel('Time (s)'); ylabel(paramId); + catch ex + fprintf('Error reading data: %s\n', ex.message); + end + + pda.Dispose(); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); diff --git a/snippets/python/README.md b/snippets/python/README.md new file mode 100644 index 0000000..a7de939 --- /dev/null +++ b/snippets/python/README.md @@ -0,0 +1,51 @@ +# Python Snippets + +Python examples use [pythonnet](https://pythonnet.github.io/) with CoreCLR to call the SQL Race .NET API from Python. + +## Prerequisites + +- **Python 3.8+** (3.10+ recommended) +- **pythonnet** — `pip install pythonnet` +- **SQL Race .NET assemblies** — installed with ATLAS 10+ at the default location +- **pandas** (optional) — `pip install pandas` (for `export_to_pandas.py`) + +## Installation + +```bash +pip install pythonnet pandas +``` + +## CoreCLR Runtime Loading + +Every snippet begins with the pythonnet CoreCLR initialisation boilerplate: + +```python +import clr +from pythonnet import load +load("coreclr") # Must be called before any CLR imports + +import os +# Reads from SQLRACE_DLL_PATH env var, falls back to default ATLAS install location +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +clr.AddReference(SQLRACE_DLL_PATH) + +from MESL.SqlRace.Domain import Core, SessionManager, SessionKey +``` + +## Common Gotchas + +| Issue | Solution | +|-------|----------| +| `FileNotFoundException` when loading DLL | Set the `SQLRACE_DLL_PATH` environment variable to point to your ATLAS installation's `MESL.SqlRace.Domain.dll` | +| `System.BadImageFormatException` | Python bitness (32/64-bit) must match the DLL. Use 64-bit Python. | +| `RuntimeError: No runtime selected` | Call `load("coreclr")` before any `import clr` or `from System import ...` | +| `ImportError: cannot import name 'load'` | Update pythonnet: `pip install --upgrade pythonnet` (need 3.0+) | +| `.Dispose()` not called | Python `with` statements do not call .NET `IDisposable.Dispose()`. Always use `try/finally` with explicit `.Dispose()` calls. | + +## Folders + +| Folder | Description | +|--------|-------------| +| [getting-started/](getting-started/) | Load sessions, read parameters, query metadata, extract events | +| [data-access/](data-access/) | Extract data to Python lists, pandas DataFrames, multi-parameter extraction | +| [automation/](automation/) | ATLAS automation via COM interop | diff --git a/snippets/python/data-access/export_to_pandas.py b/snippets/python/data-access/export_to_pandas.py new file mode 100644 index 0000000..413c1cb --- /dev/null +++ b/snippets/python/data-access/export_to_pandas.py @@ -0,0 +1,97 @@ +# ───────────────────────────────────────────────────────────── +# SQL Race Example: Export to Pandas DataFrame +# ───────────────────────────────────────────────────────────── +# +# Demonstrates: Extracting parameter data into a pandas DataFrame with +# timestamp conversion from nanoseconds to human-readable format +# Prerequisites: Python 3.8+, pythonnet, pandas, MESL.SqlRace.Domain DLL +# Input: A session with Temperature:Sensors data +# Output: DataFrame printed to console with formatted timestamps +# +# Related: snippets/python/data-access/multi_parameter_extract.py +# Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +# ───────────────────────────────────────────────────────────── + +import json +import os +import sys +import tempfile +from pathlib import Path + +# --- pythonnet CoreCLR initialisation --- +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +import pandas as pd + +from MESL.SqlRace.Domain import Core, SessionManager +from MAT.OCS.Core import SessionKey + +Core.LicenceProgramName = "SQLRace" +Core.Initialize() + +import tempfile +db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") +connection_string = f"DbEngine=SQLite;Data Source={db_path};" +param_id = "Temperature:Sensors" + +if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + +try: + session_key = SessionKey.Parse(sys.argv[1]) +except Exception as ex: + print(f"ERROR: '{sys.argv[1]}' is not a valid session GUID: {ex}") + sys.exit(1) + +session_manager = SessionManager.CreateSessionManager() +client_session = session_manager.Load(session_key, connection_string) +if client_session is None: + print(f"ERROR: Session '{session_key}' not found at '{connection_string}'.") + sys.exit(1) +pda = None + +try: + session = client_session.Session + if not any(p.Identifier == param_id for p in session.Parameters): + print(f"ERROR: Parameter '{param_id}' not found in session.") + print(f"Available: {', '.join(p.Identifier for p in session.Parameters)[:200]}") + sys.exit(1) + pda = session.CreateParameterDataAccess(param_id) + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime) + + # --- Build DataFrame --- + timestamps_ns = [samples.Timestamp[i] for i in range(samples.SampleCount)] + values = [samples.Data[i] for i in range(samples.SampleCount)] + + df = pd.DataFrame({ + "timestamp_ns": timestamps_ns, + "seconds_from_midnight": [t / 1e9 for t in timestamps_ns], + param_id: values, + }) + + # --- Convert nanoseconds to human-readable time --- + df["time"] = pd.to_timedelta(df["timestamp_ns"], unit="ns") + + print(f"DataFrame: {len(df)} rows") + print(df.head(10).to_string(index=False)) + print(f"\nDescribe:\n{df[param_id].describe()}") +finally: + if pda is not None: + pda.Dispose() + client_session.Dispose() diff --git a/snippets/python/data-access/multi_parameter_extract.py b/snippets/python/data-access/multi_parameter_extract.py new file mode 100644 index 0000000..c1fc48c --- /dev/null +++ b/snippets/python/data-access/multi_parameter_extract.py @@ -0,0 +1,99 @@ +# ───────────────────────────────────────────────────────────── +# SQL Race Example: Multi-Parameter Extract +# ───────────────────────────────────────────────────────────── +# +# Demonstrates: Extracting multiple parameters into a dict of lists or a +# multi-column DataFrame, with proper PDA disposal +# Prerequisites: Python 3.8+, pythonnet, pandas, MESL.SqlRace.Domain DLL +# Input: A session with multiple parameters +# Output: Multi-column DataFrame with all parameters +# +# Related: snippets/python/data-access/export_to_pandas.py +# Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +# ───────────────────────────────────────────────────────────── + +import json +import os +import sys +import tempfile +from pathlib import Path + +# --- pythonnet CoreCLR initialisation --- +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +import pandas as pd + +from MESL.SqlRace.Domain import Core, SessionManager +from MAT.OCS.Core import SessionKey + +Core.LicenceProgramName = "SQLRace" +Core.Initialize() + +import tempfile +db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") +connection_string = f"DbEngine=SQLite;Data Source={db_path};" + +if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + +try: + session_key = SessionKey.Parse(sys.argv[1]) +except Exception as ex: + print(f"ERROR: '{sys.argv[1]}' is not a valid session GUID: {ex}") + sys.exit(1) + +session_manager = SessionManager.CreateSessionManager() +client_session = session_manager.Load(session_key, connection_string) +if client_session is None: + print(f"ERROR: Session '{session_key}' not found at '{connection_string}'.") + sys.exit(1) + +try: + session = client_session.Session + identifiers = [p.Identifier for p in session.Parameters] + if not identifiers: + print("WARNING: Session has no parameters. Nothing to extract.") + sys.exit(0) + print(f"Session has {len(identifiers)} parameter(s): {identifiers}") + + # --- Extract each parameter into a dict --- + all_data = {} + + for param_id in identifiers: + pda = session.CreateParameterDataAccess(param_id) + try: + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime) + timestamps = [samples.Timestamp[i] for i in range(samples.SampleCount)] + values = [samples.Data[i] for i in range(samples.SampleCount)] + all_data[param_id] = {"timestamps": timestamps, "values": values} + print(f" {param_id}: {len(values)} samples") + finally: + pda.Dispose() + + # --- Build a multi-column DataFrame (using first parameter's timestamps) --- + if all_data: + first_key = identifiers[0] + df = pd.DataFrame({"timestamp_ns": all_data[first_key]["timestamps"]}) + for param_id in identifiers: + df[param_id] = all_data[param_id]["values"][:len(df)] + + print(f"\nDataFrame: {df.shape[0]} rows x {df.shape[1]} columns") + print(df.head(10).to_string(index=False)) +finally: + client_session.Dispose() diff --git a/snippets/python/data-access/read_samples_to_list.py b/snippets/python/data-access/read_samples_to_list.py new file mode 100644 index 0000000..5375c31 --- /dev/null +++ b/snippets/python/data-access/read_samples_to_list.py @@ -0,0 +1,92 @@ +# ───────────────────────────────────────────────────────────── +# SQL Race Example: Read Samples to List +# ───────────────────────────────────────────────────────────── +# +# Demonstrates: Extracting parameter samples into native Python lists +# Prerequisites: Python 3.8+, pythonnet, MESL.SqlRace.Domain DLL +# Input: A session with Temperature:Sensors data +# Output: Python lists of timestamps and values +# +# Related: snippets/python/data-access/export_to_pandas.py +# Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +# ───────────────────────────────────────────────────────────── + +import json +import os +import sys +import tempfile +from pathlib import Path + +# --- pythonnet CoreCLR initialisation --- +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +from MESL.SqlRace.Domain import Core, SessionManager +from MAT.OCS.Core import SessionKey + +Core.LicenceProgramName = "SQLRace" +Core.Initialize() + +import tempfile +db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") +connection_string = f"DbEngine=SQLite;Data Source={db_path};" +param_id = "Temperature:Sensors" + +if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + +try: + session_key = SessionKey.Parse(sys.argv[1]) +except Exception as ex: + print(f"ERROR: '{sys.argv[1]}' is not a valid session GUID: {ex}") + sys.exit(1) + +session_manager = SessionManager.CreateSessionManager() +client_session = session_manager.Load(session_key, connection_string) +if client_session is None: + print(f"ERROR: Session '{session_key}' not found at '{connection_string}'.") + sys.exit(1) +pda = None + +try: + session = client_session.Session + if not any(p.Identifier == param_id for p in session.Parameters): + print(f"ERROR: Parameter '{param_id}' not found in session.") + print(f"Available: {', '.join(p.Identifier for p in session.Parameters)[:200]}") + sys.exit(1) + pda = session.CreateParameterDataAccess(param_id) + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime) + if samples.SampleCount == 0: + print(f"WARNING: No samples in session for '{param_id}'.") + sys.exit(0) + + # --- Copy .NET arrays into native Python lists --- + timestamps = [samples.Timestamp[i] for i in range(samples.SampleCount)] + values = [samples.Data[i] for i in range(samples.SampleCount)] + + print(f"Extracted {len(values)} samples into Python lists") + print(f" Type: timestamps={type(timestamps).__name__}, values={type(values).__name__}") + print(f" First: t={timestamps[0]} ns, v={values[0]:.4f}") + print(f" Last: t={timestamps[-1]} ns, v={values[-1]:.4f}") + print(f" Min: {min(values):.4f}") + print(f" Max: {max(values):.4f}") + print(f" Mean: {sum(values) / len(values):.4f}") +finally: + if pda is not None: + pda.Dispose() + client_session.Dispose() diff --git a/snippets/python/getting-started/01_load_session.py b/snippets/python/getting-started/01_load_session.py new file mode 100644 index 0000000..3732ea4 --- /dev/null +++ b/snippets/python/getting-started/01_load_session.py @@ -0,0 +1,83 @@ +# ───────────────────────────────────────────────────────────── +# SQL Race Example: Load Session +# ───────────────────────────────────────────────────────────── +# +# Demonstrates: Loading a session from a SQLite database using SessionManager +# Prerequisites: Python 3.8+, pythonnet, MESL.SqlRace.Domain DLL +# Input: A SQLite database with sessions and a session GUID +# Output: Session summary (name, start time, end time, parameter count) +# +# Related: snippets/python/getting-started/02_read_parameters.py +# Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +# ───────────────────────────────────────────────────────────── + +import json +import os +import sys +import tempfile +from pathlib import Path + +# --- pythonnet CoreCLR initialisation --- +# Requires WindowsDesktop runtime for Microsoft.Win32.SystemEvents dependency +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +from MESL.SqlRace.Domain import Core, SessionManager +from MAT.OCS.Core import SessionKey + +# --- Initialise SQL Race --- +Core.LicenceProgramName = "SQLRace" +Core.Initialize() +print("SQL Race initialised") + +# --- Connection string --- +db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") +connection_string = f"DbEngine=SQLite;Data Source={db_path};" + +if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + print(f"Database: {connection_string}") + sys.exit(1) + +try: + session_key = SessionKey.Parse(sys.argv[1]) +except Exception as ex: + print(f"ERROR: '{sys.argv[1]}' is not a valid session GUID: {ex}") + sys.exit(1) + +# --- Load the session --- +session_manager = SessionManager.CreateSessionManager() +client_session = session_manager.Load(session_key, connection_string) + +if client_session is None: + print(f"ERROR: Session '{session_key}' not found.") + print(f"Connection: {connection_string}") + print("Tip: Run snippets/csharp/getting-started/04-create-session-write-data.cs first to create a session.") + sys.exit(1) + +try: + session = client_session.Session + print(f"Session loaded: {session_key}") + print(f" Name: {session.Identifier}") + print(f" Start time: {session.StartTime} ns") + print(f" End time: {session.EndTime} ns") + print(f" Duration: {(session.EndTime - session.StartTime) / 1e9:.3f} s") + print(f" Parameters: {session.Parameters.Count}") + print(f" Laps: {session.LapCollection.Count}") +finally: + client_session.Dispose() + print("\nSession closed.") diff --git a/snippets/python/getting-started/02_read_parameters.py b/snippets/python/getting-started/02_read_parameters.py new file mode 100644 index 0000000..a68782e --- /dev/null +++ b/snippets/python/getting-started/02_read_parameters.py @@ -0,0 +1,89 @@ +# ───────────────────────────────────────────────────────────── +# SQL Race Example: Read Parameters +# ───────────────────────────────────────────────────────────── +# +# Demonstrates: Creating a PDA, reading samples, printing timestamped values +# Prerequisites: Python 3.8+, pythonnet, MESL.SqlRace.Domain DLL +# Input: A session with Temperature:Sensors data +# Output: Timestamped parameter values +# +# Related: snippets/python/getting-started/01_load_session.py +# Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +# ───────────────────────────────────────────────────────────── + +import json +import os +import sys +import tempfile +from pathlib import Path + +# --- pythonnet CoreCLR initialisation --- +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +from MESL.SqlRace.Domain import Core, SessionManager +from MAT.OCS.Core import SessionKey + +Core.LicenceProgramName = "SQLRace" +Core.Initialize() + +import tempfile +db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") +connection_string = f"DbEngine=SQLite;Data Source={db_path};" +param_id = "Temperature:Sensors" + +if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + +try: + session_key = SessionKey.Parse(sys.argv[1]) +except Exception as ex: + print(f"ERROR: '{sys.argv[1]}' is not a valid session GUID: {ex}") + sys.exit(1) + +session_manager = SessionManager.CreateSessionManager() +client_session = session_manager.Load(session_key, connection_string) +if client_session is None: + print(f"ERROR: Session '{session_key}' not found at '{connection_string}'.") + sys.exit(1) +pda = None + +try: + session = client_session.Session + available = [p.Identifier for p in session.Parameters] + if param_id not in available: + print(f"ERROR: Parameter '{param_id}' not found in session.") + print(f"Available: {', '.join(available[:10])}") + sys.exit(1) + pda = session.CreateParameterDataAccess(param_id) + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime) + + print(f"Parameter: {param_id}") + print(f"Samples: {samples.SampleCount}") + print(f"{'Timestamp (ns)':>20} {'Value':>12}") + print("─" * 34) + + for i in range(min(samples.SampleCount, 20)): + print(f"{samples.Timestamp[i]:>20} {samples.Data[i]:>12.4f}") + + if samples.SampleCount > 20: + print(f" ... and {samples.SampleCount - 20} more samples") +finally: + if pda is not None: + pda.Dispose() + client_session.Dispose() diff --git a/snippets/python/getting-started/03_query_sessions.py b/snippets/python/getting-started/03_query_sessions.py new file mode 100644 index 0000000..517b5e2 --- /dev/null +++ b/snippets/python/getting-started/03_query_sessions.py @@ -0,0 +1,68 @@ +# ───────────────────────────────────────────────────────────── +# SQL Race Example: Query Sessions +# ───────────────────────────────────────────────────────────── +# +# Demonstrates: Using QueryManager with ScalarFilter to find sessions +# Prerequisites: Python 3.8+, pythonnet, MESL.SqlRace.Domain DLL +# Input: A SQLite database with sessions +# Output: Matching session identifiers and recording times +# +# Related: snippets/python/getting-started/01_load_session.py +# Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +# ───────────────────────────────────────────────────────────── + +import json +import os +import tempfile +from pathlib import Path + +# --- pythonnet CoreCLR initialisation --- +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +from MESL.SqlRace.Domain import Core +from MESL.SqlRace.Domain.Query import QueryManager, ScalarFilter, MatchingRule + +Core.LicenceProgramName = "SQLRace" +Core.Initialize() + +import tempfile +db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") +connection_string = f"DbEngine=SQLite;Data Source={db_path};" + +# --- Create query manager and set a filter --- +query_manager = QueryManager.CreateQueryManager(connection_string) + +search_term = "Example" +query_filter = ScalarFilter("Identifier", MatchingRule.Contains, search_term, False) +query_manager.Filter = query_filter + +print(f"Searching for sessions matching '{search_term}'...") +print(f"{'Session Key':<40} {'Identifier':<30} {'Recorded':>22}") +print("─" * 94) + +count = 0 +for summary in query_manager.ExecuteQuery(): + print(f"{str(summary.Key):<40} {summary.Identifier:<30} {summary.TimeOfRecording}") + count += 1 + if count >= 20: + break + +if count == 0: + print("No sessions found. Run 04-create-session-write-data.cs to create some.") +else: + print(f"\n{count} session(s) found.") diff --git a/snippets/python/getting-started/04_extract_events.py b/snippets/python/getting-started/04_extract_events.py new file mode 100644 index 0000000..b61bc9c --- /dev/null +++ b/snippets/python/getting-started/04_extract_events.py @@ -0,0 +1,84 @@ +# ───────────────────────────────────────────────────────────── +# SQL Race Example: Extract Events +# ───────────────────────────────────────────────────────────── +# +# Demonstrates: Loading event definitions and event data from a session +# Prerequisites: Python 3.8+, pythonnet, MESL.SqlRace.Domain DLL +# Input: A session with event data (run event-definitions-and-data.cs first) +# Output: Event definitions and instances with timestamps +# +# Related: snippets/csharp/session-management/event-definitions-and-data.cs +# Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +# ───────────────────────────────────────────────────────────── + +import json +import os +import sys +import tempfile +from pathlib import Path + +# --- pythonnet CoreCLR initialisation --- +SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") +ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) + +_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") +with open(_runtime_cfg, "w") as _f: + json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ + {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, + {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], + "additionalProbingPaths": [ATLAS_DIR]}}, _f) + +from pythonnet import load +load("coreclr", runtime_config=_runtime_cfg) + +import clr +clr.AddReference(SQLRACE_DLL_PATH) +clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) + +from MESL.SqlRace.Domain import Core, SessionManager +from MAT.OCS.Core import SessionKey + +Core.LicenceProgramName = "SQLRace" +Core.Initialize() + +import tempfile +db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") +connection_string = f"DbEngine=SQLite;Data Source={db_path};" + +if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + +try: + session_key = SessionKey.Parse(sys.argv[1]) +except Exception as ex: + print(f"ERROR: '{sys.argv[1]}' is not a valid session GUID: {ex}") + sys.exit(1) + +session_manager = SessionManager.CreateSessionManager() +client_session = session_manager.Load(session_key, connection_string) +if client_session is None: + print(f"ERROR: Session '{session_key}' not found at '{connection_string}'.") + sys.exit(1) + +try: + session = client_session.Session + + # --- Event definitions → Python dict --- + definitions = {} + for evt_def in session.Events.EventDefinitions: + definitions[evt_def.Name] = evt_def.Priority + print(f"Definition: {evt_def.Name} (priority: {evt_def.Priority})") + + # --- Event data --- + events = session.Events.GetEventData(session.StartTime, session.EndTime) + + print(f"\n{'Timestamp (ns)':>20} {'Event':<22} {'Data'}") + print("─" * 70) + + for evt in events: + values = [str(evt.Values[i]) for i in range(evt.Values.Count)] if evt.Values else [] + data_str = ", ".join(values) + print(f"{evt.Timestamp:>20} {evt.EventDefinitionName:<22} [{data_str}]") +finally: + client_session.Dispose() diff --git a/tests/SqlRace.Examples.Tests/CoreLibraryTests.cs b/tests/SqlRace.Examples.Tests/CoreLibraryTests.cs new file mode 100644 index 0000000..b5505da --- /dev/null +++ b/tests/SqlRace.Examples.Tests/CoreLibraryTests.cs @@ -0,0 +1,179 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Examples: Core Library Tests +// ───────────────────────────────────────────────────────────── +// +// Unit tests for SqlRace.Examples.Core — ConnectionStringProvider, +// ParameterBuilder, and SqlRaceBootstrapper. +// +// Run: dotnet test +// ───────────────────────────────────────────────────────────── + +using Xunit; +using SqlRace.Examples.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +namespace SqlRace.Examples.Tests; + +public class ConnectionStringProviderTests +{ + [Fact] + public void Default_ReturnsLocalSqlite() + { + var cs = ConnectionStringProvider.Resolve(); + + Assert.Contains("DbEngine=SQLite", cs); + Assert.Contains("sqlrace-examples.ssn2", cs); + } + + [Fact] + public void Explicit_OverridesDefault() + { + var cs = ConnectionStringProvider.Resolve("DbEngine=SQLite;Data Source=custom.ssn2;"); + + Assert.Contains("custom.ssn2", cs); + } + + [Fact] + public void EnvironmentVariable_OverridesDefault() + { + const string envCs = "DbEngine=SQLite;Data Source=env-test.ssn2;"; + var previous = Environment.GetEnvironmentVariable("SQLRACE_CONNECTION_STRING"); + + try + { + Environment.SetEnvironmentVariable("SQLRACE_CONNECTION_STRING", envCs); + var cs = ConnectionStringProvider.Resolve(); + + Assert.Contains("env-test.ssn2", cs); + } + finally + { + Environment.SetEnvironmentVariable("SQLRACE_CONNECTION_STRING", previous); + } + } + + [Fact] + public void ForSQLiteFile_FormatsCorrectly() + { + var cs = ConnectionStringProvider.ForSQLiteFile(@"C:\data\test.ssn2"); + + Assert.Contains("DbEngine=SQLite", cs); + Assert.Contains(@"C:\data\test.ssn2", cs); + } + + [Fact] + public void DefaultSQLiteFilePath_EndsWithSsn2() + { + Assert.EndsWith("sqlrace-examples.ssn2", ConnectionStringProvider.DefaultSQLiteFilePath); + } +} + +public class ParameterBuilderTests +{ + [Fact] + public void Builder_CreatesParameter() + { + SqlRaceBootstrapper.Initialize(); + var connStr = ConnectionStringProvider.ForSQLiteFile( + Path.Combine(Path.GetTempPath(), $"test-builder-{Guid.NewGuid():N}.ssn2")); + + using var clientSession = SessionFactory.CreateSession("Test", connStr); + var session = clientSession.Session; + + var result = new ParameterBuilder(session) + .WithIdentifier("Temperature:Sensors") + .WithDescription("Inlet temperature sensor") + .InApplicationGroup("Sensors") + .InParameterGroup("Thermal") + .WithFrequency(100, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("degC", "%5.2f") + .WithRange(min: 0, max: 500) + .Build(); + + Assert.NotNull(result.Parameter); + Assert.True(result.ChannelId > 0); + Assert.Equal(1, session.Parameters.Count); + } + + [Fact] + public void Builder_FluentChaining() + { + SqlRaceBootstrapper.Initialize(); + var connStr = ConnectionStringProvider.ForSQLiteFile( + Path.Combine(Path.GetTempPath(), $"test-fluent-{Guid.NewGuid():N}.ssn2")); + + using var clientSession = SessionFactory.CreateSession("Test", connStr); + var session = clientSession.Session; + + var result = new ParameterBuilder(session) + .WithIdentifier("Speed:Shaft") + .WithName("Shaft Speed") + .WithDescription("Rotational speed of the main shaft") + .WithFrequency(100, FrequencyUnit.Hz) + .InApplicationGroup("Powertrain") + .InParameterGroup("Drivetrain") + .Build(); + + Assert.NotNull(result.Parameter); + Assert.Equal(1, session.Parameters.Count); + } + + [Fact] + public void Builder_BatchBuild() + { + SqlRaceBootstrapper.Initialize(); + var connStr = ConnectionStringProvider.ForSQLiteFile( + Path.Combine(Path.GetTempPath(), $"test-batch-{Guid.NewGuid():N}.ssn2")); + + using var clientSession = SessionFactory.CreateSession("Test", connStr); + var session = clientSession.Session; + + var builders = new[] + { + new ParameterBuilder(session) + .WithIdentifier("Temperature:Sensors") + .InApplicationGroup("Sensors").InParameterGroup("Thermal") + .WithFrequency(100, FrequencyUnit.Hz) + .WithConversion("degC"), + new ParameterBuilder(session) + .WithIdentifier("Pressure:Inlet") + .InApplicationGroup("Sensors").InParameterGroup("Hydraulic") + .WithFrequency(50, FrequencyUnit.Hz) + .WithConversion("bar"), + new ParameterBuilder(session) + .WithIdentifier("Speed:Shaft") + .InApplicationGroup("Powertrain").InParameterGroup("Drivetrain") + .WithFrequency(200, FrequencyUnit.Hz) + .WithConversion("rpm"), + }; + + var results = ParameterBuilder.BuildBatch(session, builders); + + Assert.Equal(3, results.Count); + Assert.Equal(3, session.Parameters.Count); + } + + [Fact] + public void Builder_ThrowsWithoutIdentifier() + { + // ParameterBuilder requires a Session, but we can verify the + // null-check on the session argument without SQL Race runtime. + Assert.Throws(() => new ParameterBuilder(null!)); + } +} + +public class SqlRaceBootstrapperTests +{ + [Fact] + public void Initialize_MethodExists() + { + // SqlRaceBootstrapper.Initialize() should be idempotent + // and safe to call even without MESL assemblies at test time. + // This test verifies the method exists and is callable. + var method = typeof(SqlRaceBootstrapper).GetMethod("Initialize"); + Assert.NotNull(method); + } +} diff --git a/tests/SqlRace.Examples.Tests/GettingStartedTests.cs b/tests/SqlRace.Examples.Tests/GettingStartedTests.cs new file mode 100644 index 0000000..d9d4887 --- /dev/null +++ b/tests/SqlRace.Examples.Tests/GettingStartedTests.cs @@ -0,0 +1,107 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Examples: Getting Started Integration Tests +// ───────────────────────────────────────────────────────────── +// +// Integration tests that verify the GettingStarted project can +// create a session, write data, and read it back using a local +// SQLite database. +// +// These tests require the MESL.SQLRace.API NuGet package AND an ATLAS +// installation (for the SQL Race licence). They pass locally but are +// skipped in CI where no licence is available. +// +// Run: dotnet test --filter "FullyQualifiedName~GettingStarted" +// ───────────────────────────────────────────────────────────── + +using Xunit; +using SqlRace.Examples.Core; +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +namespace SqlRace.Examples.Tests; + +/// +/// Integration tests for session create → write → read cycle. +/// These require the SQL Race runtime to be available. +/// +public class GettingStartedTests : IDisposable +{ + private readonly string _dbPath; + private readonly string _connectionString; + + public GettingStartedTests() + { + _dbPath = Path.Combine(Path.GetTempPath(), $"sqlrace-test-{Guid.NewGuid():N}.ssn2"); + _connectionString = ConnectionStringProvider.ForSQLiteFile(_dbPath); + } + + public void Dispose() + { + // Clean up test database + if (File.Exists(_dbPath)) + { + try { File.Delete(_dbPath); } + catch { /* best effort cleanup */ } + } + } + + [Fact] + public void CreateSession_ReturnsValidSession() + { + SqlRaceBootstrapper.Initialize(); + + using var clientSession = SessionFactory.CreateSession("Test", _connectionString); + Assert.NotNull(clientSession); + Assert.NotNull(clientSession.Session); + Assert.True(clientSession.Session.StartTime >= 0); + } + + [Fact] + public void WriteAndReadData_RoundTrips() + { + SqlRaceBootstrapper.Initialize(); + + using var clientSession = SessionFactory.CreateSession("RoundTrip", _connectionString); + var session = clientSession.Session; + + var result = new ParameterBuilder(session) + .WithIdentifier("Temperature:Sensors") + .WithFrequency(100, FrequencyUnit.Hz) + .InApplicationGroup("Sensors") + .InParameterGroup("Thermal") + .WithConversion("degC") + .Build(); + + // Write data + var startTime = 36_000_000_000_000L; + var interval = new Frequency(100, FrequencyUnit.Hz).ToInterval(); + for (int i = 0; i < 10; i++) + { + session.AddChannelData(result.ChannelId, startTime + i * interval, 1, BitConverter.GetBytes(20.0 + i)); + } + + // Read data back + using var pda = session.CreateParameterDataAccess("Temperature:Sensors"); + var samples = pda.GetSamplesBetween(startTime, startTime + 10 * interval); + + Assert.Equal(10, samples.SampleCount); + Assert.Equal(20.0, samples.Data[0], precision: 2); + } + + [Fact] + public void LoadSession_WithValidKey_ReturnsSession() + { + SqlRaceBootstrapper.Initialize(); + + // Create a session first + using var created = SessionFactory.CreateSession("LoadTest", _connectionString); + var sessionKey = created.Session.Key; + + // Load it back + using var loaded = SessionFactory.LoadSession(sessionKey, _connectionString); + Assert.NotNull(loaded); + Assert.Equal(sessionKey, loaded.Session.Key); + } +} diff --git a/tests/SqlRace.Examples.Tests/SnippetCompilationTests.cs b/tests/SqlRace.Examples.Tests/SnippetCompilationTests.cs new file mode 100644 index 0000000..bb4bce4 --- /dev/null +++ b/tests/SqlRace.Examples.Tests/SnippetCompilationTests.cs @@ -0,0 +1,105 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Examples: Snippet Compilation Tests +// ───────────────────────────────────────────────────────────── +// +// Dynamically discovers all .cs files under snippets/csharp/ and +// verifies they parse as valid C# syntax using Roslyn. +// +// This catches syntax errors and missing brackets but does NOT +// verify against the real SQL Race assemblies (that requires the +// NuGet package to be resolvable at test time). +// +// Run: dotnet test --filter "FullyQualifiedName~SnippetCompilation" +// ───────────────────────────────────────────────────────────── + +using Xunit; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace SqlRace.Examples.Tests; + +public class SnippetCompilationTests +{ + private static readonly string SnippetsRoot = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "snippets", "csharp")); + + private static IEnumerable GetAllSnippetFiles() + { + if (!Directory.Exists(SnippetsRoot)) + return Enumerable.Empty(); + + return Directory.EnumerateFiles(SnippetsRoot, "*.cs", SearchOption.AllDirectories); + } + + public static IEnumerable SnippetFiles() + { + foreach (var file in GetAllSnippetFiles()) + { + var relative = Path.GetRelativePath(SnippetsRoot, file); + yield return new object[] { relative, file }; + } + } + + [Theory] + [MemberData(nameof(SnippetFiles))] + public void Snippet_ParsesWithoutSyntaxErrors(string relativePath, string fullPath) + { + var source = File.ReadAllText(fullPath); + var syntaxTree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.CSharp12)); + var diagnostics = syntaxTree.GetDiagnostics() + .Where(d => d.Severity == DiagnosticSeverity.Error) + .ToList(); + + Assert.True(diagnostics.Count == 0, + $"Syntax errors in {relativePath}:\n" + + string.Join("\n", diagnostics.Select(d => $" Line {d.Location.GetLineSpan().StartLinePosition.Line + 1}: {d.GetMessage()}"))); + } + + [Fact] + public void SnippetsDirectory_Exists() + { + // This test verifies the test infrastructure can find the snippets + // Skip if running in CI without the full repo layout + if (!Directory.Exists(SnippetsRoot)) + { + // Not a failure — just skip + return; + } + + var files = GetAllSnippetFiles().ToList(); + Assert.True(files.Count > 0, $"No .cs files found in {SnippetsRoot}"); + } + + [Fact] + public void AllSnippets_HaveHeaderComment() + { + if (!Directory.Exists(SnippetsRoot)) + return; + + foreach (var file in GetAllSnippetFiles()) + { + var content = File.ReadAllText(file); + var relative = Path.GetRelativePath(SnippetsRoot, file); + + Assert.True( + content.Contains("SQL Race Example:"), + $"{relative} is missing the standard header comment 'SQL Race Example:'"); + } + } + + [Fact] + public void AllSnippets_AreUnder100Lines() + { + if (!Directory.Exists(SnippetsRoot)) + return; + + foreach (var file in GetAllSnippetFiles()) + { + var lineCount = File.ReadLines(file).Count(); + var relative = Path.GetRelativePath(SnippetsRoot, file); + + Assert.True(lineCount <= 100, + $"{relative} has {lineCount} lines (max 100). Consider splitting into smaller snippets."); + } + } +} diff --git a/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj b/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj new file mode 100644 index 0000000..f80cabe --- /dev/null +++ b/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + diff --git a/tests/test-data/README.md b/tests/test-data/README.md new file mode 100644 index 0000000..e16bb2a --- /dev/null +++ b/tests/test-data/README.md @@ -0,0 +1,16 @@ +# Test Data + +This directory contains test fixtures for integration tests. + +## Generating test data + +Test data is generated by the `GettingStartedTests` class, which creates temporary SQLite databases during test runs. These are cleaned up automatically after each test. + +For manual testing, use the `GettingStarted` project: + +```bash +cd projects/csharp +dotnet run --project GettingStarted +``` + +This creates a session in the default SQLite database at `%TEMP%/sqlrace-examples.ssn2`. diff --git a/tools/FindDataParams/FindDataParams.csproj b/tools/FindDataParams/FindDataParams.csproj new file mode 100644 index 0000000..c02ad00 --- /dev/null +++ b/tools/FindDataParams/FindDataParams.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + enable + enable + + + + + diff --git a/tools/FindDataParams/Program.cs b/tools/FindDataParams/Program.cs new file mode 100644 index 0000000..fc3ffe2 --- /dev/null +++ b/tools/FindDataParams/Program.cs @@ -0,0 +1,99 @@ +// Quick diagnostic: find parameters with data in the latest session +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.Enumerators; +using MESL.SqlRace.Domain.Query; +using MESL.SqlRace.Enumerators; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var cs = Environment.GetEnvironmentVariable("SQLRACE_CONNECTION_STRING") + ?? $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")}"; +var qm = QueryManager.CreateQueryManager(cs); +var sessions = qm.ExecuteQuery().OrderByDescending(s => s.TimeOfRecording).ToList(); + +Console.WriteLine($"Found {sessions.Count} sessions\n"); +var latest = sessions.First(); +Console.WriteLine($"Latest: {latest.Identifier} (Key: {latest.Key})"); +Console.WriteLine($"Recorded: {latest.TimeOfRecording}\n"); + +var sm = SessionManager.CreateSessionManager(); +using var client = sm.Load(latest.Key, cs); +var session = client.Session; +Console.WriteLine($"StartTime: {session.StartTime}"); +Console.WriteLine($"EndTime: {session.EndTime}"); +Console.WriteLine($"Duration: {(session.EndTime - session.StartTime) / 1_000_000_000.0:F1}s"); +Console.WriteLine($"Parameters: {session.Parameters.Count}"); +Console.WriteLine($"Laps: {session.LapCollection.Count}\n"); + +// List the 5 most recent sessions +Console.WriteLine("Recent sessions:"); +foreach (var s in sessions.Take(5)) + Console.WriteLine($" {s.Identifier} | {s.TimeOfRecording} | {s.Key}"); +Console.WriteLine(); + +// Try known common identifiers first +// Try domain-neutral names first, then common legacy names found in existing databases +var knownParams = new[] { + "Temperature:Sensors", "Pressure:Inlet", "Speed:Shaft", + "RPM:Turbine", "Voltage:Battery", "FlowRate:Coolant", + "Force:Actuator01", "Acceleration:Platform" +}; + +Console.WriteLine("Trying known parameter names..."); +foreach (var pid in knownParams) +{ + var exists = session.Parameters.Any(p => p.Identifier == pid); + if (!exists) { Console.WriteLine($" [not in session] {pid}"); continue; } + try + { + using var pda = session.CreateParameterDataAccess(pid); + var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + Console.WriteLine($" [{samples.SampleCount,8} samples] {pid} = {(samples.SampleCount > 0 ? samples.Data[0].ToString("F4") : "no data")}"); + } + catch (Exception ex) { Console.WriteLine($" ERROR: {pid} - {ex.Message}"); } +} + +// Also scan a random selection across the full list +Console.WriteLine("\nScanning random 200 parameters for data (via GoTo + GetNextSamples)..."); +var rng = new Random(42); +var allParams = session.Parameters.ToList(); +var indices = Enumerable.Range(0, allParams.Count).OrderBy(_ => rng.Next()).Take(200).ToList(); +var found = 0; +foreach (var idx in indices) +{ + var p = allParams[idx]; + try + { + using var pda = session.CreateParameterDataAccess(p.Identifier); + // Try GoTo + GetNextSamples (works better for live sessions) + pda.GoTo(session.EndTime); + var samples = pda.GetNextSamples(10, StepDirection.Reverse); + if (samples.SampleCount > 0) + { + Console.WriteLine($" [{samples.SampleCount,8} samples] {p.Identifier} = {samples.Data[0]:F4}"); + found++; + if (found >= 10) break; + } + } + catch { } +} + +// Also try GoTo for the known params +Console.WriteLine("\nRetrying known params with GoTo..."); +foreach (var pid in knownParams) +{ + if (!session.Parameters.Any(p => p.Identifier == pid)) continue; + try + { + using var pda = session.CreateParameterDataAccess(pid); + pda.GoTo(session.EndTime); + var samples = pda.GetNextSamples(10, StepDirection.Reverse); + Console.WriteLine($" [{samples.SampleCount,8} samples] {pid} = {(samples.SampleCount > 0 ? samples.Data[0].ToString("F4") : "no data")}"); + } + catch (Exception ex) { Console.WriteLine($" ERROR: {pid} - {ex.Message}"); } +} + +if (found == 0) + Console.WriteLine("\n Still no data found. Session may be live but data hasn't arrived yet."); diff --git a/tools/FindLiveSession/FindLiveSession.csproj b/tools/FindLiveSession/FindLiveSession.csproj new file mode 100644 index 0000000..7bf73cb --- /dev/null +++ b/tools/FindLiveSession/FindLiveSession.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + enable + enable + $(NoWarn);CA1416 + + + + + diff --git a/tools/FindLiveSession/Program.cs b/tools/FindLiveSession/Program.cs new file mode 100644 index 0000000..6d063e5 --- /dev/null +++ b/tools/FindLiveSession/Program.cs @@ -0,0 +1,221 @@ +// Finds the latest live (actively recording) session in a SQL Race database. +// A session is considered "live" if its EndTime increases between two loads. +// +// Usage: +// dotnet run --project tools/FindLiveSession +// dotnet run --project tools/FindLiveSession -- [connection-string] [--wait seconds] +// +// The --wait flag controls how long to monitor the best candidate for EndTime growth. +// Default is 15s. Data flushes happen every 60-70s, so use --wait 90 for reliable detection. +// +// Output (when a live session is found): +// LIVE_SESSION_KEY=69337cf2-754c-4153-b48c-80ea070c89d9 +// LIVE_SESSION_ID=my-session-001 +// LIVE_PARAM=Temperature:Sensors + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.Enumerators; +using MESL.SqlRace.Domain.Query; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +// Parse args: [connection-string] [--wait seconds] +var connectionString = Environment.GetEnvironmentVariable("SQLRACE_CONNECTION_STRING") + ?? $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")}"; +var waitSeconds = 15; + +for (var a = 0; a < args.Length; a++) +{ + if (args[a] == "--wait" && a + 1 < args.Length && int.TryParse(args[a + 1], out var w)) + { + waitSeconds = w; + a++; + } + else if (!args[a].StartsWith("--")) + { + connectionString = args[a]; + } +} + +Console.WriteLine($"Connection: {connectionString}"); +Console.WriteLine($"Wait time: {waitSeconds}s (use --wait 90 for reliable flush detection)"); +Console.WriteLine("Querying sessions...\n"); + +var queryManager = QueryManager.CreateQueryManager(connectionString); +var sessions = queryManager.ExecuteQuery() + .OrderByDescending(s => s.TimeOfRecording) + .ToList(); + +if (sessions.Count == 0) +{ + Console.WriteLine("ERROR: No sessions found in database."); + Environment.Exit(1); +} + +Console.WriteLine($"Found {sessions.Count} sessions.\n"); + +var sessionManager = SessionManager.CreateSessionManager(); + +// ─── Phase 1: Quick scan — load each recent session, capture EndTime & metadata ─── +var candidateCount = Math.Min(10, sessions.Count); +var candidates = new List<(dynamic Info, long EndTime, int ParamCount, double DurationSec)>(); + +Console.WriteLine("─── Recent sessions ─────────────────────────────────────"); +for (var i = 0; i < candidateCount; i++) +{ + var s = sessions[i]; + using var client = sessionManager.Load(s.Key, connectionString); + var session = client.Session; + var paramCount = session.Parameters.Count; + var endTime = session.EndTime; + var duration = (endTime - session.StartTime) / 1_000_000_000.0; + + var status = paramCount == 0 ? "no-params" : $"{paramCount} params, {duration:F0}s"; + Console.WriteLine($" {s.Identifier,-30} {s.TimeOfRecording:yyyy-MM-dd HH:mm:ss} ({status})"); + + if (paramCount > 0) + candidates.Add((s, endTime, paramCount, duration)); +} +Console.WriteLine(); + +if (candidates.Count == 0) +{ + Console.WriteLine("ERROR: No sessions with parameters found."); + Environment.Exit(1); +} + +// ─── Phase 2: Monitor EndTime growth on top candidates ──────────────────── +// Check the most recent sessions with parameters. Wait and reload to detect growth. +var checkCount = Math.Min(3, candidates.Count); +Console.WriteLine($"Monitoring top {checkCount} candidate(s) for {waitSeconds}s...\n"); + +var pollInterval = Math.Min(5, waitSeconds); +var elapsed = 0; + +while (elapsed < waitSeconds) +{ + await Task.Delay(pollInterval * 1000); + elapsed += pollInterval; + + for (var c = 0; c < checkCount; c++) + { + var (info, endTime1, paramCount, _) = candidates[c]; + using var client = sessionManager.Load(info.Key, connectionString); + var endTime2 = client.Session.EndTime; + + if (endTime2 > endTime1) + { + Console.WriteLine($" {info.Identifier}: EndTime GREW at {elapsed}s! (+{(endTime2 - endTime1) / 1_000_000_000.0:F1}s)"); + + // Found a live session + var paramWithData = FindParameterWithData(client.Session); + PrintLiveResult(info, client.Session, paramCount, endTime1, endTime2, paramWithData, connectionString); + return; + } + } + + Console.Write($" [{elapsed}/{waitSeconds}s] no growth detected...\r"); +} + +Console.WriteLine(); + +// ─── Phase 3: No live session found — show the best candidate anyway ────── +var best = candidates[0]; +Console.WriteLine($"\nNo live sessions detected (EndTime static across {waitSeconds}s)."); +Console.WriteLine(); + +// Still print the most recent session info — it's probably what the user wants +using var bestClient = sessionManager.Load(best.Info.Key, connectionString); +var bestParam = FindParameterWithData(bestClient.Session); + +Console.WriteLine("─── Most recent session (not confirmed live) ─────────────"); +Console.WriteLine($" Session Key: {best.Info.Key}"); +Console.WriteLine($" Identifier: {best.Info.Identifier}"); +Console.WriteLine($" Recorded: {best.Info.TimeOfRecording:yyyy-MM-dd HH:mm:ss}"); +Console.WriteLine($" Parameters: {best.ParamCount}"); +Console.WriteLine($" Duration: {best.DurationSec:F0}s"); +Console.WriteLine($" Working param: {bestParam ?? "(none)"}"); +Console.WriteLine(); +Console.WriteLine($"SESSION_KEY={best.Info.Key}"); +Console.WriteLine($"SESSION_ID={best.Info.Identifier}"); +if (bestParam is not null) + Console.WriteLine($"SESSION_PARAM={bestParam}"); +Console.WriteLine(); +Console.WriteLine("Tip: If recorder is running but flushes are slow, try: --wait 90"); + +static void PrintLiveResult(dynamic info, Session session, int paramCount, + long endTime1, long endTime2, string? paramWithData, string connectionString) +{ + Console.WriteLine(); + Console.WriteLine("═══ LIVE SESSION FOUND ══════════════════════════════════"); + Console.WriteLine($" Session Key: {info.Key}"); + Console.WriteLine($" Identifier: {info.Identifier}"); + Console.WriteLine($" Recorded: {info.TimeOfRecording:yyyy-MM-dd HH:mm:ss}"); + Console.WriteLine($" Parameters: {paramCount}"); + Console.WriteLine($" EndTime grew: {endTime1} → {endTime2} (+{(endTime2 - endTime1) / 1_000_000_000.0:F1}s)"); + Console.WriteLine($" Working param: {paramWithData ?? "(none found)"}"); + + Console.WriteLine(); + Console.WriteLine("─── Copy-paste values ───────────────────────────────────"); + Console.WriteLine($"LIVE_SESSION_KEY={info.Key}"); + Console.WriteLine($"LIVE_SESSION_ID={info.Identifier}"); + if (paramWithData is not null) + Console.WriteLine($"LIVE_PARAM={paramWithData}"); + + Console.WriteLine(); + Console.WriteLine("─── Snippet test commands ───────────────────────────────"); + var csArg = $"\"{connectionString}\""; + var keyArg = info.Key.ToString(); + var pArg = paramWithData ?? "Temperature:Sensors"; + Console.WriteLine($"# Copy snippet then run:"); + Console.WriteLine($"# poll-live-samples"); + Console.WriteLine($"Copy-Item snippets\\csharp\\live-data\\poll-live-samples.cs tools\\SnippetRunner\\Program.cs -Force"); + Console.WriteLine($"dotnet run --project tools/SnippetRunner -- {keyArg} {csArg} \"{pArg}\""); + Console.WriteLine($"# rda-parameter-change-detection"); + Console.WriteLine($"Copy-Item snippets\\csharp\\live-data\\rda-parameter-change-detection.cs tools\\SnippetRunner\\Program.cs -Force"); + Console.WriteLine($"dotnet run --project tools/SnippetRunner -- {keyArg} {csArg} \"{pArg}\""); + Console.WriteLine("═════════════════════════════════════════════════════════"); +} + +static string? FindParameterWithData(Session session) +{ + // Try domain-neutral names first, then scan the session + string[] wellKnown = [ + "Temperature:Sensors", "Pressure:Inlet", "Speed:Shaft", + "RPM:Turbine", "Voltage:Battery", "FlowRate:Coolant", + "Force:Actuator01", "Acceleration:Platform" + ]; + + foreach (var pid in wellKnown) + { + if (!session.Parameters.Any(p => p.Identifier == pid)) + continue; + try + { + using var pda = session.CreateParameterDataAccess(pid); + pda.GoTo(long.MaxValue); + var samples = pda.GetNextSamples(1, StepDirection.Reverse); + if (samples.SampleCount > 0) + return pid; + } + catch { /* parameter exists but can't be read — skip */ } + } + + // Fall back to scanning first 100 parameters + foreach (var p in session.Parameters.Take(100)) + { + try + { + using var pda = session.CreateParameterDataAccess(p.Identifier); + pda.GoTo(long.MaxValue); + var samples = pda.GetNextSamples(1, StepDirection.Reverse); + if (samples.SampleCount > 0) + return p.Identifier; + } + catch { /* skip */ } + } + + return null; +} diff --git a/tools/SnippetRunner/Program.cs b/tools/SnippetRunner/Program.cs new file mode 100644 index 0000000..1ac5eb7 --- /dev/null +++ b/tools/SnippetRunner/Program.cs @@ -0,0 +1,82 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Example: Parameter Unit Resolution +// ───────────────────────────────────────────────────────────── +// +// Demonstrates: Resolving the engineering unit for a parameter via its conversion +// Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 +// Input: A session with configured parameters (run 04-create-session-write-data.cs first) +// Output: Parameter details with resolved units +// +// Related: snippets/csharp/configuration/conversion-types.cs +// Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html +// ───────────────────────────────────────────────────────────── + +using MAT.OCS.Core; +using MESL.SqlRace.Domain; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +var connectionString = $"DbEngine=SQLite;Data Source={Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2")};"; + +if (args.Length == 0) +{ + Console.WriteLine("Usage: dotnet run -- "); + Console.WriteLine("Tip: Run 04-create-session-write-data.cs first to create a session."); + return; +} + +SessionKey sessionKey; +try +{ + sessionKey = SessionKey.Parse(args[0]); +} +catch (Exception ex) +{ + Console.WriteLine($"ERROR: '{args[0]}' is not a valid session GUID. {ex.Message}"); + return; +} + +var sessionManager = SessionManager.CreateSessionManager(); +IClientSession clientSession; +try { clientSession = sessionManager.Load(sessionKey, connectionString); } +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Could not load session '{sessionKey}': {ex.Message}"); + Console.WriteLine("Tip: Run 04-create-session-write-data.cs first, then pass the printed GUID."); + return; +} +using (clientSession) +{ +var session = clientSession.Session; + +Console.WriteLine($"Session: {session.Identifier} ({session.Parameters.Count} parameters)\n"); +Console.WriteLine($"{"Identifier",-30} {"Conversion",-25} {"Unit",-10} {"Format",-10} {"Range"}"); +Console.WriteLine(new string('─', 95)); + +foreach (var param in session.Parameters) +{ + // --- Resolve unit from the conversion --- + var conversionName = param.ConversionFunctionName; + var unit = "(unknown)"; + var format = ""; + + // TODO: Verify — GetConversion may be on ConfigurationSet or Session + try + { + var conversion = session.GetConversion(conversionName); + if (conversion is not null) + { + unit = conversion.Units ?? "(none)"; + format = conversion.FormatString ?? ""; + } + } + catch (Exception ex) + { + unit = "(error)"; + Console.Error.WriteLine($"WARNING: Failed to resolve conversion '{conversionName}' for {param.Identifier}: {ex.Message}"); + } + + Console.WriteLine($"{param.Identifier,-30} {conversionName,-25} {unit,-10} {format,-10} [{param.MinimumValue:F1}, {param.MaximumValue:F1}]"); +} +} diff --git a/tools/SnippetRunner/Program.cs.bak b/tools/SnippetRunner/Program.cs.bak new file mode 100644 index 0000000..d077272 --- /dev/null +++ b/tools/SnippetRunner/Program.cs.bak @@ -0,0 +1,22 @@ +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Collections; + +Core.LicenceProgramName = "SQLRace"; +Core.Initialize(); + +Console.WriteLine("ItemEventArgs props:"); +foreach (var p in typeof(ItemEventArgs).GetProperties()) + Console.WriteLine(" " + p.Name + " : " + p.PropertyType.Name); +Console.WriteLine(); +Console.WriteLine("LapEventArgs props:"); +foreach (var p in typeof(LapEventArgs).GetProperties()) + Console.WriteLine(" " + p.Name + " : " + p.PropertyType.Name); +Console.WriteLine(); +Console.WriteLine("Lap props:"); +foreach (var p in typeof(Lap).GetProperties()) + Console.WriteLine(" " + p.Name + " : " + p.PropertyType.Name); +Console.WriteLine(); +Console.WriteLine("SessionEventArgs props:"); +foreach (var p in typeof(SessionEventArgs).GetProperties()) + Console.WriteLine(" " + p.Name + " : " + p.PropertyType.Name); diff --git a/tools/SnippetRunner/SnippetRunner.csproj b/tools/SnippetRunner/SnippetRunner.csproj new file mode 100644 index 0000000..7bf73cb --- /dev/null +++ b/tools/SnippetRunner/SnippetRunner.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + enable + enable + $(NoWarn);CA1416 + + + + + From bfd4a46b57b76c9b5e1fc532559bac535ca259a9 Mon Sep 17 00:00:00 2001 From: Federica Donald Date: Wed, 10 Jun 2026 12:08:09 +0000 Subject: [PATCH 2/7] PR actions working with repo secrets and some branding with new logo --- .github/workflows/Check-References.ps1 | 44 --------------------- .github/workflows/Get-Package-Version.ps1 | 25 ------------ .github/workflows/build-and-test.yml | 12 +++--- .github/workflows/pr.yml | 48 ----------------------- .github/workflows/validate-notebooks.yml | 4 +- notebooks/README.md | 2 + projects/README.md | 2 + readme.md | 2 + snippets/README.md | 2 + 9 files changed, 16 insertions(+), 125 deletions(-) delete mode 100644 .github/workflows/Check-References.ps1 delete mode 100644 .github/workflows/Get-Package-Version.ps1 delete mode 100644 .github/workflows/pr.yml diff --git a/.github/workflows/Check-References.ps1 b/.github/workflows/Check-References.ps1 deleted file mode 100644 index 6d20012..0000000 --- a/.github/workflows/Check-References.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -. $PSScriptRoot"\Get-Package-Version.ps1" - -function Check-References([string] $RootDirectory, [string] $PackageName, [string[]] $VersionMustContain, [string[]] $VersionMustNotContain) { - # Find all of the packages.config files - $packageFiles = @(Get-ChildItem -Path $RootDirectory -Filter packages.config -Recurse | Select-Object -ExpandProperty "FullName") - - # The value we will return to indicate whether the references as valid or not - $validationSuccessful = $true - - foreach($packageFile in $packageFiles) { - $version = Get-Package-Version -PackageFilePath $packageFile -PackageName $PackageName - - if($version -eq $null) { - # The package in question wasn't referenced from this packages.config file - continue - } - - # The version must contain each of the strings in $VersionMustContain array - foreach($validRegex in $VersionMustContain){ - if($version -notmatch $validRegex) { - # This is referencing a version of a package that we aren't permitted to reference - $validationSuccessful = $false - - Write-Host "[ERROR]: Package file '$packageFile' is referencing an invalid build of '$packageName' - $version must contain $validRegex" -ForegroundColor red - } - } - - # The version must not contain any of the strings in $VersionMustNotContain array - foreach($invalidRegex in $VersionMustNotContain){ - if($version -match $invalidRegex) { - # This is referencing a version of a package that we aren't permitted to reference - $validationSuccessful = $false - - Write-Host "[ERROR]: Package file '$packageFile' is referencing an invalid build of '$packageName' - $version must not contain $invalidRegex" -ForegroundColor red - } - } - } - - if (-Not $validationSuccessful) - { - Write-Error "Error(s) found in package version." - } -} - diff --git a/.github/workflows/Get-Package-Version.ps1 b/.github/workflows/Get-Package-Version.ps1 deleted file mode 100644 index 261bfbf..0000000 --- a/.github/workflows/Get-Package-Version.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -$debugMode = $env:SYSTEM_DEBUG - -# This function finds the version of a given package that is rerference from a given packages.config file -function Get-Package-Version ([string] $PackageFilePath, [string] $PackageName) { - if($debugMode -eq $true) { - Write-Host "Get-Package-Version Searching package file '$PackageFilePath' for package '$PackageName'" - } - - $packageNode = (Select-Xml -Path $PackageFilePath -XPath "//package[@id='$PackageName']" | Select-Object -ExpandProperty "node") - - if($packageNode -eq $null) { - if($debugMode -eq $true) { - Write-Host "Get-Package-Version Package reference not found" - } - - return $null - } - - $result = $packageNode | Select-Object -ExpandProperty "version" - - if($debugMode -eq $true) { - Write-Host "Get-Package-Version Found version $result" - } - return $result -} diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index bb132ba..d4a3cb5 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,9 +2,9 @@ name: Build and Test on: push: - branches: [main] + branches: [master] pull_request: - branches: [main] + branches: [master] jobs: build: @@ -20,8 +20,8 @@ jobs: - name: Restore projects run: dotnet restore projects/csharp/SqlRace.Examples.sln env: - NUGET_AUTH_USERNAME: github-actions - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUGET_AUTH_USERNAME: ${{ secrets.PACKAGES_USERNAME }} + NUGET_AUTH_TOKEN: ${{ secrets.PACKAGES_PATTOKEN }} - name: Build projects run: dotnet build projects/csharp/SqlRace.Examples.sln --no-restore --configuration Release @@ -29,8 +29,8 @@ jobs: - name: Restore tests run: dotnet restore tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj env: - NUGET_AUTH_USERNAME: github-actions - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUGET_AUTH_USERNAME: ${{ secrets.PACKAGES_USERNAME }} + NUGET_AUTH_TOKEN: ${{ secrets.PACKAGES_PATTOKEN }} - name: Build tests run: dotnet build tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj --no-restore --configuration Release diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index aba726e..0000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: PR gated build -on: - pull_request: - branches: [ master ] - schedule: - - cron: '0 12 * * *' - -jobs: - build: - runs-on: windows-latest - env: - Solution_Name: MAT.OCS.SQLRace.Examples.sln - PackagesUserName: ${{ secrets.PACKAGES_USERNAME }} - PackagesPatToken: ${{ secrets.PACKAGES_PATTOKEN }} - - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Setup NuGet.exe - uses: NuGet/setup-nuget@v1.0.5 - - - name: Check latest available package - shell: pwsh - run: | - nuget sources Add -Name "McLaren Applied GitHub Packages" -Source https://nuget.pkg.github.com/mat-docs/index.json -UserName $env:PackagesUserName -Password $env:PackagesPatToken - $Uri = "https://api.github.com/orgs/mat-docs/packages/nuget/MESL.SQLRace.API/versions" - $Headers = @{Authorization = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$env:PackagesUserName:$env:PackagesPatToken")) } - $Method = "GET" - $packageVersions = Invoke-RestMethod -Uri $Uri -Headers $Headers -Method $Method - $sortedPackages = $packageVersions.name | Where { $_ -match "^[-]?[0-9.]+$" } | %{[System.Version]$_} | sort -Descending | foreach-object { $_.ToString() } - echo "Package versions found:" $sortedPackages - $latestVersion = $sortedPackages[0] - echo "Latest Version: $latestVersion" - . ./.github/workflows/Check-References.ps1 - Check-References -Rootdirectory $Env:GITHUB_WORKSPACE -PackageName "MESL.SQLRace.API" -VersionMustContain $latestVersion - - - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '8.0.x' - - - name: Install dependencies - run: dotnet restore --verbosity normal - - - name: Build - run: dotnet build --configuration Release --no-restore diff --git a/.github/workflows/validate-notebooks.yml b/.github/workflows/validate-notebooks.yml index 5ddf059..72f989d 100644 --- a/.github/workflows/validate-notebooks.yml +++ b/.github/workflows/validate-notebooks.yml @@ -2,10 +2,10 @@ name: Validate Notebooks on: push: - branches: [main] + branches: [master] paths: ['notebooks/**'] pull_request: - branches: [main] + branches: [master] paths: ['notebooks/**'] jobs: diff --git a/notebooks/README.md b/notebooks/README.md index e9ceb1c..9ed18d7 100644 --- a/notebooks/README.md +++ b/notebooks/README.md @@ -1,3 +1,5 @@ +ATLAS + # Notebooks (Tier 3) Interactive Jupyter notebooks for exploring SQL Race session data with narrative, code, and visualisation. diff --git a/projects/README.md b/projects/README.md index 4db01cd..230c0ee 100644 --- a/projects/README.md +++ b/projects/README.md @@ -1,3 +1,5 @@ +ATLAS + # Projects (Tier 2) Complete, buildable solutions demonstrating production-quality patterns with the SQL Race API. diff --git a/readme.md b/readme.md index 3b90cd3..0ed8437 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ +ATLAS + # SQL Race API Examples Examples for the [SQL Race API](https://github.com/mat-docs/packages) — a .NET library for reading and writing high-frequency time-series data stored in SQLite (`.ssn2`) and SQL Server databases. diff --git a/snippets/README.md b/snippets/README.md index 2f672e2..56c4543 100644 --- a/snippets/README.md +++ b/snippets/README.md @@ -1,3 +1,5 @@ +ATLAS + # Snippets (Tier 1) Single-file, self-contained examples you can copy-paste into your own project. From f2a00a1098a6a02e50c772542d299e1c37e95cf6 Mon Sep 17 00:00:00 2001 From: Federica Donald Date: Wed, 10 Jun 2026 12:13:28 +0000 Subject: [PATCH 3/7] forgot tests can't run here --- .github/workflows/build-and-test.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d4a3cb5..0574549 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,4 +1,4 @@ -name: Build and Test +name: Build on: push: @@ -35,5 +35,7 @@ jobs: - name: Build tests run: dotnet build tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj --no-restore --configuration Release - - name: Run tests - run: dotnet test tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj --no-build --configuration Release --verbosity normal + # NOTE: Tests are NOT executed in CI. Several tests initialise the SQL Race + # runtime via SqlRaceBootstrapper, which requires a SQL Race licence that the + # CI runner does not have (LicenceException). CI verifies compilation only; + # run `dotnet test` locally on a licenced machine to execute the test suite. From ba2dc186baacaff6831b4e37d5e29eae81063982 Mon Sep 17 00:00:00 2001 From: Carles Abella <85241184+CarlesAB@users.noreply.github.com> Date: Fri, 12 Jun 2026 18:16:06 +0100 Subject: [PATCH 4/7] Add and expand MATLAB SQL Race examples (#61) Expand the MATLAB snippet tier to mirror the C# categories: - getting-started: create_session_write_data, list_parameters, load_session_from_database - data-access: read_samples_between, subsampled_read, reverse_iteration, data_status_filtering, plot_parameters - session-management: session_metadata, marker_management, lap_segment_management, event_extraction - configuration: introspect_session_config, parameter_unit_resolution - functions: read_function_output - live-data: poll_live_samples, server_listener Align query_sessions with the bundled scenario data and update the MATLAB README, COOKBOOK, and top-level README indexes. --- COOKBOOK.md | 67 ++++----- readme.md | 8 ++ snippets/matlab/README.md | 65 ++++++++- .../configuration/introspect_session_config.m | 101 +++++++++++++ .../configuration/parameter_unit_resolution.m | 91 ++++++++++++ .../data-access/data_status_filtering.m | 102 +++++++++++++ snippets/matlab/data-access/plot_parameters.m | 114 +++++++++++++++ .../matlab/data-access/read_samples_between.m | 91 ++++++++++++ .../matlab/data-access/reverse_iteration.m | 88 ++++++++++++ snippets/matlab/data-access/subsampled_read.m | 104 ++++++++++++++ .../matlab/functions/read_function_output.m | 101 +++++++++++++ .../create_session_write_data.m | 135 ++++++++++++++++++ .../matlab/getting-started/list_parameters.m | 132 +++++++++++++++++ .../load_session_from_database.m | 100 +++++++++++++ .../matlab/getting-started/query_sessions.m | 6 +- snippets/matlab/live-data/poll_live_samples.m | 93 ++++++++++++ snippets/matlab/live-data/server_listener.m | 113 +++++++++++++++ .../session-management/event_extraction.m | 109 ++++++++++++++ .../lap_segment_management.m | 75 ++++++++++ .../session-management/marker_management.m | 81 +++++++++++ .../session-management/session_metadata.m | 76 ++++++++++ 21 files changed, 1816 insertions(+), 36 deletions(-) create mode 100644 snippets/matlab/configuration/introspect_session_config.m create mode 100644 snippets/matlab/configuration/parameter_unit_resolution.m create mode 100644 snippets/matlab/data-access/data_status_filtering.m create mode 100644 snippets/matlab/data-access/plot_parameters.m create mode 100644 snippets/matlab/data-access/read_samples_between.m create mode 100644 snippets/matlab/data-access/reverse_iteration.m create mode 100644 snippets/matlab/data-access/subsampled_read.m create mode 100644 snippets/matlab/functions/read_function_output.m create mode 100644 snippets/matlab/getting-started/create_session_write_data.m create mode 100644 snippets/matlab/getting-started/list_parameters.m create mode 100644 snippets/matlab/getting-started/load_session_from_database.m create mode 100644 snippets/matlab/live-data/poll_live_samples.m create mode 100644 snippets/matlab/live-data/server_listener.m create mode 100644 snippets/matlab/session-management/event_extraction.m create mode 100644 snippets/matlab/session-management/lap_segment_management.m create mode 100644 snippets/matlab/session-management/marker_management.m create mode 100644 snippets/matlab/session-management/session_metadata.m diff --git a/COOKBOOK.md b/COOKBOOK.md index d10d4c5..5cc6f4d 100644 --- a/COOKBOOK.md +++ b/COOKBOOK.md @@ -7,55 +7,58 @@ A task-based index: find the example you need by what you want to accomplish. | How do I... | C# | Python | MATLAB | |------------|-----|--------|--------| | Load a session from a `.ssn2` file? | [`01-load-session-from-file.cs`](snippets/csharp/getting-started/01-load-session-from-file.cs) | [`01_load_session.py`](snippets/python/getting-started/01_load_session.py) | [`load_session.m`](snippets/matlab/getting-started/load_session.m) | -| Load a session from a database? | [`02-load-session-from-database.cs`](snippets/csharp/getting-started/02-load-session-from-database.cs) | [`01_load_session.py`](snippets/python/getting-started/01_load_session.py) | [`load_session.m`](snippets/matlab/getting-started/load_session.m) | +| Load a session from a database? | [`02-load-session-from-database.cs`](snippets/csharp/getting-started/02-load-session-from-database.cs) | [`01_load_session.py`](snippets/python/getting-started/01_load_session.py) | [`load_session_from_database.m`](snippets/matlab/getting-started/load_session_from_database.m) | | Read parameter samples? | [`03-read-parameter-samples.cs`](snippets/csharp/getting-started/03-read-parameter-samples.cs) | [`02_read_parameters.py`](snippets/python/getting-started/02_read_parameters.py) | [`read_parameters.m`](snippets/matlab/getting-started/read_parameters.m) | -| Create a session and write data? | [`04-create-session-write-data.cs`](snippets/csharp/getting-started/04-create-session-write-data.cs) | — | — | +| Create a session and write data? | [`04-create-session-write-data.cs`](snippets/csharp/getting-started/04-create-session-write-data.cs) | — | [`create_session_write_data.m`](snippets/matlab/getting-started/create_session_write_data.m) | | Find sessions by metadata? | [`05-query-sessions-by-metadata.cs`](snippets/csharp/getting-started/05-query-sessions-by-metadata.cs) | [`03_query_sessions.py`](snippets/python/getting-started/03_query_sessions.py) | [`query_sessions.m`](snippets/matlab/getting-started/query_sessions.m) | | Verify MATLAB + SQL Race setup? | — | — | [`diagnose_setup.m`](snippets/matlab/getting-started/diagnose_setup.m) | +| List all parameters in a session? | — | — | [`list_parameters.m`](snippets/matlab/getting-started/list_parameters.m) | ## Data Access | How do I... | C# | Python | MATLAB | |------------|-----|--------|--------| -| Read samples in a time range? | [`read-samples-between.cs`](snippets/csharp/data-access/read-samples-between.cs) | [`read_samples_to_list.py`](snippets/python/data-access/read_samples_to_list.py) | [`extract_to_timetable.m`](snippets/matlab/data-access/extract_to_timetable.m) | -| Downsample / subsample data? | [`read-subsampled-data.cs`](snippets/csharp/data-access/read-subsampled-data.cs) | — | — | -| Read data backwards (reverse iteration)? | [`reverse-iteration.cs`](snippets/csharp/data-access/reverse-iteration.cs) | — | — | +| Read samples in a time range? | [`read-samples-between.cs`](snippets/csharp/data-access/read-samples-between.cs) | [`read_samples_to_list.py`](snippets/python/data-access/read_samples_to_list.py) | [`read_samples_between.m`](snippets/matlab/data-access/read_samples_between.m) | +| Downsample / subsample data? | [`read-subsampled-data.cs`](snippets/csharp/data-access/read-subsampled-data.cs) | — | [`subsampled_read.m`](snippets/matlab/data-access/subsampled_read.m) | +| Read data backwards (reverse iteration)? | [`reverse-iteration.cs`](snippets/csharp/data-access/reverse-iteration.cs) | — | [`reverse_iteration.m`](snippets/matlab/data-access/reverse_iteration.m) | | Align parameters at different rates? | [`multi-rate-alignment.cs`](snippets/csharp/data-access/multi-rate-alignment.cs) | — | — | | Read many parameters efficiently? | [`bulk-parameter-read.cs`](snippets/csharp/data-access/bulk-parameter-read.cs) | [`multi_parameter_extract.py`](snippets/python/data-access/multi_parameter_extract.py) | [`multi_channel_read.m`](snippets/matlab/data-access/multi_channel_read.m) | -| Handle missing or invalid data? | [`data-status-filtering.cs`](snippets/csharp/data-access/data-status-filtering.cs) | — | — | +| Handle missing or invalid data? | [`data-status-filtering.cs`](snippets/csharp/data-access/data-status-filtering.cs) | — | [`data_status_filtering.m`](snippets/matlab/data-access/data_status_filtering.m) | | Build timestamp arrays for GetData? | [`timestamp-array-construction.cs`](snippets/csharp/data-access/timestamp-array-construction.cs) | — | — | | Export data to pandas? | — | [`export_to_pandas.py`](snippets/python/data-access/export_to_pandas.py) | — | | Export data to MATLAB timetable? | — | — | [`extract_to_timetable.m`](snippets/matlab/data-access/extract_to_timetable.m) | | Get per-segment statistics? | — | — | [`segment_statistics.m`](snippets/matlab/data-access/segment_statistics.m) | +| Plot a set of parameters? | — | — | [`plot_parameters.m`](snippets/matlab/data-access/plot_parameters.m) | ## Session Management -| How do I... | C# | Python | -|------------|-----|--------| -| Find sessions in a date range? | [`query-with-composite-filter.cs`](snippets/csharp/session-management/query-with-composite-filter.cs) | [`03_query_sessions.py`](snippets/python/getting-started/03_query_sessions.py) | -| Read and write session metadata? | [`session-metadata-crud.cs`](snippets/csharp/session-management/session-metadata-crud.cs) | — | -| Add annotations to a session? | [`marker-management.cs`](snippets/csharp/session-management/marker-management.cs) | — | -| Work with laps and segments? | [`lap-and-segment-management.cs`](snippets/csharp/session-management/lap-and-segment-management.cs) | — | -| Define and read events? | [`event-definitions-and-data.cs`](snippets/csharp/session-management/event-definitions-and-data.cs) | [`04_extract_events.py`](snippets/python/getting-started/04_extract_events.py) | -| Associate sessions together? | [`session-association.cs`](snippets/csharp/session-management/session-association.cs) | — | -| Resolve parameter units? | [`parameter-unit-resolution.cs`](snippets/csharp/session-management/parameter-unit-resolution.cs) | — | +| How do I... | C# | Python | MATLAB | +|------------|-----|--------|--------| +| Find sessions in a date range? | [`query-with-composite-filter.cs`](snippets/csharp/session-management/query-with-composite-filter.cs) | [`03_query_sessions.py`](snippets/python/getting-started/03_query_sessions.py) | [`query_sessions.m`](snippets/matlab/getting-started/query_sessions.m) | +| Read and write session metadata? | [`session-metadata-crud.cs`](snippets/csharp/session-management/session-metadata-crud.cs) | — | [`session_metadata.m`](snippets/matlab/session-management/session_metadata.m) | +| Add annotations to a session? | [`marker-management.cs`](snippets/csharp/session-management/marker-management.cs) | — | [`marker_management.m`](snippets/matlab/session-management/marker_management.m) | +| Work with laps and segments? | [`lap-and-segment-management.cs`](snippets/csharp/session-management/lap-and-segment-management.cs) | — | [`lap_segment_management.m`](snippets/matlab/session-management/lap_segment_management.m) | +| Define and read events? | [`event-definitions-and-data.cs`](snippets/csharp/session-management/event-definitions-and-data.cs) | [`04_extract_events.py`](snippets/python/getting-started/04_extract_events.py) | [`event_extraction.m`](snippets/matlab/session-management/event_extraction.m) | +| Associate sessions together? | [`session-association.cs`](snippets/csharp/session-management/session-association.cs) | — | — | +| Resolve parameter units? | [`parameter-unit-resolution.cs`](snippets/csharp/session-management/parameter-unit-resolution.cs) | — | [`parameter_unit_resolution.m`](snippets/matlab/configuration/parameter_unit_resolution.m) | ## Live Data -| How do I... | C# | -|------------|-----| -| React to new laps? | [`subscribe-to-lap-events.cs`](snippets/csharp/live-data/subscribe-to-lap-events.cs) | -| React to new data arriving? | [`subscribe-to-data-events.cs`](snippets/csharp/live-data/subscribe-to-data-events.cs) | -| Poll for the latest samples? | [`poll-live-samples.cs`](snippets/csharp/live-data/poll-live-samples.cs) | -| Detect when new parameters appear? | [`rda-parameter-change-detection.cs`](snippets/csharp/live-data/rda-parameter-change-detection.cs) | +| How do I... | C# | MATLAB | +|------------|-----|--------| +| React to new laps? | [`subscribe-to-lap-events.cs`](snippets/csharp/live-data/subscribe-to-lap-events.cs) | — | +| React to new data arriving? | [`subscribe-to-data-events.cs`](snippets/csharp/live-data/subscribe-to-data-events.cs) | [`server_listener.m`](snippets/matlab/live-data/server_listener.m) | +| Poll for the latest samples? | [`poll-live-samples.cs`](snippets/csharp/live-data/poll-live-samples.cs) | [`poll_live_samples.m`](snippets/matlab/live-data/poll_live_samples.m) | +| Detect when new parameters appear? | [`rda-parameter-change-detection.cs`](snippets/csharp/live-data/rda-parameter-change-detection.cs) | — | ## Functions (Calculated Channels) -| How do I... | C# | -|------------|-----| -| Create a calculated channel with FDL? | [`fdl-function-basic.cs`](snippets/csharp/functions/fdl-function-basic.cs) | -| Create a calculated channel with .NET? | [`dotnet-function-basic.cs`](snippets/csharp/functions/dotnet-function-basic.cs) | -| Use a PDA inside a .NET function? | [`dotnet-function-with-pda.cs`](snippets/csharp/functions/dotnet-function-with-pda.cs) | +| How do I... | C# | MATLAB | +|------------|-----|--------| +| Create a calculated channel with FDL? | [`fdl-function-basic.cs`](snippets/csharp/functions/fdl-function-basic.cs) | — | +| Create a calculated channel with .NET? | [`dotnet-function-basic.cs`](snippets/csharp/functions/dotnet-function-basic.cs) | — | +| Use a PDA inside a .NET function? | [`dotnet-function-with-pda.cs`](snippets/csharp/functions/dotnet-function-with-pda.cs) | — | +| Read calculated-channel (function) output? | — | [`read_function_output.m`](snippets/matlab/functions/read_function_output.m) | ## Composite Sessions @@ -67,12 +70,12 @@ A task-based index: find the example you need by what you want to accomplish. ## Configuration -| How do I... | C# | -|------------|-----| -| Set up parameter configuration manually? | [`create-parameter-config.cs`](snippets/csharp/configuration/create-parameter-config.cs) | -| Create in-memory-only parameters? | [`create-transient-parameter.cs`](snippets/csharp/configuration/create-transient-parameter.cs) | -| Inspect a session's configuration? | [`introspect-session-config.cs`](snippets/csharp/configuration/introspect-session-config.cs) | -| Use different conversion types? | [`conversion-types.cs`](snippets/csharp/configuration/conversion-types.cs) | +| How do I... | C# | MATLAB | +|------------|-----|--------| +| Set up parameter configuration manually? | [`create-parameter-config.cs`](snippets/csharp/configuration/create-parameter-config.cs) | [`create_session_write_data.m`](snippets/matlab/getting-started/create_session_write_data.m) | +| Create in-memory-only parameters? | [`create-transient-parameter.cs`](snippets/csharp/configuration/create-transient-parameter.cs) | — | +| Inspect a session's configuration? | [`introspect-session-config.cs`](snippets/csharp/configuration/introspect-session-config.cs) | [`introspect_session_config.m`](snippets/matlab/configuration/introspect_session_config.m) | +| Use different conversion types? | [`conversion-types.cs`](snippets/csharp/configuration/conversion-types.cs) | — | ## Projects (Tier 2) diff --git a/readme.md b/readme.md index 0ed8437..90fb346 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,14 @@ dotnet run | Build timestamp arrays | C# | [`timestamp-array-construction.cs`](snippets/csharp/data-access/timestamp-array-construction.cs) | | Load a session | Python | [`01_load_session.py`](snippets/python/getting-started/01_load_session.py) | | Load a session | MATLAB | [`load_session.m`](snippets/matlab/getting-started/load_session.m) | +| Connect to a SQL Server database | MATLAB | [`load_session_from_database.m`](snippets/matlab/getting-started/load_session_from_database.m) | +| List all parameters in a session | MATLAB | [`list_parameters.m`](snippets/matlab/getting-started/list_parameters.m) | +| Create a session and write data | MATLAB | [`create_session_write_data.m`](snippets/matlab/getting-started/create_session_write_data.m) | +| Read data in a time range | MATLAB | [`read_samples_between.m`](snippets/matlab/data-access/read_samples_between.m) | +| Plot a set of parameters | MATLAB | [`plot_parameters.m`](snippets/matlab/data-access/plot_parameters.m) | +| Work with markers, laps, events, metadata | MATLAB | [`session-management/`](snippets/matlab/session-management/) | +| Inspect session configuration and units | MATLAB | [`configuration/`](snippets/matlab/configuration/) | +| Poll the latest live samples | MATLAB | [`poll_live_samples.m`](snippets/matlab/live-data/poll_live_samples.m) | See [COOKBOOK.md](COOKBOOK.md) for a complete task-based index. diff --git a/snippets/matlab/README.md b/snippets/matlab/README.md index e8ad581..326d6d7 100644 --- a/snippets/matlab/README.md +++ b/snippets/matlab/README.md @@ -33,9 +33,70 @@ import MESL.SqlRace.Domain.*; | Wrong array types | Use `int64()` for timestamps, `double()` for values | | Memory issues with large datasets | Extract data in chunks; clear .NET objects when done | +## Connecting to a database + +| Backend | Connection string | +|---------|-------------------| +| SQLite (`.ssn2` file) | `DbEngine=SQLite;Data Source=.ssn2;` | +| SQL Server | `Data Source=;Initial Catalog=;Integrated Security=True;` | + +> **SQL Server has no `DbEngine=` prefix.** SQL Race passes SQL Server connection strings +> straight to `SqlClient`, so use a standard SQL Server string. Adding `DbEngine=SQLServer;` +> raises `Keyword not supported: 'dbengine'`. See +> [`getting-started/load_session_from_database.m`](getting-started/load_session_from_database.m). + +## Additional assemblies + +The getting-started and session-management examples that **create** sessions (rather than +just read them) need two more assemblies alongside `MESL.SqlRace.Domain.dll`: + +```matlab +NET.addAssembly(fullfile(installDir, 'MAT.OCS.Core.dll')); % SessionKey +NET.addAssembly(fullfile(installDir, 'MESL.SqlRace.Enumerators.dll')); % DataType, ChannelDataSourceType, ... +``` + +Each snippet loads exactly what it needs in its setup block. + ## Folders | Folder | Description | |--------|-------------| -| [getting-started/](getting-started/) | Load sessions, read parameters, query metadata, verify setup | -| [data-access/](data-access/) | Extract data to MATLAB timetables and arrays, per-segment statistics | +| [getting-started/](getting-started/) | Load sessions, read parameters, query metadata, verify setup, create a session and write data | +| [data-access/](data-access/) | Read samples in a time range, subsample, reverse iteration, status filtering, timetables, per-segment statistics | +| [session-management/](session-management/) | Session metadata, markers, laps/segments, event extraction | +| [configuration/](configuration/) | Introspect session configuration, resolve parameter units | +| [functions/](functions/) | Read calculated-channel (function) output | +| [live-data/](live-data/) | Poll the latest samples; server-listener live recording | + +## Snippets + +| File | Demonstrates | +|------|-------------| +| [getting-started/load_session.m](getting-started/load_session.m) | Load a session from SQLite | +| [getting-started/load_session_from_database.m](getting-started/load_session_from_database.m) | Connect to a SQL Server SQL Race database and load a session | +| [getting-started/read_parameters.m](getting-started/read_parameters.m) | Read samples into MATLAB arrays | +| [getting-started/list_parameters.m](getting-started/list_parameters.m) | Catalogue every parameter (metadata + sample counts) and export to CSV | +| [getting-started/query_sessions.m](getting-started/query_sessions.m) | Query sessions with a `ScalarFilter` | +| [getting-started/create_session_write_data.m](getting-started/create_session_write_data.m) | Create a session with multi-rate parameters and write/read data | +| [getting-started/diagnose_setup.m](getting-started/diagnose_setup.m) | Step-by-step setup verification | +| [data-access/read_samples_between.m](data-access/read_samples_between.m) | `GetSamplesBetween` over a time range | +| [data-access/subsampled_read.m](data-access/subsampled_read.m) | `GetData` with a custom timestamp array | +| [data-access/reverse_iteration.m](data-access/reverse_iteration.m) | `GoTo` + `GetNextSamples(StepDirection.Reverse)` | +| [data-access/data_status_filtering.m](data-access/data_status_filtering.m) | Filter samples by `DataStatus` | +| [data-access/extract_to_timetable.m](data-access/extract_to_timetable.m) | Build a MATLAB timetable | +| [data-access/multi_channel_read.m](data-access/multi_channel_read.m) | Read multiple parameters together | +| [data-access/segment_statistics.m](data-access/segment_statistics.m) | Per-segment `GetLapStatistics` | +| [data-access/plot_parameters.m](data-access/plot_parameters.m) | Extract a set of parameters and plot them on a shared time axis | +| [session-management/session_metadata.m](session-management/session_metadata.m) | Write/read `SessionDataItem` metadata | +| [session-management/marker_management.m](session-management/marker_management.m) | Create and read `Marker` annotations | +| [session-management/lap_segment_management.m](session-management/lap_segment_management.m) | Create `Lap` segments | +| [session-management/event_extraction.m](session-management/event_extraction.m) | Enumerate event definitions and read events per segment | +| [configuration/introspect_session_config.m](configuration/introspect_session_config.m) | Enumerate parameters, groups, conversions | +| [configuration/parameter_unit_resolution.m](configuration/parameter_unit_resolution.m) | Resolve units via `Session.GetConversion` | +| [functions/read_function_output.m](functions/read_function_output.m) | Read calculated-channel (`:Functions`) output | +| [live-data/poll_live_samples.m](live-data/poll_live_samples.m) | Poll the most recent samples | +| [live-data/server_listener.m](live-data/server_listener.m) | Discover live sessions and follow live data (requires a live server) | + +> **Live data:** `server_listener.m` requires a reachable SQL Server recorder with a live +> session and cannot run against the bundled SQLite scenario data. `poll_live_samples.m` +> demonstrates the polling pattern and runs against any session. diff --git a/snippets/matlab/configuration/introspect_session_config.m b/snippets/matlab/configuration/introspect_session_config.m new file mode 100644 index 0000000..125bf4f --- /dev/null +++ b/snippets/matlab/configuration/introspect_session_config.m @@ -0,0 +1,101 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Introspect Session Configuration +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Enumerating a session's parameters, conversions, and groups +% to understand its configuration +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: Summary tables of parameters, application groups, parameter groups +% +% Related: snippets/matlab/configuration/parameter_unit_resolution.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the turbine scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'turbine-week1.ssn2'); + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + params = session.Parameters; + nParams = params.Count; + + fprintf('Session: %s (%s)\n\n', char(session.Identifier), char(sessionKey.ToString())); + + % --- Parameters --- + fprintf('=== Parameters (%d) ===\n', nParams); + fprintf('%-30s %-25s %-15s\n', 'Identifier', 'Conversion', 'AppGroup'); + fprintf('%s\n', repmat('-', 1, 72)); + + appGroups = strings(0); + paramGroups = strings(0); + for i = 1:nParams + p = params.Item(i - 1); + fprintf('%-30s %-25s %-15s\n', char(p.Identifier), ... + char(p.ConversionFunctionName), char(p.ApplicationName)); + appGroups(end+1) = string(char(p.ApplicationName)); %#ok + g = p.GroupIdentifiers; + if ~isempty(g) + for j = 1:g.Count + paramGroups(end+1) = string(char(g.Item(j - 1))); %#ok + end + end + end + + % --- Application groups --- + fprintf('\n=== Application Groups ===\n'); + for g = unique(appGroups)' + fprintf(' %s\n', g); + end + + % --- Parameter groups --- + fprintf('\n=== Parameter Groups ===\n'); + if isempty(paramGroups) + fprintf(' (none)\n'); + else + for g = unique(paramGroups)' + fprintf(' %s\n', g); + end + end +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/configuration/parameter_unit_resolution.m b/snippets/matlab/configuration/parameter_unit_resolution.m new file mode 100644 index 0000000..cf0f8aa --- /dev/null +++ b/snippets/matlab/configuration/parameter_unit_resolution.m @@ -0,0 +1,91 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Parameter Unit Resolution +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Resolving the engineering unit for each parameter via its +% conversion (Session.GetConversion) +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: Parameter details with resolved units, format, and range +% +% Related: snippets/matlab/configuration/introspect_session_config.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the turbine scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'turbine-week1.ssn2'); + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + params = session.Parameters; + + fprintf('Session: %s (%d parameters)\n\n', char(session.Identifier), params.Count); + fprintf('%-30s %-25s %-10s %-10s %s\n', ... + 'Identifier', 'Conversion', 'Unit', 'Format', 'Range'); + fprintf('%s\n', repmat('-', 1, 95)); + + for i = 1:params.Count + p = params.Item(i - 1); + conversionName = char(p.ConversionFunctionName); + unit = '(unknown)'; + fmt = ''; + + % --- Resolve unit from the conversion --- + try + conversion = session.GetConversion(conversionName); + if ~isempty(conversion) + u = conversion.Units; + if ~isempty(u); unit = char(u); else; unit = '(none)'; end + f = conversion.FormatString; + if ~isempty(f); fmt = char(f); end + end + catch + unit = '(error)'; + end + + fprintf('%-30s %-25s %-10s %-10s [%.1f, %.1f]\n', ... + char(p.Identifier), conversionName, unit, fmt, ... + p.MinimumValue, p.MaximumValue); + end +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/data-access/data_status_filtering.m b/snippets/matlab/data-access/data_status_filtering.m new file mode 100644 index 0000000..958a98b --- /dev/null +++ b/snippets/matlab/data-access/data_status_filtering.m @@ -0,0 +1,102 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Data Status Filtering +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Checking DataStatus on returned samples to identify and +% handle missing or invalid data points +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: Per-status breakdown and filtered valid-only values +% +% Related: snippets/matlab/data-access/read_samples_between.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the motorsport scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'motorsport-lap-analysis.ssn2'); +paramId = 'Speed:Shaft'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + pda = session.CreateParameterDataAccess(paramId); + try + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + n = samples.SampleCount; + + % --- Count samples by status --- + % DataStatus values are .NET enums; convert to char for tallying. + statusNames = strings(n, 1); + for i = 1:n + statusNames(i) = string(char(samples.DataStatus(i).ToString())); + end + [uniqueStatuses, ~, idx] = unique(statusNames); + counts = accumarray(idx, 1); + + fprintf('Parameter: %s\n', paramId); + fprintf('Total samples: %d\n\n', n); + fprintf('Status breakdown:\n'); + for s = 1:numel(uniqueStatuses) + pct = 100 * counts(s) / n; + fprintf(' %-15s %6d (%.1f%%)\n', uniqueStatuses(s), counts(s), pct); + end + + % --- Filter to valid samples only (DataStatusType.Sample) --- + fprintf('\nValid samples (Sample status):\n'); + validMask = (statusNames == "Sample"); + validIdx = find(validMask); + for k = 1:min(numel(validIdx), 10) + i = validIdx(k); + ms = double(samples.Timestamp(i) - session.StartTime) / 1e6; + fprintf(' t+%8.3f ms %.4f\n', ms, samples.Data(i)); + end + fprintf('Total valid: %d / %d\n', numel(validIdx), n); + catch ex + fprintf('Error reading data: %s\n', ex.message); + end + pda.Dispose(); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/data-access/plot_parameters.m b/snippets/matlab/data-access/plot_parameters.m new file mode 100644 index 0000000..47f8476 --- /dev/null +++ b/snippets/matlab/data-access/plot_parameters.m @@ -0,0 +1,114 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Plot Multiple Parameters +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Extracting the time-series for a set of parameters and +% plotting them on a shared time axis (one stacked subplot per +% parameter, so different value ranges stay readable) +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) and a list +% of parameter identifiers to plot +% Output: A figure window with one stacked subplot per parameter +% +% Related: snippets/matlab/data-access/multi_channel_read.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the turbine scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'turbine-week1.ssn2'); + +% Parameters to plot (edit for your session; see list_parameters.m to discover them) +parameterIds = {'Pressure:Inlet', 'Temperature:Sensors', 'RPM:Turbine'}; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + startNs = session.StartTime; + + % --- Extract each parameter's time-series into MATLAB arrays --- + series = struct('id', {}, 't', {}, 'v', {}); + for k = 1:numel(parameterIds) + paramId = parameterIds{k}; + pda = session.CreateParameterDataAccess(paramId); + try + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + n = samples.SampleCount; + t = zeros(n, 1); + v = zeros(n, 1); + for i = 1:n + t(i) = double(samples.Timestamp(i) - startNs) / 1e9; % seconds + v(i) = samples.Data(i); + end + series(end+1) = struct('id', paramId, 't', t, 'v', v); %#ok + fprintf('%-25s %d samples\n', paramId, n); + catch ex + fprintf('%-25s skipped — %s\n', paramId, ex.message); + end + pda.Dispose(); + end + + if isempty(series) + error('No data extracted for the requested parameters.'); + end + + % --- Plot: one stacked subplot per parameter, shared x-axis --- + fig = figure('Position', [100, 100, 900, 250 * numel(series)]); + tl = tiledlayout(fig, numel(series), 1, 'TileSpacing', 'compact', 'Padding', 'compact'); + title(tl, sprintf('%s', char(session.Identifier)), 'Interpreter', 'none'); + + ax = gobjects(numel(series), 1); + for k = 1:numel(series) + ax(k) = nexttile(tl); + plot(ax(k), series(k).t, series(k).v, 'LineWidth', 1); + ylabel(ax(k), series(k).id, 'Interpreter', 'none'); + grid(ax(k), 'on'); + end + xlabel(ax(end), 'Time (s)'); + linkaxes(ax, 'x'); + + fprintf('\nDisplayed %d parameter(s).\n', numel(series)); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/data-access/read_samples_between.m b/snippets/matlab/data-access/read_samples_between.m new file mode 100644 index 0000000..b5d8086 --- /dev/null +++ b/snippets/matlab/data-access/read_samples_between.m @@ -0,0 +1,91 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Read Samples Between +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Using GetSamplesBetween to read data in a specific time range +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: Sample count and the first samples within the requested range +% +% Related: snippets/matlab/data-access/reverse_iteration.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the motorsport scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'motorsport-lap-analysis.ssn2'); +paramId = 'Temperature:Sensors'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + + % --- Define a time range: first 500 ms of data --- + rangeStart = session.StartTime; + rangeEnd = session.StartTime + int64(500e6); % 0.5 s in nanoseconds + + pda = session.CreateParameterDataAccess(paramId); + try + samples = pda.GetSamplesBetween(rangeStart, rangeEnd); + + fprintf('Parameter: %s\n', paramId); + fprintf('Range: %d - %d ns (%.3f s)\n', ... + rangeStart, rangeEnd, double(rangeEnd - rangeStart) / 1e9); + fprintf('Samples: %d\n', samples.SampleCount); + + nShow = min(samples.SampleCount, 10); + for i = 1:nShow + relMs = double(samples.Timestamp(i) - rangeStart) / 1e6; + fprintf(' t+%8.3f ms %.4f\n', relMs, samples.Data(i)); + end + if samples.SampleCount > 10 + fprintf(' ... and %d more\n', samples.SampleCount - 10); + end + catch ex + fprintf('Error reading data: %s\n', ex.message); + end + pda.Dispose(); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/data-access/reverse_iteration.m b/snippets/matlab/data-access/reverse_iteration.m new file mode 100644 index 0000000..8006f54 --- /dev/null +++ b/snippets/matlab/data-access/reverse_iteration.m @@ -0,0 +1,88 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Reverse Iteration +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Using GoTo and GetNextSamples with StepDirection.Reverse to +% read data backwards from the end of a session +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: Samples printed in reverse chronological order +% +% Related: snippets/matlab/data-access/read_samples_between.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; +import MESL.SqlRace.Domain.Infrastructure.Enumerators.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the flight-test scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'flight-test-manoeuvre-extraction.ssn2'); +paramId = 'Temperature:Sensors'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + pda = session.CreateParameterDataAccess(paramId); + try + % --- Position the PDA at the end of the session --- + pda.GoTo(session.EndTime); + + % --- Read 10 samples backwards --- + % StepDirection lives in MESL.SqlRace.Domain.Infrastructure.Enumerators + samples = pda.GetNextSamples(10, StepDirection.Reverse); + + fprintf('Parameter: %s\n', paramId); + fprintf('Last %d samples (reverse order):\n', samples.SampleCount); + fprintf('%20s %12s\n', 'Timestamp (ns)', 'Value'); + fprintf('%s\n', repmat('-', 1, 34)); + + for i = 1:samples.SampleCount + fprintf('%20d %12.4f\n', samples.Timestamp(i), samples.Data(i)); + end + catch ex + fprintf('Error reading data: %s\n', ex.message); + end + pda.Dispose(); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/data-access/subsampled_read.m b/snippets/matlab/data-access/subsampled_read.m new file mode 100644 index 0000000..ebad0c9 --- /dev/null +++ b/snippets/matlab/data-access/subsampled_read.m @@ -0,0 +1,104 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Subsampled Read +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Using GetData with a custom timestamp array to downsample +% high-frequency data to a lower rate +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: Values at the requested timestamps (every 100 ms = 10 Hz) +% +% Related: snippets/matlab/data-access/read_samples_between.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the turbine scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'turbine-week1.ssn2'); +paramId = 'Temperature:Sensors'; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + + % --- Build a custom timestamp array at 10 Hz (100 ms intervals) --- + desiredInterval = int64(100e6); % 100 ms in nanoseconds (= 10 Hz) + startNs = session.StartTime; + endNs = session.EndTime; + sampleCount = double(idivide(endNs - startNs, desiredInterval)); + + if sampleCount <= 0 + error('Session is too short for subsampling at 10 Hz.'); + end + + % Build a .NET int64 array of the desired timestamps + timestamps = NET.createArray('System.Int64', sampleCount); + for i = 1:sampleCount + timestamps(i) = startNs + int64(i - 1) * desiredInterval; + end + + pda = session.CreateParameterDataAccess(paramId); + try + % --- Read data at the custom timestamps --- + samples = pda.GetData(timestamps); + + fprintf('Parameter: %s\n', paramId); + fprintf('Original range: %d - %d ns\n', startNs, endNs); + fprintf('Subsampled to: %d points at 10 Hz\n\n', sampleCount); + + nShow = min(samples.SampleCount, 15); + for i = 1:nShow + relMs = double(samples.Timestamp(i) - startNs) / 1e6; + fprintf(' t+%8.1f ms %.4f\n', relMs, samples.Data(i)); + end + if samples.SampleCount > 15 + fprintf(' ... and %d more\n', samples.SampleCount - 15); + end + catch ex + fprintf('Error reading data: %s\n', ex.message); + end + pda.Dispose(); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/functions/read_function_output.m b/snippets/matlab/functions/read_function_output.m new file mode 100644 index 0000000..88be2c7 --- /dev/null +++ b/snippets/matlab/functions/read_function_output.m @@ -0,0 +1,101 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Read Function (Calculated Channel) Output +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Locating calculated-channel (function) parameters in the +% ":Functions" application group and reading their output the +% same way as any other parameter +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: List of function parameters and a sample of their output values +% +% Note: Function definitions are authored in ATLAS or via the .NET +% FunctionManager API. Once built, their output is read like any +% parameter — that is what this snippet shows. It reports gracefully +% if the session has no function parameters. +% +% Related: snippets/matlab/getting-started/read_parameters.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'motorsport-lap-analysis.ssn2'); + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + params = session.Parameters; + + % --- Find function parameters (calculated channels live in :Functions) --- + funcIds = strings(0); + for i = 1:params.Count + id = string(char(params.Item(i - 1).Identifier)); + if endsWith(id, ':Functions') + funcIds(end+1) = id; %#ok + end + end + + fprintf('Session: %s\n', char(session.Identifier)); + fprintf('Function parameters found: %d\n', numel(funcIds)); + + if isempty(funcIds) + fprintf(['\nNo function parameters in this session.\n' ... + 'Author a function in ATLAS (or via the .NET FunctionManager API),\n' ... + 'then re-run: its output appears as a ":Functions" parameter\n' ... + 'and is read exactly like the example below.\n']); + else + for k = 1:numel(funcIds) + paramId = char(funcIds(k)); + pda = session.CreateParameterDataAccess(paramId); + try + samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + fprintf('\n%s: %d samples\n', paramId, samples.SampleCount); + for i = 1:min(samples.SampleCount, 5) + fprintf(' %20d ns %.4f\n', samples.Timestamp(i), samples.Data(i)); + end + catch ex + fprintf(' Error reading %s: %s\n', paramId, ex.message); + end + pda.Dispose(); + end + end +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/getting-started/create_session_write_data.m b/snippets/matlab/getting-started/create_session_write_data.m new file mode 100644 index 0000000..2fd5b99 --- /dev/null +++ b/snippets/matlab/getting-started/create_session_write_data.m @@ -0,0 +1,135 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Create Session and Write Data +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Creating a SQLite session with multi-rate parameters, +% writing data, and reading it back to verify +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: None (creates its own .ssn2 file in the temp folder) +% Output: Session key and verified samples for Temperature (100 Hz) and +% Pressure (50 Hz) +% +% Related: snippets/matlab/getting-started/read_parameters.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +NET.addAssembly(fullfile(installDir, 'MAT.OCS.Core.dll')); +enumDll = fullfile(installDir, 'MESL.SqlRace.Enumerators.dll'); +if exist(enumDll, 'file') + NET.addAssembly(enumDll); +end +import MESL.SqlRace.Domain.*; +import MESL.SqlRace.Domain.Infrastructure.DataPipeline.*; +import MESL.SqlRace.Enumerators.*; +import MAT.OCS.Core.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection string (writable temp file) --- +dbPath = fullfile(tempdir, 'sqlrace-matlab-examples.ssn2'); +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); +sessionKey = SessionKey.NewKey(); + +% --- Create a new session --- +sessionManager = SessionManager.CreateSessionManager(); +clientSession = sessionManager.CreateSession(connectionString, sessionKey, ... + 'Example Session', System.DateTime.UtcNow, 'Example'); + +try + session = clientSession.Session; + + % --- Build parameter configuration --- + config = session.CreateConfiguration(); + tempChannelId = uint32(1); + pressChannelId = uint32(2); + tempInterval = FrequencyExtensions.ToInterval(Frequency(100, FrequencyUnit.Hz)); + pressInterval = FrequencyExtensions.ToInterval(Frequency(50, FrequencyUnit.Hz)); + + config.AddConversion(RationalConversion.CreateSimple1To1Conversion('degC_conv', 'degC', '%5.2f')); + config.AddConversion(RationalConversion.CreateSimple1To1Conversion('bar_conv', 'bar', '%5.3f')); + + config.AddChannel(Channel(tempChannelId, 'Temperature', tempInterval, ... + DataType.Double64Bit, ChannelDataSourceType.Periodic, 'Temperature:Sensors', false)); + config.AddChannel(Channel(pressChannelId, 'Pressure', pressInterval, ... + DataType.Double64Bit, ChannelDataSourceType.Periodic, 'Pressure:Inlet', false)); + + % Parameter groups expect a List of group identifiers + thermalGroups = NET.createGeneric('System.Collections.Generic.List', {'System.String'}); + thermalGroups.Add('Thermal'); + hydraulicGroups = NET.createGeneric('System.Collections.Generic.List', {'System.String'}); + hydraulicGroups.Add('Hydraulic'); + appGroupChildren = NET.createGeneric('System.Collections.Generic.List', {'System.String'}); + appGroupChildren.Add('Thermal'); + appGroupChildren.Add('Hydraulic'); + + config.AddParameterGroup(ParameterGroup('Thermal', 'Thermal')); + config.AddParameterGroup(ParameterGroup('Hydraulic', 'Hydraulic')); + appGroup = ApplicationGroup('Sensors', 'Sensors', appGroupChildren); + appGroup.SupportsRda = false; + config.AddGroup(appGroup); + + config.AddParameter(Parameter('Temperature:Sensors', 'Temperature', 'Inlet temperature sensor', ... + 500, 0, 500, 0, 0.0, uint32(0), uint32(0), 'degC_conv', thermalGroups, tempChannelId, 'Sensors')); + config.AddParameter(Parameter('Pressure:Inlet', 'Pressure', 'Inlet pressure sensor', ... + 10, 0, 10, 0, 0.0, uint32(0), uint32(0), 'bar_conv', hydraulicGroups, pressChannelId, 'Sensors')); + + config.Commit(); + + % --- Write 1 second of data --- + startTime = int64(36e12); % 10:00:00.000 + + for i = 0:99 % 100 Hz temperature + timestamp = startTime + int64(i) * tempInterval; + value = 20.0 + sin(i * 0.1) * 5.0; + session.AddChannelData(tempChannelId, timestamp, 1, System.BitConverter.GetBytes(value)); + end + + for i = 0:49 % 50 Hz pressure + timestamp = startTime + int64(i) * pressInterval; + value = 1.013 + sin(i * 0.05) * 0.2; + session.AddChannelData(pressChannelId, timestamp, 1, System.BitConverter.GetBytes(value)); + end + + % --- Read back and verify --- + tempPda = session.CreateParameterDataAccess('Temperature:Sensors'); + tempSamples = tempPda.GetSamplesBetween(startTime, startTime + 100 * tempInterval); + pressPda = session.CreateParameterDataAccess('Pressure:Inlet'); + pressSamples = pressPda.GetSamplesBetween(startTime, startTime + 50 * pressInterval); + + fprintf('Session created: %s\n', char(sessionKey.ToString())); + fprintf('Connection: %s\n\n', connectionString); + fprintf('Temperature:Sensors (100 Hz): %d samples\n', tempSamples.SampleCount); + fprintf(' First: %.2f degC Last: %.2f degC\n', ... + tempSamples.Data(1), tempSamples.Data(tempSamples.SampleCount)); + fprintf('Pressure:Inlet (50 Hz): %d samples\n', pressSamples.SampleCount); + fprintf(' First: %.3f bar Last: %.3f bar\n', ... + pressSamples.Data(1), pressSamples.Data(pressSamples.SampleCount)); + + tempPda.Dispose(); + pressPda.Dispose(); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/getting-started/list_parameters.m b/snippets/matlab/getting-started/list_parameters.m new file mode 100644 index 0000000..d85c3ef --- /dev/null +++ b/snippets/matlab/getting-started/list_parameters.m @@ -0,0 +1,132 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: List All Parameters +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Enumerating every parameter in a session and building a +% MATLAB table catalogue (identifier, name, group, conversion, +% range, sample count), then exporting it to CSV +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: A printed parameter catalogue and a CSV written to the temp folder +% +% Related: snippets/matlab/getting-started/read_parameters.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the turbine scenario data file bundled in the repo +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'turbine-week1.ssn2'); + +% Set to true to count samples per parameter (slower on large sessions) +countSamples = true; + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + params = session.Parameters; + nParams = params.Count; + + fprintf('Session: %s\n', char(session.Identifier)); + fprintf('Parameters: %d\n\n', nParams); + + % --- Pre-allocate columns for the catalogue table --- + Identifier = strings(nParams, 1); + Name = strings(nParams, 1); + Description = strings(nParams, 1); + AppGroup = strings(nParams, 1); + Groups = strings(nParams, 1); + Conversion = strings(nParams, 1); + MinValue = zeros(nParams, 1); + MaxValue = zeros(nParams, 1); + Samples = zeros(nParams, 1); + + for i = 1:nParams + p = params.Item(i - 1); + Identifier(i) = string(char(p.Identifier)); + Name(i) = string(char(p.Name)); + Description(i) = string(char(p.Description)); + AppGroup(i) = string(char(p.ApplicationName)); + Conversion(i) = string(char(p.ConversionFunctionName)); + MinValue(i) = p.MinimumValue; + MaxValue(i) = p.MaximumValue; + + % Join the parameter group identifiers + g = p.GroupIdentifiers; + if ~isempty(g) && g.Count > 0 + gnames = strings(1, g.Count); + for j = 1:g.Count + gnames(j) = string(char(g.Item(j - 1))); + end + Groups(i) = strjoin(gnames, '; '); + else + Groups(i) = ""; + end + + % Optionally count samples across the whole session + if countSamples + pda = session.CreateParameterDataAccess(char(p.Identifier)); + try + s = pda.GetSamplesBetween(session.StartTime, session.EndTime); + Samples(i) = double(s.SampleCount); + catch + Samples(i) = NaN; + end + pda.Dispose(); + else + Samples(i) = NaN; + end + end + + % --- Build and display the catalogue table --- + catalogue = table(Identifier, Name, AppGroup, Groups, Conversion, ... + MinValue, MaxValue, Samples); + disp(catalogue); + + % --- Export to CSV --- + csvPath = fullfile(tempdir, 'sqlrace-parameters.csv'); + writetable(catalogue, csvPath); + fprintf('Catalogue written to: %s\n', csvPath); +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/getting-started/load_session_from_database.m b/snippets/matlab/getting-started/load_session_from_database.m new file mode 100644 index 0000000..bfeaa2b --- /dev/null +++ b/snippets/matlab/getting-started/load_session_from_database.m @@ -0,0 +1,100 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Load Session from a SQL Server Database +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Connecting to a SQL Server SQL Race database (rather than a +% local SQLite .ssn2 file), listing sessions, and loading one +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL, +% AND access to a SQL Server instance hosting a SQL Race +% database (e.g. SQLRACE01). +% Run dotnetenv('core') once before using this snippet. +% Input: A SQL Server connection string (edit the variables below, or set +% the SQLRACE_CONNECTION_STRING environment variable) +% Output: A list of sessions and a summary of the most recent one +% +% Related: snippets/matlab/getting-started/load_session.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end + +% Pre-load Microsoft.Win32.SystemEvents — required when the runtime config +% does not include Microsoft.WindowsDesktop.App (see docs/troubleshooting.md) +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end + +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- SQL Server connection string --- +% Unlike SQLite (which uses a "DbEngine=SQLite;..." string), SQL Race passes +% SQL Server strings straight to SqlClient — so use a standard SQL Server +% connection string with NO DbEngine prefix. Integrated Security uses the +% current Windows account (Trusted_Connection=True has the same effect). +connectionString = getenv('SQLRACE_CONNECTION_STRING'); +if isempty(connectionString) + dataServer = 'SERVER_NAME'; % SQL Server instance + initialCatalog = 'DATABASE_NAME'; % SQL Race database + connectionString = sprintf(['Data Source=%s;Initial Catalog=%s;' ... + 'Integrated Security=True;'], dataServer, initialCatalog); +end + +fprintf('Connecting to: %s\n', connectionString); + +sessionManager = SessionManager.CreateSessionManager(); + +% --- List historical sessions on the server --- +try + sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +catch ex + fprintf('ERROR: could not query the database: %s\n', ex.message); + fprintf(['Tips: check the server name/instance, that the SQLRACE01 catalog\n' ... + ' exists, and that your Windows account has access.\n']); + return; +end + +fprintf('Found %d historical session(s).\n\n', sessions.Count); +if sessions.Count == 0 + disp('No sessions to load.'); + return; +end + +nShow = min(sessions.Count, 10); +fprintf('%-40s %-30s %s\n', 'Session Key', 'Identifier', 'Recorded'); +fprintf('%s\n', repmat('-', 1, 94)); +for i = 1:nShow + s = sessions.Item(i - 1); + fprintf('%-40s %-30s %s\n', ... + char(s.Key.ToString()), char(s.Identifier), char(s.TimeOfRecording.ToString())); +end + +% --- Load the most recent session and print a summary --- +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); +try + session = clientSession.Session; + fprintf('\nLoaded: %s\n', char(session.Identifier)); + fprintf(' Start time: %d ns\n', session.StartTime); + fprintf(' End time: %d ns\n', session.EndTime); + fprintf(' Duration: %.3f s\n', double(session.EndTime - session.StartTime) / 1e9); + fprintf(' Parameters: %d\n', session.Parameters.Count); + fprintf(' Laps: %d\n', session.LapCollection.Count); +catch ex + fprintf('Error reading session: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/getting-started/query_sessions.m b/snippets/matlab/getting-started/query_sessions.m index 330b744..78f39fa 100644 --- a/snippets/matlab/getting-started/query_sessions.m +++ b/snippets/matlab/getting-started/query_sessions.m @@ -50,7 +50,9 @@ % --- Create query manager with filter --- qm = QueryManager.CreateQueryManager(connectionString); -searchTerm = 'Example'; +% 'Test' matches the bundled scenario sessions (e.g. "Flight Test FT-042"). +% Change this to filter for your own session identifiers. +searchTerm = 'Test'; queryFilter = ScalarFilter('Identifier', MatchingRule.Contains, searchTerm, false); qm.Filter = queryFilter; @@ -86,7 +88,7 @@ end if count == 0 - disp('No sessions found. Run 04-create-session-write-data.cs to create some.'); + disp('No sessions found. Run create_session_write_data.m to create some.'); else fprintf('\n%d session(s) found.\n', count); end diff --git a/snippets/matlab/live-data/poll_live_samples.m b/snippets/matlab/live-data/poll_live_samples.m new file mode 100644 index 0000000..dcd6b74 --- /dev/null +++ b/snippets/matlab/live-data/poll_live_samples.m @@ -0,0 +1,93 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Poll Latest Samples +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Polling the most recent samples of a parameter by seeking to +% the end and reading backwards — the pattern used to monitor a +% live session +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data). For a live +% session, point the connection string at the live database/server. +% Output: The latest samples at each poll cycle +% +% Note: Against historical data the latest values do not change between +% polls. Against a live session, each reload picks up newly flushed data. +% +% Related: snippets/matlab/data-access/reverse_iteration.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/parameter-data-access/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; +import MESL.SqlRace.Domain.Infrastructure.Enumerators.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'motorsport-lap-analysis.ssn2'); +paramId = 'Speed:Shaft'; +nPolls = 3; % number of poll cycles +pollIntervalSec = 1; % delay between polls + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; + +fprintf('Polling %s every %ds (%d cycles)...\n\n', paramId, pollIntervalSec, nPolls); + +for pollNo = 1:nPolls + % --- Reload each cycle to pick up newly flushed data (live sessions) --- + clientSession = sessionManager.Load(sessionKey, connectionString); + try + session = clientSession.Session; + pda = session.CreateParameterDataAccess(paramId); + try + % Seek past the end, then read the most recent samples backwards + pda.GoTo(intmax('int64')); + samples = pda.GetNextSamples(5, StepDirection.Reverse); + if samples.SampleCount > 0 + fprintf(' Poll %d: latest=%.2f at %d ns (%d recent)\n', ... + pollNo, samples.Data(1), samples.Timestamp(1), samples.SampleCount); + else + fprintf(' Poll %d: no data yet\n', pollNo); + end + catch ex + fprintf(' Poll %d: error — %s\n', pollNo, ex.message); + end + pda.Dispose(); + catch ex + fprintf(' Poll %d: load error — %s\n', pollNo, ex.message); + end + clientSession.Dispose(); + + if pollNo < nPolls + pause(pollIntervalSec); + end +end + +fprintf('\nPolling complete — %d polls.\n', nPolls); diff --git a/snippets/matlab/live-data/server_listener.m b/snippets/matlab/live-data/server_listener.m new file mode 100644 index 0000000..d476c6c --- /dev/null +++ b/snippets/matlab/live-data/server_listener.m @@ -0,0 +1,113 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Server Listener (Live Recording) +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Configuring a SQL Race server listener, discovering live +% sessions on an ATLAS Data Server, and reading the latest data +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL, +% AND a reachable SQL Server recorder with a live session. +% Run dotnetenv('core') once before using this snippet. +% Input: Recorder connection details (edit the variables below) +% Output: Live session discovery and a short live-data read loop +% +% Note: This snippet REQUIRES a live ATLAS Data Server / SQL Server recorder. +% It cannot run against the bundled SQLite scenario data. Edit the +% connection details below for your environment before running. +% +% Related: snippets/matlab/live-data/poll_live_samples.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/live-data/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assemblies and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +NET.addAssembly(fullfile(installDir, 'MAT.OCS.Core.dll')); +import MESL.SqlRace.Domain.*; +import MESL.SqlRace.Domain.Infrastructure.Enumerators.*; +import MESL.SqlRace.Domain.Remoting.Server.*; +import MAT.OCS.Core.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Recorder / server configuration (edit for your environment) --- +recorderDbEngine = 'SQLServer'; +databaseName = 'SQLRACE01'; +recorderDataServer = 'MACHINE\LOCAL'; +serverListenerIp = '127.0.0.1'; +serverListenerPort = 6566; +parameterId = 'vCar:Chassis'; + +connStr = sprintf('server=%s;Initial Catalog=%s;Trusted_Connection=True;', ... + recorderDataServer, databaseName); + +% --- Configure the server listener endpoint --- +ipAddr = System.Net.IPAddress.Parse(serverListenerIp); +endPoint = System.Net.IPEndPoint(ipAddr, int32(serverListenerPort)); +Core.ConfigureServer(true, endPoint); + +recordersConfiguration = RecordersConfiguration.GetRecordersConfiguration(); +recordersConfiguration.AddConfiguration(System.Guid.NewGuid(), recorderDbEngine, ... + recorderDataServer, recorderDataServer, connStr, false); + +sessionManager = SessionManager.CreateSessionManager(); + +% --- Find live sessions --- +activeSessions = sessionManager.FindBySessionState(SessionState.Live, connStr); +fprintf('Found %d live session(s).\n', activeSessions.Count); + +if activeSessions.Count == 0 + fprintf('No live sessions. Start a recording on the server and re-run.\n'); + recordersConfiguration.RemoveConfiguration(); + return; +end + +% --- Load the most recent live session --- +summary = activeSessions.Item(activeSessions.Count - 1); +fprintf('Loading live session: %s\n', char(summary.Identifier)); +clientSession = sessionManager.Load(summary.Key, summary.GetConnectionString()); + +try + liveSession = clientSession.Session; + liveSession.LoadConfiguration(); + + if liveSession.Parameters.Count == 0 + fprintf('Session contains no parameters.\n'); + else + pda = liveSession.CreateParameterDataAccess(parameterId); + try + % Follow the live edge: read forward from the current end + pda.GoTo(liveSession.EndTime); + for iter = 1:10 + samples = pda.GetNextSamples(int32(50), StepDirection.Forward); + if samples.SampleCount > 0 + fprintf('Iter %d: %d new sample(s), latest=%.3f\n', ... + iter, samples.SampleCount, samples.Data(samples.SampleCount)); + else + fprintf('Iter %d: waiting for data...\n', iter); + end + pause(1); + end + catch ex + fprintf('Error reading live data: %s\n', ex.message); + end + pda.Dispose(); + end +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Close(); +recordersConfiguration.RemoveConfiguration(); +disp('Done.'); diff --git a/snippets/matlab/session-management/event_extraction.m b/snippets/matlab/session-management/event_extraction.m new file mode 100644 index 0000000..19e1b9d --- /dev/null +++ b/snippets/matlab/session-management/event_extraction.m @@ -0,0 +1,109 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Event Extraction +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Enumerating EventDefinitions and reading event instances +% per segment with GetEventData / GetDisplayText +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: A .ssn2 SQLite database file (defaults to scenario data) +% Output: Event definitions and a per-segment breakdown of event instances +% +% Note: Event *creation* happens in the recording/import pipeline. This +% snippet reads events; it reports zero events if the session has none. +% +% Related: snippets/matlab/session-management/marker_management.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assembly and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +import MESL.SqlRace.Domain.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection and session --- +% Default: use the motorsport scenario data file (multiple segments) +dataDir = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..', ... + 'notebooks', 'scenarios', 'data'); +dbPath = fullfile(dataDir, 'motorsport-lap-analysis.ssn2'); + +if ~exist(dbPath, 'file') + error('File not found: %s\nCheck that notebooks/scenarios/data/ contains the .ssn2 files.', dbPath); +end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); + +sessionManager = SessionManager.CreateSessionManager(); +sessions = sessionManager.FindBySessionState(SessionState.Historical, connectionString); +if sessions.Count == 0 + error('No sessions found in %s', dbPath); +end +sessionKey = sessions.Item(0).Key; +clientSession = sessionManager.Load(sessionKey, connectionString); + +try + session = clientSession.Session; + fprintf('Session: %s\n', char(session.Identifier)); + + % --- List event definitions (the catalogue of possible event types) --- + defs = session.EventDefinitions; + fprintf('Event definitions: %d\n', defs.Count); + for i = 1:defs.Count + def = defs.Item(i - 1); + fprintf(' [%s] %s (priority %s)\n', ... + char(def.EventDefinitionId), char(def.Description), char(def.Priority.ToString())); + end + + % --- Read event instances per segment --- + laps = session.LapCollection; + eventCollection = session.Events; + fprintf('\nReading events across %d segment(s):\n', laps.Count); + fprintf('%-18s %12s\n', 'Segment', 'Events'); + fprintf('%s\n', repmat('-', 1, 32)); + + totalEvents = 0; + for n = 1:laps.Count + lap = laps.Item(n - 1); + lapEnd = lap.EndTime; + if lapEnd.HasValue + endTs = lapEnd.Value; + else + endTs = session.EndTime; + end + events = eventCollection.GetEventData(lap.StartTime, endTs); + fprintf('%-18s %12d\n', char(lap.Name), events.Count); + totalEvents = totalEvents + double(events.Count); + + % Show the first few events in this segment, if any + for j = 1:min(events.Count, 5) + evt = events.Item(j - 1); + statusText = char(eventCollection.GetDisplayText(evt)); + fprintf(' t=%d ns def=%s %s\n', ... + evt.TimeStamp, char(string(evt.EventDefinitionKey)), statusText); + end + end + + fprintf('\nTotal events: %d\n', totalEvents); + if totalEvents == 0 + fprintf('(This session contains no event instances — events are written by\n'); + fprintf(' the recording/import pipeline, e.g. ATLAS or a ServerListener recorder.)\n'); + end +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/session-management/lap_segment_management.m b/snippets/matlab/session-management/lap_segment_management.m new file mode 100644 index 0000000..c99d07f --- /dev/null +++ b/snippets/matlab/session-management/lap_segment_management.m @@ -0,0 +1,75 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Lap and Segment Management +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Creating Lap objects as test segments and reading them back +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: None (creates its own .ssn2 file in the temp folder) +% Output: Segment list with name, start time, and offset +% +% Related: snippets/matlab/session-management/marker_management.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assemblies and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +NET.addAssembly(fullfile(installDir, 'MAT.OCS.Core.dll')); +import MESL.SqlRace.Domain.*; +import MAT.OCS.Core.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection string (writable temp file) --- +dbPath = fullfile(tempdir, 'sqlrace-matlab-segments.ssn2'); +if exist(dbPath, 'file'); delete(dbPath); end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); +sessionKey = SessionKey.NewKey(); + +sessionManager = SessionManager.CreateSessionManager(); +clientSession = sessionManager.CreateSession(connectionString, sessionKey, ... + 'Segment Demo', System.DateTime.UtcNow, 'Example'); + +try + session = clientSession.Session; + baseTime = int64(36e12); % 10:00:00.000 + + % --- Add segments (laps) representing test phases --- + % Lap ctor: (Int64 startTime, Int16 number, Byte triggerSource, String name, Boolean countForFastestLap) + names = {'Warmup', 'Steady State 1', 'Ramp Up', 'Steady State 2', 'Cooldown'}; + offsets = int64([0, 60e9, 180e9, 240e9, 360e9]); + + for i = 1:numel(names) + lap = Lap(baseTime + offsets(i), int16(i), uint8(0), names{i}, false); + session.LapCollection.Add(lap); + end + + fprintf('Session %s: %d segments\n\n', ... + char(sessionKey.ToString()), session.LapCollection.Count); + fprintf('%3s %-20s %20s %12s\n', '#', 'Name', 'Start (ns)', 'Offset (s)'); + fprintf('%s\n', repmat('-', 1, 58)); + + for i = 1:session.LapCollection.Count + lap = session.LapCollection.Item(i - 1); + offsetS = double(lap.StartTime - baseTime) / 1e9; + fprintf('%3d %-20s %20d %12.0f\n', lap.Number, char(lap.Name), lap.StartTime, offsetS); + end +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/session-management/marker_management.m b/snippets/matlab/session-management/marker_management.m new file mode 100644 index 0000000..03aa3ec --- /dev/null +++ b/snippets/matlab/session-management/marker_management.m @@ -0,0 +1,81 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Marker Management +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Creating Marker objects with time ranges and annotations +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: None (creates its own .ssn2 file in the temp folder) +% Output: Markers with labels and durations printed to the command window +% +% Related: snippets/matlab/session-management/lap_segment_management.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assemblies and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +NET.addAssembly(fullfile(installDir, 'MAT.OCS.Core.dll')); +import MESL.SqlRace.Domain.*; +import MAT.OCS.Core.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection string (writable temp file) --- +dbPath = fullfile(tempdir, 'sqlrace-matlab-markers.ssn2'); +if exist(dbPath, 'file'); delete(dbPath); end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); +sessionKey = SessionKey.NewKey(); + +sessionManager = SessionManager.CreateSessionManager(); +clientSession = sessionManager.CreateSession(connectionString, sessionKey, ... + 'Marker Demo', System.DateTime.UtcNow, 'Example'); + +try + session = clientSession.Session; + baseTime = int64(36e12); % 10:00:00.000 + + % --- Add markers representing annotated time regions --- + % 6-argument ctor: (startTimestamp, endTimestamp, label, markerType, description, guid) + anomaly = Marker(baseTime, baseTime + int64(2e9), ... + 'Anomaly', 'Anomaly', 'Unexpected temperature spike detected', char(System.Guid.NewGuid().ToString())); + roi = Marker(baseTime + int64(5e9), baseTime + int64(8e9), ... + 'RegionOfInterest', 'RegionOfInterest', 'Steady-state operating window', char(System.Guid.NewGuid().ToString())); + transient = Marker(baseTime + int64(10e9), baseTime + int64(12e9), ... + 'Transient', 'Transient', 'Load step response period', char(System.Guid.NewGuid().ToString())); + + session.Markers.Add(anomaly); + session.Markers.Add(roi); + session.Markers.Add(transient); + + fprintf('Session %s: %d markers added\n\n', ... + char(sessionKey.ToString()), session.Markers.Count); + + for i = 1:session.Markers.Count + marker = session.Markers.Item(i - 1); + startNs = int64(0); endNs = int64(0); + if marker.StartTimestamp.HasValue; startNs = marker.StartTimestamp.Value; end + if marker.EndTimestamp.HasValue; endNs = marker.EndTimestamp.Value; end + durationS = double(endNs - startNs) / 1e9; + fprintf(' [%s] %d-%d ns (%.1fs)\n', char(marker.Label), startNs, endNs, durationS); + fprintf(' Type: %s\n', char(marker.MarkerType)); + fprintf(' Description: %s\n\n', char(marker.Description)); + end +catch ex + fprintf('Error: %s\n', ex.message); +end + +clientSession.Dispose(); +disp('Done.'); diff --git a/snippets/matlab/session-management/session_metadata.m b/snippets/matlab/session-management/session_metadata.m new file mode 100644 index 0000000..0d098d0 --- /dev/null +++ b/snippets/matlab/session-management/session_metadata.m @@ -0,0 +1,76 @@ +% ───────────────────────────────────────────────────────────── +% SQL Race Example: Session Metadata +% ───────────────────────────────────────────────────────────── +% +% Demonstrates: Creating and reading SessionDataItem entries on a session +% Prerequisites: MATLAB R2023a+, .NET 8 runtime, MESL.SqlRace.Domain DLL +% Run dotnetenv('core') once before using this snippet. +% Input: None (creates its own .ssn2 file in the temp folder) +% Output: Session data items written and read back after reload +% +% Related: snippets/matlab/session-management/marker_management.m +% Docs: https://atlas.motionapplied.com/developer-resources/atlas/sql-race/examples/session-loading/ +% ───────────────────────────────────────────────────────────── + +% --- Configure .NET runtime (run once per MATLAB session) --- +% dotnetenv('core'); % uncomment if not already set + +% --- Load assemblies and initialise --- +sqlraceDll = getenv('SQLRACE_DLL_PATH'); +if isempty(sqlraceDll) + sqlraceDll = 'C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll'; +end +installDir = fileparts(sqlraceDll); +sysEventsDll = fullfile(installDir, 'Microsoft.Win32.SystemEvents.dll'); +if exist(sysEventsDll, 'file') + NET.addAssembly(sysEventsDll); +end +NET.addAssembly(sqlraceDll); +NET.addAssembly(fullfile(installDir, 'MAT.OCS.Core.dll')); +import MESL.SqlRace.Domain.*; +import MAT.OCS.Core.*; + +NET.setStaticProperty('MESL.SqlRace.Domain.Core.LicenceProgramName', 'SQLRace'); +Core.Initialize(); + +% --- Connection string (writable temp file) --- +dbPath = fullfile(tempdir, 'sqlrace-matlab-metadata.ssn2'); +if exist(dbPath, 'file'); delete(dbPath); end +connectionString = sprintf('DbEngine=SQLite;Data Source=%s;', dbPath); +sessionKey = SessionKey.NewKey(); + +sessionManager = SessionManager.CreateSessionManager(); + +% --- Create a session and add metadata items --- +client = sessionManager.CreateSession(connectionString, sessionKey, ... + 'Metadata Demo', System.DateTime.UtcNow, 'Example'); +try + session = client.Session; + session.Items.Add(SessionDataItem('Facility', 'Wind Tunnel A')); + session.Items.Add(SessionDataItem('Operator', 'Test Engineer 1')); + session.Items.Add(SessionDataItem('TestObjective', 'Thermal characterisation at 80% load')); + session.Items.Add(SessionDataItem('AmbientTemp', '22.5')); + session.Items.Add(SessionDataItem('RunNumber', '42')); + fprintf('Created session %s with %d metadata items\n', ... + char(sessionKey.ToString()), session.Items.Count); +catch ex + fprintf('Error writing metadata: %s\n', ex.message); +end +client.Dispose(); + +% --- Reload and read back --- +reloaded = sessionManager.Load(sessionKey, connectionString); +try + reloadedSession = reloaded.Session; + fprintf('\nReloaded session: %d metadata items\n', reloadedSession.Items.Count); + fprintf('%-20s %s\n', 'Key', 'Value'); + fprintf('%s\n', repmat('-', 1, 60)); + for i = 1:reloadedSession.Items.Count + item = reloadedSession.Items.Item(i - 1); + fprintf('%-20s %s\n', char(item.Name), char(item.Value)); + end +catch ex + fprintf('Error reading metadata: %s\n', ex.message); +end +reloaded.Dispose(); +disp('Done.'); From 45b7e1a852ba4879284c25e9cd2dee6393ce0737 Mon Sep 17 00:00:00 2001 From: Federica Donald Date: Mon, 15 Jun 2026 12:13:06 +0000 Subject: [PATCH 5/7] Adding a full test suite for validation past compilation --- .../FunctionLibrary/RollingAverageFunction.cs | 97 +++++++++--- .../FunctionLibrary/StatisticsFunction.cs | 79 +++++++--- .../FunctionLibrary/UnitConverterFunction.cs | 66 ++++++-- .../csharp/ServerListenerRecorder/Program.cs | 4 +- projects/csharp/StandaloneRecorder/Program.cs | 20 +-- projects/csharp/StandaloneRecorder/README.md | 6 +- scripts/run-examples-e2e.ps1 | 74 +++++++++ .../introspect-session-config.cs | 13 -- .../event-definitions-and-data.cs | 4 +- .../parameter-unit-resolution.cs | 1 - .../session-management/session-association.cs | 7 +- tests/README.md | 57 +++++++ .../EndToEndExampleTests.cs | 146 ++++++++++++++++++ .../FunctionComputationTests.cs | 77 +++++++++ .../SqlRace.Examples.Tests.csproj | 2 + tests/test-data/README.md | 6 + .../FixtureGenerator/FixtureGenerator.csproj | 13 ++ tools/FixtureGenerator/Program.cs | 36 +++++ tools/FixtureGenerator/VerifiedFixture.cs | 135 ++++++++++++++++ tools/SnippetRunner/Program.cs | 1 - 20 files changed, 744 insertions(+), 100 deletions(-) create mode 100644 scripts/run-examples-e2e.ps1 create mode 100644 tests/README.md create mode 100644 tests/SqlRace.Examples.Tests/EndToEndExampleTests.cs create mode 100644 tests/SqlRace.Examples.Tests/FunctionComputationTests.cs create mode 100644 tools/FixtureGenerator/FixtureGenerator.csproj create mode 100644 tools/FixtureGenerator/Program.cs create mode 100644 tools/FixtureGenerator/VerifiedFixture.cs diff --git a/projects/csharp/FunctionLibrary/RollingAverageFunction.cs b/projects/csharp/FunctionLibrary/RollingAverageFunction.cs index 55736cd..fb21059 100644 --- a/projects/csharp/FunctionLibrary/RollingAverageFunction.cs +++ b/projects/csharp/FunctionLibrary/RollingAverageFunction.cs @@ -1,65 +1,112 @@ using System.ComponentModel.Composition; -using MESL.SqlRace.Domain; using MESL.SqlRace.Domain.Functions; using MESL.SqlRace.Domain.Functions.DotNet; +using MESL.SqlRace.Enumerators; namespace FunctionLibrary; /// -/// A .NET function that computes a rolling average over a configurable time window. -/// Demonstrates state management and timestamp-based windowing. +/// A .NET function that computes a rolling (trailing) average over a fixed time window. +/// Demonstrates timestamp-based windowing across a batch of input samples. /// /// /// .NET functions are discovered via MEF [Export] and initialised by the function manager. -/// The framework handles execution scheduling; the Initialize method configures the -/// function definition with input/output bindings. +/// Initialize configures the function definition (input, output parameter and the link +/// back to this implementation); Execute is called once per data request to compute values. /// [Export(typeof(IDotNetFunction))] [Serializable] public class RollingAverageFunction : IDotNetFunction { private const string InputParameter = "Temperature:Sensors"; - private const string OutputParameter = "TemperatureAvg:Calculated"; - private const long WindowNs = 1_000_000_000L; // 1 second window - - [NonSerialized] - private readonly Queue<(long Timestamp, double Value)> _window = new(); + private const string OutputName = "TemperatureAvg"; + private const string OutputGroup = "Calculated"; + private const string OutputIdentifier = OutputName + ":" + OutputGroup; + private const long WindowNs = 1_000_000_000L; // 1-second trailing window (nanoseconds) /// public string Name => "RollingAverage"; /// - /// Initialises the function definition with input/output parameter binding. - /// The framework discovers this function via MEF and calls Initialize at load time. + /// Creates the function definition, binds the input parameter, declares the output + /// parameter, and links the definition to this implementation so the framework + /// calls . /// public void Initialize(IFunctionManager functionManager) { - // TODO: Verify — CreateFunctionDefinition parameter and IFunctionDefinition - // configuration API may differ across SQL Race versions. - var definition = functionManager.CreateFunctionDefinition( - DotNetFunctionConstants.UniqueId); + var definition = functionManager.CreateFunctionDefinition(DotNetFunctionConstants.UniqueId); + definition.Name = Name; + definition.CalculateOverWholeSession = false; + definition.InterpolateBetweenSamples = false; + definition.JoinGapsAroundNull = false; + definition.StoreInSession = false; + + // Link the definition to this .NET implementation; without this the framework + // has no Execute() body to invoke. + var implementation = (DotNetFunctionImplementationDefinition)definition.ImplementationDefinition; + implementation.Function = this; - // Configure input/output via IFunctionDefinition properties. definition.InputParameterIdentifiers.Add(InputParameter); - // TODO: Verify — OutputParameterDefinitions configuration pattern. - // Output parameter setup varies by API version; consult the SQL Race - // function authoring guide for the installed version. + definition.OutputParameterDefinitions.Add(new FunctionOutputParameterDefinition + { + Name = OutputName, + ApplicationName = OutputGroup, + Description = "1-second rolling average of temperature", + Units = "degC", + FormatOverride = "%6.2f", + ByteOrder = ByteOrderType.BigEndian, + MinimumValue = "0", + MaximumValue = "1000", + ShowInBrowser = true, + }); var buildResult = functionManager.Build(definition); - if (buildResult?.Errors?.Any() == true) + if (buildResult.Errors.Count > 0) { throw new InvalidOperationException( - $"RollingAverageFunction build failed: {string.Join("; ", buildResult.Errors)}"); + $"RollingAverageFunction build failed: {buildResult.Errors.FirstOrDefault()?.ErrorText}"); } } /// - /// Executes the function. Called by the framework for each execution cycle. + /// For each sample, averages all input samples within the trailing + /// window. Uses a two-pointer sweep so the whole batch is processed in a single pass. /// public void Execute(IExecutionContext context) { - // TODO: Verify — IExecutionContext usage pattern for reading inputs - // and writing outputs varies by SQL Race version. + var timestamps = context.FunctionInput.Timestamps; + var sampleCount = timestamps.Length; + var inputIndex = context.FunctionInput.InputParameterIndexes[InputParameter]; + var outputIndex = context.FunctionOutput.OutputParameterIndexes[OutputIdentifier]; + + var input = context.FunctionInput.Values[inputIndex]; + var output = context.FunctionOutput.OutputParametersValues[outputIndex]; + + ComputeTrailingAverage(timestamps, input, output, WindowNs); + } + + /// + /// Writes into the trailing-window average of : + /// each element is the mean of all samples within before (and + /// including) its timestamp. Single-pass two-pointer sweep. + /// + public static void ComputeTrailingAverage(long[] timestamps, double[] input, double[] output, long windowNs) + { + var windowStart = 0; + var windowSum = 0.0; + for (var i = 0; i < timestamps.Length; i++) + { + windowSum += input[i]; + + // Drop samples that have fallen outside the trailing time window. + while (timestamps[i] - timestamps[windowStart] > windowNs) + { + windowSum -= input[windowStart]; + windowStart++; + } + + output[i] = windowSum / (i - windowStart + 1); + } } } diff --git a/projects/csharp/FunctionLibrary/StatisticsFunction.cs b/projects/csharp/FunctionLibrary/StatisticsFunction.cs index 588a96a..7903ebd 100644 --- a/projects/csharp/FunctionLibrary/StatisticsFunction.cs +++ b/projects/csharp/FunctionLibrary/StatisticsFunction.cs @@ -1,60 +1,97 @@ using System.ComponentModel.Composition; -using MESL.SqlRace.Domain; using MESL.SqlRace.Domain.Functions; using MESL.SqlRace.Domain.Functions.DotNet; +using MESL.SqlRace.Enumerators; namespace FunctionLibrary; /// /// A .NET function that computes the ratio of temperature to pressure, -/// demonstrating cross-parameter calculations. +/// demonstrating cross-parameter calculations over multiple inputs. /// /// /// .NET functions are discovered via MEF [Export] and initialised by the function manager. -/// The framework handles execution scheduling; the Initialize method configures the -/// function definition with input/output bindings. +/// Initialize configures the function definition (inputs, output parameter and the link +/// back to this implementation); Execute is called once per data request to compute values. /// [Export(typeof(IDotNetFunction))] [Serializable] public class StatisticsFunction : IDotNetFunction { - private const string InputParameter = "Temperature:Sensors"; - private const string LookupParameter = "Pressure:Inlet"; - private const string OutputParameter = "TempPressRatio:Calculated"; + private const string InputTemperature = "Temperature:Sensors"; + private const string InputPressure = "Pressure:Inlet"; + private const string OutputName = "TempPressRatio"; + private const string OutputGroup = "Calculated"; + private const string OutputIdentifier = OutputName + ":" + OutputGroup; /// public string Name => "Statistics"; /// - /// Initialises the function, configuring input parameters for cross-parameter calculation. + /// Creates the function definition, binds both input parameters, declares the output + /// parameter, and links the definition to this implementation so the framework + /// calls . /// public void Initialize(IFunctionManager functionManager) { - // TODO: Verify — CreateFunctionDefinition parameter and IFunctionDefinition - // configuration API may differ across SQL Race versions. - var definition = functionManager.CreateFunctionDefinition( - DotNetFunctionConstants.UniqueId); + var definition = functionManager.CreateFunctionDefinition(DotNetFunctionConstants.UniqueId); + definition.Name = Name; + definition.CalculateOverWholeSession = false; + definition.InterpolateBetweenSamples = false; + definition.JoinGapsAroundNull = false; + definition.StoreInSession = false; - definition.InputParameterIdentifiers.Add(InputParameter); - definition.InputParameterIdentifiers.Add(LookupParameter); + // Link the definition to this .NET implementation; without this the framework + // has no Execute() body to invoke. + var implementation = (DotNetFunctionImplementationDefinition)definition.ImplementationDefinition; + implementation.Function = this; - // TODO: Verify — OutputParameterDefinitions configuration pattern. - // Output parameter setup (unit: degC/bar, format: %6.2f, range: 0 to 1000) - // varies by API version; consult the SQL Race function authoring guide. + definition.InputParameterIdentifiers.Add(InputTemperature); + definition.InputParameterIdentifiers.Add(InputPressure); + + definition.OutputParameterDefinitions.Add(new FunctionOutputParameterDefinition + { + Name = OutputName, + ApplicationName = OutputGroup, + Description = "Ratio of temperature to inlet pressure", + Units = "degC/bar", + FormatOverride = "%6.2f", + ByteOrder = ByteOrderType.BigEndian, + MinimumValue = "0", + MaximumValue = "1000", + ShowInBrowser = true, + }); var buildResult = functionManager.Build(definition); - if (buildResult?.Errors?.Any() == true) + if (buildResult.Errors.Count > 0) { throw new InvalidOperationException( - $"StatisticsFunction build failed: {string.Join("; ", buildResult.Errors)}"); + $"StatisticsFunction build failed: {buildResult.Errors.FirstOrDefault()?.ErrorText}"); } } /// - /// Executes the function. Called by the framework for each execution cycle. + /// Computes temperature / pressure for each sample. The two inputs are aligned on the + /// request's timebase, so they share the same sample count and indexing. /// public void Execute(IExecutionContext context) { - // TODO: Verify — IExecutionContext usage pattern for cross-parameter ratio. + var sampleCount = context.FunctionInput.Timestamps.Length; + var temperatureIndex = context.FunctionInput.InputParameterIndexes[InputTemperature]; + var pressureIndex = context.FunctionInput.InputParameterIndexes[InputPressure]; + var outputIndex = context.FunctionOutput.OutputParameterIndexes[OutputIdentifier]; + + var temperature = context.FunctionInput.Values[temperatureIndex]; + var pressure = context.FunctionInput.Values[pressureIndex]; + var output = context.FunctionOutput.OutputParametersValues[outputIndex]; + + for (var i = 0; i < sampleCount; i++) + { + output[i] = Ratio(temperature[i], pressure[i]); + } } + + /// Temperature-to-pressure ratio; returns 0 when pressure is zero. + public static double Ratio(double temperature, double pressure) => + pressure != 0.0 ? temperature / pressure : 0.0; } diff --git a/projects/csharp/FunctionLibrary/UnitConverterFunction.cs b/projects/csharp/FunctionLibrary/UnitConverterFunction.cs index 2760cce..a46a5fc 100644 --- a/projects/csharp/FunctionLibrary/UnitConverterFunction.cs +++ b/projects/csharp/FunctionLibrary/UnitConverterFunction.cs @@ -1,7 +1,7 @@ using System.ComponentModel.Composition; -using MESL.SqlRace.Domain; using MESL.SqlRace.Domain.Functions; using MESL.SqlRace.Domain.Functions.DotNet; +using MESL.SqlRace.Enumerators; namespace FunctionLibrary; @@ -11,48 +11,82 @@ namespace FunctionLibrary; /// /// /// .NET functions are discovered via MEF [Export] and initialised by the function manager. -/// The framework handles execution scheduling; the Initialize method configures the -/// function definition with input/output bindings. +/// Initialize configures the function definition (inputs, output parameter and the link +/// back to this implementation); Execute is called once per data request to compute values. /// [Export(typeof(IDotNetFunction))] [Serializable] public class UnitConverterFunction : IDotNetFunction { private const string InputParameter = "Temperature:Sensors"; - private const string OutputParameter = "TemperatureFahrenheit:Calculated"; + private const string OutputName = "TemperatureFahrenheit"; + private const string OutputGroup = "Calculated"; + private const string OutputIdentifier = OutputName + ":" + OutputGroup; /// public string Name => "UnitConverter"; /// - /// Initialises the function by creating its definition and binding parameters. + /// Creates the function definition, binds the input parameter, declares the output + /// parameter, and links the definition to this implementation so the framework + /// calls . /// public void Initialize(IFunctionManager functionManager) { - // TODO: Verify — CreateFunctionDefinition parameter and IFunctionDefinition - // configuration API may differ across SQL Race versions. - var definition = functionManager.CreateFunctionDefinition( - DotNetFunctionConstants.UniqueId); + var definition = functionManager.CreateFunctionDefinition(DotNetFunctionConstants.UniqueId); + definition.Name = Name; + definition.CalculateOverWholeSession = false; + definition.InterpolateBetweenSamples = false; + definition.JoinGapsAroundNull = false; + definition.StoreInSession = false; + + // Link the definition to this .NET implementation; without this the framework + // has no Execute() body to invoke. + var implementation = (DotNetFunctionImplementationDefinition)definition.ImplementationDefinition; + implementation.Function = this; definition.InputParameterIdentifiers.Add(InputParameter); - // TODO: Verify — OutputParameterDefinitions configuration pattern. - // Output parameter setup (unit: degF, format: %5.1f, range: -40 to 1000) - // varies by API version; consult the SQL Race function authoring guide. + definition.OutputParameterDefinitions.Add(new FunctionOutputParameterDefinition + { + Name = OutputName, + ApplicationName = OutputGroup, + Description = "Temperature converted to Fahrenheit", + Units = "degF", + FormatOverride = "%5.1f", + ByteOrder = ByteOrderType.BigEndian, + MinimumValue = "-40", + MaximumValue = "1000", + ShowInBrowser = true, + }); var buildResult = functionManager.Build(definition); - if (buildResult?.Errors?.Any() == true) + if (buildResult.Errors.Count > 0) { throw new InvalidOperationException( - $"UnitConverterFunction build failed: {string.Join("; ", buildResult.Errors)}"); + $"UnitConverterFunction build failed: {buildResult.Errors.FirstOrDefault()?.ErrorText}"); } } /// - /// Executes the function. Called by the framework for each execution cycle. + /// Converts each Celsius input sample to Fahrenheit. Called once per data request + /// with a batch of samples in . /// public void Execute(IExecutionContext context) { - // TODO: Verify — IExecutionContext usage pattern for C-to-F conversion. + var sampleCount = context.FunctionInput.Timestamps.Length; + var inputIndex = context.FunctionInput.InputParameterIndexes[InputParameter]; + var outputIndex = context.FunctionOutput.OutputParameterIndexes[OutputIdentifier]; + + var input = context.FunctionInput.Values[inputIndex]; + var output = context.FunctionOutput.OutputParametersValues[outputIndex]; + + for (var i = 0; i < sampleCount; i++) + { + output[i] = ToFahrenheit(input[i]); + } } + + /// Converts a Celsius value to Fahrenheit. + public static double ToFahrenheit(double celsius) => celsius * 9.0 / 5.0 + 32.0; } diff --git a/projects/csharp/ServerListenerRecorder/Program.cs b/projects/csharp/ServerListenerRecorder/Program.cs index 5092e1d..8d1278b 100644 --- a/projects/csharp/ServerListenerRecorder/Program.cs +++ b/projects/csharp/ServerListenerRecorder/Program.cs @@ -34,8 +34,8 @@ Console.WriteLine($"Listener: {recorderConfig.ListenerIpAddress}:{recorderConfig.ListenerPort}"); // ─── Configure Server Listener ─────────────────────────────── -// TODO: Verify — Core.ConfigureServer() and RecordersConfiguration API signatures -// The server listener receives data pushed from ATLAS or another telemetry source. +// The server listener (Core.ConfigureServer(bool, IPEndPoint) + RecordersConfiguration) +// receives data pushed from ATLAS or another telemetry source. // In a real deployment, the server listener port in this application and the // corresponding port in ATLAS (Tools → SqlRace → Settings) must be different. Console.WriteLine($"Data source: {recorderConfig.ResolvedDataSourcePath}"); diff --git a/projects/csharp/StandaloneRecorder/Program.cs b/projects/csharp/StandaloneRecorder/Program.cs index f0155b4..ca8853b 100644 --- a/projects/csharp/StandaloneRecorder/Program.cs +++ b/projects/csharp/StandaloneRecorder/Program.cs @@ -36,12 +36,12 @@ Console.WriteLine($"Connection: {connectionString}"); // ─── Initialise Recorder ───────────────────────────────────── -// TODO: Verify — Recorders.Initialise() API signature -// In production: Recorders.Initialise(config.ListenerIpAddress, config.ListenerPort); +// NOTE: Live recording requires a running ATLAS Data Server (ADS) and recorder types +// that aren't exercised in this example. The commented lines below sketch that +// production flow; this example simulates the data instead, so it runs without a live ADS. Console.WriteLine("\nInitialising recorder subsystem..."); // ─── Create Data Server Telemetry Recorder ─────────────────── -// TODO: Verify — DataServerTelemetryRecorder creation API // var recorder = new DataServerTelemetryRecorder(); // recorder.ServerList.Refresh(); // @@ -56,15 +56,8 @@ var sessionCreated = new TaskCompletionSource(); var sessionManager = SessionManager.CreateSessionManager(); -// TODO: Verify — SessionManager.SessionEventOccurred event signature -// sessionManager.SessionEventOccurred += (sender, e) => -// { -// if (e.EventType == SessionEventType.SessionCreated) -// { -// Console.WriteLine($" Session detected: {e.SessionKey}"); -// sessionCreated.TrySetResult(e.SessionKey); -// } -// }; +// With a live ADS, new sessions arrive through the streaming flow and are surfaced by +// the recorder; this example completes the task below from its simulation instead. Console.WriteLine("Waiting for session to be created..."); Console.WriteLine("(In production, this waits for ADS to start streaming data)"); @@ -160,7 +153,8 @@ } finally { - // TODO: Verify — recorder.StopRecording(), recorder.AutoRecordEnable = false, recorder.Dispose() + // With a live ADS recorder you would also stop and dispose it here + // (e.g. recorder.AutoRecordEnable = false; recorder.StopRecording(); recorder.Dispose()). clientSession?.Dispose(); Console.WriteLine("Session closed. Recorder shut down."); } diff --git a/projects/csharp/StandaloneRecorder/README.md b/projects/csharp/StandaloneRecorder/README.md index b973431..f8e3a47 100644 --- a/projects/csharp/StandaloneRecorder/README.md +++ b/projects/csharp/StandaloneRecorder/README.md @@ -16,9 +16,11 @@ A console application that demonstrates the pattern for connecting to an ATLAS D 2. Multi-parameter configuration with `ParameterBuilder` 3. A **read-transform-write** loop: raw telemetry → exponential moving average → derived parameter 4. Async operation with `CancellationToken` for graceful Ctrl+C shutdown -5. The pattern for detecting new sessions via `SessionManager.SessionEventOccurred` +5. The pattern for detecting new sessions as part of the ADS streaming flow -The ADS connection code is stubbed with `// TODO: Verify` comments since it requires a running ADS instance. +The ADS connection code is illustrative only (commented out). Live recording requires a +running ATLAS Data Server (ADS) instance and additional recorder types that aren't shown +in this example. ## Prerequisites diff --git a/scripts/run-examples-e2e.ps1 b/scripts/run-examples-e2e.ps1 new file mode 100644 index 0000000..1115374 --- /dev/null +++ b/scripts/run-examples-e2e.ps1 @@ -0,0 +1,74 @@ +# ───────────────────────────────────────────────────────────── +# End-to-end snippet smoke-runner. +# +# Generates the deterministic verified fixture (tools/FixtureGenerator) at the default +# SQLite path the snippets read from, then runs each runnable snippet against it and +# checks it exits cleanly and prints the expected anchor text. +# +# Requires the SQL Race runtime + licence — local / licenced-runner only, NOT GitHub CI. +# Usage: pwsh scripts/run-examples-e2e.ps1 +# ───────────────────────────────────────────────────────────── +$ErrorActionPreference = "Stop" +$repo = Split-Path $PSScriptRoot -Parent +Push-Location $repo +try { + $fixtureKey = "f1c0ffee-0000-4000-8000-000000000001" # VerifiedFixture.SessionKeyString + $fixturePath = Join-Path $env:TEMP "sqlrace-examples.ssn2" # default path snippets read + + Write-Host "Building generator + snippet runner..." -ForegroundColor Cyan + dotnet build tools/FixtureGenerator/FixtureGenerator.csproj -c Release -v quiet | Out-Null + if (-not $?) { throw "FixtureGenerator build failed" } + + Write-Host "Generating verified fixture at $fixturePath" -ForegroundColor Cyan + & "tools/FixtureGenerator/bin/Release/net8.0/FixtureGenerator.exe" $fixturePath | Out-Null + if (-not $?) { throw "Fixture generation failed" } + + # Each snippet: args to pass, and an expected substring proving it ran against the fixture. + $snippets = @( + @{ Name = "01-load-session-from-file"; Path = "snippets\csharp\getting-started\01-load-session-from-file.cs"; Args = @($fixturePath); Expect = "Verified Test Session" }, + @{ Name = "03-read-parameter-samples"; Path = "snippets\csharp\getting-started\03-read-parameter-samples.cs"; Args = @($fixtureKey); Expect = "1000" }, + @{ Name = "read-samples-between"; Path = "snippets\csharp\data-access\read-samples-between.cs"; Args = @($fixtureKey); Expect = "Temperature:Sensors" }, + @{ Name = "bulk-parameter-read"; Path = "snippets\csharp\data-access\bulk-parameter-read.cs"; Args = @($fixtureKey); Expect = $null }, + @{ Name = "multi-rate-alignment"; Path = "snippets\csharp\data-access\multi-rate-alignment.cs"; Args = @($fixtureKey); Expect = $null }, + @{ Name = "reverse-iteration"; Path = "snippets\csharp\data-access\reverse-iteration.cs"; Args = @($fixtureKey); Expect = $null }, + @{ Name = "parameter-unit-resolution"; Path = "snippets\csharp\session-management\parameter-unit-resolution.cs"; Args = @($fixtureKey); Expect = "Temperature:Sensors" } + ) + + # Preserve the SnippetRunner program (the runner mechanism overwrites it). + $runnerProgram = "tools\SnippetRunner\Program.cs" + $backup = Get-Content $runnerProgram -Raw + + $results = @() + try { + foreach ($s in $snippets) { + Write-Host "`n===== $($s.Name) =====" -ForegroundColor Cyan + Copy-Item $s.Path $runnerProgram -Force + (Get-Item $runnerProgram).LastWriteTime = Get-Date + + $out = dotnet run --project tools\SnippetRunner\SnippetRunner.csproj -c Release -- @($s.Args) 2>&1 | + Where-Object { $_ -notmatch "warning|NU1701|Restored|Determining|reachable on all platforms" -and $_ -ne "" } + $exit = $LASTEXITCODE + $text = ($out | Out-String) + + $ok = ($exit -eq 0) -and ($text -notmatch "ERROR:|Usage:|not found|No sessions") + if ($ok -and $s.Expect) { $ok = $text -match [regex]::Escape($s.Expect) } + + $status = if ($ok) { "PASS" } else { "FAIL" } + Write-Host "[$status] exit=$exit" -ForegroundColor $(if ($ok) { "Green" } else { "Red" }) + $out | Select-Object -First 4 | ForEach-Object { Write-Host " $_" } + $results += [pscustomobject]@{ Snippet = $s.Name; Status = $status } + } + } + finally { + Set-Content $runnerProgram -Value $backup -NoNewline + } + + Write-Host "`n=== Summary ===" -ForegroundColor Yellow + $results | Format-Table -AutoSize | Out-String | Write-Host + $failed = @($results | Where-Object { $_.Status -ne "PASS" }).Count + if ($failed -gt 0) { Write-Host "$failed snippet(s) failed." -ForegroundColor Red; exit 1 } + Write-Host "All snippets passed against the verified fixture." -ForegroundColor Green +} +finally { + Pop-Location +} diff --git a/snippets/csharp/configuration/introspect-session-config.cs b/snippets/csharp/configuration/introspect-session-config.cs index 16a236f..a100fb2 100644 --- a/snippets/csharp/configuration/introspect-session-config.cs +++ b/snippets/csharp/configuration/introspect-session-config.cs @@ -44,19 +44,6 @@ Console.WriteLine($"{param.Identifier,-30} {param.ConversionFunctionName,-25} {param.ApplicationName,-15}"); } -// --- Channels --- -// TODO: Verify — channel enumeration may require iterating parameters -Console.WriteLine($"\n=== Channels ==="); -Console.WriteLine($"{"ID",8} {"Name",-20} {"Interval (ns)",15} {"DataType",-20}"); -Console.WriteLine(new string('─', 65)); - -var seenChannels = new HashSet(); -foreach (var param in session.Parameters) -{ - if (!seenChannels.Add(param.Name)) continue; - Console.WriteLine($"{"—",8} {param.Name,-20} {"(see channel)",15} {"(see channel)",-20}"); -} - // --- Application Groups and Parameter Groups --- Console.WriteLine($"\n=== Application Groups ==="); var appGroups = session.Parameters.Select(p => p.ApplicationName).Distinct(); diff --git a/snippets/csharp/session-management/event-definitions-and-data.cs b/snippets/csharp/session-management/event-definitions-and-data.cs index ae63c48..72b3d43 100644 --- a/snippets/csharp/session-management/event-definitions-and-data.cs +++ b/snippets/csharp/session-management/event-definitions-and-data.cs @@ -6,8 +6,8 @@ // Prerequisites: MESL.SQLRace.API NuGet package, .NET 8 // Input: None (creates its own session) // Output: Event definitions structure and API usage patterns -// TODO: Verify — EventDefinition persistence via ConfigurationSet.Commit() may require -// a full config (with at least one channel/parameter) to work with SQLite sessions. +// Note: event definitions are persisted on ConfigurationSet.Commit(); on a brand-new +// SQLite session you may need at least one committed parameter/channel first. // // Related: snippets/csharp/session-management/marker-management.cs // Docs: https://mat-docs.github.io/Atlas.SQLRaceAPI.Documentation/api/index.html diff --git a/snippets/csharp/session-management/parameter-unit-resolution.cs b/snippets/csharp/session-management/parameter-unit-resolution.cs index 1ac5eb7..f843c69 100644 --- a/snippets/csharp/session-management/parameter-unit-resolution.cs +++ b/snippets/csharp/session-management/parameter-unit-resolution.cs @@ -61,7 +61,6 @@ var unit = "(unknown)"; var format = ""; - // TODO: Verify — GetConversion may be on ConfigurationSet or Session try { var conversion = session.GetConversion(conversionName); diff --git a/snippets/csharp/session-management/session-association.cs b/snippets/csharp/session-management/session-association.cs index dd6d924..f80c440 100644 --- a/snippets/csharp/session-management/session-association.cs +++ b/snippets/csharp/session-management/session-association.cs @@ -38,7 +38,6 @@ } // --- Associate them --- -// TODO: Verify — association API may vary; some versions use SessionManager methods using (var primary = sessionManager.Load(primaryKey, connectionString)) { primary.Session.AssociateWithParent(associateKey); @@ -52,6 +51,6 @@ Console.WriteLine($"\nLoaded primary session: {session.Identifier}"); Console.WriteLine($" Items: {session.Items.Count}"); -// TODO: Verify — enumerate associated sessions via session.Associates or similar -Console.WriteLine($"\nAssociation established. Use SessionManager.LoadAssociatesForSession()"); -Console.WriteLine($"to access the associated session's data alongside the primary."); +// --- Load the associated sessions alongside the primary --- +var associates = sessionManager.LoadAssociatesForSession(primaryKey, connectionString); +Console.WriteLine($" Associated sessions loaded: {associates.Count}"); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..4412cfe --- /dev/null +++ b/tests/README.md @@ -0,0 +1,57 @@ +# Tests & End-to-End Validation + +> Internal developer documentation — not part of the customer-facing example docs. + +The examples are validated against **verified inputs**: a deterministic SQL Race session +whose every sample is produced by a closed-form formula. Because the inputs are known, the +correct outputs are known — so the examples are checked for *correctness*, not just "it ran". + +## The pieces + +| Component | Location | What it does | +|-----------|----------|--------------| +| Fixture generator | [`../tools/FixtureGenerator`](../tools/FixtureGenerator) | Writes a deterministic `.ssn2` from known formulas. `VerifiedFixture` exposes the formulas as constants the tests assert against. | +| Capability tests | [`SqlRace.Examples.Tests/EndToEndExampleTests.cs`](SqlRace.Examples.Tests/EndToEndExampleTests.cs) | Loads the fixture and asserts that load/read/time-range/multi-rate/query operations return the values the formulas imply. | +| Function tests | [`SqlRace.Examples.Tests/FunctionComputationTests.cs`](SqlRace.Examples.Tests/FunctionComputationTests.cs) | Asserts the FunctionLibrary maths (C→F, ratio, rolling average) against hand-derived values. No licence needed. | +| Snippet smoke-runner | [`../scripts/run-examples-e2e.ps1`](../scripts/run-examples-e2e.ps1) | Generates the fixture, runs the actual runnable snippets against it, and checks they exit cleanly and print the expected anchor output. | + +## The verified fixture + +`VerifiedFixture.Generate(connectionString)` writes one session (`Verified Test Session`, +fixed key `f1c0ffee-0000-4000-8000-000000000001`, start 10:00:00, 10 s long): + +| Parameter | Rate | Samples | Formula | Anchors | +|-----------|------|---------|---------|---------| +| `Temperature:Sensors` | 100 Hz | 1000 | `20.0 + 0.1·i` | `[0]=20.0`, `[999]=119.9` | +| `Pressure:Inlet` | 100 Hz | 1000 | `2.0` (constant) | all `2.0` | +| `Speed:Shaft` | 50 Hz | 500 | `0.2·i` | `[0]=0.0`, `[499]=99.8` | + +## Running it + +> **Requires the SQL Race runtime + a valid licence.** These run locally (or on a licenced +> self-hosted runner). GitHub-hosted CI has no licence, so it builds only and does **not** +> run tests — see [`../.github/workflows/build-and-test.yml`](../.github/workflows/build-and-test.yml). + +```powershell +# Capability + function tests +dotnet test SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj -c Release + +# Snippet smoke-run against the fixture +pwsh ../scripts/run-examples-e2e.ps1 +``` + +The function-computation tests need no licence, so they can be run anywhere with +`--filter "FullyQualifiedName~FunctionComputation"`. + +## Notes / known limitations (discovered while building this) + +- **`samples.Data` is a capacity buffer.** Only indices `[0, SampleCount)` hold valid data; + reading `Data[Data.Length-1]` (or `Data[^1]`) returns a trailing zero. Index with + `Data[SampleCount-1]`. +- **Laps in freshly-created SQLite sessions.** A created session always reloads with exactly + one (default) lap; laps added via `LapCollection.Add` (individually or batched) do not + survive a reload through this API path. The verified fixture therefore does not rely on + laps. Pre-existing `.ssn2` files may contain laps written by other tooling. +- **Multi-parameter sessions.** Use `ParameterBuilder.BuildBatch` (single config commit) — + separate `Build()` calls each commit their own configuration and only the first survives a + reload. diff --git a/tests/SqlRace.Examples.Tests/EndToEndExampleTests.cs b/tests/SqlRace.Examples.Tests/EndToEndExampleTests.cs new file mode 100644 index 0000000..d6e6d77 --- /dev/null +++ b/tests/SqlRace.Examples.Tests/EndToEndExampleTests.cs @@ -0,0 +1,146 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Examples: End-to-End Example Tests +// ───────────────────────────────────────────────────────────── +// +// Verified-input / verified-output tests. A deterministic session is generated by +// VerifiedFixture (every sample produced by a closed-form formula), then the operations +// the examples demonstrate are run against it and asserted to produce the values that +// formula implies. Inputs are known, so the correct outputs are known. +// +// Requires the SQL Race runtime + licence (local/licenced runner only — not GitHub CI). +// Run: dotnet test --filter "FullyQualifiedName~EndToEnd" +// ───────────────────────────────────────────────────────────── + +using Xunit; +using SqlRace.Examples.Core; +using SqlRace.Examples.Fixtures; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Query; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; + +namespace SqlRace.Examples.Tests; + +/// +/// Generates the verified session once into a throwaway SQLite file shared by the test class. +/// +public sealed class VerifiedSessionFixture : IDisposable +{ + public string DbPath { get; } + public string ConnectionString { get; } + + public VerifiedSessionFixture() + { + DbPath = Path.Combine(Path.GetTempPath(), $"verified-e2e-{Guid.NewGuid():N}.ssn2"); + ConnectionString = ConnectionStringProvider.ForSQLiteFile(DbPath); + VerifiedFixture.Generate(ConnectionString); + } + + public void Dispose() + { + if (File.Exists(DbPath)) + { + try { File.Delete(DbPath); } catch { /* best effort */ } + } + } +} + +public class EndToEndExampleTests : IClassFixture +{ + private readonly VerifiedSessionFixture _fixture; + + public EndToEndExampleTests(VerifiedSessionFixture fixture) => _fixture = fixture; + + private IClientSession LoadSession() + { + var sessionManager = SessionManager.CreateSessionManager(); + return sessionManager.Load(VerifiedFixture.Key, _fixture.ConnectionString); + } + + [Fact] + public void LoadSummary_MatchesGeneratedShape() + { + using var client = LoadSession(); + var session = client.Session; + + Assert.Equal(VerifiedFixture.SessionName, session.Identifier); + Assert.Equal(VerifiedFixture.ParameterCount, session.Parameters.Count); + Assert.Equal(VerifiedFixture.StartTimeNs, session.StartTime); + } + + [Fact] + public void QueryByMetadata_FindsTheSession() + { + using var queryManager = QueryManager.CreateQueryManager(_fixture.ConnectionString); + var summaries = queryManager.ExecuteQuery().ToList(); + + Assert.Contains(summaries, s => s.Identifier == VerifiedFixture.SessionName); + } + + [Fact] + public void ReadTemperature_ReturnsRampMatchingTheFormula() + { + using var client = LoadSession(); + var session = client.Session; + + using var pda = session.CreateParameterDataAccess(VerifiedFixture.TemperatureId); + var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + + Assert.Equal(VerifiedFixture.TemperatureSampleCount, samples.SampleCount); + Assert.Equal(VerifiedFixture.StartTimeNs, samples.Timestamp[0]); + Assert.Equal(VerifiedFixture.Temperature(0), samples.Data[0], precision: 6); + // Note: samples.Data is a capacity buffer — only [0, SampleCount) is valid data. + Assert.Equal(VerifiedFixture.Temperature(samples.SampleCount - 1), samples.Data[samples.SampleCount - 1], precision: 6); + } + + [Fact] + public void ReadFirstSecond_ReturnsExactlyTheExpectedWindow() + { + using var client = LoadSession(); + var session = client.Session; + + var interval = new Frequency(VerifiedFixture.TemperatureHz, FrequencyUnit.Hz).ToInterval(); + // 100 samples: indices 0..99 inclusive. + var end = session.StartTime + 99 * interval; + + using var pda = session.CreateParameterDataAccess(VerifiedFixture.TemperatureId); + var samples = pda.GetSamplesBetween(session.StartTime, end); + + Assert.Equal(100, samples.SampleCount); + Assert.Equal(VerifiedFixture.Temperature(99), samples.Data[samples.SampleCount - 1], precision: 6); // 29.9 + } + + [Fact] + public void ReadPressure_IsConstant() + { + using var client = LoadSession(); + var session = client.Session; + + using var pda = session.CreateParameterDataAccess(VerifiedFixture.PressureId); + var samples = pda.GetSamplesBetween(session.StartTime, session.EndTime); + + Assert.Equal(VerifiedFixture.PressureSampleCount, samples.SampleCount); + for (var i = 0; i < samples.SampleCount; i++) + { + Assert.Equal(VerifiedFixture.PressureConstant, samples.Data[i], precision: 6); + } + } + + [Fact] + public void MultiRate_TemperatureHasTwiceAsManySamplesAsSpeed() + { + using var client = LoadSession(); + var session = client.Session; + + using var tempPda = session.CreateParameterDataAccess(VerifiedFixture.TemperatureId); + using var speedPda = session.CreateParameterDataAccess(VerifiedFixture.SpeedId); + + var temp = tempPda.GetSamplesBetween(session.StartTime, session.EndTime); + var speed = speedPda.GetSamplesBetween(session.StartTime, session.EndTime); + + Assert.Equal(VerifiedFixture.TemperatureSampleCount, temp.SampleCount); // 1000 @ 100 Hz + Assert.Equal(VerifiedFixture.SpeedSampleCount, speed.SampleCount); // 500 @ 50 Hz + Assert.Equal(2 * speed.SampleCount, temp.SampleCount); + Assert.Equal(VerifiedFixture.Speed(speed.SampleCount - 1), speed.Data[speed.SampleCount - 1], precision: 6); // 99.8 + } +} diff --git a/tests/SqlRace.Examples.Tests/FunctionComputationTests.cs b/tests/SqlRace.Examples.Tests/FunctionComputationTests.cs new file mode 100644 index 0000000..e11f065 --- /dev/null +++ b/tests/SqlRace.Examples.Tests/FunctionComputationTests.cs @@ -0,0 +1,77 @@ +// ───────────────────────────────────────────────────────────── +// SQL Race Examples: Function Computation Tests +// ───────────────────────────────────────────────────────────── +// +// Verifies the arithmetic of the FunctionLibrary .NET functions against hand-derived +// expected values. These test the pure compute helpers the functions' Execute() methods +// delegate to, so no SQL Race runtime/licence is required — they run everywhere. +// +// Run: dotnet test --filter "FullyQualifiedName~FunctionComputation" +// ───────────────────────────────────────────────────────────── + +using Xunit; +using FunctionLibrary; + +namespace SqlRace.Examples.Tests; + +public class FunctionComputationTests +{ + [Theory] + [InlineData(0.0, 32.0)] + [InlineData(20.0, 68.0)] + [InlineData(100.0, 212.0)] + [InlineData(-40.0, -40.0)] + public void UnitConverter_CelsiusToFahrenheit(double celsius, double expectedF) + { + Assert.Equal(expectedF, UnitConverterFunction.ToFahrenheit(celsius), precision: 9); + } + + [Theory] + [InlineData(20.0, 2.0, 10.0)] + [InlineData(50.0, 5.0, 10.0)] + [InlineData(7.0, 0.0, 0.0)] // guard: divide-by-zero returns 0 + public void Statistics_Ratio(double temperature, double pressure, double expected) + { + Assert.Equal(expected, StatisticsFunction.Ratio(temperature, pressure), precision: 9); + } + + [Fact] + public void RollingAverage_ConstantInput_IsUnchanged() + { + var n = 50; + var timestamps = new long[n]; + var input = new double[n]; + var output = new double[n]; + for (var i = 0; i < n; i++) + { + timestamps[i] = i * 100_000_000L; // 10 Hz + input[i] = 5.0; + } + + RollingAverageFunction.ComputeTrailingAverage(timestamps, input, output, 1_000_000_000L); + + Assert.All(output, v => Assert.Equal(5.0, v, precision: 9)); + } + + [Fact] + public void RollingAverage_RampInput_MatchesHandComputedWindow() + { + // 10 Hz samples, 1-second trailing window → up to 11 samples (indices spanning 1 s). + var n = 12; + var timestamps = new long[n]; + var input = new double[n]; + var output = new double[n]; + for (var i = 0; i < n; i++) + { + timestamps[i] = i * 100_000_000L; // 0.1 s apart + input[i] = i; // ramp: value == index + } + + RollingAverageFunction.ComputeTrailingAverage(timestamps, input, output, 1_000_000_000L); + + Assert.Equal(0.0, output[0], precision: 9); // mean(0) + Assert.Equal(2.5, output[5], precision: 9); // mean(0..5) + Assert.Equal(5.0, output[10], precision: 9); // mean(0..10), window still 1 s + Assert.Equal(6.0, output[11], precision: 9); // index 0 drops out → mean(1..11) + } +} diff --git a/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj b/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj index f80cabe..5d112ea 100644 --- a/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj +++ b/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj @@ -18,6 +18,8 @@ + + diff --git a/tests/test-data/README.md b/tests/test-data/README.md index e16bb2a..0e593bc 100644 --- a/tests/test-data/README.md +++ b/tests/test-data/README.md @@ -14,3 +14,9 @@ dotnet run --project GettingStarted ``` This creates a session in the default SQLite database at `%TEMP%/sqlrace-examples.ssn2`. + +## End-to-end validation + +For deterministic, verified-input/verified-output testing of the examples, see +[the tests README](../README.md). It describes the `FixtureGenerator`, the capability and +function tests, and the snippet smoke-runner. diff --git a/tools/FixtureGenerator/FixtureGenerator.csproj b/tools/FixtureGenerator/FixtureGenerator.csproj new file mode 100644 index 0000000..cc8fbfb --- /dev/null +++ b/tools/FixtureGenerator/FixtureGenerator.csproj @@ -0,0 +1,13 @@ + + + Exe + net8.0 + enable + enable + FixtureGenerator + SqlRace.Examples.Fixtures + + + + + diff --git a/tools/FixtureGenerator/Program.cs b/tools/FixtureGenerator/Program.cs new file mode 100644 index 0000000..72a98fb --- /dev/null +++ b/tools/FixtureGenerator/Program.cs @@ -0,0 +1,36 @@ +// ============================================================================ +// Fixture Generator — writes the deterministic "verified" session to disk. +// ============================================================================ +// Used by the end-to-end example harness: +// • the capability tests call VerifiedFixture.Generate(...) directly; +// • the snippet smoke-runner runs this exe to materialise the fixture at the +// default SQLite path the snippets read from. +// +// Usage: +// dotnet run --project tools/FixtureGenerator -- [output.ssn2] +// If no path is given, falls back to SQLRACE_FIXTURE_PATH, then the default +// snippet path (%TEMP%/sqlrace-examples.ssn2). +// ============================================================================ + +using SqlRace.Examples.Core; +using SqlRace.Examples.Fixtures; + +var path = args.Length > 0 + ? args[0] + : Environment.GetEnvironmentVariable("SQLRACE_FIXTURE_PATH") + ?? Path.Combine(Path.GetTempPath(), "sqlrace-examples.ssn2"); + +// Start from a clean file so generation is fully deterministic. +if (File.Exists(path)) +{ + File.Delete(path); +} + +var connectionString = ConnectionStringProvider.ForSQLiteFile(path); +VerifiedFixture.Generate(connectionString); + +Console.WriteLine("Verified fixture written."); +Console.WriteLine($" File: {path}"); +Console.WriteLine($" Session key: {VerifiedFixture.SessionKeyString}"); +Console.WriteLine($" Parameters: {VerifiedFixture.ParameterCount} " + + $"({VerifiedFixture.TemperatureId}, {VerifiedFixture.PressureId}, {VerifiedFixture.SpeedId})"); diff --git a/tools/FixtureGenerator/VerifiedFixture.cs b/tools/FixtureGenerator/VerifiedFixture.cs new file mode 100644 index 0000000..c7d7dcc --- /dev/null +++ b/tools/FixtureGenerator/VerifiedFixture.cs @@ -0,0 +1,135 @@ +using MAT.OCS.Core; +using MESL.SqlRace.Domain; +using MESL.SqlRace.Domain.Infrastructure.DataPipeline; +using MESL.SqlRace.Enumerators; +using SqlRace.Examples.Core; + +namespace SqlRace.Examples.Fixtures; + +/// +/// Generates a deterministic SQL Race session with analytically-defined data, so that +/// example outputs can be checked against known-correct expected values. +/// +/// This is the "verified input" for the end-to-end example tests: every sample is produced +/// by a closed-form formula, so the expected output of any example reading this session can +/// be derived by hand (and is exposed here as constants the tests assert against). +/// +public static class VerifiedFixture +{ + // ── Session identity ─────────────────────────────────────── + /// Fixed session key so examples that take a GUID can be pointed at the fixture. + public const string SessionKeyString = "f1c0ffee-0000-4000-8000-000000000001"; + public const string SessionName = "Verified Test Session"; + + /// Session start in nanoseconds-of-day (10:00:00.000 → 36,000 s). + public const long StartTimeNs = 36_000_000_000_000L; + public const int DurationSeconds = 10; + + // ── Temperature:Sensors @ 100 Hz — linear ramp ───────────── + // value(i) = TempBase + TempSlope * i + public const string TemperatureId = "Temperature:Sensors"; + public const int TemperatureHz = 100; + public const double TempBase = 20.0; + public const double TempSlope = 0.1; + public const int TemperatureSampleCount = TemperatureHz * DurationSeconds; // 1000 + + // ── Pressure:Inlet @ 100 Hz — constant ───────────────────── + public const string PressureId = "Pressure:Inlet"; + public const int PressureHz = 100; + public const double PressureConstant = 2.0; + public const int PressureSampleCount = PressureHz * DurationSeconds; // 1000 + + // ── Speed:Shaft @ 50 Hz — linear ramp ────────────────────── + // value(i) = SpeedSlope * i + public const string SpeedId = "Speed:Shaft"; + public const int SpeedHz = 50; + public const double SpeedSlope = 0.2; + public const int SpeedSampleCount = SpeedHz * DurationSeconds; // 500 + + public const int ParameterCount = 3; + + // NOTE: a freshly-created SQLite session always reloads with exactly one (default) lap. + // Laps added via LapCollection.Add do not survive a reload in this API path, so the + // verified fixture does not rely on laps. (Pre-existing .ssn2 files may contain laps + // written by other tooling.) + public const int LapCountOnReload = 1; + + /// The fixed session key as a parsed . + public static SessionKey Key => SessionKey.Parse(SessionKeyString); + + /// Expected value of the temperature ramp at sample . + public static double Temperature(int i) => TempBase + TempSlope * i; + + /// Expected value of the speed ramp at sample . + public static double Speed(int i) => SpeedSlope * i; + + /// + /// (Re)creates the verified session in the database at . + /// Any existing session with the fixed key is removed first so generation is idempotent. + /// + public static void Generate(string connectionString) + { + SqlRaceBootstrapper.Initialize(); + + var sessionManager = SessionManager.CreateSessionManager(); + + // Date with a 10:00:00 time-of-day so Session.StartTime == StartTimeNs. + var sessionDate = new DateTime(2025, 6, 15, 10, 0, 0, DateTimeKind.Utc); + + using var clientSession = sessionManager.CreateSession( + connectionString, Key, SessionName, sessionDate, "Example"); + var session = clientSession.Session; + + // Build all three parameters in a single configuration commit. (Separate Build() + // calls each create and commit their own configuration, and only the first + // survives a reload — BuildBatch is the correct API for multiple parameters.) + var built = ParameterBuilder.BuildBatch(session, new[] + { + new ParameterBuilder(session) + .WithIdentifier(TemperatureId) + .WithDescription("Inlet temperature sensor (verified ramp)") + .InApplicationGroup("Sensors") + .InParameterGroup("Thermal") + .WithFrequency(TemperatureHz, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("degC", "%5.2f") + .WithRange(min: 0, max: 500), + new ParameterBuilder(session) + .WithIdentifier(PressureId) + .WithDescription("Inlet pressure sensor (verified constant)") + .InApplicationGroup("Sensors") + .InParameterGroup("Hydraulic") + .WithFrequency(PressureHz, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("bar", "%5.3f") + .WithRange(min: 0, max: 10), + new ParameterBuilder(session) + .WithIdentifier(SpeedId) + .WithDescription("Shaft speed (verified ramp)") + .InApplicationGroup("Driveline") + .InParameterGroup("Rotation") + .WithFrequency(SpeedHz, FrequencyUnit.Hz) + .WithDataType(DataType.Double64Bit) + .WithConversion("rad/s", "%6.2f") + .WithRange(min: 0, max: 200), + }); + + WriteRamp(session, built[0].ChannelId, TemperatureHz, TemperatureSampleCount, Temperature); + WriteRamp(session, built[1].ChannelId, PressureHz, PressureSampleCount, _ => PressureConstant); + WriteRamp(session, built[2].ChannelId, SpeedHz, SpeedSampleCount, Speed); + + // Finalise the recording so all data and metadata are persisted to disk. + session.Flush(); + session.EndData(); + } + + private static void WriteRamp(Session session, uint channelId, int hz, int sampleCount, Func value) + { + var interval = new Frequency(hz, FrequencyUnit.Hz).ToInterval(); + for (var i = 0; i < sampleCount; i++) + { + var timestamp = StartTimeNs + i * interval; + session.AddChannelData(channelId, timestamp, 1, BitConverter.GetBytes(value(i))); + } + } +} diff --git a/tools/SnippetRunner/Program.cs b/tools/SnippetRunner/Program.cs index 1ac5eb7..f843c69 100644 --- a/tools/SnippetRunner/Program.cs +++ b/tools/SnippetRunner/Program.cs @@ -61,7 +61,6 @@ var unit = "(unknown)"; var format = ""; - // TODO: Verify — GetConversion may be on ConfigurationSet or Session try { var conversion = session.GetConversion(conversionName); From 0a904aee9c922a3263b37ff0dd4e5f790d9811c9 Mon Sep 17 00:00:00 2001 From: Federica Donald Date: Mon, 15 Jun 2026 12:24:25 +0000 Subject: [PATCH 6/7] Removing Automation API to be covered in that example code instead --- COOKBOOK.md | 1 - projects/README.md | 1 - projects/python/atlas_automation/README.md | 48 ----- .../atlas_automation/atlas_controller.py | 176 ------------------ .../python/atlas_automation/requirements.txt | 1 - 5 files changed, 227 deletions(-) delete mode 100644 projects/python/atlas_automation/README.md delete mode 100644 projects/python/atlas_automation/atlas_controller.py delete mode 100644 projects/python/atlas_automation/requirements.txt diff --git a/COOKBOOK.md b/COOKBOOK.md index 5cc6f4d..0ca09ba 100644 --- a/COOKBOOK.md +++ b/COOKBOOK.md @@ -89,7 +89,6 @@ For complete, buildable applications, see [projects/](projects/). | Monitor a live session in real time? | [LiveDashboard](projects/csharp/LiveDashboard/) | | Create custom .NET functions? | [FunctionLibrary](projects/csharp/FunctionLibrary/) | | Browse and export sessions from Python? | [session_explorer](projects/python/session_explorer/) | -| Control ATLAS from Python? | [atlas_automation](projects/python/atlas_automation/) | | Analyse sessions in MATLAB? | [session_analysis](projects/matlab/session_analysis/) | ### All C# projects diff --git a/projects/README.md b/projects/README.md index 230c0ee..61820be 100644 --- a/projects/README.md +++ b/projects/README.md @@ -28,7 +28,6 @@ dotnet build SqlRace.Examples.sln | Project | Description | |---------|-------------| | [session_explorer](python/session_explorer/) | Interactive CLI for browsing sessions and exporting to CSV/pickle | -| [atlas_automation](python/atlas_automation/) | ATLAS Automation API controller (connection pattern, stubbed) | ## MATLAB diff --git a/projects/python/atlas_automation/README.md b/projects/python/atlas_automation/README.md deleted file mode 100644 index 56606ee..0000000 --- a/projects/python/atlas_automation/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# ATLAS Automation - -Demonstrates controlling ATLAS 10 via the Automation API from Python. - -## What is the Automation API? - -ATLAS 10 exposes an automation interface that allows external applications to: -- Launch and connect to ATLAS instances -- Load SQL Race sessions into workbook sets -- Control the cursor position and timebase -- Query session and display information - -## Prerequisites - -- Python 3.8+ (64-bit) -- ATLAS 10 installed -- SQL Race .NET assemblies - -## Setup - -```bash -cd projects/python/atlas_automation -python -m venv .venv -source .venv/bin/activate # or .venv\Scripts\activate on Windows -pip install -r requirements.txt -``` - -Set the `SQLRACE_DLL_PATH` and `ATLAS_EXE_PATH` environment variables to match your installation, or edit the defaults at the top of `atlas_controller.py`. - -## Usage - -```bash -python atlas_controller.py [--connection-string CS] -``` - -## Current Status - -The Automation API calls are **stubbed with TODO comments** since they require a running ATLAS instance and the exact API signatures vary by ATLAS version. The structure demonstrates the correct pattern: - -1. Launch ATLAS (or connect to a running instance) -2. Connect via `ApplicationServiceClient` -3. Load a session -4. Control the cursor -5. Disconnect - -## Integration with SQL Race - -This tool uses the SQL Race API to verify the session exists before attempting to load it in ATLAS. This two-step pattern (verify in SQL Race, then load in ATLAS) is the recommended approach. diff --git a/projects/python/atlas_automation/atlas_controller.py b/projects/python/atlas_automation/atlas_controller.py deleted file mode 100644 index 6dd627f..0000000 --- a/projects/python/atlas_automation/atlas_controller.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env python3 -""" -ATLAS Automation Controller -============================ - -Demonstrates controlling ATLAS 10 via the Automation API: -launching ATLAS, loading a session, and controlling the cursor. - -Usage: - python atlas_controller.py [--connection-string CS] - -Prerequisites: - pip install pythonnet - ATLAS 10 must be installed. -""" - -import argparse -import subprocess -import sys -import time -import threading -import os -import tempfile -from pathlib import Path - -# ─── pythonnet CoreCLR initialisation ───────────────────────── -import json - -SQLRACE_DLL_PATH = os.environ.get("SQLRACE_DLL_PATH", r"C:\Program Files\McLaren Applied Technologies\ATLAS 10\MESL.SqlRace.Domain.dll") -ATLAS_EXE_PATH = os.environ.get("ATLAS_EXE_PATH", r"C:\Program Files\Motion Applied Technologies\ATLAS 10\ATLAS.exe") -ATLAS_DIR = os.path.dirname(SQLRACE_DLL_PATH) - -_runtime_cfg = os.path.join(tempfile.gettempdir(), "sqlrace_python.runtimeconfig.json") -with open(_runtime_cfg, "w") as _f: - json.dump({"runtimeOptions": {"tfm": "net8.0", "frameworks": [ - {"name": "Microsoft.NETCore.App", "version": "8.0.0"}, - {"name": "Microsoft.WindowsDesktop.App", "version": "8.0.0"}], - "additionalProbingPaths": [ATLAS_DIR]}}, _f) - -from pythonnet import load -load("coreclr", runtime_config=_runtime_cfg) - -import clr -clr.AddReference(SQLRACE_DLL_PATH) -clr.AddReference(os.path.join(ATLAS_DIR, "MAT.OCS.Core.dll")) - -from MESL.SqlRace.Domain import Core, SessionManager -from MAT.OCS.Core import SessionKey - - -class AtlasController: - """Controls an ATLAS 10 instance via the Automation API.""" - - def __init__(self): - self._lock = threading.Lock() - self._process = None - # TODO: Verify — ApplicationServiceClient API for ATLAS Automation - self._client = None - - def launch_atlas(self): - """Launch ATLAS 10 as a subprocess.""" - if not os.path.exists(ATLAS_EXE_PATH): - print(f"ATLAS not found at: {ATLAS_EXE_PATH}") - print("Update ATLAS_EXE_PATH to match your installation.") - return False - - print(f"Launching ATLAS from: {ATLAS_EXE_PATH}") - self._process = subprocess.Popen([ATLAS_EXE_PATH]) - print(f" PID: {self._process.pid}") - - # Wait for ATLAS to be ready - print(" Waiting for ATLAS to initialise...") - time.sleep(10) # TODO: Replace with proper readiness check - return True - - def connect(self): - """Connect to ATLAS via the Automation API.""" - with self._lock: - # TODO: Verify — ApplicationServiceClient connection API - # self._client = ApplicationServiceClient() - # self._client.Connect() - print(" Connected to ATLAS Automation API") - print(" (Connection is stubbed — see TODO comments for real API)") - - def load_session(self, session_key, connection_string): - """Load a SQL Race session into ATLAS.""" - with self._lock: - # TODO: Verify — API for loading a session into a workbook set - # self._client.LoadSession(session_key, connection_string) - print(f" Loading session: {session_key}") - print(f" Connection: {connection_string}") - print(" (Session loading is stubbed — see TODO comments)") - - def set_cursor(self, timestamp_ns): - """Move the ATLAS cursor to a specific timestamp.""" - with self._lock: - # TODO: Verify — API for cursor control - # self._client.SetCursorPosition(timestamp_ns) - time_s = timestamp_ns / 1e9 - print(f" Cursor set to: {timestamp_ns} ns ({time_s:.3f} s from midnight)") - print(" (Cursor control is stubbed — see TODO comments)") - - def get_session_info(self): - """Get information about the currently loaded session.""" - with self._lock: - # TODO: Verify — API for session/timebase information - print(" Session info request (stubbed)") - return None - - def close(self): - """Disconnect from ATLAS.""" - with self._lock: - if self._client is not None: - # self._client.Disconnect() - self._client = None - print(" Disconnected from ATLAS") - - -def main(): - parser = argparse.ArgumentParser(description="ATLAS Automation Controller") - parser.add_argument("session_guid", help="Session GUID to load") - parser.add_argument("--connection-string", default=None, help="SQL Race connection string") - args = parser.parse_args() - - # Resolve connection string - connection_string = args.connection_string - if not connection_string: - connection_string = os.environ.get("SQLRACE_CONNECTION_STRING") - if not connection_string: - db_path = str(Path(tempfile.gettempdir()) / "sqlrace-examples.ssn2") - connection_string = f"DbEngine=SQLite;Data Source={db_path};" - - Core.LicenceProgramName = "SQLRace" - Core.Initialize() - - # Verify session exists - session_key = SessionKey.Parse(args.session_guid) - session_manager = SessionManager.CreateSessionManager() - client_session = session_manager.Load(session_key, connection_string) - session = client_session.Session - - print(f"Session verified: {session.Identifier}") - print(f" Start: {session.StartTime} ns") - print(f" End: {session.EndTime} ns") - print(f" Parameters: {session.Parameters.Count}") - - client_session.Dispose() - - # Control ATLAS - controller = AtlasController() - - print("\n--- ATLAS Automation Demo ---") - print("(Automation API calls are stubbed with TODO comments)") - print() - - # In production, these would control a real ATLAS instance: - # controller.launch_atlas() - # controller.connect() - # controller.load_session(args.session_guid, connection_string) - # controller.set_cursor(session.StartTime) - # controller.get_session_info() - # controller.close() - - controller.connect() - controller.load_session(args.session_guid, connection_string) - - midpoint = (session.StartTime + session.EndTime) // 2 - controller.set_cursor(midpoint) - controller.get_session_info() - controller.close() - - print("\nDone.") - - -if __name__ == "__main__": - main() diff --git a/projects/python/atlas_automation/requirements.txt b/projects/python/atlas_automation/requirements.txt deleted file mode 100644 index 840fd03..0000000 --- a/projects/python/atlas_automation/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pythonnet>=3.0.0 From e17fec0971c6054c62c33539571c7d8f822b0a37 Mon Sep 17 00:00:00 2001 From: Federica Donald Date: Wed, 1 Jul 2026 16:14:23 +0000 Subject: [PATCH 7/7] Responding to comments --- nuget.config | 6 +++--- tools/SnippetRunner/Program.cs.bak | 22 ---------------------- 2 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 tools/SnippetRunner/Program.cs.bak diff --git a/nuget.config b/nuget.config index 6be70d8..4e86189 100644 --- a/nuget.config +++ b/nuget.config @@ -2,14 +2,14 @@ - + - + - + \ No newline at end of file diff --git a/tools/SnippetRunner/Program.cs.bak b/tools/SnippetRunner/Program.cs.bak deleted file mode 100644 index d077272..0000000 --- a/tools/SnippetRunner/Program.cs.bak +++ /dev/null @@ -1,22 +0,0 @@ -using MAT.OCS.Core; -using MESL.SqlRace.Domain; -using MESL.SqlRace.Domain.Collections; - -Core.LicenceProgramName = "SQLRace"; -Core.Initialize(); - -Console.WriteLine("ItemEventArgs props:"); -foreach (var p in typeof(ItemEventArgs).GetProperties()) - Console.WriteLine(" " + p.Name + " : " + p.PropertyType.Name); -Console.WriteLine(); -Console.WriteLine("LapEventArgs props:"); -foreach (var p in typeof(LapEventArgs).GetProperties()) - Console.WriteLine(" " + p.Name + " : " + p.PropertyType.Name); -Console.WriteLine(); -Console.WriteLine("Lap props:"); -foreach (var p in typeof(Lap).GetProperties()) - Console.WriteLine(" " + p.Name + " : " + p.PropertyType.Name); -Console.WriteLine(); -Console.WriteLine("SessionEventArgs props:"); -foreach (var p in typeof(SessionEventArgs).GetProperties()) - Console.WriteLine(" " + p.Name + " : " + p.PropertyType.Name);