Skip to content

Commit 3d672db

Browse files
marcschierCopilot
andcommitted
UaLens: Avalonia desktop client for OPC UA
Adds a new desktop application UaLens (Applications/Opc.Ua.Lens) — an Avalonia 11 workbench for browsing, subscribing to and operating an OPC UA server. The app is plug-in based, with a shared Connection panel and address-space tree driving five built-in plug-in tab types: * Subscription — multi-tab live notification scope with six rendering modes (Dots / Bars / Lines / Signal / Histogram / Heatmap) on a custom AnimationCanvas, plus the diagnostic header (seq, missing / republish / dropped, value counters). * Historian — Raw/Modified, Processed-Aggregate, At-Time read modes with UtcDateTimePicker composites, a RangeDialog, inline At-Time edit/save sentinel rows, SharedSizeGroup-resizable result columns, a ScottPlot line chart and CSV export. * Event View — per-tab classic subscription, severity-threshold filter, per-event-type field-selection driven by an in-dialog BrowsePickerDialog event-type chooser, pause / clear / details tree. * Performance — benchmark runner with rate slider, TimeSpan-style [N] [Unit] duration composite, throughput chart, latency histogram with percentile statistics and CSV export. * GDS Push / GDS Management — secondary-session piggyback on the main connection when it is SignAndEncrypt; auto-prompt via a shared picker when the outer session is unsuitable; AdminCredentialsRequired reactive prompt kept. Shared building blocks: * Connection/EndpointCredentialsPicker unifies Discovery → EndpointPickerDialog → CredentialsDialog and is consumed by the Connection panel and both GDS plug-ins. * Connection/DataValueCodec + Views/EncodingPickerDialog + Views/EncodedValueIO provide Binary / XML / JSON encode + decode for DataValue and Variant via the SDK encoders, powering Write Value's Import-from-file, Method Call's per-argument file import, and the address-space Export-value-to-file context menu. * Views/BrowsePickerDialog (lazy tree, NodeClass / ReferenceTypeId filter, async predicate) + Views/FlattenedBrowseDialog (live recursive flat browse with progress) deliver the node-pick fallback used by Historian, Performance and Event-source flows. * Address-space context menu with class-aware entries: Add Item, Add Recursively, Call Method, Write Value, Read history…, Show Events…, Perf…, Export value to file…. Connection plumbing: * Connection/ConnectionService owns the ManagedSession, the certificate validator hook-up (currently auto-accepting untrusted certs while the new ICertificateValidatorEx surface stabilises) and the per-tab subscription adapters. * Connected state surfaces a "Change ▾" flyout with Disconnect, Change User (credentials-only Session.UpdateSessionAsync) and Reconnect (Session.ReconnectAsync). * MainViewModel.IsAddressSpaceVisible mirrors the View menu toggle so plug-ins can short-circuit when the live tree is visible and a suitable node is already selected. * MainViewModel.AddPluginAsync(kind, seedEventSource?, seedPickTarget?) lets the address-space context-menu shortcuts create a new tab pre-bound to the right-clicked node (Historian target, EventView source via EventViewPlugin.SeedSourceAsync, or Performance PickTarget invocation). Build hygiene: * Nullable reference types enforced project-wide (<WarningsAsErrors>nullable</WarningsAsErrors>). * Every analyzer bucket cleared; CA2007 swept (await-using sites rewritten to the MS-doc-recommended block form). * UTF-8 mojibake cleaned across files that had been round-tripped through CP-1252. * dotnet build -c Release -f net10.0 clean (0 / 0). * dotnet format --verify-no-changes exit 0. Also moves the upstream "Applications/McpServer" naming to the "Applications/Opc.Ua.Mcp" folder UaLens already uses, so the project file, sign lists, build glob, agent-instruction note, the McpServer docs and the README all reference a single canonical path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 96f8964 commit 3d672db

170 files changed

Lines changed: 26604 additions & 20 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.azurepipelines/preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ jobs:
8484
dir /b /s Stack\Opc.Ua*.dll > .\list.txt
8585
dir /b /s Tools\Opc.Ua*.dll >> .\list.txt
8686
dir /b /s Libraries\Opc.Ua*.dll >> .\list.txt
87-
dir /b /s Applications\McpServer\bin\Opc.Ua*.dll >> .\list.txt
87+
dir /b /s Applications\Opc.Ua.Mcp\bin\Opc.Ua*.dll >> .\list.txt
8888
dir /b /s .azurepipelines\*.* >> .\list.txt
8989
type .\list.txt
9090
- task: CmdLine@2

