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/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 new file mode 100644 index 0000000..0574549 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,41 @@ +name: Build + +on: + push: + branches: [master] + pull_request: + branches: [master] + +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: ${{ secrets.PACKAGES_USERNAME }} + NUGET_AUTH_TOKEN: ${{ secrets.PACKAGES_PATTOKEN }} + + - 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: ${{ 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 + + # 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. 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 new file mode 100644 index 0000000..72f989d --- /dev/null +++ b/.github/workflows/validate-notebooks.yml @@ -0,0 +1,48 @@ +name: Validate Notebooks + +on: + push: + branches: [master] + paths: ['notebooks/**'] + pull_request: + branches: [master] + 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..0ca09ba --- /dev/null +++ b/COOKBOOK.md @@ -0,0 +1,128 @@ +# 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_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_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) | [`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) | — | [`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 | 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# | 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# | 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 + +| 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# | 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) + +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/) | +| 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 7650cf3..0000000 Binary files a/MESL.SqlRace.Examples.Sessions.CSharp/System.Windows.Forms.DataVisualization.dll and /dev/null differ 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 0000000..cc52506 Binary files /dev/null and b/images/ATLAS_logo_GR.png differ diff --git a/images/atlaslogo.png b/images/atlaslogo.png deleted file mode 100644 index 79c0c6e..0000000 Binary files a/images/atlaslogo.png and /dev/null differ 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..9ed18d7 --- /dev/null +++ b/notebooks/README.md @@ -0,0 +1,59 @@ +ATLAS + +# 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 0000000..4b5c523 Binary files /dev/null and b/notebooks/scenarios/data/durability-cycle1.ssn2 differ diff --git a/notebooks/scenarios/data/durability-cycle2.ssn2 b/notebooks/scenarios/data/durability-cycle2.ssn2 new file mode 100644 index 0000000..1687c09 Binary files /dev/null and b/notebooks/scenarios/data/durability-cycle2.ssn2 differ diff --git a/notebooks/scenarios/data/durability-cycle3.ssn2 b/notebooks/scenarios/data/durability-cycle3.ssn2 new file mode 100644 index 0000000..d023e4c Binary files /dev/null and b/notebooks/scenarios/data/durability-cycle3.ssn2 differ 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 0000000..9f1dbfb Binary files /dev/null and b/notebooks/scenarios/data/flight-test-manoeuvre-extraction.ssn2 differ diff --git a/notebooks/scenarios/data/motorsport-lap-analysis.ssn2 b/notebooks/scenarios/data/motorsport-lap-analysis.ssn2 new file mode 100644 index 0000000..d537c5f Binary files /dev/null and b/notebooks/scenarios/data/motorsport-lap-analysis.ssn2 differ diff --git a/notebooks/scenarios/data/turbine-week1.ssn2 b/notebooks/scenarios/data/turbine-week1.ssn2 new file mode 100644 index 0000000..02a4361 Binary files /dev/null and b/notebooks/scenarios/data/turbine-week1.ssn2 differ diff --git a/notebooks/scenarios/data/turbine-week2.ssn2 b/notebooks/scenarios/data/turbine-week2.ssn2 new file mode 100644 index 0000000..c3ba3c0 Binary files /dev/null and b/notebooks/scenarios/data/turbine-week2.ssn2 differ diff --git a/notebooks/scenarios/data/turbine-week3.ssn2 b/notebooks/scenarios/data/turbine-week3.ssn2 new file mode 100644 index 0000000..14e97c4 Binary files /dev/null and b/notebooks/scenarios/data/turbine-week3.ssn2 differ 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..4e86189 --- /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..61820be --- /dev/null +++ b/projects/README.md @@ -0,0 +1,36 @@ +ATLAS + +# 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 | + +## 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..fb21059 --- /dev/null +++ b/projects/csharp/FunctionLibrary/RollingAverageFunction.cs @@ -0,0 +1,112 @@ +using System.ComponentModel.Composition; +using MESL.SqlRace.Domain.Functions; +using MESL.SqlRace.Domain.Functions.DotNet; +using MESL.SqlRace.Enumerators; + +namespace FunctionLibrary; + +/// +/// 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. +/// 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 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"; + + /// + /// 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) + { + 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); + + 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.Count > 0) + { + throw new InvalidOperationException( + $"RollingAverageFunction build failed: {buildResult.Errors.FirstOrDefault()?.ErrorText}"); + } + } + + /// + /// 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) + { + 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 new file mode 100644 index 0000000..7903ebd --- /dev/null +++ b/projects/csharp/FunctionLibrary/StatisticsFunction.cs @@ -0,0 +1,97 @@ +using System.ComponentModel.Composition; +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 over multiple inputs. +/// +/// +/// .NET functions are discovered via MEF [Export] and initialised by the function manager. +/// 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 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"; + + /// + /// 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) + { + 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(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.Count > 0) + { + throw new InvalidOperationException( + $"StatisticsFunction build failed: {buildResult.Errors.FirstOrDefault()?.ErrorText}"); + } + } + + /// + /// 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) + { + 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 new file mode 100644 index 0000000..a46a5fc --- /dev/null +++ b/projects/csharp/FunctionLibrary/UnitConverterFunction.cs @@ -0,0 +1,92 @@ +using System.ComponentModel.Composition; +using MESL.SqlRace.Domain.Functions; +using MESL.SqlRace.Domain.Functions.DotNet; +using MESL.SqlRace.Enumerators; + +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. +/// 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 OutputName = "TemperatureFahrenheit"; + private const string OutputGroup = "Calculated"; + private const string OutputIdentifier = OutputName + ":" + OutputGroup; + + /// + public string Name => "UnitConverter"; + + /// + /// 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) + { + 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); + + 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.Count > 0) + { + throw new InvalidOperationException( + $"UnitConverterFunction build failed: {buildResult.Errors.FirstOrDefault()?.ErrorText}"); + } + } + + /// + /// Converts each Celsius input sample to Fahrenheit. Called once per data request + /// with a batch of samples in . + /// + public void Execute(IExecutionContext context) + { + 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/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..8d1278b --- /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 ─────────────────────────────── +// 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}"); + +// ─── 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..ca8853b --- /dev/null +++ b/projects/csharp/StandaloneRecorder/Program.cs @@ -0,0 +1,160 @@ +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 ───────────────────────────────────── +// 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 ─────────────────── +// 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(); + +// 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)"); +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 +{ + // 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 new file mode 100644 index 0000000..f8e3a47 --- /dev/null +++ b/projects/csharp/StandaloneRecorder/README.md @@ -0,0 +1,75 @@ +# 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 as part of the ADS streaming flow + +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 + +- .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 0000000..258580b Binary files /dev/null and b/projects/matlab/session_analysis/session_results.mat differ 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/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..90fb346 100644 --- a/readme.md +++ b/readme.md @@ -1,30 +1,111 @@ -


+ATLAS -# **SQLRace API Sample Code**. +# SQL Race API Examples -Collection of example code and best practices to use the SQLRace API. +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. -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. +SQL Race is used across **motorsport, aerospace, automotive, energy, and defence** for recording and analysing sensor data from test sessions. -SQLRace API is available as a Nuget package to registered users from our **[Nuget Repository](https://github.com/mat-docs/packages)**. +## Quick Start -See the [API Documentation](https://mat-docs.github.io/) +```bash +# 1. Create a new .NET 8 console project +dotnet new console -n MyFirstSession +cd MyFirstSession -## .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. +# 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 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. +# 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 +``` -## Notes on upgrading MESL.SQLRace.API package from versions prior to 2.1.22127.1 +> **No server required.** All getting-started snippets default to a local SQLite file — no SQL Server, no configuration. -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: +## I want to... -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 +| 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) | +| 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. +## 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/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/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..56c4543 --- /dev/null +++ b/snippets/README.md @@ -0,0 +1,120 @@ +ATLAS + +# 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..a100fb2 --- /dev/null +++ b/snippets/csharp/configuration/introspect-session-config.cs @@ -0,0 +1,56 @@ +// ───────────────────────────────────────────────────────────── +// 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}"); +} + +// --- 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..72b3d43 --- /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 +// 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 +// ───────────────────────────────────────────────────────────── + +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..f843c69 --- /dev/null +++ b/snippets/csharp/session-management/parameter-unit-resolution.cs @@ -0,0 +1,81 @@ +// ───────────────────────────────────────────────────────────── +// 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 = ""; + + 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..f80c440 --- /dev/null +++ b/snippets/csharp/session-management/session-association.cs @@ -0,0 +1,56 @@ +// ───────────────────────────────────────────────────────────── +// 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 --- +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}"); + +// --- Load the associated sessions alongside the primary --- +var associates = sessionManager.LoadAssociatesForSession(primaryKey, connectionString); +Console.WriteLine($" Associated sessions loaded: {associates.Count}"); 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..326d6d7 --- /dev/null +++ b/snippets/matlab/README.md @@ -0,0 +1,102 @@ +# 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 | + +## 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, 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/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/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/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/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/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/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.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/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 new file mode 100644 index 0000000..78f39fa --- /dev/null +++ b/snippets/matlab/getting-started/query_sessions.m @@ -0,0 +1,94 @@ +% ───────────────────────────────────────────────────────────── +% 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); + +% '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; + +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 create_session_write_data.m 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/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.'); 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/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/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/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/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..5d112ea --- /dev/null +++ b/tests/SqlRace.Examples.Tests/SqlRace.Examples.Tests.csproj @@ -0,0 +1,25 @@ + + + + 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..0e593bc --- /dev/null +++ b/tests/test-data/README.md @@ -0,0 +1,22 @@ +# 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`. + +## 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/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/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 new file mode 100644 index 0000000..f843c69 --- /dev/null +++ b/tools/SnippetRunner/Program.cs @@ -0,0 +1,81 @@ +// ───────────────────────────────────────────────────────────── +// 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 = ""; + + 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/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 + + + + +