/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* 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.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.IO;
using Opc.Ua;
using Opc.Ua.MachineTool;
namespace MachineToolSample.Server
{
///
/// Selects the model closure, independent instances and explicit simulation permissions.
///
internal sealed record ServerOptions
{
///
/// Gets the selected namespace closure, without forcing a CNC into WMTP-only hosting.
///
public MachineToolParts Parts { get; private init; } = MachineToolParts.MachineTool;
///
/// Gets the loopback port; zero selects a currently available port.
///
public int Port { get; private init; } = 62558;
///
/// Gets the acquisition window after actual OPC UA startup.
///
public int Seconds { get; private init; } = 60;
///
/// Gets the independent instance count per selected family.
///
public int Instances { get; private init; } = 1;
///
/// Gets whether optional monitoring, jobs/files and peripheral features are selected.
///
public bool Advanced { get; private init; }
///
/// Gets explicit server consent to accept simulated state-changing requests from local anonymous sessions.
///
public bool AllowCommands { get; private init; }
///
/// Gets whether simulated peripheral providers refuse every state-changing request.
///
public bool RefuseCommands { get; private init; }
///
/// Gets the explicitly selected machine on which an operator simulation may execute the declared job.
///
public string? SimulateJob { get; private init; }
///
/// Gets whether sensors retain their first acquisition so the real age policy becomes observable.
///
public bool FreezeMeasurements { get; private init; }
///
/// Gets the optional whole-application cancellation timer, including startup.
///
public int? CancelAfterMilliseconds { get; private init; }
///
/// Gets the explicit loopback-only certificate trust exception.
///
public bool DemoTrust { get; private init; }
///
/// Gets the application-owned certificate-store root.
///
public string PkiRoot { get; private init; } = Path.Combine("artifacts", "machinetool-pki", "server");
///
/// Validates all selections before opening a host, listener or certificate store.
///
internal static ServerOptions Parse(ArrayOf arguments)
{
var options = new ServerOptions();
for (int i = 0; i < arguments.Count; i++)
{
options = arguments[i] switch
{
"--mode" => options with { Parts = ReadParts(arguments, ref i) },
"--port" => options with { Port = SampleArguments.ReadInteger(arguments, ref i, 0, 65535) },
"--seconds" => options with { Seconds = SampleArguments.ReadInteger(arguments, ref i, 1, 600) },
"--instances" => options with { Instances = SampleArguments.ReadInteger(arguments, ref i, 1, 4) },
"--pki" => options with { PkiRoot = SampleArguments.ReadValue(arguments, ref i) },
"--advanced" => options with { Advanced = true },
"--allow-commands" => options with { AllowCommands = true },
"--refuse-commands" => options with { RefuseCommands = true },
"--simulate-job" => options with { SimulateJob = SampleArguments.ReadValue(arguments, ref i) },
"--freeze-measurements" => options with { FreezeMeasurements = true },
"--demo-trust" => options with { DemoTrust = true },
"--cancel-after-ms" => options with
{
CancelAfterMilliseconds = SampleArguments.ReadInteger(arguments, ref i, 1, 60000)
},
_ => throw new ArgumentException($"Unknown option '{arguments[i]}'.", nameof(arguments))
};
}
if (options.AllowCommands && (!options.Advanced || !options.DemoTrust))
{
throw new ArgumentException(
"--allow-commands requires --advanced and --demo-trust.", nameof(arguments));
}
if (options.RefuseCommands && !options.AllowCommands)
{
throw new ArgumentException("--refuse-commands requires --allow-commands.", nameof(arguments));
}
if (options.FreezeMeasurements &&
(!options.Advanced || (options.Parts & MachineToolParts.Wmtp) == 0))
{
throw new ArgumentException("--freeze-measurements requires advanced WMTP.", nameof(arguments));
}
if (options.SimulateJob != null)
{
bool selected = false;
for (int i = 1; i <= options.Instances; i++)
{
selected |= options.SimulateJob == FormattableString.Invariant($"Machine{i}");
}
if (!options.AllowCommands || (options.Parts & MachineToolParts.MachineTool) == 0 || !selected)
{
throw new ArgumentException(
"--simulate-job requires --allow-commands and an existing Machine1..Machine4 target.",
nameof(arguments));
}
}
return options;
}
///
/// Describes the selected simulation and its bounds.
///
internal static void PrintHelp()
{
Console.WriteLine("""
MachineToolServer [--mode machine|wmtp|mixed] [--advanced] [--instances 1..4]
[--port 0..65535] [--seconds 1..600] [--pki directory]
[--demo-trust] [--allow-commands] [--refuse-commands]
[--simulate-job Machine1] [--freeze-measurements]
[--cancel-after-ms 1..60000]
Defaults: mandatory-only MachineTool, one instance, port 62558, 60 seconds.
Binds 127.0.0.1 only; READY follows actual OPC UA startup, not Generic Host startup.
--advanced selects monitoring/jobs/files and all eight WMTP sensor kinds/records.
--allow-commands requires --advanced --demo-trust; authorization otherwise denies changes.
--simulate-job explicitly models one operator's execution/part observations, never hardware.
Each peripheral accepts one configuration/calibration/time/transfer of each kind;
later requests of the same kind are refused.
--demo-trust accepts unknown certificates ONLY FOR LOCAL SIMULATION; encryption stays required.
File contents are application-owned, read-only, bounded and not executable CNC programs.
Ctrl+C or the optional cancellation timer joins acquisition and disposes resources.
PKI is retained for certificate administration; never share it between the applications.
""");
}
///
/// Maps the explicit hosting mode to the existing model selection.
///
private static MachineToolParts ReadParts(ArrayOf arguments, ref int index)
{
return SampleArguments.ReadValue(arguments, ref index) switch
{
"machine" => MachineToolParts.MachineTool,
"wmtp" => MachineToolParts.Wmtp,
"mixed" => MachineToolParts.All,
_ => throw new ArgumentException("--mode must be machine, wmtp or mixed.", nameof(arguments))
};
}
}
}