.azurepipelines/signlistDebug.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@ Libraries\Opc.Ua.PubSub\bin\Debug\net48\Opc.Ua.PubSub.dll
7878
Libraries\Opc.Ua.PubSub\bin\Debug\net8.0\Opc.Ua.PubSub.dll
7979
Libraries\Opc.Ua.PubSub\bin\Debug\net9.0\Opc.Ua.PubSub.dll
8080
Libraries\Opc.Ua.PubSub\bin\Debug\net10.0\Opc.Ua.PubSub.dll
81-
Applications\McpServer\bin\Debug\net10.0\Opc.Ua.Mcp.dll
81+
Applications\Opc.Ua.Mcp\bin\Debug\net10.0\Opc.Ua.Mcp.dll

.azurepipelines/signlistRelease.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@ Libraries\Opc.Ua.PubSub\bin\Release\net48\Opc.Ua.PubSub.dll
7878
Libraries\Opc.Ua.PubSub\bin\Release\net8.0\Opc.Ua.PubSub.dll
7979
Libraries\Opc.Ua.PubSub\bin\Release\net9.0\Opc.Ua.PubSub.dll
8080
Libraries\Opc.Ua.PubSub\bin\Release\net10.0\Opc.Ua.PubSub.dll
81-
Applications\McpServer\bin\Release\net10.0\Opc.Ua.Mcp.dll
81+
Applications\Opc.Ua.Mcp\bin\Release\net10.0\Opc.Ua.Mcp.dll

.github/agents/opcua-interop-tester.agent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ You are an expert OPC UA interoperability test engineer. Your role is to systema
99

1010
## MCP Tools — Primary Test Method
1111

12-
This repository includes an **OPC UA MCP Server** (`Applications/McpServer`) that exposes all OPC UA Part 4 services as MCP tools. **Always prefer using the MCP tools over writing custom C# test code** — they are faster, require no compilation, and cover all standard services.
12+
This repository includes an **OPC UA MCP Server** (`Applications/Opc.Ua.Mcp`) that exposes all OPC UA Part 4 services as MCP tools. **Always prefer using the MCP tools over writing custom C# test code** — they are faster, require no compilation, and cover all standard services.
1313

