diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx
index 13dbf2f..4217fd8 100644
--- a/ZB.MOM.WW.OtOpcUa.slnx
+++ b/ZB.MOM.WW.OtOpcUa.slnx
@@ -15,6 +15,7 @@
+
@@ -43,6 +44,7 @@
+
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/IFrameHandler.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/IFrameHandler.cs
new file mode 100644
index 0000000..75cef97
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/IFrameHandler.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Ipc;
+
+///
+/// Dispatches a single IPC frame to the backend. Implementations own the FOCAS session
+/// state and translate request DTOs into Fwlib32 calls.
+///
+public interface IFrameHandler
+{
+ Task HandleAsync(FocasMessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct);
+
+ ///
+ /// Called once per accepted connection after the Hello handshake. Lets the handler
+ /// attach server-pushed event sinks (data-change notifications, runtime-status
+ /// changes) to the connection's . Returns an
+ /// the pipe server disposes when the connection closes —
+ /// backends use it to unsubscribe from their push sources.
+ ///
+ IDisposable AttachConnection(FrameWriter writer);
+
+ public sealed class NoopAttachment : IDisposable
+ {
+ public static readonly NoopAttachment Instance = new();
+ public void Dispose() { }
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/PipeAcl.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/PipeAcl.cs
new file mode 100644
index 0000000..aac29a1
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/PipeAcl.cs
@@ -0,0 +1,39 @@
+using System;
+using System.IO.Pipes;
+using System.Security.AccessControl;
+using System.Security.Principal;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Ipc;
+
+///
+/// Builds the for the FOCAS Host pipe. Same pattern as
+/// Galaxy.Host: only the configured OtOpcUa server principal SID gets
+/// ReadWrite | Synchronize; LocalSystem + Administrators are explicitly denied
+/// so a compromised service account on the same host can't escalate via the pipe.
+///
+public static class PipeAcl
+{
+ public static PipeSecurity Create(SecurityIdentifier allowedSid)
+ {
+ if (allowedSid is null) throw new ArgumentNullException(nameof(allowedSid));
+
+ var security = new PipeSecurity();
+
+ security.AddAccessRule(new PipeAccessRule(
+ allowedSid,
+ PipeAccessRights.ReadWrite | PipeAccessRights.Synchronize,
+ AccessControlType.Allow));
+
+ var localSystem = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null);
+ var admins = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);
+
+ if (allowedSid != localSystem)
+ security.AddAccessRule(new PipeAccessRule(localSystem, PipeAccessRights.FullControl, AccessControlType.Deny));
+ if (allowedSid != admins)
+ security.AddAccessRule(new PipeAccessRule(admins, PipeAccessRights.FullControl, AccessControlType.Deny));
+
+ security.SetOwner(allowedSid);
+
+ return security;
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/PipeServer.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/PipeServer.cs
new file mode 100644
index 0000000..582870e
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/PipeServer.cs
@@ -0,0 +1,152 @@
+using System;
+using System.IO.Pipes;
+using System.Security.Principal;
+using System.Threading;
+using System.Threading.Tasks;
+using MessagePack;
+using Serilog;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Ipc;
+
+///
+/// Accepts one client connection at a time on the FOCAS Host's named pipe with the
+/// strict ACL from . Verifies the peer SID + per-process shared
+/// secret before any RPC frame is accepted. Mirrors the Galaxy.Host pipe server byte for
+/// byte — different MessageKind enum, same negotiation semantics.
+///
+public sealed class PipeServer : IDisposable
+{
+ private readonly string _pipeName;
+ private readonly SecurityIdentifier _allowedSid;
+ private readonly string _sharedSecret;
+ private readonly ILogger _logger;
+ private readonly CancellationTokenSource _cts = new();
+ private NamedPipeServerStream? _current;
+
+ public PipeServer(string pipeName, SecurityIdentifier allowedSid, string sharedSecret, ILogger logger)
+ {
+ _pipeName = pipeName ?? throw new ArgumentNullException(nameof(pipeName));
+ _allowedSid = allowedSid ?? throw new ArgumentNullException(nameof(allowedSid));
+ _sharedSecret = sharedSecret ?? throw new ArgumentNullException(nameof(sharedSecret));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task RunOneConnectionAsync(IFrameHandler handler, CancellationToken ct)
+ {
+ using var linked = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, ct);
+ var acl = PipeAcl.Create(_allowedSid);
+
+ _current = new NamedPipeServerStream(
+ _pipeName,
+ PipeDirection.InOut,
+ maxNumberOfServerInstances: 1,
+ PipeTransmissionMode.Byte,
+ PipeOptions.Asynchronous,
+ inBufferSize: 64 * 1024,
+ outBufferSize: 64 * 1024,
+ pipeSecurity: acl);
+
+ try
+ {
+ await _current.WaitForConnectionAsync(linked.Token).ConfigureAwait(false);
+
+ if (!VerifyCaller(_current, out var reason))
+ {
+ _logger.Warning("FOCAS IPC caller rejected: {Reason}", reason);
+ _current.Disconnect();
+ return;
+ }
+
+ using var reader = new FrameReader(_current, leaveOpen: true);
+ using var writer = new FrameWriter(_current, leaveOpen: true);
+
+ var first = await reader.ReadFrameAsync(linked.Token).ConfigureAwait(false);
+ if (first is null || first.Value.Kind != FocasMessageKind.Hello)
+ {
+ _logger.Warning("FOCAS IPC first frame was not Hello; dropping");
+ return;
+ }
+
+ var hello = MessagePackSerializer.Deserialize(first.Value.Body);
+ if (!string.Equals(hello.SharedSecret, _sharedSecret, StringComparison.Ordinal))
+ {
+ await writer.WriteAsync(FocasMessageKind.HelloAck,
+ new HelloAck { Accepted = false, RejectReason = "shared-secret-mismatch" },
+ linked.Token).ConfigureAwait(false);
+ _logger.Warning("FOCAS IPC Hello rejected: shared-secret-mismatch");
+ return;
+ }
+
+ if (hello.ProtocolMajor != Hello.CurrentMajor)
+ {
+ await writer.WriteAsync(FocasMessageKind.HelloAck,
+ new HelloAck
+ {
+ Accepted = false,
+ RejectReason = $"major-version-mismatch-peer={hello.ProtocolMajor}-server={Hello.CurrentMajor}",
+ },
+ linked.Token).ConfigureAwait(false);
+ _logger.Warning("FOCAS IPC Hello rejected: major mismatch peer={Peer} server={Server}",
+ hello.ProtocolMajor, Hello.CurrentMajor);
+ return;
+ }
+
+ await writer.WriteAsync(FocasMessageKind.HelloAck,
+ new HelloAck { Accepted = true, HostName = Environment.MachineName },
+ linked.Token).ConfigureAwait(false);
+
+ using var attachment = handler.AttachConnection(writer);
+
+ while (!linked.Token.IsCancellationRequested)
+ {
+ var frame = await reader.ReadFrameAsync(linked.Token).ConfigureAwait(false);
+ if (frame is null) break;
+
+ await handler.HandleAsync(frame.Value.Kind, frame.Value.Body, writer, linked.Token).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ _current.Dispose();
+ _current = null;
+ }
+ }
+
+ public async Task RunAsync(IFrameHandler handler, CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ try { await RunOneConnectionAsync(handler, ct).ConfigureAwait(false); }
+ catch (OperationCanceledException) { break; }
+ catch (Exception ex) { _logger.Error(ex, "FOCAS IPC connection loop error — accepting next"); }
+ }
+ }
+
+ private bool VerifyCaller(NamedPipeServerStream pipe, out string reason)
+ {
+ try
+ {
+ pipe.RunAsClient(() =>
+ {
+ using var wi = WindowsIdentity.GetCurrent();
+ if (wi.User is null)
+ throw new InvalidOperationException("GetCurrent().User is null — cannot verify caller");
+ if (wi.User != _allowedSid)
+ throw new UnauthorizedAccessException(
+ $"caller SID {wi.User.Value} does not match allowed {_allowedSid.Value}");
+ });
+ reason = string.Empty;
+ return true;
+ }
+ catch (Exception ex) { reason = ex.Message; return false; }
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _current?.Dispose();
+ _cts.Dispose();
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/StubFrameHandler.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/StubFrameHandler.cs
new file mode 100644
index 0000000..2c28dac
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Ipc/StubFrameHandler.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MessagePack;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Ipc;
+
+///
+/// Placeholder handler that returns ErrorResponse{Code=not-implemented} for every
+/// FOCAS data-plane request. Exists so PR B can ship the pipe server + ACL + handshake
+/// plumbing before PR C moves the Fwlib32 calls. Heartbeats are handled fully so the
+/// supervisor's liveness detector stays happy.
+///
+public sealed class StubFrameHandler : IFrameHandler
+{
+ public Task HandleAsync(FocasMessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct)
+ {
+ if (kind == FocasMessageKind.Heartbeat)
+ {
+ var hb = MessagePackSerializer.Deserialize(body);
+ return writer.WriteAsync(FocasMessageKind.HeartbeatAck,
+ new HeartbeatAck
+ {
+ MonotonicTicks = hb.MonotonicTicks,
+ HostUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+ }, ct);
+ }
+
+ return writer.WriteAsync(FocasMessageKind.ErrorResponse,
+ new ErrorResponse
+ {
+ Code = "not-implemented",
+ Message = $"Kind {kind} is stubbed — Fwlib32 lift lands in PR C",
+ },
+ ct);
+ }
+
+ public IDisposable AttachConnection(FrameWriter writer) => IFrameHandler.NoopAttachment.Instance;
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Program.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Program.cs
new file mode 100644
index 0000000..6450747
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/Program.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Security.Principal;
+using System.Threading;
+using Serilog;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Ipc;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host;
+
+///
+/// Entry point for the OtOpcUaFocasHost Windows service / console host. The
+/// supervisor (Proxy-side) spawns this process per FOCAS driver instance and passes the
+/// pipe name, allowed-SID, and per-process shared secret as environment variables. In
+/// PR B the backend is — PR C swaps in the real
+/// Fwlib32-backed handler once the session state + STA thread move out of the .NET 10
+/// driver.
+///
+public static class Program
+{
+ public static int Main(string[] args)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Information()
+ .WriteTo.File(
+ @"%ProgramData%\OtOpcUa\focas-host-.log".Replace("%ProgramData%", Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)),
+ rollingInterval: RollingInterval.Day)
+ .CreateLogger();
+
+ try
+ {
+ var pipeName = Environment.GetEnvironmentVariable("OTOPCUA_FOCAS_PIPE") ?? "OtOpcUaFocas";
+ var allowedSidValue = Environment.GetEnvironmentVariable("OTOPCUA_ALLOWED_SID")
+ ?? throw new InvalidOperationException(
+ "OTOPCUA_ALLOWED_SID not set — the FOCAS Proxy supervisor must pass the server principal SID");
+ var sharedSecret = Environment.GetEnvironmentVariable("OTOPCUA_FOCAS_SECRET")
+ ?? throw new InvalidOperationException(
+ "OTOPCUA_FOCAS_SECRET not set — the FOCAS Proxy supervisor must pass the per-process secret at spawn time");
+
+ var allowedSid = new SecurityIdentifier(allowedSidValue);
+
+ using var server = new PipeServer(pipeName, allowedSid, sharedSecret, Log.Logger);
+ using var cts = new CancellationTokenSource();
+ Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
+
+ Log.Information("OtOpcUaFocasHost starting — pipe={Pipe} allowedSid={Sid}",
+ pipeName, allowedSidValue);
+
+ var handler = new StubFrameHandler();
+ Log.Warning("OtOpcUaFocasHost backend=stub — Fwlib32 lift lands in PR C");
+
+ server.RunAsync(handler, cts.Token).GetAwaiter().GetResult();
+
+ Log.Information("OtOpcUaFocasHost stopped cleanly");
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Log.Fatal(ex, "OtOpcUaFocasHost fatal");
+ return 2;
+ }
+ finally { Log.CloseAndFlush(); }
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.csproj b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.csproj
new file mode 100644
index 0000000..b9682f6
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.csproj
@@ -0,0 +1,40 @@
+
+
+
+ Exe
+ net48
+
+ x86
+ true
+ enable
+ latest
+ true
+ true
+ $(NoWarn);CS1591
+ ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host
+ OtOpcUa.Driver.FOCAS.Host
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests/IpcHandshakeIntegrationTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests/IpcHandshakeIntegrationTests.cs
new file mode 100644
index 0000000..b520c2a
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests/IpcHandshakeIntegrationTests.cs
@@ -0,0 +1,157 @@
+using System;
+using System.IO;
+using System.IO.Pipes;
+using System.Security.Principal;
+using System.Threading;
+using System.Threading.Tasks;
+using MessagePack;
+using Serilog;
+using Serilog.Core;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Ipc;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests
+{
+ ///
+ /// Direct FOCAS Host IPC handshake test. Drives through a
+ /// hand-rolled pipe client built on /
+ /// from FOCAS.Shared. Skipped on Administrator shells because PipeAcl denies
+ /// the BuiltinAdministrators group.
+ ///
+ [Trait("Category", "Integration")]
+ public sealed class IpcHandshakeIntegrationTests
+ {
+ private static bool IsAdministrator()
+ {
+ using var identity = WindowsIdentity.GetCurrent();
+ return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
+ }
+
+ private static async Task<(NamedPipeClientStream Stream, FrameReader Reader, FrameWriter Writer)>
+ ConnectAndHelloAsync(string pipeName, string secret, CancellationToken ct)
+ {
+ var stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
+ await stream.ConnectAsync(5_000, ct);
+
+ var reader = new FrameReader(stream, leaveOpen: true);
+ var writer = new FrameWriter(stream, leaveOpen: true);
+ await writer.WriteAsync(FocasMessageKind.Hello,
+ new Hello { PeerName = "test-client", SharedSecret = secret }, ct);
+
+ var ack = await reader.ReadFrameAsync(ct);
+ if (ack is null) throw new EndOfStreamException("no HelloAck");
+ if (ack.Value.Kind != FocasMessageKind.HelloAck)
+ throw new InvalidOperationException("unexpected first frame kind " + ack.Value.Kind);
+ var ackMsg = MessagePackSerializer.Deserialize(ack.Value.Body);
+ if (!ackMsg.Accepted) throw new UnauthorizedAccessException(ackMsg.RejectReason);
+
+ return (stream, reader, writer);
+ }
+
+ [Fact]
+ public async Task Handshake_with_correct_secret_succeeds_and_heartbeat_round_trips()
+ {
+ if (IsAdministrator()) return;
+
+ using var identity = WindowsIdentity.GetCurrent();
+ var sid = identity.User!;
+ var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}";
+ const string secret = "test-secret-2026";
+ Logger log = new LoggerConfiguration().CreateLogger();
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+
+ var server = new PipeServer(pipe, sid, secret, log);
+ var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token));
+
+ var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token);
+ using (stream)
+ using (reader)
+ using (writer)
+ {
+ await writer.WriteAsync(FocasMessageKind.Heartbeat,
+ new Heartbeat { MonotonicTicks = 42 }, cts.Token);
+
+ var hbAck = await reader.ReadFrameAsync(cts.Token);
+ hbAck.HasValue.ShouldBeTrue();
+ hbAck!.Value.Kind.ShouldBe(FocasMessageKind.HeartbeatAck);
+ MessagePackSerializer.Deserialize(hbAck.Value.Body).MonotonicTicks.ShouldBe(42L);
+ }
+
+ cts.Cancel();
+ try { await serverTask; } catch { }
+ server.Dispose();
+ }
+
+ [Fact]
+ public async Task Handshake_with_wrong_secret_is_rejected()
+ {
+ if (IsAdministrator()) return;
+
+ using var identity = WindowsIdentity.GetCurrent();
+ var sid = identity.User!;
+ var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}";
+ Logger log = new LoggerConfiguration().CreateLogger();
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+
+ var server = new PipeServer(pipe, sid, "real-secret", log);
+ var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token));
+
+ await Should.ThrowAsync(async () =>
+ {
+ var (s, r, w) = await ConnectAndHelloAsync(pipe, "wrong-secret", cts.Token);
+ s.Dispose();
+ r.Dispose();
+ w.Dispose();
+ });
+
+ cts.Cancel();
+ try { await serverTask; } catch { }
+ server.Dispose();
+ }
+
+ [Fact]
+ public async Task Stub_handler_returns_not_implemented_for_data_plane_request()
+ {
+ if (IsAdministrator()) return;
+
+ using var identity = WindowsIdentity.GetCurrent();
+ var sid = identity.User!;
+ var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}";
+ const string secret = "stub-test";
+ Logger log = new LoggerConfiguration().CreateLogger();
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+
+ var server = new PipeServer(pipe, sid, secret, log);
+ var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token));
+
+ var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token);
+ using (stream)
+ using (reader)
+ using (writer)
+ {
+ await writer.WriteAsync(FocasMessageKind.ReadRequest,
+ new ReadRequest
+ {
+ SessionId = 1,
+ Address = new FocasAddressDto { Kind = 0, PmcLetter = "R", Number = 100 },
+ DataType = FocasDataTypeCode.Int32,
+ },
+ cts.Token);
+
+ var resp = await reader.ReadFrameAsync(cts.Token);
+ resp.HasValue.ShouldBeTrue();
+ resp!.Value.Kind.ShouldBe(FocasMessageKind.ErrorResponse);
+ var err = MessagePackSerializer.Deserialize(resp.Value.Body);
+ err.Code.ShouldBe("not-implemented");
+ err.Message.ShouldContain("PR C");
+ }
+
+ cts.Cancel();
+ try { await serverTask; } catch { }
+ server.Dispose();
+ }
+ }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests.csproj
new file mode 100644
index 0000000..74b5ccf
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net48
+ x86
+ true
+ enable
+ latest
+ false
+ true
+ ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+