Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
# paid exactly once across the whole workflow (in job 2 below).
# ------------------------------------------------------------------
build-and-test:
name: build-and-test-${{ matrix.os }}
name: build-and-test
# Skip drafts: run only on push-to-master + ready (non-draft) PRs.
# The pull_request `types` list above includes `ready_for_review`
# so CI fires the moment a draft is flipped to ready.
Expand Down Expand Up @@ -323,7 +323,7 @@ jobs:
# categories run exactly once (in job 1) across the workflow.
# ------------------------------------------------------------------
route-check-e2e:
name: route-check-e2e-shard${{ matrix.shard }}of${{ matrix.shardTotal }}
name: route-check-e2e
if: github.event_name == 'push' || github.event.pull_request.draft == false
needs: docs-prepare
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions adapter/MTConnect.NET-Applications-Adapter/Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using MTConnect.Services;
using NLog;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif

namespace MTConnect.Applications
{
Expand Down
2 changes: 2 additions & 0 deletions agent/MTConnect.NET-Applications-Agents/Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using MTConnect.Services;
using NLog;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif

namespace MTConnect.Applications
{
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Configuration for an MTConnect Agent
| `changeToken` | `ChangeToken` | `string` | An opaque token regenerated each time the configuration is saved, allowing consumers to detect that the configuration has changed. |
| `convertUnits` | `ConvertUnits` | `bool` | Gets or Sets the default for Converting Units when adding Observations |
| `defaultVersion` | `DefaultVersionValue` | `string` | The string form of <see cref="DefaultVersion"/> used for serialization; assigning a parseable version string updates <see cref="DefaultVersion"/>. |
| `deviceValidationLevel` | `DeviceValidationLevel` | `DeviceValidationLevel` | Gets or Sets the default Device (MTConnectDevices) validation level. 0 = Ignore, 1 = Warning, 2 = Remove, 3 = Strict |
| `enableAgentDevice` | `EnableAgentDevice` | `bool` | Gets or Sets whether the Agent Device is output |
| `enableMetrics` | `EnableMetrics` | `bool` | Gets or Sets whether Metrics are captured (ex. ObserationUpdateRate, AssetUpdateRate) |
| `enableValidation` | `EnableValidation` | `bool` | Gets or Sets whether validation information is output |
Expand Down Expand Up @@ -194,6 +195,7 @@ Configuration for an MTConnect Agent
| `changeToken` | `ChangeToken` | `string` | An opaque token that changes whenever the underlying configuration source is reloaded, allowing consumers to detect that the configuration has been replaced. |
| `convertUnits` | `ConvertUnits` | `bool` | Gets the default for Converting Units when adding Observations |
| `defaultVersion` | `DefaultVersion` | `Version` | Gets the default MTConnect version to output response documents for. |
| `deviceValidationLevel` | `DeviceValidationLevel` | `DeviceValidationLevel` | Gets or Sets the default Device (MTConnectDevices) validation level. 0 = Ignore, 1 = Warning, 2 = Remove, 3 = Strict |
| `enableAgentDevice` | `EnableAgentDevice` | `bool` | Gets or Sets whether the Agent Device is output |
| `enableMetrics` | `EnableMetrics` | `bool` | Gets whether Metrics are captured (ex. ObserationUpdateRate, AssetUpdateRate) |
| `enableValidation` | `EnableValidation` | `bool` | Gets or Sets whether validation information is output |
Expand Down
32 changes: 32 additions & 0 deletions libraries/MTConnect.NET-Common/Agents/DeviceValidationLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.

namespace MTConnect.Agents
{
/// <summary>
/// Controls how the Agent reacts when Devices data fails validation against
/// the MTConnect Standard.
/// </summary>
public enum DeviceValidationLevel
{
/// <summary>
/// Accept invalid device information; perform no validation action.
/// </summary>
Ignore,

/// <summary>
/// Accept invalid device information but emit a validation warning.
/// </summary>
Warning,

/// <summary>
/// Drop the invalid device information and continue processing the remainder.
/// </summary>
Remove,

/// <summary>
/// Reject the entire device information on the first validation failure.
/// </summary>
Strict
}
}
64 changes: 55 additions & 9 deletions libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1312,15 +1312,15 @@ private Device NormalizeDevice(IDevice device)
foreach (var genericComponent in genericComponents)
{
var validationResults = new ValidationResult(false, $"Invalid Component : \"{genericComponent.Type}\" Not Found");
if (_configuration.InputValidationLevel > InputValidationLevel.Ignore)
if (_configuration.DeviceValidationLevel > DeviceValidationLevel.Ignore)
{
MulticastIsolation.Raise(InvalidComponentAdded, h => h(obj.Uuid, genericComponent, validationResults));

// Remove Component from Device
if (_configuration.InputValidationLevel == InputValidationLevel.Remove) obj.RemoveComponent(genericComponent.Id);
if (_configuration.DeviceValidationLevel == DeviceValidationLevel.Remove) obj.RemoveComponent(genericComponent.Id);

// Invalidate entire Device
if (_configuration.InputValidationLevel == InputValidationLevel.Strict) return null;
if (_configuration.DeviceValidationLevel == DeviceValidationLevel.Strict) return null;
}
}
}
Expand All @@ -1332,15 +1332,15 @@ private Device NormalizeDevice(IDevice device)
foreach (var genericComposition in genericCompositions)
{
var validationResults = new ValidationResult(false, $"Invalid Composition : \"{genericComposition.Type}\" Not Found");
if (_configuration.InputValidationLevel > InputValidationLevel.Ignore)
if (_configuration.DeviceValidationLevel > DeviceValidationLevel.Ignore)
{
MulticastIsolation.Raise(InvalidCompositionAdded, h => h(obj.Uuid, genericComposition, validationResults));

// Remove Compsition from Device
if (_configuration.InputValidationLevel == InputValidationLevel.Remove) obj.RemoveComposition(genericComposition.Id);
if (_configuration.DeviceValidationLevel == DeviceValidationLevel.Remove) obj.RemoveComposition(genericComposition.Id);

// Invalidate entire Device
if (_configuration.InputValidationLevel == InputValidationLevel.Strict) return null;
if (_configuration.DeviceValidationLevel == DeviceValidationLevel.Strict) return null;
}
}
}
Expand All @@ -1352,15 +1352,15 @@ private Device NormalizeDevice(IDevice device)
foreach (var genericDataItem in genericDataItems)
{
var validationResults = new ValidationResult(false, $"Invalid DataItem : \"{genericDataItem.Type}\" Not Found");
if (_configuration.InputValidationLevel > InputValidationLevel.Ignore)
if (_configuration.DeviceValidationLevel > DeviceValidationLevel.Ignore)
{
MulticastIsolation.Raise(InvalidDataItemAdded, h => h(obj.Uuid, genericDataItem, validationResults));

// Remove DataItem from Device
if (_configuration.InputValidationLevel == InputValidationLevel.Remove) obj.RemoveDataItem(genericDataItem.Id);
if (_configuration.DeviceValidationLevel == DeviceValidationLevel.Remove) obj.RemoveDataItem(genericDataItem.Id);

// Invalidate entire Device
if (_configuration.InputValidationLevel == InputValidationLevel.Strict) return null;
if (_configuration.DeviceValidationLevel == DeviceValidationLevel.Strict) return null;
}
}
}
Expand Down Expand Up @@ -2152,6 +2152,17 @@ public bool AddObservation(string deviceKey, IObservationInput observationInput,
var dataItem = GetDataItem(deviceUuid, input.DataItemKey);
if (dataItem != null)
{
// Coerce a null, empty, or whitespace-only Result to UNAVAILABLE before the
// observation reaches the buffer. The MTConnect Part 1 Observation Information
// Model mandates "UNAVAILABLE" as the sole valid representation of a missing
// value; an empty string is never a Valid Data Value. The coerce runs above
// validation so Strict no longer silently drops empty-Result observations and
// every other validation level publishes the spec-compliant sentinel.
if (dataItem.Category != DataItemCategory.CONDITION && IsEmptyResult(input))
{
CoerceEmptyResultToUnavailable(input);
}

// Add required properties
switch (dataItem.Representation)
{
Expand Down Expand Up @@ -2277,6 +2288,41 @@ public bool AddObservation(string deviceKey, IObservationInput observationInput,
}


/// <summary>
/// Returns true when the observation's Result value is null, the empty string, or whitespace-only.
/// </summary>
/// <remarks>
/// An empty Result is, by definition, not a Valid Data Value under any typed DataItem schema; the
/// MTConnect Part 1 Observation Information Model mandates "UNAVAILABLE" as the sole valid
/// representation of a missing value. This predicate identifies the inputs the SDK coerces.
/// </remarks>
private static bool IsEmptyResult(IObservationInput input)
{
var result = input.GetValue(ValueKeys.Result);
if (result == null) return true;
return string.IsNullOrWhiteSpace(result);
}

/// <summary>
/// Rewrites the observation's Result value to <see cref="Observation.Unavailable"/> and flags the input as unavailable.
/// </summary>
/// <remarks>
/// Removes any prior Result entry (so the Values collection does not carry a duplicate ValueKey),
/// adds the UNAVAILABLE sentinel, and sets <see cref="IObservationInput.IsUnavailable"/> so downstream
/// consumers that branch on the flag observe the coerced state. Spec authority: MTConnect Part 1
/// Observation Information Model - Representation - Observation Values.
/// </remarks>
private static void CoerceEmptyResultToUnavailable(IObservationInput input)
{
var preserved = (input.Values ?? Enumerable.Empty<ObservationValue>())
.Where(v => v.Key != ValueKeys.Result)
.ToList();
input.Values = preserved;
input.AddValue(ValueKeys.Result, Observation.Unavailable);
input.IsUnavailable = true;
}


/// <summary>
/// Add new Observations for DataItems to the Agent
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ public string DefaultVersionValue
[JsonPropertyName("enableValidation")]
public bool EnableValidation { get; set; }

/// <summary>
/// Gets or Sets the default Device (MTConnectDevices) validation level. 0 = Ignore, 1 = Warning, 2 = Remove, 3 = Strict
/// </summary>
[JsonPropertyName("deviceValidationLevel")]
public DeviceValidationLevel DeviceValidationLevel { get; set; }

/// <summary>
/// Gets or Sets the default Input (Observation or Asset) validation level. 0 = Ignore, 1 = Warning, 2 = Remove, 3 = Strict
/// </summary>
Expand Down Expand Up @@ -155,6 +161,7 @@ public AgentConfiguration()
ObservationBufferSize = 131072;
AssetBufferSize = 1024;
DefaultVersion = MTConnectVersions.Max;
DeviceValidationLevel = DeviceValidationLevel.Warning;
InputValidationLevel = InputValidationLevel.Warning;
ConvertUnits = true;
IgnoreObservationCase = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public interface IAgentConfiguration
/// </summary>
bool EnableValidation { get; }

/// <summary>
/// Gets or Sets the default Device (MTConnectDevices) validation level. 0 = Ignore, 1 = Warning, 2 = Remove, 3 = Strict
/// </summary>
DeviceValidationLevel DeviceValidationLevel { get; }

/// <summary>
/// Gets the default Input (Observation or Asset) validation level. 0 = Ignore, 1 = Warning, 2 = Remove, 3 = Strict
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions libraries/MTConnect.NET-SHDR/Shdr/ShdrDataItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,13 @@ private static IEnumerable<ShdrDataItem> FromKeyValuePairs(string input, long ti


dataItem.AddValue(new ObservationValue(ValueKeys.Result, valueString != null ? valueString.ToString().Trim() : string.Empty));
dataItems.Add(dataItem);
}
else
{
// Assume Empty Value

dataItem.AddValue(new ObservationValue(ValueKeys.Result, string.Empty));
dataItems.Add(dataItem);
}
}
Expand Down
2 changes: 2 additions & 0 deletions libraries/MTConnect.NET-Services/MTConnectAdapterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using System.Diagnostics;
using System.IO;
using System.Reflection;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif
using System.ServiceProcess;

namespace MTConnect.Services
Expand Down
2 changes: 2 additions & 0 deletions libraries/MTConnect.NET-Services/MTConnectAgentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using System.Diagnostics;
using System.IO;
using System.Reflection;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif
using System.ServiceProcess;

namespace MTConnect.Services
Expand Down
2 changes: 2 additions & 0 deletions libraries/MTConnect.NET-Services/WindowsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using System.Linq;
using System.Runtime.InteropServices;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif
using System.Security.Principal;
using System.ServiceProcess;

Expand Down
13 changes: 12 additions & 1 deletion libraries/MTConnect.NET-XML/MTConnect.NET-XML.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@
<PropertyGroup>
<RootNamespace>MTConnect</RootNamespace>
<Configurations>Debug;Release;Package</Configurations>


<!--
XsdPreprocessor.StripXsd11Constructs uses a C# 8.0 `using var`
declaration (`using var reader = new StringReader(...)`). The
Roslyn default LangVersion on netstandard2.0 / net4x falls
back to 7.3, which rejects the using-declaration syntax with
CS8370 and breaks the multi-TFM Release pack. Pin LangVersion
to 8.0 — the lowest version that accepts the syntax — so the
Release pack survives every declared TFM.
-->
<LangVersion>8.0</LangVersion>

<Description>MTConnect.NET-XML implements the XML Document Format for use with the MTConnect.NET library. Supports MTConnect versions up to 2.7. Supports .NET Framework 4.6.1 up to .NET 10</Description>
<PackageReadmeFile>README-Nuget.md</PackageReadmeFile>

Expand Down
Loading
Loading