/* ========================================================================
* 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.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.MachineTool.WMTP;
namespace MachineToolSample.Server
{
///
/// An explicitly selected simulation adapter for one peripheral, never a physical device driver.
///
internal sealed class DemoPeripheral :
IWmtpDeviceProvider, IWmtpMeasurementProvider, IWmtpCalibrationProvider,
IWmtpDeviceTimeProvider, IWmtpTransferProvider
{
///
/// Creates independent simulated acquisition and request state.
///
public DemoPeripheral(ServerOptions options, TimeProvider clock)
{
m_options = options;
m_clock = clock;
m_initialTime = clock.GetUtcNow();
foreach (WmtpMeasurementKind kind in MeasurementKinds)
{
m_settings.Add(kind.ToString(), (1000, Variant.From(0d)));
}
}
///
/// Gets the eight released measurement kinds without reflection-based enumeration.
///
internal static ArrayOf MeasurementKinds { get; } =
[
WmtpMeasurementKind.Temperature, WmtpMeasurementKind.RotationSpeed,
WmtpMeasurementKind.Humidity, WmtpMeasurementKind.Acceleration,
WmtpMeasurementKind.Force, WmtpMeasurementKind.Torque,
WmtpMeasurementKind.Position, WmtpMeasurementKind.Dimension
];
///
public ValueTask ReadAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
lock (m_gate)
{
DateTimeUtc time = m_clock.GetUtcNow() + m_deviceOffset;
var life = new WmtpLifetime
{
Start = Variant.From(100d),
Remaining = Variant.From(80d),
Limit = Variant.From(0d),
EngineeringUnits = Unit(4740434, "h")
};
return new ValueTask(new WmtpDeviceSnapshot
{
Observation = new DataValue(Variant.From(1u), StatusCodes.Good, time),
CalibrationMode = m_calibrating,
Battery = new DataValue(Variant.From(75d), StatusCodes.Good, time),
LicenseInformation = "Simulation entitlement; transferred bytes do not activate a license.",
LicenseValidity = life,
LastService = m_initialTime,
NextServiceDate = m_clock.GetUtcNow().AddDays(30),
LifeCycleStatus = 0,
RemainingLifetime = life,
RemainingCycles = life with { EngineeringUnits = Unit(4404786, "cycle") },
NextService = life,
OperationHours = 12,
OperationSinceService = 60000
});
}
}
///
public ValueTask ReadAsync(
string sensor, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
lock (m_gate)
{
if (!m_settings.ContainsKey(sensor))
{
throw new ServiceResultException(StatusCodes.BadNodeIdUnknown, "Unknown simulated sensor.");
}
return new ValueTask(new WmtpMeasurementSample
{
Value = new DataValue(Variant.From(21.5), StatusCodes.Good,
m_options.FreezeMeasurements ? m_initialTime : m_clock.GetUtcNow() + m_deviceOffset),
Index = 1,
MeasurementPeriod = 50
});
}
}
///
public ValueTask ConfigureAsync(
string sensor, uint triggerMilliseconds, Variant delta, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
lock (m_gate)
{
if (!m_settings.ContainsKey(sensor))
{
return new ValueTask(StatusCodes.BadNodeIdUnknown);
}
StatusCode accepted = Accept(WmtpOperation.ConfigureMeasurement, cancellationToken);
if (StatusCode.IsGood(accepted))
{
m_settings[sensor] = (triggerMilliseconds, delta.Copy());
}
return new ValueTask(accepted);
}
}
///
public ValueTask SetCalibrationAsync(bool enabled, CancellationToken cancellationToken = default)
{
lock (m_gate)
{
StatusCode accepted = Accept(WmtpOperation.Calibrate, cancellationToken);
if (StatusCode.IsGood(accepted))
{
m_calibrating = enabled;
}
return new ValueTask(accepted);
}
}
///
public ValueTask SetTimeAsync(
DateTimeUtc utcTime, TimeZoneDataType timeZone, CancellationToken cancellationToken = default)
{
lock (m_gate)
{
StatusCode accepted = Accept(WmtpOperation.SetTime, cancellationToken);
if (StatusCode.IsGood(accepted))
{
m_deviceOffset = utcTime - new DateTimeUtc(m_clock.GetUtcNow());
Console.WriteLine(FormattableString.Invariant(
$"SIMULATED_TIME_ACCEPTED utc={utcTime} offsetMinutes={timeZone.Offset}"));
}
return new ValueTask(accepted);
}
}
///
public ValueTask TransferAsync(
WmtpTransferKind kind, ByteString payload, CancellationToken cancellationToken = default)
{
lock (m_gate)
{
StatusCode accepted = payload.Length is < 1 or > 1024
? StatusCodes.BadEncodingLimitsExceeded
: Accept(kind == WmtpTransferKind.Firmware
? WmtpOperation.TransferFirmware : WmtpOperation.TransferLicense, cancellationToken);
if (StatusCode.IsGood(accepted))
{
Console.WriteLine(FormattableString.Invariant(
$"SIMULATED_TRANSFER_ACCEPTED kind={kind} size={payload.Length}; no installation/activation."));
}
return new ValueTask(accepted);
}
}
///
/// Uses the released measurement kinds and explicit UN/CEFACT units.
///
internal static WmtpMeasurementDefinition Definition(WmtpMeasurementKind kind, bool advanced)
{
(int id, string symbol) = kind switch
{
WmtpMeasurementKind.Temperature => (4408652, "degC"),
WmtpMeasurementKind.RotationSpeed => (5394509, "rpm"),
WmtpMeasurementKind.Humidity => (20529, "%"),
WmtpMeasurementKind.Acceleration => (5067595, "m/s2"),
WmtpMeasurementKind.Force => (5129559, "N"),
WmtpMeasurementKind.Torque => (20053, "N.m"),
WmtpMeasurementKind.Position => (5067858, "m"),
_ => (5066068, "mm")
};
return new WmtpMeasurementDefinition
{
Name = kind.ToString(),
Kind = kind,
EngineeringUnits = Unit(id, symbol),
Definition = "Application-owned simulation; not a hardware reading.",
Features = advanced ? WmtpMeasurementFeatures.All : WmtpMeasurementFeatures.None,
EURange = new Opc.Ua.Range { Low = 0, High = 100 },
InstrumentRange = new Opc.Ua.Range { Low = -100, High = 200 },
ValuePrecision = 0.125,
InternalUpdateInterval = 25,
RelativeUncertainty = 0.5,
AbsoluteUncertainty = 0.25,
High = 80,
HighHigh = 90,
StaleAfter = TimeSpan.FromSeconds(2)
};
}
private StatusCode Accept(WmtpOperation operation, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (!m_options.AllowCommands)
{
return StatusCodes.BadUserAccessDenied;
}
return m_options.RefuseCommands || !m_accepted.Add(operation)
? StatusCodes.BadResourceUnavailable : StatusCodes.Good;
}
private static EUInformation Unit(int id, string symbol) => new()
{
NamespaceUri = "http://www.opcfoundation.org/UA/units/un/cefact",
UnitId = id,
DisplayName = new LocalizedText(symbol)
};
private readonly ServerOptions m_options;
private readonly TimeProvider m_clock;
private readonly DateTimeUtc m_initialTime;
private readonly Lock m_gate = new();
private readonly HashSet m_accepted = [];
private readonly Dictionary m_settings = new(StringComparer.Ordinal);
private bool m_calibrating;
private TimeSpan m_deviceOffset;
}
///
/// Makes simulated writes opt-in and never authorizes aborting a different session's work.
///
internal sealed class DemoAuthorization(ServerOptions options) : IWmtpAuthorization
{
///
public ValueTask AuthorizeAsync(
WmtpRequest request, WmtpOperation operation, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
bool sameOwner = request.ActiveSessionId.IsNull || request.ActiveSessionId == request.SessionId;
bool allowed = operation == WmtpOperation.ReadRecords ||
(options.AllowCommands && (operation != WmtpOperation.Abort || sameOwner));
return new ValueTask(allowed ? StatusCodes.Good : StatusCodes.BadUserAccessDenied);
}
}
}