1414
### When to Use MCP Tools
1515
- **Always** for initial connection, browsing, reading, writing, method calling, subscription testing

Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,19 +263,24 @@
263263
<MaxQueryContinuationPoints>10</MaxQueryContinuationPoints>
264264
<MaxHistoryContinuationPoints>100</MaxHistoryContinuationPoints>
265265
<MaxRequestAge>600000</MaxRequestAge>
266-
<MinPublishingInterval>50</MinPublishingInterval>
266+
<MinPublishingInterval>1</MinPublishingInterval>
267267
<MaxPublishingInterval>3600000</MaxPublishingInterval>
268-
<PublishingResolution>50</PublishingResolution>
268+
<PublishingResolution>1</PublishingResolution>
269269
<MaxSubscriptionLifetime>3600000</MaxSubscriptionLifetime>
270270
<MaxMessageQueueSize>100</MaxMessageQueueSize>
271271
<MaxNotificationQueueSize>100</MaxNotificationQueueSize>
272272
<MaxNotificationsPerPublish>1000</MaxNotificationsPerPublish>
273273
<MinMetadataSamplingInterval>1000</MinMetadataSamplingInterval>
274274
<AvailableSamplingRates>
275275
<SamplingRateGroup>
276-
<Start>5</Start>
277-
<Increment>5</Increment>
278-
<Count>20</Count>
276+
<Start>1</Start>
277+
<Increment>1</Increment>
278+
<Count>10</Count>
279+
</SamplingRateGroup>
280+
<SamplingRateGroup>
281+
<Start>10</Start>
282+
<Increment>10</Increment>
283+
<Count>9</Count>
279284
</SamplingRateGroup>
280285
<SamplingRateGroup>
281286
<Start>100</Start>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/* ========================================================================
2+
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
3+
* OPC Foundation MIT License 1.00
4+
* ======================================================================*/
5+
6+
using System;
7+
using System.Diagnostics;
8+
using System.Diagnostics.Metrics;
9+
using System.Linq;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using Microsoft.Extensions.Logging;
13+
using Microsoft.Extensions.Logging.Abstractions;
14+
using Opc.Ua;
15+
using UaLens.Connection;
16+
using UaLens.Subscriptions;
17+
18+
namespace UaLens;
19+
20+
/// <summary>
21+
/// Adapter-lifecycle race stress test: spawns many tasks that create +
22+
/// forget adapters concurrently with a disconnect. Asserts that
23+
/// <see cref="ConnectionService.ForgetAdapter"/>'s new bool return value
24+
/// correctly differentiates "you own disposal" vs "Disconnect already
25+
/// owned it" so no zombies leak through either path.
26+
/// </summary>
27+
internal static class AdapterRaceProbe
28+
{
29+
public static async Task<int> RunAsync(string endpointUrl, CancellationToken ct = default)
30+
{
31+
var telemetry = new ConsoleTelemetry();
32+
Console.WriteLine("== AdapterRace probe ==");
33+
Console.WriteLine($" endpoint: {endpointUrl}");
34+
35+
var conn = new ConnectionService(telemetry);
36+
await using (conn.ConfigureAwait(false))
37+
{
38+
await conn.ConnectAsync(new ConnectionOptions
39+
{
40+
EndpointUrl = endpointUrl,
41+
Engine = SubscriptionEngineKind.ChannelV2
42+
}, ct).ConfigureAwait(false);
43+
44+
const int N = 24;
45+
var adapters = new ISubscriptionAdapter[N];
46+
for (int i = 0; i < N; i++)
47+
{
48+
adapters[i] = conn.CreateAdapter();
49+
}
50+
51+
// Pre-disconnect: each adapter should be forget-able exactly once.
52+
int forgottenCount = 0;
53+
for (int i = 0; i < N / 2; i++)
54+
{
55+
if (conn.ForgetAdapter(adapters[i]))
56+
{
57+
forgottenCount++;
58+
await adapters[i].DisposeAsync().ConfigureAwait(false);
59+
}
60+
}
61+
if (forgottenCount != N / 2)
62+
{
63+
Console.WriteLine($"FAIL: expected {N / 2} ForgetAdapter=true, got {forgottenCount}");
64+
return 1;
65+
}
66+
67+
// Disconnect — should dispose the remaining N/2 adapters that are
68+
// still tracked; ForgetAdapter for any of those AFTER disconnect
69+
// must return false (already disposed).
70+
Task disconnectTask = conn.DisconnectAsync();
71+
72+
// Race: try ForgetAdapter on the not-yet-forgotten adapters
73+
// concurrently with the disconnect. Outcome must be EITHER:
74+
// - ForgetAdapter returns true → caller disposes (no double dispose).
75+
// - ForgetAdapter returns false → DisconnectInternalAsync is/was disposing.
76+
int doubleDisposalAttempts = 0;
77+
var forgotAfter = new bool[N];
78+
Parallel.For(N / 2, N, i =>
79+
{
80+
forgotAfter[i] = conn.ForgetAdapter(adapters[i]);
81+
if (forgotAfter[i])
82+
{
83+
try
84+
{
85+
adapters[i].DisposeAsync().AsTask().Wait();
86+
}
87+
catch (Exception)
88+
{
89+
Interlocked.Increment(ref doubleDisposalAttempts);
90+
}
91+
}
92+
});
93+
94+
await disconnectTask.ConfigureAwait(false);
95+
96+
Console.WriteLine($" forgot pre-disconnect : {forgottenCount}");
97+
Console.WriteLine($" forgot during disconn : {forgotAfter.Skip(N / 2).Count(b => b)}");
98+
Console.WriteLine($" double-dispose excepts: {doubleDisposalAttempts}");
99+
100+
if (doubleDisposalAttempts > 0)
101+
{
102+
Console.WriteLine("FAIL: double-disposal detected.");
103+
return 1;
104+
}
105+
Console.WriteLine("ADAPTER RACE PROBE PASS");
106+
return 0;
107+
}
108+
}
109+
110+
private sealed class ConsoleTelemetry : ITelemetryContext
111+
{
112+
public ILoggerFactory LoggerFactory { get; } = NullLoggerFactory.Instance;
113+
public Meter CreateMeter() => new("UaLens.AdapterRaceProbe");
114+
public ActivitySource ActivitySource { get; } = new("UaLens.AdapterRaceProbe");
115+
}
116+
}

Applications/Opc.Ua.Lens/App.axaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Application xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
x:Class="UaLens.App"
4+
RequestedThemeVariant="Dark">
5+
<Application.Styles>
6+
<FluentTheme />
7+
</Application.Styles>
8+
</Application>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* ========================================================================
2+
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
3+
* OPC Foundation MIT License 1.00
4+
* ======================================================================*/
5+
6+
using Avalonia;
7+
using Avalonia.Controls.ApplicationLifetimes;
8+
using Avalonia.Markup.Xaml;
9+
using UaLens.Views;
10+
11+
namespace UaLens;
12+
13+
internal sealed partial class App : Application
14+
{
15+
public override void Initialize()
16+
{
17+
AvaloniaXamlLoader.Load(this);
18+
}
19+
20+
public override void OnFrameworkInitializationCompleted()
21+
{
22+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
23+
{
24+
desktop.MainWindow = new MainWindow();
25+
}
26+
base.OnFrameworkInitializationCompleted();
27+
}
28+
}
53 KB
Binary file not shown.
27.7 KB
Loading

0 commit comments

Comments
 (0)